fix: 1C response json parsing in bridge, checkup page cutouts and doctor assets

This commit is contained in:
Dana Medical Developer 2026-10-14 14:44:59 +00:00
parent 3bb44c4e80
commit e06d8a0de5
53 changed files with 81 additions and 47 deletions

View File

@ -140,7 +140,15 @@ class BridgeHandler(BaseHTTPRequestHandler):
if err: if err:
self.send_json(502, {"code": "1c_error", "message": err}) self.send_json(502, {"code": "1c_error", "message": err})
else: else:
self.send_json(200, data or []) slots = data or []
while isinstance(slots, str):
try:
slots = json.loads(slots)
except Exception:
break
if not isinstance(slots, list):
slots = []
self.send_json(200, slots)
return return
if path == "/doctors": if path == "/doctors":
@ -154,11 +162,19 @@ class BridgeHandler(BaseHTTPRequestHandler):
self.send_json(502, {"code": "1c_error", "message": err}) self.send_json(502, {"code": "1c_error", "message": err})
else: else:
doctors = data or [] doctors = data or []
for d in doctors: while isinstance(doctors, str):
try: try:
d["kids_from"] = int(d.get("kids_from", 0)) doctors = json.loads(doctors)
except (ValueError, TypeError): except Exception:
d["kids_from"] = 0 break
if not isinstance(doctors, list):
doctors = []
for d in doctors:
if isinstance(d, dict):
try:
d["kids_from"] = int(d.get("kids_from", 0))
except (ValueError, TypeError):
d["kids_from"] = 0
self.send_json(200, doctors) self.send_json(200, doctors)
return return
@ -211,16 +227,23 @@ class BridgeHandler(BaseHTTPRequestHandler):
self.send_json(502, {"code": "1c_error", "message": err}) self.send_json(502, {"code": "1c_error", "message": err})
else: else:
services = data or [] services = data or []
# Guarantee types for Go unmarshaling while isinstance(services, str):
try:
services = json.loads(services)
except Exception:
break
if not isinstance(services, list):
services = []
for s in services: for s in services:
try: if isinstance(s, dict):
s["duration_min"] = int(s.get("duration_min", 30)) try:
except (ValueError, TypeError): s["duration_min"] = int(s.get("duration_min", 30))
s["duration_min"] = 30 except (ValueError, TypeError):
try: s["duration_min"] = 30
s["price"] = float(s.get("price", 0.0)) try:
except (ValueError, TypeError): s["price"] = float(s.get("price", 0.0))
s["price"] = 0.0 except (ValueError, TypeError):
s["price"] = 0.0
self.send_json(200, services) self.send_json(200, services)
return return

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -99,12 +99,12 @@ export function directionImage(service) {
const CHECKUP_MEDIA = { const CHECKUP_MEDIA = {
'zdorovaya-devoch-a': { photo: ckGirl, icon: icoFemale, hero: checkupHeroGirl }, 'zdorovaya-devoch-a': { photo: ckGirl, icon: icoFemale, hero: checkupHeroGirl },
'proverit-svoe-zhenskoe-zdorove-': { photo: ckWoman, icon: icoCheckup, cutout: cutWomanHealth }, 'proverit-svoe-zhenskoe-zdorove-': { photo: ckWoman, icon: icoCheckup, cutout: cutWomanHealth, cutoutRatio: 0.943 },
'planirovannie-beremennosti': { photo: ckPregnancy, icon: icoPregnant, cutout: cutPlanning }, 'planirovannie-beremennosti': { photo: ckPregnancy, icon: icoPregnant, cutout: cutPlanning, cutoutRatio: 1.056 },
'profilaktika-raka-sheyki-matki': { photo: ckCervix, icon: icoRibbon, cutout: cutCervix, cutoutCenter: true }, 'profilaktika-raka-sheyki-matki': { photo: ckCervix, icon: icoRibbon, cutout: cutCervix, cutoutRatio: 1.452 },
// серо-синий фон шапки (#7886a9) — давление и родители // серо-синий фон шапки (#7886a9) — давление и родители
'davlenie-v-poryadke': { photo: ckPressure, icon: icoTonometer, cutout: cutPressure, heroBg: '#7886a9' }, 'davlenie-v-poryadke': { photo: ckPressure, icon: icoTonometer, cutout: cutPressure, cutoutRatio: 1.374, heroBg: '#7886a9' },
'zdorovye-roditeli': { photo: ckParents, icon: icoFamily, cutout: cutParents, heroBg: '#7886a9' }, 'zdorovye-roditeli': { photo: ckParents, icon: icoFamily, cutout: cutParents, cutoutRatio: 1.202, heroBg: '#7886a9' },
'mne-god': { photo: ckYear, icon: icoBaby }, 'mne-god': { photo: ckYear, icon: icoBaby },
'chek-ap-ya-v-sad': { photo: ckKindergarten, icon: icoPlayground }, 'chek-ap-ya-v-sad': { photo: ckKindergarten, icon: icoPlayground },
pervoklashka: { photo: ckSchool, icon: icoSchool }, pervoklashka: { photo: ckSchool, icon: icoSchool },
@ -112,11 +112,11 @@ const CHECKUP_MEDIA = {
detskiy: { photo: ckMini, icon: icoStethoscope }, detskiy: { photo: ckMini, icon: icoStethoscope },
'zdorovyy-zhkt': { photo: ckGut, icon: icoStomach }, 'zdorovyy-zhkt': { photo: ckGut, icon: icoStomach },
// программ ниже в макете нет — берём близкие по смыслу фото макета // программ ниже в макете нет — берём близкие по смыслу фото макета
'vedenie-beremennosti-1-trimestr': { photo: ckPregnancy, icon: icoPregnant, cutout: cutTrimester1 }, 'vedenie-beremennosti-1-trimestr': { photo: ckPregnancy, icon: icoPregnant, cutout: cutTrimester1, cutoutRatio: 1.048 },
'vedenie-beremennosti-2-trim': { photo: ckPregnancy, icon: icoPregnant, cutout: cutTrimester2 }, 'vedenie-beremennosti-2-trim': { photo: ckPregnancy, icon: icoPregnant, cutout: cutTrimester2, cutoutRatio: 1.215 },
'vedenie-beremennosti-3-trim': { photo: ckPregnancy, icon: icoPregnant, cutout: cutTrimester3 }, 'vedenie-beremennosti-3-trim': { photo: ckPregnancy, icon: icoPregnant, cutout: cutTrimester3, cutoutRatio: 1.191 },
'poslerodovoy-chek-ap-cherez-mesyats': { photo: ckWoman, icon: icoFemale, cutout: cutPostpartum }, 'poslerodovoy-chek-ap-cherez-mesyats': { photo: ckWoman, icon: icoFemale, cutout: cutPostpartum, cutoutRatio: 1.007 },
'zdorovaya-zhenshchina-40': { photo: ckWoman, icon: icoFemale, cutout: cutWoman40 }, 'zdorovaya-zhenshchina-40': { photo: ckWoman, icon: icoFemale, cutout: cutWoman40, cutoutRatio: 1.054 },
'chek-ap-ya-rodilsya': { photo: ckYear, icon: icoBaby }, 'chek-ap-ya-rodilsya': { photo: ckYear, icon: icoBaby },
}; };
@ -133,8 +133,8 @@ export function checkupMedia(checkup) {
cutout: m.cutout || '', cutout: m.cutout || '',
// свой цвет фона шапки, если в макете он не коралловый // свой цвет фона шапки, если в макете он не коралловый
heroBg: m.heroBg || '', heroBg: m.heroBg || '',
// главное в кадре по центру — при нехватке места фигура обрезается с двух сторон, а не только слева // ширина/высота файла фигуры: по ней шапка понимает, поместится ли фигура справа от текста
cutoutCenter: Boolean(m.cutoutCenter), cutoutRatio: m.cutoutRatio || 0,
}; };
} }

