gnss/frontend/src/components/SettingsPanel.tsx
 Илья Шалонин 3d65d2b7e8 Крутой дизайн
2025-12-19 14:11:32 +05:00

278 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type { MutableRefObject, RefObject } from "react";
import { FormEvent } from "react";
import type { AppConfig, SliceParams } from "../types";
type SettingsPanelProps = {
open: boolean;
onClose: () => void;
config: AppConfig | null;
status: string;
theme: "dark" | "light";
onToggleTheme: () => void;
tilesets: string[];
activeTileset: string;
onActivateTileset: (name: string) => void;
onRefreshTilesets: () => void;
onCreateTileset: (name: string) => void;
sliceParams: SliceParams;
onSliceParamsChange: (params: SliceParams) => void;
sliceStatus: string;
onSliceSubmit: (event?: FormEvent<HTMLFormElement>) => void;
onSliceReset: () => void;
selectedFileName: string;
onTriggerFileSelect: () => void;
onFileSelected: (file: File | null | undefined) => void;
fileInputRef: RefObject<HTMLInputElement | null> | MutableRefObject<HTMLInputElement | null>;
};
export const SettingsPanel = ({
open,
onClose,
config,
status,
theme,
onToggleTheme,
tilesets,
activeTileset,
onActivateTileset,
onRefreshTilesets,
onCreateTileset,
sliceParams,
onSliceParamsChange,
sliceStatus,
onSliceSubmit,
onSliceReset,
selectedFileName,
onTriggerFileSelect,
onFileSelected,
fileInputRef,
}: SettingsPanelProps) => (
<>
<div className={`settings-backdrop ${open ? "open" : ""}`} onClick={onClose} />
<aside className={`settings-panel ${open ? "open" : ""}`}>
<div className="settings-header">
<div>
<div className="panel-title">Настройки</div>
<div className="panel-subtitle">Управление источниками и нарезкой</div>
</div>
</div>
<div className="settings-content">
<section className="panel info">
<div className="info-grid">
<div className="card">
<div className="card-label">GNSS источник</div>
<div className="card-value">{config?.gnss_source ?? "—"}</div>
</div>
<div className="card">
<div className="card-label">Набор тайлов</div>
<div className="card-value">{activeTileset || "—"}</div>
</div>
<div className="card">
<div className="card-label">Статус</div>
<div className="card-value">{status}</div>
</div>
</div>
<div className="panel-header">
<div>
<div className="panel-title">Тема</div>
<div className="panel-subtitle">Светлая или тёмная</div>
</div>
<button type="button" className="btn ghost small" onClick={onToggleTheme}>
{theme === "dark" ? "Включить светлую" : "Включить тёмную"}
</button>
</div>
<div className="panel-header">
<div>
<div className="panel-title">Наборы тайлов</div>
<div className="panel-subtitle">Активация и обновление</div>
</div>
</div>
<div className="tileset-row stacked">
<select value={activeTileset} onChange={(e) => onActivateTileset(e.target.value)}>
{tilesets.map((t) => (
<option key={t} value={t}>
{t}
</option>
))}
</select>
<button type="button" className="btn ghost" onClick={onRefreshTilesets}>
Обновить
</button>
</div>
<div className="panel-header">
<div>
<div className="panel-title">Нарезка тайлов</div>
<div className="panel-subtitle">Запускает gdal2tiles на бэкенде</div>
</div>
<div className={`badge ${sliceStatus === "ok" ? "ok" : sliceStatus === "idle" ? "" : "warn"}`}>
{sliceStatus}
</div>
</div>
<form
className="slice-grid"
onSubmit={(event) => {
event.preventDefault();
onSliceSubmit(event);
}}
>
<label className="field">
<span>Источник (GeoTIFF/MBTiles)</span>
<div className="file-row">
<input
type="text"
placeholder="/path/to/input.tif"
value={sliceParams.source}
onChange={(e) => onSliceParamsChange({ ...sliceParams, source: e.target.value })}
required
/>
<button type="button" className="btn ghost" onClick={onTriggerFileSelect}>
Выбрать
</button>
<input
type="file"
accept=".tif,.tiff,.mbtiles,.jpg,.jpeg,.png"
ref={fileInputRef}
style={{ display: "none" }}
onChange={(e) => onFileSelected(e.target.files?.[0])}
/>
</div>
{selectedFileName && <small className="muted">Файл: {selectedFileName}</small>}
</label>
<label className="field">
<span>Схема тайлов</span>
<select
value={sliceParams.scheme}
onChange={(e) => onSliceParamsChange({ ...sliceParams, scheme: e.target.value as "xyz" | "tms" })}
>
<option value="xyz">XYZ (Y сверху, Leaflet)</option>
<option value="tms">TMS (Y снизу)</option>
</select>
</label>
<label className="field">
<span>Формат исходника</span>
<select
value={sliceParams.source_format}
onChange={(e) =>
onSliceParamsChange({
...sliceParams,
source_format: e.target.value as "geotiff" | "mbtiles" | "tms" | "xyz",
})
}
>
<option value="geotiff">GeoTIFF/растровый TIFF</option>
<option value="mbtiles">MBTiles (растровый)</option>
<option value="tms">Папка TMS</option>
<option value="xyz">Папка XYZ</option>
</select>
</label>
<label className="field">
<span>Tileset (имя папки)</span>
<div className="file-row">
<input
type="text"
list="tilesets-list"
placeholder="например: moscow (пусто → активный)"
value={sliceParams.tileset}
onChange={(e) => onSliceParamsChange({ ...sliceParams, tileset: e.target.value })}
/>
<button
type="button"
className="btn ghost"
onClick={() => {
const name = sliceParams.tileset.trim();
if (!name) return;
onCreateTileset(name);
}}
>
Создать
</button>
</div>
<datalist id="tilesets-list">
{tilesets.map((t) => (
<option key={t} value={t} />
))}
</datalist>
<small className="muted">Активный: {activeTileset || "—"}</small>
</label>
<label className="field">
<span>Папка тайлов (опц.)</span>
<input
type="text"
placeholder="лучше не трогать (по умолчанию tileset)"
value={sliceParams.destination}
onChange={(e) => onSliceParamsChange({ ...sliceParams, destination: e.target.value })}
/>
</label>
<label className="field">
<span>Min zoom</span>
<input
type="number"
min={0}
value={sliceParams.min_zoom}
onChange={(e) =>
onSliceParamsChange({
...sliceParams,
min_zoom: Number(e.target.value),
})
}
required
/>
</label>
<label className="field">
<span>Max zoom</span>
<input
type="number"
min={0}
value={sliceParams.max_zoom}
onChange={(e) =>
onSliceParamsChange({
...sliceParams,
max_zoom: Number(e.target.value),
})
}
required
/>
</label>
<label className="field">
<span>Формат тайлов (сейчас поддерживается PNG)</span>
<select
value={sliceParams.tile_format}
onChange={(e) => onSliceParamsChange({ ...sliceParams, tile_format: e.target.value })}
>
<option value="png">png</option>
</select>
</label>
<divz>
<div className="actions-row">
<button type="submit" className="btn primary">
Запустить
</button>
<button type="button" className="btn ghost" onClick={onSliceReset}>
Сброс
</button>
</div>
</divz>
</form>
</section>
<div className="bottom-settings">
<button type="button" className="btn primary block" onClick={onClose}>
Закрыть
</button>
</div>
</div>
</aside>
</>
);