merge: integrate origin/dev and resolve conflicts
This commit is contained in:
commit
4e983b923c
0
src/components/admin/CourseDetailManagement.vue
Normal file
0
src/components/admin/CourseDetailManagement.vue
Normal file
324
src/components/admin/ExamComponents/BatchSetScoreModal.vue
Normal file
324
src/components/admin/ExamComponents/BatchSetScoreModal.vue
Normal file
@ -0,0 +1,324 @@
|
||||
<template>
|
||||
<n-modal v-model:show="showModal" class="batch-score-modal" preset="card"
|
||||
:mask-closable="false" :closable="false" :style="{ width: '1000px' }">
|
||||
<div class="header">
|
||||
<span class="header-title">批量设置分数</span>
|
||||
</div>
|
||||
<n-divider />
|
||||
<div class="batch-score-content">
|
||||
<!-- 题目列表 -->
|
||||
<div class="question-list">
|
||||
<div v-for="(bigQuestion, bigIndex) in questionList" :key="bigQuestion.id"
|
||||
class="big-question-section">
|
||||
<div v-for="(subQuestion, subIndex) in bigQuestion.subQuestions" :key="subQuestion.id"
|
||||
class="question-item">
|
||||
<div class="question-info">
|
||||
<span class="question-number">{{ bigIndex + 1 }}.{{ subIndex + 1 }}</span>
|
||||
<div class="question-content">
|
||||
{{ subQuestion.title }}
|
||||
</div>
|
||||
<span class="question-type">{{ getQuestionTypeName(subQuestion.type) }}</span>
|
||||
</div>
|
||||
<div class="question-score">
|
||||
<span class="score-label">分数:</span>
|
||||
<n-input-number
|
||||
v-model:value="subQuestion.score"
|
||||
size="small"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:precision="1"
|
||||
@update:value="updateQuestionScore(bigIndex, subIndex, $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 底部按钮 -->
|
||||
<div class="modal-actions">
|
||||
<n-button strong secondary @click="cancelBatchSet">取消</n-button>
|
||||
<n-button type="info" @click="confirmBatchSet">确定</n-button>
|
||||
</div>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { createDiscreteApi } from 'naive-ui';
|
||||
|
||||
// 创建独立的 message API
|
||||
const { message } = createDiscreteApi(['message']);
|
||||
|
||||
// 题型枚举
|
||||
enum QuestionType {
|
||||
SINGLE_CHOICE = 'single_choice', // 单选题
|
||||
MULTIPLE_CHOICE = 'multiple_choice', // 多选题
|
||||
TRUE_FALSE = 'true_false', // 判断题
|
||||
FILL_BLANK = 'fill_blank', // 填空题
|
||||
SHORT_ANSWER = 'short_answer', // 简答题
|
||||
COMPOSITE = 'composite' // 复合题
|
||||
}
|
||||
|
||||
// 选择题选项接口
|
||||
interface ChoiceOption {
|
||||
id: string;
|
||||
content: string;
|
||||
isCorrect: boolean;
|
||||
}
|
||||
|
||||
// 填空题答案接口
|
||||
interface FillBlankAnswer {
|
||||
id: string;
|
||||
content: string;
|
||||
position: number;
|
||||
}
|
||||
|
||||
// 小题接口
|
||||
interface SubQuestion {
|
||||
id: string;
|
||||
type: QuestionType;
|
||||
title: string;
|
||||
score: number;
|
||||
difficulty: 'easy' | 'medium' | 'hard';
|
||||
required: boolean;
|
||||
options?: ChoiceOption[];
|
||||
correctAnswer?: string | string[];
|
||||
fillBlanks?: FillBlankAnswer[];
|
||||
trueFalseAnswer?: boolean;
|
||||
textAnswer?: string;
|
||||
answerKeywords?: string[];
|
||||
subQuestions?: SubQuestion[];
|
||||
explanation?: string;
|
||||
tags?: string[];
|
||||
createTime: string;
|
||||
}
|
||||
|
||||
// 大题接口
|
||||
interface BigQuestion {
|
||||
id: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
sort: number;
|
||||
totalScore: number;
|
||||
subQuestions: SubQuestion[];
|
||||
createTime: string;
|
||||
}
|
||||
|
||||
// Props 定义
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
questions: BigQuestion[];
|
||||
}
|
||||
|
||||
// Emits 定义
|
||||
interface Emits {
|
||||
(e: 'update:visible', value: boolean): void;
|
||||
(e: 'confirm', questions: BigQuestion[]): void;
|
||||
(e: 'cancel'): void;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
// 内部状态
|
||||
const showModal = computed({
|
||||
get: () => props.visible,
|
||||
set: (value) => emit('update:visible', value)
|
||||
});
|
||||
|
||||
// 题目列表的本地副本,避免直接修改父组件数据
|
||||
const questionList = ref<BigQuestion[]>([]);
|
||||
|
||||
// 监听 props.questions 变化,更新本地副本
|
||||
watch(() => props.questions, (newQuestions) => {
|
||||
// 深拷贝题目数据,避免直接修改原数据
|
||||
questionList.value = JSON.parse(JSON.stringify(newQuestions));
|
||||
}, { immediate: true, deep: true });
|
||||
|
||||
// 获取题型名称
|
||||
const getQuestionTypeName = (type: QuestionType): string => {
|
||||
const typeMap = {
|
||||
[QuestionType.SINGLE_CHOICE]: '单选题',
|
||||
[QuestionType.MULTIPLE_CHOICE]: '多选题',
|
||||
[QuestionType.TRUE_FALSE]: '判断题',
|
||||
[QuestionType.FILL_BLANK]: '填空题',
|
||||
[QuestionType.SHORT_ANSWER]: '简答题',
|
||||
[QuestionType.COMPOSITE]: '复合题'
|
||||
};
|
||||
return typeMap[type] || '未知题型';
|
||||
};
|
||||
|
||||
// 更新题目分数
|
||||
const updateQuestionScore = (bigIndex: number, subIndex: number, score: number) => {
|
||||
if (questionList.value[bigIndex] && questionList.value[bigIndex].subQuestions[subIndex]) {
|
||||
questionList.value[bigIndex].subQuestions[subIndex].score = score || 0;
|
||||
|
||||
// 重新计算大题总分
|
||||
const bigQuestion = questionList.value[bigIndex];
|
||||
bigQuestion.totalScore = bigQuestion.subQuestions.reduce((total, sub) => total + (sub.score || 0), 0);
|
||||
}
|
||||
};
|
||||
|
||||
// 取消批量设置
|
||||
const cancelBatchSet = () => {
|
||||
showModal.value = false;
|
||||
emit('cancel');
|
||||
};
|
||||
|
||||
// 确认批量设置
|
||||
const confirmBatchSet = () => {
|
||||
// 验证分数设置
|
||||
let hasInvalidScore = false;
|
||||
for (const bigQuestion of questionList.value) {
|
||||
for (const subQuestion of bigQuestion.subQuestions) {
|
||||
if (subQuestion.score <= 0) {
|
||||
hasInvalidScore = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (hasInvalidScore) break;
|
||||
}
|
||||
|
||||
if (hasInvalidScore) {
|
||||
message.warning('请确保所有题目分数都大于0');
|
||||
return;
|
||||
}
|
||||
|
||||
showModal.value = false;
|
||||
emit('confirm', questionList.value);
|
||||
message.success('批量设置分数成功');
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.batch-score-modal {
|
||||
--n-color: #ffffff;
|
||||
}
|
||||
|
||||
.header-title{
|
||||
color: #000;
|
||||
font-weight: 400;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.batch-score-content {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
min-height: 500px;
|
||||
}
|
||||
|
||||
.question-list {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.big-question-section {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.question-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
margin-bottom: 8px;
|
||||
background-color: #fafafa;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.question-item:hover {
|
||||
background-color: #f0f8ff;
|
||||
border-color: #d1e7dd;
|
||||
}
|
||||
|
||||
.question-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.question-number {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-right: 8px;
|
||||
min-width: 40px;
|
||||
}
|
||||
|
||||
.question-type {
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
margin-left: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.question-content {
|
||||
color: #062333;
|
||||
font-size: 14px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
/* max-width: 500px; */
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.question-score {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.score-label {
|
||||
margin-right: 8px;
|
||||
color: #062333;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
.batch-score-content::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.batch-score-content::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.batch-score-content::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.batch-score-content::-webkit-scrollbar-thumb:hover {
|
||||
background: #a8a8a8;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 600px) {
|
||||
.question-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.question-info {
|
||||
margin-right: 0;
|
||||
margin-bottom: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.question-content {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.question-score {
|
||||
align-self: flex-end;
|
||||
}
|
||||
}
|
||||
</style>
|
761
src/components/admin/ExamComponents/ExamSettingsModal.vue
Normal file
761
src/components/admin/ExamComponents/ExamSettingsModal.vue
Normal file
@ -0,0 +1,761 @@
|
||||
<template>
|
||||
<n-modal v-model:show="showModal" class="exam-settings-modal" preset="dialog" title="试卷设置" :mask-closable="false"
|
||||
:closable="true" :style="{ width: '1000px' }">
|
||||
|
||||
<div class="exam-settings-content">
|
||||
<!-- 试卷名称 -->
|
||||
<div class="setting-row">
|
||||
<label class="setting-label">试卷名称</label>
|
||||
<n-input v-model:value="formData.title" placeholder="试卷名称或试卷名称" class="setting-input" />
|
||||
</div>
|
||||
|
||||
<!-- 起止时间 -->
|
||||
<div class="setting-row">
|
||||
<label class="setting-label">起止时间</label>
|
||||
<div class="time-range">
|
||||
<n-date-picker v-model:value="formData.startTime" type="datetime" placeholder="选择考试开始时间"
|
||||
format="yyyy-MM-dd HH:mm" class="time-picker" />
|
||||
<span class="time-separator">到</span>
|
||||
<n-date-picker v-model:value="formData.endTime" type="datetime" placeholder="选择考试结束时间"
|
||||
format="yyyy-MM-dd HH:mm" class="time-picker" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 试卷分类 -->
|
||||
<div class="setting-row">
|
||||
<label class="setting-label">试卷分类</label>
|
||||
<n-radio-group v-model:value="formData.category" class="category-group">
|
||||
<n-radio value="exam">考试</n-radio>
|
||||
<n-radio value="practice">练习</n-radio>
|
||||
</n-radio-group>
|
||||
</div>
|
||||
|
||||
<!-- 答题时间 - 考试模式显示 -->
|
||||
<div v-if="formData.category === 'exam'" class="setting-row">
|
||||
<label class="setting-label">答题时间</label>
|
||||
<div class="answer-time-setting">
|
||||
<n-radio-group v-model:value="formData.timeLimit" class="time-limit-group">
|
||||
<n-radio value="limited">
|
||||
<template #default>
|
||||
<n-input v-model:value="formData.timeLimitValue" :min="0" size="small"
|
||||
style="width: 80px; margin-right: 8px;" />
|
||||
分钟
|
||||
</template>
|
||||
</n-radio>
|
||||
<n-radio value="no_limit">不限时长</n-radio>
|
||||
</n-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 答题次数 -->
|
||||
<div class="setting-row">
|
||||
<label class="setting-label">答题次数</label>
|
||||
<div class="exam-times-setting">
|
||||
<n-radio-group v-model:value="formData.examTimes" class="exam-times-group">
|
||||
<n-radio value="unlimited">无限次</n-radio>
|
||||
<n-radio value="limited">
|
||||
限
|
||||
<n-input v-model:value="formData.examTimesValue" :min="1" size="small"
|
||||
style="width: 80px; margin-left: 8px; margin-right: 8px;" />
|
||||
次
|
||||
</n-radio>
|
||||
</n-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 所属章节 -->
|
||||
<div class="setting-row">
|
||||
<label class="setting-label">所属章节</label>
|
||||
<n-select v-model:value="formData.chapter" placeholder="第一节 开始想着" :options="chapterOptions"
|
||||
class="setting-select" />
|
||||
</div>
|
||||
|
||||
<!-- 及格分数 -->
|
||||
<div class="setting-row">
|
||||
<label class="setting-label">及格分数</label>
|
||||
<div class="pass-score-setting">
|
||||
<n-input v-model:value="formData.passScore" :min="0" :max="100" size="small" style="width: 80px" />
|
||||
<span>分</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 参与学员 -->
|
||||
<div class="setting-row">
|
||||
<label class="setting-label">参与学员</label>
|
||||
<div class="participants-setting">
|
||||
<n-radio-group v-model:value="formData.participants" class="participants-group">
|
||||
<n-radio value="all">全部学员</n-radio>
|
||||
<n-radio value="by_school">仅部分学员</n-radio>
|
||||
</n-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 详细班级 - 当选择仅部分学员时显示 -->
|
||||
<div v-if="formData.participants === 'by_school'" class="setting-row">
|
||||
<label class="setting-label">详细班级</label>
|
||||
<n-select v-model:value="formData.selectedClasses" placeholder="选择班级" multiple :options="classOptions"
|
||||
class="setting-select" />
|
||||
</div>
|
||||
|
||||
<!-- 考试说明 -->
|
||||
<div class="setting-row">
|
||||
<label class="setting-label">考试说明</label>
|
||||
<n-input v-model:value="formData.instructions" type="textarea" placeholder="请填写试卷说明"
|
||||
:autosize="{ minRows: 3, maxRows: 6 }" class="setting-textarea" />
|
||||
</div>
|
||||
|
||||
<!-- 强制阅读考试说明 - 仅考试模式显示 -->
|
||||
<div v-if="formData.category === 'exam'" class="setting-row">
|
||||
<label class="setting-label"></label>
|
||||
<div class="checkbox-setting">
|
||||
<n-checkbox v-model:checked="formData.enforceInstructions">
|
||||
强制阅读考试说明
|
||||
</n-checkbox>
|
||||
<n-input v-if="formData.enforceInstructions" v-model:value="formData.readingTime" :min="1"
|
||||
size="small" style="width: 80px; margin-left: 8px; margin-right: 8px;" />
|
||||
<span v-if="formData.enforceInstructions">秒</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 高级设置 -->
|
||||
<n-collapse class="advanced-settings" arrow-placement="right">
|
||||
<n-collapse-item title="高级设置" name="advanced">
|
||||
<n-divider />
|
||||
<!-- 考试模式的高级设置 -->
|
||||
<div v-if="formData.category === 'exam'">
|
||||
<!-- 交卷设置 -->
|
||||
<div class="advanced-setting">
|
||||
<label class="advanced-label">交卷设置</label>
|
||||
<n-checkbox v-model:checked="formData.submitSettings.allowEarlySubmit">
|
||||
考试时间结束允许延时自动提交
|
||||
</n-checkbox>
|
||||
</div>
|
||||
|
||||
<!-- 限时进入 -->
|
||||
<div class="advanced-setting">
|
||||
<label class="advanced-label">限时进入</label>
|
||||
<div class="grading-settings">
|
||||
<n-input v-model:value="formData.gradingDelay" :min="0" size="small"
|
||||
style="width: 80px; margin-right: 8px;" />
|
||||
<span>分钟后不允许参加考试</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 查看分数 -->
|
||||
<div class="advanced-setting">
|
||||
<label class="advanced-label">查看分数</label>
|
||||
<n-radio-group v-model:value="formData.scoreDisplay" class="score-display-group">
|
||||
<n-radio value="show_all">允许教师批阅完试卷后学员查看分数</n-radio>
|
||||
</n-radio-group>
|
||||
</div>
|
||||
|
||||
<!-- 查看答案 -->
|
||||
<div class="advanced-setting">
|
||||
<label class="advanced-label">查看答案</label>
|
||||
<div class="answer-view-settings">
|
||||
<n-checkbox v-model:checked="formData.detailedSettings.showQuestions">
|
||||
考试截止后允许查看答案
|
||||
</n-checkbox>
|
||||
<n-checkbox v-model:checked="formData.detailedSettings.showAnalysis">
|
||||
学生提交后允许查看答案
|
||||
</n-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 展示排名 -->
|
||||
<div class="advanced-setting">
|
||||
<label class="advanced-label">展示排名</label>
|
||||
<n-checkbox v-model:checked="formData.showRanking">
|
||||
考试截止后展示排名
|
||||
</n-checkbox>
|
||||
</div>
|
||||
|
||||
<!-- 发送通知 -->
|
||||
<div class="advanced-setting">
|
||||
<label class="advanced-label">发送通知</label>
|
||||
<n-flex vertical>
|
||||
<n-switch v-model:value="formData.timerEnabled" />
|
||||
<div v-if="formData.timerEnabled" class="timer-setting">
|
||||
<span>考试结束前</span>
|
||||
<n-input v-model:value="formData.timerDuration" :min="1" size="small"
|
||||
style="width: 80px; margin: 0 8px;" />
|
||||
<span>分钟发送通知提醒未交学生</span>
|
||||
</div>
|
||||
</n-flex>
|
||||
</div>
|
||||
|
||||
<!-- 作答要求 -->
|
||||
<div class="advanced-setting">
|
||||
<label class="advanced-label">作答要求</label>
|
||||
<n-radio-group v-model:value="formData.answerType" class="answer-type-group">
|
||||
<n-radio value="auto_save">
|
||||
完成当前课程进度
|
||||
<n-input v-model:value="formData.courseProgress" :min="0" :max="100" size="small"
|
||||
style="width: 80px; margin: 0 8px;" />
|
||||
%允许考试
|
||||
</n-radio>
|
||||
</n-radio-group>
|
||||
</div>
|
||||
|
||||
<!-- 详分设置 -->
|
||||
<div class="advanced-setting">
|
||||
<label class="advanced-label">详分设置</label>
|
||||
<n-radio-group v-model:value="formData.detailScoreMode" class="detail-score-group">
|
||||
<n-radio value="question">填空题、简答题题目设为为主观题 <span class="tip">设为主观题后需教师手动批阅</span> </n-radio>
|
||||
<n-radio value="automatic">填空题、简答题不区分大小写 <span class="tip">勾选后,英文大写和小写都可以得分</span> </n-radio>
|
||||
<n-radio value="show_current">填空题、简答题忽略符号 <span class="tip">勾选后,答案内符号与标准答案不同也给分</span> </n-radio>
|
||||
<n-radio value="show_all">多选题未全选对时得一半分 <span class="tip">不勾选时全选对才给分</span> </n-radio>
|
||||
</n-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 练习模式的高级设置 -->
|
||||
<div v-if="formData.category === 'practice'">
|
||||
<!-- 重做设置 -->
|
||||
<div class="advanced-setting">
|
||||
<label class="advanced-label">重做设置</label>
|
||||
<n-flex>
|
||||
<n-radio-group v-model:value="formData.correctnessMode" class="correctness-group">
|
||||
<div class="practice-record-settings">
|
||||
<n-radio value="no_limit">允许学员练习不限次数</n-radio>
|
||||
<n-radio value="limit_wrong">
|
||||
允许学员重新练习
|
||||
<n-input v-model:value="formData.wrongLimit" :min="1" size="small"
|
||||
style="width: 80px; margin: 0 8px;" />
|
||||
次
|
||||
</n-radio>
|
||||
</div>
|
||||
</n-radio-group>
|
||||
<div class="practice-record-settings2">
|
||||
<n-checkbox v-model:checked="formData.practiceSettings.keepPreviousAnswers">
|
||||
学员重新练习保留前一次的作答记录
|
||||
</n-checkbox>
|
||||
<n-checkbox v-model:checked="formData.practiceSettings.useLastScore">
|
||||
最后一次练习成绩为最终成绩
|
||||
</n-checkbox>
|
||||
</div>
|
||||
</n-flex>
|
||||
</div>
|
||||
|
||||
<!-- 试卷设置 -->
|
||||
<div class="advanced-setting">
|
||||
<label class="advanced-label">试卷设置</label>
|
||||
<div class="practice-settings">
|
||||
<n-checkbox v-model:checked="formData.practiceSettings.showCorrectAnswer">
|
||||
允许学生主题完成前查看答案
|
||||
</n-checkbox>
|
||||
<n-checkbox v-model:checked="formData.practiceSettings.showWrongAnswer">
|
||||
每道一次练习题查看提交练习
|
||||
</n-checkbox>
|
||||
<n-checkbox v-model:checked="formData.practiceSettings.showAnalysis">
|
||||
允许学生查看排名
|
||||
</n-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-collapse-item>
|
||||
</n-collapse>
|
||||
</div>
|
||||
|
||||
<!-- 底部按钮 -->
|
||||
<template #action>
|
||||
<div class="modal-actions">
|
||||
<n-button @click="cancelSettings">取消</n-button>
|
||||
<n-button type="primary" @click="confirmSettings">确定</n-button>
|
||||
</div>
|
||||
</template>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { createDiscreteApi } from 'naive-ui';
|
||||
|
||||
// 创建独立的 message API
|
||||
const { message } = createDiscreteApi(['message']);
|
||||
|
||||
// 试卷设置表单数据接口
|
||||
interface ExamSettings {
|
||||
title: string;
|
||||
startTime: number | null;
|
||||
endTime: number | null;
|
||||
category: 'exam' | 'practice';
|
||||
timeLimit: 'unlimited' | 'limited' | 'no_limit';
|
||||
timeLimitValue: number;
|
||||
examTimes: 'unlimited' | 'limited' | 'each_day';
|
||||
examTimesValue: number;
|
||||
dailyLimit: number;
|
||||
chapter: string;
|
||||
passScore: number;
|
||||
participants: 'all' | 'by_school';
|
||||
selectedClasses: string[];
|
||||
instructions: string;
|
||||
|
||||
// 考试模式专用
|
||||
enforceOrder: boolean;
|
||||
enforceInstructions: boolean;
|
||||
readingTime: number;
|
||||
submitSettings: {
|
||||
allowEarlySubmit: boolean;
|
||||
};
|
||||
gradingDelay: number;
|
||||
scoreDisplay: 'show_all' | 'show_score' | 'hide_all';
|
||||
detailedSettings: {
|
||||
showQuestions: boolean;
|
||||
showAnalysis: boolean;
|
||||
showSubmissionTime: boolean;
|
||||
};
|
||||
timerEnabled: boolean;
|
||||
timerDuration: number;
|
||||
answerType: 'auto_save' | 'manual_save' | 'multiple_submit';
|
||||
detailScoreMode: 'question' | 'automatic' | 'show_current' | 'show_all';
|
||||
showRanking: boolean; // 展示排名
|
||||
courseProgress: number; // 作答要求的课程进度
|
||||
|
||||
// 练习模式专用
|
||||
correctnessMode: 'no_limit' | 'limit_wrong';
|
||||
wrongLimit: number;
|
||||
practiceSettings: {
|
||||
showCorrectAnswer: boolean;
|
||||
showWrongAnswer: boolean;
|
||||
showAnalysis: boolean;
|
||||
keepPreviousAnswers: boolean; // 学员重新练习保留前一次的作答记录
|
||||
useLastScore: boolean; // 最后一次练习成绩为最终成绩
|
||||
};
|
||||
paperMode: 'show_all' | 'show_current' | 'hide_all';
|
||||
}
|
||||
|
||||
// Props 定义
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
examData: ExamSettings;
|
||||
}
|
||||
|
||||
// Emits 定义
|
||||
interface Emits {
|
||||
(e: 'update:visible', value: boolean): void;
|
||||
(e: 'confirm', settings: ExamSettings): void;
|
||||
(e: 'cancel'): void;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
// 内部状态
|
||||
const showModal = computed({
|
||||
get: () => props.visible,
|
||||
set: (value) => emit('update:visible', value)
|
||||
});
|
||||
|
||||
// 表单数据的本地副本
|
||||
const formData = ref<ExamSettings>({
|
||||
title: '',
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
category: 'exam',
|
||||
timeLimit: 'limited',
|
||||
timeLimitValue: 0,
|
||||
examTimes: 'unlimited',
|
||||
examTimesValue: 1,
|
||||
dailyLimit: 1,
|
||||
chapter: '',
|
||||
passScore: 60,
|
||||
participants: 'all',
|
||||
selectedClasses: [],
|
||||
instructions: '',
|
||||
|
||||
// 考试模式专用
|
||||
enforceOrder: false,
|
||||
enforceInstructions: false,
|
||||
readingTime: 10,
|
||||
submitSettings: {
|
||||
allowEarlySubmit: true,
|
||||
},
|
||||
gradingDelay: 60,
|
||||
scoreDisplay: 'show_all',
|
||||
detailedSettings: {
|
||||
showQuestions: false,
|
||||
showAnalysis: false,
|
||||
showSubmissionTime: false,
|
||||
},
|
||||
timerEnabled: false,
|
||||
timerDuration: 10,
|
||||
answerType: 'auto_save',
|
||||
detailScoreMode: 'question',
|
||||
showRanking: false,
|
||||
courseProgress: 0,
|
||||
|
||||
// 练习模式专用
|
||||
correctnessMode: 'no_limit',
|
||||
wrongLimit: 10,
|
||||
practiceSettings: {
|
||||
showCorrectAnswer: false,
|
||||
showWrongAnswer: false,
|
||||
showAnalysis: false,
|
||||
keepPreviousAnswers: false,
|
||||
useLastScore: false,
|
||||
},
|
||||
paperMode: 'show_all',
|
||||
});
|
||||
|
||||
// 监听 props.examData 变化,更新本地副本
|
||||
watch(() => props.examData, (newData) => {
|
||||
if (newData) {
|
||||
formData.value = JSON.parse(JSON.stringify(newData));
|
||||
}
|
||||
}, { immediate: true, deep: true });
|
||||
|
||||
// 章节选项
|
||||
const chapterOptions = ref([
|
||||
{ label: '第一节 开课前准备', value: 'chapter1' },
|
||||
{ label: '第二节 课程内容', value: 'chapter2' },
|
||||
{ label: '第三节 课程总结', value: 'chapter3' },
|
||||
]);
|
||||
|
||||
// 班级选项
|
||||
const classOptions = ref([
|
||||
{ label: '全部学员', value: 'all' },
|
||||
{ label: '优班学习', value: 'class1' },
|
||||
{ label: '一班', value: 'class2' },
|
||||
]);
|
||||
|
||||
// 取消设置
|
||||
const cancelSettings = () => {
|
||||
showModal.value = false;
|
||||
emit('cancel');
|
||||
};
|
||||
|
||||
// 确认设置
|
||||
const confirmSettings = () => {
|
||||
// 验证必填字段
|
||||
if (!formData.value.title.trim()) {
|
||||
message.warning('请输入试卷名称');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!formData.value.startTime || !formData.value.endTime) {
|
||||
message.warning('请设置起止时间');
|
||||
return;
|
||||
}
|
||||
|
||||
if (formData.value.startTime >= formData.value.endTime) {
|
||||
message.warning('结束时间必须大于开始时间');
|
||||
return;
|
||||
}
|
||||
|
||||
showModal.value = false;
|
||||
emit('confirm', formData.value);
|
||||
message.success('试卷设置保存成功');
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.exam-settings-modal {
|
||||
--n-color: #ffffff;
|
||||
}
|
||||
|
||||
.exam-settings-content {
|
||||
max-height: 800px;
|
||||
overflow-y: auto;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.setting-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.setting-label {
|
||||
flex: 0 0 80px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-right: 16px;
|
||||
line-height: 32px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.setting-input,
|
||||
.setting-select,
|
||||
.setting-textarea {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.time-range {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.time-picker {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.time-separator {
|
||||
margin: 0 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.category-group {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.answer-time-setting,
|
||||
.exam-times-setting {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.time-limit-group,
|
||||
.exam-times-group {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.time-limit-input,
|
||||
.exam-times-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.exam-times-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.pass-score-setting {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.participants-group {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.advanced-setting {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.practice-settings{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.advanced-label {
|
||||
flex: 0 0 80px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-right: 16px;
|
||||
line-height: 32px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.answer-view-settings {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.timer-setting {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 8px;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
|
||||
.grading-settings {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.score-display-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.detailed-settings {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.timer-settings {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.answer-settings {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.answer-type-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.correctness-settings {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.correctness-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.wrong-limit-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.category-group {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.time-limit-group {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.exam-times-group {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.correctness-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.answer-type-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.score-display-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.detail-score-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.tip{
|
||||
margin-left: 10px;
|
||||
font-size: 12px;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.checkbox-setting {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.practice-options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.practice-record-settings {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.practice-record-settings2 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.paper-settings {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.paper-mode-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
.exam-settings-content::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.exam-settings-content::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.exam-settings-content::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.exam-settings-content::-webkit-scrollbar-thumb:hover {
|
||||
background: #a8a8a8;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 600px) {
|
||||
.setting-row {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.setting-label {
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.time-range {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.time-separator {
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
</style>
|
10
src/main.ts
10
src/main.ts
@ -34,6 +34,7 @@ import {
|
||||
NBreadcrumbItem,
|
||||
NInput,
|
||||
NInputGroup,
|
||||
NInputNumber,
|
||||
NForm,
|
||||
NFormItem,
|
||||
NSelect,
|
||||
@ -41,6 +42,7 @@ import {
|
||||
NTimePicker,
|
||||
NCheckbox,
|
||||
NRadio,
|
||||
NRadioGroup,
|
||||
NSwitch,
|
||||
NSlider,
|
||||
NRate,
|
||||
@ -82,7 +84,8 @@ import {
|
||||
NStep,
|
||||
NTimeline,
|
||||
NTimelineItem,
|
||||
NMessageProvider
|
||||
NMessageProvider,
|
||||
NPopselect
|
||||
} from 'naive-ui'
|
||||
|
||||
const naive = create({
|
||||
@ -104,6 +107,7 @@ const naive = create({
|
||||
NBreadcrumbItem,
|
||||
NInput,
|
||||
NInputGroup,
|
||||
NInputNumber,
|
||||
NForm,
|
||||
NFormItem,
|
||||
NSelect,
|
||||
@ -111,6 +115,7 @@ const naive = create({
|
||||
NTimePicker,
|
||||
NCheckbox,
|
||||
NRadio,
|
||||
NRadioGroup,
|
||||
NSwitch,
|
||||
NSlider,
|
||||
NRate,
|
||||
@ -152,7 +157,8 @@ const naive = create({
|
||||
NStep,
|
||||
NTimeline,
|
||||
NTimelineItem,
|
||||
NMessageProvider
|
||||
NMessageProvider,
|
||||
NPopselect
|
||||
]
|
||||
})
|
||||
|
||||
|
@ -51,9 +51,13 @@ import StatisticsManagement from '@/views/teacher/course/StatisticsManagement.vu
|
||||
import NotificationManagement from '@/views/teacher/course/NotificationManagement.vue'
|
||||
import GeneralManagement from '@/views/teacher/course/GeneralManagement.vue'
|
||||
|
||||
// 作业管理子组件
|
||||
// 作业子组件
|
||||
import HomeworkLibrary from '@/views/teacher/course/HomeworkLibrary.vue'
|
||||
import HomeworkReview from '@/views/teacher/course/HomeworkReview.vue'
|
||||
// 练考通菜单组件
|
||||
import ExamLibrary from '@/views/teacher/course/ExamPages/ExamLibrary.vue'
|
||||
import MarkingCenter from '@/views/teacher/course/ExamPages/MarkingCenter.vue'
|
||||
import AddExam from '@/views/teacher/course/ExamPages/AddExam.vue'
|
||||
|
||||
// ========== 路由配置 ==========
|
||||
const routes: RouteRecordRaw[] = [
|
||||
@ -148,7 +152,35 @@ const routes: RouteRecordRaw[] = [
|
||||
path: 'practice',
|
||||
name: 'PracticeManagement',
|
||||
component: PracticeManagement,
|
||||
meta: { title: '练考通管理' }
|
||||
meta: { title: '练考通管理' },
|
||||
redirect: (to) => `/teacher/course-editor/${to.params.id}/practice/exam-library`,
|
||||
children: [
|
||||
{
|
||||
path: 'exam-library',
|
||||
name: 'ExamLibrary',
|
||||
component: ExamLibrary,
|
||||
meta: {
|
||||
title: '试卷库'
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'marking-center',
|
||||
name: 'MarkingCenter',
|
||||
component: MarkingCenter,
|
||||
meta: {
|
||||
title: '阅卷中心'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'add',
|
||||
name: 'AddExam',
|
||||
component: AddExam,
|
||||
meta: {
|
||||
title: '添加试卷',
|
||||
hideSidebar: true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'question-bank',
|
||||
|
@ -1,11 +1,16 @@
|
||||
<template>
|
||||
<div class="course-editor">
|
||||
<!-- 左侧导航菜单 -->
|
||||
<div class="sidebar">
|
||||
<router-link :to="`/teacher/course-editor/${courseId}/courseware`" class="menu-item"
|
||||
:class="{ active: $route.path.includes('courseware') }">
|
||||
<img :src="$route.path.includes('courseware') ? '/images/teacher/课件-选中.png' : '/images/teacher/课件.png'"
|
||||
alt="课件" />
|
||||
<div class="sidebar" v-if="showSidebar">
|
||||
<router-link
|
||||
:to="`/teacher/course-editor/${courseId}/courseware`"
|
||||
class="menu-item"
|
||||
:class="{ active: $route.path.includes('courseware') }"
|
||||
>
|
||||
<img
|
||||
:src="$route.path.includes('courseware') ? '/images/teacher/课件-选中.png' : '/images/teacher/课件.png'"
|
||||
alt="课件"
|
||||
/>
|
||||
<span>课件</span>
|
||||
</router-link>
|
||||
<router-link :to="`/teacher/course-editor/${courseId}/chapters`" class="menu-item"
|
||||
@ -14,41 +19,63 @@
|
||||
alt="章节" />
|
||||
<span>章节</span>
|
||||
</router-link>
|
||||
<!-- 作业二级导航 -->
|
||||
<div class="menu-group">
|
||||
<div class="menu-header" @click="toggleHomework">
|
||||
<img :src="$route.path.includes('homework') ? '/images/teacher/作业-选中.png' : '/images/teacher/作业.png'"
|
||||
alt="作业" />
|
||||
<router-link
|
||||
:to="`/teacher/course-editor/${courseId}/homework`"
|
||||
class="menu-item"
|
||||
:class="{ active: $route.path.includes('homework') }"
|
||||
>
|
||||
<img
|
||||
:src="$route.path.includes('homework') ? '/images/teacher/作业-选中.png' : '/images/teacher/作业.png'"
|
||||
alt="作业"
|
||||
/>
|
||||
<span>作业</span>
|
||||
<i class="n-base-icon" :class="{ 'expanded': homeworkExpanded }">
|
||||
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M5.64645 3.14645C5.45118 3.34171 5.45118 3.65829 5.64645 3.85355L9.79289 8L5.64645 12.1464C5.45118 12.3417 5.45118 12.6583 5.64645 12.8536C5.84171 13.0488 6.15829 13.0488 6.35355 12.8536L10.8536 8.35355C11.0488 8.15829 11.0488 7.84171 10.8536 7.64645L6.35355 3.14645C6.15829 2.95118 5.84171 2.95118 5.64645 3.14645Z"
|
||||
fill="#C2C2C2"></path>
|
||||
</svg>
|
||||
</i>
|
||||
</div>
|
||||
<div class="submenu" v-show="homeworkExpanded">
|
||||
<router-link :to="`/teacher/course-editor/${courseId}/homework/library`" class="submenu-item"
|
||||
:class="{ active: $route.path.includes('homework/library') }">
|
||||
<span>作业库</span>
|
||||
</router-link>
|
||||
<router-link :to="`/teacher/course-editor/${courseId}/homework/review`" class="submenu-item"
|
||||
:class="{ active: $route.path.includes('homework/review') }">
|
||||
<span>批阅作业</span>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
<router-link :to="`/teacher/course-editor/${courseId}/practice`" class="menu-item"
|
||||
:class="{ active: $route.path.includes('practice') }">
|
||||
<img :src="$route.path.includes('practice') ? '/images/teacher/练考通-选中.png' : '/images/teacher/练考通.png'"
|
||||
alt="练考通" />
|
||||
|
||||
<!-- 练考通父菜单 -->
|
||||
<div
|
||||
class="menu-item"
|
||||
:class="{ active: $route.path.includes('practice') }"
|
||||
@click="togglePracticeMenu"
|
||||
>
|
||||
<img
|
||||
:src="($route.path.includes('practice')) ? '/images/teacher/练考通-选中.png' : '/images/teacher/练考通.png'"
|
||||
alt="练考通"
|
||||
/>
|
||||
<span>练考通</span>
|
||||
<div class="expand-icon" :class="{ expanded: practiceMenuExpanded }">
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none">
|
||||
<path d="M3 4.5L6 7.5L9 4.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 练考通子菜单 -->
|
||||
<div class="submenu" :class="{ expanded: practiceMenuExpanded }">
|
||||
<router-link
|
||||
:to="`/teacher/course-editor/${courseId}/practice/exam-library`"
|
||||
class="submenu-item"
|
||||
:class="{ active: $route.path.includes('exam-library') }"
|
||||
>
|
||||
<span>试卷库</span>
|
||||
</router-link>
|
||||
<router-link :to="`/teacher/course-editor/${courseId}/question-bank`" class="menu-item"
|
||||
:class="{ active: $route.path.includes('question-bank') }">
|
||||
<img :src="$route.path.includes('question-bank') ? '/images/teacher/题库-选中.png' : '/images/teacher/题库.png'"
|
||||
alt="题库" />
|
||||
<router-link
|
||||
:to="`/teacher/course-editor/${courseId}/practice/marking-center`"
|
||||
class="submenu-item"
|
||||
:class="{ active: $route.path.includes('marking-center') }"
|
||||
>
|
||||
<span>阅卷中心</span>
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<router-link
|
||||
:to="`/teacher/course-editor/${courseId}/question-bank`"
|
||||
class="menu-item"
|
||||
:class="{ active: $route.path.includes('question-bank') }"
|
||||
>
|
||||
<img
|
||||
:src="$route.path.includes('question-bank') ? '/images/teacher/题库-选中.png' : '/images/teacher/题库.png'"
|
||||
alt="题库"
|
||||
/>
|
||||
<span>题库</span>
|
||||
</router-link>
|
||||
<router-link :to="`/teacher/course-editor/${courseId}/certificate`" class="menu-item"
|
||||
@ -91,21 +118,34 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
// 获取课程ID
|
||||
const courseId = route.params.id
|
||||
|
||||
// 作业菜单展开状态
|
||||
const homeworkExpanded = ref(false)
|
||||
// 控制练考通菜单的展开状态
|
||||
const practiceMenuExpanded = ref(false)
|
||||
|
||||
// 切换作业菜单展开/收起
|
||||
const toggleHomework = () => {
|
||||
homeworkExpanded.value = !homeworkExpanded.value
|
||||
// 切换练考通菜单展开状态
|
||||
const togglePracticeMenu = () => {
|
||||
practiceMenuExpanded.value = !practiceMenuExpanded.value
|
||||
}
|
||||
|
||||
// 监听路由变化,如果当前路由是子菜单,自动展开父菜单
|
||||
watch(() => route.path, (newPath) => {
|
||||
if (newPath.includes('exam-library') || newPath.includes('marking-center')) {
|
||||
practiceMenuExpanded.value = true
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
|
||||
// 判断是否显示侧边菜单栏
|
||||
const showSidebar = computed(() => {
|
||||
return route.meta.hideSidebar !== true
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@ -257,7 +297,60 @@ const toggleHomework = () => {
|
||||
.submenu-item span {
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.expand-icon {
|
||||
transition: transform 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.expand-icon.expanded {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* 子菜单样式 */
|
||||
.submenu {
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.3s ease;
|
||||
margin-left: 20px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.submenu.expanded {
|
||||
max-height: 200px;
|
||||
}
|
||||
|
||||
.submenu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 20px 12px 60px;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
text-decoration: none;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
border-radius: 5px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.submenu-item:hover {
|
||||
background: #e9ecef;
|
||||
}
|
||||
|
||||
.submenu-item.active {
|
||||
background: #E3F2FD;
|
||||
color: #0288D1;
|
||||
}
|
||||
|
||||
.submenu-item span {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 右侧内容区域 */
|
||||
.content-area {
|
||||
flex: 1;
|
||||
|
1544
src/views/teacher/course/ExamPages/AddExam.vue
Normal file
1544
src/views/teacher/course/ExamPages/AddExam.vue
Normal file
File diff suppressed because it is too large
Load Diff
236
src/views/teacher/course/ExamPages/ExamLibrary.vue
Normal file
236
src/views/teacher/course/ExamPages/ExamLibrary.vue
Normal file
@ -0,0 +1,236 @@
|
||||
<template>
|
||||
<n-config-provider :theme-overrides="themeOverrides">
|
||||
<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>
|
||||
</n-config-provider>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h, ref, VNode } from 'vue';
|
||||
import { NButton, NSpace, useMessage, NDataTable, NPagination, NInput, NConfigProvider } from 'naive-ui';
|
||||
import type { DataTableColumns, GlobalThemeOverrides } 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 themeOverrides: GlobalThemeOverrides = {
|
||||
common: {
|
||||
primaryColor: '#0288D1',
|
||||
primaryColorHover: '#0277BD', // A slightly darker shade for hover
|
||||
primaryColorPressed: '#01579B', // A darker shade for pressed
|
||||
errorColor: '#FF4D4F',
|
||||
errorColorHover: '#E54547',
|
||||
errorColorPressed: '#C0383A',
|
||||
},
|
||||
Button: {
|
||||
// For ghost primary buttons
|
||||
textColorGhostPrimary: '#0288D1',
|
||||
borderPrimary: '1px solid #0288D1',
|
||||
// For ghost error buttons
|
||||
textColorGhostError: '#FF4D4F',
|
||||
borderError: '1px solid #FF4D4F',
|
||||
},
|
||||
Pagination: {
|
||||
itemColorActive: '#0288D1',
|
||||
itemTextColorActive: '#fff',
|
||||
itemBorderActive: '1px solid #0288D1',
|
||||
itemColorActiveHover: '#0277BD', // 使用比主色稍深一点的颜色
|
||||
itemTextColorActiveHover: '#fff', // 保持文字为白色
|
||||
itemBorderActiveHover: '1px solid #0277BD' // 边框颜色也相应加深
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
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>
|
11
src/views/teacher/course/ExamPages/MarkingCenter.vue
Normal file
11
src/views/teacher/course/ExamPages/MarkingCenter.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>阅卷中心</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
@ -1,9 +1,6 @@
|
||||
<template>
|
||||
<div class="practice-management">
|
||||
<div class="content-placeholder">
|
||||
<h2>练考通管理</h2>
|
||||
<p>练考通管理功能正在开发中...</p>
|
||||
</div>
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -17,20 +14,4 @@
|
||||
background: #fff;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.content-placeholder {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
}
|
||||
|
||||
.content-placeholder h2 {
|
||||
font-size: 24px;
|
||||
color: #333;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.content-placeholder p {
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
|
Loading…
x
Reference in New Issue
Block a user