feat: 综合管理后台扁平化 + 历史按 portal+app 隔离
- admin-portal 从多应用形态重构为单应用形态:3个一级菜单(系统设置/运营中心/设备中心)直接挂在 portal 下,URL=/admin-portal/<section>/<page> - TopBar 移除应用切换下拉,应用名改为纯文本展示 - PageHistory 隔离粒度从 portal 级改为 portal+app 级(scopeKey 形如 admin-portal__admin / tenant-portal__nutrition) - Home 原型首页区分 admin 一级菜单卡片与 tenant 应用卡片布局,tenant 新增"应用主页"入口卡片 - nav.ts 用 adminSections + getAdminVirtualApp() 替代原 adminApps 占位应用数组 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
c64072f125
commit
2727cfda86
@@ -9,22 +9,23 @@
|
||||
---
|
||||
|
||||
## 四个终端
|
||||
|
||||
| 终端 | key | UI 库 | 主要用户 | 业务定位 |
|
||||
|------|-----|-------|---------|---------|
|
||||
| 综合管理后台 | `admin-portal` | Ant Design Vue | 平台管理员 | 平台级管理(系统设置/运营中心/设备中心 等独立应用) |
|
||||
| 租户运营后台 | `tenant-portal` | Ant Design Vue | 租户运营人员 | **17 个独立可部署应用的统一入口**(营养管理/健康监测/...) |
|
||||
| 综合管理后台 | `admin-portal` | Ant Design Vue | 平台管理员 | 平台级管理(系统设置/运营中心/设备中心 为顶部一级菜单,**无子应用、无应用切换、无应用网格**) |
|
||||
| 租户运营后台 | `tenant-portal` | Ant Design Vue | 租户运营人员 | **17 个独立可部署应用的统一入口**(营养管理/健康监测/...),有应用网格首页 |
|
||||
| C 端微信小程序 | `miniprogram` | Vant 4(375×812) | 终端用户 | 用户自助查询、上报、咨询 |
|
||||
| 硬件终端屏 | `hardware` | 自定义(动态 viewport) | 现场操作员 | 净菜柜屏 / 自助查询屏 / 大屏 等 |
|
||||
|
||||
> **重要架构说明**(对齐生产 vue 项目 `D:\Work\platform-vue-tenant` 的 qiankun 微前端形态):
|
||||
> - `admin-portal` / `tenant-portal` 只是登录门户外壳。下挂多个**独立可部署的应用(NavApp)**,每个应用对应生产 vue 项目中的一个 `subXxx` 子应用。
|
||||
> - 进入某个应用后,顶部显示**该应用内的一级菜单 tabs**(NavSection),左侧显示**当前一级菜单下的二/三级菜单**(NavGroup → NavLeaf)。
|
||||
> - URL 结构:`/<portal>/<app>/<section>/<page>`,例:`/tenant-portal/nutrition/nutrition-farm/monitor/crop`
|
||||
> - 应用首页(`/<portal>`):展示应用网格卡片(参考 vue 项目 main 的 `home.vue`)。
|
||||
> **重要架构说明**(admin / tenant 形态差异):
|
||||
> - **综合管理后台为单应用形态**:3 个一级菜单(系统设置 / 运营中心 / 设备中心)直接挂在 portal 下,URL = `/admin-portal/<section>/<page>`,无应用切换概念。
|
||||
> - **租户运营后台为多应用形态**(对齐生产 vue 项目 `D:\Work\platform-vue-tenant` 的 qiankun 微前端):下挂多个**独立可部署的应用(NavApp)**,每个应用对应生产 vue 项目中的一个 `subXxx` 子应用。
|
||||
> - 进入某个应用后,顶部显示**该应用内的一级菜单 tabs**(NavSection),左侧显示**当前一级菜单下的二/三级菜单**(NavGroup → NavLeaf)
|
||||
> - URL:`/tenant-portal/<app>/<section>/<page>`,例:`/tenant-portal/nutrition/nutrition-farm/monitor/crop`
|
||||
> - 应用首页(`/tenant-portal`):展示应用网格卡片(参考 vue 项目 main 的 `home.vue`)
|
||||
> - 不支持在 TopBar 切换应用;切换应用通过"应用首页"或"返回原型首页"完成
|
||||
> - **最近打开菜单(PageHistory)按 portal + app 隔离**:scope key 形如 `admin-portal__admin` / `tenant-portal__nutrition`;切换应用后只显示当前应用打开过的菜单。
|
||||
|
||||
> 详细业务边界与各应用模块清单见 **`src/docs/business-architecture.md`**。AI 在生成原型前**必须先读这份文档**。
|
||||
|
||||
---
|
||||
|
||||
## 技术栈
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
* 页面访问历史(最近打开的菜单 Tabs)
|
||||
* - 对齐生产 vue 项目 D:\Work\platform-vue-tenant\apps\subNutritionManagement\src\store\pageHistory
|
||||
* - 实现方式:composable + localStorage(项目不引入 Pinia)
|
||||
* - 作用域:每个 portal 独立维护一份历史(key: pageHistory.<portalKey>)
|
||||
* - 作用域:按 <portal>__<app> 隔离一份历史
|
||||
* · admin-portal__admin
|
||||
* · tenant-portal__nutrition / tenant-portal__monitor / ...
|
||||
* 切换应用后只显示当前应用打开过的菜单
|
||||
*/
|
||||
|
||||
import type { Router } from 'vue-router'
|
||||
@@ -23,44 +26,44 @@ export interface PageHistoryItem {
|
||||
|
||||
const STORAGE_KEY_PREFIX = 'pageHistory.'
|
||||
|
||||
const readFromStorage = (portalKey: string): PageHistoryItem[] => {
|
||||
const readFromStorage = (scopeKey: string): PageHistoryItem[] => {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY_PREFIX + portalKey)
|
||||
const raw = localStorage.getItem(STORAGE_KEY_PREFIX + scopeKey)
|
||||
return raw ? (JSON.parse(raw) as PageHistoryItem[]) : []
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
const writeToStorage = (portalKey: string, list: PageHistoryItem[]): void => {
|
||||
localStorage.setItem(STORAGE_KEY_PREFIX + portalKey, JSON.stringify(list))
|
||||
const writeToStorage = (scopeKey: string, list: PageHistoryItem[]): void => {
|
||||
localStorage.setItem(STORAGE_KEY_PREFIX + scopeKey, JSON.stringify(list))
|
||||
}
|
||||
|
||||
/** 各 portal 的响应式历史列表 */
|
||||
/** 各 scope 的响应式历史列表 */
|
||||
const stores = new Map<string, ReturnType<typeof ref<PageHistoryItem[]>>>()
|
||||
|
||||
const getStore = (portalKey: string) => {
|
||||
let s = stores.get(portalKey)
|
||||
const getStore = (scopeKey: string) => {
|
||||
let s = stores.get(scopeKey)
|
||||
if (!s) {
|
||||
s = ref<PageHistoryItem[]>(readFromStorage(portalKey))
|
||||
stores.set(portalKey, s)
|
||||
s = ref<PageHistoryItem[]>(readFromStorage(scopeKey))
|
||||
stores.set(scopeKey, s)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用页面历史(无数量上限,对齐 vue 项目)
|
||||
* @param portalKey 'admin-portal' | 'tenant-portal' 等
|
||||
* @param scopeKey 形如 'admin-portal__admin' / 'tenant-portal__nutrition'
|
||||
*/
|
||||
export const usePageHistory = (portalKey: string) => {
|
||||
const list = getStore(portalKey)
|
||||
export const usePageHistory = (scopeKey: string) => {
|
||||
const list = getStore(scopeKey)
|
||||
|
||||
/** 新增页面(已存在则前移到末尾) */
|
||||
const addPage = (item: PageHistoryItem): void => {
|
||||
const arr = (list.value ?? []).filter(i => i.path !== item.path)
|
||||
arr.push(item)
|
||||
list.value = arr
|
||||
writeToStorage(portalKey, arr)
|
||||
writeToStorage(scopeKey, arr)
|
||||
}
|
||||
|
||||
/** 移除指定路径 */
|
||||
@@ -70,7 +73,7 @@ export const usePageHistory = (portalKey: string) => {
|
||||
if (idx === -1) return null
|
||||
arr.splice(idx, 1)
|
||||
list.value = [...arr]
|
||||
writeToStorage(portalKey, list.value)
|
||||
writeToStorage(scopeKey, list.value)
|
||||
// 返回下一个应跳转的路径(关闭当前页时用)
|
||||
return list.value[list.value.length - 1]?.path ?? null
|
||||
}
|
||||
@@ -78,21 +81,29 @@ export const usePageHistory = (portalKey: string) => {
|
||||
/** 关闭除指定路径外的所有 */
|
||||
const closeOthers = (path: string): void => {
|
||||
list.value = (list.value ?? []).filter(i => i.path === path)
|
||||
writeToStorage(portalKey, list.value)
|
||||
writeToStorage(scopeKey, list.value)
|
||||
}
|
||||
|
||||
/** 清空 */
|
||||
const clear = (): void => {
|
||||
list.value = []
|
||||
writeToStorage(portalKey, [])
|
||||
writeToStorage(scopeKey, [])
|
||||
}
|
||||
|
||||
return { list, addPage, removePage, closeOthers, clear }
|
||||
}
|
||||
|
||||
/**
|
||||
* 在 router.afterEach 钩子里自动追踪某个 portal 的访问
|
||||
* - 仅当路由 meta.portal 匹配时记录
|
||||
* 计算路由对应的历史 scope key
|
||||
* - admin 路由 meta.app 固定为 'admin'
|
||||
* - tenant 路由 meta.app 为具体应用 key(nutrition / monitor / ...)
|
||||
*/
|
||||
const resolveScopeKey = (portal: string, app: string | undefined): string =>
|
||||
`${portal}__${app ?? 'default'}`
|
||||
|
||||
/**
|
||||
* 在 router.afterEach 钩子里自动追踪访问
|
||||
* - 仅当路由 meta.portal 存在时记录
|
||||
* - 忽略应用网格首页 / 重定向路由
|
||||
*/
|
||||
export const installPageHistoryTracker = (router: Router): void => {
|
||||
@@ -101,11 +112,13 @@ export const installPageHistoryTracker = (router: Router): void => {
|
||||
if (!portal) return
|
||||
if (to.meta?.isAppGrid) return
|
||||
if (!to.name) return
|
||||
const { addPage } = usePageHistory(portal)
|
||||
const app = to.meta?.app as string | undefined
|
||||
const scopeKey = resolveScopeKey(portal, app)
|
||||
const { addPage } = usePageHistory(scopeKey)
|
||||
addPage({
|
||||
path: to.path,
|
||||
title: String(to.meta?.label ?? to.name ?? to.path),
|
||||
app: to.meta?.app as string | undefined,
|
||||
app,
|
||||
section: to.meta?.section as string | undefined,
|
||||
leafKey: String(to.name),
|
||||
})
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
> **作用**:这份文档供 AI 在生成原型时参考,确保理解业务上下文、不串端、不重复造功能。
|
||||
> **维护规则**:用户在规划每个端的具体功能模块时填写本文档,AI 在 new-page / gen-prd / cross-role-doc-builder 等任务中**必须先读这份文档**。
|
||||
> _Last updated: 2026-06-04 / 阶段 16(用户提供四端粗粒度功能清单)_
|
||||
> _Last updated: 2026-06-05 / 阶段 17(综合后台扁平化、租户后台去应用切换、历史按 portal+app 隔离)_
|
||||
|
||||
---
|
||||
|
||||
@@ -10,21 +10,32 @@
|
||||
|
||||
### 1.1 综合管理后台 `admin-portal`
|
||||
|
||||
| 子端 | 一级模块 | 说明 |
|
||||
|------|---------|------|
|
||||
| 系统设置 | (待规划) | 平台层基础设置:组织、账号、权限、字典、参数 |
|
||||
| 运营中心 | (待规划) | 平台运营视角:租户管理、应用配置、计费、统计 |
|
||||
| 设备中心 | (待规划) | 跨租户硬件接入与运维:设备注册、固件版本、在线状态 |
|
||||
> **形态**:单应用形态。**没有子应用概念**,3 个一级菜单(系统设置 / 运营中心 / 设备中心)直接挂在 portal 下。
|
||||
> **URL 模式**:`/admin-portal/<section>/<page>`
|
||||
> **UI 表现**:左上角显示固定标题"综合管理后台",**无应用切换下拉**、**无应用网格首页**、**无"返回应用首页"快捷入口**;顶部一级菜单 tabs 即三大模块。
|
||||
|
||||
| 一级模块 | 说明 |
|
||||
|---------|------|
|
||||
| 系统设置 | 平台层基础设置:组织、账号、权限、字典、参数 |
|
||||
| 运营中心 | 平台运营视角:租户管理、应用配置、计费、统计 |
|
||||
| 设备中心 | 跨租户硬件接入与运维:设备注册、固件版本、在线状态 |
|
||||
|
||||
> 综合后台是**跨租户**视角;与租户运营后台的边界:凡是只能在租户内部生效的业务(如某租户的食堂菜单),归属租户后台。
|
||||
|
||||
### 1.2 租户运营后台 `tenant-portal`
|
||||
|
||||
| 一级模块 | 说明 |
|
||||
> **形态**:多应用形态(对齐生产 vue 项目 qiankun 微前端,每个应用对应一个独立可部署的 subXxx)。
|
||||
> **URL 模式**:`/tenant-portal/<app>/<section>/<page>`;`/tenant-portal` 为应用网格首页。
|
||||
> **UI 表现**:
|
||||
> - `/tenant-portal` 为应用网格首页(无侧栏 / 无顶部一级菜单),展示 17 个应用卡片
|
||||
> - 进入应用后,左上角显示当前应用名(纯文本,**不可点击切换应用**),顶部一级菜单 tabs = 当前应用内的 sections
|
||||
> - 右上角提供"应用首页"快捷入口回到应用网格、"返回原型首页"回到全局导航
|
||||
> - 切换应用只能通过"应用首页"或"返回原型首页"
|
||||
|
||||
| 一级模块(应用) | 说明 |
|
||||
|---------|------|
|
||||
| 租户首页 | 单租户运营视角的首页:核心 KPI、待办、快捷入口 |
|
||||
| 控制台 | 租户运营的控制中心:组织架构、岗位、人员、流程 |
|
||||
| 系统管理 | 租户级系统管理:账号、角色、字典、日志、消息模板 |
|
||||
| 控制台 | 租户运营驾驶舱:核心 KPI、待办、快捷入口 |
|
||||
| 系统管理 | 租户级系统管理:组织、账号、角色、字典、日志、消息模板 |
|
||||
| 营养管理 | 食堂全链路:种植、采购、加工、菜品、食谱、订餐、出餐、质检 |
|
||||
| 健康体检 | 体检计划、机构、项目、报告、统计 |
|
||||
| 体重管理 | 体重档案、采集、目标、干预、提醒 |
|
||||
|
||||
@@ -6,11 +6,10 @@
|
||||
:current-app="isAppGrid ? undefined : currentApp"
|
||||
:sections="isAppGrid ? [] : currentAppSections"
|
||||
:current-section="currentSectionKey"
|
||||
:show-app-grid="!isAppGrid"
|
||||
:show-app-grid="!isAppGrid && variant === 'tenant'"
|
||||
:app-grid-path="portalPath"
|
||||
:portal-title="portalTitle"
|
||||
@section-change="onSectionChange"
|
||||
@app-change="onAppChange"
|
||||
@search="onSearch"
|
||||
>
|
||||
<template #extra>
|
||||
@@ -19,7 +18,7 @@
|
||||
</TopBar>
|
||||
|
||||
<a-layout class="portal-layout__body">
|
||||
<!-- 应用网格首页:不显示侧边栏 / 不显示 PageHistory -->
|
||||
<!-- 应用网格首页(仅 tenant):不显示侧边栏 / 不显示 PageHistory -->
|
||||
<a-layout-content v-if="isAppGrid" class="portal-layout__content portal-layout__content--full">
|
||||
<router-view />
|
||||
</a-layout-content>
|
||||
@@ -30,7 +29,7 @@
|
||||
<Sidebar :section="currentSection" :active-key="currentLeafKey" @navigate="onLeafClick" />
|
||||
</a-layout-sider>
|
||||
<a-layout class="portal-layout__main">
|
||||
<PageHistory :portal-key="portalKey" />
|
||||
<PageHistory :portal-key="historyScopeKey" />
|
||||
<a-layout-content class="portal-layout__content">
|
||||
<router-view />
|
||||
</a-layout-content>
|
||||
@@ -44,13 +43,10 @@
|
||||
/**
|
||||
* 后台门户布局
|
||||
* - 综合管理后台(admin)与租户运营后台(tenant)共用此 Layout
|
||||
* - 通过 props.variant 切换菜单数据源、顶部副标题与颜色
|
||||
* - 两种形态:
|
||||
* · admin:单应用形态,URL = /admin-portal/<section>/<page>,无应用网格、无应用切换
|
||||
* · tenant:多应用形态,URL = /tenant-portal/<app>/<section>/<page>,有应用网格首页
|
||||
* - 自动按当前角色 + 控制台覆写过滤菜单
|
||||
*
|
||||
* 层级(对齐 vue 项目):
|
||||
* Portal -> NavApp (应用) -> NavSection (一级菜单) -> NavGroup/NavLeaf -> NavLeaf
|
||||
*
|
||||
* URL:/<portal>/<app>/<section>/<page>
|
||||
*/
|
||||
import { getPortalApps, isNavGroup, type NavApp, type NavSection, type NavLeaf } from '@meta/nav'
|
||||
import { useVisibility, filterApps } from '@composables/useVisibility'
|
||||
@@ -66,7 +62,7 @@ const { role, overrides } = useVisibility()
|
||||
/** 当前 portal 的 routerPath 前缀 */
|
||||
const portalPath = computed(() => props.variant === 'admin' ? '/admin-portal' : '/tenant-portal')
|
||||
|
||||
/** 当前 portal key(用于 PageHistory 隔离) */
|
||||
/** 当前 portal key(用于 meta) */
|
||||
const portalKey = computed(() => props.variant === 'admin' ? 'admin-portal' : 'tenant-portal')
|
||||
|
||||
/** 当前 portal 的应用列表(按角色 + 覆写过滤) */
|
||||
@@ -76,18 +72,29 @@ const apps = computed<NavApp[]>(() => {
|
||||
return filterApps(all, effectiveRole, overrides.value)
|
||||
})
|
||||
|
||||
/** 是否处于应用网格首页(路由 meta.isAppGrid) */
|
||||
const isAppGrid = computed<boolean>(() => Boolean(route.meta.isAppGrid))
|
||||
/**
|
||||
* 是否处于应用网格首页
|
||||
* - admin:永远 false(无应用网格)
|
||||
* - tenant:路由 meta.isAppGrid
|
||||
*/
|
||||
const isAppGrid = computed<boolean>(() =>
|
||||
props.variant === 'tenant' && Boolean(route.meta.isAppGrid),
|
||||
)
|
||||
|
||||
/** 顶部 portal 标题(用于应用网格首页 logo 区) */
|
||||
const portalTitle = computed(() =>
|
||||
props.variant === 'admin' ? '综合管理后台' : '租户运营后台',
|
||||
)
|
||||
|
||||
/** 当前激活应用 key(从 route.meta 取,回退到第一个应用) */
|
||||
const currentAppKey = computed<string>(() =>
|
||||
(route.meta.app as string) ?? apps.value[0]?.key ?? '',
|
||||
)
|
||||
/**
|
||||
* 当前激活应用 key
|
||||
* - admin:固定为虚拟 app 的 key 'admin'
|
||||
* - tenant:从 route.meta 取
|
||||
*/
|
||||
const currentAppKey = computed<string>(() => {
|
||||
if (props.variant === 'admin') return 'admin'
|
||||
return (route.meta.app as string) ?? apps.value[0]?.key ?? ''
|
||||
})
|
||||
|
||||
/** 当前激活应用对象 */
|
||||
const currentApp = computed<NavApp | undefined>(() =>
|
||||
@@ -110,6 +117,13 @@ const currentSection = computed<NavSection | undefined>(() =>
|
||||
/** 当前激活叶子页面 key(即路由 name) */
|
||||
const currentLeafKey = computed<string>(() => (route.name as string) ?? '')
|
||||
|
||||
/**
|
||||
* 历史隔离 scope key:portal + app
|
||||
* - admin:'admin-portal__admin'
|
||||
* - tenant:'tenant-portal__<appKey>'
|
||||
*/
|
||||
const historyScopeKey = computed(() => `${portalKey.value}__${currentAppKey.value}`)
|
||||
|
||||
/** 找一个 section 的第一个叶子 */
|
||||
const firstLeafOfSection = (section: NavSection): NavLeaf | undefined => {
|
||||
const first = section.children[0]
|
||||
@@ -117,41 +131,39 @@ const firstLeafOfSection = (section: NavSection): NavLeaf | undefined => {
|
||||
return isNavGroup(first) ? first.children[0] : first
|
||||
}
|
||||
|
||||
/** 找一个 app 的第一个叶子(用于切换应用时跳转) */
|
||||
const firstLeafOfApp = (app: NavApp): { section: NavSection; leaf: NavLeaf } | undefined => {
|
||||
for (const s of app.children) {
|
||||
const leaf = firstLeafOfSection(s)
|
||||
if (leaf) return { section: s, leaf }
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/** 切换顶部一级菜单:跳到该 section 第一个页面 */
|
||||
/**
|
||||
* 切换顶部一级菜单:跳到该 section 第一个页面
|
||||
* - admin:/admin-portal/<section>/<page>
|
||||
* - tenant:/tenant-portal/<app>/<section>/<page>
|
||||
*/
|
||||
const onSectionChange = (sectionKey: string) => {
|
||||
const section = currentAppSections.value.find(s => s.key === sectionKey)
|
||||
if (!section) return
|
||||
const leaf = firstLeafOfSection(section)
|
||||
if (!leaf) return
|
||||
void router.push(`${portalPath.value}/${currentAppKey.value}/${section.key}/${leaf.path}`)
|
||||
}
|
||||
|
||||
/** 切换应用:跳到该应用第一个页面 */
|
||||
const onAppChange = (appKey: string) => {
|
||||
const app = apps.value.find(a => a.key === appKey)
|
||||
if (!app) return
|
||||
const entry = firstLeafOfApp(app)
|
||||
if (!entry) return
|
||||
void router.push(`${portalPath.value}/${app.key}/${entry.section.key}/${entry.leaf.path}`)
|
||||
if (props.variant === 'admin') {
|
||||
void router.push(`${portalPath.value}/${section.key}/${leaf.path}`)
|
||||
} else {
|
||||
void router.push(`${portalPath.value}/${currentAppKey.value}/${section.key}/${leaf.path}`)
|
||||
}
|
||||
}
|
||||
|
||||
/** 点击侧边栏叶子:跳路由 */
|
||||
const onLeafClick = (leaf: NavLeaf) => {
|
||||
void router.push(`${portalPath.value}/${currentAppKey.value}/${currentSectionKey.value}/${leaf.path}`)
|
||||
if (props.variant === 'admin') {
|
||||
void router.push(`${portalPath.value}/${currentSectionKey.value}/${leaf.path}`)
|
||||
} else {
|
||||
void router.push(`${portalPath.value}/${currentAppKey.value}/${currentSectionKey.value}/${leaf.path}`)
|
||||
}
|
||||
}
|
||||
|
||||
/** 顶部搜索菜单:直接跳到目标叶子 */
|
||||
const onSearch = (leaf: NavLeaf, section: NavSection) => {
|
||||
void router.push(`${portalPath.value}/${currentAppKey.value}/${section.key}/${leaf.path}`)
|
||||
if (props.variant === 'admin') {
|
||||
void router.push(`${portalPath.value}/${section.key}/${leaf.path}`)
|
||||
} else {
|
||||
void router.push(`${portalPath.value}/${currentAppKey.value}/${section.key}/${leaf.path}`)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -84,6 +84,7 @@ import {
|
||||
import type { MenuProps } from 'ant-design-vue'
|
||||
import { usePageHistory } from '@composables/usePageHistory'
|
||||
|
||||
/** props.portalKey 实际为 usePageHistory 的 scopeKey(形如 'admin-portal__admin' / 'tenant-portal__nutrition') */
|
||||
const props = defineProps<{ portalKey: string }>()
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -1,23 +1,9 @@
|
||||
<template>
|
||||
<header class="top-bar">
|
||||
<!-- 左侧 Logo + 应用图标 + 应用名(对齐 vue 项目 subXxx layouts/header) -->
|
||||
<!-- 左侧 Logo + 应用图标 + 应用名(纯文本,不可切换) -->
|
||||
<div class="top-bar__logo">
|
||||
<span class="top-bar__app-icon">{{ logoIcon }}</span>
|
||||
<a-dropdown v-if="apps.length > 1 && currentApp" :trigger="['click']">
|
||||
<span class="top-bar__app-name">
|
||||
{{ currentApp.label }}
|
||||
<DownOutlined class="top-bar__app-arrow" />
|
||||
</span>
|
||||
<template #overlay>
|
||||
<a-menu @click="(info) => emit('appChange', String(info.key))">
|
||||
<a-menu-item v-for="app in apps" :key="app.key">
|
||||
<span v-if="app.icon" class="app-switch-icon">{{ app.icon }}</span>
|
||||
<span>{{ app.label }}</span>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
<span v-else class="top-bar__app-name">{{ currentApp?.label ?? portalTitle ?? '租户运营' }}</span>
|
||||
<span class="top-bar__app-name">{{ currentApp?.label ?? portalTitle ?? '租户运营' }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 中部:应用内一级菜单(横向 a-menu, light 主题白底) -->
|
||||
@@ -70,7 +56,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { MenuProps, CascaderProps } from 'ant-design-vue'
|
||||
import { DownOutlined, HomeOutlined, AppstoreOutlined } from '@ant-design/icons-vue'
|
||||
import { HomeOutlined, AppstoreOutlined } from '@ant-design/icons-vue'
|
||||
import { isNavGroup, type NavApp, type NavLeaf, type NavSection, type NavGroup } from '@meta/nav'
|
||||
|
||||
/**
|
||||
@@ -78,13 +64,16 @@ import { isNavGroup, type NavApp, type NavLeaf, type NavSection, type NavGroup }
|
||||
* 对齐 vue 项目 D:\Work\platform-vue-tenant\apps\subNutritionManagement\src\layouts\header\index.vue
|
||||
*
|
||||
* 视觉规范:
|
||||
* - 左侧 Logo 区:深蓝 #001529,宽 220px,显示应用图标 + 应用名 ▾
|
||||
* - 左侧 Logo 区:深蓝 #001529,宽 200px,显示应用图标 + 应用名(纯文本,不可点击切换)
|
||||
* - 中部一级菜单:a-menu mode=horizontal theme=light(白底)
|
||||
* - 菜单搜索:a-cascader(应用内所有菜单的两级联动)
|
||||
* - 右侧:返回首页 + 应用首页 + 用户区
|
||||
* - 右侧:返回首页 + 应用首页(仅 tenant 应用内显示)+ 用户区
|
||||
*
|
||||
* 注:应用切换不在 TopBar 提供,统一通过"返回应用首页"或"返回原型首页"完成;
|
||||
* 综合管理后台无应用概念,左上角显示 portal 标题。
|
||||
*/
|
||||
const props = defineProps<{
|
||||
/** 当前 portal 下所有应用(已按角色过滤) */
|
||||
/** 当前 portal 下所有应用(已按角色过滤;仅用于菜单搜索数据源,不再用于切换) */
|
||||
apps: NavApp[]
|
||||
/** 当前激活应用(应用网格首页时为 undefined) */
|
||||
currentApp?: NavApp
|
||||
@@ -92,7 +81,7 @@ const props = defineProps<{
|
||||
sections: NavApp['children']
|
||||
/** 当前激活一级菜单 key */
|
||||
currentSection: string
|
||||
/** 是否显示"应用首页"快捷入口 */
|
||||
/** 是否显示"应用首页"快捷入口(仅租户应用内显示) */
|
||||
showAppGrid?: boolean
|
||||
/** 应用网格首页路径 */
|
||||
appGridPath?: string
|
||||
@@ -102,7 +91,6 @@ const props = defineProps<{
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'sectionChange', sectionKey: string): void
|
||||
(e: 'appChange', appKey: string): void
|
||||
(e: 'search', leaf: NavLeaf, section: NavSection): void
|
||||
}>()
|
||||
|
||||
|
||||
+76
-15
@@ -4,20 +4,22 @@
|
||||
* - 新增页面只改这一处,路由 + 菜单同步生效
|
||||
*
|
||||
* 四个终端:
|
||||
* - admin-portal 综合管理后台(AntDV 后台 UI)
|
||||
* - admin-portal 综合管理后台(AntDV 后台 UI;单应用形态,3 个一级菜单:系统设置 / 运营中心 / 设备中心)
|
||||
* - tenant-portal 租户运营后台(AntDV 后台 UI;下挂 17 个独立应用)
|
||||
* - miniprogram C 端微信小程序(Vant,375×812;吃 / 问 / 做 / 我)
|
||||
* - hardware 硬件终端屏(按设备类型动态 viewport;10 类设备)
|
||||
*
|
||||
* 后台菜单层级(对齐生产 vue 项目 D:\Work\platform-vue-tenant):
|
||||
* Portal (终端)
|
||||
* └─ NavApp (应用,对应 vue 项目的 subXxx 子应用,可独立部署)
|
||||
* └─ NavSection (应用内一级菜单,对应顶部横向 tabs)
|
||||
* └─ NavGroup (二级分组,可选) 或 NavLeaf (二级菜单/页面)
|
||||
* └─ NavLeaf (三级页面)
|
||||
* · admin-portal(无子应用):
|
||||
* Portal → NavSection (一级菜单) → NavLeaf / NavGroup → NavLeaf
|
||||
* URL:/admin-portal/<section>/<page>
|
||||
* · tenant-portal(多应用形态):
|
||||
* Portal → NavApp (应用,对应 vue 项目的 subXxx 子应用) → NavSection (一级菜单)
|
||||
* → NavGroup (可选) → NavLeaf
|
||||
* URL:/tenant-portal/<app>/<section>/<page>
|
||||
*
|
||||
* 图标规范:
|
||||
* - NavApp.icon:应用图标,可自定义 emoji
|
||||
* - NavApp.icon:应用图标,可自定义 emoji(仅 tenant 应用网格 / TopBar 左上角显示)
|
||||
* - NavSection / NavGroup / NavLeaf 的 icon:**必须**使用 ant-design-vue 4.2 内置图标组件名
|
||||
* (例:'FileTextOutlined' / 'AppstoreOutlined' / 'BarChartOutlined')
|
||||
* - 渲染:layouts 用 `<component :is="icon" />` 动态渲染
|
||||
@@ -161,15 +163,70 @@ const makePlaceholderApp = (
|
||||
],
|
||||
})
|
||||
|
||||
// =========================== 综合管理后台 ===========================
|
||||
// =========================== 综合管理后台(单应用形态) ===========================
|
||||
|
||||
/** 综合管理后台应用列表(粗粒度占位) */
|
||||
export const adminApps: NavApp[] = [
|
||||
makePlaceholderApp('system-settings', '系统设置', '⚙️', '平台级用户、角色、权限、字典', 'system'),
|
||||
makePlaceholderApp('operation-center', '运营中心', '📊', '租户管理、版本管理、计费', 'system'),
|
||||
makePlaceholderApp('device-center', '设备中心', '🖥️', '硬件设备注册、监控、运维', 'system'),
|
||||
/**
|
||||
* 综合管理后台占位叶子
|
||||
* - 与 tenant 的 placeholder 工厂分开,避免误读为"占位应用"
|
||||
*/
|
||||
const adminPlaceholderLeaf = (
|
||||
key: string,
|
||||
label: string,
|
||||
path: string,
|
||||
): NavLeaf => ({
|
||||
key,
|
||||
label,
|
||||
path,
|
||||
status: 'draft',
|
||||
component: PlaceholderImport,
|
||||
})
|
||||
|
||||
/**
|
||||
* 综合管理后台一级菜单(顶部横向 tabs)
|
||||
* - admin-portal 没有子应用概念,3 个一级菜单直接挂在 portal 下
|
||||
* - URL:/admin-portal/<section>/<page>
|
||||
*/
|
||||
export const adminSections: NavSection[] = [
|
||||
{
|
||||
key: 'system-settings',
|
||||
label: '系统设置',
|
||||
icon: 'SettingOutlined',
|
||||
children: [
|
||||
adminPlaceholderLeaf('admin-system-settings-overview', '系统设置首页', 'overview'),
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'operation-center',
|
||||
label: '运营中心',
|
||||
icon: 'FundProjectionScreenOutlined',
|
||||
children: [
|
||||
adminPlaceholderLeaf('admin-operation-center-overview', '运营中心首页', 'overview'),
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'device-center',
|
||||
label: '设备中心',
|
||||
icon: 'DesktopOutlined',
|
||||
children: [
|
||||
adminPlaceholderLeaf('admin-device-center-overview', '设备中心首页', 'overview'),
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
* 综合管理后台的虚拟 NavApp 包装
|
||||
* - 仅供路由/布局内部消费(PortalLayout 拿到一个 NavApp 才能复用现有的 sections 渲染逻辑)
|
||||
* - 不对外暴露"应用"语义,不参与租户应用网格 / 应用切换
|
||||
*/
|
||||
export const getAdminVirtualApp = (): NavApp => ({
|
||||
key: 'admin',
|
||||
label: '综合管理后台',
|
||||
icon: '🛠️',
|
||||
description: '平台级管理(系统设置 / 运营中心 / 设备中心)',
|
||||
category: 'system',
|
||||
children: adminSections,
|
||||
})
|
||||
|
||||
// =========================== 租户运营后台 ===========================
|
||||
|
||||
/**
|
||||
@@ -679,7 +736,11 @@ export const isNavGroup = (item: NavLeaf | NavGroup): item is NavGroup => {
|
||||
|
||||
// =========================== 工具:按 Portal 取数 ===========================
|
||||
|
||||
/** 按 portalKey 取得对应的应用数组(admin / tenant) */
|
||||
/**
|
||||
* 按 portalKey 取得对应的应用数组(admin / tenant)
|
||||
* - admin:返回一个虚拟 NavApp 包裹 adminSections(admin 无子应用概念,包装仅为复用 NavApp 渲染逻辑)
|
||||
* - tenant:返回真实的 17 个独立应用
|
||||
*/
|
||||
export const getPortalApps = (key: 'admin-portal' | 'tenant-portal'): NavApp[] => {
|
||||
return key === 'admin-portal' ? adminApps : tenantApps
|
||||
return key === 'admin-portal' ? [getAdminVirtualApp()] : tenantApps
|
||||
}
|
||||
|
||||
@@ -93,11 +93,12 @@
|
||||
*/
|
||||
import type { TableColumnsType } from 'ant-design-vue'
|
||||
import {
|
||||
adminApps,
|
||||
getPortalApps,
|
||||
tenantApps,
|
||||
miniprogramTabs,
|
||||
hardwareDevices,
|
||||
isNavGroup,
|
||||
type NavApp,
|
||||
type NavLeaf,
|
||||
type PortalKey,
|
||||
} from '@meta/nav'
|
||||
@@ -141,7 +142,7 @@ const allLeaves = computed<Row[]>(() => {
|
||||
})
|
||||
}
|
||||
|
||||
const pushApps = (apps: typeof adminApps, portal: PortalKey) => {
|
||||
const pushApps = (apps: NavApp[], portal: PortalKey) => {
|
||||
apps.forEach((app) => {
|
||||
app.children.forEach((section) => {
|
||||
section.children.forEach((item) => {
|
||||
@@ -156,7 +157,7 @@ const allLeaves = computed<Row[]>(() => {
|
||||
})
|
||||
}
|
||||
|
||||
pushApps(adminApps, 'admin-portal')
|
||||
pushApps(getPortalApps('admin-portal'), 'admin-portal')
|
||||
pushApps(tenantApps, 'tenant-portal')
|
||||
miniprogramTabs.forEach((tab) => {
|
||||
push(tab.page, 'miniprogram', tab.label)
|
||||
|
||||
+182
-19
@@ -31,8 +31,79 @@
|
||||
<!-- 内容区 -->
|
||||
<main class="home__main">
|
||||
|
||||
<!-- 综合管理后台 / 租户运营后台 内容 -->
|
||||
<div v-if="activeKey === 'admin-portal' || activeKey === 'tenant-portal'" class="module-grid">
|
||||
<!-- 综合管理后台:一级菜单卡片(无应用概念) -->
|
||||
<div v-if="activeKey === 'admin-portal'" class="module-grid">
|
||||
<div
|
||||
v-for="card in adminSectionCards"
|
||||
:key="card.key"
|
||||
class="module-card"
|
||||
:class="{
|
||||
'module-card--ready': card.pageCount > 0 && card.hasReady,
|
||||
'module-card--wip': card.pageCount > 0 && !card.hasReady,
|
||||
'module-card--empty': card.pageCount === 0,
|
||||
}"
|
||||
@click="enterAdminSection(card)"
|
||||
>
|
||||
<div class="module-card__watermark" v-if="card.pageCount === 0">
|
||||
<div class="module-card__watermark-text">待开发</div>
|
||||
</div>
|
||||
|
||||
<div class="module-card__header">
|
||||
<div class="module-card__name">{{ card.label }}</div>
|
||||
<span
|
||||
class="module-card__status"
|
||||
:class="[card.hasReady ? 'module-card__status--done' : 'module-card__status--wip']"
|
||||
v-if="card.pageCount > 0"
|
||||
>
|
||||
{{ card.hasReady ? '设计中' : '待开发' }}
|
||||
</span>
|
||||
<span class="module-card__status module-card__status--pending" v-else>未开始</span>
|
||||
</div>
|
||||
|
||||
<div class="module-card__bottom">
|
||||
<a class="module-card__prd" @click.stop="openPrd(card.key)">
|
||||
📄 PRD
|
||||
</a>
|
||||
<span class="module-card__page-count" @click.stop="togglePages(card.key)">
|
||||
{{ card.pageCount }} 个页面
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 悬浮页面清单 -->
|
||||
<div v-if="openedPagesKey === card.key" class="page-popover page-popover--visible">
|
||||
<div class="page-popover__title">{{ card.label }} · 页面清单</div>
|
||||
<a
|
||||
v-for="leaf in card.leaves"
|
||||
:key="leaf.key"
|
||||
class="page-popover__item"
|
||||
@click.stop="enterAdminLeaf(card.key, leaf)"
|
||||
>
|
||||
📄 {{ leaf.label }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 租户运营后台:应用主页卡片 + 各应用卡片 -->
|
||||
<div v-else-if="activeKey === 'tenant-portal'" class="module-grid">
|
||||
<!-- 应用主页卡片(首张) -->
|
||||
<div
|
||||
class="module-card module-card--home"
|
||||
@click="enterTenantAppGrid"
|
||||
>
|
||||
<div class="module-card__header">
|
||||
<div class="module-card__name">
|
||||
<span class="module-card__emoji">🏠</span>
|
||||
应用主页
|
||||
</div>
|
||||
<span class="module-card__status module-card__status--done">入口</span>
|
||||
</div>
|
||||
<div class="module-card__bottom">
|
||||
<span class="module-card__hint">租户应用网格首页(含全部应用卡片)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 各应用卡片 -->
|
||||
<div
|
||||
v-for="card in portalCards"
|
||||
:key="card.key"
|
||||
@@ -76,7 +147,7 @@
|
||||
v-for="leaf in card.leaves"
|
||||
:key="leaf.key"
|
||||
class="page-popover__item"
|
||||
@click.stop="enterLeaf(activeKey, card.key, leaf)"
|
||||
@click.stop="enterLeaf('tenant-portal', card.key, leaf)"
|
||||
>
|
||||
📄 {{ leaf.label }}
|
||||
</a>
|
||||
@@ -194,12 +265,14 @@
|
||||
*/
|
||||
import { useRouter } from 'vue-router'
|
||||
import {
|
||||
adminApps,
|
||||
adminSections,
|
||||
tenantApps,
|
||||
miniprogramTabs,
|
||||
hardwareDevices,
|
||||
isNavGroup,
|
||||
type NavApp,
|
||||
type NavSection,
|
||||
type NavGroup,
|
||||
type NavLeaf,
|
||||
type NavMiniprogramTab,
|
||||
type NavHardwareDevice,
|
||||
@@ -217,21 +290,30 @@ const { role, overrides } = useVisibility()
|
||||
const effectiveRole = computed(() => role.value ?? 'designer')
|
||||
|
||||
/** 过滤四端数据 */
|
||||
const filteredAdmin = computed(() => filterApps(adminApps, effectiveRole.value, overrides.value))
|
||||
const filteredTenant = computed(() => filterApps(tenantApps, effectiveRole.value, overrides.value))
|
||||
const filteredMpTabs = computed(() => filterMiniprogramTabs(miniprogramTabs, effectiveRole.value, overrides.value))
|
||||
const filteredHardware = computed(() => filterHardwareDevices(hardwareDevices, effectiveRole.value, overrides.value))
|
||||
|
||||
/** 综合管理后台一级菜单(admin 无子应用,直接展示 sections) */
|
||||
const adminAllSections = computed<NavSection[]>(() => adminSections)
|
||||
|
||||
/** 一个 section 下所有叶子(含 group 内的) */
|
||||
const flattenSectionLeaves = (section: NavSection): NavLeaf[] => {
|
||||
const out: NavLeaf[] = []
|
||||
section.children.forEach((c) => {
|
||||
if (isNavGroup(c)) out.push(...(c as NavGroup).children)
|
||||
else out.push(c as NavLeaf)
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
// ============================== Tab 计数 ==============================
|
||||
|
||||
/** 把一个应用的所有叶子展平(含分组下的)— 一个应用通常有多个 section,再嵌套 group/leaf */
|
||||
const flattenAppLeaves = (app: NavApp): NavLeaf[] => {
|
||||
const out: NavLeaf[] = []
|
||||
app.children.forEach((section) => {
|
||||
section.children.forEach((c) => {
|
||||
if (isNavGroup(c)) out.push(...c.children)
|
||||
else out.push(c)
|
||||
})
|
||||
out.push(...flattenSectionLeaves(section))
|
||||
})
|
||||
return out
|
||||
}
|
||||
@@ -239,8 +321,11 @@ const flattenAppLeaves = (app: NavApp): NavLeaf[] => {
|
||||
const countAppPages = (apps: NavApp[]): number =>
|
||||
apps.reduce((sum, a) => sum + flattenAppLeaves(a).length, 0)
|
||||
|
||||
const countSectionPages = (sections: NavSection[]): number =>
|
||||
sections.reduce((sum, s) => sum + flattenSectionLeaves(s).length, 0)
|
||||
|
||||
const tabs = computed(() => [
|
||||
{ key: 'admin-portal', label: '综合管理后台', icon: '🛠️', count: countAppPages(filteredAdmin.value) },
|
||||
{ key: 'admin-portal', label: '综合管理后台', icon: '🛠️', count: countSectionPages(adminAllSections.value) },
|
||||
{ key: 'tenant-portal', label: '租户运营后台', icon: '💼', count: countAppPages(filteredTenant.value) },
|
||||
{ key: 'miniprogram', label: '微信小程序', icon: '📱',
|
||||
count: filteredMpTabs.value.reduce((s, t) => s + 1 + (t.subPages?.length ?? 0), 0) },
|
||||
@@ -264,16 +349,15 @@ interface AppCard {
|
||||
leaves: NavLeaf[]
|
||||
}
|
||||
|
||||
/** 租户运营后台:应用卡片 */
|
||||
const portalCards = computed<AppCard[]>(() => {
|
||||
const apps = activeKey.value === 'admin-portal' ? filteredAdmin.value : filteredTenant.value
|
||||
return apps.map<AppCard>((app) => {
|
||||
return filteredTenant.value.map<AppCard>((app) => {
|
||||
const leaves = flattenAppLeaves(app)
|
||||
// 找第一个 section + 第一个 leaf
|
||||
let firstEntry: AppCard['firstEntry'] | undefined
|
||||
for (const section of app.children) {
|
||||
const first = section.children[0]
|
||||
if (!first) continue
|
||||
const leaf = isNavGroup(first) ? first.children[0] : first
|
||||
const leaf = isNavGroup(first) ? (first as NavGroup).children[0] : (first as NavLeaf)
|
||||
if (leaf) { firstEntry = { section: section.key, leaf }; break }
|
||||
}
|
||||
return {
|
||||
@@ -287,6 +371,34 @@ const portalCards = computed<AppCard[]>(() => {
|
||||
})
|
||||
})
|
||||
|
||||
/** 综合管理后台:一级菜单卡片(每张卡片 = 一个 NavSection) */
|
||||
interface AdminSectionCard {
|
||||
key: string
|
||||
label: string
|
||||
pageCount: number
|
||||
hasReady: boolean
|
||||
firstLeaf?: NavLeaf
|
||||
leaves: NavLeaf[]
|
||||
}
|
||||
|
||||
const adminSectionCards = computed<AdminSectionCard[]>(() => {
|
||||
return adminAllSections.value.map<AdminSectionCard>((section) => {
|
||||
const leaves = flattenSectionLeaves(section)
|
||||
const first = section.children[0]
|
||||
const firstLeaf = first
|
||||
? (isNavGroup(first) ? (first as NavGroup).children[0] : (first as NavLeaf))
|
||||
: undefined
|
||||
return {
|
||||
key: section.key,
|
||||
label: section.label,
|
||||
pageCount: leaves.length,
|
||||
hasReady: leaves.some(l => (l.status ?? 'ready') === 'ready'),
|
||||
firstLeaf,
|
||||
leaves,
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// ============================== 行为 ==============================
|
||||
|
||||
const openedPagesKey = ref<string | null>(null)
|
||||
@@ -296,17 +408,33 @@ const togglePages = (modKey: string) => {
|
||||
|
||||
const enterPortal = (card: AppCard) => {
|
||||
if (!card.firstEntry) return
|
||||
router.push(`/${activeKey.value}/${card.key}/${card.firstEntry.section}/${card.firstEntry.leaf.path}`)
|
||||
router.push(`/tenant-portal/${card.key}/${card.firstEntry.section}/${card.firstEntry.leaf.path}`)
|
||||
}
|
||||
|
||||
/** 在悬浮清单里找指定 leaf 所属 section 然后跳转 */
|
||||
/** 租户:进入应用主页(应用网格首页) */
|
||||
const enterTenantAppGrid = () => {
|
||||
router.push('/tenant-portal')
|
||||
}
|
||||
|
||||
/** 综合后台:点击一级菜单卡片 → 跳到该 section 第一个 page */
|
||||
const enterAdminSection = (card: AdminSectionCard) => {
|
||||
if (!card.firstLeaf) return
|
||||
router.push(`/admin-portal/${card.key}/${card.firstLeaf.path}`)
|
||||
}
|
||||
|
||||
/** 综合后台:从悬浮清单点击具体叶子 */
|
||||
const enterAdminLeaf = (sectionKey: string, leaf: NavLeaf) => {
|
||||
router.push(`/admin-portal/${sectionKey}/${leaf.path}`)
|
||||
}
|
||||
|
||||
/** 租户:在悬浮清单里找指定 leaf 所属 section 然后跳转 */
|
||||
const enterLeaf = (portalKey: string, appKey: string, leaf: NavLeaf) => {
|
||||
const apps = portalKey === 'admin-portal' ? filteredAdmin.value : filteredTenant.value
|
||||
const apps = filteredTenant.value
|
||||
const app = apps.find(a => a.key === appKey)
|
||||
if (!app) return
|
||||
for (const section of app.children) {
|
||||
for (const item of section.children) {
|
||||
const candidates = isNavGroup(item) ? item.children : [item]
|
||||
const candidates = isNavGroup(item) ? (item as NavGroup).children : [item as NavLeaf]
|
||||
if (candidates.some(l => l.key === leaf.key)) {
|
||||
router.push(`/${portalKey}/${appKey}/${section.key}/${leaf.path}`)
|
||||
return
|
||||
@@ -392,9 +520,28 @@ const buildPortalSection = (
|
||||
return { key: portalKey, label, icon, items, readyCount: items.filter(i => i.ready).length }
|
||||
}
|
||||
|
||||
/**
|
||||
* 综合管理后台的 PRD 区块:以 NavSection(一级菜单)为粒度
|
||||
* - 无应用概念,PRD 直接按一级菜单组织
|
||||
* - 约定 PRD 路径:admin-portal/<sectionKey>/_module.md 或 admin-portal/<sectionKey>.md
|
||||
*/
|
||||
const buildAdminPrdSection = (sections: NavSection[]): PrdSection => {
|
||||
const items: PrdItem[] = sections.map((s) => {
|
||||
const path = resolveModulePrd('admin-portal', s.key)
|
||||
return { key: s.key, name: s.label, path, ready: !!path }
|
||||
})
|
||||
return {
|
||||
key: 'admin-portal',
|
||||
label: '综合管理后台',
|
||||
icon: '🛠️',
|
||||
items,
|
||||
readyCount: items.filter(i => i.ready).length,
|
||||
}
|
||||
}
|
||||
|
||||
const prdSections = computed<PrdSection[]>(() => {
|
||||
const sections: PrdSection[] = [
|
||||
buildPortalSection('admin-portal', '综合管理后台', '🛠️', filteredAdmin.value),
|
||||
buildAdminPrdSection(adminAllSections.value),
|
||||
buildPortalSection('tenant-portal', '租户运营后台', '💼', filteredTenant.value),
|
||||
]
|
||||
|
||||
@@ -666,6 +813,22 @@ const emojiOf = (icon: string): string => {
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
}
|
||||
|
||||
// 租户"应用主页"卡片:蓝色主题,强调入口语义
|
||||
&--home {
|
||||
border-color: #91caff;
|
||||
background: linear-gradient(135deg, #e6f4ff 0%, #f5faff 100%);
|
||||
|
||||
&:hover {
|
||||
border-color: #1677ff;
|
||||
box-shadow: 0 8px 24px rgba(22, 119, 255, 0.18);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.module-card__hint {
|
||||
font-size: 12px;
|
||||
color: #5a607f;
|
||||
}
|
||||
|
||||
.module-card__watermark {
|
||||
|
||||
+56
-3
@@ -1,12 +1,13 @@
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import {
|
||||
adminApps,
|
||||
adminSections,
|
||||
tenantApps,
|
||||
miniprogramTabs,
|
||||
hardwareDevices,
|
||||
isNavGroup,
|
||||
type NavApp,
|
||||
type NavSection,
|
||||
type NavGroup,
|
||||
type NavLeaf,
|
||||
} from '@meta/nav'
|
||||
|
||||
@@ -142,6 +143,58 @@ const buildPortalChildren = (apps: NavApp[], portalKey: string): RouteRecordRaw[
|
||||
return children
|
||||
}
|
||||
|
||||
/**
|
||||
* 综合管理后台子路由(扁平结构)
|
||||
* - URL 模式:/admin-portal/<section>/<page>
|
||||
* - admin 无子应用概念、无应用网格首页
|
||||
* - 路由 meta.app 固定为 'admin'(用于按 portal+app 隔离 PageHistory)
|
||||
*/
|
||||
const buildAdminPortalChildren = (): RouteRecordRaw[] => {
|
||||
const children: RouteRecordRaw[] = []
|
||||
|
||||
// 找首个可访问的 section + leaf 用于 /admin-portal 根重定向
|
||||
let firstSectionKey: string | undefined
|
||||
let firstLeafPath: string | undefined
|
||||
for (const section of adminSections) {
|
||||
const first = section.children[0]
|
||||
if (!first) continue
|
||||
const leaf = isNavGroup(first) ? (first as NavGroup).children[0] : (first as NavLeaf)
|
||||
if (leaf) {
|
||||
firstSectionKey = section.key
|
||||
firstLeafPath = leaf.path
|
||||
break
|
||||
}
|
||||
}
|
||||
if (firstSectionKey && firstLeafPath) {
|
||||
children.push({
|
||||
path: '',
|
||||
redirect: `/admin-portal/${firstSectionKey}/${firstLeafPath}`,
|
||||
})
|
||||
}
|
||||
|
||||
// 展平所有页面:path = <section>/<page>
|
||||
adminSections.forEach((section: NavSection) => {
|
||||
section.children.forEach((item) => {
|
||||
const leaves: NavLeaf[] = isNavGroup(item) ? (item as NavGroup).children : [item as NavLeaf]
|
||||
leaves.forEach((leaf) => {
|
||||
children.push({
|
||||
path: `${section.key}/${leaf.path}`,
|
||||
name: leaf.key,
|
||||
component: leaf.component as RouteRecordRaw['component'],
|
||||
meta: {
|
||||
portal: 'admin-portal',
|
||||
app: 'admin',
|
||||
section: section.key,
|
||||
label: leaf.label,
|
||||
},
|
||||
} as RouteRecordRaw)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
return children
|
||||
}
|
||||
|
||||
/** 全局路由表 */
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
@@ -175,12 +228,12 @@ const routes: RouteRecordRaw[] = [
|
||||
meta: { label: '原型导航首页' },
|
||||
},
|
||||
|
||||
/** 综合管理后台 */
|
||||
/** 综合管理后台(扁平结构:/admin-portal/<section>/<page>) */
|
||||
{
|
||||
path: '/admin-portal',
|
||||
component: () => import('@layouts/PortalLayout.vue'),
|
||||
props: { variant: 'admin' },
|
||||
children: buildPortalChildren(adminApps, 'admin-portal'),
|
||||
children: buildAdminPortalChildren(),
|
||||
},
|
||||
|
||||
/** 租户运营后台 */
|
||||
|
||||
Reference in New Issue
Block a user