Start from scratch, build a simple shopping platform (19) Front-end mall part

From scratch, build a simple shopping platform (18) Front-end mall part:
https://blog.csdn.net/time_____/article/details/108918489
Project source code (continuous update): https://gitee.com/ DieHunter/myCode/tree/master/shopping

After the last article, some basic functions of the front-end mall have been implemented, including product list, product classification, home page product display, product details, shopping cart, user login and registration, remaining content: user information modification, order submission, order display, etc., this article The article will end with the remaining features.

The back-end interface for user information modification has been implemented in the management platform, and the verification call can be made directly here

The previous function of modifying user information revealed a bug in the test, because the field for generating the token is the user name. When the user information is modified, if the user name is modified, the token verification will fail, so we need to fix the token generation method , Change the previous user name generation to _id generation, the new version of the code has been submitted to Code Cloud

Effect after repair:

The following describes the implementation process. Here we put the info user information interface and the login interface in a single page, and render through the v-if condition, the condition is whether the checkToken passes

bussiness.js, verify that the token is valid

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 component

<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>

After the user logs in successfully, we need a component to display user information. This has no logic and is pure rendering, so we will not introduce it for the time being

<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>

By clicking the avatar box route to jump to the UpdateInfo, user information modification page, we will upload the avatar as a separate component

Here is a pit for native js uploading files:
Axios uploads the post file header file to simulate the "multipart/form-data" request, and this request format is different from application/x-www-form-urlencoded, you need to declare a separator Symbol'boundary'.

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

At this time, the pitfall is here. Uploading files directly with simple separator content such as ABC may cause the server to not recognize the file and fail to find the starting position of the file, so we need a complicated character, such as using new Date().getTime() generates random characters, after modification, the following configuration

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

In uploading the avatar component, we have to write a control to replace the official input element, that is, click on the image and use JS to execute the input file upload event, and submit it to the server. After the server saves the cache, the image file address is sent to the front end. Read the file and display it, the following is the
uploadPic.vue process

<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;
        }
      })
  }
}

After finishing uploading the avatar component, let’s modify the user information. The previously uploaded avatar address will be passed to the parent component through the component parameter, and submitted to the server along with other information. The server will resolve the received avatar cache address File and save, modify the user information component can reuse a province, city and county selector component, that is, the number of products used in the product details selection, other form elements are basic text types

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;
        }
      })
  }
}

This is the end of the user information modification. The next step will be to share the front-end part of the final order

The back-end logic and interface of the order have been introduced in the management system, and the front-end part is very simple data rendering and status modification


First of all, the order is based on the user and the product binding, so we implement the new order function in the shopping cart. After the addition is successful, it will jump to the order query interface. In addition, in the user information interface, add all orders of the user The list can be viewed and paid (because it is only a project case, the payment function is not implemented here)

The orderList.vue component is almost all page rendering, there is no logical function, so it will not be explained

<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 to get the order list and submit the payment status of the order

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;
        }
      });
  }
}

Order function completed

Overall project packaging

Pack webpack by running npm run build

For production environment deployment, please refer to my previous article

If you need to configure the https environment, you can refer to this article

The naming rules of folders and the assignment of module components are mentioned in this article

I hope this series of articles are helpful to you. If you read the entire series or a certain article, thank you very much for your support

Summary: As of this blog, the articles in the series of "From scratch to build a simple shopping platform" are all over. The following is a small summary of the entire project and some points of attention:

  • Build environment and configuration files: you need to have a deep understanding of your own technology stack and advantages, and choose the technology that is most suitable for you or product needs, and complete the construction of the project catalog. For example, the front-end is best to develop modular and componentized development Habit, try to subdivide folders and files into each basic component.
  • With the official documentation of the components and framework as the core, it is very necessary to learn to find problems online and solve the problems by yourself.
  • Learn to make wheels. Although there are a lot of frameworks, components, and js libraries written by others on the Internet, it is very necessary to write functions, encapsulation functions and components by yourself. It is not for saving time or other reasons. Writing by yourself can improve Own programming ideas and practical application ability, and when you write a relatively successful class or component, or even method, you will have a great sense of accomplishment
  • Object-oriented programming language reduces code coupling, improves cohesion, and makes code more robust. I am working hard to improve this. Writing code in this way helps to strip many methods, improves reusability and reduces the amount of code. To put it bluntly, someone may only need 3000 lines of code for a project, but I may need 5000 lines
  • I completed this project in a full stack, using front-end and back-end separation, but in actual development, the front-end and back-end may be developed by two or more people. At this time, self-test interfaces and functions are required. The front-end builds mock.js or uses easymock. For simulation requests, the backend can use postman, SoapUI and other tools for interface access
  • The front-end and back-end need to prevent multiple repeated requests. The front-end uses throttling to prevent repeated requests to the back-end, but also to prevent malicious attacks on the database (not implemented in this project). Pass the parameter with a timestamp to make an ip Or a user can only request a specified number of times in a short time
  • Clever use of front-end and back-end caches, front-end uses cookies and localstorage, back-end generates temp cache files
  • Front and back end encryption processing, token, Crypto encryption parameters, Bcrypt encryption password

Guess you like

Origin blog.csdn.net/time_____/article/details/109116524