Backend: .NET 10 + PostgreSQL + EF Core + JWT + SignalR Frontend patient: React 19 + TypeScript + Vite (mobile H5) Frontend doctor: React 19 + TypeScript + Vite (desktop web)
82 lines
2.9 KiB
TypeScript
82 lines
2.9 KiB
TypeScript
import { useEffect, useState, useRef } from 'react';
|
|
import { useParams } from 'react-router-dom';
|
|
import { api } from '../../services/api-client';
|
|
|
|
interface Message {
|
|
id: string; senderId: string; senderRole: string;
|
|
content: string; contentType: string; createdAt: string;
|
|
}
|
|
|
|
export function ChatPage() {
|
|
const { id } = useParams<{ id: string }>();
|
|
const [messages, setMessages] = useState<Message[]>([]);
|
|
const [input, setInput] = useState('');
|
|
const bottomRef = useRef<HTMLDivElement>(null);
|
|
|
|
useEffect(() => {
|
|
if (!id) return;
|
|
api.get<Message[]>(`/api/consultations/${id}/messages`)
|
|
.then((r) => setMessages(r.data))
|
|
.catch(() => {});
|
|
}, [id]);
|
|
|
|
useEffect(() => {
|
|
bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
}, [messages]);
|
|
|
|
const handleSend = async () => {
|
|
if (!input.trim() || !id) return;
|
|
try {
|
|
const res = await api.post<Message>(`/api/consultations/${id}/messages`, { content: input });
|
|
setMessages((prev) => [...prev, res.data]);
|
|
setInput('');
|
|
} catch { /* ignore */ }
|
|
};
|
|
|
|
return (
|
|
<div style={{ display: 'flex', flexDirection: 'column', height: 'calc(100vh - 0px)' }}>
|
|
<div style={{ padding: '14px 20px', background: '#fff', borderBottom: '1px solid #eee', fontSize: 15, fontWeight: 500 }}>
|
|
在线问诊
|
|
</div>
|
|
|
|
<div style={{ flex: 1, overflow: 'auto', padding: 20, background: '#fafafa' }}>
|
|
{messages.map((msg) => (
|
|
<div key={msg.id} style={{
|
|
display: 'flex', justifyContent: msg.senderRole === 'doctor' ? 'flex-end' : 'flex-start',
|
|
marginBottom: 12,
|
|
}}>
|
|
<div style={{
|
|
maxWidth: '70%', padding: '10px 14px', borderRadius: 12, fontSize: 14,
|
|
background: msg.senderRole === 'doctor' ? '#1976d2' : '#fff',
|
|
color: msg.senderRole === 'doctor' ? '#fff' : '#333',
|
|
boxShadow: '0 1px 3px rgba(0,0,0,0.08)',
|
|
}}>
|
|
<div>{msg.content}</div>
|
|
<div style={{
|
|
fontSize: 10, marginTop: 4, textAlign: 'right',
|
|
opacity: 0.7,
|
|
}}>
|
|
{msg.createdAt?.split('T')[1]?.slice(0, 5)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
<div ref={bottomRef} />
|
|
</div>
|
|
|
|
<div style={{ padding: '12px 20px', background: '#fff', borderTop: '1px solid #eee', display: 'flex', gap: 12 }}>
|
|
<input value={input} onChange={(e) => setInput(e.target.value)}
|
|
onKeyDown={(e) => e.key === 'Enter' && handleSend()}
|
|
placeholder="输入回复..."
|
|
style={{ flex: 1, padding: '10px 14px', border: '1px solid #ddd', borderRadius: 20, fontSize: 14 }} />
|
|
<button onClick={handleSend} style={{
|
|
padding: '10px 24px', background: '#1976d2', color: '#fff',
|
|
border: 'none', borderRadius: 20, fontSize: 14,
|
|
}}>
|
|
发送
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|