fix admin dashboard permission scope display

This commit is contained in:
WorkBuddy
2026-10-25 09:06:15 +08:00
parent 34b9ea1169
commit b38056f1d0
5 changed files with 98 additions and 37 deletions
+17 -8
View File
@@ -156,7 +156,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, watch } from 'vue' import { computed, ref, watch } from 'vue'
import { get } from '@/api/client' import { get } from '@/api/client'
import { summarizeMatchmakerServices } from '@/services/dashboard'
import { BRAND_BACKDROP, BRAND_LOGO } from '@/brand' import { BRAND_BACKDROP, BRAND_LOGO } from '@/brand'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
@@ -234,12 +233,10 @@ watch(() => route.fullPath, async () => {
const perms = authStore.permissions const perms = authStore.permissions
if (authStore.token === 'ui-preview-only') return if (authStore.token === 'ui-preview-only') return
try { try {
if (perms.includes('matchmaker.assign') || perms.includes('matchmaker.team.assign')) { if (perms.includes('workbench.view')) {
const response: any = await get('/operator/matchmaker/management/services') const response: any = await get('/operator/workbench/dashboard')
if (response.success && Array.isArray(response.data)) todayStats.value = summarizeMatchmakerServices(response.data) const value = response.data?.metrics?.matchmakerTodos?.value
} else if (perms.includes('matchmaker.view')) { if (response.success && typeof value === 'number') todayStats.value = { total: value, pending: value }
const response: any = await get('/operator/matchmaker/dashboard')
if (response.success) todayStats.value = { total: response.data.todayCount, pending: response.data.pendingCount + response.data.inProgressCount }
} }
} catch { } catch {
todayStats.value = null todayStats.value = null
@@ -256,6 +253,13 @@ const permissions = computed(() => authStore.permissions || [])
const currentRoleName = computed(() => { const currentRoleName = computed(() => {
// 按权限从高到低判定,覆盖七类后台角色(平台管理员 / 咨询管理员 / 内容编辑员 / // 按权限从高到低判定,覆盖七类后台角色(平台管理员 / 咨询管理员 / 内容编辑员 /
// 风控运营 / 公益咨询 / 服务站运营 / 资料审核 / 公益红娘)。 // 风控运营 / 公益咨询 / 服务站运营 / 资料审核 / 公益红娘)。
const role = operator.value?.roles?.[0]
const roleNames: Record<string, string> = {
super_admin: '超级管理员', platform_admin: '平台管理员', matchmaker_leader: '红娘组长', matchmaker: '公益红娘',
reviewer: '资料审核人员', counselor: '公益咨询人员', counseling_manager: '咨询管理员', service_station_operator: '服务站运营人员',
content_editor: '内容编辑员', risk_operator: '风控运营人员',
}
if (role && roleNames[role]) return roleNames[role]
const perms = permissions.value const perms = permissions.value
if (perms.includes('permission.manage')) return '平台管理员' if (perms.includes('permission.manage')) return '平台管理员'
if (perms.includes('counseling.service.assign') || perms.includes('counseling.note.view')) return '咨询管理员' if (perms.includes('counseling.service.assign') || perms.includes('counseling.note.view')) return '咨询管理员'
@@ -264,10 +268,15 @@ const currentRoleName = computed(() => {
if (perms.includes('counseling.service.view')) return '公益咨询人员' if (perms.includes('counseling.service.view')) return '公益咨询人员'
if (perms.includes('service_appointment.view')) return '服务站运营人员' if (perms.includes('service_appointment.view')) return '服务站运营人员'
if (perms.includes('review.view')) return '资料审核人员' if (perms.includes('review.view')) return '资料审核人员'
if (perms.includes('matchmaker.team.assign')) return '红娘组长'
if (perms.includes('matchmaker.view')) return '公益红娘' if (perms.includes('matchmaker.view')) return '公益红娘'
return '运营人员' return '运营人员'
}) })
const visibleNavigation = computed(() => navigation.filter((item) => item.permissions.every(hasPerm))) const visibleNavigation = computed(() => navigation.filter((item) => {
const adminRole = operator.value?.roles?.some((role) => ['super_admin', 'platform_admin'].includes(role))
if ((item.path === '/referral' || item.path === '/matchmaker') && (permissions.value.includes('permission.manage') || adminRole)) return false
return item.permissions.every(hasPerm)
}))
function hasPerm(permission: string) { function hasPerm(permission: string) {
return permissions.value.includes(permission) return permissions.value.includes(permission)
+41 -27
View File
@@ -54,19 +54,18 @@ const emptyMetric = (
key: string, key: string,
label: string, label: string,
tone: DashboardMetric['tone'], tone: DashboardMetric['tone'],
route: string, ): DashboardMetric => ({ key, label, value: 0, trendText: '暂无授权数据', tone, available: false, status: 'forbidden', route: '' })
): DashboardMetric => ({ key, label, value: 0, trendText: '暂无授权数据', tone, available: false, route })
export function createEmptyDashboard(notice = '当前账号暂无可展示的工作台数据'): DashboardModel { export function createEmptyDashboard(notice = '当前账号暂无可展示的工作台数据'): DashboardModel {
return { return {
followups: [], followups: [],
metrics: [ metrics: [
emptyMetric('pending_reviews', '待审核资料', 'rose', '/review'), emptyMetric('pending_reviews', '待审核资料', 'rose'),
emptyMetric('matchmaker_todos', '红娘服务待办', 'amber', '/matchmaker'), emptyMetric('matchmaker_todos', '红娘服务待办', 'amber'),
emptyMetric('counseling_todos', '咨询服务待办', 'blue', '/counseling'), emptyMetric('counseling_todos', '咨询服务待办', 'blue'),
emptyMetric('station_appointments', '服务站预约', 'green', '/service-appointment'), emptyMetric('station_appointments', '服务站预约', 'green'),
emptyMetric('effective_matches', '有效匹配', 'purple', '/analytics'), emptyMetric('effective_matches', '有效匹配', 'purple'),
emptyMetric('activity_registrations', '活动报名', 'red', '/activities/interests'), emptyMetric('activity_registrations', '活动报名', 'red'),
], ],
todos: [], todos: [],
activities: [], activities: [],
@@ -84,7 +83,8 @@ export async function loadDashboard(permissions: string[]): Promise<DashboardMod
const canManageMatchmaker = permissions.includes('matchmaker.assign') || permissions.includes('matchmaker.team.assign') const canManageMatchmaker = permissions.includes('matchmaker.assign') || permissions.includes('matchmaker.team.assign')
const canViewReviews = permissions.includes('review.view') const canViewReviews = permissions.includes('review.view')
const [coreResult, serviceResult, activityResult, matchmakerResult, reviewResult, managementResult] = await Promise.allSettled([ const [workbenchResult, coreResult, serviceResult, activityResult, matchmakerResult, reviewResult, managementResult] = await Promise.allSettled([
client.get('/operator/workbench/dashboard', { timeout: REQUEST_TIMEOUT }),
canViewAnalytics canViewAnalytics
? client.get('/operator/analytics/dashboard', { timeout: REQUEST_TIMEOUT }) ? client.get('/operator/analytics/dashboard', { timeout: REQUEST_TIMEOUT })
: Promise.resolve(null), : Promise.resolve(null),
@@ -101,6 +101,18 @@ export async function loadDashboard(permissions: string[]): Promise<DashboardMod
canManageMatchmaker ? client.get('/operator/matchmaker/management/services', { timeout: REQUEST_TIMEOUT }) : Promise.resolve(null), canManageMatchmaker ? client.get('/operator/matchmaker/management/services', { timeout: REQUEST_TIMEOUT }) : Promise.resolve(null),
]) ])
const workbench = fulfilledData<JsonRecord>(workbenchResult)
const workbenchMetrics = recordOf(workbench || undefined, 'metrics')
const workbenchValue = (key: string) => numberOf(recordOf(workbenchMetrics, key), 'value')
const hasWorkbenchMetric = (key: string) => Object.prototype.hasOwnProperty.call(workbenchMetrics, key)
const workbenchAvailable = (key: string) => Boolean(recordOf(workbenchMetrics, key).available)
const metricStatus = (key: string, permission: string, fallbackAvailable: boolean): DashboardMetric['status'] => {
if (hasWorkbenchMetric(key)) return workbenchAvailable(key) ? 'ready' : 'forbidden'
if (!permissions.includes(permission)) return 'forbidden'
return fallbackAvailable ? 'ready' : 'error'
}
const metricAvailable = (key: string, permission: string, fallbackAvailable: boolean) => metricStatus(key, permission, fallbackAvailable) === 'ready'
const metricRoute = (status: DashboardMetric['status'], route: string) => status === 'ready' ? route : ''
const core = fulfilledData<JsonRecord>(coreResult) const core = fulfilledData<JsonRecord>(coreResult)
const service = fulfilledData<JsonRecord>(serviceResult) const service = fulfilledData<JsonRecord>(serviceResult)
const activityRows = fulfilledData<JsonRecord[]>(activityResult) const activityRows = fulfilledData<JsonRecord[]>(activityResult)
@@ -129,39 +141,41 @@ export async function loadDashboard(permissions: string[]): Promise<DashboardMod
const metrics: DashboardMetric[] = [ const metrics: DashboardMetric[] = [
{ {
key: 'pending_reviews', label: '待审核资料', value: numberOf(reviews || undefined, 'pending') + numberOf(reviews || undefined, 'processing'), key: 'pending_reviews', label: '待审核资料', value: hasWorkbenchMetric('pendingReviews') ? workbenchValue('pendingReviews') : numberOf(reviews || undefined, 'pending') + numberOf(reviews || undefined, 'processing'),
trendText: reviews ? '待领取与审核中资料' : '暂无授权数据', tone: 'rose', available: Boolean(reviews), route: '/review', trendText: workbenchAvailable('pendingReviews') ? '当前数据范围内待审核资料' : (reviews ? '待领取与审核中资料' : '暂无授权数据'), tone: 'rose', available: metricAvailable('pendingReviews', 'review.view', Boolean(reviews)), status: metricStatus('pendingReviews', 'review.view', Boolean(reviews)), route: metricRoute(metricStatus('pendingReviews', 'review.view', Boolean(reviews)), '/review'),
}, },
{ {
key: 'matchmaker_todos', label: '红娘服务待办', key: 'matchmaker_todos', label: '红娘服务待办',
value: matchmakerPending, value: hasWorkbenchMetric('matchmakerTodos') ? workbenchValue('matchmakerTodos') : matchmakerPending,
trendText: matchmakerAvailable ? (canManageMatchmaker ? '管理范围内待完成服务' : '本人服务对象待跟进') : '暂无授权数据', tone: 'amber', available: matchmakerAvailable, route: '/matchmaker', trendText: workbenchAvailable('matchmakerTodos') ? `${workbench?.scopeLabel || '当前范围'}待完成服务` : (matchmakerAvailable ? (canManageMatchmaker ? '管理范围内待完成服务' : '本人服务对象待跟进') : '暂无授权数据'), tone: 'amber', available: metricAvailable('matchmakerTodos', 'matchmaker.view', matchmakerAvailable), status: metricStatus('matchmakerTodos', 'matchmaker.view', matchmakerAvailable), route: metricRoute(metricStatus('matchmakerTodos', 'matchmaker.view', matchmakerAvailable), permissions.includes('permission.manage') ? '' : '/matchmaker'),
}, },
{ {
key: 'counseling_todos', label: '咨询服务待办', key: 'counseling_todos', label: '咨询服务待办',
value: numberOf(counseling, 'pending') + numberOf(counseling, 'assigned'), value: hasWorkbenchMetric('counselingTodos') ? workbenchValue('counselingTodos') : numberOf(counseling, 'pending') + numberOf(counseling, 'assigned'),
trendText: service ? '待受理与已分配' : '暂无授权数据', tone: 'blue', available: Boolean(service), route: '/counseling', trendText: workbenchAvailable('counselingTodos') ? `${workbench?.scopeLabel || '当前范围'}咨询待办` : (service ? '待受理与已分配' : '暂无授权数据'), tone: 'blue', available: metricAvailable('counselingTodos', 'counseling.service.view', Boolean(service)), status: metricStatus('counselingTodos', 'counseling.service.view', Boolean(service)), route: metricRoute(metricStatus('counselingTodos', 'counseling.service.view', Boolean(service)), '/counseling'),
}, },
{ {
key: 'station_appointments', label: '服务站预约', key: 'station_appointments', label: '服务站预约',
value: numberOf(appointments, 'submitted') + numberOf(appointments, 'confirmed'), value: hasWorkbenchMetric('serviceStationAppointments') ? workbenchValue('serviceStationAppointments') : numberOf(appointments, 'submitted') + numberOf(appointments, 'confirmed'),
trendText: service ? '待确认与已确认' : '暂无授权数据', tone: 'green', available: Boolean(service), route: '/service-appointment', trendText: workbenchAvailable('serviceStationAppointments') ? `${workbench?.scopeLabel || '当前范围'}服务站预约` : (service ? '待确认与已确认' : '暂无授权数据'), tone: 'green', available: metricAvailable('serviceStationAppointments', 'service_appointment.view', Boolean(service)), status: metricStatus('serviceStationAppointments', 'service_appointment.view', Boolean(service)), route: metricRoute(metricStatus('serviceStationAppointments', 'service_appointment.view', Boolean(service)), '/service-appointment'),
}, },
{ {
key: 'effective_matches', label: '有效匹配', value: numberOf(effectiveConnections, 'activeMatches'), key: 'effective_matches', label: '有效匹配', value: hasWorkbenchMetric('effectiveMatches') ? workbenchValue('effectiveMatches') : numberOf(effectiveConnections, 'activeMatches'),
trendText: service ? '当前活跃匹配' : '暂无授权数据', tone: 'purple', available: Boolean(service), route: '/analytics', trendText: workbenchAvailable('effectiveMatches') ? `${workbench?.scopeLabel || '当前范围'}有效匹配` : (service ? '当前活跃匹配' : '暂无授权数据'), tone: 'purple', available: metricAvailable('effectiveMatches', 'analytics.view', Boolean(service)), status: metricStatus('effectiveMatches', 'analytics.view', Boolean(service)), route: metricRoute(metricStatus('effectiveMatches', 'analytics.view', Boolean(service)), '/analytics'),
}, },
{ {
key: 'activity_registrations', label: '活动报名', value: numberOf(registrationSources, 'totalRegistrations'), key: 'activity_registrations', label: '活动报名', value: hasWorkbenchMetric('activityRegistrations') ? workbenchValue('activityRegistrations') : numberOf(registrationSources, 'totalRegistrations'),
trendText: service ? '累计有效报名' : '暂无授权数据', tone: 'red', available: Boolean(service), route: '/activities/interests', trendText: workbenchAvailable('activityRegistrations') ? `${workbench?.scopeLabel || '当前范围'}活动报名` : (service ? '累计有效报名' : '暂无授权数据'), tone: 'red', available: metricAvailable('activityRegistrations', 'activity.review_registration', Boolean(service)), status: metricStatus('activityRegistrations', 'activity.review_registration', Boolean(service)), route: metricRoute(metricStatus('activityRegistrations', 'activity.review_registration', Boolean(service)), '/activities/interests'),
}, },
// 保留 forbidden 卡片以明确展示“无权限”;其数值只在 available 时渲染,route 由 metricRoute 置空。
] ]
const metricByKey = (key: string) => metrics.find(metric => metric.key === key)
const todos = ([ const todos = ([
{ key: 'review', title: '资料审核', description: '待领取与审核中资料', count: metrics[0].value, actionText: '立即审核', route: '/review', tone: 'rose', available: metrics[0].available }, { key: 'review', title: '资料审核', description: '待领取与审核中资料', count: metricByKey('pending_reviews')?.value || 0, actionText: '立即审核', route: '/review', tone: 'rose', available: metricByKey('pending_reviews')?.available || false },
{ key: 'counseling', title: '咨询预约', description: '咨询申请等待受理或跟进', count: metrics[2].value, actionText: '查看服务', route: '/counseling', tone: 'blue', available: metrics[2].available }, { key: 'counseling', title: '咨询预约', description: '咨询申请等待受理或跟进', count: metricByKey('counseling_todos')?.value || 0, actionText: '查看服务', route: '/counseling', tone: 'blue', available: metricByKey('counseling_todos')?.available || false },
{ key: 'appointment', title: '到站预约', description: '线下服务预约等待确认', count: metrics[3].value, actionText: '管理预约', route: '/service-appointment', tone: 'green', available: metrics[3].available }, { key: 'appointment', title: '到站预约', description: '线下服务预约等待确认', count: metricByKey('station_appointments')?.value || 0, actionText: '管理预约', route: '/service-appointment', tone: 'green', available: metricByKey('station_appointments')?.available || false },
{ key: 'matchmaker', title: '红娘服务', description: metrics[1].trendText, count: metrics[1].value, actionText: '查看服务', route: '/matchmaker', tone: 'amber', available: metrics[1].available }, { key: 'matchmaker', title: '红娘服务', description: metricByKey('matchmaker_todos')?.trendText || '暂无授权数据', count: metricByKey('matchmaker_todos')?.value || 0, actionText: permissions.includes('permission.manage') ? '统计' : '查看服务', route: permissions.includes('permission.manage') ? '' : '/matchmaker', tone: 'amber', available: metricByKey('matchmaker_todos')?.available || false },
{ key: 'risk', title: '举报处理', description: '风险举报等待工作人员处理', count: numberOf(core || undefined, 'pendingReports'), actionText: '去处理', route: '/risk/reports', tone: 'amber', available: Boolean(core) }, { key: 'risk', title: '举报处理', description: '风险举报等待工作人员处理', count: numberOf(core || undefined, 'pendingReports'), actionText: '去处理', route: '/risk/reports', tone: 'amber', available: Boolean(core) },
] as DashboardTodo[]).filter((item) => item.available) ] as DashboardTodo[]).filter((item) => item.available)
@@ -192,8 +206,8 @@ export async function loadDashboard(permissions: string[]): Promise<DashboardMod
{ key: 'offline_success', label: '线下牵手成功', value: numberOf(effectiveConnections, 'successConnections'), max: numberOf(effectiveConnections, 'activeMatches'), unit: '对', available: Boolean(service) }, { key: 'offline_success', label: '线下牵手成功', value: numberOf(effectiveConnections, 'successConnections'), max: numberOf(effectiveConnections, 'activeMatches'), unit: '对', available: Boolean(service) },
] as DashboardEffectivenessItem[]).filter((item) => item.available) ] as DashboardEffectivenessItem[]).filter((item) => item.available)
const rejectedCount = [coreResult, serviceResult, activityResult, matchmakerResult, reviewResult, managementResult].filter((item) => item.status === 'rejected').length const rejectedCount = [workbenchResult, coreResult, serviceResult, activityResult, matchmakerResult, reviewResult, managementResult].filter((item) => item.status === 'rejected').length
const anyData = analyticsAvailable || Boolean(activityRows) || matchmakerAvailable || Boolean(reviews) const anyData = Boolean(workbench) || analyticsAvailable || Boolean(activityRows) || matchmakerAvailable || Boolean(reviews)
const notice = !anyData const notice = !anyData
? (rejectedCount ? '工作台数据加载失败,请稍后重试' : '当前账号暂无工作台统计权限') ? (rejectedCount ? '工作台数据加载失败,请稍后重试' : '当前账号暂无工作台统计权限')
: (rejectedCount ? '部分模块暂时不可用,其余数据已正常加载' : '') : (rejectedCount ? '部分模块暂时不可用,其余数据已正常加载' : '')
+3 -1
View File
@@ -9,6 +9,7 @@ interface OperatorInfo {
username: string username: string
permissions: string[] permissions: string[]
organizationId: string | null organizationId: string | null
roles: string[]
} }
const previewRolePermissions: Record<string, string[]> = { const previewRolePermissions: Record<string, string[]> = {
@@ -22,7 +23,7 @@ export const useAuthStore = defineStore('auth', () => {
const previewParams = import.meta.env.DEV ? new URLSearchParams(window.location.search) : null const previewParams = import.meta.env.DEV ? new URLSearchParams(window.location.search) : null
const previewRole = previewParams?.get('preview') || '' const previewRole = previewParams?.get('preview') || ''
const previewOperator: OperatorInfo | null = previewRolePermissions[previewRole] const previewOperator: OperatorInfo | null = previewRolePermissions[previewRole]
? { operatorId: 'ui-preview', name: '界面验收员', username: 'preview', permissions: previewRolePermissions[previewRole], organizationId: null } ? { operatorId: 'ui-preview', name: '界面验收员', username: 'preview', permissions: previewRolePermissions[previewRole], organizationId: null, roles: [previewRole] }
: null : null
const token = ref<string>(previewOperator ? 'ui-preview-only' : '') const token = ref<string>(previewOperator ? 'ui-preview-only' : '')
@@ -42,6 +43,7 @@ export const useAuthStore = defineStore('auth', () => {
username: data.username, username: data.username,
permissions: data.permissions || [], permissions: data.permissions || [],
organizationId: data.organizationId ?? null, organizationId: data.organizationId ?? null,
roles: Array.isArray(data.roles) ? data.roles : (Array.isArray(data.roleCodes) ? data.roleCodes : [data.roleCode || data.role].filter(Boolean)),
} }
// 同步内存态令牌(供 axios 拦截器注入 Authorization 头;生产同源场景 Cookie 也会自动携带) // 同步内存态令牌(供 axios 拦截器注入 Authorization 头;生产同源场景 Cookie 也会自动携带)
setAccessToken(token.value) setAccessToken(token.value)
+25
View File
@@ -0,0 +1,25 @@
# Phase V1.2 Task 4 完成报告
## 本次完成
- 工作台保留无权限指标卡片,`available=false` 时不使用其他接口的不同数据范围作为数值回退,路由保持为空;授权接口返回真实 `0` 时保留 `0`。
- 角色名称优先使用服务端下发的 `roles`、`roleCodes`、`roleCode` 或 `role`,避免仅按权限顺序误判红娘组长等角色。
- 平台管理员/超级管理员继续隐藏独立“公益红娘”和“红娘服务”菜单。
- 增加工作台权限契约断言,覆盖 forbidden 指标、空路由和真实零值。
## 修改文件
- `admin-web/src/services/dashboard.ts`
- `admin-web/src/stores/auth.ts`
- `admin-web/src/layouts/MainLayout.vue`
- `tests/admin-dashboard-behavior.cjs`
## 测试结果
- `pnpm --dir admin-web type-check`:通过
- `pnpm --dir admin-web build`:通过(已有 CSS URL 与大 chunk 警告)
- `node tests/admin-dashboard-behavior.cjs`:通过
## 风险 / 注意事项
- 当前后端登录响应尚未在本任务中新增 `roles` 字段;字段存在时前端优先使用,缺失时继续按权限兼容判定。
+12 -1
View File
@@ -5,6 +5,7 @@ const ts = require('../admin-web/node_modules/typescript');
(async () => { (async () => {
let pending = 2; let pending = 2;
let workbench = null;
const responses = { const responses = {
'/operator/analytics/dashboard': { totalUsers: 12, approvedProfiles: 10 }, '/operator/analytics/dashboard': { totalUsers: 12, approvedProfiles: 10 },
'/operator/analytics/v11-dashboard': { localEligibility: { pending: 99 } }, '/operator/analytics/v11-dashboard': { localEligibility: { pending: 99 } },
@@ -18,7 +19,7 @@ const ts = require('../admin-web/node_modules/typescript');
vm.runInNewContext(ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS } }).outputText, { vm.runInNewContext(ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS } }).outputText, {
exports, exports,
require: () => ({ default: { get: async url => ({ success: true, data: require: () => ({ default: { get: async url => ({ success: true, data:
url === '/operator/reviews/stats' ? { pending, processing: 1 } : responses[url] }) } }), url === '/operator/workbench/dashboard' ? workbench : (url === '/operator/reviews/stats' ? { pending, processing: 1 } : responses[url]) }) } }),
}); });
const perms = ['analytics.view', 'activity.view', 'matchmaker.view', 'matchmaker.assign', 'review.view']; const perms = ['analytics.view', 'activity.view', 'matchmaker.view', 'matchmaker.assign', 'review.view'];
let model = await exports.loadDashboard(perms); let model = await exports.loadDashboard(perms);
@@ -30,6 +31,16 @@ const ts = require('../admin-web/node_modules/typescript');
pending = 1; pending = 1;
model = await exports.loadDashboard(perms); model = await exports.loadDashboard(perms);
assert.equal(model.metrics.find(m => m.key === 'pending_reviews').value, 2); assert.equal(model.metrics.find(m => m.key === 'pending_reviews').value, 2);
workbench = { metrics: {
pendingReviews: { value: 7, available: false },
matchmakerTodos: { value: 0, available: true },
} };
model = await exports.loadDashboard(perms);
const unavailableReview = model.metrics.find(m => m.key === 'pending_reviews');
assert.equal(unavailableReview.value, 7);
assert.equal(unavailableReview.available, false);
assert.equal(unavailableReview.route, '');
assert.equal(model.metrics.find(m => m.key === 'matchmaker_todos').value, 0);
assert.match(fs.readFileSync('admin-web/src/views/DashboardView.vue', 'utf8'), /onActivated\(retryLoad\)/); assert.match(fs.readFileSync('admin-web/src/views/DashboardView.vue', 'utf8'), /onActivated\(retryLoad\)/);
console.log('PASS: 审核队列真实计数、审核后减一、管理服务待办与跟进、缓存返回刷新'); console.log('PASS: 审核队列真实计数、审核后减一、管理服务待办与跟进、缓存返回刷新');
})().catch(error => { console.error(error); process.exitCode = 1; }); })().catch(error => { console.error(error); process.exitCode = 1; });