refactor: 重构项目结构
- 优化 `车站-设备-告警` 轮询机制 - 改进设备卡片的布局 - 支持修改设备 - 告警轮询中获取完整告警数据 - 车站告警详情支持导出完整的 `今日告警列表` - 支持将状态持久化到 `IndexedDB` - 新增轮询控制 (调试模式) - 新增离线开发模式 (调试模式) - 新增 `IndexedDB` 数据控制 (调试模式)
This commit is contained in:
2
src/composables/alarm/index.ts
Normal file
2
src/composables/alarm/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './use-alarm-action-column';
|
||||
export * from './use-camera-snap-column';
|
||||
149
src/composables/alarm/use-alarm-action-column.ts
Normal file
149
src/composables/alarm/use-alarm-action-column.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import { deleteCameraIgnoreApi, pageCameraIgnoreApi, saveCameraIgnoreApi, updateDeviceAlarmLogApi, type NdmDeviceAlarmLogResultVO } from '@/apis';
|
||||
import { DEVICE_TYPE_LITERALS, tryGetDeviceType } from '@/enums';
|
||||
import { parseErrorFeedback } from '@/utils';
|
||||
import { useMutation } from '@tanstack/vue-query';
|
||||
import { NButton, NFlex, NPopconfirm, type DataTableColumn, type DataTableRowData } from 'naive-ui';
|
||||
import { h, type Ref } from 'vue';
|
||||
|
||||
export const useAlarmActionColumn = (tableData: Ref<DataTableRowData[]>) => {
|
||||
const { mutate: confirmAlarm } = useMutation({
|
||||
mutationFn: async (params: { id: string | null }) => {
|
||||
const { id } = params;
|
||||
if (!id) return;
|
||||
const alarmLog = tableData.value.find((item) => item.id === id);
|
||||
if (alarmLog) {
|
||||
alarmLog['alarmConfirm'] = '1';
|
||||
}
|
||||
await updateDeviceAlarmLogApi({ id, alarmConfirm: '1' });
|
||||
},
|
||||
onError: (error, variables) => {
|
||||
console.error(error);
|
||||
const errorFeedback = parseErrorFeedback(error);
|
||||
window.$message.error(errorFeedback);
|
||||
const { id } = variables;
|
||||
if (id) {
|
||||
const alarmLog = tableData.value.find((item) => item.id === id);
|
||||
if (alarmLog) {
|
||||
alarmLog['alarmConfirm'] = '2';
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: ignoreCamera } = useMutation({
|
||||
mutationFn: async (params: { id: string | null }) => {
|
||||
const { id } = params;
|
||||
if (!id) return;
|
||||
const alarmLog = tableData.value.find((item) => item.id === id);
|
||||
if (!alarmLog) return;
|
||||
const { records } = await pageCameraIgnoreApi({
|
||||
model: { deviceId: alarmLog.deviceId },
|
||||
extra: {},
|
||||
current: 1,
|
||||
size: 10,
|
||||
sort: 'id',
|
||||
order: 'descending',
|
||||
});
|
||||
const ignoredCamera = records.at(0);
|
||||
if (ignoredCamera) {
|
||||
window.$message.info('设备已被忽略');
|
||||
return;
|
||||
}
|
||||
await saveCameraIgnoreApi({ deviceId: alarmLog.deviceId });
|
||||
window.$message.success('忽略设备成功');
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error(error);
|
||||
const errorFeedback = parseErrorFeedback(error);
|
||||
window.$message.error(errorFeedback);
|
||||
},
|
||||
});
|
||||
|
||||
// const { mutate: noticeCamera } = useMutation({
|
||||
// mutationFn: async (params: { id: string | null }) => {
|
||||
// const { id } = params;
|
||||
// if (!id) return;
|
||||
// const alarmLog = tableData.value.find((item) => item.id === id);
|
||||
// if (!alarmLog) return;
|
||||
// const { records } = await pageCameraIgnoreApi({
|
||||
// model: { deviceId: alarmLog.deviceId },
|
||||
// extra: {},
|
||||
// current: 1,
|
||||
// size: 10,
|
||||
// sort: 'id',
|
||||
// order: 'descending',
|
||||
// });
|
||||
// if (records.length === 0) {
|
||||
// window.$message.info('设备未被忽略');
|
||||
// return;
|
||||
// }
|
||||
// await deleteCameraIgnoreApi([...records.map((record) => record.id ?? '')]);
|
||||
// window.$message.success('取消忽略设备成功');
|
||||
// },
|
||||
// onError: (error) => {
|
||||
// console.error(error);
|
||||
// const errorFeedback = parseErrorFeedback(error);
|
||||
// window.$message.error(errorFeedback);
|
||||
// },
|
||||
// });
|
||||
|
||||
const alarmActionColumn: DataTableColumn<NdmDeviceAlarmLogResultVO> = {
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
render: (rowData) => {
|
||||
const { id } = rowData;
|
||||
return h(
|
||||
NFlex,
|
||||
{
|
||||
size: 'small',
|
||||
justify: 'center',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
default: () => [
|
||||
rowData.alarmConfirm === '1'
|
||||
? h(NButton, { disabled: true, secondary: true, type: 'info', size: 'tiny' }, { default: () => '确认' })
|
||||
: h(
|
||||
NPopconfirm,
|
||||
{
|
||||
onPositiveClick: () => confirmAlarm({ id }),
|
||||
},
|
||||
{
|
||||
trigger: () => h(NButton, { secondary: true, type: 'info', size: 'tiny' }, { default: () => '确认' }),
|
||||
default: () => '确认告警?',
|
||||
},
|
||||
),
|
||||
tryGetDeviceType(rowData.deviceType) === DEVICE_TYPE_LITERALS.ndmCamera && [
|
||||
h(
|
||||
NPopconfirm,
|
||||
{
|
||||
onPositiveClick: () => ignoreCamera({ id }),
|
||||
},
|
||||
{
|
||||
trigger: () => h(NButton, { tertiary: true, type: 'info', size: 'tiny' }, { default: () => '忽略' }),
|
||||
default: () => '忽略设备?',
|
||||
},
|
||||
),
|
||||
// h(
|
||||
// NPopconfirm,
|
||||
// {
|
||||
// onPositiveClick: () => noticeCamera({ id }),
|
||||
// },
|
||||
// {
|
||||
// trigger: () => h(NButton, { text: true, type: 'info', size: 'small' }, { icon: () => h(EyeOutlined) }),
|
||||
// default: () => '取消忽略设备?',
|
||||
// },
|
||||
// ),
|
||||
],
|
||||
],
|
||||
},
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
alarmActionColumn,
|
||||
};
|
||||
};
|
||||
66
src/composables/alarm/use-camera-snap-column.ts
Normal file
66
src/composables/alarm/use-camera-snap-column.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { getCameraSnapApi, type NdmDeviceAlarmLogResultVO } from '@/apis';
|
||||
import { tryGetDeviceType, DEVICE_TYPE_LITERALS } from '@/enums';
|
||||
import { parseErrorFeedback } from '@/utils';
|
||||
import { useMutation } from '@tanstack/vue-query';
|
||||
import { NButton, NImage, type DataTableColumn, type DataTableRowData } from 'naive-ui';
|
||||
import { h, ref, watch, type Ref } from 'vue';
|
||||
|
||||
export const useCameraSnapColumn = (tableData: Ref<DataTableRowData[]>) => {
|
||||
const { mutateAsync: getSnapByDeviceId } = useMutation({
|
||||
mutationFn: async (params: { deviceAlarmLog: NdmDeviceAlarmLogResultVO }) => {
|
||||
const { deviceAlarmLog } = params;
|
||||
const { deviceId } = deviceAlarmLog;
|
||||
if (!deviceId) throw new Error('设备ID不能为空');
|
||||
const snap = await getCameraSnapApi(deviceId);
|
||||
return snap;
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error(error);
|
||||
const errorFeedback = parseErrorFeedback(error);
|
||||
window.$message.error(errorFeedback);
|
||||
},
|
||||
});
|
||||
|
||||
// 控制每一行的查看按钮loading状态
|
||||
const loadingMap = ref<Record<string, boolean>>({});
|
||||
watch(tableData, () => {
|
||||
loadingMap.value = {};
|
||||
});
|
||||
|
||||
const cameraSnapColumn: DataTableColumn<NdmDeviceAlarmLogResultVO & { snapUrl?: string }> = {
|
||||
title: '实时画面截图',
|
||||
key: 'snapUrl',
|
||||
align: 'center',
|
||||
render: (rowData) => {
|
||||
const { deviceType: deviceTypeCode, snapUrl } = rowData;
|
||||
const deviceType = tryGetDeviceType(deviceTypeCode);
|
||||
if (deviceType !== DEVICE_TYPE_LITERALS.ndmCamera) return null;
|
||||
if (!snapUrl) {
|
||||
const id = rowData.id ?? '';
|
||||
return h(
|
||||
NButton,
|
||||
{
|
||||
type: 'info',
|
||||
size: 'small',
|
||||
loading: !!loadingMap.value[id],
|
||||
onClick: async () => {
|
||||
loadingMap.value[id] = true;
|
||||
try {
|
||||
const snap = await getSnapByDeviceId({ deviceAlarmLog: rowData });
|
||||
rowData.snapUrl = snap.url;
|
||||
} finally {
|
||||
loadingMap.value[id] = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
{ default: () => '查看' },
|
||||
);
|
||||
} else {
|
||||
return h(NImage, { src: snapUrl, previewDisabled: false, showToolbar: false });
|
||||
}
|
||||
},
|
||||
};
|
||||
return {
|
||||
cameraSnapColumn,
|
||||
};
|
||||
};
|
||||
1
src/composables/device/index.ts
Normal file
1
src/composables/device/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './use-device-tree';
|
||||
89
src/composables/device/use-device-tree.ts
Normal file
89
src/composables/device/use-device-tree.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import type { LineDevices, NdmDeviceResultVO } from '@/apis';
|
||||
import { tryGetDeviceType, type DeviceType } from '@/enums';
|
||||
import { ref, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
export const useDeviceTree = () => {
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const selectedStationCode = ref<string>();
|
||||
const selectedDeviceType = ref<DeviceType>();
|
||||
const selectedDevice = ref<NdmDeviceResultVO>();
|
||||
|
||||
const initFromRoute = (lineDevices: LineDevices) => {
|
||||
const { stationCode, deviceType, deviceDbId } = route.query;
|
||||
if (stationCode) {
|
||||
selectedStationCode.value = stationCode as string;
|
||||
}
|
||||
if (deviceType) {
|
||||
selectedDeviceType.value = deviceType as DeviceType;
|
||||
}
|
||||
if (deviceDbId && selectedStationCode.value && selectedDeviceType.value) {
|
||||
const selectedDeviceDbId = deviceDbId as string;
|
||||
const stationDevices = lineDevices[selectedStationCode.value];
|
||||
if (stationDevices) {
|
||||
const devices = stationDevices[selectedDeviceType.value];
|
||||
if (devices) {
|
||||
const device = devices.find((device) => device.id === selectedDeviceDbId);
|
||||
if (device) {
|
||||
selectedDevice.value = device;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const selectDevice = (device: NdmDeviceResultVO, stationCode: string) => {
|
||||
selectedDevice.value = device;
|
||||
selectedStationCode.value = stationCode;
|
||||
const deviceType = tryGetDeviceType(device.deviceType);
|
||||
if (deviceType) {
|
||||
selectedDeviceType.value = deviceType;
|
||||
}
|
||||
};
|
||||
|
||||
const routeDevice = (device: NdmDeviceResultVO, stationCode: string, to: { path: string }) => {
|
||||
const deviceDbId = device.id;
|
||||
const deviceType = tryGetDeviceType(device.deviceType);
|
||||
router.push({
|
||||
path: to.path,
|
||||
query: {
|
||||
stationCode,
|
||||
deviceType,
|
||||
deviceDbId,
|
||||
from: route.path,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const syncToRoute = () => {
|
||||
const query = { ...route.query };
|
||||
// 当选中的设备发生变化时,删除from参数
|
||||
if (selectedDevice.value?.id && route.query.deviceDbId !== selectedDevice.value.id) {
|
||||
delete query['from'];
|
||||
}
|
||||
if (selectedStationCode.value) {
|
||||
query['stationCode'] = selectedStationCode.value;
|
||||
}
|
||||
if (selectedDeviceType.value) {
|
||||
query['deviceType'] = selectedDeviceType.value;
|
||||
}
|
||||
if (selectedDevice.value?.id) {
|
||||
query['deviceDbId'] = selectedDevice.value.id;
|
||||
}
|
||||
router.replace({ query });
|
||||
};
|
||||
|
||||
watch(selectedDevice, syncToRoute);
|
||||
|
||||
return {
|
||||
selectedStationCode,
|
||||
selectedDeviceType,
|
||||
selectedDevice,
|
||||
|
||||
initFromRoute,
|
||||
selectDevice,
|
||||
routeDevice,
|
||||
};
|
||||
};
|
||||
4
src/composables/index.ts
Normal file
4
src/composables/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './alarm';
|
||||
export * from './device';
|
||||
export * from './query';
|
||||
export * from './stomp';
|
||||
4
src/composables/query/index.ts
Normal file
4
src/composables/query/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './use-line-alarms-query';
|
||||
export * from './use-line-devices-query';
|
||||
export * from './use-line-stations-query';
|
||||
export * from './use-version-check-query';
|
||||
88
src/composables/query/use-line-alarms-query.ts
Normal file
88
src/composables/query/use-line-alarms-query.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { initStationAlarms, pageDeviceAlarmLogApi, type Station } from '@/apis';
|
||||
import { LINE_ALARMS_QUERY_KEY, STATION_ALARMS_MUTATION_KEY } from '@/constants';
|
||||
import { tryGetDeviceType } from '@/enums';
|
||||
import { useAlarmStore, useStationStore } from '@/stores';
|
||||
import { parseErrorFeedback } from '@/utils';
|
||||
import { CancelledError, useMutation, useQuery } from '@tanstack/vue-query';
|
||||
import { isCancel } from 'axios';
|
||||
import dayjs from 'dayjs';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { computed } from 'vue';
|
||||
|
||||
export const useStationAlarmsMutation = () => {
|
||||
const alarmStore = useAlarmStore();
|
||||
|
||||
return useMutation({
|
||||
mutationKey: [STATION_ALARMS_MUTATION_KEY],
|
||||
mutationFn: async (params: { station: Station; signal?: AbortSignal }) => {
|
||||
const { station, signal } = params;
|
||||
const stationAlarms = initStationAlarms();
|
||||
if (!station.online) {
|
||||
return stationAlarms;
|
||||
}
|
||||
const now = dayjs();
|
||||
const todayStart = now.startOf('date').valueOf();
|
||||
const todayEnd = now.endOf('date').valueOf();
|
||||
const { records } = await pageDeviceAlarmLogApi(
|
||||
{
|
||||
model: {
|
||||
stationCode: station.code,
|
||||
},
|
||||
extra: {
|
||||
alarmDate_ge: todayStart,
|
||||
alarmDate_le: todayEnd,
|
||||
},
|
||||
size: 50000,
|
||||
current: 1,
|
||||
sort: 'alarmDate',
|
||||
order: 'descending',
|
||||
},
|
||||
{
|
||||
stationCode: station.code,
|
||||
signal,
|
||||
},
|
||||
);
|
||||
for (const alarm of records) {
|
||||
const { deviceType: deviceTypeCode } = alarm;
|
||||
const deviceType = tryGetDeviceType(deviceTypeCode);
|
||||
if (!!deviceType) {
|
||||
stationAlarms[deviceType].unshift(alarm);
|
||||
}
|
||||
}
|
||||
stationAlarms['unclassified'] = records;
|
||||
return stationAlarms;
|
||||
},
|
||||
onSuccess: (stationAlarms, { station }) => {
|
||||
alarmStore.setStationAlarms(station.code, stationAlarms);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error(error);
|
||||
if (isCancel(error) || error instanceof CancelledError) return;
|
||||
const errorFeedback = parseErrorFeedback(error);
|
||||
window.$message.error(errorFeedback);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 由 `useLineStationsQuery` 显式触发
|
||||
* @see [use-line-stations-query.ts](./use-line-stations-query.ts)
|
||||
*/
|
||||
export const useLineAlarmsQuery = () => {
|
||||
const stationStore = useStationStore();
|
||||
const { stations } = storeToRefs(stationStore);
|
||||
const { mutateAsync: getStationAlarms } = useStationAlarmsMutation();
|
||||
|
||||
return useQuery({
|
||||
queryKey: computed(() => [LINE_ALARMS_QUERY_KEY]),
|
||||
enabled: false,
|
||||
queryFn: async ({ signal }) => {
|
||||
console.time(LINE_ALARMS_QUERY_KEY);
|
||||
for (const station of stations.value) {
|
||||
await getStationAlarms({ station, signal }).catch(() => {});
|
||||
}
|
||||
console.timeEnd(LINE_ALARMS_QUERY_KEY);
|
||||
return null;
|
||||
},
|
||||
});
|
||||
};
|
||||
55
src/composables/query/use-line-devices-query.ts
Normal file
55
src/composables/query/use-line-devices-query.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { getAllDevicesApi, initStationDevices, type Station } from '@/apis';
|
||||
import { LINE_DEVICES_QUERY_KEY, STATION_DEVICES_MUTATION_KEY } from '@/constants';
|
||||
import { useDeviceStore, useStationStore } from '@/stores';
|
||||
import { parseErrorFeedback } from '@/utils';
|
||||
import { CancelledError, useMutation, useQuery } from '@tanstack/vue-query';
|
||||
import { isCancel } from 'axios';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { computed } from 'vue';
|
||||
|
||||
export const useStationDevicesMutation = () => {
|
||||
const deviceStore = useDeviceStore();
|
||||
|
||||
return useMutation({
|
||||
mutationKey: [STATION_DEVICES_MUTATION_KEY],
|
||||
mutationFn: async (params: { station: Station; signal?: AbortSignal }) => {
|
||||
const { station, signal } = params;
|
||||
if (!station.online) {
|
||||
return initStationDevices();
|
||||
}
|
||||
return await getAllDevicesApi({ stationCode: station.code, signal });
|
||||
},
|
||||
onSuccess: (devices, { station }) => {
|
||||
deviceStore.setStationDevices(station.code, devices);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error(error);
|
||||
if (isCancel(error) || error instanceof CancelledError) return;
|
||||
const errorFeedback = parseErrorFeedback(error);
|
||||
window.$message.error(errorFeedback);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 由 `useLineStationsQuery` 显式触发
|
||||
* @see [use-line-stations-query.ts](./use-line-stations-query.ts)
|
||||
*/
|
||||
export const useLineDevicesQuery = () => {
|
||||
const stationStore = useStationStore();
|
||||
const { stations } = storeToRefs(stationStore);
|
||||
const { mutateAsync: getStationDevices } = useStationDevicesMutation();
|
||||
|
||||
return useQuery({
|
||||
queryKey: computed(() => [LINE_DEVICES_QUERY_KEY]),
|
||||
enabled: false,
|
||||
queryFn: async ({ signal }) => {
|
||||
console.time(LINE_DEVICES_QUERY_KEY);
|
||||
for (const station of stations.value) {
|
||||
await getStationDevices({ station, signal }).catch(() => {});
|
||||
}
|
||||
console.timeEnd(LINE_DEVICES_QUERY_KEY);
|
||||
return null;
|
||||
},
|
||||
});
|
||||
};
|
||||
73
src/composables/query/use-line-stations-query.ts
Normal file
73
src/composables/query/use-line-stations-query.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { batchVerifyApi, type Station } from '@/apis';
|
||||
import { LINE_STATIONS_MUTATION_KEY, LINE_STATIONS_QUERY_KEY } from '@/constants';
|
||||
import { usePollingStore, useStationStore } from '@/stores';
|
||||
import { getAppEnvConfig, parseErrorFeedback } from '@/utils';
|
||||
import { CancelledError, useMutation, useQuery } from '@tanstack/vue-query';
|
||||
import axios, { isCancel } from 'axios';
|
||||
import dayjs from 'dayjs';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { computed } from 'vue';
|
||||
import { useLineDevicesQuery } from './use-line-devices-query';
|
||||
import { useLineAlarmsQuery } from './use-line-alarms-query';
|
||||
|
||||
export const useLineStationsMutation = () => {
|
||||
const stationStore = useStationStore();
|
||||
|
||||
return useMutation({
|
||||
mutationKey: [LINE_STATIONS_MUTATION_KEY],
|
||||
mutationFn: async (params: { signal?: AbortSignal }) => {
|
||||
const { signal } = params;
|
||||
const { data: ndmStationList } = await axios.get<{ code: string; name: string }[]>(`/minio/ndm/ndm-stations.json?_t=${dayjs().unix()}`, { signal });
|
||||
const stations = ndmStationList.map<Station>((station) => ({
|
||||
code: station.code ?? '',
|
||||
name: station.name ?? '',
|
||||
online: false,
|
||||
ip: '',
|
||||
}));
|
||||
const verifyList = await batchVerifyApi({ signal });
|
||||
return stations.map((station) => ({
|
||||
...station,
|
||||
online: !!verifyList.find((verify) => verify.stationCode === station.code)?.onlineState,
|
||||
ip: verifyList.find((verify) => verify.stationCode === station.code)?.ipAddress ?? '',
|
||||
}));
|
||||
},
|
||||
onSuccess: (stations) => {
|
||||
stationStore.setStations(stations);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error(error);
|
||||
if (isCancel(error) || error instanceof CancelledError) return;
|
||||
const errorFeedback = parseErrorFeedback(error);
|
||||
window.$message.error(errorFeedback);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useLineStationsQuery = () => {
|
||||
const pollingStore = usePollingStore();
|
||||
const { pollingEnabled } = storeToRefs(pollingStore);
|
||||
const { requestInterval } = getAppEnvConfig();
|
||||
const { mutateAsync: getLineStations } = useLineStationsMutation();
|
||||
const { refetch: refetchLineDevicesQuery } = useLineDevicesQuery();
|
||||
const { refetch: refetchLineAlarmsQuery } = useLineAlarmsQuery();
|
||||
|
||||
return useQuery({
|
||||
queryKey: computed(() => [LINE_STATIONS_QUERY_KEY]),
|
||||
enabled: computed(() => pollingEnabled.value),
|
||||
refetchInterval: requestInterval * 1000,
|
||||
staleTime: (requestInterval * 1000) / 2,
|
||||
queryFn: async ({ signal }) => {
|
||||
console.time(LINE_STATIONS_QUERY_KEY);
|
||||
await getLineStations({ signal }).catch(() => {});
|
||||
console.timeEnd(LINE_STATIONS_QUERY_KEY);
|
||||
|
||||
if (!pollingEnabled.value) return null;
|
||||
await refetchLineDevicesQuery();
|
||||
|
||||
if (!pollingEnabled.value) return null;
|
||||
await refetchLineAlarmsQuery();
|
||||
|
||||
return null;
|
||||
},
|
||||
});
|
||||
};
|
||||
75
src/composables/query/use-version-check-query.ts
Normal file
75
src/composables/query/use-version-check-query.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { verifyApi, type VersionInfo } from '@/apis';
|
||||
import { VERSION_CHECK_QUERY_KEY } from '@/constants';
|
||||
import { useSettingStore, useUserStore } from '@/stores';
|
||||
import { useQuery, useQueryClient } from '@tanstack/vue-query';
|
||||
import axios from 'axios';
|
||||
import { useThemeVars } from 'naive-ui';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { computed, h, ref, watch } from 'vue';
|
||||
|
||||
export const useVersionCheckQuery = () => {
|
||||
const localVersionInfo = ref<VersionInfo>();
|
||||
const showDialog = ref<boolean>(false);
|
||||
const themeVars = useThemeVars();
|
||||
const queryClient = useQueryClient();
|
||||
const userStore = useUserStore();
|
||||
const { userLoginResult } = storeToRefs(userStore);
|
||||
const settingStore = useSettingStore();
|
||||
const { offlineDev } = storeToRefs(settingStore);
|
||||
|
||||
const { data: remoteVersionInfo, dataUpdatedAt } = useQuery({
|
||||
queryKey: [VERSION_CHECK_QUERY_KEY],
|
||||
enabled: computed(() => !offlineDev.value),
|
||||
refetchInterval: 10 * 1000,
|
||||
queryFn: async ({ signal }) => {
|
||||
if (!!userLoginResult.value?.token) await verifyApi({ signal });
|
||||
const { data } = await axios.get<VersionInfo>(`/manifest.json?t=${Date.now()}`, { signal });
|
||||
return data;
|
||||
},
|
||||
});
|
||||
|
||||
watch(offlineDev, (offline) => {
|
||||
if (offline) {
|
||||
queryClient.cancelQueries({ queryKey: [VERSION_CHECK_QUERY_KEY] });
|
||||
}
|
||||
});
|
||||
|
||||
watch(dataUpdatedAt, () => {
|
||||
const newVersionInfo = remoteVersionInfo.value;
|
||||
if (!newVersionInfo) return;
|
||||
|
||||
if (!localVersionInfo.value) {
|
||||
localVersionInfo.value = newVersionInfo;
|
||||
return;
|
||||
}
|
||||
|
||||
const { version: localVersion, buildTime: localBuildTime } = localVersionInfo.value;
|
||||
const { version: remoteVersion, buildTime: remoteBuildTime } = newVersionInfo;
|
||||
|
||||
if ((localVersion !== remoteVersion || localBuildTime !== remoteBuildTime) && !showDialog.value) {
|
||||
showDialog.value = true;
|
||||
window.$dialog.info({
|
||||
title: '发现新版本',
|
||||
content: () =>
|
||||
h('div', {}, [
|
||||
h('div', {}, { default: () => `当前版本:${localVersionInfo.value?.version}` }),
|
||||
h('div', {}, { default: () => `最新版本:${newVersionInfo.version}` }),
|
||||
h('div', {}, { default: () => '请点击刷新页面以更新' }),
|
||||
h('div', { style: { marginTop: '8px', fontWeight: '700', color: themeVars.value.warningColor } }, { default: () => '⚠️ 注意,更新后可能需要重新登录或调整系统设置!' }),
|
||||
]),
|
||||
positiveText: '刷新页面',
|
||||
maskClosable: false,
|
||||
onPositiveClick: () => {
|
||||
window.location.reload();
|
||||
},
|
||||
negativeText: '稍后检查',
|
||||
onNegativeClick: () => {
|
||||
showDialog.value = false;
|
||||
},
|
||||
onClose: () => {
|
||||
showDialog.value = false;
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
1
src/composables/stomp/index.ts
Normal file
1
src/composables/stomp/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './use-stomp-client';
|
||||
123
src/composables/stomp/use-stomp-client.ts
Normal file
123
src/composables/stomp/use-stomp-client.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import type { NdmDeviceAlarmLogResultVO, Station, SyncCameraResult } from '@/apis';
|
||||
import { ALARM_TOPIC, SYNC_CAMERA_STATUS_TOPIC } from '@/constants';
|
||||
import { useAlarmStore, useSettingStore, useStationStore } from '@/stores';
|
||||
import { Client } from '@stomp/stompjs';
|
||||
import { watchDebounced } from '@vueuse/core';
|
||||
import destr from 'destr';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||
import { useStationAlarmsMutation } from '../query';
|
||||
|
||||
const getBrokerUrl = () => {
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const { host } = window.location;
|
||||
const endpoint = '/ws';
|
||||
const brokerURL = `${protocol}//${host}${endpoint}`;
|
||||
return brokerURL;
|
||||
};
|
||||
|
||||
export const useStompClient = () => {
|
||||
const stationStore = useStationStore();
|
||||
const { stations } = storeToRefs(stationStore);
|
||||
const alarmStore = useAlarmStore();
|
||||
const { unreadLineAlarms } = storeToRefs(alarmStore);
|
||||
const settingStore = useSettingStore();
|
||||
const { offlineDev } = storeToRefs(settingStore);
|
||||
|
||||
const { mutate: refreshStationAlarms } = useStationAlarmsMutation();
|
||||
|
||||
const stompClient = ref<Client | null>(null);
|
||||
|
||||
const syncCameraResult = ref<Record<Station['code'], SyncCameraResult>>({});
|
||||
|
||||
onMounted(() => {
|
||||
stompClient.value = new Client({
|
||||
brokerURL: getBrokerUrl(),
|
||||
reconnectDelay: 5000,
|
||||
heartbeatIncoming: 10000,
|
||||
heartbeatOutgoing: 10000,
|
||||
onConnect: () => {
|
||||
console.log('Stomp连接成功');
|
||||
stompClient.value?.subscribe(ALARM_TOPIC, (message) => {
|
||||
const alarm = destr<NdmDeviceAlarmLogResultVO>(message.body);
|
||||
if (alarm.alarmCategory === '1') {
|
||||
alarmStore.pushUnreadAlarm(alarm);
|
||||
}
|
||||
});
|
||||
stompClient.value?.subscribe(SYNC_CAMERA_STATUS_TOPIC, (message) => {
|
||||
const { stationCode, startTime, endTime, insertList, updateList, deleteList } = destr<SyncCameraResult>(message.body);
|
||||
syncCameraResult.value[stationCode] = { stationCode, startTime, endTime, insertList, updateList, deleteList };
|
||||
});
|
||||
},
|
||||
onDisconnect: () => {
|
||||
console.log('Stomp连接断开');
|
||||
stompClient.value?.unsubscribe(ALARM_TOPIC);
|
||||
stompClient.value?.unsubscribe(SYNC_CAMERA_STATUS_TOPIC);
|
||||
},
|
||||
onStompError: (frame) => {
|
||||
console.log('Stomp错误', frame);
|
||||
window.$message.error('Stomp错误');
|
||||
},
|
||||
onWebSocketError: (event: Event) => {
|
||||
console.log('WebSocket错误', event);
|
||||
window.$message.error('WebSocket错误');
|
||||
},
|
||||
});
|
||||
if (!offlineDev.value) {
|
||||
stompClient.value.activate();
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stompClient.value?.deactivate();
|
||||
stompClient.value = null;
|
||||
});
|
||||
|
||||
watch(offlineDev, (offline) => {
|
||||
if (offline) {
|
||||
stompClient.value?.deactivate();
|
||||
} else {
|
||||
stompClient.value?.activate();
|
||||
}
|
||||
});
|
||||
|
||||
// 当有车站的未读报警变化,即新收到告警时,需要同步告警数据,
|
||||
// 但告警可能非常频繁,所以需要防抖处理
|
||||
const abortControllerMap = ref(new Map<Station['code'], AbortController>());
|
||||
watchDebounced(
|
||||
() => Object.entries(unreadLineAlarms.value).map(([stationCode, stationAlarms]) => ({ stationCode, count: stationAlarms['unclassified'].length })),
|
||||
(newValue, oldValue) => {
|
||||
// 启用离线模式时,跳过处理
|
||||
if (offlineDev.value) return;
|
||||
if (newValue.length === 0) return;
|
||||
const codes: Station['code'][] = [];
|
||||
newValue.forEach(({ stationCode, count }) => {
|
||||
const prevState = oldValue.find((stat) => stat.stationCode === stationCode);
|
||||
if (!prevState || count !== prevState.count) {
|
||||
codes.push(stationCode);
|
||||
}
|
||||
});
|
||||
// console.log('以下车站收到新告警:', codes);
|
||||
for (const stationCode of codes) {
|
||||
const station = stations.value.find((station) => station.code === stationCode);
|
||||
if (!station) continue;
|
||||
abortControllerMap.value.get(stationCode)?.abort();
|
||||
abortControllerMap.value.set(stationCode, new AbortController());
|
||||
refreshStationAlarms({ station, signal: abortControllerMap.value.get(stationCode)?.signal });
|
||||
}
|
||||
},
|
||||
{
|
||||
debounce: 2500,
|
||||
maxWait: 5000,
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
stompClient,
|
||||
|
||||
syncCameraResult,
|
||||
afterCheckSyncCameraResult: () => {
|
||||
syncCameraResult.value = {};
|
||||
},
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user