feat: 添加权限配置页面
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
export * from './device';
|
||||
export * from './global';
|
||||
export * from './permission';
|
||||
export * from './station';
|
||||
|
||||
1
src/components/permission/index.ts
Normal file
1
src/components/permission/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './permission-config-modal';
|
||||
@@ -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 };
|
||||
@@ -0,0 +1,263 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
deletePermissionApi,
|
||||
detailBaseEmployeeApi,
|
||||
pagePermissionApi,
|
||||
savePermissionApi,
|
||||
type BaseEmployeeResultVO,
|
||||
type NdmPermissionResultVO,
|
||||
type NdmPermissionSaveVO,
|
||||
type Station,
|
||||
} from '@/apis';
|
||||
import { 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, groupBy } 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';
|
||||
|
||||
/**
|
||||
* 为避免修改权限记录而带来复杂的状态管理,引入 `action` 字段
|
||||
*
|
||||
* - `save`:新增权限
|
||||
* - `delete`:删除权限
|
||||
*
|
||||
* 修改权限的过程中会对本地的权限列表进行修改:
|
||||
* - 当勾选权限时,在本地新增一条权限,`action` 为 `save`
|
||||
* - 当取消勾选权限时,如果该权限来自服务器,则将`action` 改为 `delete`,否则直接删除权限记录
|
||||
*
|
||||
* 最后,保存权限时,只需要遍历本地权限列表:
|
||||
* - 如果 `action` 为 `save`,则调用 `savePermissionApi` 新增权限
|
||||
* - 如果 `action` 为 `delete`,则调用 `deletePermissionApi` 删除权限
|
||||
*/
|
||||
type PermissionAction = 'save' | 'delete';
|
||||
|
||||
type NdmPermissionSaveOrResultVO = NdmPermissionSaveVO | NdmPermissionResultVO;
|
||||
|
||||
type NdmPermissionExtendedSaveVO = NdmPermissionSaveVO & { action?: PermissionAction };
|
||||
|
||||
type NdmPermissionExtendedResultVO = NdmPermissionResultVO & { action?: PermissionAction };
|
||||
|
||||
type NdmPermissionExtendedSaveOrResultVO = NdmPermissionExtendedSaveVO | NdmPermissionExtendedResultVO;
|
||||
|
||||
const omitActionField = (extendedSaveOrResultVO: NdmPermissionExtendedSaveOrResultVO) => {
|
||||
const copy = cloneDeep(extendedSaveOrResultVO);
|
||||
delete copy.action;
|
||||
const saveOrResultVO = copy as NdmPermissionSaveOrResultVO;
|
||||
return saveOrResultVO;
|
||||
};
|
||||
|
||||
const isSaveVO = (saveOrResultVO: NdmPermissionSaveOrResultVO): saveOrResultVO is NdmPermissionSaveVO => {
|
||||
return !('id' in saveOrResultVO);
|
||||
};
|
||||
|
||||
const isResultVO = (saveOrResultVO: NdmPermissionSaveOrResultVO): saveOrResultVO is NdmPermissionResultVO => {
|
||||
return 'id' in saveOrResultVO;
|
||||
};
|
||||
|
||||
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 permissions = ref<Record<Station['code'], NdmPermissionExtendedSaveOrResultVO[]>>({});
|
||||
const { mutate: getPermissions, isPending: permissionsLoading } = useMutation({
|
||||
mutationFn: async () => {
|
||||
abortController.value.abort();
|
||||
abortController.value = new AbortController();
|
||||
|
||||
if (!employeeId.value) return;
|
||||
|
||||
const signal = abortController.value.signal;
|
||||
|
||||
const data = await pagePermissionApi(
|
||||
{
|
||||
model: {
|
||||
employeeId: employeeId.value,
|
||||
},
|
||||
extra: {},
|
||||
current: 1,
|
||||
size: Object.keys(PERMISSION_TYPE_NAMES).length * stations.value.length,
|
||||
sort: 'id',
|
||||
order: 'ascending',
|
||||
},
|
||||
{ signal },
|
||||
);
|
||||
|
||||
return data;
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
if (!data) return;
|
||||
const { records } = data;
|
||||
permissions.value = groupBy(records, (record) => record.stationCode ?? '');
|
||||
},
|
||||
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) {
|
||||
// 勾选时,新增一条权限记录,`action` 为 `save`
|
||||
if (!permissions.value[stationCode]) permissions.value[stationCode] = [];
|
||||
permissions.value[stationCode].push({ employeeId: employeeId.value, stationCode, type: permissionType, action: 'save' });
|
||||
} else {
|
||||
if (!permissions.value[stationCode]) return;
|
||||
// 取消勾选时,先找到所有该类型的权限并遍历,
|
||||
// 如果权限来自数据库(即有 `id` 字段),则将其 `action` 设为 `delete`,
|
||||
// 否则直接从权限记录中移除
|
||||
const targets = permissions.value[stationCode].filter((permission) => permission.type === permissionType);
|
||||
for (const permission of targets) {
|
||||
if (isResultVO(permission)) {
|
||||
permission.action = 'delete';
|
||||
} else {
|
||||
permissions.value[stationCode].splice(permissions.value[stationCode].indexOf(permission), 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const tableColumns = computed<DataTableColumns<Station>>(() => {
|
||||
return [
|
||||
{ title: '车站编号', key: 'code', align: 'center', width: 120 },
|
||||
{ title: '车站名称', key: 'name', align: 'center', width: 360 },
|
||||
// 「权限」列
|
||||
...objectEntries(PERMISSION_TYPE_NAMES).map<DataTableColumn<Station>>(([permissionType, title]) => ({
|
||||
title: title,
|
||||
key: permissionType,
|
||||
align: 'center',
|
||||
render: (rowData) => {
|
||||
const { code: stationCode } = rowData;
|
||||
return h(NCheckbox, {
|
||||
// 如果权限记录中存在该权限且 `action` 不为 `delete`,则处于勾选状态
|
||||
checked: !!(permissions.value[stationCode] ?? []).find((permission) => permission.type === permissionType && permission.action !== 'delete'),
|
||||
// 改变勾选状态
|
||||
onUpdateChecked: (checked) => onUpdatePermissionChecked(checked, stationCode, permissionType),
|
||||
});
|
||||
},
|
||||
})),
|
||||
];
|
||||
});
|
||||
|
||||
const { mutate: savePermissions, isPending: permissionsSaving } = useMutation({
|
||||
mutationFn: async () => {
|
||||
abortController.value.abort();
|
||||
abortController.value = new AbortController();
|
||||
|
||||
const signal = abortController.value.signal;
|
||||
|
||||
// 遍历所有的权限记录,根据 `action` 进行保存或删除
|
||||
for (const stationPermissions of Object.values(permissions.value)) {
|
||||
for (const permission of stationPermissions) {
|
||||
const saveOrResultVO = omitActionField(permission);
|
||||
if (permission.action === 'save' && isSaveVO(saveOrResultVO)) {
|
||||
await savePermissionApi(saveOrResultVO, { signal });
|
||||
}
|
||||
if (permission.action === 'delete' && isResultVO(saveOrResultVO)) {
|
||||
const id = saveOrResultVO.id;
|
||||
if (!id) continue;
|
||||
await deletePermissionApi([id], { 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;
|
||||
permissions.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>
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './alarm-type';
|
||||
export * from './device-type';
|
||||
export * from './fault-level';
|
||||
export * from './permission-type';
|
||||
|
||||
13
src/enums/permission-type.ts
Normal file
13
src/enums/permission-type.ts
Normal 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;
|
||||
@@ -6,7 +6,7 @@ import { useAlarmStore, useSettingStore, useUserStore } from '@/stores';
|
||||
import { parseErrorFeedback } from '@/utils';
|
||||
import { useIsFetching, useIsMutating, useMutation } from '@tanstack/vue-query';
|
||||
import { isCancel } from 'axios';
|
||||
import { ChevronDownIcon, ChevronsLeftIcon, ChevronsRightIcon, ComputerIcon, LogOutIcon, LogsIcon, MapPinIcon, SettingsIcon, SirenIcon } from 'lucide-vue-next';
|
||||
import { ChevronDownIcon, ChevronsLeftIcon, ChevronsRightIcon, ComputerIcon, KeyIcon, LogOutIcon, LogsIcon, MapPinIcon, SettingsIcon, SirenIcon } from 'lucide-vue-next';
|
||||
import {
|
||||
NBadge,
|
||||
NButton,
|
||||
@@ -106,6 +106,12 @@ const menuOptions: MenuOption[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: () => h(RouterLink, { to: '/permission' }, { default: () => '权限管理' }),
|
||||
key: '/permission',
|
||||
show: userStore.isLamp,
|
||||
icon: renderIcon(KeyIcon),
|
||||
},
|
||||
];
|
||||
|
||||
const dropdownOptions: DropdownOption[] = [
|
||||
|
||||
184
src/pages/permission-page.vue
Normal file
184
src/pages/permission-page.vue
Normal 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>
|
||||
@@ -47,6 +47,15 @@ const router = createRouter({
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'permission',
|
||||
component: () => import('@/pages/permission-page.vue'),
|
||||
beforeEnter: () => {
|
||||
const userStore = useUserStore();
|
||||
if (userStore.isLamp) return true;
|
||||
return { path: '/404' };
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
component: () => import('@/pages/not-found-page.vue'),
|
||||
|
||||
Reference in New Issue
Block a user