- 树恢复纯 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:
@@ -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'],
|
||||
|
||||
Vendored
+32
@@ -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>;
|
||||
/**
|
||||
* ---
|
||||
*
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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