diff --git a/klp-hrm/src/main/java/com/klp/hrm/domain/HrmCertificate.java b/klp-hrm/src/main/java/com/klp/hrm/domain/HrmCertificate.java index a1c3ee1b..a20caed1 100644 --- a/klp-hrm/src/main/java/com/klp/hrm/domain/HrmCertificate.java +++ b/klp-hrm/src/main/java/com/klp/hrm/domain/HrmCertificate.java @@ -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; } diff --git a/klp-hrm/src/main/java/com/klp/hrm/domain/HrmContract.java b/klp-hrm/src/main/java/com/klp/hrm/domain/HrmContract.java index 6cd604a8..8d3e8b8d 100644 --- a/klp-hrm/src/main/java/com/klp/hrm/domain/HrmContract.java +++ b/klp-hrm/src/main/java/com/klp/hrm/domain/HrmContract.java @@ -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; } diff --git a/klp-hrm/src/main/java/com/klp/hrm/domain/bo/HrmCertificateBo.java b/klp-hrm/src/main/java/com/klp/hrm/domain/bo/HrmCertificateBo.java index b5861acf..53850477 100644 --- a/klp-hrm/src/main/java/com/klp/hrm/domain/bo/HrmCertificateBo.java +++ b/klp-hrm/src/main/java/com/klp/hrm/domain/bo/HrmCertificateBo.java @@ -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; } diff --git a/klp-hrm/src/main/java/com/klp/hrm/domain/bo/HrmContractBo.java b/klp-hrm/src/main/java/com/klp/hrm/domain/bo/HrmContractBo.java index 07fbd23a..cbea96da 100644 --- a/klp-hrm/src/main/java/com/klp/hrm/domain/bo/HrmContractBo.java +++ b/klp-hrm/src/main/java/com/klp/hrm/domain/bo/HrmContractBo.java @@ -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; } diff --git a/klp-hrm/src/main/java/com/klp/hrm/domain/vo/HrmCertificateVo.java b/klp-hrm/src/main/java/com/klp/hrm/domain/vo/HrmCertificateVo.java index 8c79a89d..b65cc0d9 100644 --- a/klp-hrm/src/main/java/com/klp/hrm/domain/vo/HrmCertificateVo.java +++ b/klp-hrm/src/main/java/com/klp/hrm/domain/vo/HrmCertificateVo.java @@ -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; diff --git a/klp-hrm/src/main/java/com/klp/hrm/domain/vo/HrmContractVo.java b/klp-hrm/src/main/java/com/klp/hrm/domain/vo/HrmContractVo.java index 7b486e30..53ca6820 100644 --- a/klp-hrm/src/main/java/com/klp/hrm/domain/vo/HrmContractVo.java +++ b/klp-hrm/src/main/java/com/klp/hrm/domain/vo/HrmContractVo.java @@ -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; diff --git a/klp-ui/src/api/hrm/index.js b/klp-ui/src/api/hrm/index.js index b66bafe9..0568c011 100644 --- a/klp-ui/src/api/hrm/index.js +++ b/klp-ui/src/api/hrm/index.js @@ -514,6 +514,74 @@ export function getFlowForm(formId) { method: 'get' }) } +// 流程模板 +export function listFlowTemplate(query) { + return request({ + url: '/hrm/flow/template/list', + method: 'get', + params: query + }) +} +export function getFlowTemplate(tplId) { + return request({ + url: `/hrm/flow/template/${tplId}`, + method: 'get' + }) +} +export function addFlowTemplate(data) { + return request({ + url: '/hrm/flow/template', + method: 'post', + data + }) +} +export function updateFlowTemplate(data) { + return request({ + url: '/hrm/flow/template', + method: 'put', + data + }) +} +export function delFlowTemplate(tplIds) { + return request({ + url: `/hrm/flow/template/${tplIds}`, + method: 'delete' + }) +} +// 流程节点 +export function listFlowNode(query) { + return request({ + url: '/hrm/flow/node/list', + method: 'get', + params: query + }) +} +export function getFlowNode(nodeId) { + return request({ + url: `/hrm/flow/node/${nodeId}`, + method: 'get' + }) +} +export function addFlowNode(data) { + return request({ + url: '/hrm/flow/node', + method: 'post', + data + }) +} +export function updateFlowNode(data) { + return request({ + url: '/hrm/flow/node', + method: 'put', + data + }) +} +export function delFlowNode(nodeIds) { + return request({ + url: `/hrm/flow/node/${nodeIds}`, + method: 'delete' + }) +} // 薪酬 export function listPayPlan(query) { @@ -619,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 }) diff --git a/klp-ui/src/components/FileUpload/index.vue b/klp-ui/src/components/FileUpload/index.vue index 63d2375a..0e834686 100644 --- a/klp-ui/src/components/FileUpload/index.vue +++ b/klp-ui/src/components/FileUpload/index.vue @@ -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那么取最后的名字 如果不是直接返回 diff --git a/klp-ui/src/views/hrm/attendance/index.vue b/klp-ui/src/views/hrm/attendance/index.vue index 1126d112..93b48b1d 100644 --- a/klp-ui/src/views/hrm/attendance/index.vue +++ b/klp-ui/src/views/hrm/attendance/index.vue @@ -1,12 +1,11 @@ @@ -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 { diff --git a/klp-ui/src/views/hrm/payroll/index.vue b/klp-ui/src/views/hrm/payroll/index.vue index 7db1f95b..856db98b 100644 --- a/klp-ui/src/views/hrm/payroll/index.vue +++ b/klp-ui/src/views/hrm/payroll/index.vue @@ -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() diff --git a/klp-ui/src/views/hrm/requests/index.vue b/klp-ui/src/views/hrm/requests/index.vue index b44dd8d3..fe40a7f9 100644 --- a/klp-ui/src/views/hrm/requests/index.vue +++ b/klp-ui/src/views/hrm/requests/index.vue @@ -5,7 +5,7 @@
{{ item.title }}
- 新增 + 新增 - + @@ -131,120 +138,14 @@
- -
- - 请假 - 加班 - 出差 - 用印 - - 先选类型,再填写必填项 -
- - - - - - - - - - - - -
+ + diff --git a/klp-ui/src/views/hrm/requests/seal.vue b/klp-ui/src/views/hrm/requests/seal.vue new file mode 100644 index 00000000..445a23f5 --- /dev/null +++ b/klp-ui/src/views/hrm/requests/seal.vue @@ -0,0 +1,133 @@ + + + + + diff --git a/klp-ui/src/views/hrm/requests/travel.vue b/klp-ui/src/views/hrm/requests/travel.vue new file mode 100644 index 00000000..564c95b0 --- /dev/null +++ b/klp-ui/src/views/hrm/requests/travel.vue @@ -0,0 +1,167 @@ + + + + +