Files
OnboardXFront/src/views/biz/onboarding/entry-config/components/OnboardingTemplatePanel.vue
T
2026-08-27 11:09:33 +08:00

598 lines
21 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="template-panel">
<div class="toolbar">
<el-input v-model="query.keyword" clearable placeholder="搜索模板编码或名称" class="keyword-input"
@keyup.enter="loadTemplates" />
<el-select v-model="query.companyId" clearable filterable placeholder="全部公司" class="company-filter">
<el-option v-for="item in companies" :key="item.id" :label="item.companyName" :value="item.id" />
</el-select>
<el-button type="primary" @click="loadTemplates">查询</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
<div class="toolbar-spacer" />
<el-button type="primary" icon="Plus" @click="openCreate">新增入职模板</el-button>
</div>
<el-alert title="入职模板统一管理公司、岗位、合同文件与员工上传材料;员工入职档案自动采用标准《入职登记表》。" type="info" :closable="false" show-icon
class="tip" />
<div class="table-container">
<el-table v-loading="loading" :data="rows" border stripe height="100%">
<el-table-column prop="templateCode" label="模板编码" min-width="150" />
<el-table-column prop="templateName" label="模板名称" min-width="180" />
<el-table-column prop="companyName" label="所属公司" min-width="160" />
<el-table-column prop="departmentName" label="所属部门" min-width="150" />
<el-table-column label="适用岗位" min-width="220">
<template #default="scope">
<el-space wrap>
<el-tag v-for="name in scope.row.positionNames" :key="name" size="small">{{ name }}</el-tag>
</el-space>
</template>
</el-table-column>
<el-table-column label="配置内容" min-width="200">
<template #default="scope">
合同 {{ scope.row.contractCount || 0 }} 份 / 材料 {{ scope.row.materialCount || 0 }} 项
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="90" align="center">
<template #default="scope">
<el-tag :type="scope.row.status === 'enable' ? 'success' : 'info'">
{{ scope.row.status === "enable" ? "启用" : "停用" }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="150" fixed="right">
<template #default="scope">
<el-button link type="primary" @click="openEdit(scope.row)">编辑</el-button>
<el-button link type="danger" @click="removeTemplate(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div class="pagination">
<el-pagination v-model:current-page="query.pageNum" v-model:page-size="query.pageSize"
layout="total, sizes, prev, pager, next" :total="total" @current-change="loadTemplates"
@size-change="loadTemplates" />
</div>
<!-- 模板编辑大弹窗 -->
<el-dialog v-model="visible" :title="form.id ? '编辑入职模板' : '新增入职模板'" width="62%" top="3vh" destroy-on-close
append-to-body class="template-dialog">
<div class="template-dialog-body">
<el-form label-width="100px" label-position="top">
<!-- 1. 基本信息卡片 -->
<div class="form-block-card">
<div class="block-card-header">
<span class="block-title">基本信息</span>
<span class="block-subtitle">配置模板编码、名称、归属公司及适用岗位范围</span>
</div>
<el-row :gutter="20">
<el-col :xs="24" :sm="8">
<el-form-item label="模板编码" required>
<el-input v-model="form.templateCode" placeholder="如:FULLTIME_DEV" />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="8">
<el-form-item label="模板名称" required>
<el-input v-model="form.templateName" placeholder="如:全职技术人员入职模板" />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="8">
<el-form-item label="状态" required>
<el-radio-group v-model="form.status">
<el-radio value="enable">启用</el-radio>
<el-radio value="disable">停用</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="8">
<el-form-item label="所属公司" required>
<el-select v-model="form.companyId" filterable class="full-width" placeholder="请选择公司"
@change="onCompanyChange">
<el-option v-for="item in companies" :key="item.id" :label="item.companyName" :value="item.id" />
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="8">
<el-form-item label="所属部门" required>
<el-select v-model="form.departmentId" filterable class="full-width" placeholder="请先选择公司"
:disabled="!form.companyId" @change="onDepartmentChange">
<el-option v-for="item in departments" :key="item.id" :label="item.departmentName"
:value="item.id" />
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="8">
<el-form-item label="适用岗位" required>
<el-select v-model="form.positionIds" multiple filterable collapse-tags collapse-tags-tooltip
class="full-width" :disabled="!form.departmentId" placeholder="请选择适用的岗位(可多选)"
@change="onPositionSelectionChange">
<el-option v-for="item in positions" :key="item.id" :label="item.positionName" :value="item.id" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="模板说明">
<el-input v-model="form.description" type="textarea" :rows="2" placeholder="填写该入职模板的适用范围或注意事项" />
</el-form-item>
</el-col>
</el-row>
</div>
<!-- 2. 关联合同文件卡片 -->
<div class="form-block-card">
<div class="block-card-header flex-between">
<div>
<span class="block-title">关联合同文件</span>
<span class="block-subtitle">选择该岗位入职时需要员工确认与签署的合同模板及版本</span>
</div>
<el-button type="primary" plain icon="Plus" size="small" :disabled="!form.positionIds.length"
@click="addContract">
添加合同文件
</el-button>
</div>
<el-table :data="form.contracts" border empty-text="暂无关联合同,请点击右上角添加" class="mt-2">
<el-table-column label="合同模板" min-width="220">
<template #default="scope">
<el-select v-model="scope.row.contractTemplateId" filterable class="full-width" placeholder="选择合同模板"
@change="onContractTemplateChange(scope.row)">
<el-option v-for="item in contractTemplates" :key="item.id" :label="item.templateName"
:value="item.id" />
</el-select>
</template>
</el-table-column>
<el-table-column label="启用版本" min-width="160">
<template #default="scope">
<el-select v-model="scope.row.contractTemplateVersionId" class="full-width" placeholder="选择版本"
:disabled="!scope.row.contractTemplateId">
<el-option v-for="item in versionOptions[scope.row.contractTemplateId] || []" :key="item.id"
:label="item.versionNo" :value="item.id" />
</el-select>
</template>
</el-table-column>
<el-table-column label="必须确认" width="100" align="center">
<template #default="scope">
<el-switch v-model="scope.row.required" />
</template>
</el-table-column>
<el-table-column label="打印份数" width="130" align="center">
<template #default="scope">
<el-input-number v-model="scope.row.printCopies" :min="1" :max="10" controls-position="right"
style="width: 100px;" />
</template>
</el-table-column>
<el-table-column label="排序" width="130" align="center">
<template #default="scope">
<el-input-number v-model="scope.row.sortCode" :min="0" controls-position="right"
style="width: 100px;" />
</template>
</el-table-column>
<el-table-column label="操作" width="90" align="center" fixed="right">
<template #default="scope">
<el-button link type="danger" icon="Delete" style="white-space: nowrap;"
@click="form.contracts.splice(scope.$index, 1)">
移除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- 3. 员工上传材料卡片 -->
<div class="form-block-card">
<div class="block-card-header">
<span class="block-title">员工上传材料要求</span>
<span class="block-subtitle">勾选入职时要求候选人必须或可选上传的证件材料(如身份证、毕业证、体检表等)</span>
</div>
<el-table :data="materialTypes" border stripe height="240px" class="mt-2">
<el-table-column label="选择要求" width="85" align="center">
<template #default="scope">
<el-checkbox :model-value="hasMaterial(scope.row.id)"
@change="value => toggleMaterial(scope.row.id, Boolean(value))" />
</template>
</el-table-column>
<el-table-column prop="materialCode" label="材料编码" min-width="160" />
<el-table-column prop="materialName" label="材料名称" min-width="220" />
<el-table-column prop="maxFileCount" label="最多文件" width="100" align="center" />
<el-table-column label="必传" width="100" align="center">
<template #default="scope">
<el-switch v-if="hasMaterial(scope.row.id)" v-model="materialConfig(scope.row.id)!.required" />
<span v-else class="text-gray">-</span>
</template>
</el-table-column>
<el-table-column label="排序" width="130" align="center">
<template #default="scope">
<el-input-number v-if="hasMaterial(scope.row.id)" v-model="materialConfig(scope.row.id)!.sortCode"
:min="0" controls-position="right" style="width: 100px;" />
<span v-else class="text-gray">-</span>
</template>
</el-table-column>
</el-table>
</div>
</el-form>
</div>
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" :loading="saving" @click="save">保存入职模板</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { onMounted, reactive, ref } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import {
OnboardingCompany,
OnboardingContractTemplate,
OnboardingContractVersion,
OnboardingDepartmentOption,
OnboardingMaterialType,
OnboardingPosition,
OnboardingTemplate,
onboardingTaskApi
} from "@/api/modules/biz/onboarding";
const loading = ref(false);
const saving = ref(false);
const visible = ref(false);
const rows = ref<OnboardingTemplate[]>([]);
const total = ref(0);
const companies = ref<OnboardingCompany[]>([]);
const departments = ref<OnboardingDepartmentOption[]>([]);
const positions = ref<OnboardingPosition[]>([]);
const contractTemplates = ref<OnboardingContractTemplate[]>([]);
const materialTypes = ref<OnboardingMaterialType[]>([]);
const versionOptions = reactive<Record<number, OnboardingContractVersion[]>>({});
const query = reactive({
keyword: "",
companyId: undefined as number | undefined,
pageNum: 1,
pageSize: 20
});
const form = reactive<OnboardingTemplate>({
id: 0,
templateCode: "",
templateName: "",
companyId: 0,
departmentId: 0,
description: "",
status: "enable",
positionIds: [],
contracts: [],
formFields: [],
materials: []
});
async function loadBaseData() {
const [companyRes, materialRes] = await Promise.all([
onboardingTaskApi.companyPage({ pageNum: 1, pageSize: 100, status: "enable" }).catch(() => ({ data: { list: [] } })),
onboardingTaskApi.listMaterialTypes().catch(() => ({ data: [] }))
]);
companies.value = companyRes.data?.list || [];
materialTypes.value = materialRes.data || [];
}
async function loadTemplates() {
loading.value = true;
try {
const res = await onboardingTaskApi.onboardingTemplatePage(query);
rows.value = res.data?.list || [];
total.value = res.data?.total || 0;
} finally {
loading.value = false;
}
}
function resetQuery() {
query.keyword = "";
query.companyId = undefined;
query.pageNum = 1;
loadTemplates();
}
async function onCompanyChange(companyId: number) {
form.departmentId = undefined as any;
form.positionIds = [];
form.contracts = [];
departments.value = [];
contractTemplates.value = [];
positions.value = [];
if (!companyId) {
return;
}
const res = await onboardingTaskApi.listDepartments(companyId);
departments.value = res.data || [];
}
async function onDepartmentChange(departmentId: number) {
form.positionIds = [];
form.contracts = [];
positions.value = [];
contractTemplates.value = [];
if (!departmentId) return;
const res = await onboardingTaskApi.listPositionsByDepartment(departmentId);
positions.value = res.data || [];
}
async function onPositionSelectionChange() {
await loadAvailableContractTemplates();
}
async function loadAvailableContractTemplates() {
if (!form.companyId || !form.departmentId || !form.positionIds.length) {
contractTemplates.value = [];
form.contracts = [];
return;
}
const res = await onboardingTaskApi.listAvailableContractTemplates({
companyId: form.companyId,
departmentId: form.departmentId,
positionIds: form.positionIds
});
contractTemplates.value = res.data || [];
const availableIds = new Set(contractTemplates.value.map(item => item.id));
form.contracts = form.contracts.filter(item => availableIds.has(item.contractTemplateId));
}
async function onContractTemplateChange(row: any) {
row.contractTemplateVersionId = undefined;
if (!row.contractTemplateId) return;
await loadContractVersions(row.contractTemplateId);
}
async function loadContractVersions(templateId: number) {
if (versionOptions[templateId]) return;
const res = await onboardingTaskApi.versionPage({ templateId, pageNum: 1, pageSize: 100 });
versionOptions[templateId] = (res.data?.list || []).filter((it: any) => it.versionStatus === "enabled");
}
function addContract() {
if (!form.positionIds.length) {
ElMessage.warning("请先选择适用岗位");
return;
}
if (!contractTemplates.value.length) {
ElMessage.warning("当前公司、部门和岗位没有可用的合同模板");
return;
}
form.contracts.push({
contractTemplateId: undefined as any,
contractTemplateVersionId: undefined as any,
required: true,
printCopies: 1,
sortCode: (form.contracts.length + 1) * 10
});
}
function hasMaterial(materialTypeId: number) {
return form.materials.some(item => item.materialTypeId === materialTypeId);
}
function materialConfig(materialTypeId: number) {
return form.materials.find(item => item.materialTypeId === materialTypeId);
}
function toggleMaterial(materialTypeId: number, checked: boolean) {
if (checked) {
if (!hasMaterial(materialTypeId)) {
form.materials.push({ materialTypeId, required: true, sortCode: (form.materials.length + 1) * 10 });
}
} else {
form.materials = form.materials.filter(item => item.materialTypeId !== materialTypeId);
}
}
async function openCreate() {
await loadBaseData();
Object.assign(form, {
id: undefined,
templateCode: "",
templateName: "",
companyId: undefined as any,
departmentId: undefined as any,
description: "",
status: "enable",
positionIds: [],
contracts: [],
formFields: [],
materials: []
});
departments.value = [];
positions.value = [];
contractTemplates.value = [];
visible.value = true;
}
async function openEdit(row: OnboardingTemplate) {
await loadBaseData();
const res = await onboardingTaskApi.onboardingTemplateDetail({ id: row.id! });
const detail = res.data;
Object.assign(form, {
id: detail.id,
templateCode: detail.templateCode,
templateName: detail.templateName,
companyId: detail.companyId,
departmentId: detail.departmentId,
description: detail.description,
status: detail.status,
positionIds: detail.positionIds || [],
contracts: (detail.contracts || []).map((item: any) => ({
contractTemplateId: item.contractTemplateId,
contractTemplateVersionId: item.contractTemplateVersionId,
required: item.required,
printCopies: item.printCopies || 1,
sortCode: item.sortCode || 10
})),
formFields: [],
materials: (detail.materials || []).map((item: any) => ({
materialTypeId: item.materialTypeId,
required: item.required,
sortCode: item.sortCode || 10
}))
});
if (form.companyId) {
const departmentRes = await onboardingTaskApi.listDepartments(form.companyId);
departments.value = departmentRes.data || [];
}
if (form.departmentId) {
const positionRes = await onboardingTaskApi.listPositionsByDepartment(form.departmentId);
positions.value = positionRes.data || [];
}
await loadAvailableContractTemplates();
for (const contract of form.contracts) {
if (contract.contractTemplateId) {
await loadContractVersions(contract.contractTemplateId);
}
}
visible.value = true;
}
async function save() {
if (
!form.templateCode
|| !form.templateName
|| !form.companyId
|| !form.departmentId
|| !form.positionIds.length
) {
return ElMessage.warning("请完整填写模板编码、名称、公司、部门及适用岗位");
}
if (!form.contracts.length || form.contracts.some(item => !item.contractTemplateId || !item.contractTemplateVersionId)) {
return ElMessage.warning("请完整选择至少一份合同模板及版本");
}
saving.value = true;
try {
await onboardingTaskApi.saveOnboardingTemplate(form);
ElMessage.success("入职模板保存成功");
visible.value = false;
await loadTemplates();
} finally {
saving.value = false;
}
}
async function removeTemplate(row: OnboardingTemplate) {
await ElMessageBox.confirm(`确认删除入职模板“${row.templateName}”吗?`, "删除确认", { type: "warning" });
await onboardingTaskApi.deleteOnboardingTemplate([row.id!]);
ElMessage.success("删除成功");
query.pageNum = 1;
await loadTemplates();
}
onMounted(async () => {
await Promise.all([loadBaseData(), loadTemplates()]);
});
</script>
<style scoped>
.template-panel {
height: 100%;
display: flex;
flex-direction: column;
box-sizing: border-box;
overflow: hidden;
}
.toolbar {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
flex-shrink: 0;
}
.keyword-input {
width: 220px;
}
.company-filter {
width: 180px;
}
.toolbar-spacer {
flex: 1;
}
.tip {
margin-bottom: 10px;
flex-shrink: 0;
}
.table-container {
flex: 1;
height: 0;
overflow: hidden;
}
.pagination {
margin-top: 12px;
display: flex;
justify-content: flex-end;
flex-shrink: 0;
}
.full-width {
width: 100%;
}
.mt-2 {
margin-top: 8px;
}
.text-gray {
color: #94a3b8;
}
/* 弹窗内部卡片样式 */
.template-dialog-body {
max-height: 72vh;
overflow-y: auto;
padding-right: 6px;
}
.form-block-card {
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 16px 18px;
margin-bottom: 16px;
}
.block-card-header {
margin-bottom: 14px;
}
.flex-between {
display: flex;
align-items: center;
justify-content: space-between;
}
.block-title {
font-size: 15px;
font-weight: 700;
color: #0f172a;
margin-right: 10px;
}
.block-subtitle {
font-size: 12px;
color: #64748b;
}
</style>