|
|
@@ -19,34 +19,28 @@
|
|
|
<div class="pipe-enhancement__workspace">
|
|
|
<div ref="viewport" class="pipe-enhancement__viewport" data-pipe-enhancement-viewport role="region" aria-label="地下管线详情视图三维场景"></div>
|
|
|
<!--
|
|
|
- 设备详情浮层:维护者 2026-10-22 要求「点击后在三维场景中展现浮动 UI 面板」,
|
|
|
- 所以它脱离右侧列表、浮在场景之上(保持场景/相机不动),可单独关闭。
|
|
|
+ 维护者 2026-11-12:设备/设施详情不再浮在三维场景上(遮挡 3D 区域),
|
|
|
+ 改为右侧「资产详情」页签;「管道临近资产」保留原有的管道 + 设备双列列表。
|
|
|
-->
|
|
|
- <section v-if="deviceState.selected" class="pipe-enhancement__device-detail" data-enhancement-device-detail>
|
|
|
- <header>
|
|
|
- <strong>{{ deviceState.selected.name || deviceState.selected.code }}</strong>
|
|
|
- <button type="button" data-enhancement-device-close @click="$emit('device-close')">关闭详情</button>
|
|
|
- </header>
|
|
|
- <div class="pipe-enhancement__device-status-card" data-enhancement-device-status-card>
|
|
|
- <DeviceStateTags data-enhancement-device-status :device-code="deviceState.selected.code"
|
|
|
- :fallback-status="deviceState.selected.status || ''" :aggregate="deviceState.aggregate" />
|
|
|
- <span class="pipe-enhancement__device-status-label">设备状态</span>
|
|
|
- </div>
|
|
|
- <p>{{ deviceState.selected.placementText }}</p>
|
|
|
- <p v-if="deviceState.selected.pointCode">所属监测点:{{ deviceState.selected.pointCode }}</p>
|
|
|
- <p v-if="deviceState.loadingDetail && !deviceState.aggregate?.data">设备详情加载中</p>
|
|
|
- <template v-else-if="!deviceState.aggregate?.data"><p>设备详情暂不可用</p><button type="button" @click="$emit('device-retry')">重新加载</button></template>
|
|
|
- <EquipmentDetailSections v-else :aggregate="deviceState.aggregate" :device-code="deviceState.selected.code" monitor-source="enhancement" />
|
|
|
- </section>
|
|
|
- <aside class="pipe-enhancement__devices" aria-label="附近设备与详情">
|
|
|
+ <aside class="pipe-enhancement__side" aria-label="管道临近资产与资产详情">
|
|
|
+ <nav class="pipe-enhancement__side-tabs" role="tablist" aria-label="管线详情分区">
|
|
|
+ <button v-for="tab in sideTabs" :key="tab.key" type="button" role="tab"
|
|
|
+ :aria-selected="sideTab === tab.key" :class="{ 'is-active': sideTab === tab.key }"
|
|
|
+ :data-side-tab="tab.key" @click="sideTab = tab.key">{{ tab.label }}</button>
|
|
|
+ </nav>
|
|
|
+ <div v-show="sideTab === 'nearby'" class="pipe-enhancement__nearby" data-side-panel="nearby">
|
|
|
+ <aside class="pipe-enhancement__devices" aria-label="附近资产列表">
|
|
|
<section v-if="localPipeStates.length" class="pipe-enhancement__pipe-list" data-pipe-status-list aria-label="附近管道状态">
|
|
|
<h3>附近管道状态({{ localPipeStates.length }})</h3>
|
|
|
<div class="pipe-enhancement__pipe-scroll">
|
|
|
- <p v-for="pipe in pagedPipeStates" :key="pipe.code">
|
|
|
- <button type="button" class="pipe-enhancement__pipe" :aria-pressed="plan.object.sourceKind === 'pipe' && plan.object.code === pipe.code" @click="$emit('object-pick', { objectKind: 'pipe', code: pipe.code })">{{ pipe.title }}({{ pipe.code }})</button>:{{ pipe.stateText || '状态未提供' }}
|
|
|
- <small class="pipe-enhancement__dimensions">源管径 {{ pipe.diameterM ? pipe.diameter + ' mm' : '未提供(仅导引)' }} · 源材质 {{ pipe.material || '未提供' }}({{ pipe.surface?.label || '材质未识别' }})</small>
|
|
|
- <small class="pipe-enhancement__dimensions">{{ pipe.clipped ? '当前裁片' : '地理路径' }} {{ pipe.geographicLengthM?.toFixed(2) }} m · 后端管长 {{ pipe.lengthM ?? '未提供' }}{{ pipe.lengthM !== null ? '(源值)' : '' }}</small>
|
|
|
- </p>
|
|
|
+ <button v-for="pipe in pagedPipeStates" :key="pipe.code" type="button"
|
|
|
+ class="pipe-enhancement__asset-card" data-pipe-card
|
|
|
+ :aria-pressed="plan.object.sourceKind === 'pipe' && plan.object.code === pipe.code"
|
|
|
+ @click="$emit('object-pick', { objectKind: 'pipe', code: pipe.code })">
|
|
|
+ <b>{{ pipe.title }}</b>
|
|
|
+ <small>{{ pipe.code }} · {{ pipe.stateText || '状态未提供' }}</small>
|
|
|
+ <small>{{ pipe.diameterM ? pipe.diameter + ' mm' : '管径未提供' }} · {{ pipe.material || '材质未提供' }}</small>
|
|
|
+ </button>
|
|
|
</div>
|
|
|
<nav v-if="pipePageCount > 1" class="pipe-enhancement__pager" data-pipe-pager aria-label="附近管道状态翻页">
|
|
|
<button type="button" :disabled="pipePage <= 1" @click="pipePage -= 1">上一页</button>
|
|
|
@@ -63,11 +57,41 @@
|
|
|
<button v-if="plan.deviceCoverage?.partial && !plan.deviceCoverage?.loading" type="button" @click="$emit('devices-reload')">重新读取设备清单</button>
|
|
|
<p v-if="plan.deviceCoverage?.stalePoints">部分设备为上次保留数据,非实时。</p>
|
|
|
<p v-if="!plan.deviceList?.length && !plan.deviceCoverage?.loading">当前可用数据中没有附近设备。</p>
|
|
|
- <button v-for="device in plan.deviceList || []" :key="device.code" :data-device-code="device.code" class="pipe-enhancement__device" type="button" @click="selectDevice(device)">
|
|
|
- {{ device.name || device.code }}<small>{{ device.placementText }}{{ device.retained ? ' · 过期数据' : '' }} · 点击场景图标或此处查看详情</small>
|
|
|
+ <button v-for="device in plan.deviceList || []" :key="device.code" :data-device-code="device.code"
|
|
|
+ class="pipe-enhancement__asset-card" data-device-card type="button"
|
|
|
+ :aria-pressed="deviceState.selected?.code === device.code" @click="selectDevice(device)">
|
|
|
+ <b>{{ device.name || device.code }}</b>
|
|
|
+ <small>{{ device.code }} · {{ device.placementText }}{{ device.retained ? ' · 过期数据' : '' }}</small>
|
|
|
</button>
|
|
|
</div>
|
|
|
- </section>
|
|
|
+ </section>
|
|
|
+ </aside>
|
|
|
+ </div>
|
|
|
+ <div v-show="sideTab === 'detail'" class="pipe-enhancement__asset-pane" data-side-panel="detail">
|
|
|
+ <AssetDetailPanel :context="assetContext">
|
|
|
+ <template v-if="assetKind === 'device'">
|
|
|
+ <div class="pipe-enhancement__device-status-card" data-enhancement-device-status-card>
|
|
|
+ <DeviceStateTags data-enhancement-device-status :device-code="deviceState.selected.code"
|
|
|
+ :fallback-status="deviceState.selected.status || ''" :aggregate="deviceState.aggregate" />
|
|
|
+ <span class="pipe-enhancement__device-status-label">设备状态</span>
|
|
|
+ </div>
|
|
|
+ <p v-if="deviceState.loadingDetail && !deviceState.aggregate?.data" class="pipe-enhancement__hint">设备详情加载中</p>
|
|
|
+ <template v-else-if="!deviceState.aggregate?.data">
|
|
|
+ <p class="pipe-enhancement__hint">设备详情暂不可用</p>
|
|
|
+ <button type="button" class="pipe-enhancement__action" @click="$emit('device-retry')">重新加载</button>
|
|
|
+ </template>
|
|
|
+ <EquipmentDetailSections v-else :aggregate="deviceState.aggregate" :device-code="deviceState.selected.code" monitor-source="enhancement" />
|
|
|
+ </template>
|
|
|
+ <template v-else-if="assetKind === 'facility'">
|
|
|
+ <p v-if="assetFacility?.loading" class="pipe-enhancement__hint" data-asset-facility-loading>设施详情加载中…</p>
|
|
|
+ <p v-else-if="assetFacility?.error" class="pipe-enhancement__hint is-error" data-asset-facility-error>
|
|
|
+ 设施详情未取到,已保留现有信息。
|
|
|
+ <button type="button" class="pipe-enhancement__action" @click="$emit('facility-retry')">重试</button>
|
|
|
+ </p>
|
|
|
+ <EntityDetailSections v-if="assetFacility?.detail" :detail="assetFacility.detail" presentation="device" />
|
|
|
+ </template>
|
|
|
+ </AssetDetailPanel>
|
|
|
+ </div>
|
|
|
</aside>
|
|
|
</div>
|
|
|
<dl class="pipe-enhancement__facts">
|
|
|
@@ -87,13 +111,47 @@
|
|
|
import { computed, nextTick, onBeforeUnmount, watch, ref } from 'vue'
|
|
|
import { mountPipeEnhancement } from '../services/threePipeEnhancement.js'
|
|
|
import EquipmentDetailSections from './EquipmentDetailSections.vue'
|
|
|
+import EntityDetailSections from './EntityDetailSections.vue'
|
|
|
import DeviceStateTags from './DeviceStateTags.vue'
|
|
|
+import AssetDetailPanel from './AssetDetailPanel.vue'
|
|
|
+import { ASSET_KIND } from '../domain/assetDetail.js'
|
|
|
|
|
|
const props = defineProps({
|
|
|
plan: { type: Object, required: true },
|
|
|
deviceState: { type: Object, default: () => ({ selected: null, aggregate: null, loadingDetail: false }) },
|
|
|
+ /** 设施详情状态:{ detail, loading, error };由外壳把选中设施接进来,与设备详情并列可选。 */
|
|
|
+ facilityState: { type: Object, default: () => ({ detail: null, loading: false, error: false }) },
|
|
|
+})
|
|
|
+const emit = defineEmits(['close', 'radius-change', 'device-pick', 'device-close', 'device-retry', 'devices-reload', 'object-pick', 'facility-retry'])
|
|
|
+
|
|
|
+/**
|
|
|
+ * 右侧两个页签:管道临近资产(原双列列表)|资产详情(设备或设施)。
|
|
|
+ * 选中的资产一变就自动切到详情页,避免用户还要手动点一下才知道有详情。
|
|
|
+ */
|
|
|
+const SIDE_TABS = [
|
|
|
+ { key: 'nearby', label: '管道临近资产' },
|
|
|
+ { key: 'detail', label: '资产详情' },
|
|
|
+]
|
|
|
+const sideTabs = SIDE_TABS
|
|
|
+const sideTab = ref('nearby')
|
|
|
+const assetFacility = computed(() => props.facilityState || { detail: null, loading: false, error: false })
|
|
|
+const assetContext = computed(() => ({
|
|
|
+ device: props.deviceState?.selected || null,
|
|
|
+ facility: assetFacility.value?.detail ? {
|
|
|
+ id: assetFacility.value.detail.id || '',
|
|
|
+ category: assetFacility.value.detail.kind === 'facility' ? (assetFacility.value.detail.category || '') : '',
|
|
|
+ title: assetFacility.value.detail.name || assetFacility.value.detail.code || '',
|
|
|
+ subtitle: assetFacility.value.detail.industry || '',
|
|
|
+ } : null,
|
|
|
+}))
|
|
|
+const assetKind = computed(() => {
|
|
|
+ // 设备优先级高于设施:两者同时存在时以最近一次选择的设备为准(device 选择会清掉设施详情)。
|
|
|
+ if (props.deviceState?.selected?.code) return ASSET_KIND.DEVICE
|
|
|
+ if (assetFacility.value?.detail) return ASSET_KIND.FACILITY
|
|
|
+ return ''
|
|
|
})
|
|
|
-const emit = defineEmits(['close', 'radius-change', 'device-pick', 'device-close', 'device-retry', 'devices-reload', 'object-pick'])
|
|
|
+// 有资产被选中就切到详情页;全部取消则退回列表页,避免停在空白详情。
|
|
|
+watch(assetKind, (kind) => { sideTab.value = kind ? 'detail' : 'nearby' }, { immediate: true })
|
|
|
const viewport = ref(null)
|
|
|
const localPipeStates = computed(() => [...new Map((props.plan.objects || [])
|
|
|
.filter((object) => object.sourceKind === 'pipe').map((object) => [object.code, object])).values()])
|
|
|
@@ -208,33 +266,29 @@ onBeforeUnmount(() => {
|
|
|
.pipe-enhancement__close:hover, .pipe-enhancement__close:focus-visible { border-color: var(--cyan, #01abea); color: #fff; outline: none; }
|
|
|
/* 右侧块(附近管道状态 + 监测设备)从 32% 放宽到 42%:两块改成左右并排后,
|
|
|
每块仍需要能读下「源管径 / 源材质 / 地理路径」那几行小字,否则并排只是把两列都挤扁。 */
|
|
|
-.pipe-enhancement__workspace { position: relative; display: grid; grid-template-columns: minmax(0, 1fr) minmax(calc(270 * var(--u, 1px)), 42%); gap: 12px; min-height: 0; }
|
|
|
+.pipe-enhancement__workspace { position: relative; display: grid; grid-template-columns: minmax(0, 1fr) minmax(calc(300 * var(--u, 1px)), 46%); gap: 12px; min-height: 0; }
|
|
|
/* 票 90 第二条(维护者 2026-10-27):右侧的「附近管道状态」与「监测设备」改为**左右并排**,
|
|
|
不再上下堆叠 —— 两块本就各自独立滚动,竖排等于把同一块纵向空间切成两半,谁都读不全。
|
|
|
分隔线跟着从「上边框」改成两列之间的竖线。 */
|
|
|
-.pipe-enhancement__devices { position: relative; display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 10px; min-height: 0; overflow: hidden; border-left: 1px solid #174969; padding-left: 12px; font-size: 12px; }
|
|
|
+.pipe-enhancement__side { display: flex; flex-direction: column; min-height: 0; overflow: hidden; border-left: 1px solid #174969; padding-left: 12px; font-size: 12px; }
|
|
|
+.pipe-enhancement__side-tabs { display: flex; flex: none; gap: 6px; margin-bottom: 8px; }
|
|
|
+.pipe-enhancement__side-tabs button { flex: 1; padding: 6px 8px; border: 1px solid #226286; border-radius: calc(8 * var(--u, 1px)); background: #072039; color: #a7c4d9; font: inherit; cursor: pointer; }
|
|
|
+.pipe-enhancement__side-tabs button.is-active { border-color: var(--cyan, #01abea); background: linear-gradient(180deg, rgba(1, 171, 234, .85), rgba(0, 82, 168, .85)); color: #fff; }
|
|
|
+.pipe-enhancement__nearby, .pipe-enhancement__asset-pane { flex: 1; min-height: 0; display: flex; flex-direction: column; overflow: hidden; }
|
|
|
+.pipe-enhancement__asset-pane { overflow: auto; }
|
|
|
+.pipe-enhancement__nearby { overflow: hidden; }
|
|
|
+.pipe-enhancement__devices { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 10px; min-height: 0; overflow: hidden; }
|
|
|
.pipe-enhancement__pipe-list { display: flex; flex-direction: column; min-height: 0; overflow: hidden; border-right: 1px solid #174969; padding-right: 10px; }
|
|
|
.pipe-enhancement__device-list { display: flex; flex-direction: column; min-height: 0; overflow: hidden; }
|
|
|
.pipe-enhancement__pipe-scroll, .pipe-enhancement__device-scroll { min-height: 0; overflow: auto; overscroll-behavior: contain; }
|
|
|
-/* 设备详情浮层:浮在三维场景之上(维护者 2026-10-22),不占用右侧列表空间。 */
|
|
|
-.pipe-enhancement__device-detail {
|
|
|
- position: absolute;
|
|
|
- z-index: 6;
|
|
|
- left: 12px;
|
|
|
- top: 12px;
|
|
|
- /* 管线详情用紧凑宽度,不再套主地图详情的460px下限。 */
|
|
|
- width: min(calc(340 * var(--u, 1px)), 380px, calc(100% - 24px));
|
|
|
- max-height: calc(100% - 24px);
|
|
|
- overflow: auto;
|
|
|
- box-sizing: border-box;
|
|
|
- padding: 12px;
|
|
|
- border: 1px solid #36b8d4;
|
|
|
- border-radius: 6px;
|
|
|
- background: rgba(2, 12, 32, .97);
|
|
|
- box-shadow: 0 10px 30px rgba(0, 0, 0, .7);
|
|
|
- font-size: 12px;
|
|
|
-}
|
|
|
-.pipe-enhancement__device-detail header { display: flex; justify-content: space-between; gap: 8px; align-items: center; margin-bottom: 8px; }
|
|
|
+/* 卡片:只留卡片信息(标题 + 编码/状态 + 管径/材质),不再铺长串描述。 */
|
|
|
+.pipe-enhancement__asset-card { display: block; width: 100%; margin-bottom: 6px; padding: 7px 8px; text-align: left; border: 1px solid #21618a; border-radius: calc(8 * var(--u, 1px)); background: #08213a; color: #e4f4ff; font: inherit; cursor: pointer; }
|
|
|
+.pipe-enhancement__asset-card b { display: block; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
|
+.pipe-enhancement__asset-card small { display: block; margin-top: 3px; color: #a7c4d9; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
|
+.pipe-enhancement__asset-card[aria-pressed="true"] { border-color: #d8fbff; box-shadow: inset 0 0 4px #38bac9; }
|
|
|
+.pipe-enhancement__hint { margin: 0 0 8px; color: #a7c4d9; line-height: 1.5; }
|
|
|
+.pipe-enhancement__hint.is-error { color: var(--lv1, #ff3b52); }
|
|
|
+.pipe-enhancement__action { align-self: flex-start; padding: 5px 10px; border: 1px solid #21618a; border-radius: calc(8 * var(--u, 1px)); background: #08213a; color: #e4f4ff; cursor: pointer; }
|
|
|
.pipe-enhancement__device-status-card {
|
|
|
display: flex;
|
|
|
flex-wrap: wrap;
|
|
|
@@ -247,29 +301,9 @@ onBeforeUnmount(() => {
|
|
|
background: linear-gradient(135deg, rgba(4, 42, 76, .72), rgba(5, 23, 47, .88));
|
|
|
}
|
|
|
.pipe-enhancement__device-status-label { color: #7ba4c3; }
|
|
|
-.pipe-enhancement__device-detail button { color: #e4f4ff; background: #08213a; border: 1px solid #21618a; padding: 5px 8px; cursor: pointer;
|
|
|
- border-radius: calc(8 * var(--u, 1px));}
|
|
|
-.pipe-enhancement__device-detail :deep(.equipment-sections) { max-height: 60vh; overflow: auto; }
|
|
|
-/* 紧凑详情下缩短时间/播放列,保留横向读数空间;字段名仍禁止竖排。 */
|
|
|
-.pipe-enhancement__device-detail :deep(.collection-table__time) { width: calc(76 * var(--u, 1px)); }
|
|
|
-.pipe-enhancement__device-detail :deep(.collection-table__audio-col) { width: calc(52 * var(--u, 1px)); }
|
|
|
-.pipe-enhancement__device-detail :deep(.collection-table__value) { padding-left: calc(2 * var(--u, 1px)); }
|
|
|
-.pipe-enhancement__device-detail :deep(.collection-table__reading) { column-gap: calc(8 * var(--u, 1px)); }
|
|
|
-.pipe-enhancement__device-detail :deep(.collection-table__heading) { font-size: calc(9 * var(--u, 1px)); }
|
|
|
.pipe-enhancement__pager { display: flex; align-items: center; justify-content: space-between; gap: 6px; margin-top: 6px; font-size: 11px; color: #a7c4d9; }
|
|
|
.pipe-enhancement__pager button { padding: 3px 8px; }
|
|
|
.pipe-enhancement__pager button:disabled { opacity: .4; cursor: default; }
|
|
|
-.pipe-enhancement__pipe { padding: 3px 5px; border: 1px solid #226286; background: #072039; color: #d9f7ff; cursor: pointer; text-align: left;
|
|
|
- border-radius: calc(8 * var(--u, 1px));}
|
|
|
-.pipe-enhancement__pipe[aria-pressed="true"] { border-color: #d8fbff; box-shadow: inset 0 0 4px #38bac9; }
|
|
|
-.pipe-enhancement__dimensions { display: block; margin-top: 4px; color: #a7c4d9; }
|
|
|
-.pipe-enhancement__devices h3 { margin: 0 0 8px; }
|
|
|
-.pipe-enhancement__devices button { color: #e4f4ff; background: #08213a; border: 1px solid #21618a; padding: 7px; cursor: pointer;
|
|
|
- border-radius: calc(8 * var(--u, 1px));}
|
|
|
-.pipe-enhancement__device { display: block; width: 100%; margin-bottom: 6px; text-align: left; }
|
|
|
-.pipe-enhancement__device small { display: block; color: #a7c4d9; margin-top: 4px; }
|
|
|
-.pipe-enhancement__devices section { margin-top: 12px; }
|
|
|
-.pipe-enhancement__devices header { display: flex; justify-content: space-between; gap: 8px; align-items: center; }
|
|
|
.pipe-enhancement__scope { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; color: #a7c4d9; font-size: 12px; }
|
|
|
.pipe-enhancement__scope select { margin-left: 8px; padding: 5px 10px; border: 1px solid #168bff; border-radius: 4px; background: #031226; color: #e4f4ff; }
|
|
|
.pipe-enhancement__viewport { min-height: 0; overflow: hidden; border: 1px solid rgba(1, 171, 234, .24); background: #031226; outline: none; touch-action: none;
|
|
|
@@ -282,3 +316,4 @@ onBeforeUnmount(() => {
|
|
|
.pipe-enhancement__facts dd { margin: 2px 0 0; overflow-wrap: anywhere; }
|
|
|
.pipe-enhancement__note { margin: 0; color: #a7c4d9; font-size: 12px; line-height: 1.45; }
|
|
|
</style>
|
|
|
+
|