1
This commit is contained in:
@@ -0,0 +1,288 @@
|
||||
<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">
|
||||
|
||||
<!-- ============ 四大类目(xTabs) ============ -->
|
||||
<x-tabs :list="catTabs" :model-value="activeId" item-width="25%" height="84rpx" font-size="28rpx"
|
||||
active-font-size="28rpx" title-color="#6B7280" active-title-color="#2563EB" line-color="#2563EB"
|
||||
title-padding="0" :is-item-center="true" @change="onCatChange"></x-tabs>
|
||||
|
||||
<!-- ============ 课题筛选(组件库 xRadioButton 分段切换) ============ -->
|
||||
<view style="margin-top:10px;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 v-if="cat == 0" style="margin-top:10px">
|
||||
<view v-for="(q, i) in wrongList" :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>
|
||||
<text class="qa-ans">正确答案 <text class="qa-ans-hl">{{ rightAnswer(q) }}</text></text>
|
||||
<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">{{ wrongDates[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-empty v-else :icon="emptyIcon" :title="emptyTitle" :sub="emptySub" action="去课程库学习"
|
||||
@action="goLibrary"></hr-empty>
|
||||
|
||||
<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 { navTo } from '@/utils/nav.uts'
|
||||
import { TABS_ITEM_INFO, TABS_ITEM, RADIO_BUTTON } from '@/uni_modules/tmx-ui/interface.uts'
|
||||
|
||||
/**
|
||||
* s38 学习中心
|
||||
* 汇总「错题 / 收藏 / 历史 / 笔记」四类目,替代原先散落各处的入口。
|
||||
* 四类目改用组件库 xTabs(原 lc-tabs 自绘分段控件已移除)。
|
||||
*/
|
||||
const catTabs : TABS_ITEM_INFO[] = [
|
||||
{ title: '错题 12', id: '0' } as TABS_ITEM_INFO,
|
||||
{ title: '收藏 8', id: '1' } as TABS_ITEM_INFO,
|
||||
{ title: '历史', id: '2' } as TABS_ITEM_INFO,
|
||||
{ title: '笔记 5', id: '3' } as TABS_ITEM_INFO
|
||||
]
|
||||
|
||||
/**
|
||||
* 课题筛选(组件库 xRadioButton 分段切换,替代原 hr-chip 横向滚动)
|
||||
* ⚠ RADIO_BUTTON 的 id 是 string:统一用下标字符串,选中值即筛选序号。
|
||||
*/
|
||||
const chipTabs : RADIO_BUTTON[] = [
|
||||
{ id: '0', title: '全部' } 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 wrongDates : string[] = ['09-15', '09-14', '09-12']
|
||||
|
||||
const activeId = ref('0')
|
||||
const opened = ref<boolean[]>([false, false, false])
|
||||
const wrongList = ref<HrQuestion[]>(wrongQuestions)
|
||||
|
||||
const cat = computed(() : number => {
|
||||
const i = parseInt(activeId.value)
|
||||
return isNaN(i) ? 0 : i
|
||||
})
|
||||
|
||||
// ⚠ x-tabs 的 change 下发的是 TABS_ITEM(不是 TABS_ITEM_INFO),形参类型必须一致
|
||||
function onCatChange(item : TABS_ITEM, index : number) : void {
|
||||
activeId.value = item.id == '' ? index.toString() : item.id
|
||||
}
|
||||
|
||||
const emptyIcon = computed(() : string => {
|
||||
if (cat.value == 1) { return '★' }
|
||||
if (cat.value == 2) { return '◔' }
|
||||
return '✎'
|
||||
})
|
||||
|
||||
const emptyTitle = computed(() : string => {
|
||||
if (cat.value == 1) { return '暂无收藏题目' }
|
||||
if (cat.value == 2) { return '暂无学习历史' }
|
||||
return '暂无学习笔记'
|
||||
})
|
||||
|
||||
const emptySub = computed(() : string => {
|
||||
return '在课题中收藏题目、记录笔记后会自动汇总到这里'
|
||||
})
|
||||
|
||||
function wrongTimes(i : number) : string {
|
||||
if (i == 0) { return '错 2 次' }
|
||||
return '错 1 次'
|
||||
}
|
||||
|
||||
function rightAnswer(q : HrQuestion) : string {
|
||||
if (q.answer < q.options.length) {
|
||||
const o = q.options[q.answer]
|
||||
return `${o.key}. ${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 < wrongList.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 goLibrary() : void {
|
||||
navTo('/pages/course/library')
|
||||
}
|
||||
|
||||
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-ans {
|
||||
font-size: 26.8rpx;
|
||||
color: #6B7280;
|
||||
margin-top: 14rpx;
|
||||
}
|
||||
|
||||
.qa-ans-hl {
|
||||
font-size: 26.8rpx;
|
||||
color: #059669;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.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>
|
||||
@@ -0,0 +1,81 @@
|
||||
<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">
|
||||
|
||||
<hr-card>
|
||||
<view class="hr-row">
|
||||
<view class="hr-avt hr-avt-lg hr-avt-grn ct-avt">
|
||||
<text>▤</text>
|
||||
</view>
|
||||
<view class="hr-flex1">
|
||||
<text class="ct-title">有效证书 {{ certs.length }} 张</text>
|
||||
<text class="ct-sub">其中 1 张将于 2026-10-15 到期</text>
|
||||
</view>
|
||||
</view>
|
||||
</hr-card>
|
||||
|
||||
<hr-sec title="证书列表"></hr-sec>
|
||||
<hr-card variant="list">
|
||||
<hr-lrow v-for="(c, i) in certs" :key="i" :icon="c.statusTheme == 'grn' ? '▤' : '!'"
|
||||
:theme="c.statusTheme == 'grn' ? 'grn' : 'amb'" :title="c.name"
|
||||
:sub="c.gotAt + ' 取得 · ' + (c.statusTheme == 'grn' ? '有效至 ' + c.expireAt : c.expireAt + ' 到期')"
|
||||
:value="c.status + ' ›'" :value-theme="c.statusTheme" :value-bold="true"
|
||||
:is-last="i == certs.length - 1" @click="onCertClick(c)"></hr-lrow>
|
||||
</hr-card>
|
||||
|
||||
<hr-tip style="margin-top:14px" theme="warn" icon="⚑"
|
||||
text="BLS 证书将在 22 天后到期,请完成年度复训后自动续期。"></hr-tip>
|
||||
|
||||
<view style="height:14px"></view>
|
||||
</view>
|
||||
</hr-page>
|
||||
</template>
|
||||
|
||||
<script lang="uts" setup>
|
||||
import { ref } from 'vue'
|
||||
import { certList } from '@/mock/index.uts'
|
||||
import { HrCert } from '@/mock/types.uts'
|
||||
|
||||
/** s13 我的证书 */
|
||||
const certs = ref<HrCert[]>(certList)
|
||||
|
||||
function onCertClick(c : HrCert) : void {
|
||||
uni.showModal({
|
||||
title: c.name,
|
||||
content: `取得时间:${c.gotAt}\n有效期至:${c.expireAt}\n状态:${c.status}`,
|
||||
cancelText: '关闭',
|
||||
confirmText: '查看证书',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
uni.showToast({ title: '已生成证书图片', icon: 'none' })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function onSort() : void {
|
||||
uni.showToast({ title: '按取得时间排序', icon: 'none' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.ct-avt {
|
||||
margin-right: 24rpx;
|
||||
}
|
||||
|
||||
.ct-title {
|
||||
font-size: 31.4rpx;
|
||||
font-weight: bold;
|
||||
color: #1F2937;
|
||||
}
|
||||
|
||||
.ct-sub {
|
||||
font-size: 25.8rpx;
|
||||
color: #6B7280;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,185 @@
|
||||
<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">
|
||||
|
||||
<view class="hr-seg">
|
||||
<view v-for="(s, i) in segs" :key="i" :class="seg == i ? 'hr-seg-i hr-seg-i-on' : 'hr-seg-i'"
|
||||
@click="seg = i">
|
||||
<text :class="seg == i ? 'ex-seg-t ex-seg-t-on' : 'ex-seg-t'">{{ s }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ============ 待考(P0 紧急) ============ -->
|
||||
<view v-if="seg != 2 && plans.length > 0" class="ex-plan">
|
||||
<view class="hr-row-top">
|
||||
<view class="ex-plan-ic">
|
||||
<text class="ex-plan-ic-t">!</text>
|
||||
</view>
|
||||
<view class="hr-flex1">
|
||||
<view class="hr-row-between">
|
||||
<text class="ex-plan-t">{{ plans[0].name }}</text>
|
||||
<hr-pc :level="plans[0].priority"></hr-pc>
|
||||
</view>
|
||||
<text class="ex-plan-cd">{{ plans[0].countdown }}</text>
|
||||
<text class="ex-plan-meta">{{ plans[0].time }} · {{ plans[0].place }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="ex-plan-btn" @click="onPrepare">
|
||||
<text class="ex-plan-btn-t">进入备考</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ============ 已完成 ============ -->
|
||||
<hr-card v-if="seg != 1" variant="list" style="margin-top:10px">
|
||||
<hr-lrow v-for="(r, i) in shownRecords" :key="i" :icon="r.passed ? '✓' : '✗'"
|
||||
:theme="r.passed ? 'grn' : 'red'" :title="r.name"
|
||||
:sub="r.date + ' · 得分 ' + r.score + ' · ' + (r.passed ? '已通过' : '未通过')"
|
||||
:value="r.score.toString() + ' ›'" :value-theme="r.passed ? 'grn' : 'red'" :value-bold="true"
|
||||
:is-last="i == shownRecords.length - 1" @click="onRecordClick(r)"></hr-lrow>
|
||||
</hr-card>
|
||||
|
||||
<hr-empty v-if="seg == 1 && plans.length == 0" icon="✓" title="暂无待考安排"
|
||||
sub="有新的考试安排时会在消息中心提醒你"></hr-empty>
|
||||
|
||||
<hr-tip style="margin-top:14px" theme="info" icon="ℹ"
|
||||
text="理论测验通过后自动计入课题完成度;未通过的测验可在 7 天后重新参加。"></hr-tip>
|
||||
|
||||
<view style="height:14px"></view>
|
||||
</view>
|
||||
</hr-page>
|
||||
</template>
|
||||
|
||||
<script lang="uts" setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { examPlans, examRecords } from '@/mock/index.uts'
|
||||
import { HrExamRecord, HrExamPlan } from '@/mock/types.uts'
|
||||
import { navTo } from '@/utils/nav.uts'
|
||||
|
||||
/** s12 我的考试 */
|
||||
const segs : string[] = ['全部', '待考', '已完成']
|
||||
const seg = ref(0)
|
||||
const plans = ref<HrExamPlan[]>(examPlans)
|
||||
const records = ref<HrExamRecord[]>(examRecords)
|
||||
const passOnly = ref(false)
|
||||
|
||||
const shownRecords = computed(() : HrExamRecord[] => {
|
||||
if (passOnly.value) {
|
||||
const list : HrExamRecord[] = []
|
||||
for (let i = 0; i < records.value.length; i++) {
|
||||
if (records.value[i].passed) { list.push(records.value[i]) }
|
||||
}
|
||||
return list
|
||||
}
|
||||
return records.value
|
||||
})
|
||||
|
||||
function onPrepare() : void {
|
||||
navTo('/pages/course/practice')
|
||||
}
|
||||
|
||||
function onRecordClick(r : HrExamRecord) : void {
|
||||
if (r.passed) {
|
||||
uni.showModal({
|
||||
title: r.name,
|
||||
content: `得分 ${r.score} 分 · ${r.date}\n可查看错题解析与答卷详情。`,
|
||||
cancelText: '关闭',
|
||||
confirmText: '查看错题',
|
||||
success: (res) => {
|
||||
if (res.confirm) { navTo('/pages/study/wrong-book') }
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
uni.showModal({
|
||||
title: r.name,
|
||||
content: `得分 ${r.score} 分 · 未通过\n补考开放时间:${r.date} 起 7 天后`,
|
||||
cancelText: '关闭',
|
||||
confirmText: '进入复习',
|
||||
success: (res) => {
|
||||
if (res.confirm) { navTo('/pages/course/practice') }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function onSort() : void {
|
||||
uni.showToast({ title: '按时间排序', icon: 'none' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.ex-seg-t {
|
||||
font-size: 29.2rpx;
|
||||
color: #6B7280;
|
||||
}
|
||||
|
||||
.ex-seg-t-on {
|
||||
color: #2563EB;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.ex-plan {
|
||||
margin-top: 20rpx;
|
||||
background-color: #FFF5F5;
|
||||
border: 2rpx solid #FECACA;
|
||||
border-radius: 26rpx;
|
||||
padding: 24rpx 26rpx 24rpx 26rpx;
|
||||
}
|
||||
|
||||
.ex-plan-ic {
|
||||
width: 68rpx;
|
||||
height: 68rpx;
|
||||
border-radius: 20rpx;
|
||||
background-color: #DC2626;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.ex-plan-ic-t {
|
||||
font-size: 35.8rpx;
|
||||
font-weight: bold;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.ex-plan-t {
|
||||
font-size: 30.2rpx;
|
||||
font-weight: bold;
|
||||
color: #1F2937;
|
||||
}
|
||||
|
||||
.ex-plan-cd {
|
||||
font-size: 25.8rpx;
|
||||
color: #DC2626;
|
||||
font-weight: bold;
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
|
||||
.ex-plan-meta {
|
||||
font-size: 24.6rpx;
|
||||
color: #6B7280;
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
|
||||
.ex-plan-btn {
|
||||
margin-top: 24rpx;
|
||||
height: 88rpx;
|
||||
border-radius: 24rpx;
|
||||
background-color: #DC2626;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ex-plan-btn-t {
|
||||
font-size: 31.4rpx;
|
||||
font-weight: bold;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,270 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user