feat(mp): Phase 2 功能补全 — SOS+推送+趋势图tooltip+家属安全存储
- index: 添加 SOS 紧急求助悬浮按钮(仅患者可见) - alerts: 告警页面添加微信推送订阅 + critical 推送标识 - TrendChart: 添加触摸 tooltip 显示日期和数值 - family: edit_patient 改用 secureSet/secureGet 安全存储
This commit is contained in:
@@ -45,3 +45,21 @@
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
|
||||
// ── Tooltip ──
|
||||
|
||||
.trend-tooltip {
|
||||
position: absolute;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(15, 23, 42, 0.9);
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.trend-tooltip-text {
|
||||
font-size: 12px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useRef, useCallback } from 'react';
|
||||
import React, { useEffect, useRef, useCallback, useState } from 'react';
|
||||
import { Canvas, View, Text } from '@tarojs/components';
|
||||
import Taro from '@tarojs/taro';
|
||||
import './index.scss';
|
||||
@@ -41,6 +41,7 @@ export default React.memo(function TrendChart({
|
||||
height = 500,
|
||||
}: TrendChartProps) {
|
||||
const canvasRef = useRef<any>(null);
|
||||
const [tooltip, setTooltip] = useState<{ date: string; value: number; x: number } | null>(null);
|
||||
|
||||
const draw = useCallback(() => {
|
||||
const node = canvasRef.current;
|
||||
@@ -150,6 +151,26 @@ export default React.memo(function TrendChart({
|
||||
ctx.restore();
|
||||
}, [data, referenceMin, referenceMax]);
|
||||
|
||||
const handleTouchStart = useCallback((e: any) => {
|
||||
if (!data || data.length === 0 || !canvasRef.current) return;
|
||||
const touch = e.touches[0];
|
||||
const node = canvasRef.current;
|
||||
const dpr = getDPR();
|
||||
const x = touch.x;
|
||||
const w = node.width / dpr;
|
||||
const pad = { left: 45, right: 15 };
|
||||
const cw = w - pad.left - pad.right;
|
||||
const relX = x - pad.left;
|
||||
const idx = Math.round((relX / cw) * (data.length - 1));
|
||||
if (idx >= 0 && idx < data.length) {
|
||||
setTooltip({
|
||||
date: data[idx].date,
|
||||
value: data[idx].value,
|
||||
x: pad.left + (idx / Math.max(data.length - 1, 1)) * cw,
|
||||
});
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
useEffect(() => {
|
||||
const query = Taro.createSelectorQuery();
|
||||
query
|
||||
@@ -181,7 +202,18 @@ export default React.memo(function TrendChart({
|
||||
type='2d'
|
||||
id='trend-chart-canvas'
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
onTouchStart={handleTouchStart}
|
||||
/>
|
||||
{tooltip && (
|
||||
<View
|
||||
className='trend-tooltip'
|
||||
style={{ left: `${tooltip.x}px`, top: '8px' }}
|
||||
>
|
||||
<Text className='trend-tooltip-text'>
|
||||
{tooltip.date}: {tooltip.value}{unit ? ` ${unit}` : ''}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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()}
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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}`);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user