feat(admin): 新增活动管理功能并优化字典组件
- 新增活动管理相关功能,包括活动创建、编辑、删除和状态管理 - 新增 PublishStatus 枚举定义活动发布状态 - 优化字典组件,支持字符串和数字类型的字典值 - 重构业务数据存储逻辑,简化字典数据获取流程 - 新增活动详情页面,支持分组管理和队伍配置 - 集成富文本编辑器用于活动规则编辑 - 优化图片上传组件,支持小图上传模式 - 修复模板管理中的区域选择和分页问题 - 更新 TypeScript 类型定义,完善 API 接口 - 调整主题配置,新增活动状态相关颜色
This commit is contained in:
@ -1,230 +1,482 @@
|
||||
<script setup lang="ts">
|
||||
import { NButton, NCard, NForm, NFormItem, NGrid, NGridItem, NInputNumber, NModal, NSelect } from 'naive-ui'
|
||||
import { computed, ref } from 'vue'
|
||||
import { NButton, NCard, NForm, NFormItem, NInput, NInputNumber, NModal, NSelect, NTag, type SelectOption } from 'naive-ui'
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { VueDraggable } from 'vue-draggable-plus'
|
||||
import { PublishStatus } from '@/enum/business'
|
||||
import { useDict } from '@/hooks/business/useDict'
|
||||
import { fetchGetQuestionListAll } from '@/service/api/question'
|
||||
import { fetchTemplateList } from '@/service/api/template'
|
||||
import { useQuestionConfig } from '../../competition-add/modules/useQuestionConfig'
|
||||
|
||||
defineProps<{ isEdit: boolean }>()
|
||||
const props = defineProps<{
|
||||
isEdit: boolean
|
||||
questions: Array<Api.Competition.QuestionListRecord>
|
||||
publishStatus: number
|
||||
}>()
|
||||
|
||||
const scoreOptions = [
|
||||
{ label: '1分', value: '1' },
|
||||
{ label: '2分', value: '2' },
|
||||
{ label: '3分', value: '3' },
|
||||
{ label: '5分', value: '5' },
|
||||
{ label: '10分', value: '10' },
|
||||
]
|
||||
const emit = defineEmits(['enterEdit', 'save', 'cancel'])
|
||||
|
||||
interface Question {
|
||||
id: string
|
||||
label: string
|
||||
type: string
|
||||
score: string
|
||||
time: number | null
|
||||
}
|
||||
|
||||
// 初始化常规赛题目数据
|
||||
const regularQuestions = ref<Question[]>(Array.from({ length: 10 }, (_, i) => ({
|
||||
id: `reg-${i}`,
|
||||
label: `第${i + 1}题`,
|
||||
type: 'hanzi',
|
||||
score: '1',
|
||||
time: 30,
|
||||
})))
|
||||
|
||||
// 初始化PK赛题目数据
|
||||
const pkQuestions = ref<Question[]>(Array.from({ length: 5 }, (_, i) => ({
|
||||
id: `pk-${i}`,
|
||||
label: `PK${i + 1}`,
|
||||
type: 'hanzi',
|
||||
score: '1',
|
||||
time: 30,
|
||||
})))
|
||||
|
||||
const regularTotalScore = computed(() => regularQuestions.value.reduce((acc, cur) => acc + Number(cur.score || 0), 0))
|
||||
const pkTotalScore = computed(() => pkQuestions.value.reduce((acc, cur) => acc + Number(cur.score || 0), 0))
|
||||
|
||||
// 快速配置相关逻辑
|
||||
const showQuickConfigModal = ref(false)
|
||||
const quickConfigForm = ref({
|
||||
score: '1',
|
||||
time: 30,
|
||||
// 本地数据模型,用于适配 useQuestionConfig
|
||||
const modelValue = ref<{
|
||||
rounds: Array<{
|
||||
id: string
|
||||
name: string
|
||||
roundType: string
|
||||
questions: Array<{
|
||||
id: string
|
||||
QuestionID: number
|
||||
QuestionTime: number
|
||||
ActitvityQuestionName: string
|
||||
QuestionRule: number
|
||||
Point: number
|
||||
UIType?: Api.Competition.QuestionTemplateId
|
||||
TemplateID?: number
|
||||
// 额外字段用于回显时保留原始信息
|
||||
ID?: number
|
||||
}>
|
||||
}>
|
||||
}>({
|
||||
rounds: [],
|
||||
})
|
||||
|
||||
function handleBatchReset() {
|
||||
regularQuestions.value.forEach((q) => {
|
||||
q.score = '1'
|
||||
q.time = 30
|
||||
})
|
||||
pkQuestions.value.forEach((q) => {
|
||||
q.score = '1'
|
||||
q.time = 30
|
||||
})
|
||||
const { options: roundTypeOptions } = useDict('round_type')
|
||||
const { options: scoreTypeOptions } = useDict('score_type')
|
||||
const { options: timeOptions } = useDict('time_out')
|
||||
const { options: uiTypeOptions } = useDict('ui_type', 'string')
|
||||
|
||||
// 我们构造一个包含 modelValue 属性的对象,类似于 props
|
||||
const hookProps = reactive({
|
||||
modelValue,
|
||||
})
|
||||
|
||||
const {
|
||||
getRoundTime,
|
||||
totalStats,
|
||||
addRound,
|
||||
removeRound,
|
||||
addQuestion,
|
||||
removeQuestion,
|
||||
handleDragEnd,
|
||||
handleTypeChange,
|
||||
validate,
|
||||
reset,
|
||||
} = useQuestionConfig(hookProps)
|
||||
|
||||
const questionOptions = ref<SelectOption[]>([])
|
||||
const templateIdOptions = ref<SelectOption[]>([])
|
||||
|
||||
// 模态框控制
|
||||
const showAddModal = ref(false)
|
||||
const newRoundForm = ref({
|
||||
count: 10,
|
||||
roundType: roundTypeOptions?.value?.[0]?.value || 0,
|
||||
})
|
||||
|
||||
function handleOpenAddModal() {
|
||||
newRoundForm.value = {
|
||||
count: 10,
|
||||
roundType: roundTypeOptions?.value?.[0]?.value || 0,
|
||||
}
|
||||
showAddModal.value = true
|
||||
}
|
||||
|
||||
function handleQuickConfig() {
|
||||
showQuickConfigModal.value = true
|
||||
function handleConfirmAdd() {
|
||||
addRound(undefined, newRoundForm.value.count, newRoundForm.value.roundType as Api.Competition.CompetitionRoundType)
|
||||
showAddModal.value = false
|
||||
}
|
||||
|
||||
function applyQuickConfig() {
|
||||
const { score, time } = quickConfigForm.value
|
||||
regularQuestions.value.forEach((q) => {
|
||||
q.score = score
|
||||
q.time = time
|
||||
// 数据转换逻辑:Flat List -> Rounds
|
||||
watch(() => props.questions, (val) => {
|
||||
if (!val || val.length === 0) {
|
||||
modelValue.value.rounds = []
|
||||
return
|
||||
}
|
||||
|
||||
const roundsMap = new Map<string, typeof modelValue.value.rounds[0]>()
|
||||
|
||||
val.forEach((q) => {
|
||||
const roundTypeStr = String(q.RoundType)
|
||||
|
||||
if (!roundsMap.has(roundTypeStr)) {
|
||||
roundsMap.set(roundTypeStr, {
|
||||
id: `round_restored_${roundTypeStr}`,
|
||||
name: `${roundTypeOptions.value?.find((opt: any) => opt.value === roundTypeStr)?.label || '未知类型'}`,
|
||||
roundType: roundTypeStr,
|
||||
questions: [],
|
||||
})
|
||||
}
|
||||
|
||||
const round = roundsMap.get(roundTypeStr)
|
||||
if (round) {
|
||||
round.questions.push({
|
||||
id: `q_restored_${q.ID}`,
|
||||
ID: q.ID,
|
||||
QuestionID: q.QuestionID,
|
||||
ActitvityQuestionName: q.ActitvityQuestionName || `第${round.questions.length + 1}题`,
|
||||
QuestionTime: q.QuestionTime,
|
||||
Point: q.Point,
|
||||
QuestionRule: q.QuestionRule,
|
||||
UIType: q.UIType,
|
||||
TemplateID: q.TemplateID,
|
||||
})
|
||||
}
|
||||
})
|
||||
pkQuestions.value.forEach((q) => {
|
||||
q.score = score
|
||||
q.time = time
|
||||
})
|
||||
showQuickConfigModal.value = false
|
||||
|
||||
modelValue.value.rounds = Array.from(roundsMap.values())
|
||||
}, { immediate: true, deep: true })
|
||||
|
||||
const isDataLoaded = ref(false)
|
||||
|
||||
/** 获取所有题目 */
|
||||
async function getAllQuestions() {
|
||||
const { data: res, error } = await fetchGetQuestionListAll()
|
||||
if (error) {
|
||||
window.$message?.error(error.message)
|
||||
return
|
||||
}
|
||||
const { data } = res || []
|
||||
questionOptions.value = data?.map((item: any) => ({
|
||||
label: `${item.Name}-${item.QuestionContent}`,
|
||||
value: item.Id, // Ensure value is number if QuestionID is number
|
||||
})) || []
|
||||
}
|
||||
|
||||
/** 获取所有模板 */
|
||||
async function getAllTemplates() {
|
||||
const { data: res, error } = await fetchTemplateList()
|
||||
|
||||
if (error) {
|
||||
window.$message?.error(error.message)
|
||||
return
|
||||
}
|
||||
const { data } = res || []
|
||||
templateIdOptions.value = Array.isArray(data)
|
||||
? data.map((item: any) => ({
|
||||
label: item.Name,
|
||||
value: item.ID || item.id,
|
||||
})) || []
|
||||
: []
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!isDataLoaded.value) {
|
||||
getAllQuestions()
|
||||
getAllTemplates()
|
||||
isDataLoaded.value = true
|
||||
}
|
||||
})
|
||||
|
||||
function getRoundTypeName(roundType: string) {
|
||||
const option = roundTypeOptions.value?.find((item: any) => item.value === roundType)
|
||||
return option?.label || '未知类型'
|
||||
}
|
||||
|
||||
/** 是否显示编辑按钮 */
|
||||
const showEnterEditButton = computed(() => {
|
||||
return !props.isEdit && [PublishStatus.Unpublished, PublishStatus.Published].includes(props.publishStatus)
|
||||
})
|
||||
|
||||
/** 是否显示保存/取消按钮 */
|
||||
const showEditActions = computed(() => props.isEdit)
|
||||
|
||||
defineExpose({ validate, reset, modelValue })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NCard :bordered="false" class="rounded-xl shadow-sm">
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<div class="flex items-center gap-3 border-l-4 border-purple-500 pl-3">
|
||||
<span class="text-lg text-gray-800 font-bold">题包配置</span>
|
||||
</div>
|
||||
<!-- 编辑模式下显示批量操作按钮 -->
|
||||
<div v-if="isEdit" class="flex gap-3">
|
||||
<NButton size="small" secondary @click="handleBatchReset">
|
||||
批量重置
|
||||
</NButton>
|
||||
<NButton size="small" type="primary" secondary @click="handleQuickConfig">
|
||||
快速配置
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-6">
|
||||
<NCard :bordered="false" class="rounded-xl shadow-sm">
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="flex items-center gap-3 border-l-4 border-blue-600 pl-3">
|
||||
<span class="text-lg text-gray-800 font-bold">赛程题包配置</span>
|
||||
</div>
|
||||
<span class="pl-4 text-xs text-gray-400">您可以为比赛添加多个阶段的题包,并自定义题目、分值与限时。</span>
|
||||
</div>
|
||||
|
||||
<NGrid :x-gap="24" :cols="2">
|
||||
<!-- 常规赛 -->
|
||||
<NGridItem>
|
||||
<div class="flex items-center justify-between border-b border-blue-100 rounded-t-lg bg-blue-50/50 px-4 py-3">
|
||||
<div class="flex items-center gap-2 text-gray-800 font-bold">
|
||||
<div class="i-carbon-document-tasks text-blue-500" />
|
||||
常规赛题包
|
||||
</div>
|
||||
<div class="text-xs text-[#8DA5C3] font-medium">
|
||||
共 {{ regularQuestions.length }} 题 / 总计 {{ regularTotalScore }} 分
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-3 border border-t-0 border-blue-100 rounded-b-lg bg-[#F5F9FF] p-4">
|
||||
<div v-for="item in regularQuestions" :key="item.id" class="flex items-center gap-2">
|
||||
<div class="w-16 flex-shrink-0 text-xs text-[#8DA5C3] font-bold">
|
||||
{{ item.label }}:
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
v-if="totalStats.count > 0"
|
||||
class="flex items-center gap-3 border border-gray-100 rounded-lg bg-gray-50 px-3 py-1 text-xs font-bold"
|
||||
>
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="text-gray-400">总题数</span>
|
||||
<span class="text-sm text-gray-700">{{ totalStats.count }}</span>
|
||||
</div>
|
||||
<NSelect
|
||||
v-model:value="item.type"
|
||||
size="small"
|
||||
placeholder="题型"
|
||||
:options="[{ label: '汉字书写', value: 'hanzi' }]"
|
||||
:disabled="!isEdit"
|
||||
class="min-w-[120px] flex-1"
|
||||
/>
|
||||
<div class="w-20 flex-shrink-0">
|
||||
<NSelect
|
||||
v-model:value="item.score"
|
||||
size="small"
|
||||
:options="scoreOptions"
|
||||
:disabled="!isEdit"
|
||||
placeholder="分值"
|
||||
/>
|
||||
</div>
|
||||
<div class="w-20 flex-shrink-0">
|
||||
<NInputNumber
|
||||
v-model:value="item.time"
|
||||
size="small"
|
||||
placeholder="秒"
|
||||
:show-button="false"
|
||||
:disabled="!isEdit"
|
||||
>
|
||||
<template #suffix>
|
||||
s
|
||||
</template>
|
||||
</NInputNumber>
|
||||
<div class="h-3 w-[1px] bg-gray-200" />
|
||||
<div class="h-3 w-[1px] bg-gray-200" />
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="text-gray-400">总时长</span>
|
||||
<span class="text-sm text-blue-500">{{ totalStats.time }}s</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</NGridItem>
|
||||
|
||||
<!-- PK赛 -->
|
||||
<NGridItem>
|
||||
<div class="flex items-center justify-between border-b border-orange-100 rounded-t-lg bg-orange-50/50 px-4 py-3">
|
||||
<div class="flex items-center gap-2 text-[#5E3218] font-bold">
|
||||
<div class="i-carbon-timer text-[#D96B23]" />
|
||||
加时赛题包
|
||||
</div>
|
||||
<div class="text-xs text-[#D96B23]/60 font-medium">
|
||||
共 {{ pkQuestions.length }} 题 / 总计 {{ pkTotalScore }} 分
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-3 border border-t-0 border-orange-100 rounded-b-lg bg-[#FFF9F5] p-4">
|
||||
<div v-for="item in pkQuestions" :key="item.id" class="flex items-center gap-2">
|
||||
<div class="w-16 flex-shrink-0 text-xs text-[#FF8C38] font-bold">
|
||||
{{ item.label }}:
|
||||
</div>
|
||||
<NSelect
|
||||
v-model:value="item.type"
|
||||
size="small"
|
||||
placeholder="题型"
|
||||
:options="[{ label: '汉字书写', value: 'hanzi' }]"
|
||||
:disabled="!isEdit"
|
||||
class="min-w-[120px] flex-1"
|
||||
/>
|
||||
<div class="w-20 flex-shrink-0">
|
||||
<NSelect
|
||||
v-model:value="item.score"
|
||||
size="small"
|
||||
:options="scoreOptions"
|
||||
:disabled="!isEdit"
|
||||
placeholder="分值"
|
||||
/>
|
||||
</div>
|
||||
<div class="w-20 flex-shrink-0">
|
||||
<NInputNumber
|
||||
v-model:value="item.time"
|
||||
size="small"
|
||||
placeholder="秒"
|
||||
:show-button="false"
|
||||
:disabled="!isEdit"
|
||||
>
|
||||
<template #suffix>
|
||||
s
|
||||
</template>
|
||||
</NInputNumber>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</NGridItem>
|
||||
</NGrid>
|
||||
|
||||
<!-- 快速配置弹窗 -->
|
||||
<NModal
|
||||
v-model:show="showQuickConfigModal"
|
||||
preset="card"
|
||||
title="快速配置"
|
||||
class="w-[400px]"
|
||||
>
|
||||
<NForm :model="quickConfigForm" label-placement="left" label-width="80">
|
||||
<NFormItem label="统一分值">
|
||||
<NSelect v-model:value="quickConfigForm.score" :options="scoreOptions" />
|
||||
</NFormItem>
|
||||
<NFormItem label="统一时间">
|
||||
<NInputNumber v-model:value="quickConfigForm.time" :show-button="false">
|
||||
<template #suffix>
|
||||
秒
|
||||
<div class="flex gap-2">
|
||||
<NButton v-if="showEnterEditButton" size="small" type="primary" secondary @click="emit('enterEdit')">
|
||||
编辑
|
||||
</NButton>
|
||||
<template v-else-if="showEditActions">
|
||||
<NButton size="small" secondary @click="emit('cancel')">
|
||||
取消
|
||||
</NButton>
|
||||
<NButton size="small" type="primary" @click="emit('save')">
|
||||
保存
|
||||
</NButton>
|
||||
</template>
|
||||
</NInputNumber>
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-3">
|
||||
<NButton @click="showQuickConfigModal = false">
|
||||
取消
|
||||
</NButton>
|
||||
<NButton type="primary" @click="applyQuickConfig">
|
||||
应用
|
||||
</div>
|
||||
|
||||
<NButton
|
||||
v-if="isEdit"
|
||||
type="primary" secondary size="medium" class="!font-bold"
|
||||
@click="handleOpenAddModal"
|
||||
>
|
||||
<template #icon>
|
||||
<icon-ic-round-plus />
|
||||
</template>
|
||||
新增赛程题包
|
||||
</NButton>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div
|
||||
v-if="!modelValue.rounds?.length"
|
||||
class="flex flex-col flex-1 items-center justify-center border-2 border-gray-100 rounded-2xl border-dashed bg-gray-50/30 py-12"
|
||||
>
|
||||
<div class="h-34 w-34 flex items-center justify-center rounded-3xl bg-gray-100 text-gray-300">
|
||||
<icon-ic-outline-folder-off class="text-5xl" />
|
||||
</div>
|
||||
<div class="mt-6 text-lg text-gray-400 font-bold">
|
||||
暂无配置题包
|
||||
</div>
|
||||
<div class="mt-2 text-sm text-gray-500">
|
||||
当前暂无配置
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 列表内容 -->
|
||||
<div v-else class="flex flex-col gap-6 pb-4">
|
||||
<div
|
||||
v-for="(round, roundIndex) in modelValue.rounds" :key="round.id"
|
||||
class="group relative overflow-hidden border border-gray-100 rounded-2xl bg-white p-1 shadow-sm transition-all hover:shadow-md"
|
||||
>
|
||||
<!-- 侧边装饰条 -->
|
||||
<div class="absolute bottom-0 left-0 top-0 w-1.5 bg-blue-500" />
|
||||
|
||||
<div class="p-6 pl-8">
|
||||
<!-- 头部信息 -->
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="h-8 w-1.5 rounded-full bg-blue-500" />
|
||||
<NTag type="primary" class="!text-blue-500">
|
||||
{{ getRoundTypeName(round.roundType) }}
|
||||
</NTag>
|
||||
<div class="flex items-center gap-3">
|
||||
<NInput
|
||||
v-model:value="round.name" placeholder="请输入环节名称" :bordered="false"
|
||||
:disabled="!isEdit"
|
||||
class="border transition-all !w-80 !border-transparent !rounded-lg !bg-transparent !text-xl !font-bold focus-within:shadow-sm focus-within:!border-blue-500 hover:!border-gray-200 focus-within:!bg-white"
|
||||
/>
|
||||
<div class="flex items-center gap-2">
|
||||
<div
|
||||
class="flex items-center gap-1 rounded-full bg-gray-100 px-3 py-1 text-xs text-gray-500 font-bold"
|
||||
>
|
||||
<icon-ic-outline-format-list-numbered class="text-sm" />
|
||||
{{ round.questions.length }} 题
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center gap-1 border border-blue-100 rounded-full bg-blue-50 px-3 py-1 text-xs text-blue-500 font-bold"
|
||||
>
|
||||
<icon-ic-outline-access-time class="text-sm" />
|
||||
{{ getRoundTime(roundIndex) }} 秒
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="isEdit" class="flex items-center gap-3">
|
||||
<NButton
|
||||
secondary circle class="!text-gray-400 hover:!bg-blue-50 hover:!text-blue-500"
|
||||
@click="addQuestion(roundIndex)"
|
||||
>
|
||||
<template #icon>
|
||||
<icon-ic-round-plus class="!text-xl" />
|
||||
</template>
|
||||
</NButton>
|
||||
<NButton
|
||||
secondary circle class="!text-gray-400 hover:!bg-red-50 hover:!text-red-500"
|
||||
@click="removeRound(roundIndex)"
|
||||
>
|
||||
<template #icon>
|
||||
<icon-ic-outline-delete class="!text-xl" />
|
||||
</template>
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 题目列表 -->
|
||||
<VueDraggable
|
||||
v-model="round.questions" :animation="300" handle=".drag-handle" class="flex flex-col gap-3"
|
||||
ghost-class="ghost"
|
||||
:disabled="!isEdit"
|
||||
@end="(evt) => handleDragEnd(roundIndex, evt)"
|
||||
>
|
||||
<div
|
||||
v-for="(item, qIdx) in round.questions" :key="item.id"
|
||||
class="group/item flex flex-col gap-3 border border-gray-100 rounded-xl bg-white p-3 transition-all hover:border-blue-200 hover:shadow-sm"
|
||||
>
|
||||
<!-- 第一行:序号、标题 -->
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="flex flex-1 items-center gap-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<div
|
||||
v-if="isEdit"
|
||||
class="drag-handle flex cursor-grab items-center justify-center rounded p-1 text-gray-400 transition-colors active:cursor-grabbing hover:bg-gray-100 hover:text-blue-500"
|
||||
>
|
||||
<icon-mdi-drag class="text-xl" />
|
||||
</div>
|
||||
<span
|
||||
class="w-6 flex-shrink-0 text-center text-sm text-gray-300 font-bold transition-colors group-hover/item:text-blue-500"
|
||||
>
|
||||
<!-- {{ String(qIdx + 1).padStart(2, '0') }} -->
|
||||
<NTag type="primary" class="!text-blue-500">{{ item.QuestionID }}</NTag>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 标题 -->
|
||||
<div class="flex flex-1 items-center gap-2 rounded-lg bg-gray-50 px-3 py-1.5">
|
||||
<icon-ic-outline-title class="text-gray-400" />
|
||||
<span class="whitespace-nowrap text-sm text-gray-400 font-bold">标题:</span>
|
||||
<NInput
|
||||
v-model:value="item.ActitvityQuestionName" class="flex-1 !bg-transparent" size="small" :bordered="false"
|
||||
placeholder="请输入题目标题"
|
||||
:disabled="!isEdit"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 删除按钮 -->
|
||||
<NButton
|
||||
v-if="isEdit"
|
||||
text class="transition-opacity !text-gray-300 hover:!text-red-500"
|
||||
@click="removeQuestion(roundIndex, qIdx)"
|
||||
>
|
||||
<icon-ic-outline-delete class="!text-xl" />
|
||||
</NButton>
|
||||
</div>
|
||||
|
||||
<!-- 第二行:配置项 -->
|
||||
<div class="grid grid-cols-12 gap-3">
|
||||
<!-- 题型 -->
|
||||
<div class="col-span-4">
|
||||
<NSelect
|
||||
v-model:value="item.QuestionID" :options="questionOptions" placeholder="请选择题目类型" size="small"
|
||||
class="font-medium"
|
||||
:disabled="!isEdit"
|
||||
@update:value="() => handleTypeChange(roundIndex)"
|
||||
/>
|
||||
</div>
|
||||
<!-- UItype -->
|
||||
<div class="col-span-4">
|
||||
<NSelect
|
||||
v-model:value="item.UIType" :options="uiTypeOptions" placeholder="请选择UI类型" size="small"
|
||||
class="font-medium"
|
||||
:disabled="!isEdit"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- templateId -->
|
||||
<div class="col-span-4">
|
||||
<NSelect
|
||||
v-model:value="item.TemplateID" :options="templateIdOptions" placeholder="请选择模板" size="small"
|
||||
class="font-medium"
|
||||
:disabled="!isEdit"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 时间 -->
|
||||
<div class="col-span-4 flex items-center gap-2 rounded-lg bg-gray-50 px-3 py-1.5">
|
||||
<icon-ic-outline-timer class="text-gray-400" />
|
||||
<span class="whitespace-nowrap text-sm text-gray-400 font-bold">答题时间:</span>
|
||||
<NSelect
|
||||
v-model:value="item.QuestionTime" :options="timeOptions" class="flex-1 !bg-transparent" size="small"
|
||||
:bordered="false"
|
||||
:disabled="!isEdit"
|
||||
/>
|
||||
<span class="text-xs text-gray-400 font-bold">S</span>
|
||||
</div>
|
||||
|
||||
<!-- 分数规则 -->
|
||||
<div class="col-span-4 flex items-center gap-2 rounded-lg bg-gray-50 px-3 py-1.5">
|
||||
<icon-streamline-sharp:type-area-remix class="text-gray-400" />
|
||||
<span class="whitespace-nowrap text-sm text-gray-400 font-bold">分数规则:</span>
|
||||
<NSelect
|
||||
v-model:value="item.QuestionRule" :options="scoreTypeOptions as unknown as SelectOption[]"
|
||||
class="flex-1 !bg-transparent" size="small" :bordered="false"
|
||||
:disabled="!isEdit"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 分数 -->
|
||||
<div class="col-span-4 flex items-center gap-2 rounded-lg bg-gray-50 px-3 py-1.5">
|
||||
<icon-ic-round-star-border class="text-gray-400" />
|
||||
<span class="whitespace-nowrap text-sm text-gray-400 font-bold">分数:</span>
|
||||
<NInputNumber
|
||||
v-model:value="item.Point" :min="0" :show-button="false" class="flex-1 !bg-transparent"
|
||||
size="small" :bordered="false" placeholder="0"
|
||||
:disabled="!isEdit"
|
||||
/>
|
||||
<span class="text-xs text-gray-400 font-bold">分</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</VueDraggable>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</NCard>
|
||||
|
||||
<!-- 新增题包弹窗 -->
|
||||
<NModal v-model:show="showAddModal" transform-origin="center">
|
||||
<div class="w-[480px] rounded-2xl bg-white p-8 shadow-2xl">
|
||||
<div class="mb-8 flex flex-col items-center gap-4 text-center">
|
||||
<div class="h-14 w-14 flex items-center justify-center rounded-2xl bg-blue-50 text-blue-500">
|
||||
<icon-ic-round-library-add class="text-3xl" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-xl text-gray-800 font-bold">
|
||||
新建环节题包
|
||||
</div>
|
||||
<div class="mt-1 text-sm text-gray-500">
|
||||
请输入环节名称与题目预设数量
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<NForm size="large">
|
||||
<NFormItem label="环节/题包类型">
|
||||
<NSelect
|
||||
v-model:value="newRoundForm.roundType" :options="roundTypeOptions as unknown as SelectOption[]"
|
||||
placeholder="请选择环节类型..." :bordered="true" class="font-medium"
|
||||
/>
|
||||
</NFormItem>
|
||||
<NFormItem label="初始化题目数量">
|
||||
<NInputNumber v-model:value="newRoundForm.count" :min="1" :max="50" class="w-full" />
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
|
||||
<NButton
|
||||
type="primary" block size="large" class="mt-4 !h-12 !rounded-xl !text-lg !font-bold"
|
||||
@click="handleConfirmAdd"
|
||||
>
|
||||
生成题包卡片
|
||||
</NButton>
|
||||
</div>
|
||||
</NModal>
|
||||
</NCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ghost {
|
||||
@apply opacity-50 bg-blue-50 border-2 border-dashed border-blue-300;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user