Files
vueapp/pages/home/home.vue
T
2026-05-29 17:40:10 +08:00

394 lines
10 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>
<MatchingPage
v-if="showMatchingPage"
@open-settings="emit('open-settings')"
@open-profile="emit('open-profile')"
@go-home="showMatchingPage = false"
/>
<view v-else class="home-page">
<view class="home-shell">
<view class="top-bar">
<button class="icon-button" aria-label="配置" @click="emit('open-settings')">
<view class="settings-icon"></view>
</button>
<view class="top-spacer"></view>
<button class="icon-button" aria-label="个人中心" @click="emit('open-profile')">
<view class="account-icon"></view>
</button>
</view>
<view class="home-main">
<view class="speech-bubble">
<text class="bubble-line">{{ summary.greeting }}</text>
<view class="bubble-line-wrap">
<text class="bubble-line">让我们继续提升您的</text>
<text class="bubble-highlight">临床思维能力吧</text>
<text class="bubble-line"></text>
</view>
</view>
<view class="doctor-stage">
<view class="doctor-shadow"></view>
<image class="director-image" src="/static/config-doctor.png" mode="aspectFit"></image>
</view>
<view class="action-area">
<button class="start-button" :disabled="starting" @click="handleStartTraining">
<view v-if="starting" class="spinner"></view>
<text>{{ starting ? '正在进入...' : '开始训练' }}</text>
</button>
<text class="remaining">今日剩余{{ summary.remainingModules }}个模块</text>
</view>
</view>
</view>
<view class="toast" :class="{ visible: toastVisible }">{{ toastMessage }}</view>
</view>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, reactive, ref } from 'vue'
import { fetchHomeSummary, startTrainingSession, type HomeSummary } from '../../api/home'
import MatchingPage from '../matching/matching.vue'
const emit = defineEmits<{
(event: 'open-settings'): void
(event: 'open-profile'): void
}>()
const summary = reactive<HomeSummary>({
greeting: '下午好,医生。',
highlight: '让我们继续提升您的临床思维能力吧。',
remainingModules: 3,
doctorName: '王主任'
})
const starting = ref(false)
const toastMessage = ref('')
const toastVisible = ref(false)
const showMatchingPage = ref(false)
let toastTimer: ReturnType<typeof setTimeout> | null = null
function loadHomeSummary() {
fetchHomeSummary().then(result => {
Object.assign(summary, result)
})
}
function handleStartTraining() {
if (starting.value) return
starting.value = true
startTrainingSession().then(result => {
uni.setStorageSync('clinical-thinking-session', result)
showMatchingPage.value = true
}).finally(() => {
setTimeout(() => {
starting.value = false
}, 600)
})
}
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)
}
onMounted(loadHomeSummary)
onUnmounted(() => {
if (toastTimer) clearTimeout(toastTimer)
})
</script>
<style>
page {
min-height: 100%;
background: #f9f9ff;
}
.home-page {
min-height: 100vh;
background: #f9f9ff;
color: #191c21;
font-family: Inter, -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Helvetica Neue', Arial, sans-serif;
-webkit-tap-highlight-color: transparent;
}
.home-shell {
position: relative;
min-height: 100vh;
overflow: hidden;
background: #f9f9ff;
}
.top-bar {
position: absolute;
left: 0;
right: 0;
top: 0;
z-index: 10;
box-sizing: border-box;
height: 56px;
padding: 0 20px;
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;
}
.top-spacer {
flex: 1;
}
.icon-button {
width: 40px;
height: 40px;
padding: 0;
border-radius: 50%;
background: transparent;
display: flex;
align-items: center;
justify-content: center;
}
.icon-button::after {
border: 0;
}
.icon-button:active {
background: rgba(25, 28, 33, 0.05);
}
.settings-icon {
width: 22px;
height: 22px;
background: #424752;
-webkit-mask: url("data:image/svg+xml,%3Csvg%20viewBox='0%200%2024%2024'%20xmlns='http://www.w3.org/2000/svg'%3E%3Cpath%20d='M19.43%2012.98c.04-.32.07-.65.07-.98s-.02-.66-.07-.98l2.11-1.65c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.37-.31-.6-.22l-2.49%201c-.52-.4-1.08-.73-1.69-.98L14.5%202.42C14.47%202.18%2014.25%202%2014%202h-4c-.25%200-.46.18-.5.42l-.38%202.65c-.61.25-1.17.59-1.69.98l-2.49-1c-.23-.08-.48%200-.6.22l-2%203.46c-.13.22-.07.49.12.64l2.11%201.65c-.04.32-.08.65-.08.98s.03.66.08.98l-2.11%201.65c-.19.15-.24.42-.12.64l2%203.46c.12.22.37.31.6.22l2.49-1c.52.4%201.08.73%201.69.98l.38%202.65c.04.24.25.42.5.42h4c.25%200%20.46-.18.5-.42l.38-2.65c.61-.25%201.17-.58%201.69-.98l2.49%201c.23.08.48%200%20.6-.22l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.65zM12%2015.5A3.5%203.5%200%201%201%2012%208a3.5%203.5%200%200%201%200%207.5z'/%3E%3C/svg%3E") center / contain no-repeat;
mask: url("data:image/svg+xml,%3Csvg%20viewBox='0%200%2024%2024'%20xmlns='http://www.w3.org/2000/svg'%3E%3Cpath%20d='M19.43%2012.98c.04-.32.07-.65.07-.98s-.02-.66-.07-.98l2.11-1.65c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.37-.31-.6-.22l-2.49%201c-.52-.4-1.08-.73-1.69-.98L14.5%202.42C14.47%202.18%2014.25%202%2014%202h-4c-.25%200-.46.18-.5.42l-.38%202.65c-.61.25-1.17.59-1.69.98l-2.49-1c-.23-.08-.48%200-.6.22l-2%203.46c-.13.22-.07.49.12.64l2.11%201.65c-.04.32-.08.65-.08.98s.03.66.08.98l-2.11%201.65c-.19.15-.24.42-.12.64l2%203.46c.12.22.37.31.6.22l2.49-1c.52.4%201.08.73%201.69.98l.38%202.65c.04.24.25.42.5.42h4c.25%200%20.46-.18.5-.42l.38-2.65c.61-.25%201.17-.58%201.69-.98l2.49%201c.23.08.48%200%20.6-.22l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.65zM12%2015.5A3.5%203.5%200%201%201%2012%208a3.5%203.5%200%200%201%200%207.5z'/%3E%3C/svg%3E") center / contain no-repeat;
}
.account-icon {
width: 24px;
height: 24px;
background: #424752;
-webkit-mask: url("data:image/svg+xml,%3Csvg%20viewBox='0%200%2024%2024'%20xmlns='http://www.w3.org/2000/svg'%3E%3Cpath%20d='M12%202a10%2010%200%201%200%200%2020%2010%2010%200%200%200%200-20zm0%203a3.5%203.5%200%201%201%200%207%203.5%203.5%200%200%201%200-7zm0%2015a8%208%200%200%201-6.4-3.2c1.18-2.02%203.57-3.3%206.4-3.3s5.22%201.28%206.4%203.3A8%208%200%200%201%2012%2020z'/%3E%3C/svg%3E") center / contain no-repeat;
mask: url("data:image/svg+xml,%3Csvg%20viewBox='0%200%2024%2024'%20xmlns='http://www.w3.org/2000/svg'%3E%3Cpath%20d='M12%202a10%2010%200%201%200%200%2020%2010%2010%200%200%200%200-20zm0%203a3.5%203.5%200%201%201%200%207%203.5%203.5%200%200%201%200-7zm0%2015a8%208%200%200%201-6.4-3.2c1.18-2.02%203.57-3.3%206.4-3.3s5.22%201.28%206.4%203.3A8%208%200%200%201%2012%2020z'/%3E%3C/svg%3E") center / contain no-repeat;
}
.home-main {
box-sizing: border-box;
min-height: 100vh;
padding: 96px 20px 40px;
background: radial-gradient(circle at 50% 40%, #ffffff 0%, #f2f3fb 100%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
overflow-y: auto;
}
.speech-bubble {
position: relative;
box-sizing: border-box;
max-width: 280px;
margin-bottom: 32px;
padding: 16px 24px;
border: 1px solid #e2e8f0;
border-radius: 8px;
background: #ffffff;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
text-align: center;
animation: bubble-float 4s ease-in-out infinite;
}
.speech-bubble::before {
content: '';
position: absolute;
left: 50%;
bottom: -10px;
width: 16px;
height: 16px;
border-right: 1px solid #e2e8f0;
border-bottom: 1px solid #e2e8f0;
background: #ffffff;
transform: translateX(-50%) rotate(45deg);
}
.bubble-line,
.bubble-highlight {
font-size: 16px;
line-height: 24px;
font-weight: 400;
}
.bubble-line {
color: #191c21;
}
.bubble-highlight {
color: #00478d;
font-weight: 700;
}
.bubble-line-wrap {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
}
.doctor-stage {
position: relative;
width: 100%;
max-width: 320px;
aspect-ratio: 1;
margin-bottom: 32px;
display: flex;
align-items: center;
justify-content: center;
}
.doctor-shadow {
position: absolute;
left: 12.5%;
right: 12.5%;
bottom: 16px;
height: 32px;
border-radius: 50%;
background: rgba(0, 0, 0, 0.05);
filter: blur(12px);
}
.director-image {
position: relative;
z-index: 1;
width: 100%;
height: 100%;
transition: transform 0.5s ease;
}
.director-image:active {
transform: scale(1.03);
}
.action-area {
width: 100%;
max-width: 320px;
margin-bottom: 32px;
}
.start-button {
width: 100%;
min-height: 56px;
padding: 16px;
border-radius: 8px;
background: #00478d;
box-shadow: 0 4px 12px rgba(0, 71, 141, 0.2);
color: #ffffff;
font-size: 20px;
line-height: 28px;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
animation: pulse-gentle 3s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
.start-button::after {
border: 0;
}
.start-button:active {
transform: scale(0.95);
}
.remaining {
display: block;
margin-top: 16px;
text-align: center;
color: #424752;
font-size: 12px;
line-height: 16px;
font-weight: 500;
letter-spacing: 0;
}
.spinner {
width: 20px;
height: 20px;
border: 2px solid rgba(255, 255, 255, 0.36);
border-top-color: #ffffff;
border-radius: 50%;
box-sizing: border-box;
animation: spin 1s linear infinite;
}
.toast {
position: fixed;
left: 50%;
bottom: 96px;
z-index: 100;
max-width: 320px;
padding: 12px 24px;
border-radius: 999px;
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);
}
@keyframes pulse-gentle {
0%,
100% {
transform: scale(1);
box-shadow: 0 4px 12px rgba(0, 71, 141, 0.2);
}
50% {
transform: scale(1.02);
box-shadow: 0 8px 24px rgba(0, 71, 141, 0.35);
}
}
@keyframes bubble-float {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-6px);
}
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
</style>