feat: 手动充值、邀请码注册与后台管理增强
新增玩家手动充值全流程(收款方式配置、充值下单/审核、钱包上分), 支持邀请码注册、邀请历史与专属返水率;完善后台代理/玩家管理与响应式操作栏, 并补充前台注册、充值页及多语言错误码。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
26
apps/admin/src/utils/invite-link.ts
Normal file
26
apps/admin/src/utils/invite-link.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
19
apps/admin/src/utils/rate-percent.ts
Normal file
19
apps/admin/src/utils/rate-percent.ts
Normal 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)}%`;
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 '—';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user