| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208 |
- /**
- * services/sample-data.js —— 五端模拟视图的独立数据服务(仅演示)。
- *
- * 隔离原则(重要):
- * 1. 数据源是 preview-seed.json(334 条基线),不读线上、不读生产库。
- * 2. 一切写入(改状态 / 评分 / 评论)只落在内存 + 独立 localStorage 命名空间
- * `kf-sample::`,绝不调用 /classes/* 或 /api/functions。
- * 3. 与 board-service 暴露同名方法,页面可以复用同一套交互组件,
- * 但底层不是生产写路径 —— 这是为了「抽屉能点,但不会动线上数据」。
- * 4. 提供 resetSample() 一键还原,演示前可清场。
- */
- const SEED_URL = new URL('../../data/preview-seed.json', import.meta.url).href;
- const LS_KEY = 'kf-sample::mutations::v1';
- export const SAMPLE_DATASET_VERSION = 'ksf.innovation-preview-seed.v1';
- export const SAMPLE_BASELINE_COUNT = 334;
- let seedCache = null;
- let seeded = null;
- /** 模块级共享引用:视图加载后写这里,抽屉直接读,避免事件时序问题 */
- let currentModel = null;
- export function setCurrentModel(m) { currentModel = m; }
- export function getCurrentModel() { return currentModel; }
- /** 读种子(浏览器端缓存;失败抛错,不兜底造数据) */
- export async function loadSeed() {
- if (seedCache) return seedCache;
- const res = await fetch(SEED_URL, { cache: 'no-cache' });
- if (!res.ok) throw new Error(`种子读取失败 HTTP ${res.status}`);
- seedCache = await res.json();
- return seedCache;
- }
- /** 本地改动(状态 / 评分 / 评论),命名空间隔离,可清空 */
- function readMutations() {
- try { return JSON.parse(localStorage.getItem(LS_KEY) || '{}'); } catch { return {}; }
- }
- function writeMutations(m) {
- localStorage.setItem(LS_KEY, JSON.stringify(m));
- }
- export function resetSample() {
- localStorage.removeItem(LS_KEY);
- }
- export function hasSampleMutations() {
- return Object.keys(readMutations()).length > 0;
- }
- /**
- * 把种子适配成界面模型(五端统一用这一份)。
- * 只做字段改名与派生,不改数量、不引入种子外的对象。
- */
- export function adaptSeed(seed) {
- if (seeded) return seeded;
- const mut = readMutations();
- const groups = (seed.groups || []).map((g) => ({
- id: g.id, name: g.name || g.id,
- memberIds: g.memberIds || [], projectIds: g.projectIds || [],
- }));
- // 成员:种子里没有 stuck / stagesSubmitted 字段 —— 不造。
- // 「交付了哪些阶段」由 reports 按 groupId + stageCode 实算(见下方 reportsByGroup)。
- const members = (seed.members || []).map((m) => ({
- id: m.id, name: m.displayName || m.name || m.id,
- role: m.role || '', groupId: m.groupId || '',
- aiFamiliarity: m.aiFamiliarity || '',
- }));
- // 机会卡:状态/评分以本地改动为优先(仅演示)
- const opportunities = (seed.opportunities || []).map((o) => {
- const patch = mut.opportunities?.[o.id] || {};
- const scores = patch.scores || o.scores || [];
- const n = scores.length;
- const avg = n ? Math.round(scores.reduce((s, x) => s + (Number(x.score) || 0), 0) / n * 10) / 10 : null;
- return {
- id: o.id,
- title: o.title || '(未命名)',
- oneLiner: o.oneLiner || o.demandSummary || '',
- demandSummary: o.demandSummary || '',
- status: patch.status || o.status || 'candidate',
- statusRaw: patch.status || o.status || 'candidate',
- stageCode: o.stageCode || '',
- category: o.category || '',
- usageScenario: o.usageScenario || '',
- targetAudience: o.targetAudience || '',
- claimKind: o.claimKind || 'hypothesis',
- priority: o.priority || '',
- evidenceIds: o.evidenceIds || [],
- quoteCount: o.quoteCount != null ? o.quoteCount : (o.evidenceIds || []).length,
- citeCount: o.citeCount || 0,
- projectId: o.projectId || '',
- groupId: o.groupId || '',
- ownerName: o.ownerName || '',
- scores,
- stats: n ? { n, avg, min: Math.min(...scores.map((s) => Number(s.score))), max: Math.max(...scores.map((s) => Number(s.score))), latest: scores[n - 1] } : null,
- decision: o.decision || null,
- decisionLog: o.decisionLog || [],
- };
- });
- // 项目:阶段 / 状态用种子原值
- const projects = (seed.projects || []).map((p) => ({
- id: p.id,
- title: p.title || p.name || '(未命名项目)',
- groupId: p.groupId || '',
- ownerMemberId: p.ownerMemberId || '',
- ownerName: (seed.members || []).find((x) => x.id === p.ownerMemberId)?.displayName || '',
- targetPrice: p.targetPrice || null,
- enterpriseApproval: !!p.enterpriseApproval,
- formalReportPublished: !!p.formalReportPublished,
- stageCode: p.stageCode || '',
- stage: { code: p.stageCode || '', label: p.stageLabel || p.stageCode || '' },
- progressState: p.progressState || p.state || '',
- stateLabel: p.stateLabel || '',
- usageScenario: p.usageScenario || '',
- knownRisk: p.knownRisk || '',
- questionIds: p.questionIds || [],
- artifactIds: p.artifactIds || [],
- opportunityId: p.opportunityId || '',
- }));
- const sources = (seed.sources || []).map((s) => ({
- id: s.id, title: s.title || '(未命名来源)',
- platform: s.platform || '', authorLabel: s.authorLabel || '',
- verificationState: s.verificationState || '', kind: s.kind || '',
- }));
- const evidence = seed.evidence || [];
- const reports = (seed.reports || []).map((r) => ({
- id: r.id, title: r.title || '', stageCode: r.stageCode || '',
- projectId: r.projectId || '', groupId: r.groupId || '',
- dataMode: r.dataMode || 'demo_only', version: r.version || 1,
- }));
- const questions = (seed.questions || []).map((q) => ({
- id: q.id, text: q.text || q.title || '', projectId: q.projectId || '', status: q.status || 'open',
- }));
- const validationTasks = (seed.validationTasks || []).map((v) => ({
- id: v.id, projectId: v.projectId || '', state: v.state || '', title: v.title || '',
- }));
- const actual = seed.actualSeedCounts || {};
- const baselineCount = ['groups','members','ideas','sources','evidence','opportunities','projects','conceptVersions','reports','questions','validationTasks','assignments']
- .reduce((n, k) => n + (Array.isArray(seed[k]) ? seed[k].length : 0), 0);
- // 按组×阶段实算交付情况(种子无 stagesSubmitted,这是唯一的真实来源)
- const reportsByGroup = {};
- const reportsByStage = {};
- for (const r of reports) {
- const g = r.groupId || '未分组';
- reportsByGroup[g] = reportsByGroup[g] || { total: 0, stages: {} };
- reportsByGroup[g].total += 1;
- reportsByGroup[g].stages[r.stageCode] = (reportsByGroup[g].stages[r.stageCode] || 0) + 1;
- reportsByStage[r.stageCode] = (reportsByStage[r.stageCode] || 0) + 1;
- }
- // 半场作业状态(assignments 真实字段)
- const assignments = (seed.assignments || []).map((a) => ({
- id: a.id, groupId: a.groupId || '', halfDay: a.halfDay || 1,
- state: a.state || '', reportIds: a.reportIds || [],
- deliverableTypes: a.deliverableTypes || [],
- }));
- seeded = {
- datasetVersion: seed.version || SAMPLE_DATASET_VERSION,
- reportsByGroup, reportsByStage, assignmentsReal: assignments,
- baselineEntityCount: baselineCount,
- groups, members, opportunities, projects, sources, evidence, reports, questions, validationTasks,
- ideas: seed.ideas || [], conceptVersions: seed.conceptVersions || [], assignments: assignments,
- projectStageCounts: seed.projectStageCounts || {}, projectStateCounts: seed.projectStateCounts || {},
- actualSeedCounts: actual,
- opportunityById: Object.fromEntries(opportunities.map((o) => [o.id, o])),
- };
- return seeded;
- }
- /** 便捷:加载 + 适配 */
- export async function loadSampleModel() {
- const seed = await loadSeed();
- return adaptSeed(seed);
- }
- /**
- * 模拟写操作(与 board-service 同名同形,但只写本地命名空间)。
- * 调用方:详情抽屉在模拟模式下注入本对象,替代生产 boardService。
- */
- export const sampleWriter = {
- mode: 'sample',
- setOpportunityStatus(id, status, { by = '演示', reason = '' } = {}) {
- const m = readMutations();
- m.opportunities = m.opportunities || {};
- m.opportunities[id] = { ...(m.opportunities[id] || {}), status, by, reason, at: new Date().toISOString() };
- writeMutations(m);
- seeded = null;
- return { changed: true, scope: 'sample' };
- },
- rateOpportunity(id, { user = '演示', score, comment = '' }) {
- const m = readMutations();
- m.opportunities = m.opportunities || {};
- const rec = m.opportunities[id] || {};
- const scores = (rec.scores || []).filter((s) => s.user !== user);
- scores.push({ user, score: Number(score), comment, at: new Date().toISOString() });
- m.opportunities[id] = { ...rec, scores };
- writeMutations(m);
- seeded = null;
- return { scope: 'sample', n: scores.length };
- },
- };
|