deha magazine / オフショア開発 / VuetifyでCRUDを作る手順【Laravel6とNuxt.jsで作る管理画面】

VuetifyでCRUDを作る手順【Laravel6とNuxt.jsで作る管理画面】

2020/06/15


PHPの人気のフレームワークLaravelではWebサイトの管理画面を開発することができます。

このシリーズではそんな管理画面の構築に関して、技術者向けにその手順を紹介しています。
この記事ではVuetifyを利用してCRUDを作る方法をご紹介!

Nuxt.jsからLaravelのAPIをAjaxで通信できるようにする手順はこちらの記事で解説。CookieによるAPI経由のユーザー認証機能を作る方法はこちらの記事で解説しています。

・Laravelを使って構築をしたい方
・Webサイト構築の具体的な手法が知りたい方

これらに当てはまる方におすすめの記事となっています。このシリーズを読めばLaravel6とNuxt.jsで管理画面を作成することができますよ

Vuetifyを使う理由

VuetifyはマテリアルデザインベースのVueのUIコンポーネントライブラリです。

Vueを使って画面を作りたい!と思った時に、用意されているコンポーネントを使うだけでいい感じの画面が作れてしまいます。

プロトタイプや管理画面など独自のデザインを作る程ではないが、
いい感じのUIにはしたい場合、VuetifyのようなUIコンポーネントライブラリは便利です。

またVuetifyには管理画面のCRUD機能を簡単に実装できるコンポーネントが用意されているので、楽に今回の要件を満たすことができます。

公式サイトでも紹介されている通り、Nuxt.jsは他にも様々なUIライブラリをサポートしているのでぜひ一度目を通してみてください。

最低限やりたいことを決める

今回も実装の前に、最低限やりたいことを決めておきます。

VuetifyのUIコンポーネントには検索、ページング、ソート機能も用意されているので、CRUD機能以外にも要件に追加しました。

・ 管理者の一覧表示、追加、編集、削除ができるようにする
・ 管理者の一覧画面で検索、ページング、ソートができるようにする

最終的には以下のような画面になる予定です。

Laravel(API)側の実装

CRUD機能の見た目だけであればNuxt側だけで実装できますが、
本運用では当然データの永続化が必要ですので、LaravelのAPIも実装していきます。

認証済の管理者のみAPIを実行できるようにする

まずCRUD機能用のルーティングを追加する前に、routes/api.php を以下のように編集します。

Route::group(["middleware" => "api"], function () {
    Route::post('/login', 'Auth\LoginController@login');
    Route::get('/current_admin_user', function () {
        return Auth::user();
    });
    //追加
    Route::group(['middleware' => ['auth:api']], function () {
        //ここに認証が必要なパスを書いていく
    });
});

前回の記事でauth:apiはCookieで管理者の認証を行うようになっているので、
これで認証済の管理者のみAPIを実行できるようになります。

管理者のCRUD機能用のコントローラーを作成

次に以下のコマンドを実行して管理者のCRUD機能用のコントローラーを作成します。

php artisan make:controller Api/AdminUserController --resource --api

--apiをつけることでAPIとしては不要な新規作成画面(create)と編集画面(edit)のメソッドは省略したコントローラーを作成することができます。

管理者のCRUD機能用のルーティングを作成

次にroutes/api.php を以下のように編集します。

Route::group(["middleware" => "api"], function () {
    Route::post('/login', 'Auth\LoginController@login');
    Route::get('/current_admin_user', function () {
        return Auth::user();
    });
    Route::group(['middleware' => ['auth:api']], function () {
        //追加
        Route::apiResource('admin_users', 'Api\AdminUserController')->except(['show']);
    });
});

Route::apiResourceを使うことでCRUD機能用のルーティングを作成することができます。
今回は詳細表示は不要なのでshowは除外しています。

php artisan route:list

上記のコマンドを実行して以下のような結果になっていればOKです。

管理者のCRUD機能を実装

次にapp/Http/Controllers/Api/AdminUserController.phpを以下のように編集します。

<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
//追加
use Illuminate\Support\Facades\Hash;
use App\AdminUser;

