feat: 手动充值、邀请码注册与后台管理增强

新增玩家手动充值全流程(收款方式配置、充值下单/审核、钱包上分),
支持邀请码注册、邀请历史与专属返水率;完善后台代理/玩家管理与响应式操作栏,
并补充前台注册、充值页及多语言错误码。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-11 12:20:11 +08:00
parent 618fb49511
commit 10485ecfaf
98 changed files with 7908 additions and 856 deletions

View File

@@ -0,0 +1,26 @@
export function buildPlayerRegisterUrl(inviteCode: string): string {
const configured = (import.meta.env.VITE_PLAYER_URL as string | undefined)?.trim();
const base = configured?.replace(/\/$/, '') || `${window.location.protocol}//${window.location.hostname}:5173`;
return `${base}/register?code=${encodeURIComponent(inviteCode)}`;
}
export async function copyText(text: string): Promise<boolean> {
try {
await navigator.clipboard.writeText(text);
return true;
} catch {
try {
const el = document.createElement('textarea');
el.value = text;
el.style.position = 'fixed';
el.style.opacity = '0';
document.body.appendChild(el);
el.select();
const ok = document.execCommand('copy');
document.body.removeChild(el);
return ok;
} catch {
return false;
}
}
}

View File

@@ -0,0 +1,19 @@
/** 后端/API 存小数0.01 = 1%),后台 UI 用百分比1 = 1% */
export function decimalRateToPercent(value: string | number | null | undefined): number {
const n = typeof value === 'number' ? value : parseFloat(String(value ?? '0'));
if (!Number.isFinite(n)) return 0;
return Math.round(n * 10000) / 100;
}
export function percentToDecimalRate(percent: string | number | null | undefined): number {
const n = typeof percent === 'number' ? percent : parseFloat(String(percent ?? '0'));
if (!Number.isFinite(n) || n <= 0) return 0;
return Math.round(n * 100) / 10000;
}
export function formatRatePercent(value: string | number | null | undefined): string {
const n = typeof value === 'number' ? value : parseFloat(String(value ?? ''));
if (!Number.isFinite(n)) return '—';
return `${(n * 100).toFixed(2)}%`;
}

View File

@@ -49,6 +49,7 @@ export async function hydrateStaffSession(): Promise<boolean> {
agentLevel: typeof raw.agentLevel === 'number' ? raw.agentLevel : null,
maxAgentLevel: typeof raw.maxAgentLevel === 'number' ? raw.maxAgentLevel : null,
canManageSubAgents: raw.canManageSubAgents === true,
inviteCode: raw.inviteCode ?? null,
});
return true;
} catch (e: unknown) {

View File

@@ -3,7 +3,7 @@ import { buildBarChartOption, buildPieChartOption, type PieSegment } from './das
const STATUS_COLORS: Record<string, string> = {
PENDING: '#e8a040',
WON: '#2fb56a',
WON: '#d4d4d4',
LOST: '#ff453a',
PUSH: '#8e8e93',
VOID: '#555',
@@ -74,7 +74,7 @@ export function buildBetTypePieOption(input: {
centerLabel: string;
}): EChartsOption {
const segments: PieSegment[] = [
{ label: input.labels.single, value: input.singleBets, color: '#2fb56a' },
{ label: input.labels.single, value: input.singleBets, color: '#d4d4d4' },
{ label: input.labels.parlay, value: input.parlayBets, color: '#fb923c' },
].filter((s) => s.value > 0);
@@ -112,7 +112,7 @@ export function buildSelectionStakeBarOption(input: {
}): EChartsOption {
const base = buildBarChartOption(
input.labels,
[{ name: input.seriesName, color: '#2fb56a', values: input.stakes }],
[{ name: input.seriesName, color: '#d4d4d4', values: input.stakes }],
{ amountAxis: true },
);
return withCompactHeader(

View File

@@ -16,8 +16,38 @@ export const TX_KEY_MAP: Record<string, string> = {
RESETTLE_REVERSE: 'finance.tx.resettle',
DEPOSIT: 'finance.tx.deposit',
WITHDRAW: 'finance.tx.withdraw',
PLAYER_DEPOSIT: 'finance.tx.deposit',
};
export function walletTxTypeKey(type: string): string {
return TX_KEY_MAP[type.toUpperCase()] ?? '';
}
export const DEPOSIT_RECHARGE_TYPES = new Set([
'MANUAL_DEPOSIT',
'DEPOSIT',
'PLAYER_DEPOSIT',
]);
export const DEPOSIT_METHOD_KEY_MAP: Record<string, string> = {
MANUAL_ADMIN: 'finance.deposit_method.manual_admin',
MANUAL_AGENT: 'finance.deposit_method.manual_agent',
MANUAL: 'finance.deposit_method.manual',
};
export function walletDepositMethodLabel(
row: {
transactionType: string;
depositMethodKey?: string | null;
depositMethodName?: string | null;
},
t: (key: string) => string,
): string {
if (!DEPOSIT_RECHARGE_TYPES.has(row.transactionType.toUpperCase())) return '—';
if (row.depositMethodName?.trim()) return row.depositMethodName.trim();
if (row.depositMethodKey) {
const key = DEPOSIT_METHOD_KEY_MAP[row.depositMethodKey];
return key ? t(key) : row.depositMethodKey;
}
return '—';
}