From 51fd8a446b27d309fdfb95db7d089085a08af1ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9=20=D0=A1=D0=BE?= =?UTF-8?q?=D0=BB=D0=BE=D0=B2=D1=8C=D0=B5=D0=B2?= Date: Wed, 3 Jun 2026 09:02:27 +0300 Subject: [PATCH] =?UTF-8?q?pcp-tgu-ops-ui:=20=D0=BD=D0=B0=D0=B4=D1=91?= =?UTF-8?q?=D0=B6=D0=BD=D1=8B=D0=B9=2024=D1=87=20=D0=B2=D0=B2=D0=BE=D0=B4?= =?UTF-8?q?=20=D0=B2=D1=80=D0=B5=D0=BC=D0=B5=D0=BD=D0=B8=20(=D1=81=D0=B2?= =?UTF-8?q?=D0=BE=D1=91=20=D1=82=D0=B5=D0=BA=D1=81=D1=82=D0=BE=D0=B2=D0=BE?= =?UTF-8?q?=D0=B5=20=D0=BF=D0=BE=D0=BB=D0=B5=20=D0=B2=D0=BC=D0=B5=D1=81?= =?UTF-8?q?=D1=82=D0=BE=20=D0=BD=D0=B0=D1=82=D0=B8=D0=B2=D0=BD=D0=BE=D0=B3?= =?UTF-8?q?=D0=BE)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Нативные input type=time/datetime-local рендерят время по локали браузера/ОС (AM/PM) и игнорируют атрибут lang в Chrome — поэтому lang=ru-RU не помогал. Заменено на собственные контролы (components/timeFields.tsx): - Time24Field — текстовый ввод ЧЧ:ММ[:СС] с валидацией, детерминированный 24ч; - DateTime24Field — нативный календарь для даты + Time24Field для времени. Подключено в окне маршрута редактора и во всех datetime-local фильтрах (Планы, заявки на карте, создание/фильтр заявок). --- .../src/components/timeFields.tsx | 94 +++++++++++++++++++ .../src/features/tgu-editor/EditorPanels.tsx | 11 +-- .../features/tgu-map-2d/Tgu2DMapSidebar.tsx | 15 +-- .../src/features/tgu-planning/TguToolbar.tsx | 5 +- .../tgu-requests/RequestCreatePanel.tsx | 5 +- .../features/tgu-requests/TguRequestsTab.tsx | 13 +-- 6 files changed, 112 insertions(+), 31 deletions(-) create mode 100644 services/pcp-tgu-ops-ui/src/components/timeFields.tsx diff --git a/services/pcp-tgu-ops-ui/src/components/timeFields.tsx b/services/pcp-tgu-ops-ui/src/components/timeFields.tsx new file mode 100644 index 0000000..5238cfc --- /dev/null +++ b/services/pcp-tgu-ops-ui/src/components/timeFields.tsx @@ -0,0 +1,94 @@ +import { useEffect, useState, type CSSProperties } from "react"; + +/** + * Ввод времени суток в строгом 24-часовом формате `ЧЧ:ММ[:СС]` (UTC). + * + * Нативные ``/`type="datetime-local"` рендерят время по локали + * браузера/ОС (часто AM/PM) и не подчиняются атрибуту `lang`, поэтому время вводим + * обычным текстовым полем с собственной валидацией. Локальный черновик позволяет + * печатать промежуточные значения; наружу отдаём только корректное `ЧЧ:ММ[:СС]`. + */ +type Time24FieldProps = { + /** Текущее значение в формате `ЧЧ:ММ` или `ЧЧ:ММ:СС`. */ + value: string; + /** Показывать и принимать секунды. */ + withSeconds?: boolean; + /** Вызывается при вводе корректного значения. */ + onChange: (value: string) => void; + className?: string; + style?: CSSProperties; + "aria-label"?: string; +}; + +const TIME_RE_HM = /^([01]\d|2[0-3]):[0-5]\d$/; +const TIME_RE_HMS = /^([01]\d|2[0-3]):[0-5]\d:[0-5]\d$/; + +export function Time24Field({ value, withSeconds = false, onChange, className, style, ...rest }: Time24FieldProps) { + const re = withSeconds ? TIME_RE_HMS : TIME_RE_HM; + const [draft, setDraft] = useState(value); + const [editing, setEditing] = useState(false); + + // Пока поле не редактируется — синхронизируем отображение с внешним значением. + useEffect(() => { + if (!editing) setDraft(value); + }, [value, editing]); + + return ( + setEditing(true)} + onChange={(event) => { + const next = event.target.value; + setDraft(next); + if (re.test(next)) onChange(next); + }} + onBlur={() => { + setEditing(false); + if (re.test(draft)) onChange(draft); + else setDraft(value); // невалидный ввод — откат к последнему корректному + }} + /> + ); +} + +/** + * Замена нативного ``: нативный календарь для даты + * (у даты нет проблемы AM/PM) + [Time24Field] для времени в 24-часовом формате. + * Контракт значения тот же — строка `YYYY-MM-DDTHH:mm` (UTC), как у datetime-local. + */ +type DateTime24FieldProps = { + value: string; + onChange: (value: string) => void; + className?: string; +}; + +export function DateTime24Field({ value, onChange, className }: DateTime24FieldProps) { + const sep = value.indexOf("T"); + const datePart = sep >= 0 ? value.slice(0, sep) : value; + const timePart = sep >= 0 ? value.slice(sep + 1, sep + 6) : ""; + + const combine = (date: string, time: string) => `${date}T${time || "00:00"}`; + + return ( + + onChange(combine(event.target.value, timePart))} + /> + onChange(combine(datePart, time))} + /> + + ); +} diff --git a/services/pcp-tgu-ops-ui/src/features/tgu-editor/EditorPanels.tsx b/services/pcp-tgu-ops-ui/src/features/tgu-editor/EditorPanels.tsx index af22317..58c1db1 100644 --- a/services/pcp-tgu-ops-ui/src/features/tgu-editor/EditorPanels.tsx +++ b/services/pcp-tgu-ops-ui/src/features/tgu-editor/EditorPanels.tsx @@ -1,8 +1,8 @@ import { formatDate, formatDateTime, formatDuration, formatTime, kindColor, kindLabel } from "./editorPresentation"; import { statusStyle } from "../tgu-planning/tguStatus"; import { formatUtcDateTime, parseUtc } from "../../utils/utcTime"; +import { Time24Field } from "../../components/timeFields"; import type { RvaZone } from "../../api/ballisticsApi"; -import type { ChangeEvent } from "react"; import type { SurveyRoute } from "../../api/missionPlanningApi"; import type { Tgu2DMapSelection } from "../tgu-map-2d/model/mapSelectionTypes"; import type { RequestMapItem } from "../../model/requestTypes"; @@ -710,10 +710,8 @@ function RouteTimeStepper({ }) { const clamp = (next: number) => Math.max(min ?? Number.NEGATIVE_INFINITY, Math.min(max ?? Number.POSITIVE_INFINITY, next)); const change = (deltaMs: number) => onChange(clamp(value + deltaMs)); - const onInput = (event: ChangeEvent) => { - const raw = event.target.value; - if (!raw) return; - // Ввод — только время суток; день оставляем прежним (UTC). + const onTimeChange = (raw: string) => { + // Ввод — только время суток (ЧЧ:ММ:СС, UTC); день оставляем прежним. const datePart = new Date(value).toISOString().slice(0, 10); const ms = parseUtc(`${datePart}T${raw}`); if (Number.isFinite(ms)) onChange(clamp(ms)); @@ -722,8 +720,7 @@ function RouteTimeStepper({ return (