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-controls-review-20260915'); const read=async file=>JSON.parse((await fs.readFile(path.join(folder,file),'utf8')).replace(/^\uFEFF/,'')); 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.deepEqual(result.errors,[]); assert.ok(result.writes.every(url=>url.endsWith(`/assignments/${task.id}/preview/validate`))); task.actions=result.checks.filter(check=>check.target).length; task.validations=result.writes.length; task.workflowTargets=result.checks[0].state.workflowCoverage.length; } const lan='../bridge-controls-lan-20260915'; const small='../bridge-controls-small-20260915'; assert.equal((await read(`${lan}/desktop-1920.json`)).pass,true); assert.equal((await read(`${small}/controls-1024.json`)).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((n,t)=>n+t.steps,0),actions:tasks.reduce((n,t)=>n+t.actions,0), validationRequests:tasks.reduce((n,t)=>n+t.validations,0),validation, environments:['http://127.0.0.1:6180','http://192.168.31.168:6180'], resolutions:['1920×1080','1366×768','1280×720','1024×768'], boundary:'现有任务的管理员/教员预览;未创建任务或写入学员成绩。005 小屏使用点击,006–008 使用拖动;另在局域网大屏验证 005 拖动。', }; await fs.writeFile(path.join(folder,'summary.json'),JSON.stringify(summary,null,2)); const requirements=[ ['底栏图标','收起/展开步骤、面板及全屏放到底栏右侧。按钮仅显示图标,title 和无障碍名称随状态切换;全屏仍保留底栏和退出按钮。'], ['初始化取景','进入场景显示整车与操作台总览,按可用显示区域取景,避开教员和工具。点击标签或进入后续步骤时再聚焦,普通聚焦与主动放大分别限制显示尺寸。'], ['全部流程辉光','训练/预览进入场景时,所有当前可见的流程部件以青色轮廓显示,当前操作目标以金色区分。正式考试保持原有提示限制。'], ['独立半透明工具','新增“其他半透明”,保持流程部件实体显示。与“高亮问题设备”独立切换;关闭后恢复保存的材质透明度,不影响地形。'], ['正面与动作方向','根据实际 GLB 校准各类部件观察方向。互救面板从 -X 侧正视,文字可正读;开关绕靠近面板的固定端向“开”的上方旋转。005、006 共用校准;取力器手柄铰点也改为靠近壳体的一端。'], ['同类回归','逐项执行四套流程,验证所有目标的实际网格命中、固定观察距离、灯光一致、标签不重叠和服务端预览校验。008 两处观察记录仍按界面流程填写并留给教员复核。'], ]; const photo=(file,alt)=>`${alt}`; const html=`桥梁流程 · 取景与操作控件验收

桥梁流程 · 取景与操作控件验收

2026-09-15 · 正式 HTML + JavaScript 工作区

通过 ${summary.steps} 个步骤、${summary.actions} 个模型操作、${summary.validationRequests} 次服务端预览校验。
本地及局域网通过;生产构建、正式路由契约和 16 项相关测试通过。

本轮六项调整

${requirements.map(([title,body])=>``).join('')}
${title}${body}

初始总览

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

${t.steps} 步 / ${t.actions} 次模型操作 / ${t.workflowTargets} 个可见流程目标

${photo(`task-${t.id}-initial.png`,t.title+'初始总览')}
逐步截图和校验
${Array.from({length:t.steps},(_,i)=>`第 ${i+1} 步`).join('')}

完整校验 JSON · 其他半透明

`).join('')}

大屏初始视角与互救开关

${photo(`${lan}/task-86-initial.png`,'1920 初始总览')}

1920×1080:保留整车环境,初始不直接放大液压泵。

${photo(`${lan}/task-86-rescue-moving.png`,'互救开关向上转动')}

实际拖动中:从面板正面观察,手柄端点向上移动,固定端不位移。脚本同时验证世界坐标变化。

底栏、全屏与小屏

${photo('task-86-fullscreen.png','1280 全屏及底栏')}

1280×720 全屏:底栏右侧保留面板和退出全屏图标。

${photo(`${small}/free-inspection-controls.png`,'1024 小屏控件')}

1024×768:模型自由查看、材质切换及 Esc 退出,无页面横向溢出。

局域网 1920 完整拖动流程 · 局域网 1024 控件回归

验证范围

${summary.boundary} 所有操作均使用真实鼠标输入;浏览器注入仅用于读取相机、材质、网格和旋转端点。观察距离、可操作性与方向校验对应现有模型,未新增机械受力仿真。

汇总 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(imgs=>imgs.every(img=>img.complete&&img.naturalWidth>0))); 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')}));