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
@@ -1,11 +1,14 @@
import { usePermission } from '../permission';
import { deleteCameraIgnoreApi, pageCameraIgnoreApi, saveCameraIgnoreApi, updateDeviceAlarmLogApi, type NdmDeviceAlarmLogResultVO } from '@/apis';
import { DEVICE_TYPE_LITERALS, tryGetDeviceType } from '@/enums';
import { DEVICE_TYPE_LITERALS, PERMISSION_TYPE_LITERALS, tryGetDeviceType } from '@/enums';
import { parseErrorFeedback } from '@/utils';
import { useMutation } from '@tanstack/vue-query';
import { NButton, NFlex, NPopconfirm, type DataTableColumn, type DataTableRowData } from 'naive-ui';
import { h, type Ref } from 'vue';
export const useAlarmActionColumn = (tableData: Ref<DataTableRowData[]>) => {
const { hasPermission } = usePermission();
const { mutate: confirmAlarm } = useMutation({
mutationFn: async (params: { id: string | null }) => {
const { id } = params;
@@ -115,28 +118,30 @@ export const useAlarmActionColumn = (tableData: Ref<DataTableRowData[]>) => {
default: () => '确认告警?',
},
),
tryGetDeviceType(rowData.deviceType) === DEVICE_TYPE_LITERALS.ndmCamera && [
h(
NPopconfirm,
{
onPositiveClick: () => ignoreCamera({ id }),
},
{
trigger: () => h(NButton, { tertiary: true, type: 'info', size: 'tiny' }, { default: () => '忽略' }),
default: () => '忽略设备?',
},
),
// h(
// NPopconfirm,
// {
// onPositiveClick: () => noticeCamera({ id }),
// },
// {
// trigger: () => h(NButton, { text: true, type: 'info', size: 'small' }, { icon: () => h(EyeOutlined) }),
// default: () => '取消忽略设备?',
// },
// ),
],
tryGetDeviceType(rowData.deviceType) === DEVICE_TYPE_LITERALS.ndmCamera &&
rowData.stationCode &&
hasPermission(rowData.stationCode, PERMISSION_TYPE_LITERALS.OPERATION) && [
h(
NPopconfirm,
{
onPositiveClick: () => ignoreCamera({ id }),
},
{
trigger: () => h(NButton, { tertiary: true, type: 'info', size: 'tiny' }, { default: () => '忽略' }),
default: () => '忽略设备?',
},
),
// h(
// NPopconfirm,
// {
// onPositiveClick: () => noticeCamera({ id }),
// },
// {
// trigger: () => h(NButton, { text: true, type: 'info', size: 'small' }, { icon: () => h(EyeOutlined) }),
// default: () => '取消忽略设备?',
// },
// ),
],
],
},
);
+1
View File
@@ -1,5 +1,6 @@
export * from './alarm';
export * from './device';
export * from './permission';
export * from './query';
export * from './station';
export * from './stomp';
+1
View File
@@ -0,0 +1 @@
export * from './use-permission';
@@ -0,0 +1,14 @@
import type { PermissionType } from '@/enums';
import { usePermissionStore } from '@/stores';
export const usePermission = () => {
const permissionStore = usePermissionStore();
const hasPermission = (stationCode: string, permissionType: PermissionType) => {
return !!permissionStore.permissions[stationCode]?.includes(permissionType);
};
return {
hasPermission,
};
};
+1
View File
@@ -1,5 +1,6 @@
export * from './use-line-alarms-query';
export * from './use-line-devices-query';
export * from './use-line-stations-query';
export * from './use-user-permission-query';
export * from './use-verify-user-query';
export * from './use-version-check-query';
@@ -1,12 +1,11 @@
import { initStationAlarms, pageDeviceAlarmLogApi, type Station } from '@/apis';
import { LINE_ALARMS_QUERY_KEY, STATION_ALARMS_MUTATION_KEY } from '@/constants';
import { tryGetDeviceType } from '@/enums';
import { useAlarmStore, useStationStore } from '@/stores';
import { useAlarmStore, usePermissionStore } from '@/stores';
import { parseErrorFeedback } from '@/utils';
import { CancelledError, useMutation, useQuery } from '@tanstack/vue-query';
import { isCancel } from 'axios';
import dayjs from 'dayjs';
import { storeToRefs } from 'pinia';
import { computed } from 'vue';
export const useStationAlarmsMutation = () => {
@@ -69,8 +68,9 @@ export const useStationAlarmsMutation = () => {
* @see [use-line-stations-query.ts](./use-line-stations-query.ts)
*/
export const useLineAlarmsQuery = () => {
const stationStore = useStationStore();
const { stations } = storeToRefs(stationStore);
const permissionStore = usePermissionStore();
const stations = computed(() => permissionStore.stations.VIEW ?? []);
const { mutateAsync: getStationAlarms } = useStationAlarmsMutation();
return useQuery({
@@ -1,10 +1,9 @@
import { getAllDevicesApi, initStationDevices, type Station } from '@/apis';
import { LINE_DEVICES_QUERY_KEY, STATION_DEVICES_MUTATION_KEY } from '@/constants';
import { useDeviceStore, useStationStore } from '@/stores';
import { useDeviceStore, usePermissionStore } from '@/stores';
import { parseErrorFeedback } from '@/utils';
import { CancelledError, useMutation, useQuery } from '@tanstack/vue-query';
import { isCancel } from 'axios';
import { storeToRefs } from 'pinia';
import { computed } from 'vue';
export const useStationDevicesMutation = () => {
@@ -36,8 +35,9 @@ export const useStationDevicesMutation = () => {
* @see [use-line-stations-query.ts](./use-line-stations-query.ts)
*/
export const useLineDevicesQuery = () => {
const stationStore = useStationStore();
const { stations } = storeToRefs(stationStore);
const permissionStore = usePermissionStore();
const stations = computed(() => permissionStore.stations.VIEW ?? []);
const { mutateAsync: getStationDevices } = useStationDevicesMutation();
return useQuery({
@@ -7,8 +7,6 @@ import axios, { isCancel } from 'axios';
import dayjs from 'dayjs';
import { storeToRefs } from 'pinia';
import { computed } from 'vue';
import { useLineDevicesQuery } from './use-line-devices-query';
import { useLineAlarmsQuery } from './use-line-alarms-query';
export const useLineStationsMutation = () => {
const stationStore = useStationStore();
@@ -17,12 +15,13 @@ export const useLineStationsMutation = () => {
mutationKey: [LINE_STATIONS_MUTATION_KEY],
mutationFn: async (params: { signal?: AbortSignal }) => {
const { signal } = params;
const { data: ndmStationList } = await axios.get<{ code: string; name: string }[]>(`/minio/ndm/ndm-stations.json?_t=${dayjs().unix()}`, { signal });
const { data: ndmStationList } = await axios.get<Omit<Station, 'online' | 'ip'>[]>(`/minio/ndm/ndm-stations.json?_t=${dayjs().unix()}`, { signal });
const stations = ndmStationList.map<Station>((station) => ({
code: station.code ?? '',
name: station.name ?? '',
online: false,
ip: '',
occ: station.occ,
}));
const verifyList = await batchVerifyApi({ signal });
return stations.map((station) => ({
@@ -48,8 +47,6 @@ export const useLineStationsQuery = () => {
const { pollingStations } = storeToRefs(settingStore);
const { requestInterval } = getAppEnvConfig();
const { mutateAsync: getLineStations } = useLineStationsMutation();
const { refetch: refetchLineDevicesQuery } = useLineDevicesQuery();
const { refetch: refetchLineAlarmsQuery } = useLineAlarmsQuery();
return useQuery({
queryKey: computed(() => [LINE_STATIONS_QUERY_KEY]),
@@ -62,12 +59,6 @@ export const useLineStationsQuery = () => {
const endTime = performance.now();
console.log(`${LINE_STATIONS_QUERY_KEY}: ${endTime - startTime} ms`);
if (!pollingStations.value) return null;
await refetchLineDevicesQuery();
if (!pollingStations.value) return null;
await refetchLineAlarmsQuery();
return null;
},
});
@@ -0,0 +1,67 @@
import { useLineDevicesQuery } from './use-line-devices-query';
import { useLineAlarmsQuery } from './use-line-alarms-query';
import { pagePermissionApi } from '@/apis';
import { USER_PERMISSION_QUERY_KEY } from '@/constants';
import { PERMISSION_TYPE_LITERALS } from '@/enums';
import { usePermissionStore, useSettingStore, useStationStore, useUserStore } from '@/stores';
import { useQuery } from '@tanstack/vue-query';
import { storeToRefs } from 'pinia';
import { computed, watch } from 'vue';
import { useLineStationsQuery } from './use-line-stations-query';
export const useUserPermissionQuery = () => {
const settingStore = useSettingStore();
const { pollingStations, activeRequests } = storeToRefs(settingStore);
const userStore = useUserStore();
const { userInfo } = storeToRefs(userStore);
const stationStore = useStationStore();
const { stations } = storeToRefs(stationStore);
const permissionStore = usePermissionStore();
const { permissions } = storeToRefs(permissionStore);
const { dataUpdatedAt: stationsUpdatedTime } = useLineStationsQuery();
const { refetch: refetchLineDevicesQuery } = useLineDevicesQuery();
const { refetch: refetchLineAlarmsQuery } = useLineAlarmsQuery();
watch([permissions, stationsUpdatedTime], async ([newPermissions, newUpdatedTime], [oldPermissions, oldUpdatedTime]) => {
const newPermissionsJson = JSON.stringify(newPermissions);
const oldPermissionsJson = JSON.stringify(oldPermissions);
if (newPermissionsJson === oldPermissionsJson && newUpdatedTime === oldUpdatedTime) return;
// 设备查询和告警查询依赖pollingEnabdled
// 当关闭轮询时,只会取消当前正在执行的查询,
// 所以如果在关闭轮询时refetch还未执行,那么这一次取消就是无效的,refetch依然会执行,
// 所以在每个refetch被调用前都需要检查pollingEnabled,否则就可能会取消失败
if (!pollingStations.value) return;
await refetchLineDevicesQuery();
if (!pollingStations.value) return;
await refetchLineAlarmsQuery();
});
return useQuery({
queryKey: computed(() => [USER_PERMISSION_QUERY_KEY]),
// 启用【车站轮询】或【主动请求】时,都认为查询被启用
enabled: computed(() => (pollingStations.value || activeRequests.value) && userInfo.value?.['employeeId'] && stations.value.length > 0),
// 当启用【车站轮询】时,刷新间隔为10秒,缓存时间为5秒
refetchInterval: computed(() => (pollingStations.value ? 10 * 1000 : undefined)),
staleTime: computed(() => (pollingStations.value ? 5 * 1000 : undefined)),
queryFn: async ({ signal }) => {
const { records } = await pagePermissionApi(
{
model: {
employeeId: userInfo.value['employeeId'],
},
current: 1,
size: Object.keys(PERMISSION_TYPE_LITERALS).length * stations.value.length,
},
{
signal,
},
);
permissionStore.setPermRecords(records);
return null;
},
});
};
+36 -1
View File
@@ -1,4 +1,8 @@
import { usePermission } from '../permission';
import { type Station } from '@/apis';
import { PERMISSION_TYPE_LITERALS, type PermissionType } from '@/enums';
import { objectEntries } from '@vueuse/core';
import type { CheckboxProps } from 'naive-ui';
import { computed, ref, watch, type Ref } from 'vue';
type BatchActionKey = 'export-icmp' | 'export-record' | 'sync-camera' | 'sync-nvr';
@@ -6,29 +10,36 @@ type BatchActionKey = 'export-icmp' | 'export-record' | 'sync-camera' | 'sync-nv
type BatchAction = {
label: string;
key: BatchActionKey;
permission: PermissionType;
active: boolean;
};
export const useBatchActions = (stations: Ref<Station[]>, abortController?: Ref<AbortController | undefined>) => {
const { hasPermission } = usePermission();
const batchActions = ref<BatchAction[]>([
{
label: '导出设备状态',
key: 'export-icmp',
permission: PERMISSION_TYPE_LITERALS.VIEW,
active: false,
},
{
label: '导出录像诊断',
key: 'export-record',
permission: PERMISSION_TYPE_LITERALS.VIEW,
active: false,
},
{
label: '同步摄像机',
key: 'sync-camera',
permission: PERMISSION_TYPE_LITERALS.OPERATION,
active: false,
},
{
label: '同步录像机通道',
key: 'sync-nvr',
permission: PERMISSION_TYPE_LITERALS.OPERATION,
active: false,
},
]);
@@ -39,11 +50,33 @@ export const useBatchActions = (stations: Ref<Station[]>, abortController?: Ref<
const selectableStations = computed(() => {
if (!selectedAction.value) return [];
return stations.value;
const result: Station[] = [];
if (selectedAction.value.permission === PERMISSION_TYPE_LITERALS.VIEW) {
result.push(...stations.value.filter((station) => hasPermission(station.code, PERMISSION_TYPE_LITERALS.VIEW)));
}
if (selectedAction.value.permission === PERMISSION_TYPE_LITERALS.OPERATION) {
result.push(...stations.value.filter((station) => hasPermission(station.code, PERMISSION_TYPE_LITERALS.OPERATION)));
}
return result;
});
const stationSelection = ref<Record<Station['code'], boolean>>({});
const selectionProps = computed<CheckboxProps>(() => {
const selectableStationsLength = selectableStations.value.length;
const selectedStationsLength = objectEntries(stationSelection.value).filter(([, selected]) => selected).length;
const disabled = selectableStationsLength === 0;
const checked = selectableStationsLength > 0 && selectedStationsLength === selectableStationsLength;
const indeterminate = selectableStationsLength > 0 && selectedStationsLength > 0 && selectedStationsLength < selectableStationsLength;
return {
disabled,
checked,
indeterminate,
};
});
const toggleSelectAction = (action: BatchAction) => {
batchActions.value.forEach((batchAction) => {
if (batchAction.key === action.key) {
@@ -95,6 +128,8 @@ export const useBatchActions = (stations: Ref<Station[]>, abortController?: Ref<
selectableStations,
stationSelection,
selectionProps,
toggleSelectAction,
toggleSelectAllStations,
+13 -3
View File
@@ -1,12 +1,12 @@
import type { NdmDeviceAlarmLogResultVO, Station, SyncCameraResult } from '@/apis';
import { ALARM_TOPIC, SYNC_CAMERA_STATUS_TOPIC } from '@/constants';
import { useSettingStore, useStationStore, useUnreadStore } from '@/stores';
import { ALARM_TOPIC, PERMISSION_TOPIC, SYNC_CAMERA_STATUS_TOPIC } from '@/constants';
import { useSettingStore, useStationStore, useUnreadStore, useUserStore } from '@/stores';
import { Client } from '@stomp/stompjs';
import { watchDebounced } from '@vueuse/core';
import destr from 'destr';
import { storeToRefs } from 'pinia';
import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { useStationAlarmsMutation } from '../query';
import { useStationAlarmsMutation, useUserPermissionQuery } from '../query';
const getBrokerUrl = () => {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
@@ -26,6 +26,10 @@ export const useStompClient = () => {
const settingStore = useSettingStore();
const { subscribeMessages } = storeToRefs(settingStore);
const userStore = useUserStore();
const { userInfo } = storeToRefs(userStore);
const { refetch: refetchUserPermissionQuery } = useUserPermissionQuery();
const { mutate: refreshStationAlarms } = useStationAlarmsMutation();
const stompClient = ref<Client | null>(null);
@@ -47,6 +51,11 @@ export const useStompClient = () => {
unreadStore.pushUnreadAlarm(alarm);
}
});
stompClient.value?.subscribe(PERMISSION_TOPIC, (message) => {
const employeeId = destr<string>(message.body);
if (userInfo.value?.['employeeId'] !== employeeId) return;
refetchUserPermissionQuery();
});
stompClient.value?.subscribe(SYNC_CAMERA_STATUS_TOPIC, (message) => {
const { stationCode, startTime, endTime, insertList, updateList, deleteList } = destr<SyncCameraResult>(message.body);
syncCameraResult.value[stationCode] = { stationCode, startTime, endTime, insertList, updateList, deleteList };
@@ -55,6 +64,7 @@ export const useStompClient = () => {
onDisconnect: () => {
console.log('Stomp连接断开');
stompClient.value?.unsubscribe(ALARM_TOPIC);
stompClient.value?.unsubscribe(PERMISSION_TOPIC);
stompClient.value?.unsubscribe(SYNC_CAMERA_STATUS_TOPIC);
},
onStompError: (frame) => {