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 auditHttp = moduleRequest("/biz/onboarding/audit/");
const fileHttp = moduleRequest("/sys/dev/file/");
export interface OnboardingTask {
id: number;
@@ -178,6 +179,15 @@ export const onboardingTaskApi = {
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>) {
return taskHttp.post("create", params);
},
@@ -543,4 +553,4 @@ export const onboardingPublicApi = {
deleteMaterial(params: { token: string; taskMaterialId: number }) {
return publicHttp.post("delete-material", params);
}
};
};
+685 -147
View File
@@ -121,84 +121,147 @@
</div>
<!-- 发起任务弹窗 -->
<el-dialog v-model="createVisible" title="发起入职任务" width="760px" destroy-on-close>
<el-form :model="form" label-width="90px">
<el-form-item label="所属公司" required>
<el-select
v-model="form.companyId"
filterable
placeholder="请选择公司主体"
class="full-width"
@change="onCompanyChange"
>
<el-option v-for="item in companies" :key="item.id" :label="item.companyName" :value="item.id" />
</el-select>
</el-form-item>
<el-dialog
v-model="createVisible"
title="发起入职任务"
width="1050px"
top="4vh"
destroy-on-close
append-to-body
class="create-task-dialog"
>
<div class="create-task-dialog-body">
<el-form :model="form" label-position="top">
<!-- 1. 基础岗位选择板块 -->
<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-select
v-model="form.positionId"
filterable
placeholder="请选择岗位"
class="full-width"
:disabled="!form.companyId"
@change="onPositionChange"
>
<el-option v-for="item in positions" :key="item.id" :label="item.positionName" :value="item.id" />
</el-select>
</el-form-item>
<el-row :gutter="20">
<el-col :xs="24" :sm="8">
<el-form-item label="所属公司" required>
<el-select
v-model="form.companyId"
filterable
placeholder="请选择公司主体"
class="full-width"
@change="onCompanyChange"
>
<el-option v-for="item in companies" :key="item.id" :label="item.companyName" :value="item.id" />
</el-select>
</el-form-item>
</el-col>
<el-form-item label="入职模板" required>
<el-select
v-model="form.onboardingTemplateId"
filterable
placeholder="请选择入职模板"
class="full-width"
:disabled="!form.positionId"
@change="onOnboardingTemplateChange"
>
<el-option v-for="item in onboardingTemplates" :key="item.id" :label="item.templateName" :value="item.id" />
</el-select>
</el-form-item>
<el-col :xs="24" :sm="8">
<el-form-item label="入职岗位" required>
<el-select
v-model="form.positionId"
filterable
placeholder="请选择岗位"
class="full-width"
:disabled="!form.companyId"
@change="onPositionChange"
>
<el-option v-for="item in positions" :key="item.id" :label="item.positionName" :value="item.id" />
</el-select>
</el-form-item>
</el-col>
<template v-if="form.contractVariables.length">
<el-divider content-position="left">合同人事参数</el-divider>
<el-alert
title="以下内容会在候选人查看合同时自动替换到 DOCX 中,请在发送链接前确认。"
type="warning"
:closable="false"
class="contract-variable-tip"
/>
<el-form-item
v-for="item in form.contractVariables"
:key="`${item.contractTemplateVersionId}-${item.variableCode}`"
:label="item.variableName"
:required="item.required"
>
<el-date-picker
v-if="item.dataType === 'date'"
v-model="item.variableValue"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择日期"
class="full-width"
<el-col :xs="24" :sm="8">
<el-form-item label="入职模板" required>
<el-select
v-model="form.onboardingTemplateId"
filterable
placeholder="请选择入职模板"
class="full-width"
:disabled="!form.positionId"
@change="onOnboardingTemplateChange"
>
<el-option v-for="item in onboardingTemplates" :key="item.id" :label="item.templateName" :value="item.id" />
</el-select>
</el-form-item>
</el-col>
</el-row>
</div>
<!-- 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>
</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-input v-model="form.remark" type="textarea" :rows="3" placeholder="填写该候选人入职的相关备注(选填)" />
</el-form-item>
</el-form>
<el-row :gutter="20">
<el-col
v-for="item in form.contractVariables"
: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>
<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>
</el-dialog>
@@ -242,23 +305,32 @@
<el-drawer
v-model="detailVisible"
:title="`入职任务详情 · ${detail?.task?.taskNo || ''}`"
size="860px"
size="1180px"
destroy-on-close
@closed="clearMaterialPreviews"
>
<div v-if="detail" class="drawer-content">
<!-- 头部状态栏 -->
<div class="drawer-status-bar">
<div class="status-left">
<span class="task-no-large">{{ detail.task.taskNo }}</span>
<el-tag :type="statusType(detail.task.taskStatus)" size="large" effect="dark">
{{ statusText(detail.task.taskStatus) }}
</el-tag>
<span v-if="detail.task.submitCount > 0" class="submit-count-badge">
第 {{ detail.task.submitCount }} 次提交
</span>
<!-- 头部状态栏 (100% 满宽卡片) -->
<div class="drawer-status-banner">
<div class="banner-left">
<div class="banner-title-row">
<span class="task-no-large">{{ detail.task.taskNo }}</span>
<el-tag :type="statusType(detail.task.taskStatus)" size="large" effect="dark">
{{ statusText(detail.task.taskStatus) }}
</el-tag>
<span v-if="detail.task.submitCount > 0" class="submit-count-badge">
第 {{ 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 class="status-right">
<span class="created-time">发起于:{{ formatTime(detail.task.createTime) }}</span>
<div class="banner-right">
<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>
@@ -275,51 +347,156 @@
<!-- Tab 切换 -->
<el-tabs v-model="activeDetailTab" class="detail-tabs">
<!-- 1. 候选人档案与动态信息 -->
<el-tab-pane label="候选人信息" name="profile">
<el-card shadow="never" class="mb-4 border-card">
<template #header>
<div class="card-header-flex">
<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>
<el-tab-pane label="候选人档案 (入职登记表)" name="profile">
<div v-if="!detail.employeeInfo || !detail.employeeInfo.employeeName" class="empty-profile-box">
<el-empty description="候选人尚未填写或提交《入职登记表》" :image-size="100" />
</div>
<!-- 动态填写字段 -->
<el-card shadow="never" class="border-card">
<template #header>
<div class="card-header-flex">
<span class="font-bold">岗位补充登记信息</span>
<el-tag size="small" type="info">动态字段</el-tag>
</div>
</template>
<el-descriptions v-if="detail.formValues && detail.formValues.length" :column="2" border>
<el-descriptions-item
v-for="item in detail.formValues"
:key="item.id"
:label="item.fieldName || item.fieldCode"
>
{{ item.fieldValue || '-' }}
</el-descriptions-item>
</el-descriptions>
<el-empty v-else description="暂无岗位补充字段或未填写" :image-size="80" />
</el-card>
<div v-else class="profile-details-wrapper">
<!-- 1. 基本个人档案 -->
<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"><UserFilled /></el-icon> 基本个人信息</span>
<el-tag size="small" type="primary">档案标准项</el-tag>
</div>
</template>
<el-descriptions :column="3" border size="default">
<el-descriptions-item label="真实姓名">
<strong class="text-primary font-bold">{{ detail.employeeInfo.employeeName }}</strong>
</el-descriptions-item>
<el-descriptions-item label="性别">{{ detail.employeeInfo.gender || '-' }}</el-descriptions-item>
<el-descriptions-item label="民族">{{ detail.employeeInfo.nation || '-' }}</el-descriptions-item>
<el-descriptions-item label="出生年月日">{{ detail.employeeInfo.birthday || '-' }}</el-descriptions-item>
<el-descriptions-item label="户籍所在地" :span="2">{{ detail.employeeInfo.registeredResidence || '-' }}</el-descriptions-item>
<el-descriptions-item label="身高 / 体重">
{{ 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>
<!-- 2. 证件与材料复核画廊 -->
@@ -337,19 +514,16 @@
>
<div class="material-img-wrapper">
<el-image
v-if="isImageMaterial(item) && materialPreviewUrls[item.fileId]"
class="material-thumb"
:src="fileDownloadUrl(item.fileId)"
:preview-src-list="[fileDownloadUrl(item.fileId)]"
:src="materialPreviewUrls[item.fileId]"
fit="cover"
preview-teleported
>
<template #error>
<div class="material-thumb-placeholder">
<el-icon :size="32"><Document /></el-icon>
<span>文档文件</span>
</div>
</template>
</el-image>
@click="previewImage(item.fileId)"
/>
<div v-else class="material-thumb-placeholder">
<el-icon :size="32"><Document /></el-icon>
<span>{{ materialPreviewText(item) }}</span>
</div>
<div class="material-overlay-status">
<el-tag
:type="item.reviewStatus === 'passed' ? 'success' : item.reviewStatus === 'need_supplement' ? 'danger' : 'warning'"
@@ -367,7 +541,18 @@
</div>
<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>
@@ -519,6 +704,13 @@
</template>
</el-drawer>
<el-image-viewer
v-if="imageViewerVisible"
:url-list="imageViewerUrls"
hide-on-click-modal
@close="closeImageViewer"
/>
<!-- 合同参数弹窗 -->
<el-dialog
v-model="contractVariablesVisible"
@@ -586,7 +778,7 @@
</template>
<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 {
DocumentAdd,
@@ -632,6 +824,53 @@ const createdTaskInfo = ref<any>(null);
const detailVisible = ref(false);
const activeDetailTab = ref("profile");
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 contractVariableTask = ref<OnboardingTask>();
@@ -781,9 +1020,96 @@ const formatTime = (timeStr?: string) => {
return timeStr.replace("T", " ").substring(0, 19);
};
const fileDownloadUrl = (fileId: number) => {
return `/api/sys/dev/file/download?id=${fileId}`;
};
function materialFileSuffix(item: any) {
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) {
@@ -922,10 +1248,11 @@ function copyInviteMessage() {
}
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;
activeDetailTab.value = "profile";
detailVisible.value = true;
void loadMaterialPreviews(data?.materials || []);
}
async function openContractVariables(row: OnboardingTask) {
@@ -988,10 +1315,29 @@ async function generate(taskContractId: number) {
}
}
function download(fileId: number) {
window.open(fileDownloadUrl(fileId), "_blank");
async function download(fileId: number, fallbackFileName?: string) {
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) {
let reviewRemark = "";
if (reviewStatus === "need_supplement") {
@@ -1257,6 +1603,52 @@ onMounted(loadCompanies);
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-color: #10b981;
background: #f0fdf4;
@@ -1347,4 +1739,150 @@ onMounted(loadCompanies);
display: flex;
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-badge">
<el-icon><Promotion /></el-icon>
<span>OnboardX 智能入职系统</span>
<span>OnboardX 智能入职服务</span>
</div>
<h1 class="brand-title">欢迎加入我们</h1>
<p class="brand-subtitle">请按照指引完成入职登记表填写、证件材料上传与合同查阅确认</p>
<h1 class="brand-title">欢迎加入新团队</h1>
<p class="brand-subtitle">请按照 4 步指引完成入职登记表、证件上传与合同查阅确认</p>
</div>
</header>
@@ -38,15 +38,15 @@
<div class="wizard-card">
<div v-if="!canEdit" class="readonly-banner">
<el-alert
title="当前为只读查阅模式(资料已提交审核)"
title="当前为查阅模式(入职资料已提交审核)"
type="info"
show-icon
:closable="false"
/>
</div>
<!-- 步骤导航条 -->
<div class="steps-wrapper">
<!-- 步骤导航条:PC端展示完整步骤,移动端展示紧凑自适应导航 -->
<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" />
@@ -55,18 +55,40 @@
</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. 合同确认']"
: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: 入职登记表 -->
<div v-show="currentStep === 0" class="step-content">
<div class="registration-header">
<h2 class="reg-title">入 职 登 记 表</h2>
<p class="reg-subtitle">请准确填写您的真实个人档案、学习经历与家庭信息</p>
<p class="reg-subtitle">请准确填写您的真实个人档案、学习经历与家庭信息(带 * 为必填项)</p>
</div>
<el-form ref="regFormRef" :model="form" :rules="regRules" label-position="top" class="reg-form">
<!-- 1. 基本信息板块 -->
<div class="card-section">
<div class="card-section-title">
<el-icon><UserFilled /></el-icon> 基本个人信息
<div class="title-icon-box"><el-icon><UserFilled /></el-icon></div>
<span>基本个人信息</span>
</div>
<el-row :gutter="16">
<el-col :xs="24" :sm="8">
@@ -99,12 +121,12 @@
</el-form-item>
</el-col>
<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-form-item>
</el-col>
<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-form-item>
</el-col>
@@ -135,7 +157,8 @@
<!-- 2. 学历与资质 -->
<div class="card-section">
<div class="card-section-title">
<el-icon><School /></el-icon> 学历与职业资格
<div class="title-icon-box"><el-icon><School /></el-icon></div>
<span>学历与职业资格</span>
</div>
<el-row :gutter="16">
<el-col :xs="12" :sm="8">
@@ -151,7 +174,7 @@
</el-col>
<el-col :xs="12" :sm="8">
<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-col>
<el-col :xs="24" :sm="8">
@@ -160,49 +183,50 @@
</el-form-item>
</el-col>
<el-col :xs="12" :sm="8">
<el-form-item label="职业资格" prop="vocationalQualification">
<el-input v-model="form.vocationalQualification" placeholder="如 软考高级 / 注册会计师" :disabled="!canEdit" />
<el-col :xs="24" :sm="8">
<el-form-item label="职业资格/职称" prop="vocationalQualification">
<el-input v-model="form.vocationalQualification" placeholder="如 注册会计师/无" :disabled="!canEdit" />
</el-form-item>
</el-col>
<el-col :xs="12" :sm="8">
<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-col>
<el-col :xs="24" :sm="8">
<el-form-item label="有何技术特长" prop="technicalSpecialty">
<el-input v-model="form.technicalSpecialty" placeholder="专业特长说明" :disabled="!canEdit" />
<el-col :xs="12" :sm="8">
<el-form-item label="特长技能" prop="technicalSpecialty">
<el-input v-model="form.technicalSpecialty" placeholder="如 驾驶/英语六级" :disabled="!canEdit" />
</el-form-item>
</el-col>
</el-row>
</div>
<!-- 3. 联系方式与薪酬账户 -->
<!-- 3. 联系方式与银行卡 -->
<div class="card-section">
<div class="card-section-title">
<el-icon><Phone /></el-icon> 通讯地址与薪酬账户
<div class="title-icon-box"><el-icon><Phone /></el-icon></div>
<span>联系方式与工资银行卡</span>
</div>
<el-row :gutter="16">
<el-col :xs="24" :sm="8">
<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-col>
<el-col :xs="24" :sm="8">
<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-col>
<el-col :xs="24" :sm="8">
<el-form-item label="暂住 / 现居住地址" prop="currentAddress">
<el-input v-model="form.currentAddress" placeholder="现详细居住地址" :disabled="!canEdit" clearable />
<el-form-item label="现居住地址" prop="currentAddress">
<el-input v-model="form.currentAddress" placeholder="目前常住的详细地址" :disabled="!canEdit" clearable />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="8">
<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-col>
<el-col :xs="24" :sm="8">
@@ -221,7 +245,8 @@
<!-- 4. 紧急联系人 -->
<div class="card-section">
<div class="card-section-title">
<el-icon><FirstAidKit /></el-icon> 紧急联系人
<div class="title-icon-box"><el-icon><FirstAidKit /></el-icon></div>
<span>紧急联系人</span>
</div>
<el-row :gutter="16">
<el-col :xs="24" :sm="8">
@@ -245,26 +270,40 @@
<!-- 5. 教育学习经历动态列表 -->
<div class="card-section">
<div class="card-section-header">
<div class="card-section-title"><el-icon><Reading /></el-icon> 教育学习经历</div>
<el-button v-if="canEdit" size="small" type="primary" plain icon="Plus" @click="addEducation">添加经历</el-button>
<div class="card-section-title">
<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 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 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-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 :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 :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 :xs="20" :sm="5">
<el-input v-model="item.result" placeholder="学习结果 (如 毕业/获学士学位)" :disabled="!canEdit" />
</el-col>
<el-col :xs="4" :sm="1" class="delete-btn-col">
<el-button v-if="canEdit" link type="danger" icon="Delete" @click="educationHistory.splice(idx, 1)" />
<el-col :xs="24" :sm="6">
<el-form-item label="结果/学位">
<el-input v-model="item.result" placeholder="如 毕业/获学士学位" :disabled="!canEdit" />
</el-form-item>
</el-col>
</el-row>
</div>
@@ -273,29 +312,45 @@
<!-- 6. 工作经历动态列表 -->
<div class="card-section">
<div class="card-section-header">
<div class="card-section-title"><el-icon><Briefcase /></el-icon> 主要工作经历</div>
<el-button v-if="canEdit" size="small" type="primary" plain icon="Plus" @click="addWork">添加经历</el-button>
<div class="card-section-title">
<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 v-if="!workHistory.length" class="empty-hint">暂未添加工作经历(应届生可留空)</div>
<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-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 :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 :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 :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 :xs="20" :sm="4">
<el-input v-model="item.leaveReason" placeholder="离职原因" :disabled="!canEdit" />
</el-col>
<el-col :xs="4" :sm="1" class="delete-btn-col">
<el-button v-if="canEdit" link type="danger" icon="Delete" @click="workHistory.splice(idx, 1)" />
<el-col :xs="24" :sm="5">
<el-form-item label="离职原因">
<el-input v-model="item.leaveReason" placeholder="简述离职原因" :disabled="!canEdit" />
</el-form-item>
</el-col>
</el-row>
</div>
@@ -304,29 +359,45 @@
<!-- 7. 家庭成员动态列表 -->
<div class="card-section">
<div class="card-section-header">
<div class="card-section-title"><el-icon><HomeFilled /></el-icon> 家庭成员</div>
<el-button v-if="canEdit" size="small" type="primary" plain icon="Plus" @click="addFamily">添加成员</el-button>
<div class="card-section-title">
<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 v-if="!familyMembers.length" class="empty-hint">暂未添加家庭成员信息</div>
<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-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 :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 :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 :xs="24" :sm="7">
<el-input v-model="item.company" placeholder="工作单位" :disabled="!canEdit" />
<el-col :xs="24" :sm="8">
<el-form-item label="工作单位">
<el-input v-model="item.company" placeholder="工作单位及职务" :disabled="!canEdit" />
</el-form-item>
</el-col>
<el-col :xs="20" :sm="5">
<el-input v-model="item.phone" placeholder="联系电话" :disabled="!canEdit" />
</el-col>
<el-col :xs="4" :sm="1" class="delete-btn-col">
<el-button v-if="canEdit" link type="danger" icon="Delete" @click="familyMembers.splice(idx, 1)" />
<el-col :xs="24" :sm="5">
<el-form-item label="联系电话">
<el-input v-model="item.phone" placeholder="手机号" :disabled="!canEdit" />
</el-form-item>
</el-col>
</el-row>
</div>
@@ -392,7 +463,7 @@
<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>
</div>
<span class="mat-limit">最多 {{ mat.maxFileCount }} 份,格式:{{ mat.allowedExtensions }}</span>
<span class="mat-limit">最多 {{ mat.maxFileCount }} 份 · 格式:{{ mat.allowedExtensions }}</span>
</div>
<div class="uploaded-files-list">
@@ -401,7 +472,7 @@
:key="f.id"
class="file-badge"
>
<el-icon><Document /></el-icon>
<el-icon color="#2563eb"><Document /></el-icon>
<span class="file-name">{{ f.fileName || f.materialName }}</span>
<el-button
v-if="canEdit"
@@ -446,13 +517,14 @@
>
<div class="contract-card-top">
<div class="contract-meta">
<el-icon color="#2563eb" size="20"><Tickets /></el-icon>
<span class="contract-title font-bold">{{ c.contractName }}</span>
<el-tag size="small" :type="c.contractStatus === 'confirmed' ? 'success' : 'warning'">
{{ c.contractStatus === 'confirmed' ? '已阅并同意' : '待查阅确认' }}
</el-tag>
</div>
<div class="contract-desc">
请点击下方按钮下载/查阅带您专属信息的合同全文。
请点击下方按钮下载/查阅带您专属信息的合同全文,仔细核对您的基本信息与薪资条款。
</div>
</div>
@@ -469,6 +541,7 @@
<el-checkbox
v-if="canEdit"
v-model="c.isAgreed"
class="agree-checkbox"
:label="`我已完整阅读并同意《${c.contractName}》全部条款`"
/>
<span v-else class="confirmed-text">✓ 候选人已确认同意</span>
@@ -479,10 +552,11 @@
<!-- 底部向导操作栏 -->
<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
v-if="currentStep < 3"
type="primary"
size="large"
@click="nextStep"
>
下一步
@@ -490,6 +564,7 @@
<el-button
v-if="currentStep === 3 && canEdit"
type="success"
size="large"
icon="Check"
:loading="submitting"
@click="submitAll"
@@ -724,6 +799,7 @@ async function nextStep() {
}
}
currentStep.value++;
window.scrollTo({ top: 0, behavior: "smooth" });
}
function getMaterialUploadUrl(materialTypeId: number) {
@@ -800,186 +876,248 @@ onMounted(load);
<style scoped>
.fill-page {
min-height: 100vh;
background: #f8fafc;
padding-bottom: 40px;
background: #f1f5f9;
padding-bottom: 50px;
}
.brand-header {
background: linear-gradient(135deg, #1e40af, #3b82f6);
background: linear-gradient(135deg, #1e3a8a 0%, #2563eb 50%, #38bdf8 100%);
color: #fff;
padding: 36px 20px;
padding: 40px 20px 48px;
text-align: center;
}
.brand-badge {
display: inline-flex;
align-items: center;
gap: 6px;
background: rgba(255, 255, 255, 0.2);
padding: 4px 12px;
background: rgba(255, 255, 255, 0.18);
backdrop-filter: blur(8px);
padding: 5px 14px;
border-radius: 20px;
font-size: 13px;
font-weight: 500;
margin-bottom: 12px;
}
.brand-title {
margin: 0 0 8px 0;
font-size: 26px;
font-weight: 700;
font-weight: 800;
letter-spacing: 1px;
}
.brand-subtitle {
margin: 0;
font-size: 14px;
opacity: 0.9;
opacity: 0.92;
}
.main-container {
max-width: 1000px;
margin: -24px auto 0;
max-width: 980px;
margin: -28px auto 0;
padding: 0 16px;
}
.status-banner {
display: flex;
justify-content: space-between;
align-items: center;
background: #fff;
padding: 12px 18px;
border-radius: 8px;
padding: 14px 20px;
border-radius: 10px;
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 {
font-weight: bold;
margin-right: 10px;
font-weight: 700;
color: #1e293b;
margin-right: 12px;
}
.expire-time {
font-size: 13px;
color: #64748b;
}
.wizard-card {
background: #fff;
border-radius: 10px;
padding: 24px;
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
border-radius: 12px;
padding: 26px 24px;
box-shadow: 0 4px 20px -2px rgba(0, 0, 0, 0.05);
}
.steps-wrapper {
margin-bottom: 30px;
padding: 0 20px;
padding: 0 10px;
}
.registration-header {
text-align: center;
border-bottom: 2px solid #e2e8f0;
border-bottom: 2px solid #f1f5f9;
padding-bottom: 16px;
margin-bottom: 24px;
}
.reg-title {
letter-spacing: 4px;
font-size: 22px;
color: #1e293b;
font-weight: 800;
color: #0f172a;
margin: 0 0 6px 0;
}
.reg-subtitle {
font-size: 13px;
color: #64748b;
margin: 0;
}
/* 卡片分区样式 */
.card-section {
background: #f8fafc;
background: #ffffff;
border: 1px solid #e2e8f0;
border-left: 3px solid #3b82f6;
border-radius: 8px;
padding: 16px 20px;
margin-bottom: 18px;
padding: 18px 20px;
margin-bottom: 20px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02);
}
.card-section-title {
font-weight: 700;
font-size: 15px;
color: #0f172a;
margin-bottom: 14px;
margin-bottom: 16px;
display: flex;
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 {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 14px;
}
.dynamic-row-box {
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 14px 16px;
margin-bottom: 12px;
}
.dynamic-row-box {
background: #fff;
border: 1px solid #cbd5e1;
border-radius: 6px;
padding: 10px 12px;
.row-badge-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.delete-btn-col {
display: flex;
align-items: center;
justify-content: center;
.row-badge {
font-size: 12px;
font-weight: 700;
color: #2563eb;
background: #eff6ff;
padding: 2px 8px;
border-radius: 4px;
}
.empty-hint {
font-size: 13px;
color: #94a3b8;
padding: 8px 0;
padding: 10px 0;
text-align: center;
}
.full-width {
width: 100%;
}
.section-header {
margin-bottom: 20px;
margin-bottom: 22px;
}
.section-title {
margin: 0 0 6px 0;
font-size: 18px;
font-weight: 700;
color: #0f172a;
}
.section-desc {
margin: 0;
font-size: 13px;
color: #64748b;
}
.all-covered-card {
padding: 40px 0;
}
.materials-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 16px;
}
.material-item-card {
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 16px;
background: #f8fafc;
}
.mat-card-header {
margin-bottom: 12px;
}
.mat-title-box {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
}
.mat-name {
font-weight: 700;
color: #1e293b;
}
.mat-limit {
font-size: 12px;
color: #64748b;
}
.file-badge {
display: flex;
align-items: center;
gap: 6px;
gap: 8px;
background: #fff;
border: 1px solid #cbd5e1;
padding: 6px 10px;
border-radius: 4px;
padding: 8px 12px;
border-radius: 6px;
font-size: 13px;
margin-bottom: 8px;
}
.file-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.contract-sign-card {
border: 1px solid #e2e8f0;
border-radius: 8px;
@@ -987,20 +1125,25 @@ onMounted(load);
background: #f8fafc;
margin-bottom: 16px;
}
.contract-meta {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
}
.contract-title {
font-size: 16px;
color: #0f172a;
}
.contract-desc {
font-size: 13px;
color: #64748b;
margin-bottom: 14px;
}
.contract-card-bottom {
display: flex;
align-items: center;
@@ -1008,14 +1151,143 @@ onMounted(load);
flex-wrap: wrap;
gap: 12px;
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 {
display: flex;
justify-content: flex-end;
gap: 12px;
gap: 14px;
margin-top: 30px;
border-top: 1px solid #e2e8f0;
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>