puzhibing
2026-08-28 e92cd56bb56b111c1afe9922191f9897215ff6b0
frontend-admin/src/views/StaffGroupDetailView.vue
@@ -15,22 +15,44 @@
    </el-card>
    <el-card shadow="never">
      <el-table :data="detail.members || []" v-loading="loading" stripe border>
      <el-form :inline="true" class="member-search" @submit.prevent>
        <el-form-item label="员工姓名">
          <el-input v-model="memberFilters.realName" placeholder="请输入员工姓名" clearable style="width: 160px" />
        </el-form-item>
        <el-form-item label="联系电话">
          <el-input v-model="memberFilters.mobile" placeholder="请输入联系电话" clearable style="width: 160px" />
        </el-form-item>
        <el-form-item label="所属部门">
          <el-tree-select
            v-model="memberFilters.deptId"
            :data="deptOptions"
            :props="{ value: 'id', label: 'label', children: 'children' }"
            check-strictly
            :render-after-expand="false"
            placeholder="全部"
            clearable
            style="width: 240px"
          />
        </el-form-item>
      </el-form>
      <el-table :data="filteredMembers" v-loading="loading" stripe border>
        <el-table-column label="序号" width="64" type="index" />
        <el-table-column prop="realName" label="姓名" width="120" show-overflow-tooltip />
        <el-table-column prop="staffNo" label="编号" width="140" show-overflow-tooltip />
        <el-table-column prop="mobileMasked" label="联系电话" width="140" />
        <el-table-column prop="deptName" label="所属部门" min-width="160" show-overflow-tooltip />
        <el-table-column label="操作" width="100" fixed="right">
          <template #default="{ row }">
            <div class="table-ops">
              <el-tooltip v-if="row.id === detail.leaderMpUserId" content="不能移除负责人" placement="top">
                <span class="btn-wrap">
                  <el-button type="danger" link disabled>删除</el-button>
                  <el-button type="primary" link disabled>删除</el-button>
                </span>
              </el-tooltip>
              <el-button
                v-else
                type="danger"
                type="primary"
                link
                :loading="removingId === row.id"
                @click="confirmRemoveMember(row)"
@@ -44,14 +66,14 @@
    </el-card>
    <el-dialog v-model="picker" title="添加成员" width="960px" destroy-on-close>
      <p class="picker-hint">仅展示未加入本分组的员工;勾选后点击「追加成员」加入分组。</p>
      <p class="picker-hint">已在本分组的成员会默认勾选;勾选后点击「追加成员」加入分组。</p>
      <div class="picker-toolbar">
        <el-form :inline="true" @submit.prevent="pickerSearch">
          <el-form-item label="姓名">
            <el-input v-model="pickerFilters.realName" placeholder="模糊" clearable style="width: 140px" />
            <el-input v-model="pickerFilters.realName" placeholder="请输入姓名" clearable style="width: 140px" />
          </el-form-item>
          <el-form-item label="联系电话">
            <el-input v-model="pickerFilters.mobile" placeholder="模糊" clearable style="width: 140px" />
            <el-input v-model="pickerFilters.mobile" placeholder="请输入联系电话" clearable style="width: 140px" />
          </el-form-item>
          <el-form-item label="部门">
            <el-tree-select
@@ -75,12 +97,13 @@
      <el-table
        ref="pickerTableRef"
        :data="pickerRows"
        row-key="id"
        v-loading="pickerLoading"
        stripe
        border
        @selection-change="onPickerSelectionChange"
      >
        <el-table-column type="selection" width="48" />
        <el-table-column type="selection" width="48" :reserve-selection="true" />
        <el-table-column prop="realName" label="姓名" width="120" show-overflow-tooltip />
        <el-table-column prop="staffNo" label="编号" width="140" show-overflow-tooltip />
        <el-table-column prop="mobileMasked" label="联系电话" width="140" />
@@ -90,11 +113,13 @@
      <el-pagination
        class="pager"
        background
        layout="prev, pager, next"
        layout="sizes, prev, pager, next"
        :total="pickerTotal"
        :page-size="pickerSize"
        :page-sizes="[10, 20, 50, 100]"
        v-model:current-page="pickerPage"
        @current-change="loadPicker"
        @size-change="onPickerSizeChange"
      />
      <template #footer>
