library-picturebook-activity/frontend/src/views/contests/components/SubmitWorkDrawer.vue

401 lines
9.4 KiB
Vue
Raw Normal View History

2026-01-09 18:14:35 +08:00
<template>
<a-drawer
v-model:open="visible"
title="上传作品"
placement="right"
width="850px"
:footer-style="{ textAlign: 'right', padding: '16px 24px' }"
@close="handleCancel"
>
<template #title>
<div class="drawer-title">
<span>上传作品</span>
</div>
</template>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
layout="horizontal"
>
<a-form-item label="作品名称" name="title" required>
<a-input
v-model:value="form.title"
placeholder="请输入作品名称"
:maxlength="200"
/>
</a-form-item>
<a-form-item label="作品介绍" name="description" required>
<a-textarea
v-model:value="form.description"
placeholder="请输入作品介绍"
:rows="6"
:maxlength="2000"
show-count
/>
</a-form-item>
<a-form-item label="上传作品" name="workFile" required>
<a-upload
v-model:file-list="workFileList"
:before-upload="handleBeforeUploadWork"
:remove="handleRemoveWork"
:accept="workAcceptTypes"
:max-count="1"
>
<a-button>
<template #icon><UploadOutlined /></template>
选择
</a-button>
<template #tip>
<div class="upload-tip">
支持3D文件格式.obj, .fbx, .3ds, .dae, .blend, .max, .c4d, .stl,
.ply, .gltf, .glb
</div>
</template>
</a-upload>
</a-form-item>
<a-form-item label="上传附件">
<a-upload
v-model:file-list="attachmentFileList"
:before-upload="handleBeforeUploadAttachment"
:remove="handleRemoveAttachment"
:max-count="5"
>
<a-button>
<template #icon><UploadOutlined /></template>
上传
</a-button>
</a-upload>
</a-form-item>
</a-form>
<template #footer>
<a-space>
<a-button @click="handleCancel">取消</a-button>
<a-button type="primary" :loading="submitLoading" @click="handleSubmit">
确定
</a-button>
</a-space>
</template>
</a-drawer>
</template>
<script setup lang="ts">
import { ref, reactive, watch } from "vue"
import { message } from "ant-design-vue"
import { UploadOutlined } from "@ant-design/icons-vue"
import type { FormInstance, UploadFile } from "ant-design-vue"
import { worksApi, registrationsApi, type SubmitWorkForm } from "@/api/contests"
import { useAuthStore } from "@/stores/auth"
import request from "@/utils/request"
interface Props {
open: boolean
contestId: number
}
interface Emits {
(e: "update:open", value: boolean): void
(e: "success"): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const authStore = useAuthStore()
const visible = ref(false)
const submitLoading = ref(false)
const formRef = ref<FormInstance>()
// 3D文件格式
const workAcceptTypes =
".obj,.fbx,.3ds,.dae,.blend,.max,.c4d,.stl,.ply,.gltf,.glb"
const form = reactive<{
title: string
description: string
workFile: File | null
attachmentFiles: File[]
}>({
title: "",
description: "",
workFile: null,
attachmentFiles: [],
})
const workFileList = ref<UploadFile[]>([])
const attachmentFileList = ref<UploadFile[]>([])
// 表单验证规则
const rules = {
title: [{ required: true, message: "请输入作品名称", trigger: "blur" }],
description: [{ required: true, message: "请输入作品介绍", trigger: "blur" }],
workFile: [
{
required: true,
validator: () => {
if (!form.workFile) {
return Promise.reject(new Error("请上传作品文件"))
}
return Promise.resolve()
},
trigger: "change",
},
],
}
watch(
() => props.open,
(newVal) => {
visible.value = newVal
if (newVal) {
resetForm()
fetchRegistrationId()
}
},
{ immediate: true }
)
watch(visible, (newVal) => {
emit("update:open", newVal)
})
// 获取报名记录ID
const registrationIdRef = ref<number | null>(null)
const fetchRegistrationId = async () => {
if (!authStore.user?.id) {
message.error("用户未登录")
return
}
try {
const response = await registrationsApi.getList({
contestId: props.contestId,
userId: authStore.user.id,
page: 1,
pageSize: 1,
})
if (response.list && response.list.length > 0) {
const registration = response.list[0]
if (registration.registrationState === "passed") {
registrationIdRef.value = registration.id
} else {
message.warning("您的报名尚未通过审核,无法上传作品")
visible.value = false
}
} else {
message.warning("您尚未报名该赛事,无法上传作品")
visible.value = false
}
} catch (error: any) {
message.error(error?.response?.data?.message || "获取报名信息失败")
visible.value = false
}
}
// 上传作品前验证
const handleBeforeUploadWork = (file: File): boolean => {
const fileName = file.name.toLowerCase()
const validExtensions = [
".obj",
".fbx",
".3ds",
".dae",
".blend",
".max",
".c4d",
".stl",
".ply",
".gltf",
".glb",
]
const isValid = validExtensions.some((ext) => fileName.endsWith(ext))
if (!isValid) {
message.error(
"请上传3D文件格式.obj, .fbx, .3ds, .dae, .blend, .max, .c4d, .stl, .ply, .gltf, .glb"
)
return false
}
// 检查文件大小限制为500MB
const maxSize = 500 * 1024 * 1024 // 500MB
if (file.size > maxSize) {
message.error("文件大小不能超过500MB")
return false
}
form.workFile = file
return false // 阻止自动上传
}
// 移除作品文件
const handleRemoveWork = () => {
form.workFile = null
return true
}
// 上传附件前验证
const handleBeforeUploadAttachment = (file: File): boolean => {
// 检查文件大小限制为100MB
const maxSize = 100 * 1024 * 1024 // 100MB
if (file.size > maxSize) {
message.error("附件大小不能超过100MB")
return false
}
form.attachmentFiles.push(file)
return false // 阻止自动上传
}
// 移除附件
const handleRemoveAttachment = (file: UploadFile) => {
const index = form.attachmentFiles.findIndex(
(f) => f.name === file.name && f.size === file.size
)
if (index > -1) {
form.attachmentFiles.splice(index, 1)
}
return true
}
// 重置表单
const resetForm = () => {
form.title = ""
form.description = ""
form.workFile = null
form.attachmentFiles = []
workFileList.value = []
attachmentFileList.value = []
formRef.value?.resetFields()
}
// 提交表单
const handleSubmit = async () => {
if (!formRef.value) return
try {
await formRef.value.validate()
if (!registrationIdRef.value) {
message.error("无法获取报名记录,请重新打开")
return
}
if (!form.workFile) {
message.error("请上传作品文件")
return
}
submitLoading.value = true
// 上传文件并获取URL
const workFileUrls: string[] = []
const attachmentUrls: string[] = []
// 上传作品文件
if (form.workFile) {
try {
const workUrl = await uploadFile(form.workFile)
workFileUrls.push(workUrl)
} catch (error: any) {
message.error("作品文件上传失败:" + (error?.message || "未知错误"))
submitLoading.value = false
return
}
}
// 上传附件
for (const file of form.attachmentFiles) {
try {
const url = await uploadFile(file)
attachmentUrls.push(url)
} catch (error: any) {
console.error("附件上传失败:", error)
// 附件上传失败不影响主流程,只记录错误
}
}
const submitData: SubmitWorkForm = {
registrationId: registrationIdRef.value,
title: form.title,
description: form.description,
files: [...workFileUrls, ...attachmentUrls],
}
await worksApi.submit(submitData)
message.success("作品提交成功")
emit("success")
visible.value = false
resetForm()
} catch (error: any) {
if (error?.errorFields) {
// 表单验证错误
return
}
message.error(error?.response?.data?.message || "作品提交失败")
} finally {
submitLoading.value = false
}
}
// 文件上传函数
const uploadFile = async (file: File): Promise<string> => {
const formData = new FormData()
formData.append("file", file)
try {
// 使用文件上传接口
const response = await request.post<any>("/upload", formData, {
headers: {
"Content-Type": "multipart/form-data",
},
})
// 返回文件URL
if (response && typeof response === "object" && "url" in response) {
return String(response.url)
}
throw new Error("文件上传返回格式不正确")
} catch (error: any) {
console.error("文件上传失败:", error)
throw new Error(error?.response?.data?.message || "文件上传失败")
}
}
// 取消
const handleCancel = () => {
visible.value = false
resetForm()
}
</script>
<style lang="scss" scoped>
.drawer-title {
font-size: 16px;
font-weight: 500;
color: #303133;
}
.upload-tip {
margin-top: 8px;
font-size: 12px;
color: #8c8c8c;
}
:deep(.ant-upload-list) {
margin-top: 8px;
}
</style>