From 9d4cbff7464ebfd7803c2ce53ded49d0f73fe011 Mon Sep 17 00:00:00 2001
From: longz <longzhaoz@qq.com>
Date: 星期一, 29 六月 2026 20:33:55 +0800
Subject: [PATCH] 初步调整fix

---
 frontend-admin/src/views/DeptManageView.vue |  242 +++++++++++++++++++++++++++++++++++++++++++++++-
 1 files changed, 235 insertions(+), 7 deletions(-)

diff --git a/frontend-admin/src/views/DeptManageView.vue b/frontend-admin/src/views/DeptManageView.vue
index 2ea82db..7cffb4c 100644
--- a/frontend-admin/src/views/DeptManageView.vue
+++ b/frontend-admin/src/views/DeptManageView.vue
@@ -59,6 +59,23 @@
         <el-form-item label="部门名称" required>
           <el-input v-model="form.deptName" maxlength="128" placeholder="请输入" />
         </el-form-item>
+        <el-form-item label="部门负责人">
+          <div class="leader-picker">
+            <el-button @click="openLeaderPicker">选择负责人</el-button>
+            <el-button v-if="selectedLeaders.length" link type="primary" @click="clearLeaders">清空</el-button>
+            <div v-if="selectedLeaders.length" class="leader-tags">
+              <el-tag
+                v-for="leader in selectedLeaders"
+                :key="leader.id"
+                closable
+                @close="removeLeader(leader.id)"
+              >
+                {{ leader.realName }}
+              </el-tag>
+            </div>
+            <div v-else class="leader-empty">未选择负责人</div>
+          </div>
+        </el-form-item>
         <el-form-item label="部门状态" required>
           <el-radio-group v-model="form.status">
             <el-radio :label="1">启用</el-radio>
@@ -78,13 +95,60 @@
         </el-button>
       </template>
     </el-dialog>
+
+    <el-dialog v-model="leaderDlg" title="选择部门负责人" width="760px" destroy-on-close @closed="resetLeaderDialog">
+      <el-form class="leader-search" @submit.prevent="loadLeaderCandidates">
+        <el-form-item label="姓名">
+          <el-input v-model="leaderFilters.realName" placeholder="请输入姓名" clearable />
+        </el-form-item>
+        <el-form-item label="联系方式">
+          <el-input v-model="leaderFilters.mobile" placeholder="请输入联系方式" clearable />
+        </el-form-item>
+        <el-form-item>
+          <el-button @click="resetLeaderSearch">重置</el-button>
+          <el-button type="primary" native-type="submit">搜索</el-button>
+        </el-form-item>
+      </el-form>
+      <el-table
+        ref="leaderTableRef"
+        :data="leaderRows"
+        v-loading="leaderLoading"
+        row-key="id"
+        border
+        height="360"
+        @selection-change="onLeaderSelectionChange"
+      >
+        <el-table-column type="selection" width="48" reserve-selection />
+        <el-table-column prop="realName" label="姓名" min-width="120" show-overflow-tooltip />
+        <el-table-column prop="mobileMasked" label="联系方式" width="140" />
+        <el-table-column prop="deptPath" label="所属部门" min-width="220" show-overflow-tooltip />
+      </el-table>
+      <div class="leader-pager">
+        <div>已选 {{ leaderSelection.length }} 人</div>
+        <el-pagination
+          background
+          layout="sizes, prev, pager, next"
+          :total="leaderTotal"
+          :page-size="leaderSize"
+          :page-sizes="[10, 20, 50, 100]"
+          v-model:current-page="leaderPage"
+          @current-change="loadLeaderCandidates"
+          @size-change="onLeaderSizeChange"
+        />
+      </div>
+      <template #footer>
+        <el-button @click="leaderDlg = false">取消</el-button>
+        <el-button type="primary" @click="confirmLeaders">确定</el-button>
+      </template>
+    </el-dialog>
   </div>
 </template>
 
 <script setup lang="ts">
-import { computed, ref } from "vue";
+import { computed, nextTick, ref } from "vue";
 import { ElMessage, ElMessageBox } from "element-plus";
 import http from "../api/http";
+import { normalizeFormData } from "../utils/formHelper";
 
 type DeptNode = {
   id: number;
@@ -94,6 +158,8 @@
   sortOrder?: number;
   status: number;
   level: number;
+  leaderIds?: number[];
+  leaderNames?: string;
   children?: DeptNode[];
 };
 
@@ -116,12 +182,25 @@
 const parentDeptName = ref("");
 const childParentId = ref<number | null>(null);
 const pendingExpandRowKey = ref<string | null>(null);
