优化地图编辑中的字段管理

This commit is contained in:
18086616529
2026-06-26 09:23:16 +08:00
parent e7bd326ea2
commit ce66da772e
10 changed files with 598 additions and 139 deletions
@@ -70,7 +70,7 @@
@click="onQuickClick(item)"
>
<span
v-if="item.key !== 'add'"
v-if="item.key !== 'add' && !isMandatoryQuickKey(item.key)"
class="quick-item-remove"
title="移除"
@pointerdown.stop
@@ -407,7 +407,8 @@ const {
openPicker,
addKey,
removeKey,
swapKeys
swapKeys,
isMandatoryQuickKey
} = useDashboardQuickEntries()
interface QuickTile {
@@ -41,13 +41,14 @@
:defaults="defaults"
:layer-rows="layerRows"
:selection-kind="selectionKind"
:car-type="selectionCarType"
:viewport-style="viewportStyle"
:viewport-style-loading="viewportStyleLoading"
:viewport-syncing="viewportSyncing"
@rename="onRename"
@change-layer="onChangeLayer"
@set-field="onSetField"
@add-field="onAddField"
@delete-field="onDeleteField"
@exec-action="onExecAction"
@save-defaults="onSaveDefaults"
@layer-toggle="onLayerToggle"
@@ -241,15 +242,23 @@ let viewportPatchTimer: ReturnType<typeof setTimeout> | null = null
let viewportPatchSeq = 0
let pendingViewportPatch: { site?: ViewportStyleSite; track?: ViewportStyleTrack } = {}
/** 单选且为 site/track 时驱动「类型默认」面板;多选或车型等返回 null。 */
const selectionKind = computed<'site' | 'track' | null>(() => {
/** 单选且为 site/track/car 时驱动属性扩展;类型默认 Tab 仍只支持 site/track。 */
const selectionKind = computed<'site' | 'track' | 'car' | null>(() => {
const items = selection.items.value
if (items.length !== 1) return null
const k = items[0]!.kind
if (k === 'site' || k === 'track') return k
if (k === 'car') return 'car'
return null
})
/** 当前选中车的车型唯一标识(用于 simple_fields.carFields 过滤)。 */
const selectionCarType = computed<string | null>(() => {
if (selectionKind.value !== 'car') return null
// 选中车后 primary 会刷新为真实对象摘要,typeName 比 selection item 更可靠(不是 "Car" 占位名)。
return primary.value?.typeName?.trim() || null
})
const knownLayers = ref<string[]>(['g'])
interface LayerRow { name: string; visible: boolean; selectable: boolean; color: string }
const layerRows = ref<LayerRow[]>([
@@ -1138,7 +1147,7 @@ async function applyBatchAsCommand(label: string, ops: { action: 'create' | 'del
// 属性面板交互
// ──────────────────────────────────────────────────────────────────────────
async function refreshPrimary() {
async function refreshPrimary(opts?: { silent?: boolean }) {
if (selection.items.value.length === 0) {
primary.value = null
primaryFields.value = []
@@ -1147,7 +1156,7 @@ async function refreshPrimary() {
return
}
const first = selection.items.value[0]!
primaryLoadingStart()
if (!opts?.silent) primaryLoadingStart()
try {
const b = await reflectionApi.getBundle(first.kind, first.id)
primary.value = b.summary
@@ -1157,13 +1166,32 @@ async function refreshPrimary() {
} catch (err) {
ElMessage.error(`加载属性失败:${(err as Error).message}`)
} finally {
primaryLoadingEnd()
if (!opts?.silent) primaryLoadingEnd()
}
}
function primaryLoadingStart() { propertyLoading.value = true }
function primaryLoadingEnd() { propertyLoading.value = false }
function patchPrimaryFieldLocal(key: string, value: string) {
const idx = primaryFields.value.findIndex((f) => f.key === key)
if (idx >= 0) primaryFields.value[idx] = { key, value }
else primaryFields.value = [...primaryFields.value, { key, value }]
}
function removePrimaryFieldLocal(key: string) {
primaryFields.value = primaryFields.value.filter((f) => f.key !== key)
}
let silentRefreshTimer: ReturnType<typeof setTimeout> | null = null
function scheduleSilentRefreshPrimary() {
if (silentRefreshTimer) clearTimeout(silentRefreshTimer)
silentRefreshTimer = setTimeout(() => {
silentRefreshTimer = null
void refreshPrimary({ silent: true })
}, 200)
}
async function onRename(name: string) {
if (!primary.value || selection.items.value.length === 0) return
const it = selection.items.value[0]!
@@ -1205,34 +1233,55 @@ async function onChangeLayer(layer: string) {
async function onSetField(key: string, value: string) {
if (!primary.value) return
const it = selection.items.value[0]!
// 抓原值以便 revert 真正回滚
let oldValue: string | undefined
try {
const b = await reflectionApi.getBundle(it.kind, primary.value.id)
oldValue = b.fields?.[key] != null ? String(b.fields[key]) : undefined
} catch { /* 抓不到原值就只能不回滚 */ }
const hadKey = primaryFields.value.some((f) => f.key === key)
const oldValue = primaryFields.value.find((f) => f.key === key)?.value
await history.run({
label: `修改 ${key}`,
apply: async () => { await reflectionApi.setField(it.kind, primary.value!.id, key, value) },
revert: async () => {
if (oldValue !== undefined) {
try { await reflectionApi.setField(it.kind, primary.value!.id, key, oldValue) }
catch (err) { ElMessage.warning(`回滚 ${key} 失败:${(err as Error).message}`) }
} else {
ElMessage.warning(`回滚 ${key}:没拿到原值快照,仅本地撤销`)
patchPrimaryFieldLocal(key, value)
try {
await history.run({
label: hadKey ? `修改 ${key}` : `添加字段 ${key}`,
apply: async () => { await reflectionApi.setField(it.kind, primary.value!.id, key, value) },
revert: async () => {
if (oldValue !== undefined) {
try { await reflectionApi.setField(it.kind, primary.value!.id, key, oldValue) }
catch (err) { ElMessage.warning(`回滚 ${key} 失败:${(err as Error).message}`) }
} else {
try { await reflectionApi.deleteField(it.kind, primary.value!.id, key) }
catch (err) { ElMessage.warning(`回滚字段 ${key} 失败:${(err as Error).message}`) }
}
}
}
})
await refreshPrimary()
})
} catch (err) {
if (oldValue !== undefined) patchPrimaryFieldLocal(key, oldValue)
else removePrimaryFieldLocal(key)
ElMessage.error(`保存字段失败:${(err as Error).message}`)
return
}
scheduleSilentRefreshPrimary()
}
async function onAddField() {
async function onDeleteField(key: string) {
if (!primary.value) return
const it = selection.items.value[0]!
const oldValue = primaryFields.value.find((f) => f.key === key)?.value
if (oldValue === undefined) return
removePrimaryFieldLocal(key)
try {
const k = (await ElMessageBox.prompt('字段名', '新增字段', { inputPattern: /^[A-Za-z0-9_]+$/ })).value
const v = (await ElMessageBox.prompt('字段值', '新增字段', { inputValue: '' })).value
await onSetField(k, v)
} catch { /* cancel */ }
await history.run({
label: `删除字段 ${key}`,
apply: async () => { await reflectionApi.deleteField(it.kind, primary.value!.id, key) },
revert: async () => {
try { await reflectionApi.setField(it.kind, primary.value!.id, key, oldValue) }
catch (err) { ElMessage.warning(`回滚字段 ${key} 失败:${(err as Error).message}`) }
}
})
} catch (err) {
patchPrimaryFieldLocal(key, oldValue)
ElMessage.error(`删除字段失败:${(err as Error).message}`)
return
}
scheduleSilentRefreshPrimary()
}
async function onExecAction(method: string) {
@@ -1555,8 +1604,7 @@ const stream = useMapEditStream({
})
function refreshAfterMutation() {
// 简化:刷新当前选中的属性
refreshPrimary()
scheduleSilentRefreshPrimary()
}
watch(() => stream.connected.value, (v) => (streamConnected.value = v), { immediate: true })
@@ -0,0 +1,39 @@
<template>
<ConfigPageBase
section="location"
title="库位管理"
description="出入库、库存、库位可视化(LocationManagement"
:defaults="DEFAULT_LOCATION">
<template #default="{ payload }">
<el-tabs model-value="locs">
<el-tab-pane name="locs" label="库位">
<el-table :data="payload.locations" size="small" border>
<el-table-column label="ID" prop="id" width="100" />
<el-table-column label="编码" prop="code" width="120" />
<el-table-column label="名称" prop="name" />
<el-table-column label="站点" prop="siteId" width="100" />
<el-table-column label="容量" prop="capacity" width="100" />
<el-table-column label="占用">
<template #default="s">
<el-progress :percentage="Math.round((s.row.occupied / s.row.capacity) * 100)" :stroke-width="10" />
</template>
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane name="rules" label="库存规则">
<el-table :data="payload.inventoryRules" size="small" border>
<el-table-column label="ID" prop="id" width="100" />
<el-table-column label="物料类型" prop="itemType" />
<el-table-column label="下限" prop="minQty" width="100" />
<el-table-column label="上限" prop="maxQty" width="100" />
</el-table>
</el-tab-pane>
</el-tabs>
</template>
</ConfigPageBase>
</template>
<script setup lang="ts">
import ConfigPageBase from '@/components/ConfigPageBase.vue'
import { DEFAULT_LOCATION } from '@/mock/data/configs'
</script>