EntityDetailSections.vue 8.8 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970
  1. <template>
  2. <div class="entity-sections" :class="{ 'is-device': presentation === 'device' }" data-entity-detail-sections>
  3. <div v-if="view.summary.length && presentation !== 'device'" class="entity-sections__summary">
  4. <div v-for="item in view.summary" :key="item.label">
  5. <span>{{ item.label }}</span>
  6. <!-- 等级作为带色的指标卡(维护者 2026-10-29:不要重复的第二个等级字段)。 -->
  7. <b v-if="item.tone" :data-tone="item.tone" class="entity-sections__tag">{{ item.value }}</b>
  8. <b v-else>{{ item.value }}</b>
  9. </div>
  10. </div>
  11. <ol v-if="view.stage.length" class="entity-sections__stage">
  12. <li v-for="step in view.stage" :key="step.label" :class="'is-' + step.state"><i></i><span>{{ step.label }}</span></li>
  13. </ol>
  14. <nav v-if="tabs.length" class="entity-sections__tabs" aria-label="详情分区">
  15. <button v-for="tab in tabs" :key="tab.key" type="button" role="tab" :aria-selected="tab.key === activeTab" :class="{ 'is-active': tab.key === activeTab }" @click="activeTab = tab.key">{{ tab.label }}</button>
  16. </nav>
  17. <section v-if="showOverview" v-for="group in activeSections" :key="group.title" class="entity-sections__group">
  18. <h3>{{ group.title }}</h3><dl class="entity-sections__grid"><div v-for="item in group.fields" :key="item.label" :class="{ 'is-wide': item.wide }"><dt>{{ item.label }}</dt><dd>{{ item.value }}<small v-if="item.note" class="entity-sections__note">{{ item.note }}</small></dd></div></dl>
  19. </section>
  20. <section v-if="showPeople && view.people.length" class="entity-sections__group"><h3>相关人员</h3><ul><li v-for="item in view.people" :key="item.role"><span>{{ item.role }}</span><b>{{ item.name }}</b></li></ul></section>
  21. <section v-if="showDevices && view.devices.length" class="entity-sections__group"><h3>设备信息</h3><ul><li v-for="item in view.devices" :key="item.code || item.name"><b>{{ item.name }}</b><span>{{ item.code }} · {{ item.status }}</span><small>{{ item.type || item.location }}</small><button v-if="item.code" type="button" class="entity-sections__locate" :data-locate-device="item.code" @click.stop="locateDevice(item.code)">定位 {{ item.code }}</button></li></ul></section>
  22. <section v-if="showRecords && view.records.length" class="entity-sections__group"><h3>处置记录</h3><ol class="entity-sections__records"><li v-for="(item,index) in view.records" :key="index"><b>{{ item.title }}</b><time>{{ item.time }}</time><span>{{ item.operator }}</span><p>{{ item.description }}</p></li></ol></section>
  23. <section v-if="showAttachments" class="entity-sections__group"><h3>附件</h3><p v-if="!view.attachments.length">暂无附件</p><ul v-else><li v-for="(item,index) in view.attachments" :key="index"><a v-if="item.url" :href="item.url" target="_blank" rel="noopener">{{ item.name }}</a><span v-else>{{ item.name }}</span></li></ul></section>
  24. <section v-if="showRelated" class="entity-sections__group"><h3>关联信息</h3><p v-if="!view.devices.length">暂无关联设备数据</p><ul v-else><li v-for="item in view.devices" :key="item.code || item.name"><b>{{ item.name }}</b><span>{{ item.code }} · {{ item.status }}</span><small>{{ item.type || item.location }}</small><button v-if="item.code" type="button" class="entity-sections__locate" :data-locate-device="item.code" @click.stop="locateDevice(item.code)">定位 {{ item.code }}</button></li></ul></section>
  25. <section v-if="showAlarms" class="entity-sections__group"><h3>关联报警</h3><p v-if="!view.relatedAlarms.length">暂无关联报警</p><ul v-else><li v-for="item in view.relatedAlarms" :key="item.code"><b>{{ item.name }}</b><span>{{ item.code }} · {{ item.status }}</span></li></ul></section>
  26. <section v-if="showFlow && view.track" class="entity-sections__group"><h3>处置轨迹</h3><WorkOrderTimeline :track="view.track" /></section>
  27. </div>
  28. </template>
  29. <script setup>
  30. import { computed, ref, watch } from 'vue'
  31. import { entityDetailView } from '../domain/entityDetail.js'
  32. import WorkOrderTimeline from './WorkOrderTimeline.vue'
  33. const props = defineProps({ detail: { type: Object, default: null }, presentation: { type: String, default: 'entity' } })
  34. const emit = defineEmits(['locate-device'])
  35. const view = computed(() => entityDetailView(props.detail))
  36. const activeTab = ref('')
  37. const tabs = computed(() => view.value.tabs || [])
  38. watch(() => props.detail && (props.detail.kind + '|' + props.detail.id), () => { activeTab.value = (tabs.value[0] && tabs.value[0].key) || '' }, { immediate: true })
  39. const showOverview = computed(() => !activeTab.value || ['overview','business'].includes(activeTab.value))
  40. const activeSections = computed(() => {
  41. if (activeTab.value === 'business') return view.value.sections.filter((group) => group.title === '业务详情')
  42. if (activeTab.value === 'overview') return view.value.sections.filter((group) => group.title !== '业务详情')
  43. return view.value.sections
  44. })
  45. const showPeople = computed(() => showOverview.value)
  46. // 关联设备与定位在**概览页也可见**(维护者 2026-10-29:定位功能不能被藏进二级页签)。
  47. const showDevices = computed(() => !activeTab.value || activeTab.value === 'overview' || activeTab.value === 'devices')
  48. const showRecords = computed(() => activeTab.value === 'records')
  49. const showAttachments = computed(() => activeTab.value === 'attachments')
  50. const showFlow = computed(() => activeTab.value === 'flow')
  51. const showRelated = computed(() => activeTab.value === 'related')
  52. const showAlarms = computed(() => activeTab.value === 'alarms')
  53. /** 关联设备定位:沿用运行监测「设备台账」的定位方法(由外壳转给 monitoringStore)。 */
  54. function locateDevice(code) {
  55. if (!code) return
  56. emit('locate-device', code)
  57. }
  58. </script>
  59. <style scoped>
  60. /* 等级/状态徽标:与列表卡片同一套语义色(维护者 2026-10-29:等级那一栏要有颜色)。 */
  61. .entity-sections__locate{margin-top:4px;padding:0 8px;border:1px solid rgba(1,171,234,.45);border-radius:9px;background:rgba(0,20,72,.5);color:var(--cyan-hi,#35d8ff);font:inherit;font-size:10px;cursor:pointer}
  62. .entity-sections__locate:hover,.entity-sections__locate:focus-visible{border-color:var(--cyan-hi,#00b8ff);color:var(--text-strong)}
  63. .entity-sections__tag{display:inline-block;padding:0 7px;border:1px solid rgba(1,171,234,.35);border-radius:9px;font-weight:700}
  64. .entity-sections__tag[data-tone='lv1']{border-color:rgba(255,59,82,.65);background:rgba(255,59,82,.14);color:#ffc2c9}
  65. .entity-sections__tag[data-tone='lv2']{border-color:rgba(255,176,32,.65);background:rgba(255,176,32,.14);color:#ffdc91}
  66. .entity-sections__tag[data-tone='lv3']{border-color:rgba(1,171,234,.65);background:rgba(1,171,234,.14);color:#b7e4ff}
  67. .entity-sections__tag[data-tone='lv4']{border-color:rgba(53,219,146,.6);background:rgba(53,219,146,.14);color:#b8f2d4}
  68. .entity-sections{display:grid;gap:8px}.entity-sections__summary{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:6px}.entity-sections__summary div,.entity-sections__group{padding:8px;border:1px solid rgba(1,171,234,.18);border-radius:6px;background:rgba(0,20,72,.35)}.entity-sections__summary span,dt,.entity-sections__group li span,.entity-sections__group li small,.entity-sections__group time{display:block;color:var(--muted);font-size:var(--fs-caption)}.entity-sections__stage{display:flex;margin:0;padding:4px 0 8px;list-style:none;overflow-x:auto}.entity-sections__stage li{position:relative;flex:1;min-width:54px;text-align:center;color:var(--muted);font-size:10px}.entity-sections__stage li:before{content:'';position:absolute;top:6px;left:0;width:100%;height:1px;background:rgba(86,128,176,.5)}.entity-sections__stage i{position:relative;z-index:1;display:block;width:13px;height:13px;margin:0 auto 4px;border:1px solid var(--muted);border-radius:50%;background:#061432}.entity-sections__stage .is-done i,.entity-sections__stage .is-active i{border-color:var(--cyan-hi);background:var(--cyan)}.entity-sections__tabs{display:flex;gap:4px;overflow-x:auto;border-bottom:1px solid rgba(1,171,234,.22)}.entity-sections__tabs button{padding:5px 8px;border:0;border-bottom:2px solid transparent;background:none;color:var(--text-2);white-space:nowrap;cursor:pointer}.entity-sections__tabs .is-active{border-bottom-color:var(--cyan-hi);color:var(--text-strong)}.entity-sections__group h3{margin:0 0 6px;color:var(--cyan-hi);font-size:var(--fs-caption)}.entity-sections__grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:5px 10px;margin:0}.entity-sections__grid .is-wide{grid-column:1/-1}.entity-sections dd{margin:1px 0 0;color:var(--text);overflow-wrap:anywhere}.entity-sections__note{display:block;margin-top:1px;color:var(--muted);font-size:10px}.entity-sections__group ul,.entity-sections__records{display:grid;gap:6px;margin:0;padding:0;list-style:none}
  69. </style>