fix(dept): 撤掉树节点开关、修文案误判;部门管理迁入「基础设置」并改走接口
build / build (push) Successful in 2m20s

- 树恢复纯 zTree:去掉 MyTree.attachNodeControl 的调用与相关定位样式(绝对定位在节点行内会漂移、视觉突兀)
- 修正卡片副文案误判:原来按 type!==7 判「学生组·非部门」,实际教师组下挂的是普通组(type=1),
  改为只对 type===8 判学生组,其余都算部门(部门身份由「挂在教师组下」决定,不由自身 type 决定)
- 菜单:部门管理从「权限管理」迁入新建的「基础设置」分组(menu modId 1115,routes meta.parentId 同步为 modId 而非 menu.id)
- 「包含子组」改为读写学校节点的 includeChildren 字段(去掉 localStorage),切换即调 SetIncludeChildren
- 布局:说明条移入面板内部并改主题浅绿(iView success 在本主题下是灰绿 #D3E0D8,不跟随);
  Card 面板 flex 撑满高度;树容器收掉外层滚动条,滚动统一交给 MyTree 内部 .ztree
This commit is contained in:
chenxing
2026-10-21 15:12:55 +08:00
parent b2a4eeb2f3
commit 16d49375f1
6 changed files with 139 additions and 126 deletions
+83 -119
View File
@@ -1,18 +1,17 @@
<template>
<div class="page-wrapper tree-wrapper dept-manage-page">
<div class="main-wrapper tree-wrapper dept-manage-page">
<div v-if="!roleReady" class="dept-loading">加载中…</div>
<template v-else>
<!-- 左侧:zTree 组树(超管 / 局端;只到学校层,学校行带「包含子组」开关) -->
<!-- 左侧:zTree 组树(超管 / 局端;只到学校层) -->
<div v-if="showTree" class="tree-container dept-manage-tree">
<div class="tree-hint">树只到「学校」层;行尾开关控制该校是否含下级部门。</div>
<GroupTreeSearchPanel ref="refTreePanel" v-model:curTreeNode="selectedNode" :treeApi="treeApi"
:defaultSearchVisible="true" @on-change="onTreeChange" @on-tree-data="onTreeData" />
<GroupTreeSearchPanel class="tree-inner" ref="refTreePanel" v-model:curTreeNode="selectedNode"
:treeApi="treeApi" :defaultSearchVisible="true" @on-change="onTreeChange" />
</div>
<!-- 右侧:部门卡片 -->
<div class="dept-manage-main">
<Card dis-hover :bordered="false" class="mb-12">
<Card dis-hover :bordered="false" class="dept-panel">
<template #title>
<div class="panel-title">
<span>部门管理</span>
@@ -20,17 +19,23 @@
<Select v-if="multiSchool" v-model="currentSchoolId" size="small" class="ml-8 school-select">
<Option v-for="s in schoolOptions" :key="s.id" :value="s.id">{{ s.name }}</Option>
</Select>
<span class="inc-ctrl ml-auto" title="打开后列出该校下所有层级的部门">
<span class="inc-ctrl ml-auto" title="打开后列出该校下所有层级的部门" @click="toggleInclude">
<span class="inc-label">包含子组</span>
<i class="sw-mini" :class="{ 'is-on': includeOn }"></i>
</span>
</div>
</template>
<Alert v-if="!currentSchoolName" show-icon type="info">
<Alert show-icon type="success" class="usage-note">
<b>设置说明:</b>
停用只影响「选」(新建事项 / 流程绑定时选不到),不影响「看」(已有事项展示)与「办」(在途流程照常解析);
关闭「包含子组」只展示一级部门,打开后连同下级部门(如办公室下的校办 / 宣传 / 财务 / 党建)一并列出。
</Alert>
<Alert v-if="!currentSchoolName" show-icon type="info" class="state-tip">
{{ showTree ? '请在左侧选择学校节点' : '当前账号没有可管理的学校' }}
</Alert>
<Alert v-else-if="!deptList.length" show-icon type="info">
<Alert v-else-if="!deptList.length" show-icon type="info" class="state-tip">
该校教师组下暂无部门。部门 = 教师组的直接子节点,可前往「组,用户管理」建组。
</Alert>
<div v-else class="dept-grid">
@@ -38,11 +43,6 @@
@toggle="onToggle" />
</div>
</Card>
<Alert show-icon type="info" class="usage-note">
说明:停用只影响「选」(新建事项 / 流程绑定时选不到),不影响「看」(已有事项展示)与「办」(在途流程照常解析)。
关闭「包含子组」后只展示一级部门;打开后连同下级部门(如办公室下的校办 / 宣传 / 财务 / 党建)一并列出。
</Alert>
</div>
</template>
</div>
@@ -79,21 +79,6 @@ const refTreePanel = ref<InstanceType<typeof GroupTreeSearchPanel>>();
const selectedNode = ref<any>({});
/** 全量组树:右侧部门数据一律从这里按 id 回查真实 children */
const fullTree = ref<any[]>([]);
/** 每个学校的「包含子组」开关(按学校 id 存 localStorage,刷新不丢;未落库到后端) */
const INCLUDE_STORE_KEY = "userbase.dept.include";
function loadIncludeMap(): Record<number, boolean> {
try {
return JSON.parse(localStorage.getItem(INCLUDE_STORE_KEY) || "{}") || {};
} catch {
return {};
}
}
const includeMap = ref<Record<number, boolean>>(loadIncludeMap());
/** 树上挂载的开关控件,切换时同步外观 */
const controlMap = ref<Record<number, HTMLElement>>({});
/** 当前学校:超管由树选中,学校管理员默认取可管理学校 */
const currentSchoolId = ref<number | undefined>(undefined);
const schoolIds = computed<number[]>(() => store.userScope?.schoolGroupIds ?? []);
@@ -118,8 +103,14 @@ const deptList = computed<any[]>(() => {
return rows;
});
const includeOn = computed(() =>
currentSchoolId.value ? includeMap.value[currentSchoolId.value] !== false : false);
/**
* 当前学校的「包含子组」:读学校节点上的 includeChildren(1=含子组 / 0=仅本级)。
* 历史数据或旧库该列为 null 时按「含子组」兜底,与升级前表现一致。
*/
const includeOn = computed(() => {
const school = currentSchool.value;
return school ? school.includeChildren !== 0 : false;
});
/* ================= 数据加载 ================= */
@@ -167,54 +158,21 @@ function onTreeChange(node: any) {
}
}
/** 树渲染完成后,给每个学校节点挂「包含子组」开关 */
async function onTreeData(tree: any[]) {
await Promise.resolve();
requestAnimationFrame(() => mountSchoolControls(tree));
}
function collectSchools(list: any[], out: any[] = []): any[] {
(list || []).forEach((n) => {
if (n?.type === SCHOOL_GROUP_TYPE) out.push(n);
collectSchools(n?.children, out);
});
return out;
}
function mountSchoolControls(tree: any[]) {
const map: Record<number, HTMLElement> = {};
collectSchools(tree || []).forEach((s) => {
const el = refTreePanel.value?.attachNodeControl(s.id, "dept-inc-ctrl", '<i class="sw-mini"></i>');
if (!el) return;
el.addEventListener("click", (e) => {
e.stopPropagation();
toggleInclude(s.id);
});
map[s.id] = el;
syncIncVisual(s.id, el);
});
controlMap.value = map;
}
/** 同步树上开关外观:容器与内部滑块都要带 is-on */
function syncIncVisual(schoolId: number, el?: HTMLElement) {
const box = el || controlMap.value[schoolId];
if (!box) return;
const on = includeMap.value[schoolId] !== false;
box.classList.toggle("is-on", on);
box.querySelector(".sw-mini")?.classList.toggle("is-on", on);
}
function toggleInclude(schoolId?: number) {
const sid = schoolId ?? currentSchoolId.value;
if (!sid) return;
includeMap.value = { ...includeMap.value, [sid]: includeMap.value[sid] === false };
/** 切换「包含子组」:落库到学校节点,成功后就地更新,避免整树重绘丢失选中态 */
async function toggleInclude() {
const school = currentSchool.value;
if (!school?.id) return;
const next = !includeOn.value;
try {
localStorage.setItem(INCLUDE_STORE_KEY, JSON.stringify(includeMap.value));
} catch {
/* 隐私模式等场景写不进去也不影响使用 */
await Apis.SysGroup.setIncludeChildren({
pathParams: { id: school.id },
data: { includeChildren: next },
});
school.includeChildren = next ? 1 : 0;
Message.success(next ? "已包含下级部门" : "仅显示一级部门");
} catch (e: any) {
console.warn(e?.message || "设置包含子组失败");
}
syncIncVisual(sid);
}
function onToggle(dept: any) {
@@ -287,32 +245,65 @@ async function loadSchoolScope() {
display: flex;
gap: 12px;
align-items: stretch;
/* 主高度由 .main-wrapper(主题通道下发)提供;height:100% 是兜底:
父级无确定高度时退化为 auto,不破坏原行为 */
height: 100%;
/* 树:外层不滚(滚动交给内部 zTree),否则内容未超高也会因 1~2px 溢出出现滚动条 */
.dept-manage-tree {
flex: 0 0 320px;
min-height: 100%;
overflow: auto;
flex: 0 0 300px;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
.tree-hint {
font-size: 11.5px;
line-height: 1.6;
color: var(--text-3, #98a1b2);
padding: 0 2px 8px;
border-bottom: 1px dashed var(--line-2, #f0f3f9);
margin-bottom: 6px;
.tree-inner {
flex: 1;
min-height: 0;
}
}
.dept-manage-main {
flex: 1;
min-width: 0;
.usage-note {
margin-top: 4px;
}
display: flex;
flex-direction: column;
}
}
/* 右侧卡片面板撑满剩余高度(与其他页面一致),内容超出时在面板内滚动 */
.dept-panel {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
:deep(.ivu-card-body) {
flex: 1;
min-height: 0;
overflow: auto;
}
}
/* 面板内顶部的设置说明条:
iView 的 type="success" 在当前主题下渲染成灰绿(#D3E0D8),故显式改取主题绿色令牌 */
.usage-note {
margin-bottom: 14px;
background-color: var(--green-soft, #e9f7ef) !important;
border-color: transparent !important;
color: var(--text-2, #5a6478) !important;
:deep(.ivu-alert-icon) {
color: var(--green, #20a162);
font-size: 16px;
}
}
/* 空态 / 无权限等临时提示 */
.state-tip {
margin-bottom: 12px;
}
.dept-loading {
flex: 1;
text-align: center;
@@ -375,37 +366,10 @@ async function loadSchoolScope() {
}
}
/* 部门卡片网格 */
/* 部门卡片网格:同排卡片等高(与其他列表页一致) */
.dept-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(238px, 1fr));
gap: 12px;
align-items: start;
}
/* 树上(zTree)学校节点行尾的开关控件:
校名长时会把开关挤出可视区,故绝对定位固定在行尾,同时给节点文字留出余量 */
:deep(.ztree li) {
position: relative;
}
:deep(.ztree li a) {
display: inline-block;
max-width: calc(100% - 46px);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: middle;
}
:deep(.dept-inc-ctrl) {
position: absolute;
right: 6px;
top: 50%;
transform: translateY(-50%);
display: inline-flex;
align-items: center;
cursor: pointer;
z-index: 1;
}
</style>