システム開発

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

AI導入58%でも成果は“内向き”止まり|『DX動向2026』が示すレガシーマイグレーションの優先順位

AIや生成AIの導入が急速に進んでいます。しかし、「AIを導入した企業が、そのまま企業価値を高められているか」という問いに対しては、まだ明確に「Yes」とはいえません。 独立行政法人情報処理推進機構(IPA)が2026年7月に公表した「DX動向2026」では、日本企業のAI導入・試験利用が58.0%に達したことが明らかになりました。AI活用そのものは、もはや一部の先進企業だけの取り組みではありません。 一方で、AIによって得られた成果を見ると、「業務が効率化したり迅速化した」が91.6%であるのに対し、「顧客満足度が向上した」は4.5%、「売上や利益が向上した」はわずか3.9%にとどまっています。 つまり、AI導入は進んでいるものの、その成果はまだ「社内の効率化」という内向きの領域に集中しているのです。 この状況を変えるために重要になるのが、AIツールを増やすことだけではありません。AIが企業の中核業務や顧客接点、データ活用まで入り込めるよう、既存システムそのものを見直すことが必要です。 この記事では「DX動向2026」のデータを読み解きながら、AI活用を企業価値創出につなげるために、なぜレガシーマイグレーションが重要なのか、そしてどのシステムから刷新すべきなのかを解説します。 レガシーマイグレーションについて知りたい方 AIを活用したい方 これらに当てはまる方におすすめの記事となっています。これを読めばレガシーマイグレーションの特徴が丸わかりですよ。 (more…)

6 days ago

【2026年版】モダナイゼーション×AI:成功へ導く戦略と失敗しないための注意点

「レガシーシステムを刷新したいが、コストや移行リスクが不安」「生成AIをシステム刷新に活用したい」と考える企業が増えています。 2026年は、生成AIを活用して既存システムの解析やコード変換、ドキュメント作成、テストなどを効率化する「モダナイゼーション×AI」が注目されています。 この記事では、AIを活用したモダナイゼーションのメリットや成功のための戦略、失敗を防ぐための注意点について詳しく解説します。 モダナイゼーションが気になる方 モダナイゼーションとAIをどう組み合わせればいいのかわからない方 業務にAIを活用していきたい方 これらに当てはまる方におすすめの記事となっています。これを読めばモダナイゼーションとAIの活用方法やポイントが丸わかりですよ。 (more…)

2 weeks ago

AIで加速するモダナイゼーション:レガシーシステム刷新の手順と活用事例

多くの企業では、長年にわたって利用してきた基幹システムや業務システムが、現在も重要な役割を担っています。 一方で、こうした「レガシーシステム」は、技術の老朽化や複雑化、保守人材の不足、外部サービスとの連携の難しさなど、さまざまな課題を抱えています。 こうした状況を受け、企業ではシステムを単純に新しくする「リプレース」だけではなく、クラウドやAPI、データ分析基盤などを活用して、IT環境そのものを現代のビジネスに適した形へ変える「モダナイゼーション」が進んでいます。 本日はそんなモダナイゼーションについて、具体的な手順や活用事例を踏まえて解説していきます。 モダナイゼーションが気になる方 社内のIT人材が不足している方 レガシーシステムの刷新をしたい方 これらに当てはまる方におすすめの記事となっています。これを読めばモダナイゼーションについてわかるのはもちろん、レガシーシステム刷新の具体的な手順まで丸わかりですよ。 (more…)

2 weeks ago

DXの現状:「守りの効率化」から「攻めの価値創出」へ

