]> acesimba.cloud Git - StationBuilder.git/commitdiff
feat: 领域方案库列表增强排序与筛选(列重排+筛选栏)
authorwang.yu <wang.yu@advantech.com>
Wed, 19 Aug 2026 00:53:42 +0000 (08:53 +0800)
committerwang.yu <wang.yu@advantech.com>
Wed, 19 Aug 2026 00:53:42 +0000 (08:53 +0800)
- 列顺序改为 所属领域→方案名称→版本→状态→创建人→设备/页面/文档→操作
- 版本/状态列收窄, 所属领域/方案名称列加宽
- 新增筛选栏: 查找(方案名称/创建人) + 所属领域下拉 + 状态下拉 + 条数
- 四列(所属领域/方案名称/状态/创建人)可点击排序, 升/降切换带 ▲▼
- 同步设计文档至 v0.15

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

index f159897786cca5e77b179cd4814166bd834ed963..fd6cc8ee3519b9ae1f14b566daf1a008e3ca47f2 100644 (file)
 .data-table.editable .trash{border:0;background:transparent;color:var(--red);font-size:15px;cursor:pointer;line-height:1}
 .data-table.editable .trash:hover{color:#d63723}
 
+/* ===== 领域方案库:列表筛选栏 + 可排序列头 ===== */
+.filter-bar{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-bottom:10px}
+.filter-bar .filter-search{flex:1 1 220px;min-width:180px;height:30px;border:1px solid #d9dee2;border-radius:3px;padding:0 10px;font-size:12px;background:#fff}
+.filter-bar .filter-search:focus{border-color:var(--blue);outline:none}
+.filter-bar select{height:30px;border:1px solid #d9dee2;border-radius:3px;padding:0 8px;font-size:12px;background:#fff;color:#444}
+.filter-bar select:focus{border-color:var(--blue);outline:none}
+.filter-bar .filter-count{margin-left:auto;font-size:12px}
+.data-table th.sortable{cursor:pointer;user-select:none;white-space:nowrap}
+.data-table th.sortable:hover{color:var(--blue)}
+.data-table th.sortable .sort-ind{color:var(--blue);font-size:10px}
+
 /* ===== Assets 资产配置 ===== */
 .empty-row td{background:#fafbfc}
 .empty-row input{color:#b7bcc0;background:#fafbfc}
index 03fabbcdfc8fd23a2341bd3e6ddb7d5570ca5136..a5b25c11c43bbf40b012301ba756c50700e719df 100644 (file)
@@ -989,25 +989,66 @@ function DomainLibrary({go,notify,onOpen,domainFilter,domainOptions}:{go:(p:Page
   const [list,setList]=useState<any[]>([]);
   const [loading,setLoading]=useState(true);
   const [view,setView]=useState<"list"|"card">("list");
+  const [q,setQ]=useState("");
+  const [domainSel,setDomainSel]=useState<string>(domainFilter!=null?String(domainFilter):"");
+  const [statusSel,setStatusSel]=useState<string>("");
+  const [sort,setSort]=useState<{key:""|"domain"|"name"|"status"|"creator";dir:1|-1}>({key:"",dir:1});
   const load=async()=>{ try{ setList(await fetchDb("/domain-templates") as any); }catch(e:any){ notify("加载失败:"+e.message); } finally{ setLoading(false); } };
   useEffect(()=>{ load(); },[]);
-  const filtered=list.filter(t=>!domainFilter||t.domain_id===domainFilter);
   const del=async(id:number)=>{ if(!confirm("确定删除该领域方案?此操作不可恢复")) return; try{ await fetchDb(`/domain-templates/${id}`,{method:"DELETE"}); notify("已删除"); load(); }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;
+  const filtered=list
+    .filter(t=>domainSel===""||String(t.domain_id)===domainSel)
+    .filter(t=>statusSel===""||t.status===statusSel)
+    .filter(t=>{ const k=q.trim().toLowerCase(); if(!k) return true; return (t.name||"").toLowerCase().includes(k)||(t.creator||"").toLowerCase().includes(k); })
+    .sort((a:any,b:any)=>{
+      if(!sort.key) return 0;
+      let av:string,bv:string;
+      if(sort.key==="domain"){ av=a.domain_name; bv=b.domain_name; }
+      else if(sort.key==="name"){ av=a.name; bv=b.name; }
+      else if(sort.key==="status"){ av=statusLabel(a.status); bv=statusLabel(b.status); }
+      else { av=a.creator||""; bv=b.creator||""; }
+      av=String(av).toLowerCase(); bv=String(bv).toLowerCase();
+      if(av<bv) return -1*sort.dir; if(av>bv) return 1*sort.dir; return 0;
+    });
   return <>
     <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={domainFilter!=null?`领域方案列表 · ${domainOptions.find(d=>d.id===domainFilter)?.name||""}`:"领域方案列表"} tools={<div className="view-toggle">
+    <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>
       </div>}>
+      <div className="filter-bar">
+        <input className="filter-search" value={q} onChange={e=>setQ(e.target.value)} placeholder="查找 方案名称 / 创建人" />
+        <select value={domainSel} onChange={e=>setDomainSel(e.target.value)}>
+          <option value="">全部所属领域</option>
+          {domainOptions.map(d=><option key={d.id} value={String(d.id)}>{d.name}</option>)}
+        </select>
+        <select value={statusSel} onChange={e=>setStatusSel(e.target.value)}>
+          <option value="">全部状态</option>
+          <option value="available">可用</option>
+          <option value="unavailable">不可用</option>
+        </select>
+        <span className="filter-count muted">共 {filtered.length} 条</span>
+      </div>
       {loading? <p>加载中…</p> :
        filtered.length===0? <p className="muted">暂无方案,点击右上角「+ 新增领域方案」创建</p> :
        view==="list"? (
-        <table className="data-table"><thead><tr><th>方案名称</th><th>所属领域</th><th>版本</th><th>状态</th><th>创建人</th><th>设备/页面/文档</th><th>操作</th></tr></thead>
+        <table className="data-table domain-list"><thead><tr>
+          <th className="sortable" style={{width:160}} onClick={()=>toggleSort("domain")}>所属领域{sortInd("domain")}</th>
+          <th className="sortable" style={{width:"40%"}} onClick={()=>toggleSort("name")}>方案名称{sortInd("name")}</th>
+          <th style={{width:64}}>版本</th>
+          <th className="sortable" style={{width:82}} onClick={()=>toggleSort("status")}>状态{sortInd("status")}</th>
+          <th className="sortable" style={{width:140}} onClick={()=>toggleSort("creator")}>创建人{sortInd("creator")}</th>
+          <th style={{width:110}}>设备/页面/文档</th>
+          <th style={{width:120}}>操作</th>
+        </tr></thead>
         <tbody>
           {filtered.map((t:any)=>(<tr key={t.id}>
-            <td>{t.name}</td><td>{t.domain_name}</td><td>{t.version}</td>
+            <td>{t.domain_name}</td><td>{t.name}</td><td>{t.version}</td>
             <td><Status>{t.status==="available"?"可用":"不可用"}</Status></td>
             <td>{t.creator||"—"}</td>
             <td>{t.device_count}/{t.page_count}/{t.doc_count}</td>
index 46635ba06f160340c0e314cb89bb1238f3341dd0..5d2d38644b30829c5ea1b1ba48a1ae93509b9793 100644 (file)
@@ -85,7 +85,7 @@
   <nav class="doc-nav"><div class="doc-nav-inner" id="toc"></div></nav>
 
 <main id="doc">
-  <p class="muted">文档版本:v0.14 最后更新:2026-08-19 修改人:wang.yu 状态:开发中(演示数据已标注)</p>
+  <p class="muted">文档版本:v0.15 最后更新:2026-08-19 修改人:wang.yu 状态:开发中(演示数据已标注)</p>
   <h1>StationBuilder 产品与交互设计方案</h1>
 
   <h2>1. 文档说明</h2>
   </ul>
 
   <h3>5.9 领域方案库(domains)</h3>
-  <p>领域方案库已从硬编码演示改为<strong>真实数据库驱动</strong>。列表与详情编辑器同一页面内通过 <code>openDomainTemplateId</code> state 切换:<code>null</code>=列表,<code>"create"</code>=新增表单,<code>number</code>=该方案详情编辑器。列表支持<strong>列表 / 卡片两种视图</strong>切换(右上角视图切换按钮,<code>view</code> state 控制);点击左侧「领域分类」下的某个领域会按 <code>domainFilter</code> 自动筛选该领域方案。</p>
+  <p>领域方案库已从硬编码演示改为<strong>真实数据库驱动</strong>。列表与详情编辑器同一页面内通过 <code>openDomainTemplateId</code> state 切换:<code>null</code>=列表,<code>"create"</code>=新增表单,<code>number</code>=该方案详情编辑器。列表支持<strong>列表 / 卡片两种视图</strong>切换(右上角视图切换按钮,<code>view</code> state 控制)。列表上方提供<strong>筛选栏</strong>:文本查找框(匹配 方案名称 / 创建人)、所属领域下拉、状态下拉,结果实时过滤并显示条数;列头 <strong>所属领域 / 方案名称 / 状态 / 创建人</strong> 可点击排序(升/降切换,带 ▲▼ 指示),卡片视图复用同一筛选与排序结果。</p>
   <table>
     <tr><th>元素</th><th>功能</th><th>数据来源</th><th>交互效果</th></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>本地 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>—</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>
 
   <h4>5.9.1 交互细节</h4>
-  <figure><img src="design-doc-assets/domains.png" alt="领域方案库"/><figcaption>领域方案库:列表视图,支持列表/卡片切换、按领域分类筛选,点击「编辑」进入方案详情。</figcaption></figure>
+  <figure><img src="design-doc-assets/domains.png" alt="领域方案库"/><figcaption>领域方案库:列表视图,上方为筛选栏(查找方案名称/创建人、所属领域、状态),列头 所属领域/方案名称/状态/创建人 可点击排序;支持列表/卡片切换。</figcaption></figure>
   <p><b>① 字段 / 表格逐列</b>(列表表格,仅展示 + 操作):</p>
   <table>
     <tr><th>列</th><th>数据来源</th><th>是否可编辑</th><th>编辑类型</th><th>上线范围</th></tr>
-    <tr><td>æ\96¹æ¡\88å\90\8dç§°</td><td>GET /api/db/domain-templatesï¼\88<code>name</code>ï¼\89</td><td>å\8fªè¯»ï¼\88å\88\97表æ\80\81ï¼\89</td><td>æ\96\87æ\9c¬</td><td>æ\96¹æ¡\88æ \87è¯\86</td></tr>
-    <tr><td>æ\89\80å±\9eé¢\86å\9f\9f</td><td><code>domain_name</code></td><td>å\8fªè¯»</td><td>æ\96\87æ\9c¬</td><td>ä¾§æ \8fé¢\86å\9f\9få\88\86类维度</td></tr>
-    <tr><td>版本</td><td><code>version</code></td><td>只读</td><td>文本</td><td>方案版本</td></tr>
-    <tr><td>状态</td><td><code>status</code></td><td>只读</td><td>Status 标签(可用/不可用)</td><td>是否可选入站点方案</td></tr>
-    <tr><td>创建人</td><td><code>creator</code></td><td>只读</td><td>文本</td><td>—</td></tr>
+    <tr><td>æ\89\80å±\9eé¢\86å\9f\9f</td><td><code>domain_name</code></td><td>å\8fªè¯»</td><td>æ\96\87æ\9c¬</td><td>ä¾§æ \8fé¢\86å\9f\9få\88\86类维度ï¼\9bå\88\97宽è¾\83宽ï¼\8cå\88\97头å\8f¯ç\82¹å\87»æ\8e\92åº\8f</td></tr>
+    <tr><td>æ\96¹æ¡\88å\90\8dç§°</td><td>GET /api/db/domain-templatesï¼\88<code>name</code>ï¼\89</td><td>å\8fªè¯»ï¼\88å\88\97表æ\80\81ï¼\89</td><td>æ\96\87æ\9c¬</td><td>æ\96¹æ¡\88æ \87è¯\86ï¼\9bå\88\97宽æ\9c\80宽ï¼\88å¼¹æ\80§ï¼\89ï¼\8cå\88\97头å\8f¯ç\82¹å\87»æ\8e\92åº\8f</td></tr>
+    <tr><td>版本</td><td><code>version</code></td><td>只读</td><td>文本</td><td>方案版本;列宽收窄</td></tr>
+    <tr><td>状态</td><td><code>status</code></td><td>只读</td><td>Status 标签(可用/不可用)</td><td>是否可选入站点方案;列宽收窄,列头可点击排序</td></tr>
+    <tr><td>创建人</td><td><code>creator</code></td><td>只读</td><td>文本</td><td>—;列头可点击排序</td></tr>
     <tr><td>设备 / 页面 / 文档</td><td>聚合 <code>device_count / page_count / doc_count</code></td><td>只读</td><td>数字</td><td>方案体量概览</td></tr>
     <tr><td>操作 / 编辑·删除</td><td>编辑→<code>onOpen(id)</code>;删除→DELETE(级联删关联)</td><td>触发动作</td><td>按钮(删除二次确认)</td><td>打开详情 / 删除方案</td></tr>
   </table>
-  <p>顶部 <code>domain-hero-stats</code> 按领域统计方案数(本地计算),纯展示ï¼\9bä¾§æ \8fã\80\8cé¢\86å\9f\9få\88\86ç±»ã\80\8dæ\8c\89é\92®è®¾ç½® <code>domainFilter</code> ç­\9bé\80\89ã\80\82</p>
+  <p>顶部 <code>domain-hero-stats</code> 按领域统计方案数(本地计算),纯展示ã\80\82å\88\97表å\88\97顺åº\8f为 <strong>æ\89\80å±\9eé¢\86å\9f\9f â\86\92 æ\96¹æ¡\88å\90\8dç§° â\86\92 ç\89\88æ\9c¬ â\86\92 ç\8a¶æ\80\81 â\86\92 å\88\9b建人 â\86\92 è®¾å¤\87/页é\9d¢/æ\96\87æ¡£ â\86\92 æ\93\8dä½\9c</strong>ï¼\9bå\85¶ä¸­ æ\89\80å±\9eé¢\86å\9f\9f / æ\96¹æ¡\88å\90\8dç§° / ç\8a¶æ\80\81 / å\88\9b建人 å\9b\9bå\88\97ç\82¹å\87»å\88\97头å\8f¯æ\8e\92åº\8fï¼\88å\8d\87/é\99\8då\88\87æ\8d¢ï¼\8c表头å\8f³ä¾§æ\98¾ç¤º â\96²/â\96¼ï¼\89ã\80\82</p>
   <p><b>② 按钮与操作</b></p>
   <table>
     <tr><th>按钮</th><th>点击效果</th><th>编辑方式</th><th>确定 / 取消 / 关闭</th></tr>
     <tr><td>视图切换 列表 ▤ / 卡片 ▦</td><td><code>setView</code> 本地</td><td>—</td><td>无弹窗(卡片态点卡片即打开详情)</td></tr>
-    <tr><td>侧栏领域分类按钮</td><td><code>setDomainFilter(d.id)</code> 筛选</td><td>—</td><td>无弹窗</td></tr>
+    <tr><td>查找框(方案名称 / 创建人)</td><td><code>setQ</code> 本地模糊匹配 name/creator</td><td>输入文本</td><td>无弹窗,实时过滤并显示条数</td></tr>
+    <tr><td>所属领域下拉</td><td><code>setDomainSel</code> 本地按 domain_id 过滤</td><td>选择(全部 / 各行业)</td><td>无弹窗,实时过滤</td></tr>
+    <tr><td>状态下拉</td><td><code>setStatusSel</code> 本地按 status 过滤</td><td>选择(全部 / 可用 / 不可用)</td><td>无弹窗,实时过滤</td></tr>
+    <tr><td>可排序列头(所属领域 / 方案名称 / 状态 / 创建人)</td><td><code>toggleSort</code> 本地,升/降切换</td><td>点击列头</td><td>无弹窗,表头右侧显示 ▲/▼;再次点击反向</td></tr>
     <tr><td>编辑</td><td><code>onOpen(id)</code> → 方案详情</td><td>—</td><td>—</td></tr>
     <tr><td>删除</td><td>DELETE /api/db/domain-templates/:id(级联 device_template_domain_templates)</td><td>—</td><td>二次确认后删除并刷新</td></tr>
     <tr><td>+ 新增领域方案</td><td><code>setOpenDomainTemplateId("create")</code> → 同页 CreateTemplate 表单</td><td>表单填写</td><td>保存(POST)后自动打开详情;取消收起</td></tr>
-    <tr><td>清除领域筛选</td><td><code>setDomainFilter(null)</code></td><td>—</td><td>无弹窗</td></tr>
   </table>
   <p><b>③ 导航流</b></p>
   <ul>
   <h3>11.1 版本变更记录(Changelog)</h3>
   <table class="changelog">
     <tr><th>版本</th><th>日期</th><th>修改人</th><th>说明</th></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>
     <tr><td>v0.13</td><td>2026-08-18</td><td>wang.yu</td><td>截图显示宽度由 500px 调整为 360px(<code>.figure img</code> 的 <code>max-width</code>),等比例缩放并居中,适配常见浏览宽度。</td></tr>
     <tr><td>v0.12</td><td>2026-08-18</td><td>wang.yu</td><td>统一文档内所有截图显示尺寸:修改 <code>.figure img</code> 为 <code>max-width:500px; height:auto</code> 并居中,所有页面截图等比例压缩至 500px 宽,宽度一致。</td></tr>