puzhibing
2026-08-28 e92cd56bb56b111c1afe9922191f9897215ff6b0
frontend-admin/src/views/StaffGroupsView.vue
@@ -2,48 +2,52 @@
  <div class="page">
    <el-form :inline="true" class="search-form" @submit.prevent="doSearch">
      <el-form-item label="分组名称">
        <el-input v-model="filters.groupName" placeholder="模糊" clearable style="width: 160px" />
        <el-input v-model="filters.groupName" placeholder="请输入分组名称" clearable style="width: 160px" />
      </el-form-item>
      <el-form-item label="负责人电话">
        <el-input v-model="filters.leaderMobile" placeholder="模糊" clearable style="width: 160px" />
        <el-input v-model="filters.leaderMobile" placeholder="请输入负责人电话" clearable style="width: 160px" />
      </el-form-item>
      <el-form-item>
        <el-button type="primary" native-type="submit">搜索</el-button>
        <el-button @click="resetSearch">重置</el-button>
      </el-form-item>
      <el-form-item class="push-right">
        <el-button type="success" @click="openCreate">新建分组</el-button>
        <el-button type="primary" @click="openCreate">新建分组</el-button>
      </el-form-item>
    </el-form>
    <el-table :data="rows" v-loading="loading" stripe>
      <el-table-column label="序号" width="64" type="index" :index="indexMethod" />
      <el-table-column prop="groupName" label="分组名称" min-width="140" show-overflow-tooltip />
      <el-table-column prop="leaderName" label="负责人" width="110" show-overflow-tooltip />
      <el-table-column prop="leaderMobileMasked" label="联系电话" width="140" />
      <el-table-column prop="memberCount" label="绑定人员数量" width="120" />
      <el-table-column label="创建时间" width="170">
        <template #default="{ row }">{{ formatTime(row.createTime) }}</template>
      </el-table-column>
      <el-table-column label="操作" width="240" fixed="right">
        <template #default="{ row }">
          <div class="table-ops">
            <el-button type="primary" link @click="goDetail(row)">详情</el-button>
            <el-button type="primary" link @click="openEdit(row)">编辑</el-button>
            <el-button type="danger" link @click="confirmDelete(row)">删除</el-button>
          </div>
        </template>
      </el-table-column>
    </el-table>
    <div class="table-wrap">
      <el-table :data="rows" v-loading="loading" stripe height="100%">
        <el-table-column label="序号" width="64" type="index" :index="indexMethod" />
        <el-table-column prop="groupName" label="分组名称" min-width="140" show-overflow-tooltip />
        <el-table-column prop="leaderName" label="负责人" width="110" show-overflow-tooltip />
        <el-table-column prop="leaderMobileMasked" label="联系电话" width="140" />
        <el-table-column prop="memberCount" label="绑定人员数量" width="120" />
        <el-table-column label="创建时间" width="170">
          <template #default="{ row }">{{ formatTime(row.createTime) }}</template>
        </el-table-column>
        <el-table-column label="操作" width="240" fixed="right">
          <template #default="{ row }">
            <div class="table-ops">
              <el-button type="primary" link @click="goDetail(row)">详情</el-button>
              <el-button type="primary" link @click="openEdit(row)">编辑</el-button>
              <el-button type="primary" link @click="confirmDelete(row)">删除</el-button>
            </div>
          </template>
        </el-table-column>
      </el-table>
    </div>
    <el-pagination
      class="pager"
      class="main-pager"
      background
      layout="prev, pager, next"
      layout="sizes, prev, pager, next"
      :total="total"
      :page-size="size"
      :page-sizes="[10, 20, 50, 100]"
      v-model:current-page="page"
      @current-change="load"
      @size-change="onMainSizeChange"
    />
    <el-dialog v-model="dlg" :title="dlgTitle" width="860px" destroy-on-close class="group-dialog">
@@ -60,7 +64,12 @@
            <template #label>
              <span class="req-label">负责人</span>
            </template>
            <el-select v-model="form.leaderMpUserId" placeholder="请输入" class="fullw">
            <el-select
              v-model="form.leaderMpUserId"
              placeholder="请选择负责人"
              class="fullw"
              @visible-change="onLeaderVisibleChange"
            >
              <el-option
                v-for="m in memberList"
                :key="m.id"
@@ -96,7 +105,7 @@
            </div>
          </div>
          <el-table :data="memberList" stripe border>
          <el-table :data="memberList" stripe border :max-height="memberTableMaxHeight">
            <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 />
