• 从零开始,搭建一个简单的购物平台(十九)前端商城部分


    从零开始,搭建一个简单的购物平台(十八)前端商城部分:
    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);
         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 {
         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 {
         100%;
        .h(100);
        background: #fff;
      }
      .submit {
        margin-top: unit(30 / @pxtorem, rem);
         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) {
             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);
         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加密密码
  • 相关阅读:
    动画电影分享
    Nginx 学习
    震惊!一步激活idea,亲测有效-2020-7-9
    根据oracle判断语句结果,进行循环语句
    Oracle11g的exp导出空表提示EXP-00011: 不存在
    查询某个用户下各个表的数据量
    Oracle批量修改表字段类型(存储过程)
    PLS-00201: identifier 'SYS.DBMS_EXPORT_EXTENSION' must be declared
    Oracle AWR报告生成和大概分析
    oracle如何给原有的用户变更表空间
  • 原文地址:https://www.cnblogs.com/HelloWorld-Yu/p/13908178.html
Copyright © 2020-2023  润新知