11
This commit is contained in:
@@ -21,6 +21,14 @@ export interface OnboardingTask {
|
|||||||
createTime: string;
|
createTime: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 员工端完成合同准备后返回的当前文件版本。 */
|
||||||
|
export interface OnboardingPreparedContract {
|
||||||
|
/** 当前生成合同对应的系统文件 Id,同时作为下载缓存版本。 */
|
||||||
|
fileId: number;
|
||||||
|
/** 当前合同文件生成时间。 */
|
||||||
|
generatedAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface OnboardingCompanyOption {
|
export interface OnboardingCompanyOption {
|
||||||
id: number;
|
id: number;
|
||||||
companyName: string;
|
companyName: string;
|
||||||
@@ -556,8 +564,12 @@ export const onboardingPublicApi = {
|
|||||||
submit(params: { token: string }) {
|
submit(params: { token: string }) {
|
||||||
return publicHttp.post("submit", params);
|
return publicHttp.post("submit", params);
|
||||||
},
|
},
|
||||||
contractSourceUrl(params: { token: string; taskContractId: number }) {
|
prepareContract(params: { token: string; taskContractId: number }) {
|
||||||
return `/api/open/onboarding/contract-source?token=${encodeURIComponent(params.token)}&taskContractId=${params.taskContractId}`;
|
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 }) {
|
materialSourceUrl(params: { token: string; taskMaterialId: number; fileId?: number }) {
|
||||||
const version = params.fileId ? `&v=${params.fileId}` : "";
|
const version = params.fileId ? `&v=${params.fileId}` : "";
|
||||||
|
|||||||
@@ -572,6 +572,16 @@
|
|||||||
<p class="section-desc">系统已为您代入真实登记信息生成专属合同,请认真查阅条款并勾选确认</p>
|
<p class="section-desc">系统已为您代入真实登记信息生成专属合同,请认真查阅条款并勾选确认</p>
|
||||||
</div>
|
</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="暂无待签署入职合同" />
|
<el-empty v-if="!contracts.length" description="暂无待签署入职合同" />
|
||||||
|
|
||||||
<div v-else class="contracts-list">
|
<div v-else class="contracts-list">
|
||||||
@@ -593,20 +603,32 @@
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="contract-card-bottom">
|
||||||
<el-button
|
<el-button
|
||||||
type="primary"
|
:type="isContractPrepareFailed(c.id) ? 'danger' : 'primary'"
|
||||||
plain
|
plain
|
||||||
icon="Download"
|
:icon="isContractPrepareFailed(c.id) ? 'Refresh' : 'Download'"
|
||||||
@click="previewAndDownloadContract(c)"
|
:loading="isContractPreparing(c.id)"
|
||||||
|
:disabled="!isContractPrepared(c.id) && !isContractPrepareFailed(c.id)"
|
||||||
|
@click="handleContractAction(c)"
|
||||||
>
|
>
|
||||||
下载并查看合同
|
{{ getContractActionText(c.id) }}
|
||||||
</el-button>
|
</el-button>
|
||||||
|
|
||||||
<el-checkbox
|
<el-checkbox
|
||||||
v-if="canEdit"
|
v-if="canEdit"
|
||||||
v-model="c.isAgreed"
|
v-model="c.isAgreed"
|
||||||
class="agree-checkbox"
|
class="agree-checkbox"
|
||||||
|
:disabled="!isContractPrepared(c.id)"
|
||||||
:label="`我已完整阅读并同意《${c.contractName}》全部条款`"
|
:label="`我已完整阅读并同意《${c.contractName}》全部条款`"
|
||||||
/>
|
/>
|
||||||
<span v-else class="confirmed-text">✓ 候选人已确认同意</span>
|
<span v-else class="confirmed-text">✓ 候选人已确认同意</span>
|
||||||
@@ -662,6 +684,9 @@ const submitting = ref(false);
|
|||||||
const error = ref("");
|
const error = ref("");
|
||||||
const currentStep = ref(0);
|
const currentStep = ref(0);
|
||||||
const uploadingMaterials = reactive<Record<string, boolean>>({});
|
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 task = ref<OnboardingTask | null>(null);
|
||||||
const contracts = ref<any[]>([]);
|
const contracts = ref<any[]>([]);
|
||||||
@@ -727,6 +752,22 @@ const canEdit = computed(() => {
|
|||||||
return task.value?.taskStatus === "pending_fill" || task.value?.taskStatus === "rejected";
|
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. 变量中,登记表没有的字段)
|
// 计算差集增量变量 (合同需要的 employee. 变量中,登记表没有的字段)
|
||||||
const registrationStandardFields = [
|
const registrationStandardFields = [
|
||||||
"employeename", "name", "username", "gender", "nation", "birthday", "registeredresidence",
|
"employeename", "name", "username", "gender", "nation", "birthday", "registeredresidence",
|
||||||
@@ -858,6 +899,11 @@ async function load(silent = false) {
|
|||||||
...c,
|
...c,
|
||||||
isAgreed: c.contractStatus === "confirmed"
|
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 || [];
|
materialRequirements.value = data.materialRequirements || [];
|
||||||
uploadedMaterials.value = data.materials || [];
|
uploadedMaterials.value = data.materials || [];
|
||||||
contractVariables.value = data.contractVariables || [];
|
contractVariables.value = data.contractVariables || [];
|
||||||
@@ -919,6 +965,9 @@ async function nextStep() {
|
|||||||
}
|
}
|
||||||
currentStep.value++;
|
currentStep.value++;
|
||||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||||
|
if (currentStep.value === 3) {
|
||||||
|
await prepareContracts();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMaterialUploadUrl(materialTypeId: number, taskMaterialId?: number) {
|
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) {
|
function previewAndDownloadContract(c: any) {
|
||||||
const url = onboardingPublicApi.contractSourceUrl({
|
const url = onboardingPublicApi.contractSourceUrl({
|
||||||
token: token.value,
|
token: token.value,
|
||||||
taskContractId: c.id
|
taskContractId: c.id,
|
||||||
|
fileVersion: c.generatedDocxFileId
|
||||||
});
|
});
|
||||||
|
|
||||||
// 下载必须直接发生在用户点击事件中,避免手机浏览器拦截异步打开或二次跳转。
|
// 下载必须直接发生在用户点击事件中,避免手机浏览器拦截异步打开或二次跳转。
|
||||||
@@ -1025,6 +1156,11 @@ function previewAndDownloadContract(c: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function submitAll() {
|
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);
|
const unconfirmed = contracts.value.filter(c => !c.isAgreed);
|
||||||
if (unconfirmed.length) {
|
if (unconfirmed.length) {
|
||||||
return ElMessage.warning(`请先勾选同意全部合同(未勾选:${unconfirmed.map(c => c.contractName).join("、")})`);
|
return ElMessage.warning(`请先勾选同意全部合同(未勾选:${unconfirmed.map(c => c.contractName).join("、")})`);
|
||||||
@@ -1413,6 +1549,14 @@ onBeforeUnmount(() => {
|
|||||||
margin-bottom: 14px;
|
margin-bottom: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.contract-prepare-error {
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contract-preparing-alert {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.contract-card-bottom {
|
.contract-card-bottom {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
Reference in New Issue
Block a user