從零開始,搭建一個簡單的購物平台(十九)前端商城部分


從零開始,搭建一個簡單的購物平台(十八)前端商城部分:
https://blog.csdn.net/time_____/article/details/108918489
項目源碼(持續更新):https://gitee.com/DieHunter/myCode/tree/master/shopping

上篇文章后,前端商城部分基本功能已實現,包括商品列表,商品分類,首頁商品展示,商品詳情,購物車,用戶登錄注冊,剩余內容:用戶信息修改,提交訂單,訂單展示等,這篇文章將對剩余功能部分完結。

用戶信息修改的后端接口已經在管理平台實現,這里直接進行驗證調用即可

之前的修改用戶信息功能在測試中體現出來了一個bug,因為生成Token的字段是用戶名,當修改用戶信息時,如果修改了用戶名,就會導致token驗證失敗,於是我們需要修復token生成方式,將之前的用戶名生成改成_id生成,新版代碼已經提交至碼雲

修復后效果:

下面介紹一下實現流程 ,這里我們把info用戶信息界面和登錄界面放在單頁面中,通過v-if條件渲染,條件是checkToken是否通過

bussiness.js,驗證token是否有效

import Vue from "vue";
import config from "../../config/config";
const { ServerApi, StorageName } = config;
export default class UserInfoBussiness extends Vue {
  constructor(_vueComponent) {
    super();
    this.vueComponent = _vueComponent;
  }
  checkToken() {//驗證Token函數,若token正確,則直接登錄成功,若未成功,則切換至登錄界面
    let token = this.$storage.getStorage(StorageName.Token);
    if (!token || !token.length) return;
    this.$axios
      .get(ServerApi.token, {
        params: {
          token
        }
      })
      .then(res => {
        switch (res.result) {
          case -999://token請求拋發錯誤,token過期或錯誤
            this.vueComponent.isLogin = false;//顯示登錄頁面
            this.$storage.clearStorage(StorageName.Token);//清除之前的token
            break;
          case 1://驗證token成功
            this.vueComponent.userInfo = res.data;
            this.vueComponent.isLogin = true;//顯示個人信息頁面
            break;
          default:
            this.vueComponent.isLogin = false;
            this.$storage.clearStorage(StorageName.Token);
            break;
        }
      })
      .catch(err => {});
  }
}

 info.vue組件

<template>
  <div>
    <Top :title="isLogin?'我的':'登錄'"></Top>
    <div class="content">
      <UserInfo v-if="isLogin" :userInfo="userInfo"></UserInfo>
      <Login v-else></Login>
    </div>
    <TabBar></TabBar>
  </div>
</template>

<script>
import UserInfoBussiness from "./bussiness";
import TabBar from "../../components/tabBar/tabBar";
import UserInfo from "../../components/userInfo/userInfo";
import Login from "../../components/login/login";
import Top from "../../components/top/top";
import config from "../../config/config";
const { EventName } = config;
export default {
  components: {
    Top,
    UserInfo,
    Login,
    TabBar
  },
  data() {
    return {
      isLogin: false,
      userInfoBussiness: null,
      userInfo: null
    };
  },
  created() {
    this.userInfoBussiness = new UserInfoBussiness(this);
    this.$events.onEvent(EventName.IsLogin, () => {
      this.userInfoBussiness.checkToken();//退出登錄響應事件,重重頁面
    });
    this.userInfoBussiness.checkToken();//初始化先驗證token
  },
  destroyed() {
    this.$events.offEvent(EventName.IsLogin);
  }
};
</script>

<style lang="less" scoped>
@import "../../style/init.less";
</style>

在用戶登錄成功后,我們需要一個組件顯示用戶信息,這個沒有任何邏輯,純渲染,所以暫不做介紹

