2121
This commit is contained in:
parent
f2e2a5d052
commit
da590c2939
|
|
@ -3,10 +3,11 @@
|
|||
* 测评答题页面
|
||||
*
|
||||
* 功能:
|
||||
* - 展示所有测评题目和选项
|
||||
* - 单题翻页模式展示测评题目
|
||||
* - 每题10个选项,单选
|
||||
* - 底部导航:题目导航、上一题、下一题
|
||||
* - 侧边题目导航面板,按题号跳转
|
||||
* - 提交时检测未答题目
|
||||
* - 未答题弹窗提示
|
||||
*/
|
||||
|
||||
import { ref, computed } from 'vue'
|
||||
|
|
@ -26,17 +27,20 @@ const questions = ref([])
|
|||
// 答案数据 { questionId: selectedIndex }
|
||||
const answers = ref({})
|
||||
|
||||
// 当前题目索引
|
||||
const currentIndex = ref(0)
|
||||
|
||||
// 页面状态
|
||||
const pageLoading = ref(true)
|
||||
const submitting = ref(false)
|
||||
|
||||
// 题目导航弹窗
|
||||
const showNavPopup = ref(false)
|
||||
|
||||
// 未答题弹窗
|
||||
const showUnansweredPopup = ref(false)
|
||||
const unansweredQuestions = ref([])
|
||||
|
||||
// scroll-view 滚动目标
|
||||
const scrollTarget = ref('')
|
||||
|
||||
// 评分标准选项(从后台加载)
|
||||
const scoreOptions = ref([
|
||||
{ score: 1, label: '极弱', desc: '完全不符合' },
|
||||
|
|
@ -51,30 +55,46 @@ const scoreOptions = ref([
|
|||
{ score: 10, label: '极强', desc: '完全符合' }
|
||||
])
|
||||
|
||||
/** 已答题数量 */
|
||||
const answeredCount = computed(() => Object.keys(answers.value).length)
|
||||
/** 当前题目 */
|
||||
const currentQuestion = computed(() => questions.value[currentIndex.value] || null)
|
||||
|
||||
/** 当前题目ID */
|
||||
const currentQuestionId = computed(() => {
|
||||
if (!currentQuestion.value) return 0
|
||||
return currentQuestion.value.id || currentIndex.value + 1
|
||||
})
|
||||
|
||||
/** 总题目数量 */
|
||||
const totalCount = computed(() => questions.value.length)
|
||||
|
||||
/** 是否第一题 */
|
||||
const isFirst = computed(() => currentIndex.value === 0)
|
||||
|
||||
/** 是否最后一题 */
|
||||
const isLast = computed(() => currentIndex.value === totalCount.value - 1)
|
||||
|
||||
/** 判断某题是否已答 */
|
||||
function isAnswered(index) {
|
||||
const q = questions.value[index]
|
||||
const qId = q?.id || index + 1
|
||||
return answers.value[qId] !== undefined
|
||||
}
|
||||
|
||||
/** 加载题目列表 */
|
||||
async function loadQuestions() {
|
||||
pageLoading.value = true
|
||||
try {
|
||||
// 并行加载题目和评分标准
|
||||
const [questionsRes, scoreRes] = await Promise.all([
|
||||
getQuestionList(typeId.value),
|
||||
getScoreOptions(typeId.value)
|
||||
])
|
||||
|
||||
// 处理题目数据
|
||||
if (questionsRes && questionsRes.code === 0 && questionsRes.data) {
|
||||
questions.value = questionsRes.data.list || questionsRes.data || []
|
||||
} else {
|
||||
questions.value = generateMockQuestions()
|
||||
}
|
||||
|
||||
// 处理评分标准数据
|
||||
if (scoreRes && scoreRes.code === 0 && scoreRes.data && scoreRes.data.length > 0) {
|
||||
scoreOptions.value = scoreRes.data.map(item => ({
|
||||
score: item.score,
|
||||
|
|
@ -82,7 +102,6 @@ async function loadQuestions() {
|
|||
desc: item.description
|
||||
}))
|
||||
}
|
||||
// 如果加载失败,保留默认值
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error)
|
||||
questions.value = generateMockQuestions()
|
||||
|
|
@ -118,14 +137,43 @@ function isSelected(questionId, scoreIndex) {
|
|||
return answers.value[questionId] === scoreIndex
|
||||
}
|
||||
|
||||
/** 上一题 */
|
||||
function goPrev() {
|
||||
if (!isFirst.value) {
|
||||
currentIndex.value--
|
||||
}
|
||||
}
|
||||
|
||||
/** 下一题 */
|
||||
function goNext() {
|
||||
if (!isLast.value) {
|
||||
currentIndex.value++
|
||||
}
|
||||
}
|
||||
|
||||
/** 跳转到指定题目(题目导航用) */
|
||||
function goToQuestion(index) {
|
||||
currentIndex.value = index
|
||||
showNavPopup.value = false
|
||||
}
|
||||
|
||||
/** 打开题目导航 */
|
||||
function openNavPopup() {
|
||||
showNavPopup.value = true
|
||||
}
|
||||
|
||||
/** 关闭题目导航 */
|
||||
function closeNavPopup() {
|
||||
showNavPopup.value = false
|
||||
}
|
||||
|
||||
/** 提交答案 */
|
||||
async function handleSubmit() {
|
||||
// 检查未答题目
|
||||
const unanswered = []
|
||||
questions.value.forEach((q, index) => {
|
||||
const qId = q.id || index + 1
|
||||
if (answers.value[qId] === undefined) {
|
||||
unanswered.push(q.questionNo || index + 1)
|
||||
unanswered.push({ no: q.questionNo || index + 1, index })
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -167,14 +215,10 @@ function closeUnansweredPopup() {
|
|||
showUnansweredPopup.value = false
|
||||
}
|
||||
|
||||
/** 滚动到未答题目 */
|
||||
function scrollToQuestion(questionNo) {
|
||||
/** 跳转到未答题目 */
|
||||
function goToUnanswered(index) {
|
||||
closeUnansweredPopup()
|
||||
// 先清空再赋值,确保相同 id 也能触发滚动
|
||||
scrollTarget.value = ''
|
||||
setTimeout(() => {
|
||||
scrollTarget.value = `question-${questionNo}`
|
||||
}, 50)
|
||||
currentIndex.value = index
|
||||
}
|
||||
|
||||
/** 页面加载 */
|
||||
|
|
@ -197,45 +241,89 @@ onLoad((options) => {
|
|||
<text class="loading-text">加载题目中...</text>
|
||||
</view>
|
||||
|
||||
<!-- 题目滚动区域 -->
|
||||
<scroll-view v-else class="scroll-area" scroll-y :scroll-into-view="scrollTarget">
|
||||
<view class="scroll-inner">
|
||||
<view class="questions-card">
|
||||
<view
|
||||
v-for="(question, index) in questions"
|
||||
:key="question.id || index"
|
||||
:id="'question-' + (question.questionNo || index + 1)"
|
||||
class="question-block"
|
||||
>
|
||||
<!-- 题目标题 -->
|
||||
<view class="question-title">
|
||||
<view class="question-no">{{ question.questionNo || index + 1 }}</view>
|
||||
<text class="question-text">{{ question.content }}</text>
|
||||
</view>
|
||||
<!-- 答题区域 -->
|
||||
<view v-else class="question-area">
|
||||
<view class="question-card" v-if="currentQuestion">
|
||||
<!-- 进度信息 -->
|
||||
<view class="progress-bar">
|
||||
<text class="progress-current">当前题目</text>
|
||||
<text class="progress-sep">/总题数:</text>
|
||||
<text class="progress-nums">{{ currentIndex + 1 }}/{{ totalCount }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 选项列表 - 全部10个 -->
|
||||
<view class="options-list">
|
||||
<view
|
||||
v-for="(option, optIdx) in scoreOptions"
|
||||
:key="optIdx"
|
||||
class="option-row"
|
||||
@click="selectAnswer(question.id || index + 1, optIdx)"
|
||||
>
|
||||
<view class="radio" :class="{ 'radio-active': isSelected(question.id || index + 1, optIdx) }">
|
||||
<view v-if="isSelected(question.id || index + 1, optIdx)" class="radio-dot"></view>
|
||||
</view>
|
||||
<text class="option-label">【{{ option.label }}】{{ option.desc }}</text>
|
||||
</view>
|
||||
<!-- 题目标题 -->
|
||||
<view class="question-title">
|
||||
<view class="question-no">{{ currentQuestion.questionNo || currentIndex + 1 }}</view>
|
||||
<text class="question-text">{{ currentQuestion.content }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 选项列表 -->
|
||||
<view class="options-list">
|
||||
<view
|
||||
v-for="(option, optIdx) in scoreOptions"
|
||||
:key="optIdx"
|
||||
class="option-row"
|
||||
@click="selectAnswer(currentQuestionId, optIdx)"
|
||||
>
|
||||
<view class="radio" :class="{ 'radio-active': isSelected(currentQuestionId, optIdx) }">
|
||||
<view v-if="isSelected(currentQuestionId, optIdx)" class="radio-dot"></view>
|
||||
</view>
|
||||
<text class="option-label">【{{ option.label }}】{{ option.desc }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 底部固定提交按钮 -->
|
||||
<view v-if="!pageLoading" class="submit-fixed">
|
||||
<view class="submit-btn" :class="{ 'btn-loading': submitting }" @click="handleSubmit">
|
||||
<text>{{ submitting ? '提交中...' : '提交' }}</text>
|
||||
<!-- 底部操作栏 -->
|
||||
<view v-if="!pageLoading" class="bottom-bar">
|
||||
<view class="nav-btn-left" @click="openNavPopup">
|
||||
<view class="nav-icon-text">☰</view>
|
||||
<text class="nav-label">题目导航</text>
|
||||
</view>
|
||||
<view class="bar-divider"></view>
|
||||
<view class="bar-actions">
|
||||
<view class="action-btn action-prev" :class="{ 'action-disabled': isFirst }" @click="goPrev">
|
||||
<text>上一题</text>
|
||||
</view>
|
||||
<view
|
||||
v-if="!isLast"
|
||||
class="action-btn action-next"
|
||||
@click="goNext"
|
||||
>
|
||||
<text>下一题</text>
|
||||
</view>
|
||||
<view
|
||||
v-else
|
||||
class="action-btn action-next"
|
||||
:class="{ 'btn-loading': submitting }"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
<text>{{ submitting ? '提交中...' : '提交' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 题目导航侧边弹窗 -->
|
||||
<view v-if="showNavPopup" class="nav-popup-mask" @click="closeNavPopup">
|
||||
<view class="nav-popup-panel" @click.stop>
|
||||
<view class="nav-popup-header">
|
||||
<view class="nav-popup-indicator"></view>
|
||||
<text class="nav-popup-title">题目导航</text>
|
||||
</view>
|
||||
<view class="nav-popup-divider"></view>
|
||||
<scroll-view class="nav-popup-grid-scroll" scroll-y>
|
||||
<view class="nav-popup-grid">
|
||||
<view
|
||||
v-for="(q, idx) in questions"
|
||||
:key="idx"
|
||||
class="nav-grid-item"
|
||||
:class="{ 'nav-grid-answered': isAnswered(idx), 'nav-grid-current': idx === currentIndex }"
|
||||
@click="goToQuestion(idx)"
|
||||
>
|
||||
<text>{{ q.questionNo || idx + 1 }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
|
@ -250,12 +338,12 @@ onLoad((options) => {
|
|||
<view class="popup-msg">以下题目尚未作答,请完成后再提交:</view>
|
||||
<scroll-view class="unanswered-scroll" scroll-y>
|
||||
<view
|
||||
v-for="qNo in unansweredQuestions"
|
||||
:key="qNo"
|
||||
v-for="item in unansweredQuestions"
|
||||
:key="item.no"
|
||||
class="unanswered-row"
|
||||
@click="scrollToQuestion(qNo)"
|
||||
@click="goToUnanswered(item.index)"
|
||||
>
|
||||
<text>第 {{ qNo }} 题</text>
|
||||
<text>第 {{ item.no }} 题</text>
|
||||
<text class="goto-arrow">›</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
|
@ -307,35 +395,51 @@ onLoad((options) => {
|
|||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
// ========== 滚动区域 ==========
|
||||
.scroll-area {
|
||||
// ========== 答题区域 ==========
|
||||
.question-area {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
overflow-y: auto;
|
||||
padding: $spacing-sm $spacing-lg;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.scroll-inner {
|
||||
padding: $spacing-lg;
|
||||
}
|
||||
|
||||
.questions-card {
|
||||
.question-card {
|
||||
background-color: $bg-white;
|
||||
border-radius: $border-radius-xl;
|
||||
border-radius: 32rpx;
|
||||
padding: $spacing-xl $spacing-lg;
|
||||
width: 100%;
|
||||
min-height: 65vh;
|
||||
}
|
||||
|
||||
// ========== 单个题目 ==========
|
||||
.question-block {
|
||||
margin-bottom: $spacing-xl;
|
||||
// ========== 进度信息 ==========
|
||||
.progress-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: $spacing-lg;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
.progress-current {
|
||||
font-size: $font-size-sm;
|
||||
color: #FF6B60;
|
||||
}
|
||||
|
||||
.progress-sep {
|
||||
font-size: $font-size-sm;
|
||||
color: $text-placeholder;
|
||||
}
|
||||
|
||||
.progress-nums {
|
||||
font-size: $font-size-sm;
|
||||
color: $text-placeholder;
|
||||
margin-left: 4rpx;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 题目标题 ==========
|
||||
.question-title {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: $spacing-lg;
|
||||
margin-bottom: $spacing-xl;
|
||||
}
|
||||
|
||||
.question-no {
|
||||
|
|
@ -406,32 +510,91 @@ onLoad((options) => {
|
|||
line-height: 1.5;
|
||||
}
|
||||
|
||||
// ========== 底部固定提交按钮 ==========
|
||||
.submit-fixed {
|
||||
// ========== 底部操作栏 ==========
|
||||
.bottom-bar {
|
||||
flex-shrink: 0;
|
||||
padding: $spacing-lg $spacing-lg;
|
||||
padding-bottom: calc(#{$spacing-lg} + env(safe-area-inset-bottom));
|
||||
background-color: rgba(255, 234, 231, 1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: $spacing-md $spacing-lg;
|
||||
padding-bottom: calc(#{$spacing-md} + env(safe-area-inset-bottom));
|
||||
background-color: $bg-white;
|
||||
border-top: 1rpx solid $border-light;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
height: 96rpx;
|
||||
background-color: #FF6B60;
|
||||
border-radius: 48rpx;
|
||||
.nav-btn-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: $spacing-sm 0;
|
||||
|
||||
.nav-icon-text {
|
||||
font-size: 32rpx;
|
||||
color: #FF6B60;
|
||||
margin-right: 8rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.nav-label {
|
||||
font-size: $font-size-sm;
|
||||
color: #FF6B60;
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.bar-divider {
|
||||
width: 1rpx;
|
||||
height: 48rpx;
|
||||
background-color: $border-color;
|
||||
margin: 0 $spacing-lg;
|
||||
}
|
||||
|
||||
.bar-actions {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $spacing-md;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
height: 76rpx;
|
||||
border-radius: 38rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
text {
|
||||
font-size: $font-size-xl;
|
||||
font-weight: $font-weight-bold;
|
||||
color: $text-white;
|
||||
font-size: $font-size-md;
|
||||
font-weight: $font-weight-medium;
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 0.9;
|
||||
opacity: 0.8;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
|
||||
.action-prev {
|
||||
background-color: #FF6B60;
|
||||
|
||||
text {
|
||||
color: $text-white;
|
||||
}
|
||||
|
||||
&.action-disabled {
|
||||
opacity: 0.4;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.action-next {
|
||||
background-color: #FF6B60;
|
||||
|
||||
text {
|
||||
color: $text-white;
|
||||
}
|
||||
|
||||
&.btn-loading {
|
||||
opacity: 0.7;
|
||||
|
|
@ -439,6 +602,103 @@ onLoad((options) => {
|
|||
}
|
||||
}
|
||||
|
||||
// ========== 题目导航侧边弹窗 ==========
|
||||
.nav-popup-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.nav-popup-panel {
|
||||
width: 580rpx;
|
||||
height: 100%;
|
||||
background-color: $bg-white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.nav-popup-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: $spacing-xl $spacing-lg;
|
||||
padding-top: calc(#{$spacing-xl} + env(safe-area-inset-top) + 88rpx);
|
||||
|
||||
.nav-popup-indicator {
|
||||
width: 8rpx;
|
||||
height: 36rpx;
|
||||
background-color: $text-color;
|
||||
border-radius: 4rpx;
|
||||
margin-right: $spacing-sm;
|
||||
}
|
||||
|
||||
.nav-popup-title {
|
||||
font-size: $font-size-lg;
|
||||
font-weight: $font-weight-bold;
|
||||
color: $text-color;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-popup-divider {
|
||||
height: 1rpx;
|
||||
background: repeating-linear-gradient(
|
||||
to right,
|
||||
$border-color 0,
|
||||
$border-color 8rpx,
|
||||
transparent 8rpx,
|
||||
transparent 16rpx
|
||||
);
|
||||
margin: 0 $spacing-lg;
|
||||
}
|
||||
|
||||
.nav-popup-grid-scroll {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.nav-popup-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: $spacing-lg;
|
||||
gap: $spacing-md;
|
||||
}
|
||||
|
||||
.nav-grid-item {
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
border-radius: $border-radius-md;
|
||||
background-color: #F0F0F0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
text {
|
||||
font-size: $font-size-lg;
|
||||
color: $text-secondary;
|
||||
font-weight: $font-weight-medium;
|
||||
}
|
||||
|
||||
&.nav-grid-answered {
|
||||
background-color: #FF6B60;
|
||||
|
||||
text {
|
||||
color: $text-white;
|
||||
}
|
||||
}
|
||||
|
||||
&.nav-grid-current {
|
||||
border: 3rpx solid #FF6B60;
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 未答题弹窗 ==========
|
||||
.popup-mask {
|
||||
position: fixed;
|
||||
|
|
@ -446,7 +706,7 @@ onLoad((options) => {
|
|||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: $bg-mask;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user