Загрузка и управление файлами с Laravel и Vue

Что мы строим

На этот раз мы напишем просто сервис, который позволит загружать и управлять файлами. Итоговый пример:

Подготовка

Для начала измените эти директивы в php.ini:

upload_max_filesize = 200m
post_max_size = 250m

И добавьте эти зависимости в package.json:

"devDependencies": {
  "axios": "^0.17",
  "bootstrap-sass": "^3.3.7",
  "cross-env": "^5.1",
  "jquery": "^3.2",
  "laravel-mix": "^1.0",
  "lodash": "^4.17.4",
  "vue": "^2.5.7"
},
"dependencies": {
  font-awesome": "^4.7.0"
}

А так же измените webpack.min.js:

let mix = require('laravel-mix');
mix.options({
  processCssUrls: false
});
mix.js('resources/assets/js/app.js', 'public/js')
  .sass('resources/assets/sass/app.scss', 'public/css')
  .sass('resources/assets/sass/font-awesome.scss', 'public/css');

Настроте пути для font-awesome.scss и подключите их:

// Fonts
$fa-font-path: "../fonts/vendor/font-awesome";
@import '~font-awesome/scss/font-awesome.scss';

Так же импортируйте bulma в app.scss:

// Bulma
@import "~bulma/bulma.sass";

Остальные стили вы можете взять на Github.

Внимание!

Вы можете столкнуться с трудностями если вы используете MariaDB в качестве базы данных. Чтобы избежать проблем, добавьте в AppServiceProvider код:

public function boot()
{
  Schema::defaultStringLength(191);
}

И пока не забыли, нужно создать symlink на папку storage/app/publiс/, сделать это можно с помощью команды:

php artisan storage:link

Теперь вы можете использовать хелпер assets для генерации URL:

echo asset('storage/file.txt');

Мы будем использовать дефолтовую авторизацию Laravel, так что выполните команду:

php artisan make:auth

Маршруты

Добавьте эти строки в самый конец файла web.php:

Route::get('/', '[email protected]')->name('home')->middleware('auth');
Route::get('/files/{type}/{id?}', '[email protected]');

Route::post('files/add', '[email protected]');
Route::post('files/edit/{id}', '[email protected]');
Route::post('files/delete/{id}', '[email protected]');

Auth::routes();

База данных

Создайте модель, контроллер и миграцию с помощью команды:

php artisan make:model File -mc

Оставьте create_users_table и create_passwords_table миграции без изменений. И измените миграцию create_files_table:

public function up()
{
  Schema::create('files', function (Blueprint $table) {
    $table->increments('id');
    $table->string('name')->unique();
    $table->string('type');
    $table->string('extension');
    $table->integer('user_id')->unsigned();
    $table->timestamps();

    $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
  });
}

Измените файл модели File.php:

namespace App;

use Illuminate\Database\Eloquent\Model;

class File extends Model
{

  protected $fillable = [
    'name', 'type', 'extension', 'user_id'
  ];


  public function user()
  {
    return $this->belongsTo(User::class);
  }
}

Контроллер

У нас есть 2 контроллера - MainController:

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class MainController extends Controller
{
  public function index()
  {
    return view('main');
  }
}

и FileController:

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage;
use App;
use App\File;

class FileController extends Controller
{
  private $image_ext = ['jpg', 'jpeg', 'png', 'gif'];
  private $audio_ext = ['mp3', 'ogg', 'mpga'];
  private $video_ext = ['mp4', 'mpeg'];
  private $document_ext = ['doc', 'docx', 'pdf', 'odt'];

  /**
   * Constructor
   */
  public function __construct()
  {
    $this->middleware('auth');
  }

