优化地图编辑中的字段管理
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user