diff --git a/src/components/dialog/ContentToggleSettingsDialog.vue b/src/components/dialog/ContentToggleSettingsDialog.vue index bd072554..d8b40fe9 100644 --- a/src/components/dialog/ContentToggleSettingsDialog.vue +++ b/src/components/dialog/ContentToggleSettingsDialog.vue @@ -9,7 +9,6 @@ type UnknownRecord = Record const props = withDefaults( defineProps<{ - colors?: Record enabled: Record elevated?: boolean hint: string @@ -24,7 +23,6 @@ const props = withDefaults( valueGetter?: (item: UnknownRecord) => string }>(), { - colors: () => ({}), elevated: false, labelGetter: undefined, modelValue: true, @@ -129,12 +127,13 @@ function submitSettings() {

{{ props.hint }}

-
@@ -147,7 +146,7 @@ function submitSettings() {
{{ getItemLabel(item) }}
-
+

@@ -186,43 +185,48 @@ function submitSettings() { .settings-grid { display: grid; - gap: 12px; - grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); + gap: 10px; + grid-template-columns: repeat(3, minmax(0, 1fr)); } .setting-label { 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-weight: 500; - line-height: 1.2; + font-weight: 550; + line-height: 1.35; + text-align: start; transition: color 0.2s ease; } .setting-item { + appearance: none; position: relative; overflow: hidden; - border-radius: var(--app-surface-radius); - background-color: rgba(var(--v-theme-surface-variant), 0.3); + min-block-size: 48px; + 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; + font: inherit; padding-block: 10px; 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 { position: absolute; - background: linear-gradient(90deg, var(--item-color, rgb(var(--v-theme-primary))) 0%, transparent 100%); + background: rgb(var(--v-theme-primary)); content: ''; inline-size: 3px; inset-block: 0; inset-inline-start: 0; - opacity: 0.3; + opacity: 0; transition: opacity 0.2s ease; } .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); } @@ -230,6 +234,24 @@ function submitSettings() { 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 { display: flex; align-items: center; @@ -240,4 +262,11 @@ function submitSettings() { display: flex; flex-shrink: 0; } + +@media (width <= 760px) { + .settings-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + diff --git a/src/components/dialog/SearchBarDialog.vue b/src/components/dialog/SearchBarDialog.vue index e0e78375..b437263c 100644 --- a/src/components/dialog/SearchBarDialog.vue +++ b/src/components/dialog/SearchBarDialog.vue @@ -7,6 +7,7 @@ import { useUserStore, useGlobalSettingsStore } from '@/stores' import SearchSiteDialog from '@/components/dialog/SearchSiteDialog.vue' import { useI18n } from 'vue-i18n' import { useDisplay } from 'vuetify' +import { VDialog, VMenu } from 'vuetify/components' import { buildUserPermissionContext, hasPermission, filterMenusByPermission } from '@/utils/permission' // 显示器宽度 @@ -15,10 +16,16 @@ const display = useDisplay() // 多语言支持 const { t } = useI18n() -// 定义props,接收modelValue -const props = defineProps<{ - modelValue: boolean -}>() +// 定义 props,接收浮层状态及是否显示响应式入口。 +const props = withDefaults( + defineProps<{ + modelValue: boolean + showActivator?: boolean + }>(), + { + showActivator: false, + }, +) // 路由 const router = useRouter() @@ -78,16 +85,33 @@ const dialog = computed({ 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(null) -// ref -const searchWordInput = ref(null) +// 当前尺寸下可见的搜索输入框。 +const searchWordInput = ref(null) // 近期搜索词条 const recentSearches = ref([]) -// 检测操作系统是否是Mac +/** 检测操作系统是否为 macOS。 */ function isMac() { return navigator.platform.toUpperCase().indexOf('MAC') >= 0 } @@ -95,7 +119,7 @@ function isMac() { // 计算属性:根据操作系统显示不同的按键提示 const metaKey = computed(() => (isMac() ? '⌘+K' : 'Ctrl+K')) -// 保存近期搜索到本地 +/** 将有效关键词保存到近期搜索记录。 */ function saveRecentSearches(keyword: string) { if (!keyword) return if (recentSearches.value.includes(keyword)) return @@ -103,7 +127,7 @@ function saveRecentSearches(keyword: string) { localStorage.setItem('MP_RecentSearches', JSON.stringify(recentSearches.value)) } -// 从本地加载近期搜索 +/** 从本地存储加载并裁剪近期搜索记录。 */ function loadRecentSearches() { const recentSearchesStr = localStorage.getItem('MP_RecentSearches') if (recentSearchesStr) { @@ -115,7 +139,7 @@ function loadRecentSearches() { } } -// 所有菜单功能 +/** 获取可参与全局搜索的导航菜单和设置入口。 */ function getMenus(): NavMenu[] { let menus: NavMenu[] = [] // 导航菜单 @@ -170,7 +194,7 @@ const matchedMenuItems = computed(() => { // 所有插件(已安装) const pluginItems = ref([]) -// 获取插件列表数据 +/** 加载已安装插件,供搜索结果匹配。 */ async function fetchInstalledPlugins() { try { pluginItems.value = await api.get('plugin/', { @@ -194,7 +218,7 @@ const matchedPluginItems = computed(() => { }) }) -// 获取订阅列表数据 +/** 加载订阅列表,供搜索结果匹配。 */ async function fetchSubscribes() { try { SubscribeItems.value = await api.get('subscribe/') @@ -203,7 +227,7 @@ async function fetchSubscribes() { } } -// 从接口加载用户站点偏好设置 +/** 从接口加载用户的站点搜索偏好。 */ const loadUserSitePreferences = async () => { try { const result = await api.get('system/setting/public/IndexerSites') @@ -216,7 +240,7 @@ const loadUserSitePreferences = async () => { } } -// 查询所有站点 +/** 查询所有启用站点,并初始化默认选择。 */ async function queryAllSites() { try { const data: Site[] = await api.get('site/') @@ -231,7 +255,7 @@ async function queryAllSites() { } } -// 打开站点选择对话框 +/** 打开指定资源类型的站点选择对话框。 */ const openSiteDialog = (type: 'torrent' | 'subtitle' = 'torrent') => { siteSearchType.value = type chooseSiteDialog.value = true @@ -247,7 +271,7 @@ const matchedSubscribeItems = computed(() => { }) }) -// 搜索多站点 +/** 使用选中的站点执行当前资源类型搜索。 */ function searchSites(sites: number[]) { chooseSiteDialog.value = false selectedSites.value = sites @@ -258,7 +282,7 @@ function searchSites(sites: number[]) { searchTorrent() } -// 搜索资源 +/** 使用当前关键词搜索站点资源。 */ function searchTorrent() { if (!searchWord.value || !hasSearchPermission.value) return // 记录搜索词 @@ -273,12 +297,10 @@ function searchTorrent() { sites: selectedSites.value.join(','), }, }) - // 关闭搜索对话框 - dialog.value = false - emit('close') + closeSearch() } -// 搜索字幕资源 +/** 使用当前关键词搜索字幕资源。 */ function searchSubtitle() { if (!searchWord.value || !hasSearchPermission.value) return saveRecentSearches(searchWord.value) @@ -291,11 +313,10 @@ function searchSubtitle() { sites: selectedSites.value.join(','), }, }) - dialog.value = false - emit('close') + closeSearch() } -// 跳转媒体搜索页面 +/** 跳转到指定类型的媒体搜索结果页。 */ function searchMedia(searchType: string) { // 搜索类型 media/person if (!searchWord.value || !hasDiscoveryPermission.value) return @@ -307,10 +328,10 @@ function searchMedia(searchType: string) { type: searchType, }, }) - emit('close') + closeSearch() } -// 跳转到历史记录页面 +/** 跳转到包含当前关键词的历史记录页。 */ function searchHistory() { if (!searchWord.value) return saveRecentSearches(searchWord.value) @@ -320,10 +341,10 @@ function searchHistory() { search: searchWord.value, }, }) - emit('close') + closeSearch() } -// 跳转到订阅分享页面 +/** 跳转到包含当前关键词的订阅分享页。 */ function searchSubscribeShares() { if (!searchWord.value) return saveRecentSearches(searchWord.value) @@ -333,10 +354,10 @@ function searchSubscribeShares() { keyword: searchWord.value, }, }) - emit('close') + closeSearch() } -// 跳转插件页面 +/** 打开匹配插件的已安装详情。 */ function showPlugin(pluginId: string) { router.push({ path: `/plugins/`, @@ -345,16 +366,16 @@ function showPlugin(pluginId: string) { id: pluginId, }, }) - emit('close') + closeSearch() } -// 跳转菜单页面 +/** 跳转到匹配的功能菜单。 */ function goPage(to: string) { router.push(to) - emit('close') + closeSearch() } -// 跳转订阅页面 +/** 根据订阅类型跳转到对应订阅详情。 */ function goSubscribe(subscribe: Subscribe) { if (subscribe.type === '电影') { router.push({ @@ -371,13 +392,29 @@ function goSubscribe(subscribe: Subscribe) { }, }) } + closeSearch() +} + +/** 关闭搜索浮层并通知外层入口同步状态。 */ +function closeSearch() { + dialog.value = false emit('close') } +/** 聚焦当前可见输入框,供快捷键入口复用。 */ +function focusSearchInput() { + searchWordInput.value?.focus() +} + +watch(dialog, async isOpen => { + if (!isOpen) return + await nextTick() + focusSearchInput() +}) + +defineExpose({ focusSearchInput }) + onMounted(() => { - setTimeout(() => { - searchWordInput.value?.focus() - }, 500) // 根据权限加载不同的数据 if (hasAdminPermission.value) { fetchInstalledPlugins() @@ -395,10 +432,20 @@ onMounted(() => { })