feat: 对接部分题库接口;添加考试界面;部分界面样式优化

This commit is contained in:
yuk255 2025-08-30 17:50:14 +08:00
parent 57eb57d0ce
commit 590af0951f
10 changed files with 1678 additions and 473 deletions

View File

@ -2,6 +2,7 @@
import { ApiRequest } from '../request'
import type {
ApiResponse,
ApiResponseWithResult,
Repo,
Question,
CreateRepoRequest,
@ -36,19 +37,54 @@ export class ExamApi {
/**
*
*/
static async getCourseRepoList(): Promise<ApiResponse<Repo[]>> {
const response = await ApiRequest.get<Repo[]>(`/biz/repo/repoList`)
static async getCourseRepoList(): Promise<ApiResponseWithResult<Repo[]>> {
const response = await ApiRequest.get<{ result: Repo[] }>(`/biz/repo/repoList`)
console.log('✅ 获取课程题库列表成功:', response)
return response
}
/**
*
*/
static async getCourseList(): Promise<ApiResponse<{ id: string; name: string }[]>> {
try {
// 调用现有的课程列表API但只返回id和name字段
const response = await ApiRequest.get<any>('/biz/course/list')
console.log('✅ 获取课程列表成功:', response)
// 处理响应数据只提取id和name
if (response.data && response.data.success && response.data.result) {
const courseList = response.data.result.map((course: any) => ({
id: course.id,
name: course.name || course.title || '未命名课程'
}))
return {
code: response.data.code || 200,
message: response.data.message || 'success',
data: courseList
}
}
// 如果响应格式不符合预期,返回空数组
return {
code: 200,
message: 'success',
data: []
}
} catch (error) {
console.error('获取课程列表失败:', error)
throw error
}
}
/**
*
*/
static async deleteRepo(id: string): Promise<ApiResponse<string>> {
console.log('🚀 删除题库:', { id })
const response = await ApiRequest.delete<string>('/gen/repo/repo/delete', {
params: { id }
id
})
console.log('✅ 删除题库成功:', response)
return response

View File

@ -8,6 +8,16 @@ export interface ApiResponse<T = any> {
timestamp?: string
}
// 带有 result 包装的响应类型
export interface ApiResponseWithResult<T = any> {
code: number
message: string
data: {
result: T
}
timestamp?: string
}
// 分页响应类型
export interface PaginationResponse<T> {
list: T[]
@ -695,6 +705,8 @@ export interface Repo {
createTime: string
updateBy: string
updateTime: string
courseId?: string // 所属课程ID
courseName?: string // 所属课程名称
}
export interface Question {

301
src/data/mockExamData.ts Normal file
View File

@ -0,0 +1,301 @@
// 模拟考试数据
export const mockExamData = {
id: "exam_001",
examName: "计算机基础知识测试",
duration: 90, // 考试时长(分钟)
totalScore: 100,
questions: [
{
id: "section_1",
title: "第一大题:单选题",
type: "section",
description: "请从四个选项中选择一个正确答案",
subQuestions: [
{
id: "q1",
type: "single_choice",
title: "在HTML中用于定义无序列表的标签是",
score: 2,
options: [
{
id: "q1_a",
content: "<ol>"
},
{
id: "q1_b",
content: "<ul>"
},
{
id: "q1_c",
content: "<li>"
},
{
id: "q1_d",
content: "<dl>"
}
]
},
{
id: "q2",
type: "single_choice",
title: "CSS中用于设置字体大小的属性是",
score: 2,
options: [
{
id: "q2_a",
content: "font-weight"
},
{
id: "q2_b",
content: "font-size"
},
{
id: "q2_c",
content: "font-family"
},
{
id: "q2_d",
content: "font-style"
}
]
},
{
id: "q3",
type: "single_choice",
title: "JavaScript中声明变量使用的关键字是",
score: 2,
options: [
{
id: "q3_a",
content: "var"
},
{
id: "q3_b",
content: "let"
},
{
id: "q3_c",
content: "const"
},
{
id: "q3_d",
content: "以上都是"
}
]
},
{
id: "q4",
type: "single_choice",
title: "HTTP协议默认端口号是",
score: 2,
options: [
{
id: "q4_a",
content: "21"
},
{
id: "q4_b",
content: "80"
},
{
id: "q4_c",
content: "443"
},
{
id: "q4_d",
content: "8080"
}
]
},
{
id: "q5",
type: "single_choice",
title: "下列哪个不是Vue.js的生命周期钩子",
score: 2,
options: [
{
id: "q5_a",
content: "mounted"
},
{
id: "q5_b",
content: "created"
},
{
id: "q5_c",
content: "destroyed"
},
{
id: "q5_d",
content: "rendered"
}
]
}
]
},
{
id: "section_2",
title: "第二大题:多选题",
type: "section",
description: "请从选项中选择所有正确答案",
subQuestions: [
{
id: "q6",
type: "multiple_choice",
title: "下列哪些是前端开发常用的框架?",
score: 3,
options: [
{
id: "q6_a",
content: "React"
},
{
id: "q6_b",
content: "Vue.js"
},
{
id: "q6_c",
content: "Angular"
},
{
id: "q6_d",
content: "Express.js"
}
]
},
{
id: "q7",
type: "multiple_choice",
title: "CSS布局方式包括哪些",
score: 3,
options: [
{
id: "q7_a",
content: "Flexbox"
},
{
id: "q7_b",
content: "Grid"
},
{
id: "q7_c",
content: "Float"
},
{
id: "q7_d",
content: "Position"
}
]
},
{
id: "q8",
type: "multiple_choice",
title: "HTTP状态码中表示成功的有哪些",
score: 3,
options: [
{
id: "q8_a",
content: "200"
},
{
id: "q8_b",
content: "201"
},
{
id: "q8_c",
content: "404"
},
{
id: "q8_d",
content: "500"
}
]
}
]
},
{
id: "section_3",
title: "第三大题:判断题",
type: "section",
description: "请判断下列说法是否正确",
subQuestions: [
{
id: "q9",
type: "true_false",
title: "HTML是一种编程语言。",
score: 2
},
{
id: "q10",
type: "true_false",
title: "CSS可以用来控制网页的布局和样式。",
score: 2
},
{
id: "q11",
type: "true_false",
title: "JavaScript只能在浏览器中运行。",
score: 2
},
{
id: "q12",
type: "true_false",
title: "Git是一个版本控制系统。",
score: 2
}
]
},
{
id: "section_4",
title: "第四大题:填空题",
type: "section",
description: "请在空白处填入正确答案",
subQuestions: [
{
id: "q13",
type: "fill_blank",
title: "HTML文档的基本结构包括_____、_____和_____三个主要部分。",
score: 3,
fillBlanks: [
{ id: "blank1", answer: "DOCTYPE声明" },
{ id: "blank2", answer: "head" },
{ id: "blank3", answer: "body" }
]
},
{
id: "q14",
type: "fill_blank",
title: "CSS选择器中_____选择器用于选择具有特定ID的元素_____选择器用于选择具有特定类名的元素。",
score: 2,
fillBlanks: [
{ id: "blank4", answer: "ID" },
{ id: "blank5", answer: "类" }
]
}
]
},
{
id: "section_5",
title: "第五大题:简答题",
type: "section",
description: "请根据题目要求作答",
subQuestions: [
{
id: "q15",
type: "short_answer",
title: "请简述响应式网页设计的基本原理和实现方法。",
score: 5
},
{
id: "q16",
type: "short_answer",
title: "解释JavaScript中异步编程的概念并举例说明。",
score: 5
}
]
}
]
};
// 导出考试数据用于在组件中使用
export default mockExamData;

View File

@ -72,6 +72,7 @@ import AddQuestion from '@/views/teacher/ExamPages/AddQuestion.vue'
import StudentList from '@/views/teacher/ExamPages/StudentList.vue'
import GradingPage from '@/views/teacher/ExamPages/GradingPage.vue'
import ExamTaking from '@/views/teacher/ExamPages/ExamTaking.vue'
import ExamNoticeBeforeStart from '@/views/teacher/ExamPages/ExamNoticeBeforeStart.vue'
import ChapterEditor from '@/views/teacher/course/ChapterEditor.vue'
@ -366,6 +367,12 @@ const routes: RouteRecordRaw[] = [
]
},
{
path: '/exam/notice/:id',
name: 'ExamNoticeBeforeStart',
component: ExamNoticeBeforeStart,
meta: { title: '考前须知' }
},
{
path: '/taking/:id',
name: 'ExamTaking',
@ -457,7 +464,7 @@ const routes: RouteRecordRaw[] = [
meta: { title: '学习中心', requiresAuth: true }
},
{
path: '/profile',
path: '/profile/:tabKey?',
name: 'Profile',
component: Profile,
meta: { title: '个人中心', requiresAuth: true }

View File

@ -11,8 +11,6 @@
</div>
</div>
<!-- 考试说明页面 -->
<div class="exam-instructions" v-if="!examStarted">
<div class="container">
@ -881,8 +879,15 @@ const questionsByType = computed(() => {
//
//
const startExam = () => {
//
const examId = sectionId.value || 1 // 使 sectionId ID1
router.push(`/exam/notice/${examId}`)
}
//
const beginExam = () => {
examStarted.value = true
remainingTime.value = examDuration.value * 60
startTimer()
@ -1220,7 +1225,7 @@ onMounted(() => {
//
const fromNotice = route.query.fromNotice
if (fromNotice === 'true') {
startExam()
beginExam()
}
})
</script>

View File

@ -1071,14 +1071,14 @@
import { ref, computed, onMounted, onActivated, reactive } from 'vue'
import { useMessage, NInput, NForm, NFormItem } from 'naive-ui'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import { useUserStore } from '@/stores/user'
import SafeAvatar from '@/components/common/SafeAvatar.vue'
import QuillEditor from '@/components/common/QuillEditor.vue'
import { useRouter, useRoute } from 'vue-router'
const { t, locale } = useI18n()
const router = useRouter()
const route = useRoute()
//
const bannerImage = computed(() => {
return locale.value === 'zh' ? '/banners/banner8.png' : '/banners/banner1-en.png'
@ -2288,6 +2288,7 @@ const isMessageTab = computed(() => activeTab.value === 'message')
//
const handleMenuSelect = (key: TabType) => {
activeTab.value = key
router.push(`/profile/${key}`)
// message.info(`${getTabTitle(key)}`)
}
@ -2392,7 +2393,8 @@ const startExam = (examId: number) => {
//
const continueExam = (examId: number) => {
message.info(`继续考试 ${examId}`)
// message.info(` ${examId}`)
router.push(`/exam/notice/${examId}`)
}
//
@ -2759,6 +2761,9 @@ onMounted(() => {
window.location.reload()
}, 100)
}
const tabKey = <TabType>route.params.tabKey || 'courses'
handleMenuSelect(tabKey)
})
onActivated(() => {

View File

@ -0,0 +1,333 @@
<template>
<div class="exam-notice-container">
<!-- 页面标题 -->
<div class="page-header">
<h1 class="title">考试中心</h1>
<span class="subTit">涵盖多种题型全方位考核AI智能阅卷</span>
</div>
<div class="exam-notice-content">
<!-- 考前须知主体 -->
<div class="notice-main">
<h1 class="notice-title">考前须知</h1>
<!-- 考试信息 -->
<div class="exam-info">
<div class="exam-meta">
<span class="exam-time">考试时间2025年8月1日-8月16日</span>
<span class="exam-name">考试名称[2025] 教学技能提高暨自主学习课程期末考试</span>
<span class="exam-duration">考试时长120分钟</span>
</div>
</div>
<!-- 考试规则列表 -->
<div class="rules-list">
<div class="rule-item">
<span class="rule-number">1.</span>
<span class="rule-text">考试时间2025年8月1日-8月16日在此期间考生可自行安排开始考试考试时长共120分钟</span>
</div>
<div class="rule-item">
<span class="rule-number">2.</span>
<span class="rule-text">考生必须持本人身份证明准证入场两证缺一者不得参加考试</span>
</div>
<div class="rule-item">
<span class="rule-number">3.</span>
<span class="rule-text">考试时严禁考生携带相关文书籍资料笔记本电脑自带纸张等</span>
</div>
<div class="rule-item">
<span class="rule-number">4.</span>
<span
class="rule-text">本次考试采用机读卡答题方式考生在机读卡上填写姓名用2B铅笔在规定位置填涂考号答案未按规定在机读卡上作答造成本次考试无效者记录的责任自负</span>
</div>
<div class="rule-item">
<span class="rule-number">5.</span>
<span class="rule-text">考生若需要借用考试用具须举手示意监考人员呼叫不得向其他考生直接借取</span>
</div>
<div class="rule-item">
<span class="rule-number">6.</span>
<span
class="rule-text">考试时请考生自觉关闭手机并将携带的随身证明件签字笔2B铅笔橡皮除以外的物品存放在指定位置若手机响过在考生座位上响起按作弊处理</span>
</div>
<div class="rule-item">
<span class="rule-number">7.</span>
<span class="rule-text">考生答题完毕不得诵背清机读卡带离考场否则现场作弊处理</span>
</div>
<div class="rule-item">
<span class="rule-number">8.</span>
<span class="rule-text">找人代考者按作弊处理直至替考者离开考场假证件予以没收</span>
</div>
<div class="rule-item">
<span class="rule-number">9.</span>
<span class="rule-text">必须服从监考人员的管理严格遵守考场纪律对不服从监考人员管教抗拒者按作弊的取消考试资格</span>
</div>
<div class="rule-item">
<span class="rule-number">10.</span>
<span
class="rule-text">考生违反考生须知有关规定或或违反考场纪律进行警察时监考人员可要求该名考生离开考场收回试卷取消考试资格并在考场记录单上作好记录</span>
</div>
<div class="rule-item">
<span class="rule-number">11.</span>
<span class="rule-text">考试成绩将于十个工作日完成此间进行公示查询成绩网址www.baidu.com</span>
</div>
<div class="contact-info">
<span>*如有疑问可致电0871-5533221</span>
</div>
</div>
<!-- 按钮区域 -->
<div class="button-area">
<n-button size="large" :type="countdown > 0 ? 'default' : 'primary'" :disabled="countdown > 0"
@click="handleStartExam">
我已知晓开始考试 <span v-if="countdown > 0">{{ countdown }}</span>
</n-button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { NButton } from 'naive-ui'
const router = useRouter()
const route = useRoute()
const countdown = ref(10)
let timer: NodeJS.Timeout | null = null
//
onMounted(() => {
timer = setInterval(() => {
if (countdown.value > 0) {
countdown.value--
} else {
//
if (timer) {
clearInterval(timer)
timer = null
}
}
}, 1000)
})
onUnmounted(() => {
if (timer) {
clearInterval(timer)
}
})
//
const handleStartExam = () => {
if (countdown.value > 0) {
return //
}
// sessionStorageAPI
const examData = {
examId: route.params.id,
examName: '[2025] 教学技能提高暨自主学习课程期末考试',
duration: 120, // 120
questions: [] //
}
sessionStorage.setItem('examData', JSON.stringify(examData))
// ID
const examId = route.params.id
router.replace(`/taking/${examId}`)
}
</script>
<style scoped>
.exam-notice-container {
min-height: 100vh;
background-color: #f5f5f5;
padding: 0 20px;
}
.exam-notice-content {
max-width: 1000px;
margin: 10px auto;
background: white;
border-radius: 8px;
overflow: hidden;
}
.page-header {
background: white;
padding: 40px 0;
text-align: center;
border-bottom: 1px solid #e8e8e8;
background-size: 100% 100%;
background-image: url('/banners/考前须知.png');
}
.page-header>.title{
font-family: AlimamaShuHeiTi, AlimamaShuHeiTi;
font-weight: bold;
font-size: 32px;
color: #000000;
}
.page-header>.subtitle{
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 14px;
color: #000000;
}
.course-name {
color: #666;
font-size: 14px;
}
.notice-main {
padding: 40px 60px 60px;
}
.notice-title {
text-align: center;
font-size: 28px;
font-weight: 600;
color: #333;
margin-bottom: 40px;
}
.exam-info {
margin-bottom: 40px;
}
.exam-meta {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 0;
border-bottom: 1px solid #e9ecef;
flex-wrap: wrap;
gap: 20px;
}
.exam-time,
.exam-name,
.exam-duration {
color: #666;
font-size: 14px;
}
.exam-name {
flex: 1;
text-align: center;
min-width: 300px;
}
.rules-list {
margin-bottom: 50px;
}
.rule-item {
display: flex;
margin-bottom: 10px;
line-height: 1.2;
}
.rule-number {
color: #333;
font-weight: 500;
min-width: 24px;
margin-right: 8px;
}
.rule-text {
color: #555;
font-size: 14px;
flex: 1;
}
.contact-info {
margin-top: 30px;
color: #666;
font-size: 14px;
font-style: italic;
}
.button-area {
display: flex;
justify-content: center;
gap: 20px;
margin-top: 40px;
}
.cancel-button,
.start-button {
padding: 12px 32px;
font-size: 16px;
border-radius: 6px;
min-width: 180px;
}
.cancel-button {
background-color: #f8f9fa;
border: 1px solid #dee2e6;
color: #6c757d;
}
.cancel-button:hover {
background-color: #e9ecef;
}
.start-button {
background-color: #007bff;
border-color: #007bff;
}
.start-button:hover {
background-color: #0056b3;
border-color: #0056b3;
}
.start-button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* 响应式设计 */
@media (max-width: 768px) {
.notice-main {
padding: 30px 20px 40px;
}
.exam-meta {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.exam-name {
text-align: left;
min-width: auto;
}
.button-area {
flex-direction: column;
align-items: center;
}
.cancel-button,
.start-button {
width: 100%;
max-width: 300px;
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -6,56 +6,32 @@
<n-button type="primary" @click="addQuestionBank">新建题库</n-button>
<n-button ghost @click="importQuestionBank">导入题库</n-button>
<n-button ghost @click="exportQuestionBank">导出题库</n-button>
<n-button type="error" ghost @click="deleteSelected" :disabled="selectedRowKeys.length === 0">删除</n-button>
<n-input
v-model:value="filters.keyword"
placeholder="请输入想要搜索的内容"
style="width: 200px"
clearable
/>
<n-button type="error" ghost @click="deleteSelected"
:disabled="selectedRowKeys.length === 0">删除</n-button>
<n-select v-model:value="filters.courseId" placeholder="所属课程" :options="courseOptions" clearable
style="width: 150px" @update:value="searchQuestionBanks" />
<n-input v-model:value="filters.keyword" placeholder="请输入想要搜索的内容" style="width: 200px" clearable />
<n-button type="primary" @click="searchQuestionBanks">搜索</n-button>
</n-space>
</div>
<n-data-table
ref="tableRef"
:columns="columns"
:data="questionBankList"
:loading="loading"
:pagination="paginationConfig"
:row-key="(row: QuestionBank) => row.id"
:checked-row-keys="selectedRowKeys"
@update:checked-row-keys="handleCheck"
class="question-bank-table"
:single-line="false"
/>
<n-data-table ref="tableRef" :columns="columns" :data="questionBankList" :loading="loading"
:pagination="paginationConfig" :row-key="(row: QuestionBank) => row.id" :checked-row-keys="selectedRowKeys"
@update:checked-row-keys="handleCheck" class="question-bank-table" :single-line="false" />
<!-- 新建/编辑题库弹窗 -->
<n-modal
v-model:show="showCreateModal"
preset="dialog"
:title="isEditMode ? '编辑题库' : '新建题库'"
style="width: 500px;"
>
<n-modal v-model:show="showCreateModal" preset="dialog" :title="isEditMode ? '编辑题库' : '新建题库'"
style="width: 500px;">
<div class="create-modal-content">
<div class="form-item">
<label>题库名称</label>
<n-input
v-model:value="createForm.name"
placeholder="请输入题库名称"
style="width: 100%;"
/>
<n-input v-model:value="createForm.name" placeholder="请输入题库名称" style="width: 100%;" />
</div>
<div class="form-item">
<label>题库描述</label>
<n-input
v-model:value="createForm.description"
type="textarea"
placeholder="请输入题库描述"
style="width: 100%;"
:rows="3"
/>
<n-input v-model:value="createForm.description" type="textarea" placeholder="请输入题库描述"
style="width: 100%;" :rows="3" />
</div>
</div>
@ -70,22 +46,18 @@
</n-modal>
<!-- 导入弹窗 -->
<ImportModal
v-model:show="showImportModal"
template-name="question_bank_template.xlsx"
import-type="questionBank"
@success="handleImportSuccess"
@template-download="handleTemplateDownload"
/>
<ImportModal v-model:show="showImportModal" template-name="question_bank_template.xlsx"
import-type="questionBank" @success="handleImportSuccess" @template-download="handleTemplateDownload" />
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted, h, VNode } from 'vue';
import { NButton, NSpace, useMessage, useDialog } from 'naive-ui';
import { NButton, NSpace, NSelect, useMessage, useDialog } from 'naive-ui';
import { useRouter } from 'vue-router';
import ImportModal from '@/components/common/ImportModal.vue';
import { ExamApi } from '@/api'
import { ExamApi } from '@/api';
import type { Repo } from '@/api/types';
//
const message = useMessage();
@ -104,11 +76,19 @@ interface QuestionBank {
creator: string;
createTime: string;
lastModified: string;
courseName?: string; //
}
//
interface CourseOption {
label: string;
value: string;
}
//
const filters = reactive({
keyword: ''
keyword: '',
courseId: '' //
});
@ -117,6 +97,9 @@ const loading = ref(false);
const selectedRowKeys = ref<string[]>([]);
const questionBankList = ref<QuestionBank[]>([]);
//
const courseOptions = ref<CourseOption[]>([]);
// /
const showCreateModal = ref(false);
const isEditMode = ref(false);
@ -178,7 +161,7 @@ const createColumns = ({
{
title: '题库名称',
key: 'name',
width: 200,
width: 180,
ellipsis: {
tooltip: true
},
@ -196,7 +179,7 @@ const createColumns = ({
{
title: '题库描述',
key: 'description',
width: 250,
width: 200,
ellipsis: {
tooltip: true
}
@ -204,12 +187,27 @@ const createColumns = ({
{
title: '题目数量',
key: 'questionCount',
width: 100,
width: 80,
align: 'center' as const,
},
{
title: '所属课程',
key: 'courseName',
width: 160,
align: 'center' as const,
ellipsis: {
tooltip: true
},
render(row: QuestionBank) {
return h('span', { style: 'color: #1890ff; font-weight: 500;' }, row.questionCount.toString());
return row.courseName || '暂无课程';
}
},
{
title: '权限',
key: 'permissions',
width: 160,
align: 'center' as const
},
{
title: '创建人',
key: 'creator',
@ -219,19 +217,13 @@ const createColumns = ({
{
title: '创建时间',
key: 'createTime',
width: 160,
align: 'center' as const
},
{
title: '最后修改',
key: 'lastModified',
width: 160,
width: 150,
align: 'center' as const
},
{
title: '操作',
key: 'actions',
width: 150,
width: 180,
align: 'center' as const,
render(row: QuestionBank) {
const buttons: VNode[] = [];
@ -241,7 +233,6 @@ const createColumns = ({
size: 'small',
type: 'primary',
ghost: true,
style: 'margin: 0 3px;',
onClick: () => handleAction('进入', row)
}, { default: () => '进入' })
);
@ -250,7 +241,6 @@ const createColumns = ({
h(NButton, {
size: 'small',
ghost: true,
style: 'margin: 0 3px;',
onClick: () => handleAction('编辑', row)
}, { default: () => '编辑' })
);
@ -260,7 +250,6 @@ const createColumns = ({
size: 'small',
type: 'error',
ghost: true,
style: 'margin: 0 3px;',
onClick: () => handleAction('删除', row)
}, { default: () => '删除' })
);
@ -275,7 +264,7 @@ const createColumns = ({
const columns = createColumns({
handleAction: (action, row) => {
if (action === '进入') {
enterQuestionBank(row.id);
enterQuestionBank(row.id, row.name);
} else if (action === '编辑') {
editQuestionBank(row.id);
} else if (action === '删除') {
@ -284,27 +273,6 @@ const columns = createColumns({
},
});
//
const generateMockData = (): QuestionBank[] => {
const mockData: QuestionBank[] = [];
const creators = ['王建国', '李明', '张三', '刘老师', '陈教授'];
const names = ['计算机基础题库', '数学专业题库', '英语考试题库', '物理练习题库', '化学综合题库'];
for (let i = 1; i <= 20; i++) {
mockData.push({
id: '1960998116632399873',
sequence: i,
name: names[Math.floor(Math.random() * names.length)] + ` ${i}`,
description: `这是一个包含多种题型的综合性题库,适用于教学和考试场景...`,
questionCount: Math.floor(Math.random() * 500) + 50,
creator: creators[Math.floor(Math.random() * creators.length)],
createTime: '2025.08.20 09:20',
lastModified: '2025.08.28 15:30'
});
}
return mockData;
};
//
const handleCheck = (rowKeys: string[]) => {
selectedRowKeys.value = rowKeys;
@ -316,17 +284,55 @@ const searchQuestionBanks = () => {
loadQuestionBanks();
};
// ID
const getCourseNameById = (courseId: string): string => {
const course = courseOptions.value.find(option => option.value === courseId);
return course ? course.label : '';
};
//
const loadCourseList = async () => {
try {
const res = await ExamApi.getCourseList();
console.log('✅ 获取课程列表:', res.data);
courseOptions.value = [
{ label: '全部课程', value: '' },
...res.data.map(course => ({
label: course.name,
value: course.id
}))
];
} catch (error) {
console.error('加载课程列表失败:', error);
//
courseOptions.value = [{ label: '全部课程', value: '' }];
}
};
//
const loadQuestionBanks = async () => {
loading.value = true;
try {
// TODO API
// await ExamApi.getCourseRepoList();
// API
const res = await ExamApi.getCourseRepoList();
console.log('✅ 获取题库列表:', res.data.result);
const allData = generateMockData();
//
let filteredData = allData;
// APIRepo[]QuestionBank[]
const apiData: QuestionBank[] = res.data.result.map((repo: Repo, index: number) => ({
id: repo.id,
sequence: index + 1,
name: repo.title,
description: repo.remark || '暂无描述',
questionCount: repo.questionCount || 0,
creator: repo.createBy || '未知',
createTime: repo.createTime || '',
lastModified: repo.updateTime || '',
courseName: repo.courseName || '暂无课程'
}));
//
let filteredData = apiData;
if (filters.keyword) {
filteredData = filteredData.filter(item =>
item.name.includes(filters.keyword) ||
@ -335,6 +341,14 @@ const loadQuestionBanks = async () => {
);
}
//
if (filters.courseId) {
const courseName = getCourseNameById(filters.courseId);
filteredData = filteredData.filter(item =>
item.courseName === courseName
);
}
//
pagination.total = filteredData.length;
const start = (pagination.page - 1) * pagination.pageSize;
@ -420,8 +434,8 @@ const deleteSelected = () => {
};
//
const enterQuestionBank = (bankId: string) => {
router.push(`/teacher/exam-management/question-bank/${bankId}/questions`);
const enterQuestionBank = (bankId: string, bankTitle: string) => {
router.push(`/teacher/exam-management/question-bank/${bankId}/questions?title=${bankTitle}`);
};
const editQuestionBank = (id: string) => {
@ -541,6 +555,7 @@ const handleTemplateDownload = (type?: string) => {
//
onMounted(() => {
loadCourseList();
loadQuestionBanks();
});
</script>

View File

@ -6,12 +6,12 @@
<n-icon><ChevronBackOutline /></n-icon>
题库管理
</n-breadcrumb-item>
<n-breadcrumb-item>{{ currentBankName }}</n-breadcrumb-item>
<n-breadcrumb-item>{{ currentBankTitle }}</n-breadcrumb-item>
</n-breadcrumb>
</div>
<div class="header-section">
<h1 class="title">{{ currentBankName }} - 试题管理</h1>
<h1 class="title">{{ currentBankTitle }}</h1>
<n-space class="actions-group">
<n-button type="primary" @click="addQuestion">添加试题</n-button>
<n-button ghost @click="importQuestions">导入</n-button>
@ -190,6 +190,8 @@ const route = useRoute();
//
const currentBankId = computed(() => route.params.bankId as string);
const currentBankTitle = computed(() => route.query.title as string);
const currentBankName = ref('加载中...');
//