システム開発

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


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ソリューションへの簡単見積もりの依頼はこちら

Mai Tran

Recent Posts

Microsoft PowerAppsを活用してローコードで業務アプリを簡単に開発

現代のビジネス環境では、迅速な意思決定と効率的な業務運営が求められます。その中で、企業の業務プロセスをデジタル化するために、カスタムアプリの開発が重要な役割を果たしています。 しかし、従来のアプリ開発は多くの時間とリソースを必要とし、専門的なプログラミングスキルを持つエンジニアが必要です。 この課題を解決する手段として注目されているのが、「ローコード」開発です。 この記事ではそんなローコード開発に関して、代表的なツールMicrosoft PowerAppsについて解説していきたいと思います。 Microsoft PowerAppsが気になっている方 ローコード開発を行いたい方 社内のIT人材が不足している方 これらに当てはまる方におすすめの記事となっています。これを読めばローコード開発で人気のMicrosoft PowerAppsについてその特徴が丸わかりですよ。 (more…)

3 days ago

オフショア開発におけるAI開発の実績まとめ

近年、AI技術の進展とともに、企業のデジタルトランスフォーメーション(DX)推進が急務とされ、多くの企業がAIソリューションの開発を進めています。 オフショア開発におけるAIプロジェクトは、技術的な知見と効率的な体制が求められます。 この記事では、DEHAソリューションズでのAI開発事例について紹介し、それぞれのプロジェクトで実現されたユニークな機能や開発体制についてまとめます。 オフショア開発に興味がある方 AI開発を行いたい方 社内のIT人材が不足している方 これらに当てはまる方におすすめの記事となっています。これを読めばAI開発での具体的な事例が丸わかりですよ。 (more…)

4 days ago

ベトナム進出の日系製造業がDX化の今と今後|自動化による生産性向上

2024年現在、米中摩擦や新型コロナウイルスによるサプライチェーン混乱の影響を受け、製造業の生産拠点としてASEAN地域の需要が急速に増加しています。 その中でも特に注目されるのが、安価な労働力と豊富な人材を持つベトナムです。日系企業はこうした環境を活用し、積極的にベトナムへの進出を進めています。 しかし同時に、ASEAN域内では人件費上昇や人材確保の難しさといった課題も浮上しており、それに対応するために製造現場の自動化やDX化への関心が高まっています。 そこでここではそんなベトナムのDX化について、現状をお伝えしていきたいと思います。 ベトナム進出をお考えの方 ベトナムのDX化に興味がある方 これらに当てはまる方におすすめの記事となっています。これを読めばベトナム進出の日系企業のDX化の現状がわかるのはもちろん、今後の予測も分かりますよ。 (more…)

2 weeks ago

.NETでマイグレーション開発ならオフショアで

近年、企業のデジタルトランスフォーメーション(DX)が進む中で、ITインフラの老朽化問題も無視できなくなってきました。 レガシーシステムから新しいプラットフォームへの移行、特に.NETなどの最新フレームワークを用いたマイグレーションは、ビジネスの成長に不可欠です。 しかし、こうしたマイグレーションにはコストやリソースの確保が難しいという課題も付きまといます。ここで注目されるのがオフショア開発の活用です。 この記事では、そんな.NETによるマイグレーション開発をオフショアで実施するメリットについて詳しく解説します。 オフショア開発に興味がある方 .NETでマイグレーション開発を行いたいとお考えの方 社内のIT人材が不足している方 これらに当てはまる方におすすめの記事となっています。これを読めば.NETでのマイグレーション開発をオフショア開発で行うメリットはもちろん、注意点なども丸わかりですよ。 (more…)

2 weeks ago

BIZASKとは?特徴と料金プランを徹底解説

近年、企業における生成AIの導入が進んでいますが、その中でも注目を集めるのがAIアシスタントです。 弊社のBIZASKは、OpenAIの技術を基盤にした法人向け生成AIチャットボットサービスで、企業の業務効率化を強力に支援します。 この記事では、BIZASKの特徴や料金プランについて詳しく解説します。 生成AIアシスタントサービスが気になる方 業務効率を上げたい方 社内のIT人材が不足している方 これらに当てはまる方におすすめの記事となっています。これを読めばBIZASKの特徴が丸わかりですよ。 (more…)

3 weeks ago

クラウド型販売管理システム|導入メリットを徹底解説

クラウド型販売管理システムは、企業の販売プロセスを効率的に管理・最適化するためのソリューションで、クラウド上で提供されるのが特徴です。 従来のオンプレミス型システムとは異なり、初期投資を抑え、運用コストを削減しながらも、常に最新の機能とセキュリティ対策を利用できる点が魅力です。 この記事では、クラウド型販売管理システムのメリットと導入のポイントについて詳しく解説します。 クラウド型販売管理システムに興味がある方 販売業務を行なっている方 社内のIT人材が不足している方 これらに当てはまる方におすすめの記事となっています。これを読めばクラウド型販売管理システムに関して、そのメリットなども丸わかりですよ。 (more…)

4 weeks ago