电商小程序(基于vue-uniapp)

项目结构

电商小程序结构.png
这里只做几个重要部分的代码展示 后期会放到github上

home

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
<template>
<view>
<!--自定义搜索组件-->
<view class = "search-box">
<my-search @click = "gotoSearch"></my-search>
</view>

<!--轮播图区域-->
<swiper :indicator-dots="true" :autoplay="true" :interval="3000" :duration="1000" :circular="true">
<swiper-item v-for="(item, i) in swiperList" :key="i">
<navigator class = "swiper_item" :url ="'/subpkg/goods_detail/goods_detail?goods_id=' + item.goods_id">
<image :src="item.image_src"></image>
</navigator>
</swiper-item>
</swiper>
<!--分类导航区域-->
<view class = "nav-list">
<view class = "nav-item" v-for = "(item,i) in navList " :key="i" @click="navClickHandler(item)">
<image :src = "item.image_src" class = "nav-img"></image>
</view>
</view>
<!-- 楼层区域 -->
<view class = "floor-list">
<view class = "nav-item" v-for = "(item, i) in floorList" :key = "i">
<image :src= "item.floor_title.image_src" class = "floor-title"></image>
<!--楼层的图片区域-->
<view class = "floor-img-box">
<!--左侧大盒子-->
<navigator class = "left-img-box" :url = "item.product_list[0].url">
<image :src="item.product_list[0].image_src" :style="{width:item.product_list[0].image_width+'rpx'}" mode="widthFix"></image>
</navigator>
<!--右侧小盒子-->
<view class = "right-img-box">
<navigator class = "right-img-item" v-for="(item2,i2) in item.product_list" :key="i2" :url = "item2.url">
<view v-if="i2 !== 0">
<image :src="item2.image_src" mode="widthFix" :style="{width:item2.image_width + 'rpx'}"></image>
</view>

</navigator>
</view>
</view>
</view>
</view>
</view>
</template>

<script>
import badgeMix from '@/mixins/tabbar-badge.js'

export default {
mixins: [badgeMix],
data() {
return {
//轮播图的数据列表
swiperList: [],
navList: [],
floorList: []
};
},
onLoad(){
this.getSwiperList()
this.getNavList()
this.getfloorList()
},
methods:{
async getSwiperList(){
const {data: res} = await uni.$http.get('/api/public/v1/home/swiperdata')
console.log(res)
//请求失败
if(res.meta.status !== 200){
return uni.$showMsg()
}
this.swiperList = res.message
//uni.$showMsg('数据请求成功')
},
async getNavList(){
const {data: res} = await uni.$http.get('/api/public/v1/home/catitems')
console.log(res)
//请求失败
if(res.meta.status !== 200){
return uni.$showMsg()
}
this.navList = res.message
//uni.$showMsg('数据请求成功')
},
async getfloorList(){
const {data: res} = await uni.$http.get('/api/public/v1/home/floordata')
console.log(res)
//请求失败
if(res.meta.status !== 200){
return uni.$showMsg()
}
//对数据进行处理
res.message.forEach(floor =>{
floor.product_list.forEach(prod =>{
prod.url = '/subpkg/goods_list/goods_list?' + prod.navigator_url.split('?')[1]
})
}
)
this.floorList = res.message
//uni.$showMsg('数据请求成功')
},
navClickHandler(item){
if(item.name === '分类'){
uni.switchTab({
url: '/pages/cate/cate'
})
}
},
gotoSearch(){
uni.navigateTo({
url:'/subpkg/search/search'
})
}
}
}
</script>

<style lang="scss">
swiper{
height: 330rpx;
.swiper_item,
image{
width:100%;
height:100%;
}
}
.nav-list{
display: flex;
justify-content: space-around;
margin: 15px 0;

.nav-img{
width:128rpx;
height:140rpx;
}
}
.floor-title{
height: 60rpx;
width: 100%;
display: flex;
}
.right-img-box{
display: flex;
flex-wrap: wrap;
justify-content: space-around;
}
.floor-img-box{
display: flex;
padding-left: 10rpx;
}
.search-box{
position: sticky;
top:0;
z-index: 999;
}
</style>

地址模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
<template>
<view>
<!-- 选择收获地址的盒子 -->
<view class = "address-choose-box" v-if="JSON.stringify(address) === '{}'">
<button type = "primary" size = "mini" class = "btnChooseAddress" @click="chooseAddress">+请选择收货地址</button>
</view>
<!-- 渲染收货信息的盒子 -->
<view class = "address-info-box" v-else @click="chooseAddress">
<view class = "row1">
<view class = "row1-left">
<view class = "username">收货人:{{address.userName}}</view>
</view>
<view class = "row1-right">
<view class = "phone">电话:{{address.telNumber}}</view>
<uni-icons type = "arrowright" size = "16"></uni-icons>
</view>
</view>
<view class = "row2">
<view class="row2-left">收货地址:</view>
<view class="row2-right">{{addstr}}</view>
</view>
</view>
<!-- 底部的边框线 -->
<image src="/static/cart_border@2x.png" class="address-border"></image>
</view>
</template>

