feat(hrm): 新增员工紧急联系人管理功能
完成员工紧急联系人模块的全流程开发,包括: 1. 数据库表结构、Mapper、Service、Controller后端代码 2. 前端页面、API接口、导入导出功能 3. 配套SQL脚本、导入模板与使用文档 4. 支持批量导入导出、数据校验与用户关联匹配
This commit is contained in:
55
ruoyi-ui/src/api/hrm/emergencyContact.js
Normal file
55
ruoyi-ui/src/api/hrm/emergencyContact.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import request from '@/utils/request'
|
||||
import { download } from '@/utils/request'
|
||||
|
||||
// 查询紧急联系人列表
|
||||
export function listEmergencyContact(query) {
|
||||
return request({
|
||||
url: '/hrm/emergencyContact/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 查询紧急联系人详细
|
||||
export function getEmergencyContact(contactId) {
|
||||
return request({
|
||||
url: `/hrm/emergencyContact/${contactId}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 新增紧急联系人
|
||||
export function addEmergencyContact(data) {
|
||||
return request({
|
||||
url: '/hrm/emergencyContact',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 修改紧急联系人
|
||||
export function updateEmergencyContact(data) {
|
||||
return request({
|
||||
url: '/hrm/emergencyContact',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除紧急联系人
|
||||
export function delEmergencyContact(contactIds) {
|
||||
return request({
|
||||
url: `/hrm/emergencyContact/${contactIds}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
// 导出紧急联系人
|
||||
export function exportEmergencyContact(query) {
|
||||
return download('/hrm/emergencyContact/export', query, '紧急联系人数据.xlsx')
|
||||
}
|
||||
|
||||
// 下载导入模板
|
||||
export function importTemplate() {
|
||||
return download('/hrm/emergencyContact/importTemplate', {}, '紧急联系人导入模板.xlsx')
|
||||
}
|
||||
@@ -12,4 +12,5 @@ export * from './org'
|
||||
export * from './reimburse'
|
||||
export * from './seal'
|
||||
export * from './travel'
|
||||
export * from './emergencyContact'
|
||||
|
||||
|
||||
449
ruoyi-ui/src/views/hrm/emergencyContact/index.vue
Normal file
449
ruoyi-ui/src/views/hrm/emergencyContact/index.vue
Normal 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>
|
||||
Reference in New Issue
Block a user