282 lines
12 KiB
TypeScript
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]
|
|
}
|