Componentes de VueJS | vasak-desktop

Guía para desarrollar componentes Vue en Vasak Desktop.

Estructura Base de un Componente

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
<template>
  <div class="audio-control">
    <h2>{{ title }}</h2>
    <input
      v-model="volume"
      type="range"
      min="0"
      max="100"
      @change="handleVolumeChange"
    />
    <span>{{ volume }}%</span>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue';
import { invoke } from '@tauri-apps/api/tauri';
import { listen, UnlistenFn } from '@tauri-apps/api/event';

import type { Device } from '@/interfaces/device';

// Props
interface Props {
  title?: string;
  disabled?: boolean;
}

const props = withDefaults(defineProps<Props>(), {
  title: 'Audio Control',
  disabled: false,
});

// State
const volume = ref(0);
const devices = ref<Device[]>([]);
const isLoading = ref(false);

// Computed
const isActive = computed(() => !props.disabled && !isLoading.value);

// Métodos
async function loadVolume() {
  try {
    isLoading.value = true;
    volume.value = await invoke<number>('get_volume');
  } catch (error) {
    console.error('Error loading volume:', error);
  } finally {
    isLoading.value = false;
  }
}

async function handleVolumeChange() {
  try {
    await invoke('set_volume', { level: volume.value });
  } catch (error) {
    console.error('Error setting volume:', error);
  }
}

// Lifecycle
onMounted(() => {
  loadVolume();
  
  // Escuchar cambios del sistema
  listen('volume_changed', (event) => {
    volume.value = event.payload as number;
  });
});

onUnmounted(() => {
  // Cleanup si es necesario
});
</script>

<style scoped>
.audio-control {
  padding: 1rem;
  border: 1px solid #ddd;
  border-radius: 0.5rem;
}

.audio-control h2 {
  margin: 0 0 1rem 0;
  font-size: 1.25rem;
}

.audio-control input {
  width: 100%;
  margin-bottom: 0.5rem;
}

.audio-control span {
  display: block;
  text-align: center;
  color: #666;
}
</style>

Tipos de Componentes

Componentes Presentacionales (Dumb)

Solo reciben props y emiten eventos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<template>
  <button
    :class="['btn', `btn-${variant}`]"
    :disabled="disabled"
    @click="$emit('click')"
  >
    <slot>Click me</slot>
  </button>
</template>

<script setup lang="ts">
interface Props {
  variant?: 'primary' | 'secondary' | 'danger';
  disabled?: boolean;
}

withDefaults(defineProps<Props>(), {
  variant: 'primary',
  disabled: false,
});

defineEmits<{
  click: [];
}>();
</script>

<style scoped>
.btn {
  padding: 0.5rem 1rem;
  border: none;
  border-radius: 0.25rem;
  cursor: pointer;
}

.btn-primary {
  background: #007bff;
  color: white;
}

.btn-secondary {
  background: #6c757d;
  color: white;
}

.btn-danger {
  background: #dc3545;
  color: white;
}
</style>

Componentes Inteligentes (Smart)

Manejan lógica y estado:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<template>
  <div class="audio-manager">
    <AudioControl
      v-if="!isLoading"
      :volume="volume"
      :devices="devices"
      @volume-change="handleVolumeChange"
      @device-change="handleDeviceChange"
    />
    <LoadingSpinner v-else />
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue';
import AudioControl from './AudioControl.vue';
import LoadingSpinner from './LoadingSpinner.vue';

const volume = ref(0);
const devices = ref([]);
const isLoading = ref(true);

async function loadData() {
  // Cargar datos del backend
}

onMounted(() => {
  loadData();
});

function handleVolumeChange(newVolume: number) {
  // Actualizar backend
}
</script>

Composables (Composiciones Reutilizables)

Para lógica compartida entre componentes:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// src/composables/useAudio.ts
import { ref, onMounted, onUnmounted } from 'vue';
import { invoke } from '@tauri-apps/api/tauri';
import { listen, UnlistenFn } from '@tauri-apps/api/event';

export function useAudio() {
  const volume = ref(0);
  const isMuted = ref(false);
  const devices = ref([]);
  const isLoading = ref(false);
  
  let unlistenVolumeChanged: UnlistenFn | null = null;

  async function loadVolume() {
    try {
      isLoading.value = true;
      volume.value = await invoke<number>('get_volume');
      isMuted = await invoke<boolean>('get_mute_status');
    } catch (error) {
      console.error('Error loading audio:', error);
    } finally {
      isLoading.value = false;
    }
  }

  async function setVolume(newVolume: number) {
    try {
      await invoke('set_volume', { level: newVolume });
      volume.value = newVolume;
    } catch (error) {
      console.error('Error setting volume:', error);
    }
  }

  async function toggleMute() {
    try {
      await invoke('toggle_mute');
      isMuted.value = !isMuted.value;
    } catch (error) {
      console.error('Error toggling mute:', error);
    }
  }

  onMounted(async () => {
    await loadVolume();
    unlistenVolumeChanged = await listen('volume_changed', (event) => {
      volume.value = event.payload as number;
    });
  });

  onUnmounted(() => {
    if (unlistenVolumeChanged) {
      unlistenVolumeChanged();
    }
  });

  return {
    volume,
    isMuted,
    devices,
    isLoading,
    loadVolume,
    setVolume,
    toggleMute,
  };
}

