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
+1
View File
@@ -32,6 +32,7 @@ export default {
'SysGroup.sysGroupRemove': ['POST', '/api/sysgroup/Delete/{id}'],
'SysGroup.disable': ['POST', '/api/sysgroup/Disable/{id}'],
'SysGroup.enable': ['POST', '/api/sysgroup/Enable/{id}'],
'SysGroup.setIncludeChildren': ['POST', '/api/sysgroup/SetIncludeChildren/{id}'],
'SysGroup.batchAdd': ['POST', '/api/sysgroup/BatchAdd'],
'SysGroup.sysGroupAdd': ['POST', '/api/sysgroup/Add'],
'PushSubscription.test': ['POST', '/api/push/Test'],
+32
View File
@@ -148,6 +148,9 @@ export interface SysGroupRequest {
type: number;
sort?: number;
}
export interface SysGroupIncludeChildrenRequest {
includeChildren: boolean;
}
export interface PushTestRequest {
appId?: number;
interfacePath?: string;
@@ -1584,6 +1587,35 @@ declare global {
>(
config: Config
): Alova2Method<null, 'SysGroup.enable', Config>;
/**
* ---
*
* [POST]
*
* **path:** /api/sysgroup/SetIncludeChildren/{id}
*
* ---
*
* **RequestBody**
* ```ts
* type RequestBody = SysGroupIncludeChildrenRequest
* ```
*
* **Response**
* ```ts
* type Response = null
* ```
*/
setIncludeChildren<
Config extends Alova2MethodConfig<null> & {
pathParams: {
id: number;
};
data: SysGroupIncludeChildrenRequest;
}
>(
config: Config
): Alova2Method<null, 'SysGroup.setIncludeChildren', Config>;
/**
* ---
*
+15 -1
View File
@@ -13,6 +13,9 @@ import { UserRoleEnum } from "@/types";
* 2. 应用管理 —— S+C(局端不开放:局端的应用动作是「开通」,应用维护是平台级)
* 3. 权限管理 —— 父目录:应用开通(S+B) / 组授权 / 角色授权 / 权限一览(S+B+C)
* 4. 日志管理 —— 仅超管(父目录)
* 5. 基础设置 —— 非业务域的基础配置合集(当前:部门管理),后续字典/参数/模板等杂项归此
*
* 菜单不渲染图标:本工程侧边栏只输出文字,节点无需 icon 字段。
*
* 每个节点带唯一 id;子节点的 pid 由 layout.vue 的 flaten() 扁平化时
* 依据父节点 id 自动推导,用于「高亮自身 + 面包屑展示父级」联动。
@@ -48,7 +51,6 @@ export function getMenuList(userRole?: UserRoleEnum): Promise<IModule[]> {
{ id: 42, modId: 1104, name: "组授权", page: "/group-auth", roles: [UserRoleEnum.SuperAdmin, UserRoleEnum.Bureau, UserRoleEnum.School] },
{ id: 44, modId: 1110, name: "角色授权", page: "/role-authz/list", roles: [UserRoleEnum.SuperAdmin, UserRoleEnum.Bureau, UserRoleEnum.School] },
{ id: 43, modId: 1105, name: "权限一览", page: "/permission-view", roles: [UserRoleEnum.SuperAdmin, UserRoleEnum.Bureau, UserRoleEnum.School] },
{ id: 45, modId: 1106, name: "部门管理", page: "/dept-manage", roles: [UserRoleEnum.SuperAdmin, UserRoleEnum.Bureau, UserRoleEnum.School] },
],
},
// 4. 日志管理(父目录)
@@ -65,6 +67,18 @@ export function getMenuList(userRole?: UserRoleEnum): Promise<IModule[]> {
{ id: 54, modId: 1112, name: "推送日志", page: "/push-log", roles: [UserRoleEnum.SuperAdmin] },
],
},
// 5. 基础设置(父目录):非业务域的基础配置杂项合集,部门管理从「权限管理」迁入
{
id: 6,
modId: 1115,
name: "基础设置",
page: "",
childState: true,
roles: [UserRoleEnum.SuperAdmin, UserRoleEnum.Bureau, UserRoleEnum.School],
children: [
{ id: 61, modId: 1106, name: "部门管理", page: "/dept-manage", roles: [UserRoleEnum.SuperAdmin, UserRoleEnum.Bureau, UserRoleEnum.School] },
],
},
];
return Promise.resolve(filterMenuByRole(menu, userRole));
}
+4 -3
View File
@@ -106,13 +106,14 @@ const routes: RouteRecordRaw[] = [
component: PermissionView,
meta: { title: "权限一览", icon: "icon-list", modId: 1105, roles: ALL_ADMIN },
},
// ===== 部门管理(2026-10-20 部门线 P0 最小批:只读树 + 状态徽标 + 启停)=====
// 挂「权限管理」分组(菜单 id=45);无删除/成员/职务/新建入口——建部门=建组回「组,用户管理」
// ===== 部门管理(2026-10-20 部门线 P0 最小批,2026-10-21 迁入「基础设置」分组)=====
// 原挂「权限管理」(modId 1108);部门属于组织基础配置而非权限,改挂「基础设置」(modId 1115)
// 无删除/成员/职务/新建入口——建部门=建组回「组,用户管理」
{
path: "/dept-manage",
name: "DeptManage",
component: DeptManage,
meta: { title: "部门管理", icon: "icon-list", modId: 1106, parentId: 1108, roles: ALL_ADMIN },
meta: { title: "部门管理", icon: "icon-list", modId: 1106, parentId: 1115, roles: ALL_ADMIN },
},
{
path: "/push-log",
+4 -3
View File
@@ -18,8 +18,8 @@
<script lang="ts" setup>
import { computed } from "vue";
/** 部门口径:教师组(type=7)的直接子节点为部门,下可再挂子部门 */
const TEACHER_GROUP_TYPE = 7;
/** 部门口径:教师组下挂的都是部门,只有学生组(type=8)不算部门 */
const STUDENT_GROUP_TYPE = 8;
const props = withDefaults(defineProps<{
dept: any;
@@ -41,7 +41,7 @@ const showKids = computed(() => props.includeChildren !== false && kids.value.le
const disabled = computed(() => props.dept?.status === 1);
const subText = computed(() => {
if (props.dept?.type !== TEACHER_GROUP_TYPE) return "学生组 · 非部门";
if (props.dept?.type === STUDENT_GROUP_TYPE) return "学生组 · 非部门";
if (props.level > 0) return "下级部门";
return kids.value.length ? `${kids.value.length} 个下级部门` : "无下级部门";
});
@@ -115,6 +115,7 @@ const subText = computed(() => {
align-items: center;
gap: 8px;
min-height: 26px;
margin-top: auto;
padding-top: 9px;
border-top: 1px dashed var(--line-2, #f0f3f9);
font-size: 12px;
+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>