Polish app center grouped menu styling

This commit is contained in:
jxxghp
2026-07-02 19:32:39 +08:00
parent 67ef98bfee
commit d5f0fed954
10 changed files with 263 additions and 19 deletions
+3
View File
@@ -113,10 +113,13 @@ export interface NavLinkProps {
rel?: ATagRelAttrValues
}
export type NavMenuIconColor = 'primary' | 'info' | 'success' | 'warning' | 'secondary'
export interface NavLink extends NavLinkProps, Partial<AclProperties> {
title: string
full_title?: string
icon?: unknown
iconColor?: NavMenuIconColor
badgeContent?: string
badgeClass?: string
disable?: boolean
+153 -11
View File
@@ -18,7 +18,7 @@ const userPermissions = computed(() => buildUserPermissionContext(userStore.supe
// 应用分组(以header分组)
const appGroups = ref<Record<string, NavMenu[]>>({})
// 根据header属性对应用进行分类(含插件侧栏项,与桌面侧栏一致
/** 按菜单 header 聚合内置与插件入口,并保持与桌面侧栏一致的权限过滤结果。 */
async function categorizeApps() {
const allMenus = getNavMenus(t)
const filteredMenus = filterMenusByPermission(allMenus, userPermissions.value)
@@ -53,14 +53,14 @@ onMounted(() => {
</script>
<template>
<div class="app-settings-container">
<VContainer>
<VContainer class="app-settings-content">
<!-- 遍历所有分组 -->
<div v-for="(apps, header) in appGroups" :key="header" class="mb-3">
<VListSubheader class="ps-1">
<section v-for="(apps, header) in appGroups" :key="header" class="settings-section">
<VListSubheader class="settings-section-title">
{{ header }}
</VListSubheader>
<!-- 分组内容 - 使用卡片包装 -->
<VCard variant="flat" class="settings-section-card">
<VCard variant="flat" class="app-grouped-list settings-section-card">
<VList lines="one" class="settings-list">
<VListItem
v-for="(app, appIndex) in apps"
@@ -71,35 +71,57 @@ onMounted(() => {
rounded="0"
>
<template #prepend>
<VAvatar size="42" color="primary" variant="text" class="me-3">
<VAvatar
size="42"
color="primary"
variant="text"
class="settings-list-icon me-3"
:class="`settings-list-icon--tone-${app.iconColor || 'primary'}`"
>
<VIcon :icon="app.icon as string" size="24"></VIcon>
</VAvatar>
</template>
<VListItemTitle class="font-weight-medium">
<VListItemTitle class="settings-list-title font-weight-medium">
{{ app.full_title || app.title }}
</VListItemTitle>
<VListItemSubtitle v-if="app.description">
<VListItemSubtitle v-if="app.description" class="settings-list-subtitle">
{{ app.description }}
</VListItemSubtitle>
<template #append>
<VIcon icon="mdi-chevron-right"></VIcon>
<VIcon class="settings-list-chevron" icon="mdi-chevron-right"></VIcon>
</template>
<span
v-if="appIndex < apps.length - 1"
class="settings-list-separator"
aria-hidden="true"
></span>
</VListItem>
</VList>
</VCard>
</div>
</section>
</VContainer>
</div>
</template>
<style lang="scss" scoped>
.app-settings-container {
inline-size: 100%;
margin-block: 0;
margin-inline: auto;
max-inline-size: 960px;
overflow-x: hidden;
}
.settings-section {
margin-block-end: 12px;
}
.settings-section-title {
padding-inline-start: 4px;
}
.settings-section-card {
@@ -125,7 +147,127 @@ onMounted(() => {
}
&:hover {
background-color: rgba(var(--v-theme-primary), 0.05);
background-color: var(--app-grouped-list-hover-background);
}
}
@media (width <= 768px) {
.app-settings-content {
box-sizing: border-box;
max-inline-size: 100%;
overflow-x: hidden;
padding-block: 8px calc(24px + env(safe-area-inset-bottom));
padding-inline: 12px;
}
.settings-section {
margin-block-end: 18px;
}
.settings-section:last-child {
margin-block-end: 0;
}
.settings-section-title {
min-block-size: auto;
padding-block: 0 7px;
padding-inline: 12px;
color: var(--app-grouped-list-header-color);
font-size: 0.8125rem;
font-weight: 500;
letter-spacing: 0.015em;
line-height: 1.25rem;
text-transform: none;
}
.settings-section-card {
max-inline-size: 100%;
overflow-x: hidden;
border-radius: var(--app-grouped-list-radius) !important;
}
.settings-list {
max-inline-size: 100%;
overflow-x: hidden;
background: transparent;
border-radius: inherit !important;
}
.settings-list-item {
max-inline-size: 100%;
min-inline-size: 0;
min-block-size: 58px;
overflow: hidden;
padding-block: 7px;
padding-inline: 12px 9px;
border-block-end: 0 !important;
border-radius: 0 !important;
}
.settings-list-separator {
position: absolute;
background-color: var(--app-grouped-list-separator-color);
block-size: 1px;
inset-block-end: 0;
inset-inline: 58px 0;
pointer-events: none;
z-index: 2;
}
.settings-list-item:active {
background-color: var(--app-grouped-list-active-background);
}
.settings-list-icon {
--app-grouped-list-icon-rgb: var(--app-grouped-list-icon-primary-rgb);
border-radius: max(var(--app-control-radius), 8px) !important;
background: rgba(var(--app-grouped-list-icon-rgb), var(--app-grouped-list-icon-opacity)) !important;
color: var(--app-grouped-list-icon-foreground) !important;
}
.settings-list-icon--tone-info {
--app-grouped-list-icon-rgb: var(--app-grouped-list-icon-info-rgb);
}
.settings-list-icon--tone-success {
--app-grouped-list-icon-rgb: var(--app-grouped-list-icon-success-rgb);
}
.settings-list-icon--tone-warning {
--app-grouped-list-icon-rgb: var(--app-grouped-list-icon-warning-rgb);
}
.settings-list-icon--tone-secondary {
--app-grouped-list-icon-rgb: var(--app-grouped-list-icon-secondary-rgb);
}
.settings-list-item :deep(.v-list-item__content) {
min-inline-size: 0;
overflow: hidden;
}
.settings-list-icon :deep(.v-icon) {
font-size: 1.375rem !important;
}
.settings-list-title {
font-size: 1rem;
font-weight: 500 !important;
letter-spacing: 0;
line-height: 1.375rem;
}
.settings-list-subtitle {
margin-block-start: 2px;
color: rgba(var(--v-theme-on-surface), 0.58);
font-size: 0.75rem;
line-height: 1rem;
}
.settings-list-chevron {
color: var(--app-grouped-list-chevron-color);
font-size: 1.375rem;
}
}
</style>
+23 -8
View File
@@ -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 [
{
+27
View File
@@ -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);
+14
View File
@@ -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),
+1
View File
@@ -21,6 +21,7 @@ export function navMenuFromPluginSidebarItem(
return {
title: item.title,
icon: item.icon,
iconColor: 'primary',
to: {
name: 'plugin-app',
params: {