feat(mp): Phase 2 功能补全 — SOS+推送+趋势图tooltip+家属安全存储

- index: 添加 SOS 紧急求助悬浮按钮(仅患者可见)
- alerts: 告警页面添加微信推送订阅 + critical 推送标识
- TrendChart: 添加触摸 tooltip 显示日期和数值
- family: edit_patient 改用 secureSet/secureGet 安全存储
This commit is contained in:
iven
2026-05-21 16:24:40 +08:00
parent 6338cd7428
commit 4e9eb7b397
8 changed files with 159 additions and 6 deletions

View File

@@ -499,3 +499,43 @@
font-weight: 600;
color: $white;
}
// ── SOS 紧急求助按钮 ──
.sos-btn {
position: fixed;
right: 24px;
bottom: 140px;
width: 56px;
height: 56px;
border-radius: 50%;
background: #DC2626;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 12px rgba(220, 38, 38, 0.4);
z-index: 100;
&:active {
opacity: 0.8;
transform: scale(0.95);
}
}
.sos-btn-text {
font-size: 16px;
font-weight: 700;
color: #fff;
letter-spacing: 1px;
}
.elder-mode .sos-btn {
width: 68px;
height: 68px;
right: 20px;
bottom: 130px;
}
.elder-mode .sos-btn-text {
font-size: 20px;
}

View File

@@ -195,6 +195,35 @@ function GuestHome({ modeClass }: { modeClass: string }) {
// ─── 登录后首页 ───
function SOSButton() {
const user = useAuthStore((s) => s.user);
const isMedicalStaff = useAuthStore((s) => s.isMedicalStaff);
const currentPatient = useAuthStore((s) => s.currentPatient);
if (!user || !currentPatient || isMedicalStaff()) return null;
const handleSOS = async () => {
const { confirm } = await Taro.showModal({
title: '紧急求助',
content: '是否拨打急救电话?',
confirmText: '拨打',
cancelText: '取消',
});
if (confirm) {
Taro.makePhoneCall({
phoneNumber: '120',
fail: () => Taro.showToast({ title: '拨号失败', icon: 'none' }),
});
}
};
return (
<View className='sos-btn' onClick={handleSOS}>
<Text className='sos-btn-text'>SOS</Text>
</View>
);
}
function HomeDashboard({ modeClass }: { modeClass: string }) {
const {
healthItems, indicatorCapsules, completedCount, progressPercent,
@@ -302,6 +331,7 @@ function HomeDashboard({ modeClass }: { modeClass: string }) {
<Text className='action-btn-text'></Text>
</View>
</View>
<SOSButton />
</PageShell>
);
}

View File

@@ -126,3 +126,19 @@
color: $card;
font-weight: 500;
}
// ── 告警推送标识 ──
.alert-header-left {
display: flex;
align-items: center;
gap: 8px;
}
.alert-push-tag {
font-size: var(--tk-caption);
color: var(--tk-primary);
background: rgba(8, 145, 178, 0.1);
padding: 2px 8px;
border-radius: 4px;
}

View File

@@ -72,6 +72,15 @@ export default function PatientAlerts() {
async () => {
Taro.setNavigationBarTitle({ title: '健康告警' });
await fetchAlerts(1, status, true);
// 请求 critical 告警推送订阅
try {
const tmplId = process.env.TARO_APP_WX_TEMPLATE_CRITICAL_ALERT || '';
if (tmplId) {
await Taro.requestSubscribeMessage({ tmplIds: [tmplId] });
}
} catch {
// 用户拒绝或已订阅,不阻塞页面
}
},
{ throttleMs: 10000, enablePullDown: true },
);
@@ -111,8 +120,13 @@ export default function PatientAlerts() {
return (
<ContentCard className='alert-card' key={item.id}>
<View className='alert-header'>
<View className={`alert-badge ${sev.className}`}>
<Text className='alert-badge-text'>{sev.label}</Text>
<View className='alert-header-left'>
<View className={`alert-badge ${sev.className}`}>
<Text className='alert-badge-text'>{sev.label}</Text>
</View>
{item.severity === 'critical' && (
<Text className='alert-push-tag'></Text>
)}
</View>
<Text className='alert-time'>
{new Date(item.created_at).toLocaleDateString()}

View File

@@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
import { View, Text, Input, Picker } from '@tarojs/components';
import Taro, { useRouter } from '@tarojs/taro';
import { createPatient, updatePatient, Patient } from '../../../services/patient';
import { secureGet, secureRemove } from '@/utils/secure-storage';
import { useElderClass } from '../../../hooks/useElderClass';
import { useSafeTimeout } from '@/hooks/useSafeTimeout';
import PageShell from '@/components/ui/PageShell';
@@ -14,7 +15,8 @@ export default function FamilyAdd() {
const modeClass = useElderClass();
const router = useRouter();
const editId = router.params.id || '';
const editData = Taro.getStorageSync('edit_patient') as Patient | null;
const rawEdit = secureGet('edit_patient');
const editData: Patient | null = rawEdit ? JSON.parse(rawEdit) : null;
const [name, setName] = useState(editData?.name || '');
const [relationIdx, setRelationIdx] = useState(
@@ -28,7 +30,7 @@ export default function FamilyAdd() {
const { safeSetTimeout } = useSafeTimeout();
useEffect(() => {
return () => { Taro.removeStorageSync('edit_patient'); };
return () => { secureRemove('edit_patient'); };
}, []);
const handleSubmit = async () => {

View File

@@ -2,6 +2,7 @@ import React, { useState, useCallback } from 'react';
import { View, Text } from '@tarojs/components';
import Taro from '@tarojs/taro';
import { safeNavigateTo } from '@/utils/navigate';
import { secureSet } from '@/utils/secure-storage';
import { usePageData } from '@/hooks/usePageData';
import { listPatients, Patient } from '../../../services/patient';
import { useAuthStore } from '../../../stores/auth';
@@ -60,7 +61,7 @@ export default function FamilyList() {
};
const goToEdit = (patient: Patient) => {
Taro.setStorageSync('edit_patient', patient);
secureSet('edit_patient', JSON.stringify(patient));
safeNavigateTo(`/pages/pkg-profile/family-add/index?id=${patient.id}`);
};