+type StaffOption = { id: number; realName: string; deptPath: string; mobileMasked?: string };
+const staffOptions = ref<StaffOption[]>([]);
+const selectedLeaders = ref<StaffOption[]>([]);
+const leaderDlg = ref(false);
+const leaderTableRef = ref();
+const leaderLoading = ref(false);
+const leaderRows = ref<StaffOption[]>([]);
+const leaderSelection = ref<StaffOption[]>([]);
+const leaderPage = ref(1);
+const leaderSize = ref(10);
+const leaderTotal = ref(0);
+const leaderFilters = ref({ realName: "", mobile: "" });
 
 const form = ref({
   uuid: "",
   deptName: "",
   status: 1,
   parentId: undefined as number | undefined,
+  leaderIds: [] as number[],
 });
 
 const dlgTitle = computed(() => {
@@ -210,13 +289,38 @@
   expandedRowKeys.value = expandedRowKeys.value.filter(id => !collapsedKeys.has(id));
 }
 
+async function loadStaffOptionsByIds(ids: number[]) {
+  const missingIds = ids.filter(id => !staffOptions.value.some(s => s.id === id));
+  if (!missingIds.length) return;
+  try {
+    const { data } = await http.get("/sys/staff", { params: { page: 1, size: 1000, status: 1 } });
+    const staffList = (data.result || []) as { id: number; realName: string; deptPath?: string }[];
+    staffOptions.value = staffList.map(s => ({
+      id: s.id,
+      realName: s.realName,
+      deptPath: s.deptPath || '未分配部门',
+    }));
+  } catch (e: unknown) {
+    console.error("加载员工列表失败", e);
+  }
+}
+
+function syncSelectedLeadersFromIds(ids: number[], namesText?: string) {
+  const names = (namesText || "").split("、").filter(Boolean);
+  selectedLeaders.value = ids.map((id, index) => {
+    const cached = staffOptions.value.find(s => s.id === id);
+    return cached || { id, realName: names[index] || `员工${id}`, deptPath: "" };
+  });
+}
+
 function openAddRoot() {
   mode.value = "root";
   editingId.value = null;
   editingIsRoot.value = true;
   parentDeptName.value = "";
   childParentId.value = null;
-  form.value = { uuid: "", deptName: "", status: 1, parentId: undefined };
+  form.value = { uuid: "", deptName: "", status: 1, parentId: undefined, leaderIds: [] };
+  selectedLeaders.value = [];
   dlg.value = true;
 }
 
@@ -227,23 +331,28 @@
   editingIsRoot.value = false;
   parentDeptName.value = row.deptName;
   childParentId.value = row.id;
-  form.value = { uuid: "", deptName: "", status: 1, parentId: row.id };
+  form.value = { uuid: "", deptName: "", status: 1, parentId: row.id, leaderIds: [] };
+  selectedLeaders.value = [];
   dlg.value = true;
 }
 
