Kurs Vue.js · Moduł 4: Computed i watch

Projekt: Dashboard Misji z Computed i Watch

4 min czytania
W tej lekcji3

Za trzy doby NOVA LAB wysyła transport w stronę Valles Marineris. Dziś operatorzy przeliczają czas dotarcia na kartkach, alarmy temperatury silnika zgłaszają przez radio, a raport dla dowództwa składają ręcznie pod koniec zmiany. Dr. Nova chce jednego panelu, który zrobi to wszystko sam: policzy parametry lotu, przypilnuje systemów krytycznych i napisze raport.

Finalny projekt Centrum Obliczeń! Zbuduj zaawansowany dashboard misji kosmicznej, który w czasie rzeczywistym oblicza parametry lotu, monitoruje systemy krytyczne i automatycznie generuje raporty. Wykorzystasz computed properties do automatycznych obliczeń i watchery do reaktywnego monitorowania zmian.

Podsumowanie modułu

W tym module poznałeś:

  • Computed properties - automatyczne obliczenia zależne od reaktywnych danych. Cachowane - przeliczają się tylko wtedy, gdy zmienia się zależność. Wywoływane bez nawiasów: {{ fuelStatus }}.
  • Computed z getter/setter - dwukierunkowe właściwości obliczane. Getter odczytuje, setter parsuje i rozdziela dane. Przydatne do konwersji jednostek i parsowania formatów.
  • watch() - ręczne obserwowanie zmian z pełną kontrolą. Jawne źródło, dostęp do oldValue/newValue, opcje immediate, deep, once. Watch na wiele źródeł jednocześnie.
  • watchEffect() - automatyczne śledzenie zależności. Nie trzeba deklarować, co obserwować - Vue wykrywa to automatycznie. Wykonuje się natychmiast. Cleanup przez onCleanup.
  • Zatrzymywanie watcherów - każdy watcher zwraca funkcję stop. Pauzowanie/wznawianie w Vue 3.5+. Warunkowe zatrzymywanie w callbacku.

Co już działa w starterze

Starter pod lekcją nie jest pustą kartką. Trzyma stan misji w ref: paliwo, prędkość, temperaturę silnika, przebyty i docelowy dystans, flagę monitoringu i dziennik. Dwa computed liczą szacowany czas dotarcia i procent ukończenia trasy, a writable computed speedKmH przelicza prędkość między km/s i km/h. Watcher temperatury powstaje w funkcji startMonitoring, a stopMonitoring zamyka go zapisanym uchwytem. watchEffect dopisuje do dziennika ostrzeżenie o niskim poziomie paliwa. Starter używa uproszczonej skali, stopni Celsjusza z progami 400 i 500. Wymagania poniżej opisują docelową wersję w kelwinach, więc przeskaluj progi i dopisz brakujące elementy.

Wymagania projektu

Zbuduj Dashboard Kontroli Misji, który zawiera:

1. Computed properties do obliczeń misji

Dane wejściowe: prędkość (km/s), odległość do celu (km), zużycie paliwa (litrów/s), pojemność zbiornika (litry). Obliczaj automatycznie: szacowany czas dotarcia, całkowite zużycie paliwa, procent pozostałego paliwa, status misji (NOMINAL/WARNING/CRITICAL).

Status wylicz jednym computed, sprawdzając warunki od najpoważniejszego: najpierw CRITICAL, potem WARNING, na końcu NOMINAL. Kolejność warunków decyduje, który stan wygra. Getter ma tylko liczyć, więc żadnych wpisów do dziennika w jego wnętrzu.

2. Watch na parametrach krytycznych

Monitoruj temperaturę silnika (input range 0-5000K). Gdy przekroczy 3500K, wyświetl ostrzeżenie "WARNING". Gdy przekroczy 4500K, wyświetl "CRITICAL" i automatycznie zmniejsz prędkość. Zapisuj historię zmian temperatury (ostatnie 10 wartości).

To zadanie dla watch, bo reakcja jest akcją: zmiana prędkości i wpis do historii. Historię prowadź jak w kalkulatorze trajektorii, przez unshift i pop. Obniżenie prędkości w callbacku nie wywoła pętli, bo zmienia inne źródło niż obserwowana temperatura.

3. watchEffect na statusie systemu

Automatycznie generuj raport tekstowy w panelu bocznym, który aktualizuje się przy każdej zmianie parametrów misji. Raport powinien zawierać aktualną prędkość, temperaturę, zużycie paliwa i szacowany czas dotarcia.

watchEffect niczego nie zwraca, więc raport zapisuj do osobnego ref, na przykład reportText. Gdyby raport miał być tylko wyświetlany, wystarczyłby computed. Tutaj ćwiczymy efekt, więc dopisz do raportu godzinę jego wygenerowania, a to już akcja, nie czysta wartość.

4. Cleanup i kontrola watcherów

Dodaj przycisk "Monitoring ON/OFF", który włącza/wyłącza watcher temperatury. Gdy monitoring jest wyłączony, zmiany temperatury nie generują alarmów. Użyj funkcji stop() i ponownego tworzenia watchera.

Kluczem jest uchwyt zwracany przez watch. Zapisz go w zmiennej poza funkcją, a przy wyłączaniu wywołaj go i wyzeruj, żeby kolejne włączenie utworzyło świeży watcher.

1let stopTempWatch = null
2
3function startMonitoring() {
4  stopTempWatch = watch(engineTemp, (newTemp) => {
5    // alarms and history
6  })
7}
8
9function stopMonitoring() {
10  if (stopTempWatch) {
11    stopTempWatch()
12    stopTempWatch = null
13  }
14}

