| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158 |
- /* views/projects.js —— 项目池与项目详情。 */
- (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;
- const SORTS = {
- newest: { label: "最近活动", fn: (a, b) => new Date(b.lastActiveAt || 0) - new Date(a.lastActiveAt || 0) },
- score: { label: "评分", fn: (a, b) => U.cmpNum(a.score, b.score, -1) },
- priority: { label: "优先级", fn: (a, b) => {
- const r = (p) => (p === "S" ? 0 : p === "A" ? 1 : p === "B" ? 2 : 3);
- return r(a.priority) - r(b.priority) || U.cmpNum(a.score, b.score, -1);
- } },
- artifactCount: { label: "成果数", fn: (a, b) => U.cmpNum(a.artifactCount, b.artifactCount, -1) }
- };
- /* 项目层的优先级/品类可能为空(早期数据):用"未定"作为明确的筛选值,计数里列出来,
- * 而不是让它们从筛选条里悄悄消失。 */
- const UNSET = "未定";
- function rowOf(p) {
- return {
- ...p,
- evidenceKey: "none",
- ownerName: p.ownerName,
- priority: T.PRIORITIES.includes(p.priority) ? p.priority : UNSET,
- category: p.category || UNSET
- };
- }
- function facet(rows, c, key, keyFn) {
- const m = new Map();
- for (const r of rows) {
- if (!Q.test(r, { ...c, [key]: [] })) continue;
- const k = keyFn(r);
- if (k != null) m.set(k, (m.get(k) || 0) + 1);
- }
- return m;
- }
- function row(p) {
- return `<tr class="clickable" data-act="nav" data-href="#/project/${encodeURIComponent(p.id)}">
- <td class="cell-title"><span class="ttl">${esc(p.title)}</span><span class="sub">${esc(p.category || "品类未定")}${p.summary ? " · " + esc(p.summary) : ""}</span></td>
- <td data-l="成员">${UI.person(p.ownerName)}</td>
- <td data-l="阶段">${UI.stageCell(p.stageObj, p.stage)}</td>
- <td data-l="优先级">${UI.priorityBadge(p.priority)}</td>
- <td data-l="状态">${UI.stateBadge(p.status, p.statusLabel)}</td>
- <td data-l="评分" class="num-cell">${p.score == null ? "—" : p.score.toFixed(1)}</td>
- <td data-l="成果" class="num-cell">${p.artifactCount}</td>
- <td data-l="最近活动" class="when">${esc(U.fmtAgo(p.lastActiveAt))}</td>
- </tr>`;
- }
- function listView(db, c) {
- const all = db.projects.map(rowOf);
- const rows = all.filter((r) => Q.test(r, c));
- const sortKey = SORTS[c.sort] ? c.sort : "newest";
- const sorted = rows.slice().sort(SORTS[sortKey].fn);
- const pc = facet(all, c, "priority", (r) => r.priority);
- const sc = facet(all, c, "stage", (r) => (r.stageObj ? r.stageObj.code : null));
- const cats = [...new Set(all.map((r) => r.category).filter(Boolean))].sort((a, b) => a.localeCompare(b, "zh"));
- const cc = facet(all, c, "category", (r) => r.category);
- const mc = facet(all, c, "member", (r) => r.ownerName);
- const dims = [
- { key: "priority", label: "优先级", options: T.PRIORITIES.map((v) => ({ value: v, label: v, count: pc.get(v) || 0 })) },
- { key: "stage", label: "阶段", options: T.STAGES.map((s) => ({ value: s.code, label: s.code, count: sc.get(s.code) || 0 })) },
- { key: "category", label: "品类", options: cats.map((v) => ({ value: v, label: v, count: cc.get(v) || 0 })) },
- { key: "member", label: "成员", multi: true, options: [...mc.entries()].map(([v, n]) => ({ value: v, label: v, count: n })) }
- ];
- dims.forEach((d) => { d.active = c[d.key] || []; });
- let body;
- if (!all.length) {
- body = UI.empty({ title: "项目池是空的", body: "学员在 S3 以后从机会卡立项,KfProject 会出现新项目;这里会显示它的阶段、优先级、评分和成果数。" });
- } else if (!sorted.length) {
- body = UI.empty({ title: "当前条件下没有项目", body: `项目池有 ${all.length} 个,但没有同时满足全部条件的。`, actions: [{ label: "清除全部筛选", act: "clear-filters" }] });
- } else {
- body = `<div class="panel tbl-wrap"><table class="tbl"><thead><tr>
- <th>项目</th><th>负责人</th><th>阶段</th><th>优先级</th><th>状态</th><th class="r">评分</th><th class="r">成果</th><th>最近活动</th>
- </tr></thead><tbody>${sorted.map(row).join("")}</tbody></table></div>`;
- }
- return `<div class="view-head"><div>
- <h1>项目</h1><p class="lede">每个项目是一个学员从机会卡推进出的提案。评分右上对齐,方便横向比较。</p>
- </div>
- <div class="aside"><div class="seg" role="group" aria-label="排序">
- ${Object.entries(SORTS).map(([k, s]) => `<button class="${k === sortKey ? "on" : ""}" data-act="sort" data-sort="${k}">${esc(s.label)}</button>`).join("")}
- </div></div>
- </div>
- <div data-filterbar>${UI.filterBar(dims, c, rows.length, "个项目")}</div>
- <div data-active>${UI.activeLine(c)}</div>
- <div data-list>${body}</div>`;
- }
- /* ---------- 详情 ---------- */
- function detail(db, id) {
- const p = db.projectById[id];
- if (!p) return null;
- const arts = p.artifactIds.map((x) => db.artifactById[x]).sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
- const withQuote = arts.filter((a) => a.quoteCount > 0).length;
- const withOwner = arts.filter((a) => a.ownerKey).length;
- const citedInsights = db.insights.filter((i) => i.artifactIds.some((x) => p.artifactIds.includes(x)));
- const head = `<div class="flat detail-head">
- <div>
- <div class="kicker">${UI.stateBadge(p.status, p.statusLabel)}<span>创建于 ${esc(U.fmtDate(p.createdAt, true))}</span></div>
- <h1>${esc(p.title)}</h1>
- <div class="kv">
- <div>负责人<b>${esc(p.ownerName || "—")}</b></div>
- <div>当前阶段<b>${esc(p.stage)}</b></div>
- <div>优先级<b>${esc(p.priority || "—")}</b></div>
- <div>评分<b class="num">${p.score == null ? "—" : p.score.toFixed(1)}</b></div>
- <div>品类<b>${esc(p.category || "待定")}</b></div>
- </div>
- </div>
- <div class="integrity" aria-label="出身完整性">
- <div class="row"><span>成果数</span><span>${arts.length}</span></div>
- <div class="row ${withOwner < arts.length ? "bad" : ""}"><span>带出身(谁/会话)</span><span>${withOwner} / ${arts.length}</span></div>
- <div class="row ${withQuote < arts.length ? "bad" : ""}"><span>带原文出处</span><span>${withQuote} / ${arts.length}</span></div>
- <div class="row"><span>被归纳引用</span><span>${citedInsights.length} 条</span></div>
- </div>
- </div>`;
- const list = 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="阶段">${UI.stageCell(a.stageObj, a.stage)}</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: "学员在这个项目下的对话产出会在这里依次出现,每一条都带阶段和证据情况。" });
- const ins = citedInsights.length ? `<div class="panel">${citedInsights.map((i) => `
- <div class="insight" style="grid-template-columns:minmax(0,1fr)">
- <div class="insight-top"><h4 data-act="open-insight" data-id="${esc(i.id)}">${esc(i.title)}</h4></div>
- <div class="meta"><span>${i.ownerCount} 人提到</span><span>${i.quoteCount} 条原文</span><span>引用本项 ${i.artifactIds.filter((x) => p.artifactIds.includes(x)).length} 条记录</span></div>
- </div>`).join("")}</div>`
- : UI.empty({ sm: true, title: "尚未被归纳引用", body: "当本项目的成果足够支撑某个结论时,归纳区会指向这里的记录。" });
- return `<div>
- <div style="margin-bottom:18px">${UI.crumb([{ label: "项目", href: "#/projects" }, { label: p.title }])}</div>
- ${head}
- <div class="sec"><h2>成果时间线</h2><span class="hint">${arts.length} 条</span></div>
- ${list}
- <div class="sec"><h2>相关归纳</h2></div>
- ${ins}
- ${p.summary ? `<div class="sec"><h2>项目摘要</h2></div><div class="flat panel-pad prose">${esc(p.summary)}</div>` : ""}
- </div>`;
- }
- window.KF_VIEW_PROJECTS = { listView, detail, SORTS };
- })();
|