网易严选小程序 ( 前端 ):mpvue-shop

项目实战:小程序 专栏收录该内容
2 篇文章 0 订阅

网易严选小程序全栈开发

技术栈:mpvue + koa2 + mysql

技术文档:http://mpvue.com/mpvue/quickstart.html

mpvue常识

接收页面传递的数据

// 传递数据的页面
 wx.navigateTo({
	url: "/pages/goods/main?id=" + id
 });


// 接收数据的页面
 mounted () {
    this.id = this.$root.$mp.query.id
    console.log(this.id, '------')
  },

前端项目搭建:mpvue-shop

美团开源的语法:mpvue

技术文档:http://mpvue.com/mpvue/quickstart.html

首先安装Node.js 和 vue-cli脚手架工具

npm install -g vue-cli@2.9

创建一个基于 mpvue-quickstart 模板的新项目

vue init mpvue/mpvue-quickstart 项目名称

然后按照提示运行:

中间会询问一下问题:前面几项直接回车

直到:询问是否安装 vuex 和 ESLint,根据需要安装即可。

其余全部选中默认,直接回车

cd 项目目录

npm install
>>>>>>>>>>>>>>>>>>>>>>>>项目中使用的依赖包>>>>>>>>>>>>>>>>>>>>

npm install mpvue-wxparse less less-loader vuex mpvue -S

>>>>>>>>>>>>>>>>>>>>>>>>项目中使用的依赖包>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>


npm run dev (此时会自动打包,生成dist目录:里面就是小程序的页面结构)

然后打开微信开发者工具,导入项目:

选择导入的项目路径(直到找到wx目录):...省略部分 / 项目目录 / dist / wx

找到 src 目录

components 目录

card.vue

components / card.vue

<template>
  <div>
    <p class="card">
      {{text}}
    </p>
  </div>
</template>

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

<style>
.card {
  padding: 10px;
}
</style>

iconfont 目录

字体图标:https://www.iconfont.cn/

pages 目录

页面功能点
首页: index实时定位、轮播图、横向滚动、搜索
搜索位置:mappage搜索位置,高度地图
专题:topic下拉刷新、加载更多
分类:category商品分类:左栏商品名称;右栏商品具体分类
购物车:cart全选和下单
商品分类列表:categorylisttab切换(横向滚动)
商品详情:goods分享商品、收藏、加入购物车、立即购买、选择商品数量
订单:order选择收货地址、支付功能(未开发)
添加收货地址:addaddress默认收货地址、一键导入微信(收货地址)
修改收货地址:addressSelect按钮:修改收货地址、添加收货地址,一键导入收货地址
搜索:search热门搜索、模糊搜索、

1. address 文件夹

index.vue
<template>
  <div class="addaddress">
    <div class="item">
      <input type="text" placeholder="姓名" v-model="userName">
    </div>
    <div class="item">
      <input type="text" placeholder="手机号码" v-model="telNumber">
    </div>
    <!-- 三级选择:省市县 -->
    <div class="item">
      <picker mode="region" @change="bindRegionChange" :value="region" :custom-item="customItem">
        <input type="text" placeholder="身份、城市、区县" v-model="address">
      </picker>
    </div>
    <div class="item">
      <input type="text" placeholder="详细地址,如楼道、楼盘号等" v-model="detailaddress">
    </div>
    <!-- 小程序自带标签:多项选择器 -->
    <div class="item itemend">
      <checkbox-group @change="checkboxChange">
        <label class="checkbox">
          <checkbox class="box" value="true" :checked="checked" color="#b4282d"></checkbox>
          设置为默认地址
        </label>
      </checkbox-group>
      <div @click="wxaddress">一键导入微信</div>
    </div>

    <div class="bottom" @click="saveAddress">保存</div>
  </div>
</template>

<script>
import { get, post, getStorageOpenid } from "../../utils";
export default {
  data () {
    return {
      userName: '',
      telNumber: '',
      region: [],
      customItem: '全部',
      address: '',
      detailaddress: '',
      checked: false,  // 默认收货地址
      openId: '',
      res: '',  // 存储微信一键导入的数据
      id: ''  // 编辑收货地址:传递收货地址id
    }
  },
  // 初始化数据
  mounted() {
    // 第一步:获取用户id
    this.openId = getStorageOpenid()
    /**
     * 判断用户以什么样的方式,进入此页面
     * 1. 编辑收货地址:传递收货地址id
     * 2. 微信一键导入:传入收货地址信息res
     * 3. 新建收货地址:不传递任何数据
     */

    // 进入此页面方式一:微信一键导入
    if (this.$root.$mp.query.res) {

      // 第一步:解析传入的收货地址信息
      this.res = JSON.parse(decodeURIComponent(this.$root.$mp.query.res))
      console.log(this.res, '------')
      // 第二步:将微信导入的收货数据,直接在页面上展示
      this.userName = this.res.userName
      this.telNumber = this.res.telNumber
      this.address = `${this.res.provinceName} ${this.res.cityName} ${this.res.countyName}`
      this.detailaddress = this.res.detailInfo
    }
    // 进入此页面方式二:编辑收货地址:传递收货地址id
    if (this.$root.$mp.query.id) {
      this.id = this.$root.$mp.query.id
      this.getDetail()
    }
  },
  methods: {
    // 请求接口获取收货地址信息
    async getDetail () {
      const data = await get('/address/detailAction', {
        id: this.id
      })
      console.log(data)
      // 将接口收货地址信息,展示在页面上
      let detail = data.data
      this.userName = detail.name
      this.telNumber = detail.mobile
      this.address = detail.address
      this.detailaddress = detail.address_detail
      this.checked = detail.is_default === 1 ? true : false
    },
    // 默认收货地址
    checkboxChange (e) {
      this.checked = e.mp.detail.value[0]
    },
    // 选择地址:省市县
    bindRegionChange (e) {
      console.log(e)
      let value = e.mp.detail.value
      this.address = `${value[0]} ${value[1]} ${value[2]}`
    },
    // 一键导入微信地址
    wxaddress () {
      wx.chooseAddress({
        success: (result) => {
          console.log(result)
          this.userName = result.userName
          this.telNumber = result.telNumber
          this.address = `${result.provinceName} ${result.cityName} ${result.countyName}`
          this.detailaddress = result.detailInfo
        },
        fail: () => {},
        complete: () => {}
      });
        
    },
    // 保存收货地址信息
    async saveAddress () {
      const data = await post('/address/saveAction', {
        userName: this.userName,
        telNumber: this.telNumber,
        address: this.address,
        detailaddress: this.detailaddress,
        checked: this.checked,
        openId: this.openId,
        addressId: this.id
      })
      console.log(data)
      if (data.data) {
        wx.showToast({
          title: '添加成功',
          icon: 'success',
          duration: 2000,
          mask: true,
          success: (result) => {
            setTimeout(() => {
              wx.navigateBack({
                delta: 1
              })
            }, 2000)
          },
          fail: () => {},
          complete: () => {}
        });
          
      }
    }
  }
}
</script>

<style lang="less" scoped>
@import "./style.less";
</style>
main.js
import Vue from 'vue'
import App from './index'

const app = new Vue(App)
app.$mount()

style.less
.addaddress {
  position: absolute;
  width: 100%;
  height: 100%;
  background: #fff;
  .item {
    width: 690rpx;
    height: 70rpx;
    line-height: 70rpx;
    margin: 0 auto;
    padding: 10rpx 0;
    border-bottom: 1rpx solid #f4f4f4;
    input {
      width: 100%;
      height: 100%;
    }
  }
  .itemend {
    margin-top: 40rpx;
    display: flex;
    justify-content: space-between;
    border: none;
    div:nth-child(2) {
      color: green;
    }
  }
  .bottom {
    position: absolute;
    bottom: 0;
    left: 0;
    right: 0;
    text-align: center;
    height: 100rpx;
    line-height: 100rpx;
    background: #B4282D;
    color: #fff;
    font-size: 32rpx;
  }
}

2. addressSelect 文件夹

index.vue
<template>
  <div class="address">
    <scroll-view scroll-y="true" class="addcont" style="height: 100%">
      <div class="item" v-if="listData.length !== 0">
        <div class="list" v-for="(item, index) in listData" :key="index">
          <div class="addresslist">
            <div>
              <span>{{item.name}}</span>
              <div class="moren" v-if="item.is_default">默认</div>
            </div>
            <div class="info" @click="selAddress(item.id)">
              <p>{{item.mobile}}</p>
              <p>{{item.address+item.address_detail}}</p>
            </div>
            <div @click="toDetail(item.id)"></div>
          </div>
        </div>
      </div>

      <div class="center" v-else>
        <p>收货地址在哪里?</p>
      </div>
    </scroll-view>

    <div class="bottom">
      <div @click="wxaddress(1)">+新建地址</div>
      <div @click="wxaddress">一键导入微信地址</div>
    </div>
  </div>
</template>

<script>
import { get, getStorageOpenid } from "../../utils";
export default {
  data() {
    return {
      listData: [],
      openId: ""
    };
  },
  onShow() {
    this.openId = getStorageOpenid();
    this.getAddressList();
  },
  methods: {
    // 跳转到收货地址的详细信息(更改或者查看收货地址信息)
    toDetail(id) {
      wx.navigateTo({
        url: "/pages/addaddress/main?id=" + id
      });
    },
    // 新建收货地址 或者 一键导入微信收货地址
    wxaddress(index) {
      if (index === 1) {
        // 添加收货地址
        wx.navigateTo({
          url: "/pages/addaddress/main"
        });
      } else {
        // 一键导入微信收货地址
        wx.chooseAddress({
          success: function(res) {
            let result = encodeURIComponent(JSON.stringify(res));
            wx.navigateTo({
              url: "/pages/addaddress/main?res=" + result
            });
          }
        });
      }
    },
    // 获取接口收货地址数据
    async getAddressList() {
      let _this = this;
      const data = await get("/address/getListAction", {
        openId: _this.openId
      });
      console.log(data);
      _this.listData = data.data;
    },
    // 选择收货地址
    selAddress (id) {
      // 将当前收货地址的id,存储到本地
      wx.setStorageSync('addressId', id)
      wx.navigateBack({
        delta: 1
      });
    }
  }
};
</script>

<style lang="less" scoped>
@import "./style.less";
</style>
main.js
import Vue from 'vue'
import App from './index'

const app = new Vue(App)
app.$mount()

style.less
.address {
  height: 100%;
  display: flex;
  flex-direction: column;
  background: #fff;
  padding-bottom: 110rpx;
  box-sizing: border-box; // overflow-x: hidden;
  // overflow-y: scroll;
  // -webkit-overflow-scrolling: touch;
  .addcont {
    height: 100%; // padding-bottom: 110rpx;
    // box-sizing: border-box;
    // overflow-x: hidden;
    // overflow-y: scroll;
    // -webkit-overflow-scrolling: touch;
    .item {
      padding: 0 20rpx;
      .list {
        position: relative;
        padding: 30rpx 0;
        border-bottom: 1rpx solid #d9d9d9;
        .delete {
          position: absolute;
          width: 100rpx;
          top: 0;
          right: -120rpx;
          text-align: center;
          height: 100%;
          line-height: 100%;
          background: #b4282d;
          color: #fff;
          transition: all 200ms ease;
          display: flex;
          align-items: center;
          justify-content: center;
          div {
            color: #fff;
          }
        }
      }
    }
  }
  .addresslist {
    width: 100%;
    position: relative;
    transition: all 300ms ease;
    display: flex;
    justify-content: space-between;
    align-items: center;
    div:nth-child(1) {
      width: 100rpx;
      text-align: center;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
      align-self: flex-start;
      .moren {
        width: 60rpx;
        height: 30rpx;
        border: 1rpx solid #b4282d;
        text-align: center;
        line-height: 30rpx;
        color: #b4282d;
        margin: 10rpx auto 0 auto;
      }
    }
    .info {
      padding: 0 20rpx;
      flex: 1; // p:nth-child(1) {}
      p:nth-child(2) {
        margin-top: 5rpx;
        color: #666;
        overflow: hidden;
        text-overflow: ellipsis;
        display: -webkit-box;
        -webkit-line-clamp: 2;
        -webkit-box-orient: vertical;
      }
    }
    div:nth-child(3) {
      width: 50rpx;
      height: 50rpx;
      margin: 0 20rpx;
      background: url('../../../static/images/edit.png') no-repeat;
      background-size: 100% 100%;
    }
  }
  .center {
    width: 248rpx;
    height: 248rpx;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate3d(-50%, -80%, 0);
    background: url('http://yanxuan-static.nosdn.127.net/hxm/yanxuan-wap/p/20161201/style/img/icon-normal/noAddress-26d570cefa.png') no-repeat;
    background-size: 100% 100%;
    p {
      position: absolute;
      bottom: -20rpx;
      left: 0;
      right: 0;
      text-align: center;
    }
  }
  .bottom {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    padding: 30rpx 30rpx;
    display: flex;
    justify-content: space-between;
    background: #fff;
    div {
      width: 330rpx;
      height: 70rpx;
      line-height: 70rpx;
      text-align: center;
      border: 1rpx solid #B4282D;
      color: #B4282D;
    }
    div:nth-child(2) {
      border: 1rpx solid green;
      color: green;
    }
  }
}

