<template>
|
<div class="detail">
|
<el-card class="hdr" shadow="never">
|
<div class="hdr-row">
|
<div class="title">{{ detail.groupName || "-" }}</div>
|
<div class="actions">
|
<el-button type="primary" @click="openAddMembers">添加成员</el-button>
|
</div>
|
</div>
|
<div class="meta">
|
<div>负责人:{{ detail.leaderName || "-" }} {{ detail.leaderMobile ? `(${detail.leaderMobile})` : "" }}</div>
|
<div>人员数:{{ detail.memberCount ?? 0 }}</div>
|
<div>创建时间:{{ formatTime(detail.createTime) }}</div>
|
</div>
|
</el-card>
|
|
<el-card shadow="never">
|
<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="primary" link disabled>删除</el-button>
|
</span>
|
</el-tooltip>
|
<el-button
|
v-else
|
type="primary"
|
link
|
:loading="removingId === row.id"
|
@click="confirmRemoveMember(row)"
|
>
|
删除
|
</el-button>
|
</div>
|
</template>
|
</el-table-column>
|
</el-table>
|
</el-card>
|
|
<el-dialog v-model="picker" title="添加成员" width="960px" destroy-on-close>
|
<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-form-item>
|
<el-form-item label="联系电话">
|
<el-input v-model="pickerFilters.mobile" placeholder="请输入联系电话" clearable style="width: 140px" />
|
</el-form-item>
|
<el-form-item label="部门">
|
<el-tree-select
|
v-model="pickerFilters.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-item>
|
<el-button type="primary" native-type="submit">搜索</el-button>
|
<el-button @click="pickerReset">重置</el-button>
|
</el-form-item>
|
</el-form>
|
</div>
|
|
<el-table
|
ref="pickerTableRef"
|
:data="pickerRows"
|
row-key="id"
|
v-loading="pickerLoading"
|
stripe
|
border
|
@selection-change="onPickerSelectionChange"
|
>
|
<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-table-column prop="deptName" label="所属部门" min-width="160" show-overflow-tooltip />
|
</el-table>
|
|
<el-pagination
|
class="pager"
|
background
|
layout="prev, pager, next"
|
:total="pickerTotal"
|
:page-size="pickerSize"
|
v-model:current-page="pickerPage"
|
@current-change="loadPicker"
|
/>
|
|
<template #footer>
|
<div class="picker-footer">
|
<el-button @click="closePicker">取消</el-button>
|
<el-button @click="clearPickerSelection">清空已选</el-button>
|
<el-button type="primary" :loading="saving" @click="submitAppendMembers">追加成员</el-button>
|
</div>
|
</template>
|
</el-dialog>
|
</div>
|
</template>
|
|
<script setup lang="ts">
|
import { computed, nextTick, onMounted, reactive, ref } from "vue";
|
import { ElMessage, ElMessageBox } from "element-plus";
|
import { useRoute } from "vue-router";
|
import http from "../api/http";
|
|
type DeptTree = { id: number; label: string; children?: DeptTree[] };
|
type StaffRow = { id: number; uuid: string; realName?: string; staffNo?: string; mobileMasked?: string; deptId?: number; deptName?: string };
|
|
const route = useRoute();
|
|
const id = Number(route.params.id);
|
const uuid = String(route.query.uuid || "");
|
|
const loading = ref(false);
|
const saving = ref(false);
|
const removingId = ref<number | null>(null);
|
|
const detail = ref({
|
id: id,
|
uuid: uuid,
|
groupName: "",
|
leaderMpUserId: undefined as number | undefined,
|
leaderName: "",
|
leaderMobile: "",
|
memberCount: 0,
|
createTime: "",
|
members: [] as { id: number; uuid: string; realName: string; staffNo?: string; mobileMasked?: string; deptId?: number; deptName?: string }[],
|
});
|
|
function formatTime(s: string) {
|
if (!s) return "";
|
return s.replace("T", " ").slice(0, 19);
|
}
|
|
async function loadDetail() {
|
if (!uuid) return;
|
loading.value = true;
|
try {
|
const { data } = await http.get(`/sys/staff-groups/${id}`, { params: { uuid } });
|
detail.value = data.result;
|
} catch (e: unknown) {
|
ElMessage.error(e instanceof Error ? e.message : "加载失败");
|
} finally {
|
loading.value = false;
|
}
|
}
|
|
function confirmRemoveMember(row: { id: number; realName?: string }) {
|
if (row.id === detail.value.leaderMpUserId) return;
|
const name = row.realName || `成员#${row.id}`;
|
ElMessageBox.confirm(`确认将「${name}」从本分组移除?`, "移除成员", { type: "warning" })
|
.then(async () => {
|
removingId.value = row.id;
|
try {
|
await http.delete(`/sys/staff-groups/${id}/members/${row.id}`, { params: { uuid } });
|
ElMessage.success("已移除");
|
await loadDetail();
|
} catch (e: unknown) {
|
ElMessage.error(e instanceof Error ? e.message : "移除失败");
|
} finally {
|
removingId.value = null;
|
}
|
})
|
.catch(() => {});
|
}
|
|
// picker
|
const picker = ref(false);
|
const pickerLoading = ref(false);
|
const pickerRows = ref<StaffRow[]>([]);
|
const pickerPage = ref(1);
|
const pickerSize = ref(10);
|
const pickerTotal = ref(0);
|
const pickerTableRef = ref();
|
|
const pickerFilters = reactive({
|
realName: "",
|
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) => ({
|
id: n.id as number,
|
label: String(n.deptName),
|
children: n.children ? mapDeptSelect(n.children as Record<string, unknown>[]) : undefined,
|
}));
|
}
|
|
async function loadDeptTree() {
|
const { data } = await http.get("/sys/dept-manage/tree");
|
deptOptions.value = mapDeptSelect((data.result || []) as Record<string, unknown>[]);
|
}
|
|
function openAddMembers() {
|
picker.value = true;
|
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();
|
}
|
|
function pickerParams() {
|
return {
|
page: pickerPage.value,
|
size: pickerSize.value,
|
deptId: pickerFilters.deptId,
|
realName: pickerFilters.realName?.trim() || undefined,
|
mobile: pickerFilters.mobile?.trim() || undefined,
|
status: 1,
|
};
|
}
|
|
async function loadPicker() {
|
pickerLoading.value = true;
|
try {
|
const { data } = await http.get("/sys/staff", { params: pickerParams() });
|
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) {
|
if (!selectedOnPage.has(rid)) {
|
pickedMap.value.delete(rid);
|
}
|
}
|
for (const r of cur) {
|
pickedMap.value.set(r.id, r);
|
}
|
}
|
|
function pickerSearch() {
|
pickerPage.value = 1;
|
loadPicker();
|
}
|
|
function pickerReset() {
|
pickerFilters.realName = "";
|
pickerFilters.mobile = "";
|
pickerFilters.deptId = undefined;
|
pickerPage.value = 1;
|
loadPicker();
|
}
|
|
function clearPickerSelection() {
|
pickedMap.value = new Map();
|
pickerTableRef.value?.clearSelection?.();
|
}
|
|
function closePicker() {
|
pickedMap.value = new Map();
|
picker.value = false;
|
}
|
|
async function submitAppendMembers() {
|
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("没有可追加的新成员");
|
return;
|
}
|
saving.value = true;
|
try {
|
await http.post(`/sys/staff-groups/${id}/members`, { uuid, memberIds });
|
ElMessage.success("已追加成员");
|
picker.value = false;
|
pickedMap.value = new Map();
|
await loadDetail();
|
} catch (e: unknown) {
|
ElMessage.error(e instanceof Error ? e.message : "追加失败");
|
} finally {
|
saving.value = false;
|
}
|
}
|
|
onMounted(async () => {
|
await loadDeptTree();
|
loadDetail();
|
});
|
</script>
|
|
<style scoped>
|
.hdr {
|
margin-bottom: 12px;
|
}
|
.hdr-row {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
gap: 12px;
|
}
|
.title {
|
font-size: 16px;
|
font-weight: 600;
|
}
|
.meta {
|
margin-top: 8px;
|
display: grid;
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
gap: 6px 12px;
|
color: #606266;
|
font-size: 13px;
|
}
|
.picker-hint {
|
margin: 0 0 12px;
|
font-size: 13px;
|
color: #909399;
|
}
|
.pager {
|
margin-top: 12px;
|
justify-content: flex-end;
|
}
|
.picker-footer {
|
display: flex;
|
justify-content: flex-end;
|
gap: 10px;
|
}
|
.btn-wrap {
|
display: inline-block;
|
}
|
</style>
|