Files
thebet365/apps/admin/src/views/user-form.ts
Mars df20444be9 feat: refactor agent manager, media library, and player UX
- Split admin users page into player/tier-1/tier-2 tabs with affiliation labels and context-specific create dialogs

- Add media library with uploaded_files migration, list/delete unused files API, and admin nav route

- Enforce player username format (alphanumeric 3-32) on frontend and backend via shared package

- Improve admin dialog/panel styling; refine player parlay and match bet card kickoff display

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-09 17:56:28 +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: 'Player@123',
confirmPassword: 'Player@123',
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,
};
}