feat: 管理端 RBAC 权限体系与员工管理
新增多角色权限控制(赛事/财务/客服管理员),支持员工 CRUD、路由菜单按权限显隐、审计日志范围过滤;登录返回角色与权限列表。玩家端赛事列表增加静默刷新避免图片闪烁。Seed 补充演示员工账号与充值相关权限。附带 RBAC/审计范围单元测试及 UAT 文档更新。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -55,7 +55,38 @@ export function usePlayerHome() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const { data } = await api.get('/player/home');
|
||||
homeRaw.value = (data.data ?? null) as HomePayload | null;
|
||||
const fresh = (data.data ?? null) as HomePayload | null;
|
||||
|
||||
if (fresh && homeRaw.value) {
|
||||
// 已有数据 → 原地更新,保留对象引用,避免图片重新加载
|
||||
const existing = homeRaw.value;
|
||||
existing.banners = fresh.banners;
|
||||
existing.announcements = fresh.announcements;
|
||||
existing.ticker = fresh.ticker;
|
||||
existing.notices = fresh.notices;
|
||||
|
||||
if (fresh.hotMatches && existing.hotMatches) {
|
||||
const freshMap = new Map(fresh.hotMatches.map((m) => [m.id, m]));
|
||||
for (const m of existing.hotMatches) {
|
||||
const f = freshMap.get(m.id);
|
||||
if (f) Object.assign(m, f);
|
||||
}
|
||||
// 处理新增或删除的比赛
|
||||
const existingIds = new Set(existing.hotMatches.map((m) => m.id));
|
||||
for (const fm of fresh.hotMatches) {
|
||||
if (!existingIds.has(fm.id)) existing.hotMatches.push(fm);
|
||||
}
|
||||
for (let i = existing.hotMatches.length - 1; i >= 0; i--) {
|
||||
if (!freshMap.has(existing.hotMatches[i].id)) {
|
||||
existing.hotMatches.splice(i, 1);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
existing.hotMatches = fresh.hotMatches;
|
||||
}
|
||||
} else {
|
||||
homeRaw.value = fresh;
|
||||
}
|
||||
} catch {
|
||||
homeRaw.value = null;
|
||||
} finally {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ref, shallowRef } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
import api from '../api';
|
||||
import type { MatchPhase } from '../utils/matchPhase';
|
||||
|
||||
@@ -35,21 +35,44 @@ export interface PlayerMatchSummary {
|
||||
} | null;
|
||||
}
|
||||
|
||||
const summaryMatches = shallowRef<PlayerMatchSummary[]>([]);
|
||||
const summaryMatches = ref<PlayerMatchSummary[]>([]);
|
||||
const summaryLoading = ref(false);
|
||||
|
||||
let summaryInflight: Promise<void> | null = null;
|
||||
|
||||
async function loadSummary(force = false): Promise<void> {
|
||||
if (force) summaryMatches.value = [];
|
||||
if (!force && summaryMatches.value.length > 0) return;
|
||||
async function loadSummary(force = false, silent = false): Promise<void> {
|
||||
if (force && !silent) summaryMatches.value = [];
|
||||
if (!force && !silent && summaryMatches.value.length > 0) return;
|
||||
if (summaryInflight) return summaryInflight;
|
||||
|
||||
summaryLoading.value = true;
|
||||
if (!silent) summaryLoading.value = true;
|
||||
summaryInflight = (async () => {
|
||||
try {
|
||||
const { data } = await api.get('/player/matches');
|
||||
summaryMatches.value = (data.data ?? []) as PlayerMatchSummary[];
|
||||
const fresh = (data.data ?? []) as PlayerMatchSummary[];
|
||||
|
||||
if (silent && summaryMatches.value.length > 0) {
|
||||
// 静默模式:原地更新,保留对象引用,避免图片重新加载
|
||||
const freshMap = new Map(fresh.map((m) => [m.id, m]));
|
||||
const existingIds = new Set(summaryMatches.value.map((m) => m.id));
|
||||
|
||||
for (const fm of fresh) {
|
||||
if (!existingIds.has(fm.id)) {
|
||||
summaryMatches.value.push(fm);
|
||||
}
|
||||
}
|
||||
for (let i = summaryMatches.value.length - 1; i >= 0; i--) {
|
||||
const existing = summaryMatches.value[i];
|
||||
const f = freshMap.get(existing.id);
|
||||
if (f) {
|
||||
Object.assign(existing, f);
|
||||
} else {
|
||||
summaryMatches.value.splice(i, 1);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
summaryMatches.value = fresh;
|
||||
}
|
||||
} catch {
|
||||
if (!summaryMatches.value.length) summaryMatches.value = [];
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user