|
@@ -475,7 +475,7 @@
|
|
|
-->
|
|
-->
|
|
|
<div v-if="detailOpen && !introVisible && selectionVisibleInMode && anchorLayout.visible && !fixedLocationReason"
|
|
<div v-if="detailOpen && !introVisible && selectionVisibleInMode && anchorLayout.visible && !fixedLocationReason"
|
|
|
class="cockpit-shell__anchor-lead" data-anchor-lead :style="anchorLeadStyle" aria-hidden="true"></div>
|
|
class="cockpit-shell__anchor-lead" data-anchor-lead :style="anchorLeadStyle" aria-hidden="true"></div>
|
|
|
- <aside v-if="detailOpen && !introVisible && selectionVisibleInMode" ref="detailRef" class="cockpit-shell__detail"
|
|
|
|
|
|
|
+ <aside v-if="detailOpen && !introVisible" v-show="selectionVisibleInMode" ref="detailRef" class="cockpit-shell__detail"
|
|
|
:class="{ 'is-anchored': !fixedLocationReason, 'is-fixed-left': facilityFixedLeft }"
|
|
:class="{ 'is-anchored': !fixedLocationReason, 'is-fixed-left': facilityFixedLeft }"
|
|
|
:style="detailStyle"
|
|
:style="detailStyle"
|
|
|
:data-anchor-state="fixedLocationReason ? 'fixed' : anchorLayout.visible ? 'visible' : 'hidden'"
|
|
:data-anchor-state="fixedLocationReason ? 'fixed' : anchorLayout.visible ? 'visible' : 'hidden'"
|
|
@@ -1233,12 +1233,18 @@ const pipeEnhancementPlan = computed(() => {
|
|
|
deviceCoverage: enhancementDevicesStore.source,
|
|
deviceCoverage: enhancementDevicesStore.source,
|
|
|
relationCoverage: facilityRelationsState.value }
|
|
relationCoverage: facilityRelationsState.value }
|
|
|
})
|
|
})
|
|
|
-watch(pipeEnhancementSelection, () => enhancementDevicesStore.closeDetail())
|
|
|
|
|
|
|
+watch(() => JSON.stringify(pipeEnhancementSelection.value), () => enhancementDevicesStore.closeDetail())
|
|
|
|
|
+watch(pipeEnhancementRadius, () => enhancementDevicesStore.closeDetail())
|
|
|
|
|
+// Explicit filters/mode changes are a new scope; do not retain the old scope's local devices.
|
|
|
|
|
+watch(() => JSON.stringify([mode.value, scope.value, industry.value, deviceType.value, pointStatus.value]),
|
|
|
|
|
+ () => enhancementDevicesStore.clear(), { flush: 'sync' })
|
|
|
// 首次打开详情视图时取一次真实关联(只读、免认证);失败按空索引降级,绝不阻塞场景。
|
|
// 首次打开详情视图时取一次真实关联(只读、免认证);失败按空索引降级,绝不阻塞场景。
|
|
|
watch(() => pipeEnhancementPlan.value.visible, (visible) => {
|
|
watch(() => pipeEnhancementPlan.value.visible, (visible) => {
|
|
|
if (visible && !facilityRelationsState.value.loaded) void loadFacilityRelations()
|
|
if (visible && !facilityRelationsState.value.loaded) void loadFacilityRelations()
|
|
|
})
|
|
})
|
|
|
-watch(() => pipeEnhancementPlan.value.deviceList, (list) => enhancementDevicesStore.reconcile(list))
|
|
|
|
|
|
|
+watch(() => pipeEnhancementPlan.value.deviceList, (list) => {
|
|
|
|
|
+ if (!mapStore.refreshing) enhancementDevicesStore.reconcile(list)
|
|
|
|
|
+})
|
|
|
watch(() => mapStore.refreshing, (refreshing) => {
|
|
watch(() => mapStore.refreshing, (refreshing) => {
|
|
|
if (refreshing) enhancementDevicesStore.invalidate()
|
|
if (refreshing) enhancementDevicesStore.invalidate()
|
|
|
else if (pipeEnhancementPlan.value.visible) void enhancementDevicesStore.load(mapStore.summary)
|
|
else if (pipeEnhancementPlan.value.visible) void enhancementDevicesStore.load(mapStore.summary)
|
|
@@ -1274,9 +1280,15 @@ function closePipeEnhancement() {
|
|
|
pipeEnhancementStore.clear()
|
|
pipeEnhancementStore.clear()
|
|
|
enhancementDevicesStore.clear()
|
|
enhancementDevicesStore.clear()
|
|
|
}
|
|
}
|
|
|
-const selectionVisibleInMode = computed(() => facilityDetailOpen.value
|
|
|
|
|
- ? ['overview', 'infrastructure'].includes(mode.value)
|
|
|
|
|
- : modeLayerPlan.value.selection.visible === true)
|
|
|
|
|
|
|
+// Only a background refresh of the same viewing context may retain its visibility/anchor.
|
|
|
|
|
+const detailViewKey = computed(() => JSON.stringify([mode.value, scope.value, industry.value, deviceType.value, pointStatus.value,
|
|
|
|
|
+ selection.value.level, selection.value.pointCode, selection.value.deviceCode, entityDetailStore.identity]))
|
|
|
|
|
+const detailRefreshState = ref(null)
|
|
|
|
|
+const keepDetailDuringRefresh = computed(() => detailRefreshState.value?.key === detailViewKey.value)
|
|
|
|
|
+watch(detailViewKey, () => { detailRefreshState.value = null }, { flush: 'sync' })
|
|
|
|
|
+const selectionVisibleInMode = computed(() => keepDetailDuringRefresh.value ? detailRefreshState.value.visible
|
|
|
|
|
+ : facilityDetailOpen.value ? ['overview', 'infrastructure'].includes(mode.value)
|
|
|
|
|
+ : modeLayerPlan.value.selection.visible === true)
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* 快照变化 → 先撤下旧设备图层(规格:刷新或快照失效时先撤下,免得旧快照的设备被误读成新范围的)。
|
|
* 快照变化 → 先撤下旧设备图层(规格:刷新或快照失效时先撤下,免得旧快照的设备被误读成新范围的)。
|
|
@@ -1306,24 +1318,36 @@ function syncDeviceLayerForSnapshot(batchUpdatedAt) {
|
|
|
* · **完整加载仍找不到**:关闭详情并在顶栏说明原因。
|
|
* · **完整加载仍找不到**:关闭详情并在顶栏说明原因。
|
|
|
* 查找失败(failed 相位)**保持待核对**:已取数据与顶栏关闭入口都在,允许再次刷新重试。
|
|
* 查找失败(failed 相位)**保持待核对**:已取数据与顶栏关闭入口都在,允许再次刷新重试。
|
|
|
*/
|
|
*/
|
|
|
-function reconcileSnapshotSelection() {
|
|
|
|
|
- const pointCode = selection.value.pointCode
|
|
|
|
|
- if (!pointCode || selection.value.level === 'none') {
|
|
|
|
|
|
|
+async function reconcileSnapshotSelection() {
|
|
|
|
|
+ // A reload can pass through an empty/settled state before the current chain completes.
|
|
|
|
|
+ if (mapStore.refreshing) return
|
|
|
|
|
+ const { pointCode, deviceCode, level } = selection.value
|
|
|
|
|
+ if ((!pointCode && !deviceCode) || level === 'none') {
|
|
|
snapshotPending.value = false
|
|
snapshotPending.value = false
|
|
|
|
|
+ detailRefreshState.value = null
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
const phase = mapStore.summary.phase
|
|
const phase = mapStore.summary.phase
|
|
|
- // 失败与失效都是「查找失败」:保持待核对、已取数据与顶栏关闭入口,允许再次刷新重试
|
|
|
|
|
- // (规格 14:86 原句)——不得对已失效的快照做「找到即重载」。
|
|
|
|
|
if (phase === MAP_SUMMARY_PHASE.FAILED || phase === MAP_SUMMARY_PHASE.EXPIRED) return
|
|
if (phase === MAP_SUMMARY_PHASE.FAILED || phase === MAP_SUMMARY_PHASE.EXPIRED) return
|
|
|
- if (mapStore.hasSnapshotObject('monitoring-point', pointCode)) {
|
|
|
|
|
|
|
+ const kind = pointCode ? 'monitoring-point' : 'unassociated-equipment'
|
|
|
|
|
+ const code = pointCode || deviceCode
|
|
|
|
|
+ if (mapStore.hasSnapshotObject(kind, code)) {
|
|
|
snapshotPending.value = false
|
|
snapshotPending.value = false
|
|
|
- void detailStore.reloadPointDetail(pointCode)
|
|
|
|
|
|
|
+ const retained = detailRefreshState.value
|
|
|
|
|
+ const results = await Promise.all([
|
|
|
|
|
+ pointCode ? detailStore.reloadPointDetail(pointCode) : null,
|
|
|
|
|
+ deviceCode ? detailStore.loadEquipmentAggregate(deviceCode) : null,
|
|
|
|
|
+ ])
|
|
|
|
|
+ // Let the point-detail watcher restore its device layer before releasing the old anchor.
|
|
|
|
|
+ await nextTick()
|
|
|
|
|
+ if (detailRefreshState.value === retained && !mapStore.refreshing
|
|
|
|
|
+ && results.every((result) => !result || (!result.retained && !['failed', 'unavailable'].includes(result.state)))) detailRefreshState.value = null
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
if (phase === MAP_SUMMARY_PHASE.COMPLETE || phase === MAP_SUMMARY_PHASE.EMPTY) {
|
|
if (phase === MAP_SUMMARY_PHASE.COMPLETE || phase === MAP_SUMMARY_PHASE.EMPTY) {
|
|
|
snapshotPending.value = false
|
|
snapshotPending.value = false
|
|
|
- snapshotAbsentNote.value = `所选点位 ${pointCode} 已不在当前快照范围,详情已关闭`
|
|
|
|
|
|
|
+ detailRefreshState.value = null
|
|
|
|
|
+ snapshotAbsentNote.value = `所选${pointCode ? '点位' : '设备'} ${code} 已不在当前快照范围,详情已关闭`
|
|
|
detailStore.closeDetail()
|
|
detailStore.closeDetail()
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -1391,9 +1415,10 @@ const anchorOrigin = ref({ left: 0, top: 0 })
|
|
|
let lastAnchorKey = ''
|
|
let lastAnchorKey = ''
|
|
|
|
|
|
|
|
/** 摘要锚向监测点;设备详情锚向所选设备;业务归属编码保持不变。 */
|
|
/** 摘要锚向监测点;设备详情锚向所选设备;业务归属编码保持不变。 */
|
|
|
-const detailTarget = computed(() => facilityDetailOpen.value
|
|
|
|
|
- ? resolveFacilityDetailAnchor(facilityPointStore.selectedPoint)
|
|
|
|
|
- : resolveDetailAnchor({ selection: selection.value, objects: mapStore.summary.objects, devices: deviceRows.value }))
|
|
|
|
|
|
|
+const detailTarget = computed(() => keepDetailDuringRefresh.value && detailRefreshState.value.target
|
|
|
|
|
+ ? detailRefreshState.value.target
|
|
|
|
|
+ : facilityDetailOpen.value ? resolveFacilityDetailAnchor(facilityPointStore.selectedPoint)
|
|
|
|
|
+ : resolveDetailAnchor({ selection: selection.value, objects: mapStore.summary.objects, devices: deviceRows.value }))
|
|
|
|
|
|
|
|
/** 详情卡上要显示的对象编码(顶栏暂隐入口读它)。 */
|
|
/** 详情卡上要显示的对象编码(顶栏暂隐入口读它)。 */
|
|
|
const selectedDetailCode = computed(() => {
|
|
const selectedDetailCode = computed(() => {
|
|
@@ -1462,6 +1487,9 @@ function measureAnchor() {
|
|
|
function refreshAnchor(options = {}) {
|
|
function refreshAnchor(options = {}) {
|
|
|
if (!detailOpen.value || introVisible.value || typeof document === 'undefined') return
|
|
if (!detailOpen.value || introVisible.value || typeof document === 'undefined') return
|
|
|
if (document.hidden && options.force !== true) return
|
|
if (document.hidden && options.force !== true) return
|
|
|
|
|
+ const retainedTarget = keepDetailDuringRefresh.value && detailRefreshState.value.target
|
|
|
|
|
+ // The old marker is temporarily withdrawn, not a reason to hide/move a panel being read.
|
|
|
|
|
+ if (retainedTarget && !hasMapMarker(retainedTarget.kind, retainedTarget.code, retainedTarget.pointCode, retainedTarget.key)) return
|
|
|
// 维护者反馈:限速 10Hz 后"点位摘要/设备详情面板不跟手"。锚定必须**逐帧跟随**,
|
|
// 维护者反馈:限速 10Hz 后"点位摘要/设备详情面板不跟手"。锚定必须**逐帧跟随**,
|
|
|
// 与 DOM 图标同一条纪律;重复写 DOM 由下面的 `lastAnchorKey` 判定挡住,镜头不动时不写。
|
|
// 与 DOM 图标同一条纪律;重复写 DOM 由下面的 `lastAnchorKey` 判定挡住,镜头不动时不写。
|
|
|
|
|
|
|
@@ -2146,11 +2174,16 @@ onMounted(() => {
|
|
|
// ② 快照**失效**:expired 相位不改 batchUpdatedAt,必须单独触发撤层;
|
|
// ② 快照**失效**:expired 相位不改 batchUpdatedAt,必须单独触发撤层;
|
|
|
// ③ 链条 **settled**:对挂起中的所选点位核对收口(找到重载 / 完整找不到关闭 / 失败保持待核对)。
|
|
// ③ 链条 **settled**:对挂起中的所选点位核对收口(找到重载 / 完整找不到关闭 / 失败保持待核对)。
|
|
|
watch(() => mapStore.refreshing, (on) => {
|
|
watch(() => mapStore.refreshing, (on) => {
|
|
|
- if (!on) return
|
|
|
|
|
|
|
+ if (!on) {
|
|
|
|
|
+ if (snapshotPending.value && mapStore.settled) void reconcileSnapshotSelection()
|
|
|
|
|
+ else if (!snapshotPending.value) detailRefreshState.value = null
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ detailRefreshState.value = { key: detailViewKey.value, visible: selectionVisibleInMode.value, target: detailTarget.value }
|
|
|
deviceLayerStore.suspendForSnapshot()
|
|
deviceLayerStore.suspendForSnapshot()
|
|
|
snapshotAbsentNote.value = ''
|
|
snapshotAbsentNote.value = ''
|
|
|
- snapshotPending.value = Boolean(selection.value.pointCode)
|
|
|
|
|
- }, { deep: false }),
|
|
|
|
|
|
|
+ snapshotPending.value = Boolean(selection.value.pointCode || selection.value.deviceCode)
|
|
|
|
|
+ }, { deep: false, flush: 'sync' }),
|
|
|
watch(() => mapStore.summary.phase, (phase) => {
|
|
watch(() => mapStore.summary.phase, (phase) => {
|
|
|
if (phase !== MAP_SUMMARY_PHASE.EXPIRED || !selection.value.pointCode) return
|
|
if (phase !== MAP_SUMMARY_PHASE.EXPIRED || !selection.value.pointCode) return
|
|
|
deviceLayerStore.suspendForSnapshot()
|
|
deviceLayerStore.suspendForSnapshot()
|