wordpress_blog

This is a dynamic to static website.

Vue 出一個電商網站 (4)

第11節:Vue 出一個電商網站 (下)

GitHub – vue-dashboard-record

Dashboard 新增模擬購物頁面 – 新增卡片式產品列表

操作與講解

  1. 接下來我們要來做模擬訂單的部分,不過在模擬訂單之前,我們先補一下這個登出的功能,那我們就直接打開 Navbar.vue 這個頁面,所以我們在製作登出的時候,它每個元件都可以獨立的運作。不過這登出難度並不會很高,所以這邊我們就不重頭講解,我們直接把現成的程式碼先貼進來之後,在講解說這段程式碼做了哪些事情,那上面這段是 Navbar 的元件,一樣是 const vm = this,const url 這段是 API 登出的路徑,在往下這裡確定登出之後就會把頁面轉回 /signin 的地方,(上面登出的連結補上 @click.prevent=”signout”),那我們存檔來試一次看看。完成 Sign out 之後我們一樣可以測試先前的一些行為,像是我們在網址的地方直接輸入 admin/orders 能不能直接進去那個頁面,按下去是沒有任何的反應,當然你也可以透過 Console 來看一下它到底有沒有跳轉這個頁面、admin/orders,當然它是沒有跳轉頁面。
  2. 我們先來看一下模擬訂單的畫面,模擬訂單的畫面首先我們要先把上面這個列表先把它完成,完成之後我們下一個章節再來介紹怎麼取得單一筆的資料以及加入購物車的部分。
  3. 那麼這一段我們操作就會比較快一點,相關的行為跟先前都差不多,那一樣新增一個元件 pages/CustomerOrders.vue,這一段會提供現成的卡片的 Template 讓我們可以直接套用。這段我們就先把這個元件建起來。
  4. 接下來我們把新增的頁面加到 Vue Router 裡面來 import CustomerOrders from ‘@/components/pages/CustomerOrders’。import 進來之後我們要在下方新增相對應的路徑,我們可以先複製先前所建立好的 admin 路由,然後在往下建立一個相同的,但是這一段要不太一樣,我們在製作這一段的時候它不用掛在 admin 的頁面下面,我們可以用相同的模板但是我們不需要掛在 admin 下面,然後它的 component 是 CustomerOrder。但是它不需要經過驗證就可以使用,所以等一下可以直接透過這個路徑直接傳進來,路徑也要稍微調整一下、然後 name 也改一下。
  5. 接下來我們把這個路徑直接放到 Sidebar.vue 上面,to=”/customer_order”、然後存檔。接下來我們直接在登入的地方如果輸入 /customer_order 就可以直接進入這個頁面,但是如果在此我們想要進入其他路徑就會被登出。
  6. 我們在 CustomerOrders.vue 頁面下我們直接貼上現成的程式碼,因為這一段的流程跟先前非常接近,所以我們就不重頭寫一次給大家看。我們直接講解一下這裡做了哪些事情,和先前一樣我們先定義一個 products,並且加入用戶端取得資料的路徑。那麼用戶端取得資料的路徑我們要選擇的是客戶購物 [免驗證] 這一個、取得商品列表這一段,並不是用 admin 的那一段這裡要特別注意,然後一樣可以加入讀取的效果,然後取得資料之後將資料存到 products,然後接下來我們就可以把 products 的資料陳列在這上面。
  7. 那麼我們剛有使用讀取的效果,那讀取的效果也做好了。接下來我們要把卡片的資料內容補上,這裡可以使用 .row.mt-4 這是與上方有一些間距,那這個 row 是 Bootstrap 4 做格線用的,因為這個卡片是有運用道格線的技巧,接下來我們在把卡片整個貼進來,然後這裡使用 v-for=”item in products”,然後在補上一個 :key=”item.id”、我們來存檔一次。
  8. 我們回到畫面上重新整理,現在就可以看到這裡就有一張一張的卡片,我們只要將這些資料一個一個綁定上來就可以,現在我們將這些資料全部替換上來。上方有一張產品的圖片我們先把它加進來,可以使用 :style 等於裡面一樣是個物件,不過這裡要特別注意,background的時候後面的Image I 要大寫 Image (backgroundImage),可以用 ES6 的反引號來製作這一段的連結 url,接下來我們要用$配合大括號,這是 ES6 反引號裡面插入變數的方法,然後使用 item.imageUrl,這是我們先前插入連結的方式,我們再重新整理看一下圖片有沒有進來。接下來下方有分類、以及標題、還有內容,相關內容我們就可以一一的補上。(備註:前者顯示僅有原價的、後者顯示僅有原價 + 優惠價。),現在這個卡片列表已經完成了。
  9. 這段就先製作登出、以及模擬訂單的產品列表這一段。完成之後我們再繼續來製作加到購物車的部分。
// 1. Navbar.vue
    <ul class="navbar-nav px-3">
      <li class="nav-item text-nowrap">
        <a class="nav-link" href="#" @click.prevent="signout">Sign out</a>
      </li>
    </ul>

<script>
export default {
  name: 'Navbar',
  data () {
    return {
      msg: 'Welcome to Your Vue.js App'
    }
  },
  methods: {
    signout() {
      const api = `${process.env.APIPATH}/logout`;
      const vm = this;
      this.$http.post(api).then((response) => {
        // console.log(response.data);
        if (response.data.success) {
          // vm.$router.push('/login');
          vm.$router.push('/signin');
        }
      });
    }
  }
}
</script>
// 4. router/index.js
// 把CustomerOrder給載進來
import CustomerOrder from '@/components/pages/CustomerOrders'

    // 客戶購物的路徑(使用與Dashboard相同的模板)
    {
      path: '/',
      name: 'Dashboard',
      component: Dashboard,
      children: [
        // CustomerOrder的路徑
        {
          path: 'customer_order',
          name: 'CustomerOrder',
          component: CustomerOrder,
        },
      ],
    },
// 5. Sidebar.vue
      <ul class="nav flex-column mb-2">
        <li class="nav-item">
          <router-link  class="nav-link" to="/customer_order">
            <i class="fas fa-shopping-cart"></i>
            模擬訂單
          </router-link>
        </li>
      </ul>
// 6~8.  CustomerOrders.vue
<template>
  <div>
    <loading :active.sync="isLoading"></loading>
    <div class="row mt-4">
      <div class="col-md-4 mb-4" v-for="item in products" :key="item.id">
        <div class="card border-0 shadow-sm">
          <div style="height: 150px; background-size: cover; background-position: center"
            :style="{backgroundImage: `url(${item.imageUrl})`}">
          </div>
          <div class="card-body">
            <span class="badge badge-secondary float-right ml-2">{{ item.category }}</span>
            <h5 class="card-title">
              <a href="#" class="text-dark">{{ item.title }}</a>
            </h5>
            <p class="card-text">{{ item.content }}</p>
            <div class="d-flex justify-content-between align-items-baseline">
              <div class="h5" v-if="!item.price">{{ item.origin_price }}</div>
              <del class="h6" v-if="item.price">{{ item.origin_price }}</del>
              <div class="h5" v-if="item.price">{{ item.price }}</div>
            </div>
          </div>
          <div class="card-footer d-flex">
            <button type="button" class="btn btn-outline-secondary btn-sm">
              <i class="fas fa-spinner fa-spin"></i>
              查看更多
            </button>
            <button type="button" class="btn btn-outline-danger btn-sm ml-auto">
              <i class="fas fa-spinner fa-spin"></i>
              加到購物車
            </button>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      products: [],
      isLoading: false,
    };
  },
  methods: {
    getProducts() {
      const vm = this;
      const url = `${process.env.APIPATH}/api/${process.env.CUSTOMPATH}/products`;
      vm.isLoading = true;
      this.$http.get(url).then((response) => {
        vm.products = response.data.products;
        console.log(response);
        vm.isLoading = false;
      });
    },
  },
  created() {
    this.getProducts();
  },
};
</script>

取得單一產品

操作與講解

  1. 做出彈出 Modal 的效果,像這樣子點起來之後它就會將這個 Modal 彈開,不過跟之前比較不一樣是它這個彈出 Modal 之前,它會取得資料之後再將 Modal 打開,然後在彈出 Modal 之後它還可以選購說它需要多少數量的產品。為什麼要這樣做,我們來看一下接下來可能會做到的作業,在作業的部分我們來看一下這個畫面,這裡面有非常多的產品,但是點擊這個搶購去的時候,它是到這個頁面再重新將資料取得一次。我們可以看一下上面的網址,上面是帶入 product/ 以及這個 product id,然後將這個產品的資料取出來,我們常常在呈現這個資料列表時它這裡所取得的資料並非完整的資料,如果要看詳細的資料會在重新取得單一筆的特定資料,所以在這裡要做的是我們點擊的時候才將完整的資料取出。
  2. 接下來我們來看一下取得單一筆資料的 API,這裡有一個單一商品細節,那單一商品的話我們取得跟先前差不多,不過這個 product 是沒有 s 的,我們在取得多筆資料的時候是有 s 的,那麼取得單筆是沒有 s 並且後面會帶上 id。
  3. 現在我們來製作取得單一筆資料的 Modal,我們要做的行為會放在查看更多的按鈕上,在這部分我們就可以在 methods 的部分先加上 getProduct 但是沒有 s、參數的話會帶上 id,然後先把大括號補上。接下來我們回到 <html> 的地方,我們將上面這個(<button>)查看更多補上 @click=”getProduct”,另外要帶上 id (@click=”getProduct(item.id)”),那 id 的話在上面這裡有個 item,我們把 item 的 id 帶進來。接下來 id 之後,這段程式碼跟先前的差異不大,我們可以直接複製過來,複製過來之後將這個 s 去掉補上 id。
  4. 接下來我們就可以取得單一筆資料,然後我們取得單一筆資料會在 Modal 裡面顯示。Modal 的部分我們另外定義一個 product 的物件來存放 Modal 的資料,所以在這裡會有一個 vm.product = response.data.product;,不過在存放之前我們還是看一下這個結果是否正常。重新整理、打開 Console 來看一下,接下來點擊查看更多然後看一下這個資料內容,看起來是有正確的呈現,我們剛剛點擊的是 Vue 課程好棒棒,那麼我們把這個 product 打開之後這個 title 也是 Vue 課程好棒棒。
  5. 接下來我們把這個關掉之後回到 CustomerOrders.vue 的頁面上,這段是取得資料之後再將 Modal 打開,那 Modal 的話我們先前是使用 jQuery 的方式,在這裡也是用相同的方式,我們 import $ from ‘jquery’;,然後將 jquery 載進來。
  6. 載進來以後我們這個 Modal 就不完全重新製作了,裡面的內容跟先前都差異不大,所以我們這裡先複製已經製作好的部分,但是比較重要的地方我們在這裡還是會重新講解,像是這裡有許多的資料內容,像是這裡有 {{ product.title }}、content、description,這裡都是直接帶入的。
  7. 接下來我們要將 Modal 打開,那 Modal 打開的話我們就會把它放在 AJAX 結束之後將 Modal 打開,那這個 Modal 的名字叫做 #productModal,這個載入資料也要把它打開,所以我們在這個地方會先將資料讀取進來之後再將 Modal 打開,所以我們一打開 Modal 它基本上就會有資料,我們現在點擊查看更多,這個時候資料已經帶進來了。我們現在點擊查看更多,讀取之後這裡就會出現完整的資料。
  8. 不過在這部分我們要介紹一個比較不一樣的讀取方式,像是這裡已經有預設準備很多旋轉的讀取效果,那麼我們就要將這個全畫面的讀取效果替換成單一個的讀取效果,那我們在這個部分就可以再新增一個狀態叫做 status,在這個 status 內再新增一個值叫做 loadingItem。那這段是要判斷目前畫面上是哪一個元素正在讀取中,loadingItem 要存放的值就是產品的 id,所以在這裡我們可以把這個 isLoading 改成 status.loadingItem,然後這裡要替換的是 id。那當然讀取完之後我們要將值替換成空的,和先前一樣。接下來這個 loadingItem 我們要把它放到上面這個地方來,我們可以把剛剛的 loadingItem 帶上來 v-if 假設 status 的 loadingItem 與我們目前的 item.id 是相符合的時候,我們就會將這個讀取效果呈現出來。那我們也把這段複製過來,重新整理之後我們按下查看更多,這個時候會跳出一小段時間的讀取效果,然後在將這個畫面呈現出來。但是我們只有在點擊的那個物件上才會出現讀取效果、然後並且將相關的資料呈現出來。
