This commit is contained in:
fuenmao
2026-09-09 14:57:46 +08:00
parent 709e02a04f
commit 68704a091b
4 changed files with 685 additions and 15 deletions
+57
View File
@@ -66,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;
@@ -294,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;
@@ -813,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",
@@ -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() { listFormFields() {
return moduleRequest("/biz/onboarding/entry-config/").get< return moduleRequest("/biz/onboarding/entry-config/").get<
OnboardingFormField[] OnboardingFormField[]
+585 -8
View File
@@ -1,8 +1,37 @@
<template> <template>
<div class="table-box"> <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"> <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> </template>
</ProTable> </ProTable>
@@ -20,7 +49,12 @@
<el-tag size="small" :type="detail.employee.gender === '女' ? 'danger' : 'primary'" effect="plain"> <el-tag size="small" :type="detail.employee.gender === '女' ? 'danger' : 'primary'" effect="plain">
{{ detail.employee.gender || "未知" }} {{ detail.employee.gender || "未知" }}
</el-tag> </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>
<div class="employee-meta-row"> <div class="employee-meta-row">
<span>所属公司:{{ detail.employee.companyName || "-" }}</span> <span>所属公司:{{ detail.employee.companyName || "-" }}</span>
@@ -86,8 +120,12 @@
}}</el-descriptions-item> }}</el-descriptions-item>
<el-descriptions-item label="员工编号">{{ detail.employee.employeeNo || employeeInfo.employeeNo || "-" }}</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="入职日期">{{ formatDate(detail.employee.entryDate) }}</el-descriptions-item>
<el-descriptions-item label="社保缴纳单位">{{ detail.employee.socialInsuranceCompany || "-" }}</el-descriptions-item> <el-descriptions-item label="用工类型">{{ employmentTypeText(detail.employee.employmentType) }}</el-descriptions-item>
<el-descriptions-item label="聘用/材料性质">{{ detail.employee.contractNature || "全职劳动合同" }}</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.contractStartDate) }}</el-descriptions-item>
<el-descriptions-item label="签署材料终止日期">{{ formatDate(detail.employee.contractEndDate) }}</el-descriptions-item> <el-descriptions-item label="签署材料终止日期">{{ formatDate(detail.employee.contractEndDate) }}</el-descriptions-item>
<el-descriptions-item label="其他说明" :span="2">{{ <el-descriptions-item label="其他说明" :span="2">{{
@@ -152,6 +190,44 @@
</el-card> </el-card>
</el-tab-pane> </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. 员工入职资料与合同文件 --> <!-- 2. 员工入职资料与合同文件 -->
<el-tab-pane label="员工资料信息" name="materials"> <el-tab-pane label="员工资料信息" name="materials">
<el-card shadow="never" class="section-card"> <el-card shadow="never" class="section-card">
@@ -284,16 +360,135 @@
</el-tabs> </el-tabs>
</div> </div>
<template #footer> <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> <el-button @click="detailVisible = false">关闭</el-button>
</template> </template>
</el-drawer> </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> </div>
</template> </template>
<script setup lang="ts" name="onboardingEmployee"> <script setup lang="ts" name="onboardingEmployee">
import { computed, onBeforeUnmount, onMounted, reactive, ref } from "vue"; import { computed, onBeforeUnmount, onMounted, reactive, ref } from "vue";
import { Document, Download, UserFilled } from "@element-plus/icons-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 { import {
onboardingTaskApi, onboardingTaskApi,
type OnboardingEmployee, type OnboardingEmployee,
@@ -302,11 +497,36 @@ import {
type OnboardingPositionOption type OnboardingPositionOption
} 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 { getMessageBoxPromptResult, isMessageBoxConfirmed } from "@/utils/messageBox";
const proTable = ref<ProTableInstance>(); const proTable = ref<ProTableInstance>();
const detailVisible = ref(false); const detailVisible = ref(false);
const activeTab = ref("basic"); const activeTab = ref("basic");
const detail = ref<any>(); 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 employeeInfo = computed<Record<string, any>>(() => detail.value?.employeeInfo || {});
const materialPreviewUrls = reactive<Record<number, string>>({}); const materialPreviewUrls = reactive<Record<number, string>>({});
const materialPreviewLoading = reactive<Record<number, boolean>>({}); const materialPreviewLoading = reactive<Record<number, boolean>>({});
@@ -570,6 +790,17 @@ const columns: ColumnProps<OnboardingEmployee>[] = [
minWidth: 140, minWidth: 140,
search: { el: "input" } 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", prop: "gender",
label: "性别", label: "性别",
@@ -603,18 +834,44 @@ const columns: ColumnProps<OnboardingEmployee>[] = [
minWidth: 140, minWidth: 140,
render: scope => formatDate(scope.row.entryDate) 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", prop: "operation",
label: "操作", label: "操作",
fixed: "right", fixed: "right",
width: 160 width: 310
} }
]; ];
const auditActionText = (action: string) => { const auditActionText = (action: string) => {
const map: Record<string, string> = { const map: Record<string, string> = {
submit: "候选人提交入职资料", submit: "候选人提交入职资料",
approve: "人事复核通过并转正建档", approve: "人事审核通过并创建员工档案",
reject: "人事退回补充资料", reject: "人事退回补充资料",
archive: "任务归档", archive: "任务归档",
revoke: "任务撤销" revoke: "任务撤销"
@@ -622,6 +879,54 @@ const auditActionText = (action: string) => {
return map[action] || action; 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) => { const formatTime = (timeStr?: string) => {
if (!timeStr) return "-"; if (!timeStr) return "-";
return timeStr.replace("T", " ").substring(0, 19); return timeStr.replace("T", " ").substring(0, 19);
@@ -659,6 +964,233 @@ function resolveDownloadFileName(response: any, fallbackFileName?: string) {
return fallbackFileName || "员工资料文件"; 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) { async function download(fileId: number, fallbackFileName?: string) {
try { try {
const response: any = await onboardingTaskApi.downloadFile(fileId); const response: any = await onboardingTaskApi.downloadFile(fileId);
@@ -776,11 +1308,56 @@ onBeforeUnmount(clearMaterialPreviews);
.employee-meta-row { .employee-meta-row {
display: flex; display: flex;
flex-wrap: wrap;
gap: 16px; gap: 16px;
font-size: 13px; font-size: 13px;
color: #64748b; 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 { .section-card {
flex-wrap: wrap; flex-wrap: wrap;
border-radius: 8px; border-radius: 8px;
@@ -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">
@@ -593,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
@@ -602,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("请完整选择至少一份签署材料模板及版本");
} }
+33 -6
View File
@@ -174,7 +174,7 @@
</div> </div>
<el-row :gutter="16"> <el-row :gutter="16">
<el-col :xs="24" :sm="6"> <el-col :xs="24" :sm="8">
<el-form-item label="所属公司" required> <el-form-item label="所属公司" required>
<el-select <el-select
v-model="form.companyId" v-model="form.companyId"
@@ -188,7 +188,7 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :xs="24" :sm="6"> <el-col :xs="24" :sm="8">
<el-form-item label="所属部门" required> <el-form-item label="所属部门" required>
<el-select <el-select
v-model="form.departmentId" v-model="form.departmentId"
@@ -203,7 +203,7 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :xs="24" :sm="6"> <el-col :xs="24" :sm="8">
<el-form-item label="入职岗位" required> <el-form-item label="入职岗位" required>
<el-select <el-select
v-model="form.positionId" v-model="form.positionId"
@@ -218,7 +218,7 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :xs="24" :sm="6"> <el-col :xs="24" :sm="12">
<el-form-item label="入职模板" required> <el-form-item label="入职模板" required>
<el-select <el-select
v-model="form.onboardingTemplateId" v-model="form.onboardingTemplateId"
@@ -232,6 +232,15 @@
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-col> </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> </el-row>
</div> </div>
@@ -460,6 +469,9 @@
<el-descriptions-item label="入职岗位"> <el-descriptions-item label="入职岗位">
{{ copySourceTask?.positionName || "-" }} {{ copySourceTask?.positionName || "-" }}
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="用工类型">
{{ employmentTypeText(copySourceTask?.employmentType) }}
</el-descriptions-item>
</el-descriptions> </el-descriptions>
<div class="copy-count-row"> <div class="copy-count-row">
@@ -566,6 +578,7 @@
<span>所属公司:<strong>{{ detail.task.companyName || '-' }}</strong></span> <span>所属公司:<strong>{{ detail.task.companyName || '-' }}</strong></span>
<span>所属部门:<strong>{{ detail.task.departmentName || '-' }}</strong></span> <span>所属部门:<strong>{{ detail.task.departmentName || '-' }}</strong></span>
<span>入职岗位:<strong>{{ detail.task.positionName || '-' }}</strong></span> <span>入职岗位:<strong>{{ detail.task.positionName || '-' }}</strong></span>
<span>用工类型:<strong>{{ employmentTypeText(detail.task.employmentType) }}</strong></span>
<span>入职模板:<strong>{{ detail.task.onboardingTemplateName || '-' }}</strong></span> <span>入职模板:<strong>{{ detail.task.onboardingTemplateName || '-' }}</strong></span>
</div> </div>
</div> </div>
@@ -1703,6 +1716,7 @@ const form = reactive({
departmentId: undefined as number | undefined, departmentId: undefined as number | undefined,
positionId: undefined as number | undefined, positionId: undefined as number | undefined,
onboardingTemplateId: undefined as number | undefined, onboardingTemplateId: undefined as number | undefined,
employmentType: "full_time" as "full_time" | "part_time",
contractVariables: [] as Array<{ contractVariables: [] as Array<{
contractTemplateVersionId: number; contractTemplateVersionId: number;
variableCode: string; variableCode: string;
@@ -1806,6 +1820,13 @@ const columns: ColumnProps<OnboardingTask>[] = [
label: "所属公司 / 部门 / 岗位", label: "所属公司 / 部门 / 岗位",
minWidth: 220 minWidth: 220
}, },
{
prop: "employmentType",
label: "用工类型",
minWidth: 100,
align: "center",
render: scope => employmentTypeText(scope.row.employmentType)
},
{ {
prop: "taskStatus", prop: "taskStatus",
label: "当前状态", label: "当前状态",
@@ -1936,7 +1957,7 @@ const auditActionType = (action: string): EpTagType => {
const auditActionText = (action: string) => { const auditActionText = (action: string) => {
const map: Record<string, string> = { const map: Record<string, string> = {
submit: "候选人提交入职资料", submit: "候选人提交入职资料",
approve: "人事审核通过并创建正式员工", approve: "人事审核通过并创建员工档案",
reject: "人事驳回退回材料", reject: "人事驳回退回材料",
archive: "人事完成归档", archive: "人事完成归档",
revoke: "人事撤销任务" revoke: "人事撤销任务"
@@ -2258,6 +2279,7 @@ function openCreate() {
departmentId: undefined, departmentId: undefined,
positionId: undefined, positionId: undefined,
onboardingTemplateId: undefined, onboardingTemplateId: undefined,
employmentType: "full_time",
contractVariables: [], contractVariables: [],
remark: "" remark: ""
}); });
@@ -2290,6 +2312,7 @@ function buildTaskCreatePayload() {
departmentId: form.departmentId, departmentId: form.departmentId,
positionId: form.positionId, positionId: form.positionId,
onboardingTemplateId: form.onboardingTemplateId, onboardingTemplateId: form.onboardingTemplateId,
employmentType: form.employmentType,
remark: form.remark.trim(), remark: form.remark.trim(),
contractVariables: form.contractVariables.map(item => ({ contractVariables: form.contractVariables.map(item => ({
contractTemplateVersionId: item.contractTemplateVersionId, contractTemplateVersionId: item.contractTemplateVersionId,
@@ -2544,7 +2567,7 @@ function buildContractVariableSaveValues(group: OnboardingTaskContractVariableGr
async function approve(row: OnboardingTask) { async function approve(row: OnboardingTask) {
const confirmed = await isMessageBoxConfirmed( const confirmed = await isMessageBoxConfirmed(
ElMessageBox.confirm( ElMessageBox.confirm(
"确认审核通过?系统将自动创建正式员工档案并锁定签署材料快照。", "确认审核通过?系统将自动创建员工档案并锁定签署材料快照。",
"审核通过确认", "审核通过确认",
{ type: "success", confirmButtonText: "审核通过", cancelButtonText: "取消" } { type: "success", confirmButtonText: "审核通过", cancelButtonText: "取消" }
) )
@@ -2694,6 +2717,10 @@ async function syncElectronicSign() {
} }
} }
function employmentTypeText(value?: string) {
return value === "part_time" ? "兼职员工" : "正式员工";
}
async function previewEmployeeSignedDocument( async function previewEmployeeSignedDocument(
documentItem: OnboardingElectronicSignDocument documentItem: OnboardingElectronicSignDocument
) { ) {