feat(admin): 新增活动管理功能并优化字典组件
- 新增活动管理相关功能,包括活动创建、编辑、删除和状态管理 - 新增 PublishStatus 枚举定义活动发布状态 - 优化字典组件,支持字符串和数字类型的字典值 - 重构业务数据存储逻辑,简化字典数据获取流程 - 新增活动详情页面,支持分组管理和队伍配置 - 集成富文本编辑器用于活动规则编辑 - 优化图片上传组件,支持小图上传模式 - 修复模板管理中的区域选择和分页问题 - 更新 TypeScript 类型定义,完善 API 接口 - 调整主题配置,新增活动状态相关颜色
This commit is contained in:
@ -1,15 +1,11 @@
|
||||
import { computed } from 'vue'
|
||||
import { roundTypeOptions, scoreTypeOptions } from '@/constants/business'
|
||||
import { computed, nextTick } from 'vue'
|
||||
import { useDict } from '@/hooks/business/useDict'
|
||||
|
||||
export interface QuestionItem {
|
||||
const { options: roundTypeOptions } = useDict('round_type')
|
||||
const { options: scoreTypeOptions } = useDict('score_type')
|
||||
|
||||
export interface QuestionItem extends Partial<Api.Competition.QuestionListRecord> {
|
||||
id: string
|
||||
questionId: string | null
|
||||
score: number
|
||||
time: number
|
||||
title: string
|
||||
scoreType: Api.Competition.QuestionScoreType
|
||||
uiType?: string
|
||||
templateId?: number
|
||||
}
|
||||
|
||||
export interface RoundModel {
|
||||
@ -28,7 +24,7 @@ export function useQuestionConfig(props: any) {
|
||||
const round = props.modelValue?.rounds?.[roundIndex]
|
||||
if (!round)
|
||||
return 0
|
||||
return round.questions.reduce((sum: number, item: any) => sum + (Number(item.score) || 0), 0)
|
||||
return round.questions.reduce((sum: number, item: any) => sum + (Number(item.Point) || 0), 0)
|
||||
}
|
||||
|
||||
// 计算每个轮次的时长
|
||||
@ -36,7 +32,7 @@ export function useQuestionConfig(props: any) {
|
||||
const round = props.modelValue?.rounds?.[roundIndex]
|
||||
if (!round)
|
||||
return 0
|
||||
return round.questions.reduce((sum: number, item: any) => sum + (Number(item.time) || 0), 0)
|
||||
return round.questions.reduce((sum: number, item: any) => sum + (Number(item.QuestionTime) || 0), 0)
|
||||
}
|
||||
|
||||
// 总计统计
|
||||
@ -44,43 +40,157 @@ export function useQuestionConfig(props: any) {
|
||||
const rounds = props.modelValue?.rounds || []
|
||||
return rounds.reduce((acc: any, round: any) => {
|
||||
acc.count += round.questions.length
|
||||
acc.score += round.questions.reduce((sum: number, item: any) => sum + (Number(item.score) || 0), 0)
|
||||
acc.time += round.questions.reduce((sum: number, item: any) => sum + (Number(item.time) || 0), 0)
|
||||
acc.score += round.questions.reduce((sum: number, item: any) => sum + (Number(item.Point) || 0), 0)
|
||||
acc.time += round.questions.reduce((sum: number, item: any) => sum + (Number(item.QuestionTime) || 0), 0)
|
||||
return acc
|
||||
}, { count: 0, score: 0, time: 0 })
|
||||
})
|
||||
|
||||
// 新增轮次
|
||||
// 拖拽结束处理:根据拖动意图重新分组
|
||||
function handleDragEnd(roundIndex: number, event: any) {
|
||||
nextTick(() => {
|
||||
const round = props.modelValue.rounds[roundIndex]
|
||||
if (!round || !round.questions)
|
||||
return
|
||||
|
||||
const { newIndex } = event
|
||||
const questions = round.questions
|
||||
const draggedItem = questions[newIndex]
|
||||
|
||||
if (!draggedItem)
|
||||
return
|
||||
|
||||
// 统一转字符串进行比较,兼容 number/string
|
||||
const targetQid = String(draggedItem.QuestionID)
|
||||
|
||||
// 1. 分离目标组和其他元素
|
||||
const targetItems = questions.filter((q: any) => String(q.QuestionID) === targetQid)
|
||||
const nonTargetItems = questions.filter((q: any) => String(q.QuestionID) !== targetQid)
|
||||
|
||||
// 2. 计算插入位置
|
||||
// 我们需要找到拖拽元素在非目标元素中的“相对位置”
|
||||
// 如果它被拖到了同组元素中间,我们应该将其归并到该组的后面(保持组的完整性)
|
||||
|
||||
let prevNonTargetIndex = -1
|
||||
|
||||
// 从 newIndex 向前寻找第一个非目标元素
|
||||
for (let i = newIndex - 1; i >= 0; i--) {
|
||||
if (String(questions[i].QuestionID) !== targetQid) {
|
||||
// 找到这个非目标元素在 nonTargetItems 中的索引
|
||||
const prevItem = questions[i]
|
||||
prevNonTargetIndex = nonTargetItems.indexOf(prevItem)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
let insertionIndex = 0
|
||||
|
||||
if (prevNonTargetIndex === -1) {
|
||||
// 如果前面没有非目标元素,说明插到了最前面
|
||||
insertionIndex = 0
|
||||
}
|
||||
else {
|
||||
// 如果前面有非目标元素,检查它所属的组
|
||||
const prevItem = nonTargetItems[prevNonTargetIndex]
|
||||
const prevGroupId = String(prevItem.QuestionID)
|
||||
|
||||
// 找到该组在 nonTargetItems 中的最后一个元素的位置
|
||||
// 这样做是为了确保如果插入到了某组中间,会跳过该组剩余元素,插在该组之后
|
||||
let lastIndexInGroup = prevNonTargetIndex
|
||||
for (let i = prevNonTargetIndex + 1; i < nonTargetItems.length; i++) {
|
||||
if (String(nonTargetItems[i].QuestionID) === prevGroupId) {
|
||||
lastIndexInGroup = i
|
||||
}
|
||||
else {
|
||||
// 遇到不同组,停止
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
insertionIndex = lastIndexInGroup + 1
|
||||
}
|
||||
|
||||
// 3. 重组数组
|
||||
round.questions = [
|
||||
...nonTargetItems.slice(0, insertionIndex),
|
||||
...targetItems,
|
||||
...nonTargetItems.slice(insertionIndex),
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
// 题目类型变更处理:按首次出现顺序分组
|
||||
function handleTypeChange(roundIndex: number) {
|
||||
const round = props.modelValue.rounds[roundIndex]
|
||||
if (!round || !round.questions)
|
||||
return
|
||||
|
||||
const questions = round.questions
|
||||
const groups = new Map<string, any[]>()
|
||||
const groupOrder: string[] = []
|
||||
|
||||
// 记录分组和顺序
|
||||
for (const q of questions) {
|
||||
const qid = String(q.QuestionID)
|
||||
if (!groups.has(qid)) {
|
||||
groups.set(qid, [])
|
||||
groupOrder.push(qid)
|
||||
}
|
||||
groups.get(qid)?.push(q)
|
||||
}
|
||||
|
||||
// 重组
|
||||
const newQuestions: any[] = []
|
||||
for (const qid of groupOrder) {
|
||||
const group = groups.get(qid)
|
||||
if (group) {
|
||||
newQuestions.push(...group)
|
||||
}
|
||||
}
|
||||
|
||||
round.questions = newQuestions
|
||||
}
|
||||
|
||||
// 新增轮次(现在逻辑为:根据 roundType 添加题目到对应分组,如果分组不存在则创建)
|
||||
function addRound(customName?: string, customCount?: number, roundType?: Api.Competition.CompetitionRoundType) {
|
||||
if (!props.modelValue.rounds) {
|
||||
props.modelValue.rounds = []
|
||||
}
|
||||
const index = props.modelValue.rounds.length + 1
|
||||
const count = typeof customCount === 'number' ? customCount : 10
|
||||
const name = typeof customName === 'string' ? customName : `第${index}轮:比赛环节`
|
||||
|
||||
props.modelValue.rounds.push({
|
||||
id: `round_${Date.now()}`,
|
||||
name,
|
||||
roundType: roundType || roundTypeOptions[0].value,
|
||||
questions: Array.from({ length: count }).map(() => ({
|
||||
id: generateId(),
|
||||
questionId: null,
|
||||
score: 5,
|
||||
time: 30,
|
||||
title: '',
|
||||
scoreType: scoreTypeOptions[0].value,
|
||||
})),
|
||||
})
|
||||
const type = roundType || roundTypeOptions.value?.[0]?.value || 0
|
||||
const count = typeof customCount === 'number' ? customCount : 10
|
||||
|
||||
// 查找是否已存在该类型的轮次
|
||||
const existingRound = props.modelValue.rounds.find((r: any) => r.roundType === type)
|
||||
|
||||
const newQuestions = Array.from({ length: count }).map(() => ({
|
||||
id: generateId(),
|
||||
QuestionID: null,
|
||||
Point: 5,
|
||||
QuestionTime: 30,
|
||||
ActitvityQuestionName: '',
|
||||
QuestionRule: scoreTypeOptions.value?.[0]?.value || 0,
|
||||
}))
|
||||
|
||||
if (existingRound) {
|
||||
existingRound.questions.push(...newQuestions)
|
||||
handleTypeChange(props.modelValue.rounds.indexOf(existingRound))
|
||||
}
|
||||
else {
|
||||
// 获取类型名称作为默认名称
|
||||
const typeLabel = roundTypeOptions.value?.find(opt => opt.value === type)?.label || '未知类型'
|
||||
props.modelValue.rounds.push({
|
||||
id: `round_${Date.now()}`,
|
||||
name: typeLabel,
|
||||
roundType: type,
|
||||
questions: newQuestions, // 新创建时全是空的,不需要排序,或者排一下也无妨
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 删除轮次
|
||||
function removeRound(index: number) {
|
||||
props.modelValue.rounds.splice(index, 1)
|
||||
// 重命名剩余轮次
|
||||
props.modelValue.rounds.forEach((round: RoundModel, idx: number) => {
|
||||
round.name = round.name.replace(/第\d+轮/, `第${idx + 1}轮`)
|
||||
})
|
||||
}
|
||||
|
||||
// 新增题目
|
||||
@ -89,12 +199,13 @@ export function useQuestionConfig(props: any) {
|
||||
if (round) {
|
||||
round.questions.push({
|
||||
id: generateId(),
|
||||
value: null,
|
||||
score: 5,
|
||||
time: 30,
|
||||
title: '',
|
||||
scoreType: scoreTypeOptions[0].value,
|
||||
QuestionID: 0,
|
||||
Point: 5,
|
||||
QuestionTime: 30,
|
||||
ActitvityQuestionName: '',
|
||||
QuestionRule: 1,
|
||||
})
|
||||
handleTypeChange(roundIndex)
|
||||
}
|
||||
}
|
||||
|
||||
@ -123,31 +234,31 @@ export function useQuestionConfig(props: any) {
|
||||
const question = round.questions[j]
|
||||
const questionPrefix = `${round.name} 第 ${j + 1} 题`
|
||||
|
||||
if (!question.questionId) {
|
||||
if (question.QuestionID === undefined || question.QuestionID === null || question.QuestionID === '') {
|
||||
window.$message?.error(`${questionPrefix}未选择题型`)
|
||||
return false
|
||||
}
|
||||
if (!question.uiType) {
|
||||
if (question.UIType === undefined || question.UIType === null || question.UIType === '') {
|
||||
window.$message?.error(`${questionPrefix}未选择UI类型`)
|
||||
return false
|
||||
}
|
||||
if (!question.templateId) {
|
||||
if (question.TemplateID === undefined || question.TemplateID === null || question.TemplateID === '') {
|
||||
window.$message?.error(`${questionPrefix}未选择模板`)
|
||||
return false
|
||||
}
|
||||
if (!question.time) {
|
||||
if (question.QuestionTime === undefined || question.QuestionTime === null || question.QuestionTime === '') {
|
||||
window.$message?.error(`${questionPrefix}未设置答题时间`)
|
||||
return false
|
||||
}
|
||||
if (!question.scoreType) {
|
||||
if (question.QuestionRule === undefined || question.QuestionRule === null || question.QuestionRule === '') {
|
||||
window.$message?.error(`${questionPrefix}未设置分数规则`)
|
||||
return false
|
||||
}
|
||||
if (!question.title) {
|
||||
if (question.ActitvityQuestionName === undefined || question.ActitvityQuestionName === null || question.ActitvityQuestionName === '') {
|
||||
window.$message?.error(`${questionPrefix}未填写标题`)
|
||||
return false
|
||||
}
|
||||
if (question.score === null || question.score === undefined) {
|
||||
if (question.Point === null || question.Point === undefined) {
|
||||
window.$message?.error(`${questionPrefix}未设置分数`)
|
||||
return false
|
||||
}
|
||||
@ -169,6 +280,8 @@ export function useQuestionConfig(props: any) {
|
||||
addRound,
|
||||
removeRound,
|
||||
addQuestion,
|
||||
handleDragEnd,
|
||||
handleTypeChange,
|
||||
removeQuestion,
|
||||
validate,
|
||||
reset,
|
||||
|
||||
Reference in New Issue
Block a user