网易严选小程序全栈开发
技术栈: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 | 全选和下单 |
商品分类列表:categorylist | tab切换(横向滚动) |
商品详情: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()