Files
hms/apps/miniprogram/src/pages/messages/index.tsx
iven 085163ec7a
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled
feat(miniprogram): 访客模式 + 长辈模式 + MCP 自动化脚本
访客模式:
- 未登录用户可见首页(轮播图+健康资讯+登录引导)和"我的"页面
- 健康和消息 tab 显示 GuestGuard 登录拦截
- 登录页增加"暂不登录,先看看"跳过入口
- 401 拦截器增加 hasToken 检查,避免访客被重定向到登录页
- 退出登录后 reLaunch 到首页而非登录页

长辈模式:
- 新增 stores/ui.ts 管理显示模式(标准/长辈)
- 长辈模式放大字体 ×1.3、间距 ×1.2、按钮加大
- "我的 → 账号 → 长辈模式"切换页
- 设置持久化到 Storage

修复:
- Health/Messages 页面 Hooks 顺序违规(条件 return 在 hooks 之间)
  导致访客模式下页面白屏,所有 hooks 移到条件判断之前

工程:
- scripts/mpsync.sh/ps1 自动清理残留 DevTools 进程
- project.config.json 默认关闭域名校验
2026-05-09 11:42:44 +08:00

224 lines
8.3 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 './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 [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'>
{/* 页头 */}
<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>
);
}