fix reactivity on saved point change

This commit is contained in:
ThePetrovich 2025-07-02 21:36:53 +08:00
parent a5bfed73a1
commit 5a1a20df6c
2 changed files with 81 additions and 112 deletions

View file

@ -9,7 +9,7 @@
Input, Input,
InputGroup, InputGroup,
InputGroupText, InputGroupText,
Icon Icon,
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
import { getForecast } from "$lib/prediction"; import { getForecast } from "$lib/prediction";
@ -58,12 +58,11 @@
let { let {
handleClickSelectOnMap = () => console.log("Select on map clicked"), handleClickSelectOnMap = () => console.log("Select on map clicked"),
handleClickPointListModal = () => console.log("Open Point List Modal") handleClickPointListModal = () => console.log("Open Point List Modal"),
}: Props = $props(); }: Props = $props();
// State // State
let isCollapsed = $state(false); let isCollapsed = $state(false);
let startPoint = $state("Custom");
// Initialize date/time // Initialize date/time
const now = new Date(); const now = new Date();
@ -71,64 +70,31 @@
let startTime = $state(now.toISOString().split("T")[1].split(".")[0]); let startTime = $state(now.toISOString().split("T")[1].split(".")[0]);
// Coordinate inputs // Coordinate inputs
let inputLat = $state($FlightParametersStore.launch_latitude.toFixed(6)); let inputLat = $derived($FlightParametersStore.start_point === "Custom"
let inputLng = $state($FlightParametersStore.launch_longitude.toFixed(6)); ? $FlightParametersStore.launch_latitude.toFixed(6)
: $SavedPointsStore.find(point => point.name === $FlightParametersStore.start_point)?.lat.toFixed(6) || "0.000000");
// Reactive effects let inputLng = $derived($FlightParametersStore.start_point === "Custom"
$effect(() => { ? $FlightParametersStore.launch_longitude.toFixed(6)
$FlightParametersStore; : $SavedPointsStore.find(point => point.name === $FlightParametersStore.start_point)?.lon.toFixed(6) || "0.000000");
}); let inputAlt = $derived($FlightParametersStore.start_point === "Custom"
? $FlightParametersStore.launch_altitude.toFixed(2)
$effect(() => { : $SavedPointsStore.find(point => point.name === $FlightParametersStore.start_point)?.alt.toFixed(2) || "0.00");
// Watch for saved points changes
$SavedPointsStore;
// This effect also depends on startPoint, which is changed by setCoordinatesFromSavedPoint
// via other state variables. To avoid an infinite loop, we only call this when startPoint changes.
const sp = startPoint;
setCoordinatesFromSavedPoint();
});
// Functions
function setCoordinatesFromSavedPoint() {
const lat = parseFloat(inputLat);
const lng = parseFloat(inputLng);
if (!isNaN(lat) && !isNaN(lng)) {
$FlightParametersStore.launch_latitude = lat;
$FlightParametersStore.launch_longitude = lng;
}
else {
const selectedOption = document.querySelector(
`#startPoint option[value="${startPoint}"]`
) as HTMLOptionElement;
if (selectedOption) {
const lat = parseFloat(selectedOption.getAttribute("data-lat") || "0");
const lng = parseFloat(selectedOption.getAttribute("data-lng") || "0");
const alt = parseFloat(selectedOption.getAttribute("data-alt") || "0");
inputLat = lat.toFixed(6);
inputLng = lng.toFixed(6);
$FlightParametersStore.launch_latitude = lat;
$FlightParametersStore.launch_longitude = lng;
$FlightParametersStore.launch_altitude = alt;
}
}
}
function setToCustomOnChange() { function setToCustomOnChange() {
if (startPoint !== "Custom") { if ($FlightParametersStore.start_point !== "Custom") {
startPoint = "Custom"; $FlightParametersStore.start_point = "Custom";
} }
} }
function applyCoordinatesFromInput() { function applyCoordinatesFromInput() {
const lat = parseFloat(inputLat); const lat = parseFloat(inputLat);
const lng = parseFloat(inputLng); const lng = parseFloat(inputLng);
const alt = parseFloat(inputAlt);
if (!isNaN(lat) && !isNaN(lng)) { if (!isNaN(lat) && !isNaN(lng)) {
$FlightParametersStore.launch_latitude = lat; $FlightParametersStore.launch_latitude = lat;
$FlightParametersStore.launch_longitude = lng; $FlightParametersStore.launch_longitude = lng;
$FlightParametersStore.launch_altitude = alt || 0; // Default to 0 if alt is NaN
} else { } else {
console.error("Invalid coordinate input"); console.error("Invalid coordinate input");
// TODO: Show validation error to user // TODO: Show validation error to user
@ -137,23 +103,27 @@
async function handleGetPrediction() { async function handleGetPrediction() {
$FlightParametersStore.launch_datetime = `${startDate}T${startTime}Z`; $FlightParametersStore.launch_datetime = `${startDate}T${startTime}Z`;
$FlightParametersStore.launch_latitude = parseFloat(inputLat);
$FlightParametersStore.launch_longitude = parseFloat(inputLng);
$FlightParametersStore.launch_altitude = parseFloat(inputAlt);
writeLocalStorage<FlightParameters>("flightParameters", $FlightParametersStore); writeLocalStorage<FlightParameters>("flightParameters", $FlightParametersStore);
getForecast($FlightParametersStore).then( getForecast($FlightParametersStore)
(data) => { .then((data) => {
console.log("Forecast request successful:", data); console.log("Forecast request successful:", data);
addToast({ addToast({
header: "Forecast Request", header: "Forecast Request",
body: "Forecast request successful!", body: "Forecast request successful!",
color: "success" color: "success",
}); });
// Handle the response data as needed // Handle the response data as needed
}).catch((error) => { })
.catch((error) => {
console.error("Error getting forecast:", error); console.error("Error getting forecast:", error);
addToast({ addToast({
header: "Forecast Error", header: "Forecast Error",
body: `Error getting forecast: ${error.message}`, body: `Error getting forecast: ${error.message}`,
color: "danger" color: "danger",
}); });
}); });
} }
@ -165,7 +135,7 @@
// Exported functions for parent components // Exported functions for parent components
export function updateLaunchPosition(lat: number, lng: number) { export function updateLaunchPosition(lat: number, lng: number) {
$FlightParametersStore.launch_latitude = lat; $FlightParametersStore.launch_latitude = lat;
$FlightParametersStore.launch_longitude = lng; $FlightParametersStore.launch_longitude = lng;
inputLat = lat.toFixed(6); inputLat = lat.toFixed(6);
inputLng = lng.toFixed(6); inputLng = lng.toFixed(6);
setToCustomOnChange(); setToCustomOnChange();
@ -191,9 +161,17 @@
isCollapsed = !isCollapsed; isCollapsed = !isCollapsed;
} }
onMount(async () => { onMount(() => {
const savedPoints = await getSavedPoints(); getSavedPoints()
SavedPointsStore.set(savedPoints); .then((points) => SavedPointsStore.set(points))
.catch((error) => {
addToast({
header: "Error Loading Points",
body: `Failed to load saved points: ${error.message}`,
color: "danger",
});
return [];
});
}); });
</script> </script>
@ -218,7 +196,7 @@
</Button> </Button>
</button> </button>
</CardHeader> </CardHeader>
{#if !isCollapsed} {#if !isCollapsed}
<CardBody> <CardBody>
<div class="d-flex gap-2"> <div class="d-flex gap-2">
@ -260,10 +238,19 @@
<FormGroup spacing="mb-2"> <FormGroup spacing="mb-2">
<Label for="startPoint" class="form-label">Точка старта:</Label> <Label for="startPoint" class="form-label">Точка старта:</Label>
<InputGroup size="sm"> <InputGroup size="sm">
<Input type="select" id="startPoint" bind:value={startPoint} onchange={setCoordinatesFromSavedPoint}> <Input
type="select"
id="startPoint"
bind:value={$FlightParametersStore.start_point}
>
<optgroup label="Сохраненные точки"> <optgroup label="Сохраненные точки">
{#each $SavedPointsStore as point} {#each $SavedPointsStore as point}
<option value={point.name} data-lat={point.lat} data-lng={point.lon} data-alt={point.alt}> <option
value={point.name}
data-lat={point.lat}
data-lng={point.lon}
data-alt={point.alt}
>
{point.name} {point.name}
</option> </option>
{/each} {/each}
@ -272,11 +259,7 @@
<option value="Custom">Custom</option> <option value="Custom">Custom</option>
</optgroup> </optgroup>
</Input> </Input>
<Button <Button color="secondary" title="Edit Saved Locations" onclick={handleClickPointListModal}>
color="secondary"
title="Edit Saved Locations"
onclick={handleClickPointListModal}
>
<span>Редакт.</span> <span>Редакт.</span>
<Icon name="journal-bookmark-fill" /> <Icon name="journal-bookmark-fill" />
</Button> </Button>
@ -286,39 +269,29 @@
<FormGroup spacing="mb-2"> <FormGroup spacing="mb-2">
<Label for="latitude" class="form-label">Широта/Долгота:</Label> <Label for="latitude" class="form-label">Широта/Долгота:</Label>
<InputGroup size="sm"> <InputGroup size="sm">
<Input <Input
id="latitude" id="latitude"
type="text" type="text"
bind:value={inputLat} bind:value={inputLat}
placeholder="Latitude" placeholder="Latitude"
onchange={setToCustomOnChange} onchange={setToCustomOnChange}
/> />
<InputGroupText>/</InputGroupText> <InputGroupText>/</InputGroupText>
<Input <Input
id="longitude" id="longitude"
type="text" type="text"
bind:value={inputLng} bind:value={inputLng}
placeholder="Longitude" placeholder="Longitude"
onchange={setToCustomOnChange} onchange={setToCustomOnChange}
/> />
<Button <Button color="success" size="sm" onclick={applyCoordinatesFromInput} title="Apply Coordinates">
color="success"
size="sm"
onclick={applyCoordinatesFromInput}
title="Apply Coordinates"
>
</Button> </Button>
</InputGroup> </InputGroup>
</FormGroup> </FormGroup>
<FormGroup spacing="mb-2"> <FormGroup spacing="mb-2">
<Button <Button color="outline-secondary" size="sm" class="w-100" onclick={handleClickSelectOnMap}>
color="outline-secondary"
size="sm"
class="w-100"
onclick={handleClickSelectOnMap}
>
Указать на карте Указать на карте
</Button> </Button>
</FormGroup> </FormGroup>
@ -331,7 +304,7 @@
id="startHeight" id="startHeight"
class="form-control-sm" class="form-control-sm"
onchange={setToCustomOnChange} onchange={setToCustomOnChange}
bind:value={$FlightParametersStore.launch_altitude} bind:value={inputAlt}
/> />
</FormGroup> </FormGroup>
<FormGroup class="flex-fill w-50" spacing="mb-2"> <FormGroup class="flex-fill w-50" spacing="mb-2">
@ -370,9 +343,7 @@
<div class="d-grid gap-1"> <div class="d-grid gap-1">
<Button color="outline-secondary" size="sm">Показать график высоты</Button> <Button color="outline-secondary" size="sm">Показать график высоты</Button>
<Button size="sm" color="primary" onclick={handleGetPrediction}> <Button size="sm" color="primary" onclick={handleGetPrediction}>Выполнить прогнозирование</Button>
Выполнить прогнозирование
</Button>
</div> </div>
</CardBody> </CardBody>
{/if} {/if}

View file

@ -18,10 +18,9 @@
import { getSavedPoints, savePoint, updatePoint, deletePoint } from '$lib/api/points'; import { getSavedPoints, savePoint, updatePoint, deletePoint } from '$lib/api/points';
// Props // Props
let { isOpen = $bindable(false), onClose = () => {} } = $props(); let { isOpen = $bindable(false), onClose = () => {}, onChange = () => {} } = $props();
// Runes // Runes
let points = $state<SavedPoint[]>([]);
let selectedPoint = $state<SavedPoint | null>(null); let selectedPoint = $state<SavedPoint | null>(null);
let newPoint = $state<SavedPoint>({ id: 0, name: '', lat: 0, lon: 0, alt: 0 }); let newPoint = $state<SavedPoint>({ id: 0, name: '', lat: 0, lon: 0, alt: 0 });
let isEditing = $state(false); let isEditing = $state(false);
@ -32,18 +31,17 @@
let modalTitle = $derived(isEditing ? 'Редактирование точки' : 'Сохраненные точки'); let modalTitle = $derived(isEditing ? 'Редактирование точки' : 'Сохраненные точки');
// Table handler // Table handler
let table = $derived(new TableHandler(points, { rowsPerPage: 10 })); let table = $derived(new TableHandler($SavedPointsStore, { rowsPerPage: 10 }));
// Sync with store
$effect(() => { $effect(() => {
points = $SavedPointsStore || []; onChange();
}); });
// On mount, fetch points // On mount, fetch points
onMount(async () => { onMount(async () => {
const pts = await getSavedPoints(); const pts = await getSavedPoints();
points = pts; $SavedPointsStore = pts;
SavedPointsStore.set(points); SavedPointsStore.set($SavedPointsStore);
}); });
// Modal controls // Modal controls
@ -64,8 +62,8 @@
function handleDeletePoint(point: SavedPoint) { function handleDeletePoint(point: SavedPoint) {
deletePoint(point.id).then(() => { deletePoint(point.id).then(() => {
points = points.filter(p => p.id !== point.id); $SavedPointsStore = $SavedPointsStore.filter(p => p.id !== point.id);
SavedPointsStore.set(points); SavedPointsStore.set($SavedPointsStore);
addToast({ addToast({
header: 'Точка удалена', header: 'Точка удалена',
body: `Точка "${point.name}" успешно удалена.`, body: `Точка "${point.name}" успешно удалена.`,
@ -80,8 +78,8 @@
function handleSavePoint() { function handleSavePoint() {
if (isEditing && selectedPoint) { if (isEditing && selectedPoint) {
updatePoint(newPoint).then(updatedPoint => { updatePoint(newPoint).then(updatedPoint => {
points = points.map(p => (p.id === updatedPoint.id ? updatedPoint : p)); $SavedPointsStore = $SavedPointsStore.map(p => (p.id === updatedPoint.id ? updatedPoint : p));
SavedPointsStore.set(points); SavedPointsStore.set($SavedPointsStore);
resetForm(); resetForm();
addToast({ addToast({
header: 'Точка обновлена', header: 'Точка обновлена',
@ -93,8 +91,8 @@
}); });
} else { } else {
savePoint(newPoint).then(savedPoint => { savePoint(newPoint).then(savedPoint => {
points = [...points, savedPoint]; $SavedPointsStore = [...$SavedPointsStore, savedPoint];
SavedPointsStore.set(points); SavedPointsStore.set($SavedPointsStore);
resetForm(); resetForm();
addToast({ addToast({
header: 'Точка сохранена', header: 'Точка сохранена',
@ -136,7 +134,7 @@
<table class="table table-sm"> <table class="table table-sm">
<thead> <thead>
<tr> <tr>
<th>Имя</th> <th>Название точки</th>
<th>Широта</th> <th>Широта</th>
<th>Долгота</th> <th>Долгота</th>
<th>Высота</th> <th>Высота</th>
@ -147,9 +145,9 @@
{#each table.rows as row} {#each table.rows as row}
<tr> <tr>
<td>{row.name}</td> <td>{row.name}</td>
<td>{row.lat}</td> <td>{row.lat} °</td>
<td>{row.lon}</td> <td>{row.lon} °</td>
<td>{row.alt}</td> <td>{row.alt} м</td>
<td class="fit"> <td class="fit">
<Button color="primary" size="sm" onclick={() => handleEditPoint(row)}> <Button color="primary" size="sm" onclick={() => handleEditPoint(row)}>
<Icon name="pencil" /> <Icon name="pencil" />
@ -189,7 +187,7 @@
</Alert> </Alert>
<form onsubmit={(e) => { e.preventDefault(); handleSavePoint(); }}> <form onsubmit={(e) => { e.preventDefault(); handleSavePoint(); }}>
<div class="mb-2"> <div class="mb-2">
<Label for="name" class="small">Имя:</Label> <Label for="name" class="small">Название точки:</Label>
<Input class="form-control-sm" type="text" id="name" bind:value={newPoint.name} required /> <Input class="form-control-sm" type="text" id="name" bind:value={newPoint.name} required />
</div> </div>
<div class="d-flex gap-2"> <div class="d-flex gap-2">