]> acesimba.cloud Git - StationBuilder.git/commitdiff
领域分类管理:删除工业制造/工业园区/智慧楼宇,新增其他,支持删除领域并迁移方案
authorroot <root@localhost.localdomain>
Thu, 20 Aug 2026 08:54:55 +0000 (16:54 +0800)
committerroot <root@localhost.localdomain>
Thu, 20 Aug 2026 08:54:55 +0000 (16:54 +0800)
- DEFAULT_DOMAINS 移除 工业制造/工业园区/智慧楼宇,新增 其他(重启不再复活)
- 后端新增 DELETE /api/domains/:id:禁止删除其他,删除前将 domain_templates 与 device_template_domains 迁移至其他,再删除领域
- 前端领域方案库新增「管理领域」面板,其他标注默认保留不可删,其余提供删除并自动刷新列表/清除筛选
- 更新设计文档 v0.20

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

index 94c43d6854038fe2aa7a6dd76478be8f33cf042d..b83cf99f5ef79ffe167fe890b97fa38d35e34293 100644 (file)
 .dlg-box input:focus{outline:1px solid var(--blue)}
 .dlg-actions{display:flex;justify-content:flex-end;gap:10px}
 .dlg-actions .button{margin-left:0}
+/* 领域方案库 · 管理领域 */
+.domain-manage-list{display:flex;flex-direction:column;gap:8px}
+.domain-manage-row{display:flex;align-items:center;gap:12px;padding:9px 12px;border:1px solid var(--line);border-radius:8px;background:#fff}
+.domain-manage-name{font-weight:600;color:#333}
+.domain-manage-row .muted{font-size:11px}
+.text-button.danger{color:#d92d20}
+.text-button.danger:hover{text-decoration:underline}
 
index 55917962542200c4dc611c073693ce889da4d984..87ed1772c23e3591e9543a1023ff9723856912d2 100644 (file)
@@ -334,6 +334,19 @@ export default function Home(){
     }).catch(e=>notify("加载失败: "+e.message)).finally(()=>setLoading(false));
   },[]);
 
+  const reloadDomains = async () => {
+    try {
+      const d = await fetchDb("/domains") as {id:number;name:string}[];
+      const arr = d || [];
+      setDomainOptions(arr.map(x=>({id:x.id,name:x.name})));
+      setDomains(arr.map(x=>x.name));
+    } catch(e:any){ notify("加载领域失败: "+e.message); }
+  };
+  const handleDomainDeleted = async (id:number) => {
+    await reloadDomains();
+    if (domainFilter === id) setDomainFilter(null);
+  };
+
   const loadSite=async(id:number)=>{
     const data:SiteDetail = await fetchDb(`/sites/${id}`);
     setSite(data);
@@ -488,7 +501,7 @@ export default function Home(){
       {page==="projects"&&<Projects view={view} setView={setView} sites={sites} loading={loading} go={go} loadSite={loadSite} notify={notify} onDelete={deleteSites}/>}
       {(page==="domains"||page==="domainDetail")&&(openDomainTemplateId!=null
         ? <DomainDetail id={openDomainTemplateId==="create"?null:openDomainTemplateId} go={go} notify={notify} onBack={()=>setOpenDomainTemplateId(null)} onCreated={setOpenDomainTemplateId} viewCompletePrompt={viewDtCompletePrompt} viewCompleteAssetsPrompt={viewDtCompleteAssetsPrompt} viewCompleteCubesPrompt={viewDtCompleteCubesPrompt}/>
-        : <DomainLibrary go={go} notify={notify} onOpen={(id:number)=>setOpenDomainTemplateId(id)} domainFilter={domainFilter} domainOptions={domainOptions}/>)}
+        : <DomainLibrary go={go} notify={notify} onOpen={(id:number)=>setOpenDomainTemplateId(id)} domainFilter={domainFilter} domainOptions={domainOptions} onDomainDeleted={handleDomainDeleted}/>)}
       {page==="styleLibrary"&&<PageStyleLibrary pageStyles={pageStyles} loadPageStyles={loadPageStyles} notify={notify}/>}
       {page==="deviceTemplates"&&<DeviceTemplates notify={notify}/>}
       {page==="basic"&&<Basic site={site} domains={domains} onChange={saveSiteBasic} onAddDomain={addDomain} onUpload={uploadFile} saving={saving} notify={notify} onViewCompletePrompt={viewCompletePrompt}/>}
