feat(miniprogram): 老年友好版本全面重设计 — 5→4 Tab + 首页/健康/消息/我的重写
- TabBar 从 5 Tab 调整为 4 Tab(首页/健康/消息/我的) - 首页重写为 5 区域布局:问候+进度环+体征2x2+待办+快捷操作 - 健康页重写:体征录入大输入框+趋势柱状图+BLE设备卡片 - 新建消息页:咨询对话+系统通知双 Tab - 我的页调整:菜单高度64px+新增积分商城入口 - 设计系统更新:色彩对比度提升(WCAG AA)+触控参数+老年友好 mixin - 新增 ProgressRing 组件(CSS conic-gradient 实现) - 修复 diagnoses 页面 $suc-l 未定义变量
This commit is contained in:
@@ -3,29 +3,19 @@ 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 * as articleApi from '../../services/article';
|
||||
import './index.scss';
|
||||
|
||||
const QUICK_SERVICES = [
|
||||
{ label: '预约挂号', char: '约', path: '/pages/appointment/create/index' },
|
||||
{ label: '健康录入', char: '录', path: '/pages/pkg-health/input/index' },
|
||||
{ label: '健康趋势', char: '势', path: '/pages/pkg-health/trend/index' },
|
||||
{ label: '健康告警', char: '警', path: '/pages/pkg-health/alerts/index' },
|
||||
{ label: '资讯文章', char: '文', path: '/pages/article/index' },
|
||||
{ label: 'AI 报告', char: 'AI', path: '/pages/ai-report/list/index' },
|
||||
];
|
||||
|
||||
interface UpcomingItem {
|
||||
id: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
type: 'appointment' | 'followup';
|
||||
statusLabel: string;
|
||||
statusType: 'ok' | 'warn' | 'default';
|
||||
icon: string;
|
||||
}
|
||||
|
||||
export default function Index() {
|
||||
@@ -33,24 +23,13 @@ export default function Index() {
|
||||
const { todaySummary, loading, refreshToday } = useHealthStore();
|
||||
const [upcomingItems, setUpcomingItems] = useState<UpcomingItem[]>([]);
|
||||
const [upcomingLoading, setUpcomingLoading] = useState(false);
|
||||
const [articles, setArticles] = useState<articleApi.Article[]>([]);
|
||||
|
||||
useDidShow(() => {
|
||||
refreshToday();
|
||||
loadUpcoming();
|
||||
loadArticles();
|
||||
trackPageView('home');
|
||||
});
|
||||
|
||||
const loadArticles = async () => {
|
||||
try {
|
||||
const res = await articleApi.listArticles({ page: 1, page_size: 2 });
|
||||
setArticles(res.data || []);
|
||||
} catch {
|
||||
// 文章接口可能不可用
|
||||
}
|
||||
};
|
||||
|
||||
const loadUpcoming = async () => {
|
||||
const patientId = useAuthStore.getState().currentPatient?.id;
|
||||
if (!patientId) return;
|
||||
@@ -62,32 +41,30 @@ export default function Index() {
|
||||
followupApi.listTasks(patientId, 'pending'),
|
||||
]);
|
||||
if (apptRes.status === 'fulfilled') {
|
||||
for (const a of apptRes.value.data.slice(0, 3)) {
|
||||
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 || ''}`,
|
||||
subtitle: `${a.doctor_name || '医护'} · ${a.department || '门诊'}`,
|
||||
type: 'appointment',
|
||||
statusLabel: a.status === 'pending' ? '待确认' : '已确认',
|
||||
statusType: a.status === 'pending' ? 'warn' : 'ok',
|
||||
icon: '约',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (taskRes.status === 'fulfilled') {
|
||||
for (const t of taskRes.value.data.slice(0, 2)) {
|
||||
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, 30) || ''} · 截止 ${t.planned_date}`,
|
||||
subtitle: `${t.content_template?.slice(0, 20) || '随访任务'} · 截止 ${t.planned_date}`,
|
||||
type: 'followup',
|
||||
statusLabel: '进行中',
|
||||
statusType: 'default',
|
||||
icon: '随',
|
||||
});
|
||||
}
|
||||
}
|
||||
setUpcomingItems(items);
|
||||
setUpcomingItems(items.slice(0, 3));
|
||||
} catch {
|
||||
setUpcomingItems([]);
|
||||
} finally {
|
||||
@@ -99,11 +76,29 @@ export default function Index() {
|
||||
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: 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 },
|
||||
{ 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) => {
|
||||
@@ -114,64 +109,67 @@ export default function Index() {
|
||||
|
||||
return (
|
||||
<View className='home-page'>
|
||||
{/* 问候区 */}
|
||||
{/* 区域 1:问候 + 日期 + 消息入口 */}
|
||||
<View className='greeting-section'>
|
||||
<View className='greeting-left'>
|
||||
<Text className='greeting-time'>{greeting}</Text>
|
||||
<Text className='greeting-name'>{displayName}</Text>
|
||||
<Text className='greeting-text'>{greeting},{displayName}</Text>
|
||||
<Text className='greeting-date'>
|
||||
{new Date().toLocaleDateString('zh-CN', { month: 'long', day: 'numeric', weekday: 'short' })}
|
||||
</Text>
|
||||
</View>
|
||||
<Text className='greeting-date'>{new Date().toLocaleDateString('zh-CN', { month: 'long', day: 'numeric', weekday: 'short' })}</Text>
|
||||
</View>
|
||||
|
||||
{/* 设备快捷入口 — 点击直接跳转设备同步页面 */}
|
||||
<View className='device-section'>
|
||||
<View className='device-entry' onClick={() => Taro.navigateTo({ url: '/pages/device-sync/index' })}>
|
||||
<View className='device-entry-icon-wrap'>
|
||||
<Text className='device-entry-icon-text'>{'\u{1FA7A}'}</Text>
|
||||
</View>
|
||||
<View className='device-entry-info'>
|
||||
<Text className='device-entry-name'>血压计</Text>
|
||||
<Text className='device-entry-desc'>蓝牙同步 · 自动采集</Text>
|
||||
</View>
|
||||
<Text className='device-entry-arrow'>{'›'}</Text>
|
||||
</View>
|
||||
<View className='device-entry' onClick={() => Taro.navigateTo({ url: '/pages/device-sync/index' })}>
|
||||
<View className='device-entry-icon-wrap'>
|
||||
<Text className='device-entry-icon-text'>{'\u{1FA78}'}</Text>
|
||||
</View>
|
||||
<View className='device-entry-info'>
|
||||
<Text className='device-entry-name'>血糖仪</Text>
|
||||
<Text className='device-entry-desc'>蓝牙同步 · 自动采集</Text>
|
||||
</View>
|
||||
<Text className='device-entry-arrow'>{'›'}</Text>
|
||||
<View
|
||||
className='greeting-msg'
|
||||
onClick={() => Taro.switchTab({ url: '/pages/messages/index' })}
|
||||
>
|
||||
<Text className='greeting-msg-icon'>消</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 今日健康 */}
|
||||
<View className='health-section'>
|
||||
<Text className='section-title'>今日健康</Text>
|
||||
{/* 区域 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 />
|
||||
) : !todaySummary || (!todaySummary.blood_pressure && !todaySummary.heart_rate && !todaySummary.blood_sugar && !todaySummary.weight) ? (
|
||||
<View className='health-empty'>
|
||||
<Text className='health-empty-text'>今天还没录入数据</Text>
|
||||
<View className='health-empty-action'>
|
||||
<View className='health-empty-btn' onClick={() => Taro.navigateTo({ url: '/pages/pkg-health/input/index' })}>
|
||||
<Text className='health-empty-btn-text'>点击开始记录</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
<View className='health-grid'>
|
||||
<View className='vitals-grid'>
|
||||
{healthItems.map((item) => {
|
||||
const tag = getStatusTag(item.status);
|
||||
return (
|
||||
<View className='health-cell' key={item.label} onClick={() => Taro.navigateTo({ url: `/pages/pkg-health/trend/index?indicator=${item.label === '血压' ? 'blood_pressure_systolic' : item.label === '心率' ? 'heart_rate' : item.label === '血糖' ? 'blood_sugar_fasting' : 'weight'}` })}>
|
||||
<Text className='health-cell-label'>{item.label}</Text>
|
||||
<Text className='health-cell-value'>{item.value}</Text>
|
||||
<View className='health-cell-bottom'>
|
||||
<Text className='health-cell-unit'>{item.unit}</Text>
|
||||
{tag && <Text className={`health-cell-tag ${tag.cls}`}>{tag.label}</Text>}
|
||||
<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>
|
||||
<Text className='vital-value'>{item.value}</Text>
|
||||
<View className='vital-bottom'>
|
||||
<Text className='vital-unit'>{item.unit}</Text>
|
||||
{tag && <Text className={`vital-tag ${tag.cls}`}>{tag.label}</Text>}
|
||||
{!item.status && <Text className='vital-tag tag-empty'>未记录</Text>}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
@@ -180,37 +178,21 @@ export default function Index() {
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 快捷服务 */}
|
||||
<View className='services-section'>
|
||||
<Text className='section-title'>快捷服务</Text>
|
||||
<View className='services-row'>
|
||||
{QUICK_SERVICES.map((svc) => (
|
||||
<View className='service-btn' key={svc.label} onClick={() => Taro.navigateTo({ url: svc.path })}>
|
||||
<View className='service-icon-wrap'>
|
||||
<Text className='service-icon-text'>{svc.char}</Text>
|
||||
</View>
|
||||
<Text className='service-label'>{svc.label}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 待办事项 */}
|
||||
<View className='upcoming-section'>
|
||||
<Text className='section-title'>待办事项</Text>
|
||||
{/* 区域 4:今日待办(≤3 条) */}
|
||||
<View className='todo-section'>
|
||||
<Text className='section-title'>今日待办</Text>
|
||||
{upcomingLoading ? (
|
||||
<Loading />
|
||||
) : upcomingItems.length === 0 ? (
|
||||
<View className='upcoming-empty'>
|
||||
<Text className='upcoming-empty-text'>暂无待办事项</Text>
|
||||
<Text className='upcoming-empty-hint'>预约挂号后将在此显示</Text>
|
||||
<View className='todo-empty'>
|
||||
<Text className='todo-empty-text'>今天没有待办事项</Text>
|
||||
</View>
|
||||
) : (
|
||||
<View className='upcoming-list'>
|
||||
<View className='todo-list'>
|
||||
{upcomingItems.map((item) => (
|
||||
<View
|
||||
key={item.id}
|
||||
className='upcoming-item'
|
||||
className='todo-item'
|
||||
onClick={() => {
|
||||
if (item.type === 'appointment') {
|
||||
Taro.navigateTo({ url: '/pages/appointment/index' });
|
||||
@@ -219,36 +201,35 @@ export default function Index() {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<View className='upcoming-item-main'>
|
||||
<Text className='upcoming-item-title'>{item.title}</Text>
|
||||
<Text className='upcoming-item-sub'>{item.subtitle}</Text>
|
||||
<View className='todo-icon-wrap'>
|
||||
<Text className='todo-icon-char'>{item.icon}</Text>
|
||||
</View>
|
||||
<Text className={`upcoming-item-tag tag-${item.statusType}`}>{item.statusLabel}</Text>
|
||||
<Text className='upcoming-item-arrow'>›</Text>
|
||||
<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>
|
||||
|
||||
{/* 健康资讯 */}
|
||||
{articles.length > 0 && (
|
||||
<View className='articles-section'>
|
||||
<Text className='section-title'>健康资讯</Text>
|
||||
{articles.map((article) => (
|
||||
<View
|
||||
className='article-card'
|
||||
key={article.id}
|
||||
onClick={() => Taro.navigateTo({ url: `/pages/article/detail/index?id=${article.id}` })}
|
||||
>
|
||||
<Text className='article-card-title'>{article.title}</Text>
|
||||
<Text className='article-card-meta'>
|
||||
{article.category_name || '健康'}{article.published_at ? ` · ${article.published_at.slice(0, 10)}` : ''}
|
||||
</Text>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user