refactor: 重构项目结构
- 优化 `车站-设备-告警` 轮询机制 - 改进设备卡片的布局 - 支持修改设备 - 告警轮询中获取完整告警数据 - 车站告警详情支持导出完整的 `今日告警列表` - 支持将状态持久化到 `IndexedDB` - 新增轮询控制 (调试模式) - 新增离线开发模式 (调试模式) - 新增 `IndexedDB` 数据控制 (调试模式)
This commit is contained in:
305
src/pages/alarm-page.vue
Normal file
305
src/pages/alarm-page.vue
Normal file
@@ -0,0 +1,305 @@
|
||||
<script setup lang="ts">
|
||||
import { exportDeviceAlarmLogApi, pageDeviceAlarmLogApi, type NdmDeviceAlarmLog, type NdmDeviceAlarmLogResultVO, type PageQueryExtra, type Station } from '@/apis';
|
||||
import { useAlarmActionColumn, useCameraSnapColumn } from '@/composables';
|
||||
import { ALARM_TYPES, DEVICE_TYPE_CODES, DEVICE_TYPE_LITERALS, DEVICE_TYPE_NAMES, FAULT_LEVELS, type DeviceType } from '@/enums';
|
||||
import { renderAlarmDateCell, renderAlarmTypeCell, renderDeviceTypeCell, renderFaultLevelCell } from '@/helpers';
|
||||
import { useAlarmStore, useStationStore } from '@/stores';
|
||||
import { downloadByData, parseErrorFeedback } from '@/utils';
|
||||
import { useMutation } from '@tanstack/vue-query';
|
||||
import { watchDebounced } from '@vueuse/core';
|
||||
import dayjs from 'dayjs';
|
||||
import {
|
||||
NButton,
|
||||
NDataTable,
|
||||
NDatePicker,
|
||||
NFlex,
|
||||
NForm,
|
||||
NFormItemGi,
|
||||
NGi,
|
||||
NGrid,
|
||||
NInput,
|
||||
NSelect,
|
||||
NSwitch,
|
||||
type DataTableColumns,
|
||||
type DataTableRowData,
|
||||
type PaginationProps,
|
||||
type SelectOption,
|
||||
} from 'naive-ui';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { computed, h, onBeforeMount, reactive, ref, watch } from 'vue';
|
||||
|
||||
interface SearchFields extends PageQueryExtra<NdmDeviceAlarmLog> {
|
||||
stationCode_in: Station['code'][];
|
||||
deviceType_in: string[];
|
||||
deviceName_like: string;
|
||||
alarmType_in: string[];
|
||||
faultLevel_in: string[];
|
||||
alarmDate: [number, number];
|
||||
}
|
||||
|
||||
const stationStore = useStationStore();
|
||||
const { stations } = storeToRefs(stationStore);
|
||||
const alarmStore = useAlarmStore();
|
||||
const { unreadAlarmCount } = storeToRefs(alarmStore);
|
||||
|
||||
const stationSelectOptions = computed<SelectOption[]>(() => {
|
||||
return stations.value.map((station) => ({
|
||||
label: station.name,
|
||||
value: station.code,
|
||||
}));
|
||||
});
|
||||
const deviceTypeSelectOptions = computed<SelectOption[]>(() => {
|
||||
return Object.values(DEVICE_TYPE_LITERALS).map<SelectOption>((deviceType) => ({
|
||||
label: DEVICE_TYPE_NAMES[deviceType],
|
||||
value: deviceType,
|
||||
}));
|
||||
});
|
||||
const alarmTypeSelectOptions = computed<SelectOption[]>(() => {
|
||||
return Object.keys(ALARM_TYPES).map<SelectOption>((alarmType) => ({
|
||||
label: ALARM_TYPES[alarmType],
|
||||
value: alarmType,
|
||||
}));
|
||||
});
|
||||
const faultLevelSelectOptions = computed<SelectOption[]>(() => {
|
||||
return Object.keys(FAULT_LEVELS).map<SelectOption>((faultLevel) => ({
|
||||
label: FAULT_LEVELS[faultLevel],
|
||||
value: faultLevel,
|
||||
}));
|
||||
});
|
||||
|
||||
// 未读告警数量被清零时,代表从别的页面跳转过来,需要刷新告警表格数据
|
||||
const unreadCountCleared = computed(() => unreadAlarmCount.value === 0);
|
||||
watch(unreadCountCleared, (newValue, oldValue) => {
|
||||
if (!oldValue && newValue) {
|
||||
// 不排除是在告警页面内点击了告警徽标按钮,因此要重置查询条件再执行查询
|
||||
onClickReset();
|
||||
}
|
||||
});
|
||||
|
||||
const realtimeRefresh = ref(false);
|
||||
watchDebounced(
|
||||
[unreadAlarmCount, realtimeRefresh],
|
||||
([count, refresh]) => {
|
||||
if (count > 0 && refresh) {
|
||||
getTableData();
|
||||
}
|
||||
},
|
||||
{
|
||||
debounce: 500,
|
||||
},
|
||||
);
|
||||
|
||||
const searchFields = ref<SearchFields>({
|
||||
stationCode_in: [],
|
||||
deviceType_in: [],
|
||||
deviceName_like: '',
|
||||
alarmType_in: [],
|
||||
faultLevel_in: [],
|
||||
alarmDate: [dayjs().startOf('date').valueOf(), dayjs().endOf('date').valueOf()],
|
||||
});
|
||||
const resetSearchFields = () => {
|
||||
searchFields.value = {
|
||||
stationCode_in: [],
|
||||
deviceType_in: [],
|
||||
deviceName_like: '',
|
||||
alarmType_in: [],
|
||||
faultLevel_in: [],
|
||||
alarmDate: [dayjs().startOf('date').valueOf(), dayjs().endOf('date').valueOf()],
|
||||
};
|
||||
};
|
||||
const getExtraFields = (): PageQueryExtra<NdmDeviceAlarmLog> => {
|
||||
const stationCodeIn = searchFields.value.stationCode_in;
|
||||
const deviceTypeIn = searchFields.value.deviceType_in.flatMap((deviceType) => DEVICE_TYPE_CODES[deviceType as DeviceType]);
|
||||
const alarmDateGe = searchFields.value.alarmDate[0];
|
||||
const alarmDateLe = searchFields.value.alarmDate[1];
|
||||
return {
|
||||
stationCode_in: stationCodeIn ? (stationCodeIn.length > 0 ? [...stationCodeIn] : undefined) : undefined,
|
||||
deviceType_in: deviceTypeIn ? (deviceTypeIn.length > 0 ? [...deviceTypeIn] : undefined) : undefined,
|
||||
deviceName_like: !!searchFields.value.deviceName_like ? searchFields.value.deviceName_like : undefined,
|
||||
alarmType_in: searchFields.value.alarmType_in.length > 0 ? [...searchFields.value.alarmType_in] : undefined,
|
||||
faultLevel_in: searchFields.value.faultLevel_in.length > 0 ? [...searchFields.value.faultLevel_in] : undefined,
|
||||
alarmDate_ge: alarmDateGe,
|
||||
alarmDate_le: alarmDateLe,
|
||||
};
|
||||
};
|
||||
|
||||
const searchFieldsChanged = ref(false);
|
||||
watch(searchFields, () => {
|
||||
searchFieldsChanged.value = true;
|
||||
});
|
||||
|
||||
const tableData = ref<DataTableRowData[]>([]);
|
||||
|
||||
const { cameraSnapColumn } = useCameraSnapColumn(tableData);
|
||||
const { alarmActionColumn } = useAlarmActionColumn(tableData);
|
||||
const tableColumns: DataTableColumns<NdmDeviceAlarmLogResultVO & { snapUrl?: string }> = [
|
||||
// { title: '设备ID', key: 'deviceId' },
|
||||
// { title: '故障编码', key: 'faultCode', align: 'center' },
|
||||
// { title: '故障位置', key: 'faultLocation' },
|
||||
{ title: '告警流水号', key: 'alarmNo' },
|
||||
{ title: '告警时间', key: 'alarmDate', render: renderAlarmDateCell },
|
||||
{ title: '车站', key: 'stationName', render: (rowData) => stations.value.find((station) => station.code === rowData.stationCode)?.name ?? '-' },
|
||||
{ title: '设备类型', key: 'deviceType', render: renderDeviceTypeCell },
|
||||
{ title: '设备名称', key: 'deviceName' },
|
||||
{ title: '告警类型', key: 'alarmType', align: 'center', render: renderAlarmTypeCell },
|
||||
{ title: '故障级别', key: 'faultLevel', align: 'center', render: renderFaultLevelCell },
|
||||
{ title: '故障描述', key: 'faultDescription' },
|
||||
{ title: '修复建议', key: 'alarmRepairSuggestion' },
|
||||
{ title: '是否恢复', key: 'alarmCategory', align: 'center', render: (rowData) => (rowData.alarmCategory === '2' ? '是' : '否') },
|
||||
{ title: '恢复时间', key: 'updatedTime' },
|
||||
cameraSnapColumn,
|
||||
alarmActionColumn,
|
||||
];
|
||||
|
||||
const DEFAULT_PAGE_SIZE = 10;
|
||||
const pagination = reactive<PaginationProps>({
|
||||
showSizePicker: true,
|
||||
page: 1,
|
||||
pageSize: DEFAULT_PAGE_SIZE,
|
||||
pageSizes: [5, 10, 20, 50, 80, 100],
|
||||
itemCount: 0,
|
||||
prefix: ({ itemCount }) => {
|
||||
return h('div', {}, { default: () => `共${itemCount}条` });
|
||||
},
|
||||
onUpdatePage: (page: number) => {
|
||||
pagination.page = page;
|
||||
getTableData();
|
||||
},
|
||||
onUpdatePageSize: (pageSize: number) => {
|
||||
pagination.pageSize = pageSize;
|
||||
pagination.page = 1;
|
||||
getTableData();
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: getTableData, isPending: tableLoading } = useMutation({
|
||||
mutationFn: async () => {
|
||||
const res = await pageDeviceAlarmLogApi({
|
||||
model: {},
|
||||
extra: getExtraFields(),
|
||||
current: pagination.page ?? 1,
|
||||
size: pagination.pageSize ?? DEFAULT_PAGE_SIZE,
|
||||
sort: 'id',
|
||||
order: 'descending',
|
||||
});
|
||||
return res;
|
||||
},
|
||||
onSuccess: (res) => {
|
||||
const { records, size, total } = res;
|
||||
pagination.pageSize = parseInt(size);
|
||||
pagination.itemCount = parseInt(total);
|
||||
tableData.value = records;
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error(error);
|
||||
const errorFeedback = parseErrorFeedback(error);
|
||||
window.$message.error(errorFeedback);
|
||||
},
|
||||
});
|
||||
|
||||
const onClickReset = () => {
|
||||
resetSearchFields();
|
||||
pagination.page = 1;
|
||||
pagination.pageSize = DEFAULT_PAGE_SIZE;
|
||||
pagination.itemCount = 0;
|
||||
getTableData();
|
||||
};
|
||||
const onClickQuery = () => {
|
||||
// 点击查询按钮时关闭实时刷新
|
||||
realtimeRefresh.value = false;
|
||||
if (searchFieldsChanged.value) {
|
||||
pagination.page = 1;
|
||||
pagination.pageSize = DEFAULT_PAGE_SIZE;
|
||||
searchFieldsChanged.value = false;
|
||||
}
|
||||
getTableData();
|
||||
};
|
||||
|
||||
const { mutate: exportTableData, isPending: exporting } = useMutation({
|
||||
mutationFn: async () => {
|
||||
const data = await exportDeviceAlarmLogApi({
|
||||
model: {},
|
||||
extra: getExtraFields(),
|
||||
current: pagination.page ?? 1,
|
||||
size: pagination.pageSize ?? 10,
|
||||
order: 'descending',
|
||||
sort: 'id',
|
||||
});
|
||||
return data;
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
const time = dayjs().format('YYYY-MM-DD_HH-mm-ss');
|
||||
downloadByData(data, `设备告警记录_${time}.xlsx`);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error(error);
|
||||
const errorFeedback = parseErrorFeedback(error);
|
||||
window.$message.error(errorFeedback);
|
||||
},
|
||||
});
|
||||
|
||||
onBeforeMount(() => {
|
||||
getTableData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NFlex vertical :size="0" style="height: 100%">
|
||||
<!-- 查询面板 -->
|
||||
<NForm style="flex: 0 0 auto; padding: 8px">
|
||||
<NGrid cols="3" :x-gap="24">
|
||||
<NFormItemGi span="1" label="车站" label-placement="left">
|
||||
<NSelect multiple clearable placeholder="请选择车站" v-model:value="searchFields.stationCode_in" :options="stationSelectOptions" />
|
||||
</NFormItemGi>
|
||||
<NFormItemGi span="1" label="设备类型" label-placement="left">
|
||||
<NSelect multiple clearable placeholder="请选择设备类型" v-model:value="searchFields.deviceType_in" :options="deviceTypeSelectOptions" />
|
||||
</NFormItemGi>
|
||||
<NFormItemGi span="1" label="设备名称" label-placement="left">
|
||||
<NInput clearable placeholder="请输入设备名称" v-model:value="searchFields.deviceName_like" />
|
||||
</NFormItemGi>
|
||||
<NFormItemGi span="1" label="告警类型" label-placement="left">
|
||||
<NSelect multiple clearable placeholder="请选择告警类型" v-model:value="searchFields.alarmType_in" :options="alarmTypeSelectOptions" />
|
||||
</NFormItemGi>
|
||||
<NFormItemGi span="1" label="告警级别" label-placement="left">
|
||||
<NSelect multiple clearable placeholder="请选择告警级别" v-model:value="searchFields.faultLevel_in" :options="faultLevelSelectOptions" />
|
||||
</NFormItemGi>
|
||||
<NFormItemGi span="1" label="告警时间" label-placement="left">
|
||||
<NDatePicker v-model:value="searchFields.alarmDate" type="datetimerange" />
|
||||
</NFormItemGi>
|
||||
</NGrid>
|
||||
<!-- 操作按钮 -->
|
||||
<NGrid :cols="1">
|
||||
<NGi>
|
||||
<NFlex>
|
||||
<NButton @click="onClickReset">重置</NButton>
|
||||
<NButton type="primary" :loading="tableLoading" @click="onClickQuery">查询</NButton>
|
||||
</NFlex>
|
||||
</NGi>
|
||||
</NGrid>
|
||||
</NForm>
|
||||
|
||||
<!-- 数据表格工具栏 -->
|
||||
<NFlex align="center" style="padding: 8px; flex: 0 0 auto">
|
||||
<div style="font-size: medium">视频平台日志</div>
|
||||
<NFlex align="center" style="margin-left: auto">
|
||||
<div>实时刷新</div>
|
||||
<NSwitch size="small" v-model:value="realtimeRefresh" />
|
||||
<NButton type="primary" :loading="exporting" @click="() => exportTableData()">导出</NButton>
|
||||
</NFlex>
|
||||
</NFlex>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<NDataTable
|
||||
remote
|
||||
:columns="tableColumns"
|
||||
:data="tableData"
|
||||
:pagination="pagination"
|
||||
:loading="realtimeRefresh ? false : tableLoading"
|
||||
:single-line="false"
|
||||
flex-height
|
||||
style="height: 100%; padding: 8px; flex: 1 1 auto"
|
||||
/>
|
||||
</NFlex>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
258
src/pages/call-log-page.vue
Normal file
258
src/pages/call-log-page.vue
Normal file
@@ -0,0 +1,258 @@
|
||||
<script setup lang="ts">
|
||||
import { exportCallLogApi, pageCallLogApi, type NdmCallLog, type NdmCallLogResultVO, type PageQueryExtra, type Station } from '@/apis';
|
||||
import { useStationStore } from '@/stores';
|
||||
import { downloadByData, parseErrorFeedback } from '@/utils';
|
||||
import { useMutation } from '@tanstack/vue-query';
|
||||
import dayjs from 'dayjs';
|
||||
import {
|
||||
NButton,
|
||||
NDataTable,
|
||||
NDatePicker,
|
||||
NFlex,
|
||||
NForm,
|
||||
NFormItemGi,
|
||||
NGrid,
|
||||
NGridItem,
|
||||
NSelect,
|
||||
NTag,
|
||||
type DataTableColumns,
|
||||
type DataTableRowData,
|
||||
type PaginationProps,
|
||||
type SelectOption,
|
||||
} from 'naive-ui';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { h } from 'vue';
|
||||
import { computed, reactive, ref, watch, watchEffect } from 'vue';
|
||||
|
||||
interface SearchFields extends PageQueryExtra<NdmCallLog> {
|
||||
stationCode?: Station['code'];
|
||||
createdTime: [string, string];
|
||||
}
|
||||
|
||||
const stationStore = useStationStore();
|
||||
const { stations, onlineStations } = storeToRefs(stationStore);
|
||||
|
||||
const stationSelectOptions = computed(() => {
|
||||
return stations.value.map<SelectOption>((station) => ({
|
||||
label: station.name,
|
||||
value: station.code,
|
||||
disabled: !station.online,
|
||||
}));
|
||||
});
|
||||
|
||||
const searchFields = ref<SearchFields>({
|
||||
createdTime: [dayjs().startOf('date').subtract(1, 'week').format('YYYY-MM-DD HH:mm:ss'), dayjs().endOf('date').format('YYYY-MM-DD HH:mm:ss')],
|
||||
});
|
||||
const resetSearchFields = () => {
|
||||
searchFields.value = {
|
||||
stationCode: stations.value.find((station) => station.online)?.code,
|
||||
createdTime: [dayjs().startOf('date').subtract(1, 'week').format('YYYY-MM-DD HH:mm:ss'), dayjs().endOf('date').format('YYYY-MM-DD HH:mm:ss')],
|
||||
};
|
||||
};
|
||||
const getExtraFields = (): PageQueryExtra<NdmCallLog> => {
|
||||
const createdTime_precisest = searchFields.value.createdTime[0];
|
||||
const createdTime_preciseed = searchFields.value.createdTime[1];
|
||||
const sourceGbId_like = searchFields.value.sourceGbId_like;
|
||||
const targetGbId_like = searchFields.value.targetGbId_like;
|
||||
const method_like = searchFields.value.method_like;
|
||||
const messageType_like = searchFields.value.messageType_like;
|
||||
const cmdType_like = searchFields.value.cmdType_like;
|
||||
return {
|
||||
createdTime_precisest,
|
||||
createdTime_preciseed,
|
||||
sourceGbId_like,
|
||||
targetGbId_like,
|
||||
method_like,
|
||||
messageType_like,
|
||||
cmdType_like,
|
||||
};
|
||||
};
|
||||
|
||||
const searchFieldsChanged = ref(false);
|
||||
watch(searchFields, () => {
|
||||
searchFieldsChanged.value = true;
|
||||
});
|
||||
|
||||
const tableColumns: DataTableColumns<NdmCallLogResultVO> = [
|
||||
{ title: '时间', key: 'createdTime' },
|
||||
{ title: '调用者国标码', key: 'sourceGbId' },
|
||||
{ title: '被调用设备国标码', key: 'targetGbId' },
|
||||
{ title: '调用方法', key: 'method' },
|
||||
{ title: '消息类型', key: 'messageType' },
|
||||
{ title: '操作类型', key: 'cmdType' },
|
||||
];
|
||||
|
||||
const tableData = ref<DataTableRowData[]>([]);
|
||||
|
||||
const DEFAULT_PAGE_SIZE = 10;
|
||||
const pagination = reactive<PaginationProps>({
|
||||
showSizePicker: true,
|
||||
page: 1,
|
||||
pageSize: DEFAULT_PAGE_SIZE,
|
||||
pageSizes: [5, 10, 20, 50, 80, 100],
|
||||
itemCount: 0,
|
||||
prefix: ({ itemCount }) => {
|
||||
return h('div', {}, { default: () => `共${itemCount}条` });
|
||||
},
|
||||
onUpdatePage: (page: number) => {
|
||||
pagination.page = page;
|
||||
getTableData();
|
||||
},
|
||||
onUpdatePageSize: (pageSize: number) => {
|
||||
pagination.pageSize = pageSize;
|
||||
pagination.page = 1;
|
||||
getTableData();
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: getTableData, isPending: tableLoading } = useMutation({
|
||||
mutationFn: async () => {
|
||||
if (!searchFields.value.stationCode) throw Error('请选择车站');
|
||||
const res = await pageCallLogApi(
|
||||
{
|
||||
model: {},
|
||||
extra: getExtraFields(),
|
||||
current: pagination.page ?? 1,
|
||||
size: pagination.pageSize ?? DEFAULT_PAGE_SIZE,
|
||||
order: 'descending',
|
||||
sort: 'id',
|
||||
},
|
||||
{
|
||||
stationCode: searchFields.value.stationCode,
|
||||
},
|
||||
);
|
||||
return res;
|
||||
},
|
||||
onSuccess: (res) => {
|
||||
const { records, size, total } = res;
|
||||
pagination.pageSize = parseInt(size);
|
||||
pagination.itemCount = parseInt(total);
|
||||
tableData.value = records;
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error(error);
|
||||
const errorFeedback = parseErrorFeedback(error);
|
||||
window.$message.error(errorFeedback);
|
||||
},
|
||||
});
|
||||
|
||||
const onClickReset = () => {
|
||||
resetSearchFields();
|
||||
pagination.page = 1;
|
||||
pagination.pageSize = DEFAULT_PAGE_SIZE;
|
||||
pagination.itemCount = 0;
|
||||
getTableData();
|
||||
};
|
||||
const onClickQuery = () => {
|
||||
if (searchFieldsChanged.value) {
|
||||
pagination.page = 1;
|
||||
pagination.pageSize = DEFAULT_PAGE_SIZE;
|
||||
searchFieldsChanged.value = false;
|
||||
}
|
||||
getTableData();
|
||||
};
|
||||
|
||||
const { mutate: exportTableData, isPending: exporting } = useMutation({
|
||||
mutationFn: async () => {
|
||||
if (!searchFields.value.stationCode) throw Error('请选择车站');
|
||||
const data = await exportCallLogApi(
|
||||
{
|
||||
model: {},
|
||||
extra: getExtraFields(),
|
||||
current: pagination.page ?? 1,
|
||||
size: pagination.pageSize ?? 10,
|
||||
order: 'descending',
|
||||
sort: 'id',
|
||||
},
|
||||
{
|
||||
stationCode: searchFields.value.stationCode,
|
||||
},
|
||||
);
|
||||
return data;
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
const time = dayjs().format('YYYY-MM-DD_HH-mm-ss');
|
||||
downloadByData(data, `上级调用日志_${time}.xlsx`);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error(error);
|
||||
const errorFeedback = parseErrorFeedback(error);
|
||||
window.$message.error(errorFeedback);
|
||||
},
|
||||
});
|
||||
|
||||
// 进入页面时选择首个在线的车站
|
||||
// 当页面刷新时,车站列表还没有加载完成,defaultStation不存在,if条件为false,
|
||||
// 等待车站列表加载完成后,defaultStation会更新,if条件为true,会更新选择的车站并加载日志记录。
|
||||
// 当从其他页面跳转过来时,车站列表已经存在,defaultStation存在,if条件为true,会更新选择的车站并加载日志记录。
|
||||
// 由于if条件是and逻辑,所以即使defaultStation因车站状态变化而更新,由于已经选择了车站,所以if条件为false。
|
||||
const defaultStation = computed(() => onlineStations.value.at(0));
|
||||
watchEffect(() => {
|
||||
if (defaultStation.value?.code && !searchFields.value.stationCode) {
|
||||
searchFields.value.stationCode = defaultStation.value.code;
|
||||
}
|
||||
});
|
||||
// 当选中的车站第一次有值时,主动获取数据
|
||||
watch(
|
||||
() => searchFields.value.stationCode,
|
||||
(newCode, oldCode) => {
|
||||
if (oldCode === undefined && !!newCode) {
|
||||
getTableData();
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NFlex vertical :size="0" style="height: 100%">
|
||||
<!-- 查询面板 -->
|
||||
<NForm style="flex: 0 0 auto; padding: 8px">
|
||||
<NGrid cols="3" :x-gap="24">
|
||||
<NFormItemGi span="1" label="车站" label-placement="left">
|
||||
<NSelect
|
||||
v-model:value="searchFields.stationCode"
|
||||
:options="stationSelectOptions"
|
||||
:render-label="
|
||||
(option: SelectOption) => {
|
||||
return [
|
||||
h(NTag, { type: option.disabled ? 'error' : 'success', size: 'tiny' }, { default: () => (option.disabled ? '离线' : '在线') }),
|
||||
h('span', {}, { default: () => `${option.label}` }),
|
||||
];
|
||||
}
|
||||
"
|
||||
:multiple="false"
|
||||
clearable
|
||||
/>
|
||||
</NFormItemGi>
|
||||
<NFormItemGi span="1" label="时间" label-placement="left">
|
||||
<NDatePicker v-model:formatted-value="searchFields.createdTime" type="datetimerange" />
|
||||
</NFormItemGi>
|
||||
</NGrid>
|
||||
<!-- 操作按钮 -->
|
||||
<NGrid :cols="1">
|
||||
<NGridItem>
|
||||
<NFlex>
|
||||
<NButton @click="onClickReset">重置</NButton>
|
||||
<NButton type="primary" :loading="tableLoading" @click="onClickQuery">查询</NButton>
|
||||
</NFlex>
|
||||
</NGridItem>
|
||||
</NGrid>
|
||||
</NForm>
|
||||
|
||||
<!-- 数据表格工具栏 -->
|
||||
<NFlex align="center" style="padding: 8px; flex: 0 0 auto">
|
||||
<div style="font-size: medium">视频平台日志</div>
|
||||
<NFlex style="margin-left: auto">
|
||||
<NButton type="primary" :loading="exporting" @click="() => exportTableData()">导出</NButton>
|
||||
</NFlex>
|
||||
</NFlex>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<NDataTable remote :columns="tableColumns" :data="tableData" :pagination="pagination" :loading="tableLoading" :single-line="false" flex-height style="height: 100%; padding: 8px; flex: 1 1 auto" />
|
||||
</NFlex>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
34
src/pages/device-page.vue
Normal file
34
src/pages/device-page.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
import type { NdmDeviceResultVO, Station } from '@/apis';
|
||||
import { DeviceRenderer, DeviceTree, type DeviceTreeProps } from '@/components';
|
||||
import { useStationStore } from '@/stores';
|
||||
import { NLayout, NLayoutContent, NLayoutSider } from 'naive-ui';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { ref } from 'vue';
|
||||
|
||||
const stationStore = useStationStore();
|
||||
const { stations } = storeToRefs(stationStore);
|
||||
|
||||
const selectedStation = ref<Station>();
|
||||
const selectedDevice = ref<NdmDeviceResultVO>();
|
||||
|
||||
const onSelectDevice: DeviceTreeProps['onSelectDevice'] = (device, stationCode) => {
|
||||
selectedDevice.value = device;
|
||||
selectedStation.value = stations.value.find((station) => station.code === stationCode);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NLayout has-sider style="height: 100%">
|
||||
<NLayoutSider bordered :width="600" :collapsed-width="0" show-trigger="bar">
|
||||
<DeviceTree @select-device="onSelectDevice" />
|
||||
</NLayoutSider>
|
||||
<NLayoutContent :content-style="{ padding: '8px 8px 8px 24px' }">
|
||||
<template v-if="selectedStation && selectedDevice">
|
||||
<DeviceRenderer :station="selectedStation" :ndm-device="selectedDevice" />
|
||||
</template>
|
||||
</NLayoutContent>
|
||||
</NLayout>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
104
src/pages/login-page.vue
Normal file
104
src/pages/login-page.vue
Normal file
@@ -0,0 +1,104 @@
|
||||
<script setup lang="ts">
|
||||
import { userClient, type LoginParams } from '@/apis';
|
||||
import { ThemeSwitch } from '@/components';
|
||||
import { useUserStore } from '@/stores';
|
||||
import { parseErrorFeedback, randomNum } from '@/utils';
|
||||
import { useMutation } from '@tanstack/vue-query';
|
||||
import { NButton, NCard, NFlex, NForm, NFormItem, NInput, NLayout } from 'naive-ui';
|
||||
import { reactive } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const loginParams = reactive<LoginParams>({
|
||||
username: '',
|
||||
password: '',
|
||||
code: '',
|
||||
key: randomNum(24, 16),
|
||||
grantType: 'PASSWORD',
|
||||
});
|
||||
|
||||
const { mutate: login, isPending: loading } = useMutation({
|
||||
mutationFn: async (params: LoginParams) => {
|
||||
const userStore = useUserStore();
|
||||
await userStore.userLogin(params);
|
||||
const [err] = await userClient.post<void>(`/api/ndm/ndmKeepAlive/verify`, {}, { timeout: 5000 });
|
||||
if (err) throw err;
|
||||
},
|
||||
onSuccess: () => {
|
||||
window.$message.success('登录成功');
|
||||
router.push({ path: '/' });
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error(error);
|
||||
const errorFeedback = parseErrorFeedback(error);
|
||||
window.$message.error(errorFeedback);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NLayout class="login-page" position="absolute">
|
||||
<div class="login-card-wrapper" @keyup.enter="() => login(loginParams)">
|
||||
<NCard class="login-card">
|
||||
<ThemeSwitch class="theme-switch" />
|
||||
<NFlex vertical justify="center" align="center">
|
||||
<span class="platform-title">网络设备管理平台</span>
|
||||
<span class="login-title">登录</span>
|
||||
<NForm class="login-form" :model="loginParams" label-placement="top">
|
||||
<NFormItem label="用户名" path="username">
|
||||
<NInput v-model:value="loginParams.username" placeholder="请输入用户名" />
|
||||
</NFormItem>
|
||||
<NFormItem label="密码" path="password">
|
||||
<NInput v-model:value="loginParams.password" type="password" show-password-on="click" placeholder="请输入密码" />
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
<NButton type="primary" block strong :loading="loading" @click="() => login(loginParams)">登录</NButton>
|
||||
</NFlex>
|
||||
</NCard>
|
||||
</div>
|
||||
</NLayout>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.login-page {
|
||||
.login-card-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
width: 400px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
padding: 32px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.theme-switch {
|
||||
position: absolute;
|
||||
bottom: 16px;
|
||||
right: 16px;
|
||||
}
|
||||
|
||||
.platform-title {
|
||||
font-size: 28px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
font-size: 16px;
|
||||
color: #999;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
width: 100%;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
20
src/pages/not-found-page.vue
Normal file
20
src/pages/not-found-page.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { NButton, NResult } from 'naive-ui';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const routeToRoot = () => {
|
||||
router.push({ path: '/' });
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NResult status="404" title="404 页面不存在" style="margin-top: 50px">
|
||||
<template #footer>
|
||||
<NButton @click="routeToRoot">返回首页</NButton>
|
||||
</template>
|
||||
</NResult>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
225
src/pages/station-page.vue
Normal file
225
src/pages/station-page.vue
Normal file
@@ -0,0 +1,225 @@
|
||||
<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 { useAlarmStore, useDeviceStore, useSettingStore, useStationStore } from '@/stores';
|
||||
import { parseErrorFeedback } from '@/utils';
|
||||
import { useMutation } from '@tanstack/vue-query';
|
||||
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 onAction = (action: Action) => {
|
||||
selectedAction.value = action;
|
||||
if (action === null) return;
|
||||
showOperation.value = true;
|
||||
stationSelectable.value = true;
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
selectedAction.value = null;
|
||||
showOperation.value = false;
|
||||
stationSelectable.value = false;
|
||||
stationSelection.value = {};
|
||||
};
|
||||
|
||||
const onFinish = onCancel;
|
||||
|
||||
const { mutate: syncCamera, isPending: cameraSyncing } = useMutation({
|
||||
mutationFn: async () => {
|
||||
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] }));
|
||||
},
|
||||
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}`);
|
||||
} else {
|
||||
window.$message.warning(`摄像机同步完成:成功${successCount}个车站,失败${failureCount}个车站`);
|
||||
window.$message.warning(`${failedStations}`);
|
||||
}
|
||||
} else {
|
||||
window.$message.success('摄像机同步成功');
|
||||
}
|
||||
if (successCount > 0) {
|
||||
// 摄像机同步后,需要重新查询一次设备,待测试
|
||||
refetchLineDevicesQuery();
|
||||
}
|
||||
onFinish();
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error(error);
|
||||
const errorFeedback = parseErrorFeedback(error);
|
||||
window.$message.error(errorFeedback);
|
||||
onCancel();
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: syncNvrChannels, isPending: nvrChannelsSyncing } = useMutation({
|
||||
mutationFn: async () => {
|
||||
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] }));
|
||||
},
|
||||
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}`);
|
||||
} else {
|
||||
window.$message.warning(`录像机通道同步完成:成功${successCount}个车站,失败${failureCount}个车站`);
|
||||
window.$message.warning(`${failedStations}`);
|
||||
}
|
||||
} else {
|
||||
window.$message.success('录像机通道同步成功');
|
||||
}
|
||||
onFinish();
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error(error);
|
||||
const errorFeedback = parseErrorFeedback(error);
|
||||
window.$message.error(errorFeedback);
|
||||
onCancel();
|
||||
},
|
||||
});
|
||||
|
||||
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 selectedStation = ref<Station>();
|
||||
const showDeviceParamConfigModal = ref(false);
|
||||
const showDeviceDetailModal = ref(false);
|
||||
const showAlarmDetailModal = ref(false);
|
||||
|
||||
const onClickConfig: StationCardProps['onClickConfig'] = (station) => {
|
||||
selectedStation.value = station;
|
||||
showDeviceParamConfigModal.value = true;
|
||||
};
|
||||
|
||||
const onClickDetail: StationCardProps['onClickDetail'] = (type, station) => {
|
||||
selectedStation.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: 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>
|
||||
</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>
|
||||
</NFlex>
|
||||
|
||||
<!-- 车站 -->
|
||||
<NGrid :cols="stationGridColumns" :x-gap="6" :y-gap="6" style="padding: 8px">
|
||||
<NGridItem v-for="station in stations" :key="station.code">
|
||||
<StationCard
|
||||
:station="station"
|
||||
:devices="lineDevices[station.code] ?? initStationDevices()"
|
||||
:alarms="lineAlarms[station.code] ?? initStationAlarms()"
|
||||
:selectable="stationSelectable"
|
||||
v-model:selected="stationSelection[station.code]"
|
||||
@click-detail="onClickDetail"
|
||||
@click-config="onClickConfig"
|
||||
/>
|
||||
</NGridItem>
|
||||
</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" />
|
||||
|
||||
<DeviceParamConfigModal v-model:show="showDeviceParamConfigModal" :station="selectedStation" />
|
||||
<DeviceDetailModal v-model:show="showDeviceDetailModal" :station="selectedStation" />
|
||||
<AlarmDetailModal v-model:show="showAlarmDetailModal" :station="selectedStation" />
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
297
src/pages/vimp-log-page.vue
Normal file
297
src/pages/vimp-log-page.vue
Normal file
@@ -0,0 +1,297 @@
|
||||
<script setup lang="ts">
|
||||
import { exportVimpLogApi, pageVimpLogApi, type NdmVimpLog, type NdmVimpLogResultVO, type PageQueryExtra, type Station } from '@/apis';
|
||||
import { useStationStore } from '@/stores';
|
||||
import { downloadByData, parseErrorFeedback } from '@/utils';
|
||||
import { useMutation } from '@tanstack/vue-query';
|
||||
import dayjs from 'dayjs';
|
||||
import destr from 'destr';
|
||||
import {
|
||||
NButton,
|
||||
NDataTable,
|
||||
NDatePicker,
|
||||
NFlex,
|
||||
NForm,
|
||||
NFormItemGi,
|
||||
NGrid,
|
||||
NGridItem,
|
||||
NPopover,
|
||||
NScrollbar,
|
||||
NSelect,
|
||||
NTag,
|
||||
type DataTableColumns,
|
||||
type DataTableRowData,
|
||||
type PaginationProps,
|
||||
type SelectOption,
|
||||
} from 'naive-ui';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { computed, h, reactive, ref, watch, watchEffect } from 'vue';
|
||||
|
||||
interface SearchFields extends PageQueryExtra<NdmVimpLog> {
|
||||
stationCode?: Station['code'];
|
||||
createdTime: [string, string];
|
||||
}
|
||||
|
||||
const stationStore = useStationStore();
|
||||
const { stations, onlineStations } = storeToRefs(stationStore);
|
||||
|
||||
const stationSelectOptions = computed(() => {
|
||||
return stations.value.map<SelectOption>((station) => ({
|
||||
label: station.name,
|
||||
value: station.code,
|
||||
disabled: !station.online,
|
||||
}));
|
||||
});
|
||||
|
||||
const searchFields = ref<SearchFields>({
|
||||
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,
|
||||
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 getExtraFields = (): PageQueryExtra<NdmVimpLog> => {
|
||||
const createdTime_precisest = searchFields.value.createdTime[0];
|
||||
const createdTime_preciseed = searchFields.value.createdTime[1];
|
||||
const logType_in = (searchFields.value.logType_in ?? []).length > 0 ? [...searchFields.value.logType_in] : undefined;
|
||||
return {
|
||||
createdTime_precisest,
|
||||
createdTime_preciseed,
|
||||
logType_in,
|
||||
};
|
||||
};
|
||||
|
||||
const searchFieldsChanged = ref(false);
|
||||
watch(searchFields, () => {
|
||||
searchFieldsChanged.value = true;
|
||||
});
|
||||
|
||||
const tableColumns: DataTableColumns<NdmVimpLogResultVO> = [
|
||||
{ title: '时间', key: 'createdTime' },
|
||||
{ title: '操作类型', key: 'description' },
|
||||
{ title: '请求IP', key: 'requestIp' },
|
||||
{ title: '耗时(ms)', key: 'consumedTime' },
|
||||
{ title: '被调用设备', key: 'targetCode' },
|
||||
{
|
||||
title: '操作参数',
|
||||
key: 'params',
|
||||
width: 100,
|
||||
render(rowData) {
|
||||
const result = JSON.stringify(destr(rowData.params), null, 2);
|
||||
return h(
|
||||
NPopover,
|
||||
{ trigger: 'click' },
|
||||
{
|
||||
trigger: () => h(NButton, { size: 'tiny', text: true, type: 'primary' }, { default: () => '查看' }),
|
||||
default: () =>
|
||||
h(
|
||||
NScrollbar,
|
||||
{ style: { maxHeight: '40vh' } },
|
||||
{
|
||||
default: () => h('pre', {}, { default: () => result }),
|
||||
},
|
||||
),
|
||||
},
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作结果',
|
||||
key: 'result',
|
||||
width: 100,
|
||||
render: (rowData) => {
|
||||
const result = JSON.stringify(destr(rowData.result), null, 2);
|
||||
return h(
|
||||
NPopover,
|
||||
{ trigger: 'click' },
|
||||
{
|
||||
trigger: () => h(NButton, { size: 'tiny', text: true, type: 'primary' }, { default: () => '查看' }),
|
||||
default: () =>
|
||||
h(
|
||||
NScrollbar,
|
||||
{ style: { maxHeight: '40vh' } },
|
||||
{
|
||||
default: () => h('pre', {}, { default: () => result }),
|
||||
},
|
||||
),
|
||||
},
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const tableData = ref<DataTableRowData[]>([]);
|
||||
|
||||
const DEFAULT_PAGE_SIZE = 10;
|
||||
const pagination = reactive<PaginationProps>({
|
||||
showSizePicker: true,
|
||||
page: 1,
|
||||
pageSize: DEFAULT_PAGE_SIZE,
|
||||
pageSizes: [5, 10, 20, 50, 80, 100],
|
||||
itemCount: 0,
|
||||
prefix: ({ itemCount }) => {
|
||||
return h('div', {}, { default: () => `共${itemCount}条` });
|
||||
},
|
||||
onUpdatePage: (page: number) => {
|
||||
pagination.page = page;
|
||||
getTableData();
|
||||
},
|
||||
onUpdatePageSize: (pageSize: number) => {
|
||||
pagination.pageSize = pageSize;
|
||||
pagination.page = 1;
|
||||
getTableData();
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: getTableData, isPending: tableLoading } = useMutation({
|
||||
mutationFn: async () => {
|
||||
if (!searchFields.value.stationCode) throw Error('请选择车站');
|
||||
const res = await pageVimpLogApi(
|
||||
{
|
||||
model: {},
|
||||
extra: getExtraFields(),
|
||||
current: pagination.page ?? 1,
|
||||
size: pagination.pageSize ?? DEFAULT_PAGE_SIZE,
|
||||
order: 'descending',
|
||||
sort: 'id',
|
||||
},
|
||||
{
|
||||
stationCode: searchFields.value.stationCode,
|
||||
},
|
||||
);
|
||||
return res;
|
||||
},
|
||||
onSuccess: (res) => {
|
||||
const { records, size, total } = res;
|
||||
pagination.pageSize = parseInt(size);
|
||||
pagination.itemCount = parseInt(total);
|
||||
tableData.value = records;
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error(error);
|
||||
const errorFeedback = parseErrorFeedback(error);
|
||||
window.$message.error(errorFeedback);
|
||||
},
|
||||
});
|
||||
|
||||
const onClickReset = () => {
|
||||
resetSearchFields();
|
||||
pagination.page = 1;
|
||||
pagination.pageSize = DEFAULT_PAGE_SIZE;
|
||||
pagination.itemCount = 0;
|
||||
getTableData();
|
||||
};
|
||||
const onClickQuery = () => {
|
||||
if (searchFieldsChanged.value) {
|
||||
pagination.page = 1;
|
||||
pagination.pageSize = DEFAULT_PAGE_SIZE;
|
||||
searchFieldsChanged.value = false;
|
||||
}
|
||||
getTableData();
|
||||
};
|
||||
|
||||
const { mutate: exportTableData, isPending: exporting } = useMutation({
|
||||
mutationFn: async () => {
|
||||
if (!searchFields.value.stationCode) throw Error('请选择车站');
|
||||
const data = await exportVimpLogApi(
|
||||
{
|
||||
model: {},
|
||||
extra: getExtraFields(),
|
||||
current: pagination.page ?? 1,
|
||||
size: pagination.pageSize ?? 10,
|
||||
order: 'descending',
|
||||
sort: 'id',
|
||||
},
|
||||
{
|
||||
stationCode: searchFields.value.stationCode,
|
||||
},
|
||||
);
|
||||
return data;
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
const time = dayjs().format('YYYY-MM-DD_HH-mm-ss');
|
||||
downloadByData(data, `视频操作日志_${time}.xlsx`);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error(error);
|
||||
const errorFeedback = parseErrorFeedback(error);
|
||||
window.$message.error(errorFeedback);
|
||||
},
|
||||
});
|
||||
|
||||
// 进入页面时选择首个在线的车站
|
||||
// 当页面刷新时,车站列表还没有加载完成,defaultStation不存在,if条件为false,
|
||||
// 等待车站列表加载完成后,defaultStation会更新,if条件为true,会更新选择的车站并加载日志记录。
|
||||
// 当从其他页面跳转过来时,车站列表已经存在,defaultStation存在,if条件为true,会更新选择的车站并加载日志记录。
|
||||
// 由于if条件是and逻辑,所以即使defaultStation因车站状态变化而更新,由于已经选择了车站,所以if条件为false。
|
||||
const defaultStation = computed(() => onlineStations.value.at(0));
|
||||
watchEffect(() => {
|
||||
if (defaultStation.value?.code && !searchFields.value.stationCode) {
|
||||
searchFields.value.stationCode = defaultStation.value.code;
|
||||
}
|
||||
});
|
||||
// 当选中的车站第一次有值时,主动获取数据
|
||||
watch(
|
||||
() => searchFields.value.stationCode,
|
||||
(newCode, oldCode) => {
|
||||
if (oldCode === undefined && !!newCode) {
|
||||
getTableData();
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NFlex vertical :size="0" style="height: 100%">
|
||||
<!-- 查询面板 -->
|
||||
<NForm style="flex: 0 0 auto; padding: 8px">
|
||||
<NGrid cols="3" :x-gap="24">
|
||||
<NFormItemGi span="1" label="车站" label-placement="left">
|
||||
<NSelect
|
||||
v-model:value="searchFields.stationCode"
|
||||
:options="stationSelectOptions"
|
||||
:render-label="
|
||||
(option: SelectOption) => {
|
||||
return [
|
||||
h(NTag, { type: option.disabled ? 'error' : 'success', size: 'tiny' }, { default: () => (option.disabled ? '离线' : '在线') }),
|
||||
h('span', {}, { default: () => `${option.label}` }),
|
||||
];
|
||||
}
|
||||
"
|
||||
:multiple="false"
|
||||
clearable
|
||||
/>
|
||||
</NFormItemGi>
|
||||
<NFormItemGi span="1" label="时间" label-placement="left">
|
||||
<NDatePicker v-model:formatted-value="searchFields.createdTime" type="datetimerange" />
|
||||
</NFormItemGi>
|
||||
</NGrid>
|
||||
<!-- 操作按钮 -->
|
||||
<NGrid :cols="1">
|
||||
<NGridItem>
|
||||
<NFlex>
|
||||
<NButton @click="onClickReset">重置</NButton>
|
||||
<NButton type="primary" :loading="tableLoading" @click="onClickQuery">查询</NButton>
|
||||
</NFlex>
|
||||
</NGridItem>
|
||||
</NGrid>
|
||||
</NForm>
|
||||
|
||||
<!-- 数据表格工具栏 -->
|
||||
<NFlex align="center" style="padding: 8px; flex: 0 0 auto">
|
||||
<div style="font-size: medium">视频平台日志</div>
|
||||
<NFlex style="margin-left: auto">
|
||||
<NButton type="primary" :loading="exporting" @click="() => exportTableData()">导出</NButton>
|
||||
</NFlex>
|
||||
</NFlex>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<NDataTable remote :columns="tableColumns" :data="tableData" :pagination="pagination" :loading="tableLoading" :single-line="false" flex-height style="height: 100%; padding: 8px; flex: 1 1 auto" />
|
||||
</NFlex>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
Reference in New Issue
Block a user