@@ -1044,7 +1057,7 @@ function usePrompt(){
   return { prompt, el };
 }
 
-function DomainLibrary({go,notify,onOpen,domainFilter,domainOptions}:{go:(p:PageKey)=>void;notify:(m:string)=>void;onOpen:(id:number)=>void;domainFilter:number|null;domainOptions:{id:number;name:string}[]}){
+function DomainLibrary({go,notify,onOpen,domainFilter,domainOptions,onDomainDeleted}:{go:(p:PageKey)=>void;notify:(m:string)=>void;onOpen:(id:number)=>void;domainFilter:number|null;domainOptions:{id:number;name:string}[];onDomainDeleted:(id:number)=>void}){
   const [list,setList]=useState<any[]>([]);
   const [loading,setLoading]=useState(true);
   const [view,setView]=useState<"list"|"card">("list");
@@ -1056,6 +1069,7 @@ function DomainLibrary({go,notify,onOpen,domainFilter,domainOptions}:{go:(p:Page
   useEffect(()=>{ load(); },[]);
   const {confirm, el:confirmEl}=useConfirm();
   const del=async(id:number)=>{ if(!(await confirm("确定删除该领域方案?此操作不可恢复"))) return; try{ await fetchDb(`/domain-templates/${id}`,{method:"DELETE"}); notify("已删除"); load(); }catch(e:any){ notify("删除失败:"+e.message); } };
+  const manageDel=async(id:number,name:string)=>{ if(name==="其他") return; if(!(await confirm(`确定删除领域「${name}」?其下的方案将移入「其他」领域。`))) return; try{ await fetchDb(`/domains/${id}`,{method:"DELETE"}); notify("已删除领域,相关方案已归入「其他」"); await onDomainDeleted(id); }catch(e:any){ notify("删除失败:"+e.message); } };
   const statusLabel=(s:string)=>s==="available"?"可用":"不可用";
   const toggleSort=(key:"domain"|"name"|"status"|"creator")=>setSort(s=>s.key===key?{key,dir:(s.dir*-1) as 1|-1}:{key,dir:1});
   const sortInd=(key:"domain"|"name"|"status"|"creator")=>sort.key===key?(<span className="sort-ind">{sort.dir===1?"▲":"▼"}</span>):null;
@@ -1078,6 +1092,21 @@ function DomainLibrary({go,notify,onOpen,domainFilter,domainOptions}:{go:(p:Page
     <div className="domain-hero"><div><span className="eyebrow">DOMAIN SOLUTION LIBRARY</span><h2>领域方案库</h2><p>沉淀各领域标准方案,供客户站点直接使用或快速微调。</p></div>
       <div className="domain-hero-stats">{domainOptions.map(d=><div key={d.id}><b>{list.filter(t=>t.domain_id===d.id).length}</b><span>{d.name}</span></div>)}</div>
     </div>
+    <Panel title="管理领域" tools={<span className="muted">删除领域后,其方案将归入「其他」</span>}>
+      <div className="domain-manage-list">
+        {domainOptions.map(d=>(
+          <div key={d.id} className="domain-manage-row">
+            <span className="domain-manage-name">{d.name}</span>
+            <span className="muted">{list.filter((t:any)=>t.domain_id===d.id).length} 个方案</span>
+            <span style={{marginLeft:"auto"}}>
+              {d.name==="其他"
+                ? <span className="muted">(默认保留)</span>
+                : <button className="text-button danger" onClick={()=>manageDel(d.id,d.name)}>删除</button>}
+            </span>
+          </div>
+        ))}
+      </div>
+    </Panel>
     <Panel title="领域方案列表" tools={<div className="view-toggle">
         <button className={view==="list"?"active":""} onClick={()=>setView("list")}>▤ 列表</button>
         <button className={view==="card"?"active":""} onClick={()=>setView("card")}>▦ 卡片</button>
index bd0975f51f07777b4a84ba49869278397ee33316..93478fb84c12b25482824b9fc654fc2dfe8943b8 100644 (file)
@@ -31,8 +31,8 @@ const SEED_PAGE_STYLES = [
 ];
 
 const DEFAULT_DOMAINS = [
-  "工业园区", "数据中心", "智慧楼宇", "智慧水务", "工业制造",
-  "燃气", "水务 · 自来水厂", "新能源",
+  "数据中心", "智慧水务", "燃气", "水务 · 自来水厂",
+  "新能源", "其他",
 ];
 
 const MU_TREE = [
@@ -2151,6 +2151,7 @@ async function route(req, res) {
   const path = parsed.pathname;
   const mSites = path.match(/^\/api\/sites(?:\/(\d+))?$/);
   const mDomains = path === "/api/domains";
+  const mDomainOne = path.match(/^\/api\/domains\/(\d+)$/);
   const mHealth = path === "/api/health";
   const mMuTree = path === "/api/mu-tree";
   const mSiteAssets = path.match(/^\/api\/sites\/(\d+)\/assets$/);
@@ -2205,6 +2206,20 @@ async function route(req, res) {
   try {
     if (mHealth && req.method === "GET") return respondJson(res, 200, { ok: true });
     if (mMuTree && req.method === "GET") return respondJson(res, 200, MU_TREE);
+    if (mDomainOne && req.method === "DELETE") {
+      const id = Number(mDomainOne[1]);
+      const dom = db.prepare("SELECT * FROM domains WHERE id=?").get(id);
+      if (!dom) return respondError(res, 404, "领域不存在");
+      if (dom.name === "其他") return respondError(res, 400, "不能删除「其他」领域");
+      // 确保「其他」领域存在,作为被删除领域方案的归宿
+      let other = db.prepare("SELECT * FROM domains WHERE name='其他'").get();
+      if (!other) { const info = db.prepare("INSERT INTO domains (name) VALUES ('其他')").run(); other = { id: Number(info.lastInsertRowid) }; }
+      // 先将关联的方案 / 设备-领域关联迁移到「其他」,再删除领域(避免 ON DELETE CASCADE 误删方案)
+      db.prepare("UPDATE domain_templates SET domain_id=? WHERE domain_id=?").run(other.id, id);
+      db.prepare("UPDATE device_template_domains SET domain_id=? WHERE domain_id=?").run(other.id, id);
+      db.prepare("DELETE FROM domains WHERE id=?").run(id);
+      return respondJson(res, 200, { ok: true, movedTo: "其他" });
+    }
     if (mDomains) {
       if (req.method === "GET") return respondJson(res, 200, db.prepare("SELECT * FROM domains ORDER BY name").all());
       if (req.method === "POST") {
index 5c4ac3118f4300c396ffa879a8c77664f2af9e06..4e4bf061bffc48e5a5cdc4f9a0968baa09050d18 100644 (file)
   <nav class="doc-nav"><div class="doc-nav-inner" id="toc"></div></nav>
 
 <main id="doc">
-  <p class="muted">文档版本:v0.19 最后更新:2026-08-20 修改人:wang.yu 状态:开发中(演示数据已标注)</p>
+  <p class="muted">文档版本:v0.20 最后更新:2026-08-20 修改人:wang.yu 状态:开发中(演示数据已标注)</p>
   <h1>StationBuilder 产品与交互设计方案</h1>
 
   <h2>1. 文档说明</h2>
     <tr><th>元素</th><th>功能</th><th>数据来源</th><th>交互效果</th></tr>
     <tr><td>筛选栏(查找 / 所属领域 / 状态 / 条数)</td><td>列表实时筛选与计数</td><td>本地 state(<code>q</code> / <code>domainSel</code> / <code>statusSel</code>),作用于 GET /api/db/domain-templates 结果</td><td>查找框按 方案名称/创建人 模糊匹配;所属领域、状态下拉精确过滤;右上角显示「共 N 条」</td></tr>
     <tr><td>领域方案列表表格</td><td>展示全部领域方案(所属领域 / 方案名称 / 版本 / 状态 / 创建人 / 设备·页面·文档数 / 操作)</td><td>GET /api/db/domain-templates(含 domain_name、device_count、page_count、doc_count)</td><td>每行「编辑」→ 打开该方案详情编辑器;「删除」→ 确认后 DELETE /api/db/domain-templates/:id(级联删除 device_template_domain_templates)并刷新;空列表提示「点击右上角新增」</td></tr>
+    <tr><td>管理领域面板</td><td>领域分类管理:列出全部领域及方案数,支持删除领域</td><td>GET /api/domains(领域列表);删除调用 DELETE /api/domains/:id</td><td>「其他」标注「(默认保留)」且无删除按钮;其余领域显示「删除」按钮 → 自定义确认弹窗二次确认 → 调用 DELETE /api/domains/:id(后端先将其下 domain_templates / device_template_domains 迁移至「其他」,再删除领域)→ 前端刷新领域列表、若当前筛选命中该领域则清除筛选</td></tr>
     <tr><td>「+ 新增领域方案」</td><td>新建领域方案,进入新增表单</td><td>—</td><td>同页切到 CreateTemplate 表单:所属领域(select 来自 GET /api/domains)、模板名称、版本、状态(可用/不可用)、创建人、领域描述;POST /api/db/domain-templates 成功后自动打开该方案详情编辑器</td></tr>
     <tr><td>搜索 / 筛选 / 同步方案库</td><td>(历史功能)</td><td>—</td><td>已按计划 Phase 0 移除整行(含「同步方案库」占位按钮) <span class="todo">已移除</span></td></tr>
   </table>
   <h3>11.1 版本变更记录(Changelog)</h3>
   <table class="changelog">
     <tr><th>版本</th><th>日期</th><th>修改人</th><th>说明</th></tr>
+    <tr><td>v0.20</td><td>2026-08-20</td><td>wang.yu</td><td>领域分类管理增强:① 默认分类调整——移除「工业制造 / 工业园区 / 智慧楼宇」,新增「其他」(<code>DEFAULT_DOMAINS</code> 同步更新,重启不再复活被删分类);② 领域方案库新增「管理领域」面板,列出全部领域及其方案数,「其他」标注「默认保留」且不可删除,其余领域提供「删除」按钮,经自定义确认弹窗二次确认后调用 <code>DELETE /api/domains/:id</code>;③ 后端新增 <code>DELETE /api/domains/:id</code>:禁止删除「其他」,删除前将待删领域的 <code>domain_templates</code> 与 <code>device_template_domains</code> 关联迁移至「其他」(避免 <code>ON DELETE CASCADE</code> 误删方案),再删除领域;④ 删除后前端刷新领域列表,并在当前筛选命中该领域时自动清除筛选。</td></tr>
     <tr><td>v0.19</td><td>2026-08-20</td><td>wang.yu</td><td>修复领域方案「功能与菜单 → Cube 扩展页面」两处问题:①「生成」按钮改为流式请求(消费 NDJSON 响应体以保持连接),生成期间按钮显示「生成中…」并禁用、底部「AI 生成页面」控制台实时输出进度并据服务端 <code>{status:failed}</code> 事件弹失败提示;此前前端用普通 <code>fetch</code> 不读取响应体,连接被提前关闭导致服务端 AI 生成中断(<code>status='failed'</code>)、Json 未写入。② AI 智能补全(complete-cubes)由「逐条静默 <code>.catch(()=>{})</code> 插入」改为「收集结果后逐个 <code>await</code> 插入、失败显式 <code>notify</code>」,并捕获服务端 <code>{status:failed}</code> 事件弹失败提示,避免补全内容静默丢失、列表无变化。</td></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>