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
+112 -19
View File
@@ -15,15 +15,15 @@
<view class="profile-content">
<view class="user-card">
<view class="avatar-wrap">
<image class="avatar-image" src="/static/config-doctor.png" mode="aspectFill"></image>
<text class="pro-badge">PRO</text>
<image class="avatar-image" :src="avatarSrc" mode="aspectFill"></image>
<text class="pro-badge">{{ roleLabel }}</text>
</view>
<view class="user-copy">
<text class="doctor-name">陈伟 医生</text>
<text class="doctor-name">{{ displayName }}</text>
<view class="tag-row">
<text class="tag primary-tag">第二阶段规培</text>
<text class="tag secondary-tag">心内科</text>
<text class="tag primary-tag">{{ stageTag }}</text>
<text class="tag secondary-tag">{{ departmentTag }}</text>
</view>
<view class="meta-grid">
<view
@@ -133,7 +133,13 @@
</template>
<script setup lang="ts">
import { computed, getCurrentInstance, onUnmounted, ref } from 'vue'
import { computed, getCurrentInstance, onMounted, onUnmounted, ref } from 'vue'
import {
fetchCompetencyMetrics,
fetchUserProfile,
type CompetencyMetrics,
type UserProfile
} from '../../api/profile'
const emit = defineEmits<{
(event: 'open-settings'): void
@@ -143,6 +149,9 @@ const emit = defineEmits<{
const activeMood = ref('focused')
const toastMessage = ref('')
const toastVisible = ref(false)
const profile = ref<UserProfile | null>(null)
const competencyMetrics = ref<CompetencyMetrics | null>(null)
const loadingProfile = ref(false)
let toastTimer: ReturnType<typeof setTimeout> | null = null
@@ -150,13 +159,6 @@ const instance = getCurrentInstance()
const hasGoHomeListener = Boolean(instance?.vnode.props?.onGoHome)
const hasOpenSettingsListener = Boolean(instance?.vnode.props?.onOpenSettings)
const profileMeta = [
{ label: '北京', icon: 'location-icon' },
{ label: '北大医学部', icon: 'school-icon' },
{ label: '2022级硕士', icon: 'calendar-icon' },
{ label: '3年从业经验', icon: 'timer-icon' }
]
const moods = [
{ id: 'steady', label: '平稳专注', icon: 'satisfied-icon' },
{ id: 'focused', label: '专注度极高', icon: 'bolt-icon' },
@@ -195,12 +197,56 @@ const actionEntries: ActionEntry[] = [
}
]
const metrics = [
{ label: '已完成病例', value: '12', badge: '本周 +2' },
{ label: '累计训练时长', value: '128', unit: '小时' },
{ label: '平均分', value: '85.5', progress: '85%' },
{ label: '诊断准确率', value: '92%', trending: true }
]
const displayName = computed(() => {
const user = profile.value
const name = user?.real_name || user?.username || user?.phone
return name ? `${name} 医生` : loadingProfile.value ? '加载中...' : '未登录医生'
})
const roleLabel = computed(() => {
const map: Record<string, string> = {
student: '学员',
teacher: '教师',
admin: '管理'
}
return map[profile.value?.role_type || ''] || 'PRO'
})
const avatarSrc = computed(() => resolveAvatar(profile.value?.avatar))
const stageTag = computed(() => {
return profile.value?.training_stage || profile.value?.current_level || profile.value?.title_name || '未配置阶段'
})
const departmentTag = computed(() => {
return profile.value?.department_name || profile.value?.major || '未配置科室'
})
const profileMeta = computed(() => {
const user = profile.value
return [
{ label: user?.institution_name || '未绑定机构', icon: 'school-icon' },
{ label: user?.department_name || '未配置科室', icon: 'location-icon' },
{ label: user?.title_name || '未配置职称', icon: 'calendar-icon' },
{ label: formatPracticeYears(user?.practice_years), icon: 'timer-icon' }
]
})
const metrics = computed(() => {
const metric = competencyMetrics.value
const completedCases = metric?.completed_cases ?? profile.value?.total_case_count ?? profile.value?.total_training_count ?? 0
const completedCasesWeek = metric?.completed_cases_week ?? 0
const totalHours = metric?.total_hours ?? 0
const avgScore = metric?.avg_score ?? 0
const diagnosisAccuracy = metric?.diagnosis_accuracy ?? 0
return [
{ label: '已完成病例', value: formatNumber(completedCases), badge: `本周 +${formatNumber(completedCasesWeek)}` },
{ label: '累计训练时长', value: formatNumber(totalHours), unit: '小时' },
{ label: '平均分', value: formatScore(avgScore), progress: `${clampPercent(avgScore)}%` },
{ label: '诊断准确率', value: `${formatNumber(diagnosisAccuracy)}%`, trending: true }
]
})
const activeMoodLabel = computed(() => {
return moods.find(item => item.id === activeMood.value)?.label || '专注度极高'
@@ -215,6 +261,49 @@ function showToast(message: string) {
}, 2200)
}
async function loadProfileData() {
loadingProfile.value = true
try {
const [userProfile, metricsResult] = await Promise.all([
fetchUserProfile(),
fetchCompetencyMetrics()
])
profile.value = userProfile
competencyMetrics.value = metricsResult
uni.setStorageSync('clinical-thinking-user-profile', userProfile)
} catch (error) {
showToast(error instanceof Error ? error.message : '个人信息加载失败')
} finally {
loadingProfile.value = false
}
}
function resolveAvatar(avatar?: string) {
if (!avatar) return '/static/config-doctor.png'
if (/^https?:\/\//i.test(avatar) || avatar.startsWith('/')) return avatar
return `/${avatar}`
}
function formatPracticeYears(value?: string) {
if (!value) return '未配置年限'
return value.includes('年') ? value : `${value}年经验`
}
function formatNumber(value: number) {
if (!Number.isFinite(value)) return '0'
return Number.isInteger(value) ? String(value) : value.toFixed(1)
}
function formatScore(value: number) {
if (!Number.isFinite(value)) return '0'
return Number.isInteger(value) ? String(value) : value.toFixed(1)
}
function clampPercent(value: number) {
if (!Number.isFinite(value)) return 0
return Math.max(0, Math.min(100, Math.round(value)))
}
function handleAction(entry: ActionEntry) {
if (entry.route) {
uni.navigateTo({
@@ -248,6 +337,10 @@ function openSettings() {
})
}
onMounted(() => {
void loadProfileData()
})
onUnmounted(() => {
if (toastTimer) clearTimeout(toastTimer)
})