<script>
import { mapState, mapMutations,mapGetters } from 'vuex'

export default {
name:"my-address",
computed:{
...mapState('m_user',['address']),
...mapGetters('m_user',['addstr'])

},
data() {
return {
//address:{},
};
},
methods:{
...mapMutations('m_user',['updateAddress']),
async chooseAddress(){
//chooseAddress实现收货地址功能
//返回值第一项是错误对象,第二项为成功之后的收货地址对象
const res = await uni.chooseAddress().catch(err => err)
console.log(res)
if(res.errMsg === 'chooseAddress:ok'){
// this.address =res
this.updateAddress(res)
}

console.log(this.address)
//if(err === null && succ.errMsg === 'chooseAddress:ok'){
//console.log(succ)
//this.address = succ
//console.log(this.address)
//}
//返回消息体改版了 不能解构出两条消息
}
}
}
</script>

<style lang="scss">
.address-border{
display: block;
height: 5px;
width: 100%;
}
.address-choose-box{
display: flex;
height: 90px;
justify-content: center;
align-items: center;
}
.address-info-box{
font-size: 12px;
height: 90px;
display: flex;
flex-direction: column;
justify-content: center;
padding: 0 5px;
.row1{
display: flex;
justify-content: space-between;
.row1-left{
.username{}
}
.row1-right{
display: flex;
justify-content: space-between;
.phone{}
}
}
.row2{
display: flex;
//justify-content: space-between;
align-items: center;
margin-top: 10px;
.row2-left{
white-space: nowrap;

}
.row2-right{

}
}
}
</style>

全局数据配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { mapGetters } from 'vuex'
export default {
computed:{
...mapGetters('m_cart',['total']),
},
onShow(){
this.setBadge()
},
watch:{
// 监听total值的变化
total(){
this.setBadge()
},
},
methods:{
setBadge(){
uni.setTabBarBadge({ //设置tabbar的徽标值
index: 2,//0 1 2 ..
text: this.total +'' //text的值必须是字符串
})
},
},
}

购物车

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
export default{ //default与{之间不能有空格
namespaced: true,
state:() => ({
cart:JSON.parse(uni.getStorageSync('cart') || '[]')
}),
mutations:{
addToCart(state, goods){
const findResult = state.cart.find((x) => x.goods_id === goods.goods_id)
console.log(findResult)
if(!findResult){
state.cart.push(goods)
}else{
findResult.goods_count++
}
console.log(state.cart)
this.commit('m_cart/saveToStorage')
},
saveToStorage(state){
uni.setStorageSync('cart',JSON.stringify(state.cart))
},
// 更新购物车中商品的勾选状态
updateGoodsState(state, goods){
const findResult = state.cart.find(x => x.goods_id === goods.goods_id)

if(findResult){
findResult.goods_state = goods.goods_state
this.commit('m_cart/saveToStorage')
}
},
// 更新购物车中商品的数量
updateGoodsCount(state,goods){
const findResult = state.cart.find(x => x.goods_id === goods.goods_id)

if(findResult){
findResult.goods_count = goods.goods_count
this.commit('m_cart/saveToStorage')
}
},
// 删除购物车商品
removeGoodsById(state,goods_id){
state.cart = state.cart.filter(x => x.goods_id !== goods_id)
this.commit('m_cart/saveToStorage')
},
updateAllGoodsState(state,newState){
// 更新状态
state.cart.forEach(x=>x.goods_state = newState)
// 持久化存储
this.commit('m_cart/saveToStorage')
}
},
getters:{

// 加入购物车商品的总数量
total(state){
// let c= 0
// state.cart.forEach(x => c+= x.goods_count)

// return c
return state.cart.reduce((total,item)=>total +=item.goods_count,0)
},
// 勾选商品的总数量
checkedCount(state){
return state.cart.filter(x => x.goods_state).reduce((total,item) => total += item.goods_count, 0)
},
checkedGoodsAmount(state){
return state.cart.filter(x => x.goods_state)
.reduce((total,item) => total += item.goods_count*item.goods_price, 0)
.toFixed(2)
}
}
}

