refactor file tree

This commit is contained in:
Timi
2026-08-11 15:24:36 +08:00
parent 575d41b2e2
commit 819c1623eb
42 changed files with 86 additions and 86 deletions
+643
View File
@@ -0,0 +1,643 @@
<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="代码">
<t-input v-model.trim="formModel.code" placeholder="如 front-desk" borderless :disabled="!canEditRoleCode" />
<div v-if="isBuiltinRole" class="hint" v-text="'内建角色代码不可修改'" />
</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="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="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">
<section>
<card-title title="聚合权限 P" description="该角色拥有以下权限,可执行权限对应的功能,这些权限来自上级角色直接授权或子角色继承。" />
<t-cell-group class="permission-list" theme="card">
<t-cell
v-for="item in aggregatePermissionList"
:key="item.id"
:title="item.name"
>
<template #leftIcon>
<span class="source" :class="`source-${item.source}`" />
</template>
<template #note>
<span class="code" v-text="formatPermissionCode(item)" />
</template>
</t-cell>
</t-cell-group>
</section>
</template>
</div>
<t-popup v-model="parentRolePickerVisible" placement="bottom">
<t-picker
v-model="parentRolePickerDraft"
:columns="parentRolePickerColumns"
title="选择上级"
@cancel="cancelParentRolePicker"
@confirm="confirmParentRolePicker"
/>
</t-popup>
<t-popup v-model="childRolePickerVisible" placement="bottom">
<div class="role-picker bg-white">
<div class="picker-header">
<t-button variant="text" size="small" @click="cancelChildRolePicker">取消</t-button>
<span class="title" v-text="'继承角色'" />
<t-button theme="primary" variant="text" size="small" :loading="savingTarget === 'inherit'" @click="saveRoleInherit">保存</t-button>
</div>
<div class="picker-body">
<t-checkbox-group v-if="childOptions.length" v-model="childRolePickerDraft" class="group">
<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="暂无可继承角色" />
</div>
</div>
</t-popup>
</template>
<script setup lang="ts">
import { Toast } from "tdesign-mobile-vue";
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 { corePermissionCodes, coreRoleCodes } from "@/utils/permission";
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"
}));
});
function formatPermissionCode(permission: Permission): string {
return `${permission.moduleCode}:${permission.code}`;
}
// ---------- 选择器 ----------
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) {
Toast.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) {
Toast.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);
}
Toast.success(isCreateMode.value ? "角色已创建" : "基础信息已保存");
await router.push(`/management/permissions/roles/${role?.id || formModel.id}`);
} catch (error) {
await reloadAfterError();
Toast.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;
Toast.success("继承关系已保存");
await reloadCurrentRole();
} catch (error) {
await reloadAfterError();
childRolePickerDraft.value = [...formModel.childRoleIdList];
Toast.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 getErrorMessage(error: unknown, fallback: string): string {
if (error instanceof Error && error.message) {
return error.message;
}
if (typeof error === "string" && error) {
return error;
}
return fallback;
}
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%;
}
.permission-list {
--source-direct-color: #2563EB;
--source-inherited-color: #14B8A6;
:deep(.t-cell) {
&__left {
display: flex;
align-items: center;
}
&__title {
flex: 1 1 0;
min-width: 0;
}
&__title-text {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
&__note {
flex: 0 1 auto;
min-width: 0;
max-width: 56%;
text-align: right;
justify-content: flex-end;
}
}
.code {
width: 100%;
display: block;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.source {
width: 1rem;
height: 1rem;
display: block;
border-radius: .25rem;
&-direct {
background: var(--source-direct-color);
}
&-inherited {
background: var(--source-inherited-color);
}
}
}
.picker-text {
color: var(--td-text-color-primary);
line-height: 1.5rem;
}
.group {
--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;
}
}
}
.role-picker {
display: flex;
height: 70vh;
padding-top: 1rem;
border-radius: 1.25rem 1.25rem 0 0;
flex-direction: column;
.picker-header {
padding: 0 1rem;
display: grid;
grid-template-columns: 4rem 1fr 4rem;
align-items: center;
.title {
text-align: center;
}
}
.picker-body {
flex: 1;
overflow: auto;
min-height: 0;
padding: .75rem 1rem env(safe-area-inset-bottom, 0);
}
.group {
background: #fff;
border-radius: .75rem;
}
}
:global(html.theme-dark) {
.role-edit {
.block {
.title .hint {
color: #94a3b8;
}
.group {
background: #0f172a;
}
}
.code {
color: #94a3b8;
}
.permission-list {
.source {
&-direct {
background: #60a5fa;
}
&-inherited {
background: #2dd4bf;
}
}
}
}
.role-picker {
background: #0f172a;
.group {
background: #0f172a;
}
}
}
</style>