- 树恢复纯 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:
+83
-119
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user