diff --git a/apps/miniprogram/src/components/TrendChart/index.scss b/apps/miniprogram/src/components/TrendChart/index.scss index 020a284..8fa4a72 100644 --- a/apps/miniprogram/src/components/TrendChart/index.scss +++ b/apps/miniprogram/src/components/TrendChart/index.scss @@ -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; +} diff --git a/apps/miniprogram/src/components/TrendChart/index.tsx b/apps/miniprogram/src/components/TrendChart/index.tsx index f94c8cf..441c779 100644 --- a/apps/miniprogram/src/components/TrendChart/index.tsx +++ b/apps/miniprogram/src/components/TrendChart/index.tsx @@ -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(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 && ( + + + {tooltip.date}: {tooltip.value}{unit ? ` ${unit}` : ''} + + + )} ); }); diff --git a/apps/miniprogram/src/pages/index/index.scss b/apps/miniprogram/src/pages/index/index.scss index 81b43f7..52a0c61 100644 --- a/apps/miniprogram/src/pages/index/index.scss +++ b/apps/miniprogram/src/pages/index/index.scss @@ -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; +} diff --git a/apps/miniprogram/src/pages/index/index.tsx b/apps/miniprogram/src/pages/index/index.tsx index f506b53..8dbd68b 100644 --- a/apps/miniprogram/src/pages/index/index.tsx +++ b/apps/miniprogram/src/pages/index/index.tsx @@ -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 ( + + SOS + + ); +} + function HomeDashboard({ modeClass }: { modeClass: string }) { const { healthItems, indicatorCapsules, completedCount, progressPercent, @@ -302,6 +331,7 @@ function HomeDashboard({ modeClass }: { modeClass: string }) { 预约挂号 + ); } diff --git a/apps/miniprogram/src/pages/pkg-health/alerts/index.scss b/apps/miniprogram/src/pages/pkg-health/alerts/index.scss index 1e99415..6e085ea 100644 --- a/apps/miniprogram/src/pages/pkg-health/alerts/index.scss +++ b/apps/miniprogram/src/pages/pkg-health/alerts/index.scss @@ -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; +} diff --git a/apps/miniprogram/src/pages/pkg-health/alerts/index.tsx b/apps/miniprogram/src/pages/pkg-health/alerts/index.tsx index 858307a..daf3716 100644 --- a/apps/miniprogram/src/pages/pkg-health/alerts/index.tsx +++ b/apps/miniprogram/src/pages/pkg-health/alerts/index.tsx @@ -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 ( - - {sev.label} + + + {sev.label} + + {item.severity === 'critical' && ( + 推送通知已开启 + )} {new Date(item.created_at).toLocaleDateString()} diff --git a/apps/miniprogram/src/pages/pkg-profile/family-add/index.tsx b/apps/miniprogram/src/pages/pkg-profile/family-add/index.tsx index 2ede573..6b68de1 100644 --- a/apps/miniprogram/src/pages/pkg-profile/family-add/index.tsx +++ b/apps/miniprogram/src/pages/pkg-profile/family-add/index.tsx @@ -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 () => { diff --git a/apps/miniprogram/src/pages/pkg-profile/family/index.tsx b/apps/miniprogram/src/pages/pkg-profile/family/index.tsx index e37c3ac..1e77a63 100644 --- a/apps/miniprogram/src/pages/pkg-profile/family/index.tsx +++ b/apps/miniprogram/src/pages/pkg-profile/family/index.tsx @@ -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}`); };