Kaynağa Gözat

feat(模式工作流搜索页): 为步骤实质和形式字段添加匹配配对展示

新增匹配对展示的CSS样式,实现原值与匹配结果的对齐拆分逻辑,更新表格结构新增指令列并调整列宽,替换原有渲染函数以提升匹配结果可读性。
刘文武 2 gün önce
ebeveyn
işleme
38f9ad15cc
1 değiştirilmiş dosya ile 69 ekleme ve 6 silme
  1. 69 6
      examples/mode_workflow/search.html

+ 69 - 6
examples/mode_workflow/search.html

@@ -371,6 +371,13 @@
     .steps .inf { background: #fdf6e3 !important; position: relative; outline: 1px dashed #c9a227; outline-offset: -2px; }
     .steps .inf .ib { position: absolute; top: -1px; right: -1px; background: #c9a227; color: #fff; font-size: 9px; padding: 0 4px; border-radius: 0 0 0 4px; font-weight: 700; }
     .steps-empty { padding: 12px; color: var(--text-muted); font-size: 12px; }
+    /* 归类命中(实质/形式):原值 → 命中值 逐行配对 (口径同 index.html renderSF) */
+    .steps .sf-map { display: flex; flex-direction: column; gap: 4px; }
+    .steps .sf-pair { display: flex; align-items: center; gap: 4px; flex-wrap: wrap; line-height: 1.5; }
+    .steps .sf-old { color: #9aa0a6; text-decoration: line-through; }
+    .steps .sf-plain { color: var(--text); }
+    .steps .sf-arrow { color: #2e9e5b; flex: none; }
+    .steps .sf-new { display: inline-block; padding: 1px 7px; border-radius: 10px; background: #e3f3e8; color: #2e6b45; border: 1px solid #bfe3cb; font-weight: 600; white-space: nowrap; }
     /* 输入/输出「值」单元格:超过 4 行加蒙版,点击展开/收起 */
     .clamp-val { position: relative; }
     .clamp-val.clampable { max-height: 6.6em; overflow: hidden; cursor: zoom-in; }
@@ -924,6 +931,61 @@ function markStepClamps() {
 function fmtSF(v) {
   return v == null ? '' : Array.isArray(v) ? v.join('、') : v;
 }
+const SF_ARROW =
+  '<svg class="sf-arrow" viewBox="0 0 24 24" width="13" height="13" aria-hidden="true"><path d="M5 12h14M13 6l6 6-6 6" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
+const SF_NO_MATCH = '无'; // 后端未命中占位符(与 category_match.py NO_MATCH 一致)
+/* 原值拆分:与后端 _split_values 一致 —— 括号内的「、」不拆、去重保序,
+   保证原值子项与 *Match 子项「等长等序」可按下标配对 */
+function _splitParts(raw) {
+  if (raw == null) return [];
+  if (Array.isArray(raw)) {
+    const out = [], seen = new Set();
+    for (const x of raw) {
+      const p = String(x).trim();
+      if (p && !seen.has(p)) { seen.add(p); out.push(p); }
+    }
+    return out;
+  }
+  const parts = [];
+  let cur = '', depth = 0;
+  for (const ch of String(raw)) {
+    if (ch === '(' || ch === '(') { depth++; cur += ch; }
+    else if (ch === ')' || ch === ')') { depth--; cur += ch; }
+    else if (ch === '、' && depth === 0) { const p = cur.trim(); if (p) parts.push(p); cur = ''; }
+    else { cur += ch; }
+  }
+  const last = cur.trim();
+  if (last) parts.push(last);
+  const out = [], seen = new Set();
+  for (const p of parts) if (!seen.has(p)) { seen.add(p); out.push(p); }
+  return out;
+}
+/* *Match 拆分:按下标对齐,**不去重不滤空**,保留「无」占位 */
+function _matchParts(v) {
+  return v == null ? [] : String(v).split('、').map((x) => x.trim());
+}
+/* 实质/形式单元格:逐子项「原值 → 命中值」配对。
+   命中 → 原值灰色划除 + 箭头 + 绿色命中值;
+   未命中(占位「无」/缺失)→ 只显原值,黑色正常 */
+function renderSF(value, match) {
+  const olds = _splitParts(value);
+  const news = _matchParts(match);
+  if (!news.length) return esc(fmtSF(value)); // 整格未归类(旧数据无 *Match)→ 原值黑色
+  const n = Math.max(olds.length, news.length);
+  let rows = '';
+  for (let i = 0; i < n; i++) {
+    const a = olds[i], b = news[i];
+    const matched = b != null && b !== '' && b !== SF_NO_MATCH;
+    if (matched) {
+      rows += `<div class="sf-pair">${
+        a != null ? `<span class="sf-old">${esc(a)}</span>${SF_ARROW}` : ''
+      }<span class="sf-new">${esc(b)}</span></div>`;
+    } else if (a != null) {
+      rows += `<div class="sf-pair"><span class="sf-plain">${esc(a)}</span></div>`;
+    }
+  }
+  return `<div class="sf-map">${rows}</div>`;
+}
 function ioCell(x, kind) {
   const cls = kind === 'in' ? 'c-in' : 'c-out';
   if (!x) return `<td class="${cls}"></td><td class="${cls}"></td><td class="${cls}"></td>`;
@@ -946,13 +1008,14 @@ function renderSteps(steps) {
         rows += `<td rowspan="${n}" class="sid">${esc(s.id || '')}</td>
           <td rowspan="${n}"><div class="intent-text">${renderIntent(s.intent || s.directive || '')}</div></td>
           <td rowspan="${n}">${s.effect ? `<span class="pill navy">${esc(s.effect)}</span>` : ''}</td>
-          <td rowspan="${n}">${esc(fmtSF(s.substance))}</td>
-          <td rowspan="${n}">${esc(fmtSF(s.form))}</td>`;
+          <td rowspan="${n}">${renderSF(s.substance, s.substanceMatch)}</td>
+          <td rowspan="${n}">${renderSF(s.form, s.formMatch)}</td>`;
       }
       rows += ioCell(ins[i], 'in');
       if (i === 0) {
         rows += `<td rowspan="${n}">${s.via ? `<span class="pill teal">${esc(s.via)}</span>` : ''}</td>
-          <td rowspan="${n}" class="vtxt">${esc(s.action || '')}</td>`;
+          <td rowspan="${n}" class="vtxt">${esc(s.action || '')}</td>
+          <td rowspan="${n}" class="vtxt">${esc(s.directive || '')}</td>`;
       }
       rows += ioCell(outs[i], 'out');
       rows += '</tr>';
@@ -963,15 +1026,15 @@ function renderSteps(steps) {
       <col style="width:44px"><col style="width:200px"><col style="width:92px">
       <col style="width:112px"><col style="width:100px">
       <col style="width:112px"><col style="width:330px"><col style="width:92px">
-      <col style="width:118px"><col style="width:130px">
+      <col style="width:118px"><col style="width:130px"><col style="width:130px">
       <col style="width:112px"><col style="width:360px"><col style="width:110px">
     </colgroup>
     <thead>
-      <tr><th class="h-req" colspan="5">需 求</th><th class="h-in" colspan="3">输 入</th><th class="h-im" colspan="2">实 现</th><th class="h-out" colspan="3">输 出</th></tr>
+      <tr><th class="h-req" colspan="5">需 求</th><th class="h-in" colspan="3">输 入</th><th class="h-im" colspan="3">实 现</th><th class="h-out" colspan="3">输 出</th></tr>
       <tr>
         <th class="h-req2">#</th><th class="h-req2">目的</th><th class="h-req2">作用</th><th class="h-req2">实质</th><th class="h-req2">形式</th>
         <th class="h-in2">类型</th><th class="h-in2">值</th><th class="h-in2">来源</th>
-        <th class="h-im2">外部工具</th><th class="h-im2">动作</th>
+        <th class="h-im2">外部工具</th><th class="h-im2">动作</th><th class="h-im2">指令</th>
         <th class="h-out2">类型</th><th class="h-out2">值</th><th class="h-out2">去处</th>
       </tr>
     </thead><tbody>${rows}</tbody></table></div>`;