Files
vueapp/pages/cases/cases.vue
T
2026-06-01 15:35:17 +08:00

523 lines
15 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>
<ScenarioPage
v-if="showScenarioPage"
:case-item="selectedCase"
@open-settings="emit('open-settings')"
@open-profile="emit('open-profile')"
@go-home="emit('go-home')"
/>
<TeachingPage
v-else-if="showTeachingPage"
:case-item="selectedCase"
@open-settings="emit('open-settings')"
@open-profile="emit('open-profile')"
@go-home="emit('go-home')"
/>
<view v-else class="cases-page">
<view class="case-shell">
<view class="case-header">
<button class="icon-button" aria-label="设置" @click="emit('open-settings')">
<view class="settings-icon"></view>
</button>
<button class="icon-button home-button" aria-label="首页" @click="emit('go-home')">
<view class="home-icon"></view>
</button>
<view class="header-spacer"></view>
<button class="icon-button" aria-label="个人中心" @click="emit('open-profile')">
<view class="account-icon"></view>
</button>
</view>
<scroll-view class="case-content" scroll-y>
<view class="search-row">
<view class="search-box">
<view class="search-icon"></view>
<input
class="search-input"
v-model="keyword"
type="text"
placeholder="科室、主诉模糊搜索"
placeholder-class="search-placeholder"
/>
</view>
</view>
<view class="case-list">
<view
v-for="item in filteredCases"
:key="item.id"
class="case-card"
:class="`mode-${item.mode}`"
@click="selectCase(item)"
>
<view class="case-main">
<view class="patient-avatar" :class="`avatar-${item.tone}`">
<text>{{ item.patientName.slice(0, 1) }}</text>
</view>
<view class="case-info">
<text class="case-title">{{ item.title }}</text>
<text class="case-meta">
{{ item.patientName }}{{ item.gender }}{{ item.age }}{{ item.department }}{{ item.scene }}
</text>
</view>
</view>
<view class="case-footer">
<text class="case-no">病例编号: {{ item.caseNo }}</text>
<view class="mode-badge" :class="`mode-badge-${item.mode}`">
<view class="mode-icon" :class="`mode-icon-${item.mode}`"></view>
<text>{{ getModeLabel(item.mode) }}</text>
</view>
</view>
</view>
<view v-if="filteredCases.length === 0" class="empty-state">
<text>暂无匹配病例</text>
</view>
</view>
</scroll-view>
</view>
<view class="toast" :class="{ visible: toastVisible }">{{ toastMessage }}</view>
</view>
</template>
<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { fetchCaseList, type CaseMode, type ClinicalCase } from '../../api/cases'
import ScenarioPage from '../scenario/scenario.vue'
import TeachingPage from '../teaching/teaching.vue'
const emit = defineEmits<{
(event: 'open-settings'): void
(event: 'open-profile'): void
(event: 'go-home'): void
}>()
const cases = ref<ClinicalCase[]>([])
const keyword = ref('')
const toastMessage = ref('')
const toastVisible = ref(false)
const selectedCase = ref<ClinicalCase | null>(null)
const showScenarioPage = ref(false)
const showTeachingPage = ref(false)
const modeFilter = ref<CaseMode | ''>('')
let toastTimer: ReturnType<typeof setTimeout> | null = null
const filteredCases = computed(() => {
const value = keyword.value.trim().toLowerCase()
const source = modeFilter.value ? cases.value.filter(item => item.mode === modeFilter.value) : cases.value
if (!value) return source
return source.filter(item => {
return [
item.title,
item.patientName,
item.gender,
String(item.age),
item.department,
item.scene,
item.caseNo
].some(field => field.toLowerCase().includes(value))
})
})
function getModeLabel(mode: CaseMode) {
return mode === 'teaching' ? '教学模式' : '训练模式'
}
function loadCases() {
fetchCaseList().then(result => {
cases.value = result
})
}
function selectCase(item: ClinicalCase) {
uni.setStorageSync('clinical-thinking-selected-case', item)
uni.setStorageSync('clinical-thinking-case-mode', item.mode)
selectedCase.value = item
if (item.mode === 'teaching') {
showTeachingPage.value = true
return
}
showScenarioPage.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)
}
onLoad(query => {
const mode = query?.mode
if (mode === 'teaching' || mode === 'training') {
modeFilter.value = mode
}
})
onMounted(loadCases)
onUnmounted(() => {
if (toastTimer) clearTimeout(toastTimer)
})
</script>
<style>
page {
min-height: 100%;
background: #e7e8f0;
}
.cases-page {
min-height: 100vh;
background: #e7e8f0;
color: #191c21;
font-family: Inter, -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Helvetica Neue', Arial, sans-serif;
-webkit-tap-highlight-color: transparent;
}
.case-shell {
position: relative;
min-height: 100vh;
overflow: hidden;
background: #f2f3fb;
display: flex;
flex-direction: column;
}
.case-header {
position: absolute;
left: 0;
right: 0;
top: 0;
z-index: 20;
box-sizing: border-box;
height: 56px;
padding: 0 20px;
border-bottom: 1px solid rgba(194, 198, 212, 0.3);
background: #ffffff;
box-shadow: 0 2px 8px rgba(25, 28, 33, 0.04);
display: flex;
align-items: center;
}
.header-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);
}
.home-button {
margin-left: 4px;
}
.settings-icon,
.home-icon,
.account-icon,
.search-icon {
background: #424752;
}
.settings-icon {
width: 22px;
height: 22px;
-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;
}
.home-icon {
width: 23px;
height: 23px;
-webkit-mask: url("data:image/svg+xml,%3Csvg%20viewBox='0%200%2024%2024'%20xmlns='http://www.w3.org/2000/svg'%3E%3Cpath%20d='M10%2020v-6h4v6h5v-8h3L12%203%202%2012h3v8h5z'/%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='M10%2020v-6h4v6h5v-8h3L12%203%202%2012h3v8h5z'/%3E%3C/svg%3E") center / contain no-repeat;
}
.account-icon {
width: 24px;
height: 24px;
-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;
}
.case-content {
box-sizing: border-box;
height: 100vh;
padding: 76px 20px 24px;
}
.search-row {
margin-bottom: 12px;
}
.search-box {
position: relative;
height: 40px;
border-radius: 8px;
background: #ffffff;
box-shadow: 0 2px 8px rgba(25, 28, 33, 0.04);
}
.search-icon {
position: absolute;
left: 12px;
top: 50%;
width: 20px;
height: 20px;
background: #727783;
transform: translateY(-50%);
-webkit-mask: url("data:image/svg+xml,%3Csvg%20viewBox='0%200%2024%2024'%20xmlns='http://www.w3.org/2000/svg'%3E%3Cpath%20d='M9.5%203a6.5%206.5%200%200%200%200%2013c1.61%200%203.09-.59%204.23-1.57L19.29%2020%2020.7%2018.59l-5.56-5.56A6.47%206.47%200%200%200%2016%209.5%206.5%206.5%200%200%200%209.5%203zm0%202a4.5%204.5%200%201%201%200%209%204.5%204.5%200%200%201%200-9z'/%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='M9.5%203a6.5%206.5%200%200%200%200%2013c1.61%200%203.09-.59%204.23-1.57L19.29%2020%2020.7%2018.59l-5.56-5.56A6.47%206.47%200%200%200%2016%209.5%206.5%206.5%200%200%200%209.5%203zm0%202a4.5%204.5%200%201%201%200%209%204.5%204.5%200%200%201%200-9z'/%3E%3C/svg%3E") center / contain no-repeat;
}
.search-input {
box-sizing: border-box;
width: 100%;
height: 40px;
padding: 0 16px 0 40px;
border: 0;
background: transparent;
color: #191c21;
font-size: 14px;
line-height: 20px;
}
.search-placeholder {
color: #c2c6d4;
}
.case-list {
display: flex;
flex-direction: column;
gap: 12px;
padding-bottom: 20px;
}
.case-card {
padding: 12px;
border: 1px solid rgba(194, 198, 212, 0.3);
border-radius: 8px;
background: #ffffff;
box-shadow: 0 2px 8px rgba(25, 28, 33, 0.04);
display: flex;
flex-direction: column;
gap: 8px;
}
.case-card.mode-teaching {
border-color: rgba(0, 71, 141, 0.22);
}
.case-card:active {
transform: scale(0.99);
}
.case-main {
display: flex;
gap: 12px;
}
.patient-avatar {
position: relative;
flex: 0 0 auto;
width: 70px;
height: 70px;
border-radius: 4px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
color: #ffffff;
font-size: 28px;
line-height: 1;
font-weight: 700;
}
.patient-avatar::before {
content: '';
position: absolute;
left: 50%;
top: 15px;
width: 20px;
height: 20px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.42);
transform: translateX(-50%);
}
.patient-avatar::after {
content: '';
position: absolute;
left: 50%;
bottom: -7px;
width: 50px;
height: 34px;
border-radius: 50% 50% 0 0;
background: rgba(255, 255, 255, 0.28);
transform: translateX(-50%);
}
.patient-avatar text {
position: relative;
z-index: 1;
}
.avatar-blue {
background: linear-gradient(135deg, #7da6d9, #00478d);
}
.avatar-teal {
background: linear-gradient(135deg, #5dd8e2, #006970);
}
.avatar-pink {
background: linear-gradient(135deg, #ffb3c7, #a63a5f);
}
.avatar-orange {
background: linear-gradient(135deg, #ffb691, #9f4300);
}
.avatar-purple {
background: linear-gradient(135deg, #b5a6ff, #5f4bb6);
}
.avatar-green {
background: linear-gradient(135deg, #9edc9a, #347a35);
}
.case-info {
flex: 1;
min-width: 0;
}
.case-title {
display: -webkit-box;
margin-bottom: 4px;
overflow: hidden;
color: #191c21;
font-size: 16px;
line-height: 20px;
font-weight: 600;
text-overflow: ellipsis;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.case-meta {
color: #424752;
font-size: 13px;
line-height: 20px;
}
.case-footer {
display: flex;
align-items: flex-end;
justify-content: space-between;
}
.case-no {
color: #c2c6d4;
font-size: 11px;
line-height: 16px;
font-weight: 600;
}
.mode-badge {
flex: 0 0 auto;
height: 24px;
padding: 0 8px;
border-radius: 999px;
display: flex;
align-items: center;
gap: 4px;
font-size: 11px;
line-height: 16px;
font-weight: 700;
}
.mode-badge-training {
background: #ecedf6;
color: #424752;
}
.mode-badge-teaching {
background: #d6e3ff;
color: #00478d;
}
.mode-icon {
width: 14px;
height: 14px;
background: currentColor;
}
.mode-icon-training {
-webkit-mask: url("data:image/svg+xml,%3Csvg%20viewBox='0%200%2024%2024'%20xmlns='http://www.w3.org/2000/svg'%3E%3Cpath%20d='M9%2016.2l-3.5-3.5L4%2014.2%209%2019%2020%208l-1.5-1.5L9%2016.2z'/%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='M9%2016.2l-3.5-3.5L4%2014.2%209%2019%2020%208l-1.5-1.5L9%2016.2z'/%3E%3C/svg%3E") center / contain no-repeat;
}
.mode-icon-teaching {
-webkit-mask: 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%202.28L18.85%209%2012%2012.72%205.15%209%2012%205.28zM5%2013.18v3.2C6.63%2018.24%209.26%2019%2012%2019s5.37-.76%207-2.62v-3.2l-7%203.82-7-3.82z'/%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%203L1%209l11%206%209-4.91V17h2V9L12%203zm0%202.28L18.85%209%2012%2012.72%205.15%209%2012%205.28zM5%2013.18v3.2C6.63%2018.24%209.26%2019%2012%2019s5.37-.76%207-2.62v-3.2l-7%203.82-7-3.82z'/%3E%3C/svg%3E") center / contain no-repeat;
}
.empty-state {
padding: 40px 0;
text-align: center;
color: #727783;
font-size: 14px;
line-height: 20px;
}
.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);
}
</style>