<template>
  <ul class="userInfo">
    <router-link to="/UpdateInfo">
      <li>
        <img :src="imgPath+userInfo.headPic" alt />
        <span>{{userInfo.username}}</span>
        <div class="iconfont icon-fanhui"></div>
      </li>
    </router-link>
    <li>
      <mt-cell :title="userInfo.phoneNum"></mt-cell>
      <mt-cell :title="userInfo.mailaddress+userInfo.mailurl"></mt-cell>
      <mt-cell :title="userInfo.alladdress.join('-')+'-'+userInfo.address"></mt-cell>
      <mt-cell :title="userInfo.descript"></mt-cell>
    </li>
  </ul>
</template>

<script>
import Config from "../../config/config";
const { RequestPath, StorageName } = Config;
import { Cell } from "mint-ui";
export default {
  name: "userinfotop",
  props: ["userInfo"],//父組件傳遞用戶信息至當前組件,並渲染
  data() {
    return {
      imgPath: RequestPath
    };
  },

  created() {
    this.$storage.saveStorage(StorageName.UserInfo, this.userInfo);
  }
};
</script>


<style lang="less" scoped>
@import "../../style/init.less";
.userInfo {
  li:nth-child(1) {
    .h(230);
    width: 100%;
    .mcolor();
    .l_h(230);
    margin-top: -1px;
    color: #fff;
    > img,
    > span {
      display: inline-block;
      vertical-align: middle;
      margin-left: unit(40 / @pxtorem, rem);
    }
    > img {
      .w(145);
      .h(145);
      border-radius: 100%;
    }
    > span {
      .f_s(40);
    }
    > div {
      height: 100%;
      float: right;
      padding-left: unit(40 / @pxtorem, rem);
      transform: rotateY(180deg);
    }
  }
}
</style>

通過點擊頭像框路由跳轉至UpdateInfo,用戶信息修改頁,我們將頭像上傳單獨寫成組件

這里有一個原生js上傳文件的坑:
axios上傳post文件頭文件需模擬 "multipart/form-data"請求,而這種請求格式與application/x-www-form-urlencoded有所不同,需要聲明一個分隔符‘boundary’。

headers: {
          "Content-Type": "multipart/form-data;boundary=ABC"//ABC內容自行填寫
  },

那么這時,坑人的地方來了,直接以ABC這種簡單的分隔符內容上傳文件可能會導致服務端對文件不識別,無法找到文件起始位置,所以我們需要一個復雜的字符,比如使用new Date().getTime()生成隨機字符,修改后就有以下配置

headers: {
          "Content-Type": "multipart/form-data;boundary=" + new Date().getTime()
        },

上傳頭像組件中,我們要自己寫一個控件替代官方的input元素,也就是點擊圖片使用JS執行input文件上傳事件,並提交到服務端,服務端存好緩存后將圖片文件地址發送到前端,前端讀取文件並展示,以下是頭像上傳的所有過程
uploadPic.vue

<template>
  <div class="uploadPic">
    <img :src="picPath" @click="clickHandler" alt />
    <input class="picFile" id="picFile" type="file" @change="uploadPic" accept="image/*" />
  </div>
</template>

<script>
import Config from "../../config/config";
import UploadBussiness from "./bussiness";
const { StorageName, RequestPath, UploadKey } = Config;
export default {
  name: "uploadPic",
  props: ["picFile"],
  data() {
    return {
      imgPath: RequestPath,
      picPath: ""
    };
  },
  created() {
    this.picPath = this.imgPath + this.picFile;
    this._uploadBussiness = new UploadBussiness(this);
  },
  methods: {
    clickHandler() {//點擊頭像模擬至點擊文件上傳input-file標簽
      document.querySelector("#picFile").click();
    },
    uploadPic(e) {
      let _picFile = new FormData();//新建FormData文件
      _picFile.append("token", this.$storage.getStorage(StorageName.Token));//將token添加至文件屬性中
      _picFile.append(UploadKey.headKey, e.target.files[0]);//文件校驗字段
      this._uploadBussiness.uploadPic(_picFile);//上傳文件
    }
  }
};
</script>

<style lang="less" scoped>
@import "../../style/init.less";
.uploadPic {
  img {
    width: 100%;
    height: 100%;
  }
  .picFile {
    display: none;
  }
}
</style>