  /**
   * Fetch files by Type or Id
   * @param string $type File type
   * @param integer $id File Id
   * @return object    Files list, JSON
   */
  public function index($type, $id = null)
  {
    $model = new File();

    if (!is_null($id)) {
      $response = $model::findOrFail($id);
    } else {
      $records_per_page = ($type == 'video') ? 6 : 15;

      $files = $model::where('type', $type)
              ->where('user_id', Auth::id())
              ->orderBy('id', 'desc')->paginate($records_per_page);

      $response = [
        'pagination' => [
          'total' => $files->total(),
          'per_page' => $files->perPage(),
          'current_page' => $files->currentPage(),
          'last_page' => $files->lastPage(),
          'from' => $files->firstItem(),
          'to' => $files->lastItem()
        ],
        'data' => $files
      ];
    }

    return response()->json($response);
  }

  /**
   * Upload new file and store it
   * @param Request $request Request with form data: filename and file info
   * @return boolean     True if success, otherwise - false
   */
  public function store(Request $request)
  {
    $max_size = (int)ini_get('upload_max_filesize') * 1000;
    $all_ext = implode(',', $this->allExtensions());

    $this->validate($request, [
      'name' => 'required|unique:files',
      'file' => 'required|file|mimes:' . $all_ext . '|max:' . $max_size
    ]);

    $model = new File();

    $file = $request->file('file');
    $ext = $file->getClientOriginalExtension();
    $type = $this->getType($ext);

    if (Storage::putFileAs('/public/' . $this->getUserDir() . '/' . $type . '/', $file, $request['name'] . '.' . $ext)) {
      return $model::create([
          'name' => $request['name'],
          'type' => $type,
          'extension' => $ext,
          'user_id' => Auth::id()
        ]);
    }

    return response()->json(false);
  }

  /**
   * Edit specific file
   * @param integer $id   File Id
   * @param Request $request Request with form data: filename
   * @return boolean     True if success, otherwise - false
   */
  public function edit($id, Request $request)
  {
    $file = File::where('id', $id)->where('user_id', Auth::id())->first();

    if ($file->name == $request['name']) {
      return response()->json(false);
    }

    $this->validate($request, [
      'name' => 'required|unique:files'
    ]);

    $old_filename = '/public/' . $this->getUserDir() . '/' . $file->type . '/' . $file->name . '.' . $file->extension;
    $new_filename = '/public/' . $this->getUserDir() . '/' . $request['type'] . '/' . $request['name'] . '.' . $request['extension'];

    if (Storage::disk('local')->exists($old_filename)) {
      if (Storage::disk('local')->move($old_filename, $new_filename)) {
        $file->name = $request['name'];
        return response()->json($file->save());
      }
    }

    return response()->json(false);
  }


  /**
   * Delete file from disk and database
   * @param integer $id File Id
   * @return boolean   True if success, otherwise - false
   */
  public function destroy($id)
  {
    $file = File::findOrFail($id);

    if (Storage::disk('local')->exists('/public/' . $this->getUserDir() . '/' . $file->type . '/' . $file->name . '.' . $file->extension)) {
      if (Storage::disk('local')->delete('/public/' . $this->getUserDir() . '/' . $file->type . '/' . $file->name . '.' . $file->extension)) {
        return response()->json($file->delete());
      }
    }

    return response()->json(false);
  }


  /**
   * Get type by extension
   * @param string $ext Specific extension
   * @return string   Type
   */
  private function getType($ext)
  {
    if (in_array($ext, $this->image_ext)) {
      return 'image';
    }

    if (in_array($ext, $this->audio_ext)) {
      return 'audio';
    }

    if (in_array($ext, $this->video_ext)) {
      return 'video';
    }

    if (in_array($ext, $this->document_ext)) {
      return 'document';
    }
  }

  /**
   * Get all extensions
   * @return array Extensions of all file types
   */
  private function allExtensions()
  {
    return array_merge($this->image_ext, $this->audio_ext, $this->video_ext, $this->document_ext);
  }

  /**
   * Get directory for the specific user
   * @return string Specific user directory
   */
  private function getUserDir()
  {
    return Auth::user()->name . '_' . Auth::id();
  }
}

