55 个文件中 82 处空 catch 块添加模块前缀日志输出: - stores: auth/health/points (7 处) - services: request/ai-chat/health/ble/* (10 处) - hooks: useLongPolling/usePagination (3 处) - pages: 核心+子包共 35 个页面 (62 处) 保留静默的 catch: secure-storage fallback、Storage 恢复、 analytics 防洪、BLE 断连清理、用户拒绝订阅等合理忽略场景
245 lines
8.4 KiB
TypeScript
245 lines
8.4 KiB
TypeScript
import { useState, useCallback } from 'react';
|
||
import { View, Text } from '@tarojs/components';
|
||
import Taro, { useReachBottom } from '@tarojs/taro';
|
||
import { safeNavigateTo } from '@/utils/navigate';
|
||
import { usePageData } from '@/hooks/usePageData';
|
||
import { useAuthStore } from '@/stores/auth';
|
||
import { listConsultations, ConsultationSession } from '@/services/consultation';
|
||
import PageShell from '@/components/ui/PageShell';
|
||
import ContentCard from '@/components/ui/ContentCard';
|
||
import StatusTag from '@/components/ui/StatusTag';
|
||
import LoadingCard from '@/components/ui/LoadingCard';
|
||
import EmptyState from '@/components/EmptyState';
|
||
import ErrorState from '@/components/ErrorState';
|
||
import Loading from '@/components/Loading';
|
||
import GuestGuard from '@/components/GuestGuard';
|
||
import { useElderClass } from '../../hooks/useElderClass';
|
||
import './index.scss';
|
||
|
||
/** 读取当前页面 URL 中的查询参数 */
|
||
function getQueryParams(): Record<string, string> {
|
||
try {
|
||
const instance = Taro.getCurrentInstance();
|
||
const params = instance?.router?.params;
|
||
if (!params) return {};
|
||
const result: Record<string, string> = {};
|
||
for (const [key, value] of Object.entries(params)) {
|
||
if (typeof value === 'string') {
|
||
result[key] = value;
|
||
}
|
||
}
|
||
return result;
|
||
} catch (err) {
|
||
console.warn('[consultation] 解析查询参数失败:', err);
|
||
return {};
|
||
}
|
||
}
|
||
|
||
function formatTime(iso: string): string {
|
||
if (!iso) return '';
|
||
const d = new Date(iso);
|
||
const now = new Date();
|
||
const diffMin = Math.floor((now.getTime() - d.getTime()) / 60000);
|
||
|
||
if (diffMin < 1) return '刚刚';
|
||
if (diffMin < 60) return `${diffMin}分钟前`;
|
||
const diffHour = Math.floor(diffMin / 60);
|
||
if (diffHour < 24) return `${diffHour}小时前`;
|
||
const diffDay = Math.floor(diffHour / 24);
|
||
if (diffDay < 7) return `${diffDay}天前`;
|
||
|
||
const m = String(d.getMonth() + 1).padStart(2, '0');
|
||
const day = String(d.getDate()).padStart(2, '0');
|
||
return `${m}-${day}`;
|
||
}
|
||
|
||
/** 咨询状态到 StatusTag status 的映射 */
|
||
function getConsultStatus(status: string): string {
|
||
if (status === 'active') return 'active';
|
||
if (status === 'pending') return 'pending';
|
||
if (status === 'closed') return 'completed';
|
||
if (status === 'cancelled') return 'cancelled';
|
||
return status;
|
||
}
|
||
|
||
const STATUS_LABEL_MAP: Record<string, string> = {
|
||
active: '进行中',
|
||
pending: '等待接诊',
|
||
closed: '已结束',
|
||
cancelled: '已取消',
|
||
};
|
||
|
||
export default function Consultation() {
|
||
const user = useAuthStore((s) => s.user);
|
||
const [sessions, setSessions] = useState<ConsultationSession[]>([]);
|
||
const [loading, setLoading] = useState(true);
|
||
const [error, setError] = useState('');
|
||
const modeClass = useElderClass();
|
||
const [page, setPage] = useState(1);
|
||
const [total, setTotal] = useState(0);
|
||
|
||
// Alert context: when navigated from an alert page
|
||
const [alertContext, setAlertContext] = useState<{ alertId: string; alertTitle: string } | null>(null);
|
||
|
||
const loadSessions = useCallback(async (pageNum: number, isRefresh = false) => {
|
||
if (isRefresh) setLoading(true);
|
||
setError('');
|
||
try {
|
||
const resp = await listConsultations({ page: pageNum, page_size: 20 });
|
||
const list = resp.data || [];
|
||
if (isRefresh) {
|
||
setSessions(list);
|
||
} else {
|
||
setSessions((prev) => [...prev, ...list]);
|
||
}
|
||
setTotal(resp.total || 0);
|
||
setPage(pageNum);
|
||
} catch (err) {
|
||
console.warn('[consultation] 加载会话列表失败:', err);
|
||
if (isRefresh) {
|
||
setSessions([]);
|
||
setTotal(0);
|
||
}
|
||
setError('加载失败,请稍后重试');
|
||
Taro.showToast({ title: '加载失败,下拉重试', icon: 'none' });
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}, []);
|
||
|
||
usePageData(
|
||
useCallback(async () => {
|
||
Taro.setNavigationBarTitle({ title: '在线咨询' });
|
||
// Read alert context from URL query params
|
||
const params = getQueryParams();
|
||
if (params.context === 'alert' && params.alert_id) {
|
||
setAlertContext({
|
||
alertId: params.alert_id,
|
||
alertTitle: params.alert_title ? decodeURIComponent(params.alert_title) : '健康告警',
|
||
});
|
||
}
|
||
if (!user) return;
|
||
await loadSessions(1, true);
|
||
}, [user, loadSessions]),
|
||
{ throttleMs: 10000, enablePullDown: true },
|
||
);
|
||
|
||
useReachBottom(() => {
|
||
if (!loading && sessions.length < total) {
|
||
loadSessions(page + 1);
|
||
}
|
||
});
|
||
|
||
const handleTapSession = (session: ConsultationSession) => {
|
||
safeNavigateTo(`/pages/pkg-consultation/detail/index?id=${session.id}`);
|
||
};
|
||
|
||
if (!user) {
|
||
return (
|
||
<PageShell safeBottom className={modeClass}>
|
||
<GuestGuard title='请先登录' desc='登录后即可与医生在线交流' />
|
||
</PageShell>
|
||
);
|
||
}
|
||
|
||
if (loading && sessions.length === 0) {
|
||
return <LoadingCard count={4} layout="list" />;
|
||
}
|
||
|
||
if (error && sessions.length === 0) {
|
||
return (
|
||
<PageShell safeBottom className={modeClass}>
|
||
<ErrorState text={error} onRetry={() => loadSessions(1, true)} />
|
||
</PageShell>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<PageShell safeBottom className={modeClass}>
|
||
{/* 副标题 */}
|
||
<Text className='consultation-subtitle'>随时随地,连接专业医生</Text>
|
||
|
||
{/* Alert context banner */}
|
||
{alertContext && (
|
||
<ContentCard className='consultation-alert-banner'>
|
||
<View className='consultation-alert-banner-inner'>
|
||
<Text className='consultation-alert-banner-icon'>⚠</Text>
|
||
<Text className='consultation-alert-banner-text'>
|
||
来自告警: {alertContext.alertTitle}
|
||
</Text>
|
||
</View>
|
||
</ContentCard>
|
||
)}
|
||
|
||
{/* 发起咨询按钮 */}
|
||
<View
|
||
className='consultation-create-btn'
|
||
onClick={() => safeNavigateTo('/pages/consultation/create/index')}
|
||
>
|
||
<Text className='consultation-create-btn-text'>发起咨询</Text>
|
||
</View>
|
||
|
||
{/* 会话列表 */}
|
||
{sessions.length === 0 ? (
|
||
<EmptyState
|
||
icon='问'
|
||
text='暂无咨询记录'
|
||
hint='发起咨询后即可在这里与医生交流'
|
||
/>
|
||
) : (
|
||
<View className='session-list'>
|
||
{sessions.map((session) => {
|
||
const initial = (session.subject || '咨').charAt(0);
|
||
const isClosed = session.status === 'closed' || session.status === 'cancelled';
|
||
return (
|
||
<ContentCard
|
||
key={session.id}
|
||
className={isClosed ? 'session-card-closed' : ''}
|
||
activeFeedback="opacity"
|
||
onPress={() => handleTapSession(session)}
|
||
>
|
||
<View className='session-inner'>
|
||
<View className='session-avatar'>
|
||
<Text className='session-avatar-char'>{initial}</Text>
|
||
</View>
|
||
<View className='session-body'>
|
||
<View className='session-top'>
|
||
<Text className='session-subject'>
|
||
{session.doctor_name || session.subject || '在线咨询'}
|
||
</Text>
|
||
<Text className='session-time'>
|
||
{session.last_message_at
|
||
? formatTime(session.last_message_at)
|
||
: formatTime(session.created_at)}
|
||
</Text>
|
||
</View>
|
||
<View className='session-meta'>
|
||
<StatusTag status={getConsultStatus(session.status)} size="sm">
|
||
{STATUS_LABEL_MAP[session.status] || session.status}
|
||
</StatusTag>
|
||
</View>
|
||
<View className='session-message-row'>
|
||
<Text className='session-message'>
|
||
{session.last_message || '暂无消息'}
|
||
</Text>
|
||
{session.unread_count_patient > 0 && (
|
||
<View className='session-badge'>
|
||
<Text className='session-badge-text'>
|
||
{session.unread_count_patient > 99 ? '99+' : session.unread_count_patient}
|
||
</Text>
|
||
</View>
|
||
)}
|
||
</View>
|
||
</View>
|
||
</View>
|
||
</ContentCard>
|
||
);
|
||
})}
|
||
</View>
|
||
)}
|
||
|
||
{loading && sessions.length > 0 && <Loading />}
|
||
</PageShell>
|
||
);
|
||
}
|