mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-04 23:18:44 +08:00
Polish app center grouped menu styling
This commit is contained in:
Vendored
+3
@@ -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
@@ -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
@@ -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 [
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -21,6 +21,7 @@ export function navMenuFromPluginSidebarItem(
|
||||
return {
|
||||
title: item.title,
|
||||
icon: item.icon,
|
||||
iconColor: 'primary',
|
||||
to: {
|
||||
name: 'plugin-app',
|
||||
params: {
|
||||
|
||||
Reference in New Issue
Block a user