# 架构 mainPage 是一个 Vue 3 模块化多项目展示站。核心设计目标:**高内聚低耦合** -- 每个项目页签是一个独立「模块」,由注册表自动生成页签与路由,新增/删除页签只增删一个文件夹,互不影响。 ## 模块注册表(扩展点核心) `src/modules/index.js` 用 `import.meta.glob('./*/index.js', { eager: true })` 约定式收集每个子模块的 `index.js`。每个模块默认导出元数据: ```js export default { id: 'whiteboard', // 必填:唯一标识,同时作路由 path / KeepAlive 匹配名 tabKey: 'tabs.whiteboard', // 必填:i18n 键,页签标题与 PageShell 标题均用此 component: () => import('./Whiteboard.vue'), // 必填:懒加载组件(函数形式,独立 chunk) order: 2, // 可选:页签排序,缺省 999(排在显式页签之后) hidden: true, // 可选:不在导航显示,但 URL 访问后保持可见(如 cqy) requiresAlive: healthUrl('whiteboard'), // 可选:存活探针 URL(同源 /api/health),服务失活则隐藏页签并卸载组件 param: 'boardId', // 可选:路由参数名,生成 //:? 子路径 } ``` ### 元数据字段 -> 消费方矩阵 | 字段 | 注册表 | router | useProjects | App.vue | TabNav | |------|--------|--------|-------------|---------|--------| | `id` | 必填校验 | path+name+meta.moduleId | visited 集合 / 路由关联 | keepInclude 名单 | `:to` + `:key` | | `tabKey` | 必填校验 | - | - | - | 标题文案 | | `component` | 必填校验 | 路由 component | - | RouterView 渲染 | - | | `order` | 排序 | 决定 `/` 重定向目标 | 继承顺序 | - | 继承顺序 | | `hidden` | 透传 | - | 可见性门控 | - | - | | `requiresAlive` | 透传 | - | 可见性门控 | keepInclude + startPolling | - | | `param` | 透传 | `/:?` 后缀 | - | - | - | ### 容错 注册表对每个模块做字段完整性校验(缺 `id`/`tabKey`/`component` 的模块跳过),单个模块异常不影响其余页签加载。 ## PageShell(统一页面外壳) `src/components/ui/PageShell.vue` 是可复用的页面外壳组件,统一所有页面的: - **容器宽度**:默认复用全局 `.container`(`max-width: 1080px`,居中,左右 `padding`);`fluid` 变体突破限制全宽(如日历页) - **垂直节奏**:`padding: var(--space-xl) 0 var(--space-2xl)` - **标题**:`titleKey` prop -> i18n 文案,渲染为 `

` - **操作区**:`#actions` 具名插槽,放在标题旁(如白板的切换白板表单) 各模块根组件用 `` 包裹内容,不再各写一套 `.container`/标题/间距。新增页签照此复用。 ## KeepAlive 缓存与卸载 `App.vue` 用 `` 包裹路由组件,切换页签不卸载(状态保留)。 - `keepInclude` 是模块 id 列表,默认包含所有模块。 - 声明了 `requiresAlive` 的模块,当存活探测为 `down` 时移出名单 -> 组件卸载;恢复 `up` 后重新纳入。 - **匹配约定**:KeepAlive 按组件 `name` 匹配,因此每个模块组件需 `defineOptions({ name: <模块 id> })`。 - 内置模块(whiteboard/calendar)直接在组件内声明。 - 子模块集成(timetable/mobile-game)由 mainPage 侧包装器持有 name,子项目自身不持 name(保持纯净)。 ## 子模块集成(git submodule + 构建期组件) 部分页签由独立仓库的子项目经 git submodule 集成,mainPage 侧用包装器 import 子项目 `App.vue` 作为路由组件(非 iframe),共享构建与 KeepAlive 缓存: - **timeTableFix**(`third_party/timeTableFix`,日程整理页签):自带独立 `vue-i18n` 实例与中英 locale 文案。经 `:locale` prop 桥接父项目语言。详见 [submodule-timeTableFix.md](./submodule-timeTableFix.md)。 - **z449**(`third_party/z449`,移动游戏页签):自带独立 `vue-i18n` 实例与 locale 文案。mainPage 包装器把当前语言经 `:locale` prop 透传,子项目 `App.vue` `watch` 该 prop 同步到自己的 i18n 实例,从而跟随父项目语言开关响应式切换(不共享 messages,仅同步 locale 值)。详见 [submodule-z449.md](./submodule-z449.md)。 - **zPDF_package**(`third_party/zPDF_package`,PDF 转换页签):经同源 fetch 调用 zTools2 的 `/api/pdf/*` REST API。详见 [submodule-zPDF_package.md](./submodule-zPDF_package.md)。 - **zWhiteBoard**(`third_party/zWhiteBoard`,共享记事本页签):经同源 fetch/WS 调用 zTools2 的 `/api/wb/*` 与 `/api/ws/wb/*`。详见 [submodule-zWhiteBoard.md](./submodule-zWhiteBoard.md)。 > **i18n 桥接关键点**:子项目组件用各自 `useLocale` composable **直接读自身模块级 i18n 实例**(`i18n.global`),而非 `useI18n({ useScope: 'global' })`。后者在嵌入时会解析到宿主已安装的 i18n 实例(不含子项目文案),导致文案回退成 i18n key(显示 ID 而非文字)。 ## 存活校验(requiresAlive) `src/composables/useLiveness.js` 提供跨域存活探测: - 探测方式:`fetch(url, { mode: 'no-cors', cache: 'no-store' })`,resolve 即存活,4s 超时。 - 时机:应用加载即探测一次,之后每 30s 周期重探(`startPolling`)。 - 结果在全应用共享(模块级 `cache` Map),`useProjects`(页签可见性)与 `App.vue`(KeepAlive 卸载)共用同一 ref。 - **乐观显示**:探测中(pending)/存活(up)都显示页签,仅明确不可达(down)才隐藏并卸载;恢复后自动重新加载。 - 探针 URL 由 `healthUrl(service)` 拼出同源相对路径(`/api/health`),经 vite dev proxy / Apache 反代转发到 zTools2,与环境无关。 目前白板与 PDF 页签使用(`requiresAlive: healthUrl('whiteboard'|'pdf')`),二者探针均指向 zTools2 的 `/api/health`。 ## 路由 `src/router/index.js` 遍历注册表生成路由:每个模块一条路由,path = `/` + 可选 `/:?`。history 模式,需站点根 `.htaccess` 做 SPA 回退(见 README 安装步骤)。 ## 国际化 `src/i18n/` -- 中文同步注入主 chunk(默认语言),英文通过动态 `import()` 懒加载成独立 chunk,只看中文的访客不会下载英文资源。各模块 `tabKey` 指向 `tabs.*` 下的文案。 ## 集中配置 `src/config/app.config.js` -- 各服务的同源探针路径(`/api/health`)与默认参数集中管理,模块按需 import。改路径只改这里(需重新 build)。