View File

@ -443,53 +443,55 @@
} }
/* ---------- вырезанная фигура в шапке ---------- /* ---------- вырезанная фигура в шапке ----------
Фигура срезана сверху и снизу, поэтому всегда на всю высоту шапки (срез совпадает с краями) Фигура срезана сверху и снизу, поэтому всегда на всю высоту шапки и целиком — без обрезки
и не выходит за правую линию сетки. Если снимок шире места справа от текста, лишнее уходит и растворения. Справа может выходить за сетку к краю экрана, как в макете. Если справа от текста
влево под плавное растворение — без прямого среза. */ места не хватает, фигура встаёт сразу за текстом и уходит правым краем за экран, но на текст не
налезает. Ширину фигуры при текущей высоте даёт --cutout-ratio (ширина/высота файла). */
.checkup-hero__row--cutout .checkup-hero__copy { .checkup-hero__row--cutout .checkup-hero__copy {
max-width: 520px; max-width: 480px;
} }
.checkup-hero__image--cutout { .checkup-hero__image--cutout {
position: absolute; position: absolute;
top: 0; top: 0;
bottom: 0; bottom: 0;
left: 548px; left: 504px;
right: 0; /* правый край — край экрана, на широких экранах не дальше 220px за сеткой */
right: max(calc((100% - 100vw) / 2), -220px);
width: auto; width: auto;
height: auto; height: auto;
overflow: hidden; overflow: visible;
pointer-events: none; pointer-events: none;
-webkit-mask-image: linear-gradient(to right, transparent 0, #000 72px); container-type: size;
mask-image: linear-gradient(to right, transparent 0, #000 72px);
} }
.checkup-hero__image--cutout img { .checkup-hero__image--cutout img {
position: absolute; position: absolute;
right: 0;
bottom: 0; bottom: 0;
left: max(0px, calc(100% - var(--cutout-ratio, 1.2) * 100cqh));
width: auto; width: auto;
height: 100%; height: 100%;
max-width: none; max-width: none;
} }
/* главное в кадре по центру (лента у «Профилактики рака шейки матки») — центрируем, а не к линии */ /* планшет: места справа от текста мало — фигура целиком у правого края экрана, а часть под текстом
.checkup-hero__image--center img { бледнеет, чтобы текст читался */
right: auto;
left: 50%;
transform: translateX(-50%);
}
@media (max-width: 1023px) { @media (max-width: 1023px) {
.checkup-hero__row--cutout .checkup-hero__copy { .checkup-hero__row--cutout .checkup-hero__copy {
max-width: 55%; max-width: 55%;
} }
.checkup-hero__image--cutout { .checkup-hero__image--cutout {
left: 56%; left: calc(-1 * var(--gutter));
right: 0; right: calc(-1 * var(--gutter));
width: auto; width: auto;
height: auto; height: auto;
} }
.checkup-hero__image--cutout img {
left: auto;
right: 0;
-webkit-mask-image: linear-gradient(to right, rgba(0, 0, 0, 0.18) 35%, #000 70%);
mask-image: linear-gradient(to right, rgba(0, 0, 0, 0.18) 35%, #000 70%);
}
} }
/* телефон: фигура уходит за текст и приглушена, чтобы текст читался */ /* телефон: фигура уходит за текст и приглушена, чтобы текст читался */
@ -506,7 +508,13 @@
height: auto; height: auto;
} }
.checkup-hero__image--cutout img { .checkup-hero__image--cutout img {
left: 0;
width: 100%;
object-fit: cover;
object-position: 70% bottom;
opacity: 0.5; opacity: 0.5;
-webkit-mask-image: none;
mask-image: none;
} }
} }

