Compare commits

..

10 Commits

Author SHA1 Message Date
yangsy
4c231bd438 feat: 摄像机卡片添加摄像机类型和安装区域 2025-12-22 16:39:01 +08:00
yangsy
547ac67af6 feat: 设备header卡片添加 append-info 插槽以支持自定义底部内容 2025-12-22 15:17:51 +08:00
yangsy
f624cd0cdb fix: 修复指向minio的请求未添加时间戳参数的问题 2025-12-22 14:37:03 +08:00
yangsy
ff35fda046 feat: 调用新的设备告警日志导出接口 2025-12-19 14:19:50 +08:00
yangsy
b238af2c77 feat: 调整服务器运行时间 label 文案 2025-12-19 12:50:07 +08:00
yangsy
5b989fab0f feat: DeviceHardwareCard组件添加自定义标签属性 2025-12-19 12:46:27 +08:00
yangsy
24a7881b94 fix: 视频平台日志页面补全遗漏的操作类型字段 2025-12-19 12:44:39 +08:00
yangsy
ee019c44a0 style 2025-12-19 11:14:51 +08:00
yangsy
fbe79c4dfc chore: vite代理配置 2025-12-19 11:14:51 +08:00
yangsy
eb0ee841cf feat: 细化设备树自动定位的触发条件
- 添加 `hasFromPage` 属性,辅助区分选择设备的来源是用户操作还是路由参数
2025-12-19 11:14:51 +08:00
11 changed files with 177 additions and 21 deletions

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import { useVersionCheckQuery } from './composables';
import { GlobalFeedback } from '@/components';
import { useVersionCheckQuery } from '@/composables';
import { useSettingStore } from '@/stores';
import { VueQueryDevtools } from '@tanstack/vue-query-devtools';
import { dateZhCN, NConfigProvider, NDialogProvider, NLoadingBarProvider, NMessageProvider, NNotificationProvider, zhCN } from 'naive-ui';

View File

