Files
vueapp/pages/assessment/assessment.vue
T
2026-06-09 17:00:23 +08:00

862 lines
20 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="assessment-page">
<view class="top-app-bar">
<button class="icon-button" aria-label="返回" @click="goBack">
<view class="back-icon"></view>
</button>
<text class="app-title">AI 学习助手</text>
</view>
<scroll-view class="report-content" scroll-y>
<view class="report-head">
<text class="report-title">训练总结与 AI 评估报告</text>
<view class="report-meta">
<text>评估日期{{ report.date }}</text>
<text>模拟编号{{ report.no }}</text>
</view>
</view>
<view class="content-stack">
<view class="overall-card">
<view class="score-ring">
<svg class="ring-svg" viewBox="0 0 100 100">
<circle class="ring-track" cx="50" cy="50" fill="transparent" r="42" stroke-width="10"></circle>
<circle
class="ring-value"
cx="50"
cy="50"
fill="transparent"
r="42"
stroke-dasharray="264"
:stroke-dashoffset="scoreDashOffset"
stroke-linecap="round"
stroke-width="10"
></circle>
</svg>
<view class="score-center">
<text class="score-value">{{ report.score }}</text>
<text class="score-total">/100</text>
</view>
</view>
<view class="overall-copy">
<text class="overall-title">本次考核评价<text class="primary-text">{{ report.level }}</text></text>
<text class="overall-desc">{{ report.overallComment }}</text>
</view>
</view>
<view class="report-card">
<view class="section-heading">
<view class="hub-icon"></view>
<text>临床胜任力雷达图</text>
</view>
<view class="radar-wrap">
<svg class="radar-svg" viewBox="0 0 400 400">
<circle class="radar-grid" cx="200" cy="200" r="160"></circle>
<circle class="radar-grid" cx="200" cy="200" r="120"></circle>
<circle class="radar-grid" cx="200" cy="200" r="80"></circle>
<circle class="radar-grid" cx="200" cy="200" r="40"></circle>
<line class="radar-grid" x1="200" x2="200" y1="200" y2="40"></line>
<line class="radar-grid" x1="200" x2="352" y1="200" y2="150"></line>
<line class="radar-grid" x1="200" x2="294" y1="200" y2="330"></line>
<line class="radar-grid" x1="200" x2="106" y1="200" y2="330"></line>
<line class="radar-grid" x1="200" x2="48" y1="200" y2="150"></line>
<polygon class="radar-area" points="200,60 340,160 270,300 120,310 60,170"></polygon>
<text class="radar-label" text-anchor="middle" x="200" y="30">病史采集</text>
<text class="radar-label" text-anchor="start" x="355" y="155">体格检查</text>
<text class="radar-label" text-anchor="middle" x="310" y="360">临床思维</text>
<text class="radar-label" text-anchor="middle" x="90" y="360">诊断准确</text>
<text class="radar-label" text-anchor="end" x="40" y="155">治疗方案</text>
</svg>
</view>
</view>
<view class="report-card">
<view class="section-heading breakdown-title">
<view class="analytics-icon"></view>
<text>分项得分与解析</text>
</view>
<view class="breakdown-list">
<view
v-for="item in breakdownItems"
:key="item.label"
class="breakdown-item"
>
<view class="breakdown-head">
<text>{{ item.label }}</text>
<text class="breakdown-score">{{ item.displayScore }}</text>
</view>
<view class="progress-track">
<view class="progress-fill" :style="{ width: barsReady ? `${item.percent}%` : '0%' }"></view>
</view>
<view class="analysis-box">
<text>{{ item.analysis }}</text>
</view>
</view>
</view>
</view>
<view class="mentor-section">
<view class="mentor-head">
<image class="mentor-avatar" src="/static/config-doctor.png" mode="aspectFill"></image>
<view class="mentor-title-group">
<text class="mentor-title">王主任点评</text>
<text class="mentor-subtitle">Director's Mentorship</text>
</view>
</view>
<view class="mentor-bubble">
<view class="mentor-tail"></view>
<text class="mentor-copy">{{ mentorComment }}</text>
<view class="mentor-action-row">
<button class="read-button" @click="openLearningAssistant">
<text>去查阅</text>
<view class="arrow-forward-icon"></view>
</button>
</view>
</view>
</view>
</view>
</scroll-view>
<view class="footer-actions">
<button class="download-button" @click="showToast('完整 PDF 报告生成中')">
下载完整 PDF 报告
</button>
<button class="next-button" @click="goHome">
开始下一轮强化训练
</button>
</view>
<view class="toast" :class="{ visible: toastVisible }">{{ toastMessage }}</view>
</view>
</template>
<script setup lang="ts">
import { computed, nextTick, onMounted, onUnmounted, reactive, ref } from 'vue'
import { generateEvaluation, type EvaluationResult } from '../../api/assessment'
import { readActiveSessionId } from '../../api/session'
type BreakdownItem = {
label: string
percent: number
displayScore: string
analysis: string
}
const fallbackBreakdownItems: BreakdownItem[] = [
{
label: '病史采集',
percent: 92,
displayScore: '92%',
analysis: '问诊逻辑清晰主诉把握精准成功识别了诱发因素及既往史'
},
{
label: '体格检查',
percent: 85,
displayScore: '85%',
analysis: '操作规范但触诊顺序有微小疏漏建议更注重痛点的动态观察'
},
{
label: '临床思维',
percent: 78,
displayScore: '78%',
analysis: '能够建立初步假设但在多系统受累时思维发散性略显不足'
},
{
label: '诊断准确性',
percent: 82,
displayScore: '82%',
analysis: '核心诊断正确但在鉴别诊断中漏掉了罕见但致死性的并发症'
}
]
const report = reactive({
date: formatReportDate(new Date()),
no: 'STJ-99283-X',
score: 88,
level: '优良',
overallComment: '表现已达到临床住院医师中高级水平需在复杂病例鉴别诊断上精进'
})
const barsReady = ref(false)
const toastMessage = ref('')
const toastVisible = ref(false)
const evaluation = ref<EvaluationResult | null>(null)
const scoreDashOffset = computed(() => {
return 264 - (264 * report.score) / 100
})
const mentorComment = computed(() => report.overallComment || '本次评价已生成请结合分项得分继续强化训练')
const breakdownItems = computed<BreakdownItem[]>(() => {
const result = evaluation.value
if (!result) return fallbackBreakdownItems
if (Array.isArray(result.dimension_scores) && result.dimension_scores.length > 0) {
return result.dimension_scores.map(item => {
const percent = item.max_score > 0 ? clampScore((Number(item.score) / Number(item.max_score)) * 100) : clampScore(Number(item.score))
const analysis = [
item.comment,
item.improvement ? `改进建议:${item.improvement}` : ''
].filter(Boolean).join(' ')
return {
label: item.dimension,
percent,
displayScore: item.max_score > 0 ? `${item.score}/${item.max_score}` : `${item.score}%`,
analysis: analysis || '暂无分项解析'
}
})
}
if (Array.isArray(result.score_details) && result.score_details.length > 0) {
return result.score_details.map(item => ({
label: item.dimension,
percent: clampScore(Number(item.score)),
displayScore: `${item.score}%`,
analysis: item.comment || item.deducted_reason || '暂无分项解析'
}))
}
return fallbackBreakdownItems
})
let toastTimer: ReturnType<typeof setTimeout> | null = null
function goBack() {
if (typeof getCurrentPages === 'function' && getCurrentPages().length > 1) {
uni.navigateBack()
return
}
goHome()
}
function goHome() {
uni.reLaunch({
url: '/pages/home/home'
})
}
function openLearningAssistant() {
uni.navigateTo({
url: '/pages/learning-assistant/learning-assistant'
})
}
function showToast(message: string) {
if (toastTimer) clearTimeout(toastTimer)
toastMessage.value = message
toastVisible.value = true
toastTimer = setTimeout(() => {
toastVisible.value = false
}, 2200)
}
function formatReportDate(date: Date) {
return `${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日`
}
function clampScore(value: number) {
if (!Number.isFinite(value)) return 0
return Math.max(0, Math.min(100, Math.round(value)))
}
function getLevel(score: number) {
if (score >= 90) return '优秀'
if (score >= 80) return '优良'
if (score >= 70) return '良好'
if (score >= 60) return '合格'
return '待加强'
}
function applyEvaluation(result: EvaluationResult) {
evaluation.value = result
report.no = `EV-${result.evaluation_id}`
report.score = clampScore(Number(result.total_score))
report.level = getLevel(report.score)
report.overallComment = result.overall_comment || report.overallComment
}
function animateBars() {
barsReady.value = false
nextTick(() => {
setTimeout(() => {
barsReady.value = true
}, 120)
})
}
async function loadEvaluation() {
try {
const sessionId = readActiveSessionId()
report.no = `SID-${sessionId}`
const result = await generateEvaluation(sessionId, 'percentage')
uni.setStorageSync('clinical-thinking-evaluation', result)
applyEvaluation(result)
} catch (error) {
showToast(error instanceof Error ? error.message : '评价生成失败')
} finally {
animateBars()
}
}
onMounted(() => {
void loadEvaluation()
})
onUnmounted(() => {
if (toastTimer) clearTimeout(toastTimer)
})
</script>
<style scoped>
page {
min-height: 100%;
background: #f9f9ff;
}
.assessment-page {
position: relative;
width: 390px;
max-width: 100vw;
min-height: 100vh;
margin: 0 auto;
overflow: hidden;
background: #f9f9ff;
color: #191c21;
font-family: Inter, -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Helvetica Neue', Arial, sans-serif;
-webkit-tap-highlight-color: transparent;
}
.assessment-page view,
.assessment-page text,
.assessment-page button,
.assessment-page scroll-view {
box-sizing: border-box;
}
.assessment-page ::-webkit-scrollbar {
width: 0;
height: 0;
background: transparent;
}
.top-app-bar {
position: fixed;
left: 50%;
top: 0;
z-index: 50;
width: 390px;
max-width: 100vw;
height: 56px;
padding: 0 16px;
border-bottom: 1px solid rgba(194, 198, 212, 0.3);
background: rgba(249, 249, 255, 0.82);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
display: flex;
align-items: center;
justify-content: center;
transform: translateX(-50%);
}
.app-title {
position: absolute;
left: 50%;
top: 50%;
margin-left: 0;
transform: translate(-50%, -50%);
color: #191c21;
font-size: 20px;
line-height: 28px;
font-weight: 700;
letter-spacing: 0;
}
.icon-button,
.read-button,
.download-button,
.next-button {
padding: 0;
border: 0;
background: transparent;
}
.icon-button::after,
.read-button::after,
.download-button::after,
.next-button::after {
border: 0;
}
.icon-button {
position: absolute;
left: 16px;
top: 8px;
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.icon-button:active {
background: rgba(225, 226, 234, 0.5);
}
.back-icon,
.hub-icon,
.analytics-icon,
.arrow-forward-icon {
background: #424752;
-webkit-mask-position: center;
-webkit-mask-repeat: no-repeat;
-webkit-mask-size: contain;
mask-position: center;
mask-repeat: no-repeat;
mask-size: contain;
}
.back-icon {
width: 24px;
height: 24px;
background: #191c21;
-webkit-mask-image: url("data:image/svg+xml,%3Csvg%20viewBox='0%200%2024%2024'%20xmlns='http://www.w3.org/2000/svg'%3E%3Cpath%20d='M20%2011H7.83l5.59-5.59L12%204l-8%208%208%208%201.42-1.41L7.83%2013H20v-2z'/%3E%3C/svg%3E");
mask-image: url("data:image/svg+xml,%3Csvg%20viewBox='0%200%2024%2024'%20xmlns='http://www.w3.org/2000/svg'%3E%3Cpath%20d='M20%2011H7.83l5.59-5.59L12%204l-8%208%208%208%201.42-1.41L7.83%2013H20v-2z'/%3E%3C/svg%3E");
}
.report-content {
width: 100%;
height: calc(100vh - 145px);
margin-top: 56px;
padding: 24px 16px 24px;
position: relative;
z-index: 0;
}
.report-head {
padding: 0 0 24px;
display: flex;
flex-direction: column;
gap: 8px;
}
.report-title {
color: #00478d;
font-size: 24px;
line-height: 32px;
font-weight: 700;
letter-spacing: 0;
}
.report-meta {
display: flex;
flex-direction: column;
gap: 2px;
color: rgba(66, 71, 82, 0.8);
font-size: 14px;
line-height: 20px;
}
.content-stack {
display: flex;
flex-direction: column;
gap: 16px;
}
.overall-card,
.report-card,
.mentor-section {
border: 1px solid rgba(194, 198, 212, 0.3);
border-radius: 8px;
background: #ffffff;
box-shadow: 0 1px 4px rgba(25, 28, 33, 0.04);
}
.overall-card {
padding: 20px;
display: flex;
align-items: center;
gap: 16px;
}
.score-ring {
position: relative;
width: 80px;
height: 80px;
flex: 0 0 auto;
}
.ring-svg {
width: 100%;
height: 100%;
transform: rotate(-90deg);
transform-origin: 50% 50%;
}
.ring-track {
stroke: #e7e8f0;
}
.ring-value {
stroke: #00478d;
transition: stroke-dashoffset 0.8s ease;
}
.score-center {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.score-value {
color: #00478d;
font-size: 20px;
line-height: 22px;
font-weight: 700;
}
.score-total {
color: #424752;
font-size: 10px;
line-height: 12px;
font-weight: 500;
}
.overall-copy {
flex: 1;
display: flex;
flex-direction: column;
gap: 6px;
}
.overall-title {
color: #191c21;
font-size: 14px;
line-height: 20px;
font-weight: 600;
}
.primary-text {
color: #00478d;
font-weight: 700;
}
.overall-desc {
color: #424752;
font-size: 13px;
line-height: 18px;
}
.report-card {
padding: 20px;
}
.section-heading {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 16px;
color: #00478d;
font-size: 14px;
line-height: 20px;
font-weight: 700;
}
.breakdown-title {
margin-bottom: 24px;
}
.hub-icon,
.analytics-icon {
width: 18px;
height: 18px;
background: #00478d;
}
.hub-icon {
-webkit-mask-image: url("data:image/svg+xml,%3Csvg%20viewBox='0%200%2024%2024'%20xmlns='http://www.w3.org/2000/svg'%3E%3Cpath%20d='M12%202a3%203%200%200%201%202%205.24V9h3a3%203%200%201%201-2.83%204H14v3.76A3%203%200%201%201%2012%2016a2.9%202.9%200%200%201%201%20.18V13H9.83A3%203%200%201%201%2010%2011H13V7.24A3%203%200%200%201%2012%202z'/%3E%3C/svg%3E");
mask-image: url("data:image/svg+xml,%3Csvg%20viewBox='0%200%2024%2024'%20xmlns='http://www.w3.org/2000/svg'%3E%3Cpath%20d='M12%202a3%203%200%200%201%202%205.24V9h3a3%203%200%201%201-2.83%204H14v3.76A3%203%200%201%201%2012%2016a2.9%202.9%200%200%201%201%20.18V13H9.83A3%203%200%201%201%2010%2011H13V7.24A3%203%200%200%201%2012%202z'/%3E%3C/svg%3E");
}
.analytics-icon {
-webkit-mask-image: url("data:image/svg+xml,%3Csvg%20viewBox='0%200%2024%2024'%20xmlns='http://www.w3.org/2000/svg'%3E%3Cpath%20d='M3%203h2v18H3V3zm16%207h2v11h-2V10zM8%2013h2v8H8v-8zm5-6h2v14h-2V7z'/%3E%3C/svg%3E");
mask-image: url("data:image/svg+xml,%3Csvg%20viewBox='0%200%2024%2024'%20xmlns='http://www.w3.org/2000/svg'%3E%3Cpath%20d='M3%203h2v18H3V3zm16%207h2v11h-2V10zM8%2013h2v8H8v-8zm5-6h2v14h-2V7z'/%3E%3C/svg%3E");
}
.radar-wrap {
display: flex;
justify-content: center;
padding: 8px 0;
}
.radar-svg {
width: 100%;
max-width: 240px;
aspect-ratio: 1;
}
.radar-grid {
stroke: #e2e8f0;
stroke-width: 1;
fill: none;
}
.radar-area {
fill: rgba(0, 71, 141, 0.15);
stroke: #00478d;
stroke-width: 2;
}
.radar-label {
fill: #424752;
font-size: 16px;
font-weight: 500;
letter-spacing: 0;
}
.breakdown-list {
display: flex;
flex-direction: column;
gap: 24px;
}
.breakdown-item {
display: flex;
flex-direction: column;
gap: 8px;
}
.breakdown-head {
display: flex;
align-items: center;
justify-content: space-between;
color: #191c21;
font-size: 12px;
line-height: 16px;
font-weight: 600;
}
.breakdown-score {
color: #00478d;
font-weight: 700;
}
.progress-track {
width: 100%;
height: 6px;
border-radius: 999px;
background: #e7e8f0;
overflow: hidden;
}
.progress-fill {
height: 100%;
border-radius: 999px;
background: #00478d;
transition: width 1s cubic-bezier(0.4, 0, 0.2, 1);
}
.analysis-box {
padding: 12px;
border: 1px solid rgba(225, 226, 234, 0.35);
border-radius: 8px;
background: rgba(242, 243, 251, 0.5);
color: #424752;
font-size: 13px;
line-height: 20px;
}
.mentor-section {
margin-bottom: 16px;
padding: 20px;
border-color: rgba(0, 71, 141, 0.1);
background: rgba(0, 94, 184, 0.06);
}
.mentor-head {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
}
.mentor-avatar {
width: 48px;
height: 48px;
border: 1px solid #ffffff;
border-radius: 50%;
box-shadow: 0 1px 4px rgba(25, 28, 33, 0.12);
}
.mentor-title-group {
display: flex;
flex-direction: column;
gap: 2px;
}
.mentor-title {
color: #00478d;
font-size: 14px;
line-height: 20px;
font-weight: 700;
}
.mentor-subtitle {
color: rgba(66, 71, 82, 0.7);
font-size: 10px;
line-height: 14px;
font-weight: 500;
letter-spacing: 0;
}
.mentor-bubble {
position: relative;
padding: 16px;
border: 1px solid rgba(0, 71, 141, 0.05);
border-radius: 8px;
background: #ffffff;
box-shadow: 0 1px 4px rgba(25, 28, 33, 0.04);
}
.mentor-tail {
position: absolute;
left: 24px;
top: -7px;
width: 12px;
height: 12px;
border-left: 1px solid rgba(0, 71, 141, 0.05);
border-top: 1px solid rgba(0, 71, 141, 0.05);
background: #ffffff;
transform: rotate(45deg);
}
.mentor-copy {
color: #191c21;
font-size: 14px;
line-height: 22px;
font-style: italic;
}
.mentor-action-row {
margin-top: 16px;
width: 100%;
display: flex;
justify-content: flex-end;
}
.read-button {
width: fit-content;
min-height: 32px;
margin: 0 0 0 auto;
display: flex;
align-items: center;
justify-content: flex-end;
gap: 4px;
color: #00478d;
font-size: 14px;
line-height: 20px;
font-weight: 600;
}
.read-button:active {
transform: scale(0.95);
}
.arrow-forward-icon {
width: 18px;
height: 18px;
background: #00478d;
-webkit-mask-image: url("data:image/svg+xml,%3Csvg%20viewBox='0%200%2024%2024'%20xmlns='http://www.w3.org/2000/svg'%3E%3Cpath%20d='M12%204l1.41%201.41L8.83%2010H20v2H8.83l4.58%204.59L12%2018l-7-7%207-7z'/%3E%3C/svg%3E");
mask-image: url("data:image/svg+xml,%3Csvg%20viewBox='0%200%2024%2024'%20xmlns='http://www.w3.org/2000/svg'%3E%3Cpath%20d='M12%204l1.41%201.41L8.83%2010H20v2H8.83l4.58%204.59L12%2018l-7-7%207-7z'/%3E%3C/svg%3E");
transform: rotate(180deg);
}
.footer-actions {
position: fixed;
left: 50%;
bottom: 0;
z-index: 80;
width: 390px;
max-width: 100vw;
padding: 16px;
border-top: 1px solid rgba(194, 198, 212, 0.3);
background: rgba(249, 249, 255, 0.9);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
display: flex;
gap: 12px;
transform: translateX(-50%);
}
.download-button,
.next-button {
flex: 1;
min-height: 48px;
padding: 0 10px;
border-radius: 8px;
font-size: 14px;
line-height: 20px;
font-weight: 600;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
}
.download-button {
border: 1px solid #00478d;
background: transparent;
color: #00478d;
}
.next-button {
background: #00478d;
box-shadow: 0 2px 8px rgba(0, 71, 141, 0.2);
color: #ffffff;
}
.download-button:active,
.next-button:active {
transform: scale(0.98);
}
.toast {
position: fixed;
left: 50%;
bottom: 92px;
z-index: 100;
max-width: 320px;
padding: 12px 20px;
border-radius: 12px;
background: #2e3037;
color: #eff0f8;
font-size: 14px;
line-height: 20px;
font-weight: 600;
text-align: center;
pointer-events: none;
opacity: 0;
transform: translate(-50%, 16px);
transition: opacity 0.3s ease, transform 0.3s ease;
}
.toast.visible {
opacity: 1;
transform: translate(-50%, 0);
}
@media (min-width: 768px) {
.assessment-page {
box-shadow: 0 24px 64px rgba(25, 28, 33, 0.18);
}
}
</style>