feat(admin): 管理端列表分页、控制台图表与赛事导入
- 玩家/代理/赛事/注单/审计列表分页,默认每页 10 条,无页面滚动条布局 - ECharts 控制台概览、注单管理中文化与列宽优化 - zhibo 赛事字段迁移与导入,玩家编辑可改所属代理 - 管理端 API 分页与 dashboard 统计接口 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
290
apps/admin/src/utils/dashboard-charts.ts
Normal file
290
apps/admin/src/utils/dashboard-charts.ts
Normal file
@@ -0,0 +1,290 @@
|
||||
import type { EChartsOption } from 'echarts';
|
||||
import { formatAmount, formatAmountFull } from './format-amount';
|
||||
|
||||
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[],
|
||||
): EChartsOption {
|
||||
const data = segments.map((s) => ({
|
||||
name: s.label,
|
||||
value: s.value,
|
||||
itemStyle: { color: s.color },
|
||||
}));
|
||||
|
||||
return {
|
||||
backgroundColor: 'transparent',
|
||||
tooltip: {
|
||||
...tooltipBase,
|
||||
trigger: 'item',
|
||||
formatter: '{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: '暂无数据', value: 1, itemStyle: { color: '#333' } }],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function fmtCount(v: number) {
|
||||
return v.toLocaleString('zh-CN', { maximumFractionDigits: 0 });
|
||||
}
|
||||
|
||||
/** 7 日金额折线 + 注单柱(双 Y 轴),一图看清趋势 */
|
||||
export function buildCombinedTrendOption(
|
||||
labels: string[],
|
||||
amountSeries: ChartSeries[],
|
||||
betCounts: number[],
|
||||
): EChartsOption {
|
||||
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 === '注单笔数';
|
||||
const val = isCount ? `${fmtCount(v)} 笔` : formatAmountFull(v);
|
||||
return `${p.marker ?? ''}${p.seriesName}:${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: '金额',
|
||||
nameTextStyle: { color: '#666', fontSize: 10 },
|
||||
axisLabel: { ...axisLabel, formatter: (v: number) => formatAmount(v) },
|
||||
splitLine,
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
name: '笔数',
|
||||
nameTextStyle: { color: '#666', fontSize: 10 },
|
||||
axisLabel: { ...axisLabel, formatter: (v: number) => fmtCount(v) },
|
||||
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: '注单笔数',
|
||||
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[] }[],
|
||||
): EChartsOption {
|
||||
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: '{b}:{c}({d}%)',
|
||||
},
|
||||
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: '暂无', value: 1, itemStyle: { color: '#333' } }],
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user