Kurs Vue.js · Moduł 15: TypeScript i Nuxt
Projekt Główny: Typowany Panel Stacji NOVA LAB
W tej lekcji6
Czas połączyć całą wiedzę z tego modułu! Stworzymy typowany panel sterowania stacją kosmiczną NOVA LAB, który wykorzystuje TypeScript z Vue 3 i uwzględnia koncepcje SSR.
Wymagania projektu
Nasz panel sterowania powinien zawierać:
- Typowane propsy - każdy komponent deklaruje jakie dane przyjmuje
- Typowane emity - zdarzenia są precyzyjnie zdefiniowane
- Typowane ref i reactive - stan aplikacji jest w pełni typowany
- Template refs - do bezpośredniej interakcji z DOM
- Provide/Inject - do przekazywania konfiguracji stacji
Architektura aplikacji
1// types.ts - centralne typy aplikacji
2interface CrewMember {
3 id: number
4 name: string
5 role: 'commander' | 'engineer' | 'scientist' | 'pilot'
6 active: boolean
7}
8
9interface StationModule {
10 id: string
11 name: string
12 status: 'online' | 'offline' | 'maintenance'
13 power: number
14}
15
16interface StationState {
17 name: string
18 crew: CrewMember[]
19 modules: StationModule[]
20 alerts: Alert[]
21}
22
23interface Alert {
24 id: number
25 level: 'info' | 'warning' | 'critical'
26 message: string
27 timestamp: Date
28}Komponent ModuleCard z typami
1<script setup lang="ts">
2interface ModuleCardProps {
3 module: StationModule
4 isSelected?: boolean
5}
6
7const props = withDefaults(defineProps<ModuleCardProps>(), {
8 isSelected: false
9})
10
11const emit = defineEmits<{
12 select: [moduleId: string]
13 togglePower: [moduleId: string, newStatus: boolean]
14}>()
15</script>Panel z Provide/Inject
1<script setup lang="ts">
2import { provide, reactive, type InjectionKey } from 'vue'
3
4interface StationConfig {
5 name: string
6 sector: string
7 year: number
8}
9
10const StationKey: InjectionKey<StationConfig> = Symbol('Station')
11
12provide(StationKey, {
13 name: 'NOVA LAB',
14 sector: 'Alpha-7',
15 year: 2087
16})
17</script>Uwzględnienie SSR
Tworząc aplikację z myślą o SSR (np. z Nuxt), pamiętaj:
1// NIE używaj obiektów przeglądarki poza onMounted
2import { onMounted, ref } from 'vue'
3
4const windowWidth = ref(0)
5
6// DOBRZE - window dostępne tylko na kliencie
7onMounted(() => {
8 windowWidth.value = window.innerWidth
9})
10
11// ZLE - window nie istnieje na serwerze!
12// const width = window.innerWidth // BLAD w SSRComposable przyjazny SSR:
1function useScreenSize() {
2 const width = ref(0)
3 const height = ref(0)
4
5 onMounted(() => {
6 width.value = window.innerWidth
7 height.value = window.innerHeight
8
9 window.addEventListener('resize', () => {
10 width.value = window.innerWidth
11 height.value = window.innerHeight
12 })
13 })
14
15 return { width, height }
16}Pełny przykład
Poniżej znajduje się interaktywny edytor z typowaną aplikacją Vue. Przeanalizuj kod i eksperymentuj z typami - dodaj nowe właściwości, zmień typy i obserwuj jak TypeScript pomaga wykrywać błędy.
Kod do tej lekcji: App.vue
1<script setup lang="ts">
2import { ref, reactive, computed, provide, type InjectionKey } from 'vue'
3import ModuleList from './ModuleList.vue'
4import AlertPanel from './AlertPanel.vue'
5
6// ===== Typy =====
7interface CrewMember {
8 id: number
9 name: string
10 role: 'commander' | 'engineer' | 'scientist' | 'pilot'
11 active: boolean
12}
13
14interface StationModule {
15 id: string
16 name: string
17 status: 'online' | 'offline' | 'maintenance'
18 power: number
19}
20
21interface Alert {
22 id: number
23 level: 'info' | 'warning' | 'critical'
24 message: string
25}
26
27interface StationConfig {
28 name: string
29 sector: string
30 year: number
31}
32
33// ===== Provide =====
34const StationKey: InjectionKey<StationConfig> = Symbol('Station')
35provide(StationKey, { name: 'NOVA LAB', sector: 'Alpha-7', year: 2087 })
36
37// ===== Stan =====
38const modules = ref<StationModule[]>([
39 { id: 'm1', name: 'Life Support', status: 'online', power: 95 },
40 { id: 'm2', name: 'Navigation', status: 'online', power: 88 },
41 { id: 'm3', name: 'Science Lab', status: 'maintenance', power: 40 },
42 { id: 'm4', name: 'Communications', status: 'offline', power: 0 },
43 { id: 'm5', name: 'Power Grid', status: 'online', power: 72 }
44])
45
46const alerts = ref<Alert[]>([
47 { id: 1, level: 'info', message: 'System started' },
48 { id: 2, level: 'warning', message: 'Module m3 w trybie konserwacji' }
49])
50
51let alertId = 3
52
53// ===== Computed =====
54const onlineCount = computed(() => modules.value.filter(m => m.status === 'online').length)
55const avgPower = computed(() => {
56 const online = modules.value.filter(m => m.status === 'online')
57 if (online.length === 0) return 0
58 return Math.round(online.reduce((s, m) => s + m.power, 0) / online.length)
59})
60
61// ===== Metody =====
62function handleToggle(moduleId: string) {
63 const mod = modules.value.find(m => m.id === moduleId)
64 if (!mod) return
65 mod.status = mod.status === 'online' ? 'offline' : 'online'
66 mod.power = mod.status === 'online' ? 80 : 0
67 alerts.value.push({
68 id: alertId++,
69 level: mod.status === 'online' ? 'info' : 'warning',
70 message: mod.name + ' ' + (mod.status === 'online' ? 'wlaczony' : 'wylaczony')
71 })
72}
73
74function dismissAlert(id: number) {
75 alerts.value = alerts.value.filter(a => a.id !== id)
76}
77</script>
78
79<template>
80 <div class="nova-lab">
81 <header class="header">
82 <h1>NOVA LAB - Typowany Panel</h1>
83 <div class="stats">
84 <span class="stat">Online: {{ onlineCount }}/{{ modules.length }}</span>
85 <span class="stat">Avg Power: {{ avgPower }}%</span>
86 </div>
87 </header>
88
89 <AlertPanel :alerts="alerts" @dismiss="dismissAlert" />
90 <ModuleList :modules="modules" @toggle="handleToggle" />
91 </div>
92</template>
93
94<style scoped>
95.nova-lab {
96 background: #0a0e27;
97 color: #00ff88;
98 min-height: 100vh;
99 padding: 20px;
100 font-family: monospace;
101}
102.header {
103 display: flex;
104 justify-content: space-between;
105 align-items: center;
106 margin-bottom: 15px;
107 border-bottom: 1px solid rgba(0,180,216,0.3);
108 padding-bottom: 12px;
109}
110h1 { color: #00ff88; margin: 0; font-size: 18px; }
111.stats { display: flex; gap: 15px; }
112.stat {
113 background: rgba(0,180,216,0.1);
114 padding: 6px 12px;
115 border-radius: 6px;
116 font-size: 12px;
117 color: #00b4d8;
118}
119</style>Widzisz błąd w tej lekcji?