const { useState, useEffect, useMemo, useCallback } = React; // ------------------------------------------------------------ api async function api(path, opts = {}) { const r = await fetch(path, { method: opts.method || (opts.body ? "POST" : "GET"), headers: opts.body ? { "Content-Type": "application/json" } : {}, body: opts.body ? JSON.stringify(opts.body) : undefined, credentials: "same-origin", }); let data = null; try { data = await r.json(); } catch (e) {} if (r.status === 401 && path !== "/api/login") { window.dispatchEvent(new Event("vc-logout")); } if (!r.ok) { const err = new Error((data && data.detail) || `Ошибка ${r.status}`); err.status = r.status; throw err; } return data; } // ------------------------------------------------------------ роутинг по hash function parseHash() { const [path, qs] = (location.hash.replace(/^#/, "") || "/").split("?"); const q = Object.fromEntries(new URLSearchParams(qs || "")); return { parts: path.split("/").filter(Boolean), q }; } function go(path) { location.hash = path; } function useRoute() { const [r, setR] = useState(parseHash()); useEffect(() => { const f = () => setR(parseHash()); addEventListener("hashchange", f); return () => removeEventListener("hashchange", f); }, []); return r; } const fmtTime = (iso) => { if (!iso) return ""; const d = new Date(iso), now = new Date(); const hm = d.toLocaleTimeString("ru-RU", { hour: "2-digit", minute: "2-digit" }); if (d.toDateString() === now.toDateString()) return `сегодня ${hm}`; const y = new Date(now); y.setDate(now.getDate() - 1); if (d.toDateString() === y.toDateString()) return `вчера ${hm}`; return d.toLocaleDateString("ru-RU", { day: "2-digit", month: "2-digit" }) + " " + hm; }; const carTitle = (v) => v.alias || [v.brand_ru || v.brand, v.sys].filter(Boolean).join(" "); // ------------------------------------------------------------ шапка function Top({ me, ctx, onLogout }) { return (
ПОДБОР ПО VIN {ctx && {ctx}}
{me.admin && админка} запросов VIN осталось VIN: {me.quota && me.quota.left != null ? me.quota.left : "—"}
); } // ------------------------------------------------------------ 0. вход по ПИН function Login({ onIn }) { const [pin, setPin] = useState(""), [err, setErr] = useState(""), [busy, setBusy] = useState(false); const submit = async (e) => { e.preventDefault(); setBusy(true); setErr(""); try { onIn(await api("/api/login", { body: { pin } })); } catch (x) { setErr(x.message); setPin(""); } setBusy(false); }; return (

Подбор по VIN

Введите ваш ПИН

setPin(e.target.value)} autoFocus /> {err &&
{err}
}
); } // ------------------------------------------------------------ 1. главная: VIN + последние function Home({ me, onQuota }) { const [vin, setVin] = useState(""), [err, setErr] = useState(""), [busy, setBusy] = useState(false); const [recent, setRecent] = useState(null); const [wmi, setWmi] = useState([]); useEffect(() => { api("/api/recent").then(setRecent).catch(() => setRecent([])); api("/api/wmi").then(setWmi).catch(() => {}); }, []); const clean = vin.replace(/\s/g, "").toUpperCase(); const cached = recent && recent.some((r) => r.vin === clean); const local = clean.length >= 3 ? (wmi.find((w) => w.wmi === clean.slice(0, 3)) || (clean[0] !== "L" ? { wmi: clean.slice(0, 3), generic: true } : null)) : null; const submit = async (e) => { e.preventDefault(); setErr(""); if (!/^[A-HJ-NPR-Z0-9]{17}$/.test(clean)) { setErr("VIN — 17 знаков, латиница и цифры, без I, O, Q"); return; } setBusy(true); try { await api("/api/vin", { body: { vin: clean } }); onQuota(); go(`/vin/${clean}`); } catch (x) { setErr(x.message); } setBusy(false); }; return (

