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>
This commit is contained in:
@@ -1,5 +1,16 @@
|
||||
import { FormValidationError } from '../i18n/form-validation';
|
||||
|
||||
/** 玩家用户名:仅英文字母与数字,3–32 位 */
|
||||
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;
|
||||
@@ -21,8 +32,8 @@ export interface PlayerEditForm {
|
||||
id: string;
|
||||
username: string;
|
||||
status: string;
|
||||
parentId: string;
|
||||
parentUsername: string | null;
|
||||
affiliationAgents?: string[];
|
||||
availableBalance: string;
|
||||
frozenBalance: string;
|
||||
betCount: number;
|
||||
@@ -44,6 +55,8 @@ export interface PlayerRow {
|
||||
locale: string;
|
||||
parentId: string | null;
|
||||
parentUsername: string | null;
|
||||
/** 归属代理链:一级代理、二级代理(如有) */
|
||||
affiliationAgents?: string[];
|
||||
phone: string | null;
|
||||
email: string | null;
|
||||
managedPassword: string | null;
|
||||
@@ -62,6 +75,19 @@ export interface PlayerDetail extends PlayerRow {
|
||||
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: '',
|
||||
@@ -85,7 +111,6 @@ export function emptyPlayerEditForm(): PlayerEditForm {
|
||||
id: '',
|
||||
username: '',
|
||||
status: 'ACTIVE',
|
||||
parentId: '',
|
||||
parentUsername: null,
|
||||
availableBalance: '0',
|
||||
frozenBalance: '0',
|
||||
@@ -107,8 +132,8 @@ export function editFormFromDetail(d: PlayerDetail): PlayerEditForm {
|
||||
id: d.id,
|
||||
username: d.username,
|
||||
status: d.status,
|
||||
parentId: d.parentId ?? '',
|
||||
parentUsername: d.parentUsername,
|
||||
affiliationAgents: d.affiliationAgents,
|
||||
availableBalance: d.availableBalance,
|
||||
frozenBalance: d.frozenBalance,
|
||||
betCount: d.betCount,
|
||||
@@ -144,6 +169,7 @@ export function buildCreatePlayerPayload(form: PlayerCreateForm) {
|
||||
maxDailyDeposit: form.maxDailyDeposit > 0 ? form.maxDailyDeposit : undefined,
|
||||
};
|
||||
}
|
||||
assertPlayerUsername(form.username);
|
||||
return {
|
||||
username: form.username.trim(),
|
||||
password: form.password,
|
||||
|
||||
Reference in New Issue
Block a user