216 lines
9.0 KiB
Vue
216 lines
9.0 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 { useBatchActions, useLineDevicesQuery } from '@/composables';
|
||
import { useAlarmStore, useDeviceStore, usePermissionStore, useSettingStore } from '@/stores';
|
||
import { useMutation } from '@tanstack/vue-query';
|
||
import { useElementSize } 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, useTemplateRef } from 'vue';
|
||
|
||
const settingStore = useSettingStore();
|
||
const { stationGridCols } = storeToRefs(settingStore);
|
||
|
||
const permissionStore = usePermissionStore();
|
||
const stations = computed(() => permissionStore.stations.VIEW ?? []);
|
||
|
||
const deviceStore = useDeviceStore();
|
||
const { lineDevices } = storeToRefs(deviceStore);
|
||
|
||
const alarmStore = useAlarmStore();
|
||
const { lineAlarms } = storeToRefs(alarmStore);
|
||
|
||
const STATION_CARD_MIN_WIDTH = 230;
|
||
const STATION_GRID_PADDING = 8;
|
||
const STATION_GRID_GAP = 6;
|
||
const STATION_GRID_REF_NAME = 'stationGridRef';
|
||
const stationGridRef = useTemplateRef<HTMLDivElement>(STATION_GRID_REF_NAME);
|
||
const { width: stationGridWidth } = useElementSize(stationGridRef);
|
||
// 计算合适的车站布局列数
|
||
const actualStationGridColumns = computed(() => {
|
||
const currentStationCardWidth = (stationGridWidth.value - STATION_GRID_PADDING * 2 - (stationGridCols.value - 1) * STATION_GRID_GAP) / stationGridCols.value;
|
||
// 当卡片宽度大于最小宽度时,说明用户的设置没有问题,直接返回列数
|
||
if (currentStationCardWidth > STATION_CARD_MIN_WIDTH) return stationGridCols.value;
|
||
// 否则,说明用户的设置不合适,需要根据当前布局宽度重新计算列数
|
||
return Math.floor((stationGridWidth.value - STATION_GRID_PADDING * 2 + STATION_GRID_GAP) / STATION_CARD_MIN_WIDTH);
|
||
});
|
||
|
||
const showIcmpExportModal = ref(false);
|
||
const showRecordCheckExportModal = ref(false);
|
||
|
||
const abortController = ref(new AbortController());
|
||
|
||
const { batchActions, selectedAction, selectableStations, stationSelection, selectionProps, toggleSelectAction, toggleSelectAllStations, confirmAction, cancelAction } = useBatchActions(
|
||
stations,
|
||
abortController,
|
||
);
|
||
|
||
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 requests = await Promise.allSettled(stationCodes.map((stationCode) => syncCameraApi({ stationCode, signal })));
|
||
return requests.map((result, index) => ({ ...result, stationCode: stationCodes[index] }));
|
||
},
|
||
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 {
|
||
failedRequests.push(request);
|
||
}
|
||
}
|
||
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();
|
||
}
|
||
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 requests = await Promise.allSettled(stationCodes.map((stationCode) => syncNvrChannelsApi({ stationCode, signal })));
|
||
return requests.map((result, index) => ({ ...result, stationCode: stationCodes[index] }));
|
||
},
|
||
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 {
|
||
failedRequests.push(request);
|
||
}
|
||
}
|
||
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 onClickConfirmAction = () => {
|
||
confirmAction({
|
||
'export-icmp': () => {
|
||
showIcmpExportModal.value = true;
|
||
},
|
||
'export-record': () => {
|
||
showRecordCheckExportModal.value = true;
|
||
},
|
||
'sync-camera': () => {
|
||
syncCamera();
|
||
},
|
||
'sync-nvr': () => {
|
||
syncNvrChannels();
|
||
},
|
||
});
|
||
};
|
||
|
||
// 车站卡片的事件
|
||
const modalStation = ref<Station>();
|
||
const showDeviceParamConfigModal = ref(false);
|
||
const showDeviceDetailModal = ref(false);
|
||
const showAlarmDetailModal = ref(false);
|
||
|
||
const onClickConfig: StationCardProps['onClickConfig'] = (station) => {
|
||
modalStation.value = station;
|
||
showDeviceParamConfigModal.value = true;
|
||
};
|
||
|
||
const onClickDetail: StationCardProps['onClickDetail'] = (type, station) => {
|
||
modalStation.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: `${STATION_GRID_PADDING}px ${STATION_GRID_PADDING}px 0 ${STATION_GRID_PADDING}px` }">
|
||
<NButtonGroup>
|
||
<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="selectedAction">
|
||
<NCheckbox label="全选" :disabled="selectionProps.disabled" :checked="selectionProps.checked" :indeterminate="selectionProps.indeterminate" @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>
|
||
|
||
<!-- 车站 -->
|
||
<div :ref="STATION_GRID_REF_NAME">
|
||
<NGrid :cols="actualStationGridColumns" :x-gap="STATION_GRID_GAP" :y-gap="STATION_GRID_GAP" :style="{ padding: `${STATION_GRID_PADDING}px` }">
|
||
<NGridItem v-for="station in stations" :key="station.code">
|
||
<StationCard
|
||
:station="station"
|
||
:devices="lineDevices[station.code] ?? initStationDevices()"
|
||
:alarms="lineAlarms[station.code] ?? initStationAlarms()"
|
||
:selectable="!!selectableStations.find((selectable) => selectable.code === station.code)"
|
||
v-model:selected="stationSelection[station.code]"
|
||
@click-detail="onClickDetail"
|
||
@click-config="onClickConfig"
|
||
/>
|
||
</NGridItem>
|
||
</NGrid>
|
||
</div>
|
||
</NScrollbar>
|
||
|
||
<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="modalStation" />
|
||
<DeviceDetailModal v-model:show="showDeviceDetailModal" :station="modalStation" />
|
||
<AlarmDetailModal v-model:show="showAlarmDetailModal" :station="modalStation" />
|
||
</template>
|
||
|
||
<style scoped lang="scss"></style>
|