Initial commit: HealthManager full-stack health management platform

Backend: .NET 10 + PostgreSQL + EF Core + JWT + SignalR
Frontend patient: React 19 + TypeScript + Vite (mobile H5)
Frontend doctor: React 19 + TypeScript + Vite (desktop web)
This commit is contained in:
MingNian
2026-05-20 16:18:56 +08:00
commit 435af55c4a
215 changed files with 18595 additions and 0 deletions

View File

@@ -0,0 +1,98 @@
import { useEffect, useState, useRef } from 'react';
import { useParams } from 'react-router-dom';
import { PageHeader } from '@/components/layout/PageHeader';
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';
export function ChatPage() {
const { doctorId } = useParams<{ doctorId: string }>();
const [doctor, setDoctor] = useState<Doctor | null>(null);
const [consultation, setConsultation] = useState<Consultation | null>(null);
const [messages, setMessages] = useState<ConsultationMessage[]>([]);
const [text, setText] = useState('');
const [sending, setSending] = useState(false);
const bottomRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (doctorId) {
consultationService.getDoctor(doctorId).then((d) => setDoctor(d || null));
consultationService.getConsultation(doctorId).then(async (c) => {
if (c) {
setConsultation(c);
} else {
const newC = await consultationService.startConsultation(doctorId);
setConsultation(newC);
}
});
}
}, [doctorId]);
// Fetch messages when consultation is loaded
useEffect(() => {
if (consultation?.id) {
consultationService.getDoctorReply(consultation.id).then(() => {
// The messages are fetched as a side effect; fetch them directly
import('@/services/api-client').then(({ api }) => {
api.get<ConsultationMessage[]>(`/api/consultations/${consultation.id}/messages`)
.then((res) => setMessages(res.data));
});
});
}
}, [consultation?.id]);
useEffect(() => {
bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
const handleSend = async () => {
if (!text.trim() || !consultation || sending) return;
setSending(true);
const msgText = text;
setText('');
const sent = await consultationService.sendMessage(consultation.id, msgText);
setMessages((prev) => [...prev, sent]);
setSending(false);
// Poll for doctor reply after delay
setTimeout(async () => {
const reply = await consultationService.getDoctorReply(consultation.id);
if (reply) {
setMessages((prev) => {
if (prev.find((m) => m.id === reply.id)) return prev;
return [...prev, reply];
});
}
}, 1500);
};
return (
<div className={styles.page}>
<PageHeader title={doctor?.name || '咨询'} />
<div className={styles.messages}>
{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} disabled={sending}>
{sending ? '...' : '发送'}
</button>
</div>
</div>
);
}