3. cart 文件夹

index.vue
<template>
  <div class="cart">
    <div class="top">
      <div>30天无忧退货</div>
      <div>48小时快速退款</div>
      <div>满88元免邮费</div>
    </div>
    <div class="cartlist">
      <div class="item" v-for="(item, index) in listData" :key="index">
        <div class="con">
          <div class="left">
            <!-- 选中的样式是个圆圈:通过背景图片实现 -->
            <div class="icon" @click="changeColor(index, item.goods_id)" :class="[Listids[index] ? 'active' : '']"></div>
            <div class="img">
              <img :src="item.list_pic_url" alt="">
            </div>
            <div class="info">
              <p>{{item.goods_name}}</p>
              <p>¥{{item.retail_price}}</p>
            </div>
          </div>
          <div class="right">
            <div class="num">x {{item.number}}</div>
          </div>
        </div>
      </div>
    </div>

    <div class="fixed">
      <div class="left" @click="allCheck" :class="{'active': allcheck}">
        全选({{isCheckedNumber}})
      </div>
      <div class="right">
        <div>¥{{allPrice}}</div>
        <div @click="orderDown">下单</div>
      </div>
    </div>
  </div>
</template>

<script>
import { get, post, getStorageOpenid } from "../../utils"
export default {
  data () {
    return {
      openId: '',
      listData: [],  // 接口购物车表中的数据
      Listids: [],  // 单选:存放选中的商品id
      allcheck: false  // 全选
    }
  },
  // 小程序自带的生命周期函数:初始化页面数据
  onShow () {
    this.openId = getStorageOpenid()
    this.getListData()
  },
  methods: {
    async getListData () {
      const data = await get('/cart/cartList', {
        openId: this.openId
      })
      console.log(data)
      this.listData = data.data
    },
    // 单选功能:通过 this.$set()方法修改数据源
    changeColor (index, id) {
      if (this.Listids[index]) {
        // 如果该商品已经选中了且商品id存在于 Listids 里面 ,则设置为 false => 显示未选中状态
        this.$set(this.Listids, index, false)
      } else {
        // 如果选中的商品id,在 Listids 找不到;直接将该商品id添加到 Listids 里面
        this.$set(this.Listids, index, id)
      }
    },
    // 全选功能:
    allCheck () {
      // 第一步:先清空选择
      this.Listids = []
      // 第二步:判断全选状态
      if (this.allcheck) {
        this.allcheck = false
      } else {
        this.allcheck = true
        // 单选状态全部选中
        for (let i = 0; i < this.listData.length; i++) {
          const element = this.listData[i]
          this.Listids.push(element.goods_id)
        }
      }
    },
    // 点击下单
    async orderDown () {

      // 第一步:商品的数量非零判断
      if (this.Listids.length === 0) {
        wx.showToast({
          title: '请选择商品',
          icon: 'none',
          duration: 1500
        })
        return false
      }
      /**
       * 第二步:去除数组中空的false(因为前面设置了,如果取消勾选返回false)
       */

      // newgoodsid 此时里面存放的才是真实勾选的商品id
      let newgoodsid = []
      for (let i = 0; i < this.Listids.length; i++) {
        const element = this.Listids[i]
        if (element) {
          newgoodsid.push(element)
        }
      }
      let goodsId = newgoodsid.join(',')

      /**
       * 第三步:请求接口数据,传递下单的商品信息和用户id(后端处理:下单成功的商品,应该移除购物车)
       */
      const data = await post('/order/submitAction', {
        goodsId: goodsId,
        openId: this.openId,
        allPrice: this.allPrice
      })
      // 下单成功-跳转到订单页面
      if (data) {
        wx.navigateTo({
          url: '/pages/order/main'
        });
      }
    }
  },
  // 计算属性
  computed: {
    // 计算页面有多少条数据,处于选中状态
    isCheckedNumber () {
      let number = 0
      for (let i = 0; i < this.Listids.length; i++) {
        if (this.Listids[i]) {
          number++
        }
      }
      // 单选项全部处于选中状态的数量 = 商品的总数量;说明商品全部选中了
      if (number == this.listData.length && number !== 0) {
        this.allcheck = true
      } else {
        this.allcheck = false
      }
      return number
    },
    // 计算总价格
    allPrice () {
      let Price = 0
      for (let i = 0; i < this.Listids.length; i++) {
        if (this.Listids[i]) {
          Price += this.listData[i].retail_price * this.listData[i].number
        }
      }
      return Price
    }
  }
}
</script>

<style lang="less" scoped>
@import './style.less';
</style>
main.js
import Vue from 'vue'
import App from './index'

const app = new Vue(App)
app.$mount()

