【L1】Vuetify 3で画面を作成してみよう

# はじめに

2023年12月08日分のアドベントカレンダーの記事になります。

こんにちは、マーズフラッグの川嶋です。
社内では主にバックエンドの開発をしています。
フロントエンドの技術要素の調査はすれど、業務/個人プロジェクト含め、実際のコーディングする機会からはしばらく離れていたので、リハビリ兼ねてVuetifyを利用した画面を作成してみたいと思います。

# この記事で分かること

* Vuetify3によるレイアウト作成の容易さ
* Vuetify3のテーマ変更

本当は、figma+tailwindにてデザインして、figmaからコード生成するような一連の流れをやってみようとおもったんですが、以下の理由からVuetifyになりました。

* ドキュメント、サンプルコードが充実
    これほど資料が充実したUIフレームワークはなかなかないのではないしょうか。
* 業務で利用しているのはVue.js。
    個人的な好みとしてはReactほうが良いのですけどね。今回は業務に役立ちそうな技術要素を優先で！
* vue.jsに対応したfigmaからコード生成するプラグインが個人的に刺さるものがみつからなかった。
    tailwindに対応したVue.jsのUIフレームワーク（CSSだけでなくVuetifyのようなコンポーネントフレームワーク）は、他の方が調査されている記事を見ましたが、そんなになさそうなのと、まだ実際にさわれていないです。

※余談ですが、Vuetifyもfigma向けのUI Kit（コード生成のプラグインではない）を提供しています。こちらはMaterial Designeでのデザインで、あくまでfigma上でデザイン時に利用するfigmaコンポーネント群として提供されています。

Vuetify UI Kits
https://vuetifyjs.com/en/resources/ui-kits/#figma-plugin


# 目的

* よくあるレイアウトを作成
* パネル表示
* テーブル表示
* テーマの変更
* テーマの変更（独自テーマ）

# 実践

## 準備
今回は、プロジェクトの作成などは説明しません。
手順は以下のVuetifyの資料に記載があるのでそちらを参照していただければと。
ちなみに僕はpnpm+typescriptではじめました。

get started with vuetify-3
https://vuetifyjs.com/en/getting-started/installation/#get-started-with-vuetify-3


## よくあるレイアウトを作成

### 利用するパーツ
今回作成するレイアウトはこんな感じのものです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1784431/48f678ea-6678-8d7a-af6d-cdc67994df4e.png)

以下を参考に作成してきます。
Application Layout
https://vuetifyjs.com/en/features/application-layout/

Components
https://vuetifyjs.com/en/components/all/#components

* Vuetifyの部品としては以下のコンポーネントを利用することレイアウトの骨格を作成。
    * 全体のコンテナとしては、v-app
    * ①のナビゲーションはv-navigation-drawer
    * ②の上段ヘッダーはv-app-bar
    * ③のメインコンテンツはv-main
    * ついでに下段のフッターはv-footer


* 更に以下のコンポーネント等を装飾すればできあがり。
    * アイコンはv-icon
    * リストはv-list
    * ボタンはv-btn

* アイコンはMaterial Designのアイコンをインストールして利用してます。
Vuetifyの以下Nuxt3の説明部分で記載されています。
https://vuetifyjs.com/en/getting-started/installation/#using-nuxt-3

