Files
thebet365/apps/admin/src/utils/dashboard-charts.ts
2026-06-13 17:38:25 +08:00

315 lines
8.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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: '#ffffff',
borderColor: '#e7e2d8',
textStyle: { color: '#1f2320', fontSize: 12 },
};
const axisLabel = { color: '#78746b', fontSize: 11 };
const splitLine = { lineStyle: { color: '#eee9df' } };
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: '#78746b', fontSize: 11 },
},
grid: { left: 52, right: 12, top: 20, bottom: 48 },
xAxis: {
type: 'category',
data: labels,
axisLabel,
axisLine: { lineStyle: { color: '#e7e2d8' } },
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: '#78746b', fontSize: 11 },
},
grid: { left: 52, right: 12, top: 20, bottom: 48 },
xAxis: {
type: 'category',
data: labels,
boundaryGap: false,
axisLabel,
axisLine: { lineStyle: { color: '#e7e2d8' } },
},
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<ChartI18n>,
): 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: '#78746b', fontSize: 11 },
},
series: [
{
name: title,
type: 'pie',
radius: ['42%', '68%'],
center: ['36%', '50%'],
avoidLabelOverlap: true,
itemStyle: { borderRadius: 4, borderColor: '#ffffff', borderWidth: 2 },
label: {
show: segments.length > 0,
color: '#5f5b53',
fontSize: 11,
formatter: '{b}\n{d}%',
},
labelLine: { lineStyle: { color: '#d5cfc3' } },
emphasis: {
label: { fontSize: 12, fontWeight: 'bold' },
scaleSize: 6,
},
data: data.length ? data : [{ name: noData, value: 1, itemStyle: { color: '#e6e1d8' } }],
},
],
};
}
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), '#956400'],
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('<br/>');
},
},
legend: {
top: 0,
itemWidth: 10,
itemHeight: 10,
textStyle: { color: '#78746b', fontSize: 11 },
},
grid: { left: 56, right: 48, top: 36, bottom: 28 },
xAxis: {
type: 'category',
data: labels,
boundaryGap: true,
axisLabel,
axisLine: { lineStyle: { color: '#e7e2d8' } },
},
yAxis: [
{
type: 'value',
name: i18n.axisAmount,
nameTextStyle: { color: '#8c867c', fontSize: 10 },
axisLabel: { ...axisLabel, formatter: (v: number) => formatAmount(v, 2, loc) },
splitLine,
},
{
type: 'value',
name: i18n.axisCount,
nameTextStyle: { color: '#8c867c', 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(149, 100, 0, 0.34)', borderRadius: [3, 3, 0, 0] },
},
],
};
}
/** 三个饼图并排,占一张图 */
export function buildTriplePieOption(
blocks: { title: string; segments: PieSegment[] }[],
i18n?: Partial<ChartI18n>,
): 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: '#5f5b53',
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: '#e6e1d8' } }],
};
}),
};
}