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

@@ -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,25 +303,27 @@ const onDrawerAfterLeave = () => {
<NInputNumber v-model:value="stationGridCols" :min="1" :max="10" />
</NFormItem>
<NDivider>告警</NDivider>
<NFormItem label="告警画面截图保留天数" label-placement="left">
<NFlex justify="space-between" align="center" style="width: 100%">
<NInputNumber v-model:value="retentionDays" :min="1" :max="15" />
<NButtonGroup>
<NButton secondary size="small" :disabled="retentionDaysSaving" :loading="retentionDaysLoading" @click="() => getRetentionDays()">刷新</NButton>
<NButton secondary size="small" :disabled="retentionDaysLoading" :loading="retentionDaysSaving" @click="() => saveRetentionDays()">保存</NButton>
</NButtonGroup>
</NFlex>
</NFormItem>
<NFormItem label="自动获取告警画面截图" label-placement="left">
<NFlex justify="space-between" align="center" style="width: 100%">
<NSwitch size="small" v-model:value="snapStatus" />
<NButtonGroup>
<NButton secondary size="small" :disabled="snapStatusSaving" :loading="snapStatusLoading" @click="() => getSnapStatus()">刷新</NButton>
<NButton secondary size="small" :disabled="snapStatusLoading" :loading="snapStatusSaving" @click="() => saveSnapStatus()">保存</NButton>
</NButtonGroup>
</NFlex>
</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%">
<NInputNumber v-model:value="retentionDays" :min="1" :max="15" />
<NButtonGroup>
<NButton secondary size="small" :disabled="retentionDaysSaving" :loading="retentionDaysLoading" @click="() => getRetentionDays()">刷新</NButton>
<NButton secondary size="small" :disabled="retentionDaysLoading" :loading="retentionDaysSaving" @click="() => saveRetentionDays()">保存</NButton>
</NButtonGroup>
</NFlex>
</NFormItem>
<NFormItem label="自动获取告警画面截图" label-placement="left">
<NFlex justify="space-between" align="center" style="width: 100%">
<NSwitch size="small" v-model:value="snapStatus" />
<NButtonGroup>
<NButton secondary size="small" :disabled="snapStatusSaving" :loading="snapStatusLoading" @click="() => getSnapStatus()">刷新</NButton>
<NButton secondary size="small" :disabled="snapStatusLoading" :loading="snapStatusSaving" @click="() => saveSnapStatus()">保存</NButton>
</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);