### テンプレートのコード
```vuejs
<script setup lang="ts">
    //
</script>

<template>
  <v-app class="rounded rounded-md">
    <!-- 左ナビゲーション -->
    <v-navigation-drawer>
      <div class="d-flex px-2 my-2">
        <v-btn color="primary" height="40" variant="flat" size="large"
          ><v-icon>mdi-home-outline</v-icon>MARS Platform</v-btn
        >
        <v-btn
          color="grey"
          height="40"
          variant="text"
          size="large"
          class="pl-0 ml-n3 font-weight-bold"
          ><v-icon>mdi-chevron-double-left</v-icon></v-btn
        >
      </div>

      <v-list>
        <v-list-item>
          <v-btn color="secondary" variant="text" size="x-large"
            ><v-icon>mdi-home-outline</v-icon>メニューその１</v-btn
          >
        </v-list-item>

        <v-list-item>
          <v-btn color="secondary" variant="text" size="x-large"
            ><v-icon>mdi-file-search-outline</v-icon>メニューその２
          </v-btn>
        </v-list-item>
        <v-list-item>
          <v-btn color="secondary" variant="text" size="x-large">
            <v-icon>mdi-cloud-download-outline</v-icon>メニューその３
          </v-btn>
        </v-list-item>
        <v-list-item>
          <v-btn color="secondary" variant="text" size="x-large"
            ><v-icon>mdi-chart-bar</v-icon>メニューその４
          </v-btn>
        </v-list-item>
      </v-list>
    </v-navigation-drawer>

    <!-- 上段バー -->
    <v-app-bar>
      <v-toolbar color="primary">
        <v-toolbar-title>
          <v-breadcrumbs
            :items="['階層１', '階層２', '階層３']"
          ></v-breadcrumbs>
        </v-toolbar-title>

        <v-spacer></v-spacer>
        <v-spacer></v-spacer>

        <v-toolbar-items>
          <v-divider vertical></v-divider>

          <v-btn variant="text">組織その１</v-btn>

          <v-divider vertical></v-divider>

          <v-btn variant="text"><v-icon>mdi-information-outline</v-icon></v-btn>

          <v-divider vertical></v-divider>

          <v-btn variant="text"><v-icon>mdi-account-outline</v-icon></v-btn>
        </v-toolbar-items>

        <v-divider vertical></v-divider>

        <v-app-bar-nav-icon class="ms-2"></v-app-bar-nav-icon>
      </v-toolbar>
    </v-app-bar>

    <!-- メインコンテンツ -->
    <v-main class="d-flex align-center" style="min-height: 300px"> </v-main>

    <!-- 下段ナビゲーション -->
    <v-bottom-navigation>
      <v-btn value="recent">
        <v-icon>mdi-history</v-icon>

        <span>Recent</span>
      </v-btn>

      <v-btn value="favorites">
        <v-icon>mdi-heart</v-icon>
        <span>Favorites</span>
      </v-btn>

      <v-btn value="nearby">
        <v-icon>mdi-map-marker</v-icon>

        <span>Nearby</span>
      </v-btn>
    </v-bottom-navigation>
  </v-app>
</template>

<style scoped></style>

```

### 表示結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1784431/8cd73713-bbb4-04f9-dc02-746a364c699b.png)

## パネル表示

レイアウトだけだとつまらないので、メインコンテンツにパネルとテーブルを表示してみましょう！

### 利用するパーツ

まずはパネル表示で前述同様に、資料を参照しながらパネルコンポートネントとして以下を利用します。
* パネルコンポーネントを複数格納時によしな並べて表示してくれるコンテナコンポーネント：v-container, v-row, v-col
* パネルはv-card
* パネル内部パーツは、v-card-item等のv-card-*

### コード

変更前
```vuejs
    <!-- メインコンテンツ -->
    <v-main class="d-flex align-center" style="min-height: 300px">
    </v-main>
```

変更後
```vuejs
    <!-- メインコンテンツ -->
    <v-main class="d-flex align-center" style="min-height: 300px">
      <v-container>
        <!-- カード表記 -->
        <v-row align="center" justify="center">
          <v-col v-for="i in 3" :key="i" cols="auto">
            <v-card class="mx-auto" min-width="300">
              <v-card-item>
                <v-card-title> カードタイトル その{{ i }} </v-card-title>
                <v-card-subtitle> サブタイトル その{{ i }}</v-card-subtitle>
                <v-card-text> 内容その{{ i }} </v-card-text>
              </v-card-item>

              <v-card-actions>
                <v-btn color="primary"> Button </v-btn>
              </v-card-actions>
            </v-card>
          </v-col>
        </v-row>
      </v-container>
    </v-main>
```

### 利用するパーツ

テーブル表示は以下のコンポートネントを利用します。このままペライチのコードに記載してもよいのですが、可読性が低下するので、テーブル部分のコードは別コンポーネントコードとしてファイルを分けて記載して、ペライチから参照する形で作成してみましょう！

* src/components/SampleDataTable.vueを作成し、そこにテーブルの詳細コードを記載
* テーブルはv-data-tableを利用
* テーブルに表示データはSampleDataTable.vueにハードコーディングしたものを表示
* テーブルはソート、ページャーも表示

### コード

src/components/SampleDataTable.vue
「(sortBy as any)」「(headers as any)」はTypescriptの型エラーがでてしまうので、型を指定するのを省略してanyでとりあえずエラーをでないようにしました。（横着してすいません）

```vuejs
<script setup lang="ts">
import { ref } from "vue";
const sortBy = ref([{ key: "col2", order: "asc" }]);

const headers = [
  {
    title: "ヘッダその１",
    align: "start",
    sortable: false,
    key: "col1",
  },
  { title: "ヘッダその２", key: "col2" },
  { title: "ヘッダその３", key: "col3" },
  { title: "ヘッダその４", key: "col4" },
  { title: "ヘッダその５", key: "col5" },
  { title: "ヘッダその６", key: "col6" },
];
const rowData = [
  {
    col1: "行その１",
    col2: 10002,
    col3: 10003,
    col4: 10004,
    col5: 10005,
    col6: "その１ー６",
  },
  {
    col1: "行その２",
    col2: 20002,
    col3: 20003,
    col4: 20004,
    col5: 20005,
    col6: "その２ー６",
  },
  {
    col1: "行その３",
    col2: 30002,
    col3: 30003,
    col4: 30004,
    col5: 30005,
    col6: "その３ー６",
  },
];
</script>

<script lang="ts">
export default {
  data() {
    return {
      sortBy: [{ key: "calories", order: "asc" }],
      headers: [],
      rowData: [],
    };
  },
};
</script>

<template>
  <v-data-table
    v-model:sort-by="(sortBy as any)"
    :headers="(headers as any)"
    :items="rowData"
  ></v-data-table>
</template>

```

