Files
thebet365/apps/admin/src/views/user-form.ts
Mars ef6b15f119 feat: multi-tier agent hierarchy, wallet ledger, and player UX polish
Add configurable agent max level and default sub-agent credit ratio, per-agent block direct player login on suspend, admin/agent wallet transaction views, and match detail my-bets section with refreshed player card styling.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-10 16:15:34 +08:00

183 lines
5.1 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 { FormValidationError } from '../i18n/form-validation';
/** 玩家用户名仅英文字母与数字332 位 */
export const PLAYER_USERNAME_PATTERN = /^[a-zA-Z0-9]{3,32}$/;
export function assertPlayerUsername(username: string): void {
const trimmed = username.trim();
if (!trimmed) throw new FormValidationError('err.username_required');
if (!PLAYER_USERNAME_PATTERN.test(trimmed)) {
throw new FormValidationError('err.username_player_invalid');
}
}
export interface PlayerCreateForm {
username: string;
password: string;
confirmPassword: string;
parentId: string;
phone: string;
email: string;
initialDeposit: number;
remark: string;
/** 创建为一级代理(非玩家) */
asTier1Agent: boolean;
creditLimit: number;
cashbackRate: number;
maxSingleDeposit: number;
maxDailyDeposit: number;
}
export interface PlayerEditForm {
id: string;
username: string;
status: string;
parentUsername: string | null;
affiliationAgents?: string[];
availableBalance: string;
frozenBalance: string;
betCount: number;
totalStake: string;
totalReturn: string;
createdAt: string;
lastLoginAt: string | null;
loginFailCount: number;
phone: string;
email: string;
managedPassword: string | null;
newPassword: string;
}
export interface PlayerRow {
id: string;
username: string;
status: string;
locale: string;
parentId: string | null;
parentUsername: string | null;
/** 归属代理链:一级代理、二级代理(如有) */
affiliationAgents?: string[];
phone: string | null;
email: string | null;
managedPassword: string | null;
availableBalance: string;
frozenBalance: string;
lastLoginAt: string | null;
betCount: number;
totalStake: string;
totalReturn: string;
createdAt: string;
}
export interface PlayerDetail extends PlayerRow {
loginFailCount: number;
lockedUntil: string | null;
updatedAt: string;
}
/** 玩家归属标签,格式:玩家-平台 | 玩家-一级代理 | 玩家-一级代理-二级代理 */
export function formatPlayerAffiliationLabel(
row: Pick<PlayerRow, 'affiliationAgents'>,
playerLabel: string,
platformLabel: string,
): string {
const agents = row.affiliationAgents ?? [];
if (agents.length === 0) {
return [playerLabel, platformLabel].join('-');
}
return [playerLabel, ...agents].join('-');
}
export function emptyPlayerCreateForm(): PlayerCreateForm {
return {
username: '',
password: '',
confirmPassword: '',
parentId: '',
phone: '',
email: '',
initialDeposit: 0,
remark: '',
asTier1Agent: false,
creditLimit: 50000,
cashbackRate: 0,
maxSingleDeposit: 0,
maxDailyDeposit: 0,
};
}
export function emptyPlayerEditForm(): PlayerEditForm {
return {
id: '',
username: '',
status: 'ACTIVE',
parentUsername: null,
availableBalance: '0',
frozenBalance: '0',
betCount: 0,
totalStake: '0',
totalReturn: '0',
createdAt: '',
lastLoginAt: null,
loginFailCount: 0,
phone: '',
email: '',
managedPassword: null,
newPassword: '',
};
}
export function editFormFromDetail(d: PlayerDetail): PlayerEditForm {
return {
id: d.id,
username: d.username,
status: d.status,
parentUsername: d.parentUsername,
affiliationAgents: d.affiliationAgents,
availableBalance: d.availableBalance,
frozenBalance: d.frozenBalance,
betCount: d.betCount,
totalStake: d.totalStake,
totalReturn: d.totalReturn,
createdAt: d.createdAt,
lastLoginAt: d.lastLoginAt,
loginFailCount: d.loginFailCount,
phone: d.phone ?? '',
email: d.email ?? '',
managedPassword: d.managedPassword ?? null,
newPassword: '',
};
}
export function buildCreatePlayerPayload(form: PlayerCreateForm) {
if (!form.username.trim()) throw new FormValidationError('err.username_required');
if (form.password.length < 8) throw new FormValidationError('err.password_min');
if (form.password !== form.confirmPassword) throw new FormValidationError('err.password_mismatch');
if (form.asTier1Agent) {
if (form.parentId) throw new FormValidationError('err.agent_no_parent');
if (form.initialDeposit > 0) throw new FormValidationError('err.agent_no_initial_deposit');
if (form.creditLimit < 0) throw new FormValidationError('err.credit_negative');
return {
username: form.username.trim(),
password: form.password,
phone: form.phone.trim() || undefined,
email: form.email.trim() || undefined,
asTier1Agent: true,
creditLimit: form.creditLimit,
cashbackRate: form.cashbackRate,
maxSingleDeposit: form.maxSingleDeposit > 0 ? form.maxSingleDeposit : undefined,
maxDailyDeposit: form.maxDailyDeposit > 0 ? form.maxDailyDeposit : undefined,
};
}
assertPlayerUsername(form.username);
return {
username: form.username.trim(),
password: form.password,
parentId: form.parentId || undefined,
phone: form.phone.trim() || undefined,
email: form.email.trim() || undefined,
initialDeposit: form.initialDeposit > 0 ? form.initialDeposit : undefined,
remark: form.remark.trim() || undefined,
};
}