@@ -25,7 +25,7 @@ export const exportDeviceAlarmLogApi = async (pageQuery: PageParams<NdmDeviceAla
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmDeviceAlarmLog/defaultExportByTemplate`;
const endpoint = `${prefix}/api/ndm/ndmDeviceAlarmLog/exportByTemplateV2`;
const resp = await client.post<BlobPart>(endpoint, pageQuery, { responseType: 'blob', retRaw: true, signal });
const data = unwrapResponse(resp);
return data;

View File

@@ -8,12 +8,16 @@ const props = defineProps<{
memUsage?: string;
diskUsage?: string;
runningTime?: string;
cpuUsageLabel?: string;
memUsageLabel?: string;
diskUsageLabel?: string;
runningTimeLabel?: string;
}>();
const { cpuUsage, memUsage, diskUsage, runningTime } = toRefs(props);
const { cpuUsage, memUsage, diskUsage, runningTime, cpuUsageLabel, memUsageLabel, diskUsageLabel, runningTimeLabel } = toRefs(props);
const showCard = computed(() => {
return Object.values(props).some((value) => !!value);
return Object.values({ cpuUsage, memUsage, diskUsage, runningTime }).some((value) => !!value);
});
const cpuPercent = computed(() => {
@@ -51,22 +55,22 @@ const getProgressStatus = (percent: number): ProgressStatus => {
<NFlex vertical>
<NFlex v-if="cpuUsage" style="width: 100%" align="center" :wrap="false">
<NIcon :component="FireOutlined" />
<span style="word-break: keep-all">CPU</span>
<span style="word-break: keep-all">{{ cpuUsageLabel || 'CPU' }}</span>
<NProgress :percentage="cpuPercent" :status="getProgressStatus(cpuPercent)">{{ cpuPercent }}%</NProgress>
</NFlex>
<NFlex v-if="memUsage" style="width: 100%" align="center" :wrap="false">
<NIcon :component="CodeOutlined" />
<span style="word-break: keep-all">内存</span>
<span style="word-break: keep-all">{{ memUsageLabel || '内存' }}</span>
<NProgress :percentage="memPercent" :status="getProgressStatus(memPercent)">{{ memPercent }}%</NProgress>
</NFlex>
<NFlex v-if="diskUsage" style="width: 100%" align="center" :wrap="false">
<NIcon :component="SaveOutlined" />
<span style="word-break: keep-all">磁盘</span>
<span style="word-break: keep-all">{{ diskUsageLabel || '磁盘' }}</span>
<NProgress :percentage="diskPercent" :status="getProgressStatus(diskPercent)">{{ diskPercent }}%</NProgress>
</NFlex>
<NFlex v-if="runningTime" style="width: 100%" align="center" :wrap="false">
<NIcon :component="ClockCircleOutlined" />
<span>系统运行时间</span>
<span>{{ runningTimeLabel || '运行时间' }}</span>
<span>{{ formattedRunningTime }}</span>
</NFlex>
</NFlex>

View File

@@ -14,6 +14,10 @@ const props = defineProps<{
station: Station;
}>();
defineSlots<{
'append-info': () => any;
}>();
const { ndmDevice, station } = toRefs(props);
const type = computed(() => {
@@ -150,6 +154,9 @@ onBeforeUnmount(() => {
</div>
</div>
</template>
<template #footer>
<slot name="append-info"></slot>
</template>
</NCard>
</template>

View File

@@ -1,7 +1,28 @@
<script lang="ts">
const CAMERA_TYPES = {
'001': '模拟彩色云台摄像机',
'002': '模拟彩色半球摄像机',
'003': '模拟彩色固定摄像机',
'004': '数字高清云台摄像机',
'005': '数字高清半球摄像机',
'006': '数字高清固定摄像机',
} as const;
type CameraType = keyof typeof CAMERA_TYPES;
const isCameraTypeCode = (code: string): code is CameraType => {
return code in CAMERA_TYPES;
};
</script>
<script setup lang="ts">
import type { NdmCameraResultVO, Station } from '@/apis';
import { DeviceCommonCard, DeviceHeaderCard } from '@/components';
import { NFlex } from 'naive-ui';
import { useSettingStore } from '@/stores';
import { computedAsync } from '@vueuse/core';
import axios from 'axios';
import { NDescriptions, NDescriptionsItem, NFlex } from 'naive-ui';
import { storeToRefs } from 'pinia';
import { computed, toRefs } from 'vue';
const props = defineProps<{
@@ -9,8 +30,80 @@ const props = defineProps<{
station: Station;
}>();
const settingStore = useSettingStore();
const { offlineDev } = storeToRefs(settingStore);
const { ndmDevice, station } = toRefs(props);
const cameraType = computed(() => {
const gbCode = ndmDevice.value.gbCode;
if (!gbCode) return '-';
const cameraTypeCode = gbCode.slice(11, 14);
if (!isCameraTypeCode(cameraTypeCode)) return '-';
return CAMERA_TYPES[cameraTypeCode];
});
const installationArea = computedAsync(async (onCancel) => {
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;
const MINIO_PREFIX = `/minio`;
const CDN_VIMP_CODES_PREFIX = `${MINIO_PREFIX}/cdn/vimp/codes`;
const CODE_STATIONS_JSON_PATH = `${CDN_VIMP_CODES_PREFIX}/codeStations.json`;
const CODE_STATION_AREAS_JSON_PATH = `${CDN_VIMP_CODES_PREFIX}/codeStationAreas.json`;
const CODE_PARKING_AREAS_JSON_PATH = `${CDN_VIMP_CODES_PREFIX}/codeParkingAreas.json`;
const CODE_OCC_AREAS_JSON_PATH = `${CDN_VIMP_CODES_PREFIX}/codeOccAreas.json`;
// minio中的编码表结构
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 });
// 根据国标编码的前6位匹配minio中的编码表
const unitCode = gbCode.slice(0, 6);
const unit = unitCodes[unitCode];
if (!unit) return UNKNOWN_NAME;
// 获取编码表中的线路/单位类型
const unitType = unit.type;
// 国标编码的第7位到第8位为1级区域编码
const tier1AreaCode = gbCode.slice(6, 8);
// 国标编码的第9位到第11位为2级区域编码
const tier2AreaCode = gbCode.slice(8, 11);
if (unitType === 'train') {
return unit.name;
}
const areaJsonPaths: Record<string, string> = {
station: CODE_STATION_AREAS_JSON_PATH,
parking: CODE_PARKING_AREAS_JSON_PATH,
occ: CODE_OCC_AREAS_JSON_PATH,
};
const jsonPath = areaJsonPaths[unitType];
if (!jsonPath) return UNKNOWN_NAME;
// 获取1级区域
const { data: areaCodes } = await axios.get<Area[]>(jsonPath, { signal: abortController.signal });
const tier1Area = areaCodes.find((area) => area.code === tier1AreaCode);
if (!tier1Area) return UNKNOWN_NAME;
// 获取2级区域
const tier2Area = tier1Area.subs.find((area) => area.code === `${tier1AreaCode}${tier2AreaCode}`);
if (!tier2Area) return UNKNOWN_NAME;
// 拼接1级和2级区域名称
return `${tier1Area.name}-${tier2Area.name}`;
});
const commonInfo = computed(() => {
const {
createdTime,
@@ -44,7 +137,14 @@ const commonInfo = computed(() => {
<template>
<NFlex vertical>
<DeviceHeaderCard :ndm-device="ndmDevice" :station="station" />
<DeviceHeaderCard :ndm-device="ndmDevice" :station="station">
<template #append-info>
<NDescriptions bordered size="small" label-placement="left" :columns="1" style="width: 60%; min-width: 400px">
<NDescriptionsItem label="摄像机类型" :span="1">{{ cameraType }}</NDescriptionsItem>
<NDescriptionsItem label="安装区域" :span="1">{{ installationArea ?? '-' }}</NDescriptionsItem>
</NDescriptions>
</template>
</DeviceHeaderCard>
<DeviceCommonCard :common-info="commonInfo" />
</NFlex>
</template>

View File

@@ -28,7 +28,7 @@ const runningTime = computed(() => lastDiagInfo.value?.commInfo?.系统运行时
<template>
<NFlex vertical>
<DeviceHeaderCard :ndm-device="ndmDevice" :station="station" />
<DeviceHardwareCard :cpu-usage="cpuUsage" :mem-usage="memUsage" :disk-usage="diskUsage" :running-time="runningTime" />
<DeviceHardwareCard running-time-label="服务器运行时间" :cpu-usage="cpuUsage" :mem-usage="memUsage" :disk-usage="diskUsage" :running-time="runningTime" />
<ServerAlive :ndm-device="ndmDevice" :station="station" />
</NFlex>
</template>

View File

@@ -46,6 +46,7 @@ const {
selectedStationCode,
selectedDeviceType,
selectedDevice,
hasFromPage,
selectDevice,
routeDevice,
// 设备管理
@@ -460,12 +461,15 @@ const onLocateDeviceTree = async () => {
animated.value = true;
};
// 渲染全线设备树时,若是首次选择设备,则定位设备树
const unwatch = watch(selectedDevice, async (newDevice, oldDevice) => {
// 渲染全线设备树时,若是首次经过路由跳转而来选择设备,则定位设备树
const unwatchLocate = watch(selectedDevice, async (newDevice, oldDevice) => {
if (!!station.value) return;
if (!!newDevice && !oldDevice && !!deviceTreeInst.value) {
await onLocateDeviceTree();
unwatch();
if (!hasFromPage.value) return;
if (!!newDevice && !oldDevice) {
if (!!deviceTreeInst.value) {
await onLocateDeviceTree();
unwatchLocate();
}
}
});
</script>

View File

@@ -3,6 +3,7 @@ import type { Station, StationAlarms, StationDevices } from '@/apis';
import { DEVICE_TYPE_LITERALS } from '@/enums';
import { EllipsisOutlined, MoreOutlined } from '@vicons/antd';
import axios from 'axios';
import dayjs from 'dayjs';
import { isFunction } from 'es-toolkit';
import { NButton, NCard, NCheckbox, NDropdown, NFlex, NIcon, NTag, NTooltip, useThemeVars, type DropdownOption } from 'naive-ui';
import { computed, toRefs } from 'vue';
@@ -49,7 +50,7 @@ const alarmCount = computed(() => {
const openVideoPlatform = async () => {
try {
const response = await axios.get<Record<string, string>>('/minio/ndm/ndm-vimps.json');
const response = await axios.get<Record<string, string>>(`/minio/ndm/ndm-vimps.json?_t=${dayjs().unix()}`);
const url = response.data[station.value.code];
if (!url) {
window.$message.warning(`未找到车站编码 ${station.value.code} 对应的视频平台URL`);

View File

@@ -3,7 +3,7 @@ import { tryGetDeviceType, type DeviceType } from '@/enums';
import { useDeviceStore } from '@/stores';
import { watchDebounced } from '@vueuse/core';
import { storeToRefs } from 'pinia';
import { onMounted, ref, watch } from 'vue';
import { computed, onMounted, ref, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
export const useDeviceSelection = () => {
@@ -17,6 +17,8 @@ export const useDeviceSelection = () => {
const selectedDeviceType = ref<DeviceType>();
const selectedDevice = ref<NdmDeviceResultVO>();
const hasFromPage = computed(() => !!route.query['fromPage']);
const initFromRoute = (lineDevices: LineDevices) => {
const { stationCode, deviceType, deviceDbId } = route.query;
if (stationCode) {
@@ -105,6 +107,8 @@ export const useDeviceSelection = () => {
selectedDeviceType,
selectedDevice,
hasFromPage,
initFromRoute,
selectDevice,
routeDevice,

View File

@@ -1,3 +1,32 @@
<script lang="ts">
const vimpOperationTypeOptions: SelectOption[] = [
{ label: '视频点播', value: 10001 },
{ label: '视频回放', value: 10002 },
// { label: '停止视频回放', value: 10003 },
// { label: '回放暂停', value: 10004 },
// { label: '回放恢复', value: 10005 },
// { label: '回放倍速播放', value: 10006 },
// { label: '回放拖动播放', value: 10007 },
{ label: '云台指令', value: 10008 },
{ label: '根据国标码查录像', value: 10009 },
{ label: '下载录像', value: 10010 },
{ label: '停止下载录像', value: 10011 },
{ label: '获取预置位列表', value: 10012 },
{ label: '设置本平台分屏数量', value: 20001 },
{ label: '设置本平台monitor播放的摄像机rtsp流', value: 20002 },
{ label: '停止本平台monitor播放的摄像机rtsp流', value: 20003 },
{ label: '启动非报警时序', value: 30001 },
{ label: '停止非报警时序', value: 30002 },
{ label: '暂停非报警时序', value: 30003 },
{ label: '调用组切', value: 30004 },
{ label: '启动报警时序', value: 40001 },
{ label: '停止报警时序', value: 40002 },
{ label: '分页查询报警', value: 50001 },
{ label: '确认报警', value: 50002 },
{ label: '删除报警', value: 50004 },
];
</script>
<script setup lang="ts">
import { exportVimpLogApi, pageVimpLogApi, type NdmVimpLog, type NdmVimpLogResultVO, type PageQueryExtra, type Station } from '@/apis';
import { useStationStore } from '@/stores';
@@ -28,6 +57,7 @@ import { computed, h, reactive, ref, watch, watchEffect } from 'vue';
interface SearchFields extends PageQueryExtra<NdmVimpLog> {
stationCode?: Station['code'];
logType_in: number[];
createdTime: [string, string];
}
@@ -43,11 +73,13 @@ const stationSelectOptions = computed(() => {
});
const searchFields = ref<SearchFields>({
logType_in: [],
createdTime: [dayjs().startOf('date').subtract(1, 'week').format('YYYY-MM-DD HH:mm:ss'), dayjs().endOf('date').format('YYYY-MM-DD HH:mm:ss')] as [string, string],
});
const resetSearchFields = () => {
searchFields.value = {
stationCode: onlineStations.value.at(0)?.code,
logType_in: [],
createdTime: [dayjs().startOf('date').subtract(1, 'week').format('YYYY-MM-DD HH:mm:ss'), dayjs().endOf('date').format('YYYY-MM-DD HH:mm:ss')] as [string, string],
};
};
@@ -266,6 +298,9 @@ watch(
clearable
/>
</NFormItemGi>
<NFormItemGi :span="1" label="操作类型" label-placement="left">
<NSelect v-model:value="searchFields.logType_in" :options="vimpOperationTypeOptions" multiple clearable />
</NFormItemGi>
<NFormItemGi span="1" label="时间" label-placement="left">
<NDatePicker v-model:formatted-value="searchFields.createdTime" type="datetimerange" />
</NFormItemGi>

View File

@@ -105,12 +105,13 @@ const line10ApiProxyList: ProxyItem[] = [
const apiProxyList: ProxyItem[] = [
// { key: '/minio', target: 'http://10.15.128.10:9000', rewrite: ['/minio', ''] },
// { key: '/api', target: 'http://10.15.128.10:18760' },
// { key: '/ws', target: 'ws://10.15.128.10:18103', ws: true },
...line04ApiProxyList,
{ key: '/minio', target: 'http://10.18.128.10:9000', rewrite: ['/minio', ''] },
{ key: '/api', target: 'http://10.18.128.10:18760' },
...line04ApiProxyList,
...line10ApiProxyList,
// { key: '/ws', target: 'ws://10.15.128.10:18103', ws: true },
{ key: '/ws', target: 'ws://10.18.128.10:18103', ws: true },
...line10ApiProxyList,
];
// https://vite.dev/config/