feat: 添加权限查询和管理机制

- 新增权限管理页面
- 改进轮询链,引入权限查询
- 支持订阅权限变更或轮询权限检测变更
- 应用权限到页面和组件
This commit is contained in:
yangsy
2026-01-22 10:34:37 +08:00
parent 82789c78a9
commit 0af52c62ce
53 changed files with 1129 additions and 131 deletions

View File

@@ -16,8 +16,9 @@ const NDM_TYPES: Record<string, DeviceType> = {
<script setup lang="ts">
import { deleteCameraIgnoreApi, pageCameraIgnoreApi, type NdmCameraIgnore, type NdmCameraIgnoreResultVO, type PageQueryExtra, type Station } from '@/apis';
import { DEVICE_TYPE_LITERALS, DEVICE_TYPE_NAMES, type DeviceType } from '@/enums';
import { useDeviceStore, useStationStore } from '@/stores';
import { usePermission } from '@/composables';
import { DEVICE_TYPE_LITERALS, DEVICE_TYPE_NAMES, PERMISSION_TYPE_LITERALS, type DeviceType } from '@/enums';
import { useDeviceStore, usePermissionStore } from '@/stores';
import { useMutation } from '@tanstack/vue-query';
import { isCancel } from 'axios';
import {
@@ -46,8 +47,13 @@ interface SearchFields extends PageQueryExtra<NdmCameraIgnore> {
// deviceId_like?: string;
}
const stationStore = useStationStore();
const { stations } = storeToRefs(stationStore);
const permissionStore = usePermissionStore();
const { permissions } = storeToRefs(permissionStore);
const { hasPermission } = usePermission();
const stations = computed(() => permissionStore.stations.VIEW ?? []);
const deviceStore = useDeviceStore();
const { lineDevices } = storeToRefs(deviceStore);
@@ -64,6 +70,14 @@ const stationSelectOptions = computed<SelectOption[]>(() => {
// }));
// });
// 权限变化时,需要刷新表格数据
watch(permissions, (newPermissions, oldPermissions) => {
const oldPermissionsJson = JSON.stringify(oldPermissions);
const newPermissionsJson = JSON.stringify(newPermissions);
if (oldPermissionsJson === newPermissionsJson) return;
onClickReset();
});
const searchFields = ref<SearchFields>({});
const resetSearchFields = () => {
searchFields.value = {};
@@ -84,7 +98,7 @@ watch(searchFields, () => {
searchFieldsChanged.value = true;
});
const tableColumns: DataTableColumns<NdmCameraIgnoreResultVO> = [
const tableColumns = computed<DataTableColumns<NdmCameraIgnoreResultVO>>(() => [
{ title: '忽略时间', key: 'createdTime', align: 'center' },
// { title: '更新时间', key: 'updatedTime' },
{
@@ -142,6 +156,11 @@ const tableColumns: DataTableColumns<NdmCameraIgnoreResultVO> = [
align: 'center',
width: 120,
render: (rowData) => {
const { deviceId } = rowData;
if (!deviceId) return null;
const stationCode = deviceId.slice(0, 4);
if (!stationCode) return null;
if (!hasPermission(stationCode, PERMISSION_TYPE_LITERALS.OPERATION)) return null;
return h(
NPopconfirm,
{
@@ -167,7 +186,7 @@ const tableColumns: DataTableColumns<NdmCameraIgnoreResultVO> = [
);
},
},
];
]);
const { mutate: cancelIgnore } = useMutation({
mutationFn: async (params: { id?: string; signal?: AbortSignal }) => {

View File

@@ -3,7 +3,7 @@ import { exportDeviceAlarmLogApi, pageDeviceAlarmLogApi, type NdmDeviceAlarmLog,
import { useAlarmActionColumn, useCameraSnapColumn } from '@/composables';
import { ALARM_TYPES, DEVICE_TYPE_CODES, DEVICE_TYPE_LITERALS, DEVICE_TYPE_NAMES, FAULT_LEVELS, tryGetDeviceType, type DeviceType } from '@/enums';
import { renderAlarmDateCell, renderAlarmTypeCell, renderDeviceTypeCell, renderFaultLevelCell } from '@/helpers';
import { useDeviceStore, useStationStore, useUnreadStore } from '@/stores';
import { useDeviceStore, usePermissionStore, useUnreadStore } from '@/stores';
import { downloadByData, parseErrorFeedback } from '@/utils';
import { useMutation } from '@tanstack/vue-query';
import { watchDebounced } from '@vueuse/core';
@@ -44,8 +44,10 @@ interface SearchFields extends PageQueryExtra<NdmDeviceAlarmLog> {
const route = useRoute();
const router = useRouter();
const stationStore = useStationStore();
const { stations } = storeToRefs(stationStore);
const permissionStore = usePermissionStore();
const { permissions } = storeToRefs(permissionStore);
const stations = computed(() => permissionStore.stations.VIEW ?? []);
const deviceStore = useDeviceStore();
const { lineDevices } = storeToRefs(deviceStore);
@@ -78,6 +80,14 @@ const faultLevelSelectOptions = computed<SelectOption[]>(() => {
}));
});
// 权限变化时,需要刷新表格数据
watch(permissions, (newPermissions, oldPermissions) => {
const oldPermissionsJson = JSON.stringify(oldPermissions);
const newPermissionsJson = JSON.stringify(newPermissions);
if (oldPermissionsJson === newPermissionsJson) return;
onClickReset();
});
// 未读告警数量被清零时,代表从别的页面跳转过来,需要刷新告警表格数据
const unreadCountCleared = computed(() => unreadAlarmCount.value === 0);
watch(unreadCountCleared, (newValue, oldValue) => {
@@ -125,14 +135,17 @@ const resetSearchFields = () => {
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 deviceNameLike = searchFields.value.deviceName_like;
const alarmTypeIn = searchFields.value.alarmType_in;
const faultLevelIn = searchFields.value.faultLevel_in;
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,
stationCode_in: stationCodeIn.length > 0 ? [...stationCodeIn] : stations.value.map((station) => station.code),
deviceType_in: deviceTypeIn.length > 0 ? [...deviceTypeIn] : undefined,
deviceName_like: deviceNameLike.length > 0 ? deviceNameLike : undefined,
alarmType_in: alarmTypeIn.length > 0 ? [...alarmTypeIn] : undefined,
faultLevel_in: faultLevelIn.length > 0 ? [...faultLevelIn] : undefined,
alarmDate_ge: alarmDateGe,
alarmDate_le: alarmDateLe,
};

View File

@@ -3,13 +3,12 @@ import type { NdmDeviceResultVO, Station } from '@/apis';
import { DeviceRenderer, DeviceTree, type DeviceTreeProps } from '@/components';
import type { UseDeviceSelectionReturn } from '@/composables';
import { SELECT_DEVICE_FN_INJECTION_KEY } from '@/constants';
import { useStationStore } from '@/stores';
import { usePermissionStore } from '@/stores';
import { NLayout, NLayoutContent, NLayoutSider } from 'naive-ui';
import { storeToRefs } from 'pinia';
import { provide, ref } from 'vue';
import { computed, provide, ref } from 'vue';
const stationStore = useStationStore();
const { stations } = storeToRefs(stationStore);
const permissionStore = usePermissionStore();
const stations = computed(() => permissionStore.stations.VIEW ?? []);
const selectedStation = ref<Station>();
const selectedDevice = ref<NdmDeviceResultVO>();

View File

@@ -32,7 +32,7 @@ const callLogTypeOptions: SelectOption[] = [
<script setup lang="ts">
import { exportCallLogApi, pageCallLogApi, type NdmCallLog, type NdmCallLogResultVO, type PageQueryExtra, type Station } from '@/apis';
import { useStationStore } from '@/stores';
import { usePermissionStore } from '@/stores';
import { downloadByData, parseErrorFeedback } from '@/utils';
import { useMutation } from '@tanstack/vue-query';
import { isCancel } from 'axios';
@@ -63,8 +63,11 @@ interface SearchFields extends PageQueryExtra<NdmCallLog> {
createdTime: [string, string];
}
const stationStore = useStationStore();
const { stations, onlineStations } = storeToRefs(stationStore);
const permissionStore = usePermissionStore();
const { permissions } = storeToRefs(permissionStore);
const stations = computed(() => permissionStore.stations.VIEW ?? []);
const onlineStations = computed(() => stations.value.filter((station) => station.online));
const stationSelectOptions = computed(() => {
return stations.value.map<SelectOption>((station) => ({
@@ -74,6 +77,14 @@ const stationSelectOptions = computed(() => {
}));
});
// 权限变化时,需要刷新表格数据
watch(permissions, (newPermissions, oldPermissions) => {
const oldPermissionsJson = JSON.stringify(oldPermissions);
const newPermissionsJson = JSON.stringify(newPermissions);
if (oldPermissionsJson === newPermissionsJson) return;
onClickReset();
});
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')],

View File

@@ -29,7 +29,7 @@ const vimpLogTypeOptions: SelectOption[] = [
<script setup lang="ts">
import { exportVimpLogApi, pageVimpLogApi, type NdmVimpLog, type NdmVimpLogResultVO, type PageQueryExtra, type Station } from '@/apis';
import { useStationStore } from '@/stores';
import { usePermissionStore } from '@/stores';
import { downloadByData, parseErrorFeedback } from '@/utils';
import { useMutation } from '@tanstack/vue-query';
import { isCancel } from 'axios';
@@ -59,8 +59,11 @@ interface SearchFields extends PageQueryExtra<NdmVimpLog> {
createdTime: [string, string];
}
const stationStore = useStationStore();
const { stations, onlineStations } = storeToRefs(stationStore);
const permissionStore = usePermissionStore();
const { permissions } = storeToRefs(permissionStore);
const stations = computed(() => permissionStore.stations.VIEW ?? []);
const onlineStations = computed(() => stations.value.filter((station) => station.online));
const stationSelectOptions = computed(() => {
return stations.value.map<SelectOption>((station) => ({
@@ -70,6 +73,14 @@ const stationSelectOptions = computed(() => {
}));
});
// 权限变化时,需要刷新表格数据
watch(permissions, (newPermissions, oldPermissions) => {
const oldPermissionsJson = JSON.stringify(oldPermissions);
const newPermissionsJson = JSON.stringify(newPermissions);
if (oldPermissionsJson === newPermissionsJson) return;
onClickReset();
});
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],

View File

@@ -0,0 +1,184 @@
<script setup lang="ts">
import { pageBaseEmployeeApi, type BaseEmployeePageQuery, type BaseEmployeeResultVO } from '@/apis';
import { PermissionConfigModal } from '@/components';
import { parseErrorFeedback } from '@/utils';
import { useMutation } from '@tanstack/vue-query';
import { isCancel } from 'axios';
import { KeyIcon } from 'lucide-vue-next';
import { NButton, NDataTable, NFlex, NForm, NFormItemGi, NGrid, NGridItem, NInput, type DataTableColumns, type DataTableRowData, type PaginationProps } from 'naive-ui';
import { h, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
interface SearchFields extends BaseEmployeePageQuery {}
const searchFields = ref<SearchFields>({});
const resetSearchFields = () => {
searchFields.value = {
realName: '',
};
};
const getModelFields = (): BaseEmployeePageQuery => {
return {
realName: searchFields.value.realName,
};
};
const searchFieldsChanged = ref(false);
watch(searchFields, () => {
searchFieldsChanged.value = true;
});
const showPermissionConfigModal = ref(false);
const selectedEmployeeId = ref('');
const tableColumns: DataTableColumns<BaseEmployeeResultVO> = [
{ title: '姓名', key: 'realName', align: 'center' },
{ title: '创建时间', key: 'createdTime', align: 'center' },
{
title: '操作',
key: 'action',
align: 'center',
width: 150,
render: (rowData) => {
return h(
NButton,
{
secondary: true,
type: 'info',
size: 'small',
onClick: () => {
const { id } = rowData;
if (!id) return;
selectedEmployeeId.value = id;
showPermissionConfigModal.value = true;
},
},
{
icon: () => h(KeyIcon),
default: () => '配置权限',
},
);
},
},
];
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 abortController = ref(new AbortController());
const { mutate: getTableData, isPending: tableLoading } = useMutation({
mutationFn: async () => {
abortController.value.abort();
abortController.value = new AbortController();
const signal = abortController.value.signal;
const res = await pageBaseEmployeeApi(
{
model: getModelFields(),
extra: {},
current: pagination.page ?? 1,
size: pagination.pageSize ?? DEFAULT_PAGE_SIZE,
order: 'descending',
sort: 'id',
},
{
signal,
},
);
return res;
},
onSuccess: (res) => {
const { records, size, total } = res;
pagination.pageSize = parseInt(size);
pagination.itemCount = parseInt(total);
tableData.value = records;
},
onError: (error) => {
if (isCancel(error)) return;
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();
};
onMounted(() => {
getTableData();
});
onBeforeUnmount(() => {
abortController.value.abort();
});
</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">
<NInput v-model:value="searchFields.realName" />
</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>
<PermissionConfigModal v-model:show="showPermissionConfigModal" :employee-id="selectedEmployeeId" />
</template>
<style scoped lang="scss"></style>

View File

@@ -2,9 +2,9 @@
import { initStationAlarms, initStationDevices, syncCameraApi, syncNvrChannelsApi, type Station } from '@/apis';
import { AlarmDetailModal, DeviceDetailModal, DeviceParamConfigModal, IcmpExportModal, RecordCheckExportModal, StationCard, type StationCardProps } from '@/components';
import { useBatchActions, useLineDevicesQuery } from '@/composables';
import { useAlarmStore, useDeviceStore, useSettingStore, useStationStore } from '@/stores';
import { useAlarmStore, useDeviceStore, usePermissionStore, useSettingStore } from '@/stores';
import { useMutation } from '@tanstack/vue-query';
import { objectEntries, useElementSize } from '@vueuse/core';
import { useElementSize } from '@vueuse/core';
import { isCancel } from 'axios';
import { NButton, NButtonGroup, NCheckbox, NFlex, NGrid, NGridItem, NScrollbar } from 'naive-ui';
import { storeToRefs } from 'pinia';
@@ -13,8 +13,8 @@ import { computed, ref, useTemplateRef } from 'vue';
const settingStore = useSettingStore();
const { stationGridCols } = storeToRefs(settingStore);
const stationStore = useStationStore();
const { stations } = storeToRefs(stationStore);
const permissionStore = usePermissionStore();
const stations = computed(() => permissionStore.stations.VIEW ?? []);
const deviceStore = useDeviceStore();
const { lineDevices } = storeToRefs(deviceStore);
@@ -42,7 +42,10 @@ const showRecordCheckExportModal = ref(false);
const abortController = ref(new AbortController());
const { batchActions, selectedAction, selectableStations, stationSelection, toggleSelectAction, toggleSelectAllStations, confirmAction, cancelAction } = useBatchActions(stations, abortController);
const { batchActions, selectedAction, selectableStations, stationSelection, selectionProps, toggleSelectAction, toggleSelectAllStations, confirmAction, cancelAction } = useBatchActions(
stations,
abortController,
);
const { refetch: refetchLineDevicesQuery } = useLineDevicesQuery();
@@ -177,12 +180,7 @@ const onClickDetail: StationCardProps['onClickDetail'] = (type, station) => {
</template>
</NButtonGroup>
<template v-if="selectedAction">
<NCheckbox
label="全选"
:disabled="selectableStations.length === 0"
:checked="selectableStations.length > 0 && selectableStations.length === objectEntries(stationSelection).filter(([, selected]) => selected).length"
@update:checked="toggleSelectAllStations"
/>
<NCheckbox label="全选" :disabled="selectionProps.disabled" :checked="selectionProps.checked" :indeterminate="selectionProps.indeterminate" @update:checked="toggleSelectAllStations" />
<NButton tertiary size="small" type="primary" :focusable="false" :loading="confirming" @click="onClickConfirmAction">确定</NButton>
<NButton tertiary size="small" type="tertiary" :focusable="false" @click="cancelAction">取消</NButton>
</template>