Files
hms/apps/miniprogram/src/pages/index/index.tsx
iven 63d8b7a65d
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
fix(miniprogram): 对齐设计原型 — 移除渐变头部+体征数值内联+卡片布局
- 首页:移除渐变头部改为平铺背景,铃铛图标替代消息按钮
- 首页:体征数值与单位内联显示(同一行 baseline 对齐)
- 健康页:标题改为"健康数据",整体样式贴近原型紧凑风格
- 我的页:移除渐变头部改为平铺卡片,积分/打卡分两个独立卡片
- 我的页:菜单使用 emoji 图标替代文字图标,间距更紧凑
2026-04-30 23:04:36 +08:00

238 lines
8.8 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 ProgressRing from '../../components/ProgressRing';
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';
icon: string;
}
export default function Index() {
const { user, currentPatient } = useAuthStore();
const { todaySummary, loading, refreshToday } = useHealthStore();
const [upcomingItems, setUpcomingItems] = useState<UpcomingItem[]>([]);
const [upcomingLoading, setUpcomingLoading] = useState(false);
useDidShow(() => {
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, 2)) {
if (a.status === 'pending' || a.status === 'confirmed') {
items.push({
id: a.id,
title: `${a.appointment_date} ${a.start_time}`,
subtitle: `${a.doctor_name || '医护'} · ${a.department || '门诊'}`,
type: 'appointment',
icon: '约',
});
}
}
}
if (taskRes.status === 'fulfilled') {
for (const t of taskRes.value.data.slice(0, 1)) {
items.push({
id: t.id,
title: t.follow_up_type,
subtitle: `${t.content_template?.slice(0, 20) || '随访任务'} · 截止 ${t.planned_date}`,
type: 'followup',
icon: '随',
});
}
}
setUpcomingItems(items.slice(0, 3));
} catch {
setUpcomingItems([]);
} finally {
setUpcomingLoading(false);
}
};
const hour = new Date().getHours();
const greeting = hour < 12 ? '上午好' : hour < 18 ? '下午好' : '晚上好';
const displayName = user?.display_name || currentPatient?.name || '访客';
// 计算今日体征完成度4 个指标:血压/心率/血糖/体重)
const summary = todaySummary || {};
const indicators = [
!!summary.blood_pressure,
!!summary.heart_rate,
!!summary.blood_sugar,
!!summary.weight,
];
const completedCount = indicators.filter(Boolean).length;
const progressPercent = Math.round((completedCount / 4) * 100);
const indicatorCapsules = [
{ label: '血压', done: !!summary.blood_pressure },
{ label: '心率', done: !!summary.heart_rate },
{ label: '血糖', done: !!summary.blood_sugar },
{ label: '体重', done: !!summary.weight },
];
const healthItems = [
{ label: '血压', value: summary.blood_pressure ? `${summary.blood_pressure.systolic}/${summary.blood_pressure.diastolic}` : '—', unit: 'mmHg', status: summary.blood_pressure?.status, indicator: 'blood_pressure_systolic' },
{ label: '心率', value: summary.heart_rate ? `${summary.heart_rate.value}` : '—', unit: 'bpm', status: summary.heart_rate?.status, indicator: 'heart_rate' },
{ label: '血糖', value: summary.blood_sugar ? `${summary.blood_sugar.value}` : '—', unit: 'mmol/L', status: summary.blood_sugar?.status, indicator: 'blood_sugar_fasting' },
{ label: '体重', value: summary.weight ? `${summary.weight.value}` : '—', unit: 'kg', status: summary.weight?.status, indicator: 'weight' },
];
const getStatusTag = (status?: string) => {
if (status === 'high' || status === 'low') return { label: status === 'high' ? '偏高' : '偏低', cls: 'tag-warn' };
if (status === 'normal') return { label: '正常', cls: 'tag-ok' };
return null;
};
return (
<View className='home-page'>
{/* 区域 1问候 + 日期 + 消息入口 */}
<View className='greeting-section'>
<View className='greeting-left'>
<Text className='greeting-text'>{greeting}{displayName}</Text>
<Text className='greeting-date'>
{new Date().toLocaleDateString('zh-CN', { month: 'long', day: 'numeric', weekday: 'short' })}
</Text>
</View>
<View
className='greeting-bell'
onClick={() => Taro.switchTab({ url: '/pages/messages/index' })}
>
<Text className='greeting-bell-icon'>🔔</Text>
</View>
</View>
{/* 区域 2今日体征完成度 */}
<View
className='checkin-card'
onClick={() => Taro.switchTab({ url: '/pages/health/index' })}
>
<View className='checkin-left'>
<ProgressRing percent={progressPercent} />
</View>
<View className='checkin-right'>
<Text className='checkin-title'>
{completedCount === 4 ? '今日体征已全部记录' : completedCount === 0 ? '今日尚未记录体征' : `今日已记录 ${completedCount}/4 项`}
</Text>
<View className='checkin-capsules'>
{indicatorCapsules.map((cap) => (
<Text
key={cap.label}
className={`capsule ${cap.done ? 'capsule-done' : 'capsule-pending'}`}
>
{cap.done ? '✓' : ''}{cap.label}
</Text>
))}
</View>
</View>
</View>
{/* 区域 3今日体征 2x2 网格 */}
<View className='vitals-section'>
{loading && !todaySummary ? (
<Loading />
) : (
<View className='vitals-grid'>
{healthItems.map((item) => {
const tag = getStatusTag(item.status);
return (
<View
className='vital-card'
key={item.label}
onClick={() => Taro.navigateTo({ url: `/pages/pkg-health/trend/index?indicator=${item.indicator}` })}
>
<Text className='vital-label'>{item.label}</Text>
<View className='vital-value-row'>
<Text className='vital-value'>{item.value}</Text>
<Text className='vital-unit'>{item.unit}</Text>
</View>
<View className='vital-bottom'>
{tag && <Text className={`vital-tag ${tag.cls}`}>{tag.label}</Text>}
{!item.status && <Text className='vital-tag tag-empty'></Text>}
</View>
</View>
);
})}
</View>
)}
</View>
{/* 区域 4今日待办≤3 条) */}
<View className='todo-section'>
<Text className='section-title'></Text>
{upcomingLoading ? (
<Loading />
) : upcomingItems.length === 0 ? (
<View className='todo-empty'>
<Text className='todo-empty-text'></Text>
</View>
) : (
<View className='todo-list'>
{upcomingItems.map((item) => (
<View
key={item.id}
className='todo-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='todo-icon-wrap'>
<Text className='todo-icon-char'>{item.icon}</Text>
</View>
<View className='todo-info'>
<Text className='todo-title'>{item.title}</Text>
<Text className='todo-sub'>{item.subtitle}</Text>
</View>
<Text className='todo-arrow'></Text>
</View>
))}
</View>
)}
</View>
{/* 区域 5快捷操作 */}
<View className='action-section'>
<View
className='action-btn action-primary'
onClick={() => Taro.switchTab({ url: '/pages/health/index' })}
>
<Text className='action-btn-text'></Text>
</View>
<View
className='action-btn action-outline'
onClick={() => Taro.navigateTo({ url: '/pages/appointment/create/index' })}
>
<Text className='action-btn-text'></Text>
</View>
</View>
</View>
);
}