class AdminUserController extends Controller
{
    /**
     * Display a listing of the resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function index()
    {
        //追加
        return AdminUser::all();
    }

    /**
     * Store a newly created resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function store(Request $request)
    {
        //追加
        $admin_user = new AdminUser;
        $admin_user->fill(array_merge(
            $request->all(), ['password' => Hash::make($request->password)]
        ))->save();
        return $admin_user;
    }

    /**
     * Update the specified resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function update(Request $request, $id)
    {
        //追加
        $admin_user = AdminUser::find($id);
        $admin_user->fill(array_merge(
            $request->all(), ['password' => Hash::make($request->password)]
        ))->save();
        return $admin_user;
    }

    /**
     * Remove the specified resource from storage.
     *
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function destroy($id)
    {
        //追加
        $admin_user = AdminUser::find($id);
        $admin_user->delete();
        return $admin_user;
    }
}

新規作成と更新時にはfillメソッドを使うことで、
app/AdminUser.php$fillableで許可されているカラムのみ保存されるようにしています。

Nuxt.js側の実装

次に実装したAPIを使ってNuxt.js側でCRUD機能を作っていきます。

Vuetifyのセットアップ

まず下記コマンドでVuetifyをインストールします。

yarn add @nuxtjs/vuetify -D

次にnuxt.config.jsbuildModulesの項目を以下のように編集します。

/*
** Nuxt.js dev-modules
*/
buildModules: [
  //追加
  '@nuxtjs/vuetify',
],

これでVuetifyを利用する準備が整いました。

ログイン画面のレイアウトファイルを追加

次にVuetifyを使ってログイン画面のスタイルを整えていきます。
まず専用のレイアウトファイルとして、以下の内容のlayouts/admin_login.vueを追加します。

<template>
  <v-app>
    <!-- ヘッダー部分 -->
    <v-app-bar
      :clipped-left="clipped"
      fixed
      app
    >
      <v-toolbar-title v-text="title" />
    </v-app-bar>
    <!-- コンテンツ部分 -->
    <v-content>
      <v-container>
        <nuxt />
      </v-container>
    </v-content>
    <!-- フッター部分 -->
    <v-footer
      :fixed="fixed"
      app
    >
      <span>&copy; {{ new Date().getFullYear() }}</span>
    </v-footer>
  </v-app>
</template>

<script>
export default {
  data () {
    return {
      clipped: false,
      fixed: false,
      title: 'Laravel Nuxt Admin'
    }
  }
}
</script>

レイアウトファイルが追加できたら、pages/admin/login.vueに以下のようにlayoutの設定を追加します。

<script>
export default {
  middleware: 'logined_admin_user',
  //追加
  layout: 'admin_login',
  data () {
    return {
      email: '',
      password: ''
    }
  },
  methods: {
    async submit () {
      await this.$store.dispatch('auth/login', {
        email: this.email,
        password: this.password
      })
      this.$router.push('/admin')
    }
  }
}
</script>

http://localhost:3000/admin/login にアクセスしたら、以下のような画面になっていればOKです。

ログインフォームにスタイルを反映

次にpages/admin/login.vueのフォーム部分を以下のように編集します。

<template>
  <section>
    <h1 class="display-1 mt-5">管理者ログイン</h1>
    <v-form @submit.prevent="submit">
      <v-text-field label="メールアドレス" v-model="email" />
      <v-text-field type="password" label="パスワード" v-model="password" />
      <v-btn type="submit" class="info">ログイン</v-btn>
    </v-form>
  </section>
</template>

http://localhost:3000/admin/login にアクセスしたら、以下のような画面になっていればOKです。

テキストフォームを始め、Vuetifyで使えるコンポーネントは公式サイトで紹介されているので、興味のある方はぜひご覧ください。

ログイン後の画面のレイアウトファイルを追加

次にログイン後の画面のスタイルを整えていきます。
専用のレイアウトファイルとして以下の内容のlayouts/admin.vueを追加します。

<template>
  <v-app>
    <!-- サイドバー部分  -->
    <v-navigation-drawer
      v-model="drawer"
      :mini-variant="miniVariant"
      :clipped="clipped"
      fixed
      app
    >
      <v-list>
        <v-list-item
          v-for="(item, i) in items"
          :key="i"
          :to="item.to"
          router
          exact
        >
          <v-list-item-action>
            <v-icon>{{ item.icon }}</v-icon>
          </v-list-item-action>
          <v-list-item-content>
            <v-list-item-title v-text="item.title" />
          </v-list-item-content>
        </v-list-item>
      </v-list>
    </v-navigation-drawer>
    <!-- ヘッダー部分 -->
    <v-app-bar
      :clipped-left="clipped"
      fixed
      app
    >
      <v-app-bar-nav-icon @click.stop="drawer = !drawer" />
      <v-toolbar-title v-text="title" />
      <v-spacer />
    </v-app-bar>
    <!-- コンテンツ部分 -->
    <v-content>
      <v-container fluid pa-10>
        <nuxt />
      </v-container>
    </v-content>
    <!-- フッター部分 -->
    <v-footer
      :fixed="fixed"
      app
    >
      <span>© {{ new Date().getFullYear() }}</span>
    </v-footer>
  </v-app>
</template>

<script>
export default {
  data () {
    return {
      clipped: false,
      drawer: true,
      fixed: false,
      items: [
        {
          icon: 'mdi-account',
          title: '管理者一覧',
          to: '/admin'
        },
      ],
      miniVariant: false,
      title: 'Laravel Nuxt Admin'
    }
  }
}
</script>

ログイン画面と同様にpages/admin/index.vueにlayoutの設定を追加します。

<template>
  <section>
    <h1>管理画面へようこそ</h1>
  </section>
</template>

<script>
export default {
  middleware: 'not_logined_admin_user',
  //追加
  layout: 'admin',
}
</script>

ログインした状態で http://localhost:3000/admin にアクセスしたら、以下のような画面になっていればOKです。
ちゃんとレスポンシブに対応されているかも確認しておきましょう。

これで以前に比べれば、大分いい感じのUIになってきました。
ちなみにアイコンに関してはMaterial Design Iconsに対応しています。ぜひ一度目を通しておきましょう。

Vuetifyのサンプルではコンテンツ部分はcontainerクラスで囲まれているのですが、
これだと余白が広すぎたのでfluidオプションで横幅一杯に広げpa-10クラスで丁度いい余白に調整しています。

管理者一覧の実装

次にいよいよCRUD機能の実装に入っていきます。
まずは管理者の一覧表示を作っていきましょう。

管理者一覧用のStoreを追加

JSで動的に変更できるようにするため、APIで取得したデータをStoreで管理します。
以下の内容のstore/adminUsers.jsを追加します。

export const state = () => ({
  adminUsers: []
})

export const getters = {
  list (state) {
    return state.adminUsers
  }
}

export const mutations = {
  setList (state, data) {
    state.adminUsers = data
  }
}

export const actions = {
  async fetchList () {
    return await this.$axios.$get('/admin_users')
      .catch(err => {
        console.log(err)
      })
  }
}

基本方針として、非同期処理は全てStoreのactionsに記述するようにしています。
ViewDispatchActionAPI実行CommitViewのようなデータフローになります。

一覧テーブルの追加

次に先に作成したStoreを利用して管理者の一覧をテーブルに表示するため、
pages/admin/index.vueを以下の内容で編集します。

<template>
  <v-layout
    column
    justify-center
  >
    <v-card v-if="adminUsers">
      <v-card-title>
        管理者一覧
        <v-spacer />
        <v-text-field
          v-model="searchText"
          append-icon="mdi-magnify"
          label="検索"
          sigle-line
        />
      </v-card-title>
      <v-data-table
        :headers="headers"
        :items="adminUsers"
        :items-per-page="5"
        :search="searchText"
        sort-by="id"
        :sort-desc="true"
        class="elevation-1"
      >
      </v-data-table>
    </v-card>
  </v-layout>
</template>

<script>
export default {
  middleware: 'not_logined_admin_user',
  layout: 'admin',
  async fetch ({ store }) {
    const adminUsers = await store.dispatch('adminUsers/fetchList')
    store.commit('adminUsers/setList', adminUsers)
  },
  data () {
    return {
      searchText: '',
    }
  },
  computed: {
    adminUsers () {
      return this.$store.getters['adminUsers/list']
    },
    headers () {
     return [
        { text: 'ID', value: 'id' },
        { text: 'メールアドレス', value: 'email' },
        { text: '名前', value: 'name' },
        { text: '', value: 'edit-action' },
        { text: '', value: 'delete-action' },
      ]
    }
  }
}
</script>

fetchメソッドはNuxtの機能で、コンポーネントを表示する前に非同期処理で取得したデータで初期化することが出来ます。

似た機能でasyncDataがありますが、非同期処理で取得したデータをStoreにセットしてレンダリングしたい場合は、fetchメソッドを使います。

fetchで初期化した配列データをStoreから取得し、v-data-tableitems propsに渡すだけで動的にテーブルが作られます。

これで一覧表示、検索、ページング、ソート機能まで一気に実装が出来てしまいました。
一度触ってみて機能が動いているかを確認してみましょう。

編集機能の実装

次に一覧のデータの編集機能を実装していきます。

Storeに編集用のactionとmutationを追加

まずstore/adminUsers.jsに以下の内容のactionmutationを追加します。

export const mutations = {
  setList (state, data) {
    state.adminUsers = data
  },
  //追加
  update (state, data) {
    state.adminUsers.forEach((adminUser, index) => {
      if (adminUser.id === data.id) {
        state.adminUsers.splice(index, 1, data)
      }
    })
  }
}

export const actions = {
  async fetchList () {
    return await this.$axios.$get('/admin_users')
      .catch(err => {
        console.log(err)
      })
  },
  //追加
  async update ({ commit }, adminUser) {
    const response = await this.$axios.$patch(`/admin_users/${adminUser.id}`, adminUser)
      .catch(err => {
        console.log(err)
      })
    commit('update', response)
  }
}

ビューでupdateアクションがDispatchされたら、
LaravelのAPIを実行し更新した値をStoreにも反映して再レンダリングする処理になります。

一覧テーブルに編集用アイコンを表示

次に編集のトリガーとなるアイコンを一覧テーブルに表示します。pages/admin/index.vueを以下の内容で編集します。

<template>
  <v-layout
    column
    justify-center
  >
    <v-card v-if="adminUsers">
      <v-card-title>
        管理者一覧
        <v-spacer />
        <v-text-field
          v-model="searchText"
          append-icon="mdi-magnify"
          label="検索"
          sigle-line
        />
      </v-card-title>
      <v-data-table
        :headers="headers"
        :items="adminUsers"
        :items-per-page="5"
        :search="searchText"
        sort-by="id"
        :sort-desc="true"
        class="elevation-1"
      >
        <!-- 追加 -->
        <template v-slot:item.edit-action="{ item }">
          <v-icon
            small
            @click="onClickEditIcon(item)"
          >
            mdi-pencil
          </v-icon>
        </template>
      </v-data-table>
    </v-card>
  </v-layout>
</template>

<script>
export default {
  middleware: 'not_logined_admin_user',
  layout: 'admin',
  async fetch ({ store }) {
    const adminUsers = await store.dispatch('adminUsers/fetchList')
    store.commit('adminUsers/setList', adminUsers)
  },
  data () {
    return {
      //追加
      dialogAdminUser: {},
      //追加
      isShowDialog: false,
      searchText: '',
    }
  },
  computed: {
    adminUsers () {
      return this.$store.getters['adminUsers/list']
    },
    headers () {
     return [
        { text: 'ID', value: 'id' },
        { text: 'メールアドレス', value: 'email' },
        { text: '名前', value: 'name' },
        { text: '', value: 'edit-action' },
        { text: '', value: 'delete-action' },
      ]
    }
  },
  //追加
  methods: {
    onClickEditIcon (adminUser) {
      this.dialogAdminUser = Object.assign({}, adminUser)
      this.isShowDialog = true
    },
  }
}
</script>

これで以下のように編集用のアイコンが表示されるようになります。

ここで重要なのが、v-slotという記述です。
まずVue.jsにはスロットというコンポーネントを使う側がコンテンツデータを渡せる仕組みがあります。

スロットによるコンテンツ配信 | Vue.jsガイド

そしてコンポーネントに複数のスロットを渡したい場合は、名前付きスロットを使います。

名前付きスロット | Vue.jsガイド

以下のようにすることで、
コンポーネントを使う側がv-slot:xxxで名前をつけたスロットに複数の値を渡せるようになります。

# base-layoutコンポーネントの中身
<div>
 <header>
   <slot name="header"></slot>
 </header>
 <footer>
   <slot name="footer"></slot>
 </footer>
</div>

# base-layoutコンポーネントを使う側
<base-layout>
  <template v-slot:header>
    <h1>Here might be a page title</h1>
  </template>

