Merge branch 'dev' of http://110.42.96.65:19890/GoCo/OL-LearnPlatform-Frontend into dev
This commit is contained in:
commit
597b9a0d3f
@ -3,4 +3,5 @@ type: "manual"
|
|||||||
---
|
---
|
||||||
|
|
||||||
1、在接下来的每一个步骤当中,请帮我实现对页面的响应式设计
|
1、在接下来的每一个步骤当中,请帮我实现对页面的响应式设计
|
||||||
2、必须严格执行我给你的指令,一步一步执行,不得有缩减
|
2、必须严格执行我给你的指令,一步一步执行,不得有缩减
|
||||||
|
3、我们用的是naive UI组件 ,TS,vue3
|
||||||
|
BIN
public/images/loginImage/backImage.png
Normal file
BIN
public/images/loginImage/backImage.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.7 MiB |
BIN
public/images/loginImage/logo.png
Normal file
BIN
public/images/loginImage/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 68 KiB |
25
src/App.vue
25
src/App.vue
@ -1,9 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted } from 'vue'
|
import { onMounted, computed } from 'vue'
|
||||||
import { RouterView } from 'vue-router'
|
import { RouterView, useRoute } from 'vue-router'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
import AppLayout from '@/components/layout/AppLayout.vue'
|
import AppLayout from '@/components/layout/AppLayout.vue'
|
||||||
import { NConfigProvider } from 'naive-ui'
|
import { NConfigProvider, NMessageProvider } from 'naive-ui'
|
||||||
import type { GlobalThemeOverrides } from 'naive-ui';
|
import type { GlobalThemeOverrides } from 'naive-ui';
|
||||||
|
|
||||||
|
|
||||||
@ -58,6 +58,10 @@ const themeOverrides: GlobalThemeOverrides = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
// 检查是否为登录页面
|
||||||
|
const isLoginPage = computed(() => route.name === 'Login')
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// 初始化用户认证状态
|
// 初始化用户认证状态
|
||||||
@ -68,9 +72,18 @@ onMounted(() => {
|
|||||||
<template>
|
<template>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<n-config-provider :theme-overrides="themeOverrides">
|
<n-config-provider :theme-overrides="themeOverrides">
|
||||||
<AppLayout>
|
<!-- 登录页面不使用 AppLayout,但需要 message provider -->
|
||||||
<RouterView />
|
<template v-if="isLoginPage">
|
||||||
</AppLayout>
|
<n-message-provider>
|
||||||
|
<RouterView />
|
||||||
|
</n-message-provider>
|
||||||
|
</template>
|
||||||
|
<!-- 其他页面使用 AppLayout -->
|
||||||
|
<template v-else>
|
||||||
|
<AppLayout>
|
||||||
|
<RouterView />
|
||||||
|
</AppLayout>
|
||||||
|
</template>
|
||||||
</n-config-provider>
|
</n-config-provider>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -67,6 +67,8 @@ import ExamLibrary from '@/views/teacher/ExamPages/ExamLibrary.vue'
|
|||||||
import MarkingCenter from '@/views/teacher/ExamPages/MarkingCenter.vue'
|
import MarkingCenter from '@/views/teacher/ExamPages/MarkingCenter.vue'
|
||||||
import AddExam from '@/views/teacher/ExamPages/AddExam.vue'
|
import AddExam from '@/views/teacher/ExamPages/AddExam.vue'
|
||||||
import AddQuestion from '@/views/teacher/ExamPages/AddQuestion.vue'
|
import AddQuestion from '@/views/teacher/ExamPages/AddQuestion.vue'
|
||||||
|
import StudentList from '@/views/teacher/ExamPages/StudentList.vue'
|
||||||
|
import GradingPage from '@/views/teacher/ExamPages/GradingPage.vue'
|
||||||
|
|
||||||
import ChapterEditor from '@/views/teacher/course/ChapterEditor.vue'
|
import ChapterEditor from '@/views/teacher/course/ChapterEditor.vue'
|
||||||
|
|
||||||
@ -289,8 +291,29 @@ const routes: RouteRecordRaw[] = [
|
|||||||
{
|
{
|
||||||
path: 'marking-center',
|
path: 'marking-center',
|
||||||
name: 'MarkingCenter',
|
name: 'MarkingCenter',
|
||||||
component: MarkingCenter,
|
component: ExamManagement,
|
||||||
meta: { title: '阅卷中心' }
|
meta: { title: '阅卷中心' },
|
||||||
|
redirect: '/teacher/exam-management/marking-center/list',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'list',
|
||||||
|
name: 'MarkingList',
|
||||||
|
component: MarkingCenter,
|
||||||
|
meta: { title: '试卷列表' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'student-list/:paperId',
|
||||||
|
name: 'StudentList',
|
||||||
|
component: StudentList,
|
||||||
|
meta: { title: '阅卷页面' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'grading/:examId/:studentId',
|
||||||
|
name: 'GradingPage',
|
||||||
|
component: GradingPage,
|
||||||
|
meta: { title: '批阅试卷' }
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'add',
|
path: 'add',
|
||||||
@ -298,6 +321,12 @@ const routes: RouteRecordRaw[] = [
|
|||||||
component: AddExam,
|
component: AddExam,
|
||||||
meta: { title: '添加试卷' }
|
meta: { title: '添加试卷' }
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'preview',
|
||||||
|
name: 'ExamPreview',
|
||||||
|
component: () => import('../views/teacher/ExamPages/ExamPreview.vue'),
|
||||||
|
meta: { title: '试卷预览' }
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'add-question/:id?',
|
path: 'add-question/:id?',
|
||||||
name: 'AddQuestionPage',
|
name: 'AddQuestionPage',
|
||||||
@ -367,6 +396,14 @@ const routes: RouteRecordRaw[] = [
|
|||||||
meta: { title: 'AI伴学' }
|
meta: { title: 'AI伴学' }
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 登录页面
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
|
name: 'Login',
|
||||||
|
component: () => import('@/views/Login.vue'),
|
||||||
|
meta: { title: '登录' }
|
||||||
|
},
|
||||||
|
|
||||||
// 首页与课程
|
// 首页与课程
|
||||||
{
|
{
|
||||||
path: '/service-agreement',
|
path: '/service-agreement',
|
||||||
|
@ -1,10 +1,43 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="login-page">
|
<div class="login-page">
|
||||||
<div class="login-container">
|
<!-- 背景图片 -->
|
||||||
|
<div class="background-image">
|
||||||
|
<img src="/images/loginImage/backImage.png" alt="登录背景" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 左上角logo -->
|
||||||
|
<div class="top-logo">
|
||||||
|
<img src="/images/loginImage/logo.png" alt="云岭智教" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧登录区域 -->
|
||||||
|
<div class="login-area">
|
||||||
|
<!-- 学员端/教师端切换 -->
|
||||||
|
<div class="user-type-tabs">
|
||||||
|
<n-button
|
||||||
|
:type="activeTab === 'student' ? 'primary' : 'default'"
|
||||||
|
text
|
||||||
|
@click="activeTab = 'student'"
|
||||||
|
class="type-tab"
|
||||||
|
:class="{ active: activeTab === 'student' }"
|
||||||
|
>
|
||||||
|
学员端
|
||||||
|
</n-button>
|
||||||
|
<n-button
|
||||||
|
:type="activeTab === 'teacher' ? 'primary' : 'default'"
|
||||||
|
text
|
||||||
|
@click="activeTab = 'teacher'"
|
||||||
|
class="type-tab"
|
||||||
|
:class="{ active: activeTab === 'teacher' }"
|
||||||
|
>
|
||||||
|
教师端
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 登录表单 -->
|
||||||
<div class="login-form">
|
<div class="login-form">
|
||||||
<div class="form-header">
|
<div class="form-header">
|
||||||
<h1>登录</h1>
|
<h2>账号密码登录</h2>
|
||||||
<p>欢迎回到在线学习平台</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<n-form
|
<n-form
|
||||||
@ -14,18 +47,15 @@
|
|||||||
size="large"
|
size="large"
|
||||||
@submit.prevent="handleSubmit"
|
@submit.prevent="handleSubmit"
|
||||||
>
|
>
|
||||||
<n-form-item path="email" label="邮箱">
|
<n-form-item path="studentId" label="学号">
|
||||||
<n-input
|
<n-input
|
||||||
v-model:value="formData.email"
|
v-model:value="formData.studentId"
|
||||||
placeholder="请输入邮箱地址"
|
placeholder="2014195268"
|
||||||
type="email"
|
class="form-input"
|
||||||
>
|
/>
|
||||||
<template #prefix>
|
<div class="input-hint">
|
||||||
<n-icon>
|
没有账号?<n-button text type="primary" size="small">立即注册</n-button>
|
||||||
<MailOutline />
|
</div>
|
||||||
</n-icon>
|
|
||||||
</template>
|
|
||||||
</n-input>
|
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
|
|
||||||
<n-form-item path="password" label="密码">
|
<n-form-item path="password" label="密码">
|
||||||
@ -33,22 +63,17 @@
|
|||||||
v-model:value="formData.password"
|
v-model:value="formData.password"
|
||||||
placeholder="请输入密码"
|
placeholder="请输入密码"
|
||||||
type="password"
|
type="password"
|
||||||
show-password-on="mousedown"
|
show-password-on="click"
|
||||||
>
|
class="form-input"
|
||||||
<template #prefix>
|
/>
|
||||||
<n-icon>
|
|
||||||
<LockClosedOutline />
|
|
||||||
</n-icon>
|
|
||||||
</template>
|
|
||||||
</n-input>
|
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
|
|
||||||
<n-form-item>
|
<n-form-item>
|
||||||
<div class="form-options">
|
<div class="form-options">
|
||||||
<n-checkbox v-model:checked="rememberMe">
|
<n-checkbox v-model:checked="rememberMe" size="small">
|
||||||
记住我
|
下次自动登录
|
||||||
</n-checkbox>
|
</n-checkbox>
|
||||||
<n-button text type="primary">
|
<n-button text type="primary" size="small">
|
||||||
忘记密码?
|
忘记密码?
|
||||||
</n-button>
|
</n-button>
|
||||||
</div>
|
</div>
|
||||||
@ -61,6 +86,7 @@
|
|||||||
block
|
block
|
||||||
:loading="userStore.isLoading"
|
:loading="userStore.isLoading"
|
||||||
attr-type="submit"
|
attr-type="submit"
|
||||||
|
class="login-btn"
|
||||||
>
|
>
|
||||||
登录
|
登录
|
||||||
</n-button>
|
</n-button>
|
||||||
@ -68,45 +94,11 @@
|
|||||||
</n-form>
|
</n-form>
|
||||||
|
|
||||||
<div class="form-footer">
|
<div class="form-footer">
|
||||||
<p>
|
<p class="agreement-text">
|
||||||
还没有账号?
|
登录即同意我们的用户协议
|
||||||
<n-button text type="primary" @click="$router.push('/register')">
|
<n-button text type="primary" size="small">《服务协议和隐私政策》</n-button>
|
||||||
立即注册
|
|
||||||
</n-button>
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 社交登录 -->
|
|
||||||
<div class="social-login">
|
|
||||||
<n-divider>或使用以下方式登录</n-divider>
|
|
||||||
<n-space justify="center">
|
|
||||||
<n-button circle size="large">
|
|
||||||
<n-icon size="20">
|
|
||||||
<LogoGithub />
|
|
||||||
</n-icon>
|
|
||||||
</n-button>
|
|
||||||
<n-button circle size="large">
|
|
||||||
<n-icon size="20">
|
|
||||||
<LogoGoogle />
|
|
||||||
</n-icon>
|
|
||||||
</n-button>
|
|
||||||
<n-button circle size="large">
|
|
||||||
<n-icon size="20">
|
|
||||||
<LogoWechat />
|
|
||||||
</n-icon>
|
|
||||||
</n-button>
|
|
||||||
</n-space>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 侧边图片 -->
|
|
||||||
<div class="login-image">
|
|
||||||
<PlaceholderImage
|
|
||||||
:width="600"
|
|
||||||
:height="800"
|
|
||||||
text="登录背景图"
|
|
||||||
icon="🎨"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -117,14 +109,6 @@ import { ref, reactive } from 'vue'
|
|||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useMessage, type FormInst, type FormRules } from 'naive-ui'
|
import { useMessage, type FormInst, type FormRules } from 'naive-ui'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
import PlaceholderImage from '@/components/common/PlaceholderImage.vue'
|
|
||||||
import {
|
|
||||||
MailOutline,
|
|
||||||
LockClosedOutline,
|
|
||||||
LogoGithub,
|
|
||||||
LogoGoogle,
|
|
||||||
LogoWechat
|
|
||||||
} from '@vicons/ionicons5'
|
|
||||||
import { AuthApi } from '@/api'
|
import { AuthApi } from '@/api'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@ -133,24 +117,20 @@ const userStore = useUserStore()
|
|||||||
|
|
||||||
const formRef = ref<FormInst | null>(null)
|
const formRef = ref<FormInst | null>(null)
|
||||||
const rememberMe = ref(false)
|
const rememberMe = ref(false)
|
||||||
|
const activeTab = ref('student') // 当前选中的标签页
|
||||||
|
|
||||||
// 表单数据
|
// 表单数据
|
||||||
const formData = reactive({
|
const formData = reactive({
|
||||||
email: '',
|
studentId: '',
|
||||||
password: ''
|
password: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
// 表单验证规则
|
// 表单验证规则
|
||||||
const rules: FormRules = {
|
const rules: FormRules = {
|
||||||
email: [
|
studentId: [
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: '请输入邮箱地址',
|
message: '请输入学号',
|
||||||
trigger: ['input', 'blur']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'email',
|
|
||||||
message: '请输入有效的邮箱地址',
|
|
||||||
trigger: ['input', 'blur']
|
trigger: ['input', 'blur']
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@ -178,9 +158,9 @@ const handleSubmit = async () => {
|
|||||||
// 显示加载状态
|
// 显示加载状态
|
||||||
userStore.isLoading = true
|
userStore.isLoading = true
|
||||||
|
|
||||||
// 调用登录API
|
// 调用登录API - 使用学号登录
|
||||||
const response = await AuthApi.login({
|
const response = await AuthApi.login({
|
||||||
email: formData.email,
|
username: formData.studentId, // 使用username字段传递学号
|
||||||
password: formData.password
|
password: formData.password
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -231,9 +211,13 @@ const handleSubmit = async () => {
|
|||||||
|
|
||||||
message.success('登录成功!')
|
message.success('登录成功!')
|
||||||
|
|
||||||
// 登录成功后跳转到首页或之前的页面
|
// 根据用户类型跳转到不同页面
|
||||||
const redirect = router.currentRoute.value.query.redirect as string
|
const redirect = router.currentRoute.value.query.redirect as string
|
||||||
router.push(redirect || '/')
|
if (activeTab.value === 'teacher') {
|
||||||
|
router.push(redirect || '/teacher')
|
||||||
|
} else {
|
||||||
|
router.push(redirect || '/')
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
message.error(response.message || '登录失败')
|
message.error(response.message || '登录失败')
|
||||||
}
|
}
|
||||||
@ -242,7 +226,7 @@ const handleSubmit = async () => {
|
|||||||
|
|
||||||
// 处理不同类型的错误
|
// 处理不同类型的错误
|
||||||
if (error.response?.status === 401) {
|
if (error.response?.status === 401) {
|
||||||
message.error('邮箱或密码错误')
|
message.error('学号或密码错误')
|
||||||
} else if (error.response?.status === 429) {
|
} else if (error.response?.status === 429) {
|
||||||
message.error('登录尝试过于频繁,请稍后再试')
|
message.error('登录尝试过于频繁,请稍后再试')
|
||||||
} else if (error.response?.data?.message) {
|
} else if (error.response?.data?.message) {
|
||||||
@ -259,88 +243,337 @@ const handleSubmit = async () => {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.login-page {
|
.login-page {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding-right: 250px; /* 增加右侧边距 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 背景图片 - 居中显示,覆盖整个页面 */
|
||||||
|
.background-image {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
z-index: 1;
|
||||||
padding: 20px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-container {
|
.background-image img {
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
max-width: 1000px;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: white;
|
height: 100%;
|
||||||
border-radius: 16px;
|
object-fit: cover;
|
||||||
overflow: hidden;
|
object-position: center;
|
||||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-form {
|
/* 左上角logo */
|
||||||
padding: 60px 40px;
|
.top-logo {
|
||||||
|
position: absolute;
|
||||||
|
top: 32px;
|
||||||
|
left: 48px;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-logo img {
|
||||||
|
height: 48px;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 右侧登录区域 */
|
||||||
|
.login-area {
|
||||||
|
position: relative;
|
||||||
|
width: 516px;
|
||||||
|
min-height: 520px; /* 降低高度 */
|
||||||
|
z-index: 10;
|
||||||
|
background: transparent; /* 透明背景 */
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-header {
|
/* 用户类型切换标签 */
|
||||||
text-align: center;
|
.user-type-tabs {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 40px; /* 增加按钮间距 */
|
||||||
margin-bottom: 40px;
|
margin-bottom: 40px;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-header h1 {
|
.type-tab {
|
||||||
font-size: 2rem;
|
width: 84px;
|
||||||
|
height: 40px;
|
||||||
|
font-family: PingFangSC, PingFang SC;
|
||||||
|
font-weight: 400; /* 未点击状态字重 */
|
||||||
|
font-size: 24px;
|
||||||
|
color: #000000; /* 未点击状态颜色 */
|
||||||
|
line-height: 40px;
|
||||||
|
text-align: center;
|
||||||
|
font-style: normal;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
position: relative;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 激活状态样式 */
|
||||||
|
.type-tab.active {
|
||||||
|
font-weight: 500; /* 点击状态字重 */
|
||||||
|
color: #0288D1; /* 点击状态颜色 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 激活状态底部横线 */
|
||||||
|
.type-tab.active::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: -8px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 40px;
|
||||||
|
height: 3px;
|
||||||
|
background: #0288D1;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 悬停效果 */
|
||||||
|
.type-tab:hover {
|
||||||
|
color: #0288D1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 登录表单容器 */
|
||||||
|
.login-form {
|
||||||
|
background: rgba(255,255,255,0.5);
|
||||||
|
padding: 40px 35px; /* 减少内边距 */
|
||||||
|
border-radius: 12px; /* 添加适度的圆角 */
|
||||||
|
border: 2px solid #FFFFFF;
|
||||||
|
max-height: 630px; /* 限制最大高度 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表单样式 */
|
||||||
|
.form-header {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-header h2 {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
color: #333;
|
color: #333;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-form-item-label) {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-input) {
|
||||||
|
border-radius: 6px; /* 添加轻微圆角 */
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-input:hover) {
|
||||||
|
border-color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-input.n-input--focus) {
|
||||||
|
border-color: #1890ff;
|
||||||
|
box-shadow: none; /* 移除聚焦阴影 */
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-input__input-el) {
|
||||||
|
padding: 12px 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-header p {
|
.input-hint {
|
||||||
color: #666;
|
font-size: 12px;
|
||||||
font-size: 1rem;
|
color: #999;
|
||||||
|
text-align: right;
|
||||||
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-options {
|
.form-options {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-checkbox .n-checkbox__label) {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn {
|
||||||
|
background: #1890ff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px; /* 添加轻微圆角 */
|
||||||
|
height: 48px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-top: 20px;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.login-btn:hover) {
|
||||||
|
background: #40a9ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-footer {
|
.form-footer {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-top: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.social-login {
|
|
||||||
margin-top: 32px;
|
margin-top: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-image {
|
.agreement-text {
|
||||||
background: #f8f9fa;
|
font-size: 12px;
|
||||||
display: flex;
|
color: #999;
|
||||||
align-items: center;
|
margin: 0;
|
||||||
justify-content: center;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-image img {
|
/* 响应式设计 */
|
||||||
width: 100%;
|
@media (max-width: 1400px) {
|
||||||
height: 100%;
|
.login-page {
|
||||||
object-fit: cover;
|
padding-right: 180px; /* 调整右边距 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-area {
|
||||||
|
width: 500px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.login-page {
|
||||||
|
padding-right: 120px; /* 调整右边距 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-area {
|
||||||
|
width: 450px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.login-page {
|
||||||
|
justify-content: center;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-area {
|
||||||
|
width: 400px;
|
||||||
|
max-width: 90vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.background-image {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-logo {
|
||||||
|
top: 20px;
|
||||||
|
left: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-type-tabs {
|
||||||
|
justify-content: center;
|
||||||
|
gap: 32px; /* 中等屏幕间距 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-tab {
|
||||||
|
font-size: 24px;
|
||||||
|
width: 70px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.login-container {
|
.login-page {
|
||||||
grid-template-columns: 1fr;
|
justify-content: center;
|
||||||
max-width: 400px;
|
padding-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-image {
|
.login-area {
|
||||||
display: none;
|
width: 350px;
|
||||||
|
max-width: 85vw;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-form {
|
.login-form {
|
||||||
padding: 40px 24px;
|
padding: 36px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-logo {
|
||||||
|
top: 16px;
|
||||||
|
left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-logo img {
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-type-tabs {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
gap: 28px; /* 小屏幕间距 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-tab {
|
||||||
|
font-size: 20px;
|
||||||
|
width: 60px;
|
||||||
|
height: 35px;
|
||||||
|
line-height: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-tab.active::after {
|
||||||
|
width: 30px;
|
||||||
|
height: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.login-page {
|
||||||
|
justify-content: center;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-area {
|
||||||
|
width: 300px;
|
||||||
|
max-width: 90vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form {
|
||||||
|
padding: 32px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-header h2 {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-logo {
|
||||||
|
top: 12px;
|
||||||
|
left: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-tab {
|
||||||
|
font-size: 18px;
|
||||||
|
width: 55px;
|
||||||
|
height: 32px;
|
||||||
|
line-height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-tab.active::after {
|
||||||
|
width: 25px;
|
||||||
|
height: 2px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -45,7 +45,7 @@
|
|||||||
:class="{ active: activeSubNavItem === 'exam-library' }" @click="setActiveSubNavItem('exam-library')">
|
:class="{ active: activeSubNavItem === 'exam-library' }" @click="setActiveSubNavItem('exam-library')">
|
||||||
<span>试卷管理</span>
|
<span>试卷管理</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link to="/teacher/exam-management/marking-center" class="submenu-item"
|
<router-link to="/teacher/exam-management/marking-center/list" class="submenu-item"
|
||||||
:class="{ active: activeSubNavItem === 'marking-center' }" @click="setActiveSubNavItem('marking-center')">
|
:class="{ active: activeSubNavItem === 'marking-center' }" @click="setActiveSubNavItem('marking-center')">
|
||||||
<span>阅卷中心</span>
|
<span>阅卷中心</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
@ -429,11 +429,10 @@ const updateActiveNavItem = () => {
|
|||||||
} else if (path.includes('exam-management')) {
|
} else if (path.includes('exam-management')) {
|
||||||
activeNavItem.value = 4; // 考试管理
|
activeNavItem.value = 4; // 考试管理
|
||||||
examMenuExpanded.value = true;
|
examMenuExpanded.value = true;
|
||||||
|
|
||||||
// 获取路由的最后一层路径(不包含/)
|
const arr = ['question-management', 'exam-library', 'marking-center'];
|
||||||
const pathSegments = path.split('/');
|
const found = arr.find(item => path.includes(item));
|
||||||
const lastSegment = pathSegments[pathSegments.length - 1];
|
activeSubNavItem.value = found || '';
|
||||||
activeSubNavItem.value = lastSegment || '';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,6 +17,14 @@
|
|||||||
</template>
|
</template>
|
||||||
</n-button>
|
</n-button>
|
||||||
<h1>添加试卷</h1>
|
<h1>添加试卷</h1>
|
||||||
|
<span v-if="isAutoSaved" class="auto-save-indicator">
|
||||||
|
<n-icon size="14" color="#52c41a">
|
||||||
|
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||||
|
</svg>
|
||||||
|
</n-icon>
|
||||||
|
已自动保存
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<n-card size="small">
|
<n-card size="small">
|
||||||
@ -55,7 +63,7 @@
|
|||||||
<template v-for="(item, index) in examForm.questions" :key="index">
|
<template v-for="(item, index) in examForm.questions" :key="index">
|
||||||
<n-card size="small">
|
<n-card size="small">
|
||||||
<div class="group">
|
<div class="group">
|
||||||
<n-row>第{{ index + 1 }}题:</n-row>
|
<n-row>第{{ index + 1 }}大题:</n-row>
|
||||||
<div class="questionRow">
|
<div class="questionRow">
|
||||||
<n-input class="input-title" v-model:value="item.title" placeholder="请输入题目名称" />
|
<n-input class="input-title" v-model:value="item.title" placeholder="请输入题目名称" />
|
||||||
<n-button strong quaternary @click="deleteBigQuestion(index)">
|
<n-button strong quaternary @click="deleteBigQuestion(index)">
|
||||||
@ -119,7 +127,7 @@
|
|||||||
class="sub-question-item">
|
class="sub-question-item">
|
||||||
<!-- 小题标题栏 -->
|
<!-- 小题标题栏 -->
|
||||||
<div class="sub-question-header">
|
<div class="sub-question-header">
|
||||||
<span class="sub-question-number">*{{ index + 1 }}.{{ subIndex + 1 }} {{
|
<span class="sub-question-number">*{{ subIndex + 1 }} {{
|
||||||
getQuestionTypeName(subQuestion.type) }}</span>
|
getQuestionTypeName(subQuestion.type) }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -422,7 +430,7 @@
|
|||||||
</template>
|
</template>
|
||||||
试卷设置
|
试卷设置
|
||||||
</n-button>
|
</n-button>
|
||||||
<n-button type="primary" ghost size="large">
|
<n-button type="primary" ghost size="large" @click="previewExam">
|
||||||
预览试卷
|
预览试卷
|
||||||
</n-button>
|
</n-button>
|
||||||
</n-space>
|
</n-space>
|
||||||
@ -467,7 +475,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, reactive, ref } from 'vue';
|
import { computed, reactive, ref, onMounted, onUnmounted, watch } from 'vue';
|
||||||
import { createDiscreteApi } from 'naive-ui';
|
import { createDiscreteApi } from 'naive-ui';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { AddCircle, SettingsOutline, TrashOutline, ChevronUpSharp, BookSharp, ArrowBackOutline } from '@vicons/ionicons5'
|
import { AddCircle, SettingsOutline, TrashOutline, ChevronUpSharp, BookSharp, ArrowBackOutline } from '@vicons/ionicons5'
|
||||||
@ -483,7 +491,32 @@ const router = useRouter()
|
|||||||
|
|
||||||
// 返回上一个页面
|
// 返回上一个页面
|
||||||
const goBack = () => {
|
const goBack = () => {
|
||||||
router.back()
|
// 检查是否有未保存的内容
|
||||||
|
const hasContent = examForm.title.trim() ||
|
||||||
|
examForm.questions.some(q => q.subQuestions.length > 0) ||
|
||||||
|
examForm.description.trim()
|
||||||
|
|
||||||
|
if (hasContent) {
|
||||||
|
dialog.warning({
|
||||||
|
title: '确认返回',
|
||||||
|
content: '返回将清空当前编辑的试卷内容,确定要返回吗?',
|
||||||
|
positiveText: '确定返回',
|
||||||
|
negativeText: '继续编辑',
|
||||||
|
onPositiveClick: () => {
|
||||||
|
// 清空 sessionStorage 中的试卷数据
|
||||||
|
sessionStorage.removeItem('examPreviewData')
|
||||||
|
// 返回上一个页面
|
||||||
|
router.back()
|
||||||
|
},
|
||||||
|
onNegativeClick: () => {
|
||||||
|
// 用户选择继续编辑,不做任何操作
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// 没有编辑内容,直接返回
|
||||||
|
sessionStorage.removeItem('examPreviewData')
|
||||||
|
router.back()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 题型枚举
|
// 题型枚举
|
||||||
@ -642,6 +675,7 @@ const addQuestion = (index: number) => {
|
|||||||
{ id: '4', content: '选项D', isCorrect: false }
|
{ id: '4', content: '选项D', isCorrect: false }
|
||||||
];
|
];
|
||||||
newSubQuestion.correctAnswer = '';
|
newSubQuestion.correctAnswer = '';
|
||||||
|
newSubQuestion.explanation = '';
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case QuestionType.MULTIPLE_CHOICE:
|
case QuestionType.MULTIPLE_CHOICE:
|
||||||
@ -652,24 +686,30 @@ const addQuestion = (index: number) => {
|
|||||||
{ id: '4', content: '选项D', isCorrect: false }
|
{ id: '4', content: '选项D', isCorrect: false }
|
||||||
];
|
];
|
||||||
newSubQuestion.correctAnswer = [];
|
newSubQuestion.correctAnswer = [];
|
||||||
|
newSubQuestion.explanation = '';
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case QuestionType.TRUE_FALSE:
|
case QuestionType.TRUE_FALSE:
|
||||||
newSubQuestion.trueFalseAnswer = undefined; // 初始状态不选择任何选项
|
newSubQuestion.trueFalseAnswer = undefined; // 初始状态不选择任何选项
|
||||||
|
newSubQuestion.explanation = '';
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case QuestionType.FILL_BLANK:
|
case QuestionType.FILL_BLANK:
|
||||||
newSubQuestion.fillBlanks = [
|
newSubQuestion.fillBlanks = [
|
||||||
{ id: '1', content: '', position: 1 }
|
{ id: '1', content: '', position: 1 }
|
||||||
];
|
];
|
||||||
|
newSubQuestion.explanation = '';
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case QuestionType.SHORT_ANSWER:
|
case QuestionType.SHORT_ANSWER:
|
||||||
newSubQuestion.textAnswer = '';
|
newSubQuestion.textAnswer = '';
|
||||||
|
newSubQuestion.explanation = '';
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case QuestionType.COMPOSITE:
|
case QuestionType.COMPOSITE:
|
||||||
newSubQuestion.subQuestions = [];
|
newSubQuestion.subQuestions = [];
|
||||||
|
newSubQuestion.explanation = '';
|
||||||
|
break;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1135,6 +1175,46 @@ const saveExam = () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 预览试卷
|
||||||
|
const previewExam = () => {
|
||||||
|
// 先保存当前编辑状态到 sessionStorage,无论验证是否通过
|
||||||
|
const examData = {
|
||||||
|
...examForm,
|
||||||
|
previewTime: new Date().toISOString()
|
||||||
|
};
|
||||||
|
sessionStorage.setItem('examPreviewData', JSON.stringify(examData));
|
||||||
|
|
||||||
|
// 验证数据
|
||||||
|
if (!examForm.title.trim()) {
|
||||||
|
dialog.warning({
|
||||||
|
title: '输入提示',
|
||||||
|
content: '请输入试卷标题',
|
||||||
|
positiveText: '确定'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let hasQuestions = false;
|
||||||
|
for (const bigQuestion of examForm.questions) {
|
||||||
|
if (bigQuestion.subQuestions.length > 0) {
|
||||||
|
hasQuestions = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasQuestions) {
|
||||||
|
dialog.warning({
|
||||||
|
title: '输入提示',
|
||||||
|
content: '请至少添加一道题目再进行预览',
|
||||||
|
positiveText: '确定'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 跳转到预览页面
|
||||||
|
router.push('/teacher/exam-management/preview');
|
||||||
|
}
|
||||||
|
|
||||||
// 复合题相关方法
|
// 复合题相关方法
|
||||||
const addCompositeSubQuestion = (bigQuestionIndex: number, subQuestionIndex: number) => {
|
const addCompositeSubQuestion = (bigQuestionIndex: number, subQuestionIndex: number) => {
|
||||||
const subQuestion = examForm.questions[bigQuestionIndex].subQuestions[subQuestionIndex];
|
const subQuestion = examForm.questions[bigQuestionIndex].subQuestions[subQuestionIndex];
|
||||||
@ -1156,6 +1236,7 @@ const addCompositeSubQuestion = (bigQuestionIndex: number, subQuestionIndex: num
|
|||||||
{ id: '4', content: '选项D', isCorrect: false }
|
{ id: '4', content: '选项D', isCorrect: false }
|
||||||
],
|
],
|
||||||
correctAnswer: '',
|
correctAnswer: '',
|
||||||
|
explanation: '这是复合题子题的答案解析示例。',
|
||||||
createTime: new Date().toISOString()
|
createTime: new Date().toISOString()
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1357,6 +1438,77 @@ const previewSubQuestion = (bigQuestionIndex: number, subQuestionIndex: number)
|
|||||||
console.log('预览题目:', subQuestion);
|
console.log('预览题目:', subQuestion);
|
||||||
// 这里可以实现题目预览功能,比如打开一个模态框显示题目
|
// 这里可以实现题目预览功能,比如打开一个模态框显示题目
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 恢复试卷数据
|
||||||
|
const restoreExamData = () => {
|
||||||
|
const storedData = sessionStorage.getItem('examPreviewData');
|
||||||
|
if (storedData) {
|
||||||
|
try {
|
||||||
|
const parsedData = JSON.parse(storedData);
|
||||||
|
|
||||||
|
// 恢复试卷基本信息
|
||||||
|
examForm.title = parsedData.title || '';
|
||||||
|
examForm.type = parsedData.type || 1;
|
||||||
|
examForm.description = parsedData.description || '';
|
||||||
|
examForm.totalScore = parsedData.totalScore || 0;
|
||||||
|
examForm.duration = parsedData.duration || 60;
|
||||||
|
examForm.passScore = parsedData.passScore || 60;
|
||||||
|
examForm.instructions = parsedData.instructions || '';
|
||||||
|
examForm.useAIGrading = parsedData.useAIGrading || false;
|
||||||
|
|
||||||
|
// 恢复试卷题目
|
||||||
|
if (parsedData.questions && Array.isArray(parsedData.questions)) {
|
||||||
|
examForm.questions = parsedData.questions;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('试卷数据已恢复:', examForm);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('恢复试卷数据失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面挂载时尝试恢复数据
|
||||||
|
onMounted(() => {
|
||||||
|
restoreExamData();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 页面卸载时清理定时器
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (autoSaveTimer) {
|
||||||
|
clearTimeout(autoSaveTimer);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听试卷数据变化,自动保存到 sessionStorage
|
||||||
|
watch(
|
||||||
|
() => examForm,
|
||||||
|
(newValue) => {
|
||||||
|
// 防抖处理,避免频繁保存
|
||||||
|
isAutoSaved.value = false; // 数据变化时标记为未保存
|
||||||
|
clearTimeout(autoSaveTimer);
|
||||||
|
autoSaveTimer = setTimeout(() => {
|
||||||
|
const examData = {
|
||||||
|
...newValue,
|
||||||
|
lastModified: new Date().toISOString()
|
||||||
|
};
|
||||||
|
sessionStorage.setItem('examPreviewData', JSON.stringify(examData));
|
||||||
|
isAutoSaved.value = true; // 保存完成后标记为已保存
|
||||||
|
|
||||||
|
// 3秒后隐藏保存提示
|
||||||
|
setTimeout(() => {
|
||||||
|
isAutoSaved.value = false;
|
||||||
|
}, 3000);
|
||||||
|
}, 1000); // 1秒后保存
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
// 自动保存定时器
|
||||||
|
let autoSaveTimer: NodeJS.Timeout;
|
||||||
|
|
||||||
|
// 自动保存状态指示
|
||||||
|
const isAutoSaved = ref(false);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@ -1386,6 +1538,29 @@ const previewSubQuestion = (bigQuestionIndex: number, subQuestionIndex: number)
|
|||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.auto-save-indicator {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #52c41a;
|
||||||
|
padding: 2px 8px;
|
||||||
|
background: rgba(82, 196, 26, 0.1);
|
||||||
|
border-radius: 12px;
|
||||||
|
animation: fadeIn 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-4px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.back-button {
|
.back-button {
|
||||||
color: #666;
|
color: #666;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
|
@ -168,11 +168,12 @@ const paginationConfig = computed(() => ({
|
|||||||
pageSizes: [10, 20, 50, 100],
|
pageSizes: [10, 20, 50, 100],
|
||||||
showSizePicker: true,
|
showSizePicker: true,
|
||||||
showQuickJumper: true,
|
showQuickJumper: true,
|
||||||
prefix: (info: { startIndex: number; endIndex: number; page: number; pageSize: number; pageCount: number; itemCount?: number }) => {
|
goto: ()=>{
|
||||||
|
return '跳转'
|
||||||
|
},
|
||||||
|
prefix: (info: { itemCount?: number }) => {
|
||||||
const itemCount = info.itemCount || 0;
|
const itemCount = info.itemCount || 0;
|
||||||
const start = (currentPage.value - 1) * pageSize.value + 1;
|
return `共 ${itemCount} 条`;
|
||||||
const end = Math.min(currentPage.value * pageSize.value, itemCount);
|
|
||||||
return `显示 ${start}-${end} 条,共 ${itemCount} 条`;
|
|
||||||
},
|
},
|
||||||
onUpdatePage: (page: number) => {
|
onUpdatePage: (page: number) => {
|
||||||
currentPage.value = page;
|
currentPage.value = page;
|
||||||
|
704
src/views/teacher/ExamPages/ExamPreview.vue
Normal file
704
src/views/teacher/ExamPages/ExamPreview.vue
Normal file
@ -0,0 +1,704 @@
|
|||||||
|
<template>
|
||||||
|
<div class="exam-preview-container">
|
||||||
|
<!-- 左侧时间和题目导航 -->
|
||||||
|
<div class="exam-sidebar">
|
||||||
|
<div class="exam-title">
|
||||||
|
<n-button quaternary circle size="large" @click="goBack" class="back-button">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon>
|
||||||
|
<ArrowBackOutline />
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
<h1 class="exam-title">{{ examData?.examName || '试卷预览' }}</h1>
|
||||||
|
</div>
|
||||||
|
<n-divider />
|
||||||
|
<div class="time-section">
|
||||||
|
<div class="time-label">剩余时间</div>
|
||||||
|
<div class="time-display">{{ formatTime(examData?.duration * 60 || 3600) }}</div>
|
||||||
|
<div class="time-units">
|
||||||
|
<span>时</span>
|
||||||
|
<span>分</span>
|
||||||
|
<span>秒</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="question-nav">
|
||||||
|
<div class="nav-title">答题卡</div>
|
||||||
|
<div class="nav-grid">
|
||||||
|
<div v-for="(_, index) in allQuestions" :key="index" class="nav-item"
|
||||||
|
:class="{ 'answered': false }">
|
||||||
|
{{ index + 1 }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧主要内容区域 -->
|
||||||
|
<div class="exam-main">
|
||||||
|
<div class="header-content">
|
||||||
|
<div class="exam-info">
|
||||||
|
<div class="exam-meta">
|
||||||
|
<span>总分:{{ examData?.totalScore || 0 }}分</span>
|
||||||
|
<span>题目数量:{{ totalQuestions }}题</span>
|
||||||
|
<span>考试时长:{{ examData?.duration || 0 }}分钟</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="exam-content" v-if="examData">
|
||||||
|
<!-- 大题循环 -->
|
||||||
|
<div v-for="(bigQuestion, bigIndex) in examData.questions" :key="bigQuestion.id" class="big-question">
|
||||||
|
<div class="big-question-header">
|
||||||
|
<span class="question-desc">{{ bigQuestion.title }}</span>
|
||||||
|
<span class="question-count">共{{ bigQuestion.subQuestions.length }}题</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 小题循环 -->
|
||||||
|
<div v-for="(subQuestion, subIndex) in bigQuestion.subQuestions" :key="subQuestion.id"
|
||||||
|
class="sub-question">
|
||||||
|
<div class="question-header">
|
||||||
|
<span class="question-number">第{{ getQuestionNumber(bigIndex, subIndex) }}题 {{
|
||||||
|
getQuestionTypeName(subQuestion.type) }}</span>
|
||||||
|
<span class="question-score">{{ subQuestion.score }}分</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="question-title">{{ subQuestion.title }}</div>
|
||||||
|
|
||||||
|
<!-- 单选题 -->
|
||||||
|
<div v-if="subQuestion.type === 'single_choice'" class="question-content">
|
||||||
|
<div class="options">
|
||||||
|
<div v-for="(option, optIndex) in subQuestion.options" :key="option.id"
|
||||||
|
class="option-item" :class="{ 'correct-option': option.isCorrect }">
|
||||||
|
<span class="option-label">{{ String.fromCharCode(65 + optIndex) }}</span>
|
||||||
|
<span class="option-content">{{ option.content }}</span>
|
||||||
|
<span v-if="option.isCorrect" class="correct-mark">✓</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 答案解析 -->
|
||||||
|
<div class="answer-analysis">
|
||||||
|
<div class="correct-answer">
|
||||||
|
<span class="label">正确答案:</span>
|
||||||
|
<span class="answer">{{ getCorrectAnswerText(subQuestion) }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="subQuestion.explanation" class="explanation">
|
||||||
|
<span class="label">答案解析:</span>
|
||||||
|
<p>{{ subQuestion.explanation }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 多选题 -->
|
||||||
|
<div v-if="subQuestion.type === 'multiple_choice'" class="question-content">
|
||||||
|
<div class="options">
|
||||||
|
<div v-for="(option, optIndex) in subQuestion.options" :key="option.id"
|
||||||
|
class="option-item" :class="{ 'correct-option': option.isCorrect }">
|
||||||
|
<span class="option-label">{{ String.fromCharCode(65 + optIndex) }}</span>
|
||||||
|
<span class="option-content">{{ option.content }}</span>
|
||||||
|
<span v-if="option.isCorrect" class="correct-mark">✓</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 答案解析 -->
|
||||||
|
<div class="answer-analysis">
|
||||||
|
<div class="correct-answer">
|
||||||
|
<span class="label">正确答案:</span>
|
||||||
|
<span class="answer">{{ getCorrectAnswerText(subQuestion) }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="subQuestion.explanation" class="explanation">
|
||||||
|
<span class="label">答案解析:</span>
|
||||||
|
<p>{{ subQuestion.explanation }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 判断题 -->
|
||||||
|
<div v-if="subQuestion.type === 'true_false'" class="question-content">
|
||||||
|
<div class="true-false-options">
|
||||||
|
<div class="option-item"
|
||||||
|
:class="{ 'correct-option': subQuestion.trueFalseAnswer === true }">
|
||||||
|
<span class="option-label">A</span>
|
||||||
|
<span class="option-content">正确</span>
|
||||||
|
<span v-if="subQuestion.trueFalseAnswer === true" class="correct-mark">✓</span>
|
||||||
|
</div>
|
||||||
|
<div class="option-item"
|
||||||
|
:class="{ 'correct-option': subQuestion.trueFalseAnswer === false }">
|
||||||
|
<span class="option-label">B</span>
|
||||||
|
<span class="option-content">错误</span>
|
||||||
|
<span v-if="subQuestion.trueFalseAnswer === false" class="correct-mark">✓</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 答案解析 -->
|
||||||
|
<div class="answer-analysis">
|
||||||
|
<div class="correct-answer">
|
||||||
|
<span class="label">正确答案:</span>
|
||||||
|
<span class="answer">{{ subQuestion.trueFalseAnswer ? 'A 正确' : 'B 错误' }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="subQuestion.explanation" class="explanation">
|
||||||
|
<span class="label">答案解析:</span>
|
||||||
|
<p>{{ subQuestion.explanation }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 填空题 -->
|
||||||
|
<div v-if="subQuestion.type === 'fill_blank'" class="question-content">
|
||||||
|
<div class="fill-blanks">
|
||||||
|
<div v-for="blank in subQuestion.fillBlanks" :key="blank.id" class="blank-item">
|
||||||
|
<span>第{{ blank.position }}空:</span>
|
||||||
|
<div class="blank-answer">{{ blank.content || '未设置' }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 答案解析 -->
|
||||||
|
<div class="answer-analysis">
|
||||||
|
<div class="correct-answer">
|
||||||
|
<span class="label">正确答案:</span>
|
||||||
|
<div class="answer">
|
||||||
|
<div v-for="blank in subQuestion.fillBlanks" :key="blank.id">
|
||||||
|
第{{ blank.position }}空:{{ blank.content || '未设置' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="subQuestion.explanation" class="explanation">
|
||||||
|
<span class="label">答案解析:</span>
|
||||||
|
<p>{{ subQuestion.explanation }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 简答题 -->
|
||||||
|
<div v-if="subQuestion.type === 'short_answer'" class="question-content">
|
||||||
|
<div class="answer-area">
|
||||||
|
<div class="answer-text">{{ subQuestion.textAnswer || '未设置参考答案' }}</div>
|
||||||
|
</div>
|
||||||
|
<!-- 答案解析 -->
|
||||||
|
<div class="answer-analysis">
|
||||||
|
<div class="correct-answer">
|
||||||
|
<span class="label">参考答案:</span>
|
||||||
|
<p class="answer">{{ subQuestion.textAnswer || '未设置参考答案' }}</p>
|
||||||
|
</div>
|
||||||
|
<div v-if="subQuestion.explanation" class="explanation">
|
||||||
|
<span class="label">答案解析:</span>
|
||||||
|
<p>{{ subQuestion.explanation }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="no-data">
|
||||||
|
<n-empty description="没有找到试卷数据">
|
||||||
|
<template #extra>
|
||||||
|
<n-button @click="goBack">返回</n-button>
|
||||||
|
</template>
|
||||||
|
</n-empty>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, computed } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { NButton, NEmpty } from 'naive-ui'
|
||||||
|
import { ArrowBackOutline } from '@vicons/ionicons5'
|
||||||
|
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const examData = ref<any>(null)
|
||||||
|
|
||||||
|
// 计算所有题目数量和列表
|
||||||
|
const allQuestions = computed(() => {
|
||||||
|
if (!examData.value) return []
|
||||||
|
const questions: any[] = []
|
||||||
|
examData.value.questions.forEach((bigQ: any) => {
|
||||||
|
bigQ.subQuestions.forEach((subQ: any) => {
|
||||||
|
if (subQ.type === 'composite') {
|
||||||
|
subQ.subQuestions?.forEach((compSubQ: any) => {
|
||||||
|
questions.push(compSubQ)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
questions.push(subQ)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return questions
|
||||||
|
})
|
||||||
|
|
||||||
|
const totalQuestions = computed(() => {
|
||||||
|
return allQuestions.value.length
|
||||||
|
})
|
||||||
|
|
||||||
|
// 时间格式化函数
|
||||||
|
const formatTime = (seconds: number) => {
|
||||||
|
const hours = Math.floor(seconds / 3600)
|
||||||
|
const minutes = Math.floor((seconds % 3600) / 60)
|
||||||
|
const secs = seconds % 60
|
||||||
|
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取题型名称
|
||||||
|
const getQuestionTypeName = (type: string) => {
|
||||||
|
const typeMap: Record<string, string> = {
|
||||||
|
'single_choice': '单选题',
|
||||||
|
'multiple_choice': '多选题',
|
||||||
|
'true_false': '判断题',
|
||||||
|
'fill_blank': '填空题',
|
||||||
|
'short_answer': '简答题',
|
||||||
|
'composite': '复合题'
|
||||||
|
}
|
||||||
|
return typeMap[type] || '未知题型'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取题目编号
|
||||||
|
const getQuestionNumber = (bigIndex: number, subIndex: number) => {
|
||||||
|
let number = 1
|
||||||
|
for (let i = 0; i < bigIndex; i++) {
|
||||||
|
number += examData.value.questions[i].subQuestions.length
|
||||||
|
}
|
||||||
|
return number + subIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取正确答案文本
|
||||||
|
const getCorrectAnswerText = (question: any) => {
|
||||||
|
if (question.type === 'single_choice') {
|
||||||
|
const correctOption = question.options?.find((opt: any) => opt.isCorrect)
|
||||||
|
if (correctOption) {
|
||||||
|
const index = question.options.findIndex((opt: any) => opt.isCorrect)
|
||||||
|
return `${String.fromCharCode(65 + index)}`
|
||||||
|
}
|
||||||
|
return '未设置正确答案'
|
||||||
|
} else if (question.type === 'multiple_choice') {
|
||||||
|
const correctOptions = question.options?.filter((opt: any) => opt.isCorrect)
|
||||||
|
if (correctOptions?.length > 0) {
|
||||||
|
return correctOptions.map((opt: any) => {
|
||||||
|
const optIndex = question.options.findIndex((o: any) => o.id === opt.id)
|
||||||
|
return `${String.fromCharCode(65 + optIndex)}`
|
||||||
|
}).join('、')
|
||||||
|
}
|
||||||
|
return '未设置正确答案'
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const goBack = () => {
|
||||||
|
router.back()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 从 sessionStorage 获取试卷数据
|
||||||
|
const savedData = sessionStorage.getItem('examPreviewData')
|
||||||
|
if (savedData) {
|
||||||
|
try {
|
||||||
|
examData.value = JSON.parse(savedData)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('解析试卷数据失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.exam-preview-container {
|
||||||
|
display: flex;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 左侧边栏样式 */
|
||||||
|
.exam-sidebar {
|
||||||
|
width: 280px;
|
||||||
|
background: white;
|
||||||
|
padding: 10px;
|
||||||
|
border-right: 1px solid #e8e8e8;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exam-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 24px;
|
||||||
|
color: #333;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-section {
|
||||||
|
background: #f0f9ff;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-display {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #1890ff;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-units {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-nav {
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 1px solid #e8e8e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(6, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item.answered {
|
||||||
|
background: #1890ff;
|
||||||
|
color: white;
|
||||||
|
border-color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-legend {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-color {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-color.answered {
|
||||||
|
background: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 右侧主要内容区域 */
|
||||||
|
.exam-main {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.header-content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exam-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exam-meta {
|
||||||
|
display: flex;
|
||||||
|
gap: 24px;
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
justify-content: flex-end
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-note {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exam-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.big-question {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.big-question-header {
|
||||||
|
padding: 15px 20px;
|
||||||
|
border-bottom: 1px solid #e8e8e8;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-desc {
|
||||||
|
color: #1890ff;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 19px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-count {
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-question {
|
||||||
|
padding: 20px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-question:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-number {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-score {
|
||||||
|
background: #f0f9ff;
|
||||||
|
color: #1890ff;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-title {
|
||||||
|
font-size: 15px;
|
||||||
|
color: #333;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-content {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.options {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
background-color: #F5F8FB;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-item.correct-option {
|
||||||
|
background: #f6ffed;
|
||||||
|
border-color: #52c41a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #666;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.correct-option .option-label {
|
||||||
|
background: #52c41a;
|
||||||
|
color: white;
|
||||||
|
border-color: #52c41a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-content {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.correct-mark {
|
||||||
|
color: #52c41a;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.true-false-options {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fill-blanks {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blank-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blank-answer {
|
||||||
|
background: #f0f9ff;
|
||||||
|
border: 1px solid #1890ff;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
min-width: 100px;
|
||||||
|
color: #1890ff;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer-area {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer-text {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 1px solid #e8e8e8;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 6px;
|
||||||
|
min-height: 80px;
|
||||||
|
color: #333;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 答案解析样式 */
|
||||||
|
.answer-analysis {
|
||||||
|
background: #fafbfc;
|
||||||
|
border: 1px solid #e1e4e8;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 16px;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.correct-answer {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.correct-answer .label {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #52c41a;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.correct-answer .answer {
|
||||||
|
color: #333;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.explanation .label {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #1890ff;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.explanation p {
|
||||||
|
margin: 4px 0 0 0;
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-data {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 60px;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.exam-sidebar {
|
||||||
|
width: 240px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exam-main {
|
||||||
|
margin-left: 240px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.exam-preview-container {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exam-sidebar {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exam-main {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-grid {
|
||||||
|
grid-template-columns: repeat(8, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
1070
src/views/teacher/ExamPages/GradingPage.vue
Normal file
1070
src/views/teacher/ExamPages/GradingPage.vue
Normal file
File diff suppressed because it is too large
Load Diff
@ -125,7 +125,6 @@
|
|||||||
:page-size="pageSize"
|
:page-size="pageSize"
|
||||||
show-size-picker
|
show-size-picker
|
||||||
:page-sizes="[10, 20, 50]"
|
:page-sizes="[10, 20, 50]"
|
||||||
show-quick-jumper
|
|
||||||
:item-count="totalItems"
|
:item-count="totalItems"
|
||||||
@update:page="handlePageChange"
|
@update:page="handlePageChange"
|
||||||
@update:page-size="handlePageSizeChange"
|
@update:page-size="handlePageSizeChange"
|
||||||
@ -136,6 +135,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
import { PersonOutline, CalendarOutline } from '@vicons/ionicons5'
|
import { PersonOutline, CalendarOutline } from '@vicons/ionicons5'
|
||||||
|
|
||||||
// 接口定义
|
// 接口定义
|
||||||
@ -151,6 +151,9 @@ interface ExamItem {
|
|||||||
gradedCount: number
|
gradedCount: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 路由
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
// 响应式数据
|
// 响应式数据
|
||||||
const activeTab = ref('all')
|
const activeTab = ref('all')
|
||||||
const examFilter = ref('')
|
const examFilter = ref('')
|
||||||
@ -275,7 +278,11 @@ const handleDelete = (exam: ExamItem) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleAction = (exam: ExamItem) => {
|
const handleAction = (exam: ExamItem) => {
|
||||||
console.log('执行操作:', exam)
|
// 跳转到学生列表页面,传递考试ID
|
||||||
|
router.push({
|
||||||
|
name: 'StudentList',
|
||||||
|
params: { paperId: exam.id }
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlePageChange = (page: number) => {
|
const handlePageChange = (page: number) => {
|
||||||
|
@ -241,11 +241,12 @@ const paginationConfig = computed(() => ({
|
|||||||
pageSizes: [10, 20, 50, 100],
|
pageSizes: [10, 20, 50, 100],
|
||||||
showSizePicker: true,
|
showSizePicker: true,
|
||||||
showQuickJumper: true,
|
showQuickJumper: true,
|
||||||
prefix: (info: { startIndex: number; endIndex: number; page: number; pageSize: number; pageCount: number; itemCount?: number }) => {
|
goto: ()=>{
|
||||||
|
return '跳转'
|
||||||
|
},
|
||||||
|
prefix: (info: { itemCount?: number }) => {
|
||||||
const itemCount = info.itemCount || 0;
|
const itemCount = info.itemCount || 0;
|
||||||
const start = (pagination.page - 1) * pagination.pageSize + 1;
|
return `共 ${itemCount} 条`;
|
||||||
const end = Math.min(pagination.page * pagination.pageSize, itemCount);
|
|
||||||
return `显示 ${start}-${end} 条,共 ${itemCount} 条`;
|
|
||||||
},
|
},
|
||||||
onUpdatePage: (page: number) => {
|
onUpdatePage: (page: number) => {
|
||||||
pagination.page = page;
|
pagination.page = page;
|
||||||
|
765
src/views/teacher/ExamPages/StudentList.vue
Normal file
765
src/views/teacher/ExamPages/StudentList.vue
Normal file
@ -0,0 +1,765 @@
|
|||||||
|
<template>
|
||||||
|
<div class="student-list-container">
|
||||||
|
<!-- 页面头部 -->
|
||||||
|
<div class="header-section">
|
||||||
|
<div class="header-left">
|
||||||
|
<n-button quaternary circle size="large" @click="goBack" class="back-button">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon>
|
||||||
|
<ArrowBackOutline />
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
<div class="header-info">
|
||||||
|
<h1 class="page-title">{{ examInfo.title || '试卷名' }}</h1>
|
||||||
|
<div class="exam-meta">
|
||||||
|
<n-tag :type="getStatusType(examInfo.status)" :bordered="false" size="small">
|
||||||
|
{{ getStatusText(examInfo.status) }}
|
||||||
|
</n-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actions-group">
|
||||||
|
<n-button type="primary" @click="publishExam">
|
||||||
|
发布补考
|
||||||
|
</n-button>
|
||||||
|
<n-button ghost @click="importStudents">
|
||||||
|
导入
|
||||||
|
</n-button>
|
||||||
|
<n-button ghost @click="exportResults">
|
||||||
|
导出
|
||||||
|
</n-button>
|
||||||
|
<n-select v-model:value="classFilter" :options="classFilterOptions" placeholder="班级名称"
|
||||||
|
style="width: 120px;" clearable />
|
||||||
|
<n-input v-model:value="searchKeyword" placeholder="请输入学生姓名"
|
||||||
|
style="width: 200px;" clearable>
|
||||||
|
</n-input>
|
||||||
|
<n-button type="primary" @click="handleSearch">搜索</n-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab切换 -->
|
||||||
|
<div class="tab-container">
|
||||||
|
<n-tabs v-model:value="activeTab" type="line" animated @update:value="handleTabChange">
|
||||||
|
<n-tab-pane name="all" tab="全部">
|
||||||
|
</n-tab-pane>
|
||||||
|
<n-tab-pane name="submitted" tab="已交">
|
||||||
|
</n-tab-pane>
|
||||||
|
<n-tab-pane name="not-submitted" tab="未交">
|
||||||
|
</n-tab-pane>
|
||||||
|
</n-tabs>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 学生表格 -->
|
||||||
|
<n-data-table :columns="columns" :data="filteredStudents" :pagination="paginationReactive"
|
||||||
|
:loading="loading" size="medium" striped :scroll-x="1400" :single-line="false"
|
||||||
|
class="student-table" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, computed, onMounted, h } from 'vue'
|
||||||
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
import { ArrowBackOutline } from '@vicons/ionicons5'
|
||||||
|
import { NButton, NTag, NIcon, useMessage } from 'naive-ui'
|
||||||
|
import type { DataTableColumns } from 'naive-ui'
|
||||||
|
|
||||||
|
// 路由
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
const message = useMessage()
|
||||||
|
|
||||||
|
// 接口定义
|
||||||
|
interface StudentExamInfo {
|
||||||
|
id: string
|
||||||
|
studentId: string
|
||||||
|
studentName: string
|
||||||
|
className: string
|
||||||
|
startTime: string | null
|
||||||
|
submitTime: string | null
|
||||||
|
examDuration: string | null
|
||||||
|
score: number | null
|
||||||
|
status: 'not-started' | 'in-progress' | 'submitted' | 'graded'
|
||||||
|
gradingStatus: 'pending' | 'graded'
|
||||||
|
totalQuestions: number
|
||||||
|
answeredQuestions: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ExamInfo {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
duration: string
|
||||||
|
status: 'not-started' | 'in-progress' | 'completed'
|
||||||
|
totalStudents: number
|
||||||
|
submittedCount: number
|
||||||
|
gradedCount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const loading = ref(false)
|
||||||
|
const searchKeyword = ref('')
|
||||||
|
const statusFilter = ref('')
|
||||||
|
const gradeFilter = ref('')
|
||||||
|
|
||||||
|
// Tab相关
|
||||||
|
const activeTab = ref('all')
|
||||||
|
|
||||||
|
// 新增筛选相关
|
||||||
|
const classFilter = ref('')
|
||||||
|
|
||||||
|
// 考试信息
|
||||||
|
const examInfo = ref<ExamInfo>({
|
||||||
|
id: '',
|
||||||
|
title: '',
|
||||||
|
duration: '',
|
||||||
|
status: 'in-progress',
|
||||||
|
totalStudents: 0,
|
||||||
|
submittedCount: 0,
|
||||||
|
gradedCount: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 学生列表
|
||||||
|
const studentList = ref<StudentExamInfo[]>([])
|
||||||
|
|
||||||
|
// 新增班级筛选选项
|
||||||
|
const classFilterOptions = [
|
||||||
|
{ label: '全部班级', value: '' },
|
||||||
|
{ label: '计算机1班', value: '计算机1班' },
|
||||||
|
{ label: '计算机2班', value: '计算机2班' },
|
||||||
|
{ label: '软件工程1班', value: '软件工程1班' }
|
||||||
|
]
|
||||||
|
|
||||||
|
// 分页配置
|
||||||
|
const paginationReactive = reactive({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
showSizePicker: true,
|
||||||
|
pageSizes: [10, 20, 50],
|
||||||
|
onChange: (page: number) => {
|
||||||
|
paginationReactive.page = page
|
||||||
|
},
|
||||||
|
onUpdatePageSize: (pageSize: number) => {
|
||||||
|
paginationReactive.pageSize = pageSize
|
||||||
|
paginationReactive.page = 1
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 表格列定义
|
||||||
|
const columns: DataTableColumns<StudentExamInfo> = [
|
||||||
|
{
|
||||||
|
title: '序号',
|
||||||
|
key: 'index',
|
||||||
|
width: 80,
|
||||||
|
render: (_, index) => {
|
||||||
|
return (paginationReactive.page - 1) * paginationReactive.pageSize + index + 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '姓名',
|
||||||
|
key: 'studentName',
|
||||||
|
width: 120,
|
||||||
|
ellipsis: {
|
||||||
|
tooltip: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '学号',
|
||||||
|
key: 'studentId',
|
||||||
|
width: 150
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '班级',
|
||||||
|
key: 'className',
|
||||||
|
width: 120
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '开始时间',
|
||||||
|
key: 'startTime',
|
||||||
|
width: 180,
|
||||||
|
render: (row) => {
|
||||||
|
return row.startTime || '-'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '提交时间',
|
||||||
|
key: 'submitTime',
|
||||||
|
width: 180,
|
||||||
|
render: (row) => {
|
||||||
|
return row.submitTime || '-'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '考试用时',
|
||||||
|
key: 'examDuration',
|
||||||
|
width: 120,
|
||||||
|
render: (row) => {
|
||||||
|
return row.examDuration || '-'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '正确率',
|
||||||
|
key: 'accuracy',
|
||||||
|
width: 100,
|
||||||
|
render: (row) => {
|
||||||
|
if (row.score === null) return '-'
|
||||||
|
return `${row.score}%`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '状态',
|
||||||
|
key: 'status',
|
||||||
|
width: 100,
|
||||||
|
render: (row) => {
|
||||||
|
return h(NTag, {
|
||||||
|
type: getStudentStatusType(row.status),
|
||||||
|
bordered: false,
|
||||||
|
size: 'small'
|
||||||
|
}, {
|
||||||
|
default: () => getStudentStatusText(row.status)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '批阅时间',
|
||||||
|
key: 'gradingTime',
|
||||||
|
width: 180,
|
||||||
|
render: (row) => {
|
||||||
|
return row.gradingStatus === 'graded' ? '2025.07.25 09:20' : '-'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '得分',
|
||||||
|
key: 'score',
|
||||||
|
width: 80,
|
||||||
|
render: (row) => {
|
||||||
|
return row.score !== null ? `${row.score}` : '-'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'actions',
|
||||||
|
width: 120,
|
||||||
|
fixed: 'right',
|
||||||
|
render: (row) => {
|
||||||
|
return h('div', { style: 'display: flex; gap: 8px;' }, [
|
||||||
|
h(NButton, {
|
||||||
|
type: 'primary',
|
||||||
|
size: 'small',
|
||||||
|
onClick: () => handleViewAnswer(row)
|
||||||
|
}, { default: () => '查看' }),
|
||||||
|
h(NButton, {
|
||||||
|
type: 'primary',
|
||||||
|
size: 'small',
|
||||||
|
disabled: row.status !== 'submitted',
|
||||||
|
onClick: () => handleGrade(row)
|
||||||
|
}, { default: () => '批阅' })
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
const filteredStudents = computed(() => {
|
||||||
|
let filtered = studentList.value
|
||||||
|
|
||||||
|
// Tab 过滤
|
||||||
|
if (activeTab.value === 'submitted') {
|
||||||
|
filtered = filtered.filter(student => student.status === 'submitted' || student.status === 'graded')
|
||||||
|
} else if (activeTab.value === 'not-submitted') {
|
||||||
|
filtered = filtered.filter(student => student.status === 'not-started' || student.status === 'in-progress')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索过滤
|
||||||
|
if (searchKeyword.value) {
|
||||||
|
const keyword = searchKeyword.value.toLowerCase()
|
||||||
|
filtered = filtered.filter(student =>
|
||||||
|
student.studentName.toLowerCase().includes(keyword) ||
|
||||||
|
student.studentId.toLowerCase().includes(keyword)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 班级过滤
|
||||||
|
if (classFilter.value) {
|
||||||
|
filtered = filtered.filter(student => student.className === classFilter.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 状态过滤
|
||||||
|
if (statusFilter.value) {
|
||||||
|
filtered = filtered.filter(student => student.status === statusFilter.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批阅状态过滤
|
||||||
|
if (gradeFilter.value) {
|
||||||
|
filtered = filtered.filter(student => student.gradingStatus === gradeFilter.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered
|
||||||
|
})
|
||||||
|
|
||||||
|
// 方法
|
||||||
|
const goBack = () => {
|
||||||
|
router.back()
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusType = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'not-started':
|
||||||
|
return 'default'
|
||||||
|
case 'in-progress':
|
||||||
|
return 'warning'
|
||||||
|
case 'completed':
|
||||||
|
return 'success'
|
||||||
|
default:
|
||||||
|
return 'default'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusText = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'not-started':
|
||||||
|
return '未开始'
|
||||||
|
case 'in-progress':
|
||||||
|
return '进行中'
|
||||||
|
case 'completed':
|
||||||
|
return '已结束'
|
||||||
|
default:
|
||||||
|
return '未知'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStudentStatusType = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'not-started':
|
||||||
|
return 'primary'
|
||||||
|
case 'in-progress':
|
||||||
|
return 'warning'
|
||||||
|
case 'submitted':
|
||||||
|
return 'info'
|
||||||
|
case 'graded':
|
||||||
|
return 'success'
|
||||||
|
default:
|
||||||
|
return 'primary'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStudentStatusText = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'not-started':
|
||||||
|
return '待考试'
|
||||||
|
case 'in-progress':
|
||||||
|
return '考试中'
|
||||||
|
case 'submitted':
|
||||||
|
return '已提交'
|
||||||
|
case 'graded':
|
||||||
|
return '已批阅'
|
||||||
|
default:
|
||||||
|
return '未知'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleViewAnswer = (student: StudentExamInfo) => {
|
||||||
|
// 跳转到答题详情页面
|
||||||
|
router.push(`/teacher/exam-management/marking-center/answer-detail/${examInfo.value.id}/${student.id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleGrade = (student: StudentExamInfo) => {
|
||||||
|
// 跳转到批阅页面
|
||||||
|
router.push(`/teacher/exam-management/marking-center/grading/${examInfo.value.id}/${student.id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const exportResults = () => {
|
||||||
|
message.info('导出功能开发中...')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增方法
|
||||||
|
const handleTabChange = (value: string) => {
|
||||||
|
activeTab.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
const publishExam = () => {
|
||||||
|
message.info('发布补考功能开发中...')
|
||||||
|
}
|
||||||
|
|
||||||
|
const importStudents = () => {
|
||||||
|
message.info('导入学生功能开发中...')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSearch = () => {
|
||||||
|
// 搜索已经通过 computed 属性自动实现
|
||||||
|
message.info('搜索已应用')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载数据
|
||||||
|
const loadExamInfo = async (examId: string) => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
// 模拟API调用
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500))
|
||||||
|
|
||||||
|
// 模拟数据
|
||||||
|
examInfo.value = {
|
||||||
|
id: examId,
|
||||||
|
title: '试卷名称试卷名称试卷名称试卷名称试卷名称',
|
||||||
|
duration: '考试时间:2025.07.25 09:00 - 2025.07.25 09:20',
|
||||||
|
status: 'in-progress',
|
||||||
|
totalStudents: 12,
|
||||||
|
submittedCount: 8,
|
||||||
|
gradedCount: 5
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error('加载考试信息失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadStudentList = async (_examId: string) => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
// 模拟API调用
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500))
|
||||||
|
|
||||||
|
// 模拟学生数据
|
||||||
|
studentList.value = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
studentId: '1826685554',
|
||||||
|
studentName: '张张',
|
||||||
|
className: '待考试',
|
||||||
|
startTime: '2025.07.25 09:20',
|
||||||
|
submitTime: '2025.07.25 09:20',
|
||||||
|
examDuration: '9分钟29秒',
|
||||||
|
score: 70,
|
||||||
|
status: 'graded',
|
||||||
|
gradingStatus: 'graded',
|
||||||
|
totalQuestions: 10,
|
||||||
|
answeredQuestions: 8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
studentId: '1826685554',
|
||||||
|
studentName: '张张',
|
||||||
|
className: '待考试',
|
||||||
|
startTime: null,
|
||||||
|
submitTime: null,
|
||||||
|
examDuration: null,
|
||||||
|
score: null,
|
||||||
|
status: 'submitted',
|
||||||
|
gradingStatus: 'pending',
|
||||||
|
totalQuestions: 10,
|
||||||
|
answeredQuestions: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
studentId: '1826685554',
|
||||||
|
studentName: '张张',
|
||||||
|
className: '已提交',
|
||||||
|
startTime: '2025.07.25 09:20',
|
||||||
|
submitTime: '2025.07.25 09:20',
|
||||||
|
examDuration: '9分钟29秒',
|
||||||
|
score: 60,
|
||||||
|
status: 'graded',
|
||||||
|
gradingStatus: 'graded',
|
||||||
|
totalQuestions: 10,
|
||||||
|
answeredQuestions: 9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '4',
|
||||||
|
studentId: '1826685554',
|
||||||
|
studentName: '张张',
|
||||||
|
className: '待考试',
|
||||||
|
startTime: '2025.07.25 09:20',
|
||||||
|
submitTime: '2025.07.25 09:20',
|
||||||
|
examDuration: '9分钟29秒',
|
||||||
|
score: 70,
|
||||||
|
status: 'graded',
|
||||||
|
gradingStatus: 'graded',
|
||||||
|
totalQuestions: 10,
|
||||||
|
answeredQuestions: 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '5',
|
||||||
|
studentId: '1826685554',
|
||||||
|
studentName: '张张',
|
||||||
|
className: '待考试',
|
||||||
|
startTime: null,
|
||||||
|
submitTime: null,
|
||||||
|
examDuration: null,
|
||||||
|
score: null,
|
||||||
|
status: 'not-started',
|
||||||
|
gradingStatus: 'pending',
|
||||||
|
totalQuestions: 10,
|
||||||
|
answeredQuestions: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '6',
|
||||||
|
studentId: '1826685554',
|
||||||
|
studentName: '张张',
|
||||||
|
className: '已提交',
|
||||||
|
startTime: '2025.07.25 09:20',
|
||||||
|
submitTime: '2025.07.25 09:20',
|
||||||
|
examDuration: '9分钟29秒',
|
||||||
|
score: 60,
|
||||||
|
status: 'graded',
|
||||||
|
gradingStatus: 'graded',
|
||||||
|
totalQuestions: 10,
|
||||||
|
answeredQuestions: 8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '7',
|
||||||
|
studentId: '1826685554',
|
||||||
|
studentName: '张张',
|
||||||
|
className: '待考试',
|
||||||
|
startTime: '2025.07.25 09:20',
|
||||||
|
submitTime: '2025.07.25 09:20',
|
||||||
|
examDuration: '9分钟29秒',
|
||||||
|
score: 70,
|
||||||
|
status: 'graded',
|
||||||
|
gradingStatus: 'graded',
|
||||||
|
totalQuestions: 10,
|
||||||
|
answeredQuestions: 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '8',
|
||||||
|
studentId: '1826685554',
|
||||||
|
studentName: '张张',
|
||||||
|
className: '待考试',
|
||||||
|
startTime: null,
|
||||||
|
submitTime: null,
|
||||||
|
examDuration: null,
|
||||||
|
score: null,
|
||||||
|
status: 'not-started',
|
||||||
|
gradingStatus: 'pending',
|
||||||
|
totalQuestions: 10,
|
||||||
|
answeredQuestions: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '9',
|
||||||
|
studentId: '1826685554',
|
||||||
|
studentName: '张张',
|
||||||
|
className: '已提交',
|
||||||
|
startTime: '2025.07.25 09:20',
|
||||||
|
submitTime: '2025.07.25 09:20',
|
||||||
|
examDuration: '9分钟29秒',
|
||||||
|
score: 90,
|
||||||
|
status: 'graded',
|
||||||
|
gradingStatus: 'graded',
|
||||||
|
totalQuestions: 10,
|
||||||
|
answeredQuestions: 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '10',
|
||||||
|
studentId: '1826685554',
|
||||||
|
studentName: '张张',
|
||||||
|
className: '待考试',
|
||||||
|
startTime: '2025.07.25 09:20',
|
||||||
|
submitTime: '2025.07.25 09:20',
|
||||||
|
examDuration: '9分钟29秒',
|
||||||
|
score: 70,
|
||||||
|
status: 'graded',
|
||||||
|
gradingStatus: 'graded',
|
||||||
|
totalQuestions: 10,
|
||||||
|
answeredQuestions: 9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '11',
|
||||||
|
studentId: '1826685554',
|
||||||
|
studentName: '张张',
|
||||||
|
className: '待考试',
|
||||||
|
startTime: null,
|
||||||
|
submitTime: null,
|
||||||
|
examDuration: null,
|
||||||
|
score: null,
|
||||||
|
status: 'not-started',
|
||||||
|
gradingStatus: 'pending',
|
||||||
|
totalQuestions: 10,
|
||||||
|
answeredQuestions: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '12',
|
||||||
|
studentId: '1826685554',
|
||||||
|
studentName: '张张',
|
||||||
|
className: '已提交',
|
||||||
|
startTime: '2025.07.25 09:20',
|
||||||
|
submitTime: '2025.07.25 09:20',
|
||||||
|
examDuration: '9分钟29秒',
|
||||||
|
score: 90,
|
||||||
|
status: 'graded',
|
||||||
|
gradingStatus: 'graded',
|
||||||
|
totalQuestions: 10,
|
||||||
|
answeredQuestions: 10
|
||||||
|
}
|
||||||
|
]
|
||||||
|
} catch (error) {
|
||||||
|
message.error('加载学生列表失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件挂载
|
||||||
|
onMounted(async () => {
|
||||||
|
const examId = route.params.paperId as string
|
||||||
|
if (examId) {
|
||||||
|
await Promise.all([
|
||||||
|
loadExamInfo(examId),
|
||||||
|
loadStudentList(examId)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.student-list-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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button:hover {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exam-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tab 切换区域 */
|
||||||
|
.tab-container {
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-container :deep(.n-tabs) {
|
||||||
|
--n-tab-text-color: #666;
|
||||||
|
--n-tab-text-color-active: #1890ff;
|
||||||
|
--n-tab-text-color-hover: #1890ff;
|
||||||
|
--n-bar-color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-container :deep(.n-tabs-tab) {
|
||||||
|
padding: 16px 0;
|
||||||
|
margin-right: 32px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表格样式 */
|
||||||
|
.student-table {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.header-section {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-group {
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.student-list-container {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-group {
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-group .n-input {
|
||||||
|
width: 150px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-group .n-select {
|
||||||
|
width: 100px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-container :deep(.n-tabs-tab) {
|
||||||
|
margin-right: 24px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.header-section {
|
||||||
|
padding: 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-info {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-group {
|
||||||
|
justify-content: space-around;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-group .n-input {
|
||||||
|
width: 120px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-group .n-select {
|
||||||
|
width: 80px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-container :deep(.n-tabs-tab) {
|
||||||
|
margin-right: 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
Loading…
x
Reference in New Issue
Block a user