微信支付

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
<template>
<view class="my-settle-container">
<!-- 全选 -->
<label class="radio" @click="changeAllState">
<radio color="#FF0000" :checked="isFullCheck" /><text>全选</text>
</label>
<!-- 合计 -->
<view class="amount-box">
合计:<text class = "amount">${{checkedGoodsAmount}}</text>
</view>

<!-- 结算 -->
<view class="btn-settle" @click="settlement">
结算({{checkedCount}})
</view>

</view>
</template>

<script>
import { mapGetters,mapMutations,mapState } from 'vuex'

export default {
computed:{
...mapGetters('m_cart',['checkedCount','total','checkedGoodsAmount']),
...mapGetters('m_user',['addstr']),
...mapState('m_user',['token']),
...mapState('m_cart',['cart']),
isFullCheck(){
return this.total === this.checkedCount
}
},
name:"my-settle",
data() {
return {
seconds:3,
timer:null
};
},
methods:{
...mapMutations('m_cart',['updateAllGoodsState']),
...mapMutations('m_user',['updateRedirectInfo']),
changeAllState(){
this.updateAllGoodsState(!this.isFullCheck)
},
// 三重判断 当用户点击了结算按钮
settlement(){
if(!this.checkedCount) return uni.$showMsg('请选择要结算的商品')
if(!this.addstr) return uni.$showMsg('请选择收货地址')
// if(!this.token) return uni.$showMsg('请先登录')
if(!this.token) return this.delayNavigate()

//实现微信支付功能
this.payOrder()
},
async payOrder(){
const orderInfo = {
order_price: 0.01,//写死总价格为0.01 checkedGoodsAmount真实总价格
consignee_addr: this.addstr,
goods:this.cart.filter(x => x.goods_state).map(x=>({
goods_id: x.goods_id,
goods_number: x.goods_count,
goods_price: x.goods_price
})),
}
const {data: res} = await uni.$http.post('/api/public/v1/my/orders/create',orderInfo)
//if(res.meta.status !== 200)return uni.$showMsg('创建订单失败')
//const orderNumber = res.message.order_number//获取订单编号
console.log(res)
//console.log(orderNumber)
//token有问题所以获取不到订单编号 代码思路没问题
//const orderNumber = 'GD20180504000000000045'
const {data: res2} = await uni.$http.post('/api/public/v1/my/orders/req_unifiedorder',orderNumber)
//if(res2.data.status !== 200)return uni.$showError('预付订单生成失败')
console.log('------------------------')
console.log(res2)
//拿到订单预支付相关属性
//const payInfo = res2.message.pay

const mes = await uni.requestPayment(payInfo)
//错误检测······
if(err) return uni.$showMsg('error')
const {data: res3} = await uni.$http.post('/api/public/v1/my/orders/chkOrder',{order_number: orederNumber})
//if(res3.data.status !== 200)return uni.$showError('订单未支付')
uni.showToast({
title:'finish',
icon:'success'
})
//微信支付分三大步
// 1.生成订单基本信息发送服务器获取订单编号 token-->订单编号orderNumber
// 2.订单预支付通过订单编号发送给服务器获取订单相关参数 订单编号-->订单相关参数payInfo
// 3.通过订单相关参数发起支付请求 订单相关参数payInfo--->uni.requestPayment(payInfo)
// 注意每一步应该做好错误检测
},
// 展示倒计时提示消息
showTips(n){
uni.showToast({
icon:'none',
title:'请登陆后结算'+n+'秒后自动跳转到登录页面',
// 为页面贴加透明遮罩 防止点击穿透
mask:true,
// 1.5s后自动消失
duration:1500
})
},
delayNavigate(){
this.seconds = 3
this.showTips(this.seconds)
// 创建定时器 每隔一秒执行一次
this.timer = setInterval(()=>{
this.seconds--

if(this.seconds <= 0){
//清除计时器
clearInterval(this.timer)
uni.switchTab({
url:'/pages/my/my',
success:()=>{
this.updateRedirectInfo({
openType:'switchTab',
from:'/pages/cart/cart'
})
}
})
// 秒数为0 不再展示计时器提示信息
return
}

this.showTips(this.seconds)
},1000)
}
}
}
</script>

<style lang="scss">
.my-settle-container{
position: fixed;
bottom:0;
left:0;
width: 100%;
height: 50px;
background-color: white;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 14px;
padding-left: 5px;
.radio{
display: flex;
align-items: center;
}
.amount-box{
.amount{
color: #FF0000;
font-weight: bold;
}
}
.btn-settle{
background-color: #FF0000;
height:50px;
color:white;
line-height: 50px;
padding:0 10px;
min-width: 100px;
text-align: center;
}
}
</style>

成果展示

电商成果展示4.png

电商成果展示2.png

电商成果展示1.png

电商成果展示3.png