|
|
@@ -1,29 +1,107 @@
|
|
|
<script setup lang="ts">
|
|
|
import { theme } from 'ant-design-vue'
|
|
|
-import { computed, ref } from 'vue'
|
|
|
+import zhCN from 'ant-design-vue/es/locale/zh_CN'
|
|
|
+import dayjs, { type Dayjs } from 'dayjs'
|
|
|
+import { computed, ref, watch } from 'vue'
|
|
|
|
|
|
+import { fetchPksSeries } from '../api/series'
|
|
|
import { useAlarmsStore } from '../stores/alarms'
|
|
|
-import { useChartsStore } from '../stores/charts'
|
|
|
import { buildChartSeries } from '../utils/chart'
|
|
|
+import { shiftHours, toMs } from '../utils/time'
|
|
|
import FaultChart from './FaultChart.vue'
|
|
|
|
|
|
-defineProps<{ open: boolean }>()
|
|
|
+const props = defineProps<{ open: boolean }>()
|
|
|
const emit = defineEmits<{ (event: 'update:open', value: boolean): void }>()
|
|
|
|
|
|
const alarms = useAlarmsStore()
|
|
|
-const charts = useChartsStore()
|
|
|
const mode = ref<'relative' | 'absolute'>('relative')
|
|
|
+const activeTab = ref<'info' | 'chart'>('info')
|
|
|
+
|
|
|
+const rangeStart = ref<Dayjs | null>(null)
|
|
|
+const rangeEnd = ref<Dayjs | null>(null)
|
|
|
+const ms = ref<number[]>([])
|
|
|
+const oil = ref<(number | null)[]>([])
|
|
|
+const coupling = ref<(number | null)[]>([])
|
|
|
+const chain = ref<(number | null)[]>([])
|
|
|
+const faultStart = ref(0)
|
|
|
+const faultEnd = ref(0)
|
|
|
+const loading = ref(false)
|
|
|
+const error = ref('')
|
|
|
+const loaded = ref(false)
|
|
|
+
|
|
|
+const HOUR = 3600 * 1000
|
|
|
|
|
|
const isVibration = computed(() => alarms.selected?.alarm_type === '振动故障')
|
|
|
const info = computed<Record<string, unknown>>(() => alarms.selected?.alarm_info ?? {})
|
|
|
|
|
|
+const HOUR_KEYS = new Set([
|
|
|
+ '持续自然小时数',
|
|
|
+ '有效运行小时数',
|
|
|
+ '阶段低报警小时数',
|
|
|
+ '阶段低低报警小时数',
|
|
|
+ '阶段高报警小时数',
|
|
|
+ '阶段高高报警小时数',
|
|
|
+])
|
|
|
+
|
|
|
+const LABELS: Record<string, string> = {
|
|
|
+ 持续自然小时数: '持续',
|
|
|
+ 有效运行小时数: '有效',
|
|
|
+ 周期基线油压: '周期基线',
|
|
|
+ 近期基线振动: '近期基线',
|
|
|
+ 正常波动带: '波动带',
|
|
|
+ 阶段开始小时油压: '开始油压',
|
|
|
+ 阶段结束小时油压: '结束油压',
|
|
|
+ 阶段最低小时油压: '最低油压',
|
|
|
+ 阶段开始小时振动: '开始振动',
|
|
|
+ 阶段结束小时振动: '结束振动',
|
|
|
+ 阶段最大振动值: '最大振动',
|
|
|
+ 阶段开始相对周期基线变化: '开始相对基线',
|
|
|
+ 阶段结束相对周期基线变化: '结束相对基线',
|
|
|
+ 阶段开始相对近期基线变化: '开始相对基线',
|
|
|
+ 阶段结束相对近期基线变化: '结束相对基线',
|
|
|
+ 阶段最大连续下降有效小时数: '最大连续下降',
|
|
|
+ 阶段最大连续异常有效小时数: '最大连续异常',
|
|
|
+ 阶段低报警小时数: '低报警',
|
|
|
+ 阶段低低报警小时数: '低低报警',
|
|
|
+ 阶段高报警小时数: '高报警',
|
|
|
+ 阶段高高报警小时数: '高高报警',
|
|
|
+ 是否形成确认等级: '确认等级',
|
|
|
+ 阶段触发依据: '触发依据',
|
|
|
+ 阶段综合评分: '综合评分',
|
|
|
+ 持续时间得分: '持续得分',
|
|
|
+ 平均振动得分: '平均得分',
|
|
|
+ 最大振动值得分: '最大得分',
|
|
|
+ 峰值偏离得分: '峰值偏离',
|
|
|
+ 最强异常侧: '最强侧',
|
|
|
+ 最强侧近期峰值基线: '最强侧基线',
|
|
|
+ 最强侧近期稳健尺度: '稳健尺度',
|
|
|
+ '阶段最大峰值/基线比例': '峰值/基线',
|
|
|
+ 模型异常概率均值: '概率均值',
|
|
|
+ 模型异常概率最大值: '概率最大',
|
|
|
+ 模型异常概率最小值: '概率最小',
|
|
|
+}
|
|
|
+
|
|
|
+const infoItems = computed<{ key: string; label: string; value: string; span: number }[]>(() =>
|
|
|
+ Object.entries(info.value).map(([key, value]) => {
|
|
|
+ const empty = value === null || value === undefined || value === ''
|
|
|
+ let text = empty ? '—' : String(value)
|
|
|
+ if (!empty && HOUR_KEYS.has(key)) text = `${text} 小时`
|
|
|
+ return {
|
|
|
+ key,
|
|
|
+ label: LABELS[key] ?? key,
|
|
|
+ value: text,
|
|
|
+ span: key === '阶段触发依据' ? 3 : 1,
|
|
|
+ }
|
|
|
+ }),
|
|
|
+)
|
|
|
+
|
|
|
const built = computed(() => {
|
|
|
- if (!charts.loaded || !alarms.selected) return null
|
|
|
+ if (!loaded.value || !alarms.selected) return null
|
|
|
return buildChartSeries({
|
|
|
- ms: charts.ms,
|
|
|
- oil: charts.oil,
|
|
|
- coupling: charts.coupling,
|
|
|
- chain: charts.chain,
|
|
|
+ ms: ms.value,
|
|
|
+ oil: oil.value,
|
|
|
+ coupling: coupling.value,
|
|
|
+ chain: chain.value,
|
|
|
isVibration: isVibration.value,
|
|
|
mode: mode.value,
|
|
|
baselineOil: Number(info.value['周期基线油压']),
|
|
|
@@ -31,6 +109,42 @@ const built = computed(() => {
|
|
|
})
|
|
|
})
|
|
|
|
|
|
+async function query(): Promise<void> {
|
|
|
+ const alarm = alarms.selected
|
|
|
+ if (!alarm || !rangeStart.value || !rangeEnd.value) return
|
|
|
+ loading.value = true
|
|
|
+ error.value = ''
|
|
|
+ try {
|
|
|
+ const start = rangeStart.value.format('YYYY-MM-DD HH:mm:ss')
|
|
|
+ const end = rangeEnd.value.format('YYYY-MM-DD HH:mm:ss')
|
|
|
+ const data = await fetchPksSeries(alarm.device_code, start, end)
|
|
|
+ ms.value = data.times.map(toMs)
|
|
|
+ oil.value = data.oil
|
|
|
+ coupling.value = data.coupling
|
|
|
+ chain.value = data.chain
|
|
|
+ faultStart.value = toMs(alarm.alarm_time_start)
|
|
|
+ faultEnd.value = toMs(alarm.alarm_time_end) + HOUR
|
|
|
+ loaded.value = true
|
|
|
+ } catch (err) {
|
|
|
+ error.value = '曲线数据加载失败:' + (err as Error).message
|
|
|
+ loaded.value = false
|
|
|
+ } finally {
|
|
|
+ loading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function init(): void {
|
|
|
+ const alarm = alarms.selected
|
|
|
+ if (!alarm) return
|
|
|
+ rangeStart.value = dayjs(shiftHours(alarm.alarm_time_start, -2))
|
|
|
+ rangeEnd.value = dayjs(shiftHours(alarm.alarm_time_end, 3))
|
|
|
+ mode.value = 'relative'
|
|
|
+ query()
|
|
|
+}
|
|
|
+
|
|
|
+watch(() => props.open, (value) => { if (value) init() }, { immediate: true })
|
|
|
+watch(() => alarms.selected?.id, () => { if (props.open) init() })
|
|
|
+
|
|
|
function close(): void {
|
|
|
emit('update:open', false)
|
|
|
}
|
|
|
@@ -39,62 +153,129 @@ function close(): void {
|
|
|
<template>
|
|
|
<a-modal
|
|
|
:open="open"
|
|
|
- wrap-class-name="fault-chart-modal"
|
|
|
+ wrap-class-name="fault-detail-modal"
|
|
|
:footer="null"
|
|
|
width="900px"
|
|
|
- title="故障曲线"
|
|
|
+ title="故障详细内容"
|
|
|
@cancel="close"
|
|
|
>
|
|
|
- <a-config-provider :theme="{ algorithm: theme.defaultAlgorithm }">
|
|
|
- <div class="modal-head">
|
|
|
- <a-radio-group v-model:value="mode" size="small" button-style="solid">
|
|
|
- <a-radio-button value="relative">相对基线</a-radio-button>
|
|
|
- <a-radio-button value="absolute">原值</a-radio-button>
|
|
|
- </a-radio-group>
|
|
|
- </div>
|
|
|
- <div v-if="charts.loading" class="modal-loading">
|
|
|
- <div class="modal-loading-text">曲线数据加载中…</div>
|
|
|
- <a-progress :percent="charts.progress" size="small" stroke-color="#4da3ff" />
|
|
|
- </div>
|
|
|
- <div v-else-if="charts.error" class="modal-error">{{ charts.error }}</div>
|
|
|
- <FaultChart
|
|
|
- v-else-if="built"
|
|
|
- :series="built.series"
|
|
|
- :fault-start="charts.faultStart"
|
|
|
- :fault-end="charts.faultEnd"
|
|
|
- :height="isVibration ? 520 : 360"
|
|
|
- :y-min="built.yMin"
|
|
|
- :y-max="built.yMax"
|
|
|
- :unit="built.unit"
|
|
|
- :ref-lines="built.refLines"
|
|
|
- :show-y-axis="true"
|
|
|
- :dark="false"
|
|
|
- :zoom="true"
|
|
|
- />
|
|
|
+ <a-config-provider :locale="zhCN" :theme="{ algorithm: theme.defaultAlgorithm }">
|
|
|
+ <a-tabs v-model:activeKey="activeTab" size="small">
|
|
|
+ <a-tab-pane key="info" tab="故障信息">
|
|
|
+ <div class="info-grid">
|
|
|
+ <template v-for="item in infoItems" :key="item.key">
|
|
|
+ <div class="info-label" :class="{ full: item.span === 3 }">{{ item.label }}</div>
|
|
|
+ <div class="info-value" :class="{ full: item.span === 3 }">{{ item.value }}</div>
|
|
|
+ </template>
|
|
|
+ </div>
|
|
|
+ </a-tab-pane>
|
|
|
+
|
|
|
+ <a-tab-pane key="chart" tab="曲线图">
|
|
|
+ <div class="chart-toolbar">
|
|
|
+ <a-radio-group v-model:value="mode" size="small" button-style="solid">
|
|
|
+ <a-radio-button value="relative">相对基线</a-radio-button>
|
|
|
+ <a-radio-button value="absolute">原值</a-radio-button>
|
|
|
+ </a-radio-group>
|
|
|
+ <span class="tool-label">开始</span>
|
|
|
+ <a-date-picker v-model:value="rangeStart" show-time format="YYYY-MM-DD HH:mm:ss" size="small" />
|
|
|
+ <span class="tool-label">结束</span>
|
|
|
+ <a-date-picker v-model:value="rangeEnd" show-time format="YYYY-MM-DD HH:mm:ss" size="small" />
|
|
|
+ <a-button type="primary" size="small" :loading="loading" @click="query">查询</a-button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-if="loading" class="modal-loading">
|
|
|
+ <a-spin size="small" />
|
|
|
+ <span class="modal-loading-text">加载中</span>
|
|
|
+ </div>
|
|
|
+ <div v-else-if="error" class="modal-error">{{ error }}</div>
|
|
|
+ <FaultChart
|
|
|
+ v-else-if="built"
|
|
|
+ :series="built.series"
|
|
|
+ :fault-start="faultStart"
|
|
|
+ :fault-end="faultEnd"
|
|
|
+ :height="isVibration ? 420 : 320"
|
|
|
+ :y-min="built.yMin"
|
|
|
+ :y-max="built.yMax"
|
|
|
+ :unit="built.unit"
|
|
|
+ :ref-lines="built.refLines"
|
|
|
+ :show-y-axis="true"
|
|
|
+ :dark="false"
|
|
|
+ :zoom="true"
|
|
|
+ />
|
|
|
+ </a-tab-pane>
|
|
|
+ </a-tabs>
|
|
|
</a-config-provider>
|
|
|
</a-modal>
|
|
|
</template>
|
|
|
|
|
|
<style scoped>
|
|
|
-.modal-head {
|
|
|
+.info-grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: 120px 1fr 96px 1fr 96px 1fr;
|
|
|
+ border-top: 1px solid #f0f0f0;
|
|
|
+ border-left: 1px solid #f0f0f0;
|
|
|
+ font-size: 12px;
|
|
|
+}
|
|
|
+.info-label,
|
|
|
+.info-value {
|
|
|
+ padding: 6px 8px;
|
|
|
+ border-right: 1px solid #f0f0f0;
|
|
|
+ border-bottom: 1px solid #f0f0f0;
|
|
|
+ min-width: 0;
|
|
|
+ word-break: break-all;
|
|
|
+}
|
|
|
+.info-label {
|
|
|
+ color: #666666;
|
|
|
+ background: #fafafa;
|
|
|
+}
|
|
|
+.info-value {
|
|
|
+ color: #1a1a1a;
|
|
|
+}
|
|
|
+.info-label.full {
|
|
|
+ grid-column: 1;
|
|
|
+}
|
|
|
+.info-value.full {
|
|
|
+ grid-column: 2 / -1;
|
|
|
+}
|
|
|
+.chart-toolbar {
|
|
|
display: flex;
|
|
|
- justify-content: flex-end;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ flex-wrap: wrap;
|
|
|
margin-bottom: 8px;
|
|
|
}
|
|
|
-.modal-head :deep(.ant-radio-button-wrapper) {
|
|
|
+.tool-label {
|
|
|
font-size: 12px;
|
|
|
+ color: #666666;
|
|
|
}
|
|
|
.modal-loading {
|
|
|
- padding: 40px 4px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ gap: 6px;
|
|
|
+ padding: 60px 4px;
|
|
|
}
|
|
|
.modal-loading-text {
|
|
|
color: #666666;
|
|
|
font-size: 12px;
|
|
|
- margin-bottom: 8px;
|
|
|
+}
|
|
|
+.modal-loading :deep(.ant-spin-dot) {
|
|
|
+ font-size: 12px;
|
|
|
}
|
|
|
.modal-error {
|
|
|
color: #ff4d4f;
|
|
|
font-size: 12px;
|
|
|
- padding: 30px 0;
|
|
|
+ padding: 40px 0;
|
|
|
+}
|
|
|
+:deep(.ant-tabs-nav) {
|
|
|
+ margin-bottom: 12px;
|
|
|
+}
|
|
|
+:deep(.ant-tabs-tab) {
|
|
|
+ font-size: 13px;
|
|
|
+}
|
|
|
+:deep(.ant-picker),
|
|
|
+:deep(.ant-radio-button-wrapper),
|
|
|
+:deep(.ant-btn) {
|
|
|
+ font-size: 12px;
|
|
|
}
|
|
|
</style>
|