814 lines
28 KiB
Vue
814 lines
28 KiB
Vue
<template>
|
||
<div class="template-panel">
|
||
<div class="toolbar">
|
||
<el-input
|
||
v-model="query.keyword"
|
||
clearable
|
||
placeholder="搜索模板编码或名称"
|
||
class="keyword-input"
|
||
@keyup.enter="loadTemplates"
|
||
@clear="loadTemplates"
|
||
/>
|
||
<el-select
|
||
v-model="query.companyId"
|
||
clearable
|
||
filterable
|
||
placeholder="全部公司"
|
||
class="company-filter"
|
||
@change="onQueryCompanyChange"
|
||
>
|
||
<el-option v-for="item in companies" :key="item.id" :label="item.companyName" :value="item.id" />
|
||
</el-select>
|
||
<el-select
|
||
v-model="query.departmentId"
|
||
clearable
|
||
filterable
|
||
:placeholder="query.companyId ? '全部部门' : '请先选择所属公司'"
|
||
:disabled="!query.companyId"
|
||
class="department-filter"
|
||
@change="onQueryDepartmentChange"
|
||
>
|
||
<el-option v-for="item in queryDepartments" :key="item.id" :label="item.departmentName" :value="item.id" />
|
||
</el-select>
|
||
<el-select
|
||
v-model="query.positionId"
|
||
clearable
|
||
filterable
|
||
:placeholder="query.departmentId ? '全部岗位' : '请先选择所属部门'"
|
||
:disabled="!query.departmentId"
|
||
class="position-filter"
|
||
@change="loadTemplates"
|
||
>
|
||
<el-option v-for="item in queryPositions" :key="item.id" :label="item.positionName" :value="item.id" />
|
||
</el-select>
|
||
<el-select
|
||
v-model="query.status"
|
||
clearable
|
||
placeholder="模板状态"
|
||
class="status-filter"
|
||
@change="loadTemplates"
|
||
>
|
||
<el-option label="全部状态" value="" />
|
||
<el-option label="启用" value="enable" />
|
||
<el-option label="停用" value="disable" />
|
||
</el-select>
|
||
<el-button type="primary" icon="Search" @click="loadTemplates">查询</el-button>
|
||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||
<div class="toolbar-spacer" />
|
||
<el-button v-auth="'onboardingEntryConfigAdd'" 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
|
||
v-auth="'onboardingEntryConfigEdit'"
|
||
link
|
||
type="primary"
|
||
:disabled="!canManage(scope.row)"
|
||
:title="disabledReason(scope.row)"
|
||
@click="openEdit(scope.row)"
|
||
>编辑</el-button>
|
||
<el-button
|
||
v-auth="'onboardingEntryConfigDelete'"
|
||
link
|
||
type="danger"
|
||
:disabled="!canManage(scope.row)"
|
||
:title="disabledReason(scope.row)"
|
||
@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"
|
||
maxlength="100"
|
||
show-word-limit
|
||
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="enabledMaterialTypes" border stripe height="240px" class="mt-2">
|
||
<el-table-column label="选择要求" width="95" align="center">
|
||
<template #header>
|
||
<el-checkbox
|
||
:model-value="isAllMaterialsSelected"
|
||
:indeterminate="isMaterialsIndeterminate"
|
||
:disabled="!enabledMaterialTypes.length"
|
||
@change="handleSelectAllMaterials"
|
||
>
|
||
全选
|
||
</el-checkbox>
|
||
</template>
|
||
<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 { computed, onMounted, reactive, ref } from "vue";
|
||
import { useOnboardingScopeOptions } from "@/hooks/useOnboardingScopeOptions";
|
||
import { ElMessage, ElMessageBox } from "element-plus";
|
||
import dayjs from "dayjs";
|
||
import {
|
||
OnboardingCompanyOption,
|
||
OnboardingContractTemplate,
|
||
OnboardingContractVersion,
|
||
OnboardingDepartmentOption,
|
||
OnboardingMaterialType,
|
||
OnboardingPosition,
|
||
OnboardingTemplate,
|
||
onboardingTaskApi
|
||
} from "@/api/modules/biz/onboarding";
|
||
import { isMessageBoxConfirmed } from "@/utils/messageBox";
|
||
import { useOnboardingPermission } from "@/utils/onboardingPermission";
|
||
|
||
const { canManage, disabledReason } = useOnboardingPermission("onboardingEntryConfigManageAll");
|
||
|
||
const loading = ref(false);
|
||
const saving = ref(false);
|
||
const visible = ref(false);
|
||
const rows = ref<OnboardingTemplate[]>([]);
|
||
const total = ref(0);
|
||
|
||
const props = defineProps<{ loadMaterials: () => Promise<OnboardingMaterialType[]> }>();
|
||
const companies = ref<OnboardingCompanyOption[]>([]);
|
||
const departments = ref<OnboardingDepartmentOption[]>([]);
|
||
const positions = ref<OnboardingPosition[]>([]);
|
||
const contractTemplates = ref<OnboardingContractTemplate[]>([]);
|
||
const materialTypes = ref<OnboardingMaterialType[]>([]);
|
||
const enabledMaterialTypes = computed(() =>
|
||
materialTypes.value.filter(item => item.status === "enable")
|
||
);
|
||
const versionOptions = reactive<Record<number, OnboardingContractVersion[]>>({});
|
||
|
||
const query = reactive({
|
||
keyword: "",
|
||
companyId: undefined as number | undefined,
|
||
departmentId: undefined as number | undefined,
|
||
positionId: undefined as number | undefined,
|
||
status: "enable" as string | undefined,
|
||
pageNum: 1,
|
||
pageSize: 20
|
||
});
|
||
|
||
const form = reactive<OnboardingTemplate>({
|
||
id: 0,
|
||
templateCode: "",
|
||
templateName: "",
|
||
companyId: 0,
|
||
departmentId: 0,
|
||
description: "",
|
||
status: "enable",
|
||
positionIds: [],
|
||
contracts: [],
|
||
formFields: [],
|
||
materials: []
|
||
});
|
||
|
||
const {
|
||
departments: queryDepartments,
|
||
positions: queryPositions,
|
||
onCompanyChange: changeQueryCompany,
|
||
onDepartmentChange: changeQueryDepartment,
|
||
reset: resetQueryScope
|
||
} = useOnboardingScopeOptions(() => query);
|
||
|
||
let companiesLoaded = false;
|
||
let companyRequest: Promise<void> | undefined;
|
||
async function loadCompanies() {
|
||
if (companiesLoaded) return;
|
||
if (!companyRequest) {
|
||
companyRequest = onboardingTaskApi.listCompanies().then(({ data }) => {
|
||
companies.value = data || [];
|
||
companiesLoaded = true;
|
||
}).finally(() => { companyRequest = undefined; });
|
||
}
|
||
await companyRequest;
|
||
}
|
||
|
||
async function loadBaseData() {
|
||
const [, materials] = await Promise.all([loadCompanies(), props.loadMaterials()]);
|
||
materialTypes.value = materials;
|
||
}
|
||
|
||
let templateRequest = 0;
|
||
async function loadTemplates() {
|
||
const request = ++templateRequest;
|
||
loading.value = true;
|
||
try {
|
||
const res = await onboardingTaskApi.onboardingTemplatePage({ ...query });
|
||
if (request !== templateRequest) return;
|
||
rows.value = res.data?.list || [];
|
||
total.value = res.data?.total || 0;
|
||
} finally {
|
||
if (request === templateRequest) loading.value = false;
|
||
}
|
||
}
|
||
|
||
async function onQueryCompanyChange(companyId?: number) {
|
||
query.pageNum = 1;
|
||
await Promise.all([changeQueryCompany(companyId), loadTemplates()]);
|
||
}
|
||
|
||
async function onQueryDepartmentChange(departmentId?: number) {
|
||
query.pageNum = 1;
|
||
await Promise.all([changeQueryDepartment(departmentId), loadTemplates()]);
|
||
}
|
||
|
||
function resetQuery() {
|
||
query.keyword = "";
|
||
query.companyId = undefined;
|
||
query.departmentId = undefined;
|
||
query.positionId = undefined;
|
||
query.status = "enable";
|
||
query.pageNum = 1;
|
||
resetQueryScope();
|
||
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);
|
||
}
|
||
|
||
function isVersionCurrentlyAvailable(version: OnboardingContractVersion) {
|
||
const now = dayjs();
|
||
const effectiveAt = version.effectiveDate ? dayjs(version.effectiveDate) : undefined;
|
||
const expiresAt = version.expiryDate ? dayjs(version.expiryDate) : undefined;
|
||
return version.versionStatus === "enabled"
|
||
&& (!effectiveAt || (effectiveAt.isValid() && !effectiveAt.isAfter(now)))
|
||
&& (!expiresAt || (expiresAt.isValid() && expiresAt.isAfter(now)));
|
||
}
|
||
|
||
async function loadContractVersions(templateId: number) {
|
||
const res = await onboardingTaskApi.versionPage({
|
||
templateId,
|
||
versionStatus: "enabled",
|
||
pageNum: 1,
|
||
pageSize: 100
|
||
});
|
||
versionOptions[templateId] = (res.data?.list || []).filter(isVersionCurrentlyAvailable);
|
||
}
|
||
|
||
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);
|
||
}
|
||
}
|
||
|
||
const isAllMaterialsSelected = computed(() => {
|
||
if (!enabledMaterialTypes.value.length) return false;
|
||
return enabledMaterialTypes.value.every(item => hasMaterial(item.id));
|
||
});
|
||
|
||
const isMaterialsIndeterminate = computed(() => {
|
||
if (!enabledMaterialTypes.value.length) return false;
|
||
const selectedCount = enabledMaterialTypes.value.filter(item => hasMaterial(item.id)).length;
|
||
return selectedCount > 0 && selectedCount < enabledMaterialTypes.value.length;
|
||
});
|
||
|
||
function handleSelectAllMaterials(checked: boolean | string | number) {
|
||
const isChecked = Boolean(checked);
|
||
if (isChecked) {
|
||
enabledMaterialTypes.value.forEach(item => {
|
||
if (!hasMaterial(item.id)) {
|
||
form.materials.push({
|
||
materialTypeId: item.id,
|
||
required: true,
|
||
sortCode: (form.materials.length + 1) * 10
|
||
});
|
||
}
|
||
});
|
||
} else {
|
||
const enabledIds = new Set(enabledMaterialTypes.value.map(item => item.id));
|
||
form.materials = form.materials.filter(item => !enabledIds.has(item.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
|
||
}))
|
||
});
|
||
|
||
const enabledMaterialIds = new Set(enabledMaterialTypes.value.map(item => item.id));
|
||
const disabledMaterials = materialTypes.value.filter(item =>
|
||
item.status !== "enable" && form.materials.some(config => config.materialTypeId === item.id)
|
||
);
|
||
form.materials = form.materials.filter(item => enabledMaterialIds.has(item.materialTypeId));
|
||
if (disabledMaterials.length > 0) {
|
||
ElMessage.warning(
|
||
`该模板原来关联的停用资料“${disabledMaterials.map(item => item.materialName).join("、")}”已从本次配置中移除,保存后生效`
|
||
);
|
||
}
|
||
|
||
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 || [];
|
||
}
|
||
const configuredContractCount = form.contracts.length;
|
||
await loadAvailableContractTemplates();
|
||
const removedTemplateCount = configuredContractCount - form.contracts.length;
|
||
let clearedVersionCount = 0;
|
||
for (const contract of form.contracts) {
|
||
if (contract.contractTemplateId) {
|
||
await loadContractVersions(contract.contractTemplateId);
|
||
const availableVersions = versionOptions[contract.contractTemplateId] || [];
|
||
if (!availableVersions.some(item => item.id === contract.contractTemplateVersionId)) {
|
||
contract.contractTemplateVersionId = undefined as any;
|
||
clearedVersionCount++;
|
||
}
|
||
}
|
||
}
|
||
if (removedTemplateCount > 0 || clearedVersionCount > 0) {
|
||
ElMessage.warning(
|
||
"该入职模板原来关联了已停用、未生效或已失效的签署材料,已从当前选择中移除或清空,请重新选择后保存"
|
||
);
|
||
}
|
||
visible.value = true;
|
||
}
|
||
|
||
async function save() {
|
||
form.templateCode = form.templateCode.trim();
|
||
if (
|
||
!form.templateCode
|
||
|| !form.templateName
|
||
|| !form.companyId
|
||
|| !form.departmentId
|
||
|| !form.positionIds.length
|
||
) {
|
||
return ElMessage.warning("请完整填写模板编码、名称、公司、部门及适用岗位");
|
||
}
|
||
if (!/^[A-Za-z][A-Za-z0-9._-]{0,99}$/.test(form.templateCode)) {
|
||
return ElMessage.warning("模板编码须以字母开头,最长 100 位,仅支持字母、数字、点、下划线和短横线");
|
||
}
|
||
if (!form.contracts.length || form.contracts.some(item => !item.contractTemplateId || !item.contractTemplateVersionId)) {
|
||
return ElMessage.warning("请完整选择至少一份签署材料模板及版本");
|
||
}
|
||
const containsUnavailableVersion = form.contracts.some(contract => {
|
||
const versions = versionOptions[contract.contractTemplateId] || [];
|
||
const selected = versions.find(item => item.id === contract.contractTemplateVersionId);
|
||
return !selected || !isVersionCurrentlyAvailable(selected);
|
||
});
|
||
if (containsUnavailableVersion) {
|
||
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) {
|
||
const confirmed = await isMessageBoxConfirmed(
|
||
ElMessageBox.confirm(`确认删除入职模板“${row.templateName}”吗?`, "删除确认", { type: "warning" })
|
||
);
|
||
if (!confirmed) return;
|
||
|
||
await onboardingTaskApi.deleteOnboardingTemplate([row.id!]);
|
||
ElMessage.success("删除成功");
|
||
query.pageNum = 1;
|
||
await loadTemplates();
|
||
}
|
||
|
||
onMounted(async () => {
|
||
await Promise.all([loadCompanies(), 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: 170px;
|
||
}
|
||
|
||
.department-filter {
|
||
width: 160px;
|
||
}
|
||
|
||
.position-filter {
|
||
width: 160px;
|
||
}
|
||
|
||
.status-filter {
|
||
width: 120px;
|
||
}
|
||
|
||
.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>
|