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);