feat: 迁入 MiGu.Server、平台前端与车辆列表 reflection 回退

从 Simple-FR 拆出 Platform.Server 并重命名为 MiGu.Server;frontends 源码与构建脚本迁入本仓库。地图监控在 projection/cars 失败或为空时回退 reflection 车辆列表;Simple 仓库已移除旧 Platform.Server。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zhaowei.huang
2026-05-29 18:16:34 +08:00
co-authored by Cursor
parent 804aa68ade
commit 42978930ca
280 changed files with 30046 additions and 8 deletions
@@ -0,0 +1,12 @@
import type { Car } from '@/types/car'
const NOW = new Date().toISOString()
export const CARS: Car[] = [
{ id: 'C01', name: 'AGV-001', typeName: 'SimpleLite.RCS.CarTypes.DummyCar', x: 1200, y: 2100, theta: 0, batterySoc: 0.86, state: 'running', missionId: 'M01', lastUpdate: NOW, group: 'A 区', ip: '10.0.1.11' },
{ id: 'C02', name: 'AGV-002', typeName: 'SimpleLite.RCS.CarTypes.DummyCar', x: 2900, y: 2100, theta: 90, batterySoc: 0.42, state: 'running', missionId: 'M02', lastUpdate: NOW, group: 'A 区', ip: '10.0.1.12' },
{ id: 'C03', name: 'AGV-003', typeName: 'SimpleLite.RCS.CarTypes.DummyCar', x: 1000, y: 4900, theta: 180, batterySoc: 1.0, state: 'charging', lastUpdate: NOW, group: 'A 区', ip: '10.0.1.13' },
{ id: 'C04', name: 'AGV-004', typeName: 'SimpleLite.RCS.CarTypes.GhostCar', x: 7000, y: 2050, theta: 0, batterySoc: 0.71, state: 'idle', lastUpdate: NOW, group: 'B 区', ip: '10.0.1.14' },
{ id: 'C05', name: 'AGV-005', typeName: 'SimpleLite.RCS.CarTypes.GhostCar', x: 7050, y: 4000, theta: 270, batterySoc: 0.18, state: 'fault', lastUpdate: NOW, group: 'B 区', ip: '10.0.1.15' },
{ id: 'C06', name: 'AGV-006', typeName: 'SimpleLite.RCS.CarTypes.DummyCar', x: 9000, y: 2000, theta: 0, batterySoc: 0.95, state: 'offline', lastUpdate: NOW, group: '维护', ip: '10.0.1.16' }
]
@@ -0,0 +1,179 @@
import type {
SystemConfig,
ExternalIntegrations,
RoutingPolicy,
VehicleMaintenancePolicy,
ChargePolicy,
TaskAllocationPolicy,
TrafficRule,
DeviceManagementConfig,
FleetLifecycleConfig,
ScenarioTemplateConfig,
LocationManagement,
OpsConfig,
AuthRoleConfig,
CustomWidget,
ConfigSection
} from '@/types/config'
export const DEFAULT_SYSTEM: SystemConfig = {
dispatchLoopHz: 50,
log: { level: 'info', rollDays: 7, maxSizeMB: 256 },
security: { jwtExpireMin: 1440, enableSwagger: false, corsWhitelist: ['http://localhost:5173', 'https://*.intra'] }
}
export const DEFAULT_INTEGRATIONS: ExternalIntegrations = {
mes: [{ id: 'mes-1', name: 'MES 主线', url: 'http://mes.lan/api', enabled: true }],
wms: [{ id: 'wms-1', name: 'WMS 仓储', url: 'http://wms.lan/api', enabled: true }],
rcs: []
}
export const DEFAULT_ROUTING: RoutingPolicy = {
algorithm: 'astar',
weights: { distance: 1, congestion: 0.5, turnPenalty: 0.2 },
avoidance: [{ id: 'AV1', zoneId: 'Z-NORTH', rule: 'no-entry-while-loading' }],
zoneSpeedLimits: [{ zoneId: 'Z-NARROW', maxSpeedMps: 0.5 }]
}
export const DEFAULT_VEHICLE: VehicleMaintenancePolicy = {
lowBatteryThreshold: 0.3,
criticalBatteryThreshold: 0.15,
faultReport: { enabled: true, emailTo: ['ops@example.com'] },
autoRepair: { enabled: false, cooldownSec: 600 }
}
export const DEFAULT_CHARGE: ChargePolicy = {
allowMidTaskCharge: false,
idleChargeAfterSec: 300,
priority: [
{ id: 'CP1', condition: 'soc<0.2', weight: 100 },
{ id: 'CP2', condition: 'idle>5min', weight: 30 }
]
}
export const DEFAULT_TASK: TaskAllocationPolicy = {
mode: 'leastLoad',
loadBalance: true,
maxQueuePerCar: 3
}
export const DEFAULT_TRAFFIC: TrafficRule = {
intersections: [{ id: 'IX1', siteIds: ['S006', 'S007'], mode: 'mutex' }],
mutex: [{ id: 'MZ1', zoneIds: ['Z-CROSS'] }],
yields: [{ id: 'YD1', from: 'A 区', to: 'B 区', condition: 'priority<peer' }]
}
export const DEFAULT_AUTH: AuthRoleConfig = {
roles: [
{
id: 'role-admin', name: '管理员', scope: 'Platform',
permissions: ['*'],
widgetGrants: []
},
{
id: 'role-ops', name: '运营', scope: 'RCSMonitor',
permissions: [
'ops.car.pause', 'ops.car.resume', 'ops.car.gohome',
'ops.task.pause', 'ops.task.cancel', 'ops.task.reassign',
'ops.task.boostPriority', 'monitor.note.write'
],
widgetGrants: [
{ widgetId: 'MapEditor', visibility: 'readonly' },
{ widgetId: 'CadToolbar', visibility: 'hidden' }
]
}
],
users: [
{ id: 'u-admin', username: 'admin', roles: ['role-admin'], enabled: true },
{ id: 'u-ops', username: 'ops', roles: ['role-ops'], enabled: true }
]
}
export const DEFAULT_DEVICE: DeviceManagementConfig = {
drivers: [
{ id: 'drv-elev', deviceType: '电梯', driverName: 'OpcUaElevatorDriver', version: '1.2.0' },
{ id: 'drv-chrg', deviceType: '充电桩', driverName: 'ModbusChargerDriver', version: '1.0.5' },
{ id: 'drv-cam', deviceType: '摄像头', driverName: 'OnvifCameraDriver', version: '2.1.0' }
],
devices: [
{ id: 'dev-elev-1', name: '#1 电梯', deviceType: '电梯', protocol: 'opc-ua', address: 'opc.tcp://10.0.2.20:4840', driverId: 'drv-elev', enabled: true },
{ id: 'dev-chrg-1', name: '充电桩-A1', deviceType: '充电桩', protocol: 'modbus-tcp', address: '10.0.2.30:502', driverId: 'drv-chrg', enabled: true }
],
healthPolicy: { heartbeatSec: 5, offlineSec: 30 },
alarmPolicy: {
enabled: true,
rules: [
{ level: 'warn', condition: 'offline>30s' },
{ level: 'error', condition: 'driverException' }
]
}
}
export const DEFAULT_FLEET: FleetLifecycleConfig = {
groups: [
{ id: 'G-A', name: 'A 区车队', floor: 'F1', region: 'A', carIds: ['C01', 'C02', 'C03'] },
{ id: 'G-B', name: 'B 区车队', floor: 'F1', region: 'B', carIds: ['C04', 'C05'] }
],
ota: { enabled: true, batchSize: 2, rollbackOnFail: true },
batchOps: { confirmationRequired: true, maxBatch: 10 },
networkDiag: { rttThresholdMs: 80, packetLossThreshold: 0.02 }
}
export const DEFAULT_SCENARIO: ScenarioTemplateConfig = {
templates: [
{ id: 'tpl-sps', name: 'SPS 物料配送', category: 'SPS', version: '1.0.0', baselineJson: '{}' },
{ id: 'tpl-pack', name: '电池 Pack 自动化产线', category: 'BatteryPack', version: '1.0.0', baselineJson: '{}' },
{ id: 'tpl-loop', name: '环线运行', category: 'Loop', version: '1.0.0', baselineJson: '{}' },
{ id: 'tpl-p2p', name: '点对点柔性搬运', category: 'P2P', version: '1.0.0', baselineJson: '{}' }
],
dslPolicy: { enabled: true, schemaVersion: '1' },
lowCode: { enabled: false, editor: 'json' },
versionPolicy: { keepVersions: 10, allowRollback: true }
}
export const DEFAULT_LOCATION: LocationManagement = {
locations: [
{ id: 'L01', code: 'A-01', name: 'A 区货架 1', siteId: 'S001', capacity: 20, occupied: 12 },
{ id: 'L02', code: 'A-02', name: 'A 区货架 2', siteId: 'S002', capacity: 20, occupied: 7 },
{ id: 'L03', code: 'B-01', name: 'B 区缓存', siteId: 'S003', capacity: 30, occupied: 25 }
],
inventoryRules: [{ id: 'IR1', itemType: 'PalletA', minQty: 5, maxQty: 30 }]
}
export const DEFAULT_OPS: OpsConfig = {
playback: { retentionDays: 30, samplingHz: 5 },
logRetention: { hotDays: 7, coldDays: 180 },
version: { keepReleases: 5 },
monitor: {
car: { propertyKeys: [], statusKeys: [], actionKeys: [] },
site: { propertyKeys: [], statusKeys: [], actionKeys: [] },
track: { propertyKeys: [], statusKeys: [], actionKeys: [] },
carActionByType: {}
}
}
export const DEFAULT_WIDGETS: { items: CustomWidget[] } = {
items: [
{
id: 'widget-call-button', name: '呼叫按钮', schemaJson: '{"fields":[{"name":"siteId"}]}',
layoutJson: '{"x":0,"y":0,"w":2,"h":1}', bindToScopes: ['RCSMonitor']
}
]
}
export const CONFIG_DEFAULTS: Record<ConfigSection, unknown> = {
system: DEFAULT_SYSTEM,
integrations: DEFAULT_INTEGRATIONS,
routing: DEFAULT_ROUTING,
vehicle: DEFAULT_VEHICLE,
charge: DEFAULT_CHARGE,
task: DEFAULT_TASK,
traffic: DEFAULT_TRAFFIC,
auth: DEFAULT_AUTH,
device: DEFAULT_DEVICE,
fleet: DEFAULT_FLEET,
scenario: DEFAULT_SCENARIO,
location: DEFAULT_LOCATION,
ops: DEFAULT_OPS,
widget: DEFAULT_WIDGETS
}
@@ -0,0 +1,38 @@
import type { Mission } from '@/types/mission'
const NOW = new Date().toISOString()
export const MISSIONS: Mission[] = [
{
id: 'M01', name: 'A 区送料 #1', typeName: 'SimpleLite.RCS.Missions.MoveMission',
priority: 50, status: 'running', carId: 'C01', createdAt: NOW,
steps: [
{ id: 'S1', action: 'pickup', targetSiteId: 'S001', status: 'completed' },
{ id: 'S2', action: 'transport', targetSiteId: 'S002', status: 'running' },
{ id: 'S3', action: 'dropoff', targetSiteId: 'S002', status: 'queued' }
]
},
{
id: 'M02', name: 'A→B 缓存搬运', typeName: 'SimpleLite.RCS.Missions.MoveMission',
priority: 60, status: 'running', carId: 'C02', createdAt: NOW,
steps: [
{ id: 'S1', action: 'pickup', targetSiteId: 'S002', status: 'completed' },
{ id: 'S2', action: 'transport', targetSiteId: 'S003', status: 'running' }
]
},
{
id: 'M03', name: 'B 区工位 W1 投料', typeName: 'SimpleLite.RCS.Missions.MoveMission',
priority: 70, status: 'queued', createdAt: NOW,
steps: [
{ id: 'S1', action: 'pickup', targetSiteId: 'S003', status: 'queued' },
{ id: 'S2', action: 'transport', targetSiteId: 'S008', status: 'queued' }
]
},
{
id: 'M04', name: 'W2→维护检修', typeName: 'SimpleLite.RCS.Missions.MaintenanceMission',
priority: 30, status: 'paused', carId: 'C05', createdAt: NOW,
steps: [
{ id: 'S1', action: 'goto', targetSiteId: 'S010', status: 'paused' }
]
}
]
@@ -0,0 +1,317 @@
import type {
ReflectionAssembly,
ReflectionKind,
ReflectionKindMeta,
ReflectionKv,
ReflectionMethod,
ReflectionObject,
ReflectionTypeMethods
} from '@/api/reflection'
import { CARS } from './cars'
import { MISSIONS } from './missions'
import { SITES } from './sites'
import { TRACKS } from './tracks'
const baseMethods = (typeName: string): ReflectionMethod[] => [
{
methodName: 'Reset',
label: '复位',
description: `重置 ${typeName} 内部状态`,
returnType: 'void',
hasParams: false,
params: []
},
{
methodName: 'SetEnabled',
label: '启用 / 禁用',
returnType: 'void',
hasParams: true,
params: [
{ name: 'enabled', typeName: 'Boolean', hasDefault: true, defaultValue: 'true' }
]
}
]
const carMethods: ReflectionMethod[] = [
...baseMethods('Car'),
{
methodName: 'GoToCharge',
label: '前往充电',
returnType: 'void',
hasParams: true,
params: [
{ name: 'siteId', typeName: 'Int32', hasDefault: false, defaultValue: null }
]
},
{
methodName: 'ForceStop',
label: '强制停止',
description: '紧急停车,挂起当前任务',
returnType: 'void',
hasParams: false,
params: []
}
]
const missionMethods: ReflectionMethod[] = [
...baseMethods('Mission'),
{
methodName: 'Cancel',
label: '取消任务',
returnType: 'void',
hasParams: false,
params: []
},
{
methodName: 'Reassign',
label: '重新分配',
returnType: 'void',
hasParams: true,
params: [
{ name: 'carId', typeName: 'Int32', hasDefault: false, defaultValue: null }
]
}
]
function objectsFor(kind: ReflectionKind): ReflectionObject[] {
switch (kind) {
case 'car':
case 'vehicle':
case 'script':
return CARS.map((c, i) => ({
id: i + 1,
name: c.name,
typeName: c.typeName.split('.').pop() ?? c.typeName,
layer: c.group ?? 'g',
status: c.state,
summary: `(${c.x}, ${c.y})`
}))
case 'mission':
case 'process':
return MISSIONS.map((m, i) => ({
id: i + 1,
name: m.name,
typeName: m.typeName.split('.').pop() ?? m.typeName,
status: m.status,
summary: `priority=${m.priority}`
}))
case 'site':
return SITES.map((s, i) => ({
id: i + 1,
name: s.name,
typeName: 'UISite',
layer: s.layerId ?? 'g',
summary: `(${s.x}, ${s.y})`
}))
case 'track':
return TRACKS.map((t, i) => ({
id: i + 1,
name: `${t.fromSiteId}${t.toSiteId}`,
typeName: 'UITrack',
layer: 'g',
summary: t.kind
}))
case 'map':
return [
{ id: 1, name: 'factory_floor', typeName: 'Map', layer: 'g' },
{ id: 2, name: 'grid_overlay', typeName: 'Map', layer: 'overlay' }
]
case 'special':
return [
{ id: 11, name: 'UIHelper #11', typeName: 'UIHelper', layer: 'helper' }
]
case 'scene':
// 合成 kindsite + track + special 三者合并,每行带 subKind 字段。
return [
...objectsFor('site').map<ReflectionObject>((o) => ({ ...o, subKind: 'site' })),
...objectsFor('track').map<ReflectionObject>((o) => ({ ...o, subKind: 'track' })),
...objectsFor('special').map<ReflectionObject>((o) => ({ ...o, subKind: 'special' }))
]
default:
return []
}
}
function methodsFor(kind: ReflectionKind): ReflectionMethod[] {
if (kind === 'car' || kind === 'vehicle' || kind === 'script') return carMethods
if (kind === 'mission' || kind === 'process') return missionMethods
return baseMethods(kind)
}
function statusFor(kind: ReflectionKind, id: number): ReflectionKv[] {
if (kind === 'car' || kind === 'vehicle') {
const car = CARS[id - 1]
if (!car) return []
return [
{ key: '车体_state', value: car.state },
{ key: '车体_batterySoc', value: `${Math.round(car.batterySoc * 100)}%` },
{ key: 'address', value: car.ip ?? '-' },
{ key: 'speed', value: '0.00' }
]
}
if (kind === 'mission' || kind === 'process') {
const m = MISSIONS[id - 1]
if (!m) return []
return [
{ key: 'status', value: m.status },
{ key: 'priority', value: `${m.priority}` },
{ key: 'carId', value: m.carId ?? '—' }
]
}
return []
}
function baseFieldsFor(kind: ReflectionKind, id: number): ReflectionKv[] {
if (kind === 'car' || kind === 'vehicle') {
const car = CARS[id - 1]
if (!car) return []
return [
{ key: 'name', value: car.name, locked: false },
{ key: 'group', value: car.group ?? '', locked: false },
{ key: 'batterySoc', value: `${car.batterySoc}`, locked: false }
]
}
return [{ key: 'createdAt', value: new Date().toISOString(), locked: true }]
}
// (kind, id) -> { field: value | nullnull 表示被删除) }
// 用于演示 mock 模式下 setField / deleteField / execute 的可见效果,避免页面看起来没反应。
const overrideStore = new Map<string, Map<string, string | null>>()
const executionLog = new Map<string, Array<{ method: string; params: Record<string, string>; at: string }>>()
function keyOf(kind: ReflectionKind, id: number) { return `${kind}#${id}` }
function fieldsFor(kind: ReflectionKind, id: number): ReflectionKv[] {
const base = baseFieldsFor(kind, id)
const overrides = overrideStore.get(keyOf(kind, id))
if (!overrides) return base
const merged: ReflectionKv[] = []
const seen = new Set<string>()
for (const b of base) {
seen.add(b.key)
if (overrides.has(b.key)) {
const v = overrides.get(b.key)
if (v === null) continue
merged.push({ key: b.key, value: v ?? '', locked: b.locked })
} else {
merged.push(b)
}
}
for (const [k, v] of overrides) {
if (seen.has(k)) continue
if (v === null) continue
merged.push({ key: k, value: v ?? '', locked: false })
}
return merged
}
export function mockReflectionSetField(kind: ReflectionKind, id: number, field: string, value: string) {
const k = keyOf(kind, id)
let m = overrideStore.get(k)
if (!m) { m = new Map(); overrideStore.set(k, m) }
m.set(field, value)
return { kind, id, field, value }
}
export function mockReflectionDeleteField(kind: ReflectionKind, id: number, field: string) {
const k = keyOf(kind, id)
let m = overrideStore.get(k)
if (!m) { m = new Map(); overrideStore.set(k, m) }
m.set(field, null)
return { kind, id, field }
}
export function mockReflectionExecute(kind: ReflectionKind, id: number, method: string, params: Record<string, string>) {
const k = keyOf(kind, id)
const log = executionLog.get(k) ?? []
log.unshift({ method, params, at: new Date().toISOString() })
if (log.length > 20) log.length = 20
executionLog.set(k, log)
const ps = Object.keys(params).length ? ` ${JSON.stringify(params)}` : ''
return { returnValue: `mock(${method})${ps} ok` }
}
export function mockReflectionKinds(): { kinds: ReflectionKindMeta[]; subKinds: Array<ReflectionKindMeta & { parent: string }> } {
return {
kinds: [
{ kind: 'map', count: 2, label: '地图', group: 'primary' },
{ kind: 'car', count: CARS.length, label: '车辆', group: 'primary' },
{ kind: 'process', count: MISSIONS.length, label: '进程', group: 'primary' },
{ kind: 'scene', count: SITES.length + TRACKS.length + 1, label: '场景', group: 'primary' },
{ kind: 'script', count: CARS.length, label: '脚本', group: 'primary' }
],
subKinds: [
{ kind: 'site', count: SITES.length, label: '站点', parent: 'scene' },
{ kind: 'track', count: TRACKS.length, label: '路径', parent: 'scene' },
{ kind: 'special', count: 1, label: '装饰物', parent: 'scene' },
{ kind: 'mission', count: MISSIONS.length, label: '任务', parent: 'process' }
]
}
}
export function mockReflectionAssemblies(): ReflectionAssembly[] {
return [
{ name: 'SimpleCore', version: '1.0.0' },
{ name: 'SimpleLite', version: '1.0.0' },
{ name: 'StandardScene', version: '1.4.0', location: 'plugins/StandardScene.dll' },
{ name: 'CustomFlightDeck', version: '0.1.0', location: 'plugins/CustomFlightDeck.dll' }
]
}
export function mockReflectionObjects(kind: ReflectionKind) {
return objectsFor(kind)
}
export function mockReflectionMethods(kind: ReflectionKind) {
return methodsFor(kind)
}
export function mockReflectionMethodsByType(kind: ReflectionKind): ReflectionTypeMethods[] {
const ms = methodsFor(kind)
if (kind === 'car' || kind === 'vehicle' || kind === 'script') {
return [
{ typeName: 'GhostCar', assemblyName: 'StandardScene', methods: ms },
{ typeName: 'DummyCar', assemblyName: 'SimpleLite', methods: ms.slice(0, 2) }
]
}
if (kind === 'mission' || kind === 'process') {
return [
{ typeName: 'DeliveryMission', assemblyName: 'StandardScene', methods: ms },
{ typeName: 'PatrolMission', assemblyName: 'CustomFlightDeck', methods: ms.slice(0, 2) }
]
}
return [{ typeName: kind, assemblyName: 'SimpleLite', methods: ms }]
}
export function mockReflectionStatus(kind: ReflectionKind, id: number) {
return statusFor(kind, id)
}
export function mockReflectionFields(kind: ReflectionKind, id: number) {
return fieldsFor(kind, id)
}
export function mockReflectionBundle(kind: ReflectionKind, id: number) {
const obj = objectsFor(kind).find((o) => o.id === id) ?? {
id,
name: `${kind}#${id}`,
typeName: kind
}
const kvs = fieldsFor(kind, id)
return {
kind,
id,
typeName: obj.typeName,
assembly: 'mock',
summary: obj,
methods: methodsFor(kind),
status: statusFor(kind, id),
fields: Object.fromEntries(kvs.map((f) => [f.key, f.value])),
// Mock 数据全部当 dynamicmock 模式下没有真实 [FieldMember])。新版面板期望这个字段。
fieldList: kvs.map<ReflectionKv>((f) => ({
key: f.key, value: f.value, source: 'dynamic', typeName: 'String'
}))
}
}
@@ -0,0 +1,14 @@
import type { Site } from '@/types/map'
export const SITES: Site[] = [
{ id: 'S001', name: 'A 区-入库点', x: 1000, y: 2000, layerId: 'L1', fields: { type: 'pickup' } },
{ id: 'S002', name: 'A 区-出库点', x: 3000, y: 2000, layerId: 'L1', fields: { type: 'dropoff' } },
{ id: 'S003', name: 'B 区-缓存区', x: 5000, y: 2000, layerId: 'L1', fields: { type: 'buffer' } },
{ id: 'S004', name: '充电桩-1', x: 1000, y: 5000, layerId: 'L1', fields: { type: 'charger' } },
{ id: 'S005', name: '充电桩-2', x: 3000, y: 5000, layerId: 'L1', fields: { type: 'charger' } },
{ id: 'S006', name: '路口-N', x: 2000, y: 3500, layerId: 'L1', fields: { type: 'intersection' } },
{ id: 'S007', name: '路口-S', x: 4000, y: 3500, layerId: 'L1', fields: { type: 'intersection' } },
{ id: 'S008', name: '工位-W1', x: 7000, y: 2000, layerId: 'L2', fields: { type: 'workstation' } },
{ id: 'S009', name: '工位-W2', x: 7000, y: 4000, layerId: 'L2', fields: { type: 'workstation' } },
{ id: 'S010', name: '维护区', x: 9000, y: 2000, layerId: 'L2', fields: { type: 'maintenance' } }
]
@@ -0,0 +1,13 @@
import type { Track } from '@/types/map'
export const TRACKS: Track[] = [
{ id: 'T001', name: 'A 区入→出', kind: 'line', fromSiteId: 'S001', toSiteId: 'S002', lengthM: 2.0, bidirectional: true },
{ id: 'T002', name: 'A→B 主干', kind: 'line', fromSiteId: 'S002', toSiteId: 'S003', lengthM: 2.0, bidirectional: true },
{ id: 'T003', name: '入库→充电 1', kind: 'arc', fromSiteId: 'S001', toSiteId: 'S004', lengthM: 3.2, ctrlPts: [{ x: 1500, y: 3500 }] },
{ id: 'T004', name: '出库→充电 2', kind: 'arc', fromSiteId: 'S002', toSiteId: 'S005', lengthM: 3.2, ctrlPts: [{ x: 3500, y: 3500 }] },
{ id: 'T005', name: '路口连接 N', kind: 'line', fromSiteId: 'S002', toSiteId: 'S006', lengthM: 1.8 },
{ id: 'T006', name: '路口连接 S', kind: 'line', fromSiteId: 'S006', toSiteId: 'S007', lengthM: 2.0 },
{ id: 'T007', name: '路口→工位 W1', kind: 'bezier', fromSiteId: 'S007', toSiteId: 'S008', lengthM: 4.0, ctrlPts: [{ x: 5500, y: 2500 }, { x: 6500, y: 2200 }] },
{ id: 'T008', name: '工位 W1↔W2', kind: 'line', fromSiteId: 'S008', toSiteId: 'S009', lengthM: 2.0, bidirectional: true },
{ id: 'T009', name: 'W2→维护', kind: 'nurbs', fromSiteId: 'S009', toSiteId: 'S010', lengthM: 3.5, ctrlPts: [{ x: 8000, y: 3000 }] }
]
@@ -0,0 +1,165 @@
import type { WorkbenchList, SelectionDetail } from '@/types/workbench'
import { CARS } from './cars'
import { MISSIONS } from './missions'
import { SITES } from './sites'
import { TRACKS } from './tracks'
export function mockWorkbenchList(nav: string): WorkbenchList {
switch (nav) {
case 'map':
return {
nav: 'map',
columns: ['ID', '名称', '类型', '图层'],
rows: [
{ id: '1', name: 'factory_floor', type: 'Map', status: 'g', cells: { : 'g' } },
{ id: '2', name: 'grid_overlay', type: 'Map', status: 'overlay', cells: { : 'overlay' } }
]
}
case 'process':
return {
nav: 'process',
columns: ['名称', '状态', '类型'],
rows: MISSIONS.map((m) => ({
id: m.id.replace('M', ''),
name: m.name,
type: m.typeName.split('.').pop() ?? m.typeName,
status: m.status,
cells: { autoStart: 'true' }
}))
}
case 'vehicle':
return {
nav: 'vehicle',
columns: ['ID', '地址', '名称', '概况'],
rows: CARS.map((c, i) => ({
id: String(i + 1),
name: c.name,
type: c.typeName.split('.').pop() ?? c.typeName,
status: c.state,
overview: c.state,
cells: { 地址: c.ip ?? '-', 图层: c.group ?? 'g' }
}))
}
case 'script':
return {
nav: 'script',
columns: ['车辆', '脚本名', '状态'],
rows: CARS.map((c, i) => ({
id: String(i + 1),
name: `${c.name} (#${i + 1})`,
type: c.missionId ? 'MoveProgram' : '(无)',
status: c.state === 'running' ? 'Running' : '-',
cells: { 脚本: c.missionId ? 'MoveProgram' : '(无)', 状态: c.state }
}))
}
case 'scene':
return {
nav: 'scene',
columns: ['ID', '名称', '类型', '信息'],
rows: [
...SITES.map((s) => ({
id: s.id.replace('S', ''),
name: s.name,
type: 'UISite',
status: `(${s.x}, ${s.y})`,
cells: { : `(${s.x}, ${s.y})` }
})),
...TRACKS.map((t) => ({
id: t.id.replace('T', ''),
name: t.id,
type: t.kind,
status: `${t.fromSiteId}${t.toSiteId}`,
cells: { : 'Forward' }
}))
]
}
default:
return { nav: nav as WorkbenchList['nav'], columns: [], rows: [] }
}
}
export function mockSelectionDetail(kind: string, id: string, tab: string): SelectionDetail {
const car = CARS.find((_, i) => String(i + 1) === id || _.id === `C${id.padStart(2, '0')}`)
const mission = MISSIONS.find((m) => m.id.replace('M', '') === id || m.id === `M${id.padStart(2, '0')}`)
const base = {
objectKind: kind,
objectId: id,
tab: tab as SelectionDetail['tab'],
memberFields: [] as SelectionDetail['memberFields'],
customFields: [] as SelectionDetail['customFields'],
statusLines: [] as SelectionDetail['statusLines'],
actions: [] as SelectionDetail['actions']
}
if ((kind === 'vehicle' || kind === 'car' || kind === 'script') && car) {
if (tab === 'status') {
return {
...base,
name: car.name,
typeName: car.typeName,
layer: car.group,
summary: [{ key: 'ID', value: car.id }, { key: '类型', value: car.typeName }],
statusLines: [
{ key: '地址', value: car.ip ?? '-' },
{ key: '概况', value: car.state },
{ key: '电量', value: `${Math.round(car.batterySoc * 100)}%` },
{ key: '位姿', value: `(${car.x}, ${car.y}, ${car.theta}°)` }
]
}
}
if (tab === 'action') {
return {
...base,
name: car.name,
typeName: car.typeName,
summary: [{ key: 'ID', value: car.id }],
actions: [
{ id: 'pause', label: '暂停' },
{ id: 'resume', label: '恢复' },
{ id: 'gohome', label: '回充' }
]
}
}
return {
...base,
name: car.name,
typeName: car.typeName,
layer: car.group,
summary: [
{ key: '站点', value: '—' },
{ key: '概况', value: car.state },
{ key: '速度', value: '0.80' },
{ key: '位姿', value: `(${car.x}, ${car.y}, ${car.theta}°)` }
],
memberFields: [
{ key: 'name', value: car.name, locked: false },
{ key: 'address', value: car.ip ?? '', locked: false }
],
customFields: [{ key: 'group', value: car.group ?? '', locked: false }]
}
}
if ((kind === 'process' || kind === 'mission') && mission) {
return {
...base,
name: mission.name,
typeName: mission.typeName,
summary: [
{ key: '显示名', value: mission.typeName.split('.').pop() ?? mission.typeName },
{ key: 'autoStart', value: 'true' },
{ key: '状态', value: mission.status }
],
statusLines: tab === 'status'
? [{ key: '状态', value: mission.status }, { key: '优先级', value: `${mission.priority}` }]
: [],
actions: tab === 'action' ? [{ id: 'start', label: '启动' }, { id: 'stop', label: '停止' }] : []
}
}
return {
...base,
name: id,
typeName: kind,
summary: [{ key: '提示', value: '未找到对象或 Mock 数据不完整' }]
}
}
@@ -0,0 +1,149 @@
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'
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,
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,
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('用户名不能为空')
const token = `mock-jwt.${req.scope}.${req.username}.${Date.now()}`
// mock 服务器无法真正拉起 SimpleLite,只把 LaunchMode 翻译为 RunMode 让前端 UI 自洽。
// - 历史前端不传 launchMode → 退到 DesktopAndWeb → runMode=WebEnabled(与改造前一致)。
// - launchMode=WebOnly → runMode=WebOnly。
const runMode: LoginResponse['runMode'] = req.launchMode === 'WebOnly' ? 'WebOnly' : 'WebEnabled'
return {
token,
user: {
id: `u-${req.username}`,
username: req.username,
displayName: req.username === 'admin' ? '系统管理员' : (req.username === 'ops' ? '运营人员' : req.username),
roles: req.scope === 'Platform' ? ['role-admin'] : ['role-ops']
},
scope: req.scope,
effectivePermissions: buildPerm(req.scope, req.username),
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
}
const audits: OpsAuditEntry[] = [
{ id: 'A001', ts: nowIso(), user: 'ops', scope: 'RCSMonitor', opCode: 'ops.car.pause', target: 'C01', result: 'ok' },
{ id: 'A002', ts: nowIso(), user: 'ops', scope: 'RCSMonitor', opCode: 'ops.task.cancel', target: 'M03', result: 'ok' }
]
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')}`
audits.unshift({ id, ts: nowIso(), user: 'mock-user', scope: 'mock', opCode: req.opCode, target: req.targetId, result: 'ok', message: req.reason })
return { ok: true, auditId: id }
}
export async function mockOpsAudits(): Promise<OpsAuditEntry[]> { await delay(60); return [...audits] }
function delay(ms: number) { return new Promise((r) => setTimeout(r, ms)) }