refactor permission

This commit is contained in:
Timi
2026-08-11 15:24:29 +08:00
parent 45ea8b46aa
commit 575d41b2e2
20 changed files with 1789 additions and 625 deletions
+46
View File
@@ -0,0 +1,46 @@
<template>
<div class="card-title">
<h3 v-if="title || $slots.title" class="title">
<slot name="title">
<span v-text="title" />
</slot>
</h3>
<p v-if="description || $slots.description" class="description gray">
<slot name="description">
<span v-text="description" />
</slot>
</p>
</div>
</template>
<script setup lang="ts">
interface Props {
title?: string;
description?: string;
}
defineProps<Props>();
defineSlots<{
title?: () => unknown;
description?: () => unknown;
}>();
defineOptions({
name: "CardTitle"
});
</script>
<style lang="less" scoped>
.card-title {
margin: 1rem;
.title {
margin: 0;
}
.description {
margin: .5rem 0 0 0;
font-size: .875rem;
}
}
</style>
+2 -2
View File
@@ -1,5 +1,5 @@
<template>
<div v-role="permissionManagementRoleAccess" v-can="pageAccess" class="permission-edit">
<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 />
@@ -21,7 +21,7 @@
<script setup lang="ts">
import { Toast } from "tdesign-mobile-vue";
import { moduleCode, usePermissionSettingsStore } from "@/store/permissionSettingsStore";
import { corePermissionCodes, permissionManagementRoleAccess } from "@/utils/permission";
import { corePermissionCodes } from "@/utils/permission";
defineOptions({
name: "PermissionEdit"
+241 -72
View File
@@ -1,58 +1,131 @@
<template>
<div v-role="permissionManagementRoleAccess" v-can="{ value: [corePermissionCodes.role.read, corePermissionCodes.permission.read], mode: 'and' }" class="permission-list">
<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"
>
<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 #note>
<span class="code" v-text="item.code"></span>
<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>
<template #right>
<t-button v-can="corePermissionCodes.permission.delete" class="delete" theme="danger" @click="removePermission(item.id)">删除</t-button>
</template>
</t-swipe-cell>
</t-cell-group>
<t-empty v-else-if="!permissionStore.loading" description="暂无权限" />
</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>
<t-button v-can="corePermissionCodes.permission.create" class="add" theme="primary" type="button" @click="router.push('/management/permissions/items/create')">
<template #icon>
<t-icon name="add" />
</template>
新增权限
</t-button>
</div>
</template>
<script setup lang="ts">
import { type Role as UserRole, RoleAPI, useUserStore } from "timi-web";
import type { Permission } from "@/store/permissionSettingsStore";
import { usePermissionSettingsStore } from "@/store/permissionSettingsStore";
import { corePermissionCodes, permissionManagementRoleAccess } from "@/utils/permission";
import { moduleCode, usePermissionSettingsStore } from "@/store/permissionSettingsStore";
import { corePermissionCodes } from "@/utils/permission";
defineOptions({
name: "PermissionList"
});
const router = useRouter();
const permissionStore = usePermissionSettingsStore();
const userStore = useUserStore();
const isRefreshing = ref(false);
const isSourceLoading = ref(false);
// ---------- 列表数据 ----------
const sortedPermissions = computed<Permission[]>(() => [...permissionStore.permissionList].sort((left, right) => left.code.localeCompare(right.code)));
type PermissionSource = "direct" | "inherited" | "unknown";
type PermissionWithSource = Permission & {
source: PermissionSource;
};
async function removePermission(id: string): Promise<void> {
await permissionStore.removePermission(id);
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}`;
}
// ---------- 页面数据 ----------
@@ -61,6 +134,7 @@ async function refresh(): Promise<void> {
isRefreshing.value = true;
try {
await permissionStore.loadAll(true);
await loadSourceRoles();
} finally {
isRefreshing.value = false;
}
@@ -68,64 +142,159 @@ async function refresh(): Promise<void> {
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 {
padding: calc(var(--app-nav-height) + 1rem) 0 6rem 0;
--source-direct-color: #2563EB;
--source-inherited-color: #14B8A6;
--source-unknown-color: #94A3B8;
.item {
:deep(.t-cell__title) {
flex: 1 1 0;
min-width: 0;
}
padding: calc(var(--app-nav-height) + 1rem) 0 1rem 0;
:deep(.t-cell__title-text) {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.list-wrapper {
:deep(.t-cell__note) {
flex: 0 1 auto;
min-width: 0;
max-width: 56%;
text-align: right;
justify-content: flex-end;
}
.content {
gap: 1rem;
display: flex;
flex-direction: column;
:deep(.t-cell__right) {
flex: 0 0 auto;
}
.header {
.code {
width: 100%;
display: block;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.description {
gap: .5rem;
display: flex;
flex-direction: column;
&:last-child {
.legends {
gap: 1.5rem;
display: flex;
:deep(.t-cell:after) {
display: none;
.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;
}
}
}
}
.delete {
height: 100%;
padding: 0 1rem;
border-radius: 0;
}
}
}
.add {
left: 1rem;
right: 1rem;
bottom: calc(.75rem + env(safe-area-inset-bottom, 0));
position: fixed;
:global(html.theme-dark) {
.permission-list {
.list-wrapper {
.content {
.item {
:deep(.t-cell) {
.source {
&-direct {
background: #60a5fa;
}
&-inherited {
background: #2dd4bf;
}
&-unknown {
background: #64748b;
}
}
}
}
}
}
}
}
+97 -29
View File
@@ -4,61 +4,118 @@
class="permission-settings"
>
<t-cell-group title="权限配置" theme="card">
<t-cell v-can="corePermissionCodes.role.read" title="角色管理" :note="`${permissionStore.roleList.length} 个角色`" arrow hover @click="router.push('/management/permissions/roles')">
<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="`${permissionStore.permissionList.length} 个权限`" arrow hover @click="router.push('/management/permissions/items')">
<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="角色权限授权" note="为角色分配可用权限" arrow hover @click="router.push('/management/permissions/assign')">
<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="用户角色授权" note="为用户分配业务角色" arrow hover @click="router.push('/management/permissions/user-roles')">
<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>
<div class="struct">
<div class="header">
<span class="title">当前角色结构</span>
<span class="tips orange">上级角色包含子级角色所有权限</span>
</div>
<t-cell-group theme="card">
<t-cell
v-can="{ value: [corePermissionCodes.role.read, corePermissionCodes.permission.read], mode: 'and' }"
v-for="item in permissionStore.roleTree"
:key="item.id"
:note="`${item.permissionIdList.length} 项权限`"
arrow
hover
@click="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>
<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 } from "@/utils/permission";
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>()));
});
// ---------- 权限入口 ----------
@@ -66,25 +123,36 @@ const rolePermissionAssignAccess = {
value: [
corePermissionCodes.role.read,
corePermissionCodes.permission.read,
corePermissionCodes.rolePermission.read,
corePermissionCodes.roleRelation.read
corePermissionCodes.rolePermission.read
],
mode: "and"
} as const;
async function toAssign(roleId: string): Promise<void> {
await router.push({
path: "/management/permissions/assign",
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>
@@ -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>
+526 -87
View File
@@ -1,45 +1,116 @@
<template>
<div v-role="permissionManagementRoleAccess" v-can="pageAccess" class="role-edit">
<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 />
<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 />
<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 />
<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 />
<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>
<section class="children">
<div class="title">
<span>子角色</span>
<span class="hint">父角色会继承子角色权限</span>
</div>
<t-checkbox-group class="group" v-model="formModel.childRoleIdList">
<t-checkbox
class="item bg-transparent"
v-for="item in childOptions"
:key="item.id"
:value="item.id"
:label="item.name"
icon="line"
borderless
default-checked
/>
</t-checkbox-group>
</section>
<t-button v-can="savePermissionCode" class="save" theme="primary" @click="saveRole">保存</t-button>
<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 { moduleCode, usePermissionSettingsStore } from "@/store/permissionSettingsStore";
import { corePermissionCodes, permissionManagementRoleAccess } from "@/utils/permission";
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"
@@ -47,7 +118,10 @@ defineOptions({
const route = useRoute();
const router = useRouter();
const navBarStore = useNavBarStore();
const permissionStore = usePermissionSettingsStore();
const currentStoreStore = useCurrentStoreStore();
const userStore = useUserStore();
// ---------- 路由权限 ----------
@@ -55,10 +129,21 @@ 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: [corePermissionCodes.role.read, corePermissionCodes.permission.read, savePermissionCode.value],
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({
@@ -67,19 +152,133 @@ const formModel = reactive({
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 childOptions = computed(() => permissionStore.roleList.filter(item => item.id !== formModel.id && !hasChildRole(item.id, formModel.id)));
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 = permissionStore.getRoleById(roleId);
const current = inheritanceRoleList.value.find(item => item.id === roleId);
if (!current) {
return false;
}
@@ -88,45 +287,202 @@ function hasChildRole(roleId: string, targetRoleId: string, visitedRoleIds = new
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();
const current = await permissionStore.loadRoleDetail(routeId.value);
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,
childRoleIdList: [...current.childRoleIdList]
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 saveRole(): Promise<void> {
const role = await permissionStore.upsertRole({
id: formModel.id,
moduleCode: moduleCode,
code: formModel.code,
name: formModel.name,
description: formModel.description,
permissionIdList: permissionStore.getRoleById(formModel.id)?.permissionIdList || [],
childRoleIdList: [...formModel.childRoleIdList]
}, isCreateMode.value);
Toast.success("角色已保存");
await router.push(isCreateMode.value ? "/management/permissions/roles" : `/management/permissions/roles/${role?.id || formModel.id}`);
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 = "";
}
}
onMounted(async () => {
await fillForm();
});
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 {
padding: calc(var(--app-nav-height) + 1rem) 0 5rem 0;
gap: 1rem;
display: flex;
padding: calc(var(--app-nav-height) + 1rem) 0 2rem 0;
flex-direction: column;
.form {
border-radius: .75rem;
@@ -136,69 +492,152 @@ onMounted(async () => {
width: 100%;
}
.children {
gap: .75rem;
display: flex;
padding: 1rem;
flex-direction: column;
.permission-list {
--source-direct-color: #2563EB;
--source-inherited-color: #14B8A6;
.title {
gap: .25rem;
display: flex;
flex-direction: column;
:deep(.t-cell) {
span:first-child {
font-weight: 700;
&__left {
display: flex;
align-items: center;
}
.hint {
color: #52607a;
font-size: .8125rem;
&__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;
}
}
.group {
--td-checkbox-vertical-padding: 10px;
.code {
width: 100%;
display: block;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
background: #FFF;
border-radius: 1rem;
.source {
width: 1rem;
height: 1rem;
display: block;
border-radius: .25rem;
.item:not(:last-child)::after {
content: ' ';
left: 16px;
right: 0;
bottom: 0;
position: absolute;
box-sizing: border-box;
border-bottom: 1px solid var(--td-component-stroke, var(--td-gray-color-3, #e7e7e7));
pointer-events: none;
-webkit-box-sizing: border-box;
&-direct {
background: var(--source-direct-color);
}
&-inherited {
background: var(--source-inherited-color);
}
}
}
.save {
left: 1rem;
right: 1rem;
bottom: calc(.75rem + env(safe-area-inset-bottom, 0));
position: fixed;
.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 {
.children {
.block {
.title .hint {
color: #94a3b8;
}
.group .card {
color: #e2e8f0;
background: rgba(15, 23, 42, .72);
border-color: rgba(148, 163, 184, .16);
.group {
background: #0f172a;
}
}
.code {
color: #94a3b8;
}
.permission-list {
.source {
&-direct {
background: #60a5fa;
}
&-inherited {
background: #2dd4bf;
}
}
}
}
.role-picker {
background: #0f172a;
.group {
background: #0f172a;
}
}
}
</style>
+129 -31
View File
@@ -1,6 +1,7 @@
<template>
<div v-role="permissionManagementRoleAccess" v-can="{ value: [corePermissionCodes.role.read, corePermissionCodes.permission.read], mode: 'and' }" class="role-list">
<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"
@@ -17,42 +18,77 @@
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-can="corePermissionCodes.role.delete" class="delete" theme="danger" @click="removeRole(item.id)">删除</t-button>
<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>
<t-button v-can="corePermissionCodes.role.create" class="add" theme="primary" type="button" @click="router.push('/management/permissions/roles/create')">
<template #icon>
<t-icon name="add" />
</template>
新增角色
</t-button>
</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, permissionManagementRoleAccess } from "@/utils/permission";
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 sortedRoles = computed<Role[]>(() => [...permissionStore.roleList].sort((left, right) => (left.name || "").localeCompare(right.name || "")));
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> {
await permissionStore.removeRole(id);
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);
}
// ---------- 页面数据 ----------
@@ -61,41 +97,103 @@ async function refresh(): Promise<void> {
isRefreshing.value = true;
try {
await permissionStore.loadAll(true);
displayRoleList.value = getDisplayRoleList();
await refreshAuthorizationScope();
} finally {
isRefreshing.value = false;
}
}
onMounted(async () => {
await permissionStore.loadAll();
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 6rem 0;
padding: calc(var(--app-nav-height) + 1rem) 0 1rem 0;
.item {
.list-wrapper {
padding-bottom: 6rem;
&:last-child {
.item {
:deep(.t-cell:after) {
display: none;
&:last-child {
:deep(.t-cell:after) {
display: none;
}
}
.delete {
height: 100%;
padding: 0 1rem;
border-radius: 0;
}
}
.delete {
height: 100%;
padding: 0 1rem;
border-radius: 0;
}
}
.add {
left: 1rem;
right: 1rem;
bottom: calc(.75rem + env(safe-area-inset-bottom, 0));
position: fixed;
}
}
+121 -78
View File
@@ -1,5 +1,8 @@
<template>
<div v-role="permissionManagementRoleAccess" v-can="{ value: [corePermissionCodes.role.read, corePermissionCodes.permission.read], mode: 'and' }" class="role-permission-assign">
<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
@@ -10,16 +13,16 @@
@change="handleRoleChange"
/>
</t-dropdown-menu>
<div class="inherit">
<span class="label">显示子角色权限</span>
<t-switch class="switch" v-model="showChildRolePermissions" />
</div>
</div>
<t-cell-group v-if="activeRole && permissionStore.permissionList.length" theme="card">
<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 permissionStore.permissionList"
v-for="item in grantablePermissionList"
:key="item.id"
:title="item.name"
:title="item.name || item.code"
:class="{ inherited: isPermissionInherited(item.id) }"
>
<template #description>
@@ -27,21 +30,22 @@
</template>
<template #right-icon>
<t-switch
:disabled="!canUpdateRole || isPermissionInherited(item.id)"
: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="暂无可授权权限" />
<t-empty v-else-if="!permissionStore.loading" description="暂无可配置的直接权限 P" />
<t-button
v-can="corePermissionCodes.role.update"
v-if="canEditActiveRole"
v-can="rolePermissionWriteAccess"
class="save"
theme="primary"
@click="savePermissionList"
>
保存授权
保存 P
</t-button>
</div>
</template>
@@ -49,9 +53,9 @@
<script setup lang="ts">
import type { DropdownOption, DropdownValue } from "tdesign-mobile-vue";
import { Toast } from "tdesign-mobile-vue";
import { useUserStore } from "timi-web";
import { usePermissionSettingsStore } from "@/store/permissionSettingsStore";
import { corePermissionCodes, permissionManagementRoleAccess } from "@/utils/permission";
import { type Permission, useUserStore } from "timi-web";
import { type Role, usePermissionSettingsStore } from "@/store/permissionSettingsStore";
import { corePermissionCodes, coreRoleCodes } from "@/utils/permission";
defineOptions({
name: "RolePermissionAssign"
@@ -63,14 +67,26 @@ const route = useRoute();
// ---------- 角色选择 ----------
const activeRoleId = ref(String(route.query.roleId || ""));
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 canUpdateRole = computed(() => userStore.hasPermissions(corePermissionCodes.role.update));
const roleOptions = computed<DropdownOption[]>(() => permissionStore.roleList
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,
@@ -78,15 +94,18 @@ const roleOptions = computed<DropdownOption[]>(() => permissionStore.roleList
disabled: false
})));
const rolePermissionWriteAccess = {
value: [
corePermissionCodes.rolePermission.create,
corePermissionCodes.rolePermission.delete
],
mode: "and"
} as const;
// ---------- 权限状态 ----------
const checkedPermissionIdSet = computed(() => new Set(checkedPermissionIds.value));
const childRolePermissionIdSet = computed(() => {
if (!activeRole.value) {
return new Set<string>();
}
return collectChildRolePermissionIds(activeRole.value.childRoleIdList, new Set<string>());
});
const inheritedPermissionIdSet = computed(() => new Set(inheritedPermissionIds.value));
// ---------- 角色切换 ----------
@@ -98,25 +117,52 @@ async function handleRoleChange(value: DropdownValue): Promise<void> {
async function loadActiveRolePermissions(): Promise<void> {
const roleId = activeRoleId.value;
if (!roleId) {
checkedPermissionIds.value = [];
clearPermissionState();
return;
}
const currentSeq = permissionSyncSeq + 1;
permissionSyncSeq = currentSeq;
const role = await permissionStore.loadRoleDetail(roleId);
const [role, permissionList] = await Promise.all([
permissionStore.loadRoleDetail(roleId),
permissionStore.loadGrantableRolePermissions(roleId)
]);
if (currentSeq !== permissionSyncSeq || activeRoleId.value !== roleId) {
return;
}
checkedPermissionIds.value = [...role.permissionIdList];
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> {
await permissionStore.setRolePermissionList(activeRoleId.value, checkedPermissionIds.value);
Toast.success("授权已保存");
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 保存失败"));
}
}
}
// ---------- 权限勾选 ----------
@@ -126,7 +172,7 @@ function isPermissionChecked(permissionId: string): boolean {
}
function isPermissionInherited(permissionId: string): boolean {
return showChildRolePermissions.value && !isPermissionChecked(permissionId) && childRolePermissionIdSet.value.has(permissionId);
return showChildRolePermissions.value && !isPermissionChecked(permissionId) && inheritedPermissionIdSet.value.has(permissionId);
}
function isPermissionEnabled(permissionId: string): boolean {
@@ -134,7 +180,7 @@ function isPermissionEnabled(permissionId: string): boolean {
}
function setPermissionChecked(permissionId: string, value: boolean): void {
if (!canUpdateRole.value || isPermissionInherited(permissionId)) {
if (!canUpdateRolePermission.value || !canEditActiveRole.value || isPermissionInherited(permissionId)) {
return;
}
@@ -148,36 +194,35 @@ function setPermissionChecked(permissionId: string, value: boolean): void {
}
}
// ---------- 子角色继承 ----------
function clearPermissionState(): void {
checkedPermissionIds.value = [];
unmanagedPermissionIds.value = [];
inheritedPermissionIds.value = [];
grantablePermissionList.value = [];
}
function collectChildRolePermissionIds(roleIdList: string[], visitedRoleIdSet: Set<string>): Set<string> {
const permissionIdSet = new Set<string>();
for (const roleId of roleIdList) {
if (visitedRoleIdSet.has(roleId)) {
continue;
}
// ---------- 授权范围 ----------
visitedRoleIdSet.add(roleId);
const role = permissionStore.getRoleById(roleId);
if (!role) {
continue;
}
async function loadAuthorizationScope(): Promise<void> {
const roleList = isSystemAdmin.value ? permissionStore.roleList : await permissionStore.loadManageableRoles();
candidateRoleList.value = roleList;
}
for (const permissionId of role.permissionIdList) {
permissionIdSet.add(permissionId);
}
for (const permissionId of collectChildRolePermissionIds(role.childRoleIdList, visitedRoleIdSet)) {
permissionIdSet.add(permissionId);
}
function getErrorMessage(error: unknown, fallback: string): string {
if (error instanceof Error && error.message) {
return error.message;
}
return permissionIdSet;
if (typeof error === "string" && error) {
return error;
}
return fallback;
}
onMounted(async () => {
await permissionStore.loadAll();
await loadAuthorizationScope();
if (!activeRoleId.value) {
activeRoleId.value = permissionStore.roleList[0]?.id || "";
activeRoleId.value = candidateRoleList.value[0]?.id || "";
}
await loadActiveRolePermissions();
});
@@ -197,30 +242,34 @@ onMounted(async () => {
z-index: 20;
display: flex;
position: fixed;
background: #fff;
box-shadow: var(--td-shadow-1);
align-items: center;
justify-content: space-around;
justify-content: center;
.role-selector::after {
display: none;
}
}
.inherit {
gap: .75rem;
display: flex;
padding: .75rem 1rem;
align-items: center;
justify-content: space-between;
.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;
}
.label {
color: #334155;
font-size: .875rem;
line-height: 1.25rem;
}
.switch {
--td-switch-checked-color: #14b8a6;
}
.switch {
--td-switch-checked-color: #14b8a6;
}
}
@@ -272,18 +321,13 @@ onMounted(async () => {
:global(html.theme-dark) {
.role-permission-assign {
.role-menu {
.header,
.inherit {
background: #0f172a;
}
.inherit {
.label {
color: #cbd5e1;
}
.switch {
--td-switch-checked-color: #0f766e;
}
}
.inherit .label {
color: #cbd5e1;
}
.inherited {
@@ -298,4 +342,3 @@ onMounted(async () => {
}
}
</style>
+4 -4
View File
@@ -7,7 +7,7 @@
<t-cell-group v-if="managedUser.user" theme="card">
<t-cell
:title="managedUser.user.name"
:description="getRoleDisplayName(managedUser.user.roleList?.[0], permissionStore.roleList)"
:description="getRoleDisplayName(getManagedUserRoleList(managedUser)[0], permissionStore.roleList)"
>
<template #leftIcon>
<t-image
@@ -74,7 +74,7 @@ import type { ManagedUser } from "@/types/ManagedUser";
import type { EventItem } from "@/types/EventItem";
import { usePermissionSettingsStore } from "@/store/permissionSettingsStore";
import { buildFallbackAvatar } from "@/utils/avatar";
import { buildEffectiveRoleCodeSet, getRoleDisplayName } from "@/utils/role";
import { buildEffectiveRoleCodeSet, getManagedUserRoleList, getRoleDisplayName } from "@/utils/role";
import { permissionCodes } from "@/utils/permission";
defineOptions({
@@ -99,7 +99,7 @@ const currentUser = computed(() => {
return {
id: item.id || "",
name: user?.nick || user?.name || "未命名用户",
role: getRoleDisplayName(item.roleList?.[0], permissionStore.roleList),
role: getRoleDisplayName(getManagedUserRoleList(item)[0], permissionStore.roleList),
avatar: buildUserAvatar(user),
mobile: user?.phoneNo || "",
lastLoginAt: user?.lastLoginAt
@@ -146,7 +146,7 @@ function isUser(user?: Partial<User>): user is User {
// ---------- 事件范围 ----------
function canEventItem(action: EventItem, item: ManagedUser): boolean {
const userRoleCodeSet = buildEffectiveRoleCodeSet(item, permissionStore.roleList);
const userRoleCodeSet = buildEffectiveRoleCodeSet(item);
const roleCodeList = action.roleCodeList || [];
if (!roleCodeList.length) {
return false;
+3 -3
View File
@@ -58,7 +58,7 @@ import type { ManagedUser } from "@/types/ManagedUser";
import type { EventItem } from "@/types/EventItem";
import { usePermissionSettingsStore } from "@/store/permissionSettingsStore";
import { buildFallbackAvatar } from "@/utils/avatar";
import { buildEffectiveRoleCodeSet, getRoleDisplayName } from "@/utils/role";
import { buildEffectiveRoleCodeSet, getManagedUserRoleList, getRoleDisplayName } from "@/utils/role";
import { gaoRoleCodes, permissionCodes } from "@/utils/permission";
defineOptions({
@@ -118,7 +118,7 @@ function getUserName(item: ManagedUser): string {
}
function getRoleName(item: ManagedUser): string {
return getRoleDisplayName(item.roleList?.[0], permissionStore.roleList);
return getRoleDisplayName(getManagedUserRoleList(item)[0], permissionStore.roleList);
}
function getActionScope(item: ManagedUser): string {
@@ -131,7 +131,7 @@ function getActionScope(item: ManagedUser): string {
// ---------- 事件范围 ----------
function canEventItem(action: EventItem, item: ManagedUser): boolean {
const userRoleCodeSet = buildEffectiveRoleCodeSet(item, permissionStore.roleList);
const userRoleCodeSet = buildEffectiveRoleCodeSet(item);
const roleCodeList = action.roleCodeList || [];
if (!roleCodeList.length) {
return false;
+42 -21
View File
@@ -49,23 +49,19 @@
<script setup lang="ts">
import type { DropdownOption, DropdownValue } from "tdesign-mobile-vue";
import { Toast } from "tdesign-mobile-vue";
import { RoleAPI, type ModuleCode, type Role, useUserStore } from "timi-web";
import { RoleAPI, type ModuleCode, type Role as UserRole, useUserStore } from "timi-web";
import ManagedUserAPI from "@/api/ManagedUserAPI";
import { useCurrentStoreStore } from "@/store/currentStoreStore";
import type { Role } from "@/store/permissionSettingsStore";
import type { ManagedUser } from "@/types/ManagedUser";
import { corePermissionCodes, gaoRoleCodes } from "@/utils/permission";
type ManagedRole = Role & {
id: string;
};
defineOptions({
name: "UserRoleAssign"
});
const route = useRoute();
const moduleCode: ModuleCode = "GAO";
const pageSize = 1000;
const allStoreFilterValue = "__ALL_STORES__";
// ---------- 用户选择 ----------
@@ -73,7 +69,7 @@ const allStoreFilterValue = "__ALL_STORES__";
const userStore = useUserStore();
const currentStoreStore = useCurrentStoreStore();
const userList = ref<ManagedUser[]>([]);
const roleList = ref<ManagedRole[]>([]);
const roleList = ref<Role[]>([]);
const routeManagedUserId = computed(() => String(route.params.id || route.query.managedUserId || ""));
const activeManagedUserId = ref(routeManagedUserId.value);
const selectedStoreId = ref(allStoreFilterValue);
@@ -197,7 +193,10 @@ async function loadActiveUserRoles(): Promise<void> {
return;
}
checkedRoleIds.value = roles.map(getRoleId).filter((item): item is string => !!item);
const availableRoleIdSet = new Set(roleList.value.map(item => item.id));
checkedRoleIds.value = roles
.map(getRoleId)
.filter((item): item is string => !!item && availableRoleIdSet.has(item));
}
// ---------- 保存 ----------
@@ -216,7 +215,16 @@ async function saveRoleList(): Promise<void> {
moduleCode: moduleCode,
roleIdList: [...checkedRoleIds.value]
});
await loadActiveUserRoles();
Toast.success("用户角色已保存");
} catch (error) {
checkedRoleIds.value = [];
try {
await reloadAuthorizationScope();
await loadActiveUserRoles();
} finally {
Toast.error(getErrorMessage(error, "用户角色授权失败"));
}
} finally {
isSaving.value = false;
}
@@ -230,18 +238,21 @@ function getUserName(item: ManagedUser): string {
// ---------- 角色列表 ----------
async function loadRoleList(): Promise<ManagedRole[]> {
const result = await RoleAPI.list({
index: 0,
size: pageSize,
equalsExample: {
moduleCode: moduleCode
},
orderMap: {
code: "ASC"
}
});
return (result.list || []).filter((role): role is ManagedRole => Boolean(role.id));
async function loadRoleList(): Promise<Role[]> {
const result = await RoleAPI.grantableList(moduleCode);
return (result || [])
.filter((role): role is UserRole & { id: string } => Boolean(role.id))
.map(role => ({
...role,
id: role.id,
permissionIdList: role.permissionIdList || role.permissionList?.map(item => item.id) || [],
childRoleIdList: role.childRoleIdList || role.childRoleList?.map(item => item.id).filter((id): id is string => Boolean(id)) || [],
delegationPermissionIdList: role.delegationPermissionIdList || role.delegationPermissionList?.map(item => item.id) || []
}));
}
async function reloadAuthorizationScope(): Promise<void> {
roleList.value = await loadRoleList();
}
// ---------- 角色勾选 ----------
@@ -250,7 +261,7 @@ function isRoleChecked(roleId: string): boolean {
return checkedRoleIds.value.includes(roleId);
}
function getRoleId(role: Role | string): string {
function getRoleId(role: UserRole | string): string {
if (typeof role !== "string") {
return role.id || "";
}
@@ -259,6 +270,16 @@ function getRoleId(role: Role | string): string {
return fullRole?.id || "";
}
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;
}
function setRoleChecked(roleId: string, value: boolean): void {
if (value && !checkedRoleIds.value.includes(roleId)) {
checkedRoleIds.value.push(roleId);
+3 -5
View File
@@ -77,7 +77,7 @@ import { UserAPI, useUserStore } from "timi-web";
import { Toast } from "tdesign-mobile-vue";
import { useCurrentStoreStore } from "@/store/currentStoreStore";
import { buildFallbackAvatar } from "@/utils/avatar";
import { corePermissionCodes, coreRoleCodes, gaoRoleCodes, permissionCodes } from "@/utils/permission";
import { corePermissionCodes, gaoRoleCodes, gaoStoreScopePermissionCodes, permissionCodes } from "@/utils/permission";
type UserAvatarSource = Parameters<typeof UserAPI.getAvatarURL>[0];
type PickerValue = string | number;
@@ -107,12 +107,10 @@ const currentUser = computed(() => userStore.loginUser.user);
const currentUserAvatar = computed(() => buildUserAvatar(currentUser.value));
const canSelectStore = computed(() => userStore.hasRoles(gaoRoleCodes.globalManager));
const canLoadLoginStore = computed(() => userStore.hasRoles([gaoRoleCodes.storeManager, gaoRoleCodes.employee], "or"));
const canLoadLoginStore = computed(() => userStore.hasPermissions(gaoStoreScopePermissionCodes, "or"));
const canViewStore = computed(() => canSelectStore.value || canLoadLoginStore.value);
const canManageStore = computed(() => userStore.hasRoles([gaoRoleCodes.globalManager, gaoRoleCodes.storeManager], "or"));
const canManagePermission = computed(() => (
userStore.hasRoles(coreRoleCodes.admin) || userStore.hasRoles(coreRoleCodes.userRoleAdmin)
) && userStore.hasPermissions([
const canManagePermission = computed(() => userStore.hasPermissions([
corePermissionCodes.role.read,
corePermissionCodes.permission.read
], "and"));
+48 -1
View File
@@ -5,6 +5,7 @@ import RoleEdit from "@/pages/management/RoleEdit.vue";
import PermissionList from "@/pages/management/PermissionList.vue";
import PermissionEdit from "@/pages/management/PermissionEdit.vue";
import RolePermissionAssign from "@/pages/management/RolePermissionAssign.vue";
import RoleDelegationPermissionAssign from "@/pages/management/RoleDelegationPermissionAssign.vue";
import UserRoleAssign from "@/pages/management/UserRoleAssign.vue";
const permissionRoutes: RouteRecordRaw[] = [
@@ -64,6 +65,34 @@ const permissionRoutes: RouteRecordRaw[] = [
darkBodyBackground: "#10161f"
}
},
{
path: "/management/permissions/roles/:id/assign/p",
name: "RolePermissionAssignFromRole",
component: RolePermissionAssign,
meta: {
depth: 6,
navBarVisible: true,
navBarCanBack: true,
navBarTitle: "角色直接权限 P",
tabBarVisible: false,
bodyBackground: "#F5F5F8",
darkBodyBackground: "#10161f"
}
},
{
path: "/management/permissions/roles/:id/assign/d",
name: "RoleDelegationPermissionAssignFromRole",
component: RoleDelegationPermissionAssign,
meta: {
depth: 6,
navBarVisible: true,
navBarCanBack: true,
navBarTitle: "角色转授权限 D",
tabBarVisible: false,
bodyBackground: "#F5F5F8",
darkBodyBackground: "#10161f"
}
},
{
path: "/management/permissions/items",
name: "PermissionList",
@@ -108,13 +137,31 @@ const permissionRoutes: RouteRecordRaw[] = [
},
{
path: "/management/permissions/assign",
redirect: "/management/permissions/assign/p"
},
{
path: "/management/permissions/assign/p",
name: "RolePermissionAssign",
component: RolePermissionAssign,
meta: {
depth: 4,
navBarVisible: true,
navBarCanBack: true,
navBarTitle: "角色授权权限",
navBarTitle: "角色直接权限 P",
tabBarVisible: false,
bodyBackground: "#F5F5F8",
darkBodyBackground: "#10161f"
}
},
{
path: "/management/permissions/assign/d",
name: "RoleDelegationPermissionAssign",
component: RoleDelegationPermissionAssign,
meta: {
depth: 4,
navBarVisible: true,
navBarCanBack: true,
navBarTitle: "角色转授权限 D",
tabBarVisible: false,
bodyBackground: "#F5F5F8",
darkBodyBackground: "#10161f"
+3 -3
View File
@@ -2,7 +2,7 @@ import { Toast } from "tdesign-mobile-vue";
import { useUserStore } from "timi-web";
import StoreAPI from "@/api/StoreAPI";
import type { Store } from "@/types/Store";
import { gaoRoleCodes } from "@/utils/permission";
import { gaoRoleCodes, gaoStoreScopePermissionCodes } from "@/utils/permission";
import { getCurrentStoreId, readStoredCurrentStoreId, setCurrentStoreId, setLoginStoreId } from "@/utils/store";
export const useCurrentStoreStore = defineStore("currentStore", () => {
@@ -86,8 +86,8 @@ export const useCurrentStoreStore = defineStore("currentStore", () => {
async function loadAfterLogin(): Promise<void> {
const isGlobalManager = userStore.hasRoles(gaoRoleCodes.globalManager);
const hasStoreRole = userStore.hasRoles([gaoRoleCodes.storeManager, gaoRoleCodes.employee], "or");
if (!isGlobalManager && !hasStoreRole) {
const hasStoreScope = userStore.hasPermissions(gaoStoreScopePermissionCodes, "or");
if (!isGlobalManager && !hasStoreScope) {
return;
}
+129 -61
View File
@@ -3,40 +3,27 @@ import {
PermissionAPI,
RoleAPI,
type ModuleCode,
type Permission as UserPermission,
type PermissionPayload,
type Role as UserRole,
type RolePayload
type Role as UserRole
} from "timi-web";
import type {
Permission,
PermissionDraft,
Role,
RoleDraft,
RolePayload
} from "@/types/PermissionSettings";
export const moduleCode: ModuleCode = "GAO";
const pageSize = 1000;
export type Permission = UserPermission;
export type Role = Omit<UserRole, "id" | "permissionIdList" | "childRoleIdList"> & {
id: string;
permissionIdList: string[];
childRoleIdList: string[];
};
export type RoleDraft = {
id?: string;
code: string;
name: string;
description: string;
nameLangId?: string;
moduleCode?: ModuleCode;
permissionIdList: string[];
childRoleIdList: string[];
};
export type PermissionDraft = {
id?: string;
code: string;
name: string;
description: string;
nameLangId?: string;
moduleCode?: ModuleCode;
};
export type {
Permission,
PermissionDraft,
Role,
RoleDraft,
RolePayload
} from "@/types/PermissionSettings";
function normalizeRole(role: UserRole): Role {
return {
@@ -44,8 +31,10 @@ function normalizeRole(role: UserRole): Role {
id: role.id || "",
moduleCode: role.moduleCode || moduleCode,
description: role.description || "",
parentRoleId: role.parentRoleId || role.parentRole?.id,
permissionIdList: role.permissionIdList || role.permissionList?.map(item => item.id) || [],
childRoleIdList: role.childRoleIdList || role.childRoleList?.map(item => item.id).filter((id): id is string => Boolean(id)) || []
childRoleIdList: role.childRoleIdList || role.childRoleList?.map(item => item.id).filter((id): id is string => Boolean(id)) || [],
delegationPermissionIdList: role.delegationPermissionIdList || role.delegationPermissionList?.map((item: Permission) => item.id) || []
};
}
@@ -57,7 +46,8 @@ function createEmptyRole(): Role {
name: "",
description: "",
permissionIdList: [],
childRoleIdList: []
childRoleIdList: [],
delegationPermissionIdList: []
};
}
@@ -74,13 +64,15 @@ function createEmptyPermission(): Permission {
export const usePermissionSettingsStore = defineStore("permission-settings", () => {
const roleList = ref<Role[]>([]);
const permissionList = ref<Permission[]>([]);
const manageableRoleList = ref<Role[]>([]);
const grantablePermissionList = ref<Permission[]>([]);
const loading = ref(false);
const loaded = ref(false);
let loadTask: Promise<void> | null = null;
const roleTree = computed<Array<Role & { level: number }>>(() => {
const childIdSet = new Set(roleList.value.flatMap(item => item.childRoleIdList));
const roots = roleList.value.filter(item => !childIdSet.has(item.id));
const roleIdSet = new Set(roleList.value.map(item => item.id));
const roots = roleList.value.filter(item => !item.parentRoleId || !roleIdSet.has(item.parentRoleId));
return roots.flatMap(item => buildRoleTree(item.id, 0, new Set<string>()));
});
@@ -143,6 +135,38 @@ export const usePermissionSettingsStore = defineStore("permission-settings", ()
return role;
}
// ---------- 授权范围 ----------
async function loadManageableRoles(): Promise<Role[]> {
manageableRoleList.value = [];
const list = await RoleAPI.manageableList(moduleCode);
manageableRoleList.value = (list || []).map(normalizeRole);
return manageableRoleList.value;
}
async function loadGrantablePermissions(targetModuleCode: ModuleCode = moduleCode): Promise<Permission[]> {
grantablePermissionList.value = [];
const list = await PermissionAPI.grantableList(targetModuleCode);
grantablePermissionList.value = list || [];
return grantablePermissionList.value;
}
async function loadGrantableRolePermissions(roleId: string): Promise<Permission[]> {
return await RoleAPI.grantablePermissionList(roleId);
}
async function loadDelegationPermissions(roleId: string): Promise<Permission[]> {
return await RoleAPI.delegationList(roleId);
}
async function loadGrantableDelegationPermissions(roleId: string): Promise<Permission[]> {
return await RoleAPI.grantableDelegationList(roleId);
}
async function loadParentGrantableRoles(roleId: string): Promise<Role[]> {
return (await RoleAPI.parentGrantableList(roleId)).map(normalizeRole);
}
async function loadPermissionDetail(id: string): Promise<Permission> {
if (!id) {
return createEmptyPermission();
@@ -185,10 +209,13 @@ export const usePermissionSettingsStore = defineStore("permission-settings", ()
const nextVisitedRoleIds = new Set(visitedRoleIds);
nextVisitedRoleIds.add(roleId);
const childRoleIdList = roleList.value
.filter(item => item.parentRoleId === roleId)
.map(item => item.id);
return [{
...current,
level
}, ...current.childRoleIdList.flatMap(childRoleId => buildRoleTree(childRoleId, level + 1, nextVisitedRoleIds))];
}, ...childRoleIdList.flatMap(childRoleId => buildRoleTree(childRoleId, level + 1, nextVisitedRoleIds))];
}
function getRoleById(id: string): Role | undefined {
@@ -209,17 +236,13 @@ export const usePermissionSettingsStore = defineStore("permission-settings", ()
function getInheritedPermissionList(role: Role): Permission[] {
const permissionMap = new Map<string, Permission>();
const allPermissionList = role.allPermissionList || [];
for (const permission of [...getRolePermissionList(role), ...allPermissionList]) {
const permissionList = role.allPermissionList?.length
? role.allPermissionList
: getRolePermissionList(role);
for (const permission of permissionList) {
permissionMap.set(permission.id, permission);
}
for (const childRole of getChildRoles(role)) {
for (const permission of getRolePermissionList(childRole)) {
permissionMap.set(permission.id, permission);
}
}
return Array.from(permissionMap.values());
}
@@ -227,28 +250,38 @@ export const usePermissionSettingsStore = defineStore("permission-settings", ()
async function upsertRole(draft: RoleDraft, createMode: boolean): Promise<Role | undefined> {
const current = getRoleById(draft.id || "");
const code = draft.code.trim();
const name = draft.name.trim();
const description = draft.description.trim();
const payload: RolePayload = {
...draft,
moduleCode: moduleCode,
code: draft.code.trim(),
code,
nameLangId: current?.nameLangId,
name: draft.name.trim(),
description: draft.description.trim(),
permissionIdList: [...draft.permissionIdList],
childRoleIdList: [...draft.childRoleIdList]
name,
description
};
if (draft.permissionIdList !== undefined) {
payload.permissionIdList = [...draft.permissionIdList];
}
if (draft.childRoleIdList !== undefined) {
payload.childRoleIdList = [...draft.childRoleIdList];
}
if (createMode) {
await RoleAPI.create({
moduleCode: payload.moduleCode,
code: payload.code,
name: payload.name,
description: payload.description,
moduleCode: moduleCode,
code,
name,
description,
parentRoleId: payload.parentRoleId,
ownerType: payload.ownerType,
ownerId: payload.ownerId,
permissionIdList: payload.permissionIdList,
childRoleIdList: payload.childRoleIdList
});
await loadAll(true);
return undefined;
return roleList.value.find(item => item.code === payload.code && item.moduleCode === moduleCode);
}
await RoleAPI.update(payload);
@@ -263,6 +296,7 @@ export const usePermissionSettingsStore = defineStore("permission-settings", ()
.filter(item => item.id !== id)
.map(item => ({
...item,
parentRoleId: item.parentRoleId === id ? undefined : item.parentRoleId,
childRoleIdList: item.childRoleIdList.filter(childId => childId !== id)
}));
}
@@ -305,25 +339,56 @@ export const usePermissionSettingsStore = defineStore("permission-settings", ()
}
async function setRolePermissionList(roleId: string, permissionIds: string[]): Promise<void> {
await RoleAPI.update({
id: roleId,
permissionIdList: [...permissionIds]
});
const role = await loadRoleDetail(roleId);
await upsertRole({
id: role.id,
moduleCode: moduleCode,
code: role.code,
name: role.name || "",
description: role.description || "",
permissionIdList: [...permissionIds],
childRoleIdList: [...role.childRoleIdList]
}, false);
upsertLocalRole(role);
}
async function setRoleChildRoleList(roleId: string, childRoleIds: string[]): Promise<void> {
await RoleAPI.update({
id: roleId,
childRoleIdList: [...childRoleIds]
});
const role = await loadRoleDetail(roleId);
upsertLocalRole(role);
}
async function setRoleDelegationPermissionList(roleId: string, permissionIds: string[]): Promise<void> {
await RoleAPI.updateDelegation({
id: roleId,
delegationPermissionIdList: permissionIds
});
const role = await loadRoleDetail(roleId);
upsertLocalRole(role);
}
async function setRoleParentRole(roleId: string, parentRoleId: string): Promise<void> {
await RoleAPI.updateParent({
id: roleId,
parentRoleId
});
const role = await loadRoleDetail(roleId);
upsertLocalRole(role);
}
return {
roleList,
permissionList,
manageableRoleList,
grantablePermissionList,
roleTree,
loading,
loaded,
loadAll,
loadManageableRoles,
loadGrantablePermissions,
loadGrantableRolePermissions,
loadDelegationPermissions,
loadGrantableDelegationPermissions,
loadParentGrantableRoles,
loadRoleDetail,
loadPermissionDetail,
getRoleById,
@@ -335,7 +400,10 @@ export const usePermissionSettingsStore = defineStore("permission-settings", ()
removeRole,
upsertPermission,
removePermission,
setRolePermissionList
setRolePermissionList,
setRoleChildRoleList,
setRoleDelegationPermissionList,
setRoleParentRole
};
});
+62
View File
@@ -0,0 +1,62 @@
import type { AuthOwnerType, ModuleCode, Permission as UserPermission, Role as UserRole } from "timi-web";
export type Permission = UserPermission & {
builtin?: boolean;
protectedPermission?: boolean;
ownerType?: AuthOwnerType;
ownerId?: string;
createdBy?: string;
};
export type Role = Omit<
UserRole,
"id" | "permissionIdList" | "childRoleIdList" | "delegationPermissionIdList" | "delegationPermissionList"
> & {
id: string;
permissionIdList: string[];
childRoleIdList: string[];
delegationPermissionIdList: string[];
builtin?: boolean;
protectedRole?: boolean;
ownerType?: AuthOwnerType;
ownerId?: string;
createdBy?: string;
permissionList?: Permission[];
allPermissionList?: Permission[];
childRoleList?: UserRole[];
parentRole?: UserRole;
delegationPermissionList?: Permission[];
};
export type RoleDraft = {
id?: string;
code: string;
name: string;
description: string;
nameLangId?: string;
moduleCode?: ModuleCode;
parentRoleId?: string;
ownerType?: AuthOwnerType;
ownerId?: string;
permissionIdList?: string[];
childRoleIdList?: string[];
delegationPermissionIdList?: string[];
};
export type RolePayload = Partial<Pick<Role, "moduleCode" | "code" | "nameLangId" | "name" | "description">> & {
id?: string;
parentRoleId?: string;
ownerType?: AuthOwnerType;
ownerId?: string;
permissionIdList?: string[];
childRoleIdList?: string[];
};
export type PermissionDraft = {
id?: string;
code: string;
name: string;
description: string;
nameLangId?: string;
moduleCode?: ModuleCode;
};
+29 -15
View File
@@ -58,10 +58,10 @@ export const corePermissionCodes = {
read: "CORE:ROLE_PERMISSION:READ",
delete: "CORE:ROLE_PERMISSION:DELETE"
},
roleRelation: {
create: "CORE:ROLE_RELATION:CREATE",
read: "CORE:ROLE_RELATION:READ",
delete: "CORE:ROLE_RELATION:DELETE"
rolePermissionInherit: {
create: "CORE:ROLE_PERMISSION_INHERIT:CREATE",
read: "CORE:ROLE_PERMISSION_INHERIT:READ",
delete: "CORE:ROLE_PERMISSION_INHERIT:DELETE"
},
userRole: {
create: "CORE:USER_ROLE:CREATE",
@@ -71,23 +71,37 @@ export const corePermissionCodes = {
} as const;
export const coreRoleCodes = {
admin: "CORE:ADMIN",
userRoleAdmin: "CORE:USER_ROLE_ADMIN"
admin: "CORE:ADMIN"
} as const;
export const gaoRoleCodes = {
globalManager: "GAO:GLOBAL_MANAGER",
storeManager: "GAO:STORE_MANAGER",
employee: "GAO:EMPLOYEE"
storeManager: "GAO:STORE_MANAGER"
} as const;
export const permissionManagementRoleAccess = {
value: [
coreRoleCodes.admin,
coreRoleCodes.userRoleAdmin
],
mode: "or"
} as const;
/** 需要读取登录用户所属门店的业务权限。 */
export const gaoStoreScopePermissionCodes = [
permissionCodes.customer.create,
permissionCodes.customer.read,
permissionCodes.customer.export,
permissionCodes.customer.update,
permissionCodes.customer.delete,
permissionCodes.event.create,
permissionCodes.event.read,
permissionCodes.event.update,
permissionCodes.event.delete,
permissionCodes.eventRecord.create,
permissionCodes.eventRecord.read,
permissionCodes.eventRecord.export,
permissionCodes.eventRecord.update,
permissionCodes.eventRecord.delete,
permissionCodes.stat.read,
permissionCodes.user.create,
permissionCodes.user.read,
permissionCodes.user.update,
permissionCodes.user.delete,
permissionCodes.qrCode.create
];
export type PermissionCodes = typeof permissionCodes;
export type CorePermissionCodes = typeof corePermissionCodes;
+12 -35
View File
@@ -7,25 +7,18 @@ function buildRoleCode(role: Role): string {
return role.moduleCode && !role.code.includes(":") ? `${role.moduleCode}:${role.code}` : role.code;
}
function getBareRoleCode(code: string): string {
const codePartList = code.split(":");
return codePartList[codePartList.length - 1] || code;
}
function findRole(role: ManagedUserRole, roleList: Role[]): Role | undefined {
if (typeof role === "string") {
return roleList.find(roleItem => roleItem.code === role || buildRoleCode(roleItem) === role);
return roleList.find(roleItem => buildRoleCode(roleItem) === role);
}
return roleList.find(roleItem => roleItem.id === role.id || roleItem.code === role.code || buildRoleCode(roleItem) === role.code);
return roleList.find(roleItem => roleItem.id === role.id || buildRoleCode(roleItem) === buildRoleCode(role as Role));
}
function appendCompatibleRoleCode(roleCodeSet: Set<string>, code: string): void {
if (!code) {
return;
export function getManagedUserRoleList(user: ManagedUser): ManagedUserRole[] {
if (user.user?.roleEntityList?.length) {
return user.user.roleEntityList;
}
roleCodeSet.add(code);
roleCodeSet.add(getBareRoleCode(code));
return user.roleList || user.user?.roleList || [];
}
export function getRoleDisplayName(role: ManagedUserRole | undefined, roleList: Role[]): string {
@@ -38,34 +31,18 @@ export function getRoleDisplayName(role: ManagedUserRole | undefined, roleList:
return fullRole.name || fullRole.code || "未配置角色";
}
return typeof role === "string" ? getBareRoleCode(role) : role.name || role.code || "未配置角色";
return typeof role === "string" ? role : role.name || role.code || "未配置角色";
}
export function buildEffectiveRoleCodeSet(user: ManagedUser, roleList: Role[]): Set<string> {
export function buildEffectiveRoleCodeSet(user: ManagedUser): Set<string> {
const roleCodeSet = new Set<string>();
for (const role of user.roleList || []) {
const fullRole = findRole(role, roleList);
if (fullRole) {
appendRoleCode(roleCodeSet, fullRole, roleList, new Set<string>());
for (const role of getManagedUserRoleList(user)) {
if (typeof role !== "string") {
roleCodeSet.add(buildRoleCode(role as Role));
continue;
}
appendCompatibleRoleCode(roleCodeSet, typeof role === "string" ? role : role.code);
roleCodeSet.add(role);
}
return roleCodeSet;
}
function appendRoleCode(roleCodeSet: Set<string>, role: Role, roleList: Role[], visitedRoleIds: Set<string>): void {
if (!role.id || visitedRoleIds.has(role.id)) {
return;
}
visitedRoleIds.add(role.id);
appendCompatibleRoleCode(roleCodeSet, buildRoleCode(role));
for (const childRoleId of role.childRoleIdList) {
const childRole = roleList.find(item => item.id === childRoleId);
if (childRole) {
appendRoleCode(roleCodeSet, childRole, roleList, visitedRoleIds);
}
}
}