FaultChartModal.vue 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. <script setup lang="ts">
  2. import { theme } from 'ant-design-vue'
  3. import zhCN from 'ant-design-vue/es/locale/zh_CN'
  4. import dayjs, { type Dayjs } from 'dayjs'
  5. import { computed, ref, watch } from 'vue'
  6. import { fetchPksSeries } from '../api/series'
  7. import { useAlarmsStore } from '../stores/alarms'
  8. import { buildChartSeries } from '../utils/chart'
  9. import { shiftHours, toMs } from '../utils/time'
  10. import FaultChart from './FaultChart.vue'
  11. const props = defineProps<{ open: boolean }>()
  12. const emit = defineEmits<{ (event: 'update:open', value: boolean): void }>()
  13. const alarms = useAlarmsStore()
  14. const mode = ref<'relative' | 'absolute'>('relative')
  15. const activeTab = ref<'info' | 'chart'>('info')
  16. const rangeStart = ref<Dayjs | null>(null)
  17. const rangeEnd = ref<Dayjs | null>(null)
  18. const ms = ref<number[]>([])
  19. const oil = ref<(number | null)[]>([])
  20. const coupling = ref<(number | null)[]>([])
  21. const chain = ref<(number | null)[]>([])
  22. const faultStart = ref(0)
  23. const faultEnd = ref(0)
  24. const loading = ref(false)
  25. const error = ref('')
  26. const loaded = ref(false)
  27. const HOUR = 3600 * 1000
  28. const isVibration = computed(() => alarms.selected?.alarm_type === '振动故障')
  29. const info = computed<Record<string, unknown>>(() => alarms.selected?.alarm_info ?? {})
  30. const HOUR_KEYS = new Set([
  31. '持续自然小时数',
  32. '有效运行小时数',
  33. '阶段低报警小时数',
  34. '阶段低低报警小时数',
  35. '阶段高报警小时数',
  36. '阶段高高报警小时数',
  37. ])
  38. const LABELS: Record<string, string> = {
  39. 持续自然小时数: '持续',
  40. 有效运行小时数: '有效',
  41. 周期基线油压: '周期基线',
  42. 近期基线振动: '近期基线',
  43. 正常波动带: '波动带',
  44. 阶段开始小时油压: '开始油压',
  45. 阶段结束小时油压: '结束油压',
  46. 阶段最低小时油压: '最低油压',
  47. 阶段开始小时振动: '开始振动',
  48. 阶段结束小时振动: '结束振动',
  49. 阶段最大振动值: '最大振动',
  50. 阶段开始相对周期基线变化: '开始相对基线',
  51. 阶段结束相对周期基线变化: '结束相对基线',
  52. 阶段开始相对近期基线变化: '开始相对基线',
  53. 阶段结束相对近期基线变化: '结束相对基线',
  54. 阶段最大连续下降有效小时数: '最大连续下降',
  55. 阶段最大连续异常有效小时数: '最大连续异常',
  56. 阶段低报警小时数: '低报警',
  57. 阶段低低报警小时数: '低低报警',
  58. 阶段高报警小时数: '高报警',
  59. 阶段高高报警小时数: '高高报警',
  60. 是否形成确认等级: '确认等级',
  61. 阶段触发依据: '触发依据',
  62. 阶段综合评分: '综合评分',
  63. 持续时间得分: '持续得分',
  64. 平均振动得分: '平均得分',
  65. 最大振动值得分: '最大得分',
  66. 峰值偏离得分: '峰值偏离',
  67. 最强异常侧: '最强侧',
  68. 最强侧近期峰值基线: '最强侧基线',
  69. 最强侧近期稳健尺度: '稳健尺度',
  70. '阶段最大峰值/基线比例': '峰值/基线',
  71. 模型异常概率均值: '概率均值',
  72. 模型异常概率最大值: '概率最大',
  73. 模型异常概率最小值: '概率最小',
  74. 模型预测参考: '模型预测',
  75. }
  76. const infoItems = computed<{ key: string; label: string; value: string; span: number }[]>(() =>
  77. Object.entries(info.value).map(([key, value]) => {
  78. const empty = value === null || value === undefined || value === ''
  79. let text = empty ? '—' : String(value)
  80. if (!empty && HOUR_KEYS.has(key)) text = `${text} 小时`
  81. if (!empty && key === '模型预测参考') text = text.replace(/^模型预测参考:?/, '')
  82. return {
  83. key,
  84. label: LABELS[key] ?? key,
  85. value: text,
  86. span: key === '阶段触发依据' || key === '模型预测参考' ? 3 : 1,
  87. }
  88. }),
  89. )
  90. const built = computed(() => {
  91. if (!loaded.value || !alarms.selected) return null
  92. return buildChartSeries({
  93. ms: ms.value,
  94. oil: oil.value,
  95. coupling: coupling.value,
  96. chain: chain.value,
  97. isVibration: isVibration.value,
  98. mode: mode.value,
  99. baselineOil: Number(info.value['周期基线油压']),
  100. baselineVib: Number(info.value['近期基线振动']),
  101. })
  102. })
  103. async function query(): Promise<void> {
  104. const alarm = alarms.selected
  105. if (!alarm || !rangeStart.value || !rangeEnd.value) return
  106. loading.value = true
  107. error.value = ''
  108. try {
  109. const start = rangeStart.value.format('YYYY-MM-DD HH:mm:ss')
  110. const end = rangeEnd.value.format('YYYY-MM-DD HH:mm:ss')
  111. const data = await fetchPksSeries(alarm.device_code, start, end)
  112. ms.value = data.times.map(toMs)
  113. oil.value = data.oil
  114. coupling.value = data.coupling
  115. chain.value = data.chain
  116. faultStart.value = toMs(alarm.alarm_time_start)
  117. faultEnd.value = toMs(alarm.alarm_time_end) + HOUR
  118. loaded.value = true
  119. } catch (err) {
  120. error.value = '曲线数据加载失败:' + (err as Error).message
  121. loaded.value = false
  122. } finally {
  123. loading.value = false
  124. }
  125. }
  126. function init(): void {
  127. const alarm = alarms.selected
  128. if (!alarm) return
  129. rangeStart.value = dayjs(shiftHours(alarm.alarm_time_start, -2))
  130. rangeEnd.value = dayjs(shiftHours(alarm.alarm_time_end, 3))
  131. mode.value = 'relative'
  132. query()
  133. }
  134. watch(() => props.open, (value) => { if (value) init() }, { immediate: true })
  135. watch(() => alarms.selected?.id, () => { if (props.open) init() })
  136. function close(): void {
  137. emit('update:open', false)
  138. }
  139. </script>
  140. <template>
  141. <a-modal
  142. :open="open"
  143. wrap-class-name="fault-detail-modal"
  144. :footer="null"
  145. width="900px"
  146. title="故障详细内容"
  147. @cancel="close"
  148. >
  149. <a-config-provider :locale="zhCN" :theme="{ algorithm: theme.defaultAlgorithm }">
  150. <a-tabs v-model:activeKey="activeTab" size="small">
  151. <a-tab-pane key="info" tab="故障信息">
  152. <div class="info-grid">
  153. <template v-for="item in infoItems" :key="item.key">
  154. <div class="info-label" :class="{ full: item.span === 3 }">{{ item.label }}</div>
  155. <div class="info-value" :class="{ full: item.span === 3 }">{{ item.value }}</div>
  156. </template>
  157. </div>
  158. </a-tab-pane>
  159. <a-tab-pane key="chart" tab="曲线图">
  160. <div class="chart-toolbar">
  161. <a-radio-group v-model:value="mode" size="small" button-style="solid">
  162. <a-radio-button value="relative">相对基线</a-radio-button>
  163. <a-radio-button value="absolute">原值</a-radio-button>
  164. </a-radio-group>
  165. <span class="tool-label">开始</span>
  166. <a-date-picker v-model:value="rangeStart" show-time format="YYYY-MM-DD HH:mm:ss" size="small" />
  167. <span class="tool-label">结束</span>
  168. <a-date-picker v-model:value="rangeEnd" show-time format="YYYY-MM-DD HH:mm:ss" size="small" />
  169. <a-button type="primary" size="small" :loading="loading" @click="query">查询</a-button>
  170. </div>
  171. <div v-if="loading" class="modal-loading">
  172. <a-spin size="small" />
  173. <span class="modal-loading-text">加载中</span>
  174. </div>
  175. <div v-else-if="error" class="modal-error">{{ error }}</div>
  176. <FaultChart
  177. v-else-if="built"
  178. :series="built.series"
  179. :fault-start="faultStart"
  180. :fault-end="faultEnd"
  181. :height="isVibration ? 420 : 320"
  182. :y-min="built.yMin"
  183. :y-max="built.yMax"
  184. :unit="built.unit"
  185. :ref-lines="built.refLines"
  186. :show-y-axis="true"
  187. :dark="false"
  188. :zoom="true"
  189. />
  190. </a-tab-pane>
  191. </a-tabs>
  192. </a-config-provider>
  193. </a-modal>
  194. </template>
  195. <style scoped>
  196. .info-grid {
  197. display: grid;
  198. grid-template-columns: 120px 1fr 96px 1fr 96px 1fr;
  199. border-top: 1px solid #f0f0f0;
  200. border-left: 1px solid #f0f0f0;
  201. font-size: 12px;
  202. }
  203. .info-label,
  204. .info-value {
  205. padding: 6px 8px;
  206. border-right: 1px solid #f0f0f0;
  207. border-bottom: 1px solid #f0f0f0;
  208. min-width: 0;
  209. word-break: break-all;
  210. }
  211. .info-label {
  212. color: #666666;
  213. background: #fafafa;
  214. }
  215. .info-value {
  216. color: #1a1a1a;
  217. }
  218. .info-label.full {
  219. grid-column: 1;
  220. }
  221. .info-value.full {
  222. grid-column: 2 / -1;
  223. }
  224. .chart-toolbar {
  225. display: flex;
  226. align-items: center;
  227. gap: 8px;
  228. flex-wrap: wrap;
  229. margin-bottom: 8px;
  230. }
  231. .tool-label {
  232. font-size: 12px;
  233. color: #666666;
  234. }
  235. .modal-loading {
  236. display: flex;
  237. align-items: center;
  238. justify-content: center;
  239. gap: 6px;
  240. padding: 60px 4px;
  241. }
  242. .modal-loading-text {
  243. color: #666666;
  244. font-size: 12px;
  245. }
  246. .modal-loading :deep(.ant-spin-dot) {
  247. font-size: 12px;
  248. }
  249. .modal-error {
  250. color: #ff4d4f;
  251. font-size: 12px;
  252. padding: 40px 0;
  253. }
  254. :deep(.ant-tabs-nav) {
  255. margin-bottom: 12px;
  256. }
  257. :deep(.ant-tabs-tab) {
  258. font-size: 13px;
  259. }
  260. :deep(.ant-picker),
  261. :deep(.ant-radio-button-wrapper),
  262. :deep(.ant-btn) {
  263. font-size: 12px;
  264. }
  265. </style>