import { formatAxis, formatCount, formatDay } from "./format.js"; const charts = new Map(); let resizeObserver = null; function colors() { const styles = getComputedStyle(document.documentElement); return { text: styles.getPropertyValue("--text").trim() || "#1c1f23", muted: styles.getPropertyValue("--muted").trim() || "#6b7280", grid: styles.getPropertyValue("--grid").trim() || "#e8ebf0", accent: "#fb7299", blue: "#00aeec", surface: styles.getPropertyValue("--surface").trim() || "#ffffff", }; } 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; } const palette = { accent: "#fb7299", blue: "#00aeec", purple: "#9b7bff", green: "#22c55e", }; export function lineChart(element, { dates, values, name = "数值", color = palette.accent, scale = false }) { if (!element) return null; const theme = colors(); 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 ? "缺失" : formatCount(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, 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 }, itemStyle: { color }, areaStyle: { color: { type: "linear", x: 0, y: 0, x2: 0, y2: 1, colorStops: [ { offset: 0, color: `${color}55` }, { offset: 1, color: `${color}00` }, ], }, }, }], }; return mount(element, option); } export function barChart(element, { labels, values, name = "数值", color = palette.blue }) { if (!element) return null; const theme = colors(); 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}:${formatCount(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", 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, borderRadius: [4, 4, 0, 0] }, }], }; return mount(element, option); } export function emptyChart(element, message = "暂无数据") { if (!element) return; element.innerHTML = `
${message}
`; }