longz
2026-06-29 9d4cbff7464ebfd7803c2ce53ded49d0f73fe011
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;
}