@@ -109,13 +134,14 @@
</template>
<script setup lang="ts">
import { nextTick, onMounted, reactive, ref } from "vue";
import { computed, nextTick, onMounted, reactive, ref } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { useRoute } from "vue-router";
import http from "../api/http";
import { normalizeFormData } from "../utils/formHelper";
type DeptTree = { id: number; label: string; children?: DeptTree[] };
type StaffRow = { id: number; uuid: string; realName?: string; staffNo?: string; mobileMasked: string; deptId?: number; deptName?: string };
type StaffRow = { id: number; uuid: string; realName?: string; staffNo?: string; mobileMasked?: string; deptId?: number; deptName?: string };
const route = useRoute();
@@ -135,7 +161,7 @@
  leaderMobile: "",
  memberCount: 0,
  createTime: "",
  members: [] as { id: number; uuid: string; realName: string; staffNo?: string; deptId?: number; deptName?: string }[],
  members: [] as { id: number; uuid: string; realName: string; staffNo?: string; mobileMasked?: string; deptId?: number; deptName?: string }[],
});
function formatTime(s: string) {
@@ -189,9 +215,28 @@
  mobile: "",
  deptId: undefined as number | undefined,
});
const memberFilters = reactive({
  realName: "",
  mobile: "",
  deptId: undefined as number | undefined,
});
const pickedMap = ref(new Map<number, StaffRow>());
const restoringPickerSelection = ref(false);
const deptOptions = ref<DeptTree[]>([]);
const filteredMembers = computed(() => {
  const keywordName = memberFilters.realName.trim().toLowerCase();
  const keywordMobile = memberFilters.mobile.trim();
  const deptId = memberFilters.deptId;
  return (detail.value.members || []).filter((m) => {
    const okName =
      !keywordName || (m.realName || "").toLowerCase().includes(keywordName);
    const okMobile = !keywordMobile || (m.mobileMasked || "").includes(keywordMobile);
    const okDept = deptId == null || m.deptId === deptId;
    return okName && okMobile && okDept;
  });
});
function mapDeptSelect(nodes: Record<string, unknown>[]): DeptTree[] {
  return nodes.map((n) => ({
@@ -208,7 +253,19 @@
function openAddMembers() {
  picker.value = true;
  pickedMap.value = new Map();
  const map = new Map<number, StaffRow>();
  for (const m of detail.value.members || []) {
    map.set(m.id, {
      id: m.id,
      uuid: m.uuid,
      realName: m.realName,
      staffNo: m.staffNo,
      mobileMasked: "",
      deptId: m.deptId,
      deptName: m.deptName,
    });
  }
  pickedMap.value = map;
  pickerPage.value = 1;
  loadPicker();
}
@@ -221,7 +278,6 @@
    realName: pickerFilters.realName?.trim() || undefined,
    mobile: pickerFilters.mobile?.trim() || undefined,
    status: 1,
    excludeStaffGroupId: id,
  };
}
@@ -232,19 +288,26 @@
    pickerRows.value = (data.result || []) as StaffRow[];
    pickerTotal.value = Number(data.total) || 0;
    await nextTick();
    restoringPickerSelection.value = true;
    pickerTableRef.value?.clearSelection?.();
    for (const r of pickerRows.value) {
      if (pickedMap.value.has(r.id)) {
        pickerTableRef.value?.toggleRowSelection?.(r, true);
      }
    }
    restoringPickerSelection.value = false;
  } catch (e: unknown) {
    ElMessage.error(e instanceof Error ? e.message : "加载失败");
  } finally {
    restoringPickerSelection.value = false;
    pickerLoading.value = false;
  }
}
function onPickerSelectionChange(cur: StaffRow[]) {
  if (restoringPickerSelection.value) {
    return;
  }
  const idsOnPage = new Set(pickerRows.value.map((x) => x.id));
  const selectedOnPage = new Set(cur.map((x) => x.id));
  for (const rid of idsOnPage) {
@@ -270,6 +333,12 @@
  loadPicker();
}
function onPickerSizeChange(nextSize: number) {
  pickerSize.value = nextSize;
  pickerPage.value = 1;
  loadPicker();
}
function clearPickerSelection() {
  pickedMap.value = new Map();
  pickerTableRef.value?.clearSelection?.();
@@ -281,14 +350,16 @@
}
async function submitAppendMembers() {
  const memberIds = Array.from(pickedMap.value.keys());
  const existingIds = new Set((detail.value.members || []).map((m) => m.id));
  const memberIds = Array.from(pickedMap.value.keys()).filter((id) => !existingIds.has(id));
  if (memberIds.length === 0) {
    ElMessage.warning("请至少选择一名待追加的成员");
    ElMessage.warning("没有可追加的新成员");
    return;
  }
  saving.value = true;
  try {
    await http.post(`/sys/staff-groups/${id}/members`, { uuid, memberIds });
    const payload = normalizeFormData({ uuid, memberIds });
    await http.post(`/sys/staff-groups/${id}/members`, payload);
    ElMessage.success("已追加成员");
    picker.value = false;
    pickedMap.value = new Map();