bussiness.js

import Vue from 'vue'
import config from "../../config/config"
import {
  Toast
} from "mint-ui";
const {
  UploadName,
  EventName,
  UploadKey
} = config
export default class UploadBussiness extends Vue {
  constructor(_vueComponent) {
    super()
    this.vueComponent = _vueComponent
  }
  uploadPic(data) {
    this.$axios
      .post(UploadName.headPic, data, {
        headers: {
          "Content-Type": "multipart/form-data;boundary=" + new Date().getTime()//axios上傳post文件頭文件需模擬 "multipart/form-data"請求,而這種請求格式與application/x-www-form-urlencoded有所不同,需要聲明一個分隔符‘boundary’。
        },
      }).then(res => {
        Toast(res.msg);
        switch (res.result) {
          case 1://上傳成功后顯示圖片
            let fileRead = new FileReader();//新建文件讀取實例
            fileRead.readAsDataURL(data.get(UploadKey.headKey));//readAsDataURL讀取本地圖片信息
            fileRead.onload = () => {
              this.vueComponent.picPath = fileRead.result
            }
            this.$events.emitEvent(EventName.UploadPic, res.headPath)
            break;
          default:
            break;
        }
      })
  }
}

說完了上傳頭像組件后,來實現一下修改用戶信息,之前上傳的頭像地址會通過組件傳參傳遞到父組件中,伴隨着其他信息一起提交到服務端,服務端將收到的頭像緩存地址解析成文件並保存,修改用戶信息組件中可以復用一個省市縣選擇器組件,即之前在商品詳情中使用的商品數量選擇,其他的表單元素都是基本的文本類型

updataForm.vue

<template>
  <div class="update">
    <!-- <img :src="imgPath+userInfo.headPic" alt /> -->
    <UploadPic class="uploadPic" :picFile="userInfo.headPic"></UploadPic>
    <mt-field
      placeholder="請輸入用戶名"
      :state="userInfo.username.length?'success':'error'"
      v-model="userInfo.username"
    ></mt-field>
    <mt-field
      placeholder="請輸入手機號"
      :state="userInfo.phoneNum.length?'success':'error'"
      v-model="userInfo.phoneNum"
      type="number"
    ></mt-field>
    <mt-radio v-model="userInfo.sex" :options="sexOption"></mt-radio>
    <mt-button class="btn" @click="selectAddress">{{userInfo.alladdress.join('-')}}</mt-button>
    <mt-field
      placeholder="請輸入詳細地址"
      :state="userInfo.address.length?'success':'error'"
      v-model="userInfo.address"
    ></mt-field>
    <mt-field
      placeholder="請輸入個性簽名"
      :state="userInfo.descript.length?'success':'error'"
      v-model="userInfo.descript"
    ></mt-field>
    <mt-button class="submit" type="primary" @click="submit">修改信息</mt-button>
    <div class="shopPicker">
      <mt-popup v-model="popupVisible" position="bottom">
        <mt-picker
          :slots="myAddressSlots"
          value-key="name"
          :visibleItemCount="7"
          @change="changeAddress"
        ></mt-picker>
      </mt-popup>
    </div>
  </div>
</template>

