diff --git a/.codex-qa/appcenter-mobile-comparison.png b/.codex-qa/appcenter-mobile-comparison.png new file mode 100644 index 00000000..df988606 Binary files /dev/null and b/.codex-qa/appcenter-mobile-comparison.png differ diff --git a/.codex-qa/appcenter-mobile-light.png b/.codex-qa/appcenter-mobile-light.png new file mode 100644 index 00000000..23962306 Binary files /dev/null and b/.codex-qa/appcenter-mobile-light.png differ diff --git a/.codex-qa/appcenter-mobile-transparent.png b/.codex-qa/appcenter-mobile-transparent.png new file mode 100644 index 00000000..c580b27e Binary files /dev/null and b/.codex-qa/appcenter-mobile-transparent.png differ diff --git a/design-qa.md b/design-qa.md new file mode 100644 index 00000000..91d4fb18 --- /dev/null +++ b/design-qa.md @@ -0,0 +1,42 @@ +# App Center 移动端设计 QA + +- Source visual truth: `/Users/jxxghp/.codex/generated_images/019f227f-02c8-74c0-bea9-a59b60a9fa57/exec-9ce518f2-5bba-4249-8187-781c8d2c626e.png` +- Implementation screenshot: `/Users/jxxghp/MPProjects/MoviePilot-Frontend/.codex-qa/appcenter-mobile-light.png` +- Transparent-theme screenshot: `/Users/jxxghp/MPProjects/MoviePilot-Frontend/.codex-qa/appcenter-mobile-transparent.png` +- Full-view comparison: `/Users/jxxghp/MPProjects/MoviePilot-Frontend/.codex-qa/appcenter-mobile-comparison.png` +- Viewport: 390px wide mobile viewport; full-page capture +- State: authenticated App Center, light/default skin and transparent/default skin + +**Full-view comparison evidence** + +- The implementation preserves the selected concept's grouped iOS menu hierarchy: external section labels, one surface per group, colored rounded-square icons, inset separators, and trailing chevrons. +- Existing MoviePilot shell chrome and permission-filtered menu content intentionally remain unchanged; the reference mock only defines the App Center content surface. +- Focused-region comparison was not needed because the full-height side-by-side comparison keeps row typography, icon tiles, dividers, radii, and section spacing readable. + +**Required fidelity surfaces** + +- Fonts and typography: existing product font stack retained; mobile titles render at 16px/22px and section labels at 13px/20px without wrapping or truncation. +- Spacing and layout rhythm: 58px tap rows, 12px page gutters, 18px group gaps, inset dividers, and theme-controlled surface radii match the selected direction without horizontal overflow. +- Colors and visual tokens: icon colors come from `NavMenu.iconColor` and global Vuetify semantic color tokens; grouped surfaces, borders, separators, hover/active states, and transparent blur use global tokens. +- Image quality and asset fidelity: the reference contains no raster content; existing Material Design Icons are used rather than generated or handcrafted substitutes. +- Copy and content: existing localized labels and permission filtering are preserved. +- Accessibility and interaction: full-row route links remain functional, touch targets are at least 58px high, active feedback remains visible, and decorative separators are hidden from accessibility APIs. + +**Findings** + +- No actionable P0, P1, or P2 differences remain. +- [P3] The existing Agent Assistant bubble can overlap the far-right page edge in captures; this is shared shell behavior outside the App Center redesign scope. + +**Patches made since the previous QA pass** + +- Replaced the row pseudo-element divider, which conflicted with Vuetify's internal pseudo-element, with an explicit 1px separator element. +- Removed the transparent theme's hard-coded group border so the card follows the global bordered/default skin token. +- Moved icon color metadata into shared menu definitions and kept dynamic plugin menus on the primary fallback. +- Constrained the page, cards, lists, rows, and row content to prevent horizontal scrolling. +- Added transparent-theme blur-mode token overrides for lightweight, realtime, and blur-disabled modes. + +**Follow-up polish** + +- The existing Agent Assistant edge placement can be refined separately if it becomes distracting across other mobile pages. + +final result: passed diff --git a/src/@layouts/types.d.ts b/src/@layouts/types.d.ts index e4220639..e8425b60 100644 --- a/src/@layouts/types.d.ts +++ b/src/@layouts/types.d.ts @@ -113,10 +113,13 @@ export interface NavLinkProps { rel?: ATagRelAttrValues } +export type NavMenuIconColor = 'primary' | 'info' | 'success' | 'warning' | 'secondary' + export interface NavLink extends NavLinkProps, Partial { title: string full_title?: string icon?: unknown + iconColor?: NavMenuIconColor badgeContent?: string badgeClass?: string disable?: boolean diff --git a/src/pages/appcenter.vue b/src/pages/appcenter.vue index 26d0b992..21621c7f 100644 --- a/src/pages/appcenter.vue +++ b/src/pages/appcenter.vue @@ -18,7 +18,7 @@ const userPermissions = computed(() => buildUserPermissionContext(userStore.supe // 应用分组(以header分组) const appGroups = ref>({}) -// 根据header属性对应用进行分类(含插件侧栏项,与桌面端侧栏一致) +/** 按菜单 header 聚合内置与插件入口,并保持与桌面侧栏一致的权限过滤结果。 */ async function categorizeApps() { const allMenus = getNavMenus(t) const filteredMenus = filterMenusByPermission(allMenus, userPermissions.value) @@ -53,14 +53,14 @@ onMounted(() => { diff --git a/src/router/i18n-menu.ts b/src/router/i18n-menu.ts index 92454ef6..921e6034 100644 --- a/src/router/i18n-menu.ts +++ b/src/router/i18n-menu.ts @@ -2,7 +2,7 @@ import { useGlobalSettingsStore } from '@/stores' import type { NavMenu, NavMenuTabItem } from '@/@layouts/types' import type { Composer } from 'vue-i18n' -// 构建路由菜单,每次调用时使用当前的语言环境 +/** 构建当前语言与全局模式对应的主导航菜单。 */ export function getNavMenus(t: Composer['t']): NavMenu[] { const globalSettingsStore = useGlobalSettingsStore() @@ -13,6 +13,7 @@ export function getNavMenus(t: Composer['t']): NavMenu[] { { title: t('navItems.dashboard'), icon: 'mdi-home-outline', + iconColor: 'primary', to: '/dashboard', header: t('menu.start'), admin: false, @@ -22,6 +23,7 @@ export function getNavMenus(t: Composer['t']): NavMenu[] { { title: t('navItems.searchResult'), icon: 'mdi-magnify', + iconColor: 'info', to: '/resource', header: t('menu.start'), admin: false, @@ -30,6 +32,7 @@ export function getNavMenus(t: Composer['t']): NavMenu[] { { title: t('navItems.recommend'), icon: 'mdi-star-outline', + iconColor: 'primary', to: '/recommend', header: t('menu.discovery'), admin: false, @@ -40,6 +43,7 @@ export function getNavMenus(t: Composer['t']): NavMenu[] { { title: t('navItems.explore'), icon: 'mdi-apple-safari', + iconColor: 'info', to: '/discover', header: t('menu.discovery'), admin: false, @@ -51,6 +55,7 @@ export function getNavMenus(t: Composer['t']): NavMenu[] { title: t('navItems.movie'), full_title: t('navItems.movieSubscribe'), icon: 'mdi-movie-open-outline', + iconColor: 'success', to: '/subscribe/movie', header: t('menu.subscribe'), admin: false, @@ -62,6 +67,7 @@ export function getNavMenus(t: Composer['t']): NavMenu[] { title: t('navItems.tv'), full_title: t('navItems.tvSubscribe'), icon: 'mdi-television', + iconColor: 'warning', to: '/subscribe/tv', header: t('menu.subscribe'), admin: false, @@ -73,6 +79,7 @@ export function getNavMenus(t: Composer['t']): NavMenu[] { title: t('navItems.workflow'), full_title: t('navItems.workflow'), icon: 'mdi-state-machine', + iconColor: 'primary', to: '/workflow', header: t('menu.subscribe'), admin: true, @@ -84,6 +91,7 @@ export function getNavMenus(t: Composer['t']): NavMenu[] { title: t('navItems.calendar'), full_title: t('navItems.calendar'), icon: 'mdi-calendar', + iconColor: 'info', to: '/calendar', header: t('menu.subscribe'), admin: false, @@ -92,6 +100,7 @@ export function getNavMenus(t: Composer['t']): NavMenu[] { { title: t('navItems.downloadManager'), icon: 'mdi-download-outline', + iconColor: 'info', to: '/downloading', header: t('menu.organize'), admin: false, @@ -100,6 +109,7 @@ export function getNavMenus(t: Composer['t']): NavMenu[] { { title: t('navItems.mediaOrganize'), icon: 'mdi-folder-play-outline', + iconColor: 'warning', to: '/history', header: t('menu.organize'), admin: true, @@ -108,6 +118,7 @@ export function getNavMenus(t: Composer['t']): NavMenu[] { { title: t('navItems.fileManager'), icon: 'mdi-folder-multiple-outline', + iconColor: 'success', to: '/filemanager', header: t('menu.organize'), admin: true, @@ -116,6 +127,7 @@ export function getNavMenus(t: Composer['t']): NavMenu[] { { title: t('navItems.pluginManager'), icon: 'mdi-puzzle-outline', + iconColor: 'primary', to: '/plugins', header: t('menu.system'), admin: true, @@ -125,6 +137,7 @@ export function getNavMenus(t: Composer['t']): NavMenu[] { { title: t('navItems.siteManager'), icon: 'mdi-web', + iconColor: 'info', to: '/site', header: t('menu.system'), admin: true, @@ -133,6 +146,7 @@ export function getNavMenus(t: Composer['t']): NavMenu[] { { title: t('navItems.userManager'), icon: 'mdi-account-group-outline', + iconColor: 'success', to: '/user', header: t('menu.system'), admin: true, @@ -143,6 +157,7 @@ export function getNavMenus(t: Composer['t']): NavMenu[] { { title: t('navItems.settings'), icon: 'mdi-cog-outline', + iconColor: 'secondary', to: '/setting', header: t('menu.system'), admin: true, @@ -154,7 +169,7 @@ export function getNavMenus(t: Composer['t']): NavMenu[] { ] } -// 获取推荐标签页 +/** 返回推荐页可用的分类标签。 */ export function getRecommendTabs(t: Composer['t']): NavMenuTabItem[] { return [ { title: t('recommend.all'), icon: 'mdi-filmstrip-box-multiple', tab: t('recommend.all') }, @@ -165,7 +180,7 @@ export function getRecommendTabs(t: Composer['t']): NavMenuTabItem[] { ] } -// 获取设置标签页 +/** 返回系统设置页的配置标签。 */ export function getSettingTabs(t: Composer['t']): NavMenuTabItem[] { return [ { @@ -213,7 +228,7 @@ export function getSettingTabs(t: Composer['t']): NavMenuTabItem[] { ] } -// 获取电影订阅标签页 +/** 返回电影订阅页的业务标签。 */ export function getSubscribeMovieTabs(t: Composer['t']): NavMenuTabItem[] { return [ { @@ -229,7 +244,7 @@ export function getSubscribeMovieTabs(t: Composer['t']): NavMenuTabItem[] { ] } -// 获取电视剧订阅标签页 +/** 返回电视剧订阅页的业务标签。 */ export function getSubscribeTvTabs(t: Composer['t']): NavMenuTabItem[] { return [ { @@ -250,7 +265,7 @@ export function getSubscribeTvTabs(t: Composer['t']): NavMenuTabItem[] { ] } -// 获取插件标签页 +/** 返回插件管理页的业务标签。 */ export function getPluginTabs(t: Composer['t']): NavMenuTabItem[] { return [ { @@ -266,7 +281,7 @@ export function getPluginTabs(t: Composer['t']): NavMenuTabItem[] { ] } -// 获取发现标签页 +/** 返回发现页的媒体来源标签。 */ export function getDiscoverTabs(t: Composer['t']): NavMenuTabItem[] { return [ { @@ -287,7 +302,7 @@ export function getDiscoverTabs(t: Composer['t']): NavMenuTabItem[] { ] } -// 获取工作流标签页 +/** 返回工作流页的业务标签。 */ export function getWorkflowTabs(t: Composer['t']): NavMenuTabItem[] { return [ { diff --git a/src/styles/common.scss b/src/styles/common.scss index e3decba0..ca543956 100644 --- a/src/styles/common.scss +++ b/src/styles/common.scss @@ -97,6 +97,22 @@ html { --app-overlay-shadow: var(--app-elevation-0); --app-surface-shadow: var(--app-elevation-0); --app-surface-hover-shadow: var(--app-elevation-0); + --app-grouped-list-background: rgb(var(--v-theme-surface)); + --app-grouped-list-backdrop-filter: none; + --app-grouped-list-border: var(--app-surface-border); + --app-grouped-list-radius: var(--app-theme-surface-radius); + --app-grouped-list-separator-color: rgba(var(--v-theme-on-surface), 0.14); + --app-grouped-list-header-color: rgba(var(--v-theme-on-surface), 0.58); + --app-grouped-list-chevron-color: rgba(var(--v-theme-on-surface), 0.42); + --app-grouped-list-hover-background: rgba(var(--v-theme-primary), 0.05); + --app-grouped-list-active-background: rgba(var(--v-theme-primary), 0.1); + --app-grouped-list-icon-foreground: rgb(var(--v-theme-on-primary)); + --app-grouped-list-icon-opacity: 0.92; + --app-grouped-list-icon-primary-rgb: var(--v-theme-primary); + --app-grouped-list-icon-info-rgb: var(--v-theme-info); + --app-grouped-list-icon-success-rgb: var(--v-theme-success); + --app-grouped-list-icon-warning-rgb: var(--v-theme-warning); + --app-grouped-list-icon-secondary-rgb: var(--v-theme-secondary); --mp-motion-duration-page: 180ms; --mp-motion-duration-overlay: 160ms; --mp-motion-ease-standard: cubic-bezier(0.2, 0.8, 0.2, 1); @@ -206,6 +222,17 @@ html[data-theme-radius='extra'] { box-shadow: var(--app-surface-shadow) !important; } +@media (width <= 768px) { + // iOS 式分组菜单的共享表面,主题只需覆盖 token 即可适配实色或玻璃背景。 + .app-grouped-list.v-card { + --app-surface-radius: var(--app-grouped-list-radius); + + border: var(--app-grouped-list-border); + backdrop-filter: var(--app-grouped-list-backdrop-filter) !important; + background-color: var(--app-grouped-list-background) !important; + } +} + // 统一卡片上浮反馈;hover 命中区域应放在静止外层,避免上浮后底边反复触发 mouseleave。 .v-card.app-hover-lift-card { border: var(--app-card-light-border); diff --git a/src/styles/themes/transparent.scss b/src/styles/themes/transparent.scss index b25e8123..1289bb13 100644 --- a/src/styles/themes/transparent.scss +++ b/src/styles/themes/transparent.scss @@ -11,6 +11,14 @@ html[data-theme="transparent"] { --transparent-blur: 10px; --transparent-blur-light: 6px; --transparent-blur-heavy: 16px; + --app-grouped-list-background: rgba(var(--v-theme-surface), var(--transparent-opacity-light)); + --app-grouped-list-backdrop-filter: blur(var(--transparent-blur)); + --app-grouped-list-separator-color: rgba(var(--v-theme-on-surface), 0.18); + --app-grouped-list-header-color: rgba(var(--v-theme-on-surface), 0.64); + --app-grouped-list-chevron-color: rgba(var(--v-theme-on-surface), 0.5); + --app-grouped-list-hover-background: rgba(var(--v-theme-primary), 0.08); + --app-grouped-list-active-background: rgba(var(--v-theme-primary), 0.14); + --app-grouped-list-icon-opacity: 0.88; // 应用、布局、主内容区域 .v-application, .v-layout, .v-main, .layout-page-content { @@ -245,6 +253,8 @@ html[data-theme="transparent"].transparent-background-blur-disabled { } html[data-theme="transparent"].transparent-glass-lightweight { + --app-grouped-list-backdrop-filter: none; + .global-blur-layer { display: none !important; backdrop-filter: none !important; @@ -307,6 +317,8 @@ html[data-theme="transparent"].transparent-glass-lightweight { } html[data-theme="transparent"].transparent-glass-realtime { + --app-grouped-list-backdrop-filter: none; + .background-image.active { filter: none; transform: none; @@ -350,6 +362,8 @@ html[data-theme="transparent"].transparent-glass-realtime { } html[data-theme="transparent"].transparent-blur-disabled { + --app-grouped-list-backdrop-filter: none; + .layout-vertical-nav, .v-list, .v-card:not(.no-blur), diff --git a/src/utils/pluginSidebarNav.ts b/src/utils/pluginSidebarNav.ts index 097d7ead..db380af0 100644 --- a/src/utils/pluginSidebarNav.ts +++ b/src/utils/pluginSidebarNav.ts @@ -21,6 +21,7 @@ export function navMenuFromPluginSidebarItem( return { title: item.title, icon: item.icon, + iconColor: 'primary', to: { name: 'plugin-app', params: {