协调L3页面,新增合同证书的文件上传逻辑

This commit is contained in:
2025-12-23 15:56:15 +08:00
parent 9f423b26f9
commit 04eace18c4
11 changed files with 577 additions and 67 deletions

View File

@@ -25,6 +25,10 @@ public class HrmCertificate extends BaseEntity implements Serializable {
private Date validFrom;
private Date validTo;
private String remark;
/**
* 证书附件 fileIds逗号分隔的 OSS ID
*/
private String fileIds;
@TableLogic
private Integer delFlag;
}

View File

@@ -25,6 +25,10 @@ public class HrmContract extends BaseEntity implements Serializable {
private Date endDate;
private String status;
private String remark;
/**
* 合同附件 fileIds逗号分隔的 OSS ID
*/
private String fileIds;
@TableLogic
private Integer delFlag;
}

View File

@@ -1,8 +1,10 @@
package com.klp.hrm.domain.bo;
import com.klp.common.core.domain.BaseEntity;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.format.annotation.DateTimeFormat;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
@@ -22,7 +24,15 @@ public class HrmCertificateBo extends BaseEntity {
private String certNo;
private String issuedBy;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date validFrom;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date validTo;
private String remark;
/**
* 证书附件 fileIds逗号分隔的 OSS ID
*/
private String fileIds;
}

View File

@@ -7,6 +7,8 @@ import lombok.EqualsAndHashCode;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import org.springframework.format.annotation.DateTimeFormat;
@Data
@EqualsAndHashCode(callSuper = true)
@@ -21,8 +23,16 @@ public class HrmContractBo extends BaseEntity {
private String contractNo;
private String contractType;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date startDate;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date endDate;
private String status;
private String remark;
/**
* 合同附件 fileIds逗号分隔的 OSS ID
*/
private String fileIds;
}

View File

