feat(hrm): 新增员工紧急联系人管理功能

完成员工紧急联系人模块的全流程开发,包括:
1. 数据库表结构、Mapper、Service、Controller后端代码
2. 前端页面、API接口、导入导出功能
3. 配套SQL脚本、导入模板与使用文档
4. 支持批量导入导出、数据校验与用户关联匹配
This commit is contained in:
2026-05-26 19:19:12 +08:00
parent 81e529a2dd
commit 12076c5d0b
19 changed files with 1509 additions and 0 deletions

View File

@@ -0,0 +1,449 @@
<template>
<div class="emergency-contact-page">
<el-card shadow="never" class="main-card">
<div slot="header" class="card-header">
<span class="header-title">员工紧急联系人</span>
<div class="header-actions">
<el-input v-model="query.realName" placeholder="姓名" size="small" clearable style="width: 160px;"
@keyup.enter.native="loadList" />
<el-input v-model="query.emergencyContact" placeholder="紧急联系人" size="small" clearable style="width: 160px;"
@keyup.enter.native="loadList" />
<el-select v-model="query.deptId" size="small" placeholder="部门" clearable filterable style="width: 160px;"
@change="loadList">
<el-option v-for="dept in deptOptions" :key="dept.deptId" :label="dept.deptName" :value="dept.deptId" />
</el-select>
<el-button size="small" type="primary" icon="el-icon-search" @click="loadList">查询</el-button>
<el-button size="small" type="primary" icon="el-icon-plus" @click="handleAdd">新增</el-button>
<el-button size="small" icon="el-icon-download" @click="handleExport">导出</el-button>
<el-button size="small" icon="el-icon-upload2" @click="importVisible = true">导入</el-button>
</div>
</div>
<el-table :data="list" v-loading="loading" stripe height="calc(100vh - 280px)">
<el-table-column prop="hireDate" label="入职时间" min-width="110" />
<el-table-column prop="realName" label="姓名" min-width="100" />
<el-table-column prop="phone" label="联系电话" min-width="130" />
<el-table-column prop="gender" label="性别" min-width="60">
<template slot-scope="scope">
<span>{{ { '0': '男', '1': '女', '2': '未知' }[scope.row.gender] || '-' }}</span>
</template>
</el-table-column>
<el-table-column prop="age" label="年龄" min-width="60" />
<el-table-column prop="companyName" label="公司名称" min-width="200" show-overflow-tooltip />
<el-table-column prop="emergencyContact" label="紧急联系人" min-width="100" />
<el-table-column prop="relationship" label="关系" min-width="80" />
<el-table-column prop="emergencyPhone1" label="紧急电话1" min-width="130" />
<el-table-column prop="emergencyPhone2" label="紧急电话2" min-width="130" />
<el-table-column prop="remark" label="备注" min-width="140" show-overflow-tooltip />
<el-table-column label="操作" width="180" fixed="right">
<template slot-scope="scope">
<el-button type="text" size="mini" icon="el-icon-edit" @click="handleEdit(scope.row)">编辑</el-button>
<el-button type="text" size="mini" icon="el-icon-delete" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-wrapper">
<el-pagination :current-page="query.pageNum" :page-sizes="[10, 20, 50, 100]" :page-size="query.pageSize"
:total="total" layout="total, sizes, prev, pager, next, jumper" @size-change="handleSizeChange"
@current-change="handlePageChange" />
</div>
</el-card>
<!-- 新增/编辑弹窗 -->
<el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="800px" append-to-body>
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px" size="small">
<el-divider content-position="left">员工信息</el-divider>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="选择人员" prop="userId">
<el-select v-model="form.userId" placeholder="请选择人员" filterable remote
:remote-method="searchUsers" :loading="userLoading" style="width: 100%" clearable
@change="handleUserChange">
<el-option v-for="user in userOptions" :key="user.userId"
:label="`${user.nickName} (${user.userName})`" :value="user.userId" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="姓名" prop="realName">
<el-input v-model="form.realName" placeholder="自动带出可修改" :disabled="!form.userId" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="联系电话" prop="phone">
<el-input v-model="form.phone" placeholder="自动带出可修改" :disabled="!form.userId" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="性别" prop="gender">
<el-select v-model="form.gender" placeholder="自动带出可修改" style="width: 100%" :disabled="!form.userId">
<el-option label="男" value="0" />
<el-option label="女" value="1" />
<el-option label="未知" value="2" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="身份证号" prop="idCard">
<el-input v-model="form.idCard" placeholder="自动带出可修改" :disabled="!form.userId" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="年龄" prop="age">
<el-input-number v-model="form.age" :min="0" :max="150" style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
<el-divider content-position="left">公司信息</el-divider>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="公司名称" prop="companyName">
<el-input v-model="form.companyName" placeholder="请输入公司名称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="入职时间" prop="hireDate">
<el-date-picker v-model="form.hireDate" type="date" placeholder="选择日期" style="width: 100%"
value-format="yyyy-MM-dd" />
</el-form-item>
</el-col>
</el-row>
<el-divider content-position="left">紧急联系人</el-divider>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="联系人姓名" prop="emergencyContact">
<el-input v-model="form.emergencyContact" placeholder="请输入紧急联系人姓名" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="与本人关系" prop="relationship">
<el-select v-model="form.relationship" placeholder="请选择关系" style="width: 100%" clearable filterable>
<el-option label="配偶" value="配偶" />
<el-option label="父母" value="父母" />
<el-option label="子女" value="子女" />
<el-option label="兄弟姐妹" value="兄弟姐妹" />
<el-option label="朋友" value="朋友" />
<el-option label="同事" value="同事" />
<el-option label="其他" value="其他" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="联系电话1" prop="emergencyPhone1">
<el-input v-model="form.emergencyPhone1" placeholder="请输入紧急联系人电话" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="联系电话2" prop="emergencyPhone2">
<el-input v-model="form.emergencyPhone2" placeholder="请输入备用电话" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="24">
<el-form-item label="联系地址" prop="emergencyAddress">
<el-input v-model="form.emergencyAddress" placeholder="请输入紧急联系人地址" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="24">
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" :rows="2" placeholder="请输入备注" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="submitting" @click="handleSubmit">确定</el-button>
</div>
</el-dialog>
<!-- 导入弹窗 -->
<el-dialog title="导入紧急联系人数据" :visible.sync="importVisible" width="500px" append-to-body>
<!-- 导入规则说明 -->
<el-alert
title="导入规则说明"
type="info"
:closable="false"
style="margin-bottom: 15px;"
>
<div slot="description" style="font-size: 12px; line-height: 1.8;">
<p>1. <strong>文件格式</strong>必须是 .xlsx 格式表头顺序不能乱</p>
<p>2. <strong>必填字段</strong> * 号的为必填项姓名联系电话身份证号性别年龄等</p>
<p>3. <strong>重复数据</strong>系统会按姓名 + 身份证号去重已存在的记录会自动更新</p>
<p>4. <strong>数据校验</strong>手机号必须为 11 位数字身份证号会做基础格式校验</p>
<p>5. <strong>导入后核对</strong>导入完成后请在列表页核对数据如有错误可直接修改</p>
</div>
</el-alert>
<el-upload ref="upload" :limit="1" accept=".xlsx,.xls" :headers="upload.headers"
:action="upload.url" :disabled="upload.isUploading" :on-progress="handleFileUploadProgress"
:on-success="handleFileSuccess" :on-error="handleFileError" :auto-upload="false"
drag>
<i class="el-icon-upload" />
<div class="el-upload__text">将文件拖到此处<em>点击上传</em></div>
<div slot="tip" class="el-upload__tip" style="color:red;padding:0 12px">
仅允许导入 xlsx / xls 格式文件
<el-link type="primary" :underline="false" style="font-size:12px" @click="handleDownloadTemplate">下载导入模板含字段说明和示例数据</el-link>
</div>
</el-upload>
<div slot="footer" class="dialog-footer">
<el-button @click="importVisible = false">取消</el-button>
<el-button type="primary" :loading="upload.isUploading" @click="submitImport">确定导入</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listDept } from '@/api/system/dept'
import { selectUser } from '@/api/system/user'
import { listEmergencyContact, addEmergencyContact, updateEmergencyContact, delEmergencyContact, exportEmergencyContact, importTemplate } from '@/api/hrm/emergencyContact'
import { getToken } from '@/utils/auth'
export default {
name: 'HrmEmergencyContact',
data() {
return {
loading: false,
submitting: false,
list: [],
total: 0,
deptOptions: [],
userOptions: [],
userLoading: false,
query: {
pageNum: 1,
pageSize: 20,
realName: '',
emergencyContact: '',
deptId: null
},
dialogVisible: false,
dialogTitle: '新增紧急联系人',
form: {},
rules: {
userId: [{ required: true, message: '请选择人员', trigger: 'change' }],
emergencyContact: [{ required: true, message: '请输入紧急联系人姓名', trigger: 'blur' }],
emergencyPhone1: [{ required: true, message: '请输入紧急联系人电话', trigger: 'blur' }]
},
// 导入相关
importVisible: false,
upload: {
headers: { Authorization: 'Bearer ' + getToken() },
url: process.env.VUE_APP_BASE_API + '/hrm/emergencyContact/importData',
isUploading: false
},
// 导入字段说明
importFieldDesc: [
{ field: '入职时间', required: false, desc: '格式yyyy-MM-dd', example: '2025-06-18' },
{ field: '公司名称', required: true, desc: '全称与OA内一致', example: '山东福安德信息科技有限公司' },
{ field: '部门', required: true, desc: '与OA内部门名称一致', example: '信息化部' },
{ field: '姓名', required: true, desc: '员工真实姓名', example: '张三' },
{ field: '联系电话', required: true, desc: '员工本人手机号11位数字', example: '183xxxxxxxx' },
{ field: '身份证号', required: true, desc: '18位或15位含X大写', example: '341xxxxxxxxxxxxxxx' },
{ field: '性别', required: true, desc: '男 / 女', example: '男' },
{ field: '年龄', required: true, desc: '整数,按周岁填写', example: '23' },
{ field: '紧急联系人', required: true, desc: '联系人真实姓名', example: '李四' },
{ field: '与本人关系', required: true, desc: '如父子/母子/夫妻等', example: '母子' },
{ field: '联系电话1', required: true, desc: '紧急联系人手机号11位数字', example: '159xxxxxxxx' },
{ field: '联系电话2', required: false, desc: '备用手机号11位数字', example: '182xxxxxxxx' },
{ field: '紧急联系人地址', required: true, desc: '详细地址', example: 'xx省xx市xx县xx镇' },
{ field: '备注', required: false, desc: '补充说明', example: '仅紧急情况联系' }
]
}
},
created() {
this.loadDeptOptions()
this.loadList()
},
methods: {
loadDeptOptions() {
listDept({}).then(res => {
this.deptOptions = res.data || []
})
},
loadList() {
this.loading = true
listEmergencyContact(this.query).then(res => {
this.list = res.rows || []
this.total = res.total || 0
}).finally(() => {
this.loading = false
})
},
handleSizeChange(val) {
this.query.pageSize = val
this.loadList()
},
handlePageChange(val) {
this.query.pageNum = val
this.loadList()
},
handleAdd() {
this.dialogTitle = '新增紧急联系人'
this.form = {
userId: undefined,
realName: '',
phone: '',
idCard: '',
gender: '0',
age: undefined,
companyName: '',
hireDate: undefined,
emergencyContact: '',
relationship: '',
emergencyPhone1: '',
emergencyPhone2: '',
emergencyAddress: '',
remark: ''
}
this.userOptions = []
this.dialogVisible = true
this.$nextTick(() => { this.$refs.formRef?.clearValidate() })
},
handleEdit(row) {
this.dialogTitle = '编辑紧急联系人'
this.form = { ...row }
// 加载已选用户信息用于展示
if (this.form.userId) {
this.searchUsers('')
}
this.dialogVisible = true
this.$nextTick(() => { this.$refs.formRef?.clearValidate() })
},
handleDelete(row) {
this.$confirm(`确认删除 "${row.realName}" 的紧急联系人信息吗?`, '提示', { type: 'warning' }).then(() => {
delEmergencyContact(row.contactId).then(() => {
this.$message.success('删除成功')
this.loadList()
})
})
},
searchUsers(query) {
this.userLoading = true
selectUser({ userName: query, nickName: query, pageNum: 1, pageSize: 20 }).then(res => {
this.userOptions = res.rows || []
// 编辑时确保已选用户在选项中
if (this.form.userId && !this.userOptions.find(u => u.userId === this.form.userId)) {
selectUser({ userId: this.form.userId }).then(res2 => {
if (res2.rows && res2.rows.length > 0) {
this.userOptions.unshift(res2.rows[0])
}
})
}
}).finally(() => {
this.userLoading = false
})
},
handleUserChange(userId) {
if (!userId) return
const user = this.userOptions.find(u => u.userId === userId)
if (user) {
this.form.realName = user.nickName || ''
this.form.phone = user.phonenumber || ''
this.form.gender = user.sex || '0'
this.form.idCard = user.idCard || ''
// 根据身份证号计算年龄
if (user.idCard && user.idCard.length >= 18) {
const birthYear = parseInt(user.idCard.substring(6, 10))
const currentYear = new Date().getFullYear()
this.form.age = currentYear - birthYear
}
}
},
handleSubmit() {
this.$refs.formRef.validate(valid => {
if (!valid) return
this.submitting = true
const api = this.form.contactId ? updateEmergencyContact : addEmergencyContact
api(this.form).then(() => {
this.$message.success(this.form.contactId ? '更新成功' : '新增成功')
this.dialogVisible = false
if (!this.form.contactId) {
this.query.pageNum = 1
}
this.loadList()
}).finally(() => {
this.submitting = false
})
})
},
// 导出
handleExport() {
this.$confirm('确认导出紧急联系人数据吗?', '提示', { type: 'warning' }).then(() => {
exportEmergencyContact(this.query)
})
},
// 下载导入模板
handleDownloadTemplate() {
importTemplate()
},
handleFileUploadProgress() {
this.upload.isUploading = true
},
handleFileSuccess(res) {
this.upload.isUploading = false
this.$refs.upload.clearFiles()
this.importVisible = false
this.$modal.msgSuccess(res.msg || '导入成功')
this.loadList()
},
handleFileError() {
this.upload.isUploading = false
this.$message.error('导入失败,请检查文件格式')
},
submitImport() {
this.$refs.upload.submit()
}
}
}
</script>
<style lang="scss" scoped>
.emergency-contact-page {
padding: 20px;
background-color: #f0f2f5;
}
.main-card {
border: none;
border-radius: 4px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
.header-title {
font-size: 16px;
font-weight: 600;
color: #303133;
}
.header-actions {
display: flex;
gap: 8px;
}
}
.pagination-wrapper {
margin-top: 16px;
display: flex;
justify-content: flex-end;
}
.dialog-footer {
text-align: right;
}
</style>