feat:添加添加试题功能页面;添加试题管理的分类对应功能;删除一些不再使用的组件页面;其他样式优化

This commit is contained in:
yuk255 2025-08-25 20:23:48 +08:00
parent cc7c4ec23a
commit 638e939fe5
14 changed files with 3409 additions and 1774 deletions

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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 propname
}
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>

View File

@ -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: '添加试题' }
}
]
},

View File

@ -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 || '';
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -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>

View File

@ -1,11 +0,0 @@
<template>
<div>
<h1>阅卷中心</h1>
</div>
</template>
<script setup lang="ts">
</script>
<style scoped>
</style>