# 甜點電商網站

## :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
},
```
---
### 單一產品資料顯示

* 在商品陳列於商品頁面時,並不會呈現完整的資料,若要查看完整的產品資訊就需要點擊**查看更多**,重新取得單一比資料。
```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>
```

* 方法二:點擊**查看更多**後進入單一產品頁面選擇產品數量,並點選**加到購物車**。

```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: 頁面介紹:後台
### 建立新產品及修改商品資料

* 取得遠端資料,再將資料存入`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)'`。