401 lines
9.4 KiB
Vue
401 lines
9.4 KiB
Vue
|
|
<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>
|