Files
hms/apps/miniprogram/src/pages/consultation/index.tsx
iven d576b8ba8f fix(mp): 空 catch 块添加 console.warn 日志(82 处)
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 断连清理、用户拒绝订阅等合理忽略场景
2026-05-21 13:44:13 +08:00

245 lines
8.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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'>&#x26A0;</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>
);
}