<template>
|
<el-dialog
|
v-model="visible"
|
:title="isEdit ? '编辑评委' : '新增评委'"
|
width="600px"
|
:before-close="handleClose"
|
class="form-dialog"
|
draggable
|
overflow
|
:close-on-click-modal="false"
|
>
|
<el-form
|
ref="formRef"
|
:model="form"
|
:rules="rules"
|
label-width="100px"
|
label-position="left"
|
>
|
<!-- 基本信息 -->
|
<el-form-item label="姓名" prop="name">
|
<el-input v-model="form.name" placeholder="请输入评委姓名" />
|
</el-form-item>
|
|
<el-form-item label="联系电话" prop="phone">
|
<el-input v-model="form.phone" placeholder="请输入联系电话" />
|
</el-form-item>
|
|
<el-form-item label="性别" prop="gender">
|
<el-radio-group v-model="form.gender">
|
<el-radio :value="1">男</el-radio>
|
<el-radio :value="0">女</el-radio>
|
</el-radio-group>
|
</el-form-item>
|
|
<!-- 头像上传 -->
|
<el-form-item label="头像">
|
<div class="avatar-upload-container">
|
<el-upload
|
class="avatar-uploader"
|
:show-file-list="false"
|
:before-upload="beforeAvatarUpload"
|
:http-request="handleAvatarUpload"
|
accept="image/jpeg,image/jpg,image/png,image/gif,image/webp"
|
>
|
<!-- 显示当前头像或上传区域 -->
|
<div v-if="form.currentAvatar" class="avatar-preview">
|
<img :src="form.currentAvatar.url" class="avatar" />
|
<div class="avatar-overlay">
|
<el-button
|
type="danger"
|
size="small"
|
@click.stop="handleDeleteAvatar"
|
:loading="form.currentAvatar.deleting"
|
class="delete-btn"
|
>
|
删除
|
</el-button>
|
<div class="change-text">点击更换头像</div>
|
</div>
|
</div>
|
<div v-else class="upload-area">
|
<el-icon class="avatar-uploader-icon"><Plus /></el-icon>
|
<div class="upload-text">选择头像</div>
|
<div class="upload-hint">支持 JPG、PNG、GIF 格式,大小不超过 5MB</div>
|
</div>
|
</el-upload>
|
</div>
|
</el-form-item>
|
|
<!-- 专业标签 -->
|
<el-form-item label="专业标签" prop="specialtyIds">
|
<el-select
|
v-model="form.specialtyIds"
|
multiple
|
placeholder="请选择专业标签"
|
style="width: 100%"
|
>
|
<el-option
|
v-for="tag in availableTags"
|
:key="tag.id"
|
:label="tag.name"
|
:value="tag.id"
|
/>
|
</el-select>
|
</el-form-item>
|
|
<!-- 描述 -->
|
<el-form-item label="个人描述">
|
<el-input
|
v-model="form.description"
|
type="textarea"
|
:rows="4"
|
placeholder="请输入个人描述"
|
/>
|
</el-form-item>
|
</el-form>
|
|
<template #footer>
|
<div class="dialog-footer">
|
<el-button @click="handleClose">取 消</el-button>
|
<el-button type="primary" :loading="loading" @click="handleSubmit">
|
{{ isEdit ? '更 新' : '创 建' }}
|
</el-button>
|
</div>
|
</template>
|
</el-dialog>
|
</template>
|
|
<script setup lang="ts">
|
import { ref, reactive, computed, watch, nextTick, onMounted } from 'vue'
|
import { ElMessage, ElMessageBox, type FormInstance, type UploadRequestOptions } from 'element-plus'
|
import { Plus } from '@element-plus/icons-vue'
|
import { JudgeApi, CosUploadService } from '@/api/judge'
|
import { getMediasByTarget, deleteMedia, uploadFile } from '@/api/media'
|
import type { Judge, JudgeInput, Tag } from '@/api/graphql'
|
import type { Media } from '@/api/media'
|
|
interface Props {
|
modelValue: boolean
|
judgeData?: Judge | null
|
}
|
|
interface Emits {
|
(e: 'update:modelValue', value: boolean): void
|
(e: 'success'): void
|
}
|
|
const props = withDefaults(defineProps<Props>(), {
|
judgeData: null
|
})
|
|
const emit = defineEmits<Emits>()
|
|
const formRef = ref<FormInstance>()
|
const loading = ref(false)
|
|
// 控制弹窗显示
|
const visible = computed({
|
get: () => props.modelValue,
|
set: (value) => emit('update:modelValue', value)
|
})
|
|
// 是否为编辑模式
|
const isEdit = computed(() => !!props.judgeData?.id)
|
|
// 表单数据
|
const form = reactive<JudgeInput & {
|
avatarUrl?: string;
|
currentAvatar?: {
|
id?: string;
|
name: string;
|
url: string;
|
file?: File;
|
uploaded: boolean;
|
isExisting: boolean;
|
deleting?: boolean;
|
} | null;
|
}>({
|
id: undefined,
|
name: '',
|
phone: '',
|
gender: undefined,
|
description: '',
|
specialtyIds: [],
|
avatarUrl: '',
|
avatarMediaId: undefined,
|
currentAvatar: null
|
})
|
|
// 可选的专业标签(从后端获取)
|
const availableTags = ref<Tag[]>([])
|
|
// 表单验证规则
|
const rules = {
|
name: [
|
{ required: true, message: '请输入评委姓名', trigger: 'blur' },
|
{ min: 2, max: 20, message: '姓名长度在 2 到 20 个字符', trigger: 'blur' }
|
],
|
phone: [
|
{ required: true, message: '请输入联系电话', trigger: 'blur' },
|
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码', trigger: 'blur' }
|
]
|
}
|
|
// 监听评委数据变化,填充表单
|
watch(() => props.judgeData, (data) => {
|
nextTick(async () => {
|
if (data && data.id) {
|
form.id = data.id
|
form.name = data.name || ''
|
form.phone = data.phone || ''
|
form.gender = data.gender
|
form.description = data.description || ''
|
form.specialtyIds = data.specialties?.map(tag => tag.id) || []
|
form.avatarUrl = data.avatarUrl || ''
|
|
// 加载评委头像/媒体(targetType=1 表示评委)
|
try {
|
console.log('=== 加载评委媒体 ===');
|
console.log('评委ID:', data.id);
|
const medias = await getMediasByTarget(1, parseInt(data.id))
|
console.log('获取到的媒体列表:', medias);
|
|
// 只取第一个图片作为头像(评委只能有一个头像)
|
const firstImage = (medias || []).find((m: Media) => {
|
const isImage = (m.mediaType === 1) || (m.fileExt && ['jpg','jpeg','png','gif','webp'].includes(m.fileExt.toLowerCase()))
|
return isImage
|
})
|
|
if (firstImage) {
|
form.currentAvatar = {
|
...firstImage,
|
file: null,
|
uploaded: true,
|
isExisting: true,
|
url: firstImage.fullUrl
|
};
|
form.avatarUrl = firstImage.fullUrl;
|
console.log('设置当前头像:', form.currentAvatar);
|
} else {
|
form.currentAvatar = null;
|
}
|
} catch (e) {
|
console.warn('加载评委媒体失败:', e)
|
}
|
} else {
|
resetForm()
|
}
|
})
|
}, { immediate: true })
|
|
// 重置表单
|
const resetForm = () => {
|
form.id = undefined
|
form.name = ''
|
form.phone = ''
|
form.gender = undefined
|
form.description = ''
|
form.specialtyIds = []
|
form.avatarUrl = ''
|
form.avatarMediaId = undefined
|
form.currentAvatar = null
|
|
nextTick(() => {
|
formRef.value?.clearValidate()
|
})
|
}
|
|
// 处理头像上传
|
const handleAvatarFileUpload = async (judgeId: string) => {
|
if (!form.currentAvatar || form.currentAvatar.uploaded) {
|
return; // 没有新头像需要上传
|
}
|
|
console.log('=== 开始上传头像文件 ===');
|
console.log('评委ID:', judgeId);
|
console.log('待上传的头像:', form.currentAvatar.name);
|
|
try {
|
// 上传文件
|
const uploadResult = await uploadFile(form.currentAvatar.file!);
|
console.log('上传结果:', uploadResult);
|
|
if (uploadResult.success) {
|
// 保存媒体信息到数据库
|
await JudgeApi.saveMedia({
|
name: uploadResult.fileName,
|
path: uploadResult.path,
|
fileSize: uploadResult.fileSize,
|
fileExt: uploadResult.fileName.split('.').pop() || 'jpg',
|
mediaType: 1, // 1表示图片
|
targetType: 1, // 1表示评委
|
targetId: parseInt(judgeId)
|
});
|
|
// 标记为已上传
|
form.currentAvatar.uploaded = true;
|
form.currentAvatar.url = uploadResult.fullUrl;
|
form.avatarUrl = uploadResult.fullUrl;
|
console.log('头像上传并保存成功:', form.currentAvatar.name);
|
}
|
} catch (error) {
|
console.error('头像上传失败:', error);
|
ElMessage.error(`头像上传失败: ${(error as Error).message}`);
|
}
|
}
|
|
// 删除头像
|
const handleDeleteAvatar = async () => {
|
if (!form.currentAvatar) return;
|
|
try {
|
await ElMessageBox.confirm('确定要删除这个头像吗?', '确认删除', {
|
type: 'warning'
|
});
|
|
form.currentAvatar.deleting = true;
|
|
if (form.currentAvatar.isExisting && form.currentAvatar.id) {
|
// 删除已存在的头像文件
|
console.log('删除已存在的头像文件:', form.currentAvatar.id);
|
const result = await deleteMedia(form.currentAvatar.id);
|
console.log('删除结果:', result);
|
|
if (result) {
|
form.currentAvatar = null;
|
form.avatarUrl = '';
|
ElMessage.success('头像删除成功');
|
} else {
|
throw new Error('删除失败');
|
}
|
} else {
|
// 删除新选择但未上传的头像
|
form.currentAvatar = null;
|
form.avatarUrl = '';
|
ElMessage.success('已取消选择的头像');
|
}
|
} catch (error) {
|
console.error('删除头像失败:', error);
|
if (error !== 'cancel') {
|
ElMessage.error('头像删除失败: ' + (error as Error).message);
|
}
|
} finally {
|
if (form.currentAvatar) {
|
form.currentAvatar.deleting = false;
|
}
|
}
|
}
|
|
// 提交表单
|
const handleSubmit = async () => {
|
if (!formRef.value) return
|
|
try {
|
await formRef.value.validate()
|
loading.value = true
|
|
// 1. 先保存评委基本信息
|
const submitData: JudgeInput = {
|
id: form.id,
|
name: form.name,
|
phone: form.phone || undefined,
|
gender: form.gender,
|
description: form.description || undefined,
|
majorIds: form.specialtyIds?.length ? form.specialtyIds.map(id => parseInt(id)) : undefined
|
}
|
|
const savedJudge = await JudgeApi.saveJudge(submitData)
|
console.log('评委保存成功:', savedJudge);
|
|
// 2. 上传新选择的头像文件
|
if (savedJudge.id) {
|
await handleAvatarFileUpload(savedJudge.id);
|
}
|
|
ElMessage.success(isEdit.value ? '更新成功' : '创建成功')
|
emit('success')
|
handleClose()
|
} catch (error) {
|
console.error('保存失败:', error)
|
ElMessage.error('保存失败: ' + (error as Error).message)
|
} finally {
|
loading.value = false
|
}
|
}
|
|
// 关闭弹窗
|
const handleClose = () => {
|
visible.value = false
|
resetForm()
|
}
|
|
// 加载专业标签数据
|
const loadTags = async () => {
|
try {
|
// 获取专业类别的标签
|
const tags = await JudgeApi.getTagsByCategory('major')
|
availableTags.value = tags
|
} catch (error) {
|
console.error('加载标签失败:', error)
|
// 如果获取失败,使用所有标签
|
try {
|
const allTags = await JudgeApi.getTags()
|
availableTags.value = allTags
|
} catch (err) {
|
console.error('加载所有标签失败:', err)
|
ElMessage.error('加载专业标签失败')
|
}
|
}
|
}
|
|
onMounted(() => {
|
loadTags()
|
})
|
|
// --- 头像上传逻辑 ---
|
|
const beforeAvatarUpload = (rawFile: File) => {
|
if (!rawFile.type.startsWith('image/')) {
|
ElMessage.error('只能上传图片文件!')
|
return false
|
}
|
if (rawFile.size / 1024 / 1024 > 5) {
|
ElMessage.error('图片大小不能超过 5MB!')
|
return false
|
}
|
return true
|
}
|
|
const handleAvatarUpload = async (options: UploadRequestOptions) => {
|
const { file } = options
|
try {
|
// 如果已有头像,先删除旧的(如果是已存在的)
|
if (form.currentAvatar?.isExisting && form.currentAvatar.id) {
|
try {
|
await deleteMedia(form.currentAvatar.id);
|
console.log('已删除旧头像');
|
} catch (error) {
|
console.warn('删除旧头像失败,继续上传新头像:', error);
|
}
|
}
|
|
// 设置新头像(不立即上传,等保存时再上传)
|
form.currentAvatar = {
|
file: file,
|
name: file.name,
|
uploaded: false,
|
isExisting: false,
|
url: URL.createObjectURL(file) // 本地预览
|
};
|
|
form.avatarUrl = form.currentAvatar.url;
|
ElMessage.success('头像已选择,点击更新按钮时将上传');
|
} catch (error) {
|
console.error('头像处理失败:', error)
|
ElMessage.error('头像处理失败: ' + (error as Error).message)
|
}
|
}
|
</script>
|
|
<style lang="scss" scoped>
|
:deep(.el-dialog__header) {
|
border-bottom: 1px solid var(--el-border-color-lighter);
|
margin-right: 0;
|
padding: 16px 24px;
|
}
|
|
:deep(.el-dialog__title) {
|
font-size: 16px;
|
font-weight: 500;
|
}
|
|
:deep(.el-dialog__body) {
|
padding: 24px;
|
}
|
|
:deep(.el-dialog__footer) {
|
border-top: 1px solid var(--el-border-color-lighter);
|
padding: 16px 24px;
|
}
|
|
.dialog-footer {
|
text-align: right;
|
}
|
|
.avatar-upload-container {
|
display: flex;
|
justify-content: flex-start;
|
}
|
|
.avatar-uploader {
|
position: relative;
|
}
|
|
.avatar-preview {
|
position: relative;
|
width: 120px;
|
height: 120px;
|
border-radius: 6px;
|
overflow: hidden;
|
cursor: pointer;
|
|
&:hover .avatar-overlay {
|
opacity: 1;
|
}
|
}
|
|
.avatar {
|
width: 120px;
|
height: 120px;
|
object-fit: cover;
|
display: block;
|
}
|
|
.avatar-overlay {
|
position: absolute;
|
top: 0;
|
left: 0;
|
right: 0;
|
bottom: 0;
|
background: rgba(0, 0, 0, 0.5);
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
opacity: 0;
|
transition: opacity 0.3s;
|
color: white;
|
|
.overlay-actions {
|
display: flex;
|
gap: 8px;
|
}
|
}
|
|
.upload-area {
|
display: flex;
|
flex-direction: column;
|
align-items: center;
|
justify-content: center;
|
width: 120px;
|
height: 120px;
|
border: 1px dashed var(--el-border-color);
|
border-radius: 6px;
|
cursor: pointer;
|
transition: var(--el-transition-duration-fast);
|
text-align: center;
|
|
&:hover {
|
border-color: var(--el-color-primary);
|
}
|
}
|
|
.avatar-uploader-icon {
|
font-size: 28px;
|
color: #8c939d;
|
margin-bottom: 8px;
|
}
|
|
.upload-text {
|
font-size: 14px;
|
color: #8c939d;
|
margin-bottom: 4px;
|
}
|
|
.upload-hint {
|
font-size: 12px;
|
color: #999;
|
line-height: 1.2;
|
}
|
</style>
|