111
This commit is contained in:
@@ -66,6 +66,7 @@ export interface OnboardingTask {
|
||||
departmentName?: string;
|
||||
positionId: number;
|
||||
positionName?: string;
|
||||
employmentType?: "full_time" | "part_time";
|
||||
onboardingTemplateId?: number;
|
||||
onboardingTemplateName?: string;
|
||||
taskStatus: string;
|
||||
@@ -294,6 +295,12 @@ export interface OnboardingEmployee {
|
||||
departmentName?: string;
|
||||
positionId: number;
|
||||
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;
|
||||
idCardNumber: string;
|
||||
entryDate?: string;
|
||||
@@ -813,6 +820,14 @@ export const onboardingTaskApi = {
|
||||
>("page", params);
|
||||
},
|
||||
|
||||
employeeExport(params: Record<string, unknown>) {
|
||||
return moduleRequest("/biz/onboarding/employee/").download(
|
||||
"export",
|
||||
params,
|
||||
{ cancel: false }
|
||||
);
|
||||
},
|
||||
|
||||
employeeDetail(params: ReqId) {
|
||||
return moduleRequest("/biz/onboarding/employee/").get(
|
||||
"detail",
|
||||
@@ -820,6 +835,48 @@ 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
|
||||
);
|
||||
},
|
||||
|
||||
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() {
|
||||
return moduleRequest("/biz/onboarding/entry-config/").get<
|
||||
OnboardingFormField[]
|
||||
|
||||
@@ -1,8 +1,37 @@
|
||||
<template>
|
||||
<div class="table-box">
|
||||
<ProTable ref="proTable" title="正式员工档案" :columns="columns" :request-api="onboardingTaskApi.employeePage">
|
||||
<ProTable ref="proTable" title="员工档案" :columns="columns" :request-api="onboardingTaskApi.employeePage">
|
||||
<template #tableHeader>
|
||||
<el-button type="primary" plain icon="Download" :loading="exporting" @click="exportEmployees">
|
||||
导出当前筛选结果
|
||||
</el-button>
|
||||
</template>
|
||||
|
||||
<template #operation="scope">
|
||||
<el-button link type="primary" icon="UserFilled" @click="showDetail(scope.row)"> 查看完整档案 </el-button>
|
||||
<el-button link type="primary" icon="UserFilled" @click="showDetail(scope.row)">查看档案</el-button>
|
||||
<el-button link type="primary" icon="Edit" @click="openHrEdit(scope.row)">编辑人事信息</el-button>
|
||||
<el-button
|
||||
v-if="canRegularize(scope.row)"
|
||||
link
|
||||
type="success"
|
||||
icon="CircleCheck"
|
||||
@click="openRegularization(scope.row)"
|
||||
>
|
||||
办理转正
|
||||
</el-button>
|
||||
</template>
|
||||
<template #employmentType="scope">
|
||||
<el-tag :type="scope.row.employmentType === 'part_time' ? 'warning' : 'primary'" effect="plain">
|
||||
{{ employmentTypeText(scope.row.employmentType) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
<template #regularizationStatus="scope">
|
||||
<el-tag :type="regularizationTagType(scope.row.regularizationStatus)" effect="plain">
|
||||
{{ regularizationText(scope.row.regularizationStatus) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
<template #socialInsuranceStatus="scope">
|
||||
{{ socialInsuranceText(scope.row.socialInsuranceStatus) }}
|
||||
</template>
|
||||
</ProTable>
|
||||
|
||||
@@ -20,7 +49,12 @@
|
||||
<el-tag size="small" :type="detail.employee.gender === '女' ? 'danger' : 'primary'" effect="plain">
|
||||
{{ detail.employee.gender || "未知" }}
|
||||
</el-tag>
|
||||
<el-tag size="small" type="success">正式员工</el-tag>
|
||||
<el-tag size="small" :type="detail.employee.employmentType === 'part_time' ? 'warning' : 'primary'">
|
||||
{{ employmentTypeText(detail.employee.employmentType) }}
|
||||
</el-tag>
|
||||
<el-tag size="small" :type="regularizationTagType(detail.employee.regularizationStatus)">
|
||||
{{ regularizationText(detail.employee.regularizationStatus) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="employee-meta-row">
|
||||
<span>所属公司:{{ detail.employee.companyName || "-" }}</span>
|
||||
@@ -86,8 +120,12 @@
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="员工编号">{{ detail.employee.employeeNo || employeeInfo.employeeNo || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="入职日期">{{ formatDate(detail.employee.entryDate) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="社保缴纳单位">{{ detail.employee.socialInsuranceCompany || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="聘用/材料性质">{{ detail.employee.contractNature || "全职劳动合同" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="用工类型">{{ employmentTypeText(detail.employee.employmentType) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="转正状态">{{ regularizationText(detail.employee.regularizationStatus) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="转正日期">{{ formatDate(detail.employee.regularizationDate) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="社保状态">{{ socialInsuranceText(detail.employee.socialInsuranceStatus) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="合同社保单位">{{ detail.employee.socialInsuranceCompany || "未维护" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="合同性质">{{ detail.employee.contractNature || "未维护" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="签署材料开始日期">{{ formatDate(detail.employee.contractStartDate) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="签署材料终止日期">{{ formatDate(detail.employee.contractEndDate) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="其他说明" :span="2">{{
|
||||
@@ -152,6 +190,44 @@
|
||||
</el-card>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="人事变更记录" name="changes">
|
||||
<el-empty v-if="!detail.changeRecords?.length" description="暂无人事变更记录" />
|
||||
<el-timeline v-else class="change-timeline">
|
||||
<el-timeline-item
|
||||
v-for="record in detail.changeRecords"
|
||||
:key="record.id"
|
||||
:timestamp="formatTime(record.createTime)"
|
||||
placement="top"
|
||||
type="primary"
|
||||
>
|
||||
<div class="change-record-card">
|
||||
<div class="change-record-header">
|
||||
<strong>{{ changeTypeText(record.changeType) }}</strong>
|
||||
<span>{{ record.createUser || "系统" }}</span>
|
||||
</div>
|
||||
<div v-if="record.effectiveDate" class="change-record-line">
|
||||
生效日期:{{ formatDate(record.effectiveDate) }}
|
||||
</div>
|
||||
<div v-if="record.remark" class="change-record-line">
|
||||
备注:{{ record.remark }}
|
||||
</div>
|
||||
<div v-if="record.attachments?.length" class="change-attachments">
|
||||
<el-button
|
||||
v-for="file in record.attachments"
|
||||
:key="file.id"
|
||||
link
|
||||
type="primary"
|
||||
icon="Download"
|
||||
@click="download(file.fileId, file.fileName || '人事材料')"
|
||||
>
|
||||
{{ file.fileName || "人事材料" }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 2. 员工入职资料与合同文件 -->
|
||||
<el-tab-pane label="员工资料信息" name="materials">
|
||||
<el-card shadow="never" class="section-card">
|
||||
@@ -284,16 +360,135 @@
|
||||
</el-tabs>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button v-if="detail?.employee" type="primary" plain @click="openHrEdit(detail.employee)">编辑人事信息</el-button>
|
||||
<el-button
|
||||
v-if="detail?.employee && canRegularize(detail.employee)"
|
||||
type="success"
|
||||
plain
|
||||
@click="openRegularization(detail.employee)"
|
||||
>
|
||||
办理转正
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="detail?.employee?.regularizationStatus === 'regular'"
|
||||
type="danger"
|
||||
plain
|
||||
@click="revokeRegularization(detail.employee)"
|
||||
>
|
||||
撤销转正
|
||||
</el-button>
|
||||
<el-button @click="detailVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
|
||||
<el-dialog v-model="hrEditVisible" title="编辑员工人事信息" width="560px" destroy-on-close>
|
||||
<el-alert
|
||||
title="转正状态只能通过“办理转正”操作改变;合同日期不会自动转正。"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="dialog-alert"
|
||||
/>
|
||||
<el-form label-position="top">
|
||||
<el-form-item label="用工类型" required>
|
||||
<el-radio-group v-model="hrForm.employmentType">
|
||||
<el-radio-button value="full_time">正式员工</el-radio-button>
|
||||
<el-radio-button value="part_time">兼职员工</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="合同性质">
|
||||
<el-select
|
||||
v-model="hrForm.contractNature"
|
||||
filterable
|
||||
allow-create
|
||||
clearable
|
||||
default-first-option
|
||||
placeholder="请选择或输入合同性质"
|
||||
class="full-width"
|
||||
>
|
||||
<el-option label="固定期限劳动合同" value="固定期限劳动合同" />
|
||||
<el-option label="无固定期限劳动合同" value="无固定期限劳动合同" />
|
||||
<el-option label="劳务协议" value="劳务协议" />
|
||||
<el-option label="兼职协议" value="兼职协议" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="社保状态" required>
|
||||
<el-select v-model="hrForm.socialInsuranceStatus" class="full-width">
|
||||
<el-option label="未维护" value="unknown" />
|
||||
<el-option label="未购买" value="not_purchased" />
|
||||
<el-option label="已购买" value="purchased" />
|
||||
<el-option label="已停保" value="stopped" />
|
||||
<el-option label="不适用" value="not_applicable" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="合同社保单位">
|
||||
<el-input v-model="hrForm.socialInsuranceCompany" clearable placeholder="请输入合同约定的社保单位" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="hrEditVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="savingHr" @click="saveHrInfo">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="regularizationVisible" title="办理员工转正" width="620px" destroy-on-close>
|
||||
<el-alert
|
||||
:title="'正在为 ' + (regularizationEmployee?.employeeName || '员工') + ' 办理转正,保存后仍可在员工档案中撤销。'"
|
||||
type="success"
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="dialog-alert"
|
||||
/>
|
||||
<el-form label-position="top">
|
||||
<el-form-item label="转正日期" required>
|
||||
<el-date-picker
|
||||
v-model="regularizationForm.regularizationDate"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="请选择实际转正日期"
|
||||
class="full-width"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="转正备注">
|
||||
<el-input
|
||||
v-model="regularizationForm.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
placeholder="可填写转正说明(选填)"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="转正材料">
|
||||
<el-upload
|
||||
v-model:file-list="regularizationFiles"
|
||||
multiple
|
||||
:limit="10"
|
||||
:http-request="uploadRegularizationFile"
|
||||
:before-upload="beforeRegularizationUpload"
|
||||
:on-remove="removeRegularizationFile"
|
||||
accept=".pdf,.doc,.docx,.xls,.xlsx,.jpg,.jpeg,.png,.webp,.zip"
|
||||
>
|
||||
<el-button type="primary" plain icon="Upload">选择并上传材料</el-button>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">选填,单个文件不超过 20MB,最多 10 份。</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="regularizationVisible = false">取消</el-button>
|
||||
<el-button type="success" :loading="regularizing" @click="submitRegularization">确认转正</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="onboardingEmployee">
|
||||
import { computed, onBeforeUnmount, onMounted, reactive, ref } from "vue";
|
||||
import { Document, Download, UserFilled } from "@element-plus/icons-vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import type { UploadFile, UploadRequestOptions, UploadUserFile } from "element-plus";
|
||||
import {
|
||||
onboardingTaskApi,
|
||||
type OnboardingEmployee,
|
||||
@@ -302,11 +497,36 @@ import {
|
||||
type OnboardingPositionOption
|
||||
} from "@/api/modules/biz/onboarding";
|
||||
import type { ColumnProps, ProTableInstance } from "@/components/ProTable/interface";
|
||||
import { getMessageBoxPromptResult, isMessageBoxConfirmed } from "@/utils/messageBox";
|
||||
|
||||
const proTable = ref<ProTableInstance>();
|
||||
const detailVisible = ref(false);
|
||||
const activeTab = ref("basic");
|
||||
const detail = ref<any>();
|
||||
const exporting = ref(false);
|
||||
const hrEditVisible = ref(false);
|
||||
const savingHr = ref(false);
|
||||
const editingEmployee = ref<OnboardingEmployee>();
|
||||
const hrForm = reactive({
|
||||
id: 0,
|
||||
employmentType: "full_time" as "full_time" | "part_time",
|
||||
contractNature: "",
|
||||
socialInsuranceStatus: "unknown" as
|
||||
| "unknown"
|
||||
| "not_purchased"
|
||||
| "purchased"
|
||||
| "stopped"
|
||||
| "not_applicable",
|
||||
socialInsuranceCompany: ""
|
||||
});
|
||||
const regularizationVisible = ref(false);
|
||||
const regularizing = ref(false);
|
||||
const regularizationEmployee = ref<OnboardingEmployee>();
|
||||
const regularizationFiles = ref<UploadUserFile[]>([]);
|
||||
const regularizationForm = reactive({
|
||||
regularizationDate: "",
|
||||
remark: ""
|
||||
});
|
||||
const employeeInfo = computed<Record<string, any>>(() => detail.value?.employeeInfo || {});
|
||||
const materialPreviewUrls = reactive<Record<number, string>>({});
|
||||
const materialPreviewLoading = reactive<Record<number, boolean>>({});
|
||||
@@ -570,6 +790,17 @@ const columns: ColumnProps<OnboardingEmployee>[] = [
|
||||
minWidth: 140,
|
||||
search: { el: "input" }
|
||||
},
|
||||
{
|
||||
prop: "employmentType",
|
||||
label: "用工类型",
|
||||
minWidth: 105,
|
||||
align: "center",
|
||||
enum: [
|
||||
{ label: "正式员工", value: "full_time" },
|
||||
{ label: "兼职员工", value: "part_time" }
|
||||
],
|
||||
search: { el: "select", props: { clearable: true, placeholder: "请选择用工类型" } }
|
||||
},
|
||||
{
|
||||
prop: "gender",
|
||||
label: "性别",
|
||||
@@ -603,18 +834,44 @@ const columns: ColumnProps<OnboardingEmployee>[] = [
|
||||
minWidth: 140,
|
||||
render: scope => formatDate(scope.row.entryDate)
|
||||
},
|
||||
{
|
||||
prop: "regularizationStatus",
|
||||
label: "转正状态",
|
||||
minWidth: 105,
|
||||
align: "center",
|
||||
enum: [
|
||||
{ label: "试用期", value: "probation" },
|
||||
{ label: "已转正", value: "regular" },
|
||||
{ label: "不适用", value: "not_applicable" }
|
||||
],
|
||||
search: { el: "select", props: { clearable: true, placeholder: "请选择转正状态" } }
|
||||
},
|
||||
{
|
||||
prop: "socialInsuranceStatus",
|
||||
label: "社保状态",
|
||||
minWidth: 105,
|
||||
align: "center",
|
||||
enum: [
|
||||
{ label: "未维护", value: "unknown" },
|
||||
{ label: "未购买", value: "not_purchased" },
|
||||
{ label: "已购买", value: "purchased" },
|
||||
{ label: "已停保", value: "stopped" },
|
||||
{ label: "不适用", value: "not_applicable" }
|
||||
],
|
||||
search: { el: "select", props: { clearable: true, placeholder: "请选择社保状态" } }
|
||||
},
|
||||
{
|
||||
prop: "operation",
|
||||
label: "操作",
|
||||
fixed: "right",
|
||||
width: 160
|
||||
width: 310
|
||||
}
|
||||
];
|
||||
|
||||
const auditActionText = (action: string) => {
|
||||
const map: Record<string, string> = {
|
||||
submit: "候选人提交入职资料",
|
||||
approve: "人事复核通过并转正建档",
|
||||
approve: "人事审核通过并创建员工档案",
|
||||
reject: "人事退回补充资料",
|
||||
archive: "任务归档",
|
||||
revoke: "任务撤销"
|
||||
@@ -622,6 +879,54 @@ const auditActionText = (action: string) => {
|
||||
return map[action] || action;
|
||||
};
|
||||
|
||||
function employmentTypeText(value?: string) {
|
||||
return value === "part_time" ? "兼职员工" : "正式员工";
|
||||
}
|
||||
|
||||
function regularizationText(value?: string) {
|
||||
const map: Record<string, string> = {
|
||||
probation: "试用期",
|
||||
regular: "已转正",
|
||||
not_applicable: "不适用"
|
||||
};
|
||||
return map[value || ""] || "试用期";
|
||||
}
|
||||
|
||||
function regularizationTagType(value?: string): "success" | "warning" | "info" {
|
||||
if (value === "regular") return "success";
|
||||
if (value === "not_applicable") return "info";
|
||||
return "warning";
|
||||
}
|
||||
|
||||
function socialInsuranceText(value?: string) {
|
||||
const map: Record<string, string> = {
|
||||
unknown: "未维护",
|
||||
not_purchased: "未购买",
|
||||
purchased: "已购买",
|
||||
stopped: "已停保",
|
||||
not_applicable: "不适用"
|
||||
};
|
||||
return map[value || ""] || "未维护";
|
||||
}
|
||||
|
||||
function changeTypeText(value?: string) {
|
||||
const map: Record<string, string> = {
|
||||
hr_info_update: "编辑人事信息",
|
||||
regularization: "办理转正",
|
||||
regularization_revoke: "撤销转正"
|
||||
};
|
||||
return map[value || ""] || value || "人事变更";
|
||||
}
|
||||
|
||||
function canRegularize(employee?: OnboardingEmployee) {
|
||||
return Boolean(
|
||||
employee
|
||||
&& employee.employmentType !== "part_time"
|
||||
&& employee.regularizationStatus !== "regular"
|
||||
&& employee.regularizationStatus !== "not_applicable"
|
||||
);
|
||||
}
|
||||
|
||||
const formatTime = (timeStr?: string) => {
|
||||
if (!timeStr) return "-";
|
||||
return timeStr.replace("T", " ").substring(0, 19);
|
||||
@@ -659,6 +964,233 @@ function resolveDownloadFileName(response: any, fallbackFileName?: string) {
|
||||
return fallbackFileName || "员工资料文件";
|
||||
}
|
||||
|
||||
function openHrEdit(row: OnboardingEmployee) {
|
||||
editingEmployee.value = row;
|
||||
Object.assign(hrForm, {
|
||||
id: row.id,
|
||||
employmentType: row.employmentType === "part_time" ? "part_time" : "full_time",
|
||||
contractNature: row.contractNature || "",
|
||||
socialInsuranceStatus: row.socialInsuranceStatus || "unknown",
|
||||
socialInsuranceCompany: row.socialInsuranceCompany || ""
|
||||
});
|
||||
hrEditVisible.value = true;
|
||||
}
|
||||
|
||||
async function refreshEmployee(employeeId: number) {
|
||||
proTable.value?.refresh();
|
||||
if (detailVisible.value && detail.value?.employee?.id === employeeId) {
|
||||
const { data } = await onboardingTaskApi.employeeDetail({ id: employeeId });
|
||||
detail.value = data;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveHrInfo() {
|
||||
if (!hrForm.id) return;
|
||||
if (
|
||||
editingEmployee.value?.regularizationStatus === "regular"
|
||||
&& hrForm.employmentType === "part_time"
|
||||
) {
|
||||
const confirmed = await isMessageBoxConfirmed(
|
||||
ElMessageBox.confirm(
|
||||
"改为兼职员工后,当前转正状态会变为“不适用”,转正日期会从当前档案中清除,但历史记录仍会保留。确定继续吗?",
|
||||
"确认调整用工类型",
|
||||
{ type: "warning", confirmButtonText: "确定调整", cancelButtonText: "取消" }
|
||||
)
|
||||
);
|
||||
if (!confirmed) return;
|
||||
}
|
||||
|
||||
savingHr.value = true;
|
||||
try {
|
||||
await onboardingTaskApi.updateEmployeeHrInfo({
|
||||
id: hrForm.id,
|
||||
employmentType: hrForm.employmentType,
|
||||
contractNature: hrForm.contractNature.trim() || undefined,
|
||||
socialInsuranceStatus: hrForm.socialInsuranceStatus,
|
||||
socialInsuranceCompany: hrForm.socialInsuranceCompany.trim() || undefined
|
||||
});
|
||||
hrEditVisible.value = false;
|
||||
await refreshEmployee(hrForm.id);
|
||||
ElMessage.success("员工人事信息已保存");
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.msg || error?.message || "员工人事信息保存失败");
|
||||
} finally {
|
||||
savingHr.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function localDateText(date = new Date()) {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(date.getDate()).padStart(2, "0");
|
||||
return year + "-" + month + "-" + day;
|
||||
}
|
||||
|
||||
function openRegularization(row: OnboardingEmployee) {
|
||||
if (!canRegularize(row)) {
|
||||
return ElMessage.warning(
|
||||
row.employmentType === "part_time"
|
||||
? "兼职员工不适用转正流程"
|
||||
: "该员工当前无需重复办理转正"
|
||||
);
|
||||
}
|
||||
regularizationEmployee.value = row;
|
||||
regularizationForm.regularizationDate = localDateText();
|
||||
regularizationForm.remark = "";
|
||||
regularizationFiles.value = [];
|
||||
regularizationVisible.value = true;
|
||||
}
|
||||
|
||||
function beforeRegularizationUpload(file: File) {
|
||||
const extension = "." + (file.name.split(".").pop() || "").toLowerCase();
|
||||
const allowed = [
|
||||
".pdf", ".doc", ".docx", ".xls", ".xlsx",
|
||||
".jpg", ".jpeg", ".png", ".webp", ".zip"
|
||||
];
|
||||
if (!allowed.includes(extension)) {
|
||||
ElMessage.error("仅支持 PDF、Word、Excel、图片或 ZIP 文件");
|
||||
return false;
|
||||
}
|
||||
if (file.size > 20 * 1024 * 1024) {
|
||||
ElMessage.error("单个文件不能超过 20MB");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function uploadRegularizationFile(options: UploadRequestOptions) {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append("file", options.file);
|
||||
const { data } = await onboardingTaskApi.uploadEmployeeAttachment(formData);
|
||||
options.onSuccess({ fileId: data });
|
||||
} catch (error: any) {
|
||||
options.onError(error as any);
|
||||
ElMessage.error(error?.msg || error?.message || "材料上传失败");
|
||||
}
|
||||
}
|
||||
|
||||
function removeRegularizationFile(_file: UploadFile) {
|
||||
// 文件关联只在确认转正时写入;从列表移除后不会作为转正材料提交。
|
||||
}
|
||||
|
||||
async function submitRegularization() {
|
||||
const employee = regularizationEmployee.value;
|
||||
if (!employee) return;
|
||||
if (!regularizationForm.regularizationDate) {
|
||||
return ElMessage.warning("请选择转正日期");
|
||||
}
|
||||
if (regularizationFiles.value.some(file => file.status === "uploading")) {
|
||||
return ElMessage.warning("材料仍在上传,请稍候");
|
||||
}
|
||||
|
||||
const fileIds = regularizationFiles.value
|
||||
.map(file => Number((file.response as any)?.fileId))
|
||||
.filter(fileId => Number.isFinite(fileId) && fileId > 0);
|
||||
if (
|
||||
regularizationFiles.value.some(file => file.status === "fail")
|
||||
|| fileIds.length !== regularizationFiles.value.length
|
||||
) {
|
||||
return ElMessage.warning("有材料上传失败,请移除后重试");
|
||||
}
|
||||
|
||||
regularizing.value = true;
|
||||
try {
|
||||
await onboardingTaskApi.regularizeEmployee({
|
||||
id: employee.id,
|
||||
regularizationDate: regularizationForm.regularizationDate,
|
||||
remark: regularizationForm.remark.trim() || undefined,
|
||||
fileIds
|
||||
});
|
||||
regularizationVisible.value = false;
|
||||
await refreshEmployee(employee.id);
|
||||
ElMessage.success("员工转正已办理");
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.msg || error?.message || "办理转正失败");
|
||||
} finally {
|
||||
regularizing.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function revokeRegularization(employee: OnboardingEmployee) {
|
||||
const promptResult: any = await getMessageBoxPromptResult(
|
||||
ElMessageBox.prompt(
|
||||
"撤销后员工将恢复为试用期,原转正记录和材料仍保留。请输入撤销原因:",
|
||||
"撤销转正",
|
||||
{
|
||||
type: "warning",
|
||||
confirmButtonText: "确认撤销",
|
||||
cancelButtonText: "取消",
|
||||
inputPattern: /\S+/,
|
||||
inputErrorMessage: "请填写撤销原因"
|
||||
}
|
||||
)
|
||||
);
|
||||
if (!promptResult) return;
|
||||
|
||||
try {
|
||||
await onboardingTaskApi.revokeEmployeeRegularization({
|
||||
id: employee.id,
|
||||
remark: String(promptResult.value || "").trim()
|
||||
});
|
||||
await refreshEmployee(employee.id);
|
||||
ElMessage.success("转正已撤销");
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.msg || error?.message || "撤销转正失败");
|
||||
}
|
||||
}
|
||||
|
||||
async function blobErrorMessage(error: any, fallbackMessage: string) {
|
||||
const errorBlob = error?.response?.data instanceof Blob
|
||||
? error.response.data
|
||||
: error?.data instanceof Blob
|
||||
? error.data
|
||||
: undefined;
|
||||
if (errorBlob) {
|
||||
try {
|
||||
const result = JSON.parse(await errorBlob.text());
|
||||
return result?.msg || fallbackMessage;
|
||||
} catch {
|
||||
return fallbackMessage;
|
||||
}
|
||||
}
|
||||
|
||||
return error?.msg || error?.message || fallbackMessage;
|
||||
}
|
||||
|
||||
async function exportEmployees() {
|
||||
exporting.value = true;
|
||||
try {
|
||||
const response: any = await onboardingTaskApi.employeeExport({
|
||||
...(proTable.value?.searchParam || {})
|
||||
});
|
||||
const blob = response?.data;
|
||||
if (!(blob instanceof Blob)) {
|
||||
throw new Error("服务器未返回有效的 Excel 文件");
|
||||
}
|
||||
|
||||
const contentType = String(response?.headers?.["content-type"] || blob.type || "").toLowerCase();
|
||||
if (contentType.includes("json")) {
|
||||
const result = JSON.parse(await blob.text());
|
||||
throw new Error(result?.msg || "员工档案导出失败");
|
||||
}
|
||||
|
||||
const objectUrl = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = objectUrl;
|
||||
link.download = resolveDownloadFileName(response, `员工档案_${new Date().toISOString().slice(0, 10)}.xlsx`);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
window.setTimeout(() => URL.revokeObjectURL(objectUrl), 0);
|
||||
ElMessage.success("员工明细导出成功");
|
||||
} catch (error: any) {
|
||||
ElMessage.error(await blobErrorMessage(error, "员工档案导出失败"));
|
||||
} finally {
|
||||
exporting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function download(fileId: number, fallbackFileName?: string) {
|
||||
try {
|
||||
const response: any = await onboardingTaskApi.downloadFile(fileId);
|
||||
@@ -776,11 +1308,56 @@ onBeforeUnmount(clearMaterialPreviews);
|
||||
|
||||
.employee-meta-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dialog-alert {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.change-timeline {
|
||||
padding: 16px 8px 0;
|
||||
}
|
||||
|
||||
.change-record-card {
|
||||
padding: 12px 14px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.change-record-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.change-record-header span,
|
||||
.change-record-line {
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.change-record-line {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.change-attachments {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.section-card {
|
||||
flex-wrap: wrap;
|
||||
border-radius: 8px;
|
||||
|
||||
@@ -118,7 +118,12 @@
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="8">
|
||||
<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-col>
|
||||
<el-col :xs="24" :sm="8">
|
||||
@@ -593,6 +598,7 @@ async function openEdit(row: OnboardingTemplate) {
|
||||
}
|
||||
|
||||
async function save() {
|
||||
form.templateCode = form.templateCode.trim();
|
||||
if (
|
||||
!form.templateCode
|
||||
|| !form.templateName
|
||||
@@ -602,6 +608,9 @@ async function save() {
|
||||
) {
|
||||
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)) {
|
||||
return ElMessage.warning("请完整选择至少一份签署材料模板及版本");
|
||||
}
|
||||
|
||||
@@ -174,7 +174,7 @@
|
||||
</div>
|
||||
|
||||
<el-row :gutter="16">
|
||||
<el-col :xs="24" :sm="6">
|
||||
<el-col :xs="24" :sm="8">
|
||||
<el-form-item label="所属公司" required>
|
||||
<el-select
|
||||
v-model="form.companyId"
|
||||
@@ -188,7 +188,7 @@
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :xs="24" :sm="6">
|
||||
<el-col :xs="24" :sm="8">
|
||||
<el-form-item label="所属部门" required>
|
||||
<el-select
|
||||
v-model="form.departmentId"
|
||||
@@ -203,7 +203,7 @@
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :xs="24" :sm="6">
|
||||
<el-col :xs="24" :sm="8">
|
||||
<el-form-item label="入职岗位" required>
|
||||
<el-select
|
||||
v-model="form.positionId"
|
||||
@@ -218,7 +218,7 @@
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :xs="24" :sm="6">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item label="入职模板" required>
|
||||
<el-select
|
||||
v-model="form.onboardingTemplateId"
|
||||
@@ -232,6 +232,15 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item label="用工类型" required>
|
||||
<el-select v-model="form.employmentType" class="full-width">
|
||||
<el-option label="正式员工" value="full_time" />
|
||||
<el-option label="兼职员工" value="part_time" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
@@ -460,6 +469,9 @@
|
||||
<el-descriptions-item label="入职岗位">
|
||||
{{ copySourceTask?.positionName || "-" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="用工类型">
|
||||
{{ employmentTypeText(copySourceTask?.employmentType) }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<div class="copy-count-row">
|
||||
@@ -566,6 +578,7 @@
|
||||
<span>所属公司:<strong>{{ detail.task.companyName || '-' }}</strong></span>
|
||||
<span>所属部门:<strong>{{ detail.task.departmentName || '-' }}</strong></span>
|
||||
<span>入职岗位:<strong>{{ detail.task.positionName || '-' }}</strong></span>
|
||||
<span>用工类型:<strong>{{ employmentTypeText(detail.task.employmentType) }}</strong></span>
|
||||
<span>入职模板:<strong>{{ detail.task.onboardingTemplateName || '-' }}</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1703,6 +1716,7 @@ const form = reactive({
|
||||
departmentId: undefined as number | undefined,
|
||||
positionId: undefined as number | undefined,
|
||||
onboardingTemplateId: undefined as number | undefined,
|
||||
employmentType: "full_time" as "full_time" | "part_time",
|
||||
contractVariables: [] as Array<{
|
||||
contractTemplateVersionId: number;
|
||||
variableCode: string;
|
||||
@@ -1806,6 +1820,13 @@ const columns: ColumnProps<OnboardingTask>[] = [
|
||||
label: "所属公司 / 部门 / 岗位",
|
||||
minWidth: 220
|
||||
},
|
||||
{
|
||||
prop: "employmentType",
|
||||
label: "用工类型",
|
||||
minWidth: 100,
|
||||
align: "center",
|
||||
render: scope => employmentTypeText(scope.row.employmentType)
|
||||
},
|
||||
{
|
||||
prop: "taskStatus",
|
||||
label: "当前状态",
|
||||
@@ -1936,7 +1957,7 @@ const auditActionType = (action: string): EpTagType => {
|
||||
const auditActionText = (action: string) => {
|
||||
const map: Record<string, string> = {
|
||||
submit: "候选人提交入职资料",
|
||||
approve: "人事审核通过并创建正式员工",
|
||||
approve: "人事审核通过并创建员工档案",
|
||||
reject: "人事驳回退回材料",
|
||||
archive: "人事完成归档",
|
||||
revoke: "人事撤销任务"
|
||||
@@ -2258,6 +2279,7 @@ function openCreate() {
|
||||
departmentId: undefined,
|
||||
positionId: undefined,
|
||||
onboardingTemplateId: undefined,
|
||||
employmentType: "full_time",
|
||||
contractVariables: [],
|
||||
remark: ""
|
||||
});
|
||||
@@ -2290,6 +2312,7 @@ function buildTaskCreatePayload() {
|
||||
departmentId: form.departmentId,
|
||||
positionId: form.positionId,
|
||||
onboardingTemplateId: form.onboardingTemplateId,
|
||||
employmentType: form.employmentType,
|
||||
remark: form.remark.trim(),
|
||||
contractVariables: form.contractVariables.map(item => ({
|
||||
contractTemplateVersionId: item.contractTemplateVersionId,
|
||||
@@ -2544,7 +2567,7 @@ function buildContractVariableSaveValues(group: OnboardingTaskContractVariableGr
|
||||
async function approve(row: OnboardingTask) {
|
||||
const confirmed = await isMessageBoxConfirmed(
|
||||
ElMessageBox.confirm(
|
||||
"确认审核通过?系统将自动创建正式员工档案并锁定签署材料快照。",
|
||||
"确认审核通过?系统将自动创建员工档案并锁定签署材料快照。",
|
||||
"审核通过确认",
|
||||
{ type: "success", confirmButtonText: "审核通过", cancelButtonText: "取消" }
|
||||
)
|
||||
@@ -2694,6 +2717,10 @@ async function syncElectronicSign() {
|
||||
}
|
||||
}
|
||||
|
||||
function employmentTypeText(value?: string) {
|
||||
return value === "part_time" ? "兼职员工" : "正式员工";
|
||||
}
|
||||
|
||||
async function previewEmployeeSignedDocument(
|
||||
documentItem: OnboardingElectronicSignDocument
|
||||
) {
|
||||
|
||||
Reference in New Issue
Block a user