vue csvファイル

index.blade.php

@extends('admin.layouts.app')

@section('title', 'Customers')

@php
$dataSearchForm = [
    'inputs' => $inputs,
    'posts' => $posts,
    'jobs' => $jobs,
    'prefectures' => $prefectures,
    'sexes' => $sexes,
    'mailmaga-flgs' => $mailmagaFlgs,
    'tags' => $articleTags,
    'statuses' => $statuses
 ];
@endphp

@section('content')
<page-admin-customers
    :laravel-paginator="{{$customers->toJson()}}"
    :search-form="{{json_encode($dataSearchForm)}}"
    :max-file-size="'{{ config('oms.admin.csv_upload_max_filesize') }}'"
    :export-url="'{{ route('admin.customers.report', $inputs ?? []) }}'"
/>

@endsection

Index.vue

<template>
  <div class="customers">
    <header class="customers__header">
      <AppHeading :level="1" :visual="1" class="customers__title" dark>会員一覧</AppHeading>
      <ul class="customers__btns">
        <li class="customers__btn">
          <CreateBtn href="/customers/create">新規追加</CreateBtn>
        </li>
        <li class="customers__btn">
          <CreateBtn :href="exportUrl" :disabled="!isEnableCsvDownload" icon="cloud_download" primary>
            CSV出力
          </CreateBtn>
        </li>
        <li class="customers__btn">
          <CreateBtn @click="isVisibleSearch = true" icon="search">検索</CreateBtn>
        </li>
        <li class="customers__btn">
          <CreateBtn @click="isCsvImportVisible = true" icon="cloud" primary>CSVインポート</CreateBtn>
        </li>
      </ul>
    </header>

    <template v-if="paginator.total">
      <DataCounter
        :from="paginator.from"
        :to="paginator.to"
        :total="paginator.total"
        class="paymentTypes__counter"
        dark
      />
    </template>

    <CustomerTable
      v-model="selected"
      :items="paginator.data"
      @delete="handleDelete"
      editable
      deletable
      dark
      class="customers__table"
    />

    <template v-if="paginator.lastPage > 1">
      <AppPager
        :current="paginator.currentPage"
        :last="paginator.lastPage"
        @click="handlePager"
        class="deliveryTypes__pager"
        dark
      />
    </template>

    <form ref="delete" :action="`/customers/${deleteId}`" method="post">
      <input :value="csrfToken" type="hidden" name="_token" />
      <input type="hidden" name="_method" value="delete" />
    </form>

    <CustomersSearch
      :is-visible="isVisibleSearch"
      :prefecture-options="getFormOptions('prefectures')"
      :sex-options="getFormOptions('sexes')"
      :position-options="getFormOptions('positions')"
      :post-options="getFormOptions('posts')"
      :job-options="getFormOptions('jobs')"
      :tag-options="getFormOptions('tags')"
      :status-options="getFormOptions('statuses')"
      :mailmaga-flg-options="getFormOptions('mailmaga-flgs')"
      :notification-mail-flg-options="getFormOptions('notification-mail-flgs')"
      :default-values="search"
      @cancel="isVisibleSearch = false"
      @save="handleSearchInput"
    />

    <form ref="search" method="get" action="/customers">
      <input :value="search.customerId" type="hidden" name="customer_id" />
      <input :value="search.email" type="hidden" name="email" />
      <input :value="search.name" type="hidden" name="name" />
      <input :value="search.kana" type="hidden" name="kana" />
      <input :value="search.prefectureId" type="hidden" name="prefecture_id" />
      <input :value="search.postcode" type="hidden" name="postcode" />
      <input :value="search.city" type="hidden" name="city" />
      <input :value="search.line1" type="hidden" name="line1" />
      <template v-if="search.sexes.length">
        <input v-for="(value, i) in search.sexes" :key="`sexes-${i}`" :value="value" type="hidden" name="sexes[]" />
      </template>
      <input :value="search.tel" type="hidden" name="tel" />
      <input :value="search.fax" type="hidden" name="fax" />
      <input :value="search.birthFrom" type="hidden" name="birth_from" />
      <input :value="search.birthTo" type="hidden" name="birth_to" />
      <input :value="search.birthMonth" type="hidden" name="birth_month" />
      <input :value="search.company" type="hidden" name="company" />
      <input :value="search.unit" type="hidden" name="unit" />
      <template v-if="search.posts.length">
        <input v-for="(value, i) in search.posts" :key="`posts-${i}`" :value="value" type="hidden" name="posts[]" />
      </template>
      <template v-if="search.jobs.length">
        <input v-for="(value, i) in search.jobs" :key="`jobs-${i}`" :value="value" type="hidden" name="jobs[]" />
      </template>
      <template v-if="search.articleTags.length">
        <input
          v-for="(value, i) in search.articleTags"
          :key="`tags-${i}`"
          :value="value"
          type="hidden"
          name="article_tags[]"
        />
      </template>
      <input :value="search.createdAtFrom" type="hidden" name="created_at_from" />
      <input :value="search.createdAtTo" type="hidden" name="created_at_to" />
      <input :value="search.premiumDeadlineFrom" type="hidden" name="premium_deadline_from" />
      <input :value="search.premiumDeadlineTo" type="hidden" name="premium_deadline_to" />
      <input :value="search.lastLoginDateFrom" type="hidden" name="last_login_date_from" />
      <input :value="search.lastLoginDateTo" type="hidden" name="last_login_date_to" />
      <template v-if="search.statuses.length">
        <input
          v-for="(value, i) in search.statuses"
          :key="`statuses-${i}`"
          :value="value"
          type="hidden"
          name="statuses[]"
        />
      </template>
      <template v-if="search.mailmagaFlgs.length">
        <input
          v-for="(value, i) in search.mailmagaFlgs"
          :key="`mailmagaFlgs-${i}`"
          :value="value"
          type="hidden"
          name="mailmaga_flgs[]"
        />
      </template>
    </form>

    <form ref="import" action="/customers/import_csv" method="post" enctype="multipart/form-data">
      <input :value="csrfToken" type="hidden" name="_token" />
      <CustomersCsvImport
        :is-visible="isCsvImportVisible"
        :max-file-size="maxFileSize"
        @csv-import-button-submit="handleCsvImportSubmit"
        @cancel="handleCsvImportCancel"
      />
    </form>
  </div>
