| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283 |
- <script setup lang="ts">
- import { theme } from 'ant-design-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 { buildChartSeries } from '../utils/chart'
- import { shiftHours, toMs } from '../utils/time'
- import FaultChart from './FaultChart.vue'
- const props = defineProps<{ open: boolean }>()
- const emit = defineEmits<{ (event: 'update:open', value: boolean): void }>()
- const alarms = useAlarmsStore()
- 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} 小时`
- if (!empty && key === '模型预测参考') text = text.replace(/^模型预测参考:?/, '')
- return {
- key,
- label: LABELS[key] ?? key,
- value: text,
- span: key === '阶段触发依据' || key === '模型预测参考' ? 3 : 1,
- }
- }),
- )
- const built = computed(() => {
- if (!loaded.value || !alarms.selected) return null
- return buildChartSeries({
- ms: ms.value,
- oil: oil.value,
- coupling: coupling.value,
- chain: chain.value,
- isVibration: isVibration.value,
- mode: mode.value,
- baselineOil: Number(info.value['周期基线油压']),
- baselineVib: Number(info.value['近期基线振动']),
- })
- })
- 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)
- }
- </script>
- <template>
- <a-modal
- :open="open"
- wrap-class-name="fault-detail-modal"
- :footer="null"
- width="900px"
- title="故障详细内容"
- @cancel="close"
- >
- <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>
- .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;
- align-items: center;
- gap: 8px;
- flex-wrap: wrap;
- margin-bottom: 8px;
- }
- .tool-label {
- font-size: 12px;
- color: #666666;
- }
- .modal-loading {
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 6px;
- padding: 60px 4px;
- }
- .modal-loading-text {
- color: #666666;
- font-size: 12px;
- }
- .modal-loading :deep(.ant-spin-dot) {
- font-size: 12px;
- }
- .modal-error {
- color: #ff4d4f;
- font-size: 12px;
- 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>
|