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

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

View File

@@ -3,4 +3,5 @@ export interface Station {
name: string;
online: boolean;
ip: string;
occ?: boolean; // 是否为控制中心
}

View File

@@ -0,0 +1,21 @@
import type { BaseModel, ReduceForPageQuery, ReduceForSaveVO, ReduceForUpdateVO } from '@/apis';
import type { Nullable, Optional } from '@/types';
export interface BaseEmployee extends BaseModel {
userId: string;
realName: string;
defUser: Nullable<
{
username: string;
nickName: string;
} & BaseModel
>;
}
export type BaseEmployeeResultVO = Nullable<BaseEmployee>;
export type BaseEmployeeSaveVO = Partial<Omit<BaseEmployee, ReduceForSaveVO>>;
export type BaseEmployeeUpdateVO = Optional<Omit<BaseEmployee, ReduceForUpdateVO>>;
export type BaseEmployeePageQuery = Partial<Omit<BaseEmployee, ReduceForPageQuery>>;

View File

@@ -0,0 +1 @@
export * from './base-employee';

View File

@@ -1,2 +1,3 @@
export * from './ndm-permission';
export * from './ndm-security-box';
export * from './ndm-switch';

View File

@@ -0,0 +1,34 @@
import type { BaseModel, ReduceForPageQuery, ReduceForSaveVO, ReduceForUpdateVO, Station } from '@/apis';
import type { PermissionType } from '@/enums';
import type { Nullable, Optional } from '@/types';
export interface NdmPermission extends BaseModel {
/**
* 员工ID
*/
employeeId: string;
/**
* 服务器IP地址
*/
ipAddress: string;
/**
* 站号
*/
stationCode: Station['code'];
/**
* 站名
*/
name: string;
/**
* 权限类型
*/
type: PermissionType;
}
export type NdmPermissionResultVO = Nullable<NdmPermission>;
export type NdmPermissionSaveVO = Partial<Omit<NdmPermission, ReduceForSaveVO>>;
export type NdmPermissionUpdateVO = Optional<Omit<NdmPermission, ReduceForUpdateVO>>;
export type NdmPermissionPageQuery = Partial<Omit<NdmPermission, ReduceForPageQuery>>;

View File

@@ -1,3 +1,4 @@
export * from './base';
export * from './biz';
export * from './common';
export * from './schema';

View File

@@ -0,0 +1,21 @@
import type { BaseEmployeePageQuery, BaseEmployeeResultVO, PageParams, PageResult } from '@/apis';
import { userClient } from '@/apis/client';
import { unwrapResponse } from '@/utils';
export const pageBaseEmployeeApi = async (pageQuery: PageParams<BaseEmployeePageQuery>, options?: { signal?: AbortSignal }) => {
const { signal } = options ?? {};
const client = userClient;
const endpoint = '/api/base/baseEmployee/page';
const resp = await client.post<PageResult<BaseEmployeeResultVO>>(endpoint, pageQuery, { signal });
const data = unwrapResponse(resp);
return data;
};
export const detailBaseEmployeeApi = async (id: string, options?: { signal?: AbortSignal }) => {
const { signal } = options ?? {};
const client = userClient;
const endpoint = `/api/base/baseEmployee/detail`;
const resp = await client.get<BaseEmployeeResultVO>(endpoint, { params: { id }, signal });
const data = unwrapResponse(resp);
return data;
};

View File

@@ -0,0 +1 @@
export * from './base-employee';

View File

@@ -1,3 +1,4 @@
export * from './ndm-permission';
export * from './ndm-security-box';
export * from './ndm-service-available';
export * from './ndm-switch';

View File

@@ -0,0 +1,83 @@
import {
ndmClient,
userClient,
type NdmPermissionPageQuery,
type NdmPermissionResultVO,
type NdmPermissionSaveVO,
type NdmPermissionUpdateVO,
type PageParams,
type PageResult,
type Station,
} from '@/apis';
import type { PermissionTypeEnum } from '@/enums';
import { unwrapResponse } from '@/utils';
export const permissionTypesApi = async (options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmPermission/types`;
const resp = await client.get<PermissionTypeEnum>(endpoint, { signal });
const data = unwrapResponse(resp);
return data;
};
export const pagePermissionApi = async (pageQuery: PageParams<NdmPermissionPageQuery>, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmPermission/page`;
const resp = await client.post<PageResult<NdmPermissionResultVO>>(endpoint, pageQuery, { signal });
const data = unwrapResponse(resp);
return data;
};
export const detailPermissionApi = async (id: string, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmPermission/detail`;
const resp = await client.get<NdmPermissionResultVO>(endpoint, { params: { id }, signal });
const data = unwrapResponse(resp);
return data;
};
export const savePermissionApi = async (saveVO: NdmPermissionSaveVO, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmPermission`;
const resp = await client.post<NdmPermissionResultVO>(endpoint, saveVO, { signal });
const result = unwrapResponse(resp);
return result;
};
export const updatePermissionApi = async (updateVO: NdmPermissionUpdateVO, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmPermission`;
const resp = await client.put<NdmPermissionResultVO>(endpoint, updateVO, { signal });
const result = unwrapResponse(resp);
return result;
};
export const deletePermissionApi = async (ids: string[], options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmPermission`;
const resp = await client.delete<boolean>(endpoint, ids, { signal });
const result = unwrapResponse(resp);
return result;
};
export const modifyPermissionApi = async (params: { employeeId: string; saveList: NdmPermissionSaveVO[]; removeList: string[] }, options?: { stationCode?: Station['code']; signal?: AbortSignal }) => {
const { stationCode, signal } = options ?? {};
const client = stationCode ? ndmClient : userClient;
const prefix = stationCode ? `/${stationCode}` : '';
const endpoint = `${prefix}/api/ndm/ndmPermission/modify`;
const resp = await client.post<boolean>(endpoint, params, { signal });
const result = unwrapResponse(resp);
return result;
};

