| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166 |
- /* views/common.js —— 所有页面共用的渲染片段:状态徽标、人员、徽章、空态、骨架、筛选条。
- * 只返回 HTML 字符串,事件由 app.js 的事件委托统一处理(data-act / data-*)。
- * 设计约定:数字直接穿着强调色(不再套胶囊),标签只用字重和灰度分层。 */
- (function () {
- "use strict";
- const T = window.KF_TAXONOMY;
- const U = window.KF_UTIL;
- const esc = U.esc;
- /* ---------- 小片段 ---------- */
- /* 层:小圆点 + 英文小字,克制的色标 */
- function layerBadge(layer) {
- const l = T.layerOf(layer);
- if (!l) return `<span class="layer layer-missing" title="缺少 layer 字段">layer 缺失</span>`;
- return `<span class="layer layer-${l.key}" title="${esc(l.desc)}">${l.en}</span>`;
- }
- /* 证据:纯文字 + 状态点 */
- function evidenceBadge(row) {
- const key = row.evidenceLevel || "none";
- return `<span class="ev ev-${key}">${esc(row.evidenceLabel)}</span>`;
- }
- function priorityBadge(p) {
- const cls = ["S", "A", "B"].includes(p) ? `prio-${p}` : "prio-";
- return `<span class="prio ${cls}" title="优先级 ${esc(p || "未定")}">${esc(p || "—")}</span>`;
- }
- /* 人:不构成"卡片",只有首字与名字。之后接的文字会显示在名字后面(inline) */
- function person(name, extra) {
- return `<span class="cell-person"><span class="avatar" aria-hidden="true">${esc(U.initials(name))}</span>
- <span class="pn">${esc(name || "—")}</span>${extra ? `<span class="sub">${esc(extra)}</span>` : ""}</span>`;
- }
- /* 阶段:S 编号 + 五格进度,当前阶段用强调色指路 */
- function stageCell(stageObj, raw, opts) {
- const cur = stageObj ? stageObj.no : 0;
- const steps = T.STAGES.map((s) => {
- if (s.no < cur) return `<i class="on"></i>`;
- if (s.no === cur) return `<i class="cur"></i>`;
- return `<i></i>`;
- }).join("");
- const stuck = opts && opts.stuck;
- return `<span class="stage-cell${stuck ? " stuck" : ""}" title="${stageObj ? esc(stageObj.label) : esc(raw || "")}">
- <span class="sn">${stageObj ? "S" + stageObj.no : esc(raw || "—")}</span>
- <span class="steps">${steps}</span>
- </span>`;
- }
- function stateBadge(status, label) {
- return `<span class="state state-${esc(status || "draft")}">${esc(label || status || "—")}</span>`;
- }
- const icons = {
- search: `<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6"><circle cx="9" cy="9" r="5.5"/><path d="M13.2 13.2 17 17" stroke-linecap="round"/></svg>`,
- menu: `<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><path d="M3 6h14M3 10h14M3 14h14"/></svg>`,
- x: `<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><path d="M5 5l10 10M15 5 5 15"/></svg>`,
- arrow: `<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M4 10h12M11 5l5 5-5 5"/></svg>`,
- info: `<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="10" cy="10" r="7.5"/><path d="M10 9v5M10 6.5v.5" stroke-linecap="round"/></svg>`,
- refresh: `<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M16 8a6 6 0 1 0 .3 4"/><path d="M16 3.5V8h-4.5"/></svg>`,
- chevron: `<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M8 5l5 5-5 5"/></svg>`,
- doc: `<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"><path d="M6 2.5h5.5L15.5 6.5V17a.5.5 0 0 1-.5.5H6a.5.5 0 0 1-.5-.5V3a.5.5 0 0 1 .5-.5Z"/><path d="M11.5 2.8V6.5h3.7M8 10h5M8 13h5"/></svg>`,
- link: `<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><path d="M8.5 11.5a3 3 0 0 0 4.2 0l2.3-2.3a3 3 0 0 0-4.2-4.2l-1 1"/><path d="M11.5 8.5a3 3 0 0 0-4.2 0L5 10.8a3 3 0 0 0 4.2 4.2l1-1"/></svg>`,
- quote: `<svg viewBox="0 0 20 20" fill="currentColor"><path d="M4 5.5A1.5 1.5 0 0 1 5.5 4H9v5H6.5v2.5H9V16H5.5A1.5 1.5 0 0 1 4 14.5v-9Zm7.5 0A1.5 1.5 0 0 1 13 4h3.5v5H14v2.5h2.5V16H13a1.5 1.5 0 0 1-1.5-1.5v-9Z" opacity=".7"/></svg>`,
- layers: `<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"><path d="M10 2.5 17 6l-7 3.5L3 6l7-3.5Z"/><path d="M3 10l7 3.5L17 10M3 14l7 3.5L17 14"/></svg>`,
- grid: `<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="3" width="5.5" height="5.5" rx="1.5"/><rect x="11.5" y="3" width="5.5" height="5.5" rx="1.5"/><rect x="3" y="11.5" width="5.5" height="5.5" rx="1.5"/><rect x="11.5" y="11.5" width="5.5" height="5.5" rx="1.5"/></svg>`,
- spark: `<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"><path d="M10 2.5 11.8 7.4 16.5 9.2 11.8 11 10 16 8.2 11 3.5 9.2 8.2 7.4 10 2.5Z"/></svg>`,
- users: `<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="7.5" cy="7" r="2.6"/><path d="M3 16c.6-2.6 2.4-4 4.5-4s3.9 1.4 4.5 4" stroke-linecap="round"/><path d="M12.8 4.8a2.6 2.6 0 0 1 0 4.5M14.5 12.4c1.3.5 2.2 1.7 2.6 3.4" stroke-linecap="round"/></svg>`,
- steps: `<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M3 15h3v-4h4V7h4V3"/><circle cx="3" cy="15" r="1.2" fill="currentColor" stroke="none"/><circle cx="17" cy="3" r="1.2" fill="currentColor" stroke="none"/></svg>`,
- box: `<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"><path d="M10 2.5 17 6v8l-7 3.5L3 14V6l7-3.5Z"/><path d="M3 6l7 3.5L17 6M10 9.5V17"/></svg>`,
- archive: `<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"><rect x="3" y="4" width="14" height="3.5" rx="1"/><path d="M4.5 7.5V15a1.5 1.5 0 0 0 1.5 1.5h8a1.5 1.5 0 0 0 1.5-1.5V7.5M8.5 11h3"/></svg>`,
- cards: `<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"><rect x="3" y="6" width="11" height="11" rx="2.5"/><path d="M6.5 3.5h7.3a2.7 2.7 0 0 1 2.7 2.7v7.3" stroke-linecap="round"/></svg>`,
- back: `<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5l-5 5 5 5"/></svg>`
- };
- /* 四层各自的色点(用于图例、层级条),来自 tokens 里的 --c-* */
- function layerDot(key) {
- return `<span class="layer layer-${esc(key)}" title="${esc(T.layerOf(key) ? T.layerOf(key).desc : "")}">${esc(T.layerOf(key) ? T.layerOf(key).en : key)}</span>`;
- }
- /* ---------- 空态 / 骨架 ---------- */
- function empty(opts) {
- const o = opts || {};
- const acts = (o.actions || []).map((a) =>
- a.href ? `<a class="btn" href="${esc(a.href)}">${esc(a.label)}</a>`
- : `<button class="btn" data-act="${esc(a.act || "")}" ${a.attr || ""}>${esc(a.label)}</button>`).join("");
- return `<div class="empty ${o.sm ? "sm" : ""}" role="status">
- <div class="art" aria-hidden="true"><div class="g1"></div><div class="g2"></div><div class="ln a"></div><div class="ln b"></div><div class="dotc"></div></div>
- <h4>${esc(o.title || "这里还没有内容")}</h4>
- <p>${o.body || ""}</p>
- ${acts ? `<div class="acts">${acts}</div>` : ""}
- </div>`;
- }
- /* 骨架:高度与真实内容对齐,加载前后不顶开 */
- function skeleton(kind, n) {
- const rows = [];
- for (let i = 0; i < (n || 4); i++) {
- rows.push(`<div style="display:grid;grid-template-columns:minmax(0,1.6fr) 90px 110px 60px;gap:18px;align-items:center;padding:14px 22px;border-bottom:1px solid var(--rule)">
- <div><div class="skel skel-line w60"></div><div class="skel skel-line w40" style="margin-top:8px;height:10px"></div></div>
- <div class="skel skel-line w80"></div><div class="skel skel-line w60"></div><div class="skel skel-line" style="justify-self:end;width:36px"></div>
- </div>`);
- }
- if (kind === "metrics") {
- return `<div class="metrics">${Array.from({ length: 4 }).map(() =>
- `<div class="metric"><div class="skel skel-line w40" style="height:12px"></div><div class="skel skel-num" style="margin:14px 0 0 auto"></div><div class="skel skel-line w60" style="margin:10px 0 0 auto;height:10px"></div></div>`).join("")}</div>`;
- }
- return `<div class="panel">${rows.join("")}</div>`;
- }
- /* ---------- 筛选条 ----------
- * dims: [{key,label,multi,options:[{value,label,count}],active:[...]}]
- * 每个选项都显示"在其他条件约束下还剩几条",0 条置灰不可点(但仍可看到)。
- * 所有维度同时生效,点击任一维度只切换该维度自身的值。 */
- function filterBar(dims, c, resultCount, unit) {
- const parts = dims.map((d) => {
- const act = Array.isArray(d.active) ? d.active : [];
- const chips = d.options.map((o) => {
- const on = act.includes(o.value);
- /* 0 条的选项仍可点:结果会变空,空态里会说明是哪项卡住并给出清除按钮 */
- const zero = o.count === 0 && !on;
- return `<button class="chip${zero ? " zero" : ""}" data-act="facet" data-dim="${d.key}" data-value="${esc(o.value)}"
- data-on="${on ? "1" : "0"}" data-zero="${zero ? "1" : "0"}">${esc(o.label)}<span class="n">${o.count}</span></button>`;
- }).join("");
- return `<div class="fgroup ${d.multi ? "multi" : ""}" role="group" aria-label="${esc(d.label)}">
- <span class="fl">${esc(d.label)}</span>${chips}</div>`;
- }).join("");
- const any = !window.KF_QUERY.isEmpty(c);
- return `<div class="filters" id="filterbar">${parts}
- <div class="spacer"></div>
- <span class="result-note">符合当前条件 <b data-rn>${resultCount}</b> ${esc(unit || "条")}</span>
- ${any ? `<button class="clear-all" data-act="clear-filters">清除全部</button>` : ""}
- </div>`;
- }
- /* 已选条件回显(文字型) */
- function activeLine(c, labels) {
- const pills = [];
- const push = (key, value, label) =>
- pills.push(`<span class="pill">${esc(label)}<button data-act="remove-filter" data-dim="${key}" data-value="${esc(value)}" aria-label="移除 ${esc(label)}">${icons.x}</button></span>`);
- c.priority.forEach((v) => push("priority", v, `优先级 ${v}`));
- c.stage.forEach((v) => push("stage", v, T.stageLabel(v)));
- c.category.forEach((v) => push("category", v, `品类 ${v}`));
- c.evidence.forEach((v) => push("evidence", v, `证据 ${(T.EVIDENCE.find((e) => e.key === v) || {}).label || v}`));
- c.layer.forEach((v) => push("layer", v, `层 ${v}`));
- c.member.forEach((v) => push("member", v, `成员 ${v}`));
- if (c.q) pills.push(`<span class="pill">搜索「${esc(c.q)}」<button data-act="clear-search" aria-label="清除搜索">${icons.x}</button></span>`);
- if (!pills.length) return "";
- return `<div class="active-line">${pills.join("")}${labels || ""}</div>`;
- }
- function crumb(items) {
- return items.map((it, i) => {
- const last = i === items.length - 1;
- const node = last || !it.href ? `<span class="${last ? "now" : ""}">${esc(it.label)}</span>`
- : `<a href="${esc(it.href)}">${esc(it.label)}</a>`;
- return (i ? `<span class="sep">/</span>` : "") + node;
- }).join("");
- }
- window.KF_UI = {
- esc, layerBadge, layerDot, evidenceBadge, priorityBadge, person, stageCell, stateBadge,
- icons, empty, skeleton, filterBar, activeLine, crumb
- };
- })();
|