|
|
|
|
@ -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>
|
|
|
|
|
|