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:
@@ -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':
|
||||
// 合成 kind:site + 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 | null(null 表示被删除) }
|
||||
// 用于演示 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 数据全部当 dynamic(mock 模式下没有真实 [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)) }
|
||||
Reference in New Issue
Block a user