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; }
|
0% { background-position: 200% 0; }
|
||||||
100% { 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 { Canvas, View, Text } from '@tarojs/components';
|
||||||
import Taro from '@tarojs/taro';
|
import Taro from '@tarojs/taro';
|
||||||
import './index.scss';
|
import './index.scss';
|
||||||
@@ -41,6 +41,7 @@ export default React.memo(function TrendChart({
|
|||||||
height = 500,
|
height = 500,
|
||||||
}: TrendChartProps) {
|
}: TrendChartProps) {
|
||||||
const canvasRef = useRef<any>(null);
|
const canvasRef = useRef<any>(null);
|
||||||
|
const [tooltip, setTooltip] = useState<{ date: string; value: number; x: number } | null>(null);
|
||||||
|
|
||||||
const draw = useCallback(() => {
|
const draw = useCallback(() => {
|
||||||
const node = canvasRef.current;
|
const node = canvasRef.current;
|
||||||
@@ -150,6 +151,26 @@ export default React.memo(function TrendChart({
|
|||||||
ctx.restore();
|
ctx.restore();
|
||||||
}, [data, referenceMin, referenceMax]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
const query = Taro.createSelectorQuery();
|
const query = Taro.createSelectorQuery();
|
||||||
query
|
query
|
||||||
@@ -181,7 +202,18 @@ export default React.memo(function TrendChart({
|
|||||||
type='2d'
|
type='2d'
|
||||||
id='trend-chart-canvas'
|
id='trend-chart-canvas'
|
||||||
style={{ width: '100%', height: '100%' }}
|
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>
|
</View>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -499,3 +499,43 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: $white;
|
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 }) {
|
function HomeDashboard({ modeClass }: { modeClass: string }) {
|
||||||
const {
|
const {
|
||||||
healthItems, indicatorCapsules, completedCount, progressPercent,
|
healthItems, indicatorCapsules, completedCount, progressPercent,
|
||||||
@@ -302,6 +331,7 @@ function HomeDashboard({ modeClass }: { modeClass: string }) {
|
|||||||
<Text className='action-btn-text'>预约挂号</Text>
|
<Text className='action-btn-text'>预约挂号</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
<SOSButton />
|
||||||
</PageShell>
|
</PageShell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,3 +126,19 @@
|
|||||||
color: $card;
|
color: $card;
|
||||||
font-weight: 500;
|
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 () => {
|
async () => {
|
||||||
Taro.setNavigationBarTitle({ title: '健康告警' });
|
Taro.setNavigationBarTitle({ title: '健康告警' });
|
||||||
await fetchAlerts(1, status, true);
|
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 },
|
{ throttleMs: 10000, enablePullDown: true },
|
||||||
);
|
);
|
||||||
@@ -111,8 +120,13 @@ export default function PatientAlerts() {
|
|||||||
return (
|
return (
|
||||||
<ContentCard className='alert-card' key={item.id}>
|
<ContentCard className='alert-card' key={item.id}>
|
||||||
<View className='alert-header'>
|
<View className='alert-header'>
|
||||||
<View className={`alert-badge ${sev.className}`}>
|
<View className='alert-header-left'>
|
||||||
<Text className='alert-badge-text'>{sev.label}</Text>
|
<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>
|
</View>
|
||||||
<Text className='alert-time'>
|
<Text className='alert-time'>
|
||||||
{new Date(item.created_at).toLocaleDateString()}
|
{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 { View, Text, Input, Picker } from '@tarojs/components';
|
||||||
import Taro, { useRouter } from '@tarojs/taro';
|
import Taro, { useRouter } from '@tarojs/taro';
|
||||||
import { createPatient, updatePatient, Patient } from '../../../services/patient';
|
import { createPatient, updatePatient, Patient } from '../../../services/patient';
|
||||||
|
import { secureGet, secureRemove } from '@/utils/secure-storage';
|
||||||
import { useElderClass } from '../../../hooks/useElderClass';
|
import { useElderClass } from '../../../hooks/useElderClass';
|
||||||
import { useSafeTimeout } from '@/hooks/useSafeTimeout';
|
import { useSafeTimeout } from '@/hooks/useSafeTimeout';
|
||||||
import PageShell from '@/components/ui/PageShell';
|
import PageShell from '@/components/ui/PageShell';
|
||||||
@@ -14,7 +15,8 @@ export default function FamilyAdd() {
|
|||||||
const modeClass = useElderClass();
|
const modeClass = useElderClass();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const editId = router.params.id || '';
|
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 [name, setName] = useState(editData?.name || '');
|
||||||
const [relationIdx, setRelationIdx] = useState(
|
const [relationIdx, setRelationIdx] = useState(
|
||||||
@@ -28,7 +30,7 @@ export default function FamilyAdd() {
|
|||||||
const { safeSetTimeout } = useSafeTimeout();
|
const { safeSetTimeout } = useSafeTimeout();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => { Taro.removeStorageSync('edit_patient'); };
|
return () => { secureRemove('edit_patient'); };
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React, { useState, useCallback } from 'react';
|
|||||||
import { View, Text } from '@tarojs/components';
|
import { View, Text } from '@tarojs/components';
|
||||||
import Taro from '@tarojs/taro';
|
import Taro from '@tarojs/taro';
|
||||||
import { safeNavigateTo } from '@/utils/navigate';
|
import { safeNavigateTo } from '@/utils/navigate';
|
||||||
|
import { secureSet } from '@/utils/secure-storage';
|
||||||
import { usePageData } from '@/hooks/usePageData';
|
import { usePageData } from '@/hooks/usePageData';
|
||||||
import { listPatients, Patient } from '../../../services/patient';
|
import { listPatients, Patient } from '../../../services/patient';
|
||||||
import { useAuthStore } from '../../../stores/auth';
|
import { useAuthStore } from '../../../stores/auth';
|
||||||
@@ -60,7 +61,7 @@ export default function FamilyList() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const goToEdit = (patient: Patient) => {
|
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}`);
|
safeNavigateTo(`/pages/pkg-profile/family-add/index?id=${patient.id}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user