Files
xgy-oa/klp-ui/src/views/hrm/org/index.vue

1044 lines
38 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="hrm-page">
<section class="panel-grid">
<el-card class="metal-panel" shadow="hover">
<div slot="header" class="panel-header">
<span>组织树</span>
<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"
:data="orgTree"
node-key="orgId"
:props="{ label: 'orgName', children: 'children' }"
accordion
highlight-current
@node-click="handleOrgClick"
>
<span slot-scope="{ data }" class="custom-tree-node">
<span>{{ data.orgName }}</span>
<el-tag size="mini" effect="plain" type="info" class="tree-tag">{{ data.orgType || '组织' }}</el-tag>
</span>
</el-tree>
</el-card>
<el-card class="metal-panel" shadow="hover">
<div slot="header" class="panel-header">
<span>员工档案</span>
<div class="actions-inline">
<el-input
v-model="empQuery.empName"
placeholder="姓名/工号"
size="mini"
clearable
@keyup.enter.native="loadEmployee"
style="width: 180px"
/>
<el-select
v-model="empQuery.status"
size="mini"
placeholder="状态"
clearable
style="width: 140px"
@change="loadEmployee"
>
<el-option label="在职" value="active" />
<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">
<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" />
<el-table-column label="手机" prop="mobile" min-width="130" />
<el-table-column label="雇佣类型" prop="employmentType" min-width="120" />
<el-table-column label="状态" prop="status" min-width="100">
<template slot-scope="scope">
<el-tag :type="statusType(scope.row.status)">{{ scope.row.status || '-' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="入职日期" prop="hireDate" min-width="140">
<template slot-scope="scope">{{ formatDate(scope.row.hireDate) }}</template>
</el-table-column>
<el-table-column label="备注" prop="remark" min-width="160" show-overflow-tooltip />
</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="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="合同文件" 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" placeholder="如:劳动合同/实习协议" />
</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" placeholder="如:生效/签署中" />
</el-form-item>
<el-form-item label="备注">
<el-input v-model="contractForm.remark" type="textarea" :rows="2" placeholder="补充说明" />
</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="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-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" placeholder="补充说明(可选)" />
</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,
listPosition,
listEmpOrgPosition,
addEmpOrgPosition,
updateEmpOrgPosition,
delEmpOrgPosition,
listContract,
addContract,
updateContract,
delContract,
listCertificate,
addCertificate,
updateCertificate,
delCertificate,
addEmployee,
addOrg
} from '@/api/hrm'
import { delOss } from '@/api/system/oss'
import FileUpload from '@/components/FileUpload'
export default {
name: 'HrmOrgEmployee',
data() {
return {
orgTree: [],
orgLoading: false,
orgSelected: null,
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: {
contractNo: '',
contractType: '',
startDate: '',
endDate: '',
status: 'active',
remark: '',
fileIds: ''
},
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: [],
orgDialogVisible: false,
orgSubmitting: false,
orgForm: { orgName: '', orgType: '', parentId: undefined, orgCode: '', remark: '' },
orgRules: {
orgName: [{ required: true, message: '请输入组织名称', trigger: 'blur' }]
}
}
},
created() {
this.loadOrg()
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' }
return map[status] || 'info'
},
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() {
this.orgLoading = true
listOrg({ pageNum: 1, pageSize: 999 })
.then(res => {
const rows = res.rows || []
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(() => {
this.orgLoading = false
})
},
buildTree(list) {
const map = {}
list.forEach(item => {
map[item.orgId] = { ...item, children: [] }
})
const roots = []
list.forEach(item => {
const parent = map[item.parentId]
if (parent) parent.children.push(map[item.orgId])
else roots.push(map[item.orgId])
})
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
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
listEmployee({ ...this.empQuery, pageNum: 1, pageSize: 50 })
.then(res => {
this.employeeList = res.rows || []
})
.finally(() => {
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
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
})
},
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', fileIds: '', contractNo: '' }
this.contractDialogVisible = true
},
submitContract() {
if (!this.detailEmp) return
this.contractSubmitting = true
const api = this.contractForm.contractId ? updateContract : addContract
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
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
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
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,
fileIds: '',
issuedBy: '',
certNo: ''
}
this.certDialogVisible = true
},
submitCert() {
if (!this.detailEmp) return
this.certSubmitting = true
const api = this.certForm.certId ? updateCertificate : addCertificate
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
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)
})
})
}
}
}
</script>
<style lang="scss" scoped>
.hrm-page {
padding: 16px 20px 32px;
background: #f8f9fb;
}
.panel-grid {
display: grid;
grid-template-columns: 280px 1fr;
gap: 12px;
}
.metal-panel {
border: 1px solid #d7d9df;
border-radius: 10px;
background: #fff;
}
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
font-weight: 600;
color: #303133;
}
.actions-inline {
display: flex;
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;
justify-content: space-between;
width: 100%;
}
.tree-tag {
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 {
grid-template-columns: 1fr;
}
}
</style>