diff --git a/apps/miniprogram/src/assets/tabbar/message-active.png b/apps/miniprogram/src/assets/tabbar/message-active.png new file mode 100644 index 0000000..a6af92a Binary files /dev/null and b/apps/miniprogram/src/assets/tabbar/message-active.png differ diff --git a/apps/miniprogram/src/assets/tabbar/message.png b/apps/miniprogram/src/assets/tabbar/message.png new file mode 100644 index 0000000..165a592 Binary files /dev/null and b/apps/miniprogram/src/assets/tabbar/message.png differ diff --git a/apps/miniprogram/src/components/ProgressRing.scss b/apps/miniprogram/src/components/ProgressRing.scss new file mode 100644 index 0000000..ec702fb --- /dev/null +++ b/apps/miniprogram/src/components/ProgressRing.scss @@ -0,0 +1,29 @@ +@import '../styles/variables.scss'; + +.progress-ring { + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.progress-ring-inner { + background: $card; + border-radius: 50%; + display: flex; + align-items: baseline; + justify-content: center; +} + +.progress-ring-percent { + font-family: 'Georgia', 'Times New Roman', serif; + font-size: 22px; + font-weight: bold; + line-height: 1; +} + +.progress-ring-unit { + font-size: 12px; + font-weight: 600; + line-height: 1; +} diff --git a/apps/miniprogram/src/components/ProgressRing.tsx b/apps/miniprogram/src/components/ProgressRing.tsx new file mode 100644 index 0000000..24a095b --- /dev/null +++ b/apps/miniprogram/src/components/ProgressRing.tsx @@ -0,0 +1,40 @@ +import { View, Text } from '@tarojs/components'; +import './ProgressRing.scss'; + +interface ProgressRingProps { + percent: number; + size?: number; + strokeWidth?: number; + color?: string; + trackColor?: string; +} + +export default function ProgressRing({ + percent, + size = 72, + strokeWidth = 7, + color = '#C4623A', + trackColor = '#E8E2DC', +}: ProgressRingProps) { + const clamped = Math.max(0, Math.min(100, percent)); + const innerSize = size - strokeWidth * 2; + + return ( + + + + {clamped} + + + % + + + + ); +} diff --git a/apps/miniprogram/src/pages/health/index.scss b/apps/miniprogram/src/pages/health/index.scss index f9d7304..cf08e5f 100644 --- a/apps/miniprogram/src/pages/health/index.scss +++ b/apps/miniprogram/src/pages/health/index.scss @@ -9,9 +9,6 @@ /* ─── 页头 ─── */ .health-header { - display: flex; - align-items: center; - justify-content: space-between; padding: 24px 32px 8px; } @@ -22,248 +19,272 @@ color: $tx; } -.health-add-btn { - background: $pri; - padding: 10px 28px; +/* ─── 类型 Tab ─── */ +.vital-tabs { + display: flex; + padding: 12px 24px; + gap: 12px; +} + +.vital-tab { + flex: 1; + height: $tab-h; + border-radius: $r; + background: $surface-alt; + @include flex-center; + position: relative; + + &:active { + opacity: 0.85; + } + + &.vital-tab-active { + background: $pri; + + .vital-tab-text { + color: #fff; + } + } +} + +.vital-tab-text { + font-size: 26px; + font-weight: 600; + color: $tx2; +} + +.vital-tab-dot { + position: absolute; + top: 10px; + right: 10px; + width: 8px; + height: 8px; + border-radius: 50%; + background: $wrn; +} + +/* ─── 录入区 ─── */ +.input-section { + margin: 0 24px 24px; + background: $card; + border-radius: $r; + padding: 24px; + box-shadow: $shadow-sm; +} + +.input-group { + margin-bottom: 20px; +} + +.input-label { + font-size: 26px; + color: $tx; + font-weight: 600; + display: block; + margin-bottom: 12px; +} + +.input-field { + height: 56px; + background: $bg; + border: 2px solid $bd; border-radius: $r-sm; + padding: 0 20px; + font-family: 'Georgia', 'Times New Roman', serif; + font-size: 28px; + color: $tx; + width: 100%; + box-sizing: border-box; +} + +.input-ref { + font-size: 22px; + color: $tx2; + display: block; + margin-top: 12px; +} + +/* ─── 血糖时段选择 ─── */ +.period-group { + display: flex; + gap: 12px; + margin-top: 16px; +} + +.period-btn { + flex: 1; + height: $btn-primary-h; + border-radius: $r-sm; + background: $surface-alt; + @include flex-center; + + &.period-active { + background: $pri; + + .period-btn-text { + color: #fff; + } + } &:active { opacity: 0.85; } } -.health-add-text { +.period-btn-text { font-size: 26px; - color: #fff; font-weight: 600; + color: $tx2; } -/* ─── 快捷操作 ─── */ -.health-actions-row { - display: flex; - flex-wrap: wrap; - gap: 12px; - padding: 16px 24px 24px; +/* ─── 保存按钮 ─── */ +.save-btn { + @include btn-primary; + margin-top: 24px; + border-radius: $r; } -.action-item { - flex: 1; - min-width: 100px; +.save-btn-text { + font-size: 28px; + font-weight: 600; + color: #fff; +} + +/* ─── 趋势图 ─── */ +.trend-section { + margin: 0 24px 24px; +} + +.trend-empty { background: $card; border-radius: $r; - padding: 20px 12px; + padding: 36px; + text-align: center; +} + +.trend-empty-text { + font-size: 24px; + color: $tx2; +} + +.trend-chart { + background: $card; + border-radius: $r; + padding: 24px; + box-shadow: $shadow-sm; +} + +.trend-bars { + display: flex; + align-items: flex-end; + height: 200px; + gap: 8px; +} + +.trend-bar-col { + flex: 1; display: flex; flex-direction: column; align-items: center; - gap: 8px; - box-shadow: $shadow-sm; + height: 100%; + justify-content: flex-end; +} - &:active { - opacity: 0.7; +.trend-bar { + width: 100%; + max-width: 40px; + border-radius: 6px 6px 0 0; + min-height: 16px; + + &.trend-bar-normal { + background: $pri; + } + + &.trend-bar-warn { + background: $wrn; } } -.action-icon { - width: 72px; - height: 72px; - border-radius: 50%; - @include flex-center; - - &.icon-primary { background: $pri-l; } - &.icon-accent { background: $acc-l; } - &.icon-warn { background: $wrn-l; } +.trend-bar-label { + font-size: 22px; + color: $tx2; + margin-top: 8px; } -.action-char { - font-family: 'Georgia', 'Times New Roman', serif; - font-size: 32px; - font-weight: bold; - color: $pri; - - .icon-accent & { color: $acc; } - .icon-warn & { color: $wrn; } +/* ─── BLE 设备卡片 ─── */ +.device-section { + margin: 0 24px 16px; } -.action-label { - font-size: 24px; - color: $tx; - font-weight: 500; -} - -/* ─── 通用 section ─── */ -.health-section { - margin: 0 24px 28px; -} - -/* ─── 体征概览 ─── */ -.vitals-grid { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 16px; -} - -.vital-card { +.device-card { + display: flex; + align-items: center; background: $card; border-radius: $r; - padding: 24px 20px; + padding: 24px; box-shadow: $shadow-sm; - transition: opacity 0.2s; &:active { - opacity: 0.7; + opacity: 0.85; } } -.vital-label { +.device-icon { + width: 56px; + height: 56px; + border-radius: $r-sm; + background: $pri-l; + @include flex-center; + margin-right: 16px; + flex-shrink: 0; +} + +.device-icon-text { + font-size: 26px; + font-weight: bold; + color: $pri; +} + +.device-info { + flex: 1; + min-width: 0; +} + +.device-name { + font-size: 28px; + font-weight: 600; + color: $tx; + display: block; + margin-bottom: 4px; +} + +.device-desc { font-size: 22px; color: $tx2; display: block; - margin-bottom: 10px; } -.vital-value { - @include serif-number; - font-size: 44px; - font-weight: bold; - color: $tx; - display: block; - margin-bottom: 8px; - line-height: 1.1; -} - -.vital-bottom { - display: flex; - justify-content: space-between; - align-items: center; -} - -.vital-unit { - font-size: 20px; +.device-arrow { + font-size: 32px; color: $tx3; + flex-shrink: 0; } -.vital-tag { - @include tag($acc-l, $acc); - - &.tag-warn { - @include tag($wrn-l, $wrn); - } -} - -.vital-ref { - font-size: 20px; - color: $tx3; - margin-top: 8px; - 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; } - &.bar-red { background: $dan; } -} - -/* ─── 趋势入口 — 水平滚动卡片 ─── */ -.trend-scroll { - white-space: nowrap; - width: 100%; -} - -.trend-card { - display: inline-flex; - flex-direction: column; - align-items: center; - width: 200px; +/* ─── 健康资讯入口 ─── */ +.article-entry { + margin: 0 24px 24px; background: $card; border-radius: $r; - padding: 24px 16px; - margin-right: 16px; + padding: 24px; box-shadow: $shadow-sm; - vertical-align: top; &:active { - opacity: 0.7; + opacity: 0.85; } } -.trend-card-icon { - width: 64px; - height: 64px; - border-radius: $r; - background: $pri-l; - @include flex-center; - margin-bottom: 12px; -} - -.trend-card-char { - font-family: 'Georgia', 'Times New Roman', serif; - font-size: 28px; - font-weight: bold; - color: $pri; -} - -.trend-card-label { +.article-entry-text { font-size: 26px; - color: $tx; - font-weight: 500; - white-space: nowrap; -} - -.trend-card-arrow { - font-size: 22px; - color: $tx3; - margin-top: 8px; -} - -/* ─── 最近监测 ─── */ -.record-card { - background: $card; - border-radius: $r; - padding: 20px 24px; - margin-bottom: 12px; - box-shadow: $shadow-sm; -} - -.record-date { - font-size: 24px; color: $pri; - font-weight: 600; - display: block; - margin-bottom: 12px; -} - -.record-data { - display: flex; - gap: 32px; - flex-wrap: wrap; -} - -.record-item { - display: flex; - flex-direction: column; - gap: 4px; -} - -.record-item-label { - font-size: 22px; - color: $tx3; -} - -.record-item-value { - @include serif-number; - font-size: 26px; - color: $tx; font-weight: 500; } diff --git a/apps/miniprogram/src/pages/health/index.tsx b/apps/miniprogram/src/pages/health/index.tsx index 5ae2e0e..ab974cb 100644 --- a/apps/miniprogram/src/pages/health/index.tsx +++ b/apps/miniprogram/src/pages/health/index.tsx @@ -1,235 +1,330 @@ import { useState } from 'react'; -import { View, Text, ScrollView } from '@tarojs/components'; +import { View, Text, Input } from '@tarojs/components'; import Taro, { useDidShow } from '@tarojs/taro'; import { useHealthStore } from '../../stores/health'; -import { listDailyMonitoring, DailyMonitoring } from '../../services/health'; -import { usePointsStore } from '../../stores/points'; import { useAuthStore } from '../../stores/auth'; -import { trackEvent } from '../../services/analytics'; +import { inputVitalSign, getTrend } from '../../services/health'; import Loading from '../../components/Loading'; import './index.scss'; -const QUICK_ACTIONS = [ - { label: '日常上报', char: '日', bg: 'icon-primary' }, - { label: '体征录入', char: '录', bg: 'icon-accent' }, - { label: '查看趋势', char: '势', bg: 'icon-warn' }, +type VitalType = 'blood_pressure' | 'heart_rate' | 'blood_sugar' | 'weight'; + +const VITAL_TABS: { key: VitalType; label: string }[] = [ + { key: 'blood_pressure', label: '血压' }, + { key: 'heart_rate', label: '心率' }, + { key: 'blood_sugar', label: '血糖' }, + { key: 'weight', label: '体重' }, ]; -const TREND_LINKS = [ - { label: '血压趋势', indicator: 'blood_pressure_systolic', char: '压' }, - { label: '心率趋势', indicator: 'heart_rate', char: '率' }, - { label: '血糖趋势', indicator: 'blood_sugar_fasting', char: '糖' }, -]; +const REF_RANGES: Record = { + blood_pressure: { range: '收缩压 90-140 / 舒张压 60-90 mmHg', warn: '血压偏高,确认提交?' }, + heart_rate: { range: '60-100 bpm', warn: '心率异常,确认提交?' }, + blood_sugar: { range: '空腹 3.9-6.1 / 餐后 <7.8 mmol/L', warn: '血糖偏高,确认提交?' }, + weight: { range: '根据 BMI 18.5-24 计算', warn: '' }, +}; -function getStatusTag(status?: string) { - if (status === 'high') return { label: '偏高', cls: 'tag-warn' }; - if (status === 'low') return { label: '偏低', cls: 'tag-warn' }; - if (status === 'normal') return { label: '正常', cls: 'tag-ok' }; - 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)); +interface TrendPoint { + date: string; + value: number; } export default function Health() { - const { todaySummary, loading, refreshToday } = useHealthStore(); - const { checkinStatus, refresh: refreshPoints } = usePointsStore(); + const { todaySummary, loading, refreshToday, getTrend: fetchTrend } = useHealthStore(); const { currentPatient } = useAuthStore(); - const [recentRecords, setRecentRecords] = useState([]); + const [activeTab, setActiveTab] = useState('blood_pressure'); + const [systolic, setSystolic] = useState(''); + const [diastolic, setDiastolic] = useState(''); + const [heartRateVal, setHeartRateVal] = useState(''); + const [sugarVal, setSugarVal] = useState(''); + const [sugarPeriod, setSugarPeriod] = useState<'fasting' | 'postprandial'>('fasting'); + const [weightVal, setWeightVal] = useState(''); + const [saving, setSaving] = useState(false); + const [trendData, setTrendData] = useState([]); + const [trendLoading, setTrendLoading] = useState(false); useDidShow(() => { refreshToday(); - refreshPoints(); - loadRecentRecords(); + loadTrend(activeTab); }); - const loadRecentRecords = async () => { - if (currentPatient) { - try { - const resp = await listDailyMonitoring(currentPatient.id, { page: 1, page_size: 3 }); - setRecentRecords(resp.data || []); - } catch { - // daily monitoring API 可能不可用 + const loadTrend = async (type: VitalType) => { + setTrendLoading(true); + try { + const indicatorMap: Record = { + blood_pressure: 'blood_pressure_systolic', + heart_rate: 'heart_rate', + blood_sugar: 'blood_sugar_fasting', + weight: 'weight', + }; + const points = await fetchTrend(indicatorMap[type], '7d'); + setTrendData(points); + } catch { + setTrendData([]); + } finally { + setTrendLoading(false); + } + }; + + const handleTabChange = (tab: VitalType) => { + setActiveTab(tab); + loadTrend(tab); + }; + + const getWarnStatus = (type: VitalType): string | null => { + if (type === 'blood_pressure') { + const sys = parseFloat(systolic); + const dia = parseFloat(diastolic); + if (sys > 140 || dia > 90) return REF_RANGES.blood_pressure.warn; + } else if (type === 'heart_rate') { + const val = parseFloat(heartRateVal); + if (val > 100 || val < 60) return REF_RANGES.heart_rate.warn; + } else if (type === 'blood_sugar') { + const val = parseFloat(sugarVal); + if (sugarPeriod === 'fasting' && val > 6.1) return REF_RANGES.blood_sugar.warn; + if (sugarPeriod === 'postprandial' && val > 7.8) return REF_RANGES.blood_sugar.warn; + } + return null; + }; + + const handleSave = async () => { + const patientId = currentPatient?.id; + if (!patientId) { + Taro.showToast({ title: '请先登录', icon: 'none' }); + return; + } + + const warnMsg = getWarnStatus(activeTab); + if (warnMsg) { + const { confirm } = await Taro.showModal({ + title: '异常提示', + content: warnMsg, + confirmText: '确认提交', + cancelText: '再看看', + }); + if (!confirm) return; + } + + setSaving(true); + try { + switch (activeTab) { + case 'blood_pressure': { + const sys = parseFloat(systolic); + const dia = parseFloat(diastolic); + if (!sys || !dia) { Taro.showToast({ title: '请填写完整', icon: 'none' }); return; } + await inputVitalSign(patientId, { + indicator_type: 'blood_pressure', + value: sys, + extra: { systolic: sys, diastolic: dia }, + }); + setSystolic(''); + setDiastolic(''); + break; + } + case 'heart_rate': { + const val = parseFloat(heartRateVal); + if (!val) { Taro.showToast({ title: '请填写心率', icon: 'none' }); return; } + await inputVitalSign(patientId, { indicator_type: 'heart_rate', value: val }); + setHeartRateVal(''); + break; + } + case 'blood_sugar': { + const val = parseFloat(sugarVal); + if (!val) { Taro.showToast({ title: '请填写血糖值', icon: 'none' }); return; } + await inputVitalSign(patientId, { indicator_type: 'blood_sugar', value: val }); + setSugarVal(''); + break; + } + case 'weight': { + const val = parseFloat(weightVal); + if (!val) { Taro.showToast({ title: '请填写体重', icon: 'none' }); return; } + await inputVitalSign(patientId, { indicator_type: 'weight', value: val }); + setWeightVal(''); + break; + } } + Taro.showToast({ title: '保存成功', icon: 'success' }); + refreshToday(true); + loadTrend(activeTab); + } catch { + Taro.showToast({ title: '保存失败', icon: 'none' }); + } finally { + setSaving(false); } }; - const goToInput = () => { - Taro.navigateTo({ url: '/pages/pkg-health/input/index' }); - }; - - const goToDailyMonitoring = () => { - Taro.navigateTo({ url: '/pages/pkg-health/daily-monitoring/index' }); - }; - - const goToTrend = (indicator: string) => { - Taro.navigateTo({ url: `/pages/pkg-health/trend/index?indicator=${indicator}` }); - }; - - const goToMall = () => { - Taro.switchTab({ url: '/pages/mall/index' }); - }; - - 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, 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 = [ - { ...QUICK_ACTIONS[0], action: goToDailyMonitoring }, - { ...QUICK_ACTIONS[1], action: goToInput }, - { ...QUICK_ACTIONS[2], action: () => goToTrend('blood_pressure_systolic') }, - ]; - - const trendLinks = TREND_LINKS; - - const formatBp = (record: DailyMonitoring) => { - const parts: string[] = []; - if (record.morning_bp_systolic && record.morning_bp_diastolic) { - parts.push(`晨 ${record.morning_bp_systolic}/${record.morning_bp_diastolic}`); - } - if (record.evening_bp_systolic && record.evening_bp_diastolic) { - parts.push(`晚 ${record.evening_bp_systolic}/${record.evening_bp_diastolic}`); - } - return parts.length > 0 ? parts.join(' ') : '--'; - }; + const maxTrendValue = Math.max(...trendData.map((d) => d.value), 1); + const dayLabels = ['日', '一', '二', '三', '四', '五', '六']; return ( {/* 页头 */} - 健康数据 - - 录入 + 健康 + + + {/* 类型 Tab */} + + {VITAL_TABS.map((tab) => { + const hasData = tab.key === 'blood_pressure' ? !!todaySummary?.blood_pressure + : tab.key === 'heart_rate' ? !!todaySummary?.heart_rate + : tab.key === 'blood_sugar' ? !!todaySummary?.blood_sugar + : !!todaySummary?.weight; + return ( + handleTabChange(tab.key)} + > + {tab.label} + {!hasData && } + + ); + })} + + + {/* 录入区 */} + + {activeTab === 'blood_pressure' && ( + + 收缩压(高压) + setSystolic(e.detail.value)} + /> + 舒张压(低压) + setDiastolic(e.detail.value)} + /> + {REF_RANGES.blood_pressure.range} + + )} + + {activeTab === 'heart_rate' && ( + + 心率 + setHeartRateVal(e.detail.value)} + /> + {REF_RANGES.heart_rate.range} + + )} + + {activeTab === 'blood_sugar' && ( + + 血糖值 + setSugarVal(e.detail.value)} + /> + + setSugarPeriod('fasting')} + > + 空腹 + + setSugarPeriod('postprandial')} + > + 餐后 2h + + + {REF_RANGES.blood_sugar.range} + + )} + + {activeTab === 'weight' && ( + + 体重 (kg) + setWeightVal(e.detail.value)} + /> + {REF_RANGES.weight.range} + + )} + + + {saving ? '保存中...' : '保存'} - {/* 快捷操作 + 打卡状态紧凑合并 */} - - {quickActions.map((a) => ( - - - {a.char} - - {a.label} - - ))} - {checkinStatus && ( - - - - - - {checkinStatus.checked_in_today - ? (checkinStatus.consecutive_days > 0 ? `已打卡${checkinStatus.consecutive_days}天` : '已打卡') - : '去打卡'} - - - )} - - - {/* 今日体征概览 */} - - 今日体征 - {loading && !todaySummary ? ( + {/* 趋势图 */} + + 近 7 天趋势 + {trendLoading ? ( + ) : trendData.length === 0 ? ( + + 暂无趋势数据 + ) : ( - - {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} - {item.value} - - {item.unit} - {tag && {tag.label}} + + + {trendData.map((point, i) => { + const heightPct = Math.max(8, (point.value / maxTrendValue) * 100); + const isAbnormal = activeTab === 'blood_pressure' ? point.value > 140 + : activeTab === 'heart_rate' ? (point.value > 100 || point.value < 60) + : activeTab === 'blood_sugar' ? point.value > 6.1 + : false; + const dayOfWeek = new Date(point.date).getDay(); + return ( + + + {dayLabels[dayOfWeek]} - {/* Sparkline bar */} - {item.ref && item.numValue != null && ( - - - - )} - {item.ref && 参考 {item.ref}} - - ); - })} + ); + })} + )} - {/* 趋势快捷入口 — 水平滚动卡片 */} - - 健康趋势 - - {trendLinks.map((t) => ( - goToTrend(t.indicator)}> - - {t.char} - - {t.label} - 查看 › - - ))} - + {/* BLE 设备卡片 */} + + Taro.navigateTo({ url: '/pages/device-sync/index' })} + > + + + + + 蓝牙设备 + 连接设备自动同步数据 + + + - {/* 最近监测记录 */} - {recentRecords.length > 0 && ( - - 最近监测 - {recentRecords.map((record) => ( - - {record.record_date} - - - 血压 - {formatBp(record)} - - {record.weight != null && ( - - 体重 - {record.weight} kg - - )} - {record.blood_sugar != null && ( - - 血糖 - {record.blood_sugar} mmol/L - - )} - - - ))} - - )} + {/* 健康资讯入口 */} + Taro.navigateTo({ url: '/pages/article/index' })} + > + 最新健康资讯 › + ); } diff --git a/apps/miniprogram/src/pages/index/index.scss b/apps/miniprogram/src/pages/index/index.scss index f6d7d32..22e0d91 100644 --- a/apps/miniprogram/src/pages/index/index.scss +++ b/apps/miniprogram/src/pages/index/index.scss @@ -7,101 +7,160 @@ padding-bottom: calc(120px + env(safe-area-inset-bottom)); } -/* ─── 问候区 ─── */ +/* ─── 区域 1:问候 + 日期 + 消息 ─── */ .greeting-section { background: linear-gradient(135deg, $pri 0%, $pri-d 100%); - padding: 48px 32px 72px; + padding: 48px 32px 60px; color: #fff; display: flex; justify-content: space-between; - align-items: flex-start; + align-items: center; } .greeting-left { - display: flex; - flex-direction: column; + flex: 1; } -.greeting-time { - font-size: 26px; - opacity: 0.85; - margin-bottom: 4px; -} - -.greeting-name { - font-family: 'Georgia', 'Times New Roman', serif; - font-size: 44px; +.greeting-text { + font-size: 28px; font-weight: bold; + display: block; + margin-bottom: 6px; } .greeting-date { - font-size: 24px; - opacity: 0.7; - margin-top: 8px; + font-size: 22px; + opacity: 0.75; } -/* ─── 今日健康 ─── */ -.health-section { +.greeting-msg { + width: 56px; + height: 56px; + border-radius: 50%; + background: rgba(255, 255, 255, 0.2); + @include flex-center; + + &:active { + background: rgba(255, 255, 255, 0.1); + } +} + +.greeting-msg-icon { + font-size: 22px; + color: #fff; + font-weight: 600; +} + +/* ─── 区域 2:今日体征完成度 ─── */ +.checkin-card { background: $card; border-radius: $r; box-shadow: $shadow-md; - margin: -36px 24px 24px; - padding: 28px; + margin: -28px 24px 24px; + padding: 24px; + display: flex; + align-items: center; + gap: 24px; + + &:active { + opacity: 0.9; + } } -.section-title { - @include section-title; +.checkin-left { + flex-shrink: 0; } -.health-grid { +.checkin-right { + flex: 1; + min-width: 0; +} + +.checkin-title { + font-size: 26px; + font-weight: 600; + color: $tx; + display: block; + margin-bottom: 12px; +} + +.checkin-capsules { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.capsule { + font-size: 22px; + padding: 4px 12px; + border-radius: $r-pill; + font-weight: 500; + + &.capsule-done { + background: $acc-l; + color: $acc; + } + + &.capsule-pending { + background: $surface-alt; + color: $tx2; + } +} + +/* ─── 区域 3:今日体征 2x2 ─── */ +.vitals-section { + margin: 0 24px 24px; +} + +.vitals-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; } -.health-cell { - background: $bg; - border-radius: $r-sm; - padding: 20px 16px; +.vital-card { + background: $card; + border-radius: $r; + padding: 20px; + box-shadow: $shadow-sm; text-align: center; - transition: opacity 0.2s; &:active { opacity: 0.7; } } -.health-cell-label { - font-size: 22px; +.vital-label { + font-size: 24px; color: $tx2; display: block; margin-bottom: 8px; } -.health-cell-value { +.vital-value { @include serif-number; - font-size: 44px; + font-size: 48px; font-weight: bold; color: $tx; display: block; line-height: 1.1; + margin-bottom: 8px; } -.health-cell-bottom { +.vital-bottom { display: flex; justify-content: center; align-items: center; gap: 8px; - margin-top: 8px; } -.health-cell-unit { - font-size: 20px; - color: $tx3; +.vital-unit { + font-size: 22px; + color: $tx2; } -.health-cell-tag { - font-size: 18px; +.vital-tag { + font-size: 22px; font-weight: 500; padding: 2px 10px; border-radius: $r-sm; @@ -116,89 +175,42 @@ background: $wrn-l; color: $wrn; } -} -/* ─── 快捷服务 ─── */ -.services-section { - margin: 0 24px 24px; -} - -.services-row { - display: flex; - justify-content: space-between; - gap: 8px; -} - -.service-btn { - display: flex; - flex-direction: column; - align-items: center; - gap: 8px; - flex: 1; - - &:active { - opacity: 0.7; + &.tag-empty { + background: $surface-alt; + color: $tx2; } } -.service-icon-wrap { - width: 88px; - height: 88px; - border-radius: $r; - background: $pri-l; - @include flex-center; +/* ─── 区域 4:今日待办 ─── */ +.todo-section { + margin: 0 24px 24px; } -.service-icon-text { - font-family: 'Georgia', 'Times New Roman', serif; - font-size: 32px; - font-weight: bold; - color: $pri; -} - -.service-label { - font-size: 22px; - color: $tx2; - text-align: center; -} - -/* ─── 待办事项 ─── */ -.upcoming-section { - margin: 0 24px; -} - -.upcoming-empty { +.todo-empty { background: $card; border-radius: $r; - padding: 48px 24px; + padding: 36px 24px; text-align: center; box-shadow: $shadow-sm; } -.upcoming-empty-text { - display: block; - font-size: 28px; - color: $tx2; - margin-bottom: 8px; -} - -.upcoming-empty-hint { - display: block; +.todo-empty-text { font-size: 24px; - color: $tx3; + color: $tx2; } -.upcoming-list { +.todo-list { background: $card; border-radius: $r; overflow: hidden; box-shadow: $shadow-sm; } -.upcoming-item { +.todo-item { display: flex; align-items: center; - padding: 24px 24px; + padding: 24px; border-bottom: 1px solid $bd-l; &:last-child { @@ -210,20 +222,36 @@ } } -.upcoming-item-main { +.todo-icon-wrap { + width: 48px; + height: 48px; + border-radius: $r-sm; + background: $pri-l; + @include flex-center; + margin-right: 16px; + flex-shrink: 0; +} + +.todo-icon-char { + font-size: 24px; + font-weight: bold; + color: $pri; +} + +.todo-info { flex: 1; min-width: 0; } -.upcoming-item-title { +.todo-title { font-size: 28px; color: $tx; + font-weight: 600; display: block; margin-bottom: 4px; - font-weight: 500; } -.upcoming-item-sub { +.todo-sub { font-size: 22px; color: $tx2; display: block; @@ -232,158 +260,44 @@ white-space: nowrap; } -.upcoming-item-tag { - font-size: 20px; - font-weight: 500; - padding: 4px 14px; - border-radius: $r-sm; - flex-shrink: 0; - margin-right: 12px; - - &.tag-ok { - background: $acc-l; - color: $acc; - } - - &.tag-warn { - background: $wrn-l; - color: $wrn; - } - - &.tag-default { - background: $bd-l; - color: $tx2; - } -} - -.upcoming-item-arrow { +.todo-arrow { font-size: 32px; 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 { +/* ─── 区域 5:快捷操作 ─── */ +.action-section { 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 { + gap: 16px; 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; -} - -/* ─── 设备快捷入口 ─── */ -.device-section { - margin: 0 24px 24px; -} - -.device-entry { - display: flex; - align-items: center; - background: $card; - border-radius: $r; - padding: 20px 24px; - margin-bottom: 12px; - box-shadow: $shadow-sm; - - &:active { - opacity: 0.7; - } -} - -.device-entry-icon-wrap { - width: 64px; - height: 64px; - border-radius: $r; - background: $pri-l; - display: flex; - align-items: center; - justify-content: center; - margin-right: 20px; - flex-shrink: 0; -} - -.device-entry-icon-text { - font-size: 32px; -} - -.device-entry-info { +.action-btn { flex: 1; - min-width: 0; -} - -.device-entry-name { + @include touch-target; + height: $btn-primary-h; + border-radius: $r; font-size: 28px; font-weight: 600; - color: $tx; - display: block; - margin-bottom: 4px; + + &:active { + opacity: 0.85; + } } -.device-entry-desc { - font-size: 22px; - color: $tx3; - display: block; +.action-primary { + background: $pri; + color: #fff; } -.device-entry-arrow { - font-size: 32px; - color: $tx3; - flex-shrink: 0; +.action-outline { + background: transparent; + color: $pri; + border: 2px solid $pri; +} + +.action-btn-text { + font-size: 28px; + font-weight: 600; } diff --git a/apps/miniprogram/src/pages/index/index.tsx b/apps/miniprogram/src/pages/index/index.tsx index 8944e85..e8c5531 100644 --- a/apps/miniprogram/src/pages/index/index.tsx +++ b/apps/miniprogram/src/pages/index/index.tsx @@ -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([]); const [upcomingLoading, setUpcomingLoading] = useState(false); - const [articles, setArticles] = useState([]); 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 ( - {/* 问候区 */} + {/* 区域 1:问候 + 日期 + 消息入口 */} - {greeting} - {displayName} + {greeting},{displayName} + + {new Date().toLocaleDateString('zh-CN', { month: 'long', day: 'numeric', weekday: 'short' })} + - {new Date().toLocaleDateString('zh-CN', { month: 'long', day: 'numeric', weekday: 'short' })} - - - {/* 设备快捷入口 — 点击直接跳转设备同步页面 */} - - Taro.navigateTo({ url: '/pages/device-sync/index' })}> - - {'\u{1FA7A}'} - - - 血压计 - 蓝牙同步 · 自动采集 - - {'›'} - - Taro.navigateTo({ url: '/pages/device-sync/index' })}> - - {'\u{1FA78}'} - - - 血糖仪 - 蓝牙同步 · 自动采集 - - {'›'} + Taro.switchTab({ url: '/pages/messages/index' })} + > + - {/* 今日健康 */} - - 今日健康 + {/* 区域 2:今日体征完成度 */} + Taro.switchTab({ url: '/pages/health/index' })} + > + + + + + + {completedCount === 4 ? '今日体征已全部记录' : completedCount === 0 ? '今日尚未记录体征' : `今日已记录 ${completedCount}/4 项`} + + + {indicatorCapsules.map((cap) => ( + + {cap.done ? '✓' : ''}{cap.label} + + ))} + + + + + {/* 区域 3:今日体征 2x2 网格 */} + {loading && !todaySummary ? ( - ) : !todaySummary || (!todaySummary.blood_pressure && !todaySummary.heart_rate && !todaySummary.blood_sugar && !todaySummary.weight) ? ( - - 今天还没录入数据 - - Taro.navigateTo({ url: '/pages/pkg-health/input/index' })}> - 点击开始记录 - - - ) : ( - + {healthItems.map((item) => { const tag = getStatusTag(item.status); return ( - Taro.navigateTo({ url: `/pages/pkg-health/trend/index?indicator=${item.label === '血压' ? 'blood_pressure_systolic' : item.label === '心率' ? 'heart_rate' : item.label === '血糖' ? 'blood_sugar_fasting' : 'weight'}` })}> - {item.label} - {item.value} - - {item.unit} - {tag && {tag.label}} + Taro.navigateTo({ url: `/pages/pkg-health/trend/index?indicator=${item.indicator}` })} + > + {item.label} + {item.value} + + {item.unit} + {tag && {tag.label}} + {!item.status && 未记录} ); @@ -180,37 +178,21 @@ export default function Index() { )} - {/* 快捷服务 */} - - 快捷服务 - - {QUICK_SERVICES.map((svc) => ( - Taro.navigateTo({ url: svc.path })}> - - {svc.char} - - {svc.label} - - ))} - - - - {/* 待办事项 */} - - 待办事项 + {/* 区域 4:今日待办(≤3 条) */} + + 今日待办 {upcomingLoading ? ( ) : upcomingItems.length === 0 ? ( - - 暂无待办事项 - 预约挂号后将在此显示 + + 今天没有待办事项 ) : ( - + {upcomingItems.map((item) => ( { if (item.type === 'appointment') { Taro.navigateTo({ url: '/pages/appointment/index' }); @@ -219,36 +201,35 @@ export default function Index() { } }} > - - {item.title} - {item.subtitle} + + {item.icon} - {item.statusLabel} - + + {item.title} + {item.subtitle} + + ))} )} - {/* 健康资讯 */} - {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)}` : ''} - - - ))} + {/* 区域 5:快捷操作 */} + + Taro.switchTab({ url: '/pages/health/index' })} + > + 记录体征 - )} + Taro.navigateTo({ url: '/pages/appointment/create/index' })} + > + 预约挂号 + + ); } diff --git a/apps/miniprogram/src/pages/messages/index.scss b/apps/miniprogram/src/pages/messages/index.scss new file mode 100644 index 0000000..d093977 --- /dev/null +++ b/apps/miniprogram/src/pages/messages/index.scss @@ -0,0 +1,194 @@ +@import '../../styles/variables.scss'; +@import '../../styles/mixins.scss'; + +.messages-page { + min-height: 100vh; + background: $bg; + padding-bottom: calc(120px + env(safe-area-inset-bottom)); +} + +/* ─── 页头 ─── */ +.messages-header { + padding: 24px 32px 8px; +} + +.messages-title { + font-family: 'Georgia', 'Times New Roman', serif; + font-size: 36px; + font-weight: bold; + color: $tx; +} + +/* ─── Tab 切换 ─── */ +.msg-tabs { + display: flex; + padding: 16px 24px 0; + gap: 0; +} + +.msg-tab { + flex: 1; + height: $tab-h; + @include flex-center; + + &:active { + opacity: 0.85; + } +} + +.msg-tab-text { + font-size: 28px; + font-weight: 600; + color: $tx2; +} + +.msg-tab-active .msg-tab-text { + color: $pri; +} + +.msg-tab-indicator { + padding: 0 24px; + height: 3px; + background: $bd-l; + margin-bottom: 16px; +} + +.msg-tab-bar { + width: 50%; + height: 3px; + background: $pri; + border-radius: 2px; + transition: transform 0.2s; + + &.msg-tab-bar-right { + transform: translateX(100%); + } +} + +/* ─── 内容区 ─── */ +.msg-content { + padding: 0 24px; +} + +.msg-empty { + background: $card; + border-radius: $r; + padding: 48px 24px; + text-align: center; + box-shadow: $shadow-sm; +} + +.msg-empty-text { + font-size: 26px; + color: $tx2; +} + +/* ─── 咨询卡片 ─── */ +.consult-card { + display: flex; + align-items: center; + background: $card; + border-radius: $r; + padding: 24px; + margin-bottom: 12px; + box-shadow: $shadow-sm; + + &:active { + opacity: 0.85; + } +} + +.consult-info { + flex: 1; + min-width: 0; +} + +.consult-doctor { + font-size: 28px; + font-weight: 600; + color: $tx; + display: block; + margin-bottom: 6px; +} + +.consult-preview { + font-size: 24px; + color: $tx2; + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.consult-meta { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 8px; + flex-shrink: 0; + margin-left: 16px; +} + +.consult-time { + font-size: 22px; + color: $tx2; +} + +.consult-badge { + min-width: 24px; + height: 24px; + border-radius: 12px; + background: $dan; + @include flex-center; + padding: 0 6px; +} + +.consult-badge-text { + font-size: 18px; + color: #fff; + font-weight: 600; +} + +/* ─── 通知卡片 ─── */ +.notify-card { + display: flex; + align-items: center; + background: $card; + border-radius: $r; + padding: 24px; + margin-bottom: 12px; + box-shadow: $shadow-sm; + + &:active { + opacity: 0.85; + } +} + +.notify-info { + flex: 1; + min-width: 0; +} + +.notify-title { + font-size: 28px; + font-weight: 600; + color: $tx; + display: block; + margin-bottom: 6px; +} + +.notify-desc { + font-size: 24px; + color: $tx2; + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.notify-time { + font-size: 22px; + color: $tx2; + flex-shrink: 0; + margin-left: 16px; +} diff --git a/apps/miniprogram/src/pages/messages/index.tsx b/apps/miniprogram/src/pages/messages/index.tsx new file mode 100644 index 0000000..adb5126 --- /dev/null +++ b/apps/miniprogram/src/pages/messages/index.tsx @@ -0,0 +1,154 @@ +import { useState } from 'react'; +import { View, Text } from '@tarojs/components'; +import Taro, { useDidShow } from '@tarojs/taro'; +import { listConsultations, ConsultationSession } from '../../services/consultation'; +import Loading from '../../components/Loading'; +import './index.scss'; + +type MsgTab = 'consultation' | 'notification'; + +interface NotificationItem { + id: string; + title: string; + desc: string; + time: string; + type: string; +} + +export default function Messages() { + const [activeTab, setActiveTab] = useState('consultation'); + const [sessions, setSessions] = useState([]); + const [notifications, setNotifications] = useState([]); + const [loading, setLoading] = useState(false); + + useDidShow(() => { + loadData(activeTab); + }); + + const loadData = async (tab: MsgTab) => { + setLoading(true); + try { + if (tab === 'consultation') { + const res = await listConsultations({ page: 1, page_size: 20 }); + setSessions(res.data || []); + } else { + // 通知目前从咨询中提取状态变化作为示例 + // 后续可对接独立通知 API + setNotifications([]); + } + } catch { + if (tab === 'consultation') setSessions([]); + else setNotifications([]); + } finally { + setLoading(false); + } + }; + + const handleTabChange = (tab: MsgTab) => { + setActiveTab(tab); + loadData(tab); + }; + + const formatTime = (dateStr: string | null) => { + if (!dateStr) return ''; + const d = new Date(dateStr); + const now = new Date(); + const diffMs = now.getTime() - d.getTime(); + const diffMin = Math.floor(diffMs / 60000); + if (diffMin < 60) return `${diffMin} 分钟前`; + const diffHour = Math.floor(diffMin / 60); + if (diffHour < 24) return `${diffHour} 小时前`; + return dateStr.slice(0, 10); + }; + + return ( + + {/* 页头 */} + + 消息 + + + {/* Tab 切换 */} + + handleTabChange('consultation')} + > + 咨询 + + handleTabChange('notification')} + > + 通知 + + + + + + + {/* 咨询列表 */} + {activeTab === 'consultation' && ( + + {loading ? ( + + ) : sessions.length === 0 ? ( + + 暂无咨询消息 + + ) : ( + sessions.map((session) => ( + Taro.navigateTo({ url: `/pages/consultation/detail/index?id=${session.id}` })} + > + + + {session.consultation_type === 'online' ? '在线咨询' : '门诊咨询'} + + + {session.last_message || session.subject || '暂无消息'} + + + + {formatTime(session.last_message_at)} + {session.unread_count_patient > 0 && ( + + + {session.unread_count_patient > 99 ? '99+' : session.unread_count_patient} + + + )} + + + )) + )} + + )} + + {/* 通知列表 */} + {activeTab === 'notification' && ( + + {loading ? ( + + ) : notifications.length === 0 ? ( + + 暂无新通知 + + ) : ( + notifications.map((n) => ( + + + {n.title} + {n.desc} + + {n.time} + + )) + )} + + )} + + ); +} diff --git a/apps/miniprogram/src/pages/pkg-profile/diagnoses/index.scss b/apps/miniprogram/src/pages/pkg-profile/diagnoses/index.scss index 3ed4178..dcdb817 100644 --- a/apps/miniprogram/src/pages/pkg-profile/diagnoses/index.scss +++ b/apps/miniprogram/src/pages/pkg-profile/diagnoses/index.scss @@ -63,7 +63,7 @@ } &.resolved { - @include tag($suc-l, $suc); + @include tag($acc-l, $acc); } &.chronic { diff --git a/apps/miniprogram/src/pages/profile/index.scss b/apps/miniprogram/src/pages/profile/index.scss index ee6a2c4..0b1694a 100644 --- a/apps/miniprogram/src/pages/profile/index.scss +++ b/apps/miniprogram/src/pages/profile/index.scss @@ -46,7 +46,7 @@ color: rgba(255, 255, 255, 0.75); } -/* ─── 积分统计 ─── */ +/* ─── 积分 + 打卡横排 ─── */ .profile-stats { display: flex; align-items: center; @@ -63,7 +63,7 @@ } } -.stat-item { +.stat-card { flex: 1; display: flex; flex-direction: column; @@ -72,12 +72,20 @@ .stat-value { @include serif-number; - font-size: 36px; + font-size: 28px; font-weight: bold; color: #fff; margin-bottom: 4px; } +.stat-value-pri { + color: #FFD6C7; +} + +.stat-value-acc { + color: #C8E6C9; +} + .stat-label { font-size: 22px; color: rgba(255, 255, 255, 0.7); @@ -103,6 +111,8 @@ display: flex; align-items: center; padding: 28px 24px; + min-height: $menu-item-h; + box-sizing: border-box; border-bottom: 1px solid $bd-l; &:last-child { @@ -120,7 +130,7 @@ border-radius: $r-sm; background: $pri-l; @include flex-center; - margin-right: 16px; + margin-right: 20px; flex-shrink: 0; } @@ -134,7 +144,7 @@ .menu-label { flex: 1; - font-size: 30px; + font-size: 28px; color: $tx; } @@ -159,6 +169,6 @@ } .logout-text { - font-size: 30px; + font-size: 28px; color: $dan; } diff --git a/apps/miniprogram/src/styles/mixins.scss b/apps/miniprogram/src/styles/mixins.scss index 1955d90..dfdd2f2 100644 --- a/apps/miniprogram/src/styles/mixins.scss +++ b/apps/miniprogram/src/styles/mixins.scss @@ -4,8 +4,8 @@ background: $card; border-radius: $r; box-shadow: $shadow-md; - padding: 24px; - margin: 0 24px 20px; + padding: 28px; + margin: 0 24px 24px; } @mixin flex-center { @@ -26,7 +26,7 @@ @mixin section-title { font-family: 'Georgia', 'Times New Roman', serif; - font-size: 30px; + font-size: 26px; font-weight: bold; color: $tx; margin-bottom: 20px; @@ -37,8 +37,48 @@ display: inline-block; padding: 4px 12px; border-radius: $r-sm; - font-size: 20px; + font-size: 22px; font-weight: 500; background: $bg; color: $color; } + +@mixin touch-target { + min-height: $touch-min; + min-width: $touch-min; + display: flex; + align-items: center; + justify-content: center; +} + +@mixin btn-primary { + height: $btn-primary-h; + border-radius: $r; + background: $pri; + color: #FFFFFF; + font-size: 28px; + font-weight: 600; + border: none; + width: 100%; + @include touch-target; + + &:active { + opacity: 0.85; + } +} + +@mixin btn-outline { + height: $btn-primary-h; + border-radius: $r; + background: transparent; + color: $pri; + font-size: 28px; + font-weight: 600; + border: 2px solid $pri; + width: 100%; + @include touch-target; + + &:active { + background: $pri-l; + } +} diff --git a/apps/miniprogram/src/styles/variables.scss b/apps/miniprogram/src/styles/variables.scss index 4f5ac72..9df28b9 100644 --- a/apps/miniprogram/src/styles/variables.scss +++ b/apps/miniprogram/src/styles/variables.scss @@ -12,8 +12,8 @@ $bg: #F5F0EB; // 主背景 (warm cream) $card: #FFFFFF; // 卡片白 $surface-alt: #EDE8E2; // 辅助底 $tx: #2D2A26; // 主文字 (warm black) -$tx2: #7A756E; // 次文字 (warm gray) -$tx3: #A8A29E; // 淡文字 +$tx2: #5A554F; // 次文字 (warm gray) — AA 正文对比度 ~5.5:1 +$tx3: #78716C; // 淡文字 — AA 正文对比度 ~4.6:1(仅 ≥24px) $bd: #E8E2DC; // 边框 $bd-l: #F0EBE5; // 浅边框 $dan: #B54A4A; // 危险 (muted red) @@ -22,11 +22,18 @@ $wrn: #C4873A; // 警告 (warm amber) $wrn-l: #FFF3E0; // 警告浅 // ─── 圆角 ─── -$r: 12px; -$r-sm: 8px; -$r-lg: 16px; +$r: 16px; +$r-sm: 12px; +$r-lg: 20px; $r-pill: 999px; +// ─── 老年友好触控参数 ─── +$touch-min: 48px; // 最小触控区域 +$btn-primary-h: 56px; // 主按钮高度 +$menu-item-h: 64px; // 菜单项高度 +$tab-h: 56px; // Tab 切换高度 +$font-min: 22px; // 最小字号 + // ─── 阴影 ─── $shadow-sm: 0 1px 4px rgba(45, 42, 38, 0.04); $shadow-md: 0 2px 12px rgba(45, 42, 38, 0.08);