diff --git a/src/api/globals.d.ts b/src/api/globals.d.ts index f72e604..56e488c 100644 --- a/src/api/globals.d.ts +++ b/src/api/globals.d.ts @@ -301,6 +301,10 @@ export interface SysGroupVo { status?: number; /** 包含子组:1=含下级部门 / 0=仅本级(仅学校节点有值) */ includeChildren?: number; + /** 直属成员数(部门表格「人员」列;与服务端 SysGroupVo 同步,api:gen 后一样保留) */ + memberCount?: number; + /** 直属成员姓名预览(最多 50 个),完整名单走 /api/sysuser/List?groupId= */ + memberNames?: string[]; children?: SysGroupVo[]; } export interface AppItem { diff --git a/src/views/dept-manage/CustomDeptCard.vue b/src/views/dept-manage/CustomDeptCard.vue index 6e875ae..7b8d69e 100644 --- a/src/views/dept-manage/CustomDeptCard.vue +++ b/src/views/dept-manage/CustomDeptCard.vue @@ -1,3 +1,4 @@ + @@ -90,18 +134,20 @@ import { Message } from "view-ui-plus"; import Apis from "@/api"; import type { CustomDeptCategoryVo, CustomDepartmentVo } from "@/api/globals"; import MyConfirm from "@/components/MyConfirm"; -import CustomDeptCard from "./CustomDeptCard.vue"; import CustomMemberModal from "./CustomMemberModal.vue"; /** * CUSTOM 自定义部门管理面板(部门管理页的第二个来源页签)。 * 口径:分类(平铺列表,容器)→ 部门(可启停 / 可配成员 / 软删)→ 成员(本校教职工,服务端排除学生组)。 * 学校上下文由父级传入,和 SYSTEM 页签共用,切换来源不改学校。 + * 2026-10-23:部门改为表格 + 行拖动排序(沿用 customDeptSort),分类也支持拖动(沿用 customDeptCategorySort)。 */ const props = defineProps<{ schoolId?: number | null }>(); const categories = ref([]); const departments = ref([]); +/** deptId → 成员姓名(前 50),表格「人员」列用;完整名单在成员弹窗 */ +const memberNames = ref>({}); const activeCategoryId = ref(null); const loadingCategories = ref(false); const loadingDepartments = ref(false); @@ -154,6 +200,7 @@ async function loadCategories(keepActive = true) { async function loadDepartments() { if (!props.schoolId || !activeCategoryId.value) { departments.value = []; + memberNames.value = {}; return; } loadingDepartments.value = true; @@ -163,14 +210,54 @@ async function loadDepartments() { cacheFor: null, })) as CustomDepartmentVo[]; departments.value = rows || []; + loadMemberNames(); } catch (e: any) { console.warn(e?.message || "加载自定义部门失败"); departments.value = []; + memberNames.value = {}; } finally { loadingDepartments.value = false; } } +/** 表格「人员」列要显示姓名,departmentCount 只有数量,这里按部门并发补一次(分类下部门数通常很少) */ +async function loadMemberNames() { + const targets = departments.value.filter((d) => Number(d.memberCount || 0) > 0); + if (!targets.length) { + memberNames.value = {}; + return; + } + const map: Record = {}; + await Promise.all(targets.map(async (d) => { + try { + const rows: any[] = (await Apis.CustomDept.customDeptMembers({ + pathParams: { id: d.id as number }, + cacheFor: null, + })) || []; + // 预览上限与系统部门对齐,避免超部门时把长名单塞进页面 + map[d.id as number] = rows.slice(0, 50).map((u) => u.name || u.account || ""); + } catch (e: any) { + map[d.id as number] = []; + } + })); + memberNames.value = map; +} + +function memberText(d: CustomDepartmentVo) { + const names = memberNames.value[d.id as number] || []; + const total = Number(d.memberCount || 0); + if (!names.length) return `${total} 人`; + const head = names.slice(0, 3).join("、"); + const rest = total - Math.min(3, names.length); + return rest > 0 ? `${head} +${rest}` : head; +} +function memberTip(d: CustomDepartmentVo) { + const names = memberNames.value[d.id as number] || []; + const total = Number(d.memberCount || 0); + const more = total > names.length ? `…(仅预览前 ${names.length} 人)` : ""; + return `共 ${total} 人:${names.join("、")}${more} 点击查看全部`; +} + function selectCategory(id: number | null) { activeCategoryId.value = id; loadDepartments(); @@ -179,11 +266,47 @@ function selectCategory(id: number | null) { watch(() => props.schoolId, () => { activeCategoryId.value = null; departments.value = []; + memberNames.value = {}; loadCategories(false); }, { immediate: true }); watch(activeCategoryId, () => loadDepartments()); +/* ================= 分类拖动排序 ================= */ + +const catDragIdx = ref(null); + +function onCatDragStart(idx: number) { + catDragIdx.value = idx; +} +function onCatDragOver(idx: number) { + if (catDragIdx.value === null || catDragIdx.value === idx) return; + const ordered = [...categories.value]; + const [row] = ordered.splice(catDragIdx.value, 1); + ordered.splice(idx, 0, row); + categories.value = ordered; + catDragIdx.value = idx; +} +function onCatDrop() { + if (catDragIdx.value === null) return; + catDragIdx.value = null; + persistCategoryOrder(); +} +function onCatDragEnd() { + catDragIdx.value = null; +} +async function persistCategoryOrder() { + try { + await Apis.CustomDeptCategory.customDeptCategorySort({ + data: { schoolId: props.schoolId as number, ids: categories.value.map((c) => c.id as number) }, + cacheFor: null, + }); + } catch (e: any) { + console.warn(e?.message || "分类排序保存失败"); + await loadCategories(); + } +} + /* ================= 分类操作 ================= */ const hasDepartments = (c: CustomDeptCategoryVo) => Number(c.departmentCount || 0) > 0; @@ -228,15 +351,7 @@ async function moveCategory(idx: number, delta: number) { const [row] = ordered.splice(idx, 1); ordered.splice(target, 0, row); categories.value = ordered; - try { - await Apis.CustomDeptCategory.customDeptCategorySort({ - data: { schoolId: props.schoolId as number, ids: ordered.map((c) => c.id as number) }, - cacheFor: null, - }); - } catch (e: any) { - console.warn(e?.message || "分类排序保存失败"); - await loadCategories(); - } + await persistCategoryOrder(); } /* ================= 部门操作 ================= */ @@ -369,20 +484,43 @@ async function doRemoveDept(d: CustomDepartmentVo) { } } -/** 部门上移/下移:同分类内整表重排提交 */ -async function moveDept(idx: number, delta: number) { - const target = idx + delta; - if (target < 0 || target >= departments.value.length) return; +/* ================= 部门拖动排序 ================= */ + +const dragIdx = ref(null); +const dragOverIdx = ref(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(idx, 1); - ordered.splice(target, 0, row); + 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: ordered.map((d) => d.id as number), + ids: departments.value.map((d) => d.id as number), }, cacheFor: null, }); @@ -396,6 +534,11 @@ function openMembers(d: CustomDepartmentVo) { memberModal.dept = d; memberModal.visible = true; } + +/** 成员弹窗改完后:人数变了,姓名预览也要重取 */ +async function refreshAfterMemberChange() { + await Promise.all([loadCategories(), loadDepartments()]); +} diff --git a/src/views/dept-manage/DeptCard.vue b/src/views/dept-manage/DeptCard.vue index 613761f..4aae36c 100644 --- a/src/views/dept-manage/DeptCard.vue +++ b/src/views/dept-manage/DeptCard.vue @@ -1,3 +1,4 @@ + + + + +
+
加载中…
+
该部门暂无直属成员
+
    +
  • + {{ u.realName || u.account }} + {{ u.account }} +
  • +
+
+
+ +
+