style.less
.cart {
  overflow-x: hidden;
  .top {
    display: flex;
    justify-content: space-between;
    padding: 30rpx 20rpx;
    div {
      background: url(http://nos.netease.com/mailpub/hxm/yanxuan-wap/p/20150730/style/img/icon-normal/servicePolicyRed-518d32d74b.png) 0 center no-repeat;
      background-size: 10rpx;
      padding-left: 15rpx;
      display: flex;
      align-items: center;
      font-size: 25rpx;
      color: #666;
    }
  }
  .cartlist {
    background: #fff;
    margin-bottom: 110rpx;
    .item {
      padding: 20rpx 0;
      border-bottom: 1rpx solid #f4f4f4;
      height: 166rpx;
      position: relative;
      .con {
        display: flex;
        align-items: center;
        justify-content: space-between;
        transition: all 300ms ease;
        .left {
          display: flex;
          align-items: center;
          width: 80%;
          .icon {
            height: 125rpx;
            width: 34rpx;
            background: url(http://nos.netease.com/mailpub/hxm/yanxuan-wap/p/20150730/style/img/icon-normal/checkbox-0e09baa37e.png) no-repeat center center;
            background-size: 34rpx 34rpx;
            margin: 0 20rpx;
          }
          .icon.active {
            background: url(http://nos.netease.com/mailpub/hxm/yanxuan-wap/p/20150730/style/img/icon-normal/checkbox-checked-822e54472a.png) no-repeat center center;
            background-size: 34rpx 34rpx;
          }
          .img {
            height: 125rpx;
            width: 125rpx;
            display: block;
            background: #f4f4f4;
            img {
              width: 100%;
              height: 100%;
            }
          }
          .info {
            width: 50%;
            padding: 20rpx;
            p {
              line-height: 40rpx;
            }
          }
        }
        .right {
          padding-right: 50rpx;
        }
      }
      .delete {
        position: absolute;
        width: 100rpx;
        top: 0;
        right: -100rpx;
        text-align: center;
        height: 100%;
        background: #b4282d;
        color: #fff;
        transition: all 200ms ease;
        display: flex;
        align-items: center;
        justify-content: center;
        div {
          color: #fff;
        }
      }
    }
  }
  .fixed {
    position: fixed;
    bottom: 0;
    left: 0;
    height: 100rpx;
    line-height: 100rpx;
    width: 100%;
    background: #fff;
    display: flex;
    justify-content: space-between;
    align-items: center;
    .left {
      display: flex;
      align-items: center;
      background: url(http://nos.netease.com/mailpub/hxm/yanxuan-wap/p/20150730/style/img/icon-normal/checkbox-0e09baa37e.png) no-repeat;
      background-size: 34rpx 34rpx;
      background-position: 20rpx;
      padding-left: 70rpx;
    }
    .active {
      background: url(http://nos.netease.com/mailpub/hxm/yanxuan-wap/p/20150730/style/img/icon-normal/checkbox-checked-822e54472a.png) no-repeat;
      background-size: 34rpx 34rpx;
      background-position: 20rpx;
    }
    .right {
      display: flex;
      div:nth-child(1) {
        color: #b4282d;
        padding-right: 40rpx;
      }
      div:nth-child(2) {
        width: 200rpx;
        height: 100rpx;
        text-align: center;
        line-height: 100rpx;
        font-size: 29rpx;
        background: #b4282d;
        color: #fff;
      }
    }
  }
  .nogoods {
    margin-top: 200rpx;
    img {
      margin: 0 auto;
      display: block;
      width: 258rpx;
      height: 258rpx;
    }
  }
}

4. category 文件夹

index.vue
<template>
  <div class="category">
    <div class="search" @click="tosearch">
      <div class="ser">
        <span class="icon"></span>
        <span>商品搜索,共239款好物</span>
      </div>
    </div>
    <div class="content">
      <scroll-view class="left" scroll-y="true">
        <div class="iconText" @click="selectItem(item.id, index)" v-for="(item, index) in listData" :key="index" :class="[index === nowIndex ? 'active' : '']">
          {{item.name}}
        </div>
      </scroll-view>
      <scroll-view class="right" scroll-y="true">
        <div class="banner">
          <img :src="detailData.banner_url" alt="">
        </div>
        <div class="title">
          <span>-</span>
          <span>{{detailData.name}}分类</span>
          <span>-</span>
        </div>
        <div class="bottom">
          <div class="item" @click="categoryList(item.id)" v-for="(item, index) in detailData.subList" :key="index">
            <img :src="item.wap_banner_url" alt="">
            <span>{{item.name}}</span>
          </div>
        </div>
      </scroll-view>
    </div>
  </div>
</template>

<script>
import { get } from '../../utils'
export default {
  data () {
    return {
      listData: [],
      nowIndex: 0,  // 选中哪个分类,添加类名
      id: '1005000',
      detailData: {}
    }
  },
  mounted () {
    this.getListData()
    this.selectItem(this.id, this.nowIndex)
  },
  methods: {
    // 跳转到商品搜索页面
    tosearch () {
      wx.navigateTo({
        url: '/pages/search/main'
      });  
    },
    // 请求接口数据
    async getListData () {
      const data = await get('/category/indexaction')
      // console.log(data)
      this.listData = data.categoryList
    },
    // 点击左侧分类项
    async selectItem (id, index) {
      // 获取右边商品的数据
      this.nowIndex = index
      const data = await get('/category/currentaction', {
        id: id
      })
      console.log(data)
      this.detailData = data.data.currentOne
    },
    // 跳转到商品详情
    categoryList (id) {
      console.log(id)
      wx.navigateTo({
        url: '../categroylist/main?id=' + id
      })
    }
  }
}
</script>

<style lang="less" scoped>
@import './style.less';
</style>
main.js
import Vue from 'vue'
import App from './index'

const app = new Vue(App)
app.$mount()
style.less
.category {
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;

  .search {
    height: 88rpx;
    // width: 100%;
    padding: 0 30rpx;
    background: #fff;
    display: flex;
    align-items: center;
    border-bottom: 1rpx solid #ededed;

    .ser {
      width: 690rpx;
      height: 56rpx;
      background: #ededed;
      border-radius: 8rpx;
      display: flex;
      align-items: center;
      justify-content: center;

      span {
        display: inline-block;
      }

      .icon {
        background: url('http://yanxuan.nosdn.127.net/hxm/yanxuan-wap/p/20161201/style/img/icon-normal/search2-2fb94833aa.png') center no-repeat;
        background-size: 100%;
        width: 28rpx;
        height: 28rpx;
        margin-right: 10rpx;
      }
    }
  }

  .content {
    flex: 1;
    background: #fff;
    display: flex;

    .left {
      width: 162rpx;
      height: 100%;
      text-align: center;

      .iconText {
        text-align: center;
        line-height: 90rpx;
        width: 162rpx;
        height: 90rpx;
        color: #333;
        font-size: 28rpx;
        border-left: 6rpx solid #fff;
      }

      .active {
        color: #ab2b2b;
        font-size: 36rpx;
        border-left: 6rpx solid #ab2b2b;
      }
    }

    .right {
      flex: 1;
      border-left: 1rpx solid #fafafa;
      flex: 1;
      height: 100%;
      padding: 0 30rpx 0 30rpx;

      .banner {
        width: 100%;
        height: 222rpx;
        margin-top: 20rpx;

        img {
          width: 100%;
          height: 100%;
        }
      }

      .title {
        text-align: center;
        padding: 50rpx 0;

        span:nth-child(2) {
          font-size: 24rpx;
          color: #333;
          padding: 0 10rpx;
        }

        span:nth-child(2n + 1) {
          color: #999;
        }
      }

      .bottom {
        display: flex; // justify-content: space-between;
        flex-wrap: wrap;

        .item {
          width: 33.33%;
          text-align: center;
          margin-bottom: 20rpx;

          img {
            height: 144rpx;
            width: 144rpx;
            display: block;
            margin: 0 auto;
          }
        }
      }
    }
  }
}

5. categroylist 文件夹

index.vue
<template>
  <div class="categoryList">
    <!-- 横向滚动区域 -->
    <scroll-view scroll-x="true" class="head" :scroll-left="scrollLeft">
      <div @click="changeTab(index, item.id)" v-for="(item, index) in navData" :key="index" :class="[nowIndex == index ? 'active' : '']">
        {{item.name}}
      </div>
    </scroll-view>
    <!-- 商品的标题名称 -->
    <div class="info">
      <p>{{currentNav.name}}</p>
      <p>{{currentNav.front_desc}}</p>
    </div>
    <!-- 商品列表区域 -->
    <div class="list" v-if="goodsList.length !== 0">
      <div class="item" v-for="(item, index) in goodsList" :key="index" @click="goodsDetail(item.id)">
        <img :src="item.list_pic_url" alt="">
        <p class="name">{{item.name}}</p>
        <p class="price">¥ {{item.retail_price}}</p>
      </div>
    </div>
    <!-- 友好提示 -->
    <div class="none" v-else>数据库暂无数据...</div>
  </div>
</template>

<script>
import { get } from '../../utils'
export default {
  data () {
    return {
      scrollLeft: 0,
      navData: [],  // 横向滚动数据源
      categoryId: '',  // 商品分类id(由上一个页面传递)
      currentNav: {},  // 商品的标题信息
      nowIndex: 0,  // 根据 nowIndex 的值,为横向滚动的标题栏,添加类名(用户从首页哪个分类进入商品分类页面的)
      goodsList: []  // 商品列表数据源
    }
  },
  mounted () {
    // 获取页面传递的参数
    this.categoryId = this.$root.$mp.query.id
    this.getAllData()
  },
  methods: {
    // 请求接口,获取页面数据
    async getAllData () {
      const data = await get('/category/categoryNav', {
        id: this.categoryId
      })
      // console.log(data)
      this.navData = data.navData
      this.currentNav = data.currentNav
      for (let i = 0; i < this.navData.length; i++) {
        const id = this.navData[i].id
        // 当前横向滚动数据源的每一项 对应 首页商品分类 (用户从首页哪个分类进入商品分类页面的,此时哪个分类项就添加类名)
        if (id == this.currentNav.id) {
          this.nowIndex = i
        }
      }

      // 获取商品
      const listData = await get('/goods/goodsList', {
        categoryId: this.categoryId
      })
      // console.log(listData)
      this.goodsList = listData.data
    },
    // tab商品分类切换-请求接口数据(渲染对应的商品分类属于的商品信息)
    async changeTab (index, id) {

      // 第一步:添加选中样式
      this.nowIndex = index
      // 第二步:请求对应的分类商品数据信息
      const listData = await get('/goods/goodsList', {
        categoryId: id
      })
      // 第三步:将请求的数据,渲染到页面上
      this.goodsList = listData.data
      this.currentNav = listData.currentNav
      // 让导航栏滚动到可见区域:当点击第五个分类的时候,会自动弹出后面的分类选项(横向滚动滑块自动向左滑动)
      if (this.nowIndex > 4) {
        this.scrollLeft = this.nowIndex * 60
      } else {
        // 点击左边前4个选项,滑块儿自动向右滑动(恢复初始状态)
        this.scrollLeft = 0
      }
    },
    // 跳转商品详情
    goodsDetail (id) {
      wx.navigateTo({
        url: '/pages/goods/main?id=' + id
      })
    }
  }
}
</script>

<style lang="less" scoped>
@import './style.less';
</style>
main.js
import Vue from 'vue'
import App from './index'

const app = new Vue(App)
app.$mount()

style.less
.categoryList {
  width: 100%;
  .head {
    width: 100%;
    height: 84rpx;
    line-height: 84rpx;
    background: #fff;
    white-space: nowrap;
    div {
      display: inline-block;
      padding: 0 20rpx;
      margin-left: 20rpx;
    }
    .active {
      color: #ab2b2b;
      height: 100%;
      border-bottom: 2px solid #ab2b2b;
      box-sizing: border-box;
    }
  }
  .info {
    text-align: center;
    background: #fff;
    padding: 30rpx;
    margin-top: 20rpx;
    margin-bottom: 5rpx;
    p:nth-child(1) {
      margin-bottom: 18rpx;
      font-size: 30rpx;
      color: #333;
    }
    p:nth-child(2) {
      display: block;
      height: 24rpx;
      font-size: 24rpx;
      color: #999;
    }
  }
  .list {
    width: 100%;
    display: flex;
    justify-content: space-between;
    flex-wrap: wrap;
    .item {
      width: 372.5rpx;
      margin-bottom: 5rpx;
      text-align: center;
      background: #fff;
      padding: 15rpx 0;
      img {
        display: block;
        width: 302rpx;
        height: 302rpx;
        margin: 0 auto;
      }
      .name {
        margin: 15rpx 0 22rpx 0;
        text-align: center;
        padding: 0 20rpx;
        font-size: 24rpx;
      }
      .price {
        text-align: center;
        font-size: 30rpx;
        color: #b4282d;
      }
    }
  }
  .none {
    text-align: center;
    margin-top: 100rpx;
    color: #999;
  }
}

6. counter

index.vue
<template>
  <div class="counter-warp">
    <p>Vuex counter:{{ count }}</p>
    <p>
      <button @click="increment">+</button>
      <button @click="decrement">-</button>
    </p>
  </div>
</template>

<script>
// Use Vuex
import store from './store'

export default {
  computed: {
    count () {
      return store.state.count
    }
  },
  methods: {
    increment () {
      store.commit('increment')
    },
    decrement () {
      store.commit('decrement')
    }
  }
}
</script>

<style>
.counter-warp {
  text-align: center;
  margin-top: 100px;
}
.home {
  display: inline-block;
  margin: 100px auto;
  padding: 5px 10px;
  color: blue;
  border: 1px solid blue;
}
</style>

main.js
import Vue from 'vue'
import App from './index'

const app = new Vue(App)
app.$mount()

store.js

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

Vue.use(Vuex)

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment: (state) => {
      const obj = state
      obj.count += 1
    },
    decrement: (state) => {
      const obj = state
      obj.count -= 1
    }
  }
})

export default store

7. goods

index.vue
<template>
  <div class="goods">
    <div class="swiper">
      <swiper class="swiper-container" indicator-dots="true" autoplay="true" interval="3000" duration="1000">
        <blok v-for="(item, index) in gallery" :key="index">
          <swiper-item class="swiper-item">
            <img :src="item.img_url" alt="" class="slide-image" />
          </swiper-item>
        </blok>
      </swiper>
      <button class="share" hover-class="none" open-type="share" value="">分享商品</button>
    </div>
    <div class="swiper-b">
      <div class="item">30天无忧退货</div>
      <div class="item">48小时快速退款</div>
      <div class="item">满88元免邮费</div>
    </div>
    <div class="goods-info">
      <div class="c">
        <p>{{info.name}}</p>
        <p>{{info.goods_brief}}</p>
        <p>¥{{info.retail_price}}</p>
        <div class="brand" v-if="brand.name">
          <p>{{brand.name}}</p>
        </div>
      </div>
    </div>
    <div class="section-nav" @click="showType">
      <div>请选择规格数量</div>
      <div></div>
    </div>

    <!-- 商品参数 -->
    <div class="attribute">
      <div class="head">
        商品参数
      </div>
      <div class="item" v-for="(item, index) in attribute" :key="index">
        <div>{{item.name}}</div>
        <div>{{item.value}}</div>
      </div>
    </div>

    <!-- 图片展示:mpvue自带的标签 wxParse 图片预览;使用前需要先引入-->
    <div class="detail" v-if="goods_desc">
      <wxParse :content="goods_desc" />
    </div>

    <!-- 常见问题 -->
    <div class="common-problem">
      <div class="h">
        <text class="title">常见问题</text>
      </div>
      <div class="b">
        <div class="item" v-for="(item, index) in issueList" :key="index">
          <div class="question-box">
            <text class="spot"></text>
            <text class="question">{{item.question}}</text>
          </div>
          <div class="answer">{{item.answer}}</div>
        </div>
      </div>
    </div>

    <!-- 大家都在看 -->
    <div class="common-problem">
      <div class="h">
        <text class="title">大家都在看</text>
      </div>
      <div class="sublist">
        <div v-for="(subitem, index) in productList" :key="index">
          <img :src="subitem.list_pic_url" alt="">
          <p>{{subitem.name}}</p>
          <p>¥{{subitem.retail_price}}</p>
        </div>
      </div>
    </div>

    <!-- footer -->
    <div class="bottom-fixed">
      <div class="collect-box" @click="collect">
        <div class="collect" :class="[collectFlag ? 'active' : '']"></div>
      </div>
      <div class="car-box" @click="toCart">
        <div class="car" >
          <span>{{allnumber}}</span>
          <img src="/static/images/ic_menu_shoping_nor.png" alt="">
        </div>
      </div>
      <div @click="buy">立即购买</div>
      <div @click="addCart">加入购物车</div>
    </div>

    <!-- 选择规格的弹出层 -->
    <!-- 遮罩层 -->
    <div class="pop" v-show="showpop" @click="showType"></div>
    <!-- 弹出层 -->
    <div class="attr-pop" :class="[showpop ? 'fadeup' : 'fadedown']">
      <div class="top">
        <div class="left">
          <img :src="info.primary_pic_url" alt="">
        </div>
        <div class="right">
          <div>
            <p>价格¥{{info.retail_price}}</p>
            <p>请选择数量</p> 
          </div>
        </div>
        <div class="close" @click="showType">✖️</div>
      </div>
      <div class="b">
        <p>数量</p> 
        <div class="count">
          <div class="cut" @click="reduce">-</div>
          <input type="text" class="number" v-model="number" disabled="false">
          <div class="add" @click="add">+</div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import { get, post } from '../../utils'
/**
 * 第一步:下载 npm install mpvue-wxparse -S
 * 第二步:从mpvue-wxparse 引入组件 wxParse
 * 第三步:将组件wxParse 在 components 注册
 * 第四步:在style标签中,引入样式:@import url('~mpvue-wxparse/src/wxParse.css');
 * 第五步:以标签的形式在页面使用
 */
import wxParse from 'mpvue-wxparse'
export default {
  data () {
    return {
      gallery: [], // banner
      id: '',
      openId: '',
      info: {},
      brand: {},
      showpop: false,
      number: 0,
      attribute: [],
      goods_desc: '',
      issueList: [], // 常见问题
      productList: [],
      collectFlag: false, // 是否收藏商品
      goodsId: '',
      allnumber: 0,
      allPrice: ''
    }
  },
  components: {
    wxParse
  },
  // 商品分享:小程序自带的方法 onShareAppMessage,可以手动添加分享的设置信息
  onShareAppMessage() {
    console.log(this.info.name)
    return {
      title: this.info.name,
      path: '/pages/goods/main?id' + this.info.id,
      imageUrl: this.gallery[0].img_url
    }
  },
  mounted () {
    this.openId = wx.getStorageSync('openId') || '';
    this.id = this.$root.$mp.query.id
    console.log(this.id, '------')
    this.goodsDetail()
  },
  methods: {
    async goodsDetail () {
      const data = await get('/goods/detailaction', {
        id: this.id,
        openId: this.openId
      })
      console.log(data)
      this.info = data.info
      this.gallery = data.gallery
      this.attribute = data.attribute
      this.goods_desc = data.info.goods_desc
      this.issueList = data.issue
      this.productList = data.productList
      this.goodsId = data.info.id
      this.collectFlag = data.collected
      this.allnumber = data.allnumber
      this.allPrice = data.info.retail_price
    },
    showType () {
      this.showpop = !this.showpop
    },
    // 增加商品数量
    add () {
      this.number += 1
    },
    // 减少商品数量
    reduce () {
      if (this.number > 1) {
        this.number -= 1
      } else {
        return false
      }
    },
    // 点击商品收藏按钮
    async collect () {
      this.collectFlag = !this.collectFlag
      const data = await post('/collect/addcollect', {
        openId: this.openId,
        goodsId: this.goodsId
      })
    },
    // 跳转到tabbar页面:购物车
    toCart () {
      wx.switchTab({
        url: '/pages/cart/main'
      });  
    },
    /**
     * 点击立即购买:业务逻辑分析
     * 判断弹出层,是否出现过:
     * 1. 如果为true:
     *    判断此时的商品数量是否为0(客户是否选购了商品)
     *      如果为0 =>友好提示:请选择商品数量;
     *      如果商品数量不为0:传递信息给后端:商品goodsId 和 用户openId 和 商品的价格allPrice =>请求订单相关的接口
     * 
     * 2. false,重新设置弹出层为true
     */
    async buy () {
      if (this.showpop) {
        if (this.number === 0) {
          wx.showToast({
            title: '请选择商品数量',
            duration: 2000,
            icon: 'none',
            mask: true,
            success: res => {}
          })
          return false
        }
        // 传递信息给后端:商品goodsId 和 用户openId 和 商品的价格allPrice =>请求订单相关的接口
        const data = await post('/order/submitAction', {
          goodsId: this.goodsId,
          openId: this.openId,
          allPrice: this.allPrice
        })
        /**
         * data为true,说明:订单添加成功 或者 订单更新成功 =>跳转到支付页面order
         */
        if (data) {
          wx.navigateTo({
            url: '/pages/order/main'
          });
            
        }
      } else {
        this.showpop = true
      }
    },
    /**
     * 点击加入购物车:业务逻辑分析
     * 判断弹出层,是否出现过:
     * 1. 如果为true:
     *    判断此时的商品数量是否为0(客户是否选购了商品)
     *      如果为0 =>友好提示:请选择商品数量;
     *      如果商品数量不为0:
     * 
     * 2. false,重新设置弹出层为true
     */
    async addCart () {
      if (this.showpop) {
        if (this.number == 0) {
          wx.showToast({
            title: '请选择商品数量',
            duration: 2000,
            icon: 'none',
            mask: true,
            success: res => {}
          })
          return false
        }
        // 请求接口,将商品加入购物车
        const data = await post('cart/addCart', {
          openId: this.openId,
          goodsId: this.goodsId,
          number: this.number
        })
        if (data) {
          // 商品数量= 已有的数据量 + 要添加的数据量
          this.allnumber = this.allnumber + this.number
          wx.showToast({
            title: '添加购物车成功',
            icon: 'success',
            duration: 1500
          })
        }
      } else {
        this.showpop = true
      }
    }
  }
}
</script>

<style lang="less" scoped>
@import url('~mpvue-wxparse/src/wxParse.css');
@import './style.less';
</style>
main.js
import Vue from 'vue'
import App from './index'

const app = new Vue(App)
app.$mount()

style.less
.goods{
  overflow: hidden;

  .swiper{
    width: 750xrpx;
    height: 750rpx;
    position: relative;

    &-container{
      width: 100%;
      height: 100%;

      img{
        width: 100%;
        height: 100%;
      }
    }
    .share{
      position: absolute;
      border-radius: 40rpx 0 0 40rpx;
      width: 150rpx;
      height: 65rpx;
      line-height: 65rpx;
      text-align: center;
      right: 0;
      top: 50rpx;
      background: #e0a354;
      color: #fff;
      font-size: 24rpx;
    }
  }
  .swiper-b{
    width: 710rpx;
    height: 73rpx;
    margin: 0 auto;
    background: #f4f4f4;
    display: flex;
    align-items: center;
    justify-content: space-between;
    div{
      background: url('http://nos.netease.com/mailpub/hxm/yanxuan-wap/p/20150730/style/img/icon-normal/servicePolicyRed-518d32d74b.png') 0 center no-repeat;
      background-size: 10rpx;
      padding-left: 15rpx;
      display: flex;
      align-items: center;
      font-size: 25rpx;
      color: #666;
    }
  }
  .goods-info{
    width: 750rpx;
    height: 306rpx;
    background: #fff;
    margin: 0 auto;
    border-bottom: 1rpx solid #f4f4f4;
    .c{
      height: 100%;
      p{
        display: block;
        text-align: center;
      }
      p:nth-child(1) {
        font-size: 41rpx;
        padding: 20rpx;
      }
      p:nth-child(2) {
        font-size: 24rpx;
        margin-bottom: 25rpx;
        color: #999;
      }
      p:nth-child(3) {
        font-size: 35rpx;
        margin-top: 10rpx;
        color: #b4282d;
      }
      .brand{
        margin-top: 25rpx;
        text-align: center;
        p{
          display: inline-block;
          color: #b1a279;
          font-size: 20rpx;
          padding: 5rpx 30rpx;
          border: 1rpx solid #b1a279;
        }
      }
    }
  }
  .section-nav{
    height: 108rpx;
    background: #fff;
    margin-bottom: 20rpx;
    padding: 0 20rpx;
    display: flex;
    justify-content: space-between;
    align-items: center;
    div:nth-child(2) {
      width: 52rpx;
      height: 52rpx;
      background: url('../../../static/images/address_right.png') no-repeat;
      background-size: 100% 100%;
    }
  }
  .pop{
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(0, 0, 0, 0.5);
  }
  .attr-pop{
    position: fixed;
    width: 100%;
    height: 500rpx;
    bottom: -500rpx;
    transition: all 400ms ease;
    box-sizing: border-box;
    padding: 30rpx;
    background: #fff;
    .top{
      display: flex;
      margin-bottom: 35rpx;
      position: relative;
      .close{
        position: absolute;
        right: 0;
        top: 0;
        font-size: 30rpx;
        color: #999;
      }
      .left{
        float: left;
        height: 177rpx;
        width: 177rpx;
        margin-right: 30rpx;
        img{
          float: left;
          height: 177rpx;
          width: 177rpx;
        }
      }
      .right{
        flex: 1;
        display: flex;
        align-items: flex-end;
        p{
          width: 100%;
          line-height: 45rpx;
        }
        p:nth-child(1){
          color: #b4282d;
        }
      }
    }
    .b{
      .count{
        width: 322rpx;
        height: 71rpx;
        line-height: 71rpx;
        display: flex;
        border: 1rpx solid #ccc;
        margin-top: 20rpx;
        div{
          width: 90rpx;
          text-align: center;
        }
        input{
          flex: 1;
          height: 100%;
          text-align: center;
          border-left: 1rpx solid #ccc;
          border-right: 1rpx solid #ccc;
        }
      }
    }
  }
  .fadeup{
    transform: translateY(-500rpx);
  }
  .attribute{
    padding: 20rpx 30rpx;
    background-color: #fff;
    margin-bottom: 20rpx;
    .head{
      font-size: 38rpx;
      padding: 20rpx 0;
    }
    .item{
      display: flex;
      background: #f7f7f7;
      padding: 20rpx 0;
      margin: 20rpx;
      div:nth-child(1) {
        width: 134rpx;
        font-size: 25rpx;
        color: #999;
      }
      div:nth-child(2) {
        flex: 1;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
      }
    }
  }
  .common-problem{
    margin-bottom: 110rpx;
    .h{
      padding: 35rpx 0;
      background: #fff;
      text-align: center;
      display: flex;
      align-items: center;
      justify-content: center;
      .title{
        padding: 0 25rpx;
        background: #fff;
      }
    }
    .b{
      padding: 0 30rpx;
      background: #fff;
      .item{
        padding-bottom: 25rpx;
        .question-box{
          display: flex;
          .spot{
            width: 8rpx;
            height: 8rpx;
            background: #b4282d;
            border-radius: 50%;
            margin-top: 11rpx;
          }
          .question{
            line-height: 30rpx;
            padding-left: 8rpx;
            display: block;
            font-size: 26rpx;
            padding-bottom: 15rpx;
            color: #303030;
          }
        }
        .answer{
          line-height: 40rpx;
          padding-left: 16rpx;
          font-size: 26rpx;
          color: #787878;
        }
      }
    }
    .sublist{
      display: flex;
      flex-wrap: wrap;
      justify-content: space-between;
      width: 730rpx;
      margin: 0 auto;
      div{
        width: 360rpx;
        background: #fff;
        margin-bottom: 10rpx;
        padding-bottom: 10rpx;
        img{
          display: block;
          width: 302rpx;
          height: 302rpx;
          margin: 0 auto;
        }
        p{
          margin-bottom: 5rpx;
          text-indent: 1em;
        }
        p:nth-child(2) {
          overflow: hidden;
          text-overflow: ellipsis;
          white-space: nowrap;
          width: 98%;
        }
        p:nth-child(3) {
          color: #9c3232;
        } 
      }
    }
  }
  .bottom-fixed{
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    width: 750rpx;
    height: 100rpx;
    display: flex;
    background: #fff;
    z-index: 10;
    .collect-box{
      height: 100rpx;
      width: 162rpx;
      border: 1rpx solid #f4f4f4;
      display: flex;
      align-items: center;
      justify-content: center;
      .collect{
        display: block;
        height: 44rpx;
        width: 44rpx;
        background: url('../../../static/images/icon_collect.png') no-repeat;
        background-size: 100% 100%;
      }
      .collect.active{
        display: block;
        height: 44rpx;
        width: 44rpx;
        background: url('../../../static/images/icon_collect_checked.png') no-repeat;
        background-size: 100% 100%;
      }
    }
    .car-box{
      height: 100rpx;
      width: 162rpx;
      border: 1rpx solid #f4f4f4;
      display: flex;
      align-items: center;
      justify-content: center;
      .car{
        position: relative;
        width: 60rpx;
        height: 60rpx;
        span{
          position: absolute;
          top: 0;
          right: 0;
          width: 28rpx;
          height: 28rpx;
          z-index: 10;
          background: #b4282d;
          text-align: center;
          font-size: 18rpx;
          color: #fff;
          line-height: 28rpx;
          border-radius: 50%;
        }
        img{
          display: block;
          height: 44rpx;
          width: 44rpx;
          position: absolute;
          top: 10rpx;
          left: 0;
        }
      }
    }
    div:nth-child(3) {
      height: 100rpx;
      line-height: 96rpx;
      flex: 1;
      text-align: center;
      color: #333;
      border-top: 1rpx solid #f4f4f4;
      border-bottom: 1rpx solid #f4f4f4;
    }
    div:nth-child(4) {
      height: 100rpx;
      line-height: 96rpx;
      flex: 1;
      text-align: center;
      color: #fff;
      background: #b4282d;
      border: 1rpx solid #b4282d;
      float: left;
    }
  }
}

8. index 首页

index.vue
<template>
  <div class="index">
    <!-- 头部的搜索 -->
    <div class="search">
      <div @click="toMappage">{{cityName}}</div>
      <div @click="toSearch">
        <input type="text" placeholder="搜索商品" />
        <span class="icon"></span>
      </div>
    </div>
    <div class="swiper">
      <swiper class="swiper-container" indicator-dots="true" autoplay="true" interval="3000" circular="true" duration="500">
        <block v-for="(item, index) in banner" :key="index">
          <swiper-item class="swiper-item">
            <image class="slide-image" :src="item.image_url"/>
          </swiper-item>
        </block>
      </swiper>
    </div>
    <div class="channel">
      <div v-for="(item, index) in channel" :key="index" @click="categroyList(item.id)">
        <img :src="item.icon_url" alt="">
        <p>{{item.name}}</p>
      </div>
    </div>
    <div class="brand">
      <div class="head" @click="tobrandList">
        品牌制造商直供
      </div>
      <div class="content">
        <div v-for="(item, index) in brandList" :key="index" @click="branddetail(item.id)">
          <div>
            <p>{{item.name}}</p>
            <p class="price">{{item.floor_price}}元起</p>
          </div>
          <img :src="item.new_pic_url" alt="">
        </div>
      </div>
    </div>
    <div class="newgoods">
      <div class="newgoods-top" @click="goodsList('new')">
        <div class="top">
          <p>新品首发</p>
          <p>查看全部</p>
        </div>
      </div>
      <div class="list">
        <ul>
          <scroll-view class="scroll-view" :scroll-x="true">
            <li v-for="(item ,index) in newGoods" :key="index">
              <img :src="item.list_pic_url" alt="">
              <p>{{item.name}}</p>
              <p>{{item.goods_brief}}</p>
              <p>¥{{item.retail_price}}</p>
            </li>
          </scroll-view>
        </ul>
      </div>
    </div>
    <div class="newgoods hotgoods">
      <div class="newgoods-top" @click="goodsList('hot')">
        <div class="top">
          <p>
            人气推荐
            <span></span>
            好物精选
          </p>
          <p>查看全部</p>
        </div>
      </div>
      <div class="list">
        <ul>
          <scroll-view class="scroll-view" :scroll-x="true">
            <li v-for="(item ,index) in hotGoods" :key="index">
              <img :src="item.list_pic_url" alt="">
              <p>{{item.name}}</p>
              <p>{{item.goods_brief}}</p>
              <p>¥{{item.retail_price}}</p>
            </li>
          </scroll-view>
        </ul>
      </div>
    </div>
    <div class="topicList">
      <div class="topicList-top">
        专题精选
        <span class="icon"></span>
      </div>
      <div class="list">
        <ul>
          <scroll-view class="scroll-view" :scroll-x="true">
            <li v-for="(item ,index) in topicList" :key="index" @click="topicdetail(item.id)">
              <img :src="item.item_pic_url" alt="">
              <div class="btom">
                <div>
                  <p>{{item.title}}</p>
                  <p>{{item.subtitle}}</p>
                </div>
                <div>{{item.price_info}}元起</div>
              </div>
            </li>
          </scroll-view>
        </ul>
      </div>
    </div>
    <div class="newcategory">
      <div class="list" v-for="(item ,index) in newCategoryList" :key="index">
        <div class="head">{{item.name}}好物</div>
        <div class="sublist">
          <div v-for="(subitem, subindex) in item.goodsList" :key="subindex">
            <img :src="subitem.list_pic_url" alt="">
            <p>{{subitem.name}}</p>
            <p>{{subitem.retail_price}}</p>
          </div>
          <div>
            <div class="last">
              <p>{{item.name}}好物</p>
              <span class="icon"></span>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import amapFile from '../../utils/amap-wx.js'
import { mapState, mapMutations } from 'vuex'
import { get } from '../../utils'
export default {
  data () {
    return {
      // 轮播图
      banner: [],
      channel: [],
      brandList: [],
      newGoods: [],
      hotGoods: [],
      topicList: [],
      newCategoryList: []
    }
  },
  computed: {
    ...mapState(['cityName'])
  },
  // 放置接口请求的方法
  mounted () {
    // 获取首页接口数据
    this.getData()
    this.getCityName()
  },
  methods: {
    ...mapMutations(['update']),
    toMappage () {
      // 通过 wx.getSetting 先查询一下用户是否授权 “scoped.record”
      let _this = this
      wx.getSetting({
        success: (res) => {
          // 如果没有同意授权,打开设置
          // console.log(res)
          if (!res.authSetting['scope.userLocation']) {
            wx.openSetting({
              success: res => {
                // 获取授权位置信息
                _this.getCityName()
              }
            })
          } else {
            wx.navigateTo({
              url: '/pages/mappage/main',
            });
            // _this.getCityName()
          }
        },
        fail: (err) => {
          console.log(err)
        },
        complete: () => {}
      });
        
    },
    getCityName () {
      let _this = this
      var myAmapFun = new amapFile.AMapWX({key:'256d94ac927c73a25e9177d789a1d060'});
      myAmapFun.getRegeo({
        success: function (data) {
          // 成功回调
          console.log(data)
          // ........
        },
        fail: function (info) {
          // 失败回调
          console.log(info)
          // _this.cityName = '北京'
          _this.update({ cityName: '北京' })
        }
      })
    },
    // 获取首页接口数据
    async getData() {
      const data = await get('/index/index') // http://localhost:5757/lm/index/index
      console.log(data)
      this.banner = data.banner
      this.channel = data.channel
      this.brandList = data.brandList
      this.newGoods = data.newGoods
      this.hotGoods = data.hotGoods
      this.topicList = data.topicList
      this.newCategoryList = data.newCategoryList
    },
    toSearch () {
      wx.navigateTo({
        url: '/pages/search/main'
      })
    },
    categroyList (id) {
      console.log(123)
      wx.navigateTo({
        url: '/pages/categroylist/main?id=' + id
      })
    },
    branddetail (id) {
      wx.navigateTo({
        url: '/pages/branddetail/main?id=' + id
      })
    },
    tobrandList () {
      wx.navigateTo({
        url: '/pages/brandlist/main'
      })
    },
    goodsList (info) {
      if (info == 'hot') {
        wx.navigateTo({
          url: '/pages/newgoods/main?isHot=' + 1
        })
      } else {
        wx.navigateTo({
          url: '/pages/newgoods/main?isNew=' + 1
        })
      }
    },
    topicdetail (id) {
      wx.navigateTo({
        url: '/pages/topicdetail/main?id=' + id
      })
    }
  }
}
</script>

<style lang="less" scoped>
@import "./style.less";
</style>
main.js
import Vue from 'vue'
import App from './index'

const app = new Vue(App)
app.$mount()

style.less
.index{
  width: 100%;
  overflow: hidden;
  position: relative;
  .search{
    width: 100%;
    box-sizing: border-box;
    padding: 0 25rpx 0 10rpx;
    position: fixed;
    top: 0;
    z-index: 99;
    height: 80rpx;
    display: flex;
    align-items: center;
    background: #fff;

    div:nth-child(1) {
      width: 115rpx;
      text-align: center;
      overflow: hidden;
      white-space: nowrap;
      text-overflow: ellipsis;
      font-size: 20rpx;
      padding-right: 15rpx;
    }

    div:nth-child(2) {
      flex: 1;
      position: relative;

      input{
        width: 100%;
        height: 56rpx;
        border-radius: 8rpx;
        background: #ededed;
        box-sizing: border-box;
        padding-left: 40rpx;
      }
      .icon{
        position: absolute;
        top: 15rpx;
        left: 10rpx;
        background: url('http://yanxuan.nosdn.127.net/hxm/yanxuan-wap/p/20161201/style/img/icon-normal/search2-2fb94833aa.png') center no-repeat;
        background-size: 100%;
        width: 28rpx;
        height: 28rpx;
        margin-right: 10rpx;
      }
    }
  }
  .swiper{
    width: 100%;
    height: 417rpx;
    margin-top: 80rpx;
    &-container{
      width: 100%;
      height: 100%;
      .swiper-item{
        width: 100%;
        height: 100%;
        .slide-image{
          width: 100%;
        }
      }
    }
  }
  .channel{
    display: flex;
    padding: 20rpx 0;
    background-color: #fff;
    div{
      flex: 1;
      text-align: center;
      img{
        height: 58rpx;
        width: 58rpx;
        display: inline-block;
      }
    }
  }
  .brand{
    width: 100%;
    margin-top: 20rpx;
    background: #ffffff;
    .head{
      text-align: center;
      padding: 40rpx 0;
    }
    .content{
      width: 730rpx;
      margin: 0 auto;
      display: flex;
      justify-content: space-between;
      flex-wrap: wrap;
      div{
        width: 360rpx;
        height: 235rpx;
        margin-bottom: 10rpx;
        position: relative;
        div{
          position: absolute;
          top: 0;
          left: 0;
          padding: 10rpx;

          .price{
            font-size: 24rpx;
          }
        }
        img{
          width: 100%;
          height: 100%;
        }
      }
    }
  }
  .newgoods{
    width: 100%;
    &-top{
      margin-top: 20rpx;
      height: 260rpx;
      width: 100%;
      background: url('../../../static/images/bgnew.png') no-repeat;
      background-size: 100% 100%;
      text-align: center;
      display: flex;
      justify-content: center;
      align-items: center;
      .top{
        p{
          color: #8c9bae;
          font-size: 32rpx;
        }
        p:nth-child(2) {
          width: 180rpx;
          height: 50rpx;
          line-height: 50rpx;
          margin: 27rpx auto 0 auto;
          font-size: 22rpx;
          background: #d8e4f0;
        }
      }
    }
    .list{
      margin-top: 20rpx;
      background-color: #fff;
      padding-bottom: 10rpx;
      ul{
        .scroll-view{
          width: 100%;
          white-space: nowrap;

          li{
            width: 280rpx;
            height: 416rpx;
            margin: 5rpx 0 5rpx 25rpx;
            display: inline-block;

            img{
              width: 280rpx;
              height: 280rpx;
            }
            p{
              width: 94%;
              overflow: hidden;
              white-space: nowrap;
              text-overflow: ellipsis;
              margin-top: 8rpx;
              text-indent: 1em;
            }
            p:nth-child(2) {
              font-size: 30rpx;
              font-weight: bold;
            }
            p:nth-child(3) {
              font-size: 24rpx;
              color: #8a8a8a;
            }
            p:nth-child(4) {
              font-size: 24rpx;
              color: #9c3232;
            }
          }
        }
      }
    }
  }
  .hotgoods{
    .newgoods-top{
      background: url('../../../static/images/bgtopic.png') no-repeat;
      background-size: 100% 100%;
      .top{
        p{
          color: #b1a279;
          font-size: 32rpx;
          vertical-align: middle;
        }
        p:nth-child(1) {
          span{
            width: 4rpx;
            height: 4rpx;
            font-size: 14rpx;
            display: inline-block;
            vertical-align: middle;
            background-color: #b1a279;
          }
        }
        p:nth-child(2) {
          background: #f4e9cb;
          font-size: 22rpx;
        }
      }
    }
  }
  .topicList{
    margin-top: 20rpx;
    background-color: #fff;
    &-top{
      text-align: center;
      padding: 36rpx;
      vertical-align: middle;
      .icon{
        display: inline-block;
        width: 32rpx;
        height: 32rpx;
        margin-left: 5rpx;
        background: url('../../../static/images/right.png') no-repeat;
        background-size: 100% 100%;
        vertical-align: middle;
      }
    }
    .list{
      .scroll-view{
        white-space: nowrap;

        li{
          display: inline-block;
          width: 575rpx;
          margin-left: 25rpx;
          img{
            display: block;
            width: 575rpx;
            height: 325rpx;
            border-radius: 10rpx;
          }
          .btom{
            display: flex;
            justify-content: space-between;
            margin-top: 42rpx;
            width: 100%;
            div:nth-child(1) {
              width: 90%;
              p{
                margin-top: 8rpx;
              }
              p:nth-child(1) {
                font-size: 30rpx;
                font-weight: bold;
              }
              p:nth-child(2) {
                width: 90%;
                font-size: 24rpx;
                color: #8a8a8a;
                overflow: hidden;
                text-overflow: ellipsis;
                white-space: nowrap;
              }
            }
            div:nth-child(2) {
              margin-top: 8rpx;
              color: #9c3232;
              font-size: 24rpx;
            }
          }
        }
        li:last-child{
          margin-right: 25rpx;
        }
      }
    }
  }
  .newcategory {
    margin-top: 20rpx;
    padding: 0 10rpx 25rpx 10rpx;

    .head {
      padding: 25rpx 0;
      text-align: center;
    }

    .sublist {
      display: flex;
      flex-wrap: wrap;
      justify-content: space-between;
      width: 730rpx;
      margin: 0 auto;

      div {
        width: 360rpx;
        background: #fff;
        margin-bottom: 10rpx;
        padding-bottom: 10rpx;

        img {
          display: block;
          width: 302rpx;
          height: 302rpx;
          margin: 0 auto;
        }

        p {
          margin-bottom: 5rpx;
          text-indent: 1em;
        }

        p:nth-child(2) {
          overflow: hidden;
          text-overflow: ellipsis;
          white-space: nowrap;
          width: 98%;
        }

        p:nth-child(3) {
          color: #9c3232;
        }
      }

      .last {
        display: block;
        width: 302rpx;
        height: 302rpx;
        margin: 0 auto;
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        flex-wrap: wrap;

        p {
          height: 33rpx;
          width: 100%;
          line-height: 33rpx;
          color: #333;
          font-size: 33rpx;
          text-align: center;
        }

        .icon {
          display: inline-block;
          width: 70rpx;
          height: 70rpx;
          background: url('../../../static/images/rightbig.png') no-repeat;
          background-size: 100% 100%;
          margin-top: 60rpx;
        }
      }

      div:nth-child(2n) {
        margin-left: 10rpx;
      }
    }
  }
}

9. mappage

index.vue
<template>
  <div class="mappage">
    <div class="section">
      <input type="text" placeholder="搜索" focus="true" v-model="keywords" @input="bindInput">
    </div>
    <scroll-view :scroll-y="true" class="addcont" style="height: 500rpx;">
      <div class="result" @touchstart="bindSearch(item.name)" v-for="(item, index) in tips" :key="index">
        {{item.name}}
      </div>
    </scroll-view>

    <div class="map_container">
      <div class="title">显示当前位置:</div>
      <map class="map" id="map" scale="16" :longitude="longitude" :latitude="latitude" :markers="markers"></map>
    </div>
  </div>
</template>

<script>
import amapFile from '../../utils/amap-wx'
import { mapMutations } from 'vuex'
export default {
  data () {
    return {
      tips: [],
      longitude: 0,
      latitude: 0,
      markers: [],
      keywords: ''
    }
  },
  mounted() {
    this.getMapaddress()
  },
  methods: {
    ...mapMutations(['update']),
    getMapaddress () {
      let _this = this
      var myAmapFun = new amapFile.AMapWX({key:'256d94ac927c73a25e9177d789a1d060'})
      myAmapFun.getRegeo({
        iconPath: "/static/images/marker.png",
        iconWidth: 22,
        iconHeight: 32,
        success(data) {
          console.log(data)
          let marker = [
            {
              id: data[0].id,
              latitude: data[0].latitude,
              longitude: data[0].longitude,
              width: data[0].width,
              height: data[0].height
            }
          ]
          _this.markers = marker
          _this.longitude = data[0].longitude
          _this.latitude = data[0].latitude
        },
        fail (info) {
          console.log(info)
        }
      })
    },
    bindInput(e) {
      // console.log(e)
      let _this = this
      let keywords = _this.keywords
      var myAmapFun = new amapFile.AMapWX({key:'256d94ac927c73a25e9177d789a1d060'})
      myAmapFun.getInputtips({
        keywords: keywords,
        location: '',
        success: function(data) {
          // console.log(data)
          if (data && data.tips) {
            _this.tips = data.tips
          }
        }
      })
    },
    bindSearch (cityName) {
      this.update({cityName: cityName})
      wx.navigateBack({
        delta: 1
      });
    }
  }
}
</script>

<style lang="less" scoped>
@import "./style.less";
</style>
main.js
import Vue from 'vue'
import App from './index'

const app = new Vue(App)
app.$mount()

style.less
.mappage{
  height: 100%;
  background: #fff;
  position: relative;
  .section{
    height: 30px;
    width: 100%;
    input{
      width: 90%;
      margin: 0 auto;
      border: 1px solid #c3c3c3;
      height: 30px;
      border-radius: 3px;
      padding: 0 5px;
    }
  }
  .result{
    width: 40%;
    padding: 20rpx 0 20rpx 30rpx;
  }

  .map_container{
    position: absolute;
    bottom: 0;
    left: 0;
    right: 0;

    .title{
      font-size: 34rpx;
      font-weight: bold;
      padding: 20rpx;
    }

    .map{
      width: 100%;
      height: 500rpx;
    }
  }
}

10. my

index.vue
<template>
  <div class="my">
    <div class="myinfo" @click="toLogin">
      <img :src="avator" alt />
      <div>
        <p>{{userInfo.nickName}}</p>
        <p v-if="userInfo.nickName">微信用户</p>
        <p v-else>点击登录账号</p>
      </div>
    </div>
    <div class="iconlist">
      <div @click="goTo(item.url)" v-for="(item, index) in listData" :key="index">
        <span class="iconfont" :class="item.icon"></span>
        <span>{{item.title}}</span>
      </div>
    </div>
  </div>
</template>

<script>
import { get, login } from '../../utils'
export default {
  data() {
    return {
      listData: [
        {
          title: "我的订单",
          icon: "icon-unie64a",
          url: ""
        },
        {
          title: "优惠券",
          icon: "icon-youhuiquan",
          url: ""
        },
        {
          title: "我的足迹",
          icon: "icon-zuji",
          url: ""
        },
        {
          title: "我的收藏",
          icon: "icon-shoucang",
          url: "/pages/collectlist/main"
        },
        {
          title: "地址管理",
          icon: "icon-dizhiguanli",
          url: "/pages/address/main"
        },
        {
          title: "联系客服",
          icon: "icon-lianxikefu",
          url: ""
        },
        {
          title: "帮助中心",
          icon: "icon-bangzhuzhongxin",
          url: ""
        },
        {
          title: "意见反馈",
          icon: "icon-yijianfankui",
          url: "/pages/feedback/main"
        }
      ],
      avator: 'http://yanxuan.nosdn.127.net/8945ae63d940cc42406c3f67019c5cb6.png',
      allcheck: false,
      userInfo: {},
      Listids: []
    };
  },
   onLoad(options) {
    wx.setNavigationBarTitle({
      title: '个人中心',
    })
  },
  // 小程序自带的生命周期函数
  onShow () {
    if (login()) {
      this.userInfo = login()
      console.log(this.userInfo)
      this.avator = this.userInfo.avatarUrl
    }
  },
  methods: {
    goTo (url) {
      wx.navigateTo({
        url: url
      });
    },
    // 跳转到登录页面
    toLogin() {
      wx.login({
      success: function(res) {
         wx.setStorageSync("openId", res.code);
      }
      });
       wx.navigateTo({
        url: '/pages/login/main'
      });
    }
  }
};
</script>

<style lang="less" scoped>
@import "./style.less";
</style>
main.js
import Vue from 'vue'
import App from './index'

const app = new Vue(App)
app.$mount()

style.less
.my {
  .myinfo {
    width: 100%;
    height: 280rpx;
    display: flex;
    align-items: center;
    background: #333;
    padding: 0 30rpx;
    box-sizing: border-box;
    img {
      height: 148rpx;
      width: 148rpx;
      border-radius: 50%
    }
    div {
      margin-left: 30rpx;
      p {
        color: #fff;
        font-size: 30rpx;
        margin-bottom: 10rpx
      }
      p:nth-child(2) {
        font-size: 28rpx;
      }
    }
  }
  .iconlist {
    display: flex;
    align-items: center;
    background: #fff;
    flex-wrap: wrap;
    div {
      width: 33.33%;
      padding: 50rpx 0;
      text-align: center;
      border-right: 1rpx solid rgba(0, 0, 0, .15);
      border-bottom: 1rpx solid rgba(0, 0, 0, .15);
      box-sizing: border-box;
      span {
        display: block;
      }
      span:nth-child(1) {
        margin-bottom: 10rpx;
      }
    }
    div:nth-child(3n+3) {
      border-right: none;
    }
    div:nth-last-child(1) {
      border-bottom: none;
    }
    div:nth-last-child(2) {
      border-bottom: none;
    }
  }
}

11. order

index.vue
<template>
  <div class="order">
    <div class="address" v-if="address.name" @click="toAddressList">
      <div class="item">
        <div class="list">
          <div class="addresslist">
            <div>
              <span>{{address.name}}</span>
              <div class="moren">默认</div>
            </div>
            <div class="info">
              <p>{{address.mobile}}</p>
              <p>{{address.address+address.address_detail}}</p>
            </div>
            <div></div>
          </div>
        </div>
      </div>
    </div>
    <div class="seladdress" v-else @click="toAdd">请选择默认地址</div>

    <div class="orderbox">
      <div class="item">
        <div>商品合计</div>
        <div>¥{{allprice}}</div>
      </div>
      <div class="item">
        <div>运费</div>
        <div>免运费</div>
      </div>
      <div class="item">
        <div>优惠券</div>
        <div>暂无</div>
      </div>
    </div>

    <div class="cartlist">
      <div class="item" v-for="(item, index) in listData" :key="index">
        <div class="con">
          <div class="left">
            <div class="img">
              <img :src="item.list_pic_url" alt="">
            </div>
            <div class="info">
              <p>{{item.goods_name}}</p>
              <p>¥{{item.retail_price}}</p>
            </div>
          </div>
          <div class="right">
            <div class="num">x{{item.number}}</div>
          </div>
        </div>
      </div>
    </div>

    <div class="bottom">
      <div>实付:¥ {{allprice}}</div>
      <div class="pay" @click="pay">支付</div>
    </div>
  </div>
</template>

<script>
import { get, post, getStorageOpenid } from '../../utils'
export default {
  data () {
    return {
      address: {},  // 收货地址
      price: '',  // 单价
      allprice: '',  // 总价格
      openId: '',   // 用户授权id
      addressId: '',  // 收货地址id
      listData: []  // 列表数据
    }
  },
  // 小程序自带的生命周期
  onShow () {
    if (wx.getStorageSync('addressId')) {
      this.addressId = wx.getStorageSync('addressId')
    }
    // 获取用户的标识:openId
    this.openId = getStorageOpenid()
    this.getDetail()
  },
  methods: {
    // 跳转到:地址列表页面
    toAddressList () {
      wx.navigateTo({
        url: '/pages/addressSelect/main'
      })
    },
    // 选择收货地址
    toAdd () {
      wx.navigateTo({
        url: '/pages/addaddress/main'
      })
    },
    async getDetail () {
      const data = await get('/order/detailAction', {
        openId: this.openId,
        addressId: this.addressId
      })
      console.log(data)
      if (data) {
        // this.allprice = data.price
        this.listData = data.goodsList
        this.address = data.address
      }
      // 计算总价格
      this.listData.map((item) => {
        this.allprice = Number(item.retail_price * item.number) + Number(this.allprice)
      })
    },
    // 支付功能
    pay () {
      wx.showToast({
        title: '支付功能暂未开发',
        icon: 'none',
        duration: 1500,
        mask: false,
        success: res => {}
      })
    }
  }
}
</script>

<style lang="less" scoped>
@import './style.less';
</style>
main.js
import Vue from 'vue'
import App from './index'

const app = new Vue(App)
app.$mount()

style.less
.order {
  overflow-x: hidden;
  .seladdress {
    width: 100%;
    min-height: 166rpx;
    background: url('http://yanxuan.nosdn.127.net/hxm/yanxuan-wap/p/20161201/style/img/icon-normal/address-bg-bd30f2bfeb.png') 0 0 repeat-x #fff;
    background-size: 62rpx 10rpx;
    margin-bottom: 20rpx;
    padding-top: 10rpx;
    text-align: center;
    line-height: 166rpx;
  }
  .address {
    background: url('http://yanxuan.nosdn.127.net/hxm/yanxuan-wap/p/20161201/style/img/icon-normal/address-bg-bd30f2bfeb.png') 0 0 repeat-x #fff;
    padding: 50rpx 0 30rpx 0;
    margin-bottom: 20rpx;
    .item {
      padding: 0 20rpx;
      .addresslist {
        width: 100%;
        position: relative;
        transition: all 300ms ease;
        display: flex;
        justify-content: space-between;
        align-items: center;
        div:nth-child(1) {
          width: 100rpx;
          text-align: center;
          overflow: hidden;
          text-overflow: ellipsis;
          white-space: nowrap;
          align-self: flex-start;
          .moren {
            width: 60rpx;
            height: 30rpx;
            border: 1rpx solid #b4282d;
            text-align: center;
            line-height: 30rpx;
            color: #b4282d;
            margin: 10rpx auto 0 auto;
          }
        }
        .info {
          padding: 0 20rpx;
          flex: 1; // p:nth-child(1) {}
          p:nth-child(2) {
            margin-top: 5rpx;
            color: #666;
            overflow: hidden;
            text-overflow: ellipsis;
            display: -webkit-box;
            -webkit-line-clamp: 2;
            -webkit-box-orient: vertical;
          }
        }
        div:nth-child(3) {
          width: 50rpx;
          height: 50rpx;
          margin: 0 20rpx;
          background: url('http://yanxuan-static.nosdn.127.net/hxm/yanxuan-wap/p/20161201/style/img/icon-normal/address-right-596d39df1e.png') no-repeat;
          background-size: 100% 100%;
        }
      }
    }
  }
  .orderbox {
    padding: 0 30rpx;
    background: #ffffff;
    .item {
      padding: 30rpx 0;
      display: flex;
      justify-content: space-between;
      border-bottom: 1rpx solid #d9d9d9;
    }
    .item:last-child {
      border: none;
    }
  }
  .cartlist {
    background: #fff;
    margin-bottom: 110rpx;
    margin-top: 20rpx;
    .item {
      padding: 20rpx 0;
      border-bottom: 1rpx solid #f4f4f4; // height: 166rpx;
      position: relative;
      .con {
        display: flex;
        align-items: center;
        justify-content: space-between;
        transition: all 300ms ease;
        .left {
          display: flex;
          align-items: center;
          width: 80%;
          .img {
            height: 125rpx;
            width: 125rpx;
            display: block;
            background: #f4f4f4;
            margin-left: 20rpx;
            img {
              width: 100%;
              height: 100%;
            }
          }
          .info {
            width: 50%;
            padding: 20rpx;
            p {
              line-height: 40rpx;
            }
          }
        }
        .right {
          padding-right: 50rpx;
        }
      }
    }
  }
  .bottom {
    position: fixed;
    bottom: 0;
    height: 100rpx;
    width: 100%;
    display: flex;
    background: #fff;
    font-size: 32repx;
    div:nth-child(1) {
      flex: 1;
      line-height: 100rpx;
      padding-left: 20rpx
    }
    div:nth-child(2) {
      width: 200rpx;
      height: 100rpx;
      text-align: center;
      line-height: 100rpx;
      font-size: 29rpx;
      background: #b4282d;
      color: #fff;
    }
  }
}

12. search

index.vue
<template>
  <div class="search">
    <div class="head">
      <div>
        <img
          src="http://nos.netease.com/mailpub/hxm/yanxuan-wap/p/20150730/style/img/icon-normal/search2-2fb94833aa.png"
          alt
        />
        <input
          type="text"
          confirm-type="search"
          focus="true"
          v-model="words"
          @focus="inputFocus"
          @input="tipsearch"
          @confirm="searchWords"
          placeholder="商品搜索"
        />
        <img
          @click="clearInput"
          class="del"
          src="http://nos.netease.com/mailpub/hxm/yanxuan-wap/p/20150730/style/img/icon-normal/clearIpt-f71b83e3c2.png"
          alt
        />
      </div>
      <div @click="cancel">取消</div>
    </div>
    <div class="searchtips" v-if="words">
      <div v-if="tipsData.length != 0">
        <div
          v-for="(item, index) in tipsData"
          :key="index"
          @click="searchWords"
          :data-value="item.name"
        >{{item.name}}</div>
      </div>
      <div class="nogoods" v-else>数据库暂无此类商品...</div>
    </div>

    <div class="history" v-if="historyData.length!==0">
      <div class="t">
        <div>历史记录</div>
        <div @click="clearHistory"></div>
      </div>
      <div class="cont">
        <div
          v-for="(item, index) in historyData"
          :key="index"
          @click="searchWords"
          :data-value="item.keyword"
        >{{item.keyword}}</div>
      </div>
    </div>

    <div class="history hotsearch">
      <div class="t">
        <div>热门搜索</div>
      </div>
      <div class="cont">
        <div
          v-for="(item, index) in hotData"
          :key="index"
          :class="{active: item.is_hot === 1}"
          @click="searchWords"
          :data-value="item.keyword"
        >{{item.keyword}}</div>
      </div>
    </div>

    <!-- 商品列表 -->
    <div class="goodsList" v-if="listData.length!==0">
      <div class="sortnav">
        <div @click="changeTab(0)" :class="[0 === nowIndex ? 'active' : '']">综合</div>
        <div @click="changeTab(1)" :class="[1 === nowIndex ? 'active' : '']" class="price">价格</div>
        <div @click="changeTab(2)" :class="[2 === nowIndex ? 'active' : '']">分类</div>
      </div>
      <div class="sortlist">
        <div
          @click="goodsDetail(item.id)"
          class="item"
          v-for="(item, index) in listData"
          :key="index"
        >
          <img :src="item.list_pic_url" alt />
          <p class="name">{{item.name}}</p>
          <p class="price">¥{{item.retail_price}}</p>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import { get, post, Debounce } from "../../utils";
export default {
  data () {
    return {
      words: "",
      openid: "",
      hotData: [], // 热门搜索
      historyData: [], // 历史搜索
      tipsData: [], // 模糊搜索的结果
      order: "",
      listData: [],
      nowIndex: 0
    };
  },
  watch: {
    // 如果 `formName` 发生改变,这个函数就会运行
    formName: function (newQuestion, oldQuestion) {
      this.debouncednewFormName();
    }
  },
  created: function () {
    // `debounce` 是一个限制操作频率的函数。防抖操作,在0.5秒内连续更改数据不进行查询
    this.debouncednewFormName = Debounce(this.tipsearch(), 500);
  },
  mounted () {
    // openid 是用户的唯一标识
    this.openid = wx.getStorageSync("openId") || "";
    this.getHotData();
  },
  methods: {
    // 点击输入框的叉号
    clearInput () {
      this.words = "";
      this.listData = [];
    },
    // 点击取消
    cancel () {
      wx.navigateBack({
        delta: 1
      });
    },
    // 清除历史记录
    async clearHistory () {
      const data = await post("/search/clearhistoryAction", {
        openId: this.openid
      });
      if (data) {
        this.historyData = [];
      }
    },
    // 输入框聚焦事件
    inputFocus () {
      // 商品清空
      this.listData = [];
      // 展示搜索提示信息
      this.tipsearch();
    },
    // 获取input值实时请求接口数据 优化点:添加防抖和节流,没效果??
    async tipsearch () {
      const data = await get("/search/helperaction", {
        keyword: this.words
      });
      this.tipsData = data.keywords;
    },
    /**
     * 在历史记录或者热门搜索的每一项 :data-value="item.keyword"
     * 然后用户点击历史记录或者热门搜索的选项,同步到搜索栏中:this.words = value || this.words
     * 这样就可以实现用户点击历史记录或者热门搜索的内容,实现同用户在搜索栏输入内容,同样的效果了
     */
    async searchWords (e) {
      let value = e.currentTarget.dataset.value;

      // 用户点击历史记录或者热门搜索的选项,同步到搜索栏中
      this.words = value || this.words;

      const data = await post("/search/addhistoryaction", {
        openId: this.openid,
        keyword: value || this.words
      });
      // console.log(data)
      // 用户搜索内容后--立即获取:历史数据和热门搜索
      this.getHotData();
      // 用户输入内容--调用获取商品列表的方法
      this.getlistData();
    },
    async getHotData (first) {
      const data = await get("/search/indexaction?openId=" + this.openid);
      this.historyData = data.historyData;
      this.hotData = data.hotKeywordList;
      // console.log(data)
    },
    async getlistData () {
      // 获取商品列表
      const data = await get("/search/helperaction", {
        keyword: this.words,
        order: this.order
      });
      this.listData = data.keywords;
      this.tipsData = [];
      console.log(data);
    },
    /**
     * 综合(0)、价格排序(1)、分类(2)
     */
    changeTab (index) {
      this.nowIndex = index;
      if (index === 1) {
        this.order = this.order == "asc" ? "desc" : "asc";
      } else {
        this.order = "";
      }
      // 根据不同的匹配规则,请求接口数据
      this.getlistData();
    },
    // 点击商品列表中的选项,跳转到商品详情页面
    goodsDetail (id) {
      wx.navigateTo({
        url: "/pages/goods/main?id=" + id
      });
    }
  }
};
</script>

<style lang='less' scoped>
@import './style';
</style>
main.js
import Vue from 'vue'
import App from './index'

const app = new Vue(App)
app.$mount()

style.less
.search{
  height: 100%;
  position: relative;
  .head{
    height: 91rpx;
    display: flex;
    padding: 0 32rpx;
    align-items: center;
    justify-content: space-between;
    background-color: #fff;
    border-bottom: 1rpx solid rgba(0, 0, 0, 0.15);
    div:nth-child(1) {
      height: 59rpx;
      display: flex;
      align-items: center;
      background: #f4f4f4;
      img{
        display: inline-block;
        width: 31rpx;
        height: 31rpx;
        padding: 0 20rpx;
      }
      input{
        display: inline-block;
        width: 480rpx;
        height: 59rpx;
        margin-left: 10rpx;
      }
      .del{
        width: 53rpx;
        height: 53rpx;
        padding: 0;
      }
    }
    div:nth-child(2) {
      flex: 1;
      text-align: center;
    }
  }
  .searchtips{
    position: absolute;
    width: 100%;
    top: 91rpx;
    left: 0;
    bottom: 0;
    box-sizing: border-box;
    padding: 0 32rpx;
    z-index: 9;
    background: #fff;
    overflow-y: scroll;
    -webkit-overflow-scrolling: touch;
    div{
      div{
        padding: 20rpx 0;
      }
    }
    .nogoods{
      text-align: center;
      margin-top: 300rpx;
    }

  }
  .history{
    background: #fff;
    padding: 32rpx;
    .t{
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin-bottom: 30rpx;
      div:nth-child(2) {
        width: 55rpx;
        height: 55rpx;
        background: url("http://nos.netease.com/mailpub/hxm/yanxuan-wap/p/20150730/style/img/icon-normal/del1-93f0a4add4.png") no-repeat;
        background-size: 100% 100%;
      }
    }
    .cont{
      display: flex;
      flex-wrap: wrap;
      div{
        padding: 10rpx;
        border: 1rpx solid #999;
        margin: 0 30rpx 20rpx 0;
      }
      .active{
        border: 1rpx solid #b4282d;
        color: #b4282d;
      }
    }
  }
  .hotsearch{
    margin-top: 20rpx;
  }
  .goodsList{
    position: absolute;
    width: 100%;
    top: 91rpx;
    left: 0;
    bottom: 0;
    box-sizing: border-box;
    padding: 0 32rpx;
    z-index: 9;
    background: #fff;
    overflow-y: scroll;
    -webkit-overflow-scrolling: touch;
    .sortnav{
      display: flex;
      width: 100%;
      height: 78rpx;
      line-height: 78rpx;
      background: #fff;
      border-bottom: 1rpx solid #d9d9d9;
      div{
        width: 250rpx;
        height: 100%;
        text-align: center;
      }
      .active{
        color: #b4282d;
      }
      .price {
        background: url(//yanxuan.nosdn.127.net/hxm/yanxuan-wap/p/20161201/style/img/icon-normal/no-3127092a69.png) 165rpx center no-repeat;
        background-size: 15rpx 21rpx;
      }
      .active.desc {
        background: url(http://yanxuan.nosdn.127.net/hxm/yanxuan-wap/p/20161201/style/img/icon-normal/down-95e035f3e5.png) 165rpx center no-repeat;
        background-size: 15rpx 21rpx;
      }
      .active.asc {
        background: url(http://yanxuan.nosdn.127.net/hxm/yanxuan-wap/p/20161201/style/img/icon-normal/up-636b92c0a5.png) 165rpx center no-repeat;
        background-size: 15rpx 21rpx;
      }
    }
    .sortlist{
      display: flex;
      justify-content: space-between;
      flex-wrap: wrap;
      .item{
        box-sizing: border-box;
        width: 50%;
        text-align: center;
        background-color: #fff;
        padding: 15rpx 0;
        border-bottom: 1rpx solid #d9d9d9;
        border-right: 1rpx solid #d9d9d9;
        img{
          display: block;
          width: 302rpx;
          height: 302rpx;
          margin: 0 auto;
        }
        .name{
          margin: 15rpx 0 22rpx 0;
          text-align: center;
          padding: 0 20rpx;
          font-size: 24rpx;
        }
        .price{
          text-align: center;
          font-size: 30rpx;
          color: #b4282d;
        }
      }
      .item:nth-child(2n) {
        border-right: none;
      }
      .item.active:nth-last-child(1) {
        border-bottom: none;
      }
      .item.active:nth-last-child(2) {
        border-bottom: none;
      }
      .item.none:last-child{
        border-bottom: none;
      }
    }
  }
}

13. topic

index.vue
<template>
  <div class="topic">
    <ui class="list">
      <li v-for="(item, index) in topicList" :key="index" @click="topicDetail(item.id)">
        <div class="t-img">
          <img :src="item.scene_pic_url" alt="">
        </div>
        <div class="info">
          <p>{{item.title}}</p>
          <p>{{item.subtitle}}</p>
          <p>{{item.price_info}}元起</p>
        </div>
      </li>
    </ui>
  </div>
</template>

<script>
import { get } from '../../utils'
export default {
  data () {
    return {
      page: 1,  // 当前是第几页
      topicList: [],  // 专题数据
      total: ''  // 总页数
    }
  },
  // 小程序自带的方法:下拉刷新
  onPullDownRefresh () {
    this.page = 1
    this.getListData()
    wx.stopPullDownRefresh()
  },
  // 小程序自带的方法:上拉加载更多
  onReachBottom () {
    this.page = this.page + 1
    if (this.page > this.total) {
      return false
    }
    this.getListData()
  },
  mounted () {
    // 初始化数据(传递参数true)
    this.getListData(true)
  },
  methods: {
    // 请求接口数据
    async getListData (first) {

      // 前端传递:获取第几页数据,就可以了
      const data = await get ('/topic/listaction', {
        page: this.page
      })
      console.log(data)
      this.total = data.total
      if (first) {
        // 说明:页面初次渲染,请求接口数据(只显示第一页,5条数据)
        this.topicList = data.data
      } else {
        // 如果没有参数 默认为false:说明执行下拉刷新 或者 上拉加载更多(需要拼接数据,显示在页面上)
        this.topicList = this.topicList.concat(data.data)
      }
    },
    // 跳转到商品详情页
    topicDetail (id) {
      wx.navigateTo({
        url: '/pages/topicdetail/main?id=' + id
      })
    }
  }
}
</script>

<style lang="less" scoped>
@import './style.less';
</style>
main.js
import Vue from 'vue'
import App from './index'

const app = new Vue(App)
app.$mount()

style.less
.topic {
  .list {
    li {
      background: #fff;
      text-align: center;
      padding-bottom: 20rpx;
      margin-bottom: 20rpx;
      .t-img {
        width: 100%;
        height: 415rpx;
        img {
          width: 100%;
          height: 100%;
        }
      }
      .info {
        p:nth-child(1) {
          color: #333;
          font-size: 35rpx;
          margin-top: 30rpx;
        }
        p:nth-child(2) {
          color: #999;
          font-size: 24rpx;
          margin-top: 16rpx;
          padding: 0 20rpx;
        }
        p:nth-child(3) {
          color: #b4282d;
          font-size: 27rpx;
          margin-top: 20rpx;
        }
      }
    }
  }
}

ssdf

14. topicdetail

index.vue
<template>
  <div class="topicdetail">
    <div class="content">
      <div class="detail" v-if="goods_desc">
        <wxParse :content="goods_desc" />
      </div>
    </div>
    <div class="list">
      <p class="title">专题推荐</p>
      <div class="item" v-for="(item, index) in recommendList" :key="index">
        <img :src="item.scene_pic_url" alt="">
        <p>{{item.title}}</p>
      </div>
    </div>
  </div>
</template>

<script>
import wxParse from 'mpvue-wxparse'
import { get } from '../../utils'
export default {
  components: {
    wxParse
  },
  data () {
    return {
      goods_desc: '',  // 专题详情图片(预览)
      id: '',
      recommendList: []
    }
  },
  mounted () {
    this.id = this.$root.$mp.query.id
    this.getListData()
  },
  methods: {
    async getListData () {
      const data = await get('/topic/detailaction', {
        id: this.id
      })
      console.log(data)
      this.goods_desc = data.data.content
      this.recommendList = data.recommendList
    }
  }
}
</script>

<style lang="less" scoped>
@import './style.less';
</style>
main.js
import Vue from 'vue'
import App from './index'

const app = new Vue(App)
app.$mount()

style.less
.topicdetail {
  .list {
    width: 690rpx;
    height: auto;
    margin: 0 30rpx;
    .title {
      text-align: center;
      background: #f4f4f4;
      font-size: 30rpx;
      color: #999;
      padding: 30rpx 0;
    }
    .item {
      width: 100%;
      padding: 24rpx 24rpx 30rpx 24rpx;
      margin-bottom: 30rpx;
      background: #fff;
      box-sizing: border-box;
      img {
        height: 278rpx;
        width: 642rpx;
        display: block;
      }
      p {
        display: block;
        margin-top: 30rpx;
        font-size: 28rpx;
      }
    }
  }
}

store / index.js

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

Vue.use(Vuex)

const store = new Vuex.Store({
  state: {
    cityName: '定位中..'
  },
  mutations: {
    update (state, config) {
      Object.keys(config).map((item, key) => {
        state[item] = config[item]
      })
    }
  }
})

export default store;

utils 目录

高德地图:amap-wx.js

需要自己去高德地图下载

https://lbs.amap.com/home/news/console/

工具类:index.js

function formatNumber (n) {
  const str = n.toString()
  return str[1] ? str : `0${str}`
}

export function formatTime (date) {
  const year = date.getFullYear()
  const month = date.getMonth() + 1
  const day = date.getDate()

  const hour = date.getHours()
  const minute = date.getMinutes()
  const second = date.getSeconds()

  const t1 = [year, month, day].map(formatNumber).join('/')
  const t2 = [hour, minute, second].map(formatNumber).join(':')

  return `${t1} ${t2}`
}


// ------------------------请求的封装

const host = "http://localhost:5757/lm"
export { host };

// 请求封装
function request (url, method, data, header = {}) {
  wx.showLoading({
    title: "加载中"
  });
  return new Promise((resolve, reject) => {
    wx.request({
      url: host + url,
      method: method,
      data: data,
      header: {
        "content-type": "application/json"
      },
      success (res) {
        wx.hideLoading();
        resolve(res.data)
      },
      fail (error) {
        wx.hideLoading();
        reject(false)
      },
      complete () {
        wx.hideLoading();
      }
    })
  })
}

export function get (url, data) {
  return request(url, 'GET', data)
}
export function post (url, data) {
  return request(url, 'POST', data)
}

// 获取用户的唯一标识opendid
export function getStorageOpenid() {
  const openId = wx.getStorageSync('openId')
  if (openId) {
    return openId
  } else {
    return ''
  }
}

// 用户登录,获取用户登录信息
export function login() {
  const userInfo = wx.getStorageSync('userInfo')
  if (userInfo) {
    return userInfo
  }
}

/**
 * 函数防抖 (只执行最后一次点击)
 * @param fn
 * @param delay
 * @returns {Function}
 * @constructor
 */
export const Debounce = (fn, t) => {
  const delay = t || 500
  let timer
  return function () {
    const args = arguments
    if (timer) {
      clearTimeout(timer)
    }
    timer = setTimeout(() => {
      timer = null
      fn.apply(this, args)
    }, delay)
  }
}
/**
* 函数节流
* @param fn
* @param interval
* @returns {Function}
* @constructor
*/
export const Throttle = (fn, t) => {
  let last
  let timer
  const interval = t || 500
  return function () {
    const args = arguments
    const now = +new Date()
    if (last && now - last < interval) {
      clearTimeout(timer)
      timer = setTimeout(() => {
        last = now
        fn.apply(this, args)
      }, interval)
    } else {
      last = now
      fn.apply(this, args)
    }
  }
}


export default {
  formatNumber,
  formatTime,
  getStorageOpenid
}

app.json

{
  "pages": [
    "pages/index/main",
    "pages/login/main",
    "pages/my/main",
    "pages/collectlist/main",
    "pages/category/main",
    "pages/topic/main",
    "pages/categroylist/main",
    "pages/cart/main",
    "pages/addressSelect/main",
    "pages/order/main",
    "pages/search/main",
    "pages/mappage/main",
    "pages/brandlist/main",
    "pages/newgoods/main",
    "pages/topicdetail/main",
    "pages/goods/main",
    "pages/addaddress/main"
  ],
  "permission": {
    "scope.userLocation": {
      "desc": "你的位置信息将用于小程序位置接口的效果展示"
    }
  },
  "window": {
    "backgroundTextStyle": "light",
    "navigationBarBackgroundColor": "#fff",
    "navigationBarTitleText": "微信小程序",
    "navigationBarTextStyle": "black",
    "enablePullDownRefresh": true
  },
  "tabBar": {
    "color": "#666",
    "backgroundColor": "#fafafa",
    "selectedColor": "#b4282d",
    "borderStyle": "white",
    "list": [
      {
        "pagePath": "pages/index/main",
        "iconPath": "static/images/ic_menu_choice_nor.png",
        "selectedIconPath": "static/images/ic_menu_choice_pressed.png",
        "text": "首页"
      },
      {
        "pagePath": "pages/topic/main",
        "iconPath": "static/images/ic_menu_topic_nor.png",
        "selectedIconPath": "static/images/ic_menu_topic_pressed.png",
        "text": "专题"
      },
      {
        "pagePath": "pages/category/main",
        "iconPath": "static/images/ic_menu_sort_nor.png",
        "selectedIconPath": "static/images/ic_menu_sort_pressed.png",
        "text": "分类"
      },
      {
        "pagePath": "pages/cart/main",
        "iconPath": "static/images/ic_menu_shoping_nor.png",
        "selectedIconPath": "static/images/ic_menu_shoping_pressed.png",
        "text": "购物车"
      },
      {
        "pagePath": "pages/my/main",
        "iconPath": "static/images/ic_menu_me_nor.png",
        "selectedIconPath": "static/images/ic_menu_me_pressed.png",
        "text": "我的"
      }
    ],
    "position": "bottom"
  }
}

App.vue

<script>
export default {
  created () {
    // 调用API从本地缓存中获取数据
    var userInfo = {
      openId: "023vXGet0wswDi1Iktft04mXet0vXGe5",
      nickName: "落花流雨",
      gender: 1,
      language: "zh_CN",
      city: "Changping",
      province: "Beijing",
      country: "China",
      avatarUrl: "https://wx.qlogo.cn/mmopen/vi_32/Q0j4TwGTfTIbWFEIJj8IpGeHM7dGic1aTFZALjWcMm9ltWfFiaQfVRYticWBfgGfzXWMt2EkJWiaicPtftHAlWxUibxQ/132",
      watermark: { timestamp: 1535513485, appid: "wxd93515f2d4e24e1d" }
    };
    var openId = userInfo.openId;
    wx.setStorageSync("userInfo", userInfo);
    wx.setStorageSync("openId", openId);
    /*
     * 平台 api 差异的处理方式:  api 方法统一挂载到 mpvue 名称空间, 平台判断通过 mpvuePlatform 特征字符串
     * 微信:mpvue === wx, mpvuePlatform === 'wx'
     * 头条:mpvue === tt, mpvuePlatform === 'tt'
     * 百度:mpvue === swan, mpvuePlatform === 'swan'
     * 支付宝(蚂蚁):mpvue === my, mpvuePlatform === 'my'
     */

    let logs
    if (mpvuePlatform === 'my') {
      logs = mpvue.getStorageSync({key: 'logs'}).data || []
      logs.unshift(Date.now())
      mpvue.setStorageSync({
        key: 'logs',
        data: logs
      })
    } else {
      logs = mpvue.getStorageSync('logs') || []
      logs.unshift(Date.now())
      mpvue.setStorageSync('logs', logs)
    }
  },
  log () {
    console.log(`log at:${Date.now()}`)
  }
}
</script>

<style>
@import url('./iconfont/iconfont.css');
.container {
  height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: space-between;
  padding: 200rpx 0;
  box-sizing: border-box;
}
page{
  background: #f4f4f4;
  height: 100%;
}
button{
  background: none;
  padding: 0;
  font-weight: normal;
  font-size: 32rpx;
  box-sizing: content-box;
}
button::after{
  border: 0;
}
view,text{
  font-size: 28rpx;
  color: #333333;
}
.wxParse .p{
  margin: 0 !important;
}
.wxParse .img{
  display: block !important;
}
</style>

main.js

import Vue from 'vue'
import App from './App'
import store from './store/index'
// 把store挂载到全局
Vue.prototype.$store = store

Vue.config.productionTip = false
App.mpType = 'app'

const app = new Vue(App)
app.$mount()

  • 1
    点赞
  • 0
    评论
  • 1
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 护眼 设计师:闪电赇 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值