Compare commits

..

2 Commits

Author SHA1 Message Date
Zikai
8dcf5f0d36 Merge branch 'refactor/cleanup-failfast' into main
清理死代码/提前失败/高内聚低耦合,重写文档对齐现状(无 iframe、无外部域名)。
合并子项目文档修正与死代码清理。
2026-07-28 04:27:57 +00:00
25dbceee2d refactor: 清理死代码/提前失败/高内聚低耦合,重写文档
死代码清理:
- 删除未使用的 UI 组件 Card.vue / Tag.vue
- 移除 app.config.js 死字段 healthTimeoutMs(×2)与 isDev()
- 移除 PageShell.vue 死 prop padded(恒为默认值)
- 移除 useLocale.js 死导出 isZh/pick/locale
- 移除 router/index.js 不可达 try/catch,简化为直接 .map()
- 移除 package.json 重复依赖 ical.js(子模块自带)
- 移除 i18n 死键 common.notAvailable / common.serviceUnavailable
- 移除 vite.config.js 陈旧的 /448 /449 dev proxy(z449 为构建期组件,不走该 URL)

提前失败/日志:
- useI18nLazy.js switchLocale 加载语言包失败时 console.error 记录(原静默吞错)
- modules/index.js 字段不全的模块跳过时 console.warn 告警(原与文档承诺不符、静默跳过)
- deploy.sh 补齐 zWhiteBoard / zPDF_package 的 npm install 与子模块初始化
  (均为构建期组件 import,全新检出时缺少依赖会构建失败)

文档对齐现状(无 iframe、无外部域名):
- README 改写为简洁版,修正 iframe/ical.js/子模块数量过时描述
- architecture.md 修正 requiresAlive 示例(同源 /api/health,非外部域名)
  与「仅白板使用」断言(PDF 也用);移除已删 padded prop
- add-module.md 同步移除 padded、修正 requiresAlive 示例
- submodule-timeTableFix.md 修正 ical.js 归属(子模块自带,非父项目共享)
- submodule-zPDF_package.md 子项目数量 3 -> 4
- 新增 submodule-zWhiteBoard.md
- readme-fixes.md 记录本次勘误
2026-07-28 11:57:32 +08:00
22 changed files with 216 additions and 168 deletions

View File

