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,50 @@
|
||||
import http from './http'
|
||||
import type { LoginRequest, LoginResponse, MeResponse, Scope } from '@/types/auth'
|
||||
import { mockLogin } from '@/mock/server'
|
||||
|
||||
// 是否走前端 Mock。由 VITE_USE_MOCK 控制:
|
||||
// - .env.development 默认 'true' → 本地无后端即可联调
|
||||
// - .env.production 默认 'false' → 生产强制走 Platform.Server 真实 API
|
||||
// 任何不等于字符串 'true' 的取值都视为 false,避免 build 时静态替换出错。
|
||||
const MOCK = import.meta.env.VITE_USE_MOCK === 'true'
|
||||
|
||||
export async function login(req: LoginRequest): Promise<LoginResponse> {
|
||||
if (MOCK) return mockLogin(req)
|
||||
const { data } = await http.post<LoginResponse>('/auth/login', req)
|
||||
return data
|
||||
}
|
||||
|
||||
export async function logout(): Promise<void> {
|
||||
if (MOCK) return
|
||||
await http.post('/auth/logout')
|
||||
}
|
||||
|
||||
/**
|
||||
* AR-6 (会话 45):服务端切换 scope 并重发 token + perms。
|
||||
* 替代过去 stores/auth.ts switchScope 客户端硬编码 allowedOps 的伪权限做法。
|
||||
* Mock 模式下退化为本地翻转 scope(mockLogin 已经依据 scope 返回不同 perms)。
|
||||
*/
|
||||
export async function switchScope(scope: Scope): Promise<LoginResponse> {
|
||||
if (MOCK) {
|
||||
// mock 直接复用 mockLogin 的 perm 推算逻辑,伪造一次「免密码登录」。
|
||||
return mockLogin({ username: 'mock-user', password: 'mock', scope })
|
||||
}
|
||||
const { data } = await http.post<LoginResponse>('/auth/switch-scope', { scope })
|
||||
return data
|
||||
}
|
||||
|
||||
/**
|
||||
* 拿当前 token / Cookie 让后端实校一次身份。成功表示 token 仍被服务端接受,
|
||||
* 失败(401)由 http 拦截器统一清 state + 跳 /login。
|
||||
*
|
||||
* 解决「Platform.Server 随机 secret 重启 → 老 token 失效 → 前端 isAuthed 仍 true 误放行」的窗口。
|
||||
*/
|
||||
export async function getMe(): Promise<MeResponse> {
|
||||
if (MOCK) {
|
||||
// mock 模式下没有 secret 概念,本地 store 里有什么就是什么;直接 throw 让守卫
|
||||
// 走「认可本地状态」分支,避免 mock 联调误判为「身份失效」。
|
||||
throw new Error('mock-mode-skip-me-validation')
|
||||
}
|
||||
const { data } = await http.get<MeResponse>('/auth/me')
|
||||
return data
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import http from './http'
|
||||
import type { ConfigEnvelope, ConfigSection } from '@/types/config'
|
||||
import { mockGetConfig, mockPutConfig } from '@/mock/server'
|
||||
|
||||
// 见 auth.ts 中关于 VITE_USE_MOCK 的说明:生产默认走 Platform.Server `/api/config/*`,
|
||||
// 仅当 `.env.development` 显式声明 `VITE_USE_MOCK=true` 时才回到前端假数据。
|
||||
const MOCK = import.meta.env.VITE_USE_MOCK === 'true'
|
||||
|
||||
export async function getConfig<T = unknown>(section: ConfigSection): Promise<ConfigEnvelope<T>> {
|
||||
if (MOCK) return mockGetConfig<T>(section)
|
||||
const { data } = await http.get<ConfigEnvelope<T>>(`/config/${section}`)
|
||||
return data
|
||||
}
|
||||
|
||||
export async function putConfig<T = unknown>(
|
||||
section: ConfigSection,
|
||||
payload: T
|
||||
): Promise<ConfigEnvelope<T>> {
|
||||
if (MOCK) return mockPutConfig<T>(section, payload)
|
||||
const { data } = await http.put<ConfigEnvelope<T>>(`/config/${section}`, payload)
|
||||
return data
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import axios, { AxiosError, type AxiosInstance, type InternalAxiosRequestConfig } from 'axios'
|
||||
|
||||
const baseURL = (import.meta.env.VITE_API_BASE as string | undefined) ?? '/api'
|
||||
|
||||
// AR-5 (会话 45):withCredentials=true 让后端下发的 httpOnly Cookie (simple.auth.token)
|
||||
// 自动随请求带回。Platform.Server `/api/auth/login` 同时下发 Cookie 和返回 token 字段,
|
||||
// 过渡期内 Authorization: Bearer header 仍然兼容(旧浏览器 / 跨进程脚本)。
|
||||
const http: AxiosInstance = axios.create({
|
||||
baseURL,
|
||||
timeout: 15000,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
withCredentials: true
|
||||
})
|
||||
|
||||
http.interceptors.request.use((config: InternalAxiosRequestConfig) => {
|
||||
// 双轨:优先用 localStorage 里的 token(过渡期 fallback),Cookie 会自动带;
|
||||
// 后端首先看 Authorization: Bearer,没有再看 Cookie,两路任一通过即可。
|
||||
const token = localStorage.getItem('simple.auth.token')
|
||||
if (token) {
|
||||
config.headers.set('Authorization', `Bearer ${token}`)
|
||||
}
|
||||
const scope = localStorage.getItem('simple.auth.scope')
|
||||
if (scope) {
|
||||
config.headers.set('X-Scope', scope)
|
||||
}
|
||||
return config
|
||||
})
|
||||
|
||||
/**
|
||||
* 把 axios 异常翻译成更友好的中文文案,特别是 SimpleLite 链路:
|
||||
*
|
||||
* /api/sl/projection/** → Platform.Server YARP → SimpleLite EmbedIO :8222
|
||||
*
|
||||
* 如果 SimpleLite 没启动 / 端口未监听,YARP 一定回 502;超时一般是 504。把这两种
|
||||
* 情况单独识别,写成「SimpleLite 未连接,请先启动 SimpleLite (端口 8222)」之类,
|
||||
* 比直接抛 `Request failed with status code 502` 友好得多,也避免用户怀疑前端 bug。
|
||||
*/
|
||||
function describeError(err: AxiosError): string {
|
||||
const status = err.response?.status
|
||||
const url = err.config?.url ?? ''
|
||||
const isSimpleLite = url.startsWith('/sl/') || url.startsWith('sl/')
|
||||
if (status === 502 || status === 504 || err.code === 'ECONNABORTED' || err.code === 'ERR_NETWORK') {
|
||||
if (isSimpleLite) {
|
||||
return 'SimpleLite 未连接:请先启动 SimpleLite 后端(监听端口 8222)后重试。'
|
||||
}
|
||||
return `网关无法连接到下游服务(${status ?? err.code ?? 'network'})。`
|
||||
}
|
||||
// 后端 envelope 模式:{ success: false, message: 'xxx' }
|
||||
const data = err.response?.data as { message?: string } | undefined
|
||||
if (data?.message) return data.message
|
||||
return err.message
|
||||
}
|
||||
|
||||
http.interceptors.response.use(
|
||||
(resp) => resp,
|
||||
(err: AxiosError) => {
|
||||
if (err.response?.status === 401) {
|
||||
// 会话 45 HC-1:401 全量清 state,避免老 user/scope/perm 残留导致 UI 误判。
|
||||
try {
|
||||
localStorage.removeItem('simple.auth.token')
|
||||
localStorage.removeItem('simple.auth.user')
|
||||
localStorage.removeItem('simple.auth.scope')
|
||||
localStorage.removeItem('simple.auth.runMode')
|
||||
localStorage.removeItem('simple.auth.perm')
|
||||
} catch { /* 某些隐私模式 / iframe 沙箱可能禁用 localStorage */ }
|
||||
if (location.pathname !== '/login') location.assign('/login')
|
||||
}
|
||||
// 用 Object.defineProperty 覆盖 message,避免后续 `(err as Error).message` 还是看到原文。
|
||||
const friendly = describeError(err)
|
||||
try { Object.defineProperty(err, 'message', { value: friendly, configurable: true }) } catch { /* ignore */ }
|
||||
return Promise.reject(err)
|
||||
}
|
||||
)
|
||||
|
||||
export default http
|
||||
@@ -0,0 +1,282 @@
|
||||
import http from './http'
|
||||
|
||||
/**
|
||||
* 与 SimpleLite `MapEditApiController` 对应的前端胶水。
|
||||
* 路径前缀:`/sl/projection/map-edit`(YARP → SimpleLite `/projection/map-edit`)。
|
||||
*
|
||||
* 也覆盖 AI 服务配置 `/sl/projection/ai-config`(GET / POST),由 AiConfigController 提供。
|
||||
*
|
||||
* 所有方法都通过统一信封 `{ success, code, data, message }` 返回;调用方拿到的是 `data` 部分,
|
||||
* 失败时直接抛 Error,让上层 try/catch 或 ElMessage.error 统一处理。
|
||||
*/
|
||||
|
||||
const BASE = '/sl/projection/map-edit'
|
||||
const AI_BASE = '/sl/projection/ai-config'
|
||||
|
||||
export interface MapEditEnvelope<T> {
|
||||
success: boolean
|
||||
code: number
|
||||
data: T | null
|
||||
message: string
|
||||
}
|
||||
|
||||
export type MapEditKind =
|
||||
| 'site'
|
||||
| 'track'
|
||||
| 'bezier'
|
||||
| 'arc'
|
||||
| 'nurbs'
|
||||
| 'image'
|
||||
| 'text'
|
||||
| 'model'
|
||||
| 'special'
|
||||
|
||||
export interface CreateSitePayload {
|
||||
x: number
|
||||
y: number
|
||||
z?: number
|
||||
name?: string
|
||||
layer?: string
|
||||
[k: string]: unknown
|
||||
}
|
||||
|
||||
export interface CreateTrackPayload {
|
||||
siteA: number
|
||||
siteB: number
|
||||
name?: string
|
||||
layer?: string
|
||||
[k: string]: unknown
|
||||
}
|
||||
|
||||
export interface CreateImagePayload {
|
||||
x: number
|
||||
y: number
|
||||
imagePath: string
|
||||
size?: number
|
||||
angle?: number
|
||||
opacity?: number
|
||||
name?: string
|
||||
layer?: string
|
||||
}
|
||||
|
||||
export interface CreateTextPayload {
|
||||
x: number
|
||||
y: number
|
||||
content: string
|
||||
color?: string
|
||||
size?: number
|
||||
name?: string
|
||||
layer?: string
|
||||
}
|
||||
|
||||
export interface CreateModelPayload {
|
||||
x: number
|
||||
y: number
|
||||
modelPath: string
|
||||
scale?: number
|
||||
rotation?: number
|
||||
zOffset?: number
|
||||
name?: string
|
||||
layer?: string
|
||||
}
|
||||
|
||||
export interface CreateCarPayload {
|
||||
x: number
|
||||
y: number
|
||||
/** 朝向,弧度。可省略,默认 0。 */
|
||||
theta?: number
|
||||
name?: string
|
||||
layer?: string
|
||||
}
|
||||
|
||||
export interface BatchOp {
|
||||
action: 'create' | 'delete' | 'patch'
|
||||
kind?: MapEditKind | string
|
||||
id?: number
|
||||
data?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface PickResult {
|
||||
x: number
|
||||
y: number
|
||||
siteId: number
|
||||
}
|
||||
|
||||
export interface DashboardSummary {
|
||||
cars: number
|
||||
onlineCars: number
|
||||
alarmingCars: number
|
||||
sites: number
|
||||
tracks: number
|
||||
specials: number
|
||||
maps: number
|
||||
missions: number
|
||||
timestamp: string
|
||||
}
|
||||
|
||||
export interface AssetUploadResult {
|
||||
fileName: string
|
||||
savedPath: string
|
||||
/** 相对 url,前端拼上 webVRender host 即可显示。 */
|
||||
url: string
|
||||
}
|
||||
|
||||
export interface ViewFilterSnapshot {
|
||||
selectFilter: { sites: boolean; tracks: boolean; cars: boolean; decor: boolean }
|
||||
alignSnap: { sites: boolean; cars: boolean; tracks: boolean }
|
||||
showViewport: { sceneLabels: boolean; scenePrimitives: boolean; cars: boolean }
|
||||
}
|
||||
|
||||
/** 部分更新;只传你要改的字段即可。后端不会覆盖未传字段。 */
|
||||
export interface ViewFilterPatch {
|
||||
selectFilter?: Partial<ViewFilterSnapshot['selectFilter']>
|
||||
alignSnap?: Partial<ViewFilterSnapshot['alignSnap']>
|
||||
showViewport?: Partial<ViewFilterSnapshot['showViewport']>
|
||||
}
|
||||
|
||||
export interface AiMapGenerateRequest {
|
||||
prompt: string
|
||||
mode?: 'sites' | 'tracks' | 'sites+tracks' | 'full'
|
||||
bounds?: [number, number, number, number]
|
||||
referenceImageBase64?: string
|
||||
layer?: string
|
||||
constraints?: string
|
||||
}
|
||||
|
||||
export interface AiMapGenerateResult {
|
||||
created: Array<{ tool: string; kind: string; id: number; typeName?: string; error?: string }>
|
||||
assistantText?: string
|
||||
usedTools: number
|
||||
}
|
||||
|
||||
export interface AiConfig {
|
||||
endpoint: string
|
||||
apiKey: string
|
||||
model: string
|
||||
systemPrompt: string
|
||||
temperature: number
|
||||
maxTokens: number
|
||||
timeoutSec: number
|
||||
/** 仅 GET 返回时携带:apiKey 非空 → true。 */
|
||||
configured?: boolean
|
||||
}
|
||||
|
||||
async function unwrap<T>(promise: Promise<{ data: MapEditEnvelope<T> }>): Promise<T> {
|
||||
const { data } = await promise
|
||||
if (!data?.success) throw new Error(data?.message ?? 'request failed')
|
||||
return data.data as T
|
||||
}
|
||||
|
||||
export const mapEditApi = {
|
||||
// 对象创建/删除
|
||||
createSite: (p: CreateSitePayload) =>
|
||||
unwrap<{ kind: 'site'; id: number; name?: string }>(http.post(`${BASE}/objects/site`, p)),
|
||||
|
||||
createTrack: (kind: 'track' | 'bezier' | 'arc' | 'nurbs', p: CreateTrackPayload) =>
|
||||
unwrap<{ kind: 'track'; id: number; typeName: string }>(http.post(`${BASE}/objects/${kind}`, p)),
|
||||
|
||||
createImage: (p: CreateImagePayload) =>
|
||||
unwrap<{ kind: 'image'; id: number }>(http.post(`${BASE}/objects/image`, p)),
|
||||
|
||||
createText: (p: CreateTextPayload) =>
|
||||
unwrap<{ kind: 'text'; id: number }>(http.post(`${BASE}/objects/text`, p)),
|
||||
|
||||
createModel: (p: CreateModelPayload) =>
|
||||
unwrap<{ kind: 'model'; id: number }>(http.post(`${BASE}/objects/model`, p)),
|
||||
|
||||
/** 直接创建一辆模拟车 (DummyCar),不经过画布 pick。 */
|
||||
createCar: (p: CreateCarPayload) =>
|
||||
unwrap<{ kind: 'car'; id: number; typeName: string }>(http.post(`${BASE}/objects/car`, p)),
|
||||
|
||||
deleteObject: (kind: MapEditKind | string, id: number) =>
|
||||
unwrap<{ kind: string; id: number; deleted: boolean }>(http.delete(`${BASE}/objects/${kind}/${id}`)),
|
||||
|
||||
batch: (ops: BatchOp[]) =>
|
||||
unwrap<{ count: number; results: unknown[] }>(http.post(`${BASE}/objects/batch`, { ops })),
|
||||
|
||||
copyFieldsTo: (kind: MapEditKind | string, id: number, fieldNames: string[], targets: Array<{ kind: string; id: number }>) =>
|
||||
unwrap<{ copied: number }>(http.post(`${BASE}/objects/${kind}/${id}/fields/copy-to`, { fieldNames, targets })),
|
||||
|
||||
// 拾取 / 仪表盘
|
||||
pick: () => unwrap<PickResult>(http.post(`${BASE}/pick`)),
|
||||
|
||||
dashboardSummary: () =>
|
||||
unwrap<DashboardSummary>(http.get(`${BASE}/dashboard/summary`)),
|
||||
|
||||
// 资产上传:传 base64
|
||||
uploadAsset: (filename: string, dataBase64: string) =>
|
||||
unwrap<AssetUploadResult>(http.post(`${BASE}/assets/upload`, { filename, data: dataBase64 }, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})),
|
||||
|
||||
// AI 生图
|
||||
aiMapGenerate: (req: AiMapGenerateRequest) =>
|
||||
unwrap<AiMapGenerateResult>(http.post(`${BASE}/ai/map-generate`, req)),
|
||||
|
||||
projectSave: (path?: string) =>
|
||||
unwrap<{ path: string; savedAt: string }>(http.post(`${BASE}/project/save`, { path: path ?? '' })),
|
||||
|
||||
projectLoad: (path: string) =>
|
||||
unwrap<{ path: string; sites: number; tracks: number; specials: number; missions: number }>(
|
||||
http.post(`${BASE}/project/load`, { path })
|
||||
),
|
||||
|
||||
projectCurrent: () =>
|
||||
unwrap<{ sites: number; tracks: number; specials: number; maps: number; missions: number; timestamp: string; cwd: string }>(
|
||||
http.get(`${BASE}/project/current`)
|
||||
),
|
||||
|
||||
projectBrowse: (dir?: string) =>
|
||||
unwrap<{
|
||||
dir: string
|
||||
parent: string | null
|
||||
subdirs: Array<{ name: string; fullPath: string }>
|
||||
jsons: Array<{ name: string; fullPath: string; sizeBytes: number; modified: string }>
|
||||
}>(http.get(`${BASE}/project/browse`, { params: dir ? { dir } : {} })),
|
||||
|
||||
/**
|
||||
* 在 SimpleLite 桌面进程上弹 Windows 原生「打开文件」对话框,
|
||||
* 让用户挑一个项目 JSON。返回 { path, cancelled };cancelled=true 时 path 为 null。
|
||||
* initialDir 默认 SimpleLite 程序工作目录。
|
||||
*/
|
||||
projectNativePickOpen: (initialDir?: string) =>
|
||||
unwrap<{ path: string | null; cancelled: boolean }>(
|
||||
http.post(`${BASE}/project/native-pick-open`, { initialDir: initialDir ?? '' })
|
||||
),
|
||||
|
||||
/**
|
||||
* 弹 Windows 原生「另存为」对话框。返回 { path, cancelled }。
|
||||
* suggestedFileName 默认 project.current.json。
|
||||
*/
|
||||
projectNativePickSave: (initialDir?: string, suggestedFileName?: string) =>
|
||||
unwrap<{ path: string | null; cancelled: boolean }>(
|
||||
http.post(`${BASE}/project/native-pick-save`, {
|
||||
initialDir: initialDir ?? '',
|
||||
suggestedFileName: suggestedFileName ?? 'project.current.json'
|
||||
})
|
||||
),
|
||||
|
||||
sampleSitesAlongTrack: (trackId: number, opts: { spacingMm: number; includeEndpoints?: boolean; layer?: string }) =>
|
||||
unwrap<{ created: number; ids: number[] }>(
|
||||
http.post(`${BASE}/objects/track/${trackId}/sample-sites`, opts)
|
||||
),
|
||||
|
||||
/**
|
||||
* 读取后端「过滤」菜单当前的三组开关。编辑器载入时拉一次,让前端 reactive 状态
|
||||
* 与 SimpleLite 真实开关一致(避免「前端勾着、后端没改」的偏差)。
|
||||
*/
|
||||
getViewFilter: () => unwrap<ViewFilterSnapshot>(http.get(`${BASE}/view-filter`)),
|
||||
|
||||
/**
|
||||
* 部分更新「过滤」菜单。只传要改的字段即可,未传字段保持原状。
|
||||
* 后端写入对应 SimpleUI 静态字段 + 触发 SimpleSceneRenderer.NotifyStateChanged()
|
||||
* 立即重画工作区,对 SelectFilter 变化还会同步 CycleGUI 引擎层 SelectObject 模式。
|
||||
*/
|
||||
setViewFilter: (patch: ViewFilterPatch) =>
|
||||
unwrap<ViewFilterSnapshot>(http.post(`${BASE}/view-filter`, patch))
|
||||
}
|
||||
|
||||
export const aiConfigApi = {
|
||||
get: () => unwrap<AiConfig>(http.get(`${AI_BASE}/`)),
|
||||
save: (cfg: AiConfig) => unwrap<{ saved: boolean }>(http.post(`${AI_BASE}/`, cfg))
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import http from './http'
|
||||
import type { OpsAuditEntry } from '@/types/ops'
|
||||
import { mockOpsExecute, mockOpsAudits } from '@/mock/server'
|
||||
|
||||
// 见 auth.ts 中关于 VITE_USE_MOCK 的说明:运维白名单网关默认走真实 `/api/sl/ops/*`,
|
||||
// 仅在前端 dev 调试时由 `.env.development` 把 VITE_USE_MOCK 设为 'true' 回到假数据。
|
||||
const MOCK = import.meta.env.VITE_USE_MOCK === 'true'
|
||||
|
||||
export interface OpsExecuteReq {
|
||||
opCode: string
|
||||
targetId: string
|
||||
reason?: string
|
||||
idempotencyKey?: string
|
||||
}
|
||||
|
||||
export interface OpsExecuteResp {
|
||||
ok: boolean
|
||||
auditId: string
|
||||
message?: string
|
||||
}
|
||||
|
||||
export async function executeOp(req: OpsExecuteReq): Promise<OpsExecuteResp> {
|
||||
if (MOCK) return mockOpsExecute(req)
|
||||
const { data } = await http.post<OpsExecuteResp>('/sl/ops/execute', req)
|
||||
return data
|
||||
}
|
||||
|
||||
export async function listAudits(): Promise<OpsAuditEntry[]> {
|
||||
if (MOCK) return mockOpsAudits()
|
||||
const { data } = await http.get<OpsAuditEntry[]>('/sl/ops/audits')
|
||||
return data
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import http from './http'
|
||||
import { reflectionApi, type ReflectionObject } from './reflection'
|
||||
import type { Site, Track } from '@/types/map'
|
||||
import type { Car, CarState } from '@/types/car'
|
||||
import type { Mission, MissionStatus } from '@/types/mission'
|
||||
import { mockSites, mockTracks, mockCars, mockMissions } from '@/mock/server'
|
||||
|
||||
/** 设为 true 时使用本地 Mock;默认走 YARP → SimpleLite :8222。
|
||||
* 与 auth.ts / config.ts / ops.ts 统一走 VITE_USE_MOCK 开关(替代旧的 VITE_PROJECTION_MOCK)。 */
|
||||
const MOCK = import.meta.env.VITE_USE_MOCK === 'true'
|
||||
|
||||
export async function listSites(): Promise<Site[]> {
|
||||
if (MOCK) return mockSites()
|
||||
const { data } = await http.get<Site[]>('/sl/projection/sites')
|
||||
return data
|
||||
}
|
||||
|
||||
export async function listTracks(): Promise<Track[]> {
|
||||
if (MOCK) return mockTracks()
|
||||
const { data } = await http.get<Track[]>('/sl/projection/tracks')
|
||||
return data
|
||||
}
|
||||
|
||||
function mapStatusToCarState(status?: string | null): CarState {
|
||||
if (!status) return 'idle'
|
||||
const s = status.toLowerCase()
|
||||
if (/fault|error|failed|故障|异常|失联|超时|检修/.test(s)) return 'fault'
|
||||
if (/charg|充电/.test(s)) return 'charging'
|
||||
if (/pause|暂停|挂起/.test(s)) return 'paused'
|
||||
if (/offline|离线/.test(s)) return 'offline'
|
||||
if (/run|busy|working|运行|工作|执行|忙/.test(s)) return 'running'
|
||||
return 'idle'
|
||||
}
|
||||
|
||||
function mapStatusToMissionStatus(status?: string | null): MissionStatus {
|
||||
if (!status) return 'queued'
|
||||
const s = status.toLowerCase()
|
||||
if (/run|运行|执行/.test(s)) return 'running'
|
||||
if (/pause|暂停|挂起/.test(s)) return 'paused'
|
||||
if (/complete|done|完成|成功/.test(s)) return 'completed'
|
||||
if (/cancel|取消|中止/.test(s)) return 'cancelled'
|
||||
if (/fail|error|fault|失败|异常/.test(s)) return 'failed'
|
||||
if (/assign|分配/.test(s)) return 'assigned'
|
||||
return 'queued'
|
||||
}
|
||||
|
||||
function carFromReflection(row: ReflectionObject): Car {
|
||||
return {
|
||||
id: `C${String(row.id).padStart(2, '0')}`,
|
||||
rawId: row.id,
|
||||
name: row.name,
|
||||
typeName: row.typeName,
|
||||
x: 0,
|
||||
y: 0,
|
||||
theta: 0,
|
||||
batterySoc: 0.8,
|
||||
state: mapStatusToCarState(row.status),
|
||||
lastUpdate: new Date().toISOString(),
|
||||
group: row.layer ?? undefined
|
||||
}
|
||||
}
|
||||
|
||||
function missionFromReflection(row: ReflectionObject): Mission {
|
||||
return {
|
||||
id: `M${String(row.id).padStart(2, '0')}`,
|
||||
name: row.name,
|
||||
typeName: row.typeName,
|
||||
priority: 50,
|
||||
status: mapStatusToMissionStatus(row.status),
|
||||
steps: [],
|
||||
createdAt: new Date().toISOString()
|
||||
}
|
||||
}
|
||||
|
||||
async function listCarsFromReflection(): Promise<Car[]> {
|
||||
const rows = await reflectionApi.listObjects('car')
|
||||
return rows.map(carFromReflection)
|
||||
}
|
||||
|
||||
async function listMissionsFromReflection(): Promise<Mission[]> {
|
||||
const rows = await reflectionApi.listObjects('mission')
|
||||
return rows.map(missionFromReflection)
|
||||
}
|
||||
|
||||
/** 优先走 SimpleLite /projection/cars;502 或空列表时回退 reflection 对象列表(与 3D 场景同源)。 */
|
||||
export async function listCars(): Promise<Car[]> {
|
||||
if (MOCK) return mockCars()
|
||||
try {
|
||||
const { data } = await http.get<Car[]>('/sl/projection/cars')
|
||||
if (Array.isArray(data) && data.length > 0) return data
|
||||
const fallback = await listCarsFromReflection()
|
||||
if (fallback.length > 0) return fallback
|
||||
return Array.isArray(data) ? data : []
|
||||
} catch {
|
||||
return listCarsFromReflection()
|
||||
}
|
||||
}
|
||||
|
||||
export async function listMissions(): Promise<Mission[]> {
|
||||
if (MOCK) return mockMissions()
|
||||
try {
|
||||
const { data } = await http.get<Mission[]>('/sl/projection/missions')
|
||||
if (Array.isArray(data) && data.length > 0) return data
|
||||
const fallback = await listMissionsFromReflection()
|
||||
if (fallback.length > 0) return fallback
|
||||
return Array.isArray(data) ? data : []
|
||||
} catch {
|
||||
return listMissionsFromReflection()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,620 @@
|
||||
import http from './http'
|
||||
import {
|
||||
mockReflectionAssemblies,
|
||||
mockReflectionBundle,
|
||||
mockReflectionDeleteField,
|
||||
mockReflectionExecute,
|
||||
mockReflectionFields,
|
||||
mockReflectionKinds,
|
||||
mockReflectionMethods,
|
||||
mockReflectionMethodsByType,
|
||||
mockReflectionObjects,
|
||||
mockReflectionSetField,
|
||||
mockReflectionStatus
|
||||
} from '@/mock/data/reflection'
|
||||
|
||||
// 与 auth.ts / config.ts / ops.ts 统一走 VITE_USE_MOCK 开关,避免 PROJECTION/USE 双命名造成
|
||||
// .env 改一个忘改另一个的诡异半 mock 状态。env.d.ts 已声明 VITE_USE_MOCK 类型。
|
||||
const MOCK = import.meta.env.VITE_USE_MOCK === 'true'
|
||||
|
||||
/**
|
||||
* 与 SimpleLite `ReflectionApiController` 一一对应的前端胶水。
|
||||
* 路径前缀:`/sl/projection/reflection`(迷榖平台 YARP 反代下游 → SimpleLite EmbedIO)。
|
||||
*
|
||||
* 该客户端覆盖了 SimpleLite 主体以及通过 plugins/*.dll 动态加载的 Standard 等
|
||||
* 插件中所有标注了 `MethodMember` 的方法与所有继承自 Car/Mission/CarProgram/Site/Track
|
||||
* 的子类型,平台前端可据此动态渲染列表、属性、动作按钮。
|
||||
*/
|
||||
|
||||
const BASE = '/sl/projection/reflection'
|
||||
|
||||
export type ReflectionKind =
|
||||
| 'car'
|
||||
| 'vehicle'
|
||||
| 'mission'
|
||||
| 'process'
|
||||
| 'site'
|
||||
| 'track'
|
||||
| 'map'
|
||||
| 'special'
|
||||
| 'script'
|
||||
// 顶级合成 kind:等于 site + track + special 的并集,由 SimpleLite 后端 /objects/scene 直接返回,
|
||||
// 每行带 subKind 字段,前端 bundle / execute / setField 走对应底层 kind。
|
||||
| 'scene'
|
||||
|
||||
export interface ReflectionEnvelope<T> {
|
||||
success: boolean
|
||||
code: number
|
||||
data: T | null
|
||||
message: string
|
||||
}
|
||||
|
||||
export interface ReflectionParam {
|
||||
name: string
|
||||
typeName: string
|
||||
hasDefault: boolean
|
||||
defaultValue?: string | null
|
||||
}
|
||||
|
||||
export interface ReflectionMethod {
|
||||
methodName: string
|
||||
label?: string | null
|
||||
description?: string | null
|
||||
hint?: string | null
|
||||
returnType: string
|
||||
hasParams: boolean
|
||||
params: ReflectionParam[]
|
||||
}
|
||||
|
||||
export interface ReflectionObject {
|
||||
id: number
|
||||
name: string
|
||||
typeName: string
|
||||
layer?: string | null
|
||||
status?: string | null
|
||||
summary?: string | null
|
||||
/** 当顶级 kind 是合成 kind(如 "scene")时,此字段标记底层真实 kind。 */
|
||||
subKind?: string | null
|
||||
}
|
||||
|
||||
export interface ReflectionKv {
|
||||
key: string
|
||||
value: string
|
||||
locked?: boolean
|
||||
/** "typed" = 强类型 [FieldMember],不可删;"dynamic" = Prop.fields 动态字段,可删。 */
|
||||
source?: 'typed' | 'dynamic'
|
||||
/** 后端字段的 .NET 类型名(String / Single / Int32 / Boolean 等),用于前端渲染对应控件。 */
|
||||
typeName?: string
|
||||
}
|
||||
|
||||
export interface ReflectionTypeMethods {
|
||||
typeName: string
|
||||
fullTypeName?: string
|
||||
typeLabel?: string
|
||||
assemblyName: string
|
||||
methods: ReflectionMethod[]
|
||||
}
|
||||
|
||||
export interface ReflectionAssembly {
|
||||
name: string
|
||||
version?: string
|
||||
location?: string
|
||||
}
|
||||
|
||||
/** 通过 GET /reflection/types/{kind} 拿到的可创建子类型行(进程 / 脚本 / 车辆管理面板的「新建」下拉用)。 */
|
||||
export interface ReflectionCreatableType {
|
||||
typeName: string
|
||||
shortName: string
|
||||
label: string
|
||||
assemblyName: string
|
||||
}
|
||||
|
||||
/** GET /reflection/plugins 返回的单个插件元信息。 */
|
||||
export interface PluginEntry {
|
||||
name: string
|
||||
dllPath: string
|
||||
assemblyName: string
|
||||
assemblyVersion: string
|
||||
collectible: boolean
|
||||
loadedTypes: number
|
||||
loadedAt: string
|
||||
missionTypes: number
|
||||
carTypes: number
|
||||
}
|
||||
|
||||
export interface ReflectionKindMeta {
|
||||
kind: ReflectionKind
|
||||
count: number
|
||||
label: string
|
||||
/** "primary" 表示顶级 5 分类;undefined / 其他视为底层 kind。 */
|
||||
group?: string
|
||||
}
|
||||
|
||||
export interface ReflectionSubKindMeta extends ReflectionKindMeta {
|
||||
parent: ReflectionKind
|
||||
}
|
||||
|
||||
export interface ReflectionSelection {
|
||||
kind: ReflectionKind | null
|
||||
id: number
|
||||
name: string
|
||||
typeName?: string
|
||||
}
|
||||
|
||||
export interface ScriptSourcePayload {
|
||||
id: number
|
||||
name: string
|
||||
typeName: string
|
||||
state?: string | null
|
||||
script: string
|
||||
}
|
||||
|
||||
export interface ScriptExceptionStatusPayload {
|
||||
id: number
|
||||
name: string
|
||||
typeName: string
|
||||
car: string
|
||||
state?: string | null
|
||||
exception: string
|
||||
notifies: string
|
||||
report: string
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
// 地图监控可见性配置(与后端 MonitorVisibilityConfig / MonitorVisibilityForKind 对应)
|
||||
//
|
||||
// `config` = 管理员勾选的白名单。空数组 = 显示全部;非空 = 仅显示列表中的 key。
|
||||
// `available` = 当前 SimpleLite 进程里能扫描到的全部可勾选项(基类 + 已加载子类 +
|
||||
// 运行时实例的 Prop.fields / status 反射键)。是 GET 返回的副产物,
|
||||
// POST 写入时不需要、也不该回传。
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
export type MonitorVisibilityKind = 'car' | 'site' | 'track'
|
||||
|
||||
export interface MonitorVisibilityForKind {
|
||||
fields: string[]
|
||||
status: string[]
|
||||
methods: string[]
|
||||
}
|
||||
|
||||
export interface MonitorVisibilityMap {
|
||||
car: MonitorVisibilityForKind
|
||||
site: MonitorVisibilityForKind
|
||||
track: MonitorVisibilityForKind
|
||||
}
|
||||
|
||||
/** POST /monitor-config 请求体:三组白名单 + 可选按车型动作。 */
|
||||
export interface MonitorConfigSaveBody extends MonitorVisibilityMap {
|
||||
carActionByType?: Record<string, string[]>
|
||||
}
|
||||
|
||||
export interface MonitorConfigPayload {
|
||||
config: MonitorVisibilityMap & { carActionByType?: Record<string, string[]> }
|
||||
available: MonitorVisibilityMap
|
||||
}
|
||||
|
||||
const MOCK_MONITOR_STORAGE_KEY = 'simple-platform-mock-monitor-config'
|
||||
|
||||
function loadMockMonitorConfig(): MonitorConfigSaveBody {
|
||||
try {
|
||||
const raw = localStorage.getItem(MOCK_MONITOR_STORAGE_KEY)
|
||||
if (raw) return JSON.parse(raw) as MonitorConfigSaveBody
|
||||
} catch { /* ignore */ }
|
||||
return emptyMonitorVisibilityMap()
|
||||
}
|
||||
|
||||
function saveMockMonitorConfig(cfg: MonitorConfigSaveBody) {
|
||||
try {
|
||||
localStorage.setItem(MOCK_MONITOR_STORAGE_KEY, JSON.stringify(cfg))
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
export function emptyMonitorVisibilityForKind(): MonitorVisibilityForKind {
|
||||
return { fields: [], status: [], methods: [] }
|
||||
}
|
||||
|
||||
export function emptyMonitorVisibilityMap(): MonitorVisibilityMap {
|
||||
return {
|
||||
car: emptyMonitorVisibilityForKind(),
|
||||
site: emptyMonitorVisibilityForKind(),
|
||||
track: emptyMonitorVisibilityForKind()
|
||||
}
|
||||
}
|
||||
|
||||
// 语义上 available 与 config 同构(都是按 kind 分组的三组 key 列表),所以共用一个工厂。
|
||||
// 单独保留命名是为了表达"业务含义不同"——可勾选全集 vs 已勾选白名单。
|
||||
export const emptyMonitorAvailableMap = emptyMonitorVisibilityMap
|
||||
|
||||
async function get<T>(path: string): Promise<T> {
|
||||
const { data } = await http.get<ReflectionEnvelope<T>>(`${BASE}${path}`)
|
||||
if (!data?.success) throw new Error(data?.message ?? `reflection ${path} failed`)
|
||||
return data.data as T
|
||||
}
|
||||
|
||||
async function post<T>(path: string, params?: Record<string, string | number | boolean>): Promise<T> {
|
||||
const { data } = await http.post<ReflectionEnvelope<T>>(`${BASE}${path}`, null, { params })
|
||||
if (!data?.success) throw new Error(data?.message ?? `reflection ${path} failed`)
|
||||
return data.data as T
|
||||
}
|
||||
|
||||
async function del<T>(path: string): Promise<T> {
|
||||
const { data } = await http.delete<ReflectionEnvelope<T>>(`${BASE}${path}`)
|
||||
if (!data?.success) throw new Error(data?.message ?? `reflection ${path} failed`)
|
||||
return data.data as T
|
||||
}
|
||||
|
||||
export const reflectionApi = {
|
||||
listKinds: () => MOCK
|
||||
? Promise.resolve(mockReflectionKinds())
|
||||
: get<{ kinds: ReflectionKindMeta[]; subKinds?: ReflectionSubKindMeta[] }>('/kinds'),
|
||||
|
||||
listAssemblies: () => MOCK
|
||||
? Promise.resolve(mockReflectionAssemblies())
|
||||
: get<ReflectionAssembly[]>('/assemblies'),
|
||||
|
||||
listObjects: (kind: ReflectionKind) => MOCK
|
||||
? Promise.resolve(mockReflectionObjects(kind))
|
||||
: get<ReflectionObject[]>(`/objects/${kind}`),
|
||||
|
||||
/** 列出当前可实例化的子类型(用于「新建」下拉);mock 模式直接给空。 */
|
||||
listCreatableTypes: (kind: ReflectionKind): Promise<ReflectionCreatableType[]> => MOCK
|
||||
? Promise.resolve([])
|
||||
: get<ReflectionCreatableType[]>(`/types/${kind}`),
|
||||
|
||||
/**
|
||||
* 创建一条对象。底层 POST `/objects/{kind}?...`,所有 extras 字段都作为 query 传给后端。
|
||||
*
|
||||
* 三类典型用法:
|
||||
* - process/script + UiDiscoveryCache 子类:`createObject('process', 'TaskFlow')`
|
||||
* - car(DummyCar 兜底,typeName 可空):`createObject('car', '', { name: 'AGV-1', x: 0, y: 0 })`
|
||||
* - site/track/image/text/model:`createObject('site', '', { x: 100, y: 200, name: 'A' })`
|
||||
*
|
||||
* 后端 BuildAndPersist 会做完整的字段类型转换;前端只负责传字符串。
|
||||
*/
|
||||
createObject: (
|
||||
kind: ReflectionKind,
|
||||
typeName?: string,
|
||||
extras?: Record<string, string | number | boolean | undefined>
|
||||
) => {
|
||||
if (MOCK) return Promise.resolve({ kind, id: -1, typeName: typeName ?? '' })
|
||||
const params: Record<string, string | number | boolean> = {}
|
||||
if (typeName) params.typeName = typeName
|
||||
if (extras) {
|
||||
for (const [k, v] of Object.entries(extras)) {
|
||||
if (v === undefined || v === null || v === '') continue
|
||||
params[k] = v
|
||||
}
|
||||
}
|
||||
return post<{ kind: string; id: number; typeName: string }>(`/objects/${kind}`, params)
|
||||
},
|
||||
|
||||
/** 删除一条对象(process / script / car / site / track / special)。 */
|
||||
deleteObject: (kind: ReflectionKind, id: number) => MOCK
|
||||
? Promise.resolve({ kind, id, deleted: true })
|
||||
: del<{ kind: string; id: number; deleted: boolean }>(`/objects/${kind}/${id}`),
|
||||
|
||||
/**
|
||||
* 触发 SimpleLite 重新扫描 ./plugins 目录、加载新增 dll 并重建 UiDiscoveryCache。
|
||||
* 已加载的 dll 不会重复加载。
|
||||
* 返回:本次发现的 dll 总数、新加载的 assembly 数、当前可创建的 mission/car 类型计数。
|
||||
*/
|
||||
reloadPlugins: () => MOCK
|
||||
? Promise.resolve({ totalDlls: 0, newlyLoaded: 0, failed: 0, missionTypes: 0, carTypes: 0 })
|
||||
: post<{ totalDlls: number; newlyLoaded: number; failed: number; missionTypes: number; carTypes: number }>(
|
||||
'/plugins/reload'
|
||||
),
|
||||
|
||||
/** 列出当前已加载的所有 collectible 插件(PluginManager 跟踪范围内)。 */
|
||||
listPlugins: () => MOCK
|
||||
? Promise.resolve<PluginEntry[]>([])
|
||||
: get<PluginEntry[]>('/plugins'),
|
||||
|
||||
/**
|
||||
* 卸载一个 collectible 插件。
|
||||
* 失败原因常见:仍有 Mission / Car 实例占用插件类型 → 409。
|
||||
*/
|
||||
unloadPlugin: (name: string) => MOCK
|
||||
? Promise.resolve({ name, message: 'mock', missionTypes: 0, carTypes: 0 })
|
||||
: post<{ name: string; message: string; missionTypes: number; carTypes: number }>(
|
||||
`/plugins/${encodeURIComponent(name)}/unload`
|
||||
),
|
||||
|
||||
listMethods: (kind: ReflectionKind, id: number) => MOCK
|
||||
? Promise.resolve(mockReflectionMethods(kind))
|
||||
: get<ReflectionMethod[]>(`/methods/${kind}/${id}`),
|
||||
|
||||
listMethodsByType: (kind: ReflectionKind) => MOCK
|
||||
? Promise.resolve(mockReflectionMethodsByType(kind))
|
||||
: get<ReflectionTypeMethods[]>(`/methods-by-type/${kind}`),
|
||||
|
||||
getStatus: (kind: ReflectionKind, id: number) => MOCK
|
||||
? Promise.resolve(mockReflectionStatus(kind, id))
|
||||
: get<ReflectionKv[]>(`/status/${kind}/${id}`),
|
||||
|
||||
getFields: (kind: ReflectionKind, id: number) => MOCK
|
||||
? Promise.resolve(mockReflectionFields(kind, id))
|
||||
: get<ReflectionKv[]>(`/fields/${kind}/${id}`),
|
||||
|
||||
setField: (kind: ReflectionKind, id: number, field: string, value: string) => MOCK
|
||||
? Promise.resolve(mockReflectionSetField(kind, id, field, value))
|
||||
: post<{ kind: ReflectionKind; id: number; field: string; value: string }>(
|
||||
`/fields/${kind}/${id}/${encodeURIComponent(field)}`,
|
||||
{ value }
|
||||
),
|
||||
|
||||
deleteField: (kind: ReflectionKind, id: number, field: string) => MOCK
|
||||
? Promise.resolve(mockReflectionDeleteField(kind, id, field))
|
||||
: del<{ kind: ReflectionKind; id: number; field: string }>(
|
||||
`/fields/${kind}/${id}/${encodeURIComponent(field)}`
|
||||
),
|
||||
|
||||
getBundle: (kind: ReflectionKind, id: number) => MOCK
|
||||
? Promise.resolve(mockReflectionBundle(kind, id))
|
||||
: get<{
|
||||
kind: string
|
||||
id: number
|
||||
typeName: string
|
||||
fullTypeName?: string
|
||||
assembly: string
|
||||
summary: ReflectionObject
|
||||
methods: ReflectionMethod[]
|
||||
status: ReflectionKv[]
|
||||
/** 扁平 key→value 兼容旧组件。 */
|
||||
fields: Record<string, string>
|
||||
/** 带 source/locked/typeName 的字段表,新版「对象管理」面板用。 */
|
||||
fieldList?: ReflectionKv[]
|
||||
}>(`/bundle/${kind}/${id}`),
|
||||
|
||||
execute: (kind: ReflectionKind, id: number, method: string, params?: Record<string, string | number | boolean>) => MOCK
|
||||
? Promise.resolve(mockReflectionExecute(
|
||||
kind,
|
||||
id,
|
||||
method,
|
||||
Object.fromEntries(Object.entries(params ?? {}).map(([k, v]) => [k, String(v)]))
|
||||
))
|
||||
: post<{ returnValue: string }>(
|
||||
`/execute/${kind}/${id}/${encodeURIComponent(method)}`,
|
||||
params
|
||||
),
|
||||
|
||||
/** 车辆前往指定站点(Web「去某地」;不依赖 SimpleUI.GetPoint)。 */
|
||||
gotoCarSite: (carId: number, siteId: number) => MOCK
|
||||
? Promise.resolve({ carId, siteId, message: `mock goto ${carId} -> ${siteId}` })
|
||||
: post<{ carId: number; siteId: number; message: string }>(
|
||||
`/car/${carId}/goto-site`,
|
||||
{ siteId }
|
||||
),
|
||||
|
||||
getScriptSource: (id: number): Promise<ScriptSourcePayload> => MOCK
|
||||
? Promise.resolve({
|
||||
id,
|
||||
name: `MockScript#${id}`,
|
||||
typeName: 'CarProgram',
|
||||
state: 'Running',
|
||||
script: '// mock script'
|
||||
})
|
||||
: get<ScriptSourcePayload>(`/scripts/${id}/source`),
|
||||
|
||||
getScriptExceptionStatus: (id: number): Promise<ScriptExceptionStatusPayload> => MOCK
|
||||
? Promise.resolve({
|
||||
id,
|
||||
name: `MockScript#${id}`,
|
||||
typeName: 'CarProgram',
|
||||
car: 'AGV-1(#1)',
|
||||
state: 'Running',
|
||||
exception: '(无)',
|
||||
notifies: '(无)',
|
||||
report: '=== CarProgram 异常状态报告 ===\nstate : Running\n\n--- exception ---\n(无)'
|
||||
})
|
||||
: get<ScriptExceptionStatusPayload>(`/scripts/${id}/exception-status`),
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
// 地图监控配置(Configuration.conf.monitorVisibility)
|
||||
// /monitor-config GET 现有配置 + 各 kind 可勾选 fields/status/methods 全集
|
||||
// /monitor-config POST body JSON 全量覆盖
|
||||
// 空白名单 = 显示全部;非空 = 仅显示列表中的 key。
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
getMonitorConfig: (): Promise<MonitorConfigPayload> => MOCK
|
||||
? Promise.resolve({
|
||||
config: loadMockMonitorConfig(),
|
||||
available: emptyMonitorAvailableMap()
|
||||
})
|
||||
: get<MonitorConfigPayload>('/monitor-config'),
|
||||
|
||||
saveMonitorConfig: async (cfg: MonitorConfigSaveBody): Promise<MonitorConfigSaveBody> => {
|
||||
if (MOCK) {
|
||||
saveMockMonitorConfig(cfg)
|
||||
return cfg
|
||||
}
|
||||
const { data } = await http.post<ReflectionEnvelope<MonitorConfigSaveBody>>(
|
||||
`${BASE}/monitor-config`,
|
||||
cfg
|
||||
)
|
||||
if (!data?.success) throw new Error(data?.message ?? 'saveMonitorConfig failed')
|
||||
return data.data as MonitorConfigSaveBody
|
||||
},
|
||||
|
||||
// 选中同步:让 SimpleLite 3D 场景同步高亮被点击对象
|
||||
getSelection: () => MOCK
|
||||
? Promise.resolve<ReflectionSelection>({ kind: null, id: 0, name: '' })
|
||||
: get<ReflectionSelection>('/selection'),
|
||||
|
||||
setSelection: (kind: ReflectionKind, id: number) => MOCK
|
||||
? Promise.resolve({ kind, id, name: `mock(${kind}#${id})` })
|
||||
: post<{ kind: ReflectionKind; id: number; name: string }>('/selection', { kind, id }),
|
||||
|
||||
clearSelection: () => MOCK
|
||||
? Promise.resolve({ cleared: true })
|
||||
: post<{ cleared: boolean }>('/selection/clear'),
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
// 项目属性(Scene.conf 单例)
|
||||
// /project/fields GET → 列出 Scene.conf 全部 [FieldMember] 字段
|
||||
// /project/fields/{key} POST → ?value=xxx 写入单字段
|
||||
// /project/save POST → 把内存项目(含修改后的 conf)写回 JSON
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
getProjectFields: (): Promise<ProjectPropertiesPayload> => MOCK
|
||||
? Promise.resolve({
|
||||
target: 'Scene.conf',
|
||||
lastLoadedPath: null,
|
||||
autoloadPath: null,
|
||||
fields: []
|
||||
})
|
||||
: get<ProjectPropertiesPayload>('/project/fields'),
|
||||
|
||||
setProjectField: (field: string, value: string) => MOCK
|
||||
? Promise.resolve({ field, value })
|
||||
: post<{ field: string; value: string }>(
|
||||
`/project/fields/${encodeURIComponent(field)}`,
|
||||
{ value }
|
||||
),
|
||||
|
||||
/** 保存当前项目到磁盘。path 为空则后端用 LastLoadedPath / Configuration.conf.autoload。 */
|
||||
saveProject: (path?: string) => MOCK
|
||||
? Promise.resolve({ path: path ?? '(mock)' })
|
||||
: post<{ path: string }>('/project/save', path ? { path } : undefined),
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
// 核心配置(simple.json / Configuration.conf)
|
||||
// /app-config/fields GET → 列出 Configuration.conf 全部 [FieldMember]
|
||||
// /app-config/fields/{key} POST → ?value=xxx 写入单字段
|
||||
// /app-config/save POST → 调 Configuration.ToFile("simple.json")
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
getAppConfigFields: (): Promise<AppConfigPayload> => MOCK
|
||||
? Promise.resolve({ target: 'Configuration.conf', savePath: 'simple.json', fields: [] })
|
||||
: get<AppConfigPayload>('/app-config/fields'),
|
||||
|
||||
setAppConfigField: (field: string, value: string) => MOCK
|
||||
? Promise.resolve({ field, value })
|
||||
: post<{ field: string; value: string }>(
|
||||
`/app-config/fields/${encodeURIComponent(field)}`,
|
||||
{ value }
|
||||
),
|
||||
|
||||
saveAppConfig: () => MOCK
|
||||
? Promise.resolve({ path: 'simple.json' })
|
||||
: post<{ path: string }>('/app-config/save'),
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
// 车型样式(WorkspaceCarStylesByType / WorkspaceAlarmColorScheme)
|
||||
// /car-style/types GET 列出所有 Car 子类型样式
|
||||
// /car-style/{typeFullName} GET 单个车型当前样式
|
||||
// /car-style/{typeFullName} POST body JSON 全量覆盖
|
||||
// /car-style/{typeFullName} DELETE 恢复为默认
|
||||
// /car-style/alarm-colors GET 7 种报警键 → 颜色
|
||||
// /car-style/alarm-colors POST body JSON 全量覆盖映射
|
||||
// /car-style/save POST 写回 simple.json
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
getCarStyleTypes: () => MOCK
|
||||
? Promise.resolve<CarStyleTypesPayload>({ globalDefault: defaultCarStyleDto(), types: [] })
|
||||
: get<CarStyleTypesPayload>('/car-style/types'),
|
||||
|
||||
getCarStyle: (typeFullName: string) => MOCK
|
||||
? Promise.resolve(defaultCarStyleDto())
|
||||
: get<CarStyleDto>(`/car-style/${encodeURIComponent(typeFullName)}`),
|
||||
|
||||
/** 用 axios 直接以 JSON body 提交,避免拼 query。 */
|
||||
putCarStyle: async (typeFullName: string, body: CarStyleDto) => {
|
||||
if (MOCK) return body
|
||||
const { data } = await http.post<ReflectionEnvelope<CarStyleDto>>(
|
||||
`${BASE}/car-style/${encodeURIComponent(typeFullName)}`,
|
||||
body
|
||||
)
|
||||
if (!data?.success) throw new Error(data?.message ?? 'putCarStyle failed')
|
||||
return data.data as CarStyleDto
|
||||
},
|
||||
|
||||
deleteCarStyle: (typeFullName: string) => MOCK
|
||||
? Promise.resolve({ typeFullName, removed: true })
|
||||
: del<{ typeFullName: string; removed: boolean }>(`/car-style/${encodeURIComponent(typeFullName)}`),
|
||||
|
||||
getAlarmColors: () => MOCK
|
||||
? Promise.resolve<AlarmColorsPayload>({ defaultKeys: [], entries: [] })
|
||||
: get<AlarmColorsPayload>('/car-style/alarm-colors'),
|
||||
|
||||
putAlarmColors: async (palette: Record<string, number>) => {
|
||||
if (MOCK) return { count: Object.keys(palette).length }
|
||||
const { data } = await http.post<ReflectionEnvelope<{ count: number }>>(
|
||||
`${BASE}/car-style/alarm-colors`,
|
||||
palette
|
||||
)
|
||||
if (!data?.success) throw new Error(data?.message ?? 'putAlarmColors failed')
|
||||
return data.data as { count: number }
|
||||
},
|
||||
|
||||
saveCarStyle: () => MOCK
|
||||
? Promise.resolve({ path: 'simple.json' })
|
||||
: post<{ path: string }>('/car-style/save')
|
||||
}
|
||||
|
||||
export interface CarStyleDto {
|
||||
bodyLengthM: number
|
||||
bodyWidthM: number
|
||||
bodyColorArgb: number
|
||||
outlineColorArgb: number
|
||||
labelColorArgb: number
|
||||
showLabel: boolean
|
||||
modelPath: string
|
||||
}
|
||||
|
||||
export interface CarStyleTypeRow {
|
||||
typeName: string
|
||||
shortName: string
|
||||
label: string
|
||||
assemblyName: string
|
||||
hasOverride: boolean
|
||||
style: CarStyleDto
|
||||
}
|
||||
|
||||
export interface CarStyleTypesPayload {
|
||||
globalDefault: CarStyleDto
|
||||
types: CarStyleTypeRow[]
|
||||
}
|
||||
|
||||
export interface AlarmColorEntry {
|
||||
key: string
|
||||
colorArgb: number
|
||||
isDefault: boolean
|
||||
}
|
||||
|
||||
export interface AlarmColorsPayload {
|
||||
defaultKeys: string[]
|
||||
entries: AlarmColorEntry[]
|
||||
}
|
||||
|
||||
function defaultCarStyleDto(): CarStyleDto {
|
||||
return {
|
||||
bodyLengthM: 0.64,
|
||||
bodyWidthM: 0.42,
|
||||
bodyColorArgb: 0xffffffff,
|
||||
outlineColorArgb: 0xff2a2a2a,
|
||||
labelColorArgb: 0xffffffff,
|
||||
showLabel: true,
|
||||
modelPath: ''
|
||||
}
|
||||
}
|
||||
|
||||
export interface ProjectPropertyRow {
|
||||
key: string
|
||||
label: string
|
||||
value: string
|
||||
typeName: string
|
||||
locked: boolean
|
||||
}
|
||||
|
||||
export interface ProjectPropertiesPayload {
|
||||
target: string
|
||||
lastLoadedPath: string | null
|
||||
autoloadPath: string | null
|
||||
fields: ProjectPropertyRow[]
|
||||
}
|
||||
|
||||
export interface AppConfigPayload {
|
||||
target: string
|
||||
savePath: string
|
||||
fields: ProjectPropertyRow[]
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import http from './http'
|
||||
import type { WorkbenchList, WorkbenchNav, SelectionDetail, DetailTab } from '@/types/workbench'
|
||||
import { mockWorkbenchList, mockSelectionDetail } from '@/mock/data/workbench'
|
||||
|
||||
// 与 api/* 其它模块统一走 VITE_USE_MOCK 开关(替代旧的 VITE_PROJECTION_MOCK)。
|
||||
const MOCK = import.meta.env.VITE_USE_MOCK === 'true'
|
||||
|
||||
export async function listWorkbench(nav: WorkbenchNav): Promise<WorkbenchList> {
|
||||
if (MOCK) return mockWorkbenchList(nav)
|
||||
const { data } = await http.get<WorkbenchList>(`/sl/projection/workbench/${nav}`)
|
||||
return data
|
||||
}
|
||||
|
||||
export async function getSelectionDetail(
|
||||
objectKind: string,
|
||||
objectId: string,
|
||||
tab: DetailTab
|
||||
): Promise<SelectionDetail> {
|
||||
if (MOCK) return mockSelectionDetail(objectKind, objectId, tab)
|
||||
const { data } = await http.get<SelectionDetail>('/sl/projection/selection/detail', {
|
||||
params: { objectKind, objectId, tab }
|
||||
})
|
||||
return data
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import http from './http'
|
||||
|
||||
/**
|
||||
* 工作区画布工具栏 API(对应 SimpleLite `WorkspaceToolbarApiController`)。
|
||||
*
|
||||
* 历史背景(会话33):原 SimpleLite iframe 内 ImGui 底栏 (`Panel_4` /
|
||||
* `WorkspaceBottomBar.DefineForTerminalEmbedMinimal`) 提供 8 个按钮,平台 iframe
|
||||
* 嵌入时该底栏会盖在画布上影响交互。本会话把这条底栏整体迁到 Vue 端
|
||||
* `WorkspaceCanvasToolbar.vue` 渲染,状态读写改走该 HTTP API。
|
||||
*
|
||||
* 路径前缀:`/sl/projection/toolbar` (Platform.Server YARP → SimpleLite EmbedIO :8222)
|
||||
*/
|
||||
|
||||
const BASE = '/sl/projection/toolbar'
|
||||
|
||||
export type AlignKey = 'sites' | 'cars' | 'tracks'
|
||||
export type SelectKey = 'tracks' | 'cars' | 'decor' | 'sites'
|
||||
export type DisplayKey = 'labels' | 'primitives' | 'cars'
|
||||
|
||||
export interface ToolbarRecordingEntry {
|
||||
fileName: string
|
||||
fileSizeBytes: number
|
||||
fileWriteTime: string
|
||||
note: string
|
||||
durationMs: number
|
||||
frameCount: number
|
||||
headerReadable: boolean
|
||||
error: string
|
||||
}
|
||||
|
||||
export interface ToolbarRecordingState {
|
||||
isRecording: boolean
|
||||
mode: string
|
||||
isPlaying: boolean
|
||||
elapsedMs: number
|
||||
frameCount: number
|
||||
droppedFrames: number
|
||||
currentRecordingFile: string | null
|
||||
recordingsDirectory: string
|
||||
entries: ToolbarRecordingEntry[]
|
||||
}
|
||||
|
||||
export interface ToolbarLayer {
|
||||
name: string
|
||||
visible: boolean
|
||||
}
|
||||
|
||||
export interface ToolbarState {
|
||||
align: { sites: boolean; cars: boolean; tracks: boolean }
|
||||
select: { tracks: boolean; cars: boolean; decor: boolean; sites: boolean }
|
||||
display: { labels: boolean; primitives: boolean; cars: boolean }
|
||||
layers: ToolbarLayer[]
|
||||
recording: ToolbarRecordingState
|
||||
}
|
||||
|
||||
interface Envelope<T> {
|
||||
success: boolean
|
||||
code: number
|
||||
data: T | null
|
||||
message: string
|
||||
}
|
||||
|
||||
function compatTogglePayload(key: string, value: boolean) {
|
||||
return { key, value, Key: key, Value: value }
|
||||
}
|
||||
|
||||
function compatLayerPayload(name: string, visible: boolean) {
|
||||
return { name, visible, Name: name, Visible: visible }
|
||||
}
|
||||
|
||||
function compatNotePayload(note: string) {
|
||||
return { note, Note: note }
|
||||
}
|
||||
|
||||
function compatFilePayload(fileName: string) {
|
||||
return { fileName, FileName: fileName }
|
||||
}
|
||||
|
||||
async function unwrap<T>(p: Promise<{ data: Envelope<T> }>): Promise<T> {
|
||||
const resp = (await p).data
|
||||
if (!resp?.success) throw new Error(resp?.message ?? '请求失败')
|
||||
if (resp.data == null) throw new Error('服务端返回空数据')
|
||||
return resp.data
|
||||
}
|
||||
|
||||
export const workspaceToolbarApi = {
|
||||
getState: () => unwrap<ToolbarState>(http.get(`${BASE}/state`)),
|
||||
|
||||
setAlign: (key: AlignKey, value: boolean) =>
|
||||
unwrap<ToolbarState>(http.post(`${BASE}/align`, compatTogglePayload(key, value))),
|
||||
|
||||
setSelect: (key: SelectKey, value: boolean) =>
|
||||
unwrap<ToolbarState>(http.post(`${BASE}/select`, compatTogglePayload(key, value))),
|
||||
|
||||
setDisplay: (key: DisplayKey, value: boolean) =>
|
||||
unwrap<ToolbarState>(http.post(`${BASE}/display`, compatTogglePayload(key, value))),
|
||||
|
||||
setLayerVisibility: (name: string, visible: boolean) =>
|
||||
unwrap<ToolbarState>(http.post(`${BASE}/layer`, compatLayerPayload(name, visible))),
|
||||
|
||||
startRecording: (note?: string) =>
|
||||
unwrap<ToolbarState>(http.post(`${BASE}/recording/start`, compatNotePayload(note ?? ''))),
|
||||
|
||||
stopRecording: () => unwrap<ToolbarState>(http.post(`${BASE}/recording/stop`)),
|
||||
|
||||
deleteRecording: (fileName: string) =>
|
||||
unwrap<ToolbarState>(http.delete(`${BASE}/recording/${encodeURIComponent(fileName)}`)),
|
||||
|
||||
startPlayback: (fileName: string) =>
|
||||
unwrap<ToolbarState>(http.post(`${BASE}/playback/start`, compatFilePayload(fileName))),
|
||||
|
||||
stopPlayback: () => unwrap<ToolbarState>(http.post(`${BASE}/playback/stop`))
|
||||
}
|
||||
Reference in New Issue
Block a user