<script>
import UpdateBussiness from "./bussiness";
import Config from "../../config/config";
import { Field, Button, Picker, Popup, Radio } from "mint-ui";
import address from "../../config/city";
import UploadPic from "../uploadPic/uploadPic";
const { StorageName, RequestPath, EventName } = Config;
export default {
  name: "updateForm",
  data() {
    return {
      imgPath: RequestPath,
      updateBussiness: null,
      popupVisible: false,//控制picker顯示
      selectArea: null,
      sexOption: [//性別配置
        {
          label: "男",
          value: "man"
        },
        {
          label: "女",
          value: "woman"
        }
      ],
      myAddressSlots: [//省市縣聯動配置
        {
          flex: 1,
          defaultIndex: 0,
          values: [],
          className: "slot1",
          textAlign: "center"
        },
        {
          divider: true,
          content: "-",
          className: "slot2"
        },
        {
          flex: 1,
          values: [],
          className: "slot3",
          textAlign: "center"
        },
        {
          divider: true,
          content: "-",
          className: "slot4"
        },
        {
          flex: 1,
          values: [],
          className: "slot5",
          textAlign: "center"
        }
      ],
      userInfo: this.$storage.getStorage(StorageName.UserInfo)//獲取緩存的用戶信息,用於顯示默認項
    };
  },
  components: {
    UploadPic
  },
  created() {
    this.$events.onEvent(EventName.UploadPic, headPic => {//上傳頭像后將新地址保存至當前組件
      this.userInfo.headPic = headPic;
    });
    this.updateBussiness = new UpdateBussiness(this);
  },
  destroyed() {
    this.$events.offEvent(EventName.UploadPic);
  },
  methods: {
    selectAddress() {//顯示picker
      this.myAddressSlots[0].values = address;
      this.popupVisible = true;
    },
    changeAddress(picker, values) {//三級聯動
      if (values[0]) {
        this.userInfo.alladdress = [values[0].name];
        picker.setSlotValues(1, values[0].children);
        if (values[1]) {
          this.userInfo.alladdress.push(values[1].name);
          picker.setSlotValues(2, values[1].children);
          if (values[2]) {
            this.userInfo.alladdress.push(values[2].name);
          }
        }
      }
    },
    submit() {
      this.updateBussiness.submitData();//提交信息
    }
  }
};
</script>

<style lang="less" scoped>
@import "../../style/init.less";
.update {
  .uploadPic {
    overflow: hidden;
    .w(200);
    .h(200);
    .mg(unit(30 / @pxtorem, rem) auto);
    border-radius: 100%;
  }
  .btn {
    width: 100%;
    .h(100);
    background: #fff;
  }
  .submit {
    margin-top: unit(30 / @pxtorem, rem);
    width: 100%;
    // z-index: 100;
  }
}
</style>

bussiness.js

import Vue from 'vue'
import config from "../../config/config"
import {
  Toast
} from "mint-ui";
const {
  ServerApi,
  StorageName,
  EventName
} = config
export default class UpdateBussiness extends Vue {
  constructor(_vueComponent) {
    super()
    this.vueComponent = _vueComponent
  }
  submitData() {
    for (const key in this.vueComponent.userInfo) {//表單非空判斷
      let value = this.vueComponent.userInfo[key]
      if (!value.length && value != true && value != 0 && typeof value == 'string') {
        Toast('請填寫完整的信息');
        return
      }
    }
    this.$axios
      .post(ServerApi.user.updateUser, {
        crypto: this.$crypto.setCrypto({
          token: this.$storage.getStorage(StorageName.Token),
          ...this.vueComponent.userInfo
        })
      }).then(res => {
        switch (res.result) {
          case 1:
            Toast(res.msg);
            history.go(-1)
            break;
          default:
            break;
        }
      })
  }
}

用戶信息修改就介紹到這里,下一步將對項目的最后一步訂單的前端部分進行分享

訂單的后端邏輯與接口在管理系統中已經介紹完畢,前端部分就是很簡單的數據渲染和狀態修改


首先,訂單是基於用戶和商品綁定的,所以,我們在購物車中實現新增訂單功能,添加成功后跳轉至訂單查詢界面,除此之外,在用戶信息界面,添加用戶的所有訂單列表可以查看和付款(由於只是一個項目案例,所以這里沒有實現支付功能)

orderList.vue組件,幾乎都是頁面渲染,沒有什么邏輯功能,就不做說明