〜IPA『DX動向2026』から読み解く、日本企業のリアルな課題と本質〜 はじめに:69ページの報告書が突きつける問い 独立行政法人情報処理推進機構(IPA)が発表した最新の調査資料『DX動向2026』(有効回答数:1,799社)。本レポートを精読する中で、最も印象的かつ示唆に富んでいたのは、次の現実でした。 「コストを削減することは、新たな売上を生み出すことよりもはるかに容易である」 日本においてDXへの取り組み率は75.7%に達し、過去数年間にわたり高水準で推移しています。もはや「DXに取り組むべきか否か」という議論のフェーズは完全に過去のものとなりました。しかし、その取り組みの内実をデータで精査すると、多くの企業が直面している構造的な課題が浮かび上がってきます。 1. 「守りのDX」の成功と、「攻めのDX」の停滞 DXに取り組んでいる企業のうち、60.8%が「何らかの成果が出ている」と回答しています。しかし、その「成果」の内訳には顕著な偏りが見られます。 業務効率化・コスト削減の成果:70.9% 新規事業創出・売上拡大の成果:わずか17.9% ペーパーレス化や社内業務のデジタル化といった、既存プロセスの効率化(いわゆる「守りのDX」)は順調に進展しています。しかし、製品・サービスの変革やビジネスモデル自体の刷新といった、事業価値を拡張する「攻めのDX」で具体的な成果を上げている企業は2割にも満たないのが現状です。 効率化によるコスト削減はDXの重要な足がかりですが、それ自体がゴールではありません。真の変革は、効率化によって生み出された余力を「新たな事業機会の創出」へ再投資することで初めて達成されます。 2. 投資拡大の裏にある「投資対効果(ROI)の不可視性」 レポートが示すもう一つの注目すべきデータは、投資と効果測定のギャップです。…

3 weeks ago

モダナイゼーションとは?DXとの違いやAIを活用した業務システムの刷新手法・手順を徹底解説

企業の業務を支える基幹システムや業務システムは、長年にわたって使い続けることで、企業独自の業務ノウハウが蓄積される一方、システムの老朽化や複雑化が進みやすくなります。 「古いシステムを刷新したいが、業務を止めることはできない」「既存システムに詳しい担当者が退職し、保守が難しくなっている」「DXを推進したいものの、既存システムが足かせになっている」といった課題を抱える企業も少なくありません。 こうした問題を解決するための重要な取り組みが「モダナイゼーション」です。 モダナイゼーションは、単純に古いシステムを新しいものへ置き換えるだけではありません。既存システムが持つデータや業務ロジック、ノウハウを活かしながら、現在のビジネス環境に適したIT基盤へと刷新していく考え方です。 この記事では、モダナイゼーションの基本的な意味からDXとの違い、代表的な刷新手法、AIを活用した進め方、実施手順、成功させるためのポイントまで詳しく解説します。 モダナイゼーションについて知りたい方 古いシステムを刷新したい方 DX化をすすめたい方 これらに当てはまる方におすすめの記事となっています。これを読めばモダナイゼーションについてわかるのはもちろん、DX化との違いまで丸わかりですよ。 (more…)

3 weeks ago

AI活用で業務効率化へ:2026年度IT導入補助金のスケジュールと活用法

「AIを業務に取り入れたいけれど、導入コストが気になる」「どのようなAIツールを選べばよいのかわからない」「補助金を使ってDXを進めたい」――このような悩みを抱える中小企業・小規模事業者は少なくありません。 そこで活用を検討したいのが、2026年度の「デジタル化・AI導入補助金」です。 従来の「IT導入補助金」から名称が変更され、2026年度はAI活用を含むデジタル化をより意識した制度となっています。 業務効率化や生産性向上につながるITツールの導入費用を支援することで、中小企業のDXを後押しする制度です。 この記事では、2026年度のデジタル化・AI導入補助金について、制度の概要から申請スケジュール、AI活用の具体例、申請時の注意点までをわかりやすく解説します。 AI導入を検討している方 IT導入補助金を申請したい方 これらに当てはまる方におすすめの記事となっています。これを読めば2026年度の「デジタル化・AI導入補助金」の詳細がわかるだけでなく、具体的な申請方法も丸わかりですよ。 (more…)

4 weeks ago