11
This commit is contained in:
@@ -16,7 +16,7 @@
|
||||
<div class="main-container">
|
||||
<el-result v-if="error" icon="error" title="访问受限" :sub-title="error">
|
||||
<template #extra>
|
||||
<el-button type="primary" @click="load">重新加载</el-button>
|
||||
<el-button type="primary" @click="load()">重新加载</el-button>
|
||||
</template>
|
||||
</el-result>
|
||||
|
||||
@@ -34,6 +34,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 审核退回后向候选人明确展示需要补充或修改的内容。 -->
|
||||
<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" class="readonly-banner">
|
||||
@@ -463,38 +474,92 @@
|
||||
<el-tag v-if="mat.required" size="small" type="danger" effect="plain">必传</el-tag>
|
||||
<el-tag v-else size="small" type="info" effect="plain">选传</el-tag>
|
||||
</div>
|
||||
<span class="mat-limit">最多 {{ mat.maxFileCount }} 份 · 格式:{{ mat.allowedExtensions }}</span>
|
||||
<span class="mat-limit">{{ getMaterialLimitText(mat) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="uploaded-files-list">
|
||||
<div
|
||||
v-for="f in getUploadedMaterials(mat.materialTypeId)"
|
||||
:key="f.id"
|
||||
:key="`${f.id}-${f.fileId}`"
|
||||
class="file-badge"
|
||||
>
|
||||
<el-icon color="#2563eb"><Document /></el-icon>
|
||||
<span class="file-name">{{ f.fileName || f.materialName }}</span>
|
||||
<el-button
|
||||
v-if="canEdit"
|
||||
link
|
||||
type="danger"
|
||||
icon="Close"
|
||||
@click="deleteFile(f.id)"
|
||||
<!-- 图片点击缩略图即可在手机端放大预览,非图片继续显示文件图标。 -->
|
||||
<el-image
|
||||
v-if="isImageFile(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="#2563eb"><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="getMaterialUploadUrl(mat.materialTypeId, f.id)"
|
||||
:accept="getUploadAccept(mat)"
|
||||
:disabled="isMaterialUploading(mat.materialTypeId)"
|
||||
:show-file-list="false"
|
||||
: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 && getUploadedMaterials(mat.materialTypeId).length < mat.maxFileCount" class="upload-action-box">
|
||||
<div v-if="canEdit && getRemainingFileCount(mat) > 0" class="upload-action-box">
|
||||
<el-upload
|
||||
:action="getMaterialUploadUrl(mat.materialTypeId)"
|
||||
:accept="getUploadAccept(mat)"
|
||||
:disabled="isMaterialUploading(mat.materialTypeId)"
|
||||
:show-file-list="false"
|
||||
:on-success="onUploadSuccess"
|
||||
: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">
|
||||
上传文件 / 照片
|
||||
<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>
|
||||
@@ -533,6 +598,8 @@
|
||||
type="primary"
|
||||
plain
|
||||
icon="Download"
|
||||
:loading="isContractOpening(c.id)"
|
||||
:disabled="isContractOpening(c.id)"
|
||||
@click="previewAndDownloadContract(c)"
|
||||
>
|
||||
下载 / 查阅合同原文
|
||||
@@ -579,7 +646,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from "vue";
|
||||
import { computed, onBeforeUnmount, onMounted, reactive, ref } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { ElMessage, ElMessageBox, FormInstance, FormRules } from "element-plus";
|
||||
import {
|
||||
@@ -589,11 +656,15 @@ import {
|
||||
|
||||
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 error = ref("");
|
||||
const currentStep = ref(0);
|
||||
const uploadingMaterials = reactive<Record<string, boolean>>({});
|
||||
const openingContracts = reactive<Record<string, boolean>>({});
|
||||
|
||||
const task = ref<OnboardingTask | null>(null);
|
||||
const contracts = ref<any[]>([]);
|
||||
@@ -698,6 +769,53 @@ 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
|
||||
);
|
||||
}
|
||||
|
||||
function getAllowedExtensions(mat: any) {
|
||||
return String(mat.allowedExtensions || "")
|
||||
.toLowerCase()
|
||||
.split(",")
|
||||
.map((item: string) => item.trim().replace(/^\./, ""))
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
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 sizeText = mat.maxFileSizeMb
|
||||
? ` · 单个不超过 ${mat.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";
|
||||
@@ -726,12 +844,14 @@ function formatDateTime(str?: string) {
|
||||
return str ? str.replace("T", " ").substring(0, 16) : "";
|
||||
}
|
||||
|
||||
async function load() {
|
||||
async function load(silent = false) {
|
||||
if (!token.value) {
|
||||
error.value = "缺少入职访问 Token";
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
if (!silent) {
|
||||
loading.value = true;
|
||||
}
|
||||
error.value = "";
|
||||
try {
|
||||
const res = await onboardingPublicApi.detail({ token: token.value });
|
||||
@@ -764,7 +884,9 @@ async function load() {
|
||||
} catch (err: any) {
|
||||
error.value = err.message || "无法加载入职任务";
|
||||
} finally {
|
||||
loading.value = false;
|
||||
if (!silent) {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -802,46 +924,135 @@ async function nextStep() {
|
||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||
}
|
||||
|
||||
function getMaterialUploadUrl(materialTypeId: number) {
|
||||
return `/api/open/onboarding/upload-material?token=${encodeURIComponent(token.value)}&materialTypeId=${materialTypeId}`;
|
||||
function getMaterialUploadUrl(materialTypeId: number, taskMaterialId?: number) {
|
||||
const query = new URLSearchParams({
|
||||
token: token.value,
|
||||
materialTypeId: String(materialTypeId)
|
||||
});
|
||||
if (taskMaterialId) {
|
||||
query.set("taskMaterialId", String(taskMaterialId));
|
||||
}
|
||||
return `/api/open/onboarding/upload-material?${query.toString()}`;
|
||||
}
|
||||
|
||||
function getMaterialSourceUrl(file: any) {
|
||||
return onboardingPublicApi.materialSourceUrl({
|
||||
token: token.value,
|
||||
taskMaterialId: file.id,
|
||||
fileId: file.fileId
|
||||
});
|
||||
}
|
||||
|
||||
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 = (mat.allowedExtensions || "").toLowerCase().split(",")
|
||||
.map((s: string) => s.trim())
|
||||
.filter(Boolean);
|
||||
const allowed = getAllowedExtensions(mat);
|
||||
if (allowed.length && !allowed.includes(ext)) {
|
||||
ElMessage.error(`仅支持格式:${mat.allowedExtensions}`);
|
||||
return false;
|
||||
}
|
||||
if (mat.maxFileSizeMb && file.size > Number(mat.maxFileSizeMb) * 1024 * 1024) {
|
||||
ElMessage.error(`单个文件不能超过 ${mat.maxFileSizeMb} MB`);
|
||||
return false;
|
||||
}
|
||||
setMaterialUploading(mat.materialTypeId, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
async function onUploadSuccess(res: any) {
|
||||
async function onUploadSuccess(res: any, materialTypeId: number) {
|
||||
if (res.code !== 200 && res.code !== 0) {
|
||||
setMaterialUploading(materialTypeId, false);
|
||||
return ElMessage.error(res.msg || "上传失败");
|
||||
}
|
||||
ElMessage.success("材料上传成功");
|
||||
await load();
|
||||
try {
|
||||
await load(true);
|
||||
ElMessage.success("材料上传成功");
|
||||
} finally {
|
||||
setMaterialUploading(materialTypeId, false);
|
||||
}
|
||||
}
|
||||
|
||||
function onUploadError(error: Error, materialTypeId: number) {
|
||||
setMaterialUploading(materialTypeId, false);
|
||||
ElMessage.error(error.message || "材料上传失败,请稍后重试");
|
||||
}
|
||||
|
||||
async function deleteFile(id: number) {
|
||||
await ElMessageBox.confirm("确认删除已上传材料?", "删除确认", { type: "warning" });
|
||||
const confirmed = await isMessageBoxConfirmed(
|
||||
ElMessageBox.confirm("确认删除已上传材料?", "删除确认", { type: "warning" })
|
||||
);
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
await onboardingPublicApi.deleteMaterial({ token: token.value, taskMaterialId: id });
|
||||
ElMessage.success("材料已删除");
|
||||
await load();
|
||||
}
|
||||
|
||||
async function previewAndDownloadContract(c: any) {
|
||||
if (canEdit.value) {
|
||||
await saveCurrentEmployeeInfo();
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
const url = onboardingPublicApi.contractSourceUrl({
|
||||
token: token.value,
|
||||
taskContractId: c.id
|
||||
});
|
||||
window.open(url, "_blank");
|
||||
}
|
||||
|
||||
async function previewAndDownloadContract(c: any) {
|
||||
if (isContractOpening(c.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 必须在用户点击的同步阶段创建窗口,否则手机浏览器会拦截异步完成后的 window.open。
|
||||
const previewWindow = window.open("about:blank", "_blank");
|
||||
openingContracts[String(c.id)] = true;
|
||||
try {
|
||||
if (previewWindow) {
|
||||
previewWindow.document.title = "合同生成中";
|
||||
previewWindow.document.body.innerHTML = `
|
||||
<div style="padding:40px 20px;text-align:center;font-family:sans-serif;color:#475569;">
|
||||
正在生成合同文件,请稍候…
|
||||
</div>`;
|
||||
}
|
||||
if (canEdit.value) {
|
||||
await saveCurrentEmployeeInfo();
|
||||
}
|
||||
const url = onboardingPublicApi.contractSourceUrl({
|
||||
token: token.value,
|
||||
taskContractId: c.id
|
||||
});
|
||||
if (previewWindow && !previewWindow.closed) {
|
||||
previewWindow.location.replace(url);
|
||||
} else {
|
||||
// 微信等内置浏览器拦截新窗口时,退回当前页直接发起下载。
|
||||
window.location.assign(url);
|
||||
}
|
||||
} catch (err: any) {
|
||||
previewWindow?.close();
|
||||
ElMessage.error(err?.message || "合同生成失败,请稍后重试");
|
||||
} finally {
|
||||
openingContracts[String(c.id)] = false;
|
||||
}
|
||||
}
|
||||
|
||||
function isContractOpening(taskContractId: number) {
|
||||
return Boolean(openingContracts[String(taskContractId)]);
|
||||
}
|
||||
|
||||
async function submitAll() {
|
||||
@@ -850,11 +1061,16 @@ async function submitAll() {
|
||||
return ElMessage.warning(`请先勾选同意全部合同(未勾选:${unconfirmed.map(c => c.contractName).join("、")})`);
|
||||
}
|
||||
|
||||
await ElMessageBox.confirm("确认提交全部入职资料与合同确认?提交后将进入人事复核建档流程。", "最终提交确认", {
|
||||
type: "success",
|
||||
confirmButtonText: "确认提交",
|
||||
cancelButtonText: "再检查一下"
|
||||
});
|
||||
const confirmed = await isMessageBoxConfirmed(
|
||||
ElMessageBox.confirm("确认提交全部入职资料与合同确认?提交后将进入人事复核建档流程。", "最终提交确认", {
|
||||
type: "success",
|
||||
confirmButtonText: "确认提交",
|
||||
cancelButtonText: "再检查一下"
|
||||
})
|
||||
);
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
submitting.value = true;
|
||||
try {
|
||||
@@ -870,7 +1086,26 @@ async function submitAll() {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(load);
|
||||
/** 入职填写页使用固定移动端比例,离开页面后恢复系统原有 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(", ");
|
||||
}
|
||||
load();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (viewportMeta && originalViewportContent) {
|
||||
viewportMeta.content = originalViewportContent;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -941,6 +1176,16 @@ onMounted(load);
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.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 {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
@@ -1111,13 +1356,68 @@ onMounted(load);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
.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: #64748b;
|
||||
}
|
||||
|
||||
.preview-tip {
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.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: #eff6ff;
|
||||
color: #475569;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.contract-sign-card {
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
@@ -1262,6 +1562,12 @@ onMounted(load);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
/* iOS 会自动放大小于 16px 的表单控件,统一字号保持页面比例稳定。 */
|
||||
.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;
|
||||
}
|
||||
@@ -1285,6 +1591,14 @@ onMounted(load);
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
.file-badge {
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.file-actions {
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.contract-card-bottom {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
|
||||
Reference in New Issue
Block a user