Files
Migu2.0/frontends/apps/simple-platform-vue/src/mock/rbac.ts
T

282 lines
12 KiB
TypeScript

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<RbacUserView & { password?: string }>
}
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<string>()
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<string, string> = {
'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<RbacRole[]> {
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<RbacRole> {
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<void> {
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<RbacUserView[]> {
await delay(60)
return state.users.map(toView)
}
export async function mockRbacCreateUser(payload: CreateUserPayload): Promise<RbacUserView> {
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<RbacUserView> {
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<void> {
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<void> {
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<string>()
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]
}