| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169 |
- /* views/artifacts.js —— 成果列表:四层归属、出身、证据、筛选叠加、排序。
- * 既用于"成果"页,也被"记录层"页复用(固定 layer=record)。 */
- (function () {
- "use strict";
- const T = window.KF_TAXONOMY;
- const U = window.KF_UTIL;
- const Q = window.KF_QUERY;
- const UI = window.KF_UI;
- const esc = U.esc;
- function rowOf(a, ctx) {
- const p = ctx.db.projectById[a.projectId];
- return {
- ...a,
- priority: p ? p.priority : "",
- category: p ? p.category : "",
- projectTitle: p ? p.title : "",
- evidenceKey: a.evidenceLevel
- };
- }
- function rowsFor(db, filterLayer) {
- const base = filterLayer ? db.artifacts.filter((a) => a.layer === filterLayer) : db.artifacts;
- const ctx = { db };
- return base.map((a) => rowOf(a, ctx));
- }
- /* 排序:数字/日期维度按方向,文本维度固定方向 */
- const SORTS = {
- newest: { label: "最新提交", fn: (a, b) => new Date(b.createdAt) - new Date(a.createdAt) },
- evidence: { label: "证据强度", fn: (a, b) => b.evidenceRank - a.evidenceRank || new Date(b.createdAt) - new Date(a.createdAt) },
- owner: { label: "按成员", fn: (a, b) => a.ownerName.localeCompare(b.ownerName, "zh") },
- stage: { label: "按阶段", fn: (a, b) => (b.stageObj ? b.stageObj.no : 0) - (a.stageObj ? a.stageObj.no : 0) }
- };
- function buildFacets(db, rows, c, opts) {
- const P = T.PRIORITIES;
- const dims = [];
- if (!opts.priorityLocked) {
- const pc = Q.facetCounts(rows, c, "priority", (r) => r.priority);
- dims.push({ key: "priority", label: "优先级", options: P.map((v) => ({ value: v, label: v, count: pc.get(v) || 0 })) });
- }
- const sc = Q.facetCounts(rows, c, "stage", (r) => (r.stageObj ? r.stageObj.code : null));
- dims.push({ key: "stage", label: "阶段", options: T.STAGES.map((s) => ({ value: s.code, label: s.code, count: sc.get(s.code) || 0 })) });
- const cats = [...new Set(rows.map((r) => r.category).filter(Boolean))].sort((a, b) => a.localeCompare(b, "zh"));
- const cc = Q.facetCounts(rows, c, "category", (r) => r.category);
- dims.push({ key: "category", label: "品类", options: cats.map((v) => ({ value: v, label: v, count: cc.get(v) || 0 })) });
- const ec = Q.facetCounts(rows, c, "evidence", (r) => r.evidenceKey);
- dims.push({ key: "evidence", label: "证据", options: T.EVIDENCE.slice(0, 3).map((e) => ({ value: e.key, label: e.label, count: ec.get(e.key) || 0 })) });
- if (!opts.layerLocked) {
- const lc = Q.facetCounts(rows, c, "layer", (r) => r.layer);
- dims.push({ key: "layer", label: "层", multi: true, options: T.LAYERS.map((l) => ({ value: l.key, label: l.en, count: lc.get(l.key) || 0 })) });
- }
- const mc = Q.facetCounts(rows, c, "member", (r) => r.ownerName);
- const members = [...mc.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0], "zh"));
- dims.push({ key: "member", label: "成员", multi: true, options: members.map(([v, n]) => ({ value: v, label: v, count: n })) });
- /* 每个维度的当前选中值,供筛选条回显 */
- dims.forEach((d) => { d.active = c[d.key] || []; });
- return dims;
- }
- /* 单行。带报告的成果在标题上直接给入口——不点进抽屉也能翻开读 */
- function row(r) {
- const ttl = r.report
- ? `<a class="ttl r-inline-link" href="#/report/${encodeURIComponent(r.id)}" data-act="open-report" data-id="${esc(r.id)}">${esc(r.shortTitle)}</a>`
- : `<span class="ttl">${esc(r.title)}</span>`;
- return `<tr class="clickable${r.report ? " has-report" : ""}${r.id === (window.__KF_FOCUS || "") ? " focus-on" : ""}" data-act="open-artifact" data-id="${esc(r.id)}">
- <td class="cell-title">${ttl}<span class="sub">${r.report ? `<span class="r-dot" title="这份成果带完整报告">报告</span>` : ""}${esc(r.typeLabel)}${r.projectTitle ? " \u00b7 " + esc(r.projectTitle) : ""}</span></td>
- <td data-l="成员">${UI.person(r.ownerName)}</td>
- <td data-l="阶段">${UI.stageCell(r.stageObj, r.stage)}</td>
- <td data-l="优先级">${UI.priorityBadge(r.priority)}</td>
- <td data-l="层">${UI.layerBadge(r.layer)}${r.report ? ` <span class="tiny muted" title="这条成果带完整报告">报告</span>` : ""}${r.citedByInsight ? ` <span class="tiny muted" title="并被归纳引用">被引用</span>` : ""}</td>
- <td data-l="证据">${UI.evidenceBadge(r)}${r.evidenceUnaligned ? ` <span class="una" title="标记为有原文,但记录中没有原文出处">未对齐</span>` : ""}</td>
- <td data-l="提交" class="when">${U.fmtDate(r.createdAt, true)}</td>
- </tr>`;
- }
- function header(sorted) {
- const th = (key, label, cls) => {
- const on = sorted === key;
- return `<th class="${cls || ""} sortable${on ? " sorted" : ""}" data-act="sort" data-sort="${key}" aria-sort="${on ? "descending" : "none"}">${label}<span class="arr">${on ? "↓" : "↕"}</span></th>`;
- };
- return `<table class="tbl"><thead><tr>
- <th>成果</th><th>成员</th>${th("stage", "阶段")}<th>优先级</th><th>层</th>${th("evidence", "证据")}${th("newest", "提交时间")}
- </tr></thead><tbody>`;
- }
- /* 结果为空时,逐项去掉一个条件看还剩几条,告诉用户是哪项卡住了 */
- function relaxHint(rows, c) {
- const parts = [];
- const q = window.KF_QUERY;
- for (const dim of ["priority", "stage", "category", "evidence", "layer", "member"]) {
- const vals = c[dim];
- if (!vals.length || vals.length < 1) continue;
- const n = rows.filter((r) => q.test(r, { ...c, [dim]: [] })).length;
- parts.push(`${dimLabel(dim)}=${vals.join("、")} 时只剩 ${n} 条`);
- }
- if (c.q) parts.push(`搜索「${c.q}」时只剩 ${rows.filter((r) => q.test(r, { ...c, q: "" })).length} 条`);
- return parts.length ? "单独去掉某一项后: " + parts.join(";") + "。" : "";
- }
- function dimLabel(d) {
- return ({ priority: "优先级", stage: "阶段", category: "品类", evidence: "证据", layer: "层", member: "成员" })[d] || d;
- }
- /* 页面主体 */
- function listView(db, c, opts) {
- const rowsAll = rowsFor(db, opts.filterLayer);
- const rows = rowsAll.filter((r) => Q.test(r, c));
- const sortKey = SORTS[c.sort] ? c.sort : (opts.defaultSort || "newest");
- const sorted = (opts.sortLocked ? rowsAll.slice() : rows.slice());
- sorted.sort(SORTS[sortKey].fn);
- /* ?focus=<id>:从报告/抽屉跳回来时,标出"你要找的那条";只影响排序,不隐藏其他行 */
- const focusA = c.focus ? db.artifactById[c.focus] : null;
- window.__KF_FOCUS = focusA ? focusA.id : "";
- if (focusA) {
- const i = sorted.findIndex((r) => r.id === focusA.id);
- if (i > 0) sorted.unshift(sorted.splice(i, 1)[0]);
- }
- const dims = buildFacets(db, rowsAll, c, opts);
- const filterHtml = filterBarFor(dims, c, rows.length, opts.unit || "条成果");
- let body;
- if (!rowsAll.length) {
- body = UI.empty({
- title: opts.emptyTitle || "还没有归集的成果",
- body: opts.emptyBody || "学员对话结束后,上传 skill 会把成果写进库;刷新或等待自动刷新的那一刻,这里就会出现带出身(谁、何时、哪一步、有没有原文)的记录。",
- actions: opts.emptyActions
- });
- } else if (!rows.length) {
- body = UI.empty({
- title: "当前条件下没有成果",
- body: `已有 ${rowsAll.length} 条成果,但没有同时满足全部条件的。${relaxHint(rowsAll, c)}`,
- actions: [{ label: "清除全部筛选", act: "clear-filters" }]
- });
- } else {
- body = `<div class="panel tbl-wrap">${header(opts.sortLocked ? null : sortKey)}${sorted.map(row).join("")}</tbody></table></div>`;
- }
- return `<div class="view-head"><div>
- <h1>${esc(opts.title)}</h1>
- <p class="lede">${opts.lede}</p>
- </div>
- <div class="aside">${opts.sortLocked ? "" : sortSeg(sortKey)}</div>
- </div>
- <div data-filterbar>${filterHtml}</div>
- <div data-active>${UI.activeLine(c)}</div>
- ${focusA ? `<div class="focus-note">${UI.icons.link}<span>正在定位:<b>${esc(focusA.shortTitle)}</b> · ${esc(focusA.ownerName || "未知")} · ${esc(focusA.stage)}</span><a href="#" data-act="clear-focus" style="margin-left:auto">清除定位</a></div>` : ""}
- <div data-list>${body}</div>`;
- }
- /* 筛选条:优先复用 common.filterBar,这里补充 layer 锁定提示 */
- function filterBarFor(dims, c, n, unit) {
- return UI.filterBar(dims, c, n, unit);
- }
- function sortSeg(cur) {
- return `<div class="seg" role="group" aria-label="排序">
- ${Object.entries(SORTS).map(([k, s]) => `<button class="${k === cur ? "on" : ""}" data-act="sort" data-sort="${k}">${esc(s.label)}</button>`).join("")}
- </div>`;
- }
- window.KF_VIEW_ARTIFACTS = { listView, rowsFor, SORTS };
- })();
|