import type { RbacRole, RbacUserView, RbacCatalog, PageDef, OpDef, WidgetDef, SaveRolePayload, CreateUserPayload, UpdateUserPayload } from '@/types/rbac' /** * 前端 mock 模式(VITE_USE_MOCK=true)下的 RBAC 数据与 CRUD,内存 + localStorage 持久化。 * 与后端 MiGu.Server/Auth/PageCatalog.cs、RbacController KnownOps/Widgets 保持对齐, * 让无后端时也能完整调试「权限与角色」管理页与按页面权限的菜单/路由控制。 */ const PAGES: PageDef[] = [ { key: 'admin-dashboard', label: '总览', group: '概览', scope: 'Platform' }, { key: 'admin-setup', label: '初始配置', group: '概览', scope: 'Platform' }, { key: 'admin-map-monitor', label: '地图监控', group: '概览', scope: 'Platform' }, { key: 'admin-tasks', label: '任务管理', group: '概览', scope: 'Platform' }, { key: 'admin-alarms', label: '报警管理', group: '概览', scope: 'Platform' }, { key: 'admin-maps', label: '地图管理', group: '设计与编排', scope: 'Platform' }, { key: 'admin-map-editor', label: '地图编辑', group: '设计与编排', scope: 'Platform' }, { key: 'admin-project-properties', label: '项目属性', group: '设计与编排', scope: 'Platform' }, { key: 'admin-tracks', label: '场景管理', group: '设计与编排', scope: 'Platform' }, { key: 'admin-cars', label: '车辆管理', group: '设计与编排', scope: 'Platform' }, { key: 'admin-processes', label: '进程管理', group: '设计与编排', scope: 'Platform' }, { key: 'admin-scripts', label: '脚本管理', group: '设计与编排', scope: 'Platform' }, { key: 'admin-task-templates', label: '任务编排', group: '设计与编排', scope: 'Platform' }, { key: 'admin-simple-fields', label: '字段管理', group: '设计与编排', scope: 'Platform' }, { key: 'admin-config-strategy', label: '调度策略', group: '平台配置中心', scope: 'Platform' }, { key: 'admin-vehicle-hub', label: '车辆运维', group: '平台配置中心', scope: 'Platform' }, { key: 'admin-config-facility', label: '设备接入', group: '平台配置中心', scope: 'Platform' }, { key: 'admin-config-warehouse', label: '库位管理', group: '平台配置中心', scope: 'Platform' }, { key: 'admin-config-business', label: '业务与集成', group: '平台配置中心', scope: 'Platform' }, { key: 'admin-config-ops-center', label: '运维与回放', group: '平台配置中心', scope: 'Platform' }, { key: 'admin-config-system-center', label: '系统与权限', group: '平台配置中心', scope: 'Platform' }, { key: 'monitor-dashboard', label: '运营总览', group: '运营监控', scope: 'RCSMonitor' }, { key: 'monitor-vehicle-hub', label: '车辆运维', group: '运营监控', scope: 'RCSMonitor' }, { key: 'monitor-map', label: '地图监控', group: '运营监控', scope: 'RCSMonitor' }, { key: 'monitor-ops', label: '运维操作', group: '运营监控', scope: 'RCSMonitor' }, { key: 'monitor-notes', label: '运营备注', group: '运营监控', scope: 'RCSMonitor' } ] const OPS: OpDef[] = [ { code: '*', label: '全部操作(通配)' }, { code: 'ops.car.pause', label: '车辆 · 暂停' }, { code: 'ops.car.resume', label: '车辆 · 恢复' }, { code: 'ops.car.gohome', label: '车辆 · 回库' }, { code: 'ops.car.resetSession', label: '车辆 · 重置会话' }, { code: 'ops.car.manualCharge', label: '车辆 · 手动充电' }, { code: 'ops.task.pause', label: '任务 · 暂停' }, { code: 'ops.task.cancel', label: '任务 · 取消' }, { code: 'ops.task.reassign', label: '任务 · 改派' }, { code: 'ops.task.boostPriority', label: '任务 · 提升优先级' }, { code: 'monitor.note.write', label: '监控 · 写运营备注' }, { code: 'auth.manage', label: '系统 · 权限与角色管理' } ] const WIDGETS: WidgetDef[] = [ { id: 'MapEditor', label: '地图编辑器' }, { id: 'CadToolbar', label: 'CAD 工具栏' }, { id: 'CarPanel', label: '车辆面板' }, { id: 'MissionEditor', label: '任务编辑器' }, { id: 'OpsActionPanel', label: '运维操作面板' }, { id: 'ConfigCenter', label: '配置中心' } ] interface RbacState { roles: RbacRole[] users: Array } const STORAGE_KEY = 'simple-platform-mock-rbac' function seed(): RbacState { return { roles: [ { id: 'role-admin', name: '超级管理员', description: '拥有全部页面与操作权限的内置角色', scope: '*', pages: ['*'], ops: ['*'], widgetGrants: [], system: true }, { id: 'role-ops', name: '运营人员', description: '运营监控端默认角色:可执行运维操作、查看监控', scope: 'RCSMonitor', pages: ['monitor-dashboard', 'monitor-map', 'monitor-ops', 'monitor-notes', 'monitor-vehicle-hub'], ops: [ 'ops.car.pause', 'ops.car.resume', 'ops.car.gohome', 'ops.car.resetSession', 'ops.car.manualCharge', 'ops.task.pause', 'ops.task.cancel', 'ops.task.reassign', 'ops.task.boostPriority', 'monitor.note.write' ], widgetGrants: [ { widgetId: 'MapEditor', visibility: 'readonly' }, { widgetId: 'CadToolbar', visibility: 'hidden' }, { widgetId: 'CarPanel', visibility: 'readonly' }, { widgetId: 'MissionEditor', visibility: 'readonly' }, { widgetId: 'OpsActionPanel', visibility: 'interactive' }, { widgetId: 'ConfigCenter', visibility: 'hidden' } ], system: true } ], users: [ { id: 'u-admin', username: 'admin', displayName: '系统管理员', enabled: true, roleIds: ['role-admin'], scopes: [] }, { id: 'u-ops', username: 'ops', displayName: '运营人员', enabled: true, roleIds: ['role-ops'], scopes: [] } ] } } function load(): RbacState { try { const raw = localStorage.getItem(STORAGE_KEY) if (raw) return JSON.parse(raw) as RbacState } catch { /* ignore */ } const s = seed() persist(s) return s } let state: RbacState = load() function persist(s: RbacState = state) { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(s)) } catch { /* ignore */ } } function scopesOf(roleIds: string[]): string[] { const set = new Set() for (const id of roleIds) { const r = state.roles.find((x) => x.id === id) if (!r) continue if (r.scope === '*') { set.add('Platform'); set.add('RCSMonitor') } else set.add(r.scope) } return [...set] } function toView(u: RbacUserView & { password?: string }): RbacUserView { return { id: u.id, username: u.username, displayName: u.displayName, enabled: u.enabled, roleIds: [...u.roleIds], scopes: scopesOf(u.roleIds) } } function newId() { return Math.random().toString(36).slice(2, 10) } function delay(ms: number) { return new Promise((r) => setTimeout(r, ms)) } function sanitizePages(pages: string[]): string[] { if (pages.includes('*')) return ['*'] const valid = new Set(PAGES.map((p) => p.key)) const legacy: Record = { 'admin-config-vehicle': 'admin-vehicle-hub', 'admin-config-fleet': 'admin-vehicle-hub', // 会话 16:配置中心收敛为 6 个聚合页,旧页面 Key 迁移到对应聚合页 Key。 'admin-config-routing': 'admin-config-strategy', 'admin-config-task': 'admin-config-strategy', 'admin-config-traffic': 'admin-config-strategy', 'admin-config-charge': 'admin-config-strategy', 'admin-config-device': 'admin-config-facility', 'admin-config-location': 'admin-config-warehouse', 'admin-config-integrations': 'admin-config-business', 'admin-config-scenario': 'admin-config-business', 'admin-config-widget': 'admin-config-business', 'admin-playback': 'admin-config-ops-center', 'admin-config-ops': 'admin-config-ops-center', 'admin-config-logs': 'admin-config-ops-center', 'admin-config-map-monitor': 'admin-config-ops-center', 'admin-config-system': 'admin-config-system-center', 'admin-config-auth': 'admin-config-system-center' } return [...new Set(pages.map((p) => legacy[p] ?? p).filter((p) => valid.has(p)))] } export function mockRbacCatalog(): RbacCatalog { return { pages: PAGES, ops: OPS, widgets: WIDGETS, scopes: [ { value: 'Platform', label: '管理端 (Platform)' }, { value: 'RCSMonitor', label: '运营端 (RCSMonitor)' }, { value: '*', label: '通用 (全部域)' } ] } } export async function mockRbacRoles(): Promise { await delay(60) return state.roles.map((r) => ({ ...r, pages: [...r.pages], ops: [...r.ops], widgetGrants: [...r.widgetGrants] })) } export async function mockRbacSaveRole(payload: SaveRolePayload): Promise { await delay(80) if (!payload.name?.trim()) throw new Error('角色名称不能为空') const role: RbacRole = { id: payload.id ?? `role-${newId()}`, name: payload.name.trim(), description: payload.description?.trim() ?? '', scope: payload.scope, pages: sanitizePages(payload.pages ?? []), ops: [...new Set(payload.ops ?? [])], widgetGrants: payload.widgetGrants ?? [], system: false } const idx = payload.id ? state.roles.findIndex((r) => r.id === payload.id) : -1 if (idx >= 0) { role.system = state.roles[idx].system; state.roles[idx] = role } else state.roles.push(role) persist() return role } export async function mockRbacDeleteRole(id: string): Promise { await delay(60) const role = state.roles.find((r) => r.id === id) if (!role) throw new Error('角色不存在') if (role.system) throw new Error('内置系统角色不可删除') const inUse = state.users.filter((u) => u.roleIds.includes(id)).map((u) => u.username) if (inUse.length) throw new Error(`角色仍被 ${inUse.length} 个用户使用(${inUse.slice(0, 5).join(', ')}),请先解除关联`) state.roles = state.roles.filter((r) => r.id !== id) persist() } export async function mockRbacUsers(): Promise { await delay(60) return state.users.map(toView) } export async function mockRbacCreateUser(payload: CreateUserPayload): Promise { await delay(80) if (!payload.username?.trim()) throw new Error('用户名不能为空') if (!payload.password) throw new Error('初始密码不能为空') if (state.users.some((u) => u.username.toLowerCase() === payload.username.trim().toLowerCase())) throw new Error(`用户名 ${payload.username} 已存在`) const valid = new Set(state.roles.map((r) => r.id)) const u = { id: `u-${newId()}`, username: payload.username.trim(), displayName: payload.displayName?.trim() || payload.username.trim(), enabled: payload.enabled, roleIds: [...new Set((payload.roleIds ?? []).filter((r) => valid.has(r)))], scopes: [] as string[], password: payload.password } state.users.push(u) persist() return toView(u) } export async function mockRbacUpdateUser(id: string, payload: UpdateUserPayload): Promise { await delay(80) const u = state.users.find((x) => x.id === id) if (!u) throw new Error('用户不存在') if (payload.displayName !== undefined) u.displayName = payload.displayName.trim() if (payload.roleIds !== undefined) { const valid = new Set(state.roles.map((r) => r.id)) u.roleIds = [...new Set(payload.roleIds.filter((r) => valid.has(r)))] } if (payload.enabled !== undefined) u.enabled = payload.enabled persist() return toView(u) } export async function mockRbacSetPassword(id: string, password: string): Promise { await delay(60) const u = state.users.find((x) => x.id === id) if (!u) throw new Error('用户不存在') if (!password) throw new Error('密码不能为空') u.password = password persist() } export async function mockRbacDeleteUser(id: string): Promise { await delay(60) if (!state.users.some((u) => u.id === id)) throw new Error('用户不存在') state.users = state.users.filter((u) => u.id !== id) persist() } /** 供 mockLogin 计算 allowedPages:按 username + scope 取角色 pages 并集;未知用户给该 scope 全部页面。 */ export function mockComputeAllowedPages(username: string, scope: 'Platform' | 'RCSMonitor'): string[] { const scopeKeys = PAGES.filter((p) => p.scope === scope).map((p) => p.key) const u = state.users.find((x) => x.username.toLowerCase() === username.toLowerCase()) if (!u) return scopeKeys const roles = state.roles.filter((r) => u.roleIds.includes(r.id) && (r.scope === '*' || r.scope === scope)) const set = new Set() for (const r of roles) { if (r.pages.includes('*')) scopeKeys.forEach((k) => set.add(k)) else r.pages.filter((p) => scopeKeys.includes(p)).forEach((k) => set.add(k)) } return [...set] }