</template>

<script lang="ts">
import Vue, { PropType } from 'vue'
import { AppHeading, AppPager } from '~/components/atoms'
import { CreateBtn, CustomerTable, DataCounter } from '~/components/molecules'
import { CustomersSearch, CustomersCsvImport } from '~/components/organisms'
import recursiveToCamels from '~/utils/recursiveToCamels'
import { LaravelPaginator, Paginator } from '~/types/models'
import { SearchFormValues, SearchData } from '~/types/Customers'
import { FormOption } from '~/types/models'
import queryString from 'query-string'

interface Data {
  selected: string[]
  search: SearchData
  deleteId?: number
  isVisibleSearch: boolean
  isCsvImportVisible: boolean
}

const Customers = Vue.extend({
  components: { AppHeading, AppPager, CreateBtn, CustomerTable, DataCounter, CustomersSearch, CustomersCsvImport },
  props: {
    laravelPaginator: { type: Object as PropType<LaravelPaginator>, required: true },
    searchForm: { type: Object as PropType<SearchFormValues>, required: true },
    maxFileSize: { type: String },
    exportUrl: { type: String, required: true }
  },
  data(): Data {
    const searchQuery = this.searchForm.inputs
    return {
      selected: [],
      deleteId: null,
      search: {
        customerId: searchQuery.customer_id || '',
        email: searchQuery.email || '',
        name: searchQuery.name || '',
        kana: searchQuery.kana || '',
        prefectureId: searchQuery.prefecture_id || '',
        postcode: searchQuery.postcode || '',
        city: searchQuery.city || '',
        line1: searchQuery.line1 || '',
        sexes: searchQuery.sexes || [],
        tel: searchQuery.tel || '',
        fax: searchQuery.fax || '',
        birthFrom: searchQuery.birth_from || '',
        birthTo: searchQuery.birth_to || '',
        birthMonth: searchQuery.birth_month || '',
        company: searchQuery.company || '',
        unit: searchQuery.unit || '',
        posts: searchQuery.posts || [],
        jobs: searchQuery.jobs || [],
        articleTags: searchQuery.article_tags || [],
        createdAtFrom: searchQuery.created_at_from || '',
        createdAtTo: searchQuery.created_at_to || '',
        premiumDeadlineFrom: searchQuery.premium_deadline_from || '',
        premiumDeadlineTo: searchQuery.premium_deadline_to || '',
        lastLoginDateFrom: searchQuery.last_login_date_from || '',
        lastLoginDateTo: searchQuery.last_login_date_to || '',
        statuses: searchQuery.statuses || [],
        mailmagaFlgs: searchQuery.mailmaga_flgs || []
      },
      isVisibleSearch: false,
      isCsvImportVisible: false
    }
  },
  computed: {
    paginator(): Paginator {
      return recursiveToCamels(this.laravelPaginator) as Paginator
    },
    csrfToken(): string {
      return this.$store.getters.csrfToken
    },
    isEnableCsvDownload(): boolean {
      let isEnabled = false
      Object.keys(this.searchForm.inputs).forEach(key => {
        if (this.searchForm.inputs[key].length) {
          isEnabled = true
          return
        }
      })
      return isEnabled
    }
  },
  methods: {
    getFormOptions(name: string): FormOption[] {
      if (!(name in this.searchForm)) return []
      return Object.keys(this.searchForm[name]).map(key => ({
        id: Number(key),
        label: this.searchForm[name][key],
        value: key
      }))
    },
    handleDelete(id: number) {
      this.deleteId = id

      if (window.confirm('削除してもよろしいですか?')) {
        this.$nextTick(() => {
          ;(this.$refs.delete as HTMLFormElement).submit()
        })
      }
    },
    handlePager(page: number) {
      const oldQuery = this.$route.query
      delete oldQuery.page
      const params = queryString.stringify(oldQuery)
      const query = params ? `page=${page}&${params}` : `page=${page}`
      window.location.href = `/customers?${query}`
    },
    handleSearchInput(data) {
      this.search = data
      this.$nextTick(() => {
        ;(this.$refs.search as HTMLFormElement).submit()
      })
    },
    handleCsvImportSubmit() {
      window.removeEventListener('beforeunload', (this as any).handleBeforeUnload)
      ;(this.$refs.import as HTMLFormElement).submit()
    },
    handleCsvImportCancel() {
      this.isCsvImportVisible = false
    }
  }
})

