Files
hms/apps/miniprogram/src/pages/index/index.tsx
iven 98de5ad3b9
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled
feat(miniprogram): AI 报告查看 — 列表页/详情页/首页入口
- AI 分析 API service (ai-analysis.ts)
- 报告列表页: 滚动加载 + 状态标签 + 点击详情
- 报告详情页: Markdown 转 HTML + RichText 渲染
- app.config.ts 注册路由
- 首页添加 AI 报告快捷入口
2026-04-25 23:53:01 +08:00

185 lines
7.2 KiB
TypeScript
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.
import { View, Text } from '@tarojs/components';
import { useState } from 'react';
import Taro, { useDidShow } from '@tarojs/taro';
import { useAuthStore } from '../../stores/auth';
import { useHealthStore } from '../../stores/health';
import EmptyState from '../../components/EmptyState';
import Loading from '../../components/Loading';
import { trackPageView } from '@/services/analytics';
import * as appointmentApi from '@/services/appointment';
import * as followupApi from '@/services/followup';
import './index.scss';
interface UpcomingItem {
id: string;
title: string;
subtitle: string;
type: 'appointment' | 'followup';
}
export default function Index() {
const { user, currentPatient, restore: restoreAuth } = useAuthStore();
const { todaySummary, loading, refreshToday } = useHealthStore();
const [upcomingItems, setUpcomingItems] = useState<UpcomingItem[]>([]);
const [upcomingLoading, setUpcomingLoading] = useState(false);
useDidShow(() => {
restoreAuth();
refreshToday();
loadUpcoming();
trackPageView('home');
});
const loadUpcoming = async () => {
const patientId = useAuthStore.getState().currentPatient?.id;
if (!patientId) return;
setUpcomingLoading(true);
try {
const items: UpcomingItem[] = [];
const [apptRes, taskRes] = await Promise.allSettled([
appointmentApi.listAppointments(patientId, 1),
followupApi.listTasks(patientId, 'pending'),
]);
if (apptRes.status === 'fulfilled') {
for (const a of apptRes.value.data.slice(0, 3)) {
if (a.status === 'pending' || a.status === 'confirmed') {
items.push({
id: a.id,
title: `预约: ${a.appointment_date} ${a.start_time}`,
subtitle: `${a.doctor_name || '医护'} · ${a.status === 'pending' ? '待确认' : '已确认'}`,
type: 'appointment',
});
}
}
}
if (taskRes.status === 'fulfilled') {
for (const t of taskRes.value.data.slice(0, 2)) {
items.push({
id: t.id,
title: `随访: ${t.task_type}`,
subtitle: `${t.description?.slice(0, 30) || ''} · 截止 ${t.due_date}`,
type: 'followup',
});
}
}
setUpcomingItems(items);
} catch {
setUpcomingItems([]);
} finally {
setUpcomingLoading(false);
}
};
const hour = new Date().getHours();
const greeting = hour < 12 ? '早上好' : hour < 18 ? '下午好' : '晚上好';
const displayName = user?.display_name || currentPatient?.name || '访客';
const quickServices = [
{ label: '预约挂号', icon: '📅', path: '/pages/appointment/create/index' },
{ label: '健康录入', icon: '📊', path: '/pages/health/input/index' },
{ label: '健康趋势', icon: '📈', path: '/pages/health/trend/index' },
{ label: '资讯文章', icon: '📰', path: '/pages/article/index' },
{ label: 'AI 报告', icon: '🤖', path: '/pages/ai-report/list/index' },
];
const handleServiceClick = (path: string) => {
// tabBar 页面必须使用 switchTab其他页面用 navigateTo
const isTabBar = ['pages/index/index', 'pages/health/index', 'pages/appointment/index', 'pages/article/index', 'pages/profile/index']
.some((p) => path.includes(p));
if (isTabBar) {
Taro.switchTab({ url: path });
} else {
Taro.navigateTo({ url: path });
}
};
const healthItems = [
{ label: '血压', value: todaySummary?.blood_pressure ? `${todaySummary.blood_pressure.systolic}/${todaySummary.blood_pressure.diastolic}` : '--/--', unit: 'mmHg', status: todaySummary?.blood_pressure?.status },
{ label: '心率', value: todaySummary?.heart_rate ? `${todaySummary.heart_rate.value}` : '--', unit: 'bpm', status: todaySummary?.heart_rate?.status },
{ label: '血糖', value: todaySummary?.blood_sugar ? `${todaySummary.blood_sugar.value}` : '--', unit: 'mmol/L', status: todaySummary?.blood_sugar?.status },
{ label: '体重', value: todaySummary?.weight ? `${todaySummary.weight.value}` : '--', unit: 'kg', status: todaySummary?.weight?.status },
];
const getStatusLabel = (status?: string) => {
if (status === 'high') return '偏高 ▲';
if (status === 'low') return '偏低 ▼';
if (status === 'normal') return '正常';
return '';
};
return (
<View className='index-page'>
<View className='greeting-bar'>
<View className='greeting-text'>
<Text className='greeting-hello'>{greeting}</Text>
<Text className='greeting-name'>{displayName}</Text>
</View>
<Text className='greeting-date'>{new Date().toLocaleDateString('zh-CN')}</Text>
</View>
<View className='health-card'>
<Text className='section-title'></Text>
{loading && !todaySummary ? (
<Loading />
) : (
<View className='health-grid'>
{healthItems.map((item) => (
<View className={`health-item ${item.status === 'high' || item.status === 'low' ? 'health-item-warn' : item.status === 'normal' ? 'health-item-ok' : ''}`} key={item.label}>
<Text className='health-label'>{item.label}</Text>
<Text className='health-value'>{item.value}</Text>
<View className='health-item-bottom'>
<Text className='health-unit'>{item.unit}</Text>
{item.status && <Text className={`health-status ${item.status}`}>{getStatusLabel(item.status)}</Text>}
</View>
</View>
))}
</View>
)}
</View>
<View className='quick-services'>
<Text className='section-title'></Text>
<View className='service-grid'>
{quickServices.map((svc) => (
<View className='service-item' key={svc.label} onClick={() => handleServiceClick(svc.path)}>
<Text className='service-icon'>{svc.icon}</Text>
<Text className='service-label'>{svc.label}</Text>
</View>
))}
</View>
</View>
<View className='upcoming'>
<Text className='section-title'></Text>
{upcomingLoading ? (
<Loading />
) : upcomingItems.length === 0 ? (
<EmptyState icon='📋' text='暂无待办事项' hint='预约挂号后将在此显示' />
) : (
<View className='upcoming-list'>
{upcomingItems.map((item) => (
<View
key={item.id}
className='upcoming-item'
onClick={() => {
if (item.type === 'appointment') {
Taro.navigateTo({ url: `/pages/appointment/index` });
} else {
Taro.navigateTo({ url: `/pages/followup/detail/index?id=${item.id}` });
}
}}
>
<View className='upcoming-item-main'>
<Text className='upcoming-item-title'>{item.title}</Text>
<Text className='upcoming-item-sub'>{item.subtitle}</Text>
</View>
<Text className='upcoming-item-arrow'></Text>
</View>
))}
</View>
)}
</View>
</View>
);
}