システム開発

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

【2026年版】ベトナム デジタル状況、最新動向

2026年のベトナムは、東南アジアの中でも特に「デジタル化が成熟段階に入りつつある国」として注目を集めています。 スマートフォンの普及、ソーシャルメディアの浸透、高速通信インフラの整備、そして若く人口ボーナス期にある社会構造が相まって、デジタル技術はすでに人々の日常生活、経済活動、情報収集の中核となっています。 この記事では、DataReportal「Digital 2026 Vietnam」レポートをもとに、2026年のベトナムにおけるデジタルデバイス、インターネット、ソーシャルメディア、主要プラットフォームの利用状況とその背景、そして今後の方向性について総合的に解説していきます。 ベトナムのデジタルの最新情報が気になる方 社内のIT人材が不足している方 ベトナムのIT人材が気になる方 これらに当てはまる方におすすめの記事となっています。これを読めばベトナムのデジタルの最新情報や動向が丸わかりですよ。 関連記事: 【2024年版】ベトナムのDX市場の状況と動向 2025年のベトナム デジタル状況、最新動向 (more…)

6 days ago

コードを書く時代から「制約」を設計する時代へ

ソフトウェア開発の歴史において、エンジニアの核心的な能力は「コードを書く力」で測られてきました。しかし、AI技術が飛躍的に進歩し、人間よりも速く一貫性のあるコードを生成できるようになった今、その価値の軸が大きくシフトしています。 これからのエンジニアに求められるのは、単なるプログラミングスキルではなく、いかに高度なAI活用を行い、システムに何を許し、何を許さないかという「制約」を正しく設計できるかという点にあります。 (more…)

1 week ago

2026年のクラウド市場シェアと動向【世界及び日本国内】

クラウドコンピューティングは、企業や政府のデジタルトランスフォーメーション(DX)を支える基盤です。 データ保存、アプリケーション実行、AI・データ分析など、あらゆるITインフラがクラウドを通じて提供されるようになった現代において、クラウド市場の動向は企業戦略の要です。 2026年は世界的に5G、AI、IoT(モノのインターネット)、機械学習などがクラウド活用を加速させ、市場全体が大きく成長すると予測されています。 この記事では、2026年のクラウド市場について世界市場の最新シェアや日本国内のクラウド市場シェアとその特徴などを紹介していきます。 企業の IT戦略・DX推進担当者の方 クラウド関連ビジネスに関わる方 これらに当てはまる方におすすめの記事となっています。これを読めば2026年のクラウド市場のシェアやトレンドが丸わかりですよ。 (more…)

1 week ago

2030年までに日本のIT市場はどう変わるのか?

2030年に向けて、日本のIT市場は単なる成長産業ではなく、社会全体を支える基盤(インフラ)としての性格を一層強めていくと考えられます。 背景には、世界規模で進行するデジタル化、AI技術の急速な発展、クラウドサービスの定着、そして日本固有の人口減少・地方分散という社会構造の変化があります。 この記事では、世界のICT市場動向を起点に、日本のソーシャルメディア、メタバース、クラウド、データセンター、情報セキュリティといった分野が、2030年に向けてどのように変化していくのかを多角的に整理していきます。 IT市場の未来が気になる方 AI技術がどのように発展していくか気になる方 これらに当てはまる方におすすめの記事となっています。これを読めば日本のIT市場の未来が丸わかりですよ。 (more…)

1 week ago

【経産省公表】2040年にAI人材326万人不足。デジタル時代を生き抜く「グローバル開発」のおすすめ

日本は2030年代に入ると急激に人口が減少し、労働力全体の供給が縮小するとの構造的な課題を抱えています。 特にデジタル技術の中心となるAI(人工知能)やロボットの開発・利活用を担う人材の不足が深刻になるとの推計が経済産業省の将来試算で示されています。 現在の教育・採用のままでは、2040年にAI・ロボット関連の人材が約326万人不足する可能性があるとされています。 この数字の背景には、生成AIの急速な普及やデジタル技術の社会インフラ化がある一方で、既存の人材供給は追いつかず、求められるスキルとのミスマッチが拡大している実態があります。 この記事では、こうした人材リスクの本質を整理しつつ、デジタル人材減少時代を生き抜く方策として、オフショア(海外)によるグローバル開発チームの構築戦略をご紹介します。 人材不足にお悩みの方 オフショア開発に興味がある方 これらに当てはまる方におすすめの記事となっています。これを読めばデジタル人材減少時代をどう生き抜くかその方法がわかりますよ。 (more…)

3 weeks ago

【製造業におけるIFS活用】統合プロセスによる生産管理自動化の方式とプロセスモデル

近年、製造業はかつてないほどの環境変化に直面しています。 需要変動の激化、多品種少量生産への対応、グローバルサプライチェーンの複雑化、人手不足、原材料価格の高騰など、経営・現場の両面で不確実性が増大しているのです。 このような状況下において、多くの企業が課題として挙げるのが生産管理の属人化・分断化です。 販売計画と生産計画が連動していない 在庫情報がリアルタイムに把握できない 工程進捗が見えず、計画変更が後手に回る システムは導入しているが、Excelや紙運用が残っている これらの問題は、部分最適なシステム導入や、部門ごとに分断された業務プロセスによって引き起こされることが多いです。 こうした背景の中で注目されているのが、IFS(Industrial and Financial Systems)を活用した統合型生産管理の自動化。 この記事では、IFSの特長を踏まえながら、製造業における生産管理自動化の方式と、それを支えるプロセスモデルについて詳しく解説していきます。 (more…)

1 month ago