- 右侧由表格改为部门卡片网格;有下级部门的部门渲染为跨整行的分组卡(办公室 → 校办/宣传/财务/党建),叶子部门为单卡 - 左侧沿用 zTree,剪枝到「学校」层;学校行尾挂「包含子组」开关(MyTree 新增 attachNodeControl,不污染 node.name、不触发节点选中) - 按访问级别分视角:超管/局端显示树,学校管理员无树直接列本校部门(多校走顶部下拉) - 修复「只显示教师组一行」:右侧数据改为从全量组树按 id 回查 children,不再使用 treeLink 中已清空 children 的节点
This commit is contained in:
@@ -215,6 +215,11 @@ function getNodeById(id: number) {
|
||||
return refMyTree.value?.getNodeById(id);
|
||||
}
|
||||
|
||||
/** 在指定节点后追加自定义控件(见 MyTree.attachNodeControl) */
|
||||
function attachNodeControl(id: number, className: string, html: string) {
|
||||
return refMyTree.value?.attachNodeControl(id, className, html);
|
||||
}
|
||||
|
||||
function getCheckedNodes() {
|
||||
return refMyTree.value?.getCheckedNodes() || [];
|
||||
}
|
||||
@@ -257,6 +262,7 @@ defineExpose({
|
||||
removeNode,
|
||||
renameNode,
|
||||
getNodeById,
|
||||
attachNodeControl,
|
||||
getCheckedNodes,
|
||||
checkNodeById,
|
||||
addNote,
|
||||
|
||||
@@ -222,6 +222,33 @@ export default {
|
||||
if (on) a.addClass('ztree-search-highlight');
|
||||
else a.removeClass('ztree-search-highlight');
|
||||
},
|
||||
/**
|
||||
* 给指定节点追加自定义 DOM 控件(渲染在节点文字之后)。
|
||||
* 用途:在 zTree 节点行尾挂业务控件(如开关),不污染 node.name、
|
||||
* 也不触发节点选中(zTree 的 onClick 绑定在节点 a 标签上,控件在其外部)。
|
||||
* 树数据重载(initTree)后需重新挂载。
|
||||
* @param {*} id 节点 id
|
||||
* @param {string} className 控件容器类名(同时作为去重标识)
|
||||
* @param {string} html 控件 HTML
|
||||
* @returns {HTMLElement|null} 控件元素,失败返回 null
|
||||
*/
|
||||
attachNodeControl(id, className, html) {
|
||||
const _domId = this.domId;
|
||||
const treeObj = $.fn.zTree.getZTreeObj(_domId);
|
||||
if (!treeObj || !id || !className) return null;
|
||||
const node = treeObj.getNodesByFilter(n => n.id == id, true);
|
||||
if (!node) return null;
|
||||
const li = document.getElementById(node.tId);
|
||||
if (!li) return null;
|
||||
li.querySelectorAll("." + className).forEach(el => el.remove());
|
||||
const box = document.createElement("span");
|
||||
box.className = className;
|
||||
box.innerHTML = html;
|
||||
const a = document.getElementById(node.tId + "_a");
|
||||
if (a && a.parentNode) a.parentNode.insertBefore(box, a.nextSibling);
|
||||
else li.appendChild(box);
|
||||
return box;
|
||||
},
|
||||
clearHighlight() {
|
||||
(this.highlightNodes || []).forEach(n => this.setHighlight(n, false));
|
||||
this.highlightNodes = [];
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
<template>
|
||||
<article class="dept-card" :class="{ 'is-off': disabled, 'has-kids': showKids }">
|
||||
<div class="row-1">
|
||||
<span class="status-dot" :class="{ 'is-off': disabled }"></span>
|
||||
<h3 class="card-name" :title="dept.name">{{ dept.name }}</h3>
|
||||
<span class="sw" :class="{ 'is-off': disabled }" title="启用 / 停用" @click.stop="emit('toggle', dept)"></span>
|
||||
</div>
|
||||
<div class="row-2">
|
||||
<span class="sub">{{ subText }}</span>
|
||||
</div>
|
||||
<div v-if="showKids" class="kid-list">
|
||||
<DeptCard v-for="kid in kids" :key="kid.id" :dept="kid" :level="level + 1"
|
||||
:include-children="includeChildren" @toggle="onKidToggle" />
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue";
|
||||
|
||||
/** 部门口径:教师组(type=7)的直接子节点为部门,下可再挂子部门 */
|
||||
const TEACHER_GROUP_TYPE = 7;
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
dept: any;
|
||||
/** 层级深度:0 = 一级部门,>0 = 下级部门 */
|
||||
level?: number;
|
||||
/** 是否包含下级部门(对应学校上的「包含子组」开关) */
|
||||
includeChildren?: boolean;
|
||||
}>(), {
|
||||
level: 0,
|
||||
includeChildren: true,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{ (e: "toggle", dept: any): void }>();
|
||||
|
||||
const onKidToggle = (dept: any) => emit("toggle", dept);
|
||||
|
||||
const kids = computed<any[]>(() => props.dept?.children || []);
|
||||
const showKids = computed(() => props.includeChildren !== false && kids.value.length > 0);
|
||||
const disabled = computed(() => props.dept?.status === 1);
|
||||
|
||||
const subText = computed(() => {
|
||||
if (props.dept?.type !== TEACHER_GROUP_TYPE) return "学生组 · 非部门";
|
||||
if (props.level > 0) return "下级部门";
|
||||
return kids.value.length ? `${kids.value.length} 个下级部门` : "无下级部门";
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.dept-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 9px;
|
||||
padding: 13px 14px;
|
||||
background: var(--panel, #fff);
|
||||
border: 1px solid var(--line, #e9edf5);
|
||||
border-radius: var(--r, 10px);
|
||||
box-shadow: 0 1px 2px rgba(30, 38, 53, .03);
|
||||
transition: border-color .15s, box-shadow .15s;
|
||||
}
|
||||
|
||||
.dept-card:hover {
|
||||
border-color: #dbe2fb;
|
||||
box-shadow: 0 6px 18px rgba(30, 38, 53, .07);
|
||||
}
|
||||
|
||||
/* 有下级部门:跨整行成组,避免层级被压平 */
|
||||
.dept-card.has-kids {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.dept-card.is-off {
|
||||
background: #fbfcfd;
|
||||
border-style: dashed;
|
||||
}
|
||||
|
||||
.row-1 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
flex: 0 0 6px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--green, #20a162);
|
||||
}
|
||||
|
||||
.status-dot.is-off {
|
||||
background: #cbd2df;
|
||||
}
|
||||
|
||||
.card-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
color: var(--text, #1e2635);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dept-card.is-off .card-name {
|
||||
color: var(--text-3, #98a1b2);
|
||||
}
|
||||
|
||||
.row-2 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-height: 26px;
|
||||
padding-top: 9px;
|
||||
border-top: 1px dashed var(--line-2, #f0f3f9);
|
||||
font-size: 12px;
|
||||
color: var(--text-3, #98a1b2);
|
||||
}
|
||||
|
||||
.row-2 .sub {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 启停开关 */
|
||||
.sw {
|
||||
position: relative;
|
||||
flex: 0 0 auto;
|
||||
width: 36px;
|
||||
height: 20px;
|
||||
border-radius: 10px;
|
||||
background: var(--brand, #577afb);
|
||||
cursor: pointer;
|
||||
transition: background .15s;
|
||||
}
|
||||
|
||||
.sw::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 18px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 3px rgba(30, 38, 53, .22);
|
||||
transition: left .15s;
|
||||
}
|
||||
|
||||
.sw.is-off {
|
||||
background: #d3d9e6;
|
||||
}
|
||||
|
||||
.sw.is-off::after {
|
||||
left: 2px;
|
||||
}
|
||||
|
||||
/* 下级部门区 */
|
||||
.kid-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-top: 11px;
|
||||
padding-top: 11px;
|
||||
border-top: 1px dashed var(--line-2, #f0f3f9);
|
||||
}
|
||||
|
||||
.kid-list :deep(.dept-card) {
|
||||
flex: 1 1 168px;
|
||||
min-width: 150px;
|
||||
background: #fafbfe;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.kid-list :deep(.dept-card:hover) {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.kid-list :deep(.dept-card .card-name) {
|
||||
font-size: 13.5px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 下级部门里若还有下级,占满一行表达更深层级 */
|
||||
.kid-list :deep(.dept-card.has-kids) {
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
</style>
|
||||
+338
-96
@@ -1,147 +1,284 @@
|
||||
<template>
|
||||
<div class="page-wrapper tree-wrapper dept-manage-page">
|
||||
<!-- 左侧:只读组树(复用 GroupTreeSearchPanel,无新增/编辑/删除操作栏) -->
|
||||
<div class="tree-container dept-manage-tree">
|
||||
<GroupTreeSearchPanel ref="refTreePanel" v-model:curTreeNode="selectedNode" :treeApi="treeApi"
|
||||
:defaultSearchVisible="true" @on-change="onTreeChange" @on-tree-data="onTreeData" />
|
||||
</div>
|
||||
<div v-if="!roleReady" class="dept-loading">加载中…</div>
|
||||
|
||||
<!-- 右侧:部门管理面板(P0 最小批:只读 + 状态徽标 + 启停) -->
|
||||
<div class="dept-manage-main">
|
||||
<Card dis-hover :bordered="false" class="mb-12">
|
||||
<template #title>
|
||||
<span>部门管理</span>
|
||||
<Tag v-if="deptContainerName" color="primary" class="ml-8">{{ deptContainerName }}</Tag>
|
||||
</template>
|
||||
<Alert v-if="!selectedNode?.id" show-icon type="info">请在左侧选择组织节点</Alert>
|
||||
<template v-else-if="!isDeptContainer(selectedNode)">
|
||||
<Alert show-icon type="warning">
|
||||
当前节点【{{ selectedNode.name }}】不是部门容器(教师组),部门 = 教师组的直接子节点。
|
||||
请选择学校下的「教师组」节点。
|
||||
<template v-else>
|
||||
<!-- 左侧: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" />
|
||||
</div>
|
||||
|
||||
<!-- 右侧:部门卡片 -->
|
||||
<div class="dept-manage-main">
|
||||
<Card dis-hover :bordered="false" class="mb-12">
|
||||
<template #title>
|
||||
<div class="panel-title">
|
||||
<span>部门管理</span>
|
||||
<Tag v-if="currentSchoolName" color="primary" class="ml-8">{{ currentSchoolName }}</Tag>
|
||||
<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-label">包含子组</span>
|
||||
<i class="sw-mini" :class="{ 'is-on': includeOn }"></i>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<Alert v-if="!currentSchoolName" show-icon type="info">
|
||||
{{ showTree ? '请在左侧选择学校节点' : '当前账号没有可管理的学校' }}
|
||||
</Alert>
|
||||
</template>
|
||||
<template v-else>
|
||||
<Alert v-if="deptRows.length === 0" show-icon type="info">
|
||||
该教师组暂无子节点:按口径「空容器自身算部门」,容器本身即可作为部门使用。
|
||||
<Alert v-else-if="!deptList.length" show-icon type="info">
|
||||
该校教师组下暂无部门。部门 = 教师组的直接子节点,可前往「组,用户管理」建组。
|
||||
</Alert>
|
||||
<Table v-else :columns="columns" :data="deptRows" size="small" :border="false">
|
||||
<template #status="{ row }">
|
||||
<Tag :color="isDisabled(row) ? 'error' : 'success'">{{ isDisabled(row) ? '已停用' : '启用' }}</Tag>
|
||||
</template>
|
||||
<template #action="{ row }">
|
||||
<Button v-if="!isDisabled(row)" size="small" type="error" ghost @click="onDisable(row)">停用</Button>
|
||||
<Button v-else size="small" type="success" ghost @click="onEnable(row)">启用</Button>
|
||||
</template>
|
||||
</Table>
|
||||
</template>
|
||||
</Card>
|
||||
<Alert show-icon type="info" class="usage-note">
|
||||
说明:停用只影响「选」(新建事项/流程绑定时选不到),不影响「看」(已有事项展示)与「办」(在途流程照常解析)。
|
||||
新建部门请前往「组,用户管理」建组;本页不提供删除。
|
||||
</Alert>
|
||||
</div>
|
||||
<div v-else class="dept-grid">
|
||||
<DeptCard v-for="d in deptList" :key="d.id" :dept="d" :include-children="includeOn"
|
||||
@toggle="onToggle" />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Alert show-icon type="info" class="usage-note">
|
||||
说明:停用只影响「选」(新建事项 / 流程绑定时选不到),不影响「看」(已有事项展示)与「办」(在途流程照常解析)。
|
||||
关闭「包含子组」后只展示一级部门;打开后连同下级部门(如办公室下的校办 / 宣传 / 财务 / 党建)一并列出。
|
||||
</Alert>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from "vue";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { cloneDeep } from "lodash";
|
||||
import Apis from "@/api";
|
||||
import GroupTreeSearchPanel from "@/components/GroupTreeSearchPanel.vue";
|
||||
import MyConfirm from "@/components/MyConfirm";
|
||||
import DeptCard from "./DeptCard.vue";
|
||||
import { Message } from "view-ui-plus";
|
||||
import { useDefaultStore } from "@/store";
|
||||
import { UserRoleEnum } from "@/types";
|
||||
|
||||
/**
|
||||
* 部门管理(2026-10-20 部门线 P0 最小批)。
|
||||
* 口径:部门 = type=7(教师组)的直接子节点;空教师组自身算部门(与 flow-center collectDeptGroups 同源)。
|
||||
* 只做:只读组树 + 状态徽标 + 停用(确认弹窗)/启用(免确认)。
|
||||
* 不做(P1~P3):删除入口、占用弹窗、成员区块、「职务」列、新建部门、「已停用」筛选。
|
||||
* 部门管理(2026-10-21 卡片版)。
|
||||
* 口径:部门 = 教师组(type=7)的直接子节点,下可再挂子部门。
|
||||
* 视角:超管 / 局端 → 左侧 zTree 树只到「学校」层,学校行带「包含子组」开关;
|
||||
* 学校管理员 → 无树,直接列出本校部门(多校时顶部下拉切换)。
|
||||
*/
|
||||
const TEACHER_GROUP_TYPE = 7;
|
||||
const SCHOOL_GROUP_TYPE = 2;
|
||||
|
||||
const store = useDefaultStore();
|
||||
const isSchoolMode = computed(() => store.userRole === UserRoleEnum.School);
|
||||
/** 超管 / 局端才需要左侧树 */
|
||||
const showTree = computed(() => !isSchoolMode.value);
|
||||
/** 角色未落定前不渲染,避免先按默认角色取数再纠正 */
|
||||
const roleReady = computed(() => store.roleLoaded);
|
||||
|
||||
const treeApi = () => Apis.SysGroup.getTree();
|
||||
const refTreePanel = ref<InstanceType<typeof GroupTreeSearchPanel>>();
|
||||
const selectedNode = ref<any>({});
|
||||
const treeData = ref<any[]>([]);
|
||||
/** 全量组树:右侧部门数据一律从这里按 id 回查真实 children */
|
||||
const fullTree = ref<any[]>([]);
|
||||
/** 每个学校的「包含子组」开关(按学校 id 存 localStorage,刷新不丢;未落库到后端) */
|
||||
const INCLUDE_STORE_KEY = "userbase.dept.include";
|
||||
|
||||
const columns = [
|
||||
{ title: '部门名称', key: 'name', tree: true },
|
||||
{ title: '状态', slot: 'status', width: 100 },
|
||||
{ title: '操作', slot: 'action', width: 100 },
|
||||
];
|
||||
function loadIncludeMap(): Record<number, boolean> {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem(INCLUDE_STORE_KEY) || "{}") || {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
const deptContainerName = computed(() =>
|
||||
selectedNode.value?.type === TEACHER_GROUP_TYPE ? selectedNode.value.name : '');
|
||||
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 ?? []);
|
||||
const multiSchool = computed(() => isSchoolMode.value && schoolIds.value.length > 1);
|
||||
|
||||
const schoolOptions = computed(() => schoolIds.value.map((id) => ({
|
||||
id,
|
||||
name: findNodeById(fullTree.value, id)?.name ?? String(id),
|
||||
})));
|
||||
|
||||
const currentSchool = computed(() => findNodeById(fullTree.value, currentSchoolId.value));
|
||||
const currentSchoolName = computed(() => currentSchool.value?.name || "");
|
||||
|
||||
/** 部门 = 该校各教师组(type=7)的直接子节点 */
|
||||
const deptList = computed<any[]>(() => {
|
||||
const school = currentSchool.value;
|
||||
if (!school) return [];
|
||||
const rows: any[] = [];
|
||||
(school.children || [])
|
||||
.filter((c: any) => c.type === TEACHER_GROUP_TYPE)
|
||||
.forEach((t: any) => rows.push(...(t.children || [])));
|
||||
return rows;
|
||||
});
|
||||
|
||||
const includeOn = computed(() =>
|
||||
currentSchoolId.value ? includeMap.value[currentSchoolId.value] !== false : false);
|
||||
|
||||
/* ================= 数据加载 ================= */
|
||||
|
||||
/** 树数据源:取全量树存起来给右侧用,树上只渲染到学校层 */
|
||||
async function treeApi() {
|
||||
const full: any[] = (await Apis.SysGroup.getTree()) || [];
|
||||
fullTree.value = cloneDeep(full);
|
||||
return pruneToSchoolLevel(full);
|
||||
}
|
||||
|
||||
/** 剪枝:学校节点保留但不再下钻(超管视角树只到学校层) */
|
||||
function pruneToSchoolLevel(list: any[]): any[] {
|
||||
return (list || []).map((n: any) => {
|
||||
const node: any = { ...n };
|
||||
if (node.type === SCHOOL_GROUP_TYPE) {
|
||||
delete node.children;
|
||||
return node;
|
||||
}
|
||||
if (Array.isArray(node.children) && node.children.length) {
|
||||
node.children = pruneToSchoolLevel(node.children);
|
||||
}
|
||||
return node;
|
||||
});
|
||||
}
|
||||
|
||||
function findNodeById(list: any[], id?: number | null): any {
|
||||
if (id == null) return null;
|
||||
for (const n of list || []) {
|
||||
if (Number(n.id) === Number(id)) return n;
|
||||
const hit = findNodeById(n.children || [], id);
|
||||
if (hit) return hit;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/* ================= 交互 ================= */
|
||||
|
||||
function onTreeChange(node: any) {
|
||||
selectedNode.value = node || {};
|
||||
if (node?.type === SCHOOL_GROUP_TYPE) {
|
||||
currentSchoolId.value = node.id;
|
||||
} else if (node?.id) {
|
||||
// 点到中间层(区域 / 学校组等)不作为当前学校
|
||||
currentSchoolId.value = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function onTreeData(tree: any[]) {
|
||||
treeData.value = tree || [];
|
||||
/** 树渲染完成后,给每个学校节点挂「包含子组」开关 */
|
||||
async function onTreeData(tree: any[]) {
|
||||
await Promise.resolve();
|
||||
requestAnimationFrame(() => mountSchoolControls(tree));
|
||||
}
|
||||
|
||||
/** 部门口径判定:type=7 教师组是部门容器 */
|
||||
function isDeptContainer(node: any): boolean {
|
||||
return node?.type === TEACHER_GROUP_TYPE;
|
||||
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;
|
||||
}
|
||||
|
||||
/** 教师组无子节点时空容器自身算部门(flow-center 同口径) */
|
||||
const deptRows = computed<any[]>(() => {
|
||||
if (!isDeptContainer(selectedNode.value)) return [];
|
||||
const children = selectedNode.value?.children || [];
|
||||
if (children.length === 0) return [selectedNode.value];
|
||||
return children;
|
||||
});
|
||||
|
||||
function isDisabled(row: any): boolean {
|
||||
return row?.status === 1;
|
||||
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;
|
||||
}
|
||||
|
||||
function onDisable(row: any) {
|
||||
/** 同步树上开关外观:容器与内部滑块都要带 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 };
|
||||
try {
|
||||
localStorage.setItem(INCLUDE_STORE_KEY, JSON.stringify(includeMap.value));
|
||||
} catch {
|
||||
/* 隐私模式等场景写不进去也不影响使用 */
|
||||
}
|
||||
syncIncVisual(sid);
|
||||
}
|
||||
|
||||
function onToggle(dept: any) {
|
||||
if (!dept?.id) return;
|
||||
if (dept.status === 1) {
|
||||
doEnable(dept);
|
||||
return;
|
||||
}
|
||||
MyConfirm({
|
||||
title: '停用部门',
|
||||
msg: `确定停用部门【${row.name}】?`,
|
||||
info: '停用只影响「选」(新建时选不到),不影响「看」和「办」(已有事项展示、在途流程照常解析)。',
|
||||
title: "停用部门",
|
||||
msg: `确定停用部门【${dept.name}】?`,
|
||||
info: "停用只影响「选」(新建时选不到),不影响「看」和「办」(已有事项展示、在途流程照常解析)。",
|
||||
async onOk() {
|
||||
try {
|
||||
await Apis.SysGroup.disable({ pathParams: { id: row.id } });
|
||||
Message.success('已停用');
|
||||
await refTreePanel.value?.refreshTree();
|
||||
reselect(row.id);
|
||||
} catch (e: any) {
|
||||
console.warn(e?.message || '停用失败');
|
||||
}
|
||||
await doDisable(dept);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function onEnable(row: any) {
|
||||
async function doDisable(dept: any) {
|
||||
try {
|
||||
await Apis.SysGroup.enable({ pathParams: { id: row.id } });
|
||||
Message.success('已启用');
|
||||
await refTreePanel.value?.refreshTree();
|
||||
reselect(row.id);
|
||||
await Apis.SysGroup.disable({ pathParams: { id: dept.id } });
|
||||
Message.success("已停用");
|
||||
await reloadData();
|
||||
} catch (e: any) {
|
||||
console.warn(e?.message || '启用失败');
|
||||
console.warn(e?.message || "停用失败");
|
||||
}
|
||||
}
|
||||
|
||||
/** 刷新后尽量回到原选中节点(找不回则清空,用户重选) */
|
||||
function reselect(nodeId: number) {
|
||||
const found = findNodeById(treeData.value, nodeId);
|
||||
if (found) {
|
||||
selectedNode.value = found;
|
||||
refTreePanel.value?.selectNode?.(found);
|
||||
async function doEnable(dept: any) {
|
||||
try {
|
||||
await Apis.SysGroup.enable({ pathParams: { id: dept.id } });
|
||||
Message.success("已启用");
|
||||
await reloadData();
|
||||
} catch (e: any) {
|
||||
console.warn(e?.message || "启用失败");
|
||||
}
|
||||
}
|
||||
|
||||
function findNodeById(list: any[], id: number): any {
|
||||
for (const n of list || []) {
|
||||
if (n?.id === id) return n;
|
||||
const hit = findNodeById(n?.children || [], id);
|
||||
if (hit) return hit;
|
||||
/** 重新取数:有树走树刷新(treeApi 内同步 fullTree),无树自行取 */
|
||||
async function reloadData() {
|
||||
if (showTree.value) {
|
||||
await refTreePanel.value?.refreshTree();
|
||||
return;
|
||||
}
|
||||
const full: any[] = (await Apis.SysGroup.getTree()) || [];
|
||||
fullTree.value = cloneDeep(full);
|
||||
}
|
||||
|
||||
/* ================= 初始化 ================= */
|
||||
watch(() => store.roleLoaded, (loaded) => {
|
||||
if (!loaded || !isSchoolMode.value) return;
|
||||
loadSchoolScope();
|
||||
}, { immediate: true });
|
||||
|
||||
/** 学校管理员:GetTree 返回的就是各学校子树(多校 = 多根) */
|
||||
async function loadSchoolScope() {
|
||||
const full: any[] = (await Apis.SysGroup.getTree()) || [];
|
||||
fullTree.value = cloneDeep(full);
|
||||
const ids = schoolIds.value;
|
||||
if (ids.length) {
|
||||
currentSchoolId.value = ids[0];
|
||||
} else if (full.length) {
|
||||
currentSchoolId.value = full[0]?.id;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -155,6 +292,15 @@ function findNodeById(list: any[], id: number): any {
|
||||
flex: 0 0 320px;
|
||||
min-height: 100%;
|
||||
overflow: auto;
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
.dept-manage-main {
|
||||
@@ -166,4 +312,100 @@ function findNodeById(list: any[], id: number): any {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dept-loading {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding-top: 80px;
|
||||
color: var(--text-3, #98a1b2);
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.school-select {
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
/* 面板头的「包含子组」开关 */
|
||||
.inc-ctrl {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
font-size: 12.5px;
|
||||
color: var(--text-2, #5a6478);
|
||||
}
|
||||
|
||||
/* 「包含子组」开关:面板头与 zTree 树上共用同一套样式。
|
||||
树上的控件是运行时创建的 DOM,没有 scoped 标记,只能用 :deep 命中 */
|
||||
:deep(.sw-mini) {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 28px;
|
||||
height: 16px;
|
||||
border-radius: 8px;
|
||||
background: #d3d9e6;
|
||||
transition: background .15s;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 2px rgba(30, 38, 53, .2);
|
||||
transition: left .15s;
|
||||
}
|
||||
|
||||
&.is-on {
|
||||
background: var(--brand, #577afb);
|
||||
|
||||
&::after {
|
||||
left: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 部门卡片网格 */
|
||||
.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