Compare commits

...

10 Commits

19 changed files with 193 additions and 161 deletions

View File

@@ -1,45 +1,21 @@
import type { Nullable } from '@/types';
import type { NdmAlarmHost, NdmAlarmHostPageQuery } from './alarm';
import type { NdmSecurityBox, NdmSecurityBoxPageQuery, NdmSwitch, NdmSwitchPageQuery } from './other';
import type { NdmNvr, NdmNvrPageQuery } from './storage';
import type {
NdmCamera,
NdmCameraPageQuery,
NdmDecoder,
NdmDecoderPageQuery,
NdmKeyboard,
NdmKeyboardPageQuery,
NdmMediaServer,
NdmMediaServerPageQuery,
NdmMediaServerResultVO,
NdmMediaServerSaveVO,
NdmMediaServerUpdateVO,
NdmVideoServer,
NdmVideoServerPageQuery,
NdmVideoServerResultVO,
NdmVideoServerSaveVO,
NdmVideoServerUpdateVO,
} from './video';
import type { Nullable, Optional } from '@/types';
import type { ReduceForPageQuery, ReduceForSaveVO, ReduceForUpdateVO } from '../../base';
import type { NdmAlarmHost } from './alarm';
import type { NdmSecurityBox, NdmSwitch } from './other';
import type { NdmNvr } from './storage';
import type { NdmCamera, NdmDecoder, NdmKeyboard, NdmMediaServer, NdmVideoServer } from './video';
export type NdmDevice = NdmAlarmHost | NdmCamera | NdmDecoder | NdmKeyboard | NdmMediaServer | NdmNvr | NdmSecurityBox | NdmSwitch | NdmVideoServer;
export type NdmDevicePageQuery =
| NdmAlarmHostPageQuery
| NdmCameraPageQuery
| NdmDecoderPageQuery
| NdmKeyboardPageQuery
| NdmMediaServerPageQuery
| NdmNvrPageQuery
| NdmSecurityBoxPageQuery
| NdmSwitchPageQuery
| NdmVideoServerPageQuery;
export type NdmDeviceResultVO = Nullable<NdmDevice>;
export type NdmDeviceSaveVO = Partial<Omit<NdmDevice, ReduceForSaveVO>>;
export type NdmDeviceUpdateVO = Optional<Omit<NdmDevice, ReduceForUpdateVO>>;
export type NdmDevicePageQuery = Partial<Omit<NdmDevice, ReduceForPageQuery>>;
export type NdmServer = NdmMediaServer | NdmVideoServer;
export type NdmServerResultVO = NdmMediaServerResultVO | NdmVideoServerResultVO;
export type NdmServerSaveVO = NdmMediaServerSaveVO | NdmVideoServerSaveVO;
export type NdmServerUpdateVO = NdmMediaServerUpdateVO | NdmVideoServerUpdateVO;
export type NdmServerPageQuery = NdmMediaServerPageQuery | NdmVideoServerPageQuery;
export type NdmServerResultVO = Nullable<NdmServer>;
export type NdmServerSaveVO = Partial<Omit<NdmServer, ReduceForSaveVO>>;
export type NdmServerUpdateVO = Optional<Omit<NdmServer, ReduceForUpdateVO>>;
export type NdmServerPageQuery = Partial<Omit<NdmServer, ReduceForPageQuery>>;
export * from './alarm';
export * from './icmp';

View File

