| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119 |
- /* views/stages.js —— 按阶段:每个阶段多少人推进、多少人卡住、多少人有成果。 */
- (function () {
- "use strict";
- const T = window.KF_TAXONOMY;
- const U = window.KF_UTIL;
- const UI = window.KF_UI;
- const esc = U.esc;
- function stageStats(db) {
- return T.STAGES.map((s) => {
- const here = db.members.filter((m) => m.stageObj && m.stageObj.no === s.no);
- const reached = db.members.filter((m) => m.stageObj && m.stageObj.no >= s.no);
- const arts = db.artifacts.filter((a) => a.stageObj && a.stageObj.no === s.no && a.layer !== "raw");
- const delivered = here.filter((m) => m.done);
- const stuck = here.filter((m) => !m.done);
- return { s, here, reached, arts, delivered, stuck };
- });
- }
- function render(db) {
- const stats = stageStats(db);
- const total = db.members.length;
- if (!total && !db.artifacts.length) {
- return `<div class="view-head"><h1>阶段</h1></div>` + UI.empty({
- title: "还没有进度可以展示",
- body: "学员与分身的会话会记录他们当前处在哪一步。这里会按 S1 → S5 排开,写清每个阶段有多少人在推进、多少人停在这里。"
- });
- }
- const cards = stats.map(({ s, here, reached, arts, delivered, stuck }) => {
- const pct = total ? Math.round((here.length / total) * 100) : 0;
- const artPct = arts.length ? Math.round((arts.filter((a) => a.quoteCount > 0).length / arts.length) * 100) : 0;
- return `<a class="metric" href="#/stage/${s.code}" style="display:block;text-decoration:none;color:inherit">
- <div class="k">${esc(s.label)}</div>
- <div class="v" style="font-size:28px">${here.length}<small>人在此</small></div>
- <div class="foot">${delivered.length} 已交 · <b>${stuck.length}</b> 未交 · ${reached.length} 到达过</div>
- <div style="height:3px;border-radius:2px;background:rgba(16,24,40,.06);margin-top:12px;overflow:hidden">
- <div style="height:100%;width:${pct}%;background:var(--ink-2)"></div>
- </div>
- <div class="foot" style="margin-top:10px">本阶段成果 ${arts.length} 条 · ${artPct}% 带原文</div>
- </a>`;
- }).join("");
- return `<div class="view-head"><div>
- <h1>阶段</h1>
- <p class="lede">五个阶段各自有多少人、交没交。点进任一阶段可以看具体是谁。</p>
- </div></div>
- <div class="metrics" style="grid-template-columns:repeat(auto-fit,minmax(200px,1fr))">${cards}</div>
- <div class="sec"><h2>谁停在哪</h2><span class="hint">按"是否已交"分</span></div>
- <div class="panel tbl-wrap"><table class="tbl"><thead><tr>
- <th>阶段</th><th>已交(人)</th><th>未交(人)</th><th class="r">成果</th><th class="r">带原文</th><th class="r">推测</th></tr></thead><tbody>
- ${stats.map(({ s, here, arts, delivered, stuck }) => {
- const quoted = arts.filter((a) => a.quoteCount > 0).length;
- const spec = arts.filter((a) => a.evidenceLevel === "speculative").length;
- return `<tr class="clickable" data-act="nav" data-href="#/stage/${s.code}">
- <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>
- <td data-l="已交" class="num-cell">${delivered.length}</td>
- <td data-l="未交" class="num-cell">${stuck.length}</td>
- <td data-l="成果" class="num-cell">${arts.length}</td>
- <td data-l="带原文" class="num-cell">${quoted}</td>
- <td data-l="推测" class="num-cell">${spec}</td></tr>`;
- }).join("")}
- </tbody></table></div>`;
- }
- function detail(db, code) {
- const s = T.stageOf(code);
- if (!s) return null;
- const st = stageStats(db).find((x) => x.s.code === s.code);
- const arts = st.arts.slice().sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
- const people = st.here.slice().sort((a, b) => (a.done - b.done) || a.name.localeCompare(b.name, "zh"));
- const peopleHtml = people.length ? `<div class="panel"><div class="panel-head"><h3>停在这个阶段的人</h3><span class="hint">${people.length} 人</span></div>
- ${people.map((m) => `<div class="trace-item" style="padding:12px 20px">
- <span class="idx">${m.done ? "✓" : "—"}</span>
- <div><div class="t" data-act="nav" data-href="#/member/${encodeURIComponent(m.name)}">${esc(m.name)}</div>
- <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>
- </div>
- <span class="state"><span class="pdot ${m.done ? "done" : "stuck"}"></span>${m.done ? "已交" : "未交"}</span>
- </div>`).join("")}</div>`
- : UI.empty({ sm: true, title: "这个阶段没有人", body: "学员推进到这里时,会自动出现在这一栏。" });
- const artHtml = arts.length ? `<div class="panel tbl-wrap"><table class="tbl"><thead><tr>
- <th>成果</th><th>成员</th><th>层</th><th>证据</th><th>提交时间</th></tr></thead><tbody>
- ${arts.map((a) => `<tr class="clickable" data-act="open-artifact" data-id="${esc(a.id)}">
- <td class="cell-title"><span class="ttl">${esc(a.title)}</span><span class="sub">${esc(a.typeLabel)}</span></td>
- <td data-l="成员">${esc(a.ownerName)}</td>
- <td data-l="层">${UI.layerBadge(a.layer)}</td>
- <td data-l="证据">${UI.evidenceBadge(a)}</td>
- <td data-l="提交" class="when">${esc(U.fmtDate(a.createdAt, true))}</td></tr>`).join("")}
- </tbody></table></div>`
- : UI.empty({ sm: true, title: "这个阶段还没有成果入库", body: "学员在此阶段产生的机会卡、痛点、报告,经上传 skill 入库后会出现在这里。" });
- return `<div>
- <div style="margin-bottom:18px">${UI.crumb([{ label: "阶段", href: "#/stages" }, { label: s.label }])}</div>
- <div class="flat detail-head">
- <div>
- <div class="kicker"><span>阶段 ${s.no} / 5</span></div>
- <h1>${esc(s.label)}</h1>
- <div class="kv">
- <div>在此阶段<b>${st.here.length} 人</b></div>
- <div>已交<b>${st.delivered.length} 人</b></div>
- <div>未交<b>${st.stuck.length} 人</b></div>
- <div>成果<b>${st.arts.length} 条</b></div>
- </div>
- </div>
- <div class="integrity">
- <div class="row"><span>到达过</span><span>${st.reached.length} 人</span></div>
- <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>
- </div>
- </div>
- <div class="sec"><h2>人员</h2></div>${peopleHtml}
- <div class="sec"><h2>本阶段成果</h2></div>${artHtml}
- </div>`;
- }
- window.KF_VIEW_STAGES = { render, detail };
- })();
|