feat: 管理端 RBAC 权限体系与员工管理
新增多角色权限控制(赛事/财务/客服管理员),支持员工 CRUD、路由菜单按权限显隐、审计日志范围过滤;登录返回角色与权限列表。玩家端赛事列表增加静默刷新避免图片闪烁。Seed 补充演示员工账号与充值相关权限。附带 RBAC/审计范围单元测试及 UAT 文档更新。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3,11 +3,14 @@ import { computed, onMounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useAdminDashboard } from '../../composables/useAdminDashboard';
|
||||
import EChartPanel from '../../components/dashboard/EChartPanel.vue';
|
||||
import { buildCombinedTrendOption, buildTriplePieOption } from '../../utils/dashboard-charts';
|
||||
import { buildCombinedTrendOption, buildTriplePieOption, buildBarChartOption } from '../../utils/dashboard-charts';
|
||||
import { betStatusLabel } from '../../utils/bet-labels';
|
||||
import { useAdminLocale } from '../../composables/useAdminLocale';
|
||||
import { usePermissions } from '../../composables/usePermissions';
|
||||
|
||||
const { t } = useAdminLocale();
|
||||
const { role } = usePermissions();
|
||||
const isMatchOperatorView = computed(() => role.value === 'MATCH_ADMIN');
|
||||
const router = useRouter();
|
||||
const {
|
||||
s,
|
||||
@@ -32,8 +35,16 @@ function goKpiLink(link: KpiLink) {
|
||||
router.push(link.query ? { path: link.path, query: link.query } : link.path);
|
||||
}
|
||||
|
||||
const mainTrendOption = computed(() =>
|
||||
buildCombinedTrendOption(
|
||||
const mainTrendOption = computed(() => {
|
||||
const counts = s.value?.trend7d?.map((d) => d.betCount) ?? [];
|
||||
if (isMatchOperatorView.value) {
|
||||
return buildBarChartOption(
|
||||
trendLabels.value,
|
||||
[{ name: t('dash.chart_bet_count'), color: '#956400', values: counts }],
|
||||
{ amountAxis: false },
|
||||
);
|
||||
}
|
||||
return buildCombinedTrendOption(
|
||||
trendLabels.value,
|
||||
[
|
||||
{
|
||||
@@ -52,10 +63,10 @@ const mainTrendOption = computed(() =>
|
||||
values: s.value?.trend7d?.map((d) => toNum(d.ggr)) ?? [],
|
||||
},
|
||||
],
|
||||
s.value?.trend7d?.map((d) => d.betCount) ?? [],
|
||||
counts,
|
||||
chartI18n.value,
|
||||
),
|
||||
);
|
||||
);
|
||||
});
|
||||
|
||||
const distributionOption = computed(() => {
|
||||
const m = s.value?.matches;
|
||||
@@ -146,13 +157,15 @@ const kpiMatch = computed(() => {
|
||||
<template v-else-if="s">
|
||||
<el-card class="overview-board" shadow="never">
|
||||
<div v-if="s.generatedAt" class="board-head">
|
||||
<span class="board-hint">{{ t('dash.section_matches_hint') }}</span>
|
||||
<span class="board-hint">{{
|
||||
isMatchOperatorView ? t('dash.section_match_ops_hint') : t('dash.section_matches_hint')
|
||||
}}</span>
|
||||
<span class="dash-updated">
|
||||
{{ t('common.updated_at') }} {{ formatTime(s.generatedAt) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="kpi-grid kpi-primary">
|
||||
<div v-if="!isMatchOperatorView" class="kpi-grid kpi-primary">
|
||||
<div v-for="item in kpiPrimary" :key="item.label" class="kpi-cell">
|
||||
<span class="kpi-label">{{ item.label }}</span>
|
||||
<span class="kpi-value">{{ item.value }}</span>
|
||||
@@ -166,7 +179,7 @@ const kpiMatch = computed(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="kpi-grid kpi-secondary">
|
||||
<div class="kpi-grid" :class="isMatchOperatorView ? 'kpi-primary' : 'kpi-secondary'">
|
||||
<div
|
||||
v-for="item in kpiMatch"
|
||||
:key="item.label"
|
||||
@@ -185,7 +198,9 @@ const kpiMatch = computed(() => {
|
||||
|
||||
<div class="charts-stack">
|
||||
<EChartPanel title="" :option="mainTrendOption" height="300px" class="chart-main" />
|
||||
<div class="chart-main-caption">{{ t('dash.trend_caption') }}</div>
|
||||
<div class="chart-main-caption">{{
|
||||
isMatchOperatorView ? t('dash.trend_bet_count_caption') : t('dash.trend_caption')
|
||||
}}</div>
|
||||
<EChartPanel title="" :option="distributionOption" height="200px" class="chart-dist" />
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
Reference in New Issue
Block a user