Files
gao-customer-h5/src/pages/permission/RoleEdit.vue
T
2026-08-22 01:08:23 +08:00

531 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div v-can="pageAccess" class="role-edit">
<t-form 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="如 front-desk" 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-if="!isCreateMode" 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>
<template v-if="aggregatePermissionList.length">
<card-container 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>
</template>
</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 { Message } from "@/utils/message";
import { useUserStore } from "timi-web";
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 canEditRoleInfo = computed(() => userStore.hasPermissions(savePermissionCode.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
&& !isProtectedRole.value
));
const canSaveBaseInfo = computed(() => canEditRoleInfo.value);
const canShowSaveAction = computed(() => canSaveBaseInfo.value && userStore.hasPermissions(savePermissionCode.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) {
return;
}
await router.push(`/management/permissions/roles/${formModel.id}/assign/p`);
}
async function toRoleDelegationPage(): Promise<void> {
if (!formModel.id) {
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 = candidateRoleList.value;
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));
parentRoleOptions.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) || []
});
}
Object.assign(formModel, {
id: current.id,
moduleCode: current.moduleCode || moduleCode,
code: current.code,
name: current.name,
description: current.description,
parentRoleId: current.parentRoleId || "",
childRoleIdList
});
}
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;
}
onMounted(fillForm);
function updateNavBar(): void {
navBarStore.clearRight();
if (canShowSaveAction.value) {
navBarStore.setRightText("保存", "var(--td-brand-color)", saveBaseInfo);
}
}
watch(canShowSaveAction, updateNavBar, { immediate: true });
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>