11
This commit is contained in:
@@ -121,84 +121,147 @@
|
||||
</div>
|
||||
|
||||
<!-- 发起任务弹窗 -->
|
||||
<el-dialog v-model="createVisible" title="发起入职任务" width="760px" destroy-on-close>
|
||||
<el-form :model="form" label-width="90px">
|
||||
<el-form-item label="所属公司" required>
|
||||
<el-select
|
||||
v-model="form.companyId"
|
||||
filterable
|
||||
placeholder="请选择公司主体"
|
||||
class="full-width"
|
||||
@change="onCompanyChange"
|
||||
>
|
||||
<el-option v-for="item in companies" :key="item.id" :label="item.companyName" :value="item.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-dialog
|
||||
v-model="createVisible"
|
||||
title="发起入职任务"
|
||||
width="1050px"
|
||||
top="4vh"
|
||||
destroy-on-close
|
||||
append-to-body
|
||||
class="create-task-dialog"
|
||||
>
|
||||
<div class="create-task-dialog-body">
|
||||
<el-form :model="form" label-position="top">
|
||||
<!-- 1. 基础岗位选择板块 -->
|
||||
<div class="form-card-section">
|
||||
<div class="section-title">
|
||||
<span class="title-text">入职基础信息</span>
|
||||
<span class="title-tip">请选择新员工入职的公司主体、岗位及适用的入职模板</span>
|
||||
</div>
|
||||
|
||||
<el-form-item label="入职岗位" required>
|
||||
<el-select
|
||||
v-model="form.positionId"
|
||||
filterable
|
||||
placeholder="请选择岗位"
|
||||
class="full-width"
|
||||
:disabled="!form.companyId"
|
||||
@change="onPositionChange"
|
||||
>
|
||||
<el-option v-for="item in positions" :key="item.id" :label="item.positionName" :value="item.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="8">
|
||||
<el-form-item label="所属公司" required>
|
||||
<el-select
|
||||
v-model="form.companyId"
|
||||
filterable
|
||||
placeholder="请选择公司主体"
|
||||
class="full-width"
|
||||
@change="onCompanyChange"
|
||||
>
|
||||
<el-option v-for="item in companies" :key="item.id" :label="item.companyName" :value="item.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-form-item label="入职模板" required>
|
||||
<el-select
|
||||
v-model="form.onboardingTemplateId"
|
||||
filterable
|
||||
placeholder="请选择入职模板"
|
||||
class="full-width"
|
||||
:disabled="!form.positionId"
|
||||
@change="onOnboardingTemplateChange"
|
||||
>
|
||||
<el-option v-for="item in onboardingTemplates" :key="item.id" :label="item.templateName" :value="item.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-col :xs="24" :sm="8">
|
||||
<el-form-item label="入职岗位" required>
|
||||
<el-select
|
||||
v-model="form.positionId"
|
||||
filterable
|
||||
placeholder="请选择岗位"
|
||||
class="full-width"
|
||||
:disabled="!form.companyId"
|
||||
@change="onPositionChange"
|
||||
>
|
||||
<el-option v-for="item in positions" :key="item.id" :label="item.positionName" :value="item.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<template v-if="form.contractVariables.length">
|
||||
<el-divider content-position="left">合同人事参数</el-divider>
|
||||
<el-alert
|
||||
title="以下内容会在候选人查看合同时自动替换到 DOCX 中,请在发送链接前确认。"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
class="contract-variable-tip"
|
||||
/>
|
||||
<el-form-item
|
||||
v-for="item in form.contractVariables"
|
||||
:key="`${item.contractTemplateVersionId}-${item.variableCode}`"
|
||||
:label="item.variableName"
|
||||
:required="item.required"
|
||||
>
|
||||
<el-date-picker
|
||||
v-if="item.dataType === 'date'"
|
||||
v-model="item.variableValue"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="请选择日期"
|
||||
class="full-width"
|
||||
<el-col :xs="24" :sm="8">
|
||||
<el-form-item label="入职模板" required>
|
||||
<el-select
|
||||
v-model="form.onboardingTemplateId"
|
||||
filterable
|
||||
placeholder="请选择入职模板"
|
||||
class="full-width"
|
||||
:disabled="!form.positionId"
|
||||
@change="onOnboardingTemplateChange"
|
||||
>
|
||||
<el-option v-for="item in onboardingTemplates" :key="item.id" :label="item.templateName" :value="item.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<!-- 2. 合同人事参数卡片 -->
|
||||
<div v-if="form.contractVariables.length" class="form-card-section">
|
||||
<div class="section-title">
|
||||
<span class="title-text">合同人事参数预设</span>
|
||||
<span class="title-tip">以下参数将自动替换进 DOCX 合同中,请在向员工发送链接前核对填写</span>
|
||||
</div>
|
||||
|
||||
<el-alert
|
||||
title="提示:此处填写的内容将作为合同正式条款,如薪资、合同起止日期、试用期等,请谨慎填写。"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
style="margin-bottom: 16px;"
|
||||
/>
|
||||
<el-input
|
||||
v-else
|
||||
v-model="item.variableValue"
|
||||
:placeholder="`${item.contractName} · ${item.variableCode}`"
|
||||
/>
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<el-form-item label="人事备注">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="3" placeholder="填写该候选人入职的相关备注(选填)" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-row :gutter="20">
|
||||
<el-col
|
||||
v-for="item in form.contractVariables"
|
||||
:key="`${item.contractTemplateVersionId}-${item.variableCode}`"
|
||||
:xs="24"
|
||||
:sm="12"
|
||||
>
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<div class="var-label-row">
|
||||
<span v-if="item.required" class="required-star">*</span>
|
||||
<span class="var-name">{{ item.variableName }}</span>
|
||||
<el-tag size="small" type="info" effect="plain" class="contract-source-tag">
|
||||
{{ item.contractName }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-date-picker
|
||||
v-if="item.dataType === 'date'"
|
||||
v-model="item.variableValue"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="选择日期"
|
||||
class="var-input-field"
|
||||
/>
|
||||
<el-input-number
|
||||
v-else-if="item.dataType === 'number' || item.dataType === 'decimal'"
|
||||
v-model="item.variableValue"
|
||||
controls-position="right"
|
||||
class="var-input-field"
|
||||
placeholder="输入金额或数值"
|
||||
/>
|
||||
<el-input
|
||||
v-else
|
||||
v-model="item.variableValue"
|
||||
:placeholder="`请输入${item.variableName}`"
|
||||
class="var-input-field"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<!-- 3. 其他备注 -->
|
||||
<div class="form-card-section">
|
||||
<div class="section-title">
|
||||
<span class="title-text">补充备注</span>
|
||||
<span class="title-tip">填写该候选人入职的相关流转备注(仅人事可见,选填)</span>
|
||||
</div>
|
||||
<el-form-item label="">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="2" placeholder="请输入人事备注说明(选填)" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="createVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="creating" @click="createTask">立即创建</el-button>
|
||||
<el-button type="primary" :loading="creating" @click="createTask">立即发起入职</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
@@ -242,23 +305,32 @@
|
||||
<el-drawer
|
||||
v-model="detailVisible"
|
||||
:title="`入职任务详情 · ${detail?.task?.taskNo || ''}`"
|
||||
size="860px"
|
||||
size="1180px"
|
||||
destroy-on-close
|
||||
@closed="clearMaterialPreviews"
|
||||
>
|
||||
<div v-if="detail" class="drawer-content">
|
||||
<!-- 头部状态栏 -->
|
||||
<div class="drawer-status-bar">
|
||||
<div class="status-left">
|
||||
<span class="task-no-large">{{ detail.task.taskNo }}</span>
|
||||
<el-tag :type="statusType(detail.task.taskStatus)" size="large" effect="dark">
|
||||
{{ statusText(detail.task.taskStatus) }}
|
||||
</el-tag>
|
||||
<span v-if="detail.task.submitCount > 0" class="submit-count-badge">
|
||||
第 {{ detail.task.submitCount }} 次提交
|
||||
</span>
|
||||
<!-- 头部状态栏 (100% 满宽卡片) -->
|
||||
<div class="drawer-status-banner">
|
||||
<div class="banner-left">
|
||||
<div class="banner-title-row">
|
||||
<span class="task-no-large">{{ detail.task.taskNo }}</span>
|
||||
<el-tag :type="statusType(detail.task.taskStatus)" size="large" effect="dark">
|
||||
{{ statusText(detail.task.taskStatus) }}
|
||||
</el-tag>
|
||||
<span v-if="detail.task.submitCount > 0" class="submit-count-badge">
|
||||
第 {{ detail.task.submitCount }} 次提交
|
||||
</span>
|
||||
</div>
|
||||
<div class="banner-meta-row">
|
||||
<span>所属公司:<strong>{{ detail.task.companyName || '-' }}</strong></span>
|
||||
<span>入职岗位:<strong>{{ detail.task.positionName || '-' }}</strong></span>
|
||||
<span>入职模板:<strong>{{ detail.task.onboardingTemplateName || '-' }}</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="status-right">
|
||||
<span class="created-time">发起于:{{ formatTime(detail.task.createTime) }}</span>
|
||||
<div class="banner-right">
|
||||
<div class="meta-item">发起时间:{{ formatTime(detail.task.createTime) }}</div>
|
||||
<div v-if="detail.task.linkExpireAt" class="meta-item">链接有效期至:{{ formatTime(detail.task.linkExpireAt) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -275,51 +347,156 @@
|
||||
<!-- Tab 切换 -->
|
||||
<el-tabs v-model="activeDetailTab" class="detail-tabs">
|
||||
<!-- 1. 候选人档案与动态信息 -->
|
||||
<el-tab-pane label="候选人信息" name="profile">
|
||||
<el-card shadow="never" class="mb-4 border-card">
|
||||
<template #header>
|
||||
<div class="card-header-flex">
|
||||
<span class="font-bold">基本个人档案</span>
|
||||
<el-tag size="small" type="info">固定信息</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
<el-descriptions v-if="detail.employeeInfo" :column="2" border>
|
||||
<el-descriptions-item label="姓名">
|
||||
<span class="font-bold text-primary">{{ detail.employeeInfo.employeeName }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="性别">{{ detail.employeeInfo.gender || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="手机号码">{{ detail.employeeInfo.phone }}</el-descriptions-item>
|
||||
<el-descriptions-item label="身份证号码">{{ detail.employeeInfo.idCardNumber }}</el-descriptions-item>
|
||||
<el-descriptions-item label="毕业院校">{{ detail.employeeInfo.college || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="学历 / 专业">
|
||||
{{ detail.employeeInfo.education || '-' }} / {{ detail.employeeInfo.major || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="现居地址" :span="2">
|
||||
{{ detail.employeeInfo.contactAddress || '-' }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-empty v-else description="候选人尚未提交基础档案" :image-size="80" />
|
||||
</el-card>
|
||||
<el-tab-pane label="候选人档案 (入职登记表)" name="profile">
|
||||
<div v-if="!detail.employeeInfo || !detail.employeeInfo.employeeName" class="empty-profile-box">
|
||||
<el-empty description="候选人尚未填写或提交《入职登记表》" :image-size="100" />
|
||||
</div>
|
||||
|
||||
<!-- 动态填写字段 -->
|
||||
<el-card shadow="never" class="border-card">
|
||||
<template #header>
|
||||
<div class="card-header-flex">
|
||||
<span class="font-bold">岗位补充登记信息</span>
|
||||
<el-tag size="small" type="info">动态字段</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
<el-descriptions v-if="detail.formValues && detail.formValues.length" :column="2" border>
|
||||
<el-descriptions-item
|
||||
v-for="item in detail.formValues"
|
||||
:key="item.id"
|
||||
:label="item.fieldName || item.fieldCode"
|
||||
>
|
||||
{{ item.fieldValue || '-' }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-empty v-else description="暂无岗位补充字段或未填写" :image-size="80" />
|
||||
</el-card>
|
||||
<div v-else class="profile-details-wrapper">
|
||||
<!-- 1. 基本个人档案 -->
|
||||
<el-card shadow="never" class="mb-3 border-card">
|
||||
<template #header>
|
||||
<div class="card-header-flex">
|
||||
<span class="font-bold card-title-text"><el-icon class="mr-1"><UserFilled /></el-icon> 基本个人信息</span>
|
||||
<el-tag size="small" type="primary">档案标准项</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
<el-descriptions :column="3" border size="default">
|
||||
<el-descriptions-item label="真实姓名">
|
||||
<strong class="text-primary font-bold">{{ detail.employeeInfo.employeeName }}</strong>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="性别">{{ detail.employeeInfo.gender || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="民族">{{ detail.employeeInfo.nation || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="出生年月日">{{ detail.employeeInfo.birthday || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="户籍所在地" :span="2">{{ detail.employeeInfo.registeredResidence || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="身高 / 体重">
|
||||
{{ detail.employeeInfo.height ? detail.employeeInfo.height + ' cm' : '-' }} /
|
||||
{{ detail.employeeInfo.weight ? detail.employeeInfo.weight + ' kg' : '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="健康状况">{{ detail.employeeInfo.healthCondition || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="婚育情况">{{ detail.employeeInfo.maritalStatus || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="身份证号码" :span="3">
|
||||
<code class="code-font font-bold">{{ detail.employeeInfo.idCardNumber || '-' }}</code>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
|
||||
<!-- 2. 学历与职业资质 -->
|
||||
<el-card shadow="never" class="mb-3 border-card">
|
||||
<template #header>
|
||||
<div class="card-header-flex">
|
||||
<span class="font-bold card-title-text"><el-icon class="mr-1"><School /></el-icon> 学历与职业资格</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-descriptions :column="3" border size="default">
|
||||
<el-descriptions-item label="最高学历">{{ detail.employeeInfo.education || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="所学专业">{{ detail.employeeInfo.major || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="毕业院校">{{ detail.employeeInfo.graduatedSchool || detail.employeeInfo.college || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="职业资格/职称">{{ detail.employeeInfo.vocationalQualification || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="资格等级">{{ detail.employeeInfo.qualificationLevel || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="特长技能">{{ detail.employeeInfo.technicalSpecialty || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
|
||||
<!-- 3. 联系方式与银行卡 -->
|
||||
<el-card shadow="never" class="mb-3 border-card">
|
||||
<template #header>
|
||||
<div class="card-header-flex">
|
||||
<span class="font-bold card-title-text"><el-icon class="mr-1"><Phone /></el-icon> 联系方式与工资银行卡</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-descriptions :column="3" border size="default">
|
||||
<el-descriptions-item label="联系电话">{{ detail.employeeInfo.phone || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="电子邮箱" :span="2">{{ detail.employeeInfo.email || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="现居住详细地址" :span="2">{{ detail.employeeInfo.currentAddress || detail.employeeInfo.contactAddress || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="房东联系方式">{{ detail.employeeInfo.landlordContact || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="工资银行卡号" :span="2">
|
||||
<code class="code-font font-bold">{{ detail.employeeInfo.bankCardNumber || '-' }}</code>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="开户银行全称">{{ detail.employeeInfo.bankName || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
|
||||
<!-- 4. 紧急联系人 -->
|
||||
<el-card shadow="never" class="mb-3 border-card">
|
||||
<template #header>
|
||||
<div class="card-header-flex">
|
||||
<span class="font-bold card-title-text"><el-icon class="mr-1"><FirstAidKit /></el-icon> 紧急联系人</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-descriptions :column="3" border size="default">
|
||||
<el-descriptions-item label="紧急联系人姓名">{{ detail.employeeInfo.emergencyContact || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="联系电话">{{ detail.employeeInfo.emergencyPhone || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="联系地址">{{ detail.employeeInfo.emergencyAddress || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
|
||||
<!-- 5. 教育学习经历 -->
|
||||
<el-card shadow="never" class="mb-3 border-card">
|
||||
<template #header>
|
||||
<div class="card-header-flex">
|
||||
<span class="font-bold card-title-text"><el-icon class="mr-1"><Reading /></el-icon> 教育学习经历</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-table :data="detailEducationList" border stripe empty-text="未填写教育学习经历">
|
||||
<el-table-column prop="timeRange" label="起止时间" min-width="160" />
|
||||
<el-table-column prop="location" label="学校 / 机构" min-width="200" />
|
||||
<el-table-column prop="content" label="所学专业 / 学习内容" min-width="200" />
|
||||
<el-table-column prop="result" label="学习结果 / 获得学位" min-width="180" />
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<!-- 6. 主要工作经历 -->
|
||||
<el-card shadow="never" class="mb-3 border-card">
|
||||
<template #header>
|
||||
<div class="card-header-flex">
|
||||
<span class="font-bold card-title-text"><el-icon class="mr-1"><Briefcase /></el-icon> 主要工作经历</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-table :data="detailWorkList" border stripe empty-text="未填写工作经历(应届生无记录)">
|
||||
<el-table-column prop="timeRange" label="起止时间" min-width="160" />
|
||||
<el-table-column prop="companyName" label="工作单位" min-width="220" />
|
||||
<el-table-column prop="position" label="担任职务" width="140" />
|
||||
<el-table-column prop="salary" label="薪资待遇" width="120" />
|
||||
<el-table-column prop="leaveReason" label="离职原因" min-width="180" />
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<!-- 7. 家庭成员 -->
|
||||
<el-card shadow="never" class="mb-3 border-card">
|
||||
<template #header>
|
||||
<div class="card-header-flex">
|
||||
<span class="font-bold card-title-text"><el-icon class="mr-1"><HomeFilled /></el-icon> 家庭成员</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-table :data="detailFamilyList" border stripe empty-text="未填写家庭成员">
|
||||
<el-table-column prop="name" label="姓名" width="120" />
|
||||
<el-table-column prop="relationship" label="与本人关系" width="120" />
|
||||
<el-table-column prop="age" label="年龄" width="90" align="center" />
|
||||
<el-table-column prop="company" label="工作单位及职务" min-width="240" />
|
||||
<el-table-column prop="phone" label="联系电话" width="150" />
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<!-- 8. 合同专有自定义补充项 (如果有) -->
|
||||
<el-card v-if="detailCustomVars.length" shadow="never" class="mb-3 border-card">
|
||||
<template #header>
|
||||
<div class="card-header-flex">
|
||||
<span class="font-bold card-title-text">合同专属补充信息</span>
|
||||
<el-tag size="small" type="warning">合同专有项</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
<el-descriptions :column="2" border size="default">
|
||||
<el-descriptions-item
|
||||
v-for="item in detailCustomVars"
|
||||
:key="item.key"
|
||||
:label="item.key"
|
||||
>
|
||||
{{ item.value || '-' }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 2. 证件与材料复核画廊 -->
|
||||
@@ -337,19 +514,16 @@
|
||||
>
|
||||
<div class="material-img-wrapper">
|
||||
<el-image
|
||||
v-if="isImageMaterial(item) && materialPreviewUrls[item.fileId]"
|
||||
class="material-thumb"
|
||||
:src="fileDownloadUrl(item.fileId)"
|
||||
:preview-src-list="[fileDownloadUrl(item.fileId)]"
|
||||
:src="materialPreviewUrls[item.fileId]"
|
||||
fit="cover"
|
||||
preview-teleported
|
||||
>
|
||||
<template #error>
|
||||
<div class="material-thumb-placeholder">
|
||||
<el-icon :size="32"><Document /></el-icon>
|
||||
<span>文档文件</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
@click="previewImage(item.fileId)"
|
||||
/>
|
||||
<div v-else class="material-thumb-placeholder">
|
||||
<el-icon :size="32"><Document /></el-icon>
|
||||
<span>{{ materialPreviewText(item) }}</span>
|
||||
</div>
|
||||
<div class="material-overlay-status">
|
||||
<el-tag
|
||||
:type="item.reviewStatus === 'passed' ? 'success' : item.reviewStatus === 'need_supplement' ? 'danger' : 'warning'"
|
||||
@@ -367,7 +541,18 @@
|
||||
</div>
|
||||
|
||||
<div class="material-actions-flex">
|
||||
<el-button link type="primary" size="small" icon="Download" @click="download(item.fileId)">
|
||||
<el-button
|
||||
v-if="isImageMaterial(item)"
|
||||
link
|
||||
type="primary"
|
||||
size="small"
|
||||
icon="View"
|
||||
:disabled="!materialPreviewUrls[item.fileId]"
|
||||
@click="previewImage(item.fileId)"
|
||||
>
|
||||
图片预览
|
||||
</el-button>
|
||||
<el-button link type="primary" size="small" icon="Download" @click="download(item.fileId, item.fileName)">
|
||||
原件下载
|
||||
</el-button>
|
||||
|
||||
@@ -519,6 +704,13 @@
|
||||
</template>
|
||||
</el-drawer>
|
||||
|
||||
<el-image-viewer
|
||||
v-if="imageViewerVisible"
|
||||
:url-list="imageViewerUrls"
|
||||
hide-on-click-modal
|
||||
@close="closeImageViewer"
|
||||
/>
|
||||
|
||||
<!-- 合同参数弹窗 -->
|
||||
<el-dialog
|
||||
v-model="contractVariablesVisible"
|
||||
@@ -586,7 +778,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="onboardingTask">
|
||||
import { ref, reactive, onMounted } from "vue";
|
||||
import { ref, reactive, computed, onMounted, onBeforeUnmount } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import {
|
||||
DocumentAdd,
|
||||
@@ -632,6 +824,53 @@ const createdTaskInfo = ref<any>(null);
|
||||
const detailVisible = ref(false);
|
||||
const activeDetailTab = ref("profile");
|
||||
const detail = ref<any>();
|
||||
const materialPreviewUrls = reactive<Record<number, string>>({});
|
||||
const materialPreviewLoading = reactive<Record<number, boolean>>({});
|
||||
const materialPreviewFailed = reactive<Record<number, boolean>>({});
|
||||
const imageViewerVisible = ref(false);
|
||||
const imageViewerUrls = ref<string[]>([]);
|
||||
|
||||
const imageMimeTypes: Record<string, string> = {
|
||||
bmp: "image/bmp",
|
||||
gif: "image/gif",
|
||||
jpeg: "image/jpeg",
|
||||
jpg: "image/jpeg",
|
||||
png: "image/png",
|
||||
webp: "image/webp"
|
||||
};
|
||||
|
||||
const detailEducationList = computed(() => {
|
||||
try {
|
||||
return JSON.parse(detail.value?.employeeInfo?.educationHistoryJson || "[]");
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
const detailWorkList = computed(() => {
|
||||
try {
|
||||
return JSON.parse(detail.value?.employeeInfo?.workHistoryJson || "[]");
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
const detailFamilyList = computed(() => {
|
||||
try {
|
||||
return JSON.parse(detail.value?.employeeInfo?.familyMembersJson || "[]");
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
const detailCustomVars = computed(() => {
|
||||
try {
|
||||
const obj = JSON.parse(detail.value?.employeeInfo?.customVariablesJson || "{}");
|
||||
return Object.entries(obj).map(([k, v]) => ({ key: k, value: String(v) }));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
const contractVariablesVisible = ref(false);
|
||||
const contractVariableTask = ref<OnboardingTask>();
|
||||
@@ -781,9 +1020,96 @@ const formatTime = (timeStr?: string) => {
|
||||
return timeStr.replace("T", " ").substring(0, 19);
|
||||
};
|
||||
|
||||
const fileDownloadUrl = (fileId: number) => {
|
||||
return `/api/sys/dev/file/download?id=${fileId}`;
|
||||
};
|
||||
function materialFileSuffix(item: any) {
|
||||
const suffix = String(item?.fileSuffix || "")
|
||||
.trim()
|
||||
.replace(/^\./, "")
|
||||
.toLowerCase();
|
||||
if (suffix) return suffix;
|
||||
|
||||
const fileName = String(item?.fileName || "");
|
||||
const dotIndex = fileName.lastIndexOf(".");
|
||||
return dotIndex >= 0 ? fileName.substring(dotIndex + 1).toLowerCase() : "";
|
||||
}
|
||||
|
||||
function isImageMaterial(item: any) {
|
||||
return Boolean(imageMimeTypes[materialFileSuffix(item)]);
|
||||
}
|
||||
|
||||
function materialPreviewText(item: any) {
|
||||
if (!isImageMaterial(item)) return "非图片文件";
|
||||
if (materialPreviewLoading[item.fileId]) return "图片加载中";
|
||||
if (materialPreviewFailed[item.fileId]) return "图片预览加载失败";
|
||||
return "图片待加载";
|
||||
}
|
||||
|
||||
function closeImageViewer() {
|
||||
imageViewerVisible.value = false;
|
||||
imageViewerUrls.value = [];
|
||||
}
|
||||
|
||||
function clearMaterialPreviews() {
|
||||
Object.values(materialPreviewUrls).forEach(url => URL.revokeObjectURL(url));
|
||||
Object.keys(materialPreviewUrls).forEach(key => delete materialPreviewUrls[Number(key)]);
|
||||
Object.keys(materialPreviewLoading).forEach(key => delete materialPreviewLoading[Number(key)]);
|
||||
Object.keys(materialPreviewFailed).forEach(key => delete materialPreviewFailed[Number(key)]);
|
||||
closeImageViewer();
|
||||
}
|
||||
|
||||
function previewImage(fileId: number) {
|
||||
const previewUrl = materialPreviewUrls[fileId];
|
||||
if (!previewUrl) {
|
||||
ElMessage.warning("图片正在加载或暂时无法预览");
|
||||
return;
|
||||
}
|
||||
|
||||
imageViewerUrls.value = [previewUrl];
|
||||
imageViewerVisible.value = true;
|
||||
}
|
||||
|
||||
async function loadMaterialPreviews(materials: any[]) {
|
||||
clearMaterialPreviews();
|
||||
const imageMaterials = (materials || []).filter(isImageMaterial);
|
||||
|
||||
await Promise.all(imageMaterials.map(async item => {
|
||||
materialPreviewLoading[item.fileId] = true;
|
||||
materialPreviewFailed[item.fileId] = false;
|
||||
|
||||
try {
|
||||
const response: any = await onboardingTaskApi.downloadFile(item.fileId);
|
||||
const sourceBlob = response?.data;
|
||||
if (!(sourceBlob instanceof Blob)) {
|
||||
throw new Error("服务器未返回有效的图片文件");
|
||||
}
|
||||
|
||||
const mimeType = imageMimeTypes[materialFileSuffix(item)];
|
||||
const imageBlob = sourceBlob.type.startsWith("image/")
|
||||
? sourceBlob
|
||||
: new Blob([sourceBlob], { type: mimeType });
|
||||
materialPreviewUrls[item.fileId] = URL.createObjectURL(imageBlob);
|
||||
} catch {
|
||||
materialPreviewFailed[item.fileId] = true;
|
||||
} finally {
|
||||
materialPreviewLoading[item.fileId] = false;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
function resolveDownloadFileName(response: any, fallbackFileName?: string) {
|
||||
const disposition = String(response?.headers?.["content-disposition"] || "");
|
||||
const encodedName = disposition.match(/filename\*=UTF-8\'\'([^;]+)/i)?.[1]
|
||||
|| disposition.match(/filename="?([^";]+)"?/i)?.[1];
|
||||
|
||||
if (encodedName) {
|
||||
try {
|
||||
return decodeURIComponent(encodedName.replace(/\+/g, "%20"));
|
||||
} catch {
|
||||
return encodedName;
|
||||
}
|
||||
}
|
||||
|
||||
return fallbackFileName || "入职材料";
|
||||
}
|
||||
|
||||
// 列表查询并统计
|
||||
async function getTableData(params: any) {
|
||||
@@ -922,10 +1248,11 @@ function copyInviteMessage() {
|
||||
}
|
||||
|
||||
async function showDetail(row: OnboardingTask) {
|
||||
const { data } = await onboardingTaskApi.detail({ id: row.id });
|
||||
const { data }: any = await onboardingTaskApi.detail({ id: row.id });
|
||||
detail.value = data;
|
||||
activeDetailTab.value = "profile";
|
||||
detailVisible.value = true;
|
||||
void loadMaterialPreviews(data?.materials || []);
|
||||
}
|
||||
|
||||
async function openContractVariables(row: OnboardingTask) {
|
||||
@@ -988,10 +1315,29 @@ async function generate(taskContractId: number) {
|
||||
}
|
||||
}
|
||||
|
||||
function download(fileId: number) {
|
||||
window.open(fileDownloadUrl(fileId), "_blank");
|
||||
async function download(fileId: number, fallbackFileName?: string) {
|
||||
try {
|
||||
const response: any = await onboardingTaskApi.downloadFile(fileId);
|
||||
const blob = response?.data;
|
||||
if (!(blob instanceof Blob)) {
|
||||
throw new Error("服务器未返回有效的文件");
|
||||
}
|
||||
|
||||
const objectUrl = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = objectUrl;
|
||||
link.download = resolveDownloadFileName(response, fallbackFileName);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
window.setTimeout(() => URL.revokeObjectURL(objectUrl), 0);
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.msg || error?.message || "文件下载失败");
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeUnmount(clearMaterialPreviews);
|
||||
|
||||
async function reviewMaterial(taskMaterialId: number, reviewStatus: string) {
|
||||
let reviewRemark = "";
|
||||
if (reviewStatus === "need_supplement") {
|
||||
@@ -1257,6 +1603,52 @@ onMounted(loadCompanies);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.material-img-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
overflow: hidden;
|
||||
background: #e2e8f0;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.material-thumb {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: zoom-in;
|
||||
}
|
||||
|
||||
.material-thumb-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
gap: 8px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.material-overlay-status {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
}
|
||||
|
||||
.material-info-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.material-actions-flex,
|
||||
.review-btn-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.border-pass {
|
||||
border-color: #10b981;
|
||||
background: #f0fdf4;
|
||||
@@ -1347,4 +1739,150 @@ onMounted(loadCompanies);
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
/* 发起任务大弹窗样式 */
|
||||
.create-task-dialog-body {
|
||||
max-height: 74vh;
|
||||
overflow-y: auto;
|
||||
padding-right: 6px;
|
||||
}
|
||||
|
||||
.form-card-section {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
padding: 16px 18px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.title-text {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.title-tip {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.var-label-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.contract-source-tag {
|
||||
font-weight: normal;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.required-star {
|
||||
color: #f56c6c;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.var-input-field {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
:deep(.var-input-field.el-date-editor) {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
:deep(.var-input-field.el-input-number) {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
:deep(.var-input-field.el-input-number .el-input__inner) {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* 详情抽屉顶部横幅与档案卡片样式 */
|
||||
.drawer-status-banner {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
padding: 16px 20px;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.banner-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.banner-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.task-no-large {
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.submit-count-badge {
|
||||
font-size: 12px;
|
||||
color: #2563eb;
|
||||
background: #eff6ff;
|
||||
border: 1px solid #bfdbfe;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.banner-meta-row {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.banner-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
align-items: flex-end;
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.card-title-text {
|
||||
font-size: 14px;
|
||||
color: #1e293b;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mr-1 {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.code-font {
|
||||
font-family: Consolas, Monaco, monospace;
|
||||
background: #eff6ff;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
color: #2563eb;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user