- 系统部门:自写 table 换成 iView Table(border + 表头灰底 + 行 hover), 层级仍用名称列缩进 + 折叠;状态改为可点击的边框 Tag(已启用/已停用) - 标题行:去掉 SYSTEM / CUSTOM 英文,「包含子组」移到来源切换左侧并整体右对齐, 切换视图时标签位置不再左右跳动 - 自定义部门:同样换 iView Table;表格内不再拖行,改为「排序管理」按钮 + 弹框拖动, 保存后按新顺序提交 customDeptSort;操作列保留 编辑/成员/删除 - 修正 Tag 上的 @click.native(Vue 3 已移除 .native)
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
|
||||
<template v-else>
|
||||
<div class="custom-body">
|
||||
<!-- 左侧:分类平铺列表(可拖动排序,↑↓ 保留兜底) -->
|
||||
<!-- 左侧:分类平铺列表(容器,可拖动排序,↑↓ 保留兜底) -->
|
||||
<div class="cat-col">
|
||||
<div class="col-head">
|
||||
<span class="col-title">自定义分类</span>
|
||||
@@ -38,11 +38,14 @@
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:当前分类下的部门表格(拖动行排序) -->
|
||||
<!-- 右侧:当前分类下的部门表格(iView Table;排序走弹框) -->
|
||||
<div class="dept-col">
|
||||
<div class="col-head">
|
||||
<span class="col-title">{{ activeCategoryName || '部门' }}</span>
|
||||
<span class="col-sub">{{ departments.length }} 个部门 · 拖动行可排序</span>
|
||||
<span class="col-sub">{{ departments.length }} 个部门</span>
|
||||
<Button size="small" :disabled="!activeCategoryId || !departments.length" @click="openSort">
|
||||
排序管理
|
||||
</Button>
|
||||
<Button size="small" type="primary" :disabled="!activeCategoryId" @click="openDeptCreate">
|
||||
新增部门
|
||||
</Button>
|
||||
@@ -51,58 +54,50 @@
|
||||
请先在左侧选择一个分类,或新建一个分类。
|
||||
</Alert>
|
||||
<div v-else-if="loadingDepartments" class="col-tip">加载中…</div>
|
||||
<div v-else-if="!departments.length" class="col-tip">
|
||||
该分类下暂无部门。部门可先新建再配成员,成员为空时不影响保存。
|
||||
</div>
|
||||
<table v-else class="dept-table">
|
||||
<colgroup>
|
||||
<col style="width: 58px" />
|
||||
<col />
|
||||
<col style="width: 268px" />
|
||||
<col style="width: 74px" />
|
||||
<col style="width: 150px" />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="ta-c">序号</th>
|
||||
<th>部门名称</th>
|
||||
<th>人员</th>
|
||||
<th class="ta-c">状态</th>
|
||||
<th class="ta-r">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(d, idx) in departments" :key="d.id"
|
||||
:class="{ 'is-off': !d.enabled, dragging: dragIdx === idx, 'drag-over': dragOverIdx === idx }"
|
||||
draggable="true" @dragstart="onDragStart(idx)" @dragover.prevent="onDragOver(idx)"
|
||||
@drop.prevent="onDrop(idx)" @dragend="onDragEnd">
|
||||
<td class="ta-c dim">
|
||||
<i class="grip" title="拖动排序">⠿</i>{{ idx + 1 }}
|
||||
</td>
|
||||
<td :title="d.name">{{ d.name }}</td>
|
||||
<td>
|
||||
<Tooltip v-if="Number(d.memberCount || 0)" placement="top" max-width="360"
|
||||
:content="memberTip(d)">
|
||||
<span class="mem-cell" @click="openMembers(d)">{{ memberText(d) }}</span>
|
||||
</Tooltip>
|
||||
<span v-else class="dim">无成员</span>
|
||||
</td>
|
||||
<td class="ta-c">
|
||||
<i class="sw" :class="{ 'is-on': !!d.enabled }"
|
||||
:title="d.enabled ? '停用' : '启用'" @click="onToggleDept(d)"></i>
|
||||
</td>
|
||||
<td class="ta-r">
|
||||
<a class="lnk" @click="openDeptEdit(d)">编辑</a>
|
||||
<a class="lnk" @click="openMembers(d)">成员</a>
|
||||
<a class="lnk danger" @click="removeDept(d)">删除</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<Table v-else class="dept-itable" border :columns="deptColumns" :data="departments"
|
||||
:row-class-name="rowClassName" no-data-text="该分类下暂无部门">
|
||||
<template #members="{ row }">
|
||||
<Tooltip v-if="Number(row.memberCount || 0)" 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>
|
||||
</template>
|
||||
<template #status="{ row }">
|
||||
<Tag type="border" :color="row.enabled ? 'success' : 'error'" class="sta-tag"
|
||||
:title="row.enabled ? '点击停用' : '点击启用'" @click="onToggleDept(row)">
|
||||
{{ row.enabled ? '已启用' : '已停用' }}
|
||||
</Tag>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<a class="lnk" @click="openDeptEdit(row)">编辑</a>
|
||||
<a class="lnk" @click="openMembers(row)">成员</a>
|
||||
<a class="lnk danger" @click="removeDept(row)">删除</a>
|
||||
</template>
|
||||
</Table>
|
||||
<div v-if="departments.length" class="table-foot">共 {{ departments.length }} 个部门</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 排序弹框:拖动左侧手柄调整顺序,保存后按新顺序落库 -->
|
||||
<Modal v-model="sortVisible" :title="`部门排序 · ${activeCategoryName}`" width="520" :mask-closable="false">
|
||||
<Alert type="info" show-icon class="sort-tip">拖动左侧手柄调整顺序,保存后生效。</Alert>
|
||||
<ul class="sort-list">
|
||||
<li v-for="(d, idx) in sortList" :key="d.id" class="sort-item"
|
||||
:class="{ dragging: sortDragIdx === idx, 'drag-over': sortOverIdx === idx }" draggable="true"
|
||||
@dragstart="onSortDragStart(idx)" @dragover.prevent="onSortOver(idx)" @drop.prevent="onSortDrop(idx)"
|
||||
@dragend="onSortDragEnd">
|
||||
<i class="grip">⠿</i>
|
||||
<span class="sno">{{ idx + 1 }}</span>
|
||||
<span class="snm" :title="d.name">{{ d.name }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<template #footer>
|
||||
<Button @click="sortVisible = false">取消</Button>
|
||||
<Button type="primary" :loading="sortSaving" @click="saveSort">保存排序</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
|
||||
<!-- 名称录入(分类新增/改名,部门新增/改名共用) -->
|
||||
<Modal v-model="nameModal.visible" :title="nameModal.title" :mask-closable="false" width="420">
|
||||
<Form :label-width="72">
|
||||
@@ -140,7 +135,8 @@ import CustomMemberModal from "./CustomMemberModal.vue";
|
||||
* CUSTOM 自定义部门管理面板(部门管理页的第二个来源页签)。
|
||||
* 口径:分类(平铺列表,容器)→ 部门(可启停 / 可配成员 / 软删)→ 成员(本校教职工,服务端排除学生组)。
|
||||
* 学校上下文由父级传入,和 SYSTEM 页签共用,切换来源不改学校。
|
||||
* 2026-10-23:部门改为表格 + 行拖动排序(沿用 customDeptSort),分类也支持拖动(沿用 customDeptCategorySort)。
|
||||
* 2026-10-23:部门改为 iView Table;排序移入「排序管理」弹框拖动(沿用 customDeptSort),
|
||||
* 分类仍是列表内拖动(沿用 customDeptCategorySort)。
|
||||
*/
|
||||
const props = defineProps<{ schoolId?: number | null }>();
|
||||
|
||||
@@ -155,6 +151,18 @@ const loadingDepartments = ref(false);
|
||||
const activeCategory = computed(() => categories.value.find((c) => c.id === activeCategoryId.value) || null);
|
||||
const activeCategoryName = computed(() => activeCategory.value?.name || "");
|
||||
|
||||
/** iView Table 列定义(排序不在表格里做,行不拖拽) */
|
||||
const deptColumns: any[] = [
|
||||
{ title: "序号", type: "index", width: 80, align: "center" },
|
||||
{ title: "部门名称", key: "name", minWidth: 220 },
|
||||
{ title: "人员", slot: "members", minWidth: 300 },
|
||||
{ title: "状态", slot: "status", width: 120, align: "center" },
|
||||
{ title: "操作", slot: "actions", width: 160, align: "right" },
|
||||
];
|
||||
function rowClassName(row: any) {
|
||||
return row?.enabled ? "" : "is-off";
|
||||
}
|
||||
|
||||
const nameModal = reactive({
|
||||
visible: false,
|
||||
saving: false,
|
||||
@@ -272,6 +280,69 @@ watch(() => props.schoolId, () => {
|
||||
|
||||
watch(activeCategoryId, () => loadDepartments());
|
||||
|
||||
/* ================= 部门排序(弹框拖动) ================= */
|
||||
|
||||
const sortVisible = ref(false);
|
||||
const sortSaving = ref(false);
|
||||
const sortList = ref<CustomDepartmentVo[]>([]);
|
||||
const sortDragIdx = ref<number | null>(null);
|
||||
const sortOverIdx = ref<number | null>(null);
|
||||
|
||||
function openSort() {
|
||||
if (!departments.value.length) return;
|
||||
sortList.value = [...departments.value];
|
||||
sortDragIdx.value = null;
|
||||
sortOverIdx.value = null;
|
||||
sortVisible.value = true;
|
||||
}
|
||||
|
||||
function onSortDragStart(idx: number) {
|
||||
sortDragIdx.value = idx;
|
||||
}
|
||||
function onSortOver(idx: number) {
|
||||
if (sortDragIdx.value === null) return;
|
||||
sortOverIdx.value = idx;
|
||||
if (sortDragIdx.value === idx) return;
|
||||
// 拖动过程中实时重排,松手即落当前顺序
|
||||
const ordered = [...sortList.value];
|
||||
const [row] = ordered.splice(sortDragIdx.value, 1);
|
||||
ordered.splice(idx, 0, row);
|
||||
sortList.value = ordered;
|
||||
sortDragIdx.value = idx;
|
||||
}
|
||||
function onSortDrop() {
|
||||
sortDragIdx.value = null;
|
||||
}
|
||||
|
||||
function onSortDragEnd() {
|
||||
sortDragIdx.value = null;
|
||||
sortOverIdx.value = null;
|
||||
}
|
||||
|
||||
async function saveSort() {
|
||||
if (!sortList.value.length) return;
|
||||
sortSaving.value = true;
|
||||
try {
|
||||
await Apis.CustomDept.customDeptSort({
|
||||
data: {
|
||||
schoolId: props.schoolId as number,
|
||||
categoryId: activeCategoryId.value as number,
|
||||
ids: sortList.value.map((d) => d.id as number),
|
||||
},
|
||||
cacheFor: null,
|
||||
});
|
||||
// 右表直接采用弹框里的顺序,避免整表重取导致姓名预览再拉一轮
|
||||
departments.value = [...sortList.value];
|
||||
sortVisible.value = false;
|
||||
Message.success("排序已保存");
|
||||
} catch (e: any) {
|
||||
console.warn(e?.message || "部门排序保存失败");
|
||||
await loadDepartments();
|
||||
} finally {
|
||||
sortSaving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/* ================= 分类拖动排序 ================= */
|
||||
|
||||
const catDragIdx = ref<number | null>(null);
|
||||
@@ -484,52 +555,6 @@ async function doRemoveDept(d: CustomDepartmentVo) {
|
||||
}
|
||||
}
|
||||
|
||||
/* ================= 部门拖动排序 ================= */
|
||||
|
||||
const dragIdx = ref<number | null>(null);
|
||||
const dragOverIdx = ref<number | null>(null);
|
||||
|
||||
function onDragStart(idx: number) {
|
||||
dragIdx.value = idx;
|
||||
}
|
||||
function onDragOver(idx: number) {
|
||||
if (dragIdx.value === null) return;
|
||||
dragOverIdx.value = idx;
|
||||
}
|
||||
function onDrop(idx: number) {
|
||||
if (dragIdx.value === null || dragIdx.value === idx) {
|
||||
onDragEnd();
|
||||
return;
|
||||
}
|
||||
const ordered = [...departments.value];
|
||||
const [row] = ordered.splice(dragIdx.value, 1);
|
||||
ordered.splice(idx, 0, row);
|
||||
departments.value = ordered;
|
||||
onDragEnd();
|
||||
persistDeptOrder();
|
||||
}
|
||||
function onDragEnd() {
|
||||
dragIdx.value = null;
|
||||
dragOverIdx.value = null;
|
||||
}
|
||||
|
||||
/** 拖完一次性提交整表顺序(服务端按传入 ids 落 sort_no) */
|
||||
async function persistDeptOrder() {
|
||||
try {
|
||||
await Apis.CustomDept.customDeptSort({
|
||||
data: {
|
||||
schoolId: props.schoolId as number,
|
||||
categoryId: activeCategoryId.value as number,
|
||||
ids: departments.value.map((d) => d.id as number),
|
||||
},
|
||||
cacheFor: null,
|
||||
});
|
||||
} catch (e: any) {
|
||||
console.warn(e?.message || "部门排序保存失败");
|
||||
await loadDepartments();
|
||||
}
|
||||
}
|
||||
|
||||
function openMembers(d: CustomDepartmentVo) {
|
||||
memberModal.dept = d;
|
||||
memberModal.visible = true;
|
||||
@@ -675,7 +700,7 @@ async function refreshAfterMemberChange() {
|
||||
}
|
||||
|
||||
.cat-item:hover .grip,
|
||||
.dept-table tbody tr:hover .grip {
|
||||
.sort-item:hover .grip {
|
||||
color: #8b96a8;
|
||||
}
|
||||
|
||||
@@ -714,138 +739,62 @@ async function refreshAfterMemberChange() {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.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;
|
||||
.dept-itable {
|
||||
:deep(.ivu-table-cell) {
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
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 {
|
||||
cursor: grab;
|
||||
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
}
|
||||
|
||||
tbody tr:hover td {
|
||||
background: #fafbfe;
|
||||
}
|
||||
|
||||
tbody tr.is-off td {
|
||||
:deep(.ivu-table-row.is-off td) {
|
||||
background: #fcfcfd;
|
||||
color: var(--text-3, #98a1b2);
|
||||
}
|
||||
|
||||
tbody tr.dragging {
|
||||
opacity: .45;
|
||||
:deep(.ivu-table-row.is-off:hover td) {
|
||||
background: #f8f9fb;
|
||||
}
|
||||
}
|
||||
|
||||
.mem-cell {
|
||||
cursor: pointer;
|
||||
color: var(--text-2, #5a6478);
|
||||
border-bottom: 1px dashed transparent;
|
||||
}
|
||||
|
||||
.mem-cell:hover {
|
||||
color: var(--brand, #577afb);
|
||||
border-bottom-color: #c9d4f7;
|
||||
}
|
||||
|
||||
.dim {
|
||||
color: var(--text-3, #98a1b2);
|
||||
font-size: 11.5px;
|
||||
}
|
||||
|
||||
.sta-tag {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sta-tag:hover {
|
||||
opacity: .82;
|
||||
}
|
||||
|
||||
.lnk {
|
||||
margin-left: 10px;
|
||||
color: var(--brand, #577afb);
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
tbody tr.drag-over td {
|
||||
border-top-color: var(--brand, #577afb);
|
||||
box-shadow: inset 0 2px 0 var(--brand, #577afb);
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.ta-c {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ta-r {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.dim {
|
||||
color: var(--text-3, #98a1b2);
|
||||
font-size: 11.5px;
|
||||
}
|
||||
|
||||
.grip {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.mem-cell {
|
||||
cursor: pointer;
|
||||
border-bottom: 1px dashed transparent;
|
||||
|
||||
&:hover {
|
||||
color: var(--brand, #577afb);
|
||||
border-bottom-color: #c9d4f7;
|
||||
}
|
||||
}
|
||||
|
||||
.lnk {
|
||||
margin-left: 10px;
|
||||
color: var(--brand, #577afb);
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
&.danger {
|
||||
color: #ed4014;
|
||||
}
|
||||
}
|
||||
|
||||
/* 胶囊开关(与系统部门表格同款比例) */
|
||||
.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;
|
||||
}
|
||||
}
|
||||
&.danger {
|
||||
color: #ed4014;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -855,4 +804,66 @@ async function refreshAfterMemberChange() {
|
||||
font-size: 12px;
|
||||
color: var(--text-3, #98a1b2);
|
||||
}
|
||||
|
||||
/* ================= 排序弹框 ================= */
|
||||
.sort-tip {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.sort-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
max-height: 52vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.sort-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
height: 44px;
|
||||
padding: 0 12px;
|
||||
margin-bottom: 6px;
|
||||
border: 1px solid var(--line, #e9edf5);
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
cursor: grab;
|
||||
transition: border-color .15s, box-shadow .15s;
|
||||
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
&.dragging {
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
&.drag-over {
|
||||
border-color: var(--brand, #577afb);
|
||||
box-shadow: 0 0 0 2px rgba(87, 122, 251, .16);
|
||||
}
|
||||
}
|
||||
|
||||
.sort-item .sno {
|
||||
flex: 0 0 auto;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: var(--brand, #577afb);
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
line-height: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sort-item .snm {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 12.5px;
|
||||
color: var(--text, #1e2635);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
+139
-187
@@ -18,17 +18,19 @@
|
||||
<Option v-for="s in schoolOptions" :key="s.id" :value="s.id">{{ s.name }}</Option>
|
||||
</Select>
|
||||
<!-- 来源切换:只切右侧管理视图,学校上下文共用,不改底层组织树 -->
|
||||
<div class="source-tabs ml-auto">
|
||||
<span class="source-tab" :class="{ 'is-active': source === 'SYSTEM' }"
|
||||
@click="source = 'SYSTEM'">系统部门 SYSTEM</span>
|
||||
<span class="source-tab" :class="{ 'is-active': source === 'CUSTOM' }"
|
||||
@click="source = 'CUSTOM'">自定义部门 CUSTOM</span>
|
||||
<div class="title-tail">
|
||||
<span v-if="source === 'SYSTEM'" class="inc-ctrl" title="打开后列出该校下所有层级的部门"
|
||||
@click="toggleInclude">
|
||||
<span class="inc-label">包含子组</span>
|
||||
<i class="sw-mini" :class="{ 'is-on': includeOn }"></i>
|
||||
</span>
|
||||
<div class="source-tabs">
|
||||
<span class="source-tab" :class="{ 'is-active': source === 'SYSTEM' }"
|
||||
@click="source = 'SYSTEM'">系统部门</span>
|
||||
<span class="source-tab" :class="{ 'is-active': source === 'CUSTOM' }"
|
||||
@click="source = 'CUSTOM'">自定义部门</span>
|
||||
</div>
|
||||
</div>
|
||||
<span v-if="source === 'SYSTEM'" class="inc-ctrl" title="打开后列出该校下所有层级的部门"
|
||||
@click="toggleInclude">
|
||||
<span class="inc-label">包含子组</span>
|
||||
<i class="sw-mini" :class="{ 'is-on': includeOn }"></i>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<template v-if="source === 'SYSTEM'">
|
||||
@@ -47,50 +49,30 @@
|
||||
|
||||
<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>
|
||||
<Table class="dept-itable" border :columns="sysColumns" :data="flatRows"
|
||||
:row-class-name="rowClassName" :loading="false" no-data-text="暂无部门">
|
||||
<template #name="{ row }">
|
||||
<span class="nmcell" :style="{ paddingLeft: row.level * 20 + 'px' }">
|
||||
<i v-if="row.kidCount" class="tw" @click.stop="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>
|
||||
</template>
|
||||
<template #members="{ row }">
|
||||
<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>
|
||||
</template>
|
||||
<template #status="{ row }">
|
||||
<Tag type="border" :color="row.status === 1 ? 'error' : 'success'" class="sta-tag"
|
||||
:title="row.status === 1 ? '点击启用' : '点击停用'" @click="onToggle(row.raw)">
|
||||
{{ row.status === 1 ? '已停用' : '已启用' }}
|
||||
</Tag>
|
||||
</template>
|
||||
</Table>
|
||||
<div class="table-foot">共 {{ flatRows.length }} 个部门</div>
|
||||
</template>
|
||||
</template>
|
||||
@@ -240,6 +222,19 @@ const flatRows = computed<DeptRow[]>(() => {
|
||||
return out;
|
||||
});
|
||||
|
||||
/** iView Table 列定义:层级用「部门名称」列的缩进 + 折叠表达,不用 Table 自带树(避免样式不可控) */
|
||||
const sysColumns: any[] = [
|
||||
{ title: "序号", type: "index", width: 80, align: "center" },
|
||||
{ title: "部门名称", slot: "name", minWidth: 300 },
|
||||
{ title: "上级", key: "parentName", width: 160 },
|
||||
{ title: "人员", slot: "members", minWidth: 320 },
|
||||
{ title: "状态", slot: "status", width: 120, align: "center" },
|
||||
];
|
||||
/** 停用行整行灰化 */
|
||||
function rowClassName(row: any) {
|
||||
return row?.status === 1 ? "is-off" : "";
|
||||
}
|
||||
|
||||
/** 人员列:前 3 个姓名 + 剩余数;无姓名时退回「N 人」 */
|
||||
function memberText(row: DeptRow) {
|
||||
if (!row.memberNames.length) return `${row.memberCount} 人`;
|
||||
@@ -477,6 +472,14 @@ async function loadSchoolScope() {
|
||||
color: var(--text, #1e2635);
|
||||
}
|
||||
|
||||
/* 标题行右侧:包含子组 + 来源切换,整体右对齐;「包含子组」放左边,切视图时 tab 位置不跳 */
|
||||
.title-tail {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.school-select {
|
||||
width: 180px;
|
||||
}
|
||||
@@ -570,7 +573,7 @@ async function loadSchoolScope() {
|
||||
}
|
||||
}
|
||||
|
||||
/* ================= 部门表格 ================= */
|
||||
/* ================= 部门表格(iView Table) ================= */
|
||||
.row-sum {
|
||||
flex: 0 0 auto;
|
||||
margin-bottom: 8px;
|
||||
@@ -578,147 +581,96 @@ async function loadSchoolScope() {
|
||||
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;
|
||||
/* 表格本体外观全部交给 iView:只补单元格内部的排版细节 */
|
||||
.dept-itable {
|
||||
:deep(.ivu-table-cell) {
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
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 {
|
||||
:deep(.ivu-table-row.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;
|
||||
}
|
||||
}
|
||||
:deep(.ivu-table-row.is-off:hover td) {
|
||||
background: #f8f9fb;
|
||||
}
|
||||
}
|
||||
|
||||
.nmcell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.nmcell .tw {
|
||||
flex: 0 0 auto;
|
||||
width: 14px;
|
||||
text-align: center;
|
||||
font-style: normal;
|
||||
font-size: 9px;
|
||||
color: #aeb6c2;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nmcell .tw.blank {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.nmcell .nm {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.nmcell .nm.root {
|
||||
font-weight: 600;
|
||||
color: var(--text, #1e2635);
|
||||
}
|
||||
|
||||
.nmcell .cnt {
|
||||
flex: 0 0 auto;
|
||||
padding: 0 5px;
|
||||
line-height: 16px;
|
||||
border-radius: 4px;
|
||||
background: #f1f3f7;
|
||||
color: #8a94a6;
|
||||
font-size: 10.5px;
|
||||
}
|
||||
|
||||
.nmcell .cnt.grey {
|
||||
background: #f6f7fa;
|
||||
color: #a3adbd;
|
||||
}
|
||||
|
||||
.mem-cell {
|
||||
cursor: pointer;
|
||||
color: var(--text-2, #5a6478);
|
||||
border-bottom: 1px dashed transparent;
|
||||
}
|
||||
|
||||
.mem-cell:hover {
|
||||
color: var(--brand, #577afb);
|
||||
border-bottom-color: #c9d4f7;
|
||||
}
|
||||
|
||||
.dim {
|
||||
color: var(--text-3, #98a1b2);
|
||||
font-size: 11.5px;
|
||||
}
|
||||
|
||||
/* 状态标签可点:hover 给一点反馈,暗示能切换 */
|
||||
.sta-tag {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sta-tag:hover {
|
||||
opacity: .82;
|
||||
}
|
||||
|
||||
|
||||
.table-foot {
|
||||
flex: 0 0 auto;
|
||||
padding: 10px 2px;
|
||||
|
||||
Reference in New Issue
Block a user