// 3~8. CustomerOrders.vue
<template>
  <div>
    <loading :active.sync="isLoading"></loading>
    <div class="row mt-4">
      <div class="col-md-4 mb-4" v-for="item in products" :key="item.id">
        <div class="card border-0 shadow-sm">
          <div style="height: 150px; background-size: cover; background-position: center"
            :style="{backgroundImage: `url(${item.imageUrl})`}">
          </div>
          <div class="card-body">
            <span class="badge badge-secondary float-right ml-2">{{ item.category }}</span>
            <h5 class="card-title">
              <a href="#" class="text-dark">{{ item.title }}</a>
            </h5>
            <p class="card-text">{{ item.content }}</p>
            <div class="d-flex justify-content-between align-items-baseline">
              <div class="h5" v-if="!item.price">{{ item.origin_price }}</div>
              <del class="h6" v-if="item.price">原價 {{ item.origin_price }} 元</del>
              <div class="h5" v-if="item.price">現在只要 {{ item.price }} 元</div>
            </div>
          </div>
          <div class="card-footer d-flex">
            <button type="button" class="btn btn-outline-secondary btn-sm"
              @click="getProduct(item.id)">
              <i class="fas fa-spinner fa-spin" v-if="status.loadingItem === item.id"></i>
              查看更多
            </button>
            <button type="button" class="btn btn-outline-danger btn-sm ml-auto">
              <i class="fas fa-spinner fa-spin" v-if="status.loadingItem === item.id"></i>
              加到購物車
            </button>
          </div>
        </div>
      </div>
    </div>
    <!-- Modal CustomerOrders -->
    <div class="modal fade" id="productModal" tabindex="-1" role="dialog"
      aria-labelledby="exampleModalLabel" aria-hidden="true">
      <div class="modal-dialog" role="document">
        <div class="modal-content">
          <div class="modal-header">
            <h5 class="modal-title" id="exampleModalLabel">{{ product.title }}</h5>
            <button type="button" class="close" data-dismiss="modal" aria-label="Close">
              <span aria-hidden="true">&times;</span>
            </button>
          </div>
          <div class="modal-body">
            <img :src="product.imageUrl" class="img-fluid" alt="">
            <blockquote class="blockquote mt-3">
              <p class="mb-0">{{ product.content }}</p>
              <footer class="blockquote-footer text-right">{{ product.description }}</footer>
            </blockquote>
            <div class="d-flex justify-content-between align-items-baseline">
              <div class="h4" v-if="!product.price">{{ product.origin_price }} 元</div>
              <del class="h6" v-if="product.price">原價 {{ product.origin_price }} 元</del>
              <div class="h4" v-if="product.price">現在只要 {{ product.price }} 元</div>
            </div>
            <select name="" class="form-control mt-3" v-model="product.num">
              <option :value="num" v-for="num in 10" :key="num">
                選購 {{ num }} {{ product.unit }}
              </option>
            </select>
          </div>
          <div class="modal-footer">
            <div class="text-muted text-nowrap mr-3">
              小計 <strong>{{ product.num * product.price }}</strong> 元
            </div>
            <button type="button" class="btn btn-primary">
              <!-- <i class="fas fa-spinner fa-spin" v-if="product.id === status.loadingItem"></i> -->
              加到購物車
            </button>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import $ from 'jquery';

export default {
  data() {
    return {
      products: [],
      product: {},
      status: {
        loadingItem: '',
      },
      isLoading: false,
    };
  },
  methods: {
    getProducts() {
      const vm = this;
      const url = `${process.env.APIPATH}/api/${process.env.CUSTOMPATH}/products`;
      vm.isLoading = true;
      this.$http.get(url).then((response) => {
        vm.products = response.data.products;
        console.log(response);
        vm.isLoading = false;
      });
    },
    // 取得單一產品
    getProduct(id) {
      const vm = this;
      const url = `${process.env.APIPATH}/api/${process.env.CUSTOMPATH}/product/${id}`;
      // vm.isLoading = true;
      vm.status.loadingItem = id;
      this.$http.get(url).then((response) => {
        vm.product = response.data.product;
        $('#productModal').modal('show');
        console.log(response);
        // vm.isLoading = false;
        vm.status.loadingItem = '';
        vm.product.num = 1; // 所有商品初始值設置為 1
      });
    },
  },
  created() {
    this.getProducts();
  },
};
</script>
討論串:查看更多選購數量問題

商品預設顯示數量和回傳資料的 num 有關,
可以看到 CustomerOrders.vue 頁面中有使用 v-model=”product.num”
因此要調整這個預設數量
有兩個辦法

  1. 可以在 Products.vue 頁面中新增關於 num 資料的詳細設定,如下:
  2. 可以在 CustomerOrders.vue 頁面的 getProduct 函式中,新增 vm.product.num = 1
// 1. Products.vue
<div class="form-group col-md-4">
  <label for="num">預設單位</label>
  <input type="number" class="form-control" id="num" placeholder="請輸入預設單位" v-model="tempProduct.num" />
</div>

