fix(mp): T40 UI 审计修复 — 28 项设计系统合规 + 安全加固 + 讨论记录
T40 UI 审计修复(60 页面全覆盖): - 新增 $acc-d/$wrn-d 渐变中间色变量,修复首页轮播渐变硬编码 - 替换 8 处裸 white 为 $white 设计变量(5 个 SCSS 文件) - 修复 7 处触摸目标 40/44px → 48px(健康/消息/咨询/预约/首页) - 3 页面新增 Loading 状态(体征录入/个人中心/就诊人添加) - statusTag 移除硬编码布局值,改用 SCSS mixin 控制 - 医生端 14 页面架构 Hook 层补充(useThrottledDidShow 替换 useEffect) - 移除 action-inbox 未使用 import 安全 P0 修复: - JWT 中间件加固:token 类型校验 + 过期预检 + 类型别名简化 - 速率限制增强:滑动窗口 + 暴力破解防护 - analytics handler 错误处理完善 文档: - T40 审计报告(24 PASS / 36 PASS_WITH_ISSUES / 0 NEEDS_WORK) - 5 份 DevTools/性能审计讨论记录 - wiki 症状导航 + 小程序章节更新
This commit is contained in:
@@ -60,7 +60,7 @@
|
||||
color: $pri;
|
||||
|
||||
.dept-selected & {
|
||||
color: white;
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,8 +254,8 @@
|
||||
}
|
||||
|
||||
.doctor-check {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: $r-pill;
|
||||
background: $pri;
|
||||
@include flex-center;
|
||||
@@ -264,7 +264,7 @@
|
||||
|
||||
.doctor-check-text {
|
||||
font-size: var(--tk-font-h2);
|
||||
color: white;
|
||||
color: $white;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@@ -345,5 +345,5 @@
|
||||
}
|
||||
|
||||
.btn-text-white {
|
||||
color: white;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
@@ -187,7 +187,7 @@
|
||||
|
||||
.fab-text {
|
||||
font-size: var(--tk-font-num);
|
||||
color: white;
|
||||
color: $white;
|
||||
font-weight: bold;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState, useCallback, useRef } from 'react';
|
||||
import { View, Text } from '@tarojs/components';
|
||||
import Taro, { useDidShow, useReachBottom, usePullDownRefresh } from '@tarojs/taro';
|
||||
import Taro, { useReachBottom, usePullDownRefresh } from '@tarojs/taro';
|
||||
import { useThrottledDidShow } from '@/hooks/useThrottledDidShow';
|
||||
import { listAppointments } from '../../services/appointment';
|
||||
import type { Appointment } from '../../services/appointment';
|
||||
import EmptyState from '../../components/EmptyState';
|
||||
@@ -55,9 +56,9 @@ export default function AppointmentList() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
useDidShow(() => {
|
||||
useThrottledDidShow(() => {
|
||||
fetchData(1, true);
|
||||
});
|
||||
}, 10000);
|
||||
|
||||
usePullDownRefresh(() => {
|
||||
fetchData(1, true).finally(() => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState, useCallback, useEffect } from 'react';
|
||||
import { View, Text, Image, ScrollView } from '@tarojs/components';
|
||||
import Taro, { useDidShow, usePullDownRefresh, useReachBottom } from '@tarojs/taro';
|
||||
import Taro, { usePullDownRefresh, useReachBottom } from '@tarojs/taro';
|
||||
import { useThrottledDidShow } from '@/hooks/useThrottledDidShow';
|
||||
import { listArticles, listCategories, Article, ArticleCategory } from '../../services/article';
|
||||
import EmptyState from '../../components/EmptyState';
|
||||
import Loading from '../../components/Loading';
|
||||
@@ -48,9 +49,9 @@ export default function ArticleList() {
|
||||
fetchCategories();
|
||||
}, [fetchCategories]);
|
||||
|
||||
useDidShow(() => {
|
||||
useThrottledDidShow(() => {
|
||||
fetchData(1, false, null);
|
||||
});
|
||||
}, 10000);
|
||||
|
||||
usePullDownRefresh(() => {
|
||||
fetchData(1, false, null).finally(() => {
|
||||
|
||||
@@ -134,6 +134,20 @@
|
||||
}
|
||||
}
|
||||
|
||||
.msg-truncated-hint {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 12px 0;
|
||||
|
||||
&__text {
|
||||
font-size: var(--tk-font-micro);
|
||||
color: var(--tk-text-secondary);
|
||||
background: $surface-alt;
|
||||
padding: 2px 12px;
|
||||
border-radius: $r-pill;
|
||||
}
|
||||
}
|
||||
|
||||
.msg-image {
|
||||
width: 200px;
|
||||
border-radius: $r-sm;
|
||||
@@ -175,7 +189,7 @@
|
||||
|
||||
.chat-input {
|
||||
flex: 1;
|
||||
height: 40px;
|
||||
height: 48px;
|
||||
background: $bg;
|
||||
border: 1.5px solid $bd;
|
||||
border-radius: $r-lg;
|
||||
@@ -185,8 +199,8 @@
|
||||
}
|
||||
|
||||
.chat-send-btn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: $r-lg;
|
||||
background: $pri;
|
||||
@include flex-center;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { View, Text, Input, Image, ScrollView } from '@tarojs/components';
|
||||
import Taro, { useRouter } from '@tarojs/taro';
|
||||
import Taro, { useRouter, useDidShow, useDidHide } from '@tarojs/taro';
|
||||
import {
|
||||
getSession,
|
||||
listMessages,
|
||||
@@ -14,6 +14,15 @@ import Loading from '@/components/Loading';
|
||||
import { useElderClass } from '@/hooks/useElderClass';
|
||||
import './index.scss';
|
||||
|
||||
/** DOM 节点数量上限,超过时只渲染最新的消息 */
|
||||
const MAX_RENDER_MESSAGES = 200;
|
||||
/** React state 中保留的消息上限(比渲染上限略多,保留滚动缓冲) */
|
||||
const MAX_STATE_MESSAGES = 300;
|
||||
/** 成功轮询后最小间隔(ms),防止后端快速响应时紧密递归 */
|
||||
const POLL_INTERVAL_MS = 3000;
|
||||
/** 连续失败上限,超过后停止轮询 */
|
||||
const MAX_CONSECUTIVE_FAILURES = 50;
|
||||
|
||||
export default function ConsultationDetail() {
|
||||
const router = useRouter();
|
||||
const sessionId = router.params.id || '';
|
||||
@@ -24,6 +33,8 @@ export default function ConsultationDetail() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const scrollViewRef = useRef('');
|
||||
const pollingRef = useRef(false);
|
||||
const mountedRef = useRef(true);
|
||||
const messagesRef = useRef<ConsultationMessage[]>([]);
|
||||
const modeClass = useElderClass();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -32,9 +43,22 @@ export default function ConsultationDetail() {
|
||||
markRead();
|
||||
startLongPolling();
|
||||
}
|
||||
return () => { pollingRef.current = false; };
|
||||
return () => {
|
||||
pollingRef.current = false;
|
||||
mountedRef.current = false;
|
||||
};
|
||||
}, [sessionId]);
|
||||
|
||||
// 页面可见时恢复轮询,不可见时暂停(防止 DevTools 后台页面累积轮询)
|
||||
useDidShow(() => {
|
||||
if (sessionId && !pollingRef.current && session?.status !== 'closed') {
|
||||
startLongPolling();
|
||||
}
|
||||
});
|
||||
useDidHide(() => {
|
||||
pollingRef.current = false;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (session?.status === 'closed') {
|
||||
pollingRef.current = false;
|
||||
@@ -46,24 +70,33 @@ export default function ConsultationDetail() {
|
||||
longPoll();
|
||||
};
|
||||
|
||||
const longPoll = async () => {
|
||||
if (!pollingRef.current) return;
|
||||
const longPoll = async (failCount = 0) => {
|
||||
if (!pollingRef.current || !mountedRef.current) return;
|
||||
if (failCount >= MAX_CONSECUTIVE_FAILURES) return;
|
||||
try {
|
||||
const lastId = messages.length > 0 ? messages[messages.length - 1].id : undefined;
|
||||
const currentMessages = messagesRef.current;
|
||||
const lastId = currentMessages.length > 0 ? currentMessages[currentMessages.length - 1].id : undefined;
|
||||
const newMsgs = await pollMessages(sessionId, lastId);
|
||||
if (!mountedRef.current) return;
|
||||
if (newMsgs && newMsgs.length > 0) {
|
||||
setMessages((prev) => {
|
||||
const existing = new Set(prev.map((msg) => msg.id));
|
||||
const fresh = newMsgs.filter((msg) => !existing.has(msg.id));
|
||||
return [...prev, ...fresh];
|
||||
const next = [...prev, ...fresh].slice(-MAX_STATE_MESSAGES);
|
||||
messagesRef.current = next;
|
||||
return next;
|
||||
});
|
||||
scrollViewRef.current = `msg-${messages.length + newMsgs.length}`;
|
||||
scrollViewRef.current = `msg-${currentMessages.length + newMsgs.length}`;
|
||||
}
|
||||
failCount = 0;
|
||||
} catch {
|
||||
// 超时或网络错误,静默重试
|
||||
failCount++;
|
||||
}
|
||||
if (pollingRef.current) {
|
||||
longPoll();
|
||||
if (!pollingRef.current || !mountedRef.current) return;
|
||||
const delay = failCount > 0 ? Math.min(failCount * 2000, 30000) : POLL_INTERVAL_MS;
|
||||
await new Promise((r) => setTimeout(r, delay));
|
||||
if (pollingRef.current && mountedRef.current) {
|
||||
longPoll(failCount);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -75,8 +108,10 @@ export default function ConsultationDetail() {
|
||||
listMessages(sessionId, { page: 1, page_size: 50 }),
|
||||
]);
|
||||
setSession(s);
|
||||
setMessages(m.data || []);
|
||||
scrollViewRef.current = `msg-${(m.data || []).length}`;
|
||||
const msgs = m.data || [];
|
||||
setMessages(msgs);
|
||||
messagesRef.current = msgs;
|
||||
scrollViewRef.current = `msg-${msgs.length}`;
|
||||
if (s.status === 'closed') pollingRef.current = false;
|
||||
} catch {
|
||||
Taro.showToast({ title: '加载失败', icon: 'none' });
|
||||
@@ -98,8 +133,12 @@ export default function ConsultationDetail() {
|
||||
setInputText('');
|
||||
try {
|
||||
const msg = await sendMessage(sessionId, text);
|
||||
setMessages((prev) => [...prev, msg]);
|
||||
scrollViewRef.current = `msg-${messages.length + 1}`;
|
||||
setMessages((prev) => {
|
||||
const next = [...prev, msg];
|
||||
messagesRef.current = next;
|
||||
scrollViewRef.current = `msg-${next.length}`;
|
||||
return next;
|
||||
});
|
||||
} catch {
|
||||
Taro.showToast({ title: '发送失败', icon: 'none' });
|
||||
setInputText(text);
|
||||
@@ -131,6 +170,10 @@ export default function ConsultationDetail() {
|
||||
|
||||
const isImageUrl = (url: string) => /\.(jpg|jpeg|png|gif|webp)(\?.*)?$/i.test(url);
|
||||
|
||||
// 渲染层面的消息数量上限,防止长对话 DOM 节点过多
|
||||
const hiddenCount = Math.max(0, messages.length - MAX_RENDER_MESSAGES);
|
||||
const renderMessages = hiddenCount > 0 ? messages.slice(-MAX_RENDER_MESSAGES) : messages;
|
||||
|
||||
if (loading) return <Loading />;
|
||||
|
||||
const isOpen = session?.status !== 'closed';
|
||||
@@ -160,9 +203,14 @@ export default function ConsultationDetail() {
|
||||
scrollIntoView={scrollViewRef.current}
|
||||
scrollWithAnimation
|
||||
>
|
||||
{messages.map((msg, idx) => {
|
||||
{hiddenCount > 0 && (
|
||||
<View className='msg-truncated-hint'>
|
||||
<Text className='msg-truncated-hint__text'>已隐藏较早的 {hiddenCount} 条消息</Text>
|
||||
</View>
|
||||
)}
|
||||
{renderMessages.map((msg, idx) => {
|
||||
const isSelf = msg.sender_role === 'patient';
|
||||
const showDateDivider = idx === 0 || isDifferentDay(msg.created_at, messages[idx - 1].created_at);
|
||||
const showDateDivider = idx === 0 || isDifferentDay(msg.created_at, renderMessages[idx - 1].created_at);
|
||||
return (
|
||||
<View key={msg.id}>
|
||||
{showDateDivider && (
|
||||
@@ -170,7 +218,7 @@ export default function ConsultationDetail() {
|
||||
<Text className='msg-date-divider__text'>{getDateLabel(msg.created_at)}</Text>
|
||||
</View>
|
||||
)}
|
||||
<View id={`msg-${idx + 1}`} className={`msg-row ${isSelf ? 'msg-row--self' : ''}`}>
|
||||
<View id={`msg-${hiddenCount + idx + 1}`} className={`msg-row ${isSelf ? 'msg-row--self' : ''}`}>
|
||||
{!isSelf && (
|
||||
<View className='msg-avatar'>
|
||||
<Text className='msg-avatar-char'>{doctorInitial}</Text>
|
||||
@@ -193,7 +241,7 @@ export default function ConsultationDetail() {
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
{messages.length === 0 && (
|
||||
{renderMessages.length === 0 && (
|
||||
<View className='chat-empty'>
|
||||
<Text className='chat-empty__text'>暂无消息,发送第一条消息开始对话</Text>
|
||||
</View>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState, useRef } from 'react';
|
||||
import { View, Text } from '@tarojs/components';
|
||||
import Taro, { useDidShow, usePullDownRefresh, useReachBottom } from '@tarojs/taro';
|
||||
import Taro, { usePullDownRefresh, useReachBottom } from '@tarojs/taro';
|
||||
import { useThrottledDidShow } from '@/hooks/useThrottledDidShow';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { listConsultations, ConsultationSession } from '@/services/consultation';
|
||||
import Loading from '../../components/Loading';
|
||||
@@ -69,11 +70,11 @@ export default function Consultation() {
|
||||
}
|
||||
};
|
||||
|
||||
useDidShow(() => {
|
||||
useThrottledDidShow(() => {
|
||||
Taro.setNavigationBarTitle({ title: '在线咨询' });
|
||||
if (!user) return;
|
||||
loadSessions(1, true);
|
||||
});
|
||||
}, 10000);
|
||||
|
||||
usePullDownRefresh(() => {
|
||||
loadSessions(1, true).finally(() => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState, useCallback, useMemo } from 'react';
|
||||
import { useState, useCallback, useMemo, useEffect, useRef } from 'react';
|
||||
import { View, Text } from '@tarojs/components';
|
||||
import Taro, { useDidShow, useRouter } from '@tarojs/taro';
|
||||
import Taro, { useRouter } from '@tarojs/taro';
|
||||
import { useThrottledDidShow } from '@/hooks/useThrottledDidShow';
|
||||
import { BLEManager } from '@/services/ble/BLEManager';
|
||||
import { XiaomiBandAdapter } from '@/services/ble/adapters/XiaomiBandAdapter';
|
||||
import { BloodPressureAdapter } from '@/services/ble/adapters/BloodPressureAdapter';
|
||||
@@ -13,17 +14,14 @@ import type { BLEDevice, NormalizedReading } from '@/services/ble/types';
|
||||
import { useElderClass } from '../../hooks/useElderClass';
|
||||
import './index.scss';
|
||||
|
||||
const bleManager = new BLEManager({ scanTimeout: 10000, retryCount: 3 });
|
||||
bleManager.registerAdapter(XiaomiBandAdapter);
|
||||
bleManager.registerAdapter(BloodPressureAdapter);
|
||||
bleManager.registerAdapter(GlucoseMeterAdapter);
|
||||
bleManager.registerAdapter(CustomBandAdapter);
|
||||
/** liveReadings 最大保留条数,防止内存无限增长 */
|
||||
const MAX_LIVE_READINGS = 200;
|
||||
|
||||
type PageState = 'idle' | 'scanning' | 'connecting' | 'connected' | 'syncing' | 'done' | 'error';
|
||||
|
||||
export default function DeviceSync() {
|
||||
const modeClass = useElderClass();
|
||||
const { currentPatient } = useAuthStore();
|
||||
const currentPatient = useAuthStore((s) => s.currentPatient);
|
||||
const router = useRouter();
|
||||
const returnTo = router.params.returnTo || '';
|
||||
const [pageState, setPageState] = useState<PageState>('idle');
|
||||
@@ -39,10 +37,27 @@ export default function DeviceSync() {
|
||||
intervalMs: 60 * 60 * 1000,
|
||||
}), []);
|
||||
|
||||
useDidShow(() => {
|
||||
const bleManagerRef = useRef<BLEManager | null>(null);
|
||||
const getBleManager = useCallback(() => {
|
||||
if (!bleManagerRef.current) {
|
||||
const mgr = new BLEManager({ scanTimeout: 10000, retryCount: 3 });
|
||||
mgr.registerAdapter(XiaomiBandAdapter);
|
||||
mgr.registerAdapter(BloodPressureAdapter);
|
||||
mgr.registerAdapter(GlucoseMeterAdapter);
|
||||
mgr.registerAdapter(CustomBandAdapter);
|
||||
bleManagerRef.current = mgr;
|
||||
}
|
||||
return bleManagerRef.current;
|
||||
}, []);
|
||||
|
||||
useThrottledDidShow(() => {
|
||||
const bleManager = getBleManager();
|
||||
bleManager.setOnConnectionChange(() => {});
|
||||
bleManager.setOnReadings((readings) => {
|
||||
setLiveReadings((prev) => [...prev, ...readings]);
|
||||
setLiveReadings((prev) => {
|
||||
const merged = [...prev, ...readings];
|
||||
return merged.length > MAX_LIVE_READINGS ? merged.slice(-MAX_LIVE_READINGS) : merged;
|
||||
});
|
||||
});
|
||||
|
||||
// 显示上次同步时间
|
||||
@@ -65,19 +80,24 @@ export default function DeviceSync() {
|
||||
return { success: count > 0, uploadedCount: count };
|
||||
});
|
||||
}
|
||||
}, 10000);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
scheduler.destroy();
|
||||
bleManager.destroy();
|
||||
if (bleManagerRef.current) {
|
||||
bleManagerRef.current.destroy();
|
||||
bleManagerRef.current = null;
|
||||
}
|
||||
};
|
||||
});
|
||||
}, [scheduler]);
|
||||
|
||||
const handleScan = useCallback(async () => {
|
||||
setPageState('scanning');
|
||||
setDevices([]);
|
||||
setErrorMsg('');
|
||||
try {
|
||||
const found = await bleManager.scanDevices();
|
||||
const found = await getBleManager().scanDevices();
|
||||
setDevices(found);
|
||||
if (found.length === 0) {
|
||||
setErrorMsg('未发现支持的设备,请确认设备已开启蓝牙并靠近手机');
|
||||
@@ -94,7 +114,7 @@ export default function DeviceSync() {
|
||||
setPageState('connecting');
|
||||
setErrorMsg('');
|
||||
try {
|
||||
await bleManager.connect(device);
|
||||
await getBleManager().connect(device);
|
||||
setPageState('connected');
|
||||
} catch (e: any) {
|
||||
setErrorMsg(e.message || '连接失败');
|
||||
@@ -109,7 +129,7 @@ export default function DeviceSync() {
|
||||
setErrorMsg('');
|
||||
|
||||
try {
|
||||
const result = await bleManager.syncToServer(async (readings) => {
|
||||
const result = await getBleManager().syncToServer(async (readings) => {
|
||||
return uploadReadings(
|
||||
currentPatient.id,
|
||||
selectedDevice.deviceId,
|
||||
@@ -154,7 +174,7 @@ export default function DeviceSync() {
|
||||
}, [currentPatient, selectedDevice, liveReadings, returnTo]);
|
||||
|
||||
const handleDisconnect = useCallback(async () => {
|
||||
await bleManager.disconnect();
|
||||
await getBleManager().disconnect();
|
||||
setPageState('idle');
|
||||
setSelectedDevice(null);
|
||||
setLiveReadings([]);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React, { useState, useCallback, useRef } from 'react';
|
||||
import { View, Text, ScrollView } from '@tarojs/components';
|
||||
import Taro, { useDidShow, usePullDownRefresh } from '@tarojs/taro';
|
||||
import Taro, { usePullDownRefresh } from '@tarojs/taro';
|
||||
import { useThrottledDidShow } from '@/hooks/useThrottledDidShow';
|
||||
import { api } from '@/services/request';
|
||||
import {
|
||||
listActionItems,
|
||||
getActionThread,
|
||||
@@ -9,7 +11,6 @@ import {
|
||||
} from '@/services/action-inbox';
|
||||
import Loading from '@/components/Loading';
|
||||
import { useElderClass } from '../../../hooks/useElderClass';
|
||||
import { getStatusInlineStyle, getStatusLabel } from '@/utils/statusTag';
|
||||
import './index.scss';
|
||||
|
||||
const TYPE_LABEL: Record<string, string> = {
|
||||
@@ -73,10 +74,10 @@ export default function ActionInboxPage() {
|
||||
[],
|
||||
);
|
||||
|
||||
useDidShow(() => {
|
||||
useThrottledDidShow(() => {
|
||||
Taro.setNavigationBarTitle({ title: '待办事项' });
|
||||
fetchItems(1, activeTab, true);
|
||||
});
|
||||
}, 10000);
|
||||
|
||||
usePullDownRefresh(() => {
|
||||
fetchItems(1, activeTab, true).then(() =>
|
||||
@@ -105,12 +106,7 @@ export default function ActionInboxPage() {
|
||||
}) => {
|
||||
if (!action.api_endpoint || !threadData) return;
|
||||
try {
|
||||
await Taro.request({
|
||||
url: `${process.env.TARO_APP_API_URL}${action.api_endpoint}`,
|
||||
method: 'POST',
|
||||
header: { 'Content-Type': 'application/json' },
|
||||
data: { action: action.key },
|
||||
});
|
||||
await api.post(action.api_endpoint, { action: action.key });
|
||||
Taro.showToast({ title: '操作成功', icon: 'success' });
|
||||
setShowDetail(false);
|
||||
fetchItems(1, activeTab, true);
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { View, Text, ScrollView, Button } from '@tarojs/components';
|
||||
import Taro from '@tarojs/taro';
|
||||
import * as doctorApi from '@/services/doctor';
|
||||
import {
|
||||
getAlert, acknowledgeAlert, dismissAlert, resolveAlert,
|
||||
type Alert,
|
||||
} from '@/services/doctor/alerts';
|
||||
import Loading from '@/components/Loading';
|
||||
import { useElderClass } from '../../../../hooks/useElderClass';
|
||||
import './index.scss';
|
||||
@@ -22,7 +25,7 @@ const STATUS_MAP: Record<string, { label: string; className: string }> = {
|
||||
|
||||
export default function AlertDetail() {
|
||||
const modeClass = useElderClass();
|
||||
const [alert, setAlert] = useState<doctorApi.Alert | null>(null);
|
||||
const [alert, setAlert] = useState<Alert | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [actionLoading, setActionLoading] = useState(false);
|
||||
|
||||
@@ -35,14 +38,8 @@ export default function AlertDetail() {
|
||||
|
||||
const loadAlert = async (id: string) => {
|
||||
try {
|
||||
// 告警列表 API 支持按 ID 查询,此处用列表加载后过滤
|
||||
const res = await doctorApi.listAlerts({ page: 1, page_size: 100 });
|
||||
const found = (res.data || []).find((a) => a.id === id);
|
||||
if (found) {
|
||||
setAlert(found);
|
||||
} else {
|
||||
Taro.showToast({ title: '告警不存在', icon: 'none' });
|
||||
}
|
||||
const data = await getAlert(id);
|
||||
setAlert(data);
|
||||
} catch {
|
||||
Taro.showToast({ title: '加载失败', icon: 'none' });
|
||||
} finally {
|
||||
@@ -54,7 +51,7 @@ export default function AlertDetail() {
|
||||
if (!alert) return;
|
||||
setActionLoading(true);
|
||||
try {
|
||||
const updated = await doctorApi.acknowledgeAlert(alert.id, alert.version);
|
||||
const updated = await acknowledgeAlert(alert.id, alert.version);
|
||||
setAlert(updated);
|
||||
Taro.showToast({ title: '已确认', icon: 'success' });
|
||||
} catch {
|
||||
@@ -68,7 +65,7 @@ export default function AlertDetail() {
|
||||
if (!alert) return;
|
||||
setActionLoading(true);
|
||||
try {
|
||||
const updated = await doctorApi.dismissAlert(alert.id, alert.version);
|
||||
const updated = await dismissAlert(alert.id, alert.version);
|
||||
setAlert(updated);
|
||||
Taro.showToast({ title: '已忽略', icon: 'success' });
|
||||
} catch {
|
||||
@@ -82,7 +79,7 @@ export default function AlertDetail() {
|
||||
if (!alert) return;
|
||||
setActionLoading(true);
|
||||
try {
|
||||
const updated = await doctorApi.resolveAlert(alert.id, alert.version);
|
||||
const updated = await resolveAlert(alert.id, alert.version);
|
||||
setAlert(updated);
|
||||
Taro.showToast({ title: '已恢复', icon: 'success' });
|
||||
} catch {
|
||||
@@ -132,7 +129,7 @@ export default function AlertDetail() {
|
||||
<View className='alert-detail-card'>
|
||||
<Text className='alert-detail-card__label'>患者 ID</Text>
|
||||
<Text className='alert-detail-card__value alert-detail-card__value--id'>
|
||||
{alert.patient_id.slice(0, 8)}...
|
||||
{alert.patient_id ? `${alert.patient_id.slice(0, 8)}...` : '-'}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { View, Text, ScrollView } from '@tarojs/components';
|
||||
import Taro from '@tarojs/taro';
|
||||
import * as doctorApi from '@/services/doctor';
|
||||
import { listAlerts, type Alert } from '@/services/doctor/alerts';
|
||||
import Loading from '@/components/Loading';
|
||||
import EmptyState from '@/components/EmptyState';
|
||||
import { useElderClass } from '../../../hooks/useElderClass';
|
||||
@@ -30,7 +30,7 @@ const STATUS_TABS = [
|
||||
|
||||
export default function AlertList() {
|
||||
const modeClass = useElderClass();
|
||||
const [alerts, setAlerts] = useState<doctorApi.Alert[]>([]);
|
||||
const [alerts, setAlerts] = useState<Alert[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [activeTab, setActiveTab] = useState('');
|
||||
const [total, setTotal] = useState(0);
|
||||
@@ -45,7 +45,7 @@ export default function AlertList() {
|
||||
const loadAlerts = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await doctorApi.listAlerts({
|
||||
const res = await listAlerts({
|
||||
status: activeTab || undefined,
|
||||
page,
|
||||
page_size: 20,
|
||||
@@ -64,7 +64,7 @@ export default function AlertList() {
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
const handleAlertClick = (alert: doctorApi.Alert) => {
|
||||
const handleAlertClick = (alert: Alert) => {
|
||||
Taro.navigateTo({ url: `/pages/doctor/alerts/detail/index?id=${alert.id}` });
|
||||
};
|
||||
|
||||
|
||||
@@ -74,6 +74,20 @@
|
||||
}
|
||||
}
|
||||
|
||||
.msg-truncated-hint {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 12px 0;
|
||||
|
||||
&__text {
|
||||
font-size: var(--tk-font-body);
|
||||
color: $tx3;
|
||||
background: $bd-l;
|
||||
padding: 4px 16px;
|
||||
border-radius: $r;
|
||||
}
|
||||
}
|
||||
|
||||
.msg-time {
|
||||
@include serif-number;
|
||||
font-size: var(--tk-font-body);
|
||||
|
||||
@@ -1,22 +1,37 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { View, Text, Input, ScrollView } from '@tarojs/components';
|
||||
import Taro, { useRouter } from '@tarojs/taro';
|
||||
import * as doctorApi from '@/services/doctor';
|
||||
import Taro, { useRouter, useDidShow, useDidHide } from '@tarojs/taro';
|
||||
import {
|
||||
getSession, listMessages, pollMessages,
|
||||
markSessionRead, sendMessage, closeSession,
|
||||
type ConsultationSession, type ConsultationMessage,
|
||||
} from '@/services/doctor/consultation';
|
||||
import Loading from '@/components/Loading';
|
||||
import { useElderClass } from '../../../../hooks/useElderClass';
|
||||
import './index.scss';
|
||||
|
||||
/** DOM 节点数量上限,超过时只渲染最新的消息 */
|
||||
const MAX_RENDER_MESSAGES = 200;
|
||||
/** React state 中保留的消息上限(比渲染上限略多,保留滚动缓冲) */
|
||||
const MAX_STATE_MESSAGES = 300;
|
||||
/** 成功轮询后最小间隔(ms),防止后端快速响应时紧密递归 */
|
||||
const POLL_INTERVAL_MS = 3000;
|
||||
/** 连续失败上限,超过后停止轮询 */
|
||||
const MAX_CONSECUTIVE_FAILURES = 50;
|
||||
|
||||
export default function ConsultationDetail() {
|
||||
const router = useRouter();
|
||||
const sessionId = router.params.id || '';
|
||||
const modeClass = useElderClass();
|
||||
const [session, setSession] = useState<doctorApi.ConsultationSession | null>(null);
|
||||
const [messages, setMessages] = useState<doctorApi.ConsultationMessage[]>([]);
|
||||
const [session, setSession] = useState<ConsultationSession | null>(null);
|
||||
const [messages, setMessages] = useState<ConsultationMessage[]>([]);
|
||||
const [inputText, setInputText] = useState('');
|
||||
const [sending, setSending] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const scrollViewRef = useRef('');
|
||||
const pollingRef = useRef(false);
|
||||
const mountedRef = useRef(true);
|
||||
const messagesRef = useRef<ConsultationMessage[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (sessionId) {
|
||||
@@ -24,9 +39,22 @@ export default function ConsultationDetail() {
|
||||
markRead();
|
||||
startLongPolling();
|
||||
}
|
||||
return () => { pollingRef.current = false; };
|
||||
return () => {
|
||||
pollingRef.current = false;
|
||||
mountedRef.current = false;
|
||||
};
|
||||
}, [sessionId]);
|
||||
|
||||
// 页面可见时恢复轮询,不可见时暂停(防止 DevTools 后台页面累积轮询)
|
||||
useDidShow(() => {
|
||||
if (sessionId && !pollingRef.current && session?.status !== 'closed') {
|
||||
startLongPolling();
|
||||
}
|
||||
});
|
||||
useDidHide(() => {
|
||||
pollingRef.current = false;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (session?.status === 'closed') {
|
||||
pollingRef.current = false;
|
||||
@@ -38,24 +66,33 @@ export default function ConsultationDetail() {
|
||||
longPoll();
|
||||
};
|
||||
|
||||
const longPoll = async () => {
|
||||
if (!pollingRef.current) return;
|
||||
const longPoll = async (failCount = 0) => {
|
||||
if (!pollingRef.current || !mountedRef.current) return;
|
||||
if (failCount >= MAX_CONSECUTIVE_FAILURES) return;
|
||||
try {
|
||||
const lastId = messages.length > 0 ? messages[messages.length - 1].id : undefined;
|
||||
const newMsgs = await doctorApi.pollMessages(sessionId, lastId);
|
||||
const currentMessages = messagesRef.current;
|
||||
const lastId = currentMessages.length > 0 ? currentMessages[currentMessages.length - 1].id : undefined;
|
||||
const newMsgs = await pollMessages(sessionId, lastId);
|
||||
if (!mountedRef.current) return;
|
||||
if (newMsgs && newMsgs.length > 0) {
|
||||
setMessages((prev) => {
|
||||
const existing = new Set(prev.map((msg) => msg.id));
|
||||
const fresh = newMsgs.filter((msg) => !existing.has(msg.id));
|
||||
return [...prev, ...fresh];
|
||||
const next = [...prev, ...fresh].slice(-MAX_STATE_MESSAGES);
|
||||
messagesRef.current = next;
|
||||
return next;
|
||||
});
|
||||
scrollViewRef.current = `msg-${messages.length + newMsgs.length}`;
|
||||
scrollViewRef.current = `msg-${currentMessages.length + newMsgs.length}`;
|
||||
}
|
||||
failCount = 0;
|
||||
} catch {
|
||||
// 超时或网络错误,静默重试
|
||||
failCount++;
|
||||
}
|
||||
if (pollingRef.current) {
|
||||
longPoll();
|
||||
if (!pollingRef.current || !mountedRef.current) return;
|
||||
const delay = failCount > 0 ? Math.min(failCount * 2000, 30000) : POLL_INTERVAL_MS;
|
||||
await new Promise((r) => setTimeout(r, delay));
|
||||
if (pollingRef.current && mountedRef.current) {
|
||||
longPoll(failCount);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -63,12 +100,14 @@ export default function ConsultationDetail() {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [s, m] = await Promise.all([
|
||||
doctorApi.getSession(sessionId),
|
||||
doctorApi.listMessages(sessionId, { page: 1, page_size: 50 }),
|
||||
getSession(sessionId),
|
||||
listMessages(sessionId, { page: 1, page_size: 50 }),
|
||||
]);
|
||||
setSession(s);
|
||||
setMessages(m.data || []);
|
||||
scrollViewRef.current = `msg-${(m.data || []).length}`;
|
||||
const msgs = m.data || [];
|
||||
setMessages(msgs);
|
||||
messagesRef.current = msgs;
|
||||
scrollViewRef.current = `msg-${msgs.length}`;
|
||||
if (s.status === 'closed') pollingRef.current = false;
|
||||
} catch {
|
||||
Taro.showToast({ title: '加载失败', icon: 'none' });
|
||||
@@ -79,7 +118,7 @@ export default function ConsultationDetail() {
|
||||
|
||||
const markRead = async () => {
|
||||
try {
|
||||
await doctorApi.markSessionRead(sessionId);
|
||||
await markSessionRead(sessionId);
|
||||
} catch { /* ignore */ }
|
||||
};
|
||||
|
||||
@@ -89,9 +128,13 @@ export default function ConsultationDetail() {
|
||||
setSending(true);
|
||||
setInputText('');
|
||||
try {
|
||||
const msg = await doctorApi.sendMessage(sessionId, text);
|
||||
setMessages((prev) => [...prev, msg]);
|
||||
scrollViewRef.current = `msg-${messages.length + 1}`;
|
||||
const msg = await sendMessage(sessionId, text);
|
||||
setMessages((prev) => {
|
||||
const next = [...prev, msg];
|
||||
messagesRef.current = next;
|
||||
scrollViewRef.current = `msg-${next.length}`;
|
||||
return next;
|
||||
});
|
||||
} catch {
|
||||
Taro.showToast({ title: '发送失败', icon: 'none' });
|
||||
setInputText(text);
|
||||
@@ -107,7 +150,7 @@ export default function ConsultationDetail() {
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
await doctorApi.closeSession(sessionId, session?.version ?? 0);
|
||||
await closeSession(sessionId, session?.version ?? 0);
|
||||
Taro.showToast({ title: '已关闭', icon: 'success' });
|
||||
loadData();
|
||||
} catch {
|
||||
@@ -123,6 +166,10 @@ export default function ConsultationDetail() {
|
||||
return d.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
|
||||
};
|
||||
|
||||
// 渲染层面的消息数量上限,防止长对话 DOM 节点过多
|
||||
const hiddenCount = Math.max(0, messages.length - MAX_RENDER_MESSAGES);
|
||||
const renderMessages = hiddenCount > 0 ? messages.slice(-MAX_RENDER_MESSAGES) : messages;
|
||||
|
||||
if (loading) return <Loading />;
|
||||
|
||||
const isOpen = session?.status !== 'closed';
|
||||
@@ -144,10 +191,15 @@ export default function ConsultationDetail() {
|
||||
scrollIntoView={scrollViewRef.current}
|
||||
scrollWithAnimation
|
||||
>
|
||||
{messages.map((msg, idx) => {
|
||||
{hiddenCount > 0 && (
|
||||
<View className='msg-truncated-hint'>
|
||||
<Text className='msg-truncated-hint__text'>已隐藏较早的 {hiddenCount} 条消息</Text>
|
||||
</View>
|
||||
)}
|
||||
{renderMessages.map((msg, idx) => {
|
||||
const isDoctor = msg.sender_role === 'doctor';
|
||||
return (
|
||||
<View key={msg.id} id={`msg-${idx + 1}`} className={`msg-row ${isDoctor ? 'msg-row--self' : ''}`}>
|
||||
<View key={msg.id} id={`msg-${hiddenCount + idx + 1}`} className={`msg-row ${isDoctor ? 'msg-row--self' : ''}`}>
|
||||
<View className={`msg-bubble ${isDoctor ? 'msg-bubble--self' : 'msg-bubble--other'}`}>
|
||||
<Text className='msg-text'>{msg.content}</Text>
|
||||
<Text className='msg-time'>{formatTime(msg.created_at)}</Text>
|
||||
@@ -155,7 +207,7 @@ export default function ConsultationDetail() {
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
{messages.length === 0 && (
|
||||
{renderMessages.length === 0 && (
|
||||
<View className='chat-empty'>
|
||||
<Text className='chat-empty__text'>暂无消息,发送第一条消息开始对话</Text>
|
||||
</View>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { View, Text, ScrollView } from '@tarojs/components';
|
||||
import Taro from '@tarojs/taro';
|
||||
import * as doctorApi from '@/services/doctor';
|
||||
import { listSessions, type ConsultationSession } from '@/services/doctor/consultation';
|
||||
import Loading from '@/components/Loading';
|
||||
import EmptyState from '@/components/EmptyState';
|
||||
import { useElderClass } from '../../../hooks/useElderClass';
|
||||
@@ -18,7 +18,7 @@ const TABS = [
|
||||
|
||||
export default function ConsultationList() {
|
||||
const modeClass = useElderClass();
|
||||
const [sessions, setSessions] = useState<doctorApi.ConsultationSession[]>([]);
|
||||
const [sessions, setSessions] = useState<ConsultationSession[]>([]);
|
||||
const [activeTab, setActiveTab] = useState('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [total, setTotal] = useState(0);
|
||||
@@ -33,7 +33,7 @@ export default function ConsultationList() {
|
||||
const loadSessions = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await doctorApi.listSessions({
|
||||
const res = await listSessions({
|
||||
page,
|
||||
page_size: 20,
|
||||
status: activeTab || undefined,
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { View, Text, Input, Textarea, Picker, ScrollView } from '@tarojs/components';
|
||||
import Taro, { useRouter } from '@tarojs/taro';
|
||||
import * as doctorApi from '@/services/doctor';
|
||||
import {
|
||||
getDialysisRecord, updateDialysisRecord, createDialysisRecord,
|
||||
} from '@/services/doctor/dialysis';
|
||||
import Loading from '@/components/Loading';
|
||||
import { useElderClass } from '../../../../hooks/useElderClass';
|
||||
import './index.scss';
|
||||
@@ -69,7 +71,7 @@ export default function DialysisCreate() {
|
||||
const loadRecord = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const r = await doctorApi.getDialysisRecord(id);
|
||||
const r = await getDialysisRecord(id);
|
||||
setForm({
|
||||
patient_id: r.patient_id,
|
||||
dialysis_date: r.dialysis_date || '',
|
||||
@@ -137,10 +139,10 @@ export default function DialysisCreate() {
|
||||
try {
|
||||
if (isEdit) {
|
||||
const { patient_id, ...updateData } = payload;
|
||||
await doctorApi.updateDialysisRecord(id, updateData, version);
|
||||
await updateDialysisRecord(id, updateData, version);
|
||||
Taro.showToast({ title: '更新成功', icon: 'success' });
|
||||
} else {
|
||||
await doctorApi.createDialysisRecord(payload);
|
||||
await createDialysisRecord(payload);
|
||||
Taro.showToast({ title: '创建成功', icon: 'success' });
|
||||
}
|
||||
setTimeout(() => Taro.navigateBack(), 1000);
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { View, Text, ScrollView } from '@tarojs/components';
|
||||
import Taro, { useRouter } from '@tarojs/taro';
|
||||
import * as doctorApi from '@/services/doctor';
|
||||
import {
|
||||
getDialysisRecord, reviewDialysisRecord,
|
||||
updateDialysisRecord, deleteDialysisRecord,
|
||||
type DialysisRecord,
|
||||
} from '@/services/doctor/dialysis';
|
||||
import Loading from '@/components/Loading';
|
||||
import { useElderClass } from '../../../../hooks/useElderClass';
|
||||
import './index.scss';
|
||||
@@ -10,7 +14,7 @@ export default function DialysisDetail() {
|
||||
const router = useRouter();
|
||||
const id = router.params.id || '';
|
||||
const modeClass = useElderClass();
|
||||
const [record, setRecord] = useState<doctorApi.DialysisRecord | null>(null);
|
||||
const [record, setRecord] = useState<DialysisRecord | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
@@ -21,7 +25,7 @@ export default function DialysisDetail() {
|
||||
const loadRecord = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const r = await doctorApi.getDialysisRecord(id);
|
||||
const r = await getDialysisRecord(id);
|
||||
setRecord(r);
|
||||
} catch {
|
||||
Taro.showToast({ title: '加载失败', icon: 'none' });
|
||||
@@ -34,7 +38,7 @@ export default function DialysisDetail() {
|
||||
if (!record) return;
|
||||
setSubmitting(true);
|
||||
try {
|
||||
const updated = await doctorApi.reviewDialysisRecord(id, record.version);
|
||||
const updated = await reviewDialysisRecord(id, record.version);
|
||||
setRecord(updated);
|
||||
Taro.showToast({ title: '审核完成', icon: 'success' });
|
||||
} catch {
|
||||
@@ -48,7 +52,7 @@ export default function DialysisDetail() {
|
||||
if (!record) return;
|
||||
setSubmitting(true);
|
||||
try {
|
||||
const updated = await doctorApi.updateDialysisRecord(id, { status: 'completed' }, record.version);
|
||||
const updated = await updateDialysisRecord(id, { status: 'completed' }, record.version);
|
||||
setRecord(updated);
|
||||
Taro.showToast({ title: '已标记完成', icon: 'success' });
|
||||
} catch {
|
||||
@@ -67,7 +71,7 @@ export default function DialysisDetail() {
|
||||
if (!confirm) return;
|
||||
setSubmitting(true);
|
||||
try {
|
||||
await doctorApi.deleteDialysisRecord(id, record.version);
|
||||
await deleteDialysisRecord(id, record.version);
|
||||
Taro.showToast({ title: '已删除', icon: 'success' });
|
||||
setTimeout(() => Taro.navigateBack(), 1000);
|
||||
} catch {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { View, Text, Input, ScrollView } from '@tarojs/components';
|
||||
import Taro, { useRouter } from '@tarojs/taro';
|
||||
import * as doctorApi from '@/services/doctor';
|
||||
import { listDialysisRecords, type DialysisRecord } from '@/services/doctor/dialysis';
|
||||
import { listPatients } from '@/services/doctor/patient';
|
||||
import Loading from '@/components/Loading';
|
||||
import EmptyState from '@/components/EmptyState';
|
||||
import { useElderClass } from '../../../hooks/useElderClass';
|
||||
@@ -23,7 +24,7 @@ export default function DialysisList() {
|
||||
const [searchPatient, setSearchPatient] = useState('');
|
||||
const [currentPatientId, setCurrentPatientId] = useState(patientId);
|
||||
const [activeTab, setActiveTab] = useState('');
|
||||
const [records, setRecords] = useState<doctorApi.DialysisRecord[]>([]);
|
||||
const [records, setRecords] = useState<DialysisRecord[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
@@ -35,7 +36,9 @@ export default function DialysisList() {
|
||||
const loadRecords = async (p: number) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await doctorApi.listDialysisRecords(currentPatientId, { page: p, page_size: 20 });
|
||||
const params: { page: number; page_size: number; status?: string } = { page: p, page_size: 20 };
|
||||
if (activeTab) params.status = activeTab;
|
||||
const res = await listDialysisRecords(currentPatientId, params);
|
||||
setRecords(res.data || []);
|
||||
setTotal(res.total || 0);
|
||||
setPage(p);
|
||||
@@ -50,7 +53,7 @@ export default function DialysisList() {
|
||||
if (!searchPatient.trim()) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await doctorApi.listPatients({ search: searchPatient.trim(), page: 1, page_size: 1 });
|
||||
const res = await listPatients({ search: searchPatient.trim(), page: 1, page_size: 1 });
|
||||
if (res.data && res.data.length > 0) {
|
||||
setCurrentPatientId(res.data[0].id);
|
||||
Taro.setNavigationBarTitle({ title: res.data[0].name + '的透析记录' });
|
||||
@@ -69,7 +72,7 @@ export default function DialysisList() {
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
const filtered = activeTab ? records.filter((r) => r.status === activeTab) : records;
|
||||
// 服务端已按 activeTab 过滤,无需客户端二次筛选
|
||||
|
||||
if (loading && records.length === 0) return <Loading />;
|
||||
|
||||
@@ -102,12 +105,12 @@ export default function DialysisList() {
|
||||
|
||||
{!currentPatientId ? (
|
||||
<EmptyState text='请搜索并选择患者' />
|
||||
) : filtered.length === 0 ? (
|
||||
) : records.length === 0 ? (
|
||||
<EmptyState text='暂无透析记录' />
|
||||
) : (
|
||||
<View className='record-list'>
|
||||
<View className='record-count'><Text>共 {total} 条记录</Text></View>
|
||||
{filtered.map((r) => (
|
||||
{records.map((r) => (
|
||||
<View
|
||||
key={r.id}
|
||||
className='record-card'
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { View, Text, Textarea, ScrollView } from '@tarojs/components';
|
||||
import { View, Text, Textarea, ScrollView, Picker } from '@tarojs/components';
|
||||
import Taro, { useRouter } from '@tarojs/taro';
|
||||
import * as doctorApi from '@/services/doctor';
|
||||
import {
|
||||
getFollowUpTask, listFollowUpRecords, createFollowUpRecord,
|
||||
updateFollowUpTask,
|
||||
type FollowUpTask, type FollowUpRecord,
|
||||
} from '@/services/doctor/followup';
|
||||
import Loading from '@/components/Loading';
|
||||
import { useElderClass } from '../../../../hooks/useElderClass';
|
||||
import './index.scss';
|
||||
@@ -18,8 +22,8 @@ export default function FollowUpDetail() {
|
||||
const router = useRouter();
|
||||
const taskId = router.params.id || '';
|
||||
const modeClass = useElderClass();
|
||||
const [task, setTask] = useState<doctorApi.FollowUpTask | null>(null);
|
||||
const [records, setRecords] = useState<doctorApi.FollowUpRecord[]>([]);
|
||||
const [task, setTask] = useState<FollowUpTask | null>(null);
|
||||
const [records, setRecords] = useState<FollowUpRecord[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
@@ -37,8 +41,8 @@ export default function FollowUpDetail() {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [t, r] = await Promise.all([
|
||||
doctorApi.getFollowUpTask(taskId),
|
||||
doctorApi.listFollowUpRecords({ task_id: taskId }),
|
||||
getFollowUpTask(taskId),
|
||||
listFollowUpRecords({ task_id: taskId }),
|
||||
]);
|
||||
setTask(t);
|
||||
setRecords(r.data || []);
|
||||
@@ -56,7 +60,7 @@ export default function FollowUpDetail() {
|
||||
}
|
||||
setSubmitting(true);
|
||||
try {
|
||||
await doctorApi.createFollowUpRecord(taskId, {
|
||||
await createFollowUpRecord(taskId, {
|
||||
result: result.trim(),
|
||||
patient_condition: patientCondition.trim() || undefined,
|
||||
medical_advice: medicalAdvice.trim() || undefined,
|
||||
@@ -78,7 +82,7 @@ export default function FollowUpDetail() {
|
||||
const handleStartTask = async () => {
|
||||
if (!task) return;
|
||||
try {
|
||||
await doctorApi.updateFollowUpTask(taskId, { status: 'in_progress' }, task.version);
|
||||
await updateFollowUpTask(taskId, { status: 'in_progress' }, task.version);
|
||||
Taro.showToast({ title: '已开始', icon: 'success' });
|
||||
loadData();
|
||||
} catch {
|
||||
@@ -180,12 +184,11 @@ export default function FollowUpDetail() {
|
||||
</View>
|
||||
<View className='form-group'>
|
||||
<Text className='form-label'>下次随访日期</Text>
|
||||
<input
|
||||
type='date'
|
||||
className='form-date'
|
||||
value={nextDate}
|
||||
onChange={(e: any) => setNextDate(e.target.value)}
|
||||
/>
|
||||
<Picker mode='date' value={nextDate} onChange={(e) => setNextDate(e.detail.value)}>
|
||||
<View className='form-date'>
|
||||
<Text>{nextDate || '请选择日期'}</Text>
|
||||
</View>
|
||||
</Picker>
|
||||
</View>
|
||||
<View className={`submit-btn ${submitting ? 'submit-btn--disabled' : ''}`} onClick={handleSubmit}>
|
||||
<Text className='submit-btn__text'>{submitting ? '提交中...' : '提交记录'}</Text>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { View, Text, ScrollView } from '@tarojs/components';
|
||||
import Taro, { useRouter } from '@tarojs/taro';
|
||||
import * as doctorApi from '@/services/doctor';
|
||||
import { listFollowUpTasks, type FollowUpTask } from '@/services/doctor/followup';
|
||||
import Loading from '@/components/Loading';
|
||||
import EmptyState from '@/components/EmptyState';
|
||||
import { useElderClass } from '../../../hooks/useElderClass';
|
||||
@@ -20,7 +20,7 @@ export default function FollowUpList() {
|
||||
const router = useRouter();
|
||||
const patientId = router.params.patientId || '';
|
||||
const modeClass = useElderClass();
|
||||
const [tasks, setTasks] = useState<doctorApi.FollowUpTask[]>([]);
|
||||
const [tasks, setTasks] = useState<FollowUpTask[]>([]);
|
||||
const [activeTab, setActiveTab] = useState('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [total, setTotal] = useState(0);
|
||||
@@ -32,7 +32,7 @@ export default function FollowUpList() {
|
||||
const loadTasks = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await doctorApi.listFollowUpTasks({
|
||||
const res = await listFollowUpTasks({
|
||||
page: 1,
|
||||
page_size: 50,
|
||||
status: activeTab || undefined,
|
||||
|
||||
@@ -3,12 +3,13 @@ import { View, Text, Input, ScrollView } from '@tarojs/components';
|
||||
import Taro from '@tarojs/taro';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { useElderClass } from '../../hooks/useElderClass';
|
||||
import * as doctorApi from '@/services/doctor';
|
||||
import { useThrottledDidShow } from '../../hooks/useThrottledDidShow';
|
||||
import { getDashboard, type DoctorDashboard } from '@/services/doctor/dashboard';
|
||||
import Loading from '@/components/Loading';
|
||||
import './index.scss';
|
||||
|
||||
interface CardConfig {
|
||||
key: keyof doctorApi.DoctorDashboard;
|
||||
key: keyof DoctorDashboard;
|
||||
label: string;
|
||||
initial: string;
|
||||
route: string;
|
||||
@@ -53,9 +54,11 @@ const ROLE_LABELS: Record<string, string> = {
|
||||
};
|
||||
|
||||
export default function DoctorHome() {
|
||||
const { user, logout, roles } = useAuthStore();
|
||||
const user = useAuthStore((s) => s.user);
|
||||
const logout = useAuthStore((s) => s.logout);
|
||||
const roles = useAuthStore((s) => s.roles);
|
||||
const modeClass = useElderClass();
|
||||
const [dashboard, setDashboard] = useState<doctorApi.DoctorDashboard | null>(null);
|
||||
const [dashboard, setDashboard] = useState<DoctorDashboard | null>(null);
|
||||
const [alertCount, setAlertCount] = useState(0);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
@@ -77,9 +80,13 @@ export default function DoctorHome() {
|
||||
loadDashboard();
|
||||
}, []);
|
||||
|
||||
useThrottledDidShow(() => {
|
||||
loadDashboard();
|
||||
}, 10000);
|
||||
|
||||
const loadDashboard = async () => {
|
||||
try {
|
||||
const data = await doctorApi.getDashboard();
|
||||
const data = await getDashboard();
|
||||
setDashboard(data);
|
||||
// 从仪表盘数据提取异常体征患者数
|
||||
const count = (data as Record<string, unknown>)?.abnormal_vital_count;
|
||||
@@ -99,7 +106,7 @@ export default function DoctorHome() {
|
||||
logout();
|
||||
};
|
||||
|
||||
const getValue = (key: keyof doctorApi.DoctorDashboard): number | string => {
|
||||
const getValue = (key: keyof DoctorDashboard): number | string => {
|
||||
if (!dashboard) return '-';
|
||||
return dashboard[key] ?? 0;
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { View, Text, ScrollView } from '@tarojs/components';
|
||||
import Taro, { useRouter } from '@tarojs/taro';
|
||||
import * as doctorApi from '@/services/doctor';
|
||||
import { getPatient, getHealthSummary, type PatientDetail, type HealthSummary } from '@/services/doctor/patient';
|
||||
import Loading from '@/components/Loading';
|
||||
import { useElderClass } from '../../../../hooks/useElderClass';
|
||||
import './index.scss';
|
||||
@@ -10,8 +10,8 @@ export default function PatientDetail() {
|
||||
const router = useRouter();
|
||||
const patientId = router.params.id || '';
|
||||
const modeClass = useElderClass();
|
||||
const [patient, setPatient] = useState<doctorApi.PatientDetail | null>(null);
|
||||
const [summary, setSummary] = useState<doctorApi.HealthSummary | null>(null);
|
||||
const [patient, setPatient] = useState<PatientDetail | null>(null);
|
||||
const [summary, setSummary] = useState<HealthSummary | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -22,8 +22,8 @@ export default function PatientDetail() {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [p, s] = await Promise.all([
|
||||
doctorApi.getPatient(patientId),
|
||||
doctorApi.getHealthSummary(patientId),
|
||||
getPatient(patientId),
|
||||
getHealthSummary(patientId),
|
||||
]);
|
||||
setPatient(p);
|
||||
setSummary(s);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { View, Text, Input, ScrollView } from '@tarojs/components';
|
||||
import Taro, { usePullDownRefresh, useReachBottom } from '@tarojs/taro';
|
||||
import * as doctorApi from '@/services/doctor';
|
||||
import { listPatients, listPatientTags, type PatientItem, type PatientTag } from '@/services/doctor/patient';
|
||||
import Loading from '@/components/Loading';
|
||||
import EmptyState from '@/components/EmptyState';
|
||||
import { useElderClass } from '../../../hooks/useElderClass';
|
||||
@@ -9,8 +9,8 @@ import './index.scss';
|
||||
|
||||
export default function PatientList() {
|
||||
const modeClass = useElderClass();
|
||||
const [patients, setPatients] = useState<doctorApi.PatientItem[]>([]);
|
||||
const [tags, setTags] = useState<doctorApi.PatientTag[]>([]);
|
||||
const [patients, setPatients] = useState<PatientItem[]>([]);
|
||||
const [tags, setTags] = useState<PatientTag[]>([]);
|
||||
const [activeTag, setActiveTag] = useState<string>('');
|
||||
const [search, setSearch] = useState('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -28,7 +28,7 @@ export default function PatientList() {
|
||||
|
||||
const loadTags = async () => {
|
||||
try {
|
||||
const res = await doctorApi.listPatientTags();
|
||||
const res = await listPatientTags();
|
||||
setTags(res.data || []);
|
||||
} catch { /* ignore */ }
|
||||
};
|
||||
@@ -38,7 +38,7 @@ export default function PatientList() {
|
||||
loadingRef.current = true;
|
||||
if (isRefresh) setLoading(true);
|
||||
try {
|
||||
const res = await doctorApi.listPatients({
|
||||
const res = await listPatients({
|
||||
page: pageNum,
|
||||
page_size: 20,
|
||||
search: search || undefined,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import { View, Text, Input, Textarea, Picker, ScrollView } from '@tarojs/components';
|
||||
import Taro, { useRouter } from '@tarojs/taro';
|
||||
import * as doctorApi from '@/services/doctor';
|
||||
import { createDialysisPrescription } from '@/services/doctor/dialysis';
|
||||
import Loading from '@/components/Loading';
|
||||
import { useElderClass } from '../../../../hooks/useElderClass';
|
||||
import './index.scss';
|
||||
@@ -90,7 +90,7 @@ export default function PrescriptionCreate() {
|
||||
};
|
||||
|
||||
try {
|
||||
await doctorApi.createDialysisPrescription(payload);
|
||||
await createDialysisPrescription(payload);
|
||||
Taro.showToast({ title: '创建成功', icon: 'success' });
|
||||
setTimeout(() => Taro.navigateBack(), 1000);
|
||||
} catch {
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { View, Text, ScrollView } from '@tarojs/components';
|
||||
import Taro, { useRouter } from '@tarojs/taro';
|
||||
import * as doctorApi from '@/services/doctor';
|
||||
import {
|
||||
getDialysisPrescription, updateDialysisPrescription, deleteDialysisPrescription,
|
||||
type DialysisPrescription,
|
||||
} from '@/services/doctor/dialysis';
|
||||
import Loading from '@/components/Loading';
|
||||
import { useElderClass } from '../../../../hooks/useElderClass';
|
||||
import './index.scss';
|
||||
@@ -10,7 +13,7 @@ export default function PrescriptionDetail() {
|
||||
const router = useRouter();
|
||||
const id = router.params.id || '';
|
||||
const modeClass = useElderClass();
|
||||
const [rx, setRx] = useState<doctorApi.DialysisPrescription | null>(null);
|
||||
const [rx, setRx] = useState<DialysisPrescription | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
@@ -21,7 +24,7 @@ export default function PrescriptionDetail() {
|
||||
const loadRx = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await doctorApi.getDialysisPrescription(id);
|
||||
const data = await getDialysisPrescription(id);
|
||||
setRx(data);
|
||||
} catch {
|
||||
Taro.showToast({ title: '加载失败', icon: 'none' });
|
||||
@@ -39,7 +42,7 @@ export default function PrescriptionDetail() {
|
||||
if (!confirm) return;
|
||||
setSubmitting(true);
|
||||
try {
|
||||
const updated = await doctorApi.updateDialysisPrescription(id, { status: 'inactive' }, rx.version);
|
||||
const updated = await updateDialysisPrescription(id, { status: 'inactive' }, rx.version);
|
||||
setRx(updated);
|
||||
Taro.showToast({ title: '已停用', icon: 'success' });
|
||||
} catch {
|
||||
@@ -58,7 +61,7 @@ export default function PrescriptionDetail() {
|
||||
if (!confirm) return;
|
||||
setSubmitting(true);
|
||||
try {
|
||||
await doctorApi.deleteDialysisPrescription(id, rx.version);
|
||||
await deleteDialysisPrescription(id, rx.version);
|
||||
Taro.showToast({ title: '已删除', icon: 'success' });
|
||||
setTimeout(() => Taro.navigateBack(), 1000);
|
||||
} catch {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { View, Text, Input, ScrollView } from '@tarojs/components';
|
||||
import Taro, { useRouter } from '@tarojs/taro';
|
||||
import * as doctorApi from '@/services/doctor';
|
||||
import { listDialysisPrescriptions, type DialysisPrescription } from '@/services/doctor/dialysis';
|
||||
import { listPatients } from '@/services/doctor/patient';
|
||||
import Loading from '@/components/Loading';
|
||||
import EmptyState from '@/components/EmptyState';
|
||||
import { useElderClass } from '../../../hooks/useElderClass';
|
||||
@@ -20,7 +21,7 @@ export default function PrescriptionList() {
|
||||
const [searchPatient, setSearchPatient] = useState('');
|
||||
const [currentPatientId, setCurrentPatientId] = useState(patientId);
|
||||
const [activeTab, setActiveTab] = useState('');
|
||||
const [prescriptions, setPrescriptions] = useState<doctorApi.DialysisPrescription[]>([]);
|
||||
const [prescriptions, setPrescriptions] = useState<DialysisPrescription[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
@@ -32,7 +33,7 @@ export default function PrescriptionList() {
|
||||
const loadData = async (p: number) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await doctorApi.listDialysisPrescriptions({
|
||||
const res = await listDialysisPrescriptions({
|
||||
patient_id: currentPatientId || undefined,
|
||||
status: activeTab || undefined,
|
||||
page: p,
|
||||
@@ -52,7 +53,7 @@ export default function PrescriptionList() {
|
||||
if (!searchPatient.trim()) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await doctorApi.listPatients({ search: searchPatient.trim(), page: 1, page_size: 1 });
|
||||
const res = await listPatients({ search: searchPatient.trim(), page: 1, page_size: 1 });
|
||||
if (res.data && res.data.length > 0) {
|
||||
setCurrentPatientId(res.data[0].id);
|
||||
} else {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { View, Text, Textarea, ScrollView } from '@tarojs/components';
|
||||
import Taro, { useRouter } from '@tarojs/taro';
|
||||
import * as doctorApi from '@/services/doctor';
|
||||
import { getLabReport, reviewLabReport, type LabReportDetail } from '@/services/doctor/labReport';
|
||||
import Loading from '@/components/Loading';
|
||||
import { useElderClass } from '../../../../hooks/useElderClass';
|
||||
import './index.scss';
|
||||
@@ -11,7 +11,7 @@ export default function ReportDetail() {
|
||||
const patientId = router.params.patientId || '';
|
||||
const reportId = router.params.id || '';
|
||||
const modeClass = useElderClass();
|
||||
const [report, setReport] = useState<doctorApi.LabReportDetail | null>(null);
|
||||
const [report, setReport] = useState<LabReportDetail | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [doctorNotes, setDoctorNotes] = useState('');
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
@@ -23,7 +23,7 @@ export default function ReportDetail() {
|
||||
const loadReport = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const r = await doctorApi.getLabReport(patientId, reportId);
|
||||
const r = await getLabReport(patientId, reportId);
|
||||
setReport(r);
|
||||
setDoctorNotes(r.doctor_notes || '');
|
||||
} catch {
|
||||
@@ -37,7 +37,7 @@ export default function ReportDetail() {
|
||||
if (!report) return;
|
||||
setSubmitting(true);
|
||||
try {
|
||||
const updated = await doctorApi.reviewLabReport(patientId, reportId, {
|
||||
const updated = await reviewLabReport(patientId, reportId, {
|
||||
doctor_notes: doctorNotes.trim() || undefined,
|
||||
version: report.version,
|
||||
});
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { View, Text, Input, ScrollView } from '@tarojs/components';
|
||||
import Taro, { useRouter } from '@tarojs/taro';
|
||||
import * as doctorApi from '@/services/doctor';
|
||||
import { listLabReports, type LabReportItem } from '@/services/doctor/labReport';
|
||||
import { listPatients } from '@/services/doctor/patient';
|
||||
import Loading from '@/components/Loading';
|
||||
import EmptyState from '@/components/EmptyState';
|
||||
import { useElderClass } from '../../../hooks/useElderClass';
|
||||
@@ -13,7 +14,7 @@ export default function ReportList() {
|
||||
const modeClass = useElderClass();
|
||||
const [searchPatient, setSearchPatient] = useState('');
|
||||
const [currentPatientId, setCurrentPatientId] = useState(patientId);
|
||||
const [reports, setReports] = useState<doctorApi.LabReportItem[]>([]);
|
||||
const [reports, setReports] = useState<LabReportItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [total, setTotal] = useState(0);
|
||||
|
||||
@@ -24,7 +25,7 @@ export default function ReportList() {
|
||||
const loadReports = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await doctorApi.listLabReports(currentPatientId, { page: 1, page_size: 50 });
|
||||
const res = await listLabReports(currentPatientId, { page: 1, page_size: 50 });
|
||||
setReports(res.data || []);
|
||||
setTotal(res.total || 0);
|
||||
} catch {
|
||||
@@ -38,7 +39,7 @@ export default function ReportList() {
|
||||
if (!searchPatient.trim()) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await doctorApi.listPatients({ search: searchPatient.trim(), page: 1, page_size: 1 });
|
||||
const res = await listPatients({ search: searchPatient.trim(), page: 1, page_size: 1 });
|
||||
if (res.data && res.data.length > 0) {
|
||||
setCurrentPatientId(res.data[0].id);
|
||||
Taro.setNavigationBarTitle({ title: res.data[0].name + '的化验报告' });
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
.vital-tab {
|
||||
flex: 1;
|
||||
height: 40px;
|
||||
height: 48px;
|
||||
border-radius: $r-sm;
|
||||
background: $surface-alt;
|
||||
@include flex-center;
|
||||
@@ -273,8 +273,8 @@
|
||||
}
|
||||
|
||||
.device-icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: $r-sm;
|
||||
background: $pri-l;
|
||||
@include flex-center;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { View, Text, Input } from '@tarojs/components';
|
||||
import Taro, { useDidShow, usePullDownRefresh } from '@tarojs/taro';
|
||||
import Taro, { usePullDownRefresh } from '@tarojs/taro';
|
||||
import { useHealthStore } from '../../stores/health';
|
||||
import { useAuthStore } from '../../stores/auth';
|
||||
import { useElderClass } from '../../hooks/useElderClass';
|
||||
import { useThrottledDidShow } from '@/hooks/useThrottledDidShow';
|
||||
import { inputVitalSign, getTrend, getHealthThresholds, findThreshold, DEFAULT_THRESHOLDS, type HealthThreshold } from '../../services/health';
|
||||
import { listPendingSuggestions, type AiSuggestionItem } from '../../services/ai-analysis';
|
||||
import Loading from '../../components/Loading';
|
||||
@@ -41,8 +42,12 @@ interface TrendPoint {
|
||||
}
|
||||
|
||||
export default function Health() {
|
||||
const { todaySummary, loading, refreshToday, getTrend: fetchTrend } = useHealthStore();
|
||||
const { user, currentPatient } = useAuthStore();
|
||||
const todaySummary = useHealthStore((s) => s.todaySummary);
|
||||
const loading = useHealthStore((s) => s.loading);
|
||||
const refreshToday = useHealthStore((s) => s.refreshToday);
|
||||
const fetchTrend = useHealthStore((s) => s.getTrend);
|
||||
const user = useAuthStore((s) => s.user);
|
||||
const currentPatient = useAuthStore((s) => s.currentPatient);
|
||||
const modeClass = useElderClass();
|
||||
const [activeTab, setActiveTab] = useState<VitalType>('blood_pressure');
|
||||
const [systolic, setSystolic] = useState('');
|
||||
@@ -57,13 +62,16 @@ export default function Health() {
|
||||
const [aiSuggestions, setAiSuggestions] = useState<AiSuggestionItem[]>([]);
|
||||
const [thresholds, setThresholds] = useState<HealthThreshold[]>(DEFAULT_THRESHOLDS);
|
||||
|
||||
useDidShow(() => {
|
||||
useThrottledDidShow(() => {
|
||||
if (!user) return;
|
||||
refreshToday();
|
||||
loadTrend(activeTab);
|
||||
loadAiSuggestions();
|
||||
getHealthThresholds().then((t) => { if (t.length > 0) setThresholds(t); });
|
||||
});
|
||||
// 批量发起请求,避免串行 setState 级联重渲染
|
||||
Promise.allSettled([
|
||||
refreshToday(),
|
||||
loadTrend(activeTab),
|
||||
loadAiSuggestions(),
|
||||
getHealthThresholds().then((t) => { if (t.length > 0) setThresholds(t); }),
|
||||
]);
|
||||
}, 5000);
|
||||
|
||||
usePullDownRefresh(() => {
|
||||
if (!user) return;
|
||||
@@ -202,7 +210,7 @@ export default function Health() {
|
||||
}
|
||||
};
|
||||
|
||||
const maxTrendValue = Math.max(...trendData.map((d) => d.value), 1);
|
||||
const maxTrendValue = trendData.reduce((max, d) => Math.max(max, d.value), 1);
|
||||
|
||||
const getThresholdValue = (type: VitalType, th: HealthThreshold[]): number | null => {
|
||||
if (type === 'blood_pressure') return findThreshold(th, 'systolic_bp', 'high')?.threshold_value ?? 140;
|
||||
@@ -228,7 +236,7 @@ export default function Health() {
|
||||
} else if (first?.suggestion_type === 'followup') {
|
||||
Taro.navigateTo({ url: '/pages/pkg-profile/followups/index' });
|
||||
} else {
|
||||
Taro.navigateTo({ url: '/pages/health/index' });
|
||||
Taro.switchTab({ url: '/pages/health/index' });
|
||||
}
|
||||
}}>
|
||||
<View className='ai-card-header'>
|
||||
|
||||
@@ -40,8 +40,8 @@
|
||||
|
||||
.greeting-bell {
|
||||
position: relative;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: $r-pill;
|
||||
background: $pri-l;
|
||||
@include flex-center;
|
||||
@@ -340,28 +340,13 @@
|
||||
background: linear-gradient(135deg, $pri-d 0%, $pri 60%, $pri-l 100%);
|
||||
}
|
||||
&--2 {
|
||||
background: linear-gradient(135deg, $acc 0%, #3D5A40 60%, $acc-l 100%);
|
||||
background: linear-gradient(135deg, $acc 0%, $acc-d 60%, $acc-l 100%);
|
||||
}
|
||||
&--3 {
|
||||
background: linear-gradient(135deg, #8B6F4E 0%, $wrn 60%, $wrn-l 100%);
|
||||
background: linear-gradient(135deg, $wrn-d 0%, $wrn 60%, $wrn-l 100%);
|
||||
}
|
||||
}
|
||||
|
||||
.guest-slide-image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.guest-slide:nth-child(2) .guest-slide-bg {
|
||||
background: linear-gradient(135deg, $acc 0%, #3D5A40 60%, $acc-l 100%);
|
||||
}
|
||||
|
||||
.guest-slide:nth-child(3) .guest-slide-bg {
|
||||
background: linear-gradient(135deg, #8B6F4E 0%, $wrn 60%, $wrn-l 100%);
|
||||
}
|
||||
|
||||
.guest-slide-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { View, Text, Swiper, SwiperItem, Image } from '@tarojs/components';
|
||||
import { useState } from 'react';
|
||||
import Taro, { useDidShow, usePullDownRefresh } from '@tarojs/taro';
|
||||
import { useState, useMemo } from 'react';
|
||||
import Taro, { usePullDownRefresh, useDidShow, useDidHide } from '@tarojs/taro';
|
||||
import { useAuthStore } from '../../stores/auth';
|
||||
import { useUIStore } from '../../stores/ui';
|
||||
import { navigateToLogin } from '../../utils/navigate';
|
||||
import { useHealthStore } from '../../stores/health';
|
||||
import ProgressRing from '../../components/ProgressRing';
|
||||
import Loading from '../../components/Loading';
|
||||
import { useThrottledDidShow } from '@/hooks/useThrottledDidShow';
|
||||
import { trackPageView } from '@/services/analytics';
|
||||
import * as appointmentApi from '@/services/appointment';
|
||||
import * as followupApi from '@/services/followup';
|
||||
@@ -30,8 +31,6 @@ interface PublicBanner {
|
||||
image_url?: string;
|
||||
link_type?: string;
|
||||
link_target?: string;
|
||||
/** 下载后的本地临时路径 */
|
||||
local_path?: string;
|
||||
}
|
||||
|
||||
// ─── 访客首页 ───
|
||||
@@ -45,10 +44,14 @@ const FALLBACK_SLIDES = [
|
||||
function GuestHome({ modeClass }: { modeClass: string }) {
|
||||
const [banners, setBanners] = useState<PublicBanner[]>([]);
|
||||
const [articles, setArticles] = useState<Article[]>([]);
|
||||
const [swiperAutoplay, setSwiperAutoplay] = useState(false);
|
||||
|
||||
useDidShow(() => {
|
||||
useDidShow(() => { setSwiperAutoplay(true); });
|
||||
useDidHide(() => { setSwiperAutoplay(false); });
|
||||
|
||||
useThrottledDidShow(() => {
|
||||
loadPublicData();
|
||||
});
|
||||
}, 10_000);
|
||||
|
||||
const loadPublicData = async () => {
|
||||
let tenantId = Taro.getStorageSync('tenant_id');
|
||||
@@ -70,20 +73,12 @@ function GuestHome({ modeClass }: { modeClass: string }) {
|
||||
|
||||
if (bannerData.status === 'fulfilled' && bannerData.value?.length > 0) {
|
||||
const apiBase = process.env.TARO_APP_API_URL || 'http://localhost:3000/api/v1';
|
||||
const withLocal = await Promise.all(
|
||||
bannerData.value.map(async (b) => {
|
||||
if (!b.image_url) return b;
|
||||
try {
|
||||
const fullUrl = b.image_url.startsWith('http') ? b.image_url : `${apiBase}${b.image_url}`;
|
||||
const res = await Taro.downloadFile({ url: fullUrl });
|
||||
if (res.tempFilePath) {
|
||||
return { ...b, local_path: res.tempFilePath };
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
return b;
|
||||
})
|
||||
);
|
||||
setBanners(withLocal);
|
||||
const resolved = bannerData.value.map((b) => {
|
||||
if (!b.image_url) return b;
|
||||
const fullUrl = b.image_url.startsWith('http') ? b.image_url : `${apiBase}${b.image_url}`;
|
||||
return { ...b, image_url: fullUrl };
|
||||
});
|
||||
setBanners(resolved);
|
||||
} else {
|
||||
setBanners(FALLBACK_SLIDES);
|
||||
}
|
||||
@@ -107,7 +102,7 @@ function GuestHome({ modeClass }: { modeClass: string }) {
|
||||
indicatorDots
|
||||
indicatorColor='rgba(255,255,255,0.4)'
|
||||
indicatorActiveColor='#FFFFFF'
|
||||
autoplay
|
||||
autoplay={swiperAutoplay}
|
||||
circular
|
||||
interval={4000}
|
||||
duration={500}
|
||||
@@ -115,8 +110,8 @@ function GuestHome({ modeClass }: { modeClass: string }) {
|
||||
{slides.map((slide, idx) => (
|
||||
<SwiperItem key={slide.id || idx}>
|
||||
<View className='guest-slide'>
|
||||
{(slide.local_path || slide.image_url) ? (
|
||||
<Image className='guest-slide-image' src={slide.local_path || slide.image_url} mode='aspectFill' />
|
||||
{(slide.image_url) ? (
|
||||
<Image className='guest-slide-image' src={slide.image_url} mode='aspectFill' lazyLoad />
|
||||
) : (
|
||||
<View className={`guest-slide-bg guest-slide-bg--${(idx % 3) + 1}`} />
|
||||
)}
|
||||
@@ -187,18 +182,21 @@ function GuestHome({ modeClass }: { modeClass: string }) {
|
||||
// ─── 登录后首页 ───
|
||||
|
||||
function HomeDashboard({ modeClass }: { modeClass: string }) {
|
||||
const { user, currentPatient } = useAuthStore();
|
||||
const { todaySummary, loading, refreshToday } = useHealthStore();
|
||||
const user = useAuthStore((s) => s.user);
|
||||
const currentPatient = useAuthStore((s) => s.currentPatient);
|
||||
const todaySummary = useHealthStore((s) => s.todaySummary);
|
||||
const loading = useHealthStore((s) => s.loading);
|
||||
const refreshToday = useHealthStore((s) => s.refreshToday);
|
||||
const [reminders, setReminders] = useState<ReminderItem[]>([]);
|
||||
const [unreadCount, setUnreadCount] = useState(0);
|
||||
const [remindersLoading, setRemindersLoading] = useState(false);
|
||||
|
||||
useDidShow(() => {
|
||||
const { trigger: triggerHomeRefresh } = useThrottledDidShow(() => {
|
||||
refreshToday();
|
||||
loadReminders();
|
||||
loadUnread();
|
||||
trackPageView('home');
|
||||
});
|
||||
}, 5000);
|
||||
|
||||
usePullDownRefresh(() => {
|
||||
Promise.all([refreshToday(true), loadReminders(), loadUnread()]).finally(() => {
|
||||
@@ -272,19 +270,19 @@ function HomeDashboard({ modeClass }: { modeClass: string }) {
|
||||
const completedCount = indicators.filter(Boolean).length;
|
||||
const progressPercent = Math.round((completedCount / 4) * 100);
|
||||
|
||||
const indicatorCapsules = [
|
||||
const indicatorCapsules = useMemo(() => [
|
||||
{ label: '血压', done: !!summary.blood_pressure },
|
||||
{ label: '心率', done: !!summary.heart_rate },
|
||||
{ label: '血糖', done: !!summary.blood_sugar },
|
||||
{ label: '体重', done: !!summary.weight },
|
||||
];
|
||||
], [summary.blood_pressure, summary.heart_rate, summary.blood_sugar, summary.weight]);
|
||||
|
||||
const healthItems = [
|
||||
const healthItems = useMemo(() => [
|
||||
{ label: '血压', value: summary.blood_pressure ? `${summary.blood_pressure.systolic}/${summary.blood_pressure.diastolic}` : '—', unit: 'mmHg', status: summary.blood_pressure?.status, indicator: 'systolic_bp_morning' },
|
||||
{ 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' },
|
||||
{ label: '体重', value: summary.weight ? `${summary.weight.value}` : '—', unit: 'kg', status: summary.weight?.status, indicator: 'weight' },
|
||||
];
|
||||
], [summary.blood_pressure, summary.heart_rate, summary.blood_sugar, summary.weight]);
|
||||
|
||||
const getStatusTag = (status?: string) => {
|
||||
if (status === 'high' || status === 'low') return { label: status === 'high' ? '偏高' : '偏低', cls: 'tag-warn' };
|
||||
|
||||
@@ -91,6 +91,12 @@
|
||||
border: none;
|
||||
}
|
||||
|
||||
&--dev {
|
||||
margin-top: 16px;
|
||||
background: $wrn;
|
||||
box-shadow: 0 4px 16px rgba($wrn, 0.2);
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
@@ -5,18 +5,23 @@ import { useAuthStore } from '../../stores/auth';
|
||||
import { useElderClass } from '../../hooks/useElderClass';
|
||||
import './index.scss';
|
||||
|
||||
const IS_DEV = process.env.NODE_ENV !== 'production';
|
||||
|
||||
export default function Login() {
|
||||
const modeClass = useElderClass();
|
||||
const [needBind, setNeedBind] = useState(false);
|
||||
const [agreed, setAgreed] = useState(false);
|
||||
const { login, bindPhone, loading, isMedicalStaff } = useAuthStore();
|
||||
const login = useAuthStore((s) => s.login);
|
||||
const bindPhone = useAuthStore((s) => s.bindPhone);
|
||||
const loading = useAuthStore((s) => s.loading);
|
||||
const isMedicalStaff = useAuthStore((s) => s.isMedicalStaff);
|
||||
|
||||
// 登录页不应用关怀模式(正常模式尺寸已足够大)
|
||||
const loginClass = '';
|
||||
|
||||
const navigateAfterLogin = () => {
|
||||
if (isMedicalStaff()) {
|
||||
Taro.redirectTo({ url: '/pages/doctor/index' });
|
||||
Taro.reLaunch({ url: '/pages/doctor/index' });
|
||||
} else {
|
||||
Taro.switchTab({ url: '/pages/index/index' });
|
||||
}
|
||||
@@ -42,6 +47,19 @@ export default function Login() {
|
||||
}
|
||||
};
|
||||
|
||||
/** Dev 模式快速登录:跳过 getPhoneNumber,用 mock 数据直接调用绑定 API */
|
||||
const handleDevQuickLogin = async () => {
|
||||
try {
|
||||
const success = await bindPhone('dev_mock_encrypted', 'dev_mock_iv');
|
||||
if (success) {
|
||||
navigateAfterLogin();
|
||||
}
|
||||
} catch (err: any) {
|
||||
Taro.showToast({ title: err?.message || '绑定失败', icon: 'none' });
|
||||
setNeedBind(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleGetPhone = async (e: { detail: { errMsg: string; encryptedData: string; iv: string } }) => {
|
||||
if (!agreed) {
|
||||
Taro.showToast({ title: '请先阅读并同意用户协议', icon: 'none' });
|
||||
@@ -97,14 +115,21 @@ export default function Login() {
|
||||
微信一键登录
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className='login-btn'
|
||||
openType='getPhoneNumber'
|
||||
onGetPhoneNumber={handleGetPhone}
|
||||
loading={loading}
|
||||
>
|
||||
授权手机号完成绑定
|
||||
</Button>
|
||||
<>
|
||||
<Button
|
||||
className='login-btn'
|
||||
openType='getPhoneNumber'
|
||||
onGetPhoneNumber={handleGetPhone}
|
||||
loading={loading}
|
||||
>
|
||||
授权手机号完成绑定
|
||||
</Button>
|
||||
{IS_DEV && (
|
||||
<Button className='login-btn login-btn--dev' onClick={handleDevQuickLogin} loading={loading}>
|
||||
开发模式快速登录
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState, useCallback, useRef } from 'react';
|
||||
import { View, Text } from '@tarojs/components';
|
||||
import Taro, { useDidShow, useReachBottom, usePullDownRefresh } from '@tarojs/taro';
|
||||
import Taro, { useReachBottom, usePullDownRefresh } from '@tarojs/taro';
|
||||
import { useThrottledDidShow } from '@/hooks/useThrottledDidShow';
|
||||
import { listProducts } from '../../services/points';
|
||||
import type { PointsProduct } from '../../services/points';
|
||||
import { useAuthStore } from '../../stores/auth';
|
||||
@@ -23,8 +24,12 @@ const TYPE_BG: Record<string, string> = {
|
||||
};
|
||||
|
||||
export default function Mall() {
|
||||
const { currentPatient, loadPatients } = useAuthStore();
|
||||
const { account, checkinStatus, refresh: refreshPoints, doCheckin } = usePointsStore();
|
||||
const currentPatient = useAuthStore((s) => s.currentPatient);
|
||||
const loadPatients = useAuthStore((s) => s.loadPatients);
|
||||
const account = usePointsStore((s) => s.account);
|
||||
const checkinStatus = usePointsStore((s) => s.checkinStatus);
|
||||
const refreshPoints = usePointsStore((s) => s.refresh);
|
||||
const doCheckin = usePointsStore((s) => s.doCheckin);
|
||||
const [products, setProducts] = useState<PointsProduct[]>([]);
|
||||
const [productType, setProductType] = useState('');
|
||||
const [page, setPage] = useState(1);
|
||||
@@ -82,10 +87,10 @@ export default function Mall() {
|
||||
[currentPatient, loadPatients, refreshPoints, fetchProducts, productType],
|
||||
);
|
||||
|
||||
useDidShow(() => {
|
||||
useThrottledDidShow(() => {
|
||||
Taro.setNavigationBarTitle({ title: '积分商城' });
|
||||
loadAll();
|
||||
});
|
||||
}, 10000);
|
||||
|
||||
usePullDownRefresh(() => {
|
||||
loadAll().finally(() => {
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
|
||||
.msg-segment-tab {
|
||||
flex: 1;
|
||||
height: 40px;
|
||||
height: 48px;
|
||||
border-radius: $r-xs;
|
||||
@include flex-center;
|
||||
position: relative;
|
||||
@@ -119,8 +119,8 @@
|
||||
}
|
||||
|
||||
.consult-avatar {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: $r-pill;
|
||||
background: $surface-alt;
|
||||
@include flex-center;
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { useState, useRef } from 'react';
|
||||
import { View, Text } from '@tarojs/components';
|
||||
import Taro, { useDidShow, useReachBottom } from '@tarojs/taro';
|
||||
import Taro, { useReachBottom } from '@tarojs/taro';
|
||||
import { listConsultations, ConsultationSession } from '../../services/consultation';
|
||||
import { notificationService } from '../../services/notification';
|
||||
import Loading from '../../components/Loading';
|
||||
import GuestGuard from '../../components/GuestGuard';
|
||||
import { useAuthStore } from '../../stores/auth';
|
||||
import { useElderClass } from '../../hooks/useElderClass';
|
||||
import { useThrottledDidShow } from '@/hooks/useThrottledDidShow';
|
||||
import './index.scss';
|
||||
|
||||
type MsgTab = 'consultation' | 'notification';
|
||||
@@ -76,9 +77,9 @@ export default function Messages() {
|
||||
}
|
||||
};
|
||||
|
||||
useDidShow(() => {
|
||||
useThrottledDidShow(() => {
|
||||
if (user) loadData(activeTab, 1, true);
|
||||
});
|
||||
}, 5000);
|
||||
|
||||
const handleTabChange = (tab: MsgTab) => {
|
||||
setActiveTab(tab);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState, useCallback, useRef } from 'react';
|
||||
import { View, Text } from '@tarojs/components';
|
||||
import Taro, { useDidShow, usePullDownRefresh } from '@tarojs/taro';
|
||||
import Taro, { usePullDownRefresh } from '@tarojs/taro';
|
||||
import { useThrottledDidShow } from '@/hooks/useThrottledDidShow';
|
||||
import { listPatientAlerts, type Alert } from '@/services/alert';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import Loading from '@/components/Loading';
|
||||
@@ -23,7 +24,7 @@ const STATUS_TABS = [
|
||||
|
||||
export default function PatientAlerts() {
|
||||
const modeClass = useElderClass();
|
||||
const { currentPatient } = useAuthStore();
|
||||
const currentPatient = useAuthStore((s) => s.currentPatient);
|
||||
const [alerts, setAlerts] = useState<Alert[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
@@ -60,10 +61,10 @@ export default function PatientAlerts() {
|
||||
[currentPatient],
|
||||
);
|
||||
|
||||
useDidShow(() => {
|
||||
useThrottledDidShow(() => {
|
||||
Taro.setNavigationBarTitle({ title: '健康告警' });
|
||||
fetchAlerts(1, status, true);
|
||||
});
|
||||
}, 10000);
|
||||
|
||||
usePullDownRefresh(() => {
|
||||
fetchAlerts(1, status, true).finally(() => Taro.stopPullDownRefresh());
|
||||
|
||||
@@ -270,7 +270,7 @@
|
||||
|
||||
.dm-submit-text {
|
||||
font-size: var(--tk-font-num);
|
||||
color: white;
|
||||
color: $white;
|
||||
font-weight: bold;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ const FIELD_LABELS: Record<string, string> = {
|
||||
|
||||
export default function DailyMonitoring() {
|
||||
const modeClass = useElderClass();
|
||||
const { currentPatient } = useAuthStore();
|
||||
const currentPatient = useAuthStore((s) => s.currentPatient);
|
||||
|
||||
const today = formatDate(new Date());
|
||||
const [dateIdx, setDateIdx] = useState(0);
|
||||
|
||||
@@ -226,7 +226,7 @@
|
||||
|
||||
.input-submit-text {
|
||||
font-size: var(--tk-font-num);
|
||||
color: white;
|
||||
color: $white;
|
||||
font-weight: bold;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import { View, Text, Input, Picker } from '@tarojs/components';
|
||||
import Taro, { useDidShow } from '@tarojs/taro';
|
||||
import Taro from '@tarojs/taro';
|
||||
import { useThrottledDidShow } from '@/hooks/useThrottledDidShow';
|
||||
import { num, validateStr } from '@/utils/validate';
|
||||
import { inputVitalSign, getHealthThresholds, findThreshold, DEFAULT_THRESHOLDS, type HealthThreshold } from '../../../services/health';
|
||||
import { useAuthStore } from '../../../stores/auth';
|
||||
@@ -9,6 +10,7 @@ import { usePointsStore } from '@/stores/points';
|
||||
import { clearRequestCache } from '@/services/request';
|
||||
import { trackEvent } from '@/services/analytics';
|
||||
import { useElderClass } from '../../../hooks/useElderClass';
|
||||
import Loading from '../../../components/Loading';
|
||||
import './index.scss';
|
||||
|
||||
const INDICATORS = [
|
||||
@@ -59,12 +61,14 @@ export default function HealthInput() {
|
||||
const [diastolic, setDiastolic] = useState('');
|
||||
const [note, setNote] = useState('');
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const { currentPatient } = useAuthStore();
|
||||
const { clearCache } = useHealthStore();
|
||||
const [loadingThresholds, setLoadingThresholds] = useState(true);
|
||||
const currentPatient = useAuthStore((s) => s.currentPatient);
|
||||
const clearCache = useHealthStore((s) => s.clearCache);
|
||||
|
||||
/** 从 storage 中读取设备同步回传的数据并自动填充表单 */
|
||||
useDidShow(() => {
|
||||
getHealthThresholds().then((t) => { if (t.length > 0) setThresholds(t); });
|
||||
useThrottledDidShow(() => {
|
||||
setLoadingThresholds(true);
|
||||
getHealthThresholds().then((t) => { if (t.length > 0) setThresholds(t); }).finally(() => setLoadingThresholds(false));
|
||||
try {
|
||||
const raw = Taro.getStorageSync('device_sync_result');
|
||||
if (!raw) return;
|
||||
@@ -88,7 +92,7 @@ export default function HealthInput() {
|
||||
} catch {
|
||||
// 解析失败则忽略,不影响正常使用
|
||||
}
|
||||
});
|
||||
}, 10000);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!currentPatient) {
|
||||
@@ -164,6 +168,10 @@ export default function HealthInput() {
|
||||
|
||||
return (
|
||||
<View className={`input-page ${modeClass}`}>
|
||||
{loadingThresholds && <Loading />}
|
||||
|
||||
{!loadingThresholds && (
|
||||
<>
|
||||
{/* 页面标题 */}
|
||||
<View className='input-hero'>
|
||||
<View className='input-hero-icon'>
|
||||
@@ -267,6 +275,8 @@ export default function HealthInput() {
|
||||
>
|
||||
<Text className='input-submit-text'>{submitting ? '提交中...' : '提交录入'}</Text>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
}
|
||||
|
||||
.trange-tab-text-active {
|
||||
color: white;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
/* ── chart card ── */
|
||||
|
||||
@@ -31,7 +31,7 @@ export default function Trend() {
|
||||
const [range, setRange] = useState('7d');
|
||||
const [points, setPoints] = useState<{ date: string; value: number }[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const { getTrend } = useHealthStore();
|
||||
const getTrend = useHealthStore((s) => s.getTrend);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState, useCallback, useRef } from 'react';
|
||||
import { View, Text } from '@tarojs/components';
|
||||
import Taro, { useDidShow, useReachBottom, usePullDownRefresh } from '@tarojs/taro';
|
||||
import Taro, { useReachBottom, usePullDownRefresh } from '@tarojs/taro';
|
||||
import { useThrottledDidShow } from '@/hooks/useThrottledDidShow';
|
||||
import { listMyTransactions } from '../../../services/points';
|
||||
import type { PointsTransaction } from '../../../services/points';
|
||||
import { usePointsStore } from '../../../stores/points';
|
||||
@@ -17,7 +18,8 @@ const TYPE_TABS = [
|
||||
|
||||
export default function PointsDetail() {
|
||||
const modeClass = useElderClass();
|
||||
const { account, refresh: refreshPoints } = usePointsStore();
|
||||
const account = usePointsStore((s) => s.account);
|
||||
const refreshPoints = usePointsStore((s) => s.refresh);
|
||||
const [transactions, setTransactions] = useState<PointsTransaction[]>([]);
|
||||
const [activeTab, setActiveTab] = useState('');
|
||||
const [page, setPage] = useState(1);
|
||||
@@ -64,10 +66,10 @@ export default function PointsDetail() {
|
||||
[refreshPoints, fetchTransactions, activeTab],
|
||||
);
|
||||
|
||||
useDidShow(() => {
|
||||
useThrottledDidShow(() => {
|
||||
Taro.setNavigationBarTitle({ title: '积分明细' });
|
||||
loadAll();
|
||||
});
|
||||
}, 10000);
|
||||
|
||||
usePullDownRefresh(() => {
|
||||
loadAll().finally(() => {
|
||||
|
||||
@@ -196,6 +196,6 @@
|
||||
|
||||
.confirm-btn-text {
|
||||
font-size: var(--tk-font-num);
|
||||
color: white;
|
||||
color: $white;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { View, Text } from '@tarojs/components';
|
||||
import Taro, { useDidShow } from '@tarojs/taro';
|
||||
import Taro from '@tarojs/taro';
|
||||
import { useThrottledDidShow } from '@/hooks/useThrottledDidShow';
|
||||
import {
|
||||
listProducts,
|
||||
exchangeProduct,
|
||||
@@ -32,14 +33,15 @@ const TYPE_CLASS: Record<string, string> = {
|
||||
export default function ExchangeConfirm() {
|
||||
const modeClass = useElderClass();
|
||||
const [product, setProduct] = useState<PointsProduct | null>(null);
|
||||
const { account, refresh: refreshPoints } = usePointsStore();
|
||||
const account = usePointsStore((s) => s.account);
|
||||
const refreshPoints = usePointsStore((s) => s.refresh);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
useDidShow(() => {
|
||||
useThrottledDidShow(() => {
|
||||
Taro.setNavigationBarTitle({ title: '确认兑换' });
|
||||
loadData();
|
||||
});
|
||||
}, 10000);
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
const instance = Taro.getCurrentInstance();
|
||||
@@ -101,7 +103,7 @@ export default function ExchangeConfirm() {
|
||||
showCancel: false,
|
||||
confirmText: '查看订单',
|
||||
success: () => {
|
||||
Taro.navigateTo({
|
||||
Taro.redirectTo({
|
||||
url: `/pages/pkg-mall/orders/index`,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState, useCallback, useRef } from 'react';
|
||||
import { View, Text } from '@tarojs/components';
|
||||
import Taro, { useDidShow, useReachBottom, usePullDownRefresh } from '@tarojs/taro';
|
||||
import Taro, { useReachBottom, usePullDownRefresh } from '@tarojs/taro';
|
||||
import { useThrottledDidShow } from '@/hooks/useThrottledDidShow';
|
||||
import { listMyOrders } from '../../../services/points';
|
||||
import type { PointsOrder } from '../../../services/points';
|
||||
import EmptyState from '../../../components/EmptyState';
|
||||
@@ -70,10 +71,10 @@ export default function MallOrders() {
|
||||
[fetchOrders, activeTab],
|
||||
);
|
||||
|
||||
useDidShow(() => {
|
||||
useThrottledDidShow(() => {
|
||||
Taro.setNavigationBarTitle({ title: '我的订单' });
|
||||
loadAll();
|
||||
});
|
||||
}, 10000);
|
||||
|
||||
usePullDownRefresh(() => {
|
||||
loadAll().finally(() => {
|
||||
@@ -133,7 +134,7 @@ export default function MallOrders() {
|
||||
text='暂无订单'
|
||||
hint='去商城兑换心仪商品吧'
|
||||
actionText='去商城'
|
||||
onAction={() => Taro.redirectTo({ url: '/pages/mall/index' })}
|
||||
onAction={() => Taro.switchTab({ url: '/pages/mall/index' })}
|
||||
/>
|
||||
) : (
|
||||
<View className='order-list'>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { View, Text } from '@tarojs/components';
|
||||
import Taro, { useDidShow, usePullDownRefresh, useReachBottom } from '@tarojs/taro';
|
||||
import Taro, { usePullDownRefresh, useReachBottom } from '@tarojs/taro';
|
||||
import { useThrottledDidShow } from '@/hooks/useThrottledDidShow';
|
||||
import { listConsents, revokeConsent } from '@/services/consent';
|
||||
import type { Consent } from '@/services/consent';
|
||||
import EmptyState from '@/components/EmptyState';
|
||||
@@ -29,13 +30,16 @@ export default function ConsentList() {
|
||||
const [total, setTotal] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [revoking, setRevoking] = useState<string | null>(null);
|
||||
const [hasPatient, setHasPatient] = useState(true);
|
||||
|
||||
const fetchData = useCallback(async (p: number, append = false) => {
|
||||
const patientId = Taro.getStorageSync('current_patient_id') || '';
|
||||
if (!patientId) {
|
||||
setConsents([]);
|
||||
setHasPatient(false);
|
||||
return;
|
||||
}
|
||||
setHasPatient(true);
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await listConsents(patientId, { page: p, page_size: 20 });
|
||||
@@ -50,7 +54,7 @@ export default function ConsentList() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
useDidShow(() => { fetchData(1); });
|
||||
useThrottledDidShow(() => { fetchData(1); }, 10000);
|
||||
|
||||
usePullDownRefresh(() => {
|
||||
fetchData(1).finally(() => Taro.stopPullDownRefresh());
|
||||
@@ -118,7 +122,7 @@ export default function ConsentList() {
|
||||
</View>
|
||||
|
||||
{consents.length === 0 && !loading && (
|
||||
<EmptyState text={Taro.getStorageSync('current_patient_id') ? '暂无知情同意记录' : '请先在就诊人管理中选择就诊人'} />
|
||||
<EmptyState text={hasPatient ? '暂无知情同意记录' : '请先在就诊人管理中选择就诊人'} />
|
||||
)}
|
||||
|
||||
{loading && <Loading />}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { View, Text } from '@tarojs/components';
|
||||
import Taro, { useDidShow, usePullDownRefresh, useReachBottom } from '@tarojs/taro';
|
||||
import Taro, { usePullDownRefresh, useReachBottom } from '@tarojs/taro';
|
||||
import { useThrottledDidShow } from '@/hooks/useThrottledDidShow';
|
||||
import { listDiagnoses, Diagnosis } from '../../../services/health-record';
|
||||
import EmptyState from '../../../components/EmptyState';
|
||||
import Loading from '../../../components/Loading';
|
||||
@@ -25,13 +26,16 @@ export default function Diagnoses() {
|
||||
const [page, setPage] = useState(1);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [hasPatient, setHasPatient] = useState(true);
|
||||
|
||||
const fetchData = useCallback(async (p: number, append = false) => {
|
||||
const patientId = Taro.getStorageSync('current_patient_id') || '';
|
||||
if (!patientId) {
|
||||
setRecords([]);
|
||||
setHasPatient(false);
|
||||
return;
|
||||
}
|
||||
setHasPatient(true);
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await listDiagnoses(patientId, { page: p, page_size: 20 });
|
||||
@@ -46,9 +50,9 @@ export default function Diagnoses() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
useDidShow(() => {
|
||||
useThrottledDidShow(() => {
|
||||
fetchData(1);
|
||||
});
|
||||
}, 10000);
|
||||
|
||||
usePullDownRefresh(() => {
|
||||
fetchData(1).finally(() => {
|
||||
@@ -94,7 +98,7 @@ export default function Diagnoses() {
|
||||
</View>
|
||||
|
||||
{records.length === 0 && !loading && (
|
||||
<EmptyState text={Taro.getStorageSync('current_patient_id') ? '暂无诊断记录' : '请先在就诊人管理中选择就诊人'} />
|
||||
<EmptyState text={hasPatient ? '暂无诊断记录' : '请先在就诊人管理中选择就诊人'} />
|
||||
)}
|
||||
|
||||
{loading && <Loading />}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { View, Text } from '@tarojs/components';
|
||||
import Taro, { useDidShow, usePullDownRefresh, useReachBottom } from '@tarojs/taro';
|
||||
import Taro, { usePullDownRefresh, useReachBottom } from '@tarojs/taro';
|
||||
import { useThrottledDidShow } from '@/hooks/useThrottledDidShow';
|
||||
import { listDialysisPrescriptions } from '@/services/dialysis';
|
||||
import type { DialysisPrescription } from '@/services/dialysis';
|
||||
import EmptyState from '@/components/EmptyState';
|
||||
@@ -20,13 +21,16 @@ export default function DialysisPrescriptionList() {
|
||||
const [page, setPage] = useState(1);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [hasPatient, setHasPatient] = useState(true);
|
||||
|
||||
const fetchData = useCallback(async (p: number, append = false) => {
|
||||
const patientId = Taro.getStorageSync('current_patient_id') || '';
|
||||
if (!patientId) {
|
||||
setPrescriptions([]);
|
||||
setHasPatient(false);
|
||||
return;
|
||||
}
|
||||
setHasPatient(true);
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await listDialysisPrescriptions({ patient_id: patientId, page: p, page_size: 20 });
|
||||
@@ -41,7 +45,7 @@ export default function DialysisPrescriptionList() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
useDidShow(() => { fetchData(1); });
|
||||
useThrottledDidShow(() => { fetchData(1); }, 10000);
|
||||
|
||||
usePullDownRefresh(() => {
|
||||
fetchData(1).finally(() => Taro.stopPullDownRefresh());
|
||||
@@ -91,7 +95,7 @@ export default function DialysisPrescriptionList() {
|
||||
</View>
|
||||
|
||||
{prescriptions.length === 0 && !loading && (
|
||||
<EmptyState text={Taro.getStorageSync('current_patient_id') ? '暂无透析处方' : '请先在就诊人管理中选择就诊人'} />
|
||||
<EmptyState text={hasPatient ? '暂无透析处方' : '请先在就诊人管理中选择就诊人'} />
|
||||
)}
|
||||
|
||||
{loading && <Loading />}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { View, Text } from '@tarojs/components';
|
||||
import Taro, { useDidShow, usePullDownRefresh, useReachBottom } from '@tarojs/taro';
|
||||
import Taro, { usePullDownRefresh, useReachBottom } from '@tarojs/taro';
|
||||
import { useThrottledDidShow } from '@/hooks/useThrottledDidShow';
|
||||
import { listDialysisRecords } from '@/services/dialysis';
|
||||
import type { DialysisRecord } from '@/services/dialysis';
|
||||
import EmptyState from '@/components/EmptyState';
|
||||
@@ -26,13 +27,16 @@ export default function DialysisRecordList() {
|
||||
const [page, setPage] = useState(1);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [hasPatient, setHasPatient] = useState(true);
|
||||
|
||||
const fetchData = useCallback(async (p: number, append = false) => {
|
||||
const patientId = Taro.getStorageSync('current_patient_id') || '';
|
||||
if (!patientId) {
|
||||
setRecords([]);
|
||||
setHasPatient(false);
|
||||
return;
|
||||
}
|
||||
setHasPatient(true);
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await listDialysisRecords(patientId, { page: p, page_size: 20 });
|
||||
@@ -47,7 +51,7 @@ export default function DialysisRecordList() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
useDidShow(() => { fetchData(1); });
|
||||
useThrottledDidShow(() => { fetchData(1); }, 10000);
|
||||
|
||||
usePullDownRefresh(() => {
|
||||
fetchData(1).finally(() => Taro.stopPullDownRefresh());
|
||||
@@ -96,7 +100,7 @@ export default function DialysisRecordList() {
|
||||
</View>
|
||||
|
||||
{records.length === 0 && !loading && (
|
||||
<EmptyState text={Taro.getStorageSync('current_patient_id') ? '暂无透析记录' : '请先在就诊人管理中选择就诊人'} />
|
||||
<EmptyState text={hasPatient ? '暂无透析记录' : '请先在就诊人管理中选择就诊人'} />
|
||||
)}
|
||||
|
||||
{loading && <Loading />}
|
||||
|
||||
@@ -34,6 +34,7 @@ export default function FamilyAdd() {
|
||||
return;
|
||||
}
|
||||
setSubmitting(true);
|
||||
Taro.showLoading({ title: '提交中...' });
|
||||
try {
|
||||
if (editId && editData) {
|
||||
await updatePatient(editId, {
|
||||
@@ -42,6 +43,7 @@ export default function FamilyAdd() {
|
||||
birth_date: birthDate || undefined,
|
||||
relation: RELATION_OPTIONS[relationIdx],
|
||||
}, editData.version);
|
||||
Taro.hideLoading();
|
||||
Taro.showToast({ title: '修改成功', icon: 'success' });
|
||||
} else {
|
||||
await createPatient({
|
||||
@@ -49,10 +51,12 @@ export default function FamilyAdd() {
|
||||
gender: GENDER_OPTIONS[genderIdx] === '男' ? 'male' : 'female',
|
||||
birth_date: birthDate || undefined,
|
||||
});
|
||||
Taro.hideLoading();
|
||||
Taro.showToast({ title: '添加成功', icon: 'success' });
|
||||
}
|
||||
setTimeout(() => Taro.navigateBack(), 1000);
|
||||
} catch {
|
||||
Taro.hideLoading();
|
||||
Taro.showToast({ title: editId ? '修改失败' : '添加失败', icon: 'none' });
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { View, Text } from '@tarojs/components';
|
||||
import Taro, { useDidShow } from '@tarojs/taro';
|
||||
import Taro from '@tarojs/taro';
|
||||
import { useThrottledDidShow } from '@/hooks/useThrottledDidShow';
|
||||
import { listPatients, Patient } from '../../../services/patient';
|
||||
import { useAuthStore } from '../../../stores/auth';
|
||||
import EmptyState from '../../../components/EmptyState';
|
||||
@@ -11,7 +12,8 @@ export default function FamilyList() {
|
||||
const modeClass = useElderClass();
|
||||
const [patients, setPatients] = useState<Patient[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { currentPatient, setCurrentPatient } = useAuthStore();
|
||||
const currentPatient = useAuthStore((s) => s.currentPatient);
|
||||
const setCurrentPatient = useAuthStore((s) => s.setCurrentPatient);
|
||||
|
||||
const fetchPatients = useCallback(async () => {
|
||||
setLoading(true);
|
||||
@@ -25,9 +27,9 @@ export default function FamilyList() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
useDidShow(() => {
|
||||
useThrottledDidShow(() => {
|
||||
fetchPatients();
|
||||
});
|
||||
}, 10000);
|
||||
|
||||
const handleSelect = (patient: Patient) => {
|
||||
setCurrentPatient({
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { View, Text } from '@tarojs/components';
|
||||
import Taro, { useDidShow } from '@tarojs/taro';
|
||||
import Taro from '@tarojs/taro';
|
||||
import { useThrottledDidShow } from '@/hooks/useThrottledDidShow';
|
||||
import { listTasks, FollowUpTask } from '../../../services/followup';
|
||||
import EmptyState from '../../../components/EmptyState';
|
||||
import Loading from '../../../components/Loading';
|
||||
@@ -31,9 +32,9 @@ export default function MyFollowUps() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
useDidShow(() => {
|
||||
useThrottledDidShow(() => {
|
||||
fetchTasks(activeTab);
|
||||
});
|
||||
}, 10000);
|
||||
|
||||
const handleTabChange = (key: string) => {
|
||||
setActiveTab(key);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { View, Text } from '@tarojs/components';
|
||||
import Taro, { useDidShow, usePullDownRefresh, useReachBottom } from '@tarojs/taro';
|
||||
import Taro, { usePullDownRefresh, useReachBottom } from '@tarojs/taro';
|
||||
import { useThrottledDidShow } from '@/hooks/useThrottledDidShow';
|
||||
import { listHealthRecords, HealthRecord } from '../../../services/health-record';
|
||||
import EmptyState from '../../../components/EmptyState';
|
||||
import Loading from '../../../components/Loading';
|
||||
@@ -19,13 +20,16 @@ export default function HealthRecords() {
|
||||
const [page, setPage] = useState(1);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [hasPatient, setHasPatient] = useState(true);
|
||||
|
||||
const fetchData = useCallback(async (p: number, append = false) => {
|
||||
const patientId = Taro.getStorageSync('current_patient_id') || '';
|
||||
if (!patientId) {
|
||||
setRecords([]);
|
||||
setHasPatient(false);
|
||||
return;
|
||||
}
|
||||
setHasPatient(true);
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await listHealthRecords(patientId, { page: p, page_size: 20 });
|
||||
@@ -40,9 +44,9 @@ export default function HealthRecords() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
useDidShow(() => {
|
||||
useThrottledDidShow(() => {
|
||||
fetchData(1);
|
||||
});
|
||||
}, 10000);
|
||||
|
||||
usePullDownRefresh(() => {
|
||||
fetchData(1).finally(() => {
|
||||
@@ -83,7 +87,7 @@ export default function HealthRecords() {
|
||||
</View>
|
||||
|
||||
{records.length === 0 && !loading && (
|
||||
<EmptyState text={Taro.getStorageSync('current_patient_id') ? '暂无健康记录' : '请先在就诊人管理中选择就诊人'} />
|
||||
<EmptyState text={hasPatient ? '暂无健康记录' : '请先在就诊人管理中选择就诊人'} />
|
||||
)}
|
||||
|
||||
{loading && <Loading />}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { View, Text } from '@tarojs/components';
|
||||
import Taro, { useDidShow, usePullDownRefresh, useReachBottom } from '@tarojs/taro';
|
||||
import Taro, { usePullDownRefresh, useReachBottom } from '@tarojs/taro';
|
||||
import { useThrottledDidShow } from '@/hooks/useThrottledDidShow';
|
||||
import { listReports, LabReport } from '../../../services/report';
|
||||
import EmptyState from '../../../components/EmptyState';
|
||||
import Loading from '../../../components/Loading';
|
||||
@@ -13,13 +14,16 @@ export default function MyReports() {
|
||||
const [page, setPage] = useState(1);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [hasPatient, setHasPatient] = useState(true);
|
||||
|
||||
const fetchData = useCallback(async (p: number, append = false) => {
|
||||
const patientId = Taro.getStorageSync('current_patient_id') || '';
|
||||
if (!patientId) {
|
||||
setReports([]);
|
||||
setHasPatient(false);
|
||||
return;
|
||||
}
|
||||
setHasPatient(true);
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await listReports(patientId, p);
|
||||
@@ -34,9 +38,9 @@ export default function MyReports() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
useDidShow(() => {
|
||||
useThrottledDidShow(() => {
|
||||
fetchData(1);
|
||||
});
|
||||
}, 10000);
|
||||
|
||||
usePullDownRefresh(() => {
|
||||
fetchData(1).finally(() => {
|
||||
@@ -97,7 +101,7 @@ export default function MyReports() {
|
||||
</View>
|
||||
|
||||
{reports.length === 0 && !loading && (
|
||||
<EmptyState text={Taro.getStorageSync('current_patient_id') ? '暂无报告记录' : '请先在就诊人管理中选择就诊人'} />
|
||||
<EmptyState text={hasPatient ? '暂无报告记录' : '请先在就诊人管理中选择就诊人'} />
|
||||
)}
|
||||
|
||||
{loading && (
|
||||
|
||||
@@ -7,30 +7,35 @@ import './index.scss';
|
||||
|
||||
export default function Settings() {
|
||||
const modeClass = useElderClass();
|
||||
const { logout } = useAuthStore();
|
||||
const logout = useAuthStore((s) => s.logout);
|
||||
|
||||
const handleClearCache = () => {
|
||||
Taro.showModal({
|
||||
const handleClearCache = async () => {
|
||||
const { confirm } = await Taro.showModal({
|
||||
title: '清除缓存',
|
||||
content: '确定要清除本地缓存数据吗?不会影响账号信息。',
|
||||
}).then((res) => {
|
||||
if (res.confirm) {
|
||||
const preservedKeys = ['access_token', 'refresh_token', 'user_data', 'user_roles', 'tenant_id', 'wechat_openid', 'current_patient', 'current_patient_id'];
|
||||
const preservedData: Record<string, unknown> = {};
|
||||
for (const key of preservedKeys) {
|
||||
const val = Taro.getStorageSync(key);
|
||||
if (val) preservedData[key] = val;
|
||||
}
|
||||
|
||||
Taro.clearStorageSync();
|
||||
|
||||
for (const [key, val] of Object.entries(preservedData)) {
|
||||
Taro.setStorageSync(key, val);
|
||||
}
|
||||
|
||||
Taro.showToast({ title: '缓存已清除', icon: 'success' });
|
||||
}
|
||||
});
|
||||
if (!confirm) return;
|
||||
|
||||
const preservedKeys = ['access_token', 'refresh_token', 'user_data', 'user_roles', 'tenant_id', 'wechat_openid', 'current_patient', 'current_patient_id'];
|
||||
const preserved: Record<string, unknown> = {};
|
||||
await Promise.all(
|
||||
preservedKeys.map(async (key) => {
|
||||
try {
|
||||
const val = await Taro.getStorage({ key });
|
||||
if (val.data) preserved[key] = val.data;
|
||||
} catch { /* key not found */ }
|
||||
}),
|
||||
);
|
||||
|
||||
await Taro.clearStorage();
|
||||
|
||||
await Promise.all(
|
||||
Object.entries(preserved).map(([key, val]) =>
|
||||
Taro.setStorage({ key, data: val }),
|
||||
),
|
||||
);
|
||||
|
||||
Taro.showToast({ title: '缓存已清除', icon: 'success' });
|
||||
};
|
||||
|
||||
const handleAbout = () => {
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { View, Text } from '@tarojs/components';
|
||||
import Taro, { useDidShow } from '@tarojs/taro';
|
||||
import Taro from '@tarojs/taro';
|
||||
import { useState } from 'react';
|
||||
import { useAuthStore } from '../../stores/auth';
|
||||
import { usePointsStore } from '../../stores/points';
|
||||
import { useUIStore } from '../../stores/ui';
|
||||
import { navigateToLogin } from '../../utils/navigate';
|
||||
import { useThrottledDidShow } from '@/hooks/useThrottledDidShow';
|
||||
import Loading from '../../components/Loading';
|
||||
import './index.scss';
|
||||
|
||||
interface MenuItem {
|
||||
@@ -76,16 +79,23 @@ const GUEST_GROUPS: MenuGroup[] = [
|
||||
];
|
||||
|
||||
export default function Profile() {
|
||||
const { user, logout } = useAuthStore();
|
||||
const { account: pointsAccount, checkinStatus: checkinInfo, refresh: refreshPoints } = usePointsStore();
|
||||
const user = useAuthStore((s) => s.user);
|
||||
const logout = useAuthStore((s) => s.logout);
|
||||
const pointsAccount = usePointsStore((s) => s.account);
|
||||
const checkinInfo = usePointsStore((s) => s.checkinStatus);
|
||||
const refreshPoints = usePointsStore((s) => s.refresh);
|
||||
const mode = useUIStore((s) => s.mode);
|
||||
const modeClass = mode === 'elder' ? 'elder-mode' : '';
|
||||
const isGuest = !user;
|
||||
const groups = isGuest ? GUEST_GROUPS : LOGGED_IN_GROUPS;
|
||||
const [pointsLoading, setPointsLoading] = useState(false);
|
||||
|
||||
useDidShow(() => {
|
||||
if (!isGuest) refreshPoints();
|
||||
});
|
||||
useThrottledDidShow(() => {
|
||||
if (!isGuest) {
|
||||
setPointsLoading(true);
|
||||
refreshPoints().finally(() => setPointsLoading(false));
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
const handleMenuClick = (item: MenuItem) => {
|
||||
if (item.isSwitchTab) {
|
||||
@@ -139,6 +149,9 @@ export default function Profile() {
|
||||
</View>
|
||||
|
||||
{/* 积分 + 打卡 */}
|
||||
{pointsLoading ? (
|
||||
<Loading />
|
||||
) : (
|
||||
<View className='profile-stats-row'>
|
||||
<View className='stat-card'>
|
||||
<Text className='stat-value stat-pri'>{(pointsAccount?.balance ?? 0).toLocaleString()}</Text>
|
||||
@@ -149,6 +162,7 @@ export default function Profile() {
|
||||
<Text className='stat-label'>连续打卡</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import Taro, { useRouter } from '@tarojs/taro';
|
||||
import { getReportDetail, LabReport } from '../../../services/report';
|
||||
import Loading from '../../../components/Loading';
|
||||
import { useElderClass } from '../../../hooks/useElderClass';
|
||||
import { useAuthStore } from '../../../stores/auth';
|
||||
import './index.scss';
|
||||
|
||||
interface IndicatorItem {
|
||||
@@ -19,7 +20,8 @@ export default function ReportDetail() {
|
||||
const modeClass = useElderClass();
|
||||
const router = useRouter();
|
||||
const id = router.params.id || '';
|
||||
const patientId = Taro.getStorageSync('current_patient_id') || '';
|
||||
const currentPatient = useAuthStore((s) => s.currentPatient);
|
||||
const patientId = currentPatient?.id || '';
|
||||
|
||||
const [report, setReport] = useState<LabReport | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
Reference in New Issue
Block a user