- 新建 useElderClass hook,替代每页 3 行样板代码 - 新建 CSS 自定义属性 Design Token 系统(tokens.scss) 正常/关怀两套值:字号、间距、触控、布局参数 - 15 个页面批量接入关怀模式 class: TabBar: 商城页 主流程: 预约列表/详情/创建、咨询详情 子包: 体征录入/趋势/日常监测/告警、用药/档案/随访/报告/家庭/设置 - 新建 elder-toast 工具(关怀模式 3s + 触觉反馈) - 页面覆盖率:4/59 → 22/59 (37%) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
226 lines
8.4 KiB
TypeScript
226 lines
8.4 KiB
TypeScript
import { useState, useRef } from 'react';
|
|
import { View, Text } from '@tarojs/components';
|
|
import Taro, { useDidShow, 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 './index.scss';
|
|
|
|
type MsgTab = 'consultation' | 'notification';
|
|
|
|
interface NotificationItem {
|
|
id: string;
|
|
title: string;
|
|
desc: string;
|
|
time: string;
|
|
type: string;
|
|
read?: boolean;
|
|
}
|
|
|
|
const NOTIFY_ICONS: Record<string, { icon: string; bg: string; color: string }> = {
|
|
appointment: { icon: '约', bg: '#F0DDD4', color: '#C4623A' },
|
|
alert: { icon: '警', bg: '#FFF3E0', color: '#C4873A' },
|
|
followup: { icon: '随', bg: '#E8F0E8', color: '#5B7A5E' },
|
|
points: { icon: '分', bg: '#F0DDD4', color: '#C4623A' },
|
|
report: { icon: '报', bg: '#E8F0E8', color: '#5B7A5E' },
|
|
};
|
|
|
|
export default function Messages() {
|
|
const user = useAuthStore((s) => s.user);
|
|
const modeClass = useElderClass();
|
|
const [activeTab, setActiveTab] = useState<MsgTab>('consultation');
|
|
const [sessions, setSessions] = useState<ConsultationSession[]>([]);
|
|
const [notifications, setNotifications] = useState<NotificationItem[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [page, setPage] = useState(1);
|
|
const [total, setTotal] = useState(0);
|
|
const loadingRef = useRef(false);
|
|
|
|
const loadData = async (tab: MsgTab, pageNum: number = 1, isRefresh = false) => {
|
|
if (loadingRef.current) return;
|
|
loadingRef.current = true;
|
|
setLoading(true);
|
|
try {
|
|
if (tab === 'consultation') {
|
|
const res = await listConsultations({ page: pageNum, page_size: 20 });
|
|
const list = res.data || [];
|
|
if (isRefresh) {
|
|
setSessions(list);
|
|
} else {
|
|
setSessions((prev) => [...prev, ...list]);
|
|
}
|
|
setTotal(res.total || 0);
|
|
} else {
|
|
const res = await notificationService.list<{ data: unknown[]; total?: number }>({ page: pageNum, page_size: 20 });
|
|
const list = (res as { data?: unknown[] })?.data || [];
|
|
if (isRefresh) {
|
|
setNotifications(list as NotificationItem[]);
|
|
} else {
|
|
setNotifications((prev) => [...prev, ...(list as NotificationItem[])]);
|
|
}
|
|
setTotal((res as { total?: number })?.total || 0);
|
|
}
|
|
setPage(pageNum);
|
|
} catch {
|
|
if (isRefresh) {
|
|
if (tab === 'consultation') setSessions([]);
|
|
else setNotifications([]);
|
|
}
|
|
} finally {
|
|
setLoading(false);
|
|
loadingRef.current = false;
|
|
}
|
|
};
|
|
|
|
useDidShow(() => {
|
|
if (user) loadData(activeTab, 1, true);
|
|
});
|
|
|
|
const handleTabChange = (tab: MsgTab) => {
|
|
setActiveTab(tab);
|
|
loadData(tab, 1, true);
|
|
};
|
|
|
|
useReachBottom(() => {
|
|
const currentList = activeTab === 'consultation' ? sessions : notifications;
|
|
if (!loading && currentList.length < total) {
|
|
loadData(activeTab, page + 1);
|
|
}
|
|
});
|
|
|
|
const formatTime = (dateStr: string | null) => {
|
|
if (!dateStr) return '';
|
|
const d = new Date(dateStr);
|
|
const now = new Date();
|
|
const diffMs = now.getTime() - d.getTime();
|
|
const diffMin = Math.floor(diffMs / 60000);
|
|
if (diffMin < 60) return `${diffMin} 分钟前`;
|
|
const diffHour = Math.floor(diffMin / 60);
|
|
if (diffHour < 24) return `${diffHour} 小时前`;
|
|
return dateStr.slice(0, 10);
|
|
};
|
|
|
|
if (!user) {
|
|
return <GuestGuard title='请先登录' desc='登录后即可查看消息和通知' />;
|
|
}
|
|
|
|
const unreadConsultCount = sessions.filter((s) => s.unread_count_patient > 0).length;
|
|
|
|
return (
|
|
<View className={`messages-page ${modeClass}`}>
|
|
{/* 页头 */}
|
|
<View className='messages-header'>
|
|
<Text className='messages-title'>消息</Text>
|
|
</View>
|
|
|
|
{/* 分段控件 Tab */}
|
|
<View className='msg-segment'>
|
|
<View
|
|
className={`msg-segment-tab ${activeTab === 'consultation' ? 'msg-segment-active' : ''}`}
|
|
onClick={() => handleTabChange('consultation')}
|
|
>
|
|
<Text className='msg-segment-text'>咨询</Text>
|
|
{unreadConsultCount > 0 && (
|
|
<View className='msg-segment-badge'>
|
|
<Text className='msg-segment-badge-text'>{unreadConsultCount}</Text>
|
|
</View>
|
|
)}
|
|
</View>
|
|
<View
|
|
className={`msg-segment-tab ${activeTab === 'notification' ? 'msg-segment-active' : ''}`}
|
|
onClick={() => handleTabChange('notification')}
|
|
>
|
|
<Text className='msg-segment-text'>通知</Text>
|
|
</View>
|
|
</View>
|
|
|
|
<View className='msg-content'>
|
|
{/* 咨询列表 */}
|
|
{activeTab === 'consultation' && (
|
|
loading ? (
|
|
<Loading />
|
|
) : sessions.length === 0 ? (
|
|
<View className='msg-empty'>
|
|
<Text className='msg-empty-text'>暂无咨询消息</Text>
|
|
</View>
|
|
) : (
|
|
<View className='msg-list'>
|
|
{sessions.map((session) => {
|
|
const doctorName = session.last_message?.slice(0, 1) || '医';
|
|
const hasUnread = session.unread_count_patient > 0;
|
|
return (
|
|
<View
|
|
key={session.id}
|
|
className={`consult-card ${hasUnread ? '' : 'consult-card-muted'}`}
|
|
onClick={() => Taro.navigateTo({ url: `/pages/consultation/detail/index?id=${session.id}` })}
|
|
>
|
|
<View className={`consult-avatar ${hasUnread ? 'consult-avatar-active' : ''}`}>
|
|
<Text className='consult-avatar-char'>{doctorName}</Text>
|
|
</View>
|
|
<View className='consult-body'>
|
|
<View className='consult-row'>
|
|
<Text className='consult-doctor'>
|
|
{session.consultation_type === 'online' ? '在线咨询' : '门诊咨询'}
|
|
</Text>
|
|
<Text className='consult-time'>{formatTime(session.last_message_at)}</Text>
|
|
</View>
|
|
<View className='consult-row'>
|
|
<Text className='consult-preview'>
|
|
{session.last_message || session.subject || '暂无消息'}
|
|
</Text>
|
|
{hasUnread && (
|
|
<View className='consult-badge'>
|
|
<Text className='consult-badge-text'>
|
|
{session.unread_count_patient > 99 ? '99+' : session.unread_count_patient}
|
|
</Text>
|
|
</View>
|
|
)}
|
|
</View>
|
|
</View>
|
|
</View>
|
|
);
|
|
})}
|
|
</View>
|
|
)
|
|
)}
|
|
|
|
{/* 通知列表 */}
|
|
{activeTab === 'notification' && (
|
|
loading ? (
|
|
<Loading />
|
|
) : notifications.length === 0 ? (
|
|
<View className='msg-empty'>
|
|
<Text className='msg-empty-text'>暂无新通知</Text>
|
|
</View>
|
|
) : (
|
|
<View className='msg-list'>
|
|
{notifications.map((n) => {
|
|
const cfg = NOTIFY_ICONS[n.type] || NOTIFY_ICONS.report;
|
|
const isUnread = !n.read;
|
|
return (
|
|
<View key={n.id} className={`notify-card ${isUnread ? '' : 'notify-card-muted'}`}>
|
|
<View className='notify-icon' style={`background:${cfg.bg};`}>
|
|
<Text className='notify-icon-char' style={`color:${cfg.color};`}>{cfg.icon}</Text>
|
|
</View>
|
|
<View className='notify-body'>
|
|
<View className='notify-row'>
|
|
<Text className={`notify-title ${isUnread ? 'notify-title-bold' : ''}`}>{n.title}</Text>
|
|
<Text className='notify-time'>{n.time}</Text>
|
|
</View>
|
|
<Text className='notify-desc'>{n.desc}</Text>
|
|
</View>
|
|
{isUnread && <View className='notify-dot' />}
|
|
</View>
|
|
);
|
|
})}
|
|
</View>
|
|
)
|
|
)}
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|