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

@ -8,6 +8,25 @@
</el-button> </el-button>
</div> </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 class="card-list">
<div <div
v-for="item in personnelList" v-for="item in personnelList"
@ -15,7 +34,7 @@
class="personnel-card" class="personnel-card"
> >
<div class="card-header"> <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-icon :size="32"><UserFilled /></el-icon>
</el-avatar> </el-avatar>
<div class="person-info"> <div class="person-info">
@ -28,14 +47,9 @@
<div class="info-row"> <div class="info-row">
<span class="label">人脸样本</span> <span class="label">人脸样本</span>
<div class="face-samples"> <div class="face-samples">
<el-avatar <span class="no-data">
v-for="(img, idx) in item.faceSamples" {{ `${item.faceCount || '暂无样本'}` }}
:key="idx" </span>
:size="36"
:src="img"
class="face-thumb"
/>
<span v-if="!item.faceSamples?.length" class="no-data"></span>
</div> </div>
</div> </div>
<div class="info-row"> <div class="info-row">
@ -44,11 +58,11 @@
</div> </div>
<div class="info-row"> <div class="info-row">
<span class="label">联系方式</span> <span class="label">联系方式</span>
<span class="value">{{ item.phone || '-' }}</span> <span class="value">{{ item.contact || '-' }}</span>
</div> </div>
<div class="info-row"> <div class="info-row">
<span class="label">性别</span> <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>
<div class="info-row"> <div class="info-row">
<span class="label">年龄</span> <span class="label">年龄</span>
@ -97,75 +111,57 @@
</template> </template>
<script setup> <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 PersonnelFormDialog from './components/PersonnelFormDialog.vue'
import { getPersonnelList, deletePerson } from '@/api/personnel'
import fileHttp from '@/utils/fileHttp'
const currentPage = ref(1) const currentPage = ref(1)
const pageSize = ref(12) const pageSize = ref(12)
const total = ref(0) const total = ref(0)
const personnelList = ref([]) const personnelList = ref([])
//
const searchForm = reactive({
name: '',
department: ''
})
// //
const formDialogVisible = ref(false) const formDialogVisible = ref(false)
const isEdit = ref(false) const isEdit = ref(false)
const currentPerson = ref({}) const currentPerson = ref({})
// //
const mockData = [ const fetchPersonnelList = async () => {
{ try {
id: 1, const res = await getPersonnelList({
name: '张三', name: searchForm.name || undefined,
avatar: '', department: searchForm.department || undefined,
employeeId: 'EMP001', pageNum: currentPage.value,
faceSamples: [], pageSize: pageSize.value
department: '技术部', })
phone: '138****1234', personnelList.value = res.data?.list || res.data || []
gender: 1, total.value = res.data?.total || 0
age: 28 } catch {
}, personnelList.value = []
{ total.value = 0
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 = () => { const handleSearch = () => {
// currentPage.value = 1
// fetchPersonnelList()
const start = (currentPage.value - 1) * pageSize.value }
const end = start + pageSize.value
personnelList.value = mockData.slice(start, end) //
total.value = mockData.length const handleReset = () => {
searchForm.name = ''
searchForm.department = ''
currentPage.value = 1
fetchPersonnelList()
} }
// //
@ -195,8 +191,19 @@ const handleEdit = (item) => {
} }
// //
const handleDelete = (item) => { const handleDelete = async (item) => {
console.log('删除', 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 { .card-list {
flex: 1; flex: 1;
display: grid; display: grid;
@ -307,10 +324,6 @@ onMounted(() => {
gap: 4px; gap: 4px;
flex-wrap: wrap; flex-wrap: wrap;
.face-thumb {
border: 1px solid #ebeef5;
}
.no-data { .no-data {
color: #c0c4cc; color: #c0c4cc;
font-size: 12px; font-size: 12px;

Loading…
Cancel
Save