Laravel mix vue No.12 - Web Application - 画像ファイルの一覧と詳細

こんにちは、あすかのkoheiです。

今回はAWS S3にアップしたファイルの一覧と詳細を作ります。

連載記事

AWS S3 - AWS S3 に写真をアップロード

サンプル


フォルダ構成


└─ server
   ├─ app
   |  └─ Http
   |       └─ Controllers
   |          └─ PhotoController.php
   ├─ routes
   |  └─ api.php
   └─ resources 
      └─ js
         ├─ components
+        |  ├─ Photo.vue
+        |  └─ Pagination.vue
         ├─ pages
+        |  ├─ PhotoDetail.vue
         |  └─ Home.vue
         └─ router.js

dockerスタート

gitからクローンした場合は.envの作成と設定を忘れないように!

# コンテナスタート
docker-compose start

# コンテナに入る
docker-compose exec php bash

# composerをインストール(前回からの続きで行う場合はいらない)
composer install

# npmをインストール(前回からの続きで行う場合はいらない)
npm i

# encryption keyを作成(前回からの続きで行う場合はいらない)
php artisan key:generate

# ホットリリード開始
npm run watch

ルートを追加

Apiのルートを追加

server\routes\api.phpを編集


    ...

        // 写真投稿
        Route::post('/photos', 'PhotoController@create')->name('photo.create');

+       // 写真一覧
+       Route::get('/photos', 'PhotoController@index')->name('photo.index');

+       // 写真詳細
+       Route::get('/photos/{id}', 'PhotoController@show')->name('photo.show');

    ...

Vueのルートを追加

server\resources\js\router.jsを編集


    ...

    {
        // urlのパス
        path: "/",
        // ルートネーム
        name: 'home',
        // インポートしたページ
        component: Home,
        // https://router.vuejs.org/ja/guide/essentials/passing-props.html
        // クエリストリングをプロップスとして返す
        props: route => {
            const page = route.query.page;
            // 整数と解釈されない値は「1」を返却
            return {
                page: /^[1-9][0-9]*$/.test(page) ? page * 1 : 1
            };
        }
    },
+   {
+       path: "/photos/:id",
+       component: PhotoDetail,
+       // props: true は :id を props として受け取ることを意味
+       // props を true に設定しているので、この :id の値が <PhotoDetail> コンポーネントに props として渡される
+       // リンクは「{name: 'photo', params: { id: item.id }}」このように渡す
+       props: true
+   },

    ...

フォトコントローラに写真一覧と写真詳細を追加

server\app\Http\Controllers\PhotoController.phpを編集


    ...

    class PhotoController extends Controller
    {
        public function __construct()
        {
            // 認証が必要
            $this->middleware('auth')
                ->except(['index', 'download', 'show']); // 認証を除外するアクション
        }

+       /**
+        * 写真一覧
+        */
+       public function index()
+       {
+           // get の代わりに paginate を使うことで、
+           // JSON レスポンスでも示した total(総ページ数)や current_page(現在のページ)といった情報が自動的に追加される
+           $photos = Photo::with(['user'])
+               ->orderBy(Photo::CREATED_AT, 'desc')
+               ->paginate();
+
+           return $photos;
+       }

+       /**
+        * 写真詳細
+        * @param string $id
+        * @return Photo
+        */
+       public function show(string $id)
+       {
+           // get photo
+           // 'comments.author'でcommentsと、そのbelongsToに設定されている'author'も取得
+           $photo = Photo::with(['user'])->findOrFail($id);
+           return $photo;
+       }

        ...
    }

Vue側で「HOME」に一覧を作成

Photoコンポーネントを作る

画像1つを表示させるコンポーネントをserver\resources\js\components\Photo.vueを作成


    <template>
        <div class="photo">

            <RouterLink
                class="photo__overlay"
                :to="{ name: 'photo', params: { id: item.id } }"
                :title="`View the photo by ${item.user.name}`"
            >
                <!-- 画像 -->
                <figure class="image-wrap">
                    <img
                        class="image-wrap__image"
                        :src="item.url"
                        :alt="`Photo by ${item.user.name}`"
                    />
                </figure>
                <!-- 画像 -->

                <!-- photo username -->
                <div class="photo__username">{{ item.user.name }}</div>
                <!-- /photo username -->
            </RouterLink>

        </div>
    </template>

    <script>
    export default {
        // プロップスとして画像オブジェクトをもらう
        props: {
            item: {
                type: Object,
                required: true
            }
        },
    };
    </script>

ページネーションのコンポーネントを作成

画像を一定量表示するのでページネーションのコンポーネントserver\resources\js\components\Pagination.vueを作成


    <template>
        <div class="pagination">
            <!-- 前へのリンク -->
            <!-- urlを引き継ぐので以下のように設定 -->
            <!-- 現在のページはクエリストリング「page」で指定 -->
            <RouterLink
                v-if="! isFirstPage"
                :to="`/?page=${currentPage - 1}`"
                class="button"
            >« prev</RouterLink>
            <!-- 次へのリンク -->
            <RouterLink
                v-if="! isLastPage"
                :to="`/?page=${currentPage + 1}`"
                class="button"
            >next »</RouterLink>
        </div>
    </template>

    <script>
    export default {
        // プロップスとして現在のページとラストページをもらう
        props: {
            currentPage: {
                type: Number,
                required: true
            },
            lastPage: {
                type: Number,
                required: true
            }
        },
        computed: {
            // クエリストリングで渡されたページが最初のページかチェック
            isFirstPage() {
                return this.currentPage === 1;
            },
            // クエリストリングで渡されたページが最後のページかチェック
            isLastPage() {
                return this.currentPage === this.lastPage;
            }
        }
    };
    </script>

