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 ExamLibrary from '@/views/teacher/ExamPages/ExamLibrary.vue'
|
||||||
import MarkingCenter from '@/views/teacher/ExamPages/MarkingCenter.vue'
|
import MarkingCenter from '@/views/teacher/ExamPages/MarkingCenter.vue'
|
||||||
import AddExam from '@/views/teacher/ExamPages/AddExam.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'
|
import ChapterEditor from '@/views/teacher/course/ChapterEditor.vue'
|
||||||
|
|
||||||
@ -267,6 +268,12 @@ const routes: RouteRecordRaw[] = [
|
|||||||
name: 'AddExam',
|
name: 'AddExam',
|
||||||
component: AddExam,
|
component: AddExam,
|
||||||
meta: { title: '添加试卷' }
|
meta: { title: '添加试卷' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'add-question/:id?',
|
||||||
|
name: 'AddQuestionPage',
|
||||||
|
component: AddQuestion,
|
||||||
|
meta: { title: '添加试题' }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -498,6 +498,14 @@ const updateActiveNavItem = () => {
|
|||||||
activeNavItem.value = 2; // 我的资源
|
activeNavItem.value = 2; // 我的资源
|
||||||
} else if (path.includes('personal-center')) {
|
} else if (path.includes('personal-center')) {
|
||||||
activeNavItem.value = 3; // 个人中心
|
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="importQuestions">导入</n-button>
|
||||||
<n-button ghost @click="exportQuestions">导出</n-button>
|
<n-button ghost @click="exportQuestions">导出</n-button>
|
||||||
<n-button type="error" ghost @click="deleteSelected" :disabled="selectedRowKeys.length === 0">删除</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
|
<n-select
|
||||||
v-model:value="filters.category"
|
v-model:value="filters.category"
|
||||||
placeholder="分类"
|
placeholder="分类"
|
||||||
:options="categoryOptions"
|
:options="[{ label: '全部', value: '' }, ...allCategoryOptions]"
|
||||||
style="width: 120px"
|
style="width: 120px"
|
||||||
@update:value="handleFilterChange"
|
@update:value="handleFilterChange"
|
||||||
/>
|
/>
|
||||||
@ -45,14 +46,135 @@
|
|||||||
@success="handleImportSuccess"
|
@success="handleImportSuccess"
|
||||||
@template-download="handleTemplateDownload"
|
@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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, computed, onMounted, h, VNode } from 'vue';
|
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';
|
import ImportModal from '@/components/common/ImportModal.vue';
|
||||||
|
|
||||||
|
// 消息提示
|
||||||
|
const message = useMessage();
|
||||||
|
|
||||||
|
// 路由
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
// 题目数据接口
|
// 题目数据接口
|
||||||
interface Question {
|
interface Question {
|
||||||
id: string;
|
id: string;
|
||||||
@ -72,11 +194,17 @@ const filters = reactive({
|
|||||||
keyword: ''
|
keyword: ''
|
||||||
});
|
});
|
||||||
|
|
||||||
// 分类选项
|
// 自定义分类列表(用于分类管理)
|
||||||
const categoryOptions = ref([
|
const customCategories = ref([
|
||||||
{ label: '全部', value: '' },
|
|
||||||
{ label: '分类试题', value: 'category' },
|
{ 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 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({
|
const pagination = reactive({
|
||||||
page: 1,
|
page: 1,
|
||||||
@ -164,7 +303,11 @@ const createColumns = ({
|
|||||||
title: '分类',
|
title: '分类',
|
||||||
key: 'category',
|
key: 'category',
|
||||||
width: 100,
|
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: '难度',
|
title: '难度',
|
||||||
@ -249,7 +392,7 @@ const generateMockData = (): Question[] => {
|
|||||||
const mockData: Question[] = [];
|
const mockData: Question[] = [];
|
||||||
const types = ['single_choice', 'multiple_choice', 'true_false', 'fill_blank', 'short_answer'];
|
const types = ['single_choice', 'multiple_choice', 'true_false', 'fill_blank', 'short_answer'];
|
||||||
const difficulties = ['easy', 'medium', 'hard'];
|
const difficulties = ['easy', 'medium', 'hard'];
|
||||||
const categories = ['试题分类', '考试分类'];
|
const categories = customCategories.value.map(cat => cat.value);
|
||||||
const creators = ['王建国', '李明', '张三', '刘老师'];
|
const creators = ['王建国', '李明', '张三', '刘老师'];
|
||||||
|
|
||||||
for (let i = 1; i <= 50; i++) {
|
for (let i = 1; i <= 50; i++) {
|
||||||
@ -322,7 +465,7 @@ const loadQuestions = async () => {
|
|||||||
|
|
||||||
// 操作方法
|
// 操作方法
|
||||||
const addQuestion = () => {
|
const addQuestion = () => {
|
||||||
console.log('添加试题');
|
router.push('/teacher/exam-management/add-question');
|
||||||
};
|
};
|
||||||
|
|
||||||
const importQuestions = () => {
|
const importQuestions = () => {
|
||||||
@ -362,6 +505,142 @@ const deleteQuestion = (id: string) => {
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadQuestions();
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@ -395,6 +674,61 @@ onMounted(() => {
|
|||||||
margin-top: 20px;
|
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) {
|
@media (max-width: 1200px) {
|
||||||
.header-section {
|
.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