Po wywołaniu stopTempWatch() temperatura nadal jest reaktywna i panel dalej ją pokazuje, gaśnie wyłącznie nasłuch alarmów. Vue 3.5 ma też pause() i resume(), ale w tym projekcie ćwiczymy pełny cykl: zatrzymanie i ponowne utworzenie watchera.

Polecam budować panel etapami: najpierw computed i sprawdzenie liczb, potem alarmy, na końcu raport i przełącznik. W następnym module, w Symulatorze Sterowania, poznasz zdarzenia i formularze, dzięki którym załoga będzie mogła sterować takim panelem. Rozbuduj starter poniżej!

Pamiętaj: dobry dashboard misji liczy przez computed, reaguje przez watch, a każdy kanał nasłuchu, który otworzy, potrafi też zamknąć.

Kod do tej lekcji: App.vue
1<script setup>
2import { ref, computed, watch, watchEffect } from 'vue'
3
4// Dashboard Kontroli Misji - Projekt koncowy modulu 4
5const fuelLevel = ref(100)
6const speed = ref(0)
7const engineTemp = ref(200)
8const distance = ref(0)
9const targetDistance = ref(1000)
10const monitoringActive = ref(true)
11
12const logs = ref([])
13
14// Computed: szacowany czas dotarcia
15const estimatedTime = computed(() => {
16  if (speed.value <= 0) return 'N/A'
17  const remaining = targetDistance.value - distance.value
18  return (remaining / speed.value).toFixed(1) + ' h'
19})
20
21// Computed: procent ukonczenia misji
22const missionProgress = computed(() => {
23  return Math.min(100, (distance.value / targetDistance.value * 100)).toFixed(1)
24})
25
26// Computed z getter/setter: predkosc w km/s i km/h
27const speedKmH = computed({
28  get: () => speed.value * 3600,
29  set: (val) => { speed.value = val / 3600 }
30})
31
32// Watch na temperaturze silnika
33let stopTempWatch = null
34function startMonitoring() {
35  monitoringActive.value = true
36  stopTempWatch = watch(engineTemp, (newTemp) => {
37    if (newTemp > 500) {
38      logs.value.unshift('ALARM: Temperatura krytyczna!')
39    } else if (newTemp > 400) {
40      logs.value.unshift('Ostrzezenie: Wysoka temperatura silnika')
41    }
42  })
43}
44
45function stopMonitoring() {
46  if (stopTempWatch) {
47    stopTempWatch()
48    monitoringActive.value = false
49    logs.value.unshift('Monitoring zatrzymany')
50  }
51}
52
53startMonitoring()
54
55// watchEffect: automatyczny raport
56watchEffect(() => {
57  if (fuelLevel.value < 20) {
58    logs.value.unshift('RAPORT: Niski poziom paliwa!')
59  }
60})
61
62function accelerate() { speed.value = Math.min(100, speed.value + 5); engineTemp.value += 15 }
63function decelerate() { speed.value = Math.max(0, speed.value - 5); engineTemp.value -= 10 }
64function travel() { distance.value += speed.value * 10; fuelLevel.value = Math.max(0, fuelLevel.value - 2) }
65</script>
66
67<template>
68  <div class="dashboard">
69    <h1>Dashboard Kontroli Misji</h1>
70
71    <div class="stats">
72      <div class="stat"><span>Paliwo</span><strong>{{ fuelLevel }}%</strong></div>
73      <div class="stat"><span>Predkosc</span><strong>{{ speed.toFixed(1) }} km/s</strong></div>
74      <div class="stat"><span>Temperatura</span><strong>{{ engineTemp }}°C</strong></div>
75      <div class="stat"><span>Dystans</span><strong>{{ distance.toFixed(0) }} / {{ targetDistance }} km</strong></div>
76      <div class="stat"><span>Postep</span><strong>{{ missionProgress }}%</strong></div>
77      <div class="stat"><span>ETA</span><strong>{{ estimatedTime }}</strong></div>
78    </div>
79
80    <div class="controls">
81      <button @click="accelerate">Przyspiesz</button>
82      <button @click="decelerate">Zwolnij</button>
83      <button @click="travel">Podroz (+10h)</button>
84      <button @click="monitoringActive ? stopMonitoring() : startMonitoring()">
85        {{ monitoringActive ? 'Wylacz monitoring' : 'Wlacz monitoring' }}
86      </button>
87    </div>
88
89    <div class="logs" v-if="logs.length">
90      <h3>Logi systemowe</h3>
91      <p v-for="(log, i) in logs" :key="i">{{ log }}</p>
92    </div>
93  </div>
94</template>
95
96<style scoped>
97.dashboard { background: #0a0a2e; color: #e0e0ff; padding: 2rem; min-height: 100vh; font-family: monospace; }
98h1 { color: #00d4ff; text-align: center; }
99.stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; margin: 1rem 0; }
100.stat { background: #1a1a4e; padding: 1rem; border-radius: 8px; text-align: center; }
101.stat span { display: block; color: #888; font-size: 0.8rem; }
102.stat strong { font-size: 1.2rem; color: #00d4ff; }
103.controls { display: flex; gap: 0.5rem; flex-wrap: wrap; justify-content: center; margin: 1rem 0; }
104button { background: #333; color: white; border: none; padding: 0.5rem 1rem; cursor: pointer; border-radius: 4px; }
105button:hover { background: #555; }
106.logs { background: #111; padding: 1rem; border-radius: 8px; max-height: 200px; overflow-y: auto; }
107.logs h3 { color: #ffaa00; }
108.logs p { color: #aaa; font-size: 0.85rem; margin: 0.2rem 0; }
109</style>

Widzisz błąd w tej lekcji?

Przydatne artykuły