feat(platform): improve map and project save workflows

This commit is contained in:
zhaowei.huang
2026-06-08 18:15:54 +08:00
parent 991502b4b1
commit 3dd08e28e5
11 changed files with 676 additions and 87 deletions
@@ -0,0 +1,22 @@
import type { ReflectionKind } from '@/api/reflection'
/** 管理页「查看对象」可跳转地图并定位的反射 kind(进程/脚本等逻辑对象除外)。 */
export type MapFocusKind = 'car' | 'site' | 'track' | 'special'
const MAP_VIEWABLE_KINDS = new Set<ReflectionKind>(['car', 'vehicle', 'site', 'track', 'special'])
export function isMapViewableKind(kind: ReflectionKind): boolean {
return MAP_VIEWABLE_KINDS.has(kind)
}
export function toMapFocusKind(kind: ReflectionKind): MapFocusKind | null {
if (kind === 'vehicle' || kind === 'car') return 'car'
if (kind === 'site' || kind === 'track' || kind === 'special') return kind
return null
}
export const MAP_MONITOR_PATH = '/admin/map-monitor'
export function buildMapFocusQuery(kind: MapFocusKind, id: number) {
return { focusKind: kind, focusId: String(id) }
}
@@ -0,0 +1,14 @@
import type { ReflectionKind } from '@/api/reflection'
/** 增删改会写入 SimpleProject 内存、需落盘到项目 JSON 的 kind(不含脚本等运行时只读列表)。 */
const PROJECT_PERSISTABLE_KINDS = new Set<ReflectionKind>([
'car',
'site',
'track',
'special',
'process'
])
export function isProjectPersistableKind(kind: ReflectionKind): boolean {
return PROJECT_PERSISTABLE_KINDS.has(kind)
}
@@ -0,0 +1,127 @@
import { ElMessage, ElMessageBox } from 'element-plus'
import { mapsApi } from '@/api/mapEdit'
import { reflectionApi } from '@/api/reflection'
export type SaveMode = 'new' | 'replace' | 'cancel'
/** 文件名安全的时间戳:yyyyMMdd_HHmmss */
export function formatSaveTimestamp(d = new Date()): string {
const p = (n: number) => String(n).padStart(2, '0')
return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}_${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`
}
/** 名称后追加修改日期。 */
export function defaultNewSaveName(baseName: string, d = new Date()): string {
const trimmed = baseName.trim()
if (!trimmed) return formatSaveTimestamp(d)
return `${trimmed}_${formatSaveTimestamp(d)}`
}
function fileBaseName(path: string): string {
const normalized = path.replace(/\\/g, '/')
const file = normalized.slice(normalized.lastIndexOf('/') + 1)
const dot = file.lastIndexOf('.')
return dot >= 0 ? file.slice(0, dot) : file
}
/** 与源路径同目录拼接新文件名。 */
export function joinPathWithName(sourcePath: string, newBaseName: string, ext = '.json'): string {
const normalized = sourcePath.replace(/\\/g, '/')
const slash = normalized.lastIndexOf('/')
const sep = sourcePath.includes('\\') ? '\\' : '/'
// 统一用归一化后的斜杠下标取目录,再换回平台分隔符;避免混合分隔符(如 C:\foo/bar.json)算错目录。
const dir = slash >= 0 ? normalized.slice(0, slash).replace(/\//g, sep) : ''
const file = `${newBaseName}${ext}`
return dir ? `${dir}${sep}${file}` : file
}
export async function resolveBaseProjectPath(): Promise<string | null> {
try {
const pf = await reflectionApi.getProjectFields()
if (pf.lastLoadedPath?.trim()) return pf.lastLoadedPath.trim()
if (pf.autoloadPath?.trim()) return pf.autoloadPath.trim()
} catch {
// ignore
}
return null
}
/** 解析当前正在使用的地图名(编辑页未带 ?map= 时兜底)。 */
export async function resolveCurrentMapName(): Promise<string> {
try {
const r = await mapsApi.list()
const cur = r.maps.find((m) => m.isCurrent)
if (cur?.name?.trim()) return cur.name.trim()
if (r.currentFileName?.trim()) {
return r.currentFileName.replace(/\.json$/i, '').trim()
}
} catch {
// ignore
}
return ''
}
const SAVE_NAME_PATTERN = /^[^\\/:*?"<>|]+$/
/** 是否保存成新项目?否 = 在原项目基础上直接覆盖。 */
export async function promptSaveMode(): Promise<SaveMode> {
try {
await ElMessageBox.confirm(
'是否保存成新项目?\n\n' +
'· 否,在原项目基础上保存:名称不变,直接覆盖原文件\n' +
'· 是,保存成新项目:在服务器另存(默认名称含修改日期)',
'保存确认',
{
confirmButtonText: '是,保存成新项目',
cancelButtonText: '否,在原项目基础上保存',
distinguishCancelAndClose: true,
type: 'info'
}
)
return 'new'
} catch (action) {
if (action === 'cancel') return 'replace'
return 'cancel'
}
}
/** 弹窗输入另存名称,默认「原名_修改日期」。 */
export async function promptNewSaveName(
title: string,
baseName: string
): Promise<string | null> {
try {
const r = await ElMessageBox.prompt(`请输入${title}名称`, title, {
inputValue: defaultNewSaveName(baseName),
inputPattern: SAVE_NAME_PATTERN,
inputErrorMessage: '名称不能包含 \\ / : * ? " < > | 等字符',
confirmButtonText: '保存到服务器',
cancelButtonText: '取消'
})
const picked = (r.value ?? '').trim()
return picked || null
} catch {
return null
}
}
/** 项目:在原路径上覆盖,或另存为新文件(同目录 + 新名称)。 */
export async function executeProjectSave(mode: SaveMode): Promise<string | null> {
if (mode === 'cancel') return null
if (mode === 'replace') {
const r = await reflectionApi.saveProject()
return r.path
}
const basePath = await resolveBaseProjectPath()
const baseName = basePath ? fileBaseName(basePath) : 'project'
const newName = await promptNewSaveName('保存成新项目', baseName)
if (!newName) return null
const targetPath = basePath
? joinPathWithName(basePath, newName)
: `${newName}.json`
const r = await reflectionApi.saveProject(targetPath)
return r.path
}