271 lines
6.9 KiB
Plaintext
271 lines
6.9 KiB
Plaintext
<template>
|
||
<hr-page page-bg="#F3F4F6" status-bg="#FFFFFF" nav-title="错题本" :xloading="true" @right-click="onSort">
|
||
<template v-slot:right>
|
||
<x-icon name="arrow-up-down-line" color="#6B7280" font-size="17"></x-icon>
|
||
</template>
|
||
|
||
<view class="hr-body">
|
||
|
||
<!-- 课题筛选(组件库 xRadioButton 分段切换) -->
|
||
<view style="margin-bottom:8rpx">
|
||
<x-radio-button v-model="chipId" :list="chipTabs" height="64rpx" space="4rpx" round="22rpx"
|
||
text-style="font-weight:bold;" bg-color="#E5E7EB" active-color="#2563EB"
|
||
active-font-color="#FFFFFF" font-color="#6B7280" font-size="22rpx"></x-radio-button>
|
||
</view>
|
||
|
||
<!-- 错题卡片 -->
|
||
<view style="margin-top:10px">
|
||
<view v-for="(q, i) in list" :key="q.id" class="qa-card">
|
||
<view class="qa-top">
|
||
<text class="qa-stem">{{ q.stem }}</text>
|
||
<hr-chip :text="wrongTimes[i]" theme="red"></hr-chip>
|
||
</view>
|
||
<view class="qa-row">
|
||
<text class="qa-k">你的答案</text>
|
||
<text class="qa-wrong">{{ wrongKeys[i] }}</text>
|
||
<text class="qa-k qa-k2">正确答案</text>
|
||
<text class="qa-right">{{ rightAnswer(q) }}</text>
|
||
</view>
|
||
<view class="qa-meta">
|
||
<text class="qa-meta-t">来自 <text class="qa-meta-hl">{{ q.from }}</text></text>
|
||
<text class="qa-meta-t qa-meta-date">{{ dates[i] }} 答错</text>
|
||
<view class="hr-flex1"></view>
|
||
<view class="qa-fold" @click="toggle(i)">
|
||
<x-icon :name="opened[i] ? 'arrow-up-s-line' : 'arrow-down-s-line'" color="#2563EB"
|
||
font-size="26rpx"></x-icon>
|
||
<text class="qa-fold-t">{{ opened[i] ? '收起解析' : '查看解析' }}</text>
|
||
</view>
|
||
</view>
|
||
<!-- 手风琴展开:展开进度由 accStyle() 逐帧下发 -->
|
||
<view v-if="opened[i]" :style="accStyle(i)">
|
||
<view class="qa-analysis">
|
||
<text class="qa-analysis-t">{{ q.analysis }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<hr-tip theme="info" icon="ℹ" text="已复习的错题不会自动移除;连续答对 2 次后可手动移出错题本。"></hr-tip>
|
||
|
||
<view style="height:14px"></view>
|
||
</view>
|
||
</hr-page>
|
||
</template>
|
||
|
||
<script lang="uts" setup>
|
||
import { ref, computed } from 'vue'
|
||
import { wrongQuestions } from '@/mock/index.uts'
|
||
import { HrQuestion } from '@/mock/types.uts'
|
||
import { RADIO_BUTTON } from '@/uni_modules/tmx-ui/interface.uts'
|
||
|
||
/** s16 错题本 */
|
||
/**
|
||
* 课题筛选(组件库 xRadioButton 分段切换,替代原 hr-chip 横向滚动)
|
||
* ⚠ RADIO_BUTTON 的 id 是 string:统一用下标字符串,选中值即筛选序号。
|
||
*/
|
||
const chipTabs : RADIO_BUTTON[] = [
|
||
{ id: '0', title: '全部 12' } as RADIO_BUTTON,
|
||
{ id: '1', title: '心肺复苏 5' } as RADIO_BUTTON,
|
||
{ id: '2', title: '气道管理 4' } as RADIO_BUTTON,
|
||
{ id: '3', title: 'AED 3' } as RADIO_BUTTON
|
||
]
|
||
const chipId = ref('0')
|
||
|
||
const wrongTimes : string[] = ['答错 2 次', '答错 1 次', '答错 1 次']
|
||
const dates : string[] = ['09-15', '09-14', '09-12']
|
||
const wrongKeys : string[] = ['A. 3~4 cm', 'C. 60~80 次', 'D. 每 6 秒 1 次']
|
||
|
||
const opened = ref<boolean[]>([false, false, false])
|
||
const all = ref<HrQuestion[]>(wrongQuestions)
|
||
|
||
const list = computed(() : HrQuestion[] => {
|
||
return all.value
|
||
})
|
||
|
||
/** 选项文案里若已含「A. 」前缀则去掉,避免出现「A. A. xxx」 */
|
||
function stripKey(text : string) : string {
|
||
const i = text.indexOf('. ')
|
||
if (i == 1 || i == 2) { return text.substring(i + 2) }
|
||
return text
|
||
}
|
||
|
||
// ⚠ UTS 不提升函数声明:stripKey 必须写在 rightAnswer 之前
|
||
function rightAnswer(q : HrQuestion) : string {
|
||
if (q.answer < q.options.length) {
|
||
const o = q.options[q.answer]
|
||
return `${o.key}. ${stripKey(o.text)}`
|
||
}
|
||
return '—'
|
||
}
|
||
|
||
/*
|
||
* 手风琴展开入场动画
|
||
* ⚠ App 端 uvue 不支持 @keyframes / animation(编译期直接报 ERROR),
|
||
* 改为定时器逐帧推进 accT(0 → 1),再由 accStyle() 下发 opacity + translateY。
|
||
* 240ms(30ms × 8 帧),与设计稿 qa-acc-in 一致。
|
||
*/
|
||
const accIdx = ref(-1)
|
||
const accT = ref(1)
|
||
let accTimer : number = 0
|
||
|
||
function stopAcc() : void {
|
||
if (accTimer != 0) {
|
||
clearInterval(accTimer)
|
||
accTimer = 0
|
||
}
|
||
}
|
||
|
||
function runAcc(i : number) : void {
|
||
stopAcc()
|
||
accIdx.value = i
|
||
accT.value = 0
|
||
accTimer = setInterval(() => {
|
||
const t = accT.value + 0.16
|
||
if (t >= 1) {
|
||
accT.value = 1
|
||
stopAcc()
|
||
return
|
||
}
|
||
accT.value = t
|
||
}, 30)
|
||
}
|
||
|
||
/** 仅正在展开的那一项需要过渡样式,其余返回空串(即最终态) */
|
||
function accStyle(i : number) : string {
|
||
if (i != accIdx.value || accT.value >= 1) { return '' }
|
||
const t = accT.value
|
||
const y = Math.round(-16 * (1 - t))
|
||
return 'opacity:' + t.toString() + ';transform:translateY(' + y.toString() + 'rpx);'
|
||
}
|
||
|
||
/** 手风琴:先全部收起,再切换当前项(同时只展开一条) */
|
||
function toggle(i : number) : void {
|
||
const wasOpen = opened.value[i]
|
||
const arr : boolean[] = []
|
||
for (let k = 0; k < list.value.length; k++) { arr.push(false) }
|
||
arr[i] = !wasOpen
|
||
opened.value = arr
|
||
if (wasOpen) {
|
||
stopAcc()
|
||
accIdx.value = -1
|
||
accT.value = 1
|
||
} else {
|
||
runAcc(i)
|
||
}
|
||
}
|
||
|
||
onUnload(() => {
|
||
stopAcc()
|
||
})
|
||
|
||
function onSort() : void {
|
||
uni.showToast({ title: '按错误次数 / 时间排序', icon: 'none' })
|
||
}
|
||
</script>
|
||
|
||
<style>
|
||
.qa-card {
|
||
background-color: #FFFFFF;
|
||
border: 2rpx solid #E5E7EB;
|
||
border-left: 8rpx solid #2563EB;
|
||
border-radius: 20rpx;
|
||
padding: 22rpx 24rpx 22rpx 24rpx;
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.qa-top {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.qa-stem {
|
||
flex: 1;
|
||
font-size: 29.2rpx;
|
||
font-weight: bold;
|
||
color: #1F2937;
|
||
margin-right: 18rpx;
|
||
}
|
||
|
||
.qa-row {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
margin-top: 14rpx;
|
||
}
|
||
|
||
.qa-k {
|
||
font-size: 26.8rpx;
|
||
color: #6B7280;
|
||
}
|
||
|
||
.qa-k2 {
|
||
margin-left: 12rpx;
|
||
}
|
||
|
||
.qa-wrong {
|
||
font-size: 26.8rpx;
|
||
color: #DC2626;
|
||
font-weight: bold;
|
||
margin-left: 6rpx;
|
||
}
|
||
|
||
.qa-right {
|
||
font-size: 26.8rpx;
|
||
color: #059669;
|
||
font-weight: bold;
|
||
margin-left: 6rpx;
|
||
}
|
||
|
||
.qa-meta {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
margin-top: 18rpx;
|
||
}
|
||
|
||
.qa-meta-t {
|
||
font-size: 24.6rpx;
|
||
color: #9CA3AF;
|
||
}
|
||
|
||
.qa-meta-hl {
|
||
font-size: 24.6rpx;
|
||
color: #1F2937;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.qa-meta-date {
|
||
margin-left: 24rpx;
|
||
}
|
||
|
||
.qa-fold {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
}
|
||
|
||
.qa-fold-t {
|
||
font-size: 25.8rpx;
|
||
color: #2563EB;
|
||
font-weight: bold;
|
||
margin-left: 4rpx;
|
||
}
|
||
|
||
/* ---------- 手风琴展开 ----------
|
||
⚠ App 端 uvue 不支持 @keyframes/animation,
|
||
入场过渡已改为 script 内定时器推进 accT + accStyle() 下发样式。 */
|
||
.qa-analysis {
|
||
margin-top: 20rpx;
|
||
padding: 18rpx 20rpx 18rpx 20rpx;
|
||
border-radius: 18rpx;
|
||
background-color: #F9FAFB;
|
||
}
|
||
|
||
.qa-analysis-t {
|
||
font-size: 25.8rpx;
|
||
color: #6B7280;
|
||
}
|
||
</style>
|