feat:添加添加试题功能页面;添加试题管理的分类对应功能;删除一些不再使用的组件页面;其他样式优化
This commit is contained in:
parent
cc7c4ec23a
commit
638e939fe5
394
src/components/teacher/CompositeQuestion.vue
Normal file
394
src/components/teacher/CompositeQuestion.vue
Normal file
@ -0,0 +1,394 @@
|
||||
<template>
|
||||
<div class="composite-question-container">
|
||||
<!-- 题目内容 -->
|
||||
<div class="form-section">
|
||||
<n-card size="small">
|
||||
<div class="section-header">
|
||||
<h3 class="section-title required">题目内容</h3>
|
||||
</div>
|
||||
<n-form-item label="" required>
|
||||
<n-input
|
||||
:value="title"
|
||||
@update:value="(value: string) => $emit('update:title', value)"
|
||||
type="textarea"
|
||||
placeholder="请输入题目内容或阅读材料..."
|
||||
:rows="6"
|
||||
show-count
|
||||
maxlength="2000"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-card>
|
||||
</div>
|
||||
|
||||
<!-- 小题目列表 -->
|
||||
<div class="form-section">
|
||||
<n-card size="small">
|
||||
<div class="section-header">
|
||||
<h3 class="section-title required">小题目</h3>
|
||||
<n-button type="primary" size="small" @click="addSubQuestion">
|
||||
添加小题
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<!-- 如果没有小题,显示提示 -->
|
||||
<div v-if="subQuestions.length === 0" class="empty-state">
|
||||
<n-text depth="3">请至少添加一道小题</n-text>
|
||||
</div>
|
||||
|
||||
<!-- 小题目列表 -->
|
||||
<div v-else class="sub-questions-list">
|
||||
<div v-for="(subQuestion, index) in subQuestions" :key="subQuestion.id" class="sub-question-item">
|
||||
<div class="sub-question-header">
|
||||
<div class="sub-question-title">
|
||||
<span class="sub-question-number">第{{ index + 1 }}题</span>
|
||||
<div class="score-input">
|
||||
<span style="margin-right: 8px;">选择题型:</span>
|
||||
<n-select
|
||||
:value="subQuestion.type"
|
||||
@update:value="(value: string) => updateSubQuestionType(index, value)"
|
||||
:options="questionTypeOptions"
|
||||
placeholder="选择题型"
|
||||
style="width: 150px; margin-left: 12px;"
|
||||
/>
|
||||
</div>
|
||||
<div class="score-input">
|
||||
<span style="margin-right: 8px;">分值:</span>
|
||||
<n-input-number
|
||||
:value="subQuestion.score"
|
||||
@update:value="(value: number | null) => updateSubQuestionScore(index, value)"
|
||||
:min="0.5"
|
||||
:max="100"
|
||||
:step="0.5"
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<n-button
|
||||
type="error"
|
||||
size="small"
|
||||
text
|
||||
@click="removeSubQuestion(index)"
|
||||
:disabled="subQuestions.length === 1"
|
||||
>
|
||||
删除
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<!-- 小题目内容 -->
|
||||
<div class="sub-question-content">
|
||||
<component
|
||||
:is="getComponentName(subQuestion.type)"
|
||||
v-if="subQuestion.type"
|
||||
:id="subQuestion.id"
|
||||
:title="subQuestion.title"
|
||||
@update:title="(value: string) => updateSubQuestionTitle(index, value)"
|
||||
:model-value="subQuestion.data"
|
||||
@update:model-value="(value: any) => updateSubQuestionData(index, value)"
|
||||
:correct-answer="subQuestion.correctAnswer"
|
||||
@update:correct-answer="(value: any) => updateSubQuestionCorrectAnswer(index, value)"
|
||||
:correct-answers="subQuestion.correctAnswers"
|
||||
@update:correct-answers="(value: any) => updateSubQuestionCorrectAnswers(index, value)"
|
||||
:answer="subQuestion.answer"
|
||||
@update:answer="(value: any) => updateSubQuestionAnswer(index, value)"
|
||||
:answers="subQuestion.answers"
|
||||
@update:answers="(value: any) => updateSubQuestionAnswers(index, value)"
|
||||
:explanation="subQuestion.explanation"
|
||||
@update:explanation="(value: string) => updateSubQuestionExplanation(index, value)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<n-button type="primary" size="small" @click="addSubQuestion" style="margin-top: 10px;">
|
||||
添加小题
|
||||
</n-button>
|
||||
</n-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { NCard, NFormItem, NInput, NButton, NText, NSelect, NInputNumber } from 'naive-ui'
|
||||
import SingleChoiceQuestion from './SingleChoiceQuestion.vue'
|
||||
import MultipleChoiceQuestion from './MultipleChoiceQuestion.vue'
|
||||
import TrueFalseQuestion from './TrueFalseQuestion.vue'
|
||||
import FillBlankQuestion from './FillBlankQuestion.vue'
|
||||
import ShortAnswerQuestion from './ShortAnswerQuestion.vue'
|
||||
|
||||
interface SubQuestion {
|
||||
id: number
|
||||
type: string
|
||||
title: string
|
||||
score: number
|
||||
data: any
|
||||
correctAnswer?: any
|
||||
correctAnswers?: any
|
||||
answer?: any
|
||||
answers?: any
|
||||
explanation: string
|
||||
}
|
||||
|
||||
interface CompositeQuestionData {
|
||||
subQuestions: SubQuestion[]
|
||||
}
|
||||
|
||||
interface Props {
|
||||
title: string
|
||||
modelValue: CompositeQuestionData
|
||||
explanation: string
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:title', value: string): void
|
||||
(e: 'update:modelValue', value: CompositeQuestionData): void
|
||||
(e: 'update:explanation', value: string): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
// 题型选项
|
||||
const questionTypeOptions = [
|
||||
{ label: '单选题', value: 'single' },
|
||||
{ label: '多选题', value: 'multiple' },
|
||||
{ label: '判断题', value: 'truefalse' },
|
||||
{ label: '填空题', value: 'fillblank' },
|
||||
{ label: '简答题', value: 'shortanswer' }
|
||||
]
|
||||
|
||||
// 小题目列表
|
||||
const subQuestions = ref<SubQuestion[]>(props.modelValue?.subQuestions || [])
|
||||
|
||||
// 下一个ID
|
||||
let nextId = 1
|
||||
|
||||
// 监听props变化
|
||||
watch(() => props.modelValue, (newValue) => {
|
||||
if (newValue?.subQuestions) {
|
||||
subQuestions.value = newValue.subQuestions
|
||||
}
|
||||
}, { deep: true })
|
||||
|
||||
// 发射更新事件
|
||||
const emitUpdate = () => {
|
||||
emit('update:modelValue', {
|
||||
subQuestions: subQuestions.value
|
||||
})
|
||||
}
|
||||
|
||||
// 获取组件名称
|
||||
const getComponentName = (type: string) => {
|
||||
const componentMap: Record<string, any> = {
|
||||
single: SingleChoiceQuestion,
|
||||
multiple: MultipleChoiceQuestion,
|
||||
truefalse: TrueFalseQuestion,
|
||||
fillblank: FillBlankQuestion,
|
||||
shortanswer: ShortAnswerQuestion
|
||||
}
|
||||
return componentMap[type]
|
||||
}
|
||||
|
||||
// 添加小题
|
||||
const addSubQuestion = () => {
|
||||
const newSubQuestion: SubQuestion = {
|
||||
id: nextId++,
|
||||
type: '',
|
||||
title: '',
|
||||
score: 5,
|
||||
data: null,
|
||||
explanation: ''
|
||||
}
|
||||
subQuestions.value.push(newSubQuestion)
|
||||
emitUpdate()
|
||||
}
|
||||
|
||||
// 删除小题
|
||||
const removeSubQuestion = (index: number) => {
|
||||
subQuestions.value.splice(index, 1)
|
||||
emitUpdate()
|
||||
}
|
||||
|
||||
// 更新小题类型
|
||||
const updateSubQuestionType = (index: number, type: string) => {
|
||||
const subQuestion = subQuestions.value[index]
|
||||
subQuestion.type = type
|
||||
|
||||
// 根据题型初始化数据
|
||||
switch (type) {
|
||||
case 'single':
|
||||
subQuestion.data = [{ option: '', id: 1 }, { option: '', id: 2 }]
|
||||
subQuestion.correctAnswer = ''
|
||||
break
|
||||
case 'multiple':
|
||||
subQuestion.data = [{ option: '', id: 1 }, { option: '', id: 2 }]
|
||||
subQuestion.correctAnswers = []
|
||||
break
|
||||
case 'truefalse':
|
||||
subQuestion.answer = null
|
||||
break
|
||||
case 'fillblank':
|
||||
subQuestion.answers = [{ value: '', score: 1, caseSensitive: false }]
|
||||
break
|
||||
case 'shortanswer':
|
||||
subQuestion.data = ''
|
||||
break
|
||||
}
|
||||
|
||||
emitUpdate()
|
||||
}
|
||||
|
||||
// 更新小题分值
|
||||
const updateSubQuestionScore = (index: number, score: number | null) => {
|
||||
if (score !== null) {
|
||||
subQuestions.value[index].score = score
|
||||
emitUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
// 更新小题标题
|
||||
const updateSubQuestionTitle = (index: number, title: string) => {
|
||||
subQuestions.value[index].title = title
|
||||
emitUpdate()
|
||||
}
|
||||
|
||||
// 更新小题数据
|
||||
const updateSubQuestionData = (index: number, data: any) => {
|
||||
subQuestions.value[index].data = data
|
||||
emitUpdate()
|
||||
}
|
||||
|
||||
// 更新小题正确答案
|
||||
const updateSubQuestionCorrectAnswer = (index: number, correctAnswer: any) => {
|
||||
subQuestions.value[index].correctAnswer = correctAnswer
|
||||
emitUpdate()
|
||||
}
|
||||
|
||||
// 更新小题多选答案
|
||||
const updateSubQuestionCorrectAnswers = (index: number, correctAnswers: any) => {
|
||||
subQuestions.value[index].correctAnswers = correctAnswers
|
||||
emitUpdate()
|
||||
}
|
||||
|
||||
// 更新小题答案
|
||||
const updateSubQuestionAnswer = (index: number, answer: any) => {
|
||||
subQuestions.value[index].answer = answer
|
||||
emitUpdate()
|
||||
}
|
||||
|
||||
// 更新小题答案组
|
||||
const updateSubQuestionAnswers = (index: number, answers: any) => {
|
||||
subQuestions.value[index].answers = answers
|
||||
emitUpdate()
|
||||
}
|
||||
|
||||
// 更新小题解析
|
||||
const updateSubQuestionExplanation = (index: number, explanation: string) => {
|
||||
subQuestions.value[index].explanation = explanation
|
||||
emitUpdate()
|
||||
}
|
||||
|
||||
// 初始化:如果没有小题,添加一个
|
||||
if (subQuestions.value.length === 0) {
|
||||
addSubQuestion()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.composite-question-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.section-title.required::after {
|
||||
content: ' *';
|
||||
color: #d03050;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
background: #fafafa;
|
||||
border-radius: 6px;
|
||||
border: 1px dashed #d9d9d9;
|
||||
}
|
||||
|
||||
.sub-questions-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.sub-question-item {
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.sub-question-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
}
|
||||
|
||||
.sub-question-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.sub-question-number {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.score-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: auto;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.sub-question-content {
|
||||
background: white;
|
||||
border-radius: 6px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
/* 覆盖子组件的样式,避免嵌套太深 */
|
||||
.sub-question-content :deep(.form-section) {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.sub-question-content :deep(.section-header) {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.sub-question-content :deep(.section-title) {
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
280
src/components/teacher/FillBlankQuestion.vue
Normal file
280
src/components/teacher/FillBlankQuestion.vue
Normal file
@ -0,0 +1,280 @@
|
||||
<template>
|
||||
<div class="fill-blank-container">
|
||||
<!-- 题目内容 -->
|
||||
<div class="form-section">
|
||||
<n-card size="small">
|
||||
<div class="section-header">
|
||||
<h3 class="section-title required">题目内容</h3>
|
||||
</div>
|
||||
<n-form-item label="" required>
|
||||
<n-input
|
||||
:value="title"
|
||||
@update:value="(value: string) => $emit('update:title', value)"
|
||||
type="textarea"
|
||||
placeholder="请输入题目内容..."
|
||||
:rows="4"
|
||||
show-count
|
||||
maxlength="500"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-card>
|
||||
</div>
|
||||
|
||||
<!-- 填空答案 -->
|
||||
<n-card size="small">
|
||||
<div class="section-header">
|
||||
<h3 class="section-title required">填空答案</h3>
|
||||
</div>
|
||||
<div class="answers-container">
|
||||
<div v-for="(answer, index) in answers" :key="index" class="answer-item">
|
||||
<div class="answer-header">
|
||||
<span class="answer-number">{{ index + 1 }}.</span>
|
||||
<span class="answer-label">请输入答案</span>
|
||||
</div>
|
||||
<div class="answer-content">
|
||||
<n-input :value="answer.content"
|
||||
@update:value="(value: string) => handleUpdateAnswer(index, 'content', value)"
|
||||
placeholder="请输入答案内容" show-count maxlength="100" class="answer-input" />
|
||||
<div class="answer-settings">
|
||||
<div class="score-setting">
|
||||
<span class="setting-label">得分:</span>
|
||||
<n-input type="number" :value="answer.score"
|
||||
@update:value="(value: number | null) => handleUpdateAnswer(index, 'score', value || 0)"
|
||||
:min="0" :max="100" :step="0.5" :precision="1" size="small" />
|
||||
</div>
|
||||
<div class="case-setting">
|
||||
<span class="setting-label">区分大小写:</span>
|
||||
<n-select :value="answer.caseSensitive ? '区分' : '不区分'"
|
||||
@update:value="(value: string) => handleUpdateAnswer(index, 'caseSensitive', value === '区分')"
|
||||
:options="caseOptions" size="small" class="case-select" />
|
||||
</div>
|
||||
<n-button v-if="answers.length > 1" @click="handleRemoveAnswer(index)" type="error" ghost
|
||||
size="small" class="delete-answer-btn">
|
||||
删除
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<n-button v-if="answers.length < 10" @click="handleAddAnswer" dashed block class="add-answer-btn">
|
||||
+ 添加填空
|
||||
</n-button>
|
||||
</div>
|
||||
</n-card>
|
||||
|
||||
<!-- 答案解析 -->
|
||||
<div class="form-section">
|
||||
<n-card size="small">
|
||||
<div class="section-header">
|
||||
<h3 class="section-title">答案解析</h3>
|
||||
</div>
|
||||
<n-form-item label="">
|
||||
<n-input
|
||||
:value="explanation"
|
||||
@update:value="(value: string) => $emit('update:explanation', value)"
|
||||
type="textarea"
|
||||
placeholder="请输入答案解析..."
|
||||
:rows="3"
|
||||
show-count
|
||||
maxlength="300"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineProps, defineEmits } from 'vue';
|
||||
|
||||
interface FillBlankAnswer {
|
||||
content: string;
|
||||
score: number;
|
||||
caseSensitive: boolean;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
answers: FillBlankAnswer[];
|
||||
title: string;
|
||||
explanation: string;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:answers', value: FillBlankAnswer[]): void;
|
||||
(e: 'update:title', value: string): void;
|
||||
(e: 'update:explanation', value: string): void;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
// 大小写选项
|
||||
const caseOptions = [
|
||||
{ label: '不区分', value: '不区分' },
|
||||
{ label: '区分', value: '区分' }
|
||||
];
|
||||
|
||||
// 添加填空
|
||||
const handleAddAnswer = () => {
|
||||
const newAnswers = [...props.answers, {
|
||||
content: '',
|
||||
score: 1,
|
||||
caseSensitive: false
|
||||
}];
|
||||
emit('update:answers', newAnswers);
|
||||
};
|
||||
|
||||
// 删除填空
|
||||
const handleRemoveAnswer = (index: number) => {
|
||||
if (props.answers.length > 1) {
|
||||
const newAnswers = [...props.answers];
|
||||
newAnswers.splice(index, 1);
|
||||
emit('update:answers', newAnswers);
|
||||
}
|
||||
};
|
||||
|
||||
// 更新填空答案
|
||||
const handleUpdateAnswer = (index: number, field: keyof FillBlankAnswer, value: any) => {
|
||||
const newAnswers = [...props.answers];
|
||||
newAnswers[index] = { ...newAnswers[index], [field]: value };
|
||||
emit('update:answers', newAnswers);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fill-blank-container {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
margin: 16px 0;
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.required::before {
|
||||
content: "*";
|
||||
color: red;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.answers-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.answer-item {
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.answer-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.answer-number {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.answer-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.answer-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.answer-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.answer-settings {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 24px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.score-setting,
|
||||
.case-setting {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.setting-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.score-input {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.case-select {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.delete-answer-btn {
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.add-answer-btn {
|
||||
margin-top: 8px;
|
||||
height: 40px;
|
||||
color: #666;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.add-answer-btn:hover {
|
||||
border-color: #1890ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.answer-settings {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.score-setting,
|
||||
.case-setting {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
</style>
|
260
src/components/teacher/MultipleChoiceQuestion.vue
Normal file
260
src/components/teacher/MultipleChoiceQuestion.vue
Normal file
@ -0,0 +1,260 @@
|
||||
<template>
|
||||
<div class="multiple-choice-container">
|
||||
<!-- 题目内容 -->
|
||||
<div class="form-section">
|
||||
<n-card size="small">
|
||||
<div class="section-header">
|
||||
<h3 class="section-title required">题目内容</h3>
|
||||
</div>
|
||||
<n-form-item label="" required>
|
||||
<n-input
|
||||
:value="title"
|
||||
@update:value="(value: string) => $emit('update:title', value)"
|
||||
type="textarea"
|
||||
placeholder="请输入题目内容..."
|
||||
:rows="4"
|
||||
show-count
|
||||
maxlength="500"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-card>
|
||||
</div>
|
||||
|
||||
<!-- 选择答案 -->
|
||||
<n-card size="small">
|
||||
<div class="section-header">
|
||||
<h3 class="section-title required">选择答案</h3>
|
||||
</div>
|
||||
<div class="options-container">
|
||||
<div
|
||||
v-for="(option, index) in modelValue"
|
||||
:key="index"
|
||||
class="option-item"
|
||||
:class="{ 'is-selected': correctAnswers.includes(index) }"
|
||||
>
|
||||
<div class="option-content">
|
||||
<div class="option-left">
|
||||
<n-checkbox
|
||||
:checked="correctAnswers.includes(index)"
|
||||
@update:checked="(checked: boolean) => handleToggleCorrectAnswer(index, checked)"
|
||||
class="correct-checkbox"
|
||||
/>
|
||||
<span class="option-label">{{ String.fromCharCode(65 + index) }}</span>
|
||||
</div>
|
||||
<n-input
|
||||
:value="option.content"
|
||||
@update:value="(value: string) => handleUpdateOption(index, value)"
|
||||
placeholder="请输入内容"
|
||||
show-count
|
||||
maxlength="200"
|
||||
class="option-input"
|
||||
/>
|
||||
<n-button
|
||||
v-if="modelValue.length > 2"
|
||||
@click="handleRemoveOption(index)"
|
||||
type="error"
|
||||
ghost
|
||||
size="small"
|
||||
class="delete-option-btn"
|
||||
>
|
||||
删除
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
<n-button
|
||||
v-if="modelValue.length < 6"
|
||||
@click="handleAddOption"
|
||||
dashed
|
||||
block
|
||||
class="add-option-btn"
|
||||
>
|
||||
+ 添加选项
|
||||
</n-button>
|
||||
</div>
|
||||
</n-card>
|
||||
|
||||
<!-- 答案解析 -->
|
||||
<div class="form-section">
|
||||
<n-card size="small">
|
||||
<div class="section-header">
|
||||
<h3 class="section-title">答案解析</h3>
|
||||
</div>
|
||||
<n-form-item label="">
|
||||
<n-input
|
||||
:value="explanation"
|
||||
@update:value="(value: string) => $emit('update:explanation', value)"
|
||||
type="textarea"
|
||||
placeholder="请输入答案解析..."
|
||||
:rows="3"
|
||||
show-count
|
||||
maxlength="300"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineProps, defineEmits } from 'vue';
|
||||
|
||||
interface Option {
|
||||
content: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
modelValue: Option[];
|
||||
correctAnswers: number[];
|
||||
title: string;
|
||||
explanation: string;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: Option[]): void;
|
||||
(e: 'update:correctAnswers', value: number[]): void;
|
||||
(e: 'update:title', value: string): void;
|
||||
(e: 'update:explanation', value: string): void;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
// 添加选项
|
||||
const handleAddOption = () => {
|
||||
if (props.modelValue.length < 6) {
|
||||
const newOptions = [...props.modelValue, { content: '' }];
|
||||
emit('update:modelValue', newOptions);
|
||||
}
|
||||
};
|
||||
|
||||
// 删除选项
|
||||
const handleRemoveOption = (index: number) => {
|
||||
if (props.modelValue.length > 2) {
|
||||
const newOptions = [...props.modelValue];
|
||||
newOptions.splice(index, 1);
|
||||
emit('update:modelValue', newOptions);
|
||||
|
||||
// 更新正确答案索引
|
||||
const newCorrectAnswers = props.correctAnswers
|
||||
.filter(i => i !== index)
|
||||
.map(i => i > index ? i - 1 : i);
|
||||
emit('update:correctAnswers', newCorrectAnswers);
|
||||
}
|
||||
};
|
||||
|
||||
// 更新选项内容
|
||||
const handleUpdateOption = (index: number, content: string) => {
|
||||
const newOptions = [...props.modelValue];
|
||||
newOptions[index] = { content };
|
||||
emit('update:modelValue', newOptions);
|
||||
};
|
||||
|
||||
// 切换正确答案
|
||||
const handleToggleCorrectAnswer = (index: number, checked: boolean) => {
|
||||
const newCorrectAnswers = [...props.correctAnswers];
|
||||
|
||||
if (checked) {
|
||||
if (!newCorrectAnswers.includes(index)) {
|
||||
newCorrectAnswers.push(index);
|
||||
}
|
||||
} else {
|
||||
const idx = newCorrectAnswers.indexOf(index);
|
||||
if (idx > -1) {
|
||||
newCorrectAnswers.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
|
||||
emit('update:correctAnswers', newCorrectAnswers);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.multiple-choice-container {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
margin: 16px 0;
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.required::before {
|
||||
content: "*";
|
||||
color: red;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.options-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.option-item {
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.option-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.option-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.option-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.option-input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.delete-option-btn {
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.add-option-btn {
|
||||
margin-top: 8px;
|
||||
height: 40px;
|
||||
color: #666;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.add-option-btn:hover {
|
||||
border-color: #1890ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.correct-checkbox {
|
||||
color: #52c41a;
|
||||
}
|
||||
</style>
|
170
src/components/teacher/QuestionTypeContainer.vue
Normal file
170
src/components/teacher/QuestionTypeContainer.vue
Normal file
@ -0,0 +1,170 @@
|
||||
<template>
|
||||
<div class="question-type-container">
|
||||
<!-- 单选题 -->
|
||||
<SingleChoiceQuestion
|
||||
v-if="questionType === 'single_choice'"
|
||||
v-model="optionsModel"
|
||||
v-model:correctAnswer="correctAnswerModel"
|
||||
v-model:title="titleModel"
|
||||
v-model:explanation="explanationModel"
|
||||
/>
|
||||
|
||||
<!-- 多选题 -->
|
||||
<MultipleChoiceQuestion
|
||||
v-else-if="questionType === 'multiple_choice'"
|
||||
v-model="optionsModel"
|
||||
v-model:correctAnswers="correctAnswersModel"
|
||||
v-model:title="titleModel"
|
||||
v-model:explanation="explanationModel"
|
||||
/>
|
||||
|
||||
<!-- 判断题 -->
|
||||
<TrueFalseQuestion
|
||||
v-else-if="questionType === 'true_false'"
|
||||
v-model:answer="trueFalseAnswerModel"
|
||||
v-model:title="titleModel"
|
||||
v-model:explanation="explanationModel"
|
||||
/>
|
||||
|
||||
<!-- 填空题 -->
|
||||
<FillBlankQuestion
|
||||
v-else-if="questionType === 'fill_blank'"
|
||||
v-model:answers="fillBlankAnswersModel"
|
||||
v-model:title="titleModel"
|
||||
v-model:explanation="explanationModel"
|
||||
/>
|
||||
|
||||
<!-- 简答题 -->
|
||||
<ShortAnswerQuestion
|
||||
v-else-if="questionType === 'short_answer'"
|
||||
v-model="shortAnswerModel"
|
||||
v-model:title="titleModel"
|
||||
v-model:explanation="explanationModel"
|
||||
/>
|
||||
|
||||
<!-- 复合题 -->
|
||||
<CompositeQuestion
|
||||
v-else-if="questionType === 'composite'"
|
||||
v-model="compositeDataModel"
|
||||
v-model:title="titleModel"
|
||||
v-model:explanation="explanationModel"
|
||||
/>
|
||||
|
||||
<!-- 其他题型的占位 -->
|
||||
<div v-else class="unsupported-type">
|
||||
<p>{{ questionType }} 题型正在开发中...</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import SingleChoiceQuestion from './SingleChoiceQuestion.vue';
|
||||
import MultipleChoiceQuestion from './MultipleChoiceQuestion.vue';
|
||||
import TrueFalseQuestion from './TrueFalseQuestion.vue';
|
||||
import FillBlankQuestion from './FillBlankQuestion.vue';
|
||||
import ShortAnswerQuestion from './ShortAnswerQuestion.vue';
|
||||
import CompositeQuestion from './CompositeQuestion.vue';
|
||||
|
||||
interface Option {
|
||||
content: string;
|
||||
}
|
||||
|
||||
interface FillBlankAnswer {
|
||||
content: string;
|
||||
score: number;
|
||||
caseSensitive: boolean;
|
||||
}
|
||||
|
||||
interface CompositeData {
|
||||
subQuestions: any[];
|
||||
}
|
||||
|
||||
interface Props {
|
||||
questionType: string;
|
||||
options: Option[];
|
||||
correctAnswer: number | null;
|
||||
correctAnswers?: number[];
|
||||
trueFalseAnswer?: boolean | null;
|
||||
fillBlankAnswers?: FillBlankAnswer[];
|
||||
shortAnswer?: string;
|
||||
compositeData?: CompositeData;
|
||||
title: string;
|
||||
explanation: string;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:options', value: Option[]): void;
|
||||
(e: 'update:correctAnswer', value: number | null): void;
|
||||
(e: 'update:correctAnswers', value: number[]): void;
|
||||
(e: 'update:trueFalseAnswer', value: boolean | null): void;
|
||||
(e: 'update:fillBlankAnswers', value: FillBlankAnswer[]): void;
|
||||
(e: 'update:shortAnswer', value: string): void;
|
||||
(e: 'update:compositeData', value: CompositeData): void;
|
||||
(e: 'update:title', value: string): void;
|
||||
(e: 'update:explanation', value: string): void;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
// 创建计算属性来处理双向绑定
|
||||
const optionsModel = computed({
|
||||
get: () => props.options,
|
||||
set: (value: Option[]) => emit('update:options', value)
|
||||
});
|
||||
|
||||
const correctAnswerModel = computed({
|
||||
get: () => props.correctAnswer,
|
||||
set: (value: number | null) => emit('update:correctAnswer', value)
|
||||
});
|
||||
|
||||
const correctAnswersModel = computed({
|
||||
get: () => props.correctAnswers || [],
|
||||
set: (value: number[]) => emit('update:correctAnswers', value)
|
||||
});
|
||||
|
||||
const trueFalseAnswerModel = computed({
|
||||
get: () => props.trueFalseAnswer ?? null,
|
||||
set: (value: boolean | null) => emit('update:trueFalseAnswer', value)
|
||||
});
|
||||
|
||||
const fillBlankAnswersModel = computed({
|
||||
get: () => props.fillBlankAnswers || [{ content: '', score: 1, caseSensitive: false }],
|
||||
set: (value: FillBlankAnswer[]) => emit('update:fillBlankAnswers', value)
|
||||
});
|
||||
|
||||
const shortAnswerModel = computed({
|
||||
get: () => props.shortAnswer || '',
|
||||
set: (value: string) => emit('update:shortAnswer', value)
|
||||
});
|
||||
|
||||
const titleModel = computed({
|
||||
get: () => props.title,
|
||||
set: (value: string) => emit('update:title', value)
|
||||
});
|
||||
|
||||
const explanationModel = computed({
|
||||
get: () => props.explanation,
|
||||
set: (value: string) => emit('update:explanation', value)
|
||||
});
|
||||
|
||||
const compositeDataModel = computed({
|
||||
get: () => props.compositeData || { subQuestions: [] },
|
||||
set: (value: CompositeData) => emit('update:compositeData', value)
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.question-type-container {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.unsupported-type {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 8px;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
132
src/components/teacher/ShortAnswerQuestion.vue
Normal file
132
src/components/teacher/ShortAnswerQuestion.vue
Normal file
@ -0,0 +1,132 @@
|
||||
<template>
|
||||
<div class="short-answer-container">
|
||||
<!-- 题目内容 -->
|
||||
<div class="form-section">
|
||||
<n-card size="small">
|
||||
<div class="section-header">
|
||||
<h3 class="section-title required">题目内容</h3>
|
||||
</div>
|
||||
<n-form-item label="" required>
|
||||
<n-input
|
||||
:value="title"
|
||||
@update:value="(value: string) => $emit('update:title', value)"
|
||||
type="textarea"
|
||||
placeholder="请输入题目内容..."
|
||||
:rows="4"
|
||||
show-count
|
||||
maxlength="500"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-card>
|
||||
</div>
|
||||
|
||||
<!-- 参考答案 -->
|
||||
<n-card size="small">
|
||||
<div class="section-header">
|
||||
<h3 class="section-title required">参考答案</h3>
|
||||
</div>
|
||||
<div class="answer-container">
|
||||
<n-input
|
||||
:value="modelValue"
|
||||
@update:value="(value: string) => $emit('update:modelValue', value)"
|
||||
type="textarea"
|
||||
placeholder="请输入简答题的参考答案..."
|
||||
:rows="6"
|
||||
show-count
|
||||
maxlength="1000"
|
||||
class="answer-textarea"
|
||||
/>
|
||||
<div class="answer-tip">
|
||||
<n-text depth="3" style="font-size: 12px;">
|
||||
请输入详细的参考答案,可作为评分依据
|
||||
</n-text>
|
||||
</div>
|
||||
</div>
|
||||
</n-card>
|
||||
|
||||
<!-- 答案解析 -->
|
||||
<div class="form-section">
|
||||
<n-card size="small">
|
||||
<div class="section-header">
|
||||
<h3 class="section-title">答案解析</h3>
|
||||
</div>
|
||||
<n-form-item label="">
|
||||
<n-input
|
||||
:value="explanation"
|
||||
@update:value="(value: string) => $emit('update:explanation', value)"
|
||||
type="textarea"
|
||||
placeholder="请输入答案解析..."
|
||||
:rows="3"
|
||||
show-count
|
||||
maxlength="300"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineProps, defineEmits } from 'vue';
|
||||
|
||||
interface Props {
|
||||
modelValue: string;
|
||||
title: string;
|
||||
explanation: string;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: string): void;
|
||||
(e: 'update:title', value: string): void;
|
||||
(e: 'update:explanation', value: string): void;
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
defineEmits<Emits>();
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.short-answer-container {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
margin: 16px 0;
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.required::before {
|
||||
content: "*";
|
||||
color: red;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.answer-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.answer-textarea {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.answer-tip {
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
239
src/components/teacher/SingleChoiceQuestion.vue
Normal file
239
src/components/teacher/SingleChoiceQuestion.vue
Normal file
@ -0,0 +1,239 @@
|
||||
<template>
|
||||
<div class="single-choice-container">
|
||||
<!-- 题目内容 -->
|
||||
<div class="form-section">
|
||||
<n-card size="small">
|
||||
<div class="section-header">
|
||||
<h3 class="section-title required">题目内容</h3>
|
||||
</div>
|
||||
<n-form-item label="" required>
|
||||
<n-input
|
||||
:value="title"
|
||||
@update:value="(value: string) => $emit('update:title', value)"
|
||||
type="textarea"
|
||||
placeholder="请输入题目内容..."
|
||||
:rows="4"
|
||||
show-count
|
||||
maxlength="500"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-card>
|
||||
</div>
|
||||
|
||||
<!-- 选择答案 -->
|
||||
<n-card size="small">
|
||||
<div class="section-header">
|
||||
<h3 class="section-title required">选择答案</h3>
|
||||
</div>
|
||||
<div class="options-container">
|
||||
<div v-for="(option, index) in modelValue" :key="index" class="option-item"
|
||||
:class="{ 'is-selected': correctAnswer === index }">
|
||||
<div class="option-content">
|
||||
<div class="option-left">
|
||||
<n-radio :checked="correctAnswer === index"
|
||||
@update:checked="() => handleSetCorrectAnswer(index)" name="correct-answer"
|
||||
class="correct-radio" />
|
||||
<span class="option-label">{{ String.fromCharCode(65 + index) }}</span>
|
||||
</div>
|
||||
<n-input :value="option.content"
|
||||
@update:value="(value: string) => handleUpdateOption(index, value)" placeholder="请输入内容"
|
||||
show-count maxlength="200" class="option-input" />
|
||||
<n-button v-if="modelValue.length > 2" @click="handleRemoveOption(index)" type="error" ghost
|
||||
size="small" class="delete-option-btn">
|
||||
删除
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
<n-button v-if="modelValue.length < 6" @click="handleAddOption" dashed block class="add-option-btn">
|
||||
+ 添加选项
|
||||
</n-button>
|
||||
</div>
|
||||
</n-card>
|
||||
|
||||
<!-- 答案解析 -->
|
||||
<div class="form-section">
|
||||
<n-card size="small">
|
||||
<div class="section-header">
|
||||
<h3 class="section-title">答案解析</h3>
|
||||
</div>
|
||||
<n-form-item label="">
|
||||
<n-input
|
||||
:value="explanation"
|
||||
@update:value="(value: string) => $emit('update:explanation', value)"
|
||||
type="textarea"
|
||||
placeholder="请输入答案解析..."
|
||||
:rows="3"
|
||||
show-count
|
||||
maxlength="300"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineProps, defineEmits } from 'vue';
|
||||
|
||||
interface Option {
|
||||
content: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
modelValue: Option[];
|
||||
correctAnswer: number | null;
|
||||
title: string;
|
||||
explanation: string;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: Option[]): void;
|
||||
(e: 'update:correctAnswer', value: number | null): void;
|
||||
(e: 'update:title', value: string): void;
|
||||
(e: 'update:explanation', value: string): void;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
// 添加选项
|
||||
const handleAddOption = () => {
|
||||
if (props.modelValue.length < 6) {
|
||||
const newOptions = [...props.modelValue, { content: '' }];
|
||||
emit('update:modelValue', newOptions);
|
||||
}
|
||||
};
|
||||
|
||||
// 删除选项
|
||||
const handleRemoveOption = (index: number) => {
|
||||
if (props.modelValue.length > 2) {
|
||||
const newOptions = [...props.modelValue];
|
||||
newOptions.splice(index, 1);
|
||||
emit('update:modelValue', newOptions);
|
||||
|
||||
// 更新正确答案索引
|
||||
if (props.correctAnswer === index) {
|
||||
emit('update:correctAnswer', null);
|
||||
} else if (props.correctAnswer !== null && props.correctAnswer > index) {
|
||||
emit('update:correctAnswer', props.correctAnswer - 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 更新选项内容
|
||||
const handleUpdateOption = (index: number, content: string) => {
|
||||
const newOptions = [...props.modelValue];
|
||||
newOptions[index] = { content };
|
||||
emit('update:modelValue', newOptions);
|
||||
};
|
||||
|
||||
// 设置正确答案
|
||||
const handleSetCorrectAnswer = (index: number) => {
|
||||
emit('update:correctAnswer', index);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.single-choice-container {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
margin: 16px 0;
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.required::before {
|
||||
content: "*";
|
||||
color: red;
|
||||
}
|
||||
|
||||
.section-number {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background-color: #1890ff;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.options-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.option-item {
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.option-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.option-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.option-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
|
||||
.option-input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.delete-option-btn {
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.add-option-btn {
|
||||
margin-top: 8px;
|
||||
height: 40px;
|
||||
color: #666;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.add-option-btn:hover {
|
||||
border-color: #1890ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.correct-radio {
|
||||
color: #52c41a;
|
||||
}
|
||||
</style>
|
194
src/components/teacher/TrueFalseQuestion.vue
Normal file
194
src/components/teacher/TrueFalseQuestion.vue
Normal file
@ -0,0 +1,194 @@
|
||||
<template>
|
||||
<div class="true-false-container">
|
||||
<!-- 题目内容 -->
|
||||
<div class="form-section">
|
||||
<n-card size="small">
|
||||
<div class="section-header">
|
||||
<h3 class="section-title required">题目内容</h3>
|
||||
</div>
|
||||
<n-form-item label="" required>
|
||||
<n-input
|
||||
:value="title"
|
||||
@update:value="(value: string) => $emit('update:title', value)"
|
||||
type="textarea"
|
||||
placeholder="请输入题目内容..."
|
||||
:rows="4"
|
||||
show-count
|
||||
maxlength="500"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-card>
|
||||
</div>
|
||||
|
||||
<!-- 选择答案 -->
|
||||
<n-card size="small">
|
||||
<div class="section-header">
|
||||
<h3 class="section-title required">选择答案</h3>
|
||||
</div>
|
||||
<div class="options-container">
|
||||
<div class="option-item" :class="{ 'is-selected': answer === true }">
|
||||
<div class="option-content">
|
||||
<div class="option-left">
|
||||
<n-radio
|
||||
:checked="answer === true"
|
||||
@update:checked="() => handleSetAnswer(true)"
|
||||
:name="radioName"
|
||||
class="correct-radio"
|
||||
/>
|
||||
<span class="option-label">A</span>
|
||||
</div>
|
||||
<div class="option-text">对</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="option-item" :class="{ 'is-selected': answer === false }">
|
||||
<div class="option-content">
|
||||
<div class="option-left">
|
||||
<n-radio
|
||||
:checked="answer === false"
|
||||
@update:checked="() => handleSetAnswer(false)"
|
||||
:name="radioName"
|
||||
class="correct-radio"
|
||||
/>
|
||||
<span class="option-label">B</span>
|
||||
</div>
|
||||
<div class="option-text">错</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-card>
|
||||
|
||||
<!-- 答案解析 -->
|
||||
<div class="form-section">
|
||||
<n-card size="small">
|
||||
<div class="section-header">
|
||||
<h3 class="section-title">答案解析</h3>
|
||||
</div>
|
||||
<n-form-item label="">
|
||||
<n-input
|
||||
:value="explanation"
|
||||
@update:value="(value: string) => $emit('update:explanation', value)"
|
||||
type="textarea"
|
||||
placeholder="请输入答案解析..."
|
||||
:rows="3"
|
||||
show-count
|
||||
maxlength="300"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineProps, defineEmits, computed } from 'vue';
|
||||
|
||||
interface Props {
|
||||
answer: boolean | null;
|
||||
title: string;
|
||||
explanation: string;
|
||||
id?: string | number; // 添加id prop用于生成唯一的name
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:answer', value: boolean | null): void;
|
||||
(e: 'update:title', value: string): void;
|
||||
(e: 'update:explanation', value: string): void;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
// 生成唯一的radio name
|
||||
const radioName = computed(() => `true-false-answer-${props.id || 'default'}`);
|
||||
|
||||
// 设置答案
|
||||
const handleSetAnswer = (value: boolean) => {
|
||||
emit('update:answer', value);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.true-false-container {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
margin: 16px 0;
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.required::before {
|
||||
content: "*";
|
||||
color: red;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.options-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.option-item {
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.option-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.option-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.option-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.option-text {
|
||||
flex: 1;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
padding: 8px 12px;
|
||||
background-color: #fafafa;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.option-item.is-selected .option-text {
|
||||
background-color: #e6f7ff;
|
||||
border-color: #1890ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.correct-radio {
|
||||
color: #52c41a;
|
||||
}
|
||||
</style>
|
@ -61,6 +61,7 @@ import QuestionManagement from '@/views/teacher/ExamPages/QuestionManagement.vue
|
||||
import ExamLibrary from '@/views/teacher/ExamPages/ExamLibrary.vue'
|
||||
import MarkingCenter from '@/views/teacher/ExamPages/MarkingCenter.vue'
|
||||
import AddExam from '@/views/teacher/ExamPages/AddExam.vue'
|
||||
import AddQuestion from '@/views/teacher/ExamPages/AddQuestion.vue'
|
||||
|
||||
import ChapterEditor from '@/views/teacher/course/ChapterEditor.vue'
|
||||
|
||||
@ -267,6 +268,12 @@ const routes: RouteRecordRaw[] = [
|
||||
name: 'AddExam',
|
||||
component: AddExam,
|
||||
meta: { title: '添加试卷' }
|
||||
},
|
||||
{
|
||||
path: 'add-question/:id?',
|
||||
name: 'AddQuestionPage',
|
||||
component: AddQuestion,
|
||||
meta: { title: '添加试题' }
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -498,6 +498,14 @@ const updateActiveNavItem = () => {
|
||||
activeNavItem.value = 2; // 我的资源
|
||||
} else if (path.includes('personal-center')) {
|
||||
activeNavItem.value = 3; // 个人中心
|
||||
} else if (path.includes('exam-management')) {
|
||||
activeNavItem.value = 4; // 考试管理
|
||||
examMenuExpanded.value = true;
|
||||
|
||||
// 获取路由的最后一层路径(不包含/)
|
||||
const pathSegments = path.split('/');
|
||||
const lastSegment = pathSegments[pathSegments.length - 1];
|
||||
activeSubNavItem.value = lastSegment || '';
|
||||
}
|
||||
}
|
||||
|
||||
|
1382
src/views/teacher/ExamPages/AddQuestion.vue
Normal file
1382
src/views/teacher/ExamPages/AddQuestion.vue
Normal file
File diff suppressed because it is too large
Load Diff
@ -7,10 +7,11 @@
|
||||
<n-button ghost @click="importQuestions">导入</n-button>
|
||||
<n-button ghost @click="exportQuestions">导出</n-button>
|
||||
<n-button type="error" ghost @click="deleteSelected" :disabled="selectedRowKeys.length === 0">删除</n-button>
|
||||
<n-button @click="setCategoryForSelected" :disabled="selectedRowKeys.length === 0">分类设置</n-button>
|
||||
<n-select
|
||||
v-model:value="filters.category"
|
||||
placeholder="分类"
|
||||
:options="categoryOptions"
|
||||
:options="[{ label: '全部', value: '' }, ...allCategoryOptions]"
|
||||
style="width: 120px"
|
||||
@update:value="handleFilterChange"
|
||||
/>
|
||||
@ -45,14 +46,135 @@
|
||||
@success="handleImportSuccess"
|
||||
@template-download="handleTemplateDownload"
|
||||
/>
|
||||
|
||||
<!-- 分类设置弹窗 -->
|
||||
<n-modal
|
||||
v-model:show="showCategoryModal"
|
||||
preset="dialog"
|
||||
title="分类设置"
|
||||
style="width: 500px;"
|
||||
>
|
||||
<div class="category-modal-content">
|
||||
<div class="selected-info">
|
||||
<n-alert type="info" :show-icon="false" style="margin-bottom: 16px;">
|
||||
已选择 {{ selectedRowKeys.length }} 个试题
|
||||
</n-alert>
|
||||
</div>
|
||||
|
||||
<div class="category-selection">
|
||||
<div class="form-item">
|
||||
<label>选择分类:</label>
|
||||
<n-select
|
||||
v-model:value="selectedCategory"
|
||||
:options="allCategoryOptions"
|
||||
placeholder="请选择分类"
|
||||
style="width: 100%;"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="showAddCategoryInput" class="form-item">
|
||||
<label>新分类名称:</label>
|
||||
<n-space>
|
||||
<n-input
|
||||
v-model:value="newCategoryName"
|
||||
placeholder="请输入新分类名称"
|
||||
style="width: 200px;"
|
||||
@keyup.enter="addNewCategory"
|
||||
/>
|
||||
<n-button type="primary" @click="addNewCategory" :disabled="!newCategoryName.trim()">
|
||||
添加
|
||||
</n-button>
|
||||
</n-space>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #action>
|
||||
<n-space>
|
||||
<n-button @click="showAddCategoryInput = !showAddCategoryInput" secondary type="info">
|
||||
{{ showAddCategoryInput ? '取消新增' : '新增分类' }}
|
||||
</n-button>
|
||||
<n-button @click="closeCategoryModal">取消</n-button>
|
||||
<n-button type="primary" @click="applyCategoryChange" :disabled="!selectedCategory">
|
||||
确认设置
|
||||
</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-modal>
|
||||
|
||||
<!-- 分类管理弹窗 -->
|
||||
<n-modal
|
||||
v-model:show="showCategoryManageModal"
|
||||
preset="dialog"
|
||||
title="分类管理"
|
||||
style="width: 600px;"
|
||||
>
|
||||
<div class="category-manage-content">
|
||||
<div class="category-header">
|
||||
<n-space>
|
||||
<n-button type="primary" @click="showAddCategoryInManage = true">
|
||||
新增分类
|
||||
</n-button>
|
||||
</n-space>
|
||||
</div>
|
||||
|
||||
<div v-if="showAddCategoryInManage" class="add-category-section">
|
||||
<n-space>
|
||||
<n-input
|
||||
v-model:value="newCategoryInManage"
|
||||
placeholder="请输入分类名称"
|
||||
style="width: 200px;"
|
||||
@keyup.enter="addCategoryInManage"
|
||||
/>
|
||||
<n-button type="primary" @click="addCategoryInManage" :disabled="!newCategoryInManage.trim()">
|
||||
添加
|
||||
</n-button>
|
||||
<n-button @click="cancelAddCategoryInManage">
|
||||
取消
|
||||
</n-button>
|
||||
</n-space>
|
||||
</div>
|
||||
|
||||
<div class="category-list">
|
||||
<n-list>
|
||||
<n-list-item v-for="category in customCategories" :key="category.value">
|
||||
<div class="category-item">
|
||||
<span class="category-name">{{ category.label }}</span>
|
||||
<n-space>
|
||||
<n-button size="small" @click="editCategory(category)">
|
||||
编辑
|
||||
</n-button>
|
||||
<n-button size="small" type="error" @click="deleteCategory(category.value)">
|
||||
删除
|
||||
</n-button>
|
||||
</n-space>
|
||||
</div>
|
||||
</n-list-item>
|
||||
</n-list>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #action>
|
||||
<n-space>
|
||||
<n-button @click="closeCategoryManageModal">关闭</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted, h, VNode } from 'vue';
|
||||
import { NButton, NTag, NSpace } from 'naive-ui';
|
||||
import { NButton, NTag, NSpace, useMessage } from 'naive-ui';
|
||||
import { useRouter } from 'vue-router';
|
||||
import ImportModal from '@/components/common/ImportModal.vue';
|
||||
|
||||
// 消息提示
|
||||
const message = useMessage();
|
||||
|
||||
// 路由
|
||||
const router = useRouter();
|
||||
|
||||
// 题目数据接口
|
||||
interface Question {
|
||||
id: string;
|
||||
@ -72,11 +194,17 @@ const filters = reactive({
|
||||
keyword: ''
|
||||
});
|
||||
|
||||
// 分类选项
|
||||
const categoryOptions = ref([
|
||||
{ label: '全部', value: '' },
|
||||
// 自定义分类列表(用于分类管理)
|
||||
const customCategories = ref([
|
||||
{ label: '分类试题', value: 'category' },
|
||||
{ label: '考试试题', value: 'exam' }
|
||||
{ label: '考试试题', value: 'exam' },
|
||||
{ label: '练习试题', value: 'practice' },
|
||||
{ label: '模拟试题', value: 'simulation' }
|
||||
]);
|
||||
|
||||
// 所有分类选项(包含自定义分类,用于分类设置)
|
||||
const allCategoryOptions = computed(() => [
|
||||
...customCategories.value
|
||||
]);
|
||||
|
||||
// 表格相关状态
|
||||
@ -87,6 +215,17 @@ const questionList = ref<Question[]>([]);
|
||||
// 导入弹窗状态
|
||||
const showImportModal = ref(false);
|
||||
|
||||
// 分类设置相关状态
|
||||
const showCategoryModal = ref(false);
|
||||
const selectedCategory = ref('');
|
||||
const showAddCategoryInput = ref(false);
|
||||
const newCategoryName = ref('');
|
||||
|
||||
// 分类管理相关状态
|
||||
const showCategoryManageModal = ref(false);
|
||||
const showAddCategoryInManage = ref(false);
|
||||
const newCategoryInManage = ref('');
|
||||
|
||||
// 分页配置
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
@ -164,7 +303,11 @@ const createColumns = ({
|
||||
title: '分类',
|
||||
key: 'category',
|
||||
width: 100,
|
||||
align: 'center' as const
|
||||
align: 'center' as const,
|
||||
render(row: Question) {
|
||||
const categoryInfo = allCategoryOptions.value.find(cat => cat.value === row.category);
|
||||
return categoryInfo ? categoryInfo.label : row.category;
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '难度',
|
||||
@ -249,7 +392,7 @@ const generateMockData = (): Question[] => {
|
||||
const mockData: Question[] = [];
|
||||
const types = ['single_choice', 'multiple_choice', 'true_false', 'fill_blank', 'short_answer'];
|
||||
const difficulties = ['easy', 'medium', 'hard'];
|
||||
const categories = ['试题分类', '考试分类'];
|
||||
const categories = customCategories.value.map(cat => cat.value);
|
||||
const creators = ['王建国', '李明', '张三', '刘老师'];
|
||||
|
||||
for (let i = 1; i <= 50; i++) {
|
||||
@ -322,7 +465,7 @@ const loadQuestions = async () => {
|
||||
|
||||
// 操作方法
|
||||
const addQuestion = () => {
|
||||
console.log('添加试题');
|
||||
router.push('/teacher/exam-management/add-question');
|
||||
};
|
||||
|
||||
const importQuestions = () => {
|
||||
@ -362,6 +505,142 @@ const deleteQuestion = (id: string) => {
|
||||
onMounted(() => {
|
||||
loadQuestions();
|
||||
});
|
||||
|
||||
// 分类设置相关方法
|
||||
const setCategoryForSelected = () => {
|
||||
if (selectedRowKeys.value.length === 0) {
|
||||
message.warning('请先选择要设置分类的试题');
|
||||
return;
|
||||
}
|
||||
selectedCategory.value = '';
|
||||
showAddCategoryInput.value = false;
|
||||
newCategoryName.value = '';
|
||||
showCategoryModal.value = true;
|
||||
};
|
||||
|
||||
const closeCategoryModal = () => {
|
||||
showCategoryModal.value = false;
|
||||
selectedCategory.value = '';
|
||||
showAddCategoryInput.value = false;
|
||||
newCategoryName.value = '';
|
||||
};
|
||||
|
||||
const addNewCategory = () => {
|
||||
const trimmedName = newCategoryName.value.trim();
|
||||
if (!trimmedName) {
|
||||
message.warning('请输入分类名称');
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否已存在
|
||||
const exists = customCategories.value.some(cat => cat.label === trimmedName);
|
||||
if (exists) {
|
||||
message.warning('该分类已存在');
|
||||
return;
|
||||
}
|
||||
|
||||
// 添加新分类
|
||||
const newCategory = {
|
||||
label: trimmedName,
|
||||
value: trimmedName.toLowerCase().replace(/\s+/g, '_')
|
||||
};
|
||||
customCategories.value.push(newCategory);
|
||||
selectedCategory.value = newCategory.value;
|
||||
newCategoryName.value = '';
|
||||
showAddCategoryInput.value = false;
|
||||
message.success('分类添加成功');
|
||||
};
|
||||
|
||||
const applyCategoryChange = async () => {
|
||||
if (!selectedCategory.value) {
|
||||
message.warning('请选择分类');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 模拟 API 调用
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
|
||||
// 更新本地数据
|
||||
const selectedCategoryLabel = allCategoryOptions.value.find(cat => cat.value === selectedCategory.value)?.label || selectedCategory.value;
|
||||
questionList.value.forEach(question => {
|
||||
if (selectedRowKeys.value.includes(question.id)) {
|
||||
question.category = selectedCategory.value;
|
||||
}
|
||||
});
|
||||
|
||||
message.success(`已将 ${selectedRowKeys.value.length} 个试题的分类设置为「${selectedCategoryLabel}」`);
|
||||
selectedRowKeys.value = [];
|
||||
closeCategoryModal();
|
||||
|
||||
} catch (error) {
|
||||
console.error('设置分类失败:', error);
|
||||
message.error('设置分类失败,请重试');
|
||||
}
|
||||
};
|
||||
|
||||
// // 分类管理相关方法
|
||||
// const openCategoryManage = () => {
|
||||
// showCategoryManageModal.value = true;
|
||||
// };
|
||||
|
||||
const closeCategoryManageModal = () => {
|
||||
showCategoryManageModal.value = false;
|
||||
showAddCategoryInManage.value = false;
|
||||
newCategoryInManage.value = '';
|
||||
};
|
||||
|
||||
const addCategoryInManage = () => {
|
||||
const trimmedName = newCategoryInManage.value.trim();
|
||||
if (!trimmedName) {
|
||||
message.warning('请输入分类名称');
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否已存在
|
||||
const exists = customCategories.value.some(cat => cat.label === trimmedName);
|
||||
if (exists) {
|
||||
message.warning('该分类已存在');
|
||||
return;
|
||||
}
|
||||
|
||||
// 添加新分类
|
||||
const newCategory = {
|
||||
label: trimmedName,
|
||||
value: trimmedName.toLowerCase().replace(/\s+/g, '_')
|
||||
};
|
||||
customCategories.value.push(newCategory);
|
||||
newCategoryInManage.value = '';
|
||||
showAddCategoryInManage.value = false;
|
||||
message.success('分类添加成功');
|
||||
};
|
||||
|
||||
const cancelAddCategoryInManage = () => {
|
||||
showAddCategoryInManage.value = false;
|
||||
newCategoryInManage.value = '';
|
||||
};
|
||||
|
||||
const editCategory = (category: { label: string; value: string }) => {
|
||||
// TODO: 实现编辑分类功能
|
||||
console.log('编辑分类:', category);
|
||||
message.info('编辑分类功能待实现');
|
||||
};
|
||||
|
||||
const deleteCategory = (categoryValue: string) => {
|
||||
// 检查是否有试题使用该分类
|
||||
const hasQuestions = questionList.value.some(q => q.category === categoryValue);
|
||||
if (hasQuestions) {
|
||||
message.warning('该分类下还有试题,不能删除');
|
||||
return;
|
||||
}
|
||||
|
||||
const index = customCategories.value.findIndex(cat => cat.value === categoryValue);
|
||||
if (index > -1) {
|
||||
const categoryName = customCategories.value[index].label;
|
||||
customCategories.value.splice(index, 1);
|
||||
message.success(`已删除分类「${categoryName}」`);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@ -395,6 +674,61 @@ onMounted(() => {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* 分类设置弹窗样式 */
|
||||
.category-modal-content {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.category-modal-content .selected-info {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.category-modal-content .form-item {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.category-modal-content .form-item label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 分类管理弹窗样式 */
|
||||
.category-manage-content {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.category-header {
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.add-category-section {
|
||||
margin-bottom: 16px;
|
||||
padding: 12px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.category-list {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.category-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.category-name {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 1200px) {
|
||||
.header-section {
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,210 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="exam-library-container">
|
||||
<div class="header-section">
|
||||
<h1 class="title">试卷库</h1>
|
||||
<n-space class="actions-group">
|
||||
<n-button type="primary" @click="handleAddExam">添加试卷</n-button>
|
||||
<n-button ghost>导入</n-button>
|
||||
<n-button ghost>导出</n-button>
|
||||
<n-button type="error" ghost>删除</n-button>
|
||||
<n-input placeholder="请输入想要搜索的内容" />
|
||||
<n-button type="primary">搜索</n-button>
|
||||
</n-space>
|
||||
</div>
|
||||
|
||||
<n-data-table :columns="columns" :data="examData" :row-key="(row: Exam) => row.id"
|
||||
@update:checked-row-keys="handleCheck" class="exam-table" :single-line="false" />
|
||||
|
||||
<div class="pagination-container">
|
||||
<n-pagination v-model:page="currentPage" :page-count="totalPages" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h, ref, VNode } from 'vue';
|
||||
import { NButton, NSpace, useMessage, NDataTable, NPagination, NInput } from 'naive-ui';
|
||||
import type { DataTableColumns } from 'naive-ui';
|
||||
import { useRouter } from 'vue-router';
|
||||
const router = useRouter();
|
||||
|
||||
// 定义考试条目的数据类型
|
||||
type Exam = {
|
||||
id: number;
|
||||
name: string;
|
||||
category: '练习' | '考试';
|
||||
questionCount: number;
|
||||
chapter: string;
|
||||
totalScore: number;
|
||||
difficulty: '易' | '中' | '难';
|
||||
status: '发布中' | '未发布' | '已结束';
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
creator: string;
|
||||
creationTime: string;
|
||||
};
|
||||
|
||||
// 主题颜色已在全局配置中统一设置
|
||||
|
||||
|
||||
const message = useMessage();
|
||||
|
||||
// 创建表格列的函数
|
||||
const createColumns = ({
|
||||
handleAction,
|
||||
}: {
|
||||
handleAction: (action: string, rowData: Exam) => void;
|
||||
}): DataTableColumns<Exam> => {
|
||||
return [
|
||||
{
|
||||
type: 'selection',
|
||||
},
|
||||
{
|
||||
title: '序号',
|
||||
key: 'id',
|
||||
width: 60,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '试卷名称',
|
||||
key: 'name',
|
||||
},
|
||||
{
|
||||
title: '分类',
|
||||
key: 'category',
|
||||
},
|
||||
{
|
||||
title: '题量',
|
||||
key: 'questionCount',
|
||||
},
|
||||
{
|
||||
title: '所属章节',
|
||||
key: 'chapter',
|
||||
},
|
||||
{
|
||||
title: '总分',
|
||||
key: 'totalScore',
|
||||
},
|
||||
{
|
||||
title: '难度',
|
||||
key: 'difficulty',
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status',
|
||||
},
|
||||
{
|
||||
title: '起止时间',
|
||||
key: 'startTime',
|
||||
render(row) {
|
||||
return `${row.startTime} - ${row.endTime}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '创建人',
|
||||
key: 'creator',
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
key: 'creationTime',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
render(row) {
|
||||
const buttons: VNode[] = [];
|
||||
if (row.status === '发布中') {
|
||||
buttons.push(
|
||||
h(NButton, { size: 'small', type: 'primary', ghost: true, style: 'margin: 0 3px;', onClick: () => handleAction('批阅', row) }, { default: () => '批阅' })
|
||||
);
|
||||
} else if (row.status === '未发布') {
|
||||
buttons.push(
|
||||
h(NButton, { size: 'small', type: 'primary', style: 'margin: 0 3px;', onClick: () => handleAction('发布', row) }, { default: () => '发布' })
|
||||
);
|
||||
}
|
||||
|
||||
buttons.push(
|
||||
h(NButton, { size: 'small', type: 'primary', ghost: true, style: 'margin: 0 3px;', onClick: () => handleAction('编辑', row) }, { default: () => '编辑' })
|
||||
);
|
||||
buttons.push(
|
||||
h(NButton, { size: 'small', type: 'error', ghost: true, style: 'margin: 0 3px;', onClick: () => handleAction('删除', row) }, { default: () => '删除' })
|
||||
);
|
||||
return h(NSpace, {}, { default: () => buttons });
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
// 模拟数据
|
||||
const examData = ref<Exam[]>([
|
||||
{ id: 1, name: '试卷名称试卷名称', category: '练习', questionCount: 100, chapter: '第一节 开课前准备', totalScore: 150, difficulty: '易', status: '发布中', startTime: '2025.07.25 09:20', endTime: '2025.07.25 09:20', creator: '王建国', creationTime: '2025.07.25 9:20' },
|
||||
{ id: 2, name: '试卷名称试卷名称', category: '考试', questionCount: 100, chapter: '第一节 开课前准备', totalScore: 150, difficulty: '易', status: '未发布', startTime: '2025.07.25 09:20', endTime: '2025.07.25 09:20', creator: '王建国', creationTime: '2025.07.25 9:20' },
|
||||
{ id: 3, name: '试卷名称试卷名称', category: '练习', questionCount: 100, chapter: '第一节 开课前准备', totalScore: 150, difficulty: '易', status: '发布中', startTime: '2025.07.25 09:20', endTime: '2025.07.25 09:20', creator: '王建国', creationTime: '2025.07.25 9:20' },
|
||||
{ id: 4, name: '试卷名称试卷名称', category: '考试', questionCount: 100, chapter: '第一节 开课前准备', totalScore: 150, difficulty: '易', status: '发布中', startTime: '2025.07.25 09:20', endTime: '2025.07.25 09:20', creator: '王建国', creationTime: '2025.07.25 9:20' },
|
||||
{ id: 5, name: '试卷名称试卷名称', category: '练习', questionCount: 100, chapter: '第一节 开课前准备', totalScore: 150, difficulty: '易', status: '发布中', startTime: '2025.07.25 09:20', endTime: '2025.07.25 09:20', creator: '王建国', creationTime: '2025.07.25 9:20' },
|
||||
{ id: 6, name: '试卷名称试卷名称', category: '考试', questionCount: 100, chapter: '第一节 开课前准备', totalScore: 150, difficulty: '易', status: '未发布', startTime: '2025.07.25 09:20', endTime: '2025.07.25 09:20', creator: '王建国', creationTime: '2025.07.25 9:20' },
|
||||
{ id: 7, name: '试卷名称试卷名称', category: '练习', questionCount: 100, chapter: '第一节 开课前准备', totalScore: 150, difficulty: '易', status: '发布中', startTime: '2025.07.25 09:20', endTime: '2025.07.25 09:20', creator: '王建国', creationTime: '2025.07.25 9:20' },
|
||||
{ id: 8, name: '试卷名称试卷名称', category: '考试', questionCount: 100, chapter: '第一节 开课前准备', totalScore: 150, difficulty: '易', status: '未发布', startTime: '2025.07.25 09:20', endTime: '2025.07.25 09:20', creator: '王建国', creationTime: '2025.07.25 9:20' },
|
||||
]);
|
||||
|
||||
const columns = createColumns({
|
||||
handleAction: (action, row) => {
|
||||
message.info(`执行操作: ${action} on row ${row.id}`);
|
||||
},
|
||||
});
|
||||
|
||||
const checkedRowKeys = ref<Array<string | number>>([]);
|
||||
const handleCheck = (rowKeys: Array<string | number>) => {
|
||||
checkedRowKeys.value = rowKeys;
|
||||
};
|
||||
|
||||
// 分页状态
|
||||
const currentPage = ref(1);
|
||||
const totalPages = ref(29); // 来自图片
|
||||
|
||||
const handleAddExam = () => {
|
||||
// 这里可以添加导航到添加试卷页面的逻辑
|
||||
router.push({ name: 'AddExam' });
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.exam-library-container {
|
||||
background-color: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.header-section {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid #E6E6E6;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.actions-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.exam-table {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
@ -1,11 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>阅卷中心</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
Loading…
x
Reference in New Issue
Block a user