Files
soft/frontend-patient/src/pages/services/ChatPage.tsx

147 lines
4.9 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 { 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<Doctor | null>(null);
const [consultation, setConsultation] = useState<Consultation | null>(null);
const [messages, setMessages] = useState<ConsultationMessage[]>([]);
const [text, setText] = useState('');
const [connected, setConnected] = useState(false);
const bottomRef = useRef<HTMLDivElement>(null);
const connRef = useRef<HubConnection | null>(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<Consultation[]>('/api/consultations').then((res) => {
const existing = (res.data as Record<string, unknown>[]).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<ConsultationMessage[]>(`/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 (
<div className={styles.page}>
<PageHeader
title={doctor?.name || '在线问诊'}
rightAction={
<span style={{
display: 'inline-block', width: 8, height: 8, borderRadius: '50%',
background: connected ? '#4caf50' : '#ccc',
marginLeft: 8,
}} />
}
/>
<div className={styles.messages}>
{messages.length === 0 && (
<div style={{ textAlign: 'center', color: '#9CA3AF', marginTop: 40, fontSize: 14 }}>
{doctor?.name || '医生'}
</div>
)}
{messages.map((msg) => (
<div key={msg.id} className={`${styles.bubble} ${msg.senderRole === 'patient' ? styles.patient : styles.doctor}`}>
<div className={styles.bubbleContent}>{msg.content}</div>
<div className={styles.bubbleTime}>{formatRelative(msg.createdAt)}</div>
</div>
))}
<div ref={bottomRef} />
</div>
<div className={styles.inputBar}>
<input className={styles.input} value={text} onChange={(e) => setText(e.target.value)}
placeholder="输入消息..." onKeyDown={(e) => e.key === 'Enter' && handleSend()} />
<button className={styles.sendBtn} onClick={handleSend}></button>
</div>
</div>
);
}