@
feat(圆桌): 大屏多页浅色模板重写为蓝图工程图风(参考设计图) - 浅色 palette 改玫瑰红(#E11D48)+slate 中性+白底网格,席位色序对齐设计图 - 卡片四角红色角标 + slate 网格 overlay(蓝图风);KPI 加彩色图标 - 共识竖条摘要 + 要点列表;席位卡进度条 + 风险药丸;席位专页精致化 - 新增折线图 svgLine(趋势),donut 升级镂空中心数字+图例;契约加 line 类型 - 图表保持纯 SVG(零依赖离线自包含,沙箱/新窗口/下载/分享通用,与参考源码一致) - 深色保持深蓝 token 套同结构 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> @
This commit is contained in:
1 parent
f3202f95b6
commit
e9f44c8f0f
2 files changed
+201
-122
No files matched your search
@@ -107,9 +107,10 @@ test("renderDashboardHtml:主题跟随 —— dark=深蓝规范 / light=浅红
|
|||||||
assert.ok(dark.includes("#122347"));
|
assert.ok(dark.includes("#122347"));
|
||||||
assert.ok(dark.includes("#1677FF"));
|
assert.ok(dark.includes("#1677FF"));
|
||||||
const light = renderDashboardHtml(d, { theme: "light" });
|
const light = renderDashboardHtml(d, { theme: "light" });
|
||||||
// 浅红规范:页面 #FAFAFA / 容器 #FFFFFF / 主色 #F5222D(§1 色彩)
|
// 蓝图工程图风(参考设计图):页面 #F8FAFC / 玫瑰红主色 #E11D48 / slate 文字
|
||||||
assert.ok(light.includes("#FAFAFA"));
|
assert.ok(light.includes("#F8FAFC"));
|
||||||
assert.ok(light.includes("#F5222D"));
|
assert.ok(light.includes("#E11D48"));
|
||||||
|
assert.ok(light.includes("#1E293B"));
|
||||||
assert.ok(!light.includes("#0C1836"));
|
assert.ok(!light.includes("#0C1836"));
|
||||||
// 缺省 = dark(向后兼容)
|
// 缺省 = dark(向后兼容)
|
||||||
const def = renderDashboardHtml(d);
|
const def = renderDashboardHtml(d);
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export interface ReportChartItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ReportChart {
|
export interface ReportChart {
|
||||||
type: "bar" | "donut" | "radar";
|
type: "bar" | "donut" | "radar" | "line";
|
||||||
title: string;
|
title: string;
|
||||||
items: ReportChartItem[];
|
items: ReportChartItem[];
|
||||||
}
|
}
|
||||||
@@ -240,10 +240,9 @@ export function coerceReportData(raw: unknown): ReportData | null {
|
|||||||
const co = (c ?? {}) as Record<string, unknown>;
|
const co = (c ?? {}) as Record<string, unknown>;
|
||||||
const type = str(co.type);
|
const type = str(co.type);
|
||||||
return {
|
return {
|
||||||
type: (type === "bar" || type === "donut" || type === "radar" ? type : "bar") as
|
type: (type === "bar" || type === "donut" || type === "radar" || type === "line"
|
||||||
| "bar"
|
? type
|
||||||
| "donut"
|
: "bar") as "bar" | "donut" | "radar" | "line",
|
||||||
| "radar",
|
|
||||||
title: str(co.title),
|
title: str(co.title),
|
||||||
items: arr(co.items)
|
items: arr(co.items)
|
||||||
.map((it) => {
|
.map((it) => {
|
||||||
@@ -297,7 +296,7 @@ export function buildDashboardPrompt(opts: {
|
|||||||
'{"title":"大屏主标题(≤20字)","subtitle":"一句话定位",',
|
'{"title":"大屏主标题(≤20字)","subtitle":"一句话定位",',
|
||||||
' "kpis":[{"label":"指标名","value":"数值","unit":"单位","trend":"up|down|flat"}], // 3-6 个核心指标,从研讨内容提取或合理估算',
|
' "kpis":[{"label":"指标名","value":"数值","unit":"单位","trend":"up|down|flat"}], // 3-6 个核心指标,从研讨内容提取或合理估算',
|
||||||
' "consensus":{"headline":"一句话最终结论","points":["结论要点,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 阶段',
|
' "timeline":[{"label":"阶段名","desc":"一句话说明"}], // 执行路线 3-6 阶段',
|
||||||
' "seats":[{"id":"seat-1","name":"席位名(与交付席位一致)","summary":"该席位结论一两句",',
|
' "seats":[{"id":"seat-1","name":"席位名(与交付席位一致)","summary":"该席位结论一两句",',
|
||||||
' "sections":[{"heading":"小节标题","bullets":["要点,每条≤40字"]}], // 2-4 节,提炼该席位交付',
|
' "sections":[{"heading":"小节标题","bullets":["要点,每条≤40字"]}], // 2-4 节,提炼该席位交付',
|
||||||
@@ -364,27 +363,29 @@ const PALETTES: Record<DashboardTheme, Palette> = {
|
|||||||
risk: { high: "#F5222D", mid: "#FAAD14", low: "#52C41A" },
|
risk: { high: "#F5222D", mid: "#FAAD14", low: "#52C41A" },
|
||||||
modalMask: "rgba(6,14,36,.78)",
|
modalMask: "rgba(6,14,36,.78)",
|
||||||
},
|
},
|
||||||
|
// 蓝图/工程图风(参考大屏设计图):白底 + slate 网格、玫瑰红(rose-600)强调、
|
||||||
|
// slate 中性文字、卡片四角红色角标、席位色序(scheme红/ux靛/ad绿/risk琥珀/summary天蓝)。
|
||||||
light: {
|
light: {
|
||||||
bgPage: "#FAFAFA",
|
bgPage: "#F8FAFC",
|
||||||
bgGradient:
|
bgGradient:
|
||||||
"radial-gradient(1200px 600px at 70% -10%,rgba(245,34,45,.05),transparent),linear-gradient(160deg,#FFFFFF,#FAFAFA)",
|
"radial-gradient(1100px 520px at 78% -12%,rgba(225,29,72,.05),transparent),linear-gradient(180deg,#FFFFFF,#F8FAFC)",
|
||||||
gridLine: "rgba(245,34,45,.035)",
|
gridLine: "rgba(148,163,184,.16)",
|
||||||
card: "#FFFFFF",
|
card: "#FFFFFF",
|
||||||
cardBorder: "#E8E8E8",
|
cardBorder: "rgba(226,232,240,.9)",
|
||||||
cardGlow: "0 2px 8px rgba(0,0,0,.04)",
|
cardGlow: "0 3px 10px rgba(15,23,42,.04)",
|
||||||
bracket: "#F5222D",
|
bracket: "#E11D48",
|
||||||
text: "#222222",
|
text: "#475569",
|
||||||
textSecondary: "#575757",
|
textSecondary: "#64748B",
|
||||||
textStrong: "#222222",
|
textStrong: "#1E293B",
|
||||||
heading: "#A8071A",
|
heading: "#1E293B",
|
||||||
primary: "#F5222D",
|
primary: "#E11D48",
|
||||||
link: "#0079FE",
|
link: "#E11D48",
|
||||||
titleColor: "linear-gradient(90deg,#F5222D,#CF1322 60%,#A8071A)",
|
titleColor: "linear-gradient(90deg,#1E293B,#334155)",
|
||||||
navBg: "rgba(255,255,255,.92)",
|
navBg: "rgba(255,255,255,.95)",
|
||||||
trackBg: "#F0F0F0",
|
trackBg: "#F1F5F9",
|
||||||
accents: ["#F5222D", "#0079FE", "#189D72", "#FF9900", "#45C3F3", "#7F7F7F"],
|
accents: ["#E11D48", "#4F46E5", "#059669", "#D97706", "#0284C7", "#7C3AED"],
|
||||||
risk: { high: "#F5222D", mid: "#FF9900", low: "#189D72" },
|
risk: { high: "#DC2626", mid: "#D97706", low: "#059669" },
|
||||||
modalMask: "rgba(34,34,34,.45)",
|
modalMask: "rgba(15,23,42,.42)",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -427,24 +428,59 @@ function svgBar(chart: ReportChart): string {
|
|||||||
function svgDonut(chart: ReportChart): string {
|
function svgDonut(chart: ReportChart): string {
|
||||||
const items = chart.items.slice(0, 6);
|
const items = chart.items.slice(0, 6);
|
||||||
const total = items.reduce((s, i) => s + Math.max(0, i.value), 0) || 1;
|
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;
|
const C = 2 * Math.PI * R;
|
||||||
let offset = 0;
|
let acc = 0;
|
||||||
const segs = items
|
const segs = items
|
||||||
.map((it, i) => {
|
.map((it, i) => {
|
||||||
const frac = Math.max(0, it.value) / total;
|
const len = (Math.max(0, it.value) / total) * C;
|
||||||
const seg = `<circle cx="80" cy="80" r="${R}" fill="none" stroke="${P.accents[i % P.accents.length]}" stroke-width="20" stroke-dasharray="${(frac * C).toFixed(2)} ${C.toFixed(2)}" stroke-dashoffset="${(-offset * C).toFixed(2)}" transform="rotate(-90 80 80)" opacity="0.9"/>`;
|
const seg = `<circle cx="70" cy="70" r="${R}" fill="none" stroke="${P.accents[i % P.accents.length]}" stroke-width="${SW}" stroke-dasharray="${len.toFixed(2)} ${(C - len).toFixed(2)}" stroke-dashoffset="${(-acc).toFixed(2)}" transform="rotate(-90 70 70)"/>`;
|
||||||
offset += frac;
|
acc += len;
|
||||||
return seg;
|
return seg;
|
||||||
})
|
})
|
||||||
.join("");
|
.join("");
|
||||||
|
// 中心:总分类数 + 标签(对齐设计图环形图的镂空中心)。
|
||||||
|
const center = `<text x="70" y="68" text-anchor="middle" class="dc-num">${items.length}</text><text x="70" y="82" text-anchor="middle" class="dc-cap">类</text>`;
|
||||||
const legend = items
|
const legend = items
|
||||||
.map(
|
.map(
|
||||||
(it, i) =>
|
(it, i) =>
|
||||||
`<div class="lg"><i style="background:${P.accents[i % P.accents.length]}"></i>${esc(it.label)} <b>${esc(String(it.value))}</b></div>`,
|
`<div class="lg"><i style="background:${P.accents[i % P.accents.length]}"></i><span>${esc(it.label)}</span><b>${esc(String(it.value))}</b></div>`,
|
||||||
)
|
)
|
||||||
.join("");
|
.join("");
|
||||||
return `<div class="donut"><svg viewBox="0 0 160 160" role="img">${segs}</svg><div class="lgs">${legend}</div></div>`;
|
return `<div class="donut"><svg viewBox="0 0 140 140" role="img"><circle cx="70" cy="70" r="${R}" fill="none" stroke="var(--track)" stroke-width="${SW}"/>${segs}${center}</svg><div class="lgs">${legend}</div></div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 折线图(趋势):多取前 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 `<line x1="${padL}" y1="${gy}" x2="${W - padR}" y2="${gy}" class="gr"/>`;
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
|
const area = `<polygon points="${padL},${(H - padB).toFixed(1)} ${pts} ${(W - padR).toFixed(1)},${(H - padB).toFixed(1)}" fill="${P.primary}14"/>`;
|
||||||
|
const dots = items
|
||||||
|
.map((it, i) => `<circle cx="${x(i).toFixed(1)}" cy="${y(it.value).toFixed(1)}" r="2.6" fill="${P.primary}"/>`)
|
||||||
|
.join("");
|
||||||
|
const labels = items
|
||||||
|
.map((it, i) => `<text x="${x(i).toFixed(1)}" y="${H - 8}" text-anchor="middle" class="cl">${esc(it.label)}</text>`)
|
||||||
|
.join("");
|
||||||
|
return `<svg viewBox="0 0 ${W} ${H}" role="img">${grid}${area}<polyline points="${pts}" fill="none" stroke="${P.primary}" stroke-width="2.2" stroke-linejoin="round" stroke-linecap="round"/>${dots}${labels}</svg>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function svgRadar(chart: ReportChart): string {
|
function svgRadar(chart: ReportChart): string {
|
||||||
@@ -480,7 +516,13 @@ function svgRadar(chart: ReportChart): string {
|
|||||||
|
|
||||||
function chartHtml(chart: ReportChart): string {
|
function chartHtml(chart: ReportChart): string {
|
||||||
const body =
|
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 `<div class="card chart"><h3>${esc(chart.title || "图表")}</h3>${body}</div>`;
|
return `<div class="card chart"><h3>${esc(chart.title || "图表")}</h3>${body}</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -501,7 +543,7 @@ function metricBars(metrics: ReportSeatMetric[]): string {
|
|||||||
return `<div class="metrics">${metrics
|
return `<div class="metrics">${metrics
|
||||||
.map(
|
.map(
|
||||||
(m, i) =>
|
(m, i) =>
|
||||||
`<div class="metric"><span>${esc(m.label)}</span><div class="track"><div class="fill" style="width:${m.value}%;background:${P.accents[i % P.accents.length]}"></div></div><b>${m.value}</b></div>`,
|
`<div class="metric"><div class="mrow"><span>${esc(m.label)}</span><b>${m.value}</b></div><div class="track"><div class="fill" style="width:${m.value}%;background:${P.accents[i % P.accents.length]}"></div></div></div>`,
|
||||||
)
|
)
|
||||||
.join("")}</div>`;
|
.join("")}</div>`;
|
||||||
}
|
}
|
||||||
@@ -533,20 +575,23 @@ export function renderDashboardHtml(
|
|||||||
`<a class="tab" href="#overview">总览</a>` +
|
`<a class="tab" href="#overview">总览</a>` +
|
||||||
seats.map((s) => `<a class="tab" href="#${esc(s.id)}">${esc(s.name)}</a>`).join("");
|
seats.map((s) => `<a class="tab" href="#${esc(s.id)}">${esc(s.name)}</a>`).join("");
|
||||||
|
|
||||||
|
const KPI_ICONS = ["📊", "📈", "⚠️", "🧩", "🏆", "📅"];
|
||||||
const kpis = data.kpis
|
const kpis = data.kpis
|
||||||
.map(
|
.map((k, i) => {
|
||||||
(k, i) =>
|
const ac = P.accents[i % P.accents.length];
|
||||||
`<div class="card kpi"><span class="klabel">${esc(k.label)}</span><div class="kval" style="color:${P.accents[i % P.accents.length]}">${esc(k.value)}<small>${esc(k.unit ?? "")}</small>${
|
return `<div class="card kpi">
|
||||||
|
<div class="khead"><span class="klabel">${esc(k.label)}</span><span class="kico" style="background:${ac}1a;color:${ac}">${KPI_ICONS[i % KPI_ICONS.length]}</span></div>
|
||||||
|
<div class="kval">${esc(k.value)}<small>${esc(k.unit ?? "")}</small>${
|
||||||
k.trend === "up" ? '<i class="tr up">▲</i>' : k.trend === "down" ? '<i class="tr dn">▼</i>' : ""
|
k.trend === "up" ? '<i class="tr up">▲</i>' : k.trend === "down" ? '<i class="tr dn">▼</i>' : ""
|
||||||
}</div></div>`,
|
}</div></div>`;
|
||||||
)
|
})
|
||||||
.join("");
|
.join("");
|
||||||
|
|
||||||
const seatCards = seats
|
const seatCards = seats
|
||||||
.map((s, i) => {
|
.map((s, i) => {
|
||||||
const color = P.accents[i % P.accents.length];
|
const color = P.accents[i % P.accents.length];
|
||||||
return `<a class="card seat" href="#${esc(s.id)}" style="--ac:${color}">
|
return `<a class="card seat" href="#${esc(s.id)}" style="--ac:${color}">
|
||||||
<div class="shead"><i></i><h3>${esc(s.name)}</h3><span class="go">进入席位页 →</span></div>
|
<div class="shead"><i></i><h3>${esc(s.name)}</h3><span class="go">席位页 →</span></div>
|
||||||
<p>${esc(s.summary || "(无摘要)")}</p>
|
<p>${esc(s.summary || "(无摘要)")}</p>
|
||||||
${metricBars(s.metrics.slice(0, 2))}
|
${metricBars(s.metrics.slice(0, 2))}
|
||||||
${riskChips(s.risks.slice(0, 2))}
|
${riskChips(s.risks.slice(0, 2))}
|
||||||
@@ -562,17 +607,20 @@ export function renderDashboardHtml(
|
|||||||
|
|
||||||
const consensusPoints = data.consensus.points.map((p) => `<li>${esc(p)}</li>`).join("");
|
const consensusPoints = data.consensus.points.map((p) => `<li>${esc(p)}</li>`).join("");
|
||||||
|
|
||||||
|
const chartsBlock = data.charts.length
|
||||||
|
? `<div class="charts">${data.charts.map(chartHtml).join("")}</div>`
|
||||||
|
: "";
|
||||||
const overview = `<section class="page" id="overview">
|
const overview = `<section class="page" id="overview">
|
||||||
<div class="kpis">${kpis}</div>
|
<div class="kpis">${kpis}</div>
|
||||||
<div class="grid2">
|
<div class="ovgrid">
|
||||||
<div class="card consensus"><h3>总控最终共识</h3>
|
<div class="card consensus"><h3>总控最终共识</h3>
|
||||||
<p class="headline">${esc(data.consensus.headline || "(未提供)")}</p>
|
<p class="headline">${esc(data.consensus.headline || "(未提供)")}</p>
|
||||||
<ul>${consensusPoints}</ul>
|
<ul>${consensusPoints}</ul>
|
||||||
<a class="more" href="#consensus-modal">查看详情</a>
|
<a class="more" href="#consensus-modal">查看研讨详情 →</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="charts">${data.charts.map(chartHtml).join("")}</div>
|
${chartsBlock}
|
||||||
</div>
|
</div>
|
||||||
<h2 class="sec">席位交付总览 <small>点击卡片进入席位专页</small></h2>
|
<h2 class="sec">席位交付总览 <small>点击卡片进入对应席位专页</small></h2>
|
||||||
<div class="seats">${seatCards}</div>
|
<div class="seats">${seatCards}</div>
|
||||||
${timeline}
|
${timeline}
|
||||||
</section>`;
|
</section>`;
|
||||||
@@ -620,95 +668,125 @@ export function renderDashboardHtml(
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>${esc(data.title)}</title>
|
<title>${esc(data.title)}</title>
|
||||||
<style>
|
<style>
|
||||||
/* 主题色板(light=浅红色 UI 规范 / dark=深蓝色 UI 规范,渲染时注入) */
|
/* 蓝图工程图风模板(light=参考大屏设计图 / dark=深蓝 token,渲染时注入 palette) */
|
||||||
:root{--line:${P.cardBorder};--txt:${P.text};--dim:${P.textSecondary};--strong:${P.textStrong};
|
:root{--line:${P.cardBorder};--txt:${P.text};--dim:${P.textSecondary};--strong:${P.textStrong};
|
||||||
--primary:${P.primary};--link:${P.link};--heading:${P.heading};--track:${P.trackBg}}
|
--primary:${P.primary};--link:${P.link};--heading:${P.heading};--track:${P.trackBg};--bracket:${P.bracket}}
|
||||||
*{box-sizing:border-box;margin:0;padding:0}
|
*{box-sizing:border-box;margin:0;padding:0}
|
||||||
body{font-family:"Noto Sans S Chinese","Microsoft YaHei",system-ui,sans-serif;color:var(--txt);min-height:100vh;
|
body{font-family:"Inter","Noto Sans SC","Microsoft YaHei",system-ui,sans-serif;color:var(--txt);min-height:100vh;
|
||||||
background:${P.bgGradient};background-color:${P.bgPage};background-attachment:fixed}
|
font-size:13px;background:${P.bgGradient};background-color:${P.bgPage};background-attachment:fixed}
|
||||||
body::before{content:"";position:fixed;inset:0;pointer-events:none;opacity:.6;
|
body::before{content:"";position:fixed;inset:0;pointer-events:none;
|
||||||
background:linear-gradient(${P.gridLine} 1px,transparent 1px),linear-gradient(90deg,${P.gridLine} 1px,transparent 1px);
|
background:linear-gradient(${P.gridLine} 1px,transparent 1px),linear-gradient(90deg,${P.gridLine} 1px,transparent 1px);
|
||||||
background-size:42px 42px}
|
background-size:22px 22px}
|
||||||
.top{position:sticky;top:0;z-index:5;display:flex;flex-wrap:wrap;align-items:center;gap:12px;
|
.top{position:sticky;top:0;z-index:5;display:flex;flex-wrap:wrap;align-items:center;gap:12px;
|
||||||
padding:14px 26px;background:${P.navBg};backdrop-filter:blur(6px);border-bottom:1px solid var(--line)}
|
padding:13px 26px;background:${P.navBg};backdrop-filter:blur(8px);border-bottom:1px solid var(--line)}
|
||||||
.top h1{font-size:21px;letter-spacing:2px;background:${P.titleColor};
|
.top h1{font-size:18px;font-weight:800;letter-spacing:.5px;background:${P.titleColor};
|
||||||
-webkit-background-clip:text;background-clip:text;color:transparent;white-space:nowrap}
|
-webkit-background-clip:text;background-clip:text;color:transparent;white-space:nowrap}
|
||||||
.top .sub{font-size:12px;color:var(--dim)}
|
.top .sub{font-size:11.5px;color:var(--dim);padding:2px 8px;border:1px solid var(--line);border-radius:4px}
|
||||||
.top .meta{margin-left:auto;font-size:11px;color:var(--dim)}
|
.top .meta{margin-left:auto;font-size:10.5px;color:var(--dim);font-family:"JetBrains Mono",Consolas,monospace;letter-spacing:.5px}
|
||||||
nav{display:flex;flex-wrap:wrap;gap:8px;padding:10px 26px;border-bottom:1px solid var(--line)}
|
nav{position:sticky;top:51px;z-index:4;display:flex;flex-wrap:wrap;gap:7px;padding:9px 26px;
|
||||||
.tab{font-size:12px;padding:5px 14px;border:1px solid var(--line);border-radius:2px;color:var(--dim);text-decoration:none;transition:.15s}
|
background:${P.navBg};backdrop-filter:blur(8px);border-bottom:1px solid var(--line)}
|
||||||
.tab:hover{color:${P.primary};border-color:${P.primary};box-shadow:0 0 10px ${P.primary}40}
|
.tab{font-size:12px;font-weight:600;padding:5px 13px;border:1px solid var(--line);border-radius:4px;color:var(--dim);text-decoration:none;transition:.15s;background:var(--track)}
|
||||||
main{padding:20px 26px 40px;max-width:1480px;margin:0 auto}
|
.tab:hover{color:#fff;border-color:var(--primary);background:var(--primary)}
|
||||||
|
main{padding:22px 26px 48px;max-width:1500px;margin:0 auto}
|
||||||
.page{display:none;animation:fi .25s ease}
|
.page{display:none;animation:fi .25s ease}
|
||||||
#overview{display:block}
|
#overview{display:block}
|
||||||
.page:target{display:block}
|
.page:target{display:block}
|
||||||
body:has(.page:target) #overview:not(:target){display:none}
|
body:has(.page:target) #overview:not(:target){display:none}
|
||||||
@keyframes fi{from{opacity:0;transform:translateY(6px)}to{opacity:1}}
|
@keyframes fi{from{opacity:0;transform:translateY(8px)}to{opacity:1}}
|
||||||
/* 卡片:规范圆角 4px;浅色实底白卡 + 轻阴影,深色深蓝渐变 + 内发光 */
|
/* 蓝图卡片:白底 + 四角玫瑰红角标 + 极淡网格 overlay */
|
||||||
.card{position:relative;border:1px solid var(--line);border-radius:4px;padding:16px 18px;
|
.card{position:relative;border:1px solid var(--line);border-radius:6px;padding:17px 19px;
|
||||||
background:${P.card};box-shadow:${P.cardGlow}}
|
background:${P.card};box-shadow:${P.cardGlow};transition:box-shadow .3s,transform .3s,border-color .3s}
|
||||||
.card::before,.card::after{content:"";position:absolute;width:14px;height:14px;border-color:${P.bracket};border-style:solid;opacity:.55}
|
.card::before,.card::after{content:"";position:absolute;width:12px;height:12px;border-color:var(--bracket);border-style:solid;z-index:2}
|
||||||
.card::before{top:-1px;left:-1px;border-width:2px 0 0 2px;border-radius:4px 0 0 0}
|
.card::before{top:-1px;left:-1px;border-width:2px 0 0 2px}
|
||||||
.card::after{bottom:-1px;right:-1px;border-width:0 2px 2px 0;border-radius:0 0 4px 0}
|
.card::after{bottom:-1px;right:-1px;border-width:0 2px 2px 0}
|
||||||
.card h3{font-size:13px;letter-spacing:1.5px;color:var(--heading);margin-bottom:10px}
|
.card>*{position:relative;z-index:1}
|
||||||
.kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:14px;margin-bottom:18px}
|
.card h3{font-size:13.5px;font-weight:700;letter-spacing:.3px;color:var(--strong);margin-bottom:12px;
|
||||||
.kpi .klabel{font-size:11px;color:var(--dim);letter-spacing:1px}
|
display:flex;align-items:center;gap:7px;padding-bottom:11px;border-bottom:1px solid var(--line)}
|
||||||
.kpi .kval{font-size:30px;font-weight:800;margin-top:4px;font-family:"D-DIN",Consolas,monospace}
|
.card h3::before{content:"";width:9px;height:9px;border-radius:2px;background:var(--primary);flex:none}
|
||||||
.kpi small{font-size:12px;margin-left:4px;color:var(--dim)}
|
/* KPI */
|
||||||
.kpi .tr{font-style:normal;font-size:12px;margin-left:6px}.tr.up{color:${P.risk.low}}.tr.dn{color:${P.risk.high}}
|
.kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(168px,1fr));gap:13px;margin-bottom:18px}
|
||||||
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-bottom:18px}
|
.kpi{padding:15px 17px;min-height:104px;display:flex;flex-direction:column;justify-content:space-between}
|
||||||
@media(max-width:980px){.grid2{grid-template-columns:1fr}}
|
.kpi .khead{display:flex;justify-content:space-between;align-items:flex-start;gap:6px}
|
||||||
.consensus .headline{font-size:15px;font-weight:700;color:var(--strong);margin-bottom:8px}
|
.kpi .klabel{font-size:11.5px;color:var(--dim);font-weight:500;letter-spacing:.3px}
|
||||||
.consensus ul{padding-left:18px;display:grid;gap:5px;font-size:12.5px;color:var(--txt)}
|
.kpi .kico{width:30px;height:30px;border-radius:6px;display:flex;align-items:center;justify-content:center;font-size:15px;flex:none}
|
||||||
.consensus .more{display:inline-block;margin-top:10px;font-size:11px;color:${P.link};text-decoration:none;border-bottom:1px dashed ${P.link}}
|
.kpi .kval{font-size:30px;font-weight:800;letter-spacing:-.5px;color:var(--strong);font-family:"JetBrains Mono",Consolas,monospace;line-height:1;display:flex;align-items:baseline;gap:3px;margin-top:8px}
|
||||||
.charts{display:grid;gap:14px}
|
.kpi small{font-size:12px;font-weight:500;color:var(--dim)}
|
||||||
.chart svg{width:100%;height:auto}
|
.kpi .tr{font-size:11px;font-weight:700;padding:1px 5px;border-radius:3px;margin-left:4px}
|
||||||
.cl{fill:var(--dim);font-size:11px}.cv{fill:var(--txt);font-size:11px;font-weight:700}
|
.tr.up{color:${P.risk.low};background:${P.risk.low}1a}.tr.dn{color:${P.risk.high};background:${P.risk.high}1a}
|
||||||
.ct{fill:var(--track)}.gr{fill:none;stroke:${P.primary}2e;stroke-width:1}
|
.ovgrid{display:grid;grid-template-columns:1.25fr 1fr;gap:13px;margin-bottom:20px;align-items:start}
|
||||||
.donut{display:flex;align-items:center;gap:14px}.donut svg{width:130px;flex:none}
|
@media(max-width:980px){.ovgrid{grid-template-columns:1fr}}
|
||||||
.lgs{display:grid;gap:5px}.lg{font-size:11.5px;color:var(--dim)}.lg b{color:var(--txt);margin-left:4px}
|
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:13px;margin-bottom:14px}
|
||||||
.lg i{display:inline-block;width:9px;height:9px;border-radius:2px;margin-right:6px}
|
@media(max-width:760px){.grid2{grid-template-columns:1fr}}
|
||||||
.sec{font-size:14px;letter-spacing:2px;color:var(--heading);margin:6px 0 12px}
|
/* 共识:玫瑰红竖条摘要 + 要点列表 */
|
||||||
.sec small{font-size:11px;color:var(--dim);font-weight:400;margin-left:8px}
|
.consensus .headline{font-size:13.5px;font-weight:600;color:var(--strong);line-height:1.7;margin-bottom:13px;
|
||||||
.seats{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:14px;margin-bottom:18px}
|
background:${P.primary}0d;padding:12px 14px;border-radius:4px;border-left:3px solid var(--primary)}
|
||||||
.seat{display:block;text-decoration:none;color:inherit;transition:.15s}
|
.consensus ul{list-style:none;display:grid;gap:11px}
|
||||||
.seat:hover{border-color:var(--ac);box-shadow:0 0 16px ${P.primary}30;transform:translateY(-2px)}
|
.consensus li{display:flex;gap:9px;font-size:12px;line-height:1.65;color:var(--txt)}
|
||||||
.shead{display:flex;align-items:center;gap:8px;margin-bottom:8px}
|
.consensus li::before{content:"";width:6px;height:6px;border-radius:50%;background:var(--primary);margin-top:6px;flex:none}
|
||||||
.shead i{width:8px;height:8px;border-radius:50%;background:var(--ac);box-shadow:0 0 8px var(--ac)}
|
.consensus .more{display:inline-block;margin-top:13px;font-size:11.5px;font-weight:600;color:var(--primary);text-decoration:none}
|
||||||
.shead h3{margin:0;flex:1}
|
.consensus .more:hover{text-decoration:underline}
|
||||||
.go{font-size:10.5px;color:var(--dim)}
|
.charts{display:grid;gap:13px}
|
||||||
|
.chart svg{width:100%;height:auto;display:block}
|
||||||
|
.cl{fill:var(--dim);font-size:9.5px}.cv{fill:var(--strong);font-size:11px;font-weight:700;font-family:monospace}
|
||||||
|
.ct{fill:var(--track)}.gr{fill:none;stroke:${P.gridLine.replace(/[\d.]+\)$/, "0.6)")};stroke:var(--line);stroke-width:1}
|
||||||
|
.dc-num{fill:var(--strong);font-size:26px;font-weight:800;font-family:monospace}
|
||||||
|
.dc-cap{fill:var(--dim);font-size:9px}
|
||||||
|
.donut{display:flex;align-items:center;gap:14px;flex-wrap:wrap;justify-content:center}.donut svg{width:128px;flex:none}
|
||||||
|
.lgs{display:grid;gap:7px;min-width:130px}.lg{display:flex;align-items:center;gap:7px;font-size:11.5px;color:var(--txt)}
|
||||||
|
.lg span{flex:1}.lg b{color:var(--strong);font-family:monospace;font-weight:700}
|
||||||
|
.lg i{display:inline-block;width:9px;height:9px;border-radius:2px;flex:none}
|
||||||
|
.sec{font-size:15px;font-weight:700;color:var(--strong);margin:8px 0 13px;display:flex;align-items:center;gap:8px}
|
||||||
|
.sec small{font-size:11.5px;color:var(--dim);font-weight:400}
|
||||||
|
.seats{display:grid;grid-template-columns:repeat(auto-fill,minmax(270px,1fr));gap:13px;margin-bottom:20px}
|
||||||
|
.seat{display:flex;flex-direction:column;text-decoration:none;color:inherit}
|
||||||
|
.seat:hover{border-color:var(--ac);box-shadow:0 8px 22px rgba(15,23,42,.09);transform:translateY(-2px)}
|
||||||
|
.shead{display:flex;align-items:center;gap:7px;padding-bottom:10px;border-bottom:1px solid var(--line);margin-bottom:0}
|
||||||
|
.shead::before{display:none}
|
||||||
|
.shead i{width:9px;height:9px;border-radius:50%;background:var(--ac);flex:none}
|
||||||
|
.shead h3{margin:0;padding:0;border:0;font-size:12.5px;flex:1}
|
||||||
|
.shead h3::before{display:none}
|
||||||
|
.go{font-size:10px;color:var(--dim);display:flex;align-items:center;gap:2px}
|
||||||
.seat:hover .go{color:var(--ac)}
|
.seat:hover .go{color:var(--ac)}
|
||||||
.seat p{font-size:12px;color:var(--dim);line-height:1.6;margin-bottom:8px}
|
.seat .anly{font-size:10px;color:var(--dim);font-family:monospace;text-transform:uppercase;letter-spacing:.5px;margin:11px 0 6px}
|
||||||
.metrics{display:grid;gap:7px}
|
.seat p{font-size:11.5px;color:var(--dim);line-height:1.65;margin-bottom:12px;flex:1;
|
||||||
.metric{display:flex;align-items:center;gap:8px;font-size:11px;color:var(--dim)}
|
display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}
|
||||||
.metric span{width:72px;flex:none}.metric b{width:26px;text-align:right;color:var(--txt)}
|
.metrics{display:grid;gap:9px}
|
||||||
.track{flex:1;height:7px;border-radius:4px;background:var(--track);overflow:hidden}
|
.metric span{font-size:10.5px;color:var(--dim);font-weight:500}
|
||||||
.fill{height:100%;border-radius:4px}
|
.metric .mrow{display:flex;justify-content:space-between;margin-bottom:4px}
|
||||||
.chips{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}
|
.metric b{font-size:11px;color:var(--strong);font-family:monospace;font-weight:700}
|
||||||
.chip{font-size:10.5px;padding:2px 9px;border:1px solid;border-radius:2px}
|
.track{height:6px;border-radius:99px;background:var(--track);overflow:hidden}
|
||||||
.tl ol{list-style:none;display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;counter-reset:tl}
|
.fill{height:100%;border-radius:99px}
|
||||||
.tl li{position:relative;padding:10px 12px 10px 14px;border-left:2px solid ${P.primary};background:${P.primary}10;border-radius:0 4px 4px 0}
|
.chips{display:flex;flex-direction:column;gap:6px;margin-top:12px;padding-top:11px;border-top:1px solid var(--line)}
|
||||||
.tl b{display:block;font-size:12.5px;color:var(--heading);margin-bottom:3px}
|
.chip{font-size:10px;padding:3px 8px;border-radius:4px;border-left:2px solid;display:flex;align-items:center;gap:5px}
|
||||||
.tl span{font-size:11px;color:var(--dim);line-height:1.5}
|
.chip::before{content:"⚠";font-size:9px}
|
||||||
.pbar{display:flex;justify-content:space-between;align-items:center;margin-bottom:14px;font-size:12px}
|
.tl{margin-top:2px}
|
||||||
.pbar a{color:${P.link};text-decoration:none}.pnav{display:flex;gap:14px}.pnav a{color:var(--dim)}.pnav a:hover{color:${P.link}}
|
.tl ol{list-style:none;display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:11px}
|
||||||
.phead{display:flex;align-items:center;gap:12px;margin-bottom:12px}
|
.tl li{position:relative;padding:12px 14px;border:1px solid var(--line);border-left:3px solid var(--primary);background:var(--track);border-radius:0 5px 5px 0}
|
||||||
.phead i{width:10px;height:34px;border-radius:3px;background:var(--ac);box-shadow:0 0 10px var(--ac)}
|
.tl b{display:block;font-size:12.5px;font-weight:700;color:var(--strong);margin-bottom:5px}
|
||||||
.phead h2{font-size:19px;letter-spacing:2px;color:var(--strong)}
|
.tl span{font-size:11px;color:var(--dim);line-height:1.6}
|
||||||
.phead p{font-size:12px;color:var(--dim);margin-top:3px}
|
/* 席位专页 */
|
||||||
.deps{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:14px}
|
.pbar{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;font-size:12px}
|
||||||
.dep{font-size:11px;padding:3px 11px;border:1px dashed ${P.link};border-radius:2px;color:${P.link};text-decoration:none}
|
.pbar a{color:var(--primary);text-decoration:none;font-weight:600}.pnav{display:flex;gap:16px}.pnav a{color:var(--dim);font-weight:500}.pnav a:hover{color:var(--primary)}
|
||||||
.dep.sup{border-color:${P.risk.low};color:${P.risk.low}}
|
.phead{display:flex;align-items:center;gap:13px;margin-bottom:14px;padding-bottom:14px;border-bottom:1px solid var(--line)}
|
||||||
.dep:hover{background:${P.link}1a}
|
.phead i{width:6px;height:38px;border-radius:3px;background:var(--ac);flex:none}
|
||||||
.psections{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:14px;margin-bottom:14px}
|
.phead h2{font-size:20px;font-weight:800;color:var(--strong)}
|
||||||
.sect ul{padding-left:16px;display:grid;gap:6px;font-size:12.5px;line-height:1.6}
|
.phead p{font-size:12px;color:var(--dim);margin-top:4px;line-height:1.6}
|
||||||
|
.deps{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:16px}
|
||||||
|
.dep{font-size:11px;font-weight:600;padding:4px 11px;border:1px dashed var(--primary);border-radius:4px;color:var(--primary);text-decoration:none}
|
||||||
|
.dep.sup{border-color:${P.accents[4]};color:${P.accents[4]}}
|
||||||
|
.dep:hover{background:var(--primary)12}
|
||||||
|
.psections{display:grid;grid-template-columns:repeat(auto-fit,minmax(290px,1fr));gap:13px;margin-bottom:14px}
|
||||||
|
.sect ul{list-style:none;display:grid;gap:8px;font-size:12px;line-height:1.65}
|
||||||
|
.sect li{display:flex;gap:8px;color:var(--txt)}
|
||||||
|
.sect li::before{content:"";width:5px;height:5px;border-radius:50%;background:var(--primary);margin-top:7px;flex:none}
|
||||||
.sect .empty{font-size:12px;color:var(--dim)}
|
.sect .empty{font-size:12px;color:var(--dim)}
|
||||||
.modal{display:none;position:fixed;inset:0;z-index:20;background:${P.modalMask};padding:6vh 16px}
|
.modal{display:none;position:fixed;inset:0;z-index:20;background:${P.modalMask};padding:6vh 16px}
|
||||||
.modal:target{display:flex;justify-content:center;align-items:flex-start}
|
.modal:target{display:flex;justify-content:center;align-items:flex-start}
|
||||||
.mbody{max-width:680px;width:100%;max-height:84vh;overflow:auto}
|
.mbody{max-width:700px;width:100%;max-height:84vh;overflow:auto}
|
||||||
.mbody ul{padding-left:18px;display:grid;gap:6px;font-size:13px}
|
.mbody ul{list-style:none;display:grid;gap:9px;font-size:13px;margin-top:6px}
|
||||||
.mclose{position:absolute;top:8px;right:14px;font-size:20px;color:var(--dim);text-decoration:none}
|
.mbody li{display:flex;gap:9px;line-height:1.65}.mbody li::before{content:"";width:6px;height:6px;border-radius:50%;background:var(--primary);margin-top:7px;flex:none}
|
||||||
.mclose:hover{color:${P.risk.high}}
|
.mclose{position:absolute;top:10px;right:16px;font-size:22px;color:var(--dim);text-decoration:none;line-height:1}
|
||||||
|
.mclose:hover{color:var(--primary)}
|
||||||
</style></head>
|
</style></head>
|
||||||
<body>
|
<body>
|
||||||
<header class="top"><h1>${esc(data.title)}</h1><span class="sub">${esc(data.subtitle)}</span><span class="meta">${generated}</span></header>
|
<header class="top"><h1>${esc(data.title)}</h1><span class="sub">${esc(data.subtitle)}</span><span class="meta">${generated}</span></header>
|
||||||
|
|||||||
Reference in new issue
Block a user