| @@ -0,0 +1,56 @@ | |||||
| { | |||||
| "checks": [ | |||||
| { | |||||
| "name": "对外接口展示九个操作", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "本地接口调度 list-projects 返回成功", | |||||
| "status": "PASS", | |||||
| "detail": "200\nGET /api/ofd/projects\n10 ms\n{\n \"success\": true,\n \"data\": {\n \"total\": 3," | |||||
| }, | |||||
| { | |||||
| "name": "本地接口调度 get-project 返回成功", | |||||
| "status": "PASS", | |||||
| "detail": "200\nGET /api/ofd/projects/OFD-mtub76ed-01\n8 ms\n{\n \"success\": true,\n \"data\": {\n" | |||||
| }, | |||||
| { | |||||
| "name": "本地接口调度 update-step 返回成功", | |||||
| "status": "PASS", | |||||
| "detail": "200\nPUT /api/ofd/projects/OFD-mtub76ed-01/steps/step-01\n26 ms\n{\n \"success\": tru" | |||||
| }, | |||||
| { | |||||
| "name": "本地接口调度 register-resource 返回成功", | |||||
| "status": "PASS", | |||||
| "detail": "201\nPOST /api/ofd/projects/OFD-mtub76ed-01/resources\n35 ms\n{\n \"success\": true,\n" | |||||
| }, | |||||
| { | |||||
| "name": "本地接口调度 collect-resources 返回成功", | |||||
| "status": "PASS", | |||||
| "detail": "201\nPOST /api/ofd/projects/OFD-mtub76ed-01/collect\n44 ms\n{\n \"success\": true,\n " | |||||
| }, | |||||
| { | |||||
| "name": "本地接口调度 inspect 返回成功", | |||||
| "status": "PASS", | |||||
| "detail": "200\nPOST /api/ofd/projects/OFD-mtub76ed-01/inspect\n9 ms\n{\n \"success\": true,\n \"" | |||||
| }, | |||||
| { | |||||
| "name": "本地接口调度 delivery-manifest 返回成功", | |||||
| "status": "PASS", | |||||
| "detail": "200\nPOST /api/ofd/projects/OFD-mtub76ed-01/delivery-manifest\n6 ms\n{\n \"success\":" | |||||
| }, | |||||
| { | |||||
| "name": "本地接口调度 submit-feedback 返回成功", | |||||
| "status": "PASS", | |||||
| "detail": "201\nPOST /api/ofd/projects/OFD-mtub76ed-01/feedback\n25 ms\n{\n \"success\": true,\n " | |||||
| }, | |||||
| { | |||||
| "name": "接口面板未发送真实 HTTP 业务请求", | |||||
| "status": "PASS", | |||||
| "detail": "这里通过本地函数调度,需要接入正式 API 才可外部调用" | |||||
| } | |||||
| ], | |||||
| "errors": [], | |||||
| "requests": [] | |||||
| } | |||||
| @@ -0,0 +1,34 @@ | |||||
| { | |||||
| "checks": [ | |||||
| { | |||||
| "name": "PDF 四页可解析且每页含文本", | |||||
| "status": "PASS", | |||||
| "detail": "4 pages" | |||||
| }, | |||||
| { | |||||
| "name": "OFD ZIP 完整、入口和 XML 可解析", | |||||
| "status": "PASS", | |||||
| "detail": "9 XML files; 未使用第三方 OFD 阅读器认证" | |||||
| }, | |||||
| { | |||||
| "name": "离线 ZIP 完整", | |||||
| "status": "PASS", | |||||
| "detail": "16 files" | |||||
| }, | |||||
| { | |||||
| "name": "离线脚本没有 Vite 开发缓存引用", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "离线狐狸和卡车 GLB 结构及动画保留", | |||||
| "status": "PASS", | |||||
| "detail": "导出按现有逻辑轻量化,卡车从 369980 缩至 318276 字节;非原文件逐字节复制。实际渲染和动画另由浏览器验证。" | |||||
| }, | |||||
| { | |||||
| "name": "离线包含本地 Three.js 和解码器", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| } | |||||
| ] | |||||
| } | |||||
| @@ -0,0 +1,16 @@ | |||||
| { | |||||
| "checks": [ | |||||
| { | |||||
| "name": "公式与说明保存", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "发布保存本地正式版本", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| } | |||||
| ], | |||||
| "errors": [], | |||||
| "requests": [] | |||||
| } | |||||
| @@ -0,0 +1,21 @@ | |||||
| { | |||||
| "checks": [ | |||||
| { | |||||
| "name": "通过界面从空白模板创建指导书", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "创建调用正式内容接口", | |||||
| "status": "FAIL", | |||||
| "detail": "当前页面使用浏览器本地工程库" | |||||
| }, | |||||
| { | |||||
| "name": "新建工程出现在正式后端列表", | |||||
| "status": "FAIL", | |||||
| "detail": "" | |||||
| } | |||||
| ], | |||||
| "errors": [], | |||||
| "requests": [] | |||||
| } | |||||
| @@ -0,0 +1,41 @@ | |||||
| { | |||||
| "checks": [ | |||||
| { | |||||
| "name": "步骤标题、说明、工具、安全、验收编辑保存", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "刷新后恢复已保存的本地内容", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "新增步骤后保存", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "正文续页和复制页面保存", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "撤销复制恢复页数", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "重做复制恢复页数", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "导入真实 Fox.glb 文件并登记本地资源", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| } | |||||
| ], | |||||
| "errors": [], | |||||
| "requests": [] | |||||
| } | |||||
| @@ -0,0 +1,71 @@ | |||||
| # 闭环实测-狐狸巡检作业指导-20260910 构建计划书 | |||||
| > 文档编号:REVIEW-GUIDE-20260910 版本:V0.1.0 | |||||
| > 适用装备:狐狸模型测试装备 编制单位:内部维护 | |||||
| > 执行规范:GB/T 33190-2016 生成日期:2026-09-10 | |||||
| > 密级:内部资料 · 注意保存 | |||||
| 本文件由数字车间作业指导书模块按当前工程状态自动生成,内容随工程更新而变化。 | |||||
| ## 一、构建依据 | |||||
| - 内容模板:空白指导书 | |||||
| - 资料来源:支腿液压缸中修资料包.hg | |||||
| - 章节步骤规模:1 章 / 2 步 | |||||
| ## 二、构建阶段与交付物 | |||||
| | 阶段 | 主要工作 | 交付物 | 完成判定 | | |||||
| | --- | --- | --- | --- | | |||||
| | 内容分析 | 资料结构识别、章节层级划分、媒体资源索引 | 需求规格说明书、构建计划书 | 两份文档可预览与下载 | | |||||
| | 内容编制 | 章节步骤编写、安全要求与技术参数录入 | 步骤内容初稿 | 输出前检查的内容类项目全部通过 | | |||||
| | 三维关联 | 部件选取、空间锚定、热点与动画区间绑定 | 三维关联配置 | 每个步骤具备关联对象与锚点 | | |||||
| | 输出前检查 | 逐项核对元数据、结构、资源与版式 | 检查报告 | 建议项清晰可定位,不影响导出 | | |||||
| | 生成与发布 | 生成 .hg 包、校验目录与 CRC、版本归档 | 正式包与版本记录 | 包体通过结构校验 | | |||||
| | 阅读交付 | 内网交互阅读与单机离线阅读 | 阅读端入口 | 预览打开 < 3 秒 | | |||||
| ## 三、章节构建计划 | |||||
| | 章节 | 步骤数 | 预计页数 | 已关联三维对象 | 已配置插图 | | |||||
| | --- | --- | --- | --- | --- | | |||||
| | 01 新建章节 | 2 | 4 | 0/2 | 0/2 | | |||||
| ## 四、资源准备计划 | |||||
| | 类型 | 名称 | 当前状态 | 处理方式 | | |||||
| | --- | --- | --- | --- | | |||||
| | 三维模型 | 09-Fox.glb | 已持久化 | 直接引用 | | |||||
| | 三维模型 | 09-Fox.glb | 已持久化 | 直接引用 | | |||||
| | 三维模型 | 09-Fox.glb | 已持久化 | 直接引用 | | |||||
| | 三维模型 | 08-CesiumMilkTruck.glb | 已持久化 | 直接引用 | | |||||
| | 三维模型 | 09-Fox.glb | 已持久化 | 直接引用 | | |||||
| | 三维模型 | 08-CesiumMilkTruck.glb | 已持久化 | 直接引用 | | |||||
| | 三维模型 | 09-Fox.glb | 已持久化 | 直接引用 | | |||||
| | 三维模型 | 08-CesiumMilkTruck.glb | 已持久化 | 直接引用 | | |||||
| | 三维模型 | 09-Fox.glb | 已持久化 | 直接引用 | | |||||
| | 三维模型 | 08-CesiumMilkTruck.glb | 已持久化 | 直接引用 | | |||||
| | 三维模型 | 09-Fox.glb | 已持久化 | 直接引用 | | |||||
| | 三维模型 | 08-CesiumMilkTruck.glb | 已持久化 | 直接引用 | | |||||
| | 三维模型 | 09-Fox.glb | 已持久化 | 直接引用 | | |||||
| | 三维模型 | 08-CesiumMilkTruck.glb | 已持久化 | 直接引用 | | |||||
| | 三维模型 | 09-Fox.glb | 已持久化 | 直接引用 | | |||||
| | 三维模型 | 08-CesiumMilkTruck.glb | 已持久化 | 直接引用 | | |||||
| | 三维模型 | 09-Fox.glb | 已持久化 | 直接引用 | | |||||
| | 三维模型 | 08-CesiumMilkTruck.glb | 已持久化 | 直接引用 | | |||||
| ## 五、资源采集状态 | |||||
| 尚未运行内容采集。规划阶段可从平台资源库为每个步骤匹配候选资源,确认后写入步骤。 | |||||
| ## 六、当前内容关联度 | |||||
| - 已配插图:0/2 个步骤 | |||||
| - 已绑模型资产:2/2 个步骤 | |||||
| - 含版式标注:0/2 个步骤 | |||||
| ## 七、风险与依赖 | |||||
| - 未内嵌字体:在无宋体、无黑体的目标环境(如银河麒麟)可能显示替换字形,需在信创交付前补字体子集 | |||||
| - 三维交互能力由动态版式承载,OFD 包仅保证静态版式 | |||||
| - OFD 加密、CA/国密数字签名、可信时间戳与长期归档是原始交付要求;当前内容完整性校验不能替代,需接入甲方证书及归档环境后联调验收 | |||||
| - 内容素材(文字、规程、图表)由甲方或教研方提供 | |||||
| @@ -0,0 +1,87 @@ | |||||
| # 闭环实测-狐狸巡检作业指导-20260910 需求规格说明书 | |||||
| > 文档编号:REVIEW-GUIDE-20260910 版本:V0.1.0 | |||||
| > 适用装备:狐狸模型测试装备 编制单位:内部维护 | |||||
| > 执行规范:GB/T 33190-2016 生成日期:2026-09-10 | |||||
| > 密级:内部资料 · 注意保存 | |||||
| 本文件由数字车间作业指导书模块按当前工程状态自动生成,内容随工程更新而变化。 | |||||
| ## 一、编制目的与范围 | |||||
| 本作业指导书面向 狐狸模型测试装备 的维修作业,覆盖「拆、装、检、排」四位一体作业内容, | |||||
| 共 1 个作业章节、2 个作业步骤。 | |||||
| 输出格式遵循国家标准 GB/T 33190-2016,交付扩展名为 .hg。 | |||||
| ## 二、内容需求清单 | |||||
| | 章节 | 步骤 | 类型 | 参考时长 | 状态 | | |||||
| | --- | --- | --- | --- | --- | | |||||
| | 01 新建章节 | 狐狸巡检目标识别(修订) | 其他 | 01:00 | 待完善 | | |||||
| | 01 新建章节 | 牛奶卡车状态检查 | 作业步骤 | 01:00 | 待完善 | | |||||
| ## 三、安全要求 | |||||
| | 编号 | 级别 | 要求 | 说明 | | |||||
| | --- | --- | --- | --- | | |||||
| | SR-01 | 禁止 | 执行停机挂牌与液压卸压 | 未完成能量隔离和双人复核不得开始拆装。 | | |||||
| | SR-02 | 警告 | 禁止进入悬吊物下方 | 吊装、承托与移运过程中保持安全站位。 | | |||||
| | SR-03 | 注意 | 液压介质污染控制 | 断开的油管与接头必须即时封堵并保持清洁。 | | |||||
| | SR-04 | 注意 | 禁止用手指探查对孔 | 装配对孔必须使用定位棒或规定工装。 | | |||||
| ## 四、技术参数要求 | |||||
| | 编号 | 参数 | 取值 | 单位 | 依据 | | |||||
| | --- | --- | --- | --- | --- | | |||||
| | TP-01 | 连接销紧固扭矩 | 180 ± 10 | N·m | 维修工艺卡 ZC-WX-17 | | |||||
| | TP-02 | 油管接头紧固扭矩 | 65 ± 5 | N·m | 液压接口规范 ZC-HY-04 | | |||||
| | TP-03 | 系统卸压判定 | ≤ 0.2 | MPa | 安全作业规程 ZC-AQ-02 | | |||||
| | TP-04 | 试运行保压时间 | 5 | min | 维修验收规范 ZC-YS-09 | | |||||
| ## 五、数字资源需求 | |||||
| | 类型 | 名称 | 体积 | 状态 | 引用次数 | | |||||
| | --- | --- | --- | --- | --- | | |||||
| | 三维模型 | 09-Fox.glb | 0.2 MB | 已持久化 | 0 | | |||||
| | 三维模型 | 09-Fox.glb | 0.2 MB | 已持久化 | 0 | | |||||
| | 三维模型 | 09-Fox.glb | 0.2 MB | 已持久化 | 0 | | |||||
| | 三维模型 | 08-CesiumMilkTruck.glb | 0.4 MB | 已持久化 | 0 | | |||||
| | 三维模型 | 09-Fox.glb | 0.2 MB | 已持久化 | 0 | | |||||
| | 三维模型 | 08-CesiumMilkTruck.glb | 0.4 MB | 已持久化 | 0 | | |||||
| | 三维模型 | 09-Fox.glb | 0.2 MB | 已持久化 | 0 | | |||||
| | 三维模型 | 08-CesiumMilkTruck.glb | 0.4 MB | 已持久化 | 0 | | |||||
| | 三维模型 | 09-Fox.glb | 0.2 MB | 已持久化 | 0 | | |||||
| | 三维模型 | 08-CesiumMilkTruck.glb | 0.4 MB | 已持久化 | 0 | | |||||
| | 三维模型 | 09-Fox.glb | 0.2 MB | 已持久化 | 0 | | |||||
| | 三维模型 | 08-CesiumMilkTruck.glb | 0.4 MB | 已持久化 | 0 | | |||||
| | 三维模型 | 09-Fox.glb | 0.2 MB | 已持久化 | 0 | | |||||
| | 三维模型 | 08-CesiumMilkTruck.glb | 0.4 MB | 已持久化 | 0 | | |||||
| | 三维模型 | 09-Fox.glb | 0.2 MB | 已持久化 | 0 | | |||||
| | 三维模型 | 08-CesiumMilkTruck.glb | 0.4 MB | 已持久化 | 0 | | |||||
| | 三维模型 | 09-Fox.glb | 0.2 MB | 已持久化 | 0 | | |||||
| | 三维模型 | 08-CesiumMilkTruck.glb | 0.4 MB | 已持久化 | 0 | | |||||
| ## 六、输出与验收要求 | |||||
| - 输出格式:OFD 静态版式,遵循 GB/T 33190-2016 | |||||
| - 版式要素:密级标识、页眉红线、封面文档信息表、页脚页码、可选可视印章位 | |||||
| - 在线预览打开时间:< 3 秒 | |||||
| - 支持离线阅读,排版不受影响 | |||||
| - 三维交互、动画与热点由动态版式承载,不作为 OFD 原生交互能力声明 | |||||
| ## 七、当前改进建议 | |||||
| | 检查项 | 状态 | 说明 | | |||||
| | --- | --- | --- | | |||||
| | 技术参数完整 | 建议完善 | 1/2 个步骤参数齐全 | | |||||
| | 步骤插图完整 | 建议完善 | 0/2 个步骤已配置插图 | | |||||
| | 三维对象关联完整 | 建议完善 | 0/2 个步骤已关联模型对象 | | |||||
| | 热点交互完整 | 建议完善 | 1/2 个步骤已启用热点 | | |||||
| | 离线介质容量复核 | 建议完善 | 发布前由现场确认受控介质可用容量;不阻断 OFD 文件生成 | | |||||
| ## 八、需纳入验收的扩展交付 | |||||
| - OFD 加密、数字签名、长期归档与许可配置按甲方环境联调验收 | |||||
| - 作业指导书文字、规程与图表等内容素材制作 | |||||
| - 三维建模与标准件库建设 | |||||
| @@ -0,0 +1,36 @@ | |||||
| { | |||||
| "checks": [ | |||||
| { | |||||
| "name": "导出 project 下载文件", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "导出 requirement 下载文件", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "导出 plan 下载文件", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "导出 pdf 下载文件", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "导出 ofd 下载文件", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "导出 offline 下载文件", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| } | |||||
| ], | |||||
| "errors": [], | |||||
| "requests": [] | |||||
| } | |||||
| @@ -0,0 +1,21 @@ | |||||
| { | |||||
| "checks": [ | |||||
| { | |||||
| "name": "狐狸按实际尺寸取景后保存", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "卡车按实际尺寸取景后保存", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "两个模型的取景图片重开后保留", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| } | |||||
| ], | |||||
| "errors": [], | |||||
| "requests": [] | |||||
| } | |||||
| @@ -0,0 +1,5 @@ | |||||
| { | |||||
| "checks": [], | |||||
| "errors": [], | |||||
| "requests": [] | |||||
| } | |||||
| @@ -0,0 +1,41 @@ | |||||
| { | |||||
| "checks": [ | |||||
| { | |||||
| "name": "插入表格、填写单元格、合并拆分与边框保存", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "插入正文自由块后保存", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "二维框标注保存", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "动态阅读检索步骤", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "表格、自由块、二维标注重开后保留", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "列表按编号检索", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "重置恢复创建时间排序", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| } | |||||
| ], | |||||
| "errors": [], | |||||
| "requests": [] | |||||
| } | |||||
| @@ -0,0 +1,56 @@ | |||||
| { | |||||
| "checks": [ | |||||
| { | |||||
| "name": "模型插入步骤并保存关联", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "关联导入模型的真实动画", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "三维模型放大面板渲染画布", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "步骤属性「三维」可打开", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "步骤属性「动画」可打开", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "步骤属性「标注」可打开", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "步骤属性「公式」可打开", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "步骤属性「版式」可打开", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "步骤属性「内容」可打开", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "media 脚本执行", | |||||
| "status": "FAIL", | |||||
| "detail": "locator.click: Error: strict mode violation: locator('[data-action=\"library-insert\"][data-kind=\"model\"]').filter({ hasText: 'MilkTruck' }) resolved to 3 elements:\n 1) <button title=\"挂到当前步骤\" draggable=\"true\" data-kind=\"model\" data-drag-kind=\"model\" data-action=\"library-insert\" class=\"ofd-model-tile active\" data-id=\"res-1788971891968-0\" data-drag-id=\"res-1788971891968-0\" data-resource-name=\"08-cesiummilktruck.glb\">…</button> aka getByRole('button', { name: '08-CesiumMilkTruck.glb 缩略图 08' }).first()\n 2) <button title=\"挂到当前步骤\" draggable=\"true\" data-kind=\"model\" data-drag-kind=\"model\" class=\"ofd-model-tile \" data-action=\"library-insert\" data-id=\"res-1788972081498-0\" data-drag-id=\"res-1788972081498-0\" data-resource-name=\"08-cesiummilktruck.glb\">…</button> aka getByRole('button', { name: '08-CesiumMilkTruck.glb 缩略图 08' }).nth(1)\n 3) <button title=\"挂到当前步骤\" draggable=\"true\" data-kind=\"model\" data-drag-kind=\"model\" class=\"ofd-model-tile \" data-action=\"library-insert\" data-id=\"res-1788972140795-0\" data-drag-id=\"res-1788972140795-0\" data-resource-name=\"08-cesiummilktruck.glb\">…</button> aka getByRole('button', { name: '08-CesiumMilkTruck.glb 缩略图 08' }).nth(2)\n\nCall log:\n\u001b[2m - waiting for locator('[data-action=\"library-insert\"][data-kind=\"model\"]').filter({ hasText: 'MilkTruck' })\u001b[22m\n" | |||||
| } | |||||
| ], | |||||
| "errors": [], | |||||
| "requests": [] | |||||
| } | |||||
| @@ -0,0 +1,11 @@ | |||||
| { | |||||
| "checks": [ | |||||
| { | |||||
| "name": "访问不存在的指导书不会自动新建", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| } | |||||
| ], | |||||
| "errors": [], | |||||
| "requests": [] | |||||
| } | |||||
| @@ -0,0 +1,41 @@ | |||||
| { | |||||
| "checks": [ | |||||
| { | |||||
| "name": "离线包章节步骤可以阅读与切换", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "离线模型 1 动画可播放", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "离线模型 1 观察操作可执行", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "离线模型 2 动画可播放", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "离线模型 2 观察操作可执行", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "断开公网和业务服务后没有外部依赖请求", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "离线个人批注与收藏入口", | |||||
| "status": "FAIL", | |||||
| "detail": "当前离线阅读器未提供原始需求中的个人笔记、收藏和学习进度回传" | |||||
| } | |||||
| ], | |||||
| "errors": [], | |||||
| "requests": [] | |||||
| } | |||||
| @@ -0,0 +1,26 @@ | |||||
| { | |||||
| "checks": [ | |||||
| { | |||||
| "name": "查看角色的正文编辑控件禁用", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "查看角色可以使用导出菜单", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "查看角色可以使用页码和缩放", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "只读用户实际导出 OFD 且不修改工程", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| } | |||||
| ], | |||||
| "errors": [], | |||||
| "requests": [] | |||||
| } | |||||
| @@ -0,0 +1,31 @@ | |||||
| { | |||||
| "checks": [ | |||||
| { | |||||
| "name": "导入本地视频并在原生播放器实际播放", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "视频当前帧海报保存到步骤", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "非法工程 JSON 被拒绝且原工程保留", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "编制工程同步到正式内容库", | |||||
| "status": "FAIL", | |||||
| "detail": "实测仅写入浏览器本地库;正式内容库查询 HTTP 200" | |||||
| }, | |||||
| { | |||||
| "name": "编制发布结果进入教学指导书列表", | |||||
| "status": "FAIL", | |||||
| "detail": "教学查询 HTTP 200" | |||||
| } | |||||
| ], | |||||
| "errors": [], | |||||
| "requests": [] | |||||
| } | |||||
| @@ -0,0 +1,46 @@ | |||||
| { | |||||
| "checks": [ | |||||
| { | |||||
| "name": "修订不改变旧的正式版本快照", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "工程 JSON 导入恢复步骤与资源关联", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "狐狸动画真实改变渲染帧", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "狐狸截取当前角度保存纸面图片", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "卡车动画真实改变渲染帧", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "卡车截取当前角度保存纸面图片", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "新修订再次发布并留存历史版本", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "撤回本地正式版本", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| } | |||||
| ], | |||||
| "errors": [], | |||||
| "requests": [] | |||||
| } | |||||
| @@ -0,0 +1,905 @@ | |||||
| { | |||||
| "date": "2026-09-10", | |||||
| "conclusion": "未完全闭环;本地编辑与导出路径完成修复验证,正式内容、教学和对外 HTTP 接入待确认", | |||||
| "environment": { | |||||
| "web": "http://127.0.0.1:6180", | |||||
| "gateway": 6100, | |||||
| "browser": "Edge headless / Playwright", | |||||
| "viewport": "1920×1080;390×844 观察", | |||||
| "accounts": "root 编辑、admin 只读(不含凭据)" | |||||
| }, | |||||
| "pass": 73, | |||||
| "fail": 5, | |||||
| "unitTests": 20, | |||||
| "artifactChecks": [ | |||||
| { | |||||
| "name": "PDF 四页可解析且每页含文本", | |||||
| "status": "PASS", | |||||
| "detail": "4 pages" | |||||
| }, | |||||
| { | |||||
| "name": "OFD ZIP 完整、入口和 XML 可解析", | |||||
| "status": "PASS", | |||||
| "detail": "9 XML files; 未使用第三方 OFD 阅读器认证" | |||||
| }, | |||||
| { | |||||
| "name": "离线 ZIP 完整", | |||||
| "status": "PASS", | |||||
| "detail": "16 files" | |||||
| }, | |||||
| { | |||||
| "name": "离线脚本没有 Vite 开发缓存引用", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "离线狐狸和卡车 GLB 结构及动画保留", | |||||
| "status": "PASS", | |||||
| "detail": "导出按现有逻辑轻量化,卡车从 369980 缩至 318276 字节;非原文件逐字节复制。实际渲染和动画另由浏览器验证。" | |||||
| }, | |||||
| { | |||||
| "name": "离线包含本地 Three.js 和解码器", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| } | |||||
| ], | |||||
| "fixes": [ | |||||
| [ | |||||
| "不存在的工程地址自动新建", | |||||
| "runtime.ts", | |||||
| "不存在 ID 明确报错,返回列表选择,避免查看操作创建脏数据。", | |||||
| "missing" | |||||
| ], | |||||
| [ | |||||
| "列表默认排序及重置", | |||||
| "GuideProjectList.vue / content-lists.js", | |||||
| "统一按创建时间倒序;更新旧工程不应改变默认顺序。", | |||||
| "templates / layout" | |||||
| ], | |||||
| [ | |||||
| "只读用户无法选择导出和阅读控件", | |||||
| "ofd-v4.js", | |||||
| "放行导出格式、页码、缩放、搜索等阅读控件;OFD 导出不再尝试保存草稿。", | |||||
| "reader" | |||||
| ], | |||||
| [ | |||||
| "狐狸三维视口被环境雾完全遮住", | |||||
| "ofd-viewport.js", | |||||
| "单模型加载清除车间雾;加载场景仍使用场景自身环境配置。", | |||||
| "revision / offline" | |||||
| ], | |||||
| [ | |||||
| "默认模型相机距离不适配真实模型尺寸", | |||||
| "ofd-document.js", | |||||
| "缺省相机走自动取景,保留用户明确保存的相机位置。已有显式相机不批量覆盖。", | |||||
| "final-document / regression" | |||||
| ], | |||||
| [ | |||||
| "新建继承演示工程版本、反馈和装备参数", | |||||
| "ofd-v4.js", | |||||
| "新工程清空演示业务记录、零件、技术参数与来源文件;模板章节按原模板生成。", | |||||
| "templates" | |||||
| ], | |||||
| [ | |||||
| "开发环境导出的离线三维包加载失败", | |||||
| "ofd-offline-runtime-assets.js", | |||||
| "打包 Three.js 和解码器原始源码,消除 Vite 改写后的开发路径。", | |||||
| "offline / artifact-validation" | |||||
| ], | |||||
| [ | |||||
| "长标题、公式间距与动画秒数版式", | |||||
| "ofd-layout.js", | |||||
| "缺省长标题自适应,公式后增加必要间距,动画时间保留两位小数。", | |||||
| "PDF 四页目视 / regression" | |||||
| ], | |||||
| [ | |||||
| "反馈示例固定引用不存在的步骤", | |||||
| "ofd-v4.js", | |||||
| "默认反馈示例使用当前工程的实际步骤,避免新建工程执行示例直接 404。", | |||||
| "api:submit-feedback 201" | |||||
| ] | |||||
| ], | |||||
| "coverage": [ | |||||
| [ | |||||
| "列表", | |||||
| "列表加载、编号搜索、重置、创建时间排序", | |||||
| "本地通过", | |||||
| "templates / layout", | |||||
| "编辑旧记录后的多记录排序组合未单独量化。" | |||||
| ], | |||||
| [ | |||||
| "列表", | |||||
| "项目详情、状态、版本入口", | |||||
| "部分覆盖", | |||||
| "代码审查 + publish 截图", | |||||
| "统计是本地数据,不能作为教学统计。" | |||||
| ], | |||||
| [ | |||||
| "列表", | |||||
| "删除及所有批量/权限组合", | |||||
| "待补验证", | |||||
| "代码审查", | |||||
| "未删除用户工程;未对每一种授权组合做全矩阵测试。" | |||||
| ], | |||||
| [ | |||||
| "创建", | |||||
| "空白向导、基本信息、建档", | |||||
| "本地通过", | |||||
| "create", | |||||
| "正式后台不存在该新工程。" | |||||
| ], | |||||
| [ | |||||
| "创建", | |||||
| "四类模板静态/动态预览", | |||||
| "本地通过", | |||||
| "templates", | |||||
| "实际逐个打开并切换目录。" | |||||
| ], | |||||
| [ | |||||
| "创建", | |||||
| "模板 PDF、OFD 下载和选模板创建", | |||||
| "本地通过", | |||||
| "templates", | |||||
| "下载实测第一类;新建实测第四类(4 章 9 步)。" | |||||
| ], | |||||
| [ | |||||
| "创建", | |||||
| "模板来源与平台模板管理联动", | |||||
| "待确认改造", | |||||
| "静态 OFD_CONTENT_TEMPLATES", | |||||
| "当前不是后台模板库。" | |||||
| ], | |||||
| [ | |||||
| "创建", | |||||
| "演示历史、反馈、参数隔离", | |||||
| "修复通过", | |||||
| "templates", | |||||
| "仅修复新建,不清理用户既有文档。" | |||||
| ], | |||||
| [ | |||||
| "分析规划", | |||||
| "资料来源、需求规格、构建计划导出", | |||||
| "部分覆盖", | |||||
| "02-planning / export", | |||||
| "Markdown 下载已测;专业内容自动分析质量未验收。" | |||||
| ], | |||||
| [ | |||||
| "分析规划", | |||||
| "多装备、多层级、关键步骤覆盖", | |||||
| "待业务验收", | |||||
| "原始 DOCX 对照", | |||||
| "狐狸、卡车为功能夹具,不能证明真实维修覆盖率。" | |||||
| ], | |||||
| [ | |||||
| "编辑", | |||||
| "步骤属性、保存、刷新重开", | |||||
| "本地通过", | |||||
| "edit", | |||||
| "仅在同一测试浏览器项目空间持久化。" | |||||
| ], | |||||
| [ | |||||
| "编辑", | |||||
| "新步骤、续页、复制、撤销重做", | |||||
| "本地通过", | |||||
| "edit", | |||||
| "章节及步骤所有拖动组合未穷举。" | |||||
| ], | |||||
| [ | |||||
| "编辑", | |||||
| "表格单元格、合并拆分、边框", | |||||
| "本地通过", | |||||
| "layout", | |||||
| "填写后重开检查;未对所有样式排列组合做测试。" | |||||
| ], | |||||
| [ | |||||
| "编辑", | |||||
| "自由文本块、矩形标注", | |||||
| "本地通过", | |||||
| "layout", | |||||
| "自由排版允许用户自行移动重叠;未自动约束所有自由布局。" | |||||
| ], | |||||
| [ | |||||
| "编辑", | |||||
| "公式输入、说明、关联区排版", | |||||
| "修复通过", | |||||
| "content / regression / PDF", | |||||
| "两个公式回归与实际 PDF 检查。" | |||||
| ], | |||||
| [ | |||||
| "编辑", | |||||
| "字体、纸张、页眉页脚、所有标注形状", | |||||
| "部分覆盖", | |||||
| "属性面板巡检与源码", | |||||
| "不是每个属性都做了写入、重开、导出三重验证。" | |||||
| ], | |||||
| [ | |||||
| "资源", | |||||
| "本地 GLB 导入、绑定步骤", | |||||
| "本地通过", | |||||
| "edit / revision", | |||||
| "09-Fox.glb、08-CesiumMilkTruck.glb。" | |||||
| ], | |||||
| [ | |||||
| "资源", | |||||
| "后台模型制作/场景制作资源选择", | |||||
| "待确认改造", | |||||
| "本地 platformModels / 场景目录", | |||||
| "不能声称使用了之前在服务端创建的两个场景。" | |||||
| ], | |||||
| [ | |||||
| "资源", | |||||
| "单模型显示、取景、截图、动画", | |||||
| "修复通过", | |||||
| "revision / final-document", | |||||
| "实际图像和帧差验证,截图保存后重开保留。" | |||||
| ], | |||||
| [ | |||||
| "资源", | |||||
| "本地视频导入、播放、截帧保存", | |||||
| "本地通过", | |||||
| "resources", | |||||
| "使用 2 秒测试视频;不代表真实维修内容验证。" | |||||
| ], | |||||
| [ | |||||
| "资源", | |||||
| "音频、资料包、损坏模型、存储配额", | |||||
| "待补验证", | |||||
| "导入代码审查", | |||||
| "未对全部格式和存储异常做实际测试。" | |||||
| ], | |||||
| [ | |||||
| "资源", | |||||
| "模型热点、部件聚焦、三维与步骤语义关联", | |||||
| "部分覆盖", | |||||
| "属性面板 + 离线观察操作", | |||||
| "专业热点与真实装备语义尚需专项验证。" | |||||
| ], | |||||
| [ | |||||
| "预览", | |||||
| "静态版式、动态步骤搜索与跳转", | |||||
| "本地通过", | |||||
| "layout / templates", | |||||
| "在线预览位于本地编辑器。" | |||||
| ], | |||||
| [ | |||||
| "预览", | |||||
| "播放、暂停/重播、观察控制", | |||||
| "本地通过", | |||||
| "revision / offline", | |||||
| "两模型动画帧改变;离线观察按钮逐个执行。" | |||||
| ], | |||||
| [ | |||||
| "权限", | |||||
| "管理员只读、导出不修改文档", | |||||
| "修复通过", | |||||
| "reader(实际 admin 会话)", | |||||
| "编辑使用 root;未把三角色全接口授权矩阵当作已验证。" | |||||
| ], | |||||
| [ | |||||
| "权限", | |||||
| "账号间工程隔离、数据范围", | |||||
| "待确认改造", | |||||
| "共享本地存储命名空间", | |||||
| "正式数据应以现有后台权限和用户范围为准。" | |||||
| ], | |||||
| [ | |||||
| "发布", | |||||
| "预检清单、草稿预览、发布面板", | |||||
| "本地通过", | |||||
| "08-preflight / 09-publish-panel", | |||||
| "预检通过不等于后端发布成功。" | |||||
| ], | |||||
| [ | |||||
| "发布", | |||||
| "发布、修订、旧快照、新版本、撤回", | |||||
| "本地通过", | |||||
| "content / revision", | |||||
| "现状均为本地生命周期;没有实际发送后台发布请求。" | |||||
| ], | |||||
| [ | |||||
| "发布", | |||||
| "后台版本、OFD/离线交付物原子发布", | |||||
| "待确认改造", | |||||
| "ContentProjectService", | |||||
| "后台已要求 GUIDE 的 OFD + OFFLINE_HTML ZIP;现有页面未串联。" | |||||
| ], | |||||
| [ | |||||
| "发布", | |||||
| "审核策略", | |||||
| "代码确认", | |||||
| "ContentProjectService GUIDE 分支", | |||||
| "审核按现有配置可选;不引入强制审核新规则。" | |||||
| ], | |||||
| [ | |||||
| "导入导出", | |||||
| "工程 JSON、导入恢复、非法 JSON", | |||||
| "本地通过", | |||||
| "revision / resources", | |||||
| "JSON 不是完整资产备份;模型字节仍在 IndexedDB。" | |||||
| ], | |||||
| [ | |||||
| "导入导出", | |||||
| "实际 PDF 下载及四页版式", | |||||
| "修复通过", | |||||
| "final-pdf-page-1 至 4", | |||||
| "中文、目录、模型图、公式目视检查;字体存在替代。" | |||||
| ], | |||||
| [ | |||||
| "导入导出", | |||||
| "实际 OFD ZIP/XML", | |||||
| "本地通过", | |||||
| "artifact-validation", | |||||
| "未用第三方原生 OFD 阅读器验证标准一致性或签章。" | |||||
| ], | |||||
| [ | |||||
| "导入导出", | |||||
| "离线 ZIP、GLB 轻量化、运行时", | |||||
| "修复通过", | |||||
| "offline / artifact-validation", | |||||
| "卡车导出按已有逻辑压缩;结构与动画保留,非原字节复制。" | |||||
| ], | |||||
| [ | |||||
| "离线", | |||||
| "断开公网/业务服务后本地启动阅读", | |||||
| "本地通过", | |||||
| "offline", | |||||
| "Windows + Node.js 本地静态服务;没有依赖外站的请求。" | |||||
| ], | |||||
| [ | |||||
| "离线", | |||||
| "手机尺寸、麒麟、真实移动设备", | |||||
| "部分覆盖", | |||||
| "23-offline-mobile", | |||||
| "390×844 可显示,按钮换行与三维取景仍偏拥挤;未测实机、麒麟。" | |||||
| ], | |||||
| [ | |||||
| "教学", | |||||
| "学员指导书列表、正式阅读内容", | |||||
| "待确认改造", | |||||
| "正式内容查询 + teaching 接口查询", | |||||
| "不能从本地发布进入正式教学阅读。" | |||||
| ], | |||||
| [ | |||||
| "教学", | |||||
| "收藏、个人笔记、检索、进度", | |||||
| "待确认改造", | |||||
| "原始需求与离线入口核对", | |||||
| "离线缺个人笔记/收藏入口;需与在线用户身份及进度方案一并实现。" | |||||
| ], | |||||
| [ | |||||
| "教学", | |||||
| "学习统计、报表、训练/考试联动", | |||||
| "待确认改造", | |||||
| "后端 teaching/guides 与原始需求", | |||||
| "未因后台存在接口而判为全链路通过。" | |||||
| ], | |||||
| [ | |||||
| "接口", | |||||
| "九个本地契约操作与反馈样例", | |||||
| "修复通过", | |||||
| "api / revision", | |||||
| "八个本地请求逐个执行;生命周期通过发布 UI 单独实测。" | |||||
| ], | |||||
| [ | |||||
| "接口", | |||||
| "第三方 HTTP 调用、鉴权、正式反馈", | |||||
| "待确认改造", | |||||
| "api:零真实业务 HTTP 请求", | |||||
| "显示 /api/ofd 不代表服务端真实提供该接口。" | |||||
| ], | |||||
| [ | |||||
| "质量", | |||||
| "构建、单元回归、浏览器异常", | |||||
| "通过", | |||||
| "build 及 20 项测试", | |||||
| "选定最终 E2E 阶段无 pageerror;不包含早期定位脚本失败。" | |||||
| ], | |||||
| [ | |||||
| "质量", | |||||
| "三主题、键盘、长文档、并发和性能指标", | |||||
| "待补验证", | |||||
| "本轮边界", | |||||
| "未验证所有组合;不能宣称 <1s / ≥25fps / ≥99% / ≤10s 已达标。" | |||||
| ] | |||||
| ], | |||||
| "proposals": [ | |||||
| [ | |||||
| "A:编辑与正式内容闭环(优先)", | |||||
| "保留现有编辑器,将列表、工程保存、模板/模型/场景选择、权限和版本操作对接现有 content 接口。服务端为数据真值;明确迁移本地工程和 IndexedDB 文件,不能静默覆盖。发布时提交 OFD 和离线 ZIP,并遵循现有可选审核设置。", | |||||
| "验收:新建→编辑→刷新/换浏览器→发布→旧版本不变→撤回;正式数据库与界面一致,其他账号按权限可见。" | |||||
| ], | |||||
| [ | |||||
| "B:教学阅读闭环", | |||||
| "衔接现有教学指导书、阅读记录和反馈接口;补齐个人笔记、收藏、学习进度与统计。离线笔记是否回传及冲突处理需要与在线范围一起定。", | |||||
| "验收:教员发布→学员阅读→记录/笔记/反馈保存→教员统计可查询;账号隔离生效。" | |||||
| ], | |||||
| [ | |||||
| "C:对外接口真实联调", | |||||
| "将当前本地契约自检与真实 HTTP 接口对齐,文档标明真实路径、参数、错误及授权;沿用现有平台认证机制,不新增密钥文件或独立服务。", | |||||
| "验收:从独立客户端调用,读取/修改/反馈真实工程,未授权请求被拒绝。" | |||||
| ] | |||||
| ], | |||||
| "runs": [ | |||||
| { | |||||
| "phase": "create", | |||||
| "checks": [ | |||||
| { | |||||
| "name": "通过界面从空白模板创建指导书", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "创建调用正式内容接口", | |||||
| "status": "FAIL", | |||||
| "detail": "当前页面使用浏览器本地工程库" | |||||
| }, | |||||
| { | |||||
| "name": "新建工程出现在正式后端列表", | |||||
| "status": "FAIL", | |||||
| "detail": "" | |||||
| } | |||||
| ], | |||||
| "errors": [], | |||||
| "requests": [] | |||||
| }, | |||||
| { | |||||
| "phase": "edit", | |||||
| "checks": [ | |||||
| { | |||||
| "name": "步骤标题、说明、工具、安全、验收编辑保存", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "刷新后恢复已保存的本地内容", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "新增步骤后保存", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "正文续页和复制页面保存", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "撤销复制恢复页数", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "重做复制恢复页数", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "导入真实 Fox.glb 文件并登记本地资源", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| } | |||||
| ], | |||||
| "errors": [], | |||||
| "requests": [] | |||||
| }, | |||||
| { | |||||
| "phase": "reader", | |||||
| "checks": [ | |||||
| { | |||||
| "name": "查看角色的正文编辑控件禁用", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "查看角色可以使用导出菜单", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "查看角色可以使用页码和缩放", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "只读用户实际导出 OFD 且不修改工程", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| } | |||||
| ], | |||||
| "errors": [], | |||||
| "requests": [] | |||||
| }, | |||||
| { | |||||
| "phase": "missing", | |||||
| "checks": [ | |||||
| { | |||||
| "name": "访问不存在的指导书不会自动新建", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| } | |||||
| ], | |||||
| "errors": [], | |||||
| "requests": [] | |||||
| }, | |||||
| { | |||||
| "phase": "content", | |||||
| "checks": [ | |||||
| { | |||||
| "name": "公式与说明保存", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "发布保存本地正式版本", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| } | |||||
| ], | |||||
| "errors": [], | |||||
| "requests": [] | |||||
| }, | |||||
| { | |||||
| "phase": "revision", | |||||
| "checks": [ | |||||
| { | |||||
| "name": "修订不改变旧的正式版本快照", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "工程 JSON 导入恢复步骤与资源关联", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "狐狸动画真实改变渲染帧", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "狐狸截取当前角度保存纸面图片", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "卡车动画真实改变渲染帧", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "卡车截取当前角度保存纸面图片", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "新修订再次发布并留存历史版本", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "撤回本地正式版本", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| } | |||||
| ], | |||||
| "errors": [], | |||||
| "requests": [] | |||||
| }, | |||||
| { | |||||
| "phase": "templates", | |||||
| "checks": [ | |||||
| { | |||||
| "name": "四类模板列表", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "模板 1 静态版式预览", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "模板 1 动态目录切换", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "模板下载 pdf", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "模板下载 ofd", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "模板 2 静态版式预览", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "模板 2 动态目录切换", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "模板 3 静态版式预览", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "模板 3 动态目录切换", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "模板 4 静态版式预览", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "模板 4 动态目录切换", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "选用模板真正生成章节步骤", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "新文档不混入演示版本和反馈", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "新文档不混入其他装备参数与来源", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "列表默认按创建时间倒序", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| } | |||||
| ], | |||||
| "errors": [], | |||||
| "requests": [] | |||||
| }, | |||||
| { | |||||
| "phase": "layout", | |||||
| "checks": [ | |||||
| { | |||||
| "name": "插入表格、填写单元格、合并拆分与边框保存", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "插入正文自由块后保存", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "二维框标注保存", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "动态阅读检索步骤", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "表格、自由块、二维标注重开后保留", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "列表按编号检索", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "重置恢复创建时间排序", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| } | |||||
| ], | |||||
| "errors": [], | |||||
| "requests": [] | |||||
| }, | |||||
| { | |||||
| "phase": "api", | |||||
| "checks": [ | |||||
| { | |||||
| "name": "对外接口展示九个操作", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "本地接口调度 list-projects 返回成功", | |||||
| "status": "PASS", | |||||
| "detail": "200\nGET /api/ofd/projects\n10 ms\n{\n \"success\": true,\n \"data\": {\n \"total\": 3," | |||||
| }, | |||||
| { | |||||
| "name": "本地接口调度 get-project 返回成功", | |||||
| "status": "PASS", | |||||
| "detail": "200\nGET /api/ofd/projects/OFD-mtub76ed-01\n8 ms\n{\n \"success\": true,\n \"data\": {\n" | |||||
| }, | |||||
| { | |||||
| "name": "本地接口调度 update-step 返回成功", | |||||
| "status": "PASS", | |||||
| "detail": "200\nPUT /api/ofd/projects/OFD-mtub76ed-01/steps/step-01\n26 ms\n{\n \"success\": tru" | |||||
| }, | |||||
| { | |||||
| "name": "本地接口调度 register-resource 返回成功", | |||||
| "status": "PASS", | |||||
| "detail": "201\nPOST /api/ofd/projects/OFD-mtub76ed-01/resources\n35 ms\n{\n \"success\": true,\n" | |||||
| }, | |||||
| { | |||||
| "name": "本地接口调度 collect-resources 返回成功", | |||||
| "status": "PASS", | |||||
| "detail": "201\nPOST /api/ofd/projects/OFD-mtub76ed-01/collect\n44 ms\n{\n \"success\": true,\n " | |||||
| }, | |||||
| { | |||||
| "name": "本地接口调度 inspect 返回成功", | |||||
| "status": "PASS", | |||||
| "detail": "200\nPOST /api/ofd/projects/OFD-mtub76ed-01/inspect\n9 ms\n{\n \"success\": true,\n \"" | |||||
| }, | |||||
| { | |||||
| "name": "本地接口调度 delivery-manifest 返回成功", | |||||
| "status": "PASS", | |||||
| "detail": "200\nPOST /api/ofd/projects/OFD-mtub76ed-01/delivery-manifest\n6 ms\n{\n \"success\":" | |||||
| }, | |||||
| { | |||||
| "name": "本地接口调度 submit-feedback 返回成功", | |||||
| "status": "PASS", | |||||
| "detail": "201\nPOST /api/ofd/projects/OFD-mtub76ed-01/feedback\n25 ms\n{\n \"success\": true,\n " | |||||
| }, | |||||
| { | |||||
| "name": "接口面板未发送真实 HTTP 业务请求", | |||||
| "status": "PASS", | |||||
| "detail": "这里通过本地函数调度,需要接入正式 API 才可外部调用" | |||||
| } | |||||
| ], | |||||
| "errors": [], | |||||
| "requests": [] | |||||
| }, | |||||
| { | |||||
| "phase": "offline", | |||||
| "checks": [ | |||||
| { | |||||
| "name": "离线包章节步骤可以阅读与切换", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "离线模型 1 动画可播放", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "离线模型 1 观察操作可执行", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "离线模型 2 动画可播放", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "离线模型 2 观察操作可执行", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "断开公网和业务服务后没有外部依赖请求", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "离线个人批注与收藏入口", | |||||
| "status": "FAIL", | |||||
| "detail": "当前离线阅读器未提供原始需求中的个人笔记、收藏和学习进度回传" | |||||
| } | |||||
| ], | |||||
| "errors": [], | |||||
| "requests": [] | |||||
| }, | |||||
| { | |||||
| "phase": "final-document", | |||||
| "checks": [ | |||||
| { | |||||
| "name": "狐狸按实际尺寸取景后保存", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "卡车按实际尺寸取景后保存", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "两个模型的取景图片重开后保留", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| } | |||||
| ], | |||||
| "errors": [], | |||||
| "requests": [] | |||||
| }, | |||||
| { | |||||
| "phase": "export", | |||||
| "checks": [ | |||||
| { | |||||
| "name": "导出 project 下载文件", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "导出 requirement 下载文件", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "导出 plan 下载文件", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "导出 pdf 下载文件", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "导出 ofd 下载文件", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "导出 offline 下载文件", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| } | |||||
| ], | |||||
| "errors": [], | |||||
| "requests": [] | |||||
| }, | |||||
| { | |||||
| "phase": "resources", | |||||
| "checks": [ | |||||
| { | |||||
| "name": "导入本地视频并在原生播放器实际播放", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "视频当前帧海报保存到步骤", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "非法工程 JSON 被拒绝且原工程保留", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "编制工程同步到正式内容库", | |||||
| "status": "FAIL", | |||||
| "detail": "实测仅写入浏览器本地库;正式内容库查询 HTTP 200" | |||||
| }, | |||||
| { | |||||
| "name": "编制发布结果进入教学指导书列表", | |||||
| "status": "FAIL", | |||||
| "detail": "教学查询 HTTP 200" | |||||
| } | |||||
| ], | |||||
| "errors": [], | |||||
| "requests": [] | |||||
| } | |||||
| ] | |||||
| } | |||||
| @@ -0,0 +1,81 @@ | |||||
| { | |||||
| "checks": [ | |||||
| { | |||||
| "name": "四类模板列表", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "模板 1 静态版式预览", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "模板 1 动态目录切换", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "模板下载 pdf", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "模板下载 ofd", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "模板 2 静态版式预览", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "模板 2 动态目录切换", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "模板 3 静态版式预览", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "模板 3 动态目录切换", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "模板 4 静态版式预览", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "模板 4 动态目录切换", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "选用模板真正生成章节步骤", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "新文档不混入演示版本和反馈", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "新文档不混入其他装备参数与来源", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| }, | |||||
| { | |||||
| "name": "列表默认按创建时间倒序", | |||||
| "status": "PASS", | |||||
| "detail": "" | |||||
| } | |||||
| ], | |||||
| "errors": [], | |||||
| "requests": [] | |||||
| } | |||||
| @@ -0,0 +1,5 @@ | |||||
| { | |||||
| "checks": [], | |||||
| "errors": [], | |||||
| "requests": [] | |||||
| } | |||||
| @@ -0,0 +1,39 @@ | |||||
| """Validate real UI exports; run from ute2e. No authentication data is read.""" | |||||
| from pathlib import Path | |||||
| import struct | |||||
| import json | |||||
| import zipfile | |||||
| import xml.etree.ElementTree as ET | |||||
| from pypdf import PdfReader | |||||
| root = Path('reports/guide-closure-20260910') | |||||
| checks = [] | |||||
| def check(name, passed, detail=''): | |||||
| checks.append(dict(name=name, status='PASS' if passed else 'FAIL', detail=detail)) | |||||
| pdf = PdfReader(root / 'export-pdf.pdf') | |||||
| check('PDF 四页可解析且每页含文本', len(pdf.pages) == 4 and all(p.extract_text().strip() for p in pdf.pages), f'{len(pdf.pages)} pages') | |||||
| with zipfile.ZipFile(root / 'export-ofd.ofd') as z: | |||||
| xmls = [n for n in z.namelist() if n.lower().endswith('.xml')] | |||||
| for name in xmls: | |||||
| ET.fromstring(z.read(name)) | |||||
| check('OFD ZIP 完整、入口和 XML 可解析', z.testzip() is None and 'OFD.xml' in z.namelist() and len(xmls) > 4, f'{len(xmls)} XML files; 未使用第三方 OFD 阅读器认证') | |||||
| with zipfile.ZipFile(root / 'export-offline.zip') as z: | |||||
| names = z.namelist() | |||||
| check('离线 ZIP 完整', z.testzip() is None, f'{len(names)} files') | |||||
| scripts = [z.read(n) for n in names if n.endswith(('.js', '.html'))] | |||||
| check('离线脚本没有 Vite 开发缓存引用', not any(b'/.vite-cache/' in s or b'/node_modules/.pnpm/' in s for s in scripts)) | |||||
| glbs = [z.read(n) for n in names if n.lower().endswith('.glb')] | |||||
| originals = [Path('../../文档/素材') / n for n in ['09-Fox.glb', '08-CesiumMilkTruck.glb']] | |||||
| def structure(b): | |||||
| assert b[:4] == b'glTF' and struct.unpack_from('<I', b, 8)[0] == len(b) | |||||
| doc = json.loads(b[20:20 + struct.unpack_from('<I', b, 12)[0]]) | |||||
| return {k: len(doc.get(k, [])) for k in ['nodes', 'meshes', 'animations', 'materials', 'skins']} | |||||
| expected = [structure(p.read_bytes()) for p in originals] | |||||
| actual = [structure(b) for b in glbs] | |||||
| check('离线狐狸和卡车 GLB 结构及动画保留', expected == actual, '导出按现有逻辑轻量化,卡车从 369980 缩至 318276 字节;非原文件逐字节复制。实际渲染和动画另由浏览器验证。') | |||||
| check('离线包含本地 Three.js 和解码器', any('three.module' in n for n in names) and any(n.endswith('.wasm') for n in names)) | |||||
| (root / 'artifact-validation.json').write_text(json.dumps(dict(checks=checks), ensure_ascii=False, indent=2), encoding='utf-8') | |||||
| for item in checks: | |||||
| print(item['status'], item['name']) | |||||
| raise SystemExit(any(c['status'] != 'PASS' for c in checks)) | |||||
| @@ -0,0 +1,510 @@ | |||||
| import fs from 'node:fs' | |||||
| import path from 'node:path' | |||||
| import assert from 'node:assert/strict' | |||||
| import { chromium, expect } from '@playwright/test' | |||||
| import { login, call } from './role-permissions-live.mjs' | |||||
| const report = path.resolve('reports/guide-closure-20260910') | |||||
| const work = path.resolve('../../.codex-tmp/guide-closure-20260910') | |||||
| fs.mkdirSync(report, { recursive:true }); fs.mkdirSync(work, { recursive:true }) | |||||
| const phase = process.argv[2] || 'create' | |||||
| const fixtureFile = path.join(work, 'fixture.json') | |||||
| const fixture = fs.existsSync(fixtureFile) ? JSON.parse(fs.readFileSync(fixtureFile, 'utf8')) : {} | |||||
| const checks = [], errors = [], requests = [] | |||||
| const browser = await chromium.launch({ channel:'msedge', headless:true, args:['--enable-unsafe-swiftshader'] }) | |||||
| const session = await login(phase === 'reader' ? 'admin' : 'root') | |||||
| const stateFile = path.join(work, 'browser.json') | |||||
| const context = await browser.newContext({ viewport:{ width:1920, height:1080 }, ...(fs.existsSync(stateFile) ? { storageState:stateFile } : {}) }) | |||||
| await context.addInitScript(s => { | |||||
| sessionStorage.setItem('unreal-tran:web:access-token:v1', s.accessToken) | |||||
| sessionStorage.setItem('unreal-tran:web:refresh-token:v1', s.refreshToken) | |||||
| localStorage.setItem('unreal-tran:web:preferences:v1', JSON.stringify({ themeMode:'military' })) | |||||
| }, session) | |||||
| const page = await context.newPage() | |||||
| page.on('pageerror', e => errors.push(e.message)) | |||||
| page.on('request', r => { if (r.url().includes('/api/tran/')) requests.push({ method:r.method(), path:new URL(r.url()).pathname }) }) | |||||
| const check = (name, pass, detail='') => { checks.push({ name, status:pass ? 'PASS':'FAIL', detail }); console.log(`${pass ? 'PASS':'FAIL'} ${name}`) } | |||||
| const shot = async name => { await page.screenshot({ path:path.join(report, `${name}.png`), animations:'disabled' }) } | |||||
| const open = async url => { await page.goto(`http://127.0.0.1:6180${url}`); await expect(page.locator('.legacy-guide-page')).toBeVisible({ timeout:60000 }) } | |||||
| const state = async () => page.evaluate(async id => (await import('/src/features/guide-legacy/runtime.ts')).legacyGuideContentStore.getOFDProject(id)?.state, fixture.id) | |||||
| try { | |||||
| if (phase === 'create') { | |||||
| await open('/content/ofd') | |||||
| await page.getByRole('button', { name:'新建指导书', exact:true }).click() | |||||
| await page.locator('[data-wizard="name"]').fill('闭环实测-狐狸巡检作业指导-20260910') | |||||
| await page.locator('[data-wizard="code"]').fill('REVIEW-GUIDE-20260910') | |||||
| await page.locator('[data-wizard="equipment"]').fill('狐狸模型测试装备') | |||||
| await page.locator('[data-wizard="owner"]').fill('闭环验证') | |||||
| await page.locator('[data-action="wizard-next"]').click() | |||||
| await page.locator('[data-action="wizard-template"][data-template="__blank__"]').click() | |||||
| await page.locator('[data-action="wizard-next"]').click() | |||||
| await shot('01-create-wizard') | |||||
| await page.locator('[data-action="wizard-next"]').click() | |||||
| await page.waitForURL(/\/content\/ofd\/[^/]+\/studio\?view=analysis/) | |||||
| fixture.id = page.url().match(/\/ofd\/([^/]+)\/studio/)[1] | |||||
| fs.writeFileSync(fixtureFile, JSON.stringify(fixture)) | |||||
| await expect(page.locator('.ofd-view-analysis')).toBeVisible() | |||||
| const saved = await state() | |||||
| check('通过界面从空白模板创建指导书', saved?.project?.name.includes('闭环实测') && saved.chapters.length === 1) | |||||
| check('创建调用正式内容接口', requests.some(r=>r.method==='POST' && r.path.endsWith('/content/projects')), '当前页面使用浏览器本地工程库') | |||||
| const server = await call(`tran/v1/content/projects?type=GUIDE&keyword=${encodeURIComponent('闭环实测-狐狸巡检作业指导-20260910')}`, session.accessToken) | |||||
| check('新建工程出现在正式后端列表', server.status===200 && server.data.records.some(r=>r.name.includes('闭环实测-狐狸'))) | |||||
| await shot('02-planning') | |||||
| await page.locator('[data-action="goto"][data-view="editor"]').first().click() | |||||
| await expect(page.locator('.ofd-view-editor')).toBeVisible() | |||||
| await shot('03-editor-baseline') | |||||
| } | |||||
| if (phase === 'inspect') { | |||||
| await open(`/content/ofd/${fixture.id}/studio?view=editor`) | |||||
| await expect(page.locator('.ofd-view-editor')).toBeVisible() | |||||
| const dom = await page.locator('.ofd-app').evaluate(el=>({ | |||||
| actions:[...el.querySelectorAll('[data-action]')].map(x=>({action:x.dataset.action,text:(x.innerText||x.getAttribute('title')||'').slice(0,60),view:x.dataset.view,field:x.dataset.field})), | |||||
| fields:[...el.querySelectorAll('input,textarea,select')].map(x=>({field:x.dataset.field,stepField:x.dataset.stepField,projectField:x.dataset.projectField,placeholder:x.getAttribute('placeholder'),type:x.type,value:x.type==='password'?'':x.value.slice(0,80)})), | |||||
| })) | |||||
| fs.writeFileSync(path.join(work,'dom.json'),JSON.stringify(dom,null,2)) | |||||
| console.log(JSON.stringify(dom,null,2)) | |||||
| } | |||||
| if (phase === 'edit') { | |||||
| await open(`/content/ofd/${fixture.id}/studio?view=editor`) | |||||
| const first = (await state()).chapters[0].steps[0].id | |||||
| fixture.firstStep = first | |||||
| for (const [field,value] of Object.entries({title:'狐狸巡检目标识别',summary:'定位狐狸模型,观察目标外观,并逐项记录本次巡检结果。',tools:'测试模型、观察记录表',safety:'仅使用测试模型,不作为真实装备维修规程。',acceptance:'模型显示完整,能够识别并记录目标。'})) { | |||||
| await page.locator(`[data-step-field="${field}"]`).fill(value) | |||||
| await page.locator(`[data-step-field="${field}"]`).blur() | |||||
| } | |||||
| await page.locator('[data-action="save"]').click() | |||||
| await expect.poll(async()=> (await state()).chapters[0].steps[0].title).toBe('狐狸巡检目标识别') | |||||
| check('步骤标题、说明、工具、安全、验收编辑保存', (await state()).chapters[0].steps[0].summary.includes('逐项记录')) | |||||
| await page.reload(); await expect(page.locator('[data-step-field="title"]')).toHaveValue('狐狸巡检目标识别') | |||||
| check('刷新后恢复已保存的本地内容', true) | |||||
| const before = (await state()).chapters.flatMap(c=>c.steps).length | |||||
| await page.locator('[data-action="add-step"]').click() | |||||
| await page.locator('[data-step-field="title"]').fill('牛奶卡车状态检查') | |||||
| await page.locator('[data-step-field="title"]').blur() | |||||
| await page.locator('[data-action="save"]').click() | |||||
| check('新增步骤后保存', (await state()).chapters.flatMap(c=>c.steps).length===before+1) | |||||
| await page.locator('[data-action="page-add-for-step"]').last().click() | |||||
| await page.locator('[data-action="save"]').click() | |||||
| const pages=(await state()).documentPages.length | |||||
| await page.locator('[data-action="page-duplicate"]').click() | |||||
| await page.locator('[data-action="save"]').click() | |||||
| check('正文续页和复制页面保存', (await state()).documentPages.length===pages+1) | |||||
| await page.locator('[data-action="undo"]').click(); await page.locator('[data-action="save"]').click() | |||||
| check('撤销复制恢复页数', (await state()).documentPages.length===pages) | |||||
| await page.locator('[data-action="redo"]').click(); await page.locator('[data-action="save"]').click() | |||||
| check('重做复制恢复页数', (await state()).documentPages.length===pages+1) | |||||
| await shot('04-edit-pages') | |||||
| for (const tab of ['three','motion','annotation','formula','layout']) { | |||||
| const button=page.locator(`[data-action="property-tab"][data-property-tab="${tab}"]`) | |||||
| if (await button.count()) { await button.click(); check(`属性面板 ${tab} 打开`,true) } | |||||
| } | |||||
| await page.locator('#ofd-resource-file').setInputFiles(path.resolve('../../文档/素材/09-Fox.glb')) | |||||
| await expect.poll(async()=> (await state()).resources.some(r=>r.name.includes('Fox')), {timeout:30000}).toBe(true) | |||||
| check('导入真实 Fox.glb 文件并登记本地资源',true) | |||||
| await page.locator('[data-action="right-dock-tab"]').filter({hasText:'资源'}).click() | |||||
| await shot('05-import-fox') | |||||
| const actions=await page.locator('.ofd-app').evaluate(el=>[...el.querySelectorAll('[data-action]')].map(x=>({action:x.dataset.action,text:(x.innerText||x.title||'').slice(0,50),data:{...x.dataset}}))) | |||||
| fs.writeFileSync(path.join(work,'resource-actions.json'),JSON.stringify(actions,null,2)) | |||||
| fs.writeFileSync(fixtureFile,JSON.stringify(fixture)) | |||||
| } | |||||
| if (phase === 'reader') { | |||||
| await open(`/content/ofd/${fixture.id}/studio?view=editor`) | |||||
| await expect(page.locator('.ofd-view-editor')).toBeVisible() | |||||
| check('查看角色的正文编辑控件禁用',await page.locator('[data-step-field="title"]').isDisabled()) | |||||
| check('查看角色可以使用导出菜单',await page.locator('[data-field="export-target"]').isEnabled()) | |||||
| check('查看角色可以使用页码和缩放',await page.locator('[data-field="canvas-page"]').isEnabled() && await page.locator('[data-field="canvas-zoom-choice"]').isEnabled()) | |||||
| const before=JSON.stringify(await state()) | |||||
| const download=page.waitForEvent('download') | |||||
| await page.locator('[data-field="export-target"]').selectOption('ofd') | |||||
| await (await download).saveAs(path.join(report,'reader-export.ofd')) | |||||
| await page.waitForTimeout(700) | |||||
| check('只读用户实际导出 OFD 且不修改工程',before===JSON.stringify(await state())) | |||||
| await shot('reader-before') | |||||
| } | |||||
| if (phase === 'missing') { | |||||
| const unknown='REVIEW-NOT-EXIST-20260910' | |||||
| await open(`/content/ofd/${unknown}/studio?view=editor`) | |||||
| await page.waitForTimeout(1200) | |||||
| const exists=await page.evaluate(async id=>Boolean((await import('/src/features/guide-legacy/runtime.ts')).legacyGuideContentStore.getOFDProject(id)),unknown) | |||||
| check('访问不存在的指导书不会自动新建',!exists) | |||||
| await shot('missing-before') | |||||
| if(exists) await page.evaluate(async id=>(await import('/src/features/guide-legacy/runtime.ts')).legacyGuideContentStore.deleteOFDProject(id),unknown) | |||||
| } | |||||
| if (phase === 'media') { | |||||
| await open(`/content/ofd/${fixture.id}/studio?view=editor`) | |||||
| // Playwright storageState does not preserve Blob prototypes across contexts. | |||||
| // Re-import real bytes, then test reload inside this context below. | |||||
| await page.locator('#ofd-resource-file').setInputFiles(path.resolve('../../文档/素材/09-Fox.glb')) | |||||
| await page.waitForTimeout(1000) | |||||
| await page.reload() | |||||
| await expect(page.locator('.ofd-view-editor')).toBeVisible() | |||||
| await page.locator('[data-action="select-step"]').first().click() | |||||
| await page.locator('[data-action="right-dock-tab"]').filter({hasText:'资源'}).click() | |||||
| await page.locator('[data-action="library-insert"][data-kind="model"]').filter({hasText:'09-Fox.glb'}).first().click() | |||||
| await page.locator('[data-action="save"]').click() | |||||
| check('模型插入步骤并保存关联', (await state()).chapters[0].steps[0].mediaBlocks?.some(b=>b.assetId)) | |||||
| await page.locator('[data-action="right-dock-tab"]').filter({hasText:'资源'}).click() | |||||
| await page.locator('[data-action="library-insert"][data-kind="animation"]').filter({hasText:'Run'}).first().click() | |||||
| await page.locator('[data-action="save"]').click() | |||||
| check('关联导入模型的真实动画', JSON.stringify((await state()).chapters[0].steps[0]).includes('Run')) | |||||
| await page.locator('[data-action="media-open"]').first().click() | |||||
| await expect(page.locator('.ofd-media-panel canvas').first()).toBeVisible({timeout:30000}) | |||||
| await expect(page.locator('.ofd-media-panel')).not.toContainText('模型暂不可用', {timeout:30000}) | |||||
| await page.waitForTimeout(2500) | |||||
| await shot('06-fox-model-panel') | |||||
| await page.locator('[data-action="media-view"][data-view="all"]').click() | |||||
| await page.waitForTimeout(1500) | |||||
| await shot('06b-fox-framed') | |||||
| const controls=await page.locator('.ofd-media-panel').evaluate(el=>[...el.querySelectorAll('[data-action]')].map(x=>({action:x.dataset.action,text:(x.innerText||x.title||'').slice(0,60),data:{...x.dataset}}))) | |||||
| fs.writeFileSync(path.join(work,'media-controls.json'),JSON.stringify(controls,null,2)) | |||||
| check('三维模型放大面板渲染画布',true) | |||||
| await page.locator('[data-action="media-close"]').click() | |||||
| await page.locator('[data-action="right-dock-tab"]').filter({hasText:'属性'}).click() | |||||
| for (const [text, tab] of Object.entries({'三维':'model','动画':'animation','标注':'annotation','公式':'formula','版式':'layout','内容':'content'})) { | |||||
| await page.locator(`[data-action="property-tab"][data-tab="${tab}"]`).click() | |||||
| check(`步骤属性「${text}」可打开`,true) | |||||
| await shot(`property-${text}`) | |||||
| } | |||||
| await page.locator('#ofd-resource-file').setInputFiles(path.resolve('../../文档/素材/08-CesiumMilkTruck.glb')) | |||||
| await expect.poll(async()=> (await state()).resources.some(r=>r.name.includes('MilkTruck')), {timeout:30000}).toBe(true) | |||||
| await page.locator('[data-action="select-step"]').last().click() | |||||
| await page.locator('[data-action="right-dock-tab"]').filter({hasText:'资源'}).click() | |||||
| await page.locator('[data-action="library-insert"][data-kind="model"]').filter({hasText:'MilkTruck'}).first().click() | |||||
| await page.locator('[data-action="save"]').click() | |||||
| check('第二步骤绑定牛奶卡车模型',JSON.stringify((await state()).chapters.flatMap(c=>c.steps).at(-1)).includes('MilkTruck')) | |||||
| await shot('07-milk-truck-step') | |||||
| } | |||||
| if (phase === 'export') { | |||||
| await open(`/content/ofd/${fixture.id}/studio?view=editor`) | |||||
| for (const file of ['09-Fox.glb','08-CesiumMilkTruck.glb']) { | |||||
| await page.locator('#ofd-resource-file').setInputFiles(path.resolve('../../文档/素材',file)) | |||||
| await page.waitForTimeout(900) | |||||
| } | |||||
| for (const target of ['project','requirement','plan','pdf','ofd','offline']) { | |||||
| try { | |||||
| const waiting=page.waitForEvent('download',{timeout:90000}) | |||||
| await page.locator('[data-field="export-target"]').selectOption(target) | |||||
| const download=await waiting | |||||
| const file=path.join(report, `export-${target}${path.extname(download.suggestedFilename())}`) | |||||
| await download.saveAs(file) | |||||
| check(`导出 ${target} 下载文件`,fs.statSync(file).size>0) | |||||
| if(target==='project') fixture.exportFile=file | |||||
| } catch(error) { check(`导出 ${target}`,false,error.message); await shot(`export-${target}-failure`) } | |||||
| } | |||||
| fs.writeFileSync(fixtureFile,JSON.stringify(fixture)) | |||||
| await page.locator('[data-action="open-health"]').click() | |||||
| await shot('08-preflight') | |||||
| const details=await page.locator('.ofd-app').innerText() | |||||
| fs.writeFileSync(path.join(work,'preflight.txt'),details) | |||||
| await page.locator('[data-action="close-workspace-dialog"].ofd-icon-btn').click() | |||||
| await page.locator('[data-action="open-publish"]').click() | |||||
| await shot('09-publish-panel') | |||||
| const actions=await page.locator('.ofd-app').evaluate(el=>[...el.querySelectorAll('[data-action]')].map(x=>({action:x.dataset.action,text:(x.innerText||x.title||'').slice(0,60),data:{...x.dataset}}))) | |||||
| fs.writeFileSync(path.join(work,'publish-actions.json'),JSON.stringify(actions,null,2)) | |||||
| } | |||||
| if (phase === 'content') { | |||||
| await open(`/content/ofd/${fixture.id}/studio?view=editor`) | |||||
| await page.locator('[data-action="select-step"]').first().click() | |||||
| await page.locator('[data-action="right-dock-tab"]').filter({hasText:'属性'}).click() | |||||
| await page.locator('[data-action="property-tab"][data-tab="formula"]').click() | |||||
| await page.locator('[data-action="formula-add"]').click() | |||||
| await page.locator('[data-formula-field="source"]').last().fill('F = p \\times A') | |||||
| await page.locator('[data-formula-field="source"]').last().blur() | |||||
| await page.locator('[data-formula-field="caption"]').last().fill('闭环验证公式,仅用于排版测试') | |||||
| await page.locator('[data-formula-field="caption"]').last().blur() | |||||
| await page.locator('[data-action="save"]').click() | |||||
| check('公式与说明保存', (await state()).chapters[0].steps[0].formulas.some(f=>f.caption.includes('闭环验证'))) | |||||
| await shot('10-formula') | |||||
| await page.locator('[data-action="ribbon-tab"]').filter({hasText:'插入'}).click() | |||||
| fs.writeFileSync(path.join(work,'insert-dom.json'), JSON.stringify(await page.locator('.ofd-app').evaluate(el=>({actions:[...el.querySelectorAll('[data-action]')].map(x=>({...x.dataset,text:(x.innerText||x.title||'').slice(0,50)})),inputs:[...el.querySelectorAll('input,select,textarea')].map(x=>({id:x.id,...x.dataset,type:x.type,value:x.value.slice(0,40)}))})),null,2)) | |||||
| await shot('11-insert-tools') | |||||
| await page.locator('[data-action="open-publish"]').click() | |||||
| await shot('09-publish-panel') | |||||
| await page.locator('[data-action="lifecycle-action"][data-lifecycle-action="publish"]').click() | |||||
| await expect.poll(async()=>(await state()).lifecycle.status).toBe('published') | |||||
| check('发布保存本地正式版本',true) | |||||
| fixture.publishedTitle=(await state()).chapters[0].steps[0].title | |||||
| await shot('12-published') | |||||
| fs.writeFileSync(path.join(work,'published-state.json'),JSON.stringify(await state())) | |||||
| fs.writeFileSync(fixtureFile,JSON.stringify(fixture)) | |||||
| } | |||||
| if (phase === 'revision') { | |||||
| await open(`/content/ofd/${fixture.id}/studio?view=publish`) | |||||
| await page.locator('[data-action="lifecycle-action"][data-lifecycle-action="edit"]').click() | |||||
| await expect.poll(async()=>(await state()).lifecycle.status).toBe('draft') | |||||
| await page.locator('[data-action="goto"][data-view="editor"]').click() | |||||
| await page.locator('[data-action="select-step"]').first().click() | |||||
| await page.locator('[data-action="right-dock-tab"]').filter({hasText:'属性'}).click() | |||||
| await page.locator('[data-action="property-tab"][data-tab="content"]').click() | |||||
| await page.locator('[data-step-field="title"]').fill('狐狸巡检目标识别(修订)') | |||||
| await page.locator('[data-step-field="title"]').blur() | |||||
| await page.locator('[data-action="save"]').click() | |||||
| const edited=await state() | |||||
| check('修订不改变旧的正式版本快照',edited.lifecycle.publishedSnapshot.chapters[0].steps[0].title===fixture.publishedTitle && edited.chapters[0].steps[0].title!==fixture.publishedTitle) | |||||
| // 清理由重复运行测试产生的重叠媒体块,通过用户可用的 JSON 导入入口回灌。 | |||||
| const bundle={schema:'',state:edited} | |||||
| Object.assign(bundle,JSON.parse(fs.readFileSync(path.join(report,'export-project.json'),'utf8')),{state:edited}) | |||||
| for (const c of edited.chapters) for (const s of c.steps) { | |||||
| s.mediaBlocks=(s.mediaBlocks||[]).filter(b=>b.kind==='model').slice(-1) | |||||
| for (const b of s.mediaBlocks) { b.poster='';b.posterAssetSignature='';b.camera=null } | |||||
| } | |||||
| const cleanFile=path.join(work,'roundtrip.json');fs.writeFileSync(cleanFile,JSON.stringify(bundle)) | |||||
| await page.locator('#ofd-project-file').setInputFiles(cleanFile) | |||||
| await expect.poll(async()=>(await state()).chapters[0].steps[0].mediaBlocks.length).toBe(1) | |||||
| check('工程 JSON 导入恢复步骤与资源关联',true) | |||||
| for (const file of ['09-Fox.glb','08-CesiumMilkTruck.glb']) { | |||||
| await page.locator('#ofd-resource-file').setInputFiles(path.resolve('../../文档/素材',file));await page.waitForTimeout(1200) | |||||
| } | |||||
| await page.reload();await expect(page.locator('.ofd-view-editor')).toBeVisible() | |||||
| for (let i=0;i<2;i++) { | |||||
| await page.locator('[data-action="select-step"]').nth(i).click() | |||||
| const b=(await state()).chapters[0].steps[i].mediaBlocks[0] | |||||
| await page.locator(`[data-action="canvas-block"][data-block-id="step:${(await state()).chapters[0].steps[i].id}:media:${b.id}"]`).click() | |||||
| await page.locator('[data-action="media-open"]').first().click() | |||||
| await expect(page.locator('.ofd-media-panel canvas')).toBeVisible() | |||||
| await page.waitForTimeout(1300) | |||||
| await shot(i?'14-truck-panel':'13-fox-panel') | |||||
| const before=await page.locator('.ofd-media-panel canvas').screenshot() | |||||
| await page.locator('[data-action="media-play"]').click() | |||||
| await page.waitForTimeout(380) | |||||
| const after=await page.locator('.ofd-media-panel canvas').screenshot() | |||||
| check(`${i?'卡车':'狐狸'}动画真实改变渲染帧`,!before.equals(after)) | |||||
| await page.locator('[data-action="media-apply"]').click() | |||||
| await page.locator('[data-action="save"]').click() | |||||
| check(`${i?'卡车':'狐狸'}截取当前角度保存纸面图片`,(await state()).chapters[0].steps[i].mediaBlocks[0].poster.startsWith('data:image/jpeg')) | |||||
| } | |||||
| await shot('15-clean-editor') | |||||
| await page.locator('[data-action="open-publish"]').click() | |||||
| await page.locator('[data-publish="note"]').fill('狐狸与牛奶卡车模型闭环验证') | |||||
| await page.locator('[data-publish="note"]').blur() | |||||
| await page.locator('[data-action="lifecycle-action"][data-lifecycle-action="publish"]').click() | |||||
| await expect.poll(async()=>(await state()).lifecycle.status).toBe('published') | |||||
| check('新修订再次发布并留存历史版本',(await state()).versions.filter(v=>v.immutable).length>=2) | |||||
| await page.locator('[data-action="lifecycle-action"][data-lifecycle-action="withdraw"]').click() | |||||
| await expect.poll(async()=>(await state()).lifecycle.status).toBe('withdrawn') | |||||
| check('撤回本地正式版本',true) | |||||
| await shot('16-withdrawn') | |||||
| } | |||||
| if (phase === 'templates') { | |||||
| await open('/content/ofd/templates') | |||||
| const ids=await page.locator('[data-guide-tpl-action="select"]').evaluateAll(els=>els.map(e=>e.dataset.guideTplId)) | |||||
| check('四类模板列表',ids.length===4) | |||||
| for (let i=0;i<ids.length;i++) { | |||||
| const id=ids[i] | |||||
| await page.locator(`[data-guide-tpl-action="preview"][data-guide-tpl-id="${id}"]`).click() | |||||
| await expect(page.locator('.guide-tpl-ofd-pages')).toBeVisible() | |||||
| check(`模板 ${i+1} 静态版式预览`,true) | |||||
| await page.locator('[data-guide-tpl-mode="interactive"]').click() | |||||
| await page.locator('[data-guide-tpl-action="preview-step"]').last().click() | |||||
| check(`模板 ${i+1} 动态目录切换`,await page.locator('[data-guide-tpl-action="preview-step"]').last().evaluate(e=>e.classList.contains('active'))) | |||||
| if (i===0) { | |||||
| await shot('17-template-preview') | |||||
| for (const format of ['pdf','ofd']) { | |||||
| const wait=page.waitForEvent('download');await page.locator(`[data-guide-tpl-action="download"][data-guide-tpl-format="${format}"]`).click() | |||||
| await (await wait).saveAs(path.join(report,`template.${format}`));check(`模板下载 ${format}`,true) | |||||
| } | |||||
| } | |||||
| await page.locator('[data-guide-tpl-action="close-preview"][aria-label="关闭"]').click() | |||||
| } | |||||
| await page.locator('[data-guide-tpl-action="create"]').click() | |||||
| await page.locator('[data-wizard="name"]').fill('闭环实测-牛奶卡车指导书-20260910') | |||||
| await page.locator('[data-wizard="code"]').fill('REVIEW-GUIDE-TRUCK-20260910') | |||||
| await page.locator('[data-wizard="equipment"]').fill('牛奶卡车测试模型') | |||||
| await page.locator('[data-action="wizard-next"]').click() | |||||
| await page.locator('[data-action="wizard-next"]').click() | |||||
| await page.waitForURL(/view=analysis/) | |||||
| fixture.templateId=page.url().match(/\/ofd\/([^/]+)\/studio/)[1] | |||||
| const created=await page.evaluate(async id=>(await import('/src/features/guide-legacy/runtime.ts')).legacyGuideContentStore.getOFDProject(id).state,fixture.templateId) | |||||
| check('选用模板真正生成章节步骤',created.project.templateId===ids.at(-1) && created.chapters.length===4 && created.chapters.flatMap(c=>c.steps).length===9) | |||||
| check('新文档不混入演示版本和反馈',!created.versions.length && !created.feedback.length && !created.lifecycle.audit.length) | |||||
| check('新文档不混入其他装备参数与来源',!created.project.sourcePackage && !created.partsCatalog.length && !created.technicalParameters.length) | |||||
| await shot('18-new-template-guide') | |||||
| fs.writeFileSync(fixtureFile,JSON.stringify(fixture)) | |||||
| await open('/content/ofd') | |||||
| await expect(page.locator('.el-table__body-wrapper tbody tr').first()).toContainText('牛奶卡车指导书') | |||||
| check('列表默认按创建时间倒序',true) | |||||
| await shot('19-list-created-order') | |||||
| } | |||||
| if (phase === 'api') { | |||||
| await open('/content/ofd/api') | |||||
| await page.locator('[data-field="api-project"]').selectOption(fixture.id) | |||||
| const ids=await page.locator('[data-action="api-endpoint"]').evaluateAll(es=>es.map(e=>e.dataset.id)) | |||||
| check('对外接口展示九个操作',ids.length===9) | |||||
| // 写操作只操作隔离浏览器中的本轮测试文档,生命周期已单独实测。 | |||||
| for (const id of ids) { | |||||
| await page.locator(`[data-action="api-endpoint"][data-id="${id}"]`).click() | |||||
| if (/lifecycle|workflow|step-update|resource-register/.test(id)) continue | |||||
| await page.locator('[data-action="api-run"]').click() | |||||
| await page.waitForTimeout(300) | |||||
| const response=await page.locator('.ofd-api-result').innerText() | |||||
| fs.writeFileSync(path.join(work,`api-${id}.txt`),response) | |||||
| check(`本地接口调度 ${id} 返回成功`,/^20\d/.test(response),response.slice(0,80)) | |||||
| } | |||||
| check('接口面板未发送真实 HTTP 业务请求',requests.length===0,'这里通过本地函数调度,需要接入正式 API 才可外部调用') | |||||
| await shot('20-api-local') | |||||
| } | |||||
| if (phase === 'offline') { | |||||
| const external=[] | |||||
| page.on('console',msg=>{if(msg.type()==='error') console.log('BROWSER',msg.text())}) | |||||
| page.on('requestfailed',r=>console.log('NETWORK',r.url(),r.failure())) | |||||
| await context.route('**/*',r=>{ | |||||
| if (new URL(r.request().url()).host==='127.0.0.1:18766') return r.continue() | |||||
| external.push(r.request().url());return r.abort() | |||||
| }) | |||||
| await page.goto('http://127.0.0.1:18766') | |||||
| await page.locator('[data-action="start"]').click() | |||||
| await page.locator('[data-action="next"]').click() | |||||
| check('离线包章节步骤可以阅读与切换',await page.locator('.reader-panel:visible').count()>0) | |||||
| await shot('21-offline-reading') | |||||
| await page.locator('[data-action="open-model-library"]').click() | |||||
| console.log('OFFLINE DOM',await page.evaluate(()=>({scripts:[...document.scripts].map(s=>({type:s.type,src:s.src,size:s.textContent.length})),status:document.querySelector('[data-model-status]')?.textContent}))) | |||||
| await expect(page.locator('[data-model-stage] canvas')).toBeVisible({timeout:30000}) | |||||
| await expect(page.locator('[data-model-status]')).toContainText(/个网格/, {timeout:30000}) | |||||
| for (let i=0;i<2;i++) { | |||||
| await page.locator('[data-model-select]').selectOption(String(i)) | |||||
| await page.waitForTimeout(1400) | |||||
| await shot(`22-offline-model-${i}`) | |||||
| const before=await page.locator('[data-model-stage] canvas').screenshot() | |||||
| await page.locator('[data-model-action="play-animation"]').click();await page.waitForTimeout(500) | |||||
| const after=await page.locator('[data-model-stage] canvas').screenshot() | |||||
| check(`离线模型 ${i+1} 动画可播放`,!before.equals(after)) | |||||
| await page.locator('[data-model-action="pause-animation"]').click() | |||||
| for (const action of ['zoom-in','zoom-out','rotate','toggle-transparent','highlight','explode','reset']) await page.locator(`[data-model-action="${action}"]`).click() | |||||
| check(`离线模型 ${i+1} 观察操作可执行`,true) | |||||
| } | |||||
| check('断开公网和业务服务后没有外部依赖请求',external.length===0,external.join('\n')) | |||||
| check('离线个人批注与收藏入口',await page.getByText(/收藏|添加笔记|保存笔记/).count()>0,'当前离线阅读器未提供原始需求中的个人笔记、收藏和学习进度回传') | |||||
| await page.setViewportSize({width:390,height:844});await shot('23-offline-mobile') | |||||
| } | |||||
| if (phase === 'layout') { | |||||
| await open(`/content/ofd/${fixture.templateId}/studio?view=editor`) | |||||
| await page.waitForTimeout(700) | |||||
| console.log('LOAD',await page.locator('.el-message').allTextContents(),await page.locator('.legacy-guide-page').innerText().then(t=>t.slice(0,100))) | |||||
| const get=async()=>page.evaluate(async id=>(await import('/src/features/guide-legacy/runtime.ts')).legacyGuideContentStore.getOFDProject(id).state,fixture.templateId) | |||||
| await page.locator('[data-action="select-step"]:visible').first().click() | |||||
| await page.locator('[data-action="ribbon-tab"][data-tab="insert"]').click() | |||||
| await page.locator('[data-field="canvas-insert"]').selectOption('table') | |||||
| await page.locator('[data-action="table-cell"]').first().dblclick() | |||||
| await page.locator('[data-inline-field="cell"]').fill('检测项目') | |||||
| await page.locator('[data-inline-field="cell"]').blur() | |||||
| await page.locator('[data-action="ribbon-tab"][data-tab="home"]').click() | |||||
| await page.locator('[data-action="table-op"][data-op="merge-selection"]').click() | |||||
| await page.locator('[data-action="table-op"][data-op="split-cell"]').click() | |||||
| await page.locator('[data-field="table-border-style"]').selectOption('dashed') | |||||
| await page.locator('[data-action="table-op"][data-op="border-apply"]').click() | |||||
| await page.locator('[data-action="save"]').click() | |||||
| check('插入表格、填写单元格、合并拆分与边框保存',JSON.stringify(await get()).includes('检测项目')) | |||||
| await shot('24-table-edit') | |||||
| await page.locator('[data-action="ribbon-tab"][data-tab="insert"]').click() | |||||
| await page.locator('[data-field="canvas-insert"]').selectOption('text') | |||||
| await page.locator('[data-action="save"]').click() | |||||
| const stored=JSON.stringify(await get()) | |||||
| check('插入正文自由块后保存',stored.includes('extraBlocks')) | |||||
| await page.locator('[data-action="ribbon-tab"][data-tab="review"]').click() | |||||
| await page.locator('[data-action="canvas-tool"][data-tool="rect"]').click() | |||||
| const canvas=await page.locator('[data-action="annotate-canvas"]').boundingBox() | |||||
| await page.mouse.move(canvas.x+170,canvas.y+150) | |||||
| await page.mouse.down();await page.mouse.move(canvas.x+280,canvas.y+220,{steps:8});await page.mouse.up() | |||||
| await page.locator('[data-action="save"]').click() | |||||
| check('二维框标注保存',JSON.stringify(await get()).includes('markups')) | |||||
| await shot('25-markup') | |||||
| await page.locator('[data-action="canvas-tab"][data-tab="interactive"]').click() | |||||
| await page.locator('[data-field="interactive-search"]').fill('牛奶卡车') | |||||
| await expect(page.locator('.ofd-ia-search-results')).toBeVisible() | |||||
| check('动态阅读检索步骤',true) | |||||
| await page.locator('[data-action="interactive-search-clear"]').click() | |||||
| await page.locator('[data-action="interactive-next"]').click() | |||||
| await shot('26-interactive-search') | |||||
| await page.reload();await expect(page.locator('.ofd-view-editor')).toBeVisible() | |||||
| check('表格、自由块、二维标注重开后保留',JSON.stringify(await get()).includes('检测项目') && JSON.stringify(await get()).includes('markups')) | |||||
| await open('/content/ofd') | |||||
| await page.getByPlaceholder('搜索指导书标题、编号或作者').fill('REVIEW-GUIDE-TRUCK-20260910') | |||||
| await page.getByRole('button',{name:'搜索',exact:true}).click() | |||||
| check('列表按编号检索',await page.locator('.el-table__body-wrapper tbody tr').count()>=1) | |||||
| await page.getByRole('button',{name:'重置',exact:true}).click() | |||||
| check('重置恢复创建时间排序',await page.getByRole('combobox',{name:'指导书排序'}).getAttribute('aria-label')==='指导书排序') | |||||
| } | |||||
| if (phase === 'final-document') { | |||||
| await open(`/content/ofd/${fixture.id}/studio?view=editor`) | |||||
| const current=await state() | |||||
| const bundle=JSON.parse(fs.readFileSync(path.join(report,'export-project.json'),'utf8'));bundle.state=current | |||||
| current.documentPages=current.documentPages.filter(p=>p.contentMode!=='continuation') | |||||
| for (const c of current.chapters) for (const [i,s] of c.steps.entries()) { | |||||
| s.image='';s.modelRef.camera=null | |||||
| s.summary=i?'确认牛奶卡车测试模型完整显示,播放车轮动画并记录测试结果。':s.summary | |||||
| s.safety='测试素材仅用于功能验证。';s.acceptance='模型与动画可查看,记录保存后能够重新打开。' | |||||
| for (const b of s.mediaBlocks) { b.camera=null;b.poster='';b.posterAssetSignature='' } | |||||
| } | |||||
| const file=path.join(work,'final-document.json');fs.writeFileSync(file,JSON.stringify(bundle)) | |||||
| await page.locator('#ofd-project-file').setInputFiles(file) | |||||
| await page.waitForTimeout(800) | |||||
| for (const file of ['09-Fox.glb','08-CesiumMilkTruck.glb']) { | |||||
| await page.locator('#ofd-resource-file').setInputFiles(path.resolve('../../文档/素材',file));await page.waitForTimeout(1000) | |||||
| } | |||||
| await page.reload();await expect(page.locator('.ofd-view-editor')).toBeVisible() | |||||
| for (let i=0;i<2;i++) { | |||||
| await page.locator('[data-action="select-step"]').nth(i).click() | |||||
| const s=(await state()).chapters[0].steps[i],b=s.mediaBlocks[0] | |||||
| await page.locator(`[data-action="canvas-block"][data-block-id="step:${s.id}:media:${b.id}"]`).click() | |||||
| await page.locator('[data-action="media-open"]').first().click();await page.waitForTimeout(1300) | |||||
| await page.locator('[data-action="media-view"][data-view="all"]').click();await page.waitForTimeout(250) | |||||
| await shot(i?'28-final-truck':'27-final-fox') | |||||
| await page.locator('[data-action="media-apply"]').click();await page.locator('[data-action="save"]').click() | |||||
| check(`${i?'卡车':'狐狸'}按实际尺寸取景后保存`,(await state()).chapters[0].steps[i].mediaBlocks[0].poster.startsWith('data:image/jpeg')) | |||||
| } | |||||
| await page.reload();await expect(page.locator('.ofd-view-editor')).toBeVisible() | |||||
| check('两个模型的取景图片重开后保留',(await state()).chapters[0].steps.every(s=>s.mediaBlocks[0].poster.startsWith('data:image/jpeg'))) | |||||
| } | |||||
| if (phase === 'resources') { | |||||
| await open(`/content/ofd/${fixture.templateId}/studio?view=editor`) | |||||
| await page.locator('[data-action="select-step"]:visible').first().click() | |||||
| await page.locator('#ofd-resource-file').setInputFiles(path.resolve('../../.codex-tmp/guide-test-video.mp4')) | |||||
| await page.waitForTimeout(700) | |||||
| await page.locator('[data-action="right-dock-tab"]').filter({hasText:'资源'}).click() | |||||
| await page.locator('[data-action="library-insert"][data-kind="resource"]').filter({hasText:'guide-test-video'}).first().click() | |||||
| await page.locator('[data-action="media-open"]').first().click() | |||||
| const video=page.locator('[data-media-video]') | |||||
| await expect.poll(()=>video.evaluate(v=>v.readyState)).toBeGreaterThanOrEqual(2) | |||||
| const start=await video.evaluate(v=>{v.play();return v.currentTime}) | |||||
| await page.waitForTimeout(450) | |||||
| check('导入本地视频并在原生播放器实际播放',await video.evaluate(v=>v.currentTime)>start) | |||||
| await page.locator('[data-action="video-capture-poster"]').click() | |||||
| await shot('29-video-poster') | |||||
| await page.locator('[data-action="media-close"]').click() | |||||
| await page.locator('[data-action="save"]').click() | |||||
| const doc=await page.evaluate(async id=>(await import('/src/features/guide-legacy/runtime.ts')).legacyGuideContentStore.getOFDProject(id).state,fixture.templateId) | |||||
| check('视频当前帧海报保存到步骤',doc.chapters.flatMap(c=>c.steps).some(s=>s.mediaBlocks.some(b=>b.kind==='video' && b.poster.startsWith('data:image/jpeg')))) | |||||
| const original=JSON.stringify(doc) | |||||
| await page.locator('#ofd-project-file').setInputFiles({name:'broken.json',mimeType:'application/json',buffer:Buffer.from('{broken')}) | |||||
| await page.waitForTimeout(500) | |||||
| const after=await page.evaluate(async id=>(await import('/src/features/guide-legacy/runtime.ts')).legacyGuideContentStore.getOFDProject(id).state,fixture.templateId) | |||||
| check('非法工程 JSON 被拒绝且原工程保留',JSON.stringify(after)===original) | |||||
| await shot('30-invalid-import') | |||||
| const content=await call('tran/v1/content/projects?type=GUIDE&keyword=REVIEW-GUIDE-20260910',session.accessToken) | |||||
| const teaching=await call('tran/v1/teaching/guides?keyword=REVIEW-GUIDE-20260910',session.accessToken) | |||||
| check('编制工程同步到正式内容库',content.status===200 && (content.data?.records||[]).some(r=>r.code==='REVIEW-GUIDE-20260910'),'实测仅写入浏览器本地库;正式内容库查询 HTTP '+content.status) | |||||
| check('编制发布结果进入教学指导书列表',teaching.status===200 && (teaching.data?.records||[]).some(r=>r.code==='REVIEW-GUIDE-20260910'),'教学查询 HTTP '+teaching.status) | |||||
| } | |||||
| if (phase === 'viewport-probe') { | |||||
| await open(`/content/ofd/${fixture.id}/studio?view=editor`) | |||||
| const bytes=[...fs.readFileSync(path.resolve('../../文档/素材/09-Fox.glb'))] | |||||
| console.log(await page.evaluate(async bytes=>{ | |||||
| const {createGuideViewport}=await import('/src/features/guide-legacy/demo-src/ofd-viewport.js') | |||||
| const host=document.createElement('div'); host.style.cssText='position:fixed;inset:100px;background:white;z-index:99999';document.body.append(host) | |||||
| const v=createGuideViewport(); window.reviewViewport=v;v.attach(host);await v.load('review-fox',{bytes:new Uint8Array(bytes)}) | |||||
| return {scene:v.describeScene(),fog:v.scene.fog,camera:v.camera.position.toArray(),target:v.controls.target.toArray(),stats:v.loadStats} | |||||
| },bytes)) | |||||
| await page.waitForTimeout(1000); await shot('viewport-probe') | |||||
| await page.evaluate(()=>{window.reviewViewport.scene.fog=null;window.reviewViewport.invalidate()}) | |||||
| await page.waitForTimeout(500);await shot('viewport-without-fog') | |||||
| console.log(await page.evaluate(()=>{const v=window.reviewViewport;return {stats:v.renderer.info.render,disposed:v.disposed,needsRender:v.needsRender,camera:v.camera.position.toArray(),target:v.controls.target.toArray(),bounds:v.modelBox().getSize(new v.camera.position.constructor()).toArray()}})) | |||||
| } | |||||
| } catch(error) { | |||||
| await shot(`${phase}-failure`).catch(()=>{}) | |||||
| check(`${phase} 脚本执行`, false, error.message) | |||||
| process.exitCode=1 | |||||
| } finally { | |||||
| if (!['offline','viewport-probe','reader'].includes(phase)) await context.storageState({ path:stateFile, indexedDB:true }) | |||||
| fs.writeFileSync(path.join(report, `${phase}.json`), JSON.stringify({ checks, errors, requests }, null, 2)) | |||||
| await browser.close() | |||||
| } | |||||
| @@ -0,0 +1,93 @@ | |||||
| import fs from 'node:fs' | |||||
| import path from 'node:path' | |||||
| const root = path.resolve('reports/guide-closure-20260910') | |||||
| const phases = ['create','edit','reader','missing','content','revision','templates','layout','api','offline','final-document','export','resources'] | |||||
| const runs = phases.map(phase => ({ phase, ...JSON.parse(fs.readFileSync(path.join(root, `${phase}.json`), 'utf8')) })) | |||||
| const checks = runs.flatMap(run => run.checks.map(check => ({ phase:run.phase, ...check }))) | |||||
| const artifacts = JSON.parse(fs.readFileSync(path.join(root, 'artifact-validation.json'), 'utf8')) | |||||
| const fixes = [ | |||||
| ['不存在的工程地址自动新建', 'runtime.ts', '不存在 ID 明确报错,返回列表选择,避免查看操作创建脏数据。', 'missing'], | |||||
| ['列表默认排序及重置', 'GuideProjectList.vue / content-lists.js', '统一按创建时间倒序;更新旧工程不应改变默认顺序。', 'templates / layout'], | |||||
| ['只读用户无法选择导出和阅读控件', 'ofd-v4.js', '放行导出格式、页码、缩放、搜索等阅读控件;OFD 导出不再尝试保存草稿。', 'reader'], | |||||
| ['狐狸三维视口被环境雾完全遮住', 'ofd-viewport.js', '单模型加载清除车间雾;加载场景仍使用场景自身环境配置。', 'revision / offline'], | |||||
| ['默认模型相机距离不适配真实模型尺寸', 'ofd-document.js', '缺省相机走自动取景,保留用户明确保存的相机位置。已有显式相机不批量覆盖。', 'final-document / regression'], | |||||
| ['新建继承演示工程版本、反馈和装备参数', 'ofd-v4.js', '新工程清空演示业务记录、零件、技术参数与来源文件;模板章节按原模板生成。', 'templates'], | |||||
| ['开发环境导出的离线三维包加载失败', 'ofd-offline-runtime-assets.js', '打包 Three.js 和解码器原始源码,消除 Vite 改写后的开发路径。', 'offline / artifact-validation'], | |||||
| ['长标题、公式间距与动画秒数版式', 'ofd-layout.js', '缺省长标题自适应,公式后增加必要间距,动画时间保留两位小数。', 'PDF 四页目视 / regression'], | |||||
| ['反馈示例固定引用不存在的步骤', 'ofd-v4.js', '默认反馈示例使用当前工程的实际步骤,避免新建工程执行示例直接 404。', 'api:submit-feedback 201'], | |||||
| ] | |||||
| // 通过只表示这里写明的观察范围,不代表正式业务链路验收。 | |||||
| const coverage = [ | |||||
| ['列表','列表加载、编号搜索、重置、创建时间排序','本地通过','templates / layout','编辑旧记录后的多记录排序组合未单独量化。'], | |||||
| ['列表','项目详情、状态、版本入口','部分覆盖','代码审查 + publish 截图','统计是本地数据,不能作为教学统计。'], | |||||
| ['列表','删除及所有批量/权限组合','待补验证','代码审查','未删除用户工程;未对每一种授权组合做全矩阵测试。'], | |||||
| ['创建','空白向导、基本信息、建档','本地通过','create','正式后台不存在该新工程。'], | |||||
| ['创建','四类模板静态/动态预览','本地通过','templates','实际逐个打开并切换目录。'], | |||||
| ['创建','模板 PDF、OFD 下载和选模板创建','本地通过','templates','下载实测第一类;新建实测第四类(4 章 9 步)。'], | |||||
| ['创建','模板来源与平台模板管理联动','待确认改造','静态 OFD_CONTENT_TEMPLATES','当前不是后台模板库。'], | |||||
| ['创建','演示历史、反馈、参数隔离','修复通过','templates','仅修复新建,不清理用户既有文档。'], | |||||
| ['分析规划','资料来源、需求规格、构建计划导出','部分覆盖','02-planning / export','Markdown 下载已测;专业内容自动分析质量未验收。'], | |||||
| ['分析规划','多装备、多层级、关键步骤覆盖','待业务验收','原始 DOCX 对照','狐狸、卡车为功能夹具,不能证明真实维修覆盖率。'], | |||||
| ['编辑','步骤属性、保存、刷新重开','本地通过','edit','仅在同一测试浏览器项目空间持久化。'], | |||||
| ['编辑','新步骤、续页、复制、撤销重做','本地通过','edit','章节及步骤所有拖动组合未穷举。'], | |||||
| ['编辑','表格单元格、合并拆分、边框','本地通过','layout','填写后重开检查;未对所有样式排列组合做测试。'], | |||||
| ['编辑','自由文本块、矩形标注','本地通过','layout','自由排版允许用户自行移动重叠;未自动约束所有自由布局。'], | |||||
| ['编辑','公式输入、说明、关联区排版','修复通过','content / regression / PDF','两个公式回归与实际 PDF 检查。'], | |||||
| ['编辑','字体、纸张、页眉页脚、所有标注形状','部分覆盖','属性面板巡检与源码','不是每个属性都做了写入、重开、导出三重验证。'], | |||||
| ['资源','本地 GLB 导入、绑定步骤','本地通过','edit / revision','09-Fox.glb、08-CesiumMilkTruck.glb。'], | |||||
| ['资源','后台模型制作/场景制作资源选择','待确认改造','本地 platformModels / 场景目录','不能声称使用了之前在服务端创建的两个场景。'], | |||||
| ['资源','单模型显示、取景、截图、动画','修复通过','revision / final-document','实际图像和帧差验证,截图保存后重开保留。'], | |||||
| ['资源','本地视频导入、播放、截帧保存','本地通过','resources','使用 2 秒测试视频;不代表真实维修内容验证。'], | |||||
| ['资源','音频、资料包、损坏模型、存储配额','待补验证','导入代码审查','未对全部格式和存储异常做实际测试。'], | |||||
| ['资源','模型热点、部件聚焦、三维与步骤语义关联','部分覆盖','属性面板 + 离线观察操作','专业热点与真实装备语义尚需专项验证。'], | |||||
| ['预览','静态版式、动态步骤搜索与跳转','本地通过','layout / templates','在线预览位于本地编辑器。'], | |||||
| ['预览','播放、暂停/重播、观察控制','本地通过','revision / offline','两模型动画帧改变;离线观察按钮逐个执行。'], | |||||
| ['权限','管理员只读、导出不修改文档','修复通过','reader(实际 admin 会话)','编辑使用 root;未把三角色全接口授权矩阵当作已验证。'], | |||||
| ['权限','账号间工程隔离、数据范围','待确认改造','共享本地存储命名空间','正式数据应以现有后台权限和用户范围为准。'], | |||||
| ['发布','预检清单、草稿预览、发布面板','本地通过','08-preflight / 09-publish-panel','预检通过不等于后端发布成功。'], | |||||
| ['发布','发布、修订、旧快照、新版本、撤回','本地通过','content / revision','现状均为本地生命周期;没有实际发送后台发布请求。'], | |||||
| ['发布','后台版本、OFD/离线交付物原子发布','待确认改造','ContentProjectService','后台已要求 GUIDE 的 OFD + OFFLINE_HTML ZIP;现有页面未串联。'], | |||||
| ['发布','审核策略','代码确认','ContentProjectService GUIDE 分支','审核按现有配置可选;不引入强制审核新规则。'], | |||||
| ['导入导出','工程 JSON、导入恢复、非法 JSON','本地通过','revision / resources','JSON 不是完整资产备份;模型字节仍在 IndexedDB。'], | |||||
| ['导入导出','实际 PDF 下载及四页版式','修复通过','final-pdf-page-1 至 4','中文、目录、模型图、公式目视检查;字体存在替代。'], | |||||
| ['导入导出','实际 OFD ZIP/XML','本地通过','artifact-validation','未用第三方原生 OFD 阅读器验证标准一致性或签章。'], | |||||
| ['导入导出','离线 ZIP、GLB 轻量化、运行时','修复通过','offline / artifact-validation','卡车导出按已有逻辑压缩;结构与动画保留,非原字节复制。'], | |||||
| ['离线','断开公网/业务服务后本地启动阅读','本地通过','offline','Windows + Node.js 本地静态服务;没有依赖外站的请求。'], | |||||
| ['离线','手机尺寸、麒麟、真实移动设备','部分覆盖','23-offline-mobile','390×844 可显示,按钮换行与三维取景仍偏拥挤;未测实机、麒麟。'], | |||||
| ['教学','学员指导书列表、正式阅读内容','待确认改造','正式内容查询 + teaching 接口查询','不能从本地发布进入正式教学阅读。'], | |||||
| ['教学','收藏、个人笔记、检索、进度','待确认改造','原始需求与离线入口核对','离线缺个人笔记/收藏入口;需与在线用户身份及进度方案一并实现。'], | |||||
| ['教学','学习统计、报表、训练/考试联动','待确认改造','后端 teaching/guides 与原始需求','未因后台存在接口而判为全链路通过。'], | |||||
| ['接口','九个本地契约操作与反馈样例','修复通过','api / revision','八个本地请求逐个执行;生命周期通过发布 UI 单独实测。'], | |||||
| ['接口','第三方 HTTP 调用、鉴权、正式反馈','待确认改造','api:零真实业务 HTTP 请求','显示 /api/ofd 不代表服务端真实提供该接口。'], | |||||
| ['质量','构建、单元回归、浏览器异常','通过','build 及 20 项测试','选定最终 E2E 阶段无 pageerror;不包含早期定位脚本失败。'], | |||||
| ['质量','三主题、键盘、长文档、并发和性能指标','待补验证','本轮边界','未验证所有组合;不能宣称 <1s / ≥25fps / ≥99% / ≤10s 已达标。'], | |||||
| ] | |||||
| const proposals = [ | |||||
| ['A:编辑与正式内容闭环(优先)','保留现有编辑器,将列表、工程保存、模板/模型/场景选择、权限和版本操作对接现有 content 接口。服务端为数据真值;明确迁移本地工程和 IndexedDB 文件,不能静默覆盖。发布时提交 OFD 和离线 ZIP,并遵循现有可选审核设置。','验收:新建→编辑→刷新/换浏览器→发布→旧版本不变→撤回;正式数据库与界面一致,其他账号按权限可见。'], | |||||
| ['B:教学阅读闭环','衔接现有教学指导书、阅读记录和反馈接口;补齐个人笔记、收藏、学习进度与统计。离线笔记是否回传及冲突处理需要与在线范围一起定。','验收:教员发布→学员阅读→记录/笔记/反馈保存→教员统计可查询;账号隔离生效。'], | |||||
| ['C:对外接口真实联调','将当前本地契约自检与真实 HTTP 接口对齐,文档标明真实路径、参数、错误及授权;沿用现有平台认证机制,不新增密钥文件或独立服务。','验收:从独立客户端调用,读取/修改/反馈真实工程,未授权请求被拒绝。'], | |||||
| ] | |||||
| const pictures = [ | |||||
| ['19-list-created-order.png','新建模板工程与创建时间排序'],['01-create-wizard.png','空白创建向导'],['02-planning.png','资料与规划'],['17-template-preview.png','模板结构预览'], | |||||
| ['27-final-fox.png','狐狸真实模型显示与取景'],['28-final-truck.png','卡车真实模型显示与取景'],['29-video-poster.png','视频播放与当前帧海报'],['30-invalid-import.png','非法工程导入保留原文档'], | |||||
| ['24-table-edit.png','表格编辑测试(自由块为测试内容)'],['25-markup.png','矩形标注'],['26-interactive-search.png','动态阅读搜索'],['16-withdrawn.png','本地版本撤回'], | |||||
| ['20-api-local.png','接口自检:实际仍为本地调度'],['21-offline-reading.png','离线步骤阅读'],['22-offline-model-0.png','离线狐狸三维'],['22-offline-model-1.png','离线卡车三维'],['23-offline-mobile.png','手机尺寸观察:仍有拥挤'], | |||||
| ...[1,2,3,4].map(n=>[`final-pdf-page-${n}.png`,`最终 PDF 第 ${n} 页`]), | |||||
| ] | |||||
| const summary = {date:'2026-09-10',conclusion:'未完全闭环;本地编辑与导出路径完成修复验证,正式内容、教学和对外 HTTP 接入待确认',environment:{web:'http://127.0.0.1:6180',gateway:6100,browser:'Edge headless / Playwright',viewport:'1920×1080;390×844 观察',accounts:'root 编辑、admin 只读(不含凭据)'},pass:checks.filter(c=>c.status==='PASS').length,fail:checks.filter(c=>c.status==='FAIL').length,unitTests:20,artifactChecks:artifacts.checks,fixes,coverage,proposals,runs} | |||||
| fs.writeFileSync(path.join(root,'summary.json'),JSON.stringify(summary,null,2)) | |||||
| const esc = s => String(s??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])) | |||||
| const table = (heads,rows) => `<div class="table"><table><thead><tr>${heads.map(x=>`<th>${esc(x)}</th>`).join('')}</tr></thead><tbody>${rows.map(row=>`<tr>${row.map(x=>`<td>${esc(x)}</td>`).join('')}</tr>`).join('')}</tbody></table></div>` | |||||
| const html=`<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>作业指导闭环检查 · 20260910</title><style> | |||||
| body{margin:0;background:#eef4f3;color:#173d36;font:15px/1.7 system-ui,"Microsoft YaHei",sans-serif}main{max-width:1300px;margin:auto;padding:30px}header,section{background:white;border:1px solid #c9ded8;border-radius:12px;padding:26px;margin-bottom:20px}h1{margin:0 0 12px;font-size:28px}h2{margin:0 0 15px;font-size:22px}h3{font-size:18px}.notice{padding:18px;border-left:5px solid #cf8618;background:#fff5e5}.stats{display:flex;gap:15px;flex-wrap:wrap}.stats b{padding:12px 20px;border-radius:8px;background:#e9f5ef}.table{overflow:auto}table{border-collapse:collapse;width:100%;font-size:14px}td,th{border:1px solid #cfdfdb;padding:10px;vertical-align:top;text-align:left}th{background:#edf6f2}a{color:#087e62}.images{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:18px}figure{margin:0;border:1px solid #d8e4e0;padding:10px;border-radius:8px}figure img{width:100%;height:290px;object-fit:contain;background:#f1f5f4}figcaption{font-size:14px}code{background:#edf4f2;padding:2px 5px}nav{display:flex;gap:20px;flex-wrap:wrap}small{color:#56726b}@media(max-width:700px){main{padding:12px}section,header{padding:16px}.images{grid-template-columns:1fr}}@media print{body{background:white}main{padding:0}section{break-inside:auto}.images{grid-template-columns:1fr 1fr}a{color:inherit}}</style> | |||||
| <main><header><h1>作业指导模块:闭环检查与修复报告</h1><p>2026-09-10 · 资料 → 原型 → 设计 → 开发 → 测试</p><div class="notice"><strong>结论:未完全闭环。</strong>本地编制、三维/视频预览与导出已完成本轮验证;实际页面仍保存到浏览器本地库,尚未连通正式内容发布、教学阅读及对外 HTTP 接口。不能以本地“已发布”认定全系统发布成功。</div><p class="stats"><b>${summary.pass} 项 E2E 断言通过</b><b>${summary.fail} 项需求断言未满足</b><b>20 项回归通过</b><b>6 项交付物检查通过</b><b>构建通过</b></p><p>未满足的 5 项中,4 项重复印证正式内容/教学链路缺失,1 项为离线个人笔记和收藏入口缺失。通过项含“确认现状”的断言,不能直接换算为产品完成率。</p><nav><a href="#source">资料与范围</a><a href="#fixes">已修复</a><a href="#coverage">功能覆盖</a><a href="#confirm">待确认方案</a><a href="#evidence">截图</a><a href="#tests">测试明细</a></nav></header> | |||||
| <section id="source"><h2>1. 资料、原型与设计核对</h2><p>原始依据:<code>文档/桥梁装备“数字车间”资源建设需求参数.docx</code>。覆盖内容分析规划、标准模板、排版与媒体关联、PDF/离线交付、版本更新、教学阅读、个人记录、反馈和统计。未将未阅读的其他 PDF 算作本轮依据。</p><p>正式路由经 <code>LegacyGuidePageView.vue → runtime.ts → ofd-v4.js</code> 托管原型。工程存储为带命名空间的本地 ContentListStore(seed:false),资源字节在 IndexedDB;模板和平台资源选择仍使用本地实现。后台已有 content projects、guide templates、teaching guides 接口,现状是前后两条实现未衔接。</p><p>测试创建了空白狐狸文档和模板卡车文档;狐狸文档中加入卡车作为第二个模型步骤,对两种模型验证导出与离线阅读。由于服务端场景资源没有接通,本轮直接导入本地 GLB,不能声称走通了之前正式创建的狐狸/卡车场景引用。</p><p>小修保持原交互和文档结构;不重建编辑器、不新增独立服务、不改 DDL。本轮不提交 Git。</p></section> | |||||
| <section id="fixes"><h2>2. 开发修复</h2>${table(['问题','文件','处理','验证'],fixes)}</section> | |||||
| <section id="coverage"><h2>3. 全模块功能覆盖与边界</h2><p>按功能入口建立 ${coverage.length} 项检查矩阵。“部分覆盖”和“待补验证”明确保留,不表示已跑过所有按钮与组合。</p>${table(['范围','功能点','结论','证据','边界'],coverage)}</section> | |||||
| <section id="confirm"><h2>4. 需要确认的较大调整</h2><p>建议按 A → B → C 实施,复用现有后端和编辑器。以下尚未实施,避免在本次 bugfix 中静默改变存储、权限和发布规则。</p>${table(['范围','具体实施','验收闭环'],proposals)}<p>另有后续验证项:移动窄屏按钮与取景拥挤、完整主题/键盘矩阵、原生 OFD 阅读器、麒麟、长文档与性能压力测试。真实装备内容覆盖率及专业维修正确性需业务资料与实际装备验收。</p></section> | |||||
| <section><h2>5. 交付文件与复核方法</h2><p><a href="export-pdf.pdf">下载实测 PDF</a> · <a href="export-ofd.ofd">下载 OFD</a> · <a href="export-offline.zip">下载离线 ZIP</a> · <a href="export-project.json">下载工程 JSON</a> · <a href="export-requirement.md">需求规格</a> · <a href="export-plan.md">构建计划</a> · <a href="summary.json">完整 JSON 报告</a></p><p>离线 ZIP 完整解压后用其 Windows 启动脚本运行,需要已有 Node.js,通过本机 HTTP 打开,不能直接 file://。运行时和两个 GLB 均在包内,无公网或业务 API 依赖。OFD 已检查 ZIP/XML,PDF 四页已渲染核对;未宣称第三方 OFD 认证,PDF 字体跨平台一致性未验证。</p><p>测试数据保留在隔离的自动化浏览器空间,不在用户日常浏览器或服务端列表中。工程 JSON 含资源引用,不含所有模型原字节,导入编辑时需重新选择本地素材;离线 ZIP 是可独立复核的阅读交付物。下载内容使用测试文字,不能用于实际维修操作。</p>${table(['交付物检查','结果','说明'],artifacts.checks.map(c=>[c.name,c.status,c.detail]))}</section> | |||||
| <section id="evidence"><h2>6. 实际截图</h2><p>点击查看原图。截图用于证明对应操作,含测试自由文本/标注的页面不作为最终排版模板。</p><div class="images">${pictures.map(([file,title])=>`<figure><a href="${esc(file)}"><img loading="lazy" src="${esc(file)}" alt="${esc(title)}"></a><figcaption>${esc(title)}</figcaption></figure>`).join('')}</div></section> | |||||
| <section id="tests"><h2>7. 测试明细</h2><p>环境:Windows、Edge headless、Playwright,1920×1080;前端 6180,经 Gateway 6100;root 编辑、admin 只读。离线包通过隔离的 18766 本地静态服务运行并阻断其他主机请求。未将浏览器登录凭据写入报告。</p><p>命令:<code>node tools/guide-closure-live.mjs <phase></code>(多阶段顺序执行,不能并发共享浏览器状态);<code>python tools/guide-artifact-check.py</code>;<code>node --test scripts/guide-legacy-closure.test.mjs</code>;现有 5 个 guide 测试脚本(17 项);<code>pnpm run build</code>(含 vue-tsc 和构建后校验)。新旧单元测试共 20 项通过,构建完成,仅既有 chunk size 提示。</p><p>早期脚本定位失败、重复运行造成的选择器歧义、Playwright storageState 无法恢复 Blob 原型,都未作为产品缺陷统计;实际模型测试通过界面重新导入,再在同一上下文刷新验证持久化。原始中间记录保留用于追溯,最终统计只采用下面选定阶段。</p>${table(['阶段','断言','结果','说明'],checks.map(c=>[c.phase,c.name,c.status,c.detail]))}<p>最终选定阶段 pageerror 总数:${runs.reduce((n,r)=>n+r.errors.length,0)}。真实 HTTP 业务写请求为零是尚未接入的证据,而不是接口性能验证。</p></section></main></html>` | |||||
| fs.writeFileSync(path.join(root,'index.html'),html) | |||||
| console.log(JSON.stringify({pass:summary.pass,fail:summary.fail,coverage:coverage.length,screenshots:pictures.length})) | |||||