mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-07 08:46:40 +08:00
refactor(dashboard): remove unused color management logic
This commit is contained in:
@@ -9,7 +9,6 @@ type UnknownRecord = Record<string, any>
|
|||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
colors?: Record<string, string>
|
|
||||||
enabled: Record<string, boolean>
|
enabled: Record<string, boolean>
|
||||||
elevated?: boolean
|
elevated?: boolean
|
||||||
hint: string
|
hint: string
|
||||||
@@ -24,7 +23,6 @@ const props = withDefaults(
|
|||||||
valueGetter?: (item: UnknownRecord) => string
|
valueGetter?: (item: UnknownRecord) => string
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
colors: () => ({}),
|
|
||||||
elevated: false,
|
elevated: false,
|
||||||
labelGetter: undefined,
|
labelGetter: undefined,
|
||||||
modelValue: true,
|
modelValue: true,
|
||||||
@@ -129,12 +127,13 @@ function submitSettings() {
|
|||||||
<VCardText>
|
<VCardText>
|
||||||
<p class="settings-hint">{{ props.hint }}</p>
|
<p class="settings-hint">{{ props.hint }}</p>
|
||||||
<div class="settings-grid">
|
<div class="settings-grid">
|
||||||
<div
|
<button
|
||||||
v-for="item in props.items"
|
v-for="item in props.items"
|
||||||
:key="getItemValue(item)"
|
:key="getItemValue(item)"
|
||||||
|
type="button"
|
||||||
class="setting-item"
|
class="setting-item"
|
||||||
:class="{ 'enabled': localEnabled[getItemValue(item)] }"
|
:class="{ 'enabled': localEnabled[getItemValue(item)] }"
|
||||||
:style="{ '--item-color': props.colors[getItemValue(item)] }"
|
:aria-pressed="Boolean(localEnabled[getItemValue(item)])"
|
||||||
@click="toggleItem(item)"
|
@click="toggleItem(item)"
|
||||||
>
|
>
|
||||||
<div class="setting-item-inner">
|
<div class="setting-item-inner">
|
||||||
@@ -147,7 +146,7 @@ function submitSettings() {
|
|||||||
</div>
|
</div>
|
||||||
<span class="setting-label">{{ getItemLabel(item) }}</span>
|
<span class="setting-label">{{ getItemLabel(item) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="props.switchLabel" class="mt-3">
|
<p v-if="props.switchLabel" class="mt-3">
|
||||||
<VSwitch v-model="elevatedValue" :label="props.switchLabel" />
|
<VSwitch v-model="elevatedValue" :label="props.switchLabel" />
|
||||||
@@ -186,43 +185,48 @@ function submitSettings() {
|
|||||||
|
|
||||||
.settings-grid {
|
.settings-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 12px;
|
gap: 10px;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
.setting-label {
|
.setting-label {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
color: rgba(var(--v-theme-on-surface), 0.8);
|
color: rgba(var(--v-theme-on-surface), 0.72);
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
font-weight: 500;
|
font-weight: 550;
|
||||||
line-height: 1.2;
|
line-height: 1.35;
|
||||||
|
text-align: start;
|
||||||
transition: color 0.2s ease;
|
transition: color 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.setting-item {
|
.setting-item {
|
||||||
|
appearance: none;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-radius: var(--app-surface-radius);
|
min-block-size: 48px;
|
||||||
background-color: rgba(var(--v-theme-surface-variant), 0.3);
|
border: 1px solid rgba(var(--v-theme-on-surface), 0.1);
|
||||||
|
border-radius: 10px;
|
||||||
|
background-color: rgba(var(--v-theme-on-surface), 0.04);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
font: inherit;
|
||||||
padding-block: 10px;
|
padding-block: 10px;
|
||||||
padding-inline: 12px;
|
padding-inline: 12px;
|
||||||
transition: all 0.2s ease;
|
transition: border-color 0.2s ease, background-color 0.2s ease, transform 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.setting-item::before {
|
.setting-item::before {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background: linear-gradient(90deg, var(--item-color, rgb(var(--v-theme-primary))) 0%, transparent 100%);
|
background: rgb(var(--v-theme-primary));
|
||||||
content: '';
|
content: '';
|
||||||
inline-size: 3px;
|
inline-size: 3px;
|
||||||
inset-block: 0;
|
inset-block: 0;
|
||||||
inset-inline-start: 0;
|
inset-inline-start: 0;
|
||||||
opacity: 0.3;
|
opacity: 0;
|
||||||
transition: opacity 0.2s ease;
|
transition: opacity 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.setting-item.enabled {
|
.setting-item.enabled {
|
||||||
border-color: rgba(var(--v-theme-primary), 0.4);
|
border-color: rgba(var(--v-theme-primary), 0.3);
|
||||||
background-color: rgba(var(--v-theme-primary), 0.08);
|
background-color: rgba(var(--v-theme-primary), 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,6 +234,24 @@ function submitSettings() {
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.setting-item:hover {
|
||||||
|
border-color: rgba(var(--v-theme-primary), 0.32);
|
||||||
|
background-color: rgba(var(--v-theme-primary), 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-item:active {
|
||||||
|
transform: scale(0.99);
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-item:focus-visible {
|
||||||
|
outline: 3px solid rgba(var(--v-theme-primary), 0.28);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-item.enabled .setting-label {
|
||||||
|
color: rgb(var(--v-theme-on-surface));
|
||||||
|
}
|
||||||
|
|
||||||
.setting-item-inner {
|
.setting-item-inner {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -240,4 +262,11 @@ function submitSettings() {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (width <= 760px) {
|
||||||
|
.settings-grid {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { useUserStore, useGlobalSettingsStore } from '@/stores'
|
|||||||
import SearchSiteDialog from '@/components/dialog/SearchSiteDialog.vue'
|
import SearchSiteDialog from '@/components/dialog/SearchSiteDialog.vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useDisplay } from 'vuetify'
|
import { useDisplay } from 'vuetify'
|
||||||
|
import { VDialog, VMenu } from 'vuetify/components'
|
||||||
import { buildUserPermissionContext, hasPermission, filterMenusByPermission } from '@/utils/permission'
|
import { buildUserPermissionContext, hasPermission, filterMenusByPermission } from '@/utils/permission'
|
||||||
|
|
||||||
// 显示器宽度
|
// 显示器宽度
|
||||||
@@ -15,10 +16,16 @@ const display = useDisplay()
|
|||||||
// 多语言支持
|
// 多语言支持
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
// 定义props,接收modelValue
|
// 定义 props,接收浮层状态及是否显示响应式入口。
|
||||||
const props = defineProps<{
|
const props = withDefaults(
|
||||||
modelValue: boolean
|
defineProps<{
|
||||||
}>()
|
modelValue: boolean
|
||||||
|
showActivator?: boolean
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
showActivator: false,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
// 路由
|
// 路由
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -78,16 +85,33 @@ const dialog = computed({
|
|||||||
set: val => emit('update:modelValue', val),
|
set: val => emit('update:modelValue', val),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 桌面使用锚定下拉,小屏继续使用原有搜索弹窗。
|
||||||
|
const searchOverlay = computed(() => (display.mdAndUp.value ? VMenu : VDialog))
|
||||||
|
const searchOverlayProps = computed(() =>
|
||||||
|
display.mdAndUp.value
|
||||||
|
? {
|
||||||
|
closeOnContentClick: false,
|
||||||
|
location: 'bottom start' as const,
|
||||||
|
offset: 8,
|
||||||
|
scrollStrategy: 'reposition' as const,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
fullscreen: true,
|
||||||
|
maxWidth: '40rem',
|
||||||
|
scrollable: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
// 搜索词
|
// 搜索词
|
||||||
const searchWord = ref<string | null>(null)
|
const searchWord = ref<string | null>(null)
|
||||||
|
|
||||||
// ref
|
// 当前尺寸下可见的搜索输入框。
|
||||||
const searchWordInput = ref<HTMLElement | null>(null)
|
const searchWordInput = ref<HTMLInputElement | null>(null)
|
||||||
|
|
||||||
// 近期搜索词条
|
// 近期搜索词条
|
||||||
const recentSearches = ref<string[]>([])
|
const recentSearches = ref<string[]>([])
|
||||||
|
|
||||||
// 检测操作系统是否是Mac
|
/** 检测操作系统是否为 macOS。 */
|
||||||
function isMac() {
|
function isMac() {
|
||||||
return navigator.platform.toUpperCase().indexOf('MAC') >= 0
|
return navigator.platform.toUpperCase().indexOf('MAC') >= 0
|
||||||
}
|
}
|
||||||
@@ -95,7 +119,7 @@ function isMac() {
|
|||||||
// 计算属性:根据操作系统显示不同的按键提示
|
// 计算属性:根据操作系统显示不同的按键提示
|
||||||
const metaKey = computed(() => (isMac() ? '⌘+K' : 'Ctrl+K'))
|
const metaKey = computed(() => (isMac() ? '⌘+K' : 'Ctrl+K'))
|
||||||
|
|
||||||
// 保存近期搜索到本地
|
/** 将有效关键词保存到近期搜索记录。 */
|
||||||
function saveRecentSearches(keyword: string) {
|
function saveRecentSearches(keyword: string) {
|
||||||
if (!keyword) return
|
if (!keyword) return
|
||||||
if (recentSearches.value.includes(keyword)) return
|
if (recentSearches.value.includes(keyword)) return
|
||||||
@@ -103,7 +127,7 @@ function saveRecentSearches(keyword: string) {
|
|||||||
localStorage.setItem('MP_RecentSearches', JSON.stringify(recentSearches.value))
|
localStorage.setItem('MP_RecentSearches', JSON.stringify(recentSearches.value))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从本地加载近期搜索
|
/** 从本地存储加载并裁剪近期搜索记录。 */
|
||||||
function loadRecentSearches() {
|
function loadRecentSearches() {
|
||||||
const recentSearchesStr = localStorage.getItem('MP_RecentSearches')
|
const recentSearchesStr = localStorage.getItem('MP_RecentSearches')
|
||||||
if (recentSearchesStr) {
|
if (recentSearchesStr) {
|
||||||
@@ -115,7 +139,7 @@ function loadRecentSearches() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 所有菜单功能
|
/** 获取可参与全局搜索的导航菜单和设置入口。 */
|
||||||
function getMenus(): NavMenu[] {
|
function getMenus(): NavMenu[] {
|
||||||
let menus: NavMenu[] = []
|
let menus: NavMenu[] = []
|
||||||
// 导航菜单
|
// 导航菜单
|
||||||
@@ -170,7 +194,7 @@ const matchedMenuItems = computed(() => {
|
|||||||
// 所有插件(已安装)
|
// 所有插件(已安装)
|
||||||
const pluginItems = ref<Plugin[]>([])
|
const pluginItems = ref<Plugin[]>([])
|
||||||
|
|
||||||
// 获取插件列表数据
|
/** 加载已安装插件,供搜索结果匹配。 */
|
||||||
async function fetchInstalledPlugins() {
|
async function fetchInstalledPlugins() {
|
||||||
try {
|
try {
|
||||||
pluginItems.value = await api.get('plugin/', {
|
pluginItems.value = await api.get('plugin/', {
|
||||||
@@ -194,7 +218,7 @@ const matchedPluginItems = computed(() => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// 获取订阅列表数据
|
/** 加载订阅列表,供搜索结果匹配。 */
|
||||||
async function fetchSubscribes() {
|
async function fetchSubscribes() {
|
||||||
try {
|
try {
|
||||||
SubscribeItems.value = await api.get('subscribe/')
|
SubscribeItems.value = await api.get('subscribe/')
|
||||||
@@ -203,7 +227,7 @@ async function fetchSubscribes() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从接口加载用户站点偏好设置
|
/** 从接口加载用户的站点搜索偏好。 */
|
||||||
const loadUserSitePreferences = async () => {
|
const loadUserSitePreferences = async () => {
|
||||||
try {
|
try {
|
||||||
const result = await api.get('system/setting/public/IndexerSites')
|
const result = await api.get('system/setting/public/IndexerSites')
|
||||||
@@ -216,7 +240,7 @@ const loadUserSitePreferences = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查询所有站点
|
/** 查询所有启用站点,并初始化默认选择。 */
|
||||||
async function queryAllSites() {
|
async function queryAllSites() {
|
||||||
try {
|
try {
|
||||||
const data: Site[] = await api.get('site/')
|
const data: Site[] = await api.get('site/')
|
||||||
@@ -231,7 +255,7 @@ async function queryAllSites() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 打开站点选择对话框
|
/** 打开指定资源类型的站点选择对话框。 */
|
||||||
const openSiteDialog = (type: 'torrent' | 'subtitle' = 'torrent') => {
|
const openSiteDialog = (type: 'torrent' | 'subtitle' = 'torrent') => {
|
||||||
siteSearchType.value = type
|
siteSearchType.value = type
|
||||||
chooseSiteDialog.value = true
|
chooseSiteDialog.value = true
|
||||||
@@ -247,7 +271,7 @@ const matchedSubscribeItems = computed(() => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// 搜索多站点
|
/** 使用选中的站点执行当前资源类型搜索。 */
|
||||||
function searchSites(sites: number[]) {
|
function searchSites(sites: number[]) {
|
||||||
chooseSiteDialog.value = false
|
chooseSiteDialog.value = false
|
||||||
selectedSites.value = sites
|
selectedSites.value = sites
|
||||||
@@ -258,7 +282,7 @@ function searchSites(sites: number[]) {
|
|||||||
searchTorrent()
|
searchTorrent()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 搜索资源
|
/** 使用当前关键词搜索站点资源。 */
|
||||||
function searchTorrent() {
|
function searchTorrent() {
|
||||||
if (!searchWord.value || !hasSearchPermission.value) return
|
if (!searchWord.value || !hasSearchPermission.value) return
|
||||||
// 记录搜索词
|
// 记录搜索词
|
||||||
@@ -273,12 +297,10 @@ function searchTorrent() {
|
|||||||
sites: selectedSites.value.join(','),
|
sites: selectedSites.value.join(','),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
// 关闭搜索对话框
|
closeSearch()
|
||||||
dialog.value = false
|
|
||||||
emit('close')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 搜索字幕资源
|
/** 使用当前关键词搜索字幕资源。 */
|
||||||
function searchSubtitle() {
|
function searchSubtitle() {
|
||||||
if (!searchWord.value || !hasSearchPermission.value) return
|
if (!searchWord.value || !hasSearchPermission.value) return
|
||||||
saveRecentSearches(searchWord.value)
|
saveRecentSearches(searchWord.value)
|
||||||
@@ -291,11 +313,10 @@ function searchSubtitle() {
|
|||||||
sites: selectedSites.value.join(','),
|
sites: selectedSites.value.join(','),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
dialog.value = false
|
closeSearch()
|
||||||
emit('close')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 跳转媒体搜索页面
|
/** 跳转到指定类型的媒体搜索结果页。 */
|
||||||
function searchMedia(searchType: string) {
|
function searchMedia(searchType: string) {
|
||||||
// 搜索类型 media/person
|
// 搜索类型 media/person
|
||||||
if (!searchWord.value || !hasDiscoveryPermission.value) return
|
if (!searchWord.value || !hasDiscoveryPermission.value) return
|
||||||
@@ -307,10 +328,10 @@ function searchMedia(searchType: string) {
|
|||||||
type: searchType,
|
type: searchType,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
emit('close')
|
closeSearch()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 跳转到历史记录页面
|
/** 跳转到包含当前关键词的历史记录页。 */
|
||||||
function searchHistory() {
|
function searchHistory() {
|
||||||
if (!searchWord.value) return
|
if (!searchWord.value) return
|
||||||
saveRecentSearches(searchWord.value)
|
saveRecentSearches(searchWord.value)
|
||||||
@@ -320,10 +341,10 @@ function searchHistory() {
|
|||||||
search: searchWord.value,
|
search: searchWord.value,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
emit('close')
|
closeSearch()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 跳转到订阅分享页面
|
/** 跳转到包含当前关键词的订阅分享页。 */
|
||||||
function searchSubscribeShares() {
|
function searchSubscribeShares() {
|
||||||
if (!searchWord.value) return
|
if (!searchWord.value) return
|
||||||
saveRecentSearches(searchWord.value)
|
saveRecentSearches(searchWord.value)
|
||||||
@@ -333,10 +354,10 @@ function searchSubscribeShares() {
|
|||||||
keyword: searchWord.value,
|
keyword: searchWord.value,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
emit('close')
|
closeSearch()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 跳转插件页面
|
/** 打开匹配插件的已安装详情。 */
|
||||||
function showPlugin(pluginId: string) {
|
function showPlugin(pluginId: string) {
|
||||||
router.push({
|
router.push({
|
||||||
path: `/plugins/`,
|
path: `/plugins/`,
|
||||||
@@ -345,16 +366,16 @@ function showPlugin(pluginId: string) {
|
|||||||
id: pluginId,
|
id: pluginId,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
emit('close')
|
closeSearch()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 跳转菜单页面
|
/** 跳转到匹配的功能菜单。 */
|
||||||
function goPage(to: string) {
|
function goPage(to: string) {
|
||||||
router.push(to)
|
router.push(to)
|
||||||
emit('close')
|
closeSearch()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 跳转订阅页面
|
/** 根据订阅类型跳转到对应订阅详情。 */
|
||||||
function goSubscribe(subscribe: Subscribe) {
|
function goSubscribe(subscribe: Subscribe) {
|
||||||
if (subscribe.type === '电影') {
|
if (subscribe.type === '电影') {
|
||||||
router.push({
|
router.push({
|
||||||
@@ -371,13 +392,29 @@ function goSubscribe(subscribe: Subscribe) {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
closeSearch()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 关闭搜索浮层并通知外层入口同步状态。 */
|
||||||
|
function closeSearch() {
|
||||||
|
dialog.value = false
|
||||||
emit('close')
|
emit('close')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 聚焦当前可见输入框,供快捷键入口复用。 */
|
||||||
|
function focusSearchInput() {
|
||||||
|
searchWordInput.value?.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(dialog, async isOpen => {
|
||||||
|
if (!isOpen) return
|
||||||
|
await nextTick()
|
||||||
|
focusSearchInput()
|
||||||
|
})
|
||||||
|
|
||||||
|
defineExpose({ focusSearchInput })
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
setTimeout(() => {
|
|
||||||
searchWordInput.value?.focus()
|
|
||||||
}, 500)
|
|
||||||
// 根据权限加载不同的数据
|
// 根据权限加载不同的数据
|
||||||
if (hasAdminPermission.value) {
|
if (hasAdminPermission.value) {
|
||||||
fetchInstalledPlugins()
|
fetchInstalledPlugins()
|
||||||
@@ -395,10 +432,20 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<VDialog v-model="dialog" max-width="40rem" scrollable :fullscreen="!display.mdAndUp.value">
|
<component :is="searchOverlay" v-model="dialog" v-bind="searchOverlayProps">
|
||||||
<VCard class="search-dialog">
|
<template v-if="showActivator" #activator="{ props: activatorProps }">
|
||||||
<!-- 搜索输入框区域 -->
|
<!-- 小屏入口保持图标形态,点击后打开全屏搜索弹窗。 -->
|
||||||
<div class="search-header">
|
<IconBtn
|
||||||
|
v-if="!display.mdAndUp.value"
|
||||||
|
v-bind="activatorProps"
|
||||||
|
class="search-icon-trigger"
|
||||||
|
:aria-label="t('dialog.searchBar.openSearch')"
|
||||||
|
>
|
||||||
|
<VIcon class="search-icon-trigger__icon" icon="mdi-magnify" />
|
||||||
|
</IconBtn>
|
||||||
|
|
||||||
|
<!-- 中屏及以上常驻搜索输入框,输入时直接在下方展示同一组选项。 -->
|
||||||
|
<div v-else v-bind="activatorProps" class="search-desktop-activator">
|
||||||
<div class="search-input-wrapper">
|
<div class="search-input-wrapper">
|
||||||
<VIcon icon="mdi-magnify" size="22" class="search-input-icon" />
|
<VIcon icon="mdi-magnify" size="22" class="search-input-icon" />
|
||||||
<input
|
<input
|
||||||
@@ -408,7 +455,26 @@ onMounted(() => {
|
|||||||
class="search-native-input"
|
class="search-native-input"
|
||||||
:placeholder="t('dialog.searchBar.searchPlaceholder')"
|
:placeholder="t('dialog.searchBar.searchPlaceholder')"
|
||||||
@keydown.enter="searchMedia('media')"
|
@keydown.enter="searchMedia('media')"
|
||||||
@keydown.escape="emit('close')"
|
@keydown.escape.stop="closeSearch"
|
||||||
|
/>
|
||||||
|
<kbd class="search-shortcut-badge">{{ metaKey }}</kbd>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<VCard class="search-dialog" :class="{ 'search-dialog--dropdown': display.mdAndUp.value }">
|
||||||
|
<!-- 弹窗模式保留原有搜索输入区。 -->
|
||||||
|
<div v-if="!display.mdAndUp.value" class="search-header">
|
||||||
|
<div class="search-input-wrapper">
|
||||||
|
<VIcon icon="mdi-magnify" size="22" class="search-input-icon" />
|
||||||
|
<input
|
||||||
|
ref="searchWordInput"
|
||||||
|
v-model="searchWord"
|
||||||
|
type="text"
|
||||||
|
class="search-native-input"
|
||||||
|
:placeholder="t('dialog.searchBar.searchPlaceholder')"
|
||||||
|
@keydown.enter="searchMedia('media')"
|
||||||
|
@keydown.escape.stop="closeSearch"
|
||||||
/>
|
/>
|
||||||
<VBtn icon size="small" variant="text" class="search-submit-btn" @click="searchMedia('media')">
|
<VBtn icon size="small" variant="text" class="search-submit-btn" @click="searchMedia('media')">
|
||||||
<VIcon icon="mdi-magnify" size="20" />
|
<VIcon icon="mdi-magnify" size="20" />
|
||||||
@@ -697,26 +763,14 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 底部区域 -->
|
<!-- 弹窗形态保留底部关闭按钮,桌面下拉不显示页脚。 -->
|
||||||
<!-- 桌面端:快捷键提示 -->
|
<div v-if="!display.mdAndUp.value" class="search-footer-mobile">
|
||||||
<div v-if="display.mdAndUp.value" class="search-footer">
|
<VBtn icon variant="tonal" @click="closeSearch">
|
||||||
<div class="shortcut-group">
|
|
||||||
<kbd>Esc</kbd>
|
|
||||||
<span class="shortcut-label">{{ t('dialog.searchBar.escClose') }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="shortcut-group">
|
|
||||||
<kbd>{{ metaKey }}</kbd>
|
|
||||||
<span class="shortcut-label">{{ t('dialog.searchBar.openSearch') }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- 移动端:关闭图标 -->
|
|
||||||
<div v-else class="search-footer-mobile">
|
|
||||||
<VBtn icon variant="tonal" @click="emit('close')">
|
|
||||||
<VIcon icon="mdi-close" size="20" />
|
<VIcon icon="mdi-close" size="20" />
|
||||||
</VBtn>
|
</VBtn>
|
||||||
</div>
|
</div>
|
||||||
</VCard>
|
</VCard>
|
||||||
</VDialog>
|
</component>
|
||||||
|
|
||||||
<!-- 站点选择对话框 -->
|
<!-- 站点选择对话框 -->
|
||||||
<SearchSiteDialog
|
<SearchSiteDialog
|
||||||
@@ -737,6 +791,39 @@ onMounted(() => {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.search-dialog--dropdown {
|
||||||
|
border: 1px solid rgba(var(--v-theme-on-surface), 0.08);
|
||||||
|
inline-size: min(40rem, calc(100vw - 2rem));
|
||||||
|
max-block-size: min(72vh, 42rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-desktop-activator {
|
||||||
|
flex: 0 1 32rem;
|
||||||
|
inline-size: clamp(18rem, 32vw, 32rem);
|
||||||
|
max-inline-size: calc(100vw - 10rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-desktop-activator .search-input-wrapper {
|
||||||
|
border-color: rgba(var(--v-theme-on-surface), 0.12);
|
||||||
|
background: rgba(var(--v-theme-surface), 0.72);
|
||||||
|
block-size: 42px;
|
||||||
|
box-shadow: var(--app-surface-shadow);
|
||||||
|
padding-inline: 14px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme='transparent'] .search-desktop-activator .search-input-wrapper,
|
||||||
|
.v-theme--transparent .search-desktop-activator .search-input-wrapper {
|
||||||
|
background: rgba(var(--v-theme-surface), var(--transparent-opacity-light, 0.2));
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-icon-trigger {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-icon-trigger__icon {
|
||||||
|
transform: scaleX(-1);
|
||||||
|
}
|
||||||
|
|
||||||
/* 搜索头部区域 */
|
/* 搜索头部区域 */
|
||||||
.search-header {
|
.search-header {
|
||||||
padding-block: 16px 12px;
|
padding-block: 16px 12px;
|
||||||
@@ -748,18 +835,19 @@ onMounted(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border: 1.5px solid rgba(var(--v-theme-primary), 0.4);
|
border: 1.5px solid rgba(var(--v-theme-primary), 0.4);
|
||||||
border-radius: 28px;
|
border-radius: var(--app-vuetify-rounded-pill);
|
||||||
background-color: rgba(var(--v-theme-surface-variant), 0.04);
|
background-color: rgba(var(--v-theme-surface-variant), 0.04);
|
||||||
block-size: 48px;
|
block-size: 48px;
|
||||||
padding-inline: 14px 6px;
|
padding-inline: 14px 6px;
|
||||||
transition:
|
transition:
|
||||||
border-color 0.2s ease,
|
border-color 0.2s ease,
|
||||||
|
border-radius 0.2s ease,
|
||||||
box-shadow 0.2s ease;
|
box-shadow 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-input-wrapper:focus-within {
|
.search-input-wrapper:focus-within {
|
||||||
border-color: rgb(var(--v-theme-primary));
|
border-color: rgb(var(--v-theme-primary));
|
||||||
box-shadow: 0 0 0 3px rgba(var(--v-theme-on-surface), 0.04);
|
box-shadow: var(--app-surface-shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-input-icon {
|
.search-input-icon {
|
||||||
@@ -797,6 +885,12 @@ onMounted(() => {
|
|||||||
background-color: rgba(var(--v-theme-on-surface), 0.12) !important;
|
background-color: rgba(var(--v-theme-on-surface), 0.12) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.search-shortcut-badge {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-inline-start: 10px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
/* 主内容区域 */
|
/* 主内容区域 */
|
||||||
.search-content {
|
.search-content {
|
||||||
max-block-size: 600px;
|
max-block-size: 600px;
|
||||||
@@ -804,6 +898,10 @@ onMounted(() => {
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.search-dialog--dropdown .search-content {
|
||||||
|
max-block-size: min(58vh, 34rem);
|
||||||
|
}
|
||||||
|
|
||||||
.search-list {
|
.search-list {
|
||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
}
|
}
|
||||||
@@ -854,22 +952,6 @@ onMounted(() => {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 底部快捷键提示 */
|
|
||||||
.search-footer {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
border-block-start: 1px solid rgba(var(--v-theme-on-surface), 0.08);
|
|
||||||
gap: 16px;
|
|
||||||
padding-block: 10px;
|
|
||||||
padding-inline: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shortcut-group {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
kbd {
|
kbd {
|
||||||
border: 1px solid rgba(var(--v-theme-on-surface), 0.15);
|
border: 1px solid rgba(var(--v-theme-on-surface), 0.15);
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
@@ -883,11 +965,6 @@ kbd {
|
|||||||
padding-inline: 6px;
|
padding-inline: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shortcut-label {
|
|
||||||
color: rgba(var(--v-theme-on-surface), 0.45);
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 移动端底部关闭图标 */
|
/* 移动端底部关闭图标 */
|
||||||
.search-footer-mobile {
|
.search-footer-mobile {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -912,10 +989,5 @@ kbd {
|
|||||||
.search-native-input {
|
.search-native-input {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-footer {
|
|
||||||
padding-block: 8px;
|
|
||||||
padding-inline: 12px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -523,8 +523,8 @@ onMounted(async () => {
|
|||||||
class="theme-navbar-actions d-flex align-center"
|
class="theme-navbar-actions d-flex align-center"
|
||||||
:class="{ 'theme-navbar-actions--horizontal': showHorizontalThemeNav }"
|
:class="{ 'theme-navbar-actions--horizontal': showHorizontalThemeNav }"
|
||||||
>
|
>
|
||||||
<!-- 👉 Horizontal Search Icon -->
|
<!-- 👉 Horizontal Search Bar -->
|
||||||
<SearchBar v-if="showHorizontalThemeNav" icon-only />
|
<SearchBar v-if="showHorizontalThemeNav" />
|
||||||
<!-- 👉 Shortcuts -->
|
<!-- 👉 Shortcuts -->
|
||||||
<ShortcutBar v-if="canAdmin" />
|
<ShortcutBar v-if="canAdmin" />
|
||||||
<!-- 👉 Notification -->
|
<!-- 👉 Notification -->
|
||||||
|
|||||||
@@ -1,109 +1,31 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import * as Mousetrap from 'mousetrap'
|
import * as Mousetrap from 'mousetrap'
|
||||||
import SearchBarDialog from '@/components/dialog/SearchBarDialog.vue'
|
import SearchBarDialog from '@/components/dialog/SearchBarDialog.vue'
|
||||||
import { openSharedDialog } from '@/composables/useSharedDialog'
|
|
||||||
import { useDisplay } from 'vuetify'
|
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
|
|
||||||
const props = withDefaults(
|
const searchOpen = ref(false)
|
||||||
defineProps<{
|
const searchBar = ref<InstanceType<typeof SearchBarDialog> | null>(null)
|
||||||
iconOnly?: boolean
|
|
||||||
}>(),
|
|
||||||
{
|
|
||||||
iconOnly: false,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
const display = useDisplay()
|
/** 打开全局搜索,并在桌面端聚焦常驻搜索输入框。 */
|
||||||
const { t } = useI18n()
|
function openSearch() {
|
||||||
|
searchOpen.value = true
|
||||||
// 注册快捷键
|
nextTick(() => searchBar.value?.focusSearchInput())
|
||||||
Mousetrap.bind(['command+k', 'ctrl+k'], openSearchDialog)
|
|
||||||
|
|
||||||
/** 打开全局共享搜索弹窗。 */
|
|
||||||
function openSearchDialog() {
|
|
||||||
openSharedDialog(SearchBarDialog, {}, {}, { closeOn: ['close', 'update:modelValue'] })
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 检测操作系统是否是 Mac。 */
|
/** 关闭当前搜索浮层。 */
|
||||||
function isMac() {
|
function closeSearch() {
|
||||||
return navigator.platform.toUpperCase().indexOf('MAC') >= 0
|
searchOpen.value = false
|
||||||
}
|
}
|
||||||
// 计算属性:根据操作系统显示不同的按键提示
|
|
||||||
const metaKey = computed(() => (isMac() ? '⌘+K' : 'Ctrl+K'))
|
onMounted(() => {
|
||||||
const showIconOnly = computed(() => props.iconOnly || !display.mdAndUp.value)
|
Mousetrap.bind(['command+k', 'ctrl+k'], openSearch)
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
Mousetrap.unbind(['command+k', 'ctrl+k'])
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- 小屏或水平导航右侧工具区:仅显示搜索图标。 -->
|
<SearchBarDialog ref="searchBar" v-model="searchOpen" show-activator @close="closeSearch" />
|
||||||
<IconBtn
|
|
||||||
v-if="showIconOnly"
|
|
||||||
class="search-icon-trigger"
|
|
||||||
:aria-label="t('dialog.searchBar.openSearch')"
|
|
||||||
@click="openSearchDialog"
|
|
||||||
>
|
|
||||||
<VIcon class="search-icon-trigger__icon" icon="mdi-magnify" />
|
|
||||||
</IconBtn>
|
|
||||||
|
|
||||||
<!-- 中屏及以上:与用户头像同尺寸的搜索入口。 -->
|
|
||||||
<VTooltip v-else :text="`${t('dialog.searchBar.openSearch')} (${metaKey})`">
|
|
||||||
<template #activator="{ props: tooltipProps }">
|
|
||||||
<button
|
|
||||||
v-bind="tooltipProps"
|
|
||||||
class="search-trigger"
|
|
||||||
type="button"
|
|
||||||
:aria-label="t('dialog.searchBar.openSearch')"
|
|
||||||
@click="openSearchDialog"
|
|
||||||
>
|
|
||||||
<VIcon icon="mdi-magnify" size="24" class="search-trigger-icon" />
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
</VTooltip>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.search-trigger {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border: 1px solid rgba(var(--v-theme-on-surface), 0.1);
|
|
||||||
border-radius: 50%;
|
|
||||||
background: rgba(var(--v-theme-surface), 0.44);
|
|
||||||
block-size: 40px;
|
|
||||||
color: rgba(var(--v-theme-on-surface), 0.72);
|
|
||||||
cursor: pointer;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
inline-size: 40px;
|
|
||||||
padding: 0;
|
|
||||||
transition:
|
|
||||||
border-color 0.2s ease,
|
|
||||||
background-color 0.2s ease,
|
|
||||||
box-shadow 0.2s ease;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-trigger:hover {
|
|
||||||
border-color: rgba(var(--v-theme-on-surface), 0.18);
|
|
||||||
background-color: rgba(var(--v-theme-surface), 0.62);
|
|
||||||
box-shadow: 0 4px 14px rgba(0, 0, 0, 6%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-trigger-icon {
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
html[data-theme='transparent'] .search-trigger,
|
|
||||||
.v-theme--transparent .search-trigger {
|
|
||||||
backdrop-filter: none;
|
|
||||||
background: rgba(var(--v-theme-surface), var(--transparent-opacity-light, 0.2));
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-icon-trigger {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-icon-trigger__icon {
|
|
||||||
transform: scaleX(-1);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import DashboardElement from '@/components/misc/DashboardElement.vue'
|
|||||||
import { useDynamicButton, type DynamicButtonMenuItem } from '@/composables/useDynamicButton'
|
import { useDynamicButton, type DynamicButtonMenuItem } from '@/composables/useDynamicButton'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { usePWA } from '@/composables/usePWA'
|
import { usePWA } from '@/composables/usePWA'
|
||||||
import { getItemColor, initializeItemColors } from '@/utils/colorUtils'
|
|
||||||
import { openSharedDialog } from '@/composables/useSharedDialog'
|
import { openSharedDialog } from '@/composables/useSharedDialog'
|
||||||
import { useUserStore } from '@/stores'
|
import { useUserStore } from '@/stores'
|
||||||
import { buildUserPermissionContext, hasPermission } from '@/utils/permission'
|
import { buildUserPermissionContext, hasPermission } from '@/utils/permission'
|
||||||
@@ -282,9 +281,6 @@ const pluginDashboardMeta = ref<any[]>([])
|
|||||||
// 插件仪表板的刷新状态
|
// 插件仪表板的刷新状态
|
||||||
const pluginDashboardRefreshStatus = ref<{ [key: string]: boolean }>({})
|
const pluginDashboardRefreshStatus = ref<{ [key: string]: boolean }>({})
|
||||||
|
|
||||||
// 为每个项目生成随机颜色
|
|
||||||
const itemColors = ref<{ [key: string]: string }>({})
|
|
||||||
|
|
||||||
// 当前启用且可渲染的仪表板 Grid 项。
|
// 当前启用且可渲染的仪表板 Grid 项。
|
||||||
const dashboardGridItems = computed<DashboardGridItem[]>(() =>
|
const dashboardGridItems = computed<DashboardGridItem[]>(() =>
|
||||||
dashboardConfigs.value
|
dashboardConfigs.value
|
||||||
@@ -723,15 +719,6 @@ function buildDashboardGridWidget(item: DashboardItem, id: string): GridStackWid
|
|||||||
return widget
|
return widget
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化颜色。
|
|
||||||
function initializeColors() {
|
|
||||||
initializeItemColors(dashboardConfigs.value, item => buildPluginDashboardId(item.id, item.key))
|
|
||||||
dashboardConfigs.value.forEach(item => {
|
|
||||||
const itemId = buildPluginDashboardId(item.id, item.key)
|
|
||||||
itemColors.value[itemId] = getItemColor(itemId)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用动态按钮钩子
|
// 使用动态按钮钩子
|
||||||
let settingsDialogController: ReturnType<typeof openSharedDialog> | null = null
|
let settingsDialogController: ReturnType<typeof openSharedDialog> | null = null
|
||||||
|
|
||||||
@@ -741,7 +728,6 @@ function openDashboardSettings() {
|
|||||||
settingsDialogController = openSharedDialog(
|
settingsDialogController = openSharedDialog(
|
||||||
ContentToggleSettingsDialog,
|
ContentToggleSettingsDialog,
|
||||||
{
|
{
|
||||||
colors: itemColors.value,
|
|
||||||
enabled: enableConfig.value,
|
enabled: enableConfig.value,
|
||||||
hint: t('dashboard.chooseContent'),
|
hint: t('dashboard.chooseContent'),
|
||||||
items: dashboardConfigs.value,
|
items: dashboardConfigs.value,
|
||||||
@@ -1001,11 +987,6 @@ async function getPluginDashboard(id: string, key: string) {
|
|||||||
dashboardConfigs.value[index] = res
|
dashboardConfigs.value[index] = res
|
||||||
} else {
|
} else {
|
||||||
dashboardConfigs.value.push(res)
|
dashboardConfigs.value.push(res)
|
||||||
// 为新增的插件仪表板生成颜色
|
|
||||||
const pluginDashboardId = buildPluginDashboardId(id, key)
|
|
||||||
if (!itemColors.value[pluginDashboardId]) {
|
|
||||||
itemColors.value[pluginDashboardId] = getItemColor(pluginDashboardId)
|
|
||||||
}
|
|
||||||
// 排序
|
// 排序
|
||||||
sortDashboardConfigs()
|
sortDashboardConfigs()
|
||||||
}
|
}
|
||||||
@@ -1366,7 +1347,6 @@ watch(
|
|||||||
|
|
||||||
onBeforeMount(async () => {
|
onBeforeMount(async () => {
|
||||||
await loadDashboardConfig()
|
await loadDashboardConfig()
|
||||||
initializeColors()
|
|
||||||
await getPluginDashboardMeta()
|
await getPluginDashboardMeta()
|
||||||
isDashboardConfigLoaded.value = true
|
isDashboardConfigLoaded.value = true
|
||||||
scheduleDashboardReveal()
|
scheduleDashboardReveal()
|
||||||
|
|||||||
Reference in New Issue
Block a user