Files
reader-star/apps/admin/src/views/competition/competition-add/modules/useQuestionConfig.ts
rjl f1ff053b68 fix(admin): 移除题目标题必填验证并优化诗词理解题目展示
移除题目配置中的标题输入框及相关必填验证,使标题字段变为可选
诗词理解题目类型使用富文本编辑器,其他类型使用普通文本域
优化用户端汉字听写和词语听写题目的米字格展示样式
统一图片展示样式,限制最大宽度为50%并居中显示
2026-02-13 08:34:09 +08:00

290 lines
9.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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,
}
}