artifacts.js 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. /* views/artifacts.js —— 成果列表:四层归属、出身、证据、筛选叠加、排序。
  2. * 既用于"成果"页,也被"记录层"页复用(固定 layer=record)。 */
  3. (function () {
  4. "use strict";
  5. const T = window.KF_TAXONOMY;
  6. const U = window.KF_UTIL;
  7. const Q = window.KF_QUERY;
  8. const UI = window.KF_UI;
  9. const esc = U.esc;
  10. function rowOf(a, ctx) {
  11. const p = ctx.db.projectById[a.projectId];
  12. return {
  13. ...a,
  14. priority: p ? p.priority : "",
  15. category: p ? p.category : "",
  16. projectTitle: p ? p.title : "",
  17. evidenceKey: a.evidenceLevel
  18. };
  19. }
  20. function rowsFor(db, filterLayer) {
  21. const base = filterLayer ? db.artifacts.filter((a) => a.layer === filterLayer) : db.artifacts;
  22. const ctx = { db };
  23. return base.map((a) => rowOf(a, ctx));
  24. }
  25. /* 排序:数字/日期维度按方向,文本维度固定方向 */
  26. const SORTS = {
  27. newest: { label: "最新提交", fn: (a, b) => new Date(b.createdAt) - new Date(a.createdAt) },
  28. evidence: { label: "证据强度", fn: (a, b) => b.evidenceRank - a.evidenceRank || new Date(b.createdAt) - new Date(a.createdAt) },
  29. owner: { label: "按成员", fn: (a, b) => a.ownerName.localeCompare(b.ownerName, "zh") },
  30. stage: { label: "按阶段", fn: (a, b) => (b.stageObj ? b.stageObj.no : 0) - (a.stageObj ? a.stageObj.no : 0) }
  31. };
  32. function buildFacets(db, rows, c, opts) {
  33. const P = T.PRIORITIES;
  34. const dims = [];
  35. if (!opts.priorityLocked) {
  36. const pc = Q.facetCounts(rows, c, "priority", (r) => r.priority);
  37. dims.push({ key: "priority", label: "优先级", options: P.map((v) => ({ value: v, label: v, count: pc.get(v) || 0 })) });
  38. }
  39. const sc = Q.facetCounts(rows, c, "stage", (r) => (r.stageObj ? r.stageObj.code : null));
  40. dims.push({ key: "stage", label: "阶段", options: T.STAGES.map((s) => ({ value: s.code, label: s.code, count: sc.get(s.code) || 0 })) });
  41. const cats = [...new Set(rows.map((r) => r.category).filter(Boolean))].sort((a, b) => a.localeCompare(b, "zh"));
  42. const cc = Q.facetCounts(rows, c, "category", (r) => r.category);
  43. dims.push({ key: "category", label: "品类", options: cats.map((v) => ({ value: v, label: v, count: cc.get(v) || 0 })) });
  44. const ec = Q.facetCounts(rows, c, "evidence", (r) => r.evidenceKey);
  45. 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 })) });
  46. if (!opts.layerLocked) {
  47. const lc = Q.facetCounts(rows, c, "layer", (r) => r.layer);
  48. dims.push({ key: "layer", label: "层", multi: true, options: T.LAYERS.map((l) => ({ value: l.key, label: l.en, count: lc.get(l.key) || 0 })) });
  49. }
  50. const mc = Q.facetCounts(rows, c, "member", (r) => r.ownerName);
  51. const members = [...mc.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0], "zh"));
  52. dims.push({ key: "member", label: "成员", multi: true, options: members.map(([v, n]) => ({ value: v, label: v, count: n })) });
  53. /* 每个维度的当前选中值,供筛选条回显 */
  54. dims.forEach((d) => { d.active = c[d.key] || []; });
  55. return dims;
  56. }
  57. /* 单行。带报告的成果在标题上直接给入口——不点进抽屉也能翻开读 */
  58. function row(r) {
  59. const ttl = r.report
  60. ? `<a class="ttl r-inline-link" href="#/report/${encodeURIComponent(r.id)}" data-act="open-report" data-id="${esc(r.id)}">${esc(r.shortTitle)}</a>`
  61. : `<span class="ttl">${esc(r.title)}</span>`;
  62. return `<tr class="clickable${r.report ? " has-report" : ""}${r.id === (window.__KF_FOCUS || "") ? " focus-on" : ""}" data-act="open-artifact" data-id="${esc(r.id)}">
  63. <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>
  64. <td data-l="成员">${UI.person(r.ownerName)}</td>
  65. <td data-l="阶段">${UI.stageCell(r.stageObj, r.stage)}</td>
  66. <td data-l="优先级">${UI.priorityBadge(r.priority)}</td>
  67. <td data-l="层">${UI.layerBadge(r.layer)}${r.report ? ` <span class="tiny muted" title="这条成果带完整报告">报告</span>` : ""}${r.citedByInsight ? ` <span class="tiny muted" title="并被归纳引用">被引用</span>` : ""}</td>
  68. <td data-l="证据">${UI.evidenceBadge(r)}${r.evidenceUnaligned ? ` <span class="una" title="标记为有原文,但记录中没有原文出处">未对齐</span>` : ""}</td>
  69. <td data-l="提交" class="when">${U.fmtDate(r.createdAt, true)}</td>
  70. </tr>`;
  71. }
  72. function header(sorted) {
  73. const th = (key, label, cls) => {
  74. const on = sorted === key;
  75. 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>`;
  76. };
  77. return `<table class="tbl"><thead><tr>
  78. <th>成果</th><th>成员</th>${th("stage", "阶段")}<th>优先级</th><th>层</th>${th("evidence", "证据")}${th("newest", "提交时间")}
  79. </tr></thead><tbody>`;
  80. }
  81. /* 结果为空时,逐项去掉一个条件看还剩几条,告诉用户是哪项卡住了 */
  82. function relaxHint(rows, c) {
  83. const parts = [];
  84. const q = window.KF_QUERY;
  85. for (const dim of ["priority", "stage", "category", "evidence", "layer", "member"]) {
  86. const vals = c[dim];
  87. if (!vals.length || vals.length < 1) continue;
  88. const n = rows.filter((r) => q.test(r, { ...c, [dim]: [] })).length;
  89. parts.push(`${dimLabel(dim)}=${vals.join("、")} 时只剩 ${n} 条`);
  90. }
  91. if (c.q) parts.push(`搜索「${c.q}」时只剩 ${rows.filter((r) => q.test(r, { ...c, q: "" })).length} 条`);
  92. return parts.length ? "单独去掉某一项后: " + parts.join(";") + "。" : "";
  93. }
  94. function dimLabel(d) {
  95. return ({ priority: "优先级", stage: "阶段", category: "品类", evidence: "证据", layer: "层", member: "成员" })[d] || d;
  96. }
  97. /* 页面主体 */
  98. function listView(db, c, opts) {
  99. const rowsAll = rowsFor(db, opts.filterLayer);
  100. const rows = rowsAll.filter((r) => Q.test(r, c));
  101. const sortKey = SORTS[c.sort] ? c.sort : (opts.defaultSort || "newest");
  102. const sorted = (opts.sortLocked ? rowsAll.slice() : rows.slice());
  103. sorted.sort(SORTS[sortKey].fn);
  104. /* ?focus=<id>:从报告/抽屉跳回来时,标出"你要找的那条";只影响排序,不隐藏其他行 */
  105. const focusA = c.focus ? db.artifactById[c.focus] : null;
  106. window.__KF_FOCUS = focusA ? focusA.id : "";
  107. if (focusA) {
  108. const i = sorted.findIndex((r) => r.id === focusA.id);
  109. if (i > 0) sorted.unshift(sorted.splice(i, 1)[0]);
  110. }
  111. const dims = buildFacets(db, rowsAll, c, opts);
  112. const filterHtml = filterBarFor(dims, c, rows.length, opts.unit || "条成果");
  113. let body;
  114. if (!rowsAll.length) {
  115. body = UI.empty({
  116. title: opts.emptyTitle || "还没有归集的成果",
  117. body: opts.emptyBody || "学员对话结束后,上传 skill 会把成果写进库;刷新或等待自动刷新的那一刻,这里就会出现带出身(谁、何时、哪一步、有没有原文)的记录。",
  118. actions: opts.emptyActions
  119. });
  120. } else if (!rows.length) {
  121. body = UI.empty({
  122. title: "当前条件下没有成果",
  123. body: `已有 ${rowsAll.length} 条成果,但没有同时满足全部条件的。${relaxHint(rowsAll, c)}`,
  124. actions: [{ label: "清除全部筛选", act: "clear-filters" }]
  125. });
  126. } else {
  127. body = `<div class="panel tbl-wrap">${header(opts.sortLocked ? null : sortKey)}${sorted.map(row).join("")}</tbody></table></div>`;
  128. }
  129. return `<div class="view-head"><div>
  130. <h1>${esc(opts.title)}</h1>
  131. <p class="lede">${opts.lede}</p>
  132. </div>
  133. <div class="aside">${opts.sortLocked ? "" : sortSeg(sortKey)}</div>
  134. </div>
  135. <div data-filterbar>${filterHtml}</div>
  136. <div data-active>${UI.activeLine(c)}</div>
  137. ${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>` : ""}
  138. <div data-list>${body}</div>`;
  139. }
  140. /* 筛选条:优先复用 common.filterBar,这里补充 layer 锁定提示 */
  141. function filterBarFor(dims, c, n, unit) {
  142. return UI.filterBar(dims, c, n, unit);
  143. }
  144. function sortSeg(cur) {
  145. return `<div class="seg" role="group" aria-label="排序">
  146. ${Object.entries(SORTS).map(([k, s]) => `<button class="${k === cur ? "on" : ""}" data-act="sort" data-sort="${k}">${esc(s.label)}</button>`).join("")}
  147. </div>`;
  148. }
  149. window.KF_VIEW_ARTIFACTS = { listView, rowsFor, SORTS };
  150. })();