Введите VIN

Система определит модель, год, двигатель и коробку. Размер колёс спросим отдельно — по нему подбираются тормоза.

setVin(e.target.value)} autoFocus spellCheck={false} />
{clean.length === 17 && (cached ? "Этот VIN уже определялся — бесплатно, из памяти." : "Если VIN найдётся — спишется 1 запрос из квоты (ненайденный не списывается). Повторный ввод того же VIN — бесплатно.")}
{local && !cached && (
{local.generic ? Префикс {local.wmi} — машина собрана не в Китае (VIN китайских заводов начинаются на L). : Префикс {local.wmi} — {local.brands}, сборка: {local.plant}.} Китайские каталоги VIN местной сборки обычно не знают — скорее всего, машина не найдётся. Квоту это не тратит: поставщик списывает запрос только за найденный VIN. {local.note}
)} {err &&
{err}
}
Последние запросы
{recent === null &&
загрузка…
} {recent && !recent.length &&
Пока пусто
} {recent && recent.map((r) => ( {r.vin} {r.found ? {[r.title, r.engine, r.year].filter(Boolean).join(" · ")} : {r.local ? (r.local.generic ? "не найдено — сборка не в Китае" : `не найдено — местная сборка ${r.local.brands} (${r.local.plant})`) : "не найдено в базе"}} {fmtTime(r.last_access)} ))}
); } // ------------------------------------------------------------ 2. машина function Vehicle({ vin, route, setCtx }) { const [v, setV] = useState(null), [err, setErr] = useState(""); const [showAll, setShowAll] = useState(false); const rim = route.q.rim || ""; const mjsid = route.q.mjsid || ""; useEffect(() => { api(`/api/vin/${vin}`).then(setV).catch((x) => setErr(x.message)); }, [vin]); useEffect(() => { if (v && v.found) setCtx(`${carTitle(v)} · ${v.displacement || ""} · ${v.vin_year || ""}`); }, [v]); if (err) return
{err}
; if (!v) return
загрузка…
; if (!v.found) return (

{vin}

Поставщик не нашёл этот VIN в своей базе (код {v.code}, «{v.msg}»).

{v.local ? (
{v.local.generic ? Почему: префикс {v.local.wmi} — машина собрана не в Китае (VIN китайских заводов начинаются на L). Какой это завод, у нас пока не записано. : Почему: префикс {v.local.wmi} — это {v.local.brands}, сборка «{v.local.plant}».} Китайские каталоги знают VIN китайских заводов, а у машин местной сборки свой код завода — такие VIN в их базе отсутствуют. {v.local.note}
) : (
Префикс {vin.slice(0, 3)} в нашем списке местных сборок не значится. Возможные причины: марка не покрыта поставщиком, ошибка в VIN, или это местная сборка, которой ещё нет в списке (Вадим может добавить её в админке → Префиксы).
)}

Ответ сохранён — повторный ввод не спишет квоту. Другой VIN

); const setQ = (patch) => { const q = new URLSearchParams({ ...route.q, ...patch }); [...q.keys()].forEach((k) => { if (!q.get(k)) q.delete(k); }); go(`/vin/${vin}?${q}`); }; const matches = (t) => !rim || t.rim === rim; const vinYear = v.trims.filter((t) => t.vin_year); const other = v.trims.filter((t) => !t.vin_year); const sorted = (list) => [...list].sort((a, b) => (matches(b) - matches(a))); const selected = v.trims.find((t) => t.mjsid === mjsid); const rimMatchVinYear = rim ? vinYear.filter(matches) : []; const TrimRow = (t) => ( ); return (

{carTitle(v)}

в базе поставщика: {v.brand} {v.sys} {v.alias ? " — внутреннее китайское название той же машины" : " — русское название модели не задано (админка → Модели)"}
VIN
{v.vin}
Двигатель
{[v.engine, v.displacement].filter(Boolean).join(" · ")}
Коробка
{v.transmission || "—"}
Год по VIN
{v.vin_year || "—"}
Кузов
{[v.body, v.fuel].filter(Boolean).join(" · ")}
Размер колёс — по нему подбираются тормоза
Спросите клиента: {v.rims.map((r) => ( ))}
VIN размер колёс не определяет. {v.rims.map((r) => `${r.rim}: ${r.tyres.join(", ")}`).join(" · ")}
По VIN поставщик отдаёт {v.trims.length} комплектаций за {v.years.join(", ")} гг. — до одной не сужает. {rim && rimMatchVinYear.length === 1 && <> Для {v.vin_year} года и {rim} подходит одна — выберите её.} {rim && rimMatchVinYear.length > 1 && <> Для {v.vin_year} года и {rim} подходят {rimMatchVinYear.length}: для тормозов разницы обычно нет, для салона и электрики может понадобиться уточнение.}
Комплектации {v.vin_year} года{rim ? ` · ${rim} выделены` : ""}
{sorted(vinYear).map(TrimRow)} {!showAll && other.length > 0 && ( )} {showAll && sorted(other).map(TrimRow)}
{!selected && {rim ? "Выберите комплектацию" : "Сначала размер колёс, потом комплектация"}}
); } // ------------------------------------------------------------ 3. подбор const BADGE = { exact: { cls: "exact", text: "НОМЕР ЭТОЙ МАШИНЫ", hint: "API явно подтвердил номер для этой комплектации (containFlag = 0)" }, analog: { cls: "analog", text: "АНАЛОГ ПО ПОЗИЦИИ — ПРОВЕРЬТЕ", hint: "Номер этой машине не принадлежит: система подставила деталь той же позиции (containFlag = 1). Перед заказом сверьте." }, unknown: { cls: "unknown", text: "ПРИВЯЗКА НЕ УКАЗАНА", hint: "API не сообщил, родной ли это номер для комплектации (поле containFlag пустое). Точность не гарантирована." }, }; function mergeParts(parts) { // одинаковый номер с одинаковым примечанием склеиваем (API шлёт дубли с заменой и без) const m = new Map(); for (const p of parts) { const k = `${p.key}|${p.partNumber}|${p.comment || ""}`; if (!m.has(k)) { m.set(k, { ...p, substitutes: p.substitute ? [p.substitute] : [], refs: p.ref ? [p.ref] : [] }); continue; } const x = m.get(k); if (p.substitute && !x.substitutes.includes(p.substitute)) x.substitutes.push(p.substitute); if (p.ref && !x.refs.includes(p.ref)) x.refs.push(p.ref); if (x.badge !== p.badge) x.badge = x.badge === "analog" || p.badge === "analog" ? "analog" : "unknown"; } // группы: один ключ + одно название, разные номера → варианты const groups = new Map(); for (const p of m.values()) { const g = `${p.key}|${p.stdPartName}`; if (!groups.has(g)) groups.set(g, []); groups.get(g).push(p); } return [...groups.values()]; } function PartBody({ p, compact }) { return ( <> {/* Поиск поставщика нечёткий: оригинал рядом с переводом, чтобы было видно, если вернулось не то */}
{p.stdPartName_ru || перевода нет} {p.stdPartName} {p.stdPartName_src === "llm" && машинный перевод}
{p.srcPartName && p.srcPartName !== p.stdPartName && (
заводское: {p.srcPartName}{p.srcPartName_ru ? ` — ${p.srcPartName_ru}` : ""}
)} {p.comment && (
{p.comment_ru ? <>{p.comment_ru}{p.comment_src === "llm" && · машинный перевод} : перевода нет}
{p.comment}
)} {p.substitutes && p.substitutes.length > 0 && (
Замена в базе: {p.substitutes.join(", ")}
)} {!compact && p.reqPartNumber && p.reqPartNumber !== p.partNumber && (
Искали номер {p.reqPartNumber}
)} ); } function PriceBox({ p, onScheme }) { const price = parseFloat(p.partPrice); return (
{price > 0 ? <>
{price.toFixed(0)} ¥
заводская цена
:
цены в прайсе нет
}
кол-во в позиции: {p.qty || "—"}
{p.image && }
); } function PartCard({ p, onScheme }) { const b = BADGE[p.badge]; return (
{p.partNumber} {b.text}
{p.badge === "analog" &&
{b.hint}
}
); } function Variants({ list, onScheme }) { const dated = list.some((p) => p.date_split); return (
{list.length} варианта «{list[0].stdPartName_ru || list[0].stdPartName}» — {dated ? "зависят от даты сборки машины" : "смотрите примечания"}
{list.map((p) => { const b = BADGE[p.badge]; return (
{p.partNumber} {b.text}
); })}
{dated &&
Точная дата сборки по VIN не приходит — только год. Проверьте по ПТС, табличке в проёме двери или по резьбе колёсного болта.
}
); } function Parts({ vin, route, setCtx, lastSearch, setLastSearch }) { const mjsid = route.q.mjsid, rim = route.q.rim; const [v, setV] = useState(null); const [q, setQ] = useState(lastSearch && lastSearch.vin === vin && lastSearch.mjsid === mjsid ? lastSearch.q : ""); const [res, setRes] = useState(lastSearch && lastSearch.vin === vin && lastSearch.mjsid === mjsid ? lastSearch.res : null); const [busy, setBusy] = useState(false), [err, setErr] = useState(""); useEffect(() => { api(`/api/vin/${vin}`).then(setV).catch((x) => setErr(x.message)); }, [vin]); const trim = v && v.trims.find((t) => t.mjsid === mjsid); useEffect(() => { if (v) setCtx(`${carTitle(v)} · ${v.displacement || ""} · ${v.vin_year || ""}${trim && trim.frontTyre ? " · " + trim.frontTyre : ""}`); }, [v, trim]); const run = async (text) => { const qq = (text != null ? text : q).trim(); if (!qq) return; setQ(qq); setBusy(true); setErr(""); try { const r = await api("/api/search", { body: { vin, mjsid, q: qq } }); setRes(r); setLastSearch({ vin, mjsid, q: qq, res: r }); } catch (x) { setErr(x.message); } setBusy(false); }; const groups = useMemo(() => (res ? mergeParts(res.parts) : []), [res]); const onScheme = (p) => { go(`/vin/${vin}/scheme?mjsid=${mjsid}&image=${encodeURIComponent(p.image)}&prefix=${encodeURIComponent(p.prefix || (v && v.prefix) || "")}&focus=${encodeURIComponent(p.partNumber)}${rim ? `&rim=${rim}` : ""}`); }; if (!mjsid) return
Не выбрана комплектация. Выбрать
; return (
{trim ? <>Комплектация: {trim.salesName_ru || trim.salesName} · {trim.drive_ru} · {trim.frontTyre} : {mjsid}} {" · "}сменить
в работе Каталог по узлам
{ e.preventDefault(); run(); }}> setQ(e.target.value)} placeholder="колодки передние · фильтр масляный · 204000453AA" autoFocus />
Поставщик ищет только по-китайски: русское слово переводится по словарю. Можно ввести номер детали или китайское слово.
{err &&
{err}
} {res && res.mode === "no_dict" && (
Слова «{res.q}» нет в словаре — поставщику искать нечего. {res.suggestions.length > 0 ? "Возможно, вы имели в виду:" : "Добавить слово может Вадим в админке → Словарь."} {res.suggestions.length > 0 && (
{res.suggestions.map((s) => ( ))}
)}
)} {res && res.mode !== "no_dict" && ( <>
{res.mode === "oe" ? "Поиск по номеру" : res.mode === "zh" ? "Поиск по-китайски" : `Запрос к поставщику: ${res.keys.join(", ")}`} найдено {res.parts.length} номер этой машины аналог по позиции — проверьте привязка не указана
{res.parts.length === 0 && (
Поставщик ничего не нашёл.
{res.per_key.map((k) => `${k.key}: ${k.code === "402" ? "нет результата" : k.code}`).join(" · ")} {res.mode === "oe" && " · Номер отсутствует в базе поставщика или не относится к этой комплектации."}
)} {/* «Варианты» — только развилка по дате сборки. Одинаковое stdPartName у разных деталей (передний и задний сайлентблок рычага) — это разные детали, а не выбор. */} {groups.map((list) => list.length > 1 && list.some((p) => p.date_split) ? : list.map((p) => ))} )}
); } // ------------------------------------------------------------ 4. схема function Scheme({ vin, route, setCtx, lastSearch }) { const { mjsid, image, prefix, focus, rim } = route.q; const [s, setS] = useState(null), [err, setErr] = useState(""), [full, setFull] = useState(false); useEffect(() => { setS(null); setErr(""); api(`/api/scheme?${new URLSearchParams({ image, prefix: prefix || "", vin, mjsid })}`).then(setS).catch((x) => setErr(x.message)); }, [image]); useEffect(() => { setCtx("Схема узла"); }, []); useEffect(() => { const f = (e) => { if (e.key === "Escape") setFull(false); }; if (full) { addEventListener("keydown", f); return () => removeEventListener("keydown", f); } }, [full]); // пока getPartByImage отвечает 403 — список из результатов поиска с той же схемой const fromSearch = useMemo(() => { if (!lastSearch || lastSearch.vin !== vin) return []; const seen = new Set(); return lastSearch.res.parts.filter((p) => p.image === image && !seen.has(p.partNumber + p.ref) && seen.add(p.partNumber + p.ref)); }, [lastSearch, image]); const list = s && s.parts_status === "ok" ? s.parts : fromSearch; const back = `#/vin/${vin}/parts?mjsid=${mjsid}${rim ? `&rim=${rim}` : ""}`; return (
← к результатам
{err &&
{err}
} {!s && !err &&
Загружаю схему у поставщика…
} {s && (
{s.width && `${s.width}×${s.height}`} · нажмите, чтобы открыть во весь экран в работе Точки на схеме
Схема сохранена у нас — ссылки поставщика временные. Кликабельных позиций пока нет: метод деталей схемы отвечает ошибкой ({s.parts_code || "нет ответа"}), вопрос у поставщика. Номера позиций — в списке справа.
Детали на схеме
{s.parts_status !== "ok" && (
Полный список деталей узла поставщик пока не отдаёт. Показаны найденные поиском детали с этой схемы.
)} {!list.length &&
Нет данных
}
{list.map((p, i) => (
{p.ref || "–"}
{p.partNumber}
{p.stdPartName_ru || перевода нет} {p.stdPartName}
{p.comment_ru &&
{p.comment_ru}
} {BADGE[p.badge].text}
{parseFloat(p.partPrice) > 0 ? `${parseFloat(p.partPrice).toFixed(0)} ¥` : ""}
))}
{list.length > 0 && ( )}
)} {full && s && (
setFull(false)}> Схема узла
)}
); } // ------------------------------------------------------------ 5. админка const ACTIONS = { vin: "VIN", search: "поиск", scheme: "схема", login: "вход", login_fail: "неверный ПИН" }; function AdminLog({ users }) { const [f, setF] = useState({ who_f: "", errors: 0 }); const [data, setData] = useState(null), [offset, setOffset] = useState(0), [raw, setRaw] = useState(null); const qs = new URLSearchParams({ who_f: f.who_f, errors: f.errors, limit: 100, offset }); useEffect(() => { api(`/api/admin/log?${qs}`).then(setData); }, [f, offset]); const openRaw = async (id) => setRaw(await api(`/api/admin/raw/${id}`)); const set = (patch) => { setOffset(0); setF({ ...f, ...patch }); }; return (
Журнал запросов {data && · {data.total}}
{Object.entries(users).map(([k, n]) => )}
{data && data.rows.map((r) => { const ids = (r.detail && r.detail.raw_ids) || (r.raw_id ? [r.raw_id] : []); return ( ); })}
ВремяКтоДействиеЗапросОтветКэшКвотамсСырое
{fmtTime(r.ts)} {users[r.who] || r.who} {ACTIONS[r.action] || r.action} {r.query}{r.detail && r.detail.found != null && · {r.detail.found} шт} {r.code || (r.error ? "ошибка" : "ок")} {r.cache_hit ? "да" : ""} {r.frequency != null ? r.frequency : "—"} {r.duration_ms != null ? r.duration_ms : ""} {ids.map((id) => { e.preventDefault(); openRaw(id); }} style={{ marginRight: 6 }}>#{id})}
Выгрузить в CSV {offset > 0 && } {data && offset + 100 < data.total && }
{raw && (
setRaw(null)}>
e.stopPropagation()}>
Сырой ответ #{raw.id} · {raw.method} · {raw.who} · http {raw.http} · code {raw.code} · квота {raw.frequency != null ? raw.frequency : "—"}
Параметры
{JSON.stringify(raw.params, null, 2)}
Ответ как есть (userId вырезан)
{typeof raw.body === "string" ? raw.body : JSON.stringify(raw.body, null, 2)}
)}
); } const TABLE_DEF = { dict: { title: "Словарь поиска (рус → кит)", cols: [["ru", "Русское слово"], ["zh", "Китайский ключ"], ["grp", "Группа"]], pk: "id", extra: (r) => r.hits == null ? не проверено : r.hits === 0 ? 0 деталей : {r.hits} шт }, tr: { title: "Переводы (кит → рус)", cols: [["zh", "Китайский"], ["ru", "Русский"]], pk: "zh", extra: (r) => {r.source === "llm" ? "машинный" : "ручной"} }, wmi: { title: "Префиксы VIN местной сборки", cols: [["wmi", "Префикс (3 знака)"], ["plant", "Завод"], ["brands", "Марки"], ["note", "Что сказать менеджеру"]], pk: "wmi" }, alias: { title: "Русские названия моделей", cols: [["brand", "Марка (кит.)"], ["sys", "Модель у поставщика"], ["ru_name", "Название для менеджера"]], pk: "id" }, }; function AdminTable({ name }) { const def = TABLE_DEF[name]; const [rows, setRows] = useState(null), [edit, setEdit] = useState(null), [flt, setFlt] = useState(""), [err, setErr] = useState(""); const load = () => api(`/api/admin/table/${name}`).then(setRows); useEffect(() => { setRows(null); setEdit(null); load(); }, [name]); const save = async () => { setErr(""); try { await api(`/api/admin/table/${name}`, { body: edit }); setEdit(null); load(); } catch (x) { setErr(x.message); } }; const del = async (r) => { if (confirm("Удалить строку?")) { await api(`/api/admin/table/${name}/${encodeURIComponent(r[def.pk])}`, { method: "DELETE" }); load(); } }; const shown = (rows || []).filter((r) => !flt || def.cols.some(([c]) => String(r[c] || "").toLowerCase().includes(flt.toLowerCase()))); return (
{def.title} {rows && · {rows.length}}
setFlt(e.target.value)} />
{edit && (
{def.cols.map(([c, t]) => ( setEdit({ ...edit, [c]: e.target.value })} /> ))} {err && {err}}
)}
{def.cols.map(([c, t]) => )}{def.extra && {shown.map((r) => ( {def.cols.map(([c]) => )} {def.extra && } ))}
{t}}
{r[c]}{def.extra(r)} { e.preventDefault(); setEdit({ ...r, _existing: true }); }}>правка{" · "} { e.preventDefault(); del(r); }} style={{ color: "var(--err)" }}>удалить
); } function DictCheck() { const [vins, setVins] = useState([]), [vin, setVin] = useState(""), [mjsid, setMjsid] = useState(""), [v, setV] = useState(null); const [busy, setBusy] = useState(false), [res, setRes] = useState(null), [err, setErr] = useState(""); useEffect(() => { api("/api/recent").then((r) => setVins(r.filter((x) => x.found))); }, []); useEffect(() => { if (vin) api(`/api/vin/${vin}`).then(setV); }, [vin]); const run = async () => { setBusy(true); setErr(""); try { setRes(await api("/api/admin/dict/check", { body: { vin, mjsid } })); } catch (x) { setErr(x.message); } setBusy(false); }; return (
Проверка словаря
Прогоняет все китайские ключи словаря по одной машине и отмечает, сколько деталей нашёл каждый. Квоту не тратит (поиск бесплатный), идёт несколько минут.
{err &&
{err}
} {res &&
Проверено ключей: {res.checked}. Без результата: {res.zero.length ? res.zero.join(", ") : "нет"}.
}
); } function Admin({ me, setCtx, onQuota }) { const [s, setS] = useState(null), [tab, setTab] = useState("log"), [forget, setForget] = useState(""); useEffect(() => { setCtx("админка"); api("/api/admin/summary").then(setS); onQuota(); }, []); if (!me.admin) return
Только для Вадима.
; if (!s) return
загрузка…
; const doForget = async () => { if (!forget || !confirm(`Стереть ${forget} из памяти? Следующий ввод этого VIN спишет 1 запрос квоты.`)) return; await api(`/api/admin/vin/${forget.trim().toUpperCase()}`, { method: "DELETE" }); setForget(""); }; return (
Остаток квоты
{s.quota.left != null ? s.quota.left : "—"}
по последнему ответу поставщика{s.quota.updated_at ? `, ${fmtTime(s.quota.updated_at)}` : ""}
VIN определено
{s.vins_cached}
платных запросов из сервиса: {s.vin_calls}
Действий в журнале
{s.requests}
из кэша {s.cache_hits} · вызовов API {s.api_calls}
Ошибок
{s.errors}
средний ответ API {s.avg_ms != null ? `${(s.avg_ms / 1000).toFixed(1)} с` : "—"}
{[["log", "Журнал"], ["dict", "Словарь"], ["tr", "Переводы"], ["alias", "Модели"], ["wmi", "Префиксы"], ["check", "Проверка словаря"], ["vin", "Память VIN"]].map(([k, t]) => )}
{tab === "log" && } {(tab === "dict" || tab === "tr" || tab === "alias" || tab === "wmi") && } {tab === "check" && } {tab === "vin" && (
Стереть VIN из памяти
Ответы по VIN хранятся навсегда, чтобы не тратить квоту. Стирать — только если ответ поставщика был ошибочным и нужно спросить заново.
setForget(e.target.value)} />
)}
); } // ------------------------------------------------------------ приложение function App() { const [me, setMe] = useState(undefined); const [ctx, setCtx] = useState(""); const [lastSearch, setLastSearch] = useState(null); const route = useRoute(); const refreshMe = useCallback(() => api("/api/me").then(setMe).catch(() => setMe(null)), []); useEffect(() => { refreshMe(); const f = () => setMe(null); addEventListener("vc-logout", f); return () => removeEventListener("vc-logout", f); }, []); useEffect(() => { setCtx(""); }, [route.parts.join("/")]); if (me === undefined) return null; if (!me) return ; const logout = async () => { if (confirm("Выйти?")) { await api("/api/logout", { body: {} }); setMe(null); } }; const [a, vin, sub] = route.parts; let page; if (a === "vin" && vin && sub === "parts") page = ; else if (a === "vin" && vin && sub === "scheme") page = ; else if (a === "vin" && vin) page = ; else if (a === "admin") page = ; else page = ; return (<>{page}); } ReactDOM.createRoot(document.getElementById("root")).render();