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,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))
|
||||
}
|
||||
Reference in New Issue
Block a user