Vuetify 2.0 で datatable を使って一覧画面を作る
Vuetify 2.0の v-datatable を用いて以下の方針で一覧画面を作ってみます。
- 並べ変えに対応させる (第2ソート、第3ソートにも対応)
- 絞り込みに対応させる
- 極力日本語化
- ページ切り替えの度にAjaxでレコードを取得
画面イメージ
PCで見た場合
スマホで見た場合 (表形式ではなく縦長のレイアウトになる!)
まずは全体のソース
<template>
<div id="form">
<v-container fluid>
<v-row justify="center">
<v-col>
<v-card>
<v-app-bar color="primary" dark>
<v-toolbar-title><v-icon>list_alt</v-icon> ○○一覧</v-toolbar-title>
<v-tooltip bottom>
<template v-slot:activator="{ on }">
<v-btn color="pink" dark small absolute bottom right fab to="/create" v-on="on"><v-icon>add</v-icon></v-btn>
</template>
<span>新しい○○を作成する</span>
</v-tooltip>
</v-app-bar>
<v-form v-model="valid" ref="listForm" lazy-validation>
<v-container fluid class="pa-1">
<v-row class="px-2">
<v-col cols="6" class="pa-1">
<v-text-field
prepend-inner-icon="search"
clearable
label="タイトル"
name="title"
maxlength="64"
v-model="model.title"
@change="loadList"
></v-text-field>
</v-col>
<v-col cols="3" class="pa-1">
<v-select
label="種別"
name="type"
item-text="label"
item-value="value"
:items="[
{ label: '-', value: null },
{ label: '銀行', value: 0 },
{ label: '郵便局', value: 1 },
]"
v-model="model.type"
@change="loadList"
></v-select>
</v-col>
<v-col cols="3" class="pa-1">
<v-select
label="公開"
name="is_open"
item-text="label"
item-value="value"
:items="[
{ label: '-', value: null },
{ label: '公開中', value: true },
{ label: '未公開', value: false },
]"
v-model="model.is_open"
@change="loadList"
></v-select>
</v-col>
</v-row>
</v-container>
<v-data-table
:headers="headers"
:items="items"
:options.sync="options"
:server-items-length="total"
:footer-props="{
'items-per-page-options': [10, 20, 50, 100, 200, 300, 400, 500],
showFirstLastPage: true,
}"
:loading="loading"
multi-sort
locale="ja-jp"
loading-text="読込中"
no-data-text="データがありません。"
class="elevation-1"
>
<template v-slot:item.uri="{ item }">
<a
:href="'https://hoge.jp/' + item.uri"
target="_blank"
>hoge.jp/{{ item.uri }}</a
>
</template>
<template v-slot:item.type="{ item }">
{{ selectionItems.type[item.type] }}
</template>
<template v-slot:item.is_open="{ item }">
<v-icon v-if="item.is_open">check</v-icon>
</template>
<template v-slot:item.created_at="{ item }">
{{ item.created_at.replace('T', ' ').replace(/-/g, '/') }}
</template>
<template v-slot:item.updated_at="{ item }">
{{ item.updated_at.replace('T', ' ').replace(/-/g, '/') }}
</template>
<template v-slot:item.code="{ item }">
{{ item.name }}
</template>
<template v-slot:item.action="{ item }">
<v-btn small class="mx-1" color="orange accent-4" :to="'/detail/' + item.uri">
<v-icon>pageview</v-icon>詳細
</v-btn>
<v-btn small class="mx-1" color="orange accent-4" :to="'/stats/' + item.uri">
<v-icon>bar_chart</v-icon>集計
</v-btn>
</template>
</v-data-table>
</v-form>
</v-card>
</v-col>
</v-row>
</v-container>
</div>
</template>
<script>
import Axios from 'axios'
export default {
data: () => ({
loading: false,
headers: [
{ text: 'ID', align: 'center', sortable: true, value: 'form_id' },
{ text: 'タイトル', align: 'center', sortable: true, value: 'title' },
{ text: 'URL', align: 'center', sortable: false, value: 'uri' },
{ text: '種別', align: 'center', sortable: false, value: 'form_type' },
{ text: '公開', align: 'center', sortable: false, value: 'is_open' },
{ text: '作成日時', align: 'center', sortable: true, value: 'created_at' },
{ text: '更新日時', align: 'center', sortable: true, value: 'updated_at' },
{ text: '組織', align: 'center', sortable: false, value: 'code' },
{ text: '操作', align: 'center', sortable: false, value: 'action' }
],
options: {
page: 1,
itemsPerPage: 20,
sortBy: ['form_id'],
sortDesc: [true],
},
items: [],
total: 0,
selectionItems: {
form_type: ['銀行', '郵便局'],
},
model: {
'title': '',
type: null,
is_open: null,
},
}),
watch: {
options: {
handler() {
this.loadList()
},
deep: true,
},
},
methods: {
async loadList() {
this.loading = true
try {
let sorts = []
if (this.options.sortBy !== null) {
this.options.sortBy.forEach((value, index) => {
sorts.push((this.options.sortDesc[index] ? '-' : '+') + value)
})
}
const res = await Axios.post(
'/api/list',
Object.assign(this.model, {
offset: (this.options.page - 1) * this.options.itemsPerPage,
limit: this.options.itemsPerPage,
sort: sorts.join(' ')
})
)
if (res.data) {
this.items = res.data.items
this.total = res.data.total
}
} catch (error) {
alert('情報を取得できませんでした。時間をおいてやり直してください。')
}
this.loading = false
},
},
created: function() {
this.loadList()
},
}
</script>
Vuetify 1.5との違い
結構色々変わってて苦戦しました。
-
v-datatable
の項目名が色々変わっている -
td
タグを書いていく方式から、各カラムのtemplate
タグを実装していく方式に変わった - ソートカラムが文字列ではなく配列で返ってくる (multi-sortでない場合でも)
Ajaxでのデータ取得
datatable
は、
- 全レコードを取得して並べ替えや絞り込みをJS内で行う方式
- ページ切り替えやソート方法変更の度に該当ページのレコードを取得する方式
があります。レコードが数万レコードレベルで大量に存在する場合は前者の方法は使えません。
そこで後者の方式で実装します。
<v-data-table
:headers="headers"
:items="items"
:server-items-length="total"
>
:server-item-length
でレコードの総数を渡してあげると後者のモードになる模様。あとは、
const res = await Axios.post(
'/api/list',
{
offset: (this.options.page - 1) * this.options.itemsPerPage,
limit: this.options.itemsPerPage
}
)
if (res.data) {
this.items = res.data.items
this.total = res.data.total
}
で、現在のページ番号に応じたレコードを取得して変数に格納してあげるだけでいけました。
ソート
<v-data-table
multi-sort
>
第二ソート以上に対応させる場合は、multi-sort
を指定し、対応させない場合は multi-sort
を外して、必要に応じて must-sort
を入れます。
multi-sort
と must-sort
を共存させてしまうと、ソートが外せなくなり、第一ソートと第二ソートの入れ替えなどができなくなってしまいます。
あとは、options.sortByにソート方法の配列 (例えば、['title', 'id']
)、options.sortDescに昇順・降順の配列 (例えば、[true, false]
)が格納されるので、APIにあった方法に変換してAjaxで渡します。
今回は、 +title -id
といった形式で渡すようにしてみました。
let sorts = []
if (this.options.sortBy !== null) {
this.options.sortBy.forEach((value, index) => {
sorts.push((this.options.sortDesc[index] ? '-' : '+') + value)
})
}
sorts = sorts.join(' ')
テーブルの描画
Ajaxから取得したデータをそのまま出力する場合は問題無いのですが、リンクやボタンをつけたり、複数の情報を組み合わせてデータを出力する場合などは、セルの描画内容をカスタマイズします。
<!-- リンクを出力する例 -->
<template v-slot:item.uri="{ item }">
<a
:href="'https://hoge.jp/' + item.uri"
target="_blank"
>hoge.jp/{{ item.uri }}</a
>
</template>
<!-- カラム値を文字列に変換して出力する例 -->
<template v-slot:item.type="{ item }">
{{ selectionItems.type[item.type] }}
</template>
<!-- チェックマークをつける例 -->
<template v-slot:item.is_open="{ item }">
<v-icon v-if="item.is_open">check</v-icon>
</template>
<!-- ボタンを設置する例 -->
<template v-slot:item.action="{ item }">
<v-btn small class="mx-1" color="orange accent-4" :to="'/detail/' + item.uri">
<v-icon>pageview</v-icon>詳細
</v-btn>
</template>
絞り込み
各絞り込みフィールドに @change
属性を追加し、発火したらAjaxでレコードを取得し直せば問題無いでしょう。
日本語化
Vuetifyの言語設定を ja
に変えれば日本語になってくれました。 Vuetify
を初期化している処理で、
import ja from 'vuetify/es5/locale/ja.js'
new Vuetify({
lang: {
locales: {ja},
current: 'ja'
}
})
とすることで概ね日本語化されました。ただ、それでも日本語化されない箇所があったので別途 datatable
側でも直に日本語設定を入れました。
<v-data-table
loading-text="読込中"
no-data-text="データがありません。"
>