Laravel(Controller)
/**
* Display the listing of the resource.
*
* @return View
*/
public function index(TagRequest $requset): View
{
$names = explode(',', $requset->input('name'));
$tags = $this->tagRepository->getByNames($names);
return view('admin.tags.index', ['tags' => $tags]);
}
Laravel(Repository)
/**
* @param array<string> $names
* @return LengthAwarePaginator
*/
public function getByNames(array $names): LengthAwarePaginator
{
$query = $this->tag->query();
foreach ($names as $name) {
$keyword = trim($name);
if ($keyword === '') {
continue;
}
$query->where('name', 'LIKE', "%{$keyword}%");
}
return $query->latest()->paginate();
}
index.blade.php
@extends('admin.layouts.app')
@section('title', 'Tags')
@section('content')
<page-admin-tags :laravel-paginator="{{$tags->toJson()}}" />
@endsection
index.vue
<template>
<div class="tags">
<header class="tags__header">
<AppHeading :level="1" :visual="1" dark class="tags__title">タグ一覧</AppHeading>
<div class="tags__createBtn">
<CreateBtn href="/tags/create">新規追加</CreateBtn>
<CreateBtn @click="isSearchVisible = true" icon="search" primary>検索</CreateBtn>
</div>
</header>
<template v-if="toastText">
<ToastBox :caution="hasRequestError" class="posts__toaster">{{ toastText }}</ToastBox>
</template>
<template v-if="paginator.total">
<DataCounter :from="paginator.from" :to="paginator.to" :total="paginator.total" class="tags__counter" dark />
</template>
<FadeTransition>
<template v-if="selected.length">
<BulkActions v-model="bulkAction" :actions="bulkActions" @click="handleBulkAction" />
</template>
</FadeTransition>
<TagTable v-model="selected" :items="tags" @delete="handleDelete" editable deletable dark class="tags__table" />
<template v-if="paginator.lastPage > 1">
<AppPager
:current="paginator.currentPage"
:last="paginator.lastPage"
@click="handlePager"
dark
class="tags__pager"
/>
</template>
<form ref="delete" :action="`/tags/${deleteId}`" method="post">
<input type="hidden" name="_method" value="delete" />
<input :value="csrfToken" type="hidden" name="_token" />
</form>
<TagSearch
:is-visible="isSearchVisible"
:searched-name="searchQuery.name"
@search="handleSearchSubmit"
@cancel="handleSearchCancel"
/>
</div>
</template>
<script lang="ts">
import Vue, { PropType } from 'vue'
import { AppHeading, AppPager } from '~/components/atoms'
import { CreateBtn, DataCounter, TagTable, BulkActions } from '~/components/molecules'
import { TagSearch } from '~/components/organisms'
import { FadeTransition } from '~/components/presenters'
import { LaravelPaginator, Paginator, FormOption } from '~/types/models'
import { Tag } from '~/types/Tags'
import recursiveToCamels from '~/utils/recursiveToCamels'
import { AxiosResponse } from 'axios'
interface Data {
bulkAction: string
selected: string[]
tags: Tag[]
deleteId: number | null
isSearchVisible: boolean
searchQuery: {
name: string
}
toastText: string
hasRequestError: boolean
}
const Tags = Vue.extend({
components: { AppHeading, AppPager, CreateBtn, DataCounter, TagTable, BulkActions, FadeTransition, TagSearch },
props: {
laravelPaginator: { type: Object as PropType<LaravelPaginator>, required: true }
},
data(): Data {
return {
bulkAction: '',
selected: [],
tags: [],
deleteId: null,
isSearchVisible: false,
searchQuery: {
name: ''
},
toastText: '',
hasRequestError: false
}
},
computed: {
paginator(): Paginator {
return recursiveToCamels(this.laravelPaginator) as Paginator
},
csrfToken(): string {
return this.$store.getters.csrfToken
},
bulkActions(): FormOption[] {
return [{ id: 0, label: '削除', value: 'delete' }]
}
},
created() {
this.tags = this.paginator.data as Tag[]
},
methods: {
apiUrl() {
return window.Laravel.apiUrl
},
async requestBulk(action: string, payload: { [key: string]: unknown }): Promise<AxiosResponse> {
const endPoint = `${window.Laravel.apiUrl}/tags`
let response
switch (action) {
case 'delete':
response = await this.$axios.delete(endPoint, { data: payload })
break
default:
return
}
return response
},
async deleteTags() {
const ids: number[] = this.selected.map(tagId => Number(tagId))
await this.requestBulk('delete', { id: ids })
this.tags = this.tags.filter(tag => {
return ids.indexOf(tag.id) === -1
})
this.selected = []
},
handleBulkAction() {
switch (this.bulkAction) {
case 'delete':
if (window.confirm('選択したタグを削除します。よろしいですか?')) {
this.deleteTags()
}
break
}
},
handleDelete(id: number) {
this.deleteId = id
if (window.confirm('削除してもよろしいですか?')) {
this.$nextTick(() => {
;(this.$refs.delete as HTMLFormElement).submit()
})
}
},
handlePager(page: number) {
const currentPage = new URL(window.location.href)
currentPage.searchParams.set('page', String(page))
window.location.href = currentPage.toString()
},
handleSearchSubmit(values: { [key: string]: string }) {
const currentPage = new URL(window.location.href)
const searchParams = new URLSearchParams(values)
searchParams.forEach((value: string, key: string) => {
searchParams.set(key, value.replace(/\s/g, ''))
})
window.location.href = `${currentPage.pathname}?${searchParams.toString()}`
},
handleSearchCancel() {
this.isSearchVisible = false
this.searchQuery = {
name: ''
}
}
}
})
export default Vue.component('page-admin-tags', Tags)
</script>
<style lang="scss">
</style>
models.ts
export interface FormOption {
label: string
value: string
id: number
}
export interface LaravelFormValue {
dataSource?: any[] | { [key: string]: any }
hasError: boolean
msgError: string
value?: any
}
export interface LaravelPaginator {
current_page: number
data: any[]
first_page_url: string
from: number
last_page: number
last_page_url: string
next_page_url?: string
path: string
per_page: number
prev_page_url?: string
to: number
total: number
}
export interface Paginator {
currentPage: number
data: any[]
firstPageUrl: string
from: number
lastPage: number
lastPageUrl: string
nextPageUrl?: string
path: string
perPage: number
prevPageUrl?: string
to: number
total: number
}
export interface Tab {
id: number
to: string
label: string
}
export interface ECRow {
id: number
name: string
createdAt: string
updatedAt: string
}
export interface Pager {
id: number
to: string | number
}
export interface Link {
id: number
to: string
label: string
}
export interface NavLink extends Link {
icon?: string
slug?: string
}
export interface NavGroup {
id: number
name: string
items: NavLink[]
}
export interface NavTab extends Tab {
icon: string
}
export interface DescriptionGroup {
id: number
name: string
items: string[]
}
export interface BulkActionType {
label: string
action: string
}
export interface AutocompleteOption {
id: number
label: string
disabled?: boolean
}
AppPager.vue
<template>
<ul :class="styledClasses" class="appPager">
<template v-if="last > 1">
<li class="appPager__item">
<button :disabled="current < 2" @click="$emit('click', current - 1)" class="appPager__btn">Prev</button>
</li>
</template>
<template v-if="last > 1">
<li class="appPager__item">
<button :aria-current="String(1 === current)" @click="$emit('click', 1)" class="appPager__btn">1</button>
</li>
</template>
<template v-if="displayBeforeEllipsis">
<li class="appPager__item">...</li>
</template>
<template v-for="item in pages">
<li :key="item" class="appPager__item">
<button :aria-current="String(item === current)" @click="$emit('click', item)" class="appPager__btn">
{{ item }}
</button>
</li>
</template>
<template v-if="displayAfterEllipsis">
<li class="appPager__item">...</li>
</template>
<template v-if="last > 1">
<li class="appPager__item">
<button :aria-current="String(last === current)" @click="$emit('click', last)" class="appPager__btn">
{{ last }}
</button>
</li>
</template>
<template v-if="last > 1">
<li class="appPager__item">
<button :disabled="current >= last" @click="$emit('click', current + 1)" class="appPager__btn">Next</button>
</li>
</template>
</ul>
</template>
<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
props: {
current: { type: Number, required: true },
last: { type: Number, required: true },
small: { type: Boolean },
dark: { type: Boolean }
},
computed: {
styledClasses(): string[] {
const classes: string[] = []
if (this.small) classes.push('-small')
if (this.dark) classes.push('-dark')
return classes
},
displayBeforeEllipsis(): boolean {
if (this.last <= 7) return false
return this.current - 3 > 1
},
displayAfterEllipsis(): boolean {
if (this.last <= 7) return false
return this.current + 3 < this.last
},
pages(): number[] {
const items: number[] = []
if (this.last <= 6) {
for (let i = 2; i < this.last; i++) {
items.push(i)
}
return items
}
if (this.current < 4) return [2, 3, 4, 5, 6]
if (this.current + 3 >= this.last) {
for (let i = this.last - 5; i < this.last; i++) {
items.push(i)
}
return items
}
for (let i = this.current - 2, length = this.current + 3; i < length; i++) {
items.push(i)
}
return items
}
}
})
</script>
<style lang="scss" scoped>
</style>
コメント