<template>
  <div class="content">
    <div class="orderTop">
      <div>
        <div>
          <p class="fontcl">
            下單時間:
            <span>{{new Date(orderList.orderTime).toLocaleString()}}</span>
          </p>
          <p class="fontcl">
            訂單編號:
            <span>{{orderList.orderId}}</span>
          </p>
        </div>
        <div
          :class="orderList.orderState==0?'noPay':orderList.orderState==4?'isFinish':'isPay'"
        >{{orderState[orderList.orderState||0].name}}</div>
      </div>
      <div>
        <div>
          <span class="icon-yonghuming iconfont">{{orderList.username}}</span>
          <span class="icon-shoujihao iconfont">{{orderList.phoneNum}}</span>
        </div>
        <div class="fontcl">{{orderList.address}}</div>
      </div>
    </div>
    <ul class="orderList">
      <li v-for="(item,index) in orderList.shopList" :key="index">
        <img :src="imgPath+item.shopPic" alt />
        <div>
          {{item.shopName+item.shopScale}}
          <br />
          ¥{{item.shopPrice}}
        </div>
        <span>×{{item.shopCount}}</span>
      </li>
    </ul>
    <div class="submitOrder">
      <span>付款合計:¥{{orderList.orderPrice}}</span>
      <span @click="submitOrder" v-show="orderList.orderState==0">去付款</span>
    </div>
  </div>
</template>

<script>
import OrderBussiness from "./bussiness";
import Config from "../../config/config";
import ShopType from "../../config/shopType";
export default {
  name: "orderList",
  data() {
    return {
      orderState: ShopType.orderState,
      imgPath: Config.RequestPath,
      orderList: [],//訂單詳情
      orderBussiness: null,
    };
  },
  created() {
    this.orderBussiness = new OrderBussiness(this);
    this.orderBussiness.getOrderList();
  },
  methods: {
    submitOrder() {
      this.orderBussiness.sendOrderPay(this.orderList);//支付
    },
  },
};
</script>

