541 lines
17 KiB
Vue
541 lines
17 KiB
Vue
<template>
|
||
<div v-can="pageAccess" class="role-edit">
|
||
<t-form v-entrance-transition class="form" label-align="left">
|
||
<t-form-item label="名称">
|
||
<t-input v-model.trim="formModel.name" placeholder="角色名称" borderless :disabled="!canEditRoleInfo" />
|
||
</t-form-item>
|
||
<t-form-item label="代码" :help="isBuiltinRole ? '内建角色代码不可修改' : ''">
|
||
<t-input v-model.trim="formModel.code" placeholder="如 ROLE_CODE" borderless :disabled="!canEditRoleCode" />
|
||
</t-form-item>
|
||
<t-form-item label="模块">
|
||
<t-input v-model.trim="formModel.moduleCode" placeholder="模块代码" borderless disabled />
|
||
</t-form-item>
|
||
<t-form-item
|
||
label="上级"
|
||
:arrow="canEditParentRole"
|
||
@click.stop="openParentRolePicker"
|
||
>
|
||
<div class="picker-text" v-text="selectedParentRoleName" />
|
||
</t-form-item>
|
||
<t-form-item label="说明">
|
||
<t-textarea v-model.trim="formModel.description" class="textarea" placeholder="角色说明" autosize borderless :disabled="!canEditRoleInfo" />
|
||
</t-form-item>
|
||
<t-form-item
|
||
label="继承角色"
|
||
:arrow="canEditRoleRelation"
|
||
@click.stop="openChildRolePicker"
|
||
>
|
||
<div class="picker-text" v-text="selectedChildRoleName" />
|
||
</t-form-item>
|
||
</t-form>
|
||
<t-cell-group v-entrance-transition="70" v-if="!isCreateMode && canManageRole" title="权限授权" theme="card">
|
||
<t-cell
|
||
v-can="rolePermissionReadAccess"
|
||
title="直接授权权限 P"
|
||
arrow
|
||
@click="toRolePermissionPage"
|
||
>
|
||
<template #leftIcon>
|
||
<t-icon name="secured" size="1.5rem" />
|
||
</template>
|
||
</t-cell>
|
||
<t-cell
|
||
v-can="rolePermissionReadAccess"
|
||
title="可转授权限 D"
|
||
arrow
|
||
@click="toRoleDelegationPage"
|
||
>
|
||
<template #leftIcon>
|
||
<t-icon name="user-checked" size="1.5rem" />
|
||
</template>
|
||
</t-cell>
|
||
</t-cell-group>
|
||
<card-container v-entrance-transition="140" v-if="aggregatePermissionList.length" title="聚合权限 P">
|
||
<template #description>
|
||
<div>该角色拥有以下权限,可执行权限对应的功能,这些权限来自上级角色直接授权或子角色继承。</div>
|
||
<permission-legend />
|
||
</template>
|
||
<t-cell-group class="permission-list" theme="card">
|
||
<permission-cell
|
||
v-for="item in aggregatePermissionList"
|
||
:key="item.id"
|
||
:permission="item"
|
||
:source="item.source"
|
||
/>
|
||
</t-cell-group>
|
||
</card-container>
|
||
</div>
|
||
<t-popup v-model="parentRolePickerVisible" placement="bottom">
|
||
<t-picker
|
||
v-model="parentRolePickerDraft"
|
||
:columns="parentRolePickerColumns"
|
||
title="选择上级"
|
||
@cancel="cancelParentRolePicker"
|
||
@confirm="confirmParentRolePicker"
|
||
/>
|
||
</t-popup>
|
||
<bottom-popup
|
||
v-model="childRolePickerVisible"
|
||
title="继承角色"
|
||
left-button-text="取消"
|
||
right-button-text="保存"
|
||
@leftButtonClick="cancelChildRolePicker"
|
||
@rightButtonClick="saveRoleInherit"
|
||
>
|
||
<t-checkbox-group v-if="childOptions.length" v-model="childRolePickerDraft" class="role-options">
|
||
<t-checkbox
|
||
v-for="item in childOptions"
|
||
:key="item.id"
|
||
class="item"
|
||
:value="item.id"
|
||
:label="item.name || item.code"
|
||
icon="line"
|
||
borderless
|
||
/>
|
||
</t-checkbox-group>
|
||
<t-empty v-else description="暂无可继承角色" />
|
||
</bottom-popup>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { BottomPopup } from "timi-tdesign-mobile";
|
||
import { Message } from "@/utils/message";
|
||
import { useUserStore } from "timi-web-api";
|
||
import { useCurrentStoreStore } from "@/store/currentStoreStore";
|
||
import { useNavBarStore } from "@/store/navBarStore";
|
||
import {
|
||
moduleCode,
|
||
type Permission,
|
||
type Role,
|
||
type RoleDraft,
|
||
usePermissionSettingsStore
|
||
} from "@/store/permissionSettingsStore";
|
||
import { getErrorMessage } from "@/utils/error";
|
||
import { corePermissionCodes, coreRoleCodes } from "@/utils/permission";
|
||
import PermissionCell from "@/pages/permission/component/PermissionCell.vue";
|
||
import PermissionLegend from "@/pages/permission/component/PermissionLegend.vue";
|
||
|
||
defineOptions({
|
||
name: "RoleEdit"
|
||
});
|
||
|
||
const route = useRoute();
|
||
const router = useRouter();
|
||
const navBarStore = useNavBarStore();
|
||
const permissionStore = usePermissionSettingsStore();
|
||
const currentStoreStore = useCurrentStoreStore();
|
||
const userStore = useUserStore();
|
||
|
||
// ---------- 路由权限 ----------
|
||
|
||
const routeId = computed(() => String(route.params.id || ""));
|
||
const isCreateMode = computed(() => !routeId.value);
|
||
const savePermissionCode = computed(() => isCreateMode.value ? corePermissionCodes.role.create : corePermissionCodes.role.update);
|
||
const pageAccess = computed(() => ({
|
||
value: isCreateMode.value
|
||
? [corePermissionCodes.role.read, corePermissionCodes.role.create]
|
||
: [corePermissionCodes.role.read],
|
||
mode: "and" as const
|
||
}));
|
||
|
||
const rolePermissionReadAccess = {
|
||
value: [
|
||
corePermissionCodes.role.read,
|
||
corePermissionCodes.permission.read,
|
||
corePermissionCodes.rolePermission.read
|
||
],
|
||
mode: "and"
|
||
} as const;
|
||
|
||
// ---------- 表单 ----------
|
||
|
||
const formModel = reactive({
|
||
id: "",
|
||
moduleCode: moduleCode,
|
||
code: "",
|
||
name: "",
|
||
description: "",
|
||
parentRoleId: "",
|
||
childRoleIdList: [] as string[]
|
||
});
|
||
|
||
const currentRole = ref<Role>();
|
||
const candidateRoleList = ref<Role[]>([]);
|
||
const inheritanceRoleList = ref<Role[]>([]);
|
||
const parentRoleOptions = ref<Role[]>([]);
|
||
const savingTarget = ref("");
|
||
|
||
const isSystemAdmin = computed(() => userStore.hasRoles(coreRoleCodes.admin));
|
||
const isBuiltinRole = computed(() => currentRole.value?.builtin === true);
|
||
const isProtectedRole = computed(() => !isSystemAdmin.value && currentRole.value?.protectedRole === true);
|
||
const canManageRole = computed(() => isCreateMode.value || Boolean(currentRole.value && candidateRoleList.value.some(item => item.id === formModel.id)));
|
||
const isCurrentUserDirectRole = computed(() => {
|
||
const current = currentRole.value;
|
||
return Boolean(current?.moduleCode && current.code && userStore.hasRoles(`${current.moduleCode}:${current.code}`));
|
||
});
|
||
const hasSavePermission = computed(() => userStore.hasPermissions(savePermissionCode.value));
|
||
const canEditRoleInfo = computed(() => hasSavePermission.value && (isCreateMode.value || (
|
||
canManageRole.value
|
||
&& !isProtectedRole.value
|
||
)));
|
||
const canEditRoleCode = computed(() => canEditRoleInfo.value && !isBuiltinRole.value);
|
||
const canEditParentRole = computed(() => canEditRoleInfo.value && !isBuiltinRole.value);
|
||
const canUpdateRoleRelation = computed(() => userStore.hasPermissions([
|
||
corePermissionCodes.rolePermissionInherit.create,
|
||
corePermissionCodes.rolePermissionInherit.delete
|
||
], "and"));
|
||
const canEditRoleRelation = computed(() => !isCreateMode.value && canUpdateRoleRelation.value && (
|
||
(canManageRole.value || isCurrentUserDirectRole.value)
|
||
&& !isProtectedRole.value
|
||
));
|
||
const canSaveBaseInfo = computed(() => canEditRoleInfo.value);
|
||
type PermissionSource = "direct" | "inherited";
|
||
type PermissionWithSource = Permission & {
|
||
source: PermissionSource;
|
||
};
|
||
|
||
const aggregatePermissionList = computed<PermissionWithSource[]>(() => {
|
||
const current = currentRole.value;
|
||
const directPermissionIdList = current?.permissionList?.length
|
||
? current.permissionList.map(item => item.id)
|
||
: current?.permissionIdList || [];
|
||
const directPermissionIdSet = new Set(directPermissionIdList);
|
||
return (current?.allPermissionList || []).map(permission => ({
|
||
...permission,
|
||
source: directPermissionIdSet.has(permission.id) ? "direct" : "inherited"
|
||
}));
|
||
});
|
||
|
||
// ---------- 选择器 ----------
|
||
type PickerValue = string | number;
|
||
const parentRolePickerVisible = ref(false);
|
||
const parentRolePickerDraft = ref<PickerValue[]>([]);
|
||
const childRolePickerVisible = ref(false);
|
||
const childRolePickerDraft = ref<string[]>([]);
|
||
|
||
const parentRolePickerColumns = computed(() => [parentRoleOptions.value
|
||
.map(item => ({
|
||
label: item.name || item.code,
|
||
value: item.id
|
||
}))
|
||
.filter(item => item.value)]);
|
||
const selectedParentRoleName = computed(() => {
|
||
const parentRole = parentRoleOptions.value.find(item => item.id === formModel.parentRoleId);
|
||
return parentRole?.name || parentRole?.code || (formModel.parentRoleId ? "已选择上级" : "未设置上级");
|
||
});
|
||
const selectedChildRoleName = computed(() => {
|
||
const nameList = childOptions.value
|
||
.filter(item => formModel.childRoleIdList.includes(item.id))
|
||
.map(item => item.name || item.code);
|
||
return nameList.length ? nameList.join("、") : "未设置继承角色";
|
||
});
|
||
|
||
function openParentRolePicker(): void {
|
||
if (!canEditParentRole.value) {
|
||
return;
|
||
}
|
||
if (!parentRoleOptions.value.length) {
|
||
Message.warning("暂无可选上级");
|
||
return;
|
||
}
|
||
parentRolePickerDraft.value = [formModel.parentRoleId];
|
||
parentRolePickerVisible.value = true;
|
||
}
|
||
|
||
function cancelParentRolePicker(): void {
|
||
parentRolePickerDraft.value = [formModel.parentRoleId];
|
||
parentRolePickerVisible.value = false;
|
||
}
|
||
|
||
function confirmParentRolePicker(): void {
|
||
const parentRoleId = String(parentRolePickerDraft.value[0] || "");
|
||
if (!parentRoleId) {
|
||
return;
|
||
}
|
||
|
||
formModel.parentRoleId = parentRoleId;
|
||
parentRolePickerVisible.value = false;
|
||
}
|
||
|
||
function openChildRolePicker(): void {
|
||
if (!canEditRoleRelation.value) {
|
||
return;
|
||
}
|
||
childRolePickerDraft.value = [...formModel.childRoleIdList];
|
||
childRolePickerVisible.value = true;
|
||
}
|
||
|
||
function cancelChildRolePicker(): void {
|
||
childRolePickerDraft.value = [...formModel.childRoleIdList];
|
||
childRolePickerVisible.value = false;
|
||
}
|
||
|
||
// ---------- 角色选项 ----------
|
||
|
||
const childOptions = computed(() => inheritanceRoleList.value.filter(item => item.id !== formModel.id
|
||
&& isManagementChildRole(item.id, formModel.id)
|
||
&& !hasChildRole(item.id, formModel.id)));
|
||
|
||
function hasChildRole(roleId: string, targetRoleId: string, visitedRoleIds = new Set<string>()): boolean {
|
||
if (visitedRoleIds.has(roleId)) {
|
||
return false;
|
||
}
|
||
|
||
const current = inheritanceRoleList.value.find(item => item.id === roleId);
|
||
if (!current) {
|
||
return false;
|
||
}
|
||
|
||
visitedRoleIds.add(roleId);
|
||
return current.childRoleIdList.some(childRoleId => childRoleId === targetRoleId || hasChildRole(childRoleId, targetRoleId, visitedRoleIds));
|
||
}
|
||
|
||
function isManagementChildRole(roleId: string, parentRoleId: string, visitedRoleIds = new Set<string>()): boolean {
|
||
if (!roleId || !parentRoleId || visitedRoleIds.has(roleId)) {
|
||
return false;
|
||
}
|
||
|
||
const current = inheritanceRoleList.value.find(item => item.id === roleId);
|
||
if (!current?.parentRoleId) {
|
||
return false;
|
||
}
|
||
|
||
if (current.parentRoleId === parentRoleId) {
|
||
return true;
|
||
}
|
||
|
||
visitedRoleIds.add(roleId);
|
||
return isManagementChildRole(current.parentRoleId, parentRoleId, visitedRoleIds);
|
||
}
|
||
|
||
// ---------- 页面跳转 ----------
|
||
|
||
async function toRolePermissionPage(): Promise<void> {
|
||
if (!formModel.id || !canManageRole.value) {
|
||
return;
|
||
}
|
||
await router.push(`/management/permissions/roles/${formModel.id}/assign/p`);
|
||
}
|
||
|
||
async function toRoleDelegationPage(): Promise<void> {
|
||
if (!formModel.id || !canManageRole.value) {
|
||
return;
|
||
}
|
||
await router.push(`/management/permissions/roles/${formModel.id}/assign/d`);
|
||
}
|
||
|
||
// ---------- 页面数据 ----------
|
||
|
||
async function fillForm(): Promise<void> {
|
||
await permissionStore.loadAll();
|
||
await loadAuthorizationScope();
|
||
if (isCreateMode.value) {
|
||
parentRoleOptions.value = await permissionStore.loadCreateParentGrantableRoles();
|
||
formModel.parentRoleId = parentRoleOptions.value[0]?.id || "";
|
||
return;
|
||
}
|
||
|
||
await reloadCurrentRole();
|
||
}
|
||
|
||
async function reloadCurrentRole(): Promise<void> {
|
||
const current = await permissionStore.loadRoleDetail(routeId.value || formModel.id);
|
||
currentRole.value = current.id ? current : undefined;
|
||
const childOptionIdSet = new Set(inheritanceRoleList.value
|
||
.filter(item => item.id !== current.id && isManagementChildRole(item.id, current.id) && !hasChildRole(item.id, current.id))
|
||
.map(item => item.id));
|
||
const childRoleIdList = current.childRoleIdList.filter(id => childOptionIdSet.has(id));
|
||
Object.assign(formModel, {
|
||
id: current.id,
|
||
moduleCode: current.moduleCode || moduleCode,
|
||
code: current.code,
|
||
name: current.name,
|
||
description: current.description,
|
||
parentRoleId: current.parentRoleId || "",
|
||
childRoleIdList
|
||
});
|
||
parentRoleOptions.value = canEditParentRole.value
|
||
? await permissionStore.loadParentGrantableRoles(current.id)
|
||
: [];
|
||
if (current.parentRole?.id && !parentRoleOptions.value.some(item => item.id === current.parentRoleId)) {
|
||
parentRoleOptions.value.unshift({
|
||
...current.parentRole,
|
||
id: current.parentRole.id,
|
||
permissionIdList: current.parentRole.permissionIdList || current.parentRole.permissionList?.map(item => item.id) || [],
|
||
childRoleIdList: current.parentRole.childRoleIdList || current.parentRole.childRoleList?.map(item => item.id).filter((id): id is string => Boolean(id)) || [],
|
||
delegationPermissionIdList: current.parentRole.delegationPermissionIdList || current.parentRole.delegationPermissionList?.map(item => item.id) || []
|
||
});
|
||
}
|
||
}
|
||
|
||
async function loadAuthorizationScope(): Promise<void> {
|
||
candidateRoleList.value = isSystemAdmin.value ? permissionStore.roleList : await permissionStore.loadManageableRoles();
|
||
inheritanceRoleList.value = mergeRoleList(permissionStore.roleList, candidateRoleList.value);
|
||
}
|
||
|
||
function mergeRoleList(...roleLists: Role[][]): Role[] {
|
||
const roleMap = new Map<string, Role>();
|
||
for (const role of roleLists.flat()) {
|
||
roleMap.set(role.id, role);
|
||
}
|
||
return Array.from(roleMap.values());
|
||
}
|
||
|
||
// ---------- 保存 ----------
|
||
|
||
async function saveBaseInfo(): Promise<void> {
|
||
if (!canSaveBaseInfo.value || savingTarget.value) {
|
||
return;
|
||
}
|
||
|
||
savingTarget.value = "base";
|
||
try {
|
||
if (isCreateMode.value && !formModel.parentRoleId) {
|
||
Message.warning("请选择管理父级");
|
||
return;
|
||
}
|
||
|
||
const draft: RoleDraft = {
|
||
id: formModel.id,
|
||
moduleCode: moduleCode,
|
||
code: formModel.code,
|
||
name: formModel.name,
|
||
description: formModel.description,
|
||
parentRoleId: isCreateMode.value ? formModel.parentRoleId : undefined,
|
||
ownerType: isCreateMode.value ? "STORE" : undefined,
|
||
ownerId: isCreateMode.value ? await getCurrentStoreId() : undefined,
|
||
childRoleIdList: isCreateMode.value ? [] : undefined
|
||
};
|
||
const role = await permissionStore.upsertRole(draft, isCreateMode.value);
|
||
if (!isCreateMode.value && formModel.parentRoleId !== currentRole.value?.parentRoleId) {
|
||
await permissionStore.setRoleParentRole(formModel.id, formModel.parentRoleId);
|
||
}
|
||
Message.success(isCreateMode.value ? "角色已创建" : "基础信息已保存");
|
||
await router.push(`/management/permissions/roles/${role?.id || formModel.id}`);
|
||
} catch (error) {
|
||
await reloadAfterError();
|
||
Message.error(getErrorMessage(error, "角色保存失败"));
|
||
} finally {
|
||
savingTarget.value = "";
|
||
}
|
||
}
|
||
|
||
async function saveRoleInherit(): Promise<void> {
|
||
if (!formModel.id || !canEditRoleRelation.value || savingTarget.value) {
|
||
return;
|
||
}
|
||
|
||
savingTarget.value = "inherit";
|
||
try {
|
||
const childRoleIdList = [...childRolePickerDraft.value];
|
||
await permissionStore.setRoleChildRoleList(formModel.id, childRoleIdList);
|
||
formModel.childRoleIdList = childRoleIdList;
|
||
childRolePickerVisible.value = false;
|
||
Message.success("继承关系已保存");
|
||
await reloadCurrentRole();
|
||
} catch (error) {
|
||
await reloadAfterError();
|
||
childRolePickerDraft.value = [...formModel.childRoleIdList];
|
||
Message.error(getErrorMessage(error, "继承关系保存失败"));
|
||
} finally {
|
||
savingTarget.value = "";
|
||
}
|
||
}
|
||
|
||
async function reloadAfterError(): Promise<void> {
|
||
await loadAuthorizationScope();
|
||
if (formModel.id || routeId.value) {
|
||
await reloadCurrentRole();
|
||
}
|
||
}
|
||
|
||
async function getCurrentStoreId(): Promise<string> {
|
||
const store = await currentStoreStore.loadLoginStore();
|
||
const storeId = store?.id || currentStoreStore.currentStoreId;
|
||
if (!storeId) {
|
||
throw new Error("未找到当前门店,无法创建门店角色");
|
||
}
|
||
return storeId;
|
||
}
|
||
|
||
function updateNavBar(): void {
|
||
navBarStore.clearRight();
|
||
if (!hasSavePermission.value) {
|
||
return;
|
||
}
|
||
|
||
navBarStore.setRightText("保存", "var(--td-brand-color)", saveBaseInfo);
|
||
navBarStore.setRightDisabled(!canSaveBaseInfo.value);
|
||
}
|
||
|
||
watch([hasSavePermission, canSaveBaseInfo], updateNavBar, { immediate: true });
|
||
|
||
onMounted(async () => {
|
||
await fillForm();
|
||
updateNavBar();
|
||
});
|
||
|
||
onUnmounted(() => navBarStore.clearRight());
|
||
</script>
|
||
|
||
<style scoped lang="less">
|
||
.role-edit {
|
||
gap: 1rem;
|
||
display: flex;
|
||
padding: calc(var(--app-nav-height) + 1rem) 0 2rem 0;
|
||
flex-direction: column;
|
||
|
||
.form {
|
||
border-radius: .75rem;
|
||
}
|
||
|
||
.textarea {
|
||
width: 100%;
|
||
}
|
||
|
||
.picker-text {
|
||
color: var(--td-text-color-primary);
|
||
line-height: 1.5rem;
|
||
cursor: pointer;
|
||
}
|
||
}
|
||
|
||
.role-options {
|
||
--td-checkbox-vertical-padding: 10px;
|
||
|
||
background: #fff;
|
||
border-radius: .75rem;
|
||
|
||
.item:not(:last-child)::after {
|
||
left: 16px;
|
||
right: 0;
|
||
bottom: 0;
|
||
content: ' ';
|
||
position: absolute;
|
||
box-sizing: border-box;
|
||
pointer-events: none;
|
||
border-bottom: 1px solid var(--td-component-stroke, var(--td-gray-color-3, #e7e7e7));
|
||
-webkit-box-sizing: border-box;
|
||
}
|
||
}
|
||
|
||
:global(html.theme-dark) {
|
||
.role-edit {
|
||
.block {
|
||
.title .hint {
|
||
color: #94a3b8;
|
||
}
|
||
|
||
.group {
|
||
background: #0f172a;
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
.role-options {
|
||
background: #0f172a;
|
||
}
|
||
}
|
||
</style>
|