- 系统部门:卡片网格改为表格(序号/部门名称/上级/人员/状态),层级用缩进+折叠表达; 去掉新增、排序与操作列,只保留启停与「包含子组」 - 人员列:memberCount + memberNames 前 3 个姓名 + N,Tooltip 展示完整预览, 点击弹窗分页查看全部(/api/sysuser/List?groupId=,不传 withSub 只看直属) - 自定义部门:卡片改表格(序号/名称/人员/状态/操作),行拖动排序沿用 customDeptSort, 分类列表支持拖动(customDeptCategorySort),原有新增/编辑/删除/成员/上下移保留 - DeptCard.vue、CustomDeptCard.vue 标记为已废弃(无引用,可安全删除)
This commit is contained in:
+402
-23
@@ -9,7 +9,7 @@
|
||||
:treeApi="treeApi" :defaultSearchVisible="true" @on-change="onTreeChange" />
|
||||
</div>
|
||||
|
||||
<!-- 右侧:部门卡片(白卡由主题的 .main-container 提供,不再套 iView Card) -->
|
||||
<!-- 右侧:部门表格(白卡由主题的 .main-container 提供) -->
|
||||
<div class="main-container dept-manage-main">
|
||||
<div class="panel-title">
|
||||
<span class="panel-caption">部门管理</span>
|
||||
@@ -35,46 +35,112 @@
|
||||
<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" class="state-tip">
|
||||
<Alert v-else-if="!deptRows.length" show-icon type="info" class="state-tip">
|
||||
该校教师组下暂无部门。部门 = 教师组的直接子节点,可前往「组,用户管理」建组。
|
||||
</Alert>
|
||||
<div v-else class="dept-grid">
|
||||
<DeptCard v-for="d in deptList" :key="d.id" :dept="d" :include-children="includeOn"
|
||||
@toggle="onToggle" />
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div class="row-sum">{{ deptRows.length }} 个一级部门 · 共 {{ flatRows.length }} 行</div>
|
||||
<table class="dept-table">
|
||||
<colgroup>
|
||||
<col style="width: 58px" />
|
||||
<col />
|
||||
<col style="width: 120px" />
|
||||
<col style="width: 268px" />
|
||||
<col style="width: 74px" />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="ta-c">序号</th>
|
||||
<th>部门名称</th>
|
||||
<th>上级</th>
|
||||
<th>人员</th>
|
||||
<th class="ta-c">状态</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="row in flatRows" :key="row.id" :class="{ 'is-off': row.status === 1 }">
|
||||
<td class="ta-c dim">{{ row.no }}</td>
|
||||
<td>
|
||||
<span class="nmcell" :style="{ paddingLeft: row.level * 20 + 'px' }">
|
||||
<i v-if="row.kidCount" class="tw" @click="toggleRow(row.id)">{{ row.open ? '▾' : '▸' }}</i>
|
||||
<i v-else class="tw blank"></i>
|
||||
<span class="nm" :class="{ root: row.level === 0 }" :title="row.name">{{ row.name }}</span>
|
||||
<span v-if="row.kidCount" class="cnt">{{ row.kidCount }} 个下级</span>
|
||||
<span v-if="row.isStudent" class="cnt grey">学生组</span>
|
||||
</span>
|
||||
</td>
|
||||
<td class="dim" :title="row.parentName">{{ row.parentName }}</td>
|
||||
<td>
|
||||
<Tooltip v-if="row.memberCount" placement="top" max-width="360"
|
||||
:content="memberTip(row)">
|
||||
<span class="mem-cell" @click="openMembers(row)">{{ memberText(row) }}</span>
|
||||
</Tooltip>
|
||||
<span v-else class="dim">无成员</span>
|
||||
</td>
|
||||
<td class="ta-c">
|
||||
<i class="sw" :class="{ 'is-on': row.status !== 1 }"
|
||||
:title="row.status === 1 ? '启用' : '停用'" @click="onToggle(row.raw)"></i>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="table-foot">共 {{ flatRows.length }} 个部门</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<CustomDeptPanel v-else :school-id="currentSchoolId" />
|
||||
|
||||
<!-- 系统部门人员:预览之外看全部,走 /api/sysuser/List?groupId= -->
|
||||
<Modal v-model="memModal.visible" :title="`部门人员 · ${memModal.deptName}`" width="560"
|
||||
:mask-closable="true" footer-hide>
|
||||
<div class="mem-modal-body">
|
||||
<div v-if="memModal.loading" class="mem-empty">加载中…</div>
|
||||
<div v-else-if="!memModal.rows.length" class="mem-empty">该部门暂无直属成员</div>
|
||||
<ul v-else class="mem-list">
|
||||
<li v-for="u in memModal.rows" :key="u.id">
|
||||
<span class="mname">{{ u.realName || u.account }}</span>
|
||||
<span class="macc">{{ u.account }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="memModal.total > memModal.pageSize" class="mem-pager">
|
||||
<Page :current="memModal.page" :total="memModal.total" :page-size="memModal.pageSize"
|
||||
size="small" show-total @on-change="onMemPageChange" />
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { computed, reactive, 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 CustomDeptPanel from "./CustomDeptPanel.vue";
|
||||
import { Message } from "view-ui-plus";
|
||||
import { useDefaultStore } from "@/store";
|
||||
import { UserRoleEnum } from "@/types";
|
||||
|
||||
/**
|
||||
* 部门管理(2026-10-21 卡片版)。
|
||||
* 口径:部门 = 教师组(type=7)的直接子节点,下可再挂子部门。
|
||||
* 视角:超管 / 局端 → 左侧 zTree 树只到「学校」层,学校行带「包含子组」开关;
|
||||
* 部门管理(2026-10-23 表格版)。
|
||||
* 口径:部门 = 教师组(type=7)的直接子节点,下可再挂子部门(只有教师组下才可能有多级)。
|
||||
* 视角:超管 / 局端 → 左侧 zTree 树只到「学校」层,右侧是该学校的部门表格;
|
||||
* 学校管理员 → 无树,直接列出本校部门(多校时顶部下拉切换)。
|
||||
* 表格:层级用缩进 + 折叠表达;不提供新增 / 排序 / 行内操作,只保留启停与「包含子组」。
|
||||
* 人员:getTree 已带 memberCount + memberNames(前 50),超出走 /api/sysuser/List 看全部。
|
||||
*/
|
||||
const TEACHER_GROUP_TYPE = 7;
|
||||
const STUDENT_GROUP_TYPE = 8;
|
||||
const SCHOOL_GROUP_TYPE = 2;
|
||||
|
||||
const store = useDefaultStore();
|
||||
@@ -103,17 +169,130 @@ const schoolOptions = computed(() => schoolIds.value.map((id) => ({
|
||||
const currentSchool = computed(() => findNodeById(fullTree.value, currentSchoolId.value));
|
||||
const currentSchoolName = computed(() => currentSchool.value?.name || "");
|
||||
|
||||
/** 部门 = 该校各教师组(type=7)的直接子节点 */
|
||||
const deptList = computed<any[]>(() => {
|
||||
/** 折叠状态:默认全展开,只记「被收起的 id」 */
|
||||
const closedIds = ref<Set<number>>(new Set());
|
||||
function toggleRow(id: number) {
|
||||
const next = new Set(closedIds.value);
|
||||
if (next.has(id)) next.delete(id);
|
||||
else next.add(id);
|
||||
closedIds.value = next;
|
||||
}
|
||||
|
||||
interface DeptRow {
|
||||
id: number;
|
||||
name: string;
|
||||
status: number;
|
||||
parentName: string;
|
||||
level: number;
|
||||
isStudent: boolean;
|
||||
memberCount: number;
|
||||
memberNames: string[];
|
||||
kidCount: number;
|
||||
open: boolean;
|
||||
no: number;
|
||||
raw: any;
|
||||
children: DeptRow[];
|
||||
}
|
||||
|
||||
/** 表格树:一级部门来自各教师组下的直接子节点,子级递归;「包含子组」关闭时不带子级 */
|
||||
const deptRows = computed<DeptRow[]>(() => {
|
||||
const school = currentSchool.value;
|
||||
if (!school) return [];
|
||||
const rows: any[] = [];
|
||||
const rows: DeptRow[] = [];
|
||||
(school.children || [])
|
||||
.filter((c: any) => c.type === TEACHER_GROUP_TYPE)
|
||||
.forEach((t: any) => rows.push(...(t.children || [])));
|
||||
.forEach((t: any) => {
|
||||
(t.children || []).forEach((d: any) => rows.push(toRow(d, t.name, 0)));
|
||||
});
|
||||
return rows;
|
||||
});
|
||||
|
||||
function toRow(d: any, parentName: string, level: number): DeptRow {
|
||||
const kids: any[] = includeOn.value ? (d.children || []) : [];
|
||||
return {
|
||||
id: d.id,
|
||||
name: d.name,
|
||||
status: d.status ?? 0,
|
||||
parentName,
|
||||
level,
|
||||
isStudent: d.type === STUDENT_GROUP_TYPE,
|
||||
memberCount: Number(d.memberCount ?? 0),
|
||||
memberNames: Array.isArray(d.memberNames) ? d.memberNames : [],
|
||||
kidCount: kids.length,
|
||||
open: !closedIds.value.has(d.id),
|
||||
no: 0,
|
||||
raw: d,
|
||||
children: kids.map((k) => toRow(k, d.name, level + 1)),
|
||||
};
|
||||
}
|
||||
|
||||
/** 拍平成可渲染行(按折叠状态裁剪子级),并顺序编号 */
|
||||
const flatRows = computed<DeptRow[]>(() => {
|
||||
const out: DeptRow[] = [];
|
||||
const walk = (list: DeptRow[]) => {
|
||||
list.forEach((row) => {
|
||||
out.push(row);
|
||||
if (row.kidCount && row.open) walk(row.children);
|
||||
});
|
||||
};
|
||||
walk(deptRows.value);
|
||||
out.forEach((row, i) => (row.no = i + 1));
|
||||
return out;
|
||||
});
|
||||
|
||||
/** 人员列:前 3 个姓名 + 剩余数;无姓名时退回「N 人」 */
|
||||
function memberText(row: DeptRow) {
|
||||
if (!row.memberNames.length) return `${row.memberCount} 人`;
|
||||
const head = row.memberNames.slice(0, 3).join("、");
|
||||
const rest = row.memberCount - Math.min(3, row.memberNames.length);
|
||||
return rest > 0 ? `${head} +${rest}` : head;
|
||||
}
|
||||
function memberTip(row: DeptRow) {
|
||||
const list = row.memberNames.join("、");
|
||||
const more = row.memberCount > row.memberNames.length ? `…(仅预览前 ${row.memberNames.length} 人)` : "";
|
||||
return `共 ${row.memberCount} 人:${list}${more} 点击查看全部`;
|
||||
}
|
||||
|
||||
/* ================= 人员弹窗(查看全部,分页) ================= */
|
||||
const memModal = reactive({
|
||||
visible: false,
|
||||
loading: false,
|
||||
deptId: 0,
|
||||
deptName: "",
|
||||
rows: [] as any[],
|
||||
total: 0,
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
});
|
||||
function openMembers(row: DeptRow) {
|
||||
Object.assign(memModal, {
|
||||
visible: true, loading: true, deptId: row.id, deptName: row.name,
|
||||
rows: [], total: 0, page: 1,
|
||||
});
|
||||
loadMembers();
|
||||
}
|
||||
function onMemPageChange(page: number) {
|
||||
memModal.page = page;
|
||||
loadMembers();
|
||||
}
|
||||
async function loadMembers() {
|
||||
memModal.loading = true;
|
||||
try {
|
||||
const res: any = await Apis.SysUser.list({
|
||||
params: { groupId: memModal.deptId, pageIndex: memModal.page, pageSize: memModal.pageSize },
|
||||
cacheFor: null,
|
||||
});
|
||||
memModal.rows = res?.content ?? [];
|
||||
memModal.total = Number(res?.total ?? 0);
|
||||
} catch (e: any) {
|
||||
console.warn(e?.message || "加载部门人员失败");
|
||||
memModal.rows = [];
|
||||
memModal.total = 0;
|
||||
} finally {
|
||||
memModal.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前学校的「包含子组」:读学校节点上的 includeChildren(1=含子组 / 0=仅本级)。
|
||||
* 历史数据或旧库该列为 null 时按「含子组」兜底,与升级前表现一致。
|
||||
@@ -163,6 +342,7 @@ function onTreeChange(node: any) {
|
||||
selectedNode.value = node || {};
|
||||
if (node?.type === SCHOOL_GROUP_TYPE) {
|
||||
currentSchoolId.value = node.id;
|
||||
closedIds.value = new Set();
|
||||
} else if (node?.id) {
|
||||
// 点到中间层(区域 / 学校组等)不作为当前学校
|
||||
currentSchoolId.value = undefined;
|
||||
@@ -180,6 +360,7 @@ async function toggleInclude() {
|
||||
data: { includeChildren: next },
|
||||
});
|
||||
school.includeChildren = next ? 1 : 0;
|
||||
closedIds.value = new Set();
|
||||
Message.success(next ? "已包含下级部门" : "仅显示一级部门");
|
||||
} catch (e: any) {
|
||||
// 全局 alova 拦截器(alova-iview 的 onBizError/onHttpError)已统一 Message.error,此处只记日志避免二次弹窗;
|
||||
@@ -274,7 +455,7 @@ async function loadSchoolScope() {
|
||||
}
|
||||
}
|
||||
|
||||
/* 右侧白卡内部纵向排布:标题行 / 说明条 / 卡片网格 */
|
||||
/* 右侧白卡内部纵向排布:标题行 / 说明条 / 表格 */
|
||||
.dept-manage-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -328,7 +509,6 @@ async function loadSchoolScope() {
|
||||
box-shadow: 0 1px 2px rgba(30, 38, 53, .08);
|
||||
}
|
||||
|
||||
/* 右侧卡片面板撑满剩余高度(与其他页面一致),内容超出时在面板内滚动 */
|
||||
/* 面板内顶部说明条的间距(外观完全交给 iView Alert 自身,不覆写配色) */
|
||||
.usage-note {
|
||||
margin-bottom: 14px;
|
||||
@@ -390,10 +570,209 @@ async function loadSchoolScope() {
|
||||
}
|
||||
}
|
||||
|
||||
/* 部门卡片网格:同排卡片等高(与其他列表页一致) */
|
||||
.dept-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(238px, 1fr));
|
||||
gap: 12px;
|
||||
/* ================= 部门表格 ================= */
|
||||
.row-sum {
|
||||
flex: 0 0 auto;
|
||||
margin-bottom: 8px;
|
||||
font-size: 11.5px;
|
||||
color: var(--text-3, #98a1b2);
|
||||
}
|
||||
|
||||
.dept-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
font-size: 12.5px;
|
||||
color: var(--text, #1e2635);
|
||||
|
||||
th {
|
||||
height: 40px;
|
||||
padding: 0 12px;
|
||||
background: #f7f8fa;
|
||||
border-bottom: 1px solid var(--line, #e9edf5);
|
||||
color: var(--text-2, #5a6478);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
td {
|
||||
height: 46px;
|
||||
padding: 0 12px;
|
||||
border-bottom: 1px solid var(--line, #e9edf5);
|
||||
vertical-align: middle;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
tbody tr:hover td {
|
||||
background: #fafbfe;
|
||||
}
|
||||
|
||||
tbody tr.is-off td {
|
||||
background: #fcfcfd;
|
||||
}
|
||||
|
||||
.ta-c {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dim {
|
||||
color: var(--text-3, #98a1b2);
|
||||
font-size: 11.5px;
|
||||
}
|
||||
|
||||
.nmcell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.tw {
|
||||
flex: 0 0 auto;
|
||||
width: 14px;
|
||||
text-align: center;
|
||||
font-style: normal;
|
||||
font-size: 9px;
|
||||
color: #aeb6c2;
|
||||
cursor: pointer;
|
||||
|
||||
&.blank {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.nm {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
&.root {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
tr.is-off .nm {
|
||||
color: var(--text-3, #98a1b2);
|
||||
}
|
||||
|
||||
.cnt {
|
||||
flex: 0 0 auto;
|
||||
padding: 0 5px;
|
||||
line-height: 16px;
|
||||
border-radius: 4px;
|
||||
background: #f1f3f7;
|
||||
color: #8a94a6;
|
||||
font-size: 10.5px;
|
||||
|
||||
&.grey {
|
||||
background: #f6f7fa;
|
||||
color: #a3adbd;
|
||||
}
|
||||
}
|
||||
|
||||
.mem-cell {
|
||||
cursor: pointer;
|
||||
color: var(--text-2, #5a6478);
|
||||
border-bottom: 1px dashed transparent;
|
||||
|
||||
&:hover {
|
||||
color: var(--brand, #577afb);
|
||||
border-bottom-color: #c9d4f7;
|
||||
}
|
||||
}
|
||||
|
||||
/* 胶囊开关(与小组件管理同款比例) */
|
||||
.sw {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 30px;
|
||||
height: 17px;
|
||||
border-radius: 9px;
|
||||
background: #d5d8de;
|
||||
cursor: pointer;
|
||||
transition: background .18s;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, .2);
|
||||
transition: left .18s;
|
||||
}
|
||||
|
||||
&.is-on {
|
||||
background: var(--brand, #577afb);
|
||||
|
||||
&::after {
|
||||
left: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table-foot {
|
||||
flex: 0 0 auto;
|
||||
padding: 10px 2px;
|
||||
text-align: right;
|
||||
font-size: 12px;
|
||||
color: var(--text-3, #98a1b2);
|
||||
}
|
||||
|
||||
/* ================= 人员弹窗 ================= */
|
||||
.mem-modal-body {
|
||||
max-height: 380px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.mem-empty {
|
||||
padding: 26px 0;
|
||||
text-align: center;
|
||||
font-size: 12.5px;
|
||||
color: var(--text-3, #98a1b2);
|
||||
}
|
||||
|
||||
.mem-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
height: 34px;
|
||||
padding: 0 4px;
|
||||
border-bottom: 1px solid #f2f5f9;
|
||||
font-size: 12.5px;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.mname {
|
||||
color: var(--text, #1e2635);
|
||||
}
|
||||
|
||||
.macc {
|
||||
margin-left: auto;
|
||||
color: var(--text-3, #98a1b2);
|
||||
font-size: 11.5px;
|
||||
}
|
||||
}
|
||||
|
||||
.mem-pager {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding-top: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user