fix: consultation notifications, profile edit hint, doctor report in Chinese, chat UI polish, tabbar badge
This commit is contained in:
@@ -59,6 +59,30 @@ public class ConsultationService(AppDbContext db)
|
||||
ImageUrl = imageUrl,
|
||||
};
|
||||
db.ConsultationMessages.Add(message);
|
||||
|
||||
// Create notification for the recipient
|
||||
var consultation = await db.Consultations
|
||||
.Include(c => c.Patient)
|
||||
.Include(c => c.Doctor)
|
||||
.FirstOrDefaultAsync(c => c.Id == consultationId);
|
||||
|
||||
if (consultation != null)
|
||||
{
|
||||
var targetUserId = senderRole == "patient" ? consultation.DoctorId : consultation.PatientId;
|
||||
var senderName = senderRole == "patient" ? consultation.Patient?.Name : consultation.Doctor?.Name;
|
||||
var notifyTitle = senderRole == "patient" ? "新患者消息" : "医生已回复";
|
||||
var notifyContent = $"{senderName ?? "对方"}:{TruncateContent(content)}";
|
||||
|
||||
db.Notifications.Add(new Notification
|
||||
{
|
||||
UserId = targetUserId,
|
||||
Type = "consultation",
|
||||
Title = notifyTitle,
|
||||
Content = notifyContent,
|
||||
RelatedId = consultationId,
|
||||
});
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
return message;
|
||||
}
|
||||
@@ -70,4 +94,7 @@ public class ConsultationService(AppDbContext db)
|
||||
query = query.Where(u => u.Department == department);
|
||||
return await query.ToListAsync();
|
||||
}
|
||||
|
||||
private static string TruncateContent(string content) =>
|
||||
content.Length > 50 ? content[..50] + "..." : content;
|
||||
}
|
||||
|
||||
@@ -16,18 +16,20 @@ interface RawItem {
|
||||
unit?: string; referenceRange?: string; isAbnormal: boolean;
|
||||
}
|
||||
|
||||
const categoryMap: Record<string, string> = {
|
||||
'血液检查': '血液检查', '心电图': '心电图', '影像学': '影像学', '尿液检查': '尿液检查', '其他': '其他',
|
||||
'Blood Test': '血液检查', 'ECG': '心电图', 'Imaging': '影像学',
|
||||
};
|
||||
|
||||
export function ReportDetailPage() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const [report, setReport] = useState<RawReport | null>(null);
|
||||
const [lightbox, setLightbox] = useState<string | null>(null);
|
||||
|
||||
// Interpretation form
|
||||
const [summary, setSummary] = useState('');
|
||||
const [riskLevel, setRiskLevel] = useState('normal');
|
||||
const [suggestions, setSuggestions] = useState('');
|
||||
const [items, setItems] = useState<{ itemName: string; resultValue: string; unit: string; referenceRange: string; isAbnormal: boolean }[]>([
|
||||
{ itemName: '', resultValue: '', unit: '', referenceRange: '', isAbnormal: false },
|
||||
]);
|
||||
const [items, setItems] = useState([{ itemName: '', resultValue: '', unit: '', referenceRange: '', isAbnormal: false }]);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -35,51 +37,46 @@ export function ReportDetailPage() {
|
||||
api.get<RawReport>(`/api/reports/${id}`).then((r) => setReport(r.data)).catch(() => {});
|
||||
}, [id]);
|
||||
|
||||
const addItem = () => {
|
||||
setItems((prev) => [...prev, { itemName: '', resultValue: '', unit: '', referenceRange: '', isAbnormal: false }]);
|
||||
};
|
||||
|
||||
const updateItem = (index: number, field: string, value: string | boolean) => {
|
||||
setItems((prev) => prev.map((item, i) => i === index ? { ...item, [field]: value } : item));
|
||||
};
|
||||
|
||||
const removeItem = (index: number) => {
|
||||
if (items.length <= 1) return;
|
||||
setItems((prev) => prev.filter((_, i) => i !== index));
|
||||
};
|
||||
const addItem = () => setItems((prev) => [...prev, { itemName: '', resultValue: '', unit: '', referenceRange: '', isAbnormal: false }]);
|
||||
const updateItem = (i: number, field: string, value: string | boolean) =>
|
||||
setItems((prev) => prev.map((it, idx) => idx === i ? { ...it, [field]: value } : it));
|
||||
const removeItem = (i: number) => { if (items.length > 1) setItems((prev) => prev.filter((_, idx) => idx !== i)); };
|
||||
|
||||
const handleInterpret = async () => {
|
||||
if (!summary.trim() || !id) return;
|
||||
setSubmitting(true);
|
||||
try {
|
||||
await api.post(`/api/reports/${id}/interpret`, {
|
||||
summary,
|
||||
items: items.filter((it) => it.itemName.trim()),
|
||||
riskLevel,
|
||||
summary, items: items.filter((it) => it.itemName.trim()), riskLevel,
|
||||
suggestions: suggestions || null,
|
||||
});
|
||||
const updated = await api.get<RawReport>(`/api/reports/${id}`);
|
||||
setReport(updated.data);
|
||||
} catch { alert('submit failed'); }
|
||||
} catch { alert('提交失败'); }
|
||||
finally { setSubmitting(false); }
|
||||
};
|
||||
|
||||
if (!report) return <div style={{ padding: 24 }}>Loading...</div>;
|
||||
if (!report) return <div style={{ padding: 24 }}>加载中...</div>;
|
||||
|
||||
const isCompleted = report.status === 'completed';
|
||||
const riskMap: Record<string, { text: string; color: string }> = {
|
||||
normal: { text: '正常', color: '#2e7d32' },
|
||||
attention: { text: '关注', color: '#f57c00' },
|
||||
abnormal: { text: '异常', color: '#c62828' },
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: 24 }}>
|
||||
<Link to="/reports" style={{ fontSize: 13, color: '#1976d2' }}>← Back to Reports</Link>
|
||||
<Link to="/reports" style={{ fontSize: 13, color: '#1976d2' }}>← 返回报告列表</Link>
|
||||
|
||||
<div style={{ background: '#fff', marginTop: 16, padding: 24, borderRadius: 8, boxShadow: '0 1px 4px rgba(0,0,0,0.08)' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'start' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
||||
<div>
|
||||
<h2 style={{ margin: 0 }}>{report.title}</h2>
|
||||
<div style={{ marginTop: 8, fontSize: 13, color: '#888' }}>
|
||||
Patient: {report.patientName || 'unknown'} |
|
||||
Category: {report.category} |
|
||||
Date: {report.createdAt?.split('T')[0]}
|
||||
患者:{report.patientName || '未知'} |
|
||||
分类:{categoryMap[report.category] || report.category} |
|
||||
日期:{report.createdAt?.split('T')[0]}
|
||||
</div>
|
||||
</div>
|
||||
<span style={{
|
||||
@@ -87,68 +84,61 @@ export function ReportDetailPage() {
|
||||
background: isCompleted ? '#e8f5e9' : '#fff3e0',
|
||||
color: isCompleted ? '#2e7d32' : '#f57c00',
|
||||
}}>
|
||||
{isCompleted ? 'Completed' : 'Pending Review'}
|
||||
{isCompleted ? '已完成' : '待审核'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Image gallery */}
|
||||
{/* 图片 */}
|
||||
{report.imageUrls && report.imageUrls.length > 0 && (
|
||||
<div style={{ marginTop: 20 }}>
|
||||
<h4 style={{ fontSize: 14, marginBottom: 8 }}>Uploaded Images ({report.imageUrls.length})</h4>
|
||||
<h4 style={{ fontSize: 14, marginBottom: 8 }}>上传图片({report.imageUrls.length}张)</h4>
|
||||
<div style={{ display: 'flex', gap: 10, flexWrap: 'wrap' }}>
|
||||
{report.imageUrls.map((url, i) => (
|
||||
<div
|
||||
key={i}
|
||||
onClick={() => setLightbox(url)}
|
||||
style={{
|
||||
width: 120, height: 120, borderRadius: 8, overflow: 'hidden',
|
||||
cursor: 'pointer', border: '2px solid #eee',
|
||||
background: '#f5f5f5', display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={`http://localhost:5000${url}`}
|
||||
alt={`report-${i}`}
|
||||
<div key={i} onClick={() => setLightbox(url)} style={{
|
||||
width: 120, height: 120, borderRadius: 8, overflow: 'hidden',
|
||||
cursor: 'pointer', border: '2px solid #eee', background: '#f5f5f5',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
}}>
|
||||
<img src={`http://localhost:5000${url}`} alt={`图片${i}`}
|
||||
style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'cover' }}
|
||||
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none'; }}
|
||||
/>
|
||||
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none'; }} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Lightbox */}
|
||||
{/* 灯箱 */}
|
||||
{lightbox && (
|
||||
<div onClick={() => setLightbox(null)} style={{
|
||||
position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.85)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 999,
|
||||
cursor: 'pointer',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 999, cursor: 'pointer',
|
||||
}}>
|
||||
<img src={`http://localhost:5000${lightbox}`} alt="preview" style={{ maxWidth: '90vw', maxHeight: '90vh', borderRadius: 8 }} />
|
||||
<img src={`http://localhost:5000${lightbox}`} alt="预览" style={{ maxWidth: '90vw', maxHeight: '90vh', borderRadius: 8 }} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Completed interpretation display */}
|
||||
{/* 已完成解读 */}
|
||||
{isCompleted && (
|
||||
<div style={{ marginTop: 20, padding: 16, background: '#e8f5e9', borderRadius: 8 }}>
|
||||
<h4 style={{ fontSize: 14, marginBottom: 8 }}>Interpretation Result</h4>
|
||||
<h4 style={{ fontSize: 14, marginBottom: 8 }}>解读结果</h4>
|
||||
<div style={{ fontSize: 13 }}>
|
||||
<p><strong>Risk Level:</strong> <span style={{
|
||||
color: report.riskLevel === 'normal' ? '#2e7d32' : report.riskLevel === 'abnormal' ? '#c62828' : '#f57c00',
|
||||
fontWeight: 600,
|
||||
}}>{report.riskLevel || '-'}</span></p>
|
||||
<p><strong>Summary:</strong> {report.summary || '-'}</p>
|
||||
{report.suggestions && <p><strong>Suggestions:</strong> {report.suggestions}</p>}
|
||||
<p><strong>风险等级:</strong>
|
||||
<span style={{ color: riskMap[report.riskLevel || '']?.color, fontWeight: 600 }}>
|
||||
{riskMap[report.riskLevel || '']?.text || report.riskLevel || '-'}
|
||||
</span>
|
||||
</p>
|
||||
<p><strong>总结:</strong>{report.summary || '-'}</p>
|
||||
{report.suggestions && <p><strong>建议:</strong>{report.suggestions}</p>}
|
||||
</div>
|
||||
|
||||
{report.items && report.items.length > 0 && (
|
||||
<table style={{ width: '100%', marginTop: 12, borderCollapse: 'collapse', fontSize: 12 }}>
|
||||
<thead><tr style={{ textAlign: 'left', borderBottom: '2px solid #c8e6c9' }}>
|
||||
<th style={{ padding: '6px 8px' }}>Item</th>
|
||||
<th style={{ padding: '6px 8px' }}>Result</th>
|
||||
<th style={{ padding: '6px 8px' }}>Reference</th>
|
||||
<th style={{ padding: '6px 8px' }}>Abnormal</th>
|
||||
<th style={{ padding: '6px 8px' }}>检查项目</th>
|
||||
<th style={{ padding: '6px 8px' }}>结果</th>
|
||||
<th style={{ padding: '6px 8px' }}>参考范围</th>
|
||||
<th style={{ padding: '6px 8px' }}>是否异常</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
{report.items.map((item) => (
|
||||
@@ -157,7 +147,7 @@ export function ReportDetailPage() {
|
||||
<td style={{ padding: '6px 8px' }}>{item.resultValue} {item.unit || ''}</td>
|
||||
<td style={{ padding: '6px 8px' }}>{item.referenceRange || '-'}</td>
|
||||
<td style={{ padding: '6px 8px', color: item.isAbnormal ? '#c62828' : '#2e7d32', fontWeight: 500 }}>
|
||||
{item.isAbnormal ? 'Yes' : 'No'}
|
||||
{item.isAbnormal ? '是' : '否'}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
@@ -167,66 +157,62 @@ export function ReportDetailPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Interpretation form (only for pending reports) */}
|
||||
{/* 解读表单 */}
|
||||
{!isCompleted && (
|
||||
<div style={{ marginTop: 24, borderTop: '1px solid #eee', paddingTop: 20 }}>
|
||||
<h3 style={{ fontSize: 15, marginBottom: 16 }}>Doctor Interpretation</h3>
|
||||
<h3 style={{ fontSize: 15, marginBottom: 16 }}>医生解读</h3>
|
||||
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 4 }}>Summary</label>
|
||||
<textarea
|
||||
value={summary}
|
||||
onChange={(e) => setSummary(e.target.value)}
|
||||
placeholder="Enter your interpretation summary..."
|
||||
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 4 }}>解读总结</label>
|
||||
<textarea value={summary} onChange={(e) => setSummary(e.target.value)}
|
||||
placeholder="请输入您的专业解读总结..."
|
||||
rows={4}
|
||||
style={{ width: '100%', padding: '10px 12px', border: '1px solid #ddd', borderRadius: 6, fontSize: 13, resize: 'vertical', fontFamily: 'inherit', boxSizing: 'border-box' }}
|
||||
/>
|
||||
style={{ width: '100%', padding: '10px 12px', border: '1px solid #ddd', borderRadius: 6, fontSize: 13, resize: 'vertical', fontFamily: 'inherit', boxSizing: 'border-box' }} />
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: 16, marginBottom: 12 }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 4 }}>Risk Level</label>
|
||||
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 4 }}>风险等级</label>
|
||||
<select value={riskLevel} onChange={(e) => setRiskLevel(e.target.value)}
|
||||
style={{ width: '100%', padding: '8px 12px', border: '1px solid #ddd', borderRadius: 6, fontSize: 13, fontFamily: 'inherit' }}>
|
||||
<option value="normal">Normal</option>
|
||||
<option value="attention">Attention</option>
|
||||
<option value="abnormal">Abnormal</option>
|
||||
<option value="normal">正常</option>
|
||||
<option value="attention">需关注</option>
|
||||
<option value="abnormal">异常</option>
|
||||
</select>
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 4 }}>Suggestions</label>
|
||||
<input
|
||||
value={suggestions}
|
||||
onChange={(e) => setSuggestions(e.target.value)}
|
||||
placeholder="e.g. Continue current medication"
|
||||
style={{ width: '100%', padding: '8px 12px', border: '1px solid #ddd', borderRadius: 6, fontSize: 13, fontFamily: 'inherit', boxSizing: 'border-box' }}
|
||||
/>
|
||||
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 4 }}>医生建议</label>
|
||||
<input value={suggestions} onChange={(e) => setSuggestions(e.target.value)}
|
||||
placeholder="如:继续当前用药方案"
|
||||
style={{ width: '100%', padding: '8px 12px', border: '1px solid #ddd', borderRadius: 6, fontSize: 13, fontFamily: 'inherit', boxSizing: 'border-box' }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Test items */}
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 6 }}>Test Items</label>
|
||||
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 6 }}>检查项目</label>
|
||||
{items.map((item, i) => (
|
||||
<div key={i} style={{ display: 'flex', gap: 8, marginBottom: 6, alignItems: 'center' }}>
|
||||
<input placeholder="Item name" value={item.itemName} onChange={(e) => updateItem(i, 'itemName', e.target.value)}
|
||||
<input placeholder="项目名称" value={item.itemName} onChange={(e) => updateItem(i, 'itemName', e.target.value)}
|
||||
style={{ flex: 2, padding: '6px 10px', border: '1px solid #ddd', borderRadius: 4, fontSize: 12, fontFamily: 'inherit' }} />
|
||||
<input placeholder="Result" value={item.resultValue} onChange={(e) => updateItem(i, 'resultValue', e.target.value)}
|
||||
<input placeholder="结果" value={item.resultValue} onChange={(e) => updateItem(i, 'resultValue', e.target.value)}
|
||||
style={{ flex: 1, padding: '6px 10px', border: '1px solid #ddd', borderRadius: 4, fontSize: 12, fontFamily: 'inherit' }} />
|
||||
<input placeholder="Unit" value={item.unit} onChange={(e) => updateItem(i, 'unit', e.target.value)}
|
||||
style={{ width: 80, padding: '6px 10px', border: '1px solid #ddd', borderRadius: 4, fontSize: 12, fontFamily: 'inherit' }} />
|
||||
<input placeholder="Range" value={item.referenceRange} onChange={(e) => updateItem(i, 'referenceRange', e.target.value)}
|
||||
<input placeholder="单位" value={item.unit} onChange={(e) => updateItem(i, 'unit', e.target.value)}
|
||||
style={{ width: 70, padding: '6px 10px', border: '1px solid #ddd', borderRadius: 4, fontSize: 12, fontFamily: 'inherit' }} />
|
||||
<input placeholder="参考范围" value={item.referenceRange} onChange={(e) => updateItem(i, 'referenceRange', e.target.value)}
|
||||
style={{ flex: 1, padding: '6px 10px', border: '1px solid #ddd', borderRadius: 4, fontSize: 12, fontFamily: 'inherit' }} />
|
||||
<label style={{ fontSize: 12, whiteSpace: 'nowrap', display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<label style={{ fontSize: 12, whiteSpace: 'nowrap', display: 'flex', alignItems: 'center', gap: 3 }}>
|
||||
<input type="checkbox" checked={item.isAbnormal} onChange={(e) => updateItem(i, 'isAbnormal', e.target.checked)} />
|
||||
Abnormal
|
||||
异常
|
||||
</label>
|
||||
<button onClick={() => removeItem(i)} style={{ background: 'none', border: 'none', color: '#c62828', cursor: 'pointer', fontSize: 16 }} disabled={items.length <= 1}>✕</button>
|
||||
<button onClick={() => removeItem(i)}
|
||||
style={{ background: 'none', border: 'none', color: '#c62828', cursor: 'pointer', fontSize: 16 }}
|
||||
disabled={items.length <= 1}>✕</button>
|
||||
</div>
|
||||
))}
|
||||
<button onClick={addItem} style={{ padding: '4px 12px', border: '1px dashed #1976d2', borderRadius: 4, background: 'none', color: '#1976d2', cursor: 'pointer', fontSize: 12 }}>
|
||||
+ Add Item
|
||||
</button>
|
||||
<button onClick={addItem} style={{
|
||||
padding: '4px 12px', border: '1px dashed #1976d2', borderRadius: 4,
|
||||
background: 'none', color: '#1976d2', cursor: 'pointer', fontSize: 12,
|
||||
}}>+ 添加项目</button>
|
||||
</div>
|
||||
|
||||
<button onClick={handleInterpret} disabled={submitting} style={{
|
||||
@@ -234,7 +220,7 @@ export function ReportDetailPage() {
|
||||
border: 'none', borderRadius: 6, fontSize: 14, cursor: 'pointer',
|
||||
opacity: submitting ? 0.7 : 1, marginTop: 8,
|
||||
}}>
|
||||
{submitting ? 'Submitting...' : 'Submit Interpretation'}
|
||||
{submitting ? '提交中...' : '提交解读'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -42,3 +42,25 @@
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.tabIcon {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.badge {
|
||||
position: absolute;
|
||||
top: -6px;
|
||||
right: -10px;
|
||||
min-width: 16px;
|
||||
height: 16px;
|
||||
padding: 0 4px;
|
||||
background: #EF4444;
|
||||
color: #fff;
|
||||
border-radius: 10px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { NAV_ITEMS } from '@/utils/constants';
|
||||
import { useNotificationStore } from '@/stores/notification.store';
|
||||
import styles from './TabBar.module.css';
|
||||
|
||||
export function TabBar() {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const unreadCount = useNotificationStore((s) => s.unreadCount);
|
||||
|
||||
return (
|
||||
<nav className={styles.tabBar}>
|
||||
@@ -16,7 +18,12 @@ export function TabBar() {
|
||||
className={`${styles.tab} ${isActive ? styles.tabActive : ''}`}
|
||||
onClick={() => navigate(item.path)}
|
||||
>
|
||||
<span className={styles.tabIcon}>{item.icon}</span>
|
||||
<span className={styles.tabIcon}>
|
||||
{item.icon}
|
||||
{item.path === '/services' && unreadCount > 0 && (
|
||||
<span className={styles.badge}>{unreadCount > 99 ? '99+' : unreadCount}</span>
|
||||
)}
|
||||
</span>
|
||||
<span className={styles.tabLabel}>{item.label}</span>
|
||||
</button>
|
||||
);
|
||||
|
||||
@@ -25,7 +25,7 @@ export function ProfilePage() {
|
||||
<Card className={styles.profileCard} onClick={() => navigate('/profile/edit')}>
|
||||
<div className={styles.avatar}>{user?.nickname?.[0] || '用'}</div>
|
||||
<div className={styles.profileInfo}>
|
||||
<div className={styles.nickname}>{user?.nickname || '用户'} <span className={styles.editHint}>编辑</span></div>
|
||||
<div className={styles.nickname}>{user?.nickname || '用户'} <span className={styles.editHint}>›</span></div>
|
||||
<div className={styles.phone}>{user?.phone}</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background: var(--color-bg);
|
||||
background: #EDF1F7;
|
||||
}
|
||||
|
||||
.messages {
|
||||
@@ -10,74 +10,92 @@
|
||||
overflow-y: auto;
|
||||
padding: 16px;
|
||||
padding-top: calc(var(--header-height) + 8px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.bubble {
|
||||
margin-bottom: 14px;
|
||||
max-width: 80%;
|
||||
max-width: 78%;
|
||||
animation: bubbleIn 0.25s ease-out;
|
||||
}
|
||||
|
||||
@keyframes bubbleIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.patient {
|
||||
margin-left: auto;
|
||||
text-align: right;
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.doctor {
|
||||
margin-right: auto;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.bubbleContent {
|
||||
padding: 10px 14px;
|
||||
border-radius: var(--radius-md);
|
||||
font-size: var(--font-size-sm);
|
||||
border-radius: 18px;
|
||||
font-size: var(--font-size-base);
|
||||
line-height: 1.5;
|
||||
display: inline-block;
|
||||
text-align: left;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.patient .bubbleContent {
|
||||
background: var(--color-primary);
|
||||
color: var(--color-text-inverse);
|
||||
background: linear-gradient(135deg, #1E6BFF, #4D8FFF);
|
||||
color: #fff;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
|
||||
.doctor .bubbleContent {
|
||||
background: var(--color-white);
|
||||
background: #fff;
|
||||
color: var(--color-text-primary);
|
||||
border-bottom-left-radius: 4px;
|
||||
box-shadow: var(--shadow-sm);
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.06);
|
||||
}
|
||||
|
||||
.bubbleTime {
|
||||
font-size: 10px;
|
||||
color: var(--color-text-tertiary);
|
||||
margin-top: 4px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.patient .bubbleTime { text-align: right; }
|
||||
|
||||
.inputBar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 10px 14px;
|
||||
background: var(--color-white);
|
||||
background: #fff;
|
||||
border-top: 1px solid var(--color-border);
|
||||
padding-bottom: env(safe-area-inset-bottom, 10px);
|
||||
}
|
||||
|
||||
.input {
|
||||
flex: 1;
|
||||
padding: 10px 14px;
|
||||
background: var(--color-bg);
|
||||
border-radius: var(--radius-full);
|
||||
font-size: var(--font-size-sm);
|
||||
padding: 10px 16px;
|
||||
background: #EDF1F7;
|
||||
border: none;
|
||||
border-radius: 24px;
|
||||
font-size: var(--font-size-base);
|
||||
outline: none;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.sendBtn {
|
||||
padding: 10px 18px;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
background: var(--color-primary);
|
||||
color: var(--color-text-inverse);
|
||||
border-radius: var(--radius-full);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sendBtn:disabled {
|
||||
|
||||
Reference in New Issue
Block a user