[vue3-jp-admin]Vue3 + Vuetify3でv-alertを関数的に操作する方法とPiniaでの管理

# Vue3 + Vuetify3でのv-alert関数式呼び出しの実装

【vue3-jp-admin】は長期的なプロジェクトであり、継続的に新しい技術を導入していきます。問題がある場合や開発に参加したい場合は、メール（yangrongwei1996@gmail.com）でご連絡ください。また、GitHubのissuesを通じて質問や提案をお寄せいただくことも可能です（https://github.com/yangrongwe/vue3-jp-admin/issues）。

皆さん、こんにちは！【vue3-jp-admin】にご興味を持っていただきありがとうございます。皆さんの「いいね」やコメントが、私の継続的な更新の励みになっています。:grin:

今回の実装内容は、Vue3でのv-alertの関数呼び出し機能です。デモは[こちら](https://wxcorer.com/components/alert)からご覧いただけます。:robot:

## 機能概要

- **集中状態管理**: PiniaやVuexを利用して、v-alertの追加や削除を関数経由で管理
- **複数アラートの表示**: 複数のアラートが順番に表示され、タイムアウトで自動的に消えます
- ![alert.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3833523/10154329-384d-c12a-6ba4-207e0165ba07.png)


## 実装手順

以下に、Vue3 と Vuetify3 を使った v-alert の関数式呼び出し機能の実装手順をまとめます。

### 1. v-alertコンポーネントの封装

以下のコードで v-alert コンポーネントを封装します。

```vue
<template>
  <div>
    <div v-for="(alert, index) in alerts" :key="alert.id">
      <v-alert
        v-bind="alert.props"
        :class="alertClasses"
        :style="getTopStyle(index)"
        @click="removeAlert(alert.id)"
      >
        {{ alert.message }}
      </v-alert>
    </div>
  </div>
</template>

<script lang="ts">
import { defineComponent, computed } from 'vue';
import { VAlert } from 'vuetify/components';
import { useDisplay } from 'vuetify';
import { useAlertStore } from '@/store/alert';

export default defineComponent({
  name: 'AlertComponent',
  components: {
    VAlert,
  },
  setup() {
    const alertStore = useAlertStore();
    const alerts = computed(() => alertStore.alerts);
    const { mobile } = useDisplay();

    const alertClasses = computed(() =>
      mobile.value
        ? ['tw-absolute', 'tw-z-[9999]', 'tw-inset-x-[15px]', 'tw-w-auto']
        : ['tw-absolute', 'tw-z-[9999]', 'tw-inset-x-[20%]', 'tw-w-auto']
    );

    const getTopStyle = (index: number) => {
      const topPosition = 150 + index * 80;
      return { top: `${topPosition}px` };
    };

    const removeAlert = (id: number) => {
      alertStore.removeAlert(id);
    };

    return {
      alertClasses,
      alerts,
      getTopStyle,
      removeAlert,
    };
  },
});
</script>

<style scoped lang="scss"></style>
```

### 2. Pinia の封装

Pinia ストアの実装は以下の通りです。これにより、アラートの状態管理を簡単に行えます。
```ts
import { defineStore } from 'pinia';

interface Alert {
  id: number;
  message: string;
  timeout: number;
  props: Record<string, any>;
}

export const useAlertStore = defineStore('alert', {
  state: () => ({
    alerts: [] as Alert[], // アラートのリスト
  }),
  actions: {
    // アラートを追加する
    addAlert(alert: Omit<Alert, 'id'>) {
      const id = Date.now();
      const newAlert = { ...alert, id };
      this.alerts.push(newAlert);
      if (alert.timeout) {
        setTimeout(() => {
          this.removeAlert(id);
        }, alert.timeout);
      }
    },
    // 特定の ID のアラートを削除する
    removeAlert(id: number) {
      this.alerts = this.alerts.filter((alert) => alert.id !== id);
      if (this.alerts.length === 0) {
        this.$reset();
      }
    },
    // すべてのアラートをクリアする
    clearAlerts() {
      this.alerts = [];
    },
  },
});
```
### 3. 呼び出しツールの封装

アラートを表示するためのツールを以下のコードで封装します。
```ts
import { createApp } from 'vue';
import AlertComponent from '@/components/JpAlert/index.vue';
import { useAlertStore } from '@/store/alert';
import vuetify from '@/plugins/vuetify/index.ts';

interface AlertOptions {
  message: string;
  timeout?: number;
  props?: Record<string, any>; // v-alert の props
}

// グローバル変数の作成
let appInstance: ReturnType<typeof createApp> | null = null;
let mountPoint: HTMLElement | null = null;

export function showAlert(options: AlertOptions): void {
  const { message, timeout, props = {} } = options;

  if (!appInstance) {
    // アプリケーションインスタンスの作成
    appInstance = createApp(AlertComponent);
    appInstance.use(vuetify);

    // マウントポイントの作成とマウント
    mountPoint = document.createElement('div');
    document.body.appendChild(mountPoint);
    appInstance.mount(mountPoint);
  }

  // Pinia で状態管理
  const alertStore = useAlertStore();
  alertStore.addAlert({ message, timeout, props });
}

export function clearAllAlerts(): void {
  // Pinia で管理されている全てのアラートをクリア
  const alertStore = useAlertStore();
  alertStore.clearAlerts();

  // アプリケーションインスタンスとマウントポイントをアンマウントし、削除
  if (appInstance && mountPoint) {
    appInstance.unmount();
    document.body.removeChild(mountPoint);
    appInstance = null;
    mountPoint = null;
  }
}
```
### 4. Hook の使用例

フックを使用してアラートを表示する方法は以下の通りです。
```tsx
import { showAlert, clearAllAlerts } from '@/utils/dynamicAlert';
import { MainTitle } from '@/types/index';

const mainTitle: MainTitle = {
  title: 'Vuetify3 アラート 二次封装【関数式呼び出し】',
  linkText: 'src/views/componentsDemo/alert/index.vue',
  path: 'https://github.com/yangrongwe/vue3-jp-admin/blob/main/src/views/componentsDemo/alert/index.vue',
};

const handleShowAlert = (btnIndex: number) => {
  switch (btnIndex) {
    case 1:
      showAlert({
        message: 'This is an alert default!',
        props: {
          closable: true,
        },
      });
      break;
    case 2:
      showAlert({
        message: 'This is an alert outlined!',
        props: {
          variant: 'outlined',
          closable: true,
        },
      });
      break;
    case 3:
      showAlert({
        message: 'This is an alert tonal!',
        props: {
          variant: 'tonal',
          closable: true,
        },
      });
      break;
    case 4:
      showAlert({
        message: 'This is an alert info!',
        props: {
          type: 'info',
          closable: true,
          border: 'start',
          borderColor: 'white',
        },
      });
      break;
    case 5:
      showAlert({
        message: 'This is an alert success!',
        props: {
          type: 'success ',
          closable: true,
          border: 'top',
          borderColor: 'white',
        },
      });
      break;
    case 6:
      showAlert({
        message: 'This is an alert warning!',
        props: {
          type: 'warning ',
          closable: true,
          border: 'bottom',
          borderColor: 'white',
        },
      });
      break;
    case 7:
      showAlert({
        message: 'This is an alert error!',
        props: {
          type: 'error',
          closable: true,
          border: 'end',
          borderColor: 'white',
        },
      });
      break;
    case 8:
      showAlert({
        message: 'This is an alert timeout!',
        timeout: 3000,
        props: {
          type: 'info',
          closable: true,
          border: 'start',
          borderColor: 'white',
        },
      });
      break;
    default:
      break;
  }
};

const handleClearAllAlerts = () => {
  clearAllAlerts();
};

export { handleShowAlert, handleClearAllAlerts, mainTitle };
```
最後に関数式でのコンポーネントの封装はそれほど難しくありませんが、メモリリークに注意が必要です。例えば、clearAllAlerts で Vue インスタンスを削除するのを忘れると、メモリに不要なデータが残る可能性があります。今回の実装が皆さんのプロジェクトに役立つことを願っています！