import fs from 'node:fs/promises'; import path from 'node:path'; import assert from 'node:assert/strict'; import { pathToFileURL } from 'node:url'; import { chromium } from '@playwright/test'; const folder = path.resolve('reports/bridge-inspection-20260915'); const read = async file => JSON.parse(await fs.readFile(path.join(folder, file), 'utf8')); const tasks = [ { id: 86, code: '005', title: '液压泵应急切换', steps: 5 }, { id: 87, code: '006', title: '液压泵应急替代', steps: 7 }, { id: 88, code: '007', title: '前泵换件修复', steps: 11 }, { id: 89, code: '008', title: '平衡阀拆拼互换', steps: 16 }, ]; for (const task of tasks) { const result = await read(`task-${task.id}-verification.json`); assert.equal(result.pass, true); assert.equal(result.errors.length, 0); assert.ok(result.writes.every(url => url.endsWith(`/assignments/${task.id}/preview/validate`))); task.actions = result.checks.filter(check => check.target).length; task.validationRequests = result.writes.length; } const lan = '../bridge-inspection-lan-20260915'; for (const file of ['task-87-click-verification.json', `${lan}/click-flow-1280.json`, `${lan}/controls-1024.json`]) { assert.equal((await read(file)).pass, true); } const validation = await read('validation.json'); assert.equal(validation.build.pass, true); const summary = { pass: true, generatedAt: new Date().toISOString(), tasks, steps: tasks.reduce((sum, task) => sum + task.steps, 0), actions: tasks.reduce((sum, task) => sum + task.actions, 0), validationRequests: tasks.reduce((sum, task) => sum + task.validationRequests, 0), environments: ['http://127.0.0.1:6180', 'http://192.168.31.168:6180'], resolutions: ['1366×768', '1280×720', '1024×768'], accountType: '管理员 / 教员预览', residualData: '未新增教学任务;未写入学员成绩。仅调用现有任务的教员预览校验接口。', validation, }; await fs.writeFile(path.join(folder, 'summary.json'), JSON.stringify(summary, null, 2)); const requirements = [ ['逐步聚焦', '每个步骤和子动作自动对准对应部件,移出的螺栓、泵体随动作跟随。相机取景避开教员和面板。'], ['稳定距离与 HDR', '四套脚本使用统一的相机观察距离(约 2.5322),按部件尺寸调整光学缩放;曝光 1、环境光 0.8、主光 1.4 在步骤间不变。Venice Sunset HDR 已随项目静态交付。'], ['实体与轮廓', '物理网格使用双面材质,OutlinePass 绘制轮廓辉光。聚焦内部部件时局部隐藏遮挡外壳;复位后恢复。实体透明度不因选中或高亮而变化。'], ['小屏与全屏', '步骤栏、右侧面板可收起,小屏默认收起;操作区支持全屏、按钮退出和 Esc 退出。1024×768、1280×720 均无页面横向溢出。'], ['模型动作', '旋转、拆卸、移动和连接采用模型动画;拖动时模型实时跟随。纯旋转开关的端部铰点在动作中保持固定。点击模式也完成实际动画后才提交校验。'], ['标签', '去掉前导圆点,动态避让部件、浮层及其他标签;无法安放的额外标签隐藏。点击只聚焦放大,不产生步骤校验或成绩写入。'], ['高亮恢复', '选中部件后反复开关问题高亮,全部材质透明度与保存的基线一致,不残留整车半透明状态。'], ['隐藏其他', '隐藏其他模型时同时隐藏其标签;点标签后会同步查看目标,避免沿用之前的整车选择。'], ['操作范围', '流程目标继续按步骤顺序判断;非流程整车网格可点击查看、拖动旋转视角,不增加操作错误、不调用步骤校验。'], ]; const html = `四套桥梁脚本 · 3D 交互验收

四套桥梁脚本 · 3D 交互验收

2026-09-15 · 正式 HTML + JavaScript 工作区 · 管理员账号 / 教员预览

通过:${summary.steps} 个步骤、${summary.actions} 个模型操作、${summary.validationRequests} 次真实服务端预览校验。
另完成 005、006 点击动画回归,以及局域网小屏、自由查看、材质恢复和全屏退出检查。构建、正式路由契约及 16 项模型/脚本/讲解视频测试通过。

检查范围与结果

${requirements.map(([name, detail]) => ``).join('')}
需求实现及验证
${name}${detail}

各任务实际截图

${tasks.map(task => `
TASK-VIRTUAL-${task.code} · ${task.title}

${task.steps} 步 / ${task.actions} 次模型操作 / ${task.validationRequests} 次预览校验

${task.title}初始聚焦
查看各步完成截图与记录
${Array.from({length:task.steps},(_,i)=>`第 ${i+1} 步`).join('')}

全屏截图 · 完整 JSON

`).join('')}

局域网与小屏

入口:127.0.0.1:6180192.168.31.168:6180,页面 API 经 6100 Gateway。分辨率:1366×768、1280×720、1024×768。以下为 1280×720 的截图。

小屏自由查看

整车选择、拖动视角、切换高亮与 Esc 退出后。

全屏聚焦

全屏内仍可使用工具、讲解和退出按钮。

1024 小屏记录 · 1280 点击流程记录 · 006 点击动画记录

验证边界

${summary.residualData} 008 中需要观察记录的管路步骤仍要求记录并保留教员复核,未用界面操作绕过该条件。本轮验证的是三维交互与预览校验,未新增实装尺寸、受力或扭矩测量能力。截图中的补充模型沿用任务原有资源。

运行方式:Edge / Playwright;检查脚本 tools/bridge-inspection-live.mjs 仅注入只读诊断,模型操作全部由真实鼠标点击或拖动完成。

HDR 来自 Three.js r185 示例,原资源 Poly Haven / Greg Zaal / Venice Sunset(CC0),静态文件已纳入构建,无运行时外网请求。

汇总 JSON · 命令与构建结果

本报告保留关键截图及通过记录;较早调试文件可能包含失败尝试,以本报告链接记录为准。
`; await fs.writeFile(path.join(folder, 'index.html'), html); const browser = await chromium.launch({channel:'msedge',headless:true}); try { const page = await browser.newPage({viewport:{width:1440,height:1000}}); await page.goto(pathToFileURL(path.join(folder,'index.html')).href); assert.ok(await page.locator('img').evaluateAll(images => images.every(image => image.complete && image.naturalWidth > 0)), 'all report screenshots load'); await page.screenshot({path:path.join(folder,'report-overview.png')}); } finally { await browser.close(); } console.log(JSON.stringify({pass:true,steps:summary.steps,actions:summary.actions,report:path.join(folder,'index.html')}));