В этом контроллере есть четыре массива с расширениями файлов, которые используются для валидации. Вы можете расширить этот список.

В конструкторе мы определяем middleware для этого контроллера.

В методе index мы вытаскиваем все файлы с определенным типом и ID пользователя используя метод paginate. Затем подготавливаем и возвращаем ответ в JSON формате:

$response = [
  'pagination' => [
    'total' => $files->total(),
    'per_page' => $files->perPage(),
    'current_page' => $files->currentPage(),
    'last_page' => $files->lastPage(),
    'from' => $files->firstItem(),
    'to' => $files->lastItem()
  ],
  'data' => $files
];

return response()->json($response);

В методе store, мы загружаем файл и создаем новую запись в базе данных для этого файла. Но сначала мы задачем переменные $max_size и $all_ext для валидации размера файла и его типа:

$max_size = (int)ini_get('upload_max_filesize') * 1000;
$all_ext = implode(',', $this->allExtensions());

Правила валидации достаточно простые. Поле name - обязательно и уникально. Поле file - должно быть файлом, файл должен иметь разрешенный тип, и не должен превышать $max_size размер:

$this->validate($request, [
  'name' => 'required|unique:files',
  'file' => 'required|file|mimes:' . $all_ext . '|max:' . $max_size
]);

Далее мы получаем информацию о файле (тип и расширение):

$file = $request->file('file'); $ext = $file->getClientOriginalExtension();
$type = $this->getType($ext);

И после всего, мы можем загрузить файл (используя File Storage) в персональную директорию пользователя (storage/public/{user_name}/{file_type}/{file_name}). Если все прошло успешно - мы создаем запись в базе данных:

if (Storage::putFileAs('/public/' . $this=>getUserDir() . '/' . $type . '/', $file, $request['name'] . '.' . $ext)) {
  return $model::create([
    'name' => $request['name'],
    'type' => $type,
    'extension' => $ext,
    'user_id' => Auth::id()
  ]);
}

Вы можете изменить имя файла в методе edit. Новое имя должно отличаться от старого, и должно быть уникально. Далее мы проверяем существует ли файл с таким именем, и если да, то переименовываем его и обновляем в базе данных:

if (Storage::disk('local')->exists($old_filename)) {
  if (Storage::disk('local')->move($old_filename, $new_filename)) {
    $file->name = $request['name'];
    return response()->json($file->save());
  }
}

Остальные методы достаточно просты, так что вы можете разобрать их самостоятельно.

Шаблоны

У нас есть всего несколько шаблонов: главный, авторизация и layouts.

Layouts

Главный layout app.blade.php:

<!DOCTYPE html>
<html lang="{{ app()->getLocale() }}">
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">

  <!-- CSRF Token -->
  <meta name="csrf-token" content="{{ csrf_token() }}">
  <script>
    window.Laravel = { csrfToken: '{{ csrf_token() }}' }
  </script>

  <title>{{ config('app.name', 'Laravel') }}</title>

  <!-- Fonts -->
  <link rel="stylesheet" type="text/css" href="{{ asset('css/font-awesome.css') }}">

  <!-- Styles -->
  <link href="{{ asset('css/app.css') }}" rel="stylesheet">
