xmall商城项目实战

课程目标

  • 运用vue+vue-router+vuex+element-ui搭建网站
  • 对项目进行需求分析和模块划分以及功能划分
  • 实现首页+全部+品牌周边页面渲染
  • 查看商品详情页制作、商品排序以及分页功能实现
  • 使用token+jwt实现网站用户登录退出 (后台)
  • 使用meta元信息实现路由权限控制
  • 实现加入购物车、图片懒加载功能
  • 实现数据持久化存储用户数据和购物车数据
  • 项目优化以及如何打包上线整个流程

项目初始化

  • vue create xmall_front

  • 项目目录如下

    image-20191111095211353

  • image-20191111095850507

cd xmall_front
npm run server
//访问https://localhost:8080

效果如下:

image-20191111100105022

安装依赖

image-20191111101032460

  • 安装图片懒加载插件:npm i vue-lazyload -S 安装vant解决后期懒加载警告图片显示不了问题:npm i vant@2.12.47 -S

  • 安装请求库:npm i axios -S

  • 注意版本问题

    1664823579790

jwt-token原理:

  • 安装 模拟后端数据一些对应模块

npm i jsonwebtoken -S

npm i cors -S

npm i body-parse -S

npm i express -S

1664913945129

路由配置

import Vue from 'vue'
import VueRouter from 'vue-router'


// import Index from "../views/Index";
// import Login from '../views/Login'
// import Home from '../views/Home'
// import Goods from '../views/Goods'
// import Thanks from '../views/Thanks'

// 解决路由命名冲突的方法
const routerPush = VueRouter.prototype.push
VueRouter.prototype.push = function push(location) {
  return routerPush.call(this, location).catch(error => error)
}

// 异步组件加载,控制台中通过查看source查看到js分成很多小文件加载(而不是一个大的app.js加载),减轻资源消耗问题
const Index = ()=> import('@/views/Index')
const Login = ()=> import('@/views/Login')
const Home = ()=> import('@/views/Home')
const Goods = ()=> import('@/views/Goods')
const Thanks = ()=> import('@/views/Thanks')


Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'Home',
    redirect: '/home',
    component: Index,
    children:[
      {
        path: '/home',
        name: 'Home',
        component: Home,
      },
      {
        path: '/goods',
        name: 'Goods',
        component: Goods,
      },
      {
        path: 'goodsDetail',
        name: 'GoodsDetail',
        component: ()=>import('@/views/GoodsDetail')
      },
      {
        path: 'thanks',
        name: 'Thanks',
        component: Thanks,
      },
      {
        path: 'user',
        name: 'User',
        component: ()=>import('@/views/User'),
        meta:{
          auth:true
        }
      },

    ]
  },
  {
    path: '/login',
    name: 'Login',
    component: Login,
  }

]

const router = new VueRouter({
  mode: 'history',    // 去除路由的#
  base: process.env.BASE_URL,
  routes
})

export default router

组件模板样例

common/Mheader.vue

<template>
  <div class="header-box">
    <!-- mjj1015481875 -->
    <div>
      <header class="w">
        <div class="w-box">
          <div class="nav-logo">
            <h1>
              <router-link to="/" title="商城官网">Xmall商城</router-link>
            </h1>
          </div>
          <div class="right-box">
            <div class="nav-list">
              <el-input
                placeholder="请输入商品信息"
                suffix-icon="el-icon-search"
                v-model="productInfo"
                minlength="1"
                maxlength="100"
              ></el-input>
              <router-link to="/goods">全部商品</router-link>
              <router-link to="/thanks">捐赠</router-link>
            </div>

            <div class="nav-aside">
              <!-- 用户 -->
              <div class="user pr">
                <router-link to="/user">个人中心</router-link>
                <div class="nav-user-wrapper pa" v-if="login">
                  <div class="nav-user-list">
                    <ul>
                      <!-- 头像 -->
                      <li class="nav-user-avatar">
                        <div>
                          <span class="avatar" :style="{backgroundImage:'url('+userInfo.file+')'}"></span>
                        </div>
                        <p class="name">{{userInfo.username}}</p>
                      </li>
                      <li>
                        <router-link to="/user/orderList">我的订单</router-link>
                      </li>
                      <li>
                        <router-link to="/user/information">账号资料</router-link>
                      </li>
                      <li>
                        <router-link to="/user/addressList">收货地址</router-link>
                      </li>
                      <li>
                        <router-link to="/user/support">售后服务</router-link>
                      </li>
                      <li>
                        <router-link to="/user/coupon">我的优惠</router-link>
                      </li>
                      <li>
                        <a href="javascript:;" @click="logout">退出</a>
                      </li>
                    </ul>
                  </div>
                </div>
              </div>

              <!-- 购物车 -->
              <!--鼠标悬浮方法-->
              <div
                class="shop pr"
                @mouseenter="cartShowState(true)"
                @mouseleave="cartShowState(false)"
              >
                <router-link to="/cart"></router-link>
                <span class="cart-num">
                  <i class="num" :class="{no:totalNum == 0}">{{totalNum}}</i>
                </span>

                <!-- 购物车显示 -->
                <div class="nav-user-wrapper pa active" v-show="showCart">
                  <div class="nav-user-list">
                    <div class="full">
                      <div class="nav-cart-items">
                        <ul>
                          <li class="clearfix" v-for="(goods,index) in cartList" :key="index">
                            <div class="cart-item">
                              <div class="cart-item-inner">
                                <a>
                                  <div class="item-thumb">
                                    <img :src="goods.productImageBig">
                                  </div>
                                  <div class="item-desc">
                                    <div class="cart-cell">
                                      <h4>
                                        <a href>{{goods.productName}}</a>
                                      </h4>
<!--                                       <p class="attrs"><span>白色</span></p>-->
                                      <h6>
                                        <span class="price-icon">¥</span>
                                        <span class="price-num">{{goods.salePrice}}</span>
                                        <span class="item-num">x {{goods.productNum}}</span>
                                      </h6>
                                    </div>
                                  </div>
                                </a>
                                <div class="del-btn del">删除</div>
                              </div>
                            </div>
                          </li>
                        </ul>
                      </div>
                      <!-- 总件数 -->
                      <div class="nav-cart-total">
                        <p>
                          共
                          <strong>{{totalNum}}</strong> 件商品
                        </p>
                        <h5>
                          合计:
                          <span class="price-icon">¥</span>
                          <span class="price-num">{{totalPrice}}</span>
                        </h5>
                        <h6>
                          <el-button type="danger">去购物车</el-button>
                        </h6>
                      </div>
                    </div>
                    <div style="height: 313px;text-align: center" class="cart-con" v-if='!totalNum'>
                      <p>您的购物车竟然是空的!</p>
                    </div>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      </header>
      <slot name="nav">
        <div class="nav-sub">
          <div class="nav-sub-bg"></div>
          <div class="nav-sub-wrapper">
            <div class="w">
              <el-breadcrumb separator-class="el-icon-arrow-right">
                <el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
                <el-breadcrumb-item :to="{path:'/goods'}">全部</el-breadcrumb-item>
                <el-breadcrumb-item :to="{path:'/goods?cid=1184'}">品牌周边</el-breadcrumb-item>
                <el-breadcrumb-item :to="{path:'/thanks'}">捐赠名单</el-breadcrumb-item>
              </el-breadcrumb>
            </div>
          </div>
        </div>
      </slot>
    </div>
  </div>
</template>

<script>
import {mapState, mapMutations} from 'vuex'
import {getStore, removeStore, setStore} from '../utils/storage'