  <template v-slot:footer>
    <p>Here's some contact info</p>
  </template>
</base-layout>

v-data-tableコンポーネントはこの名前付きスロットの仕組みを利用しており、
headersで指定した名前のスロットがtdタグの子要素として出力されるようになっています (コードはこちら)

<template v-slot:item.edit-action="{ item }">
  <v-icon
    small
    @click="onClickEditIcon(item)"
  >
    mdi-pencil
  </v-icon>
</template>

そのため上記のように書くことで編集アイコンが表示されるようになります。

編集モーダルの追加

次に編集用のモーダルを追加します。
pages/admin/index.vueを以下の内容で編集します。

<template>
  <v-layout
    column
    justify-center
  >
    <v-card v-if="adminUsers">
      <v-card-title>
        管理者一覧
        <v-spacer />
        <v-text-field
          v-model="searchText"
          append-icon="mdi-magnify"
          label="検索"
          sigle-line
        />
      </v-card-title>
      <v-data-table
        :headers="headers"
        :items="adminUsers"
        :items-per-page="5"
        :search="searchText"
        sort-by="id"
        :sort-desc="true"
        class="elevation-1"
      >
        <!-- 追加 -->
        <template v-slot:top>
          <v-dialog v-model="isShowDialog" max-width="500px">
            <v-card>
              <v-card-title>
                <span class="headline">管理者編集</span>
              </v-card-title>
              <v-card-text>
                <v-container>
                  <v-row>
                    <v-col cols="6">
                      <v-text-field v-model="dialogAdminUser.email" label="メールアドレス" />
                    </v-col>
                    <v-col cols="6">
                      <v-text-field v-model="dialogAdminUser.name" label="名前" />
                    </v-col>
                    <v-col cols="12">
                      <v-text-field type="password" v-model="dialogAdminUser.password" label="パスワード" />
                    </v-col>
                  </v-row>
                </v-container>
              </v-card-text>
              <v-card-actions>
                <v-spacer />
                <v-btn @click="closeDialog">閉じる</v-btn>
                <v-btn class="primary" @click="onClickUpdateBtn">更新する</v-btn>
                <v-spacer />
              </v-card-actions>
            </v-card>
          </v-dialog>
        </template>
        <template v-slot:item.edit-action="{ item }">
          <v-icon
            small
            @click="onClickEditIcon(item)"
          >
            mdi-pencil
          </v-icon>
        </template>
      </v-data-table>
    </v-card>
  </v-layout>
</template>

<script>
export default {
  middleware: 'not_logined_admin_user',
  layout: 'admin',
  async fetch ({ store }) {
    const adminUsers = await store.dispatch('adminUsers/fetchList')
    store.commit('adminUsers/setList', adminUsers)
  },
  data () {
    return {
      dialogAdminUser: {},
      isShowDialog: false,
      searchText: '',
    }
  },
  computed: {
    adminUsers () {
      return this.$store.getters['adminUsers/list']
    },
    headers () {
     return [
        { text: 'ID', value: 'id' },
        { text: 'メールアドレス', value: 'email' },
        { text: '名前', value: 'name' },
        { text: '', value: 'edit-action' },
        { text: '', value: 'delete-action' },
      ]
    }
  },
  methods: {
    //追加
    closeDialog () {
      this.dialogAdminUser = {}
      this.isShowDialog = false
    },
    onClickEditIcon (adminUser) {
      this.dialogAdminUser = Object.assign({}, adminUser)
      this.isShowDialog = true
    },
    //追加
    async onClickUpdateBtn () {
      await this.$store.dispatch('adminUsers/update', this.dialogAdminUser)
      this.closeDialog()
    }
  }
}
</script>

v-slot:topでテーブルの上にモーダル用のテンプレートがスロットとして出力され、isShowDialogがtrueになった時に表示されるようになります。

v-data-tableコンポーネントで利用できるスロットの種類はこちらを参照してください。

これで下記のようなモーダルで内容を編集できるようになりました。


削除機能の実装

次に削除機能を実装していきます。

Storeに削除用のactionとmutationを追加

まずstore/adminUsers.jsに以下の内容のactionmutationを追加します。

export const state = () => ({
  adminUsers: []
})

export const getters = {
  list (state) {
    return state.adminUsers
  }
}

export const mutations = {
  setList (state, data) {
    state.adminUsers = data
  },
  //追加
  delete (state, data) {
    state.adminUsers.forEach((adminUser, index) => {
      if (adminUser.id === data.id) {
        state.adminUsers.splice(index, 1)
      }
    })
  },
  update (state, data) {
    state.adminUsers.forEach((adminUser, index) => {
      if (adminUser.id === data.id) {
        state.adminUsers.splice(index, 1, data)
      }
    })
  }
}

export const actions = {
  async fetchList () {
    return await this.$axios.$get('/admin_users')
      .catch(err => {
        console.log(err)
      })
  },
  //追加
  async delete ({ commit }, adminUser) {
    const response = await this.$axios.$delete(`/admin_users/${adminUser.id}`)
      .catch(err => {
        console.log(err)
      })
    commit('delete', response)
  },
  async update ({ commit }, adminUser) {
    const response = await this.$axios.$patch(`/admin_users/${adminUser.id}`, adminUser)
      .catch(err => {
        console.log(err)
      })
    commit('update', response)
  }
}


一覧テーブルに削除用アイコンを表示

次に同様に削除のトリガーとなるアイコンを一覧テーブルに表示します。
pages/admin/index.vueを以下の内容で編集します。

<template>
  <v-layout
    column
    justify-center
  >
    <v-card v-if="adminUsers">
      <v-card-title>
        管理者一覧
        <v-spacer />
        <v-text-field
          v-model="searchText"
          append-icon="mdi-magnify"
          label="検索"
          sigle-line
        />
      </v-card-title>
      <v-data-table
        :headers="headers"
        :items="adminUsers"
        :items-per-page="5"
        :search="searchText"
        sort-by="id"
        :sort-desc="true"
        class="elevation-1"
      >
        <template v-slot:top>
          <v-dialog v-model="isShowDialog" max-width="500px">
            <v-card>
              <v-card-title>
                <span class="headline">管理者編集</span>
              </v-card-title>
              <v-card-text>
                <v-container>
                  <v-row>
                    <v-col cols="6">
                      <v-text-field v-model="dialogAdminUser.email" label="メールアドレス" />
                    </v-col>
                    <v-col cols="6">
                      <v-text-field v-model="dialogAdminUser.name" label="名前" />
                    </v-col>
                    <v-col cols="12">
                      <v-text-field type="password" v-model="dialogAdminUser.password" label="パスワード" />
                    </v-col>
                  </v-row>
                </v-container>
              </v-card-text>
              <v-card-actions>
                <v-spacer />
                <v-btn @click="closeDialog">閉じる</v-btn>
                <v-btn class="primary" @click="onClickUpdateBtn">更新する</v-btn>
                <v-spacer />
              </v-card-actions>
            </v-card>
          </v-dialog>
        </template>
        <template v-slot:item.edit-action="{ item }">
          <v-icon
            small
            @click="onClickEditIcon(item)"
          >
            mdi-pencil
          </v-icon>
        </template>
        <!-- 追加 -->
        <template v-slot:item.delete-action="{ item }">
          <v-icon
            small
            @click="onClickDeleteIcon(item)"
          >
            mdi-delete
          </v-icon>
        </template>
      </v-data-table>
    </v-card>
  </v-layout>
</template>

<script>
export default {
  middleware: 'not_logined_admin_user',
  layout: 'admin',
  async fetch ({ store }) {
    const adminUsers = await store.dispatch('adminUsers/fetchList')
    store.commit('adminUsers/setList', adminUsers)
  },
  data () {
    return {
      dialogAdminUser: {},
      isShowDialog: false,
      searchText: '',
    }
  },
  computed: {
    adminUsers () {
      return this.$store.getters['adminUsers/list']
    },
    headers () {
     return [
        { text: 'ID', value: 'id' },
        { text: 'メールアドレス', value: 'email' },
        { text: '名前', value: 'name' },
        { text: '', value: 'edit-action' },
        { text: '', value: 'delete-action' },
      ]
    }
  },
  methods: {
    closeDialog () {
      this.dialogAdminUser = {}
      this.isShowDialog = false
    },
    onClickEditIcon (adminUser) {
      this.dialogAdminUser = Object.assign({}, adminUser)
      this.isShowDialog = true
    },
    //追加
    async onClickDeleteIcon (adminUser) {
      await this.$store.dispatch('adminUsers/delete', adminUser)
    },
    async onClickUpdateBtn () {
      await this.$store.dispatch('adminUsers/update', this.dialogAdminUser)
      this.closeDialog()
    }
  }
}
</script>

これで以下のように削除アイコンが表示され、データを削除出来るようになりました。

新規追加の実装

最後に新規追加の実装を行っていきます。

Storeに新規追加用のactionとmutationを追加

export const state = () => ({
  adminUsers: []
})

export const getters = {
  list (state) {
    return state.adminUsers
  }
}

export const mutations = {
  setList (state, data) {
    state.adminUsers = data
  },
  //追加
  create (state, data) {
    state.adminUsers.push(data)
  },
  delete (state, data) {
    state.adminUsers.forEach((adminUser, index) => {
      if (adminUser.id === data.id) {
        state.adminUsers.splice(index, 1)
      }
    })
  },
  update (state, data) {
    state.adminUsers.forEach((adminUser, index) => {
      if (adminUser.id === data.id) {
        state.adminUsers.splice(index, 1, data)
      }
    })
  }
}

export const actions = {
  async fetchList () {
    return await this.$axios.$get('/admin_users')
      .catch(err => {
        console.log(err)
      })
  },
  //追加
  async create ({ commit }, adminUser) {
    const response = await this.$axios.$post('/admin_users', adminUser)
      .catch(err => {
        console.log(err)
      })
    commit('create', response)
  },
  async delete ({ commit }, adminUser) {
    const response = await this.$axios.$delete(`/admin_users/${adminUser.id}`)
      .catch(err => {
        console.log(err)
      })
    commit('delete', response)
  },
  async update ({ commit }, adminUser) {
    const response = await this.$axios.$patch(`/admin_users/${adminUser.id}`, adminUser)
      .catch(err => {
        console.log(err)
      })
    commit('update', response)
  }
}


モーダルを新規追加・編集両方で利用できるように修正

次に新規追加でもモーダルを利用できるように、pages/admin/index.vueを以下の内容で編集します。

<template>
  <v-layout
    column
    justify-center
  >
    <v-card v-if="adminUsers">
      <v-card-title>
        管理者一覧
        <v-spacer />
        <v-text-field
          v-model="searchText"
          append-icon="mdi-magnify"
          label="検索"
          sigle-line
        />
      </v-card-title>
      <v-data-table
        :headers="headers"
        :items="adminUsers"
        :items-per-page="5"
        :search="searchText"
        sort-by="id"
        :sort-desc="true"
        class="elevation-1"
      >
        <template v-slot:top>
          <v-dialog v-model="isShowDialog" max-width="500px">
            <v-card>
              <v-card-title>
                <!-- 編集 -->
                <span class="headline">{{ formTitle }}</span>
              </v-card-title>
              <v-card-text>
                <v-container>
                  <v-row>
                    <v-col cols="6">
                      <v-text-field v-model="dialogAdminUser.email" label="メールアドレス" />
                    </v-col>
                    <v-col cols="6">
                      <v-text-field v-model="dialogAdminUser.name" label="名前" />
                    </v-col>
                    <v-col cols="12">
                      <v-text-field type="password" v-model="dialogAdminUser.password" label="パスワード" />
                    </v-col>
                  </v-row>
                </v-container>
              </v-card-text>
              <v-card-actions>
                <v-spacer />
                <v-btn @click="closeDialog">閉じる</v-btn>
                <!-- 編集 -->
                <v-btn v-if="isPersistedAdminUser" class="primary" @click="onClickUpdateBtn">更新する</v-btn>
                <!-- 追加 -->
                <v-btn v-else class="primary" @click="onClickCreateBtn">追加する</v-btn>
                <v-spacer />
              </v-card-actions>
            </v-card>
          </v-dialog>
        </template>
        <template v-slot:item.edit-action="{ item }">
          <v-icon
            small
            @click="onClickEditIcon(item)"
          >
            mdi-pencil
          </v-icon>
        </template>
        <template v-slot:item.delete-action="{ item }">
          <v-icon
            small
            @click="onClickDeleteIcon(item)"
          >
            mdi-delete
          </v-icon>
        </template>
      </v-data-table>
    </v-card>
  </v-layout>
</template>

<script>
export default {
  middleware: 'not_logined_admin_user',
  layout: 'admin',
  async fetch ({ store }) {
    const adminUsers = await store.dispatch('adminUsers/fetchList')
    store.commit('adminUsers/setList', adminUsers)
  },
  data () {
    return {
      dialogAdminUser: {},
      isShowDialog: false,
      searchText: '',
    }
  },
  computed: {
    adminUsers () {
      return this.$store.getters['adminUsers/list']
    },
    //追加
    formTitle () {
      return this.isPersistedAdminUser ? '管理者編集' : '管理者追加'
    },
    headers () {
     return [
        { text: 'ID', value: 'id' },
        { text: 'メールアドレス', value: 'email' },
        { text: '名前', value: 'name' },
        { text: '', value: 'edit-action' },
        { text: '', value: 'delete-action' },
      ]
    },
    //追加
    isPersistedAdminUser () {
      return !!this.dialogAdminUser.id
    },
  },
  methods: {
    closeDialog () {
      //編集
      this.isShowDialog = false
      setTimeout(() => {
        this.dialogAdminUser = {}
      }, 1000)
    },
    onClickEditIcon (adminUser) {
      this.dialogAdminUser = Object.assign({}, adminUser)
      this.isShowDialog = true
    },
    //追加
    async onClickCreateBtn () {
      await this.$store.dispatch('adminUsers/create', this.dialogAdminUser)
      this.closeDialog()
    },
    async onClickDeleteIcon (adminUser) {
      await this.$store.dispatch('adminUsers/delete', adminUser)
    },
    async onClickUpdateBtn () {
      await this.$store.dispatch('adminUsers/update', this.dialogAdminUser)
      this.closeDialog()
    }
  }
}
</script>


新規追加用のボタンを追加

次に新規追加でモーダルを表示するためのボタンを追加します。pages/admin/index.vueを以下の内容で編集します。

<template>
  <v-layout
    column
    justify-center
  >
    <v-card v-if="adminUsers">
      <v-card-title>
        管理者一覧
        <v-spacer />
        <v-text-field
          v-model="searchText"
          append-icon="mdi-magnify"
          label="検索"
          sigle-line
        />
      </v-card-title>
      <v-data-table
        :headers="headers"
        :items="adminUsers"
        :items-per-page="5"
        :search="searchText"
        sort-by="id"
        :sort-desc="true"
        class="elevation-1"
      >
        <template v-slot:top>
          <v-dialog v-model="isShowDialog" max-width="500px">
            <!-- 追加 -->
            <template v-slot:activator>
              <v-btn fab dark color="pink" class="mb-2"
                @click="onClickAddBtn"
              >
                <v-icon dark>
                  mdi-plus
                </v-icon>
              </v-btn>
            </template>
            <v-card>
              <v-card-title>
                <span class="headline">{{ formTitle }}</span>
              </v-card-title>
              <v-card-text>
                <v-container>
                  <v-row>
                    <v-col cols="6">
                      <v-text-field v-model="dialogAdminUser.email" label="メールアドレス" />
                    </v-col>
                    <v-col cols="6">
                      <v-text-field v-model="dialogAdminUser.name" label="名前" />
                    </v-col>
                    <v-col cols="12">
                      <v-text-field type="password" v-model="dialogAdminUser.password" label="パスワード" />
                    </v-col>
                  </v-row>
                </v-container>
              </v-card-text>
              <v-card-actions>
                <v-spacer />
                <v-btn @click="closeDialog">閉じる</v-btn>
                <v-btn v-if="isPersistedAdminUser" class="primary" @click="onClickUpdateBtn">更新する</v-btn>
                <v-btn v-else class="primary" @click="onClickCreateBtn">追加する</v-btn>
                <v-spacer />
              </v-card-actions>
            </v-card>
          </v-dialog>
        </template>
        <template v-slot:item.edit-action="{ item }">
          <v-icon
            small
            @click="onClickEditIcon(item)"
          >
            mdi-pencil
          </v-icon>
        </template>
        <template v-slot:item.delete-action="{ item }">
          <v-icon
            small
            @click="onClickDeleteIcon(item)"
          >
            mdi-delete
          </v-icon>
        </template>
      </v-data-table>
    </v-card>
  </v-layout>
</template>

<script>
export default {
  middleware: 'not_logined_admin_user',
  layout: 'admin',
  async fetch ({ store }) {
    const adminUsers = await store.dispatch('adminUsers/fetchList')
    store.commit('adminUsers/setList', adminUsers)
  },
  data () {
    return {
      dialogAdminUser: {},
      isShowDialog: false,
      searchText: '',
    }
  },
  computed: {
    adminUsers () {
      return this.$store.getters['adminUsers/list']
    },
    formTitle () {
      return this.isPersistedAdminUser ? '管理者編集' : '管理者追加'
    },
    headers () {
     return [
        { text: 'ID', value: 'id' },
        { text: 'メールアドレス', value: 'email' },
        { text: '名前', value: 'name' },
        { text: '', value: 'edit-action' },
        { text: '', value: 'delete-action' },
      ]
    },
    isPersistedAdminUser () {
      return !!this.dialogAdminUser.id
    },
  },
  methods: {
    closeDialog () {
      this.isShowDialog = false
      setTimeout(() => {
        this.dialogAdminUser = {}
      }, 1000)
    },
    //追加
    onClickAddBtn () {
      this.dialogAdminUser = {}
      this.isShowDialog = true
    },
    onClickEditIcon (adminUser) {
      this.dialogAdminUser = Object.assign({}, adminUser)
      this.isShowDialog = true
    },
    async onClickCreateBtn () {
      await this.$store.dispatch('adminUsers/create', this.dialogAdminUser)
      this.closeDialog()
    },
    async onClickDeleteIcon (adminUser) {
      await this.$store.dispatch('adminUsers/delete', adminUser)
    },
    async onClickUpdateBtn () {
      await this.$store.dispatch('adminUsers/update', this.dialogAdminUser)
      this.closeDialog()
    }
  }
}
</script>

v-dialogコンポーネントのv-slot:activatorスロットとして、
フローティングボタンを追加して、クリックしたらモーダルを表示するようにしています。

以下のような画面が表示されればOKです。新規追加が正常に動くかも確認しておきましょう。

おわりに

今回はLaravel(API)とVuetifyを組み合わせてCRUD機能の実装を行いました。
UIコンポーネントを組み合わせれば、柔軟にカスタマイズが可能です。

これでLaravel(API)とNuxt.jsで管理画面を作ることができました。
1つ1つの手順を見直して実装をしていきましょう。


本日紹介したようなデザインを外注してみるのはいかがでしょうか。 dehaソリューションズではオフショア開発によって低コストで迅速な開発をサポートしています。

Laravelに関して詳しくお話を聞きたい方、開発相談や無料お見積りをしたい方はこちらからご気軽にお問い合わせください。

▼ dehaソリューションへの簡単見積もりの依頼はこちら

株式会社DEHA SOLUTIONS
私達は約200名のIT人材を有するテクノロジースタジオです。
ベトナムの情報通信系大学やEduTechベンチャーと連携し、潤沢なタレントプールから選りすぐりの人材を採用し、日本企業に最適化した教育を施しています
お客様の課題感にあった最適なITチームをご提供いたしますので、ITリソースの人材不足にお悩みのご担当者様はお気軽にお問合わせ下さい。
開発実績 お問い合わせ