</head>
<body>
  <div id="app">
    <nav class="navbar is-info">
      <div class="navbar-brand">
        <a class="navbar-item" href="{{ url('/') }}">
          <img src="https://bulma.io/images/bulma-logo-white.png" alt="File Hosting" width="112" height="28">
        </a>
      </div>
      <div class="navbar-menu">
        <div class="navbar-start">
          <a class="navbar-item" href="{{ url('/') }}">
            Home
          </a>
        </div>
        <div class="navbar-end">
          @guest
            <a class="navbar-item" href="{{ route('login') }}">Login</a>
            <a class="navbar-item" href="{{ route('register') }}">Register</a>
          @else
            <div class="navbar-item has-dropdown is-hoverable">
              <a class="navbar-link">
                <span class="fa fa-user-o"></span>  
                {{ Auth::user()->name }}
              </a>

              <div class="navbar-dropdown is-right">
                  <a class="navbar-item" href="{{ route('logout') }}"
                    onclick="event.preventDefault();
                         document.getElementById('logout-form').submit();">
                    Logout
                  </a>

                  <form id="logout-form" action="{{ route('logout') }}" method="POST" style="display: none;">
                    {{ csrf_field() }}
                  </form>
              </div>
            </div>
          @endguest
        </div>
      </div>
    </nav>

    @if(Auth::check())
      @include('layouts.notification')
      @include('layouts.file-form')
      @include('layouts.confirm')
      @include('layouts.modal')
    @endif

    @yield('content')
  </div>

  <!-- Scripts -->
  <script src="{{ asset('js/app.js') }}"></script>
</body>
</html>

Как вы заметили, в этом файле мы подключаем дополнительные файлы: нотификации, форма, и другие. Эти файлы подключаются только если пользователь авторизован.

file-form.blade.php:

<div class="container is-fluid box">
  <div class="new-file">
    <form id="new-file-form" action="#" method="#" @submit.prevent="submitForm">
      <div class="field is-grouped">
        <p class="control is-expanded">
          <input class="input" type="text" name="name" placeholder="File name" v-model="fileName" required>
        </p>
        <div class="file is-info has-name">
          <label class="file-label">
            <input class="file-input" type="file" ref="file" name="file" @change="addFile()">
            <span class="file-cta">
              <span class="file-icon">
                <i class="fa fa-upload"></i>
              </span>
              <span class="file-label">
                Upload new file
              </span>
            </span>
            <span class="file-name" v-if="attachment.name" v-html="attachment.name"></span>
          </label>
        </div>
        <p class="control">
          <button type="submit" class="button is-primary">
            Add new file
          </button>
        </p>
      </div>
    </form>
  </div>
</div>

В этом layout содержится форма для загрузки файлов. Вот несколько ключевых моментов:

При сабмите формы вызывается метод SubmitForm:

<form id="new-file-form" @submit.prevent="submitForm">

Задаем v-model для имени файла:

<input class="input" type="text" name="name" placeholder="File name" v-model="fileName" required>

К полю с типом "file" добавляем аттрибут ref, который позволит обрщаться к этому html элементу из Vue. Так же добавляем аттрибут @change для обработки одноименного события:

<input class="file-input" type="file" ref="file" name="file" @change="addFile()">

notification.blade.php:

<transition name="fade">
  <div class="container is-fluid" id="message" v-show="notification">

    <div class="notification is-success" v-cloak v-if="!anyError()">
      <button class="delete" @click="notification=false"></button>
      <h1 class="subtitle">
        @{{ message }}
      </h1>
    </div>

    <div class="notification is-danger" v-cloak v-if="anyError()">
      <button class="delete" @click="notification=false"></button>

      <h1 class="subtitle">
        @{{ message }}
      </h1>

      <div class="content">
        <ul v-for="error in errors">
          <li v-for="error_item in error">
            @{{ error_item }}
          </li>
        </ul>
      </div>
    </div>

  </div>
</transition>

Тут мы используем "переход" (transition) с именем "fade" (читайте подробнее тут).

modal.blade.php:

<div class="modal" :class="{'is-active' : modalActive}">
  <div class="modal-background" @click="closeModal()"></div>
    <div class="modal-content">
      <p class="image is-4by3">
        <img src="" :src="'{{ asset('storage/' . Auth::user()->name . '_' . Auth::id()) }}' + '/' + file.type + '/' + file.name + '.' + file.extension" :alt="file.name">
      </p>
    </div>
  <button class="modal-close is-large" aria-label="close" @click="closeModal()"></button>
</div>

confirm.blade.php:

