import { formatAxis, formatDay, formatNumber } from "./format.js"; const charts = new Map(); let resizeObserver = null; function colors() { const styles = getComputedStyle(document.documentElement); return { text: styles.getPropertyValue("--text").trim() || "#2a201a", muted: styles.getPropertyValue("--muted").trim() || "#7b6a58", grid: styles.getPropertyValue("--grid").trim() || "#eadfce", accent: styles.getPropertyValue("--accent").trim() || "#a9702f", blue: styles.getPropertyValue("--blue").trim() || "#5f7f96", success: styles.getPropertyValue("--success").trim() || "#5f7a4e", danger: styles.getPropertyValue("--danger").trim() || "#b4553f", surface: styles.getPropertyValue("--surface").trim() || "#fffdf9", }; } export function themeColor(name) { return colors()[name] || colors().accent; } function ensureEcharts() { if (!window.echarts) { throw new Error("ECharts 未加载,请检查 /static/vendor/echarts.min.js"); } return window.echarts; } function mount(element, option) { const echarts = ensureEcharts(); let chart = charts.get(element); if (!chart) { chart = echarts.init(element, null, { renderer: "canvas" }); charts.set(element, chart); } chart.setOption(option, true); if (!resizeObserver && typeof ResizeObserver !== "undefined") { resizeObserver = new ResizeObserver((entries) => { for (const entry of entries) { charts.get(entry.target)?.resize(); } }); } resizeObserver?.observe(element); return chart; } export function disposeAll() { for (const chart of charts.values()) { try { chart.dispose(); } catch { // 页面切换时忽略已销毁实例 } } charts.clear(); resizeObserver?.disconnect(); resizeObserver = null; } export function lineChart(element, { dates, values, name = "数值", color, scale = false }) { if (!element) return null; const theme = colors(); const lineColor = color || theme.accent; const option = { animationDuration: 300, backgroundColor: "transparent", grid: { left: 8, right: 12, top: 22, bottom: 8, containLabel: true }, tooltip: { trigger: "axis", backgroundColor: theme.surface, borderColor: theme.grid, textStyle: { color: theme.text, fontSize: 12 }, formatter(params) { const point = params[0]; if (!point) return ""; const value = point.value; return `${point.axisValue}
${name}:${ value === null || value === undefined ? "缺失" : formatNumber(value) }`; }, }, xAxis: { type: "category", data: dates, boundaryGap: false, axisLine: { lineStyle: { color: theme.grid } }, axisTick: { show: false }, axisLabel: { color: theme.muted, fontSize: 11, formatter: (value) => formatDay(value), hideOverlap: true, }, }, yAxis: { type: "value", scale, minInterval: 1, splitLine: { lineStyle: { color: theme.grid, type: "dashed" } }, axisLabel: { color: theme.muted, fontSize: 11, formatter: (value) => formatAxis(value), }, }, series: [{ name, type: "line", data: values, smooth: false, connectNulls: false, symbol: "circle", symbolSize: 5, showSymbol: values.length <= 45, lineStyle: { width: 2.2, color: lineColor }, itemStyle: { color: lineColor }, areaStyle: { color: { type: "linear", x: 0, y: 0, x2: 0, y2: 1, colorStops: [ { offset: 0, color: `${lineColor}55` }, { offset: 1, color: `${lineColor}00` }, ], }, }, }], }; return mount(element, option); } export function barChart(element, { labels, values, name = "数值", color }) { if (!element) return null; const theme = colors(); const barColor = color || theme.blue; const option = { animationDuration: 300, backgroundColor: "transparent", grid: { left: 8, right: 12, top: 22, bottom: 8, containLabel: true }, tooltip: { trigger: "axis", axisPointer: { type: "shadow" }, backgroundColor: theme.surface, borderColor: theme.grid, textStyle: { color: theme.text, fontSize: 12 }, formatter(params) { const point = params[0]; if (!point) return ""; return `${point.name}
${name}:${formatNumber(point.value)}`; }, }, xAxis: { type: "category", data: labels, axisLine: { lineStyle: { color: theme.grid } }, axisTick: { show: false }, axisLabel: { color: theme.muted, fontSize: 10, interval: 0, rotate: labels.length > 8 ? 32 : 0, formatter: (value) => String(value).slice(0, 10), }, }, yAxis: { type: "value", minInterval: 1, splitLine: { lineStyle: { color: theme.grid, type: "dashed" } }, axisLabel: { color: theme.muted, fontSize: 11, formatter: (value) => formatAxis(value), }, }, series: [{ name, type: "bar", data: values, barMaxWidth: 26, itemStyle: { color: barColor, borderRadius: [4, 4, 0, 0] }, }], }; return mount(element, option); } export function emptyChart(element, message = "暂无数据") { if (!element) return; element.innerHTML = `
${message}
`; }