export default Vue.component('page-admin-customers', Customers)
</script>

<style lang="scss">

</style>

CustomerCsvImport

<template>
  <FormDialog
    :is-visible="isVisible"
    @save="$emit('csv-import-button-submit')"
    @close="handleCancel"
    @cancel="handleCancel"
    title="CSVインポート"
    save-label="インポート"
    class="postCsvImport searchFields"
  >
    <div class="postCsvImport__content">
      <InputFile :value="files" name="csv" accept="text/csv" />
    </div>
    <template v-if="maxFileSize">
      <AppTexts class="mediaForm__notice">最大アップロードサイズ:{{ maxFileSize }}</AppTexts>
    </template>
  </FormDialog>
</template>

<script lang="ts">
import Vue from 'vue'
import SearchFields from './mixins/SearchFields.vue'
import { InputFile, AppTexts } from '~/components/atoms'
import { FormDialog } from '~/components/organisms'

interface Data {
  files: File[]
}

export default Vue.extend({
  components: { FormDialog, InputFile, AppTexts },
  mixins: [SearchFields],
  props: {
    maxFileSize: { type: String }
  },
  data(): Data {
    return {
      files: []
    }
  },
  methods: {
    handleCancel() {
      this.$emit('cancel')
    }
  }
})
</script>

InputFile

<template>
  <label
    :class="styledClasses"
    :style="styled"
    @dragover.prevent="checkDrag"
    @dragleave.prevent="checkDrag"
    @drop.prevent="handleDrop"
    class="inputFile"
  >
    <input ref="files" v-bind="$attrs" :accept="accept" @change="handleChange" type="file" class="inputFile__input" />

    <template v-if="!media">
      <span class="inputFile__btn">ファイルを選択</span>
      <span>{{ firstFileName }}</span>
    </template>

    <template v-if="isVisibleExtension">
      <span class="inputFile__extension">{{ extension }}</span>
    </template>
  </label>
