| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118 |
- /* views/pool.js —— 机会卡片池(组织资产)。
- *
- * 这一页回答:"研发中心现在手上有哪些机会、值不值得做、谁在什么时候用什么方式送上来的。"
- * 卡片形态(不是列表行);可筛(优先级/阶段/品类/状态/上传人)、可排(优先级/评分/最新)、
- * 可点评分(多人平均)、可改状态(候选/立项/否决,落库后刷新保持)。
- * 筛选/排序/打开的卡都写进 URL;评分/状态落库,不做仅本地的假操作。
- */
- (function () {
- "use strict";
- const T = window.KF_TAXONOMY;
- const U = window.KF_UTIL;
- const UI = window.KF_UI;
- const OPP = window.KF_OPP;
- const esc = U.esc;
- const STATUS_FILTER = ["candidate", "approved", "rejected"];
- /* 数组.contains 有坑:CSS 里 .o-cat/.o-stage 是类名,这里绝不能把数组当字符串用。
- * 统一用 Set,筛选是精确命中,不做子串匹配。 */
- const has = (arr, v) => new Set(arr).has(v);
- function matches(o, p) {
- if (p.priority && p.priority.length && !has(p.priority, o.priority)) return false;
- if (p.stage && p.stage.length && !(o.stageObj && has(p.stage, o.stageObj.code))) return false;
- if (p.category && p.category.length && !has(p.category, o.category)) return false;
- if (p.status && p.status.length && !has(p.status, o.status)) return false;
- if (p.member && p.member.length && !has(p.member, o.upload.by)) return false;
- return true;
- }
- const list = (v) => (v ? String(v).split(",").filter(Boolean) : []);
- /* 概览条:池子总量 + 三个状态 + 平均分(给判断,不只给数字) */
- function summary(all) {
- const n = (k) => all.filter((o) => o.status === k).length;
- const scored = all.filter((o) => o.stats.n);
- const avg = scored.length ? Math.round(scored.reduce((s, o) => s + o.stats.avg, 0) / scored.length * 10) / 10 : null;
- const top = OPP.sortCards(all, "score")[0];
- return `<div class="metrics pool-metrics">
- <div class="metric"><div class="k">池中机会卡</div><div class="v">${all.length}<small>张</small></div>
- <div class="foot"><b>${n("candidate")}</b> 候选 · <em>${n("approved")}</em> 已立项 · ${n("rejected")} 已否决</div></div>
- <div class="metric"><div class="k">池均综合分</div><div class="v">${avg == null ? "—" : avg}</div>
- <div class="foot">${scored.length} / ${all.length} 张已有人评分</div></div>
- <div class="metric"><div class="k">最高分机会</div><div class="v" style="font-size:20px;letter-spacing:0;line-height:1.3">${top ? esc(top.title) : "—"}</div>
- <div class="foot">${top ? `综合 <b>${top.stats.avg}</b> · ${top.stats.n} 人评 · ${esc(top.stage)}` : ""}</div></div>
- <div class="metric"><div class="k">进池的人</div><div class="v">${new Set(all.map((o) => o.upload.by).filter(Boolean)).size}<small>人</small></div>
- <div class="foot">${new Set(all.map((o) => o.upload.agentName).filter(Boolean)).size} 个 Agent 参与上传</div></div>
- </div>`;
- }
- function facet(label, dim, values, active, counts, multi) {
- if (!values.length) return "";
- const chips = values.map((v) => {
- const on = active.includes(v.value);
- const zero = !counts.get(v.value) && !on;
- return `<button class="chip${zero ? " zero" : ""}" data-act="opp-facet" data-dim="${dim}" data-value="${esc(v.value)}"
- data-on="${on ? "1" : "0"}" data-zero="${zero ? "1" : "0"}">${esc(v.label)}<span class="n">${counts.get(v.value) || 0}</span></button>`;
- }).join("");
- return `<div class="fgroup ${multi ? "multi" : ""}" role="group" aria-label="${esc(label)}"><span class="fl">${esc(label)}</span>${chips}</div>`;
- }
- function filterBar(all, c, n) {
- const rest = (dim) => all.filter((o) => matches(o, { ...c, [dim]: [] }));
- const count = (rows, fn) => U.countBy(rows, fn);
- const pc = count(rest("priority"), (o) => o.priority);
- const sc = count(rest("stage"), (o) => (o.stageObj ? o.stageObj.code : ""));
- const cc = count(rest("category"), (o) => o.category);
- const stc = count(rest("status"), (o) => o.status);
- const mc = count(rest("member"), (o) => o.upload.by);
- const cats = [...new Set(all.map((o) => o.category).filter(Boolean))].sort((a, b) => a.localeCompare(b, "zh"));
- const uploaders = [...mc.entries()].sort((a, b) => b[1] - a[1]).map(([v, k]) => ({ value: v, label: v }));
- const any = ["priority", "stage", "category", "status", "member"].some((k) => c[k].length);
- return `<div class="filters" id="filterbar">
- ${facet("优先级", "priority", T.PRIORITIES.map((v) => ({ value: v, label: v })), c.priority, pc)}
- ${facet("阶段", "stage", T.STAGES.map((s) => ({ value: s.code, label: s.code })), c.stage, sc)}
- ${facet("品类", "category", cats.map((v) => ({ value: v, label: v })), c.category, cc)}
- ${facet("状态", "status", T.OPP_STATUS.map((s) => ({ value: s.key, label: s.label })), c.status, stc)}
- ${facet("上传人", "member", uploaders, c.member, mc, true)}
- <div class="spacer"></div>
- <span class="result-note">符合当前条件 <b data-rn>${n}</b> 张卡</span>
- ${any ? `<button class="clear-all" data-act="opp-clear">清除全部</button>` : ""}
- </div>`;
- }
- function render(db, params) {
- const all = db.opportunities;
- const c = {
- priority: list(params.priority), stage: list(params.stage), category: list(params.category),
- status: list(params.status).filter((s) => STATUS_FILTER.includes(s)), member: list(params.member)
- };
- const sortKey = OPP.SORTS[params.sort] ? params.sort : "prio";
- const rows = OPP.sortCards(all.filter((o) => matches(o, c)), sortKey);
- const head = `<div class="view-head"><div>
- <h1>机会卡片池</h1>
- <p class="lede">全员机会卡汇总入库:每张卡带评分、优先级与决策记录,构成研发中心的机会储备。卡片上的每一步都能追到「谁 · 哪个 Agent · 什么方式 · 什么时候」上传。</p>
- </div><div class="aside">${OPP.sortSeg(sortKey, params.sort)}</div></div>`;
- const body = !all.length
- ? UI.empty({
- title: "池子里还没有机会卡",
- body: "学员在 S3 机会识别阶段用主分身上传机会卡后,会自动进池。这里会按卡片形态排开,带评分、状态和决策记录。",
- actions: [{ label: "看阶段板块", href: "#/stages" }]
- })
- : !rows.length
- ? UI.empty({ title: "当前条件下没有卡", body: `池中 ${all.length} 张卡,没有同时满足全部条件的。单独去掉某一项后会有卡出现。`, actions: [{ label: "清除全部筛选", act: "opp-clear" }] })
- : `<ul class="ogrid">${rows.map((o) => OPP.card(o)).join("")}</ul>`;
- return `${head}
- ${summary(all)}
- <div data-filterbar>${filterBar(all, c, rows.length)}</div>
- <div data-active></div>
- <div data-list>${body}</div>
- <p class="tiny muted pool-foot">共 ${all.length} 张 · ${rows.length} 张符合当前条件 · 点卡片看详情、评分、改状态;平均分由全部评分现算,改状态会记进决策日志。</p>`;
- }
- window.KF_VIEW_POOL = { render, matches };
- })();
|