feat(admin,player,api): 玩家账号密码管理与代理上下分

新增玩家头像、可查密码与全局改密/改账号开关;玩家资料页合并账号密码展示;代理直属玩家列表支持自定义上下分。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-04 11:36:53 +08:00
parent f76728dc3e
commit a8e4ead618
81 changed files with 1763 additions and 217 deletions

View File

@@ -9,6 +9,8 @@ export interface PlayerHomeMatch {
id: string;
homeTeamName: string;
awayTeamName: string;
homeTeamCode?: string;
awayTeamCode?: string;
startTime: string;
isHot?: boolean;
}

View File

@@ -0,0 +1,143 @@
import { ref, computed } from 'vue';
import { isValidAvatarKey, playerAvatarUrl, randomAvatarKey } from '@thebet365/shared';
import api from '../api';
type ProfileData = {
id?: string | number;
username?: string;
locale?: string;
preferences?: {
phone?: string | null;
email?: string | null;
avatarKey?: string | null;
allowPasswordChange?: boolean;
allowUsernameChange?: boolean;
viewablePassword?: string | null;
};
wallet?: { availableBalance: string; frozenBalance: string };
};
const AVATAR_CACHE_PREFIX = 'player_avatar_key:';
const profileRaw = ref<ProfileData | null>(null);
const loading = ref(false);
let loadPromise: Promise<void> | null = null;
let assigningDefault = false;
function profileSeed(profile: ProfileData | null): string {
if (!profile) return '';
return String(profile.id ?? profile.username ?? '');
}
function readCachedAvatarKey(seed: string): string | null {
if (!seed) return null;
try {
const key = localStorage.getItem(`${AVATAR_CACHE_PREFIX}${seed}`);
return key && isValidAvatarKey(key) ? key : null;
} catch {
return null;
}
}
function writeCachedAvatarKey(seed: string, key: string) {
if (!seed || !key) return;
try {
localStorage.setItem(`${AVATAR_CACHE_PREFIX}${seed}`, key);
} catch {
/* ignore */
}
}
function applyAvatarKey(key: string | null) {
if (!profileRaw.value) {
profileRaw.value = { preferences: { avatarKey: key } };
return;
}
profileRaw.value = {
...profileRaw.value,
preferences: {
...profileRaw.value.preferences,
avatarKey: key,
},
};
}
async function ensureDefaultAvatar() {
if (assigningDefault || !profileRaw.value) return;
const seed = profileSeed(profileRaw.value);
const current =
profileRaw.value.preferences?.avatarKey ?? readCachedAvatarKey(seed);
if (current && isValidAvatarKey(current)) {
applyAvatarKey(current);
return;
}
assigningDefault = true;
const key = randomAvatarKey(seed);
try {
try {
await api.patch('/player/profile', { avatarKey: key });
} catch {
/* 数据库未迁移等情况仍展示本地头像 */
}
applyAvatarKey(key);
writeCachedAvatarKey(seed, key);
} finally {
assigningDefault = false;
}
}
async function loadProfile(force = false) {
if (loadPromise) return loadPromise;
if (!force && profileRaw.value) {
await ensureDefaultAvatar();
return;
}
loadPromise = (async () => {
loading.value = true;
try {
const { data } = await api.get('/player/profile');
profileRaw.value = data.data ?? null;
await ensureDefaultAvatar();
} finally {
loading.value = false;
loadPromise = null;
}
})();
return loadPromise;
}
const avatarKey = computed(() => {
const saved = profileRaw.value?.preferences?.avatarKey;
if (saved && isValidAvatarKey(saved)) return saved;
const seed = profileSeed(profileRaw.value);
if (!seed) return null;
const cached = readCachedAvatarKey(seed);
if (cached) return cached;
return randomAvatarKey(seed);
});
const avatarUrl = computed(() => playerAvatarUrl(avatarKey.value));
function setAvatarKey(key: string | null) {
applyAvatarKey(key);
const seed = profileSeed(profileRaw.value);
if (key && seed) writeCachedAvatarKey(seed, key);
}
export function usePlayerProfile() {
return {
profileRaw,
loading,
avatarKey,
avatarUrl,
loadProfile,
setAvatarKey,
};
}