Photoコンポーネントをつかって一覧の表示

server\resources\js\pages\Home.vueを編集


    <template>
        <div class="page">

          <h1>{{ $t('word.home') }}</h1>

+         <div class="photo-list">
+             <div class="grid">
+                 <Photo
+                     class="grid__item"
+                     v-for="photo in photos"
+                     :key="photo.id"
+                     :item="photo"
+                 />
+             </div>
+             <Pagination :current-page="currentPage" :last-page="lastPage" />
+           </div>
+       </div>
    </template>

+   <script>
+   import { OK } from "../const";
+   // コンポーネントをインポート
+   import Photo from "../components/Photo.vue";
+   import Pagination from "../components/Pagination.vue";
+
+   export default {
+       // コンポーネントを登録
+       components: {
+           Photo,
+           Pagination
+       },
+       // プロップスとして表示するページをもらう
+       props: {
+           page: {
+               type: Number,
+               required: false,
+               default: 1
+           }
+       },
+       data() {
+           return {
+               // 表示する画像の配列
+               photos: [],
+               // 現在のページ
+               currentPage: 0,
+               // 最後のページ
+               lastPage: 0
+           };
+       },
+       methods: {
+           /**
+            * fetch photos
+            * 表示する画像を取得
+            */
+           async fetchPhotos() {
+               // request api
+               const response = await axios.get(`photos/?page=${this.page}`);
+
+               if (response.status !== OK) {
+                   // commit error store
+                   this.$store.commit("error/setCode", response.status);
+
+                   return false;
+               }
+
+               // 画像をデータにセット
+               this.photos = response.data.data;
+               // 現在のページをセット
+               this.currentPage = response.data.current_page;
+               // 最後のページをデータにセット
+               this.lastPage = response.data.last_page;
+           },
+       },
+       watch: {
+           // ページネーションで使い回すので $route の監視ハンドラ内で fetchPhotos を実行
+           $route: {
+               async handler() {
+                   await this.fetchPhotos();
+               },
+               // コンポーネントが生成されたタイミングでも実行
+               immediate: true
+           }
+       }
+   };
+   </script>

詳細ページの追加

server\resources\js\pages\PhotoDetail.vueを作成


<template>
    <!-- photo-detail -->
    <div
        v-if="photo"
        class="photo-detail"
        :class="{ 'photo-detail--column': fullWidth }"
    >
        <!-- pane image -->
        <figure
            class="photo-detail__pane photo-detail__image"
            @click="fullWidth = !fullWidth"
        >
            <img :src="photo.url" alt />
            <figcaption>Posted by {{ photo.user.name }}</figcaption>
        </figure>
        <!-- /pane image -->
    </div>
    <!-- /photo-detail -->
</template>

<script>
import { OK, CREATED, UNPROCESSABLE_ENTITY } from "../const";

export default {
    props: {
        // routeで設定された :id の値が入る
        id: {
            type: String,
            required: true,
        },
    },
    data() {
        return {
            loading: false,
            photo: null,
            fullWidth: false,
        };
    },
    computed: {
        isLogin() {
            return this.$store.getters["auth/check"];
        },
    },
    methods: {
        async fetchPhoto() {
            // api request
            const response = await axios.get(`photos/${this.id}`);

            // error
            if (response.status !== OK) {
                // commit errer store
                this.$store.commit("error/setCode", response.status);
                return false;
            }

            // set photo data
            this.photo = response.data;
        },
    },
    watch: {
        // 詳細ページで使い回すので $route の監視ハンドラ内で fetchPhoto を実行
        $route: {
            async handler() {
                await this.fetchPhoto();
            },
            // コンポーネントが生成されたタイミングでも実行
            immediate: true,
        },
    },
};
</script>

その他気になったところ修正

クラスつけわすれ


    <template>
-      <div>
+      <div class="wrapper">

Illuminate\Http\Requestをuse

-   Route::get('/refresh-token', function (Illuminate\Http\Request $request) {
+   Route::get('/refresh-token', function (Request $request) {
            $request->session()->regenerateToken();
            return response()->json();
        })->name('refresh-token');

        // 写真のアップロード
        Route::post('/photos', 'PhotoController@store')->name('photo.store');
    });

    // set lang
-   Route::get('/set-lang/{lang}', function (Illuminate\Http\Request $request, $lang) {
+   Route::get('/set-lang/{lang}', function (Request $request, $lang) {
        $request->session()->put('language', $lang);
        return response()->json();
    })->name('set-lang');

次は今回作ったものにライクボタンとダウンロードボタンを追加していこうと思います。

Laravel mix vue No.13 - Web Application2 - ライクボタンとダウンロードボタンの実装

okuda

Webdeveloper

Laravel mix vue No.12 - Web Application - 画像ファイルの一覧と詳細

お気軽に
お問い合わせください。

お問い合わせ