pcp-tgu-ops-ui: надёжный 24ч ввод времени (своё текстовое поле вместо нативного)
Нативные input type=time/datetime-local рендерят время по локали браузера/ОС (AM/PM) и игнорируют атрибут lang в Chrome — поэтому lang=ru-RU не помогал. Заменено на собственные контролы (components/timeFields.tsx): - Time24Field — текстовый ввод ЧЧ:ММ[:СС] с валидацией, детерминированный 24ч; - DateTime24Field — нативный календарь для даты + Time24Field для времени. Подключено в окне маршрута редактора и во всех datetime-local фильтрах (Планы, заявки на карте, создание/фильтр заявок).
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
import { useEffect, useState, type CSSProperties } from "react";
|
||||
|
||||
/**
|
||||
* Ввод времени суток в строгом 24-часовом формате `ЧЧ:ММ[:СС]` (UTC).
|
||||
*
|
||||
* Нативные `<input type="time">`/`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 (
|
||||
<input
|
||||
{...rest}
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
className={className}
|
||||
style={style}
|
||||
placeholder={withSeconds ? "чч:мм:сс" : "чч:мм"}
|
||||
value={draft}
|
||||
onFocus={() => 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); // невалидный ввод — откат к последнему корректному
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Замена нативного `<input type="datetime-local">`: нативный календарь для даты
|
||||
* (у даты нет проблемы 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 (
|
||||
<span className={`tgu-dt24${className ? ` ${className}` : ""}`} style={{ display: "flex", alignItems: "center", gap: "0.3rem" }}>
|
||||
<input
|
||||
type="date"
|
||||
lang="ru-RU"
|
||||
style={{ width: "8rem" }}
|
||||
value={datePart}
|
||||
onChange={(event) => onChange(combine(event.target.value, timePart))}
|
||||
/>
|
||||
<Time24Field
|
||||
style={{ width: "4.6rem", textAlign: "center" }}
|
||||
value={timePart}
|
||||
onChange={(time) => onChange(combine(datePart, time))}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -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<HTMLInputElement>) => {
|
||||
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 (
|
||||
<label className="tgu-editor-time-stepper tgu-editor-time-stepper--seconds">
|
||||
<span>{label}</span>
|
||||
{/* lang=ru-RU — чтобы поле показывало время в 24-часовом формате, а не AM/PM. */}
|
||||
<input lang="ru-RU" type="time" step={1} value={new Date(value).toISOString().slice(11, 19)} onChange={onInput} />
|
||||
<Time24Field withSeconds value={new Date(value).toISOString().slice(11, 19)} onChange={onTimeChange} />
|
||||
<div className="tgu-editor-time-stepper__steps">
|
||||
<button type="button" onClick={() => change(-30_000)}>−30с</button>
|
||||
<button type="button" onClick={() => change(-5_000)}>−5с</button>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { TGU_2D_MAP_LAYERS, type Tgu2DMapLayerKey, type Tgu2DMapLayersState } fr
|
||||
import type { Tgu2DMapLayerAvailability } from "./Tgu2DMapLayers";
|
||||
import type { RequestsState } from "./Tgu2DMapTab";
|
||||
import { parseUtc } from "../../utils/utcTime";
|
||||
import { DateTime24Field } from "../../components/timeFields";
|
||||
|
||||
const DAY_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
@@ -105,21 +106,11 @@ export function Tgu2DMapSidebar({
|
||||
<div className="tgu-map-requests-filter">
|
||||
<label className="tgu-field">
|
||||
<span>С</span>
|
||||
<input
|
||||
lang="ru-RU"
|
||||
type="datetime-local"
|
||||
value={requestsState.fromValue}
|
||||
onChange={(e) => onRequestsFromChange(e.target.value)}
|
||||
/>
|
||||
<DateTime24Field value={requestsState.fromValue} onChange={onRequestsFromChange} />
|
||||
</label>
|
||||
<label className="tgu-field">
|
||||
<span>По</span>
|
||||
<input
|
||||
lang="ru-RU"
|
||||
type="datetime-local"
|
||||
value={requestsState.toValue}
|
||||
onChange={(e) => onRequestsToChange(e.target.value)}
|
||||
/>
|
||||
<DateTime24Field value={requestsState.toValue} onChange={onRequestsToChange} />
|
||||
</label>
|
||||
<div className="tgu-map-requests-filter__actions">
|
||||
<button
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { TguActiveTab } from "../../model/timelineTypes";
|
||||
import { DateTime24Field } from "../../components/timeFields";
|
||||
|
||||
type TguToolbarProps = {
|
||||
fromValue: string;
|
||||
@@ -77,11 +78,11 @@ export function TguToolbar({
|
||||
|
||||
<label className="tgu-field">
|
||||
<span>С</span>
|
||||
<input lang="ru-RU" type="datetime-local" value={fromValue} onChange={(event) => onFromChange(event.target.value)} />
|
||||
<DateTime24Field value={fromValue} onChange={onFromChange} />
|
||||
</label>
|
||||
<label className="tgu-field">
|
||||
<span>По</span>
|
||||
<input lang="ru-RU" type="datetime-local" value={toValue} onChange={(event) => onToChange(event.target.value)} />
|
||||
<DateTime24Field value={toValue} onChange={onToChange} />
|
||||
</label>
|
||||
|
||||
<button className="tgu-button tgu-button--primary" type="button" onClick={onApply} disabled={loading}>
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
RsaResultType
|
||||
} from "../../model/requestTypes";
|
||||
import { parseUtc } from "../../utils/utcTime";
|
||||
import { DateTime24Field } from "../../components/timeFields";
|
||||
|
||||
export type RequestFormSubmit = {
|
||||
name: string;
|
||||
@@ -200,11 +201,11 @@ export function RequestCreatePanel({
|
||||
</label>
|
||||
<label className="tgu-requests-field">
|
||||
<span>Начало</span>
|
||||
<input lang="ru-RU" type="datetime-local" value={begin} onChange={(e) => setBegin(e.target.value)} />
|
||||
<DateTime24Field value={begin} onChange={setBegin} />
|
||||
</label>
|
||||
<label className="tgu-requests-field">
|
||||
<span>Конец</span>
|
||||
<input lang="ru-RU" type="datetime-local" value={end} onChange={(e) => setEnd(e.target.value)} />
|
||||
<DateTime24Field value={end} onChange={setEnd} />
|
||||
</label>
|
||||
<label className="tgu-requests-field">
|
||||
<span>КПП</span>
|
||||
|
||||
@@ -10,6 +10,7 @@ import type { Tgu2DMapSelection } from "../tgu-map-2d/model/mapSelectionTypes";
|
||||
import type { GeoPoint, MapPolygon } from "../tgu-map-2d/model/mapTypes";
|
||||
import { generateUuid } from "../../utils/uuid";
|
||||
import { parseUtc, toUtcInputValue, toUtcIso } from "../../utils/utcTime";
|
||||
import { DateTime24Field } from "../../components/timeFields";
|
||||
import { RequestCreatePanel, type RequestFormSubmit } from "./RequestCreatePanel";
|
||||
import { RequestDetailsPanel } from "./RequestDetailsPanel";
|
||||
import { polygonToWkt } from "./requestGeometry";
|
||||
@@ -288,20 +289,16 @@ export function TguRequestsTab({ range, invalidRange, platforms }: TguRequestsTa
|
||||
<div className="tgu-map-requests-filter">
|
||||
<label className="tgu-field">
|
||||
<span>С</span>
|
||||
<input
|
||||
lang="ru-RU"
|
||||
type="datetime-local"
|
||||
<DateTime24Field
|
||||
value={requestsState.fromValue}
|
||||
onChange={(e) => setRequestsState((cur) => ({ ...cur, fromValue: e.target.value }))}
|
||||
onChange={(value) => setRequestsState((cur) => ({ ...cur, fromValue: value }))}
|
||||
/>
|
||||
</label>
|
||||
<label className="tgu-field">
|
||||
<span>По</span>
|
||||
<input
|
||||
lang="ru-RU"
|
||||
type="datetime-local"
|
||||
<DateTime24Field
|
||||
value={requestsState.toValue}
|
||||
onChange={(e) => setRequestsState((cur) => ({ ...cur, toValue: e.target.value }))}
|
||||
onChange={(value) => setRequestsState((cur) => ({ ...cur, toValue: value }))}
|
||||
/>
|
||||
</label>
|
||||
<div className="tgu-map-requests-filter__actions">
|
||||
|
||||
Reference in New Issue
Block a user