Files
vueapp/pages/home/home.vue
T
2026-06-08 16:39:10 +08:00

613 lines
18 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="openProfile"
@go-home="showMatchingPage = false"
/>
<LearningAssistantPage
v-else-if="showLearningAssistantPage"
@open-settings="emit('open-settings')"
@open-profile="openProfile"
@go-home="showLearningAssistantPage = false"
/>
<ChatPage
v-else-if="showChatPage"
:case-item="null"
@open-settings="emit('open-settings')"
@open-profile="openProfile"
@go-home="showChatPage = 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="openProfile">
<view class="account-icon"></view>
</button>
</view>
<view class="home-main">
<view class="speech-bubble">
<text class="bubble-copy">下午好医生准备好开始今天的</text>
<text class="bubble-strong">带教模拟</text>
<text class="bubble-copy">精进</text>
<text class="bubble-highlight">临床思维</text>
<text class="bubble-copy">了吗</text>
</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="training-panel">
<view class="primary-action">
<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 class="module-grid">
<button
v-for="module in trainingModules"
:key="module.title"
class="module-card"
@click="handleStartTraining"
>
<view class="module-icon" :class="module.icon"></view>
<text class="module-title">{{ module.title }}</text>
</button>
</view>
<view class="assistant-actions">
<button class="assistant-button" @click="openLearningAssistant">
<view class="assistant-icon chat-icon"></view>
<text>AI 学习助手医院知识库</text>
</button>
<button class="assistant-button" @click="openTeachingAssistant">
<view class="assistant-icon forum-icon"></view>
<text>方老师AI教学助手沟通</text>
</button>
</view>
</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 { createProfileOpener } from '../../api/navigation'
import ChatPage from '../chat/chat.vue'
import LearningAssistantPage from '../learning-assistant/learning-assistant.vue'
import MatchingPage from '../matching/matching.vue'
const emit = defineEmits<{
(event: 'open-settings'): void
(event: 'open-profile'): void
}>()
const openProfile = createProfileOpener(emit)
const summary = reactive<HomeSummary>({
greeting: '下午好,医生。',
highlight: '让我们继续提升您的临床思维能力吧。',
remainingModules: 3,
doctorName: '王主任'
})
const trainingModules = [
{ title: '精准补强·薄弱环节训练', icon: 'trend-icon' },
{ title: '实战进阶·科室专项训练', icon: 'notes-icon' },
{ title: '新手入门·教学互动模式模式训练', icon: 'school-icon' },
{ title: '精益管理·老师针对性任务训练', icon: 'admin-icon' }
]
const starting = ref(false)
const toastMessage = ref('')
const toastVisible = ref(false)
const showMatchingPage = ref(false)
const showLearningAssistantPage = ref(false)
const showChatPage = 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
}).catch(error => {
showToast(error instanceof Error ? error.message : '进入训练失败')
}).finally(() => {
setTimeout(() => {
starting.value = false
}, 300)
})
}
function openLearningAssistant() {
showLearningAssistantPage.value = true
}
function openTeachingAssistant() {
showChatPage.value = true
}
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,
.start-button::after,
.module-card::after,
.assistant-button::after {
border: 0;
}
.icon-button:active {
background: rgba(25, 28, 33, 0.05);
}
.settings-icon,
.account-icon,
.module-icon,
.assistant-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;
}
.settings-icon {
width: 22px;
height: 22px;
-webkit-mask-image: 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");
mask-image: 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");
}
.account-icon {
width: 24px;
height: 24px;
-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%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");
mask-image: 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");
}
.home-main {
box-sizing: border-box;
min-height: 100vh;
padding: 72px 20px 24px;
background: radial-gradient(circle at 50% 40%, #ffffff 0%, #f2f3fb 100%);
display: flex;
flex-direction: column;
align-items: center;
overflow-y: auto;
}
.speech-bubble {
position: relative;
box-sizing: border-box;
max-width: 300px;
margin: 16px 0;
padding: 8px 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;
z-index: 0;
width: 16px;
height: 16px;
border-right: 1px solid #e2e8f0;
border-bottom: 1px solid #e2e8f0;
background: #ffffff;
transform: translateX(-50%) rotate(45deg);
}
.bubble-copy,
.bubble-strong,
.bubble-highlight {
position: relative;
z-index: 1;
font-size: 16px;
line-height: 24px;
color: #191c21;
}
.bubble-strong,
.bubble-highlight {
font-weight: 700;
}
.bubble-highlight {
color: #00478d;
}
.doctor-stage {
position: relative;
width: 100%;
max-width: 180px;
aspect-ratio: 1;
margin-bottom: 12px;
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);
}
.training-panel {
width: 100%;
max-width: 320px;
display: flex;
flex-direction: column;
gap: 16px;
}
.primary-action {
display: flex;
flex-direction: column;
gap: 8px;
}
.start-button {
width: 100%;
min-height: 48px;
padding: 12px;
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:active {
transform: scale(0.95);
}
.remaining {
text-align: center;
color: #424752;
font-size: 12px;
line-height: 16px;
font-weight: 500;
letter-spacing: 0;
}
.module-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.module-card {
box-sizing: border-box;
min-height: 92px;
padding: 8px;
border: 1px solid rgba(194, 198, 212, 0.3);
border-radius: 8px;
background: #f2f3fb;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
text-align: center;
transition: background 0.2s ease, transform 0.2s ease;
}
.module-card:active {
background: #e7e8f0;
transform: scale(0.98);
}
.module-icon {
width: 24px;
height: 24px;
background: #00478d;
flex: 0 0 auto;
}
.trend-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='M16%206l2.29%202.29-4.88%204.88-4-4L2%2016.59%203.41%2018l6-6%204%204%206.3-6.29L22%2012V6h-6z'/%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='M16%206l2.29%202.29-4.88%204.88-4-4L2%2016.59%203.41%2018l6-6%204%204%206.3-6.29L22%2012V6h-6z'/%3E%3C/svg%3E");
}
.notes-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='M6%202h9l5%205v15H6a2%202%200%200%201-2-2V4a2%202%200%200%201%202-2zm8%201.5V8h4.5L14%203.5zM8%2011v2h8v-2H8zm0%204v2h8v-2H8zm0%204h5v-2H8v2z'/%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='M6%202h9l5%205v15H6a2%202%200%200%201-2-2V4a2%202%200%200%201%202-2zm8%201.5V8h4.5L14%203.5zM8%2011v2h8v-2H8zm0%204v2h8v-2H8zm0%204h5v-2H8v2z'/%3E%3C/svg%3E");
}
.school-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%203L1%209l11%206%209-4.91V17h2V9L12%203zm0%2014L5%2013.18V17l7%204%207-4v-3.82L12%2017z'/%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%203L1%209l11%206%209-4.91V17h2V9L12%203zm0%2014L5%2013.18V17l7%204%207-4v-3.82L12%2017z'/%3E%3C/svg%3E");
}
.admin-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%201L3%205v6c0%205.55%203.84%2010.74%209%2012%205.16-1.26%209-6.45%209-12V5l-9-4zm4.3%207.7l1.4%201.4-6.1%206.1-3.3-3.3%201.4-1.4%201.9%201.9%204.7-4.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%201L3%205v6c0%205.55%203.84%2010.74%209%2012%205.16-1.26%209-6.45%209-12V5l-9-4zm4.3%207.7l1.4%201.4-6.1%206.1-3.3-3.3%201.4-1.4%201.9%201.9%204.7-4.7z'/%3E%3C/svg%3E");
}
.module-title {
color: #191c21;
font-size: 14px;
line-height: 20px;
font-weight: 600;
letter-spacing: 0;
}
.assistant-actions {
padding-top: 8px;
display: flex;
flex-direction: column;
gap: 8px;
}
.assistant-button {
width: 100%;
min-height: 44px;
box-sizing: border-box;
padding: 10px 24px;
border-radius: 12px;
background: #005eb8;
color: #c8daff;
font-size: 14px;
line-height: 20px;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: background 0.2s ease, transform 0.2s ease;
}
.assistant-button:active {
background: rgba(0, 94, 184, 0.9);
transform: scale(0.97);
}
.assistant-icon {
width: 20px;
height: 20px;
background: #c8daff;
flex: 0 0 auto;
}
.chat-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='M4%204h16a2%202%200%200%201%202%202v9a2%202%200%200%201-2%202H9l-5%204v-4a2%202%200%200%201-2-2V6a2%202%200%200%201%202-2zm3%205v2h10V9H7zm0%204v2h7v-2H7z'/%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='M4%204h16a2%202%200%200%201%202%202v9a2%202%200%200%201-2%202H9l-5%204v-4a2%202%200%200%201-2-2V6a2%202%200%200%201%202-2zm3%205v2h10V9H7zm0%204v2h7v-2H7z'/%3E%3C/svg%3E");
}
.forum-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='M4%204h13a2%202%200%200%201%202%202v8a2%202%200%200%201-2%202H8l-4%204v-4a2%202%200%200%201-2-2V6a2%202%200%200%201%202-2zm16%204h1a2%202%200%200%201%202%202v8a2%202%200%200%201-2%202h-1v3l-3-3h-6a2%202%200%200%201-2-2v-1h8a3%203%200%200%200%203-3V8z'/%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='M4%204h13a2%202%200%200%201%202%202v8a2%202%200%200%201-2%202H8l-4%204v-4a2%202%200%200%201-2-2V6a2%202%200%200%201%202-2zm16%204h1a2%202%200%200%201%202%202v8a2%202%200%200%201-2%202h-1v3l-3-3h-6a2%202%200%200%201-2-2v-1h8a3%203%200%200%200%203-3V8z'/%3E%3C/svg%3E");
}
.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);
}
}
@media (min-width: 768px) {
.home-page {
display: flex;
justify-content: center;
background: #d8dae2;
}
.home-shell {
width: 390px;
min-height: 100vh;
box-shadow: 0 24px 64px rgba(25, 28, 33, 0.18);
}
}
@media (max-height: 740px) {
.home-main {
padding-top: 64px;
}
.speech-bubble {
margin: 10px 0;
}
.doctor-stage {
max-width: 148px;
margin-bottom: 8px;
}
.training-panel {
gap: 12px;
}
.module-card {
min-height: 84px;
}
}
</style>