@@ -106,10 +115,10 @@
                <div class="table-ops">
                  <el-tooltip v-if="row.id === form.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" link @click="removeMember(row.id)">移除</el-button>
                  <el-button v-else type="primary" link @click="removeMember(row.id)">移除</el-button>
                </div>
              </template>
            </el-table-column>
@@ -129,10 +138,10 @@
      <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
@@ -156,12 +165,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" :selectable="pickerRowSelectable" />
        <el-table-column type="selection" width="48" :selectable="pickerRowSelectable" :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" />
@@ -171,11 +181,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>
@@ -194,6 +206,7 @@
import { ElMessage, ElMessageBox } from "element-plus";
import { useRouter } from "vue-router";
import http from "../api/http";
import { normalizeFormData } from "../utils/formHelper";
type GroupRow = {
  id: number;
@@ -276,6 +289,12 @@
  load();
}
function onMainSizeChange(nextSize: number) {
  size.value = nextSize;
  page.value = 1;
  load();
}
function goDetail(r: GroupRow) {
  router.push({ name: "staff-group-detail", params: { id: r.id }, query: { uuid: r.uuid } });
}
@@ -294,6 +313,7 @@
const membersMap = ref(new Map<number, StaffRow>());
const memberList = computed(() => Array.from(membersMap.value.values()));
const memberTableMaxHeight = computed(() => (memberList.value.length > 20 ? 560 : undefined));
async function refreshLeaderMobile() {
  const id = form.leaderMpUserId;
@@ -409,6 +429,16 @@
  membersMap.value.delete(id);
}
function onLeaderVisibleChange(visible: boolean) {
  if (!visible) {
    return;
  }
  if (memberList.value.length > 0) {
    return;
  }
  ElMessage.warning("请先选择人员,再从已有人员列表中选择负责人");
}
async function submitForm() {
  if (!form.groupName.trim()) {
    ElMessage.warning("请填写分组名称");
@@ -430,19 +460,21 @@
  saving.value = true;
  try {
    if (!editing.value) {
      await http.post("/sys/staff-groups", {
      const payload = normalizeFormData({
        groupName: form.groupName.trim(),
        leaderMpUserId: form.leaderMpUserId,
        memberIds,
      });
      await http.post("/sys/staff-groups", payload);
      ElMessage.success("已创建");
    } else {
      await http.put(`/sys/staff-groups/${editing.value.id}`, {
      const payload = normalizeFormData({
        uuid: form.uuid,
        groupName: form.groupName.trim(),
        leaderMpUserId: form.leaderMpUserId,
        memberIds,
      });
      await http.put(`/sys/staff-groups/${editing.value.id}`, payload);
      ElMessage.success("已保存");
    }
    dlg.value = false;
@@ -484,6 +516,7 @@
});
const pickedMap = ref(new Map<number, StaffRow>());
const restoringPickerSelection = ref(false);
const deptOptions = ref<DeptTree[]>([]);
@@ -521,6 +554,12 @@
  loadPicker();
}
function onPickerSizeChange(nextSize: number) {
  pickerSize.value = nextSize;
  pickerPage.value = 1;
  loadPicker();
}
function pickerParams() {
  return {
    page: pickerPage.value,
@@ -529,7 +568,6 @@
    realName: pickerFilters.realName?.trim() || undefined,
    mobile: pickerFilters.mobile?.trim() || undefined,
    status: 1,
    excludeStaffGroupId: editing.value?.id,
  };
}
@@ -540,20 +578,26 @@
    pickerRows.value = (data.result || []) as StaffRow[];
    pickerTotal.value = Number(data.total) || 0;
    await nextTick();
    // re-apply selection
    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) {
@@ -600,13 +644,25 @@
<style scoped>
.search-form {
  margin-bottom: 12px;
  flex: none;
}
.push-right {
  margin-left: auto;
}
.pager {
.page {
  display: flex;
  flex-direction: column;
  height: calc(100vh - 190px);
  min-height: 420px;
}
.table-wrap {
  flex: 1;
  min-height: 0;
}
.main-pager {
  margin-top: 12px;
  justify-content: flex-end;
  flex: none;
}
.member-actions {
  display: flex;
@@ -628,18 +684,8 @@
}
.req-label {
  position: relative;
  padding-left: 10px;
  color: #364867;
  font-weight: 600;
}
.req-label::before {
  content: "*";
  position: absolute;
  left: 0;
  top: 0;
  color: #ff5b5b;
}
.phone-item {