Files
reader-star/apps/admin/src/views/question-store/modules/QuestionDrawer.vue
T
ranjl 771fb807eb feat(admin): 新增题目类型、优化题目库管理并添加拼音助手
- 新增6种题目类型枚举:逆向接诗句、场景猜诗句、联想对对碰、字词飞花令、诗词常识、换字组成语
- 新增QuestionDrawer组件,支持富文本编辑和拼音转换功能
- 优化CategoryTree组件,增加分类选择本地存储和自动填充题目类型
- 更新question API接口,简化新增和更新题目库的参数传递
- 扩展QuestionRenderer组件,支持新增的题目类型渲染
- 添加多个题目库JSON文件,包含成语、诗词常识、字词飞花令等题目数据
- 调整环境配置,更新开发环境API地址
2026-03-16 17:59:49 +08:00

283 lines
8.8 KiB
Vue

<script setup lang="ts">
import type { FormInst, FormItemRule } from 'naive-ui'
import Clipboard from 'clipboard'
import {
NButton,
NCard,
NDrawer,
NDrawerContent,
NForm,
NFormItem,
NInput,
NInputNumber,
NRadio,
NRadioGroup,
} from 'naive-ui'
import { pinyin } from 'pinyin-pro'
import { computed, onUnmounted, ref, watch } from 'vue'
import OssImageUpload from '@/components/common/oss-image-upload/index.vue'
import WangEditor from '@/components/common/wang-editor.vue'
import { QuestionCategoryEnum, QuestionType } from '@/enum/business'
import { fetchAddQuestionLibrary, fetchUpdateQuestionLibrary } from '@/service/api/question'
const props = defineProps<{
show: boolean
operation: 'add' | 'edit'
questionData?: any
currentCategory: any
}>()
const emit = defineEmits<{
(e: 'update:show', value: boolean): void
(e: 'success'): void
}>()
const showQuestionModal = computed({
get: () => props.show,
set: val => emit('update:show', val),
})
// 拼音转换工具
const pinyinTool = ref({
input: '',
output: '',
})
// 监听输入变化自动转换
watch(() => pinyinTool.value.input, (val) => {
if (val) {
pinyinTool.value.output = pinyin(val)
}
else {
pinyinTool.value.output = ''
}
})
// 复制拼音
const copyBtnRef = ref<any>(null)
let clipboard: Clipboard | null = null
watch(copyBtnRef, (inst) => {
if (clipboard) {
clipboard.destroy()
clipboard = null
}
if (inst) {
const domEl = inst.$el || inst
clipboard = new Clipboard(domEl)
clipboard.on('success', () => {
window?.$message?.success('拼音已复制到剪贴板')
})
clipboard.on('error', () => {
window?.$message?.error('复制失败,请手动复制')
})
}
})
onUnmounted(() => {
clipboard?.destroy()
})
const questionForm = ref({
name: '',
answer: '',
type: QuestionCategoryEnum.ChineseCharacterDictation1 as QuestionCategoryEnum | number,
imageUrl: '',
IsGood: 0,
IsDisabled: false,
KeyWord: '',
KeyWordsIndex: 0,
})
const questionFormRef = ref<FormInst | null>(null)
const rules = computed(() => {
const dynamicRules: any = {
answer: [{ required: true, message: '请输入题目答案', trigger: ['blur'] }],
type: [{ required: true, message: '请选择题目类型', trigger: ['change'] }],
}
// 题目正文内容和图片至少要有一个
if (!questionForm.value.imageUrl) {
dynamicRules.name = [{ required: true, message: '请输入题目正文内容或上传图片', trigger: ['blur'] }]
}
// 只有“联想对对碰”类型才需要关键词
if (questionForm.value.type === QuestionCategoryEnum.AssociationMatching) {
dynamicRules.KeyWord = [{ required: true, message: '请输入关键词', trigger: ['blur'] }]
dynamicRules.KeyWordsIndex = [
{
required: true,
validator: (_rule: FormItemRule, value: number) => {
return typeof value === 'number'
},
message: '请输入关键词索引',
trigger: ['blur', 'change'],
},
]
}
return dynamicRules
})
watch(() => props.show, (val) => {
if (val) {
if (props.operation === 'add') {
questionForm.value = {
name: '',
answer: '',
type: props.currentCategory?.QuestionType || QuestionType.SingleChoice,
imageUrl: '',
IsGood: 0,
IsDisabled: false,
KeyWord: '',
KeyWordsIndex: 0,
}
pinyinTool.value.input = ''
pinyinTool.value.output = ''
}
else if (props.operation === 'edit' && props.questionData) {
const question = props.questionData
questionForm.value = {
name: question.Name,
answer: question.Answer,
type: Number(question.Type),
KeyWord: question.KeyWord || '',
KeyWordsIndex: question.KeyWordsIndex || 0,
imageUrl: question.ImageUrl || '',
IsGood: question.IsPriority || 0,
IsDisabled: !!question.IsDisabled,
}
pinyinTool.value.input = ''
pinyinTool.value.output = ''
}
}
})
async function submitQuestion() {
const valid = await questionFormRef.value?.validate()
if (!valid) {
window?.$message?.error('请填写完整信息')
return
}
const params = {
...questionForm.value,
questionId: props.currentCategory.Id,
// type: String(questionForm.value.type) as Api.Question.QuestionScoreType,
}
if (questionForm.value.type !== QuestionCategoryEnum.AssociationMatching) {
params.KeyWord = ''
params.KeyWordsIndex = 0
}
if (props.operation === 'add') {
const { error } = await fetchAddQuestionLibrary({ ...params, id: 0 })
if (!error) {
window?.$message?.success('题目添加成功')
emit('success')
}
else {
window?.$message?.error('添加失败')
}
}
else if (props.operation === 'edit' && props.questionData) {
const { error } = await fetchUpdateQuestionLibrary({ ...params, id: props.questionData.Id })
if (!error) {
window?.$message?.success('题目修改成功')
emit('success')
}
else {
window?.$message?.error('修改失败')
}
}
}
</script>
<template>
<NDrawer v-model:show="showQuestionModal" :width="800">
<NDrawerContent :title="props.operation === 'edit' ? '编辑题目详情' : '新增题目详情'">
<div class="mb-4">
<NCard size="small" title="🛠️ 拼音助手 (输入汉字获取带音标拼音)" class="bg-gray-50/50">
<div class="flex gap-2">
<NInput v-model:value="pinyinTool.input" placeholder="输入汉字..." class="flex-1" />
<div class="flex flex-1 items-center justify-between border border-gray-200 rounded bg-white px-3 py-1">
<span class="text-gray-600">{{ pinyinTool.output || '拼音结果将显示在这里' }}</span>
<textarea id="pinyinCopyTarget" v-model="pinyinTool.output" class="absolute opacity-0 -z-1" />
<div v-if="pinyinTool.output" ref="copyBtnRef" data-clipboard-target="#pinyinCopyTarget">
<NButton size="tiny" type="primary" secondary>
复制
</NButton>
</div>
</div>
</div>
</NCard>
</div>
<NForm ref="questionFormRef" label-placement="top" :rules="rules" :model="questionForm" size="small">
<NFormItem label="题目正文内容" path="name">
<div
v-if="[QuestionCategoryEnum.PoetryComprehension, QuestionCategoryEnum.PoetryCommonKnowledge, QuestionCategoryEnum.ReversePoemMatching, QuestionCategoryEnum.ChineseCharacterDictation1].includes(props.currentCategory?.QuestionType)"
class="w-full overflow-hidden border border-gray-200 rounded-lg"
>
<WangEditor
v-model="questionForm.name" placeholder="请输入题目内容..." height="300px"
:exclude-keys="['header1', 'fontSize', 'fontFamily', 'lineHeight', 'justifyLeft', 'justifyRight', 'justifyCenter', 'justifyJustify', 'group-image', 'group-video', 'insertTable', 'headerSelect']"
/>
</div>
<NInput v-else v-model:value="questionForm.name" type="textarea" placeholder="请输入题目内容" :rows="3" />
</NFormItem>
<NFormItem label="是否优先使用" path="IsGood">
<NRadioGroup v-model:value="questionForm.IsGood">
<NRadio :value="0">
</NRadio>
<NRadio :value="1">
</NRadio>
</NRadioGroup>
</NFormItem>
<NFormItem label="是否禁用" path="IsDisabled">
<NRadioGroup v-model:value="questionForm.IsDisabled">
<NRadio :value="false">
</NRadio>
<NRadio :value="true">
</NRadio>
</NRadioGroup>
</NFormItem>
<NFormItem label="上传图片">
<OssImageUpload v-model="questionForm.imageUrl" />
</NFormItem>
<template v-if="currentCategory?.QuestionType === QuestionCategoryEnum.AssociationMatching">
<NFormItem label="关键词" path="KeyWord">
<NInput v-model:value="questionForm.KeyWord" placeholder="请输入关键词" />
</NFormItem>
<NFormItem label="关键词索引" path="KeyWordsIndex">
<NInputNumber v-model:value="questionForm.KeyWordsIndex" placeholder="请输入关键词索引" />
</NFormItem>
</template>
<NFormItem label="参考标准答案" path="answer">
<NInput v-model:value="questionForm.answer" placeholder="正确答案" />
</NFormItem>
</NForm>
<template #footer>
<div class="flex justify-end gap-3">
<NButton @click="showQuestionModal = false">
取消并返回
</NButton>
<NButton type="primary" @click="submitQuestion">
保存并入库
</NButton>
</div>
</template>
</NDrawerContent>
</NDrawer>
</template>