feat: 病例列表联调

This commit is contained in:
王天骄
2026-06-13 06:05:37 +08:00
parent 37716f200b
commit bc2e03920e
13 changed files with 1518 additions and 258 deletions
+216 -51
View File
@@ -15,13 +15,13 @@
<view class="summary-copy">
<view class="summary-head">
<text class="summary-label">当前能力评分</text>
<view class="trend-chip">
<view v-if="scoreDeltaText" class="trend-chip" :class="{ down: isScoreDeltaDown }">
<view class="trend-icon"></view>
<text>+12%</text>
<text>{{ scoreDeltaText }}</text>
</view>
</view>
<text class="summary-score">84.5</text>
<text class="summary-desc">相较于上周您的临床推理能力有显著提升</text>
<text class="summary-score">{{ currentScoreText }}</text>
<text class="summary-desc">{{ summaryDesc }}</text>
</view>
</view>
</view>
@@ -37,7 +37,7 @@
<view class="bar-chart">
<view
v-for="item in trendItems"
:key="item.label"
:key="`${item.label}-${item.score}`"
class="bar-column"
>
<view class="bar-track">
@@ -50,6 +50,7 @@
<text class="bar-label" :class="{ active: item.highlight }">{{ item.label }}</text>
</view>
</view>
<view v-if="trendItems.length === 0" class="empty-card-text">{{ emptyAnalysisText }}</view>
</view>
</view>
@@ -74,35 +75,33 @@
<view class="radar-axis axis-diagonal-four"></view>
</view>
<view class="radar-label label-top">病史采集</view>
<view class="radar-label label-right-top">体格检查</view>
<view class="radar-label label-right-bottom">诊断能力</view>
<view class="radar-label label-bottom">治疗决策</view>
<view class="radar-label label-left-bottom">沟通技巧</view>
<view class="radar-label label-left-top">临床推理</view>
<view
v-for="item in radarItems"
:key="item.dimension"
class="radar-label dynamic-label"
:style="{ left: item.labelLeft, top: item.labelTop }"
>
{{ item.dimension }}
</view>
<svg class="radar-svg" viewBox="0 0 100 100">
<polygon
v-if="radarPolygonPoints"
class="radar-polygon"
points="50,15 85,35 80,75 50,85 25,70 20,40"
:points="radarPolygonPoints"
></polygon>
<circle cx="50" cy="15" r="3"></circle>
<circle cx="85" cy="35" r="3"></circle>
<circle cx="80" cy="75" r="3"></circle>
<circle cx="50" cy="85" r="3"></circle>
<circle cx="25" cy="70" r="3"></circle>
<circle cx="20" cy="40" r="3"></circle>
<circle
v-for="item in radarItems"
:key="`${item.dimension}-point`"
:cx="item.pointX"
:cy="item.pointY"
r="3"
></circle>
</svg>
</view>
<view class="analysis-note">
<text>您的</text>
<text class="strong-text">诊断能力</text>
<text></text>
<text class="strong-text">治疗决策</text>
<text>表现突出</text>
<text class="secondary-text">病史采集</text>
<text>仍有提升空间建议增加相关模拟训练</text>
<text>{{ analysisComment }}</text>
</view>
</view>
</view>
@@ -115,8 +114,8 @@
<view class="book-icon"></view>
</view>
<view class="suggestion-copy">
<text class="suggestion-title">重点回顾心血管查体</text>
<text class="suggestion-desc">基于您最近在心衰案例中的表现</text>
<text class="suggestion-title">重点回顾{{ primaryWeakDimension }}</text>
<text class="suggestion-desc">基于当前能力雷达图的薄弱项推荐</text>
</view>
<view class="chevron-icon"></view>
</view>
@@ -127,7 +126,7 @@
</view>
<view class="suggestion-copy">
<text class="suggestion-title">临床思维专项挑战</text>
<text class="suggestion-desc">推荐参与难度等级高级</text>
<text class="suggestion-desc">推荐参与难度等级{{ challengeLevel }}</text>
</view>
<view class="chevron-icon"></view>
</view>
@@ -144,36 +143,127 @@
</view>
</view>
<view class="mentor-bubble">
<text>“你的进步非常扎实。今天休息一下,明天我们来挑战一个更复杂的病例如何?”</text>
<text>{{ mentorAdvice }}</text>
</view>
</view>
</view>
</view>
</scroll-view>
<view class="toast" :class="{ visible: toastVisible }">{{ toastMessage }}</view>
</view>
</view>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue'
const trendItems = [
{ label: '周一', height: '60%', highlight: false },
{ label: '周二', height: '70%', highlight: false },
{ label: '周三', height: '65%', highlight: false },
{ label: '周四', height: '80%', highlight: false },
{ label: '周五', height: '85%', highlight: true },
{ label: '周六', height: '90%', highlight: false },
{ label: '周日', height: '95%', highlight: false }
]
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { fetchUserAnalysis, type UserAnalysis } from '../../api/profile'
const fadeVisible = ref(false)
const toastMessage = ref('')
const toastVisible = ref(false)
const analysis = ref<UserAnalysis | null>(null)
const loadingAnalysis = ref(false)
const analysisLoaded = ref(false)
const analysisFailed = ref(false)
let fadeTimer: ReturnType<typeof setTimeout> | null = null
let toastTimer: ReturnType<typeof setTimeout> | null = null
const currentScoreText = computed(() => {
const score = analysis.value?.current_score
if (typeof score !== 'number' || !Number.isFinite(score)) return '--'
return Number.isInteger(score) ? String(score) : score.toFixed(1)
})
const scoreDeltaText = computed(() => {
const delta = analysis.value?.score_delta_pct
if (typeof delta !== 'number' || !Number.isFinite(delta)) return ''
const prefix = delta > 0 ? '+' : ''
return `${prefix}${delta}%`
})
const isScoreDeltaDown = computed(() => {
const delta = analysis.value?.score_delta_pct
return typeof delta === 'number' && delta < 0
})
const summaryDesc = computed(() => {
if (analysisFailed.value) return '智能分析加载失败请稍后重试'
if (!analysisLoaded.value) return '正在加载您的临床能力分析'
const delta = analysis.value?.score_delta_pct
if (typeof delta === 'number' && Number.isFinite(delta)) {
if (delta > 0) return '相较于近期表现您的临床能力评分有所提升'
if (delta < 0) return '相较于近期表现当前评分略有回落建议继续专项练习'
}
return '当前已生成您的临床能力评分'
})
const trendItems = computed(() => {
const items = analysis.value?.recent_trend || []
const lastIndex = items.length - 1
return items.map((item, index) => ({
label: item.label,
score: item.score,
height: `${clampScore(item.score)}%`,
highlight: index === lastIndex
}))
})
const radarItems = computed(() => {
const items = (analysis.value?.radar || []).slice(0, 6)
const total = items.length
if (total < 3) return []
return items.map((item, index) => {
const angle = -Math.PI / 2 + (Math.PI * 2 * index) / total
const pointRadius = 35 * (clampScore(item.score) / 100)
const labelRadius = 47
const labelX = 50 + Math.cos(angle) * labelRadius
const labelY = 50 + Math.sin(angle) * labelRadius
const boundedLabelX = clampLabelPosition(labelX, 14, 86)
const boundedLabelY = clampLabelPosition(labelY, 6, 94)
return {
dimension: item.dimension,
pointX: roundSvgValue(50 + Math.cos(angle) * pointRadius),
pointY: roundSvgValue(50 + Math.sin(angle) * pointRadius),
labelLeft: `${boundedLabelX}%`,
labelTop: `${boundedLabelY}%`
}
})
})
const radarPolygonPoints = computed(() => {
return radarItems.value.map(item => `${item.pointX},${item.pointY}`).join(' ')
})
const emptyAnalysisText = computed(() => {
if (loadingAnalysis.value) return '分析数据加载中...'
if (analysisFailed.value) return '分析数据加载失败'
return '暂无趋势数据'
})
const analysisComment = computed(() => {
return analysis.value?.comment || emptyAnalysisText.value
})
const primaryWeakDimension = computed(() => {
return analysis.value?.weak_dimensions?.[0] || '病史采集'
})
const challengeLevel = computed(() => {
const score = analysis.value?.current_score || 0
if (score >= 85) return '高级'
if (score >= 70) return '中级'
return '基础'
})
const mentorAdvice = computed(() => {
if (analysis.value?.comment) return `“${analysis.value.comment}”`
if (analysisFailed.value) return '分析数据暂时未能加载请稍后再试'
return '正在读取你的训练表现稍后给出更具体的强化建议'
})
function goBack() {
if (typeof getCurrentPages === 'function' && getCurrentPages().length > 1) {
uni.navigateBack()
@@ -185,23 +275,47 @@ function goBack() {
})
}
async function loadAnalysis() {
loadingAnalysis.value = true
analysisFailed.value = false
try {
analysis.value = await fetchUserAnalysis()
analysisLoaded.value = true
} catch (error) {
analysisFailed.value = true
showToast(error instanceof Error ? error.message : '智能分析加载失败')
} finally {
loadingAnalysis.value = false
}
}
function showToast(message: string) {
if (toastTimer) clearTimeout(toastTimer)
toastMessage.value = message
toastVisible.value = true
uni.showToast({
title: message,
icon: 'none'
})
toastTimer = setTimeout(() => {
toastVisible.value = false
}, 2200)
}
function clampScore(value: number) {
if (!Number.isFinite(value)) return 0
return Math.max(0, Math.min(100, Math.round(value)))
}
function roundSvgValue(value: number) {
return Math.round(value * 100) / 100
}
function clampLabelPosition(value: number, min: number, max: number) {
return Math.max(min, Math.min(max, Math.round(value * 100) / 100))
}
onMounted(() => {
fadeTimer = setTimeout(() => {
fadeVisible.value = true
}, 60)
void loadAnalysis()
})
onUnmounted(() => {
@@ -217,6 +331,7 @@ page {
}
.analysis-page {
width: 100%;
min-height: 100vh;
background: #f9f9ff;
color: #191c21;
@@ -226,10 +341,10 @@ page {
.analysis-shell {
position: relative;
width: 380px;
max-width: 100vw;
min-height: 922px;
margin: 0 auto;
width: 100%;
max-width: none;
min-height: 100vh;
margin: 0;
background: #f9f9ff;
overflow: hidden;
display: flex;
@@ -329,9 +444,10 @@ page {
}
.analysis-main {
max-width: 390px;
margin: 0 auto;
padding: 24px 16px 96px;
width: 100%;
max-width: none;
margin: 0;
padding: 24px 24px 96px;
display: flex;
flex-direction: column;
gap: 24px;
@@ -404,6 +520,10 @@ page {
gap: 4px;
}
.trend-chip.down {
background: #793100;
}
.trend-icon {
width: 16px;
height: 16px;
@@ -461,6 +581,16 @@ page {
gap: 8px;
}
.empty-card-text {
min-height: 80px;
color: #727783;
font-size: 14px;
line-height: 20px;
display: flex;
align-items: center;
justify-content: center;
}
.bar-column {
flex: 1;
display: flex;
@@ -503,6 +633,7 @@ page {
.radar-wrap {
position: relative;
height: 300px;
overflow: hidden;
}
.radar-background {
@@ -575,6 +706,14 @@ page {
letter-spacing: 0;
}
.dynamic-label {
width: 68px;
max-width: 68px;
text-align: center;
word-break: break-all;
transform: translate(-50%, -50%);
}
.label-top {
top: -2px;
left: 50%;
@@ -808,6 +947,29 @@ page {
transform: rotate(45deg);
}
.toast {
position: absolute;
left: 50%;
bottom: 24px;
z-index: 80;
max-width: 300px;
padding: 8px 12px;
border-radius: 12px;
background: rgba(46, 48, 55, 0.9);
color: #eff0f8;
font-size: 12px;
line-height: 16px;
opacity: 0;
pointer-events: none;
transform: translate(-50%, 8px);
transition: opacity 0.2s ease, transform 0.2s ease;
}
.toast.visible {
opacity: 1;
transform: translate(-50%, 0);
}
.analysis-page .analysis-main .fade-section {
opacity: 1;
transform: translateY(0);
@@ -825,6 +987,9 @@ page {
}
.analysis-shell {
width: 480px;
max-width: 100vw;
margin: 0 auto;
box-shadow: 0 24px 64px rgba(25, 28, 33, 0.18);
border-left: 1px solid #c2c6d4;
border-right: 1px solid #c2c6d4;