-function openEdit(row: DeptNode) {
+async function openEdit(row: DeptNode) {
   mode.value = "edit";
   editingId.value = row.id;
   const root = !row.parentId || row.parentId === 0;
   editingIsRoot.value = root;
   parentDeptName.value = "";
   childParentId.value = null;
+  const leaderIds = Array.isArray(row.leaderIds) ? row.leaderIds : [];
+  await loadStaffOptionsByIds(leaderIds);
   form.value = {
     uuid: row.uuid,
     deptName: row.deptName,
     status: row.status,
     parentId: root ? undefined : row.parentId,
+    leaderIds,
   };
+  syncSelectedLeadersFromIds(leaderIds, row.leaderNames);
   if (!root) {
     const disabled = new Set(collectDescendantIds(row));
     parentTreeSelectData.value = toTreeSelectData(treeData.value, disabled);
@@ -258,6 +367,89 @@
   parentTreeSelectData.value = [];
 }
 
+function openLeaderPicker() {
+  leaderSelection.value = [...selectedLeaders.value];
+  leaderDlg.value = true;
+  loadLeaderCandidates();
+}
+
+async function loadLeaderCandidates() {
+  leaderLoading.value = true;
+  try {
+    const { data } = await http.get("/sys/staff", {
+      params: {
+        page: leaderPage.value,
+        size: leaderSize.value,
+        status: 1,
+        realName: leaderFilters.value.realName.trim() || undefined,
+        mobile: leaderFilters.value.mobile.trim() || undefined,
+      },
+    });
+    leaderRows.value = ((data.result || []) as StaffOption[]).map(s => ({
+      ...s,
+      deptPath: s.deptPath || "未分配部门",
+    }));
+    leaderTotal.value = Number(data.total) || 0;
+    await nextTick();
+    syncLeaderTableSelection();
+  } catch (e: unknown) {
+    ElMessage.error(e instanceof Error ? e.message : "加载员工失败");
+  } finally {
+    leaderLoading.value = false;
+  }
+}
+
+function syncLeaderTableSelection() {
+  const selectedIds = new Set(leaderSelection.value.map(s => s.id));
+  leaderRows.value.forEach(row => {
+    leaderTableRef.value?.toggleRowSelection(row, selectedIds.has(row.id));
+  });
+}
+
+function onLeaderSelectionChange(selection: StaffOption[]) {
+  const currentPageIds = new Set(leaderRows.value.map(s => s.id));
+  const kept = leaderSelection.value.filter(s => !currentPageIds.has(s.id));
+  const merged = [...kept, ...selection];
+  const seen = new Set<number>();
+  leaderSelection.value = merged.filter(item => {
+    if (seen.has(item.id)) return false;
+    seen.add(item.id);
+    return true;
+  });
+}
+
+function resetLeaderSearch() {
+  leaderFilters.value = { realName: "", mobile: "" };
+  leaderPage.value = 1;
+  loadLeaderCandidates();
+}
+
+function onLeaderSizeChange(v: number) {
+  leaderSize.value = v;
+  leaderPage.value = 1;
+  loadLeaderCandidates();
+}
+
+function confirmLeaders() {
+  selectedLeaders.value = [...leaderSelection.value];
+  form.value.leaderIds = selectedLeaders.value.map(s => s.id);
+  leaderDlg.value = false;
+}
+
+function resetLeaderDialog() {
+  leaderRows.value = [];
+}
+
+function removeLeader(id: number) {
+  selectedLeaders.value = selectedLeaders.value.filter(s => s.id !== id);
+  form.value.leaderIds = selectedLeaders.value.map(s => s.id);
+}
+
+function clearLeaders() {
+  selectedLeaders.value = [];
+  form.value.leaderIds = [];
+}
+
 async function submitDlg() {
   if (!form.value.deptName.trim()) {
     ElMessage.warning("请填写部门名称");
@@ -266,11 +458,13 @@
   saving.value = true;
   try {
     if (mode.value === "root") {
-      await http.post("/sys/dept-manage", {
+      const payload = normalizeFormData({
         parentId: 0,
         deptName: form.value.deptName.trim(),
         status: form.value.status,
+        leaderIds: form.value.leaderIds,
       });
+      await http.post("/sys/dept-manage", payload);
       ElMessage.success("已创建");
     } else if (mode.value === "child") {
       if (childParentId.value == null) {
@@ -278,17 +472,20 @@
         return;
       }
       pendingExpandRowKey.value = toRowKey(childParentId.value);
-      await http.post("/sys/dept-manage", {
+      const payload = normalizeFormData({
         parentId: childParentId.value,
         deptName: form.value.deptName.trim(),
         status: form.value.status,
+        leaderIds: form.value.leaderIds,
       });
+      await http.post("/sys/dept-manage", payload);
       ElMessage.success("已创建");
     } else if (editingId.value != null) {
       const body: Record<string, unknown> = {
         uuid: form.value.uuid,
         deptName: form.value.deptName.trim(),
         status: form.value.status,
+        leaderIds: form.value.leaderIds,
       };
       if (!editingIsRoot.value) {
         if (form.value.parentId == null) {
@@ -297,7 +494,8 @@
         }
         body.parentId = form.value.parentId;
       }
-      await http.put(`/sys/dept-manage/${editingId.value}`, body);
+      const payload = normalizeFormData(body);
+      await http.put(`/sys/dept-manage/${editingId.value}`, payload);
       ElMessage.success("已保存");
     }
     dlg.value = false;
@@ -344,6 +542,36 @@
   width: 100%;
 }
 
+.leader-picker {
+  width: 100%;
+}
+
+.leader-tags {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 6px;
+  margin-top: 8px;
+}
+
+.leader-empty {
+  color: #909399;
+  font-size: 13px;
+  margin-top: 8px;
+}
+
+.leader-search {
+  display: grid;
+  grid-template-columns: 1fr 1fr auto;
+  gap: 12px;
+}
+
+.leader-pager {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-top: 12px;
+}
+
 :deep(.table-ops .el-button) {
   font-size: 14px;
 }

--
Gitblit v1.7.1