- 优化 `车站-设备-告警` 轮询机制 - 改进设备卡片的布局 - 支持修改设备 - 告警轮询中获取完整告警数据 - 车站告警详情支持导出完整的 `今日告警列表` - 支持将状态持久化到 `IndexedDB` - 新增轮询控制 (调试模式) - 新增离线开发模式 (调试模式) - 新增 `IndexedDB` 数据控制 (调试模式)
226 lines
9.3 KiB
Vue
226 lines
9.3 KiB
Vue
<script setup lang="ts">
|
|
import { initStationAlarms, initStationDevices, syncCameraApi, syncNvrChannelsApi, type Station } from '@/apis';
|
|
import { AlarmDetailModal, DeviceDetailModal, DeviceParamConfigModal, IcmpExportModal, RecordCheckExportModal, StationCard, type StationCardProps } from '@/components';
|
|
import { useLineDevicesQuery } from '@/composables';
|
|
import { useAlarmStore, useDeviceStore, useSettingStore, useStationStore } from '@/stores';
|
|
import { parseErrorFeedback } from '@/utils';
|
|
import { useMutation } from '@tanstack/vue-query';
|
|
import { NButton, NButtonGroup, NCheckbox, NFlex, NGrid, NGridItem, NScrollbar } from 'naive-ui';
|
|
import { storeToRefs } from 'pinia';
|
|
import { computed, ref } from 'vue';
|
|
|
|
const settingStore = useSettingStore();
|
|
const { stationGridCols: stationGridColumns } = storeToRefs(settingStore);
|
|
const stationStore = useStationStore();
|
|
const { stations } = storeToRefs(stationStore);
|
|
const deviceStore = useDeviceStore();
|
|
const { lineDevices } = storeToRefs(deviceStore);
|
|
const alarmStore = useAlarmStore();
|
|
const { lineAlarms } = storeToRefs(alarmStore);
|
|
|
|
const { refetch: refetchLineDevicesQuery } = useLineDevicesQuery();
|
|
|
|
// 操作栏
|
|
// 当点击操作栏中的一个按钮时,其他按钮会被禁用
|
|
type Action = 'export-icmp' | 'export-record' | 'sync-camera' | 'sync-nvr' | null;
|
|
const selectedAction = ref<Action>(null);
|
|
const showOperation = ref(false);
|
|
const stationSelectable = ref(false);
|
|
const stationSelection = ref<Record<Station['code'], boolean>>({});
|
|
const showIcmpExportModal = ref(false);
|
|
const showRecordCheckExportModal = ref(false);
|
|
|
|
const onToggleSelectAll = (checked: boolean) => {
|
|
if (!checked) {
|
|
stationSelection.value = {};
|
|
} else {
|
|
stationSelection.value = Object.fromEntries(stations.value.map((station) => [station.code, true]));
|
|
}
|
|
};
|
|
|
|
const onAction = (action: Action) => {
|
|
selectedAction.value = action;
|
|
if (action === null) return;
|
|
showOperation.value = true;
|
|
stationSelectable.value = true;
|
|
};
|
|
|
|
const onCancel = () => {
|
|
selectedAction.value = null;
|
|
showOperation.value = false;
|
|
stationSelectable.value = false;
|
|
stationSelection.value = {};
|
|
};
|
|
|
|
const onFinish = onCancel;
|
|
|
|
const { mutate: syncCamera, isPending: cameraSyncing } = useMutation({
|
|
mutationFn: async () => {
|
|
const stationCodes = Object.entries(stationSelection.value)
|
|
.filter(([, selected]) => selected)
|
|
.map(([code]) => code);
|
|
const results = await Promise.allSettled(stationCodes.map((stationCode) => syncCameraApi({ stationCode })));
|
|
return results.map((result, index) => ({ ...result, stationCode: stationCodes[index] }));
|
|
},
|
|
onSuccess: (results) => {
|
|
const successCount = results.filter((result) => result.status === 'fulfilled').length;
|
|
const failures = results.filter((result) => result.status === 'rejected');
|
|
const failureCount = failures.length;
|
|
if (failureCount > 0) {
|
|
const failedStations = failures.map((f) => stations.value.find((s) => s.code === f.stationCode)?.name).join('、');
|
|
if (successCount === 0) {
|
|
window.$message.error('摄像机同步全部失败');
|
|
window.$message.error(`${failedStations}`);
|
|
} else {
|
|
window.$message.warning(`摄像机同步完成:成功${successCount}个车站,失败${failureCount}个车站`);
|
|
window.$message.warning(`${failedStations}`);
|
|
}
|
|
} else {
|
|
window.$message.success('摄像机同步成功');
|
|
}
|
|
if (successCount > 0) {
|
|
// 摄像机同步后,需要重新查询一次设备,待测试
|
|
refetchLineDevicesQuery();
|
|
}
|
|
onFinish();
|
|
},
|
|
onError: (error) => {
|
|
console.error(error);
|
|
const errorFeedback = parseErrorFeedback(error);
|
|
window.$message.error(errorFeedback);
|
|
onCancel();
|
|
},
|
|
});
|
|
|
|
const { mutate: syncNvrChannels, isPending: nvrChannelsSyncing } = useMutation({
|
|
mutationFn: async () => {
|
|
const stationCodes = Object.entries(stationSelection.value)
|
|
.filter(([, selected]) => selected)
|
|
.map(([code]) => code);
|
|
const results = await Promise.allSettled(stationCodes.map((stationCode) => syncNvrChannelsApi({ stationCode })));
|
|
return results.map((result, index) => ({ ...result, stationCode: stationCodes[index] }));
|
|
},
|
|
onSuccess: (results) => {
|
|
const successCount = results.filter((result) => result.status === 'fulfilled').length;
|
|
const failures = results.filter((result) => result.status === 'rejected');
|
|
const failureCount = failures.length;
|
|
if (failureCount > 0) {
|
|
const failedStations = failures.map((failure) => stations.value.find((station) => station.code === failure.stationCode)?.name).join('、');
|
|
if (successCount === 0) {
|
|
window.$message.error('录像机通道同步全部失败');
|
|
window.$message.error(`${failedStations}`);
|
|
} else {
|
|
window.$message.warning(`录像机通道同步完成:成功${successCount}个车站,失败${failureCount}个车站`);
|
|
window.$message.warning(`${failedStations}`);
|
|
}
|
|
} else {
|
|
window.$message.success('录像机通道同步成功');
|
|
}
|
|
onFinish();
|
|
},
|
|
onError: (error) => {
|
|
console.error(error);
|
|
const errorFeedback = parseErrorFeedback(error);
|
|
window.$message.error(errorFeedback);
|
|
onCancel();
|
|
},
|
|
});
|
|
|
|
const confirming = computed(() => cameraSyncing.value || nvrChannelsSyncing.value);
|
|
|
|
const onConfirm = async () => {
|
|
const noStationSelected = !Object.values(stationSelection.value).some((selected) => selected);
|
|
if (selectedAction.value === 'export-icmp') {
|
|
if (noStationSelected) {
|
|
window.$message.warning('请选择要导出设备状态的车站');
|
|
return;
|
|
}
|
|
showIcmpExportModal.value = true;
|
|
} else if (selectedAction.value === 'export-record') {
|
|
if (noStationSelected) {
|
|
window.$message.warning('请选择要导出录像诊断的车站');
|
|
return;
|
|
}
|
|
showRecordCheckExportModal.value = true;
|
|
} else if (selectedAction.value === 'sync-camera') {
|
|
if (noStationSelected) {
|
|
window.$message.warning('请选择要同步摄像机的车站');
|
|
return;
|
|
}
|
|
syncCamera();
|
|
} else if (selectedAction.value === 'sync-nvr') {
|
|
if (noStationSelected) {
|
|
window.$message.warning('请选择要同步录像机通道的车站');
|
|
return;
|
|
}
|
|
syncNvrChannels();
|
|
} else {
|
|
return;
|
|
}
|
|
};
|
|
|
|
// 车站卡片的事件
|
|
const selectedStation = ref<Station>();
|
|
const showDeviceParamConfigModal = ref(false);
|
|
const showDeviceDetailModal = ref(false);
|
|
const showAlarmDetailModal = ref(false);
|
|
|
|
const onClickConfig: StationCardProps['onClickConfig'] = (station) => {
|
|
selectedStation.value = station;
|
|
showDeviceParamConfigModal.value = true;
|
|
};
|
|
|
|
const onClickDetail: StationCardProps['onClickDetail'] = (type, station) => {
|
|
selectedStation.value = station;
|
|
if (type === 'device') {
|
|
showDeviceDetailModal.value = true;
|
|
}
|
|
if (type === 'alarm') {
|
|
showAlarmDetailModal.value = true;
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<NScrollbar content-style="padding-right: 8px" style="width: 100%; height: 100%">
|
|
<!-- 工具栏 -->
|
|
<NFlex align="center" style="padding: 8px 8px 0 8px">
|
|
<NButtonGroup>
|
|
<NButton secondary :focusable="false" :disabled="!!selectedAction && selectedAction !== 'export-icmp'" @click="() => onAction('export-icmp')">导出设备状态</NButton>
|
|
<NButton secondary :focusable="false" :disabled="!!selectedAction && selectedAction !== 'export-record'" @click="() => onAction('export-record')">导出录像诊断</NButton>
|
|
<NButton secondary :focusable="false" :disabled="!!selectedAction && selectedAction !== 'sync-camera'" @click="() => onAction('sync-camera')">同步摄像机</NButton>
|
|
<NButton secondary :focusable="false" :disabled="!!selectedAction && selectedAction !== 'sync-nvr'" @click="() => onAction('sync-nvr')">同步录像机通道</NButton>
|
|
</NButtonGroup>
|
|
<template v-if="showOperation">
|
|
<NCheckbox label="全选" @update:checked="onToggleSelectAll" />
|
|
<NButton tertiary size="small" type="primary" :focusable="false" :loading="confirming" @click="onConfirm">确定</NButton>
|
|
<NButton tertiary size="small" type="tertiary" :focusable="false" :disabled="confirming" @click="onCancel">取消</NButton>
|
|
</template>
|
|
</NFlex>
|
|
|
|
<!-- 车站 -->
|
|
<NGrid :cols="stationGridColumns" :x-gap="6" :y-gap="6" style="padding: 8px">
|
|
<NGridItem v-for="station in stations" :key="station.code">
|
|
<StationCard
|
|
:station="station"
|
|
:devices="lineDevices[station.code] ?? initStationDevices()"
|
|
:alarms="lineAlarms[station.code] ?? initStationAlarms()"
|
|
:selectable="stationSelectable"
|
|
v-model:selected="stationSelection[station.code]"
|
|
@click-detail="onClickDetail"
|
|
@click-config="onClickConfig"
|
|
/>
|
|
</NGridItem>
|
|
</NGrid>
|
|
</NScrollbar>
|
|
|
|
<IcmpExportModal v-model:show="showIcmpExportModal" :stations="stations.filter((station) => stationSelection[station.code])" @after-leave="onFinish" />
|
|
<RecordCheckExportModal v-model:show="showRecordCheckExportModal" :stations="stations.filter((station) => stationSelection[station.code])" @after-leave="onFinish" />
|
|
|
|
<DeviceParamConfigModal v-model:show="showDeviceParamConfigModal" :station="selectedStation" />
|
|
<DeviceDetailModal v-model:show="showDeviceDetailModal" :station="selectedStation" />
|
|
<AlarmDetailModal v-model:show="showAlarmDetailModal" :station="selectedStation" />
|
|
</template>
|
|
|
|
<style scoped lang="scss"></style>
|