import { useEffect, useState, useRef, useCallback } from 'react'; import { PageHeader } from '@/components/layout/PageHeader'; import { HubConnectionBuilder, HubConnection, HubConnectionState } from '@microsoft/signalr'; import { api } from '@/services/api-client'; import * as consultationService from '@/services/consultation.service'; import type { Consultation, ConsultationMessage, Doctor } from '@/types'; import { formatRelative } from '@/utils/format'; import styles from './ChatPage.module.css'; function getToken(): string { try { const raw = localStorage.getItem('hrt_auth'); if (!raw) return ''; const state = JSON.parse(raw); return state?.state?.token ?? ''; } catch { return ''; } } export function ChatPage() { const [doctor, setDoctor] = useState(null); const [consultation, setConsultation] = useState(null); const [messages, setMessages] = useState([]); const [text, setText] = useState(''); const [connected, setConnected] = useState(false); const bottomRef = useRef(null); const connRef = useRef(null); const initRef = useRef(false); // Init consultation once useEffect(() => { if (initRef.current) return; initRef.current = true; consultationService.getDoctors().then((docs) => { if (docs.length > 0) { const doc = docs[0]; setDoctor(doc); api.get('/api/consultations').then((res) => { const existing = (res.data as Record[]).find( (c) => c.doctorId === doc.id && c.status === 'active' ); if (existing) { setConsultation(existing as unknown as Consultation); } else { consultationService.startConsultation(doc.id, '在线咨询').then((c) => { setConsultation(c); }); } }); } }); }, []); // Load initial messages + set up SignalR when consultation is ready useEffect(() => { if (!consultation?.id) return; // Load message history api.get(`/api/consultations/${consultation.id}/messages`) .then((res) => setMessages(res.data)) .catch(() => {}); // Set up SignalR connection const conn = new HubConnectionBuilder() .withUrl('http://localhost:5000/hubs/chat', { accessTokenFactory: () => getToken(), }) .withAutomaticReconnect() .build(); conn.on('ReceiveMessage', (msg: ConsultationMessage) => { setMessages((prev) => { if (prev.some((m) => m.id === msg.id)) return prev; return [...prev, msg]; }); }); conn.onreconnected(() => { conn.invoke('JoinConsultation', consultation.id).catch(() => {}); }); conn.start() .then(() => { setConnected(true); return conn.invoke('JoinConsultation', consultation.id); }) .catch(() => {}); connRef.current = conn; return () => { if (conn.state === HubConnectionState.Connected) { conn.invoke('LeaveConsultation', consultation.id).catch(() => {}); } conn.stop(); }; }, [consultation?.id]); // Auto-scroll on new messages useEffect(() => { bottomRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages]); const handleSend = useCallback(async () => { if (!text.trim() || !consultation?.id || !connRef.current) return; const msgText = text; setText(''); try { await connRef.current.invoke('SendMessage', consultation.id, msgText); } catch { /* ignore */ } }, [text, consultation?.id]); return (
} />
{messages.length === 0 && (
您好,我是{doctor?.name || '医生'},请问有什么可以帮您?
)} {messages.map((msg) => (
{msg.content}
{formatRelative(msg.createdAt)}
))}
setText(e.target.value)} placeholder="输入消息..." onKeyDown={(e) => e.key === 'Enter' && handleSend()} />
); }