598 lines
21 KiB
Vue
598 lines
21 KiB
Vue
<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>
|