Merge branch 'dev' of http://110.42.96.65:19890/GoCo/OL-LearnPlatform-Frontend into dev
# Conflicts: # src/views/Faculty.vue
This commit is contained in:
commit
bc5d21fc76
BIN
public/images/teacher/发布人.png
Normal file
BIN
public/images/teacher/发布人.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 831 B |
BIN
public/images/teacher/起点时间.png
Normal file
BIN
public/images/teacher/起点时间.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 668 B |
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
|
||||
]
|
||||
})
|
||||
|
||||
|
@ -43,7 +43,6 @@ import CourseCreate from '@/components/admin/CourseComponents/CourseCreate.vue'
|
||||
import CourseEditor from '@/views/teacher/course/CourseEditor.vue'
|
||||
import CoursewareManagement from '@/views/teacher/course/CoursewareManagement.vue'
|
||||
import ChapterManagement from '@/views/teacher/course/ChapterManagement.vue'
|
||||
import HomeworkManagement from '@/views/teacher/course/HomeworkManagement.vue'
|
||||
import PracticeManagement from '@/views/teacher/course/PracticeManagement.vue'
|
||||
import QuestionBankManagement from '@/views/teacher/course/QuestionBankManagement.vue'
|
||||
import CertificateManagement from '@/views/teacher/course/CertificateManagement.vue'
|
||||
@ -52,6 +51,14 @@ 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[] = [
|
||||
// 管理后台路由
|
||||
@ -125,14 +132,55 @@ const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: 'homework',
|
||||
name: 'HomeworkManagement',
|
||||
component: HomeworkManagement,
|
||||
meta: { title: '作业管理' }
|
||||
redirect: (to) => `/teacher/course-editor/${to.params.id}/homework/library`,
|
||||
children: [
|
||||
{
|
||||
path: 'library',
|
||||
name: 'HomeworkLibrary',
|
||||
component: HomeworkLibrary,
|
||||
meta: { title: '作业库' }
|
||||
},
|
||||
{
|
||||
path: 'review',
|
||||
name: 'HomeworkReview',
|
||||
component: HomeworkReview,
|
||||
meta: { title: '批阅作业' }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
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',
|
||||
|
@ -128,7 +128,7 @@ const navigateToTeacherDetail = (teacherId: number) => {
|
||||
router.push(`/teacher/${teacherId}`)
|
||||
}
|
||||
|
||||
// 处理箭头点击(保留以防后续需要)
|
||||
// 处理箭头点击
|
||||
// const toggleCourseInfo = (teacherId: number) => {
|
||||
// if (expandedTeacherId.value === teacherId) {
|
||||
// expandedTeacherId.value = null
|
||||
@ -143,7 +143,7 @@ const showCourseInfo = (teacherId: number) => {
|
||||
}
|
||||
|
||||
// 鼠标离开隐藏课程信息
|
||||
const hideCourseInfo = (_teacherId: number) => {
|
||||
const hideCourseInfo = (_: number) => {
|
||||
expandedTeacherId.value = null
|
||||
}
|
||||
|
||||
|
@ -1,36 +1,786 @@
|
||||
<template>
|
||||
<div class="chapter-management">
|
||||
<div class="content-placeholder">
|
||||
<h2>章节管理</h2>
|
||||
<p>章节管理功能正在开发中...</p>
|
||||
<!-- 顶部操作栏 -->
|
||||
<div class="toolbar">
|
||||
<h2>全部章节</h2>
|
||||
<div class="toolbar-actions">
|
||||
<button class="btn btn-primary" @click="addChapter">添加章节</button>
|
||||
<button class="btn btn-new" @click="importChapters">导入</button>
|
||||
<button class="btn btn-new" @click="exportChapters">导出</button>
|
||||
<button class="btn btn-danger" @click="deleteSelected" :disabled="selectedChapters.length === 0">删除</button>
|
||||
<div class="search-box">
|
||||
<input type="text" placeholder="请输入想要搜索的内容" v-model="searchKeyword" />
|
||||
<button class="btn btn-search" @click="searchChapters">搜索</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 章节列表表格 -->
|
||||
<div class="table-box">
|
||||
<n-config-provider :locale="zhCN" :date-locale="dateZhCN">
|
||||
<n-data-table :columns="columns" :data="paginatedChapters" :row-key="rowKey"
|
||||
:checked-row-keys="selectedChapters" @update:checked-row-keys="handleCheck" :bordered="false"
|
||||
:single-line="false" size="medium" class="chapter-data-table" :row-class-name="rowClassName" />
|
||||
</n-config-provider>
|
||||
|
||||
<!-- 自定义分页器 -->
|
||||
<div class="custom-pagination">
|
||||
<div class="pagination-content">
|
||||
<div class="page-numbers">
|
||||
<span class="page-number nav-button" :class="{ disabled: currentPage === 1 }" @click="goToPage('first')">
|
||||
首页
|
||||
</span>
|
||||
<span class="page-number nav-button" :class="{ disabled: currentPage === 1 }" @click="goToPage('prev')">
|
||||
上一页
|
||||
</span>
|
||||
|
||||
<span v-for="page in visiblePages" :key="page" class="page-number page-number-bordered"
|
||||
:class="{ active: page === currentPage }" @click="goToPage(page)">
|
||||
{{ page }}
|
||||
</span>
|
||||
|
||||
<span v-if="showRightEllipsis" class="page-number">...</span>
|
||||
<span v-if="totalPages > 1" class="page-number page-number-bordered" @click="goToPage(totalPages)">
|
||||
{{ totalPages }}
|
||||
</span>
|
||||
|
||||
<span class="page-number nav-button" :class="{ disabled: currentPage === totalPages }"
|
||||
@click="goToPage('next')">
|
||||
下一页
|
||||
</span>
|
||||
<span class="page-number nav-button" :class="{ disabled: currentPage === totalPages }"
|
||||
@click="goToPage('last')">
|
||||
尾页
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// 章节管理逻辑
|
||||
import { ref, computed, h } from 'vue'
|
||||
import { NButton, useMessage, NDataTable, NConfigProvider, zhCN, dateZhCN } from 'naive-ui'
|
||||
import type { DataTableColumns } from 'naive-ui'
|
||||
|
||||
const message = useMessage()
|
||||
|
||||
// 章节类型定义
|
||||
interface Chapter {
|
||||
id: number
|
||||
name: string
|
||||
type: string
|
||||
sort: string | number
|
||||
createTime: string
|
||||
isParent: boolean
|
||||
children?: Chapter[]
|
||||
expanded?: boolean
|
||||
}
|
||||
|
||||
// 搜索关键词
|
||||
const searchKeyword = ref('')
|
||||
|
||||
// 选中的章节
|
||||
const selectedChapters = ref<number[]>([])
|
||||
|
||||
// 章节列表数据
|
||||
const chapterList = ref<Chapter[]>([
|
||||
{
|
||||
id: 1,
|
||||
name: '第一章 课前准备',
|
||||
type: '-',
|
||||
sort: '-',
|
||||
createTime: '2025.07.25 09:20',
|
||||
isParent: true,
|
||||
expanded: false,
|
||||
children: [
|
||||
{
|
||||
id: 2,
|
||||
name: '开课彩蛋:新开始新征程',
|
||||
type: '视频',
|
||||
sort: 1,
|
||||
createTime: '2025.07.25 09:20',
|
||||
isParent: false
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '课件准备PPT',
|
||||
type: '课件',
|
||||
sort: 2,
|
||||
createTime: '2025.07.25 09:20',
|
||||
isParent: false
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: '第一节 课程定位与目标',
|
||||
type: '视频',
|
||||
sort: 3,
|
||||
createTime: '2025.07.25 09:20',
|
||||
isParent: false
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: '第二节 教学安排及学习建议',
|
||||
type: '作业',
|
||||
sort: 4,
|
||||
createTime: '2025.07.25 09:20',
|
||||
isParent: false
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: '第三节 教学安排及学习建议',
|
||||
type: '考试',
|
||||
sort: 5,
|
||||
createTime: '2025.07.25 09:20',
|
||||
isParent: false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: '第二章 课前准备',
|
||||
type: '-',
|
||||
sort: '-',
|
||||
createTime: '2025.07.25 09:20',
|
||||
isParent: true,
|
||||
expanded: false,
|
||||
children: [
|
||||
{
|
||||
id: 8,
|
||||
name: '第一节 新开始新征程',
|
||||
type: '视频',
|
||||
sort: 1,
|
||||
createTime: '2025.07.25 09:20',
|
||||
isParent: false
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
name: '第二节 教学安排及学习建议',
|
||||
type: '课件',
|
||||
sort: 2,
|
||||
createTime: '2025.07.25 09:20',
|
||||
isParent: false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
name: '第三章 课前准备',
|
||||
type: '-',
|
||||
sort: '-',
|
||||
createTime: '2025.07.25 09:20',
|
||||
isParent: true,
|
||||
expanded: false,
|
||||
children: [
|
||||
{
|
||||
id: 12,
|
||||
name: '第一节 新开始新征程',
|
||||
type: '视频',
|
||||
sort: 1,
|
||||
createTime: '2025.07.25 09:20',
|
||||
isParent: false
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
name: '第二节 教学安排及学习建议',
|
||||
type: '课件',
|
||||
sort: 2,
|
||||
createTime: '2025.07.25 09:20',
|
||||
isParent: false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
name: '第四章 课前准备',
|
||||
type: '-',
|
||||
sort: '-',
|
||||
createTime: '2025.07.25 09:20',
|
||||
isParent: true,
|
||||
expanded: false,
|
||||
children: []
|
||||
}
|
||||
])
|
||||
|
||||
// 扁平化章节列表(用于显示和分页)
|
||||
const flattenedChapters = computed(() => {
|
||||
const result: Chapter[] = []
|
||||
|
||||
const flatten = (chapters: Chapter[]) => {
|
||||
chapters.forEach(chapter => {
|
||||
result.push(chapter)
|
||||
if (chapter.children && chapter.expanded) {
|
||||
flatten(chapter.children)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
flatten(chapterList.value)
|
||||
return result
|
||||
})
|
||||
|
||||
// 过滤后的章节列表
|
||||
const filteredChapters = computed(() => {
|
||||
if (!searchKeyword.value) {
|
||||
return flattenedChapters.value
|
||||
}
|
||||
return flattenedChapters.value.filter((chapter: Chapter) =>
|
||||
chapter.name.toLowerCase().includes(searchKeyword.value.toLowerCase())
|
||||
)
|
||||
})
|
||||
|
||||
// 分页相关状态
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const totalPages = computed(() => Math.ceil(filteredChapters.value.length / pageSize.value))
|
||||
|
||||
// 可见的页码范围
|
||||
const visiblePages = computed(() => {
|
||||
const pages = []
|
||||
const current = currentPage.value
|
||||
const total = totalPages.value
|
||||
|
||||
if (total <= 7) {
|
||||
// 如果总页数小于等于7,显示所有页码
|
||||
for (let i = 1; i <= total; i++) {
|
||||
pages.push(i)
|
||||
}
|
||||
} else {
|
||||
// 显示当前页附近的页码
|
||||
const start = Math.max(1, current - 2)
|
||||
const end = Math.min(total, current + 2)
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
pages.push(i)
|
||||
}
|
||||
}
|
||||
|
||||
return pages
|
||||
})
|
||||
|
||||
// 是否显示右侧省略号
|
||||
const showRightEllipsis = computed(() => {
|
||||
return currentPage.value < totalPages.value - 3
|
||||
})
|
||||
|
||||
// 分页后的数据
|
||||
const paginatedChapters = computed(() => {
|
||||
const start = (currentPage.value - 1) * pageSize.value
|
||||
const end = start + pageSize.value
|
||||
return filteredChapters.value.slice(start, end)
|
||||
})
|
||||
|
||||
// 表格行键
|
||||
const rowKey = (row: Chapter) => row.id
|
||||
|
||||
// 表格选择处理
|
||||
const handleCheck = (rowKeys: number[]) => {
|
||||
selectedChapters.value = rowKeys
|
||||
}
|
||||
|
||||
// 行样式名称
|
||||
const rowClassName = () => {
|
||||
return 'chapter-table-row'
|
||||
}
|
||||
|
||||
// 获取章节图标
|
||||
// const getChapterIcon = (type: string) => {
|
||||
// return '/images/teacher/章节.png'
|
||||
// }
|
||||
|
||||
// 获取类型图标
|
||||
// const getTypeIcon = (type: string) => {
|
||||
// const iconMap: { [key: string]: string } = {
|
||||
// '视频': '/images/teacher/视频.png',
|
||||
// '课件': '/images/teacher/课件.png',
|
||||
// '作业': '/images/teacher/作业.png',
|
||||
// '考试': '/images/teacher/考试.png'
|
||||
// }
|
||||
// return iconMap[type] || '/images/teacher/默认.png'
|
||||
// }
|
||||
|
||||
// 分页方法
|
||||
const goToPage = (page: string | number) => {
|
||||
if (typeof page === 'string') {
|
||||
switch (page) {
|
||||
case 'first':
|
||||
currentPage.value = 1
|
||||
break
|
||||
case 'prev':
|
||||
if (currentPage.value > 1) currentPage.value--
|
||||
break
|
||||
case 'next':
|
||||
if (currentPage.value < totalPages.value) currentPage.value++
|
||||
break
|
||||
case 'last':
|
||||
currentPage.value = totalPages.value
|
||||
break
|
||||
}
|
||||
} else {
|
||||
currentPage.value = page
|
||||
}
|
||||
}
|
||||
|
||||
// 展开/收起章节
|
||||
const toggleChapter = (chapter: Chapter) => {
|
||||
if (chapter.isParent && chapter.children) {
|
||||
chapter.expanded = !chapter.expanded
|
||||
}
|
||||
}
|
||||
|
||||
// 章节操作方法
|
||||
const addChapter = () => {
|
||||
message.info('添加章节功能')
|
||||
}
|
||||
|
||||
const importChapters = () => {
|
||||
message.info('导入章节功能')
|
||||
}
|
||||
|
||||
const exportChapters = () => {
|
||||
message.info('导出章节功能')
|
||||
}
|
||||
|
||||
const deleteSelected = () => {
|
||||
if (selectedChapters.value.length === 0) return
|
||||
if (confirm(`确定要删除选中的 ${selectedChapters.value.length} 个章节吗?`)) {
|
||||
selectedChapters.value.forEach((id: number) => {
|
||||
const index = chapterList.value.findIndex((c: Chapter) => c.id === id)
|
||||
if (index > -1) {
|
||||
chapterList.value.splice(index, 1)
|
||||
}
|
||||
})
|
||||
selectedChapters.value = []
|
||||
message.success('删除成功')
|
||||
}
|
||||
}
|
||||
|
||||
const searchChapters = () => {
|
||||
message.info('搜索章节: ' + searchKeyword.value)
|
||||
currentPage.value = 1
|
||||
}
|
||||
|
||||
const editChapter = (chapter: Chapter) => {
|
||||
message.info('编辑章节: ' + chapter.name)
|
||||
}
|
||||
|
||||
const deleteChapter = (chapter: Chapter) => {
|
||||
if (confirm('确定要删除这个章节吗?')) {
|
||||
const index = chapterList.value.findIndex((c: Chapter) => c.id === chapter.id)
|
||||
if (index > -1) {
|
||||
chapterList.value.splice(index, 1)
|
||||
message.success('删除成功')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 表格列配置
|
||||
const columns: DataTableColumns<Chapter> = [
|
||||
{
|
||||
type: 'selection'
|
||||
},
|
||||
{
|
||||
title: '章节名称',
|
||||
key: 'name',
|
||||
minWidth: 350,
|
||||
render: (row: Chapter) => {
|
||||
return h('div', {
|
||||
style: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '20px',
|
||||
cursor: row.isParent ? 'pointer' : 'default',
|
||||
marginLeft: row.isParent ? '40px' : '12px'
|
||||
},
|
||||
onClick: row.isParent ? () => toggleChapter(row) : undefined
|
||||
}, [
|
||||
row.isParent ? h('i', {
|
||||
class: 'n-base-icon',
|
||||
style: {
|
||||
transition: 'transform 0.2s',
|
||||
transform: row.expanded ? 'rotate(90deg)' : 'rotate(0deg)',
|
||||
cursor: 'pointer'
|
||||
}
|
||||
}, [
|
||||
h('svg', {
|
||||
viewBox: '0 0 16 16',
|
||||
fill: 'none',
|
||||
xmlns: 'http://www.w3.org/2000/svg'
|
||||
}, [
|
||||
h('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: 'currentColor'
|
||||
})
|
||||
])
|
||||
]) : h('div', { style: { width: '16px' } }),
|
||||
h('span', {
|
||||
style: {
|
||||
color: row.isParent ? '#062333' : '#666666',
|
||||
fontSize: '14px',
|
||||
fontWeight: row.isParent ? '500' : 'normal',
|
||||
marginLeft: row.isParent ? '0' : '24px'
|
||||
}
|
||||
}, row.name)
|
||||
])
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '类型',
|
||||
key: 'type',
|
||||
width: 156,
|
||||
render: (row: Chapter) => {
|
||||
if (row.type === '-') {
|
||||
return h('span', { style: { color: '#BABABA' } }, '-')
|
||||
}
|
||||
return h('div', {
|
||||
style: {
|
||||
display: 'inline-block',
|
||||
padding: '4px 8px',
|
||||
backgroundColor: 'transparent',
|
||||
border: '1px solid #BABABA',
|
||||
borderRadius: '4px',
|
||||
fontSize: '12px',
|
||||
color: '#BABABA'
|
||||
}
|
||||
}, row.type)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '排序',
|
||||
key: 'sort',
|
||||
width: 120,
|
||||
render: (row: Chapter) => {
|
||||
return h('span', { style: { color: '#062333', fontSize: '12px' } }, row.sort)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
key: 'createTime',
|
||||
width: 235,
|
||||
render: (row: Chapter) => {
|
||||
return h('span', { style: { color: '#062333', fontSize: '12px' } }, row.createTime)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 200,
|
||||
render: (row: Chapter) => {
|
||||
return h('div', { style: { display: 'flex', gap: '16px', alignItems: 'center', justifyContent: 'center' } }, [
|
||||
h(NButton, {
|
||||
size: 'small',
|
||||
type: 'info',
|
||||
secondary: true,
|
||||
onClick: () => editChapter(row)
|
||||
}, { default: () => '编辑' }),
|
||||
h(NButton, {
|
||||
size: 'small',
|
||||
type: 'error',
|
||||
secondary: true,
|
||||
onClick: () => deleteChapter(row)
|
||||
}, { default: () => '删除' })
|
||||
])
|
||||
}
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chapter-management {
|
||||
padding: 20px;
|
||||
background: #fff;
|
||||
height: 100%;
|
||||
width: 1293px;
|
||||
background: #fff;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* 顶部工具栏 */
|
||||
.toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-right: 25px;
|
||||
background: #fff;
|
||||
padding: 30px 0 20px 30px;
|
||||
border-bottom: 2px solid #F6F6F6;
|
||||
}
|
||||
|
||||
.toolbar h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.toolbar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 7px 16px;
|
||||
border: none;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #0288D1;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #40a9ff;
|
||||
}
|
||||
|
||||
.btn-new {
|
||||
background-color: #fff;
|
||||
border: 1px solid #0288D1;
|
||||
color: #0288D1;
|
||||
}
|
||||
|
||||
.btn-default {
|
||||
background: white;
|
||||
color: #999999;
|
||||
border: 1px solid #999999;
|
||||
}
|
||||
|
||||
.btn-default:hover {
|
||||
border-color: #1890ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: white;
|
||||
color: #FF4D4F;
|
||||
border: 1px solid #FF4D4F;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #ff7875;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px solid #F1F3F4;
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.search-box input {
|
||||
border: none;
|
||||
padding: 6px 12px;
|
||||
outline: none;
|
||||
width: 200px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.btn-search {
|
||||
background: #0288D1;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 6px 16px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.btn-search:hover {
|
||||
background: #0277bd;
|
||||
}
|
||||
|
||||
.table-box {
|
||||
height: 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Naive UI 表格样式定制 */
|
||||
:deep(.chapter-data-table) {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 40px;
|
||||
height: 100%;
|
||||
min-height: 500px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 表格头部样式 */
|
||||
:deep(.chapter-data-table .n-data-table-thead) {
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
:deep(.chapter-data-table .n-data-table-th) {
|
||||
background: #fafafa;
|
||||
font-weight: 500;
|
||||
color: #062333;
|
||||
font-size: 14px;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
padding: 12px 8px;
|
||||
text-align: center;
|
||||
|
||||
}
|
||||
|
||||
/* 表格行样式 */
|
||||
:deep(.chapter-data-table .n-data-table-td) {
|
||||
font-size: 13px;
|
||||
color: #062333;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
padding: 12px 8px;
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 所有列居中对齐 */
|
||||
:deep(.chapter-data-table .n-data-table-td) {
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
:deep(.chapter-data-table .n-data-table-th) {
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
:deep(.chapter-data-table .n-data-table-tr:hover) {
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
/* 复选框样式 */
|
||||
:deep(.chapter-data-table .n-checkbox) {
|
||||
--n-size: 16px;
|
||||
}
|
||||
|
||||
/* 隐藏Naive UI默认的展开触发器 */
|
||||
:deep(.chapter-data-table .n-data-table-expand-trigger) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* 隐藏Naive UI默认的展开占位符 */
|
||||
:deep(.chapter-data-table .n-data-table-expand-placeholder) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* 按钮组样式调整 */
|
||||
:deep(.chapter-data-table .n-button) {
|
||||
font-size: 12px;
|
||||
height: 28px;
|
||||
padding: 0 12px;
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
/* 操作按钮样式 - 只有边框和文字颜色,无背景色 */
|
||||
:deep(.chapter-data-table .n-button--info-type.n-button--secondary) {
|
||||
background-color: transparent !important;
|
||||
border: 1px solid #0288D1;
|
||||
color: #0288D1;
|
||||
}
|
||||
|
||||
:deep(.chapter-data-table .n-button--info-type.n-button--secondary:hover) {
|
||||
background-color: rgba(32, 128, 240, 0.05) !important;
|
||||
border: 1px solid #0288D1;
|
||||
color: #248DD3;
|
||||
}
|
||||
|
||||
:deep(.chapter-data-table .n-button--error-type.n-button--secondary) {
|
||||
background-color: transparent !important;
|
||||
border: 1px solid #FF4D4F;
|
||||
color: #FD8485;
|
||||
}
|
||||
|
||||
:deep(.chapter-data-table .n-button--error-type.n-button--secondary:hover) {
|
||||
background-color: rgba(208, 48, 80, 0.05) !important;
|
||||
border: 1px solid #FF4D4F;
|
||||
color: #FD8485;
|
||||
}
|
||||
|
||||
/* 表格行样式 */
|
||||
:deep(.chapter-table-row) {
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
:deep(.chapter-table-row:hover) {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
/* 自定义分页器样式 */
|
||||
.custom-pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
background: #fff;
|
||||
padding: 20px 0;
|
||||
margin-top: auto;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.pagination-content {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 0 auto;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.page-numbers {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.page-number {
|
||||
display: inline-block;
|
||||
min-width: 38px;
|
||||
height: 38px;
|
||||
line-height: 38px;
|
||||
text-align: center;
|
||||
color: #333;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
padding: 0 5px;
|
||||
margin: 0 4px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.page-number-bordered {
|
||||
border: 1px solid #d9d9d9;
|
||||
}
|
||||
|
||||
.page-number.active {
|
||||
background-color: #0088D1;
|
||||
color: white;
|
||||
border-color: #0088D1;
|
||||
}
|
||||
|
||||
.page-number:hover:not(.disabled) {
|
||||
color: #0088D1;
|
||||
border-color: #0088D1;
|
||||
}
|
||||
|
||||
.page-number.disabled {
|
||||
color: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.page-number.disabled:hover {
|
||||
color: #ccc;
|
||||
border-color: #d9d9d9;
|
||||
}
|
||||
|
||||
.nav-button {
|
||||
padding: 0 8px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.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;
|
||||
.nav-button:hover:not(.disabled) {
|
||||
color: #0088D1;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="course-editor">
|
||||
<!-- 左侧导航菜单 -->
|
||||
<div class="sidebar">
|
||||
<div class="sidebar" v-if="showSidebar">
|
||||
<router-link
|
||||
:to="`/teacher/course-editor/${courseId}/courseware`"
|
||||
class="menu-item"
|
||||
@ -13,15 +13,10 @@
|
||||
/>
|
||||
<span>课件</span>
|
||||
</router-link>
|
||||
<router-link
|
||||
:to="`/teacher/course-editor/${courseId}/chapters`"
|
||||
class="menu-item"
|
||||
:class="{ active: $route.path.includes('chapters') }"
|
||||
>
|
||||
<img
|
||||
:src="$route.path.includes('chapters') ? '/images/teacher/章节-选中.png' : '/images/teacher/章节.png'"
|
||||
alt="章节"
|
||||
/>
|
||||
<router-link :to="`/teacher/course-editor/${courseId}/chapters`" class="menu-item"
|
||||
:class="{ active: $route.path.includes('chapters') }">
|
||||
<img :src="$route.path.includes('chapters') ? '/images/teacher/章节-选中.png' : '/images/teacher/章节.png'"
|
||||
alt="章节" />
|
||||
<span>章节</span>
|
||||
</router-link>
|
||||
<router-link
|
||||
@ -35,17 +30,43 @@
|
||||
/>
|
||||
<span>作业</span>
|
||||
</router-link>
|
||||
<router-link
|
||||
:to="`/teacher/course-editor/${courseId}/practice`"
|
||||
|
||||
<!-- 练考通父菜单 -->
|
||||
<div
|
||||
class="menu-item"
|
||||
:class="{ active: $route.path.includes('practice') }"
|
||||
@click="togglePracticeMenu"
|
||||
>
|
||||
<img
|
||||
:src="$route.path.includes('practice') ? '/images/teacher/练考通-选中.png' : '/images/teacher/练考通.png'"
|
||||
:src="($route.path.includes('practice')) ? '/images/teacher/练考通-选中.png' : '/images/teacher/练考通.png'"
|
||||
alt="练考通"
|
||||
/>
|
||||
<span>练考通</span>
|
||||
</router-link>
|
||||
<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}/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"
|
||||
@ -57,59 +78,34 @@
|
||||
/>
|
||||
<span>题库</span>
|
||||
</router-link>
|
||||
<router-link
|
||||
:to="`/teacher/course-editor/${courseId}/certificate`"
|
||||
class="menu-item"
|
||||
:class="{ active: $route.path.includes('certificate') }"
|
||||
>
|
||||
<img
|
||||
:src="$route.path.includes('certificate') ? '/images/teacher/证书-选中.png' : '/images/teacher/证书.png'"
|
||||
alt="证书"
|
||||
/>
|
||||
<router-link :to="`/teacher/course-editor/${courseId}/certificate`" class="menu-item"
|
||||
:class="{ active: $route.path.includes('certificate') }">
|
||||
<img :src="$route.path.includes('certificate') ? '/images/teacher/证书-选中.png' : '/images/teacher/证书.png'"
|
||||
alt="证书" />
|
||||
<span>证书</span>
|
||||
</router-link>
|
||||
<router-link
|
||||
:to="`/teacher/course-editor/${courseId}/discussion`"
|
||||
class="menu-item"
|
||||
:class="{ active: $route.path.includes('discussion') }"
|
||||
>
|
||||
<img
|
||||
:src="$route.path.includes('discussion') ? '/images/teacher/讨论-选中.png' : '/images/teacher/讨论.png'"
|
||||
alt="讨论"
|
||||
/>
|
||||
<router-link :to="`/teacher/course-editor/${courseId}/discussion`" class="menu-item"
|
||||
:class="{ active: $route.path.includes('discussion') }">
|
||||
<img :src="$route.path.includes('discussion') ? '/images/teacher/讨论-选中.png' : '/images/teacher/讨论.png'"
|
||||
alt="讨论" />
|
||||
<span>讨论</span>
|
||||
</router-link>
|
||||
<router-link
|
||||
:to="`/teacher/course-editor/${courseId}/statistics`"
|
||||
class="menu-item"
|
||||
:class="{ active: $route.path.includes('statistics') }"
|
||||
>
|
||||
<img
|
||||
:src="$route.path.includes('statistics') ? '/images/teacher/统计-选中.png' : '/images/teacher/统计.png'"
|
||||
alt="统计"
|
||||
/>
|
||||
<router-link :to="`/teacher/course-editor/${courseId}/statistics`" class="menu-item"
|
||||
:class="{ active: $route.path.includes('statistics') }">
|
||||
<img :src="$route.path.includes('statistics') ? '/images/teacher/统计-选中.png' : '/images/teacher/统计.png'"
|
||||
alt="统计" />
|
||||
<span>统计</span>
|
||||
</router-link>
|
||||
<router-link
|
||||
:to="`/teacher/course-editor/${courseId}/notification`"
|
||||
class="menu-item"
|
||||
:class="{ active: $route.path.includes('notification') }"
|
||||
>
|
||||
<img
|
||||
:src="$route.path.includes('notification') ? '/images/teacher/通知-选中.png' : '/images/teacher/通知.png'"
|
||||
alt="通知"
|
||||
/>
|
||||
<router-link :to="`/teacher/course-editor/${courseId}/notification`" class="menu-item"
|
||||
:class="{ active: $route.path.includes('notification') }">
|
||||
<img :src="$route.path.includes('notification') ? '/images/teacher/通知-选中.png' : '/images/teacher/通知.png'"
|
||||
alt="通知" />
|
||||
<span>通知</span>
|
||||
</router-link>
|
||||
<router-link
|
||||
:to="`/teacher/course-editor/${courseId}/management`"
|
||||
class="menu-item"
|
||||
:class="{ active: $route.path.includes('management') }"
|
||||
>
|
||||
<img
|
||||
:src="$route.path.includes('management') ? '/images/teacher/管理-选中.png' : '/images/teacher/管理.png'"
|
||||
alt="管理"
|
||||
/>
|
||||
<router-link :to="`/teacher/course-editor/${courseId}/management`" class="menu-item"
|
||||
:class="{ active: $route.path.includes('management') }">
|
||||
<img :src="$route.path.includes('management') ? '/images/teacher/管理-选中.png' : '/images/teacher/管理.png'"
|
||||
alt="管理" />
|
||||
<span>管理</span>
|
||||
</router-link>
|
||||
</div>
|
||||
@ -122,12 +118,34 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
// 获取课程ID
|
||||
const courseId = route.params.id
|
||||
|
||||
// 控制练考通菜单的展开状态
|
||||
const practiceMenuExpanded = ref(false)
|
||||
|
||||
// 切换练考通菜单展开状态
|
||||
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>
|
||||
@ -178,14 +196,159 @@ const courseId = route.params.id
|
||||
.menu-item img {
|
||||
margin-left: 40px;
|
||||
margin-top: 1px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.menu-item span {
|
||||
font-size: 18px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 作业二级导航样式 */
|
||||
.menu-group {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.menu-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 15px 20px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
border-left: 3px solid transparent;
|
||||
font-size: 18px;
|
||||
color: #666;
|
||||
border-radius: 5px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.menu-header:hover {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.menu-header img {
|
||||
margin-left: 40px;
|
||||
margin-top: 1px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.menu-header span {
|
||||
font-size: 18px;
|
||||
color: #666;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.menu-header .n-base-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
transition: transform 0.2s ease;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.menu-header .n-base-icon.expanded {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.submenu {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.submenu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 20px;
|
||||
margin-bottom: 2px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
border-left: 3px solid transparent;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.submenu-item::before {
|
||||
content: '';
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin-left: 40px;
|
||||
margin-right: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.submenu-item:hover {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.submenu-item.active {
|
||||
background: #F5F8FB;
|
||||
color: #0288D1;
|
||||
}
|
||||
|
||||
.submenu-item.active span {
|
||||
color: #0288D1;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
/* 右侧内容区域 */
|
||||
|
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>
|
720
src/views/teacher/course/HomeworkLibrary.vue
Normal file
720
src/views/teacher/course/HomeworkLibrary.vue
Normal file
@ -0,0 +1,720 @@
|
||||
<template>
|
||||
<div class="homework-library">
|
||||
<div class="toolbar">
|
||||
<h2>作业库</h2>
|
||||
<div class="toolbar-actions">
|
||||
<button class="btn btn-primary">添加作业</button>
|
||||
<button class="btn btn-new">导入</button>
|
||||
<button class="btn btn-new">导出</button>
|
||||
<button class="btn btn-danger">删除</button>
|
||||
<div class="search-box">
|
||||
<input type="text" placeholder="请输入想要搜索的内容" />
|
||||
<button class="btn btn-search">搜索</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-area">
|
||||
<div class="table-container">
|
||||
<n-config-provider :locale="zhCN" :date-locale="dateZhCN">
|
||||
<n-data-table :columns="columns" :data="homeworkList" :row-key="rowKey" :checked-row-keys="selectedHomework"
|
||||
@update:checked-row-keys="handleCheck" :bordered="false" :single-line="false" size="medium"
|
||||
class="homework-data-table" />
|
||||
</n-config-provider>
|
||||
</div>
|
||||
|
||||
<div class="pagination-container">
|
||||
<!-- 自定义分页器 -->
|
||||
<div class="custom-pagination">
|
||||
<div class="pagination-content">
|
||||
<div class="page-numbers">
|
||||
<span class="page-number nav-button" :class="{ disabled: currentPage === 1 }" @click="goToPage('first')">
|
||||
首页
|
||||
</span>
|
||||
<span class="page-number nav-button" :class="{ disabled: currentPage === 1 }" @click="goToPage('prev')">
|
||||
上一页
|
||||
</span>
|
||||
|
||||
<span v-for="page in visiblePages" :key="page" class="page-number page-number-bordered"
|
||||
:class="{ active: page === currentPage }" @click="goToPage(page)">
|
||||
{{ page }}
|
||||
</span>
|
||||
|
||||
<span v-if="showRightEllipsis" class="page-number">...</span>
|
||||
<span v-if="totalPages > 1" class="page-number page-number-bordered" @click="goToPage(totalPages)">
|
||||
{{ totalPages }}
|
||||
</span>
|
||||
|
||||
<span class="page-number nav-button" :class="{ disabled: currentPage === totalPages }"
|
||||
@click="goToPage('next')">
|
||||
下一页
|
||||
</span>
|
||||
<span class="page-number nav-button" :class="{ disabled: currentPage === totalPages }"
|
||||
@click="goToPage('last')">
|
||||
尾页
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, h, computed } from 'vue'
|
||||
import { NButton, NDropdown, NDataTable, NConfigProvider, zhCN, dateZhCN } from 'naive-ui'
|
||||
import type { DataTableColumns } from 'naive-ui'
|
||||
|
||||
// 作业类型定义
|
||||
interface HomeworkItem {
|
||||
id: number
|
||||
name: string
|
||||
chapter: string
|
||||
class: string
|
||||
creator: string
|
||||
createTime: string
|
||||
isTop: boolean
|
||||
}
|
||||
|
||||
// 选中的作业行
|
||||
const selectedHomework = ref<number[]>([])
|
||||
|
||||
// 分页相关
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const totalCount = ref(6)
|
||||
|
||||
// 计算总页数
|
||||
const totalPages = computed(() => Math.ceil(totalCount.value / pageSize.value))
|
||||
|
||||
// 计算可见页码
|
||||
const visiblePages = computed(() => {
|
||||
const pages = []
|
||||
const maxVisible = 5
|
||||
let start = Math.max(1, currentPage.value - Math.floor(maxVisible / 2))
|
||||
let end = Math.min(totalPages.value, start + maxVisible - 1)
|
||||
|
||||
if (end - start + 1 < maxVisible) {
|
||||
start = Math.max(1, end - maxVisible + 1)
|
||||
}
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
pages.push(i)
|
||||
}
|
||||
return pages
|
||||
})
|
||||
|
||||
// 计算是否显示右侧省略号
|
||||
const showRightEllipsis = computed(() => {
|
||||
return totalPages.value > 5 && currentPage.value < totalPages.value - 2
|
||||
})
|
||||
|
||||
// 分页导航函数
|
||||
const goToPage = (target: number | string) => {
|
||||
if (typeof target === 'string') {
|
||||
switch (target) {
|
||||
case 'first':
|
||||
currentPage.value = 1
|
||||
break
|
||||
case 'prev':
|
||||
if (currentPage.value > 1) currentPage.value--
|
||||
break
|
||||
case 'next':
|
||||
if (currentPage.value < totalPages.value) currentPage.value++
|
||||
break
|
||||
case 'last':
|
||||
currentPage.value = totalPages.value
|
||||
break
|
||||
}
|
||||
} else {
|
||||
currentPage.value = target
|
||||
}
|
||||
}
|
||||
|
||||
// 作业列表数据
|
||||
const homeworkList = ref<HomeworkItem[]>([
|
||||
{
|
||||
id: 1,
|
||||
name: '作业名称作业名称作业名称',
|
||||
chapter: '第一节 开课彩蛋新开始',
|
||||
class: '班级一、班级二',
|
||||
creator: '王建国',
|
||||
createTime: '2025.07.25 09:20',
|
||||
isTop: true
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '作业名称作业名称作业名称',
|
||||
chapter: '第一节 开课彩蛋新开始',
|
||||
class: '班级一、班级二',
|
||||
creator: '王建国',
|
||||
createTime: '2025.07.25 09:20',
|
||||
isTop: false
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '作业名称作业名称作业名称',
|
||||
chapter: '第一节 开课彩蛋新开始',
|
||||
class: '班级一、班级二',
|
||||
creator: '王建国',
|
||||
createTime: '2025.07.25 09:20',
|
||||
isTop: false
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: '作业名称作业名称作业名称',
|
||||
chapter: '-',
|
||||
class: '班级一、班级二',
|
||||
creator: '王建国',
|
||||
createTime: '2025.07.25 09:20',
|
||||
isTop: false
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: '作业名称作业名称作业名称',
|
||||
chapter: '第一节 开课彩蛋新开始',
|
||||
class: '班级一、班级二',
|
||||
creator: '王建国',
|
||||
createTime: '2025.07.25 09:20',
|
||||
isTop: false
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: '作业名称作业名称作业名称',
|
||||
chapter: '第一节 开课彩蛋新开始',
|
||||
class: '班级一、班级二',
|
||||
creator: '王建国',
|
||||
createTime: '2025.07.25 09:20',
|
||||
isTop: false
|
||||
}
|
||||
])
|
||||
|
||||
// 行键
|
||||
const rowKey = (row: HomeworkItem) => row.id
|
||||
|
||||
// 处理复选框选择
|
||||
const handleCheck = (keys: number[]) => {
|
||||
selectedHomework.value = keys
|
||||
}
|
||||
|
||||
// 表格列定义
|
||||
const columns: DataTableColumns<HomeworkItem> = [
|
||||
{
|
||||
type: 'selection',
|
||||
width: 50
|
||||
},
|
||||
{
|
||||
title: '序号',
|
||||
key: 'index',
|
||||
width: 80,
|
||||
render: (_, index) => index + 1
|
||||
},
|
||||
{
|
||||
title: '作业名称',
|
||||
key: 'name',
|
||||
render: (row) => {
|
||||
return h('div', { style: 'display: flex; align-items: center; gap: 8px;' }, [
|
||||
h('span', { class: 'homework-name' }, row.name),
|
||||
row.isTop ? h('span', { class: 'tag tag-pinned' }, '置顶') : null
|
||||
])
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '所属章节',
|
||||
key: 'chapter'
|
||||
},
|
||||
{
|
||||
title: '绑定班级',
|
||||
key: 'class'
|
||||
},
|
||||
{
|
||||
title: '创建人',
|
||||
key: 'creator'
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
key: 'createTime'
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 200,
|
||||
render: (row) => {
|
||||
return h('div', { style: 'display: flex; gap: 8px; align-items: center;' }, [
|
||||
h(NButton, {
|
||||
size: 'small',
|
||||
type: 'info',
|
||||
secondary: true,
|
||||
onClick: () => editHomework(row.id)
|
||||
}, { default: () => '编辑' }),
|
||||
h(NButton, {
|
||||
size: 'small',
|
||||
type: 'error',
|
||||
secondary: true,
|
||||
onClick: () => deleteHomework(row.id)
|
||||
}, { default: () => '删除' }),
|
||||
h(NDropdown, {
|
||||
trigger: 'click',
|
||||
options: [
|
||||
{
|
||||
label: '重命名',
|
||||
key: 'rename',
|
||||
icon: () => h('img', { src: '/public/images/teacher/重命名.png', style: 'width: 10px; height: 10px;' })
|
||||
},
|
||||
{
|
||||
label: row.isTop ? '取消置顶' : '置顶',
|
||||
key: 'toggleTop',
|
||||
icon: () => h('img', { src: '/public/images/teacher/置顶.png', style: 'width: 10px; height: 10px;' })
|
||||
},
|
||||
{
|
||||
label: '权限设置',
|
||||
key: 'permissions',
|
||||
icon: () => h('img', { src: '/public/images/teacher/权限设置.png', style: 'width: 10px; height: 10px;' })
|
||||
},
|
||||
{
|
||||
label: '下载',
|
||||
key: 'download',
|
||||
icon: () => h('img', { src: '/public/images/teacher/下载.png', style: 'width: 10px; height: 10px;' })
|
||||
}
|
||||
],
|
||||
onSelect: (key) => handleAction(key, row)
|
||||
}, {
|
||||
default: () => h(NButton, {
|
||||
size: 'small',
|
||||
type: 'info',
|
||||
secondary: true
|
||||
}, { default: () => '更多' })
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
// 编辑作业
|
||||
const editHomework = (id: number) => {
|
||||
console.log('编辑作业:', id)
|
||||
}
|
||||
|
||||
// 删除作业
|
||||
const deleteHomework = (id: number) => {
|
||||
console.log('删除作业:', id)
|
||||
}
|
||||
|
||||
// 处理下拉菜单操作
|
||||
const handleAction = (key: string, row: HomeworkItem) => {
|
||||
console.log('操作:', key, row)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.homework-library {
|
||||
width: 1293px;
|
||||
padding: 10px 20px 20px 0;
|
||||
height: 100%;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
border-bottom: 2px solid #F6F6F6;
|
||||
}
|
||||
|
||||
.toolbar h2 {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.toolbar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.content-area {
|
||||
height: calc(100% - 80px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 80px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 7px 16px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 2px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
background: #fff;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
border-color: #40a9ff;
|
||||
color: #40a9ff;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #0288D1;
|
||||
border-color: #0288D1;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #40a9ff;
|
||||
border-color: #40a9ff;
|
||||
}
|
||||
|
||||
.btn-new {
|
||||
background: #fff;
|
||||
border-color: #0288D1;
|
||||
color: #0288D1;
|
||||
}
|
||||
|
||||
.btn-new:hover {
|
||||
background: #0288D1;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-default {
|
||||
background: #fff;
|
||||
border-color: #0288D1;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.btn-default:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
border-color: #FF4D4F;
|
||||
color: #FF4D4F;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #FF4D4F;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-danger:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.btn-small {
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.btn-info {
|
||||
border-color: #0288D1;
|
||||
color: #0288D1;
|
||||
}
|
||||
|
||||
.btn-more {
|
||||
border-color: #0288D1;
|
||||
color: #0288D1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px solid #F1F3F4;
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.search-box input {
|
||||
border: none;
|
||||
padding: 6px 12px;
|
||||
outline: none;
|
||||
width: 200px;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.search-box input::placeholder {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.btn-search {
|
||||
background: #0288D1;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 6px 16px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.btn-search:hover {
|
||||
background: #0288D1;
|
||||
}
|
||||
|
||||
/* Naive UI 表格样式定制 */
|
||||
:deep(.homework-data-table) {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
:deep(.n-data-table-table) {
|
||||
border: 1px solid #F1F3F4;
|
||||
}
|
||||
|
||||
/* 表格头部样式 */
|
||||
:deep(.homework-data-table .n-data-table-thead) {
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
:deep(.homework-data-table .n-data-table-th) {
|
||||
background: #fafafa;
|
||||
font-weight: 500;
|
||||
color: #062333;
|
||||
font-size: 14px;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
padding: 12px 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 表格行样式 */
|
||||
:deep(.homework-data-table .n-data-table-td) {
|
||||
font-size: 12px;
|
||||
color: #062333;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
padding: 12px 8px;
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 名称列也居中 */
|
||||
:deep(.homework-data-table .n-data-table-td[data-col-key="name"]) {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
:deep(.homework-data-table .n-data-table-th[data-col-key="name"]) {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
:deep(.homework-data-table .n-data-table-tr:hover) {
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
/* 复选框样式 */
|
||||
:deep(.homework-data-table .n-checkbox) {
|
||||
--n-size: 16px;
|
||||
}
|
||||
|
||||
/* 按钮组样式调整 */
|
||||
:deep(.homework-data-table .n-button) {
|
||||
font-size: 12px;
|
||||
height: 28px;
|
||||
padding: 0 12px;
|
||||
margin: 2px;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
/* 编辑按钮样式 */
|
||||
:deep(.homework-data-table .n-button--info-type) {
|
||||
border: 1px solid #0288D1 !important;
|
||||
color: #0288D1 !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
:deep(.homework-data-table .n-button--info-type:hover) {
|
||||
border-color: #0288D1 !important;
|
||||
color: #0288D1 !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
/* 删除按钮样式 */
|
||||
:deep(.homework-data-table .n-button--error-type) {
|
||||
border: 1px solid #FF4D4F !important;
|
||||
color: #FF4D4F !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
:deep(.homework-data-table .n-button--error-type:hover) {
|
||||
border-color: #FF4D4F !important;
|
||||
color: #FF4D4F !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
/* 更多按钮样式 */
|
||||
:deep(.homework-data-table .n-button--info-type:last-child) {
|
||||
border: 1px solid #4165D7 !important;
|
||||
color: #4165D7 !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
:deep(.homework-data-table .n-button--info-type:last-child:hover) {
|
||||
border-color: #4165D7 !important;
|
||||
color: #4165D7 !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
/* 下拉菜单样式 */
|
||||
:deep(.n-dropdown-option) {
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
color: #062333;
|
||||
}
|
||||
|
||||
/* 分页器容器 */
|
||||
.pagination-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 20px 0;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: #fff;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 自定义分页器样式 */
|
||||
.custom-pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
background: #fff;
|
||||
padding: 20px 0;
|
||||
margin-top: auto;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.pagination-content {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 0 auto;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.page-numbers {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.page-number {
|
||||
display: inline-block;
|
||||
min-width: 38px;
|
||||
height: 38px;
|
||||
line-height: 38px;
|
||||
text-align: center;
|
||||
color: #333;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
padding: 0 5px;
|
||||
margin: 0 4px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.page-number-bordered {
|
||||
border: 1px solid #d9d9d9;
|
||||
}
|
||||
|
||||
.page-number.active {
|
||||
background-color: #0088D1;
|
||||
color: white;
|
||||
border-color: #0088D1;
|
||||
}
|
||||
|
||||
.page-number:hover:not(.disabled) {
|
||||
color: #0088D1;
|
||||
border-color: #0088D1;
|
||||
}
|
||||
|
||||
.page-number.disabled {
|
||||
color: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.page-number.disabled:hover {
|
||||
color: #ccc;
|
||||
border-color: #d9d9d9;
|
||||
}
|
||||
|
||||
.nav-button {
|
||||
padding: 0 8px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.nav-button:hover:not(.disabled) {
|
||||
color: #0088D1;
|
||||
}
|
||||
|
||||
/* 表格内所有文字颜色统一 */
|
||||
:deep(.homework-data-table .n-data-table-th__title) {
|
||||
color: #062333;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
:deep(.homework-data-table .n-data-table-td) {
|
||||
color: #062333;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 作业名称和标签样式 */
|
||||
.homework-name {
|
||||
display: inline-block;
|
||||
margin-right: 8px;
|
||||
color: #062333;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: inline-block;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
color: #9ac1d6;
|
||||
background: transparent;
|
||||
border: 1px solid #9ac1d6;
|
||||
}
|
||||
|
||||
.tag-pinned {
|
||||
color: #9ac1d6;
|
||||
border-color: #9ac1d6;
|
||||
}
|
||||
|
||||
/* 确保在Naive UI表格中标签样式正确显示 */
|
||||
:deep(.homework-data-table .tag) {
|
||||
display: inline-block !important;
|
||||
padding: 2px 6px !important;
|
||||
border-radius: 4px !important;
|
||||
font-size: 10px !important;
|
||||
color: #9ac1d6 !important;
|
||||
background: transparent !important;
|
||||
border: 1px solid #9ac1d6 !important;
|
||||
}
|
||||
|
||||
:deep(.homework-data-table .tag-pinned) {
|
||||
color: #9ac1d6 !important;
|
||||
border-color: #9ac1d6 !important;
|
||||
font-size: 10px !important;
|
||||
}
|
||||
</style>
|
409
src/views/teacher/course/HomeworkReview.vue
Normal file
409
src/views/teacher/course/HomeworkReview.vue
Normal file
@ -0,0 +1,409 @@
|
||||
<template>
|
||||
<div class="homework-review">
|
||||
<!-- 顶部筛选区域 -->
|
||||
<div class="top-section">
|
||||
<div class="filter-tabs">
|
||||
<span class="tab-item" :class="{ active: activeTab === 'all' }" @click="setActiveTab('all')">全部</span>
|
||||
<span class="tab-item" :class="{ active: activeTab === 'publishing' }"
|
||||
@click="setActiveTab('publishing')">发布中</span>
|
||||
<span class="tab-item" :class="{ active: activeTab === 'ended' }" @click="setActiveTab('ended')">已结束</span>
|
||||
</div>
|
||||
<div class="class-dropdown">
|
||||
<span class="dropdown-text">班级名称</span>
|
||||
<svg class="dropdown-arrow" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M3.14645 5.64645C3.34171 5.45118 3.65829 5.45118 3.85355 5.64645L8 9.79289L12.1464 5.64645C12.3417 5.45118 12.6583 5.45118 12.8536 5.64645C13.0488 5.84171 13.0488 6.15829 12.8536 6.35355L8.35355 10.8536C8.15829 11.0488 7.84171 11.0488 7.64645 10.8536L3.14645 6.35355C2.95118 6.15829 2.95118 5.84171 3.14645 5.64645Z"
|
||||
fill="#C2C2C2"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 作业列表 -->
|
||||
<div class="homework-list">
|
||||
<!-- 已结束作业 -->
|
||||
<div v-for="homework in filteredHomeworks" :key="homework.id">
|
||||
|
||||
|
||||
<div v-if="homework.status === 'ended'" class="homework-card ended">
|
||||
<a class="delete-link" @click="deleteHomework(homework.id)">删除</a>
|
||||
<div class="card-header">
|
||||
<h3 class="homework-title">{{ homework.title }}</h3>
|
||||
<span class="status-badge ended">已结束</span>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="content-left">
|
||||
<p class="homework-desc">{{ homework.content }}</p>
|
||||
<div class="homework-info">
|
||||
<div class="info-item">
|
||||
<img class="icon" src="/images/teacher/发布人.png" alt="发布人" />
|
||||
<span class="text">发布人:{{ homework.publisher }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<img class="icon" src="/images/teacher/起点时间.png" alt="时间" />
|
||||
<span class="text">起止时间:{{ homework.timeRange }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content-right">
|
||||
<div class="stats-area">
|
||||
<span class="big-number">{{ homework.pendingCount }}</span>
|
||||
<span class="label primary">待批</span>
|
||||
<span class="label secondary">{{ homework.submittedCount }}已交</span>
|
||||
<span class="label secondary">{{ homework.unsubmittedCount }}未交</span>
|
||||
</div>
|
||||
<button class="action-button review" @click="reviewHomework(homework.id)">批阅</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 发布中作业 -->
|
||||
<div v-if="homework.status === 'publishing'" class="homework-card publishing">
|
||||
<a class="delete-link" @click="deleteHomework(homework.id)">删除</a>
|
||||
|
||||
|
||||
<div class="card-header">
|
||||
<h3 class="homework-title">{{ homework.title }}</h3>
|
||||
<span class="status-badge publishing">发布中</span>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="content-left">
|
||||
<p class="homework-desc">{{ homework.content }}</p>
|
||||
<div class="homework-info">
|
||||
<div class="info-item">
|
||||
<img class="icon" src="/images/teacher/发布人.png" alt="发布人" />
|
||||
<span class="text">发布人:{{ homework.publisher }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<img class="icon" src="/images/teacher/起点时间.png" alt="时间" />
|
||||
<span class="text">起止时间:{{ homework.timeRange }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content-right">
|
||||
<div class="stats-area">
|
||||
<span class="big-number">{{ homework.pendingCount }}</span>
|
||||
<span class="label primary">待批</span>
|
||||
<span class="label secondary">{{ homework.submittedCount }}已交</span>
|
||||
<span class="label secondary">{{ homework.unsubmittedCount }}未交</span>
|
||||
</div>
|
||||
<button class="action-button view" @click="viewHomework(homework.id)">查看</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
interface HomeworkItem {
|
||||
id: number
|
||||
title: string
|
||||
content: string
|
||||
publisher: string
|
||||
timeRange: string
|
||||
status: 'publishing' | 'ended'
|
||||
pendingCount: number
|
||||
submittedCount: number
|
||||
unsubmittedCount: number
|
||||
}
|
||||
|
||||
const activeTab = ref<'all' | 'publishing' | 'ended'>('all')
|
||||
|
||||
const homeworks = ref<HomeworkItem[]>([
|
||||
{
|
||||
id: 1,
|
||||
title: '作业名称作业名称作业名称作业名称',
|
||||
content: '作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容',
|
||||
publisher: '王建国',
|
||||
timeRange: '2025.8.18-2025.9.18',
|
||||
status: 'ended',
|
||||
pendingCount: 10,
|
||||
submittedCount: 0,
|
||||
unsubmittedCount: 0
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '作业名称作业名称作业名称作业名称',
|
||||
content: '作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容作业内容',
|
||||
publisher: '王建国',
|
||||
timeRange: '2025.8.18-2025.9.18',
|
||||
status: 'publishing',
|
||||
pendingCount: 0,
|
||||
submittedCount: 0,
|
||||
unsubmittedCount: 0
|
||||
}
|
||||
])
|
||||
|
||||
const setActiveTab = (tab: 'all' | 'publishing' | 'ended') => {
|
||||
activeTab.value = tab
|
||||
}
|
||||
|
||||
const filteredHomeworks = computed(() => {
|
||||
if (activeTab.value === 'all') {
|
||||
return homeworks.value
|
||||
}
|
||||
return homeworks.value.filter(homework => homework.status === activeTab.value)
|
||||
})
|
||||
|
||||
const reviewHomework = (id: number) => {
|
||||
console.log('批阅作业:', id)
|
||||
}
|
||||
|
||||
const viewHomework = (id: number) => {
|
||||
console.log('查看作业:', id)
|
||||
}
|
||||
|
||||
const deleteHomework = (id: number) => {
|
||||
console.log('删除作业:', id)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.homework-review {
|
||||
width: 1293px;
|
||||
background: #fff;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* 顶部筛选区域 */
|
||||
.top-section {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 30px 0 0 25px;
|
||||
margin-right: 30px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
}
|
||||
|
||||
.filter-tabs {
|
||||
display: flex;
|
||||
gap: 50px;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
color: #333333;
|
||||
cursor: pointer;
|
||||
padding-top: 6px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 3px solid transparent;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tab-item.active {
|
||||
color: #5BADD9;
|
||||
border-bottom: 4px solid #0288D1;
|
||||
}
|
||||
|
||||
.tab-item:hover {
|
||||
color: #2196F3;
|
||||
}
|
||||
|
||||
.class-dropdown {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 16px;
|
||||
background: #fff;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 2px;
|
||||
margin-bottom: 20px;
|
||||
cursor: pointer;
|
||||
width: 276px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.dropdown-text {
|
||||
font-size: 14px;
|
||||
color: #062333;
|
||||
}
|
||||
|
||||
.dropdown-arrow {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
/* 作业列表 */
|
||||
.homework-list {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.homework-card {
|
||||
background: #fff;
|
||||
margin: 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
padding: 20px 25px;
|
||||
position: relative;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.homework-card:hover {
|
||||
background-color: #F5F5F5;
|
||||
}
|
||||
|
||||
.homework-card:hover .delete-link {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.homework-card:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* 卡片头部 */
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.homework-title {
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
color: #062333;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.status-badge.ended {
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.status-badge.publishing {
|
||||
color: #FF9800;
|
||||
}
|
||||
|
||||
/* 卡片内容 */
|
||||
.card-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.content-left {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.homework-desc {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
line-height: 1.5;
|
||||
margin: 0 0 20px 0;
|
||||
}
|
||||
|
||||
.homework-info {
|
||||
display: flex;
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.info-item .icon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.info-item .text {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 右侧内容 */
|
||||
.content-right {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 80px;
|
||||
}
|
||||
|
||||
.delete-link {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 35%;
|
||||
transform: translateY(-50%);
|
||||
color: #2196F3;
|
||||
font-size: 14px;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
align-self: flex-start;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.delete-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.stats-area {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.big-number {
|
||||
font-size: 30px;
|
||||
font-weight: bold;
|
||||
color: #062333;
|
||||
line-height: 0.9;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.label {
|
||||
font-size: 12px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.label.primary {
|
||||
color: #062333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.label.secondary {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 操作按钮 */
|
||||
.action-button {
|
||||
background: #0288D1;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 6px 24px;
|
||||
border-radius: 2px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s ease;
|
||||
min-width: 92px;
|
||||
}
|
||||
|
||||
.action-button:hover {
|
||||
background: #0288D1;
|
||||
}
|
||||
|
||||
.action-button.review {}
|
||||
|
||||
.action-button.view {
|
||||
background: #0288D1;
|
||||
}
|
||||
|
||||
/* 发布中状态特殊布局调整 */
|
||||
.homework-card.publishing .content-right {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.homework-card.publishing .stats-area {
|
||||
margin-top: 20px;
|
||||
}
|
||||
</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