From 8bc311916d8f6bda41b7578454d7a5c3fac352ed Mon Sep 17 00:00:00 2001 From: xiaomai Date: Sun, 3 May 2026 11:27:43 +0800 Subject: [PATCH] feat(admin): redesign navigation with grouped secondary sidebar Replace flat tabs with categorized navigation groups (Content, Config, etc.). Update layout styles to support a responsive secondary sidebar. --- DESIGN.md | 5 ++ frontend/src/styles/main.css | 113 +++++++++++++++++++++++++++++- frontend/src/views/AdminView.vue | 115 ++++++++++++++++++++++--------- system-wordings.ts | 12 +++- 4 files changed, 208 insertions(+), 37 deletions(-) diff --git a/DESIGN.md b/DESIGN.md index 1de262b..3857f01 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -619,6 +619,11 @@ API 暴露边界: - UI 风格以 `DesignGuidelines.html` 为准。 - 页面结构以 `AppShell`、`PageHeader`、列表、详情区和管理区为核心。 - 全局主导航使用 `AppShell` 侧边栏;移动端通过导航按钮打开侧边栏抽屉。 +- 管理入口在全局侧边栏中保持单一 Admin 入口,`/admin` 内部使用页面内二级菜单分组组织管理模块: + - 配置:System config。 + - 内容:Daily CheckList、Pokemon、物品、材料单和栖息地的维护、排序或删除入口。 + - 本地化:Languages、System wordings。 + - 访问权限:Users、Roles、Permissions。 - 登录用户的侧边栏账号入口进入 `/profile`;User Profile 属于账号入口,不作为 Wiki 主内容导航项。 - 页面级分类、筛选或辅助内容切换使用 Tabs,避免在内容页继续增加侧边栏。 - 导航和主要操作使用图标增强识别。 diff --git a/frontend/src/styles/main.css b/frontend/src/styles/main.css index 27257e4..dc2ed3f 100644 --- a/frontend/src/styles/main.css +++ b/frontend/src/styles/main.css @@ -4459,11 +4459,102 @@ button:disabled, .admin-layout { display: grid; - grid-template-columns: minmax(320px, 420px) minmax(0, 1fr); + grid-template-columns: minmax(220px, 280px) minmax(0, 1fr); gap: 16px; align-items: start; } +.admin-layout--loading { + grid-template-columns: 1fr; +} + +.admin-secondary-nav { + position: sticky; + top: 18px; + display: grid; + align-content: start; + gap: 14px; + min-width: 0; + padding: 12px; + border: 2px solid var(--line-strong); + border-radius: var(--radius-card); + background: var(--surface); + box-shadow: var(--shadow-soft); +} + +.admin-secondary-nav__group { + display: grid; + gap: 6px; + min-width: 0; +} + +.admin-secondary-nav__group + .admin-secondary-nav__group { + padding-top: 12px; + border-top: 1px solid var(--line); +} + +.admin-secondary-nav__title { + padding: 0 4px; + color: var(--muted); + font-size: 13px; + font-weight: 900; +} + +.admin-secondary-nav__items { + display: grid; + gap: 6px; +} + +.admin-secondary-nav__item { + width: 100%; + min-height: 44px; + display: flex; + align-items: center; + justify-content: flex-start; + gap: 10px; + padding: 9px 10px; + border: 1px solid transparent; + border-radius: var(--radius-control); + background: transparent; + color: var(--ink-soft); + font-weight: 850; + line-height: 1.2; + text-align: left; + cursor: pointer; + transition: + background 0.14s ease, + border-color 0.14s ease, + color 0.14s ease, + box-shadow 0.14s ease; +} + +.admin-secondary-nav__item:hover { + border-color: rgba(42, 117, 187, 0.24); + background: rgba(255, 203, 5, 0.2); + color: var(--pokemon-blue-deep); +} + +.admin-secondary-nav__item.active { + border-color: var(--line-strong); + background: var(--pokemon-blue); + color: #ffffff; + box-shadow: 0 2px 0 var(--line-strong); +} + +.admin-secondary-nav__item span { + min-width: 0; +} + +.admin-secondary-nav__icon { + width: 19px; + height: 19px; + flex: 0 0 auto; +} + +.admin-content { + min-width: 0; +} + .form-actions, .row-actions, .check-row, @@ -4802,6 +4893,26 @@ button:disabled, white-space: nowrap; } + .admin-secondary-nav { + position: static; + display: flex; + gap: 12px; + overflow-x: auto; + padding: 10px; + } + + .admin-secondary-nav__group { + flex: 0 0 auto; + min-width: min(260px, 76vw); + } + + .admin-secondary-nav__group + .admin-secondary-nav__group { + padding-top: 0; + padding-left: 12px; + border-top: 0; + border-left: 1px solid var(--line); + } + .coming-soon-panel { grid-template-columns: auto minmax(0, 1fr); } diff --git a/frontend/src/views/AdminView.vue b/frontend/src/views/AdminView.vue index c716df0..6b666ce 100644 --- a/frontend/src/views/AdminView.vue +++ b/frontend/src/views/AdminView.vue @@ -61,6 +61,9 @@ type AdminTab = | 'items' | 'recipes' | 'habitats'; +type AdminGroup = 'content' | 'configuration' | 'localization' | 'access'; +type AdminNavItem = { key: AdminTab; label: string; permission: string | string[] }; +type AdminNavGroup = { key: AdminGroup; label: string; items: AdminNavItem[] }; type EditableConfig = (NamedEntity | Skill) & { hasItemDrop?: boolean }; const adminTabIcons: Record = { @@ -79,21 +82,48 @@ const adminTabIcons: Record = { const { locale, t } = useI18n(); -const tabs = computed>(() => - [ - { key: 'users' as const, label: t('pages.admin.users'), permission: 'admin.users.read' }, - { key: 'roles' as const, label: t('pages.admin.roles'), permission: 'admin.roles.read' }, - { key: 'permissions' as const, label: t('pages.admin.permissions'), permission: 'admin.permissions.read' }, - { key: 'config' as const, label: t('pages.admin.config'), permission: 'admin.config.read' }, - { key: 'languages' as const, label: t('pages.admin.languages'), permission: 'admin.languages.read' }, - { key: 'wordings' as const, label: t('pages.admin.wordings'), permission: 'admin.wordings.read' }, - { key: 'checklist' as const, label: t('pages.admin.checklist'), permission: ['checklist.create', 'checklist.update', 'checklist.delete', 'checklist.order'] }, - { key: 'pokemon' as const, label: 'Pokemon', permission: ['pokemon.order', 'pokemon.delete'] }, - { key: 'items' as const, label: t('pages.items.title'), permission: ['items.order', 'items.delete'] }, - { key: 'recipes' as const, label: t('pages.recipes.title'), permission: ['recipes.order', 'recipes.delete'] }, - { key: 'habitats' as const, label: t('pages.habitats.title'), permission: ['habitats.order', 'habitats.delete'] } - ].filter((tab) => canAny(tab.permission)) -); +const adminNavigationGroups = computed(() => { + const groups: AdminNavGroup[] = [ + { + key: 'configuration', + label: t('pages.admin.configurationGroup'), + items: [{ key: 'config', label: t('pages.admin.config'), permission: 'admin.config.read' }] + }, + { + key: 'content', + label: t('pages.admin.contentGroup'), + items: [ + { key: 'checklist', label: t('pages.admin.checklist'), permission: ['checklist.create', 'checklist.update', 'checklist.delete', 'checklist.order'] }, + { key: 'pokemon', label: t('pages.admin.pokemonList'), permission: ['pokemon.order', 'pokemon.delete'] }, + { key: 'items', label: t('pages.admin.itemList'), permission: ['items.order', 'items.delete'] }, + { key: 'recipes', label: t('pages.admin.recipeList'), permission: ['recipes.order', 'recipes.delete'] }, + { key: 'habitats', label: t('pages.admin.habitatList'), permission: ['habitats.order', 'habitats.delete'] } + ] + }, + { + key: 'localization', + label: t('pages.admin.localizationGroup'), + items: [ + { key: 'languages', label: t('pages.admin.languages'), permission: 'admin.languages.read' }, + { key: 'wordings', label: t('pages.admin.wordings'), permission: 'admin.wordings.read' } + ] + }, + { + key: 'access', + label: t('pages.admin.accessGroup'), + items: [ + { key: 'users', label: t('pages.admin.users'), permission: 'admin.users.read' }, + { key: 'roles', label: t('pages.admin.roles'), permission: 'admin.roles.read' }, + { key: 'permissions', label: t('pages.admin.permissions'), permission: 'admin.permissions.read' } + ] + } + ]; + + return groups + .map((group) => ({ ...group, items: group.items.filter((tab) => canAny(tab.permission)) })) + .filter((group) => group.items.length); +}); +const tabs = computed(() => adminNavigationGroups.value.flatMap((group) => group.items)); const configTypes = computed>(() => [ { key: 'pokemon-types', label: t('config.pokemonTypes') }, @@ -971,29 +1001,44 @@ onMounted(() => {