@@ -20,40 +20,31 @@ export const detailDeviceApi = async (device: NdmDeviceResultVO, options?: { sta
const deviceType = tryGetDeviceType(deviceTypeCode);
if (!deviceType) throw new Error('未知的设备');
if (deviceType === DEVICE_TYPE_LITERALS.ndmAlarmHost) {
await detailAlarmHostApi(id, { stationCode, signal });
return;
return await detailAlarmHostApi(id, { stationCode, signal });
}
if (deviceType === DEVICE_TYPE_LITERALS.ndmCamera) {
await detailCameraApi(id, { stationCode, signal });
return;
return await detailCameraApi(id, { stationCode, signal });
}
if (deviceType === DEVICE_TYPE_LITERALS.ndmDecoder) {
await detailDecoderApi(id, { stationCode, signal });
return;
return await detailDecoderApi(id, { stationCode, signal });
}
if (deviceType === DEVICE_TYPE_LITERALS.ndmKeyboard) {
await detailKeyboardApi(id, { stationCode, signal });
return;
return await detailKeyboardApi(id, { stationCode, signal });
}
if (deviceType === DEVICE_TYPE_LITERALS.ndmMediaServer) {
await detailMediaServerApi(id, { stationCode, signal });
return;
return await detailMediaServerApi(id, { stationCode, signal });
}
if (deviceType === DEVICE_TYPE_LITERALS.ndmNvr) {
await detailNvrApi(id, { stationCode, signal });
return;
return await detailNvrApi(id, { stationCode, signal });
}
if (deviceType === DEVICE_TYPE_LITERALS.ndmSecurityBox) {
await detailSecurityBoxApi(id, { stationCode, signal });
return;
return await detailSecurityBoxApi(id, { stationCode, signal });
}
if (deviceType === DEVICE_TYPE_LITERALS.ndmSwitch) {
await detailSwitchApi(id, { stationCode, signal });
return;
return await detailSwitchApi(id, { stationCode, signal });
}
if (deviceType === DEVICE_TYPE_LITERALS.ndmVideoServer) {
await detailVideoServerApi(id, { stationCode, signal });
return;
return await detailVideoServerApi(id, { stationCode, signal });
}
return undefined;
};

View File

@@ -3,3 +3,4 @@ export * from './detail-device';
export * from './export-device';
export * from './import-device';
export * from './probe-device';
export * from './update-device';

View File

@@ -0,0 +1,59 @@
import {
updateAlarmHostApi,
updateCameraApi,
updateDecoderApi,
updateKeyboardApi,
updateMediaServerApi,
updateNvrApi,
updateSecurityBoxApi,
updateSwitchApi,
updateVideoServerApi,
type NdmDeviceResultVO,
type Station,
} from '@/apis';
import { DEVICE_TYPE_LITERALS, tryGetDeviceType } from '@/enums';
export const updateDeviceApi = async (device: NdmDeviceResultVO, options?: { stationCode?: Station['code']; signal?: AbortSignal }): Promise<NdmDeviceResultVO | undefined> => {
const { stationCode, signal } = options ?? {};
const { id, deviceType: deviceTypeCode } = device;
if (!id || !deviceTypeCode) throw new Error('未知的设备');
const deviceType = tryGetDeviceType(deviceTypeCode);
if (!deviceType) throw new Error('未知的设备');
if (deviceType === DEVICE_TYPE_LITERALS.ndmAlarmHost) {
await updateAlarmHostApi(device, { stationCode, signal });
return;
}
if (deviceType === DEVICE_TYPE_LITERALS.ndmCamera) {
await updateCameraApi(device, { stationCode, signal });
return;
}
if (deviceType === DEVICE_TYPE_LITERALS.ndmDecoder) {
await updateDecoderApi(device, { stationCode, signal });
return;
}
if (deviceType === DEVICE_TYPE_LITERALS.ndmKeyboard) {
await updateKeyboardApi(device, { stationCode, signal });
return;
}
if (deviceType === DEVICE_TYPE_LITERALS.ndmMediaServer) {
await updateMediaServerApi(device, { stationCode, signal });
return;
}
if (deviceType === DEVICE_TYPE_LITERALS.ndmNvr) {
await updateNvrApi(device, { stationCode, signal });
return;
}
if (deviceType === DEVICE_TYPE_LITERALS.ndmSecurityBox) {
await updateSecurityBoxApi(device, { stationCode, signal });
return;
}
if (deviceType === DEVICE_TYPE_LITERALS.ndmSwitch) {
await updateSwitchApi(device, { stationCode, signal });
return;
}
if (deviceType === DEVICE_TYPE_LITERALS.ndmVideoServer) {
await updateVideoServerApi(device, { stationCode, signal });
return;
}
return undefined;
};

View File

@@ -139,9 +139,6 @@ onBeforeUnmount(() => {
<NFormItem label-placement="left" label="设备描述">
<NInput v-model:value="localDevice.description" />
</NFormItem>
<NFormItem label-placement="left" label="上游设备">
<NInput v-model:value="localDevice.linkDescription" />
</NFormItem>
</NForm>
</template>
<template #action>

View File

@@ -19,11 +19,11 @@ const isCameraTypeCode = (code: string): code is CameraType => {
import type { NdmCameraResultVO, Station } from '@/apis';
import { DeviceCommonCard, DeviceHeaderCard } from '@/components';
import { useSettingStore } from '@/stores';
import { computedAsync } from '@vueuse/core';
import { useQuery, useQueryClient } from '@tanstack/vue-query';
import axios from 'axios';
import { NDescriptions, NDescriptionsItem, NFlex } from 'naive-ui';
import { storeToRefs } from 'pinia';
import { computed, toRefs } from 'vue';
import { computed, toRefs, watch } from 'vue';
const props = defineProps<{
ndmDevice: NdmCameraResultVO;
@@ -33,6 +33,8 @@ const props = defineProps<{
const settingStore = useSettingStore();
const { offlineDev } = storeToRefs(settingStore);
const queryClient = useQueryClient();
const { ndmDevice, station } = toRefs(props);
const cameraType = computed(() => {
@@ -43,14 +45,15 @@ const cameraType = computed(() => {
return CAMERA_TYPES[cameraTypeCode];
});
const installationArea = computedAsync(async (onCancel) => {
const QUERY_KEY = 'camera-installation-area-query';
const { data: installationArea } = useQuery({
queryKey: computed(() => [QUERY_KEY, ndmDevice.value.gbCode, station.value.code]),
enabled: computed(() => !offlineDev.value),
gcTime: 0,
queryFn: async ({ signal }) => {
const UNKNOWN_NAME = '-';
if (offlineDev.value) return UNKNOWN_NAME;
const abortController = new AbortController();
onCancel(() => abortController.abort());
const gbCode = ndmDevice.value.gbCode;
if (!gbCode) return UNKNOWN_NAME;
@@ -65,7 +68,7 @@ const installationArea = computedAsync(async (onCancel) => {
type Unit = { name: string; type: 'train' | 'station' | 'parking' | 'occ' };
type Area = { code: string; name: string; subs: Array<{ code: string; name: string }> };
const { data: unitCodes } = await axios.get<Record<string, Unit>>(CODE_STATIONS_JSON_PATH, { signal: abortController.signal });
const { data: unitCodes } = await axios.get<Record<string, Unit>>(CODE_STATIONS_JSON_PATH, { signal });
// 根据国标编码的前6位匹配minio中的编码表
const unitCode = gbCode.slice(0, 6);
@@ -92,7 +95,7 @@ const installationArea = computedAsync(async (onCancel) => {
if (!jsonPath) return UNKNOWN_NAME;
// 获取1级区域
const { data: areaCodes } = await axios.get<Area[]>(jsonPath, { signal: abortController.signal });
const { data: areaCodes } = await axios.get<Area[]>(jsonPath, { signal });
const tier1Area = areaCodes.find((area) => area.code === tier1AreaCode);
if (!tier1Area) return UNKNOWN_NAME;
@@ -102,6 +105,12 @@ const installationArea = computedAsync(async (onCancel) => {
// 拼接1级和2级区域名称
return `${tier1Area.name}-${tier2Area.name}`;
},
});
watch(offlineDev, (offline) => {
if (offline) {
queryClient.cancelQueries({ queryKey: [QUERY_KEY] });
}
});
const commonInfo = computed(() => {

View File

@@ -120,9 +120,6 @@ onBeforeUnmount(() => {
<NFormItem label-placement="left" label="设备描述">
<NInput v-model:value="localDevice.description" />
</NFormItem>
<NFormItem label-placement="left" label="上游设备">
<NInput v-model:value="localDevice.linkDescription" />
</NFormItem>
</NForm>
</template>
<template #action>

View File

@@ -141,9 +141,6 @@ onBeforeUnmount(() => {
<NFormItem label-placement="left" label="设备描述">
<NInput v-model:value="localDevice.description" />
</NFormItem>
<NFormItem label-placement="left" label="上游设备">
<NInput v-model:value="localDevice.linkDescription" />
</NFormItem>
</NForm>
</template>
<template #action>

View File

@@ -129,9 +129,6 @@ onBeforeUnmount(() => {
<NFormItem label-placement="left" label="设备描述">
<NInput v-model:value="localDevice.description" />
</NFormItem>
<NFormItem label-placement="left" label="上游设备">
<NInput v-model:value="localDevice.linkDescription" />
</NFormItem>
</NForm>
</template>
<template #action>

View File

@@ -141,9 +141,6 @@ onBeforeUnmount(() => {
<NFormItem label-placement="left" label="设备描述">
<NInput v-model:value="localDevice.description" />
</NFormItem>
<NFormItem label-placement="left" label="上游设备">
<NInput v-model:value="localDevice.linkDescription" />
</NFormItem>
</NForm>
</template>
<template #action>

View File

@@ -132,9 +132,6 @@ onBeforeUnmount(() => {
<NFormItem label-placement="left" label="设备描述">
<NInput v-model:value="localDevice.description" />
</NFormItem>
<NFormItem label-placement="left" label="上游设备">
<NInput v-model:value="localDevice.linkDescription" />
</NFormItem>
</NForm>
</template>
<template #action>

View File

@@ -2,10 +2,10 @@
import { isMediaServerAliveApi, isSipServerAliveApi, type NdmServerResultVO, type Station } from '@/apis';
import { DEVICE_TYPE_LITERALS, tryGetDeviceType } from '@/enums';
import { useSettingStore } from '@/stores';
import { useQuery } from '@tanstack/vue-query';
import { useQuery, useQueryClient } from '@tanstack/vue-query';
import { NCard, NTag } from 'naive-ui';
import { storeToRefs } from 'pinia';
import { computed, toRefs } from 'vue';
import { computed, toRefs, watch } from 'vue';
const props = defineProps<{
ndmDevice: NdmServerResultVO;
@@ -15,26 +15,38 @@ const props = defineProps<{
const settingStore = useSettingStore();
const { offlineDev } = storeToRefs(settingStore);
const queryClient = useQueryClient();
const { ndmDevice, station } = toRefs(props);
const deviceType = computed(() => tryGetDeviceType(ndmDevice.value.deviceType));
const MEDIA_SERVER_ALIVE_QUERY_KEY = 'media-server-alive-query';
const VIDEO_SERVER_ALIVE_QUERY_KEY = 'video-server-alive-query';
const { data: isMediaServerAlive } = useQuery({
queryKey: computed(() => ['media-server-alive-query', ndmDevice.value.id, ndmDevice.value.lastDiagTime]),
queryKey: computed(() => [MEDIA_SERVER_ALIVE_QUERY_KEY, ndmDevice.value.id, ndmDevice.value.lastDiagTime]),
enabled: computed(() => !offlineDev.value && deviceType.value === DEVICE_TYPE_LITERALS.ndmMediaServer),
refetchInterval: 30 * 1000,
gcTime: 0,
queryFn: async ({ signal }) => {
const alives = await isMediaServerAliveApi({ stationCode: station.value.code, signal });
return alives.find((alive) => alive.ip === ndmDevice.value.ipAddress);
},
refetchInterval: 30 * 1000,
});
const { data: isSipServerAlive } = useQuery({
queryKey: computed(() => ['video-server-alive-query', ndmDevice.value.id, ndmDevice.value.lastDiagTime]),
queryKey: computed(() => [VIDEO_SERVER_ALIVE_QUERY_KEY, ndmDevice.value.id, ndmDevice.value.lastDiagTime]),
enabled: computed(() => !offlineDev.value && deviceType.value === DEVICE_TYPE_LITERALS.ndmVideoServer),
refetchInterval: 30 * 1000,
gcTime: 0,
queryFn: async ({ signal }) => {
return await isSipServerAliveApi({ stationCode: station.value.code, signal });
},
refetchInterval: 30 * 1000,
});
watch(offlineDev, (offline) => {
if (offline) {
queryClient.cancelQueries({ queryKey: [MEDIA_SERVER_ALIVE_QUERY_KEY] });
queryClient.cancelQueries({ queryKey: [VIDEO_SERVER_ALIVE_QUERY_KEY] });
}
});
</script>
@@ -46,14 +58,14 @@ const { data: isSipServerAlive } = useQuery({
<template #default>
<div v-if="deviceType === DEVICE_TYPE_LITERALS.ndmMediaServer">
<span>流媒体服务状态</span>
<template v-if="isMediaServerAlive">
<template v-if="!!isMediaServerAlive">
<NTag size="small" :type="isMediaServerAlive.online ? 'success' : 'error'">{{ isMediaServerAlive.online ? '在线' : '离线' }}</NTag>
</template>
<span v-else>-</span>
</div>
<div v-if="deviceType === DEVICE_TYPE_LITERALS.ndmVideoServer">
<span>信令服务状态</span>
<template v-if="isSipServerAlive">
<template v-if="isSipServerAlive !== undefined">
<NTag size="small" :type="isSipServerAlive ? 'success' : 'error'">{{ isSipServerAlive ? '在线' : '离线' }}</NTag>
</template>
<span v-else>-</span>

View File

@@ -1,16 +1,5 @@
<script setup lang="ts">
import {
detailMediaServerApi,
detailVideoServerApi,
icmpEntityByDeviceId,
updateMediaServerApi,
updateVideoServerApi,
type NdmMediaServerUpdateVO,
type NdmServerResultVO,
type NdmServerUpdateVO,
type NdmVideoServerUpdateVO,
type Station,
} from '@/apis';
import { detailMediaServerApi, detailVideoServerApi, icmpEntityByDeviceId, updateMediaServerApi, updateVideoServerApi, type NdmServerResultVO, type NdmServerUpdateVO, type Station } from '@/apis';
import { DEVICE_TYPE_LITERALS, tryGetDeviceType } from '@/enums';
import { useDeviceStore } from '@/stores';
import { parseErrorFeedback } from '@/utils';
@@ -151,9 +140,6 @@ onBeforeUnmount(() => {
<NFormItem label-placement="left" label="设备描述">
<NInput v-model:value="localDevice.description" />
</NFormItem>
<NFormItem label-placement="left" label="上游设备">
<NInput v-model:value="localDevice.linkDescription" />
</NFormItem>
</NForm>
</template>
<template #action>

View File

@@ -141,9 +141,6 @@ onBeforeUnmount(() => {
<NFormItem label-placement="left" label="设备描述">
<NInput v-model:value="localDevice.description" />
</NFormItem>
<NFormItem label-placement="left" label="上游设备">
<NInput v-model:value="localDevice.linkDescription" />
</NFormItem>
</NForm>
</template>
<template #action>

View File

@@ -439,7 +439,7 @@ const onLocateDeviceTree = async () => {
activeTab.value = deviceType;
// 展开选择的车站
expandedKeys.value = [selectedStationCode.value];
expandedKeys.value.push(selectedStationCode.value);
// 当选择录像机时,如果不是集群,进一步展开该录像机所在的集群节点
if (deviceType === DEVICE_TYPE_LITERALS.ndmNvr) {

View File

@@ -48,8 +48,8 @@ export const useDeviceManagement = () => {
downloadByData(data, `${stationName}_${deviceTypeName}列表_${time}.xlsx`);
},
onError: (error) => {
if (isCancel(error)) return;
window.$loadingBar.error();
if (isCancel(error)) return;
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
@@ -84,8 +84,8 @@ export const useDeviceManagement = () => {
downloadByData(data, `${stationName}_${deviceTypeName}导入模板_${time}.xlsx`);
},
onError: (error) => {
if (isCancel(error)) return;
window.$loadingBar.error();
if (isCancel(error)) return;
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
@@ -137,8 +137,8 @@ export const useDeviceManagement = () => {
}
},
onError: (error) => {
if (isCancel(error)) return;
window.$loadingBar.error();
if (isCancel(error)) return;
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
@@ -163,8 +163,8 @@ export const useDeviceManagement = () => {
}
},
onError: (error) => {
if (isCancel(error)) return;
window.$loadingBar.error();
if (isCancel(error)) return;
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);

View File

@@ -1,6 +1,7 @@
<script setup lang="ts">
import { SettingsDrawer, SyncCameraResultModal } from '@/components';
import { useLineStationsQuery, useStompClient, useVerifyUserQuery } from '@/composables';
import { LINE_ALARMS_QUERY_KEY, LINE_DEVICES_QUERY_KEY, LINE_STATIONS_MUTATION_KEY, LINE_STATIONS_QUERY_KEY, STATION_ALARMS_MUTATION_KEY, STATION_DEVICES_MUTATION_KEY } from '@/constants';
import { useAlarmStore, useSettingStore, useUserStore } from '@/stores';
import { parseErrorFeedback } from '@/utils';
import { useIsFetching, useIsMutating, useMutation } from '@tanstack/vue-query';
@@ -43,10 +44,22 @@ const { syncCameraResult, afterCheckSyncCameraResult } = useStompClient();
useVerifyUserQuery();
useLineStationsQuery();
// 带key的query和mutation用于全局轮询 可用于渲染loading状态
const fetchingCount = useIsFetching({ predicate: (query) => !!query.options.queryKey });
const mutatingCount = useIsMutating({ predicate: (mutation) => !!mutation.options.mutationKey });
const loadingCount = computed(() => fetchingCount.value + mutatingCount.value);
// 全局loading状态依赖于轮询query的queryKey以及相关的mutationKey
const queryingCount = useIsFetching({
predicate: (query) => {
const pollingKeys = [LINE_STATIONS_QUERY_KEY, LINE_DEVICES_QUERY_KEY, LINE_ALARMS_QUERY_KEY];
const queryKey = query.options.queryKey;
return !!queryKey && Array.isArray(queryKey) && pollingKeys.some((key) => queryKey.includes(key));
},
});
const mutatingCount = useIsMutating({
predicate: (mutation) => {
const mutationKeys = [LINE_STATIONS_MUTATION_KEY, STATION_DEVICES_MUTATION_KEY, STATION_ALARMS_MUTATION_KEY];
const mutationKey = mutation.options.mutationKey;
return !!mutationKey && Array.isArray(mutationKey) && mutationKeys.some((key) => mutationKey.includes(key));
},
});
const appLoading = computed(() => queryingCount.value + mutatingCount.value > 0);
const onToggleMenuCollapsed = () => {
menuCollpased.value = !menuCollpased.value;
@@ -169,7 +182,7 @@ function renderIcon(icon: Component): () => VNode {
<NFlex justify="space-between" align="center" :size="8" style="width: 100%; height: 100%">
<NFlex align="center">
<h3 style="margin: 0 0 0 16px; cursor: pointer" @click="routeToRoot">网络设备管理平台</h3>
<NButton text size="tiny" :loading="loadingCount > 0"></NButton>
<NButton text size="tiny" :loading="appLoading"></NButton>
</NFlex>
<NFlex align="center" :size="0" style="height: 100%">
<NDropdown trigger="hover" show-arrow :options="dropdownOptions" @select="onSelectDropdownOption">

View File

@@ -0,0 +1,5 @@
import type { InjectionKey } from 'vue';
export const createInjectionKey = <T>(key: string): InjectionKey<T> => {
return Symbol(key);
};

View File

@@ -1,4 +1,5 @@
export * from './cipher';
export * from './create-injection-key';
export * from './download';
export * from './env';
export * from './format-duration';