Add endoscopy result viewer screen
New screen for viewing endoscopy results: 2×2 snapshot grid with simulated endoscopic view (radial gradient + specular highlight), diagnosis chips, conclusion, numbered recommendations, PDF/share actions, and full-screen image viewer with pagination. Wired via nav route `result:<id>` from ResultsScreen and exposed in Tweaks screen selector. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -60,6 +60,7 @@ const SCREEN_OPTIONS = [
|
||||
{ id: 'appt:a1', lb: 'Детали приёма' },
|
||||
{ id: 'results', lb: 'Результаты' },
|
||||
{ id: 'result-audio', lb: 'Аудиограмма' },
|
||||
{ id: 'result:r2', lb: 'Эндоскопия носоглотки' },
|
||||
{ id: 'recovery', lb: 'Восстановление' },
|
||||
{ id: 'audiotest', lb: 'Тест слуха' },
|
||||
{ id: 'chat', lb: 'Чат' },
|
||||
|
||||
+2
-1
@@ -8,7 +8,7 @@ import {
|
||||
} from './screens/screens-booking.jsx';
|
||||
import {
|
||||
ApptsTabScreen, ApptDetailScreen,
|
||||
ResultsScreen, ResultAudioScreen,
|
||||
ResultsScreen, ResultAudioScreen, ResultEndoscopyScreen,
|
||||
RecoveryScreen, AudioTestScreen,
|
||||
ChatTabScreen, ProfileTabScreen, QRScreen,
|
||||
TelemedScreen, MedcardScreen, NotificationsScreen,
|
||||
@@ -31,6 +31,7 @@ function renderScreen(screenId, nav, ctx) {
|
||||
case 'appt': return <ApptDetailScreen nav={nav} apptId={parts[1]} />;
|
||||
case 'results': return <ResultsScreen nav={nav} />;
|
||||
case 'result-audio': return <ResultAudioScreen nav={nav} />;
|
||||
case 'result': return <ResultEndoscopyScreen nav={nav} resultId={parts[1]} />;
|
||||
case 'recovery': return <RecoveryScreen nav={nav} />;
|
||||
case 'audiotest': return <AudioTestScreen nav={nav} />;
|
||||
case 'chat': return <ChatTabScreen nav={nav} />;
|
||||
|
||||
@@ -115,6 +115,182 @@ export function ApptDetailScreen({ nav, apptId }) {
|
||||
);
|
||||
}
|
||||
|
||||
function EndoscopyTile({ label, time, seed = 0, big = false }) {
|
||||
const variants = [
|
||||
'radial-gradient(circle at 45% 40%, #FCE1DD 0%, #E9A29B 35%, #B36962 65%, #5A2624 100%)',
|
||||
'radial-gradient(circle at 55% 50%, #FBD5D0 0%, #DC8981 40%, #9B4640 75%, #3F1B19 100%)',
|
||||
'radial-gradient(circle at 40% 35%, #FFE4DE 0%, #E7988F 40%, #A55651 70%, #4A1E1B 100%)',
|
||||
'radial-gradient(circle at 50% 55%, #FCDAD4 0%, #E5968C 35%, #A8544E 75%, #442220 100%)',
|
||||
];
|
||||
const insetPad = big ? 14 : 8;
|
||||
return (
|
||||
<div style={{ position: 'relative', background: '#0B0606', borderRadius: big ? 22 : 14, overflow: 'hidden', aspectRatio: '1', width: '100%' }}>
|
||||
<div style={{
|
||||
position: 'absolute', inset: insetPad, borderRadius: '50%',
|
||||
background: variants[seed % 4],
|
||||
}} />
|
||||
<div style={{
|
||||
position: 'absolute', inset: insetPad, borderRadius: '50%',
|
||||
boxShadow: 'inset 0 0 30px rgba(0,0,0,0.65), inset 0 0 6px rgba(255,255,255,0.15)',
|
||||
pointerEvents: 'none',
|
||||
}} />
|
||||
{/* subtle highlight spot (specular) */}
|
||||
<div style={{
|
||||
position: 'absolute', top: `${22 + (seed % 3) * 6}%`, left: `${30 + (seed % 4) * 5}%`,
|
||||
width: big ? 26 : 14, height: big ? 26 : 14, borderRadius: '50%',
|
||||
background: 'radial-gradient(circle, rgba(255,255,255,0.55) 0%, transparent 70%)',
|
||||
pointerEvents: 'none',
|
||||
}} />
|
||||
<div style={{
|
||||
position: 'absolute', left: insetPad + 4, right: insetPad + 4, bottom: insetPad + 4,
|
||||
padding: big ? '8px 12px' : '5px 8px',
|
||||
background: 'rgba(0,0,0,0.55)', borderRadius: 8,
|
||||
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
||||
color: '#fff', fontSize: big ? 13 : 11,
|
||||
}}>
|
||||
<span style={{ fontWeight: 700 }}>{label}</span>
|
||||
<span style={{ opacity: .7, fontFamily: 'var(--font-narrow)' }}>{time}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ResultEndoscopyScreen({ nav, resultId = 'r2' }) {
|
||||
const r = CLINIC_DATA.results.find(x => x.id === resultId) || CLINIC_DATA.results.find(x => x.kind === 'image');
|
||||
const doctor = CLINIC_DATA.doctors.find(d => d.id === r.doctor);
|
||||
const [selected, setSelected] = useState(null);
|
||||
|
||||
const images = [
|
||||
{ label: 'Носоглотка', time: '10:32' },
|
||||
{ label: 'Аденоиды', time: '10:33' },
|
||||
{ label: 'Хоана лев.', time: '10:34' },
|
||||
{ label: 'Хоана прав.', time: '10:35' },
|
||||
];
|
||||
const diagnoses = ['Гипертрофия аденоидов III ст.'];
|
||||
const conclusion = 'Слизистая носоглотки умеренно гиперемирована, отёчна. Глоточная миндалина (аденоиды) III степени, заполняет просвет хоан на 2/3 с обеих сторон. Отделяемое слизистое. Устья слуховых труб обозримы, без особенностей.';
|
||||
const recommendations = [
|
||||
'Консультация ЛОР-хирурга в течение 2 недель',
|
||||
'Курс промываний «Кукушка» (5 процедур)',
|
||||
'Контрольная эндоскопия через 1 месяц',
|
||||
'Обсудить показания к аденотомии',
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ paddingBottom: 40 }}>
|
||||
<ScreenHeader title={r.name} subtitle={`${r.date} · ${images.length} снимков`} onBack={() => nav.pop()} rightIcon={I.doc} />
|
||||
|
||||
<div style={{ padding: '0 20px' }}>
|
||||
<div className="card" style={{ padding: 14, display: 'flex', gap: 12, alignItems: 'center', marginBottom: 14 }}>
|
||||
<Avatar init={doctor.init} size={44} />
|
||||
<div style={{ flex: 1 }}>
|
||||
<div className="sub" style={{ fontSize: 11, marginBottom: 2 }}>Исследование провёл</div>
|
||||
<div style={{ fontSize: 14, fontWeight: 700 }}>{doctor.name.split(' ').slice(0,2).join(' ')}</div>
|
||||
</div>
|
||||
<span className="chip chip-success">Готово</span>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10, marginBottom: 16 }}>
|
||||
{images.map((img, i) => (
|
||||
<button key={i} onClick={() => setSelected(i)} className="press" style={{ padding: 0, background: 'transparent', borderRadius: 14 }}>
|
||||
<EndoscopyTile label={img.label} time={img.time} seed={i} />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="card" style={{ marginBottom: 12 }}>
|
||||
<div className="h-row" style={{ marginBottom: 10 }}>Диагноз</div>
|
||||
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
|
||||
{diagnoses.map(d => <span key={d} className="chip chip-warm" style={{ fontSize: 13, padding: '7px 12px' }}>{d}</span>)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card" style={{ marginBottom: 12 }}>
|
||||
<div className="h-row" style={{ marginBottom: 8 }}>Заключение</div>
|
||||
<p style={{ margin: 0, fontSize: 14, color: 'var(--c-fg-2)', lineHeight: 1.55 }}>{conclusion}</p>
|
||||
</div>
|
||||
|
||||
<div className="card" style={{ marginBottom: 16 }}>
|
||||
<div className="h-row" style={{ marginBottom: 6 }}>Рекомендации</div>
|
||||
{recommendations.map((rec, i, a) => (
|
||||
<div key={i}>
|
||||
<div style={{ display: 'flex', gap: 12, padding: '10px 0', alignItems: 'flex-start' }}>
|
||||
<div style={{
|
||||
width: 22, height: 22, borderRadius: 999, background: 'var(--c-primary-100)',
|
||||
color: 'var(--c-primary-darker)', fontSize: 12, fontWeight: 700,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0, marginTop: 1,
|
||||
}}>{i + 1}</div>
|
||||
<div style={{ fontSize: 14, color: 'var(--c-fg-2)', lineHeight: 1.5 }}>{rec}</div>
|
||||
</div>
|
||||
{i < a.length - 1 && <div className="divider" />}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
||||
<button className="btn-p block">
|
||||
<I.doc size={18} /> Скачать PDF
|
||||
</button>
|
||||
<div style={{ display: 'flex', gap: 10 }}>
|
||||
<button onClick={() => nav.push('chat')} className="btn-g" style={{ flex: 1, padding: 14 }}>
|
||||
<I.chat size={16} /> Обсудить
|
||||
</button>
|
||||
<button className="btn-g" style={{ flex: 1, padding: 14 }}>
|
||||
<I.arrow size={16} /> Поделиться
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selected !== null && (
|
||||
<div
|
||||
onClick={() => setSelected(null)}
|
||||
style={{
|
||||
position: 'absolute', inset: 0, background: 'rgba(0,0,0,0.95)', zIndex: 200,
|
||||
display: 'flex', flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<div style={{ padding: '50px 20px 16px', display: 'flex', alignItems: 'center', justifyContent: 'space-between', color: '#fff' }}>
|
||||
<button onClick={(e) => { e.stopPropagation(); setSelected(null); }} className="press" style={{
|
||||
width: 38, height: 38, borderRadius: 999, background: 'rgba(255,255,255,0.15)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
}}>
|
||||
<I.close size={18} style={{ color: '#fff' }} />
|
||||
</button>
|
||||
<div style={{ fontSize: 15, fontWeight: 700 }}>{images[selected].label}</div>
|
||||
<button onClick={(e) => e.stopPropagation()} className="press" style={{
|
||||
width: 38, height: 38, borderRadius: 999, background: 'rgba(255,255,255,0.15)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
}}>
|
||||
<I.doc size={18} style={{ color: '#fff' }} />
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '0 20px' }}
|
||||
>
|
||||
<div style={{ width: '100%', maxWidth: 340 }}>
|
||||
<EndoscopyTile label={images[selected].label} time={images[selected].time} seed={selected} big />
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ padding: '16px 20px 36px', display: 'flex', justifyContent: 'center', gap: 8 }}>
|
||||
{images.map((_, i) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={(e) => { e.stopPropagation(); setSelected(i); }}
|
||||
style={{
|
||||
width: i === selected ? 24 : 8, height: 8, borderRadius: 999,
|
||||
background: i === selected ? '#fff' : 'rgba(255,255,255,0.35)',
|
||||
border: 0, padding: 0, cursor: 'pointer', transition: 'width .2s',
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ResultsScreen({ nav }) {
|
||||
const { results, doctors } = CLINIC_DATA;
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user