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:
2026-06-09 17:56:28 +08:00
parent d5e7c8edb3
commit df20444be9
27 changed files with 2136 additions and 563 deletions

View File

@@ -1,5 +1,16 @@
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;
@@ -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,