This commit is contained in:
fuenmao
2026-08-28 17:32:47 +08:00
parent 14515ccdd6
commit e2697b4bdb
9 changed files with 249 additions and 174 deletions
+2 -2
View File
@@ -25,10 +25,10 @@ VITE_PWA = true
VITE_HTTP_PROXY = true
# 线上环境接口地址
VITE_API_URL = http://medical.23544.com:7106/
VITE_API_URL = https://onboardx-api.cnzhyx.cn/
# 开发环境跨域代理,支持配置多个
VITE_PROXY = [["/api","http://medical.23544.com:7106/"]]
VITE_PROXY = [["/api","https://onboardx-api.cnzhyx.cn/"]]
#是否启用mqtt
VITE_MQTT = false
+3 -1
View File
@@ -145,13 +145,15 @@ export interface OnboardingEmployee {
employeeNo?: string;
employeeName: string;
companyId: number;
companyName?: string;
departmentId?: number;
departmentName?: string;
positionId: number;
positionName?: string;
phone: string;
idCardNumber: string;
entryDate?: string;
createTime: string;
createTime?: string;
}
export interface OnboardingFormField {
+21 -21
View File
@@ -2,13 +2,13 @@
<div class="table-box">
<ProTable
ref="templateTable"
title="合同模板列表"
title="签署材料模板列表"
:columns="templateColumns"
:request-api="onboardingTaskApi.templatePage"
>
<template #tableHeader>
<el-button type="primary" icon="Plus" @click="openTemplateCreate">
新增合同模板
新增签署材料模板
</el-button>
</template>
@@ -60,10 +60,10 @@
</template>
</ProTable>
<!-- 新增 / 编辑合同模板基础信息弹窗 -->
<!-- 新增 / 编辑签署材料模板基础信息弹窗 -->
<el-dialog
v-model="templateVisible"
:title="templateEditing ? '编辑合同模板' : '新增合同模板'"
:title="templateEditing ? '编辑签署材料模板' : '新增签署材料模板'"
width="720px"
destroy-on-close
append-to-body
@@ -112,7 +112,7 @@
collapse-tags
collapse-tags-tooltip
class="w-full"
placeholder="请选择该合同可适用的岗位"
placeholder="请选择该签署材料可适用的岗位"
:disabled="!templateForm.departmentId"
>
<el-option
@@ -132,7 +132,7 @@
<el-input v-model="templateForm.templateName" placeholder="如:标准全职劳动合同" />
</el-form-item>
<el-form-item label="合同类型" required>
<el-form-item label="材料类型" required>
<el-input v-model="templateForm.contractType" placeholder="如:劳动合同、保密协议、竞业协议" />
</el-form-item>
@@ -144,7 +144,7 @@
</el-form-item>
<el-form-item label="模板说明">
<el-input v-model="templateForm.description" type="textarea" :rows="3" placeholder="合同模板适用说明或签署注意事项" />
<el-input v-model="templateForm.description" type="textarea" :rows="3" placeholder="签署材料模板适用说明或签署注意事项" />
</el-form-item>
</el-form>
@@ -154,10 +154,10 @@
</template>
</el-dialog>
<!-- 一站式合同版本与配置大抽屉 -->
<!-- 一站式签署材料版本与配置大抽屉 -->
<el-drawer
v-model="drawerVisible"
:title="`合同模板管理 · ${selectedTemplate?.templateName || ''}`"
:title="`签署材料模板管理 · ${selectedTemplate?.templateName || ''}`"
size="1150px"
destroy-on-close
>
@@ -188,7 +188,7 @@
上传新版本 DOCX
</el-button>
<span class="pane-tip-text">
支持多版本迭代。启用某个版本后,该版本的 DOCX 将用于生成新入职员工的合同。
支持多版本迭代。启用某个版本后,该版本的 DOCX 将用于生成新入职员工的签署材料。
</span>
</div>
@@ -274,7 +274,7 @@
</template>
</el-drawer>
<!-- 上传合同 DOCX 版本弹窗 -->
<!-- 上传签署材料 DOCX 版本弹窗 -->
<el-dialog
v-model="versionVisible"
title="上传新版本 DOCX"
@@ -334,7 +334,7 @@
<!-- 变量配置弹窗 -->
<el-dialog
v-model="variablesVisible"
:title="`合同变量配置 · 版本 ${selectedVersion?.versionNo || ''}`"
:title="`签署材料变量配置 · 版本 ${selectedVersion?.versionNo || ''}`"
width="1220px"
top="4vh"
destroy-on-close
@@ -353,7 +353,7 @@
<el-tabs v-model="variableTab" type="card" class="variable-filter-tabs">
<el-tab-pane :label="`全部变量 (${variableCounts.all})`" name="all" />
<el-tab-pane :label="`员工填写 (${variableCounts.employee})`" name="employee" />
<el-tab-pane :label="`人事合同参数 (${variableCounts.hr})`" name="hr" />
<el-tab-pane :label="`人事预设参数 (${variableCounts.hr})`" name="hr" />
<el-tab-pane :label="`公司信息 (${variableCounts.company})`" name="company" />
<el-tab-pane :label="`系统预置 (${variableCounts.system})`" name="system" />
</el-tabs>
@@ -439,7 +439,7 @@
<el-select v-model="variableForm.variableType" class="w-full">
<el-option label="员工填写(从新员工基本信息/动态字段读取)" value="employee" />
<el-option label="公司信息(从公司档案自动读取)" value="company" />
<el-option label="合同/人事填写(人事发起时录入)" value="contract" />
<el-option label="人事预设填写(人事发起时录入)" value="contract" />
<el-option label="系统生成(签署日期等)" value="system" />
</el-select>
</el-form-item>
@@ -630,7 +630,7 @@ const templateColumns: ColumnProps<OnboardingContractTemplate>[] = [
},
{ prop: "templateCode", label: "模板编码", minWidth: 160, search: { el: "input" } },
{ prop: "templateName", label: "模板名称", minWidth: 200, search: { el: "input" } },
{ prop: "contractType", label: "合同类型", width: 140, search: { el: "input" } },
{ prop: "contractType", label: "材料类型", width: 140, search: { el: "input" } },
{
prop: "status",
label: "状态",
@@ -738,7 +738,7 @@ async function saveTemplate() {
|| !templateForm.templateName
|| !templateForm.contractType
) {
return ElMessage.warning("请完整填写公司、部门、适用岗位及合同模板基本信息");
return ElMessage.warning("请完整填写公司、部门、适用岗位及签署材料模板基本信息");
}
await onboardingTaskApi.saveTemplate(templateForm, templateEditing.value);
templateVisible.value = false;
@@ -747,7 +747,7 @@ async function saveTemplate() {
}
async function removeTemplate(row: OnboardingContractTemplate) {
await ElMessageBox.confirm(`确认删除合同模板“${row.templateName}”?`, "删除确认", { type: "warning" });
await ElMessageBox.confirm(`确认删除签署材料模板“${row.templateName}”?`, "删除确认", { type: "warning" });
await onboardingTaskApi.deleteTemplate([row.id]);
templateTable.value?.refresh();
ElMessage.success("删除成功");
@@ -760,7 +760,7 @@ function openVersionCreate() {
function beforeDocxUpload(file: File) {
if (!file.name.toLowerCase().endsWith(".docx")) {
ElMessage.error("合同模板仅支持 DOCX 文件");
ElMessage.error("签署材料模板仅支持 DOCX 文件");
return false;
}
return true;
@@ -796,7 +796,7 @@ async function changeVersionStatus(row: OnboardingContractVersion, status: strin
}
async function removeVersion(row: OnboardingContractVersion) {
await ElMessageBox.confirm(`确认删除合同版本“${row.versionNo}”?此操作不可撤销。`, "删除确认", { type: "warning" });
await ElMessageBox.confirm(`确认删除签署材料版本“${row.versionNo}”?此操作不可撤销。`, "删除确认", { type: "warning" });
await onboardingTaskApi.deleteVersion(row.id);
await loadVersions();
ElMessage.success("版本删除成功");
@@ -819,7 +819,7 @@ async function downloadDocx(fileId?: number) {
const encodedName =
disposition.match(/filename\*=UTF-8\'\'([^;]+)/i)?.[1] ||
disposition.match(/filename="?([^";]+)"?/i)?.[1];
let fileName = "合同模板.docx";
let fileName = "签署材料模板.docx";
if (encodedName) {
try {
fileName = decodeURIComponent(encodedName.replace(/\+/g, "%20"));
@@ -870,7 +870,7 @@ function variableTypeText(type: string) {
const map: Record<string, string> = {
employee: "员工填写",
company: "公司信息",
contract: "人事合同参数",
contract: "人事预设参数",
system: "系统自动",
unresolved: "待确认"
};
+160 -87
View File
@@ -1,43 +1,32 @@
<template>
<div class="table-box">
<ProTable
ref="proTable"
title="正式员工档案"
:columns="columns"
:request-api="onboardingTaskApi.employeePage"
>
<ProTable ref="proTable" title="正式员工档案" :columns="columns" :request-api="onboardingTaskApi.employeePage">
<template #operation="scope">
<el-button link type="primary" icon="UserFilled" @click="showDetail(scope.row)">
查看完整档案
</el-button>
<el-button link type="primary" icon="UserFilled" @click="showDetail(scope.row)"> 查看完整档案 </el-button>
</template>
</ProTable>
<!-- 员工完整档案抽屉 -->
<el-drawer
v-model="detailVisible"
:title="`员工档案详情 · ${detail?.employee?.employeeName || ''}`"
size="820px"
destroy-on-close
>
<el-drawer v-model="detailVisible" :title="`员工档案详情 · ${detail?.employee?.employeeName || ''}`" size="820px" destroy-on-close>
<div v-if="detail?.employee" class="employee-drawer-content">
<!-- 头部个人简要信息 -->
<div class="employee-header-card">
<el-avatar :size="64" class="employee-avatar">
{{ detail.employee.employeeName ? detail.employee.employeeName.substring(0, 1) : '员' }}
{{ detail.employee.employeeName ? detail.employee.employeeName.substring(0, 1) : "员" }}
</el-avatar>
<div class="employee-header-info">
<div class="employee-name-row">
<span class="employee-name">{{ detail.employee.employeeName }}</span>
<el-tag size="small" :type="detail.employee.gender === '女' ? 'danger' : 'primary'" effect="plain">
{{ detail.employee.gender || '未知' }}
{{ detail.employee.gender || "未知" }}
</el-tag>
<el-tag size="small" type="success">正式员工</el-tag>
</div>
<div class="employee-meta-row">
<span>员工编号:{{ detail.employee.employeeNo || '-' }}</span>
<span>联系电话:{{ detail.employee.phone }}</span>
<span>入职日期:{{ detail.employee.entryDate || '-' }}</span>
<span>所属公司:{{ detail.employee.companyName || "-" }}</span>
<span>所属部门:{{ detail.employee.departmentName || "-" }}</span>
<span>所属岗位:{{ detail.employee.positionName || "-" }}</span>
<span>入职日期:{{ formatDate(detail.employee.entryDate) }}</span>
</div>
</div>
</div>
@@ -46,38 +35,64 @@
<!-- 1. 基本人事档案 -->
<el-tab-pane label="基本人事档案" name="basic">
<el-card shadow="never" class="section-card">
<template #header>
<div class="card-header-title">身份与联系信息</div>
</template>
<template #header><div class="card-header-title">个人基本信息</div></template>
<el-descriptions :column="2" border>
<el-descriptions-item label="姓名">{{ detail.employee.employeeName }}</el-descriptions-item>
<el-descriptions-item label="性别">{{ detail.employee.gender || '-' }}</el-descriptions-item>
<el-descriptions-item label="联系电话">{{ detail.employee.phone }}</el-descriptions-item>
<el-descriptions-item label="身份证号码">{{ detail.employee.idCardNumber }}</el-descriptions-item>
<el-descriptions-item label="出生日期">{{ detail.employee.birthDate || '-' }}</el-descriptions-item>
<el-descriptions-item label="民族">{{ detail.employee.nation || '-' }}</el-descriptions-item>
<el-descriptions-item label="户口所在地" :span="2">
{{ detail.employee.registeredResidence || '-' }}
</el-descriptions-item>
<el-descriptions-item label="现居通讯地址" :span="2">
{{ detail.employee.contactAddress || '-' }}
</el-descriptions-item>
<el-descriptions-item label="姓名">{{ employeeInfo.employeeName || detail.employee.employeeName }}</el-descriptions-item>
<el-descriptions-item label="性别">{{ employeeInfo.gender || detail.employee.gender || "-" }}</el-descriptions-item>
<el-descriptions-item label="民族">{{ employeeInfo.nation || detail.employee.nation || "-" }}</el-descriptions-item>
<el-descriptions-item label="出生日期">{{ employeeInfo.birthday || formatDate(detail.employee.birthDate) }}</el-descriptions-item>
<el-descriptions-item label="身高">{{ employeeInfo.height ? `${employeeInfo.height} cm` : "-" }}</el-descriptions-item>
<el-descriptions-item label="体重">{{ employeeInfo.weight ? `${employeeInfo.weight} kg` : "-" }}</el-descriptions-item>
<el-descriptions-item label="健康状况">{{ employeeInfo.healthCondition || "-" }}</el-descriptions-item>
<el-descriptions-item label="婚育情况">{{ employeeInfo.maritalStatus || "-" }}</el-descriptions-item>
<el-descriptions-item label="联系电话">{{ employeeInfo.phone || detail.employee.phone || "-" }}</el-descriptions-item>
<el-descriptions-item label="电子邮箱">{{ employeeInfo.email || "-" }}</el-descriptions-item>
<el-descriptions-item label="身份证号码" :span="2">{{
employeeInfo.idCardNumber || detail.employee.idCardNumber || "-"
}}</el-descriptions-item>
<el-descriptions-item label="户口所在地" :span="2">{{
employeeInfo.registeredResidence || detail.employee.registeredResidence || "-"
}}</el-descriptions-item>
<el-descriptions-item label="现居通讯地址" :span="2">{{
employeeInfo.currentAddress || detail.employee.contactAddress || "-"
}}</el-descriptions-item>
<el-descriptions-item label="房东联系方式" :span="2">{{ employeeInfo.landlordContact || "-" }}</el-descriptions-item>
</el-descriptions>
</el-card>
<el-card shadow="never" class="section-card mt-4">
<template #header>
<div class="card-header-title">学历与就职信息</div>
</template>
<template #header><div class="card-header-title">学历、资质与特长</div></template>
<el-descriptions :column="2" border>
<el-descriptions-item label="毕业院校">{{ detail.employee.college || '-' }}</el-descriptions-item>
<el-descriptions-item label="最高学历">{{ detail.employee.education || '-' }}</el-descriptions-item>
<el-descriptions-item label="所学专业">{{ detail.employee.major || '-' }}</el-descriptions-item>
<el-descriptions-item label="部门名称">{{ detail.employee.departmentName || '-' }}</el-descriptions-item>
<el-descriptions-item label="合同社保单位">{{ detail.employee.socialInsuranceCompany || '-' }}</el-descriptions-item>
<el-descriptions-item label="合同性质">{{ detail.employee.contractNature || '全职劳动合同' }}</el-descriptions-item>
<el-descriptions-item label="合同开始日期">{{ detail.employee.contractStartDate || '-' }}</el-descriptions-item>
<el-descriptions-item label="合同终止日期">{{ detail.employee.contractEndDate || '-' }}</el-descriptions-item>
<el-descriptions-item label="最高学历">{{ employeeInfo.education || detail.employee.education || "-" }}</el-descriptions-item>
<el-descriptions-item label="所学专业">{{ employeeInfo.major || detail.employee.major || "-" }}</el-descriptions-item>
<el-descriptions-item label="毕业院校" :span="2">{{
employeeInfo.graduatedSchool || detail.employee.college || "-"
}}</el-descriptions-item>
<el-descriptions-item label="职业资格">{{ employeeInfo.vocationalQualification || "-" }}</el-descriptions-item>
<el-descriptions-item label="资格等级">{{ employeeInfo.qualificationLevel || "-" }}</el-descriptions-item>
<el-descriptions-item label="技术特长" :span="2">{{ employeeInfo.technicalSpecialty || "-" }}</el-descriptions-item>
</el-descriptions>
</el-card>
<el-card shadow="never" class="section-card mt-4">
<template #header><div class="card-header-title">任职与签署材料信息</div></template>
<el-descriptions :column="2" border>
<el-descriptions-item label="所属公司">{{ detail.employee.companyName || "-" }}</el-descriptions-item>
<el-descriptions-item label="所属部门">{{
detail.employee.departmentName || employeeInfo.onboardingDepartment || "-"
}}</el-descriptions-item>
<el-descriptions-item label="所属岗位">{{
detail.employee.positionName || employeeInfo.onboardingPosition || "-"
}}</el-descriptions-item>
<el-descriptions-item label="员工编号">{{ detail.employee.employeeNo || employeeInfo.employeeNo || "-" }}</el-descriptions-item>
<el-descriptions-item label="入职日期">{{ formatDate(detail.employee.entryDate) }}</el-descriptions-item>
<el-descriptions-item label="社保缴纳单位">{{ detail.employee.socialInsuranceCompany || "-" }}</el-descriptions-item>
<el-descriptions-item label="聘用/材料性质">{{ detail.employee.contractNature || "全职劳动合同" }}</el-descriptions-item>
<el-descriptions-item label="签署材料开始日期">{{ formatDate(detail.employee.contractStartDate) }}</el-descriptions-item>
<el-descriptions-item label="签署材料终止日期">{{ formatDate(detail.employee.contractEndDate) }}</el-descriptions-item>
<el-descriptions-item label="其他说明" :span="2">{{
employeeInfo.otherRemarks || detail.employee.remark || "-"
}}</el-descriptions-item>
</el-descriptions>
</el-card>
@@ -86,16 +101,60 @@
<div class="card-header-title">工资卡账户信息</div>
</template>
<el-descriptions :column="2" border>
<el-descriptions-item label="银行卡号">{{ detail.employee.bankCardNumber || '-' }}</el-descriptions-item>
<el-descriptions-item label="银行卡号">{{ detail.employee.bankCardNumber || "-" }}</el-descriptions-item>
<el-descriptions-item label="开户人姓名">{{ detail.employee.bankAccountName || detail.employee.employeeName }}</el-descriptions-item>
<el-descriptions-item label="工资卡开户行" :span="2">{{ detail.employee.bankName || '-' }}</el-descriptions-item>
<el-descriptions-item label="工资卡开户行" :span="2">{{ detail.employee.bankName || "-" }}</el-descriptions-item>
</el-descriptions>
</el-card>
<el-card shadow="never" class="section-card mt-4">
<template #header><div class="card-header-title">紧急联系人</div></template>
<el-descriptions :column="2" border>
<el-descriptions-item label="联系人姓名">{{ employeeInfo.emergencyContact || "-" }}</el-descriptions-item>
<el-descriptions-item label="联系电话">{{ employeeInfo.emergencyPhone || "-" }}</el-descriptions-item>
<el-descriptions-item label="联系地址" :span="2">{{ employeeInfo.emergencyAddress || "-" }}</el-descriptions-item>
</el-descriptions>
</el-card>
<el-card shadow="never" class="section-card mt-4">
<template #header><div class="card-header-title">教育学习经历</div></template>
<el-empty v-if="!educationHistory.length" description="暂无教育学习经历" :image-size="70" />
<el-table v-else :data="educationHistory" border size="small">
<el-table-column prop="timeRange" label="时间" min-width="140" />
<el-table-column prop="location" label="学校/地点" min-width="150" />
<el-table-column prop="content" label="学习内容" min-width="180" />
<el-table-column prop="result" label="学历/结果" min-width="120" />
</el-table>
</el-card>
<el-card shadow="never" class="section-card mt-4">
<template #header><div class="card-header-title">主要工作经历</div></template>
<el-empty v-if="!workHistory.length" description="暂无工作经历" :image-size="70" />
<el-table v-else :data="workHistory" border size="small">
<el-table-column prop="timeRange" label="时间" min-width="140" />
<el-table-column prop="companyName" label="工作单位" min-width="160" />
<el-table-column prop="position" label="岗位" min-width="120" />
<el-table-column prop="salary" label="薪资" min-width="100" />
<el-table-column prop="leaveReason" label="离职原因" min-width="160" />
</el-table>
</el-card>
<el-card shadow="never" class="section-card mt-4">
<template #header><div class="card-header-title">家庭成员</div></template>
<el-empty v-if="!familyMembers.length" description="暂无家庭成员信息" :image-size="70" />
<el-table v-else :data="familyMembers" border size="small">
<el-table-column prop="name" label="姓名" min-width="100" />
<el-table-column prop="relationship" label="关系" min-width="100" />
<el-table-column prop="age" label="年龄" min-width="80" />
<el-table-column prop="company" label="工作单位" min-width="160" />
<el-table-column prop="phone" label="联系电话" min-width="140" />
</el-table>
</el-card>
</el-tab-pane>
<!-- 2. 归档合同与协议 -->
<el-tab-pane label="归档合同协议" name="contracts">
<el-empty v-if="!detail.contracts?.length" description="暂无已归档的劳动合同" />
<el-tab-pane label="归档签署材料" name="contracts">
<el-empty v-if="!detail.contracts?.length" description="暂无已归档的签署材料" />
<div v-else class="contracts-list">
<div v-for="contract in detail.contracts" :key="contract.id" class="contract-box">
<div class="contract-info-left">
@@ -112,9 +171,14 @@
plain
size="small"
icon="Download"
@click="download(contract.generatedDocxFileId, `${detail?.employee?.employeeName || '员工'}_${contract.contractName || '入职合同'}.docx`)"
@click="
download(
contract.generatedDocxFileId,
`${detail?.employee?.employeeName || '员工'}_${contract.contractName || '入职合同'}.docx`
)
"
>
下载合同 DOCX
下载签署材料 DOCX
</el-button>
<el-tag v-else type="info">尚未生成电子文件</el-tag>
</div>
@@ -127,21 +191,13 @@
<el-empty v-if="!detail.audits?.length" description="暂无流转记录" />
<div v-else class="timeline-box">
<el-timeline>
<el-timeline-item
v-for="item in detail.audits"
:key="item.id"
type="primary"
:timestamp="formatTime(item.auditedAt)"
placement="top"
>
<el-timeline-item v-for="item in detail.audits" :key="item.id" type="primary" :timestamp="formatTime(item.auditedAt)" placement="top">
<div class="audit-card">
<div class="audit-action-name font-bold">
{{ auditActionText(item.auditAction) }}
<span v-if="item.createUser" class="audit-operator">(操作人:{{ item.createUser }})</span>
</div>
<div v-if="item.auditOpinion" class="audit-opinion">
审核意见:{{ item.auditOpinion }}
</div>
<div v-if="item.auditOpinion" class="audit-opinion">审核意见:{{ item.auditOpinion }}</div>
</div>
</el-timeline-item>
</el-timeline>
@@ -157,25 +213,33 @@
</template>
<script setup lang="ts" name="onboardingEmployee">
import { ref } from "vue";
import { computed, ref } from "vue";
import { UserFilled, Download } from "@element-plus/icons-vue";
import { ElMessage } from "element-plus";
import {
onboardingTaskApi,
type OnboardingEmployee
} from "@/api/modules/biz/onboarding";
import { onboardingTaskApi, type OnboardingEmployee } from "@/api/modules/biz/onboarding";
import type { ColumnProps } from "@/components/ProTable/interface";
const detailVisible = ref(false);
const activeTab = ref("basic");
const detail = ref<any>();
const employeeInfo = computed<Record<string, any>>(() => detail.value?.employeeInfo || {});
function parseJsonList(value?: string) {
if (!value) return [];
try {
const result = JSON.parse(value);
return Array.isArray(result) ? result : [];
} catch {
return [];
}
}
const educationHistory = computed(() => parseJsonList(employeeInfo.value.educationHistoryJson));
const workHistory = computed(() => parseJsonList(employeeInfo.value.workHistoryJson));
const familyMembers = computed(() => parseJsonList(employeeInfo.value.familyMembersJson));
const columns: ColumnProps<OnboardingEmployee>[] = [
{
prop: "employeeNo",
label: "员工编号",
minWidth: 160
},
{
prop: "employeeName",
label: "姓名",
@@ -189,10 +253,9 @@ const columns: ColumnProps<OnboardingEmployee>[] = [
align: "center"
},
{
prop: "phone",
label: "手机号码",
minWidth: 160,
search: { el: "input" }
prop: "companyName",
label: "所属公司",
minWidth: 180
},
{
prop: "departmentName",
@@ -200,15 +263,21 @@ const columns: ColumnProps<OnboardingEmployee>[] = [
minWidth: 140
},
{
prop: "entryDate",
label: "入职日期",
prop: "positionName",
label: "所属岗位",
minWidth: 140
},
{
prop: "createTime",
label: "建档时间",
minWidth: 180,
render: scope => formatTime(scope.row.createTime)
prop: "phone",
label: "手机号码",
minWidth: 160,
search: { el: "input" }
},
{
prop: "entryDate",
label: "入职日期",
minWidth: 140,
render: scope => formatDate(scope.row.entryDate)
},
{
prop: "operation",
@@ -234,6 +303,11 @@ const formatTime = (timeStr?: string) => {
return timeStr.replace("T", " ").substring(0, 19);
};
const formatDate = (timeStr?: string) => {
if (!timeStr) return "-";
return timeStr.replace("T", " ").substring(0, 10);
};
async function showDetail(row: OnboardingEmployee) {
const { data } = await onboardingTaskApi.employeeDetail({
id: row.id
@@ -246,9 +320,7 @@ async function showDetail(row: OnboardingEmployee) {
function resolveDownloadFileName(response: any, fallbackFileName?: string) {
const disposition = String(response?.headers?.["content-disposition"] || "");
const encodedName =
disposition.match(/filename\*=UTF-8\'\'([^;]+)/i)?.[1] ||
disposition.match(/filename="?([^";]+)"?/i)?.[1];
const encodedName = disposition.match(/filename\*=UTF-8\'\'([^;]+)/i)?.[1] || disposition.match(/filename="?([^";]+)"?/i)?.[1];
if (encodedName) {
try {
@@ -258,7 +330,7 @@ function resolveDownloadFileName(response: any, fallbackFileName?: string) {
}
}
return fallbackFileName || "入职合同.docx";
return fallbackFileName || "入职签署材料.docx";
}
async function download(fileId: number, fallbackFileName?: string) {
@@ -278,7 +350,7 @@ async function download(fileId: number, fallbackFileName?: string) {
link.remove();
window.setTimeout(() => URL.revokeObjectURL(objectUrl), 0);
} catch (error: any) {
ElMessage.error(error?.msg || error?.message || "合同文件下载失败");
ElMessage.error(error?.msg || error?.message || "签署材料文件下载失败");
}
}
</script>
@@ -341,6 +413,7 @@ async function download(fileId: number, fallbackFileName?: string) {
}
.section-card {
flex-wrap: wrap;
border-radius: 8px;
border: 1px solid #e2e8f0;
}
@@ -60,7 +60,7 @@
<el-button type="primary" icon="Plus" @click="openCreate">新增入职模板</el-button>
</div>
<el-alert title="入职模板统一管理公司、岗位、合同文件与员工上传材料;员工入职档案自动采用标准《入职登记表》。" type="info" :closable="false" show-icon
<el-alert title="入职模板统一管理公司、岗位、签署材料文件与员工上传材料;员工入职档案自动采用标准《入职登记表》。" type="info" :closable="false" show-icon
class="tip" />
<div class="table-container">
@@ -78,7 +78,7 @@
</el-table-column>
<el-table-column label="配置内容" min-width="200">
<template #default="scope">
合同 {{ scope.row.contractCount || 0 }} 份 / 材料 {{ scope.row.materialCount || 0 }} 项
签署材料 {{ scope.row.contractCount || 0 }} 份 / 材料 {{ scope.row.materialCount || 0 }} 项
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="90" align="center">
@@ -186,7 +186,7 @@
<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="选择合同模板"
<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" />
@@ -494,7 +494,7 @@ function addContract() {
return;
}
if (!contractTemplates.value.length) {
ElMessage.warning("当前公司、部门和岗位没有可用的合同模板");
ElMessage.warning("当前公司、部门和岗位没有可用的签署材料模板");
return;
}
@@ -602,7 +602,7 @@ async function save() {
return ElMessage.warning("请完整填写模板编码、名称、公司、部门及适用岗位");
}
if (!form.contracts.length || form.contracts.some(item => !item.contractTemplateId || !item.contractTemplateVersionId)) {
return ElMessage.warning("请完整选择至少一份合同模板及版本");
return ElMessage.warning("请完整选择至少一份签署材料模板及版本");
}
saving.value = true;
@@ -18,7 +18,7 @@
>
<el-option label="全部变量分类" value="" />
<el-option label="员工登记表变量 (employee)" value="employee" />
<el-option label="人事合同参数 (hr)" value="hr" />
<el-option label="人事预设参数 (hr)" value="hr" />
<el-option label="公司信息变量 (company)" value="company" />
<el-option label="系统预置变量 (system)" value="system" />
</el-select>
@@ -29,7 +29,7 @@
</div>
<el-alert
title="变量字典库定义了合同 DOCX 中所有合法的占位符(如 {{employee.EmployeeName}})。上传 Word 时系统会自动校验占位符是否在此字典中,防止拼写错误。"
title="变量字典库定义了签署材料 DOCX 中所有合法的占位符(如 {{employee.EmployeeName}})。上传 Word 时系统会自动校验占位符是否在此字典中,防止拼写错误。"
type="info"
show-icon
:closable="false"
@@ -112,7 +112,7 @@
<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="人事预设参数 (hr.)" value="hr" />
<el-option label="公司信息 (company.)" value="company" />
<el-option label="系统变量 (system.)" value="system" />
</el-select>
+6 -6
View File
@@ -149,8 +149,8 @@
</el-col>
</el-row>
<el-form-item label="合同甲方全称" required>
<el-input v-model="companyForm.legalName" placeholder="用于合同填写的法定主体全称" />
<el-form-item label="签署材料甲方全称" required>
<el-input v-model="companyForm.legalName" placeholder="用于签署材料填写的法定主体全称" />
</el-form-item>
<el-row :gutter="20">
@@ -290,7 +290,7 @@
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="默认合同年限">
<el-form-item label="默认签署材料年限">
<el-input-number v-model="positionForm.defaultContractYears" :min="1" :max="20" class="w-full" />
</el-form-item>
</el-col>
@@ -419,7 +419,7 @@ const companyColumns: ColumnProps<OnboardingCompany>[] = [
},
{
prop: "legalName",
label: "合同甲方全称",
label: "签署材料甲方全称",
minWidth: 220
},
{
@@ -534,7 +534,7 @@ const positionColumns: ColumnProps<OnboardingPosition>[] = [
},
{
prop: "defaultContractYears",
label: "默认合同年限",
label: "默认签署材料年限",
width: 130,
align: "center",
render: scope => `${scope.row.defaultContractYears || 3} 年`
@@ -648,7 +648,7 @@ async function openPositionEdit(row: OnboardingPosition) {
async function save() {
if (dialogKind.value === "company") {
if (!companyForm.companyCode || !companyForm.companyName || !companyForm.legalName) {
return ElMessage.warning("请填写公司编码、公司名称和合同甲方全称");
return ElMessage.warning("请填写公司编码、公司名称和签署材料甲方全称");
}
await onboardingTaskApi.saveCompany(companyForm, editing.value);
companyTable.value?.refresh();
+23 -23
View File
@@ -64,7 +64,7 @@
icon="Setting"
@click="openContractVariables(scope.row)"
>
合同参数
签署材料参数
</el-button>
<el-button
@@ -214,12 +214,12 @@
<!-- 2. 合同人事参数卡片 -->
<div v-if="form.contractVariables.length" class="form-card-section">
<div class="section-title">
<span class="title-text">合同人事参数预设</span>
<span class="title-tip">以下参数将自动替换进 DOCX 合同中,请在向员工发送链接前核对填写</span>
<span class="title-text">签署材料人事参数预设</span>
<span class="title-tip">以下参数将自动替换进 DOCX 签署材料中,请在向员工发送链接前核对填写</span>
</div>
<el-alert
title="提示:此处填写的内容将作为合同正式条款,如薪资、合同起止日期、试用期等,请谨慎填写。"
title="提示:此处填写的内容将作为签署材料正式条款,如薪资、协议起止日期、试用期等,请谨慎填写。"
type="warning"
:closable="false"
show-icon
@@ -292,7 +292,7 @@
<!-- 任务创建成功与分享弹窗 -->
<el-dialog v-model="shareVisible" title="入职任务已创建" width="560px" destroy-on-close>
<el-result icon="success" title="任务创建成功!" sub-title="请先确认合同人事参数,再将专属链接发送给候选人">
<el-result icon="success" title="任务创建成功!" sub-title="请先确认签署材料人事参数,再将专属链接发送给候选人">
<template #extra>
<div class="share-box">
<div class="share-item">
@@ -510,8 +510,8 @@
<el-card v-if="detailCustomVars.length" shadow="never" class="mb-3 border-card">
<template #header>
<div class="card-header-flex">
<span class="font-bold card-title-text">合同专属补充信息</span>
<el-tag size="small" type="warning">合同专有项</el-tag>
<span class="font-bold card-title-text">签署材料专属补充信息</span>
<el-tag size="small" type="warning">签署材料专有项</el-tag>
</div>
</template>
<el-descriptions :column="2" border size="default">
@@ -611,8 +611,8 @@
</el-tab-pane>
<!-- 3. 合同管理与归档 -->
<el-tab-pane label="入职合同列表" name="contracts">
<el-empty v-if="!detail.contracts || !detail.contracts.length" description="暂无关联合同" />
<el-tab-pane label="入职签署材料列表" name="contracts">
<el-empty v-if="!detail.contracts || !detail.contracts.length" description="暂无关联签署材料" />
<div v-else class="contracts-panel">
<div v-for="item in detail.contracts" :key="item.id" class="contract-card-item">
<div class="contract-left">
@@ -634,7 +634,7 @@
icon="DocumentChecked"
@click="generate(item.id)"
>
生成DOCX合同
生成DOCX签署材料
</el-button>
<el-button
v-else
@@ -728,24 +728,24 @@
@close="closeImageViewer"
/>
<!-- 合同参数录入弹窗 -->
<!-- 签署材料参数录入弹窗 -->
<el-dialog
v-model="contractVariablesVisible"
:title="`${contractVariableTask?.taskNo || ''} · 合同参数录入`"
:title="`${contractVariableTask?.taskNo || ''} · 签署材料参数录入`"
width="1180px"
top="4vh"
destroy-on-close
>
<div class="contract-vars-dialog-body">
<el-alert
title="说明:此处展示本次入职任务关联的所有合同变量。带有【人事变量】标签的项为本次需要 HR 填写的核心条款(如薪资、试用期、合同起止日等);员工变量与公司变量将在流程中自动带入。"
title="说明:此处展示本次入职任务关联的所有签署材料变量。带有【人事变量】标签的项为本次需要 HR 填写的核心条款(如薪资、试用期、协议起止日等);员工变量与公司变量将在流程中自动带入。"
type="info"
show-icon
:closable="false"
style="margin-bottom: 16px;"
/>
<el-empty v-if="!contractVariableGroups.length" description="当前任务合同无需配置人事变量" />
<el-empty v-if="!contractVariableGroups.length" description="当前任务签署材料无需配置人事变量" />
<div v-else>
<!-- 顶部合同分组切换 Tab -->
@@ -862,7 +862,7 @@
<div class="dialog-footer-flex">
<el-button @click="contractVariablesVisible = false">关闭</el-button>
<el-button type="primary" icon="Check" :loading="savingAllVars" @click="saveAllContractVariables">
一键保存全部合同参数
一键保存全部签署材料参数
</el-button>
</div>
</template>
@@ -1112,7 +1112,7 @@ const variableTypeText = (type: string) => {
const map: Record<string, string> = {
employee: "员工变量",
company: "公司变量",
contract: "合同人事参数",
contract: "人事预设参数",
system: "系统变量"
};
return map[type] || type;
@@ -1359,7 +1359,7 @@ async function createTask() {
item => item.required && isBlankContractVariableValue(item.variableValue)
);
if (missingVariable) {
return ElMessage.warning(`请填写合同人事参数“${missingVariable.variableName}”`);
return ElMessage.warning(`请填写签署材料人事参数“${missingVariable.variableName}”`);
}
creating.value = true;
@@ -1403,7 +1403,7 @@ function copyText(text?: string, successMsg = "复制成功") {
function copyInviteMessage() {
if (!createdTaskInfo.value) return;
const msg = `【入职邀请】您好!欢迎加入${createdTaskInfo.value.companyName},担任${createdTaskInfo.value.positionName}一职。请点击以下链接完成您的入职登记与合同确认:${createdTaskInfo.value.linkUrl} (链接有效期至:${createdTaskInfo.value.expiresAt})`;
const msg = `【入职邀请】您好!欢迎加入${createdTaskInfo.value.companyName},担任${createdTaskInfo.value.positionName}一职。请点击以下链接完成您的入职登记与签署材料确认:${createdTaskInfo.value.linkUrl} (链接有效期至:${createdTaskInfo.value.expiresAt})`;
copyText(msg, "邀请文案已复制至剪贴板");
}
@@ -1457,7 +1457,7 @@ async function saveContractVariables(group: OnboardingTaskContractVariableGroup)
);
if (requiredBlank) {
return ElMessage.warning("请填写所有必填合同人事参数");
return ElMessage.warning("请填写所有必填签署材料人事参数");
}
await onboardingTaskApi.saveTaskContractVariables({
@@ -1489,7 +1489,7 @@ async function saveAllContractVariables() {
values: buildContractVariableSaveValues(group)
});
}
ElMessage.success("全部合同参数已保存成功!");
ElMessage.success("全部签署材料参数已保存成功!");
contractVariablesVisible.value = false;
} finally {
savingAllVars.value = false;
@@ -1520,7 +1520,7 @@ function buildContractVariableSaveValues(group: OnboardingTaskContractVariableGr
async function approve(row: OnboardingTask) {
await ElMessageBox.confirm(
"确认审核通过?系统将自动创建正式员工档案并锁定合同快照。",
"确认审核通过?系统将自动创建正式员工档案并锁定签署材料快照。",
"审核通过确认",
{ type: "success", confirmButtonText: "审核通过", cancelButtonText: "取消" }
);
@@ -1546,7 +1546,7 @@ async function reject(row: OnboardingTask) {
async function generate(taskContractId: number) {
await onboardingTaskApi.generateContract(taskContractId);
ElMessage.success("DOCX合同文件已生成");
ElMessage.success("DOCX签署材料文件已生成");
if (detail.value?.task?.id) {
await showDetail(detail.value.task, true);
}
@@ -2163,7 +2163,7 @@ onMounted(loadCompanies);
color: #2563eb;
}
/* 合同参数录入弹窗优化 */
/* 签署材料参数录入弹窗优化 */
.contract-vars-dialog-body {
max-height: 68vh;
overflow-y: auto;
+26 -26
View File
@@ -8,7 +8,7 @@
<span>OnboardX 智能入职服务</span>
</div>
<h1 class="brand-title">欢迎加入新团队</h1>
<p class="brand-subtitle">请按照 4 步指引完成入职登记表、证件上传与合同查阅确认</p>
<p class="brand-subtitle">请按照 4 步指引完成入职登记表、证件上传与签署材料查阅确认</p>
</div>
</header>
@@ -60,16 +60,16 @@
<div class="steps-wrapper desktop-only">
<el-steps :active="currentStep" finish-status="success" align-center>
<el-step title="入职登记表" icon="User" />
<el-step title="合同补充项" icon="Document" />
<el-step title="材料补充项" icon="Document" />
<el-step title="证件材料" icon="FolderChecked" />
<el-step title="合同确认" icon="Tickets" />
<el-step title="材料确认" icon="Tickets" />
</el-steps>
</div>
<div class="mobile-steps-wrapper mobile-only">
<div class="mobile-steps-track">
<div
v-for="(stepName, index) in ['1. 登记表', '2. 补充项', '3. 证件材料', '4. 合同确认']"
v-for="(stepName, index) in ['1. 登记表', '2. 补充项', '3. 证件材料', '4. 材料确认']"
:key="index"
class="mobile-step-item"
:class="{
@@ -83,7 +83,7 @@
</div>
</div>
<div class="mobile-current-indicator">
当前进行:<span class="highlight-step">第 {{ currentStep + 1 }}/4 步 · {{ ['入职登记表填写', '合同专有补充项', '证件材料上传', '合同查阅与确认'][currentStep] }}</span>
当前进行:<span class="highlight-step">第 {{ currentStep + 1 }}/4 步 · {{ ['入职登记表填写', '签署材料补充项', '证件材料上传', '签署材料查阅与确认'][currentStep] }}</span>
</div>
</div>
@@ -419,21 +419,21 @@
<!-- 步骤 2: 合同专属增量补充项 (自适应差集) -->
<div v-show="currentStep === 1" class="step-content">
<div class="section-header">
<h3 class="section-title">合同专属补充信息</h3>
<p class="section-desc">以下是本次签署合同中特殊约定的增量字段,登记表未涵盖的项需在此补充确认</p>
<h3 class="section-title">签署材料专属补充信息</h3>
<p class="section-desc">以下是本次签署材料中特殊约定的增量字段,登记表未涵盖的项需在此补充确认</p>
</div>
<div v-if="!extraVariables.length" class="all-covered-card">
<el-result
icon="success"
title="个人信息已完整覆盖"
sub-title="您在《入职登记表》中填写的姓名、身份证、电话、银行卡等信息已完整满足本次合同签署所需,无需额外补充!"
sub-title="您在《入职登记表》中填写的姓名、身份证、电话、银行卡等信息已完整满足本次签署材料所需,无需额外补充!"
/>
</div>
<el-form v-else label-position="top" class="extra-form">
<el-alert
title="请补充填写以下合同专有信息:"
title="请补充填写以下签署材料专有信息:"
type="info"
show-icon
:closable="false"
@@ -568,21 +568,21 @@
<!-- 步骤 4: 合同查阅与签署 -->
<div v-show="currentStep === 3" class="step-content">
<div class="section-header">
<h3 class="section-title">入职合同查阅与确认</h3>
<p class="section-desc">系统已为您代入真实登记信息生成专属合同,请认真查阅条款并勾选确认</p>
<h3 class="section-title">入职签署材料查阅与确认</h3>
<p class="section-desc">系统已为您代入真实登记信息生成专属签署材料,请认真查阅条款并勾选确认</p>
</div>
<el-alert
v-if="preparingContract"
class="contract-preparing-alert"
title="正在生成您的专属合同"
title="正在生成您的专属签署材料"
:description="contractPreparingDescription"
type="info"
show-icon
:closable="false"
/>
<el-empty v-if="!contracts.length" description="暂无待签署入职合同" />
<el-empty v-if="!contracts.length" description="暂无待确认签署材料" />
<div v-else class="contracts-list">
<div
@@ -599,7 +599,7 @@
</el-tag>
</div>
<div class="contract-desc">
请点击下方按钮下载/查阅带您专属信息的合同全文,仔细核对您的基本信息与薪资条款。
请点击下方按钮下载/查阅带您专属信息的签署材料全文,仔细核对您的基本信息与条款内容。
</div>
</div>
@@ -638,7 +638,7 @@
v-if="canEdit && isContractPrepared(c.id) && !c.hasViewed"
class="contract-view-hint"
>
请先点击“下载并查看合同”,查看后才能勾选确认。
请先点击“下载并查看签署材料”,查看后才能勾选确认。
</div>
</div>
</div>
@@ -772,7 +772,7 @@ const contractPreparingDescription = computed(() => {
}
const index = contracts.value.findIndex(item => item.id === contract.id);
return `正在校验合同信息并生成第 ${index + 1}/${contracts.value.length} 份《${contract.contractName}》,请稍候,不要关闭页面。`;
return `正在校验材料信息并生成第 ${index + 1}/${contracts.value.length} 份《${contract.contractName}》,请稍候,不要关闭页面。`;
});
// 计算差集增量变量 (合同需要的 employee. 变量中,登记表没有的字段)
@@ -1091,15 +1091,15 @@ function isContractPrepareFailed(taskContractId: number) {
function getContractActionText(taskContractId: number) {
const status = getContractPrepareStatus(taskContractId);
if (status === "preparing") {
return "正在准备合同";
return "正在准备签署材料";
}
if (status === "error") {
return "重新准备合同";
return "重新准备签署材料";
}
if (status === "ready") {
return "下载并查看合同";
return "下载并查看签署材料";
}
return "等待准备合同";
return "等待准备签署材料";
}
/**
@@ -1126,7 +1126,7 @@ async function prepareContract(contract: any) {
contractPrepareStatus[key] = "ready";
return true;
} catch (err: any) {
const message = err?.msg || err?.message || "合同准备失败,请稍后重试";
const message = err?.msg || err?.message || "签署材料准备失败,请稍后重试";
contractPrepareStatus[key] = "error";
contractPrepareErrors[key] = message;
// 业务异常已由统一请求拦截器提示;仅为网络等非标准异常补充提示,避免重复弹窗。
@@ -1150,7 +1150,7 @@ async function handleContractAction(contract: any) {
if (isContractPrepareFailed(contract.id)) {
const prepared = await prepareContract(contract);
if (prepared) {
ElMessage.success("合同已重新准备完成,请再次点击下载查看");
ElMessage.success("签署材料已重新准备完成,请再次点击下载查看");
}
return;
}
@@ -1179,21 +1179,21 @@ function previewAndDownloadContract(c: any) {
async function submitAll() {
const unprepared = contracts.value.filter(c => !isContractPrepared(c.id));
if (unprepared.length) {
return ElMessage.warning(`请先等待合同准备完成:${unprepared.map(c => c.contractName).join("、")}`);
return ElMessage.warning(`请先等待签署材料准备完成:${unprepared.map(c => c.contractName).join("、")}`);
}
const unviewed = contracts.value.filter(c => !c.hasViewed);
if (unviewed.length) {
return ElMessage.warning(`请先下载并查看合同:${unviewed.map(c => c.contractName).join("、")}`);
return ElMessage.warning(`请先下载并查看签署材料:${unviewed.map(c => c.contractName).join("、")}`);
}
const unconfirmed = contracts.value.filter(c => !c.isAgreed);
if (unconfirmed.length) {
return ElMessage.warning(`请先勾选同意全部合同(未勾选:${unconfirmed.map(c => c.contractName).join("、")})`);
return ElMessage.warning(`请先勾选同意全部签署材料(未勾选:${unconfirmed.map(c => c.contractName).join("、")})`);
}
const confirmed = await isMessageBoxConfirmed(
ElMessageBox.confirm("确认提交全部入职资料与合同确认?提交后将进入人事复核建档流程。", "最终提交确认", {
ElMessageBox.confirm("确认提交全部入职资料与签署材料确认?提交后将进入人事复核建档流程。", "最终提交确认", {
type: "success",
confirmButtonText: "确认提交",
cancelButtonText: "再检查一下"