ペライチから上記のテーブル定義を参照する
```vuejs

<script setup lang="ts">
// 別ファイルのコンポーネントを参照するのでここでインポート
import SampleDataTable from "@/components/SampleDataTable.vue";
</script>

<template>
  <v-app class="rounded rounded-md">
    <!-- 左ナビゲーション -->
        ・・・省略・・・

    <!-- メインコンテンツ -->
    <v-main class="d-flex align-center" style="min-height: 300px">
      <v-container>
        <!-- カード表記 -->
            ・・・省略・・・
            
        <!-- テーブル表記 -->
        <v-row align="center" justify="center">
　        <!-- テーブルのコードを定義している別コンポーネントファイルを参照  -->
          <SampleDataTable />
        </v-row>
      </v-container>
    </v-main>

```

### 表示結果

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1784431/8d862ac3-d51e-c2ed-8647-28d0017a7abe.png)

## テーマの変更（ダークモード）

### 利用するパーツ

利用するパーツは、特にありませんがVuetify3では既に通常のテーマの他に、Darkモードを追加されていますのでダークモードを設定してみましょう。main.tsを修正することで対応可能です。

### コード

```typescript
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";

// Vuetify
import "vuetify/styles";
import { createVuetify } from "vuetify";
import * as components from "vuetify/components";
import * as directives from "vuetify/directives";
import "@mdi/font/css/materialdesignicons.css";

// import "./style.css";

const vuetify = createVuetify({
  components,
  directives,
  icons: {
    defaultSet: "mdi",
  },
  // 以下を追加することでDarkモードになる
  theme: {
    defaultTheme: "dark",
  },  
});

createApp(App).use(router).use(vuetify).mount("#app");
```

### 表示結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1784431/4d1063b6-82b0-c809-5b2d-eecfeb6ef711.png)


## テーマの変更（独自テーマ）

### 利用するパーツ
今度は独自テーマ作ってそれを利用してみましょう。
独自テーマは既に設定されている属性を上書きすることで作成できます。
今回はmain.ts内に設定してしまいましょう！

設定内容は以下のファイルに記載されています。
node_modules/vuetify/lib/composables/theme.mjs

### コード

```typescript
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";

// Vuetify
import "vuetify/styles";
import { createVuetify } from "vuetify";
import * as components from "vuetify/components";
import * as directives from "vuetify/directives";
import "@mdi/font/css/materialdesignicons.css";

// import "./style.css";

const customDarkTheme = {
  dark: true,
  colors: {
    background: "#121212",
    surface: "#212121",
    "surface-bright": "#ccbfd6",
    "surface-variant": "#a3a3a3",
    "on-surface-variant": "#424242",
    // この色を変えてみた
    primary: "#E65100",
    "primary-darken-1": "#277CC1",
    // この色を変えてみた
    secondary: "#FFD180",
    "secondary-darken-1": "#48A9A6",
    error: "#CF6679",
    info: "#2196F3",
    success: "#4CAF50",
    warning: "#FB8C00",
  },
};

const vuetify = createVuetify({
  components,
  directives,
  icons: {
    defaultSet: "mdi",
  },
  // 独自で作成したテーマを適用
  theme: {
    defaultTheme: "customDarkTheme",
    themes: {
      customDarkTheme,
    },
  },
});

createApp(App).use(router).use(vuetify).mount("#app");


```

### 表示結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1784431/cf0f61a1-a249-274b-808b-1971a6031638.png)



# まとめ

駆け足で、Vueutify3のレイアウト、テーマ利用を説明してしまいましたが、Vueutify3を利用した開発の容易さが少しでも伝われば幸いです。

Vuetify2よりもデザインも洗練され、レイアウトシステムが強力で、よく利用するコンポーネント群が用意されているのですばやく開発することができます。ピクセル単位のデザインをしないのであればこういったフレームワークをうまく利用して開発期間を短くしていけるといいですね。

コンポーネントも必要なもののみインポート可能なのでページサイズも抑えることができ、至れり尽くせりです。

次回はFigma+tailwindでコード生成とかを記事にしてみたいと思います。Figma＋Amplify＋Amplify Studioが強力そうです。

それでは～