# 甜點電商網站 ![](https://i.imgur.com/uqm3yBt.jpg) ## :memo: 網站介紹 透過Vue框架完成模擬的甜電商網站。專案包含前台的商品及結帳頁面,以及後台的商品資料建立、訂單資料確認、串接後端API。 ## :memo: 使用技術 1.HTML/CSS 網頁切版 2.Bootstrap 4 3.Vue.js 4.JavaScript 5.AJAX / Plugin ## :memo: 引用遠端資料API 用Vue cli建立好webpack環境後,下載vue-axios套件用來取的遠端資料,並將相關指令貼至main.js的檔案中。 將環境重新運行後,在app.vue檔案中試著取得遠端資料,取得資料的方式可以參考axio提供的方式。 ```javascript= created() { const api = `${process.env.APIPATH}/api/${process.env.CUSTOMPATH}/products`; // api伺服器的路徑,所申請的api PATH, this.$http.get(api).then((rs) => { console.log(rs.data) }) } // created取得遠端資料 ``` :::info :bulb: **API改以環境變數來取得** ::: ## :memo: Async/Await 同步/非同步 **- 傳統的Promise寫法在串接多個非同步行為時會太冗長,此時就可以使用Async/Await的方式精簡改寫。** **- Promise原本會被放進事件佇列,但改成async函式加上await就會變成逐行執行。** ```javascript= function promiseFn(num){ return new Promise ((resolve,reject)=>{ setTimeout(()=>{ if(num){ resolve('成功'); }else{ reject(new Error('失敗')); } },0); }); }; const asyncFn = async(num)=>{ try{ const res = await promiseFn(num); return res; }catch(error){ throw new Error('失敗'); } }; asyncFn(num) .then((res)=>{ console.log(res); }); .catch((err)=>{ console.log('錯誤',error); }) ``` :::info :bulb: **Try : 捕捉正確的結果。** :bulb: **Throw : 拋出錯誤的結果。** ::: **- JavaScript是 ==單執行緒(single thread)== 的程式,為多個任務分開執行。** * 同步:依事件的順序執行。 * 非同步:遇到非同步的程式碼(任務)時會先將其放置 ==事件佇列== ,等其他同步執行的任務執行完後再執行事件佇列中的任務。 ```javascript= function eatBreakfast(){ alert('吃早餐') }; function washPlate(){ alert('洗碗盤') }; function callSomeone(someone){ alert('打給' + someone) }; setTimeout (function(){ alert(someone + '回電') },3000) function DOWORK(someone){ var aunit = '阿姨'; eatBreakfast(); callSomeone(aunit); washPlate(); }; doWork(); ``` > setTimeout為非同步行為,先移至事件佇列。無論如何調整秒數都不會優先執行,仍須等到所有程式碼同步執行完才會執行事件佇列中的程式碼。 * 執行堆疊順序:第一層 ==doWork==;第二層 ==吃早餐== ==打電話== ==洗碗==,執行完再進入事件佇列執行==等待回電==。 ## :memo: 頁面介紹:前台 **- 產品分類** {%youtube C4KdjcZ4QPY %} ```javascript= <div class="list-group sticky-top"> <a class="list-group-item list-group-item-action active btn-warning" data-toggle="list" href="#list-gift" @click.prevent="getProducts"> <i class="fa fa-suitcase" aria-hidden="true"></i>所有商品</a> <a v-for="item in category" class="list-group-item list-group-item-action btn-warning" data-toggle="list" href="#list-gift" @click.prevent="changeCategory(item)"> <i class="fa fa-gift" aria-hidden="true"></i> {{ item }}</a> </div> ``` ```javascript= methods: { changeCategory(str) { const vm = this; vm.filter.str = str; vm.getProductsfilt(); //getProductsfilt 是取得商品的方法 vm.isLoading = false; this.$http.get(api).then((rs) => { vm.isLoading = false; vm.products = rs.data.products; vm.pagination = rs.data.pagination; vm.category = vm.products.map(m => m.category).filter((e, i, arr) => arr.indexOf(e) === i) }) } } ``` :::info :bulb: **參考文章:https://medium.com/javascript%E5%88%9D%E5%BF%83%E8%80%85%E8%B8%8F%E9%9B%AA%E5%B0%8B%E6%A2%85/vue-js-%E7%94%A8-cilck-%E4%BA%8B%E4%BB%B6%E9%81%94%E5%88%B0%E5%88%87%E6%8F%9B%E5%95%86%E5%93%81%E5%88%86%E9%A1%9E%E7%9A%84%E5%8A%9F%E8%83%BD-18d4a012a901** ::: **- 鍵字搜尋** 用compute及filter來完成關鍵字的搜尋,並建立新元件來呈現搜尋出來的資料。 :::info :bulb: **參考文章:https://hackmd.io/@Zihyin/B1SwD-Gmq** ::: ```javascript= computed: { searchData: function () { var search = this.search; if (search) { return this.products.filter(function (product) { return Object.keys(product).some(function (key) { return String(product[key]).toLowerCase().indexOf(search) > -1 }) }) } return this.products; }, }, getProductsfilt() { const vm = this; const url = `${process.env.APIPATH}/api/${process.env.CUSTOMPATH}/products`; vm.isLoading = false; this.$http.get(url).then((response) => { if (vm.filter.str !== '全部商品') { let filterPro = response.data.products.filter(function (item) { return item.category == vm.filter.str; }); vm.products = filterPro; } else { vm.products = response.data.products; } }); }, searchProducts() { this.products = this.filterProducts }, ``` --- ### 單一產品資料顯示 ![](https://i.imgur.com/gJyPuza.jpg) * 在商品陳列於商品頁面時,並不會呈現完整的資料,若要查看完整的產品資訊就需要點擊**查看更多**,重新取得單一比資料。 ```javascript= getProduct(id) { const vm = this; const url = `${process.env.APIPATH}/api/${process.env.CUSTOMPATH}/product/${id}`; vm.status.loadingItem = id; this.$router.push(`/product/${id}`); this.$http.get(url).then((response) => { vm.product = response.data.product; // 將資料讀取進來 vm.status.loadingItem = ''; }); } ``` * 由於點擊查看更多後會跳至新的頁面,就需要在router中建立單一商品的路徑及呈現資料頁面的元件,並在**查看更多**的按鍵上綁定` <router-link :to="{ name: 'ToProductDetail', params: { id: item.id } }">` ```vue= { name: 'ToProductDetail', path: '/product/:id', component: ToProductDetail, }, ``` --- ### 選購產品及購物車 {%youtube 3aRoq26V-MQ%} **- 選購產品** 選購產品的方式有兩個,點選產品加入購物車後,會傳入==商品的ID及數量==。 * 方法一:直接點擊商品頁面中產品下方的**加到購物車**。在**加到購物車**的按鍵用`v-on`監聽click事件並觸發`addtoCart`。 ```vue= <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> ``` ![](https://i.imgur.com/CWRmIse.jpg) * 方法二:點擊**查看更多**後進入單一產品頁面選擇產品數量,並點選**加到購物車**。 ![](https://i.imgur.com/ZohA3HG.jpg) ```vue= <select name="" class="form-control mt-3" v-model="product.num"> <option :value="num" v-for="num in 10" :key="num"> 選購 {{ num }} </option> </select> ``` ```javascript= addtoCart(id, qty = 1) { //此處使用ES6預設值的方法將qty預設為1,函式傳入時若無帶入qty,就會使用預設值。 const vm = this; const url = `${process.env.APIPATH}/api/${process.env.CUSTOMPATH}/cart`; vm.status.lodingItem = id; const cart = { product_id: id, qty, } //定義資料結構,傳入商品ID及數量 this.$http.post(url, { data: cart }).then((response) => { console.log(response); vm.status.lodingItem = ''; vm.getCart(); $('#productModal').modal('hide'); }); }, ``` **- 購物車icon旁的數量顯示** 用compute的方式監聽carts的陣列長度並回傳,就可以得知目前購物車中的商品數量。 ```javascript= <span class="badge badge-pill badge-danger">{{ cartNum }}</span> ``` ```javascript= computed: { cartNum() { return this.cart.carts.length ||0 }, ``` --- ## :memo: 頁面介紹:後台 ### 建立新產品及修改商品資料 ![](https://i.imgur.com/a7o7egf.jpg) * 取得遠端資料,再將資料存入`products:[]`中,因為要將資料存入自訂的變數中(products),必須用`const vm = this`,確保在http結束後可以把取回的資料再存回`vm`中。最後補上`created(){this.products}`才會觸發products事件。 ```javascript= 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; vm.isLoading = false; }); }, }, created() { this.getProducts(); }, ``` * 按下**建立新的產品**就會跳出Bootstrap的Modal元件,在data的部分新增`tempProduct:{}`儲存欄位送出的內容,並將tempProduct以`v-model`的方式綁定在各個欄位。 :::info :bulb: **備註 : v-model的主要功能為綁定雙向的資料,監聽輸入的資訊並更新。** ::: * 最後在**確認**的按鍵上以`v-on`的方式綁定click事件,並觸發`updateProduct()` ```javascript= openModal(isNew, item) { if (isNew) { this.tempProduct = {}; this.isNew = true; } else { this.tempProduct = Object.assign({}, item); this.isNew = false; } $('#productModal').modal('show'); }, updateProduct() { let api = `${process.env.APIPATH}/api/${process.env.CUSTOMPATH}/admin/product`;// 'https://vue-course-api.hexschool.io/api/chenn10814/products'; let httpMethod = 'post'; const vm = this; if (!vm.isNew) { api = `${process.env.APIPATH}/api/${process.env.CUSTOMPATH}/admin/product/${vm.tempProduct.id}`; httpMethod = 'put'; } this.$http[httpMethod](api, { data: vm.tempProduct }).then((rs) => { if (rs.data.success) { $('#productModal').modal('hide'); vm.getProducts(); } else { $('#productModal').modal('hide'); vm.getProducts(); } }) }, ``` * 在openModal判定資料的新舊 1. 傳入參數(isNew) 或item 2. 用判斷式,若為新增的話`this.tempProduct`就會等於一個空的物件並且`this.isNew`會等於true,表示為新增的資料。 3. 若不是新增的資料,`this.isNew`則等於false,`this.tempProduct`會等於`Object.assign({},item)`。 :::info :bulb: **備註 : 若直接撰寫`this.tempProduct = item`,因為物件傳參考的特性這兩個值會一模一樣,故在此會使用ES6的方式撰寫為`Object.assign({},item)`。用這個寫法可以將item寫入空的物件,也比免item與tempProduct有參考的特性。** ::: 4. 修改資料的方式是用put。 5. 在編輯的按鍵上綁定`@click='openModal(false,item)'`。