2026-05-29 18:16:34 +08:00
|
|
|
import type { LoginRequest, LoginResponse, EffectivePermissions } from '@/types/auth'
|
|
|
|
|
import type { Site, Track } from '@/types/map'
|
|
|
|
|
import type { Car } from '@/types/car'
|
|
|
|
|
import type { Mission } from '@/types/mission'
|
|
|
|
|
import type { OpsAuditEntry } from '@/types/ops'
|
|
|
|
|
import type { ConfigEnvelope, ConfigSection } from '@/types/config'
|
|
|
|
|
import { SITES } from './data/sites'
|
|
|
|
|
import { TRACKS } from './data/tracks'
|
|
|
|
|
import { CARS } from './data/cars'
|
|
|
|
|
import { MISSIONS } from './data/missions'
|
|
|
|
|
import { CONFIG_DEFAULTS } from './data/configs'
|
2026-08-26 17:50:02 +08:00
|
|
|
import { mockComputeAllowedPages, mockResolveLoginScope } from './rbac'
|
2026-05-29 18:16:34 +08:00
|
|
|
|
|
|
|
|
const PLATFORM_OPS = ['*']
|
|
|
|
|
|
|
|
|
|
const RCS_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'
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
function nowIso() { return new Date().toISOString() }
|
|
|
|
|
|
|
|
|
|
function buildPerm(scope: 'Platform' | 'RCSMonitor', username: string): EffectivePermissions {
|
|
|
|
|
if (scope === 'Platform') {
|
|
|
|
|
return {
|
|
|
|
|
userId: `u-${username}`,
|
|
|
|
|
version: 1,
|
|
|
|
|
allowedOps: PLATFORM_OPS,
|
2026-05-29 23:51:57 +08:00
|
|
|
allowedPages: mockComputeAllowedPages(username, 'Platform'),
|
2026-05-29 18:16:34 +08:00
|
|
|
visibleWidgets: [
|
|
|
|
|
{ widgetId: 'MapEditor', visibility: 'interactive' },
|
|
|
|
|
{ widgetId: 'CadToolbar', visibility: 'interactive' },
|
|
|
|
|
{ widgetId: 'CarPanel', visibility: 'interactive' },
|
|
|
|
|
{ widgetId: 'MissionEditor', visibility: 'interactive' },
|
|
|
|
|
{ widgetId: 'OpsActionPanel', visibility: 'interactive' },
|
|
|
|
|
{ widgetId: 'ConfigCenter', visibility: 'interactive' }
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
userId: `u-${username}`,
|
|
|
|
|
version: 1,
|
|
|
|
|
allowedOps: RCS_OPS,
|
2026-05-29 23:51:57 +08:00
|
|
|
allowedPages: mockComputeAllowedPages(username, 'RCSMonitor'),
|
2026-05-29 18:16:34 +08:00
|
|
|
visibleWidgets: [
|
|
|
|
|
{ widgetId: 'MapEditor', visibility: 'readonly' },
|
|
|
|
|
{ widgetId: 'CadToolbar', visibility: 'hidden' },
|
|
|
|
|
{ widgetId: 'CarPanel', visibility: 'readonly' },
|
|
|
|
|
{ widgetId: 'MissionEditor', visibility: 'readonly' },
|
|
|
|
|
{ widgetId: 'OpsActionPanel', visibility: 'interactive' },
|
|
|
|
|
{ widgetId: 'ConfigCenter', visibility: 'hidden' }
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function mockLogin(req: LoginRequest): Promise<LoginResponse> {
|
|
|
|
|
await delay(150)
|
|
|
|
|
if (!req.username) throw new Error('用户名不能为空')
|
2026-08-26 17:50:02 +08:00
|
|
|
const scope = mockResolveLoginScope(req.username)
|
|
|
|
|
const token = `mock-jwt.${scope}.${req.username}.${Date.now()}`
|
|
|
|
|
// mock 无法真正拉起内核;Simple3 仅 Web,默认 WebOnly。
|
|
|
|
|
const runMode: LoginResponse['runMode'] =
|
|
|
|
|
req.launchMode === 'DesktopAndWeb' ? 'WebEnabled' : 'WebOnly'
|
2026-05-29 18:16:34 +08:00
|
|
|
return {
|
|
|
|
|
token,
|
|
|
|
|
user: {
|
|
|
|
|
id: `u-${req.username}`,
|
|
|
|
|
username: req.username,
|
|
|
|
|
displayName: req.username === 'admin' ? '系统管理员' : (req.username === 'ops' ? '运营人员' : req.username),
|
2026-08-26 17:50:02 +08:00
|
|
|
roles: scope === 'Platform' ? ['role-admin'] : ['role-ops']
|
2026-05-29 18:16:34 +08:00
|
|
|
},
|
2026-08-26 17:50:02 +08:00
|
|
|
scope,
|
|
|
|
|
effectivePermissions: buildPerm(scope, req.username),
|
2026-05-29 18:16:34 +08:00
|
|
|
runMode
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function mockSites(): Promise<Site[]> { await delay(80); return [...SITES] }
|
|
|
|
|
export async function mockTracks(): Promise<Track[]> { await delay(80); return [...TRACKS] }
|
|
|
|
|
export async function mockCars(): Promise<Car[]> { await delay(80); return [...CARS] }
|
|
|
|
|
export async function mockMissions(): Promise<Mission[]> { await delay(80); return [...MISSIONS] }
|
|
|
|
|
|
|
|
|
|
const MOCK_CONFIG_STORAGE_KEY = 'simple-platform-mock-config'
|
|
|
|
|
|
|
|
|
|
const memConfig = new Map<ConfigSection, ConfigEnvelope>()
|
|
|
|
|
|
|
|
|
|
function loadPersistedConfig(): Partial<Record<ConfigSection, ConfigEnvelope>> {
|
|
|
|
|
try {
|
|
|
|
|
const raw = localStorage.getItem(MOCK_CONFIG_STORAGE_KEY)
|
|
|
|
|
if (raw) return JSON.parse(raw) as Partial<Record<ConfigSection, ConfigEnvelope>>
|
|
|
|
|
} catch { /* ignore */ }
|
|
|
|
|
return {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function persistAllConfig() {
|
|
|
|
|
try {
|
|
|
|
|
const obj: Partial<Record<ConfigSection, ConfigEnvelope>> = {}
|
|
|
|
|
for (const [k, v] of memConfig.entries()) obj[k] = v
|
|
|
|
|
localStorage.setItem(MOCK_CONFIG_STORAGE_KEY, JSON.stringify(obj))
|
|
|
|
|
} catch { /* ignore */ }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function ensureSection(section: ConfigSection): ConfigEnvelope {
|
|
|
|
|
let cur = memConfig.get(section)
|
|
|
|
|
if (!cur) {
|
|
|
|
|
const persisted = loadPersistedConfig()[section]
|
|
|
|
|
cur = persisted ?? {
|
|
|
|
|
section,
|
|
|
|
|
version: 1,
|
|
|
|
|
updatedAt: nowIso(),
|
|
|
|
|
payload: CONFIG_DEFAULTS[section]
|
|
|
|
|
}
|
|
|
|
|
memConfig.set(section, cur)
|
|
|
|
|
}
|
|
|
|
|
return cur
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 启动时从 localStorage 恢复,避免刷新丢配置
|
|
|
|
|
for (const section of Object.keys(CONFIG_DEFAULTS) as ConfigSection[]) {
|
|
|
|
|
ensureSection(section)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function mockGetConfig<T>(section: ConfigSection): Promise<ConfigEnvelope<T>> {
|
|
|
|
|
await delay(60)
|
|
|
|
|
return ensureSection(section) as ConfigEnvelope<T>
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function mockPutConfig<T>(section: ConfigSection, payload: T): Promise<ConfigEnvelope<T>> {
|
|
|
|
|
await delay(80)
|
|
|
|
|
const cur = ensureSection(section)
|
|
|
|
|
const next: ConfigEnvelope<T> = { section, version: cur.version + 1, updatedAt: nowIso(), payload }
|
|
|
|
|
memConfig.set(section, next as ConfigEnvelope)
|
|
|
|
|
persistAllConfig()
|
|
|
|
|
return next
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-26 17:50:02 +08:00
|
|
|
function currentMockUsername(): string {
|
|
|
|
|
try {
|
|
|
|
|
const raw = localStorage.getItem('simple.auth.user')
|
|
|
|
|
const u = raw ? JSON.parse(raw) as { username?: string } : null
|
|
|
|
|
return u?.username?.trim() || 'mock-user'
|
|
|
|
|
} catch {
|
|
|
|
|
return 'mock-user'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-29 18:16:34 +08:00
|
|
|
const audits: OpsAuditEntry[] = [
|
|
|
|
|
{ id: 'A001', ts: nowIso(), user: 'ops', scope: 'RCSMonitor', opCode: 'ops.car.pause', target: 'C01', result: 'ok' },
|
2026-08-26 17:50:02 +08:00
|
|
|
{ id: 'A002', ts: nowIso(), user: 'ops', scope: 'RCSMonitor', opCode: 'ops.task.cancel', target: 'M03', result: 'ok' },
|
|
|
|
|
{ id: 'A003', ts: nowIso(), user: 'admin', scope: 'Platform', opCode: 'ops.car.resetSession', target: 'C02', result: 'failed', message: '示例:他人记录,运营账号不可见' }
|
2026-05-29 18:16:34 +08:00
|
|
|
]
|
|
|
|
|
|
|
|
|
|
export async function mockOpsExecute(req: { opCode: string; targetId: string; reason?: string; idempotencyKey?: string }) {
|
|
|
|
|
await delay(120)
|
|
|
|
|
const id = `A${String(audits.length + 1).padStart(3, '0')}`
|
2026-08-26 17:50:02 +08:00
|
|
|
audits.unshift({
|
|
|
|
|
id,
|
|
|
|
|
ts: nowIso(),
|
|
|
|
|
user: currentMockUsername(),
|
|
|
|
|
scope: 'mock',
|
|
|
|
|
opCode: req.opCode,
|
|
|
|
|
target: req.targetId,
|
|
|
|
|
result: 'ok',
|
|
|
|
|
message: req.reason
|
|
|
|
|
})
|
2026-05-29 18:16:34 +08:00
|
|
|
return { ok: true, auditId: id }
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-26 17:50:02 +08:00
|
|
|
export async function mockOpsAudits(): Promise<OpsAuditEntry[]> {
|
|
|
|
|
await delay(60)
|
|
|
|
|
const me = currentMockUsername().toLowerCase()
|
|
|
|
|
return audits.filter((a) => a.user.toLowerCase() === me)
|
|
|
|
|
}
|
2026-05-29 18:16:34 +08:00
|
|
|
|
|
|
|
|
function delay(ms: number) { return new Promise((r) => setTimeout(r, ms)) }
|