<template>
|
<div class="page">
|
<el-form :inline="true" class="search-form" @submit.prevent="doSearch">
|
<el-form-item label="属地单位">
|
<el-input v-model="filters.unitName" placeholder="名称模糊" clearable style="width: 180px" />
|
</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-form-item>
|
</el-form>
|
|
<el-table :data="rows" v-loading="loading" stripe border>
|
<el-table-column label="序号" width="64" type="index" :index="indexMethod" />
|
<el-table-column prop="unitName" label="单位名称" min-width="140" show-overflow-tooltip />
|
<el-table-column prop="approverSummary" label="审批人员" min-width="160" show-overflow-tooltip />
|
<el-table-column prop="remark" label="备注" min-width="120" show-overflow-tooltip />
|
<el-table-column label="操作" width="140" fixed="right">
|
<template #default="{ row }">
|
<div class="table-ops">
|
<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>
|
|
<el-pagination
|
class="pager"
|
background
|
layout="prev, pager, next"
|
:total="total"
|
:page-size="size"
|
v-model:current-page="page"
|
@current-change="load"
|
/>
|
|
<el-dialog v-model="dlg" :title="dlgTitle" width="720px" destroy-on-close>
|
<el-form :model="form" label-width="100px">
|
<el-form-item label="单位名称" required>
|
<el-input v-model="form.unitName" maxlength="128" placeholder="必填" show-word-limit />
|
</el-form-item>
|
<el-form-item label="备注">
|
<el-input v-model="form.remark" type="textarea" :rows="2" maxlength="512" placeholder="非必填" show-word-limit />
|
</el-form-item>
|
<el-form-item label="审批人员" required>
|
<el-radio-group v-model="form.approverType" @change="onApproverTypeChange">
|
<el-radio-button :value="1">部门</el-radio-button>
|
<el-radio-button :value="2">人员分组</el-radio-button>
|
<el-radio-button :value="3">人员</el-radio-button>
|
</el-radio-group>
|
</el-form-item>
|
|
<el-form-item v-if="form.approverType === 1" label="审批部门" required>
|
<el-cascader
|
v-model="form.deptIds"
|
class="fullw"
|
:options="deptCascaderOptions"
|
:props="deptCascaderProps"
|
collapse-tags
|
collapse-tags-tooltip
|
clearable
|
filterable
|
placeholder="五级部门树,可多选"
|
/>
|
</el-form-item>
|
|
<el-form-item v-if="form.approverType === 2" label="人员分组" required>
|
<el-select
|
v-model="form.approverGroupId"
|
placeholder="请选择分组"
|
class="fullw"
|
filterable
|
clearable
|
>
|
<el-option v-for="g in groupOptions" :key="g.id" :label="g.groupName" :value="g.id" />
|
</el-select>
|
</el-form-item>
|
|
<template v-if="form.approverType === 3">
|
<el-form-item label=" ">
|
<div class="member-actions">
|
<el-button type="primary" @click="openPicker">选择人员</el-button>
|
<el-button @click="clearStaffSelection">清空</el-button>
|
</div>
|
</el-form-item>
|
<el-form-item label="已选人员">
|
<el-table :data="memberList" stripe border max-height="280">
|
<el-table-column label="序号" width="56" type="index" />
|
<el-table-column prop="realName" label="人员" width="120" show-overflow-tooltip />
|
<el-table-column prop="deptName" label="所属部门" min-width="140" show-overflow-tooltip />
|
<el-table-column label="操作" width="88" fixed="right">
|
<template #default="{ row }">
|
<div class="table-ops">
|
<el-button type="danger" link @click="removeMember(row.id)">移除</el-button>
|
</div>
|
</template>
|
</el-table-column>
|
</el-table>
|
</el-form-item>
|
</template>
|
</el-form>
|
|
<template #footer>
|
<el-button @click="dlg = false">取消</el-button>
|
<el-button type="primary" :loading="saving" @click="submitForm">保存</el-button>
|
</template>
|
</el-dialog>
|
|
<el-dialog v-model="picker" title="选择人员" width="960px" destroy-on-close>
|
<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="deptTreeSelectOptions"
|
: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"
|
v-loading="pickerLoading"
|
stripe
|
border
|
@selection-change="onPickerSelectionChange"
|
>
|
<el-table-column type="selection" width="48" />
|
<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="clearPickerSelection">取消选择</el-button>
|
<el-button type="primary" @click="savePicker">保存</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 http from "../api/http";
|
|
type UnitRow = {
|
id: number;
|
uuid: string;
|
unitName: string;
|
approverSummary?: string;
|
remark?: string;
|
createTime?: string;
|
};
|
|
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 GroupOpt = { id: number; groupName: string };
|
|
const loading = ref(false);
|
const saving = ref(false);
|
const rows = ref<UnitRow[]>([]);
|
const page = ref(1);
|
const size = ref(10);
|
const total = ref(0);
|
|
const filters = reactive({
|
unitName: "",
|
});
|
|
function indexMethod(i: number) {
|
return (page.value - 1) * size.value + i + 1;
|
}
|
|
function buildParams() {
|
return {
|
page: page.value,
|
size: size.value,
|
unitName: filters.unitName?.trim() || undefined,
|
};
|
}
|
|
async function load() {
|
loading.value = true;
|
try {
|
const { data } = await http.get("/sys/local-units", { params: buildParams() });
|
rows.value = (data.result || []) as UnitRow[];
|
total.value = Number(data.total) || 0;
|
} catch (e: unknown) {
|
ElMessage.error(e instanceof Error ? e.message : "加载失败");
|
} finally {
|
loading.value = false;
|
}
|
}
|
|
function doSearch() {
|
page.value = 1;
|
load();
|
}
|
|
function resetSearch() {
|
filters.unitName = "";
|
page.value = 1;
|
load();
|
}
|
|
const dlg = ref(false);
|
const dlgTitle = ref("添加属地单位");
|
const editing = ref<{ id: number; uuid: string } | null>(null);
|
|
const form = reactive({
|
uuid: "",
|
unitName: "",
|
remark: "",
|
approverType: 1 as 1 | 2 | 3,
|
deptIds: [] as number[],
|
approverGroupId: undefined as number | undefined,
|
});
|
|
const membersMap = ref(new Map<number, StaffRow>());
|
const memberList = computed(() => Array.from(membersMap.value.values()));
|
|
const deptCascaderOptions = ref<DeptTree[]>([]);
|
const deptTreeSelectOptions = ref<DeptTree[]>([]);
|
|
const deptCascaderProps = {
|
multiple: true,
|
checkStrictly: true,
|
emitPath: false,
|
value: "id",
|
label: "label",
|
children: "children",
|
};
|
|
function mapDeptCascader(nodes: Record<string, unknown>[]): DeptTree[] {
|
return nodes.map((n) => ({
|
id: n.id as number,
|
label: (n.status === 0 ? `${n.deptName}(禁用)` : String(n.deptName)) as string,
|
children: n.children ? mapDeptCascader(n.children as Record<string, unknown>[]) : undefined,
|
}));
|
}
|
|
async function loadDeptTree() {
|
const { data } = await http.get("/sys/dept-manage/tree");
|
const raw = (data.result || []) as Record<string, unknown>[];
|
const mapped = mapDeptCascader(raw);
|
deptCascaderOptions.value = mapped;
|
deptTreeSelectOptions.value = mapped;
|
}
|
|
const groupOptions = ref<GroupOpt[]>([]);
|
|
async function loadGroupOptions() {
|
const { data } = await http.get("/sys/staff-groups", { params: { page: 1, size: 500 } });
|
groupOptions.value = (data.result || []) as GroupOpt[];
|
}
|
|
function onApproverTypeChange() {
|
form.deptIds = [];
|
form.approverGroupId = undefined;
|
membersMap.value = new Map();
|
}
|
|
function openCreate() {
|
editing.value = null;
|
dlgTitle.value = "添加属地单位";
|
form.uuid = "";
|
form.unitName = "";
|
form.remark = "";
|
form.approverType = 1;
|
form.deptIds = [];
|
form.approverGroupId = undefined;
|
membersMap.value = new Map();
|
dlg.value = true;
|
}
|
|
async function openEdit(r: UnitRow) {
|
editing.value = { id: r.id, uuid: r.uuid };
|
dlgTitle.value = "编辑属地单位";
|
try {
|
const { data } = await http.get(`/sys/local-units/${r.id}`, { params: { uuid: r.uuid } });
|
const d = data.result as {
|
uuid: string;
|
unitName: string;
|
remark?: string;
|
approverType: 1 | 2 | 3;
|
approverDeptIds?: number[];
|
approverGroupId?: number | null;
|
members: { mpUserId: number; uuid: string; realName?: string; deptName?: string }[];
|
};
|
form.uuid = d.uuid;
|
form.unitName = d.unitName || "";
|
form.remark = d.remark || "";
|
form.approverType = d.approverType ?? 1;
|
form.deptIds = [...(d.approverDeptIds || [])];
|
form.approverGroupId = d.approverGroupId ?? undefined;
|
const map = new Map<number, StaffRow>();
|
for (const m of d.members || []) {
|
map.set(m.mpUserId, {
|
id: m.mpUserId,
|
uuid: m.uuid,
|
realName: m.realName,
|
staffNo: "",
|
mobileMasked: "",
|
deptName: m.deptName,
|
});
|
}
|
membersMap.value = map;
|
dlg.value = true;
|
} catch (e: unknown) {
|
ElMessage.error(e instanceof Error ? e.message : "加载详情失败");
|
}
|
}
|
|
function clearStaffSelection() {
|
membersMap.value = new Map();
|
}
|
|
function removeMember(id: number) {
|
membersMap.value.delete(id);
|
}
|
|
function buildSubmitBody() {
|
const t = form.approverType;
|
const base: Record<string, unknown> = {
|
unitName: form.unitName.trim(),
|
remark: form.remark?.trim() || undefined,
|
approverType: t,
|
};
|
if (t === 1) {
|
base.approverDeptIds = form.deptIds?.length ? [...form.deptIds] : [];
|
} else if (t === 2) {
|
base.approverGroupId = form.approverGroupId;
|
} else {
|
base.memberIds = Array.from(membersMap.value.keys());
|
}
|
return base;
|
}
|
|
function validateForm(): boolean {
|
if (!form.unitName.trim()) {
|
ElMessage.warning("请填写单位名称");
|
return false;
|
}
|
if (form.approverType === 1) {
|
if (!form.deptIds?.length) {
|
ElMessage.warning("请选择审批部门");
|
return false;
|
}
|
} else if (form.approverType === 2) {
|
if (form.approverGroupId == null) {
|
ElMessage.warning("请选择人员分组");
|
return false;
|
}
|
} else {
|
if (membersMap.value.size === 0) {
|
ElMessage.warning("请选择审批人员");
|
return false;
|
}
|
}
|
return true;
|
}
|
|
async function submitForm() {
|
if (!validateForm()) return;
|
saving.value = true;
|
try {
|
const body = buildSubmitBody();
|
if (!editing.value) {
|
await http.post("/sys/local-units", body);
|
ElMessage.success("已创建");
|
} else {
|
await http.put(`/sys/local-units/${editing.value.id}`, { uuid: form.uuid, ...body });
|
ElMessage.success("已保存");
|
}
|
dlg.value = false;
|
load();
|
} catch (e: unknown) {
|
ElMessage.error(e instanceof Error ? e.message : "保存失败");
|
} finally {
|
saving.value = false;
|
}
|
}
|
|
function confirmDelete(r: UnitRow) {
|
ElMessageBox.confirm(`确认删除属地单位「${r.unitName}」?`, "删除确认", { type: "warning" })
|
.then(async () => {
|
try {
|
await http.delete(`/sys/local-units/${r.id}`, { params: { uuid: r.uuid } });
|
ElMessage.success("已删除");
|
load();
|
} catch (e: unknown) {
|
ElMessage.error(e instanceof Error ? e.message : "删除失败");
|
}
|
})
|
.catch(() => {});
|
}
|
|
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 pickedMap = ref(new Map<number, StaffRow>());
|
|
function openPicker() {
|
picker.value = true;
|
pickedMap.value = new Map(membersMap.value);
|
pickerPage.value = 1;
|
loadPicker();
|
}
|
|
function pickerSearch() {
|
pickerPage.value = 1;
|
loadPicker();
|
}
|
|
function pickerReset() {
|
pickerFilters.realName = "";
|
pickerFilters.mobile = "";
|
pickerFilters.deptId = undefined;
|
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();
|
for (const r of pickerRows.value) {
|
if (pickedMap.value.has(r.id)) {
|
pickerTableRef.value?.toggleRowSelection?.(r, true);
|
}
|
}
|
} catch (e: unknown) {
|
ElMessage.error(e instanceof Error ? e.message : "加载失败");
|
} finally {
|
pickerLoading.value = false;
|
}
|
}
|
|
function onPickerSelectionChange(cur: StaffRow[]) {
|
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 clearPickerSelection() {
|
pickedMap.value = new Map();
|
pickerTableRef.value?.clearSelection?.();
|
}
|
|
function savePicker() {
|
membersMap.value = new Map(pickedMap.value);
|
picker.value = false;
|
}
|
|
onMounted(async () => {
|
try {
|
await loadDeptTree();
|
} catch {
|
// ignore
|
}
|
try {
|
await loadGroupOptions();
|
} catch {
|
// ignore
|
}
|
load();
|
});
|
</script>
|
|
<style scoped>
|
.page {
|
padding: 0;
|
}
|
.search-form {
|
margin-bottom: 12px;
|
display: flex;
|
flex-wrap: wrap;
|
align-items: center;
|
}
|
.push-right {
|
margin-left: auto;
|
}
|
.pager {
|
margin-top: 12px;
|
justify-content: flex-end;
|
}
|
.member-actions {
|
display: flex;
|
gap: 10px;
|
}
|
.picker-footer {
|
display: flex;
|
justify-content: flex-end;
|
gap: 10px;
|
}
|
.fullw {
|
width: 100%;
|
}
|
</style>
|