refactor file tree
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<div v-can="pageAccess" class="permission-edit">
|
||||
<t-form class="form" label-align="left">
|
||||
<t-form-item label="名称">
|
||||
<t-input v-model.trim="formModel.name" placeholder="权限名称" borderless />
|
||||
</t-form-item>
|
||||
<t-form-item label="代码">
|
||||
<t-input v-model.trim="formModel.code" placeholder="如 customer:read" borderless />
|
||||
</t-form-item>
|
||||
<t-form-item label="模块">
|
||||
<t-input v-model.trim="formModel.moduleCode" placeholder="模块代码" borderless />
|
||||
</t-form-item>
|
||||
<t-form-item label="说明">
|
||||
<t-textarea v-model.trim="formModel.description" class="textarea" placeholder="权限说明" autosize borderless />
|
||||
</t-form-item>
|
||||
</t-form>
|
||||
<t-button v-can="savePermissionCode" class="save" theme="primary" @click="savePermission">保存</t-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Toast } from "tdesign-mobile-vue";
|
||||
import { moduleCode, usePermissionSettingsStore } from "@/store/permissionSettingsStore";
|
||||
import { corePermissionCodes } from "@/utils/permission";
|
||||
|
||||
defineOptions({
|
||||
name: "PermissionEdit"
|
||||
});
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const permissionStore = usePermissionSettingsStore();
|
||||
|
||||
// ---------- 路由权限 ----------
|
||||
|
||||
const routeId = computed(() => String(route.params.id || ""));
|
||||
const isCreateMode = computed(() => !routeId.value);
|
||||
const savePermissionCode = computed(() => isCreateMode.value ? corePermissionCodes.permission.create : corePermissionCodes.permission.update);
|
||||
const pageAccess = computed(() => ({
|
||||
value: [corePermissionCodes.role.read, corePermissionCodes.permission.read, savePermissionCode.value],
|
||||
mode: "and" as const
|
||||
}));
|
||||
|
||||
// ---------- 表单 ----------
|
||||
|
||||
const formModel = reactive({
|
||||
id: "",
|
||||
moduleCode: moduleCode,
|
||||
code: "",
|
||||
name: "",
|
||||
description: ""
|
||||
});
|
||||
|
||||
// ---------- 页面数据 ----------
|
||||
|
||||
async function fillForm(): Promise<void> {
|
||||
await permissionStore.loadAll();
|
||||
const current = await permissionStore.loadPermissionDetail(routeId.value);
|
||||
Object.assign(formModel, {
|
||||
id: current.id,
|
||||
moduleCode: current.moduleCode || moduleCode,
|
||||
code: current.code,
|
||||
name: current.name,
|
||||
description: current.description
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- 保存 ----------
|
||||
|
||||
async function savePermission(): Promise<void> {
|
||||
const permission = await permissionStore.upsertPermission({
|
||||
id: formModel.id,
|
||||
moduleCode: moduleCode,
|
||||
code: formModel.code,
|
||||
name: formModel.name,
|
||||
description: formModel.description
|
||||
}, isCreateMode.value);
|
||||
Toast.success("权限已保存");
|
||||
await router.push(isCreateMode.value ? "/management/permissions/items" : `/management/permissions/items/${permission?.id || formModel.id}`);
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await fillForm();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.permission-edit {
|
||||
padding: calc(var(--app-nav-height) + 1rem) 0 5rem 0;
|
||||
|
||||
.form {
|
||||
border-radius: .75rem;
|
||||
}
|
||||
|
||||
.textarea {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.save {
|
||||
left: 1rem;
|
||||
right: 1rem;
|
||||
bottom: calc(.75rem + env(safe-area-inset-bottom, 0));
|
||||
position: fixed;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
<template>
|
||||
<div v-can="{ value: [corePermissionCodes.role.read, corePermissionCodes.permission.read], mode: 'and' }" class="permission-list">
|
||||
<t-pull-down-refresh
|
||||
class="list-wrapper"
|
||||
v-model="isRefreshing"
|
||||
:refresh-timeout="30000"
|
||||
@refresh="refresh"
|
||||
>
|
||||
<div class="content">
|
||||
<t-cell-group class="header" theme="card">
|
||||
<t-cell title="当前账号角色聚合权限 P">
|
||||
<template #description>
|
||||
<div class="description">
|
||||
<div>你拥有以下权限,可执行权限对应的功能,这些权限来自上级角色直接授权或子角色继承。</div>
|
||||
<div class="legends">
|
||||
<div class="legend">
|
||||
<span class="block direct"></span>
|
||||
<span>上级角色直接授权</span>
|
||||
</div>
|
||||
<div class="legend">
|
||||
<span class="block inherited"></span>
|
||||
<span>子角色继承授权</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</t-cell>
|
||||
</t-cell-group>
|
||||
<t-cell-group v-if="sortedPermissions.length" theme="card">
|
||||
<t-swipe-cell
|
||||
v-for="item in sortedPermissions"
|
||||
:key="item.id"
|
||||
class="item"
|
||||
>
|
||||
<t-cell :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-swipe-cell>
|
||||
</t-cell-group>
|
||||
<t-empty v-else-if="!permissionStore.loading && !isSourceLoading" description="暂无权限" />
|
||||
</div>
|
||||
</t-pull-down-refresh>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { type Role as UserRole, RoleAPI, useUserStore } from "timi-web";
|
||||
import type { Permission } from "@/store/permissionSettingsStore";
|
||||
import { moduleCode, usePermissionSettingsStore } from "@/store/permissionSettingsStore";
|
||||
import { corePermissionCodes } from "@/utils/permission";
|
||||
|
||||
defineOptions({
|
||||
name: "PermissionList"
|
||||
});
|
||||
|
||||
const permissionStore = usePermissionSettingsStore();
|
||||
const userStore = useUserStore();
|
||||
const isRefreshing = ref(false);
|
||||
const isSourceLoading = ref(false);
|
||||
|
||||
// ---------- 列表数据 ----------
|
||||
|
||||
type PermissionSource = "direct" | "inherited" | "unknown";
|
||||
type PermissionWithSource = Permission & {
|
||||
source: PermissionSource;
|
||||
};
|
||||
|
||||
const sourceRoleList = ref<UserRole[]>([]);
|
||||
const sortedPermissions = computed<PermissionWithSource[]>(() => {
|
||||
const permissionMap = new Map<string, PermissionWithSource>();
|
||||
for (const role of sourceRoleList.value) {
|
||||
for (const permission of getRoleAggregatePermissions(role)) {
|
||||
permissionMap.set(permission.id, {
|
||||
...permission,
|
||||
source: getPermissionSource(permission.id)
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const permission of getLoginUserPermissions()) {
|
||||
if (!permissionMap.has(permission.id)) {
|
||||
permissionMap.set(permission.id, {
|
||||
...permission,
|
||||
source: "unknown"
|
||||
});
|
||||
}
|
||||
}
|
||||
return Array.from(permissionMap.values()).sort((left, right) => formatPermissionCode(left).localeCompare(formatPermissionCode(right)));
|
||||
});
|
||||
|
||||
function getRoleAggregatePermissions(role: UserRole): Permission[] {
|
||||
if (role.allPermissionList?.length) {
|
||||
return role.allPermissionList;
|
||||
}
|
||||
if (role.permissionList?.length) {
|
||||
return role.permissionList;
|
||||
}
|
||||
return (role.permissionIdList || []).map(permissionStore.getPermissionById).filter(Boolean) as Permission[];
|
||||
}
|
||||
|
||||
function getLoginUserPermissions(): Permission[] {
|
||||
const permissionCodeSet = new Set(userStore.getPermissionList());
|
||||
return permissionStore.permissionList.filter(item => permissionCodeSet.has(formatPermissionCode(item)));
|
||||
}
|
||||
|
||||
function getPermissionSource(permissionId: string): PermissionSource {
|
||||
if (sourceRoleList.value.some(role => getDirectPermissionIdSet(role).has(permissionId))) {
|
||||
return "direct";
|
||||
}
|
||||
if (sourceRoleList.value.some(role => role.allPermissionList?.some(permission => permission.id === permissionId))) {
|
||||
return "inherited";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
function getDirectPermissionIdSet(role: UserRole): Set<string> {
|
||||
if (role.permissionList?.length) {
|
||||
return new Set(role.permissionList.map(item => item.id));
|
||||
}
|
||||
return new Set(role.permissionIdList || []);
|
||||
}
|
||||
|
||||
function formatPermissionCode(permission: Permission): string {
|
||||
return `${permission.moduleCode}:${permission.code}`;
|
||||
}
|
||||
|
||||
// ---------- 页面数据 ----------
|
||||
|
||||
async function refresh(): Promise<void> {
|
||||
isRefreshing.value = true;
|
||||
try {
|
||||
await permissionStore.loadAll(true);
|
||||
await loadSourceRoles();
|
||||
} finally {
|
||||
isRefreshing.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await permissionStore.loadAll();
|
||||
await loadSourceRoles();
|
||||
});
|
||||
|
||||
async function loadSourceRoles(): Promise<void> {
|
||||
isSourceLoading.value = true;
|
||||
try {
|
||||
sourceRoleList.value = await RoleAPI.currentDetailList(moduleCode);
|
||||
} finally {
|
||||
isSourceLoading.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.permission-list {
|
||||
--source-direct-color: #2563EB;
|
||||
--source-inherited-color: #14B8A6;
|
||||
--source-unknown-color: #94A3B8;
|
||||
|
||||
padding: calc(var(--app-nav-height) + 1rem) 0 1rem 0;
|
||||
|
||||
.list-wrapper {
|
||||
|
||||
.content {
|
||||
gap: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.header {
|
||||
|
||||
.description {
|
||||
gap: .5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.legends {
|
||||
gap: 1.5rem;
|
||||
display: flex;
|
||||
|
||||
.legend {
|
||||
gap: .25rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.block {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
display: block;
|
||||
border-radius: .25rem;
|
||||
|
||||
&.direct {
|
||||
background: var(--source-direct-color);
|
||||
}
|
||||
|
||||
&.inherited {
|
||||
background: var(--source-inherited-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item {
|
||||
|
||||
&:last-child {
|
||||
|
||||
:deep(.t-cell:after) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
: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;
|
||||
}
|
||||
|
||||
.source {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
display: block;
|
||||
border-radius: .25rem;
|
||||
|
||||
&-direct {
|
||||
background: var(--source-direct-color);
|
||||
}
|
||||
|
||||
&-inherited {
|
||||
background: var(--source-inherited-color);
|
||||
}
|
||||
|
||||
&-unknown {
|
||||
background: var(--source-unknown-color);
|
||||
}
|
||||
}
|
||||
|
||||
.code {
|
||||
width: 100%;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:global(html.theme-dark) {
|
||||
.permission-list {
|
||||
.list-wrapper {
|
||||
.content {
|
||||
.item {
|
||||
:deep(.t-cell) {
|
||||
.source {
|
||||
&-direct {
|
||||
background: #60a5fa;
|
||||
}
|
||||
|
||||
&-inherited {
|
||||
background: #2dd4bf;
|
||||
}
|
||||
|
||||
&-unknown {
|
||||
background: #64748b;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
<template>
|
||||
<div
|
||||
v-can="{ value: [corePermissionCodes.role.read, corePermissionCodes.permission.read], mode: 'and' }"
|
||||
class="permission-settings"
|
||||
>
|
||||
<t-cell-group title="权限配置" theme="card">
|
||||
<t-cell
|
||||
v-can="corePermissionCodes.role.read"
|
||||
title="角色管理"
|
||||
:note="`${roleCount} 个角色`"
|
||||
arrow
|
||||
@click="router.push('/management/permissions/roles')"
|
||||
>
|
||||
<template #leftIcon>
|
||||
<t-icon name="user-setting" size="1.75rem" />
|
||||
</template>
|
||||
</t-cell>
|
||||
<t-cell
|
||||
v-can="corePermissionCodes.permission.read"
|
||||
title="权限管理"
|
||||
:note="`${permissionCount} 个权限`"
|
||||
arrow
|
||||
@click="router.push('/management/permissions/items')"
|
||||
>
|
||||
<template #leftIcon>
|
||||
<t-icon name="secured" size="1.75rem" />
|
||||
</template>
|
||||
</t-cell>
|
||||
<t-cell
|
||||
v-can="rolePermissionAssignAccess"
|
||||
title="角色直接授权 P"
|
||||
arrow
|
||||
@click="router.push('/management/permissions/assign/p')"
|
||||
>
|
||||
<template #leftIcon>
|
||||
<t-icon name="secured" size="1.75rem" />
|
||||
</template>
|
||||
</t-cell>
|
||||
<t-cell
|
||||
v-can="rolePermissionAssignAccess"
|
||||
title="角色转授权限 D"
|
||||
arrow
|
||||
@click="router.push('/management/permissions/assign/d')"
|
||||
>
|
||||
<template #leftIcon>
|
||||
<t-icon name="user-checked" size="1.75rem" />
|
||||
</template>
|
||||
</t-cell>
|
||||
<t-cell
|
||||
v-can="corePermissionCodes.userRole.read"
|
||||
title="用户角色授权"
|
||||
arrow
|
||||
@click="router.push('/management/permissions/user-roles')"
|
||||
>
|
||||
<template #leftIcon>
|
||||
<t-icon name="usergroup" size="1.75rem" />
|
||||
</template>
|
||||
</t-cell>
|
||||
</t-cell-group>
|
||||
<t-cell-group title="当前角色结构" theme="card">
|
||||
<t-cell
|
||||
v-can="{ value: [corePermissionCodes.role.read, corePermissionCodes.permission.read], mode: 'and' }"
|
||||
v-for="item in visibleRoleTree"
|
||||
:key="item.id"
|
||||
:arrow="canAssignRole(item)"
|
||||
:hover="canAssignRole(item)"
|
||||
@click="canAssignRole(item) && toAssign(item.id)"
|
||||
>
|
||||
<template #title>
|
||||
<div class="title" v-text="item.name" :style="{ paddingLeft: `${item.level * 1.25}rem` }" />
|
||||
</template>
|
||||
</t-cell>
|
||||
</t-cell-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useUserStore } from "timi-web";
|
||||
import type { Role } from "@/store/permissionSettingsStore";
|
||||
import { usePermissionSettingsStore } from "@/store/permissionSettingsStore";
|
||||
import { corePermissionCodes, coreRoleCodes } from "@/utils/permission";
|
||||
|
||||
defineOptions({
|
||||
name: "PermissionSettings"
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
const permissionStore = usePermissionSettingsStore();
|
||||
const authorizationRoleList = ref<Role[]>([]);
|
||||
const isSystemAdmin = computed(() => userStore.hasRoles(coreRoleCodes.admin));
|
||||
const roleCount = computed(() => authorizationRoleList.value.length);
|
||||
const permissionCount = computed(() => permissionStore.permissionList.length);
|
||||
type RoleTreeItem = Pick<Role, "id" | "name" | "parentRoleId" | "protectedRole"> & { level: number };
|
||||
|
||||
const visibleRoleTree = computed<RoleTreeItem[]>(() => {
|
||||
if (isSystemAdmin.value) {
|
||||
return permissionStore.roleTree;
|
||||
}
|
||||
|
||||
const roleMap = new Map(authorizationRoleList.value.map(item => [item.id, item]));
|
||||
const roleIdSet = new Set(authorizationRoleList.value.map(item => item.id));
|
||||
const roots = authorizationRoleList.value.filter(item => !item.parentRoleId || !roleIdSet.has(item.parentRoleId));
|
||||
const buildTree = (roleId: string, level: number, visitedRoleIds: Set<string>): RoleTreeItem[] => {
|
||||
const role = roleMap.get(roleId);
|
||||
if (!role || visitedRoleIds.has(roleId)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const nextVisitedRoleIds = new Set(visitedRoleIds);
|
||||
nextVisitedRoleIds.add(roleId);
|
||||
const childRoleIdList = authorizationRoleList.value
|
||||
.filter(item => item.parentRoleId === roleId)
|
||||
.map(item => item.id);
|
||||
return [{ ...role, level }, ...childRoleIdList.flatMap(childRoleId => buildTree(childRoleId, level + 1, nextVisitedRoleIds))];
|
||||
};
|
||||
return roots.flatMap(item => buildTree(item.id, 0, new Set<string>()));
|
||||
});
|
||||
|
||||
// ---------- 权限入口 ----------
|
||||
|
||||
const rolePermissionAssignAccess = {
|
||||
value: [
|
||||
corePermissionCodes.role.read,
|
||||
corePermissionCodes.permission.read,
|
||||
corePermissionCodes.rolePermission.read
|
||||
],
|
||||
mode: "and"
|
||||
} as const;
|
||||
|
||||
async function toAssign(roleId: string): Promise<void> {
|
||||
await router.push({
|
||||
path: "/management/permissions/assign/p",
|
||||
query: {
|
||||
roleId
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function canAssignRole(role: Pick<Role, "protectedRole">): boolean {
|
||||
return isSystemAdmin.value || role.protectedRole !== true;
|
||||
}
|
||||
|
||||
// ---------- 页面数据 ----------
|
||||
|
||||
onMounted(async () => {
|
||||
if (isSystemAdmin.value) {
|
||||
await permissionStore.loadAll();
|
||||
authorizationRoleList.value = permissionStore.roleList;
|
||||
return;
|
||||
}
|
||||
|
||||
await permissionStore.loadAll();
|
||||
const roleList = await permissionStore.loadManageableRoles();
|
||||
authorizationRoleList.value = roleList;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.permission-settings {
|
||||
gap: 1rem;
|
||||
padding: calc(var(--app-nav-height) + 1rem) 0 1rem 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.struct {
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
padding: 0 var(--td-cell-group-title-padding-left, 16px);
|
||||
font-size: var(--td-cell-group-title-font-size, 14px);
|
||||
text-align: left;
|
||||
background: var(--td-cell-group-title-bg-color, var(--td-bg-color-secondarycontainer, var(--td-gray-color-1, #f3f3f3)));
|
||||
line-height: var(--td-cell-group-title-line-height, 45px);
|
||||
justify-content: space-between;
|
||||
|
||||
.title {
|
||||
color: var(--td-cell-group-title-color, var(--td-text-color-placeholder, var(--td-font-gray-3, rgba(0, 0, 0, 0.4))));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,277 @@
|
||||
<template>
|
||||
<div
|
||||
v-can="{ value: [corePermissionCodes.role.read, corePermissionCodes.permission.read, corePermissionCodes.rolePermission.read], mode: 'and' }"
|
||||
class="role-delegation-permission-assign"
|
||||
>
|
||||
<div class="header bg-white">
|
||||
<t-dropdown-menu class="role-selector">
|
||||
<t-dropdown-item
|
||||
:label="activeRoleLabel"
|
||||
:options="roleOptions"
|
||||
:value="activeRoleId"
|
||||
borderless
|
||||
@change="handleRoleChange"
|
||||
/>
|
||||
</t-dropdown-menu>
|
||||
</div>
|
||||
<t-cell-group v-if="activeRole && grantablePermissionList.length" title="可配置的转授权限 D" theme="card">
|
||||
<t-cell
|
||||
v-for="item in grantablePermissionList"
|
||||
:key="item.id"
|
||||
:title="item.name || item.code"
|
||||
>
|
||||
<template #description>
|
||||
<span class="code" v-text="item.code"></span>
|
||||
</template>
|
||||
<template #right-icon>
|
||||
<t-switch
|
||||
:disabled="!canUpdateRolePermission || !canEditActiveRole"
|
||||
:model-value="isPermissionChecked(item.id)"
|
||||
@change="value => setPermissionChecked(item.id, value === true)"
|
||||
/>
|
||||
</template>
|
||||
</t-cell>
|
||||
</t-cell-group>
|
||||
<t-empty v-else-if="!permissionStore.loading" description="暂无可配置的转授权限 D" />
|
||||
<t-button
|
||||
v-if="canEditActiveRole"
|
||||
v-can="rolePermissionWriteAccess"
|
||||
class="save"
|
||||
theme="primary"
|
||||
@click="savePermissionList"
|
||||
>
|
||||
保存 D
|
||||
</t-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { DropdownOption, DropdownValue } from "tdesign-mobile-vue";
|
||||
import { Toast } from "tdesign-mobile-vue";
|
||||
import { type Permission, useUserStore } from "timi-web";
|
||||
import { type Role, usePermissionSettingsStore } from "@/store/permissionSettingsStore";
|
||||
import { corePermissionCodes, coreRoleCodes } from "@/utils/permission";
|
||||
|
||||
defineOptions({
|
||||
name: "RoleDelegationPermissionAssign"
|
||||
});
|
||||
|
||||
const permissionStore = usePermissionSettingsStore();
|
||||
const userStore = useUserStore();
|
||||
const route = useRoute();
|
||||
|
||||
// ---------- 角色选择 ----------
|
||||
|
||||
const activeRoleId = ref(String(route.query.roleId || route.params.id || ""));
|
||||
const checkedPermissionIds = ref<string[]>([]);
|
||||
const unmanagedPermissionIds = ref<string[]>([]);
|
||||
const grantablePermissionList = ref<Permission[]>([]);
|
||||
const candidateRoleList = ref<Role[]>([]);
|
||||
let permissionSyncSeq = 0;
|
||||
|
||||
const activeRole = computed(() => permissionStore.getRoleById(activeRoleId.value));
|
||||
const activeRoleLabel = computed(() => activeRole.value ? `当前角色:${activeRole.value.name}` : "选择角色");
|
||||
const canUpdateRolePermission = computed(() => userStore.hasPermissions([
|
||||
corePermissionCodes.rolePermission.create,
|
||||
corePermissionCodes.rolePermission.delete
|
||||
], "and"));
|
||||
const isSystemAdmin = computed(() => userStore.hasRoles(coreRoleCodes.admin));
|
||||
const canEditActiveRole = computed(() => Boolean(activeRole.value)
|
||||
&& candidateRoleList.value.some(item => item.id === activeRoleId.value)
|
||||
&& (isSystemAdmin.value || activeRole.value?.protectedRole !== true));
|
||||
const roleOptions = computed<DropdownOption[]>(() => candidateRoleList.value
|
||||
.filter(item => !!item.id)
|
||||
.map(item => ({
|
||||
label: item.name || item.code,
|
||||
value: item.id,
|
||||
disabled: false
|
||||
})));
|
||||
|
||||
const rolePermissionWriteAccess = {
|
||||
value: [
|
||||
corePermissionCodes.rolePermission.create,
|
||||
corePermissionCodes.rolePermission.delete
|
||||
],
|
||||
mode: "and"
|
||||
} as const;
|
||||
|
||||
// ---------- 权限状态 ----------
|
||||
|
||||
const checkedPermissionIdSet = computed(() => new Set(checkedPermissionIds.value));
|
||||
|
||||
// ---------- 角色切换 ----------
|
||||
|
||||
async function handleRoleChange(value: DropdownValue): Promise<void> {
|
||||
activeRoleId.value = Array.isArray(value) ? String(value[0] || "") : String(value);
|
||||
await loadActiveRolePermissions();
|
||||
}
|
||||
|
||||
async function loadActiveRolePermissions(): Promise<void> {
|
||||
const roleId = activeRoleId.value;
|
||||
if (!roleId) {
|
||||
clearPermissionState();
|
||||
return;
|
||||
}
|
||||
|
||||
const currentSeq = permissionSyncSeq + 1;
|
||||
permissionSyncSeq = currentSeq;
|
||||
const [role, permissionList] = await Promise.all([
|
||||
permissionStore.loadRoleDetail(roleId),
|
||||
permissionStore.loadGrantableDelegationPermissions(roleId)
|
||||
]);
|
||||
if (currentSeq !== permissionSyncSeq || activeRoleId.value !== roleId) {
|
||||
return;
|
||||
}
|
||||
|
||||
grantablePermissionList.value = permissionList;
|
||||
const grantablePermissionIdSet = new Set(grantablePermissionList.value.map(item => item.id));
|
||||
checkedPermissionIds.value = role.delegationPermissionIdList.filter(id => grantablePermissionIdSet.has(id));
|
||||
unmanagedPermissionIds.value = role.delegationPermissionIdList.filter(id => !grantablePermissionIdSet.has(id));
|
||||
}
|
||||
|
||||
// ---------- 保存 ----------
|
||||
|
||||
async function savePermissionList(): Promise<void> {
|
||||
if (!activeRoleId.value || !canEditActiveRole.value || !canUpdateRolePermission.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await permissionStore.setRoleDelegationPermissionList(activeRoleId.value, Array.from(new Set([
|
||||
...checkedPermissionIds.value,
|
||||
...unmanagedPermissionIds.value
|
||||
])));
|
||||
Toast.success("D 已保存");
|
||||
await loadActiveRolePermissions();
|
||||
} catch (error) {
|
||||
clearPermissionState();
|
||||
try {
|
||||
await loadAuthorizationScope();
|
||||
await loadActiveRolePermissions();
|
||||
} finally {
|
||||
Toast.error(getErrorMessage(error, "D 保存失败"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 权限勾选 ----------
|
||||
|
||||
function isPermissionChecked(permissionId: string): boolean {
|
||||
return checkedPermissionIdSet.value.has(permissionId);
|
||||
}
|
||||
|
||||
function setPermissionChecked(permissionId: string, value: boolean): void {
|
||||
if (!canUpdateRolePermission.value || !canEditActiveRole.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (value && !checkedPermissionIds.value.includes(permissionId)) {
|
||||
checkedPermissionIds.value.push(permissionId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
checkedPermissionIds.value = checkedPermissionIds.value.filter(item => item !== permissionId);
|
||||
}
|
||||
}
|
||||
|
||||
function clearPermissionState(): void {
|
||||
checkedPermissionIds.value = [];
|
||||
unmanagedPermissionIds.value = [];
|
||||
grantablePermissionList.value = [];
|
||||
}
|
||||
|
||||
// ---------- 授权范围 ----------
|
||||
|
||||
async function loadAuthorizationScope(): Promise<void> {
|
||||
const roleList = isSystemAdmin.value ? permissionStore.roleList : await permissionStore.loadManageableRoles();
|
||||
candidateRoleList.value = roleList;
|
||||
}
|
||||
|
||||
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(async () => {
|
||||
await permissionStore.loadAll();
|
||||
await loadAuthorizationScope();
|
||||
if (!activeRoleId.value) {
|
||||
activeRoleId.value = candidateRoleList.value[0]?.id || "";
|
||||
}
|
||||
await loadActiveRolePermissions();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.role-delegation-permission-assign {
|
||||
gap: 1rem;
|
||||
display: flex;
|
||||
padding: calc(var(--app-nav-height) + 4.5rem) 0 6rem 0;
|
||||
flex-direction: column;
|
||||
|
||||
.header {
|
||||
top: var(--app-nav-height);
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 20;
|
||||
display: flex;
|
||||
position: fixed;
|
||||
background: #fff;
|
||||
box-shadow: var(--td-shadow-1);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.role-selector::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.t-cell__title) {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
:deep(.t-cell__title-text) {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
:deep(.t-cell__description) {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
:deep(.t-cell__right) {
|
||||
flex: 0 0 auto;
|
||||
margin-left: .75rem;
|
||||
}
|
||||
|
||||
.code {
|
||||
width: 100%;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.save {
|
||||
left: 1rem;
|
||||
right: 1rem;
|
||||
bottom: calc(.75rem + env(safe-area-inset-bottom, 0));
|
||||
position: fixed;
|
||||
}
|
||||
}
|
||||
|
||||
:global(html.theme-dark) {
|
||||
.role-delegation-permission-assign {
|
||||
.header {
|
||||
background: #0f172a;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,201 @@
|
||||
<template>
|
||||
<div v-can="{ value: [corePermissionCodes.role.read, corePermissionCodes.permission.read], mode: 'and' }" class="role-list">
|
||||
<t-pull-down-refresh
|
||||
class="list-wrapper"
|
||||
v-model="isRefreshing"
|
||||
:refresh-timeout="30000"
|
||||
@refresh="refresh"
|
||||
>
|
||||
<t-cell-group v-if="sortedRoles.length" theme="card">
|
||||
<t-swipe-cell
|
||||
v-for="item in sortedRoles"
|
||||
:key="item.id"
|
||||
class="item"
|
||||
>
|
||||
<t-cell
|
||||
:title="item.name"
|
||||
:note="item.code"
|
||||
arrow
|
||||
hover
|
||||
@click="router.push(`/management/permissions/roles/${item.id}`)"
|
||||
>
|
||||
<template #title>
|
||||
<div class="title" v-text="item.name" :style="{ paddingLeft: `${item.level * 1.25}rem` }" />
|
||||
</template>
|
||||
</t-cell>
|
||||
<template #right>
|
||||
<t-button v-if="canDeleteRole(item)" v-can="corePermissionCodes.role.delete" class="delete" theme="danger" @click="removeRole(item.id)">删除</t-button>
|
||||
</template>
|
||||
</t-swipe-cell>
|
||||
</t-cell-group>
|
||||
<t-empty v-else-if="!permissionStore.loading" description="暂无角色" />
|
||||
</t-pull-down-refresh>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Toast } from "tdesign-mobile-vue";
|
||||
import { useUserStore } from "timi-web";
|
||||
import type { Role } from "@/store/permissionSettingsStore";
|
||||
import { useNavBarStore } from "@/store/navBarStore";
|
||||
import { usePermissionSettingsStore } from "@/store/permissionSettingsStore";
|
||||
import { corePermissionCodes, coreRoleCodes } from "@/utils/permission";
|
||||
|
||||
defineOptions({
|
||||
name: "RoleList"
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
const navBarStore = useNavBarStore();
|
||||
const permissionStore = usePermissionSettingsStore();
|
||||
const userStore = useUserStore();
|
||||
const isRefreshing = ref(false);
|
||||
const deletableRoleIdSet = ref(new Set<string>());
|
||||
|
||||
// ---------- 顶部导航 ----------
|
||||
|
||||
const canCreateRole = computed(() => userStore.hasPermissions(corePermissionCodes.role.create));
|
||||
|
||||
async function toCreate(): Promise<void> {
|
||||
if (!canCreateRole.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
await router.push("/management/permissions/roles/create");
|
||||
}
|
||||
|
||||
// ---------- 列表数据 ----------
|
||||
|
||||
const displayRoleList = ref<Role[]>([]);
|
||||
const sortedRoles = computed<Array<Role & { level: number }>>(() => {
|
||||
if (userStore.hasRoles(coreRoleCodes.admin)) {
|
||||
return permissionStore.roleTree;
|
||||
}
|
||||
return buildManageableRoleTree();
|
||||
});
|
||||
|
||||
async function removeRole(id: string): Promise<void> {
|
||||
try {
|
||||
await permissionStore.removeRole(id);
|
||||
} catch (error) {
|
||||
await refreshAuthorizationScope();
|
||||
await permissionStore.loadAll(true);
|
||||
Toast.error(getErrorMessage(error, "角色删除失败"));
|
||||
}
|
||||
}
|
||||
|
||||
function canDeleteRole(role: Role): boolean {
|
||||
if (userStore.hasRoles(coreRoleCodes.admin)) {
|
||||
return true;
|
||||
}
|
||||
return role.builtin !== true && role.protectedRole !== true && deletableRoleIdSet.value.has(role.id);
|
||||
}
|
||||
|
||||
// ---------- 页面数据 ----------
|
||||
|
||||
async function refresh(): Promise<void> {
|
||||
isRefreshing.value = true;
|
||||
try {
|
||||
await permissionStore.loadAll(true);
|
||||
displayRoleList.value = getDisplayRoleList();
|
||||
await refreshAuthorizationScope();
|
||||
} finally {
|
||||
isRefreshing.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
navBarStore.clearRight();
|
||||
if (canCreateRole.value) {
|
||||
navBarStore.setRightText("添加", "var(--td-brand-color)", toCreate);
|
||||
}
|
||||
if (userStore.hasRoles(coreRoleCodes.admin)) {
|
||||
await permissionStore.loadAll();
|
||||
}
|
||||
await refreshAuthorizationScope();
|
||||
if (!displayRoleList.value.length) {
|
||||
displayRoleList.value = getDisplayRoleList();
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
navBarStore.clearRight();
|
||||
});
|
||||
|
||||
function getDisplayRoleList(): Role[] {
|
||||
return userStore.hasRoles(coreRoleCodes.admin) ? permissionStore.roleList : permissionStore.manageableRoleList;
|
||||
}
|
||||
|
||||
function buildManageableRoleTree(): Array<Role & { level: number }> {
|
||||
const roleIdSet = new Set(displayRoleList.value.map(item => item.id));
|
||||
const roleMap = new Map(displayRoleList.value.map(item => [item.id, item]));
|
||||
const roots = displayRoleList.value.filter(item => !item.parentRoleId || !roleIdSet.has(item.parentRoleId));
|
||||
const buildTree = (roleId: string, level: number, visitedRoleIds: Set<string>): Array<Role & { level: number }> => {
|
||||
const role = roleMap.get(roleId);
|
||||
if (!role || visitedRoleIds.has(roleId)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const nextVisitedRoleIds = new Set(visitedRoleIds);
|
||||
nextVisitedRoleIds.add(roleId);
|
||||
const childRoleIdList = displayRoleList.value
|
||||
.filter(item => item.parentRoleId === roleId)
|
||||
.map(item => item.id);
|
||||
return [{ ...role, level }, ...childRoleIdList.flatMap(childRoleId => buildTree(childRoleId, level + 1, nextVisitedRoleIds))];
|
||||
};
|
||||
return roots.flatMap(item => buildTree(item.id, 0, new Set<string>()));
|
||||
}
|
||||
|
||||
async function refreshAuthorizationScope(): Promise<void> {
|
||||
if (userStore.hasRoles(coreRoleCodes.admin)) {
|
||||
deletableRoleIdSet.value = new Set(permissionStore.roleList.map(item => item.id));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const roleList = await permissionStore.loadManageableRoles();
|
||||
displayRoleList.value = roleList;
|
||||
deletableRoleIdSet.value = new Set(roleList.map(item => item.id).filter((id): id is string => Boolean(id)));
|
||||
} catch {
|
||||
deletableRoleIdSet.value = new Set<string>();
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.role-list {
|
||||
padding: calc(var(--app-nav-height) + 1rem) 0 1rem 0;
|
||||
|
||||
.list-wrapper {
|
||||
padding-bottom: 6rem;
|
||||
|
||||
.item {
|
||||
|
||||
&:last-child {
|
||||
|
||||
:deep(.t-cell:after) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.delete {
|
||||
height: 100%;
|
||||
padding: 0 1rem;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,344 @@
|
||||
<template>
|
||||
<div
|
||||
v-can="{ value: [corePermissionCodes.role.read, corePermissionCodes.permission.read, corePermissionCodes.rolePermission.read], mode: 'and' }"
|
||||
class="role-permission-assign"
|
||||
>
|
||||
<div class="header bg-white">
|
||||
<t-dropdown-menu class="role-selector">
|
||||
<t-dropdown-item
|
||||
:label="activeRoleLabel"
|
||||
:options="roleOptions"
|
||||
:value="activeRoleId"
|
||||
borderless
|
||||
@change="handleRoleChange"
|
||||
/>
|
||||
</t-dropdown-menu>
|
||||
</div>
|
||||
<div class="inherit">
|
||||
<span class="label">显示继承权限</span>
|
||||
<t-switch class="switch" v-model="showChildRolePermissions" />
|
||||
</div>
|
||||
<t-cell-group v-if="activeRole && grantablePermissionList.length" title="可配置的直接权限 P" theme="card">
|
||||
<t-cell
|
||||
v-for="item in grantablePermissionList"
|
||||
:key="item.id"
|
||||
:title="item.name || item.code"
|
||||
:class="{ inherited: isPermissionInherited(item.id) }"
|
||||
>
|
||||
<template #description>
|
||||
<span class="code" v-text="item.code"></span>
|
||||
</template>
|
||||
<template #right-icon>
|
||||
<t-switch
|
||||
:disabled="!canUpdateRolePermission || !canEditActiveRole || isPermissionInherited(item.id)"
|
||||
:model-value="isPermissionEnabled(item.id)"
|
||||
@change="value => setPermissionChecked(item.id, value === true)"
|
||||
/>
|
||||
</template>
|
||||
</t-cell>
|
||||
</t-cell-group>
|
||||
<t-empty v-else-if="!permissionStore.loading" description="暂无可配置的直接权限 P" />
|
||||
<t-button
|
||||
v-if="canEditActiveRole"
|
||||
v-can="rolePermissionWriteAccess"
|
||||
class="save"
|
||||
theme="primary"
|
||||
@click="savePermissionList"
|
||||
>
|
||||
保存 P
|
||||
</t-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { DropdownOption, DropdownValue } from "tdesign-mobile-vue";
|
||||
import { Toast } from "tdesign-mobile-vue";
|
||||
import { type Permission, useUserStore } from "timi-web";
|
||||
import { type Role, usePermissionSettingsStore } from "@/store/permissionSettingsStore";
|
||||
import { corePermissionCodes, coreRoleCodes } from "@/utils/permission";
|
||||
|
||||
defineOptions({
|
||||
name: "RolePermissionAssign"
|
||||
});
|
||||
|
||||
const permissionStore = usePermissionSettingsStore();
|
||||
const userStore = useUserStore();
|
||||
const route = useRoute();
|
||||
|
||||
// ---------- 角色选择 ----------
|
||||
|
||||
const activeRoleId = ref(String(route.query.roleId || route.params.id || ""));
|
||||
const checkedPermissionIds = ref<string[]>([]);
|
||||
const unmanagedPermissionIds = ref<string[]>([]);
|
||||
const inheritedPermissionIds = ref<string[]>([]);
|
||||
const grantablePermissionList = ref<Permission[]>([]);
|
||||
const candidateRoleList = ref<Role[]>([]);
|
||||
const showChildRolePermissions = ref(false);
|
||||
let permissionSyncSeq = 0;
|
||||
|
||||
const activeRole = computed(() => permissionStore.getRoleById(activeRoleId.value));
|
||||
const activeRoleLabel = computed(() => activeRole.value ? `当前角色:${activeRole.value.name}` : "选择角色");
|
||||
const canUpdateRolePermission = computed(() => userStore.hasPermissions([
|
||||
corePermissionCodes.rolePermission.create,
|
||||
corePermissionCodes.rolePermission.delete
|
||||
], "and"));
|
||||
const isSystemAdmin = computed(() => userStore.hasRoles(coreRoleCodes.admin));
|
||||
const canEditActiveRole = computed(() => Boolean(activeRole.value)
|
||||
&& candidateRoleList.value.some(item => item.id === activeRoleId.value)
|
||||
&& (isSystemAdmin.value || activeRole.value?.protectedRole !== true));
|
||||
const roleOptions = computed<DropdownOption[]>(() => candidateRoleList.value
|
||||
.filter(item => !!item.id)
|
||||
.map(item => ({
|
||||
label: item.name || item.code,
|
||||
value: item.id,
|
||||
disabled: false
|
||||
})));
|
||||
|
||||
const rolePermissionWriteAccess = {
|
||||
value: [
|
||||
corePermissionCodes.rolePermission.create,
|
||||
corePermissionCodes.rolePermission.delete
|
||||
],
|
||||
mode: "and"
|
||||
} as const;
|
||||
|
||||
// ---------- 权限状态 ----------
|
||||
|
||||
const checkedPermissionIdSet = computed(() => new Set(checkedPermissionIds.value));
|
||||
const inheritedPermissionIdSet = computed(() => new Set(inheritedPermissionIds.value));
|
||||
|
||||
// ---------- 角色切换 ----------
|
||||
|
||||
async function handleRoleChange(value: DropdownValue): Promise<void> {
|
||||
activeRoleId.value = Array.isArray(value) ? String(value[0] || "") : String(value);
|
||||
await loadActiveRolePermissions();
|
||||
}
|
||||
|
||||
async function loadActiveRolePermissions(): Promise<void> {
|
||||
const roleId = activeRoleId.value;
|
||||
if (!roleId) {
|
||||
clearPermissionState();
|
||||
return;
|
||||
}
|
||||
|
||||
const currentSeq = permissionSyncSeq + 1;
|
||||
permissionSyncSeq = currentSeq;
|
||||
const [role, permissionList] = await Promise.all([
|
||||
permissionStore.loadRoleDetail(roleId),
|
||||
permissionStore.loadGrantableRolePermissions(roleId)
|
||||
]);
|
||||
if (currentSeq !== permissionSyncSeq || activeRoleId.value !== roleId) {
|
||||
return;
|
||||
}
|
||||
|
||||
grantablePermissionList.value = permissionList;
|
||||
const grantablePermissionIdSet = new Set(grantablePermissionList.value.map(item => item.id));
|
||||
checkedPermissionIds.value = role.permissionIdList.filter(id => grantablePermissionIdSet.has(id));
|
||||
unmanagedPermissionIds.value = role.permissionIdList.filter(id => !grantablePermissionIdSet.has(id));
|
||||
inheritedPermissionIds.value = (role.allPermissionList || [])
|
||||
.map(item => item.id)
|
||||
.filter(id => grantablePermissionIdSet.has(id) && !checkedPermissionIdSet.value.has(id));
|
||||
}
|
||||
|
||||
// ---------- 保存 ----------
|
||||
|
||||
async function savePermissionList(): Promise<void> {
|
||||
if (!activeRoleId.value || !canEditActiveRole.value || !canUpdateRolePermission.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await permissionStore.setRolePermissionList(activeRoleId.value, Array.from(new Set([
|
||||
...checkedPermissionIds.value,
|
||||
...unmanagedPermissionIds.value
|
||||
])));
|
||||
Toast.success("P 已保存");
|
||||
await loadActiveRolePermissions();
|
||||
} catch (error) {
|
||||
clearPermissionState();
|
||||
try {
|
||||
await loadAuthorizationScope();
|
||||
await loadActiveRolePermissions();
|
||||
} finally {
|
||||
Toast.error(getErrorMessage(error, "P 保存失败"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 权限勾选 ----------
|
||||
|
||||
function isPermissionChecked(permissionId: string): boolean {
|
||||
return checkedPermissionIdSet.value.has(permissionId);
|
||||
}
|
||||
|
||||
function isPermissionInherited(permissionId: string): boolean {
|
||||
return showChildRolePermissions.value && !isPermissionChecked(permissionId) && inheritedPermissionIdSet.value.has(permissionId);
|
||||
}
|
||||
|
||||
function isPermissionEnabled(permissionId: string): boolean {
|
||||
return isPermissionChecked(permissionId) || isPermissionInherited(permissionId);
|
||||
}
|
||||
|
||||
function setPermissionChecked(permissionId: string, value: boolean): void {
|
||||
if (!canUpdateRolePermission.value || !canEditActiveRole.value || isPermissionInherited(permissionId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (value && !checkedPermissionIds.value.includes(permissionId)) {
|
||||
checkedPermissionIds.value.push(permissionId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
checkedPermissionIds.value = checkedPermissionIds.value.filter(item => item !== permissionId);
|
||||
}
|
||||
}
|
||||
|
||||
function clearPermissionState(): void {
|
||||
checkedPermissionIds.value = [];
|
||||
unmanagedPermissionIds.value = [];
|
||||
inheritedPermissionIds.value = [];
|
||||
grantablePermissionList.value = [];
|
||||
}
|
||||
|
||||
// ---------- 授权范围 ----------
|
||||
|
||||
async function loadAuthorizationScope(): Promise<void> {
|
||||
const roleList = isSystemAdmin.value ? permissionStore.roleList : await permissionStore.loadManageableRoles();
|
||||
candidateRoleList.value = roleList;
|
||||
}
|
||||
|
||||
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(async () => {
|
||||
await permissionStore.loadAll();
|
||||
await loadAuthorizationScope();
|
||||
if (!activeRoleId.value) {
|
||||
activeRoleId.value = candidateRoleList.value[0]?.id || "";
|
||||
}
|
||||
await loadActiveRolePermissions();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.role-permission-assign {
|
||||
gap: 1rem;
|
||||
display: flex;
|
||||
padding: calc(var(--app-nav-height) + 4.5rem) 0 6rem 0;
|
||||
flex-direction: column;
|
||||
|
||||
.header {
|
||||
top: var(--app-nav-height);
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 20;
|
||||
display: flex;
|
||||
position: fixed;
|
||||
background: #fff;
|
||||
box-shadow: var(--td-shadow-1);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.role-selector::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.inherit {
|
||||
gap: .75rem;
|
||||
display: flex;
|
||||
margin: 0 1rem;
|
||||
padding: .75rem 1rem;
|
||||
background: #fff;
|
||||
align-items: center;
|
||||
border-radius: .75rem;
|
||||
justify-content: space-between;
|
||||
|
||||
.label {
|
||||
color: #334155;
|
||||
font-size: .875rem;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
|
||||
.switch {
|
||||
--td-switch-checked-color: #14b8a6;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.t-cell__title) {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
:deep(.t-cell__title-text) {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
:deep(.t-cell__description) {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
:deep(.t-cell__right) {
|
||||
flex: 0 0 auto;
|
||||
margin-left: .75rem;
|
||||
}
|
||||
|
||||
.code {
|
||||
width: 100%;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.inherited {
|
||||
:deep(.t-cell__title) {
|
||||
color: #0f766e;
|
||||
}
|
||||
|
||||
:deep(.t-switch--checked.t-switch--disabled) {
|
||||
background: #14b8a6;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.save {
|
||||
left: 1rem;
|
||||
right: 1rem;
|
||||
bottom: calc(.75rem + env(safe-area-inset-bottom, 0));
|
||||
position: fixed;
|
||||
}
|
||||
}
|
||||
|
||||
:global(html.theme-dark) {
|
||||
.role-permission-assign {
|
||||
.header,
|
||||
.inherit {
|
||||
background: #0f172a;
|
||||
}
|
||||
|
||||
.inherit .label {
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
.inherited {
|
||||
:deep(.t-cell__title) {
|
||||
color: #5eead4;
|
||||
}
|
||||
|
||||
:deep(.t-switch--checked.t-switch--disabled) {
|
||||
background: #0f766e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user