pcp-tgu-ops-ui: панель «Параметры включения» в инспекторе маршрута

- WorkInspector показывает аппарат, виток, крен, угол захвата, центр контура (съёмка)
  и станцию/сбрасываемые съёмки (сброс)
- planModesToWorks/addShoot прокидывают planModeId, revolution, rollDeg, captureAngleDeg,
  centerLat/Lon, surveyIds в модель включения
- место под перечень заданий маршрута (привязка — позже)
- секция «Станция приёма» рендерится только в режиме правки
This commit is contained in:
Дмитрий Соловьев
2026-06-03 12:40:53 +03:00
parent e060e13b85
commit 80e281dd6d
6 changed files with 141 additions and 15 deletions
@@ -29,6 +29,10 @@ type WorkInspectorProps = {
work: EditorWork;
modes: EditorMode[];
stations: EditorGroundStation[];
/** Название аппарата редактора — для блока параметров включения. */
spacecraftLabel?: string;
/** Метка съёмки по id режима — для перечня сбрасываемых маршрутов у сброса. */
surveyModeLabel?: (surveyModeId: number) => string;
conflicts: string[];
/** Включение существующего плана — показываем только информацию, без правок. */
readOnly?: boolean;
@@ -118,6 +122,8 @@ export function WorkInspector({
work,
modes,
stations,
spacecraftLabel,
surveyModeLabel,
conflicts,
readOnly,
onUpdate,
@@ -160,6 +166,58 @@ export function WorkInspector({
)}
</section>
<section className="tgu-editor-panel__section">
<div className="tgu-editor-eyebrow">Параметры включения</div>
<dl className="tgu-map-request-details__grid">
<dt>Аппарат</dt>
<dd>{spacecraftLabel ?? "—"}</dd>
<dt>Виток</dt>
<dd className="mono">{work.revolution ?? "—"}</dd>
{work.kind === "shoot" && (
<>
<dt>Крен</dt>
<dd className="mono">{work.rollDeg != null ? `${work.rollDeg.toFixed(1)}°` : "—"}</dd>
<dt>Угол захвата</dt>
<dd className="mono">{work.captureAngleDeg != null ? `${work.captureAngleDeg.toFixed(1)}°` : "—"}</dd>
<dt>Центр контура</dt>
<dd className="mono">
{work.centerLat != null && work.centerLon != null
? `${work.centerLat.toFixed(2)}, ${work.centerLon.toFixed(2)}`
: "—"}
</dd>
</>
)}
{work.kind === "downlink" && (
<>
<dt>Станция</dt>
<dd>{stations.find((station) => station.id === work.stationId)?.name ?? work.stationId ?? "—"}</dd>
</>
)}
</dl>
</section>
{work.kind === "shoot" && (
<section className="tgu-editor-panel__section">
<div className="tgu-editor-eyebrow">Задания под маршрутом</div>
<div className="tgu-editor-panel__hint">Привязка заданий к маршруту появится позже.</div>
</section>
)}
{work.kind === "downlink" && (
<section className="tgu-editor-panel__section">
<div className="tgu-editor-eyebrow">Сбрасываемые съёмки</div>
{work.surveyIds && work.surveyIds.length > 0 ? (
<ul className="tgu-editor-panel__list">
{work.surveyIds.map((surveyId) => (
<li key={surveyId}>{surveyModeLabel ? surveyModeLabel(surveyId) : `Маршрут #${surveyId}`}</li>
))}
</ul>
) : (
<div className="tgu-editor-panel__hint">Маршруты съёмки к сбросу не привязаны.</div>
)}
</section>
)}
{work.kind === "shoot" && (
<section className="tgu-editor-panel__section">
<div className="tgu-editor-eyebrow">Режим аппаратуры</div>
@@ -183,20 +241,16 @@ export function WorkInspector({
</section>
)}
{work.kind === "downlink" && (
{work.kind === "downlink" && !readOnly && (
<section className="tgu-editor-panel__section">
<div className="tgu-editor-eyebrow">Станция приёма</div>
{readOnly ? (
<div className="tgu-editor-panel__hint">{stations.find((station) => station.id === work.stationId)?.name ?? work.stationId ?? "—"}</div>
) : (
<select value={work.stationId ?? ""} onChange={(event) => onUpdate(work.id, { stationId: event.target.value })}>
{stations.map((station) => (
<option key={station.id} value={station.id}>
{station.name}
</option>
))}
</select>
)}
<select value={work.stationId ?? ""} onChange={(event) => onUpdate(work.id, { stationId: event.target.value })}>
{stations.map((station) => (
<option key={station.id} value={station.id}>
{station.name}
</option>
))}
</select>
</section>
)}
@@ -453,7 +453,12 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
endMs,
label: mode?.label ?? "Съёмка",
modeId,
target
target,
revolution: surveyRoute?.revolution ?? selectedShootPass.revolution,
rollDeg: surveyRoute?.roll ?? routeRoll,
captureAngleDeg: surveyRoute?.captureAngle ?? DEFAULT_CAPTURE_ANGLE_DEG,
centerLat: surveyRoute?.lat,
centerLon: surveyRoute?.longitude
})
);
setSelectedWorkId(id);
@@ -468,6 +473,17 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
[stationDtos]
);
// Метка съёмки по id режима — для перечня сбрасываемых маршрутов в инспекторе сброса.
const surveyModeLabel = useCallback(
(surveyModeId: number) => {
const mode = planModes.find(
(m): m is PlanSurveyMode => m.type === "SURVEY" && m.id === surveyModeId
);
return mode ? `Съёмка · виток ${mode.revolution}` : `Маршрут #${surveyModeId}`;
},
[planModes]
);
const addDropWork = async () => {
if (!selectedPlan || !selectedZone || selectedDropSurveyIds.size === 0) return;
const numericNorad = noradId ? Number(noradId) : NaN;
@@ -735,6 +751,8 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
work={selectedWork}
modes={modes}
stations={stations}
spacecraftLabel={activeLabel}
surveyModeLabel={surveyModeLabel}
conflicts={conflicts[selectedWork.id] ?? []}
readOnly={selectedWork.origin === "plan"}
onUpdate={onChangeWork}
@@ -88,6 +88,11 @@ describe("editorDraft", () => {
startMs: parseUtc("2026-05-29T01:10:00Z"),
endMs: parseUtc("2026-05-29T01:10:00Z") + 180_000,
origin: "plan",
planModeId: 11,
revolution: 4321,
rollDeg: 0,
centerLat: 55.7,
centerLon: 37.6,
targetLat: 55.7,
targetLon: 37.6
},
@@ -98,7 +103,10 @@ describe("editorDraft", () => {
startMs: parseUtc("2026-05-29T01:40:00Z"),
endMs: parseUtc("2026-05-29T01:40:00Z") + 120_000,
origin: "plan",
stationId: "102"
planModeId: 22,
revolution: 4322,
stationId: "102",
surveyIds: [11]
}
]);
});
@@ -22,6 +22,12 @@ export type AddShootInput = {
label: string;
modeId?: string;
target: EditorTarget;
/** Параметры построенного маршрута (если съёмка строилась с превью). */
revolution?: number;
rollDeg?: number;
captureAngleDeg?: number;
centerLat?: number;
centerLon?: number;
};
export type AddDownlinkInput = {
@@ -66,6 +72,12 @@ export function planModesToWorks(modes: PlanMode[]): EditorWork[] {
startMs,
endMs,
origin: "plan",
planModeId: mode.id,
revolution: mode.revolution,
rollDeg: mode.roll,
centerLat: mode.lat,
centerLon: mode.longitude,
// Центр контура — он же опорная точка для проверки видимости.
targetLat: mode.lat,
targetLon: mode.longitude
}];
@@ -78,7 +90,10 @@ export function planModesToWorks(modes: PlanMode[]): EditorWork[] {
startMs,
endMs,
origin: "plan",
stationId: mode.station ?? undefined
planModeId: mode.id,
revolution: mode.revolution,
stationId: mode.station ?? undefined,
surveyIds: mode.surveys
}];
});
}
@@ -136,6 +151,11 @@ export function addShoot(draft: Draft, input: AddShootInput): Draft {
targetName: input.target.name,
targetLat: input.target.lat,
targetLon: input.target.lon,
revolution: input.revolution,
rollDeg: input.rollDeg,
captureAngleDeg: input.captureAngleDeg,
centerLat: input.centerLat,
centerLon: input.centerLon,
origin: "added",
isNew: true
};
@@ -20,11 +20,28 @@ export type EditorShootWork = EditorWorkBase & {
targetName?: string;
targetLat?: number;
targetLon?: number;
/** Идентификатор сохранённого режима съёмки в БД (есть у включений плана). */
planModeId?: number;
/** Номер витка. */
revolution?: number;
/** Крен (gamma), град. */
rollDeg?: number;
/** Угол захвата (полуширина полосы), град. Есть только у свежепостроенного маршрута. */
captureAngleDeg?: number;
/** Центр контура съёмки, град. */
centerLat?: number;
centerLon?: number;
};
export type EditorDownlinkWork = EditorWorkBase & {
kind: "downlink";
stationId?: string;
/** Идентификатор сохранённого режима сброса в БД (есть у включений плана). */
planModeId?: number;
/** Номер витка зоны радиовидимости. */
revolution?: number;
/** Идентификаторы режимов съёмки (survey_modes), сбрасываемых в этом сеансе. */
surveyIds?: number[];
};
export type EditorWork = EditorShootWork | EditorDownlinkWork;
@@ -47,6 +47,15 @@
font-size: 0.68rem;
}
.tgu-editor-panel__list {
margin: 0;
padding-left: 1.1rem;
display: flex;
flex-direction: column;
gap: 0.2rem;
font-size: 0.72rem;
}
.tgu-editor-toolbar__group {
display: flex;
gap: 0.35rem;