11
This commit is contained in:
@@ -21,6 +21,14 @@ export interface OnboardingTask {
|
||||
createTime: string;
|
||||
}
|
||||
|
||||
/** 员工端完成合同准备后返回的当前文件版本。 */
|
||||
export interface OnboardingPreparedContract {
|
||||
/** 当前生成合同对应的系统文件 Id,同时作为下载缓存版本。 */
|
||||
fileId: number;
|
||||
/** 当前合同文件生成时间。 */
|
||||
generatedAt?: string;
|
||||
}
|
||||
|
||||
export interface OnboardingCompanyOption {
|
||||
id: number;
|
||||
companyName: string;
|
||||
@@ -556,8 +564,12 @@ export const onboardingPublicApi = {
|
||||
submit(params: { token: string }) {
|
||||
return publicHttp.post("submit", params);
|
||||
},
|
||||
contractSourceUrl(params: { token: string; taskContractId: number }) {
|
||||
return `/api/open/onboarding/contract-source?token=${encodeURIComponent(params.token)}&taskContractId=${params.taskContractId}`;
|
||||
prepareContract(params: { token: string; taskContractId: number }) {
|
||||
return publicHttp.post<OnboardingPreparedContract>("prepare-contract", params);
|
||||
},
|
||||
contractSourceUrl(params: { token: string; taskContractId: number; fileVersion?: number }) {
|
||||
const version = params.fileVersion ? `&version=${params.fileVersion}` : `&_t=${Date.now()}`;
|
||||
return `/api/open/onboarding/contract-source?token=${encodeURIComponent(params.token)}&taskContractId=${params.taskContractId}${version}`;
|
||||
},
|
||||
materialSourceUrl(params: { token: string; taskMaterialId: number; fileId?: number }) {
|
||||
const version = params.fileId ? `&v=${params.fileId}` : "";
|
||||
|
||||
@@ -572,6 +572,16 @@
|
||||
<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">
|
||||
@@ -593,20 +603,32 @@
|
||||
</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="primary"
|
||||
:type="isContractPrepareFailed(c.id) ? 'danger' : 'primary'"
|
||||
plain
|
||||
icon="Download"
|
||||
@click="previewAndDownloadContract(c)"
|
||||
:icon="isContractPrepareFailed(c.id) ? 'Refresh' : 'Download'"
|
||||
:loading="isContractPreparing(c.id)"
|
||||
:disabled="!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)"
|
||||
:label="`我已完整阅读并同意《${c.contractName}》全部条款`"
|
||||
/>
|
||||
<span v-else class="confirmed-text">✓ 候选人已确认同意</span>
|
||||
@@ -662,6 +684,9 @@ const submitting = ref(false);
|
||||
const error = ref("");
|
||||
const currentStep = ref(0);
|
||||
const uploadingMaterials = reactive<Record<string, boolean>>({});
|
||||
type ContractPrepareStatus = "idle" | "preparing" | "ready" | "error";
|
||||
const contractPrepareStatus = reactive<Record<string, ContractPrepareStatus>>({});
|
||||
const contractPrepareErrors = reactive<Record<string, string>>({});
|
||||
|
||||
const task = ref<OnboardingTask | null>(null);
|
||||
const contracts = ref<any[]>([]);
|
||||
@@ -727,6 +752,22 @@ const canEdit = computed(() => {
|
||||
return task.value?.taskStatus === "pending_fill" || task.value?.taskStatus === "rejected";
|
||||
});
|
||||
|
||||
/** 当前正在校验变量并生成文件的合同。 */
|
||||
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",
|
||||
@@ -858,6 +899,11 @@ async function load(silent = false) {
|
||||
...c,
|
||||
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 || [];
|
||||
@@ -919,6 +965,9 @@ async function nextStep() {
|
||||
}
|
||||
currentStep.value++;
|
||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||
if (currentStep.value === 3) {
|
||||
await prepareContracts();
|
||||
}
|
||||
}
|
||||
|
||||
function getMaterialUploadUrl(materialTypeId: number, taskMaterialId?: number) {
|
||||
@@ -1011,10 +1060,92 @@ async function isMessageBoxConfirmed(confirmPromise: Promise<unknown>) {
|
||||
}
|
||||
}
|
||||
|
||||
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 isContractPrepared(taskContractId: number) {
|
||||
return getContractPrepareStatus(taskContractId) === "ready";
|
||||
}
|
||||
|
||||
function isContractPrepareFailed(taskContractId: number) {
|
||||
return getContractPrepareStatus(taskContractId) === "error";
|
||||
}
|
||||
|
||||
function getContractActionText(taskContractId: number) {
|
||||
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 response = await onboardingPublicApi.prepareContract({
|
||||
token: token.value,
|
||||
taskContractId: contract.id
|
||||
});
|
||||
// 使用后端返回的新文件 Id 作为下载版本,避免手机浏览器复用旧合同缓存。
|
||||
contract.generatedDocxFileId = response.data.fileId;
|
||||
contract.generatedAt = response.data.generatedAt;
|
||||
contractPrepareStatus[key] = "ready";
|
||||
return true;
|
||||
} catch (err: any) {
|
||||
const message = err?.message || "合同准备失败,请稍后重试";
|
||||
contractPrepareStatus[key] = "error";
|
||||
contractPrepareErrors[key] = message;
|
||||
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)) {
|
||||
await prepareContract(contract);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isContractPrepared(contract.id)) {
|
||||
previewAndDownloadContract(contract);
|
||||
}
|
||||
}
|
||||
|
||||
function previewAndDownloadContract(c: any) {
|
||||
const url = onboardingPublicApi.contractSourceUrl({
|
||||
token: token.value,
|
||||
taskContractId: c.id
|
||||
taskContractId: c.id,
|
||||
fileVersion: c.generatedDocxFileId
|
||||
});
|
||||
|
||||
// 下载必须直接发生在用户点击事件中,避免手机浏览器拦截异步打开或二次跳转。
|
||||
@@ -1025,6 +1156,11 @@ 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("、")}`);
|
||||
}
|
||||
|
||||
const unconfirmed = contracts.value.filter(c => !c.isAgreed);
|
||||
if (unconfirmed.length) {
|
||||
return ElMessage.warning(`请先勾选同意全部合同(未勾选:${unconfirmed.map(c => c.contractName).join("、")})`);
|
||||
@@ -1413,6 +1549,14 @@ onBeforeUnmount(() => {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.contract-prepare-error {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.contract-preparing-alert {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.contract-card-bottom {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
Reference in New Issue
Block a user