| | |
| | | </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)" |
| | |
| | | </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 |
| | |
| | | <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" /> |
| | |
| | | <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> |
| | |
| | | </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(); |
| | | |
| | |
| | | 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) { |
| | |
| | | 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) => ({ |
| | |
| | | |
| | | 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(); |
| | | } |
| | |
| | | realName: pickerFilters.realName?.trim() || undefined, |
| | | mobile: pickerFilters.mobile?.trim() || undefined, |
| | | status: 1, |
| | | excludeStaffGroupId: id, |
| | | }; |
| | | } |
| | | |
| | |
| | | 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) { |
| | |
| | | loadPicker(); |
| | | } |
| | | |
| | | function onPickerSizeChange(nextSize: number) { |
| | | pickerSize.value = nextSize; |
| | | pickerPage.value = 1; |
| | | loadPicker(); |
| | | } |
| | | |
| | | function clearPickerSelection() { |
| | | pickedMap.value = new Map(); |
| | | pickerTableRef.value?.clearSelection?.(); |
| | |
| | | } |
| | | |
| | | 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(); |