init: UserBaseFront (frontend)
This commit is contained in:
@@ -0,0 +1,513 @@
|
||||
<template>
|
||||
<div class="main-wrapper tree-wrapper">
|
||||
<div class="tree-container">
|
||||
<Input v-model="userKeyword" placeholder="输入用户,回车查找" suffix="ios-search" search clearable
|
||||
class="search-input" @on-search="onSearchUser" @on-clear="onClearUser" />
|
||||
<GroupTreeSearchPanel ref="groupTreeRef" :treeApi="fetchGroupTree" :autoClick="true"
|
||||
:showCloseButton="false" cacheKey="permission-view.group" @on-change="onGroupChange" />
|
||||
</div>
|
||||
<div class="main-container">
|
||||
<div class="user-panel">
|
||||
<div class="panel-title">
|
||||
<template v-if="mode === 'group'">组用户列表</template>
|
||||
<template v-else>用户搜索结果</template>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div v-if="userLoading" class="loading-tip">加载中...</div>
|
||||
<div v-else-if="userList.length === 0" class="empty-tip">
|
||||
<template v-if="mode === 'group'">请选择左侧组节点</template>
|
||||
<template v-else>未找到匹配用户</template>
|
||||
</div>
|
||||
<div v-else class="user-list">
|
||||
<div v-for="user in userList" :key="user.id" class="user-item"
|
||||
:class="{ active: selectedUser?.id === user.id }" @click="onSelectUser(user)">
|
||||
<div class="user-name">{{ userDisplayName(user) }}</div>
|
||||
<div v-if="mode === 'user' && user.groupPath" class="user-path">{{ user.groupPath }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="perm-panel">
|
||||
<div class="panel-title">
|
||||
<template v-if="selectedUser">{{ selectedUser.realName || selectedUser.account }}({{
|
||||
selectedUser.account }}) 权限浏览</template>
|
||||
<template v-else>权限浏览</template>
|
||||
</div>
|
||||
<div v-if="!selectedUser" class="panel-body">
|
||||
<div class="empty-tip">请选择左侧用户查看权限</div>
|
||||
</div>
|
||||
<div v-else class="perm-body">
|
||||
<!-- 左列:所属部门(上) + 所属角色(下) -->
|
||||
<div class="perm-side">
|
||||
<div class="side-block">
|
||||
<div class="side-title">所属部门</div>
|
||||
<div class="side-body">
|
||||
<div v-if="treeLoading" class="loading-tip small">加载中...</div>
|
||||
<div v-else-if="deptTree.length === 0" class="empty-tip small">暂无部门归属</div>
|
||||
<MyTree v-else :treeData="deptTree" :setting="treeSetting" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="side-block">
|
||||
<div class="side-title">所属角色</div>
|
||||
<div class="side-body">
|
||||
<div v-if="treeLoading" class="loading-tip small">加载中...</div>
|
||||
<div v-else-if="roleTree.length === 0" class="empty-tip small">暂无所属角色</div>
|
||||
<MyTree v-else :treeData="roleTree" :setting="treeSetting" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 右列:已授权应用(原有) -->
|
||||
<div class="perm-main">
|
||||
<div class="side-title">已授权应用</div>
|
||||
<div class="side-body">
|
||||
<div v-if="treeLoading" class="loading-tip">权限加载中...</div>
|
||||
<div v-else-if="permissionTree.length === 0" class="empty-tip">该用户暂无已授权应用模块</div>
|
||||
<MyTree v-else ref="permTreeRef" :treeData="permissionTree" :setting="treeSetting" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from "vue";
|
||||
import { Input, Message } from "view-ui-plus";
|
||||
import GroupTreeSearchPanel from "@/components/GroupTreeSearchPanel.vue";
|
||||
import MyTree from "@/components/MyTree.vue";
|
||||
import Apis from "@/api";
|
||||
|
||||
interface UserItem {
|
||||
id?: number;
|
||||
account?: string;
|
||||
realName?: string;
|
||||
isMain?: number;
|
||||
groupPath?: string;
|
||||
}
|
||||
|
||||
interface ModuleNode {
|
||||
id?: number;
|
||||
parentId?: number;
|
||||
name?: string;
|
||||
customId?: string;
|
||||
path?: string;
|
||||
level?: number;
|
||||
sortNo?: number;
|
||||
children?: ModuleNode[];
|
||||
}
|
||||
|
||||
interface AppNode {
|
||||
id?: number;
|
||||
name?: string;
|
||||
appKey?: number;
|
||||
pcIcon?: string;
|
||||
sortNo?: number;
|
||||
modules?: ModuleNode[];
|
||||
}
|
||||
|
||||
interface CategoryNode {
|
||||
id?: number;
|
||||
name?: string;
|
||||
sortNo?: number;
|
||||
apps?: AppNode[];
|
||||
}
|
||||
|
||||
interface GroupNode {
|
||||
id?: number;
|
||||
name?: string;
|
||||
own?: boolean;
|
||||
children?: GroupNode[];
|
||||
}
|
||||
|
||||
interface RoleItem {
|
||||
id?: number;
|
||||
name?: string;
|
||||
allAppFlag?: boolean;
|
||||
appId?: number;
|
||||
appName?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const groupTreeRef = ref<InstanceType<typeof GroupTreeSearchPanel> | null>(null);
|
||||
const permTreeRef = ref<InstanceType<typeof MyTree> | null>(null);
|
||||
|
||||
const mode = ref<"group" | "user">("group");
|
||||
const userKeyword = ref("");
|
||||
const selectedGroup = ref<any>(null);
|
||||
const selectedUser = ref<UserItem | null>(null);
|
||||
const userList = ref<UserItem[]>([]);
|
||||
const permissionTree = ref<any[]>([]);
|
||||
const deptTree = ref<any[]>([]);
|
||||
const roleTree = ref<any[]>([]);
|
||||
const userLoading = ref(false);
|
||||
const treeLoading = ref(false);
|
||||
|
||||
const treeSetting = ref({
|
||||
view: { dblClickExpand: false, showIcon: false, showLine: true },
|
||||
edit: { enable: false, showRemoveBtn: false, showRenameBtn: false, drag: false },
|
||||
check: { enable: false },
|
||||
data: {
|
||||
simpleData: { enable: false },
|
||||
key: { children: "children" },
|
||||
},
|
||||
callback: {},
|
||||
});
|
||||
|
||||
function fetchGroupTree() {
|
||||
return Apis.SysGroup.getTree();
|
||||
}
|
||||
|
||||
function userDisplayName(user: UserItem): string {
|
||||
const name = user.realName || user.account || "";
|
||||
const account = user.account || "";
|
||||
return name === account ? name : `${name}(${account})`;
|
||||
}
|
||||
|
||||
function onGroupChange(node: any) {
|
||||
mode.value = "group";
|
||||
selectedGroup.value = node;
|
||||
selectedUser.value = null;
|
||||
clearPermPanels();
|
||||
loadGroupUsers(node?.id);
|
||||
}
|
||||
|
||||
function clearPermPanels() {
|
||||
permissionTree.value = [];
|
||||
deptTree.value = [];
|
||||
roleTree.value = [];
|
||||
}
|
||||
|
||||
async function loadGroupUsers(groupId?: number) {
|
||||
if (!groupId) {
|
||||
userList.value = [];
|
||||
return;
|
||||
}
|
||||
userLoading.value = true;
|
||||
try {
|
||||
userList.value = (await Apis.GroupAuthz.groupUsers({ params: { groupId }, cacheFor: null })) as UserItem[];
|
||||
} catch (e: any) {
|
||||
Message.error(e?.message || "加载组用户失败");
|
||||
userList.value = [];
|
||||
} finally {
|
||||
userLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function onSearchUser() {
|
||||
const kw = userKeyword.value.trim();
|
||||
if (!kw) {
|
||||
onClearUser();
|
||||
return;
|
||||
}
|
||||
mode.value = "user";
|
||||
selectedGroup.value = null;
|
||||
selectedUser.value = null;
|
||||
clearPermPanels();
|
||||
userLoading.value = true;
|
||||
try {
|
||||
userList.value = (await Apis.GroupAuthz.searchUsers({ params: { keyword: kw }, cacheFor: null })) as UserItem[];
|
||||
} catch (e: any) {
|
||||
Message.error(e?.message || "搜索用户失败");
|
||||
userList.value = [];
|
||||
} finally {
|
||||
userLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function onClearUser() {
|
||||
userKeyword.value = "";
|
||||
mode.value = "group";
|
||||
selectedUser.value = null;
|
||||
clearPermPanels();
|
||||
if (selectedGroup.value?.id) {
|
||||
loadGroupUsers(Number(selectedGroup.value.id));
|
||||
} else {
|
||||
userList.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
async function onSelectUser(user: UserItem) {
|
||||
selectedUser.value = user;
|
||||
treeLoading.value = true;
|
||||
const userId = user.id as number;
|
||||
const groupId = selectedGroup.value?.id;
|
||||
// 三块数据并行加载,互不阻塞
|
||||
// userGroupTree 为新增接口:apiDefinitions 未重新生成(npm run api:gen)时方法不存在,降级为友好报错
|
||||
const userGroupTreeApi = (Apis.GroupAuthz as any).userGroupTree
|
||||
? Apis.GroupAuthz.userGroupTree({ params: { userId }, cacheFor: null })
|
||||
: Promise.reject(new Error("所属部门接口未生成,请重启后端后执行 npm run api:gen"));
|
||||
const [permRes, deptRes, roleRes] = await Promise.allSettled([
|
||||
Apis.GroupAuthz.userPermissionTree({
|
||||
params: { userId, groupId: groupId ? Number(groupId) : undefined },
|
||||
cacheFor: null,
|
||||
}),
|
||||
userGroupTreeApi,
|
||||
Apis.GroupAuthz.userRoles({ params: { userId }, cacheFor: null }),
|
||||
]);
|
||||
if (permRes.status === "fulfilled") {
|
||||
permissionTree.value = formatPermissionTree((permRes.value as CategoryNode[]) || []);
|
||||
} else {
|
||||
Message.error(permRes.reason?.message || "加载权限树失败");
|
||||
permissionTree.value = [];
|
||||
}
|
||||
if (deptRes.status === "fulfilled") {
|
||||
deptTree.value = formatDeptTree((deptRes.value as GroupNode[]) || []);
|
||||
} else {
|
||||
Message.error(deptRes.reason?.message || "加载所属部门失败");
|
||||
deptTree.value = [];
|
||||
}
|
||||
if (roleRes.status === "fulfilled") {
|
||||
roleTree.value = formatRoleTree((roleRes.value as RoleItem[]) || []);
|
||||
} else {
|
||||
Message.error(roleRes.reason?.message || "加载所属角色失败");
|
||||
roleTree.value = [];
|
||||
}
|
||||
treeLoading.value = false;
|
||||
}
|
||||
|
||||
// 所属部门:祖先链树,用户直接归属的组加「本组」标记
|
||||
function formatDeptTree(nodes: GroupNode[]): any[] {
|
||||
return (nodes || []).map((n) => ({
|
||||
id: `grp_${n.id}`,
|
||||
name: n.own ? `${n.name || ""}(本组)` : n.name || "",
|
||||
open: true,
|
||||
children: formatDeptTree(n.children || []),
|
||||
isParent: (n.children || []).length > 0,
|
||||
}));
|
||||
}
|
||||
|
||||
// 所属角色:2 级树 —— 一级为「平台角色」(全应用) / 应用名分组,二级为角色
|
||||
function formatRoleTree(roles: RoleItem[]): any[] {
|
||||
const groups = new Map<string, any>();
|
||||
for (const r of roles) {
|
||||
const key = r.allAppFlag ? "平台角色" : r.appName || "未知应用";
|
||||
if (!groups.has(key)) {
|
||||
groups.set(key, { id: `rgrp_${key}`, name: key, open: true, isParent: true, children: [] });
|
||||
}
|
||||
groups.get(key).children.push({
|
||||
id: `role_${r.id}`,
|
||||
name: r.disabled ? `${r.name}(已禁用)` : r.name,
|
||||
});
|
||||
}
|
||||
// 平台角色置顶,其余按应用名
|
||||
return [...groups.values()].sort((a, b) =>
|
||||
a.name === "平台角色" ? -1 : b.name === "平台角色" ? 1 : a.name.localeCompare(b.name)
|
||||
);
|
||||
}
|
||||
|
||||
function formatPermissionTree(categories: CategoryNode[]): any[] {
|
||||
const result: any[] = [];
|
||||
for (const cat of categories) {
|
||||
const catNode: any = {
|
||||
id: `cat_${cat.id}`,
|
||||
name: cat.name || `应用类型${cat.id}`,
|
||||
open: true,
|
||||
children: formatApps(cat.apps || [], cat.id),
|
||||
};
|
||||
catNode.isParent = catNode.children.length > 0;
|
||||
result.push(catNode);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function formatApps(apps: AppNode[], catId?: number): any[] {
|
||||
return (apps || []).map((app) => ({
|
||||
id: `app_${app.id}_${catId || 0}`,
|
||||
name: app.name || `应用${app.id}`,
|
||||
open: true,
|
||||
children: formatModules(app.modules || []),
|
||||
}));
|
||||
}
|
||||
|
||||
function formatModules(modules: ModuleNode[]): any[] {
|
||||
return (modules || []).map((m) => {
|
||||
const children = formatModules(m.children || []);
|
||||
return {
|
||||
id: `mod_${m.id}`,
|
||||
name: m.name || `模块${m.id}`,
|
||||
open: true,
|
||||
children,
|
||||
isParent: children.length > 0,
|
||||
};
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tree-container {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 8px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.main-container {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.user-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
width: 240px;
|
||||
min-width: 240px;
|
||||
border-right: 1px solid #e8eaec;
|
||||
}
|
||||
|
||||
.user-search {
|
||||
padding: 12px 16px;
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.perm-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
padding: 10px 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
border-bottom: 1px solid #e8eaec;
|
||||
background: #fafafa;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
/* 权限浏览 3 块布局:左列(所属部门/所属角色) + 右列(已授权应用) */
|
||||
.perm-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.perm-side {
|
||||
width: 260px;
|
||||
min-width: 260px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
border-right: 1px solid #e8eaec;
|
||||
}
|
||||
|
||||
.side-block {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&+.side-block {
|
||||
border-top: 1px solid #e8eaec;
|
||||
}
|
||||
}
|
||||
|
||||
.perm-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.side-title {
|
||||
flex-shrink: 0;
|
||||
padding: 8px 16px;
|
||||
font-weight: 600;
|
||||
color: #515a6e;
|
||||
font-size: 13px;
|
||||
background: #fafafa;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.side-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding: 6px 0;
|
||||
}
|
||||
|
||||
.empty-tip.small,
|
||||
.loading-tip.small {
|
||||
padding: 20px 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.user-item {
|
||||
padding: 10px 16px;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
transition: background 0.15s;
|
||||
|
||||
&:hover {
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: #fff3e0;
|
||||
}
|
||||
}
|
||||
|
||||
.user-name {
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.user-path {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
line-height: 1.3;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.empty-tip,
|
||||
.loading-tip {
|
||||
color: #999;
|
||||
padding: 48px 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loading-tip {
|
||||
color: #2d8cf0;
|
||||
}
|
||||
|
||||
:deep(.group-tree-search-panel) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.ztree) {
|
||||
font-size: 14px;
|
||||
|
||||
li {
|
||||
line-height: 26px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user