feat(admin,api,player): 结算预览分页、统计图表与返水限额

完善结算计算与预览 API(含后端分页),加强管理端结算/返水/权限,并优化玩家端投注单与队徽展示。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-05 13:54:33 +08:00
parent 6264b8806c
commit efff7c27e6
40 changed files with 3560 additions and 578 deletions

View File

@@ -0,0 +1,123 @@
import type { EChartsOption } from 'echarts';
import { buildBarChartOption, buildPieChartOption, type PieSegment } from './dashboard-charts';
const STATUS_COLORS: Record<string, string> = {
PENDING: '#e8a040',
WON: '#2fb56a',
LOST: '#ff453a',
PUSH: '#8e8e93',
VOID: '#555',
};
function withCompactHeader(option: EChartsOption, text: string, subtext?: string): EChartsOption {
const grid = (option.grid ?? {}) as { top?: number; bottom?: number; left?: number; right?: number };
const top = subtext ? 36 : 24;
return {
...option,
title: {
text,
subtext,
left: 'center',
top: 0,
textStyle: { color: '#ccc', fontSize: 11, fontWeight: 600 },
subtextStyle: { color: '#777', fontSize: 9 },
itemGap: 2,
},
grid: { ...grid, top: Math.max(top, grid.top ?? 0) },
};
}
function compactPie(option: EChartsOption, centerLines?: string[]): EChartsOption {
const series = option.series as Array<Record<string, unknown>> | undefined;
if (series?.[0]) {
series[0].center = ['50%', '55%'];
series[0].radius = ['34%', '50%'];
series[0].label = { show: false };
series[0].labelLine = { show: false };
}
const lines = centerLines?.filter(Boolean) ?? [];
const graphic = lines.map((line, i) => ({
type: 'text' as const,
left: 'center',
top: lines.length === 1 ? '52%' : `${48 + i * 12}%`,
style: {
text: line,
textAlign: 'center' as const,
fill: i === 0 ? '#eee' : '#888',
fontSize: i === 0 ? 15 : 9,
fontWeight: i === 0 ? 700 : 400,
},
}));
return {
...option,
legend: {
bottom: 0,
left: 'center',
itemWidth: 8,
itemHeight: 8,
textStyle: { color: '#999', fontSize: 9 },
},
graphic,
};
}
export function buildBetTypePieOption(input: {
title: string;
singleBets: number;
parlayBets: number;
totalBets: number;
legCount: number;
labels: { single: string; parlay: string };
subtext: string;
centerLabel: string;
}): EChartsOption {
const segments: PieSegment[] = [
{ label: input.labels.single, value: input.singleBets, color: '#2fb56a' },
{ label: input.labels.parlay, value: input.parlayBets, color: '#fb923c' },
].filter((s) => s.value > 0);
const base = buildPieChartOption(input.title, segments, { pieTooltip: '{b}{c}{d}%' });
const withHeader = withCompactHeader(base, input.title, input.subtext);
return compactPie(withHeader, [String(input.totalBets), input.centerLabel]);
}
export function buildStatusPieOption(input: {
title: string;
statusCounts: Record<string, number>;
labelFor: (status: string) => string;
subtext: string;
}): EChartsOption {
const segments: PieSegment[] = Object.entries(input.statusCounts)
.filter(([, count]) => count > 0)
.map(([status, value]) => ({
label: input.labelFor(status),
value,
color: STATUS_COLORS[status] ?? '#666',
}));
const total = segments.reduce((sum, s) => sum + s.value, 0);
const base = buildPieChartOption(input.title, segments, { pieTooltip: '{b}{c}{d}%' });
const withHeader = withCompactHeader(base, input.title, input.subtext);
return compactPie(withHeader, [String(total)]);
}
export function buildSelectionStakeBarOption(input: {
title: string;
subtext: string;
labels: string[];
stakes: number[];
seriesName: string;
}): EChartsOption {
const base = buildBarChartOption(
input.labels,
[{ name: input.seriesName, color: '#2fb56a', values: input.stakes }],
{ amountAxis: true },
);
return withCompactHeader(
{ ...base, grid: { left: 40, right: 6, top: 36, bottom: 22 }, legend: { show: false } },
input.title,
input.subtext,
);
}