<transition name="modal">
  <div class="modal-mask" v-if="showConfirm" v-cloak>
    <div class="modal-wrapper">
      <div class="modal-container">

        <div class="modal-body">
          <h2>Are you sure?</h2>
        </div>

        <div class="modal-footer">
          <button class="button" @click="deleteFile()">
            Confirm
          </button>
          <button class="button" @click="cancelDeleting()">
            Cancel
          </button>
        </div>
      </div>
    </div>
  </div>
</transition>

Авторизация

Для авторизации мы заменим дефолтовые bootstrap шаблоны, на шаблоны Bulma. Скачать эти шаблоны можно тут.

Главный шаблон

Главный шаблон находится в файле main.blade.php, который использует файл app.blade.php в качестве layout. Содержимое main.blade.php:

@extends('layouts.app')

@section('content')
  <div class="container is-fluid box">
    <div class="tabs is-centered is-large">
      <ul>
        <li :class="{'is-active': isActive('image')}" @click="getFiles('image')">
          <a>
            <span class="icon is-small"><i class="fa fa-image"></i></span>
            <span>Pictures</span>
          </a>
        </li>
        <li :class="{'is-active': isActive('audio')}" @click="getFiles('audio')">
         <a>
            <span class="icon is-small"><i class="fa fa-music"></i></span>
            <span>Music</span>
          </a>
        </li>
        <li :class="{'is-active': isActive('video')}" @click="getFiles('video')">
          <a>
            <span class="icon is-small"><i class="fa fa-film"></i></span>
            <span>Videos</span>
          </a>
        </li>
        <li :class="{'is-active': isActive('document')}" @click="getFiles('document')">
          <a>
            <span class="icon is-small"><i class="fa fa-file-text-o"></i></span>
            <span>Documents</span>
          </a>
        </li>
      </ul>
    </div>

    <div class="tabs-details">
      <div class="columns is-multiline">

        <div class="is-empty column is-4 is-offset-4" v-if="pagination.total == 0" v-cloak>
          <figure>
            <img src="{{ asset('images/folder_empty.png') }}" alt="Folder empty" id="folder_empty">
            <figcaption>
              <p class="title is-2">
                This folder is empty!
              </p>
            </figcaption>
          </figure>
        </div>

        <div class="loading column is-4 is-offset-4" v-if="loading">
          <i class="fa fa-spinner fa-pulse fa-3x fa-fw"></i>
          <span class="sr-only">Loading...</span>
        </div>

        <div class="column " :class="isVideo ? 'is-half' : 'is-one-fifth'" v-for="file in files" v-cloak>
          <div class="card " :class="file.type == 'image' ? 'is-image' : ''">
            <div class="card-image">
              <button class="delete delete-file" title="Delete" @click="prepareToDelete(file)"></button>
              <figure class="image is-4by3" v-if="file.type == 'image'" @click="showModal(file)">
                <img src="" :src="'{{ asset('storage/' . Auth::user()->name . '_' . Auth::id()) }}' + '/' + file.type + '/' + file.name + '.' + file.extension" :alt="file.name">
              </figure>

              <div v-if="file.type == 'audio'">
                <figure class="image is-4by3">
                  <img src="{{ asset('images/music.png') }}" alt="Audio image" id="audio_image">
                </figure>
                <audio controls>
                  <source src="" :src="'{{ asset('storage/' . Auth::user()->name . '_' . Auth::id()) }}' + '/' + file.type + '/' + file.name + '.' + file.extension" :type="'audio/' + file.extension">
                  Your browser does not support the audio tag.
                </audio>
              </div>

              <div v-if="file.type == 'video'" class="video_block">
                <video controls>
                  <source src="" :src="'{{ asset('storage/' . Auth::user()->name . '_' . Auth::id()) }}' + '/' + file.type + '/' + file.name + '.' + file.extension" :type="'video/' + file.extension">
                  Your browser does not support the video tag.
                </video>
              </div>

              <div v-if="file.type == 'document'" class="document_block">
                <figure class="image is-4by3">
                  <img src="{{ asset('images/document.png') }}" alt="Audio image" id="audio_image">
                </figure>
                <a class="button is-primary" href="" :href="'{{ asset('storage/' . Auth::user()->name . '_' . Auth::id()) }}' + '/' + file.type + '/' + file.name + '.' + file.extension" target="_blank">
                  <i class="fa fa-download" aria-hidden="true"></i>
                   Download
                </a>
              </div>
           </div>
           <div class="card-content">
              <div class="content">
                <p v-if="file !== editingFile" @dblclick="editFile(file)" :title="'Double click for editing filename'">
                  @{{ file.name + '.' + file.extension}}
                </p>
                <input class="input" v-if="file === editingFile" v-autofocus @keyup.enter="endEditing(file)" @blur="endEditing(file)" type="text" :placeholder="file.name" v-model="file.name">
                <time datetime="2016-1-1">@{{ file.created_at }}</time>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>

    <nav class="pagination is-centered" role="navigation" aria-label="pagination" v-if="pagination.last_page > 1" v-cloak>
      <a class="pagination-previous" @click.prevent="changePage(1)" :disabled="pagination.current_page <= 1">First page</a>
      <a class="pagination-previous" @click.prevent="changePage(pagination.current_page - 1)" :disabled="pagination.current_page <= 1">Previous</a>
      <a class="pagination-next" @click.prevent="changePage(pagination.current_page + 1)" :disabled="pagination.current_page >= pagination.last_page">Next page</a>
      <a class="pagination-next" @click.prevent="changePage(pagination.last_page)" :disabled="pagination.current_page >= pagination.last_page">Last page</a>
      <ul class="pagination-list">
        <li v-for="page in pages">
          <a class="pagination-link" :class="isCurrentPage(page) ? 'is-current' : ''" @click.prevent="changePage(page)">
            @{{ page }}
          </a>
        </li>
      </ul>
    </nav>
  </div>
