feat: 病例列表联调
This commit is contained in:
+112
-19
@@ -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)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user