From e9f44c8f0fc7741ada540c48069e791661ba3753 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=91=A3=E5=AE=8F=E5=87=AF?= Date: Sat, 13 Jun 2026 10:51:10 +0800 Subject: [PATCH] =?UTF-8?q?@=20feat(=E5=9C=86=E6=A1=8C):=20=E5=A4=A7?= =?UTF-8?q?=E5=B1=8F=E5=A4=9A=E9=A1=B5=E6=B5=85=E8=89=B2=E6=A8=A1=E6=9D=BF?= =?UTF-8?q?=E9=87=8D=E5=86=99=E4=B8=BA=E8=93=9D=E5=9B=BE=E5=B7=A5=E7=A8=8B?= =?UTF-8?q?=E5=9B=BE=E9=A3=8E(=E5=8F=82=E8=80=83=E8=AE=BE=E8=AE=A1?= =?UTF-8?q?=E5=9B=BE)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 浅色 palette 改玫瑰红(#E11D48)+slate 中性+白底网格,席位色序对齐设计图 - 卡片四角红色角标 + slate 网格 overlay(蓝图风);KPI 加彩色图标 - 共识竖条摘要 + 要点列表;席位卡进度条 + 风险药丸;席位专页精致化 - 新增折线图 svgLine(趋势),donut 升级镂空中心数字+图例;契约加 line 类型 - 图表保持纯 SVG(零依赖离线自包含,沙箱/新窗口/下载/分享通用,与参考源码一致) - 深色保持深蓝 token 套同结构 Co-Authored-By: Claude Fable 5 @ --- .../lib/report-template.test.ts | 7 +- .../lib/report-template.ts | 316 +++++++++++------- 2 files changed, 201 insertions(+), 122 deletions(-) diff --git a/frontend-web/src/roundtable-planning/lib/report-template.test.ts b/frontend-web/src/roundtable-planning/lib/report-template.test.ts index 59c6aaef..cc7b5e9b 100644 --- a/frontend-web/src/roundtable-planning/lib/report-template.test.ts +++ b/frontend-web/src/roundtable-planning/lib/report-template.test.ts @@ -107,9 +107,10 @@ test("renderDashboardHtml:主题跟随 —— dark=深蓝规范 / light=浅红 assert.ok(dark.includes("#122347")); assert.ok(dark.includes("#1677FF")); const light = renderDashboardHtml(d, { theme: "light" }); - // 浅红规范:页面 #FAFAFA / 容器 #FFFFFF / 主色 #F5222D(§1 色彩) - assert.ok(light.includes("#FAFAFA")); - assert.ok(light.includes("#F5222D")); + // 蓝图工程图风(参考设计图):页面 #F8FAFC / 玫瑰红主色 #E11D48 / slate 文字 + assert.ok(light.includes("#F8FAFC")); + assert.ok(light.includes("#E11D48")); + assert.ok(light.includes("#1E293B")); assert.ok(!light.includes("#0C1836")); // 缺省 = dark(向后兼容) const def = renderDashboardHtml(d); diff --git a/frontend-web/src/roundtable-planning/lib/report-template.ts b/frontend-web/src/roundtable-planning/lib/report-template.ts index 23b326e3..e7a76164 100644 --- a/frontend-web/src/roundtable-planning/lib/report-template.ts +++ b/frontend-web/src/roundtable-planning/lib/report-template.ts @@ -48,7 +48,7 @@ export interface ReportChartItem { } export interface ReportChart { - type: "bar" | "donut" | "radar"; + type: "bar" | "donut" | "radar" | "line"; title: string; items: ReportChartItem[]; } @@ -240,10 +240,9 @@ export function coerceReportData(raw: unknown): ReportData | null { const co = (c ?? {}) as Record; const type = str(co.type); return { - type: (type === "bar" || type === "donut" || type === "radar" ? type : "bar") as - | "bar" - | "donut" - | "radar", + type: (type === "bar" || type === "donut" || type === "radar" || type === "line" + ? type + : "bar") as "bar" | "donut" | "radar" | "line", title: str(co.title), items: arr(co.items) .map((it) => { @@ -297,7 +296,7 @@ export function buildDashboardPrompt(opts: { '{"title":"大屏主标题(≤20字)","subtitle":"一句话定位",', ' "kpis":[{"label":"指标名","value":"数值","unit":"单位","trend":"up|down|flat"}], // 3-6 个核心指标,从研讨内容提取或合理估算', ' "consensus":{"headline":"一句话最终结论","points":["结论要点,3-6 条"]},', - ' "charts":[{"type":"bar|donut|radar","title":"图表标题","items":[{"label":"维度","value":数字}]}], // 1-3 个总览图表(风险分布/资源占比/方案评分等),3-6 项/图', + ' "charts":[{"type":"line|bar|donut|radar","title":"图表标题","items":[{"label":"维度","value":数字}]}], // 1-3 个总览图表:line=趋势(销量/渗透率随时间)、donut=占比(份额/构成)、radar=多维对比(竞争力)、bar=排名;3-8 项/图', ' "timeline":[{"label":"阶段名","desc":"一句话说明"}], // 执行路线 3-6 阶段', ' "seats":[{"id":"seat-1","name":"席位名(与交付席位一致)","summary":"该席位结论一两句",', ' "sections":[{"heading":"小节标题","bullets":["要点,每条≤40字"]}], // 2-4 节,提炼该席位交付', @@ -364,27 +363,29 @@ const PALETTES: Record = { risk: { high: "#F5222D", mid: "#FAAD14", low: "#52C41A" }, modalMask: "rgba(6,14,36,.78)", }, + // 蓝图/工程图风(参考大屏设计图):白底 + slate 网格、玫瑰红(rose-600)强调、 + // slate 中性文字、卡片四角红色角标、席位色序(scheme红/ux靛/ad绿/risk琥珀/summary天蓝)。 light: { - bgPage: "#FAFAFA", + bgPage: "#F8FAFC", bgGradient: - "radial-gradient(1200px 600px at 70% -10%,rgba(245,34,45,.05),transparent),linear-gradient(160deg,#FFFFFF,#FAFAFA)", - gridLine: "rgba(245,34,45,.035)", + "radial-gradient(1100px 520px at 78% -12%,rgba(225,29,72,.05),transparent),linear-gradient(180deg,#FFFFFF,#F8FAFC)", + gridLine: "rgba(148,163,184,.16)", card: "#FFFFFF", - cardBorder: "#E8E8E8", - cardGlow: "0 2px 8px rgba(0,0,0,.04)", - bracket: "#F5222D", - text: "#222222", - textSecondary: "#575757", - textStrong: "#222222", - heading: "#A8071A", - primary: "#F5222D", - link: "#0079FE", - titleColor: "linear-gradient(90deg,#F5222D,#CF1322 60%,#A8071A)", - navBg: "rgba(255,255,255,.92)", - trackBg: "#F0F0F0", - accents: ["#F5222D", "#0079FE", "#189D72", "#FF9900", "#45C3F3", "#7F7F7F"], - risk: { high: "#F5222D", mid: "#FF9900", low: "#189D72" }, - modalMask: "rgba(34,34,34,.45)", + cardBorder: "rgba(226,232,240,.9)", + cardGlow: "0 3px 10px rgba(15,23,42,.04)", + bracket: "#E11D48", + text: "#475569", + textSecondary: "#64748B", + textStrong: "#1E293B", + heading: "#1E293B", + primary: "#E11D48", + link: "#E11D48", + titleColor: "linear-gradient(90deg,#1E293B,#334155)", + navBg: "rgba(255,255,255,.95)", + trackBg: "#F1F5F9", + accents: ["#E11D48", "#4F46E5", "#059669", "#D97706", "#0284C7", "#7C3AED"], + risk: { high: "#DC2626", mid: "#D97706", low: "#059669" }, + modalMask: "rgba(15,23,42,.42)", }, }; @@ -427,24 +428,59 @@ function svgBar(chart: ReportChart): string { function svgDonut(chart: ReportChart): string { const items = chart.items.slice(0, 6); const total = items.reduce((s, i) => s + Math.max(0, i.value), 0) || 1; - const R = 56; + const R = 50; + const SW = 14; const C = 2 * Math.PI * R; - let offset = 0; + let acc = 0; const segs = items .map((it, i) => { - const frac = Math.max(0, it.value) / total; - const seg = ``; - offset += frac; + const len = (Math.max(0, it.value) / total) * C; + const seg = ``; + acc += len; return seg; }) .join(""); + // 中心:总分类数 + 标签(对齐设计图环形图的镂空中心)。 + const center = `${items.length}类`; const legend = items .map( (it, i) => - `
${esc(it.label)} ${esc(String(it.value))}
`, + `
${esc(it.label)}${esc(String(it.value))}
`, ) .join(""); - return `
${segs}
${legend}
`; + return `
${segs}${center}
${legend}
`; +} + +// 折线图(趋势):多取前 8 点;自动归一化到画布。设计图的销量趋势用折线。 +function svgLine(chart: ReportChart): string { + const items = chart.items.slice(0, 8); + if (items.length < 2) return svgBar(chart); + const W = 300; + const H = 150; + const padL = 30; + const padR = 12; + const padT = 12; + const padB = 24; + const max = Math.max(...items.map((i) => i.value), 1); + const min = Math.min(...items.map((i) => i.value), 0); + const span = max - min || 1; + const x = (i: number) => padL + (i / (items.length - 1)) * (W - padL - padR); + const y = (v: number) => padT + (1 - (v - min) / span) * (H - padT - padB); + const pts = items.map((it, i) => `${x(i).toFixed(1)},${y(it.value).toFixed(1)}`).join(" "); + const grid = [0, 0.5, 1] + .map((k) => { + const gy = (padT + k * (H - padT - padB)).toFixed(1); + return ``; + }) + .join(""); + const area = ``; + const dots = items + .map((it, i) => ``) + .join(""); + const labels = items + .map((it, i) => `${esc(it.label)}`) + .join(""); + return `${grid}${area}${dots}${labels}`; } function svgRadar(chart: ReportChart): string { @@ -480,7 +516,13 @@ function svgRadar(chart: ReportChart): string { function chartHtml(chart: ReportChart): string { const body = - chart.type === "donut" ? svgDonut(chart) : chart.type === "radar" ? svgRadar(chart) : svgBar(chart); + chart.type === "donut" + ? svgDonut(chart) + : chart.type === "radar" + ? svgRadar(chart) + : chart.type === "line" + ? svgLine(chart) + : svgBar(chart); return `

${esc(chart.title || "图表")}

${body}
`; } @@ -501,7 +543,7 @@ function metricBars(metrics: ReportSeatMetric[]): string { return `
${metrics .map( (m, i) => - `
${esc(m.label)}
${m.value}
`, + `
${esc(m.label)}${m.value}
`, ) .join("")}
`; } @@ -533,20 +575,23 @@ export function renderDashboardHtml( `总览` + seats.map((s) => `${esc(s.name)}`).join(""); + const KPI_ICONS = ["📊", "📈", "⚠️", "🧩", "🏆", "📅"]; const kpis = data.kpis - .map( - (k, i) => - `
${esc(k.label)}
${esc(k.value)}${esc(k.unit ?? "")}${ + .map((k, i) => { + const ac = P.accents[i % P.accents.length]; + return `
+
${esc(k.label)}${KPI_ICONS[i % KPI_ICONS.length]}
+
${esc(k.value)}${esc(k.unit ?? "")}${ k.trend === "up" ? '▲' : k.trend === "down" ? '▼' : "" - }
`, - ) + }
`; + }) .join(""); const seatCards = seats .map((s, i) => { const color = P.accents[i % P.accents.length]; return ` -

${esc(s.name)}

进入席位页 →
+

${esc(s.name)}

席位页 →

${esc(s.summary || "(无摘要)")}

${metricBars(s.metrics.slice(0, 2))} ${riskChips(s.risks.slice(0, 2))} @@ -562,17 +607,20 @@ export function renderDashboardHtml( const consensusPoints = data.consensus.points.map((p) => `
  • ${esc(p)}
  • `).join(""); + const chartsBlock = data.charts.length + ? `
    ${data.charts.map(chartHtml).join("")}
    ` + : ""; const overview = `
    ${kpis}
    -
    + -

    席位交付总览 点击卡片进入席位专页

    +

    席位交付总览 点击卡片进入对应席位专页

    ${seatCards}
    ${timeline}
    `; @@ -620,95 +668,125 @@ export function renderDashboardHtml( ${esc(data.title)}

    ${esc(data.title)}

    ${esc(data.subtitle)}${generated}