- 投稿日:2019-02-15T20:36:21+09:00
Vue.jsで扱うBootstrap4
Vue.jsでBootstarap4をいじってみよう!
タイトルの通りVue.jsのプロジェクトにBootstrapを導入しよう、使ってみよう!という記事です。昨今のVue界隈ではみなElementUIを使うっぽいですが自分は普通にVueでもいつも通りBootstrapを使いたかったので導入してみることにしました。
非常にわかりやすい公式ドキュメントが存在するのですが英語で初心者にはとっつきにくいかなと思ったので公式ドキュメントの和訳もかねています。
以下の点に注意してください。
※今回はNuxt.jsについては触れていません
※VueやJavaScript, node.js, npm, yarn, webpack, bootstrapなどをそもそも知らないよ!という方向けの記事ではないです。その辺は他のめちゃくちゃわかりやすい記事や公式ドキュメントが存在するのでそちらを参考にしてください。1. 導入編
導入方法としては二つ存在します。すでに作成したプロジェクト(webpack)に導入する方法と、vue-cliを用いてすでに導入済みのプロジェクトのテンプレートを作成する方法です。適宜使う方を参考にしてください。
Webpack
もしプロジェクトでwebpackを用いているならこの方法で導入できます(他のモジュールバンドラでは未検証)
①npmかyarnを用いてbootstrap-vueをインストールします。
以下のコマンドを入力してください。bashなどのCLInpm install bootstrap-vue yarn add bootstrap-vue
②インストールが完了したらBootstrapVue プラグインを読み込みます。
src/index.jsimport Vue from 'vue' import BootstrapVue from 'bootstrap-vue' import 'bootstrap/dist/css/bootstrap.css' import 'bootstrap-vue/dist/bootstrap-vue.css' Vue.use(BootstrapVue)ちなみに、自分はESlintのStandardを使っているのでセミコロンは勝手に消しちゃいました。
③webpackの設定をします。以下の二つをインストールしてください。
webpack公式の解説はこちらを見て下さい。bashnpm install --save-dev style-loader css-loader④最後にwebpackの設定に以下を追記してください。
webpack.config.jsconst path = require('path'); module.exports = { entry: './src/index.js', output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist') }, // ここから module: { rules: [ { test: /\.css$/, use: [ 'style-loader', 'css-loader' ] } ] } // ここまで };vue-cli
!注意!
公式ドキュメントにはvue-cliと書いてありますが、最新版は@vue/cliと名称を変更しています。また、vue init コマンドを実行するには@vue/cli-initをインストールする必要があるます。こちらも別途公式ドキュメントを参照して下さい。vue initが実行できる環境であればあとはコマンドを実行するだけです。
eslintの設定などはお好みで選択してください。bashvue init bootstrap-vue/webpack-simple my-project # Change into the directory cd my-project # Install dependencies npm i # Fire up the dev server with HMR npm run devこのときもちろんwebpack-simpleだけでなくwebpackテンプレートも使えます。
bashvue init bootstrap-vue/webpack my-project導入編は以上になります。npm run devできちんと機能してるか確認しておきましょう。
App.vue<template> <div id="app"> <h1>Home</h1> <a href="#">bootstrap導入成功</a> <b-alert show>Default Alert</b-alert> <b-alert variant="danger" dismissible :show="showDismissibleAlert" @dismissed="showDismissibleAlert=false"> Dismissible Alert! </b-alert> </div> </template> <script> export default { name: 'App' } </script> <style> html, body { width: 100vw; margin: 0; } </style>無事導入できましたでしょうか?
続いてサンプルを紹介して実際にコンポーネントを使ってみよう、と言いたいところですがかなり長くなってしまいそうなので公式ドキュメントの和訳は以下の記事にまとめまることにします!必要に応じて見てみてください。
- 投稿日:2019-02-15T17:37:08+09:00
Vue.jsでよくある初心者の落とし穴にハマった記録
公式ドキュメントに書いてある落とし穴にしっかりとハマってきたので記録を残しておきます。
Viewが更新されない
Vueインスタンスのデータとして配列を持っているとします。
その配列の値をメソッドなどから下記のように直接インデックスを指定して更新します。
例this.array[i] = 123;この更新はDOMに反映されません!
公式ドキュメントのこちらにしっかりと記載されていました。
反映させたい場合
配列のインデックスを指定して要素の値を更新し、DOMにも反映させたい場合は
array.$set(index, value)
メソッドが使えます。他にも配列の変化を検出する方法はこちらに記載されています。
参考
- 投稿日:2019-02-15T17:37:08+09:00
Vue.jsでよくある初心者の落とし穴にハマった話
公式ドキュメントに書いてある落とし穴にしっかりとハマってきたので記録を残しておきます。
Viewが更新されない
Vueインスタンスのデータとして配列を持っているとします。
その配列の値をメソッドなどから下記のように直接インデックスを指定して更新します。
例this.array[i] = 123;この更新はDOMに反映されません!
公式ドキュメントのこちらにしっかりと記載されていました。
反映させたい場合
配列のインデックスを指定して要素の値を更新し、DOMにも反映させたい場合は
array.$set(index, value)
メソッドが使えます。他にも配列の変化を検出する方法はこちらに記載されています。
参考
- 投稿日:2019-02-15T16:43:50+09:00
Vueテンプレート構文:条件で属性を付けたり外したり【備忘録】
属性値のバインディングはわかるけど、
属性自体の切替はどうやるんだと迷ったので備忘録。やりたいこと
- blankフラグが
false
のときはtarget属性無し- blankフラグが
true
のときはtarget属性有りtemplate.vue<template lang="pug"> .component template(v-for="item in linkList") // blankフラグがfalseのときはtarget属性なし a(:href="item.href") |{{item.text}} // blankフラグがtrueのときはtarget="_blank" a(:href="item.href" target="_blank") |{{item.text}} </template> <script> export default { data(){ return { linkList: [ { text: "同窓リンク", href: "hoge.hoge", blank: false }, { text: "別窓リンク", href: "fuga.fuga", blank: true }, ] } } } </script>やり方
三項演算子を使って不一致時に
false
やnull
を渡してあげると属性ごと消えてくれた属性が一個だけの時
OK.vue<template lang="pug"> .component template(v-for="item in linkList") a(:href="item.href" :target="item.blank? '_blank':false") |{{item.text}} </template>↓
OK.html<a href="hoge.hoge">同窓リンク</a> <a href="fuga.fuga" target="_blank">別窓リンク</a>属性が複数の時
v-bind
の引数を省略すると複数の属性をオブジェクトで渡せるので、
rel="noopener noreferrer"
もつけたりとか、複数の属性をつけるならこっちが楽OK.vue<template lang="pug"> .component template(v-for="item in linkList") a(:href="item.href" v-bind="item.blank? {target:'_blank',rel:'noopener noreferrer'}:false") |{{item.text}} </template>↓
OK.html<a href="hoge.hoge">同窓リンク</a> <a href="fuga.fuga" target="_blank" rel="noopener noreferrer">別窓リンク</a>NGな例
最初なにも考えずにクラスバインディングのノリでオブジェクト構文で書いてみたらダメだった
オブジェクトを渡せるのはv-bind
(引数省略時)と、v-bind:class
、v-bind:style
のみ
https://jp.vuejs.org/v2/api/#v-bindNG.vue<template lang="pug"> .component template(v-for="item in linkList") a(:href="item.href" :target="{_blank:item.blank}") |{{item.text}} </template>↓
NG.html<a href="hoge.hoge" target="[object Object]">同窓リンク</a> <a href="fuga.fuga" target="[object Object]">別窓リンク</a>
- 投稿日:2019-02-15T13:42:33+09:00
STEP14:Laravel5.7 + Vue2.5 で操作ログを自動記録する
Laravel のミドルウェアを使って操作のログを自動で記録してみます
操作ログはデータベースへ保存して、ログイン済みであればユーザIDも自動で記録
記録した操作ログを一覧表示する画面も作成しときます
ログテーブル作成
まずは操作(アクション)ログを記録するテーブルを作成します
php artisan make:migration create_actlogs_table --create=actlogsdatabase/migrations/*actlogs*.php<?php use Illuminate\Support\Facades\Schema; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class CreateActlogsTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('actlogs', function (Blueprint $table) { $table->increments('id'); $table->integer('user_id') -> unsigned() -> nullable() -> comment("ユーザID"); $table->string('route') -> nullable() -> comment("route.webで設定した名称"); $table->string('url') -> nullable() -> comment("要求Path"); $table->string('method') -> nullable() -> comment("要求メソッド Get Post"); $table->integer('status') -> unsigned() -> nullable() -> comment("要求結果 200 OK とか 301 move 等"); $table->text('data') -> nullable() -> comment("要求内容(暗号化して保存)"); $table->string('remote_addr') -> nullable() -> comment("クライアントIPアドレス"); $table->string('user_agent') -> nullable() -> comment("ブラウザ名"); $table->timestamps(); $table->index(['created_at']); $table->index(['user_id']); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('actlogs'); } }ログに記録するのは以下
・ユーザID
・ページ名(routes/web.php の name でつけといた名前)
・要求パス
・要求メソッド(GETとかPOSTとか)
・要求結果 200 OK とか 301 Move とか
・要求内容(あれば)
・接続元 IPアドレス
・ブラウザ名(UserAgent)
・作成時間(DB記録時間)定義から実テーブルの作成もしときます
php artisan migrate:refresh --seed
テーブルに対応するモデルも作成
app/Actlog.php<?php namespace App; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\Crypt; class Actlog extends Model { // 更新日時記録無効化 const UPDATED_AT = null; // Jsonに追加で含める protected $appends = ['name']; // ユーザの氏名を取得 -- リレーションできなかった場合は空文字を返す public function getNameAttribute() { $user = $this -> user; if ($user) return $user -> name; else return ''; } // カラム暗号化 - 要求内容は暗号化して保存する public function setDataAttribute($value) { if ($value) { $this->attributes['data'] = Crypt::encrypt( serialize($value) ); } } // カラム複合化 - 要求内容を取り出すときに複合化する public function getDataAttribute($value) { if ($value) { return unserialize( Crypt::decrypt($value) ); } else { return $value; } } /** * The attributes that are mass assignable. * * @var array */ protected $fillable = [ 'user_id', 'route', 'url', 'method', 'status', 'data', 'remote_addr', 'user_agent', ]; // Json に出力する項目 protected $visible = [ 'user_id', 'route', 'url', 'method', 'status', 'data', 'remote_addr', 'user_agent', 'created_at', 'name', ]; public function user() { return $this -> belongsTo('App\User', 'user_id', 'id'); } }通信内容( 'data' )は暗号化して保存
あと、UPDATEはしないので記録しないように設定しときます
一覧で見やすいようにユーザ名( 'name' )も返すようにしてありますミドルウェア作成
実際にログを記録するミドルウェアを作成です
$ php artisan make:middleware ActlogMiddleware
app/Http/Middleware/ActlogMiddleware.php<?php namespace App\Http\Middleware; use Closure; use App\Actlog; use \Route; class ActlogMiddleware { /** * Handle an incoming request. * * @param \Illuminate\Http\Request $request * @param \Closure $next * @return mixed */ public function handle($request, Closure $next) { $response = $next($request); $this->actlog($request, $response -> getStatusCode()); return $response; } public function actlog($request, $status) { $user = $request -> user(); $data = [ 'user_id' => $user ? $user->id : null, 'route' => Route::currentRouteName(), 'url' => $request -> path(), 'method' => $request -> method(), 'status' => $status, 'data' => count($request->toArray()) != 0 ? json_encode($request->toArray()) : null, 'remote_addr' => $request -> ip(), 'user_agent' => $request -> userAgent(), ]; Actlog::create($data); } }ルーティング情報を取得するために Route を利用してます
ログインユーザは$request -> user() から取得
要求内容( 'data' )はJson形式で保存しときますミドルウェア登録
app/Http/Kernel.phpprotected $middleware = [ ~~~~ ★ \App\Http\Middleware\ActlogMiddleware::class, ]; ~~~~$middleware の中に追加すると グローバルとして必ず呼ばれるミドルウェアとして登録されるみたいです
詳しくはこちら
https://readouble.com/laravel/5.6/ja/middleware.htmlログアウト対応
このミドルウェアの実行タイミングは各処理が「終わった後」になるので「誰」がログアウトしたのか操作者のIDが記録できません
なのでログアウトの実行前にログを記録するようにしておきますログアウトを実際に行うのは 「 vendor/laravel/framework/src/Illuminate/Foundation/Auth/AuthenticatesUsers.php 」のようなので
ログアウト操作をこっち「 app/Http/Controllers/Auth/LoginController.php 」で上書きしてやりますapp/Http/Controllers/Auth/LoginController.php<?php namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; use Illuminate\Foundation\Auth\AuthenticatesUsers; use Illuminate\Http\Request; class LoginController extends Controller { ~~~~~~~~ /** * Log the user out of the application. * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\Response */ public function logout(Request $request) { $actlog = new \App\Http\Middleware\ActlogMiddleware; $actlog -> actlog($request, 999); $this->guard()->logout(); $request->session()->invalidate(); return $this->loggedOut($request) ?: redirect('/'); } }実際にログアウトを実行する「$this->guard()->logout();」前に、actlogを記録してやります
見分けやすいようにステータスを 「999」で記録しときます動作確認
1)DBに接続してテーブル定義とテーブル内容を確認
mysql -u lara_user -p lara mysql> desc actlogs; +-------------+------------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +-------------+------------------+------+-----+---------+----------------+ | id | int(10) unsigned | NO | PRI | NULL | auto_increment | | user_id | int(10) unsigned | YES | MUL | NULL | | | route | varchar(255) | YES | | NULL | | | url | varchar(255) | YES | | NULL | | | method | varchar(255) | YES | | NULL | | | status | int(10) unsigned | YES | | NULL | | | data | text | YES | | NULL | | | remote_addr | varchar(255) | YES | | NULL | | | user_agent | varchar(255) | YES | | NULL | | | created_at | timestamp | YES | MUL | NULL | | | updated_at | timestamp | YES | | NULL | | +-------------+------------------+------+-----+---------+----------------+ mysql> SELECT * FROM actlogs; Empty set (0.00 sec)2)ログイン画面表示
ブラウザでログイン画面を開きます
mysql> SELECT * FROM actlogs; +----+---------+------------+-------+--------+--------+------+-------------+--------------------------------------------------------------------------------------------------------------------+---------------------+------------+ | id | user_id | route | url | method | status | data | remote_addr | user_agent | created_at | updated_at | +----+---------+------------+-------+--------+--------+------+-------------+--------------------------------------------------------------------------------------------------------------------+---------------------+------------+ | 1 | NULL | NULL | / | GET | 302 | NULL | 172.16.0.1 | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.96 Safari/537.36 | 2019-02-08 07:42:45 | NULL | | 2 | NULL | login.show | login | GET | 200 | NULL | 172.16.0.1 | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.96 Safari/537.36 | 2019-02-08 07:42:45 | NULL | +----+---------+------------+-------+--------+--------+------+-------------+--------------------------------------------------------------------------------------------------------------------+---------------------+------------+ 2 rows in set (0.01 sec)最初 / にアクセスして 302で遷移
ログイン画面(login.show)を表示したログが記録されてます3)ログイン実行
ブラウザでログイン操作
+----+---------+------------+-------+--------+--------+----------------------------------------------------------------------+-------------+--------------------------------------------------------------------------------------------------------------------+---------------------+------------+ | id | user_id | route | url | method | status | data | remote_addr | user_agent | created_at | updated_at | +----+---------+------------+-------+--------+--------+----------------------------------------------------------------------+-------------+--------------------------------------------------------------------------------------------------------------------+---------------------+------------+ | 1 | NULL | NULL | / | GET | 302 | NULL | 172.16.0.1 | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.96 Safari/537.36 | 2019-02-08 07:42:45 | NULL | | 2 | NULL | login.show | login | GET | 200 | NULL | 172.16.0.1 | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.96 Safari/537.36 | 2019-02-08 07:42:45 | NULL | | 3 | 1 | login | login | POST | 302 | eyJpdiI6IjVmNE5UZ2ExSUw2QWYrTVh6ZUpqRnc9PSIsInZhbHVlZTRjYzAwZGI0NCJ9 | 172.16.0.1 | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.96 Safari/537.36 | 2019-02-08 07:44:28 | NULL | | 4 | 1 | NULL | / | GET | 200 | NULL | 172.16.0.1 | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.96 Safari/537.36 | 2019-02-08 07:44:28 | NULL | +----+---------+------------+-------+--------+--------+----------------------------------------------------------------------+-------------+--------------------------------------------------------------------------------------------------------------------+---------------------+------------+ 4 rows in set (0.00 sec)id:3 の行でログイン後、
id:4 / に行って Vue の画面を表示してます( routes/web.php で指定 )要求内容もちゃんと暗号化されているようです
ユーザIDも記録されてますね4)ページ遷移
別記事で作成済みの「社員一覧」ページを開いてみます
mysql> SELECT * FROM actlogs; +----+---------+------------------+----------------+--------+--------+----------------------------------------------------------------------+-------------+--------------------------------------------------------------------------------------------------------------------+---------------------+------------+ | id | user_id | route | url | method | status | data | remote_addr | user_agent | created_at | updated_at | +----+---------+------------------+----------------+--------+--------+----------------------------------------------------------------------+-------------+--------------------------------------------------------------------------------------------------------------------+---------------------+------------+ | 1 | NULL | NULL | / | GET | 302 | NULL | 172.16.0.1 | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.96 Safari/537.36 | 2019-02-08 07:42:45 | NULL | | 2 | NULL | login.show | login | GET | 200 | NULL | 172.16.0.1 | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.96 Safari/537.36 | 2019-02-08 07:42:45 | NULL | | 3 | 1 | login | login | POST | 302 | eyJpdiI6IjVmNE5UZ2UxOWRhMzhjNDdiN2I4NTAxNDcwMDZlODVlZTRjYzAwZGI0NCJ9 | 172.16.0.1 | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.96 Safari/537.36 | 2019-02-08 07:44:28 | NULL | | 4 | 1 | NULL | / | GET | 200 | NULL | 172.16.0.1 | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.96 Safari/537.36 | 2019-02-08 07:44:28 | NULL | | 5 | 1 | admin.user.index | api/admin/user | POST | 200 | NULL | 172.16.0.1 | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.96 Safari/537.36 | 2019-02-08 07:47:39 | NULL | +----+---------+------------------+----------------+--------+--------+----------------------------------------------------------------------+-------------+--------------------------------------------------------------------------------------------------------------------+---------------------+------------+id:5 でadmin.user.index ページを開いたログが記録されてます
5)ログアウト
ログアウト操作をしてみます
| 5 | 1 | admin.user.index | api/admin/user | POST | 200 | NULL | 172.16.0.1 | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.96 Safari/537.36 | 2019-02-08 07:47:39 | NULL | | 6 | 1 | logout | logout | POST | 999 | NULL | 172.16.0.1 | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.96 Safari/537.36 | 2019-02-08 07:50:28 | NULL | | 7 | NULL | logout | logout | POST | 302 | NULL | 172.16.0.1 | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.96 Safari/537.36 | 2019-02-08 07:50:28 | NULL | | 8 | NULL | NULL | / | GET | 401 | NULL | 172.16.0.1 | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.96 Safari/537.36 | 2019-02-08 07:50:28 | NULL | | 9 | NULL | NULL | / | GET | 302 | NULL | 172.16.0.1 | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.96 Safari/537.36 | 2019-02-08 07:50:28 | NULL | | 10 | NULL | login.show | login | GET | 200 | NULL | 172.16.0.1 | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.96 Safari/537.36 | 2019-02-08 07:50:28 | NULL | +----+---------+------------------+----------------+--------+--------+----------------------------------------------------------------------+-------------+--------------------------------------------------------------------------------------------------------------------+---------------------+------------+ 10 rows in set (0.00 sec)id:6 で実際のログアウト処理の前にログアウトを記録(999)して
id:7 で実際にログアウト
id:8 で 401(未認証)だから / に行って
id:9 で / から 302 でリダイレクトして
id:10 で ログイン画面を表示ってことになってますね
ちゃんと操作のログが記録されているようですVue 操作ログの一覧画面
ログ記録の確認が取れたので、画面側も作っておきます
resources/js/components/Admin/ActlogComponent.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" prepend-icon="search" label="Search" single-line hide-details clearable ></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-spacer></v-spacer> <v-card-actions> <csv-download url="/api/admin/actlog/download" color="primary" @axios-logout="$emit('axios-logout')"></csv-download> </v-card-actions> </v-card> </v-flex> </template> <script> import csv_download from './CsvDownload.vue' export default { name: 'ActlogComponent', components: { 'csv-download': csv_download, }, props: { }, data: () => ({ loading: false, search: '', pagination: { sortBy: 'created_at', descending: true, }, tabledata: [], headers: [ { align: 'center', sortable: false, text: 'No', }, { align: 'left', sortable: true, text: '日時', value: 'created_at' }, { align: 'left', sortable: true, text: '氏名', value: 'name' }, { align: 'left', sortable: true, text: '操作', value: 'action' }, { align: 'center', sortable: true, text: '結果', value: 'status' }, { align: 'left', sortable: true, text: 'データ', value: 'data' }, { align: 'left', sortable: true, text: 'IP', value: 'remote_addr' }, { align: 'left', sortable: true, text: 'UA', value: 'user_agent' }, ], }), created() { if (process.env.MIX_DEBUG) console.log('Actlog Component created.') this.initialize() }, methods: { initialize() { this.getData() }, getData() { if (process.env.MIX_DEBUG) console.log('Actlog Component getData') this.loading = true axios.post('/api/admin/actlog') .then( function (response) { this.loading = false if (process.env.MIX_DEBUG) console.log(response) if (response.data.data) { this.tabledata = this.setAction(response.data.data) } }.bind(this)) .catch(function (error) { this.loading = false console.log(error) if (error.response && [401, 419].includes(error.response.status)) { this.$emit('axios-logout') } }.bind(this)) }, setAction(data) { if (process.env.MIX_DEBUG) console.log('Actlog Component setAction') var wk = '' for (let i=0; i<data.length; i++) { switch (data[i].route) { // Login - Logout case 'show.login' : wk = 'ログイン画面'; break; case 'login' : wk = 'ログイン'; break; case 'logout' : wk = 'ログアウト'; break; // USER case 'admin.user.index' : wk = '社員一覧'; break; case 'admin.user.store' : wk = '社員追加'; break; case 'admin.user.destroy' : wk = '社員削除'; break; case 'admin.user.download' : wk = '社員CSV_DL'; break; case 'admin.user.upload' : wk = '社員CSV_UP'; break; // OTHER default: wk = data[i].route } data[i].action = wk } return data }, }, } </script>一覧を取ってきて、route名をわかりやすい日本語にして表示
ってことをしています操作履歴はデータ量が多そうなので、取得範囲やら「誰」やらの検索条件指定などもそのうち追加しなくちゃですかね
Vue ナビゲーション追加
ページを追加したのでナビゲーションも追加しときます
まずは定義ファイル
resources/js/router/index.jsimport 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' import admin_payslip from '../components/Admin/PayslipComponent.vue' ★ import admin_actlog from '../components/Admin/ActlogComponent.vue' export default new Router({ mode: 'history', routes: [ { path: '/admin/user', name: 'admin_user', component: admin_user, meta: {name: '社員管理', icon: 'supervisor_account'}}, { path: '/home', name: 'home', component: home, meta: {name: 'ホーム', icon: 'home'}}, { path: '/admin/payslip',name: 'admin_payslip', component: admin_payslip, meta: {name: '給与明細', icon: 'fa-file-invoice-dollar'}}, ★ { path: '/admin/actlog', name: 'admin_actlog', component: admin_actlog, meta: {name: '操作履歴', icon: 'list'}}, { path: '*', redirect: '/home' }, ], })そしてリンク埋め込み
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> <r-link linkname='admin_payslip'></r-link> ★ <r-link linkname='admin_actlog'></r-link> </v-list> </v-navigation-drawer> <v-toolbar color="primary" dark fixed app clipped-left> ~~~~Laravel 操作ログのコントローラ
クライアント(Vue)からの一覧表示要求に答えるコントローラを追加しときます
ついでにCSV ダウンロード機能もapp/Http/Controllers/ActlogController.php<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use Illuminate\Support\Facades\Log; use App\Actlog; use App\Facades\Csv; class ActlogController extends Controller { public function index() { Log::Debug(__CLASS__.':'.__FUNCTION__); $actlog = Actlog::where('user_id', '>', '0') -> whereNotNull('route') -> orderby('created_at', 'desc') -> limit(100) -> get(); return ['data' => $actlog]; } public function download(Request $request) { Log::Debug(__CLASS__.':'.__FUNCTION__, $request->all()); // 取得項目設定 $head = ['created_at', 'route', 'status', 'remote_addr', 'user_agent', 'user_id']; // 抽出 $data = Actlog::select( $head ) -> where('user_id', '>', '0') -> whereNotNull('route') -> orderby('created_at', 'desc') -> get() -> toArray(); // 自動付与の名前をヘッダーに追加 $head[] = 'name'; // CSV DOWNLOAD return Csv::download($data, $head, 'test.csv'); } }ログイン後の操作履歴( user_id > 0 )だけを表示するようにしてます
さらに正規の要求(whereNotNull route)の履歴だけを対象に
ついでに直近100件(limit 100)のみを表示対象としときますLaravel ルーティング設定
コントローラを追加したのでルーティングも設定
routes/web.php<?php /* |-------------------------------------------------------------------------- | Web Routes |-------------------------------------------------------------------------- | | Here is where you can register web routes for your application. These | routes are loaded by the RouteServiceProvider within a group which | contains the "web" middleware group. Now create something great! | */ Route::get('/', function () { return view('home'); })->middleware('auth'); // Authentication Routes... Route::get('/login', 'Auth\LoginController@showLoginForm') -> name('login.show'); Route::post('/login', 'Auth\LoginController@login') -> name('login'); Route::post('/logout', 'Auth\LoginController@logout') -> name('logout'); // Admin Route::GROUP(['middleware' => ['auth', 'can:admin']], function() { Route::prefix('/api/admin/') -> name('admin.') -> group(function() { // USER Route::name('user.') -> group(function() { Route::post('user', 'UserController@index') -> name('index'); Route::post('user/store', 'UserController@store') -> name('store'); Route::post('user/destroy', 'UserController@destroy') -> name('destroy'); Route::post('user/download', 'UserController@download') -> name('download'); Route::post('user/upload', 'UserController@upload') -> name('upload'); }); // CsvPayslip Route::name('csvpayslip.') -> group(function() { Route::post('csvpayslip/index', 'CsvPayslipController@index') -> name('index'); Route::post('csvpayslip/upload', 'CsvPayslipController@upload') -> name('upload'); Route::post('csvpayslip/delete', 'CsvPayslipController@delete') -> name('delete'); Route::post('csvpayslip/publish', 'CsvPayslipController@publish') -> name('publish'); }); // Payslip Route::name('payslip.') -> group(function() { Route::post('payslip/index', 'PayslipController@index') -> name('index'); Route::post('payslip/delete', 'PayslipController@delete') -> name('delete'); Route::post('payslip/pdf', 'PayslipController@pdf') -> name('pdf'); }); ★ // Actlog Route::name('actlog.') -> group(function() { Route::post('actlog', 'ActlogController@index') -> name('index'); Route::post('actlog/download', 'ActlogController@download') -> name('download'); }); }); }); // Other Route::get('/{any}', function () { return view('home'); })->middleware('auth')->where('any', '.*');一覧画面動作確認
以上
ミドルウェアとして登録したことで処理系には一切手を入れずにアクションログを自動で記録できるようになりましたLaravel ステキ
今回もソースはこちら
https://github.com/u9m31/u9m31/tree/step14
- 投稿日:2019-02-15T02:13:40+09:00
Vue on RailsでActive Storageを使って画像を保存する
はじめに
Rails API モードと Vue.js で作成した 自作ブログ で Active Storage を使う際に、画像の受け渡しでハマったので実装方法を残します。
実装するのは Active Storage を使って、eyecatch (アイキャッチ画像) 付きの Post (記事) を投稿できるようなサンプルです。環境
$ ruby -v ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-darwin16] $ bundle exec rails --version Rails 5.2.2実装方法
API 部分 (Rails)
Ⅰ. サンプルアプリケーション新規作成
Rails API モードで作成します。
$ bundle exec rails new sampleapp --database=mysql --api --force $ cd sampleapp/ $ bundle exec rails db:createⅡ. Active Storage インストール
Active Storage をインストールします。
$ bundle exec rails active_storage:install $ bundle exec rails db:migrateⅢ. Post リソースの作成
今回利用する Post モデルとコントローラを作成します。
$ bundle exec rails g resource post title body:text $ bundle exec rails db:migrateⅣ. 各種ファイルの修正
各 Post に eyecatch を設定できるようにモデルを修正します。
追加する eyecatch= メソッドで、Active Storage に画像を保存します。このメソッドは、Base64 形式で受け取った image データをエンコードし、一時的に /tmp 配下に画像ファイルを作成、作成した画像ファイルをアタッチ、その後画像ファイルを削除します。
app/models/post.rbclass Post < ApplicationRecord has_one_attached :eyecatch attr_accessor :image def eyecatch=(image) if image.present? prefix = image[/(image|application)(\/.*)(?=\;)/] type = prefix.sub(/(image|application)(\/)/, '') data = Base64.decode64(image.sub(/data:#{prefix};base64,/, '')) filename = "#{Time.zone.now.strftime('%Y%m%d%H%M%S%L')}.#{type}" File.open("#{Rails.root}/tmp/#{filename}", 'wb') do |f| f.write(data) end eyecatch.detach if eyecatch.attached? eyecatch.attach(io: File.open("#{Rails.root}/tmp/#{filename}"), filename: filename) FileUtils.rm("#{Rails.root}/tmp/#{filename}") end end endモデルで追加した eyecatch= メソッドに POST で受け取る画像のパラメータを渡すように修正します。
app/controllers/posts_controller.rbclass PostsController < ApplicationController def create post = Post.new(post_params) if post.save post.eyecatch = post_params[:image] render json: post, status: :created else render json: post.errors, status: :unprocessable_entity end end private def post_params params.require(:post).permit(:title, :body, :image) end endⅤ. 動作確認
アプリケーションを起動し、curl コマンドでアイキャッチ付き Post が作成できるか確認します。
$ bundle exec rails s $ curl -v -H "Accept: application/json" -H "Content-type: application/json" -X POST -d '{"post": {"title": "Sample title.", "body": "Sample body.", "image": "data:application/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAKCAYAAACALL/6AAAACXBIWXMAADXU\nAAA11AFeZeUIAAAAgUlEQVQYlZWQMQ6DMAxFnyMGJFZCc4be/yisRYyVmDN0\niTuQULCCoH9z/PLtb/hTsi8SaA1yO85ZWHxgAqb8HuVoJAX+iKPtB17L++D+\nyN6drpOa0mj7AYBg1pmz99NmSKDiVzzmKTM/uOTYMjgQNetYuKoEqj7oCHp2\nteqn2/CVvuDZJy0n3DrVAAAAAElFTkSuQmCC\n"}}' http://localhost:3000/postscurl コマンドで作成後、Rails Console でアイキャッチが正常に追加できているか確認します。
$ bundle exec rails c irb(main):001:0> Post.find_by(title: "Sample title.").eyecatch.attached? => true # 画像のパスは以下のように取得できます。 # irb(main):002:0> app.url_for(Post.find_by(title: "Sample title.").eyecatch) # => "画像のパス"フロント部分 (Vue.js)
Ⅰ. Webpacker をインストール
webpacker gem を追加する。
Gemfilegem 'webpacker', '~> 3.5'Webpacker をインストールします。
$ bundle $ bundle exec rails webpacker:installⅡ. Vue.js をインストール
Webpacker で Vue.js をインストールします。
$ bundle exec rails webpacker:install:vueⅢ. Home ページを作成
Vue.js を返すための Home ページを作成します。
$ bundle exec rails g controller Pages HomeRoot パスに Home ページを設定します。
config/routes.rbRails.application.routes.draw do root 'pages#home' end標準の JSON ではなく、ERB を返すために ActionController::Base に修正します。
app/controllers/pages_controller.rbclass PagesController < ActionController::Base def home end endHome ページに利用する View を作成します。
$ mkdir -p app/views/pages/ $ touch app/views/pages/home.html.erbⅣ. Vue.js を利用
Vue.js を利用するための設定を行います。
app/views/pages/home.html.erb<%= javascript_pack_tag 'main' %>利用する各種ファイルを作成します。
$ touch app/javascript/packs/main.js $ touch app/javascript/packs/App.vueapp/javascript/packs/main.jsimport Vue from 'vue' import App from './App.vue' document.addEventListener('DOMContentLoaded', () => { const el = document.body.appendChild(document.createElement('main')) new Vue({ el, render: h => h(App) }) })app/javascript/packs/App.vue<template> <div id="app"> <p>投稿フォーム</p> </div> </template>Ⅴ. 投稿フォームを作成
API コールに利用する axios をインストールします。
$ yarn add axios
画像投稿するフォームを用意します。
画像は POST する前に Base64 にデコードしています。app/javascript/packs/App.vue<template> <div id="app"> <p>投稿フォーム</p> <form v-on:submit.prevent="postItem()"> <p> <label>Title</label> <input name="post.title" type="text" v-model="post.title"><br /> </p> <p> <label>Body</label> <input name="post.body" type="text" v-model="post.body"><br /> </p> <p> <label>画像</label> <input name="uploadedImage" type="file" ref="file" v-on:change="onFileChange()"><br /> </p> <input type="submit" value="Submit"> </form> </div> </template> <script> import axios from 'axios' export default { data() { return { post: {}, uploadedImage: '' } }, methods: { onFileChange() { let file = event.target.files[0] || event.dataTransfer.files let reader = new FileReader() reader.onload = () => { this.uploadedImage = event.target.result this.post.image = this.uploadedImage } reader.readAsDataURL(file) }, postItem() { return new Promise((resolve, _) => { axios({ url: '/posts', data: { post: this.post }, method: 'POST' }).then(res => { this.post = {} this.uploadedImage = '' this.$refs.file.value = '' resolve(res) }).catch(e => { console.log(e) }) }) } } } </script>Ⅵ. 動作確認
アプリケーションを起動し、投稿フォームから画像を投稿します。
http://localhost:3000/$ bundle exec rails sRails Console でアイキャッチが正常に追加できているか確認します。
$ bundle exec rails c irb(main):001:0> Post.last.eyecatch.attached? => true参考記事
https://qiita.com/ozin/items/5ec81a4b126b8ebf7a96
最後に
読んでいただいてありがとうございます。
間違っている点などがありましたら、ご指摘いただけると喜びます!