提示优化

This commit is contained in:
fuenmao
2026-09-02 15:55:12 +08:00
parent 058727982b
commit 7e74fb3b4b
8 changed files with 262 additions and 42 deletions
+138 -13
View File
@@ -34,6 +34,17 @@
</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'"
@@ -47,7 +58,7 @@
<!-- 分步向导模式 -->
<div class="wizard-card">
<div v-if="!canEdit" class="readonly-banner">
<div v-if="!canEdit && !isLinkExpired" class="readonly-banner">
<el-alert
title="当前为查阅模式(入职资料已提交审核)"
type="info"
@@ -290,7 +301,12 @@
</el-button>
</div>
<div v-if="!educationHistory.length" class="empty-hint">暂未添加学习经历,可点击右上角添加</div>
<div v-for="(item, idx) in educationHistory" :key="idx" class="dynamic-row-box">
<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>
@@ -332,7 +348,12 @@
</el-button>
</div>
<div v-if="!workHistory.length" class="empty-hint">暂未添加工作经历(应届生可留空)</div>
<div v-for="(item, idx) in workHistory" :key="idx" class="dynamic-row-box">
<div
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>
@@ -379,7 +400,12 @@
</el-button>
</div>
<div v-if="!familyMembers.length" class="empty-hint">暂未添加家庭成员信息</div>
<div v-for="(item, idx) in familyMembers" :key="idx" class="dynamic-row-box">
<div
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>
@@ -440,7 +466,13 @@
class="mb-3"
/>
<el-row :gutter="16">
<el-col v-for="v in extraVariables" :key="v.variableCode" :xs="24" :sm="12">
<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"
@@ -500,6 +532,7 @@
<div v-else class="materials-grid">
<div
v-for="mat in materialRequirements"
:id="`material-requirement-${mat.materialTypeId}`"
:key="mat.materialTypeId"
class="material-item-card"
>
@@ -622,6 +655,7 @@
<div v-else class="contracts-list">
<div
v-for="c in contracts"
:id="`contract-confirm-${c.id}`"
:key="c.id"
class="contract-sign-card"
>
@@ -708,7 +742,7 @@
</template>
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, reactive, ref } from "vue";
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref } from "vue";
import { useRoute } from "vue-router";
import { ElMessage, ElMessageBox, FormInstance, FormRules } from "element-plus";
import {
@@ -735,6 +769,8 @@ const originalViewportContent = viewportMeta?.content;
const loading = ref(false);
const submitting = ref(false);
const error = ref("");
const currentTimestamp = ref(Date.now());
let linkExpiryTimer: number | undefined;
const currentStep = ref(0);
const uploadingMaterials = reactive<Record<string, boolean>>({});
type ContractPrepareStatus = "idle" | "preparing" | "ready" | "error";
@@ -883,8 +919,20 @@ const regRules: FormRules = {
emergencyPhone: [{ validator: validateOptionalContactPhone, trigger: "blur" }]
};
/** 根据当前 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(() => {
return task.value?.taskStatus === "pending_fill" || task.value?.taskStatus === "rejected";
const editableStatus = task.value?.taskStatus === "pending_fill"
|| task.value?.taskStatus === "rejected";
return editableStatus && !isLinkExpired.value;
});
/** 当前正在校验变量并生成文件的合同。 */
@@ -995,10 +1043,36 @@ function isIsoDate(value: string) {
&& 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 variable of extraVariables.value) {
for (const [index, variable] of extraVariables.value.entries()) {
const variableCode = variable.variableCode;
const variableName = variable.variableName || variableCode;
const value = String(customVariables[variableCode] ?? "").trim();
@@ -1019,6 +1093,7 @@ function validateExtraVariables() {
if (!firstMessage && extraVariableErrors[variableCode]) {
firstMessage = extraVariableErrors[variableCode];
firstTargetId = `extra-variable-${index}`;
}
}
@@ -1027,6 +1102,7 @@ function validateExtraVariables() {
}
ElMessage.warning(firstMessage);
locateValidationTarget(firstTargetId);
return false;
}
@@ -1042,6 +1118,7 @@ function validateRegistrationLists() {
const missing = requiredFields.find(([field]) => !String(row[field] || "").trim());
if (missing) {
ElMessage.warning(`第 ${index + 1} 条教育经历请填写${missing[1]},或删除整条经历`);
locateValidationTarget(`education-history-${index}`);
return false;
}
}
@@ -1057,6 +1134,7 @@ function validateRegistrationLists() {
const missing = requiredFields.find(([field]) => !String(row[field] || "").trim());
if (missing) {
ElMessage.warning(`第 ${index + 1} 条工作经历请填写${missing[1]},或删除整条经历`);
locateValidationTarget(`work-history-${index}`);
return false;
}
}
@@ -1072,16 +1150,19 @@ function validateRegistrationLists() {
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;
}
}
@@ -1244,9 +1325,36 @@ async function saveCurrentEmployeeInfo() {
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 (currentStep.value === 0) {
await regFormRef.value?.validate();
if (!await validateRegistrationForm()) {
return;
}
if (!validateRegistrationLists()) {
return;
}
@@ -1264,7 +1372,9 @@ async function nextStep() {
} else if (currentStep.value === 2) {
const missing = materialRequirements.value.filter(m => m.required && !getUploadedMaterials(m.materialTypeId).length);
if (missing.length) {
return ElMessage.warning(`请先上传必传材料:${missing.map((m: any) => m.materialName).join("、")}`);
ElMessage.warning(`请先上传必传材料:${missing.map((m: any) => m.materialName).join("、")}`);
locateValidationTarget(`material-requirement-${missing[0].materialTypeId}`);
return;
}
}
currentStep.value++;
@@ -1475,17 +1585,23 @@ function previewAndDownloadContract(c: any) {
async function submitAll() {
const unprepared = contracts.value.filter(c => !isContractPrepared(c.id));
if (unprepared.length) {
return ElMessage.warning(`请先等待签署材料准备完成:${unprepared.map(c => c.contractName).join("、")}`);
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) {
return ElMessage.warning(`请先下载并查看签署材料:${unviewed.map(c => c.contractName).join("、")}`);
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) {
return ElMessage.warning(`请先勾选同意全部签署材料(未勾选:${unconfirmed.map(c => c.contractName).join("、")})`);
ElMessage.warning(`请先勾选同意全部签署材料(未勾选:${unconfirmed.map(c => c.contractName).join("、")})`);
locateValidationTarget(`contract-confirm-${unconfirmed[0].id}`);
return;
}
const confirmed = await isMessageBoxConfirmed(
@@ -1525,10 +1641,18 @@ onMounted(() => {
"viewport-fit=cover"
].join(", ");
}
currentTimestamp.value = Date.now();
linkExpiryTimer = window.setInterval(() => {
currentTimestamp.value = Date.now();
}, 1000);
load();
});
onBeforeUnmount(() => {
if (linkExpiryTimer !== undefined) {
window.clearInterval(linkExpiryTimer);
}
if (viewportMeta && originalViewportContent) {
viewportMeta.content = originalViewportContent;
}
@@ -1603,6 +1727,7 @@ onBeforeUnmount(() => {
color: #64748b;
}
.link-expired-alert,
.reject-reason-alert {
margin-bottom: 16px;
}