diff --git a/apps/miniprogram/src/components/TrendChart/index.scss b/apps/miniprogram/src/components/TrendChart/index.scss index 7e26901..bd44ba9 100644 --- a/apps/miniprogram/src/components/TrendChart/index.scss +++ b/apps/miniprogram/src/components/TrendChart/index.scss @@ -2,6 +2,7 @@ .trend-chart { width: 100%; + position: relative; } .trend-chart-empty { @@ -15,3 +16,32 @@ font-size: 28px; color: $tx3; } + +.trend-chart-skeleton { + position: absolute; + top: 20px; + left: 45px; + right: 15px; + bottom: 30px; + display: flex; + flex-direction: column; + justify-content: space-around; + z-index: 1; +} + +.skeleton-line { + height: 8px; + border-radius: 4px; + background: linear-gradient(90deg, #e2e8f0 25%, #f1f5f9 50%, #e2e8f0 75%); + background-size: 200% 100%; + animation: skeleton-pulse 1.5s ease-in-out infinite; +} + +.skeleton-line-1 { width: 70%; } +.skeleton-line-2 { width: 90%; } +.skeleton-line-3 { width: 60%; } + +@keyframes skeleton-pulse { + 0% { background-position: 200% 0; } + 100% { background-position: -200% 0; } +} diff --git a/apps/miniprogram/src/components/TrendChart/index.tsx b/apps/miniprogram/src/components/TrendChart/index.tsx index dfd177a..b397961 100644 --- a/apps/miniprogram/src/components/TrendChart/index.tsx +++ b/apps/miniprogram/src/components/TrendChart/index.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef, useCallback } from 'react'; +import React, { useEffect, useRef, useCallback, useState } from 'react'; import { View, Text } from '@tarojs/components'; import EcCanvas from '../EcCanvas'; import type { EcCanvasRef } from '../EcCanvas'; @@ -20,6 +20,7 @@ export default function TrendChart({ height = 500, }: TrendChartProps) { const chartRef = useRef(null); + const [chartReady, setChartReady] = useState(false); const getOption = useCallback(() => { if (!data || data.length === 0) return null; @@ -107,6 +108,7 @@ export default function TrendChart({ const option = getOption(); if (option) { chartRef.current.setOption(option); + setChartReady(true); } } }, [data, getOption]); @@ -121,6 +123,13 @@ export default function TrendChart({ return ( + {!chartReady && ( + + + + + + )} ); diff --git a/apps/miniprogram/src/pages/appointment/create/index.scss b/apps/miniprogram/src/pages/appointment/create/index.scss index 45bb134..b0d94ac 100644 --- a/apps/miniprogram/src/pages/appointment/create/index.scss +++ b/apps/miniprogram/src/pages/appointment/create/index.scss @@ -102,8 +102,9 @@ border-color: $wrn; } &.slot-full { - opacity: 0.5; + opacity: 0.4; background: $bd-l; + pointer-events: none; } &.slot-selected { border-color: $pri; diff --git a/apps/miniprogram/src/pages/consultation/detail/index.scss b/apps/miniprogram/src/pages/consultation/detail/index.scss index 4d928b9..e0ecc2c 100644 --- a/apps/miniprogram/src/pages/consultation/detail/index.scss +++ b/apps/miniprogram/src/pages/consultation/detail/index.scss @@ -73,6 +73,26 @@ } } +.msg-date-divider { + display: flex; + justify-content: center; + padding: 16px 0 12px; + + &__text { + font-size: 22px; + color: #94A3B8; + background: #F1F5F9; + padding: 4px 16px; + border-radius: 8px; + } +} + +.msg-image { + width: 320px; + border-radius: 12px; + margin-top: 4px; +} + .msg-time { font-size: 20px; color: $tx3; diff --git a/apps/miniprogram/src/pages/consultation/detail/index.tsx b/apps/miniprogram/src/pages/consultation/detail/index.tsx index b698ca0..78c4fef 100644 --- a/apps/miniprogram/src/pages/consultation/detail/index.tsx +++ b/apps/miniprogram/src/pages/consultation/detail/index.tsx @@ -1,5 +1,5 @@ import { useState, useEffect, useRef } from 'react'; -import { View, Text, Input, ScrollView } from '@tarojs/components'; +import { View, Text, Input, Image, ScrollView } from '@tarojs/components'; import Taro, { useRouter } from '@tarojs/taro'; import { getSession, @@ -116,6 +116,24 @@ export default function ConsultationDetail() { return d.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }); }; + const getDateLabel = (dateStr: string): string => { + const d = new Date(dateStr); + const today = new Date(); + const yesterday = new Date(); + yesterday.setDate(yesterday.getDate() - 1); + + const dStr = d.toDateString(); + if (dStr === today.toDateString()) return '今天'; + if (dStr === yesterday.toDateString()) return '昨天'; + return d.toLocaleDateString('zh-CN', { month: 'long', day: 'numeric' }); + }; + + const isDifferentDay = (a: string, b: string): boolean => { + return new Date(a).toDateString() !== new Date(b).toDateString(); + }; + + const isImageUrl = (url: string) => /\.(jpg|jpeg|png|gif|webp)(\?.*)?$/i.test(url); + if (loading) return ; const isOpen = session?.status !== 'closed'; @@ -137,11 +155,28 @@ export default function ConsultationDetail() { > {messages.map((msg, idx) => { const isSelf = msg.sender_role === 'patient'; + const showDateDivider = idx === 0 || isDifferentDay(msg.created_at, messages[idx - 1].created_at); return ( - - - {msg.content} - {formatTime(msg.created_at)} + + {showDateDivider && ( + + {getDateLabel(msg.created_at)} + + )} + + + {isImageUrl(msg.content) ? ( + Taro.previewImage({ urls: [msg.content], current: msg.content })} + /> + ) : ( + {msg.content} + )} + {formatTime(msg.created_at)} + ); diff --git a/apps/miniprogram/src/pages/doctor/index.scss b/apps/miniprogram/src/pages/doctor/index.scss index 356e501..600642f 100644 --- a/apps/miniprogram/src/pages/doctor/index.scss +++ b/apps/miniprogram/src/pages/doctor/index.scss @@ -29,6 +29,54 @@ color: $tx3; } + &__alert { + display: flex; + align-items: center; + margin: 16px 24px; + padding: 16px 20px; + background: #FEF2F2; + border-radius: $r; + border-left: 4px solid #EF4444; + } + + &__alert-icon { + width: 36px; + height: 36px; + border-radius: 50%; + background: #EF4444; + color: #fff; + text-align: center; + line-height: 36px; + font-weight: bold; + font-size: 22px; + margin-right: 12px; + flex-shrink: 0; + } + + &__alert-text { + flex: 1; + font-size: 26px; + color: #991B1B; + } + + &__alert-link { + font-size: 24px; + color: #EF4444; + flex-shrink: 0; + } + + &__search { + margin: 0 24px 16px; + } + + &__search-input { + background: #F1F5F9; + border-radius: $r; + padding: 16px 20px; + font-size: 26px; + color: #94A3B8; + } + &__section { margin-bottom: 40px; } diff --git a/apps/miniprogram/src/pages/doctor/index.tsx b/apps/miniprogram/src/pages/doctor/index.tsx index 899bba1..4a8a3ed 100644 --- a/apps/miniprogram/src/pages/doctor/index.tsx +++ b/apps/miniprogram/src/pages/doctor/index.tsx @@ -1,5 +1,5 @@ import { useState, useEffect } from 'react'; -import { View, Text, ScrollView } from '@tarojs/components'; +import { View, Text, Input, ScrollView } from '@tarojs/components'; import Taro from '@tarojs/taro'; import { useAuthStore } from '@/stores/auth'; import * as doctorApi from '@/services/doctor'; @@ -29,11 +29,14 @@ const HEALTH_CARDS: CardConfig[] = [ const QUICK_ACTIONS = [ { label: '化验审核', initial: '审', route: '/pages/doctor/report/index' }, { label: '患者查询', initial: '查', route: '/pages/doctor/patients/index' }, + { label: '随访记录', initial: '随', route: '/pages/doctor/followup/index' }, + { label: '排班查看', initial: '排', route: '/pages/doctor/patients/index' }, ]; export default function DoctorHome() { const { user, logout } = useAuthStore(); const [dashboard, setDashboard] = useState(null); + const [alertCount, setAlertCount] = useState(0); const [loading, setLoading] = useState(true); useEffect(() => { @@ -44,6 +47,9 @@ export default function DoctorHome() { try { const data = await doctorApi.getDashboard(); setDashboard(data); + // 从仪表盘数据提取异常体征患者数 + const count = (data as Record)?.abnormal_vital_count; + setAlertCount(typeof count === 'number' ? count : 0); } catch { // 静默失败,显示占位 } finally { @@ -78,6 +84,22 @@ export default function DoctorHome() { + {alertCount > 0 && ( + + ! + {alertCount} 位患者体征异常 + Taro.navigateTo({ url: '/pages/doctor/patients/index' })}>查看 → + + )} + + + Taro.navigateTo({ url: '/pages/doctor/patients/index' })} + /> + + 工作概览 diff --git a/apps/miniprogram/src/pages/health/daily-monitoring/index.scss b/apps/miniprogram/src/pages/health/daily-monitoring/index.scss index 711b6ea..33a99ce 100644 --- a/apps/miniprogram/src/pages/health/daily-monitoring/index.scss +++ b/apps/miniprogram/src/pages/health/daily-monitoring/index.scss @@ -42,7 +42,7 @@ color: $tx3; } -/* ── card ── */ +/* ── card (standalone, used for date picker) ── */ .dm-card { background: $card; border-radius: $r; @@ -58,22 +58,6 @@ margin-bottom: 20px; } -.dm-card-serial { - @include flex-center; - width: 40px; - height: 40px; - border-radius: $r-sm; - background: $pri-l; - flex-shrink: 0; -} - -.dm-card-serial-text { - font-family: 'Georgia', 'Times New Roman', serif; - font-size: 22px; - font-weight: bold; - color: $pri; -} - .dm-card-title { font-family: 'Georgia', 'Times New Roman', serif; font-size: 28px; @@ -112,6 +96,61 @@ display: inline-block; } +/* ── collapsible group ── */ +.dm-group { + background: $card; + border-radius: $r; + box-shadow: $shadow-md; + margin: 0 24px 20px; + overflow: hidden; +} + +.dm-group-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 24px 28px; + + &:active { + background: $bd-l; + } +} + +.dm-group-title { + font-family: 'Georgia', 'Times New Roman', serif; + font-size: 28px; + font-weight: 600; + color: $tx; +} + +.dm-group-arrow { + font-size: 24px; + color: $tx3; + transition: transform 0.2s ease; + display: inline-block; +} + +.dm-group-arrow-open { + transform: rotate(90deg); +} + +.dm-group-body { + padding: 0 28px 28px; +} + +.dm-group-collapsed .dm-group-body { + display: none; +} + +/* ── inner field spacing (within groups) ── */ +.dm-inner-field { + margin-bottom: 24px; + + &:last-child { + margin-bottom: 0; + } +} + /* ── blood pressure group ── */ .dm-bp-group { display: flex; @@ -192,6 +231,23 @@ width: 100%; } +/* ── abnormal value highlighting ── */ +.dm-input-abnormal { + border: 2px solid $wrn; + background: $wrn-l; +} + +.dm-field-warning { + font-size: 22px; + color: $wrn; + margin-top: 8px; + display: block; +} + +.dm-field-warning-low { + color: #0284C7; +} + /* ── submit ── */ .dm-submit { background: $pri; diff --git a/apps/miniprogram/src/pages/health/daily-monitoring/index.tsx b/apps/miniprogram/src/pages/health/daily-monitoring/index.tsx index a331013..94b4460 100644 --- a/apps/miniprogram/src/pages/health/daily-monitoring/index.tsx +++ b/apps/miniprogram/src/pages/health/daily-monitoring/index.tsx @@ -19,6 +19,41 @@ function formatDate(date: Date): string { return `${y}-${m}-${d}`; } +// ── Abnormal value detection ── + +const REFERENCE_RANGES: Record = { + systolic: { min: 90, max: 140 }, + diastolic: { min: 60, max: 90 }, + bloodSugar: { min: 3.9, max: 6.1 }, + weight: null, + fluidIntake: null, + urineOutput: null, +}; + +type AbnormalResult = { abnormal: boolean; direction: 'high' | 'low' | null }; + +const checkAbnormal = (value: string, field: string): AbnormalResult => { + const ref = REFERENCE_RANGES[field]; + if (!value || !ref) return { abnormal: false, direction: null }; + const num = parseFloat(value); + if (isNaN(num)) return { abnormal: false, direction: null }; + if (num > ref.max) return { abnormal: true, direction: 'high' }; + if (num < ref.min) return { abnormal: true, direction: 'low' }; + return { abnormal: false, direction: null }; +}; + +// ── Section state type ── + +type SectionKey = 'morning' | 'evening' | 'other'; + +const FIELD_LABELS: Record = { + morningSystolic: '晨间收缩压', + morningDiastolic: '晨间舒张压', + eveningSystolic: '晚间收缩压', + eveningDiastolic: '晚间舒张压', + bloodSugar: '血糖', +}; + export default function DailyMonitoring() { const { currentPatient } = useAuthStore(); @@ -46,6 +81,17 @@ export default function DailyMonitoring() { const [notes, setNotes] = useState(''); const [submitting, setSubmitting] = useState(false); + // ── Collapsible sections ── + const [collapsed, setCollapsed] = useState>({ + morning: false, + evening: false, + other: true, + }); + + const toggleSection = (key: SectionKey) => { + setCollapsed(prev => ({ ...prev, [key]: !prev[key] })); + }; + useDidShow(() => { Taro.setNavigationBarTitle({ title: '日常监测上报' }); }); @@ -62,6 +108,28 @@ export default function DailyMonitoring() { setNotes(''); }; + // ── Abnormal field gathering for submit confirmation ── + const gatherAbnormalFields = (): string[] => { + const abnormalFields: string[] = []; + + const checks: Array<[string, string]> = [ + ['morningSystolic', morningSystolic], + ['morningDiastolic', morningDiastolic], + ['eveningSystolic', eveningSystolic], + ['eveningDiastolic', eveningDiastolic], + ['bloodSugar', bloodSugar], + ]; + + for (const [field, value] of checks) { + const result = checkAbnormal(value, field); + if (result.abnormal) { + abnormalFields.push(FIELD_LABELS[field]); + } + } + + return abnormalFields; + }; + const handleSubmit = async () => { if (!currentPatient) { Taro.showToast({ title: '请先选择就诊人', icon: 'none' }); @@ -120,6 +188,18 @@ export default function DailyMonitoring() { } } + // ── Pre-submit abnormal confirmation ── + const abnormalFields = gatherAbnormalFields(); + if (abnormalFields.length > 0) { + const confirmed = await Taro.showModal({ + title: '数值异常提醒', + content: `以下指标超出正常范围:${abnormalFields.join('、')}。确认提交?`, + confirmText: '确认提交', + cancelText: '返回修改', + }); + if (!confirmed.confirm) return; + } + setSubmitting(true); try { await createDailyMonitoring({ @@ -164,6 +244,13 @@ export default function DailyMonitoring() { const isToday = recordDate === today; + // ── Abnormal state helpers for rendering ── + const morningSysAbnormal = checkAbnormal(morningSystolic, 'systolic'); + const morningDiaAbnormal = checkAbnormal(morningDiastolic, 'diastolic'); + const eveningSysAbnormal = checkAbnormal(eveningSystolic, 'systolic'); + const eveningDiaAbnormal = checkAbnormal(eveningDiastolic, 'diastolic'); + const bloodSugarAbnormal = checkAbnormal(bloodSugar, 'bloodSugar'); + return ( {/* 页面标题 */} @@ -175,12 +262,9 @@ export default function DailyMonitoring() { 每日健康数据上报 - {/* 日期选择 */} + {/* 日期选择 (standalone card) */} - - 1 - 记录日期 {isToday && ( 今日 @@ -199,176 +283,185 @@ export default function DailyMonitoring() { - {/* 晨起血压 */} - - - - 2 - - 晨起血压 + {/* ── Group 1: 晨间体征 (default open) ── */} + + toggleSection('morning')}> + 晨间体征 + - - - 收缩压 - setMorningSystolic(e.detail.value)} - /> + + + + 收缩压 + setMorningSystolic(e.detail.value)} + /> + {morningSysAbnormal.abnormal && ( + + {morningSysAbnormal.direction === 'high' ? '偏高' : '偏低'} + + )} + + + + / + + + + 舒张压 + setMorningDiastolic(e.detail.value)} + /> + {morningDiaAbnormal.abnormal && ( + + {morningDiaAbnormal.direction === 'high' ? '偏高' : '偏低'} + + )} + - - - / - + mmHg + + + + {/* ── Group 2: 晚间体征 (default open) ── */} + + toggleSection('evening')}> + 晚间体征 + + + + + + 收缩压 + setEveningSystolic(e.detail.value)} + /> + {eveningSysAbnormal.abnormal && ( + + {eveningSysAbnormal.direction === 'high' ? '偏高' : '偏低'} + + )} + + + + / + + + + 舒张压 + setEveningDiastolic(e.detail.value)} + /> + {eveningDiaAbnormal.abnormal && ( + + {eveningDiaAbnormal.direction === 'high' ? '偏高' : '偏低'} + + )} + - - 舒张压 + mmHg + + + + {/* ── Group 3: 其他指标 (default collapsed) ── */} + + toggleSection('other')}> + 其他指标 + + + + {/* 体重 */} + + 体重 + + setWeight(e.detail.value)} + /> + kg + + + + {/* 血糖 */} + + 血糖 + + setBloodSugar(e.detail.value)} + /> + mmol/L + + {bloodSugarAbnormal.abnormal && ( + + {bloodSugarAbnormal.direction === 'high' ? '偏高' : '偏低'} + + )} + + + {/* 饮水量 */} + + 饮水量 + + setFluidIntake(e.detail.value)} + /> + ml + + + + {/* 尿量 */} + + 尿量 + + setUrineOutput(e.detail.value)} + /> + ml + + + + {/* 备注 */} + + 备注 setMorningDiastolic(e.detail.value)} + className='dm-input-box dm-input-full' + placeholder='如:头晕、乏力等(可选)' + value={notes} + onInput={(e) => setNotes(e.detail.value)} /> - mmHg - - - {/* 晚间血压 */} - - - - 3 - - 晚间血压 - - - - 收缩压 - setEveningSystolic(e.detail.value)} - /> - - - - / - - - - 舒张压 - setEveningDiastolic(e.detail.value)} - /> - - - mmHg - - - {/* 体重 */} - - - - 4 - - 体重 - - - setWeight(e.detail.value)} - /> - kg - - - - {/* 血糖 */} - - - - 5 - - 血糖 - - - setBloodSugar(e.detail.value)} - /> - mmol/L - - - - {/* 饮水量 */} - - - - 6 - - 饮水量 - - - setFluidIntake(e.detail.value)} - /> - ml - - - - {/* 尿量 */} - - - - 7 - - 尿量 - - - setUrineOutput(e.detail.value)} - /> - ml - - - - {/* 备注 */} - - - - 8 - - 备注 - - setNotes(e.detail.value)} - /> {/* 提交 */} diff --git a/apps/miniprogram/src/pages/health/index.scss b/apps/miniprogram/src/pages/health/index.scss index 45cb7c3..0cc8538 100644 --- a/apps/miniprogram/src/pages/health/index.scss +++ b/apps/miniprogram/src/pages/health/index.scss @@ -88,57 +88,6 @@ font-weight: 500; } -/* ─── 打卡卡片 ─── */ -.checkin-card { - display: flex; - align-items: center; - justify-content: space-between; - background: $card; - border-radius: $r; - padding: 24px 28px; - margin: 0 24px 24px; - box-shadow: $shadow-sm; -} - -.checkin-info { - display: flex; - flex-direction: column; - gap: 4px; -} - -.checkin-done { - font-size: 28px; - color: $acc; - font-weight: 600; -} - -.checkin-streak { - font-size: 22px; - color: $tx3; -} - -.checkin-pending { - font-size: 28px; - color: $tx2; - font-weight: 500; -} - -.checkin-go { - background: $pri; - border-radius: $r-sm; - padding: 12px 28px; - - &:active { - opacity: 0.85; - } -} - -.checkin-go-text { - font-size: 24px; - color: #fff; - font-weight: 600; -} - /* ─── 通用 section ─── */ .health-section { margin: 0 24px 28px; @@ -206,6 +155,23 @@ display: block; } +.vital-bar-track { + height: 6px; + background: $bd-l; + border-radius: 3px; + margin-top: 12px; + overflow: hidden; +} + +.vital-bar-fill { + height: 100%; + border-radius: 3px; + transition: width 0.3s ease; + + &.bar-green { background: $acc; } + &.bar-orange { background: $wrn; } +} + /* ─── 趋势入口 ─── */ .trend-row { background: $card; diff --git a/apps/miniprogram/src/pages/health/index.tsx b/apps/miniprogram/src/pages/health/index.tsx index f8de010..c674843 100644 --- a/apps/miniprogram/src/pages/health/index.tsx +++ b/apps/miniprogram/src/pages/health/index.tsx @@ -1,5 +1,5 @@ import { useState } from 'react'; -import { View, Text } from '@tarojs/components'; +import { View, Text, ScrollView } from '@tarojs/components'; import Taro, { useDidShow } from '@tarojs/taro'; import { useHealthStore } from '../../stores/health'; import { listDailyMonitoring, DailyMonitoring } from '../../services/health'; @@ -16,6 +16,27 @@ function getStatusTag(status?: string) { return null; } +/** 根据 status 计算 sparkline bar 的颜色 */ +function getBarColor(status?: string): string { + if (status === 'normal') return 'bar-green'; + if (status === 'high' || status === 'low') return 'bar-orange'; + return 'bar-green'; +} + +/** 计算数值在参考范围中的位置百分比 (0-100) */ +function getBarPercent(value: number | undefined, ref?: string): number { + if (!value || !ref) return 50; + const match = ref.match(/([\d.]+)\s*[-–]\s*([\d.]+)/); + if (!match) return 50; + const low = parseFloat(match[1]); + const high = parseFloat(match[2]); + if (high <= low) return 50; + // 将值映射到 0-100 范围,参考范围占据中间 70%(15%-85%) + const range = high - low; + const normalized = (value - low + range * 0.3) / (range * 1.6); + return Math.max(5, Math.min(95, normalized * 100)); +} + export default function Health() { const { todaySummary, loading, refreshToday } = useHealthStore(); const { currentPatient } = useAuthStore(); @@ -63,10 +84,10 @@ export default function Health() { const summary = todaySummary || {}; const items = [ - { label: '血压', value: summary.blood_pressure ? `${summary.blood_pressure.systolic}/${summary.blood_pressure.diastolic}` : '--/--', unit: 'mmHg', indicator: 'blood_pressure_systolic', status: summary.blood_pressure?.status, ref: summary.blood_pressure?.reference_range }, - { label: '心率', value: summary.heart_rate ? `${summary.heart_rate.value}` : '--', unit: 'bpm', indicator: 'heart_rate', status: summary.heart_rate?.status, ref: summary.heart_rate?.reference_range }, - { label: '血糖', value: summary.blood_sugar ? `${summary.blood_sugar.value}` : '--', unit: 'mmol/L', indicator: 'blood_sugar_fasting', status: summary.blood_sugar?.status, ref: summary.blood_sugar?.reference_range }, - { label: '体重', value: summary.weight ? `${summary.weight.value}` : '--', unit: 'kg', indicator: 'weight', status: summary.weight?.status, ref: summary.weight?.reference_range }, + { label: '血压', value: summary.blood_pressure ? `${summary.blood_pressure.systolic}/${summary.blood_pressure.diastolic}` : '--/--', unit: 'mmHg', indicator: 'blood_pressure_systolic', status: summary.blood_pressure?.status, ref: summary.blood_pressure?.reference_range, numValue: summary.blood_pressure?.systolic }, + { label: '心率', value: summary.heart_rate ? `${summary.heart_rate.value}` : '--', unit: 'bpm', indicator: 'heart_rate', status: summary.heart_rate?.status, ref: summary.heart_rate?.reference_range, numValue: summary.heart_rate?.value }, + { label: '血糖', value: summary.blood_sugar ? `${summary.blood_sugar.value}` : '--', unit: 'mmol/L', indicator: 'blood_sugar_fasting', status: summary.blood_sugar?.status, ref: summary.blood_sugar?.reference_range, numValue: summary.blood_sugar?.value }, + { label: '体重', value: summary.weight ? `${summary.weight.value}` : '--', unit: 'kg', indicator: 'weight', status: summary.weight?.status, ref: summary.weight?.reference_range, numValue: summary.weight?.value }, ]; const quickActions = [ @@ -102,7 +123,7 @@ export default function Health() { - {/* 快捷操作 */} + {/* 快捷操作 + 打卡状态紧凑合并 */} {quickActions.map((a) => ( @@ -112,30 +133,22 @@ export default function Health() { {a.label} ))} - - - {/* 打卡状态 */} - {checkinStatus && ( - - - {checkinStatus.checked_in_today ? ( - <> - 今日已打卡 - {checkinStatus.consecutive_days > 0 && ( - 连续 {checkinStatus.consecutive_days} 天 - )} - - ) : ( - 今日未打卡 - )} - - {!checkinStatus.checked_in_today && ( - - 去打卡 + {checkinStatus && ( + + + - )} - - )} + + {checkinStatus.checked_in_today + ? (checkinStatus.consecutive_days > 0 ? `已打卡${checkinStatus.consecutive_days}天` : '已打卡') + : '去打卡'} + + + )} + {/* 今日体征概览 */} @@ -146,6 +159,8 @@ export default function Health() { {items.map((item) => { const tag = getStatusTag(item.status); + const barColor = getBarColor(item.status); + const barPercent = getBarPercent(item.numValue, item.ref); return ( goToTrend(item.indicator)}> {item.label} @@ -154,6 +169,12 @@ export default function Health() { {item.unit} {tag && {tag.label}} + {/* Sparkline bar */} + {item.ref && item.numValue != null && ( + + + + )} {item.ref && 参考 {item.ref}} ); @@ -162,20 +183,20 @@ export default function Health() { )} - {/* 趋势快捷入口 */} + {/* 趋势快捷入口 — 水平滚动卡片 */} 健康趋势 - + {trendLinks.map((t) => ( - goToTrend(t.indicator)}> - - {t.char} + goToTrend(t.indicator)}> + + {t.char} - {t.label} - + {t.label} + 查看 › ))} - + {/* 最近监测记录 */} diff --git a/apps/miniprogram/src/pages/index/index.scss b/apps/miniprogram/src/pages/index/index.scss index 69fcb1e..f5c4add 100644 --- a/apps/miniprogram/src/pages/index/index.scss +++ b/apps/miniprogram/src/pages/index/index.scss @@ -261,3 +261,69 @@ color: $tx3; flex-shrink: 0; } + +/* ─── 健康空状态 ─── */ +.health-empty { + background: $bg; + border-radius: $r-sm; + padding: 40px 24px; + text-align: center; +} + +.health-empty-text { + display: block; + font-size: 28px; + color: $tx2; + margin-bottom: 8px; +} + +.health-empty-action { + display: flex; + justify-content: center; + padding: 24px 0 0; +} + +.health-empty-btn { + background: $pri; + border-radius: $r; + padding: 16px 40px; +} + +.health-empty-btn-text { + color: #fff; + font-size: 26px; + font-weight: 500; +} + +/* ─── 健康资讯 ─── */ +.articles-section { + margin: 0 24px 24px; +} + +.article-card { + background: $card; + border-radius: $r; + padding: 24px; + margin-bottom: 16px; + box-shadow: $shadow-sm; + + &:active { + opacity: 0.7; + } +} + +.article-card-title { + font-size: 28px; + color: $tx; + display: block; + font-weight: 500; + margin-bottom: 8px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.article-card-meta { + font-size: 22px; + color: $tx3; +} diff --git a/apps/miniprogram/src/pages/index/index.tsx b/apps/miniprogram/src/pages/index/index.tsx index 1523877..f374048 100644 --- a/apps/miniprogram/src/pages/index/index.tsx +++ b/apps/miniprogram/src/pages/index/index.tsx @@ -8,6 +8,7 @@ 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'; interface UpcomingItem { @@ -24,14 +25,25 @@ export default function Index() { const { todaySummary, loading, refreshToday } = useHealthStore(); const [upcomingItems, setUpcomingItems] = useState([]); const [upcomingLoading, setUpcomingLoading] = useState(false); + const [articles, setArticles] = useState([]); useDidShow(() => { restoreAuth(); 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; @@ -81,11 +93,11 @@ export default function Index() { const displayName = user?.display_name || currentPatient?.name || '访客'; const quickServices = [ - { label: '预约挂号', path: '/pages/appointment/create/index' }, - { label: '健康录入', path: '/pages/health/input/index' }, - { label: '健康趋势', path: '/pages/health/trend/index' }, - { label: '资讯文章', path: '/pages/article/index' }, - { label: 'AI 报告', path: '/pages/ai-report/list/index' }, + { label: '预约挂号', char: '约', path: '/pages/appointment/create/index' }, + { label: '健康录入', char: '录', path: '/pages/health/input/index' }, + { label: '健康趋势', char: '势', path: '/pages/health/trend/index' }, + { label: '资讯文章', char: '文', path: '/pages/article/index' }, + { label: 'AI 报告', char: 'AI', path: '/pages/ai-report/list/index' }, ]; const handleServiceClick = (path: string) => { @@ -121,6 +133,15 @@ export default function Index() { 今日健康 {loading && !todaySummary ? ( + ) : !todaySummary || (!todaySummary.blood_pressure && !todaySummary.heart_rate && !todaySummary.blood_sugar && !todaySummary.weight) ? ( + + 今天还没录入数据 + + Taro.navigateTo({ url: '/pages/health/input/index' })}> + 点击开始记录 + + + ) : ( {healthItems.map((item) => { @@ -147,7 +168,7 @@ export default function Index() { {quickServices.map((svc) => ( handleServiceClick(svc.path)}> - {svc.label[0]} + {svc.char} {svc.label} @@ -190,6 +211,25 @@ export default function Index() { )} + + {/* 健康资讯 */} + {articles.length > 0 && ( + + 健康资讯 + {articles.map((article) => ( + Taro.navigateTo({ url: `/pages/article/detail/index?id=${article.id}` })} + > + {article.title} + + {article.category_name || '健康'}{article.published_at ? ` · ${article.published_at.slice(0, 10)}` : ''} + + + ))} + + )} ); }