@@ -5,6 +5,7 @@ import lombok.Data;
import java.io.Serializable;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
@Data
public class HrmCertificateVo implements Serializable {
@@ -21,11 +22,17 @@ public class HrmCertificateVo implements Serializable {
@Excel(name = "颁发机构")
private String issuedBy;
@Excel(name = "有效期开始", width = 20, dateFormat = "yyyy-MM-dd")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date validFrom;
@Excel(name = "有效期结束", width = 20, dateFormat = "yyyy-MM-dd")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date validTo;
@Excel(name = "备注")
private String remark;
/**
* 证书附件 fileIds逗号分隔的 OSS ID
*/
private String fileIds;
private String createBy;
private Date createTime;
private String updateBy;

View File

@@ -5,6 +5,7 @@ import lombok.Data;
import java.io.Serializable;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
@Data
public class HrmContractVo implements Serializable {
@@ -19,13 +20,19 @@ public class HrmContractVo implements Serializable {
@Excel(name = "合同类型")
private String contractType;
@Excel(name = "开始日期", width = 20, dateFormat = "yyyy-MM-dd")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date startDate;
@Excel(name = "结束日期", width = 20, dateFormat = "yyyy-MM-dd")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date endDate;
@Excel(name = "状态")
private String status;
@Excel(name = "备注")
private String remark;
/**
* 合同附件 fileIds逗号分隔的 OSS ID
*/
private String fileIds;
private String createBy;
private Date createTime;
private String updateBy;

View File

@@ -687,7 +687,7 @@ export function delPayslip(slipIds) {
// 指标快照
export function listStatSnapshot(query) {
return request({
url: '/hrm/stat/snapshot/list',
url: '/hrm/pay/stat/list',
method: 'get',
params: query
})

View File

@@ -48,6 +48,11 @@ export default {
props: {
// 值
value: [String, Object, Array],
// 额外的自定义校验函数,返回 false 阻止上传
beforeValidate: {
type: Function,
default: null,
},
// 数量限制
limit: {
type: Number,
@@ -127,6 +132,15 @@ export default {
methods: {
// 上传前校检格式和大小
handleBeforeUpload(file) {
// 额外自定义校验
if (this.beforeValidate) {
const pass = this.beforeValidate(file);
if (pass === false) {
// 防止残留文件
this.resetUpload();
return false;
}
}
// 校检文件类型
if (this.fileType) {
const fileName = file.name.split('.');
@@ -189,6 +203,17 @@ export default {
this.$modal.closeLoading();
}
},
// 重置上传状态(外部可调用)
resetUpload() {
this.number = 0;
this.uploadList = [];
this.fileList = [];
this.$emit("input", "");
if (this.$refs.fileUpload) {
this.$refs.fileUpload.clearFiles();
}
this.$modal.closeLoading();
},
// 获取文件名称
getFileName(name) {
// 如果是url那么取最后的名字 如果不是直接返回

View File

@@ -1,12 +1,11 @@
<template>
<div class="hrm-page">
<section class="panel-grid triple">
<el-card class="metal-panel" shadow="hover">
<div slot="header" class="panel-header">
<span>班次</span>
<div class="hrm-page attendance-page">
<section class="panel-grid">
<el-card class="metal-panel flat" shadow="never">
<div class="card-toolbar">
<div class="actions-inline">
<el-button size="mini" type="primary" icon="el-icon-plus" @click="openShiftDialog()">新增</el-button>
<el-button size="mini" icon="el-icon-refresh" @click="loadShift">刷新</el-button>
<el-button size="mini" type="primary" plain icon="el-icon-plus" @click="openShiftDialog()">新增</el-button>
<el-button size="mini" plain icon="el-icon-refresh" @click="loadShift">刷新</el-button>
</div>
</div>
<el-table :data="shiftList" v-loading="shiftLoading" height="320" stripe>
@@ -31,9 +30,8 @@
</el-table>
</el-card>
<el-card class="metal-panel" shadow="hover">
<div slot="header" class="panel-header">
<span>排班</span>
<el-card class="metal-panel flat" shadow="never">
<div class="card-toolbar">
<div class="actions-inline">
<el-date-picker
v-model="scheduleQuery.date"
@@ -43,8 +41,8 @@
style="width: 140px"
@change="loadSchedule"
/>
<el-button size="mini" type="primary" @click="loadSchedule">查询</el-button>
<el-button size="mini" icon="el-icon-plus" @click="openScheduleDialog()">新增</el-button>
<el-button size="mini" type="primary" plain @click="loadSchedule">查询</el-button>
<el-button size="mini" plain icon="el-icon-plus" @click="openScheduleDialog()">新增</el-button>
</div>
</div>
<el-table :data="scheduleList" v-loading="scheduleLoading" height="320" stripe>
@@ -69,9 +67,8 @@
</el-table>
</el-card>
<el-card class="metal-panel" shadow="hover">
<div slot="header" class="panel-header">
<span>打卡与考勤结果</span>
<el-card class="metal-panel flat wide" shadow="never">
<div class="card-toolbar">
<div class="actions-inline">
<el-date-picker
v-model="punchQuery.range"
@@ -448,45 +445,66 @@ export default {
</script>
<style lang="scss" scoped>
.hrm-page {
padding: 16px 20px 32px;
background: #f8f9fb;
.attendance-page {
padding: 0;
background: transparent;
}
.panel-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 14px;
grid-template-columns: 1fr 1fr;
gap: 12px;
align-items: start;
}
.metal-panel {
border: 1px solid #d7d9df;
border-radius: 10px;
background: #fff;
border: none;
border-radius: 0;
background: transparent;
box-shadow: none;
}
.panel-header {
.metal-panel :deep(.el-card__body) {
padding: 0;
background: transparent;
}
.card-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
font-weight: 600;
color: #303133;
justify-content: flex-end;
padding: 4px 0 8px;
}
.actions-inline {
display: flex;
gap: 8px;
align-items: center;
}
.flat :deep(.el-card__body) {
padding: 0;
}
.flat {
padding: 0;
}
.wide {
grid-column: span 2;
}
.flat :deep(.el-table),
.flat :deep(.el-table__body-wrapper),
.flat :deep(.el-table__header-wrapper),
.flat :deep(.el-table__empty-block) {
background: transparent;
}
.dual-tables {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.table-half {
border: 1px dashed #e6e8ed;
border-radius: 8px;
padding: 8px;
border: none;
border-radius: 0;
padding: 0;
background: transparent;
}
.table-title {
font-weight: 600;
margin-bottom: 6px;
color: #1f2937;
}
.exception-tag {
color: #409eff;
@@ -499,6 +517,9 @@ export default {
.panel-grid {
grid-template-columns: 1fr;
}
.wide {
grid-column: span 1;
}
.dual-tables {
grid-template-columns: 1fr;
}

View File

@@ -4,7 +4,10 @@
<el-card class="metal-panel" shadow="hover">
<div slot="header" class="panel-header">
<span>组织树</span>
<el-button size="mini" icon="el-icon-refresh" @click="loadOrg">刷新</el-button>
<div class="actions-inline">
<el-button size="mini" type="primary" icon="el-icon-plus" @click="openOrgDialog()">新增组织</el-button>
<el-button size="mini" icon="el-icon-refresh" @click="loadOrg">刷新</el-button>
</div>
</div>
<el-tree
v-loading="orgLoading"
@@ -46,6 +49,7 @@
<el-option label="离职" value="inactive" />
</el-select>
<el-button size="mini" type="primary" icon="el-icon-search" @click="loadEmployee">查询</el-button>
<el-button size="mini" type="primary" icon="el-icon-plus" @click="openEmpDialog">新增员工</el-button>
</div>
</div>
<el-table :data="employeeList" v-loading="empLoading" height="700" stripe @row-click="openDetail">
@@ -184,17 +188,154 @@
</el-drawer>
<el-dialog
title="合同"
:visible.sync="contractDialogVisible"
width="480px"
title="新增员工"
:visible.sync="empDialogVisible"
width="520px"
append-to-body
>
<el-form ref="empFormRef" :model="empForm" :rules="empRules" label-width="100px" size="small">
<el-form-item label="工号" prop="empNo">
<el-input v-model="empForm.empNo" />
</el-form-item>
<el-form-item label="姓名" prop="empName">
<el-input v-model="empForm.empName" />
</el-form-item>
<el-form-item label="性别">
<el-select v-model="empForm.gender" clearable placeholder="选择性别" style="width: 100%">
<el-option label="男" value="male" />
<el-option label="女" value="female" />
</el-select>
</el-form-item>
<el-form-item label="手机">
<el-input v-model="empForm.mobile" />
</el-form-item>
<el-form-item label="雇佣类型">
<el-input v-model="empForm.employmentType" placeholder="如 全职/实习" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="empForm.status" style="width: 100%">
<el-option label="在职" value="active" />
<el-option label="离职" value="inactive" />
</el-select>
</el-form-item>
<el-form-item label="入职日期">
<el-date-picker v-model="empForm.hireDate" type="date" placeholder="选择日期" style="width: 100%" />
</el-form-item>
<el-form-item label="所属组织" prop="mainOrgId">
<el-select v-model="empForm.mainOrgId" filterable placeholder="选择组织" style="width: 100%">
<el-option
v-for="org in flatOrgOptions"
:key="org.orgId"
:label="org.label"
:value="org.orgId"
/>
</el-select>
</el-form-item>
<el-form-item label="备注">
<el-input v-model="empForm.remark" type="textarea" :rows="2" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="empDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="empSubmitting" @click="submitEmp">保存</el-button>
</div>
</el-dialog>
<el-dialog
title="新增组织"
:visible.sync="orgDialogVisible"
width="620px"
append-to-body
class="org-dialog"
>
<el-form
ref="orgFormRef"
:model="orgForm"
:rules="orgRules"
label-width="110px"
size="small"
label-position="top"
class="org-form"
>
<el-row :gutter="14">
<el-col :span="12">
<el-form-item label="组织名称" prop="orgName">
<div class="inline-field">
<el-input v-model="orgForm.orgName" placeholder="如:生产一部 / 市场部" />
<el-button type="primary" icon="el-icon-magic-stick" plain size="mini" @click="autoFillOrgName">智能生成</el-button>
</div>
<div class="field-hint">支持快速生成示例名称可自行修改</div>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="组织类型" prop="orgType">
<el-select v-model="orgForm.orgType" placeholder="选择类型" filterable style="width: 100%">
<el-option label="部门" value="department" />
<el-option label="事业部" value="division" />
<el-option label="公司" value="company" />
<el-option label="项目组" value="project" />
<el-option label="其他" value="other" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="14">
<el-col :span="24">
<el-form-item label="上级组织" prop="parentId">
<el-cascader
v-model="orgForm.parentId"
:options="orgTree"
:props="{ label: 'orgName', value: 'orgId', children: 'children', emitPath: false, checkStrictly: true }"
clearable
filterable
placeholder="默认取当前选中的组织,可搜索"
style="width: 100%"
/>
<div class="field-hint">留空则创建为顶级组织</div>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="备注" prop="remark">
<el-input v-model="orgForm.remark" type="textarea" :rows="2" placeholder="补充说明(可选)" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="orgDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="orgSubmitting" @click="submitOrg">保存</el-button>
</div>
</el-dialog>
<el-dialog
title="劳动合同"
:visible.sync="contractDialogVisible"
width="520px"
append-to-body
>
<el-alert
type="info"
show-icon
class="contract-hint"
title="上传后自动用文件名填充合同编号"
description="建议文件名格式合同编号_员工名.pdf"
/>
<el-form :model="contractForm" label-width="100px" size="small">
<el-form-item label="合同编号">
<el-input v-model="contractForm.contractNo" />
<el-form-item label="合同文件" prop="file">
<file-upload
ref="contractUploader"
v-model="contractForm.fileIds"
:limit="1"
:file-size="50"
:file-type="['pdf','doc','docx','jpg','png']"
:before-validate="validateContractBeforeUpload"
@success="handleContractUploadSuccess"
/>
<div class="field-hint">仅限 1 个文件文件名将自动写入合同编号</div>
</el-form-item>
<el-form-item label="合同编号" prop="contractNo">
<el-input v-model="contractForm.contractNo" placeholder="上传后自动带出,可手动调整" />
</el-form-item>
<el-form-item label="类型">
<el-input v-model="contractForm.contractType" />
<el-input v-model="contractForm.contractType" placeholder="如:劳动合同/实习协议" />
</el-form-item>
<el-form-item label="开始日期">
<el-date-picker v-model="contractForm.startDate" type="date" placeholder="开始" style="width: 100%" />
@@ -203,10 +344,10 @@
<el-date-picker v-model="contractForm.endDate" type="date" placeholder="结束" style="width: 100%" />
</el-form-item>
<el-form-item label="状态">
<el-input v-model="contractForm.status" />
<el-input v-model="contractForm.status" placeholder="如:生效/签署中" />
</el-form-item>
<el-form-item label="备注">
<el-input v-model="contractForm.remark" type="textarea" :rows="2" />
<el-input v-model="contractForm.remark" type="textarea" :rows="2" placeholder="补充说明" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
@@ -218,27 +359,53 @@
<el-dialog
title="证书"
:visible.sync="certDialogVisible"
width="480px"
width="720px"
append-to-body
>
<el-alert
type="info"
show-icon
class="contract-hint"
title="上传证书附件,支持 pdf/doc/jpg/png"
description="可登记证书编号、签发机构与有效期起始"
/>
<el-form :model="certForm" label-width="100px" size="small">
<el-form-item label="证书名称">
<el-input v-model="certForm.certName" />
</el-form-item>
<el-form-item label="证书编号">
<el-input v-model="certForm.certNo" />
</el-form-item>
<el-form-item label="签发机构">
<el-input v-model="certForm.issuedBy" />
</el-form-item>
<el-form-item label="有效期自">
<el-date-picker v-model="certForm.validFrom" type="date" placeholder="开始" style="width: 100%" />
</el-form-item>
<el-form-item label="有效期至">
<el-date-picker v-model="certForm.validTo" type="date" placeholder="结束" style="width: 100%" />
<el-row :gutter="12">
<el-col :span="12">
<el-form-item label="证书名称">
<el-input v-model="certForm.certName" placeholder="如:安全生产证书" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="证书编号">
<el-input v-model="certForm.certNo" placeholder="编号/执照号(可选)" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="12">
<el-col :span="12">
<el-form-item label="签发机构">
<el-input v-model="certForm.issuedBy" placeholder="颁发单位" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="有效期起">
<el-date-picker v-model="certForm.validFrom" type="date" placeholder="选择日期" style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="证书附件">
<file-upload
v-model="certForm.fileIds"
:limit="1"
:file-size="20"
:file-type="['pdf','doc','docx','jpg','png']"
:is-show-tip="false"
/>
<div class="field-hint">仅限 1 个附件最大 20MB</div>
</el-form-item>
<el-form-item label="备注">
<el-input v-model="certForm.remark" type="textarea" :rows="2" />
<el-input v-model="certForm.remark" type="textarea" :rows="2" placeholder="补充说明(可选)" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
@@ -287,6 +454,7 @@
<el-button type="primary" :loading="relSubmitting" @click="submitRel">保存</el-button>
</div>
</el-dialog>
</div>
</template>
@@ -306,8 +474,12 @@ import {
listCertificate,
addCertificate,
updateCertificate,
delCertificate
delCertificate,
addEmployee,
addOrg
} from '@/api/hrm'
import { delOss } from '@/api/system/oss'
import FileUpload from '@/components/FileUpload'
export default {
name: 'HrmOrgEmployee',
@@ -319,13 +491,30 @@ export default {
employeeList: [],
empLoading: false,
empQuery: { empName: '', status: undefined, mainOrgId: undefined },
empDialogVisible: false,
empSubmitting: false,
empForm: { empNo: '', empName: '', gender: '', mobile: '', employmentType: '', status: 'active', hireDate: '', remark: '', mainOrgId: undefined },
empRules: {
empNo: [{ required: true, message: '请输入工号', trigger: 'blur' }],
empName: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
mainOrgId: [{ required: true, message: '请选择组织', trigger: 'change' }],
status: [{ required: true, message: '请选择状态', trigger: 'change' }]
},
detailVisible: false,
detailEmp: null,
contractList: [],
contractLoading: false,
contractDialogVisible: false,
contractSubmitting: false,
contractForm: {},
contractForm: {
contractNo: '',
contractType: '',
startDate: '',
endDate: '',
status: 'active',
remark: '',
fileIds: ''
},
certList: [],
certLoading: false,
certDialogVisible: false,
@@ -343,19 +532,35 @@ export default {
},
positionOptions: [],
positionLoading: false,
flatOrgOptions: []
flatOrgOptions: [],
orgDialogVisible: false,
orgSubmitting: false,
orgForm: { orgName: '', orgType: '', parentId: undefined, orgCode: '', remark: '' },
orgRules: {
orgName: [{ required: true, message: '请输入组织名称', trigger: 'blur' }]
}
}
},
created() {
this.loadOrg()
this.loadEmployee()
this.loadPositions()
},
components: {
FileUpload
},
methods: {
loadTree() {
// 兼容旧调用,直接复用 loadOrg
this.loadOrg()
},
renderOrg(orgId) {
const found = this.flatOrgOptions.find(o => o.orgId === orgId)
return found ? found.label : orgId
},
renderPosition(positionId) {
const found = this.positionOptions.find(p => p.positionId === positionId)
return found ? (found.positionName || positionId) : positionId
},
statusType(status) {
if (!status) return 'info'
const map = { pending: 'warning', draft: 'info', approved: 'success', rejected: 'danger', active: 'success', inactive: 'info' }
@@ -412,6 +617,55 @@ export default {
this.empQuery.mainOrgId = node.orgId
this.loadEmployee()
},
openOrgDialog() {
this.orgForm = {
orgName: '',
orgType: '',
parentId: this.orgSelected || undefined,
orgCode: '',
remark: ''
}
this.orgDialogVisible = true
this.$nextTick(() => this.$refs.orgFormRef && this.$refs.orgFormRef.clearValidate())
},
buildOrgCode(name) {
const clean = (name || '').replace(/\s+/g, '').replace(/[^\w\u4e00-\u9fa5]/g, '')
if (clean) return clean.slice(0, 20)
return `ORG${Date.now()}`
},
autoFillOrgName() {
const typeLabelMap = {
department: '部门',
division: '事业部',
company: '公司',
project: '项目组',
other: '组织'
}
const bases = ['运营', '生产', '市场', '研发', '交付', '采购', '质控', '客服', '财务', '人力']
const randomBase = bases[Math.floor(Math.random() * bases.length)]
const typeLabel = typeLabelMap[this.orgForm.orgType] || '部门'
const suffix = new Date().getMilliseconds().toString().padStart(3, '0')
this.orgForm.orgName = `${randomBase}${typeLabel}${suffix}`
},
submitOrg() {
this.$refs.orgFormRef.validate(valid => {
if (!valid) return
this.orgSubmitting = true
const payload = { ...this.orgForm }
if (!payload.orgCode) {
payload.orgCode = this.buildOrgCode(payload.orgName)
}
addOrg(payload)
.then(() => {
this.$message.success('新增成功')
this.orgDialogVisible = false
this.loadOrg()
})
.finally(() => {
this.orgSubmitting = false
})
})
},
loadEmployee() {
if (!this.empQuery.mainOrgId) return
this.empLoading = true
@@ -423,6 +677,47 @@ export default {
this.empLoading = false
})
},
openEmpDialog() {
const defaultOrg = this.orgSelected || (this.orgTree[0] && this.orgTree[0].orgId) || undefined
this.empForm = {
empNo: '',
empName: '',
gender: '',
mobile: '',
employmentType: '',
status: 'active',
hireDate: '',
remark: '',
mainOrgId: defaultOrg
}
this.empDialogVisible = true
this.$nextTick(() => this.$refs.empFormRef && this.$refs.empFormRef.clearValidate())
},
formatDateOnly(val) {
if (!val) return ''
const d = new Date(val)
const p = n => (n < 10 ? `0${n}` : n)
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} 00:00:00`
},
submitEmp() {
this.$refs.empFormRef.validate(valid => {
if (!valid) return
this.empSubmitting = true
const payload = { ...this.empForm }
if (payload.hireDate) {
payload.hireDate = this.formatDateOnly(payload.hireDate)
}
addEmployee(payload)
.then(() => {
this.$message.success('新增成功')
this.empDialogVisible = false
this.loadEmployee()
})
.finally(() => {
this.empSubmitting = false
})
})
},
openDetail(row) {
this.detailEmp = row
this.detailVisible = true
@@ -440,15 +735,73 @@ export default {
this.contractLoading = false
})
},
handleContractUploadSuccess(fileList) {
const arr = Array.isArray(fileList) ? fileList : fileList ? [fileList] : []
if (!arr.length) return
const first = arr[0]
const name = (first && first.name) || ''
if (!name) return
// 二次兜底校验,防止后端已接收但命名不合规
if (!this.validateContractBeforeUpload(first)) {
this.resetContractUpload(first)
return
}
const base = name.split('/').pop()
const withoutExt = base ? base.replace(/\.[^.]+$/, '') : ''
const no = withoutExt && withoutExt.indexOf('_') !== -1 ? withoutExt.split('_')[0] : ''
if (no && !this.contractForm.contractNo) {
this.contractForm.contractNo = no
}
},
validateContractBeforeUpload(file) {
const name = file && file.name
if (!name) return false
const base = name.split('/').pop()
const withoutExt = base ? base.replace(/\.[^.]+$/, '') : ''
if (!withoutExt || withoutExt.indexOf('_') === -1) {
this.$message.error('文件名需包含编号与员工名并用下划线分隔例如合同编号_员工名.pdf')
this.resetContractUpload()
return false
}
const no = withoutExt.split('_')[0]
const validNo = /^[A-Za-z0-9-]+$/.test(no)
if (!validNo) {
this.$message.error('合同编号仅支持字母/数字/短横线,请调整文件名')
this.resetContractUpload()
return false
}
return true
},
async resetContractUpload(firstFile) {
// 清前端状态
this.contractForm.fileIds = ''
this.contractForm.contractNo = ''
const uploader = this.$refs.contractUploader
if (uploader && uploader.resetUpload) {
uploader.resetUpload()
}
// 仅在有 ossId 时请求删除
const ossId = firstFile && firstFile.ossId
if (ossId) {
try {
await delOss(ossId)
} catch (e) {
// 忽略删除异常
}
}
},
openContractDialog(row) {
this.contractForm = row ? { ...row } : { empId: this.detailEmp?.empId, status: 'active' }
this.contractForm = row ? { ...row } : { empId: this.detailEmp?.empId, status: 'active', fileIds: '', contractNo: '' }
this.contractDialogVisible = true
},
submitContract() {
if (!this.detailEmp) return
this.contractSubmitting = true
const api = this.contractForm.contractId ? updateContract : addContract
api({ ...this.contractForm, empId: this.detailEmp.empId })
const payload = { ...this.contractForm, empId: this.detailEmp.empId }
if (payload.startDate) payload.startDate = this.formatDateOnly(payload.startDate)
if (payload.endDate) payload.endDate = this.formatDateOnly(payload.endDate)
api(payload)
.then(() => {
this.$message.success('已保存')
this.contractDialogVisible = false
@@ -519,7 +872,10 @@ export default {
if (!valid) return
this.relSubmitting = true
const api = this.relForm.relId ? updateEmpOrgPosition : addEmpOrgPosition
api(this.relForm)
const payload = { ...this.relForm }
if (payload.startDate) payload.startDate = this.formatDateOnly(payload.startDate)
if (payload.endDate) payload.endDate = this.formatDateOnly(payload.endDate)
api(payload)
.then(() => {
this.$message.success('已保存')
this.relDialogVisible = false
@@ -539,14 +895,23 @@ export default {
})
},
openCertDialog(row) {
this.certForm = row ? { ...row } : { empId: this.detailEmp?.empId }
this.certForm = row
? { ...row }
: {
empId: this.detailEmp?.empId,
fileIds: '',
issuedBy: '',
certNo: ''
}
this.certDialogVisible = true
},
submitCert() {
if (!this.detailEmp) return
this.certSubmitting = true
const api = this.certForm.certId ? updateCertificate : addCertificate
api({ ...this.certForm, empId: this.detailEmp.empId })
const payload = { ...this.certForm, empId: this.detailEmp.empId }
if (payload.validFrom) payload.validFrom = this.formatDateOnly(payload.validFrom)
api(payload)
.then(() => {
this.$message.success('已保存')
this.certDialogVisible = false
@@ -618,7 +983,57 @@ export default {
width: 100%;
}
.tree-tag {
margin-left: 8px;
margin: 0;
align-self: center;
height: 22px;
line-height: 22px;
padding: 0 8px;
}
.org-dialog ::v-deep .el-dialog__body {
background: linear-gradient(180deg, #fbfcff 0%, #f7f9fc 100%);
padding: 18px 20px 10px;
}
.org-form .el-form-item {
margin-bottom: 14px;
}
.org-form .el-input-group__append {
padding: 0;
background: transparent;
border: none;
}
.org-form .el-input-group__append .el-button {
height: 32px;
margin: 0;
border-radius: 0 4px 4px 0;
}
.org-form .el-form-item__label {
font-weight: 600;
color: #3c4257;
}
.org-dialog ::v-deep .el-dialog__header {
border-bottom: 1px solid #ebeef5;
padding-bottom: 10px;
}
.inline-field {
display: flex;
align-items: center;
gap: 6px;
}
.inline-field .el-input {
flex: 1;
}
.inline-field .el-button {
border-radius: 6px;
box-shadow: 0 2px 6px rgba(64, 158, 255, 0.2);
height: 32px;
line-height: 32px;
padding: 0 12px;
}
.field-hint {
color: #9aa2b1;
font-size: 12px;
line-height: 16px;
margin-top: 4px;
}
@media (max-width: 1200px) {
.panel-grid {

View File

@@ -328,6 +328,13 @@ export default {
}
}
},
computed: {
latestStatDate() {
const dates = (this.statList || []).map(i => i.statDate).filter(Boolean)
if (!dates.length) return ''
return dates.sort().pop()
}
},
created() {
this.loadPayPlan()
this.loadPayRun()