import type { EChartsOption } from 'echarts'; import type { AdminLocale } from '../i18n/admin-messages'; import { formatAmount, formatAmountFull } from './format-amount'; export type ChartI18n = { locale?: AdminLocale; betCountSeries: string; axisAmount: string; axisCount: string; countSuffix: string; pieTooltip: string; noData: string; pieEmpty: string; }; const tooltipBase = { backgroundColor: '#141414', borderColor: '#2a2a2a', textStyle: { color: '#e0e0e0', fontSize: 12 }, }; const axisLabel = { color: '#888', fontSize: 11 }; const splitLine = { lineStyle: { color: '#252525' } }; export type ChartSeries = { name: string; color: string; values: number[] }; export type PieSegment = { label: string; value: number; color: string }; export function buildBarChartOption( labels: string[], series: ChartSeries[], opts?: { amountAxis?: boolean }, ): EChartsOption { const amountAxis = opts?.amountAxis !== false; return { backgroundColor: 'transparent', color: series.map((s) => s.color), tooltip: { ...tooltipBase, trigger: 'axis', axisPointer: { type: 'shadow' }, valueFormatter: (v) => amountAxis ? formatAmountFull(Number(v)) : fmtCount(Number(v)), }, legend: { bottom: 0, itemWidth: 10, itemHeight: 10, textStyle: { color: '#999', fontSize: 11 }, }, grid: { left: 52, right: 12, top: 20, bottom: 48 }, xAxis: { type: 'category', data: labels, axisLabel, axisLine: { lineStyle: { color: '#333' } }, axisTick: { show: false }, }, yAxis: { type: 'value', axisLabel: { ...axisLabel, formatter: (v: number) => (amountAxis ? formatAmount(v) : fmtCount(v)), }, splitLine, }, series: series.map((s) => ({ name: s.name, type: 'bar', data: s.values, itemStyle: { color: s.color, borderRadius: [4, 4, 0, 0] }, barMaxWidth: 22, emphasis: { focus: 'series' }, })), }; } export function buildMultiLineChartOption( labels: string[], series: ChartSeries[], ): EChartsOption { return { backgroundColor: 'transparent', color: series.map((s) => s.color), tooltip: { ...tooltipBase, trigger: 'axis', valueFormatter: (v) => formatAmountFull(Number(v)), }, legend: { bottom: 0, itemWidth: 10, itemHeight: 10, textStyle: { color: '#999', fontSize: 11 }, }, grid: { left: 52, right: 12, top: 20, bottom: 48 }, xAxis: { type: 'category', data: labels, boundaryGap: false, axisLabel, axisLine: { lineStyle: { color: '#333' } }, }, yAxis: { type: 'value', axisLabel: { ...axisLabel, formatter: (v: number) => formatAmount(v) }, splitLine, }, series: series.map((s) => ({ name: s.name, type: 'line', smooth: true, symbol: 'circle', symbolSize: 6, data: s.values, itemStyle: { color: s.color }, lineStyle: { color: s.color, width: 2 }, areaStyle: { color: s.color, opacity: 0.12 }, })), }; } export function buildPieChartOption( title: string, segments: PieSegment[], i18n?: Partial, ): EChartsOption { const loc = i18n?.locale; const noData = i18n?.noData ?? '暂无数据'; const data = segments.map((s) => ({ name: s.label, value: s.value, itemStyle: { color: s.color }, })); return { backgroundColor: 'transparent', tooltip: { ...tooltipBase, trigger: 'item', formatter: i18n?.pieTooltip ?? '{b}:{c}({d}%)', }, legend: { orient: 'vertical', right: 4, top: 'middle', itemWidth: 8, itemHeight: 8, textStyle: { color: '#999', fontSize: 11 }, }, series: [ { name: title, type: 'pie', radius: ['42%', '68%'], center: ['36%', '50%'], avoidLabelOverlap: true, itemStyle: { borderRadius: 4, borderColor: '#111', borderWidth: 2 }, label: { show: segments.length > 0, color: '#bbb', fontSize: 11, formatter: '{b}\n{d}%', }, labelLine: { lineStyle: { color: '#444' } }, emphasis: { label: { fontSize: 12, fontWeight: 'bold' }, scaleSize: 6, }, data: data.length ? data : [{ name: noData, value: 1, itemStyle: { color: '#333' } }], }, ], }; } function fmtCount(v: number, locale?: AdminLocale) { const tag = locale ?? 'zh-CN'; return v.toLocaleString(tag, { maximumFractionDigits: 0 }); } /** 7 日金额折线 + 注单柱(双 Y 轴),一图看清趋势 */ export function buildCombinedTrendOption( labels: string[], amountSeries: ChartSeries[], betCounts: number[], i18n: ChartI18n, ): EChartsOption { const loc = i18n.locale; const betCountName = i18n.betCountSeries; const countSuffix = i18n.countSuffix; return { backgroundColor: 'transparent', color: [...amountSeries.map((s) => s.color), '#fb923c'], tooltip: { ...tooltipBase, trigger: 'axis', formatter(params) { const items = Array.isArray(params) ? params : [params]; return items .map((p) => { const v = Number(p.value ?? 0); const isCount = p.seriesName === betCountName; const val = isCount ? `${fmtCount(v, loc)} ${countSuffix}` : formatAmountFull(v, loc); const sep = loc?.startsWith('zh') ? ':' : ': '; return `${p.marker ?? ''}${p.seriesName}${sep}${val}`; }) .join('
'); }, }, legend: { top: 0, itemWidth: 10, itemHeight: 10, textStyle: { color: '#999', fontSize: 11 }, }, grid: { left: 56, right: 48, top: 36, bottom: 28 }, xAxis: { type: 'category', data: labels, boundaryGap: true, axisLabel, axisLine: { lineStyle: { color: '#333' } }, }, yAxis: [ { type: 'value', name: i18n.axisAmount, nameTextStyle: { color: '#666', fontSize: 10 }, axisLabel: { ...axisLabel, formatter: (v: number) => formatAmount(v, 2, loc) }, splitLine, }, { type: 'value', name: i18n.axisCount, nameTextStyle: { color: '#666', fontSize: 10 }, axisLabel: { ...axisLabel, formatter: (v: number) => fmtCount(v, loc) }, splitLine: { show: false }, }, ], series: [ ...amountSeries.map((s) => ({ name: s.name, type: 'line' as const, yAxisIndex: 0, smooth: true, symbol: 'circle', symbolSize: 5, data: s.values, itemStyle: { color: s.color }, lineStyle: { color: s.color, width: 2 }, })), { name: betCountName, type: 'bar', yAxisIndex: 1, data: betCounts, barMaxWidth: 14, itemStyle: { color: 'rgba(251, 146, 60, 0.45)', borderRadius: [3, 3, 0, 0] }, }, ], }; } /** 三个饼图并排,占一张图 */ export function buildTriplePieOption( blocks: { title: string; segments: PieSegment[] }[], i18n?: Partial, ): EChartsOption { const pieEmpty = i18n?.pieEmpty ?? '暂无'; const pieTooltip = i18n?.pieTooltip ?? '{b}:{c}({d}%)'; const slots = [ { center: ['18%', '58%'] as [string, string], titleLeft: '14%' }, { center: ['50%', '58%'] as [string, string], titleLeft: '46%' }, { center: ['82%', '58%'] as [string, string], titleLeft: '78%' }, ]; return { backgroundColor: 'transparent', tooltip: { ...tooltipBase, trigger: 'item', formatter: pieTooltip, }, graphic: blocks.map((b, i) => ({ type: 'text' as const, left: slots[i]?.titleLeft ?? '50%', top: '6%', style: { text: b.title, fill: '#aaa', fontSize: 12, fontWeight: 600, textAlign: 'center', }, })), series: blocks.map((b, i) => { const data = b.segments.map((s) => ({ name: s.label, value: s.value, itemStyle: { color: s.color }, })); return { name: b.title, type: 'pie' as const, radius: ['32%', '48%'], center: slots[i]?.center ?? ['50%', '55%'], label: { show: false }, labelLine: { show: false }, data: data.length ? data : [{ name: pieEmpty, value: 1, itemStyle: { color: '#333' } }], }; }), }; }