export default {
  data(){
    return{
      productInfo: '',

    }
  },
  computed:{
    ...mapState(['login',  'userInfo', 'cartList', 'showCart']),
    totalNum(){
      return this.cartList && this.cartList.reduce((total, item)=>{
          total += item.productNum;
          return total
      }, 0)
    },
    totalPrice(){
      return this.cartList && this.cartList.reduce((total, item)=>{
          total += item.productNum*item.salePrice;
          return total
      }, 0)
    },

  },
  async mounted() {
    if(this.login){
      // 解决网页初始化刷新丢失购物车数据的问题
      const res = await this.$http.post('/api/cartList', {userId:getStore('id')})
      if(res.data.success === true){
        setStore('buyCart', res.data.cartList.cartList);   // 解决导航栏小数字与本地buyCart数量不符问题
        this.INITBUYCART();
      }
    }else {
      this.INITBUYCART();
    }
  },
  methods:{
    ...mapMutations(['SHOWCART', 'INITBUYCART']),
    cartShowState(status){
      this.SHOWCART({
        showCart:status
      })
    },

    logout(){
      // 注销登录
      removeStore('token');
      removeStore('userinfo');
      removeStore('id');
      removeStore('buyCart');
      window.location.href = '/';
    }
  },
  created() {}
};
</script>

<style lang="scss" scoped>
@import "../assets/style/theme";
@import "../assets/style/mixin";

