11
This commit is contained in:
@@ -10,7 +10,12 @@ export interface OnboardingTask {
|
|||||||
taskNo: string;
|
taskNo: string;
|
||||||
companyId: number;
|
companyId: number;
|
||||||
positionId: number;
|
positionId: number;
|
||||||
|
onboardingTemplateId?: number;
|
||||||
|
companyName?: string;
|
||||||
|
positionName?: string;
|
||||||
|
onboardingTemplateName?: string;
|
||||||
taskStatus: string;
|
taskStatus: string;
|
||||||
|
rejectReason?: string;
|
||||||
linkExpireAt?: string;
|
linkExpireAt?: string;
|
||||||
submitCount: number;
|
submitCount: number;
|
||||||
createTime: string;
|
createTime: string;
|
||||||
@@ -87,7 +92,7 @@ export interface OnboardingTaskContractVariableItem {
|
|||||||
variableName: string;
|
variableName: string;
|
||||||
variableType: string;
|
variableType: string;
|
||||||
dataType: string;
|
dataType: string;
|
||||||
variableValue?: string;
|
variableValue?: string | number;
|
||||||
required: boolean;
|
required: boolean;
|
||||||
hrEditable: boolean;
|
hrEditable: boolean;
|
||||||
editable: boolean;
|
editable: boolean;
|
||||||
@@ -179,6 +184,10 @@ export const onboardingTaskApi = {
|
|||||||
return taskHttp.get("detail", params);
|
return taskHttp.get("detail", params);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getTaskLink(params: ReqId) {
|
||||||
|
return taskHttp.get<any>("get-link", params);
|
||||||
|
},
|
||||||
|
|
||||||
/** 使用后台登录令牌读取文件流,供入职材料下载和图片预览使用。 */
|
/** 使用后台登录令牌读取文件流,供入职材料下载和图片预览使用。 */
|
||||||
downloadFile(fileId: number) {
|
downloadFile(fileId: number) {
|
||||||
return fileHttp.get<any>(
|
return fileHttp.get<any>(
|
||||||
@@ -550,6 +559,10 @@ export const onboardingPublicApi = {
|
|||||||
contractSourceUrl(params: { token: string; taskContractId: number }) {
|
contractSourceUrl(params: { token: string; taskContractId: number }) {
|
||||||
return `/api/open/onboarding/contract-source?token=${encodeURIComponent(params.token)}&taskContractId=${params.taskContractId}`;
|
return `/api/open/onboarding/contract-source?token=${encodeURIComponent(params.token)}&taskContractId=${params.taskContractId}`;
|
||||||
},
|
},
|
||||||
|
materialSourceUrl(params: { token: string; taskMaterialId: number; fileId?: number }) {
|
||||||
|
const version = params.fileId ? `&v=${params.fileId}` : "";
|
||||||
|
return `/api/open/onboarding/material-source?token=${encodeURIComponent(params.token)}&taskMaterialId=${params.taskMaterialId}${version}`;
|
||||||
|
},
|
||||||
deleteMaterial(params: { token: string; taskMaterialId: number }) {
|
deleteMaterial(params: { token: string; taskMaterialId: number }) {
|
||||||
return publicHttp.post("delete-material", params);
|
return publicHttp.post("delete-material", params);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,6 +67,16 @@
|
|||||||
合同参数
|
合同参数
|
||||||
</el-button>
|
</el-button>
|
||||||
|
|
||||||
|
<el-button
|
||||||
|
v-if="['pending_fill', 'rejected'].includes(scope.row.taskStatus)"
|
||||||
|
link
|
||||||
|
type="warning"
|
||||||
|
icon="CopyDocument"
|
||||||
|
@click="copyOrShareTaskLink(scope.row)"
|
||||||
|
>
|
||||||
|
复制链接
|
||||||
|
</el-button>
|
||||||
|
|
||||||
<el-button
|
<el-button
|
||||||
v-if="scope.row.taskStatus === 'reviewing'"
|
v-if="scope.row.taskStatus === 'reviewing'"
|
||||||
link
|
link
|
||||||
@@ -310,39 +320,41 @@
|
|||||||
@closed="clearMaterialPreviews"
|
@closed="clearMaterialPreviews"
|
||||||
>
|
>
|
||||||
<div v-if="detail" class="drawer-content">
|
<div v-if="detail" class="drawer-content">
|
||||||
<!-- 头部状态栏 (100% 满宽卡片) -->
|
<!-- 头部固定状态栏 (100% 满宽卡片) -->
|
||||||
<div class="drawer-status-banner">
|
<div class="drawer-sticky-header">
|
||||||
<div class="banner-left">
|
<div class="drawer-status-banner">
|
||||||
<div class="banner-title-row">
|
<div class="banner-left">
|
||||||
<span class="task-no-large">{{ detail.task.taskNo }}</span>
|
<div class="banner-title-row">
|
||||||
<el-tag :type="statusType(detail.task.taskStatus)" size="large" effect="dark">
|
<span class="task-no-large">{{ detail.task.taskNo }}</span>
|
||||||
{{ statusText(detail.task.taskStatus) }}
|
<el-tag :type="statusType(detail.task.taskStatus)" size="large" effect="dark">
|
||||||
</el-tag>
|
{{ statusText(detail.task.taskStatus) }}
|
||||||
<span v-if="detail.task.submitCount > 0" class="submit-count-badge">
|
</el-tag>
|
||||||
第 {{ detail.task.submitCount }} 次提交
|
<span v-if="detail.task.submitCount > 0" class="submit-count-badge">
|
||||||
</span>
|
第 {{ detail.task.submitCount }} 次提交
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="banner-meta-row">
|
||||||
|
<span>所属公司:<strong>{{ detail.task.companyName || '-' }}</strong></span>
|
||||||
|
<span>入职岗位:<strong>{{ detail.task.positionName || '-' }}</strong></span>
|
||||||
|
<span>入职模板:<strong>{{ detail.task.onboardingTemplateName || '-' }}</strong></span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="banner-meta-row">
|
<div class="banner-right">
|
||||||
<span>所属公司:<strong>{{ detail.task.companyName || '-' }}</strong></span>
|
<div class="meta-item">发起时间:{{ formatTime(detail.task.createTime) }}</div>
|
||||||
<span>入职岗位:<strong>{{ detail.task.positionName || '-' }}</strong></span>
|
<div v-if="detail.task.linkExpireAt" class="meta-item">链接有效期至:{{ formatTime(detail.task.linkExpireAt) }}</div>
|
||||||
<span>入职模板:<strong>{{ detail.task.onboardingTemplateName || '-' }}</strong></span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="banner-right">
|
|
||||||
<div class="meta-item">发起时间:{{ formatTime(detail.task.createTime) }}</div>
|
|
||||||
<div v-if="detail.task.linkExpireAt" class="meta-item">链接有效期至:{{ formatTime(detail.task.linkExpireAt) }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 驳回说明 -->
|
<!-- 驳回说明 -->
|
||||||
<el-alert
|
<el-alert
|
||||||
v-if="detail.task.rejectReason && detail.task.taskStatus === 'rejected'"
|
v-if="detail.task.rejectReason && detail.task.taskStatus === 'rejected'"
|
||||||
:title="`最近驳回原因:${detail.task.rejectReason}`"
|
:title="`最近驳回原因:${detail.task.rejectReason}`"
|
||||||
type="warning"
|
type="warning"
|
||||||
show-icon
|
show-icon
|
||||||
:closable="false"
|
:closable="false"
|
||||||
class="mb-4"
|
class="mb-3"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Tab 切换 -->
|
<!-- Tab 切换 -->
|
||||||
<el-tabs v-model="activeDetailTab" class="detail-tabs">
|
<el-tabs v-model="activeDetailTab" class="detail-tabs">
|
||||||
@@ -654,17 +666,6 @@
|
|||||||
<!-- 抽屉底部操作栏 -->
|
<!-- 抽屉底部操作栏 -->
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div v-if="detail" class="drawer-footer-actions">
|
<div v-if="detail" class="drawer-footer-actions">
|
||||||
<div class="drawer-footer-left">
|
|
||||||
<el-button
|
|
||||||
v-if="['pending_fill', 'rejected'].includes(detail.task.taskStatus)"
|
|
||||||
plain
|
|
||||||
type="primary"
|
|
||||||
icon="Setting"
|
|
||||||
@click="openContractVariables(detail.task)"
|
|
||||||
>
|
|
||||||
配置合同参数
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
<div class="drawer-footer-right">
|
<div class="drawer-footer-right">
|
||||||
<el-button
|
<el-button
|
||||||
v-if="detail.task.taskStatus === 'reviewing'"
|
v-if="detail.task.taskStatus === 'reviewing'"
|
||||||
@@ -711,68 +712,144 @@
|
|||||||
@close="closeImageViewer"
|
@close="closeImageViewer"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 合同参数弹窗 -->
|
<!-- 合同参数录入弹窗 -->
|
||||||
<el-dialog
|
<el-dialog
|
||||||
v-model="contractVariablesVisible"
|
v-model="contractVariablesVisible"
|
||||||
:title="`${contractVariableTask?.taskNo || ''} · 合同参数录入`"
|
:title="`${contractVariableTask?.taskNo || ''} · 合同参数录入`"
|
||||||
width="920px"
|
width="1180px"
|
||||||
|
top="4vh"
|
||||||
destroy-on-close
|
destroy-on-close
|
||||||
>
|
>
|
||||||
<el-alert
|
<div class="contract-vars-dialog-body">
|
||||||
title="请填写薪资、补贴等可编辑人事变量,填写完成后即可发送给候选人。员工与系统参数由系统自动生成替换。"
|
<el-alert
|
||||||
type="info"
|
title="说明:此处展示本次入职任务关联的所有合同变量。带有【人事变量】标签的项为本次需要 HR 填写的核心条款(如薪资、试用期、合同起止日等);员工变量与公司变量将在流程中自动带入。"
|
||||||
show-icon
|
type="info"
|
||||||
:closable="false"
|
show-icon
|
||||||
class="mb-4"
|
:closable="false"
|
||||||
/>
|
style="margin-bottom: 16px;"
|
||||||
|
/>
|
||||||
|
|
||||||
<el-empty v-if="!contractVariableGroups.length" description="当前任务合同无需配置人事变量" />
|
<el-empty v-if="!contractVariableGroups.length" description="当前任务合同无需配置人事变量" />
|
||||||
|
|
||||||
<el-card
|
<div v-else>
|
||||||
v-for="group in contractVariableGroups"
|
<!-- 顶部合同分组切换 Tab -->
|
||||||
:key="group.taskContractId"
|
<el-tabs v-model="activeContractGroupTab" type="card" class="contract-group-tabs">
|
||||||
:header="group.contractName"
|
<el-tab-pane
|
||||||
shadow="never"
|
v-for="(group, gIdx) in contractVariableGroups"
|
||||||
class="contract-card mb-4"
|
:key="group.taskContractId"
|
||||||
>
|
:label="group.contractName"
|
||||||
<el-table :data="group.variables" border stripe>
|
:name="String(gIdx)"
|
||||||
<el-table-column prop="variableCode" label="变量编码" width="160" />
|
>
|
||||||
<el-table-column prop="variableName" label="变量名称" width="160" />
|
<div class="contract-group-pane">
|
||||||
<el-table-column prop="variableType" label="类型" width="100">
|
<!-- 变量类型过滤栏 + 单合同保存按钮 -->
|
||||||
<template #default="scope">
|
<div class="pane-header-bar">
|
||||||
<el-tag size="small" :type="variableTypeTag(scope.row.variableType)">
|
<div class="filter-radio-group">
|
||||||
{{ variableTypeText(scope.row.variableType) }}
|
<span class="filter-label">变量分类筛选:</span>
|
||||||
</el-tag>
|
<el-radio-group v-model="varTypeFilter" size="small">
|
||||||
</template>
|
<el-radio-button label="all">全部 ({{ group.variables.length }})</el-radio-button>
|
||||||
</el-table-column>
|
<el-radio-button label="contract">
|
||||||
<el-table-column label="参数设定值" min-width="260">
|
💼 人事参数 ({{ countVarType(group.variables, 'contract') }})
|
||||||
<template #default="scope">
|
</el-radio-button>
|
||||||
<el-input
|
<el-radio-button label="employee">
|
||||||
v-if="scope.row.editable"
|
👤 员工填写 ({{ countVarType(group.variables, 'employee') }})
|
||||||
v-model="scope.row.variableValue"
|
</el-radio-button>
|
||||||
:placeholder="scope.row.required ? '必填人事参数' : '可选参数'"
|
<el-radio-button label="company">
|
||||||
clearable
|
🏢 公司变量 ({{ countVarType(group.variables, 'company') }})
|
||||||
/>
|
</el-radio-button>
|
||||||
<span v-else class="text-secondary">
|
<el-radio-button label="system">
|
||||||
{{ scope.row.variableValue || "自动带入" }}
|
⚙️ 系统预置 ({{ countVarType(group.variables, 'system') }})
|
||||||
</span>
|
</el-radio-button>
|
||||||
</template>
|
</el-radio-group>
|
||||||
</el-table-column>
|
</div>
|
||||||
<el-table-column label="必填" width="80" align="center">
|
|
||||||
<template #default="scope">
|
|
||||||
<el-tag size="small" :type="scope.row.required ? 'danger' : 'info'">
|
|
||||||
{{ scope.row.required ? "是" : "否" }}
|
|
||||||
</el-tag>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
|
|
||||||
<div class="card-actions">
|
<el-button
|
||||||
<el-button type="primary" icon="Check" @click="saveContractVariables(group)">
|
type="primary"
|
||||||
保存《{{ group.contractName }}》参数
|
size="small"
|
||||||
|
icon="Check"
|
||||||
|
@click="saveContractVariables(group)"
|
||||||
|
>
|
||||||
|
保存《{{ group.contractName }}》参数
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 变量表格,限制高度,带上下滚动条 -->
|
||||||
|
<el-table
|
||||||
|
:data="getFilteredVariables(group.variables)"
|
||||||
|
border
|
||||||
|
stripe
|
||||||
|
max-height="450px"
|
||||||
|
class="contract-vars-table"
|
||||||
|
>
|
||||||
|
<el-table-column prop="variableCode" label="变量编码" min-width="200">
|
||||||
|
<template #default="scope">
|
||||||
|
<code class="code-font">{{ scope.row.variableCode }}</code>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="variableName" label="变量名称" min-width="180">
|
||||||
|
<template #default="scope">
|
||||||
|
<span class="font-bold text-slate-800">{{ scope.row.variableName }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="variableType" label="类型" width="150" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tag size="small" :type="variableTypeTag(scope.row.variableType)">
|
||||||
|
{{ variableTypeText(scope.row.variableType) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="必填" width="90" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tag size="small" :type="scope.row.required ? 'danger' : 'info'" effect="plain">
|
||||||
|
{{ scope.row.required ? "必填" : "选填" }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="参数设定值 (HR录入 / 自动生成)" min-width="280">
|
||||||
|
<template #default="scope">
|
||||||
|
<div v-if="scope.row.editable" class="editable-cell">
|
||||||
|
<el-date-picker
|
||||||
|
v-if="scope.row.dataType === 'date'"
|
||||||
|
v-model="scope.row.variableValue"
|
||||||
|
type="date"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
placeholder="选择日期"
|
||||||
|
style="width: 100%;"
|
||||||
|
/>
|
||||||
|
<el-input-number
|
||||||
|
v-else-if="scope.row.dataType === 'number' || scope.row.dataType === 'decimal'"
|
||||||
|
v-model="scope.row.variableValue"
|
||||||
|
controls-position="right"
|
||||||
|
placeholder="输入金额或数值"
|
||||||
|
style="width: 100%;"
|
||||||
|
/>
|
||||||
|
<el-input
|
||||||
|
v-else
|
||||||
|
v-model="scope.row.variableValue"
|
||||||
|
:placeholder="scope.row.required ? '请输入必填人事参数' : '请输入参数设定值'"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-else class="readonly-val-cell">
|
||||||
|
<span v-if="scope.row.variableValue" class="text-primary font-medium">{{ scope.row.variableValue }}</span>
|
||||||
|
<span v-else class="text-placeholder">系统自动替换</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer-flex">
|
||||||
|
<el-button @click="contractVariablesVisible = false">关闭</el-button>
|
||||||
|
<el-button type="primary" icon="Check" :loading="savingAllVars" @click="saveAllContractVariables">
|
||||||
|
一键保存全部合同参数
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -875,6 +952,20 @@ const detailCustomVars = computed(() => {
|
|||||||
const contractVariablesVisible = ref(false);
|
const contractVariablesVisible = ref(false);
|
||||||
const contractVariableTask = ref<OnboardingTask>();
|
const contractVariableTask = ref<OnboardingTask>();
|
||||||
const contractVariableGroups = ref<OnboardingTaskContractVariableGroup[]>([]);
|
const contractVariableGroups = ref<OnboardingTaskContractVariableGroup[]>([]);
|
||||||
|
const activeContractGroupTab = ref("0");
|
||||||
|
const varTypeFilter = ref("all");
|
||||||
|
const savingAllVars = ref(false);
|
||||||
|
|
||||||
|
function countVarType(variables: any[], type: string) {
|
||||||
|
if (!variables) return 0;
|
||||||
|
return variables.filter(v => v.variableType === type).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFilteredVariables(variables: any[]) {
|
||||||
|
if (!variables) return [];
|
||||||
|
if (varTypeFilter.value === "all") return variables;
|
||||||
|
return variables.filter(v => v.variableType === varTypeFilter.value);
|
||||||
|
}
|
||||||
|
|
||||||
// 统计数量
|
// 统计数量
|
||||||
const statusCounts = reactive({
|
const statusCounts = reactive({
|
||||||
@@ -892,7 +983,7 @@ const form = reactive({
|
|||||||
contractTemplateVersionId: number;
|
contractTemplateVersionId: number;
|
||||||
variableCode: string;
|
variableCode: string;
|
||||||
variableName: string;
|
variableName: string;
|
||||||
variableValue?: string;
|
variableValue?: string | number;
|
||||||
dataType: string;
|
dataType: string;
|
||||||
required: boolean;
|
required: boolean;
|
||||||
contractName: string;
|
contractName: string;
|
||||||
@@ -945,7 +1036,7 @@ const columns: ColumnProps<OnboardingTask>[] = [
|
|||||||
prop: "operation",
|
prop: "operation",
|
||||||
label: "操作",
|
label: "操作",
|
||||||
fixed: "right",
|
fixed: "right",
|
||||||
width: 320
|
width: 360
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -1205,18 +1296,27 @@ async function createTask() {
|
|||||||
if (!form.companyId || !form.positionId || !form.onboardingTemplateId) {
|
if (!form.companyId || !form.positionId || !form.onboardingTemplateId) {
|
||||||
return ElMessage.warning("请选择所属公司、入职岗位和入职模板");
|
return ElMessage.warning("请选择所属公司、入职岗位和入职模板");
|
||||||
}
|
}
|
||||||
const missingVariable = form.contractVariables.find(item => item.required && !String(item.variableValue || "").trim());
|
const missingVariable = form.contractVariables.find(
|
||||||
|
item => item.required && isBlankContractVariableValue(item.variableValue)
|
||||||
|
);
|
||||||
if (missingVariable) {
|
if (missingVariable) {
|
||||||
return ElMessage.warning(`请填写合同人事参数“${missingVariable.variableName}”`);
|
return ElMessage.warning(`请填写合同人事参数“${missingVariable.variableName}”`);
|
||||||
}
|
}
|
||||||
|
|
||||||
creating.value = true;
|
creating.value = true;
|
||||||
try {
|
try {
|
||||||
const { data: result }: any = await onboardingTaskApi.create(form);
|
const payload = {
|
||||||
|
...form,
|
||||||
|
contractVariables: form.contractVariables.map(item => ({
|
||||||
|
...item,
|
||||||
|
variableValue: normalizeContractVariableValue(item.variableValue)
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
const { data: result }: any = await onboardingTaskApi.create(payload);
|
||||||
createVisible.value = false;
|
createVisible.value = false;
|
||||||
proTable.value?.refresh();
|
proTable.value?.refresh();
|
||||||
|
|
||||||
const linkUrl = `${location.origin}/onboarding/fill?token=${result.linkToken}`;
|
const linkUrl = `${location.origin}/#/onboarding/fill?token=${result.linkToken}`;
|
||||||
const selectedCompany = companies.value.find(c => c.id === form.companyId)?.companyName || "";
|
const selectedCompany = companies.value.find(c => c.id === form.companyId)?.companyName || "";
|
||||||
const selectedPosition = positions.value.find(p => p.id === form.positionId)?.positionName || "";
|
const selectedPosition = positions.value.find(p => p.id === form.positionId)?.positionName || "";
|
||||||
|
|
||||||
@@ -1247,6 +1347,26 @@ function copyInviteMessage() {
|
|||||||
copyText(msg, "邀请文案已复制至剪贴板");
|
copyText(msg, "邀请文案已复制至剪贴板");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function copyOrShareTaskLink(row: OnboardingTask) {
|
||||||
|
try {
|
||||||
|
const { data }: any = await onboardingTaskApi.getTaskLink({ id: row.id });
|
||||||
|
const linkUrl = `${location.origin}/#/onboarding/fill?token=${data.linkToken}`;
|
||||||
|
|
||||||
|
createdTaskInfo.value = {
|
||||||
|
taskNo: row.taskNo,
|
||||||
|
linkUrl,
|
||||||
|
expiresAt: formatTime(data.expiresAt),
|
||||||
|
companyName: row.companyName || "公司",
|
||||||
|
positionName: row.positionName || "岗位"
|
||||||
|
};
|
||||||
|
|
||||||
|
copyText(linkUrl, "专属入职链接已复制到剪贴板!");
|
||||||
|
shareVisible.value = true;
|
||||||
|
} catch (err: any) {
|
||||||
|
ElMessage.error(err?.message || "获取专属链接失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function showDetail(row: OnboardingTask) {
|
async function showDetail(row: OnboardingTask) {
|
||||||
const { data }: any = await onboardingTaskApi.detail({ id: row.id });
|
const { data }: any = await onboardingTaskApi.detail({ id: row.id });
|
||||||
detail.value = data;
|
detail.value = data;
|
||||||
@@ -1258,13 +1378,17 @@ async function showDetail(row: OnboardingTask) {
|
|||||||
async function openContractVariables(row: OnboardingTask) {
|
async function openContractVariables(row: OnboardingTask) {
|
||||||
const { data } = await onboardingTaskApi.taskContractVariables(row.id);
|
const { data } = await onboardingTaskApi.taskContractVariables(row.id);
|
||||||
contractVariableTask.value = row;
|
contractVariableTask.value = row;
|
||||||
contractVariableGroups.value = data;
|
contractVariableGroups.value = data || [];
|
||||||
|
activeContractGroupTab.value = "0";
|
||||||
|
varTypeFilter.value = "all";
|
||||||
contractVariablesVisible.value = true;
|
contractVariablesVisible.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveContractVariables(group: OnboardingTaskContractVariableGroup) {
|
async function saveContractVariables(group: OnboardingTaskContractVariableGroup) {
|
||||||
const requiredBlank = group.variables.some(
|
const requiredBlank = group.variables.some(
|
||||||
item => item.editable && item.required && !item.variableValue?.trim()
|
item => item.editable
|
||||||
|
&& item.required
|
||||||
|
&& isBlankContractVariableValue(item.variableValue)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (requiredBlank) {
|
if (requiredBlank) {
|
||||||
@@ -1273,14 +1397,62 @@ async function saveContractVariables(group: OnboardingTaskContractVariableGroup)
|
|||||||
|
|
||||||
await onboardingTaskApi.saveTaskContractVariables({
|
await onboardingTaskApi.saveTaskContractVariables({
|
||||||
taskContractId: group.taskContractId,
|
taskContractId: group.taskContractId,
|
||||||
values: group.variables
|
values: buildContractVariableSaveValues(group)
|
||||||
.filter(item => item.editable)
|
|
||||||
.map(item => ({ id: item.id, variableValue: item.variableValue }))
|
|
||||||
});
|
});
|
||||||
|
|
||||||
ElMessage.success("《" + group.contractName + "》参数已保存");
|
ElMessage.success("《" + group.contractName + "》参数已保存");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function saveAllContractVariables() {
|
||||||
|
if (!contractVariableGroups.value.length) return;
|
||||||
|
for (const group of contractVariableGroups.value) {
|
||||||
|
const requiredBlank = group.variables.some(
|
||||||
|
item => item.editable
|
||||||
|
&& item.required
|
||||||
|
&& isBlankContractVariableValue(item.variableValue)
|
||||||
|
);
|
||||||
|
if (requiredBlank) {
|
||||||
|
return ElMessage.warning(`请填写《${group.contractName}》中的所有必填人事参数`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
savingAllVars.value = true;
|
||||||
|
try {
|
||||||
|
for (const group of contractVariableGroups.value) {
|
||||||
|
await onboardingTaskApi.saveTaskContractVariables({
|
||||||
|
taskContractId: group.taskContractId,
|
||||||
|
values: buildContractVariableSaveValues(group)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
ElMessage.success("全部合同参数已保存成功!");
|
||||||
|
contractVariablesVisible.value = false;
|
||||||
|
} finally {
|
||||||
|
savingAllVars.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 合同数值控件会返回 number,文本和日期控件返回 string;统一判断可避免数字调用 trim。
|
||||||
|
* 使用空值合并判断而不是逻辑或,确保合法数值 0 不会被当成未填写。
|
||||||
|
*/
|
||||||
|
function isBlankContractVariableValue(value: unknown) {
|
||||||
|
return value == null || String(value).trim() === "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 后端合同变量快照使用字符串存储,请求前统一规范化控件返回值。 */
|
||||||
|
function normalizeContractVariableValue(value: unknown) {
|
||||||
|
return value == null ? "" : String(value).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildContractVariableSaveValues(group: OnboardingTaskContractVariableGroup) {
|
||||||
|
return group.variables
|
||||||
|
.filter(item => item.editable)
|
||||||
|
.map(item => ({
|
||||||
|
id: item.id,
|
||||||
|
variableValue: normalizeContractVariableValue(item.variableValue)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
async function approve(row: OnboardingTask) {
|
async function approve(row: OnboardingTask) {
|
||||||
await ElMessageBox.confirm(
|
await ElMessageBox.confirm(
|
||||||
"确认审核通过?系统将自动创建正式员工档案并锁定合同快照。",
|
"确认审核通过?系统将自动创建正式员工档案并锁定合同快照。",
|
||||||
@@ -1731,12 +1903,8 @@ onMounted(loadCompanies);
|
|||||||
|
|
||||||
.drawer-footer-actions {
|
.drawer-footer-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: flex-end;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
|
||||||
|
|
||||||
.footer-action-right {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1808,13 +1976,57 @@ onMounted(loadCompanies);
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 详情抽屉顶部横幅与档案卡片样式 */
|
/* 详情抽屉顶部横幅与档案卡片样式 (头部固定 + 内容区滚动) */
|
||||||
|
:deep(.el-drawer__body) {
|
||||||
|
padding: 0 !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-content {
|
||||||
|
flex: 1;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 20px 24px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-sticky-header {
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: #ffffff;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-tabs {
|
||||||
|
flex: 1;
|
||||||
|
height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.detail-tabs > .el-tabs__header) {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
background: #fff;
|
||||||
|
z-index: 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.detail-tabs > .el-tabs__content) {
|
||||||
|
flex: 1;
|
||||||
|
height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-right: 6px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.drawer-status-banner {
|
.drawer-status-banner {
|
||||||
background: #f8fafc;
|
background: #f8fafc;
|
||||||
border: 1px solid #e2e8f0;
|
border: 1px solid #e2e8f0;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 16px 20px;
|
padding: 16px 20px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 14px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -1885,4 +2097,83 @@ onMounted(loadCompanies);
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
color: #2563eb;
|
color: #2563eb;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 合同参数录入弹窗优化 */
|
||||||
|
.contract-vars-dialog-body {
|
||||||
|
max-height: 68vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contract-group-tabs {
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contract-group-pane {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pane-header-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-radio-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-label {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contract-vars-table {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.contract-vars-table .el-input__inner) {
|
||||||
|
text-align: center !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.contract-vars-table .el-date-editor .el-input__inner) {
|
||||||
|
text-align: center !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.contract-vars-table .el-input-number .el-input__inner) {
|
||||||
|
text-align: center !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-slate-800 {
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.readonly-val-cell {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 13px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-placeholder {
|
||||||
|
color: #94a3b8;
|
||||||
|
font-style: italic;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer-flex {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
<div class="main-container">
|
<div class="main-container">
|
||||||
<el-result v-if="error" icon="error" title="访问受限" :sub-title="error">
|
<el-result v-if="error" icon="error" title="访问受限" :sub-title="error">
|
||||||
<template #extra>
|
<template #extra>
|
||||||
<el-button type="primary" @click="load">重新加载</el-button>
|
<el-button type="primary" @click="load()">重新加载</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-result>
|
</el-result>
|
||||||
|
|
||||||
@@ -34,6 +34,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 审核退回后向候选人明确展示需要补充或修改的内容。 -->
|
||||||
|
<el-alert
|
||||||
|
v-if="task.taskStatus === 'rejected'"
|
||||||
|
class="reject-reason-alert"
|
||||||
|
title="退回原因"
|
||||||
|
:description="task.rejectReason?.trim() || '资料已退回,请联系工作人员确认需要补充的内容。'"
|
||||||
|
type="warning"
|
||||||
|
show-icon
|
||||||
|
:closable="false"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- 分步向导模式 -->
|
<!-- 分步向导模式 -->
|
||||||
<div class="wizard-card">
|
<div class="wizard-card">
|
||||||
<div v-if="!canEdit" class="readonly-banner">
|
<div v-if="!canEdit" class="readonly-banner">
|
||||||
@@ -463,38 +474,92 @@
|
|||||||
<el-tag v-if="mat.required" size="small" type="danger" effect="plain">必传</el-tag>
|
<el-tag v-if="mat.required" size="small" type="danger" effect="plain">必传</el-tag>
|
||||||
<el-tag v-else size="small" type="info" effect="plain">选传</el-tag>
|
<el-tag v-else size="small" type="info" effect="plain">选传</el-tag>
|
||||||
</div>
|
</div>
|
||||||
<span class="mat-limit">最多 {{ mat.maxFileCount }} 份 · 格式:{{ mat.allowedExtensions }}</span>
|
<span class="mat-limit">{{ getMaterialLimitText(mat) }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="uploaded-files-list">
|
<div class="uploaded-files-list">
|
||||||
<div
|
<div
|
||||||
v-for="f in getUploadedMaterials(mat.materialTypeId)"
|
v-for="f in getUploadedMaterials(mat.materialTypeId)"
|
||||||
:key="f.id"
|
:key="`${f.id}-${f.fileId}`"
|
||||||
class="file-badge"
|
class="file-badge"
|
||||||
>
|
>
|
||||||
<el-icon color="#2563eb"><Document /></el-icon>
|
<!-- 图片点击缩略图即可在手机端放大预览,非图片继续显示文件图标。 -->
|
||||||
<span class="file-name">{{ f.fileName || f.materialName }}</span>
|
<el-image
|
||||||
<el-button
|
v-if="isImageFile(f)"
|
||||||
v-if="canEdit"
|
class="material-thumbnail"
|
||||||
link
|
:src="getMaterialSourceUrl(f)"
|
||||||
type="danger"
|
:preview-src-list="[getMaterialSourceUrl(f)]"
|
||||||
icon="Close"
|
fit="cover"
|
||||||
@click="deleteFile(f.id)"
|
preview-teleported
|
||||||
/>
|
/>
|
||||||
|
<el-icon v-else class="material-file-icon" color="#2563eb"><Document /></el-icon>
|
||||||
|
|
||||||
|
<div class="file-content">
|
||||||
|
<span class="file-name" :title="f.fileName || f.materialName">
|
||||||
|
{{ f.fileName || f.materialName }}
|
||||||
|
</span>
|
||||||
|
<span v-if="f.fileSizeInfo" class="file-size">{{ f.fileSizeInfo }}</span>
|
||||||
|
<span v-if="isImageFile(f)" class="preview-tip">点击图片可放大预览</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="canEdit" class="file-actions">
|
||||||
|
<el-upload
|
||||||
|
:action="getMaterialUploadUrl(mat.materialTypeId, f.id)"
|
||||||
|
:accept="getUploadAccept(mat)"
|
||||||
|
:disabled="isMaterialUploading(mat.materialTypeId)"
|
||||||
|
:show-file-list="false"
|
||||||
|
:on-success="(response: any) => onUploadSuccess(response, mat.materialTypeId)"
|
||||||
|
:on-error="(error: Error) => onUploadError(error, mat.materialTypeId)"
|
||||||
|
:before-upload="(file: File) => beforeUpload(file, mat)"
|
||||||
|
>
|
||||||
|
<el-button
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
icon="Refresh"
|
||||||
|
:loading="isMaterialUploading(mat.materialTypeId)"
|
||||||
|
>
|
||||||
|
重新上传
|
||||||
|
</el-button>
|
||||||
|
</el-upload>
|
||||||
|
<el-button link type="danger" icon="Delete" @click="deleteFile(f.id)">
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="canEdit && getUploadedMaterials(mat.materialTypeId).length < mat.maxFileCount" class="upload-action-box">
|
<div v-if="canEdit && getRemainingFileCount(mat) > 0" class="upload-action-box">
|
||||||
<el-upload
|
<el-upload
|
||||||
:action="getMaterialUploadUrl(mat.materialTypeId)"
|
:action="getMaterialUploadUrl(mat.materialTypeId)"
|
||||||
|
:accept="getUploadAccept(mat)"
|
||||||
|
:disabled="isMaterialUploading(mat.materialTypeId)"
|
||||||
:show-file-list="false"
|
:show-file-list="false"
|
||||||
:on-success="onUploadSuccess"
|
:on-success="(response: any) => onUploadSuccess(response, mat.materialTypeId)"
|
||||||
|
:on-error="(error: Error) => onUploadError(error, mat.materialTypeId)"
|
||||||
:before-upload="(file: File) => beforeUpload(file, mat)"
|
:before-upload="(file: File) => beforeUpload(file, mat)"
|
||||||
>
|
>
|
||||||
<el-button size="small" type="primary" plain icon="Upload">
|
<el-button
|
||||||
上传文件 / 照片
|
size="small"
|
||||||
|
type="primary"
|
||||||
|
plain
|
||||||
|
icon="Upload"
|
||||||
|
:loading="isMaterialUploading(mat.materialTypeId)"
|
||||||
|
>
|
||||||
|
{{ getUploadedMaterials(mat.materialTypeId).length ? "继续上传" : "上传文件 / 照片" }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</el-upload>
|
</el-upload>
|
||||||
|
<span class="upload-count">
|
||||||
|
已上传 {{ getUploadedMaterials(mat.materialTypeId).length }}/{{ getMaxFileCount(mat) }},
|
||||||
|
还可上传 {{ getRemainingFileCount(mat) }} 份
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-else-if="canEdit && getUploadedMaterials(mat.materialTypeId).length"
|
||||||
|
class="upload-limit-reached"
|
||||||
|
>
|
||||||
|
已上传 {{ getUploadedMaterials(mat.materialTypeId).length }}/{{ getMaxFileCount(mat) }};
|
||||||
|
如需更换,请点击对应文件的“重新上传”
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -533,6 +598,8 @@
|
|||||||
type="primary"
|
type="primary"
|
||||||
plain
|
plain
|
||||||
icon="Download"
|
icon="Download"
|
||||||
|
:loading="isContractOpening(c.id)"
|
||||||
|
:disabled="isContractOpening(c.id)"
|
||||||
@click="previewAndDownloadContract(c)"
|
@click="previewAndDownloadContract(c)"
|
||||||
>
|
>
|
||||||
下载 / 查阅合同原文
|
下载 / 查阅合同原文
|
||||||
@@ -579,7 +646,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, reactive, ref } from "vue";
|
import { computed, onBeforeUnmount, onMounted, reactive, ref } from "vue";
|
||||||
import { useRoute } from "vue-router";
|
import { useRoute } from "vue-router";
|
||||||
import { ElMessage, ElMessageBox, FormInstance, FormRules } from "element-plus";
|
import { ElMessage, ElMessageBox, FormInstance, FormRules } from "element-plus";
|
||||||
import {
|
import {
|
||||||
@@ -589,11 +656,15 @@ import {
|
|||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const token = ref((route.query.token as string) || "");
|
const token = ref((route.query.token as string) || "");
|
||||||
|
const viewportMeta = document.querySelector<HTMLMetaElement>('meta[name="viewport"]');
|
||||||
|
const originalViewportContent = viewportMeta?.content;
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const submitting = ref(false);
|
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 openingContracts = reactive<Record<string, boolean>>({});
|
||||||
|
|
||||||
const task = ref<OnboardingTask | null>(null);
|
const task = ref<OnboardingTask | null>(null);
|
||||||
const contracts = ref<any[]>([]);
|
const contracts = ref<any[]>([]);
|
||||||
@@ -698,6 +769,53 @@ function getUploadedMaterials(matId: number) {
|
|||||||
return uploadedMaterials.value.filter(item => item.materialTypeId === matId);
|
return uploadedMaterials.value.filter(item => item.materialTypeId === matId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 防止旧任务中的异常数量导致上传入口被错误隐藏。 */
|
||||||
|
function getMaxFileCount(mat: any) {
|
||||||
|
const maxFileCount = Number(mat.maxFileCount);
|
||||||
|
return Number.isFinite(maxFileCount) && maxFileCount > 0
|
||||||
|
? Math.floor(maxFileCount)
|
||||||
|
: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRemainingFileCount(mat: any) {
|
||||||
|
return Math.max(
|
||||||
|
getMaxFileCount(mat) - getUploadedMaterials(mat.materialTypeId).length,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAllowedExtensions(mat: any) {
|
||||||
|
return String(mat.allowedExtensions || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.split(",")
|
||||||
|
.map((item: string) => item.trim().replace(/^\./, ""))
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUploadAccept(mat: any) {
|
||||||
|
return getAllowedExtensions(mat)
|
||||||
|
.map((item: string) => `.${item}`)
|
||||||
|
.join(",");
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMaterialLimitText(mat: any) {
|
||||||
|
const allowedExtensions = getAllowedExtensions(mat);
|
||||||
|
const formatText = allowedExtensions.length
|
||||||
|
? allowedExtensions.join("、").toUpperCase()
|
||||||
|
: "不限";
|
||||||
|
const sizeText = mat.maxFileSizeMb
|
||||||
|
? ` · 单个不超过 ${mat.maxFileSizeMb} MB`
|
||||||
|
: "";
|
||||||
|
return `最多 ${getMaxFileCount(mat)} 份 · 格式:${formatText}${sizeText}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isImageFile(file: any) {
|
||||||
|
const suffix = String(
|
||||||
|
file.fileSuffix || file.fileName?.split(".").pop() || ""
|
||||||
|
).toLowerCase().replace(/^\./, "");
|
||||||
|
return ["png", "jpg", "jpeg", "gif", "bmp", "webp"].includes(suffix);
|
||||||
|
}
|
||||||
|
|
||||||
function statusTagType(status?: string) {
|
function statusTagType(status?: string) {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case "pending_fill": return "warning";
|
case "pending_fill": return "warning";
|
||||||
@@ -726,12 +844,14 @@ function formatDateTime(str?: string) {
|
|||||||
return str ? str.replace("T", " ").substring(0, 16) : "";
|
return str ? str.replace("T", " ").substring(0, 16) : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
async function load() {
|
async function load(silent = false) {
|
||||||
if (!token.value) {
|
if (!token.value) {
|
||||||
error.value = "缺少入职访问 Token";
|
error.value = "缺少入职访问 Token";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
loading.value = true;
|
if (!silent) {
|
||||||
|
loading.value = true;
|
||||||
|
}
|
||||||
error.value = "";
|
error.value = "";
|
||||||
try {
|
try {
|
||||||
const res = await onboardingPublicApi.detail({ token: token.value });
|
const res = await onboardingPublicApi.detail({ token: token.value });
|
||||||
@@ -764,7 +884,9 @@ async function load() {
|
|||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
error.value = err.message || "无法加载入职任务";
|
error.value = err.message || "无法加载入职任务";
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
if (!silent) {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -802,46 +924,135 @@ async function nextStep() {
|
|||||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMaterialUploadUrl(materialTypeId: number) {
|
function getMaterialUploadUrl(materialTypeId: number, taskMaterialId?: number) {
|
||||||
return `/api/open/onboarding/upload-material?token=${encodeURIComponent(token.value)}&materialTypeId=${materialTypeId}`;
|
const query = new URLSearchParams({
|
||||||
|
token: token.value,
|
||||||
|
materialTypeId: String(materialTypeId)
|
||||||
|
});
|
||||||
|
if (taskMaterialId) {
|
||||||
|
query.set("taskMaterialId", String(taskMaterialId));
|
||||||
|
}
|
||||||
|
return `/api/open/onboarding/upload-material?${query.toString()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMaterialSourceUrl(file: any) {
|
||||||
|
return onboardingPublicApi.materialSourceUrl({
|
||||||
|
token: token.value,
|
||||||
|
taskMaterialId: file.id,
|
||||||
|
fileId: file.fileId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function isMaterialUploading(materialTypeId: number) {
|
||||||
|
return Boolean(uploadingMaterials[String(materialTypeId)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setMaterialUploading(materialTypeId: number, value: boolean) {
|
||||||
|
uploadingMaterials[String(materialTypeId)] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
function beforeUpload(file: File, mat: any) {
|
function beforeUpload(file: File, mat: any) {
|
||||||
const ext = file.name.split(".").pop()?.toLowerCase() || "";
|
const ext = file.name.split(".").pop()?.toLowerCase() || "";
|
||||||
const allowed = (mat.allowedExtensions || "").toLowerCase().split(",")
|
const allowed = getAllowedExtensions(mat);
|
||||||
.map((s: string) => s.trim())
|
|
||||||
.filter(Boolean);
|
|
||||||
if (allowed.length && !allowed.includes(ext)) {
|
if (allowed.length && !allowed.includes(ext)) {
|
||||||
ElMessage.error(`仅支持格式:${mat.allowedExtensions}`);
|
ElMessage.error(`仅支持格式:${mat.allowedExtensions}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (mat.maxFileSizeMb && file.size > Number(mat.maxFileSizeMb) * 1024 * 1024) {
|
||||||
|
ElMessage.error(`单个文件不能超过 ${mat.maxFileSizeMb} MB`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
setMaterialUploading(mat.materialTypeId, true);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onUploadSuccess(res: any) {
|
async function onUploadSuccess(res: any, materialTypeId: number) {
|
||||||
if (res.code !== 200 && res.code !== 0) {
|
if (res.code !== 200 && res.code !== 0) {
|
||||||
|
setMaterialUploading(materialTypeId, false);
|
||||||
return ElMessage.error(res.msg || "上传失败");
|
return ElMessage.error(res.msg || "上传失败");
|
||||||
}
|
}
|
||||||
ElMessage.success("材料上传成功");
|
try {
|
||||||
await load();
|
await load(true);
|
||||||
|
ElMessage.success("材料上传成功");
|
||||||
|
} finally {
|
||||||
|
setMaterialUploading(materialTypeId, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onUploadError(error: Error, materialTypeId: number) {
|
||||||
|
setMaterialUploading(materialTypeId, false);
|
||||||
|
ElMessage.error(error.message || "材料上传失败,请稍后重试");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteFile(id: number) {
|
async function deleteFile(id: number) {
|
||||||
await ElMessageBox.confirm("确认删除已上传材料?", "删除确认", { type: "warning" });
|
const confirmed = await isMessageBoxConfirmed(
|
||||||
|
ElMessageBox.confirm("确认删除已上传材料?", "删除确认", { type: "warning" })
|
||||||
|
);
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await onboardingPublicApi.deleteMaterial({ token: token.value, taskMaterialId: id });
|
await onboardingPublicApi.deleteMaterial({ token: token.value, taskMaterialId: id });
|
||||||
ElMessage.success("材料已删除");
|
ElMessage.success("材料已删除");
|
||||||
await load();
|
await load();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function previewAndDownloadContract(c: any) {
|
/**
|
||||||
if (canEdit.value) {
|
* Element Plus 在取消或关闭确认框时会拒绝 Promise。
|
||||||
await saveCurrentEmployeeInfo();
|
* 取消属于正常操作,不应继续交给全局异常提示处理。
|
||||||
|
*/
|
||||||
|
async function isMessageBoxConfirmed(confirmPromise: Promise<unknown>) {
|
||||||
|
try {
|
||||||
|
await confirmPromise;
|
||||||
|
return true;
|
||||||
|
} catch (action) {
|
||||||
|
if (action === "cancel" || action === "close") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw action;
|
||||||
}
|
}
|
||||||
const url = onboardingPublicApi.contractSourceUrl({
|
}
|
||||||
token: token.value,
|
|
||||||
taskContractId: c.id
|
async function previewAndDownloadContract(c: any) {
|
||||||
});
|
if (isContractOpening(c.id)) {
|
||||||
window.open(url, "_blank");
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 必须在用户点击的同步阶段创建窗口,否则手机浏览器会拦截异步完成后的 window.open。
|
||||||
|
const previewWindow = window.open("about:blank", "_blank");
|
||||||
|
openingContracts[String(c.id)] = true;
|
||||||
|
try {
|
||||||
|
if (previewWindow) {
|
||||||
|
previewWindow.document.title = "合同生成中";
|
||||||
|
previewWindow.document.body.innerHTML = `
|
||||||
|
<div style="padding:40px 20px;text-align:center;font-family:sans-serif;color:#475569;">
|
||||||
|
正在生成合同文件,请稍候…
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
if (canEdit.value) {
|
||||||
|
await saveCurrentEmployeeInfo();
|
||||||
|
}
|
||||||
|
const url = onboardingPublicApi.contractSourceUrl({
|
||||||
|
token: token.value,
|
||||||
|
taskContractId: c.id
|
||||||
|
});
|
||||||
|
if (previewWindow && !previewWindow.closed) {
|
||||||
|
previewWindow.location.replace(url);
|
||||||
|
} else {
|
||||||
|
// 微信等内置浏览器拦截新窗口时,退回当前页直接发起下载。
|
||||||
|
window.location.assign(url);
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
previewWindow?.close();
|
||||||
|
ElMessage.error(err?.message || "合同生成失败,请稍后重试");
|
||||||
|
} finally {
|
||||||
|
openingContracts[String(c.id)] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isContractOpening(taskContractId: number) {
|
||||||
|
return Boolean(openingContracts[String(taskContractId)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function submitAll() {
|
async function submitAll() {
|
||||||
@@ -850,11 +1061,16 @@ async function submitAll() {
|
|||||||
return ElMessage.warning(`请先勾选同意全部合同(未勾选:${unconfirmed.map(c => c.contractName).join("、")})`);
|
return ElMessage.warning(`请先勾选同意全部合同(未勾选:${unconfirmed.map(c => c.contractName).join("、")})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
await ElMessageBox.confirm("确认提交全部入职资料与合同确认?提交后将进入人事复核建档流程。", "最终提交确认", {
|
const confirmed = await isMessageBoxConfirmed(
|
||||||
type: "success",
|
ElMessageBox.confirm("确认提交全部入职资料与合同确认?提交后将进入人事复核建档流程。", "最终提交确认", {
|
||||||
confirmButtonText: "确认提交",
|
type: "success",
|
||||||
cancelButtonText: "再检查一下"
|
confirmButtonText: "确认提交",
|
||||||
});
|
cancelButtonText: "再检查一下"
|
||||||
|
})
|
||||||
|
);
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
submitting.value = true;
|
submitting.value = true;
|
||||||
try {
|
try {
|
||||||
@@ -870,7 +1086,26 @@ async function submitAll() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(load);
|
/** 入职填写页使用固定移动端比例,离开页面后恢复系统原有 viewport 配置。 */
|
||||||
|
onMounted(() => {
|
||||||
|
if (viewportMeta) {
|
||||||
|
viewportMeta.content = [
|
||||||
|
"width=device-width",
|
||||||
|
"initial-scale=1.0",
|
||||||
|
"minimum-scale=1.0",
|
||||||
|
"maximum-scale=1.0",
|
||||||
|
"user-scalable=no",
|
||||||
|
"viewport-fit=cover"
|
||||||
|
].join(", ");
|
||||||
|
}
|
||||||
|
load();
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (viewportMeta && originalViewportContent) {
|
||||||
|
viewportMeta.content = originalViewportContent;
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -941,6 +1176,16 @@ onMounted(load);
|
|||||||
color: #64748b;
|
color: #64748b;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.reject-reason-alert {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reject-reason-alert :deep(.el-alert__description) {
|
||||||
|
line-height: 1.6;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
.wizard-card {
|
.wizard-card {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
@@ -1111,13 +1356,68 @@ onMounted(load);
|
|||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-name {
|
.material-thumbnail {
|
||||||
|
width: 68px;
|
||||||
|
height: 68px;
|
||||||
|
flex: 0 0 68px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: zoom-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
.material-file-icon {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
flex: 0 0 36px;
|
||||||
|
font-size: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-content {
|
||||||
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-name {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.file-size,
|
||||||
|
.preview-tip,
|
||||||
|
.upload-count {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-tip {
|
||||||
|
color: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-actions,
|
||||||
|
.upload-action-box {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-action-box {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-limit-reached {
|
||||||
|
margin-top: 4px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #eff6ff;
|
||||||
|
color: #475569;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
.contract-sign-card {
|
.contract-sign-card {
|
||||||
border: 1px solid #e2e8f0;
|
border: 1px solid #e2e8f0;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@@ -1262,6 +1562,12 @@ onMounted(load);
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
|
/* iOS 会自动放大小于 16px 的表单控件,统一字号保持页面比例稳定。 */
|
||||||
|
.fill-page :deep(.el-input__inner),
|
||||||
|
.fill-page :deep(.el-textarea__inner),
|
||||||
|
.fill-page :deep(.el-select__input) {
|
||||||
|
font-size: 16px !important;
|
||||||
|
}
|
||||||
.desktop-only {
|
.desktop-only {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
@@ -1285,6 +1591,14 @@ onMounted(load);
|
|||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
.file-badge {
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.file-actions {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
.contract-card-bottom {
|
.contract-card-bottom {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
|
|||||||
Reference in New Issue
Block a user