@endsection

Assets

О стилях мы писали выше, теперь поговорим о Javacript. Файл app.js:

window.Vue = require('vue');
window.axios = require('axios');

let token = document.head.querySelector('meta[name="csrf-token"]');

if (token) {
  window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content;
} else {
  console.error('CSRF token not found: https://laravel.com/docs/csrf#csrf-x-csrf-token');
}

const app = new Vue({
  el: '#app',

  directives: {
      'autofocus': {
        inserted(el) {
          el.focus();
        }
      }
  },

  data: {
    files: {},
    file: {},

    pagination: {},
    offset: 5,

    activeTab: 'image',
    isVideo: false,
    loading: false,

    formData: {},
    fileName: '',
    attachment: '',

    editingFile: {},
    deletingFile: {},

    notification: false,
    showConfirm: false,
    modalActive: false,
    message: '',
    errors: {}
  },

  methods: {
    isActive(tabItem) {
      return this.activeTab === tabItem;
    },

    setActive(tabItem) {
      this.activeTab = tabItem;
    },

    isCurrentPage(page) {
      return this.pagination.current_page === page;
    },

    fetchFile(type, page) {
      this.loading = true;
      axios.get('files/' + type + '?page=' + page).then(result => {
        this.loading = false;
        this.files = result.data.data.data;
        this.pagination = result.data.pagination;
      }).catch(error => {
        console.log(error);
        this.loading = false;
      });

    },

    getFiles(type) {
      this.setActive(type);
      this.fetchFile(type);

      if (this.activeTab === 'video') {
        this.isVideo = true;
      } else {
        this.isVideo = false;
      }
    },

    submitForm() {
      this.formData = new FormData();
      this.formData.append('name', this.fileName);
      this.formData.append('file', this.attachment);

      axios.post('files/add', this.formData, {headers: {'Content-Type': 'multipart/form-data'}})
        .then(response => {
          this.resetForm();
          this.showNotification('File successfully upload!', true);
          this.fetchFile(this.activeTab);
        })
        .catch(error => {
          this.errors = error.response.data.errors;
          this.showNotification(error.response.data.message, false);
          this.fetchFile(this.activeTab);
        });
    },

    addFile() {
      this.attachment = this.$refs.file.files[0];
    },

    prepareToDelete(file) {
      this.deletingFile = file;
      this.showConfirm = true;
    },

    cancelDeleting() {
      this.deletingFile = {};
      this.showConfirm = false;
    },

    deleteFile() {
      axios.post('files/delete/' + this.deletingFile.id)
        .then(response => {
          this.showNotification('File successfully deleted!', true);
          this.fetchFile(this.activeTab, this.pagination.current_page);
        })
        .catch(error => {
          this.errors = error.response.data.errors();
          this.showNotification('Something went wrong! Please try again later.', false);
          this.fetchFile(this.activeTab, this.pagination.current_page);
        });

      this.cancelDeleting();
    },

    editFile(file) {
      this.editingFile = file;
    },

    endEditing(file) {
      this.editingFile = {};

      if (file.name.trim() === '') {
        alert('Filename cannot be empty!');
        this.fetchFile(this.activeTab);
      } else {
        let formData = new FormData();
        formData.append('name', file.name);
        formData.append('type', file.type);
        formData.append('extension', file.extension);

        axios.post('files/edit/' + file.id, formData)
          .then(response => {
            if (response.data === true) {
              this.showNotification('Filename successfully changed!', true);

              var src = document.querySelector('[alt="' + file.name +'"]').getAttribute("src");
              document.querySelector('[alt="' + file.name +'"]').setAttribute('src', src);
            }
          })
          .catch(error => {
            this.errors = error.response.data.errors;
            this.showNotification(error.response.data.message, false);
          });

        this.fetchFile(this.activeTab, this.pagination.current_page);
      }
    },

    showNotification(text, success) {
      if (success === true) {
        this.clearErrors();
      }

      var application = this;
      application.message = text;
      application.notification = true;
      setTimeout(function() {
        application.notification = false;
      }, 15000);
    },

    showModal(file) {
      this.file = file;
      this.modalActive = true;
    },

    closeModal() {
      this.modalActive = false;
      this.file = {};
    },

    changePage(page) {
      if (page > this.pagination.last_page) {
        page = this.pagination.last_page;
      }
      this.pagination.current_page = page;
      this.fetchFile(this.activeTab, page);
    },

    resetForm() {
      this.formData = {};
      this.fileName = '';
      this.attachment = '';
    },

    anyError() {
      return Object.keys(this.errors).length > 0;
    },

    clearErrors() {
      this.errors = {};
    }
  },

  mounted() {
    this.fetchFile(this.activeTab, this.pagination.current_page);
  },

  computed: {
    pages() {
      let pages = [];

      let from = this.pagination.current_page - Math.floor(this.offset / 2);

      if (from < 1) {
        from = 1;
      }

      let to = from + this.offset - 1;

      if (to > this.pagination.last_page) {
        to = this.pagination.last_page;
      }

      while (from <= to) {
        pages.push(from);
        from++;
      }

      return pages;
    }
  }
});

Тут нет ничего сложного, думаю вы сами разберетесь что к чему.

Заключение

Теперь у нас есть готовый сервис, который может загружать и управлять файлами различных типов. Это могут быть картинки, видео, аудио и документы. Наша система автоматически распознает тип файла и загружает его в отведенную ему папку.

Так же у нас есть различные кастомные компоненты. Например модальное окно для отображения картинок:

Модальное окно

Нотификации:

Success notification

Error notification

Кастомное окно для подтверждения действий:

Кастомный alert css html

И пагинация:

Пагинация

Конечно, тут осталось еще много работы. Но первый шаг сделан, и дальнейший путь вы можете проделать сами.

Весь исходный код этого проекта вы можете найти на Github. Если у вас остались какие-то вопросы, вы можете оставлять их в комментариях. Спасибо за чтение!

Это перевод статьи за авторством fl2top. 

Опубликовано:

Категории: Статьи