微信小程序商城项目
准备
工具篇
- 下载 HBuilder https://www.dcloud.io/index.html
- 安装所有插件
- 下载微信开发者工具 https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html
- 注册并登录微信公众平台:https://mp.weixin.qq.com/
- 获取 AppID(小程序ID)
前言
生成小程序以及要跳过的坑
- 生成小程序时不能用ID选择器,用class
- uniapp没有router路由
- 小程序生命周期函数(比如onload)不能用在子组件里面,而是用在父组件里面
- 小程序没有 window对象,包括没有 document… 等节点操作 (例如:document.getElementById() 。。。)
搭建项目
主界面配置
HBuilder新建默认模板项目,运行到小程序模拟器微信开发者工具,同时打开微信开发者工具的设置-安全设置-开启服务端口。
配置主界面下方四个tab栏:
将准备好的素材文件夹 static 覆盖到项目根目录,
在pages下新建 sort(分类)、shopping(购物车)、my(我的)三个文件夹,并都在内部创建子vue文件,
接着在pages.json文件配置底部导航栏,代码如下:
{
"pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
{
// 展示用户初次进入的页面
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "首页",
"navigationBarBackgroundColor":"#fe0024",
"navigationBarTextStyle":"white"
}
},
{
"path": "pages/search/search",
"style": {
"navigationBarTitleText": "搜索"
}
},
{
"path": "pages/sort/sort",
"style": {
"navigationBarTitleText": "分类"
}
},
{
"path": "pages/shopping/shopping",
"style": {
"navigationBarTitleText": "购物车"
}
},
{
"path": "pages/my/my",
"style": {
"navigationBarTitleText": "个人中心"
}
}
],
// globalStyle 用于设置应用的状态栏、导航条、标题、窗口背景色等。
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "uni-app",
"navigationBarBackgroundColor": "#F8F8F8",
"backgroundColor": "#F8F8F8"
},
// 如果应用是一个多 tab 应用,可以通过 tabBar 配置项指定一级导航栏,
// 以及 tab 切换时显示的对应页
"tabBar": {
"color": "#333333",
"selectedColor": "#8fc0ff",
"backgroundColor": "#ffffff",
"borderStyle": "black",
"list": [{
"pagePath": "pages/index/index",
"text": "首页",
"iconPath": "./static/tab/tab-a.png",
"selectedIconPath": "./static/tab/tab-b.png"
},
{
"pagePath": "pages/sort/sort",
"text": "分类",
"iconPath": "./static/tab/tab-c.png",
"selectedIconPath": "./static/tab/tab-d.png"
},
{
"pagePath": "pages/shopping/shopping",
"text": "购物车",
"iconPath": "./static/tab/tab-e.png",
"selectedIconPath": "./static/tab/tab-f.png"
},
{
"pagePath": "pages/my/my",
"text": "我的",
"iconPath": "./static/tab/tab-g.png",
"selectedIconPath": "./static/tab/tab-h.png"
}
]
}
}
首页组件拆分
在pages/index下新建文件夹‘sonzjijian’ ,依次新建 search.vue、swipers.vue、purchase.vue、list.vue等文件,首页下方是动态的加载效果所以考虑引用公共组件 ,即在pages下新建comonents文件夹,内部新建card.vue
// 引入组件 (首页从上至下排序)
搜索 search.vue
轮播 swipers.vue
抢购 purchase.vue
天猫榜单 list.vue
引入公用组件 --懒加载卡片 card.vue
在pages/index/index.vue里引入所有子组件
<template>
<view >
<!-- 搜索 -->
<Search></Search>
<!-- 轮播图 -->
<Swipers></Swipers>
<!-- 抢购 -->
<Purchase ></Purchase>
<!-- 榜单 -->
<List ></List>
<!-- 卡片流 懒加载卡片-->
<Card ></Card>
</view>
</template>
。。。
// 引入组件
// 搜索
import Search from './sonzhujian/search.vue'
// 轮播
import Swipers from './sonzhujian/swipers.vue'
// 抢购
import Purchase from './sonzhujian/purchase.vue'
// 天猫榜单
import List from './sonzhujian/list.vue'
// 引入公用的组件 --懒加载卡片
import Card from '../commponents/card.vue'
export default {
components:{
Search,Swipers,Purchase,List,Card
},
}
完成搜索组件展示
关于upx单位:
uni-app 使用 upx 作为默认尺寸单位, upx 是相对于基准宽度的单位,可以根据屏幕宽度进行自适应。uni-app 规定屏幕基准宽度750upx。
<template>
<!-- 首页搜索区域 -->
<view class="search-view">
<view class="search-men">
<!-- uniapp 不需要。。/ 可以直接输入文件夹名字查找-->
<image src="/static/search/logo.png" mode="widthFix"></image>
</view>
<view class="search-ing">
<image src="/static/search/sousuo.png" mode="widthFix"></image>
<input type="text" placeholder="搜索喜欢的内容" disabled />
</view>
<view class="search-saoma">
<image src="/static/search/saoma.png"></image>
</view>
</view>
</template>
<style scoped>
/* scoped css样式仅在当前文件生效*/
.search-view {
height: 100upx;
background: #fe001d;
display: flex;
justify-content: space-between;
align-items: center;
}
.search-view image {
width: 50upx;
height: 50upx;
}
.search-ing {
background: #ffffff;
height: 70upx;
border-radius: 50upx;
display: flex;
flex-direction: row;
flex: 1;
position: relative;
margin-right: 30upx;
}
.search-ing input {
height: 70upx;
line-height: 70upx;
width: 100%;
font-size: 30upx;
color: #b2b2b2;
padding-left: 80upx;
}
.search-ing image {
width: 35upx;
height: 35upx;
position: absolute;
left: 30upx;
align-self: center;
}
.search-saoma {
margin-right: 30upx;
}
.search-men {
width: 50upx;
height: 50upx;
padding: 0 20upx;
}
</style>
自定义轮播展示与请求轮播数据
使用 swiper 组件 请求轮播数据完成数据渲染
打开项目接口文档拿到 首页轮播的接口地址
<template>
<view class="swiper-top">
<!-- @change 事件-->
<swiper :autoplay="true" :circular="true" :interval="2000" :duration="1000" @change="swiperFun()">
<!-- 每个swiper-item 都是独立的会滑动板块 是需要要遍历的整体 -->
<block v-for="(item,index) in bannerdata" :key="index">
<swiper-item>
<view class="swiper-item">
<!-- mode 图片裁剪、缩放的模式 -->
<img :src=item.image mode="aspectFill">
</view>
</swiper-item>
</block>
</swiper>
<!-- 指示点 -->
<view class="instruct-view">
<block v-for="(item,index) in bannerdata" :key="index">
<!--
小方块颜色动态的跟随背景图变化而变化 num默认0,即让下标为0的第一个方块套上了该class属性,
activee选择器生效的前提是value值为真
想让方块颜色跟随背景图变化需要找到滑动事件:
current --当前所在滑块的index
@change --current改变时会触发 change 事件,
-->
<view class="instruct" :class="{active:index == num}"></view>
</block>
</view>
</view>
</template>
<script>
export default {
data() {
return {
// 指示方块的下标值等于num
num:0,
bannerdata:[]
};
},
methods: {
banner(){
uni.request({
url:'https://meituan.thexxdd.cn/api/getbanner',
methods:'GET',
success:(res)=>{
console.log(res)
this.bannerdata = res.data.data
},
fail:(err)=>{
console.log(err)
}
})
}
// 使用swiper自带的@change事件 --滑块滑动时的触发事件 它接收一个参数 每次滑动后current值会增加 而ecurrent的值和index的值是相等的
swiperFun(e){
// console.log(e.detail.current);
this.num = e.detail.current
}
},
};
</script>
<style scoped>
.swiper-top {
height: 250upx;
margin: 20upx 10upx;
border-radius: 10upx;
position: relative;
}
swiper {
height: 250upx !important;
background: yellow;
border-radius: 10upx;
overflow: hidden;
}
.swiper-item {
height: 250upx !important;
overflow: hidden;
}
.swiper-item image {
width: 100%;
height: 250upx !important;
border-radius: 10upx;
}
/* 指示点 */
.instruct-view {
display: flex;
justify-content: center;
position: absolute;
bottom: 10upx;
left: 0;
width: 100%;
}
.instruct {
background: #4e90a6;
height: 10upx;
width: 40upx;
border-radius: 50upx;
margin: 0 10upx;
}
.active {
background: #ffffff !important;
}
</style>
class封装request请求
原因:很多页面都需要发起请求,即需要使用uni.request(),那么就比较麻烦。
uni.request({
url:'',
methods:'',
success:(res)=>{
console.log(res)
},
fail:(err)=>{
console.log(err)
}
})
如果将其抽出封装成公用的方法或者类就很方便了。
项目根目录下新建api文件夹,其下再建立api.js、request.js
封装请求接口
在request.js里放置所有的请求接口,
// 该文件用于写请求地址
const url = 'https://meituan.thexxdd.cn/api/'
// 使用面向对象class方法创造 urls类
const urls = class {
//constructor 构造方法 --可以从别的页面传值过来 接收值
constructor(){
}
// static:静态方法 --直接使用类来调用(即 urls.m() 就可以调用),不是静态方法的话需要实例化类 也就是new
// 存放所有的方法
static m(){
// 首页轮播图 --使用es6的模板字符串方法拼接
let bannerget = `${url}getbanner`
// 导出对象
return {
bannerget,
}
}
}
export default urls
封装请求方式
在api.js中作request请求
// request 该文件用于 写请求方法
const request = class {
// 参数url是request.js暴露出的url
// 参数data是post请求传递的值
constructor(url,arg) {
this.url = url;
this.arg = arg;
}
// 封装 post请求
modepost() {
return new Promise((resolve,reject) => {
uni.request({
url:this.url,
method:'POST',
data:this.arg,
}).then(res => {
resolve(res[1].data);
}).then(err =>{
reject('出错了');
})
})
}
// 封装 get请求
modeget() {
return new Promise((resolve,reject) => {
uni.request({
url:this.url,
method:'GET',
}).then(res => {
resolve(res[1].data);
}).then(err =>{
reject('出错了');
})
})
}
}
export default request
在swiper组件此时可以这样使用
<script>
import urls from '../../../api/request.js'
import request from '../../../api/api.js'
export default {
data() {
return {
// 指示方块的下标值等于num
num:0,
bannerdata:[]
};
},
methods: {
banner() {
//返回新的promise实例对象
new request(urls.m().bannerget).modeget()
.then( res => {
console.log(res)
this.bannerdata = res.data
})
.catch( err => {
console.log(err)
})
},
swiperFun(e){
this.num = e.detail.current
}
},
created(){
//加载完后直接调用banner方法
this.banner()
}
};
</script>
还能使用try catch 方法
methods: {
async banner() {
try {
//返回新的promise实例对象用await接收到结果(记得方法前加上axync) 当返回错误的时候会进入 catch里
let data = await new request(urls.m().bannerget).modeget()
console.log(data)
this.bannerdata = data.data
}catch(e){
}
},
因为每个页面几乎都有请求发送,每次引入api和request就很麻烦,所以挂载到全局 main.js
//main.js中引入api和request
// 引入请求地址
import urls from './api/request.js'
Vue.prototype.Urls = urls
// 引入请求方法
import request from './api/api.js'
Vue.prototype.Request = request
//此时的swiper组件中就这样发送请求了
methods: {
async banner() {
try {
//返回新的promise实例对象用await接收到结果(记得方法前加上axync) 当返回错误的时候会进入 catch里
let data = await new this.Request(this.Urls.m().bannerget).modeget()
console.log(data)
this.bannerdata = data.data
}catch(e){
}
},
统一请求接口到父组件
每个页面都再调用请求也很麻烦,所以将所有请求放到父组件统一发送,父组件将接口响应的数据传递给子组件
<template>
<view >
<!-- 搜索 -->
<Search></Search>
<!-- 轮播图 -->
<Swipers :bannerdata="bannerdata"></Swipers>
<!-- 抢购 -->
<Purchase></Purchase>
<!-- 榜单 -->
<List></List>
<!-- 卡片流 懒加载卡片-->
<Card></Card>
</view>
</template>
<script>
// 引入组件
// 搜索
import Search from './sonzhujian/search.vue'
// 轮播
import Swipers from './sonzhujian/swipers.vue'
// 抢购
import Purchase from './sonzhujian/purchase.vue'
// 天猫榜单
import List from './sonzhujian/list.vue'
// 引入公用的组件 --懒加载卡片
import Card from '../commponents/card.vue'
export default {
components:{
Search,Swipers,Purchase,List,Card
},
data(){
return {
bannerdata:[],
recomdata:[],
billdata:[],
commdata:[],
pageid:0,
}
},
methods:{
// 把请求统一放到父组件
async indexRequest() {
// 轮播
let banner = new this.Request(this.Urls.m().bannerget).modeget()
try {
// 同时并发请求:Promise.all([]) 里边的所有请求全部得到结果后才会返回
let res = await Promise.all([banner])
console.log(res);
// 轮播
this.bannerdata = res[0].data
}catch(e) {
}
},
// 上拉加载所得数据
},
mounted() {
this.indexRequest()
},
}
</script>
<style>
/* 全局背景色 */
page {
background: #f4f4f4;
}
</style>
子组件 swiper.vue 通过props接收父组件传递来的数据
export default {
// bannerdata是父组件 通过ref传递进来的数据 类型是Array
props:{
bannerdata:Array,
},
}
完成推荐抢购商品组件数据
在request.js中添加抢购商品接口
// 该文件用于写请求地址
// static:静态方法 --直接使用类来调用(即 urls.m() 就可以调用),不是静态方法的话需要实例化类 也就是new
// 存放所有的方法
static m(){
// 首页轮播图 --使用es6的模板字符串方法拼接
let bannerget = `${url}getbanner`
// 推荐抢购商品
let getrecommurl = `${url}recom`
// 导出对象
return {
bannerget,
getrecommurl,
}
}
在index/index.vue中使用,并将recomdata数据通过ref传递到子组件
methods:{
// 把请求统一放到父组件
async indexRequest() {
// 轮播
let banner = new this.Request(this.Urls.m().bannerget).modeget()
// 推荐抢购商品
let recomm = new this.Request(this.Urls.m().getrecommurl).modeget()
try {
// 同时并发请求:Promise.all([])
let res = await Promise.all([banner,recomm])
console.log(res);
// 轮播
this.bannerdata = res[0].data
// 快抢购
this.recomdata = res[1].data
}catch(e) {
}
},
},
mounted() {
this.indexRequest()
},
}
子组件purchase.vue代码如下:
<template>
<!-- 抢购组件 -->
<view class="Purchase-view">
<view class="pur-flex">
<!-- 左边 -->
<view class="pur-left widthing">
<view class="pur-top">
<text>{{recomdata[0].title}}</text>
<text>{{recomdata[0].lable}}</text>
</view>
<view class="pur-img">
<block v-for="(item,index) in recomdata[0].image" :key="index">
<view><img :src="item.img" alt=""></view>
</block>
</view>
</view>
<!-- 右边 -->
<view class="pur-right widthing">
<!-- 上 -->
<view class="pur-right-top">
<view class="pur-top two">
<text>{{recomdata[1].title}}</text>
<text>{{recomdata[1].lable}}</text>
</view>
<view class="pur-right-img">
<block v-for="(item,index) in recomdata[1].image" :key="index">
<view><img :src="item.img" alt=""></view>
</block>
</view>
</view>
<!-- 下 -->
<view>
<view class="pur-top three">
<text>{{recomdata[2].title}}</text>
<text>{{recomdata[2].lable}}</text>
</view>
<view class="pur-right-img">
<block v-for="(item,index) in recomdata[2].image" :key="index">
<view><img :src="item.img" alt=""></view>
</block>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
props: {
recomdata: Array,
},
};
</script>
<style scoped>
.Purchase-view {
background: #ffffff;
margin: 0 10upx;
border-radius: 10upx;
}
.pur-top {
display: flex;
align-items: center;
height: 50upx;
padding-left: 9upx;
}
.pur-top text:nth-child(1) {
font-size: 30upx;
font-weight: bold;
}
.pur-top text:nth-child(2) {
font-size: 24upx;
color: red;
padding-left: 10upx;
}
/* 图片 */
.pur-img {
display: flex;
flex-wrap: wrap;
}
.pur-img view {
width: 50%;
height: 150upx;
display: flex;
align-items: center;
justify-content: center;
}
.pur-img image {
width: 120upx;
height: 120upx !important;
}
.pur-img view:nth-child(n + 3) {
padding-top: 50upx;
}
.pur-right-img {
display: flex;
align-items: center;
}
.pur-right-img image {
width: 120upx;
height: 120upx;
}
.pur-right-img view {
width: 50%;
height: 150upx;
display: flex;
align-items: center;
justify-content: center;
}
.pur-right {
display: flex;
flex-wrap: wrap;
}
.pur-right view {
width: 100%;
}
.pur-flex {
display: flex;
align-items: center;
}
.widthing {
width: 50%;
box-sizing: border-box;
}
.pur-left {
border-right: 1rpx solid #f1f1f1;
}
.pur-right-top {
border-bottom: 1rpx solid #f1f1f1;
}
.two text:nth-child(2) {
color: #ff7594 !important;
}
.three text:nth-child(2) {
color: #96b06f !important;
}
</style>
完成天猫榜单组件数据
在request.js中添加抢购商品接口
// 存放所有的方法
static m(){
// 首页轮播图 --使用es6的模板字符串方法拼接
let bannerget = `${url}getbanner`
// 推荐抢购商品
let getrecommurl = `${url}recom`
// 天猫榜单
let billboardurl = `${url}billboard`
// 导出对象
return {
bannerget,
getrecommurl,
billboardurl,
}
}
在index/index.vue中使用,并将billdata数据通过ref传递到子组件
methods:{
// 把请求统一放到父组件
async indexRequest() {
// 轮播
let banner = new this.Request(this.Urls.m().bannerget).modeget()
// 推荐抢购商品
let recomm = new this.Request(this.Urls.m().getrecommurl).modeget()
// 天猫榜单
let billboard = new this.Request(this.Urls.m().billboardurl).modeget()
try {
// 同时并发请求:Promise.all([])
let res = await Promise.all([banner,recomm,billboard])
console.log(res);
// 轮播
this.bannerdata = res[0].data
// 快抢购
this.recomdata = res[1].data
// 天猫榜单
this.billdata = res[2].data
}catch(e) {
}
},
},
mounted() {
this.indexRequest()
},
}
子组件list.vue代码如下:
<template>
<view class="list-top">
<view class="list-text">
<text></text>
<text>天猫榜单</text>
<text>闭着眼睛跟榜买</text>
</view>
<view class="list-view">
<block v-for="(item, index) in billdata" :key="index">
<view class="menb">
<image :src="item.image" mode="aspectFill" />
<text>{{ item.title }}</text>
<text>{{ item.want }}人想要</text>
</view>
</block>
</view>
</view>
</template>
<script>
export default {
props: { billdata: Array },
};
</script>
<style scoped>
/* /less */
.list-top {
margin: 20upx 10upx;
}
.list-text {
height: 70rpx;
display: flex;
align-items: center;
}
.list-text text:nth-child(1) {
background: #2c405a;
height: 35upx;
width: 10upx;
}
.list-text text:nth-child(2) {
font-size: 30upx;
font-weight: bold;
padding-left: 5upx;
}
.list-text text:nth-child(3) {
font-size: 24upx;
color: #999999;
padding-left: 10upx;
}
.list-view {
height: 400rpx;
background: #ffffff;
border-radius: 9rpx;
display: flex;
flex-wrap: wrap;
}
.menb text:nth-child(2) {
font-size: 28rpx;
}
.menb text:nth-child(3) {
font-size: 20rpx;
color: #fe0024;
}
.menb image {
width: 80rpx;
height: 100rpx !important;
padding-bottom: 4rpx;
}
.menb {
width: 33.333%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding-bottom: 2rpx;
box-sizing: border-box;
}
.list-view view:nth-child(-n + 3) {
border-bottom: 1rpx solid #f1f1f1;
}
.list-view view:nth-child(2) {
border-left: 1rpx solid #f1f1f1;
border-right: 1rpx solid #f1f1f1;
}
.list-view view:nth-child(5) {
border-left: 1rpx solid #f1f1f1;
border-right: 1rpx solid #f1f1f1;
}
</style>
封装并请求卡片流商品组件
因为该组件的(流动布局)功能在其他页面也会用到,所以封装到公共组件文件夹去,即在pages下新建comonents文件夹,再新建card.vue文件
在request.js中添加卡片商品流以及上拉加载接口
由于接口文档显示get请求需要携带?query参数,
// 存放所有的方法
static m(){
// 首页轮播图 --使用es6的模板字符串方法拼接
let bannerget = `${url}getbanner`
// 推荐抢购商品
let getrecommurl = `${url}recom`
// 天猫榜单
let billboardurl = `${url}billboard`
// 卡片商品流以及上拉加载
let commodcradurl = `${url}commodcrad`
// 导出对象
return {
bannerget,
getrecommurl,
billboardurl,
commodcradurl,
searchurl,
}
}
在index/index.vue中使用,并将recomdata数据通过ref传递到子组件
。。。。。。
// 引入公用的组件 --懒加载卡片
import Card from '../commponents/card.vue'
// 把请求统一放到父组件
menthods:{
async indexRequest() {
// 轮播
let banner = new this.Request(this.Urls.m().bannerget).modeget()
// 推荐抢购商品
let recomm = new this.Request(this.Urls.m().getrecommurl).modeget()
// 天猫榜单
let billboard = new this.Request(this.Urls.m().billboardurl).modeget()
// 卡片商品流以及上拉加载 (!!注意需要携带参数)
let commodcrad = new this.Request(this.Urls.m().commodcradurl + '?page=' + this.pageid).modeget()
try {
// 同时并发请求:Promise.all([])
let res = await Promise.all([banner,recomm,billboard,commodcrad])
console.log(res);
// 轮播
this.bannerdata = res[0].data
// 快抢购
this.recomdata = res[1].data
// 天猫榜单
this.billdata = res[2].data
// 卡片商品流
this.commdata = res[3].data
}catch(e) {
}
},
},
}
mounted() {
this.indexRequest()
},
公用组件Card.vue代码如下:
<template>
<view class="comm-view">
<block v-for="(item,index) in commdata" :key="index">
<view class="comm-card">
<view class="comm-img"><image :src="item.image" mode="aspectFill" /></view>
<view class="comm-title"><text>{{item.title}}</text></view>
<view class="comm-left">
<text>{{item.freight}}</text>
<text>预计{{item.Duration}}h发货</text>
</view>
<view class="comm-right">{{item.Price}}¥</view>
</view>
</block>
</view>
</template>
<script>
export default {
// props接收父组件传递的数组对象
props:{commdata:Array},
methods: {}
};
</script>
<style scoped>
.comm-img {
height: 450rpx;
}
.comm-img image {
width: 100%;
height: 450rpx;
border-top-left-radius: 10upx;
border-top-right-radius: 10upx;
}
.comm-card {
width: calc(50% - 5rpx * 2);
margin: 5rpx;
background: #ffffff;
border-radius: 10upx;
}
.comm-title {
font-size: 28rpx;
height: 100rpx;
line-height: 50rpx;
padding-left: 10rpx;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2; // 控制多行的行数
-webkit-box-orient: vertical;
}
.comm-left {
font-size: 20rpx;
text-align: center;
display: flex;
align-items: center;
padding-left: 10rpx;
height: 50rpx;
}
.comm-left text {
padding: 3rpx 4rpx;
border-radius: 10rpx;
}
.comm-left text:nth-child(1) {
border: 2rpx solid #1183e6;
margin-right: 10rpx;
color: #1183e6;
}
.comm-left text:nth-child(2) {
border: 2rpx solid red;
color: red;
}
.comm-right {
font-size: 30rpx;
font-weight: bold;
color: #f3022f;
height: 50rpx;
line-height: 50rpx;
padding-left: 10rpx;
}
.comm-view {
display: flex;
flex-wrap: wrap;
padding: 0 5rpx;
}
</style>
完成上拉加载
上拉加载时,一般会加载全部服务器返回的数据,一般用户不会看完那么多的数据,体验感不好,因此一般是仅显示前几条数据,其他数据下拉屏幕获取
首先先封装上拉加载时执行的函数 **pullup()**,
使用uni-app 生命周期 onReachBottom 函数来操作 – 页面滚动到底部的事件(不是scroll-view滚到底),常用于下拉下一页数据。
在index.vue中添加如下代码
// 上拉加载所得数据
methods:{
async pullup() {
// 卡片流商品
try {
let commodcrad = await new this.Request(this.Urls.m().commodcradurl + '?page=' + this.pageid).modeget()
console.log(commodcrad);
// 把之前得到的数据和上拉得到的数据作数组合并 ! ES6的数组合并方法
this.commdata = [...this.commdata,...commodcrad.data]
}catch(e) {
}
}
},
// 页面处理事件--上拉加载更多
onReachBottom () {
this.pageid ++;
this.pullup()
}
封装上拉提示
用户在上拉的时候,应该给出一个提示比如:正在加载中… 或者到底了…
因为该功能属于公共的,所以考虑封装到公共commponents文件夹里 新建loading-men.vue文件
<template>
<!-- 上拉加载组件 -->
<!-- 默认是隐藏的该组件,当上拉加载时,父组件传递的对象参数 到loAD() 中,解构后让 v-if为真 组件显示-->
<view class="loading-men" v-if="loading">
<!-- 没数据后图片会隐藏 控制图片的代码是写在index.vue的pullup()方法里 -->
<image src="/static/loading/loadtmall.gif" mode="widthFix" v-if="picture"></image>
<text>{{tips}}</text>
</view>
</template>
<script>
export default {
data() {
return {
loading: false,
picture: true,
tips: '玩命加载中'
};
},
methods: {
loAd(value) {
// 接收父组件传递过来的 whether 数据 其他俩值是默认值
let { whether = false, picture = true, tips = '玩命加载中'} = value;
// 下方数据的数据会自动获取上边的解构的对象值,如果父组件没有传递,就取默认值
this.loading = whether;
this.picture = picture;
this.tips = tips;
}
}
}
</script>
<style scoped>
.loading-men {
width: 200rpx;
height: 100upx;
font-size: 20rpx;
color: #a7a7a7;
margin: 0 auto;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.loading-men image {
width: 60rpx;
height: 80upx;
display: block;
}
</style>
因为用到的地方较多,所以引入到main.js
// 引入上拉加载组件
import loadn from 'pages/commponents/loading-men.vue'
// 注册到组件
Vue.component('loader-on',loadn)
--------------------------------------------------------
// 然后在index.vue中引入组件loader-on组件
<!-- 上拉加载 -->
<loader-on ref="loadon"></loader-on>
同时在index.vue中 pullup() 方法和 onReachBottom() 事件 代码修改如下:
// 上拉加载所得数据
async pullup() {
// 卡片流商品
try {
// 下拉后得到的数据(数组)
let commodcrad = await new this.Request(this.Urls.m().commodcradurl + '?page=' + this.pageid).modeget()
console.log(commodcrad,'上拉加载的数据');
if(commodcrad.data.length == 0) {
// 当没有数据时,通过ref,将下拉加载的图片隐藏 同时调整提示的文字
this.$refs.loadon.loAd({ whether : true, picture :false , tips : '我是有底线的!'})
}else {
// 需要把之前得到的数据和上拉得到的数据做数组合并 !! 。。。表示展开
this.commdata = [...this.commdata,...commodcrad.data]
}
}catch(e) {
}
}
// 页面处理事件--上拉加载更多
// 给子组件传递数据,即让index.vue中调用子组件的方法,考虑使用ref(在组件上添加) --类似传统的document.getElementById()
onReachBottom () {
this.$refs.loadon.loAd({ whether: true, })
this.pageid ++;
this.pullup()
}
商品搜索页面
因为商品搜索并不是在 index.vue 组件上执行,而是跳转到搜索的功能组件里执行。
因此在 pages **文件夹 新建 **search / search.vue
先在pages.json里配置路径
{
"path": "pages/search/search",
"style": {
"navigationBarTitleText": "搜索"
}
},
在 index / sonzhujian / search.vue 中设置点击input框区域的跳转功能
<view class="search-ing" @click="searCh()">
<image src="/static/search/sousuo.png" mode="widthFix"></image>
<input type="text" placeholder="搜索喜欢的内容" disabled />
</view>
// ...
methods:{
searCh (){
// uni.navigateTo 保留当前页面,跳转到目标页面
uni.navigateTo({
url: '../search/search'
});
}
}
搜索页面布局
<template>
<!-- 搜索页面组件 -->
<view>
<!-- 文本框 和 搜索按钮 -->
<view class="search-cont">
<view class="search">
<!-- 使用v-model 获取输入框的值 -->
<input type="text" placeholder-class="inputclass" confirm-type="搜索" focus="true"
placeholder="请输入关键字" v-model='searchdata' />
</view>
<view class="search-code" >搜索</view>
</view>
<!-- 搜索历史 -->
<view class="search-history" v-if="ifhistory">
<!-- 最近搜索 和 删除icon-->
<view class="search-title">
<view>最近搜索</view>
<view ><image src="/static/search/searchend.svg" mode="widthFix"></image></view>
</view>
<!-- 历史关键词 -->
<view class="menu-block">
<view>衣服</view>
<view>裤子</view>
<view>女装</view>
</view>
</view>
</view>
</template>
<script>
export default {
}
</script>
<style scoped>
.search {
height: 70upx;
line-height: 70upx;
width: 100%;
display: flex;
flex-direction: row;
background: #FFFFFF;
border-radius: 20upx;
margin-left: 20upx;
}
.search input {
height: 70upx;
line-height: 70upx;
width: 100%;
font-size: 30upx;
color: #666666;
padding-left: 30upx;
}
.search-cont {
display: flex;
justify-content: space-between;
height: 70upx;
align-items: center;
padding: 30upx 0;
background: #f8f8f8;
}
.search-code {
width: 150upx;
height: 50upx;
text-align: center;
font-size: 30upx;
}
.search-history{margin: 20upx;}
.search-title{font-size: 30upx; font-weight: bold;
display: flex;
justify-content:space-between;
align-items: center;
height: 60upx;
line-height: 60upx;}
.search-title image{width: 36upx; height: 36upx; display: block;}
.menu-block view {
background: #f7f8fa;
border-radius: 6upx;
font-size: 27upx;
color: #292c33;
text-align: center;
padding: 10upx;
margin: 20upx 20upx 0 0;
}
.menu-block {
display: flex;
flex-direction: row;
justify-content: flex-start;
flex-wrap: wrap;
}
</style>
历史搜索的功能实现
<template>
<!-- 搜索页面组件 -->
<view>
<!-- 文本框 和 搜索按钮 -->
<view class="search-cont">
<view class="search">
<!-- 使用v-model 获取输入框的值 @confirm 点击键盘按钮触发事件-->
<input type="text" placeholder-class="inputclass" confirm-type="搜索" focus="true"
placeholder="请输入关键字" v-model='searchdata' @confirm="onKeyInput"/>
</view>
<!-- 给点击搜索绑定点击事件 -->
<view class="search-code" @click="seArch()" >搜索</view>
</view>
<!-- 搜索历史 -->
<view class="search-history" v-if="ifhistory">
<!-- 最近搜索 和 删除icon-->
<view class="search-title">
<view>最近搜索</view>
<view><image src="/static/search/searchend.svg" mode="widthFix"></image></view>
</view>
<!-- 历史关键词 -->
<view class="menu-block">
<block v-for="(item,index) in Storagedata" :key="index">
<view @click="menubtn(item)">{{item}}</view>
</block>
</view>
</view>
</view>
</template>
<script>
// 引入公用的组件 --懒加载卡片
import Card from '../commponents/card.vue'
// 解构console.log()方便使用
var {log} = console
export default {
components:{Card},
data () {
return {
// 存放输入框的值
searchdata:'',
// 是否有搜索历史
ifhistory:'false',
// 搜索历史数据
Storagedata:'',
// 请求页数
pageid:0,
// 搜索的结果
commdata:[],
// 没有搜索结果
searchno: false,
// 最后一次搜索结果
searchkey:''
}
},
methods :{
// 点击 搜索按钮 触发搜索功能
seArch() {
if(this.searchdata != '') {
this.ifhistory = false
this.pageid = 0
this.getStorage(this.searchdata)
this.searchData(this.searchdata)
}
},
// 键盘 回车键 触发搜索功能
onKeyInput(e) {
let searchkey = e.detail.value
// 当文本框不为空时
if(searchkey != '') {
this.ifhistory = false
this.pageid = 0
this.getStorage(searchkey)
this.searchData(searchkey)
}
},
// 在去请求接口之前,要把搜索结果先存到 本地缓存,然后放到最近搜索这里 (接收的参数是回车事件获取到的 value)
getStorage(searchkey) {
// 存之前先取之前用户搜索的历史 --key值随意取名 如果没有就取空数组表示本地缓存没有数据
let searcharray = uni.getStorageSync('search_key') || []
// 存到 searcharray 数组前面
searcharray.unshift(searchkey)
// 存 uni.setStorageSync(KEY,DATA) key是名称 DATA是内容
uni.setStorageSync('search_key',searcharray)
// 取 uni.getStorageSync(KEY)
},
// 取出本地缓存的搜索历史
// uni.setStorage(OBJECT) 将数据存储在本地缓存中指定的 key 中,会覆盖掉原来该 key 对应的内容,这是一个异步接口。
setStorage() {
let setdata = uni.getStorageSync('search_key')
// 1.判断缓存里有没有重复的关键词 --有就 数组去重
// 使用的是ES6 Set方法
let setdataarr = Array.from(new Set(setdata))
// 2.判断缓存里有没有数据
if(setdataarr.length == 0) {
// 隐藏搜索历史
this.ifhistory = false
}else {
this.Storagedata = setdataarr
this.ifhistory = true
}
},
},
created() {
this.setStorage()
},
}
</script>
//.....
商品搜索请求接口
先添加清除历史功能
<!-- 最近搜索 和 删除icon-->
<view class="search-title">
<view>最近搜索</view>
<!-- 给图标添加点击事件 removeStorage -->
<view @click="removeStorage()"><image src="/static/search/searchend.svg" mode="widthFix"></image></view>
</view>
// 清空本地缓存 即清空搜索历史
removeStorage() {
uni.removeStorageSync('search_key');
// 在调用一下setStorage方法,主要目的是将最近搜索给隐藏
this.setStorage()
},
公用的请求接口方法
// 公用的请求接口方法 搜索商品
async searchData(searchkey,idn='001'){
this.searchkey = searchkey
// 请求的参数
const keys = '?keyword=' + searchkey + '&' + 'page=' + this.pageid
try{
let searchdata = await new this.Request(this.Urls.m().searchurl + keys).modeget()
// 判断
if(idn == '002'){
// 上拉加载
if(searchdata.data.length == 0) {
this.$refs.loadon.loAd({ whether : true, picture :false , tips : '我是有底线的!'})
}else {
this.commdata = [...this.commdata,...searchdata.data]
}
}else {
// 隐藏
this.$refs.loadon.loAd({whether : false})
// 判断是否有数据 没有就展示提示
if(searchdata.data.length == 0) {
this.searchno = true
}else {
// 有数据的话就赋值给data里 不展示提示
this.commdata = searchdata.data
this.searchno = false
}
}
}catch(e){
//TODO handle the exception
}
},
引入style文件夹里的公共样式 在 main.js
// 提示框的css样式
import './style/style.css'
最终代码如下:
<template>
<!-- 搜索页面组件 -->
<view>
<!-- 文本框 和 搜索按钮 -->
<view class="search-cont">
<view class="search">
<!-- 使用v-model 获取输入框的值 @confirm 点击键盘按钮触发事件-->
<input type="text" placeholder-class="inputclass" confirm-type="搜索" focus="true"
placeholder="请输入关键字" v-model='searchdata' @confirm="onKeyInput"/>
</view>
<!-- 给点击搜索绑定点击事件 -->
<view class="search-code" @click="seArch()" >搜索</view>
</view>
<!-- 搜索历史 -->
<view class="search-history" v-if="ifhistory">
<!-- 最近搜索 和 删除icon-->
<view class="search-title">
<view>最近搜索</view>
<view @click="removeStorage()"><image src="/static/search/searchend.svg" mode="widthFix"></image></view>
</view>
<!-- 历史关键词 -->
<view class="menu-block">
<block v-for="(item,index) in Storagedata" :key="index">
<view @click="menubtn(item)">{{item}}</view>
</block>
</view>
</view>
<!-- 展示搜索结果 -->
<Card :commdata="commdata" v-if="!searchno"></Card>
<!-- 没有搜索结果 -->
<view class="empty-cart" v-if="searchno">
<image src="/static/search/sousuono.svg" mode="widthFix"></image>
<text>抱歉!暂无相关商品</text>
</view>
<!-- 上拉加载 -->
<loader-on ref="loadon"></loader-on>
</view>
</template>
<script>
// 引入公用的组件 --懒加载卡片
import Card from '../commponents/card.vue'
// 解构console.log()方便使用
var {log} = console
export default {
components:{Card},
data () {
return {
// 存放输入框的值
searchdata:'',
// 是否有搜索历史
ifhistory:'false',
// 搜索历史数据
Storagedata:'',
// 请求页数
pageid:0,
// 搜索的结果
commdata:[],
// 没有搜索结果
searchno: false,
// 搜索值最后一次搜索结果
searchkey:''
}
},
methods :{
// 点击 搜索按钮 触发搜索功能
seArch() {
if(this.searchdata != '') {
this.ifhistory = false
this.pageid = 0
this.getStorage(this.searchdata)
this.searchData(this.searchdata)
}
},
// 键盘 回车键 触发搜索功能
onKeyInput(e) {
let searchkey = e.detail.value
// 当文本框不为空时
if(searchkey != '') {
this.ifhistory = false
this.pageid = 0
this.getStorage(searchkey)
this.searchData(searchkey)
}
},
// 在去请求接口之前,要把搜索结果先存到 本地缓存,然后放到最近搜索这里 (接收的参数是回车事件获取到的 value)
getStorage(searchkey) {
// 存之前先取之前用户搜索的历史 --key值随意取名 如果没有就取空数组表示本地缓存没有数据
let searcharray = uni.getStorageSync('search_key') || []
// 存到 searcharray 数组前面
searcharray.unshift(searchkey)
// 存 uni.setStorageSync(KEY,DATA) key是名称 DATA是内容
uni.setStorageSync('search_key',searcharray)
// 取 uni.getStorageSync(KEY)
},
// 取出本地缓存的搜索历史
// uni.setStorage(OBJECT) 将数据存储在本地缓存中指定的 key 中,会覆盖掉原来该 key 对应的内容,这是一个异步接口。
setStorage() {
let setdata = uni.getStorageSync('search_key')
// 1.判断缓存里有没有重复的关键词 --有就 数组去重
// 使用的是ES6 Set方法
let setdataarr = Array.from(new Set(setdata))
// 2.判断缓存里有没有数据
if(setdataarr.length == 0) {
// 隐藏搜索历史
this.ifhistory = false
}else {
this.Storagedata = setdataarr
this.ifhistory = true
}
},
// 清空本地缓存的Storage即清空搜索历史
removeStorage() {
uni.removeStorageSync('search_key');
this.setStorage()
},
// 搜索历史
menubtn (item) {
// 先隐藏搜索历史
this.ifhistory = false
this.pageid = 0
// 再发起请求
this.searchData(item)
},
// 公用的请求接口方法 搜索商品 给参数传一个值 idn 用于判断触发搜索
async searchData(searchkey,idn='001'){
this.searchkey = searchkey
// 请求的参数
const keys = '?keyword=' + searchkey + '&' + 'page=' + this.pageid
try{
let searchdata = await new this.Request(this.Urls.m().searchurl + keys).modeget()
// 判断
if(idn == '002'){
// 上拉加载
if(searchdata.data.length == 0) {
this.$refs.loadon.loAd({ whether : true, picture :false , tips : '我是有底线的!'})
}else {
this.commdata = [...this.commdata,...searchdata.data]
}
}else {
// 隐藏
this.$refs.loadon.loAd({whether : false})
// 判断是否有数据 没有就展示提示
if(searchdata.data.length == 0) {
this.searchno = true
}else {
// 有数据的话就赋值给data里 不展示提示
this.commdata = searchdata.data
this.searchno = false
}
}
}catch(e){
//TODO handle the exception
}
},
},
created() {
this.setStorage()
},
// 页面处理事件--上拉加载更多
onReachBottom () {
// 显示提示 通过ref 操作dom
this.$refs.loadon.loAd({ whether: true, })
this.pageid++
this.searchData(this.searchkey,'002')
}
}
</script>
商品详情页
自定义顶部导航栏
在pages文件夹下新建 details.vue文件
pages.json中写入地址
{
// 展示用户初次进入的页面
"path": "pages/details/details",
"style": {
"navigationBarTitleText": "详情页",
"navigationStyle": "custom" // 文字可以排列到顶部
}
},
获取右上角胶囊按钮的宽高和到边框的距离
// getMenuButtonBoundingClientRect() 本API用于获取小程序下胶囊按钮的布局位置信息(宽高、边距等等),方便开发者布局顶部内容时避开该按钮坐标信息以屏幕左上角为原点。
下方的部分新建sonzujian文件夹下新建top.vue:
<template>
<view>
<view class="navs-view">
<view class="navs-image" :style=" 'height:'+ tophight.height + 'px;' ">
<image src="/static/details/fanhuihei.png" mode="widthFix"></image>
</view>
<view class="navs">
<block v-for="(item,index) in navalue" :key="index">
<!-- 动态设置 高度 和 行高 与胶囊对齐 同时设置一个底部横线-->
<view class="navs-nav" :class="{navactivetext:index == num}"
:style=" 'height:'+ tophight.height + 'px;' + 'line-height:' + tophight.height + 'px;' "
@click="navbtn(index)"
>{{item.name}}</view>
</block>
</view>
<view style="width: 20px; padding-right: 20rpx;"></view>
</view>
</view>
</template>
<script>
export default{
props:{
// 接收父组件数据 子组件自定义高度
tophight:Object
},
data() {
return {
num:0,
navalue: [
{
'name':'商品'
},
{
'name':'评价'
},
{
'name':'详情'
}
]
}
},
methods:{
// 控制短横线跳转位置
navbtn(index){
this.num = index
}
}
}
</script>
<style scoped>
.navs-nav{font-size: 30upx; width: 100upx;
text-align: center; color: #464230;
}
.navs-image image{width: 20px; height: 20px;}
.navs-image{width: 20px; display: flex; align-items: center; padding-left: 20rpx;}
.navs{display: flex; justify-content: center; align-self: center;
}
.navactivetext{border-bottom: 5upx solid #3d3b32;}
.navs-view{display: flex; align-items: center; justify-content: space-between;}
</style>
此时父组件details.vue是这样:
<template>
<view >
<!-- 返回按钮 -->
<view class="header-fixed backno" v-show="showAbs">
<!-- 箭头符号设置的动态的高 -->
<view class="status_bar" :style="'height: '+ tophight.top + 'px;'"></view>
<view class="navs-image" :style="'height: '+ tophight.height + 'px;' ">
<image src="/static/details/fanhuibai.jpg" mode="widthFix"></image>
</view>
</view>
<!-- tab -->
<!--子组件 默认不显示出来-->
<view class="header-fixed backyes" v-show="!showAbs">
<!-- 放置一个div 撑开到顶部距离 -->
<view class="status_bar" :style="'height: '+ tophight.top + 'px;'"></view>
<!-- 引用子组件 且给子组件传递 tophight 子组件里设置高度-->
<top :tophight="tophight"></top>
</view>
</view>
</template>
<script>
import Top from './sonzhujian/top.vue'
export default {
components:{
Top,
},
data(){
return {
tophight:{}, // 胶囊按钮的数据
showAbs:true, //控制箭头按钮显示/隐藏
}
},
created(){
// 获取胶囊按钮的数据 m包含胶囊按钮的所有信息
this.tophight = uni.getMenuButtonBoundingClientRect()
}
}
</script>
<style scoped>
page{background: #f2f2f2;}
.header-fixed{
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 2;
}
.backyes{background: #f8f8f8;}
.backno{background: none;}
.status_bar {
width: 100%;
/* background: #007AFF; */
}
.navs-image image{width: 28px; height: 28px;
border-radius: 50%;
}
.navs-image{display: flex; align-items: center;
padding-left: 20upx;
}
/* banner部分 */
.imageurl {
width: 100%;
height: 700upx !important;
}
swiper {
height: 700upx !important;
}
.swiper-video{height: 700upx; background: #4CD964;}
.swiper-video video{width: 100%; height: 700upx;}
/* 自定义按钮 */
.video-img image{width: 90upx; height: 90upx; z-index: 999;
border: 2px solid #FFFFFF;
border-radius: 50%;}
.video-img{width: 90upx; height: 90upx;
position: absolute; bottom: 0;
left: 50%;
top: 50%;
transform: translateX(-50%) translateY(-50%);
}
.video-btn{height: 700upx; position: relative;}
</style>
滚动监听显示隐藏导航栏
首先有滚动监听的方法: onPageScroll ( ) – 监听页面滚动,参数为Object
然后有滚动的淡入淡出效果: opacity属性
此时details.vue文件代码如下:
<template>
<view >
<!-- 返回按钮 -->
<view class="header-fixed backno" v-show="showAbs">
<!-- 箭头符号设置的动态的高 -->
<view class="status_bar" :style="'height: '+ tophight.top + 'px;'"></view>
<view class="navs-image" :style="'height: '+ tophight.height + 'px;' ">
<image src="/static/details/fanhuibai.jpg" mode="widthFix"></image>
</view>
</view>
<!-- tab -->
<!--子组件 默认不显示出来 同时透明度值默认是0 -->
<view class="header-fixed backyes" v-show="!showAbs" :style="{opacity:styleObject}">
<!-- 放置一个div 撑开到顶部距离 -->
<view class="status_bar" :style="'height: '+ tophight.top + 'px;'"></view>
<!-- 引用子组件 且给子组件传递 tophight 子组件里设置高度-->
<top :tophight="tophight"></top>
</view>
<view style="height: 2000upx;"></view>
</view>
</template>
<script>
import Top from './sonzhujian/top.vue'
export default {
components:{
Top,
},
data(){
return {
tophight:{}, // 胶囊按钮的数据
showAbs:true, //控制箭头按钮显示/隐藏 true为显示
styleObject:0, //透明度
}
},
created(){
// 获取胶囊按钮的数据 m包含胶囊按钮的所有信息
this.tophight = uni.getMenuButtonBoundingClientRect()
},
// 这里的 e 就是滚动的距离
onPageScroll(e){
console.log(e);
this.handleScroll(e.scrollTop)
},
methods:{
// 导航栏的显示 接收一个参数 top是滚动的距离
handleScroll(top) {
// 90 300 是任意随便写的 当滑动的距离大于90时
if(top > 90) {
let opacity = top / 300 // 这一步是让透明度尽量变小
opacity = opacity > 1 ? 1 : opacity // 如果透明度大于1就等于1 否则透明度就取小于1
this.showAbs = false // 控制顶部栏 的箭头按钮隐藏
this.styleObject = opacity // 透明度赋值一下
}else {
this.shhowAbs = true //小于90 即显示顶部导航栏
}
}
}
}
</script>
带视频和图片的轮播组件
轮播组件其实也就是swiper组件
因为uniapp 的swiper组件 再子组件内时,带有视频的轮播不能播放视频,所以轮播还是写到父组件 details.vue 里边
:show-center-play-btn = “false” 隐藏视频中间默认得播放按钮
:controls=”false” 隐藏左下角播放组件
object-fit=”cover” 当视频大小与 video 容器大小不一致时,视频的表现形式。contain:包含,fill:填充,cover:覆盖uni.createVideoContext(videoId, this) 创建并返回 video 上下文 videoContext 对象获取videodom对象
<template>
<view >
<!-- 返回按钮 -->
...
<!-- 子组件 tab -->
<!--组件 默认不显示出来 同时透明度值默认是0 -->
...
<!-- 图片视频宣传样例 -->
<!-- 注意:如果含有视频播放,在子组件里无法触发视频播放,必须要在父组件 -->
<view>
<swiper :indicator-dots="dots" :interval="3000" :duration="1000" :circular="true"
indicator-color="rgba(216, 216, 216)"
indicator-active-color="#7a7a7a"
>
<swiper-item class="swiper-video">
<view class="video-btn">
<view>
<video id="myVideo" src="http://vd2.bdstatic.com/mda-naa98nr851kwmwya/cae_h264_delogo/1641911777690304151/mda-naa98nr851kwmwya.mp4"
:show-center-play-btn = "false"
:controls="false"
object-fit="cover"
></video>
</view>
<!-- 播放按钮 -->
<view class="video-img" @click="videoPlay()" >
<image src="/static/details/bofang.svg" mode="widthFix"></image>
</view>
</view>
</swiper-item>
<swiper-item>
<view>
<image :src="item" mode="aspectFill" class="imageurl" ></image>
</view>
</swiper-item>
</swiper>
</view>
</view>
</template>
data(){
return {
videoplay:{}, // 得到视频上下文 得到结果是对象
}
},
mounted(){
// 得到视频节点 (对象)
this.videoplay = uni.createVideoContext('myVideo')
},
methods:{
// 手动触发视频播放
videoPlay(){
setTimeout(()=>{
this.videoplay.play()
},200)
}
}
视频操作的一些方法
在video上添加属性
@play=”playFun” 当开始/继续播放时触发 play事件
// 视频播放时触发的方法
playFun(){
this.startVideo = false // 中间播放按钮 隐藏
this.conting = true // 显示视频左下角播放组控件
}
@pause=”pauseFun” 当暂停播放时触发 pause 事件
// 视频暂停时触发的方法
pauseFun(){
this.conting = false
this.startVideo = true
}
@ended=”endedFun“ 当播放到末尾时触发 ended 事件
endedFun(){
this.conting = false
this.startVideo = true
}
swiper 的滑动事件
@change current 改变时会触发 change 事件,
@change="bannerfun()" //写在swiper属性中
// 滑块滑动时触发
bannerfun(){
this.videoplay.pause()
},
请求banner栏的数据
接口的添加就和之前一样定义好就不赘述了,
在mthods里定义请求的方法
methods:{
// 请求数据方法
async detRequest(id){
let introduce = new this.Request(this.Urls.m().introduceurl + '?id=' + id).modeget()
try{
// 因为后期需要发起多次请求,用promise.all做一个并发请求
let res = await Promise.all([introduce])
// 图片视频的数据
this.imagetext = res[0].data
console.log(res);
}catch(e) {
}
},
}
// 在onLoad里边调用请求方法 这里的参数是暂时选择的一个接口地址
onLoad(){
this.detRequest('5f8bbf2823954733542169a1')
}
在swiper里遍历
<swiper :indicator-dots="dots" :interval="3000" :duration="1000" :circular="true"
indicator-color="rgba(216, 216, 216)"
indicator-active-color="#7a7a7a"
@change="bannerfun()"
>
<block v-for="(iteming,index) in imagetext[0].media" :key="index">
<swiper-item class="swiper-video">
<view class="video-btn">
<view>
<video id="myVideo" :src="iteming.video"
:show-center-play-btn = "false"
:controls="conting"
object-fit="cover"
@play="playFun"
@pause="pauseFun"
@ended="endedFun"
></video>
</view>
<!-- 播放按钮 -->
<view class="video-img" @click="videoPlay()" v-show="startVideo">
<image src="/static/details/bofang.svg" mode="widthFix"></image>
</view>
</view>
</swiper-item>
<block v-for="(item,indexing) in iteming.imgArray" :key="indexing">
<swiper-item>
<view>
<image :src="item" mode="aspectFill" class="imageurl" ></image>
</view>
</swiper-item>
</block>
</block>
</swiper>
对指示点的出现时机作配置,当请求到的数据里的video有数据时,不显示指示点,否则显示指示点
// 请求数据方法
async detRequest(id){
let introduce = new this.Request(this.Urls.m().introduceurl + '?id=' + id).modeget()
try{
// 因为后期需要发起多次请求,用promise.all做一个并发请求
let res = await Promise.all([introduce])
// 图片视频的数据
this.imagetext = res[0].data
let mendata = res[0].data[0]
// 如果有视频,不显示面板指示点 先拿到数据里的video地址
this.truevideo = mendata.media[0].video
// 三元表达式 如果视频数据为空 就显示指示点 否则不显示
this.dots = this.truevideo == ''? true : false
}catch(e) {
}
},
同时用v-if判断,当没有请求到video数据时,隐藏视频的swiper=
<!-- 视频数据不为空 才显示视频组件 -->
<swiper-item class="swiper-video" v-if="iteming.video !== ''">
对有视频的情况,进行指示点的详细判断,在视频页面不显示指示点,图片页面显示指示点,核心思路是e.current值
// 滑块滑动时触发 这里的 e 包含了很多的数据 其中 current值等于 请求到的index值
bannerfun(e){
// if(e.detail.current !== 0) { //滑块滑动到图片的时候显示面板指示点 方法 1
// this.dots = true
// }else {
// this.dots = false
// }
let inx = e.detail.current //滑块滑动到图片的时候显示面板指示点 方法 2
if(this.truevideo !== '') {
this.videoplay.pause() //暂停视频
this.dots = inx == 0 ? false : true
}else {
this.dots = true
}
},
当滑动视频时候会触发自带的 enable-progress-gesture 属性 –开启控制进度的手势 ,给它关闭掉,这样下次会继续播放之前暂停的视频。
// 在video标签内
:enable-progress-gesture = "false"
封装预览图片
预览图片功能是公共的组件,因此抽出到新目录
在根目录下新建public/logic.js
// 预览图片
class Login{
// index --当前图片下标 imgarr --图片地址(是数组参数)
constructor(index,imgarr){
this.index = index
this.imgarr = imgarr
}
// 调用预览图片的方法
previewImg(){
uni.previewImage({
current:this.index,
urls:this.imgarr
})
.then(res => {
console.log(res)
})
.catch(err => {
console.log(err);
})
}
}
// 导出
module.exports = { Login }
引入到details.vue文件
// 预览图片 node导入模式
const { Login } = require('../../public/logic.js')
给图片上添加点击事情,同时实例化引入的Login,并传入参数后调用内置的previewImg方法
<image :src="item" mode="aspectFill" class="imageurl" @click="previmg(indexing,iteming.imgArray)"></image>
//...
// 预览图片
previmg(index,imgArray){
console.log(index,imgArray)
// 实例化类 ,同时传入俩参数,接着调用里边的previewImg方法
new Login(index,imgArray).previewImg()
},
获取商品标题价格等数据
在pages/details/sonzhujian下新建price.vue
接着在details.vue引入price.vue,
<template>
<view>
<!-- 价格区域 -->
<view class="price-view">
<view>当前价</view>
<view class="price-list">
<text>¥{{priceetc.Trueprice}}</text>
<text class="scribing">原价¥{{priceetc.Crossedprice}}</text>
</view>
</view>
<!-- 淘金币抵6% -->
<view class="Discount-view">
<text>{{priceetc.Goldcoin}}</text>
<text>{{priceetc.integral}}</text>
</view>
<!-- 标题 -->
<view class="price-Title">
<view class="titleing">{{priceetc.title}}</view>
<view class="price-Image" >
<image src="/static/details/canshu.svg" mode="widthFix"></image>
<text>参数</text>
</view>
</view>
<!-- 月销 -->
<view class="Monthly">销量{{priceetc.sales_volume}}</view>
</view>
</template>
<script>
export default {
props:{
priceetc:Object,
},
}
</script>
<style scoped>
.price-view{
/* background: #FFFFFF; */
background:linear-gradient(to right,#ff0000,#ff4000,#ff8000);
height: 120upx;
font-size: 35upx;
padding-left: 20upx;
display: flex;
flex-direction: column;
justify-content: center;
color: #FFFFFF;
}
.price-list{display: flex; align-items: center; line-height: 50upx;}
.price-list text:nth-child(1){font-size: 40upx; padding-right: 10upx;}
.price-list text:nth-child(2){
font-size: 25upx;
/* background: red;
color: #fe0032;
height: 30upx;
line-height: 30upx;
text-align: center;
border-radius: 50upx;
padding: 9upx 23upx; */
}
.scribing{text-decoration: line-through; color: #b5b5b5 !important;
padding-left: 7rpx;
}
.textstyle text:nth-child(2){background: #FFFFFF !important;}
.coupon-text{color: #FFFFFF !important;}
/* 优惠券 */
.Discount-view{
font-size: 25upx;
background: #FFFFFF;
height: 100upx;
display: flex;
align-items: center;
padding: 0 20upx;
}
.Discount-view text:nth-child(-n+2){background: #ffeaef;
height: 30upx;
line-height: 30upx;
text-align: center;
padding: 10upx;
border-radius: 10upx;
margin-right: 10upx;
}
.Discount-view text:nth-child(3){flex: 1;
text-align: right;}
/* 标题 */
.price-Title{font-size: 35upx;
height: 100upx;
background: #FFFFFF;
display: flex;
align-items: center;
padding: 0 20upx;}
.titleing{
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2; // 控制多行的行数
-webkit-box-orient: vertical;
flex: 1;
}
.price-Title image{width: 50upx; height: 50upx; padding-bottom: 5upx;}
.price-Image{display: flex; flex-direction: column; justify-content: center;
align-items: flex-end;
font-size: 25upx;
width: 150upx;
/* background: #1183E6; */
}
/* 月销 */
.Monthly{
background: #FFFFFF;
font-size: 25upx; color: #999999;
display: flex;
justify-content: flex-end;
height: 70upx;
line-height: 70upx;
padding-right: 20upx;
}
</style>
兄弟组件传值请求产品参数数据
在pages/details/sonzhujian下新建parame.vue
接着在details.vue引入parame.vue,同时引入写好的公共样式文件夹pattern,并在main.js中引入
功能需求:price.vue组件中点击参数后,parame.vue组件显示产品参数页面
思路:
默认使用v-if 隐藏
然后兄弟组件间传值 –使用bus总线传值
public文件夹下新建文件bus.js
// vue 的总线传值
import Vue from 'vue'
export default new Vue()
然后在main.js中引入
// bus传值
import bus from './public/bus.js'
Vue.prototype.$bus = bus
在要发送数据的组件里使用全局事件总线:
methods:{
// 兄弟组件传值显示产品参数
parameFun(){
// $emit() 传值 第一个值是要调用组件的名称可以随意命名 第二个值是要传递的参数
this.$bus.$emit('parames',{show:true})
}
}
在要用到数据的组件里接收:
created(){
// 接收 $on
this.$bus.$on('parames',res => {
console.log(res);
// 让键值对的键相对应
this.couponif = res.show
})
},
在price.vue组件的点击参数事件中,添加请求方法,并通过全局总线传递给兄弟组件
<!-- priceetc.id -- 通过父组件details.vue传递过来的priceetc.id 值作为本组件发起请求的id参数 -->
<view class="price-Image" @click="parameFun(priceetc.id)">
<image src="/static/details/canshu.svg" mode="widthFix"></image>
<text>参数</text>
</view>
methods:{
// 兄弟组件传值显示产品参数
async parameFun(id){
try{
let parameter = await new this.Request(this.Urls.m().parameterurl + '?id=' + id).modeget()
// $emit() 传值 第一个值是要调用组件的名称可以随意命名 第二个值是要传递的参数
this.$bus.$emit('parames',{show:true,data:parameter.data})
}catch(e){
}
}
}
子组件代码如下:
<template>
<view v-if="couponif">
<!-- 遮罩层 -->
<!-- catchtouchmove 防止穿透点击下方的事件 -->
<view class="Coupon-yin anim" :catchtouchmove="true" @click="hideCou()"></view>
<view class="Coupon-view coup-anim">
<view class="Coupon-title">产品参数</view>
<block v-for="(item,index) in paramedata" :key="index">
<view class="parame-view">
<view class="parame-left">{{item.title}}</view>
<view class="parame-right">{{item.label}}</view>
</view>
</block>
</view>
</view>
</view>
</template>
<script>
export default {
data(){
return {
couponif:false, // 隐藏组件的数据
paramedata:[], // 接收所有产品参数的数据
}
},
created(){
// 接收 $on 兄弟组件数据
this.$bus.$on('parames',res => {
// 让键值对的键相对应
console.log(res);
let {show,data} = res
this.paramedata = data
this.couponif = show
})
},
methods:{
// 隐藏该组件
hideCou(){
this.couponif = false
}
}
}
</script>
<style scoped>
.parame-view{
font-size: 28rpx;
display: flex;
border-bottom: 1px solid #F0F0F0;
justify-content: flex-start;
padding: 25rpx 15rpx;
}
.parame-left{
width: 100upx;
padding-right: 25upx;
}
.parame-right{
flex: 1;
}
</style>
获取商品评价数据
在pages/details/sonzhujian下新建evaluate.vue
接着在details.vue引入evaluate.vue,引入css样式
在添加请求商品评价数据的方法,通过ref传递给子组件
<!-- 商品评价 -->
<evaluate :comment="comment"></evaluate>
// 请求数据方法
async detRequest(id){
// 商品评价的数据
let wxcommnt = new this.Request(this.Urls.m().wxcommnturl + '?id=' + id).modeget()
try{
// 因为后期需要发起多次请求,用promise.all做一个并发请求
let res = await Promise.all([introduce,wxcommnt])
console.log(res);
// 拿到评价数据
this.comment = res[1].data
}catch(e) {
}
},
子组件evaluate.vue代码如下
<template>
<!-- 当返回的评价数据大于0条才展示此组件 -->
<view class="evaluate-view" v-if="comment[0].commlen > 0">
<!-- 第一排 -->
<view class="evaluate-top">
<view>商品评价({{comment[0].commlen}})</view>
<view class="evaluate-rig">
<text>查看全部</text>
<image src="/static/details/quanbu.svg" mode="widthFix"></image>
</view>
</view>
<!-- 评论的标签 -->
<!-- 如果评价标签数据数组的长度大于0才展示数据 -->
<view class="evaluate-class" v-if="comment[0].commtag.length > 0">
<block v-for="(item,index) in comment[0].commtag" :key="index">
<text>{{item.label}}({{item.num}})</text>
</block>
</view>
<!-- 评价内容 -->
<view class="evaluate-User">
<block v-for="(item,index) in comment[0].parcontent" :key="index">
<view>
<view class="USering">
<image :src="item.avatarUrl" mode="widthFix"></image>
<text>{{item.nickName}}</text>
</view>
<view class="USering-text">{{item.text}}</view>
<view class="USering-time">
<text>{{item.time}}</text>
<text>颜色:{{item.color}}</text>
<text>尺码:{{item.size}}</text>
</view>
</view>
</block>
</view>
</view>
</template>
<script>
export default {
// 接收父组件的数据
props: {
comment:Array
}
}
</script>
<style scoped>
.evaluate-view{background: #FFFFFF;
margin-top: 20upx;
padding: 20upx;
}
.evaluate-top{font-size: 30upx; height: 50upx;
display: flex;
justify-content: space-between;
align-items: center;
/* background: #4CD964; */
}
.evaluate-rig image{width: 27upx; height: 27upx;
padding-left: 20upx;
}
.evaluate-rig{display: flex; align-items: center;}
/* 分类 */
.evaluate-class{font-size: 25upx; display: flex;
flex-wrap: wrap;
margin: 15upx 0;
}
.evaluate-class text{background: #feecea;
border-radius: 50upx;
padding: 15upx;
margin: 0upx 14upx 14upx 0;
}
/* 用户评价 */
.evaluate-User{font-size: 25upx; color: #999999;}
.evaluate-User image{width: 50upx; height: 50upx !important;
border-radius: 50%;
}
.USering{height: 50upx;
/* background: #007AFF; */
display: flex;
align-items: center;
}
.USering text{padding-left: 10rpx;}
.USering-text{color: #333333;
line-height: 40upx;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2; // 控制多行的行数
-webkit-box-orient: vertical;
}
.USering-time{display: flex; align-items: center;
height: 50upx;
}
.USering-time text:nth-child(1){padding-right: 10upx;}
</style>
请求商品详情图片介绍数据
数据在datails中已经取到,是priceetc:{} 数据里
在pages/details/sonzhujian下新建produce.vue,在父组件引入
同时父组件将priceetc通过ref传递给produce.vue
其中图片是应该点击能够预览,可以使用之前封装的 logic.js
// 预览图片 node导入模式
const { Login } = require('../../public/logic.js')
<template>
<view class="Product-view">
<block v-for="(item,index) in priceetc.Detaileddrawing" :key="index">
<image :src="item" mode="widthFix" @click="previmg(index,priceetc.Detaileddrawing)"></image>
</block>
</view>
</template>
<script>
// 预览图片 node导入模式
const { Login } = require('../../../public/logic.js')
export default{
props:{priceetc:Object},
methods:{
// 参数1 --当前图片下标 参数2 --图片地址(所有图片的数组)
previmg(index,imgArray){
new Login(index,imgArray).previewImg()
}
}
}
</script>
<style scoped>
.Product-view{margin-top: 20upx; padding-bottom: 90upx;}
.Product-view image{width: 100%; vertical-align: bottom;}
</style>
商品所有的评价
在pages下新建comments文件夹,同时在其下 新建comments.vue,
为了便于观察,可以在page.json中先将comments.vue,放到第一位
开始写comments.vue组件内容
<template>
<view class="comments-view">
<!-- 评价分类的标签 -->
<!-- 如果评价标签的数据长度大于0,就表示有数据,才显示标签 -->
<view class="evaluate-class" v-if="labeldata.length > 0">
<block v-for="(item, index) in labeldata" :key="index">
<!-- 如果评价标签数据中的num值为空代表是'全部'标签,就不展条数 -->
<text v-if="item.num == ''">{{ item.label }}</text>
<text v-else>{{ item.label }}({{ item.num }})</text>
</block>
</view>
<!-- 完整的评价内容 -->
<block v-for="(item,index) in commdata" :key="index">
<view class="comments-user">
<view class="comments-top comments-flex">
<image :src="item.avatarUrl" mode="widthFix"></image> <!-- 用户头像 -->
<text>{{item.nickName}}</text> <!-- 用户昵称 -->
</view>
<view class="comments-zh comments-flex">
<text>{{item.time}}</text>
<text>颜色分类:{{item.color}}</text>
<text>尺码:{{item.size}}</text>
</view>
<view class="comments-mes">{{item.text}}</view>
<!-- 如果 item.isimg 中有数据,就显示图片 -->
<view class="comments-img" v-if="item.isimg">
<block v-for="(items,indexs) in item.image" :key="indexs">
<!-- 评论图片 有没有数据 取决于isimg的布尔值 -->
<view class="user-images">
<image :src="items" mode="aspectFill"></image>
</view>
</block>
</view>
</view>
</block>
</view>
</template>
<script>
export default {
data() {
return {
// 选中第几个
labeldata: [],
commdata: []
};
},
methods: {
async reqComm(id) {
let comtag = new this.Request(this.Urls.m().comtagurl + '?id=' + id).modeget(); //评价分类标签数据
let comtconent = new this.Request(this.Urls.m().comtconenturl + '?id=' + id + '&label=' + '全部').modeget(); //评价的内容数据
try {
let res = await Promise.all([comtag, comtconent]);
console.log(res);
// 标签数据
this.labeldata = res[0].data;
// 评价数据
this.commdata = res[1].data;
} catch {}
}
},
onLoad() {
this.reqComm('5f8bbf2823954733542169a1');
}
};
</script>
<style scoped>
.comments-view {
margin: 20upx;
}
/* 分类 */
.evaluate-class {
font-size: 25upx;
display: flex;
flex-wrap: wrap;
padding-bottom: 50upx;
border-bottom: 1px solid #eeeeee;
}
.evaluate-class text {
background: #feecea;
border-radius: 50upx;
padding: 15upx;
margin: 0upx 14upx 14upx 0;
}
/* 选中*/
.actives {
background: #ff0036 !important;
color: #fff !important;
}
/* user评价 */
.comments-user {
font-size: 30upx;
padding-bottom: 15upx;
border-bottom: 1px solid #eeeeee;
margin: 25upx 0;
}
.comments-flex {
display: flex;
align-items: center;
}
.comments-top image {
width: 60upx;
height: 60upx !important;
border-radius: 50%;
}
.comments-top text {
padding-left: 10upx;
}
.comments-zh {
height: 80upx;
font-size: 25upx;
color: #a7a7a7;
}
.comments-zh text {
padding-right: 8upx;
}
.comments-mes {
line-height: 1.5;
}
.comments-top {
height: 60upx;
}
.comments-img {
display: flex;
flex-wrap: wrap;
}
.user-images {
width: calc(33.33% - 10rpx * 2);
height: 200rpx;
padding: 10rpx;
}
.user-images image {
width: 100%;
height: 200rpx;
border-radius: 2rpx;
}
</style>
完成商品分类查询评价
先完成图片预览功能
<image :src="items" mode="aspectFill" @click="preTmage(indexs,item.image)"></image>
// 预览图片 node导入模式
const { Login } = require('../../public/logic.js')
// 阅览图片方法
preTmage(index,imgArray){
new Login(index,imgArray).previewImg()
}
然后完成点击标签变色,且展示相关评论功能,
<template>
<view class="comments-view">
<!-- 评价分类的标签 -->
<!-- 如果评价标签的数据长度大于0,就表示有数据,才显示标签 -->
<view class="evaluate-class" v-if="labeldata.length > 0">
<block v-for="(item, index) in labeldata" :key="index">
<!-- 如果评价标签数据中的num值为空代表是'全部'标签,就不展示条数 --> <!-- 添加动态的class 当num等于下标值时触发-->
<text v-if="item.num == ''" :class="{ actives: num == index }" @click="labelFun(item.commid,item.label,index)">{{ item.label }}</text>
<text v-else :class="{ actives: num == index }" @click="labelFun(item.commid,item.label,index)">{{ item.label }}({{ item.num }})</text>
</block>
</view>
<!-- 完整的评价内容 -->
<block v-for="(item, index) in commdata" :key="index">
<view class="comments-user">
<view class="comments-top comments-flex">
<image :src="item.avatarUrl" mode="widthFix"></image>
<!-- 用户头像 -->
<text>{{ item.nickName }}</text>
<!-- 用户昵称 -->
</view>
<view class="comments-zh comments-flex">
<text>{{ item.time }}</text>
<text>颜色分类:{{ item.color }}</text>
<text>尺码:{{ item.size }}</text>
</view>
<view class="comments-mes">{{ item.text }}</view>
<!-- 如果 item.isimg 中有数据,就显示图片 -->
<view class="comments-img" v-if="item.isimg">
<block v-for="(items, indexs) in item.image" :key="indexs">
<!-- 评论图片 有没有数据 取决于isimg的布尔值 -->
<view class="user-images"><image :src="items" mode="aspectFill" @click="preTmage(indexs, item.image)"></image></view>
</block>
</view>
</view>
</block>
</view>
</template>
<script>
// 预览图片 node导入模式
const { Login } = require('../../public/logic.js');
export default {
data() {
return {
// 选中第几个
num:0,
labeldata: [], // 标签数据
commdata: [], // 评价数据
};
},
methods: {
async reqComm(id) {
let comtag = new this.Request(this.Urls.m().comtagurl + '?id=' + id).modeget(); //评价分类标签数据
let comtconent = new this.Request(this.Urls.m().comtconenturl + '?id=' + id + '&label=' + '全部').modeget(); //评价的内容数据
try {
let res = await Promise.all([comtag, comtconent]);
console.log(res);
// 标签数据
this.labeldata = res[0].data;
// 评价数据
this.commdata = res[1].data;
} catch {}
},
// 预览览图片方法
preTmage(index, imgArray) {
new Login(index, imgArray).previewImg();
},
// 点击标签切换的方法
async labelFun(commid,label,index){
this.num = index
// 请求分类标签的接口
let comtconent1 = await new this.Request(this.Urls.m().comtconenturl + '?id=' + commid + '&label=' + label).modeget();
this.commdata = comtconent1.data
}
},
// onLoad生命周期可以接收上个页面过来的值 e就包含接收的数据
onLoad(e) {
this.reqComm(e.id);
}
};
</script>
<style scoped>
.comments-view {
margin: 20upx;
}
/* 分类 */
.evaluate-class {
font-size: 25upx;
display: flex;
flex-wrap: wrap;
padding-bottom: 50upx;
border-bottom: 1px solid #eeeeee;
}
.evaluate-class text {
background: #feecea;
border-radius: 50upx;
padding: 15upx;
margin: 0upx 14upx 14upx 0;
}
/* 选中*/
.actives {
background: #ff0036 !important;
color: #fff !important;
}
/* user评价 */
.comments-user {
font-size: 30upx;
padding-bottom: 15upx;
border-bottom: 1px solid #eeeeee;
margin: 25upx 0;
}
.comments-flex {
display: flex;
align-items: center;
}
.comments-top image {
width: 60upx;
height: 60upx !important;
border-radius: 50%;
}
.comments-top text {
padding-left: 10upx;
}
.comments-zh {
height: 80upx;
font-size: 25upx;
color: #a7a7a7;
}
.comments-zh text {
padding-right: 8upx;
}
.comments-mes {
line-height: 1.5;
}
.comments-top {
height: 60upx;
}
.comments-img {
display: flex;
flex-wrap: wrap;
}
.user-images {
width: calc(33.33% - 10rpx * 2);
height: 200rpx;
padding: 10rpx;
}
.user-images image {
width: 100%;
height: 200rpx;
border-radius: 2rpx;
}
</style>
最后完成点击evaluate.vue组件的查看全部,携带id跳转到当前商品的评价页面功能
先给evaluate添加点击事件并且拿到评价数据中携带的商品id,然后使用uni.navigateTo跳转到comments页面
<view class="evaluate-rig" @click="commEnts(comment[0].parcontent[0].commid)">
<text>查看全部</text>
<image src="/static/details/quanbu.svg" mode="widthFix"></image>
</view>
// 跳转到评论详情
commEnts(id){
uni.navigateTo({
url:'../comments/comments?id=' + id
})
},
comments组件接收值,然后调用请求接口的方法
// onLoad生命周期可以接收上个页面过来的值 e就包含接收的数据
onLoad(e) {
this.reqComm(e.id);
}
锚点链接将页面滚动到目标位置
uni.pageScrollTo(OBJECT) –将页面滚动到目标位置
在top.vue文件中,通过$parent调用父组件的方法,同时传递参数index过去
methods:{
// 控制短横线跳转位置
navbtn(index){
this.num = index
if(index === 0) {
// 跳转到页面顶部
uni.pageScrollTo({
scrollTop:0,
duration:300
})
}else {
this.$parent.fathEr(index)
}
}
}
父组件中给节点添加 class属性,即可使用uni.pageScrollTo的方法去跳转到该节点
<!-- 商品评价 -->
<evaluate :comment="comment" class="evaluate"></evaluate>
<!-- 商品详情 -->
<product :priceetc="priceetc" class="produce"></product>
// 被 子组件top 用来锚点链接
fathEr(index){
let clsdata = index === 1 ? '.evaluate' : '.produce'
uni.pageScrollTo({
selector:clsdata,
duration:300
})
}
此时发现因为之前固定了导航栏,所以滑动到评价或者详情区域,被胶囊按钮的高度和它到顶部的距离之和盖住了一部分
如下代码导致:
<!-- 子组件 tab -->
<!--组件 默认不显示出来 同时透明度值默认是0 -->
<view class="header-fixed backyes" v-show="!showAbs" :style="{opacity:styleObject}">
<!-- 放置一个div 撑开到顶部距离 -->
<view class="status_bar" :style="'height: '+ tophight.top + 'px;'"></view>
<!-- 引用子组件 且给子组件传递 tophight 子组件里设置高度-->
<top :tophight="tophight"></top>
</view>
解决方法:减去顶部被盖住的距离
思路:不使用uniapp的方法,使用微信小程序的一个方法
SelectorQuery wx.createSelectorQuery()
功能描述:
返回一个 SelectorQuery 对象实例。在自定义组件或包含自定义组件的页面中,应使用 this.createSelectorQuery()
来代替。
const query = wx.createSelectorQuery()
query.select('#the-id').boundingClientRect()
query.selectViewport().scrollOffset()
query.exec(function(res){
res[0].top // #the-id节点的上边界坐标
res[1].scrollTop // 显示区域的竖直滚动位置
})
// 被子组件top 用来锚点链接
fathEr(index) {
let clsdata = index === 1 ? '.evaluate' : '.produce';
let he = this.tophight.top + this.tophight.height; //胶囊按钮到顶部距离 + 自身高度
const query = this.createSelectorQuery();
query.select(clsdata).boundingClientRect();
query.selectViewport().scrollOffset();
query.exec(res => {
// res[0].top // clsdata节点到上边界的距离 节点的上边界坐标
// res[1].scrollTop // 往下滑动屏幕后,到节点的距离 显示区域的竖直滚动位置
// 跳转
uni.pageScrollTo({
scrollTop: res[0].top + res[1].scrollTop - he,
duration: 300
});
});
}
添加功能:当和滑动到目标位置时,顶部的导航栏自动选中对应的tab
/*
* 定义mutations事件类型:函数名称port const ADDCOUNT =
商品详情页下方的功能组件
pages/details/sonzhujian/新建shopping.vue
静态布局如下:
<template>
<view>
<view class="shopping-view">
<view class="shopping-text">
<image src="/static/details/fenxiang.svg" mode="widthFix" />
<text>分享</text>
</view>
<view class="shopping-text middle">
<image src="/static/details/gouwuche.svg" mode="widthFix" />
<text>购物车</text>
<text>10</text>
</view>
<!-- 未收藏 -->
<view class="shopping-text">
<image src="/static/details/shoucang.svg" mode="widthFix" />
<text>收藏</text>
</view>
<!-- 已收藏 -->
<view class="shopping-text">
<image src="/static/details/yishoucang.svg" mode="widthFix" />
<text>已收藏</text>
</view>
<view class="join join-right">加入购物车</view>
<view class="join join-left">立即购买</view>
</view>
<!-- 登录弹窗 -->
</view>
</template>
<script>
export default {
data(){
return {
}
},
methods:{
},
}
</script>
<style scoped>
.shopping-view image {
width: 35rpx;
height: 35rpx;
}
.shopping-view {
font-size: 30upx;
background: #ffffff;
border-top: 1rpx solid #cccccc;
height: 100upx;
display: flex;
align-items: center;
position: fixed;
bottom: 0;
left: 0;
width: 100%;
}
.join {
flex: 2;
text-align: center;
height: 80upx;
line-height: 80upx;
color: white;
}
.shopping-text {
height: 100upx;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.shopping-text text {
font-size: 23upx;
padding-top: 5upx;
color: #666666;
}
.join-right {
background: linear-gradient(to right, #ffc800 10%, #ff9602 80%);
border-top-left-radius: 50upx;
border-bottom-left-radius: 50upx;
}
.join-left {
background: linear-gradient(to right, #ff7500 10%, #ff4b00 80%);
border-top-right-radius: 50upx;
border-bottom-right-radius: 50upx;
margin-right: 10upx;
}
.middle {
border-left: 1rpx solid #f4f4f4;
border-right: 1rpx solid #f4f4f4;
position: relative;
}
.middle text:nth-child(3) {
font-size: 16upx;
background: #fe0036;
color: #ffffff;
border-radius: 50%;
width: 30upx;
height: 30upx;
padding: 0 !important;
text-align: center;
line-height: 30upx;
position: absolute;
top: 10upx;
right: 10upx;
}
</style>
收藏和取消收藏功能
点击收藏后,发起请求
所以这个接口是有权限的,因为一般收藏需要用户登录了才能操作,收藏只属于自己,不属于别人。(权限接口,需携带token)
根据请求的返回值,来显示或者隐藏 组件
在shopping组件中要写俩对应不同的图标情况
<!-- 未收藏 -->
<!-- 根据接口文档 id为1 是收藏 -->
<view class="shopping-text" @click="collEct(1)" v-if="collects == 0">
<image src="/static/details/shoucang.svg" mode="widthFix" />
<text>收藏</text>
</view>
<!-- 已收藏 -->
<!-- 根据接口文档 id为2 是取消收藏 -->
<view class="shopping-text" @click="collEct(0)" v-if="collects == 1">
<image src="/static/details/yishoucang.svg" mode="widthFix" />
<text>已收藏</text>
</view>
在details.vue中拿到商品id
// 商品id
this.goodid = mendata.id
通过ref传递给子组件
props:{
goodid:String,
},
<!-- 未收藏 -->
<!-- 子组件将接收到的goodid 当作第二个data参数填入 -->
<view class="shopping-text" @click="collEct(1,goodid)" v-if="collects == 0">
<image src="/static/details/shoucang.svg" mode="widthFix" />
<text>收藏</text>
</view>
<!-- 已收藏 -->
<!-- 根据接口文档 id为2 是取消收藏 -->
<view class="shopping-text" @click="collEct(0,goodid)" v-if="collects == 1">
<image src="/static/details/yishoucang.svg" mode="widthFix" />
<text>已收藏</text>
</view>
在点击事件中发起post请求
methods:{
// 收藏和取消收藏
async collEct(num,id){
let data = { num:num,id:id } // 等于 { num,id }
try{
// this.Urls.m().enshrineurl 代表url , data 代表携带的参数 data
let enshrine = await new this.Request(this.Urls.m().enshrineurl,data).modepost();
console.log(enshrine);
let { errrcode } = enshrine.msg
if(errrcode == '401') {
// 要登录
}else {
}
}catch {
}
}
},
此时点击收藏会返回401,没有权限,即没有登录的时候,后端不知道是谁在操作收藏会阻止。
/*
* 定义mutations事件类型:函数名称port const ADDCOUNT =
登录弹窗以及微信小程序登录
当点击收藏后,弹出登录弹窗,登陆后刷新页面
登录弹窗
功能在很多页面都要用到,因此抽出做一个公用的组件
在pages/commponents/新建showmodal.vue,并注册到main.js,同时在shopping组件中引入showmodal组件
showmodal.vue 布局如下:
<template>
<view class="showmodel anim" v-if="modeling">
<view class="showmodel-view">
<view class="showmodel-tips">
<text>请登录后在操作</text>
</view>
<view class="showmodel-button">
<button plain="true" >取消</button>
<!-- 小程序中只有button触发登录 -->
<button plain="true" open-type="getUserInfo" @click="getUserInfo()">确定</button>
</view>
</view>
</view>
</template>
<script>
export default {
data(){
return {
// 默认隐藏的数据
modeling:false,
}
},
methods:{
cancel(){
this.modeling = true
},
getUserInfo(){
},
}
}
</script>
<style scoped>
.showmodel{
background: rgba(0,0,0,0.7);
position: fixed;
left: 0;
right: 0;
bottom: 0;
top: 0;
display: flex;
justify-content: center;
align-items: center;
z-index: 999;
}
.showmodel-view{
background: white;
width: 100%;
margin: 0 90rpx;
height: 280rpx;
border-radius: 20rpx;
position: relative;
}
.showmodel-tips{
display: flex;
flex-direction: column;
justify-content: center;
height: 190rpx;
}
.showmodel-tips text{display: block; text-align: center; font-size: 32rpx;}
.showmodel-button{
border-top: 0.5rpx solid #FEECEA;
display: flex; justify-content: space-between;
align-items: center;
position: absolute;
bottom: 0;
left: 0;
right: 0;
}
.showmodel-button button{
font-size: 32rpx;
font-weight: bold;
border: none;
padding: 0 !important;
margin: 0;
width: 100%;
border-radius: inherit;
}
.showmodel-button button:nth-child(1){border-right: 0.5rpx solid #FEECEA;}
.showmodel-button button:nth-child(2){color: #ff6699}
</style>
然后 注册到main.js
// 注册到main.js
// 引入登录弹窗组件
import showmodal from 'pages/commponents/showmodal.vue'
Vue.component('showmodal',showmodal)
//在 shopping组件中引入showmodal组件
<!-- 使用 全局引入的登录弹窗 组件-->
<showmodal></showmodal>
showmodal.vue中,默认隐藏组件
<view class="showmodel anim" v-if="modeling">
当有登录请求的时候显示,即当点击 shopping组件中收藏时,后端返回401检测到没有登录时候,在shopping组件中去操控子组件showmodal的v-if属性
思路:父子组件传值,refs
<!-- 使用 全局引入的登录弹窗 组件-->
<showmodal ref="show"></showmodal>
// 请求的时候如果返回值是401 就展示弹窗组件 (通过ref调用子组件里的方法)
if(errcode == '401') {
// 要登录
this.$refs.show.cancel()
}else {
}
登录逻辑
思路:
封装发起登录的请求方法:
新建login/login.js
引入请求接口、请求方法、弹框类
新建wxLogin类, 接收俩参数,user和msg,在类中封装wxlogin( )方法 和 loGin( )方法
wxlogin方法获取登录请求要携带的五个参数:
1)首先将user对象 解构出微信头像、微信昵称。
2)然后返回一个promise实例对象,执行 **wx.login(Object object)**,调用接口获取登录凭证(code)。通过凭证进而换取用户登录态信息,包括用户在当前小程序的唯一标识(openid)、微信开放平台帐号下的唯一标识(unionid,若当前小程序已绑定到微信开放平台帐号)及本次登录的会话密钥(session_key)等。用户数据的加解密通讯需要依赖会话密钥完成。
将五个数据赋值给了data中,返回的成功值取到data 将会return出去
loGin( )方法中:
1)首先判断constructor构造函数的第二个参数msg,是否值为”getUserProfile:ok”,如果不是就抛出错误(‘登录失败’), //抛出错误,阻止代码允许
2)接着获取code,即调用wxlogin()方法结果赋值给userdata,此时userdata包含了所有的参数,记得添加await和async拿到返回值
3)然后再try,catch中调用后台的微信接口返回结果给user,添加刚才得到的userdata作为请求的data参数
4)判断user中msg值是否为 SUCCESS,如果是就 执行 **uni.setStorageSync(KEY,DATA) **
将 data 存储在本地缓存中指定的 key 中,会覆盖掉原来该 key 对应的内容,这是一个同步接口。
//即存储用户信息到本地缓存
5)然后调用 弹窗 中的showtoast方法,显示登录成功
6)最后返回 return(‘SUCCESS’)
// 微信登录
import request from '../api/api.js' // 引入请求接口
import urls from '../api/request.js' // 引入请求方法
import toast from '../public/toast.js' // 引入弹框类
class wxLogin {
constructor(user,msg){
this.user = user
this.msg = msg
}
// 调用接口登录,async其实是返回一个promise
async loGin(){
// 拒绝登录 即登陆失败
if(this.msg !== "getUserInfo:ok") {
console.log('拒绝登录')
throw ('登录失败')
}
// 获取code
let userdata = await this.wxlogin()
try{
let user = await new request(urls.m().wxloginurl,userdata).modepost()
if(user.msg == 'SUCCESS'){
uni.setStorageSync('wxuser', user.data);//存储用户信息到本地缓存
new toast('登录成功').showtoast()
return('SUCCESS')
}
}catch(e){
return e
// 500,501,502,
//TODO handle the exception
}
}
// 获取code
wxlogin(){
let {avatarUrl,nickName} = this.user
return new Promise((resolve,reject)=>{
wx.login({
success: (res) => {
let data = {//wx404e88f75f32c85b
appid:'wx404e88f75f32c85b',
secret:'0402d0b4b834bf9449b2477115a2e25f',
nickName,
avatarUrl,
code:res.code
}
resolve(data)
},
fail: (err) => {
reject('登录失败')
}
})
})
}
}
export default wxLogin
收藏按钮逻辑
点击收藏按钮后 getUserInfo() 执行思路:
点击确定 会隐藏 showmodel组件
实例化 Toast 类,并传递 title值,同时实例调用 **showloading()**方法,内部 **uni.showLoading **方法生效即显示loading提示框,展示提示文字title值,并且使用mask显示透明蒙层防止触摸穿透
获取用户信息 –wx.getUserProfile,获取用户头像昵称等。当点击拒绝,可以在err回调中查看errMsg,当点击允许后调用res回调,回调中res能够拿到err数据中的包含用户头像和昵称的信息userInfo和 代表成功值的errMsg信息,
然后调用wxusEr(),即调用前边写在login.js里的登录请求方法,成功后存储用户头像、昵称和token值到本地,然后调用其他弹窗组件**showtoast()**,显示登录成功后自动隐藏。
// 登录
async getUserInfo(){
this.modeling = false
new this.$Toast('登录中').showloading()
/*wx.getUserProfile 获取用户信息。页面产生点击事件后才可调用,每次请求都会弹出授权窗口,用户同意后返回userInfo。该接口用于替换 wx.getUserInfo, */
wx.getUserProfile({
desc: '登录' //声明获取用户个人信息后的用途,不超过30个字符
})
.then(res=>{
let {userInfo,errMsg} = res
this.wxusEr(userInfo,errMsg)
})
.catch(err=>{
console.log('拒绝登录或登录失败')
})
},
// 调用登录
async wxusEr(userInfo,errMsg){
try{
let data = await new wxLogin(userInfo,errMsg).loGin()
if(this.msg == 'coll'){
// 更新收藏状态
this.$bus.$emit('collict', {colldata:data})
}
}catch(e){
//TODO handle the exception
}
}
封装消息提示框
微信弹窗已经弃用
现在准备一个登录时候的弹窗反馈:使用微信小程序(uni)自带的uni.showToast
新建public/toast.js
// 一些弹窗
class Toast{
// title String 提示的内容,长度与 icon 取值有关
// icon String 图标
// duration Number 提示的延迟时间,单位毫秒,默认:1500
// mask Boolean 是否显示透明蒙层,防止触摸穿透,默认:false
constructor(title,icon,duration=1300,mask=true) {
this.title = title
this.icon = icon
this.duration = duration
this.mask = mask
}
// 消息提示框:自动消失
showtoast(){
uni.showToast({
title: this.title,
icon:this.icon,
duration: this.duration,
mask:this.mask
});
}
// 消息提示框:手动消失
showloading(){
// uni.showLoading 显示 loading 提示框, 需主动调用 uni.hideLoading 才能关闭提示框。
uni.showLoading({
title: this.title,
mask:this.mask
});
}
}
export default Toast
main.js引入
// toast弹窗
import toast from './public/toast.js'
Vue.prototype.$Toast = toast
携带token令牌登录小程序
使用 js-base64加密token,下载 js-base.js到本地的api下,
在login.js中引入加密方法,取出本地存储的token进行加密,然后将token放置在header中
// request 该文件封装了请求方法
// 引入js-base.js
let Base64 = require('./base64').Base64
const request = class {
// 参数url是request.js暴露出的url
// 参数data是post请求传递的值
constructor(url, arg) {
this.url = url;
this.arg = arg;
}
// 封装 post请求
modepost() {
return new Promise((resolve, reject) => {
uni.request({
url: this.url,
method: 'POST',
data: this.arg,
header: {
Authorization: this.baseFun()
}
}).then(res => {
resolve(res[1].data);
}).then(err => {
reject('出错了');
})
})
}
// 封装 get请求
modeget() {
return new Promise((resolve, reject) => {
uni.request({
url: this.url,
method: 'GET',
header: {
Authorization: this.baseFun()
}
})
.then(res => {
// 输出成功的值
resolve(res[1].data);
}).then(err => {
// 失败的原因/提示
reject('出错了');
})
})
}
// 加密token 方法
baseFun() {
const token = uni.getStorage('wxuser').token //取出token
const base64 = Base64.encode(token + ':') //使用base里的方法
return 'Basic ' + base64
}
}
export default request
当请求携带了token后,shopping.vue中
let enshrine = await new this.Request(this.Urls.m().enshrineurl,data).modepost();
enshrine就能返回带有:
成功的200 状态码 errcodes
表示收藏与否的数值 1或者 2
和提示信息msg
判断:当collects值为errcodes时,下方栏展示收藏或者未收藏的图标,但因为每次重新请求数据默认是取到collects的默认值0,所以图标不会变化。
因此还需要请求一个接口 –获取详情页商品是否收藏:GET /collection?query= {权限接口,需携带token}
// 收藏和取消收藏
async collEct(num,id){
let data = { num:num,id:id } // 等于 { num,id }
try{
// this.Urls.m().enshrineurl 代表url , data 代表携带的参数 data
let enshrine = await new this.Request(this.Urls.m().enshrineurl,data).modepost();
console.log(enshrine,'我是enshrine');
let { errcode } = enshrine.msg
if(errcode == '401') {
// 要登录
// 使用 全局引入的登录弹窗 组件里的方法
this.$refs.show.showing()
}else if(errcode == '200'){
this.collects = enshrine.msg.collects //判断当collects值为errcodes时,下方栏展示收藏或者未收藏的图标,但因为每次重新请求数据默认是取到collects的默认值0,使用图标不会变化
}
}catch {
}
},
获取该商品是否已经收藏
父组件datails中调用获取详情页商品是否收藏的接口 collectionur
即当页面加载就调用该接口检查是否res[2]的返回值,并将返回值赋值给colldata ,通过refs将colldata传递给子组件
// 获取商品是否收藏
let collection = new this.Request(this.Urls.m().collectionurl + '?id=' + id).modeget();
let res = await Promise.all([introduce, wxcommnt,collection]);
console.log('商品是否收藏数据',res[2]);
this.colldata = res[2];
<!-- 底部操作栏组件 -->
<shopping :goodid="goodid" :colldata="colldata"></shopping>
子组件shopping中接收数据,
监听器wtach 监听msg对象中是否有变化,有新值就赋值给collects,这样就实现了收藏图标变化
props:{
goodid:String,
colldata:Object,
},
watch:{
// 收藏和获取该商品是否收藏
colldata(newValue,oldValue) {
console.log(newValue);
let { collects } = newValue.msg
this.collects = collects
}
}
当本地没有token的时候,即用户重新登陆后,
虽然这个请求返回的msg.errcode 值是 302 ,msg.msg返回值是 “未登录,收藏默认未收藏状态”
let collection = new this.Request(this.Urls.m().collectionurl + '?id=' + id).modeget();
let res = await Promise.all([introduce, wxcommnt,collection]);
// .......
{__ob__: Observer}
msg: Object
collects: 0
errcode: "302"
msg: "未登录,收藏默认未收藏状态"
但是:当登陆后,之前收藏的图标仍然显示未收藏
思路:每次登录后刷新收藏状态
首先需要知道登陆成功,即shopping组件中,当errcode值等于401时候,表示没有登录,就调用弹窗组件showmodal.vue展示提示登录的弹窗,并且传递值 ‘coll’ 到子组件的参数中
// 收藏和取消收藏
async collEct(num,id){
let data = { num:num,id:id } // 等于 { num,id }
try{
// this.Urls.m().enshrineurl 代表url , data 代表携带的参数 data
let enshrine = await new this.Request(this.Urls.m().enshrineurl,data).modepost();
console.log(enshrine,'我是查看收藏成功与否');
let { errcode } = enshrine.msg
if(errcode == '401') {
// 要登录
// 使用 全局引入的登录弹窗 组件里的方法
// 登陆这里传值过去 coll
this.$refs.show.showing('coll')
}else if(errcode == '200'){
this.collects = enshrine.msg.collects
}
}catch {
}
},
子组件中,showing方法显示组件,同时接受值‘coll’,
data(){
return {
// 默认隐藏的数据
modeling:false,
// 告知是哪个组件传值过来
msg:'',
}
},
// 显示组件 接收父组件shopping里的coll 这里参数默认是空值
showing(msg = ''){
this.msg = msg
this.modeling = true
},
然后拿着msg到调用登录方法 wxusEr 下:
判断如果当前组件的msg等于了‘coll’,就使用全局事件总线将data值传递出去 命名未 collict
// 调用登录
async wxusEr(userInfo,errMsg){
try{
let data = await new wxLogin(userInfo,errMsg).loGin()
console.log(data);
// data 值是 success 来自于login.js中的返回值
//如果登录成功就
if(this.msg == 'coll'){
// 更新收藏状态 emit是传值的作用 collict随意命名 值的话是data data值是SUCCESS
this.$bus.$emit('collict', {colldata:data})
}
}catch(e){
//TODO handle the exception
}
}
最后在父组件这,先封装一个更新收藏状态的方法,然后在生命周期created中接收 collict 值,当值等于SUCCESS时,调用更新状态的方法
// 登录成功后,更新收藏状态
async reFresh(){
try{
// 获取商品是否已经收藏
let collection = await new this.Request(this.Urls.m().collectionurl + '?id=' + this.goodid).modeget();
console.log(collection,'更新商品是否收藏');
this.collects = collection.msg.collects
}catch(err) {
console.log(err);
}
}
// 接收showmodal组件传过来的判断是否登录的 SUCCESS 值
created(){
this.$bus.$on('collict',res => {
if(res.colldata == 'SUCCESS'){
// 更新收藏状态
this.reFresh()
}
})
},
获取购物车商品件数
在接口文件内添加获取商品件数接口 ,
// 获取购物车件数
let mycarturl = `${url}mycart`
// 加入购物车
let atcarturl = `${url}atcart`
在details组件内,发起请求,拿到res数据,初次其长度为0,即购物车里0件商品
// 获取购物车件数
let mycart = new this.Request(this.Urls.m().mycarturl).modeget();
try {
// 因为后期需要发起多次请求,用promise.all做一个并发请求
let res = await Promise.all([ ... , mycart] );
console.log( ' ... ' , 获取购物车数据',res[3] );
} catch (e) {
}
然后shopping中完成登陆之后刷新购物车功能
// 登录成功后,更新收藏状态
async reFresh(){
try{
// 获取商品是否已经收藏
let collection = await new this.Request(this.Urls.m().collectionurl + '?id=' + this.goodid).modeget();
console.log(collection,'更新商品是否收藏');
this.collects = collection.msg.collects
// 更新购物车件数
let mycart = await new this.Request(this.Urls.m().mycarturl).modeget();
this.cartnum = mycart.data.length
}catch(err) {
console.log(err);
}
}
封装商品sku选择组件
在pages/commponents下新建addtocart组件,同时在details中引入,
静态布局代码如下:
<template>
<!-- 公用的商品sku选择组件 -->
<view >
<view class="Coupon-yin anim" :catchtouchmove="true" ></view>
<view class="Coupon-view coup-anim padd">
<!-- 商品图片 -->
<view class="commodity-view">
<view class="commodity-left-img">
<image :src="attribute.image" mode="aspectFill">图片1
</image>
</view>
<view class="commodity-zh">
<view>¥300</view>
<view>库存100件</view>
<view class="choice">
<text>请选择:</text>
<text>颜色</text>
<text>尺码</text>
</view>
</view>
<view class="commodity-right-img">
<image src="/static/details/guanbi.svg" mode="widthFix">
图片2
</image>
</view>
</view>
<!-- 主要颜色 -->
<view class="sku-view">
<text class="sku-title">主要颜色</text>
<view class="sku-mian">
<view >
<image :src="item.image" mode="aspectFill">
图片3
</image>
<text>黑色</text>
</view>
</view>
</view>
<!-- 尺寸 -->
<view class="sku-view">
<text class="sku-title">尺码</text>
<view class="sku-mian sku-two">
<view>S</view>
<view>M</view>
</view>
</view>
<!-- 购买数量 -->
<view class="sku-view sku-height" >
<view class="sku-title numes">购买数量</view>
<view class="sku-mums-gight">
<view>-</view>
<view>0</view>
<view>+</view>
</view>
</view>
</view>
<!-- 确定按钮 -->
<view class="determine coup-anim" >确定</view>
<!-- 登录弹窗 -->
<showmodal ref="show"></showmodal>
</view>
</template>
<script></script>
<style scoped>
.padd {
padding: 15upx;
margin-bottom: 90rpx;
}
.Coupon-view text {
display: block;
}
.commodity-view {
/* background: #4CD964; */
height: 200upx;
display: flex;
}
.commodity-left-img {
width: 200upx;
height: 200upx;
}
.commodity-left-img image {
width: 200upx;
height: 200upx;
border-radius: 10upx;
}
.commodity-zh {
font-size: 27upx;
color: #3d4245;
flex: 1;
}
.commodity-zh view:nth-child(1) {
font-size: 35upx;
color: #fe0036;
}
.commodity-zh view {
padding-top: 10upx;
padding-left: 15upx;
}
.choice {
display: flex;
align-items: center;
}
.choice text {
padding-right: 8upx;
}
.commodity-right-img {
width: 50upx;
height: 50upx;
}
.commodity-right-img image {
width: 50upx;
height: 50upx;
}
/* sku */
.sku-view {
margin-top: 40upx;
border-bottom: 1px solid #e5e5e5;
}
.sku-title {
font-size: 30upx;
color: #051b28;
font-weight: bold;
margin-bottom: 20upx;
}
.sku-mian image {
width: 45upx;
height: 45upx;
padding-right: 17upx;
}
.sku-mian view {
display: flex;
align-items: center;
height: 65upx;
background: #f5f5f5;
border: 1rpx solid #f5f5f5;
border-radius: 20upx;
padding: 0 14upx;
margin-right: 30upx;
margin-bottom: 30upx;
}
.sku-mian {
font-size: 27upx;
color: #051b28;
display: flex;
align-items: center;
flex-wrap: wrap;
}
.sku-two view {
padding: 0 35upx;
}
/* 购买数量 */
.sku-height {
height: 120upx;
display: flex;
align-items: center;
justify-content: space-between;
}
.numes {
margin: 0 !important;
}
.sku-mums-gight {
display: flex;
align-items: center;
}
.sku-mums-gight view {
font-size: 30upx;
color: #051b28;
font-weight: bold;
background: #f5f5f5;
border-radius: 10upx;
width: 90upx;
height: 70upx;
line-height: 70upx;
text-align: center;
}
.sku-mums-gight view:nth-child(2) {
background: none !important;
}
.determine {
z-index: 999;
font-size: 30upx;
color: #ffffff;
background: #fe0036;
height: 90upx;
line-height: 90upx;
text-align: center;
position: fixed;
bottom: 0;
left: 0;
right: 0;
}
/* 选了颜色加上边框 */
.active {
border: 1rpx solid red !important;
}
</style>
添加 隐藏或显示 购物车组件功能
显示组件思路:
在父组件details中通过ref绑定子组件Addtocart,然后details中封装一个方法用于在子组件shopping中用$parent调用该方法来控制Addtocart组件中的数据
<!-- 公用的商品sku组件 父组件通过ref可以通过这个方式 控制节点 -->
<Addtocart ref="addto"></Addtocart>
// 父组件details中被子组件shopping调用的方法
shoPp(){
this.$refs.addto.showCou()
}
// shopping调用父组件details里的sku组件
couponsFun(){
this.$parent.shoPp()
}
// Addtocart组件中控制显示隐藏的方法 被其他组件调用
methods:{
// 被其他组件调用显示sku组件
showCou(){
this.couponif = true
}
}
隐藏组件思路:
给叉叉按钮或者遮罩背景添加点击事件,然后将v-if属性变为false
<!-- 背景遮罩层 -->
<view class="Coupon-yin anim" :catchtouchmove="true" @click="hideCou()"></view>
<view class="commodity-right-img" @click="hideCou()">
<!-- 叉叉按钮 -->
<image src="/static/details/guanbi.svg" mode="widthFix">
图片2
</image>
</view>
// 隐藏sku组件
hideCou(){
this.couponif = false
}
sku组件的确定功能
点击立即购买也会显示sku组件,但是立即购买显示的sku组件的确定按钮是会跳转到付款页面,而点击加入购物车显示的sku组件的确定按钮仅仅是加入了购物车功能,在点击确定按钮时,要让按钮知道点击的目的。
所以要传值过去,让确定按钮执行相应的操作。
思路:
多写两条确定按钮,分别触发的点击事件不一样,通过v-if来选择不同的情况。
<!-- 确定按钮 -->
<view class="determine coup-anim" v-if="mean == '002'">确定</view>
<view class="determine coup-anim" v-if="mean == '003'">确定</view>
// data中默认mean值是002
data() {
return {
couponif:false, //显示 或 隐藏该组件
mean: '002', // 设置一个值 002代表加入购物车 003代表直接购买
}
},
// 在shopping组件中给立即购买添加相同的点击事件,但传递的参数值不同
<view class="join join-right" @click="couponsFun('002')">加入购物车</view>
<view class="join join-left" @click="couponsFun('003')">立即购买</view>
// 调用父组件details里的sku组件 并且将相应点击触发的值传递给父组件的shoPp方法的参数中
couponsFun(mean){
this.$parent.shoPp(mean)
}
// 父组件details被子组件shopping调用 同时接收子组件传递的参数值mean 最终通过ref拿到子组件sku的节点并使用其showCou方法 同时传递mean参数值
shoPp(mean){
this.$refs.addto.showCou(mean)
}
请求商品sku组件的数据
后台request.js配置很好接口后,到父组件details,发起请求拿到数据,并拿到轮播的第一张图片和价格、库存、商品id、标题等数据然后psuh到res[4]数据中,最后传递给子组件sku使用数据
// 请求数据方法 id是商品的id
async detRequest(id) {
// ......
// 商品sku组件数据
let wxsku = new this.Request(this.Urls.m().wxskuurl + '?id=' + id).modeget();
try {
// 因为后期需要发起多次请求,用promise.all做一个并发请求
let res = await Promise.all([ ... , wxsku ]);
console.log(' ... , 'sku组件数据',res[4]);
// 获取sku数据 这里的数据来自轮播图和商品价格
let defaultdata = {
image:mendata.media[0].imgArray[0], // 拿到轮播图第一张图片
price:mendata.describe.Trueprice, // 拿到价格
totalstock:mendata.describe.Total_stock, // 拿到库存
id:mendata.id, // 拿到商品id
title:mendata.describe.title, // 拿到商品标题,在提交到购物车需要用到
}
// 将以上数据 push到一个数组里面 即 商品sku组件数据
this.skudata = res[4].data
this.skudata.push(defaultdata)
console.log(this.skudata,'商品sku新数据')
} catch (e) {}
},
子组件保存数据:
props:{
skudata:Array,
},
// watch监听数据
// 因为图片会随着选择码数时变化,所以使用监听更高效
watch:{
// 获取父组件来的sku默认展示数据
skudata(newValue,oldValue) {
console.log(newValue,'sku新值');
this.id = newValue[2].id
this.attribute = newValue[2]
this.title = newValue[2].title
}
}
data() {
return {
couponif:false, //显示 或 隐藏该组件
mean: '002', // 设置一个值 002代表加入购物车 003代表直接购买
id:'', //商品id
attribute:{}, // 商品展示的sku
title:'', // 商品标题
}
},
sku组件中模板代码修改如下:
<!-- 商品图片 -->
<view class="commodity-view">
<view class="commodity-left-img">
<image :src="attribute.image" mode="aspectFill"></image>
</view>
<view class="commodity-zh">
<view>¥{{attribute.price}}</view>
<view>库存{{attribute.totalstock}}件</view>
<view class="choice">
<text>请选择:</text>
<text>颜色</text>
<text>尺码</text>
</view>
</view>
<view class="commodity-right-img" @click="hideCou()">
<!-- 叉叉按钮 -->
<image src="/static/details/guanbi.svg" mode="widthFix"></image>
</view>
</view>
<!-- 主要颜色 -->
<view class="sku-view">
<text class="sku-title">主要颜色</text>
<view class="sku-mian">
<block v-for="(item,index) in skudata[1]" :key="index">
<view >
<image :src="item.image" mode="aspectFill">
</image>
<text>{{item.color}}</text>
</view>
</block>
</view>
</view>
<!-- 尺寸 -->
<view class="sku-view">
<text class="sku-title">尺码</text>
<view class="sku-mian sku-two">
<block v-for="(item,index) in skudata[0]" :key="index">
<view>{{item}}</view>
</block>
</view>
</view>
计算属性处理选中某个sku
给 颜色 / 尺码 按钮添加点击事件,并且动态绑定边框变色的css属性 ,属性值默认-1,index值取自于对应的item 或 item.color
// 给颜色按钮添加点击事件,并且动态绑定边框变色的css属性
<view @click="colorFun(item.color,index)" :class="{active:index == colornum}">
<image :src="item.image" mode="aspectFill"></image>
<text>{{item.color}}</text>
</view>
// 尺码添加点击事件, 并且动态绑定边框变色的css属性
<block v-for="(item,index) in skudata[0]" :key="index">
<view @click="sizeFun(item,index)" :class="{active:index == sizenum}">{{item}}</view>
</block>
// 选择颜色
colorFun(color,index){
console.log(color,index);
this.colornum = index
},
// 选择尺码
sizeFun(size,index){
console.log(size,index);
this.sizenum = index
}
接着完成请选择:颜色 尺码 的文字动态变化
使用计算属性:choice
// 选择颜色
colorFun(color,index){
console.log(color,index);
this.colornum = index
this.colorvalue = color //每次选中颜色后 data中的colorvalue就有值 可以将其赋值给请选择。。。
},
// 选择尺码
sizeFun(size,index){
console.log(size,index);
this.sizenum = index
this.sizevalue = size //每次选中尺码后 data中的sizevalue就有值 可以将其赋值给请选择。。。
}
computed:{
// 选择sku的改变
choice(){
if(this.colorvalue == '' || this.sizevalue == '') {
return '请选择'
}else {
return '已选择'
}
}
},
**此时,在template模板中的代码块如下使用计算属性中的方法 **
<view class="choice">
<text>{{choice}}:</text>
<text>颜色</text>
<text>尺码</text>
</view>
再一次使用计算属性:skumen 完成选择颜色或者尺码时,上方主要颜色下 显示的文字
// 选择颜色或者尺码 主要颜色下显示的文字
skumen(){
// 两个都被选中
if(this.sizevalue != '' && this.colorvalue != '') {
// 返回一个对象
return {
color:this.colorvalue,size:this.sizevalue,
}
// 两个都未选中
}else if(this.sizevalue == '' && this.colorvalue == ''){
return {
color:'主要颜色',size:'尺码',
}
// 假如尺码没有选择, 并且 颜色选择了
}else if(this.sizevalue == '' && this.colorvalue != '') {
return {
color:'',size:'尺码',
}
// 假如尺码选择了, 并且 颜色没选择
}else if(this.sizevalue != '' && this.colorvalue == '') {
return {
color:'主要颜色',size:'',
}
}
}
最后当未选择商品和尺码时候 ,禁止确定按钮功能
在计算属性中,添加return的一个 键值对,当ban为true时,可以触发确定的点击功能
扩展知识:JS中的逻辑运算符&&、||
- 只要“&&”前面是false,无论“&&”后面是true还是false,结果都将返“&&”前面的值;
- 只要“&&”前面是true,无论“&&”后面是true还是false,结果都将返“&&”后面的值;
- 只要“||”前面为false,不管“||”后面是true还是false,都返回“||”后面的值。
- 只要“||”前面为true,不管“||”后面是true还是false,都返回“||”前面的值。
<!-- 确定按钮 -->
<!-- 利用计算属性 如果两个未选中就禁止使用确定按钮 -->
<view class="determine coup-anim" v-if="mean == '002'" @click="skumen.ban && detErmine()">确定</view>
// 。。。。。。
// 选择颜色或者尺码 主要颜色下显示的文字
skumen(){
// 两个都被选中
if(this.sizevalue != '' && this.colorvalue != '') {
// 返回一个对象
return {
color:this.colorvalue,size:this.sizevalue,ban:true
}
// 两个都未选中
}else if(this.sizevalue == '' && this.colorvalue == ''){
return {
color:'主要颜色',size:'尺码',ban:false
}
// 假如尺码没有选择, 并且 颜色选择了
}else if(this.sizevalue == '' && this.colorvalue != '') {
return {
color:'',size:'尺码',ban:false
}
// 假如尺码选择了, 并且 颜色没选择
}else if(this.sizevalue != '' && this.colorvalue == '') {
return {
color:'主要颜色',size:'',ban:false
}
}
}
请求每个sku的库存、价格
分析可得当同时选择了颜色和尺码,才会展示相应的价格和库存,即发起请求 this.skuing()
那么如何得知选择了尺码和颜色呢
**在监听器中,监听skumen方法返回的数据变化: ** 当得到的newValue中的color和size数据不为空,即都选择了就调用请求方法
methods:{
// 获取请求每个sku的数据
async skuRequest(obj){
try{
let querysku = await new this.Request(this.Urls.m().queryskuurl,obj).modepost();
console.log(querysku);
// 刚好得到的data和之前用于展示库存、价格、图片的对象里的键值对一致
this.attribute = querysku.data[0]
}catch(err){
//TODO handle the exception
console.log(err);
}
}
}
watch:{
// 是否选择了尺码和颜色
skumen(newValue,oldValue){
console.log(newValue,'更新后的尺码和颜色数据');
let { color,size } = newValue
if(color != '' && size != '') {
this.skuRequest(
// 将请求需要的参数在这里放到一个对象中一个带到请求方法参数中
{id:this.id, color:color,size:size}
)
}
}
}
商品购买数量加减
对减号和加号添加点击事件,并且当数值为1时,禁用减号按钮,核心思路是利用三元表达式和 click 的false情况
<!-- 购买数量 -->
<view class="sku-view sku-height" >
<view class="sku-title numes">购买数量</view>
<view class="sku-mums-gight">
<!-- 三元表达式 如果商品数量等于1 就禁用 - 按钮 ,否则可以使用减号按钮-->
<view @click="many === 1 ? forbid==false : forbid==true && reDuce()">-</view>
<view>{{many}}</view>
<view @click="pLus()">+</view>
</view>
</view>
data() {
return {
many:1, // 购买数量
forbid:true, // 购买数量最少为1,<=1 时禁止按钮点击
}
}
// 数量
reDuce(){
this.many --
if(this.many === 1) {
new this.$Toast('数量不能低于1','error').showtoast()
}
},
// 加数量
pLus(){
this.many ++
},
加入所选商品到购物车
当点击确定按钮,需要在addtoart.vue文件中获取如下的data对象中的所有数据用作请求的data参数,
拿到返回结果后,判断如果存在errcode 即没有登录,调用登录弹窗和登录请求弹窗。
如果返回的数据值 ‘SUCCESS’ ,那么隐藏sku组件,同时调用反馈弹窗,并且调用一次获取购物车件数的接口,将拿到的件数(length值)传递给shoppping.vue组件中,刷新购物车的件数 —(使用vuex传值)
// 加入购物车按钮 (需要选择颜色和尺码后才能触发)
detErmine(){
// 点击确定按钮 要将这里的data对象传到请求作为参数使用
let data = {
id:this.id,
size:this.sizevalue,
color:this.colorvalue,
image:this.attribute.image,
price:this.attribute.price,
title:this.title,
many:this.many,
}
try{
let atcartdata = await new this.Request(this.Urls.m().atcarturl,data).modepost();
console.log(atcartdata,'加入购物车的返回数据');
if(atcartdata.msg.errcode){
// 如果errcode存在就需要登录
// 登陆这里传值过去 'coll'
this.$refs.show.showing('coll')
}else if(atcartdata.msg == "SUCCESS"){
this.hideCou()
new this.$Toast('加入购物车成功','success').showtoast() //反馈 弹窗
// 相同的sku(颜色、尺码一样)加入购物车后只显示一条,仅件数变化 (后端接口已经做了处理)
let mycart = await new this.Request(this.Urls.m().mycarturl).modeget();
console.log(mycart,"购物车件数");
}
}catch(e){
//TODO handle the exception
}
}
vuex传值到购物车的商品数量
在项目根目录下新建文件夹 store,其下新建store.js
// vuex存放数据,数据仓库,数据管理中心
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
// 购物车件数
const cartnum = {
nums:''
}
const state = {
// 购物车件数
cartnum,
}
export default new Vuex.Store({
// 数据仓库中心
state,
// 传值
// actions : 异步传值 --不能直接到达数据仓库, 组件中使用 : store.dispatch()
// getters : 类似计算属性,带有缓存,有新的值进来才会被触发 (不常用)
// mutitions : 同步传值:可以直接传给state 组件中使用 : store.commit('mutations中方法名',value)
mutations:{
// 加入购物车成功后传值的购物车条数 (第一个参数是state仓库,第二个是要传递过来的值)
mutacart(state,nums){
// 接收到组件传过来的值之后要存储到数据中心仓库
state.cartnum = {
nums:nums
}
}
}
})
在sku组件中,将获取到的购物车件数,存储到vuex中,
// 加入购物车按钮 (需要选择颜色和尺码后才能触发)
async detErmine(){
// 点击确定按钮 要将这里的data对象传到请求作为参数使用
let data = {
id:this.id,
size:this.sizevalue,
color:this.colorvalue,
image:this.attribute.image,
price:this.attribute.price,
title:this.title,
many:this.many,
}
try{
let atcartdata = await new this.Request(this.Urls.m().atcarturl,data).modepost();
console.log(atcartdata,'加入购物车的返回数据');
if(atcartdata.msg.errcode){
// 如果errcode存在就需要登录
// 登陆这里传值过去 'coll'
this.$refs.show.showing('coll')
}else if(atcartdata.msg == "SUCCESS"){
this.hideCou()
new this.$Toast('加入购物车成功','success').showtoast() //反馈 弹窗
// 相同的sku(颜色、尺码一样)加入购物车后只显示一条,仅件数变化 (后端接口已经做了处理)
let mycart = await new this.Request(this.Urls.m().mycarturl).modeget();
console.log(mycart,"购物车件数");
// 使用vuex方法 将购物车件数值传递到 仓库
this.$store.commit('mutacart',mycart.data.length)
}
}catch(e){
//TODO handle the exception
}
},
在shopping.vue组件,监听器中使用vuex的数据
watch:{
// 加入购物车成功后存储到vuex后,从vuex仓库中心拿取值;监听vuex数据中心的值变化,
// 如果vuex数据中心的值变化才会被触发执行
"$store.state.cartnum"(newValue,oldValue) {
console.log(newValue,'监听器监听vuex中购物车件数');
this.cartnum = newValue
}
},
商品订单页
直接下单获取待付款商品数据
pages新建 payment-page目录,下方新建payment.vue,同时去page.json中引入
然后对addtocart.vue文件中的确定按钮进行添加点击事件,并配置方法,
<view class="determine coup-anim" v-if="mean == '003'" @click="skumen.ban && purChase()">确定</view>
// 直接下单
async purChase(){
// 点击确定按钮 要将这里的data对象传到请求作为参数使用,因为购物车的商品类目不止一个,所以用数组包裹很多对象
let data = [
{
id:this.id,
size:this.sizevalue,
color:this.colorvalue,
image:this.attribute.image,
price:this.attribute.price,
title:this.title,
many:this.many,
total_price:this.attribute.price * this.many, //商品总价
},
]
// 校验登录状态
try{
let tokening = await new this.Request(this.Urls.m().tokeningurl).modeget();
console.log(tokening,'检查登录状态');
if(tokening.msg.errcode == "401") {
// 没有token 就展示登录弹窗
this.$refs.show.showing('coll')
}else if(tokening.msg == "SUCCESS") {
// 数组,对象转换成字符串才能携带值
let cartdata = JSON.stringify(data)
// 跳转到下单页面 同时携带参数
uni.navigateTo({
url:'../payment-page/payment?cartdata=' + cartdata,
})
}
}catch(e){
//TODO handle the exception
}
},
// 在payment.vue组件中,使用onLoad生命周期 的e参数 接收 参数
export default {
// e就是上个页面使用navigateTo路径,携带的值 --来自于addtocart组件 purChase方法
onLoad(e){
// 将JSON字符串转化为对象
console.log(JSON.parse(e.cartdata))
},
}
视图层展示待付款的商品
js浮点数解决:
在上个组件addtocart中对total_price数据进行修改
*total_price:parseFloat((this.attribute.price * this.many).toFixed(10)),* //商品总价 ,解决浮点数
总价显示小数点后两位:
npm i e-commerce_price
// 引入下载的node库 –支付小数点后两位 价格补零
const Price = require(‘e-commerce_price’)
payment.vue 完整代码如下:
<template>
<view>
<view class="payment-view">
<!-- 收货地址 -->
<view class="payment-name" >
<view class="payment-left-img">
<image src="/static/loading/address-shouhuo.svg" mode="widthFix"></image>
</view>
<!-- 收货地址 -->
<view class="payment-add" >请选择收货地址</view>
<view class="payment-right-img">
<image src="/static/loading/shouhuo-jiantou.svg" mode="widthFix"></image>
</view>
</view>
<!-- 商品详情 -->
<view class="payment-commodity">
<block v-for="(item,index) in comminfo" :key="index">
<view class="payment-order">
<view class="payment-order-img">
<image :src="item.image" mode="aspectFill"></image>
</view>
<view class="payment-title">
<text>{{item.title}}</text>
<text class="text-you">颜色:{{item.color}};尺码:{{item.size}}</text>
</view>
<view class="payment-flex">
<view class="payment-price">
<text>¥ {{item.price}}</text>
<text class="text-you">x{{item.many}}</text>
</view>
<view>
共{{item.many}}件 小计: ¥ {{item.total_price}}
</view>
</view>
</view>
</block>
</view>
</view>
<!-- 底部提交订单 -->
<view class="place-order">
<text>合计: ¥{{Totalprice}}</text>
<text @click="placeOrder()">提交订单</text>
</view>
</view>
</template>
<script>
// 引入下载的node库 --支付小数点后两位 价格补零
const Price = require('e-commerce_price')
export default {
data(){
return {
comminfo:[], //上个页面接收的商品数据
Totalprice:0, //商品总价 小数点两位数
}
},
// e就是路径上携带的值 来自于addtocart组件 purChase方法
onLoad(e){
// 将JSON字符串转化为对象
console.log(JSON.parse(e.cartdata))
this.comminfo = JSON.parse(e.cartdata)
// 合计支付总价计算 numdata默认为0,使用forEach遍历
let numdata = 0
JSON.parse(e.cartdata).forEach(item=>{
numdata += item.total_price
})
// 合计总价 小数点两位数
this.Totalprice = Price(numdata) // 将需补0的数据传入方法
},
}
</script>
<style>
page{background-color: #f2f2f2;}
.payment-view{margin: 15upx;}
.payment-name{
height: 150upx;
background-color: #FFFFFF; border-radius: 15upx;
font-size: 28upx;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 10upx;
}
.payment-left-img{width: 60upx; height: 60upx;}
.payment-left-img image{width: 60upx; height: 60upx;}
.payment-add text{display: block;}
.payment-add text:nth-child(2){padding: 4rpx 0;}
.payment-add{flex: 1;padding-left: 20rpx;}
.payment-right-img{width: 40upx; height: 40upx;}
.payment-right-img image{width: 40upx; height: 40upx;}
/* 下单商品 */
.payment-commodity{
background-color: #FFFFFF; border-radius: 15upx;
font-size: 28upx;
padding: 10rpx;
margin: 15upx 0;
}
.payment-order{display: flex;justify-content: space-between;
height: 200upx;
/* background: #4CD964; */
margin-bottom: 20rpx;
}
.payment-order text{display: block;}
.payment-order-img{width: 200upx; height: 200upx;
border-radius: 8upx;
padding-right: 10upx;
}
.payment-order-img image{width: 200upx; height: 200upx;
border-radius: 8upx;
}
.payment-title{flex: 1;}
.payment-title text:nth-child(1){
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.payment-title text:nth-child(2){
background-color: rgb(250, 250, 250);
padding: 5upx;
border-radius: 7upx;
}
.payment-price{text-align: right;}
.payment-flex{
display: flex;
flex-direction: column;
justify-content: space-between;}
.text-you{color: rgb(156, 156, 156);
margin-top: 8upx;
}
/* 提交 */
.place-order{height: 90upx;
background-color: #FFFFFF;
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
align-items: center;
justify-content: flex-end;
}
.place-order text:nth-child(2){background-color: rgb(255, 0, 54);
height: 90upx;
width: 270upx;
color: #FFFFFF;
text-align: center;
line-height: 90upx;
margin-left: 30upx;
}
</style>
我的收货地址页面
在pages下新建my-address文件夹,然后在下方新建my-address.vue、new-address.vue文件
然后注册到pages.json
{
"path": "pages/my-address/my-addres",
"style": {
"navigationBarTitleText": "我的收货地址"
}
},
{
"path": "pages/my-address/new-address",
"style": {
"navigationBarTitleText": "新增收货地址"
}
},
my-address.vue组件静态布局
<template>
<view>
<view class="my-address-view">
<view class="my-address-name" @click="getAdd(item)">
<text>北京大学</text>
<view class="my-address-adding">
<text>张三</text>
<text>10086</text>
</view>
</view>
<view class="my-address-change">
<image src="/static/loading/genggai.svg" mode="widthFix"></image>
</view>
</view>
<!-- 新增收货地址 -->
<view class="button-address" >
<image src="/static/loading/xinzeng.svg" mode="widthFix"></image>
<text>新增收货地址</text>
</view>
</view>
</template>
<script>
export default {
data(){
return {
}
},
methods:{
async getadd(){
try{
let atcartdata = await new this.Request(this.Urls.m().gainaddurl).modeget()
console.log(atcartdata,'请求收货地址信息');
}catch(e){
//TODO handle the exception
}
}
},
created(){
this.getadd()
}
}
</script>
<style>
page{background-color: #F2F2F2;}
.my-address-view{
font-size: 30upx;
background: #FFFFFF; margin: 10upx;
padding: 0 10upx;
border-radius: 10upx;
display: flex;
align-items: center;
justify-content: space-between;
}
.my-address-name{
flex: 1;
height: 130rpx;
display: flex;
flex-direction: column;
justify-content: center;
}
.my-address-name view{display: flex; align-items: center;}
.my-address-change{width: 50upx; height: 50upx;}
.my-address-change image{width: 50upx; height: 50upx;}
.my-address-adding{color: #A7A7A7; padding-top: 8upx;}
.my-address-adding text:nth-child(1){padding-right: 15upx;}
/* 新增收货地址 */
.button-address{
font-size: 30upx;
display: flex; align-items: center;
justify-content: center;
height: 100upx;
background: #FFFFFF;
position: fixed;
bottom: 0;
left: 0;
right: 0;
}
.button-address image{width: 50upx; height: 50upx;
padding-right: 20upx;
}
</style>
完成新的登录页面
在pages/components/新建login-page.vue
在里边完成登录逻辑
<template>
<view class="login-page" v-if="login">
<button type="primary" open-type="getUserInfo" @click="getUserInfo()">登录</button>
</view>
</template>
<script>
// 引入登录
import wxLogin from '../../login/login.js'
export default{
data() {
return {
login: false
}
},
methods:{
showing(boll=true){
this.login = boll
},
// 登录
async getUserInfo(event){
new this.$Toast('登录中').showloading()
//新的登录接口:wx.getUserProfile
wx.getUserProfile({
desc: '登录'
})
.then(res=>{
let {userInfo,errMsg} = res
this.wxusEr(userInfo,errMsg)
})
.catch(err=>{
console.log('拒绝登录或登录失败')
})
},
// 调用登录
async wxusEr(userInfo,errMsg){
try{
let data = await new wxLogin(userInfo,errMsg).loGin()
// 登录成功更新需要请求的接口或者数据
this.$bus.$emit('mycart', {cart:data})
this.login = false
}catch(e){
//TODO handle the exception
}
}
}
}
</script>
<style scoped>
.login-page{
position: fixed;
left: 0;
right: 0;
bottom: 0;
top: 0;
display: flex;
justify-content: center;
align-items: center;
background: #FFFFFF;
z-index: 999;
}
.login-page button{
width: 350rpx;
}
</style>
因为其他地方会用到,所以放到公共main.js中
// 引入新的登录组件
import loginpage from 'pages/commponents/login-page.vue'
Vue.component('loginpage',loginpage)
引入到收货组件my-address.vue里使用
<template>
<view>
<view class="my-address-view">
。。。。。
</view>
<!-- 新增收货地址 -->
。。。。。
<!-- 登录界面 -->
<loginpage></loginpage>
</view>
</template>
完整login-page.vue逻辑代码
<script>
// 引入 微信登录逻辑
import wxLogin from '../../login/login.js'
export default{
data() {
return {
login: false //默认不展示登录组件,除非登录接口返回数据
}
},
methods:{
// 当需要展示组件时,调用这个方法 默认是true 即可以展示组件
showing(boll=true){
this.login = boll
},
// 登录的方法
async getUserInfo(event){
// 显示其他的弹窗 登录时的反馈 弹窗
new this.$Toast('登录中').showloading()
/*wx.getUserProfile 获取用户信息。页面产生点击事件
后才可调用,每次请求都会弹出授权窗口,用户同意后返回 userInfo
*/
//新的登录接口:wx.getUserProfile
wx.getUserProfile({
desc: '登录咯'
})
.then(res=>{
let {userInfo,errMsg} = res // res中包含用户的个人信息 如昵称,头像等
this.wxusEr(userInfo,errMsg)
})
.catch(err=>{
console.log('拒绝登录或登录失败')
new this.$Toast('用户取消登录','none').showtoast() //登录时的反馈 弹窗
})
},
// 调用登录
async wxusEr(userInfo,errMsg){
try{
// 微信登录逻辑
let data = await new wxLogin(userInfo,errMsg).loGin()
// 登录成功后 更新需要请求的接口或者数据 emit是传值的作用
// mycart、cart随意命名 cart值是data data值是SUCCESS 在my-addres.vue中接收 $on
this.$bus.$emit('mycart', {cart:data})
this.login = false
}catch(e){
//TODO handle the exception
}
}
}
}
</script>
my-address.vue组件逻辑代码
<script>
export default {
data(){
return {
}
},
methods:{
// 获取收货地址
async getadd(){
try{
let atcartdata = await new this.Request(this.Urls.m().gainaddurl).modeget()
console.log(atcartdata,'请求到的收货地址信息');
if(atcartdata.msg.errcode) {
// 需要登录 使用ref获取登录组件的节点 然后调用节点下的控制展示组件的方法showing()
this.$refs.loginmen.showing()
}
}catch(e){
//TODO handle the exception
}
}
},
created(){
this.getadd()
},
mounted() {
this.$bus.$on('mycart',res => {
// res就是收到的值 这里是来自于loain-oage.vue中的 cart: "SUCCESS"
console.log(res,'我是收到的data')
if(res.cart == 'SUCCESS') {
// 如果登录成功就 获取收货地址
this.getadd()
}
})
}
// gainaddurl
}
</script>
获取我的收货地址数据
首次获取到的数据肯定是空的,因为没有设置过地址
对获取到的请求结果进行判断,出现errcode值就需要重新登陆,数据长度为0,展示提示新增地址的消息,最后拿到结果赋值
data(){
return {
adddata:[], //存放收货地址信息
searchno:false, //默认不展示提示
}
},
methods:{
// 获取收货地址
async getadd(){
try{
let atcartdata = await new this.Request(this.Urls.m().gainaddurl).modeget()
console.log(atcartdata,'请求到的收货地址信息');
// 如果存在errcode值就表示没有token 没有登录
if(atcartdata.msg.errcode) {
// 就需要登录 使用ref获取登录组件的节点 然后调用节点下的控制展示组件的方法showing()
this.$refs.loginmen.showing()
// 如果data长度为0,就表示收货地址为空
}else if(atcartdata.data.length === 0) {
// 展示提示信息
this.searchno = true
}else {
// 登陆后拿到数据时赋值给一个数组中
this.adddata = atcartdata.data
this.searchno = false //同时隐藏提示信息
}
}catch(e){
//TODO handle the exception
}
}
}
最终拿到数据后,遍历数据然后放到view标签中展示
<template>
<view>
<block v-for="(item,index) in adddata" :key="index">
<view class="my-address-view">
<view class="my-address-name" @click="getAdd(item)">
<text>{{item.city + item.address}}</text>
<view class="my-address-adding">
<text>{{item.name}}</text>
<text>{{item.mobile}}</text>
</view>
</view>
<view class="my-address-change">
<image src="/static/loading/genggai.svg" mode="widthFix"></image>
</view>
</view>
</block>
<!-- 新增收货地址 -->
<view class="button-address" >
<image src="/static/loading/xinzeng.svg" mode="widthFix"></image>
<text>新增收货地址</text>
</view>
<!-- 登录界面 -->
<loginpage ref="loginmen"></loginpage>
<!-- 如果没有保存收货地址 就展示此处 -->
<view class="empty-cart" v-if="searchno">
<image src="/static/search/sousuono.svg" mode="widthFix"></image>
<text>你还没有收货地址</text>
</view>
</view>
</template>
然后在new-address.vue文件中,进行代码编写 :静态布局如下 主要难点是使用了 uniapp的picker选择器
picker:
从底部弹起的滚动选择器。支持五种选择器,通过mode来区分,分别是普通选择器,多列选择器,时间选择器,日期选择器,省市区选择器,默认是普通选择器。
<template>
<view class="new-address-view">
<!-- 收货地址 -->
<view class="new-address">
<view>收货城市:</view>
<view>
<input type="text" placeholder="请选择收货城市" disabled placeholder-style="color:#9c9c9c" />
</view>
<view>
<picker mode="region" >
<text>选择城市</text>
</picker>
</view>
</view>
<!-- 详细地址 -->
<view class="new-address">
<view>详细地址:</view>
<view><input type="text" placeholder="请填写详细地址" placeholder-style="color:#9c9c9c" /></view>
</view>
<!-- 联系人 -->
<view class="new-address">
<view>联系人:</view>
<view><input type="text" placeholder="请填写收货人姓名" placeholder-style="color:#9c9c9c" /></view>
</view>
<!-- 手机号码 -->
<view class="new-address">
<view>手机号:</view>
<view><input type="number" placeholder="请填写收货人手机号码" placeholder-style="color:#9c9c9c" /></view>
</view>
<!-- 保存地址 -->
<view class="conServe adcolor" >保存地址</view>
<!-- 修改地址 -->
<!-- <view class="conServe adcolor" v-if="!nameadd">修改地址</view> -->
<!-- 删除地址 -->
<!-- <view class="conServe decolor" v-if="!nameadd">删除</view> -->
</view>
</template>
<script></script>
<style scoped>
.new-address-view {
padding: 10upx 30upx;
}
.new-address {
height: 100upx;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 28rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.new-address view:nth-child(1) {
width: 150rpx;
}
.new-address view:nth-child(2) {
flex: 1;
}
.new-address view:nth-child(3) {
color: #4cd964;
}
/* 保存 */
.conServe {
height: 80rpx;
font-size: 30rpx;
border-radius: 10rpx;
font-weight: bold;
display: flex;
align-items: center;
justify-content: center;
margin-top: 30rpx;
}
.adcolor {
background: linear-gradient(to right, #ffd300, #e6be00);
}
.decolor {
background-color: red;
color: #ffffff;
}
</style>
在选择城市这里,添加**@change事件 – value 改变时触发 change 事件,event.detail = {value: value}**
然后把e里边拿到的数据赋值给city,然后将city字符串通过 v-model绑定到 input标签中,
其他input也通过v-model绑定数据到data中
<!-- 收货城市 -->
<view class="new-address">
<view>收货城市:</view>
<view>
<input type="text" v-model="city" placeholder="请选择收货城市" disabled placeholder-style="color:#9c9c9c" />
</view>
<view>
<picker mode="region" @change="regionChange">
<text>选择城市</text>
</picker>y
</view>
<!-- 详细地址 -->
<view class="new-address">
<view>详细地址:</view>
<view>
<input type="text" v-model="address" placeholder="请填写详细地址" placeholder-style="color:#9c9c9c" />
</view>
</view>
<!-- 联系人 -->
<view class="new-address">
<view>联系人:</view>
<view>
<input type="text" v-model="name" placeholder="请填写收货人姓名" placeholder-style="color:#9c9c9c" />
</view>
</view>
<!-- 手机号码 -->
<view class="new-address">
<view>手机号:</view>
<view>
<input type="number" v-model="mobile" placeholder="请填写收货人手机号码" placeholder-style="color:#9c9c9c" />
</view>
</view>
</view>
data() {
return {
city:'',
address:'',
name:'',
mobile:'',
}
},
methods:{
// 省市区选择 e里边包含了选择的城市数据
regionChange(e){
// console.log(e.detail)
let values = e.detail.value
// 浙江省 杭州市 上城区
var str = '' //空字符串
values.forEach((item) => {
str += item + ' '
})
this.city = str
},
}
新增收货地址
拿到用户填写的收货地址数据后,需要点击保存地址按钮提交数据到后台,
在new-address.vue 中,给按钮绑定点击事件preTion( ),在事件中发起请求,声明一个对象保存需要的参数数据,作为请求的参数使用,并且请求成功和失败时候设置对应的提示弹窗,
<script>
export default {
data() {
return {
city:'',
address:'',
name:'',
mobile:'',
}
},
methods:{
// 省市区选择 e里边包含了选择的城市数据
regionChange(e){
// console.log(e.detail)
let values = e.detail.value
// 浙江省 杭州市 上城区
var str = '' //空字符串
values.forEach((item) => {
str += item + ' '
})
this.city = str
},
// 保存并提交收货地址
async preTion(){
new this.$Toast('正在提交中','loading').showloading() //反馈 弹窗
let obj = {
city:this.city,
address:this.address,
name:this.name,
mobile:this.mobile
}
try{
let sudeadd = await new this.Request(this.Urls.m().sudeaddurl,obj).modepost()
console.log(sudeadd,'提交保存的收货地址');
if(sudeadd.msg == 'SUCCESS') {
new this.$Toast('提交成功').showtoast() //反馈 弹窗
// 完成自动返回到上一级页面
setTimeout(()=>{
uni.navigateBack({
data: 1
})
},200)
}else {
new this.$Toast(sudeadd.msg,'none').showtoast() //反馈 弹窗
}
// 如果出现errcode 就表示没有登录
if(sudeadd.msg.errcode) {
// 就需要登录 使用ref获取登录组件的节点 然后调用节点下的控制展示组件的方法showing()
this.$refs.loginmen.showing()
uni.hideLoading();
}
}catch(e){
//TODO handle the exception
}
}
}
}
</script>
当新增保存地址后,完成自动返回到上一级页面
if(sudeadd.msg == 'SUCCESS') {
new this.$Toast('提交成功').showtoast() //反馈 弹窗
setTimeout(()=>{
uni.navigateBack({
data: 1
})
},200)
}else {
new this.$Toast(sudeadd.msg,'none').showtoast() //反馈 弹窗
}
上一级页面 my-address.vue 中每次进入页面要刷新一下 收货地址
methods:{
// 获取收货地址
async getadd(){
try{
let atcartdata = await new this.Request(this.Urls.m().gainaddurl).modeget()
console.log(atcartdata,'请求到的收货地址信息');
// 如果存在errcode值就表示没有token 没有登录
if(atcartdata.msg.errcode) {
// 就需要登录 使用ref获取登录组件的节点 然后调用节点下的控制展示组件的方法showing()
this.$refs.loginmen.showing()
// 如果data长度为0,就表示收货地址为空
}else if(atcartdata.data.length === 0) {
// 展示提示信息
this.searchno = true
}else {
// 登陆后拿到数据时赋值给一个数组中
this.adddata = atcartdata.data
this.searchno = false //同时隐藏提示信息
}
}catch(e){
//TODO handle the exception
}
},
// 点击新增收货地址按钮
chAnge(){
uni.navigateTo({
url:'./new-address'
})
}
},
// onShow生命周期 表示:无论从什么页面返回,都会再一次执行下面的方法
onShow(){
this.getadd()
},
实现收货地址的修改功能
点击修改图标,进入修改地址页面,可以编辑和删除
给点击事件传递两个值,第一个001代表修改收货地址,002代表新增收货地址,第二个参数item,包含了地址的详细信息,是放到修改点击事件里
<block v-for="(item,index) in adddata" :key="index">
<view class="my-address-view">
<view class="my-address-name" @click="getAdd(item)">
<text>{{item.city + item.address}}</text>
<view class="my-address-adding">
<text>{{item.name}}</text>
<text>{{item.mobile}}</text>
</view>
</view>
<!-- 传值 001表示修改 同时将item传递过去-->
<view class="my-address-change" @click="chAnge('001',item)">
<image src="/static/loading/genggai.svg" mode="widthFix"></image>
</view>
</view>
</block>
<!-- 新增收货地址 -->
<!-- 传值 002表示新增收货 -->
<view class="button-address" @click="chAnge('002')">
<image src="/static/loading/xinzeng.svg" mode="widthFix"></image>
<text>新增收货地址</text>
</view>
// 新增收货地址 同时也修改地址 传递参数判断 点击后跳转的页面,将参数转为json字符串添加到要跳转链接后边
chAnge(value = '002',data=[]){
let obj = { value,data }
let str = JSON.stringify(obj)
uni.navigateTo({
url:'./new-address?value=' + str
})
}
在new-address.vue中,接收数据 同时调用获取按钮的方法,接收到的数据传入useradd方法中 从而变更data中控制v-if的值,达到显示需要的按钮,
同时,将数据解构出来,赋值给data中数据,即页面显示的收货地址信息填充为用户之前输入的信息
<!-- 保存地址 -->
<view class="conServe adcolor" @click="preTion()" v-if="nameadd">保存地址</view>
<!-- 修改地址 -->
<view class="conServe adcolor" @click="preTion()" v-if="!nameadd">修改地址</view>
<!-- 删除地址 -->
<view class="conServe decolor" @click="preTion()" v-if="!nameadd">删除地址</view>
// 获取用户从那个按钮点击进来的
useradd(data){
// 修改地址或者删除地址
if(data.value == '001') {
this.nameadd = false
// 将接收的到数据解构出来存储
let { city,address,mobile,name,_id } = data.data
this.city = city
this.address = address
this.name = name
this.mobile = mobile
this.id = _id
}else {
this.nameadd = true
}
}
// e 里边包含了上个组件传递的数据
onLoad(e) {
console.log(e,'接收到上个组件的数据');
// JSON.parse(e.value) json字符串还原为js对象
this.useradd(JSON.parse(e.value))
}
更改删除收货地址
在new-address.vue中
<!-- 修改地址 -->
<view class="conServe adcolor" @click="modifyAdd()" v-if="!nameadd">修改地址</view>
<!-- 删除地址 -->
<view class="conServe decolor" @click="deleteAdd()" v-if="!nameadd">删除地址</view>
修改收货地址方法
// 修改收货地址
async modifyAdd(){
new this.$Toast('正在修改中','loading').showloading() //反馈 弹窗
let obj = {
id:this.id,
city:this.city,
address:this.address,
name:this.name,
mobile:this.mobile,
}
try{
let modifyadd = await new this.Request(this.Urls.m().modifyaddurl,obj).modepost()
console.log(modifyadd,'修改收货地址')
if(modifyadd.msg == 'SUCCESS') {
new this.$Toast('修改成功').showtoast() //反馈 弹窗
setTimeout(()=>{
uni.navigateBack({
data: 1
})
},200)
}else {
new this.$Toast(modifyadd.msg,'none').showtoast() //反馈 弹窗
}
// 如果出现errcode 就表示没有登录
if(modifyadd.msg.errcode) {
// 就需要登录 使用ref获取登录组件的节点 然后调用节点下的控制展示组件的方法showing()
this.$refs.loginmen.showing()
uni.hideLoading();
}
}catch(e){
//TODO handle the exception
}
},
删除收货地址方法
// 删除收货地址
async deleteAdd(){
new this.$Toast('删除中','loading').showloading() //反馈 弹窗
let id = this.id
try{
let deleadd = await new this.Request(this.Urls.m().deleaddurl + '?id=' + id).modeget()
// console.log(deleadd,'删除收货地址')
if(deleadd.msg == 'SUCCESS') {
new this.$Toast('删除成功').showtoast() //反馈 弹窗
setTimeout(()=>{
uni.navigateBack({
data: 1
})
},500)
}else {
new this.$Toast(deleadd.msg,'none').showtoast() //反馈 弹窗
}
// 如果出现errcode 就表示没有登录
if(deleadd.msg.errcode) {
// 就需要登录 使用ref获取登录组件的节点 然后调用节点下的控制展示组件的方法showing()
this.$refs.loginmen.showing()
uni.hideLoading();
}
}catch(e){
//TODO handle the exception
}
}
商品订单页选择收货地址
提交订单页面 –payment.vue 给 “请选择收货地址” 添加点击事件 @click=”shAddress()**,跳转到我的收货地址 – **my-address.vue组件中
<!-- 收货地址 -->
<view class="payment-name" @click="shAddress()">
<view class="payment-left-img">
<image src="/static/loading/address-shouhuo.svg" mode="widthFix"></image>
</view>
<!-- 收货地址 -->
<view class="payment-add" v-if="nameadd">
<text>{{orderdata.name}}</text>
<text>{{orderdata.mobile}}</text>
<text>{{orderdata.city + orderdata.address}}</text>
</view>
<view class="payment-add" v-if="!nameadd">请选择收货地址</view>
<view class="payment-right-img">
<image src="/static/loading/shouhuo-jiantou.svg" mode="widthFix"></image>
</view>
</view>
methods:{
// 选择收货地址
shAddress(){
//跳转到我的收货地址 -- myaddress.vue组件中
uni.navigateTo({
url:'../my-address/my-addres',
})
}
},
my-address.vue组件中,添加点击地址后的事件**@click=”getAdd(item)”**,同时拿到包含收货地址详细信息的item作为参数,
在方法中,调用 **this.$store.commit(‘mutaadd’,item)**,将item传入vuex的mutation中的mutaadd方法中,将item赋值给state中的addordr。
接着uni.navigateBack 返回上一级 payment.vue
<!-- 新增收货地址页面 -->
<block v-for="(item,index) in adddata" :key="index">
<view class="my-address-view">
<!-- 设置点击地址后的点击事件 -->
<view class="my-address-name" @click="getAdd(item)">
<text>{{item.city + item.address}}</text>
<view class="my-address-adding">
<text>{{item.name}}</text>
<text>{{item.mobile}}</text>
</view>
</view>
<!-- 传值 001表示修改 同时将item传递过去-->
<view class="my-address-change" @click="chAnge('001',item)">
<image src="/static/loading/genggai.svg" mode="widthFix"></image>
</view>
</view>
</block>
// 选中某个收货地址 携带数据返回上一级订单页面
getAdd(item){
// 使用vuex传值 将这item传值到state中
this.$store.commit('mutaadd',item)
uni.navigateBack({
data:1
})
},
payment.vue中设置监听器,监听state中addordr是否变化,将变化后的数据( 即收货地址信息)赋值给 this.orderdata,
最后计算属性中,nameadd()方法,判断 转化为JSON字符串的this.orderdata是否为空,空的话nameadd返回false,否则true。即控制显示的是否显示请选择收货地址还是已经选择的收货地址
watch:{
// 监听vuex的收货地址数据 newValue里边就包含了vuex里的数据
"$store.state.addordr"(newValue,oldValue) {
console.log(newValue,'监听到的新值');
this.orderdata = newValue.address
}
},
computed:{
// 是否选择了收货地址
nameadd(){
if(JSON.stringify(this.orderdata) === "{}") {
return false
}else {
return true
}
}
}
微信支付
获取支付所需数据,以及try{}catch{}的应用
payment.vue组件,点击提交订单按钮的点击事件
代码如下:
<!-- 底部提交订单 -->
<view class="place-order">
<text>合计: ¥{{Totalprice}}</text>
<text @click="placeOrder()">提交订单</text>
</view>
// 提交订单
async placeOrder(){
// 商品数据 通过对comminfo数据进行map方法遍历,会返回一个新的数组对象
let codata = this.comminfo.map(item => {
let data ={
id:item.id,
image:item.image,
title:item.title,
size:item.size,
color:item.color,
price:item.price,
many:item.many
}
return data
})
// 准备一个对象 将请求需要的参数放入
let dataobj = {
consignee:this.orderdata, // 收货地址相关数据
commodity:codata, // 商品相关数据
total_price:this.Totalprice, // 商品总价数据
idcard:this.idcard, // 购物车内所有商品的id相关数据
}
// 1.请求统一下单 接口
try{
let wxpayurl = await new this.Request(this.Urls.m().wxpay,dataobj).modepost();
console.log(wxpayurl);
if(wxpayurl.msg == "SUCCESS") {
}else {
// throw 用来捕获一个错误 如果try 中出现throw关键词,就会进入catch里面来
throw wxpayurl.msg
}
}catch(e){
// e就是throw 捕获的错误
new this.$Toast(e,'none').showtoast() //反馈 弹窗
throw e //如果try 中出现throw关键词,此时try catch 后边的代码将不会执行
}
// 2.调用微信支付接口
}
wx.requestpayment()调起微信付款
payment.vue组件中,点击提交订单后。
原本在请求接口时,是调用wxpay接口 – var wxpay = await new this.Request(this.Urls.m().wxpay,dataobj).modepost();
但因为个人没有微信商户号,所以这里使用的是虚拟支付接口fictpay –let fictpay = await new this.Request(this.Urls.m().fictpay,dataobj).modepost();
携带参数完整时, 返回结果是”SUCCESS”
methods中方法如下:
// 选择收货地址
shAddress(){
uni.navigateTo({
url:'../my-address/my-addres',
})
},
// 提交订单
async placeOrder(){
// 商品数据 通过对comminfo数据进行map方法遍历,会返回一个新的数组对象
let codata = this.comminfo.map(item=>{
let data ={
id:item.id,
image:item.image,
title:item.title,
size:item.size,
color:item.color,
price:item.price,
many:item.many,
}
return data
})
// 准备一个对象 将请求需要的参数放入
let dataobj = {
consignee:this.orderdata, // 收货地址相关数据
commodity:codata, // 商品相关数据
total_price:this.Totalprice, // 支付总价数据
idcard:this.idcard, // 购物车内所有商品的id相关数据
}
// 1.请求统一下单接口
try{
// 微信支付: 统一下单接口
// 因为个人没有认证微信商户号,这里使用的是虚拟支付接口 返回结果是"SUCCESS"
let fictpay = await new this.Request(this.Urls.m().fictpayurl,dataobj).modepost();
if(fictpay.msg == "SUCCESS") {
new this.$Toast(fictpay.data,'none').showtoast() //反馈 弹窗
}else {
// throw 用来捕获一个错误 如果try 中出现throw关键词,就会进入catch里面来
throw fictpay.msg
}
}catch(e){
// e就是throw 捕获的错误
new this.$Toast(e,'none').showtoast() //反馈 弹窗
// throw e //如果try 中出现throw关键词,此时try catch 后边的代码将不会执行
}
// 2.调用微信支付接口
// 3.查询订单是否支付成功
},
当拥有微信商户认证时候,发起真正的请求时,
使用 wxpay接口 – var wxpay = await new this.Request(this.Urls.m().wxpay,dataobj).modepost();
methods中方法如下:
methods:{
// 选择收货地址
shAddress(){
uni.navigateTo({
url:'../my-address/my-addres',
})
},
// 提交订单
async placeOrder(){
// 商品数据 通过对comminfo数据进行map方法遍历,会返回一个新的数组对象
let codata = this.comminfo.map(item=>{
let data ={
id:item.id,
image:item.image,
title:item.title,
size:item.size,
color:item.color,
price:item.price,
many:item.many,
}
return data
})
// 准备一个对象 将请求需要的参数放入
let dataobj = {
consignee:this.orderdata, // 收货地址相关数据
commodity:codata, // 商品相关数据
total_price:this.Totalprice, // 支付总价数据
idcard:this.idcard, // 购物车内所有商品的id相关数据
}
// 1.请求统一下单接口
try{
// 微信支付: 统一下单接口
var wxpay = await new this.Request(this.Urls.m().wxpay,dataobj).modepost();
if(wxpay.msg == "SUCCESS") {
// 存储商户订单号和订单id
this.outno = wxpayurl.data.out_trade_no
this.ide = wxpayurl.data.id
new this.$Toast(wxpay.data,'none').showtoast() //反馈 弹窗
}else {
// throw 用来捕获一个错误 如果try 中出现throw关键词,就会进入catch里面来
throw wxpay.msg
}
}catch(e){
// e就是throw 捕获的错误
new this.$Toast(e,'none').showtoast() //反馈 弹窗
throw e //如果try 中出现throw关键词,此时try catch 后边的代码将不会执行即不会调用下边的微信接口
}
// 2.调用微信支付接口
try{
// package:属于源码里的关键词 因此这个参数不能解构使用 而是:package:wxpay.data.package
// 从真正的微信支付接口中拿到返回结果,这4(5)个数据用于调用微信支付接口时作为参数,
let { nonceStr,paySign,signType,timeStamp } = wxpay.data
// 使用下方调用支付的方法 wxPay(payment)
let wxpay = await this.wxPay({ nonceStr,paySign,signType,timeStamp,package:wxpay.data.package })
console.log(wxpay)
}catch(e){
new this.$Toast('支付失败','none').showtoast() //反馈 弹窗
throw e
}
},
// 调用支付的方法:包装一个promise
// payment就是接收到的5个数据,用于调用微信支付接口的参数
wxPay(payment){
return new Promise((resolve,reject) => {
wx.requestpayment({
...payment,
success:res=>{
resolve(res)
},
fail:Error=>{
reject(Error)
}
})
})
}
}
查询订单是否支付成功
// 3.查询订单是否支付成功 (去微信那边查询是否成功)
try{
let dataobj = {outno:this.outno,id:this.ide}
let queryorderurl = await new this.Request(this.Urls.m().queryorderurl,dataobj).modepost()
if(queryorderurl.data.msg == 'SUCCESS') {
new this.$Toast('支付成功').showtoast() //反馈 弹窗
}else {
throw '支付失败'
}
}catch(e){
new this.$Toast(e,'none').showtoast() //反馈 弹窗
}
动态路由跳转详情页
在pages.json文件中,将首页排到首位
完成轮播页,点击图片跳转到对应页面功能
在pages/index/sonzhujian/swipers.vue下,添加点击事件 deTails(item._id)
deTails( item._id )这里的 _id 就是通过轮播接口,返回的成功数据中的_id ,表示商品的唯一标识
<swiper-item>
//这里的 _id 就是通过轮播接口,返回的成功数据中的_id表示商品的唯一标识
<view class="swiper-item" @click="deTails(item._id)">
<!-- mode 图片裁剪、缩放的模式 -->
<img :src=item.image mode="aspectFill">
</view>
</swiper-item>
// 跳转到详情页
deTails(id) {
uni.navigateTo({
// 跳转到详情页组件,同时携带商品id参数
url:'../details/details?id=' + id
})
}
- 完成快抢购页,点击图片跳转到对应页面功能
- 在pages/index/sonzhujian/purchhase.vue下,添加点击事件 deTails(item._id)
<view class="pur-img">
<block v-for="(item,index) in recomdata[0].image" :key="index">
<view>
<image :src="item.img" mode="widthFix" @click="daTails(item._id)"></image>
</view>
</block>
</view>
// 跳转到详情页
deTails(id) {
uni.navigateTo({
// 跳转到详情页组件,同时携带商品id参数
url:'../details/details?id=' + id
})
}
- 完成天猫榜单页,点击图片跳转到对应页面功能
- 在pages/index/sonzhujian/list.vue下,添加点击事件 deTails(item._id)
<block v-for="(item, index) in billdata" :key="index">
<view class="menb" @click="deTails(item._id)">
<image :src="item.image" mode="aspectFill"></image>
<text>{{ item.title }}</text>
<text>{{ item.want }}人想要</text>
</view>
</block>
// 跳转到详情页
deTails(id) {
uni.navigateTo({
// 跳转到详情页组件,同时携带商品id参数
url:'../details/details?id=' + id
})
}
- 完成卡片流页,点击图片跳转到对应页面功能
- 在pages/commponents/card.vue下,添加点击事件 deTails(item._id)
<block v-for="(item,index) in commdata" :key="index">
<view class="comm-card" @click="deTails(item._id)">
<view class="comm-img"><image :src="item.image" mode="aspectFill" /></view>
<view class="comm-title"><text>{{item.title}}</text></view>
<view class="comm-left">
<text>{{item.freight}}</text>
<text>预计{{item.Duration}}h发货</text>
</view>
<view class="comm-right">{{item.Price}}¥</view>
</view>
</block>
// 跳转到详情页
deTails(id) {
uni.navigateTo({
// 跳转到详情页组件,同时携带商品id参数
url:'../details/details?id=' + id
})
}
完成右上角按钮的返回功能
进入pages/details/details.vue文件
<!-- 返回按钮 -->
<view class="header-fixed backno" v-show="showAbs">
<!-- 箭头符号设置的动态的高 -->
<view class="status_bar" :style="'height: ' + tophight.top + 'px;'"></view>
<view class="navs-image" :style="'height: ' + tophight.height + 'px;'" @click="pageRe()" >
<image src="/static/details/fanhuibai.jpg" mode="widthFix"></image>
</view>
</view>
// 返回上级页面 按钮
pageRe(){
uni.navigateBack({
data:1
})
}
同时给top.vue组件中的左上角按钮 也设置点击事件
<view class="navs-image" :style=" 'height:'+ tophight.height + 'px;'" @click="pageRe()" >
<image src="/static/details/fanhuihei.png" mode="widthFix"></image>
</view>
// 返回上级页面 按钮
pageRe(){
uni.navigateBack({
data:1
})
}
个人中心页面
静态布局
<template>
<view class="my-view">
<view class="my-view-user">
<view>
<image src="/static/details/weidenglu.svg" mode="widthFix"></image>
</view>
<view class="my-view-name">
<button plain="true" open-type="getUserInfo" @click="getUserInfo()">点击登录</button>
</view>
</view>
<!-- 菜单 -->
<view class="my-menu">
<block v-for="(item,index) in datas" :key="index">
<view >
<image :src="item.img" mode="widthFix"></image>
<text>{{item.name}}</text>
</view>
</block>
</view>
</view>
</template>
<script>
export default {
data(){
return {
datas:[
{
'img':'/static/details/daifukuan.svg',
'name':'待付款'
},
{
'img':'/static/details/daifahuo.svg',
'name':'待发货'
},
{
'img':'/static/details/daishouhuo.svg',
'name':'待收货'
},
{
'img':'/static/details/daipingjia.svg',
'name':'待评价'
}
],
usering:true,
userdata:{}
}
}
}
</script>
<style scoped>
.my-view{
background: linear-gradient(to top, #ff0714 10%, #ff3388 100%);
height: 500upx;
border-bottom-left-radius: 50upx;
border-bottom-right-radius: 50upx;
position: relative;
}
.my-view-user{height: 100upx;
display: flex;
align-items: center;
position: absolute;
left: 50upx;
top: 170upx;
/* width: 100%; */
color: #FFFFFF;
}
.my-view-user text{display: block;}
.my-view-user image{width: 100upx; height: 100upx; border-radius: 50%;}
.my-view-user view:nth-child(1){width: 100upx; height: 100upx;}
.my-view-name text{font-size: 32upx;}
.my-view-name button{border: none;font-size: 32upx;
color: #FFFFFF;
padding: 0 !important;
}
.my-view-name{padding-left: 20upx;}
.my-menu{height: 200upx; background: #FFFFFF;
position: absolute;
top: 350upx;
left: 35upx;
right: 35upx;
border-radius: 10upx;
/* margin: 0 35upx; */
box-shadow: 0upx 10rpx 10rpx #e9e9e9;
display: flex;
align-items: center;
justify-content: space-around;
font-size: 28upx;
}
.my-menu image{width: 45upx; height: 45upx;
display: block;
align-self: center;
padding-bottom: 10upx;
}
.my-menu view{
display: flex;
flex-direction: column;
}
</style>
当本地存在token时候,显示个人信息:
使用v-if 和 v-else来判断头像和个人名称的显示
取出本地token,如果取不到就显示请登录,取到数据吧表示已经登录,则显示个人头像和名称
<view class="my-view-user">
<view>
<image v-if="usering" src="/static/details/weidenglu.svg" mode="widthFix"></image>
<image v-else :src="userdata.avatarUrl" mode="widthFix"></image>
</view>
<view class="my-view-name">
<button v-if="usering" plain="true" open-type="getUserInfo" @click="getUserInfo()">点击登录</button>
<text v-else >{{userdata.nickName}}</text>
</view>
</view>
methods:{
// 取出本地
ifUser(){
let user = uni.getStorageSync('wxuser')
console.log(user);
if(!user){
this.usering = true
}else {
this.usering = false
this.userdata = user
}
}
},
onShow() {
this.ifUser()
}
当本地没有token时,点击按钮登录事件
<button v-if="usering" plain="true" open-type="getUserInfo" @click="getUserInfo()">点击登录</button>
// 引入 微信登录逻辑
import wxLogin from '../../login/login.js'
// 触发登录事件
getUserInfo(){
// 显示其他的弹窗 登录时的反馈 弹窗
new this.$Toast('登录中').showloading()
/*wx.getUserProfile 获取用户信息。页面产生点击事件
后才可调用,每次请求都会弹出授权窗口,用户同意后返回 userInfo
*/
//新的登录接口:wx.getUserProfile
wx.getUserProfile({
desc: '登录咯'
})
.then(res=>{
let {userInfo,errMsg} = res // res中包含用户的个人信息 如昵称,头像等
this.wxusEr(userInfo,errMsg)
})
.catch(err=>{
console.log('拒绝登录或登录失败')
new this.$Toast('用户取消登录','none').showtoast() //登录时的反馈 弹窗
})
},
// 调用登录
async wxusEr(userInfo,errMsg){
try{
// 微信登录逻辑
let data = await new wxLogin(userInfo,errMsg).loGin()
this.ifUser() //登录成功后刷新一下本组件
}catch(e){
//TODO handle the exception
}
}
订单中心页面
需求:点击待付款或者待发货应该跳转到订单中心
在pages/新建 personal文件夹/personal.vue
静态布局
<template>
<view>
<view class="choice-tips">
<block v-for="(item, index) in datas" :key="index">
<!-- 如果下标index 等于 num值,就让当前下标对应的标签拥有 activetext css属性 -->
<text :class="{ activetext: index == num }" @click="menubtn(index)">{{ item }}</text>
</block>
</view>
<!-- <view class="choice-content">
<Tobepaid v-if="num == 0 ? true : false"></Tobepaid>
<Deliverde v-if="num == 1 ? true : false"></Deliverde>
<Received v-if="num == 2 ? true : false"></Received>
<Evaluated v-if="num == 3 ? true : false"></Evaluated>
</view> -->
</view>
</template>
<script>
export default {
data() {
return {
num: 0,
datas: ['待付款', '待发货', '待收货', '待评价']
};
},
methods: {
menubtn(index) {
this.num = index;
}
},
onLoad(e) {
this.num = e.index;
}
};
</script>
<style>
page {
background: #eeeeee;
}
.choice-tips {
background: #d9ffe7;
display: flex;
justify-content: space-around;
color: #6d6d6d;
font-size: 28upx;
position: fixed;
top: 0;
left: 0;
right: 0;
}
.choice-tips text {
height: 70upx;
line-height: 70upx;
}
.activetext {
color: #fe0036 !important;
border-bottom: 4upx solid #fe0036;
}
.choice-content {
margin-top: 100upx;
}
</style>
针对订单中心的四个板块,分别创建子组件
在pages/personal/创建 sonzujian文件夹,
在sonzujian文件夹下新建 待付款组件 tobepaid.vue、待发货组件 delivered.vue、待收货组件 received.vue、待评价组件 evaluated.vue
通过 三元表达式,对num值是否等于index值作判断,从而显示或隐藏子组件
<!-- 展示子组件 -->
<view class="choice-content">
<!-- 显示的条件是 num的值是否和当前的index下标对上 -->
<Tobepaid v-if="num == 0 ? true : false"></Tobepaid>
<Delivered v-if="num == 1 ? true : false"></Delivered>
<Received v-if="num == 2 ? true : false"></Received>
<Evaluated v-if="num == 3 ? true : false"></Evaluated>
</view>
// 引入子组件
import tobepaid from './sonzujian/tobepaid.vue'
import delivered from './sonzujian/delivered.vue'
import received from './sonzujian/received.vue'
import evaluated from './sonzujian/evaluated.vue'
获取待付款订单
tobepaid 静态布局代码:
css样式 引入的是style里的公共样式
没有商户号是无法拿到待付款数据
<template>
<!-- 待付款组件 -->
<view class="payment-view">
<!-- 商品详情 -->
<view class="payment-commodity">
<text class="order-tips" v-if="item.expire">该订单已过期</text>
<text class="order-tips" v-else>等待买家付款</text>
<view class="payment-order">
<view class="payment-order-img">
<image src="iteming.image" mode="aspectFill">图片1</image>
</view>
<view class="payment-title">
<text>标题</text>
<text class="text-you">颜色:黑色;尺码:L</text>
</view>
<view class="payment-flex">
<view class="payment-price">
<text>¥0.6</text>
<text class="text-you">x2</text>
</view>
</view>
</view>
<!-- 价格 -->
<view class="presonal-Price">需付款¥1.2</view>
<!-- 付款 -->
<view class="presonal-payment">
<text>付款</text>
</view>
</view>
<!-- 登录界面弹窗 -->
<loginpage ref="loginmen"></loginpage>
</view>
</template>
<script>
// 引入公共样式
import '../../../style/order.css'
export default {
data(){
return{
}
},
methods:{
// 获取待付款数据
async Tobepaid() {
try{
let topebaid = await new this.Request(this.Urls.m().topebaidurl).modeget();
// 如果没有添加购物数据
if(topebaid == 'Not Found') {
console.log('没有商户号,不会有待付款数据');
}
}catch(e){
}
},
},
created() {
this.Tobepaid()
}
}
</script>
<style>
</style>
假如有商户号代码如下
(使用一些假数组数据)
<template>
<!-- 待付款组件 -->
<view class="payment-view">
<!-- 商品详情 -->
<!-- 遍历商品条数和数据 -->
<block v-for="(item,index) in topebaid" :key="index">
<view class="payment-commodity">
<!-- 两种情况:1、订单没有过期时候,俩小时内发起付款没有支付,俩小时内可以再次付款,2、俩小时之后就不能再付款了,商品显示该订单已过期 -->
<text class="order-tips" v-if="item.expire">该订单已过期</text>
<text class="order-tips" v-else>等待买家付款</text>
<!-- 遍历商品里面 因为可能支付时候不止一个商品 -->
<block v-for="(iteming,indexs) in item.order" :key="indexs">
<view class="payment-order">
<view class="payment-order-img">
<image :src="iteming.image" mode="aspectFill"></image>
</view>
<view class="payment-title">
<text>{{iteming.title}}</text>
<text class="text-you">颜色:{{iteming.color}};尺码:{{iteming.size}}</text>
</view>
<view class="payment-flex">
<view class="payment-price">
<text>¥{{iteming.price}}</text>
<text class="text-you">x{{iteming.many}}</text>
</view>
</view>
</view>
</block>
<!-- 价格 -->
<view class="presonal-Price">需付款¥{{item.total_price}}</view>
<!-- 付款 -->
<view class="presonal-payment">
<!-- 如果过期,就显示删除订单 否则可以付款-->
<text v-if="item.expire">删除订单</text>
<text v-else>付款</text>
</view>
</view>
</block>
<!-- 没有订单数据 -->
<!-- <ordering ref="orderload"></ordering> -->
<!-- 登录界面弹窗 -->
<loginpage ref="loginmen"></loginpage>
</view>
</template>
<script>
// 引入公共样式
import '../../../style/order.css'
export default {
data(){
return{
topebaid:[
{
expire:false,
total_price:169,
order:
[
{
color:'5526网面-天空蓝',
image:"http://h.thexxdd.cn/tianmao/public%5Cuploads%5C1606059327494-4142549.jpg",
many:1,
price:169,
size:"37",
title:"耐克新款2022 女生白色皮面鞋子高级【店长推荐】"
},
]
},
{
expire:true,
total_price:200,
order:
[
{
color:'5526网面-渣渣灰',
image:"http://h.thexxdd.cn/tianmao/public%5Cuploads%5C1606059327494-4142549.jpg",
many:1,
price:200,
size:"41",
title:"361新款2022 男生灰色皮面鞋子【店长推荐】"
},
]
},
],
}
},
methods:{
// 获取待付款数据
async Tobepaid() {
try{
let topebaid = await new this.Request(this.Urls.m().topebaidurl).modeget();
// 如果没有添加购物数据
if(topebaid == 'Not Found') {
console.log('没有商户号,不会有待付款数据');
}
// 如果未登录 调用登录弹窗
if(topebaid.msg.errcode) {
this.$refs.loginmen.showing()
// 如果登录成功后
}else if(topebaid.msg == 'SUCCESS') {
// 没有待付款数据
if(topebaid.data.length == 0) {
}else {
this.topebaid = topebaid.data
}
}
}catch(e){
}
},
},
created() {
this.Tobepaid()
},
// 接收登录组件传递的值 登录成功后也要刷新数据
mounted() {
this.$bus.$on('mycart',res=>{
// 当登录成功后
if(res.cart == 'SUCCESS'){
// 调用方法刷新数据
this.Tobepaid()
}
})
}
}
</script>
<style>
</style>
订单的详情页
准备一个没有订单时候,页面的展示效果提示
pages/commponents **下新建 **ordering.vue,
静态布局
<template>
<!-- 没有订单数据 -->
<view class="empty-cart" v-if="emcart">
<image src="http://h.thexxdd.cn/video/tianmao/noorder.png" mode="aspectFit"></image>
<text>没有相关订单数据</text>
</view>
</template>
<script>
export default{
data() {
return {
emcart: false
}
},
methods:{
init(od=true){
this.emcart = od
}
}
}
</script>
<style>
</style>
在 tobepaid.vue组件中引入,
<!-- 没有订单数据 -->
<ordering ref="orderload"></ordering>
接着开始新建目录 放订单详情,因为详情是对于四个板块都类似布局所以抽出到单独文件夹
pages下新建 order-details文件夹,其下新建order.vue文件,记得去pages.json中注册
静态布局
<template>
<!-- 订单详情 -->
<view>
<view class="order-details-view">
<view>等待买家付款</view>
<image src="http://h.thexxdd.cn/video/tianmao/001.webp" mode="aspectFill"></image>
</view>
<!-- 地址 -->
<view class="order-details-address">
<view>
<image src="/static/loading/address-shouhuo.svg" mode="aspectFit"></image>
</view>
<view class="order-details-name">
<text>马云 10086</text>
<text>四川省成都市 阿里巴巴</text>
</view>
</view>
<!-- 商品详情 -->
<view class="payment-commodity pay-border">
<text class="order-tips tips-import"></text>
<view class="payment-order">
<view class="payment-order-img">
<image src="http://h.thexxdd.cn/tianmao/public%5Cuploads%5C1606059327494-4142549.jpg" mode="aspectFill"></image>
</view>
<view class="payment-title">
<text>商品标题</text>
<text class="text-you">颜色:黑色;尺码:</text>
</view>
<view class="payment-flex">
<view class="payment-price">
<text>¥ 100</text>
<text class="text-you">x1</text>
</view>
</view>
</view>
<!-- 价格 -->
<view class="presonal-Price">需付款¥100</view>
</view>
<!-- 订单编号 -->
<view class="order-number">
<text>订单编号: 123456</text>
<text>创建时间: 2022-07-16</text>
</view>
<!-- 按钮 -->
<view class="order-details-play" >
<text>右下角按钮</text>
</view>
</view>
</template>
<script>
import '../../style/order.css'
import '../../style/order-details.css'
export default{
data() {
return {
values:{},
orderdata:[]
}
},
}
</script>
然后回到订单中心 personal.vue
设置点击商品跳转到商品详情页,如果订单过期无法点击!
<!-- 不存在订单过期的数据才能点击 -->
<view class="payment-order" @click="!item.expire && payDetail()">
// 跳转到商品详情页
payDetail(){
uni.navigateTo({
url:'../order-details/order'
})
}
因为待付款、待发货、待收货三个板块的详情页也有不同之处,所以需要一些数据进行动态传值
// 跳转到商品详情页
payDetail(id){
// 准备一个对象传入 ,不能直接从路径携带过去
let tip = {
tips:'等待买家付款',
sum:'需付款',
show:true, //是否展示付款按钮
text:'付款',
id:id
}
// 将对象数据转化成json字符串 然后传入到跳转的组件里
let value = JSON.stringify(tip)
uni.navigateTo({
url:'../order-details/order?value=' + value
})
}
商品详情组件中,order.vue 在onload接收数据
// e 接收tobepaid.vue组件 跳转过来时携带的数据
onLoad(e) {
// 将数据转化为js对象
let value = JSON.parse(e.value)
console.log(value);
this.values = value //接收值然后遍历
}
tobepaid.vue 组件 完整代码如下:
因为是没有商户号的购买数据,无法拿到相关数据
因此自己创建一个假数组对象当作拿到的tobedetail接口数据
然后将数据进行几次遍历到相应位置上
<template>
<!-- 订单详情 -->
<view>
<view class="order-details-view">
<view>{{values.tips}}</view>
<image src="http://h.thexxdd.cn/video/tianmao/001.webp" mode="aspectFill"></image>
</view>
<!-- 地址 -->
<view class="order-details-address">
<view>
<image src="/static/loading/address-shouhuo.svg" mode="aspectFit"></image>
</view>
<view class="order-details-name">
<block v-for="(item,index) in orderdata" :key="index">
<text>{{item.consignee.name}} {{item.consignee.mobile}}</text>
<text>{{item.consignee.city}} {{item.consignee.address}}</text>
</block>
</view>
</view>
<!-- 商品详情 -->
<block v-for="(items,indexs) in orderdata" :key="indexs">
<view class="payment-commodity pay-border">
<text class="order-tips tips-import"></text>
<block v-for="(itemimg,indexss) in items.order" :key="indexss">
<!-- 商品详情 -->
<view class="payment-order">
<view class="payment-order-img">
<image :src="itemimg.image" mode="aspectFill"></image>
</view>
<view class="payment-title">
<text>{{itemimg.title}}</text>
<text class="text-you">颜色:{{itemimg.color}};尺码:{{itemimg.size}}</text>
</view>
<view class="payment-flex">
<view class="payment-price">
<text>¥{{itemimg.price}}</text>
<text class="text-you">x{{itemimg.many}}</text>
</view>
</view>
</view>
</block>
<!-- 价格 -->
<view class="presonal-Price">{{values.sum}}¥{{items.total_price}}</view>
</view>
</block>
<!-- 订单编号 -->
<view class="order-number">
<block v-for="(item,index) in orderdata" :key="index">
<text>订单编号: {{item.order_number}}</text>
<text>创建时间: {{item.time}}</text>
</block>
</view>
<!-- 按钮 -->
<view class="order-details-play" v-if="values.sum">
<text>{{values.text}}</text>
</view>
</view>
</template>
<script>
import '../../style/order.css'
import '../../style/order-details.css'
export default{
data() {
return {
// 准备空对象,接收tobepaid.vue组件 跳转过来时携带的数据
values:{},
orderdata:[]
}
},
methods:{
// 请求商品数据的方法
async tobedetail(id){
try{
let tobedetail = await new this.Request(this.Urls.m().tobedetail + '?id=' + id).modeget();
// 此处因为是没有商户号的购买数据,无法拿到相关数据
// 因此自己创建一个假数组对象当作拿到的tobedetail数据
let tobedetails = [
{
consignee:{
address:"阿里巴巴",
city:"四川省 成都市 高新区",
mobile:"15858588888",
name:"马云",
},
nonceStr:"yybvvcyP6CDZ08E4",
order:[
{
color:'5526网面-天空蓝',
image:"http://h.thexxdd.cn/tianmao/public%5Cuploads%5C1606059327494-4142549.jpg",
many:1,
price:169,
size:"37",
title:"耐克新款2022 女生白色皮面鞋子高级【店长推荐】"
},
],
order_number:"1607585433768778712",
out_trade_no:"2zmCzfDZe6Zr8rKYCz43AbXEe2Kk2Pcs",
package:"prepay_id=wx101530337150731019fc4f4e85085f0000",
paySign:"C0E5435D1E89E2562048740360E37201",
signType:"MD5",
time:"2022-07-15 16:46:00",
timeStamp:"1657874760",
total_price:169,
_id:"5fd1ce998c16885a1972d20b",
}
]
tobedetail.data = tobedetails
this.orderdata = tobedetail.data
}catch(e){
//TODO handle the exception
}
}
},
// e 接收tobepaid.vue组件 跳转过来时携带的数据
onLoad(e) {
// 将数据转化为js对象
let value = JSON.parse(e.value)
console.log(value);
this.values = value
// 调用请求方法
this.tobedetail(value.id)
}
}
</script>
待付款再次付款(上)
在public文件夹中新建 payment.js
封装一个类,用于待付款的付款请求
// 用于待付款的付款请求
import request from '../api/api.js'
import urls from '../api/request.js'
// 待付款
class Payment {
constructor(payment) {
this.payment = payment
}
// 1、拉起付款,
paySuccess(){
try{
await this.wxPay()
}catch(e){
throw e
}
// 2、查询订单
try{
let queryorder = await new request(urls.m().queryorderurl,{}).modepost();
return queryorder
}catch(e){
throw '支付失败'
}
}
// 调用微信支付
wxPay(){
return new Promise((resolve,reject)={
wx.requestPayment({
timeStamp: '',
nonceStr: '',
package: '',
signType: 'MD5',
paySign: '',
success: res=> {
resolve(res)
},
fail: Error=> {
reject(Error)
}
})
})
}
}
module.exports = {Payment}
在待付款组件中添加点击事件,
<text v-else @click="payMent()">付款</text>
待付款再次付款(下)
给方法传值(对象)
<text v-else @click="payMent({_id:item.id,})">付款</text>
// 在付款
payMent(obj){
console.log(obj);
}
引入付款类
// 引入付款类
const {Payment} = require('../../../public/payment.js')
// 在付款
payMent(obj){
new Payment(obj).paySucc()
.then(res=>{
console.log(res);
})
.catch(err=>{
console.log(err);
})
}
然后去方法类中拿到obj对象参数后,代码如下
// 用于待付款的付款请求
import request from '../api/api.js'
import urls from '../api/request.js'
// 待付款
class Payment {
constructor(payment) {
this.payment = payment
}
// 1、拉起付款,
async paySuccess(){
try{
await this.wxPay()
}catch(e){
throw e
}
// 2、查询订单
try{
let queryorder = await new request(urls.m().queryorderurl,{id:this.payment._id,outno:this.payment.out_trade_no}).modepost();
return queryorder
}catch(e){
throw '支付失败'
}
}
// 调用微信支付
wxPay(){
return new Promise((resolve,reject)=>{
wx.requestPayment({
timeStamp: 'this.payment.timeStamp',
nonceStr: 'this.payment.nonceStr',
package: 'this.payment.package',
signType: 'this.payment.signType',
paySign: 'this.payment.paySign',
success: res=> {
resolve(res)
},
fail: Error=> {
reject(Error)
}
})
})
}
}
module.exports = {Payment}
此时点击tobepaid组件的付款将会弹出二维码,但是由于我这是假数据,仅仅返回一个无权限的数据。
下一步做在订单详情页的付款按钮的功能
在order.vue组件中,添加点击事件
<!-- 按钮 -->
<view class="order-details-play" v-if="values.sum" >
<text @click="conFirm()">{{values.text}}</text>
</view>
// 右下角按钮(付款、确认收货、评价)
conFirm(text){
if(text == '付款') {
new this.$Toast('正在下单中','none').showloading() //手动 弹窗
new Payment(this.orderdata[0]).paySuccess()
.then(res=>{
console.log(res);
new this.$Toast('支付成功','none').showtoast() //反馈 弹窗
// 支付成功跳转
uni.redirectTo({
url:'../personal/personal'
})
})
.catch(err=>{
console.log(err);
setTimeout(()=>{
new this.$Toast('支付失败(无商品号权限)','none').showtoast() //反馈 弹窗
},1000)
})
}else if(text == '确认收货'){
}else if(text == '评价') {
}
}
订单详情页
删除订单
在待付款组件tobepaid.vue中,有的商品过期,需要删除
准备删除接口
// 删除订单
let deleorder = `${url}deleorder`
给删除按钮添加点击事件 deteOrder(_id)
<!-- 付款 -->
<view class="presonal-payment">
<!-- 如果过期,就显示删除订单 否则可以付款-->
<text v-if="item.expire" @click="deteOrder(item._id)">删除订单</text>
<text v-else @click="payMent({
_id:item._id,
timeStamp:item.timeStamp,
nonceStr:item.nonceStr,
package:item.package,
signType:item.signType,
paySign:item.paySign,
out_trade_no:item.out_trade_no,
})">付款</text>
</view>
// 删除订单
async deteOrder(id){
try{
let deleorder = await new this.Request(this.Urls.m().deleorder + '?orderid' + id).modeget();
console.log(deleorder,'删除订单');
if(deleorder.msg == 'SUCCESS') {
this.Tobepaid() //刷新页面
new this.$Toast('删除成功','none').showtoast() //反馈 弹窗
}else if(deleorder.msg == "参数填写错误") {
new this.$Toast('删除失败(无商品号权限)','none').showtoast() //反馈 弹窗
}
}catch(e){
//TODO handle the exception
}
}
待发货和待收货
**delivered.vue待发货 **
组件中, 代码如下:
<template>
<!-- 待发货组件 -->
<view class="payment-view">
<!-- 商品详情 -->
<block v-for="(item,index) in tbdelivered" :key="index">
<view class="payment-commodity" >
<text class="order-tips">买家已付款</text>
<block v-for="(items,indexs) in item.order" :key="indexs">
<view class="payment-order" @click="payDetail(item._id)">
<!-- 图片 -->
<view class="payment-order-img">
<image :src="items.image" mode="aspectFill"></image>
</view>
<view class="payment-title">
<text>{{items.title}}</text>
<text class="text-you">颜色:{{items.color}};尺码:{{items.size}}</text>
</view>
<!-- 单价、数量 -->
<view class="payment-flex">
<view class="payment-price">
<text>¥ {{items.price}}</text>
<text class="text-you">x{{items.many}}</text>
</view>
</view>
</view>
<!-- 价格 -->
<view class="presonal-Price">实付款¥{{item.total_price}}</view>
</block>
</view>
</block>
<!-- 登录界面 -->
<loginpage ref="loginmen"></loginpage>
<!-- 没有相关订单 -->
<ordering ref="orderload"></ordering>
</view>
</template>
<script>
import '../../../style/order.css'
export default{
data() {
return {
tbdelivered:[],
}
},
methods:{
async Tobepaid(){
try{
let tbdelivered = await new this.Request(this.Urls.m().tbdelivered).modeget();
console.log(tbdelivered,'获取待发货数据');
// 如果未登录 调用登录弹窗
if(tbdelivered.msg.errcode) {
this.$refs.loginmen.showing()
// 如果登录成功后
}else if(tbdelivered.msg == 'SUCCESS') {
// 没有待付款数据
if(tbdelivered.data.length == 0) {
// 就提示没有数据
this.$refs.orderload.init()
}else {
this.tbdelivered = tbdelivered.data
}
}
}catch(e){
}
},
// 跳转到商品详情页
payDetail(id){
// 准备一个对象传入 ,不能直接从路径携带过去
let tip = {
tips:'买家已经付款',
sum:'实付款',
show:false, //是否展示付款按钮
text:'付款',
id:id
}
// 将对象数据转化成json字符串 然后传入到跳转的组件里
let value = JSON.stringify(tip)
uni.navigateTo({
url:'../order-details/order?value=' + value
})
},
},
created() {
this.Tobepaid()
},
// 登录成功后也要刷新数据 接收登录组件传递的值
mounted() {
this.$bus.$on('mycart',res=>{
// 当登录成功后
if(res.cart == 'SUCCESS'){
// 调用方法刷新数据
this.Tobepaid()
}
})
}
}
</script>
**received.vue **待收货
组件中, 代码如下:
ps: 由于待收货接口拿不到数据,这里使用的是待发货的接口
<template>
<!-- 待收货组件 -->
<view class="payment-view">
<!-- 商品详情 -->
<block v-for="(item,index) in gtbreceived" :key='index'>
<view class="payment-commodity">
<text class="order-tips">卖家已发货</text>
<block v-for="(iteming, indexs) in item.order" :key='indexs'>
<view class="payment-order" @click="payDeatil(item._id)">
<view class="payment-order-img">
<image :src="iteming.image" mode="aspectFill"></image>
</view>
<view class="payment-title">
<text>{{iteming.title}}</text>
<text class="text-you">颜色:{{iteming.color}};尺码:{{iteming.size}}</text>
</view>
<view class="payment-flex">
<view class="payment-price">
<text>¥ {{iteming.price}}</text>
<text class="text-you">x{{iteming.many}}</text>
</view>
</view>
</view>
</block>
<!-- 价格 -->
<view class="presonal-Price">实付款¥{{item.total_price}}</view>
<!-- 付款 -->
<view class="presonal-payment">
<text @click="conRece(item._id)">确认收货</text>
</view>
</view>
</block>
<!-- 登录界面 -->
<loginpage ref="loginmen"></loginpage>
<!-- 没有相关订单 -->
<ordering ref="orderload"></ordering>
</view>
</template>
<script>
import '../../../style/order.css'
export default{
data() {
return {
gtbreceived:[],
}
},
methods:{
// 获取待收货数据
async Tobepaid(){
try{
// 由于待收货接口拿不到数据,这里使用的是待发货的接口
let data = await new this.Request(this.Urls.m().tbdelivered).modeget()
console.log(data,'获取待收货数据');
if(data.msg.errcode){
this.$refs.loginmen.showing()
}else if(data.msg == 'SUCCESS'){
if(data.data.length == 0){
this.gtbreceived = [] // 当数据为0 时候,要清空页面数据
this.$refs.orderload.init()
}else{
this.gtbreceived = data.data
}
}
}catch(e){
//TODO handle the exception
}
},
// 订单详情页
payDeatil(id){
let tip = {tips:'卖家已发货',sum:'实付款',show:true,text:'确认收货',id}
let value = JSON.stringify(tip)
uni.navigateTo({
url:'../order-details/order?value=' + value
})
},
// 确认收货
async conRece(id){
try{
let conreceipt = await new this.Request(this.Urls.m().conreceipt + '?id=' + id).modeget()
if(conreceipt.msg == 'SUCCESS'){
// 刷新数据
this.Tobepaid()
new this.$Toast('确认收货成功').showtoast()
}
}catch(e){
//TODO handle the exception
}
}
},
created() {
this.Tobepaid()
},
// 小程序的生命周期在子组件不会执行
mounted() {
this.$bus.$on('mycart',res=>{
if(res.cart == 'SUCCESS'){
this.Tobepaid()
}
})
}
}
</script>
<style>
</style>
然后点击待收货进入商品详情组件order.vue,对右下角的确认收货作点击事件
是
// 右下角按钮(付款、确认收货、评价)
conFirm(text){
if(text == '付款') {
new this.$Toast('正在下单中','none').showloading() //手动 弹窗
new Payment(this.orderdata[0]).paySuccess()
.then(res=>{
console.log(res);
new this.$Toast('支付成功','none').showtoast() //反馈 弹窗
// 支付成功跳转
uni.redirectTo({
url:'../personal/personal'
})
})
.catch(err=>{
console.log(err);
setTimeout(()=>{
new this.$Toast('支付失败(无商品号权限)','none').showtoast() //反馈 弹窗
},1000)
})
}else if(text == '确认收货'){
this.conRece()
// 支付成功跳转
uni.redirectTo({
url:'../personal/personal'
})
}
},
// 确认收货
async conRece(){
try{
let conreceipt = await new this.Request(this.Urls.m().conreceipt + '?id=' + this.values.id).modeget()
if(conreceipt.msg == 'SUCCESS'){
// 刷新数据
new this.$Toast('确认收货成功').showtoast()
}
}catch(e){
//TODO handle the exception
}
}
待评价
使用假数据
<template>
<view class="payment-view">
<!-- 商品详情 -->
<block v-for="(item,index) in tbevaluated" :key='index'>
<view class="payment-commodity">
<text class="order-tips">交易完成</text>
<view class="payment-order" @click="payDeatil(item._id)">
<view class="payment-order-img">
<image :src="item.image" mode="aspectFill"></image>
</view>
<view class="payment-title">
<text>{{item.title}}</text>
<text class="text-you">颜色:{{item.color}};尺码:{{item.size}}</text>
</view>
<view class="payment-flex">
<view class="payment-price">
<text>¥ {{item.price}}</text>
<text class="text-you">x{{item.many}}</text>
</view>
</view>
</view>
<!-- 价格-->
<view class="presonal-Price">实付款¥{{parseFloat((item.price * item.many).toFixed(10))}}</view>
<!-- 去评价 -->
<view class="presonal-payment">
<text @click="conRece(item._id,item.id,item.size,item.color)">去评价</text>
</view>
</view>
</block>
<!-- 登录界面 -->
<loginpage ref="loginmen"></loginpage>
<!-- 没有相关订单 -->
<ordering ref="orderload"></ordering>
</view>
</template>
<script>
import '../../../style/order.css'
export default {
data(){
return {
// 假数据
tbevaluated:[
{
_id:"5fd1ce998c16885a1972d20b", //订单号
id:"5f8bbf2823954733542169a1",
color:'5526网面-天空蓝',
image:"http://h.thexxdd.cn/tianmao/public%5Cuploads%5C1606059327494-4142549.jpg",
many:1,
price:169,
size:"37",
title:"耐克新款2022 女生白色皮面鞋子高级【店长推荐】"
},
{
_id:"5fd1ce998c16885a1972d20b", //订单号
id:"5f8bbf2823954733542169a1",
color:'5526网面-渣渣灰',
image:"http://h.thexxdd.cn/tianmao/public%5Cuploads%5C1606059327494-4142549.jpg",
many:1,
price:200,
size:"41",
title:"361新款2022 男生灰色皮面鞋子【店长推荐】"
},
]
}
},
methods:{
// 获取待评价数据
async Tobepaid(){
try{
let data = await new this.Request(this.Urls.m().tbevaluated).modeget()
console.log(data,'获取待评价数据');
// 由于没有商户号,待评价接口拿不到数据,这里使用的是假数据当作接口返回的数据
if(data.msg.errcode){
// 如果没有登录 弹出登录框
this.$refs.loginmen.showing()
}else if(data.msg == 'SUCCESS'){
if(data.data.length == 0){
// this.tbevaluated = [] // 当数据为0 时候,要清空页面数据
// this.$refs.orderload.init() //展示没有数据的提示
console.log('没有商品号,使用假数据当作接口返回的数据');
}
}
}catch(e){
//TODO handle the exception
}
},
// 点击商品跳转到订单详情页
payDeatil(id){
let tip = {tips:'交易完成',sum:'实付款',show:true,text:'去评价',id,evt:'002'}
let value = JSON.stringify(tip)
uni.navigateTo({
url:'../order-details/order?value=' + value
})
},
},
created() {
this.Tobepaid()
},
// 小程序的生命周期在子组件不会执行
mounted() {
this.$bus.$on('mycart',res=>{
if(res.cart == 'SUCCESS'){
this.Tobepaid()
}
})
}
}
</script>
<style>
</style>
然后对点击商品跳转到详情组件的功能实现,通过在跳转的方法payDeatil(id)中,传递的参数多加一个evt,当evt为002,就代表是待评价的组件去往详情页,
然后详情组件中判断evt值进行相应操作
// 请求商品数据的方法
async tobedetail(id,evt='001'){
// 当第二个参数是 evt为001 的时候代表的是 待付款、待发货、待收货的订单详情
// 当第二个参数是 evt为002 的时候代表的是 待评价的订单详情
if(evt == '001') {
try{
let tobedetail = await new this.Request(this.Urls.m().tobedetail + '?id=' + id).modeget();
// 此处因为是没有商户号的购买数据,无法拿到相关数据
// 因此自己创建一个假数组对象当作拿到的tobedetail数据
console.log(tobedetail,'我是待付款、待发货、待收货的商品数据');
let tobedetails =
[
{
consignee:{
address:"阿里巴巴",
city:"四川省 成都市 高新区",
mobile:"15858588888",
name:"马云",
},
nonceStr:"yybvvcyP6CDZ08E4",
order:
[
{
color:'5526网面-天空蓝',
image:"http://h.thexxdd.cn/tianmao/public%5Cuploads%5C1606059327494-4142549.jpg",
many:1,
price:169,
size:"37",
title:"耐克新款2022 女生白色皮面鞋子高级【店长推荐】"
},
],
order_number:"1607585433768778712",
out_trade_no:"2zmCzfDZe6Zr8rKYCz43AbXEe2Kk2Pcs",
package:"prepay_id=wx101530337150731019fc4f4e85085f0000",
paySign:"C0E5435D1E89E2562048740360E37201",
signType:"MD5",
time:"2022-07-15 16:46:00",
timeStamp:"1657874760",
total_price:169,
_id:"5fd1ce998c16885a1972d20b",
}
]
tobedetail.data = tobedetails
this.orderdata = tobedetail.data
}catch(e){
//TODO handle the exception
}
}else {
// 待评价组件的商品详情接口数据
try{
let dtpepage = await new this.Request(this.Urls.m().dtpepage + '?id=' + id).modeget();
// 此处因为是没有商户号的购买数据,拿到是空数据
// 因此自己创建一个假数组对象当作拿到的tobedetail数据
console.log(dtpepage,'我是待评价组件的商品数据');
let tobedetails = [
{
consignee:{
address:"阿里巴巴",
city:"四川省 成都市 高新区",
mobile:"15858588888",
name:"马云",
},
nonceStr:"yybvvcyP6CDZ08E4",
order:
[
{
color:'5526网面-天空蓝',
image:"http://h.thexxdd.cn/tianmao/public%5Cuploads%5C1606059327494-4142549.jpg",
many:1,
price:169,
size:"37",
title:"耐克新款2022 女生白色皮面鞋子高级【店长推荐】"
},
],
order_number:"1607585433768778712",
out_trade_no:"2zmCzfDZe6Zr8rKYCz43AbXEe2Kk2Pcs",
package:"prepay_id=wx101530337150731019fc4f4e85085f0000",
paySign:"C0E5435D1E89E2562048740360E37201",
signType:"MD5",
time:"2022-07-15 16:46:00",
timeStamp:"1657874760",
total_price:169,
_id:"5fd1ce998c16885a1972d20b",
}
]
dtpepage.data = tobedetails
this.orderdata = dtpepage.data
}catch(e){
//TODO handle the exception
}
}
},
商品评价(上)
在evaluated.vue组件中,点击评价后跳转到新的组件中,
在pages/order-details/新建pu-coments.vue文件,同时去page.json注册
静态布局:
<template>
<view>
<!-- 描述 -->
<view class="pu-coments-input">
<textarea placeholder="宝贝满足你的期待吗?说说它的优点和美中不足的地方吧" v-model="tipsdata" maxlength="200"/>
</view>
<!-- 上传图片 -->
<view class="Upload-pictures">
<view class="Upload-button" >
<image src="/static/loading/uptu.svg" mode="widthFix"></image>
</view>
<view class="conteng">
<view class="conteng-img">
<image src="http://h.thexxdd.cn/tianmao/public%5Cuploads%5C1606059327494-4142549.jpg" mode="aspectFill" class="uploadimg" ></image>
<image src="/static/loading/guanbi.svg" mode="widthFix" class="deleteimg" @click="deteImg(index)"></image>
</view>
</view>
</view>
<!-- 提交 -->
<view class="Su-comments" @click="subMit()">提交评论</view>
</view>
</template>
<script>
export default {
data() {
return {
}
},
methods:{
},
}
</script>
<style>
page{background: #eeeeee;}
.pu-coments-input{background-color: #FFFFFF; height: 350rpx;}
.pu-coments-input textarea{width: 100%; color: rgb(0, 0, 0) !important;
padding: 10rpx;
font-size: 30rpx;
height: 350rpx;
overflow-y: auto;
}
.Upload-button{width: 130rpx; height: 130rpx;
padding-left: 10rpx; padding-bottom: 5rpx;}
.Upload-button image{width: 130rpx; height: 130rpx;}
.Upload-pictures{margin: 15rpx;}
/* 上传图片 */
.conteng{display: flex;flex-wrap: wrap;}
.conteng-img{width: calc(25% - 10rpx*2);
height: 150rpx;
padding: 10rpx;
position: relative;
}
.conteng-img image{width: 100%; height: 150rpx;
border-radius: 6rpx;
}
.deleteimg{width: 33upx !important; height: 33upx !important;
position: absolute;
top: 0upx;
right: 0upx;}
/* 提交评论 */
.Su-comments{background-color: rgb(255, 83, 2);
color: #FFFFFF;
height: 100rpx;
line-height: 100rpx;
position: fixed;
bottom: 0;
right: 0;
left: 0;
text-align: center;
}
</style>
商品评价(下)
<template>
<view>
<!-- 描述 -->
<view class="pu-coments-input">
<textarea placeholder="宝贝满足你的期待吗?说说它的优点和美中不足的地方吧" v-model="tipsdata" maxlength="200"/>
</view>
<!-- 上传图片 -->
<view class="Upload-pictures">
<!-- 使用计算属性 -->
<view class="Upload-button" @click="uploadImg()" v-if="upbutton">
<image src="/static/loading/uptu.svg" mode="widthFix"></image>
</view>
<view class="conteng">
<block v-for="(item,index) in upimg" :key="index">
<view class="conteng-img">
<image :src=item mode="aspectFill" class="uploadimg" @click="preImage(index,upimg)"></image>
<image src="/static/loading/guanbi.svg" mode="widthFix" class="deleteimg" @click="deteImg(index)"></image>
</view>
</block>
</view>
</view>
<!-- 提交 -->
<view class="Su-comments" @click="subMit()">提交评论</view>
</view>
</template>
<script>
// 引入预览图片js
const {Login} = require('../../public/logic.js')
export default {
data() {
return {
// 双向绑定的input框文本
tipsdata:'',
upimg:[], //准备上传的图片信息
comming:{},//接收上个组件的数据
}
},
methods:{
// 上传图片 使用微信的接口
uploadImg(){
wx.chooseMedia({
count: 1, //图片张数
mediaType: ['image','video'], //文件类型
sizeType: ['compressed'], //仅对 mediaType 为 image 时有效,是否压缩所选文件
sourceType: ['album', 'camera'], //图片和视频选择的来源
maxDuration: 30, //拍摄视频最长拍摄时间,单位秒。时间范围为 3s 至 60s 之间。不限制相册。
success:(res)=>{
new this.$Toast('等待上传').showloading() //加载提示
console.log(res,'拍摄或从手机相册中选择图片或视频');
this.cloudImg(res.tempFiles[0].tempFilePath)
},
fail:(err)=>{
console.log(err)
}
})
},
// 微信提供 上传图片到服务器方法
cloudImg(image){
wx.uploadFile({
url:'https://meituan.thexxdd.cn/api/potimg',
filePath:image, //要上传文件资源的路径 (本地路径)
name:'file', //文件对应的 key,开发者在服务端可以通过这个 key 获取文件的二进制内容
success:(res)=>{
console.log(res,'上传图片到服务器');
this.upimg.push(JSON.parse(res.data).data)
new this.$Toast('上传成功').showtoast()
},
fail: (err) => {
console.log(err)
}
})
},
// 预览图片
preImage(index,upimg){
new Login(index,upimg).previewImg()
},
// 删除图片
deteImg(index){
this.upimg.splice(index,1)
},
// 提交评论
async subMit(){
new this.$Toast('正在提交','none').showloading()
// 拿到评论接口需要的参数
let {_id,id,size,color} = this.comming
let obj = {orderid:_id,commid:id,size,color,comment:this.tipsdata,commimage:this.upimg}
try{
let data = await new this.Request(this.Urls.m().subcomm,obj).modepost()
console.log(data,'提交评论');
if(data.msg == 'SUCCESS'){
new this.$Toast('评论成功','SUCCESS').showtoast()
setTimeout(()=>{
uni.redirectTo({
url: '../personal/personal'
});
},1000)
}else{
new this.$Toast(data.msg,'none').showtoast()
}
}catch(e){
//TODO handle the exception
}
}
},
//使用计算属性完成 当图片数量达到2时,隐藏上传入口
computed:{
upbutton(){
if(this.upimg.length >= 2){
return false
}else{
return true
}
}
},
// 接收上个组件传递的数据
onLoad(e){
this.comming = JSON.parse(e.data)
}
}
</script>
待评价页面进入商品详情页 order.vue 中的 去评价 功能
}else if(text == '去评价') {
let {_id,id,size,color} = this.orderdata[0].order[0]
let data = JSON.stringify({_id,id,size,color})
uni.navigateTo({
url:'../order-details/pu-coments?data=' + data
})
}
然后打开my文件夹下 my.vue 设置跳转到各个组件详情页方法
// 跳转到各板块组件 --详情页
myIng(index){
uni.navigateTo({
url:'../personal/personal?index=' + index
})
}
商品分类
商品一级分类
sort.vue组件 静态布局
<template>
<view class="sort-view">
<!-- 左边 -->
<view class="sort-left">
<view class="sort-name" >潮流男装</view>
</view>
<!-- 右边 -->
<view class="sort-right">
<view class="sort-name sort-title">潮流男装</view>
<view class="sort-flex">
<view class="sort-goods" >
<image :src="iteming.name_image" mode="aspectFill">图片</image>
<text>外套</text>
</view>
</view>
</view>
</view>
</template>
<script></script>
<style scoped>
.sort-view {
display: flex;
}
.sort-left {
width: 150upx;
background: #f8f8f8;
height: 100vh;
overflow-y: auto;
position: fixed;
left: 0;
bottom: 0;
top: 0;
}
.sort-right {
flex: 1;
margin-left: 150upx;
}
.sort-name {
height: 80upx;
line-height: 80upx;
font-size: 28upx;
text-align: center;
}
.activesort {
background: #ffffff;
}
/* right */
.sort-title {
text-align: left !important;
padding-left: 40upx;
font-weight: bold;
}
.sort-flex {
display: flex;
flex-wrap: wrap;
align-items: center;
}
.sort-flex image {
width: 80upx;
height: 100upx;
display: block;
}
.sort-goods text {
padding-top: 10upx;
}
.sort-goods {
font-size: 28upx;
width: 33.333%;
display: flex;
flex-direction: column;
align-items: center;
padding-bottom: 40upx;
}
</style>
完整逻辑
<template>
<view class="sort-view">
<!-- 左边 -->
<view class="sort-left">
<block v-for="(item, index) in comclass" :key="index">
<!-- 动态的背景色:条件是下标值和num相等-->
<view class="sort-name" :class="{ activesort: index == num }" @click="sortSwitch(index,item.cid)">{{ item.sort }}</view>
</block>
</view>
<!-- 右边 -->
<view class="sort-right">
<block v-for="(item, index) in secondclass" :key="index">
<view class="sort-name sort-title">{{ item.sort }}</view>
<view class="sort-flex">
<block v-for="(iteming, indexs) in item.secon_classif" :key="indexs">
<view class="sort-goods">
<image :src="iteming.name_image" mode="aspectFill"></image>
<text>{{ iteming.name }}</text>
</view>
</block>
</view>
</block>
</view>
</view>
</template>
<script>
export default {
data() {
return {
comclass: [], //左侧一级数据
secondclass: [], //右侧二级数据
num: 0
};
},
methods: {
// comclass
// 请求商品一级分类接口
async sortRequest() {
try {
let comclass = await new this.Request(this.Urls.m().comclass).modeget();
console.log(comclass, '商品一级接口');
this.comclass = comclass.data;
// 进入页面默认请求选中的分类下的商品
this.seCond(comclass.data[0].cid);
} catch (e) {
//TODO handle the exception
}
},
// 选中左侧菜单项
sortSwitch(index,cid) {
this.num = index;
this.seCond(cid) //每次点击左侧都会有不同的cid参数传入 二级请求方法
},
// 请求商品二级分类接口
async seCond(cid) {
try {
let secondclass = await new this.Request(this.Urls.m().secondclass + '?cid=' + cid).modeget();
console.log(secondclass, '商品二级接口');
this.secondclass = secondclass.data;
} catch (e) {}
}
},
created() {
this.sortRequest();
}
};
</script>
商品二级分类
在pages下新建 class-goods文件夹,在其下新建goods.vue文件
静态布局如下:
<template>
<view>
<!-- 排序 -->
<view class="condition-view">
<view>
<text>全部</text>
<image src=""></image>
</view>
<view>
<text>销量</text>
<image src=""></image>
</view>
<view>
<text>价格</text>
<image src="/static/details/jiantou.svg"></image>
</view>
</view>
<view style="height: 70rpx;"></view>
<!-- 商品 -->
<Commodity :commdata="commdata"></Commodity>
</view>
</template>
<script>
export default {
data() {
return {};
},
methods: {}
};
</script>
<style>
page {
background: #f4f4f4;
}
.condition-view {
background: #ffffff;
height: 70upx;
font-size: 28upx;
display: flex;
align-items: center;
justify-content: space-around;
position: fixed;
top: 0;
left: 0;
right: 0;
}
.condition-view image {
width: 25upx;
height: 25upx;
}
.condition-view text {
padding-right: 8upx;
}
.condition-view view {
display: flex;
align-items: center;
color: #999;
}
.active {
color: #dd2727 !important;
}
.admining {
transform: rotate(180deg);
}
</style>
在sort.vue组件中,点击商品携带俩个参数到商品分类goods.vue组件中
// 点击商品跳转到分类详情页
sortIng(cid,name){
let data = JSON.stringify({cid,name})
uni.navigateTo({
url:'../class-goods/goods?data=' + data
})
}
goods.vue组件中,onLoad(e)接收俩参数
<template>
<!-- 二级分类详情组件 -->
<view>
<!-- 排序 -->
<view class="condition-view">
<block v-for="(item,index) in sorting" :key="index">
<view :class="{active : index == num}" @click="sortImg(index)">
<text >{{item.text}}</text>
<image :src='item.image'></image>
</view>
</block>
</view>
<view style="height: 70rpx;"></view>
<!-- 商品 -->
<Card :commdata="commdata"></Card>
</view>
</template>
<script>
// 引入公用的组件 --懒加载卡片
import Card from '../commponents/card.vue'
export default {
components:{
Card,
},
data() {
return {
num:0,
querydata:{}, //查询值
sorting:[
{
text:'全部',
image:'',
},
{
text:'销量',
image:'',
},
{
text:'价格',
image:'/static/details/jiantou.svg',
}
],
commdata:[], //二级分类下的商品数据
};
},
methods: {
sortImg(index){
this.num = index
},
// 进入页面后执行的 查询二级分类下的商品
async goodsRequest(){
let {cid,name} = this.querydata
let id = '?cid=' + cid + '&name=' + name
console.log(id);
try{
let queryclass = await new this.Request(this.Urls.m().queryclass + id).modeget()
console.log(queryclass,'二级分类下的商品')
// 将接口拿到的数据 传递给卡片流组件
this.commdata = queryclass.data
}catch(e){
console.log(e)
}
}
},
onLoad(e) {
// 接收sort组件传递进来的数据并保存到data中
this.querydata = JSON.parse(e.data)
console.log(this.querydata);
this.goodsRequest()
}
};
</script>
默认展示的是全部的商品,接下来完成排序查询
按销量、按价格查询商品
思路:
通过切换按钮时,下标不同,就触发不同的请求方法
下标为0 全部 按钮执行 goodsRequest( )方法
下标为1 销量 按钮执行 goodsRequest( )方法
下标为2 价格 按钮执行 goodsRequest( )方法
sortImg(index){
this.num = index
// 当下标为0 即处于全部按钮 调用全部商品接口
if(index == 0) {
this.goodsRequest()
// 当下标为1 即处于 销量 按钮
}else if (index == 1) {
// 每次点击销量按钮 值都会改变 1 或者 -1 --即升序或者降序排列
this.savo = this.savo == 1 ? -1 : 1
this.qsFun('001',this.savo)
// 当下标为2 即处于 价格 按钮
}else if (index == 2) {
this.rotate = this.rotate == true ? false : true
this.pri = this.pri == 1 ? -1 : 1
this.qsFun('002',this.pri)
}
},
good.vue完整代码
<template>
<!-- 二级分类详情组件 -->
<view>
<!-- 排序 -->
<view class="condition-view">
<block v-for="(item,index) in sorting" :key="index">
<view :class="{active : index == num}" @click="sortImg(index)">
<text >{{item.text}}</text>
<image :src='item.image' :class="{admining : rotate}"></image>
</view>
</block>
</view>
<view style="height: 70rpx;"></view>
<!-- 商品 -->
<Card :commdata="commdata"></Card>
</view>
</template>
<script>
// 引入公用的组件 --懒加载卡片
import Card from '../commponents/card.vue'
// node.js 的 qs模块
const qs = require('querystring')
export default {
components:{
Card,
},
data() {
return {
num:0,
querydata:{}, //查询值
sorting:[
{
text:'全部',
image:'',
},
{
text:'销量',
image:'',
},
{
text:'价格',
image:'/static/details/jiantou.svg',
}
],
commdata:[], //二级分类下的商品数据
savo:1, // 按销量默认为1 代表升序
pri:1, // 按价格
rotate:false, //箭头默认朝下
};
},
methods: {
sortImg(index){
this.num = index
// 当下标为0 即处于全部按钮 调用全部商品接口
if(index == 0) {
this.goodsRequest()
// 当下标为1 即处于 销量 按钮
}else if (index == 1) {
// 每次点击销量按钮 值都会改变 1 或者 -1 --即升序或者降序排列
this.savo = this.savo == 1 ? -1 : 1
this.qsFun('001',this.savo)
// 当下标为2 即处于 价格 按钮
}else if (index == 2) {
this.rotate = this.rotate == true ? false : true
this.pri = this.pri == 1 ? -1 : 1
this.qsFun('002',this.pri)
}
},
// 进入页面后执行的 查询二级分类下的商品
async goodsRequest(){
let {cid,name} = this.querydata
let id = '?cid=' + cid + '&name=' + name
console.log(id);
try{
let queryclass = await new this.Request(this.Urls.m().queryclass + id).modeget()
this.commdata = queryclass.data
console.log(this.commdata,'二级分类下的商品数据')
}catch(e){
console.log(e)
}
},
// 查询商品(按销量、价格)
qsFun(spvalue,number){
let id = '?cid=' + this.querydata.cid + '&name=' + this
// qs.stringify 把一个参数对象格式化为字符串 获得参数 param
const param = qs.stringify({
cid:this.querydata.cid,
name:this.querydata.name,
spvalue,spvalue,
number,number,
})
console.log(param,'查询商品(按销量、按价格)');
this.queryCod(param)
},
// 调用查询商品接口
async queryCod(param){
try{
let querycod = await new this.Request(this.Urls.m().querycod + '?' + param).modeget()
console.log(querycod,'查询商品(按销量、按价格)');
this.commdata = querycod.data
}catch(e){
//TODO handle the exception
}
}
},
onLoad(e) {
// 接收到分类组件传递来的第一级分类数据
this.querydata = JSON.parse(e.data)
console.log(this.querydata,'接收到分类组件传递来的第一级分类数据');
this.goodsRequest()
}
};
</script>
购物车
pages/shopping/shopping.vue组件中
静态布局
代码如下:
<template>
<view>
<view class="shopp-view">
<view class="manage-tli">完成</view>
<view style="margin-bottom: 150rpx;">
<view class="shopp-goods">
<!-- 商品 -->
<view class="shopp-goods-view">
<view class="shopp-chick">
<!-- <image src="/static/details/xuanzhong.svg" mode="widthFix"></image> -->
<image src="/static/details/weixuan.svg" mode="widthFix"></image>
</view>
<view class="shopp-chick-img">
<image src="" mode="scaleToFill">图片1</image>
</view>
<view class="shopp-chick-gight">
<view class="shopp-chick-right">
<view>标题</view>
<view class="shopp-chick-sku">
<text>尺码;</text>
<text>颜色</text>
<image src="/static/details/jiantou.svg" mode="widthFix"></image>
</view>
</view>
<!-- 价格 -->
<view class="shopp-price">
<view>¥100</view>
<view class="shopp-price-nums">
<view>-</view>
<view>0</view>
<view>+</view>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 底部结算 -->
<view class="settlement-view">
<view class="settlement-img">
<image src="/static/details/weixuan.svg" mode="widthFix" ></image>
<!-- <image v-if="selectall" src="/static/details/xuanzhong.svg" mode="widthFix" @click="selectAll('deall')"></image> -->
</view>
<view class="settlement-tips">
<view class="total-view">
<text>总价合计:</text>
<text>¥ 100</text>
</view>
</view>
<view class="settlement-money">结算</view>
<!-- <view class="settlement-money">删除</view> -->
</view>
</view>
<!-- sku组件-->
<Addtocart ref="addto" :skudata="skudata"></Addtocart>
<!-- 登录界面 -->
<loginpage ref="loginmen"></loginpage>
<!-- 购物车没有数据的提示-->
<view class="empty-cart" v-if="!emcart">
<image src="/static/loading/kongcart.svg" mode="widthFix"></image>
<text>购物车竟然是空的</text>
</view>
</view>
</template>
<script>
export default {
data() {
return {
emcart: true
};
}
};
</script>
<style>
page {
background: #f2f2f2;
}
.shopp-view {
margin: 0 10upx;
}
.manage-tli {
font-size: 28upx;
color: #6b6b6b;
height: 80upx;
line-height: 80upx;
text-align: right;
}
/* 商品 */
.shopp-goods {
font-size: 28upx;
/* height: 340upx; */
background: #ffffff;
border-radius: 10upx;
padding: 20upx 10upx;
margin-bottom: 15rpx;
}
.shopp-goods-Coupon {
height: 70upx;
/* background: #4CD964; */
display: flex;
align-items: center;
justify-content: flex-end;
}
.shopp-chick {
width: 45upx;
height: 200upx;
display: flex;
flex-direction: column;
justify-content: center;
}
.shopp-chick image {
width: 45upx;
height: 45upx;
}
.shopp-chick-img {
width: 200upx;
height: 200upx;
padding: 0 15upx;
}
.shopp-chick-img image {
width: 100%;
height: 100%;
border-radius: 8upx;
}
.shopp-chick-gight view:nth-child(1) {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
.shopp-chick-right image {
width: 20upx;
height: 20upx;
padding-left: 8upx;
}
.shopp-chick-sku {
font-size: 25upx;
color: #999999;
height: 50upx;
background: #f9f9f9;
display: inline-flex;
align-items: center;
margin: 17upx 0;
padding: 0 20rpx;
border-radius: 5rpx;
}
.shopp-price {
display: flex;
align-items: center;
justify-content: space-between;
height: 50upx;
/* background: #9ACD32; */
}
.shopp-price view:nth-child(1) {
color: #f75807;
font-weight: bold;
}
.shopp-price-nums {
display: flex;
align-items: center;
border: 1px solid #f9f9f9;
width: 200upx;
border-radius: 10upx;
}
.shopp-price-nums view {
font-weight: bold;
width: 90upx;
height: 50upx;
line-height: 50upx;
text-align: center;
}
.shopp-price-nums view:nth-child(2) {
border-left: 1px solid #f9f9f9;
border-right: 1px solid #f9f9f9;
padding: 0 10upx;
}
.shopp-goods-view {
display: flex;
}
.shopp-chick-gight {
flex: 1;
}
.Special-rate {
color: #ff5000;
display: flex;
align-items: center;
height: 50upx;
font-weight: bold;
}
/* 结算 */
.settlement-view {
font-size: 28upx;
background: #ffffff;
height: 90upx;
border-top: 1px solid #f2f2f2;
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
align-items: center;
justify-content: space-between;
}
.settlement-img {
width: 45upx;
height: 45upx;
}
.settlement-img image {
width: 45upx;
height: 45upx;
padding-left: 20upx;
}
.total-view {
display: flex;
align-items: center;
justify-content: center;
}
.total-view text:nth-child(1) {
font-size: 30upx;
}
.total-view text:nth-child(2) {
font-weight: bold;
color: #ff4401;
padding-left: 10upx;
}
.settlement-money {
background: #fe0036;
height: 90upx;
line-height: 90upx;
width: 220upx;
text-align: center;
font-size: 30upx;
color: #ffffff;
}
.settlement-tips {
text-align: center;
}
</style>
拉取购物车数据
<template>
<view>
<view class="shopp-view" v-if="emcart">
<view class="manage-tli" @click="adMinis()">{{editdata}}</view>
<view style="margin-bottom: 150rpx;">
<block v-for="(item,index) in mycarturl" :key="index">
<view class="shopp-goods">
<!-- 商品 -->
<view class="shopp-goods-view">
<!-- 选中的圆圈样式 -->
<view class="shopp-chick">
<!-- 选中图标 -->
<image v-if="item.choice" src="/static/details/xuanzhong.svg" mode="widthFix"></image>
<!-- 未选中图标 -->
<image v-else src="/static/details/weixuan.svg" mode="widthFix"></image>
</view>
<view class="shopp-chick-img">
<image :src="item.image" mode="scaleToFill"></image>
</view>
<view class="shopp-chick-gight">
<view class="shopp-chick-right">
<view>{{item.title}}</view>
<view class="shopp-chick-sku">
<text>{{item.size}}</text>
<text>{{item.color}}</text>
<image src="/static/details/jiantou.svg" mode="widthFix"></image>
</view>
</view>
<!-- 价格 -->
<view class="shopp-price">
<view>¥{{item.price}}</view>
<view class="shopp-price-nums">
<view>-</view>
<view>{{item.many}}</view>
<view>+</view>
</view>
</view>
</view>
</view>
</view>
</block>
</view>
<!-- 底部结算 -->
<view class="settlement-view">
<view class="settlement-img">
<image src="/static/details/weixuan.svg" mode="widthFix" ></image>
<!-- <image v-if="selectall" src="/static/details/xuanzhong.svg" mode="widthFix" @click="selectAll('deall')"></image> -->
</view>
<view class="settlement-tips" v-show="settips">
<view class="total-view">
<text>总价合计:</text>
<text>¥ 100</text>
</view>
</view>
<view class="settlement-money" v-show="settips">结算</view>
<view class="settlement-money" v-show="!settips">删除</view>
</view>
</view>
<!-- sku组件-->
<Addtocart ref="addto" :skudata="skudata"></Addtocart>
<!-- 登录界面 -->
<loginpage ref="loginmen"></loginpage>
<!-- 购物车没有数据的提示-->
<view class="empty-cart" v-if="!emcart">
<image src="/static/loading/kongcart.svg" mode="widthFix"></image>
<text>购物车竟然是空的</text>
</view>
<!-- 使用 全局引入的登录弹窗 组件-->
<showmodal ref="show"></showmodal>
</view>
</template>
<script>
export default {
data() {
return {
emcart: true,
mycarturl:[], //购物车数据
editdata:'管理',
settips:true, //默认展示右下角 结算还是删除
};
},
methods:{
// 获取购物车数据
async ShoppingRequest() {
let mycarturl = await new this.Request(this.Urls.m().mycarturl).modeget()
console.log(mycarturl,'获取到的购物车数据');
if(mycarturl.msg.errcode ) {
// 401 表示没有方法权限或者没有登录 使用 全局引入的登录弹窗 组件里的方法
// 登陆这里传值过去 'coll'
this.$refs.show.showing('coll')
}else if(mycarturl.msg == 'SUCCESS'){
// 购物车没数据的情况
if(mycarturl.data.length != 0) {
this.mycarturl = mycarturl.data
// 显示提示
this.emcart = true
}else {
this.emcart = false
}
}
},
// 管理、完成与删除的切换
adMinis(){
this.editdata = this.editdata == '管理'? '完成' : '管理'
if(this.editdata == '完成'){
// 要不是趁年轻
this.settips = false
}else if(this.editdata == '管理'){
this.settips = true
}
}
onShow(){
this.ShoppingRequest()
},
mounted() {
// 当登录成功后刷新页面
this.$bus.$on('mycart',res => {
// res就是收到的值 这里是来自于loain-oage.vue中的 cart: "SUCCESS"
console.log(res,'我是收到的data')
if(res.cart == 'SUCCESS') {
// 如果登录成功就 获取收货地址
this.ShoppingRequest()
}
})
}
};
</scrip
计算已选中的合计总价
// 获取购物车数据
async ShoppingRequest() {
let mycarturl = await new this.Request(this.Urls.m().mycarturl).modeget()
console.log(mycarturl,'获取到的购物车数据');
if(mycarturl.msg.errcode ) {
// 401 表示没有方法权限或者没有登录 使用 全局引入的登录弹窗 组件里的方法
// 登陆这里传值过去 'coll'
this.$refs.show.showing('coll')
}else if(mycarturl.msg == 'SUCCESS'){
// 购物车有数据的情况
if(mycarturl.data.length != 0) {
this.mycarturl = mycarturl.data //获取数据
this.coselect()// 计算价格
this.emcart = true //不展示提示
// 购物车没有数据的情况
}else {
this.emcart = false // 显示提示
}
}
},
// 计算单个商品总价
coselect(){
// 过滤出数组中 choice为 true的对象 filter会将目标对象放到新数组中
let filselect = this.mycarturl.filter(item => item.choice)
// 取出被选中的 数组中的的总价
this.mapselect = filselect.map(item => item.total_price)
},
结算和删除按钮的功能
通过计算属性,当勾选长度为0,没有勾选数据就返回一个disable值为false
computed:{
//计算总价之和
total(){
// 判断是否有勾选的数据
if(this.mapselect.length == 0) {
return {price:0,disable:false} //返回一个总价 值为0
}else {
let numdata = 0
this.mapselect.forEach((item)=>{numdata += item})
return {price:parseFloat((numdata).toFixed(10)),disable:true} //解决浮点数
}
}
},
<view class="settlement-money" v-show="settips" @click="total.disable && semEnt()">结算</view>
<view class="settlement-money" v-show="!settips" @click="total.disable && deLece()">删除</view>
// 结算
semEnt(){
console.log('结算');
},
// 删除
delece(){
},
单个商品选中和取消选中
<!-- 选中的圆圈样式 -->
<view class="shopp-chick">
<!-- 选中图标 -->
<image v-if="item.choice" @click="seLect(item._id,'unchecked')" 1 src="/static/details/xuanzhong.svg" mode="widthFix">
</image>
<!-- 未选中图标 -->
<image v-else @click="seLect(item._id,'select')" src="/static/details/weixuan.svg" mode="widthFix">
</image>
</view>
// 单个商品选中或取消
async seLect(id,nums){
new this.$Toast('请求中','none').showloading()
try{
let selecting = await new this.Request(this.Urls.m().selecting,{id:id,nums:nums}).modepost()
console.log(selecting,'单个商品选中或取消');
this.ShoppingRequest()
uni.hideLoading()
}catch(e){
//TODO handle the exception
}
}
购物车加减商品数量
<!-- 价格 -->
<view class="shopp-price">
<view>¥{{item.price}}</view>
<view class="shopp-price-nums">
//当商品数量为1的时候静止点击减按钮
<view @click="item.many === 1 ? false : true && reDuce(item._id,item.many,item.price)">-</view>
<view>{{item.many}}</view>
<view @click="pLus(item._id,item.many,item.price)">+</view>
</view>
</view>
//结算后边的数字 由 many 控制
<view class="settlement-money" v-show="settips" @click="total.disable && semEnt()">结算{{(mapselect.length)}}</view>
<view class="settlement-money" v-show="!settips" @click="total.disable && deLece()">删除</view>
// 加减商品数量请求
async shoppingNumber(id,many,price){
new this.$Toast('请求中','none').showloading()
try{
let pridec = await new this.Request(this.Urls.m().pride,{id:id,many:many,price:price}).modepost()
this.ShoppingRequest()
uni.hideLoading()
}catch(e){
//TODO handle the exception
}
},
// 减
reDuce(id,many,price){
many --
this.shoppingNumber(id,many,price)
},
// 加
pLus(id,many,price){
many ++
this.shoppingNumber(id,many,price)
},
全选和取消全选
思路:在计算属性中,添加一个方法:
判断之前过滤出选的中商品的数组长度 如果等于了购物车里数据长度 就表示全选了所有商品
// 控制全选和取消全选
selectall(){
// 判断之前过滤出选中了的商品的数组的长度 如果等于了购物车里数据长度 就表示全选了所有商品
if(this.mapselect.length === this.mycarturl.length) {
return true
}else {
return false
}
}
将计算属性返回的方法布尔值通过v-if分别绑定给未全选和全选。
同时给按钮(圆圈)添加点击事件,在点击事件中发起请求,并刷新页面
<view class="settlement-img">
<!-- 未全选 -->
<image v-if="!selectall" @click="quanxuan('seall')" src="/static/details/weixuan.svg" mode="widthFix" >
</image>
<!-- 全选 -->
<image v-if="selectall" @click="quanxuan('deall')" src="/static/details/xuanzhong.svg" mode="widthFix" >
</image>
</view>
// 全选商品
async quanxuan(idft) {
new this.$Toast('请求中','none').showloading()
try{
let selectall = await new this.Request(this.Urls.m().selectall + '?idft=' + idft).modeget()
console.log(selectall,'全选商品');
this.ShoppingRequest()
uni.hideLoading()
}catch(e){
}
}
删除购物车商品
点击删除按钮,删除选中的商品
由接口可知,删除请求需要携带的参数是已经勾选的商品_id,所以将已经勾选的商品数组中的 _id单独 拿出存放到新数组中
<view class="settlement-money" v-show="!settips" @click="total.disable && deLece()">删除 </view>
data() {
return {
emcart: true,
mycarturl:[], //购物车数据
editdata:'管理',
settips:true, //默认展示右下角 结算还是删除
mapselect:[], //存放已勾选单个商品的总价
cartdele:[], //存放已经被勾选的商品 _id
};
},
// 计算单个商品总价
coselect(){
// 过滤出数组中 choice为 true的对象 即已经勾选的商品 filter会将目标对象放到新数组中
let filselect = this.mycarturl.filter(item => item.choice)
// 取出被选中的 数组中的的总价
this.mapselect = filselect.map(item => item.total_price)
// 将已经被勾选的商品, 购物车_id 单独存放出来
this.cartdele = filselect.map(item => item._id)
},
// 删除
async deLece(){
new this.$Toast('请求中','none').showloading()
try{
let cartdelete = await new this.Request(this.Urls.m().cartdelete,{arrid:this.cartdele}).modepost()
console.log(cartdelete,'购物车删除商品');
this.ShoppingRequest()
uni.hideLoading()
}catch(e){
}
}
购物车重选sku颜色尺码(上)
思路:公共组件addtocart.vue,就是之前写好的sku组件,因此要引用到shopping.vue组件中,然后展示到页面中使用
<!-- sku组件-->
<!-- 为什么是skudata 因为 sku组件中 props接收父组件的 键值对k名称就是skudata -->
<Addtocart ref="addto" :skudata="skudata"></Addtocart>
// sku组件 引用到shopping.vue组件中
import Addtocart from '../commponents/addtocart.vue'
// 弹出sku组件 直接弹出组件是没数据的因此要调用 相关接口
normsFun(id){
try{
let mycarturl = await new this.Request(this.Urls.m().mycarturl).modeget()
}catch(e){
//TODO handle the exception
}
},
然后给相应的位置添加点击事件,同时携带商品id传入请求方法中,同时通过ref控制sku组件中的showCou()方法,将001传入,001代表的是
// 弹出sku组件 直接弹出组件是没数据的因此要调用 相关接口
async normsFun(id){
try{
let cartsku = await new this.Request(this.Urls.m().cartsku + '?id=' + id ).modeget()
console.log(cartsku,'弹出sku组件');
this.skudata = cartsku.data
this.$refs.addto.showCou('001') // 显示组件
}catch(e){
//TODO handle the exception
}
},
在sku组件中,001表示调用001表示购物车重选
<!-- 确定按钮 -->
<!-- 利用计算属性 如果两个未选中静止使用确定按钮 -->
<view class="determine coup-anim" v-if="mean == '001'" @click="skumen.ban && detErmine()">确定</view>
<view class="determine coup-anim" v-if="mean == '002'" @click="skumen.ban && detErmine()">确定</view>
<view class="determine coup-anim" v-if="mean == '003'" @click="skumen.ban && purChase()">确定</view>
// 被其他组件调用显示sku组件 mean 等于 002代表加入购物车 003代表直接购买 001表示购物车重选
showCou(mean){
this.mean = mean
this.couponif = true
},
难点1:当在购物车组件中点击sku时候,展示的效果应该和购物车所选好的尺码颜色等信息相同
思路:点击事件携带的参数,再加上从购物车获取到的本条商品的一些信息传递到sku组件
在购物车组件中,点击normsFun方法,携带相关参数,使用id发起请求,将请求结果cartsku.data 和normsFun点击事件拿到的商品_id 尺码 颜色 商品数量一起传递到 sku组件addtocart中。
<!-- 点击调出sku组件 -->
<view class="shopp-chick-sku" @click="normsFun(item.id,item.id,item.size,item.color,item.many)">
<text>{{item.size}}</text>
<text>{{item.color}}</text>
<image src="/static/details/jiantou.svg" mode="widthFix"></image>
</view>
// 弹出sku组件 直接弹出组件是没数据的因此要调用 相关接口
async normsFun(id,_id,size,color,many){
try{
// 购物车页面重选sku接口
let cartsku = await new this.Request(this.Urls.m().cartsku + '?id=' + id ).modeget()
console.log(cartsku,'购物车弹出的sku组件');
this.skudata = cartsku.data //得到商品sku展示的所以商品尺码 颜色等信息
let obj = {sku:this.skudata,size,color,_id,many} // 这里重新组合一个对象,传递到sku组件中用于修改弹出的商品信息
this.$refs.addto.showCou('001',obj) // 显示组件同时传递 obj对象
}catch(e){
//TODO handle the exception
}
}
sku组件addtocart,showCou方法的第二个参数,接收了数据,解构数据后进行了如下操作
// 被其他组件调用显示sku组件 mean 等于 002代表加入购物车 003代表直接购买 001表示购物车重选
showCou(mean,obj={}){
this.mean = mean
if(mean == '001') {
// 解构接收到的值
let {sku,size,color,_id,many} = obj
// 将这些值 替代sku组件中的值
this._id = _id // 购物车选中的商品 _id
this.momany = many // 购物车选中的商品数量
// 使用findIndex 方法,对购物车弹出的sku所展示的第0项商品信息进行遍历,找出尺码和购物车点击的尺码一样的下标
// findIndex() 方法对数组中存在的每个元素执行一次函数,返回该符合数组元素的索引 否则返回 -1
this.sizenum = sku[0].findIndex(item => item == size) // 获取在购物车页面点击商品尺码哪里,已经展示的尺码对应的下标 并赋值给sku页面 所展示已经选中的尺码
this.colornum = sku[1].findIndex(item => item.color == color) // 获取在购物车页面点击商品尺码哪里,已经展示的颜色对应的下标 并赋值给sku页面 所展示已经选中的颜色
this.colorvalue = color //这里是值下方的监听方法,监听到colorvalue值改变后进行改变颜色
this.sizevalue = size //同上监听方法
}
this.couponif = true
}
购物车重选sku颜色尺码(下)
难点:当用户从购物车点进sku组件时,重新选择了商品尺码和颜色后,
点击事件命名为 modifySku
传入修改sku请求接口需要的参数 data 发起请求
<!-- 确定按钮 -->
<!-- 利用计算属性 如果两个未选中静止使用确定按钮 -->
<view class="determine coup-anim" v-if="mean == '001'" @click="skumen.ban && modifySku()">确定</view>
<view class="determine coup-anim" v-if="mean == '002'" @click="skumen.ban && detErmine()">确定</view>
<view class="determine coup-anim" v-if="mean == '003'" @click="skumen.ban && purChase()">确定</view>
// 购物车修改sku
async modifySku(){
let {image,price} = this.attribute
let data = {
id:this._id,
skuid:this.id,
size:this.sizevalue,
color:this.colorvalue,
image,
price,
many:this.momany,
}
console.log(data);
try{
// 购物车修改sku接口
let skubase = await new this.Request(this.Urls.m().skubase,data).modepost();
console.log(skubase,'购物车修改sku接口');
if(skubase.msg == 'SUCCESS') {
this.hideCou() //隐藏sku组件
this.$bus.$emit('mycart',{cart:skubase.msg})// 刷新购物车
}
}catch(e){
//TODO handle the exception
}
},
购物车结算
思路:在购物车组件 shopping.vue 中,需要勾选了商品才能结算
提前在计算总价方法中存储已经勾选的商品数据,将其传递到付款组件中,注意因为JSON.parse识别不到某些特殊字符,因此使用 encodeURIComponent、decodeURIComponent方式
// 计算单个商品总价 (已经勾选的商品 filselect )
coselect(){
// 过滤出数组中 choice 为 true的对象 即已经勾选的商品 filter会将目标对象放到新数组中
let filselect = this.mycarturl.filter(item => item.choice)
// 取出被选中的 数组中的的总价
this.mapselect = filselect.map(item => item.total_price)
// 将已经被勾选的商品, 购物车_id 单独存放出来
this.cartdele = filselect.map(item => item._id)
// 存储已经勾选的商品数据
this.settlement = filselect
},
// 结算
semEnt(){
console.log('结算');
// 数组,对象转换成字符串才能携带值
let cartdata = JSON.stringify(this.settlement)
console.log(cartdata);
// 跳转到下单页面
uni.navigateTo({
url:'../payment-page/payment?cartdata=' + encodeURIComponent(cartdata),
})
},
在付款组件,payment.vue中接收值
// e就是路径上携带的值 来自于addtocart组件 purChase方法
onLoad(e){
let cartdataing = decodeURIComponent(e.cartdata)
// 将JSON字符串转化为js对象
this.comminfo = JSON.parse(cartdataing)
// 合计支付总价计算 numdata默认为0,使用forEach遍历
let numdata = 0
JSON.parse(cartdataing).forEach(item=>{
numdata += item.total_price
})
// 合计总价 小数点两位数
this.Totalprice = Price(numdata) // 将需补0的数据传入方法
},
由于支付接口要求:如果是购物车商品结算,需要把购物车中每个商品的_id作为一个数组字符串存储起来,否则填写空数组
如果支付成功后,购物车里就删除这个商品id的商品
// e就是路径上携带的值 来自于addtocart组件 purChase方法
onLoad(e){
let cartdataing = decodeURIComponent(e.cartdata) //接收json字符串
this.comminfo = JSON.parse(cartdataing) // 将JSON字符串转化为js对象
console.log(this.comminfo );
let numdata = 0 // 合计支付总价计算 numdata默认为0,使用forEach遍历
JSON.parse(cartdataing).forEach(item=>{
numdata += item.total_price
})
// 合计总价 小数点两位数
this.Totalprice = Price(numdata) // 将需补0的数据传入方法
// 存储购物车来的数据的商品_id
// _id 都在数组 this.comminfo 里边 有可能不止一个商品 因此要 筛选取出
let _id = JSON.parse(cartdataing).filter(item => item._id)
this.idcard = _id.map(item => item._id)
},
同时,当提交订单支付成功后,应该跳到待发货界面, 2-3步,因为没有商品号,所以注释掉,使用的虚拟支付接口
// 提交订单
async placeOrder(){
// 触发下单按钮后的提示
new this.$Toast('正在下单中','none').showloading() //手动 弹窗
// 商品数据 通过对comminfo数据进行map方法遍历,会返回一个新的数组对象
let codata = this.comminfo.map(item=>{
let data ={
id:item.id,
image:item.image,
title:item.title,
size:item.size,
color:item.color,
price:item.price,
many:item.many,
}
return data
})
// 准备一个对象 将请求需要的参数放入
let dataobj = {
consignee:this.orderdata, // 收货地址相关数据
commodity:codata, // 商品相关数据
total_price:this.Totalprice, // 支付总价数据
idcard:this.idcard, // 购物车内所有商品的id相关数据
}
// 1.请求统一下单接口
try{
// 微信支付: 统一下单接口
// var wxpay = await new this.Request(this.Urls.m().wxpay,dataobj).modepost();
// 因为个人没有认证微信商户号,这里使用的是虚拟支付接口 返回结果是"SUCCESS"
let fictpay = await new this.Request(this.Urls.m().fictpayurl,dataobj).modepost();
if(fictpay.msg == "SUCCESS") {
// 存储商户订单号和订单id
// this.outno = wxpayurl.data.out_trade_no
// this.ide = wxpayurl.data.id
setTimeout(()=>{
new this.$Toast(fictpay.data,'none').showtoast() //反馈 弹窗
// 关闭当前下单页面,跳转到代发货页面
uni.redirectTo({
url:'../personal/personal?index=' + 1
})
},300)
}
}catch(e){
// e就是throw 捕获的错误
// throw 用来捕获一个错误
//如果catch 中出现throw关键词,此时try catch 后边的代码将不会执行 即不会调用下边的微信接口
new this.$Toast(e,'none').showtoast() //反馈 弹窗
throw e
}
// 2.调用微信支付接口
// try{
// package:属于源码里的关键词
// 从真正的微信支付接口中拿到返回结果,这五个数据用于调用微信支付接口时作为参数,
// let { nonceStr,paySign,signType,timeStamp } = wxpay.data
// 使用下方调用支付的方法
// await this.wxPay({ nonceStr,paySign,signType,timeStamp,package:wxpay.data.package })
// }catch(e){
// new this.$Toast('支付失败','none').showtoast() //反馈 弹窗
// uni.redirectTo({
// url:'../personal/personal?index=' + 0
// })
// throw e
// }
// 3.查询订单是否支付成功 (去微信那边查询是否成功)
// try{
// let dataobj = {outno:this.outno,id:this.ide}
// let queryorderurl = await new this.Request(this.Urls.m().queryorderurl,dataobj).modepost()
// if(queryorderurl.data.msg == 'SUCCESS') {
// new this.$Toast('支付成功').showtoast() //反馈 弹窗
// uni.redirectTo({
// url:'../personal/personal?index=' + 1
// })
// }
// }catch(e){
// new this.$Toast(e,'none').showtoast() //反馈 弹窗
// }
},
项目优化
在首页,点击商品后,应该出现一个正在加载的图标
在pages下新建 full-loading.vue 组件,
<template>
<view class="full-loading" v-if="Loading">
<image src="/static/loading/loadtmall.gif" mode="widthFix"></image>
</view>
</template>
<script>
export default{
data() {
return {
Loading: true
}
},
methods:{
init(value = false){
this.Loading = value
}
}
}
</script>
<style scoped>
.full-loading{
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background: #fff;
z-index: 999;
}
.full-loading image{width: 100rpx; height: 100rpx;
}
</style>
再取main.js中引入
// 页面加载loading
import loading from 'pages/commponents/full-loading.vue'
Vue.component('full-loading',loading)
最后在想要加上加载效果的页面上如下添加
<!-- 页面加载 -->
<full-loading ref="fullload"></full-loading>
// 取消loading组件的加载效果
this.$refs.fullload.init()
THE END