/* 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 `layer 缺失`;
return `${l.en}`;
}
/* 证据:纯文字 + 状态点 */
function evidenceBadge(row) {
const key = row.evidenceLevel || "none";
return `${esc(row.evidenceLabel)}`;
}
function priorityBadge(p) {
const cls = ["S", "A", "B"].includes(p) ? `prio-${p}` : "prio-";
return `${esc(p || "—")}`;
}
/* 人:不构成"卡片",只有首字与名字。之后接的文字会显示在名字后面(inline) */
function person(name, extra) {
return `${esc(U.initials(name))}
${esc(name || "—")}${extra ? `${esc(extra)}` : ""}`;
}
/* 阶段:S 编号 + 五格进度,当前阶段用强调色指路 */
function stageCell(stageObj, raw, opts) {
const cur = stageObj ? stageObj.no : 0;
const steps = T.STAGES.map((s) => {
if (s.no < cur) return ``;
if (s.no === cur) return ``;
return ``;
}).join("");
const stuck = opts && opts.stuck;
return `
${stageObj ? "S" + stageObj.no : esc(raw || "—")}
${steps}
`;
}
function stateBadge(status, label) {
return `${esc(label || status || "—")}`;
}
const icons = {
search: ``,
menu: ``,
x: ``,
arrow: ``,
info: ``,
refresh: ``,
chevron: ``,
doc: ``,
link: ``,
quote: ``,
layers: ``,
grid: ``,
spark: ``,
users: ``,
steps: ``,
box: ``,
archive: ``,
cards: ``,
back: ``
};
/* 四层各自的色点(用于图例、层级条),来自 tokens 里的 --c-* */
function layerDot(key) {
return `${esc(T.layerOf(key) ? T.layerOf(key).en : key)}`;
}
/* ---------- 空态 / 骨架 ---------- */
function empty(opts) {
const o = opts || {};
const acts = (o.actions || []).map((a) =>
a.href ? `${esc(a.label)}`
: ``).join("");
return `
${esc(o.title || "这里还没有内容")}
${o.body || ""}
${acts ? `
${acts}
` : ""}
`;
}
/* 骨架:高度与真实内容对齐,加载前后不顶开 */
function skeleton(kind, n) {
const rows = [];
for (let i = 0; i < (n || 4); i++) {
rows.push(``);
}
if (kind === "metrics") {
return `${Array.from({ length: 4 }).map(() =>
`
`).join("")}
`;
}
return `${rows.join("")}
`;
}
/* ---------- 筛选条 ----------
* 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 ``;
}).join("");
return `
${esc(d.label)}${chips}
`;
}).join("");
const any = !window.KF_QUERY.isEmpty(c);
return `${parts}
符合当前条件 ${resultCount} ${esc(unit || "条")}
${any ? `
` : ""}
`;
}
/* 已选条件回显(文字型) */
function activeLine(c, labels) {
const pills = [];
const push = (key, value, label) =>
pills.push(`${esc(label)}`);
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(`搜索「${esc(c.q)}」`);
if (!pills.length) return "";
return `${pills.join("")}${labels || ""}
`;
}
function crumb(items) {
return items.map((it, i) => {
const last = i === items.length - 1;
const node = last || !it.href ? `${esc(it.label)}`
: `${esc(it.label)}`;
return (i ? `/` : "") + node;
}).join("");
}
window.KF_UI = {
esc, layerBadge, layerDot, evidenceBadge, priorityBadge, person, stageCell, stateBadge,
icons, empty, skeleton, filterBar, activeLine, crumb
};
})();