/* 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 `
| ${esc(p.title)}${esc(p.category || "品类未定")}${p.summary ? " · " + esc(p.summary) : ""} |
${UI.person(p.ownerName)} |
${UI.stageCell(p.stageObj, p.stage)} |
${UI.priorityBadge(p.priority)} |
${UI.stateBadge(p.status, p.statusLabel)} |
${p.score == null ? "—" : p.score.toFixed(1)} |
${p.artifactCount} |
${esc(U.fmtAgo(p.lastActiveAt))} |
`;
}
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 = `
| 项目 | 负责人 | 阶段 | 优先级 | 状态 | 评分 | 成果 | 最近活动 |
${sorted.map(row).join("")}
`;
}
return `
项目
每个项目是一个学员从机会卡推进出的提案。评分右上对齐,方便横向比较。
${Object.entries(SORTS).map(([k, s]) => ``).join("")}
${UI.filterBar(dims, c, rows.length, "个项目")}
${UI.activeLine(c)}
${body}
`;
}
/* ---------- 详情 ---------- */
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 = `
${UI.stateBadge(p.status, p.statusLabel)}创建于 ${esc(U.fmtDate(p.createdAt, true))}
${esc(p.title)}
负责人${esc(p.ownerName || "—")}
当前阶段${esc(p.stage)}
优先级${esc(p.priority || "—")}
评分${p.score == null ? "—" : p.score.toFixed(1)}
品类${esc(p.category || "待定")}
成果数${arts.length}
带出身(谁/会话)${withOwner} / ${arts.length}
带原文出处${withQuote} / ${arts.length}
被归纳引用${citedInsights.length} 条
`;
const list = arts.length ? `
| 成果 | 阶段 | 层 | 证据 | 提交时间 |
${arts.map((a) => `
| ${esc(a.title)}${esc(a.typeLabel)} |
${UI.stageCell(a.stageObj, a.stage)} |
${UI.layerBadge(a.layer)} |
${UI.evidenceBadge(a)} |
${esc(U.fmtDate(a.createdAt, true))} |
`).join("")}
`
: UI.empty({ sm: true, title: "这个项目还没有成果", body: "学员在这个项目下的对话产出会在这里依次出现,每一条都带阶段和证据情况。" });
const ins = citedInsights.length ? `${citedInsights.map((i) => `
${esc(i.title)}
${i.ownerCount} 人提到${i.quoteCount} 条原文引用本项 ${i.artifactIds.filter((x) => p.artifactIds.includes(x)).length} 条记录
`).join("")}
`
: UI.empty({ sm: true, title: "尚未被归纳引用", body: "当本项目的成果足够支撑某个结论时,归纳区会指向这里的记录。" });
return `
${UI.crumb([{ label: "项目", href: "#/projects" }, { label: p.title }])}
${head}
成果时间线
${arts.length} 条
${list}
相关归纳
${ins}
${p.summary ? `
项目摘要
${esc(p.summary)}
` : ""}
`;
}
window.KF_VIEW_PROJECTS = { listView, detail, SORTS };
})();