Files
thebet365/apps/admin/src/utils/dashboard-charts.ts
Mars cbfa18d1d3 feat(i18n): 管理端与玩家端三语支持(中/英/马来语)
- 管理后台 adminT 文案库、结算与代理端页面、表单校验
- 玩家端 vue-i18n 补全首页/公告/串关与 ms 文案
- Element Plus ms 语言包与共享 locale 工具
2026-06-03 15:05:36 +08:00

315 lines
8.3 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: '#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<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: '#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('<br/>');
},
},
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<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: '#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' } }],
};
}),
};
}