.w-box .nav-list .el-input {
  margin-right: 20px;
}
.header-box {
  background: $head-bgc;
  background-image: -webkit-linear-gradient(#000, #121212);
  background-image: linear-gradient(#000, #121212);
  width: 100%;
}

header {
  height: 100px;
  z-index: 30;
  position: relative;
}

.w-box {
  display: flex;
  justify-content: space-between;
  align-items: center;
  height: 100%;
  // position: relative;
  h1 {
    height: 100%;
    display: flex;
    align-items: center;
    > a {
      background: url(/static/images/global-logo-red@2x.png) no-repeat 50%;
      background-size: cover;
      display: block;
      @include wh(50px, 40px);
      text-indent: -9999px;
      background-position: 0 0;
    }
  }
  .nav-list {
    display: flex;
    justify-content: center;
    align-items: center;
    margin-right: 22px;
    .el-input {
      width: 305px;
    }
    a {
      width: 110px;
      color: #c8c8c8;
      display: block;
      font-size: 14px;
      padding: 0 25px;
      &:hover {
        color: #fff;
      }
    }
    a:nth-child(2) {
      // width: 5vw;
      margin-left: -10px;
    }
    // a:nth-child(3){
    //   width: 5vw;
    // }
  }
  .nav-aside {
    position: relative;
    &:before {
      background: #333;
      background: hsla(0, 0%, 100%, 0.2);
      content: " ";
      @include wh(1px, 13px);
      overflow: hidden;
      // position: absolute;
      display: flex;
      align-items: center;
      // top: 4px;
      left: 0;
    }
    &.fixed {
      width: 262px;
      position: fixed;
      left: 50%;
      top: 19px;
      margin-left: 451px;
      margin-top: 0;
      z-index: 32;
      top: -40px;
      -webkit-transform: translate3d(0, 59px, 0);
      transform: translate3d(0, 59px, 0);
      -webkit-transition: -webkit-transform 0.3s
        cubic-bezier(0.165, 0.84, 0.44, 1);
      transition: transform 0.3s cubic-bezier(0.165, 0.84, 0.44, 1);
      .user {
        &:hover {
          a:before {
            background-position: -215px 0;
          }
        }
      }
      .shop {
        &:hover {
          a:before {
            background-position: -210px -22px;
          }
        }
      }
    }
  }

  .right-box {
    display: flex;
  }
  .nav-aside {
    display: flex;
    align-items: center;
  }
  // 用户
  .user {
    margin-left: 41px;
    width: 36px;
    &:hover {
      a:before {
        background-position: -5px 0;
      }
      .nav-user-wrapper {
        top: 18px;
        visibility: visible;
        opacity: 1;
        -webkit-transition: opacity 0.15s ease-out;
        transition: opacity 0.15s ease-out;
      }
    }
    > a {
      position: relative;
      @include wh(36px, 20px);
      display: block;
      text-indent: -9999px;
      &:before {
        content: " ";
        position: absolute;
        left: 8px;
        top: 0;
        @include wh(20px);
        background: url(/static/images/account-icon@2x.32d87deb02b3d1c3cc5bcff0c26314ac.png) -155px
          0;
        background-size: 240px 107px;
        transition: none;
      }
    }
    li + li {
      text-align: center;
      position: relative;
      border-top: 1px solid #f5f5f5;
      line-height: 44px;
      height: 44px;
      color: #616161;
      font-size: 12px;
      &:hover {
        background: #fafafa;
      }
      a {
        display: block;
        color: #616161;
      }
    }
    .nav-user-avatar {
      > div {
        position: relative;
        margin: 0 auto 8px;
        @include wh(46px);
        text-align: center;
        &:before {
          content: "";
          position: absolute;
          left: 0;
          right: 0;
          top: 0;
          bottom: 0;
          border-radius: 50%;
          box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.06);
        }
        .avatar {
          border-radius: 50%;
          display: block;
          @include wh(100%);
          background-repeat: no-repeat;
          background-size: contain;
        }
      }
      .name {
        margin-bottom: 16px;
        font-size: 12px;
        line-height: 1.5;
        text-align: center;
        color: #757575;
      }
    }
    .nav-user-wrapper {
      width: 168px;
      transform: translate(-50%);
      left: 50%;
    }
    .nav-user-list {
      width: 168px;
      &:before {
        left: 50%;
      }
    }
  }
  .shop {
    position: relative;
    float: left;
    margin-left: 21px;
    width: 61px;
    z-index: 99;
    &:hover {
      a:before {
        content: " ";
        background-position: 0 -22px;
      }
    }
    .nav-user-wrapper.active {
      top: 18px;
      visibility: visible;
      opacity: 1;
      -webkit-transition: opacity 0.15s ease-out;
      transition: opacity 0.15s ease-out;
    }
    > a {
      position: absolute;
      left: 0;
      top: 0;
      bottom: 0;
      display: block;
      right: 0;
      z-index: 1;
      &:before {
        display: block;
        @include wh(30px, 100%);
        content: " ";
        background: url(/static/images/account-icon@2x.32d87deb02b3d1c3cc5bcff0c26314ac.png)
          0 -22px;
        background-size: 240px 107px;
        background-position: -150px -22px;
      }
    }
    .cart-num {
      position: relative;
      display: block;
      margin-left: 31px;
      margin-top: -1px;
      min-width: 30px;
      text-indent: 0;
      line-height: 20px;
      > i {
        background: #eb746b;
        background-image: -webkit-linear-gradient(#eb746b, #e25147);
        background-image: linear-gradient(#eb746b, #e25147);
        box-shadow: inset 0 0 1px hsla(0, 0%, 100%, 0.15),
          0 1px 2px hsla(0, 0%, 100%, 0.15);
        text-align: center;
        font-style: normal;
        display: inline-block;
        @include wh(20px);
        line-height: 20px;
        border-radius: 10px;
        color: #fff;
        font-size: 12px;
        &.no {
          background: #969696;
          background-image: -webkit-linear-gradient(#a4a4a4, #909090);
          background-image: linear-gradient(#a4a4a4, #909090);
          box-shadow: inset 0 0 1px #838383, 0 1px 2px #838383;
        }
      }
    }
    .nav-user-wrapper {
      right: 0;
      width: 360px;
      .nav-user-list {
        &:before {
          right: 34px;
        }
      }
    }
    .nav-user-list {
      padding: 0;
      width: 100%;
      .full {
        border-radius: 8px;
        overflow: hidden;
      }
      .nav-cart-items {
        max-height: 363px;
        overflow-x: hidden;
        overflow-y: auto;
      }
      .cart-item {
        height: 120px;
        width: 100%;
        overflow: hidden;
        border-top: 1px solid #f0f0f0;
        &:hover {
          background: #fcfcfc;
          .del {
            display: block;
          }
        }
      }
      li:first-child .cart-item:first-child {
        border-top: none;
        border-radius: 8px 8px 0 0;
        overflow: hidden;
      }
      .cart-item-inner {
        padding: 20px;
        position: relative;
      }
      .item-thumb {
        position: relative;
        float: left;
        width: 80px;
        height: 80px;
        border-radius: 3px;
        &:before {
          content: "";
          position: absolute;
          left: 0;
          right: 0;
          top: 0;
          bottom: 0;
          z-index: 2;
          border: 1px solid #f0f0f0;
          border: 0 solid transparent;
          box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.06);
          border-radius: 3px;
        }
        img {
          display: block;
          @include wh(80px, 80px);
          border-radius: 3px;
          overflow: hidden;
        }
      }
      .item-desc {
        margin-left: 98px;
        display: table;
        @include wh(205px, 80px);
        h4 {
          color: #000;
          width: 185px;
          overflow: hidden;
          word-break: keep-all;
          white-space: nowrap;
          text-overflow: ellipsis;
          font-size: 14px;
          line-height: 16px;
          margin-bottom: 10px;
        }
        .attrs span {
          position: relative;
          display: inline-block;
          margin-right: 20px;
          font-size: 14px;
          line-height: 14px;
          color: #999;
        }
        .attrs span:last-child {
          margin-right: 0;
        }
        h6 {
          color: #cacaca;
          font-size: 12px;
          line-height: 14px;
          margin-top: 20px;
          span {
            display: inline-block;
            font-weight: 700;
            color: #cacaca;
          }
          .price-icon,
          .price-num {
            color: #d44d44;
          }
          .price-num {
            margin-left: 5px;
            font-size: 14px;
          }
          .item-num {
            margin-left: 10px;
          }
        }
      }
      .cart-cell {
        display: table-cell;
        vertical-align: middle;
      }
      .del {
        display: none;
        overflow: hidden;
        position: absolute;
        right: 20px;
        top: 50%;
        transform: translateY(-50%);
      }
    }
    .nav-cart-total {
      box-sizing: content-box;
      position: relative;
      padding: 20px;
      height: 40px;
      background: #fafafa;
      border-top: 1px solid #f0f0f0;
      border-radius: 0 0 8px 8px;
      box-shadow: inset 0 -1px 0 hsla(0, 0%, 100%, 0.5),
        0 -3px 8px rgba(0, 0, 0, 0.04);
      background: -webkit-linear-gradient(#fafafa, #f5f5f5);
      background: linear-gradient(#fafafa, #f5f5f5);
      p {
        margin-bottom: 4px;
        line-height: 16px;
        font-size: 12px;
        color: #c1c1c1;
      }
      h5 {
        line-height: 20px;
        font-size: 14px;
        color: #6f6f6f;
        span {
          font-size: 18px;
          color: #de4037;
          display: inline-block;
          font-weight: 700;
        }
        span:first-child {
          font-size: 12px;
          margin-right: 5px;
        }
      }
      h6 {
        position: absolute;
        right: 20px;
        top: 20px;
        width: 108px;
      }
    }
  }
}

@media (max-height: 780px) {
  .nav-cart-items {
    max-height: 423px !important;
  }
}

@media (max-height: 900px) {
  .nav-cart-items {
    max-height: 544px !important;
  }
}

@media (max-height: 1080px) {
  .nav-cart-items {
    max-height: 620px !important;
  }
}

// 用户信息弹出
.nav-user-wrapper {
  position: absolute;
  z-index: 30;
  padding-top: 18px;
  opacity: 0;
  visibility: hidden;
  top: -3000px;
  .nav-user-list {
    position: relative;
    padding-top: 20px;
    background: #fff;
    border: 1px solid #d6d6d6;
    border-color: rgba(0, 0, 0, 0.08);
    border-radius: 8px;
    box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
    z-index: 10;
    &:before {
      position: absolute;
      content: " ";
      background: url(/static/images/account-icon@2x.32d87deb02b3d1c3cc5bcff0c26314ac.png)
        no-repeat -49px -43px;
      background-size: 240px 107px;
      @include wh(20px, 8px);
      top: -8px;
      margin-left: -10px;
    }
  }
}

.nav-sub {
  position: relative;
  z-index: 20;
  height: 90px;
  background: #f7f7f7;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.04);
  &.fixed {
    position: fixed;
    z-index: 21;
    height: 60px;
    top: 0;
    left: 0;
    right: 0;
    border-bottom: 1px solid #dadada;
    background-image: -webkit-linear-gradient(#fff, #f1f1f1);
    background-image: linear-gradient(#fff, #f1f1f1);
  }
  .nav-sub-wrapper {
    padding: 31px 0;
    height: 90px;
    position: relative;
    &.fixed {
      padding: 0;
      height: 100%;
      display: flex;
      align-items: center;
    }
    &:after {
      content: " ";
      position: absolute;
      top: 89px;
      left: 50%;
      margin-left: -610px;
      width: 1220px;
      background: #000;
      height: 1px;
      display: none;
      opacity: 0;
      -webkit-transition: opacity 0.3s ease-in;
      transition: opacity 0.3s ease-in;
    }
  }
  .w {
    display: flex;
    justify-content: space-between;
  }
  .nav-list2 {
    height: 28px;
    line-height: 28px;
    display: flex;
    align-items: center;
    height: 100%;
    li:first-child {
      padding-left: 0;
      a {
        padding-left: 10px;
      }
    }
    li {
      position: relative;
      float: left;
      padding-left: 2px;
      a {
        display: block;
        padding: 0 10px;
        color: #666;
        &.active {
          font-weight: bold;
        }
      }
      a:hover {
        color: #5683ea;
      }
    }
    li:before {
      content: " ";
      position: absolute;
      left: 0;
      top: 13px;
      width: 2px;
      height: 2px;
      background: #bdbdbd;
    }
  }
}

@media (min-width: 1px) {
  .nav-sub .nav-sub-wrapper:after {
    display: block;
  }
}

.cart-con {
  /*display: flex;*/
  text-align: center;
  position: relative;
  p {
    padding-top: 185px;
    color: #333333;
    font-size: 16px;
  }
}

.cart-con:before {
  position: absolute;
  content: " ";
  left: 50%;
  transform: translate(-50%, -70%);
  top: 50%;
  width: 76px;
  height: 62px;
  background: url("/static/images/cart-empty-new.png") no-repeat;
  background-size: cover;
}
</style>

APP.vue

<template>
  <div id="app">
        <router-view class="main"></router-view>
  </div>
</template>

<script>

export default {
  name: 'app',
  components: {

  }
}
</script>

<style lang="scss">
    @import "assets/style/index";
    #app{
        height: 100%;
    }
    .main{
        background-color: #ededed;
        overflow: hidden;
        width: 100%;
    }
</style>

Index.vue

<template>
    <div>
        <m-header></m-header>
        <!-- 子路由的出口-->
        <router-view></router-view>
    </div>
</template>

<script>
    import MHeader from '@/common/MHeader';
    export default {
        components: {
            MHeader,
        },
    }
</script>

<style lang="scss" scoped>
    
</style>

src/components/Mshelf.vue

<template>
  <div class="gray-box">
    <div class="title">
        <h2>{{title}}</h2>
    </div>
    <div>
        <!-- 具名插槽 -->
        <slot name='content'></slot>
    </div>
  </div>
</template>

<script>
export default {
    props:['title']
};
</script>

<style lang="scss" scoped>
.gray-box {
  position: relative;
  margin-bottom: 30px;
  overflow: hidden;
  background: #fff;
  border-radius: 8px;
  border: 1px solid #dcdcdc;
  border-color: rgba(0, 0, 0, 0.14);
  box-shadow: 0 3px 8px -6px rgba(0, 0, 0, 0.1);
  .title {
    padding-left: 30px;
    position: relative;
    z-index: 10;
    height: 60px;
    padding: 0 10px 0 24px;
    border-bottom: 1px solid #d4d4d4;
    border-radius: 8px 8px 0 0;
    box-shadow: rgba(0, 0, 0, 0.06) 0 1px 7px;
    background: #f3f3f3;
    background: -webkit-linear-gradient(#fbfbfb, #ececec);
    background: linear-gradient(#fbfbfb, #ececec);
    line-height: 60px;
    font-size: 18px;
    color: #333;
    display: flex;
    justify-content: space-between;
    align-items: center;
    h2 {
      font-size: 18px;
      font-weight: 400;
      color: #626262;
      display: inline-block;
    }
  }
}
</style>

src/components/MallGoods.vue

<template>
  <el-row class="good-item">
    <el-col>
      <el-card :body-style="{padding: 0}">
        <div class="good-img">
          <a>
            <img v-lazy="goods.productImageBig" alt>  <!--图片懒加载-->
          </a>
        </div>
        <h6 class="good-title">{{goods.productName}}</h6>
        <h3 class="sub-title ellipsis">{{goods.subTitle}}</h3>
        <div class="good-price pr">
          <div class="ds pa">
            <a href='javascript:;'>
              <el-button type="default" size="medium" @click="productDetail(goods.productId)">查看详情</el-button>
            </a>
            <a href="javascript:;">
              <el-button
                type="primary"
                size="medium"
                @click="addCart(goods.productId, goods.salePrice, goods.productName, goods.productImageBig)"
              >加入购物车</el-button>
            </a>
          </div>
          <p>
            <span style="font-size:14px">¥</span>
            {{Number(goods.salePrice).toFixed(2)}}
          </p>
        </div>
      </el-card>
    </el-col>
  </el-row>
</template>
<script>
  import {mapState, mapMutations} from 'vuex'
  import {getStore, setStore} from "../utils/storage";
export default {
  props: ["goods"],

  computed:{
    ...mapState(['login'])
  },
  methods:{
    ...mapMutations(['ADDCART', ]),
    productDetail(id){
      // 编程式导航
      this.$router.push({path:`goodsDetail?productId=${id}`})
    },

    addCart(id, price, name, img){
      if(this.login){
        // 用户已登录
        this.$http.post('/api/addCart', {
          userId: getStore('id'),
          productId: id,
          productNum: 1
        });
        // 存储到后端,将当前商品存储的store的cartList中
        this.ADDCART({
          productId: id,
          salePrice: price,
          productName: name,
          productImageBig: img,
        })
      }else {
        // 用户未登录,将当前商品存储的store的cartList中
        this.ADDCART({
          productId: id,
          salePrice: price,
          productName: name,
          productImageBig: img,
        })
      }
    }
  }


};
</script>

<style lang="scss" scoped>
.good-img {
  display: flex;
  justify-content: center;
  a {
    display: block;
    img {
      margin: 50px auto 10px;
      width: 206px;
      height: 206px;
      display: block;
    }
  }
}
.good-price {
  margin: 15px 0;
  height: 30px;
  text-align: center;
  line-height: 30px;
  color: #d44d44;
  font-family: Arial;
  font-size: 18px;
  font-weight: 700;
  display: flex;
  justify-content: space-around;
  padding-bottom: 60px;
  a {
    margin-right: 5px;
  }
  .ds {
    display: none;
  }
}
.good-price:hover .ds {
  display: block;
}
.good-title {
  line-height: 1.2;
  font-size: 16px;
  color: #424242;
  margin: 0 auto;
  padding: 0 14px;
  text-align: center;
  overflow: hidden;
}
h3 {
  text-align: center;
  line-height: 1.2;
  font-size: 12px;
  color: #d0d0d0;
  padding: 10px;
}
.good-item {
  background: #fff;
  width: 25%;
  transition: all 0.5s;
  height: 410px;
  &:hover {
    transform: translateY(-3px);
    box-shadow: 1px 1px 20px #999;
    .good-price p {
      display: none;
    }
    .ds {
      display: flex;
    }
  }
}
.el-card {
  border: none;
}
</style>

src/components/BuyNum.vue

<template>
  <el-input-number v-model="num" @change="handleChange" :min="1" :max="10" label="描述文字"></el-input-number>
</template>

<script>
export default {
  data() {
    return {
      num: 1
    };
  },
  methods: {
    handleChange(value) {
      this.$emit('handleValue',value)
    }
  }
};
</script>

<style lang="scss" scoped>
</style>

views/Goods/index.vue

<template>
    <div class="goods">
        <div class="nav">
            <div class="w">
                <a @click="handlerSort(index)" :class="{active:index === isIndex}" href="javascript:;" v-for="(item,index) in navList" :key="index">{{item.title}}</a>
                <div class="price-interval">
                    <input type="number" class="input" placeholder="价格" v-model="min">
                    <span style="margin: 0 5px">-</span>
                    <input type="number" placeholder="价格" v-model="max">
                    <el-button type="primary" size="small" style="margin-left: 10px;" @click="reset">确定</el-button>
                </div>
            </div>
        </div>
        <div>
            <div class="goods-box w">
                <MallGoods v-for="goods in allGoods" :key="goods.id" :goods="goods"></MallGoods>
            </div>
            <div class="w">
                <el-pagination
                        style="float: right"
                        v-model:currentPage="currentPage"
                        v-model:page-size="pageSize"
                        :page-sizes="[10, 20, 30, 40]"
                        layout="total, sizes, prev, pager, next, jumper"
                        :total="totalCount"
                        @size-change="handleSizeChange"
                        @current-change="handleCurrentChange"
                />
            </div>
        </div>
    </div>
</template>

<script>
    import MallGoods from "../../components/MallGoods";

    export default {
        data() {
            return {
                max: "",
                min: "",
                navList:[
                    {'title':'综合排序'},
                    {'title':'价格由低到高'},
                    {'title':'价格由高到低'},
                ],
                isIndex:0,
                currentPage:1,  // 当前页默认1
                pageSize:20,    // 每页数据
                sort:'',    // 排序
                allGoods:[],
                totalCount: '', // 总数据多少条显示在分页组件中
            };
        },
        components:{
            MallGoods
        },
        watch:{
          $route:'getAllGoods'      // 监听路由发生变化重新执行getAllGoods方法
        },
        created() {
            this.getAllGoods();
        },
        methods:{
            handleSizeChange(val){
                this.pageSize = val;
                this.getAllGoods();
                // (`${val} items per page`)
            },
            handleCurrentChange (val){
                this.currentPage = val;
                this.getAllGoods();
                // (`current page: ${val}`)
            },

            async getAllGoods(){
                const url = this.$route.query.cid ? `api/goods/allGoods?page=${this.currentPage}&size=${this.pageSize}&sort=${this.sort}&priceGt=${this.min}&priceLte=${this.max}&cid=${this.$route.query.cid}` : `api/goods/allGoods?page=${this.currentPage}&size=${this.pageSize}&sort=${this.sort}&priceGt=${this.min}&priceLte=${this.max}`
                try {
                    const res = await this.$http.get(url)
                    // (res)
                    this.allGoods = res.data.data;
                    this.totalCount = res.data.total;
                }catch (e) {
                    (e)
                }
            },

            // 价格排序
            priceSort(v){
              this.sort = v;
              this.getAllGoods();
            },
            reset(){
                this.currentPage = 1;
                this.sort = '';
                this.getAllGoods();
            },
            handlerSort(i){
                this.isIndex = i;   // 处理活跃标签

                switch (i) {
                    case 0:
                        // 综合排序
                        this.reset();
                        break

                    case 1:
                        // 正序
                        this.priceSort(1);
                        break

                    case 2:
                        // 倒序
                        this.priceSort(-1);
                        break
                }
            },

        }
    };
</script>

<style lang="scss" scoped>
    @import "../../assets/style/mixin";
    @import "../../assets/style/theme";

    .nav {
        height: 60px;
        line-height: 60px;
        > div {
            display: flex;
            align-items: center;
            a {
                padding: 0 30px 0 0;
                height: 100%;
                @extend %block-center;
                font-size: 12px;
                color: #999;
                &.active {
                    color: #5683ea;
                }
                &:hover {
                    color: #5683ea;
                }
            }
            input {
                @include wh(80px, 30px);
                border: 1px solid #ccc;
            }
            input + input {
                margin-left: 10px;
            }
        }
        .price-interval {
            padding: 0 15px;
            @extend %block-center;
            input[type="number"] {
                border: 1px solid #ccc;
                text-align: center;
                background: none;
                border-radius: 5px;
            }
        }
    }

    .goods-box {
        overflow: hidden;
        > div {
            float: left;
            border: 1px solid #efefef;
        }
    }

    .no-info {
        padding: 100px 0;
        text-align: center;
        font-size: 30px;
        display: flex;
        flex-direction: column;
        .no-data {
            align-self: center;
        }
    }

    .img-item {
        display: flex;
        flex-direction: column;
    }

    .el-pagination {
        align-self: flex-end;
        margin: 3vw 10vw 2vw;
    }

    .section {
        padding-top: 8vw;
        margin-bottom: -5vw;
        width: 1218px;
        align-self: center;
    }

    .recommend {
        display: flex;
        > div {
            flex: 1;
            width: 25%;
        }
    }
</style>

src/GoodsDetails/index.vue

<template>
    <div class="w store-content">
        <div class="gray-box">
            <div class="gallery-wrapper">
                <div class="gallery">
                    <div class="thumbnail">
                        <ul>
                            <!--如果小图等于大图地址,显示on效果-->
                            <li v-for="(item,i) in small" :key="i" :class="{on:item===big}" @click="handleClick(item)">
                                <img :src='item'>
                            </li>
                        </ul>
                    </div>
                    <div class="thumb">
                        <div class="big">
                            <img :src='big'>
                        </div>
                    </div>
                </div>
            </div>
            <!--右边-->
            <div class="banner">
                <div class="sku-custom-title">
                    <h4>{{product.productName}}</h4>
                    <h6>
                        <span>{{product.subTitle}}</span>
                        <span class="price">
              <em>¥</em>
              <i>{{Number(product.salePrice).toFixed(2)}}</i>
            </span>
                    </h6>
                </div>
                <div class="num">
                    <span class="params-name">数量</span>
                    <BuyNum @handlerValue="productNum"></BuyNum>
                </div>
                <div class="buy">
                    <el-button
                            type="primary"
                            @click="addCart"
                    >加入购物车</el-button>
                    <el-button type="danger">现在购买</el-button>
                </div>
            </div>
        </div>
        <!--产品信息-->
        <div class="item-info">
            <Mshelf title="产品信息">
                <div slot="content">
                    <div v-if="product.detail">
                        <div v-html="product.detail"></div>
                    </div>
                    <div class="no-info" v-else>
                        <img src="/static/images/no-data.png" alt="">
                        <br>该产品暂无数据提示哦
                    </div>
                </div>
            </Mshelf>
        </div>
    </div>
</template>

<script>
    import Mshelf from "../../components/Mshelf";
    import BuyNum from "../../components/BuyNum";

    export default {
        name: "goodsDetails",
        components:{
            Mshelf,
            BuyNum
        },
        data(){
            return{
                product: {},
                small: [],
                big: ""
            }
        },
        created() {
            this.getGoodsDetail();
        },
        methods:{
            // TODO:加入购物车
            addCart(){

            },
            // 由子组件el-input-number通过$emit传过来的number
            productNum(num){
                // (num);
            },
            handleClick(src){
                this.big = src;     // 保证点击时小图地址给大图地址赋值,从而展示放大效果
            },
            async getGoodsDetail(){
                try {
                    const res = await this.$http.get(`/api/goods/productDet?productId=${this.$route.query.productId}`)
                    // (res)
                    this.product = res.data
                    this.small = this.product.productImageSmall;
                    this.big = this.small[0];
                }catch (e) {
                    (e)
                }
            }
        }

    };
</script>

<style lang="scss" scoped>
    @import "../../assets/style/mixin";

    .store-content {
        clear: both;
        width: 1220px;
        min-height: 600px;
        padding: 0 0 25px;
        margin: 0 auto;
    }

    .gray-box {
        display: flex;
        padding: 60px;
        margin: 20px 0;
        .gallery-wrapper {
            .gallery {
                display: flex;
                width: 540px;
                .thumbnail {
                    li:first-child {
                        margin-top: 0px;
                    }
                    li {
                        @include wh(80px);
                        margin-top: 10px;
                        padding: 12px;
                        border: 1px solid #f0f0f0;
                        border: 1px solid rgba(0, 0, 0, 0.06);
                        border-radius: 5px;
                        cursor: pointer;
                        &.on {
                            padding: 10px;
                            border: 3px solid #ccc;
                            border: 3px solid rgba(0, 0, 0, 0.2);
                        }
                        img {
                            display: block;
                            @include wh(100%);
                        }
                    }
                }
                .thumb {
                    .big {
                        margin-left: 20px;
                    }
                    img {
                        display: block;
                        @include wh(440px);
                    }
                }
            }
        }
        // 右边
        .banner {
            width: 450px;
            margin-left: 10px;
            h4 {
                font-size: 24px;
                line-height: 1.25;
                color: #000;
                margin-bottom: 13px;
            }
            h6 {
                font-size: 14px;
                line-height: 1.5;
                color: #bdbdbd;
                display: flex;
                align-items: center;
                justify-content: space-between;
            }
            .sku-custom-title {
                overflow: hidden;
                padding: 8px 8px 18px 10px;
                position: relative;
            }
            .params-name {
                padding-right: 20px;
                font-size: 14px;
                color: #8d8d8d;
                line-height: 36px;
            }
            .num {
                padding: 29px 0 8px 10px;
                border-top: 1px solid #ebebeb;
                display: flex;
                align-items: center;
            }
            .buy {
                position: relative;
                border-top: 1px solid #ebebeb;
                padding: 30px 0 0 10px;
            }
        }
    }

    .item-info {
        .gray-box {
            padding: 0;
            display: block;
        }
        .img-item {
            width: 1220px;
            // padding: 1vw;
            text-align: center;
            img {
                width: 100%;
                height: auto;
                display: block;
            }
        }
    }

    .no-info {
        padding: 200px 0;
        text-align: center;
        font-size: 30px;
    }

    .price {
        display: block;
        color: #d44d44;
        font-weight: 700;
        font-size: 16px;
        line-height: 20px;
        text-align: right;
        i {
            padding-left: 2px;
            font-size: 24px;
        }
    }
</style>

src/Home/index.vue

<template>
    <div class="home">
        <!--轮播图-->
        <div class="banner">
            <el-carousel indicator-position="outside" height="480px">
                <el-carousel-item v-for="item in banner" :key="item.id">
                    <img :src="item.picUrl" class="img1" v-if="item.picUrl">
                    <img :src="item.picUrl2" class="img2" v-if="item.picUrl2">
                    <img :src="item.picUrl3" class="img3" v-if="item.picUrl3">
                </el-carousel-item>
            </el-carousel>
        </div>

        <div v-for="(item,index) in homeList" :key="index">
            <div class="activity-panel" v-if="item.type===1">
                <!-- 仅仅要活动版块的内容 -->
                <el-row>
                    <el-col class="content" :span="8" v-for="o in item.panelContents" :key="o.id">
                        <el-card :body-style="{ padding: '0px' }">
                            <img :src="o.picUrl" class="i">
                            <a href="#" class="cover-link"></a>
                        </el-card>
                    </el-col>
                </el-row>
            </div>

            <!-- 商品title -->
            <section class="w mt30 clearfix" v-if="item.type===2">
                <Mshelf :title="item.name">
                    <div slot="content" class="hot">
                        <MallGoods v-for="(o,i) in item.panelContents" :key="i" :goods="o"></MallGoods>
                    </div>
                </Mshelf>
            </section>


            <section class="w mt30 clearfix" v-if="item.type===3">
                <Mshelf :title="item.name">
                    <div slot="content" class="floors">
                        <div
                                class="imgbanner"
                                v-for="(o,j) in item.panelContents"
                                :key="j"
                                v-if="o.type===2 || o.type===3"
                        >
                            <img :src="o.picUrl" alt="">
                        </div>
                        <MallGoods :goods='o' v-for='(o,i) in item.panelContents' :key='i' v-if='o.type===0'></MallGoods>
                    </div>
                </Mshelf>
            </section>




        </div>


    </div>
</template>

<script>
    import Mshelf from "../../components/Mshelf";
    import MallGoods from "../../components/MallGoods";

    export default {
        name: "index",
        data(){
            return{
                banner:[],
                homeList:[]
            }
        },
        components:{
            Mshelf,
            MallGoods

        },
        async created() {
           const res = await this.$http.get('api/goods/home');
           // (res)
           if(res.data.code === 200){
               let result = res.data.result
               this.homeList = result;
               // (this.homeList)
               // 获取轮播图数据
               let item = result.find(item=>item.type===0);
               this.banner = item.panelContents;
           }
        }
    }
</script>

<style lang="scss" scoped>


    .home {
        display: flex;
        flex-direction: column;
    }

    .no-info {
        padding: 100px 0;
        text-align: center;
        font-size: 30px;
        display: flex;
        flex-direction: column;
        .no-data {
            align-self: center;
        }
    }

    .fade-enter-active,
    .fade-leave-active {
        transition: opacity 0.5s;
    }
    .fade-enter,
    .fade-leave-to {
        opacity: 0;
    }

    .page {
        position: absolute;
        width: 100%;
        top: 470px;
        z-index: 30;
        .dots {
            display: flex;
            flex-direction: row;
            align-items: center;
            justify-content: center;
            .dot-active {
                display: inline-block;
                width: 15px;
                height: 15px;
                background-color: whitesmoke;
                border-radius: 8px;
                margin-right: 10px;
                cursor: pointer;
            }
            .dot {
                opacity: 0.2;
            }
        }
    }

    .activity-panel {
        width: 1220px;
        margin: 0 auto;
        .box {
            overflow: hidden;
            position: relative;
            z-index: 0;
            margin-top: 25px;
            box-sizing: border-box;
            border: 1px solid rgba(0, 0, 0, 0.14);
            border-radius: 8px;
            background: #fff;
            box-shadow: 0 3px 8px -6px rgba(0, 0, 0, 0.1);
        }
        .content {
            float: left;
            position: relative;
            box-sizing: border-box;
            width: 25%;
            height: 200px;
            text-align: center;
        }
        .content ::before {
            position: absolute;
            top: 0;
            left: 0;
            z-index: 1;
            box-sizing: border-box;
            border-left: 1px solid #f2f2f2;
            border-left: 1px solid rgba(0, 0, 0, 0.1);
            width: 100%;
            height: 100%;
            content: "";
            pointer-events: none;
        }
        .i {
            width: 305px;
            height: 200px;
        }
        .cover-link {
            cursor: pointer;
            display: block;
            position: absolute;
            top: 0;
            right: 0;
            bottom: 0;
            left: 0;
            z-index: 4;
            background: url(data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEHAAEALAAAAAABAAEAAAICTAEAOw==)
            repeat;
        }
        a {
            color: #5079d9;
            cursor: pointer;
            transition: all 0.15s ease-out;
            text-decoration: none;
        }
        a:hover {
            box-shadow: inset 0 0 38px rgba(0, 0, 0, 0.08);
            transition: all 0.15s ease;
        }
    }

    .banner,
    .banner span,
    .banner div {
        font-family: "Microsoft YaHei";
        transition: all 0.3s;
        transition-timing-function: linear;
    }

    .banner {
        cursor: pointer;
        perspective: 3000px;
        position: relative;
        z-index: 19;
        margin: 0 auto 40px;
        width: 1220px;
    }

    .bg {
        position: relative;
        width: 1220px;
        height: 500px;
        margin: 20px auto;
        background-size: 100% 100%;
        border-radius: 10px;
        transform-style: preserve-3d;
        transform-origin: 50% 50%;
        transform: rotateY(0deg) rotateX(0deg);
        & div {
            position: relative;
            height: 100%;
            width: 100%;
        }
    }

    .img1 {
        display: block;
        position: absolute;
        width: 100%;
        height: 100%;
        top: 0;
        border-radius: 10px;
    }

    .img2 {
        display: block;
        position: absolute;
        width: 100%;
        height: 100%;
        bottom: 5px;
        left: 0;
        background-size: 95% 100%;
        border-radius: 10px;
    }

    .img3 {
        display: block;
        position: absolute;
        width: 100%;
        height: 100%;
        top: 0;
        border-radius: 10px;
    }

    .a {
        z-index: 20;
        transform: translateZ(40px);
    }

    .b {
        z-index: 20;
        transform: translateZ(30px);
    }

    .c {
        transform: translateZ(0px);
    }

    .sk_item {
        width: 170px;
        height: 225px;
        padding: 0 14px 0 15px;
        > div {
            width: 100%;
        }
        a {
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;
            transition: all 0.3s;
            &:hover {
                transform: translateY(-5px);
            }
        }
        img {
            width: 130px;
            height: 130px;
            margin: 17px 0;
        }
        .sk_item_name {
            color: #999;
            display: block;
            max-width: 100%;
            _width: 100%;
            overflow: hidden;
            font-size: 12px;
            text-align: left;
            height: 32px;
            line-height: 16px;
            word-wrap: break-word;
            word-break: break-all;
        }
        .sk_item_price {
            padding: 3px 0;
            height: 25px;
        }
        .price_new {
            font-size: 18px;
            font-weight: 700;
            margin-right: 8px;
            color: #f10214;
        }
        .price_origin {
            color: #999;
            font-size: 12px;
        }
    }

    .box {
        overflow: hidden;
        position: relative;
        z-index: 0;
        margin-top: 29px;
        box-sizing: border-box;
        border: 1px solid rgba(0, 0, 0, 0.14);
        border-radius: 8px;
        background: #fff;
        box-shadow: 0 3px 8px -6px rgba(0, 0, 0, 0.1);
    }

    ul.box {
        display: flex;
        li {
            flex: 1;
            img {
                display: block;
                width: 305px;
                height: 200px;
            }
        }
    }

    .mt30 {
        margin-top: 30px;
    }

    .hot {
        display: flex;
        > div {
            flex: 1;
            width: 25%;
        }
    }

    .floors {
        width: 100%;
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        .imgbanner {
            width: 50%;
            height: 430px;
            .cover-link {
                cursor: pointer;
                display: block;
                position: absolute;
                top: 60px;
                left: 0;
                width: 50%;
                height: 430px;
                z-index: 4;
                background: url(data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEHAAEALAAAAAABAAEAAAICTAEAOw==)
                repeat;
            }
            .cover-link:hover {
                box-shadow: inset 0 0 38px rgba(0, 0, 0, 0.08);
                transition: all 0.15s ease;
            }
        }
        img {
            display: block;
            width: 100%;
            height: 100%;
        }
    }
</style>

src/Login/index.vue

<template>
    <div class="login">
        <div class="box">
            <span>使用账号 登录官网</span>
            <el-form
                    :model="ruleForm"
                    status-icon
                    :rules="rules"
                    ref="ruleForm"
                    label-width="100px"
                    class="demo-ruleForm"
            >
                <el-form-item label="账号" prop="user">
                    <el-input type="text" v-model="ruleForm.user" autocomplete="off" placeholder="请输入账号"></el-input>
                </el-form-item>
                <el-form-item label="密码" prop="pwd">
                    <el-input type="password" v-model="ruleForm.pwd" autocomplete="off" placeholder="请输入密码"></el-input>
                </el-form-item>
                <div class="geetest"></div>
                <el-form-item>
                    <el-button type="primary" @click="submitForm('ruleForm')">登录</el-button>
                    <el-button>返回</el-button>
                </el-form-item>
            </el-form>
        </div>
    </div>
</template>

<script>
    import {getStore, removeStore, setStore} from '../../utils/storage'
    export default {
        data(){
            let validateUser = (rule, value, callback)=>{
                    if(value === ''){
                        callback(new Error('请输入账号!'))
                    }else {
                        callback();
                    }
                };
            let validatePwd = (rule, value, callback)=>{
                    if(value === ''){
                        callback(new Error('请输入密码!'))
                    }else {
                        callback();
                    }
                };
            return{
                ruleForm:{
                    user:'',
                    pwd:'',
                },
                rules: {
                    user: [{ validator: validateUser, trigger: "blur" }],
                    pwd: [{ validator: validatePwd, trigger: "blur" }]
                },
                cart:[],
            }
        },

        mounted() {
          // 缓存未登录之前当前购物车数据
            this.login_addCart();
        },
        methods:{
            login_addCart(){
              let cartArr = []; // 缓存数组
              let localCart = JSON.parse(getStore('buyCart'));
              if(localCart && localCart.length){
                  localCart.forEach(item=>{
                      cartArr.push({
                          userId:getStore('id'),
                          productId:item.productId,
                          productNum: item.productNum,
                      })
                  })
              }
              this.cart = cartArr;  // 拿到未登录时购物车数据
            },
            submitForm(formName){
                this.$refs[formName].validate(async (valid)=>{
                    if(valid){
                       const res = await this.$http.post('/api/login', this.ruleForm);
                       // console.log('这是校验过的数据', res)
                        if(res.data.code===200){
                            let {username, token, id} = res.data;
                            // 持久化存储
                            setStore('token', token);
                            setStore('id', id)
                            // 在请求拦截器里给所有请求头增加authorization字段
                            if(this.cart && this.cart.length){
                                try {
                                    // 将本地缓存的数组遍历去请求添加到购物车中
                                    this.cart.forEach(async item=>{
                                        item.userId = getStore('id')    // 解决userId找不到从而不能向下执行拿不到res的bug
                                        // console.log('this.cart--->', this.cart)
                                        let res = await this.$http.post("/api/addCart", item);
                                        // console.log('res-->', res)
                                        if(res.data.success === true){
                                            // .....自定制一些前段内容
                                            console.log('添加购物车成功!')
                                        }
                                        removeStore('buyCart');     // 登录成功后清除本地buyCart
                                        this.$router.push('/')
                                    });
                                }catch (e) {
                                    console.log(e)
                                }

                            }else {
                                await this.$router.push('/');
                            }
                        }
                    } else {
                        console.log("error submit!!");
                        return false;
                    }
                });
            }
        }


    };
</script>

<style lang="scss" scoped>
    .login {
        position: relative;
        overflow: visible;
        background: #ededed;
        .box {
            width: 450px;
            border: 1px solid #dadada;
            border-radius: 10px;
            position: absolute;
            top: 200px;
            left: 50%;
            padding: 50px 50px 50px 10px;
            margin-left: -225px;
            box-shadow: 0 9px 30px -6px rgba(0, 0, 0, 0.2),
            0 18px 20px -10px rgba(0, 0, 0, 0.04),
            0 18px 20px -10px rgba(0, 0, 0, 0.04),
            0 10px 20px -10px rgba(0, 0, 0, 0.04);
            text-align: center;
            form {
                margin-top: 30px;
            }
            span {
                color: #333;
                font-weight: 400;
            }
        }
    }
</style>

src/Thanks/idnex.vue

<template>
    <div>
        <h1>Thanks</h1>
    </div>
</template>

<script>
    export default {
        name: "index"
    }
</script>

<style scoped>

</style>

src/User/index.vue

<template>
    <div class="layout-container">
        <m-header>
            <div slot="nav"></div>
        </m-header>
        <div class="w">
            <div class="content"></div>
        </div>
    </div>
</template>

<script>
    import MHeader from "@/common/MHeader";
    export default {
        components: {
            MHeader
        }
    };
</script>

<style lang="scss" scoped>
    @import "../../assets/style/mixin";

    .w {
        padding-top: 40px;
    }

    .content {
        display: flex;
        height: 100%;
    }

    .account-sidebar {
        width: 210px;
        border-radius: 6px;
        .avatar {
            padding-top: 20px;
            border-radius: 10px;
            text-align: center;
            img {
                width: 168px;
                height: 168px;
            }
            h5 {
                font-size: 18px;
                line-height: 48px;
                font-weight: 700;
            }
        }
        .account-nav {
            padding-top: 15px;
            li {
                position: relative;
                height: 48px;
                border-top: 1px solid #ebebeb;
                line-height: 48px;
                &:hover {
                    a {
                        position: relative;
                        z-index: 1;
                        height: 50px;
                        background-color: #98afee;
                        line-height: 50px;
                        color: #fff;
                    }
                }
                a {
                    display: block;
                }
                &.current {
                    a {
                        position: relative;
                        z-index: 1;
                        height: 50px;
                        background-color: #98afee;
                        line-height: 50px;
                        color: #fff;
                    }
                }
            }
        }
    }

    .account-content {
        margin-left: 20px;
        flex: 1;
    }
</style>

/utils/storage.js

export const setStore = (name, content)=>{
    if(!name) return;
    if(typeof content !== 'string'){
        content = JSON.stringify(content);
    }
    window.localStorage.setItem(name, content);
}


export const getStore = name =>{
    if(!name) return;
    return window.localStorage.getItem(name);
}

export const removeStore = name =>{
    if(!name) return;
    window.localStorage.removeItem(name);

}

仓库存储vuex

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)
import {getStore, setStore} from '../utils/storage'
export default new Vuex.Store({
  state: {
    login:false,    // 用户是否登录
    userInfo:null,  // 用户信息
    cartList:[],  // 加入购物车商品列表
    showCart:false
  },
  mutations: {
      // 网页初始化从本地缓存拿到购物车数据
      INITBUYCART(state){
          let initCart = getStore('buyCart')
          if(initCart){
              state.cartList = JSON.parse(initCart)
          }
      },
      SHOWCART(state, {showCart}){
        state.showCart = showCart;
      },
      ISLOGIN(state, info){
          state.userInfo = info;
          state.login = true;
          // 持久化到本地
          setStore('userinfo', info)
      },
    ADDCART(state, {productId, salePrice, productName, productImageBig, productNum=1}){
        let cart = state.cartList;
        let goods = {
          productId, salePrice, productName, productImageBig
        };

        let flag = false;
        // 注意:此处顺序不能颠倒,一定得先判断购物车是否有值,否则添加到购物车数量不准
        if(cart.length){
            // 如果购物车有值
            cart.forEach(item=>{
                if(item.productId === productId){
                    if(item.productNum >= 0){
                        flag = true;
                        item.productNum += productNum;
                    }
                    // console.log('item.productNum--->', item.productNum)
                }
            })
        }
        if(!cart.length || !flag){
          // 如果购物车为空,数量为默认1
          goods.productNum = productNum;
          cart.push(goods)
        }
        // 给cartList重新赋值
        state.cartList = cart;
        setStore('buyCart', cart);

    }
  },
  actions: {
  },
  modules: {
  }
})


plugins/element.js

import Vue from 'vue'
import {
    Button,
    Input,
    Breadcrumb,
    BreadcrumbItem,
    Carousel,
    CarouselItem,
    Col,
    Card,
    Row,
    Pagination,
    InputNumber,
    Form, FormItem
} from 'element-ui'

Vue.use(Button)
Vue.use(Input)
Vue.use(Breadcrumb)
Vue.use(BreadcrumbItem)
Vue.use(Carousel)
Vue.use(CarouselItem)
Vue.use(Row)
Vue.use(Col)
Vue.use(Card)
Vue.use(Pagination)
Vue.use(InputNumber)
Vue.use(Form)
Vue.use(FormItem)

接口配置

请同学们自行封装对应的模块

初始化:npm init --yes

安装包:npm i cors body-parse express jsonwebtoken -S

配置server/app.js

const express = require('express');
const app = express();
const fs = require('fs');
/**
 * 
 * @param {*当前页的数量} pageSize
 * @param {*当前页} currentPage 
 * @param {*当前数组} arr 
 * 
 * 总32条
 * 8
 * 1 2
 */
function pagination(pageSize, currentPage, arr) {
    let skipNum = (currentPage - 1) * pageSize;
    let newArr = (skipNum + pageSize >= arr.length) ? arr.slice(skipNum, arr.length) : arr.slice(skipNum, skipNum + pageSize);
    return newArr;
}

// 升序还是降序
/**
 * 
 * @param {*排序的属性} attr 
 * @param {*true表示升序排序 false表示降序排序} rev 
 */

function sortBy(attr, rev) {
    if (rev === undefined) {
        rev = 1;
    } else {
        rev = rev ? 1 : -1;
    }
    return function (a, b) {
        a = a[attr];
        b = b[attr];
        if (a < b) {
            return rev * -1;
        }
        if (a > b) {
            return rev * 1;
        }
        return 0;
    }
}
function range(arr, gt, lte) {
    return arr.filter(item => item.salePrice >= gt && item.salePrice <= lte)
}
const cors = require('cors');
const jwt = require('jsonwebtoken')
const bodyParser = require('body-parser');
const cartListJSON = require('./db/cartList.json');

app.use(cors());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }))
app.get('/api/goods/home', (req, res) => {
    fs.readFile('./db/home.json', 'utf8', (err, data) => {
        if (!err) {
            res.json(JSON.parse(data));
        }
    })
})
app.get('/api/goods/allGoods', (req, res) => {
    // 获取的是前端地址栏上的查询参数
    const page = parseInt(req.query.page);
    const size = parseInt(req.query.size);
    const sort = parseInt(req.query.sort);
    const gt = parseInt(req.query.priceGt);
    const lte = parseInt(req.query.priceLte);
    const cid = req.query.cid;
    let newData = []
    fs.readFile('./db/allGoods.json', 'utf8', (err, data) => {
        let { result } = JSON.parse(data);
        let allData = result.data;
        // 分页显示
        newData = pagination(size, page, allData);
        if (cid === '1184') { //品牌周边
            newData = allData.filter((item) => item.productName.match(RegExp(/Smartisan/)))
            if (sort === 1) { //价格由低到高
                newData = newData.sort(sortBy('salePrice', true))
            } else if (sort === -1) { //价格由高到低
                newData = newData.sort(sortBy('salePrice', false))
            }
        } else {
            if (sort === 1) { //价格由低到高
                newData = newData.sort(sortBy('salePrice', true))
            } else if (sort === -1) { //价格由高到低
                newData = newData.sort(sortBy('salePrice', false))
            }
            if (gt && lte) {
                // 过滤 10~1000
                newData = range(newData, gt, lte)
            }
            // 32 

        }
        if (newData.length < size) {
            res.json({
                data: newData,
                total: newData.length
            })
        } else {
            res.json({
                data: newData,
                total: allData.length
            })
        }
    })
})
// 商品详情的数据
app.get('/api/goods/productDet', (req, res) => {
    const productId = req.query.productId;
    console.log(productId);
    fs.readFile('./db/goodsDetail.json', 'utf8', (err, data) => {
        if (!err) {
            let { result } = JSON.parse(data);
            let newData = result.find(item => item.productId == productId)
            res.json(newData)
        }
    })
})

// 模拟一个登陆的接口
app.post('/api/login', (req, res) => {
    console.log(req.body.user);
    // 登录成功获取用户名
    let username = req.body.user
    //一系列的操作
    res.json({
        // 进行加密的方法
        // sing 参数一:加密的对象 参数二:加密的规则 参数三:对象
        token: jwt.sign({ username: username }, 'abcd', {
            // 过期时间
            expiresIn: "3000s"
        }),
        username,
        state: 1,
        file: '/static/images/1570600179870.png',
        code: 200,
        address: null,
        balance: null,
        description: null,
        email: null,
        message: null,
        phone: null,
        points: null,
        sex: null,
        id: 62
    })
})

// 登录持久化验证接口 访问这个接口的时候 一定要访问token(前端页面每切换一次,就访问一下这个接口,问一下我有没有登录/登陆过期)
// 先访问登录接口,得到token,在访问这个,看是否成功
app.post('/api/validate', function (req, res) {
    let token = req.headers.authorization;
    console.log(token);

    // 验证token合法性 对token进行解码
    jwt.verify(token, 'abcd', function (err, decode) {
        if (err) {
            res.json({
                msg: '当前用户未登录'
            })
        } else {
            // 证明用户已经登录
            res.json({
                token: jwt.sign({ username: decode.username }, 'abcd', {
                    // 过期时间
                    expiresIn: "3000s"
                }),
                username: decode.username,
                msg: '已登录',
                address: null,
                balance: null,
                description: null,
                email: null,
                file: "/static/images/1570600179870.png",
                id: 62,
                message: null,
                phone: null,
                points: null,
                sex: null,
                state: 1,
            })
        }
    })
})


app.post('/api/addCart', (req, res) => {
    let { userId, productId, productNum } = req.body;
    fs.readFile('./db/allGoods.json', (err, data) => {
        let { result } = JSON.parse(data);
        if (productId && userId) {
            let { cartList } = cartListJSON.result.find(item => item.id == userId)
            // 找到对应的商品
            let newData = result.data.find(item => item.productId == productId);
            newData.limitNum = 100;

            let falg = true;
            if (cartList && cartList.length) {
                cartList.forEach(item => {
                    if (item.productId == productId) {
                        if (item.productNum >= 1) {
                            falg = false;
                            item.productNum += parseInt(productNum);
                        }
                    }
                })
            }
            if (!cartList.length || falg) {  //购物车为空
                newData.productNum = parseInt(productNum)
                cartList.push(newData);
            }

            // 序列化

            fs.writeFile('./db/cartList.json', JSON.stringify(cartListJSON), (err) => {
                if (!err) {
                    res.json({
                        code: 200,
                        message: "success",
                        result: 1,
                        success: true,
                        timestamp: 1571296313981,
                    })
                }
            })
        }

    })

})

app.post('/api/cartList', (req, res) => {
    let { userId } = req.body;
    fs.readFile('./db/cartList.json', (err, data) => {
        let { result } = JSON.parse(data);
        let newData = result.find(item => item.id == userId);
        res.json({
            code: 200,
            cartList: newData,
            success: true,
            message: 'success'
        })
    })
})

app.listen(3000);	// 将接口改为3000

修改db文件夹的位置到当前server目录下

测试启动: nodemon app.js

浏览器访问:http://localhost:3000/api/goods/home拿到全部数据

修改项目中端口地址:http://localhost:3000/

上线部署

www.pyhonav.cn

阿里云买台服务器

登录服务器

用户名:ssh root@123.206.16.61

密码:xxxx


安装node二进制文件

node版本必须高于8,否则后面 npm install 会报错

cd /tmp/

wget https://nodejs.org/download/release/v10.15.3/node-v10.15.3-linux-x64.tar.xz

解压node

xz -d node-v10.15.3-linux-x64.tar.xz :去除掉.xz后缀

tar -xf node-v10.15.3-linux-x64.tar

配置环境变量

ln -s /opt/node-v10.15.3-linux-x64/bin/node  /usr/local/sbin/ 
ln -s /opt/node-v10.15.3-linux-x64/bin/npm  /usr/local/sbin/

安装pm2进程管理工具

npm install pm2 -g

部署Node后端

  • git pull https://www.github.com/xiaomage/server
  • 模拟本地文件上传到服务器
    • 本地终端运行:scp ./server.zip root@123.206.16.61:/tmp
    • 服务器终端运行:unzip server.zip && cd server && npm install && pm2 start app.js

开放3000端口在防火墙中:

1665013758495

部署Vue前端项目

前端打包文件dist:

npm run build

  • 本地终端运行:scp ./dist.zip root@123.206.16.61:/tmp

  • 服务器终端运行:unzip dist.zip

部署nginx

找到nginx的安装目录

//以我的服务器为例:nginx目录
cd /opt/ngx112/conf 
vim nginx.conf

//修改配置文件如下
 server {
        listen       80; //端口号
        server_name  www.pythonav.cn; //域名
        location / {
                 try_files $uri $uri/ /index.html;  #匹配所有的路由
                 root /tmp/dist; //填写前端的根目录
            index  index.html index.htm;
        }
    }

输入nginx的启动命令

nginx 第一次输入是启动
nginx -s reload  #平滑重启,重新读取配置文件,不重启进程
nginx -s stop

访问http://www.pythonav.cn 查看xmall商城项目

image-20191118151652251

posted @ 2022-10-09 09:42  凫弥  阅读(254)  评论(0编辑  收藏  举报