From beaaee72b48ce5e3a67eded17a5e0488d40d6aa8 Mon Sep 17 00:00:00 2001
From: rjl <98n9@163.com>
Date: Thu, 29 Jan 2026 18:03:13 +0800
Subject: [PATCH] =?UTF-8?q?feat(competition):=20=E5=AE=9E=E7=8E=B0?=
=?UTF-8?q?=E6=AF=94=E8=B5=9B=E5=88=9B=E5=BB=BA=E5=8A=9F=E8=83=BD=E5=B9=B6?=
=?UTF-8?q?=E9=9B=86=E6=88=90=E5=90=8E=E7=AB=AFAPI?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 新增比赛创建页面,包含基本信息、组别管理和题目配置三个步骤
- 添加 OSS 图片上传组件,支持海报上传到阿里云OSS
- 集成后端API:获取房间列表、创建活动、创建队伍和创建题目
- 优化表单数据绑定和验证逻辑,使用防抖减少频繁更新
- 修复组别管理Excel导入的数据填充问题
- 更新类型定义,添加CommonResponse和比赛相关接口类型
---
.../common/oss-image-upload/index.vue | 123 ++++++++++++++++
apps/admin/src/service/api/competition.ts | 42 ++++++
apps/admin/src/service/api/template.ts | 2 +-
apps/admin/src/typings/api/Competition.d.ts | 77 ++++++++++
apps/admin/src/typings/api/common.d.ts | 8 ++
apps/admin/src/typings/components.d.ts | 2 +
.../competition/competition-add/index.vue | 134 ++++++++++++++----
.../competition-add/modules/BasicInfo.vue | 62 +++-----
.../modules/GroupManagement.vue | 20 ++-
.../modules/QuestionConfig.vue | 78 ++++++----
.../competition-add/modules/useBasicInfo.ts | 38 +++--
.../modules/useGroupManagement.ts | 30 ++--
.../modules/useQuestionConfig.ts | 24 +---
13 files changed, 493 insertions(+), 147 deletions(-)
create mode 100644 apps/admin/src/components/common/oss-image-upload/index.vue
create mode 100644 apps/admin/src/service/api/competition.ts
diff --git a/apps/admin/src/components/common/oss-image-upload/index.vue b/apps/admin/src/components/common/oss-image-upload/index.vue
new file mode 100644
index 0000000..f951486
--- /dev/null
+++ b/apps/admin/src/components/common/oss-image-upload/index.vue
@@ -0,0 +1,123 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 更换
+
+
+ 删除
+
+
+
+
+
+
+
点击上传海报
+
{{ hint }}
+
+
+
+
+
+
+
diff --git a/apps/admin/src/service/api/competition.ts b/apps/admin/src/service/api/competition.ts
new file mode 100644
index 0000000..33f6ec0
--- /dev/null
+++ b/apps/admin/src/service/api/competition.ts
@@ -0,0 +1,42 @@
+import { request } from '../request'
+
+/** 房间相关接口 */
+export function fetchGetRoomList() {
+ return request>({
+ url: '/Base/ActivityMain/GetActivity_RoomList',
+ method: 'get',
+ })
+}
+
+// 现在服务端把活动新建弄成了三个接口:
+// 1. 创建活动
+// 2. 创建活动房间
+// 3. 创建活动队伍
+// 但是业务要求最后一步创建
+
+/** 创建活动基础信息 */
+export function fetchCreateActivity(data: Api.Competition.CreateRoomRequest) {
+ return request>({
+ url: '/Base/ActivityMain/AddActivity',
+ method: 'post',
+ data,
+ })
+}
+
+/** 创建活动队伍 */
+export function fetchCreateTeamList(data: Api.Competition.CreateTeamListRequest[]) {
+ return request>({
+ url: '/Base/ActivityMain/AddActivity_TeamList',
+ method: 'post',
+ data,
+ })
+}
+
+/** 创建活动题目 */
+export function fetchCreateQuestion(data: Api.Competition.CreateQuestionRequest[]) {
+ return request>({
+ url: '/Base/ActivityMain/AddOrUpdateActivity_Question',
+ method: 'post',
+ data,
+ })
+}
diff --git a/apps/admin/src/service/api/template.ts b/apps/admin/src/service/api/template.ts
index 541bf79..2577be7 100644
--- a/apps/admin/src/service/api/template.ts
+++ b/apps/admin/src/service/api/template.ts
@@ -4,7 +4,7 @@ import { request } from '../request'
* 获取模板列表
*/
export function fetchTemplateList(data?: Api.Template.TemplateSearchParams) {
- return request>>({
+ return request>({
url: '/Base/ActivityMain/GetTemplete_Pag',
method: 'get',
params: data || {},
diff --git a/apps/admin/src/typings/api/Competition.d.ts b/apps/admin/src/typings/api/Competition.d.ts
index 680facd..1fa2697 100644
--- a/apps/admin/src/typings/api/Competition.d.ts
+++ b/apps/admin/src/typings/api/Competition.d.ts
@@ -52,6 +52,83 @@ declare namespace Api {
/** competition round type */
type CompetitionRoundType = '1' | '2' /** 题包环节 | 加时环节 */
+ /** room */
+ interface Room {
+ /** room id */
+ ID: number
+ /** room name */
+ Name: string
+ }
+
+ /** create room request */
+ interface CreateRoomRequest {
+ /** 活动 id */
+ id: number
+ /** 活动名称 */
+ name: string
+ /** 开始时间 */
+ startTime: string
+ /** 结束时间 */
+ endTime: string
+ /** 队伍数量 */
+ teams: number
+ /** 创建时间 */
+ createdTime: string
+ /** 房间 id */
+ roomID: number
+ /** 队伍分组数量 */
+ teamGroupNumber: number
+ /** 背景图片 */
+ backgroundImg: string
+ }
+
+ /** create team list request */
+ interface CreateTeamListRequest {
+ /** 队伍 id */
+ id: number
+ /** 主 id */
+ mainId: number
+ /** 队伍编号 */
+ number: string
+ /** 队伍名称 */
+ name: string
+ /** 笔序列号 */
+ penSerial: string
+ /** 队员名单 */
+ nameList: string
+ /** 学校名称 */
+ schoolName: string
+ /** 队伍分组 id */
+ teamGroupId: number
+ }
+
+ interface CreateQuestionRequest {
+ /** 题目 id */
+ id: number
+ /** 活动 id */
+ activityID: number
+ /** 问题 id */
+ questionID: number
+ /** 题目序号 */
+ questionIndex: number
+ /** 活动题目名称 */
+ actitvityQuestionName: string
+ /** 答题时间(秒) */
+ questionTime: number
+ /** 题目规则 */
+ questionRule: number
+ /** UI 类型 */
+ uiType: string
+ /** 分值 */
+ point: number
+ /** 题目副标题 */
+ questionSubTitle: string
+ /** 题目模板 id */
+ templateID: QuestionTemplateId
+ /** 赛题包类型 */
+ roundType: number
+ }
+
/** common record */
type CommonRecord = {
/** record id */
diff --git a/apps/admin/src/typings/api/common.d.ts b/apps/admin/src/typings/api/common.d.ts
index 1316152..73c3e24 100644
--- a/apps/admin/src/typings/api/common.d.ts
+++ b/apps/admin/src/typings/api/common.d.ts
@@ -23,6 +23,14 @@ declare namespace Api {
/** common search params of table */
type CommonSearchParams = Pick
+ /** common response */
+ interface CommonResponse {
+ success: boolean
+ code: number
+ msg: string
+ data: any
+ }
+
/**
* enable status
*
diff --git a/apps/admin/src/typings/components.d.ts b/apps/admin/src/typings/components.d.ts
index daaf174..4868069 100644
--- a/apps/admin/src/typings/components.d.ts
+++ b/apps/admin/src/typings/components.d.ts
@@ -147,6 +147,7 @@ declare module 'vue' {
NTreeSelect: typeof import('naive-ui')['NTreeSelect']
NUpload: typeof import('naive-ui')['NUpload']
NWatermark: typeof import('naive-ui')['NWatermark']
+ OssImageUpload: typeof import('./../components/common/oss-image-upload/index.vue')['default']
PageHeader: typeof import('./../components/common/page-header.vue')['default']
PinToggler: typeof import('./../components/common/pin-toggler.vue')['default']
ReloadButton: typeof import('./../components/common/reload-button.vue')['default']
@@ -307,6 +308,7 @@ declare global {
const NTreeSelect: typeof import('naive-ui')['NTreeSelect']
const NUpload: typeof import('naive-ui')['NUpload']
const NWatermark: typeof import('naive-ui')['NWatermark']
+ const OssImageUpload: typeof import('./../components/common/oss-image-upload/index.vue')['default']
const PageHeader: typeof import('./../components/common/page-header.vue')['default']
const PinToggler: typeof import('./../components/common/pin-toggler.vue')['default']
const ReloadButton: typeof import('./../components/common/reload-button.vue')['default']
diff --git a/apps/admin/src/views/competition/competition-add/index.vue b/apps/admin/src/views/competition/competition-add/index.vue
index 8a52796..05901b4 100644
--- a/apps/admin/src/views/competition/competition-add/index.vue
+++ b/apps/admin/src/views/competition/competition-add/index.vue
@@ -1,8 +1,11 @@
@@ -182,8 +266,8 @@ function handleReset() {
diff --git a/apps/admin/src/views/competition/competition-add/modules/BasicInfo.vue b/apps/admin/src/views/competition/competition-add/modules/BasicInfo.vue
index af6c701..7422bbe 100644
--- a/apps/admin/src/views/competition/competition-add/modules/BasicInfo.vue
+++ b/apps/admin/src/views/competition/competition-add/modules/BasicInfo.vue
@@ -1,24 +1,29 @@
@@ -54,15 +59,9 @@ defineExpose({ validate, reset })
*
*
*
-
-
-
-
-
- 更换
-
-
- 删除
-
-
-
-
![Poster]()
-
-
-
点击上传海报
-
支持 JPG/PNG 格式
-
-
-
+
diff --git a/apps/admin/src/views/competition/competition-add/modules/GroupManagement.vue b/apps/admin/src/views/competition/competition-add/modules/GroupManagement.vue
index c202bcc..88178f2 100644
--- a/apps/admin/src/views/competition/competition-add/modules/GroupManagement.vue
+++ b/apps/admin/src/views/competition/competition-add/modules/GroupManagement.vue
@@ -1,18 +1,36 @@
diff --git a/apps/admin/src/views/competition/competition-add/modules/QuestionConfig.vue b/apps/admin/src/views/competition/competition-add/modules/QuestionConfig.vue
index 734a9ac..311d9ab 100644
--- a/apps/admin/src/views/competition/competition-add/modules/QuestionConfig.vue
+++ b/apps/admin/src/views/competition/competition-add/modules/QuestionConfig.vue
@@ -1,8 +1,10 @@
@@ -271,7 +291,7 @@ defineExpose({ validate, reset })
@@ -279,16 +299,16 @@ defineExpose({ validate, reset })
diff --git a/apps/admin/src/views/competition/competition-add/modules/useBasicInfo.ts b/apps/admin/src/views/competition/competition-add/modules/useBasicInfo.ts
index aa67237..d319204 100644
--- a/apps/admin/src/views/competition/competition-add/modules/useBasicInfo.ts
+++ b/apps/admin/src/views/competition/competition-add/modules/useBasicInfo.ts
@@ -1,30 +1,38 @@
-import type { UploadCustomRequestOptions } from 'naive-ui'
import { debounce } from '@sa/utils'
import { ref, watch } from 'vue'
export interface BasicInfoModel {
name: string
- startTime: number | null
- endTime: number | null
+ startTime: string | null
+ endTime: string | null
groupCount: number
teamCount: number
poster: string
- ap: string
+ roomId: number | null
}
export function useBasicInfo(props: any, emit: any) {
const formData = ref({ ...props.modelValue })
- // 修复死循环:添加值对比逻辑
watch(() => props.modelValue, (val) => {
- if (JSON.stringify(val) !== JSON.stringify(formData.value)) {
- formData.value = { ...val }
+ const newForm: BasicInfoModel = {
+ name: val.name,
+ startTime: val.startTime,
+ endTime: val.endTime,
+ groupCount: val.groupCount,
+ teamCount: val.teamCount,
+ poster: val.poster,
+ roomId: val.roomId,
+ }
+
+ if (JSON.stringify(newForm) !== JSON.stringify(formData.value)) {
+ formData.value = newForm
}
}, { deep: true })
// 添加防抖:300ms 延迟,避免频繁触发父组件更新
const handleUpdate = debounce((val: typeof formData.value) => {
- emit('update:modelValue', { ...val })
+ emit('update:modelValue', { ...props.modelValue, ...val })
}, 300)
watch(formData, (val) => {
@@ -46,17 +54,6 @@ export function useBasicInfo(props: any, emit: any) {
}
}
- // 模拟上传处理
- function handleUpload({ file, onFinish }: UploadCustomRequestOptions) {
- const reader = new FileReader()
- reader.readAsDataURL(file.file as File)
- reader.onload = () => {
- // 模拟上传成功,直接使用 base64 作为图片地址
- formData.value.poster = reader.result as string
- onFinish()
- }
- }
-
// 校验方法
function validate() {
if (!formData.value.name) {
@@ -95,14 +92,13 @@ export function useBasicInfo(props: any, emit: any) {
groupCount: 10,
teamCount: 4,
poster: '',
- ap: '',
+ roomId: null,
}
}
return {
formData,
updateCount,
- handleUpload,
validate,
reset,
}
diff --git a/apps/admin/src/views/competition/competition-add/modules/useGroupManagement.ts b/apps/admin/src/views/competition/competition-add/modules/useGroupManagement.ts
index 8bd9235..a316e43 100644
--- a/apps/admin/src/views/competition/competition-add/modules/useGroupManagement.ts
+++ b/apps/admin/src/views/competition/competition-add/modules/useGroupManagement.ts
@@ -13,7 +13,10 @@ export function useGroupManagement(props: any) {
if (!groupCount || groupCount <= 0)
return
- const oldGroups = groups.value
+ // 优先使用现有的 groups.value,如果为空则尝试使用 props 中的初始数据
+ const oldGroups = groups.value.length > 0
+ ? groups.value
+ : (props.modelValue?.groupManagement || [])
groups.value = Array.from({ length: groupCount }).map((_, index) => {
const i = index + 1
@@ -39,7 +42,7 @@ export function useGroupManagement(props: any) {
})
}
- watch(() => props.config, generateGroups, { deep: true, immediate: true })
+ watch(() => [props.config?.groupCount, props.config?.teamCount], generateGroups, { immediate: true, deep: true })
// 提交时候校验表单是否符合要求
function validate() {
@@ -253,25 +256,34 @@ export function useExcelImport(props: any, groups: any) {
}
// 填充数据
- finalData.forEach((row: any, index) => {
- if (index >= groups.value.length)
- return
+ const newGroups = groups.value.map((group: any, index: number) => {
+ const row = finalData[index]
+ if (!row)
+ return group
- const group = groups.value[index]
+ const newGroup = { ...group }
// 分组名称
if (row['分组名称']) {
- group.name = String(row['分组名称'])
+ newGroup.name = String(row['分组名称'])
}
// 队伍名称
- group.teams.forEach((team: any, tIndex: number) => {
+ newGroup.teams = group.teams.map((team: any, tIndex: number) => {
const key = `队伍${tIndex + 1}名称`
if (row[key]) {
- team.name = String(row[key])
+ return {
+ ...team,
+ name: String(row[key]),
+ }
}
+ return team
})
+
+ return newGroup
})
+
+ groups.value = newGroups
}
catch (error) {
console.error('Excel 解析失败:', error)
diff --git a/apps/admin/src/views/competition/competition-add/modules/useQuestionConfig.ts b/apps/admin/src/views/competition/competition-add/modules/useQuestionConfig.ts
index ec2f1c6..28f4dfa 100644
--- a/apps/admin/src/views/competition/competition-add/modules/useQuestionConfig.ts
+++ b/apps/admin/src/views/competition/competition-add/modules/useQuestionConfig.ts
@@ -3,13 +3,13 @@ import { roundTypeOptions, scoreTypeOptions } from '@/constants/business'
export interface QuestionItem {
id: string
- value: string | null
+ questionId: string | null
score: number
time: number
title: string
scoreType: Api.Competition.QuestionScoreType
uiType?: string
- templateId?: string
+ templateId?: number
}
export interface RoundModel {
@@ -50,21 +50,6 @@ export function useQuestionConfig(props: any) {
}, { count: 0, score: 0, time: 0 })
})
- const questionOptions = [
- { label: '请根据提示书写正确的汉字-jiū 表示小鸟的叫声。', value: 'hanzi' },
- { label: '请根据提示书写正确的汉字-“春色满园关不住,一枝红杏出墙来”。请书写“杏”字。', value: 'tongyin' },
- { label: '请写出含有“车”的汉字。', value: 'pianpang' },
- { label: '请根据拼音书写正确的词语。', value: 'ciyu' },
- { label: '请写出含有反义字的四字成语。 - 例如:“不日而月”', value: 'chengyu' },
- { label: '请根据图片书写正确的成语。', value: 'fanyi' },
- { label: '近义词挑战', value: 'jinyi' },
- { label: '看图猜字', value: 'kantu' },
- { label: '古诗词接龙', value: 'gushi' },
- { label: '名句听写', value: 'mingju' },
- { label: '极速成语接龙 - 每题10秒', value: 'speed10' },
- { label: '极速成语接龙 - 每题20秒', value: 'speed20' },
- ]
-
// 新增轮次
function addRound(customName?: string, customCount?: number, roundType?: Api.Competition.CompetitionRoundType) {
if (!props.modelValue.rounds) {
@@ -80,7 +65,7 @@ export function useQuestionConfig(props: any) {
roundType: roundType || roundTypeOptions[0].value,
questions: Array.from({ length: count }).map(() => ({
id: generateId(),
- value: null,
+ questionId: null,
score: 5,
time: 30,
title: '',
@@ -138,7 +123,7 @@ export function useQuestionConfig(props: any) {
const question = round.questions[j]
const questionPrefix = `${round.name} 第 ${j + 1} 题`
- if (!question.value) {
+ if (!question.questionId) {
window.$message?.error(`${questionPrefix}未选择题型`)
return false
}
@@ -181,7 +166,6 @@ export function useQuestionConfig(props: any) {
getRoundScore,
getRoundTime,
totalStats,
- questionOptions,
addRound,
removeRound,
addQuestion,