STEP04：Laravel5.7 + Vue2.5 でvue-routerとVuetifyを使ってみる

※ 2018/10/17 Laravel5.6 から 5.7 に記述を更新

Laravel でユーザ認証した後、Vue側でRouter使ってページを切り替えてみる
Vuetify の[データテーブル](https://vuetifyjs.com/ja/components/data-tables)を使ったユーザ管理ページを作って、データテーブルの検索やページング、ソートの機能も使ってみる


環境設定他関連記事はこちら
[Laravel + Vue + Vuetify で業務サイト作ってみる](https://qiita.com/nobu-maple/items/d1e7170d62ab07890a7a)


#１．各パッケージをインストール

npm でサクッと vue-router と vuetify を入れとく
vuetify は[こちら](https://vuetifyjs.com/ja/getting-started/quick-start)を参考に

```bash
npm install vue-router
npm install vuetify
npm install css-loader
npm install material-design-icons-iconfont
```

インストール後はこんな感じ

```json:package.json
{
    "private": true,
    "scripts": {
        "dev": "npm run development",
        "development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
        "watch": "npm run development -- --watch",
        "watch-poll": "npm run watch -- --watch-poll",
        "hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js",
        "prod": "npm run production",
        "production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js"
    },
    "devDependencies": {
        "axios": "^0.18",
        "bootstrap": "^4.0.0",
        "cross-env": "^5.1",
        "jquery": "^3.2",
        "laravel-mix": "^2.0",
        "lodash": "^4.17.5",
        "popper.js": "^1.12",
        "vue": "^2.5.7"
    },
    "dependencies": {
        "css-loader": "^1.0.0",
        "material-design-icons-iconfont": "^3.0.3",
        "vue-router": "^3.0.1",
        "vuetify": "^1.2.9"
    }
}
```


Vuetify は 1.2.9 が入りました



#２．読み込み設定

追加した Vuetify と router を読み込むように設定
router については外出しして管理しやすいようにしとく
Vuetify は[色設定](https://vuetifyjs.com/ja/style/theme) https://vuetifyjs.com/ja/style/theme  も入れておく


```javascript:resources/js/app.js
require('./bootstrap');

// Vue
import Vue from 'vue'


// Vuetify
import Vuetify from 'vuetify'
import colors from 'vuetify/es5/util/colors'

Vue.use(Vuetify, {
  theme: {
    primary: colors.indigo.base,
    secondary: colors.blue.base,
    accent: colors.amber.base,
  }
});
import 'vuetify/dist/vuetify.min.css'
import 'material-design-icons-iconfont/dist/material-design-icons.css'


// Vue-Router
import router from './router'


// Main app
const app = new Vue({
    el: '#app',
    router,
});
```


## router 用設定

app.js と同じ階層に router ディレクトリを掘っておく

```bash
$ mkdir resources/js/router
```

作った ディレクトリに router 用設定ファイルを作成

```javascript:resources/js/router/index.js

import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)

//
import example_component from '../components/ExampleComponent.vue'
import admin_component   from '../components/AdminComponent.vue'
import r_link            from '../components/RouterLink.vue'

//
Vue.component('example-component', example_component)
Vue.component('admin-component', admin_component)
Vue.component('r-link', r_link)

//
import home              from '../components/HomeComponent.vue'
import admin_user        from '../components/Admin/UserComponent.vue'

export default new Router({
  mode: 'history',
  routes: [
    { path: '/',             name: 'home',          component: home,          meta: {name: 'ホーム',   icon: 'home'}},
    { path: '/admin/user',   name: 'admin_user',    component: admin_user,    meta: {name: '社員管理', icon: 'supervisor_account'}},
  ],
})
```

リンク用の文字とかアイコンをここに定義
各画面で表示する文字とかアイコンはこの定義を参照することで変更も１か所で済むようにしておく




#３．routerで切り替えるベースコンポーネント作成

vue-routerで切り替えるページのベースとなるコンポーネントを作成する
レイアウトは Vuetify のテンプレートを利用させてもらう

```html:resources/js/components/AdminComponent.vue
<template>
  <v-app id="app">
    <v-navigation-drawer v-model="drawer" clipped fixed app >
      <v-list dense>
        <r-link linkname='home'></r-link>
        <r-link linkname='admin_user'></r-link>
      </v-list>
    </v-navigation-drawer>

    <v-toolbar color="primary" dark fixed app clipped-left>
      <v-toolbar-side-icon @click.stop="drawer = !drawer"></v-toolbar-side-icon>
      <v-toolbar-title>{{title}}</v-toolbar-title>
      <v-spacer></v-spacer>
      {{ name }}
      <v-btn icon @click="axiosLogout()">
        <v-tooltip left>
          <v-icon slot="activator" color="white" dark >exit_to_app</v-icon>
          <span>ログアウト</span>
        </v-tooltip>
      </v-btn>
    </v-toolbar>

    <v-content>
      <v-container fluid fill-height
        <v-layout justify-center fluid column>
          <v-fade-transition mode="out-in">
            <router-view @axios-logout="axiosLogout"></router-view>
          </v-fade-transition>
        </v-layout>
      </v-container>
    </v-content>

    <v-footer color="primary" dark app fixed>
      <span class="white--text ml-3" v-html="footer"></span>
    </v-footer>
  </v-app>
</template>

<script>
  export default {
    name: 'AdminComponent',

    props: {
      name: String,
      logout: String,
    },

    data: () => ({
      drawer: false,
      footer: 'foo-----footer',
      title: 'tit------title',
    }),

    mounted() {
      console.log('AdminComponent mounted.')

      if (process.env.MIX_FOOTER) { this.footer = process.env.MIX_FOOTER }
      if (process.env.MIX_TITLE) { this.title = process.env.MIX_TITLE }
    },

    methods: {
      axiosLogout: function() {
        axios.post(this.logout)
        .then( function (response) {
          console.log(response)
        }.bind(this))

        .catch(function (error) {
          console.log(error)
          if (error.response) {
            if (error.response.status) {
              if (error.response.status == 401 || error.response.status == 419) {
                var parser = new URL(this.logout)
                location.href=parser.origin
              }
            }
          }
        }.bind(this))
      },
    },
  }
</script>
```


## リンク部分をコンポーネント化

リンク毎に同じ定義を書くのもめんどくさいのでコンポーネントにして、アイコンやらタイトルやらは route/index.js の定義から設定できるようにしとく


```html:resources/js/components/RouterLink.vue
<template>
  <router-link :to="{name: linkname}">
    <v-list-tile>
      <v-list-tile-action>
        <v-icon>{{ getRoute( linkname, 'icon' ) }}</v-icon>
      </v-list-tile-action>
      <v-list-tile-content>
        <v-list-tile-title>{{getRoute( linkname, 'name' )}}</v-list-tile-title>
      </v-list-tile-content>
    </v-list-tile>
  </router-link>
</template>

<script>
  export default {
    name: 'routerlink',
    props: {
      linkname: String,
    },

    methods: {
      getRoute(name, key) {
        for(var i=0 ; i<this.$router.options.routes.length; i++) {
          if (this.$router.options.routes[i].name == name ) {
            return this.$router.options.routes[i].meta[key]
          }
        }
      },
    },

  }
</script>

<style>
  a:hover {
    text-decoration: none;
  }
</style>
```




#４．home コンポーネント作成

とりあえず空のコンポーネント
Vuetifyで書いておく

```html:resources/js/components/HomeComponent.vue
<template>
  <v-content>
    <v-container fluid fill-height>
      <v-layout justify-center fluid>
        <v-flex xs12 offset-mx5>
          Home Component.
        </v-flex>
      </v-layout>
    </v-container>
  </v-content>
</template>

<script>
  export default {
    data: () => ({
    }),

    props: {
    },

    created() {
      console.log('Home Component created.')
      this.initialize()
    },

    mounted() {
      console.log('Home Component mounted.')
    },

    methods: {
      initialize: function() {
      },
    },

  }
</script>
```

#５．ユーザ一覧を表示するコンポーネントを作成

Vuetify の[データテーブル](https://vuetifyjs.com/ja/components/data-tables)を利用して登録済みのユーザ（社員）情報を一覧表示してみます

ユーザ情報は axios で取ってきてデータテーブルにセット
検索とページングとソートの設定もしときます

データテーブル構造は動的に headers に設定しているやつをセットするようにしてます
ついでに連番も表示するようにしておきました

```html:resources/js/components/Admin/UserComponent.vue
<template>
  <v-flex>
    <v-card xs12 class="m-3 px-3">

      <v-card-title class="title">
        <v-icon class="pr-2">{{ $route.meta.icon }}</v-icon> {{ $route.meta.name }} {{ /* 社員管理 */ }}
        <v-spacer></v-spacer>
        <v-spacer></v-spacer>
        <v-text-field
          v-model="search"
          append-icon="search"
          label="Search"
          single-line
          hide-details
        ></v-text-field>
      </v-card-title>

      <v-data-table
        :headers="headers"
        :items="tabledata"
        :pagination.sync="pagination"
        :rows-per-page-items='[10,25,50,{"text":"All","value":-1}]'
        :loading="loading"
        :search="search"
        class="elevation-0 p-1"
      >
        <v-progress-linear slot="progress" color="blue" indeterminate></v-progress-linear>

        <template slot="items" slot-scope="props">
          <tr>
            <td class="text-xs-center" xs1>{{ (props.index + 1) + (pagination.page - 1) * pagination.rowsPerPage }}</td>
            <template v-for="n in (headers.length - 1)">
              <td :class="'text-xs-' + headers[n].align" style="white-space: nowrap;" v-text="props.item[headers[n].value]"></td>
            </template>
          </tr>
        </template>

      </v-data-table>
    </v-card>
  </v-flex>
</template>

<script>
  export default {
    name: 'UserComponent',

    props: {
      logout: String,
    },

    data: () => ({
      loading: true,
      search: '',
      pagination: { sortBy: 'name', descending: false, },

      tabledata: [],
      headers: [
        { align: 'center', sortable: false, text: 'No',       },
        { align: 'left',   sortable: true,  text: '社員ID',   value: 'loginid' },
        { align: 'left',   sortable: true,  text: '氏名',     value: 'name' },
        { align: 'center', sortable: true,  text: '権限',     value: 'role' },
      ],
    }),

    created() {
      console.log('User Component created.')
      this.initialize()
    },

    methods: {
      initialize: function() {
        this.getUsers()
      },

      getUsers() {
        this.loading = true
        axios.post('/api/admin/user')

        .then( function (response) {
          this.loading = false
console.log(response)
          if (response.data.users) {
            this.tabledata = response.data.users
            this.setRole()
          }
        }.bind(this))

        .catch(function (error) {
          this.loading = false
          console.log(error)
          if (error.response) {
            if (error.response.status) {
              if (error.response.status == 401 || error.response.status == 419) {
                this.$emit('axios-logout')
              }
            }
          }
        }.bind(this))
      },

      setRole() {
        for (var i=0; i<this.tabledata.length; i++) {
          if (this.tabledata[i].role) {
            if (this.tabledata[i].role == 5) { this.tabledata[i].role = '管理者'  }
            if (this.tabledata[i].role == 10) { this.tabledata[i].role = 'ユーザ'  }
          }
        }
      },
    },
  }
</script>
```

権限（role）は数字で出されてもわかりにくいので日本語に変換(setRole)して表示するようにしています



#６．Laravel側でユーザ用コントローラを作成

ひな形をまず作って

```bash
php artisan make:controller UserController
```


一覧のデータとして全件を返す index を定義しときます


```php:app/Http/Controllers/UserController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\User;

class UserController extends Controller
{
  public function index()
  {
    $users = User::all();
    return ['users' => $users];
  }
}
```

## 権限(role)も表示対象にする

App/User.php でroleは隠す設定にしていたので、hiddenからfillableに変更

```php:app/User.php
<?php

namespace App;

use Illuminate\Notifications\Notifiable;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    use Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name',
//        'email',
        'loginid',
        'password',
        'role',
    ];

    /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */
    protected $hidden = [
        'password',
        'remember_token',
    ];
}
```


#７．Laravel側のルーティング設定

管理者以外はアクセスできないように定義したGATEで制限（can:admin）もかけときます

```php:routes/web.php
<?php

Route::get('/', function () {
    return view('home');
})->middleware('auth');

// Authentication Routes...
Route::get('/login', 'Auth\LoginController@showLoginForm')->name('login');
Route::post('/login', 'Auth\LoginController@login');
Route::post('/logout', 'Auth\LoginController@logout')->name('logout');

// Admin
Route::group( ['middleware' => ['auth', 'can:admin']], function() {

  // USER
  Route::post('/api/admin/user', 'UserController@index')->name('admin/user');
});

// Other
Route::get('/{any}', function () {
  return view('home');
})->middleware('auth')->where('any', '.*');

```


#８．動作確認

コンパイルして、サーバを起動して

```bash
npm run dev
php artisan serve --host=172.16.0.100  --port=8000
```

ブラウザでアクセスして
http://172.16.0.100:8000/

↓↓

ログインページへ飛ばされて
http://172.16.0.100:8000/login

↓↓

User の権限でログインしたら今までの Vue コンポーネントが表示されて
![172.16.0.100_8000_js_colors.js.map.png](https://qiita-image-store.s3.amazonaws.com/0/153259/5efb3449-3cd5-654a-717c-f0414945f979.png)

↓↓

ログアウトして 今度は Admin の管理者権限でログインしたら


↓↓

Vuetify で作った Adminコンポーネントが表示されて
![172.16.0.100_8000_js_colors.js.map.png](https://qiita-image-store.s3.amazonaws.com/0/153259/01b0fef8-34b4-f004-97b8-34adf116f3fa.png)

↓↓

メニューから 「社員管理」を選ぶと vue-router の機能で一覧のコンポーネントに切り替わり
![172.16.0.100_8000_js_colors.js.map.png](https://qiita-image-store.s3.amazonaws.com/0/153259/da61591e-b254-0e7a-c91e-1493eced5aee.png)



↓↓

User情報を axios が取ってきて Vuetify のデータテーブルにセットして表示
![172.16.0.100_8000_js_colors.js.map.png](https://qiita-image-store.s3.amazonaws.com/0/153259/325a2bac-4548-0e3d-3370-6d75f52d2c68.png)




検索とかページングとかソートとかの動作もうまく動いているようです
![172.16.0.100_8000_js_colors.js.map.png](https://qiita-image-store.s3.amazonaws.com/0/153259/2d60f8f7-ba7a-f4ba-ad7a-5d4ad6b5e72c.png)


#９． ヘッダーとフッターの環境設定

ヘッダーとフッターを環境変数から取得するようにしてあるので

```html:resources/js/components/AdminComponent.vue
～～
    mounted() {
      console.log('AdminComponent mounted.')

      if (process.env.MIX_FOOTER) { this.footer = process.env.MIX_FOOTER }
      if (process.env.MIX_TITLE) { this.title = process.env.MIX_TITLE }
    },
～～
```

環境変数を設定

```javascript:.env
～～

MIX_FOOTER="フッター文字列を指定"
MIX_TITLE="タイトル指定"
```

末尾に定義を追加

###ついでにVuetifyの色設定も変更してみる

```javascript:resources/js/app.js
～～～
Vue.use(Vuetify, {
  theme: {
    primary: colors.amber.base,
～～～
```

indigo 指定を amber 指定に変更


###コンパイルして再表示


```bash
npm run dev
php artisan serve --host=172.16.0.100  --port=8000
```

色とタイトルとフッターが変わったよ
![172.16.0.100_8000_admin_user.png](https://qiita-image-store.s3.amazonaws.com/0/153259/74f3cb9d-26f2-9663-edc5-63d48ad5c982.png)



以上
Vue-Router でページを切り替えて
Vuetify でデータテーブルを表示できました

ソースはこちら
https://github.com/u9m31/u9m31/tree/step04