// 這樣可以對每個商品的預設數量做調整。
// 2. CustomerOrders.vue
getProduct(id){
  ...
  this.axios.get(url).then((response) => {
    console.log(response.data)
    vm.product = response.data.product;
    $('#productModal').modal('show')
    vm.product.num = 1;//多加了這行
    vm.status.loadingItem = '';
  });

// 這樣所有商品初始值就會變成1了。
討論串:如何將login改為首頁
  • 若要改網站連結的路徑都是在 router/index.js 裡面去修改
  • 課程只是示範顯示首頁的元件為 Dashboard 來當首頁,並不代表網站首頁都一定是 Dashboard,可自己選擇要用哪個元件來當首頁
  • 路徑(path)的值可以自己去設定
  • 名稱(name)的值相同會產生黃色錯誤
排除錯誤:we can’t find `tsconfig.json` or `jsconfig.json`

Vetur – Setup/Advanced

修正:Navbar.vue
// Navbar.vue
<template>
  <nav class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0 shadow">
    <a class="navbar-brand col-md-3 col-lg-2 mr-0 px-3" href="#">Company name</a>
    <button class="navbar-toggler position-absolute d-md-none collapsed"
      type="button" data-toggle="collapse" data-target="#sidebarMenu"
      aria-controls="sidebarMenu" aria-expanded="false" aria-label="Toggle navigation">
      <span class="navbar-toggler-icon"></span>
    </button>
    <input class="form-control form-control-dark w-100"
      type="text" placeholder="Search" aria-label="Search" />
    <ul class="navbar-nav px-3">
      <li class="nav-item text-nowrap">
        <a class="nav-link" href="#" @click.prevent="signout">Sign out</a>
      </li>
    </ul>
  </nav>
</template>

<script>
export default {
  name: 'Navbar',
  data () {
    return {
      msg: 'Welcome to Your Vue.js App'
    }
  },
  methods: {
    signout() {
      const api = `${process.env.APIPATH}/logout`;
      const vm = this;
      this.$http.post(api).then((response) => {
        // console.log(response.data);
        if (response.data.success) {
          vm.$router.push('/login');
          // vm.$router.push('/signin');
        }
      });
    }
  }
}
</script>

選購產品及加入購物車

操作與講解

  1. 這段要來介紹加入購物車,在加入購物車之前我們先來看一下這一段的操作行為,在這個畫面上我們按查看更多,裡面就可以加到購物車,不過加入購物車之前,這裡可以選擇數量,像是這裡我們選擇2的話,在按加入購物車,那下面這個購物車列表就會跳出我們加入了2堂。另外一個就是我們直接點選加入購物車,直接點選的話,我們就會加入1堂的數量,所以我們在這邊行為會有兩種。一種是可以選擇數量、另外一種是直接將單一筆加進去。
  2. 那我們來看 API(文件)的部分,在 API 的部分加入購物車這裡會傳入參數,參數的地方第一個是傳入產品的 id、第二個是傳入(產品的)數量,所以在這裡要特別注意,我們在傳入的時候基本上不會傳入產品的價格,用戶可以傳入產品的價格的話,用戶就可能可以篡改整個購物車最終所結帳的金額。所以這裡要特別注意,通常我們只會上傳 id,那金額的計算是由後端來決定的。
  3. 那我們來看一下購物車的列表,在購物車列表的地方這裡有一個 carts,那 carts 它裡面是個陣列,所以這裡面會有很多個產品,那產品的話它就會回傳它相關的價格、以及它產品的內容。還有這裡要特別注意,除了產品的列表之外,然後價格會有兩個,一個是它的原始價格、以及最終的價格,那這個最終價格會受到優惠券的影響,當然這也是一般電商在設計平台裡面最複雜的地方,因為價格的計算邏輯非常多種,但在課程中會用比較簡單的方式呈現給大家看。
  4. 接下來我們在回到 API 上面這一段,現在我們回到購物車這個地方,我們在來回顧一下參數的部分,參數的部分我們要傳入的是一個產品的 id、以及數量,那我們現在來開始撰寫 JavaScript 的部分。
  5. 我們一樣到 (CustomerOrders.vue) methods 這個地方,然後我們用 addtoCart(),然後這裡會傳入兩個參數,一個就是 id,product 的 id、以及它的數量 qty,這個數量它是基本上一定要傳入數量,傳入數量的話基本上最少就是1,所以這裡可以用 ES6 一個方法,叫做預設值。這裡如果使用 qty = 1的話,代表函式傳進來的時候,如果沒有帶入 qty,它會使用預設值1的方式。
  6. 接下來我們先把另外一個函式(getProduct(id))打開,因為執行的內容差不多,我們就先把另外一段給貼過來,貼過來之後我們把這邊的 API 的路徑稍微做一下調整,加入購物車是使用 cart,這裡一樣會有讀取,然後行為會改成 post,接下來我們要把資料結構定義起來,我們會使用 const cart 等於一個物件,那物件會傳入兩個參數,一個是 product_id,那 product_id 就是 id,那另外一個是 qty,那 qty 在這裡不用這樣子寫,我們可以只寫一個 qty 的變數,那這個的話就會直接將這 qty 以及它的值自動帶進來。接下來在把這個 cart 放到這個後方,然後我們在傳入的時候,要記得資料結構,它的資料結構是 data 裡面在包著購物車的內容,那麼我們把多餘的方法把它移除。
  7. 然後接下來我們把這個 addtoCart 加到 <html> 的部分來,在上面這個卡片的部分,這個加到購物車就可以把 @click=”addtoCart()” 帶進來,然後我們要帶進的參數是 item 的 id、以及數量,那數量的話我們可以不用帶,它的預設值就是1,加進來之後我們重新整理一下,看一下能不能順利的加入購物車。現在我們按一下這個加到購物車,按下去之後這裡有回傳一些訊息,這裡回傳訊息說 true、已經成功加入購物車了。
  8. 接下來我們要介紹一下 Modal 的部分,那 Modal 的部分我們這裡可以選擇數量,那麼數量的話我們先暫存在 product.num 裡面來,那這個 product 就是我們點下查看更多取回來的那個 product 資料,那我們就會把 num、就是數量的資訊先存在 product 這個物件裡面來。現在目前如果按下這個下拉選單,它只會跳出選購一件,因為我們是寫死這個數值,如果說我們要讓它可以選擇多筆的數量該怎麼做,我們在這裡可以使用 v-for 等於 num in 固定的數量,比如說我們可以直接輸入10,那接下來這個 num 就是從1~10,後面要記得帶上 :key 等於 num。value 的部分我們就可以把 num 帶上來,就是數量、選擇的數量,不過記得把這個 value 改成動態屬性。接下來我們在把相關的文字帶上去,選購幾件,後面這個件我們一樣可以把它改成 product 的 unit,那這樣子的話就會變成選購多少數量、以及數量的單位,我們存檔試一次看看、重新整理。接下來按查看更多,那這邊的下拉選單就會出現選購幾堂,那這個堂這個字就是我們帶入的 product.unit,加到購物車的時候,我們就可以帶入 product 的 id、以及 product 的數量,然後存檔。現在我們來試著來送出不同數量的數值,這裡我們就可以按選購5堂、然後按加到購物車,這裡的話回傳的數值就會跳出 success、然後我們的數量是5,我們把5堂的 Vue 的課程加到購物車。
  9. 當然我們這裡按下選購幾堂之後要記得把這個 Modal 也關掉,那在這個部分我們就可以在一樣的把 Modal 關掉,這裡有個 modal(‘show’),我們把這個 modal(‘show’) 一樣把它貼過來,把它改成 modal(‘hide’),加到購物車之後就把這個 Modal 隱藏起來,購物車完成之後我們就可以在取得購物車的內容,就是 getCart(),取得購物車的內容其實方法都跟之前差不多,我們在把先前的 API (getProducts()) 複製過來,那複製過來之後把這個 product 換成 cart,其他的行為差距不大,我們接下來把這個 getCart() 完成之後要記得在一開始的時候,我們就把購物車也取得回來、this.getCart();,然後另外一個就是加入購物車之後也一樣要把購物車給取得回來,vm.getCart();,我們來看一下這個購物車有沒有正確的取得,我們來看一下 Console、然後按加到購物車,加到購物車之後會有兩次 AJAX 的行為,第一次是將物件加到購物車內、第二個是將購物車的資料取得回來,那取得回來之後這裡就有一些資訊,它是放在 data 的 data 內,這裡會有兩個總價,一個是 total、一個是 final_total,final_total 就是經過計算可能是有一些優惠等等的,然後下方有另外一個是購物車的內容,那我們就可以把這購物車的內容再把它呈現出來。呈現的方式就會像這樣子,有表格、然後這裡有品名、數量、以及單價,那下方還有總計。
  10. 購物車的列表這一段就讓大家自己練習,下個章節我們要來介紹怎麼刪除單一個商品、以及套用優惠碼的細節,如果沒有問題的話,大家先製作這一段。
// CustomerOrders.vue
<template>
  <div>
    <loading :active.sync="isLoading"></loading>
    <div class="row mt-4">
      <div class="col-md-4 mb-4" v-for="item in products" :key="item.id">
        <div class="card border-0 shadow-sm">
          <div style="height: 150px; background-size: cover; background-position: center"
            :style="{backgroundImage: `url(${item.imageUrl})`}">
          </div>
          <div class="card-body">
            <span class="badge badge-secondary float-right ml-2">{{ item.category }}</span>
            <h5 class="card-title">
              <a href="#" class="text-dark">{{ item.title }}</a>
            </h5>
            <p class="card-text">{{ item.content }}</p>
            <div class="d-flex justify-content-between align-items-baseline">
              <div class="h5" v-if="!item.price">{{ item.origin_price }}</div>
              <del class="h6" v-if="item.price">原價 {{ item.origin_price }} 元</del>
              <div class="h5" v-if="item.price">現在只要 {{ item.price }} 元</div>
            </div>
          </div>
          <div class="card-footer d-flex">
            <button type="button" class="btn btn-outline-secondary btn-sm"
              @click="getProduct(item.id)">
              <i class="fas fa-spinner fa-spin" v-if="status.loadingItem === item.id"></i>
              查看更多
            </button>
            <button type="button" class="btn btn-outline-danger btn-sm ml-auto"
              @click="addtoCart(item.id)">
              <i class="fas fa-spinner fa-spin" v-if="status.loadingItem === item.id"></i>
              加到購物車
            </button>
          </div>
        </div>
      </div>
    </div>
    <!-- Modal CustomerOrders -->
    <div class="modal fade" id="productModal" tabindex="-1" role="dialog"
      aria-labelledby="exampleModalLabel" aria-hidden="true">
      <div class="modal-dialog" role="document">
        <div class="modal-content">
          <div class="modal-header">
            <h5 class="modal-title" id="exampleModalLabel">{{ product.title }}</h5>
            <button type="button" class="close" data-dismiss="modal" aria-label="Close">
              <span aria-hidden="true">&times;</span>
            </button>
          </div>
          <div class="modal-body">
            <img :src="product.imageUrl" class="img-fluid" alt="">
            <blockquote class="blockquote mt-3">
              <p class="mb-0">{{ product.content }}</p>
              <footer class="blockquote-footer text-right">{{ product.description }}</footer>
            </blockquote>
            <div class="d-flex justify-content-between align-items-baseline">
              <div class="h4" v-if="!product.price">{{ product.origin_price }} 元</div>
              <del class="h6" v-if="product.price">原價 {{ product.origin_price }} 元</del>
              <div class="h4" v-if="product.price">現在只要 {{ product.price }} 元</div>
            </div>
            <select name="" class="form-control mt-3" v-model="product.num">
              <option :value="num" v-for="num in 10" :key="num">
                選購 {{ num }} {{ product.unit }}
              </option>
            </select>
          </div>
          <div class="modal-footer">
            <div class="text-muted text-nowrap mr-3">
              小計 <strong>{{ product.num * product.price }}</strong> 元
            </div>
            <button type="button" class="btn btn-primary"
              @click="addtoCart(product.id, product.num)">
              <!-- <i class="fas fa-spinner fa-spin" v-if="product.id === status.loadingItem"></i> -->
              加到購物車
            </button>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import $ from 'jquery';

export default {
  data() {
    return {
      products: [],
      product: {},
      status: {
        loadingItem: '',
      },
      isLoading: false,
    };
  },
  methods: {
    getProducts() {
      const vm = this;
      const url = `${process.env.APIPATH}/api/${process.env.CUSTOMPATH}/products`;
      vm.isLoading = true;
      this.$http.get(url).then((response) => {
        vm.products = response.data.products;
        console.log(response);
        vm.isLoading = false;
      });
    },
    // 取得單一產品
    getProduct(id) {
      const vm = this;
      const url = `${process.env.APIPATH}/api/${process.env.CUSTOMPATH}/product/${id}`;
      // vm.isLoading = true;
      vm.status.loadingItem = id;
      this.$http.get(url).then((response) => {
        vm.product = response.data.product;
        $('#productModal').modal('show');
        console.log(response);
        // vm.isLoading = false;
        vm.status.loadingItem = '';
        vm.product.num = 1; // 所有商品初始值設置為 1
      });
    },
    // 加入購物車
    addtoCart(id, qty = 1) {
      const vm = this;
      const url = `${process.env.APIPATH}/api/${process.env.CUSTOMPATH}/cart`;
      // vm.isLoading = true;
      vm.status.loadingItem = id;
      const cart = {
        product_id: id,
        qty,
      };
      this.$http.post(url, { data: cart }).then((response) => {
        console.log(response);
        // vm.isLoading = false;
        vm.status.loadingItem = '';
        vm.getCart();
        $('#productModal').modal('hide');
      });
    },
    // 取得購物車的內容
    getCart() {
      const vm = this;
      const url = `${process.env.APIPATH}/api/${process.env.CUSTOMPATH}/cart`;
      vm.isLoading = true;
      this.$http.get(url).then((response) => {
        // vm.products = response.data.products;
        console.log(response);
        vm.isLoading = false;
      });
    },
  },
  created() {
    this.getProducts();
    this.getCart();
  },
};
</script>

刪除購物車品項及新增優惠碼

操作與講解

  1. 這個地方已經把購物車的列表都已經呈現出來了,那購物車(列表)的部分還缺少刪除以及加入優惠券的部分,那我們來看一下 API,API 的部分如果刪除特定的一筆資料其實也蠻單純的,就是將 API 的路由帶進去之後,後面在補上購物車內的產品 id 就可以了。那麼另外一個套用優惠券,比較不一樣是說我們要上傳的是優惠碼,那麼先前有請大家先製作優惠券的頁面,如果你已經做好優惠券的頁面的話,也就可以製作這一段,就是將優惠碼也套用進來。那麼套用優惠碼之後,它的價格也會調整,所以每次套用優惠券後,這整個購物車也必需要重新取得。
  2. 我們這個地方先來製作刪除特定購物車內容的部分,一樣回到下方(CustomerOrders.vue),下面這個地方我們可以先把 AJAX 的操作先複製起來,接下來再新增一個行為叫做 removeCartItem() 的方法,然後並且把剛剛複製的 AJAX 行為先貼進來,那麼刪除購物車商品的時候它必須帶入 id,我們就可以把這個 id 一樣貼到這個後方 ${id},然後要特別注意,在這個部分操作並不是使用 get 或 post,所使用的是 delete,我們會送出刪除的行為,然後後端就會把這一筆資料給刪掉,在刪除之前我們先把多的行為先把它關掉、然後把這個 removeCartItem() 貼到上面刪除的按鈕上面,我們使用 @click 等於 removeCartItem() 之後,並且把 item 的 id 帶進來。
  3. 那我們來試試看能不能正確的刪除,我們重新整理、然後打開 Console,這個時候按下這個刪除的按鈕,刪除之後這裡會跳出一個訊息就是已經刪除,那麼刪除之後記得整個購物車也要重新取得,我們再把後面的程式碼補上,刪除之後我們再重新取得購物車的內容(vm.getCart();),當然讀取效果也要把它關掉,那我們重新整理,重新整理之後這裡購物車只剩一個品項、把它刪掉,刪除之後這裡就沒有品項,那實際製作的時候要確定購物車有沒有內容才顯示這個畫面,假設購物車都沒有內容的時候要記得把這個畫面把它隱藏起來,現在我們再把一些品項給加進來。
  4. 加進來之後,接下來我們要來套用優惠碼的部分,那套用優惠碼之前,這裡來跟大家講一個小技巧,這裡我們先準備好一個註解,那這個註解要呈現的是最終的一個價錢,那我們先存檔來試一次看看。現在因為我們沒有套用優惠券,所以這裡會有個總計的價錢、下面會有個折扣價,但是這兩個價格如果都同樣的時候,其實這個折扣價不需要出現,所以我們可以做個比對,假設 final_total 的價錢不等於 cart.total 的時候它才會顯示這一段,那我們重新整理、重新整理之後下面的我們比對之後總計下方就不會出現折扣價,接下來我們在把優惠碼給補上。
  5. 那優惠碼的部分這裡已經準備好表單,上方是一個 <input>、下方是一個按鈕,那在上面的部分,我們就可以補上一個 v-model 等於 coupon_code,那下方的部分我們就可以補上行為,不過在補上行為之前,我們先把行為寫出來,資料的部分剛剛有新增一個 coupon_code 先是一個空的(值),然後接下來我們在把上面這一段(removeCartItem(id)裡面的程式碼)先複製一份,然後摺疊起來,addCouponCode(),然後把剛剛複製的那一段貼上來,那我們來看一下 coupon_code 的資料結構是什麼樣子,coupon_code 的資料結構是 data 裡面有個 code,那這個 code 裡面才包著你所提供的優惠碼。那我們在這裡就在宣告一個 coupon 等於 code、然後對應的是剛剛所新宣告的一個 coupon_code 的變數,那這裡剛剛是使用 delete,那我們要再把它改成 post、然後送出 data: coupon ,然後 API 的路徑也記得要稍作調整,這裡對應的是 coupon 的 API,不過套用前我們先來看一下它是否有正常的運作。程式碼寫好之後記得把這個(addCouponCode)行為放回剛剛的按鈕上,@click 等於 addCouponCode。
  6. 接下來這個地方我們就輸入優惠碼,先輸入錯誤的優惠碼試試看,輸入之後這邊會跳出兩個 request,第一個是送出優惠碼的訊息,那這裡就會出現 success: false,所以我們這裡的驗證還要加上一個 success: false、然後它會出現找不到優惠券,所以這邊就沒有套用優惠券的內容。接下來我們這裡輸入正確的優惠碼是 code,這 code 是我們從優惠券的後台去新增的,所以我們要新增自己的優惠碼才能套用,那這個地方就套用優惠券,套用優惠券之後這裡就會已經套用優惠券,並且在這個地方就會出現折扣價,然後接下來我們把購物車的內容在打開看一下,購物車的內容在 data 裡面,這裡面還是一樣有每個產品的內容,然後這裡就會多一個 coupon,我們在套用這 coupon 的時候是將這個 coupon_code 套用在每個商品上,所以每個商品的價錢也都會有調整,這裡的價錢就會不太一樣,原價是1650,那麼在套用之後這個優惠券就會影響這個的價錢。
  7. 那麼如果說我們要知道哪些產品有套用優惠碼的話,我們一樣可以在這個地方加入我們這邊有先註解好的一段,假設它裡面有包含優惠券的話就會跳出已套用優惠券的字樣,那麼我們重新整理一下、重新整理之後,這裡所有的商品就會套用優惠券的字樣,所以這些就是已經有套用優惠券的商品。
  8. 如果沒有問題的話,我們就準備到下一個章節,要把訂單給送出囉
// CustomerOrders.vue
<template>
  <div>
    <loading :active.sync="isLoading"></loading>
    <div class="row mt-4">
      <div class="col-md-4 mb-4" v-for="item in products" :key="item.id">
        <div class="card border-0 shadow-sm">
          <div style="height: 150px; background-size: cover; background-position: center"
            :style="{backgroundImage: `url(${item.imageUrl})`}">
          </div>
          <div class="card-body">
            <span class="badge badge-secondary float-right ml-2">{{ item.category }}</span>
            <h5 class="card-title">
              <a href="#" class="text-dark">{{ item.title }}</a>
            </h5>
            <p class="card-text">{{ item.content }}</p>
            <div class="d-flex justify-content-between align-items-baseline">
              <div class="h5" v-if="!item.price">{{ item.origin_price }}</div>
              <del class="h6" v-if="item.price">原價 {{ item.origin_price }} 元</del>
              <div class="h5" v-if="item.price">現在只要 {{ item.price }} 元</div>
            </div>
          </div>
          <div class="card-footer d-flex">
            <button type="button" class="btn btn-outline-secondary btn-sm"
              @click="getProduct(item.id)">
              <i class="fas fa-spinner fa-spin" v-if="status.loadingItem === item.id"></i>
              查看更多
            </button>
            <button type="button" class="btn btn-outline-danger btn-sm ml-auto"
              @click="addtoCart(item.id)">
              <i class="fas fa-spinner fa-spin" v-if="status.loadingItem === item.id"></i>
              加到購物車
            </button>
          </div>
        </div>
      </div>
    </div>
    <!-- Modal CustomerOrders -->
    <div class="modal fade" id="productModal" tabindex="-1" role="dialog"
      aria-labelledby="exampleModalLabel" aria-hidden="true">
      <div class="modal-dialog" role="document">
        <div class="modal-content">
          <div class="modal-header">
            <h5 class="modal-title" id="exampleModalLabel">{{ product.title }}</h5>
            <button type="button" class="close" data-dismiss="modal" aria-label="Close">
              <span aria-hidden="true">&times;</span>
            </button>
          </div>
          <div class="modal-body">
            <img :src="product.imageUrl" class="img-fluid" alt="">
            <blockquote class="blockquote mt-3">
              <p class="mb-0">{{ product.content }}</p>
              <footer class="blockquote-footer text-right">{{ product.description }}</footer>
            </blockquote>
            <div class="d-flex justify-content-between align-items-baseline">
              <div class="h4" v-if="!product.price">{{ product.origin_price }} 元</div>
              <del class="h6" v-if="product.price">原價 {{ product.origin_price }} 元</del>
              <div class="h4" v-if="product.price">現在只要 {{ product.price }} 元</div>
            </div>
            <select name="" class="form-control mt-3" v-model="product.num">
              <option :value="num" v-for="num in 10" :key="num">
                選購 {{ num }} {{ product.unit }}
              </option>
            </select>
          </div>
          <div class="modal-footer">
            <div class="text-muted text-nowrap mr-3">
              小計 <strong>{{ product.num * product.price }}</strong> 元
            </div>
            <button type="button" class="btn btn-primary"
              @click="addtoCart(product.id, product.num)">
              <!-- <i class="fas fa-spinner fa-spin" v-if="product.id === status.loadingItem"></i> -->
              加到購物車
            </button>
          </div>
        </div>
      </div>
    </div>
    <!-- 購物車列表 -->
    <div class="my-5 row justify-content-center">
      <div class="col-md-6">
        <table class="table">
          <thead>
            <th></th>
            <th>品名</th>
            <th>數量</th>
            <th>單價</th>
          </thead>
          <tbody>
            <tr v-for="item in cart.carts" :key="item.id" v-if="cart.carts">
              <td class="align-middle">
                <button type="button" class="btn btn-outline-danger btn-sm"
                  @click="removeCartItem(item.id)">
                  <i class="far fa-trash-alt"></i>
                </button>
              </td>
              <td class="align-middle">
                {{ item.product.title }}
                <div class="text-success" v-if="item.coupon">
                  已套用優惠券
                </div>
              </td>
              <td class="align-middle">{{ item.qty }}/{{ item.product.unit }}</td>
              <td class="align-middle text-right">{{ item.final_total }}</td>
            </tr>
          </tbody>
          <tfoot>
            <tr>
              <td colspan="3" class="text-right">總計</td>
              <td class="text-right">{{ cart.total }}</td>
            </tr>
            <tr v-if="cart.final_total !== cart.total">
              <td colspan="3" class="text-right text-success">折扣價</td>
              <td class="text-right text-success">{{ cart.final_total }}</td>
            </tr>
          </tfoot>
        </table>
        <div class="input-group mb-3 input-group-sm">
          <input type="text" class="form-control" v-model="coupon_code" placeholder="請輸入優惠碼">
          <div class="input-group-append">
            <button class="btn btn-outline-secondary" type="button" @click="addCouponCode">
              套用優惠碼
            </button>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import $ from 'jquery';

export default {
  data() {
    return {
      products: [],
      product: {},
      status: {
        loadingItem: '',
      },
      cart: {},
      isLoading: false,
      coupon_code: '',
    };
  },
  methods: {
    getProducts() {
      const vm = this;
      const url = `${process.env.APIPATH}/api/${process.env.CUSTOMPATH}/products`;
      vm.isLoading = true;
      this.$http.get(url).then((response) => {
        vm.products = response.data.products;
        console.log(response);
        vm.isLoading = false;
      });
    },
    // 取得單一產品
    getProduct(id) {
      const vm = this;
      const url = `${process.env.APIPATH}/api/${process.env.CUSTOMPATH}/product/${id}`;
      // vm.isLoading = true;
      vm.status.loadingItem = id;
      this.$http.get(url).then((response) => {
        vm.product = response.data.product;
        $('#productModal').modal('show');
        console.log(response);
        // vm.isLoading = false;
        vm.status.loadingItem = '';
        vm.product.num = 1; // 所有商品初始值設置為 1
      });
    },
    // 加入購物車
    addtoCart(id, qty = 1) {
      const vm = this;
      const url = `${process.env.APIPATH}/api/${process.env.CUSTOMPATH}/cart`;
      // vm.isLoading = true;
      vm.status.loadingItem = id;
      const cart = {
        product_id: id,
        qty,
      };
      this.$http.post(url, { data: cart }).then((response) => {
        console.log(response);
        // vm.isLoading = false;
        vm.status.loadingItem = '';
        vm.getCart();
        $('#productModal').modal('hide');
      });
    },
    // 取得購物車的內容
    getCart() {
      const vm = this;
      const url = `${process.env.APIPATH}/api/${process.env.CUSTOMPATH}/cart`;
      vm.isLoading = true;
      this.$http.get(url).then((response) => {
        // vm.products = response.data.products;
        vm.cart = response.data.data;
        console.log(response);
        vm.isLoading = false;
      });
    },
    // 刪除購物車品項
    removeCartItem(id) {
      const vm = this;
      const url = `${process.env.APIPATH}/api/${process.env.CUSTOMPATH}/cart/${id}`;
      vm.isLoading = true;
      this.$http.delete(url).then(() => {
        vm.getCart();
        vm.isLoading = false;
      });
    },
    // 新增優惠碼
    addCouponCode() {
      const vm = this;
      const url = `${process.env.APIPATH}/api/${process.env.CUSTOMPATH}/coupon`;
      const coupon = {
        code: vm.coupon_code,
      };
      vm.isLoading = true;
      this.$http.post(url, { data: coupon }).then((response) => {
        console.log(response);
        vm.getCart();
        vm.isLoading = false;
      });
    },
  },
  created() {
    this.getProducts();
    this.getCart();
  },
};
</script>

*建立訂單及表單驗證技巧

資源

操作與講解

  1. 接下來來到建立訂單的部分,這個地方也是蠻重要而且比較困難的地方,我們在建立訂單的時候會把原本所選的購物車的內容全部刪掉,然後它會成立一個新的訂單,那麼在訂單送出前也必需把用戶的一些相關資訊也都存下來。
  2. 那我們來看一下畫面在這個部分會做什麼事情,下面這個地方就是訂單所要填入的內容,在這個部分最困難的就是說這個訂單不能隨意讓用戶送出,像我們現在直接按下送出訂單,這個訊息它會直接被阻擋,並且如果點一些特定的欄位,它會跳出提示說姓名欄位不得留空,在這個地方我們來填入錯誤的 email 內容,這裡會跳出說 email 必需是有效的電子郵件地址,所以我們在輸入這個表單的時候,要特別做這樣的驗證,不能讓用戶隨意送出空的表單內容。
  3. 這個時候就可以使用一個套件叫 vee-validate,這個套件使用上蠻容易的,等下課程也會介紹怎麼使用這個套件,而且這個套件還包含中文的語系檔,所以可以直接的使用。它可以在畫面上加入一些條件,像是這裡就可以要求說一定要 email 格式、或者是使用正規式的方式來驗證這個表單內容是否正確。VeeValidate 這個套件跟 Bootstrap 也是沒有問題的,那我們來開始製作。
  4. 我們回到畫面上,下方這個地方還沒有表單內容,那表單這部分我們就不重頭打,會直接提供現成的給大家,我們把表單的格式直接貼進來、重新排版,調整好之後,下面要建立對應的資料格式,那麼這個資料格式大家要記得對應送出的 API 的結構,所以在這裡我們就要建立 form,那這個表單的結構大家可以直接參考 API 所提供的格式。
  5. 那下面的部分我們就來開始撰寫 AJAX,但是這個 AJAX 跟先前的方法也都差異不大,所以這段我們就加速帶過,訂單完成之後,補上一個訂單已建立的字樣,那這裡還沒有完成,先做到這部分就可以,接下來把這個 createOrder 加到這個表單上面來,這個方法可以清除它預設的 submit 行為、我們存檔。
  6. 存檔之後我們來看一下,這個表單格式錯誤、表單完成以後,我們先不要做驗證,先試試看這個送出的行為是否正確,那我們先輸入假的資料、然後將這個訂單給送出,那這個時候會跳出訂單已建立,所以我們現在 AJAX 行為是沒有問題的,接下來我們要來製作驗證的部分。
  7. 我們打開 Terminal(終端機),然後將這個驗證工具把它裝下來,驗證工具安裝、npm install vee-validate,存檔(安裝)這部分一樣花一些時間,這部分我們就先來準備一些後面所需要的程式碼,它一樣是需要做 import,那我們打開 main.js,那我們就先把它給 import 進來,import 進來之後它一樣去啟用它、Vue.use(VeeValidate) 把它啟用、存檔之後,我們來將服務 run 起來 npm run dev。
  8. 這裡有寫一下它的使用方式,它的使用方式就是在你的元件上面加上 v-validate 這個字樣,那我們先把它加上來,先使用收件人姓名這個部分,我們把它加上來、然後後面 email 的部分先把它拿掉,我們先加上 required 就好、存檔。然後回到我們畫面上、重新整理這裡不會有任何變化,我們先試著把錯誤的訊息把它抓出來,下方有一個 <span> 這裡是會輸出紅色的字樣,我們先用兩個大括號使用 errors 這個是它所提供的一個變數、然後 has,然後假設它有 name 這個欄位,它對應的是我們 <input> 裡面 name 這個屬性,那剛好我們這個名稱是一樣,是叫做 name,那我們把它輸入進來、然後存檔 (註:在此條件是 “required”,所以是不得為空)。這個條件是當它被觸發之後,然後它的 name 並不存在的話,它就會跳出這個錯誤、那我們重新整理一下,重新整理我們到畫面上之後,現在我們還沒有觸發它,所以他現在是跳出 false,那麼我們先輸入一些內容,它還是 false,可是當這個欄位清空之後,它就會跳出 true,也就是說它這個欄位當被觸發過之後,但是如果他沒有輸入內容的話,它就會跳出錯誤,那我們再重新整理一次。現在我們先直接點一下、然後移出來(點空白處),這個時候它就算錯誤。我應該要輸入這個欄位,但是我跳過的話,這個就算錯誤。
  9. 那接下來我們就可以用這個方式驗證它,當它為 true 的時候、當它為錯誤的時候,我們就顯示這個欄位 v-if=”errors.has(‘name’)”,那這個 name 是對應上面那個 name (值那個 name),然後(<span>標籤內修改成姓名必須輸入)、存檔之後我們在重新整理,現在是空的、但是如果說我們點一下這個欄位、然後在點一下外面它就會跳出這個錯誤,姓名必須輸入,那這個方式我們也可以套用在這個 <input> 的 class 上面,我們可以在這個 <input> 上面再加上一個 :class 把它改為一個物件,前方我們加入 ‘is-invalid’,代表說它的驗證是錯誤的,那它的條件就是 errors.has(‘name’) 的時候,它會出現紅色的框框,那我們來試一次看看。重新整理之後,我們點一下收件者姓名、然後在點一下外面,這個時候它的框框就變紅色的,而且會跳出姓名必須輸入的字樣。
  10. 接下來我們來製作 Email 這個欄位,Email 這個欄位製作方式跟其他有一些些不同,我們直接參照它的官網,有一些特定的欄位它有提供特殊的驗證方法,那我們這一段就直接使用它的 Email 驗證,接下來我們回到 CustomerOrders.vue 的頁面上面來、然後並且將 v-validate 貼進來,貼進來之後它就會自動驗證 Email,那下方這個地方我們一樣用 v-if 去判斷 email 這個欄位是否正確,接下來裡面這一段驗證可以用它所提供的另一個方法,errors.first 然後括號、使用 email 這個欄位,這個方式會比較特別,它會直接告訴你說你的 Email 輸入錯誤在哪裡、那我們存檔。接下來重新整理之後,我們先點一下 Email 這個欄位,點一下之後再點擊外面,它會回應說這個欄位是必須的。那我們再輸入一些東西,那我們隨便輸入一些文字之後,這會跳出這個 Email 的欄位它不是有效的 Email 格式,用這種方式它就可以提供一些額外的回饋訊息,那當然如果說我們把它輸入成正確的格式,它這個回饋訊息就會移掉。現在這個回饋訊息由於是英文的,那當然我們會希望用中文的方式來呈現這個回饋訊息,我們就可以做一些些簡單的調整,就可以把它改成中文的回饋。
  11. 我們回到 main.js 、並且回到它的 node_modules 裡面(有個 vee-validate),然後裡面有個 dist,dist 下面有一個 locale,這個 locale 下就是各種語系的檔案,那我們就把這個 locale import 進來,假設我們在這裡需要中文的話、import,import 之後就必需啟用它,接下來我們把它的、接下來選擇 Validator 這個物件,下方還有一個 localize 的一個方法,那這個方法就可以將中文語系載進來,zh_TW 然後把這個語系檔給載進來,這個載入方式比較特別,不過在它的文件裡面也有寫到,在這部分我們就直接打給大家看、存檔之後就可以使用它的中文語系。在這個部分它要求我們的單字要改一下、改成這樣,接下來存檔,存檔之後我們回到畫面上,我們再來試一次這個 Email 的格式,這個時候它就會改成中文的回饋,如果說我們改成錯誤的話,它一樣會跳出對應的訊息。
  12. 我們現在在看一下這個表單的部分,現在這個表單這個欄位,我們一樣留空,但是我們按下送出訂單,這個時候會跳出一個請填寫這個欄位,這個請填寫這個欄位是 Chrome 自動跳出來,只要你在 <input> 裡面有加上 required 的話,它就會跳出這個訊息,那假設說我們不要使用這個 required,我們要使用這個套件的功能,要阻止它送出的話,該怎麼做,像我們現在把這個 required 拿掉,如果按下送出,它是真的會送出這個表單,所以我們現在要確保我們在送出的時候,它的欄位是符合我們的套件的要求的、該怎麼做,我們在滑到下方程式碼的部分,它在它的 API 裡面有提供其中一段,就是在我們 submit 的時候可以先做一下我們套件的驗證。我們現在直接把這一段複製下來,貼到我們的程式碼裡面,這個 result 要加上括號,加進來之後,假設這邊有一個 if result 為 true 的時候,把這一段貼進來,那在另外一個地方加上 else 假設沒有成功的時候,我們跳出欄位不完整,我們先使用 console 來做一下就可以了,並且把這個 isLoading 拿掉,因為這個會擋到我們的畫面、然後存檔。接下來回到畫面上重新整理、然後我們這個時候按下送出訂單,按下去之後(Console)會跳出欄位不完整,並且我們往上看這上面的部分都會全部算是被驗證過的狀態,如果說我們這個欄位完全都沒有碰過,它一樣會顯示這個紅色的錯誤。
  13. 這邊就是一個比較完整的驗證模式,現在這個部分就給大家自己練習,把這個表單完整的製作完成,並且可以送出表單的狀態。
// 7. VeeValidate 安裝
// npm
npm install vee-validate --save
// 7. main.js
// import VeeValidate
import VeeValidate from 'vee-validate'
版本問題
// 移除 vee-validate 套件 3.x.x 版本
npm uninstall vee-validate --save

// 安裝 vee-validate 套件 2.x.x 版本
npm install vee-validate@2.2.15 --save
即時監聽 – Focus 到 <input> 再點外面即時監聽到 <span> 的行為
// main.js
// 修改 events,加上 blur
Vue.use(VeeValidate, {
  events: 'input|blur',
});
// 11. main.js
// 中文語系
import zhTW_Validate from 'vee-validate/dist/locale/zh_TW';

VeeValidate.Validator.localize('zh_TW', zhTW_Validate)
// 11. main.js
// 中文語系修改後
import zhTWValidate from 'vee-validate/dist/locale/zh_TW'

// 寫在這無法顯示中文
// VeeValidate.Validator.localize('zh_TW', zhTWValidate)
Vue.use(VeeValidate, {
  events: 'input|blur',
})
// 寫在這才能顯示中文
VeeValidate.Validator.localize('zh_TW', zhTWValidate)
// CustomerOrders.vue
<template>
  <div>
    <loading :active.sync="isLoading"></loading>
    <div class="row mt-4">
      <div class="col-md-4 mb-4" v-for="item in products" :key="item.id">
        <div class="card border-0 shadow-sm">
          <div style="height: 150px; background-size: cover; background-position: center"
            :style="{backgroundImage: `url(${item.imageUrl})`}">
          </div>
          <div class="card-body">
            <span class="badge badge-secondary float-right ml-2">{{ item.category }}</span>
            <h5 class="card-title">
              <a href="#" class="text-dark">{{ item.title }}</a>
            </h5>
            <p class="card-text">{{ item.content }}</p>
            <div class="d-flex justify-content-between align-items-baseline">
              <div class="h5" v-if="!item.price">{{ item.origin_price }}</div>
              <del class="h6" v-if="item.price">原價 {{ item.origin_price }} 元</del>
              <div class="h5" v-if="item.price">現在只要 {{ item.price }} 元</div>
            </div>
          </div>
          <div class="card-footer d-flex">
            <button type="button" class="btn btn-outline-secondary btn-sm"
              @click="getProduct(item.id)">
              <i class="fas fa-spinner fa-spin" v-if="status.loadingItem === item.id"></i>
              查看更多
            </button>
            <button type="button" class="btn btn-outline-danger btn-sm ml-auto"
              @click="addtoCart(item.id)">
              <i class="fas fa-spinner fa-spin" v-if="status.loadingItem === item.id"></i>
              加到購物車
            </button>
          </div>
        </div>
      </div>
    </div>
    <!-- Modal CustomerOrders -->
    <div class="modal fade" id="productModal" tabindex="-1" role="dialog"
      aria-labelledby="exampleModalLabel" aria-hidden="true">
      <div class="modal-dialog" role="document">
        <div class="modal-content">
          <div class="modal-header">
            <h5 class="modal-title" id="exampleModalLabel">{{ product.title }}</h5>
            <button type="button" class="close" data-dismiss="modal" aria-label="Close">
              <span aria-hidden="true">&times;</span>
            </button>
          </div>
          <div class="modal-body">
            <img :src="product.imageUrl" class="img-fluid" alt="">
            <blockquote class="blockquote mt-3">
              <p class="mb-0">{{ product.content }}</p>
              <footer class="blockquote-footer text-right">{{ product.description }}</footer>
            </blockquote>
            <div class="d-flex justify-content-between align-items-baseline">
              <div class="h4" v-if="!product.price">{{ product.origin_price }} 元</div>
              <del class="h6" v-if="product.price">原價 {{ product.origin_price }} 元</del>
              <div class="h4" v-if="product.price">現在只要 {{ product.price }} 元</div>
            </div>
            <select name="" class="form-control mt-3" v-model="product.num">
              <option :value="num" v-for="num in 10" :key="num">
                選購 {{ num }} {{ product.unit }}
              </option>
            </select>
          </div>
          <div class="modal-footer">
            <div class="text-muted text-nowrap mr-3">
              小計 <strong>{{ product.num * product.price }}</strong> 元
            </div>
            <button type="button" class="btn btn-primary"
              @click="addtoCart(product.id, product.num)">
              <!-- <i class="fas fa-spinner fa-spin" v-if="product.id === status.loadingItem"></i> -->
              加到購物車
            </button>
          </div>
        </div>
      </div>
    </div>
    <!-- 購物車列表 -->
    <div class="my-5 row justify-content-center">
      <div class="col-md-6">
        <table class="table">
          <thead>
            <th></th>
            <th>品名</th>
            <th>數量</th>
            <th>單價</th>
          </thead>
          <tbody>
            <tr v-for="item in cart.carts" :key="item.id" v-if="cart.carts">
              <td class="align-middle">
                <button type="button" class="btn btn-outline-danger btn-sm"
                  @click="removeCartItem(item.id)">
                  <i class="far fa-trash-alt"></i>
                </button>
              </td>
              <td class="align-middle">
                {{ item.product.title }}
                <div class="text-success" v-if="item.coupon">
                  已套用優惠券
                </div>
              </td>
              <td class="align-middle">{{ item.qty }}/{{ item.product.unit }}</td>
              <td class="align-middle text-right">{{ item.final_total }}</td>
            </tr>
          </tbody>
          <tfoot>
            <tr>
              <td colspan="3" class="text-right">總計</td>
              <td class="text-right">{{ cart.total }}</td>
            </tr>
            <tr v-if="cart.final_total !== cart.total">
              <td colspan="3" class="text-right text-success">折扣價</td>
              <td class="text-right text-success">{{ cart.final_total }}</td>
            </tr>
          </tfoot>
        </table>
        <div class="input-group mb-3 input-group-sm">
          <input type="text" class="form-control" v-model="coupon_code" placeholder="請輸入優惠碼">
          <div class="input-group-append">
            <button class="btn btn-outline-secondary" type="button" @click="addCouponCode">
              套用優惠碼
            </button>
          </div>
        </div>
      </div>
    </div>
    <!-- 建立訂購表單 -->
    <div class="my-5 row justify-content-center">
      <form class="col-md-6" @submit.prevent="createOrder">
        <div class="form-group">
          <label for="useremail">Email</label>
          <input type="email" class="form-control" name="email" id="useremail"
            v-validate="'required|email'"
            :class="{'is-invalid': errors.has('email')}"
            v-model="form.user.email" placeholder="請輸入 Email">
          <span class="text-danger" v-if="errors.has('email')">
            {{ errors.first('email') }}
          </span>
        </div>
      
        <div class="form-group">
          <label for="username">收件人姓名</label>
          <input type="text" class="form-control" name="name" id="username"
            :class="{'is-invalid': errors.has('name')}"
            v-model="form.user.name" v-validate="'required'" placeholder="輸入姓名">
          <span class="text-danger" v-if="errors.has('name')">姓名必須輸入</span>
        </div>
      
        <div class="form-group">
          <label for="usertel">收件人電話</label>
          <input type="tel" class="form-control" name="tel" id="usertel"
            :class="{'is-invalid': errors.has('tel')}"
            v-model="form.user.tel" v-validate="'required'" placeholder="請輸入電話">
          <span class="text-danger" v-if="errors.has('tel')">電話欄位不得留空</span>
        </div>
      
        <div class="form-group">
          <label for="useraddress">收件人地址</label>
          <input type="text" class="form-control" name="address" id="useraddress"
            :class="{'is-invalid': errors.has('address')}"
            v-model="form.user.address" v-validate="'required'" placeholder="請輸入地址">
          <span class="text-danger" v-if="errors.has('address')">地址欄位不得留空</span>
        </div>
      
        <div class="form-group">
          <label for="comment">留言</label>
          <textarea name="" id="comment" class="form-control" cols="30" rows="10" v-model="form.message"></textarea>
        </div>
        <div class="text-right">
          <button class="btn btn-danger">送出訂單</button>
        </div>
      </form>
    </div>
  </div>
</template>

<script>
import $ from 'jquery';

export default {
  data() {
    return {
      products: [],
      product: {},
      status: {
        loadingItem: '',
      },
      form: {
        user: {
          name: '',
          email: '',
          tel: '',
          address: '',
        },
        message: '',
      },
      cart: {},
      isLoading: false,
      coupon_code: '',
    };
  },
  methods: {
    getProducts() {
      const vm = this;
      const url = `${process.env.APIPATH}/api/${process.env.CUSTOMPATH}/products`;
      vm.isLoading = true;
      this.$http.get(url).then((response) => {
        vm.products = response.data.products;
        console.log(response);
        vm.isLoading = false;
      });
    },
    // 取得單一產品
    getProduct(id) {
      const vm = this;
      const url = `${process.env.APIPATH}/api/${process.env.CUSTOMPATH}/product/${id}`;
      // vm.isLoading = true;
      vm.status.loadingItem = id;
      this.$http.get(url).then((response) => {
        vm.product = response.data.product;
        $('#productModal').modal('show');
        console.log(response);
        // vm.isLoading = false;
        vm.status.loadingItem = '';
        vm.product.num = 1; // 所有商品初始值設置為 1
      });
    },
    // 加入購物車
    addtoCart(id, qty = 1) {
      const vm = this;
      const url = `${process.env.APIPATH}/api/${process.env.CUSTOMPATH}/cart`;
      // vm.isLoading = true;
      vm.status.loadingItem = id;
      const cart = {
        product_id: id,
        qty,
      };
      this.$http.post(url, { data: cart }).then((response) => {
        console.log(response);
        // vm.isLoading = false;
        vm.status.loadingItem = '';
        vm.getCart();
        $('#productModal').modal('hide');
      });
    },
    // 取得購物車的內容
    getCart() {
      const vm = this;
      const url = `${process.env.APIPATH}/api/${process.env.CUSTOMPATH}/cart`;
      vm.isLoading = true;
      this.$http.get(url).then((response) => {
        // vm.products = response.data.products;
        vm.cart = response.data.data;
        console.log(response);
        vm.isLoading = false;
      });
    },
    // 刪除購物車品項
    removeCartItem(id) {
      const vm = this;
      const url = `${process.env.APIPATH}/api/${process.env.CUSTOMPATH}/cart/${id}`;
      vm.isLoading = true;
      this.$http.delete(url).then(() => {
        vm.getCart();
        vm.isLoading = false;
      });
    },
    // 新增優惠碼
    addCouponCode() {
      const vm = this;
      const url = `${process.env.APIPATH}/api/${process.env.CUSTOMPATH}/coupon`;
      const coupon = {
        code: vm.coupon_code,
      };
      vm.isLoading = true;
      this.$http.post(url, { data: coupon }).then((response) => {
        console.log(response);
        vm.getCart();
        vm.isLoading = false;
      });
    },
    // 建立訂購表單
    createOrder() {
      const vm = this;
      const url = `${process.env.APIPATH}/api/${process.env.CUSTOMPATH}/order`;
      const order = vm.form;
      // vm.isLoading = true;
      this.$validator.validate().then((result) => {
        if (result) {
          this.$http.post(url, { data: order }).then((response) => {
            console.log('訂單已建立', response);
            // vm.getCart();
            vm.isLoading = false;
          });
        } else {
          console.log('欄位不完整');
        }
      });
    },
  },
  created() {
    this.getProducts();
    this.getCart();
  },
};
</script>
// main.js
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
// 第三方的套件
import axios from 'axios'
import VueAxios from 'vue-axios'
// import component
import Loading from 'vue-loading-overlay'
// import stylesheet
import 'vue-loading-overlay/dist/vue-loading.css'
import 'bootstrap'
// import VeeValidate
import VeeValidate from 'vee-validate'
// import 中文語系
import zhTWValidate from 'vee-validate/dist/locale/zh_TW'
// 自己撰寫
import App from './App'
import router from './router'
import './bus'
import currencyFilter from './filters/currency'
import dateFilter from './filters/date'

Vue.config.productionTip = false
Vue.use(VueAxios, axios)

// 寫在這無法顯示中文
// VeeValidate.Validator.localize('zh_TW', zhTWValidate)
Vue.use(VeeValidate, {
  events: 'input|blur',
})
// 寫在這才能顯示中文
VeeValidate.Validator.localize('zh_TW', zhTWValidate)

Vue.component('Loading', Loading)
Vue.filter('currency', currencyFilter)
Vue.filter('date', dateFilter)

// 前端跨域設定
axios.defaults.withCredentials = true;

/* eslint-disable no-new */
new Vue({
  el: '#app',
  router,
  components: { App },
  template: '<App/>'
})

// 導航守衛
router.beforeEach((to, from, next) => {
  // console.log('to', to, 'from', from, 'next', next);
  // ...
  if (to.meta.requiresAuth) {
    const api = `${process.env.APIPATH}/api/user/check`;
    axios.post(api).then((response) => {
      // console.log(response.data);
      if (response.data.success) {
        next();
      } else {
        next({
          path: '/login',
        });
      }
    });
  } else {
    next();
  }
})

表單驗證補充

操作與講解

  1. 在我們課程原本的介紹是使用第二版的 Vee Validate,那我們現在直接跳到指定的行數,在過去我們使用 Vee Validate 它的驗證主要是透過 Vue 的指令、它算是 HTML 的屬性來進行驗證。
  2. 那在新版就有很大的不同,新版的話主要是使用元件的方式來進行驗證,那相對來說使用元件的方式進行驗證會稍微複雜一點點,但是它會更符合 Vue 的邏輯,因此在這個地方我們就額外推出關於 Vee Validate 新版的驗證方式,也因為新版的驗證它稍微複雜一點點,所以在這個地方我們會提供完整的文件。
  3. 這個文件我們只要按照這個流程我們就可以把 Vee Validate 安裝好,我們就透過這個文件一一的解說我們到底做了哪些事情,我們先把多餘的 <input> 的欄位先把它關掉,我們先只留下 Email 這一段就好了,這個是對應我們的這個頁面、就是我們現在只留下這個 Email,那下面的收件人姓名到留言這一段,我們先把它們關掉,這個是 Email,因此我們就把下面的收件人姓名到留言這個地方,我們都先把它刪掉,現在我們只留下畫面的這一塊。接下來我們把原本的 Vee-Validate 解除安裝、然後並且安裝新版的 Vee-Validate,那我們來打開終端機、打開終端機之後,npm uninstall vee-validate,解除安裝之後,我們就可以準備直接安裝新版的 Vee-Validate,這個地方我們就輸入 npm install vee-validate –save,安裝完成之後,我們會建議先打開 package.json 這一個頁面,打開 package.json 之後我們要先確定我們 vee-validate 的版本,那目前來說它的使用版本是3.x.x的版本,那麼如果你是使用第四版測試版的話,它是支援 Vue 3。
  4. 我們先把 Vue 的環境運行起來 npm run dev,運行起來它預期會跳一些錯誤,因為新舊版是有一點點不同的,但是我們先不管它。現在我們先把多的頁面關掉,我們打開 main.js、我們在進入點上面需要做一點點調整。在原本 vee-validate 上面有進行一些設定像是這兩行,還有上面會有引入 vee-validate 的方法,我們先把這四行先把它刪掉,這是屬於舊版的驗證。
  5. 那現在我們要導入新版的驗證,存檔之後、我們現在跳到步驟二,我們必需先把新版是使用元件的方式進行驗證,所以我們先把元件給導入進來,這邊是 import 相關的元件內容,那我們把它放在這個地方、原本的這個地方,我們先整理一下程式碼。接下來我們再把這裡有一些規則、以及元件的設定,我們先把它直接給貼進來,貼進來之後我們再一一解說這些程式碼到底代表什麼意思,我先稍微整理一下程式碼、存檔之後,我們現在來稍微做一下說明,在這個地方我們分別引入這些區塊,這邊是將元件、以及相關的驗證設定檔,從 VeeValidate 給導出來,validation-provider 是 input 驗證元件、validation-observer 則是整體 <form> 驗證元件,(extend)這邊是VeeValidate的一些擴充功能、(localize)是關於語系的設定、(configure)是VeeValidate的一些設定檔,這是針對繁體中文的一些語系設定,它這個可以直接導出、讓我們就可以直接運用,然後另外一個這是一個比較大、不同的地方就是 VeeValidate 在新版的時候,它所有驗證都是把它規則化,像是我們要驗證 email 的時候,它就提供 email 的驗證規則,一般來說它可以自己撰寫、另外一種方式就是直接使用它所提供的一些規則,這些規則我們就把它全部導出出來,就不需要一個一個導入了。
  6. 那我們再往下看,下面這個地方是我們剛剛所加入的其他設定檔,那麼大家可能會比較疑惑的是屬於這一段,這一段是我們會把剛剛的規則全部都導出出來,並且把它加到 VeeValidate 的擴充的裡面,就是把它的規則預設其實是沒有加入進去,我們就必須手動的把它導出之後,再把它加進去。這樣你才可以運用這些規則。這邊是運用繁體中文的語系檔,另外下方有兩個元件、下面這邊有兩個元件,兩個元件我們會分別做介紹,其中一個是針對單一 input 的驗證,另外一個是針對表單完整的 form 表單進行驗證。另外最後還有關於 class name 的一些設定檔,那麼這個 class name 的設定檔目前這邊所撰寫的這兩行是針對 Bootstrap 所進行驗證的一些 class name 的設定,這個就是整個我們在導入新版的VeeValidate 會加入的片段。
  7. 建議你可以先檢查一下你目前的程式碼有沒有正確的運行,照理說你在設定完成之後,你的環境是可以運行起來的,那這邊我們就把 main.js 關掉,現在我們再回到我們這段程式碼來(CustomerOrders.vue),這個是我們 CustomerOrders.vue 這個頁面,對應的就是我們目前的訂購表單

表單驗證補充單元未完成

// 3. terminal
// 解除安裝舊的 Vee-Validate 版本
npm uninstall vee-validate
// 安裝新的版本 V3
npm install vee-validate --save
// 4. main.js 這兩行
VeeValidate.Validator.localize('zh_TW', zhTWValidate);
Vue.use(VeeValidate);
// 引入的 vee-validate
import VeeValidate from 'vee-validate';
import zhTWValidate from 'vee-validate/dist/locale/zh_TW';
// 5. main.js

import { ValidationObserver, ValidationProvider, extend, localize, configure } from 'vee-validate';
import TW from 'vee-validate/dist/locale/zh_TW.json'
import * rules from 'vee-validate/dist/rules';

Object.keys(rules).forEach((rule) => {
  extend(rule, rules[rule]);
});

localize('zh_TW', TW);

Vue.component('ValidationObserver', ValidationObserver)
Vue.component('ValidationProvider', ValidationProvider)

configure({
  classes: {
    valid: 'is-valid',
    invalid: 'is-invalid'
  }
});
口誤:
validation-provider 是 input 驗證元件
validation-observer 則是整體 <form> 驗證元件

表單驗證補充文件

第三版

如何為單一表單(input)進行驗證

步驟一

載入 vee-validate

npm install vee-validate --save

步驟二

註冊全域的表單驗證元件 (ValidationProvider)

import { ValidationObserver, ValidationProvider, extend, localize, configure } from 'vee-validate';
import TW from 'vee-validate/dist/locale/zh_TW.json'
import * rules from 'vee-validate/dist/rules';

Object.keys(rules).forEach((rule) => {
  extend(rule, rules[rule]);
});

localize('zh_TW', TW);

Vue.component('ValidationObserver', ValidationObserver)
Vue.component('ValidationProvider', ValidationProvider)

configure({
  classes: {
    valid: 'is-valid',
    invalid: 'is-invalid'
  }
});

步驟三

  1. 建立 validation-provider 元件:
    • rules 帶上驗證的規則,規則列表可參考。注意:規則之間不需要帶上空白鍵。
    • v-slot 帶上預計回傳的回饋內容,常用的可參考下方範例,完整列表可參考
  2. 建立 input 欄位內容
  3. 將回饋帶至驗證 (v-slot 的內容)
<validation-provider rules="required|email" v-slot="{ errors }">
  <!-- 輸入框 -->
  <label for="email">Email</label>
  <input id="email" type="email" name="email" v-model="email"
      class="form-control">
  <!-- 錯誤訊息 -->
  <span>{‌{ errors[0] }}</span>
</validation-provider>

備註:v-slot 稱為插槽 (Vue 的元件語法之一),可以將驗證結果的回饋資料直接帶入於區塊中,相關概念可參考:

作用域插槽

步驟四

加入樣式,JavaScript 加入 Bootstrap 樣式設定,可使用以下設定,或參考官方文件

// Class 設定檔案
VeeValidate.configure({
  classes: {
    valid: 'is-valid',
    invalid: 'is-invalid',
  }
});

將 HTML 的部分進行更新

  • v-slot 增加 classes
  • input 增加 :class=”classes”
<validation-provider rules="required|email" v-slot="{ errors, classes }">
  <!-- 輸入框 -->
  <label for="email">Email</label>
  <input id="email" type="email" name="email" v-model="email"
    class="form-control" :class="classes">
  <!-- 錯誤訊息 -->
  <span class="invalid-feedback">{‌{ errors[0] }}</span>
</validation-provider>

步驟五

增加多國語系的方法

// 匯入語系檔案
import zh_TW from './zh_TW.js';
 
// 加入至 VeeValidate 的設定檔案
VeeValidate.localize('tw', zh_TW);

範例 (不包含多國語系)

如何為完整表單進行驗證

步驟一

建立 validation-observer 元件:v-slot 帶上預計回傳的回饋內容,常用的可參考下方範例,完整列表可參考

<validation-observer v-slot="{ invalid }">
</validation-observer>

步驟二

加入,方法可參考上篇的 「如何為單一表單(input)進行驗證」

步驟三

加入 <form> 標籤及 <button> 標籤:

  • 表單送出改為使用 form submit 的方法
  • 送出表單使用 submit 的方法,如果驗證未通過則 disabled 該按鈕
<!-- validation-observer 驗證整體表單 -->
<validation-observer v-slot="{ invalid }">
  <!-- 表單送出改為使用 form submit 的方法 -->
  <form @submit.prevent="submitForm">
    <validation-provider rules="required|email" class="form-group" tag="div" v-slot="{ errors, classes, passed }">
      ...
    </validation-provider>
    <!-- 送出表單使用 submit 的方法,如果驗證未通過則 disabled 該按鈕 -->
    <button type="submit" class="btn btn-primary" :disabled="invalid">送出表單</button>
  </form>
</validation-observer>

完整範例

使用 JS 觸發驗證

  1. <v-slot> 加入 handleSubmit
  2. 表單送出改為 handleSubmit(自訂方法)
<validation-observer v-slot="{ handleSubmit }">
  <form @submit.prevent="handleSubmit(submitForm)">
    ...
    <button type="submit" class="btn btn-primary">送出表單</button>
  </form>
</validation-observer>

完整範例

驗證補充說明

由於驗證套件更新
所以方法略有些微調整
以下感謝 洪同學提供

vee-validate 更改語系的方法在 2.1.0-beta.24 版之後已經更改了,因此照著這個影片實作會無法中文化,以下是由老師爬 stack overflow 以及官方範例後的結果,希望能幫助到遇到同樣問題的大家。

  1. 安裝 vue-i18n
  2. 在 main.js 中將 vue-i18n import 進來
  3. 將 VeeValidate.Validator.localize(‘zh_TW’, zhTWValidate) 及 Vue.use(VeeValidate) 刪除,並加入下列程式碼
  4. 在 Vue 物件中新增 i18n
// 1. 在 terminal 中輸入
npm install vue-i18n --save
// 2. main.js
import VueI18n from 'vue-i18n';
Vue.use(VueI18n);
// 3.
const i18n = new VueI18n({
  locale: 'zhTW'
});
Vue.use(VeeValidate, {
  i18n,
  dictionary: {
    zhTW
  }
});
// 4.
new Vue({
  i18n,
  el: '#app',
  components: { App },
  template: '<App/>',
  router,
})

結帳頁面製作

操作與講解

  1. 最後這個部分,我們要來做結帳的頁面,那我們先隨意加入一些商品到購物車內。
  2. 接下來我們到下面填寫表單的地方,我們把表單填寫完成,到目前這個階段我們都已經完成了,接下來我們按下送出訂單,送出訂單之後它會到達這個連結,它是 CustomerCheckout、然後後面會帶上這筆訂單的 id。那這筆訂單會呈現這個商品的資料、以及用戶所填寫的內容,用戶確認說這筆訂單就是他所填寫的部分,下面還會有一個付款狀態,那目前這個付款狀態是尚未付款的狀態。
  3. 當我們按下確認付款之後,他這個付款狀態也會有所更動。接下來我們按下確認付款,按下確認付款之後,你可以看到這個連結並沒有做變換,但是它會因為這筆訂單他已經付款完成、然後就把下面的文字做一些切換,並且把這個付款的連結把它拿掉。當這個付款完成之後,這個訂單列表它的付款狀態也會呈現已付款,那我們再回到上一個頁面。
  4. 最後這個章節主要介紹怎麼製作這個頁面,那我們先來看一下API。API的部分主要是有兩個、一個是取得某一筆特定的訂單,我們剛剛這個連結後面其實有帶上這筆訂單的id,我們就可以透過這筆id將特定的訂單內容把它取回來,那取得這筆訂單之後,我們就可以把這筆訂單的資料呈現出來、還有裡面有一個很重要的值就是它到底付款了沒,再往下我們可以看到結帳付款,主要觸發這個API就會將上面這筆訂單轉為已經付款完成的狀態。
  5. 我們來開始製作這個章節,在這個章節的部分,老師一樣會把HTML的Template直接提供給同學,那所以這個地方,同學不需要重刻這段HTML,我們在CustomerOrders.vue這裡其實還少一個步驟,就是我們在付款完成的時候,其實我們並沒有做跳頁,所以我們現在要先把另外一個checkout頁面先把它完成。
  6. 那我們在這個部分,我們就先新增一個檔案,叫做CustomerCheckout.vue,那我們先準備一個<template>裡面是<div>結帳確認</div>,這個頁面完成之後,我們再到達Vue router的index.js,我們這個下面有另外一個是CustomerOrder,我們先複製一下這個物件,這個物件我們要做為載入另外一個Checkout的頁面使用,所以我們現在要把CustomerCheckout把它載進來、然後接下來我們將這個頁面放到我們這個元件上面來,我們在加上這個連結之後它還沒有完成,後面記得帶上/:orderId,完成之後我們就把這個連結(customer_checkout)先把它複製起來,然後我們回到這個CustomerOrders.vue的地方,我們在先前建立訂單的時候,這裡有收到一個response,那我們這裡先判斷一下 if (response.data.success),我們這筆訂單如果確定建立完成之後,我們將這個頁面 vm.$router,這段我們在Vue router的課程有介紹過,我們可以使用router來轉換它的頁面,那麼router在轉換的時候,我們就可以把customer_checkout、然後後面再帶上這個order的id(${response.data.orderId}),這裡應該使用反引號才是正確的。
  7. 現在我們來試試看這個頁面有沒有正確的跳轉。確認購物車有資料,那我們輸入表單內容、然後按下送出訂單,送出訂單之後,這裡就有轉到customer_checkout,並且後面有帶上這筆訂單的id,那有這筆訂單的id之後,我們就可以開始撰寫接下來的 script。
  8. 我們打開 CustomerCheckout.vue 、我們來開始撰寫 script 的部分,script 的話一開始就是 export default,物件一開始我們要先定義資料結構 data(),return 這個地方我們就可以先加入 orderId,我們要先把 orderId 取得之後,我們才能取得其他的內容,那要取得 orderId 的話,我們在一開始的 created() 就可以來做取得了,取得的時候我們就可以使用 this的orderId等於this,這是我們在先前router 的課程有介紹到的一個方法,我們可以用這個方式來取得網址上的參數,那最後的參數(params)是指這一段.orderId,最後這個 orderId 是對應我們路由所自定義的名稱,這裡要特別注意,當你這兩個名稱(orderId)如果輸入不一致的話,是沒有辦法正確取得的。那我們來看一下這個 orderId 有沒有取得回來,那透過 console 來看一下這個 orderId 有沒有取得回來,那看起來是有的,這裡的名稱會和我們路徑上的名稱是一致的,有 orderId 之後就可以把資料內容給取得回來了。那取得的資料方式跟先前的方法都一樣,所以我們可以先複製先前所寫好的方法,那在這裡加上 methods、然後它是個物件、然後 getOrder(),那把相同內容貼進來,貼進來之後我們再來對應一下 API,這段 API 是使用 api_path/order 然後再對應 order_id,所以我們把這個 order 加過來、order 後面再加上 orderId,那 orderId 的話我們剛已經存起來,在 vm.orderId,現在可以用 get 的方式把這筆資料內容存起來,那我們存起來之前先來確認一下這筆資料是不是我們要的、存檔,資料取得回來之後,我們打開看一下、它是存放在 data 的 data 內,這邊要特別注意在 success 的旁邊還有一個 order,所以它不是直接存放於這個 data 內,它是存放在 order 內。那在這個 order 內會有這個產品列表、以及我的訂單的資料,所以在這個地方我們就可以把這個註解打開、改成 vm.order、然後 response.data.order,將我們現成的 template 貼進來。
  9. 現成的 template 貼進來,但是有些行為我們還沒加進來,所以我們要先把它移掉、存檔試試看,存檔之後它會發現說 email undefined,但其實 email 有在上面,原因是因為這個 email 是放在 user 下,它這裡已經跨第二層了,如果要避免跳出這個錯誤,比較簡單的方式是在 order: {} 下(裡)再新增 user 這一層,那這樣子的話它就不會跳出這樣子的錯誤、重新整理一次,現在就沒有跳出這個錯誤,並且有把整個訂單的資料完整的呈現出來,那資料都完整的呈現出來之後,這裡有一個確認付款去,所以我們再補上確認付款去的行為就可以了,所以我們在下方就可以再做一個 payOrder() 的一個行為,這邊我們可以複製我們先前寫好的程式碼、然後來稍作調整,結帳付款是在 api_path 下有一個 pay、然後後面接上 order_id 就可以直接付款,那我們在這個部分就可以使用 api、然後後面加上 pay、以及 orderId。在這個地方行為要特別注意,我們使用的是 post 的行為,後面的話這一段(vm.order = response.data.order;)可以移除,接下來這個地方記得把這個 payOrder() 加到我們上面的表單來,我們可以使用 @submit.prevent=”payOrder”。那我們來送出一次表單試試看、重新整理、打開 console 然後按下確認付款去,那確認付款去這裡就會顯示付款完成,那我們顯示付款完成,但是其實我們這個訂單還是呈現尚未付款,所以在下方程式碼的部分也要稍微做一些調整,假設它付款成功之後,我們可以跳出一個訊息,那跳出訊息之外,我們還可以重新取得訂單資料,它還可以再重新取得一次訂單資料,我們再重新整理一次,這個時候它就會轉換成付款完成,並且把它的付款連結也把它拿掉,這個時候如果進入訂單列表,假設你這個畫面已經完成的狀態,你也可以看到這個商品已經被付款完成,那這個就是最後付款結帳頁面。
// 6. CustomerCheckout.vue
<template>
  <div>
    結帳確認
  </div>
</template>
// 6. router/index.js
import Vue from 'vue'
import Router from 'vue-router'
// 之後都不會用到,可以直接移除
// import HelloWorld from '@/components/HelloWorld'
// 把Dashboard給載進來
import Dashboard from '@/components/Dashboard'
// 把Login給載進來
import Login from '@/components/pages/Login'
// 把Product給載進來
import Products from '@/components/pages/Products'
// 把Order給載進來
import Orders from '@/components/pages/Orders'
// 把Coupon給載進來
import Coupons from '@/components/pages/Coupons'
// 把CustomerOrder給載進來
import CustomerOrder from '@/components/pages/CustomerOrders' 
// 把CustomerCheckout給載進來
import CustomerCheckout from '@/components/pages/CustomerCheckout' 

// 解決重複導航
const inCludPush = Router.prototype.push
Router.prototype.push = function push(location) {
  return inCludPush.call(this, location).catch(err => err)
}

Vue.use(Router)

export default new Router({
  linkActiveClass: 'active',
  routes: [
    // 重新導向
    {
      path: '*',
      // redirect: 'login',
      redirect: 'shop/customer_order',
    },
    // 之後都不會用到,可以直接移除
    // // 首頁的路徑
    // {
    //   path: '/',
    //   name: 'HelloWorld',
    //   component: HelloWorld,
    //   meta: { requiresAuth: true }
    // },
    // 新增一個登入的路徑
    {
      path: '/login',
      name: 'Login',
      component: Login,
    },
    // Dashboard的路徑
    {
      path: '/admin',
      name: 'Dashboard',
      component: Dashboard,
      // 設定 meta 是否需要驗證
      meta: { requiresAuth: true },
      children: [
        {
          path: 'products',
          name: 'Products',
          component: Products,
          meta: { requiresAuth: true },
        },
        // Orders的路徑
        {
          path: 'orders',
          name: 'Orders',
          component: Orders,
          meta: { requiresAuth: true },
        },
        // Coupons的路徑
        {
          path: 'coupons',
          name: 'Coupons',
          component: Coupons,
          meta: { requiresAuth: true },
        },
      ],
    },
    // 客戶購物的路徑(使用與Dashboard相同的模板)
    {
      // path: '/',
      // name: 'Dashboard',
      path: '/shop',
      name: 'DashboardCustomerOrder',
      component: Dashboard,
      // 設定 meta 是否需要驗證
      meta: { requiresAuth: true },
      children: [
        // CustomerOrder的路徑
        {
          path: 'customer_order',
          name: 'CustomerOrder',
          component: CustomerOrder,
        },
        // CustomerCheckout的路徑 
        {
          path: 'customer_checkout/:orderId',
          name: 'CustomerCheckout',
          component: CustomerCheckout,
        },
      ],
    },
  ]
})
結帳頁面製作範例程式碼
// index.js
import Vue from 'vue'
import Router from 'vue-router'
// 之後都不會用到,可以直接移除
// import HelloWorld from '@/components/HelloWorld'
// 把Dashboard給載進來
import Dashboard from '@/components/Dashboard'
// 把Login給載進來
import Login from '@/components/pages/Login'
// 把Product給載進來
import Products from '@/components/pages/Products'
// 把Order給載進來
import Orders from '@/components/pages/Orders'
// 把Coupon給載進來
import Coupons from '@/components/pages/Coupons'
// 把CustomerOrder給載進來
import CustomerOrder from '@/components/pages/CustomerOrders' 
// 把CustomerCheckout給載進來
import CustomerCheckout from '@/components/pages/CustomerCheckout' 



Vue.use(Router)

export default new Router({
  linkActiveClass: 'active',
  routes: [
    // 重新導向
    {
      path: '*',
      // redirect: 'login',
      redirect: 'shop/customer_order',
    },
    // 之後都不會用到,可以直接移除
    // // 首頁的路徑
    // {
    //   path: '/',
    //   name: 'HelloWorld',
    //   component: HelloWorld,
    //   meta: { requiresAuth: true }
    // },
    // 新增一個登入的路徑
    {
      path: '/login',
      name: 'Login',
      component: Login,
    },
    // Dashboard的路徑
    {
      path: '/admin',
      name: 'Dashboard',
      component: Dashboard,
      // 設定 meta 是否需要驗證
      meta: { requiresAuth: true },
      children: [
        {
          path: 'products',
          name: 'Products',
          component: Products,
          meta: { requiresAuth: true },
        },
        // Orders的路徑
        {
          path: 'orders',
          name: 'Orders',
          component: Orders,
          meta: { requiresAuth: true },
        },
        // Coupons的路徑
        {
          path: 'coupons',
          name: 'Coupons',
          component: Coupons,
          meta: { requiresAuth: true },
        },
      ],
    },
    // 客戶購物的路徑(使用與Dashboard相同的模板)
    {
      // path: '/',
      // name: 'Dashboard',
      path: '/shop',
      name: 'DashboardCustomerOrder',
      component: Dashboard,
      // 設定 meta 是否需要驗證
      meta: { requiresAuth: true },
      children: [
        // CustomerOrder的路徑
        {
          path: 'customer_order',
          name: 'CustomerOrder',
          component: CustomerOrder,
        },
        // CustomerCheckout的路徑 
        {
          path: 'customer_checkout/:orderId',
          name: 'CustomerCheckout',
          component: CustomerCheckout,
        },
      ],
    },
  ]
})
// CustomerOrders.vue
<template>
  <div>
    <loading :active.sync="isLoading"></loading>
    <div class="row mt-4">
      <div class="col-md-4 mb-4" v-for="item in products" :key="item.id">
        <div class="card border-0 shadow-sm">
          <div style="height: 150px; background-size: cover; background-position: center"
            :style="{backgroundImage: `url(${item.imageUrl})`}">
          </div>
          <div class="card-body">
            <span class="badge badge-secondary float-right ml-2">{{ item.category }}</span>
            <h5 class="card-title">
              <a href="#" class="text-dark">{{ item.title }}</a>
            </h5>
            <p class="card-text">{{ item.content }}</p>
            <div class="d-flex justify-content-between align-items-baseline">
              <div class="h5" v-if="!item.price">{{ item.origin_price }}</div>
              <del class="h6" v-if="item.price">原價 {{ item.origin_price }} 元</del>
              <div class="h5" v-if="item.price">現在只要 {{ item.price }} 元</div>
            </div>
          </div>
          <div class="card-footer d-flex">
            <button type="button" class="btn btn-outline-secondary btn-sm"
              @click="getProduct(item.id)">
              <i class="fas fa-spinner fa-spin" v-if="status.loadingItem === item.id"></i>
              查看更多
            </button>
            <button type="button" class="btn btn-outline-danger btn-sm ml-auto"
              @click="addtoCart(item.id)">
              <i class="fas fa-spinner fa-spin" v-if="status.loadingItem === item.id"></i>
              加到購物車
            </button>
          </div>
        </div>
      </div>
    </div>
    <!-- Modal CustomerOrders -->
    <div class="modal fade" id="productModal" tabindex="-1" role="dialog"
      aria-labelledby="exampleModalLabel" aria-hidden="true">
      <div class="modal-dialog" role="document">
        <div class="modal-content">
          <div class="modal-header">
            <h5 class="modal-title" id="exampleModalLabel">{{ product.title }}</h5>
            <button type="button" class="close" data-dismiss="modal" aria-label="Close">
              <span aria-hidden="true">&times;</span>
            </button>
          </div>
          <div class="modal-body">
            <img :src="product.imageUrl" class="img-fluid" alt="">
            <blockquote class="blockquote mt-3">
              <p class="mb-0">{{ product.content }}</p>
              <footer class="blockquote-footer text-right">{{ product.description }}</footer>
            </blockquote>
            <div class="d-flex justify-content-between align-items-baseline">
              <div class="h4" v-if="!product.price">{{ product.origin_price }} 元</div>
              <del class="h6" v-if="product.price">原價 {{ product.origin_price }} 元</del>
              <div class="h4" v-if="product.price">現在只要 {{ product.price }} 元</div>
            </div>
            <select name="" class="form-control mt-3" v-model="product.num">
              <option :value="num" v-for="num in 10" :key="num">
                選購 {{ num }} {{ product.unit }}
              </option>
            </select>
          </div>
          <div class="modal-footer">
            <div class="text-muted text-nowrap mr-3">
              小計 <strong>{{ product.num * product.price }}</strong> 元
            </div>
            <button type="button" class="btn btn-primary"
              @click="addtoCart(product.id, product.num)">
              <!-- <i class="fas fa-spinner fa-spin" v-if="product.id === status.loadingItem"></i> -->
              加到購物車
            </button>
          </div>
        </div>
      </div>
    </div>
    <!-- 購物車列表 -->
    <div class="my-5 row justify-content-center">
      <div class="col-md-6">
        <table class="table">
          <thead>
            <th></th>
            <th>品名</th>
            <th>數量</th>
            <th>單價</th>
          </thead>
          <tbody>
            <tr v-for="item in cart.carts" :key="item.id" v-if="cart.carts">
              <td class="align-middle">
                <button type="button" class="btn btn-outline-danger btn-sm"
                  @click="removeCartItem(item.id)">
                  <i class="far fa-trash-alt"></i>
                </button>
              </td>
              <td class="align-middle">
                {{ item.product.title }}
                <div class="text-success" v-if="item.coupon">
                  已套用優惠券
                </div>
              </td>
              <td class="align-middle">{{ item.qty }}/{{ item.product.unit }}</td>
              <td class="align-middle text-right">{{ item.final_total }}</td>
            </tr>
          </tbody>
          <tfoot>
            <tr>
              <td colspan="3" class="text-right">總計</td>
              <td class="text-right">{{ cart.total }}</td>
            </tr>
            <tr v-if="cart.final_total !== cart.total">
              <td colspan="3" class="text-right text-success">折扣價</td>
              <td class="text-right text-success">{{ cart.final_total }}</td>
            </tr>
          </tfoot>
        </table>
        <div class="input-group mb-3 input-group-sm">
          <input type="text" class="form-control" v-model="coupon_code" placeholder="請輸入優惠碼">
          <div class="input-group-append">
            <button class="btn btn-outline-secondary" type="button" @click="addCouponCode">
              套用優惠碼
            </button>
          </div>
        </div>
      </div>
    </div>
    <!-- 建立訂購表單 -->
    <div class="my-5 row justify-content-center">
      <form class="col-md-6" @submit.prevent="createOrder">
        <div class="form-group">
          <label for="useremail">Email</label>
          <input type="email" class="form-control" name="email" id="useremail"
            v-validate="'required|email'"
            :class="{'is-invalid': errors.has('email')}"
            v-model="form.user.email" placeholder="請輸入 Email">
          <span class="text-danger" v-if="errors.has('email')">
            {{ errors.first('email') }}
          </span>
        </div>
      
        <div class="form-group">
          <label for="username">收件人姓名</label>
          <input type="text" class="form-control" name="name" id="username"
            :class="{'is-invalid': errors.has('name')}"
            v-model="form.user.name" v-validate="'required'" placeholder="輸入姓名">
          <span class="text-danger" v-if="errors.has('name')">姓名必須輸入</span>
        </div>
      
        <div class="form-group">
          <label for="usertel">收件人電話</label>
          <input type="tel" class="form-control" name="tel" id="usertel"
            :class="{'is-invalid': errors.has('tel')}"
            v-model="form.user.tel" v-validate="'required'" placeholder="請輸入電話">
          <span class="text-danger" v-if="errors.has('tel')">電話欄位不得留空</span>
        </div>
      
        <div class="form-group">
          <label for="useraddress">收件人地址</label>
          <input type="text" class="form-control" name="address" id="useraddress"
            :class="{'is-invalid': errors.has('address')}"
            v-model="form.user.address" v-validate="'required'" placeholder="請輸入地址">
          <span class="text-danger" v-if="errors.has('address')">地址欄位不得留空</span>
        </div>
      
        <div class="form-group">
          <label for="comment">留言</label>
          <textarea name="" id="comment" class="form-control" cols="30" rows="10" v-model="form.message"></textarea>
        </div>
        <div class="text-right">
          <button class="btn btn-danger">送出訂單</button>
        </div>
      </form>
    </div>
  </div>
</template>

<script>
import $ from 'jquery';

export default {
  data() {
    return {
      products: [],
      product: {},
      status: {
        loadingItem: '',
      },
      form: {
        user: {
          name: '',
          email: '',
          tel: '',
          address: '',
        },
        message: '',
      },
      cart: {},
      isLoading: false,
      coupon_code: '',
    };
  },
  methods: {
    getProducts() {
      const vm = this;
      const url = `${process.env.APIPATH}/api/${process.env.CUSTOMPATH}/products`;
      vm.isLoading = true;
      this.$http.get(url).then((response) => {
        vm.products = response.data.products;
        // console.log(response);
        vm.isLoading = false;
      });
    },
    // 取得單一產品
    getProduct(id) {
      const vm = this;
      const url = `${process.env.APIPATH}/api/${process.env.CUSTOMPATH}/product/${id}`;
      // vm.isLoading = true;
      vm.status.loadingItem = id;
      this.$http.get(url).then((response) => {
        vm.product = response.data.product;
        $('#productModal').modal('show');
        // console.log(response);
        // vm.isLoading = false;
        vm.status.loadingItem = '';
        vm.product.num = 1; // 所有商品初始值設置為 1
      });
    },
    // 加入購物車
    addtoCart(id, qty = 1) {
      const vm = this;
      const url = `${process.env.APIPATH}/api/${process.env.CUSTOMPATH}/cart`;
      // vm.isLoading = true;
      vm.status.loadingItem = id;
      const cart = {
        product_id: id,
        qty,
      };
      this.$http.post(url, { data: cart }).then((response) => {
        console.log(response);
        // vm.isLoading = false;
        vm.status.loadingItem = '';
        vm.getCart();
        $('#productModal').modal('hide');
      });
    },
    // 取得購物車的內容
    getCart() {
      const vm = this;
      const url = `${process.env.APIPATH}/api/${process.env.CUSTOMPATH}/cart`;
      vm.isLoading = true;
      this.$http.get(url).then((response) => {
        // vm.products = response.data.products;
        vm.cart = response.data.data;
        // console.log(response);
        vm.isLoading = false;
      });
    },
    // 刪除購物車品項
    removeCartItem(id) {
      const vm = this;
      const url = `${process.env.APIPATH}/api/${process.env.CUSTOMPATH}/cart/${id}`;
      vm.isLoading = true;
      this.$http.delete(url).then(() => {
        vm.getCart();
        vm.isLoading = false;
      });
    },
    // 新增優惠碼
    addCouponCode() {
      const vm = this;
      const url = `${process.env.APIPATH}/api/${process.env.CUSTOMPATH}/coupon`;
      const coupon = {
        code: vm.coupon_code,
      };
      vm.isLoading = true;
      this.$http.post(url, { data: coupon }).then((response) => {
        // console.log(response);
        vm.getCart();
        vm.isLoading = false;
      });
    },
    // 建立訂購表單
    createOrder() {
      const vm = this;
      const url = `${process.env.APIPATH}/api/${process.env.CUSTOMPATH}/order`;
      const order = vm.form;
      // vm.isLoading = true;
      this.$validator.validate().then((result) => {
        if (result) {
          this.$http.post(url, { data: order }).then((response) => {
            console.log('訂單已建立', response);
            if (response.data.success) {
              vm.$router.push(`/shop/customer_checkout/${response.data.orderId}`);
            }
            // vm.getCart();
            vm.isLoading = false;
          });
        } else {
          console.log('欄位不完整');
        }
      });
    },
  },
  created() {
    this.getProducts();
    this.getCart();
  },
};
</script>
// CustomerCheckout.vue
<template>
  <div class="my-5 row justify-content-center">
    <form class="col-md-6" @submit.prevent="payOrder">
      <table class="table">
        <thead>
          <th>品名</th>
          <th>數量</th>
          <th>單價</th>
        </thead>
        <tbody>
          <tr v-for="item in order.products" :key="item.id">
            <td class="align-middle">{{ item.product.title }}</td>
            <td class="align-middle">{{ item.qty }}/{{ item.product.unit }}</td>
            <td class="align-middle text-right">{{ item.final_total }}</td>
          </tr>
        </tbody>
        <tfoot>
          <tr>
            <td colspan="2" class="text-right">總計</td>
            <td class="text-right">{{ order.total }}</td>
          </tr>
        </tfoot>
      </table>

      <table class="table">
        <tbody>
          <tr>
            <th width="100">Email</th>
            <td>{{ order.user.email }}</td>
          </tr>
          <tr>
            <th>姓名</th>
            <td>{{ order.user.name }}</td>
          </tr>
          <tr>
            <th>收件人電話</th>
            <td>{{ order.user.tel }}</td>
          </tr>
          <tr>
            <th>收件人地址</th>
            <td>{{ order.user.address }}</td>
          </tr>
          <tr>
            <th>付款狀態</th>
            <td>
              <span v-if="!order.is_paid">尚未付款</span>
              <span v-else class="text-success">付款完成</span>
            </td>
          </tr>
        </tbody>
      </table>
      <div class="text-right" v-if="order.is_paid === false">
        <button class="btn btn-danger">確認付款去</button>
      </div>
    </form>
  </div>
</template>

<script>
export default {
  data() {
    return {
      order: {
        user: {},
      },
      orderId: '',
    };
  },
  methods: {
    getOrder() {
      const vm = this;
      const url = `${process.env.APIPATH}/api/${process.env.CUSTOMPATH}/order/${vm.orderId}`;
      vm.isLoading = true;
      this.$http.get(url).then((response) => {
        vm.order = response.data.order;
        console.log(response);
        vm.isLoading = false;
      });
    },
    payOrder() {
      const vm = this;
      const url = `${process.env.APIPATH}/api/${process.env.CUSTOMPATH}/pay/${vm.orderId}`;
      vm.isLoading = true;
      this.$http.post(url).then((response) => {
        console.log(response);
        if (response.data.success) {
          vm.getOrder();
        }
        vm.isLoading = false;
      });
    },
  },
  created() {
    this.orderId = this.$route.params.orderId;
    this.getOrder();
    console.log(this.orderId);
  },
};
</script>

最終作業寄送變更

依照課程指示。

最終作業說明

操作與講解

  1. 這個專案到目前為止都完成了差不多了,目前我們都是使用 npm run dev 的方式在運行這個專案,但我們不可能使用這個 webpack 直接給別人看,我們必定要編譯完成之後上傳到正式的伺服器。
  2. 在做最後的編譯之前,跟同學分享一些小地方。我們到目前為止都是使用 npm run dev,那這裡面所使用的設定檔是使用 config/dev.env.js 這個檔案,裡面就包含 APIPATH、以及 CUSTOMPATH。但如果說你在編譯的時候沒有把這些設定檔加到 config/prod.env.js 裡面的話,你的正式釋出的檔案是不會包含這些內容的,所以在這裡,你在釋出前記得把 APIPATH、以及 CUSTOMPATH 把它加進來,加進來之後你在進行 npm run build 才能把這些內容都加進去。
  3. 現在來執行 npm run build,執行完之後這裡就有寫到這些檔案必需在 HTTP server 下面才能運行。如果你直接打開的話,在 file 的路徑下是沒有辦法運行的,接下來我們把這個資料夾打開,在上傳前建議還是檢查一下這個檔案有沒有問題,這個時候一樣就可以把它丟在 VSCode 裡面來,然後透過我們先前所介紹的 preview on web server 就可以快速地檢視這個檔案。現在是登入的狀態,我先把它登出。登出之後、接下來(在網址)輸入 adminProducts 是沒有辦法進去的,我們就一定要輸入帳密,輸入帳密之後才能正確進來我們的 admin 後台。
  4. 這裡再跟同學分享另外一個小地方,假設我們所釋出的檔案並不是在這個根目錄下,就不是在這個 Domain 下的話,我們先把後面的路徑刪掉。假設我們所釋出的路徑不是直接在這個 Domain 下,而是在其他的路徑下,像是這邊我再加一個 vue-testing,我是加在這個 vue-testing 下的話,我們就要另外做一些些調整。
  5. 調整的方式我們可以把 config/index.js 打開,裡面有一個 build、然後下方可以找到 assetPublicPath,那這個是針對編譯後的路徑來做調整,所以這個時候如果我把 vue-testing 這個路徑加進來,再運行 npm run build 我們就可以在這個目錄下運行,假設如果你沒有加入這一段的話,它就只能在根目錄的 Domain 下才能運行這個專案。那這裡就跟同學分享一下這一段。
  6. 最後我們在講一下作業的部分,最後的作業的部分就有分為前台跟後台,那前台跟後台同學都可以選擇使用老師所提供的版型、 Bootstrap 的版型、或自定義版型都是可以的。當然也希望同學可以經營成屬於自己主題的購物網站,那圖庫的話,你可以使用 unsplash 這個網站圖庫,細節的話有些部分希望同學可以自己做加強,像是錯誤回饋的部分,同學可以加上連線、以及表單的錯誤回饋…等等,然後還有增加 Vue 的 Component 使用,另外還有一個、希望同學的作品都可以有增加多一點的獨特性,所以盡量去自定義 Bootstrap 的樣式、色彩,然後不要只使用預設的樣式。
  7. 那作業交送的方式有兩種,一種是直接寄給我們,寄給我們的話請不要包含 node_modules 這個資料夾,這個資料夾非常的大,而且你上傳給我的話,我還是必需要重新安裝。那另外一個比較推薦的做法是你直接將這個 dist 資料夾上傳到 GitHub Pages,那前後台的原始碼上傳到 GitHub,相關連結直接提供到 Udemy 問答區,你都提供完之後,老師就會來做檢視。如果沒有問題的話,大家來開始練習吧。
// config/prod.env.js

'use strict'
module.exports = {
  NODE_ENV: '"production"',
  APIPATH: '"https://vue-course-api.hexschool.io"',
  CUSTOMPATH: '"geehsu"',
}

最終作業文件

最終作業說明

注意:每個最終作業限制檢視三次,講師會在每次檢視後回覆的剩餘批改次數。

在第一次提交時:

  • 請提供 GitHub 及 GitHub Page 兩種版本
  • 確保 GitHub 上的程式碼下載後可正確運行,並修正 Console 所有錯誤

重新提交檢視時:

  • 請確保修正完講師的所有建議
  • 如有疑問可回覆詢問講師 (詢問不會算次數)
  • 若是課程觀念問題請提交至線上問答區

課程中提供完整的 API 與後台教學
同學的作業是完成此作品,並上傳至 Github Pages
那麼同學就來完成此作業吧

  • 後台:可使用 Bootstrap 的 Dashboard 版型、或自選撰寫 CSS、上網下載其他版型皆可
  • 前台:可使用課程提供的免費版型或自定義版型
  • 經營成屬於自己主題的購物網站
  • 圖庫可用:unsplash
  • 細節要求:
    • 增加錯誤回饋如:連線錯誤、表單錯誤等錯誤訊息提示
    • 增加 Vue 元件的使用,如頁碼、Modal、卡片都可運用 Component 練習
    • 自定義 Bootstrap 的樣式、色彩,盡可能不使用預設的樣式
  • 作業前後台需要釋出至 Github Pages 或寄送到 service@hexschool.com
    另外必需提供原始碼給老師查閱(不然有錯很難抓阿~)

作業寄送:

  1. 推薦做法:
    編譯後的檔案(dist) 上傳至 Github Pages
    前後台原始碼上傳至 Github
    將相關連結提供到任務系統回報任務

    Github Pages 基礎部屬教學 – 連結

如果作業希望做為應徵作品集使用
可在回覆時增加:希望作為履歷作品集的一部分

作業審核標準會不太一樣,許多內容將針對畫面處理給予建議
希望作為履歷作品集的一部分盡可能以 “接近實際運作的網站” 經營,
以下部分可先多加強

  • 網頁主要文案 (怎麼吸引用戶在這個網站購買)
  • 產品文案可多加調整 (吸引用戶買該產品)
  • 網頁可增加互動性內容 (我的最愛、類似品項、優惠券介紹…)

可參考同學們的作品 (連結頁面下方) – 連結

Github 部屬時,如果出現資源路徑錯誤
則需要調整設定檔的路徑

// ./config/index.js

build: {
  // ...
  assetsPublicPath: '/view', // 修改這裡得值,使其對應 Github Pages 的路徑

}

接下來輸出後,再把 dist 資料夾推到 gh-pages 分支

如果使用 npm run build 時遇到類似下方的錯誤訊息:

⠋ building for production.../.../node_modules/last-call-webpack-plugin/src/index.js:170
  compiler.hooks.compilation.tap(
    ^
TypeError: Cannot read property 'compilation' of undefined

因為部分套件更新導致錯誤,可以打開 package.json 調整套件版號如下:

"optimize-css-assets-webpack-plugin": "3.2.0",

此講座的資源

  • shoppingCartTemplate.zip (在 Udemy 課程內下載)