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