From 714e6410f3d15b9bdff0cb5c02779e398fe9e63e Mon Sep 17 00:00:00 2001 From: "wang.yu" Date: Thu, 20 Aug 2026 11:06:02 +0800 Subject: [PATCH] =?utf8?q?=E9=A2=86=E5=9F=9F=E6=96=B9=E6=A1=88=20Cube=20?= =?utf8?q?=E6=89=A9=E5=B1=95=E9=A1=B5=E9=9D=A2=EF=BC=9A=E6=9D=A5=E6=BA=90?= =?utf8?q?=E5=BE=BD=E6=A0=87=20+=20Json=E6=96=87=E4=BB=B6=20=E4=B8=8B?= =?utf8?q?=E8=BD=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit - 表格新增「来源」列,以徽标区分 上传 / 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 | 4 ++++ app/page.tsx | 22 ++++++++++++++-------- db-sidecar.mjs | 10 +++++++--- public/design-doc.html | 5 +++-- 4 files changed, 28 insertions(+), 13 deletions(-) diff --git a/app/globals.css b/app/globals.css index 6d39c7f..94c43d6 100644 --- a/app/globals.css +++ b/app/globals.css @@ -222,6 +222,10 @@ /* 领域方案 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} diff --git a/app/page.tsx b/app/page.tsx index 6038eec..901ac7e 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -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 {children}} +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 {children}} function Button({children,secondary,onClick,disabled,title}:{children:React.ReactNode;secondary?:boolean;onClick?:()=>void;disabled?:boolean;title?:string}){return } function Panel({title,tools,children,className=""}:{title?:string;tools?:React.ReactNode;children:React.ReactNode;className?:string}){return
{title&&
{title}
{tools}
}
{children}
} function Field({label,required,hint,children}:{label:string;required?:boolean;hint?:string;children:React.ReactNode}){return } @@ -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;onUploadPageJson:(f:File)=>Promise;onSavePageMeta:(p:DomainPage)=>Promise;onDeletePage:(p:DomainPage)=>Promise;onGeneratePage:(p:DomainPage)=>Promise;onDownloadPage:(p:DomainPage)=>void;onPreviewPage:(p:DomainPage)=>void;cubesAi:ReturnType;onRunCubesComplete:()=>Promise;onViewCompleteCubesPrompt:()=>void;updatePageField:(i:number,patch:Partial)=>void;tName:string;saving:boolean;notify:(m:string)=>void}){ + const sourceBadge=(src?:string|null)=>{ + if(src==="upload") return ↑ 上传; + if(src==="ai") return ✦ AI生成; + return 手动; + }; const [tab,setTab]=useState<"mu"|"cube"|"menu">("mu"); return
{tab==="mu" &&
{grouped.map(g=>
{g.parentMenu}
{g.items.map((item:any)=>)}
)}
} {tab==="cube" &&
-
{pages.map((p,i)=> +
页面名称 最多6字页面描述生成文件操作
{pages.map((p,i)=> - + + )}
页面名称 最多6字页面描述来源Json文件操作
updatePageField(i,{name:e.target.value})}/> updatePageField(i,{description:e.target.value})}/>{p.status==="generated"? : 未生成}{sourceBadge(p.source)}{p.gen_filename? : 未生成}
@@ -1681,7 +1687,7 @@ function DomainGenerateReview({tName,assets,effSelected,grouped,pages,onBack,onC

以下为方案中已配置的 Cube 扩展页面预览(点击可放大查看)。如需调整,请返回「功能与菜单」。

-
{pages.length===0?
暂无 Cube 页面
: pages.map(p=>
{p.status==="generated"?"已生成":"未生成"}

{p.name}

)}
+
{pages.length===0?
暂无 Cube 页面
: pages.map(p=>
{p.status==="generated"?"AI已生成":p.status==="uploaded"?"已上传":"未生成"}{p.source?` · ${p.source==="ai"?"AI":p.source==="upload"?"上传":"手动"}`:""}

{p.name}

)}
; diff --git a/db-sidecar.mjs b/db-sidecar.mjs index 06d13c8..bd0975f 100644 --- a/db-sidecar.mjs +++ b/db-sidecar.mjs @@ -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 }); } diff --git a/public/design-doc.html b/public/design-doc.html index e0574da..3687f02 100644 --- a/public/design-doc.html +++ b/public/design-doc.html @@ -549,7 +549,7 @@ 模板信息编辑所属领域 / 名称 / 版本 / 状态 / 创建人 / 描述GET /api/db/domain-templates/:id 详情;PUT /api/db/domain-templates/:id 保存「保存模板信息」写回并重新加载 选用设备模版(DeviceTemplatePicker)从「设备模版」页中勾选本方案要用的设备模版详情 devices[](已选用的设备模版 id 列表);GET /api/db/device-templates(全部可选);PUT /api/db/domain-templates/:id(传 device_template_ids,服务端重写 device_template_domain_templates 关联)勾选即保存:PUT 携带当前全部选中 id,服务端先 DELETE 旧关联再按 id 重建;未找到合适模版可点「+ 新增设备模版」跳到 deviceTemplates 页新建。设备模版的编辑(名称 / 说明 / 图片 / 采集点)已移至独立的「设备模版」页,不再在本面板内联编辑;设备不再维护「所属领域」多选(改为由领域 / 方案侧选用设备) 采集点设备下的采集点(名称 / 说明 / 单位 / 数值范围 / 描述)POST /api/db/device-templates/:id/collection-points(名称);DELETE /api/db/collection-points/:id「+ 采集点」填名称新增;行内「删」移除 - 领域页面(PageRow)编辑领域页面(=StationBuilder 的 Cube 扩展页面)详情 pages[];POST /api/db/domain-templates/:id/pages;PUT / DELETE /api/db/domain-pages/:id编辑态展示名称 / 说明 / Cube JSON(textarea);保存前校验 JSON 合法性;「+ 新增页面」建默认空页 + 领域页面(Cube 扩展页面)编辑领域页面(=StationBuilder 的 Cube 扩展页面)详情 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(流式)「功能与菜单 → Cube 扩展页面」表格逐行管理,列:页面名称(≤6 字)/ 页面描述 / 来源 / Json文件 / 操作;来源以徽标区分「↑ 上传 / ✦ AI生成 / 手动」;Json文件列对已上传或已生成的页面提供「下载 文件名」按钮(上传=原始 .json 文件本身,AI生成=生成的 Cube .json),未生成显示"未生成";支持行内编辑名称/描述、预览/生成/保存/删除、+ 添加页面、↑ 上传页面 Json;保存前校验 JSON 合法性;生成确认页对每页显示「AI已生成 / 已上传 / 未生成 · 来源」 模板菜单编辑模板菜单(=StationBuilder 的 MU + Cube 菜单保存结构)详情 menu[];POST /api/db/domain-templates/:id/menu;DELETE /api/db/template-menu-items/:id表格展示菜单项 / 类型 / 父级 / 关联;「+ 新增菜单项」建占位项 知识文档管理知识文档(名称 + 本体)详情 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新增时选择「超链」(填 URL) 或「文件」(上传,落盘 data/db/uploads/docs/);列表可下载 / 删除 运维流程(OpsFlowsViewer)查看运维流程(流程名称 + 内容 JSON 串)GET /api/ops-flows仅展示占位记录,本功能 尚未实现 @@ -562,7 +562,7 @@ 面板 / 区域数据来源 / 接口是否可编辑编辑类型上线范围 模板信息GET/PUT /api/db/domain-templates/:id可编辑所属领域(select) / 模板名称 / 版本 / 状态(select) / 创建人 / 领域描述(textarea)方案元信息,被站点方案引用 选用设备模版(DeviceTemplatePicker)详情 devices[];GET /api/db/device-templates;PUT /api/db/domain-templates/:id(device_template_ids,服务端重写关联)可编辑(勾选即保存)复选框列表该方案选用的设备模版集合(供站点方案引用) - 领域页面(PageRow)详情 pages[];POST /api/db/domain-templates/:id/pages;PUT/DELETE /api/db/domain-pages/:id可编辑名称 / 说明 / Cube JSON(保存前校验合法 JSON)方案内置页面(=StationBuilder 的 Cube) + 领域页面(Cube 扩展页面)详情 pages[];POST /api/db/domain-templates/:id/pages;PUT/DELETE /api/db/domain-pages/:id;POST .../:pid/generate、/refine可编辑表格逐列:页面名称(≤6字)/ 页面描述 / 来源(徽标:上传·AI生成·手动)/ Json文件(已上传或已生成→「下载 文件名」;否则"未生成")/ 操作(预览·生成·保存·删除)方案内置页面(=StationBuilder 的 Cube);source 字段区分来源,gen_filename 为下载文件名 模板菜单详情 menu[];POST /api/db/domain-templates/:id/menu;DELETE /api/db/template-menu-items/:id可编辑(新增占位项)表格(菜单项/类型/父级/关联)+ 删除方案菜单结构 知识文档详情 docs[];POST /api/db/domain-templates/:id/docs;GET /api/db/knowledge-docs/:id/file;DELETE可编辑新增选「超链」(填 URL) 或「文件」(上传 base64);列表可下载/删方案知识库 运维流程(OpsFlowsViewer)GET /api/ops-flows只读占位展示尚未实现 @@ -752,6 +752,7 @@ + -- 2.43.0
版本日期修改人说明
v0.172026-08-19wang.yu修复左侧目录(TOC)点击章节后消失:根因为顶部版本说明段落含全角空格不换行,在 flex 布局下把 main#doc 撑到约 2238px 宽(flex item 默认 min-width:auto 不收缩),点击锚点时浏览器水平滚动将 sticky 目录推出左边界。给 main#doc 增加 min-width:0 使其可收缩、内容正常折行,消除水平滚动,目录任意宽度常驻可见。
v0.182026-08-20wang.yu领域方案「功能与菜单 → Cube 扩展页面」表格增强:① 新增「来源」列,以徽标区分 上传 / AI生成 / 手动;② 原「生成文件」列更名为「Json文件」,对上传页面提供原始 .json 文件名下载、对 AI 生成页面提供生成的 Cube .json 下载(未生成显示"未生成");③ domain_pages 新增 source 字段,上传/生成时分别写入 source=upload/ai 与 gen_filename;④ 生成确认页状态徽标区分「AI已生成 / 已上传 / 未生成 · 来源」。同步更新 5.10 章节。
v0.162026-08-19wang.yu设计方案文档修订:① 左侧目录改为任意宽度常驻可见(移除窄屏 display:none,窄屏下列表转为顶部可滚动常驻栏),点击章节后目录不再消失;② 移除「内部登录」章节(5.1)及 5.13 登录截图,可达性说明去掉登录;③ 新增 AI 智能补全子章(5.3.2 / 5.4.2 / 5.5.2),附 AI 流式输出终端截图(ai-complete-basic/assets/functions.png);④ 修复 PDF 导出图片被分页截断(@media print 增加 page-break-inside:avoid 与图片适配);⑤ 修复 Word 导出无图片(downloadWord 将图片转为 base64 data URI 内嵌)。
v0.152026-08-19wang.yu领域方案库列表增强:列顺序调整为「所属领域 → 方案名称 → 版本 → 状态 → 创建人 → 设备/页面/文档 → 操作」,版本/状态列收窄、所属领域/方案名称列加宽;新增筛选栏(查找 方案名称/创建人 + 所属领域下拉 + 状态下拉 + 条数)与可排序列头(所属领域/方案名称/状态/创建人,升降切换带 ▲▼),卡片视图复用同结果。
v0.142026-08-19wang.yu左侧目录(TOC)贴齐浏览器最左侧:.doc-layout 取消 margin:0 auto 自动居中(改为 margin:0; padding-left:12px),宽屏下不再左右留白,目录紧贴视口左缘。