Uso en componente:

1
2
3
4
5
6
7
8
9
import { useAudio } from '@/composables/useAudio';

export default {
  setup() {
    const { volume, setVolume, toggleMute } = useAudio();
    
    return { volume, setVolume, toggleMute };
  }
};

Comunicación con Backend

Invocar Comandos

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import { invoke } from '@tauri-apps/api/tauri';

// Sin argumentos
const version = await invoke<string>('get_version');

// Con argumentos
const result = await invoke('set_volume', {
  level: 50
});

// Con manejo de errores
try {
  await invoke('set_volume', { level: 150 }); // Invalid
} catch (error) {
  console.error('Validation error:', error);
}

Escuchar Eventos

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import { listen, UnlistenFn } from '@tauri-apps/api/event';

let unlisten: UnlistenFn;

onMounted(async () => {
  // Escuchar evento
  unlisten = await listen('audio_volume_changed', (event) => {
    console.log('Volume changed to:', event.payload);
  });
});

onUnmounted(() => {
  // Dejar de escuchar
  if (unlisten) unlisten();
});

Manejo de Estado Global (Pinia)

Para estado compartido entre componentes:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// src/stores/audioStore.ts
import { defineStore } from 'pinia';
import { ref } from 'vue';

export const useAudioStore = defineStore('audio', () => {
  const volume = ref(0);
  const isMuted = ref(false);
  const currentDevice = ref('default');

  function setVolume(newVolume: number) {
    volume.value = newVolume;
  }

  function toggleMute() {
    isMuted.value = !isMuted.value;
  }

  return {
    volume,
    isMuted,
    currentDevice,
    setVolume,
    toggleMute,
  };
});

Uso en componente:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import { useAudioStore } from '@/stores/audioStore';

export default {
  setup() {
    const audioStore = useAudioStore();
    
    const handleVolumeChange = (newVolume: number) => {
      audioStore.setVolume(newVolume);
    };
    
    return { audioStore, handleVolumeChange };
  }
};

Slots (Contenido Dinámico)

Para componentes flexibles:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<!-- Card.vue -->
<template>
  <div class="card">
    <div class="card-header">
      <slot name="header">Default Header</slot>
    </div>
    <div class="card-body">
      <slot>Default content</slot>
    </div>
    <div class="card-footer">
      <slot name="footer">Default Footer</slot>
    </div>
  </div>
</template>

<!-- Uso -->
<Card>
  <template #header>
    <h2>Mi Card</h2>
  </template>
  
  <p>Contenido principal</p>
  
  <template #footer>
    <button @click="close">Cerrar</button>
  </template>
</Card>

Directivas Personalizadas

Para reutilizar lógica de DOM:

1
2
3
4
5
6
7
8
9
// src/directives/vFocus.ts
import { DirectiveBinding } from 'vue';

export const vFocus = {
  mounted(el: HTMLElement, binding: DirectiveBinding) {
    // Enfocar el elemento cuando se monta
    el.focus();
  },
};

Uso:

1
2
3
4
5
6
7
<template>
  <input v-focus type="text" />
</template>

<script setup>
import { vFocus } from '@/directives/vFocus';
</script>

Transiciones y Animaciones

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<template>
  <Transition name="fade">
    <div v-if="isVisible" class="content">
      Contenido que aparece/desaparece
    </div>
  </Transition>

  <TransitionGroup name="list" tag="ul">
    <li v-for="item in items" :key="item.id">
      {{ item.name }}
    </li>
  </TransitionGroup>
</template>

<script setup>
import { ref } from 'vue';

const isVisible = ref(true);
const items = ref([
  { id: 1, name: 'Item 1' },
  { id: 2, name: 'Item 2' },
]);
</script>

<style scoped>
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.3s;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}

.list-enter-active,
.list-leave-active {
  transition: all 0.3s;
}

.list-enter-from,
.list-leave-to {
  opacity: 0;
  transform: translateX(-30px);
}
</style>

Testing de Componentes

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// AudioControl.spec.ts
import { describe, it, expect, vi } from 'vitest';
import { mount } from '@vue/test-utils';
import AudioControl from './AudioControl.vue';

describe('AudioControl', () => {
  it('renders correctly', () => {
    const wrapper = mount(AudioControl, {
      props: {
        title: 'Test Volume'
      }
    });
    
    expect(wrapper.text()).toContain('Test Volume');
  });

  it('emits volume change event', async () => {
    const wrapper = mount(AudioControl);
    
    await wrapper.find('input').setValue(75);
    
    expect(wrapper.emitted('volume-change')).toBeTruthy();
    expect(wrapper.emitted('volume-change')[0]).toEqual([75]);
  });

  it('disables controls when disabled prop is true', async () => {
    const wrapper = mount(AudioControl, {
      props: {
        disabled: true
      }
    });
    
    expect(wrapper.find('input').attributes('disabled')).toBeDefined();
  });
});

Mejores Prácticas

Recuerda revisar la documentacion de   buenas practicas de VueJS para mantener buenas practivas.

Vasak group © Todos los derechos reservados