hcm前端2版

This commit is contained in:
2025-12-22 16:53:48 +08:00
parent 40f96069ab
commit 007c450f63
8 changed files with 1671 additions and 41 deletions

View File

@@ -48,7 +48,7 @@
<el-button size="mini" type="primary" icon="el-icon-search" @click="loadEmployee">查询</el-button>
</div>
</div>
<el-table :data="employeeList" v-loading="empLoading" height="700" stripe>
<el-table :data="employeeList" v-loading="empLoading" height="700" stripe @row-click="openDetail">
<el-table-column label="工号" prop="empNo" min-width="110" />
<el-table-column label="姓名" prop="empName" min-width="120" />
<el-table-column label="性别" prop="gender" min-width="80" />
@@ -66,11 +66,248 @@
</el-table>
</el-card>
</section>
<el-drawer
title="员工档案"
:visible.sync="detailVisible"
size="60%"
append-to-body
>
<div v-if="detailEmp" class="detail-wrap">
<div class="basic-grid">
<el-card shadow="hover" class="metal-panel">
<div slot="header" class="panel-header">基础信息</div>
<el-descriptions :column="2" size="small" border>
<el-descriptions-item label="工号">{{ detailEmp.empNo }}</el-descriptions-item>
<el-descriptions-item label="姓名">{{ detailEmp.empName }}</el-descriptions-item>
<el-descriptions-item label="性别">{{ detailEmp.gender }}</el-descriptions-item>
<el-descriptions-item label="手机">{{ detailEmp.mobile }}</el-descriptions-item>
<el-descriptions-item label="雇佣类型">{{ detailEmp.employmentType }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="statusType(detailEmp.status)">{{ detailEmp.status }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="入职日期">{{ formatDate(detailEmp.hireDate) }}</el-descriptions-item>
<el-descriptions-item label="备注">{{ detailEmp.remark || '-' }}</el-descriptions-item>
</el-descriptions>
</el-card>
</div>
<el-card shadow="hover" class="metal-panel" style="margin-top:12px">
<div slot="header" class="panel-header">
<span>组织/岗位关系</span>
<el-button size="mini" type="primary" icon="el-icon-plus" @click="openRelDialog()">新增</el-button>
</div>
<el-table :data="relList" v-loading="relLoading" size="mini">
<el-table-column label="组织" prop="orgId" min-width="160">
<template slot-scope="scope">
{{ renderOrg(scope.row.orgId) }}
</template>
</el-table-column>
<el-table-column label="岗位" prop="positionId" min-width="150">
<template slot-scope="scope">
{{ renderPosition(scope.row.positionId) }}
</template>
</el-table-column>
<el-table-column label="主岗" prop="isPrimary" width="80">
<template slot-scope="scope">
<el-tag size="mini" :type="scope.row.isPrimary ? 'success' : 'info'">{{ scope.row.isPrimary ? '是' : '否' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="开始" prop="startDate" min-width="120">
<template slot-scope="scope">{{ formatDate(scope.row.startDate, 'date') }}</template>
</el-table-column>
<el-table-column label="结束" prop="endDate" min-width="120">
<template slot-scope="scope">{{ formatDate(scope.row.endDate, 'date') }}</template>
</el-table-column>
<el-table-column label="备注" prop="remark" min-width="140" show-overflow-tooltip />
<el-table-column label="操作" width="140" fixed="right">
<template slot-scope="scope">
<el-button size="mini" type="text" @click="openRelDialog(scope.row)">编辑</el-button>
<el-button size="mini" type="text" @click="delRel(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<el-card shadow="hover" class="metal-panel" style="margin-top:12px">
<div slot="header" class="panel-header">
<span>劳动合同</span>
<el-button size="mini" type="primary" icon="el-icon-plus" @click="openContractDialog()">新增</el-button>
</div>
<el-table :data="contractList" v-loading="contractLoading" size="mini">
<el-table-column label="编号" prop="contractNo" min-width="120" />
<el-table-column label="类型" prop="contractType" min-width="120" />
<el-table-column label="开始" prop="startDate" min-width="120">
<template slot-scope="scope">{{ formatDate(scope.row.startDate, 'date') }}</template>
</el-table-column>
<el-table-column label="结束" prop="endDate" min-width="120">
<template slot-scope="scope">{{ formatDate(scope.row.endDate, 'date') }}</template>
</el-table-column>
<el-table-column label="状态" prop="status" min-width="100" />
<el-table-column label="备注" prop="remark" min-width="140" show-overflow-tooltip />
<el-table-column label="操作" width="140" fixed="right">
<template slot-scope="scope">
<el-button size="mini" type="text" @click="openContractDialog(scope.row)">编辑</el-button>
<el-button size="mini" type="text" @click="delContractRow(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<el-card shadow="hover" class="metal-panel" style="margin-top:12px">
<div slot="header" class="panel-header">
<span>证书</span>
<el-button size="mini" type="primary" icon="el-icon-plus" @click="openCertDialog()">新增</el-button>
</div>
<el-table :data="certList" v-loading="certLoading" size="mini">
<el-table-column label="名称" prop="certName" min-width="140" />
<el-table-column label="证书编号" prop="certNo" min-width="140" />
<el-table-column label="签发机构" prop="issuedBy" min-width="140" />
<el-table-column label="有效期自" prop="validFrom" min-width="120">
<template slot-scope="scope">{{ formatDate(scope.row.validFrom, 'date') }}</template>
</el-table-column>
<el-table-column label="有效期至" prop="validTo" min-width="120">
<template slot-scope="scope">{{ formatDate(scope.row.validTo, 'date') }}</template>
</el-table-column>
<el-table-column label="备注" prop="remark" min-width="140" show-overflow-tooltip />
<el-table-column label="操作" width="140" fixed="right">
<template slot-scope="scope">
<el-button size="mini" type="text" @click="openCertDialog(scope.row)">编辑</el-button>
<el-button size="mini" type="text" @click="delCertRow(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
<div v-else class="preview-placeholder">请选择员工查看档案</div>
</el-drawer>
<el-dialog
title="合同"
:visible.sync="contractDialogVisible"
width="480px"
append-to-body
>
<el-form :model="contractForm" label-width="100px" size="small">
<el-form-item label="合同编号">
<el-input v-model="contractForm.contractNo" />
</el-form-item>
<el-form-item label="类型">
<el-input v-model="contractForm.contractType" />
</el-form-item>
<el-form-item label="开始日期">
<el-date-picker v-model="contractForm.startDate" type="date" placeholder="开始" style="width: 100%" />
</el-form-item>
<el-form-item label="结束日期">
<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-form-item>
<el-form-item label="备注">
<el-input v-model="contractForm.remark" type="textarea" :rows="2" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="contractDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="contractSubmitting" @click="submitContract">保存</el-button>
</div>
</el-dialog>
<el-dialog
title="证书"
:visible.sync="certDialogVisible"
width="480px"
append-to-body
>
<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-form-item>
<el-form-item label="备注">
<el-input v-model="certForm.remark" type="textarea" :rows="2" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="certDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="certSubmitting" @click="submitCert">保存</el-button>
</div>
</el-dialog>
<el-dialog
title="组织/岗位关系"
:visible.sync="relDialogVisible"
width="480px"
append-to-body
>
<el-form ref="relFormRef" :model="relForm" :rules="relRules" label-width="100px" size="small">
<el-form-item label="组织" prop="orgId">
<el-select v-model="relForm.orgId" 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="岗位" prop="positionId">
<el-select v-model="relForm.positionId" filterable placeholder="选择岗位" style="width: 100%">
<el-option v-for="pos in positionOptions" :key="pos.positionId" :label="pos.positionName || pos.positionId" :value="pos.positionId" />
</el-select>
</el-form-item>
<el-form-item label="主岗" prop="isPrimary">
<el-switch v-model="relForm.isPrimary" :active-value="1" :inactive-value="0" />
</el-form-item>
<el-form-item label="开始日期">
<el-date-picker v-model="relForm.startDate" type="date" placeholder="开始" style="width: 100%" />
</el-form-item>
<el-form-item label="结束日期">
<el-date-picker v-model="relForm.endDate" type="date" placeholder="结束" style="width: 100%" />
</el-form-item>
<el-form-item label="备注">
<el-input v-model="relForm.remark" type="textarea" :rows="2" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="relDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="relSubmitting" @click="submitRel">保存</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listOrg, listEmployee } from '@/api/hrm'
import {
listOrg,
listEmployee,
listPosition,
listEmpOrgPosition,
addEmpOrgPosition,
updateEmpOrgPosition,
delEmpOrgPosition,
listContract,
addContract,
updateContract,
delContract,
listCertificate,
addCertificate,
updateCertificate,
delCertificate
} from '@/api/hrm'
export default {
name: 'HrmOrgEmployee',
@@ -81,22 +318,54 @@ export default {
orgSelected: null,
employeeList: [],
empLoading: false,
empQuery: { empName: '', status: undefined, mainOrgId: undefined }
empQuery: { empName: '', status: undefined, mainOrgId: undefined },
detailVisible: false,
detailEmp: null,
contractList: [],
contractLoading: false,
contractDialogVisible: false,
contractSubmitting: false,
contractForm: {},
certList: [],
certLoading: false,
certDialogVisible: false,
certSubmitting: false,
certForm: {},
relList: [],
relLoading: false,
relDialogVisible: false,
relSubmitting: false,
relForm: {},
relRules: {
orgId: [{ required: true, message: '请选择组织', trigger: 'change' }],
positionId: [{ required: true, message: '请选择岗位', trigger: 'change' }],
isPrimary: [{ required: true, message: '请选择是否主岗', trigger: 'change' }]
},
positionOptions: [],
positionLoading: false,
flatOrgOptions: []
}
},
created() {
this.loadOrg()
this.loadEmployee()
this.loadPositions()
},
methods: {
loadTree() {
// 兼容旧调用,直接复用 loadOrg
this.loadOrg()
},
statusType(status) {
if (!status) return 'info'
const map = { pending: 'warning', draft: 'info', approved: 'success', rejected: 'danger', active: 'success', inactive: 'info' }
return map[status] || 'info'
},
formatDate(val) {
formatDate(val, mode = 'datetime') {
if (!val) return ''
const d = new Date(val)
const p = n => (n < 10 ? `0${n}` : n)
if (mode === 'date') return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`
},
loadOrg() {
@@ -107,6 +376,7 @@ export default {
this.orgTree = this.buildTree(rows)
if (!this.orgSelected && this.orgTree.length) this.orgSelected = this.orgTree[0].orgId
this.empQuery.mainOrgId = this.orgSelected
this.buildFlatOrgOptions(this.orgTree)
this.loadEmployee()
})
.finally(() => {
@@ -126,6 +396,17 @@ export default {
})
return roots
},
buildFlatOrgOptions(orgs) {
const flat = []
const walk = list => {
list.forEach(o => {
flat.push({ orgId: o.orgId, label: `${o.orgName || o.orgId}${o.parentName ? ` / ${o.parentName}` : ''}` })
if (o.children && o.children.length) walk(o.children)
})
}
walk(orgs || [])
this.flatOrgOptions = flat
},
handleOrgClick(node) {
this.orgSelected = node.orgId
this.empQuery.mainOrgId = node.orgId
@@ -141,6 +422,147 @@ export default {
.finally(() => {
this.empLoading = false
})
},
openDetail(row) {
this.detailEmp = row
this.detailVisible = true
this.loadContracts(row.empId)
this.loadCertificates(row.empId)
this.loadEmpOrgRel(row.empId)
},
loadContracts(empId) {
this.contractLoading = true
listContract({ empId, pageNum: 1, pageSize: 50 })
.then(res => {
this.contractList = res.rows || []
})
.finally(() => {
this.contractLoading = false
})
},
openContractDialog(row) {
this.contractForm = row ? { ...row } : { empId: this.detailEmp?.empId, status: 'active' }
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 })
.then(() => {
this.$message.success('已保存')
this.contractDialogVisible = false
this.loadContracts(this.detailEmp.empId)
})
.finally(() => {
this.contractSubmitting = false
})
},
delContractRow(row) {
this.$confirm('确认删除该合同吗?', '提示', { type: 'warning' }).then(() => {
delContract(row.contractId).then(() => {
this.$message.success('已删除')
this.loadContracts(this.detailEmp.empId)
})
})
},
loadCertificates(empId) {
if (!empId) return
this.certLoading = true
listCertificate({ empId, pageNum: 1, pageSize: 200 })
.then(res => {
this.certList = res.rows || []
})
.finally(() => {
this.certLoading = false
})
},
loadPositions() {
this.positionLoading = true
listPosition({ pageNum: 1, pageSize: 200 })
.then(res => {
this.positionOptions = res.rows || []
})
.finally(() => {
this.positionLoading = false
})
},
loadEmpOrgRel(empId) {
if (!empId) return
this.relLoading = true
listEmpOrgPosition({ empId, pageNum: 1, pageSize: 200 })
.then(res => {
this.relList = res.rows || []
})
.finally(() => {
this.relLoading = false
})
},
openRelDialog(row) {
if (!this.detailEmp) return
this.relForm = row
? { ...row }
: {
empId: this.detailEmp.empId,
orgId: this.detailEmp.mainOrgId || this.orgSelected || (this.orgTree[0] && this.orgTree[0].orgId),
positionId: '',
isPrimary: 0,
startDate: '',
endDate: '',
remark: ''
}
this.relDialogVisible = true
this.$nextTick(() => this.$refs.relFormRef && this.$refs.relFormRef.clearValidate())
},
submitRel() {
this.$refs.relFormRef.validate(valid => {
if (!valid) return
this.relSubmitting = true
const api = this.relForm.relId ? updateEmpOrgPosition : addEmpOrgPosition
api(this.relForm)
.then(() => {
this.$message.success('已保存')
this.relDialogVisible = false
this.loadEmpOrgRel(this.detailEmp.empId)
})
.finally(() => {
this.relSubmitting = false
})
})
},
delRel(row) {
this.$confirm('确认删除该关系吗?', '提示', { type: 'warning' }).then(() => {
delEmpOrgPosition(row.relId).then(() => {
this.$message.success('已删除')
this.loadEmpOrgRel(this.detailEmp.empId)
})
})
},
openCertDialog(row) {
this.certForm = row ? { ...row } : { empId: this.detailEmp?.empId }
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 })
.then(() => {
this.$message.success('已保存')
this.certDialogVisible = false
this.loadCertificates(this.detailEmp.empId)
})
.finally(() => {
this.certSubmitting = false
})
},
delCertRow(row) {
this.$confirm('确认删除该证书吗?', '提示', { type: 'warning' }).then(() => {
delCertificate(row.certId).then(() => {
this.$message.success('已删除')
this.loadCertificates(this.detailEmp.empId)
})
})
}
}
}
@@ -173,6 +595,22 @@ export default {
gap: 8px;
align-items: center;
}
.detail-wrap {
padding-right: 4px;
}
.basic-grid {
display: grid;
grid-template-columns: 1fr;
gap: 12px;
}
.preview-placeholder {
color: #a0a3ad;
font-size: 13px;
padding: 12px;
background: #fafafa;
border: 1px dashed #ebeef5;
border-radius: 6px;
}
.custom-tree-node {
display: flex;
align-items: center;