This commit is contained in:
fuenmao
2026-08-26 11:00:10 +08:00
parent eab359d9fa
commit f1a910c25a
3 changed files with 1078 additions and 258 deletions
+11 -1
View File
@@ -3,6 +3,7 @@ import type { ReqId, ResPage } from "@/api/interface";
const taskHttp = moduleRequest("/biz/onboarding/task/"); const taskHttp = moduleRequest("/biz/onboarding/task/");
const auditHttp = moduleRequest("/biz/onboarding/audit/"); const auditHttp = moduleRequest("/biz/onboarding/audit/");
const fileHttp = moduleRequest("/sys/dev/file/");
export interface OnboardingTask { export interface OnboardingTask {
id: number; id: number;
@@ -178,6 +179,15 @@ export const onboardingTaskApi = {
return taskHttp.get("detail", params); return taskHttp.get("detail", params);
}, },
/** 使用后台登录令牌读取文件流,供入职材料下载和图片预览使用。 */
downloadFile(fileId: number) {
return fileHttp.get<any>(
"download",
{ id: fileId },
{ responseType: "blob", loading: false, cancel: false }
);
},
create(params: Record<string, unknown>) { create(params: Record<string, unknown>) {
return taskHttp.post("create", params); return taskHttp.post("create", params);
}, },
@@ -543,4 +553,4 @@ export const onboardingPublicApi = {
deleteMaterial(params: { token: string; taskMaterialId: number }) { deleteMaterial(params: { token: string; taskMaterialId: number }) {
return publicHttp.post("delete-material", params); return publicHttp.post("delete-material", params);
} }
}; };
+685 -147
View File
@@ -121,84 +121,147 @@
</div> </div>
<!-- 发起任务弹窗 --> <!-- 发起任务弹窗 -->
<el-dialog v-model="createVisible" title="发起入职任务" width="760px" destroy-on-close> <el-dialog
<el-form :model="form" label-width="90px"> v-model="createVisible"
<el-form-item label="所属公司" required> title="发起入职任务"
<el-select width="1050px"
v-model="form.companyId" top="4vh"
filterable destroy-on-close
placeholder="请选择公司主体" append-to-body
class="full-width" class="create-task-dialog"
@change="onCompanyChange" >
> <div class="create-task-dialog-body">
<el-option v-for="item in companies" :key="item.id" :label="item.companyName" :value="item.id" /> <el-form :model="form" label-position="top">
</el-select> <!-- 1. 基础岗位选择板块 -->
</el-form-item> <div class="form-card-section">
<div class="section-title">
<span class="title-text">入职基础信息</span>
<span class="title-tip">请选择新员工入职的公司主体、岗位及适用的入职模板</span>
</div>
<el-form-item label="入职岗位" required> <el-row :gutter="20">
<el-select <el-col :xs="24" :sm="8">
v-model="form.positionId" <el-form-item label="所属公司" required>
filterable <el-select
placeholder="请选择岗位" v-model="form.companyId"
class="full-width" filterable
:disabled="!form.companyId" placeholder="请选择公司主体"
@change="onPositionChange" class="full-width"
> @change="onCompanyChange"
<el-option v-for="item in positions" :key="item.id" :label="item.positionName" :value="item.id" /> >
</el-select> <el-option v-for="item in companies" :key="item.id" :label="item.companyName" :value="item.id" />
</el-form-item> </el-select>
</el-form-item>
</el-col>
<el-form-item label="入职模板" required> <el-col :xs="24" :sm="8">
<el-select <el-form-item label="入职岗位" required>
v-model="form.onboardingTemplateId" <el-select
filterable v-model="form.positionId"
placeholder="请选择入职模板" filterable
class="full-width" placeholder="请选择岗位"
:disabled="!form.positionId" class="full-width"
@change="onOnboardingTemplateChange" :disabled="!form.companyId"
> @change="onPositionChange"
<el-option v-for="item in onboardingTemplates" :key="item.id" :label="item.templateName" :value="item.id" /> >
</el-select> <el-option v-for="item in positions" :key="item.id" :label="item.positionName" :value="item.id" />
</el-form-item> </el-select>
</el-form-item>
</el-col>
<template v-if="form.contractVariables.length"> <el-col :xs="24" :sm="8">
<el-divider content-position="left">合同人事参数</el-divider> <el-form-item label="入职模板" required>
<el-alert <el-select
title="以下内容会在候选人查看合同时自动替换到 DOCX 中,请在发送链接前确认。" v-model="form.onboardingTemplateId"
type="warning" filterable
:closable="false" placeholder="请选择入职模板"
class="contract-variable-tip" class="full-width"
/> :disabled="!form.positionId"
<el-form-item @change="onOnboardingTemplateChange"
v-for="item in form.contractVariables" >
:key="`${item.contractTemplateVersionId}-${item.variableCode}`" <el-option v-for="item in onboardingTemplates" :key="item.id" :label="item.templateName" :value="item.id" />
:label="item.variableName" </el-select>
:required="item.required" </el-form-item>
> </el-col>
<el-date-picker </el-row>
v-if="item.dataType === 'date'" </div>
v-model="item.variableValue"
type="date" <!-- 2. 合同人事参数卡片 -->
value-format="YYYY-MM-DD" <div v-if="form.contractVariables.length" class="form-card-section">
placeholder="请选择日期" <div class="section-title">
class="full-width" <span class="title-text">合同人事参数预设</span>
<span class="title-tip">以下参数将自动替换进 DOCX 合同中,请在向员工发送链接前核对填写</span>
</div>
<el-alert
title="提示:此处填写的内容将作为合同正式条款,如薪资、合同起止日期、试用期等,请谨慎填写。"
type="warning"
:closable="false"
show-icon
style="margin-bottom: 16px;"
/> />
<el-input
v-else
v-model="item.variableValue"
:placeholder="`${item.contractName} · ${item.variableCode}`"
/>
</el-form-item>
</template>
<el-form-item label="人事备注"> <el-row :gutter="20">
<el-input v-model="form.remark" type="textarea" :rows="3" placeholder="填写该候选人入职的相关备注(选填)" /> <el-col
</el-form-item> v-for="item in form.contractVariables"
</el-form> :key="`${item.contractTemplateVersionId}-${item.variableCode}`"
:xs="24"
:sm="12"
>
<el-form-item>
<template #label>
<div class="var-label-row">
<span v-if="item.required" class="required-star">*</span>
<span class="var-name">{{ item.variableName }}</span>
<el-tag size="small" type="info" effect="plain" class="contract-source-tag">
{{ item.contractName }}
</el-tag>
</div>
</template>
<el-date-picker
v-if="item.dataType === 'date'"
v-model="item.variableValue"
type="date"
value-format="YYYY-MM-DD"
placeholder="选择日期"
class="var-input-field"
/>
<el-input-number
v-else-if="item.dataType === 'number' || item.dataType === 'decimal'"
v-model="item.variableValue"
controls-position="right"
class="var-input-field"
placeholder="输入金额或数值"
/>
<el-input
v-else
v-model="item.variableValue"
:placeholder="`请输入${item.variableName}`"
class="var-input-field"
clearable
/>
</el-form-item>
</el-col>
</el-row>
</div>
<!-- 3. 其他备注 -->
<div class="form-card-section">
<div class="section-title">
<span class="title-text">补充备注</span>
<span class="title-tip">填写该候选人入职的相关流转备注(仅人事可见,选填)</span>
</div>
<el-form-item label="">
<el-input v-model="form.remark" type="textarea" :rows="2" placeholder="请输入人事备注说明(选填)" />
</el-form-item>
</div>
</el-form>
</div>
<template #footer> <template #footer>
<el-button @click="createVisible = false">取消</el-button> <el-button @click="createVisible = false">取消</el-button>
<el-button type="primary" :loading="creating" @click="createTask">立即创建</el-button> <el-button type="primary" :loading="creating" @click="createTask">立即发起入职</el-button>
</template> </template>
</el-dialog> </el-dialog>
@@ -242,23 +305,32 @@
<el-drawer <el-drawer
v-model="detailVisible" v-model="detailVisible"
:title="`入职任务详情 · ${detail?.task?.taskNo || ''}`" :title="`入职任务详情 · ${detail?.task?.taskNo || ''}`"
size="860px" size="1180px"
destroy-on-close destroy-on-close
@closed="clearMaterialPreviews"
> >
<div v-if="detail" class="drawer-content"> <div v-if="detail" class="drawer-content">
<!-- 头部状态栏 --> <!-- 头部状态栏 (100% 满宽卡片) -->
<div class="drawer-status-bar"> <div class="drawer-status-banner">
<div class="status-left"> <div class="banner-left">
<span class="task-no-large">{{ detail.task.taskNo }}</span> <div class="banner-title-row">
<el-tag :type="statusType(detail.task.taskStatus)" size="large" effect="dark"> <span class="task-no-large">{{ detail.task.taskNo }}</span>
{{ statusText(detail.task.taskStatus) }} <el-tag :type="statusType(detail.task.taskStatus)" size="large" effect="dark">
</el-tag> {{ statusText(detail.task.taskStatus) }}
<span v-if="detail.task.submitCount > 0" class="submit-count-badge"> </el-tag>
第 {{ detail.task.submitCount }} 次提交 <span v-if="detail.task.submitCount > 0" class="submit-count-badge">
</span> 第 {{ detail.task.submitCount }} 次提交
</span>
</div>
<div class="banner-meta-row">
<span>所属公司:<strong>{{ detail.task.companyName || '-' }}</strong></span>
<span>入职岗位:<strong>{{ detail.task.positionName || '-' }}</strong></span>
<span>入职模板:<strong>{{ detail.task.onboardingTemplateName || '-' }}</strong></span>
</div>
</div> </div>
<div class="status-right"> <div class="banner-right">
<span class="created-time">发起于:{{ formatTime(detail.task.createTime) }}</span> <div class="meta-item">发起时间:{{ formatTime(detail.task.createTime) }}</div>
<div v-if="detail.task.linkExpireAt" class="meta-item">链接有效期至:{{ formatTime(detail.task.linkExpireAt) }}</div>
</div> </div>
</div> </div>
@@ -275,51 +347,156 @@
<!-- Tab 切换 --> <!-- Tab 切换 -->
<el-tabs v-model="activeDetailTab" class="detail-tabs"> <el-tabs v-model="activeDetailTab" class="detail-tabs">
<!-- 1. 候选人档案与动态信息 --> <!-- 1. 候选人档案与动态信息 -->
<el-tab-pane label="候选人信息" name="profile"> <el-tab-pane label="候选人档案 (入职登记表)" name="profile">
<el-card shadow="never" class="mb-4 border-card"> <div v-if="!detail.employeeInfo || !detail.employeeInfo.employeeName" class="empty-profile-box">
<template #header> <el-empty description="候选人尚未填写或提交《入职登记表》" :image-size="100" />
<div class="card-header-flex"> </div>
<span class="font-bold">基本个人档案</span>
<el-tag size="small" type="info">固定信息</el-tag>
</div>
</template>
<el-descriptions v-if="detail.employeeInfo" :column="2" border>
<el-descriptions-item label="姓名">
<span class="font-bold text-primary">{{ detail.employeeInfo.employeeName }}</span>
</el-descriptions-item>
<el-descriptions-item label="性别">{{ detail.employeeInfo.gender || '-' }}</el-descriptions-item>
<el-descriptions-item label="手机号码">{{ detail.employeeInfo.phone }}</el-descriptions-item>
<el-descriptions-item label="身份证号码">{{ detail.employeeInfo.idCardNumber }}</el-descriptions-item>
<el-descriptions-item label="毕业院校">{{ detail.employeeInfo.college || '-' }}</el-descriptions-item>
<el-descriptions-item label="学历 / 专业">
{{ detail.employeeInfo.education || '-' }} / {{ detail.employeeInfo.major || '-' }}
</el-descriptions-item>
<el-descriptions-item label="现居地址" :span="2">
{{ detail.employeeInfo.contactAddress || '-' }}
</el-descriptions-item>
</el-descriptions>
<el-empty v-else description="候选人尚未提交基础档案" :image-size="80" />
</el-card>
<!-- 动态填写字段 --> <div v-else class="profile-details-wrapper">
<el-card shadow="never" class="border-card"> <!-- 1. 基本个人档案 -->
<template #header> <el-card shadow="never" class="mb-3 border-card">
<div class="card-header-flex"> <template #header>
<span class="font-bold">岗位补充登记信息</span> <div class="card-header-flex">
<el-tag size="small" type="info">动态字段</el-tag> <span class="font-bold card-title-text"><el-icon class="mr-1"><UserFilled /></el-icon> 基本个人信息</span>
</div> <el-tag size="small" type="primary">档案标准项</el-tag>
</template> </div>
<el-descriptions v-if="detail.formValues && detail.formValues.length" :column="2" border> </template>
<el-descriptions-item <el-descriptions :column="3" border size="default">
v-for="item in detail.formValues" <el-descriptions-item label="真实姓名">
:key="item.id" <strong class="text-primary font-bold">{{ detail.employeeInfo.employeeName }}</strong>
:label="item.fieldName || item.fieldCode" </el-descriptions-item>
> <el-descriptions-item label="性别">{{ detail.employeeInfo.gender || '-' }}</el-descriptions-item>
{{ item.fieldValue || '-' }} <el-descriptions-item label="民族">{{ detail.employeeInfo.nation || '-' }}</el-descriptions-item>
</el-descriptions-item> <el-descriptions-item label="出生年月日">{{ detail.employeeInfo.birthday || '-' }}</el-descriptions-item>
</el-descriptions> <el-descriptions-item label="户籍所在地" :span="2">{{ detail.employeeInfo.registeredResidence || '-' }}</el-descriptions-item>
<el-empty v-else description="暂无岗位补充字段或未填写" :image-size="80" /> <el-descriptions-item label="身高 / 体重">
</el-card> {{ detail.employeeInfo.height ? detail.employeeInfo.height + ' cm' : '-' }} /
{{ detail.employeeInfo.weight ? detail.employeeInfo.weight + ' kg' : '-' }}
</el-descriptions-item>
<el-descriptions-item label="健康状况">{{ detail.employeeInfo.healthCondition || '-' }}</el-descriptions-item>
<el-descriptions-item label="婚育情况">{{ detail.employeeInfo.maritalStatus || '-' }}</el-descriptions-item>
<el-descriptions-item label="身份证号码" :span="3">
<code class="code-font font-bold">{{ detail.employeeInfo.idCardNumber || '-' }}</code>
</el-descriptions-item>
</el-descriptions>
</el-card>
<!-- 2. 学历与职业资质 -->
<el-card shadow="never" class="mb-3 border-card">
<template #header>
<div class="card-header-flex">
<span class="font-bold card-title-text"><el-icon class="mr-1"><School /></el-icon> 学历与职业资格</span>
</div>
</template>
<el-descriptions :column="3" border size="default">
<el-descriptions-item label="最高学历">{{ detail.employeeInfo.education || '-' }}</el-descriptions-item>
<el-descriptions-item label="所学专业">{{ detail.employeeInfo.major || '-' }}</el-descriptions-item>
<el-descriptions-item label="毕业院校">{{ detail.employeeInfo.graduatedSchool || detail.employeeInfo.college || '-' }}</el-descriptions-item>
<el-descriptions-item label="职业资格/职称">{{ detail.employeeInfo.vocationalQualification || '-' }}</el-descriptions-item>
<el-descriptions-item label="资格等级">{{ detail.employeeInfo.qualificationLevel || '-' }}</el-descriptions-item>
<el-descriptions-item label="特长技能">{{ detail.employeeInfo.technicalSpecialty || '-' }}</el-descriptions-item>
</el-descriptions>
</el-card>
<!-- 3. 联系方式与银行卡 -->
<el-card shadow="never" class="mb-3 border-card">
<template #header>
<div class="card-header-flex">
<span class="font-bold card-title-text"><el-icon class="mr-1"><Phone /></el-icon> 联系方式与工资银行卡</span>
</div>
</template>
<el-descriptions :column="3" border size="default">
<el-descriptions-item label="联系电话">{{ detail.employeeInfo.phone || '-' }}</el-descriptions-item>
<el-descriptions-item label="电子邮箱" :span="2">{{ detail.employeeInfo.email || '-' }}</el-descriptions-item>
<el-descriptions-item label="现居住详细地址" :span="2">{{ detail.employeeInfo.currentAddress || detail.employeeInfo.contactAddress || '-' }}</el-descriptions-item>
<el-descriptions-item label="房东联系方式">{{ detail.employeeInfo.landlordContact || '-' }}</el-descriptions-item>
<el-descriptions-item label="工资银行卡号" :span="2">
<code class="code-font font-bold">{{ detail.employeeInfo.bankCardNumber || '-' }}</code>
</el-descriptions-item>
<el-descriptions-item label="开户银行全称">{{ detail.employeeInfo.bankName || '-' }}</el-descriptions-item>
</el-descriptions>
</el-card>
<!-- 4. 紧急联系人 -->
<el-card shadow="never" class="mb-3 border-card">
<template #header>
<div class="card-header-flex">
<span class="font-bold card-title-text"><el-icon class="mr-1"><FirstAidKit /></el-icon> 紧急联系人</span>
</div>
</template>
<el-descriptions :column="3" border size="default">
<el-descriptions-item label="紧急联系人姓名">{{ detail.employeeInfo.emergencyContact || '-' }}</el-descriptions-item>
<el-descriptions-item label="联系电话">{{ detail.employeeInfo.emergencyPhone || '-' }}</el-descriptions-item>
<el-descriptions-item label="联系地址">{{ detail.employeeInfo.emergencyAddress || '-' }}</el-descriptions-item>
</el-descriptions>
</el-card>
<!-- 5. 教育学习经历 -->
<el-card shadow="never" class="mb-3 border-card">
<template #header>
<div class="card-header-flex">
<span class="font-bold card-title-text"><el-icon class="mr-1"><Reading /></el-icon> 教育学习经历</span>
</div>
</template>
<el-table :data="detailEducationList" border stripe empty-text="未填写教育学习经历">
<el-table-column prop="timeRange" label="起止时间" min-width="160" />
<el-table-column prop="location" label="学校 / 机构" min-width="200" />
<el-table-column prop="content" label="所学专业 / 学习内容" min-width="200" />
<el-table-column prop="result" label="学习结果 / 获得学位" min-width="180" />
</el-table>
</el-card>
<!-- 6. 主要工作经历 -->
<el-card shadow="never" class="mb-3 border-card">
<template #header>
<div class="card-header-flex">
<span class="font-bold card-title-text"><el-icon class="mr-1"><Briefcase /></el-icon> 主要工作经历</span>
</div>
</template>
<el-table :data="detailWorkList" border stripe empty-text="未填写工作经历(应届生无记录)">
<el-table-column prop="timeRange" label="起止时间" min-width="160" />
<el-table-column prop="companyName" label="工作单位" min-width="220" />
<el-table-column prop="position" label="担任职务" width="140" />
<el-table-column prop="salary" label="薪资待遇" width="120" />
<el-table-column prop="leaveReason" label="离职原因" min-width="180" />
</el-table>
</el-card>
<!-- 7. 家庭成员 -->
<el-card shadow="never" class="mb-3 border-card">
<template #header>
<div class="card-header-flex">
<span class="font-bold card-title-text"><el-icon class="mr-1"><HomeFilled /></el-icon> 家庭成员</span>
</div>
</template>
<el-table :data="detailFamilyList" border stripe empty-text="未填写家庭成员">
<el-table-column prop="name" label="姓名" width="120" />
<el-table-column prop="relationship" label="与本人关系" width="120" />
<el-table-column prop="age" label="年龄" width="90" align="center" />
<el-table-column prop="company" label="工作单位及职务" min-width="240" />
<el-table-column prop="phone" label="联系电话" width="150" />
</el-table>
</el-card>
<!-- 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>
</div>
</template>
<el-descriptions :column="2" border size="default">
<el-descriptions-item
v-for="item in detailCustomVars"
:key="item.key"
:label="item.key"
>
{{ item.value || '-' }}
</el-descriptions-item>
</el-descriptions>
</el-card>
</div>
</el-tab-pane> </el-tab-pane>
<!-- 2. 证件与材料复核画廊 --> <!-- 2. 证件与材料复核画廊 -->
@@ -337,19 +514,16 @@
> >
<div class="material-img-wrapper"> <div class="material-img-wrapper">
<el-image <el-image
v-if="isImageMaterial(item) && materialPreviewUrls[item.fileId]"
class="material-thumb" class="material-thumb"
:src="fileDownloadUrl(item.fileId)" :src="materialPreviewUrls[item.fileId]"
:preview-src-list="[fileDownloadUrl(item.fileId)]"
fit="cover" fit="cover"
preview-teleported @click="previewImage(item.fileId)"
> />
<template #error> <div v-else class="material-thumb-placeholder">
<div class="material-thumb-placeholder"> <el-icon :size="32"><Document /></el-icon>
<el-icon :size="32"><Document /></el-icon> <span>{{ materialPreviewText(item) }}</span>
<span>文档文件</span> </div>
</div>
</template>
</el-image>
<div class="material-overlay-status"> <div class="material-overlay-status">
<el-tag <el-tag
:type="item.reviewStatus === 'passed' ? 'success' : item.reviewStatus === 'need_supplement' ? 'danger' : 'warning'" :type="item.reviewStatus === 'passed' ? 'success' : item.reviewStatus === 'need_supplement' ? 'danger' : 'warning'"
@@ -367,7 +541,18 @@
</div> </div>
<div class="material-actions-flex"> <div class="material-actions-flex">
<el-button link type="primary" size="small" icon="Download" @click="download(item.fileId)"> <el-button
v-if="isImageMaterial(item)"
link
type="primary"
size="small"
icon="View"
:disabled="!materialPreviewUrls[item.fileId]"
@click="previewImage(item.fileId)"
>
图片预览
</el-button>
<el-button link type="primary" size="small" icon="Download" @click="download(item.fileId, item.fileName)">
原件下载 原件下载
</el-button> </el-button>
@@ -519,6 +704,13 @@
</template> </template>
</el-drawer> </el-drawer>
<el-image-viewer
v-if="imageViewerVisible"
:url-list="imageViewerUrls"
hide-on-click-modal
@close="closeImageViewer"
/>
<!-- 合同参数弹窗 --> <!-- 合同参数弹窗 -->
<el-dialog <el-dialog
v-model="contractVariablesVisible" v-model="contractVariablesVisible"
@@ -586,7 +778,7 @@
</template> </template>
<script setup lang="ts" name="onboardingTask"> <script setup lang="ts" name="onboardingTask">
import { ref, reactive, onMounted } from "vue"; import { ref, reactive, computed, onMounted, onBeforeUnmount } from "vue";
import { ElMessage, ElMessageBox } from "element-plus"; import { ElMessage, ElMessageBox } from "element-plus";
import { import {
DocumentAdd, DocumentAdd,
@@ -632,6 +824,53 @@ const createdTaskInfo = ref<any>(null);
const detailVisible = ref(false); const detailVisible = ref(false);
const activeDetailTab = ref("profile"); const activeDetailTab = ref("profile");
const detail = ref<any>(); const detail = ref<any>();
const materialPreviewUrls = reactive<Record<number, string>>({});
const materialPreviewLoading = reactive<Record<number, boolean>>({});
const materialPreviewFailed = reactive<Record<number, boolean>>({});
const imageViewerVisible = ref(false);
const imageViewerUrls = ref<string[]>([]);
const imageMimeTypes: Record<string, string> = {
bmp: "image/bmp",
gif: "image/gif",
jpeg: "image/jpeg",
jpg: "image/jpeg",
png: "image/png",
webp: "image/webp"
};
const detailEducationList = computed(() => {
try {
return JSON.parse(detail.value?.employeeInfo?.educationHistoryJson || "[]");
} catch {
return [];
}
});
const detailWorkList = computed(() => {
try {
return JSON.parse(detail.value?.employeeInfo?.workHistoryJson || "[]");
} catch {
return [];
}
});
const detailFamilyList = computed(() => {
try {
return JSON.parse(detail.value?.employeeInfo?.familyMembersJson || "[]");
} catch {
return [];
}
});
const detailCustomVars = computed(() => {
try {
const obj = JSON.parse(detail.value?.employeeInfo?.customVariablesJson || "{}");
return Object.entries(obj).map(([k, v]) => ({ key: k, value: String(v) }));
} catch {
return [];
}
});
const contractVariablesVisible = ref(false); const contractVariablesVisible = ref(false);
const contractVariableTask = ref<OnboardingTask>(); const contractVariableTask = ref<OnboardingTask>();
@@ -781,9 +1020,96 @@ const formatTime = (timeStr?: string) => {
return timeStr.replace("T", " ").substring(0, 19); return timeStr.replace("T", " ").substring(0, 19);
}; };
const fileDownloadUrl = (fileId: number) => { function materialFileSuffix(item: any) {
return `/api/sys/dev/file/download?id=${fileId}`; const suffix = String(item?.fileSuffix || "")
}; .trim()
.replace(/^\./, "")
.toLowerCase();
if (suffix) return suffix;
const fileName = String(item?.fileName || "");
const dotIndex = fileName.lastIndexOf(".");
return dotIndex >= 0 ? fileName.substring(dotIndex + 1).toLowerCase() : "";
}
function isImageMaterial(item: any) {
return Boolean(imageMimeTypes[materialFileSuffix(item)]);
}
function materialPreviewText(item: any) {
if (!isImageMaterial(item)) return "非图片文件";
if (materialPreviewLoading[item.fileId]) return "图片加载中";
if (materialPreviewFailed[item.fileId]) return "图片预览加载失败";
return "图片待加载";
}
function closeImageViewer() {
imageViewerVisible.value = false;
imageViewerUrls.value = [];
}
function clearMaterialPreviews() {
Object.values(materialPreviewUrls).forEach(url => URL.revokeObjectURL(url));
Object.keys(materialPreviewUrls).forEach(key => delete materialPreviewUrls[Number(key)]);
Object.keys(materialPreviewLoading).forEach(key => delete materialPreviewLoading[Number(key)]);
Object.keys(materialPreviewFailed).forEach(key => delete materialPreviewFailed[Number(key)]);
closeImageViewer();
}
function previewImage(fileId: number) {
const previewUrl = materialPreviewUrls[fileId];
if (!previewUrl) {
ElMessage.warning("图片正在加载或暂时无法预览");
return;
}
imageViewerUrls.value = [previewUrl];
imageViewerVisible.value = true;
}
async function loadMaterialPreviews(materials: any[]) {
clearMaterialPreviews();
const imageMaterials = (materials || []).filter(isImageMaterial);
await Promise.all(imageMaterials.map(async item => {
materialPreviewLoading[item.fileId] = true;
materialPreviewFailed[item.fileId] = false;
try {
const response: any = await onboardingTaskApi.downloadFile(item.fileId);
const sourceBlob = response?.data;
if (!(sourceBlob instanceof Blob)) {
throw new Error("服务器未返回有效的图片文件");
}
const mimeType = imageMimeTypes[materialFileSuffix(item)];
const imageBlob = sourceBlob.type.startsWith("image/")
? sourceBlob
: new Blob([sourceBlob], { type: mimeType });
materialPreviewUrls[item.fileId] = URL.createObjectURL(imageBlob);
} catch {
materialPreviewFailed[item.fileId] = true;
} finally {
materialPreviewLoading[item.fileId] = false;
}
}));
}
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];
if (encodedName) {
try {
return decodeURIComponent(encodedName.replace(/\+/g, "%20"));
} catch {
return encodedName;
}
}
return fallbackFileName || "入职材料";
}
// 列表查询并统计 // 列表查询并统计
async function getTableData(params: any) { async function getTableData(params: any) {
@@ -922,10 +1248,11 @@ function copyInviteMessage() {
} }
async function showDetail(row: OnboardingTask) { async function showDetail(row: OnboardingTask) {
const { data } = await onboardingTaskApi.detail({ id: row.id }); const { data }: any = await onboardingTaskApi.detail({ id: row.id });
detail.value = data; detail.value = data;
activeDetailTab.value = "profile"; activeDetailTab.value = "profile";
detailVisible.value = true; detailVisible.value = true;
void loadMaterialPreviews(data?.materials || []);
} }
async function openContractVariables(row: OnboardingTask) { async function openContractVariables(row: OnboardingTask) {
@@ -988,10 +1315,29 @@ async function generate(taskContractId: number) {
} }
} }
function download(fileId: number) { async function download(fileId: number, fallbackFileName?: string) {
window.open(fileDownloadUrl(fileId), "_blank"); try {
const response: any = await onboardingTaskApi.downloadFile(fileId);
const blob = response?.data;
if (!(blob instanceof Blob)) {
throw new Error("服务器未返回有效的文件");
}
const objectUrl = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = objectUrl;
link.download = resolveDownloadFileName(response, fallbackFileName);
document.body.appendChild(link);
link.click();
link.remove();
window.setTimeout(() => URL.revokeObjectURL(objectUrl), 0);
} catch (error: any) {
ElMessage.error(error?.msg || error?.message || "文件下载失败");
}
} }
onBeforeUnmount(clearMaterialPreviews);
async function reviewMaterial(taskMaterialId: number, reviewStatus: string) { async function reviewMaterial(taskMaterialId: number, reviewStatus: string) {
let reviewRemark = ""; let reviewRemark = "";
if (reviewStatus === "need_supplement") { if (reviewStatus === "need_supplement") {
@@ -1257,6 +1603,52 @@ onMounted(loadCompanies);
gap: 8px; gap: 8px;
} }
.material-img-wrapper {
position: relative;
width: 100%;
height: 150px;
overflow: hidden;
background: #e2e8f0;
border-radius: 6px;
}
.material-thumb {
width: 100%;
height: 100%;
cursor: zoom-in;
}
.material-thumb-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
gap: 8px;
color: #64748b;
}
.material-overlay-status {
position: absolute;
top: 8px;
right: 8px;
}
.material-info-box {
display: flex;
flex-direction: column;
gap: 8px;
}
.material-actions-flex,
.review-btn-group {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 6px;
}
.border-pass { .border-pass {
border-color: #10b981; border-color: #10b981;
background: #f0fdf4; background: #f0fdf4;
@@ -1347,4 +1739,150 @@ onMounted(loadCompanies);
display: flex; display: flex;
gap: 10px; gap: 10px;
} }
</style>
/* 发起任务大弹窗样式 */
.create-task-dialog-body {
max-height: 74vh;
overflow-y: auto;
padding-right: 6px;
}
.form-card-section {
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 16px 18px;
margin-bottom: 16px;
}
.section-title {
margin-bottom: 14px;
}
.title-text {
font-size: 15px;
font-weight: 700;
color: #0f172a;
margin-right: 10px;
}
.title-tip {
font-size: 12px;
color: #64748b;
}
.var-label-row {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
font-weight: 600;
color: #1e293b;
}
.contract-source-tag {
font-weight: normal;
border-radius: 4px;
}
.required-star {
color: #f56c6c;
font-weight: bold;
font-size: 14px;
line-height: 1;
}
.var-input-field {
width: 100% !important;
}
:deep(.var-input-field.el-date-editor) {
width: 100% !important;
}
:deep(.var-input-field.el-input-number) {
width: 100% !important;
}
:deep(.var-input-field.el-input-number .el-input__inner) {
text-align: left;
}
/* 详情抽屉顶部横幅与档案卡片样式 */
.drawer-status-banner {
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 16px 20px;
margin-bottom: 20px;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 12px;
}
.banner-left {
display: flex;
flex-direction: column;
gap: 8px;
}
.banner-title-row {
display: flex;
align-items: center;
gap: 12px;
}
.task-no-large {
font-size: 18px;
font-weight: 800;
color: #0f172a;
}
.submit-count-badge {
font-size: 12px;
color: #2563eb;
background: #eff6ff;
border: 1px solid #bfdbfe;
padding: 2px 8px;
border-radius: 4px;
font-weight: 600;
}
.banner-meta-row {
display: flex;
gap: 20px;
font-size: 13px;
color: #64748b;
flex-wrap: wrap;
}
.banner-right {
display: flex;
flex-direction: column;
gap: 4px;
align-items: flex-end;
font-size: 13px;
color: #64748b;
}
.card-title-text {
font-size: 14px;
color: #1e293b;
display: inline-flex;
align-items: center;
}
.mr-1 {
margin-right: 4px;
}
.code-font {
font-family: Consolas, Monaco, monospace;
background: #eff6ff;
padding: 2px 6px;
border-radius: 4px;
color: #2563eb;
}
</style>
+382 -110
View File
@@ -5,10 +5,10 @@
<div class="brand-content"> <div class="brand-content">
<div class="brand-badge"> <div class="brand-badge">
<el-icon><Promotion /></el-icon> <el-icon><Promotion /></el-icon>
<span>OnboardX 智能入职系统</span> <span>OnboardX 智能入职服务</span>
</div> </div>
<h1 class="brand-title">欢迎加入我们</h1> <h1 class="brand-title">欢迎加入新团队</h1>
<p class="brand-subtitle">请按照指引完成入职登记表填写、证件材料上传与合同查阅确认</p> <p class="brand-subtitle">请按照 4 步指引完成入职登记表、证件上传与合同查阅确认</p>
</div> </div>
</header> </header>
@@ -38,15 +38,15 @@
<div class="wizard-card"> <div class="wizard-card">
<div v-if="!canEdit" class="readonly-banner"> <div v-if="!canEdit" class="readonly-banner">
<el-alert <el-alert
title="当前为只读查阅模式(资料已提交审核)" title="当前为查阅模式(入职资料已提交审核)"
type="info" type="info"
show-icon show-icon
:closable="false" :closable="false"
/> />
</div> </div>
<!-- 步骤导航条 --> <!-- 步骤导航条:PC端展示完整步骤,移动端展示紧凑自适应导航 -->
<div class="steps-wrapper"> <div class="steps-wrapper desktop-only">
<el-steps :active="currentStep" finish-status="success" align-center> <el-steps :active="currentStep" finish-status="success" align-center>
<el-step title="入职登记表" icon="User" /> <el-step title="入职登记表" icon="User" />
<el-step title="合同补充项" icon="Document" /> <el-step title="合同补充项" icon="Document" />
@@ -55,18 +55,40 @@
</el-steps> </el-steps>
</div> </div>
<div class="mobile-steps-wrapper mobile-only">
<div class="mobile-steps-track">
<div
v-for="(stepName, index) in ['1. 登记表', '2. 补充项', '3. 证件材料', '4. 合同确认']"
:key="index"
class="mobile-step-item"
:class="{
'is-active': currentStep === index,
'is-finished': currentStep > index,
'is-pending': currentStep < index
}"
>
<div class="step-indicator-bar" />
<span class="step-label">{{ stepName }}</span>
</div>
</div>
<div class="mobile-current-indicator">
当前进行:<span class="highlight-step">第 {{ currentStep + 1 }}/4 步 · {{ ['入职登记表填写', '合同专有补充项', '证件材料上传', '合同查阅与确认'][currentStep] }}</span>
</div>
</div>
<!-- 步骤 1: 入职登记表 --> <!-- 步骤 1: 入职登记表 -->
<div v-show="currentStep === 0" class="step-content"> <div v-show="currentStep === 0" class="step-content">
<div class="registration-header"> <div class="registration-header">
<h2 class="reg-title">入 职 登 记 表</h2> <h2 class="reg-title">入 职 登 记 表</h2>
<p class="reg-subtitle">请准确填写您的真实个人档案、学习经历与家庭信息</p> <p class="reg-subtitle">请准确填写您的真实个人档案、学习经历与家庭信息(带 * 为必填项)</p>
</div> </div>
<el-form ref="regFormRef" :model="form" :rules="regRules" label-position="top" class="reg-form"> <el-form ref="regFormRef" :model="form" :rules="regRules" label-position="top" class="reg-form">
<!-- 1. 基本信息板块 --> <!-- 1. 基本信息板块 -->
<div class="card-section"> <div class="card-section">
<div class="card-section-title"> <div class="card-section-title">
<el-icon><UserFilled /></el-icon> 基本个人信息 <div class="title-icon-box"><el-icon><UserFilled /></el-icon></div>
<span>基本个人信息</span>
</div> </div>
<el-row :gutter="16"> <el-row :gutter="16">
<el-col :xs="24" :sm="8"> <el-col :xs="24" :sm="8">
@@ -99,12 +121,12 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :xs="12" :sm="4"> <el-col :xs="12" :sm="4">
<el-form-item label="身高(cm)" prop="height"> <el-form-item label="身高 (cm)" prop="height">
<el-input v-model="form.height" placeholder="如 175" :disabled="!canEdit" /> <el-input v-model="form.height" placeholder="如 175" :disabled="!canEdit" />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :xs="12" :sm="4"> <el-col :xs="12" :sm="4">
<el-form-item label="体重(kg)" prop="weight"> <el-form-item label="体重 (kg)" prop="weight">
<el-input v-model="form.weight" placeholder="如 65" :disabled="!canEdit" /> <el-input v-model="form.weight" placeholder="如 65" :disabled="!canEdit" />
</el-form-item> </el-form-item>
</el-col> </el-col>
@@ -135,7 +157,8 @@
<!-- 2. 学历与资质 --> <!-- 2. 学历与资质 -->
<div class="card-section"> <div class="card-section">
<div class="card-section-title"> <div class="card-section-title">
<el-icon><School /></el-icon> 学历与职业资格 <div class="title-icon-box"><el-icon><School /></el-icon></div>
<span>学历与职业资格</span>
</div> </div>
<el-row :gutter="16"> <el-row :gutter="16">
<el-col :xs="12" :sm="8"> <el-col :xs="12" :sm="8">
@@ -151,7 +174,7 @@
</el-col> </el-col>
<el-col :xs="12" :sm="8"> <el-col :xs="12" :sm="8">
<el-form-item label="所学专业" prop="major"> <el-form-item label="所学专业" prop="major">
<el-input v-model="form.major" placeholder="请输入专业" :disabled="!canEdit" clearable /> <el-input v-model="form.major" placeholder="请输入专业全称" :disabled="!canEdit" clearable />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :xs="24" :sm="8"> <el-col :xs="24" :sm="8">
@@ -160,49 +183,50 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :xs="12" :sm="8"> <el-col :xs="24" :sm="8">
<el-form-item label="职业资格" prop="vocationalQualification"> <el-form-item label="职业资格/职称" prop="vocationalQualification">
<el-input v-model="form.vocationalQualification" placeholder="如 软考高级 / 注册会计师" :disabled="!canEdit" /> <el-input v-model="form.vocationalQualification" placeholder="如 注册会计师/无" :disabled="!canEdit" />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :xs="12" :sm="8"> <el-col :xs="12" :sm="8">
<el-form-item label="资格等级" prop="qualificationLevel"> <el-form-item label="资格等级" prop="qualificationLevel">
<el-input v-model="form.qualificationLevel" placeholder="如 高级 / 中级" :disabled="!canEdit" /> <el-input v-model="form.qualificationLevel" placeholder="如 高级/中级/无" :disabled="!canEdit" />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :xs="24" :sm="8"> <el-col :xs="12" :sm="8">
<el-form-item label="有何技术特长" prop="technicalSpecialty"> <el-form-item label="特长技能" prop="technicalSpecialty">
<el-input v-model="form.technicalSpecialty" placeholder="专业特长说明" :disabled="!canEdit" /> <el-input v-model="form.technicalSpecialty" placeholder="如 驾驶/英语六级" :disabled="!canEdit" />
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
</div> </div>
<!-- 3. 联系方式与薪酬账户 --> <!-- 3. 联系方式与银行卡 -->
<div class="card-section"> <div class="card-section">
<div class="card-section-title"> <div class="card-section-title">
<el-icon><Phone /></el-icon> 通讯地址与薪酬账户 <div class="title-icon-box"><el-icon><Phone /></el-icon></div>
<span>联系方式与工资银行卡</span>
</div> </div>
<el-row :gutter="16"> <el-row :gutter="16">
<el-col :xs="24" :sm="8"> <el-col :xs="24" :sm="8">
<el-form-item label="联系电话" prop="phone"> <el-form-item label="联系电话" prop="phone">
<el-input v-model="form.phone" placeholder="11位手机号码" :disabled="!canEdit" maxlength="11" clearable /> <el-input v-model="form.phone" placeholder="手机号码" :disabled="!canEdit" maxlength="11" clearable />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :xs="24" :sm="8"> <el-col :xs="24" :sm="8">
<el-form-item label="电子邮箱" prop="email"> <el-form-item label="电子邮箱" prop="email">
<el-input v-model="form.email" placeholder="个人常用电子邮箱" :disabled="!canEdit" clearable /> <el-input v-model="form.email" placeholder="常用电子邮箱" :disabled="!canEdit" clearable />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :xs="24" :sm="8"> <el-col :xs="24" :sm="8">
<el-form-item label="暂住 / 现居住地址" prop="currentAddress"> <el-form-item label="现居住地址" prop="currentAddress">
<el-input v-model="form.currentAddress" placeholder="现详细居住地址" :disabled="!canEdit" clearable /> <el-input v-model="form.currentAddress" placeholder="目前常住的详细地址" :disabled="!canEdit" clearable />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :xs="24" :sm="8"> <el-col :xs="24" :sm="8">
<el-form-item label="房东联系方式" prop="landlordContact"> <el-form-item label="房东联系方式" prop="landlordContact">
<el-input v-model="form.landlordContact" placeholder="租房房东姓名及电话(选填)" :disabled="!canEdit" /> <el-input v-model="form.landlordContact" placeholder="租房房东姓名及电话 (选填)" :disabled="!canEdit" />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :xs="24" :sm="8"> <el-col :xs="24" :sm="8">
@@ -221,7 +245,8 @@
<!-- 4. 紧急联系人 --> <!-- 4. 紧急联系人 -->
<div class="card-section"> <div class="card-section">
<div class="card-section-title"> <div class="card-section-title">
<el-icon><FirstAidKit /></el-icon> 紧急联系人 <div class="title-icon-box"><el-icon><FirstAidKit /></el-icon></div>
<span>紧急联系人</span>
</div> </div>
<el-row :gutter="16"> <el-row :gutter="16">
<el-col :xs="24" :sm="8"> <el-col :xs="24" :sm="8">
@@ -245,26 +270,40 @@
<!-- 5. 教育学习经历动态列表 --> <!-- 5. 教育学习经历动态列表 -->
<div class="card-section"> <div class="card-section">
<div class="card-section-header"> <div class="card-section-header">
<div class="card-section-title"><el-icon><Reading /></el-icon> 教育学习经历</div> <div class="card-section-title">
<el-button v-if="canEdit" size="small" type="primary" plain icon="Plus" @click="addEducation">添加经历</el-button> <div class="title-icon-box"><el-icon><Reading /></el-icon></div>
<span>教育学习经历</span>
</div>
<el-button v-if="canEdit" size="small" type="primary" plain icon="Plus" @click="addEducation">
添加经历
</el-button>
</div> </div>
<div v-if="!educationHistory.length" class="empty-hint">暂未添加学习经历</div> <div v-if="!educationHistory.length" class="empty-hint">暂未添加学习经历,可点击右上角添加</div>
<div v-for="(item, idx) in educationHistory" :key="idx" class="dynamic-row-box"> <div v-for="(item, idx) in educationHistory" :key="idx" class="dynamic-row-box">
<div class="row-badge-header">
<span class="row-badge">学习经历 #{{ idx + 1 }}</span>
<el-button v-if="canEdit" link type="danger" icon="Delete" @click="educationHistory.splice(idx, 1)">删除本条</el-button>
</div>
<el-row :gutter="12"> <el-row :gutter="12">
<el-col :xs="24" :sm="6"> <el-col :xs="24" :sm="6">
<el-input v-model="item.timeRange" placeholder="时间 (如 2018.09-2022.06)" :disabled="!canEdit" /> <el-form-item label="时间范围">
<el-input v-model="item.timeRange" placeholder="如 2018.09-2022.06" :disabled="!canEdit" />
</el-form-item>
</el-col> </el-col>
<el-col :xs="24" :sm="6"> <el-col :xs="24" :sm="6">
<el-input v-model="item.location" placeholder="学校 / 地点" :disabled="!canEdit" /> <el-form-item label="学校/机构">
<el-input v-model="item.location" placeholder="毕业学校全称" :disabled="!canEdit" />
</el-form-item>
</el-col> </el-col>
<el-col :xs="24" :sm="6"> <el-col :xs="24" :sm="6">
<el-input v-model="item.content" placeholder="学习专业 / 内容" :disabled="!canEdit" /> <el-form-item label="专业/学习内容">
<el-input v-model="item.content" placeholder="所学专业" :disabled="!canEdit" />
</el-form-item>
</el-col> </el-col>
<el-col :xs="20" :sm="5"> <el-col :xs="24" :sm="6">
<el-input v-model="item.result" placeholder="学习结果 (如 毕业/获学士学位)" :disabled="!canEdit" /> <el-form-item label="结果/学位">
</el-col> <el-input v-model="item.result" placeholder="如 毕业/获学士学位" :disabled="!canEdit" />
<el-col :xs="4" :sm="1" class="delete-btn-col"> </el-form-item>
<el-button v-if="canEdit" link type="danger" icon="Delete" @click="educationHistory.splice(idx, 1)" />
</el-col> </el-col>
</el-row> </el-row>
</div> </div>
@@ -273,29 +312,45 @@
<!-- 6. 工作经历动态列表 --> <!-- 6. 工作经历动态列表 -->
<div class="card-section"> <div class="card-section">
<div class="card-section-header"> <div class="card-section-header">
<div class="card-section-title"><el-icon><Briefcase /></el-icon> 主要工作经历</div> <div class="card-section-title">
<el-button v-if="canEdit" size="small" type="primary" plain icon="Plus" @click="addWork">添加经历</el-button> <div class="title-icon-box"><el-icon><Briefcase /></el-icon></div>
<span>主要工作经历</span>
</div>
<el-button v-if="canEdit" size="small" type="primary" plain icon="Plus" @click="addWork">
添加经历
</el-button>
</div> </div>
<div v-if="!workHistory.length" class="empty-hint">暂未添加工作经历(应届生可留空)</div> <div v-if="!workHistory.length" class="empty-hint">暂未添加工作经历(应届生可留空)</div>
<div v-for="(item, idx) in workHistory" :key="idx" class="dynamic-row-box"> <div v-for="(item, idx) in workHistory" :key="idx" class="dynamic-row-box">
<div class="row-badge-header">
<span class="row-badge">工作经历 #{{ idx + 1 }}</span>
<el-button v-if="canEdit" link type="danger" icon="Delete" @click="workHistory.splice(idx, 1)">删除本条</el-button>
</div>
<el-row :gutter="12"> <el-row :gutter="12">
<el-col :xs="24" :sm="5"> <el-col :xs="24" :sm="5">
<el-input v-model="item.timeRange" placeholder="时间 (如 2022.07-2024.05)" :disabled="!canEdit" /> <el-form-item label="起止时间">
<el-input v-model="item.timeRange" placeholder="如 2022.07-2024.05" :disabled="!canEdit" />
</el-form-item>
</el-col> </el-col>
<el-col :xs="24" :sm="6"> <el-col :xs="24" :sm="6">
<el-input v-model="item.companyName" placeholder="工作单位名称" :disabled="!canEdit" /> <el-form-item label="工作单位名称">
<el-input v-model="item.companyName" placeholder="公司全称" :disabled="!canEdit" />
</el-form-item>
</el-col> </el-col>
<el-col :xs="12" :sm="4"> <el-col :xs="12" :sm="4">
<el-input v-model="item.salary" placeholder="工资待遇" :disabled="!canEdit" /> <el-form-item label="担任职务">
<el-input v-model="item.position" placeholder="职位名称" :disabled="!canEdit" />
</el-form-item>
</el-col> </el-col>
<el-col :xs="12" :sm="4"> <el-col :xs="12" :sm="4">
<el-input v-model="item.position" placeholder="担任职务" :disabled="!canEdit" /> <el-form-item label="薪资待遇">
<el-input v-model="item.salary" placeholder="薪资 (选填)" :disabled="!canEdit" />
</el-form-item>
</el-col> </el-col>
<el-col :xs="20" :sm="4"> <el-col :xs="24" :sm="5">
<el-input v-model="item.leaveReason" placeholder="离职原因" :disabled="!canEdit" /> <el-form-item label="离职原因">
</el-col> <el-input v-model="item.leaveReason" placeholder="简述离职原因" :disabled="!canEdit" />
<el-col :xs="4" :sm="1" class="delete-btn-col"> </el-form-item>
<el-button v-if="canEdit" link type="danger" icon="Delete" @click="workHistory.splice(idx, 1)" />
</el-col> </el-col>
</el-row> </el-row>
</div> </div>
@@ -304,29 +359,45 @@
<!-- 7. 家庭成员动态列表 --> <!-- 7. 家庭成员动态列表 -->
<div class="card-section"> <div class="card-section">
<div class="card-section-header"> <div class="card-section-header">
<div class="card-section-title"><el-icon><HomeFilled /></el-icon> 家庭成员</div> <div class="card-section-title">
<el-button v-if="canEdit" size="small" type="primary" plain icon="Plus" @click="addFamily">添加成员</el-button> <div class="title-icon-box"><el-icon><HomeFilled /></el-icon></div>
<span>家庭成员</span>
</div>
<el-button v-if="canEdit" size="small" type="primary" plain icon="Plus" @click="addFamily">
添加成员
</el-button>
</div> </div>
<div v-if="!familyMembers.length" class="empty-hint">暂未添加家庭成员信息</div> <div v-if="!familyMembers.length" class="empty-hint">暂未添加家庭成员信息</div>
<div v-for="(item, idx) in familyMembers" :key="idx" class="dynamic-row-box"> <div v-for="(item, idx) in familyMembers" :key="idx" class="dynamic-row-box">
<div class="row-badge-header">
<span class="row-badge">家庭成员 #{{ idx + 1 }}</span>
<el-button v-if="canEdit" link type="danger" icon="Delete" @click="familyMembers.splice(idx, 1)">删除本条</el-button>
</div>
<el-row :gutter="12"> <el-row :gutter="12">
<el-col :xs="12" :sm="4"> <el-col :xs="12" :sm="4">
<el-input v-model="item.name" placeholder="姓名" :disabled="!canEdit" /> <el-form-item label="姓名">
<el-input v-model="item.name" placeholder="成员姓名" :disabled="!canEdit" />
</el-form-item>
</el-col> </el-col>
<el-col :xs="12" :sm="4"> <el-col :xs="12" :sm="4">
<el-input v-model="item.relationship" placeholder="与本人关系" :disabled="!canEdit" /> <el-form-item label="关系">
<el-input v-model="item.relationship" placeholder="如 父母/配偶" :disabled="!canEdit" />
</el-form-item>
</el-col> </el-col>
<el-col :xs="12" :sm="3"> <el-col :xs="12" :sm="3">
<el-input v-model="item.age" placeholder="年龄" :disabled="!canEdit" /> <el-form-item label="年龄">
<el-input v-model="item.age" placeholder="年龄" :disabled="!canEdit" />
</el-form-item>
</el-col> </el-col>
<el-col :xs="24" :sm="7"> <el-col :xs="24" :sm="8">
<el-input v-model="item.company" placeholder="工作单位" :disabled="!canEdit" /> <el-form-item label="工作单位">
<el-input v-model="item.company" placeholder="工作单位及职务" :disabled="!canEdit" />
</el-form-item>
</el-col> </el-col>
<el-col :xs="20" :sm="5"> <el-col :xs="24" :sm="5">
<el-input v-model="item.phone" placeholder="联系电话" :disabled="!canEdit" /> <el-form-item label="联系电话">
</el-col> <el-input v-model="item.phone" placeholder="手机号" :disabled="!canEdit" />
<el-col :xs="4" :sm="1" class="delete-btn-col"> </el-form-item>
<el-button v-if="canEdit" link type="danger" icon="Delete" @click="familyMembers.splice(idx, 1)" />
</el-col> </el-col>
</el-row> </el-row>
</div> </div>
@@ -392,7 +463,7 @@
<el-tag v-if="mat.required" size="small" type="danger" effect="plain">必传</el-tag> <el-tag v-if="mat.required" size="small" type="danger" effect="plain">必传</el-tag>
<el-tag v-else size="small" type="info" effect="plain">选传</el-tag> <el-tag v-else size="small" type="info" effect="plain">选传</el-tag>
</div> </div>
<span class="mat-limit">最多 {{ mat.maxFileCount }} 份,格式:{{ mat.allowedExtensions }}</span> <span class="mat-limit">最多 {{ mat.maxFileCount }} 份 · 格式:{{ mat.allowedExtensions }}</span>
</div> </div>
<div class="uploaded-files-list"> <div class="uploaded-files-list">
@@ -401,7 +472,7 @@
:key="f.id" :key="f.id"
class="file-badge" class="file-badge"
> >
<el-icon><Document /></el-icon> <el-icon color="#2563eb"><Document /></el-icon>
<span class="file-name">{{ f.fileName || f.materialName }}</span> <span class="file-name">{{ f.fileName || f.materialName }}</span>
<el-button <el-button
v-if="canEdit" v-if="canEdit"
@@ -446,13 +517,14 @@
> >
<div class="contract-card-top"> <div class="contract-card-top">
<div class="contract-meta"> <div class="contract-meta">
<el-icon color="#2563eb" size="20"><Tickets /></el-icon>
<span class="contract-title font-bold">{{ c.contractName }}</span> <span class="contract-title font-bold">{{ c.contractName }}</span>
<el-tag size="small" :type="c.contractStatus === 'confirmed' ? 'success' : 'warning'"> <el-tag size="small" :type="c.contractStatus === 'confirmed' ? 'success' : 'warning'">
{{ c.contractStatus === 'confirmed' ? '已阅并同意' : '待查阅确认' }} {{ c.contractStatus === 'confirmed' ? '已阅并同意' : '待查阅确认' }}
</el-tag> </el-tag>
</div> </div>
<div class="contract-desc"> <div class="contract-desc">
请点击下方按钮下载/查阅带您专属信息的合同全文。 请点击下方按钮下载/查阅带您专属信息的合同全文,仔细核对您的基本信息与薪资条款。
</div> </div>
</div> </div>
@@ -469,6 +541,7 @@
<el-checkbox <el-checkbox
v-if="canEdit" v-if="canEdit"
v-model="c.isAgreed" v-model="c.isAgreed"
class="agree-checkbox"
:label="`我已完整阅读并同意《${c.contractName}》全部条款`" :label="`我已完整阅读并同意《${c.contractName}》全部条款`"
/> />
<span v-else class="confirmed-text">✓ 候选人已确认同意</span> <span v-else class="confirmed-text">✓ 候选人已确认同意</span>
@@ -479,10 +552,11 @@
<!-- 底部向导操作栏 --> <!-- 底部向导操作栏 -->
<div class="wizard-footer"> <div class="wizard-footer">
<el-button v-if="currentStep > 0" @click="currentStep--">上一步</el-button> <el-button v-if="currentStep > 0" size="large" @click="currentStep--">上一步</el-button>
<el-button <el-button
v-if="currentStep < 3" v-if="currentStep < 3"
type="primary" type="primary"
size="large"
@click="nextStep" @click="nextStep"
> >
下一步 下一步
@@ -490,6 +564,7 @@
<el-button <el-button
v-if="currentStep === 3 && canEdit" v-if="currentStep === 3 && canEdit"
type="success" type="success"
size="large"
icon="Check" icon="Check"
:loading="submitting" :loading="submitting"
@click="submitAll" @click="submitAll"
@@ -724,6 +799,7 @@ async function nextStep() {
} }
} }
currentStep.value++; currentStep.value++;
window.scrollTo({ top: 0, behavior: "smooth" });
} }
function getMaterialUploadUrl(materialTypeId: number) { function getMaterialUploadUrl(materialTypeId: number) {
@@ -800,186 +876,248 @@ onMounted(load);
<style scoped> <style scoped>
.fill-page { .fill-page {
min-height: 100vh; min-height: 100vh;
background: #f8fafc; background: #f1f5f9;
padding-bottom: 40px; padding-bottom: 50px;
} }
.brand-header { .brand-header {
background: linear-gradient(135deg, #1e40af, #3b82f6); background: linear-gradient(135deg, #1e3a8a 0%, #2563eb 50%, #38bdf8 100%);
color: #fff; color: #fff;
padding: 36px 20px; padding: 40px 20px 48px;
text-align: center; text-align: center;
} }
.brand-badge { .brand-badge {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 6px; gap: 6px;
background: rgba(255, 255, 255, 0.2); background: rgba(255, 255, 255, 0.18);
padding: 4px 12px; backdrop-filter: blur(8px);
padding: 5px 14px;
border-radius: 20px; border-radius: 20px;
font-size: 13px; font-size: 13px;
font-weight: 500;
margin-bottom: 12px; margin-bottom: 12px;
} }
.brand-title { .brand-title {
margin: 0 0 8px 0; margin: 0 0 8px 0;
font-size: 26px; font-size: 26px;
font-weight: 700; font-weight: 800;
letter-spacing: 1px;
} }
.brand-subtitle { .brand-subtitle {
margin: 0; margin: 0;
font-size: 14px; font-size: 14px;
opacity: 0.9; opacity: 0.92;
} }
.main-container { .main-container {
max-width: 1000px; max-width: 980px;
margin: -24px auto 0; margin: -28px auto 0;
padding: 0 16px; padding: 0 16px;
} }
.status-banner { .status-banner {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
background: #fff; background: #fff;
padding: 12px 18px; padding: 14px 20px;
border-radius: 8px; border-radius: 10px;
margin-bottom: 16px; margin-bottom: 16px;
box-shadow: 0 2px 6px rgba(0,0,0,0.05); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
} }
.task-no { .task-no {
font-weight: bold; font-weight: 700;
margin-right: 10px; color: #1e293b;
margin-right: 12px;
} }
.expire-time { .expire-time {
font-size: 13px; font-size: 13px;
color: #64748b; color: #64748b;
} }
.wizard-card { .wizard-card {
background: #fff; background: #fff;
border-radius: 10px; border-radius: 12px;
padding: 24px; padding: 26px 24px;
box-shadow: 0 2px 12px rgba(0,0,0,0.06); box-shadow: 0 4px 20px -2px rgba(0, 0, 0, 0.05);
} }
.steps-wrapper { .steps-wrapper {
margin-bottom: 30px; margin-bottom: 30px;
padding: 0 20px; padding: 0 10px;
} }
.registration-header { .registration-header {
text-align: center; text-align: center;
border-bottom: 2px solid #e2e8f0; border-bottom: 2px solid #f1f5f9;
padding-bottom: 16px; padding-bottom: 16px;
margin-bottom: 24px; margin-bottom: 24px;
} }
.reg-title { .reg-title {
letter-spacing: 4px; letter-spacing: 4px;
font-size: 22px; font-size: 22px;
color: #1e293b; font-weight: 800;
color: #0f172a;
margin: 0 0 6px 0; margin: 0 0 6px 0;
} }
.reg-subtitle { .reg-subtitle {
font-size: 13px; font-size: 13px;
color: #64748b; color: #64748b;
margin: 0; margin: 0;
} }
/* 卡片分区样式 */
.card-section { .card-section {
background: #f8fafc; background: #ffffff;
border: 1px solid #e2e8f0; border: 1px solid #e2e8f0;
border-left: 3px solid #3b82f6;
border-radius: 8px; border-radius: 8px;
padding: 16px 20px; padding: 18px 20px;
margin-bottom: 18px; margin-bottom: 20px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02);
} }
.card-section-title { .card-section-title {
font-weight: 700; font-weight: 700;
font-size: 15px; font-size: 15px;
color: #0f172a; color: #0f172a;
margin-bottom: 14px; margin-bottom: 16px;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 6px; gap: 8px;
} }
.title-icon-box {
width: 24px;
height: 24px;
background: #eff6ff;
color: #2563eb;
border-radius: 6px;
display: inline-flex;
align-items: center;
justify-content: center;
}
.card-section-header { .card-section-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 14px;
}
.dynamic-row-box {
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 14px 16px;
margin-bottom: 12px; margin-bottom: 12px;
} }
.dynamic-row-box {
background: #fff; .row-badge-header {
border: 1px solid #cbd5e1; display: flex;
border-radius: 6px; justify-content: space-between;
padding: 10px 12px; align-items: center;
margin-bottom: 10px; margin-bottom: 10px;
} }
.delete-btn-col {
display: flex; .row-badge {
align-items: center; font-size: 12px;
justify-content: center; font-weight: 700;
color: #2563eb;
background: #eff6ff;
padding: 2px 8px;
border-radius: 4px;
} }
.empty-hint { .empty-hint {
font-size: 13px; font-size: 13px;
color: #94a3b8; color: #94a3b8;
padding: 8px 0; padding: 10px 0;
text-align: center;
} }
.full-width { .full-width {
width: 100%; width: 100%;
} }
.section-header { .section-header {
margin-bottom: 20px; margin-bottom: 22px;
} }
.section-title { .section-title {
margin: 0 0 6px 0; margin: 0 0 6px 0;
font-size: 18px; font-size: 18px;
font-weight: 700;
color: #0f172a;
} }
.section-desc { .section-desc {
margin: 0; margin: 0;
font-size: 13px; font-size: 13px;
color: #64748b; color: #64748b;
} }
.all-covered-card {
padding: 40px 0;
}
.materials-grid { .materials-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 16px; gap: 16px;
} }
.material-item-card { .material-item-card {
border: 1px solid #e2e8f0; border: 1px solid #e2e8f0;
border-radius: 8px; border-radius: 8px;
padding: 16px; padding: 16px;
background: #f8fafc; background: #f8fafc;
} }
.mat-card-header { .mat-card-header {
margin-bottom: 12px; margin-bottom: 12px;
} }
.mat-title-box { .mat-title-box {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
margin-bottom: 4px; margin-bottom: 4px;
} }
.mat-name { .mat-name {
font-weight: 700; font-weight: 700;
color: #1e293b;
} }
.mat-limit { .mat-limit {
font-size: 12px; font-size: 12px;
color: #64748b; color: #64748b;
} }
.file-badge { .file-badge {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 6px; gap: 8px;
background: #fff; background: #fff;
border: 1px solid #cbd5e1; border: 1px solid #cbd5e1;
padding: 6px 10px; padding: 8px 12px;
border-radius: 4px; border-radius: 6px;
font-size: 13px; font-size: 13px;
margin-bottom: 8px; margin-bottom: 8px;
} }
.file-name { .file-name {
flex: 1; flex: 1;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
.contract-sign-card { .contract-sign-card {
border: 1px solid #e2e8f0; border: 1px solid #e2e8f0;
border-radius: 8px; border-radius: 8px;
@@ -987,20 +1125,25 @@ onMounted(load);
background: #f8fafc; background: #f8fafc;
margin-bottom: 16px; margin-bottom: 16px;
} }
.contract-meta { .contract-meta {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; gap: 10px;
margin-bottom: 8px; margin-bottom: 8px;
} }
.contract-title { .contract-title {
font-size: 16px; font-size: 16px;
color: #0f172a;
} }
.contract-desc { .contract-desc {
font-size: 13px; font-size: 13px;
color: #64748b; color: #64748b;
margin-bottom: 14px; margin-bottom: 14px;
} }
.contract-card-bottom { .contract-card-bottom {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -1008,14 +1151,143 @@ onMounted(load);
flex-wrap: wrap; flex-wrap: wrap;
gap: 12px; gap: 12px;
border-top: 1px dashed #cbd5e1; border-top: 1px dashed #cbd5e1;
padding-top: 12px; padding-top: 14px;
} }
.agree-checkbox {
font-weight: 600;
}
.confirmed-text {
color: #16a34a;
font-weight: 600;
font-size: 14px;
}
/* 底部操作栏吸底优化 */
.wizard-footer { .wizard-footer {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
gap: 12px; gap: 14px;
margin-top: 30px; margin-top: 30px;
border-top: 1px solid #e2e8f0; border-top: 1px solid #e2e8f0;
padding-top: 20px; padding-top: 20px;
position: sticky;
bottom: 0;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(8px);
padding-bottom: max(10px, env(safe-area-inset-bottom));
z-index: 10;
}
.readonly-banner {
margin-bottom: 24px;
}
.desktop-only {
display: block;
}
.mobile-only {
display: none;
}
.mobile-steps-wrapper {
margin-bottom: 24px;
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 12px 14px;
}
.mobile-steps-track {
display: flex;
justify-content: space-between;
gap: 8px;
margin-bottom: 10px;
}
.mobile-step-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
}
.step-indicator-bar {
width: 100%;
height: 4px;
border-radius: 2px;
background: #e2e8f0;
transition: all 0.3s;
}
.mobile-step-item.is-active .step-indicator-bar {
background: #2563eb;
height: 5px;
}
.mobile-step-item.is-finished .step-indicator-bar {
background: #16a34a;
}
.step-label {
font-size: 11px;
color: #64748b;
white-space: nowrap;
}
.mobile-step-item.is-active .step-label {
color: #2563eb;
font-weight: 700;
}
.mobile-step-item.is-finished .step-label {
color: #16a34a;
font-weight: 600;
}
.mobile-current-indicator {
font-size: 12px;
color: #475569;
text-align: center;
border-top: 1px dashed #e2e8f0;
padding-top: 8px;
}
.highlight-step {
color: #2563eb;
font-weight: 700;
}
@media (max-width: 640px) {
.desktop-only {
display: none !important;
}
.mobile-only {
display: block !important;
}
.brand-header {
padding: 28px 16px 36px;
}
.brand-title {
font-size: 22px;
}
.main-container {
padding: 0 10px;
}
.wizard-card {
padding: 18px 14px;
}
.status-banner {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.contract-card-bottom {
flex-direction: column;
align-items: stretch;
}
} }
</style> </style>