滚动条(上次改动引入的回归):
- MyTree 根节点加 .my-tree 类并打通高度链(flex 纵向 + .ztree 承担滚动),
父级定高时滚动条只落在树节点区域;父级 auto 的卡片内树行为不变
- GroupTreeSearchPanel 的 .tree-body 改 flex 纵向传递高度,删除已失效的 .ztree 规则
叶子节点 +/−(以组用户管理左侧树为参考统一):
- zTree 只要 children 是数组即强制 isParent=true,故空数组必须不挂键;
修正 GroupAuth / AppModuleCheckTree / PermissionView 共 9 处节点构造
- MyTree 初始化前加节点规范化兜底,防止后续新增树重犯
- removeNode 删除末个子节点后同步父节点父/叶状态,不再残留 +/−
权限一览:
- 删除 li{line-height:26px} 覆盖(zTree 图标按 21px 节距,26px 会让连接线出现断口)
- 左列改用页面级 permission-tree 类:flex 纵向 + overflow:hidden,
消除整列滚动与外层裁切,滚动收敛到树体
This commit is contained in:
@@ -285,10 +285,13 @@ defineExpose({
|
|||||||
.tree-body {
|
.tree-body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-body :deep(.ztree) {
|
/* 滚动职责统一由 MyTree(.my-tree > .ztree) 承担:此处只负责把可用高度传给子组件 */
|
||||||
height: 100%;
|
.tree-body > .my-tree {
|
||||||
overflow-y: auto;
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,9 +1,35 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="my-tree">
|
||||||
<div :id="domId" :ref="domId" class="ztree"></div>
|
<div :id="domId" :ref="domId" class="ztree"></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
|
/**
|
||||||
|
* 节点规范化兜底:zTree 判定父节点的规则是「children 是数组即 isParent=true」(jquery.ztree.all.min.js
|
||||||
|
* 中 `tools.isArray(s) ? (data.nodeIsParent(setting,n,!0), ...)`),因此叶子节点只要被构造成
|
||||||
|
* `children: []` 就会渲染出 +/− 展开图标——此时再写 isParent:false 也无效(被数组分支覆盖)。
|
||||||
|
* 这里统一按「空 children 不挂键」,与参考实现 GroupTreeSearchPanel.formatTree 的口径一致。
|
||||||
|
*/
|
||||||
|
function normalizeTreeNodes(nodes) {
|
||||||
|
if (Array.isArray(nodes)) {
|
||||||
|
nodes.forEach(normalizeTreeNode);
|
||||||
|
} else if (nodes) {
|
||||||
|
normalizeTreeNode(nodes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeTreeNode(node) {
|
||||||
|
if (!node || typeof node !== "object") return;
|
||||||
|
const kids = node.children;
|
||||||
|
if (Array.isArray(kids) && kids.length > 0) {
|
||||||
|
node.isParent = true;
|
||||||
|
kids.forEach(normalizeTreeNode);
|
||||||
|
} else {
|
||||||
|
delete node.children;
|
||||||
|
node.isParent = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
treeData: {
|
treeData: {
|
||||||
@@ -65,6 +91,7 @@ export default {
|
|||||||
},
|
},
|
||||||
initTree(_data, _domId) {
|
initTree(_data, _domId) {
|
||||||
const _this = this;
|
const _this = this;
|
||||||
|
normalizeTreeNodes(_data);
|
||||||
_this.treeSetting.callback.onClick = (event, treeId, treeNode, clickFlag) => {
|
_this.treeSetting.callback.onClick = (event, treeId, treeNode, clickFlag) => {
|
||||||
// console.log(event, treeId, treeNode, clickFlag);
|
// console.log(event, treeId, treeNode, clickFlag);
|
||||||
if (_this.isCheck) {
|
if (_this.isCheck) {
|
||||||
@@ -132,7 +159,10 @@ export default {
|
|||||||
var node = treeObj.getNodesByFilter(node => node.id == id, true);
|
var node = treeObj.getNodesByFilter(node => node.id == id, true);
|
||||||
var pnode = node.getParentNode();
|
var pnode = node.getParentNode();
|
||||||
treeObj.removeNode(node);
|
treeObj.removeNode(node);
|
||||||
pnode.isParent = true;
|
// 同步父节点的父/叶状态:删掉最后一个子节点后不能继续显示 +/− 展开图标
|
||||||
|
const hasChild = !!(pnode.children && pnode.children.length);
|
||||||
|
pnode.isParent = hasChild;
|
||||||
|
pnode.open = hasChild && pnode.open;
|
||||||
treeObj.updateNode(pnode);
|
treeObj.updateNode(pnode);
|
||||||
this.selectNode(pnode.id);
|
this.selectNode(pnode.id);
|
||||||
},
|
},
|
||||||
@@ -211,6 +241,21 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
/* 高度链:父级定高时 .ztree 成为唯一滚动体(滚动条只在树节点区域);
|
||||||
|
父级高度 auto(如卡片内按内容展开的树)时 height:100% 退化为 auto,表现为按内容展开,不受影响 */
|
||||||
|
.my-tree {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.my-tree > .ztree {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.ztree-search-highlight {
|
.ztree-search-highlight {
|
||||||
background-color: #ff6600 !important;
|
background-color: #ff6600 !important;
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-4
@@ -107,6 +107,7 @@ type AuthzTreeNode = {
|
|||||||
nodeType: "app" | "module";
|
nodeType: "app" | "module";
|
||||||
rawId: number;
|
rawId: number;
|
||||||
appId: number;
|
appId: number;
|
||||||
|
isParent?: boolean;
|
||||||
children?: AuthzTreeNode[];
|
children?: AuthzTreeNode[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -230,8 +231,10 @@ function isModuleAllInherit(node: AppModuleVo): boolean {
|
|||||||
return (node.children || []).every((c) => isModuleAllInherit(c));
|
return (node.children || []).every((c) => isModuleAllInherit(c));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 空 children 不挂键:zTree 只要见到 children 是数组就强制 isParent=true 而渲染 +/−(叶子也会出现展开图标)
|
||||||
function buildModuleNode(m: AppModuleVo, appId: number): AuthzTreeNode {
|
function buildModuleNode(m: AppModuleVo, appId: number): AuthzTreeNode {
|
||||||
return {
|
const kids = (m.children || []).map((c) => buildModuleNode(c, appId));
|
||||||
|
const node: AuthzTreeNode = {
|
||||||
id: `mod_${m.id}`,
|
id: `mod_${m.id}`,
|
||||||
name: String(m.name || ""),
|
name: String(m.name || ""),
|
||||||
open: true,
|
open: true,
|
||||||
@@ -240,13 +243,16 @@ function buildModuleNode(m: AppModuleVo, appId: number): AuthzTreeNode {
|
|||||||
nodeType: "module",
|
nodeType: "module",
|
||||||
rawId: Number(m.id),
|
rawId: Number(m.id),
|
||||||
appId,
|
appId,
|
||||||
children: (m.children || []).map((c) => buildModuleNode(c, appId)),
|
isParent: kids.length > 0,
|
||||||
};
|
};
|
||||||
|
if (kids.length > 0) node.children = kids;
|
||||||
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildAppNode(app: AppItem): AuthzTreeNode {
|
function buildAppNode(app: AppItem): AuthzTreeNode {
|
||||||
const appId = Number(app.id);
|
const appId = Number(app.id);
|
||||||
return {
|
const kids = (app.modules || []).map((m) => buildModuleNode(m, appId));
|
||||||
|
const node: AuthzTreeNode = {
|
||||||
id: `app_${appId}`,
|
id: `app_${appId}`,
|
||||||
name: String(app.name || ""),
|
name: String(app.name || ""),
|
||||||
open: false, // 一级默认闭合,点击才展开模块
|
open: false, // 一级默认闭合,点击才展开模块
|
||||||
@@ -255,8 +261,10 @@ function buildAppNode(app: AppItem): AuthzTreeNode {
|
|||||||
nodeType: "app",
|
nodeType: "app",
|
||||||
rawId: appId,
|
rawId: appId,
|
||||||
appId,
|
appId,
|
||||||
children: (app.modules || []).map((m) => buildModuleNode(m, appId)),
|
isParent: kids.length > 0,
|
||||||
};
|
};
|
||||||
|
if (kids.length > 0) node.children = kids;
|
||||||
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 分类 -> zTree 数据(应用为一级节点,模块为子节点);weight 用于三列均衡
|
// 分类 -> zTree 数据(应用为一级节点,模块为子节点);weight 用于三列均衡
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="main-wrapper tree-wrapper">
|
<div class="main-wrapper tree-wrapper permission-view">
|
||||||
<div class="tree-container">
|
<div class="tree-container permission-tree">
|
||||||
<Input v-model="userKeyword" placeholder="输入用户,回车查找" suffix="ios-search" search clearable
|
<Input v-model="userKeyword" placeholder="输入用户,回车查找" suffix="ios-search" search clearable
|
||||||
class="search-input" @on-search="onSearchUser" @on-clear="onClearUser" />
|
class="search-input" @on-search="onSearchUser" @on-clear="onClearUser" />
|
||||||
<GroupTreeSearchPanel ref="groupTreeRef" :treeApi="fetchGroupTree" :autoClick="true"
|
<GroupTreeSearchPanel ref="groupTreeRef" :treeApi="fetchGroupTree" :autoClick="true"
|
||||||
@@ -389,19 +389,21 @@ function formatRoleAuthzTree(apps: any[], appListRes: any, typeRes: any): any[]
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// zTree 的父节点判定是「children 是数组即 isParent=true」——空数组也会渲染出 +/− 展开图标,
|
||||||
|
// 因此统一约定:有子节点才挂 children 键,否则不挂(与组用户管理左侧树的构造口径一致)
|
||||||
|
function treeNode(base: Record<string, any>, kids: any[]): any {
|
||||||
|
const node: any = { ...base, isParent: kids.length > 0 };
|
||||||
|
if (kids.length > 0) node.children = kids;
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
// 仅保留勾选(checked)的模块;父未勾选但子孙有勾选时保留父节点以维持层级
|
// 仅保留勾选(checked)的模块;父未勾选但子孙有勾选时保留父节点以维持层级
|
||||||
function filterCheckedModules(nodes: any[]): any[] {
|
function filterCheckedModules(nodes: any[]): any[] {
|
||||||
const out: any[] = [];
|
const out: any[] = [];
|
||||||
for (const n of nodes || []) {
|
for (const n of nodes || []) {
|
||||||
const children = filterCheckedModules(n.children || []);
|
const kids = filterCheckedModules(n.children || []);
|
||||||
if (n.checked || children.length > 0) {
|
if (n.checked || kids.length > 0) {
|
||||||
out.push({
|
out.push(treeNode({ id: `mod_${n.id}`, name: n.name || `模块${n.id}`, open: true }, kids));
|
||||||
id: `mod_${n.id}`,
|
|
||||||
name: n.name || `模块${n.id}`,
|
|
||||||
open: true,
|
|
||||||
children,
|
|
||||||
isParent: (n.children || []).length > 0,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
@@ -409,13 +411,12 @@ function filterCheckedModules(nodes: any[]): any[] {
|
|||||||
|
|
||||||
// 所属部门:祖先链树,用户直接归属的组加「本组」标记
|
// 所属部门:祖先链树,用户直接归属的组加「本组」标记
|
||||||
function formatDeptTree(nodes: GroupNode[]): any[] {
|
function formatDeptTree(nodes: GroupNode[]): any[] {
|
||||||
return (nodes || []).map((n) => ({
|
return (nodes || []).map((n) =>
|
||||||
id: `grp_${n.id}`,
|
treeNode(
|
||||||
name: n.own ? `${n.name || ""}(本组)` : n.name || "",
|
{ id: `grp_${n.id}`, name: n.own ? `${n.name || ""}(本组)` : n.name || "", open: true },
|
||||||
open: true,
|
formatDeptTree(n.children || [])
|
||||||
children: formatDeptTree(n.children || []),
|
)
|
||||||
isParent: (n.children || []).length > 0,
|
);
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 所属角色:2 级树 —— 一级为「平台角色」(全应用) / 应用名分组,二级为角色
|
// 所属角色:2 级树 —— 一级为「平台角色」(全应用) / 应用名分组,二级为角色
|
||||||
@@ -438,40 +439,30 @@ function formatRoleTree(roles: RoleItem[]): any[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function formatPermissionTree(categories: CategoryNode[]): any[] {
|
function formatPermissionTree(categories: CategoryNode[]): any[] {
|
||||||
const result: any[] = [];
|
return (categories || []).map((cat) =>
|
||||||
for (const cat of categories) {
|
treeNode(
|
||||||
const catNode: any = {
|
{ id: `cat_${cat.id}`, name: cat.name || `应用类型${cat.id}`, open: true },
|
||||||
id: `cat_${cat.id}`,
|
formatApps(cat.apps || [], 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[] {
|
function formatApps(apps: AppNode[], catId?: number): any[] {
|
||||||
return (apps || []).map((app) => ({
|
return (apps || []).map((app) =>
|
||||||
id: `app_${app.id}_${catId || 0}`,
|
treeNode(
|
||||||
name: app.name || `应用${app.id}`,
|
{ id: `app_${app.id}_${catId || 0}`, name: app.name || `应用${app.id}`, open: false }, // 应用一级默认闭合,点击才展开模块
|
||||||
open: false, // 应用一级默认闭合,点击才展开模块
|
formatModules(app.modules || [])
|
||||||
children: formatModules(app.modules || []),
|
)
|
||||||
}));
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatModules(modules: ModuleNode[]): any[] {
|
function formatModules(modules: ModuleNode[]): any[] {
|
||||||
return (modules || []).map((m) => {
|
return (modules || []).map((m) =>
|
||||||
const children = formatModules(m.children || []);
|
treeNode(
|
||||||
return {
|
{ id: `mod_${m.id}`, name: m.name || `模块${m.id}`, open: true },
|
||||||
id: `mod_${m.id}`,
|
formatModules(m.children || [])
|
||||||
name: m.name || `模块${m.id}`,
|
)
|
||||||
open: true,
|
);
|
||||||
children,
|
|
||||||
isParent: children.length > 0,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -635,19 +626,29 @@ function formatModules(modules: ModuleNode[]): any[] {
|
|||||||
color: #2d8cf0;
|
color: #2d8cf0;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.group-tree-search-panel) {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.ztree) {
|
:deep(.ztree) {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
|
||||||
li {
|
|
||||||
line-height: 26px;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
/* 左列对齐「组用户管理」页的写法(不覆盖全局 .tree-container):
|
||||||
|
容器改 flex 纵向并收起自身滚动,把滚动条收敛到树节点区域,避免整列滚动把搜索框滚走、树尾被裁。
|
||||||
|
注意不要在此处覆盖 zTree 的行高:zTree 官方样式的图标/连线按 21px 节距排布,
|
||||||
|
把 li line-height 改大(如 26px)会让每行多出的空隙在连接线上露出断口。 */
|
||||||
|
.tree-wrapper.permission-view .tree-container.permission-tree {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.group-tree-search-panel {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -371,7 +371,8 @@ function onSortSaved() {
|
|||||||
left: 348px !important;
|
left: 348px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 左侧树容器改为 flex 纵向布局并收起自身滚动,把滚动条收敛到树节点区域(.ztree 自带 overflow-y:auto),
|
/* 左侧树容器改为 flex 纵向布局并收起自身滚动,把滚动条收敛到树节点区域
|
||||||
|
(滚动体是 MyTree 内部的 .ztree,见 components/MyTree.vue),
|
||||||
避免树过长时把顶部 opera-ul 操作按钮条滚出屏幕;仅作用于本页,不影响全局 .tree-container */
|
避免树过长时把顶部 opera-ul 操作按钮条滚出屏幕;仅作用于本页,不影响全局 .tree-container */
|
||||||
.tree-wrapper.group-user-page .tree-container.group-user-tree {
|
.tree-wrapper.group-user-page .tree-container.group-user-tree {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ type AuthzTreeNode = {
|
|||||||
nodeType: "app" | "module";
|
nodeType: "app" | "module";
|
||||||
rawId: number;
|
rawId: number;
|
||||||
appId: number;
|
appId: number;
|
||||||
|
isParent?: boolean;
|
||||||
children?: AuthzTreeNode[];
|
children?: AuthzTreeNode[];
|
||||||
};
|
};
|
||||||
type CategoryTreeItem = { cat: CategoryItem; treeData: AuthzTreeNode[]; weight: number };
|
type CategoryTreeItem = { cat: CategoryItem; treeData: AuthzTreeNode[]; weight: number };
|
||||||
@@ -115,8 +116,10 @@ function isModuleAllChecked(node: AppModuleVo): boolean {
|
|||||||
if (!node.checked) return false;
|
if (!node.checked) return false;
|
||||||
return (node.children || []).every((c) => isModuleAllChecked(c));
|
return (node.children || []).every((c) => isModuleAllChecked(c));
|
||||||
}
|
}
|
||||||
|
// 空 children 不挂键:zTree 只要见到 children 是数组就强制 isParent=true 而渲染 +/−(叶子也会出现展开图标)
|
||||||
function buildModuleNode(m: AppModuleVo, appId: number): AuthzTreeNode {
|
function buildModuleNode(m: AppModuleVo, appId: number): AuthzTreeNode {
|
||||||
return {
|
const kids = (m.children || []).map((c) => buildModuleNode(c, appId));
|
||||||
|
const node: AuthzTreeNode = {
|
||||||
id: `mod_${m.id}`,
|
id: `mod_${m.id}`,
|
||||||
name: String(m.name || ""),
|
name: String(m.name || ""),
|
||||||
open: true,
|
open: true,
|
||||||
@@ -124,21 +127,26 @@ function buildModuleNode(m: AppModuleVo, appId: number): AuthzTreeNode {
|
|||||||
nodeType: "module",
|
nodeType: "module",
|
||||||
rawId: Number(m.id),
|
rawId: Number(m.id),
|
||||||
appId,
|
appId,
|
||||||
children: (m.children || []).map((c) => buildModuleNode(c, appId)),
|
isParent: kids.length > 0,
|
||||||
};
|
};
|
||||||
|
if (kids.length > 0) node.children = kids;
|
||||||
|
return node;
|
||||||
}
|
}
|
||||||
function buildAppNode(app: AppItem): AuthzTreeNode {
|
function buildAppNode(app: AppItem): AuthzTreeNode {
|
||||||
const appId = Number(app.id);
|
const appId = Number(app.id);
|
||||||
return {
|
const kids = (app.modules || []).map((m) => buildModuleNode(m, appId));
|
||||||
|
const node: AuthzTreeNode = {
|
||||||
id: `app_${appId}`,
|
id: `app_${appId}`,
|
||||||
name: String(app.name || ""),
|
name: String(app.name || ""),
|
||||||
open: false, // 一级默认闭合,点击才展开模块
|
open: false,
|
||||||
checked: isAppChecked(app),
|
checked: isAppChecked(app),
|
||||||
nodeType: "app",
|
nodeType: "app",
|
||||||
rawId: appId,
|
rawId: appId,
|
||||||
appId,
|
appId,
|
||||||
children: (app.modules || []).map((m) => buildModuleNode(m, appId)),
|
isParent: kids.length > 0,
|
||||||
};
|
};
|
||||||
|
if (kids.length > 0) node.children = kids;
|
||||||
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------- 数据加载 ----------------
|
// ---------------- 数据加载 ----------------
|
||||||
|
|||||||
Reference in New Issue
Block a user