@@ -9,28 +9,30 @@ Zikai 的作品集主页 -- Vue 3 模块化多项目展示站。浅色极简、
``` ```
mainPage/ mainPage/
├── index.html ├── index.html
├── package.json # 含 ical.js / vue-i18n(子模块嵌入时共享) ├── package.json # vue / vue-i18n / vue-router
├── vite.config.js # 构建输出到 dist/,再由 npm run deploy rsync 到 /root/html ├── vite.config.js # 构建输出到 dist/,再由 npm run deploy rsync 到 /var/www/html
├── .gitmodules # git submoduletimeTableFix / z449 ├── deploy.sh # 整体部署:构建 + rsync + Apache 反代 + 重启 zTools2
├── .gitmodules # git submoduletimeTableFix / z449 / zPDF_package / zWhiteBoard
├── README.md ├── README.md
├── docs/ # 架构 / 新增页签 / 子模块管理文档 ├── docs/ # 架构 / 新增页签 / 子模块管理 / 部署文档
├── public/favicon.svg ├── public/favicon.svg
├── third_party/ ├── third_party/ # ★ git submodule 子项目
│ ├── timeTableFix/ # ★ git submoduleICS 日程整理器 │ ├── timeTableFix/ # ICS 日程整理器(纯前端)
│ ├── z449/ # ★ git submodule移动游戏项目展示页 │ ├── z449/ # 移动游戏项目展示页(纯前端)
── zPDF_package/ # ★ git submodulePDF 转换前端iframe 嵌入 zTools2 ── zPDF_package/ # PDF 转换前端(同源调用 zTools2 /api/pdf/*
│ └── zWhiteBoard/ # 共享记事本前端(同源调用 zTools2 /api/wb/* + /api/ws/wb/*
└── src/ └── src/
├── main.js # 应用入口(挂载 i18n / router / 全局样式) ├── main.js # 应用入口(挂载 i18n / router / 全局样式)
├── App.vue # 根布局:顶栏 + 标签导航 + <KeepAlive router-view> + 页脚 ├── App.vue # 根布局:顶栏 + 标签导航 + <KeepAlive router-view> + 页脚
├── config/app.config.js # 集中配置:外部服务 URL白板地址 / 存活探针等) ├── config/app.config.js # 集中配置:各服务的同源探针路径(/api/health与默认参数
├── i18n/ # 国际化(中文同步注入;英文动态 import() 懒加载) ├── i18n/ # 国际化(中文同步注入;英文动态 import() 懒加载)
├── router/index.js # 路由表由模块注册表自动生成 ├── router/index.js # 路由表由模块注册表自动生成
├── modules/ # ★ 扩展点:每个页签一个文件夹 ├── modules/ # ★ 扩展点:每个页签一个文件夹
│ ├── index.js # 注册表import.meta.glob 约定式收集,带容错) │ ├── index.js # 注册表import.meta.glob 约定式收集,带容错)
│ ├── mobile-game/ # 移动游戏项目页签(包装 z449 子模块) │ ├── mobile-game/ # 移动游戏页签(包装 z449 子模块)
│ ├── whiteboard/ # 共享记事本页签(iframe 嵌入 + 存活校验) │ ├── whiteboard/ # 共享记事本页签(包装 zWhiteBoard 子模块 + 存活校验)
│ ├── timetable/ # 日程整理页签(包装 timeTableFix 子模块) │ ├── timetable/ # 日程整理页签(包装 timeTableFix 子模块)
│ ├── pdf/ # PDF 转换页签(iframe 嵌入 zTools2 /pdf + 存活校验) │ ├── pdf/ # PDF 转换页签(包装 zPDF_package 子模块 + 存活校验)
│ └── calendar/ # 隐藏页(仅手动访问 /cqy │ └── calendar/ # 隐藏页(仅手动访问 /cqy
├── components/ # 共享组件TabNav / LanguageSwitcher / ui 基元含 PageShell ├── components/ # 共享组件TabNav / LanguageSwitcher / ui 基元含 PageShell
├── composables/ # useProjects / useLiveness / useI18nLazy / useLocale ├── composables/ # useProjects / useLiveness / useI18nLazy / useLocale
@@ -42,11 +44,11 @@ mainPage/
| 项 | 要求 | | 项 | 要求 |
|---|---| |---|---|
| Node.js | ≥ 18 | | Node.js | ≥ 18 |
| Web 服务器 | Apache 2.4(启用 `mod_rewrite`),对外静态服务构建产物 | | Web 服务器 | Apache 2.4(启用 `mod_rewrite` / `mod_proxy` / `mod_proxy_wstunnel`),对外静态服务构建产物 |
| SPA 回退 | history 模式需站点根 `.htaccess` 回退到 `index.html` | | SPA 回退 | history 模式需站点根 `.htaccess` 回退到 `index.html` |
| 共享记事本 | iframe 嵌入外部服务 `https://f.zikai.wang`URL 在 `src/config/app.config.js` 配置) | | 后端 | zTools2127.0.0.1:6867由 Apache 反代 `/api/`PDF/白板页签依赖其 REST API 与 WebSocket |
> 子模块timeTableFix / z449构建期组件集成(非 iframe共享 mainPage 的构建与 KeepAlive 缓存。 > 四个子模块均为**构建期组件集成(非 iframe**,共享 mainPage 的构建与 KeepAlive 缓存。各子模块依赖在其自身 `node_modules`,由各自 `package.json` 管理。
## 如何使用 ## 如何使用
@@ -55,21 +57,23 @@ mainPage/
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo bash - curl -fsSL https://deb.nodesource.com/setup_18.x | sudo bash -
sudo apt-get install -y nodejs sudo apt-get install -y nodejs
# 2. 取代码(含子模块 timeTableFix / z449 # 2. 取代码(含四个子模块)
git clone --recursive https://git.zikai.wang/zikai/zMainPage.git mainPage git clone --recursive https://git.zikai.wang/zikai/zMainPage.git mainPage
cd mainPage cd mainPage
# 若已 clone 但未带子模块git submodule update --init --recursive # 若已 clone 但未带子模块git submodule update --init --recursive
npm install npm install
# 子模块需各自安装依赖(构建期编译其 .vue # 子模块需各自安装依赖(构建期编译其 .vue
(cd third_party/z449 && npm install)
(cd third_party/timeTableFix && npm install) (cd third_party/timeTableFix && npm install)
(cd third_party/z449 && npm install)
(cd third_party/zPDF_package && npm install)
(cd third_party/zWhiteBoard && npm install)
# 3. 构建到 dist/(再由 npm run deploy rsync 同步到 /root/html # 3. 构建到 dist/(再由 npm run deploy rsync 同步到 /var/www/html
npm run build npm run build
# 4. 安装并启用 Apache # 4. 安装并启用 Apache
sudo apt-get install -y apache2 sudo apt-get install -y apache2
sudo a2enmod rewrite sudo a2enmod rewrite proxy proxy_http proxy_wstunnel headers
``` ```
站点根放 `.htaccess`Apache 需 `AllowOverride All`)做 SPA 回退: 站点根放 `.htaccess`Apache 需 `AllowOverride All`)做 SPA 回退:
@@ -82,7 +86,25 @@ RewriteRule ^ - [L]
RewriteRule ^ index.html [L] RewriteRule ^ index.html [L]
``` ```
`/root/html`(或 `/var/www/html`设为 `DocumentRoot`,访问站点即可 `/var/www/html` 设为 `DocumentRoot`。后端 zTools2 需监听 `127.0.0.1:6867`,并在 vhost 加反代规则(见下方 Apache 配置)。整体部署可用 `./deploy.sh` 一键完成(详见 [`docs/deployment.md`](./docs/deployment.md)
### Apache 反代 zTools2/api/ -> 127.0.0.1:6867
```apache
<VirtualHost *:80>
DocumentRoot /var/www/html
ProxyPreserveHost On
# 所有 zTools2 入口API/静态/探针/WS统一在 /api/ 下,一条规则即可
ProxyPass /api/ http://127.0.0.1:6867/api/
ProxyPassReverse /api/ http://127.0.0.1:6867/api/
<Directory /var/www/html>
AllowOverride All
Require all granted
</Directory>
</VirtualHost>
```
> WebSocket 走 `/api/ws/wb/{id}`,需 `a2enmod proxy_wstunnel` 以透传 Upgrade 头。
## 开发命令 ## 开发命令
@@ -90,7 +112,7 @@ RewriteRule ^ index.html [L]
npm install # 首次安装依赖 npm install # 首次安装依赖
npm run dev # 开发服务器 http://localhost:5173 npm run dev # 开发服务器 http://localhost:5173
npm run build # 构建到 dist/ npm run build # 构建到 dist/
npm run deploy # 构建并 rsync 同步到 /root/html npm run deploy # 构建并 rsync 同步到 /var/www/html
npm run preview # 本地预览构建产物 npm run preview # 本地预览构建产物
``` ```
@@ -100,6 +122,7 @@ npm run preview # 本地预览构建产物
- [如何新增一个项目页签](./docs/add-module.md) - [如何新增一个项目页签](./docs/add-module.md)
- [子项目 timeTableFix 管理submodule 升级 / 移除)](./docs/submodule-timeTableFix.md) - [子项目 timeTableFix 管理submodule 升级 / 移除)](./docs/submodule-timeTableFix.md)
- [子项目 z449 管理submodule + i18n prop 桥接)](./docs/submodule-z449.md) - [子项目 z449 管理submodule + i18n prop 桥接)](./docs/submodule-z449.md)
- [子项目 zPDF_package 管理submodule + iframe 同源集成)](./docs/submodule-zPDF_package.md) - [子项目 zPDF_package 管理submodule + 同源 API 集成)](./docs/submodule-zPDF_package.md)
- [子项目 zWhiteBoard 管理submodule + 同源 API/WS 集成)](./docs/submodule-zWhiteBoard.md)
- [部署指南(组件独立 / 整体 / 持久化 systemd](./docs/deployment.md) - [部署指南(组件独立 / 整体 / 持久化 systemd](./docs/deployment.md)
- [README 勘误记录](./docs/readme-fixes.md) - [README 勘误记录](./docs/readme-fixes.md)

View File

@@ -28,8 +28,11 @@ echo "==> [1/4] 构建 zMainPage含子模块"
# 子模块依赖需各自安装(构建期编译其 .vue # 子模块依赖需各自安装(构建期编译其 .vue
[[ -d third_party/timeTableFix/node_modules ]] || (cd third_party/timeTableFix && npm install) [[ -d third_party/timeTableFix/node_modules ]] || (cd third_party/timeTableFix && npm install)
[[ -d third_party/z449/node_modules ]] || (cd third_party/z449 && npm install) [[ -d third_party/z449/node_modules ]] || (cd third_party/z449 && npm install)
# zPDF_package 是 iframe 集成,不参与 mainPage 构建,但确保其源码存在 [[ -d third_party/zPDF_package/node_modules ]] || (cd third_party/zPDF_package && npm install)
[[ -d third_party/zPDF_package ]] || git submodule update --init third_party/zPDF_package [[ -d third_party/zWhiteBoard/node_modules ]] || (cd third_party/zWhiteBoard && npm install)
# 确保子模块源码存在(全新检出时)
[[ -d third_party/zPDF_package/src ]] || git submodule update --init third_party/zPDF_package
[[ -d third_party/zWhiteBoard/src ]] || git submodule update --init third_party/zWhiteBoard
npm run build npm run build
echo "==> [2/4] 部署 zMainPage 构建产物到 $WEB_ROOT" echo "==> [2/4] 部署 zMainPage 构建产物到 $WEB_ROOT"
@@ -89,4 +92,4 @@ echo
echo "部署完成。" echo "部署完成。"
echo " - zMainPage: http://localhostApache :80静态文件 + SPA" echo " - zMainPage: http://localhostApache :80静态文件 + SPA"
echo " - zTools2: http://127.0.0.1:6867经 Apache 反代 /api/" echo " - zTools2: http://127.0.0.1:6867经 Apache 反代 /api/"
echo " - PDF 页签: http://localhost -> iframe 同源嵌入 zTools2 /api/pdf" echo " - PDF 页签: http://localhost -> 构建期组件嵌入,同源调用 zTools2 /api/pdf/*"

View File

@@ -41,7 +41,6 @@ const { t } = useLocale()
PageShell props PageShell props
- `titleKey`i18n 键,渲染页面标题;不传则无标题 - `titleKey`i18n 键,渲染页面标题;不传则无标题
- `fluid`true 突破 1080px 全宽(如日历页) - `fluid`true 突破 1080px 全宽(如日历页)
- `padded`false 去掉默认上下间距(页面自管布局时用)
- `#actions` 具名插槽:标题旁的操作区(如白板切换表单) - `#actions` 具名插槽:标题旁的操作区(如白板切换表单)
### 3. 加 i18n 文案 ### 3. 加 i18n 文案
@@ -80,7 +79,7 @@ export default { id: 'secret', tabKey: 'tabs.secret', order: 99, hidden: true, c
import { healthUrl } from '../../config/app.config.js' import { healthUrl } from '../../config/app.config.js'
export default { export default {
id: 'my-service', tabKey: 'tabs.myService', order: 5, id: 'my-service', tabKey: 'tabs.myService', order: 5,
requiresAlive: 'https://example.com/health', // 存活探针 URL requiresAlive: healthUrl('myService'), // 同源探针 URL/api/health
component: () => import('./MyService.vue') component: () => import('./MyService.vue')
} }
``` ```

View File

@@ -13,7 +13,7 @@ export default {
component: () => import('./Whiteboard.vue'), // 必填:懒加载组件(函数形式,独立 chunk component: () => import('./Whiteboard.vue'), // 必填:懒加载组件(函数形式,独立 chunk
order: 2, // 可选:页签排序,缺省 999排在显式页签之后 order: 2, // 可选:页签排序,缺省 999排在显式页签之后
hidden: true, // 可选:不在导航显示,但 URL 访问后保持可见(如 cqy hidden: true, // 可选:不在导航显示,但 URL 访问后保持可见(如 cqy
requiresAlive: 'https://f.zikai.wang/health', // 可选:存活探针 URL服务失活则隐藏页签并卸载组件 requiresAlive: healthUrl('whiteboard'), // 可选:存活探针 URL(同源 /api/health,服务失活则隐藏页签并卸载组件
param: 'boardId', // 可选:路由参数名,生成 /<id>/:<param>? 子路径 param: 'boardId', // 可选:路由参数名,生成 /<id>/:<param>? 子路径
} }
``` ```
@@ -39,7 +39,7 @@ export default {
`src/components/ui/PageShell.vue` 是可复用的页面外壳组件,统一所有页面的: `src/components/ui/PageShell.vue` 是可复用的页面外壳组件,统一所有页面的:
- **容器宽度**:默认复用全局 `.container``max-width: 1080px`,居中,左右 `padding``fluid` 变体突破限制全宽(如日历页) - **容器宽度**:默认复用全局 `.container``max-width: 1080px`,居中,左右 `padding``fluid` 变体突破限制全宽(如日历页)
- **垂直节奏**`padding: var(--space-xl) 0 var(--space-2xl)``padded: false` 可关 - **垂直节奏**`padding: var(--space-xl) 0 var(--space-2xl)`
- **标题**`titleKey` prop -> i18n 文案,渲染为 `<h1>` - **标题**`titleKey` prop -> i18n 文案,渲染为 `<h1>`
- **操作区**`#actions` 具名插槽,放在标题旁(如白板的切换白板表单) - **操作区**`#actions` 具名插槽,放在标题旁(如白板的切换白板表单)
@@ -59,8 +59,10 @@ export default {
部分页签由独立仓库的子项目经 git submodule 集成mainPage 侧用包装器 import 子项目 `App.vue` 作为路由组件(非 iframe共享构建与 KeepAlive 缓存: 部分页签由独立仓库的子项目经 git submodule 集成mainPage 侧用包装器 import 子项目 `App.vue` 作为路由组件(非 iframe共享构建与 KeepAlive 缓存:
- **timeTableFix**`third_party/timeTableFix`,日程整理页签):自带独立 `vue-i18n` 实例与中英 locale 文案。与 z449 同样`:locale` prop 桥接父项目语言。详见 [submodule-timeTableFix.md](./submodule-timeTableFix.md)。 - **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)。 - **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 而非文字)。 > **i18n 桥接关键点**:子项目组件用各自 `useLocale` composable **直接读自身模块级 i18n 实例**`i18n.global`),而非 `useI18n({ useScope: 'global' })`。后者在嵌入时会解析到宿主已安装的 i18n 实例(不含子项目文案),导致文案回退成 i18n key显示 ID 而非文字)。
@@ -72,8 +74,9 @@ export default {
- 时机:应用加载即探测一次,之后每 30s 周期重探(`startPolling`)。 - 时机:应用加载即探测一次,之后每 30s 周期重探(`startPolling`)。
- 结果在全应用共享(模块级 `cache` Map`useProjects`(页签可见性)与 `App.vue`KeepAlive 卸载)共用同一 ref。 - 结果在全应用共享(模块级 `cache` Map`useProjects`(页签可见性)与 `App.vue`KeepAlive 卸载)共用同一 ref。
- **乐观显示**探测中pending/存活up都显示页签仅明确不可达down才隐藏并卸载恢复后自动重新加载。 - **乐观显示**探测中pending/存活up都显示页签仅明确不可达down才隐藏并卸载恢复后自动重新加载。
- 探针 URL 由 `healthUrl(service)` 拼出同源相对路径(`/api/health`),经 vite dev proxy / Apache 反代转发到 zTools2与环境无关。
目前白板页签使用(`requiresAlive: 'https://f.zikai.wang/health'` 目前白板与 PDF 页签使用(`requiresAlive: healthUrl('whiteboard'|'pdf')`),二者探针均指向 zTools2 的 `/api/health`
## 路由 ## 路由
@@ -85,4 +88,4 @@ export default {
## 集中配置 ## 集中配置
`src/config/app.config.js` -- 外部服务 URL白板地址、存活探针路径等集中管理,模块按需 import。改 URL 只改这里(需重新 build `src/config/app.config.js` -- 各服务的同源探针路径(`/api/health`)与默认参数集中管理,模块按需 import。改路径只改这里(需重新 build

View File

@@ -16,7 +16,7 @@ zPDF_package 功能涉及三个项目协同:**zMainPage**父站Vue3 静
PDF 页签 = zMainPage 构建期 import zPDF_package 的 `App.vue` 作为组件(非 iframe经同源 fetch 调用 zTools2 的 `/api/pdf/*` REST API**同源相对路径**cookie 第一方生效,与环境无关)。 PDF 页签 = zMainPage 构建期 import zPDF_package 的 `App.vue` 作为组件(非 iframe经同源 fetch 调用 zTools2 的 `/api/pdf/*` REST API**同源相对路径**cookie 第一方生效,与环境无关)。
> **路由约定**zTools2 所有入口(页面 / 静态 / 探针 / WS / API统一挂在 `/api/` 下,反代与 vite proxy 只需一条 `/api/` 规则。前端(无论是 iframe 嵌入页还是组件 fetch API一律用同源相对路径(`/api/pdf/jobs`、`/api/health` 等),自动指向**当前环境**的 zTools2无需区分 dev/prod也不会误打到其它环境域名。详见 zTools2 README「路由约定」。 > **路由约定**zTools2 所有入口(页面 / 静态 / 探针 / WS / API统一挂在 `/api/` 下,反代与 vite proxy 只需一条 `/api/` 规则。前端组件经同源相对路径(`/api/pdf/jobs`、`/api/health` 等)调用,自动指向**当前环境**的 zTools2无需区分 dev/prod也不会误打到其它环境域名。详见 zTools2 README「路由约定」。
--- ---

View File

@@ -18,3 +18,36 @@
**修正**:三处均改为说明构建输出到 `dist/`,由 `npm run deploy` rsync 到 `/root/html`,并在「开发命令」补 `npm run deploy` 一行。 **修正**:三处均改为说明构建输出到 `dist/`,由 `npm run deploy` rsync 到 `/root/html`,并在「开发命令」补 `npm run deploy` 一行。
> 背景:本次升级 timeTableFix 子模块(新增时区选择)时,按 README 从 0 构建复现发现该不一致。 > 背景:本次升级 timeTableFix 子模块(新增时区选择)时,按 README 从 0 构建复现发现该不一致。
## 2026-07-28部署路径统一为 /var/www/html
**原描述(不一致)**
- `package.json``npm run deploy` 脚本 rsync 到 `/root/html`(依赖 `/root/html` 软链到 `/var/www/html`)。
- `deploy.sh` 与 README 用 `/var/www/html`Apache 标准 DocumentRoot不依赖软链
- `vite.config.js` 顶部注释写 `/root/html(软链到 /var/www/html)`
**问题**:全新 Ubuntu 安装时 `/root/html` 软链不存在,`npm run deploy` 会失败或写到错误位置;三处路径不统一。
**修正**`package.json` deploy 脚本与 `vite.config.js` 注释统一为 `/var/www/html`,与 `deploy.sh`、README、Apache DocumentRoot 一致。
## 2026-07-28iframe / ical.js / 子模块数量描述过时
**原描述(错误)**
- README「外部依赖」称白板页「iframe 嵌入外部服务 `https://f.zikai.wang`」。
- README「项目结构」注释称 zPDF_package「iframe 嵌入 zTools2」、白板「iframe 嵌入」。
- README「package.json」注释含 `ical.js`(子模块依赖被误列在父项目)。
- README 子模块只列 timeTableFix / z449漏 zPDF_package / zWhiteBoard克隆后安装依赖步骤也漏后两者。
- architecture.md 的 `requiresAlive` 示例为 `https://f.zikai.wang/health`且称「仅白板页签使用」PDF 也用)。
- architecture.md / add-module.md 提及 PageShell 的 `padded` prop已被移除
- submodule-timeTableFix.md 称「共享 mainPage 的依赖 `ical.js`」(实际子模块自带)。
**实际行为**
- 四个子模块timeTableFix / z449 / zPDF_package / zWhiteBoard均为**构建期组件 import**(非 iframe
- 白板与 PDF 页签的探针均为同源相对路径 `/api/health`(经反代/vite proxy 指向 zTools2非外部域名。
- `ical.js` 仅 timeTableFix 使用,由其自身 `package.json` 管理mainPage 的 `package.json` 不再列。
- PageShell 已无 `padded` prop默认即上下间距
**修正**README 改写为简洁版,结构/外部依赖/克隆步骤均对齐现状docs 各文件同步修正。

View File

@@ -7,7 +7,7 @@ timeTableFixICS 日程整理器)作为 **git submodule** 被 mainPage 引
mainPage 侧的 `src/modules/timetable/Timetable.vue` 包装器 import 子模块的 `src/App.vue` 作为组件,套 `PageShell` 统一容器与标题,并持有 KeepAlive 匹配名 `defineOptions({ name: 'timetable' })` mainPage 侧的 `src/modules/timetable/Timetable.vue` 包装器 import 子模块的 `src/App.vue` 作为组件,套 `PageShell` 统一容器与标题,并持有 KeepAlive 匹配名 `defineOptions({ name: 'timetable' })`
- 子模块 `App.vue` 保持纯净(不持 name、不依赖宿主可独立 `npm run build` / `npm test` / 部署。 - 子模块 `App.vue` 保持纯净(不持 name、不依赖宿主可独立 `npm run build` / `npm test` / 部署。
- 构建期集成(非 iframe共享 mainPage 的构建依赖(`ical.js`与 KeepAlive 缓存 - 构建期集成(非 iframe共享 mainPage 的构建与 KeepAlive 缓存。子模块自身依赖(`ical.js``third_party/timeTableFix/node_modules`,由其自身 `package.json` 管理
- 子模块样式已隔离scoped + `ttf-` 前缀类名),全局重置仅在独立 app 的 `main.js` 加载,不污染宿主。 - 子模块样式已隔离scoped + `ttf-` 前缀类名),全局重置仅在独立 app 的 `main.js` 加载,不污染宿主。
## 克隆(含子模块) ## 克隆(含子模块)
@@ -23,6 +23,12 @@ cd mainPage
git submodule update --init --recursive git submodule update --init --recursive
``` ```
> 构建前需在子模块目录内 `npm install`mainPage 的 Vite 编译子模块 `.vue`,但子模块依赖如 `ical.js` 须各自安装):
```bash
cd third_party/timeTableFix && npm install
```
## 升级子模块到最新版本 ## 升级子模块到最新版本
```bash ```bash

View File

@@ -61,4 +61,4 @@ git commit -m "chore: 移除 zPDF_package 子模块集成"
| cookie | 不涉及 | httpOnly `zk_pdf`,同源自动携带(首次上传种下) | | cookie | 不涉及 | httpOnly `zk_pdf`,同源自动携带(首次上传种下) |
| mainPage 是否编译子模块 | 是import .vue | 是import .vue | | mainPage 是否编译子模块 | 是import .vue | 是import .vue |
> 个子项目timeTableFix / z449 / zPDF_package均采用构建期组件 import 集成,无 iframe。zPDF_package 额外依赖 zTools2 后端 API页签带存活探针。 > 个子项目timeTableFix / z449 / zPDF_package / zWhiteBoard)均采用构建期组件 import 集成,无 iframe。zPDF_package 与 zWhiteBoard 额外依赖 zTools2 后端 API页签带存活探针。

View File

@@ -0,0 +1,72 @@
# 子项目 zWhiteBoardgit submodule
zWhiteBoard共享记事本前端作为 **git submodule** 被 mainPage 引入,位于 `third_party/zWhiteBoard`,独立仓库为 https://git.zikai.wang/zikai/zWhiteBoard.git 。
## 集成方式
mainPage 侧的 `src/modules/whiteboard/Whiteboard.vue` 包装器 import 子模块的 `src/App.vue` 作为组件,套 `PageShell` 统一容器与标题,并持有 KeepAlive 匹配名 `defineOptions({ name: 'whiteboard' })`
- 子模块 `App.vue` 保持纯净(不持 name、不依赖宿主可独立 `npm run build` / `npm test` / 部署。
- **构建期集成(非 iframe**,共享 mainPage 的构建与 KeepAlive 缓存。前后端分离UI/交互在 zWhiteBoard能力由 zTools2 的 `/api/wb/*` REST API 与 `/api/ws/wb/*` WebSocket 提供。
- 子模块样式已隔离scoped全局重置仅在独立 app 的 `main.js` 加载,不污染宿主。
- 页签带 `requiresAlive: healthUrl('whiteboard')`(同源 `/api/health`zTools2 不可达时隐藏页签并卸载组件。
- **身份标识**:白板不依赖 cookie。客户端 id 由 WebSocket 首帧 `hello` 携带或服务端生成(`client_id`),用于区分广播来源。组件经同源 fetch/WS 调用,反代使 `/api/` 与主页同源,无需 CORS。
- 包装器解析路由参数 `/whiteboard/:boardId` 并经 `:board-id` prop 透传给子模块,支持直链预填白板 id。
## 克隆(含子模块)
```bash
git clone --recursive https://git.zikai.wang/zikai/zMainPage.git mainPage
```
若已 clone 但未带子模块:
```bash
cd mainPage
git submodule update --init --recursive
```
> 构建前需在子模块目录内 `npm install`mainPage 的 Vite 编译子模块 `.vue`,但子模块依赖如 `vue-i18n` 须各自安装):
```bash
cd third_party/zWhiteBoard && npm install
```
## 升级子模块到最新版本
```bash
cd mainPage
git submodule update --remote third_party/zWhiteBoard # 拉取最新
cd third_party/zWhiteBoard && git checkout master # 固定到目标分支/commit
cd ../..
git add third_party/zWhiteBoard # 更新 gitlink 指针
git commit -m "chore: 升级 zWhiteBoard 子模块"
npm run build # 重新构建 mainPage
git push # 推送 mainPagegitlink
```
> 推送顺序:先确保子模块的 commit 已推到 zWhiteBoard.git再推 mainPage 的 gitlink否则别人 clone mainPage 时拉不到子模块对应 commit。
## 移除子模块集成(仅移除 mainPage 页签,不影响 zWhiteBoard 本身)
```bash
cd mainPage
git submodule deinit -f third_party/zWhiteBoard
git rm third_party/zWhiteBoard
git commit -m "chore: 移除 zWhiteBoard 子模块集成"
```
同时删 `src/modules/whiteboard/` 文件夹即移除页签。zWhiteBoard 独立仓库不受影响。
## 与其他子模块的差异
| 维度 | timeTableFix / z449 | zPDF_package | zWhiteBoard |
|------|---------------------|--------------|-------------|
| 集成方式 | 构建期组件 import共享构建/KeepAlive | 构建期组件 import共享构建/KeepAlive | 构建期组件 import共享构建/KeepAlive |
| 后端依赖 | 无(纯前端) | 有zTools2 `/api/pdf/*` | 有zTools2 `/api/wb/*` + `/api/ws/wb/*` |
| 同源/CORS | 不涉及 | 同源经反代,无需 CORS | 同源经反代,无需 CORS |
| 身份标识 | 不涉及 | httpOnly `zk_pdf` cookie同源自动携带 | WS 首帧 `client_id`(无 cookie |
| 路由参数 | 无 | 无 | `:boardId`(直链预填白板 id |
| mainPage 是否编译子模块 | 是import .vue | 是import .vue | 是import .vue |
> 四个子项目均采用构建期组件 import 集成,无 iframe。zWhiteBoard 与 zPDF_package 额外依赖 zTools2 后端 API页签带存活探针zWhiteBoard 是唯一带 WebSocket 与路由参数的子模块。

6
package-lock.json generated
View File

@@ -8,7 +8,6 @@
"name": "mainpage", "name": "mainpage",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"ical.js": "^2.1.0",
"vue": "^3.4.21", "vue": "^3.4.21",
"vue-i18n": "^9.13.1", "vue-i18n": "^9.13.1",
"vue-router": "^4.3.0" "vue-router": "^4.3.0"
@@ -1119,11 +1118,6 @@
"url": "https://github.com/fb55/entities?sponsor=1" "url": "https://github.com/fb55/entities?sponsor=1"
} }
}, },
"node_modules/ical.js": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/ical.js/-/ical.js-2.2.1.tgz",
"integrity": "sha512-yK/UlPbEs316igb/tjRgbFA8ZV75rCsBJp/hWOatpyaPNlgw0dGDmU+FoicOcwX4xXkeXOkYiOmCqNPFpNPkQg=="
},
"node_modules/lower-case": { "node_modules/lower-case": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz",

View File

@@ -8,10 +8,9 @@
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "vite build",
"preview": "vite preview", "preview": "vite preview",
"deploy": "vite build && rsync -a --delete dist/js/mainPage/ /root/html/js/mainPage/ && rsync -a --delete dist/css/mainPage/ /root/html/css/mainPage/ && rsync -a dist/index.html /root/html/index.html && rsync -a dist/favicon.svg /root/html/favicon.svg" "deploy": "vite build && rsync -a --delete dist/js/mainPage/ /var/www/html/js/mainPage/ && rsync -a --delete dist/css/mainPage/ /var/www/html/css/mainPage/ && rsync -a dist/index.html /var/www/html/index.html && rsync -a dist/favicon.svg /var/www/html/favicon.svg"
}, },
"dependencies": { "dependencies": {
"ical.js": "^2.1.0",
"vue": "^3.4.21", "vue": "^3.4.21",
"vue-i18n": "^9.13.1", "vue-i18n": "^9.13.1",
"vue-router": "^4.3.0" "vue-router": "^4.3.0"

View File

@@ -1,28 +0,0 @@
<script setup>
defineProps({
hoverable: { type: Boolean, default: false }
})
</script>
<template>
<div class="card" :class="{ 'card--hover': hoverable }">
<slot />
</div>
</template>
<style scoped>
.card {
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow-sm);
padding: var(--space-lg);
}
.card--hover {
transition: transform var(--transition), box-shadow var(--transition);
}
.card--hover:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-hover);
}
</style>

View File

@@ -3,17 +3,15 @@ import { computed } from 'vue'
import { useLocale } from '../../composables/useLocale.js' import { useLocale } from '../../composables/useLocale.js'
// ★ 可复用页面外壳 -- 统一页面的容器宽度、垂直节奏与标题,消除各模块重复包裹。 // ★ 可复用页面外壳 -- 统一页面的容器宽度、垂直节奏与标题,消除各模块重复包裹。
// 高内聚:所有页面布局令牌集中于此;低耦合:模块只声明 titleKey/fluid/padded // 高内聚:所有页面布局令牌集中于此;低耦合:模块只声明 titleKey/fluid
// 不关心 .container 实现。新增页签照此复用,无需各写一套 wrapper。 // 不关心 .container 实现。新增页签照此复用,无需各写一套 wrapper。
// //
// Props: // Props:
// titleKey: i18n 键(如 'tabs.whiteboard'),渲染为页面主标题;不传则无标题 // titleKey: i18n 键(如 'tabs.whiteboard'),渲染为页面主标题;不传则无标题
// fluid: true 时突破 1080px 限制(全宽 + 左右 padding如日历页 // fluid: true 时突破 1080px 限制(全宽 + 左右 padding如日历页
// padded: false 时去掉默认上下间距(页面完全自管布局时用)
const props = defineProps({ const props = defineProps({
titleKey: { type: String, default: '' }, titleKey: { type: String, default: '' },
fluid: { type: Boolean, default: false }, fluid: { type: Boolean, default: false }
padded: { type: Boolean, default: true }
}) })
const { t } = useLocale() const { t } = useLocale()
@@ -21,7 +19,7 @@ const title = computed(() => (props.titleKey ? t(props.titleKey) : ''))
</script> </script>
<template> <template>
<div class="page-shell" :class="{ 'page-shell--padded': padded }"> <div class="page-shell">
<div :class="fluid ? 'page-shell__fluid' : 'container page-shell__inner'"> <div :class="fluid ? 'page-shell__fluid' : 'container page-shell__inner'">
<div v-if="title || $slots.actions" class="page-shell__head"> <div v-if="title || $slots.actions" class="page-shell__head">
<h1 v-if="title" class="page-shell__title">{{ title }}</h1> <h1 v-if="title" class="page-shell__title">{{ title }}</h1>
@@ -35,7 +33,7 @@ const title = computed(() => (props.titleKey ? t(props.titleKey) : ''))
</template> </template>
<style scoped> <style scoped>
.page-shell--padded { .page-shell {
padding: var(--space-xl) 0 var(--space-2xl); padding: var(--space-xl) 0 var(--space-2xl);
} }
/* 标准容器宽度复用全局 .containermax-width 1080px居中左右 padding */ /* 标准容器宽度复用全局 .containermax-width 1080px居中左右 padding */

View File

@@ -1,25 +0,0 @@
<script setup>
defineProps({
tag: { type: String, default: 'span' }
})
</script>
<template>
<component :is="tag" class="tag">
<slot />
</component>
</template>
<style scoped>
.tag {
display: inline-flex;
align-items: center;
padding: 2px 10px;
font-size: 0.78rem;
font-weight: 500;
color: var(--accent);
background: var(--accent-weak);
border-radius: 999px;
line-height: 1.6;
}
</style>

View File

@@ -14,6 +14,9 @@ export function useI18nLazy() {
switching.value = true switching.value = true
try { try {
await loadLocaleAsync(locale) await loadLocaleAsync(locale)
} catch (e) {
// 语言包加载失败不能静默:记录到控制台,便于排查(如 chunk 404
console.error('[mainPage] 加载语言包失败', locale, e)
} finally { } finally {
switching.value = false switching.value = false
} }

View File

@@ -1,15 +1,11 @@
import { computed } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
// 共享的语言辅助 composable消除各组件重复定义 L() // 共享的语言辅助 composable消除各组件重复定义
// 用法: // 用法:
// const { t, locale, isZh, pick } = useLocale() // const { t } = useLocale()
// t('some.key') // 走 i18n推荐,文案在 locale 文件) // t('some.key') // 走 i18n文案在 locale 文件)
// isZh.value // 判断当前是否中文 // 页签包装器透传 locale 给子模块时直接读 i18n.global.locale见各 wrapper
// pick({ zh, en }) // 从 {zh,en} 对象取当前语言值(用于数据文件)
export function useLocale() { export function useLocale() {
const { t, locale } = useI18n({ useScope: 'global' }) const { t } = useI18n({ useScope: 'global' })
const isZh = computed(() => locale.value === 'zh-CN') return { t }
const pick = (obj) => (isZh.value ? obj.zh : obj.en)
return { t, locale, isZh, pick }
} }

View File

@@ -20,18 +20,14 @@ export const appConfig = {
// 存活探针路径;周期探测,挂掉则隐藏页签并卸载组件 // 存活探针路径;周期探测,挂掉则隐藏页签并卸载组件
healthPath: '/api/health', healthPath: '/api/health',
// 默认打开的记事本 idWhiteboard.vue 的 resolveBoardId 回退用) // 默认打开的记事本 idWhiteboard.vue 的 resolveBoardId 回退用)
defaultBoardId: 'share', defaultBoardId: 'share'
// 探测超时(毫秒)
healthTimeoutMs: 4000
}, },
// PDF 转换服务zTools2 提供 /api/pdf/* REST APIzPDF_package 组件同源 fetch 调用) // PDF 转换服务zTools2 提供 /api/pdf/* REST APIzPDF_package 组件同源 fetch 调用)
pdf: { pdf: {
// 服务根地址(含协议与域名);默认空串 = 同源相对路径 // 服务根地址(含协议与域名);默认空串 = 同源相对路径
baseUrl: BASE_URL, baseUrl: BASE_URL,
// 存活探针路径;周期探测,挂掉则隐藏页签并卸载组件 // 存活探针路径;周期探测,挂掉则隐藏页签并卸载组件
healthPath: '/api/health', healthPath: '/api/health'
// 探测超时(毫秒)
healthTimeoutMs: 4000
} }
} }
@@ -45,11 +41,3 @@ export function healthUrl(service) {
if (!cfg) return '' if (!cfg) return ''
return `${cfg.baseUrl}${cfg.healthPath}` return `${cfg.baseUrl}${cfg.healthPath}`
} }
/**
* 当前是否为开发/测试环境(便于组件按环境调整行为)。
* @returns {boolean}
*/
export function isDev() {
return !import.meta.env.PROD
}

View File

@@ -5,9 +5,7 @@ export default {
title: 'Zikai' title: 'Zikai'
}, },
common: { common: {
loading: 'Loading…', loading: 'Loading…'
notAvailable: 'Not available',
serviceUnavailable: 'Service is temporarily unavailable. Please try again later.'
}, },
tabs: { tabs: {
mobileGame: 'Mobile Game Project', mobileGame: 'Mobile Game Project',

View File

@@ -4,9 +4,7 @@ export default {
title: 'Zikai' title: 'Zikai'
}, },
common: { common: {
loading: '加载中…', loading: '加载中…'
notAvailable: '暂不可用',
serviceUnavailable: '服务暂不可用,请稍后再试。'
}, },
tabs: { tabs: {
mobileGame: '移动游戏项目', mobileGame: '移动游戏项目',

View File

@@ -19,11 +19,16 @@
const moduleFiles = import.meta.glob('./*/index.js', { eager: true }) const moduleFiles = import.meta.glob('./*/index.js', { eager: true })
const modules = Object.values(moduleFiles) const modules = Object.entries(moduleFiles)
.map((mod) => mod?.default) .map(([path, mod]) => ({ path, mod: mod?.default }))
.filter(Boolean) .filter(({ mod }) => mod != null)
// 字段完整性兜底:缺 id/tabKey/component 的模块无法生成页签与路由,跳过 // 字段完整性兜底:缺 id/tabKey/component 的模块无法生成页签与路由,跳过并告警
.filter((m) => m && m.id && m.tabKey && typeof m.component === 'function') .filter(({ path, mod }) => {
if (mod.id && mod.tabKey && typeof mod.component === 'function') return true
console.warn('[mainPage] 模块缺少必填字段,已跳过', path)
return false
})
.map(({ mod }) => mod)
// order 缺省 999排在所有显式 order 页签之后hidden 页常用 99 之类显式值) // order 缺省 999排在所有显式 order 页签之后hidden 页常用 99 之类显式值)
.sort((a, b) => (a.order ?? 999) - (b.order ?? 999)) .sort((a, b) => (a.order ?? 999) - (b.order ?? 999))

View File

@@ -2,9 +2,7 @@
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import modules from '../modules/index.js' import modules from '../modules/index.js'
const moduleRoutes = modules const moduleRoutes = modules.map((m) => {
.map((m) => {
try {
// 模块可声明 param如 'boardId')以接受 /<id>/<param> 形式的子路径, // 模块可声明 param如 'boardId')以接受 /<id>/<param> 形式的子路径,
// 形如 /whiteboard/share。可选参数 (? 后缀) 使 /whiteboard 仍可访问。 // 形如 /whiteboard/share。可选参数 (? 后缀) 使 /whiteboard 仍可访问。
const paramPart = m.param ? `/:${m.param}?` : '' const paramPart = m.param ? `/:${m.param}?` : ''
@@ -14,12 +12,7 @@ const moduleRoutes = modules
component: m.component, component: m.component,
meta: { moduleId: m.id } meta: { moduleId: m.id }
} }
} catch (e) {
console.warn(`[router] 模块 ${m?.id} 路由生成失败,已跳过:`, e)
return null
}
}) })
.filter(Boolean)
const routes = [ const routes = [
{ {

View File

@@ -3,9 +3,9 @@ import vue from '@vitejs/plugin-vue'
import { minify } from 'html-minifier-terser' import { minify } from 'html-minifier-terser'
// 构建到项目本地 dist/(每次清空,避免旧产物残留), // 构建到项目本地 dist/(每次清空,避免旧产物残留),
// 再由 `npm run deploy` 用 rsync 同步到 Apache 站点根 /root/html(软链到 /var/www/html) // 再由 `npm run deploy` 或 `deploy.sh` 用 rsync 同步到 Apache 站点根 /var/www/html。
// 文件名去掉内容 hash保持稳定JS 由 esbuild 压缩HTML 由下方插件压缩。 // 文件名去掉内容 hash保持稳定JS 由 esbuild 压缩HTML 由下方插件压缩。
// 用 mainPage/ 命名空间子目录避免与 /root/html/js、/root/html/css 既有旧文件冲突。 // 用 mainPage/ 命名空间子目录避免与 /var/www/html/js、/var/www/html/css 既有旧文件冲突。
function minifyHtml() { function minifyHtml() {
return { return {
name: 'minify-html', name: 'minify-html',
@@ -51,19 +51,9 @@ export default defineConfig({
}, },
}, },
server: { server: {
// 开发服务器把站点根的静态资源代理到 /root/html方便引用 /448 /449 等
port: 5173, port: 5173,
proxy: { proxy: {
'/448': { // zTools2 后端同源代理所有入口API/静态/探针/WS统一在 /api/ 下,一条规则即可。
target: 'http://localhost:8080',
changeOrigin: true,
},
'/449': {
target: 'http://localhost:8080',
changeOrigin: true,
},
// zTools2 后端同源代理:所有入口(页面/静态/探针/WS/API统一在 /api/ 下,
// 一条 /api 规则即可覆盖 iframe/api/pdf与 fetch/api/pdf/*)。
// ws:true 让 /api/ws/wb/{id} 的 WebSocket 也走此代理。 // ws:true 让 /api/ws/wb/{id} 的 WebSocket 也走此代理。
// 同源相对路径与环境无关dev 命中本地 zTools2生产由反代命中当前环境 zTools2。 // 同源相对路径与环境无关dev 命中本地 zTools2生产由反代命中当前环境 zTools2。
'/api': { '/api': {