]> acesimba.cloud Git - StationBuilder.git/commitdiff
领域方案 Cube 扩展页面:来源徽标 + Json文件 下载
authorwang.yu <wang.yu@advantech.com>
Thu, 20 Aug 2026 03:06:02 +0000 (11:06 +0800)
committerwang.yu <wang.yu@advantech.com>
Thu, 20 Aug 2026 03:06:02 +0000 (11:06 +0800)
- 表格新增「来源」列,以徽标区分 上传 / AI生成 / 手动
- 原「生成文件」列更名为「Json文件」:上传页下载原始 .json 文件,
  AI 生成页下载生成的 Cube .json,未生成显示'未生成'
- domain_pages 新增 source 字段;上传/生成时分别写入 source=upload/ai
  与 gen_filename;生成确认页状态徽标区分 AI已生成/已上传/未生成·来源
- 同步更新 design-doc.html(5.10 章节 + v0.18 变更记录)

app/globals.css
app/page.tsx
db-sidecar.mjs
public/design-doc.html

index 6d39c7f18a773866be8435d1ce08e85a3f6461e5..94c43d6854038fe2aa7a6dd76478be8f33cf042d 100644 (file)
 /* 领域方案 7 步向导 */
 .placeholder-block{display:flex;flex-direction:column;gap:12px;padding:6px 2px}
 .badge.todo{display:inline-block;align-self:flex-start;background:#fff4d8;color:#b37400;border:1px solid #f6bc44;border-radius:10px;padding:2px 10px;font-size:10px}
+/* Cube 页面来源徽标 */
+.badge.src-upload{display:inline-flex;align-items:center;gap:2px;background:#e7f5fc;color:#007cc3;border:1px solid #b9e0f4;border-radius:10px;padding:2px 9px;font-size:10px}
+.badge.src-ai{display:inline-flex;align-items:center;gap:2px;background:#f3e8ff;color:#7a3cc0;border:1px solid #dcc4f3;border-radius:10px;padding:2px 9px;font-size:10px}
+.badge.src-manual{display:inline-flex;align-items:center;gap:2px;background:#f1f2f3;color:#6b7177;border:1px solid #dfe2e4;border-radius:10px;padding:2px 9px;font-size:10px}
 .domain-preview-canvas{background:#1e2630;min-height:430px;border-radius:6px;overflow:hidden}
 .domain-preview-canvas .dashboard-thumb-live{min-height:430px}
 .domain-preview-feedback{margin-top:14px}
index 6038eecfc5143b42c6576a24b85eabfecf71622e..901ac7edd1b22bb9e8e6cd8d12ec15ce587d5637 100644 (file)
@@ -49,11 +49,11 @@ type MuTab = string | { label:string; disabled?:boolean; note?:string };
 type MuNode = { key:string; children?:MuNode[]; tabs?:MuTab[] };
 type PageStyle = { id:number; name:string; created_at?:string };
 type DomainAsset = { id?:number; device_template_id:number; device_name:string; count:number; rule:string; sort_order:number };
-type DomainPage = { id:number; name:string; description:string; json_text:string; status:string; gen_filename?:string|null; prompt?:string|null; sort_order:number };
+type DomainPage = { id:number; name:string; description:string; json_text:string; status:string; gen_filename?:string|null; prompt?:string|null; source?:string|null; sort_order:number };
 type DomainStepKey = "basic" | "assets" | "functions" | "opsflow" | "knowledge" | "generate" | "expert";
 
 // ---- helpers ----
-function Status({children}:{children:React.ReactNode}){const v=String(children);const tone=/完成|正常|成功|通过|推荐|generated/.test(v)?"ok":/生成中|校验中|告警|running/.test(v)?"warn":/失败|异常|failed/.test(v)?"bad":"muted";return <span className={"status "+tone}>{children}</span>}
+function Status({children}:{children:React.ReactNode}){const v=String(children);const tone=/完成|正常|成功|通过|推荐|已生成|已上传|generated/.test(v)?"ok":/生成中|校验中|告警|running/.test(v)?"warn":/失败|异常|failed/.test(v)?"bad":"muted";return <span className={"status "+tone}>{children}</span>}
 function Button({children,secondary,onClick,disabled,title}:{children:React.ReactNode;secondary?:boolean;onClick?:()=>void;disabled?:boolean;title?:string}){return <button title={title} disabled={disabled} onClick={onClick} className={"button "+(secondary?"secondary":"")}>{children}</button>}
 function Panel({title,tools,children,className=""}:{title?:string;tools?:React.ReactNode;children:React.ReactNode;className?:string}){return <section className={"panel "+className}>{title&&<div className="panel-head"><strong>{title}</strong><div>{tools}</div></div>}<div className="panel-body">{children}</div></section>}
 function Field({label,required,hint,children}:{label:string;required?:boolean;hint?:string;children:React.ReactNode}){return <label className="field"><span>{required&&<i>*</i>}{label}</span>{children}{hint&&<small>{hint}</small>}</label>}
@@ -1442,7 +1442,7 @@ function DomainWizard({id,doms,go,notify,onBack,viewCompletePrompt,viewCompleteA
       setT(d);
       setM({domain_id:d.domain_id,name:d.name,description:d.description||"",status:d.status,version:d.version,creator:d.creator||"",supplement:d.supplement||""});
       setAssets((d.assets||[]).map((a:any)=>({id:a.id,device_template_id:a.device_template_id,device_name:a.device_name||"",count:Number(a.count)||0,rule:a.rule||"",sort_order:a.sort_order||0})));
-      setPages((d.pages||[]).map((p:any)=>({id:p.id,name:p.name,description:p.description||"",json_text:p.json_text||"",status:p.status||"pending",gen_filename:p.gen_filename||null,prompt:p.prompt||null,sort_order:p.sort_order||0})));
+      setPages((d.pages||[]).map((p:any)=>({id:p.id,name:p.name,description:p.description||"",json_text:p.json_text||"",status:p.status||"pending",gen_filename:p.gen_filename||null,prompt:p.prompt||null,source:p.source||null,sort_order:p.sort_order||0})));
       setMu(d.mu && d.mu.length ? d.mu : []);
     }catch(e:any){ notify("加载失败:"+e.message); }
     finally{ if(!silent) setLoading(false); }
@@ -1519,14 +1519,14 @@ function DomainWizard({id,doms,go,notify,onBack,viewCompletePrompt,viewCompleteA
   const onDevCreated=async(nid:number)=>{ await loadAllDevices(); setDevCreateOpen(false); if(pendingDevRow!=null){ const dev=allDevices.concat([{id:nid}]).find(d=>d.id===nid); updateAsset(pendingDevRow,{device_template_id:nid,device_name:dev?.name||""}); setPendingDevRow(null); } };
 
   // ===== 页面操作 =====
-  const addPage=async()=>{ await fetchDb(`/domain-templates/${id}/pages`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:"新页面",description:"",json_text:"{}"})}); await load(true); notify("已新增页面"); };
+  const addPage=async()=>{ await fetchDb(`/domain-templates/${id}/pages`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:"新页面",description:"",json_text:"{}",source:"manual"})}); await load(true); notify("已新增页面"); };
   const uploadPageJson=async(file:File, replaceId?:number)=>{
     const text=await file.text();
     try{ JSON.parse(text); }catch{ notify("不是合法 JSON 文件"); return; }
     const name=file.name.replace(/\.json$/i,"");
     try{
       if(replaceId){ await fetchDb(`/domain-pages/${replaceId}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({json_text:text})}); notify("已更新页面 JSON"); }
-      else { await fetchDb(`/domain-templates/${id}/pages`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name,description:"",json_text:text})}); notify("已上传页面 JSON"); }
+      else { await fetchDb(`/domain-templates/${id}/pages`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name,description:"",json_text:text,source:"upload",gen_filename:file.name,status:"uploaded"})}); notify("已上传页面 JSON"); }
       await load(true);
     }catch(e:any){ notify("上传失败:"+e.message); }
   };
@@ -1629,15 +1629,21 @@ function PlaceholderStep({title,note}:{title:string;note:string}){
 }
 
 function FunctionsStep({muTree,grouped,effSelected,onToggleMu,pages,onAddPage,onUploadPageJson,onSavePageMeta,onDeletePage,onGeneratePage,onDownloadPage,onPreviewPage,cubesAi,onRunCubesComplete,onViewCompleteCubesPrompt,updatePageField,tName,saving,notify}:{muTree:MuNode[];grouped:any[];effSelected:string[];onToggleMu:(k:string)=>void;pages:DomainPage[];onAddPage:()=>Promise<void>;onUploadPageJson:(f:File)=>Promise<void>;onSavePageMeta:(p:DomainPage)=>Promise<void>;onDeletePage:(p:DomainPage)=>Promise<void>;onGeneratePage:(p:DomainPage)=>Promise<void>;onDownloadPage:(p:DomainPage)=>void;onPreviewPage:(p:DomainPage)=>void;cubesAi:ReturnType<typeof useStreamComplete>;onRunCubesComplete:()=>Promise<void>;onViewCompleteCubesPrompt:()=>void;updatePageField:(i:number,patch:Partial<DomainPage>)=>void;tName:string;saving:boolean;notify:(m:string)=>void}){
+  const sourceBadge=(src?:string|null)=>{
+    if(src==="upload") return <span className="badge src-upload">↑ 上传</span>;
+    if(src==="ai") return <span className="badge src-ai">✦ AI生成</span>;
+    return <span className="badge src-manual">手动</span>;
+  };
   const [tab,setTab]=useState<"mu"|"cube"|"menu">("mu");
   return <Panel><div className="tabs"><button className={tab==="mu"?"active":""} onClick={()=>setTab("mu")}>MU 标准页面 <i>{effSelected.length}</i></button><button className={tab==="cube"?"active":""} onClick={()=>setTab("cube")}>Cube 扩展页面 <i>{pages.length}</i></button><button className={tab==="menu"?"active":""} onClick={()=>setTab("menu")}>菜单结构</button></div>
     {tab==="mu" && <div className="mu-tree">{grouped.map(g=><div key={g.parentMenu} className="mu-group"><b>{g.parentMenu}</b><div className="mu-items">{g.items.map((item:any)=><label key={item.key} className={item.disabled?"disabled":""}><input type="checkbox" disabled={item.disabled} checked={effSelected.includes(item.key)} onChange={()=>onToggleMu(item.key)}/><span>{item.label}</span></label>)}</div></div>)}</div>}
     {tab==="cube" && <div className="two-column">
       <div>
-        <div className="cube-editor"><table className="data-table editable cube-editor-table"><thead><tr><th>页面名称 <small>最多6字</small></th><th>页面描述</th><th>生成文件</th><th>操作</th></tr></thead><tbody>{pages.map((p,i)=><tr key={p.id}>
+        <div className="cube-editor"><table className="data-table editable cube-editor-table"><thead><tr><th>页面名称 <small>最多6字</small></th><th>页面描述</th><th>来源</th><th>Json文件</th><th>操作</th></tr></thead><tbody>{pages.map((p,i)=><tr key={p.id}>
           <td><input value={p.name} maxLength={6} placeholder="例如:运行总览" onChange={e=>updatePageField(i,{name:e.target.value})}/></td>
           <td><input value={p.description} placeholder="描述该页面要展示的内容与指标" onChange={e=>updatePageField(i,{description:e.target.value})}/></td>
-          <td>{p.status==="generated"? <button className="link" onClick={()=>onDownloadPage(p)}>下载 {p.gen_filename}</button> : <span className="muted">未生成</span>}</td>
+          <td>{sourceBadge(p.source)}</td>
+          <td>{p.gen_filename? <button className="link" onClick={()=>onDownloadPage(p)}>下载 {p.gen_filename}</button> : <span className="muted">未生成</span>}</td>
           <td className="cube-actions"><button onClick={()=>onPreviewPage(p)}>预览</button><button onClick={()=>onGeneratePage(p)}>生成</button><button onClick={()=>onSavePageMeta(p)}>保存</button><button className="trash" onClick={()=>onDeletePage(p)}>删除</button></td>
         </tr>)}</tbody></table></div>
         <div style={{display:"flex",gap:8,marginTop:8}}><Button onClick={onAddPage}>+ 添加 Cube 页面</Button><label className="button secondary" style={{cursor:"pointer"}}>↑ 上传页面 Json<input type="file" accept=".json,application/json" hidden onChange={e=>{const f=e.target.files?.[0]; if(f) onUploadPageJson(f); e.target.value="";}}/></label></div>
@@ -1681,7 +1687,7 @@ function DomainGenerateReview({tName,assets,effSelected,grouped,pages,onBack,onC
   <div className="gen-right">
     <Panel title="页面总览" className="task-panel">
       <p className="hint">以下为方案中已配置的 Cube 扩展页面预览(点击可放大查看)。如需调整,请返回「功能与菜单」。</p>
-      <div className="domain-review-pages">{pages.length===0? <div className="muted">暂无 Cube 页面</div> : pages.map(p=><article key={p.id} className="domain-review-page"><DomainPageThumb jsonText={p.json_text} emptyLabel="未生成"/><div><Status>{p.status==="generated"?"已生成":"未生成"}</Status><h4>{p.name}</h4></div></article>)}</div>
+      <div className="domain-review-pages">{pages.length===0? <div className="muted">暂无 Cube 页面</div> : pages.map(p=><article key={p.id} className="domain-review-page"><DomainPageThumb jsonText={p.json_text} emptyLabel="未生成"/><div><Status>{p.status==="generated"?"AI已生成":p.status==="uploaded"?"已上传":"未生成"}{p.source?` · ${p.source==="ai"?"AI":p.source==="upload"?"上传":"手动"}`:""}</Status><h4>{p.name}</h4></div></article>)}</div>
       <div style={{display:"flex",gap:8,marginTop:12}}><Button secondary onClick={onBack}>返回上一步</Button><Button onClick={onConfirm}>确认方案</Button></div>
     </Panel>
   </div></div>;
index 06d13c88df0cd2e55b948dc09f946062824b35cf..bd0975f51f07777b4a84ba49869278397ee33316 100644 (file)
@@ -342,6 +342,7 @@ ensureColumn("domain_templates", "supplement", "TEXT");
 ensureColumn("domain_pages", "status", "TEXT DEFAULT 'pending'");
 ensureColumn("domain_pages", "gen_filename", "TEXT");
 ensureColumn("domain_pages", "prompt", "TEXT");
+ensureColumn("domain_pages", "source", "TEXT");
 
 // GeoMap 中国地图底图:若 data/db/china-geo.json 存在(中国 34 省 + 九段线 FeatureCollection),
 // 则在 fixDashboard 中自动注入 GeoMap 面板的 custom.dataConfig.geoJson,避免地图气泡不渲染。
@@ -1878,7 +1879,7 @@ async function handleDomainPageGenerate(req, res, tmplId, pageId) {
     const { json, prompt } = await generateCubeJson(ctx, cube, onEvent, (t) => emit({ log: t }));
     const filename = `${safeFilename(page.name)}.json`;
     const jsonText = JSON.stringify(json, null, 2);
-    db.prepare("UPDATE domain_pages SET json_text=?, status='generated', gen_filename=?, prompt=? WHERE id=?").run(jsonText, filename, prompt, page.id);
+    db.prepare("UPDATE domain_pages SET json_text=?, status='generated', gen_filename=?, prompt=?, source='ai' WHERE id=?").run(jsonText, filename, prompt, page.id);
     emit({ log: `✓ 「${page.name}」生成完成\n` });
     emit({ task: page.name, status: "done" });
     res.end();
@@ -1912,7 +1913,7 @@ async function handleDomainPageRefine(req, res, tmplId, pageId) {
     emit({ task: `修改「${page.name}」`, status: "running" });
     const { json } = await refineCubeJson(ctx, cube, currentJson, feedback, onEvent, (t) => emit({ log: t }));
     const jsonText = JSON.stringify(json, null, 2);
-    db.prepare("UPDATE domain_pages SET json_text=?, status='generated' WHERE id=?").run(jsonText, page.id);
+    db.prepare("UPDATE domain_pages SET json_text=?, status='generated', source='ai' WHERE id=?").run(jsonText, page.id);
     emit({ log: `✓ 「${page.name}」修改完成\n` });
     emit({ task: `修改「${page.name}」`, status: "done" });
     res.end();
@@ -2008,7 +2009,10 @@ async function handleDomainPageCreate(req, res, tmplId) {
   if (!name) return respondError(res, 400, "页面名称不能为空");
   if (!body.json_text) return respondError(res, 400, "Cube JSON 不能为空");
   try { JSON.parse(body.json_text); } catch { return respondError(res, 400, "Cube JSON 不是合法 JSON"); }
-  const info = db.prepare("INSERT INTO domain_pages (domain_template_id,name,description,json_text) VALUES (?,?,?,?)").run(tmplId, name, body.description || "", body.json_text);
+  const source = body.source || "manual";
+  const gen_filename = body.gen_filename || null;
+  const status = body.status || (source === "upload" ? "uploaded" : "pending");
+  const info = db.prepare("INSERT INTO domain_pages (domain_template_id,name,description,json_text,status,gen_filename,source) VALUES (?,?,?,?,?,?,?)").run(tmplId, name, body.description || "", body.json_text, status, gen_filename, source);
   respondJson(res, 200, { ok: true, id: info.lastInsertRowid });
 }
 
index e0574da3b4aacb648beec23efec87046b40d57dd..3687f02b23c9dc7b516bd01be3e5c3a19740a271 100644 (file)
     <tr><td>模板信息</td><td>编辑所属领域 / 名称 / 版本 / 状态 / 创建人 / 描述</td><td>GET /api/db/domain-templates/:id 详情;PUT /api/db/domain-templates/:id 保存</td><td>「保存模板信息」写回并重新加载</td></tr>
     <tr><td>选用设备模版(DeviceTemplatePicker)</td><td>从「设备模版」页中勾选本方案要用的设备模版</td><td>详情 devices[](已选用的设备模版 id 列表);GET /api/db/device-templates(全部可选);PUT /api/db/domain-templates/:id(传 <code>device_template_ids</code>,服务端重写 device_template_domain_templates 关联)</td><td>勾选即保存:PUT 携带当前全部选中 id,服务端先 DELETE 旧关联再按 id 重建;未找到合适模版可点「+ 新增设备模版」跳到 deviceTemplates 页新建。设备模版的编辑(名称 / 说明 / 图片 / 采集点)已移至独立的「设备模版」页,不再在本面板内联编辑;设备不再维护「所属领域」多选(改为由领域 / 方案侧选用设备)</td></tr>
     <tr><td>采集点</td><td>设备下的采集点(名称 / 说明 / 单位 / 数值范围 / 描述)</td><td>POST /api/db/device-templates/:id/collection-points(名称);DELETE /api/db/collection-points/:id</td><td>「+ 采集点」填名称新增;行内「删」移除</td></tr>
-    <tr><td>领域页面(PageRow)</td><td>编辑领域页面(=StationBuilder 的 Cube 扩展页面)</td><td>详情 pages[];POST /api/db/domain-templates/:id/pages;PUT / DELETE /api/db/domain-pages/:id</td><td>编辑态展示名称 / 说明 / Cube JSON(textarea);保存前校验 JSON 合法性;「+ 新增页面」建默认空页</td></tr>
+    <tr><td>领域页面(Cube 扩展页面)</td><td>编辑领域页面(=StationBuilder 的 Cube 扩展页面)</td><td>详情 pages[];POST /api/db/domain-templates/:id/pages;PUT / DELETE /api/db/domain-pages/:id;POST /api/db/domain-templates/:id/pages/:pid/generate、/refine(流式)</td><td>「功能与菜单 → Cube 扩展页面」表格逐行管理,列:页面名称(≤6 字)/ 页面描述 / 来源 / Json文件 / 操作;来源以徽标区分「↑ 上传 / ✦ AI生成 / 手动」;Json文件列对已上传或已生成的页面提供「下载 文件名」按钮(上传=原始 .json 文件本身,AI生成=生成的 Cube .json),未生成显示"未生成";支持行内编辑名称/描述、预览/生成/保存/删除、+ 添加页面、↑ 上传页面 Json;保存前校验 JSON 合法性;生成确认页对每页显示「AI已生成 / 已上传 / 未生成 · 来源」</td></tr>
     <tr><td>模板菜单</td><td>编辑模板菜单(=StationBuilder 的 MU + Cube 菜单保存结构)</td><td>详情 menu[];POST /api/db/domain-templates/:id/menu;DELETE /api/db/template-menu-items/:id</td><td>表格展示菜单项 / 类型 / 父级 / 关联;「+ 新增菜单项」建占位项</td></tr>
     <tr><td>知识文档</td><td>管理知识文档(名称 + 本体)</td><td>详情 docs[];POST /api/db/domain-templates/:id/docs(doc_type=link 传 url,或 doc_type=file 传 base64 文件);GET /api/db/knowledge-docs/:id/file 下载;DELETE /api/db/knowledge-docs/:id</td><td>新增时选择「超链」(填 URL) 或「文件」(上传,落盘 <code>data/db/uploads/docs/</code>);列表可下载 / 删除</td></tr>
     <tr><td>运维流程(OpsFlowsViewer)</td><td>查看运维流程(流程名称 + 内容 JSON 串)</td><td>GET /api/ops-flows</td><td>仅展示占位记录,本功能 <span class="todo">尚未实现</span></td></tr>
     <tr><th>面板 / 区域</th><th>数据来源 / 接口</th><th>是否可编辑</th><th>编辑类型</th><th>上线范围</th></tr>
     <tr><td>模板信息</td><td>GET/PUT /api/db/domain-templates/:id</td><td>可编辑</td><td>所属领域(select) / 模板名称 / 版本 / 状态(select) / 创建人 / 领域描述(textarea)</td><td>方案元信息,被站点方案引用</td></tr>
     <tr><td>选用设备模版(DeviceTemplatePicker)</td><td>详情 <code>devices[]</code>;GET /api/db/device-templates;PUT /api/db/domain-templates/:id(<code>device_template_ids</code>,服务端重写关联)</td><td>可编辑(勾选即保存)</td><td>复选框列表</td><td>该方案选用的设备模版集合(供站点方案引用)</td></tr>
-    <tr><td>领域页面(PageRow)</td><td>详情 <code>pages[]</code>;POST /api/db/domain-templates/:id/pages;PUT/DELETE /api/db/domain-pages/:id</td><td>可编辑</td><td>名称 / 说明 / Cube JSON(保存前校验合法 JSON)</td><td>方案内置页面(=StationBuilder 的 Cube)</td></tr>
+    <tr><td>领域页面(Cube 扩展页面)</td><td>详情 <code>pages[]</code>;POST /api/db/domain-templates/:id/pages;PUT/DELETE /api/db/domain-pages/:id;POST .../:pid/generate、/refine</td><td>可编辑</td><td>表格逐列:页面名称(≤6字)/ 页面描述 / 来源(徽标:上传·AI生成·手动)/ Json文件(已上传或已生成→「下载 文件名」;否则"未生成")/ 操作(预览·生成·保存·删除)</td><td>方案内置页面(=StationBuilder 的 Cube);source 字段区分来源,gen_filename 为下载文件名</td></tr>
     <tr><td>模板菜单</td><td>详情 <code>menu[]</code>;POST /api/db/domain-templates/:id/menu;DELETE /api/db/template-menu-items/:id</td><td>可编辑(新增占位项)</td><td>表格(菜单项/类型/父级/关联)+ 删除</td><td>方案菜单结构</td></tr>
     <tr><td>知识文档</td><td>详情 <code>docs[]</code>;POST /api/db/domain-templates/:id/docs;GET /api/db/knowledge-docs/:id/file;DELETE</td><td>可编辑</td><td>新增选「超链」(填 URL) 或「文件」(上传 base64);列表可下载/删</td><td>方案知识库</td></tr>
     <tr><td>运维流程(OpsFlowsViewer)</td><td>GET /api/ops-flows</td><td>只读</td><td>占位展示</td><td><span class="todo">尚未实现</span></td></tr>
   <table class="changelog">
     <tr><th>版本</th><th>日期</th><th>修改人</th><th>说明</th></tr>
     <tr><td>v0.17</td><td>2026-08-19</td><td>wang.yu</td><td>修复左侧目录(TOC)点击章节后消失:根因为顶部版本说明段落含全角空格不换行,在 flex 布局下把 <code>main#doc</code> 撑到约 2238px 宽(flex item 默认 <code>min-width:auto</code> 不收缩),点击锚点时浏览器水平滚动将 sticky 目录推出左边界。给 <code>main#doc</code> 增加 <code>min-width:0</code> 使其可收缩、内容正常折行,消除水平滚动,目录任意宽度常驻可见。</td></tr>
+    <tr><td>v0.18</td><td>2026-08-20</td><td>wang.yu</td><td>领域方案「功能与菜单 → Cube 扩展页面」表格增强:① 新增「来源」列,以徽标区分 上传 / AI生成 / 手动;② 原「生成文件」列更名为「Json文件」,对上传页面提供原始 .json 文件名下载、对 AI 生成页面提供生成的 Cube .json 下载(未生成显示"未生成");③ domain_pages 新增 source 字段,上传/生成时分别写入 source=upload/ai 与 gen_filename;④ 生成确认页状态徽标区分「AI已生成 / 已上传 / 未生成 · 来源」。同步更新 5.10 章节。</td></tr>
     <tr><td>v0.16</td><td>2026-08-19</td><td>wang.yu</td><td>设计方案文档修订:① 左侧目录改为任意宽度常驻可见(移除窄屏 <code>display:none</code>,窄屏下列表转为顶部可滚动常驻栏),点击章节后目录不再消失;② 移除「内部登录」章节(5.1)及 5.13 登录截图,可达性说明去掉登录;③ 新增 AI 智能补全子章(5.3.2 / 5.4.2 / 5.5.2),附 AI 流式输出终端截图(ai-complete-basic/assets/functions.png);④ 修复 PDF 导出图片被分页截断(<code>@media print</code> 增加 <code>page-break-inside:avoid</code> 与图片适配);⑤ 修复 Word 导出无图片(<code>downloadWord</code> 将图片转为 base64 data URI 内嵌)。</td></tr>
     <tr><td>v0.15</td><td>2026-08-19</td><td>wang.yu</td><td>领域方案库列表增强:列顺序调整为「所属领域 → 方案名称 → 版本 → 状态 → 创建人 → 设备/页面/文档 → 操作」,版本/状态列收窄、所属领域/方案名称列加宽;新增筛选栏(查找 方案名称/创建人 + 所属领域下拉 + 状态下拉 + 条数)与可排序列头(所属领域/方案名称/状态/创建人,升降切换带 ▲▼),卡片视图复用同结果。</td></tr>
     <tr><td>v0.14</td><td>2026-08-19</td><td>wang.yu</td><td>左侧目录(TOC)贴齐浏览器最左侧:<code>.doc-layout</code> 取消 <code>margin:0 auto</code> 自动居中(改为 <code>margin:0; padding-left:12px</code>),宽屏下不再左右留白,目录紧贴视口左缘。</td></tr>