</template>

<script lang="ts">
import Vue, { PropType } from 'vue'

interface Data {
  isDrag: boolean
  previewUrl: string
  firstFileName: string
}

export default Vue.extend({
  inheritAttrs: false,
  props: {
    value: { type: Array as PropType<File[]>, required: true },
    src: { type: String },
    accept: { type: String },
    media: { type: Boolean }
  },
  data(): Data {
    return {
      isDrag: false,
      previewUrl: '',
      firstFileName: ''
    }
  },
  computed: {
    styledClasses(): string[] {
      const classes: string[] = []

      if (this.media) classes.push('-media')

      return classes
    },
    styled(): { [key: string]: string } {
      const styled: { [key: string]: string } = {}

      styled['--bg'] = `url("${require('~/assets/img/default-upload.png')}")`

      if (this.src) {
        styled['--bg'] = `url(${this.src})`
      }

      if (this.value.length) {
        styled['--bg'] = `url(${this.previewUrl})`
      }

      return styled
    },
    extension(): string {
      if (this.value.length) {
        return this.value[0].name.replace(/^.+\.(.*?)$/, '$1')
      }

      if (this.src) {
        return this.src.replace(/^.+\.(.*?)$/, '$1')
      }

      return ''
    },
    acceptMimeTypes(): RegExp[] | null {
      if (!this.accept) return null

      const mimeTypes: RegExp[] = this.accept
        .replace(/\s+/, '')
        .split(',')
        .map(accept => {
          if (/^(.*?)\/\*$/.test(accept)) {
            return new RegExp(accept.replace(/^(.*?)\/\*$/, '$1\/.*$'))
          }
          return new RegExp(accept.replace(/^(.*?)\/(.*?)$/, '$1\/$2$'))
        })

      return mimeTypes
    },
    isVisibleExtension(): boolean {
      if (this.value.length) {
        return !/^.*?\.(jpe?g|png|gif|svg|webp|csv)$/.test(this.value[0].name)
      }

      if (this.src) {
        return !/^.*?\.(jpe?g|png|gif|svg|webp|csv)$/.test(this.src)
      }

      return false
    }
  },
  watch: {
    value(newValue: File[]) {
      if (!newValue.length) {
        this.previewUrl = ''
      } else {
        this.createPreviewUrl(newValue[0])
      }
    }
  },
  methods: {
    validation(fileList: FileList): boolean {
      if (!this.accept) return true
      const correctFiles: File[] = Array.from(fileList).filter(file => {
        for (const mimeType of this.acceptMimeTypes) {
          if (mimeType.test(file.type)) return true
        }
        return false
      })

      return correctFiles.length === fileList.length
    },
    createPreviewUrl(file: File) {
      const reader = new FileReader()
      reader.onload = (e: Event) => {
        this.previewUrl = (e.target as any).result
      }
      reader.readAsDataURL(file)
    },
    convertFileListToArray(fileList: FileList): File[] {
      const files: File[] = []

      for (let i = 0, length = fileList.length; i < length; i++) {
        files.push(fileList[i])
      }

      return files
    },
    checkDrag(e: DragEvent) {
      if (e.dataTransfer.types.find(type => type === 'text/plain')) return
      this.isDrag = e.type === 'dragover'
    },
    handleChange(e: Event) {
      const fileList: FileList = (e.target as HTMLInputElement).files
      // 複数選択の場合は一番最初のファイル名だけを表示
      if (!this.media && fileList) this.firstFileName = fileList[0].name
      this.$emit('change', this.convertFileListToArray(fileList))
    },
    handleDrop(e: DragEvent) {
      if (!this.isDrag) return
      const files: FileList = e.dataTransfer.files

      if (!this.validation(files)) return
      this.$emit('change', this.convertFileListToArray(files))
      this.isDrag = false
      ;(this.$refs.files as HTMLInputElement).files = files
    }
  }
})
</script>

<style lang="scss" scoped>

</style>

コメント

タイトルとURLをコピーしました