View File

@@ -1,2 +1,3 @@
export * from './base';
export * from './biz';
export * from './system';

View File

@@ -1,6 +1,8 @@
<script setup lang="ts">
import type { NdmAlarmHostResultVO, Station } from '@/apis';
import { AlarmHostCurrentDiag, AlarmHostHistoryDiag, AlarmHostUpdate, DeviceRawCard } from '@/components';
import { usePermission } from '@/composables';
import { PERMISSION_TYPE_LITERALS } from '@/enums';
import { useSettingStore } from '@/stores';
import { NCard, NPageHeader, NScrollbar, NTab, NTabs } from 'naive-ui';
import { storeToRefs } from 'pinia';
@@ -18,6 +20,8 @@ const router = useRouter();
const settingStore = useSettingStore();
const { showDeviceRawData } = storeToRefs(settingStore);
const { hasPermission } = usePermission();
const { ndmDevice, station } = toRefs(props);
const showPageHeader = computed(() => {
@@ -45,7 +49,7 @@ watch([ndmDevice, showDeviceRawData], ([newDevice, enabled], [oldDevice]) => {
<NTabs :value="activeTabName" @update:value="onTabChange">
<NTab name="当前诊断">当前诊断</NTab>
<NTab name="历史诊断">历史诊断</NTab>
<NTab name="修改设备">修改设备</NTab>
<NTab v-if="hasPermission(station.code, PERMISSION_TYPE_LITERALS.OPERATION)" name="修改设备">修改设备</NTab>
<NTab v-if="showDeviceRawData" name="原始数据">原始数据</NTab>
</NTabs>
</template>

View File

@@ -1,6 +1,8 @@
<script setup lang="ts">
import type { NdmCameraResultVO, Station } from '@/apis';
import { CameraCurrentDiag, CameraHistoryDiag, CameraUpdate, DeviceRawCard } from '@/components';
import { usePermission } from '@/composables';
import { PERMISSION_TYPE_LITERALS } from '@/enums';
import { useSettingStore } from '@/stores';
import { NCard, NPageHeader, NScrollbar, NTab, NTabs } from 'naive-ui';
import { storeToRefs } from 'pinia';
@@ -18,6 +20,8 @@ const router = useRouter();
const settingStore = useSettingStore();
const { showDeviceRawData } = storeToRefs(settingStore);
const { hasPermission } = usePermission();
const { ndmDevice, station } = toRefs(props);
const showPageHeader = computed(() => {
@@ -45,7 +49,7 @@ watch([ndmDevice, showDeviceRawData], ([newDevice, enabled], [oldDevice]) => {
<NTabs :value="activeTabName" @update:value="onTabChange">
<NTab name="当前诊断">当前诊断</NTab>
<NTab name="历史诊断">历史诊断</NTab>
<NTab name="修改设备">修改设备</NTab>
<NTab v-if="hasPermission(station.code, PERMISSION_TYPE_LITERALS.OPERATION)" name="修改设备">修改设备</NTab>
<NTab v-if="showDeviceRawData" name="原始数据">原始数据</NTab>
</NTabs>
</template>

View File

@@ -1,6 +1,8 @@
<script setup lang="ts">
import type { NdmDecoderResultVO, Station } from '@/apis';
import { DecoderCurrentDiag, DecoderHistoryDiag, DecoderUpdate, DeviceRawCard } from '@/components';
import { usePermission } from '@/composables';
import { PERMISSION_TYPE_LITERALS } from '@/enums';
import { useSettingStore } from '@/stores';
import { NCard, NPageHeader, NScrollbar, NTab, NTabs } from 'naive-ui';
import { storeToRefs } from 'pinia';
@@ -18,6 +20,8 @@ const router = useRouter();
const settingStore = useSettingStore();
const { showDeviceRawData } = storeToRefs(settingStore);
const { hasPermission } = usePermission();
const { ndmDevice, station } = toRefs(props);
const showPageHeader = computed(() => {
@@ -45,7 +49,7 @@ watch([ndmDevice, showDeviceRawData], ([newDevice, enabled], [oldDevice]) => {
<NTabs :value="activeTabName" @update:value="onTabChange">
<NTab name="当前诊断">当前诊断</NTab>
<NTab name="历史诊断">历史诊断</NTab>
<NTab name="修改设备">修改设备</NTab>
<NTab v-if="hasPermission(station.code, PERMISSION_TYPE_LITERALS.OPERATION)" name="修改设备">修改设备</NTab>
<NTab v-if="showDeviceRawData" name="原始数据">原始数据</NTab>
</NTabs>
</template>

View File

@@ -1,6 +1,8 @@
<script setup lang="ts">
import type { NdmKeyboardResultVO, Station } from '@/apis';
import { DeviceRawCard, KeyboardCurrentDiag, KeyboardHistoryDiag, KeyboardUpdate } from '@/components';
import { usePermission } from '@/composables';
import { PERMISSION_TYPE_LITERALS } from '@/enums';
import { useSettingStore } from '@/stores';
import { NCard, NPageHeader, NScrollbar, NTab, NTabs } from 'naive-ui';
import { storeToRefs } from 'pinia';
@@ -18,6 +20,8 @@ const router = useRouter();
const settingStore = useSettingStore();
const { showDeviceRawData } = storeToRefs(settingStore);
const { hasPermission } = usePermission();
const { ndmDevice, station } = toRefs(props);
const showPageHeader = computed(() => {
@@ -45,7 +49,7 @@ watch([ndmDevice, showDeviceRawData], ([newDevice, enabled], [oldDevice]) => {
<NTabs :value="activeTabName" @update:value="onTabChange">
<NTab name="当前诊断">当前诊断</NTab>
<NTab name="历史诊断">历史诊断</NTab>
<NTab name="修改设备">修改设备</NTab>
<NTab v-if="hasPermission(station.code, PERMISSION_TYPE_LITERALS.OPERATION)" name="修改设备">修改设备</NTab>
<NTab v-if="showDeviceRawData" name="原始数据">原始数据</NTab>
</NTabs>
</template>

View File

@@ -1,6 +1,8 @@
<script setup lang="ts">
import type { NdmNvrResultVO, Station } from '@/apis';
import { DeviceRawCard, NvrCurrentDiag, NvrHistoryDiag, NvrUpdate } from '@/components';
import { usePermission } from '@/composables';
import { PERMISSION_TYPE_LITERALS } from '@/enums';
import { useSettingStore } from '@/stores';
import { NCard, NPageHeader, NScrollbar, NTab, NTabs } from 'naive-ui';
import { storeToRefs } from 'pinia';
@@ -18,6 +20,8 @@ const router = useRouter();
const settingStore = useSettingStore();
const { showDeviceRawData } = storeToRefs(settingStore);
const { hasPermission } = usePermission();
const { ndmDevice, station } = toRefs(props);
const showPageHeader = computed(() => {
@@ -45,7 +49,7 @@ watch([ndmDevice, showDeviceRawData], ([newDevice, enabled], [oldDevice]) => {
<NTabs :value="activeTabName" @update:value="onTabChange">
<NTab name="当前诊断">当前诊断</NTab>
<NTab name="历史诊断">历史诊断</NTab>
<NTab name="修改设备">修改设备</NTab>
<NTab v-if="hasPermission(station.code, PERMISSION_TYPE_LITERALS.OPERATION)" name="修改设备">修改设备</NTab>
<NTab v-if="showDeviceRawData" name="原始数据">原始数据</NTab>
</NTabs>
</template>

View File

@@ -1,6 +1,8 @@
<script setup lang="ts">
import type { NdmSecurityBoxResultVO, Station } from '@/apis';
import { DeviceRawCard, SecurityBoxCurrentDiag, SecurityBoxHistoryDiag, SecurityBoxUpdate } from '@/components';
import { usePermission } from '@/composables';
import { PERMISSION_TYPE_LITERALS } from '@/enums';
import { useSettingStore } from '@/stores';
import { NCard, NPageHeader, NScrollbar, NTab, NTabs } from 'naive-ui';
import { storeToRefs } from 'pinia';
@@ -18,6 +20,8 @@ const router = useRouter();
const settingStore = useSettingStore();
const { showDeviceRawData } = storeToRefs(settingStore);
const { hasPermission } = usePermission();
const { ndmDevice, station } = toRefs(props);
const showPageHeader = computed(() => {
@@ -45,7 +49,7 @@ watch([ndmDevice, showDeviceRawData], ([newDevice, enabled], [oldDevice]) => {
<NTabs :value="activeTabName" @update:value="onTabChange">
<NTab name="当前诊断">当前诊断</NTab>
<NTab name="历史诊断">历史诊断</NTab>
<NTab name="修改设备">修改设备</NTab>
<NTab v-if="hasPermission(station.code, PERMISSION_TYPE_LITERALS.OPERATION)" name="修改设备">修改设备</NTab>
<NTab v-if="showDeviceRawData" name="原始数据">原始数据</NTab>
</NTabs>
</template>

View File

@@ -1,6 +1,8 @@
<script setup lang="ts">
import type { NdmServerResultVO, Station } from '@/apis';
import { DeviceRawCard, ServerCurrentDiag, ServerHistoryDiag, ServerUpdate } from '@/components';
import { usePermission } from '@/composables';
import { PERMISSION_TYPE_LITERALS } from '@/enums';
import { useSettingStore } from '@/stores';
import { NCard, NPageHeader, NScrollbar, NTab, NTabs } from 'naive-ui';
import { storeToRefs } from 'pinia';
@@ -18,6 +20,8 @@ const router = useRouter();
const settingStore = useSettingStore();
const { showDeviceRawData } = storeToRefs(settingStore);
const { hasPermission } = usePermission();
const { ndmDevice, station } = toRefs(props);
const showPageHeader = computed(() => {
@@ -45,7 +49,7 @@ watch([ndmDevice, showDeviceRawData], ([newDevice, enabled], [oldDevice]) => {
<NTabs :value="activeTabName" @update:value="onTabChange">
<NTab name="当前诊断">当前诊断</NTab>
<NTab name="历史诊断">历史诊断</NTab>
<NTab name="修改设备">修改设备</NTab>
<NTab v-if="hasPermission(station.code, PERMISSION_TYPE_LITERALS.OPERATION)" name="修改设备">修改设备</NTab>
<NTab v-if="showDeviceRawData" name="原始数据">原始数据</NTab>
</NTabs>
</template>

View File

@@ -1,6 +1,8 @@
<script setup lang="ts">
import type { NdmSwitchResultVO, Station } from '@/apis';
import { DeviceRawCard, SwitchCurrentDiag, SwitchHistoryDiag, SwitchUpdate } from '@/components';
import { usePermission } from '@/composables';
import { PERMISSION_TYPE_LITERALS } from '@/enums';
import { useSettingStore } from '@/stores';
import { NCard, NPageHeader, NScrollbar, NTab, NTabs } from 'naive-ui';
import { storeToRefs } from 'pinia';
@@ -18,6 +20,8 @@ const router = useRouter();
const settingStore = useSettingStore();
const { showDeviceRawData } = storeToRefs(settingStore);
const { hasPermission } = usePermission();
const { ndmDevice, station } = toRefs(props);
const showPageHeader = computed(() => {
@@ -45,7 +49,7 @@ watch([ndmDevice, showDeviceRawData], ([newDevice, enabled], [oldDevice]) => {
<NTabs :value="activeTabName" @update:value="onTabChange">
<NTab name="当前诊断">当前诊断</NTab>
<NTab name="历史诊断">历史诊断</NTab>
<NTab name="修改设备">修改设备</NTab>
<NTab v-if="hasPermission(station.code, PERMISSION_TYPE_LITERALS.OPERATION)" name="修改设备">修改设备</NTab>
<NTab v-if="showDeviceRawData" name="原始数据">原始数据</NTab>
</NTabs>
</template>

View File

@@ -1,9 +1,9 @@
<script setup lang="ts">
import { initStationDevices, type NdmDeviceResultVO, type NdmNvrResultVO, type Station } from '@/apis';
import { useDeviceTree, type UseDeviceTreeReturn } from '@/composables';
import { DEVICE_TYPE_NAMES, DEVICE_TYPE_LITERALS, tryGetDeviceType, type DeviceType } from '@/enums';
import { useDeviceTree, usePermission, type UseDeviceTreeReturn } from '@/composables';
import { DEVICE_TYPE_NAMES, DEVICE_TYPE_LITERALS, tryGetDeviceType, type DeviceType, PERMISSION_TYPE_LITERALS } from '@/enums';
import { isNvrCluster } from '@/helpers';
import { useDeviceStore, useStationStore } from '@/stores';
import { useDeviceStore, usePermissionStore } from '@/stores';
import { watchImmediate } from '@vueuse/core';
import destr from 'destr';
import { isFunction } from 'es-toolkit';
@@ -60,6 +60,8 @@ const { station, events, syncRoute, devicePrefixLabel } = toRefs(props);
const themeVars = useThemeVars();
const { hasPermission } = usePermission();
const {
// 设备选择
selectedStationCode,
@@ -87,8 +89,9 @@ const onSelectDevice = (device: NdmDeviceResultVO, stationCode: Station['code'])
emit('afterSelectDevice', device, stationCode);
};
const stationStore = useStationStore();
const { stations } = storeToRefs(stationStore);
const permissionStore = usePermissionStore();
const stations = computed(() => permissionStore.stations.VIEW ?? []);
const deviceStore = useDeviceStore();
const { lineDevices } = storeToRefs(deviceStore);
@@ -220,13 +223,17 @@ const nodeProps: TreeProps['nodeProps'] = ({ option }) => {
payload.stopPropagation();
payload.preventDefault();
// 仅当事件列表包含 `manage` 时才显示右键菜单
// 如果事件列表包含 `manage`,则直接结束逻辑
if (!events.value?.includes('manage')) return;
const { clientX, clientY } = payload;
const stationCode = option['stationCode'] as Station['code'];
// 仅当用户在该车站拥有操作权限时才显示右键菜单
if (!hasPermission(stationCode, PERMISSION_TYPE_LITERALS.OPERATION)) return;
const deviceType = option['deviceType'] as DeviceType | undefined;
const device = option['device'] as NdmDeviceResultVO | undefined;
const { clientX, clientY } = payload;
contextmenu.value = { x: clientX, y: clientY, stationCode, deviceType, device };
showContextmenu.value = true;
},

View File

@@ -1,8 +1,10 @@
<script setup lang="ts">
import { retentionDaysApi, snapStatusApi, type LineAlarms, type LineDevices, type Station, type VersionInfo } from '@/apis';
import { ThemeSwitch } from '@/components';
import { usePermission } from '@/composables';
import { NDM_ALARM_STORE_ID, NDM_DEVICE_STORE_ID, NDM_STATION_STORE_ID } from '@/constants';
import { useSettingStore } from '@/stores';
import { PERMISSION_TYPE_LITERALS } from '@/enums';
import { useSettingStore, useStationStore } from '@/stores';
import { downloadByData, getAppEnvConfig, parseErrorFeedback, sleep } from '@/utils';
import { useMutation } from '@tanstack/vue-query';
import { useEventListener } from '@vueuse/core';
@@ -13,13 +15,20 @@ import localforage from 'localforage';
import { DownloadIcon, Trash2Icon, UploadIcon } from 'lucide-vue-next';
import { NButton, NButtonGroup, NDivider, NDrawer, NDrawerContent, NDropdown, NFlex, NFormItem, NIcon, NInput, NInputNumber, NModal, NSwitch, NText, type DropdownOption } from 'naive-ui';
import { storeToRefs } from 'pinia';
import { ref, watch } from 'vue';
import { computed, ref, watch } from 'vue';
const show = defineModel<boolean>('show', { default: false });
const stationStore = useStationStore();
const { stations } = storeToRefs(stationStore);
const occStation = computed(() => stations.value.find((station) => !!station.occ));
const settingsStore = useSettingStore();
const { menuCollpased, stationGridCols, debugMode, showDeviceRawData, pollingStations, activeRequests, subscribeMessages, mockUser, useLocalDB } = storeToRefs(settingsStore);
const { hasPermission } = usePermission();
const versionInfo = ref<VersionInfo>({ version: '', buildTime: '' });
const { mutate: getVersionInfo } = useMutation({
@@ -294,6 +303,7 @@ const onDrawerAfterLeave = () => {
<NInputNumber v-model:value="stationGridCols" :min="1" :max="10" />
</NFormItem>
<template v-if="!!occStation && hasPermission(occStation.code, PERMISSION_TYPE_LITERALS.OPERATION)">
<NDivider>告警</NDivider>
<NFormItem label="告警画面截图保留天数" label-placement="left">
<NFlex justify="space-between" align="center" style="width: 100%">
@@ -313,6 +323,7 @@ const onDrawerAfterLeave = () => {
</NButtonGroup>
</NFlex>
</NFormItem>
</template>
<template v-if="debugMode">
<NDivider title-placement="center">调试</NDivider>

View File

@@ -1,3 +1,4 @@
export * from './device';
export * from './global';
export * from './permission';
export * from './station';

View File

@@ -0,0 +1,6 @@
import type { ComponentInstance } from 'vue';
import PermissionConfigModal from './permission-config-modal.vue';
export type PermissionConfigModalProps = ComponentInstance<typeof PermissionConfigModal>['$props'];
export { PermissionConfigModal };

View File

@@ -0,0 +1,302 @@
<script setup lang="ts">
import { detailBaseEmployeeApi, modifyPermissionApi, pagePermissionApi, type BaseEmployeeResultVO, type NdmPermissionResultVO, type NdmPermissionSaveVO, type Station } from '@/apis';
import { PERMISSION_TYPE_LITERALS, PERMISSION_TYPE_NAMES, type PermissionType } from '@/enums';
import { useStationStore } from '@/stores';
import { parseErrorFeedback } from '@/utils';
import { useMutation } from '@tanstack/vue-query';
import { objectEntries } from '@vueuse/core';
import { isCancel } from 'axios';
import { cloneDeep } from 'es-toolkit';
import { NButton, NCheckbox, NDataTable, NFlex, NModal, NText, type DataTableColumn, type DataTableColumns } from 'naive-ui';
import { storeToRefs } from 'pinia';
import { computed, h, ref, toRefs } from 'vue';
type NdmPermissionSaveOrResultVO = NdmPermissionSaveVO | NdmPermissionResultVO;
const props = defineProps<{
employeeId?: string;
}>();
const stationStore = useStationStore();
const { stations } = storeToRefs(stationStore);
const show = defineModel<boolean>('show', { default: false });
const { employeeId } = toRefs(props);
const abortController = ref<AbortController>(new AbortController());
const employee = ref<BaseEmployeeResultVO>();
const { mutateAsync: getEmployeeAsync } = useMutation({
mutationFn: async () => {
abortController.value.abort();
abortController.value = new AbortController();
if (!employeeId.value) return;
const signal = abortController.value.signal;
const data = await detailBaseEmployeeApi(employeeId.value, { signal });
return data;
},
onSuccess: (data) => {
if (!data) return;
employee.value = data;
},
onError: (error) => {
if (isCancel(error)) return;
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
},
});
// 从后端获取的原始权限列表
const originalList = ref<NdmPermissionResultVO[]>([]);
// 当前用户配置的权限列表
const currentList = ref<NdmPermissionSaveOrResultVO[]>([]);
const { mutate: getPermissions, isPending: permissionsLoading } = useMutation({
mutationFn: async () => {
if (!employeeId.value) throw new Error('员工ID不能为空');
abortController.value.abort();
abortController.value = new AbortController();
const signal = abortController.value.signal;
const data = await pagePermissionApi(
{
model: {
employeeId: employeeId.value,
},
current: 1,
size: Object.keys(PERMISSION_TYPE_LITERALS).length * stations.value.length,
},
{ signal },
);
return data;
},
onSuccess: (data) => {
if (!data) return;
const { records } = data;
originalList.value = cloneDeep(records);
currentList.value = cloneDeep(records);
},
onError: (error) => {
if (isCancel(error)) return;
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
},
});
const onUpdatePermissionChecked = (checked: boolean, stationCode: Station['code'], permissionType: PermissionType) => {
if (!employeeId.value) return;
if (checked) {
const existed = currentList.value.some((permission) => permission.stationCode === stationCode && permission.type === permissionType);
if (!existed) {
const saveVO: NdmPermissionSaveVO = {
employeeId: employeeId.value,
stationCode,
type: permissionType,
};
currentList.value.push(saveVO);
}
} else {
const index = currentList.value.findIndex((permission) => permission.stationCode === stationCode && permission.type === permissionType);
if (index !== -1) {
currentList.value.splice(index, 1);
}
}
};
const tableColumns = computed<DataTableColumns<Station>>(() => {
return [
{
title: () => {
const permissionCount = currentList.value.length;
const permissionTypeCount = objectEntries(PERMISSION_TYPE_LITERALS).length;
const checked = permissionCount === stations.value.length * permissionTypeCount;
const indeterminate = permissionCount > 0 && permissionCount < stations.value.length * permissionTypeCount;
return h(NCheckbox, {
checked,
indeterminate,
onUpdateChecked: (checked) => {
objectEntries(PERMISSION_TYPE_LITERALS).forEach(([permissionType]) => {
stations.value.forEach((station) => {
onUpdatePermissionChecked(checked, station.code, permissionType);
});
});
},
});
},
key: 'row-check',
align: 'center',
width: 60,
fixed: 'left',
render: (rowData) => {
const { code: stationCode } = rowData;
const permissionTypeCount = objectEntries(PERMISSION_TYPE_LITERALS).length;
const stationCheckedPermissions = currentList.value.filter((permission) => permission.stationCode === stationCode);
const checked = stationCheckedPermissions.length === permissionTypeCount;
const indeterminate = stationCheckedPermissions.length > 0 && stationCheckedPermissions.length < permissionTypeCount;
return h(NCheckbox, {
checked,
indeterminate,
onUpdateChecked: (checked) => {
objectEntries(PERMISSION_TYPE_LITERALS).forEach(([permissionType]) => {
onUpdatePermissionChecked(checked, stationCode, permissionType);
});
},
});
},
},
{ title: '车站编号', key: 'code', align: 'center', width: 120 },
{ title: '车站名称', key: 'name', align: 'center', width: 360 },
// 「权限」列
...objectEntries(PERMISSION_TYPE_NAMES).map<DataTableColumn<Station>>(([permissionType, title]) => ({
title: () => {
const permissionCount = currentList.value.filter((permission) => permission.type === permissionType).length;
const checked = permissionCount === stations.value.length;
const indeterminate = permissionCount > 0 && permissionCount < stations.value.length;
return h(
NFlex,
{
justify: 'center',
align: 'center',
},
{
default: () => [
h(NCheckbox, {
checked,
indeterminate,
onUpdateChecked: (checked) => {
stations.value.forEach((station) => {
onUpdatePermissionChecked(checked, station.code, permissionType);
});
},
}),
h('span', title),
],
},
);
},
key: permissionType,
align: 'center',
render: (rowData) => {
const { code: stationCode } = rowData;
return h(NCheckbox, {
checked: currentList.value.some((permission) => permission.stationCode === stationCode && permission.type === permissionType),
onUpdateChecked: (checked) => onUpdatePermissionChecked(checked, stationCode, permissionType),
});
},
})),
];
});
const { mutate: savePermissions, isPending: permissionsSaving } = useMutation({
mutationFn: async () => {
if (!employeeId.value) throw new Error('员工ID不能为空');
abortController.value.abort();
abortController.value = new AbortController();
const signal = abortController.value.signal;
// 执行diff计算生成需要保存的权限列表和需要删除的权限ID列表
const saveList: NdmPermissionSaveVO[] = [];
const removeList: string[] = [];
// 遍历当前状态,如果权限不在原始权限列表中,说明是需要新增的权限
currentList.value.forEach((permission) => {
const { stationCode, type } = permission;
if (!stationCode || !type) return;
if (!originalList.value.some((permission) => permission.stationCode === stationCode && permission.type === type)) {
saveList.push({
employeeId: employeeId.value,
stationCode,
type,
});
}
});
// 遍历原始状态,如果权限不在当前状态中,说明是需要删除的权限
originalList.value.forEach((permission) => {
const { id, stationCode, type } = permission;
if (!id) return;
if (!currentList.value.some((permission) => permission.stationCode === stationCode && permission.type === type)) {
removeList.push(id);
}
});
await modifyPermissionApi(
{
employeeId: employeeId.value,
saveList,
removeList,
},
{
signal,
},
);
},
onSuccess: () => {
window.$message.success('权限配置保存成功');
getPermissions();
},
onError: (error) => {
if (isCancel(error)) return;
console.error(error);
const errorFeedback = parseErrorFeedback(error);
window.$message.error(errorFeedback);
},
});
const onAfterEnter = () => {
getEmployeeAsync().then(() => getPermissions());
};
const onAfterLeave = () => {
employee.value = undefined;
originalList.value = [];
currentList.value = [];
};
const onClose = () => {
abortController.value.abort();
show.value = false;
};
</script>
<template>
<NModal
v-model:show="show"
preset="card"
style="width: 100vw; height: 100vh"
:content-style="{ height: '100%', overflow: 'hidden' }"
:close-on-esc="false"
:mask-closable="false"
:auto-focus="false"
@after-enter="onAfterEnter"
@after-leave="onAfterLeave"
@close="onClose"
>
<template #header>
<span>{{ `配置权限 - ${employee?.realName ?? ''}` }}</span>
</template>
<template #default>
<NDataTable flex-height style="height: 100%" :columns="tableColumns" :data="stations" :loading="permissionsLoading" :single-line="false" />
</template>
<template #footer>
<NText depth="3" style="font-size: smaller">*未勾选任何权限的用户将被认为拥有所有权限</NText>
</template>
<template #action>
<NFlex justify="end">
<NButton size="small" @click="onClose">取消</NButton>
<NButton type="primary" size="small" :loading="permissionsSaving" @click="() => savePermissions()">保存</NButton>
</NFlex>
</template>
</NModal>
</template>
<style scoped lang="scss"></style>

View File

@@ -1,6 +1,7 @@
<script setup lang="ts">
import type { Station, StationAlarms, StationDevices } from '@/apis';
import { DEVICE_TYPE_LITERALS } from '@/enums';
import { usePermission } from '@/composables';
import { DEVICE_TYPE_LITERALS, PERMISSION_TYPE_LITERALS } from '@/enums';
import axios from 'axios';
import dayjs from 'dayjs';
import { isFunction } from 'es-toolkit';
@@ -24,6 +25,8 @@ const emit = defineEmits<{
clickConfig: [station: Station];
}>();
const { hasPermission } = usePermission();
const { station, devices, alarms, selectable } = toRefs(props);
const onlineDeviceCount = computed(() => {
@@ -71,7 +74,7 @@ const openDeviceConfigModal = () => {
emit('clickConfig', station.value);
};
const dropdownOptions: DropdownOption[] = [
const dropdownOptions = computed<DropdownOption[]>(() => [
{
label: '视频平台',
key: 'video-platform',
@@ -80,9 +83,10 @@ const dropdownOptions: DropdownOption[] = [
{
label: '设备配置',
key: 'device-config',
show: hasPermission(station.value.code, PERMISSION_TYPE_LITERALS.OPERATION),
onSelect: openDeviceConfigModal,
},
];
]);
const onSelectDropdownOption = (key: string, option: DropdownOption) => {
const onSelect = option['onSelect'];

View File

@@ -1,10 +1,9 @@
<script setup lang="ts">
import type { Station, SyncCameraResult } from '@/apis';
import { useStationStore } from '@/stores';
import { usePermissionStore } from '@/stores';
import { watchDebounced } from '@vueuse/core';
import { EditIcon, PlusCircleIcon, Trash2Icon } from 'lucide-vue-next';
import { NFlex, NIcon, NList, NListItem, NModal, NScrollbar, NStatistic, NText, NThing } from 'naive-ui';
import { storeToRefs } from 'pinia';
import { computed, ref, toRefs } from 'vue';
const props = defineProps<{
@@ -15,8 +14,8 @@ const emit = defineEmits<{
afterLeave: [];
}>();
const stationStore = useStationStore();
const { stations } = storeToRefs(stationStore);
const permissionStore = usePermissionStore();
const stations = computed(() => permissionStore.stations.VIEW ?? []);
const { syncCameraResult } = toRefs(props);

View File

@@ -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,7 +118,9 @@ export const useAlarmActionColumn = (tableData: Ref<DataTableRowData[]>) => {
default: () => '确认告警?',
},
),
tryGetDeviceType(rowData.deviceType) === DEVICE_TYPE_LITERALS.ndmCamera && [
tryGetDeviceType(rowData.deviceType) === DEVICE_TYPE_LITERALS.ndmCamera &&
rowData.stationCode &&
hasPermission(rowData.stationCode, PERMISSION_TYPE_LITERALS.OPERATION) && [
h(
NPopconfirm,
{

View File

@@ -1,5 +1,6 @@
export * from './alarm';
export * from './device';
export * from './permission';
export * from './query';
export * from './station';
export * from './stomp';

View File

@@ -0,0 +1 @@
export * from './use-permission';

View File

@@ -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,
};
};

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';

View File

@@ -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({

View File

@@ -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({

View File

@@ -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;
},
});

View File

@@ -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;
},
});
};

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,

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) => {

View File

@@ -1,5 +1,6 @@
export const LINE_ALARMS_QUERY_KEY = 'line-alarms';
export const LINE_DEVICES_QUERY_KEY = 'line-devices';
export const LINE_STATIONS_QUERY_KEY = 'line-stations';
export const USER_PERMISSION_QUERY_KEY = 'user-permission';
export const VERIFY_USER_QUERY_KEY = 'verify-user';
export const VERSION_CHECK_QUERY_KEY = 'version-check';

View File

@@ -1,3 +1,3 @@
export const ALARM_TOPIC = '/topic/deviceAlarm';
export const PERMISSION_TOPIC = '/topic/permission';
export const SYNC_CAMERA_STATUS_TOPIC = '/topic/syncCameraStatus';

View File

@@ -1,5 +1,6 @@
export const NDM_ALARM_STORE_ID = 'ndm-alarm-store';
export const NDM_DEVICE_STORE_ID = 'ndm-device-store';
export const NDM_PERMISSION_STORE_ID = 'ndm-permission-store';
export const NDM_POLLIING_STORE_ID = 'ndm-polling-store';
export const NDM_SETTING_STORE_ID = 'ndm-setting-store';
export const NDM_STATION_STORE_ID = 'ndm-station-store';

View File

@@ -1,3 +1,4 @@
export * from './alarm-type';
export * from './device-type';
export * from './fault-level';
export * from './permission-type';

View File

@@ -0,0 +1,13 @@
export const PERMISSION_TYPE_LITERALS = {
VIEW: 'VIEW',
OPERATION: 'OPERATION',
} as const;
export type PermissionType = keyof typeof PERMISSION_TYPE_LITERALS;
export const PERMISSION_TYPE_NAMES = {
[PERMISSION_TYPE_LITERALS.VIEW]: '查看',
[PERMISSION_TYPE_LITERALS.OPERATION]: '操作',
} as const;
export type PermissionTypeEnum = typeof PERMISSION_TYPE_NAMES;

View File

@@ -1,10 +1,10 @@
<script setup lang="ts">
import { SettingsDrawer, SyncCameraResultModal } from '@/components';
import { useLineStationsQuery, useStompClient, useVerifyUserQuery } from '@/composables';
import { useLineStationsQuery, useStompClient, useUserPermissionQuery, useVerifyUserQuery } from '@/composables';
import { LINE_ALARMS_QUERY_KEY, LINE_DEVICES_QUERY_KEY, LINE_STATIONS_MUTATION_KEY, LINE_STATIONS_QUERY_KEY, STATION_ALARMS_MUTATION_KEY, STATION_DEVICES_MUTATION_KEY } from '@/constants';
import { useSettingStore, useUnreadStore, useUserStore } from '@/stores';
import { useIsFetching, useIsMutating } from '@tanstack/vue-query';
import { ChevronDownIcon, ChevronsLeftIcon, ChevronsRightIcon, ComputerIcon, LogOutIcon, LogsIcon, MapPinIcon, SettingsIcon, SirenIcon } from 'lucide-vue-next';
import { ChevronDownIcon, ChevronsLeftIcon, ChevronsRightIcon, ComputerIcon, KeyRoundIcon, LogOutIcon, LogsIcon, MapPinIcon, SettingsIcon, SirenIcon } from 'lucide-vue-next';
import {
NBadge,
NButton,
@@ -29,7 +29,7 @@ const route = useRoute();
const router = useRouter();
const userStore = useUserStore();
const { userInfo } = storeToRefs(userStore);
const { userInfo, isLamp } = storeToRefs(userStore);
const unreadStore = useUnreadStore();
const { unreadAlarmCount } = storeToRefs(unreadStore);
@@ -41,6 +41,7 @@ const { syncCameraResult, afterCheckSyncCameraResult } = useStompClient();
useVerifyUserQuery();
useLineStationsQuery();
useUserPermissionQuery();
// 全局loading状态依赖于轮询query的queryKey以及相关的mutationKey
const queryingCount = useIsFetching({
@@ -63,7 +64,7 @@ const onToggleMenuCollapsed = () => {
menuCollpased.value = !menuCollpased.value;
};
const menuOptions: MenuOption[] = [
const menuOptions = computed<MenuOption[]>(() => [
{
label: () => h(RouterLink, { to: '/station' }, { default: () => '车站状态' }),
key: '/station',
@@ -104,7 +105,13 @@ const menuOptions: MenuOption[] = [
},
],
},
];
{
label: () => h(RouterLink, { to: '/permission' }, { default: () => '权限管理' }),
key: '/permission',
show: isLamp.value,
icon: renderIcon(KeyRoundIcon),
},
]);
const dropdownOptions: DropdownOption[] = [
{

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, useStationStore, 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>

View File

@@ -47,6 +47,15 @@ const router = createRouter({
},
],
},
{
path: 'permission',
component: () => import('@/pages/permission/permission-page.vue'),
beforeEnter: () => {
const userStore = useUserStore();
if (userStore.isLamp) return true;
return { path: '/404' };
},
},
{
path: '/:pathMatch(.*)*',
component: () => import('@/pages/error/not-found-page.vue'),

View File

@@ -1,5 +1,6 @@
export * from './alarm';
export * from './device';
export * from './permission';
export * from './setting';
export * from './station';
export * from './unread';

77
src/stores/permission.ts Normal file
View File

@@ -0,0 +1,77 @@
import type { NdmPermissionResultVO, Station } from '@/apis';
import { NDM_PERMISSION_STORE_ID } from '@/constants';
import { PERMISSION_TYPE_NAMES, type PermissionType } from '@/enums';
import { useStationStore } from '@/stores';
import { defineStore } from 'pinia';
import { computed, ref } from 'vue';
import { objectEntries } from '@vueuse/core';
type Permissions = Record<Station['code'], PermissionType[]>;
export const usePermissionStore = defineStore(
NDM_PERMISSION_STORE_ID,
() => {
const stationStore = useStationStore();
const permissionRecords = ref<NdmPermissionResultVO[] | null>(null);
const permissions = computed<Permissions>(() => {
const result: Permissions = {};
const records = permissionRecords.value;
// 如果权限记录不存在,则不做权限配置
if (!records) return result;
// 如果该用户没有任何权限记录,则开放所有权限,否则根据记录配置权限
if (records.length === 0) {
stationStore.stations.forEach((station) => {
result[station.code] = [...objectEntries(PERMISSION_TYPE_NAMES).map(([permType]) => permType)];
});
} else {
stationStore.stations.forEach((station) => {
result[station.code] = [];
const stationPermRecords = records.filter((record) => record.stationCode === station.code);
if (stationPermRecords.length === 0) return;
stationPermRecords.forEach(({ type: permType }) => {
if (!permType) return;
result[station.code]?.push(permType);
});
});
}
return result;
});
// 按权限对车站进行分类
const stations = computed(() => {
const result: Partial<Record<PermissionType, Station[]>> = {};
// 按原始的车站顺序进行遍历,保持显示顺序不变
stationStore.stations.forEach((station) => {
const permissionTypes = permissions.value[station.code];
if (!permissionTypes) return;
permissionTypes.forEach((permissionType) => {
if (!result[permissionType]) result[permissionType] = [];
result[permissionType].push(station);
});
});
return result;
});
const setPermRecords = (records: NdmPermissionResultVO[]) => {
permissionRecords.value = records;
};
return {
permissionRecords,
permissions,
stations,
setPermRecords,
};
},
{
persist: true,
},
);