feat: 优化车站状态页面的操作栏交互逻辑
This commit is contained in:
@@ -1,177 +1,148 @@
|
||||
<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 { useBatchActions, useLineDevicesQuery } from '@/composables';
|
||||
import { useAlarmStore, useDeviceStore, useSettingStore, useStationStore } from '@/stores';
|
||||
import { parseErrorFeedback } from '@/utils';
|
||||
import { useMutation } from '@tanstack/vue-query';
|
||||
import { objectEntries } from '@vueuse/core';
|
||||
import { isCancel } from 'axios';
|
||||
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 abortController = ref(new AbortController());
|
||||
|
||||
const onAction = (action: Action) => {
|
||||
selectedAction.value = action;
|
||||
if (action === null) return;
|
||||
showOperation.value = true;
|
||||
stationSelectable.value = true;
|
||||
};
|
||||
const { batchActions, selectedAction, selectableStations, stationSelection, toggleSelectAction, toggleSelectAllStations, confirmAction, cancelAction } = useBatchActions(stations, abortController);
|
||||
|
||||
const onCancel = () => {
|
||||
selectedAction.value = null;
|
||||
showOperation.value = false;
|
||||
stationSelectable.value = false;
|
||||
stationSelection.value = {};
|
||||
};
|
||||
|
||||
const onFinish = onCancel;
|
||||
const { refetch: refetchLineDevicesQuery } = useLineDevicesQuery();
|
||||
|
||||
const { mutate: syncCamera, isPending: cameraSyncing } = useMutation({
|
||||
mutationFn: async () => {
|
||||
abortController.value.abort();
|
||||
abortController.value = new AbortController();
|
||||
|
||||
const signal = abortController.value.signal;
|
||||
|
||||
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] }));
|
||||
const requests = await Promise.allSettled(stationCodes.map((stationCode) => syncCameraApi({ stationCode, signal })));
|
||||
return requests.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}`);
|
||||
onSuccess: (requests) => {
|
||||
type PromiseRequest = (typeof requests)[number];
|
||||
const successRequests: PromiseRequest[] = [];
|
||||
const failedRequests: PromiseRequest[] = [];
|
||||
const canceledRequests: PromiseRequest[] = [];
|
||||
for (const request of requests) {
|
||||
if (request.status === 'fulfilled') {
|
||||
successRequests.push(request);
|
||||
} else if (isCancel(request.reason)) {
|
||||
canceledRequests.push(request);
|
||||
} else {
|
||||
window.$message.warning(`摄像机同步完成:成功${successCount}个车站,失败${failureCount}个车站`);
|
||||
window.$message.warning(`${failedStations}`);
|
||||
failedRequests.push(request);
|
||||
}
|
||||
} else {
|
||||
window.$message.success('摄像机同步成功');
|
||||
}
|
||||
if (successCount > 0) {
|
||||
// 摄像机同步后,需要重新查询一次设备,待测试
|
||||
const notices: string[] = [`成功 ${successRequests.length} 个车站`, `失败 ${failedRequests.length} 个车站`];
|
||||
if (canceledRequests.length > 0) notices.push(`取消 ${canceledRequests.length} 个车站`);
|
||||
window.$notification.info({
|
||||
title: '摄像机同步结果',
|
||||
content: notices.join(','),
|
||||
duration: 3000,
|
||||
});
|
||||
if (successRequests.length > 0) {
|
||||
// 摄像机同步后,需要重新查询一次设备
|
||||
refetchLineDevicesQuery();
|
||||
}
|
||||
onFinish();
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error(error);
|
||||
const errorFeedback = parseErrorFeedback(error);
|
||||
window.$message.error(errorFeedback);
|
||||
onCancel();
|
||||
cancelAction();
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: syncNvrChannels, isPending: nvrChannelsSyncing } = useMutation({
|
||||
mutationFn: async () => {
|
||||
abortController.value.abort();
|
||||
abortController.value = new AbortController();
|
||||
|
||||
const signal = abortController.value.signal;
|
||||
|
||||
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] }));
|
||||
const requests = await Promise.allSettled(stationCodes.map((stationCode) => syncNvrChannelsApi({ stationCode, signal })));
|
||||
return requests.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}`);
|
||||
onSuccess: (requests) => {
|
||||
type PromiseRequest = (typeof requests)[number];
|
||||
const successRequests: PromiseRequest[] = [];
|
||||
const failedRequests: PromiseRequest[] = [];
|
||||
const canceledRequests: PromiseRequest[] = [];
|
||||
for (const request of requests) {
|
||||
if (request.status === 'fulfilled') {
|
||||
successRequests.push(request);
|
||||
} else if (isCancel(request.reason)) {
|
||||
canceledRequests.push(request);
|
||||
} else {
|
||||
window.$message.warning(`录像机通道同步完成:成功${successCount}个车站,失败${failureCount}个车站`);
|
||||
window.$message.warning(`${failedStations}`);
|
||||
failedRequests.push(request);
|
||||
}
|
||||
} else {
|
||||
window.$message.success('录像机通道同步成功');
|
||||
}
|
||||
onFinish();
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error(error);
|
||||
const errorFeedback = parseErrorFeedback(error);
|
||||
window.$message.error(errorFeedback);
|
||||
onCancel();
|
||||
const notices: string[] = [`成功 ${successRequests.length} 个车站`, `失败 ${failedRequests.length} 个车站`];
|
||||
if (canceledRequests.length > 0) notices.push(`取消 ${canceledRequests.length} 个车站`);
|
||||
window.$notification.info({
|
||||
title: '录像机通道同步结果',
|
||||
content: notices.join(','),
|
||||
duration: 3000,
|
||||
});
|
||||
cancelAction();
|
||||
},
|
||||
});
|
||||
|
||||
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 onClickConfirmAction = () => {
|
||||
confirmAction({
|
||||
'export-icmp': () => {
|
||||
showIcmpExportModal.value = true;
|
||||
},
|
||||
'export-record': () => {
|
||||
showRecordCheckExportModal.value = true;
|
||||
},
|
||||
'sync-camera': () => {
|
||||
syncCamera();
|
||||
},
|
||||
'sync-nvr': () => {
|
||||
syncNvrChannels();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 车站卡片的事件
|
||||
const selectedStation = ref<Station>();
|
||||
const modalStation = ref<Station>();
|
||||
const showDeviceParamConfigModal = ref(false);
|
||||
const showDeviceDetailModal = ref(false);
|
||||
const showAlarmDetailModal = ref(false);
|
||||
|
||||
const onClickConfig: StationCardProps['onClickConfig'] = (station) => {
|
||||
selectedStation.value = station;
|
||||
modalStation.value = station;
|
||||
showDeviceParamConfigModal.value = true;
|
||||
};
|
||||
|
||||
const onClickDetail: StationCardProps['onClickDetail'] = (type, station) => {
|
||||
selectedStation.value = station;
|
||||
modalStation.value = station;
|
||||
if (type === 'device') {
|
||||
showDeviceDetailModal.value = true;
|
||||
}
|
||||
@@ -186,15 +157,19 @@ const onClickDetail: StationCardProps['onClickDetail'] = (type, station) => {
|
||||
<!-- 工具栏 -->
|
||||
<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>
|
||||
<template v-for="batchAction in batchActions" :key="batchAction.key">
|
||||
<NButton :secondary="!batchAction.active" :focusable="false" @click="() => toggleSelectAction(batchAction)">{{ batchAction.label }}</NButton>
|
||||
</template>
|
||||
</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 v-if="selectedAction">
|
||||
<NCheckbox
|
||||
label="全选"
|
||||
:disabled="selectableStations.length === 0"
|
||||
:checked="selectableStations.length > 0 && selectableStations.length === objectEntries(stationSelection).filter(([, selected]) => selected).length"
|
||||
@update:checked="toggleSelectAllStations"
|
||||
/>
|
||||
<NButton tertiary size="small" type="primary" :focusable="false" :loading="confirming" @click="onClickConfirmAction">确定</NButton>
|
||||
<NButton tertiary size="small" type="tertiary" :focusable="false" @click="cancelAction">取消</NButton>
|
||||
</template>
|
||||
</NFlex>
|
||||
|
||||
@@ -205,7 +180,7 @@ const onClickDetail: StationCardProps['onClickDetail'] = (type, station) => {
|
||||
:station="station"
|
||||
:devices="lineDevices[station.code] ?? initStationDevices()"
|
||||
:alarms="lineAlarms[station.code] ?? initStationAlarms()"
|
||||
:selectable="stationSelectable"
|
||||
:selectable="!!selectableStations.find((selectable) => selectable.code === station.code)"
|
||||
v-model:selected="stationSelection[station.code]"
|
||||
@click-detail="onClickDetail"
|
||||
@click-config="onClickConfig"
|
||||
@@ -214,12 +189,12 @@ const onClickDetail: StationCardProps['onClickDetail'] = (type, station) => {
|
||||
</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" />
|
||||
<IcmpExportModal v-model:show="showIcmpExportModal" :stations="stations.filter((station) => stationSelection[station.code])" @after-leave="cancelAction" />
|
||||
<RecordCheckExportModal v-model:show="showRecordCheckExportModal" :stations="stations.filter((station) => stationSelection[station.code])" @after-leave="cancelAction" />
|
||||
|
||||
<DeviceParamConfigModal v-model:show="showDeviceParamConfigModal" :station="selectedStation" />
|
||||
<DeviceDetailModal v-model:show="showDeviceDetailModal" :station="selectedStation" />
|
||||
<AlarmDetailModal v-model:show="showAlarmDetailModal" :station="selectedStation" />
|
||||
<DeviceParamConfigModal v-model:show="showDeviceParamConfigModal" :station="modalStation" />
|
||||
<DeviceDetailModal v-model:show="showDeviceDetailModal" :station="modalStation" />
|
||||
<AlarmDetailModal v-model:show="showAlarmDetailModal" :station="modalStation" />
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
Reference in New Issue
Block a user