feat: 支持教学图鉴

This commit is contained in:
richarjiang
2026-10-18 17:26:16 +08:00
parent 3635b2a906
commit 3dc56a113b
29 changed files with 3056 additions and 5 deletions

151
docs/reformer-education.md Normal file
View File

@@ -0,0 +1,151 @@
# 「探索」Tab 与 Reformer 器械图鉴
用户界面采用正式内容呈现,不显示审阅状态或未开放功能占位。审核与来源记录仅作内部内容管理,移除标识不会自动补写或改变教练审核结论。器械示意及无实测阻力参数的说明继续保留。
## 本地查看
```sh
pnpm build:education
open packages/server/public/learn/reformer/index.html
```
`index.html` 是独立页面:内嵌 Vanilla JS、CSS 和 SVG,不请求 CDN、字体、图片、API 或 WebGL,可直接通过 `file://` 打开,也可部署到任意 HTTPS 静态站点。当前约 202 KiB,gzip 约 47 KiB,以构建日志为准。源码用 TypeScript 编写,由 esbuild 输出浏览器 JavaScript;没有 Vue/React 运行时。
开发时也可启动静态服务:
```sh
python3 -m http.server 4178 --directory packages/server/public
# 浏览器打开 http://localhost:4178/learn/reformer/
```
浏览器本地预览不需要微信业务域名配置。
## 小程序入口
底部导航新增「探索」:
- `pages/learn/index`:原生内容目录首页,采用统一封面卡片与简短导语。内容目录位于 `packages/app/src/content/explore.ts`;后续专题按条目追加,首页不绑定某一期内容。
- `pages/learn/reformer`:微信 `web-view` 容器,加载 `https://focus.richarjiang.com/learn/reformer/`。
- `pages/learn/reformer-static`:不依赖网页与网络的完整中文部件说明;网页错误回调也会提供此入口。容器先通过 HEAD 检查页面状态,未部署造成的 404 或网络失败会直接进入原生回退,避免显示服务器错误页面。
探索首页只保留导语和内容卡片。新增专题时在 `content/explore.ts` 追加标题、摘要、分类、形式、封面与页面路径,并确保目标页面已在 `pages.json` 注册。目录随小程序版本发布,已有 H5 正文可独立更新;没有为尚未上线的内容显示占位卡片。
网址统一在 `packages/app/src/config/education.ts` 配置,不接受外部 query 指定任意 URL。`web-view` 使用微信原生导航栏,其它页面沿用现有 CustomNavBar。分享入口回到探索首页。
## 交互
点击器械部件或横向部件列表,可以高亮并阅读 9 组部件说明。组装/分解按钮可随时反向切换,分解仅表达层次,不是实际拆装步骤。
「床板位置」滑块控制归一化的示意行程;「动作进度」控制完整的推出与回程。推出段占进度 0–50%,回程段占 50–100%,因此进度 100% 时床板回到起点。暂停保持当前位置,续播从当前进度继续,回放从起点开始。首次进入有一次缓慢演示,任何操作都会取消自动演示。浏览器隐藏或器械离屏时停止播放,回来后需手动继续。手机端器械卡片在控制区域滚动时保持可见。
支持逐根挂接及 3 个组合示例,至少保留一根连接。未连接弹簧的自由端随床板收纳,长度保持不变;已连接弹簧的固定端保持在弹簧杆,另一端随床板移动。绳索展示拉环收纳状态;组装状态的示意绳路保持绘图长度,松弛向框架外侧展开,绕滑轮外缘转向。分解时绳路只用于看清归属,不保持真实约束,不模拟具体人体动作、负载或传动比。
触摸与鼠标都可使用原生 range 控件;键盘方向键、Home/End 可调节滑块,Tab/Enter 可操作全部部件及按钮。有焦点提示和中文辅助说明。`prefers-reduced-motion` 下关闭播放与自动演示,位置和视图即时响应。关闭 JavaScript 时,图、全部部件文案和来源仍可阅读;「查看静态图鉴」还能主动切换阅读模式。
## 源码与构建
| 文件 | 作用 |
| --- | --- |
| `packages/app/src/content/reformer.ts` | 共用部件文案、参考来源、五弹簧配置、内部审核记录和版本 |
| `education/reformer/src/model.ts` | 纯运动模型、动画时序、实测标定接口;不包含 SVG/DOM |
| `education/reformer/src/geometry.ts` | Anatomy 等轴测实体、材料、绳路与弹簧端点;全部尺寸为绘图单位 |
| `education/reformer/src/controller.ts` | Vanilla DOM 交互、单个 rAF 循环、减少动态与媒体接口 |
| `education/reformer/src/page.html` | 页面结构与无脚本阅读内容 |
| `education/reformer/src/page.css` | 375–430px 移动布局及桌面布局 |
| `education/reformer/src/illustration.css` | SVG 四色阶材料与高亮样式 |
| `education/reformer/build.mjs` | 生成完整独立 HTML 和独立 SVG |
| `education/reformer/vendor/anatomy/` | 原版 Anatomy 绘图库、MIT 许可、上游版本说明 |
| `packages/server/public/learn/reformer/index.html` | 可直接部署的完整页面 |
`pnpm build:app` 和 `pnpm build:server` 都先生成教学页面与独立 SVG,不需联网下载 Anatomy。小程序内使用已生成的 PNG,避免原生图片渲染器对 SVG/CSS 的兼容差异。修改绘图后执行 `node education/reformer/check-browser.mjs /tmp/reformer-checks --write-native-fallback` 更新本地 PNG,再构建小程序。独立的 `pnpm build:education:release` 要求审核记录完整,否则失败。日常构建允许 pending 状态;该内部状态不显示在用户页面上。
```sh
pnpm build:education
pnpm --filter @mp-pilates/app type-check
pnpm build:app
pnpm build:server
node education/reformer/check-model.mjs
node education/reformer/check-browser.mjs /tmp/reformer-checks
```
浏览器检查需要 Node.js 22+(原生 WebSocket)与 Chrome/Chromium;默认查找 macOS Google Chrome,可通过 `CHROME_PATH` 指定其它位置。它检查 375/390/430/1100px、动作关键帧的 SVG 边界、部件选择、弹簧组合、播放暂停回放、实际触摸拖动、动画反转、无脚本与减少动态模式,以及控制台错误与外部资源请求,并输出截图。
模型检查覆盖:一个完整往返、固定端/随动端、收纳弹簧不伸长、运动中几何数值有效,以及未标定、缺失单根数据、无审核记录、数据无序、超出测量区间时拒绝计算。测试中的合成数据仅用于数学验证,不会进入页面。浏览器图形边界、端点和 Anatomy 线条检查不是完整的机械碰撞证明或工程认证;最终必须由教练对照实机检查。
## 部署到现有服务
Nest 启动时已通过静态中间件开放 `/learn/`,与 `/api/` 分离,不经过 JSON 包装或登录验证。`__dirname/../public/learn` 同时适用于现有 watch 与 dist 目录结构。部署脚本原有 rsync 会携带 `packages/server/public`,无需额外 Node 依赖,也无需数据库迁移。
完成教练审核后:
1. 更新共用内容文件中的 `review.status`、`coach`、`reviewedAt`,修改内容同时更新 `revision`。审核记录应对应本次修订。
2. 执行 `pnpm build:education:release`、类型检查和构建;由负责人确认页面与小程序。
3. 部署服务端或将 `public/learn/reformer/index.html` 放到静态 HTTPS 站点的 `/learn/reformer/index.html`。
4. 如现有 Nginx 只转发 `/api/`,增加 `/learn/` 转发。生产配置示例(保留现有其它 location):
```nginx
location ^~ /learn/ {
proxy_pass http://127.0.0.1:3008;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
gzip on;
gzip_types text/css application/javascript image/svg+xml;
# Nginx 默认会压缩 text/html。
}
```
`proxy_pass` 此处不带结尾 `/`,确保后端收到完整 `/learn/reformer/` 路径。也可用 Nginx 直接托管该目录。页面无第三方资源;服务器返回 `X-Content-Type-Options: nosniff`,HTML 使用可重新验证的缓存策略。
5. 在微信公众平台确认当前主体具备 `web-view` 能力,按[微信官方 web-view 文档](https://developers.weixin.qq.com/miniprogram/dev/component/web-view.html)配置 `focus.richarjiang.com` 为**业务域名**并放置校验文件。容器的 HEAD 检查也需要该域名在 request 合法域名中;默认与现有 API 共用域名。已有的 request 合法域名不等于业务域名;开发者工具的“不校验”选项不能代替生产配置。
6. 确认证书、HTTPS、正式账号与体验版真机都能打开页面,测试网页加载失败时的静态入口,再上传小程序。若账号暂不能使用 web-view,原生静态图鉴仍可访问。
2026-10-18 17:11(北京时间)按用户授权完成服务端发布,线上地址为 https://focus.richarjiang.com/learn/reformer/ 。本次没有上传或发布微信小程序。首次发布时页面带有审阅标识,随后按用户要求移除用户可见标识;微信 web-view 文档本次网络读取不可用,小程序发布前应在微信后台核对当前主体、业务域名和基础库的实际要求。
发布记录:
- 发布目录:\`/usr/local/web/releases/reformer-20261018-1792314611961/\`。
- 发布包 SHA-256:\`00a5d4cd0251903b11eda139e0b5fd4bdf66950dad64bf8a641125a4bcba1430\`,上传前后相同。
- 对比线上 204 个 server 构建文件,仅 \`main.js\` 与 \`main.js.map\` 有变化;96 个 shared 文件完全一致。本次更新两个入口文件并新增教学 HTML 与 SVG,沿用线上依赖与 Prisma Client,没有执行数据库迁移。
- 发布前保存 \`previous-build.tgz\`、进程与健康状态。发布后对运行文件逐项校验,确认环境文件摘要一致。
- 公网 HTML、SVG、健康接口与场馆信息接口均为 200,小程序容器使用的 HEAD 请求也为 200。HTML 为 206386 字节。PM2 在线,切换时重启一次,错误日志没有新增。
- 发布目录内保留 \`verification.json\`、\`process-before.json\`、\`process-after.json\` 和 \`rollback.sh\`。如需应用回退,先检查该脚本与当前线上后续变更,再执行;它恢复旧构建、撤回本次新增教学文件并重启原服务,不修改数据库和环境文件。
## 真实性与教练审核
微信业务域名校验文件已于 2026-10-18 17:24(北京时间)部署:
- 公网地址:`https://focus.richarjiang.com/IYfSihR4Io.txt`,HTTP 200、`text/plain`、32 字节,内容与用户原文件逐字节一致。
- 文件位置:`/www/wwwroot/focus.richarjiang.com/IYfSihR4Io.txt`;Nginx 为该根路径配置了精确匹配,避免被默认反向代理转发到 API。
- 配置备份和验证记录:`/usr/local/web/releases/wechat-domain-20261018-1792315399093/`。配置测试通过并已平滑加载,健康接口正常。微信后台仍需由用户点击保存完成平台校验。
2026-10-18 17:21(北京时间)按用户要求完成界面精简,移除首页、静态图鉴、H5 与 SVG 无障碍描述中的审阅标识;资料来源与示意说明归入详情。H5 静态更新备份位于 `/usr/local/web/releases/reformer-ui-20261018-1792315235344/`,公网 HTML/SVG 校验通过,健康接口 200,服务进程与重启次数保持不变。新的原生首页已构建,尚未上传小程序。
参考来源:
- [Balanced Body Studio Reformer 产品资料](https://www.pilates.com/products/pilates-studio-reformer):木质框架、五弹簧配置(3 红/1 蓝/1 绿)、滚轮与肩靠配置。
- [Balanced Body Reformer 指南](https://www.pilates.com/ultimate-guide-to-the-pilates-reformer/):床板、滑轨、脚踏杆、绳索、滑轮、弹簧的作用,以及阻力和辅助的区别。
- [Studio Reformer / Tower 厂家装配手册](https://balancedbody.eu/wp-content/uploads/2022/01/Studio-Reformer-with-tower-Unpacking-and-Assembly.pdf):供教练逐项对照实机。此手册应由教练完整复核,本次联网检索仅能读取其摘要。
- [Anatomy Skill](https://github.com/wheresryan22/anatomy):按 Figure 规模使用其 SVG 实体、投影和四色阶能力;来源版本和许可保存在 vendor 的 NOTICE 中。
这不是所有品牌的通用实机复刻。当前绘图尺寸、归一化行程、8 秒播放周期、分解距离均为界面示意,不表示实机的尺寸、可用行程、训练节奏或安全限位。弹簧颜色只对应上述参考机型。教练审阅时至少核对:
- 工作室实际品牌、型号、弹簧数量和颜色定义;是否需要替换参考配置。
- 弹簧杆、床板下方固定点、挂钩、轮组、限位、脚踏杆锁定与支撑关系。
- 滑轮外缘绳路、固定/调节位置、拉环收纳位置、绳索松弛表现。
- 头枕与肩靠位置、分解图层级是否易于理解且不会被误认为拆装教学。
- 示意行程、播放节奏和往返原理文案;全部 UI 文案与弹簧组合是否会被误解为练习处方。
- 375–430px 真机点击、拖动、暂停、回放和低性能设备表现。
## 弹簧实测接口
当前 `springCalibration` 是空表。页面始终显示「待实测标定」,没有 kg/N 读数,也不把颜色换算成公斤数。
`SpringCalibration` 为后续接入预留:来源、明确实机型号、教练、日期、同一安装状态下实际床板位置(m)与实测单根拉力(N)的有序采样,采样应包含安装预拉力。`forceAt()` 只对完整数据在测量区间内插值并求已连接弹簧之和;任意一根缺参数、无审核记录、数据不合法或需要外推,返回 `null`。
这个函数**尚未接到 UI**:真实力值还需要同一实机的有效行程映射、相同弹簧杆档位和测量方案,经教练审核后再实现。即使计算出弹簧合力,也不等于人体所受净阻力;摩擦、绳路与姿势等仍需另行建模。不要直接给当前绘图单位赋予米或牛顿。
## 外部人体教学接口
浏览器提供 `window.ReformerLesson.registerMedia(media)`,类型为 `ExternalTeachingMedia`,支持 `video` 或外部 `model` 页面。接入前在 controller 中配置 `approvedMediaOrigins`(默认空列表),并提供 HTTPS URL、来源、教练、审核日期和标题。默认不加载任何人体或外部资源。
视频使用原生 `<video controls playsinline preload="none">`;3D 教学页使用独立 iframe(sandbox 仅允许脚本,不开放顶级跳转)。实际模型供应方若需要其它沙箱能力,应单独评估并按微信业务域名要求配置。当前没有医学人体生成,也没有自建 WebGL 人体模型。