fix:人员管理接口联调

master
zhoulexin 2 days ago
parent 79b80d5cb3
commit 07e6ee814f

@ -0,0 +1,38 @@
import request from '@/utils/request'
/** 添加人员(含人脸样本) */
export const addPersonWithFace = (formData) => {
return request({
url: '/person/addWithFace',
method: 'post',
data: formData,
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/** 获取人员列表(分页) */
export const getPersonnelList = (params) => {
return request({
url: '/person/page',
method: 'get',
params
})
}
/** 编辑人员 */
export const updatePerson = (data) => {
return request({
url: '/person/update',
method: 'post',
data
})
}
/** 删除人员 */
export const deletePerson = (id) => {
return request({
url: '/person/delete',
method: 'post',
params: { id }
})
}

@ -2,7 +2,7 @@
<el-dialog
v-model="visible"
:title="isEdit ? '编辑人员' : '添加人员'"
width="560px"
width="720px"
:close-on-click-modal="false"
@closed="handleClosed"
>
@ -45,49 +45,59 @@
<el-input v-model="formData.contact" placeholder="请输入手机号或邮箱" />
</el-form-item>
<el-form-item label="人脸样本" prop="faceSamples" v-if="!isEdit">
<div class="face-upload-list">
<div class="face-upload-item">
<span class="face-label">正脸照片</span>
<el-upload
class="face-uploader"
:show-file-list="false"
:auto-upload="false"
:on-change="(file) => handleFaceChange(file, 'front')"
accept="image/*"
<el-form-item label="人脸样本" prop="faceSamples" v-if="!isEdit" class="face-form-item">
<div class="face-upload-group">
<div
v-for="item in faceTypes"
:key="item.key"
class="face-upload-item"
>
<img v-if="formData.faceSamples.front" :src="formData.faceSamples.front" class="face-img" />
<el-icon v-else class="face-uploader-icon"><Plus /></el-icon>
</el-upload>
<div class="face-item-header">
<span class="face-label">{{ item.label }}</span>
<span class="face-count">
{{ fileListMap[item.key]?.length || 0 }}
</span>
</div>
<div class="face-upload-item">
<span class="face-label">左脸照片</span>
<el-upload
class="face-uploader"
:show-file-list="false"
:auto-upload="false"
:on-change="(file) => handleFaceChange(file, 'left')"
accept="image/*"
<div class="face-photo-row">
<div class="face-upload-trigger" @click="triggerUpload(item.key)">
<div class="upload-trigger-box">
<el-icon><Plus /></el-icon>
</div>
</div>
<div class="face-scroll-area">
<div
v-for="(file, idx) in fileListMap[item.key]"
:key="file.uid"
class="face-thumb-item"
>
<img v-if="formData.faceSamples.left" :src="formData.faceSamples.left" class="face-img" />
<el-icon v-else class="face-uploader-icon"><Plus /></el-icon>
</el-upload>
<el-image
:src="file.url"
:preview-src-list="getPreviewSrcList(item.key)"
:initial-index="idx"
fit="cover"
class="face-thumb-img"
/>
<div class="face-thumb-actions">
<el-icon @click="handleRemoveFace(item.key, idx)"><Delete /></el-icon>
</div>
</div>
</div>
</div>
<div class="face-upload-item">
<span class="face-label">右脸照片</span>
<el-upload
class="face-uploader"
:show-file-list="false"
:ref="(el) => setUploadRef(item.key, el)"
class="hidden-upload"
:action="'#'"
:auto-upload="false"
:on-change="(file) => handleFaceChange(file, 'right')"
accept="image/*"
>
<img v-if="formData.faceSamples.right" :src="formData.faceSamples.right" class="face-img" />
<el-icon v-else class="face-uploader-icon"><Plus /></el-icon>
</el-upload>
multiple
:limit="10"
:show-file-list="false"
:on-change="(file) => handleFaceChange(item.key, file)"
:on-exceed="() => handleExceed()"
/>
</div>
</div>
<div class="face-tip">上传清晰的正面左侧右侧人脸照片</div>
<div class="face-tip">分别为正面左侧右侧各上传至少1张人脸照片每组最多10张支持 jpg/png 格式</div>
</el-form-item>
</el-form>
@ -99,8 +109,9 @@
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { ref, computed, watch, reactive } from 'vue'
import { ElMessage } from 'element-plus'
import { addPersonWithFace, updatePerson } from '@/api/personnel'
const props = defineProps({
modelValue: Boolean,
@ -124,19 +135,51 @@ const visible = computed({
const initFormData = () => ({
name: '',
gender: '',
age: '',
age: null,
employeeId: '',
department: '',
contact: '',
faceSamples: {
front: '',
left: '',
right: ''
front: [],
left: [],
right: []
}
})
const formData = ref(initFormData())
const faceTypes = [
{ key: 'front', label: '正脸照片' },
{ key: 'left', label: '左脸照片' },
{ key: 'right', label: '右脸照片' }
]
//
const fileListMap = reactive({
front: [],
left: [],
right: []
})
// upload refs
const uploadRefs = reactive({})
const setUploadRef = (key, el) => {
if (el) uploadRefs[key] = el
}
//
const triggerUpload = (key) => {
const upload = uploadRefs[key]
if (upload) {
// el-upload input
const input = upload.$el?.querySelector('input[type="file"]')
if (input) {
input.click()
}
}
}
//
const validatePhone = (rule, value, callback) => {
if (!value) {
@ -179,8 +222,8 @@ const rules = {
return
}
const { front, left, right } = value
if (!front || !left || !right) {
callback(new Error('请上传完整的人脸样本'))
if (!front?.length || !left?.length || !right?.length) {
callback(new Error('请上传完整的正脸、左脸、右脸照片每组至少1张'))
} else {
callback()
}
@ -190,22 +233,73 @@ const rules = {
]
}
// formData
watch(() => props.formData, (val) => {
if (val && Object.keys(val).length > 0) {
formData.value = {
...val,
faceSamples: val.faceSamples || { front: '', left: '', right: '' }
faceSamples: val.faceSamples || { front: [], left: [], right: [] }
}
//
const samples = val.faceSamples || {}
Object.keys(fileListMap).forEach(key => {
if (samples[key] && Array.isArray(samples[key])) {
fileListMap[key] = samples[key].map((url, i) => ({
uid: `${key}-${i}`,
name: `${key}_${i + 1}.jpg`,
url
}))
} else {
fileListMap[key] = []
}
})
} else {
formData.value = initFormData()
Object.keys(fileListMap).forEach(key => {
fileListMap[key] = []
})
}
}, { immediate: true, deep: true })
const handleFaceChange = (file, type) => {
// fileList URL
const syncUrlsFromFileList = (key) => {
formData.value.faceSamples[key] = fileListMap[key]
.filter(f => f.url)
.map(f => f.url)
}
//
const handleFaceChange = (key, file) => {
const url = URL.createObjectURL(file.raw)
formData.value.faceSamples[type] = url
//
if (!fileListMap[key].some(f => f.uid === file.uid)) {
fileListMap[key].push({
uid: file.uid,
name: file.name,
url,
raw: file.raw
})
}
syncUrlsFromFileList(key)
formRef.value?.validateField('faceSamples')
// el-upload
uploadRefs[key]?.clearFiles()
}
//
const handleRemoveFace = (key, idx) => {
fileListMap[key].splice(idx, 1)
syncUrlsFromFileList(key)
formRef.value?.validateField('faceSamples')
}
// el-image 使
const getPreviewSrcList = (key) => {
return fileListMap[key]?.map(f => f.url) || []
}
//
const handleExceed = () => {
ElMessage.warning('每组最多可上传10张照片')
}
const handleSubmit = async () => {
@ -214,17 +308,48 @@ const handleSubmit = async () => {
loading.value = true
try {
//
const submitData = {
...formData.value,
faceSamples: props.isEdit ? undefined : formData.value.faceSamples
if (!props.isEdit) {
// FormData
const fd = new FormData()
fd.append('name', formData.value.name)
fd.append('age', formData.value.age)
fd.append('contact', formData.value.contact)
fd.append('department', formData.value.department)
fd.append('employeeId', formData.value.employeeId)
fd.append('gender', formData.value.gender)
// FormData
const keyMap = { front: 'frontImage', left: 'leftImage', right: 'rightImage' }
faceTypes.forEach(({ key }) => {
const files = fileListMap[key] || []
files.forEach((file) => {
if (file.raw) {
fd.append(keyMap[key], file.raw)
}
})
})
await addPersonWithFace(fd)
} else {
// JSON
await updatePerson({
id: formData.value.id,
name: formData.value.name,
age: formData.value.age,
contact: formData.value.contact,
department: formData.value.department,
employeeId: formData.value.employeeId,
gender: formData.value.gender
})
}
console.log('提交数据:', submitData)
ElMessage.success(props.isEdit ? '编辑成功' : '添加成功')
emit('success')
visible.value = false
} catch (error) {
ElMessage.error(props.isEdit ? '编辑失败' : '添加失败')
//
if (!props.isEdit) {
ElMessage.error('添加失败')
} else {
ElMessage.error('编辑失败')
}
} finally {
loading.value = false
}
@ -232,63 +357,165 @@ const handleSubmit = async () => {
const handleClosed = () => {
formRef.value?.resetFields()
Object.keys(fileListMap).forEach(key => {
fileListMap[key] = []
})
Object.keys(uploadRefs).forEach(key => {
uploadRefs[key]?.clearFiles()
})
formData.value = initFormData()
}
</script>
<style lang="scss" scoped>
.face-upload-list {
.face-form-item {
:deep(.el-form-item__content) {
flex-wrap: wrap;
}
}
.face-upload-group {
display: flex;
gap: 12px;
flex-direction: column;
gap: 10px;
width: 100%;
}
.face-upload-item {
.face-item-header {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
// justify-content: space-between;
gap: 10px;
margin-bottom: 8px;
.face-label {
font-size: 13px;
font-weight: 600;
color: #303133;
}
.face-count {
font-size: 12px;
color: #909399;
}
}
}
.face-uploader {
:deep(.el-upload) {
.face-photo-row {
display: flex;
gap: 0;
}
.face-upload-trigger {
flex-shrink: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
padding-right: 10px;
.upload-trigger-box {
width: 80px;
height: 80px;
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
transition: border-color 0.3s;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
color: #8c939d;
transition: all 0.3s;
background: #fafafa;
&:hover {
border-color: #409eff;
color: #409eff;
}
}
}
.face-scroll-area {
flex: 1;
min-width: 0;
display: flex;
gap: 8px;
overflow-x: auto;
padding-bottom: 4px;
align-items: flex-start;
&::-webkit-scrollbar {
height: 4px;
}
&::-webkit-scrollbar-track {
background: #f5f5f5;
border-radius: 2px;
}
&::-webkit-scrollbar-thumb {
background: #d9d9d9;
border-radius: 2px;
&:hover {
background: #bfbfbf;
}
}
}
.face-img {
.face-thumb-item {
position: relative;
width: 80px;
height: 80px;
flex-shrink: 0;
border-radius: 6px;
overflow: hidden;
border: 1px solid #d9d9d9;
.face-thumb-img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.face-uploader-icon {
width: 80px;
height: 80px;
.face-thumb-actions {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: rgba(0, 0, 0, 0.55);
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
color: #8c939d;
justify-content: space-around;
height: 24px;
opacity: 0;
transition: opacity 0.3s;
.el-icon {
font-size: 14px;
color: #fff;
cursor: pointer;
&:hover {
color: #ffd04b;
}
}
}
&:hover .face-thumb-actions {
opacity: 1;
}
}
.hidden-upload {
display: none;
}
}
.face-tip {
font-size: 12px;
color: #c0c4cc;
margin-top: 8px;
margin-top: 12px;
line-height: 1.6;
}
</style>

@ -8,6 +8,25 @@
</el-button>
</div>
<div class="search-bar">
<el-input
v-model="searchForm.name"
placeholder="姓名"
clearable
style="width: 160px"
@keyup.enter="handleSearch"
/>
<el-input
v-model="searchForm.department"
placeholder="所属部门"
clearable
style="width: 160px"
@keyup.enter="handleSearch"
/>
<el-button type="primary" @click="handleSearch"></el-button>
<el-button @click="handleReset"></el-button>
</div>
<div class="card-list">
<div
v-for="item in personnelList"
@ -15,7 +34,7 @@
class="personnel-card"
>
<div class="card-header">
<el-avatar :size="64" :src="item.avatar">
<el-avatar :size="64" :src="fileHttp + item.avatar">
<el-icon :size="32"><UserFilled /></el-icon>
</el-avatar>
<div class="person-info">
@ -28,14 +47,9 @@
<div class="info-row">
<span class="label">人脸样本</span>
<div class="face-samples">
<el-avatar
v-for="(img, idx) in item.faceSamples"
:key="idx"
:size="36"
:src="img"
class="face-thumb"
/>
<span v-if="!item.faceSamples?.length" class="no-data"></span>
<span class="no-data">
{{ `${item.faceCount || '暂无样本'}` }}
</span>
</div>
</div>
<div class="info-row">
@ -44,11 +58,11 @@
</div>
<div class="info-row">
<span class="label">联系方式</span>
<span class="value">{{ item.phone || '-' }}</span>
<span class="value">{{ item.contact || '-' }}</span>
</div>
<div class="info-row">
<span class="label">性别</span>
<span class="value">{{ item.gender === 1 ? '男' : item.gender === 2 ? '女' : '-' }}</span>
<span class="value">{{ item.gender == 1 ? '男' : item.gender == 2 ? '女' : '-' }}</span>
</div>
<div class="info-row">
<span class="label">年龄</span>
@ -97,75 +111,57 @@
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import PersonnelFormDialog from './components/PersonnelFormDialog.vue'
import { getPersonnelList, deletePerson } from '@/api/personnel'
import fileHttp from '@/utils/fileHttp'
const currentPage = ref(1)
const pageSize = ref(12)
const total = ref(0)
const personnelList = ref([])
//
const searchForm = reactive({
name: '',
department: ''
})
//
const formDialogVisible = ref(false)
const isEdit = ref(false)
const currentPerson = ref({})
//
const mockData = [
{
id: 1,
name: '张三',
avatar: '',
employeeId: 'EMP001',
faceSamples: [],
department: '技术部',
phone: '138****1234',
gender: 1,
age: 28
},
{
id: 2,
name: '李四',
avatar: '',
employeeId: 'EMP002',
faceSamples: [],
department: '市场部',
phone: '139****5678',
gender: 2,
age: 32
},
{
id: 3,
name: '王五',
avatar: '',
employeeId: 'EMP003',
faceSamples: [],
department: '人力资源部',
phone: '137****9012',
gender: 1,
age: 26
},
{
id: 4,
name: '赵六',
avatar: '',
employeeId: 'EMP004',
faceSamples: [],
department: '财务部',
phone: '136****3456',
gender: 1,
age: 35
//
const fetchPersonnelList = async () => {
try {
const res = await getPersonnelList({
name: searchForm.name || undefined,
department: searchForm.department || undefined,
pageNum: currentPage.value,
pageSize: pageSize.value
})
personnelList.value = res.data?.list || res.data || []
total.value = res.data?.total || 0
} catch {
personnelList.value = []
total.value = 0
}
]
}
//
const fetchPersonnelList = () => {
//
//
const start = (currentPage.value - 1) * pageSize.value
const end = start + pageSize.value
personnelList.value = mockData.slice(start, end)
total.value = mockData.length
//
const handleSearch = () => {
currentPage.value = 1
fetchPersonnelList()
}
//
const handleReset = () => {
searchForm.name = ''
searchForm.department = ''
currentPage.value = 1
fetchPersonnelList()
}
//
@ -195,8 +191,19 @@ const handleEdit = (item) => {
}
//
const handleDelete = (item) => {
console.log('删除', item)
const handleDelete = async (item) => {
try {
await ElMessageBox.confirm(`确认删除人员「${item.name}」吗?删除后不可恢复。`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
await deletePerson(item.id)
ElMessage.success('删除成功')
fetchPersonnelList()
} catch {
//
}
}
//
@ -229,6 +236,16 @@ onMounted(() => {
}
}
.search-bar {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 16px;
padding: 12px 16px;
background: #fff;
border-radius: 8px;
}
.card-list {
flex: 1;
display: grid;
@ -307,10 +324,6 @@ onMounted(() => {
gap: 4px;
flex-wrap: wrap;
.face-thumb {
border: 1px solid #ebeef5;
}
.no-data {
color: #c0c4cc;
font-size: 12px;

Loading…
Cancel
Save