<style lang="less" scoped>
@import "../../style/init.less";
.content {
  font-size: unit(32 / @pxtorem, rem);
  .fontcl {
    .cl(#979797);
  }
  .orderTop {
    > div {
      padding-left: unit(35 / @pxtorem, rem);
      padding-right: unit(35 / @pxtorem, rem);
    }
    > div:nth-child(1) {
      .h(160);
      border-bottom: unit(3 / @pxtorem, rem) solid #e8e8e8;
      > div:nth-child(1) {
        float: left;
        p {
          .l_h(80);
          span {
            .cl(#000);
          }
        }
      }
      > div:nth-child(2) {
        float: right;
        .h(160);
        .l_h(160);
      }
      .isFinish {
        .cl(@mainColor);
      }
      .isPay {
        .cl(#000);
      }
      .noPay {
        .cl(#A71A2D);
      }
    }
    > div:nth-child(2) {
      .h(180);
      border-bottom: unit(30 / @pxtorem, rem) solid #f3f3f3;
      > div:nth-child(1) {
        overflow: hidden;
        .l_h(100);
        span:nth-child(1) {
          float: left;
        }
        span:nth-child(2) {
          float: right;
        }
      }
      > div:nth-child(2) {
        width: 100%;
      }
    }
  }
  .orderList {
    li {
      .h(250);
      padding-left: unit(20 / @pxtorem, rem);
      padding-right: unit(35 / @pxtorem, rem);
      > div,
      > span,
      img {
        display: inline-block;
        vertical-align: middle;
      }
      img {
        .w(220);
        .h(220);
        margin-right: unit(30 / @pxtorem, rem);
      }
      > div {
        .l_h(60);
      }
      > span {
        vertical-align: top;
        margin-top: unit(50 / @pxtorem, rem);
        float: right;
      }
    }
  }
  .submitOrder {
    .h(130);
    width: 100%;
    position: fixed;
    bottom: 0;
    background: #fff;
    border-top: unit(3 / @pxtorem, rem) solid #cdcdcd;
    span:nth-child(1) {
      float: left;
      .pd(unit(40 / @pxtorem, rem));
      .cl(#852332);
    }
    span:nth-child(2) {
      .mcolor();
      .pd(unit(45 / @pxtorem, rem) unit(110 / @pxtorem, rem));
      float: right;
      .cl(#fff);
    }
  }
}
</style>

獲取訂單列表和提交訂單支付狀態的bussiness.js

import Vue from "vue";
import { MessageBox } from "mint-ui";
import config from "../../config/config";
import Clone from "../../utils/clone";
const { ServerApi, StorageName, EventName, DefaultPageConfig } = config;
export default class OrderBussiness extends Vue {
  constructor(_vueComponent) {
    super();
    this.vueComponent = _vueComponent;
    this._defaultPageConfig = Clone.shallowClone(DefaultPageConfig);
  }
  getOrderList() {//獲取個人訂單信息列表
    this._defaultPageConfig.token = this.$storage.getStorage(StorageName.Token);
    this._defaultPageConfig.orderId = this.vueComponent.$route.query.orderId;
    this.$axios
      .get(ServerApi.order.orderList, {
        params: {
          crypto: this.$crypto.setCrypto(this._defaultPageConfig)
        }
      })
      .then(res => {
        switch (res.result) {
          case 1:
            this.vueComponent.orderList = res.data.list[0];
            break;
          default:
            break;
        }
      });
  }
  sendOrderPay(data) {
    MessageBox("提示", "本案例僅為參考,未開通支付功能");
    data.orderState = 1;//修改訂單狀態為已支付
    data.token = this.$storage.getStorage(StorageName.Token);
    this.$axios
      .post(ServerApi.order.updateOrder, {
        crypto: this.$crypto.setCrypto(data)
      })
      .then(res => {
        switch (res.result) {
          case 1:
            break;
          default:
            break;
        }
      });
  }
}

訂單功能完成

項目整體打包

通過運行   npm run build   進行webpack打包

生產環境部署可以參照我之前的一篇文章

如果需要配置https環境可以參照這篇文章

文件夾的命名規則以及模塊組件的分配在這篇文章有說到

希望這個系列的文章對你有幫助,如果你閱讀完了整個系列或者某篇文章,非常感謝你的支持

總結:到這篇博客為止,《從零開始,搭建一個簡單的購物平台》系列的文章全部完結,以下是本人完成整個項目的一個小總結以及一些注意點:

  • 搭建環境及配置文件:對自己的技術棧以及優勢需要深入了解,並且選擇最適合自己或者是產品需求所需要的技術,完成項目目錄的搭建,比如前端最好養成模塊化,組件化開發的習慣,盡量將文件夾以及文件細分到每個基本組件。
  • 以組件和框架的官方文檔為核心,學會自己上網查找問題,自己動手解決問題非常有必要。
  • 學會造輪子,雖然網上有大量的框架,組件,別人寫好的js庫,但是自己動手寫函數,封裝功能以及組件是非常有必要的,並不是節省時間或者其他方面的原因,自己寫能提升自己編程思路和實際應用能力,而且當自己寫出了一個比較成功的類或者組件,甚至方法時,會有很大的成就感
  • 面向對象編程語言,減少代碼耦合度,提高內聚性,使代碼健壯性更加強大,這點我自己正在努力改善,這樣寫代碼有利於把很多方法剝離,可以提升復用性,減少代碼量,說白了,一個項目別人可能只需要3000行代碼,而我可能需要5000行
  • 這個項目我是全棧完成的,采用的是前后端分離,但是實際開發中,前后端可能是兩個或者多個人開發,這時需要自測接口及功能,前端搭建mock.js或使用easymock來進行模擬請求,后端可以使用postman,SoapUI等工具進行接口訪問
  • 前端和后端需要防止多次重復請求,前端通過節流的方式,防止對后端重復請求,但是也要防止數據庫的惡意攻擊(這個項目中沒有實現),通過參數附帶時間戳,使一個ip或者一個用戶只能在短時間內請求規定次數
  • 巧用前后端緩存,前端使用cookie和localstorage,后端生成temp緩存文件
  • 前后端加密處理,token,Crypto加密參數,Bcrypt加密密碼


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM