3185 lines
104 KiB
Vue
3185 lines
104 KiB
Vue
<template>
|
||
<main class="fill-page" :class="{ 'is-error-state': Boolean(error) }" v-loading="loading">
|
||
<!-- 异常状态展示(居中悬浮卡片,对齐 public-status 风格) -->
|
||
<div v-if="error" class="public-error-container">
|
||
<section class="error-status-card">
|
||
<div class="error-status-mark">!</div>
|
||
<h1 class="error-status-title">{{ errorTitle }}</h1>
|
||
<p class="error-status-desc">{{ errorDescription }}</p>
|
||
<div class="error-status-actions">
|
||
<el-button type="primary" class="error-retry-btn" @click="load()">重新加载</el-button>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
|
||
<!-- 正常入职内容 -->
|
||
<template v-else>
|
||
<!-- 顶部品牌横幅 -->
|
||
<header class="brand-header">
|
||
<div class="brand-content">
|
||
<div class="brand-badge">
|
||
<el-icon>
|
||
<OfficeBuilding />
|
||
</el-icon>
|
||
<span>智能入职服务</span>
|
||
</div>
|
||
<h1 class="brand-title">很高兴与你相遇,欢迎加入!</h1>
|
||
<p class="brand-subtitle">请完成以下入职流程:填写登记信息、上传证件、阅读并签署相关入职材料,开启你的职场新旅程。</p>
|
||
</div>
|
||
</header>
|
||
|
||
<!-- 容器卡片 -->
|
||
<div class="main-container">
|
||
<template v-if="task">
|
||
<!-- 任务状态提示条 -->
|
||
<div class="status-banner" :class="task.taskStatus">
|
||
<div class="status-banner-main">
|
||
<div class="status-info">
|
||
<span class="task-no">任务编号:{{ task.taskNo }}</span>
|
||
<el-tag :type="statusTagType(task.taskStatus)" size="small" effect="plain" class="status-tag">
|
||
{{ statusText(task.taskStatus) }}
|
||
</el-tag>
|
||
</div>
|
||
<div v-if="task.linkExpireAt" class="expire-time">
|
||
链接有效期至:{{ formatDateTime(task.linkExpireAt) }}
|
||
</div>
|
||
</div>
|
||
<div class="status-badge-art" aria-hidden="true">
|
||
<svg width="44" height="44" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||
<rect x="6" y="8" width="36" height="32" rx="6" fill="#ECFDF5" stroke="#10B981" stroke-width="1.5" />
|
||
<rect x="12" y="14" width="10" height="12" rx="3" fill="#059669" />
|
||
<rect x="25" y="16" width="12" height="2.5" rx="1.25" fill="#059669" />
|
||
<rect x="25" y="21" width="8" height="2" rx="1" fill="#34D399" />
|
||
<rect x="12" y="29" width="24" height="2" rx="1" fill="#10B981" />
|
||
<rect x="12" y="33" width="16" height="2" rx="1" fill="#34D399" opacity="0.7" />
|
||
<circle cx="24" cy="7" r="3" fill="#10B981" />
|
||
<path d="M21 7H27" stroke="#ECFDF5" stroke-width="1.5" stroke-linecap="round" />
|
||
</svg>
|
||
</div>
|
||
</div>
|
||
|
||
<el-alert v-if="isLinkExpired" class="link-expired-alert" title="入职链接已过期"
|
||
description="该链接已超过有效期,不能继续填写或提交。请联系工作人员重新获取入职链接。" type="error" show-icon :closable="false" />
|
||
|
||
|
||
<!-- 审核退回后向候选人明确展示需要补充或修改的内容。 -->
|
||
<el-alert v-if="task.taskStatus === 'rejected'" class="reject-reason-alert" title="退回原因"
|
||
:description="task.rejectReason?.trim() || '资料已退回,请联系工作人员确认需要补充的内容。'" type="warning" show-icon
|
||
:closable="false" />
|
||
|
||
<!-- 分步向导模式 -->
|
||
<div class="wizard-card">
|
||
<div v-if="!canEdit && !isLinkExpired" class="readonly-banner">
|
||
<el-alert title="当前为查阅模式(入职资料已提交审核)" type="info" show-icon :closable="false" />
|
||
</div>
|
||
|
||
<!-- 步骤导航条:PC端展示完整步骤,移动端展示紧凑自适应导航 -->
|
||
<div class="steps-wrapper desktop-only">
|
||
<el-steps :active="currentStep" finish-status="success" align-center>
|
||
<el-step v-for="step in visibleSteps" :key="step.key" :title="step.title" :icon="step.icon" />
|
||
|
||
|
||
|
||
</el-steps>
|
||
</div>
|
||
|
||
<div class="mobile-steps-wrapper mobile-only">
|
||
<div class="mobile-steps-track">
|
||
<div v-for="(step, index) in visibleSteps" :key="step.key"
|
||
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">{{ (index + 1) + '. ' + step.shortTitle }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="mobile-current-indicator">
|
||
当前进行:<span class="highlight-step">第 {{ currentStep + 1 }}/{{ visibleSteps.length }} 步 · {{ currentStepDefinition.description }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 步骤 1: 入职登记表 -->
|
||
<div v-show="currentStepKey === 'registration'" class="step-content">
|
||
<div class="registration-header">
|
||
<h2 class="reg-title">入 职 登 记 表</h2>
|
||
<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">
|
||
<div class="title-icon-box"><el-icon>
|
||
<User />
|
||
</el-icon></div>
|
||
<span>基本个人信息</span>
|
||
</div>
|
||
<el-row :gutter="16">
|
||
<el-col :xs="24" :sm="8">
|
||
<el-form-item label="真实姓名" prop="employeeName">
|
||
<el-input v-model="form.employeeName" placeholder="请输入真实姓名" :disabled="!canEdit" clearable />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :xs="24" :sm="8">
|
||
<el-form-item label="身份证号码" prop="idCardNumber">
|
||
<el-input v-model="form.idCardNumber" placeholder="请输入18位身份证号" :disabled="!canEdit" maxlength="18"
|
||
clearable />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :xs="24" :sm="8">
|
||
<el-form-item label="出生年月日" prop="birthday">
|
||
<el-input v-model="form.birthday" placeholder="根据身份证号自动生成" readonly :disabled="!canEdit"
|
||
class="full-width readonly-derived-input">
|
||
<template #prefix>
|
||
<el-icon><Calendar /></el-icon>
|
||
</template>
|
||
</el-input>
|
||
</el-form-item>
|
||
</el-col>
|
||
|
||
<el-col :xs="12" :sm="8">
|
||
<el-form-item label="性别" prop="gender">
|
||
<el-select v-model="form.gender" placeholder="请选择性别" :disabled="!canEdit" class="full-width">
|
||
<el-option label="男" value="男" />
|
||
<el-option label="女" value="女" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :xs="12" :sm="8">
|
||
<el-form-item label="民族" prop="nation">
|
||
<el-input v-model="form.nation" placeholder="如 汉族" :disabled="!canEdit" clearable />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :xs="24" :sm="8">
|
||
<el-form-item label="户籍所在地" prop="registeredResidence">
|
||
<el-input v-model="form.registeredResidence" placeholder="如 某省某市某区/县" :disabled="!canEdit"
|
||
clearable />
|
||
</el-form-item>
|
||
</el-col>
|
||
|
||
<el-col :xs="12" :sm="4">
|
||
<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-input v-model="form.weight" placeholder="如 65" :disabled="!canEdit" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :xs="12" :sm="8">
|
||
<el-form-item label="健康状况" prop="healthCondition">
|
||
<el-input v-model="form.healthCondition" placeholder="如 良好 / 无既往病史" :disabled="!canEdit" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :xs="12" :sm="8">
|
||
<el-form-item label="婚育情况" prop="maritalStatus">
|
||
<el-select v-model="form.maritalStatus" placeholder="请选择" :disabled="!canEdit" class="full-width">
|
||
<el-option label="未婚" value="未婚" />
|
||
<el-option label="已婚已育" value="已婚已育" />
|
||
<el-option label="已婚未育" value="已婚未育" />
|
||
<el-option label="其他" value="其他" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
</div>
|
||
|
||
<!-- 2. 学历与资质 -->
|
||
<div class="card-section">
|
||
<div class="card-section-title">
|
||
<div class="title-icon-box"><el-icon>
|
||
<School />
|
||
</el-icon></div>
|
||
<span>学历与职业资格</span>
|
||
</div>
|
||
<el-row :gutter="16">
|
||
<el-col :xs="12" :sm="8">
|
||
<el-form-item label="最高学历" prop="education">
|
||
<el-select v-model="form.education" placeholder="请选择学历" :disabled="!canEdit" class="full-width">
|
||
<el-option label="博士研究生" value="博士研究生" />
|
||
<el-option label="硕士研究生" value="硕士研究生" />
|
||
<el-option label="本科" value="本科" />
|
||
<el-option label="大专" value="大专" />
|
||
<el-option label="中专/高中及以下" value="高中及以下" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</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-form-item>
|
||
</el-col>
|
||
<el-col :xs="24" :sm="8">
|
||
<el-form-item label="毕业院校" prop="graduatedSchool">
|
||
<el-input v-model="form.graduatedSchool" placeholder="请输入毕业院校全称" :disabled="!canEdit" clearable />
|
||
</el-form-item>
|
||
</el-col>
|
||
|
||
<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-form-item>
|
||
</el-col>
|
||
<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. 联系方式与银行卡 -->
|
||
<div class="card-section">
|
||
<div class="card-section-title">
|
||
<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="手机号码" :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-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>
|
||
</el-col>
|
||
|
||
<el-col :xs="24" :sm="8">
|
||
<el-form-item label="房东联系方式" prop="landlordContact">
|
||
<el-input v-model="form.landlordContact" placeholder="租房房东姓名及电话 (选填)" :disabled="!canEdit" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :xs="24" :sm="8">
|
||
<el-form-item label="工资银行卡号" prop="bankCardNumber">
|
||
<el-input v-model="form.bankCardNumber" placeholder="用于发放工资的银行卡号" :disabled="!canEdit"
|
||
clearable />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :xs="24" :sm="8">
|
||
<el-form-item label="开户银行全称" prop="bankName">
|
||
<el-input v-model="form.bankName" placeholder="如 招商银行深圳高新支行" :disabled="!canEdit" clearable />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
</div>
|
||
|
||
<!-- 4. 紧急联系人 -->
|
||
<div class="card-section">
|
||
<div class="card-section-title">
|
||
<div class="title-icon-box"><el-icon>
|
||
<FirstAidKit />
|
||
</el-icon></div>
|
||
<span>紧急联系人</span>
|
||
</div>
|
||
<el-row :gutter="16">
|
||
<el-col :xs="24" :sm="8">
|
||
<el-form-item label="紧急联系人姓名" prop="emergencyContact">
|
||
<el-input v-model="form.emergencyContact" placeholder="亲属或紧急联系人姓名" :disabled="!canEdit" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :xs="24" :sm="8">
|
||
<el-form-item label="紧急联系人电话" prop="emergencyPhone">
|
||
<el-input v-model="form.emergencyPhone" placeholder="联系电话" :disabled="!canEdit" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :xs="24" :sm="8">
|
||
<el-form-item label="紧急联系人地址" prop="emergencyAddress">
|
||
<el-input v-model="form.emergencyAddress" placeholder="联系地址" :disabled="!canEdit" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
</div>
|
||
|
||
<!-- 5. 教育学习经历动态列表 -->
|
||
<div id="education-history-section" class="card-section">
|
||
<div class="card-section-header">
|
||
<div class="card-section-title">
|
||
<div class="title-icon-box"><el-icon>
|
||
<Reading />
|
||
</el-icon></div>
|
||
<span><span class="required-asterisk">*</span>教育学习经历</span>
|
||
</div>
|
||
<el-button v-if="canEdit" size="small" class="section-action-btn" icon="Plus" @click="addEducation">
|
||
添加经历
|
||
</el-button>
|
||
</div>
|
||
<div v-if="!educationHistory.length" class="empty-hint">暂未添加学习经历,可点击右上角添加(至少填写一条最高学历)</div>
|
||
<div v-for="(item, idx) in educationHistory" :id="`education-history-${idx}`" :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-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-form-item label="学校/机构">
|
||
<el-input v-model="item.location" placeholder="毕业学校全称" :disabled="!canEdit" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :xs="24" :sm="6">
|
||
<el-form-item label="专业/学习内容">
|
||
<el-input v-model="item.content" placeholder="所学专业" :disabled="!canEdit" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<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>
|
||
</div>
|
||
|
||
<!-- 6. 工作经历动态列表 -->
|
||
<div id="work-history-section" class="card-section">
|
||
<div class="card-section-header">
|
||
<div class="card-section-title">
|
||
<div class="title-icon-box"><el-icon>
|
||
<Briefcase />
|
||
</el-icon></div>
|
||
<span><span class="required-asterisk">*</span>主要工作经历</span>
|
||
</div>
|
||
<el-button v-if="canEdit" size="small" class="section-action-btn" icon="Plus" @click="addWork">
|
||
添加经历
|
||
</el-button>
|
||
</div>
|
||
<div v-if="!workHistory.length" class="empty-hint">暂未添加工作经历,可点击右上角添加(应届生可填兼职、在校或实习经历)</div>
|
||
<div v-for="(item, idx) in workHistory" :id="`work-history-${idx}`" :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-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-form-item label="工作单位名称">
|
||
<el-input v-model="item.companyName" placeholder="工作单位/实习单位全称" :disabled="!canEdit" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :xs="12" :sm="4">
|
||
<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-form-item label="薪资待遇">
|
||
<el-input v-model="item.salary" placeholder="薪资 (选填)" :disabled="!canEdit" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<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>
|
||
</div>
|
||
|
||
<!-- 7. 家庭成员动态列表 -->
|
||
<div id="family-members-section" class="card-section">
|
||
<div class="card-section-header">
|
||
<div class="card-section-title">
|
||
<div class="title-icon-box"><el-icon>
|
||
<HomeFilled />
|
||
</el-icon></div>
|
||
<span><span class="required-asterisk">*</span>家庭成员</span>
|
||
</div>
|
||
<el-button v-if="canEdit" size="small" class="section-action-btn" icon="Plus" @click="addFamily">
|
||
添加成员
|
||
</el-button>
|
||
</div>
|
||
<div v-if="!familyMembers.length" class="empty-hint">暂未添加家庭成员信息,可点击右上角添加(至少填写一位直系亲属)</div>
|
||
<div v-for="(item, idx) in familyMembers" :id="`family-member-${idx}`" :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-form-item label="姓名">
|
||
<el-input v-model="item.name" placeholder="成员姓名" :disabled="!canEdit" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :xs="12" :sm="4">
|
||
<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-form-item label="年龄">
|
||
<el-input v-model="item.age" placeholder="年龄" :disabled="!canEdit" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<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="24" :sm="5">
|
||
<el-form-item label="联系电话">
|
||
<el-input v-model="item.phone" placeholder="手机号" :disabled="!canEdit" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
</div>
|
||
</div>
|
||
</el-form>
|
||
</div>
|
||
|
||
<!-- 步骤 2: 合同专属增量补充项 (自适应差集) -->
|
||
<div v-if="extraVariables.length" v-show="currentStepKey === 'extra'" class="step-content">
|
||
<div class="section-header">
|
||
<h3 class="section-title">签署材料专属补充信息</h3>
|
||
<p class="section-desc">以下是本次签署材料中特殊约定的增量字段,登记表未涵盖的项需在此补充确认</p>
|
||
</div>
|
||
|
||
<el-form label-position="top" class="extra-form">
|
||
<el-alert title="请补充填写以下签署材料专有信息:" type="info" show-icon :closable="false" class="mb-3" />
|
||
<el-row :gutter="16">
|
||
<el-col v-for="(v, index) in extraVariables" :id="`extra-variable-${index}`" :key="v.variableCode"
|
||
:xs="24" :sm="12">
|
||
<el-form-item :label="v.variableName || v.variableCode" :required="v.required"
|
||
:error="extraVariableErrors[v.variableCode]">
|
||
<el-select v-if="getVariableOptions(v).length" v-model="customVariables[v.variableCode]"
|
||
:placeholder="`请选择${v.variableName}`" :disabled="!canEdit" class="full-width" clearable
|
||
@change="clearExtraVariableError(v.variableCode)">
|
||
<el-option v-for="option in getVariableOptions(v)" :key="option.value" :label="option.label"
|
||
:value="option.value" />
|
||
</el-select>
|
||
<el-date-picker v-else-if="getVariableDataType(v) === 'date'"
|
||
v-model="customVariables[v.variableCode]" type="date" value-format="YYYY-MM-DD"
|
||
:placeholder="`请选择${v.variableName}`" :disabled="!canEdit" class="full-width"
|
||
@change="clearExtraVariableError(v.variableCode)" />
|
||
<el-input v-else v-model="customVariables[v.variableCode]"
|
||
:type="getVariableDataType(v) === 'number' ? 'number' : 'text'"
|
||
:inputmode="getVariableDataType(v) === 'number' ? 'decimal' : 'text'"
|
||
:placeholder="v.defaultValue || `请输入${v.variableName}`" :disabled="!canEdit" clearable
|
||
@input="clearExtraVariableError(v.variableCode)" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
</el-form>
|
||
</div>
|
||
|
||
<!-- 步骤 3: 证件材料上传 -->
|
||
<div v-show="currentStepKey === 'materials'" class="step-content">
|
||
<div class="section-header">
|
||
<h3 class="section-title">证件与材料上传</h3>
|
||
<p class="section-desc">请上传清晰真实有效的证件扫描件或照片(支持 PNG、JPG、PDF)</p>
|
||
</div>
|
||
|
||
<el-empty v-if="!materialRequirements.length" description="本次入职无需上传附加证件材料" />
|
||
|
||
<div v-else class="materials-grid">
|
||
<div v-for="mat in materialRequirements" :id="`material-requirement-${mat.materialTypeId}`"
|
||
:key="mat.materialTypeId" class="material-item-card">
|
||
<div class="mat-card-header">
|
||
<div class="mat-title-box">
|
||
<span class="mat-name">{{ mat.materialName }}</span>
|
||
<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">{{ getMaterialLimitText(mat) }}</span>
|
||
</div>
|
||
|
||
<div class="uploaded-files-list">
|
||
<div v-for="f in getUploadedMaterials(mat.materialTypeId)" :key="`${f.id}-${f.fileId}`"
|
||
class="file-badge">
|
||
<!-- 图片点击缩略图即可在手机端放大预览,非图片继续显示文件图标。 -->
|
||
<el-image v-if="isImageFile(f) && getMaterialSourceUrl(f)" class="material-thumbnail"
|
||
:src="getMaterialSourceUrl(f)" :preview-src-list="[getMaterialSourceUrl(f)]" fit="cover" preview-teleported />
|
||
<el-icon v-else class="material-file-icon" color="#059669">
|
||
<Document />
|
||
</el-icon>
|
||
|
||
<div class="file-content">
|
||
<span class="file-name" :title="f.fileName || f.materialName">
|
||
{{ f.fileName || f.materialName }}
|
||
</span>
|
||
<span v-if="f.fileSizeInfo" class="file-size">{{ f.fileSizeInfo }}</span>
|
||
<span v-if="isImageFile(f)" class="preview-tip">点击图片可放大预览</span>
|
||
</div>
|
||
|
||
<div v-if="canEdit" class="file-actions">
|
||
<el-upload action="#" :accept="getUploadAccept(mat)"
|
||
:disabled="isMaterialUploading(mat.materialTypeId)" :show-file-list="false"
|
||
:http-request="(options: any) => uploadMaterial(options, mat.materialTypeId, f.id)"
|
||
:on-success="(response: any) => onUploadSuccess(response, mat.materialTypeId)"
|
||
:on-error="(error: Error) => onUploadError(error, mat.materialTypeId)"
|
||
:before-upload="(file: File) => beforeUpload(file, mat)">
|
||
<el-button link type="primary" icon="Refresh"
|
||
:loading="isMaterialUploading(mat.materialTypeId)">
|
||
重新上传
|
||
</el-button>
|
||
</el-upload>
|
||
<el-button link type="danger" icon="Delete" @click="deleteFile(f.id)">
|
||
删除
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="canEdit && getRemainingFileCount(mat) > 0" class="upload-action-box">
|
||
<el-upload action="#" :accept="getUploadAccept(mat)"
|
||
:disabled="isMaterialUploading(mat.materialTypeId)" :show-file-list="false"
|
||
:http-request="(options: any) => uploadMaterial(options, mat.materialTypeId)"
|
||
:on-success="(response: any) => onUploadSuccess(response, mat.materialTypeId)"
|
||
:on-error="(error: Error) => onUploadError(error, mat.materialTypeId)"
|
||
:before-upload="(file: File) => beforeUpload(file, mat)">
|
||
<el-button size="small" type="primary" plain icon="Upload"
|
||
:loading="isMaterialUploading(mat.materialTypeId)">
|
||
{{ getUploadedMaterials(mat.materialTypeId).length ? "继续上传" : "上传文件 / 照片" }}
|
||
</el-button>
|
||
</el-upload>
|
||
<span class="upload-count">
|
||
已上传 {{ getUploadedMaterials(mat.materialTypeId).length }}/{{ getMaxFileCount(mat) }},
|
||
还可上传 {{ getRemainingFileCount(mat) }} 份
|
||
</span>
|
||
</div>
|
||
|
||
<div v-else-if="canEdit && getUploadedMaterials(mat.materialTypeId).length"
|
||
class="upload-limit-reached">
|
||
已上传 {{ getUploadedMaterials(mat.materialTypeId).length }}/{{ getMaxFileCount(mat) }};
|
||
如需更换,请点击对应文件的“重新上传”
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 步骤 4: 合同查阅与签署 -->
|
||
<div v-show="currentStepKey === 'confirmation'" class="step-content">
|
||
<div v-if="canEdit" id="identity-confirmation-card" class="identity-confirmation-card">
|
||
<div class="identity-confirmation-heading">
|
||
<div>
|
||
<h3 class="identity-confirmation-title">实名认证信息核对确认</h3>
|
||
<p class="identity-confirmation-desc">
|
||
以下三项将直接用于电子签约实名核验与法定人事建档,请对照二代身份证及本人实名手机号逐项核对。
|
||
</p>
|
||
</div>
|
||
<el-tag type="primary" effect="plain">签约实名核验</el-tag>
|
||
</div>
|
||
|
||
<el-alert title="请核对姓名、证件号码及手机号与实际完全一致,信息有误将导致电子签约证书核发失败。" type="info" show-icon :closable="false" />
|
||
|
||
<div class="identity-confirmation-list">
|
||
<div v-for="field in identityFields" :key="field.key" class="identity-confirmation-item">
|
||
<div class="identity-field-row">
|
||
<span class="identity-field-label">{{ field.label }}</span>
|
||
<span class="identity-field-value">{{ identityDisplayValue(field.key) }}</span>
|
||
</div>
|
||
<el-checkbox :model-value="isIdentityFieldConfirmed(field.key)" class="identity-confirmation-checkbox"
|
||
@change="setIdentityFieldConfirmation(field.key, $event)">
|
||
{{ field.confirmText }}
|
||
</el-checkbox>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="identity-confirmation-footer">
|
||
<span class="identity-confirmation-progress" :class="{ 'is-complete': isIdentityConfirmationComplete }">
|
||
已确认 {{ confirmedIdentityFieldCount }}/{{ identityFields.length }} 项
|
||
</span>
|
||
<el-button type="primary" plain @click="returnToIdentityInformation">
|
||
返回第一步修改实名信息
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section-header">
|
||
<h3 class="section-title">入职签署材料查阅与确认</h3>
|
||
<p class="section-desc">系统已为您代入真实登记信息生成专属签署材料,请认真查阅条款并勾选确认</p>
|
||
</div>
|
||
|
||
<el-alert v-if="preparingContract" class="contract-preparing-alert" title="正在生成您的专属签署材料"
|
||
:description="contractPreparingDescription" type="info" show-icon :closable="false" />
|
||
|
||
<el-empty v-if="!contracts.length" description="暂无待确认签署材料" />
|
||
|
||
<div v-else class="contracts-list">
|
||
<div v-for="c in contracts" :id="`contract-confirm-${c.id}`" :key="c.id" class="contract-sign-card">
|
||
<div class="contract-card-top">
|
||
<div class="contract-meta">
|
||
<el-icon color="#059669" 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>
|
||
|
||
<el-alert v-if="getContractPrepareError(c.id)" class="contract-prepare-error"
|
||
:title="getContractPrepareError(c.id)" type="error" show-icon :closable="false" />
|
||
|
||
<div class="contract-card-bottom">
|
||
<el-button :type="isContractPrepareFailed(c.id) ? 'danger' : 'primary'" plain
|
||
:icon="isContractPrepareFailed(c.id) ? 'Refresh' : 'Download'" :loading="isContractBusy(c.id)"
|
||
:disabled="isContractDownloading(c.id) || (!isContractPrepared(c.id) && !isContractPrepareFailed(c.id))"
|
||
@click="handleContractAction(c)">
|
||
{{ getContractActionText(c.id) }}
|
||
</el-button>
|
||
|
||
<el-checkbox v-if="canEdit" v-model="c.isAgreed" class="agree-checkbox"
|
||
:disabled="!isContractPrepared(c.id) || !c.hasViewed" :label="`我已完整阅读并同意《${c.contractName}》全部条款`" />
|
||
<span v-else class="confirmed-text">✓ 候选人已确认同意</span>
|
||
</div>
|
||
|
||
<div v-if="canEdit && isContractPrepared(c.id) && !c.hasViewed" class="contract-view-hint">
|
||
请先点击“下载并查看签署材料”,查看后才能勾选确认。
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 底部向导操作栏 -->
|
||
<div class="wizard-footer">
|
||
<el-button v-if="currentStep > 0" size="large" :disabled="savingStep || isAnyMaterialUploading" @click="currentStep--">上一步</el-button>
|
||
<el-button v-if="currentStep < visibleSteps.length - 1" type="primary" size="large"
|
||
:loading="savingStep || (currentStepKey === 'materials' && isAnyMaterialUploading)"
|
||
:disabled="savingStep || isAnyMaterialUploading"
|
||
@click="nextStep">
|
||
{{ (currentStepKey === 'materials' && isAnyMaterialUploading) ? "材料上传中..." : (savingStep ? "保存中..." : "下一步") }}
|
||
</el-button>
|
||
<el-button v-if="currentStepKey === 'confirmation' && canEdit" type="success" size="large" icon="Check"
|
||
:loading="submitting" :disabled="savingStep || isAnyMaterialUploading" @click="submitAll">
|
||
确认无误并提交入职资料
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</template>
|
||
</main>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from "vue";
|
||
import { useRoute } from "vue-router";
|
||
import { ElMessage as RawElMessage, ElMessageBox, FormInstance, FormRules } from "element-plus";
|
||
|
||
/**
|
||
* 移动端/公开端单例独占式 Toast 封装:
|
||
* 1. 弹出新提示前自动 closeAll(),彻底杜绝手机端多条提示同时排队堆叠满屏的问题
|
||
* 2. 启用 grouping: true 自动合并短时间内同一文案的提示
|
||
*/
|
||
const ElMessage = {
|
||
success(options: any) {
|
||
RawElMessage.closeAll();
|
||
const config = typeof options === "string" ? { message: options } : options;
|
||
return RawElMessage.success({ grouping: true, ...config });
|
||
},
|
||
warning(options: any) {
|
||
RawElMessage.closeAll();
|
||
const config = typeof options === "string" ? { message: options } : options;
|
||
return RawElMessage.warning({ grouping: true, ...config });
|
||
},
|
||
error(options: any) {
|
||
RawElMessage.closeAll();
|
||
const config = typeof options === "string" ? { message: options } : options;
|
||
return RawElMessage.error({ grouping: true, ...config });
|
||
},
|
||
info(options: any) {
|
||
RawElMessage.closeAll();
|
||
const config = typeof options === "string" ? { message: options } : options;
|
||
return RawElMessage.info({ grouping: true, ...config });
|
||
},
|
||
closeAll() {
|
||
RawElMessage.closeAll();
|
||
}
|
||
};
|
||
import {
|
||
OnboardingPublicTask,
|
||
onboardingPublicApi
|
||
} from "@/api/modules/open/onboarding";
|
||
import {
|
||
getIdCardBirthday,
|
||
getIdCardGender,
|
||
getIdCardValidationMessage,
|
||
isBankCardNumber,
|
||
isContactPhone,
|
||
isEmail,
|
||
isMobilePhone,
|
||
isNumberInRange,
|
||
normalizeBankCardNumber,
|
||
normalizeIdCardNumber
|
||
} from "@/utils/onboardingValidation";
|
||
|
||
const route = useRoute();
|
||
const token = ref((route.query.token as string) || "");
|
||
const viewportMeta = document.querySelector<HTMLMetaElement>('meta[name="viewport"]');
|
||
const originalViewportContent = viewportMeta?.content;
|
||
|
||
const loading = ref(false);
|
||
const submitting = ref(false);
|
||
const savingStep = ref(false);
|
||
const error = ref("");
|
||
const errorTitle = computed(() => {
|
||
const msg = error.value || "";
|
||
if (msg.includes("链接不存在") || msg.includes("Token") || msg.includes("无效")) {
|
||
return "入职链接无效";
|
||
}
|
||
if (msg.includes("过期")) {
|
||
return "入职链接已过期";
|
||
}
|
||
return "访问受限";
|
||
});
|
||
|
||
const errorDescription = computed(() => {
|
||
const msg = error.value || "";
|
||
if (msg.includes("链接不存在") || msg.includes("Token") || msg.includes("无效")) {
|
||
return "入职链接不存在或已失效,请联系招聘人员重新获取入职链接。";
|
||
}
|
||
if (msg.includes("过期")) {
|
||
return "该链接已超过有效期,不能继续填写。请联系招聘人员重新获取入职链接。";
|
||
}
|
||
return `${msg},请检查网络或联系招聘人员协助处理。`;
|
||
});
|
||
const currentTimestamp = ref(Date.now());
|
||
let linkExpiryTimer: number | undefined;
|
||
const currentStep = ref(0);
|
||
const uploadingMaterials = reactive<Record<string, boolean>>({});
|
||
const isAnyMaterialUploading = computed(() => Object.values(uploadingMaterials).some(Boolean));
|
||
type ContractPrepareStatus = "idle" | "preparing" | "ready" | "error";
|
||
const contractPrepareStatus = reactive<Record<string, ContractPrepareStatus>>({});
|
||
const contractPrepareErrors = reactive<Record<string, string>>({});
|
||
const contractDownloading = reactive<Record<string, boolean>>({});
|
||
const materialPreviewUrls = reactive<Record<string, string>>({});
|
||
let materialPreviewGeneration = 0;
|
||
|
||
const task = ref<OnboardingPublicTask | null>(null);
|
||
const contracts = ref<any[]>([]);
|
||
const materialRequirements = ref<any[]>([]);
|
||
const uploadedMaterials = ref<any[]>([]);
|
||
const contractVariables = ref<any[]>([]);
|
||
const formFieldRequirements = ref<any[]>([]);
|
||
|
||
const regFormRef = ref<FormInstance>();
|
||
|
||
// 1. 入职登记表主表单
|
||
const form = reactive<any>({
|
||
employeeName: "",
|
||
gender: "男",
|
||
nation: "汉族",
|
||
birthday: "",
|
||
registeredResidence: "",
|
||
height: "",
|
||
weight: "",
|
||
healthCondition: "良好",
|
||
maritalStatus: "未婚",
|
||
education: "本科",
|
||
major: "",
|
||
graduatedSchool: "",
|
||
vocationalQualification: "",
|
||
qualificationLevel: "",
|
||
technicalSpecialty: "",
|
||
phone: "",
|
||
email: "",
|
||
idCardNumber: "",
|
||
currentAddress: "",
|
||
landlordContact: "",
|
||
bankCardNumber: "",
|
||
bankName: "",
|
||
bankAccountName: "",
|
||
emergencyContact: "",
|
||
emergencyPhone: "",
|
||
emergencyAddress: "",
|
||
onboardingDepartment: "",
|
||
onboardingDate: "",
|
||
onboardingPosition: "",
|
||
otherRemarks: ""
|
||
});
|
||
|
||
type IdentityField = "employeeName" | "idCardNumber" | "phone";
|
||
|
||
const identityFields: Array<{
|
||
key: IdentityField;
|
||
label: string;
|
||
confirmText: string;
|
||
}> = [
|
||
{ key: "employeeName", label: "真实姓名", confirmText: "我已核对姓名,与本人身份证一致" },
|
||
{ key: "idCardNumber", label: "身份证号码", confirmText: "我已核对身份证号码完整且无误" },
|
||
{ key: "phone", label: "手机号码", confirmText: "我已核对手机号,可用于本人实名认证" }
|
||
];
|
||
|
||
const identityConfirmations = reactive<Record<IdentityField, boolean>>({
|
||
employeeName: false,
|
||
idCardNumber: false,
|
||
phone: false
|
||
});
|
||
const identityConfirmedValues = reactive<Record<IdentityField, string>>({
|
||
employeeName: "",
|
||
idCardNumber: "",
|
||
phone: ""
|
||
});
|
||
|
||
function normalizeIdentityValue(field: IdentityField, value: unknown) {
|
||
const normalized = String(value ?? "").trim();
|
||
return field === "idCardNumber" ? normalized.toUpperCase() : normalized;
|
||
}
|
||
|
||
function identityDisplayValue(field: IdentityField) {
|
||
return normalizeIdentityValue(field, form[field]) || "未填写";
|
||
}
|
||
|
||
function isIdentityFieldConfirmed(field: IdentityField) {
|
||
return identityConfirmations[field]
|
||
&& identityConfirmedValues[field] === normalizeIdentityValue(field, form[field]);
|
||
}
|
||
|
||
function setIdentityFieldConfirmation(field: IdentityField, value: unknown) {
|
||
const checked = Boolean(value);
|
||
identityConfirmations[field] = checked;
|
||
identityConfirmedValues[field] = checked
|
||
? normalizeIdentityValue(field, form[field])
|
||
: "";
|
||
}
|
||
|
||
function resetIdentityConfirmations() {
|
||
identityFields.forEach(({ key }) => {
|
||
identityConfirmations[key] = false;
|
||
identityConfirmedValues[key] = "";
|
||
});
|
||
}
|
||
|
||
const confirmedIdentityFieldCount = computed(() =>
|
||
identityFields.filter(({ key }) => isIdentityFieldConfirmed(key)).length
|
||
);
|
||
const isIdentityConfirmationComplete = computed(() =>
|
||
confirmedIdentityFieldCount.value === identityFields.length
|
||
);
|
||
|
||
function returnToIdentityInformation() {
|
||
resetIdentityConfirmations();
|
||
currentStep.value = 0;
|
||
window.scrollTo({ top: 0, behavior: "smooth" });
|
||
nextTick(() => regFormRef.value?.scrollToField("employeeName"));
|
||
}
|
||
|
||
watch(
|
||
() => identityFields.map(({ key }) => normalizeIdentityValue(key, form[key])),
|
||
values => {
|
||
identityFields.forEach(({ key }, index) => {
|
||
if (identityConfirmations[key] && identityConfirmedValues[key] !== values[index]) {
|
||
identityConfirmations[key] = false;
|
||
identityConfirmedValues[key] = "";
|
||
}
|
||
});
|
||
},
|
||
{ flush: "sync" }
|
||
);
|
||
|
||
// 多条动态经历
|
||
const educationHistory = ref<{ timeRange: string; location: string; content: string; result: string }[]>([]);
|
||
const workHistory = ref<{ timeRange: string; companyName: string; salary: string; position: string; leaveReason: string }[]>([]);
|
||
const familyMembers = ref<{ name: string; relationship: string; age: string; company: string; phone: string }[]>([]);
|
||
|
||
// 2. 特殊合同增量变量 (差集)
|
||
const customVariables = reactive<Record<string, string>>({});
|
||
|
||
function validateEmployeeName(_rule: any, value: unknown, callback: (error?: Error) => void) {
|
||
const employeeName = String(value ?? "").trim();
|
||
if (!employeeName) {
|
||
return callback(new Error("请输入真实姓名"));
|
||
}
|
||
if (employeeName.length > 100) {
|
||
return callback(new Error("姓名不能超过100个字符"));
|
||
}
|
||
callback();
|
||
}
|
||
|
||
function validatePhone(_rule: any, value: unknown, callback: (error?: Error) => void) {
|
||
if (!isMobilePhone(value)) {
|
||
return callback(new Error("请输入正确的11位手机号码"));
|
||
}
|
||
callback();
|
||
}
|
||
|
||
function validateIdCardNumber(_rule: any, value: unknown, callback: (error?: Error) => void) {
|
||
const message = getIdCardValidationMessage(value);
|
||
if (message) {
|
||
return callback(new Error(message));
|
||
}
|
||
callback();
|
||
}
|
||
|
||
function validateBirthday(_rule: any, value: unknown, callback: (error?: Error) => void) {
|
||
if (!value) {
|
||
return callback();
|
||
}
|
||
|
||
const birthday = String(value);
|
||
const idCardBirthday = getIdCardBirthday(form.idCardNumber);
|
||
if (idCardBirthday && birthday !== idCardBirthday) {
|
||
return callback(new Error("出生日期与身份证号码不一致"));
|
||
}
|
||
callback();
|
||
}
|
||
|
||
// 监听身份证号码输入,自动提取出生年月日并联动推导性别
|
||
watch(
|
||
() => form.idCardNumber,
|
||
(newVal) => {
|
||
const rawIdCard = normalizeIdCardNumber(newVal);
|
||
if (!rawIdCard) {
|
||
form.birthday = "";
|
||
return;
|
||
}
|
||
const birthday = getIdCardBirthday(rawIdCard);
|
||
if (birthday) {
|
||
form.birthday = birthday;
|
||
// 联动推导性别(若尚未手动选择过)
|
||
const gender = getIdCardGender(rawIdCard);
|
||
if (gender && (!form.gender || form.gender === "男" || form.gender === "女")) {
|
||
form.gender = gender;
|
||
}
|
||
nextTick(() => {
|
||
regFormRef.value?.clearValidate(["birthday", "gender"]);
|
||
});
|
||
} else if (rawIdCard.length < 18) {
|
||
form.birthday = "";
|
||
}
|
||
},
|
||
{ immediate: true }
|
||
);
|
||
|
||
function validateBankCardNumber(_rule: any, value: unknown, callback: (error?: Error) => void) {
|
||
if (value && !isBankCardNumber(value)) {
|
||
return callback(new Error("请输入12至19位银行卡号"));
|
||
}
|
||
callback();
|
||
}
|
||
|
||
function validateOptionalEmail(_rule: any, value: unknown, callback: (error?: Error) => void) {
|
||
if (value && !isEmail(value)) {
|
||
return callback(new Error("请输入正确的电子邮箱"));
|
||
}
|
||
callback();
|
||
}
|
||
|
||
function validateOptionalHeight(_rule: any, value: unknown, callback: (error?: Error) => void) {
|
||
if (value && !isNumberInRange(value, 100, 250)) {
|
||
return callback(new Error("身高应为100至250厘米之间的数字"));
|
||
}
|
||
callback();
|
||
}
|
||
|
||
function validateOptionalWeight(_rule: any, value: unknown, callback: (error?: Error) => void) {
|
||
if (value && !isNumberInRange(value, 30, 300)) {
|
||
return callback(new Error("体重应为30至300公斤之间的数字"));
|
||
}
|
||
callback();
|
||
}
|
||
|
||
function validateOptionalContactPhone(_rule: any, value: unknown, callback: (error?: Error) => void) {
|
||
if (value && !isContactPhone(value)) {
|
||
return callback(new Error("请输入正确的手机号码或座机号码"));
|
||
}
|
||
callback();
|
||
}
|
||
|
||
/** 将 employee.EmployeeName 转为登记表模型使用的 employeeName。 */
|
||
function getRegistrationFieldKey(fieldCode: unknown) {
|
||
const code = String(fieldCode || "").trim();
|
||
if (!code.toLowerCase().startsWith("employee.")) {
|
||
return "";
|
||
}
|
||
|
||
const propertyName = code.substring(code.lastIndexOf(".") + 1);
|
||
return propertyName
|
||
? propertyName.charAt(0).toLowerCase() + propertyName.slice(1)
|
||
: "";
|
||
}
|
||
|
||
const requiredRegistrationFields = computed(() => {
|
||
const result = new Map<string, any>();
|
||
formFieldRequirements.value.forEach(requirement => {
|
||
const fieldKey = getRegistrationFieldKey(requirement.fieldCode);
|
||
if (!fieldKey || !(fieldKey in form)) {
|
||
return;
|
||
}
|
||
|
||
const existing = result.get(fieldKey);
|
||
if (!existing || requirement.required) {
|
||
result.set(fieldKey, requirement);
|
||
}
|
||
});
|
||
return result;
|
||
});
|
||
|
||
function appendRegistrationRule(rules: FormRules, fieldKey: string, rule: any) {
|
||
const existingRules = rules[fieldKey];
|
||
const fieldRules = Array.isArray(existingRules)
|
||
? existingRules
|
||
: existingRules
|
||
? [existingRules]
|
||
: [];
|
||
rules[fieldKey] = [...fieldRules, rule];
|
||
}
|
||
|
||
// 固定登记表必填状态读取任务快照;姓名、身份证和手机号仍是电子签约的系统必填项。
|
||
const regRules = computed<FormRules>(() => {
|
||
const rules: FormRules = {};
|
||
const changeTriggerFields = new Set(["gender", "birthday", "maritalStatus", "education"]);
|
||
|
||
requiredRegistrationFields.value.forEach((requirement, fieldKey) => {
|
||
if (!requirement.required) {
|
||
return;
|
||
}
|
||
|
||
const useChangeTrigger = changeTriggerFields.has(fieldKey);
|
||
const message = fieldKey === "birthday"
|
||
? "请先输入正确的身份证号码以生成出生日期"
|
||
: `${useChangeTrigger ? "请选择" : "请填写"}${requirement.fieldName || fieldKey}`;
|
||
rules[fieldKey] = [{
|
||
required: true,
|
||
message,
|
||
trigger: useChangeTrigger ? "change" : "blur"
|
||
}];
|
||
});
|
||
|
||
rules.employeeName = [{ required: true, validator: validateEmployeeName, trigger: "blur" }];
|
||
rules.phone = [{ required: true, validator: validatePhone, trigger: "blur" }];
|
||
rules.idCardNumber = [{ required: true, validator: validateIdCardNumber, trigger: "blur" }];
|
||
appendRegistrationRule(rules, "birthday", { validator: validateBirthday, trigger: "change" });
|
||
appendRegistrationRule(rules, "height", { validator: validateOptionalHeight, trigger: "blur" });
|
||
appendRegistrationRule(rules, "weight", { validator: validateOptionalWeight, trigger: "blur" });
|
||
appendRegistrationRule(rules, "email", { validator: validateOptionalEmail, trigger: "blur" });
|
||
appendRegistrationRule(rules, "bankCardNumber", { validator: validateBankCardNumber, trigger: "blur" });
|
||
appendRegistrationRule(rules, "bankName", {
|
||
max: 100,
|
||
message: "开户银行名称不能超过100个字符",
|
||
trigger: "blur"
|
||
});
|
||
appendRegistrationRule(rules, "emergencyPhone", {
|
||
validator: validateOptionalContactPhone,
|
||
trigger: "blur"
|
||
});
|
||
|
||
return rules;
|
||
});
|
||
|
||
/** 根据当前 Token 的实际有效期实时锁定已打开的填写页面。 */
|
||
const isLinkExpired = computed(() => {
|
||
if (!task.value?.linkExpireAt) {
|
||
return false;
|
||
}
|
||
|
||
const expiresAt = Date.parse(task.value.linkExpireAt.replace(" ", "T"));
|
||
return !Number.isNaN(expiresAt) && expiresAt <= currentTimestamp.value;
|
||
});
|
||
|
||
const canEdit = computed(() => {
|
||
const editableStatus = task.value?.taskStatus === "pending_fill"
|
||
|| task.value?.taskStatus === "rejected";
|
||
return editableStatus && !isLinkExpired.value;
|
||
});
|
||
|
||
/** 当前正在校验变量并生成文件的合同。 */
|
||
const preparingContract = computed(() => {
|
||
return contracts.value.find(contract => isContractPreparing(contract.id));
|
||
});
|
||
|
||
/** 合同准备提示,明确告诉员工当前进度及正在处理的合同。 */
|
||
const contractPreparingDescription = computed(() => {
|
||
const contract = preparingContract.value;
|
||
if (!contract) {
|
||
return "";
|
||
}
|
||
|
||
const index = contracts.value.findIndex(item => item.id === contract.id);
|
||
return `正在校验材料信息并生成第 ${index + 1}/${contracts.value.length} 份《${contract.contractName}》,请稍候,不要关闭页面。`;
|
||
});
|
||
|
||
// 计算差集增量变量 (合同需要的 employee. 变量中,登记表没有的字段)
|
||
const registrationStandardFields = [
|
||
"employeename", "name", "username", "gender", "nation", "birthday", "registeredresidence",
|
||
"height", "weight", "healthcondition", "maritalstatus", "education", "major", "graduatedschool",
|
||
"college", "vocationalqualification", "qualificationlevel", "technicalspecialty", "phone",
|
||
"mobile", "email", "idcardnumber", "idcard", "currentaddress", "contactaddress", "landlordcontact",
|
||
"bankcardnumber", "bankcard", "bankname", "bankaccountname", "emergencycontact", "emergencyphone",
|
||
"emergencyaddress", "onboardingdepartment", "onboardingdate", "onboardingposition", "otherremarks"
|
||
];
|
||
|
||
const extraVariables = computed(() => {
|
||
const result: any[] = [];
|
||
const visited = new Set<string>();
|
||
for (const v of contractVariables.value) {
|
||
if (v.variableType === "employee") {
|
||
const fieldCode = (v.variableCode.split(".")[1] || v.variableCode).toLowerCase();
|
||
if (!registrationStandardFields.includes(fieldCode) && !visited.has(v.variableCode)) {
|
||
visited.add(v.variableCode);
|
||
result.push(v);
|
||
}
|
||
}
|
||
}
|
||
return result;
|
||
});
|
||
|
||
type OnboardingStepKey = "registration" | "extra" | "materials" | "confirmation";
|
||
const visibleSteps = computed(() => {
|
||
const steps = [
|
||
{ key: "registration" as OnboardingStepKey, title: "入职登记表", shortTitle: "登记表", description: "入职登记表填写", icon: "User" }
|
||
];
|
||
if (extraVariables.value.length) {
|
||
steps.push({ key: "extra", title: "材料补充项", shortTitle: "补充项", description: "签署材料补充项", icon: "Document" });
|
||
}
|
||
steps.push(
|
||
{ key: "materials", title: "证件材料", shortTitle: "证件材料", description: "证件材料上传", icon: "FolderChecked" },
|
||
{ key: "confirmation", title: "材料确认", shortTitle: "材料确认", description: "签署材料查阅与确认", icon: "Tickets" }
|
||
);
|
||
return steps;
|
||
});
|
||
const currentStepDefinition = computed(() => visibleSteps.value[currentStep.value] || visibleSteps.value[0]);
|
||
const currentStepKey = computed<OnboardingStepKey>(() => currentStepDefinition.value.key);
|
||
|
||
function addEducation() {
|
||
educationHistory.value.push({ timeRange: "", location: "", content: "", result: "" });
|
||
}
|
||
function addWork() {
|
||
workHistory.value.push({ timeRange: "", companyName: "", salary: "", position: "", leaveReason: "" });
|
||
}
|
||
function addFamily() {
|
||
familyMembers.value.push({ name: "", relationship: "", age: "", company: "", phone: "" });
|
||
}
|
||
|
||
const extraVariableErrors = reactive<Record<string, string>>({});
|
||
|
||
function getVariableDataType(variable: any) {
|
||
return String(variable?.dataType || "string").trim().toLowerCase();
|
||
}
|
||
|
||
function getVariableOptions(variable: any) {
|
||
if (!variable?.optionsJson) {
|
||
return [];
|
||
}
|
||
|
||
try {
|
||
const options = JSON.parse(variable.optionsJson);
|
||
if (!Array.isArray(options)) {
|
||
return [];
|
||
}
|
||
|
||
return options
|
||
.map((option: any) => {
|
||
if (option && typeof option === "object") {
|
||
const value = option.value ?? option.label;
|
||
const label = option.label ?? option.value;
|
||
return value == null
|
||
? null
|
||
: { value: String(value), label: String(label ?? value) };
|
||
}
|
||
|
||
return option == null
|
||
? null
|
||
: { value: String(option), label: String(option) };
|
||
})
|
||
.filter(Boolean) as { value: string; label: string }[];
|
||
} catch {
|
||
return [];
|
||
}
|
||
}
|
||
|
||
function clearExtraVariableError(variableCode: string) {
|
||
extraVariableErrors[variableCode] = "";
|
||
}
|
||
|
||
function isIsoDate(value: string) {
|
||
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
|
||
if (!match) {
|
||
return false;
|
||
}
|
||
|
||
const date = new Date(
|
||
Number(match[1]),
|
||
Number(match[2]) - 1,
|
||
Number(match[3])
|
||
);
|
||
return date.getFullYear() === Number(match[1])
|
||
&& date.getMonth() === Number(match[2]) - 1
|
||
&& date.getDate() === Number(match[3]);
|
||
}
|
||
|
||
/**
|
||
* 将长表单平滑定位到首个错误区域,并尽量聚焦其中可编辑的控件。
|
||
*/
|
||
function locateValidationTarget(target: string | HTMLElement | null | undefined) {
|
||
nextTick(() => {
|
||
const element = typeof target === "string"
|
||
? document.getElementById(target)
|
||
: target;
|
||
if (!element) {
|
||
return;
|
||
}
|
||
|
||
element.scrollIntoView({ behavior: "smooth", block: "center" });
|
||
|
||
window.setTimeout(() => {
|
||
const focusable = element.matches("input:not([disabled]), textarea:not([disabled]), button:not([disabled]), [tabindex]:not([tabindex='-1'])")
|
||
? element as HTMLElement
|
||
: element.querySelector<HTMLElement>(
|
||
"input:not([disabled]), textarea:not([disabled]), button:not([disabled]), [tabindex]:not([tabindex='-1'])"
|
||
);
|
||
focusable?.focus({ preventScroll: true });
|
||
}, 350);
|
||
});
|
||
}
|
||
|
||
function validateExtraVariables() {
|
||
let firstMessage = "";
|
||
let firstTargetId = "";
|
||
|
||
for (const [index, variable] of extraVariables.value.entries()) {
|
||
const variableCode = variable.variableCode;
|
||
const variableName = variable.variableName || variableCode;
|
||
const value = String(customVariables[variableCode] ?? "").trim();
|
||
const options = getVariableOptions(variable);
|
||
const dataType = getVariableDataType(variable);
|
||
customVariables[variableCode] = value;
|
||
extraVariableErrors[variableCode] = "";
|
||
|
||
if (!value && variable.required) {
|
||
extraVariableErrors[variableCode] = `请填写${variableName}`;
|
||
} else if (value && options.length && !options.some(option => option.value === value)) {
|
||
extraVariableErrors[variableCode] = `${variableName}必须从配置选项中选择`;
|
||
} else if (value && dataType === "number" && !/^-?\d+(\.\d+)?$/.test(value)) {
|
||
extraVariableErrors[variableCode] = `${variableName}必须填写数字`;
|
||
} else if (value && dataType === "date" && !isIsoDate(value)) {
|
||
extraVariableErrors[variableCode] = `${variableName}必须填写正确日期`;
|
||
}
|
||
|
||
if (!firstMessage && extraVariableErrors[variableCode]) {
|
||
firstMessage = extraVariableErrors[variableCode];
|
||
firstTargetId = `extra-variable-${index}`;
|
||
}
|
||
}
|
||
|
||
if (!firstMessage) {
|
||
return true;
|
||
}
|
||
|
||
ElMessage.warning(firstMessage);
|
||
locateValidationTarget(firstTargetId);
|
||
return false;
|
||
}
|
||
|
||
function validateRegistrationLists() {
|
||
if (!educationHistory.value.length) {
|
||
ElMessage.warning("请至少添加一条教育学习经历(最高学历)");
|
||
locateValidationTarget("education-history-section");
|
||
return false;
|
||
}
|
||
|
||
for (let index = 0; index < educationHistory.value.length; index++) {
|
||
const row = educationHistory.value[index];
|
||
const requiredFields = [
|
||
["timeRange", "时间范围"],
|
||
["location", "学校或机构"],
|
||
["content", "专业或学习内容"],
|
||
["result", "结果或学位"]
|
||
] as const;
|
||
const missing = requiredFields.find(([field]) => !String(row[field] || "").trim());
|
||
if (missing) {
|
||
ElMessage.warning(`第 ${index + 1} 条教育经历请填写${missing[1]},或删除整条经历`);
|
||
locateValidationTarget(`education-history-${index}`);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
if (!workHistory.value.length) {
|
||
ElMessage.warning("请至少添加一条工作经历(应届生可填写兼职、在校或实习经历)");
|
||
locateValidationTarget("work-history-section");
|
||
return false;
|
||
}
|
||
|
||
for (let index = 0; index < workHistory.value.length; index++) {
|
||
const row = workHistory.value[index];
|
||
const requiredFields = [
|
||
["timeRange", "起止时间"],
|
||
["companyName", "工作单位"],
|
||
["position", "担任职务"],
|
||
["leaveReason", "离职原因"]
|
||
] as const;
|
||
const missing = requiredFields.find(([field]) => !String(row[field] || "").trim());
|
||
if (missing) {
|
||
ElMessage.warning(`第 ${index + 1} 条工作经历请填写${missing[1]},或删除整条经历`);
|
||
locateValidationTarget(`work-history-${index}`);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
if (!familyMembers.value.length) {
|
||
ElMessage.warning("请至少添加一位家庭成员信息(如父母或配偶)");
|
||
locateValidationTarget("family-members-section");
|
||
return false;
|
||
}
|
||
|
||
for (let index = 0; index < familyMembers.value.length; index++) {
|
||
const row = familyMembers.value[index];
|
||
const requiredFields = [
|
||
["name", "姓名"],
|
||
["relationship", "关系"],
|
||
["age", "年龄"],
|
||
["phone", "联系电话"]
|
||
] as const;
|
||
const missing = requiredFields.find(([field]) => !String(row[field] || "").trim());
|
||
if (missing) {
|
||
ElMessage.warning(`第 ${index + 1} 条家庭成员请填写${missing[1]},或删除整条记录`);
|
||
locateValidationTarget(`family-member-${index}`);
|
||
return false;
|
||
}
|
||
|
||
const age = Number(row.age);
|
||
if (!Number.isInteger(age) || age < 0 || age > 120) {
|
||
ElMessage.warning(`第 ${index + 1} 条家庭成员的年龄应为0至120之间的整数`);
|
||
locateValidationTarget(`family-member-${index}`);
|
||
return false;
|
||
}
|
||
if (!isContactPhone(row.phone)) {
|
||
ElMessage.warning(`第 ${index + 1} 条家庭成员的联系电话格式不正确`);
|
||
locateValidationTarget(`family-member-${index}`);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
function getUploadedMaterials(matId: number) {
|
||
return uploadedMaterials.value.filter(item => item.materialTypeId === matId);
|
||
}
|
||
|
||
/** 防止旧任务中的异常数量导致上传入口被错误隐藏。 */
|
||
function getMaxFileCount(mat: any) {
|
||
const maxFileCount = Number(mat.maxFileCount);
|
||
return Number.isFinite(maxFileCount) && maxFileCount > 0
|
||
? Math.floor(maxFileCount)
|
||
: 1;
|
||
}
|
||
|
||
function getRemainingFileCount(mat: any) {
|
||
return Math.max(
|
||
getMaxFileCount(mat) - getUploadedMaterials(mat.materialTypeId).length,
|
||
0
|
||
);
|
||
}
|
||
|
||
const PUBLIC_MATERIAL_MAX_FILE_SIZE_MB = 20;
|
||
const PUBLIC_MATERIAL_DEFAULT_EXTENSIONS = ["png", "jpg", "jpeg", "pdf"];
|
||
|
||
function getAllowedExtensions(mat: any) {
|
||
const configured = String(mat.allowedExtensions || "")
|
||
.toLowerCase()
|
||
.split(",")
|
||
.map((item: string) => item.trim().replace(/^\./, ""))
|
||
.filter(Boolean);
|
||
return configured.length ? configured : [...PUBLIC_MATERIAL_DEFAULT_EXTENSIONS];
|
||
}
|
||
|
||
function getUploadAccept(mat: any) {
|
||
return getAllowedExtensions(mat)
|
||
.map((item: string) => `.${item}`)
|
||
.join(",");
|
||
}
|
||
|
||
function getMaterialLimitText(mat: any) {
|
||
const allowedExtensions = getAllowedExtensions(mat);
|
||
const formatText = allowedExtensions.length
|
||
? allowedExtensions.join("、").toUpperCase()
|
||
: "不限";
|
||
const configuredSize = Number(mat.maxFileSizeMb);
|
||
const maxFileSizeMb = configuredSize > 0
|
||
? Math.min(configuredSize, PUBLIC_MATERIAL_MAX_FILE_SIZE_MB)
|
||
: PUBLIC_MATERIAL_MAX_FILE_SIZE_MB;
|
||
const sizeText = ` · 单个不超过 ${maxFileSizeMb} MB`;
|
||
return `最多 ${getMaxFileCount(mat)} 份 · 格式:${formatText}${sizeText}`;
|
||
}
|
||
|
||
function isImageFile(file: any) {
|
||
const suffix = String(
|
||
file.fileSuffix || file.fileName?.split(".").pop() || ""
|
||
).toLowerCase().replace(/^\./, "");
|
||
return ["png", "jpg", "jpeg", "gif", "bmp", "webp"].includes(suffix);
|
||
}
|
||
|
||
function statusTagType(status?: string) {
|
||
switch (status) {
|
||
case "pending_fill": return "warning";
|
||
case "reviewing": return "primary";
|
||
case "approved":
|
||
case "archived": return "success";
|
||
case "rejected":
|
||
case "revoked": return "danger";
|
||
default: return "info";
|
||
}
|
||
}
|
||
|
||
function statusText(status?: string) {
|
||
switch (status) {
|
||
case "pending_fill": return "待填写";
|
||
case "reviewing": return "审核中";
|
||
case "approved": return "已通过";
|
||
case "archived": return "已归档";
|
||
case "rejected": return "已退回";
|
||
case "revoked": return "已撤销";
|
||
default: return status || "未知";
|
||
}
|
||
}
|
||
|
||
function formatDateTime(str?: string) {
|
||
return str ? str.replace("T", " ").substring(0, 16) : "";
|
||
}
|
||
|
||
async function load(silent = false) {
|
||
if (!token.value) {
|
||
error.value = "缺少入职访问 Token";
|
||
return;
|
||
}
|
||
if (!silent) {
|
||
loading.value = true;
|
||
}
|
||
error.value = "";
|
||
try {
|
||
const res = await onboardingPublicApi.detail({ token: token.value });
|
||
const data = res.data;
|
||
task.value = data.task;
|
||
contracts.value = (data.contracts || []).map((c: any) => ({
|
||
...c,
|
||
hasViewed: Boolean(c.readAt),
|
||
isAgreed: c.contractStatus === "confirmed"
|
||
}));
|
||
contracts.value.forEach((contract: any) => {
|
||
const key = String(contract.id);
|
||
contractPrepareStatus[key] = contract.generatedDocxFileId ? "ready" : "idle";
|
||
contractPrepareErrors[key] = "";
|
||
});
|
||
materialRequirements.value = data.materialRequirements || [];
|
||
uploadedMaterials.value = data.materials || [];
|
||
contractVariables.value = data.contractVariables || [];
|
||
formFieldRequirements.value = data.formFields || [];
|
||
Object.keys(customVariables).forEach(key => delete customVariables[key]);
|
||
extraVariables.value.forEach((variable: any) => {
|
||
if (!(variable.variableCode in customVariables) && variable.defaultValue != null) {
|
||
customVariables[variable.variableCode] = String(variable.defaultValue);
|
||
}
|
||
});
|
||
|
||
// 回填入职登记表
|
||
if (data.employeeInfo) {
|
||
Object.assign(form, data.employeeInfo);
|
||
if (data.employeeInfo.educationHistoryJson) {
|
||
try { educationHistory.value = JSON.parse(data.employeeInfo.educationHistoryJson); } catch { }
|
||
}
|
||
if (data.employeeInfo.workHistoryJson) {
|
||
try { workHistory.value = JSON.parse(data.employeeInfo.workHistoryJson); } catch { }
|
||
}
|
||
if (data.employeeInfo.familyMembersJson) {
|
||
try { familyMembers.value = JSON.parse(data.employeeInfo.familyMembersJson); } catch { }
|
||
}
|
||
if (data.employeeInfo.customVariablesJson) {
|
||
try {
|
||
const savedVariables = JSON.parse(data.employeeInfo.customVariablesJson);
|
||
extraVariables.value.forEach((variable: any) => {
|
||
if (Object.prototype.hasOwnProperty.call(savedVariables, variable.variableCode)) {
|
||
customVariables[variable.variableCode] = savedVariables[variable.variableCode];
|
||
}
|
||
});
|
||
} catch { }
|
||
}
|
||
}
|
||
await refreshMaterialPreviews();
|
||
} catch (err: any) {
|
||
error.value = err.message || "无法加载入职任务";
|
||
} finally {
|
||
if (!silent) {
|
||
loading.value = false;
|
||
}
|
||
}
|
||
}
|
||
|
||
async function saveCurrentEmployeeInfo() {
|
||
form.employeeName = String(form.employeeName || "").trim();
|
||
form.phone = String(form.phone || "").trim();
|
||
form.email = String(form.email || "").trim();
|
||
form.idCardNumber = normalizeIdCardNumber(form.idCardNumber);
|
||
form.bankCardNumber = normalizeBankCardNumber(form.bankCardNumber);
|
||
form.bankName = String(form.bankName || "").trim();
|
||
form.bankAccountName = form.employeeName; // 开户名始终与员工姓名保持一致
|
||
const payload = {
|
||
...form,
|
||
token: token.value,
|
||
educationHistoryJson: JSON.stringify(educationHistory.value),
|
||
workHistoryJson: JSON.stringify(workHistory.value),
|
||
familyMembersJson: JSON.stringify(familyMembers.value),
|
||
customVariablesJson: JSON.stringify(Object.fromEntries(
|
||
extraVariables.value.map((variable: any) => [variable.variableCode, customVariables[variable.variableCode] ?? ""])
|
||
))
|
||
};
|
||
await onboardingPublicApi.saveEmployeeInfo(payload);
|
||
}
|
||
|
||
/**
|
||
* Element Plus 校验失败时会拒绝 Promise 并返回字段错误对象。
|
||
* 这是正常的表单提示,不应继续交给全局异常处理显示“未知错误”。
|
||
*/
|
||
async function validateRegistrationForm() {
|
||
if (!regFormRef.value) {
|
||
return false;
|
||
}
|
||
|
||
try {
|
||
await regFormRef.value.validate();
|
||
return true;
|
||
} catch (fields: any) {
|
||
const firstField = Object.keys(fields || {})[0];
|
||
const firstMessage = firstField ? fields[firstField]?.[0]?.message : "";
|
||
if (firstField) {
|
||
regFormRef.value.scrollToField(firstField);
|
||
}
|
||
ElMessage.warning(firstMessage || "请检查并完善标红的登记信息");
|
||
await nextTick();
|
||
locateValidationTarget(document.querySelector<HTMLElement>(".reg-form .el-form-item.is-error"));
|
||
return false;
|
||
}
|
||
}
|
||
|
||
async function nextStep() {
|
||
if (savingStep.value) {
|
||
return;
|
||
}
|
||
if (isAnyMaterialUploading.value) {
|
||
ElMessage.closeAll();
|
||
ElMessage.warning("材料正在上传中,请稍候再点击下一步");
|
||
return;
|
||
}
|
||
savingStep.value = true;
|
||
try {
|
||
if (currentStepKey.value === "registration") {
|
||
if (!await validateRegistrationForm()) {
|
||
return;
|
||
}
|
||
if (!validateRegistrationLists()) {
|
||
return;
|
||
}
|
||
if (canEdit.value) {
|
||
await saveCurrentEmployeeInfo();
|
||
ElMessage.closeAll();
|
||
ElMessage.success("入职登记表已临时保存");
|
||
}
|
||
} else if (currentStepKey.value === "extra") {
|
||
if (!validateExtraVariables()) {
|
||
return;
|
||
}
|
||
if (canEdit.value) {
|
||
await saveCurrentEmployeeInfo();
|
||
}
|
||
} else if (currentStepKey.value === "materials") {
|
||
if (isAnyMaterialUploading.value) {
|
||
ElMessage.closeAll();
|
||
ElMessage.warning("材料正在上传中,请稍候再点击下一步");
|
||
return;
|
||
}
|
||
const missing = materialRequirements.value.filter(m => m.required && !getUploadedMaterials(m.materialTypeId).length);
|
||
if (missing.length) {
|
||
ElMessage.closeAll();
|
||
ElMessage.warning(`请先上传必传材料:${missing.map((m: any) => m.materialName).join("、")}`);
|
||
locateValidationTarget(`material-requirement-${missing[0].materialTypeId}`);
|
||
return;
|
||
}
|
||
}
|
||
currentStep.value++;
|
||
window.scrollTo({ top: 0, behavior: "smooth" });
|
||
if (currentStepKey.value === "confirmation") {
|
||
await prepareContracts();
|
||
}
|
||
} catch (error: any) {
|
||
const message = error?.msg || error?.message || "临时保存失败,请稍后重试";
|
||
ElMessage.closeAll();
|
||
await ElMessageBox.alert(message, "无法进入下一步", {
|
||
type: "warning",
|
||
confirmButtonText: "我知道了",
|
||
closeOnClickModal: false
|
||
}).catch(() => undefined);
|
||
} finally {
|
||
savingStep.value = false;
|
||
}
|
||
}
|
||
|
||
function materialPreviewKey(file: any) {
|
||
return `${file.id}-${file.fileId || 0}`;
|
||
}
|
||
|
||
function getMaterialSourceUrl(file: any) {
|
||
return materialPreviewUrls[materialPreviewKey(file)] || "";
|
||
}
|
||
|
||
function revokeMaterialPreviewUrls() {
|
||
Object.values(materialPreviewUrls).forEach(url => URL.revokeObjectURL(url));
|
||
Object.keys(materialPreviewUrls).forEach(key => delete materialPreviewUrls[key]);
|
||
}
|
||
|
||
async function refreshMaterialPreviews() {
|
||
const generation = ++materialPreviewGeneration;
|
||
revokeMaterialPreviewUrls();
|
||
|
||
await Promise.all(uploadedMaterials.value
|
||
.filter(file => isImageFile(file))
|
||
.map(async file => {
|
||
try {
|
||
const result = await onboardingPublicApi.downloadMaterial({
|
||
token: token.value,
|
||
taskMaterialId: file.id,
|
||
fileId: file.fileId
|
||
}, false);
|
||
const objectUrl = URL.createObjectURL(result.blob);
|
||
if (generation !== materialPreviewGeneration) {
|
||
URL.revokeObjectURL(objectUrl);
|
||
return;
|
||
}
|
||
materialPreviewUrls[materialPreviewKey(file)] = objectUrl;
|
||
} catch {
|
||
// 单张历史图片损坏时保留文件信息,不阻断整个入职任务。
|
||
}
|
||
}));
|
||
}
|
||
|
||
async function uploadMaterial(
|
||
options: any,
|
||
materialTypeId: number,
|
||
taskMaterialId?: number
|
||
) {
|
||
try {
|
||
const response = await onboardingPublicApi.uploadMaterial({
|
||
token: token.value,
|
||
materialTypeId,
|
||
taskMaterialId,
|
||
file: options.file
|
||
});
|
||
options.onSuccess(response);
|
||
} catch (error: any) {
|
||
options.onError(error);
|
||
}
|
||
}
|
||
|
||
function isMaterialUploading(materialTypeId: number) {
|
||
return Boolean(uploadingMaterials[String(materialTypeId)]);
|
||
}
|
||
|
||
function setMaterialUploading(materialTypeId: number, value: boolean) {
|
||
uploadingMaterials[String(materialTypeId)] = value;
|
||
}
|
||
|
||
function beforeUpload(file: File, mat: any) {
|
||
const ext = file.name.split(".").pop()?.toLowerCase() || "";
|
||
const allowed = getAllowedExtensions(mat);
|
||
if (allowed.length && !allowed.includes(ext)) {
|
||
ElMessage.error(`仅支持格式:${mat.allowedExtensions}`);
|
||
return false;
|
||
}
|
||
const configuredSize = Number(mat.maxFileSizeMb);
|
||
const maxFileSizeMb = configuredSize > 0
|
||
? Math.min(configuredSize, PUBLIC_MATERIAL_MAX_FILE_SIZE_MB)
|
||
: PUBLIC_MATERIAL_MAX_FILE_SIZE_MB;
|
||
if (file.size > maxFileSizeMb * 1024 * 1024) {
|
||
ElMessage.error(`单个文件不能超过 ${maxFileSizeMb} MB,请压缩后重新上传`);
|
||
return false;
|
||
}
|
||
setMaterialUploading(mat.materialTypeId, true);
|
||
return true;
|
||
}
|
||
|
||
async function onUploadSuccess(res: any, materialTypeId: number) {
|
||
if (res.code !== 200 && res.code !== 0) {
|
||
setMaterialUploading(materialTypeId, false);
|
||
return ElMessage.error(res.msg || "上传失败");
|
||
}
|
||
try {
|
||
await load(true);
|
||
ElMessage.success("材料上传成功");
|
||
} finally {
|
||
setMaterialUploading(materialTypeId, false);
|
||
}
|
||
}
|
||
|
||
function onUploadError(error: any, materialTypeId: number) {
|
||
setMaterialUploading(materialTypeId, false);
|
||
if (!error?.reported) {
|
||
ElMessage.error(error?.message || "材料上传失败,请稍后重试");
|
||
}
|
||
}
|
||
|
||
async function deleteFile(id: number) {
|
||
const confirmed = await isMessageBoxConfirmed(
|
||
ElMessageBox.confirm("确认删除已上传材料?", "删除确认", { type: "warning" })
|
||
);
|
||
if (!confirmed) {
|
||
return;
|
||
}
|
||
|
||
await onboardingPublicApi.deleteMaterial({ token: token.value, taskMaterialId: id });
|
||
ElMessage.success("材料已删除");
|
||
await load();
|
||
}
|
||
|
||
/**
|
||
* Element Plus 在取消或关闭确认框时会拒绝 Promise。
|
||
* 取消属于正常操作,不应继续交给全局异常提示处理。
|
||
*/
|
||
async function isMessageBoxConfirmed(confirmPromise: Promise<unknown>) {
|
||
try {
|
||
await confirmPromise;
|
||
return true;
|
||
} catch (action) {
|
||
if (action === "cancel" || action === "close") {
|
||
return false;
|
||
}
|
||
|
||
throw action;
|
||
}
|
||
}
|
||
|
||
function getContractPrepareStatus(taskContractId: number) {
|
||
return contractPrepareStatus[String(taskContractId)] || "idle";
|
||
}
|
||
|
||
function getContractPrepareError(taskContractId: number) {
|
||
return contractPrepareErrors[String(taskContractId)] || "";
|
||
}
|
||
|
||
function isContractPreparing(taskContractId: number) {
|
||
return getContractPrepareStatus(taskContractId) === "preparing";
|
||
}
|
||
|
||
function isContractDownloading(taskContractId: number) {
|
||
return Boolean(contractDownloading[String(taskContractId)]);
|
||
}
|
||
|
||
function isContractBusy(taskContractId: number) {
|
||
return isContractPreparing(taskContractId) || isContractDownloading(taskContractId);
|
||
}
|
||
|
||
function isContractPrepared(taskContractId: number) {
|
||
return getContractPrepareStatus(taskContractId) === "ready";
|
||
}
|
||
|
||
function isContractPrepareFailed(taskContractId: number) {
|
||
return getContractPrepareStatus(taskContractId) === "error";
|
||
}
|
||
|
||
function getContractActionText(taskContractId: number) {
|
||
if (isContractDownloading(taskContractId)) {
|
||
return "正在下载签署材料";
|
||
}
|
||
const status = getContractPrepareStatus(taskContractId);
|
||
if (status === "preparing") {
|
||
return "正在准备签署材料";
|
||
}
|
||
if (status === "error") {
|
||
return "重新准备签署材料";
|
||
}
|
||
if (status === "ready") {
|
||
return "下载并查看签署材料";
|
||
}
|
||
return "等待准备签署材料";
|
||
}
|
||
|
||
/**
|
||
* 提前校验变量并生成合同,让业务异常能够在当前页面直接展示。
|
||
*/
|
||
async function prepareContract(contract: any) {
|
||
const key = String(contract.id);
|
||
contractPrepareStatus[key] = "preparing";
|
||
contractPrepareErrors[key] = "";
|
||
|
||
try {
|
||
const previousFileId = contract.generatedDocxFileId;
|
||
const response = await onboardingPublicApi.prepareContract({
|
||
token: token.value,
|
||
taskContractId: contract.id
|
||
});
|
||
// 使用后端返回的新文件 Id 作为下载版本,避免手机浏览器复用旧合同缓存。
|
||
contract.generatedDocxFileId = response.data.fileId;
|
||
contract.generatedAt = response.data.generatedAt;
|
||
if (previousFileId && previousFileId !== response.data.fileId) {
|
||
contract.hasViewed = false;
|
||
contract.isAgreed = false;
|
||
}
|
||
contractPrepareStatus[key] = "ready";
|
||
return true;
|
||
} catch (err: any) {
|
||
const message = err?.msg || err?.message || "签署材料准备失败,请稍后重试";
|
||
contractPrepareStatus[key] = "error";
|
||
contractPrepareErrors[key] = message;
|
||
// 业务异常已由统一请求拦截器提示;仅为网络等非标准异常补充提示,避免重复弹窗。
|
||
if (!err?.msg) {
|
||
ElMessage.error(`${contract.contractName}:${message}`);
|
||
}
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 进入合同确认步骤时逐份准备,避免同时生成多个大文件造成服务端瞬时压力。
|
||
*/
|
||
async function prepareContracts() {
|
||
for (const contract of contracts.value) {
|
||
await prepareContract(contract);
|
||
}
|
||
}
|
||
|
||
async function handleContractAction(contract: any) {
|
||
if (isContractPrepareFailed(contract.id)) {
|
||
const prepared = await prepareContract(contract);
|
||
if (prepared) {
|
||
ElMessage.success("签署材料已重新准备完成,请再次点击下载查看");
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (isContractPrepared(contract.id)) {
|
||
await previewAndDownloadContract(contract);
|
||
}
|
||
}
|
||
|
||
function normalizeDownloadFileName(fileName: string | undefined, fallback: string) {
|
||
const normalized = String(fileName || fallback)
|
||
.replace(/[\\/:*?"<>|]/g, "_")
|
||
.trim();
|
||
return normalized || fallback;
|
||
}
|
||
|
||
function triggerContractDownload(blob: Blob, fileName: string) {
|
||
if (!blob.size) {
|
||
throw new Error("服务器返回的合同文件为空");
|
||
}
|
||
|
||
const objectUrl = URL.createObjectURL(blob);
|
||
const link = document.createElement("a");
|
||
link.href = objectUrl;
|
||
link.download = fileName;
|
||
link.rel = "noopener noreferrer";
|
||
link.style.display = "none";
|
||
document.body.appendChild(link);
|
||
link.click();
|
||
link.remove();
|
||
|
||
// 安卓 WebView 可能延迟接管 Blob 下载,不能过早释放。
|
||
window.setTimeout(() => URL.revokeObjectURL(objectUrl), 60_000);
|
||
}
|
||
|
||
function buildDirectContractDownloadUrl(c: any) {
|
||
const url = new URL("/api/open/onboarding/contract-source", window.location.origin);
|
||
url.searchParams.set("token", token.value);
|
||
url.searchParams.set("taskContractId", String(c.id));
|
||
if (c.generatedDocxFileId) {
|
||
url.searchParams.set("version", String(c.generatedDocxFileId));
|
||
}
|
||
url.searchParams.set("_t", String(Date.now()));
|
||
return `${url.pathname}${url.search}`;
|
||
}
|
||
|
||
function triggerDirectContractDownload(url: string) {
|
||
const link = document.createElement("a");
|
||
link.href = url;
|
||
link.target = "_blank";
|
||
link.rel = "noopener noreferrer";
|
||
link.style.display = "none";
|
||
document.body.appendChild(link);
|
||
link.click();
|
||
link.remove();
|
||
}
|
||
|
||
async function previewAndDownloadContract(c: any) {
|
||
const key = String(c.id);
|
||
if (contractDownloading[key]) {
|
||
return;
|
||
}
|
||
|
||
contractDownloading[key] = true;
|
||
try {
|
||
const userAgent = window.navigator.userAgent;
|
||
if (/Android/i.test(userAgent)) {
|
||
// 安卓 WebView 通常不支持 blob: 下载,直接交给系统的 HTTP 下载能力处理。
|
||
// 此处在用户点击事件后的第一个同步分支触发,避免被 WebView 当成弹窗拦截。
|
||
triggerDirectContractDownload(buildDirectContractDownloadUrl(c));
|
||
c.hasViewed = true;
|
||
c.isAgreed = false;
|
||
ElMessage.success("签署材料下载已发起,请使用 Word/WPS 打开核对");
|
||
if (/MicroMessenger/i.test(userAgent)) {
|
||
void ElMessageBox.alert(
|
||
"如果微信中没有出现文件,请点击右上角“…”选择“在浏览器打开”,然后再次点击下载。",
|
||
"安卓微信下载提示",
|
||
{ confirmButtonText: "我知道了" }
|
||
).catch(() => undefined);
|
||
}
|
||
return;
|
||
}
|
||
|
||
const result = await onboardingPublicApi.downloadContract({
|
||
token: token.value,
|
||
taskContractId: c.id,
|
||
fileVersion: c.generatedDocxFileId
|
||
});
|
||
const fallbackName = `${c.contractName || "入职签署材料"}.docx`;
|
||
triggerContractDownload(
|
||
result.blob,
|
||
normalizeDownloadFileName(result.fileName, fallbackName)
|
||
);
|
||
|
||
c.hasViewed = true;
|
||
c.isAgreed = false;
|
||
ElMessage.success("签署材料已开始下载,请使用 Word/WPS 打开核对");
|
||
|
||
} catch (err: any) {
|
||
if (!err?.reported) {
|
||
ElMessage.error(err?.message || "签署材料下载失败,请稍后重试");
|
||
}
|
||
} finally {
|
||
contractDownloading[key] = false;
|
||
}
|
||
}
|
||
|
||
async function submitAll() {
|
||
if (!isIdentityConfirmationComplete.value) {
|
||
const missingLabels = identityFields
|
||
.filter(({ key }) => !isIdentityFieldConfirmed(key))
|
||
.map(item => item.label);
|
||
ElMessage.warning(`请逐项核对并确认实名认证信息:${missingLabels.join("、")}`);
|
||
locateValidationTarget("identity-confirmation-card");
|
||
return;
|
||
}
|
||
|
||
const unprepared = contracts.value.filter(c => !isContractPrepared(c.id));
|
||
if (unprepared.length) {
|
||
ElMessage.warning(`请先等待签署材料准备完成:${unprepared.map(c => c.contractName).join("、")}`);
|
||
locateValidationTarget(`contract-confirm-${unprepared[0].id}`);
|
||
return;
|
||
}
|
||
|
||
const unviewed = contracts.value.filter(c => !c.hasViewed);
|
||
if (unviewed.length) {
|
||
ElMessage.warning(`请先下载并查看签署材料:${unviewed.map(c => c.contractName).join("、")}`);
|
||
locateValidationTarget(`contract-confirm-${unviewed[0].id}`);
|
||
return;
|
||
}
|
||
|
||
const unconfirmed = contracts.value.filter(c => !c.isAgreed);
|
||
if (unconfirmed.length) {
|
||
ElMessage.warning(`请先勾选同意全部签署材料(未勾选:${unconfirmed.map(c => c.contractName).join("、")})`);
|
||
locateValidationTarget(`contract-confirm-${unconfirmed[0].id}`);
|
||
return;
|
||
}
|
||
|
||
const confirmed = await isMessageBoxConfirmed(
|
||
ElMessageBox.confirm("请再次确认姓名、身份证号码和手机号码均准确无误。提交后将进入人事复核建档流程,您不能再自行修改。", "最终提交确认", {
|
||
type: "success",
|
||
confirmButtonText: "确认提交",
|
||
cancelButtonText: "再检查一下"
|
||
})
|
||
);
|
||
if (!confirmed) {
|
||
return;
|
||
}
|
||
|
||
submitting.value = true;
|
||
try {
|
||
// 员工资料在进入材料步骤前已经保存;此处重复保存会使刚生成的合同失效。
|
||
for (const c of contracts.value) {
|
||
await onboardingPublicApi.confirmContract({ token: token.value, taskContractId: c.id });
|
||
}
|
||
await onboardingPublicApi.submit({
|
||
token: token.value,
|
||
identityInformationConfirmed: true,
|
||
confirmedEmployeeName: identityConfirmedValues.employeeName,
|
||
confirmedIdCardNumber: identityConfirmedValues.idCardNumber,
|
||
confirmedPhone: identityConfirmedValues.phone
|
||
});
|
||
ElMessage.success("入职资料已成功提交!请等待人事审核。");
|
||
await load();
|
||
} finally {
|
||
submitting.value = false;
|
||
}
|
||
}
|
||
|
||
/** 入职填写页使用固定移动端比例,离开页面后恢复系统原有 viewport 配置。 */
|
||
onMounted(() => {
|
||
if (viewportMeta) {
|
||
viewportMeta.content = [
|
||
"width=device-width",
|
||
"initial-scale=1.0",
|
||
"minimum-scale=1.0",
|
||
"maximum-scale=1.0",
|
||
"user-scalable=no",
|
||
"viewport-fit=cover"
|
||
].join(", ");
|
||
}
|
||
currentTimestamp.value = Date.now();
|
||
linkExpiryTimer = window.setInterval(() => {
|
||
currentTimestamp.value = Date.now();
|
||
}, 1000);
|
||
|
||
load();
|
||
});
|
||
|
||
onBeforeUnmount(() => {
|
||
materialPreviewGeneration++;
|
||
revokeMaterialPreviewUrls();
|
||
if (linkExpiryTimer !== undefined) {
|
||
window.clearInterval(linkExpiryTimer);
|
||
}
|
||
if (viewportMeta && originalViewportContent) {
|
||
viewportMeta.content = originalViewportContent;
|
||
}
|
||
});
|
||
</script>
|
||
|
||
<style scoped>
|
||
/* ============================================================
|
||
Solid Fresh Light Green (纯色清爽淡绿,无渐变) 视觉系统
|
||
============================================================ */
|
||
|
||
.fill-page {
|
||
min-height: 100vh;
|
||
background: #f8faf9;
|
||
padding-bottom: 60px;
|
||
}
|
||
|
||
/* 异常状态居中卡片样式(对齐 public-status 风格) */
|
||
.fill-page.is-error-state {
|
||
display: flex;
|
||
min-height: 100vh;
|
||
padding: 24px;
|
||
align-items: center;
|
||
justify-content: center;
|
||
box-sizing: border-box;
|
||
background: #f0fdf7;
|
||
}
|
||
|
||
.public-error-container {
|
||
display: flex;
|
||
width: 100%;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.error-status-card {
|
||
width: min(100%, 420px);
|
||
padding: 42px 28px;
|
||
text-align: center;
|
||
background: #ffffff;
|
||
border: 1px solid #d1fae5;
|
||
border-radius: 16px;
|
||
box-shadow: 0 12px 36px rgb(5 150 105 / 10%);
|
||
}
|
||
|
||
.error-status-mark {
|
||
display: flex;
|
||
width: 56px;
|
||
height: 56px;
|
||
margin: 0 auto 20px;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: #ffffff;
|
||
font-size: 30px;
|
||
font-weight: 700;
|
||
background: #059669;
|
||
border-radius: 50%;
|
||
}
|
||
|
||
.error-status-title {
|
||
margin: 0;
|
||
color: #064e3b;
|
||
font-size: 24px;
|
||
font-weight: 700;
|
||
line-height: 1.3;
|
||
}
|
||
|
||
.error-status-desc {
|
||
margin: 14px 0 0;
|
||
color: #64748b;
|
||
font-size: 15px;
|
||
line-height: 1.8;
|
||
}
|
||
|
||
.error-status-actions {
|
||
margin-top: 24px;
|
||
}
|
||
|
||
.error-retry-btn {
|
||
height: 40px;
|
||
padding: 0 26px;
|
||
font-size: 15px;
|
||
font-weight: 500;
|
||
border-radius: 8px;
|
||
background-color: #059669 !important;
|
||
border-color: #059669 !important;
|
||
color: #ffffff !important;
|
||
box-shadow: 0 4px 12px rgba(5, 150, 105, 0.2);
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.error-retry-btn:hover {
|
||
background-color: #047857 !important;
|
||
border-color: #047857 !important;
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 6px 16px rgba(5, 150, 105, 0.3);
|
||
}
|
||
|
||
/* 顶部大背景 (Hero Header Background) - 纯色柔和淡绿,去渐变 */
|
||
.brand-header {
|
||
background: #ecfdf5;
|
||
color: #111827;
|
||
padding: 46px 20px 54px;
|
||
text-align: center;
|
||
position: relative;
|
||
border-bottom: 1px solid #d1fae5;
|
||
}
|
||
|
||
/* 顶部平台标识 (Header Tag) */
|
||
.brand-badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
background: #ffffff;
|
||
border: 1px solid #a7f3d0;
|
||
padding: 5px 14px;
|
||
border-radius: 20px;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: #065f46;
|
||
letter-spacing: 0.5px;
|
||
margin-bottom: 12px;
|
||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
|
||
}
|
||
|
||
.brand-badge .el-icon {
|
||
color: #059669;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.brand-title {
|
||
margin: 0 0 8px 0;
|
||
font-size: 26px;
|
||
font-weight: 700;
|
||
letter-spacing: 1.5px;
|
||
color: #064e3b;
|
||
}
|
||
|
||
.brand-subtitle {
|
||
margin: 0 auto;
|
||
font-size: 14px;
|
||
line-height: 1.6;
|
||
color: #374151;
|
||
max-width: 680px;
|
||
}
|
||
|
||
.main-container {
|
||
max-width: 980px;
|
||
margin: -26px auto 0;
|
||
padding: 0 16px;
|
||
position: relative;
|
||
z-index: 1;
|
||
}
|
||
|
||
/* 任务状态卡片 (Status Banner) */
|
||
.status-banner {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
background: #ffffff;
|
||
padding: 14px 22px;
|
||
border-radius: 8px;
|
||
margin-bottom: 16px;
|
||
border: 1px solid #e5e7eb;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
|
||
}
|
||
|
||
.status-banner-main {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
|
||
.status-info {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.status-badge-art {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.task-no {
|
||
font-weight: 600;
|
||
color: #111827;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.status-tag {
|
||
border-color: #a7f3d0 !important;
|
||
color: #065f46 !important;
|
||
background-color: #ecfdf5 !important;
|
||
}
|
||
|
||
.expire-time {
|
||
font-size: 13px;
|
||
color: #4b5563;
|
||
}
|
||
|
||
.link-expired-alert,
|
||
.reject-reason-alert {
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.reject-reason-alert :deep(.el-alert__description) {
|
||
line-height: 1.6;
|
||
white-space: pre-wrap;
|
||
overflow-wrap: anywhere;
|
||
}
|
||
|
||
/* 主向导卡片 (Wizard Card) */
|
||
.wizard-card {
|
||
background: #ffffff;
|
||
border-radius: 10px;
|
||
padding: 28px 26px;
|
||
border: 1px solid #e5e7eb;
|
||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.04);
|
||
}
|
||
|
||
/* 流程指示器 (Step Bar) */
|
||
.steps-wrapper {
|
||
margin-bottom: 30px;
|
||
padding: 6px 14px 18px;
|
||
border-bottom: 1px solid #e5e7eb;
|
||
}
|
||
|
||
.steps-wrapper :deep(.el-step__title) {
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
color: #9ca3af;
|
||
}
|
||
|
||
.steps-wrapper :deep(.el-step__title.is-process) {
|
||
font-weight: 700;
|
||
color: #059669;
|
||
}
|
||
|
||
.steps-wrapper :deep(.el-step__head.is-process) {
|
||
color: #059669;
|
||
border-color: #059669;
|
||
}
|
||
|
||
.steps-wrapper :deep(.el-step__head.is-process .el-step__icon.is-text) {
|
||
background: #059669;
|
||
border-color: #059669;
|
||
color: #ffffff;
|
||
}
|
||
|
||
.steps-wrapper :deep(.el-step__title.is-wait) {
|
||
color: #9ca3af;
|
||
}
|
||
|
||
.steps-wrapper :deep(.el-step__head.is-wait) {
|
||
color: #9ca3af;
|
||
border-color: #e5e7eb;
|
||
}
|
||
|
||
.steps-wrapper :deep(.el-step__head.is-wait .el-step__icon.is-text) {
|
||
background: #f9fafb;
|
||
color: #9ca3af;
|
||
}
|
||
|
||
.steps-wrapper :deep(.el-step__title.is-success) {
|
||
color: #374151;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.steps-wrapper :deep(.el-step__head.is-success) {
|
||
color: #059669;
|
||
border-color: #059669;
|
||
}
|
||
|
||
.steps-wrapper :deep(.el-step__head.is-success .el-step__icon.is-text) {
|
||
background: #ecfdf5;
|
||
color: #059669;
|
||
}
|
||
|
||
.steps-wrapper :deep(.el-step__line) {
|
||
background-color: #e5e7eb;
|
||
}
|
||
|
||
.steps-wrapper :deep(.el-step__head.is-success .el-step__line) {
|
||
background-color: #059669;
|
||
}
|
||
|
||
.registration-header {
|
||
text-align: center;
|
||
border-bottom: 1px solid #e5e7eb;
|
||
padding-bottom: 18px;
|
||
margin-bottom: 26px;
|
||
}
|
||
|
||
.reg-title {
|
||
letter-spacing: 6px;
|
||
font-size: 22px;
|
||
font-weight: 700;
|
||
color: #111827;
|
||
margin: 0 0 8px 0;
|
||
}
|
||
|
||
.reg-subtitle {
|
||
font-size: 13px;
|
||
line-height: 1.6;
|
||
color: #4b5563;
|
||
margin: 0 auto;
|
||
max-width: 720px;
|
||
}
|
||
|
||
/* 卡片分区样式:淡绿顶边框与纯白背景 */
|
||
.card-section {
|
||
background: #ffffff;
|
||
border: 1px solid #e5e7eb;
|
||
border-top: 2px solid #059669;
|
||
border-radius: 8px;
|
||
padding: 20px 22px;
|
||
margin-bottom: 22px;
|
||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.02);
|
||
}
|
||
|
||
.card-section-title {
|
||
font-weight: 700;
|
||
font-size: 15px;
|
||
color: #111827;
|
||
margin-bottom: 18px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
/* 模块标题图标 (Section Icon) - 纯色无渐变 */
|
||
.title-icon-box {
|
||
width: 28px;
|
||
height: 28px;
|
||
background: #ecfdf5;
|
||
border: 1px solid #a7f3d0;
|
||
color: #059669;
|
||
border-radius: 6px;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 15px;
|
||
}
|
||
|
||
.card-section-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 14px;
|
||
}
|
||
|
||
.card-section-header .card-section-title {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.required-asterisk {
|
||
color: #e11d48;
|
||
margin-right: 4px;
|
||
font-weight: 700;
|
||
font-size: 15px;
|
||
}
|
||
|
||
.section-action-btn {
|
||
background-color: #ecfdf5 !important;
|
||
border: 1px solid #a7f3d0 !important;
|
||
color: #059669 !important;
|
||
font-weight: 500;
|
||
border-radius: 6px;
|
||
height: 28px;
|
||
line-height: 26px;
|
||
padding: 0 10px;
|
||
font-size: 12px;
|
||
transition: all 0.2s ease-in-out;
|
||
}
|
||
|
||
.section-action-btn :deep(.el-icon) {
|
||
color: inherit !important;
|
||
}
|
||
|
||
.section-action-btn:hover {
|
||
background-color: #059669 !important;
|
||
border-color: #059669 !important;
|
||
color: #ffffff !important;
|
||
}
|
||
|
||
.section-action-btn:active {
|
||
background-color: #047857 !important;
|
||
border-color: #047857 !important;
|
||
color: #ffffff !important;
|
||
}
|
||
|
||
.dynamic-row-box {
|
||
background: #fbfdfb;
|
||
border: 1px solid #e5e7eb;
|
||
border-radius: 6px;
|
||
padding: 16px;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.row-badge-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.row-badge {
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: #059669;
|
||
background: #ecfdf5;
|
||
border: 1px solid #a7f3d0;
|
||
padding: 2px 8px;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.empty-hint {
|
||
font-size: 13px;
|
||
color: #9ca3af;
|
||
padding: 10px 0;
|
||
text-align: center;
|
||
}
|
||
|
||
.full-width {
|
||
width: 100%;
|
||
}
|
||
|
||
.section-header {
|
||
margin-bottom: 22px;
|
||
}
|
||
|
||
.section-title {
|
||
margin: 0 0 6px 0;
|
||
font-size: 18px;
|
||
font-weight: 700;
|
||
color: #111827;
|
||
}
|
||
|
||
.section-desc {
|
||
margin: 0;
|
||
font-size: 13px;
|
||
color: #4b5563;
|
||
}
|
||
|
||
.materials-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||
gap: 16px;
|
||
}
|
||
|
||
.material-item-card {
|
||
border: 1px solid #e5e7eb;
|
||
border-radius: 8px;
|
||
padding: 16px;
|
||
background: #ffffff;
|
||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.02);
|
||
}
|
||
|
||
.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: #111827;
|
||
}
|
||
|
||
.mat-limit {
|
||
font-size: 12px;
|
||
color: #6b7280;
|
||
}
|
||
|
||
.file-badge {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
background: #fbfdfb;
|
||
border: 1px solid #e5e7eb;
|
||
padding: 10px 12px;
|
||
border-radius: 6px;
|
||
font-size: 13px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.material-thumbnail {
|
||
width: 68px;
|
||
height: 68px;
|
||
flex: 0 0 68px;
|
||
border-radius: 6px;
|
||
cursor: zoom-in;
|
||
}
|
||
|
||
.material-file-icon {
|
||
width: 36px;
|
||
height: 36px;
|
||
flex: 0 0 36px;
|
||
font-size: 26px;
|
||
}
|
||
|
||
.file-content {
|
||
display: flex;
|
||
flex: 1;
|
||
min-width: 0;
|
||
flex-direction: column;
|
||
gap: 3px;
|
||
}
|
||
|
||
.file-name {
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.file-size,
|
||
.preview-tip,
|
||
.upload-count {
|
||
font-size: 12px;
|
||
color: #6b7280;
|
||
}
|
||
|
||
.preview-tip {
|
||
color: #059669;
|
||
}
|
||
|
||
.file-actions,
|
||
.upload-action-box {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.upload-action-box {
|
||
flex-wrap: wrap;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.upload-limit-reached {
|
||
margin-top: 4px;
|
||
padding: 8px 10px;
|
||
border-radius: 6px;
|
||
background: #ecfdf5;
|
||
color: #065f46;
|
||
font-size: 12px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
/* 身份确认卡片 */
|
||
.identity-confirmation-card {
|
||
margin-bottom: 26px;
|
||
padding: 20px 22px;
|
||
border: 1px solid #bbf7d0;
|
||
border-top: 3px solid #059669;
|
||
border-radius: 8px;
|
||
background: #f0fdf4;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.identity-confirmation-heading {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: space-between;
|
||
gap: 14px;
|
||
margin-bottom: 14px;
|
||
}
|
||
|
||
.identity-confirmation-title {
|
||
margin: 0 0 6px;
|
||
color: #111827;
|
||
font-size: 16px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.identity-confirmation-desc {
|
||
margin: 0;
|
||
color: #4b5563;
|
||
font-size: 13px;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.identity-confirmation-list {
|
||
display: grid;
|
||
gap: 12px;
|
||
margin-top: 14px;
|
||
}
|
||
|
||
.identity-confirmation-item {
|
||
min-width: 0;
|
||
padding: 14px 16px;
|
||
border: 1px solid #e5e7eb;
|
||
border-radius: 6px;
|
||
background: #ffffff;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.identity-confirmation-item:hover {
|
||
border-color: #059669;
|
||
}
|
||
|
||
.identity-field-row {
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: 14px;
|
||
margin-bottom: 9px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.identity-field-label {
|
||
flex: 0 0 86px;
|
||
color: #4b5563;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.identity-field-value {
|
||
min-width: 0;
|
||
color: #111827;
|
||
font-size: 15px;
|
||
font-weight: 700;
|
||
letter-spacing: 0.02em;
|
||
overflow-wrap: anywhere;
|
||
}
|
||
|
||
.identity-confirmation-checkbox {
|
||
max-width: 100%;
|
||
height: auto;
|
||
margin-right: 0;
|
||
white-space: normal;
|
||
color: #111827;
|
||
}
|
||
|
||
.identity-confirmation-checkbox :deep(.el-checkbox__input) {
|
||
flex: 0 0 auto;
|
||
margin-top: 3px;
|
||
}
|
||
|
||
.identity-confirmation-checkbox :deep(.el-checkbox__input.is-checked .el-checkbox__inner) {
|
||
background-color: #059669;
|
||
border-color: #059669;
|
||
}
|
||
|
||
.identity-confirmation-checkbox :deep(.el-checkbox__label) {
|
||
min-width: 0;
|
||
padding-left: 8px;
|
||
white-space: normal;
|
||
overflow-wrap: anywhere;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.identity-confirmation-footer {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
margin-top: 16px;
|
||
padding-top: 12px;
|
||
border-top: 1px dashed #d1d5db;
|
||
}
|
||
|
||
.identity-confirmation-progress {
|
||
color: #4b5563;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.identity-confirmation-progress.is-complete {
|
||
color: #059669;
|
||
}
|
||
|
||
.contract-sign-card {
|
||
border: 1px solid #e5e7eb;
|
||
border-left: 3px solid #059669;
|
||
border-radius: 8px;
|
||
padding: 20px;
|
||
background: #ffffff;
|
||
margin-bottom: 16px;
|
||
min-width: 0;
|
||
max-width: 100%;
|
||
box-sizing: border-box;
|
||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.02);
|
||
}
|
||
|
||
.contract-meta {
|
||
display: flex;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: 10px;
|
||
margin-bottom: 8px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.contract-title {
|
||
font-size: 16px;
|
||
font-weight: 700;
|
||
color: #111827;
|
||
min-width: 0;
|
||
overflow-wrap: anywhere;
|
||
}
|
||
|
||
.contract-desc {
|
||
font-size: 13px;
|
||
color: #4b5563;
|
||
margin-bottom: 14px;
|
||
}
|
||
|
||
.contract-prepare-error {
|
||
margin-bottom: 14px;
|
||
}
|
||
|
||
.contract-preparing-alert {
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.contract-card-bottom {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
flex-wrap: wrap;
|
||
gap: 12px;
|
||
border-top: 1px dashed #e5e7eb;
|
||
padding-top: 14px;
|
||
min-width: 0;
|
||
max-width: 100%;
|
||
}
|
||
|
||
.agree-checkbox {
|
||
font-weight: 600;
|
||
min-width: 0;
|
||
max-width: 100%;
|
||
height: auto;
|
||
margin-right: 0;
|
||
white-space: normal;
|
||
}
|
||
|
||
.agree-checkbox :deep(.el-checkbox__input) {
|
||
flex: 0 0 auto;
|
||
margin-top: 3px;
|
||
}
|
||
|
||
.agree-checkbox :deep(.el-checkbox__input.is-checked .el-checkbox__inner) {
|
||
background-color: #059669;
|
||
border-color: #059669;
|
||
}
|
||
|
||
.agree-checkbox :deep(.el-checkbox__label) {
|
||
min-width: 0;
|
||
max-width: 100%;
|
||
padding-left: 8px;
|
||
white-space: normal;
|
||
overflow-wrap: anywhere;
|
||
word-break: break-word;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.contract-view-hint {
|
||
margin-top: 10px;
|
||
color: #059669;
|
||
font-size: 13px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.confirmed-text {
|
||
color: #059669;
|
||
font-weight: 600;
|
||
font-size: 14px;
|
||
}
|
||
|
||
/* 底部操作栏吸底优化 (Wizard Footer) - 纯色无渐变 */
|
||
.wizard-footer {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 14px;
|
||
margin-top: 32px;
|
||
border-top: 1px solid #e5e7eb;
|
||
padding-top: 20px;
|
||
position: sticky;
|
||
bottom: 0;
|
||
background: rgba(248, 250, 249, 0.96);
|
||
backdrop-filter: blur(8px);
|
||
padding-bottom: max(12px, env(safe-area-inset-bottom));
|
||
z-index: 10;
|
||
}
|
||
|
||
.wizard-footer .el-button--primary {
|
||
background-color: #059669;
|
||
border: 1px solid #059669;
|
||
color: #ffffff;
|
||
font-weight: 600;
|
||
border-radius: 6px;
|
||
padding: 10px 24px;
|
||
height: 40px;
|
||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.wizard-footer .el-button--primary:hover {
|
||
background-color: #047857;
|
||
border-color: #047857;
|
||
color: #ffffff;
|
||
}
|
||
|
||
.wizard-footer .el-button--primary:active {
|
||
background-color: #065f46;
|
||
border-color: #065f46;
|
||
color: #ffffff;
|
||
}
|
||
|
||
.wizard-footer .el-button--default {
|
||
border-radius: 6px;
|
||
padding: 10px 20px;
|
||
height: 40px;
|
||
color: #374151;
|
||
border-color: #d1d5db;
|
||
background-color: #ffffff;
|
||
}
|
||
|
||
.wizard-footer .el-button--default:hover {
|
||
color: #059669;
|
||
border-color: #059669;
|
||
background-color: #ecfdf5;
|
||
}
|
||
|
||
.wizard-footer .el-button--success {
|
||
background-color: #059669 !important;
|
||
border-color: #059669 !important;
|
||
color: #ffffff !important;
|
||
font-weight: 600;
|
||
border-radius: 6px;
|
||
padding: 10px 24px;
|
||
height: 40px;
|
||
box-shadow: 0 2px 8px rgba(5, 150, 105, 0.25);
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.wizard-footer .el-button--success:hover {
|
||
background-color: #047857 !important;
|
||
border-color: #047857 !important;
|
||
color: #ffffff !important;
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 4px 12px rgba(5, 150, 105, 0.35);
|
||
}
|
||
|
||
.wizard-footer .el-button--success:active {
|
||
background-color: #065f46 !important;
|
||
border-color: #065f46 !important;
|
||
color: #ffffff !important;
|
||
}
|
||
|
||
/* 个人信息已完整覆盖卡片与图标 */
|
||
.all-covered-card {
|
||
padding: 38px 24px;
|
||
background: #ffffff;
|
||
border: 1px solid #d1fae5;
|
||
border-radius: 14px;
|
||
box-shadow: 0 4px 20px rgba(5, 150, 105, 0.06);
|
||
text-align: center;
|
||
margin: 16px 0 24px;
|
||
}
|
||
|
||
.covered-status-mark {
|
||
display: flex;
|
||
width: 56px;
|
||
height: 56px;
|
||
margin: 0 auto 16px;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: #ffffff;
|
||
background: #059669;
|
||
border-radius: 50%;
|
||
box-shadow: 0 4px 14px rgba(5, 150, 105, 0.25);
|
||
}
|
||
|
||
.covered-status-title {
|
||
margin: 0 0 10px;
|
||
color: #064e3b;
|
||
font-size: 20px;
|
||
font-weight: 700;
|
||
letter-spacing: 0.5px;
|
||
}
|
||
|
||
.covered-status-desc {
|
||
max-width: 480px;
|
||
margin: 0 auto;
|
||
color: #64748b;
|
||
font-size: 14px;
|
||
line-height: 1.7;
|
||
}
|
||
|
||
.readonly-banner {
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.desktop-only {
|
||
display: block;
|
||
}
|
||
|
||
.mobile-only {
|
||
display: none;
|
||
}
|
||
|
||
/* 移动端步骤条 - 纯色无渐变 */
|
||
.mobile-steps-wrapper {
|
||
margin-bottom: 24px;
|
||
background: #ffffff;
|
||
border: 1px solid #e5e7eb;
|
||
border-radius: 8px;
|
||
padding: 12px 14px;
|
||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.02);
|
||
}
|
||
|
||
.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: #e5e7eb;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.mobile-step-item.is-active .step-indicator-bar {
|
||
background: #059669;
|
||
height: 4px;
|
||
}
|
||
|
||
.mobile-step-item.is-finished .step-indicator-bar {
|
||
background: #059669;
|
||
}
|
||
|
||
.step-label {
|
||
font-size: 11px;
|
||
color: #9ca3af;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.mobile-step-item.is-active .step-label {
|
||
color: #059669;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.mobile-step-item.is-finished .step-label {
|
||
color: #374151;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.mobile-current-indicator {
|
||
font-size: 12px;
|
||
color: #4b5563;
|
||
text-align: center;
|
||
border-top: 1px dashed #e5e7eb;
|
||
padding-top: 8px;
|
||
}
|
||
|
||
.highlight-step {
|
||
color: #059669;
|
||
font-weight: 700;
|
||
}
|
||
|
||
/* 表单与交互元素 (Form Controls) */
|
||
.reg-form :deep(.el-form-item__label) {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: #111827;
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
/* 必填星号 * 颜色:#e11d48 */
|
||
.reg-form :deep(.el-form-item.is-required:not(.is-no-asterisk) > .el-form-item__label:before),
|
||
.reg-form :deep(.el-form-item.is-required:not(.is-no-asterisk) .el-form-item__label-wrap > .el-form-item__label:before) {
|
||
color: #e11d48 !important;
|
||
}
|
||
|
||
/* 输入框边框默认态与聚焦态纯色清晰 */
|
||
.reg-form :deep(.el-input__wrapper),
|
||
.reg-form :deep(.el-select__wrapper),
|
||
.reg-form :deep(.el-textarea__inner) {
|
||
box-shadow: 0 0 0 1px #d1d5db inset !important;
|
||
border-radius: 6px;
|
||
background-color: #ffffff;
|
||
transition: all 0.2s ease-in-out;
|
||
}
|
||
|
||
.reg-form :deep(.el-input__wrapper:hover),
|
||
.reg-form :deep(.el-select__wrapper:hover),
|
||
.reg-form :deep(.el-textarea__inner:hover) {
|
||
box-shadow: 0 0 0 1px #059669 inset !important;
|
||
}
|
||
|
||
.reg-form :deep(.el-input__wrapper.is-focus),
|
||
.reg-form :deep(.el-select__wrapper.is-focused),
|
||
.reg-form :deep(.el-textarea__inner:focus) {
|
||
box-shadow: 0 0 0 1px #059669 inset !important;
|
||
border-color: #059669 !important;
|
||
}
|
||
|
||
.readonly-derived-input :deep(.el-input__wrapper) {
|
||
background-color: #f9fafb !important;
|
||
cursor: default;
|
||
}
|
||
|
||
.readonly-derived-input :deep(.el-input__inner) {
|
||
color: #374151 !important;
|
||
-webkit-text-fill-color: #374151 !important;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.readonly-derived-input :deep(.el-input__prefix) {
|
||
color: #059669;
|
||
}
|
||
|
||
@media (max-width: 640px) {
|
||
|
||
.fill-page :deep(.el-input__inner),
|
||
.fill-page :deep(.el-textarea__inner),
|
||
.fill-page :deep(.el-select__input) {
|
||
font-size: 16px !important;
|
||
}
|
||
|
||
.desktop-only {
|
||
display: none !important;
|
||
}
|
||
|
||
.mobile-only {
|
||
display: block !important;
|
||
}
|
||
|
||
.brand-header {
|
||
padding: 32px 16px 40px;
|
||
}
|
||
|
||
.brand-title {
|
||
font-size: 22px;
|
||
}
|
||
|
||
.main-container {
|
||
padding: 0 10px;
|
||
}
|
||
|
||
.wizard-card {
|
||
padding: 20px 16px;
|
||
}
|
||
|
||
.status-banner {
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
gap: 8px;
|
||
}
|
||
|
||
.status-badge-art {
|
||
display: none;
|
||
}
|
||
|
||
.file-badge {
|
||
align-items: flex-start;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.file-actions {
|
||
width: 100%;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.identity-confirmation-card {
|
||
padding: 14px;
|
||
}
|
||
|
||
.identity-confirmation-heading,
|
||
.identity-confirmation-footer {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
}
|
||
|
||
.identity-field-row {
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
|
||
.identity-field-label {
|
||
flex-basis: auto;
|
||
}
|
||
|
||
.identity-confirmation-footer>.el-button {
|
||
width: 100%;
|
||
}
|
||
|
||
.contract-card-bottom {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
}
|
||
|
||
.contract-sign-card {
|
||
padding: 16px;
|
||
}
|
||
|
||
.contract-card-bottom>.el-button,
|
||
.contract-card-bottom>.agree-checkbox {
|
||
width: 100%;
|
||
max-width: 100%;
|
||
box-sizing: border-box;
|
||
}
|
||
}
|
||
</style>
|