Files
reader-star/apps/admin/src/views/competition/competition-add/modules/useQuestionConfig.ts

290 lines
9.3 KiB
TypeScript
Raw Normal View History

import { computed, nextTick } from 'vue'
import { useDict } from '@/hooks/business/useDict'
const { options: roundTypeOptions } = useDict('round_type')
const { options: scoreTypeOptions } = useDict('score_type')
export interface QuestionItem extends Partial<Api.Competition.QuestionListRecord> {
id: string
}
export interface RoundModel {
id: string
name: string
questions: QuestionItem[]
}
function generateId() {
return `q_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`
}
export function useQuestionConfig(props: any) {
// 计算每个轮次的分数
const getRoundScore = (roundIndex: number) => {
const round = props.modelValue?.rounds?.[roundIndex]
if (!round)
return 0
return round.questions.reduce((sum: number, item: any) => sum + (Number(item.Point) || 0), 0)
}
// 计算每个轮次的时长
const getRoundTime = (roundIndex: number) => {
const round = props.modelValue?.rounds?.[roundIndex]
if (!round)
return 0
return round.questions.reduce((sum: number, item: any) => sum + (Number(item.QuestionTime) || 0), 0)
}
// 总计统计
const totalStats = computed(() => {
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.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 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)
}
// 新增题目
function addQuestion(roundIndex: number) {
const round = props.modelValue.rounds[roundIndex]
if (round) {
round.questions.push({
id: generateId(),
QuestionID: 0,
Point: 5,
QuestionTime: 30,
ActitvityQuestionName: '',
QuestionRule: 1,
})
handleTypeChange(roundIndex)
}
}
// 删除题目
function removeQuestion(roundIndex: number, questionIndex: number) {
const round = props.modelValue.rounds[roundIndex]
if (round) {
round.questions.splice(questionIndex, 1)
}
}
function validate() {
const rounds = props.modelValue?.rounds || []
if (rounds.length === 0) {
window.$message?.error('请至少配置一个比赛环节')
return false
}
for (let i = 0; i < rounds.length; i++) {
const round = rounds[i]
if (round.questions.length === 0) {
window.$message?.error(`${round.name} 至少需要配置一道题目`)
return false
}
for (let j = 0; j < round.questions.length; j++) {
const question = round.questions[j]
const questionPrefix = `${round.name}${j + 1}`
if (question.QuestionID === undefined || question.QuestionID === null || question.QuestionID === '') {
window.$message?.error(`${questionPrefix}未选择题型`)
return false
}
if (question.UIType === undefined || question.UIType === null || question.UIType === '') {
window.$message?.error(`${questionPrefix}未选择UI类型`)
return false
}
if (question.TemplateID === undefined || question.TemplateID === null || question.TemplateID === '') {
window.$message?.error(`${questionPrefix}未选择模板`)
return false
}
if (question.QuestionTime === undefined || question.QuestionTime === null || question.QuestionTime === '') {
window.$message?.error(`${questionPrefix}未设置答题时间`)
return false
}
if (question.QuestionRule === undefined || question.QuestionRule === null || question.QuestionRule === '') {
window.$message?.error(`${questionPrefix}未设置分数规则`)
return false
}
if (question.ActitvityQuestionName === undefined || question.ActitvityQuestionName === null || question.ActitvityQuestionName === '') {
window.$message?.error(`${questionPrefix}未填写标题`)
return false
}
if (question.Point === null || question.Point === undefined) {
window.$message?.error(`${questionPrefix}未设置分数`)
return false
}
}
}
return true
}
function reset() {
// 默认重置为1个轮次10个题目
props.modelValue.rounds = []
addRound()
}
return {
getRoundScore,
getRoundTime,
totalStats,
addRound,
removeRound,
addQuestion,
handleDragEnd,
handleTypeChange,
removeQuestion,
validate,
reset,
}
}