This commit is contained in:
fuenmao
2026-08-25 20:37:24 +08:00
parent aec6c1f36c
commit eab359d9fa
6 changed files with 1748 additions and 1503 deletions
+47
View File
@@ -495,5 +495,52 @@ export const onboardingTaskApi = {
"material-type/delete", "material-type/delete",
{ ids } { ids }
); );
},
// 合同变量字典定义
variableDefinitionPage(params: { keyword?: string; variableCategory?: string; pageNum: number; pageSize: number }) {
return moduleRequest("/biz/onboarding/variable-definition/").get<ResPage<any>>("page", params);
},
variableDefinitionList() {
return moduleRequest("/biz/onboarding/variable-definition/").get<any[]>("list");
},
addVariableDefinition(params: Record<string, unknown>) {
return moduleRequest("/biz/onboarding/variable-definition/").post("add", params);
},
editVariableDefinition(params: Record<string, unknown>) {
return moduleRequest("/biz/onboarding/variable-definition/").post("edit", params);
},
deleteVariableDefinitions(ids: number[]) {
return moduleRequest("/biz/onboarding/variable-definition/").post("delete", { ids });
}
};
const publicHttp = moduleRequest("/open/onboarding/");
export const onboardingPublicApi = {
detail(params: { token: string }) {
return publicHttp.post<any>("detail", params);
},
saveEmployeeInfo(params: Record<string, unknown>) {
return publicHttp.post("save-employee-info", params);
},
saveMaterial(params: { token: string; materialTypeId: number; fileId: number }) {
return publicHttp.post("save-material", params);
},
confirmContract(params: { token: string; taskContractId: number }) {
return publicHttp.post("confirm-contract", params);
},
submit(params: { token: string }) {
return publicHttp.post("submit", params);
},
contractSourceUrl(params: { token: string; taskContractId: number }) {
return `/api/open/onboarding/contract-source?token=${encodeURIComponent(params.token)}&taskContractId=${params.taskContractId}`;
},
deleteMaterial(params: { token: string; taskMaterialId: number }) {
return publicHttp.post("delete-material", params);
} }
}; };
+170 -97
View File
@@ -82,7 +82,7 @@
<el-drawer <el-drawer
v-model="drawerVisible" v-model="drawerVisible"
:title="`合同模板管理 · ${selectedTemplate?.templateName || ''}`" :title="`合同模板管理 · ${selectedTemplate?.templateName || ''}`"
size="960px" size="1150px"
destroy-on-close destroy-on-close
> >
<div v-if="selectedTemplate" class="drawer-content-box"> <div v-if="selectedTemplate" class="drawer-content-box">
@@ -135,54 +135,56 @@
<el-table-column prop="expiryDate" label="失效日期" width="130" align="center" /> <el-table-column prop="expiryDate" label="失效日期" width="130" align="center" />
<el-table-column prop="remark" label="版本说明" min-width="150" /> <el-table-column prop="remark" label="版本说明" min-width="150" />
<el-table-column label="操作" width="280" fixed="right"> <el-table-column label="操作" width="340" fixed="right" align="center">
<template #default="scope"> <template #default="scope">
<el-button <div class="table-actions">
link <el-button
type="primary" link
icon="Setting" type="primary"
@click="openVariables(scope.row)" icon="Setting"
> @click="openVariables(scope.row)"
变量 >
</el-button> 变量
</el-button>
<el-button <el-button
link link
type="primary" type="primary"
icon="Download" icon="Download"
@click="downloadDocx(scope.row.sourceDocxFileId)" @click="downloadDocx(scope.row.sourceDocxFileId)"
> >
下载源文件 下载源文件
</el-button> </el-button>
<el-button <el-button
v-if="scope.row.versionStatus !== 'enabled'" v-if="scope.row.versionStatus !== 'enabled'"
link link
type="success" type="success"
icon="Check" icon="Check"
@click="changeVersionStatus(scope.row, 'enabled')" @click="changeVersionStatus(scope.row, 'enabled')"
> >
启用 启用
</el-button> </el-button>
<el-button <el-button
v-else v-else
link link
type="warning" type="warning"
icon="Close" icon="Close"
@click="changeVersionStatus(scope.row, 'disabled')" @click="changeVersionStatus(scope.row, 'disabled')"
> >
停用 停用
</el-button> </el-button>
<el-button <el-button
link link
type="danger" type="danger"
icon="Delete" icon="Delete"
@click="removeVersion(scope.row)" @click="removeVersion(scope.row)"
> >
删除 删除
</el-button> </el-button>
</div>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@@ -257,69 +259,87 @@
<el-dialog <el-dialog
v-model="variablesVisible" v-model="variablesVisible"
:title="`合同变量配置 · 版本 ${selectedVersion?.versionNo || ''}`" :title="`合同变量配置 · 版本 ${selectedVersion?.versionNo || ''}`"
width="1050px" width="1220px"
top="4vh"
destroy-on-close destroy-on-close
append-to-body append-to-body
class="variables-dialog"
> >
<el-alert <el-alert
title="变量已从 DOCX 自动识别。请逐项检查来源、显示名称、数据类型和取值字段;未识别变量调整完成后才能启用版本。" title="变量已从 DOCX 自动识别与解析。请核对来源分类、显示名称及数据类型;确保全部配置正确后即可启用版本。"
type="info" type="info"
show-icon show-icon
:closable="false" :closable="false"
class="mb-3"
/> />
<el-table :data="variables" border stripe class="mt-3"> <!-- 分组 Tabs 切换 -->
<el-table-column prop="variableCode" label="变量编码 (DOCX占位符)" min-width="220"> <el-tabs v-model="variableTab" type="card" class="variable-filter-tabs">
<template #default="scope"> <el-tab-pane :label="`全部变量 (${variableCounts.all})`" name="all" />
<div style="display: flex; align-items: center; justify-content: space-between;"> <el-tab-pane :label="`员工填写 (${variableCounts.employee})`" name="employee" />
<code style="font-weight: 600; color: #2563eb;">{{ formatPlaceholder(scope.row.variableCode) }}</code> <el-tab-pane :label="`人事合同参数 (${variableCounts.hr})`" name="hr" />
<el-button <el-tab-pane :label="`公司信息 (${variableCounts.company})`" name="company" />
size="small" <el-tab-pane :label="`系统预置 (${variableCounts.system})`" name="system" />
link </el-tabs>
type="primary"
icon="CopyDocument" <div class="variables-table-wrapper">
@click="copyPlaceholder(scope.row.variableCode)" <el-table :data="filteredVariables" border stripe height="500px">
> <el-table-column prop="variableCode" label="变量全编码 (DOCX占位符)" min-width="260">
复制 <template #default="scope">
<div class="variable-code-cell">
<code class="code-font">{{ formatPlaceholder(scope.row.variableCode) }}</code>
<el-button
size="small"
link
type="primary"
icon="CopyDocument"
@click="copyPlaceholder(scope.row.variableCode)"
>
复制
</el-button>
</div>
</template>
</el-table-column>
<el-table-column prop="variableName" label="变量显示名称" min-width="170" />
<el-table-column prop="variableType" label="来源板块" width="130" align="center">
<template #default="scope">
<el-tag size="small" :type="variableTypeTag(scope.row.variableType)">
{{ variableTypeText(scope.row.variableType) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="dataType" label="数据类型" width="100" align="center">
<template #default="scope">
<el-tag size="small" type="info">{{ scope.row.dataType || 'string' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="employeeField" label="对应字段" min-width="160" show-overflow-tooltip />
<el-table-column label="必填" width="80" align="center">
<template #default="scope">
<el-tag size="small" :type="scope.row.required ? 'danger' : 'info'">
{{ scope.row.required ? "是" : "否" }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="120" align="center" fixed="right">
<template #default="scope">
<el-button link type="primary" icon="Edit" @click="openVariableEdit(scope.row)">
检查调整
</el-button> </el-button>
</div> </template>
</template> </el-table-column>
</el-table-column> </el-table>
</div>
<el-table-column prop="variableName" label="变量显示名称" min-width="160" /> <template #footer>
<el-button type="primary" @click="variablesVisible = false">关闭</el-button>
<el-table-column prop="variableType" label="来源类型" width="130" align="center"> </template>
<template #default="scope">
<el-tag size="small" :type="variableTypeTag(scope.row.variableType)">
{{ variableTypeText(scope.row.variableType) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="dataType" label="数据类型" width="100" align="center">
<template #default="scope">
<el-tag size="small" type="info">{{ scope.row.dataType }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="employeeField" label="对应员工字段" min-width="140" />
<el-table-column label="必填" width="80" align="center">
<template #default="scope">
<el-tag size="small" :type="scope.row.required ? 'danger' : 'info'">
{{ scope.row.required ? "是" : "否" }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="150" fixed="right">
<template #default="scope">
<el-button link type="primary" icon="Edit" @click="openVariableEdit(scope.row)">
检查调整
</el-button>
</template>
</el-table-column>
</el-table>
</el-dialog> </el-dialog>
<!-- 新增 / 编辑单个变量 --> <!-- 新增 / 编辑单个变量 -->
@@ -391,7 +411,7 @@
</template> </template>
<script setup lang="ts" name="onboardingContract"> <script setup lang="ts" name="onboardingContract">
import { reactive, ref } from "vue"; import { computed, reactive, ref } from "vue";
import { ElMessage, ElMessageBox } from "element-plus"; import { ElMessage, ElMessageBox } from "element-plus";
import { Check } from "@element-plus/icons-vue"; import { Check } from "@element-plus/icons-vue";
import { import {
@@ -414,6 +434,25 @@ const versionVisible = ref(false);
const variablesVisible = ref(false); const variablesVisible = ref(false);
const variables = ref<OnboardingTemplateVariable[]>([]); const variables = ref<OnboardingTemplateVariable[]>([]);
const variableVisible = ref(false); const variableVisible = ref(false);
const variableTab = ref("all");
const filteredVariables = computed(() => {
if (variableTab.value === "all") return variables.value;
if (variableTab.value === "employee") return variables.value.filter(v => v.variableType === "employee");
if (variableTab.value === "hr") return variables.value.filter(v => v.variableType === "hr" || v.variableType === "contract" || v.variableCode.startsWith("hr."));
if (variableTab.value === "company") return variables.value.filter(v => v.variableType === "company");
if (variableTab.value === "system") return variables.value.filter(v => v.variableType === "system");
return variables.value;
});
const variableCounts = computed(() => {
const all = variables.value.length;
const employee = variables.value.filter(v => v.variableType === "employee").length;
const hr = variables.value.filter(v => v.variableType === "hr" || v.variableType === "contract" || v.variableCode.startsWith("hr.")).length;
const company = variables.value.filter(v => v.variableType === "company").length;
const system = variables.value.filter(v => v.variableType === "system").length;
return { all, employee, hr, company, system };
});
const emptyTemplate = (): Partial<OnboardingContractTemplate> => ({ const emptyTemplate = (): Partial<OnboardingContractTemplate> => ({
templateCode: "", templateCode: "",
@@ -734,4 +773,38 @@ async function saveVariable() {
display: flex; display: flex;
align-items: center; align-items: center;
} }
.table-actions {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 4px;
white-space: nowrap;
}
.variable-filter-tabs {
margin-bottom: 12px;
}
.variables-table-wrapper {
border-radius: 6px;
overflow: hidden;
}
.variable-code-cell {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
white-space: nowrap;
}
.code-font {
font-family: Consolas, Monaco, monospace;
font-weight: 600;
color: #2563eb;
background: #eff6ff;
padding: 2px 6px;
border-radius: 4px;
}
</style> </style>
@@ -1,13 +1,8 @@
<template> <template>
<div class="template-panel"> <div class="template-panel">
<div class="toolbar"> <div class="toolbar">
<el-input <el-input v-model="query.keyword" clearable placeholder="搜索模板编码或名称" class="keyword-input"
v-model="query.keyword" @keyup.enter="loadTemplates" />
clearable
placeholder="搜索模板编码或名称"
class="keyword-input"
@keyup.enter="loadTemplates"
/>
<el-select v-model="query.companyId" clearable filterable placeholder="全部公司" class="company-filter"> <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-option v-for="item in companies" :key="item.id" :label="item.companyName" :value="item.id" />
</el-select> </el-select>
@@ -17,193 +12,202 @@
<el-button type="primary" icon="Plus" @click="openCreate">新增入职模板</el-button> <el-button type="primary" icon="Plus" @click="openCreate">新增入职模板</el-button>
</div> </div>
<el-alert <el-alert title="入职模板统一管理公司、岗位、合同文件与员工上传材料;员工入职档案自动采用标准《入职登记表》。" type="info" :closable="false" show-icon
title="入职模板统一管理公司、岗位、合同、员工补充字段和上传材料;创建任务后会冻结本次配置。" class="tip" />
type="info"
:closable="false"
show-icon
class="tip"
/>
<el-table v-loading="loading" :data="rows" border> <div class="table-container">
<el-table-column prop="templateCode" label="模板编码" min-width="150" /> <el-table v-loading="loading" :data="rows" border stripe height="100%">
<el-table-column prop="templateName" label="模板名称" min-width="180" /> <el-table-column prop="templateCode" label="模板编码" min-width="150" />
<el-table-column prop="companyName" label="所属公司" min-width="160" /> <el-table-column prop="templateName" label="模板名称" min-width="180" />
<el-table-column label="适用岗位" min-width="220"> <el-table-column prop="companyName" label="所属公司" min-width="160" />
<template #default="scope"> <el-table-column label="适用岗位" min-width="220">
<el-space wrap> <template #default="scope">
<el-tag v-for="name in scope.row.positionNames" :key="name" size="small">{{ name }}</el-tag> <el-space wrap>
</el-space> <el-tag v-for="name in scope.row.positionNames" :key="name" size="small">{{ name }}</el-tag>
</template> </el-space>
</el-table-column> </template>
<el-table-column label="配置内容" min-width="230"> </el-table-column>
<template #default="scope"> <el-table-column label="配置内容" min-width="200">
合同 {{ scope.row.contractCount || 0 }} 份 / 补充字段 {{ scope.row.formFieldCount || 0 }} 项 / 材料 {{ scope.row.materialCount || 0 }} 项 <template #default="scope">
</template> 合同 {{ scope.row.contractCount || 0 }} 份 / 材料 {{ scope.row.materialCount || 0 }} 项
</el-table-column> </template>
<el-table-column prop="status" label="状态" width="90" align="center"> </el-table-column>
<template #default="scope"> <el-table-column prop="status" label="状态" width="90" align="center">
<el-tag :type="scope.row.status === 'enable' ? 'success' : 'info'"> <template #default="scope">
{{ scope.row.status === "enable" ? "启用" : "停用" }} <el-tag :type="scope.row.status === 'enable' ? 'success' : 'info'">
</el-tag> {{ scope.row.status === "enable" ? "启用" : "停用" }}
</template> </el-tag>
</el-table-column> </template>
<el-table-column label="操作" width="150" fixed="right"> </el-table-column>
<template #default="scope"> <el-table-column label="操作" width="150" fixed="right">
<el-button link type="primary" @click="openEdit(scope.row)">编辑</el-button> <template #default="scope">
<el-button link type="danger" @click="removeTemplate(scope.row)">删除</el-button> <el-button link type="primary" @click="openEdit(scope.row)">编辑</el-button>
</template> <el-button link type="danger" @click="removeTemplate(scope.row)">删除</el-button>
</el-table-column> </template>
</el-table> </el-table-column>
</el-table>
<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> </div>
<el-dialog <div class="pagination">
v-model="visible" <el-pagination v-model:current-page="query.pageNum" v-model:page-size="query.pageSize"
:title="form.id ? '编辑入职模板' : '新增入职模板'" layout="total, sizes, prev, pager, next" :total="total" @current-change="loadTemplates"
width="1080px" @size-change="loadTemplates" />
top="5vh" </div>
destroy-on-close
>
<el-form label-width="110px">
<el-divider content-position="left">基本信息</el-divider>
<el-row :gutter="16">
<el-col :span="8">
<el-form-item label="模板编码" required>
<el-input v-model="form.templateCode" placeholder="例如 FULLTIME_DEFAULT" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="模板名称" required>
<el-input v-model="form.templateName" placeholder="例如 全职员工入职模板" />
</el-form-item>
</el-col>
<el-col :span="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-row>
<el-row :gutter="16">
<el-col :span="10">
<el-form-item label="所属公司" required>
<el-select v-model="form.companyId" filterable class="full-width" @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 :span="14">
<el-form-item label="适用岗位" required>
<el-select v-model="form.positionIds" multiple filterable collapse-tags class="full-width" :disabled="!form.companyId">
<el-option v-for="item in positions" :key="item.id" :label="item.positionName" :value="item.id" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="模板说明">
<el-input v-model="form.description" type="textarea" :rows="2" />
</el-form-item>
<el-divider content-position="left">合同文件</el-divider> <!-- 模板编辑大弹窗 -->
<div class="section-action"> <el-dialog v-model="visible" :title="form.id ? '编辑入职模板' : '新增入职模板'" width="62%" top="3vh" destroy-on-close
<span>合同变量由上传 DOCX 时自动识别,这里只选择已确认并启用的版本。</span> append-to-body class="template-dialog">
<el-button type="primary" plain icon="Plus" @click="addContract">添加合同</el-button> <div class="template-dialog-body">
</div> <el-form label-width="100px" label-position="top">
<el-table :data="form.contracts" border empty-text="请至少添加一份合同"> <!-- 1. 基本信息卡片 -->
<el-table-column label="合同模板" min-width="210"> <div class="form-block-card">
<template #default="scope"> <div class="block-card-header">
<el-select v-model="scope.row.contractTemplateId" filterable class="full-width" @change="onContractTemplateChange(scope.row)"> <span class="block-title">基本信息</span>
<el-option v-for="item in contractTemplates" :key="item.id" :label="item.templateName" :value="item.id" /> <span class="block-subtitle">配置模板编码、名称、归属公司及适用岗位范围</span>
</el-select> </div>
</template>
</el-table-column>
<el-table-column label="启用版本" min-width="180">
<template #default="scope">
<el-select v-model="scope.row.contractTemplateVersionId" class="full-width" :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">
<template #default="scope"><el-input-number v-model="scope.row.printCopies" :min="1" :max="10" /></template>
</el-table-column>
<el-table-column label="排序" width="110">
<template #default="scope"><el-input-number v-model="scope.row.sortCode" :min="0" /></template>
</el-table-column>
<el-table-column label="操作" width="80" align="center">
<template #default="scope"><el-button link type="danger" @click="form.contracts.splice(scope.$index, 1)">移除</el-button></template>
</el-table-column>
</el-table>
<el-divider content-position="left">员工补充字段</el-divider> <el-row :gutter="20">
<el-alert title="姓名、手机号、身份证号等基本信息固定展示;这里只选择额外需要员工填写的字段。" type="info" :closable="false" class="section-tip" /> <el-col :xs="24" :sm="8">
<el-table :data="formFields" border max-height="260"> <el-form-item label="模板编码" required>
<el-table-column label="选择" width="70" align="center"> <el-input v-model="form.templateCode" placeholder="如:FULLTIME_DEV" />
<template #default="scope"> </el-form-item>
<el-checkbox :model-value="hasField(scope.row.id)" @change="value => toggleField(scope.row.id, Boolean(value))" /> </el-col>
</template> <el-col :xs="24" :sm="8">
</el-table-column> <el-form-item label="模板名称" required>
<el-table-column prop="fieldCode" label="字段编码" min-width="170" /> <el-input v-model="form.templateName" placeholder="如:全职技术人员入职模板" />
<el-table-column prop="fieldName" label="字段名称" min-width="180" /> </el-form-item>
<el-table-column prop="dataType" label="数据类型" width="110" /> </el-col>
<el-table-column label="必填" width="100" align="center"> <el-col :xs="24" :sm="8">
<template #default="scope"> <el-form-item label="状态" required>
<el-switch v-if="hasField(scope.row.id)" v-model="fieldConfig(scope.row.id)!.required" /> <el-radio-group v-model="form.status">
</template> <el-radio value="enable">启用</el-radio>
</el-table-column> <el-radio value="disable">停用</el-radio>
<el-table-column label="排序" width="120"> </el-radio-group>
<template #default="scope"> </el-form-item>
<el-input-number v-if="hasField(scope.row.id)" v-model="fieldConfig(scope.row.id)!.sortCode" :min="0" /> </el-col>
</template>
</el-table-column>
</el-table>
<el-divider content-position="left">员工上传材料</el-divider> <el-col :xs="24" :sm="10">
<el-table :data="materialTypes" border max-height="260"> <el-form-item label="所属公司" required>
<el-table-column label="选择" width="70" align="center"> <el-select v-model="form.companyId" filterable class="full-width" placeholder="请选择公司"
<template #default="scope"> @change="onCompanyChange">
<el-checkbox :model-value="hasMaterial(scope.row.id)" @change="value => toggleMaterial(scope.row.id, Boolean(value))" /> <el-option v-for="item in companies" :key="item.id" :label="item.companyName" :value="item.id" />
</template> </el-select>
</el-table-column> </el-form-item>
<el-table-column prop="materialCode" label="材料编码" min-width="170" /> </el-col>
<el-table-column prop="materialName" label="材料名称" min-width="200" /> <el-col :xs="24" :sm="14">
<el-table-column prop="maxFileCount" label="最多文件" width="100" align="center" /> <el-form-item label="适用岗位" required>
<el-table-column label="必传" width="100" align="center"> <el-select v-model="form.positionIds" multiple filterable collapse-tags collapse-tags-tooltip
<template #default="scope"> class="full-width" :disabled="!form.companyId" placeholder="请选择适用的岗位(可多选)">
<el-switch v-if="hasMaterial(scope.row.id)" v-model="materialConfig(scope.row.id)!.required" /> <el-option v-for="item in positions" :key="item.id" :label="item.positionName" :value="item.id" />
</template> </el-select>
</el-table-column> </el-form-item>
<el-table-column label="排序" width="120"> </el-col>
<template #default="scope">
<el-input-number v-if="hasMaterial(scope.row.id)" v-model="materialConfig(scope.row.id)!.sortCode" :min="0" /> <el-col :span="24">
</template> <el-form-item label="模板说明">
</el-table-column> <el-input v-model="form.description" type="textarea" :rows="2" placeholder="填写该入职模板的适用范围或注意事项" />
</el-table> </el-form-item>
</el-form> </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" @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> <template #footer>
<el-button @click="visible = false">取消</el-button> <el-button @click="visible = false">取消</el-button>
<el-button type="primary" :loading="saving" @click="save">保存</el-button> <el-button type="primary" :loading="saving" @click="save">保存入职模板</el-button>
</template> </template>
</el-dialog> </el-dialog>
</div> </div>
@@ -213,28 +217,24 @@
import { onMounted, reactive, ref } from "vue"; import { onMounted, reactive, ref } from "vue";
import { ElMessage, ElMessageBox } from "element-plus"; import { ElMessage, ElMessageBox } from "element-plus";
import { import {
onboardingTaskApi, OnboardingCompany,
type OnboardingCompanyOption, OnboardingContractTemplate,
type OnboardingContractTemplate, OnboardingContractVersion,
type OnboardingContractVersion, OnboardingMaterialType,
type OnboardingFormField, OnboardingPosition,
type OnboardingMaterialType, OnboardingTemplate,
type OnboardingPositionOption, onboardingTaskApi
type OnboardingTemplate,
type OnboardingTemplateContractConfig,
type OnboardingTemplateFieldConfig,
type OnboardingTemplateMaterialConfig
} from "@/api/modules/biz/onboarding"; } from "@/api/modules/biz/onboarding";
const loading = ref(false); const loading = ref(false);
const saving = ref(false); const saving = ref(false);
const visible = ref(false); const visible = ref(false);
const total = ref(0);
const rows = ref<OnboardingTemplate[]>([]); const rows = ref<OnboardingTemplate[]>([]);
const companies = ref<OnboardingCompanyOption[]>([]); const total = ref(0);
const positions = ref<OnboardingPositionOption[]>([]);
const companies = ref<OnboardingCompany[]>([]);
const positions = ref<OnboardingPosition[]>([]);
const contractTemplates = ref<OnboardingContractTemplate[]>([]); const contractTemplates = ref<OnboardingContractTemplate[]>([]);
const formFields = ref<OnboardingFormField[]>([]);
const materialTypes = ref<OnboardingMaterialType[]>([]); const materialTypes = ref<OnboardingMaterialType[]>([]);
const versionOptions = reactive<Record<number, OnboardingContractVersion[]>>({}); const versionOptions = reactive<Record<number, OnboardingContractVersion[]>>({});
@@ -245,101 +245,80 @@ const query = reactive({
pageSize: 20 pageSize: 20
}); });
const emptyForm = (): OnboardingTemplate => ({ const form = reactive<OnboardingTemplate>({
id: undefined as unknown as number, id: 0,
templateCode: "", templateCode: "",
templateName: "", templateName: "",
companyId: undefined as unknown as number, companyId: 0,
description: "",
status: "enable",
positionIds: [], positionIds: [],
contracts: [], contracts: [],
formFields: [], formFields: [],
materials: [], materials: []
status: "enable",
description: ""
}); });
const form = reactive<OnboardingTemplate>(emptyForm());
async function loadBaseData() { async function loadBaseData() {
try { const [companyRes, templateRes, materialRes] = await Promise.all([
const [companyRes, contractRes, fieldRes, materialRes] = await Promise.all([ onboardingTaskApi.companyPage({ pageNum: 1, pageSize: 100, status: "enable" }).catch(() => ({ data: { list: [] } })),
onboardingTaskApi.listCompanies().catch(() => ({ data: [] })), onboardingTaskApi.templatePage({ pageNum: 1, pageSize: 100, status: "enable" }).catch(() => ({ data: { list: [] } })),
onboardingTaskApi.templatePage({ pageNum: 1, pageSize: 100, status: "enable" }).catch(() => ({ data: { list: [] } })), onboardingTaskApi.listMaterialTypes().catch(() => ({ data: [] }))
onboardingTaskApi.listFormFields().catch(() => ({ data: [] })), ]);
onboardingTaskApi.listMaterialTypes().catch(() => ({ data: [] })) companies.value = companyRes.data?.list || [];
]); contractTemplates.value = templateRes.data?.list || [];
companies.value = companyRes.data || []; materialTypes.value = materialRes.data || [];
contractTemplates.value = contractRes.data?.list || [];
formFields.value = (fieldRes.data || []).filter(item => item.status === "enable");
materialTypes.value = (materialRes.data || []).filter(item => item.status === "enable");
} catch (e) {
console.error("加载基础数据失败", e);
}
} }
async function loadTemplates() { async function loadTemplates() {
loading.value = true; loading.value = true;
try { try {
const { data } = await onboardingTaskApi.onboardingTemplatePage(query); const res = await onboardingTaskApi.onboardingTemplatePage(query);
rows.value = data?.list || []; rows.value = res.data?.list || [];
total.value = data?.total || 0; total.value = res.data?.total || 0;
} finally { } finally {
loading.value = false; loading.value = false;
} }
} }
function resetQuery() { function resetQuery() {
Object.assign(query, { keyword: "", companyId: undefined, pageNum: 1 }); query.keyword = "";
query.companyId = undefined;
query.pageNum = 1;
loadTemplates(); loadTemplates();
} }
async function loadPositions(companyId: number) {
const { data } = await onboardingTaskApi.listPositions(companyId);
positions.value = data || [];
}
async function onCompanyChange(companyId: number) { async function onCompanyChange(companyId: number) {
form.positionIds = []; form.positionIds = [];
positions.value = []; if (!companyId) {
if (companyId) await loadPositions(companyId); positions.value = [];
return;
}
const res = await onboardingTaskApi.positionPage({ companyId, pageNum: 1, pageSize: 100, status: "enable" });
positions.value = res.data?.list || [];
}
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() { function addContract() {
form.contracts.push({ form.contracts.push({
contractTemplateId: undefined as unknown as number, contractTemplateId: undefined as any,
contractTemplateVersionId: undefined as unknown as number, contractTemplateVersionId: undefined as any,
required: true, required: true,
printCopies: 1, printCopies: 1,
sortCode: (form.contracts.length + 1) * 10 sortCode: (form.contracts.length + 1) * 10
}); });
} }
async function loadVersions(templateId: number) {
if (!templateId || versionOptions[templateId]) return;
const { data } = await onboardingTaskApi.versionPage({ templateId, pageNum: 1, pageSize: 100 });
versionOptions[templateId] = (data?.list || []).filter(item => item.versionStatus === "enabled");
}
async function onContractTemplateChange(row: OnboardingTemplateContractConfig) {
row.contractTemplateVersionId = undefined as unknown as number;
await loadVersions(row.contractTemplateId);
}
function hasField(fieldId: number) {
return form.formFields.some(item => item.fieldId === fieldId);
}
function fieldConfig(fieldId: number) {
return form.formFields.find(item => item.fieldId === fieldId);
}
function toggleField(fieldId: number, selected: boolean) {
if (selected && !hasField(fieldId)) {
form.formFields.push({ fieldId, required: true, sortCode: (form.formFields.length + 1) * 10 });
} else if (!selected) {
form.formFields = form.formFields.filter(item => item.fieldId !== fieldId);
}
}
function hasMaterial(materialTypeId: number) { function hasMaterial(materialTypeId: number) {
return form.materials.some(item => item.materialTypeId === materialTypeId); return form.materials.some(item => item.materialTypeId === materialTypeId);
} }
@@ -348,35 +327,77 @@ function materialConfig(materialTypeId: number) {
return form.materials.find(item => item.materialTypeId === materialTypeId); return form.materials.find(item => item.materialTypeId === materialTypeId);
} }
function toggleMaterial(materialTypeId: number, selected: boolean) { function toggleMaterial(materialTypeId: number, checked: boolean) {
if (selected && !hasMaterial(materialTypeId)) { if (checked) {
form.materials.push({ materialTypeId, required: true, sortCode: (form.materials.length + 1) * 10 }); if (!hasMaterial(materialTypeId)) {
} else if (!selected) { form.materials.push({ materialTypeId, required: true, sortCode: (form.materials.length + 1) * 10 });
}
} else {
form.materials = form.materials.filter(item => item.materialTypeId !== materialTypeId); form.materials = form.materials.filter(item => item.materialTypeId !== materialTypeId);
} }
} }
async function openCreate() { async function openCreate() {
Object.assign(form, emptyForm());
positions.value = [];
await loadBaseData(); await loadBaseData();
Object.assign(form, {
id: undefined,
templateCode: "",
templateName: "",
companyId: undefined as any,
description: "",
status: "enable",
positionIds: [],
contracts: [],
formFields: [],
materials: []
});
positions.value = [];
visible.value = true; visible.value = true;
} }
async function openEdit(row: OnboardingTemplate) { async function openEdit(row: OnboardingTemplate) {
await loadBaseData(); await loadBaseData();
const { data } = await onboardingTaskApi.onboardingTemplateDetail({ id: row.id! }); const res = await onboardingTaskApi.onboardingTemplateDetail({ id: row.id! });
Object.assign(form, emptyForm(), data); const detail = res.data;
await loadPositions(form.companyId); Object.assign(form, {
await Promise.all(form.contracts.map(item => loadVersions(item.contractTemplateId))); id: detail.id,
templateCode: detail.templateCode,
templateName: detail.templateName,
companyId: detail.companyId,
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 posRes = await onboardingTaskApi.positionPage({ companyId: form.companyId, pageNum: 1, pageSize: 100, status: "enable" });
positions.value = posRes.data?.list || [];
}
for (const contract of form.contracts) {
if (contract.contractTemplateId) {
await loadContractVersions(contract.contractTemplateId);
}
}
visible.value = true; visible.value = true;
} }
async function save() { async function save() {
if (!form.templateCode.trim() || !form.templateName.trim() || !form.companyId) { if (!form.templateCode || !form.templateName || !form.companyId || !form.positionIds.length) {
return ElMessage.warning("请填写模板编码、模板名称和所属公司"); return ElMessage.warning("请完整填写模板编码、名称、公司及适用岗位");
} }
if (!form.positionIds.length) return ElMessage.warning("请至少选择一个适用岗位");
if (!form.contracts.length || form.contracts.some(item => !item.contractTemplateId || !item.contractTemplateVersionId)) { if (!form.contracts.length || form.contracts.some(item => !item.contractTemplateId || !item.contractTemplateVersionId)) {
return ElMessage.warning("请完整选择至少一份合同模板及版本"); return ElMessage.warning("请完整选择至少一份合同模板及版本");
} }
@@ -396,6 +417,7 @@ async function removeTemplate(row: OnboardingTemplate) {
await ElMessageBox.confirm(`确认删除入职模板“${row.templateName}”吗?`, "删除确认", { type: "warning" }); await ElMessageBox.confirm(`确认删除入职模板“${row.templateName}”吗?`, "删除确认", { type: "warning" });
await onboardingTaskApi.deleteOnboardingTemplate([row.id!]); await onboardingTaskApi.deleteOnboardingTemplate([row.id!]);
ElMessage.success("删除成功"); ElMessage.success("删除成功");
query.pageNum = 1;
await loadTemplates(); await loadTemplates();
} }
@@ -406,48 +428,97 @@ onMounted(async () => {
<style scoped> <style scoped>
.template-panel { .template-panel {
padding-top: 4px; height: 100%;
display: flex;
flex-direction: column;
box-sizing: border-box;
overflow: hidden;
} }
.toolbar { .toolbar {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; gap: 10px;
margin-bottom: 14px; margin-bottom: 10px;
flex-shrink: 0;
} }
.keyword-input { .keyword-input {
width: 260px; width: 220px;
} }
.company-filter { .company-filter {
width: 220px; width: 180px;
} }
.toolbar-spacer { .toolbar-spacer {
flex: 1; flex: 1;
} }
.tip, .tip {
.section-tip { margin-bottom: 10px;
margin-bottom: 14px; flex-shrink: 0;
}
.table-container {
flex: 1;
height: 0;
overflow: hidden;
} }
.pagination { .pagination {
margin-top: 12px;
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
margin-top: 16px; flex-shrink: 0;
} }
.full-width { .full-width {
width: 100%; width: 100%;
} }
.section-action { .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; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
margin-bottom: 10px;
color: var(--el-text-color-secondary);
} }
</style>
.block-title {
font-size: 15px;
font-weight: 700;
color: #0f172a;
margin-right: 10px;
}
.block-subtitle {
font-size: 12px;
color: #64748b;
}
</style>
@@ -0,0 +1,359 @@
<template>
<div class="variable-panel">
<div class="toolbar">
<el-input
v-model="query.keyword"
placeholder="搜索变量编码 / 变量名称"
clearable
style="width: 240px"
@keyup.enter="loadData"
@clear="loadData"
/>
<el-select
v-model="query.variableCategory"
placeholder="变量分类"
clearable
style="width: 180px"
@change="loadData"
>
<el-option label="全部变量分类" value="" />
<el-option label="员工登记表变量 (employee)" value="employee" />
<el-option label="人事合同参数 (hr)" value="hr" />
<el-option label="公司信息变量 (company)" value="company" />
<el-option label="系统预置变量 (system)" value="system" />
</el-select>
<el-button type="primary" icon="Search" @click="loadData">查询</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
<div class="spacer"></div>
<el-button type="primary" icon="Plus" @click="openCreate">新增自定义变量</el-button>
</div>
<el-alert
title="变量字典库定义了合同 DOCX 中所有合法的占位符(如 {{employee.EmployeeName}})。上传 Word 时系统会自动校验占位符是否在此字典中,防止拼写错误。"
type="info"
show-icon
:closable="false"
class="tip-alert"
/>
<div class="table-container">
<el-table :data="tableData" border stripe height="100%" v-loading="loading">
<el-table-column prop="variableCode" label="变量全编码 (DOCX占位符)" min-width="260">
<template #default="{ row }">
<div class="code-col">
<el-tag :type="categoryTag(row.variableCategory)" effect="light" size="small" class="tag-badge">
{{ categoryText(row.variableCategory) }}
</el-tag>
<code class="code-font">{{ formatPlaceholder(row.variableCode) }}</code>
</div>
</template>
</el-table-column>
<el-table-column prop="variableName" label="中文显示名称" min-width="160" />
<el-table-column prop="dataType" label="数据类型" width="100" align="center">
<template #default="{ row }">
<el-tag size="small" type="info">{{ row.dataType || 'string' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="isBuiltIn" label="内置标准" width="120" align="center">
<template #default="{ row }">
<el-tag size="small" :type="row.isBuiltIn ? 'success' : 'warning'">
{{ row.isBuiltIn ? '入职登记表' : '自定义' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="required" label="必填" width="80" align="center">
<template #default="{ row }">
<el-tag size="small" :type="row.required ? 'danger' : 'info'">
{{ row.required ? '是' : '否' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="remark" label="说明备注" min-width="200" show-overflow-tooltip />
<el-table-column label="操作" width="140" align="center" fixed="right">
<template #default="{ row }">
<el-button link type="primary" icon="Edit" @click="openEdit(row)">编辑</el-button>
<el-button
v-if="!row.isBuiltIn"
link
type="danger"
icon="Delete"
@click="remove(row)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div class="pagination-box">
<el-pagination
v-model:current-page="query.pageNum"
v-model:page-size="query.pageSize"
:total="total"
:page-sizes="[20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@size-change="loadData"
@current-change="loadData"
/>
</div>
<!-- 弹窗 -->
<el-dialog v-model="visible" :title="form.id ? '编辑变量' : '新增自定义变量'" width="560px" destroy-on-close>
<el-form ref="formRef" :model="form" :rules="rules" label-width="110px">
<el-form-item label="变量分类" prop="variableCategory" required>
<el-select v-model="form.variableCategory" :disabled="form.isBuiltIn" style="width: 100%">
<el-option label="员工变量 (employee.)" value="employee" />
<el-option label="人事合同参数 (hr.)" value="hr" />
<el-option label="公司信息 (company.)" value="company" />
<el-option label="系统变量 (system.)" value="system" />
</el-select>
</el-form-item>
<el-form-item label="变量全编码" prop="variableCode" required>
<el-input
v-model="form.variableCode"
placeholder="如 employee.WeChat 或 hr.SpecialSubsidy"
:disabled="Boolean(form.id)"
/>
<div class="form-tip">规范格式:分类.变量名,例如 employee.CarNo</div>
</el-form-item>
<el-form-item label="中文名称" prop="variableName" required>
<el-input v-model="form.variableName" placeholder="如 微信号、特殊津贴" />
</el-form-item>
<el-form-item label="数据类型">
<el-select v-model="form.dataType" style="width: 100%">
<el-option label="文本 (string)" value="string" />
<el-option label="数字 / 金额 (number)" value="number" />
<el-option label="日期 (date)" value="date" />
</el-select>
</el-form-item>
<el-form-item label="默认值">
<el-input v-model="form.defaultValue" placeholder="未填写时的默认值" />
</el-form-item>
<el-form-item label="是否必填">
<el-switch v-model="form.required" />
</el-form-item>
<el-form-item label="排序码">
<el-input-number v-model="form.sortCode" :min="1" :max="9999" />
</el-form-item>
<el-form-item label="说明备注">
<el-input v-model="form.remark" type="textarea" :rows="2" placeholder="变量业务说明" />
</el-form-item>
</el-form>
<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 { ref, reactive, onMounted } from "vue";
import { ElMessage, ElMessageBox, FormInstance, FormRules } from "element-plus";
import { onboardingTaskApi } from "@/api/modules/biz/onboarding";
const loading = ref(false);
const saving = ref(false);
const visible = ref(false);
const tableData = ref<any[]>([]);
const total = ref(0);
const formRef = ref<FormInstance>();
const query = reactive({
keyword: "",
variableCategory: "",
pageNum: 1,
pageSize: 50
});
const form = reactive<any>({
id: undefined,
variableCode: "",
variableName: "",
variableCategory: "employee",
dataType: "string",
defaultValue: "",
isBuiltIn: false,
required: false,
sortCode: 100,
remark: ""
});
const rules: FormRules = {
variableCode: [{ required: true, message: "请输入变量编码", trigger: "blur" }],
variableName: [{ required: true, message: "请输入中文显示名称", trigger: "blur" }],
variableCategory: [{ required: true, message: "请选择分类", trigger: "change" }]
};
function categoryTag(cat: string) {
switch (cat) {
case "employee": return "primary";
case "hr": return "warning";
case "company": return "success";
default: return "info";
}
}
function categoryText(cat: string) {
switch (cat) {
case "employee": return "员工变量";
case "hr": return "人事参数";
case "company": return "公司信息";
default: return "系统变量";
}
}
function formatPlaceholder(code?: string) {
return code ? `{{${code}}}` : "";
}
async function loadData() {
loading.value = true;
try {
const res = await onboardingTaskApi.variableDefinitionPage(query);
tableData.value = res.data?.list || [];
total.value = res.data?.total || 0;
} finally {
loading.value = false;
}
}
function resetQuery() {
query.keyword = "";
query.variableCategory = "";
query.pageNum = 1;
loadData();
}
function openCreate() {
Object.assign(form, {
id: undefined,
variableCode: "",
variableName: "",
variableCategory: "employee",
dataType: "string",
defaultValue: "",
isBuiltIn: false,
required: false,
sortCode: 100,
remark: ""
});
visible.value = true;
}
function openEdit(row: any) {
Object.assign(form, {
id: row.id,
variableCode: row.variableCode,
variableName: row.variableName,
variableCategory: row.variableCategory,
dataType: row.dataType || "string",
defaultValue: row.defaultValue || "",
isBuiltIn: row.isBuiltIn || false,
required: row.required || false,
sortCode: row.sortCode || 100,
remark: row.remark || ""
});
visible.value = true;
}
async function save() {
await formRef.value?.validate();
saving.value = true;
try {
if (form.id) {
await onboardingTaskApi.editVariableDefinition(form);
ElMessage.success("变量修改成功");
} else {
await onboardingTaskApi.addVariableDefinition(form);
ElMessage.success("自定义变量添加成功");
}
visible.value = false;
loadData();
} finally {
saving.value = false;
}
}
async function remove(row: any) {
await ElMessageBox.confirm(`确认删除自定义变量“${row.variableCode}”?`, "删除确认", { type: "warning" });
await onboardingTaskApi.deleteVariableDefinitions([row.id]);
ElMessage.success("删除成功");
loadData();
}
onMounted(loadData);
</script>
<style scoped>
.variable-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;
}
.spacer {
flex: 1;
}
.tip-alert {
margin-bottom: 10px;
flex-shrink: 0;
}
.table-container {
flex: 1;
height: 0;
overflow: hidden;
}
.code-col {
display: inline-flex;
align-items: center;
gap: 6px;
white-space: nowrap;
}
.tag-badge {
flex-shrink: 0;
}
.code-font {
font-family: Consolas, Monaco, monospace;
font-weight: 600;
color: #b45309;
background: #fef3c7;
padding: 2px 6px;
border-radius: 4px;
white-space: nowrap;
}
.pagination-box {
display: flex;
justify-content: flex-end;
margin-top: 12px;
flex-shrink: 0;
}
.form-tip {
font-size: 12px;
color: #94a3b8;
margin-top: 4px;
}
</style>
+72 -187
View File
@@ -1,58 +1,24 @@
<template> <template>
<div class="main-box config-container"> <div class="main-box config-container">
<el-tabs v-model="activeTab" class="config-tabs"> <el-tabs v-model="activeTab" class="config-tabs">
<el-tab-pane label="动态填写字段" name="fields"> <el-tab-pane label="合同变量字典" name="variables">
<div class="tab-card-wrapper"> <div class="tab-card-wrapper">
<div class="toolbar"> <VariableDefinitionPanel />
<el-button type="primary" icon="Plus" @click="openFieldCreate">
新增字段
</el-button>
</div>
<el-table :data="fields" border stripe>
<el-table-column prop="fieldCode" label="字段编码" min-width="140" />
<el-table-column prop="fieldName" label="字段名称" min-width="150" />
<el-table-column prop="dataType" label="数据类型" width="120" align="center">
<template #default="scope">
<el-tag size="small" type="info">{{ scope.row.dataType }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="placeholder" label="提示语" min-width="160" />
<el-table-column prop="status" label="状态" width="100" align="center">
<template #default="scope">
<el-tag size="small" :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" icon="Edit" @click="openFieldEdit(scope.row)">
编辑
</el-button>
<el-button link type="danger" icon="Delete" @click="removeField(scope.row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
</div> </div>
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="上传资料类型" name="materials"> <el-tab-pane label="上传资料类型" name="materials">
<div class="tab-card-wrapper"> <div class="tab-card-wrapper material-tab-wrapper">
<div class="toolbar"> <div class="toolbar">
<el-button type="primary" icon="Plus" @click="openMaterialCreate"> <el-button type="primary" icon="Plus" @click="openMaterialCreate">
新增资料类型 新增资料类型
</el-button> </el-button>
</div> </div>
<el-table :data="materialTypes" border stripe> <el-table :data="materialTypes" border stripe height="100%">
<el-table-column prop="materialCode" label="资料编码" min-width="140" /> <el-table-column prop="materialCode" label="资料编码" min-width="140" />
<el-table-column prop="materialName" label="资料名称" min-width="150" /> <el-table-column prop="materialName" label="资料名称" min-width="150" />
<el-table-column prop="allowedExtensions" label="允许扩展名" min-width="140"> <el-table-column prop="allowedExtensions" label="允许扩展名" min-width="160">
<template #default="scope"> <template #default="scope">
<code>{{ scope.row.allowedExtensions || '不限' }}</code> <code>{{ scope.row.allowedExtensions || '不限' }}</code>
</template> </template>
@@ -67,7 +33,7 @@
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" width="150" fixed="right"> <el-table-column label="操作" width="150" fixed="right" align="center">
<template #default="scope"> <template #default="scope">
<el-button link type="primary" icon="Edit" @click="openMaterialEdit(scope.row)"> <el-button link type="primary" icon="Edit" @click="openMaterialEdit(scope.row)">
编辑 编辑
@@ -89,60 +55,9 @@
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
<el-dialog v-model="fieldVisible" :title="fieldForm.id ? '编辑字段' : '新增字段'" width="560px" destroy-on-close> <!-- 资料类型弹窗 -->
<el-form :model="fieldForm" label-width="110px">
<el-form-item label="字段编码" required>
<el-input v-model="fieldForm.fieldCode" />
</el-form-item>
<el-form-item label="字段名称" required>
<el-input v-model="fieldForm.fieldName" />
</el-form-item>
<el-form-item label="数据类型" required>
<el-select v-model="fieldForm.dataType">
<el-option label="文本" value="string" />
<el-option label="日期" value="date" />
<el-option label="整数" value="int" />
<el-option label="数字" value="number" />
<el-option label="金额" value="decimal" />
<el-option label="下拉选择" value="select" />
</el-select>
</el-form-item>
<el-form-item label="提示语">
<el-input v-model="fieldForm.placeholder" />
</el-form-item>
<el-form-item label="选项JSON">
<el-input v-model="fieldForm.optionsJson" type="textarea" placeholder="下拉/多选字段可填写选项JSON" />
</el-form-item>
<el-form-item label="校验规则">
<el-input v-model="fieldForm.validationRule" />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="fieldForm.status">
<el-option label="启用" value="enable" />
<el-option label="停用" value="disable" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="fieldVisible = false">
取消
</el-button>
<el-button type="primary" @click="saveField">
保存
</el-button>
</template>
</el-dialog>
<el-dialog v-model="materialVisible" :title="materialForm.id ? '编辑资料类型' : '新增资料类型'" width="560px" destroy-on-close> <el-dialog v-model="materialVisible" :title="materialForm.id ? '编辑资料类型' : '新增资料类型'" width="560px" destroy-on-close>
<el-form :model="materialForm" label-width="120px"> <el-form :model="materialForm" label-width="110px">
<el-form-item label="资料编码" required> <el-form-item label="资料编码" required>
<el-input v-model="materialForm.materialCode" /> <el-input v-model="materialForm.materialCode" />
</el-form-item> </el-form-item>
@@ -152,110 +67,72 @@
</el-form-item> </el-form-item>
<el-form-item label="允许扩展名"> <el-form-item label="允许扩展名">
<el-input v-model="materialForm.allowedExtensions" placeholder="如:jpg,png,pdf" /> <el-input v-model="materialForm.allowedExtensions" placeholder="逗号分隔,如:png,jpg,jpeg,pdf" />
</el-form-item> </el-form-item>
<el-form-item label="最大文件数量"> <el-form-item label="最大文件数">
<el-input-number v-model="materialForm.maxFileCount" :min="1" /> <el-input-number v-model="materialForm.maxFileCount" :min="1" :max="10" />
</el-form-item> </el-form-item>
<el-form-item label="最大大小(MB)"> <el-form-item label="单文件大小(MB)">
<el-input-number v-model="materialForm.maxFileSizeMb" :min="1" /> <el-input-number v-model="materialForm.maxFileSizeMb" :min="1" :max="100" />
</el-form-item> </el-form-item>
<el-form-item label="状态"> <el-form-item label="状态">
<el-select v-model="materialForm.status"> <el-radio-group v-model="materialForm.status">
<el-option label="启用" value="enable" /> <el-radio label="enable">启用</el-radio>
<el-option label="停用" value="disable" /> <el-radio label="disable">停用</el-radio>
</el-select> </el-radio-group>
</el-form-item> </el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
<el-button @click="materialVisible = false"> <el-button @click="materialVisible = false">取消</el-button>
取消 <el-button type="primary" @click="saveMaterial">保存</el-button>
</el-button>
<el-button type="primary" @click="saveMaterial">
保存
</el-button>
</template> </template>
</el-dialog> </el-dialog>
</div> </div>
</template> </template>
<script setup lang="ts" name="onboardingEntryConfig"> <script setup lang="ts" name="onboardingEntryConfig">
import { onMounted, reactive, ref } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { import {
onboardingTaskApi, OnboardingMaterialType,
type OnboardingFormField, onboardingTaskApi
type OnboardingMaterialType
} from "@/api/modules/biz/onboarding"; } from "@/api/modules/biz/onboarding";
import OnboardingTemplatePanel from "./components/OnboardingTemplatePanel.vue"; import OnboardingTemplatePanel from "./components/OnboardingTemplatePanel.vue";
import VariableDefinitionPanel from "./components/VariableDefinitionPanel.vue";
const activeTab = ref("fields"); const activeTab = ref("variables");
const fields = ref<OnboardingFormField[]>([]);
const materialTypes = ref<OnboardingMaterialType[]>([]); const materialTypes = ref<OnboardingMaterialType[]>([]);
const fieldVisible = ref(false);
const materialVisible = ref(false); const materialVisible = ref(false);
const fieldForm = reactive<Partial<OnboardingFormField>>({});
const materialForm = reactive<Partial<OnboardingMaterialType>>({});
async function loadBase() { const materialForm = reactive<Partial<OnboardingMaterialType>>({
const [fieldResult, materialResult] = await Promise.all([ materialCode: "",
onboardingTaskApi.listFormFields(), materialName: "",
onboardingTaskApi.listMaterialTypes() allowedExtensions: "png,jpg,jpeg,pdf",
]); maxFileCount: 3,
fields.value = fieldResult.data || []; maxFileSizeMb: 10,
materialTypes.value = materialResult.data || []; status: "enable"
});
async function loadMaterials() {
const res = await onboardingTaskApi.listMaterialTypes();
materialTypes.value = res.data || [];
} }
function resetField() { function openMaterialCreate() {
Object.assign(fieldForm, {
id: undefined,
fieldCode: "",
fieldName: "",
dataType: "string",
placeholder: "",
optionsJson: "",
validationRule: "",
status: "enable"
});
}
function resetMaterial() {
Object.assign(materialForm, { Object.assign(materialForm, {
id: undefined, id: undefined,
materialCode: "", materialCode: "",
materialName: "", materialName: "",
allowedExtensions: "", allowedExtensions: "png,jpg,jpeg,pdf",
maxFileCount: 1, maxFileCount: 3,
maxFileSizeMb: undefined, maxFileSizeMb: 10,
status: "enable" status: "enable"
}); });
}
function openFieldCreate() {
resetField();
fieldVisible.value = true;
}
function openFieldEdit(row: OnboardingFormField) {
Object.assign(fieldForm, row);
fieldVisible.value = true;
}
async function saveField() {
if (!fieldForm.fieldCode || !fieldForm.fieldName || !fieldForm.dataType) {
return ElMessage.warning("请填写字段编码、名称和数据类型");
}
await onboardingTaskApi.saveFormField(fieldForm);
fieldVisible.value = false;
ElMessage.success("保存成功");
await loadBase();
}
function openMaterialCreate() {
resetMaterial();
materialVisible.value = true; materialVisible.value = true;
} }
@@ -266,29 +143,24 @@ function openMaterialEdit(row: OnboardingMaterialType) {
async function saveMaterial() { async function saveMaterial() {
if (!materialForm.materialCode || !materialForm.materialName) { if (!materialForm.materialCode || !materialForm.materialName) {
return ElMessage.warning("请填写资料编码和名称"); return ElMessage.warning("请填写资料编码和资料名称");
} }
await onboardingTaskApi.saveMaterialType(materialForm); await onboardingTaskApi.saveMaterialType(materialForm);
ElMessage.success("资料类型保存成功");
materialVisible.value = false; materialVisible.value = false;
ElMessage.success("保存成功"); await loadMaterials();
await loadBase();
}
async function removeField(row: OnboardingFormField) {
await ElMessageBox.confirm(`确认删除字段“${row.fieldName}”?`, "删除确认");
await onboardingTaskApi.deleteFormFields([row.id]);
ElMessage.success("删除成功");
await loadBase();
} }
async function removeMaterial(row: OnboardingMaterialType) { async function removeMaterial(row: OnboardingMaterialType) {
await ElMessageBox.confirm(`确认删除资料类型“${row.materialName}”?`, "删除确认"); await ElMessageBox.confirm(`确认删除资料类型“${row.materialName}”吗?`, "删除确认", { type: "warning" });
await onboardingTaskApi.deleteMaterialTypes([row.id]); await onboardingTaskApi.deleteMaterialTypes([row.id!]);
ElMessage.success("删除成功"); ElMessage.success("删除成功");
await loadBase(); await loadMaterials();
} }
onMounted(loadBase); onMounted(() => {
loadMaterials();
});
</script> </script>
<style scoped> <style scoped>
@@ -318,21 +190,34 @@ onMounted(loadBase);
.config-tabs :deep(.el-tabs__content) { .config-tabs :deep(.el-tabs__content) {
flex: 1; flex: 1;
overflow: auto; height: 0;
overflow: hidden;
}
.config-tabs :deep(.el-tab-pane) {
height: 100%;
display: flex;
flex-direction: column;
} }
.tab-card-wrapper { .tab-card-wrapper {
height: 100%;
width: 100%;
background: #fff; background: #fff;
border-radius: 8px; border-radius: 8px;
padding: 16px; padding: 16px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); box-sizing: border-box;
display: flex;
flex-direction: column;
overflow: hidden;
}
.material-tab-wrapper {
display: flex;
flex-direction: column;
} }
.toolbar { .toolbar {
margin-bottom: 14px; margin-bottom: 12px;
}
.hint {
margin-bottom: 14px;
} }
</style> </style>
File diff suppressed because it is too large Load Diff