stages.js 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  1. /* views/stages.js —— 按阶段:每个阶段多少人推进、多少人卡住、多少人有成果。 */
  2. (function () {
  3. "use strict";
  4. const T = window.KF_TAXONOMY;
  5. const U = window.KF_UTIL;
  6. const UI = window.KF_UI;
  7. const esc = U.esc;
  8. function stageStats(db) {
  9. return T.STAGES.map((s) => {
  10. const here = db.members.filter((m) => m.stageObj && m.stageObj.no === s.no);
  11. const reached = db.members.filter((m) => m.stageObj && m.stageObj.no >= s.no);
  12. const arts = db.artifacts.filter((a) => a.stageObj && a.stageObj.no === s.no && a.layer !== "raw");
  13. const delivered = here.filter((m) => m.done);
  14. const stuck = here.filter((m) => !m.done);
  15. return { s, here, reached, arts, delivered, stuck };
  16. });
  17. }
  18. function render(db) {
  19. const stats = stageStats(db);
  20. const total = db.members.length;
  21. if (!total && !db.artifacts.length) {
  22. return `<div class="view-head"><h1>阶段</h1></div>` + UI.empty({
  23. title: "还没有进度可以展示",
  24. body: "学员与分身的会话会记录他们当前处在哪一步。这里会按 S1 → S5 排开,写清每个阶段有多少人在推进、多少人停在这里。"
  25. });
  26. }
  27. const cards = stats.map(({ s, here, reached, arts, delivered, stuck }) => {
  28. const pct = total ? Math.round((here.length / total) * 100) : 0;
  29. const artPct = arts.length ? Math.round((arts.filter((a) => a.quoteCount > 0).length / arts.length) * 100) : 0;
  30. return `<a class="metric" href="#/stage/${s.code}" style="display:block;text-decoration:none;color:inherit">
  31. <div class="k">${esc(s.label)}</div>
  32. <div class="v" style="font-size:28px">${here.length}<small>人在此</small></div>
  33. <div class="foot">${delivered.length} 已交 · <b>${stuck.length}</b> 未交 · ${reached.length} 到达过</div>
  34. <div style="height:3px;border-radius:2px;background:rgba(16,24,40,.06);margin-top:12px;overflow:hidden">
  35. <div style="height:100%;width:${pct}%;background:var(--ink-2)"></div>
  36. </div>
  37. <div class="foot" style="margin-top:10px">本阶段成果 ${arts.length} 条 · ${artPct}% 带原文</div>
  38. </a>`;
  39. }).join("");
  40. return `<div class="view-head"><div>
  41. <h1>阶段</h1>
  42. <p class="lede">五个阶段各自有多少人、交没交。点进任一阶段可以看具体是谁。</p>
  43. </div></div>
  44. <div class="metrics" style="grid-template-columns:repeat(auto-fit,minmax(200px,1fr))">${cards}</div>
  45. <div class="sec"><h2>谁停在哪</h2><span class="hint">按"是否已交"分</span></div>
  46. <div class="panel tbl-wrap"><table class="tbl"><thead><tr>
  47. <th>阶段</th><th>已交(人)</th><th>未交(人)</th><th class="r">成果</th><th class="r">带原文</th><th class="r">推测</th></tr></thead><tbody>
  48. ${stats.map(({ s, here, arts, delivered, stuck }) => {
  49. const quoted = arts.filter((a) => a.quoteCount > 0).length;
  50. const spec = arts.filter((a) => a.evidenceLevel === "speculative").length;
  51. return `<tr class="clickable" data-act="nav" data-href="#/stage/${s.code}">
  52. <td class="cell-title"><span class="ttl">${esc(s.label)}</span><span class="sub">${here.length ? esc(here.map((m) => m.name).slice(0, 6).join("、")) + (here.length > 6 ? "…" : "") : "暂无人在此"}</span></td>
  53. <td data-l="已交" class="num-cell">${delivered.length}</td>
  54. <td data-l="未交" class="num-cell">${stuck.length}</td>
  55. <td data-l="成果" class="num-cell">${arts.length}</td>
  56. <td data-l="带原文" class="num-cell">${quoted}</td>
  57. <td data-l="推测" class="num-cell">${spec}</td></tr>`;
  58. }).join("")}
  59. </tbody></table></div>`;
  60. }
  61. function detail(db, code) {
  62. const s = T.stageOf(code);
  63. if (!s) return null;
  64. const st = stageStats(db).find((x) => x.s.code === s.code);
  65. const arts = st.arts.slice().sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
  66. const people = st.here.slice().sort((a, b) => (a.done - b.done) || a.name.localeCompare(b.name, "zh"));
  67. const peopleHtml = people.length ? `<div class="panel"><div class="panel-head"><h3>停在这个阶段的人</h3><span class="hint">${people.length} 人</span></div>
  68. ${people.map((m) => `<div class="trace-item" style="padding:12px 20px">
  69. <span class="idx">${m.done ? "✓" : "—"}</span>
  70. <div><div class="t" data-act="nav" data-href="#/member/${encodeURIComponent(m.name)}">${esc(m.name)}</div>
  71. <div class="m"><span>${esc(m.groupId || "未分组")}</span><span>${m.msgCount} 条对话</span><span>${m.artifactIds.length} 条成果</span><span>最后活动 ${esc(U.fmtAgo(m.lastActiveAt))}</span></div>
  72. </div>
  73. <span class="state"><span class="pdot ${m.done ? "done" : "stuck"}"></span>${m.done ? "已交" : "未交"}</span>
  74. </div>`).join("")}</div>`
  75. : UI.empty({ sm: true, title: "这个阶段没有人", body: "学员推进到这里时,会自动出现在这一栏。" });
  76. const artHtml = arts.length ? `<div class="panel tbl-wrap"><table class="tbl"><thead><tr>
  77. <th>成果</th><th>成员</th><th>层</th><th>证据</th><th>提交时间</th></tr></thead><tbody>
  78. ${arts.map((a) => `<tr class="clickable" data-act="open-artifact" data-id="${esc(a.id)}">
  79. <td class="cell-title"><span class="ttl">${esc(a.title)}</span><span class="sub">${esc(a.typeLabel)}</span></td>
  80. <td data-l="成员">${esc(a.ownerName)}</td>
  81. <td data-l="层">${UI.layerBadge(a.layer)}</td>
  82. <td data-l="证据">${UI.evidenceBadge(a)}</td>
  83. <td data-l="提交" class="when">${esc(U.fmtDate(a.createdAt, true))}</td></tr>`).join("")}
  84. </tbody></table></div>`
  85. : UI.empty({ sm: true, title: "这个阶段还没有成果入库", body: "学员在此阶段产生的机会卡、痛点、报告,经上传 skill 入库后会出现在这里。" });
  86. return `<div>
  87. <div style="margin-bottom:18px">${UI.crumb([{ label: "阶段", href: "#/stages" }, { label: s.label }])}</div>
  88. <div class="flat detail-head">
  89. <div>
  90. <div class="kicker"><span>阶段 ${s.no} / 5</span></div>
  91. <h1>${esc(s.label)}</h1>
  92. <div class="kv">
  93. <div>在此阶段<b>${st.here.length} 人</b></div>
  94. <div>已交<b>${st.delivered.length} 人</b></div>
  95. <div>未交<b>${st.stuck.length} 人</b></div>
  96. <div>成果<b>${st.arts.length} 条</b></div>
  97. </div>
  98. </div>
  99. <div class="integrity">
  100. <div class="row"><span>到达过</span><span>${st.reached.length} 人</span></div>
  101. <div class="row ${st.arts.length && !st.arts.some((a) => a.quoteCount > 0) ? "bad" : ""}"><span>带原文的成果</span><span>${st.arts.filter((a) => a.quoteCount > 0).length} / ${st.arts.length}</span></div>
  102. </div>
  103. </div>
  104. <div class="sec"><h2>人员</h2></div>${peopleHtml}
  105. <div class="sec"><h2>本阶段成果</h2></div>${artHtml}
  106. </div>`;
  107. }
  108. window.KF_VIEW_STAGES = { render, detail };
  109. })();