Replace approximate recipes with 27 source-matched component layouts
wiki CI and deployment / verify-and-deploy (push) Successful in 16m40s

This commit is contained in:
2026-10-26 16:02:03 +08:00
parent f81aed8ce8
commit a8515e575a
674 changed files with 18935 additions and 622 deletions
@@ -0,0 +1,22 @@
import {readFile,writeFile,mkdir} from 'node:fs/promises';
import path from 'node:path';
import layouts from '../src/content/layouts/reference/manifest.json';
const root=path.resolve(import.meta.dirname,'..'),target=path.join(root,'public/layout-reference');
const esc=(s:string)=>s.replaceAll('&','&amp;').replaceAll('<','&lt;').replaceAll('"','&quot;');
async function main(){
await writeFile(path.join(target,'manifest.json'),JSON.stringify(layouts,null,2)+'\n');
const runtime=await readFile(path.join(root,'src/content/layouts/reference/ReferenceLayout.tsx'),'utf8');await writeFile(path.join(target,'ReferenceLayout.tsx'),runtime);
for(const l of layouts){
const layers=l.elements.map(e=>{const[x,y,w,h]=e.box;return `<img data-component="${l.id}/${e.id}" data-kind="${e.kind}" src="${e.asset}" alt="${esc(e.kind==='text'?e.text||e.role+'(未辨识原字形)':'')}" style="left:${x/l.width*100}%;top:${y/l.height*100}%;width:${w/l.width*100}%;height:${h/l.height*100}%">`}).join('\n');
const html=`<!doctype html>\n<html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${l.id} · ${l.name}</title><style>body{margin:0;padding:20px;background:#e8e8e3;color:#222;font:15px/1.7 system-ui}main{display:grid;grid-template-columns:minmax(280px,560px) minmax(280px,1fr);gap:24px;max-width:1250px;margin:auto}.canvas{position:relative;isolation:isolate;aspect-ratio:${l.width}/${l.height};background:#f4f3ee}.canvas img{position:absolute;display:block}.canvas .base{inset:0;width:100%;height:100%}.overlay{position:absolute;z-index:10;pointer-events:none;inset:0;width:100%;height:100%;display:none}label{display:inline-flex;gap:5px;margin-right:16px}button,a{color:#244f9d}table{width:100%;border-collapse:collapse}td,th{padding:7px;border-bottom:1px solid #bbb;text-align:left;font-size:13px}.muted{color:#666}#detail{padding:12px;background:#fff}tr{cursor:pointer}tr:focus,tr:hover{background:#fff}fieldset{border:1px solid #aaa;margin:0 0 16px}.canvas img[data-kind="text"]{z-index:3}.canvas img[data-kind="decoration"]{z-index:2}.canvas img[data-kind="image"]{z-index:1}@media(max-width:800px){main{display:block}.canvas{max-width:560px;margin:auto}aside{margin-top:24px}}</style></head><body><main><section><h1>${l.id} / ${l.name}</h1><div class="canvas" aria-label="按独立图层重建的原版面"><img class="base" src="${l.background}" alt="原图纸底与画框">\n${layers}\n<img class="overlay" src="${l.annotated}" alt="构件标注"></div><p class="muted">原图第 ${l.source.row} 行、第 ${l.source.column} 列;${l.width}×${l.height} 像素。显示为原始构件重组,无新生成图形。放大不增加细节。</p></section><aside><fieldset><legend>按类型检查</legend><label><input type="checkbox" data-toggle="text" checked>文字 T</label><label><input type="checkbox" data-toggle="image" checked>图片/色块 I</label><label><input type="checkbox" data-toggle="decoration" checked>装饰 D</label><label><input id="labels" type="checkbox">显示边界</label></fieldset><p id="detail">点击表格查看对应构件。蓝色 T 为文字、橙色 I 为图片占位/色块、绿色 D 为装饰;原图没有真实照片。</p><table><thead><tr><th>ID</th><th>分类与用途</th><th>文字内容</th></tr></thead><tbody>${l.elements.map(e=>`<tr tabindex="0" data-key="${e.id}"><td>${e.id}</td><td>${e.kind==='text'?'文字':e.kind==='image'?'图片占位/色块':'装饰'} / ${esc(e.role)}</td><td>${esc(e.kind==='text'?e.text??'待核对,保留原字形':'—')}</td></tr>`).join('')}</tbody></table><p><a href="/layout-reference/${l.id}/manifest.json">构件 JSON</a> · <a href="${l.reference}">原裁片</a> · <a href="${l.annotated}">标注图</a> · <a href="/layout-reference/index.html">27 张索引</a></p><p class="muted">修改文字会产生字体差异;替换图片会改变视觉内容。严格原图对应模式保持原字形与原颜色。构件坐标、方向与所属版面固定。</p></aside></main><script>
const manifest=${JSON.stringify(l).replaceAll('<','\\u003c')};
for(const input of document.querySelectorAll('[data-toggle]'))input.addEventListener('change',()=>{for(const el of document.querySelectorAll('[data-kind="'+input.dataset.toggle+'"]'))el.style.visibility=input.checked?'visible':'hidden'});
document.getElementById('labels').addEventListener('change',event=>{document.querySelector('.overlay').style.display=event.target.checked?'block':'none'});
for(const row of document.querySelectorAll('[data-key]')){const select=()=>{const e=manifest.elements.find(e=>e.id===row.dataset.key);for(const el of document.querySelectorAll('[data-component]'))el.style.outline=el.dataset.component.endsWith('/'+e.id)?'2px solid #ed4433':'';document.getElementById('detail').textContent=manifest.id+'/'+e.id+' · '+e.kind+' · '+e.role+' · bbox ['+e.box.join(', ')+']'+(e.kind==='text'?' · '+(e.text||'内容未辨识,保留原字形'):'')};row.addEventListener('click',select);row.addEventListener('keydown',event=>{if(event.key==='Enter'||event.key===' '){event.preventDefault();select()}})}
</script></body></html>\n`;
await writeFile(path.join(target,l.id,'index.html'),html);
}
await writeFile(path.join(target,'index.html'),`<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>27 张原图版面索引</title><style>body{font:16px/1.6 system-ui;background:#eee;padding:24px}nav{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:18px}a{color:#222;text-decoration:none;background:#fff;padding:12px}img{width:100%;aspect-ratio:113/173;object-fit:contain}small{display:block}</style><h1>27 张原图版面 · 一一对应</h1><p>按原图从上到下、每行从左到右编号,排除左侧说明栏。点击每张可分开检查文字、图片占位/色块与装饰。</p><p><a href="/layout-reference/numbered-source.png">查看编号原图</a> · <a href="/layout-reference/index.json">机器索引</a></p><nav>${layouts.map(l=>`<a href="/layout-reference/${l.id}/index.html"><img src="${l.reference}" alt="${l.id} 原裁片"><b>${l.id}</b><small>${esc(l.name)}</small><small>原图 ${l.source.row} 行 ${l.source.column} 列</small></a>`).join('')}</nav></html>\n`);
console.log('Built 27 layer inspectors and the source ID index.');
}
main().catch(e=>{console.error(e);process.exitCode=1});