Explorar el Código

fix(mode_workflow): 工具表格对齐 fixed_query_eval(案例子列+rowspan+限高展开)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
刘文武 hace 4 días
padre
commit
72e415ac1e
Se han modificado 1 ficheros con 115 adiciones y 41 borrados
  1. 115 41
      examples/mode_workflow/index.html

+ 115 - 41
examples/mode_workflow/index.html

@@ -184,13 +184,46 @@ select:focus,input:focus{border-color:var(--navy)}
   padding:0 4px;border-radius:0 0 0 4px;font-weight:700}
 .anchor{font-family:'IBM Plex Mono',monospace;font-size:10.5px;color:var(--ink-faint);white-space:nowrap}
 
-/* 工具表格(复用 .steps 分区表头) */
-.tools-table td{min-width:88px}
-.tools-table td:first-child{min-width:120px}
-.tools-table .tcell-ul{padding-left:15px;margin:0}
-.tools-table .pill{margin:1px 3px 2px 0;display:inline-block}
-.case{border:1px dashed var(--line-dark);border-radius:6px;padding:6px 9px;margin:0 0 5px;font-size:11.5px;min-width:200px}
-.case .ck{color:var(--amber);font-weight:700;margin-right:6px}
+/* 工具表格(移植自 fixed_query_eval:案例逐行 rowspan + 限高展开) */
+.mw-ttwrap{overflow-x:auto;border:1px solid #e6ded2;border-radius:10px;box-shadow:0 2px 12px rgba(0,0,0,.05)}
+.mw-tt{border-collapse:separate;border-spacing:0;width:100%;min-width:1180px;background:#fff;font-size:12.5px}
+.mw-tt thead th{
+  position:sticky;top:0;z-index:2;text-align:left;white-space:nowrap;
+  background:linear-gradient(180deg,#2aa79b,#1c8076);color:#fff;font-weight:700;
+  padding:10px 12px;letter-spacing:.3px;border-right:1px solid rgba(255,255,255,.18);
+}
+.mw-tt thead th:last-child{border-right:none}
+.mw-tt thead tr:first-child th{top:0}
+.mw-tt thead tr:nth-child(2) th{top:38px}
+.mw-tt .th-group{text-align:center}
+.mw-tt .th-sub{background:linear-gradient(180deg,#36bdb0,#23897f);font-weight:600}
+.mw-tt tbody td{padding:9px 12px;vertical-align:top;line-height:1.6;
+  border-bottom:1px solid #f0eae0;border-right:1px solid #f5f0e8;color:#3a3a3a}
+.mw-tt tbody td:last-child{border-right:none}
+.mw-tt td.col-case{background:#fafdfc}
+.mw-tt tbody tr.tr-b td{background:#fbfaf6}
+.mw-tt tbody tr.tr-b td.col-case{background:#f6fbfa}
+.mw-tt tbody td.col-tool{font-weight:700;color:#176d64;white-space:nowrap;
+  border-left:3px solid #2aa79b;background:#f3faf8}
+.mw-tt tbody tr:hover td.col-tool{background:#e3f4f0}
+.mw-tt ul{margin:0;padding-left:17px}
+.mw-tt ul li{margin:3px 0}
+.mw-tt ul li::marker{color:#2aa79b}
+.mw-tt .layer-badge{display:inline-block;font-weight:700;font-size:11px;padding:2px 10px;border-radius:20px;white-space:nowrap}
+.mw-tt .layer-badge.make{color:#0e7490;background:#d6f0ee}
+.mw-tt .layer-badge.create{color:#b8731a;background:#fef0db}
+.mw-tt .dash{color:#c9c2b6}
+.mw-tt .tcell{position:relative;max-height:7.8em;overflow:hidden;transition:max-height .15s}
+.mw-tt .tcell.clamped{cursor:zoom-in}
+.mw-tt .tcell.clamped::after{content:'▾ 展开';position:absolute;left:0;right:0;bottom:0;
+  height:2.6em;display:flex;align-items:flex-end;justify-content:center;padding-bottom:2px;
+  font-size:11px;font-weight:700;color:#176d64;
+  background:linear-gradient(rgba(255,255,255,0),#fff 72%);pointer-events:none}
+.mw-tt tbody tr.tr-b .tcell.clamped::after{background:linear-gradient(rgba(251,250,246,0),#fbfaf6 72%)}
+.mw-tt td.col-case .tcell.clamped::after{background:linear-gradient(rgba(250,253,252,0),#fafdfc 72%)}
+.mw-tt tbody tr.tr-b td.col-case .tcell.clamped::after{background:linear-gradient(rgba(246,251,250,0),#f6fbfa 72%)}
+.mw-tt .tcell.open{max-height:none;cursor:zoom-out}
+.mw-tt .tcell.open::after{content:'';height:0}
 
 /* 任务面板 */
 #task-panel{
@@ -489,6 +522,7 @@ async function loadExtract(){
   state.version = data.version;
   syncVersionSelect();
   body.innerHTML = isProc ? renderProcedures(data) : renderTools(data);
+  if (!isProc) requestAnimationFrame(markClampedCells);
 }
 function renderExtractHead(versions, data, missing){
   const isProc = state.mode === 'process';
@@ -584,44 +618,84 @@ function ioCell(x, kind){
     <td class="${cls}"><span class="anchor">${esc(x.anchor||'')}</span></td>`;
 }
 
-/* ── 工具渲染(表格,与工序分区表头同语言)── */
+/* ── 工具渲染(表格,移植自 fixed_query_eval renderToolTable)── */
+const DASH = '<span class="dash">—</span>';
+function _toolCell(v){
+  if (v === null || v === undefined || v === '' || (Array.isArray(v) && !v.length)) return DASH;
+  if (Array.isArray(v)) return '<ul>' + v.map(x => `<li>${esc(String(x))}</li>`).join('') + '</ul>';
+  return esc(String(v));
+}
+function _scopeCell(v){
+  /* 作用域是 JSON 数组:按「、」拼接成短语,不出列表 */
+  if (v === null || v === undefined || (Array.isArray(v) && !v.length) || v === '') return DASH;
+  return esc(Array.isArray(v) ? v.join('、') : String(v));
+}
+function _ttCell(inner, clampable){
+  return clampable ? `<div class="tcell" onclick="this.classList.toggle('open')">${inner}</div>` : inner;
+}
+function _toolCellContent(c, t){
+  let inner, cls = '', clampable = true, style = '';
+  if (c === '工具名称'){
+    cls = 'col-tool'; clampable = false; inner = `🔧 ${esc(t[c] || '(未命名)')}`;
+  } else if (c === '来源链接'){
+    clampable = false;
+    inner = t[c] ? `<a href="${esc(t[c])}" target="_blank" style="color:#176d64;font-weight:600;">🔗 打开</a>` : DASH;
+  } else if (c === '创作层级'){
+    clampable = false;
+    inner = t[c] ? `<span class="layer-badge ${t[c] === '制作层' ? 'make' : 'create'}">${esc(t[c])}</span>` : DASH;
+  } else if (c === '实质作用域' || c === '形式作用域'){
+    inner = _scopeCell(t[c]);
+  } else {
+    inner = _toolCell(t[c]);
+  }
+  if (['输入','输出','用法','缺点'].includes(c)) style = 'max-width:240px;';
+  else if (c === '实质作用域' || c === '形式作用域') style = 'max-width:170px;';
+  else if (!clampable) style = 'white-space:nowrap;';
+  return {inner, cls, clampable, style};
+}
+function _td(c, t, rowspan){
+  const {inner, cls, clampable, style} = _toolCellContent(c, t);
+  const rs = rowspan > 1 ? ` rowspan="${rowspan}"` : '';
+  return `<td class="${cls}" style="${style}"${rs}>${_ttCell(inner, clampable)}</td>`;
+}
+function _caseTd(cse, key){
+  const v = (cse && cse[key] != null && cse[key] !== '') ? esc(String(cse[key])) : DASH;
+  return `<td class="col-case" style="max-width:210px;">${_ttCell(v, true)}</td>`;
+}
 function renderTools(data){
   const tools = data.tools || [];
   if (!tools.length) return '<div class="empty">本版本无工具</div>';
-  const pills = (a, cls) => (Array.isArray(a)?a:(a?[a]:[]))
-    .flatMap(x=>String(x).split('、')).filter(Boolean)
-    .map(x=>`<span class="pill ${cls}">${esc(x)}</span>`).join(' ');
-  const ul = (a, style) => (a&&a.length) ? `<ul class="tcell-ul"${style?` style="${style}"`:''}>${a.map(x=>`<li>${esc(x)}</li>`).join('')}</ul>` : '';
-  const rows = tools.map(t => {
-    const cases = (t['案例']||[]).map(c=>`<div class="case">
-      <div><span class="ck">入</span>${esc(c['输入']||'')}</div>
-      <div><span class="ck">出</span>${esc(c['输出']||'')}</div>
-      ${c['效果']?`<div><span class="ck">效</span>${esc(c['效果'])}</div>`:''}</div>`).join('');
-    const name = `<b>${esc(t['工具名称']||'(未命名)')}</b>
-      ${t['来源链接']?`<br><a class="anchor" href="${esc(t['来源链接'])}" target="_blank">来源 ↗</a>`:''}
-      ${t['最新更新时间']?`<br><span class="anchor">更新 ${esc(t['最新更新时间'])}</span>`:''}`;
-    return `<tr>
-      <td>${name}</td>
-      <td>${t['创作层级']?`<span class="pill navy">${esc(t['创作层级'])}</span>`:''}</td>
-      <td>${pills(t['实质作用域'],'amber')}</td>
-      <td>${pills(t['形式作用域'],'teal')}</td>
-      <td class="c-in">${esc(t['输入']||'')}</td>
-      <td class="c-out">${esc(t['输出']||'')}</td>
-      <td>${ul(t['用法'])}</td>
-      <td>${cases}</td>
-      <td>${ul(t['缺点'],'color:var(--seal)')}</td>
-    </tr>`;
+  /* 案例 group(输入/输出/效果)放在 用法 后、缺点 前;用 colspan/rowspan 做两层表头 */
+  const before = ['工具名称','创作层级','实质作用域','形式作用域','输入','输出','用法'];
+  const after = ['缺点','来源链接','最新更新时间'];
+  const thead = `<thead>
+    <tr>
+      ${before.map(c => `<th rowspan="2">${c}</th>`).join('')}
+      <th colspan="3" class="th-group">案例</th>
+      ${after.map(c => `<th rowspan="2">${c}</th>`).join('')}
+    </tr>
+    <tr>${['输入','输出','效果'].map(c => `<th class="th-sub">${c}</th>`).join('')}</tr>
+  </thead>`;
+  const rows = tools.map((t, ti) => {
+    const cases = (Array.isArray(t['案例']) && t['案例'].length) ? t['案例'] : [null];
+    const K = cases.length;
+    const par = ti % 2 ? 'tr-b' : 'tr-a';
+    return cases.map((cse, i) => {
+      const caseTds = `${_caseTd(cse,'输入')}${_caseTd(cse,'输出')}${_caseTd(cse,'效果')}`;
+      if (i === 0){
+        return `<tr class="${par}">${before.map(c => _td(c, t, K)).join('')}${caseTds}${after.map(c => _td(c, t, K)).join('')}</tr>`;
+      }
+      return `<tr class="${par}">${caseTds}</tr>`;
+    }).join('');
   }).join('');
-  return `<div style="overflow-x:auto"><table class="steps tools-table">
-    <thead>
-      <tr><th class="h-req" colspan="2">工 具</th><th class="h-in" colspan="2">作 用 域</th><th class="h-im" colspan="2">输 入 / 输 出</th><th class="h-out" colspan="3">详 情</th></tr>
-      <tr>
-        <th class="h-req2">名称</th><th class="h-req2">创作层级</th>
-        <th class="h-in2">实质作用域</th><th class="h-in2">形式作用域</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>`;
+  return `<div class="mw-ttwrap"><table class="mw-tt">${thead}<tbody>${rows}</tbody></table></div>`;
+}
+/* 渲染后标记真正溢出的单元格(才显示蒙版+可点击) */
+function markClampedCells(){
+  document.querySelectorAll('#xp-body .mw-tt .tcell').forEach(el => {
+    if (!el.classList.contains('open') && el.scrollHeight > el.clientHeight + 2) el.classList.add('clamped');
+    else if (el.scrollHeight <= el.clientHeight + 2) el.classList.remove('clamped');
+  });
 }
 
 /* ════ 解构任务 ════ */