feat(competition): 实现比赛创建功能并集成后端API
- 新增比赛创建页面,包含基本信息、组别管理和题目配置三个步骤 - 添加 OSS 图片上传组件,支持海报上传到阿里云OSS - 集成后端API:获取房间列表、创建活动、创建队伍和创建题目 - 优化表单数据绑定和验证逻辑,使用防抖减少频繁更新 - 修复组别管理Excel导入的数据填充问题 - 更新类型定义,添加CommonResponse和比赛相关接口类型
This commit is contained in:
123
apps/admin/src/components/common/oss-image-upload/index.vue
Normal file
123
apps/admin/src/components/common/oss-image-upload/index.vue
Normal file
@ -0,0 +1,123 @@
|
||||
<script setup lang="ts">
|
||||
import type { UploadCustomRequestOptions } from 'naive-ui'
|
||||
import { NButton, NSpin, NUpload, useMessage } from 'naive-ui'
|
||||
import { ref } from 'vue'
|
||||
import { getAliOssTokenAxios } from '@/service/api/upload'
|
||||
import { browserPathJoin } from '@/utils/date'
|
||||
import { initOSSClient, uploadFileToOSS } from '@/utils/oss'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
/** 图片地址 */
|
||||
modelValue?: string
|
||||
/** 上传路径 */
|
||||
path?: string
|
||||
/** 提示文字 */
|
||||
hint?: string
|
||||
/** 最大文件大小 (KB) */
|
||||
maxSize?: number
|
||||
/** 接受的文件类型 */
|
||||
accept?: string
|
||||
}>(), {
|
||||
hint: '支持 JPG/PNG 格式',
|
||||
accept: 'image/*',
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const loading = ref(false)
|
||||
const message = useMessage()
|
||||
|
||||
async function customRequest({ file, onFinish, onError }: UploadCustomRequestOptions) {
|
||||
try {
|
||||
// 校验文件大小
|
||||
if (props.maxSize && file.file) {
|
||||
const sizeKB = file.file.size / 1024
|
||||
if (sizeKB > props.maxSize) {
|
||||
const maxSizeText = props.maxSize >= 1024 ? `${(props.maxSize / 1024).toFixed(2)}MB` : `${props.maxSize}KB`
|
||||
throw new Error(`文件大小不能超过 ${maxSizeText}`)
|
||||
}
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
// 1. 获取上传凭证
|
||||
const { data: tokenData, error: tokenError } = await getAliOssTokenAxios()
|
||||
if (tokenError || !tokenData) {
|
||||
throw new Error('获取上传凭证失败')
|
||||
}
|
||||
|
||||
// 2. 初始化 OSS 客户端
|
||||
const client = initOSSClient(tokenData.data || {})
|
||||
|
||||
// 3. 准备路径
|
||||
let path = props.path || `temp/${Date.now()}`
|
||||
path = browserPathJoin(path, file.name)
|
||||
|
||||
// 4. 上传文件
|
||||
await uploadFileToOSS(client, file.file as File, path)
|
||||
|
||||
// 5. 获取 URL
|
||||
const url = browserPathJoin(import.meta.env.VITE_BASE_OSS_URL, path)
|
||||
|
||||
emit('update:modelValue', url)
|
||||
onFinish()
|
||||
}
|
||||
catch (error: any) {
|
||||
message.error(error.message || '上传失败')
|
||||
onError()
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleRemove() {
|
||||
emit('update:modelValue', '')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NUpload
|
||||
:accept="accept"
|
||||
:show-file-list="false"
|
||||
:custom-request="customRequest"
|
||||
class="block w-full"
|
||||
>
|
||||
<div
|
||||
class="relative h-[400px] w-full flex flex-col cursor-pointer items-center justify-center overflow-hidden rounded-3xl bg-[#F5F8FF] transition-all hover:bg-gray-100"
|
||||
:class="{ 'border-2 border-dashed border-gray-300': !modelValue }"
|
||||
>
|
||||
<div v-if="loading" class="absolute inset-0 z-50 flex items-center justify-center bg-white/50">
|
||||
<NSpin size="large" />
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div
|
||||
v-if="modelValue"
|
||||
class="group absolute inset-0 flex items-center justify-center bg-black/50 opacity-0 transition-opacity hover:opacity-100"
|
||||
>
|
||||
<div class="flex gap-4">
|
||||
<!-- NUpload 的 trigger 会自动处理点击事件 -->
|
||||
<NButton ghost color="#fff" size="small">
|
||||
更换
|
||||
</NButton>
|
||||
<NButton ghost color="#ff4d4f" size="small" @click.stop="handleRemove">
|
||||
删除
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
<img v-if="modelValue" :src="modelValue" class="h-full w-full object-cover" alt="Poster">
|
||||
<div v-else class="flex flex-col items-center text-gray-400">
|
||||
<div class="i-carbon-add-filled mb-4 text-4xl text-[#3B82F6]" />
|
||||
<span class="text-sm font-bold">点击上传海报</span>
|
||||
<span class="mt-2 text-xs opacity-60">{{ hint }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</NUpload>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.n-upload-trigger) {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user