1044 lines
32 KiB
Vue
1044 lines
32 KiB
Vue
<template>
|
||
<view class="exam-index-container">
|
||
<!-- 顶部header -->
|
||
<view class="top-header">
|
||
<view class="header-left">
|
||
<text class="app-title">考核</text>
|
||
</view>
|
||
<view class="header-right">
|
||
<view class="avatar-wrapper" @click="goToProfile">
|
||
<image
|
||
v-if="userInfo.avatar"
|
||
:src="userInfo.avatar"
|
||
class="avatar"
|
||
mode="aspectFill"
|
||
/>
|
||
<view v-else class="avatar-placeholder">
|
||
<text class="avatar-text">{{ (userInfo.realName || userInfo.username || 'U').charAt(0) }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 语音评测入口 -->
|
||
<view class="voice-evaluation-entry" v-if="isStudent" style="margin-top: 0;">
|
||
<view class="entry-card" @click="goToVoiceEvaluation">
|
||
<view class="entry-icon-wrapper">
|
||
<image src="/static/icon/voice.png" class="entry-icon" mode="aspectFit"></image>
|
||
</view>
|
||
<view class="entry-content">
|
||
<text class="entry-title">语音评测</text>
|
||
<text class="entry-desc">语音练习与评测</text>
|
||
</view>
|
||
<text class="entry-arrow">›</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 筛选栏 -->
|
||
<view class="filter-section">
|
||
<view class="filter-row">
|
||
<picker
|
||
mode="selector"
|
||
:range="statusOptions"
|
||
range-key="label"
|
||
:value="selectedStatusIndex"
|
||
@change="onStatusChange"
|
||
>
|
||
<view class="filter-item">
|
||
<text class="filter-label">状态:</text>
|
||
<text class="filter-value">{{ selectedStatusName || '全部' }}</text>
|
||
<text class="filter-icon">▼</text>
|
||
</view>
|
||
</picker>
|
||
</view>
|
||
<view class="filter-actions" v-if="hasFilter">
|
||
<text class="reset-btn" @click="resetFilter">重置</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 考核列表 -->
|
||
<view v-if="filteredExamList.length === 0" class="empty-wrapper">
|
||
<view class="empty-illustration">
|
||
<text class="empty-icon">📝</text>
|
||
</view>
|
||
<text class="empty-text">暂无考核</text>
|
||
</view>
|
||
|
||
<view v-else class="exam-list">
|
||
<view
|
||
v-for="exam in filteredExamList"
|
||
:key="exam.id"
|
||
class="exam-item"
|
||
>
|
||
<view class="exam-border" :class="getExamBorderClass(exam)"></view>
|
||
<view class="exam-content">
|
||
<view class="exam-header">
|
||
<text class="exam-name">{{ exam.examName }}</text>
|
||
<view class="exam-status-badge" :class="getStatusClass(exam)">
|
||
<text class="status-text">{{ getStatusText(exam) }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="exam-info">
|
||
<view class="info-row">
|
||
<text class="info-label">学科:</text>
|
||
<text class="info-value">{{ exam.subjectName || '未知' }}</text>
|
||
</view>
|
||
<view class="info-row">
|
||
<text class="info-label">题目数:</text>
|
||
<text class="info-value">{{ exam.questionCount || 0 }}题</text>
|
||
</view>
|
||
<view class="info-row">
|
||
<text class="info-label">时长:</text>
|
||
<text class="info-value">{{ exam.duration || 0 }}分钟</text>
|
||
</view>
|
||
<view class="info-row">
|
||
<text class="info-label">总分:</text>
|
||
<text class="info-value">{{ exam.totalScore || 0 }}分</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="exam-time" v-if="exam.deadline || exam.endTime">
|
||
<text class="time-label">截止:</text>
|
||
<text class="time-value">{{ formatDate(exam.deadline || exam.endTime) }}</text>
|
||
<view class="time-remaining">
|
||
<u-icon name="clock" color="#999" size="28"></u-icon>
|
||
<text class="remaining-text">{{ getRemainingTime(exam) }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="exam-actions">
|
||
<view
|
||
v-if="getExamStatus(exam) === 'active'"
|
||
class="btn-start"
|
||
@click.stop="startExam(exam)"
|
||
>
|
||
开始考试
|
||
</view>
|
||
<view
|
||
v-else-if="getExamStatus(exam) === 'pending'"
|
||
class="btn-disabled"
|
||
>
|
||
未开始
|
||
</view>
|
||
<view
|
||
v-else-if="getExamStatus(exam) === 'completed' || getExamStatus(exam) === 'ended'"
|
||
class="btn-view"
|
||
@click.stop="viewResult(exam)"
|
||
>
|
||
查看结果
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import { mapGetters } from 'vuex'
|
||
import { getMyExams } from '@/api/study/exam.js'
|
||
import { getMyScores } from '@/api/study/score.js'
|
||
|
||
export default {
|
||
computed: {
|
||
...mapGetters('auth', ['userInfo', 'userRole']),
|
||
isStudent() {
|
||
return this.userRole === 'student'
|
||
},
|
||
hasFilter() {
|
||
return this.selectedStatus !== null
|
||
}
|
||
},
|
||
data() {
|
||
return {
|
||
examList: [],
|
||
filteredExamList: [],
|
||
loading: false,
|
||
statusOptions: [
|
||
{ value: null, label: '全部' },
|
||
{ value: 'active', label: '进行中' },
|
||
{ value: 'pending', label: '未开始' },
|
||
{ value: 'completed', label: '已完成' },
|
||
{ value: 'ended', label: '已结束' }
|
||
],
|
||
selectedStatus: null,
|
||
selectedStatusName: null,
|
||
selectedStatusIndex: 0,
|
||
hasLoadedOnce: false
|
||
}
|
||
},
|
||
onLoad() {
|
||
this.loadExamList({ showSkeleton: true })
|
||
},
|
||
onShow() {
|
||
// 每次显示时刷新列表
|
||
if (this.hasLoadedOnce) {
|
||
this.loadExamList()
|
||
}
|
||
// 通知底部导航栏更新
|
||
uni.$emit('tabbar-update')
|
||
},
|
||
onPullDownRefresh() {
|
||
this.loadExamList({ showSkeleton: false }).finally(() => {
|
||
uni.stopPullDownRefresh()
|
||
})
|
||
},
|
||
methods: {
|
||
async loadExamList({ showSkeleton = false } = {}) {
|
||
const shouldShowSkeleton = showSkeleton || !this.hasLoadedOnce
|
||
if (shouldShowSkeleton) {
|
||
this.loading = true
|
||
}
|
||
try {
|
||
// 并行加载考试列表和成绩列表
|
||
const [examResponse, scoreResponse] = await Promise.all([
|
||
getMyExams(),
|
||
getMyScores().catch(() => ({ code: 200, data: [] })) // 如果成绩接口失败,返回空数组
|
||
])
|
||
|
||
if (examResponse.code === 200) {
|
||
let examList = examResponse.data || []
|
||
const scoreList = (scoreResponse.code === 200 && scoreResponse.data) ? scoreResponse.data : []
|
||
|
||
// 创建已完成考试的examId集合
|
||
const completedExamIds = new Set(scoreList.map(score => score.examId))
|
||
|
||
// 标记已完成的考试
|
||
examList = examList.map(exam => {
|
||
if (completedExamIds.has(exam.id)) {
|
||
return { ...exam, isCompleted: true, scoreId: scoreList.find(s => s.examId === exam.id)?.id }
|
||
}
|
||
return exam
|
||
})
|
||
|
||
this.examList = examList
|
||
} else {
|
||
uni.showToast({
|
||
title: examResponse.msg || '加载失败',
|
||
icon: 'none'
|
||
})
|
||
}
|
||
|
||
this.applyFilter()
|
||
} catch (error) {
|
||
console.error('加载考试列表失败', error)
|
||
uni.showToast({
|
||
title: error.message || '加载失败',
|
||
icon: 'none'
|
||
})
|
||
} finally {
|
||
if (shouldShowSkeleton) {
|
||
this.loading = false
|
||
}
|
||
this.hasLoadedOnce = true
|
||
}
|
||
},
|
||
|
||
applyFilter() {
|
||
let filtered = [...this.examList]
|
||
|
||
// 按状态筛选(使用计算后的状态,而不是原始status字段)
|
||
if (this.selectedStatus !== null) {
|
||
filtered = filtered.filter(exam => {
|
||
const examStatus = this.getExamStatus(exam)
|
||
return examStatus === this.selectedStatus
|
||
})
|
||
}
|
||
|
||
this.filteredExamList = filtered
|
||
},
|
||
|
||
getExamStatus(exam) {
|
||
// 优先检查用户是否已完成考试
|
||
if (exam.scoreId || exam.isCompleted || exam.hasCompleted || exam.completed || exam.isFinished) {
|
||
return 'completed' // 已完成
|
||
}
|
||
|
||
if (!exam.status || exam.status === '0') {
|
||
return 'pending' // 未开始
|
||
}
|
||
if (exam.status === '2') {
|
||
return 'ended' // 已结束
|
||
}
|
||
// status === '1' 已发布
|
||
const now = new Date()
|
||
if (exam.startTime && new Date(exam.startTime) > now) {
|
||
return 'pending'
|
||
}
|
||
if (exam.endTime && new Date(exam.endTime) < now) {
|
||
return 'ended'
|
||
}
|
||
return 'active' // 进行中
|
||
},
|
||
|
||
getStatusText(exam) {
|
||
const status = this.getExamStatus(exam)
|
||
const statusMap = {
|
||
'active': '进行中',
|
||
'pending': '未开始',
|
||
'ended': '已结束',
|
||
'completed': '已完成'
|
||
}
|
||
return statusMap[status] || '未知'
|
||
},
|
||
|
||
getStatusClass(exam) {
|
||
const status = this.getExamStatus(exam)
|
||
return `status-${status}`
|
||
},
|
||
|
||
getExamBorderClass(exam) {
|
||
const status = this.getExamStatus(exam)
|
||
if (status === 'pending') {
|
||
return 'border-blue'
|
||
}
|
||
if (status === 'active') {
|
||
const days = this.getRemainingDays(exam)
|
||
if (days <= 1) {
|
||
return 'border-orange'
|
||
}
|
||
return 'border-purple'
|
||
}
|
||
if (status === 'ended') {
|
||
return 'border-green'
|
||
}
|
||
if (status === 'completed') {
|
||
return 'border-green'
|
||
}
|
||
return 'border-blue'
|
||
},
|
||
|
||
getRemainingTime(exam) {
|
||
if (!exam.deadline && !exam.endTime) {
|
||
return '暂无截止时间'
|
||
}
|
||
const deadline = exam.deadline || exam.endTime
|
||
const now = new Date()
|
||
const endTime = new Date(deadline)
|
||
const diff = endTime - now
|
||
const days = Math.ceil(diff / (1000 * 60 * 60 * 24))
|
||
|
||
if (days < 0) {
|
||
return '已过期'
|
||
}
|
||
if (days === 0) {
|
||
return '今天截止'
|
||
}
|
||
if (days === 1) {
|
||
return '剩余1天'
|
||
}
|
||
return `剩余${days}天`
|
||
},
|
||
|
||
getRemainingDays(exam) {
|
||
if (!exam.deadline && !exam.endTime) {
|
||
return 999
|
||
}
|
||
const deadline = exam.deadline || exam.endTime
|
||
const now = new Date()
|
||
const endTime = new Date(deadline)
|
||
const diff = endTime - now
|
||
return Math.ceil(diff / (1000 * 60 * 60 * 24))
|
||
},
|
||
|
||
formatDate(dateStr) {
|
||
if (!dateStr) return ''
|
||
const date = new Date(dateStr)
|
||
const year = date.getFullYear()
|
||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||
const day = String(date.getDate()).padStart(2, '0')
|
||
return `${year}-${month}-${day}`
|
||
},
|
||
|
||
onStatusChange(e) {
|
||
const index = e.detail.value
|
||
const selected = this.statusOptions[index]
|
||
this.selectedStatusIndex = index
|
||
this.selectedStatus = selected.value
|
||
this.selectedStatusName = selected.value ? selected.label : null
|
||
this.applyFilter()
|
||
},
|
||
|
||
resetFilter() {
|
||
this.selectedStatus = null
|
||
this.selectedStatusName = null
|
||
this.selectedStatusIndex = 0
|
||
this.applyFilter()
|
||
},
|
||
|
||
|
||
startExam(exam) {
|
||
uni.showModal({
|
||
title: '开始考试',
|
||
content: `确定要开始考试"${exam.examName}"吗?\n\n考试时长:${exam.duration}分钟`,
|
||
confirmText: '开始',
|
||
cancelText: '取消',
|
||
confirmColor: '#378CE0',
|
||
success: (res) => {
|
||
if (res.confirm) {
|
||
uni.navigateTo({
|
||
url: `/pages/exam/detail?id=${exam.id}`
|
||
})
|
||
}
|
||
}
|
||
})
|
||
},
|
||
|
||
viewResult(exam) {
|
||
uni.navigateTo({
|
||
url: `/pages/exam/result?examId=${exam.id}`
|
||
})
|
||
},
|
||
|
||
goToProfile() {
|
||
uni.switchTab({
|
||
url: '/pages/profile/profile'
|
||
})
|
||
},
|
||
|
||
goToVoiceEvaluation() {
|
||
uni.showModal({
|
||
title: '进入语音评测',
|
||
content: '确定要进入语音评测吗?\n\n可以进行语音练习与评测',
|
||
confirmText: '确定',
|
||
cancelText: '取消',
|
||
confirmColor: '#378CE0',
|
||
success: (res) => {
|
||
if (res.confirm) {
|
||
uni.navigateTo({
|
||
url: '/pages/speech/speech'
|
||
})
|
||
}
|
||
}
|
||
})
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.exam-index-container {
|
||
padding: 0;
|
||
background-color: #f5f7fa;
|
||
min-height: 100vh;
|
||
padding-bottom: 120rpx; // 为底部导航栏留出空间
|
||
|
||
@media (min-width: 768px) {
|
||
padding-bottom: 140rpx;
|
||
}
|
||
}
|
||
|
||
// 顶部header
|
||
.top-header {
|
||
background: linear-gradient(135deg, rgb(55 140 224) 0%, rgb(45 120 200) 100%);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 20rpx 30rpx;
|
||
padding-top: calc(var(--status-bar-height) + 20rpx);
|
||
|
||
@media (min-width: 768px) {
|
||
padding: 20rpx 40rpx;
|
||
padding-top: calc(var(--status-bar-height) + 20rpx);
|
||
}
|
||
|
||
.header-left {
|
||
flex: 1;
|
||
|
||
.app-title {
|
||
font-size: 36rpx;
|
||
font-weight: bold;
|
||
color: #fff;
|
||
|
||
@media (min-width: 768px) {
|
||
font-size: 32rpx;
|
||
}
|
||
}
|
||
}
|
||
|
||
.header-right {
|
||
display: flex;
|
||
align-items: center;
|
||
|
||
.avatar-wrapper {
|
||
width: 64rpx;
|
||
height: 64rpx;
|
||
border-radius: 50%;
|
||
overflow: hidden;
|
||
background: rgba(255, 255, 255, 0.2);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
|
||
@media (min-width: 768px) {
|
||
width: 56rpx;
|
||
height: 56rpx;
|
||
}
|
||
|
||
.avatar {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.avatar-placeholder {
|
||
width: 100%;
|
||
height: 100%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: rgba(255, 255, 255, 0.3);
|
||
|
||
.avatar-text {
|
||
font-size: 28rpx;
|
||
color: #fff;
|
||
font-weight: bold;
|
||
|
||
@media (min-width: 768px) {
|
||
font-size: 24rpx;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 语音评测入口
|
||
.voice-evaluation-entry {
|
||
padding: 20rpx 30rpx;
|
||
|
||
@media (min-width: 768px) {
|
||
padding: 24rpx 40rpx;
|
||
}
|
||
|
||
.entry-card {
|
||
background: #fff;
|
||
border-radius: 20rpx;
|
||
padding: 28rpx 30rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
|
||
transition: all 0.3s ease;
|
||
|
||
@media (min-width: 768px) {
|
||
border-radius: 24rpx;
|
||
padding: 36rpx 40rpx;
|
||
}
|
||
|
||
&:active {
|
||
background-color: #f8f9fa;
|
||
transform: translateX(4rpx);
|
||
}
|
||
|
||
.entry-icon-wrapper {
|
||
width: 72rpx;
|
||
height: 72rpx;
|
||
border-radius: 16rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-right: 24rpx;
|
||
flex-shrink: 0;
|
||
background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
|
||
transition: transform 0.3s ease;
|
||
|
||
@media (min-width: 768px) {
|
||
width: 88rpx;
|
||
height: 88rpx;
|
||
border-radius: 20rpx;
|
||
margin-right: 32rpx;
|
||
}
|
||
|
||
.entry-icon {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
}
|
||
|
||
.entry-content {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
|
||
.entry-title {
|
||
font-size: 30rpx;
|
||
font-weight: 500;
|
||
color: #1a1a1a;
|
||
margin-bottom: 6rpx;
|
||
transition: color 0.3s;
|
||
|
||
@media (min-width: 768px) {
|
||
font-size: 32rpx;
|
||
margin-bottom: 8rpx;
|
||
}
|
||
}
|
||
|
||
.entry-desc {
|
||
font-size: 24rpx;
|
||
color: #999;
|
||
|
||
@media (min-width: 768px) {
|
||
font-size: 26rpx;
|
||
}
|
||
}
|
||
}
|
||
|
||
.entry-arrow {
|
||
font-size: 32rpx;
|
||
color: #d9d9d9;
|
||
margin-left: 16rpx;
|
||
flex-shrink: 0;
|
||
transition: transform 0.3s ease, color 0.3s;
|
||
|
||
@media (min-width: 768px) {
|
||
font-size: 36rpx;
|
||
margin-left: 24rpx;
|
||
}
|
||
}
|
||
|
||
&:active {
|
||
.entry-icon-wrapper {
|
||
transform: scale(0.95);
|
||
}
|
||
|
||
.entry-title {
|
||
color: rgb(55 140 224);
|
||
}
|
||
|
||
.entry-arrow {
|
||
transform: translateX(4rpx);
|
||
color: rgb(55 140 224);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.filter-section {
|
||
background: #fff;
|
||
padding: 20rpx 30rpx;
|
||
margin-bottom: 20rpx;
|
||
border-bottom: 1rpx solid #f0f0f0;
|
||
|
||
@media (min-width: 768px) {
|
||
padding: 20rpx 40rpx;
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.filter-row {
|
||
display: flex;
|
||
gap: 20rpx;
|
||
|
||
.filter-item {
|
||
flex: 0 0 calc((100% - 20rpx) / 2);
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 24rpx 28rpx;
|
||
background: #f5f7fa;
|
||
border-radius: 16rpx;
|
||
transition: background-color 0.2s;
|
||
min-height: 88rpx;
|
||
|
||
@media (min-width: 768px) {
|
||
flex: 0 0 calc((100% - 30rpx) / 2);
|
||
padding: 28rpx 36rpx;
|
||
border-radius: 20rpx;
|
||
min-height: 96rpx;
|
||
}
|
||
|
||
&:active {
|
||
background: #e8eef5;
|
||
}
|
||
|
||
.filter-label {
|
||
font-size: 30rpx;
|
||
color: #666;
|
||
margin-right: 16rpx;
|
||
font-weight: 500;
|
||
|
||
@media (min-width: 768px) {
|
||
font-size: 32rpx;
|
||
margin-right: 20rpx;
|
||
}
|
||
}
|
||
|
||
.filter-value {
|
||
flex: 1;
|
||
font-size: 30rpx;
|
||
color: #1a1a1a;
|
||
font-weight: 500;
|
||
|
||
@media (min-width: 768px) {
|
||
font-size: 32rpx;
|
||
}
|
||
}
|
||
|
||
.filter-icon {
|
||
font-size: 24rpx;
|
||
color: #999;
|
||
margin-left: 16rpx;
|
||
|
||
@media (min-width: 768px) {
|
||
font-size: 26rpx;
|
||
margin-left: 20rpx;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.filter-actions {
|
||
margin-top: 24rpx;
|
||
text-align: right;
|
||
|
||
@media (min-width: 768px) {
|
||
margin-top: 20rpx;
|
||
}
|
||
|
||
.reset-btn {
|
||
font-size: 28rpx;
|
||
color: rgb(55 140 224);
|
||
padding: 12rpx 24rpx;
|
||
font-weight: 500;
|
||
|
||
@media (min-width: 768px) {
|
||
font-size: 26rpx;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.empty-wrapper {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 150rpx 0;
|
||
|
||
@media (min-width: 768px) {
|
||
padding: 200rpx 0;
|
||
}
|
||
|
||
.empty-illustration {
|
||
margin-bottom: 40rpx;
|
||
|
||
@media (min-width: 768px) {
|
||
margin-bottom: 30rpx;
|
||
}
|
||
|
||
.empty-icon {
|
||
font-size: 120rpx;
|
||
opacity: 0.3;
|
||
|
||
@media (min-width: 768px) {
|
||
font-size: 100rpx;
|
||
}
|
||
}
|
||
}
|
||
|
||
.empty-text {
|
||
font-size: 28rpx;
|
||
color: #999;
|
||
|
||
@media (min-width: 768px) {
|
||
font-size: 26rpx;
|
||
}
|
||
}
|
||
}
|
||
|
||
.exam-list {
|
||
padding: 0 30rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 20rpx;
|
||
|
||
@media (min-width: 768px) {
|
||
padding: 0 40rpx;
|
||
gap: 24rpx;
|
||
}
|
||
|
||
.exam-item {
|
||
background: #fff;
|
||
border-radius: 20rpx;
|
||
padding: 24rpx;
|
||
display: flex;
|
||
position: relative;
|
||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
|
||
overflow: hidden;
|
||
|
||
@media (min-width: 768px) {
|
||
border-radius: 18rpx;
|
||
padding: 24rpx;
|
||
}
|
||
|
||
.exam-border {
|
||
position: absolute;
|
||
left: 0;
|
||
top: 0;
|
||
bottom: 0;
|
||
width: 6rpx;
|
||
|
||
@media (min-width: 768px) {
|
||
width: 8rpx;
|
||
}
|
||
|
||
&.border-blue {
|
||
background: rgb(55 140 224);
|
||
}
|
||
|
||
&.border-purple {
|
||
background: rgb(55 140 224);
|
||
}
|
||
|
||
&.border-orange {
|
||
background: rgb(55 140 224);
|
||
}
|
||
|
||
&.border-green {
|
||
background: rgb(55 140 224);
|
||
}
|
||
}
|
||
|
||
.exam-content {
|
||
flex: 1;
|
||
margin-left: 20rpx;
|
||
min-width: 0;
|
||
overflow: hidden;
|
||
|
||
@media (min-width: 768px) {
|
||
margin-left: 24rpx;
|
||
}
|
||
|
||
.exam-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 16rpx;
|
||
|
||
@media (min-width: 768px) {
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.exam-name {
|
||
font-size: 30rpx;
|
||
font-weight: bold;
|
||
color: #1a1a1a;
|
||
flex: 1;
|
||
|
||
@media (min-width: 768px) {
|
||
font-size: 30rpx;
|
||
}
|
||
}
|
||
|
||
.exam-status-badge {
|
||
padding: 8rpx 20rpx;
|
||
border-radius: 50rpx;
|
||
font-size: 22rpx;
|
||
|
||
@media (min-width: 768px) {
|
||
padding: 8rpx 20rpx;
|
||
font-size: 22rpx;
|
||
}
|
||
|
||
.status-text {
|
||
font-size: 22rpx;
|
||
font-weight: 500;
|
||
|
||
@media (min-width: 768px) {
|
||
font-size: 22rpx;
|
||
}
|
||
}
|
||
|
||
&.status-pending {
|
||
background: #fff7e6;
|
||
|
||
.status-text {
|
||
color: #fa8c16;
|
||
}
|
||
}
|
||
|
||
&.status-active {
|
||
background: #e6f7ff;
|
||
|
||
.status-text {
|
||
color: rgb(55 140 224);
|
||
}
|
||
}
|
||
|
||
&.status-ended {
|
||
background: #f5f5f5;
|
||
|
||
.status-text {
|
||
color: #999;
|
||
}
|
||
}
|
||
|
||
&.status-completed {
|
||
background: #f6ffed;
|
||
|
||
.status-text {
|
||
color: #52c41a;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.exam-info {
|
||
margin-bottom: 16rpx;
|
||
|
||
@media (min-width: 768px) {
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.info-row {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 12rpx;
|
||
font-size: 24rpx;
|
||
|
||
@media (min-width: 768px) {
|
||
font-size: 24rpx;
|
||
margin-bottom: 10rpx;
|
||
}
|
||
|
||
.info-label {
|
||
color: #666;
|
||
width: 120rpx;
|
||
|
||
@media (min-width: 768px) {
|
||
width: 100rpx;
|
||
}
|
||
}
|
||
|
||
.info-value {
|
||
color: #1a1a1a;
|
||
font-weight: 500;
|
||
}
|
||
}
|
||
}
|
||
|
||
.exam-time {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding-top: 16rpx;
|
||
border-top: 1rpx solid #f0f0f0;
|
||
margin-bottom: 16rpx;
|
||
|
||
@media (min-width: 768px) {
|
||
padding-top: 14rpx;
|
||
margin-bottom: 14rpx;
|
||
}
|
||
|
||
.time-label {
|
||
font-size: 24rpx;
|
||
color: #666;
|
||
margin-right: 8rpx;
|
||
|
||
@media (min-width: 768px) {
|
||
font-size: 24rpx;
|
||
}
|
||
}
|
||
|
||
.time-value {
|
||
font-size: 24rpx;
|
||
color: #666;
|
||
flex: 1;
|
||
|
||
@media (min-width: 768px) {
|
||
font-size: 24rpx;
|
||
}
|
||
}
|
||
|
||
.time-remaining {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8rpx;
|
||
|
||
.remaining-text {
|
||
font-size: 24rpx;
|
||
color: #999;
|
||
|
||
@media (min-width: 768px) {
|
||
font-size: 24rpx;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.exam-actions {
|
||
margin-top: 0;
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
|
||
.btn-start {
|
||
width: 100%;
|
||
max-width: 100%;
|
||
background: rgb(55 140 224);
|
||
color: #fff;
|
||
border-radius: 12rpx;
|
||
padding: 20rpx 32rpx;
|
||
font-size: 28rpx;
|
||
font-weight: 500;
|
||
text-align: center;
|
||
transition: all 0.3s;
|
||
box-shadow: 0 2rpx 8rpx rgba(55, 140, 224, 0.2);
|
||
box-sizing: border-box;
|
||
|
||
@media (min-width: 768px) {
|
||
padding: 20rpx 32rpx;
|
||
font-size: 28rpx;
|
||
}
|
||
|
||
&:active {
|
||
background: rgb(45 120 200);
|
||
transform: scale(0.98);
|
||
box-shadow: 0 1rpx 4rpx rgba(55, 140, 224, 0.3);
|
||
}
|
||
}
|
||
|
||
.btn-disabled {
|
||
width: 100%;
|
||
max-width: 100%;
|
||
background: #f5f5f5;
|
||
color: #999;
|
||
border-radius: 12rpx;
|
||
padding: 20rpx 32rpx;
|
||
font-size: 28rpx;
|
||
text-align: center;
|
||
box-sizing: border-box;
|
||
|
||
@media (min-width: 768px) {
|
||
padding: 20rpx 32rpx;
|
||
font-size: 28rpx;
|
||
}
|
||
}
|
||
|
||
.btn-view {
|
||
width: 100%;
|
||
max-width: 100%;
|
||
background: rgba(55, 140, 224, 0.1);
|
||
color: rgb(55 140 224);
|
||
border-radius: 12rpx;
|
||
padding: 20rpx 32rpx;
|
||
font-size: 28rpx;
|
||
font-weight: 500;
|
||
text-align: center;
|
||
transition: all 0.3s;
|
||
box-sizing: border-box;
|
||
|
||
@media (min-width: 768px) {
|
||
padding: 20rpx 32rpx;
|
||
font-size: 28rpx;
|
||
}
|
||
|
||
&:active {
|
||
background: rgba(55, 140, 224, 0.15);
|
||
transform: scale(0.98);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</style>
|
||
|