members.js 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. /* views/members.js —— 按人:成员列表与成员详情(他交了什么、卡在哪)。 */
  2. (function () {
  3. "use strict";
  4. const U = window.KF_UTIL;
  5. const Q = window.KF_QUERY;
  6. const UI = window.KF_UI;
  7. const esc = U.esc;
  8. function memberStatus(m) {
  9. if (m.done) return { key: "done", label: "已提交", cls: "done" };
  10. if (m.msgCount >= 5) return { key: "stuck", label: "在对话,未提交", cls: "stuck" };
  11. return { key: "idle", label: "未开始 / 未提交", cls: "idle" };
  12. }
  13. function listView(db, c) {
  14. const q = c.q;
  15. const rows = db.members.filter((m) => {
  16. if (c.member.length && !c.member.includes(m.name)) return false;
  17. if (q && !m.name.toLowerCase().includes(q)) return false;
  18. return true;
  19. }).sort((a, b) => (a.done - b.done) || b.msgCount - a.msgCount || a.name.localeCompare(b.name, "zh"));
  20. const total = db.members.length;
  21. const done = db.members.filter((m) => m.done).length;
  22. const body = !total
  23. ? UI.empty({ title: "还没有成员", body: "学员打开与分身的会话后,KfSession 会记录姓名、组、当前阶段和消息数;这里会按「交没交」排序。" })
  24. : !rows.length
  25. ? UI.empty({ title: "没有匹配的成员", body: "换个名字试试,或清除筛选。", actions: [{ label: "清除筛选", act: "clear-filters" }] })
  26. : `<div class="panel tbl-wrap"><table class="tbl"><thead><tr>
  27. <th>成员</th><th>组</th><th>阶段</th><th class="r">消息</th><th class="r">成果</th><th>最后活动</th><th>状态</th>
  28. </tr></thead><tbody>${rows.map((m) => {
  29. const s = memberStatus(m);
  30. return `<tr class="clickable" data-act="nav" data-href="#/member/${encodeURIComponent(m.name)}">
  31. <td class="cell-title">${UI.person(m.name)}</td>
  32. <td data-l="组">${esc(m.groupId || "—")}</td>
  33. <td data-l="阶段">${UI.stageCell(m.stageObj, "—")}</td>
  34. <td data-l="消息" class="num-cell">${m.msgCount}</td>
  35. <td data-l="成果" class="num-cell">${m.artifactIds.length}</td>
  36. <td data-l="最后活动" class="when">${esc(U.fmtAgo(m.lastActiveAt))}</td>
  37. <td data-l="状态"><span class="state"><span class="pdot ${s.cls}"></span>${s.label}</span></td>
  38. </tr>`;
  39. }).join("")}</tbody></table></div>`;
  40. return `<div class="view-head"><div>
  41. <h1>成员</h1>
  42. <p class="lede">${total} 人参与,${done} 人已提交成果。未提交的排前面,现场可以逐个盯进度。</p>
  43. </div></div>
  44. ${UI.activeLine(c)}
  45. ${body}`;
  46. }
  47. function detail(db, name) {
  48. const m = db.members.find((x) => x.name === name);
  49. if (!m) return null;
  50. const arts = m.artifactIds.map((id) => db.artifactById[id]).sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
  51. const projects = m.projectIds.map((id) => db.projectById[id]);
  52. const sessions = db.sessions.filter((s) => s.studentName === name);
  53. const s = memberStatus(m);
  54. const head = `<div class="flat detail-head">
  55. <div>
  56. <div class="kicker"><span class="state"><span class="pdot ${s.cls}"></span>${s.label}</span><span>${esc(m.groupId || "未分组")}</span></div>
  57. <h1>${esc(m.name)}</h1>
  58. <div class="kv">
  59. <div>当前阶段<b>${m.stageObj ? esc(m.stageObj.label) : "—"}</b></div>
  60. <div>对话消息<b class="num">${m.msgCount}</b></div>
  61. <div>最后活动<b>${esc(U.fmtAgo(m.lastActiveAt))}</b></div>
  62. </div>
  63. </div>
  64. <div class="integrity">
  65. <div class="row"><span>成果</span><span>${arts.length}</span></div>
  66. <div class="row"><span>项目</span><span>${projects.length}</span></div>
  67. <div class="row"><span>会话</span><span>${sessions.length}</span></div>
  68. </div>
  69. </div>`;
  70. const artList = arts.length ? `<div class="panel">${arts.map((a) => `
  71. <div class="trace-item" style="padding:12px 20px">
  72. <span class="idx">${esc(U.fmtDate(a.createdAt))}</span>
  73. <div><div class="t" data-act="open-artifact" data-id="${esc(a.id)}">${esc(a.title)}</div>
  74. <div class="m">${UI.stageCell(a.stageObj, a.stage)}${UI.layerBadge(a.layer)}${UI.evidenceBadge(a)}<span>${esc(a.typeLabel)}</span></div>
  75. </div>
  76. <span class="tiny muted">${a.quoteCount} 条原文</span>
  77. </div>`).join("")}</div>`
  78. : UI.empty({ sm: true, title: "这个人还没有交成果", body: `已有 ${m.msgCount} 条对话。对话结束后,成果经上传 skill 入库,会出现在这里。` });
  79. const projList = projects.length ? `<div class="panel tbl-wrap"><table class="tbl"><thead><tr>
  80. <th>项目</th><th>阶段</th><th>优先级</th><th class="r">评分</th><th>状态</th></tr></thead><tbody>
  81. ${projects.map((p) => `<tr class="clickable" data-act="nav" data-href="#/project/${encodeURIComponent(p.id)}">
  82. <td class="cell-title"><span class="ttl">${esc(p.title)}</span></td>
  83. <td data-l="阶段">${UI.stageCell(p.stageObj, p.stage)}</td>
  84. <td data-l="优先级">${UI.priorityBadge(p.priority)}</td>
  85. <td data-l="评分" class="num-cell">${p.score == null ? "—" : p.score.toFixed(1)}</td>
  86. <td data-l="状态">${UI.stateBadge(p.status, p.statusLabel)}</td></tr>`).join("")}
  87. </tbody></table></div>`
  88. : UI.empty({ sm: true, title: "还没有立项", body: "在 S3 机会识别之后,学员可以从机会卡立项;立项后这里会列出项目。" });
  89. return `<div>
  90. <div style="margin-bottom:18px">${UI.crumb([{ label: "成员", href: "#/members" }, { label: m.name }])}</div>
  91. ${head}
  92. <div class="sec"><h2>成果</h2><span class="hint">${arts.length} 条</span></div>${artList}
  93. <div class="sec"><h2>项目</h2></div>${projList}
  94. <div class="sec"><h2>过滤此人的归纳 / 记录</h2></div>
  95. <div class="note">${UI.icons.info}<span>查看 <a href="#/artifacts?member=${encodeURIComponent(m.name)}">他的全部成果</a>,或 <a href="#/insights?member=${encodeURIComponent(m.name)}">引用过他成果的归纳</a>。</span></div>
  96. </div>`;
  97. }
  98. window.KF_VIEW_MEMBERS = { listView, detail };
  99. })();