Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0db1848b10 | ||
|
|
fadc3328e7 | ||
|
|
68704a091b | ||
|
|
709e02a04f | ||
|
|
802d9cde89 | ||
|
|
7e74fb3b4b | ||
|
|
058727982b |
Vendored
+2
@@ -29,6 +29,8 @@ declare module 'vue' {
|
|||||||
ElCascader: typeof import('element-plus/es')['ElCascader']
|
ElCascader: typeof import('element-plus/es')['ElCascader']
|
||||||
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||||
ElCol: typeof import('element-plus/es')['ElCol']
|
ElCol: typeof import('element-plus/es')['ElCol']
|
||||||
|
ElCollapse: typeof import('element-plus/es')['ElCollapse']
|
||||||
|
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
|
||||||
ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
|
ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
|
||||||
ElContainer: typeof import('element-plus/es')['ElContainer']
|
ElContainer: typeof import('element-plus/es')['ElContainer']
|
||||||
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
||||||
|
|||||||
@@ -3,8 +3,60 @@ import type { ReqId, ResPage } from "@/api/interface";
|
|||||||
|
|
||||||
const taskHttp = moduleRequest("/biz/onboarding/task/");
|
const taskHttp = moduleRequest("/biz/onboarding/task/");
|
||||||
const auditHttp = moduleRequest("/biz/onboarding/audit/");
|
const auditHttp = moduleRequest("/biz/onboarding/audit/");
|
||||||
|
const contractDocumentHttp = moduleRequest("/biz/onboarding/contract-document/");
|
||||||
|
const electronicSignHttp = moduleRequest("/biz/onboarding/electronic-sign/");
|
||||||
const fileHttp = moduleRequest("/sys/dev/file/");
|
const fileHttp = moduleRequest("/sys/dev/file/");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载创建任务前的合同预览,并将统一响应中的 JSON 错误转换成可读异常。
|
||||||
|
*/
|
||||||
|
async function requestContractPreview(
|
||||||
|
action: string,
|
||||||
|
params: Record<string, unknown>,
|
||||||
|
fallbackMessage: string
|
||||||
|
) {
|
||||||
|
let response: any;
|
||||||
|
try {
|
||||||
|
response = await contractDocumentHttp.post<any>(
|
||||||
|
action,
|
||||||
|
params,
|
||||||
|
{ responseType: "blob", loading: false, cancel: false }
|
||||||
|
);
|
||||||
|
} catch (error: any) {
|
||||||
|
const errorBlob = error?.response?.data;
|
||||||
|
if (errorBlob instanceof Blob) {
|
||||||
|
try {
|
||||||
|
const result = JSON.parse(await errorBlob.text());
|
||||||
|
throw new Error(result?.msg || fallbackMessage);
|
||||||
|
} catch (parseError) {
|
||||||
|
if (!(parseError instanceof SyntaxError)) throw parseError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = response?.data;
|
||||||
|
const contentType = String(
|
||||||
|
response?.headers?.["content-type"] || blob?.type || ""
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
blob instanceof Blob &&
|
||||||
|
contentType.toLowerCase().includes("application/json")
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const result = JSON.parse(await blob.text());
|
||||||
|
throw new Error(result?.msg || fallbackMessage);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof SyntaxError) {
|
||||||
|
throw new Error(fallbackMessage);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
export interface OnboardingTask {
|
export interface OnboardingTask {
|
||||||
id: number;
|
id: number;
|
||||||
taskNo: string;
|
taskNo: string;
|
||||||
@@ -14,6 +66,7 @@ export interface OnboardingTask {
|
|||||||
departmentName?: string;
|
departmentName?: string;
|
||||||
positionId: number;
|
positionId: number;
|
||||||
positionName?: string;
|
positionName?: string;
|
||||||
|
employmentType?: "full_time" | "part_time";
|
||||||
onboardingTemplateId?: number;
|
onboardingTemplateId?: number;
|
||||||
onboardingTemplateName?: string;
|
onboardingTemplateName?: string;
|
||||||
taskStatus: string;
|
taskStatus: string;
|
||||||
@@ -21,6 +74,33 @@ export interface OnboardingTask {
|
|||||||
linkExpireAt?: string;
|
linkExpireAt?: string;
|
||||||
submitCount: number;
|
submitCount: number;
|
||||||
createTime: string;
|
createTime: string;
|
||||||
|
remark?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 当前查询条件下、不受状态筛选影响的任务统计。 */
|
||||||
|
export interface OnboardingTaskStatistics {
|
||||||
|
pendingFill: number;
|
||||||
|
reviewing: number;
|
||||||
|
rejected: number;
|
||||||
|
approved: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 带状态统计的入职任务分页结果。 */
|
||||||
|
export interface OnboardingTaskPage extends ResPage<OnboardingTask> {
|
||||||
|
statistics: OnboardingTaskStatistics;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批量复制任务返回的单条新任务链接。 */
|
||||||
|
export interface OnboardingTaskCreatedResult {
|
||||||
|
taskId: number;
|
||||||
|
taskNo: string;
|
||||||
|
linkToken: string;
|
||||||
|
expiresAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批量复制任务结果。 */
|
||||||
|
export interface OnboardingTaskCopyResult {
|
||||||
|
tasks: OnboardingTaskCreatedResult[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 员工端完成合同准备后返回的当前文件版本。 */
|
/** 员工端完成合同准备后返回的当前文件版本。 */
|
||||||
@@ -31,6 +111,69 @@ export interface OnboardingPreparedContract {
|
|||||||
generatedAt?: string;
|
generatedAt?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 一次电子签流程中的单份合同文件。 */
|
||||||
|
export interface OnboardingElectronicSignDocument {
|
||||||
|
id: number;
|
||||||
|
taskContractId: number;
|
||||||
|
providerDocumentId?: string;
|
||||||
|
documentName: string;
|
||||||
|
documentSort: number;
|
||||||
|
originalDocxFileId?: number;
|
||||||
|
signedPdfFileId?: number;
|
||||||
|
employeeSignedPreviewed: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 员工已签、企业未盖章文件的契约锁只读预览。 */
|
||||||
|
export interface OnboardingElectronicSignPreview {
|
||||||
|
signDocumentId: number;
|
||||||
|
previewUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 管理端查看的入职电子签流程详情。 */
|
||||||
|
export interface OnboardingElectronicSignDetail {
|
||||||
|
taskId: number;
|
||||||
|
taskNo: string;
|
||||||
|
canInitiate: boolean;
|
||||||
|
cannotInitiateReason?: string;
|
||||||
|
canReinitiate: boolean;
|
||||||
|
cannotReinitiateReason?: string;
|
||||||
|
hasProcess: boolean;
|
||||||
|
processId?: number;
|
||||||
|
providerContractId?: string;
|
||||||
|
bizId?: string;
|
||||||
|
subject?: string;
|
||||||
|
signStatus?: string;
|
||||||
|
providerStatus?: string;
|
||||||
|
employeeName?: string;
|
||||||
|
employeeMobile?: string;
|
||||||
|
signUrl?: string;
|
||||||
|
signUrlExpireAt?: string;
|
||||||
|
signUrlValid: boolean;
|
||||||
|
startedAt?: string;
|
||||||
|
sentAt?: string;
|
||||||
|
completedAt?: string;
|
||||||
|
archivedAt?: string;
|
||||||
|
archiveFileId?: number;
|
||||||
|
lastError?: string;
|
||||||
|
documents: OnboardingElectronicSignDocument[];
|
||||||
|
historyProcesses: OnboardingElectronicSignHistory[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 已被新流程替代的电子签历史记录。 */
|
||||||
|
export interface OnboardingElectronicSignHistory {
|
||||||
|
processId: number;
|
||||||
|
bizId: string;
|
||||||
|
providerContractId?: string;
|
||||||
|
signStatus?: string;
|
||||||
|
providerStatus?: string;
|
||||||
|
employeeName?: string;
|
||||||
|
employeeMobile?: string;
|
||||||
|
startedAt?: string;
|
||||||
|
reissuedAt?: string;
|
||||||
|
reissueReason?: string;
|
||||||
|
lastError?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface OnboardingCompanyOption {
|
export interface OnboardingCompanyOption {
|
||||||
id: number;
|
id: number;
|
||||||
companyName: string;
|
companyName: string;
|
||||||
@@ -53,6 +196,8 @@ export interface OnboardingCompany extends OnboardingCompanyOption {
|
|||||||
legalRepresentative?: string;
|
legalRepresentative?: string;
|
||||||
registeredAddress?: string;
|
registeredAddress?: string;
|
||||||
contactPhone?: string;
|
contactPhone?: string;
|
||||||
|
qiyuesuoCategoryId?: string;
|
||||||
|
qiyuesuoSealId?: string;
|
||||||
remark?: string;
|
remark?: string;
|
||||||
status: string;
|
status: string;
|
||||||
}
|
}
|
||||||
@@ -150,6 +295,12 @@ export interface OnboardingEmployee {
|
|||||||
departmentName?: string;
|
departmentName?: string;
|
||||||
positionId: number;
|
positionId: number;
|
||||||
positionName?: string;
|
positionName?: string;
|
||||||
|
employmentType?: "full_time" | "part_time";
|
||||||
|
regularizationStatus?: "probation" | "regular" | "not_applicable";
|
||||||
|
socialInsuranceStatus?: "unknown" | "not_purchased" | "purchased" | "stopped" | "not_applicable";
|
||||||
|
regularizationDate?: string;
|
||||||
|
socialInsuranceCompany?: string;
|
||||||
|
contractNature?: string;
|
||||||
phone: string;
|
phone: string;
|
||||||
idCardNumber: string;
|
idCardNumber: string;
|
||||||
entryDate?: string;
|
entryDate?: string;
|
||||||
@@ -218,7 +369,7 @@ export interface OnboardingTemplate {
|
|||||||
}
|
}
|
||||||
export const onboardingTaskApi = {
|
export const onboardingTaskApi = {
|
||||||
page(params: Record<string, unknown>) {
|
page(params: Record<string, unknown>) {
|
||||||
return taskHttp.get<ResPage<OnboardingTask>>("page", params);
|
return taskHttp.get<OnboardingTaskPage>("page", params);
|
||||||
},
|
},
|
||||||
|
|
||||||
detail(params: ReqId) {
|
detail(params: ReqId) {
|
||||||
@@ -254,10 +405,36 @@ export const onboardingTaskApi = {
|
|||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** 创建任务前生成并下载指定的一份临时 DOCX 合同预览。 */
|
||||||
|
async previewContractBeforeCreate(params: Record<string, unknown>) {
|
||||||
|
return requestContractPreview(
|
||||||
|
"preview-before-create",
|
||||||
|
params,
|
||||||
|
"合同预览生成失败"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 创建任务前生成全部临时 DOCX 合同预览,并以 ZIP 下载。 */
|
||||||
|
async previewAllContractsBeforeCreate(params: Record<string, unknown>) {
|
||||||
|
return requestContractPreview(
|
||||||
|
"preview-all-before-create",
|
||||||
|
params,
|
||||||
|
"合同预览打包生成失败"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
create(params: Record<string, unknown>) {
|
create(params: Record<string, unknown>) {
|
||||||
return taskHttp.post("create", params);
|
return taskHttp.post("create", params);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
copy(params: {
|
||||||
|
sourceTaskId: number;
|
||||||
|
count: number;
|
||||||
|
items: Array<{ remark: string }>;
|
||||||
|
}) {
|
||||||
|
return taskHttp.post<OnboardingTaskCopyResult>("copy", params);
|
||||||
|
},
|
||||||
|
|
||||||
approve(params: {
|
approve(params: {
|
||||||
taskId: number;
|
taskId: number;
|
||||||
auditOpinion?: string;
|
auditOpinion?: string;
|
||||||
@@ -279,6 +456,67 @@ export const onboardingTaskApi = {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** 查询任务当前电子签流程和全部合同文件。 */
|
||||||
|
electronicSignDetail(taskId: number) {
|
||||||
|
return electronicSignHttp.get<OnboardingElectronicSignDetail>("detail", {
|
||||||
|
id: taskId
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 手动发起;失败流程会从已保存的本地断点继续。 */
|
||||||
|
initiateElectronicSign(taskId: number) {
|
||||||
|
return electronicSignHttp.post<OnboardingElectronicSignDetail>("initiate", {
|
||||||
|
taskId
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 保留旧流程记录,并以新的业务编号重新发起一次签约。 */
|
||||||
|
reinitiateElectronicSign(taskId: number, reason: string) {
|
||||||
|
return electronicSignHttp.post<OnboardingElectronicSignDetail>("reinitiate", {
|
||||||
|
taskId,
|
||||||
|
reason
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 主动同步签署状态,完成时后端自动执行归档。 */
|
||||||
|
syncElectronicSign(taskId: number) {
|
||||||
|
return electronicSignHttp.post<OnboardingElectronicSignDetail>("sync", {
|
||||||
|
taskId
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 获取有效的员工签署链接,旧链接失效时由后端重新生成。 */
|
||||||
|
getElectronicSignLink(taskId: number) {
|
||||||
|
return electronicSignHttp.post<OnboardingElectronicSignDetail>("get-link", {
|
||||||
|
taskId
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 获取一份员工已签合同的契约锁只读浏览页面。 */
|
||||||
|
previewEmployeeSignedDocument(taskId: number, signDocumentId: number) {
|
||||||
|
return electronicSignHttp.post<OnboardingElectronicSignPreview>(
|
||||||
|
"preview-employee-signed",
|
||||||
|
{ taskId, signDocumentId }
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 全部合同核对完成后,由后台管理员确认企业盖章。 */
|
||||||
|
companySealElectronicSign(taskId: number) {
|
||||||
|
return electronicSignHttp.post<OnboardingElectronicSignDetail>("company-seal", {
|
||||||
|
taskId
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 催办当前待签员工。 */
|
||||||
|
noticeElectronicSign(taskId: number) {
|
||||||
|
return electronicSignHttp.post("notice", { taskId });
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 作废尚未完成的电子签流程。 */
|
||||||
|
invalidElectronicSign(taskId: number, reason: string) {
|
||||||
|
return electronicSignHttp.post("invalid", { taskId, reason });
|
||||||
|
},
|
||||||
|
|
||||||
archive(params: {
|
archive(params: {
|
||||||
taskId: number;
|
taskId: number;
|
||||||
auditOpinion?: string;
|
auditOpinion?: string;
|
||||||
@@ -582,6 +820,14 @@ export const onboardingTaskApi = {
|
|||||||
>("page", params);
|
>("page", params);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
employeeExport(params: Record<string, unknown>) {
|
||||||
|
return moduleRequest("/biz/onboarding/employee/").download(
|
||||||
|
"export",
|
||||||
|
params,
|
||||||
|
{ cancel: false }
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
employeeDetail(params: ReqId) {
|
employeeDetail(params: ReqId) {
|
||||||
return moduleRequest("/biz/onboarding/employee/").get(
|
return moduleRequest("/biz/onboarding/employee/").get(
|
||||||
"detail",
|
"detail",
|
||||||
@@ -589,6 +835,49 @@ export const onboardingTaskApi = {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updateEmployeeHrInfo(params: {
|
||||||
|
id: number;
|
||||||
|
employmentType: "full_time" | "part_time";
|
||||||
|
contractNature?: string;
|
||||||
|
socialInsuranceStatus: "unknown" | "not_purchased" | "purchased" | "stopped" | "not_applicable";
|
||||||
|
socialInsuranceCompany?: string;
|
||||||
|
}) {
|
||||||
|
return moduleRequest("/biz/onboarding/employee/").post(
|
||||||
|
"hr-info/update",
|
||||||
|
params
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
uploadEmployeeAttachment(params: FormData) {
|
||||||
|
return moduleRequest("/biz/onboarding/employee/").post<number>(
|
||||||
|
"attachment/upload",
|
||||||
|
params,
|
||||||
|
{ cancel: false, loading: false }
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
regularizeEmployee(params: {
|
||||||
|
id: number;
|
||||||
|
regularizationDate: string;
|
||||||
|
remark?: string;
|
||||||
|
fileIds: number[];
|
||||||
|
}) {
|
||||||
|
return moduleRequest("/biz/onboarding/employee/").post(
|
||||||
|
"regularization/confirm",
|
||||||
|
params
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
revokeEmployeeRegularization(params: {
|
||||||
|
id: number;
|
||||||
|
remark: string;
|
||||||
|
}) {
|
||||||
|
return moduleRequest("/biz/onboarding/employee/").post(
|
||||||
|
"regularization/revoke",
|
||||||
|
params
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
listFormFields() {
|
listFormFields() {
|
||||||
return moduleRequest("/biz/onboarding/entry-config/").get<
|
return moduleRequest("/biz/onboarding/entry-config/").get<
|
||||||
OnboardingFormField[]
|
OnboardingFormField[]
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
/** 判断 Element Plus 弹窗是否由用户主动取消或关闭。 */
|
||||||
|
function isUserCancelled(action: unknown) {
|
||||||
|
return action === "cancel" || action === "close";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行确认弹窗。用户取消时返回 false,真正的异常继续抛出。
|
||||||
|
*/
|
||||||
|
export async function isMessageBoxConfirmed(confirmPromise: Promise<unknown>) {
|
||||||
|
try {
|
||||||
|
await confirmPromise;
|
||||||
|
return true;
|
||||||
|
} catch (action) {
|
||||||
|
if (isUserCancelled(action)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
throw action;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行输入弹窗。用户取消时返回 null,真正的异常继续抛出。
|
||||||
|
*/
|
||||||
|
export async function getMessageBoxPromptResult<T>(promptPromise: Promise<T>) {
|
||||||
|
try {
|
||||||
|
return await promptPromise;
|
||||||
|
} catch (action) {
|
||||||
|
if (isUserCancelled(action)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
throw action;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
/**
|
||||||
|
* 入职登记表固定字段校验工具。
|
||||||
|
*
|
||||||
|
* 前端校验用于即时提示,后端仍会执行同样的核心校验,避免绕过页面直接提交非法数据。
|
||||||
|
*/
|
||||||
|
|
||||||
|
const mobilePhonePattern = /^1[3-9]\d{9}$/;
|
||||||
|
const landlinePhonePattern = /^0\d{2,3}-?\d{7,8}$/;
|
||||||
|
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
const bankCardPattern = /^\d{12,19}$/;
|
||||||
|
|
||||||
|
const idCardWeights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
|
||||||
|
const idCardCheckCodes = ["1", "0", "X", "9", "8", "7", "6", "5", "4", "3", "2"];
|
||||||
|
|
||||||
|
export function normalizeIdCardNumber(value: unknown) {
|
||||||
|
return String(value ?? "").trim().toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeBankCardNumber(value: unknown) {
|
||||||
|
return String(value ?? "").replace(/[\s-]/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isMobilePhone(value: unknown) {
|
||||||
|
return mobilePhonePattern.test(String(value ?? "").trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isContactPhone(value: unknown) {
|
||||||
|
const phone = String(value ?? "").trim();
|
||||||
|
return mobilePhonePattern.test(phone) || landlinePhonePattern.test(phone);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isEmail(value: unknown) {
|
||||||
|
return emailPattern.test(String(value ?? "").trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isBankCardNumber(value: unknown) {
|
||||||
|
return bankCardPattern.test(normalizeBankCardNumber(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验中国大陆 18 位居民身份证,包括出生日期和校验位。
|
||||||
|
* 返回空字符串表示校验通过,否则返回适合直接展示给用户的错误信息。
|
||||||
|
*/
|
||||||
|
export function getIdCardValidationMessage(value: unknown) {
|
||||||
|
const idCardNumber = normalizeIdCardNumber(value);
|
||||||
|
if (!/^\d{17}[0-9X]$/.test(idCardNumber)) {
|
||||||
|
return "请输入正确的18位身份证号码";
|
||||||
|
}
|
||||||
|
|
||||||
|
const birthday = getIdCardBirthday(idCardNumber);
|
||||||
|
if (!birthday || !isValidDateText(birthday) || birthday > getTodayText()) {
|
||||||
|
return "身份证号码中的出生日期不正确";
|
||||||
|
}
|
||||||
|
|
||||||
|
const total = idCardWeights.reduce(
|
||||||
|
(sum, weight, index) => sum + Number(idCardNumber[index]) * weight,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
if (idCardCheckCodes[total % 11] !== idCardNumber[17]) {
|
||||||
|
return "身份证号码校验位不正确";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getIdCardBirthday(value: unknown) {
|
||||||
|
const idCardNumber = normalizeIdCardNumber(value);
|
||||||
|
if (!/^\d{17}[0-9X]$/.test(idCardNumber)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = idCardNumber.slice(6, 14);
|
||||||
|
return `${text.slice(0, 4)}-${text.slice(4, 6)}-${text.slice(6, 8)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isNumberInRange(value: unknown, min: number, max: number) {
|
||||||
|
const text = String(value ?? "").trim();
|
||||||
|
if (!/^\d+(\.\d+)?$/.test(text)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const number = Number(text);
|
||||||
|
return Number.isFinite(number) && number >= min && number <= max;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidDateText(value: string) {
|
||||||
|
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
|
||||||
|
if (!match) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const year = Number(match[1]);
|
||||||
|
const month = Number(match[2]);
|
||||||
|
const day = Number(match[3]);
|
||||||
|
const date = new Date(year, month - 1, day);
|
||||||
|
return date.getFullYear() === year
|
||||||
|
&& date.getMonth() === month - 1
|
||||||
|
&& date.getDate() === day;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTodayText() {
|
||||||
|
const now = new Date();
|
||||||
|
const year = now.getFullYear();
|
||||||
|
const month = String(now.getMonth() + 1).padStart(2, "0");
|
||||||
|
const day = String(now.getDate()).padStart(2, "0");
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
}
|
||||||
@@ -500,6 +500,7 @@ import {
|
|||||||
type OnboardingTemplateVariable
|
type OnboardingTemplateVariable
|
||||||
} from "@/api/modules/biz/onboarding";
|
} from "@/api/modules/biz/onboarding";
|
||||||
import type { ColumnProps, ProTableInstance } from "@/components/ProTable/interface";
|
import type { ColumnProps, ProTableInstance } from "@/components/ProTable/interface";
|
||||||
|
import { isMessageBoxConfirmed } from "@/utils/messageBox";
|
||||||
|
|
||||||
const templateTable = ref<ProTableInstance>();
|
const templateTable = ref<ProTableInstance>();
|
||||||
const selectedTemplate = ref<OnboardingContractTemplate>();
|
const selectedTemplate = ref<OnboardingContractTemplate>();
|
||||||
@@ -514,6 +515,8 @@ const positions = ref<OnboardingPositionOption[]>([]);
|
|||||||
const searchCompanies = ref<OnboardingCompanyOption[]>([]);
|
const searchCompanies = ref<OnboardingCompanyOption[]>([]);
|
||||||
const searchDepartments = ref<OnboardingDepartmentOption[]>([]);
|
const searchDepartments = ref<OnboardingDepartmentOption[]>([]);
|
||||||
const searchPositions = ref<OnboardingPositionOption[]>([]);
|
const searchPositions = ref<OnboardingPositionOption[]>([]);
|
||||||
|
const allSearchDepartments = ref<OnboardingDepartmentOption[]>([]);
|
||||||
|
const allSearchPositions = ref<OnboardingPositionOption[]>([]);
|
||||||
|
|
||||||
const companySearchOptions = computed(() =>
|
const companySearchOptions = computed(() =>
|
||||||
searchCompanies.value.map(c => ({
|
searchCompanies.value.map(c => ({
|
||||||
@@ -536,6 +539,58 @@ const positionSearchOptions = computed(() =>
|
|||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
async function onSearchCompanyChange(companyId?: number) {
|
||||||
|
if (companyId) {
|
||||||
|
const [depRes, posRes] = await Promise.all([
|
||||||
|
onboardingTaskApi.listDepartments(companyId).catch(() => ({ data: [] })),
|
||||||
|
onboardingTaskApi.listPositions(companyId).catch(() => ({ data: [] }))
|
||||||
|
]);
|
||||||
|
searchDepartments.value = depRes.data || [];
|
||||||
|
searchPositions.value = posRes.data || [];
|
||||||
|
} else {
|
||||||
|
searchDepartments.value = allSearchDepartments.value;
|
||||||
|
searchPositions.value = allSearchPositions.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
templateTable.value?.enumMap.set("departmentId", departmentSearchOptions.value);
|
||||||
|
templateTable.value?.enumMap.set("positionId", positionSearchOptions.value);
|
||||||
|
|
||||||
|
if (templateTable.value?.searchParam) {
|
||||||
|
const currentDept = templateTable.value.searchParam.departmentId;
|
||||||
|
if (currentDept && !searchDepartments.value.some(d => d.id === currentDept)) {
|
||||||
|
templateTable.value.searchParam.departmentId = undefined;
|
||||||
|
}
|
||||||
|
const currentPos = templateTable.value.searchParam.positionId;
|
||||||
|
if (currentPos && !searchPositions.value.some(p => p.id === currentPos)) {
|
||||||
|
templateTable.value.searchParam.positionId = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onSearchDepartmentChange(departmentId?: number) {
|
||||||
|
if (departmentId) {
|
||||||
|
const posRes = await onboardingTaskApi.listPositionsByDepartment(departmentId).catch(() => ({ data: [] }));
|
||||||
|
searchPositions.value = posRes.data || [];
|
||||||
|
} else {
|
||||||
|
const selectedCompanyId = templateTable.value?.searchParam?.companyId;
|
||||||
|
if (selectedCompanyId) {
|
||||||
|
const posRes = await onboardingTaskApi.listPositions(selectedCompanyId).catch(() => ({ data: [] }));
|
||||||
|
searchPositions.value = posRes.data || [];
|
||||||
|
} else {
|
||||||
|
searchPositions.value = allSearchPositions.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
templateTable.value?.enumMap.set("positionId", positionSearchOptions.value);
|
||||||
|
|
||||||
|
if (templateTable.value?.searchParam) {
|
||||||
|
const currentPos = templateTable.value.searchParam.positionId;
|
||||||
|
if (currentPos && !searchPositions.value.some(p => p.id === currentPos)) {
|
||||||
|
templateTable.value.searchParam.positionId = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const versions = ref<OnboardingContractVersion[]>([]);
|
const versions = ref<OnboardingContractVersion[]>([]);
|
||||||
const versionVisible = ref(false);
|
const versionVisible = ref(false);
|
||||||
const variablesVisible = ref(false);
|
const variablesVisible = ref(false);
|
||||||
@@ -605,7 +660,12 @@ const templateColumns: ColumnProps<OnboardingContractTemplate>[] = [
|
|||||||
enum: companySearchOptions,
|
enum: companySearchOptions,
|
||||||
search: {
|
search: {
|
||||||
el: "select",
|
el: "select",
|
||||||
props: { filterable: true, clearable: true, placeholder: "请选择所属公司" }
|
props: {
|
||||||
|
filterable: true,
|
||||||
|
clearable: true,
|
||||||
|
placeholder: "请选择所属公司",
|
||||||
|
onChange: (val?: number) => onSearchCompanyChange(val)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -615,7 +675,12 @@ const templateColumns: ColumnProps<OnboardingContractTemplate>[] = [
|
|||||||
enum: departmentSearchOptions,
|
enum: departmentSearchOptions,
|
||||||
search: {
|
search: {
|
||||||
el: "select",
|
el: "select",
|
||||||
props: { filterable: true, clearable: true, placeholder: "请选择所属部门" }
|
props: {
|
||||||
|
filterable: true,
|
||||||
|
clearable: true,
|
||||||
|
placeholder: "请选择所属部门",
|
||||||
|
onChange: (val?: number) => onSearchDepartmentChange(val)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -747,7 +812,11 @@ async function saveTemplate() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function removeTemplate(row: OnboardingContractTemplate) {
|
async function removeTemplate(row: OnboardingContractTemplate) {
|
||||||
await ElMessageBox.confirm(`确认删除签署材料模板“${row.templateName}”?`, "删除确认", { type: "warning" });
|
const confirmed = await isMessageBoxConfirmed(
|
||||||
|
ElMessageBox.confirm(`确认删除签署材料模板“${row.templateName}”?`, "删除确认", { type: "warning" })
|
||||||
|
);
|
||||||
|
if (!confirmed) return;
|
||||||
|
|
||||||
await onboardingTaskApi.deleteTemplate([row.id]);
|
await onboardingTaskApi.deleteTemplate([row.id]);
|
||||||
templateTable.value?.refresh();
|
templateTable.value?.refresh();
|
||||||
ElMessage.success("删除成功");
|
ElMessage.success("删除成功");
|
||||||
@@ -796,7 +865,11 @@ async function changeVersionStatus(row: OnboardingContractVersion, status: strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function removeVersion(row: OnboardingContractVersion) {
|
async function removeVersion(row: OnboardingContractVersion) {
|
||||||
await ElMessageBox.confirm(`确认删除签署材料版本“${row.versionNo}”?此操作不可撤销。`, "删除确认", { type: "warning" });
|
const confirmed = await isMessageBoxConfirmed(
|
||||||
|
ElMessageBox.confirm(`确认删除签署材料版本“${row.versionNo}”?此操作不可撤销。`, "删除确认", { type: "warning" })
|
||||||
|
);
|
||||||
|
if (!confirmed) return;
|
||||||
|
|
||||||
await onboardingTaskApi.deleteVersion(row.id);
|
await onboardingTaskApi.deleteVersion(row.id);
|
||||||
await loadVersions();
|
await loadVersions();
|
||||||
ElMessage.success("版本删除成功");
|
ElMessage.success("版本删除成功");
|
||||||
@@ -910,31 +983,43 @@ async function saveVariable() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadSearchOptions() {
|
async function loadSearchOptions() {
|
||||||
await loadCompanies();
|
try {
|
||||||
searchCompanies.value = companies.value;
|
await loadCompanies();
|
||||||
|
searchCompanies.value = companies.value;
|
||||||
|
|
||||||
const deptList: OnboardingDepartmentOption[] = [];
|
const deptList: OnboardingDepartmentOption[] = [];
|
||||||
const posList: OnboardingPositionOption[] = [];
|
const posList: OnboardingPositionOption[] = [];
|
||||||
|
|
||||||
if (companies.value && companies.value.length > 0) {
|
if (companies.value && companies.value.length > 0) {
|
||||||
const promises = companies.value.map(async comp => {
|
const promises = companies.value.map(async comp => {
|
||||||
const [depRes, posRes] = await Promise.all([
|
const [depRes, posRes] = await Promise.all([
|
||||||
onboardingTaskApi.listDepartments(comp.id),
|
onboardingTaskApi.listDepartments(comp.id).catch(() => ({ data: [] })),
|
||||||
onboardingTaskApi.listPositions(comp.id)
|
onboardingTaskApi.listPositions(comp.id).catch(() => ({ data: [] }))
|
||||||
]);
|
]);
|
||||||
if (depRes.data) deptList.push(...depRes.data);
|
if (depRes.data) deptList.push(...depRes.data);
|
||||||
if (posRes.data) posList.push(...posRes.data);
|
if (posRes.data) posList.push(...posRes.data);
|
||||||
});
|
});
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
|
}
|
||||||
|
|
||||||
|
const uniqueDeptMap = new Map<number, OnboardingDepartmentOption>();
|
||||||
|
deptList.forEach(d => uniqueDeptMap.set(d.id, d));
|
||||||
|
const finalDepts = Array.from(uniqueDeptMap.values());
|
||||||
|
allSearchDepartments.value = finalDepts;
|
||||||
|
searchDepartments.value = finalDepts;
|
||||||
|
|
||||||
|
const uniquePosMap = new Map<number, OnboardingPositionOption>();
|
||||||
|
posList.forEach(p => uniquePosMap.set(p.id, p));
|
||||||
|
const finalPositions = Array.from(uniquePosMap.values());
|
||||||
|
allSearchPositions.value = finalPositions;
|
||||||
|
searchPositions.value = finalPositions;
|
||||||
|
|
||||||
|
templateTable.value?.enumMap.set("companyId", companySearchOptions.value);
|
||||||
|
templateTable.value?.enumMap.set("departmentId", departmentSearchOptions.value);
|
||||||
|
templateTable.value?.enumMap.set("positionId", positionSearchOptions.value);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("加载签署材料搜索选项失败", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
const uniqueDeptMap = new Map<number, OnboardingDepartmentOption>();
|
|
||||||
deptList.forEach(d => uniqueDeptMap.set(d.id, d));
|
|
||||||
searchDepartments.value = Array.from(uniqueDeptMap.values());
|
|
||||||
|
|
||||||
const uniquePosMap = new Map<number, OnboardingPositionOption>();
|
|
||||||
posList.forEach(p => uniquePosMap.set(p.id, p));
|
|
||||||
searchPositions.value = Array.from(uniquePosMap.values());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(loadSearchOptions);
|
onMounted(loadSearchOptions);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -118,7 +118,12 @@
|
|||||||
<el-row :gutter="20">
|
<el-row :gutter="20">
|
||||||
<el-col :xs="24" :sm="8">
|
<el-col :xs="24" :sm="8">
|
||||||
<el-form-item label="模板编码" required>
|
<el-form-item label="模板编码" required>
|
||||||
<el-input v-model="form.templateCode" placeholder="如:FULLTIME_DEV" />
|
<el-input
|
||||||
|
v-model="form.templateCode"
|
||||||
|
maxlength="100"
|
||||||
|
show-word-limit
|
||||||
|
placeholder="如:FULLTIME_DEV(纯字母也可以)"
|
||||||
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :xs="24" :sm="8">
|
<el-col :xs="24" :sm="8">
|
||||||
@@ -287,6 +292,7 @@ import {
|
|||||||
OnboardingTemplate,
|
OnboardingTemplate,
|
||||||
onboardingTaskApi
|
onboardingTaskApi
|
||||||
} from "@/api/modules/biz/onboarding";
|
} from "@/api/modules/biz/onboarding";
|
||||||
|
import { isMessageBoxConfirmed } from "@/utils/messageBox";
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const saving = ref(false);
|
const saving = ref(false);
|
||||||
@@ -592,6 +598,7 @@ async function openEdit(row: OnboardingTemplate) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function save() {
|
async function save() {
|
||||||
|
form.templateCode = form.templateCode.trim();
|
||||||
if (
|
if (
|
||||||
!form.templateCode
|
!form.templateCode
|
||||||
|| !form.templateName
|
|| !form.templateName
|
||||||
@@ -601,6 +608,9 @@ async function save() {
|
|||||||
) {
|
) {
|
||||||
return ElMessage.warning("请完整填写模板编码、名称、公司、部门及适用岗位");
|
return ElMessage.warning("请完整填写模板编码、名称、公司、部门及适用岗位");
|
||||||
}
|
}
|
||||||
|
if (!/^[A-Za-z][A-Za-z0-9._-]{0,99}$/.test(form.templateCode)) {
|
||||||
|
return ElMessage.warning("模板编码须以字母开头,最长 100 位,仅支持字母、数字、点、下划线和短横线");
|
||||||
|
}
|
||||||
if (!form.contracts.length || form.contracts.some(item => !item.contractTemplateId || !item.contractTemplateVersionId)) {
|
if (!form.contracts.length || form.contracts.some(item => !item.contractTemplateId || !item.contractTemplateVersionId)) {
|
||||||
return ElMessage.warning("请完整选择至少一份签署材料模板及版本");
|
return ElMessage.warning("请完整选择至少一份签署材料模板及版本");
|
||||||
}
|
}
|
||||||
@@ -617,7 +627,11 @@ async function save() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function removeTemplate(row: OnboardingTemplate) {
|
async function removeTemplate(row: OnboardingTemplate) {
|
||||||
await ElMessageBox.confirm(`确认删除入职模板“${row.templateName}”吗?`, "删除确认", { type: "warning" });
|
const confirmed = await isMessageBoxConfirmed(
|
||||||
|
ElMessageBox.confirm(`确认删除入职模板“${row.templateName}”吗?`, "删除确认", { type: "warning" })
|
||||||
|
);
|
||||||
|
if (!confirmed) return;
|
||||||
|
|
||||||
await onboardingTaskApi.deleteOnboardingTemplate([row.id!]);
|
await onboardingTaskApi.deleteOnboardingTemplate([row.id!]);
|
||||||
ElMessage.success("删除成功");
|
ElMessage.success("删除成功");
|
||||||
query.pageNum = 1;
|
query.pageNum = 1;
|
||||||
|
|||||||
@@ -75,20 +75,22 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
|
||||||
<el-table-column prop="remark" label="说明备注" min-width="200" show-overflow-tooltip />
|
<el-table-column prop="remark" label="说明备注" min-width="140" show-overflow-tooltip />
|
||||||
|
|
||||||
<el-table-column label="操作" width="140" align="center" fixed="right">
|
<el-table-column label="操作" width="160" align="center" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button link type="primary" icon="Edit" @click="openEdit(row)">编辑</el-button>
|
<div style="display: flex; align-items: center; justify-content: center; gap: 8px; white-space: nowrap;">
|
||||||
<el-button
|
<el-button link type="primary" icon="Edit" @click="openEdit(row)">编辑</el-button>
|
||||||
v-if="!row.isBuiltIn"
|
<el-button
|
||||||
link
|
v-if="!row.isBuiltIn"
|
||||||
type="danger"
|
link
|
||||||
icon="Delete"
|
type="danger"
|
||||||
@click="remove(row)"
|
icon="Delete"
|
||||||
>
|
@click="remove(row)"
|
||||||
删除
|
>
|
||||||
</el-button>
|
删除
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -168,6 +170,7 @@
|
|||||||
import { ref, reactive, onMounted } from "vue";
|
import { ref, reactive, onMounted } from "vue";
|
||||||
import { ElMessage, ElMessageBox, FormInstance, FormRules } from "element-plus";
|
import { ElMessage, ElMessageBox, FormInstance, FormRules } from "element-plus";
|
||||||
import { onboardingTaskApi } from "@/api/modules/biz/onboarding";
|
import { onboardingTaskApi } from "@/api/modules/biz/onboarding";
|
||||||
|
import { isMessageBoxConfirmed } from "@/utils/messageBox";
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const saving = ref(false);
|
const saving = ref(false);
|
||||||
@@ -293,7 +296,11 @@ async function save() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function remove(row: any) {
|
async function remove(row: any) {
|
||||||
await ElMessageBox.confirm(`确认删除自定义变量“${row.variableCode}”?`, "删除确认", { type: "warning" });
|
const confirmed = await isMessageBoxConfirmed(
|
||||||
|
ElMessageBox.confirm(`确认删除自定义变量“${row.variableCode}”?`, "删除确认", { type: "warning" })
|
||||||
|
);
|
||||||
|
if (!confirmed) return;
|
||||||
|
|
||||||
await onboardingTaskApi.deleteVariableDefinitions([row.id]);
|
await onboardingTaskApi.deleteVariableDefinitions([row.id]);
|
||||||
ElMessage.success("删除成功");
|
ElMessage.success("删除成功");
|
||||||
loadData();
|
loadData();
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ import {
|
|||||||
OnboardingMaterialType,
|
OnboardingMaterialType,
|
||||||
onboardingTaskApi
|
onboardingTaskApi
|
||||||
} from "@/api/modules/biz/onboarding";
|
} from "@/api/modules/biz/onboarding";
|
||||||
|
import { isMessageBoxConfirmed } from "@/utils/messageBox";
|
||||||
import OnboardingTemplatePanel from "./components/OnboardingTemplatePanel.vue";
|
import OnboardingTemplatePanel from "./components/OnboardingTemplatePanel.vue";
|
||||||
import VariableDefinitionPanel from "./components/VariableDefinitionPanel.vue";
|
import VariableDefinitionPanel from "./components/VariableDefinitionPanel.vue";
|
||||||
|
|
||||||
@@ -152,7 +153,11 @@ async function saveMaterial() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function removeMaterial(row: OnboardingMaterialType) {
|
async function removeMaterial(row: OnboardingMaterialType) {
|
||||||
await ElMessageBox.confirm(`确认删除资料类型“${row.materialName}”吗?`, "删除确认", { type: "warning" });
|
const confirmed = await isMessageBoxConfirmed(
|
||||||
|
ElMessageBox.confirm(`确认删除资料类型“${row.materialName}”吗?`, "删除确认", { type: "warning" })
|
||||||
|
);
|
||||||
|
if (!confirmed) return;
|
||||||
|
|
||||||
await onboardingTaskApi.deleteMaterialTypes([row.id!]);
|
await onboardingTaskApi.deleteMaterialTypes([row.id!]);
|
||||||
ElMessage.success("删除成功");
|
ElMessage.success("删除成功");
|
||||||
await loadMaterials();
|
await loadMaterials();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="main-box setup-container">
|
<div class="main-box setup-container">
|
||||||
<el-tabs v-model="activeTab" class="setup-tabs">
|
<el-tabs v-model="activeTab" class="setup-tabs">
|
||||||
<!-- 1. 入职公司管理 -->
|
<!-- 1. 入职公司管理 -->
|
||||||
@@ -23,6 +23,15 @@
|
|||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template #qiyuesuoConfig="scope">
|
||||||
|
<el-tag
|
||||||
|
size="small"
|
||||||
|
:type="scope.row.qiyuesuoCategoryId && scope.row.qiyuesuoSealId ? 'success' : 'warning'"
|
||||||
|
>
|
||||||
|
{{ scope.row.qiyuesuoCategoryId && scope.row.qiyuesuoSealId ? "已配置" : "未配置" }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template #operation="scope">
|
<template #operation="scope">
|
||||||
<el-button link type="primary" icon="Edit" @click="openCompanyEdit(scope.row)">
|
<el-button link type="primary" icon="Edit" @click="openCompanyEdit(scope.row)">
|
||||||
编辑
|
编辑
|
||||||
@@ -130,50 +139,62 @@
|
|||||||
<el-dialog
|
<el-dialog
|
||||||
v-model="dialogVisible"
|
v-model="dialogVisible"
|
||||||
:title="dialogTitle"
|
:title="dialogTitle"
|
||||||
width="680px"
|
width="820px"
|
||||||
|
top="6vh"
|
||||||
destroy-on-close
|
destroy-on-close
|
||||||
append-to-body
|
append-to-body
|
||||||
>
|
>
|
||||||
<!-- 1. 公司表单 -->
|
<!-- 1. 公司表单 -->
|
||||||
<el-form v-if="dialogKind === 'company'" :model="companyForm" label-width="140px">
|
<el-form
|
||||||
|
v-if="dialogKind === 'company'"
|
||||||
|
ref="companyFormRef"
|
||||||
|
:model="companyForm"
|
||||||
|
:rules="companyRules"
|
||||||
|
label-width="140px"
|
||||||
|
>
|
||||||
<el-row :gutter="20">
|
<el-row :gutter="20">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="公司编码" required>
|
<el-form-item label="公司编码" prop="companyCode">
|
||||||
<el-input v-model="companyForm.companyCode" placeholder="如:TECH_CORP" />
|
<el-input v-model.trim="companyForm.companyCode" placeholder="如:TECH_CORP" clearable />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="公司简称/名称" required>
|
<el-form-item label="公司简称/名称" prop="companyName">
|
||||||
<el-input v-model="companyForm.companyName" placeholder="如:总公司 / 科技分部" />
|
<el-input v-model.trim="companyForm.companyName" placeholder="如:总公司 / 科技分部" clearable />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<el-form-item label="签署材料甲方全称" required>
|
<el-form-item label="签署材料甲方全称" prop="legalName">
|
||||||
<el-input v-model="companyForm.legalName" placeholder="用于签署材料填写的法定主体全称" />
|
<el-input v-model.trim="companyForm.legalName" placeholder="必须与契约锁中的企业名称完全一致" clearable />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-row :gutter="20">
|
<el-row :gutter="20">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="统一社会信用代码">
|
<el-form-item label="统一社会信用代码" prop="unifiedSocialCreditCode">
|
||||||
<el-input v-model="companyForm.unifiedSocialCreditCode" placeholder="18位统一信用代码" />
|
<el-input
|
||||||
|
v-model.trim="companyForm.unifiedSocialCreditCode"
|
||||||
|
placeholder="18位统一社会信用代码"
|
||||||
|
maxlength="18"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="法定代表人">
|
<el-form-item label="法定代表人" prop="legalRepresentative">
|
||||||
<el-input v-model="companyForm.legalRepresentative" placeholder="法定代表人姓名" />
|
<el-input v-model.trim="companyForm.legalRepresentative" placeholder="法定代表人姓名" clearable />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<el-row :gutter="20">
|
<el-row :gutter="20">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="联系电话">
|
<el-form-item label="联系电话" prop="contactPhone">
|
||||||
<el-input v-model="companyForm.contactPhone" placeholder="企业联系电话" />
|
<el-input v-model.trim="companyForm.contactPhone" placeholder="企业联系电话(手机或固话)" clearable />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="状态" required>
|
<el-form-item label="状态" prop="status">
|
||||||
<el-select v-model="companyForm.status" class="w-full">
|
<el-select v-model="companyForm.status" class="w-full">
|
||||||
<el-option label="启用" value="enable" />
|
<el-option label="启用" value="enable" />
|
||||||
<el-option label="停用" value="disable" />
|
<el-option label="停用" value="disable" />
|
||||||
@@ -182,13 +203,34 @@
|
|||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<el-form-item label="注册地址">
|
<el-form-item label="注册地址" prop="registeredAddress">
|
||||||
<el-input v-model="companyForm.registeredAddress" placeholder="企业法定注册地址" />
|
<el-input v-model.trim="companyForm.registeredAddress" placeholder="企业法定注册地址" clearable />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="备注">
|
<el-form-item label="备注" prop="remark">
|
||||||
<el-input v-model="companyForm.remark" type="textarea" :rows="2" placeholder="备注信息" />
|
<el-input v-model="companyForm.remark" type="textarea" :rows="2" placeholder="备注信息(选填)" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-divider content-position="left">契约锁电子签配置(由技术人员添加)</el-divider>
|
||||||
|
<el-alert
|
||||||
|
title="说明:默认业务分类 Id 与企业印章 Id 由技术人员添加配置,非技术人员请勿随意修改。"
|
||||||
|
type="info"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
style="margin-bottom: 18px"
|
||||||
|
/>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="默认业务分类 Id" prop="qiyuesuoCategoryId">
|
||||||
|
<el-input v-model.trim="companyForm.qiyuesuoCategoryId" placeholder="契约锁当前公司的分类 Id" clearable />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="企业印章 Id" prop="qiyuesuoSealId">
|
||||||
|
<el-input v-model.trim="companyForm.qiyuesuoSealId" placeholder="契约锁当前公司的印章 Id" clearable />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
<!-- 2. 部门表单 -->
|
<!-- 2. 部门表单 -->
|
||||||
@@ -322,8 +364,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts" name="onboardingSetup">
|
<script setup lang="ts" name="onboardingSetup">
|
||||||
import { ref, reactive, computed, onMounted } from "vue";
|
import { ref, reactive, computed, onMounted, nextTick } from "vue";
|
||||||
import { ElMessage, ElMessageBox } from "element-plus";
|
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from "element-plus";
|
||||||
import { Plus, Edit, Delete } from "@element-plus/icons-vue";
|
import { Plus, Edit, Delete } from "@element-plus/icons-vue";
|
||||||
import {
|
import {
|
||||||
onboardingTaskApi,
|
onboardingTaskApi,
|
||||||
@@ -334,6 +376,7 @@ import {
|
|||||||
type OnboardingPosition
|
type OnboardingPosition
|
||||||
} from "@/api/modules/biz/onboarding";
|
} from "@/api/modules/biz/onboarding";
|
||||||
import type { ColumnProps, ProTableInstance, EnumProps } from "@/components/ProTable/interface";
|
import type { ColumnProps, ProTableInstance, EnumProps } from "@/components/ProTable/interface";
|
||||||
|
import { isMessageBoxConfirmed } from "@/utils/messageBox";
|
||||||
|
|
||||||
const activeTab = ref("company");
|
const activeTab = ref("company");
|
||||||
|
|
||||||
@@ -369,6 +412,8 @@ const emptyCompany = (): Partial<OnboardingCompany> => ({
|
|||||||
legalRepresentative: "",
|
legalRepresentative: "",
|
||||||
registeredAddress: "",
|
registeredAddress: "",
|
||||||
contactPhone: "",
|
contactPhone: "",
|
||||||
|
qiyuesuoCategoryId: "",
|
||||||
|
qiyuesuoSealId: "",
|
||||||
status: "enable",
|
status: "enable",
|
||||||
remark: ""
|
remark: ""
|
||||||
});
|
});
|
||||||
@@ -393,7 +438,72 @@ const emptyPosition = (): Partial<OnboardingPosition> => ({
|
|||||||
remark: ""
|
remark: ""
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const companyFormRef = ref<FormInstance>();
|
||||||
const companyForm = reactive<Partial<OnboardingCompany>>(emptyCompany());
|
const companyForm = reactive<Partial<OnboardingCompany>>(emptyCompany());
|
||||||
|
|
||||||
|
const companyRules = reactive<FormRules>({
|
||||||
|
companyCode: [
|
||||||
|
{ required: true, message: "请输入公司编码", trigger: "blur" },
|
||||||
|
{
|
||||||
|
pattern: /^[a-zA-Z0-9_-]{2,50}$/,
|
||||||
|
message: "公司编码格式不正确,支持2-50位字母、数字、下划线或短横线",
|
||||||
|
trigger: "blur"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
companyName: [
|
||||||
|
{ required: true, message: "请输入公司简称/名称", trigger: "blur" },
|
||||||
|
{ min: 2, max: 100, message: "公司名称长度需在 2 到 100 个字符之间", trigger: "blur" }
|
||||||
|
],
|
||||||
|
legalName: [
|
||||||
|
{ required: true, message: "请输入签署材料甲方全称", trigger: "blur" },
|
||||||
|
{ min: 2, max: 100, message: "甲方全称长度需在 2 到 100 个字符之间", trigger: "blur" }
|
||||||
|
],
|
||||||
|
unifiedSocialCreditCode: [
|
||||||
|
{ required: true, message: "请输入统一社会信用代码", trigger: "blur" },
|
||||||
|
{
|
||||||
|
pattern: /^[0-9A-HJ-NPQRTUWXY]{2}\d{6}[0-9A-HJ-NPQRTUWXY]{10}$/i,
|
||||||
|
message: "统一社会信用代码格式不正确,请输入18位标准社会信用代码",
|
||||||
|
trigger: "blur"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
legalRepresentative: [
|
||||||
|
{ required: true, message: "请输入法定代表人姓名", trigger: "blur" },
|
||||||
|
{
|
||||||
|
pattern: /^[\u4e00-\u9fa5a-zA-Z·.\s]{2,20}$/,
|
||||||
|
message: "法定代表人姓名格式不正确,请输入2-20位中英文姓名",
|
||||||
|
trigger: "blur"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
contactPhone: [
|
||||||
|
{ required: true, message: "请输入企业联系电话", trigger: "blur" },
|
||||||
|
{
|
||||||
|
pattern: /^(1[3-9]\d{9}|(0\d{2,3}-?)?\d{7,8}|400(-?\d{3,4}){2})$/,
|
||||||
|
message: "联系电话格式不正确,请输入有效的手机号或固话号码",
|
||||||
|
trigger: "blur"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
status: [
|
||||||
|
{ required: true, message: "请选择公司启用状态", trigger: "change" }
|
||||||
|
],
|
||||||
|
registeredAddress: [
|
||||||
|
{ required: true, message: "请输入企业法定注册地址", trigger: "blur" },
|
||||||
|
{ min: 4, max: 200, message: "注册地址请至少输入4个字符", trigger: "blur" }
|
||||||
|
],
|
||||||
|
qiyuesuoCategoryId: [
|
||||||
|
{
|
||||||
|
pattern: /^\d*$/,
|
||||||
|
message: "契约锁业务分类 Id 必须为纯数字",
|
||||||
|
trigger: "blur"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
qiyuesuoSealId: [
|
||||||
|
{
|
||||||
|
pattern: /^\d*$/,
|
||||||
|
message: "契约锁企业印章 Id 必须为纯数字",
|
||||||
|
trigger: "blur"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
const departmentForm = reactive<Partial<OnboardingDepartment>>(emptyDepartment());
|
const departmentForm = reactive<Partial<OnboardingDepartment>>(emptyDepartment());
|
||||||
const positionForm = reactive<Partial<OnboardingPosition>>(emptyPosition());
|
const positionForm = reactive<Partial<OnboardingPosition>>(emptyPosition());
|
||||||
|
|
||||||
@@ -437,6 +547,12 @@ const companyColumns: ColumnProps<OnboardingCompany>[] = [
|
|||||||
label: "联系电话",
|
label: "联系电话",
|
||||||
width: 140
|
width: 140
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
prop: "qiyuesuoConfig",
|
||||||
|
label: "契约锁配置",
|
||||||
|
width: 120,
|
||||||
|
align: "center"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
prop: "status",
|
prop: "status",
|
||||||
label: "状态",
|
label: "状态",
|
||||||
@@ -601,6 +717,9 @@ function openCompanyCreate() {
|
|||||||
editing.value = false;
|
editing.value = false;
|
||||||
resetCompany();
|
resetCompany();
|
||||||
dialogVisible.value = true;
|
dialogVisible.value = true;
|
||||||
|
nextTick(() => {
|
||||||
|
companyFormRef.value?.clearValidate();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function openDepartmentCreate() {
|
function openDepartmentCreate() {
|
||||||
@@ -624,6 +743,9 @@ async function openCompanyEdit(row: OnboardingCompany) {
|
|||||||
editing.value = true;
|
editing.value = true;
|
||||||
Object.assign(companyForm, data);
|
Object.assign(companyForm, data);
|
||||||
dialogVisible.value = true;
|
dialogVisible.value = true;
|
||||||
|
nextTick(() => {
|
||||||
|
companyFormRef.value?.clearValidate();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openDepartmentEdit(row: OnboardingDepartment) {
|
async function openDepartmentEdit(row: OnboardingDepartment) {
|
||||||
@@ -647,8 +769,21 @@ async function openPositionEdit(row: OnboardingPosition) {
|
|||||||
|
|
||||||
async function save() {
|
async function save() {
|
||||||
if (dialogKind.value === "company") {
|
if (dialogKind.value === "company") {
|
||||||
if (!companyForm.companyCode || !companyForm.companyName || !companyForm.legalName) {
|
const valid = await companyFormRef.value?.validate().catch(() => false);
|
||||||
return ElMessage.warning("请填写公司编码、公司名称和签署材料甲方全称");
|
if (!valid) return;
|
||||||
|
|
||||||
|
const categoryId = companyForm.qiyuesuoCategoryId?.trim() || "";
|
||||||
|
const sealId = companyForm.qiyuesuoSealId?.trim() || "";
|
||||||
|
if (Boolean(categoryId) !== Boolean(sealId)) {
|
||||||
|
return ElMessage.warning("契约锁默认业务分类 Id 和企业印章 Id 必须同时填写(或均不填)");
|
||||||
|
}
|
||||||
|
if (categoryId && (!/^\d+$/.test(categoryId) || !/^\d+$/.test(sealId))) {
|
||||||
|
return ElMessage.warning("契约锁默认业务分类 Id 和企业印章 Id 只能填写数字");
|
||||||
|
}
|
||||||
|
companyForm.qiyuesuoCategoryId = categoryId;
|
||||||
|
companyForm.qiyuesuoSealId = sealId;
|
||||||
|
if (companyForm.unifiedSocialCreditCode) {
|
||||||
|
companyForm.unifiedSocialCreditCode = companyForm.unifiedSocialCreditCode.trim().toUpperCase();
|
||||||
}
|
}
|
||||||
await onboardingTaskApi.saveCompany(companyForm, editing.value);
|
await onboardingTaskApi.saveCompany(companyForm, editing.value);
|
||||||
companyTable.value?.refresh();
|
companyTable.value?.refresh();
|
||||||
@@ -672,20 +807,32 @@ async function save() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function removeCompany(row: OnboardingCompany) {
|
async function removeCompany(row: OnboardingCompany) {
|
||||||
await ElMessageBox.confirm(`确认删除公司“${row.companyName}”?`, "删除确认", { type: "warning" });
|
const confirmed = await isMessageBoxConfirmed(
|
||||||
|
ElMessageBox.confirm(`确认删除公司“${row.companyName}”?`, "删除确认", { type: "warning" })
|
||||||
|
);
|
||||||
|
if (!confirmed) return;
|
||||||
|
|
||||||
await onboardingTaskApi.deleteCompany([row.id]);
|
await onboardingTaskApi.deleteCompany([row.id]);
|
||||||
companyTable.value?.refresh();
|
companyTable.value?.refresh();
|
||||||
await loadCompanies();
|
await loadCompanies();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeDepartment(row: OnboardingDepartment) {
|
async function removeDepartment(row: OnboardingDepartment) {
|
||||||
await ElMessageBox.confirm(`确认删除部门“${row.departmentName}”?`, "删除确认", { type: "warning" });
|
const confirmed = await isMessageBoxConfirmed(
|
||||||
|
ElMessageBox.confirm(`确认删除部门“${row.departmentName}”?`, "删除确认", { type: "warning" })
|
||||||
|
);
|
||||||
|
if (!confirmed) return;
|
||||||
|
|
||||||
await onboardingTaskApi.deleteDepartment([row.id]);
|
await onboardingTaskApi.deleteDepartment([row.id]);
|
||||||
departmentTable.value?.refresh();
|
departmentTable.value?.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removePosition(row: OnboardingPosition) {
|
async function removePosition(row: OnboardingPosition) {
|
||||||
await ElMessageBox.confirm(`确认删除岗位“${row.positionName}”?`, "删除确认", { type: "warning" });
|
const confirmed = await isMessageBoxConfirmed(
|
||||||
|
ElMessageBox.confirm(`确认删除岗位“${row.positionName}”?`, "删除确认", { type: "warning" })
|
||||||
|
);
|
||||||
|
if (!confirmed) return;
|
||||||
|
|
||||||
await onboardingTaskApi.deletePosition([row.id]);
|
await onboardingTaskApi.deletePosition([row.id]);
|
||||||
positionTable.value?.refresh();
|
positionTable.value?.refresh();
|
||||||
}
|
}
|
||||||
|
|||||||
+1883
-196
File diff suppressed because it is too large
Load Diff
@@ -7,8 +7,8 @@
|
|||||||
<el-icon><Promotion /></el-icon>
|
<el-icon><Promotion /></el-icon>
|
||||||
<span>OnboardX 智能入职服务</span>
|
<span>OnboardX 智能入职服务</span>
|
||||||
</div>
|
</div>
|
||||||
<h1 class="brand-title">欢迎加入新团队</h1>
|
<h1 class="brand-title">很高兴与你相遇,欢迎加入!</h1>
|
||||||
<p class="brand-subtitle">请按照 4 步指引完成入职登记表、证件上传与签署材料查阅确认</p>
|
<p class="brand-subtitle">请完成以下入职流程:填写登记信息、上传证件、阅读并签署相关入职材料,开启你的职场新旅程。</p>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -34,6 +34,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<el-alert
|
||||||
|
v-if="isLinkExpired"
|
||||||
|
class="link-expired-alert"
|
||||||
|
title="入职链接已过期"
|
||||||
|
description="该链接已超过有效期,不能继续填写或提交。请联系工作人员重新获取入职链接。"
|
||||||
|
type="error"
|
||||||
|
show-icon
|
||||||
|
:closable="false"
|
||||||
|
/>
|
||||||
|
|
||||||
|
|
||||||
<!-- 审核退回后向候选人明确展示需要补充或修改的内容。 -->
|
<!-- 审核退回后向候选人明确展示需要补充或修改的内容。 -->
|
||||||
<el-alert
|
<el-alert
|
||||||
v-if="task.taskStatus === 'rejected'"
|
v-if="task.taskStatus === 'rejected'"
|
||||||
@@ -47,7 +58,7 @@
|
|||||||
|
|
||||||
<!-- 分步向导模式 -->
|
<!-- 分步向导模式 -->
|
||||||
<div class="wizard-card">
|
<div class="wizard-card">
|
||||||
<div v-if="!canEdit" class="readonly-banner">
|
<div v-if="!canEdit && !isLinkExpired" class="readonly-banner">
|
||||||
<el-alert
|
<el-alert
|
||||||
title="当前为查阅模式(入职资料已提交审核)"
|
title="当前为查阅模式(入职资料已提交审核)"
|
||||||
type="info"
|
type="info"
|
||||||
@@ -91,7 +102,7 @@
|
|||||||
<div v-show="currentStep === 0" class="step-content">
|
<div v-show="currentStep === 0" class="step-content">
|
||||||
<div class="registration-header">
|
<div class="registration-header">
|
||||||
<h2 class="reg-title">入 职 登 记 表</h2>
|
<h2 class="reg-title">入 职 登 记 表</h2>
|
||||||
<p class="reg-subtitle">请准确填写您的真实个人档案、学习经历与家庭信息(带 * 为必填项)</p>
|
<p class="reg-subtitle">请如实填写个人基础档案、教育经历及家庭相关信息,标注 * 为必填项,信息将用于建立人事档案,请务必保证资料真实准确。</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-form ref="regFormRef" :model="form" :rules="regRules" label-position="top" class="reg-form">
|
<el-form ref="regFormRef" :model="form" :rules="regRules" label-position="top" class="reg-form">
|
||||||
@@ -290,7 +301,12 @@
|
|||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!educationHistory.length" class="empty-hint">暂未添加学习经历,可点击右上角添加</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">
|
<div class="row-badge-header">
|
||||||
<span class="row-badge">学习经历 #{{ idx + 1 }}</span>
|
<span class="row-badge">学习经历 #{{ idx + 1 }}</span>
|
||||||
<el-button v-if="canEdit" link type="danger" icon="Delete" @click="educationHistory.splice(idx, 1)">删除本条</el-button>
|
<el-button v-if="canEdit" link type="danger" icon="Delete" @click="educationHistory.splice(idx, 1)">删除本条</el-button>
|
||||||
@@ -332,7 +348,12 @@
|
|||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!workHistory.length" class="empty-hint">暂未添加工作经历(应届生可留空)</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">
|
<div class="row-badge-header">
|
||||||
<span class="row-badge">工作经历 #{{ idx + 1 }}</span>
|
<span class="row-badge">工作经历 #{{ idx + 1 }}</span>
|
||||||
<el-button v-if="canEdit" link type="danger" icon="Delete" @click="workHistory.splice(idx, 1)">删除本条</el-button>
|
<el-button v-if="canEdit" link type="danger" icon="Delete" @click="workHistory.splice(idx, 1)">删除本条</el-button>
|
||||||
@@ -379,7 +400,12 @@
|
|||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!familyMembers.length" class="empty-hint">暂未添加家庭成员信息</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">
|
<div class="row-badge-header">
|
||||||
<span class="row-badge">家庭成员 #{{ idx + 1 }}</span>
|
<span class="row-badge">家庭成员 #{{ idx + 1 }}</span>
|
||||||
<el-button v-if="canEdit" link type="danger" icon="Delete" @click="familyMembers.splice(idx, 1)">删除本条</el-button>
|
<el-button v-if="canEdit" link type="danger" icon="Delete" @click="familyMembers.splice(idx, 1)">删除本条</el-button>
|
||||||
@@ -440,12 +466,53 @@
|
|||||||
class="mb-3"
|
class="mb-3"
|
||||||
/>
|
/>
|
||||||
<el-row :gutter="16">
|
<el-row :gutter="16">
|
||||||
<el-col v-for="v in extraVariables" :key="v.variableCode" :xs="24" :sm="12">
|
<el-col
|
||||||
<el-form-item :label="v.variableName || v.variableCode" :required="v.required">
|
v-for="(v, index) in extraVariables"
|
||||||
<el-input
|
:id="`extra-variable-${index}`"
|
||||||
|
:key="v.variableCode"
|
||||||
|
:xs="24"
|
||||||
|
:sm="12"
|
||||||
|
>
|
||||||
|
<el-form-item
|
||||||
|
:label="v.variableName || v.variableCode"
|
||||||
|
:required="v.required"
|
||||||
|
:error="extraVariableErrors[v.variableCode]"
|
||||||
|
>
|
||||||
|
<el-select
|
||||||
|
v-if="getVariableOptions(v).length"
|
||||||
v-model="customVariables[v.variableCode]"
|
v-model="customVariables[v.variableCode]"
|
||||||
|
:placeholder="`请选择${v.variableName}`"
|
||||||
|
:disabled="!canEdit"
|
||||||
|
class="full-width"
|
||||||
|
clearable
|
||||||
|
@change="clearExtraVariableError(v.variableCode)"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="option in getVariableOptions(v)"
|
||||||
|
:key="option.value"
|
||||||
|
:label="option.label"
|
||||||
|
:value="option.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
<el-date-picker
|
||||||
|
v-else-if="getVariableDataType(v) === 'date'"
|
||||||
|
v-model="customVariables[v.variableCode]"
|
||||||
|
type="date"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
:placeholder="`请选择${v.variableName}`"
|
||||||
|
:disabled="!canEdit"
|
||||||
|
class="full-width"
|
||||||
|
@change="clearExtraVariableError(v.variableCode)"
|
||||||
|
/>
|
||||||
|
<el-input
|
||||||
|
v-else
|
||||||
|
v-model="customVariables[v.variableCode]"
|
||||||
|
:type="getVariableDataType(v) === 'number' ? 'number' : 'text'"
|
||||||
|
:inputmode="getVariableDataType(v) === 'number' ? 'decimal' : 'text'"
|
||||||
:placeholder="v.defaultValue || `请输入${v.variableName}`"
|
:placeholder="v.defaultValue || `请输入${v.variableName}`"
|
||||||
:disabled="!canEdit"
|
:disabled="!canEdit"
|
||||||
|
clearable
|
||||||
|
@input="clearExtraVariableError(v.variableCode)"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
@@ -465,6 +532,7 @@
|
|||||||
<div v-else class="materials-grid">
|
<div v-else class="materials-grid">
|
||||||
<div
|
<div
|
||||||
v-for="mat in materialRequirements"
|
v-for="mat in materialRequirements"
|
||||||
|
:id="`material-requirement-${mat.materialTypeId}`"
|
||||||
:key="mat.materialTypeId"
|
:key="mat.materialTypeId"
|
||||||
class="material-item-card"
|
class="material-item-card"
|
||||||
>
|
>
|
||||||
@@ -587,6 +655,7 @@
|
|||||||
<div v-else class="contracts-list">
|
<div v-else class="contracts-list">
|
||||||
<div
|
<div
|
||||||
v-for="c in contracts"
|
v-for="c in contracts"
|
||||||
|
:id="`contract-confirm-${c.id}`"
|
||||||
:key="c.id"
|
:key="c.id"
|
||||||
class="contract-sign-card"
|
class="contract-sign-card"
|
||||||
>
|
>
|
||||||
@@ -673,13 +742,24 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 { useRoute } from "vue-router";
|
||||||
import { ElMessage, ElMessageBox, FormInstance, FormRules } from "element-plus";
|
import { ElMessage, ElMessageBox, FormInstance, FormRules } from "element-plus";
|
||||||
import {
|
import {
|
||||||
OnboardingTask,
|
OnboardingTask,
|
||||||
onboardingPublicApi
|
onboardingPublicApi
|
||||||
} from "@/api/modules/biz/onboarding";
|
} from "@/api/modules/biz/onboarding";
|
||||||
|
import {
|
||||||
|
getIdCardBirthday,
|
||||||
|
getIdCardValidationMessage,
|
||||||
|
isBankCardNumber,
|
||||||
|
isContactPhone,
|
||||||
|
isEmail,
|
||||||
|
isMobilePhone,
|
||||||
|
isNumberInRange,
|
||||||
|
normalizeBankCardNumber,
|
||||||
|
normalizeIdCardNumber
|
||||||
|
} from "@/utils/onboardingValidation";
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const token = ref((route.query.token as string) || "");
|
const token = ref((route.query.token as string) || "");
|
||||||
@@ -689,6 +769,8 @@ const originalViewportContent = viewportMeta?.content;
|
|||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const submitting = ref(false);
|
const submitting = ref(false);
|
||||||
const error = ref("");
|
const error = ref("");
|
||||||
|
const currentTimestamp = ref(Date.now());
|
||||||
|
let linkExpiryTimer: number | undefined;
|
||||||
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";
|
type ContractPrepareStatus = "idle" | "preparing" | "ready" | "error";
|
||||||
@@ -745,18 +827,112 @@ const familyMembers = ref<{ name: string; relationship: string; age: string; com
|
|||||||
// 2. 特殊合同增量变量 (差集)
|
// 2. 特殊合同增量变量 (差集)
|
||||||
const customVariables = reactive<Record<string, string>>({});
|
const customVariables = reactive<Record<string, string>>({});
|
||||||
|
|
||||||
// 表单验证规则
|
function validateEmployeeName(_rule: any, value: unknown, callback: (error?: Error) => void) {
|
||||||
|
const employeeName = String(value ?? "").trim();
|
||||||
|
if (!employeeName) {
|
||||||
|
return callback(new Error("请输入真实姓名"));
|
||||||
|
}
|
||||||
|
if (employeeName.length > 100) {
|
||||||
|
return callback(new Error("姓名不能超过100个字符"));
|
||||||
|
}
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
function validatePhone(_rule: any, value: unknown, callback: (error?: Error) => void) {
|
||||||
|
if (!isMobilePhone(value)) {
|
||||||
|
return callback(new Error("请输入正确的11位手机号码"));
|
||||||
|
}
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateIdCardNumber(_rule: any, value: unknown, callback: (error?: Error) => void) {
|
||||||
|
const message = getIdCardValidationMessage(value);
|
||||||
|
if (message) {
|
||||||
|
return callback(new Error(message));
|
||||||
|
}
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateBirthday(_rule: any, value: unknown, callback: (error?: Error) => void) {
|
||||||
|
if (!value) {
|
||||||
|
return callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
const birthday = String(value);
|
||||||
|
const idCardBirthday = getIdCardBirthday(form.idCardNumber);
|
||||||
|
if (idCardBirthday && birthday !== idCardBirthday) {
|
||||||
|
return callback(new Error("出生日期与身份证号码不一致"));
|
||||||
|
}
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateBankCardNumber(_rule: any, value: unknown, callback: (error?: Error) => void) {
|
||||||
|
if (!isBankCardNumber(value)) {
|
||||||
|
return callback(new Error("请输入12至19位银行卡号"));
|
||||||
|
}
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateOptionalEmail(_rule: any, value: unknown, callback: (error?: Error) => void) {
|
||||||
|
if (value && !isEmail(value)) {
|
||||||
|
return callback(new Error("请输入正确的电子邮箱"));
|
||||||
|
}
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateOptionalHeight(_rule: any, value: unknown, callback: (error?: Error) => void) {
|
||||||
|
if (value && !isNumberInRange(value, 100, 250)) {
|
||||||
|
return callback(new Error("身高应为100至250厘米之间的数字"));
|
||||||
|
}
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateOptionalWeight(_rule: any, value: unknown, callback: (error?: Error) => void) {
|
||||||
|
if (value && !isNumberInRange(value, 30, 300)) {
|
||||||
|
return callback(new Error("体重应为30至300公斤之间的数字"));
|
||||||
|
}
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateOptionalContactPhone(_rule: any, value: unknown, callback: (error?: Error) => void) {
|
||||||
|
if (value && !isContactPhone(value)) {
|
||||||
|
return callback(new Error("请输入正确的手机号码或座机号码"));
|
||||||
|
}
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 固定登记表字段在前端即时校验;后端仍会执行相同的核心规则作为安全兜底。
|
||||||
const regRules: FormRules = {
|
const regRules: FormRules = {
|
||||||
employeeName: [{ required: true, message: "请输入真实姓名", trigger: "blur" }],
|
employeeName: [{ required: true, validator: validateEmployeeName, trigger: "blur" }],
|
||||||
gender: [{ required: true, message: "请选择性别", trigger: "change" }],
|
gender: [{ required: true, message: "请选择性别", trigger: "change" }],
|
||||||
phone: [{ required: true, message: "请输入联系电话", trigger: "blur" }],
|
birthday: [{ validator: validateBirthday, trigger: "change" }],
|
||||||
idCardNumber: [{ required: true, message: "请输入身份证号码", trigger: "blur" }],
|
height: [{ validator: validateOptionalHeight, trigger: "blur" }],
|
||||||
bankCardNumber: [{ required: true, message: "请输入工资银行卡号", trigger: "blur" }],
|
weight: [{ validator: validateOptionalWeight, trigger: "blur" }],
|
||||||
bankName: [{ required: true, message: "请输入开户银行名称", trigger: "blur" }]
|
phone: [{ required: true, validator: validatePhone, trigger: "blur" }],
|
||||||
|
email: [{ validator: validateOptionalEmail, trigger: "blur" }],
|
||||||
|
idCardNumber: [{ required: true, validator: validateIdCardNumber, trigger: "blur" }],
|
||||||
|
bankCardNumber: [{ required: true, validator: validateBankCardNumber, trigger: "blur" }],
|
||||||
|
bankName: [
|
||||||
|
{ required: true, message: "请输入开户银行名称", trigger: "blur" },
|
||||||
|
{ max: 100, message: "开户银行名称不能超过100个字符", trigger: "blur" }
|
||||||
|
],
|
||||||
|
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(() => {
|
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;
|
||||||
});
|
});
|
||||||
|
|
||||||
/** 当前正在校验变量并生成文件的合同。 */
|
/** 当前正在校验变量并生成文件的合同。 */
|
||||||
@@ -810,6 +986,190 @@ function addFamily() {
|
|||||||
familyMembers.value.push({ name: "", relationship: "", age: "", company: "", phone: "" });
|
familyMembers.value.push({ name: "", relationship: "", age: "", company: "", phone: "" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const extraVariableErrors = reactive<Record<string, string>>({});
|
||||||
|
|
||||||
|
function getVariableDataType(variable: any) {
|
||||||
|
return String(variable?.dataType || "string").trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVariableOptions(variable: any) {
|
||||||
|
if (!variable?.optionsJson) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const options = JSON.parse(variable.optionsJson);
|
||||||
|
if (!Array.isArray(options)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return options
|
||||||
|
.map((option: any) => {
|
||||||
|
if (option && typeof option === "object") {
|
||||||
|
const value = option.value ?? option.label;
|
||||||
|
const label = option.label ?? option.value;
|
||||||
|
return value == null
|
||||||
|
? null
|
||||||
|
: { value: String(value), label: String(label ?? value) };
|
||||||
|
}
|
||||||
|
|
||||||
|
return option == null
|
||||||
|
? null
|
||||||
|
: { value: String(option), label: String(option) };
|
||||||
|
})
|
||||||
|
.filter(Boolean) as { value: string; label: string }[];
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearExtraVariableError(variableCode: string) {
|
||||||
|
extraVariableErrors[variableCode] = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function isIsoDate(value: string) {
|
||||||
|
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
|
||||||
|
if (!match) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const date = new Date(
|
||||||
|
Number(match[1]),
|
||||||
|
Number(match[2]) - 1,
|
||||||
|
Number(match[3])
|
||||||
|
);
|
||||||
|
return date.getFullYear() === Number(match[1])
|
||||||
|
&& date.getMonth() === Number(match[2]) - 1
|
||||||
|
&& 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 [index, variable] of extraVariables.value.entries()) {
|
||||||
|
const variableCode = variable.variableCode;
|
||||||
|
const variableName = variable.variableName || variableCode;
|
||||||
|
const value = String(customVariables[variableCode] ?? "").trim();
|
||||||
|
const options = getVariableOptions(variable);
|
||||||
|
const dataType = getVariableDataType(variable);
|
||||||
|
customVariables[variableCode] = value;
|
||||||
|
extraVariableErrors[variableCode] = "";
|
||||||
|
|
||||||
|
if (!value && variable.required) {
|
||||||
|
extraVariableErrors[variableCode] = `请填写${variableName}`;
|
||||||
|
} else if (value && options.length && !options.some(option => option.value === value)) {
|
||||||
|
extraVariableErrors[variableCode] = `${variableName}必须从配置选项中选择`;
|
||||||
|
} else if (value && dataType === "number" && !/^-?\d+(\.\d+)?$/.test(value)) {
|
||||||
|
extraVariableErrors[variableCode] = `${variableName}必须填写数字`;
|
||||||
|
} else if (value && dataType === "date" && !isIsoDate(value)) {
|
||||||
|
extraVariableErrors[variableCode] = `${variableName}必须填写正确日期`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!firstMessage && extraVariableErrors[variableCode]) {
|
||||||
|
firstMessage = extraVariableErrors[variableCode];
|
||||||
|
firstTargetId = `extra-variable-${index}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!firstMessage) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ElMessage.warning(firstMessage);
|
||||||
|
locateValidationTarget(firstTargetId);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateRegistrationLists() {
|
||||||
|
for (let index = 0; index < educationHistory.value.length; index++) {
|
||||||
|
const row = educationHistory.value[index];
|
||||||
|
const requiredFields = [
|
||||||
|
["timeRange", "时间范围"],
|
||||||
|
["location", "学校或机构"],
|
||||||
|
["content", "专业或学习内容"],
|
||||||
|
["result", "结果或学位"]
|
||||||
|
] as const;
|
||||||
|
const missing = requiredFields.find(([field]) => !String(row[field] || "").trim());
|
||||||
|
if (missing) {
|
||||||
|
ElMessage.warning(`第 ${index + 1} 条教育经历请填写${missing[1]},或删除整条经历`);
|
||||||
|
locateValidationTarget(`education-history-${index}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let index = 0; index < workHistory.value.length; index++) {
|
||||||
|
const row = workHistory.value[index];
|
||||||
|
const requiredFields = [
|
||||||
|
["timeRange", "起止时间"],
|
||||||
|
["companyName", "工作单位"],
|
||||||
|
["position", "担任职务"],
|
||||||
|
["leaveReason", "离职原因"]
|
||||||
|
] as const;
|
||||||
|
const missing = requiredFields.find(([field]) => !String(row[field] || "").trim());
|
||||||
|
if (missing) {
|
||||||
|
ElMessage.warning(`第 ${index + 1} 条工作经历请填写${missing[1]},或删除整条经历`);
|
||||||
|
locateValidationTarget(`work-history-${index}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let index = 0; index < familyMembers.value.length; index++) {
|
||||||
|
const row = familyMembers.value[index];
|
||||||
|
const requiredFields = [
|
||||||
|
["name", "姓名"],
|
||||||
|
["relationship", "关系"],
|
||||||
|
["age", "年龄"],
|
||||||
|
["phone", "联系电话"]
|
||||||
|
] as const;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
function getUploadedMaterials(matId: number) {
|
function getUploadedMaterials(matId: number) {
|
||||||
return uploadedMaterials.value.filter(item => item.materialTypeId === matId);
|
return uploadedMaterials.value.filter(item => item.materialTypeId === matId);
|
||||||
}
|
}
|
||||||
@@ -915,6 +1275,11 @@ async function load(silent = false) {
|
|||||||
materialRequirements.value = data.materialRequirements || [];
|
materialRequirements.value = data.materialRequirements || [];
|
||||||
uploadedMaterials.value = data.materials || [];
|
uploadedMaterials.value = data.materials || [];
|
||||||
contractVariables.value = data.contractVariables || [];
|
contractVariables.value = data.contractVariables || [];
|
||||||
|
contractVariables.value.forEach((variable: any) => {
|
||||||
|
if (!(variable.variableCode in customVariables) && variable.defaultValue != null) {
|
||||||
|
customVariables[variable.variableCode] = String(variable.defaultValue);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 回填入职登记表
|
// 回填入职登记表
|
||||||
if (data.employeeInfo) {
|
if (data.employeeInfo) {
|
||||||
@@ -942,7 +1307,13 @@ async function load(silent = false) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function saveCurrentEmployeeInfo() {
|
async function saveCurrentEmployeeInfo() {
|
||||||
form.bankAccountName = form.employeeName; // 开户名自动对齐姓名
|
form.employeeName = String(form.employeeName || "").trim();
|
||||||
|
form.phone = String(form.phone || "").trim();
|
||||||
|
form.email = String(form.email || "").trim();
|
||||||
|
form.idCardNumber = normalizeIdCardNumber(form.idCardNumber);
|
||||||
|
form.bankCardNumber = normalizeBankCardNumber(form.bankCardNumber);
|
||||||
|
form.bankName = String(form.bankName || "").trim();
|
||||||
|
form.bankAccountName = form.employeeName; // 开户名始终与员工姓名保持一致
|
||||||
const payload = {
|
const payload = {
|
||||||
...form,
|
...form,
|
||||||
token: token.value,
|
token: token.value,
|
||||||
@@ -954,21 +1325,56 @@ async function saveCurrentEmployeeInfo() {
|
|||||||
await onboardingPublicApi.saveEmployeeInfo(payload);
|
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() {
|
async function nextStep() {
|
||||||
if (currentStep.value === 0) {
|
if (currentStep.value === 0) {
|
||||||
await regFormRef.value?.validate();
|
if (!await validateRegistrationForm()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!validateRegistrationLists()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (canEdit.value) {
|
if (canEdit.value) {
|
||||||
await saveCurrentEmployeeInfo();
|
await saveCurrentEmployeeInfo();
|
||||||
ElMessage.success("入职登记表已临时保存");
|
ElMessage.success("入职登记表已临时保存");
|
||||||
}
|
}
|
||||||
} else if (currentStep.value === 1) {
|
} else if (currentStep.value === 1) {
|
||||||
|
if (!validateExtraVariables()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (canEdit.value) {
|
if (canEdit.value) {
|
||||||
await saveCurrentEmployeeInfo();
|
await saveCurrentEmployeeInfo();
|
||||||
}
|
}
|
||||||
} else if (currentStep.value === 2) {
|
} else if (currentStep.value === 2) {
|
||||||
const missing = materialRequirements.value.filter(m => m.required && !getUploadedMaterials(m.materialTypeId).length);
|
const missing = materialRequirements.value.filter(m => m.required && !getUploadedMaterials(m.materialTypeId).length);
|
||||||
if (missing.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++;
|
currentStep.value++;
|
||||||
@@ -1179,17 +1585,23 @@ function previewAndDownloadContract(c: any) {
|
|||||||
async function submitAll() {
|
async function submitAll() {
|
||||||
const unprepared = contracts.value.filter(c => !isContractPrepared(c.id));
|
const unprepared = contracts.value.filter(c => !isContractPrepared(c.id));
|
||||||
if (unprepared.length) {
|
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);
|
const unviewed = contracts.value.filter(c => !c.hasViewed);
|
||||||
if (unviewed.length) {
|
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);
|
const unconfirmed = contracts.value.filter(c => !c.isAgreed);
|
||||||
if (unconfirmed.length) {
|
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(
|
const confirmed = await isMessageBoxConfirmed(
|
||||||
@@ -1229,10 +1641,18 @@ onMounted(() => {
|
|||||||
"viewport-fit=cover"
|
"viewport-fit=cover"
|
||||||
].join(", ");
|
].join(", ");
|
||||||
}
|
}
|
||||||
|
currentTimestamp.value = Date.now();
|
||||||
|
linkExpiryTimer = window.setInterval(() => {
|
||||||
|
currentTimestamp.value = Date.now();
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
load();
|
load();
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
|
if (linkExpiryTimer !== undefined) {
|
||||||
|
window.clearInterval(linkExpiryTimer);
|
||||||
|
}
|
||||||
if (viewportMeta && originalViewportContent) {
|
if (viewportMeta && originalViewportContent) {
|
||||||
viewportMeta.content = originalViewportContent;
|
viewportMeta.content = originalViewportContent;
|
||||||
}
|
}
|
||||||
@@ -1276,6 +1696,7 @@ onBeforeUnmount(() => {
|
|||||||
.brand-subtitle {
|
.brand-subtitle {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
opacity: 0.92;
|
opacity: 0.92;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1307,6 +1728,7 @@ onBeforeUnmount(() => {
|
|||||||
color: #64748b;
|
color: #64748b;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.link-expired-alert,
|
||||||
.reject-reason-alert {
|
.reject-reason-alert {
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
@@ -1346,6 +1768,7 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
.reg-subtitle {
|
.reg-subtitle {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
@@ -1555,18 +1978,25 @@ onBeforeUnmount(() => {
|
|||||||
padding: 18px;
|
padding: 18px;
|
||||||
background: #f8fafc;
|
background: #f8fafc;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.contract-meta {
|
.contract-meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.contract-title {
|
.contract-title {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
|
min-width: 0;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
.contract-desc {
|
.contract-desc {
|
||||||
@@ -1591,10 +2021,32 @@ onBeforeUnmount(() => {
|
|||||||
gap: 12px;
|
gap: 12px;
|
||||||
border-top: 1px dashed #cbd5e1;
|
border-top: 1px dashed #cbd5e1;
|
||||||
padding-top: 14px;
|
padding-top: 14px;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.agree-checkbox {
|
.agree-checkbox {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
margin-right: 0;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agree-checkbox :deep(.el-checkbox__input) {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agree-checkbox :deep(.el-checkbox__label) {
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
padding-left: 8px;
|
||||||
|
white-space: normal;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
word-break: break-word;
|
||||||
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.contract-view-hint {
|
.contract-view-hint {
|
||||||
@@ -1749,5 +2201,14 @@ onBeforeUnmount(() => {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
.contract-sign-card {
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
|
.contract-card-bottom > .el-button,
|
||||||
|
.contract-card-bottom > .agree-checkbox {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user