View File

@ -27,7 +27,7 @@ export default function CheckupPage() {
if (!checkup.data || checkup.data.slug !== slug) return <p className="state checkup-loading">Загружаем…</p>; if (!checkup.data || checkup.data.slug !== slug) return <p className="state checkup-loading">Загружаем…</p>;
const c = checkup.data; const c = checkup.data;
const { hero, photo, cutout, heroBg, cutoutCenter } = checkupMedia(c); const { hero, photo, cutout, heroBg, cutoutRatio } = checkupMedia(c);
const items = c.items?.length ? c.items.map(cleanText) : checklistItems(c.blocks); const items = c.items?.length ? c.items.map(cleanText) : checklistItems(c.blocks);
const intro = introText(c.blocks); const intro = introText(c.blocks);
const lead = cleanText(c.lead); const lead = cleanText(c.lead);
@ -64,7 +64,10 @@ export default function CheckupPage() {
</div> </div>
</div> </div>
{cutout ? ( {cutout ? (
<div className={`checkup-hero__image checkup-hero__image--cutout${cutoutCenter ? ' checkup-hero__image--center' : ''}`}> <div
className="checkup-hero__image checkup-hero__image--cutout"
style={cutoutRatio ? { '--cutout-ratio': cutoutRatio } : undefined}
>
<img src={cutout} alt="" /> <img src={cutout} alt="" />
</div> </div>
) : hero ? ( ) : hero ? (