一.结算页的实现
所有的结算本质上都会跳转到订单结算页Pay/index.vue
,跳转的同时携带对应的订单相应参数
具体需要哪些参数,基于pay页的需求来定的
1.静态页面布局
由两个部分组成:
- 上面的确认收货地址
- 下面的订单信息
<template>
<div class="pay">
<van-nav-bar fixed title="订单结算台" left-arrow @click-left="$router.go(-1)" />
<!-- 地址相关 -->
<div class="address">
<div class="left-icon">
<van-icon name="logistics" />
</div>
<div class="info" v-if="true">
<div class="info-content">
<span class="name">小红</span>
<span class="mobile">13811112222</span>
</div>
<div class="info-address">
江苏省 无锡市 南长街 110号 504
</div>
</div>
<div class="info" v-else>
请选择配送地址
</div>
<div class="right-icon">
<van-icon name="arrow" />
</div>
</div>
<!-- 订单明细 -->
<div class="pay-list">
<div class="list">
<div class="goods-item">
<div class="left">
<img src="http://cba.itlike.com/public/uploads/10001/20230321/8f505c6c437fc3d4b4310b57b1567544.jpg" alt="" />
</div>
<div class="right">
<p class="tit text-ellipsis-2">
三星手机 SAMSUNG Galaxy S23 8GB+256GB 超视觉夜拍系统 超清夜景 悠雾紫 5G手机 游戏拍照旗舰机s23
</p>
<p class="info">
<span class="count">x3</span>
<span class="price">¥9.99</span>
</p>
</div>
</div>
</div>
<div class="flow-num-box">
<span>共 12 件商品,合计:</span>
<span class="money">¥1219.00</span>
</div>
<div class="pay-detail">
<div class="pay-cell">
<span>订单总金额:</span>
<span class="red">¥1219.00</span>
</div>
<div class="pay-cell">
<span>优惠券:</span>
<span>无优惠券可用</span>
</div>
<div class="pay-cell">
<span>配送费用:</span>
<span v-if="false">请先选择配送地址</span>
<span v-else class="red">+¥0.00</span>
</div>
</div>
<!-- 支付方式 -->
<div class="pay-way">
<span class="tit">支付方式</span>
<div class="pay-cell">
<span><van-icon name="balance-o" />余额支付(可用 ¥ 999919.00 元)</span>
<!-- <span>请先选择配送地址</span> -->
<span class="red"><van-icon name="passed" /></span>
</div>
</div>
<!-- 买家留言 -->
<div class="buytips">
<textarea placeholder="选填:买家留言(50字内)" name="" id="" cols="30" rows="10"></textarea>
</div>
</div>
<!-- 底部提交 -->
<div class="footer-fixed">
<div class="left">实付款:<span>¥999919</span></div>
<div class="tipsbtn">提交订单</div>
</div>
</div>
</template>
<script>
export default {
name: 'PayIndex',
data () {
return {
}
},
methods: {
}
}
</script>
<style lang="less" scoped>
.pay {
padding-top: 46px;
padding-bottom: 46px;
::v-deep {
.van-nav-bar__arrow {
color: #333;
}
}
}
.address {
display: flex;
align-items: center;
justify-content: flex-start;
padding: 20px;
font-size: 14px;
color: #666;
position: relative;
// background: url(@/assets/border-line.png) bottom repeat-x;
background-size: 60px auto;
.left-icon {
margin-right: 20px;
}
.right-icon {
position: absolute;
right: 20px;
top: 50%;
transform: translateY(-7px);
}
}
.goods-item {
height: 100px;
margin-bottom: 6px;
padding: 10px;
background-color: #fff;
display: flex;
.left {
width: 100px;
img {
display: block;
width: 80px;
margin: 10px auto;
}
}
.right {
flex: 1;
font-size: 14px;
line-height: 1.3;
padding: 10px;
padding-right: 0px;
display: flex;
flex-direction: column;
justify-content: space-evenly;
color: #333;
.info {
margin-top: 5px;
display: flex;
justify-content: space-between;
.price {
color: #fa2209;
}
}
}
}
.flow-num-box {
display: flex;
justify-content: flex-end;
padding: 10px 10px;
font-size: 14px;
border-bottom: 1px solid #efefef;
.money {
color: #fa2209;
}
}
.pay-cell {
font-size: 14px;
padding: 10px 12px;
color: #333;
display: flex;
justify-content: space-between;
.red {
color: #fa2209;
}
}
.pay-detail {
border-bottom: 1px solid #efefef;
}
.pay-way {
font-size: 14px;
padding: 10px 12px;
border-bottom: 1px solid #efefef;
color: #333;
.tit {
line-height: 30px;
}
.pay-cell {
padding: 10px 0;
}
.van-icon {
font-size: 20px;
margin-right: 5px;
}
}
.buytips {
display: block;
textarea {
display: block;
width: 100%;
border: none;
font-size: 14px;
padding: 12px;
height: 100px;
}
}
.footer-fixed {
position: fixed;
background-color: #fff;
left: 0;
bottom: 0;
width: 100%;
height: 46px;
line-height: 46px;
border-top: 1px solid #efefef;
font-size: 14px;
display: flex;
.left {
flex: 1;
padding-left: 12px;
color: #666;
span {
color:#fa2209;
}
}
.tipsbtn {
width: 121px;
background: linear-gradient(90deg,#f9211c,#ff6335);
color: #fff;
text-align: center;
line-height: 46px;
display: block;
font-size: 14px;
}
}
</style>
2.【获取收货地址】查看文档,封装接口,页面调用,动态渲染
- 查看文档
- 封装接口
//新建api/address.js
import request from "@/utils/request"
//获取地址列表
export const getAddressList=()=>{
return request.get('/address/list')
}
- 页面调用
import { getAddressList } from '@/api/address'
export default {
name: 'PayIndex',
data () {
return {
addressList: []
}
},
computed: {
selectedAddress () {
// 由于地址管理非该项目的主线业务,直接获取第一个项作为选中的地址
return this.addressList[0] || {}
}
},
created () {
this.getAddressList()
},
methods: {
async getAddressList () {
const { data: { list } } = await getAddressList()// 调用
this.addressList = list
// console.log('获取收货地址:', list)// 取里面的第一项作为值渲染==>可以提供一个计算属性selectedAddress
}
}
}
- 动态渲染
<!-- 地址相关 -->
<div class="address">
......
<div class="info" v-if="selectedAddress.address_id">
<div class="info-content">
<span class="name">{{selectedAddress.name}}</span>
<span class="mobile">{{selectedAddress.phone}}</span>
</div>
<!-- 用计算属性拼接省市街道:江苏省 无锡市 南长街 110号 504 -->
<div class="info-address">
{{longAddress}}
</div>
</div>
......
</div>
computed: {
selectedAddress () {
// 由于地址管理非该项目的主线业务,直接获取第一个项作为选中的地址
return this.addressList[0] || {}
},
longAddress () {
const region = this.selectedAddress.region
return region.province + ' ' + region.city + ' ' + region.region//手动拼接地址
}
}
3.【获取订单信息-购物车结算】查看文档,封装接口,路由传参,页面调用,动态渲染
- 查看文档
订单信息的确认分为购物车结算(cart页=>pay页
)和立即购买结算(productDetail页=>pay页
),从请求参数mode的值来确认
购物车结算时,需要带上的请求参数是:mode:cart,cartsId
立即购买结算时,需要带上的请求参数是:mode:buyNow,goodsId,goodsNum,goodsSkuId
- 封装接口:(两种情况通用)
import request from '@/utils/request'
export const checkOrder = (mode, obj) => {
return request.get('/checkout.order', {
params: {
mode,
...obj, // 如何根据不同的mode值,传递不同的参数对象:动态展开
/* 非主线业务:给默认值 */
delivery: 10, // 10代表快速配送,20代表门店自提
couponId: 0, // 优惠券id,传参,默认不使用
isUsePoints: 0// 使用积分,默认不使用
}
})
}
Cart
页配置查询参数传参并路由跳转到pay
页
//cart.vue:传参
//绑定点击事件
<div v-if="!isEdit" @click="goPay" class="goPay" :disabled="selCount === 0">结算({{ selCount}})</div>
//定义点击事件
// 跳转结算页
goPay () {
// 先判断有没有选中商品(getters属性里封装的selCount或selCountList)
if (this.selCount > 0) {
// 查询参数传参(完整写法)
this.$router.push({
path: 'pay',
query: {
mode: 'cart',
// 得到一个当前选中的商品的id组成的新数组,再用join拼接成字符串就是我们想要的格式:70642.70643.70644.70661.70664
cartIds: this.selCartList.map(item => item.id).join('.')
}
})
}
}
//Pay/index.js:接收参数
computed:{
mode () { return this.$route.query.mode },
cartIds () { return this.$route.query.cartIds }
},
created () {
console.log('pay页获得的mode和cartIds:', this.cartIds, this.mode)//70661.70664.70666.70667 cart
}
Pay
页调用接口获取数据:用cart
页传过来的参数作为调用时的传参参数
import { checkOrder } from "@/api/order.js"
.....
data(){
return{
order:{},
personal:{}
}
},
created(){
this.checkOrderMethods()
},
methods:{
//获取订单列表
async checkOrderMethods(){
const { data :{ order, personal }}=checkOrder(this.mode,{cartIds: this.cartIds})
//console.log(res)
this.order=order
this.personal=personal
}
}
- 动态渲染:用获取到的
order
和personal
渲染页面数据
<!-- 订单明细 -->
<div class="pay-list" v-if="order.goodsList">
<div class="list">
<div class="goods-item" v-for="item in order.goodsList" :key="item.goods_id">
<div class="left">
<img :src="item.goods_image" alt="" />
</div>
<div class="right">
<p class="tit text-ellipsis-2">
{{ item.goods_name }}
</p>
<p class="info">
<span class="count">x{{ item.total_num }}</span>
<span class="price">¥{{ item.total_pay_price }}</span>
</p>
</div>
</div>
</div>
<div class="flow-num-box">
<span>共 {{ order.orderTotalNum }} 件商品,合计:</span>
<span class="money">¥{{ order.orderTotalPrice }}</span>
</div>
<div class="pay-detail">
<div class="pay-cell">
<span>订单总金额:</span>
<span class="red">¥{{ order.orderTotalPrice }}</span>
</div>
<div class="pay-cell">
<span>优惠券:</span>
<span>无优惠券可用</span>
</div>
<div class="pay-cell">
<span>配送费用:</span>
<span v-if="!selectAddress">请先选择配送地址</span>
<span v-else class="red">+¥0.00</span>
</div>
</div>
<!-- 支付方式 -->
<div class="pay-way">
<span class="tit">支付方式</span>
<div class="pay-cell">
<span><van-icon name="balance-o" />余额支付(可用 ¥ {{ personal.balance }} 元)</span>
<!-- <span>请先选择配送地址</span> -->
<span class="red"><van-icon name="passed" /></span>
</div>
</div>
<!-- 买家留言 -->
<div class="buytips">
<textarea placeholder="选填:买家留言(50字内)" name="" id="" cols="30" rows="10"></textarea>
</div>
</div>
<!-- 底部提交 -->
<div class="footer-fixed">
<div class="left">实付款:<span>¥{{ order.orderTotalPrice }}</span></div>
<div class="tipsbtn">提交订单</div>
</div>
4.【获取订单信息-立即购买结算】路由传参,页面调用,动态渲染,确认框封装到mixin
- 详情页踩坑纠正:由于要验证token,误将"加入购物车"和"立即购买"绑在了同一个点击事件handlePannel中,现纠正如下
/******************之前的错误代码*****************/
<!-- ActionSheet弹层 -->
<van-action-sheet @click='handlePannel' v-model="showPannel" :title="mode==='cart'?'加入购物车':'立即购买'">
<!-- v-if有库存 -->
<div class="showbtn" v-if="detail.stock_total>0">
<div class="btn" v-if="mode==='cart'">加入购物车</div>
<div class="btn" v-else>立即购买</div>
</div>
</van-action-sheet>
//在handlePannel事件中验证token
// 点击弹层中的按钮:"加入购物车"或"立即购买"
handlePannel () {
// 验证token是否存在
// console.log('我是token', this.userInfo.token)
if (!this.userInfo.token) { // 不存在:弹出提示框
// Dialog确认框
this.$dialog.confirm({
title: '温馨提示',
message: '此时需要先登录才能继续操作哦',
confirmButtomText: '去登录',
cancelButtonText: '再逛逛'
}).then(() => {
this.$router.replace({
path: '/login',
query: {
backUrl: this.$route.fullPath
}
})
}).catch(() => {})
}
this.addCartMethods()
this.$toast(this.msg)
this.showPannel = false
}
/******************现在的纠正代码*****************/
<!-- ActionSheet弹层 -->
<van-action-sheet v-model="showPannel" :title="mode==='cart'?'加入购物车':'立即购买'">
<!-- v-if有库存 -->
<div class="showbtn" v-if="detail.stock_total>0">
<div class="btn" v-if="mode==='cart'" @click='handleAddCart'>加入购物车</div>
<div class="btn" v-else @click='handleBuyNow'>立即购买</div>
</div>
</van-action-sheet>
// 点击弹层中的按钮:"加入购物车"
handleAddCart () {
// 验证token是否存在
// console.log('我是token', this.userInfo.token)
if (!this.userInfo.token) { // 不存在:弹出提示框
// Dialog确认框
this.$dialog.confirm({
title: '温馨提示',
message: '此时需要先登录才能继续操作哦',
confirmButtomText: '去登录',
cancelButtonText: '再逛逛'
}).then(() => {
this.$router.replace({
path: '/login',
query: {
backUrl: this.$route.fullPath
}
})
}).catch(() => {})
}
this.addCartMethods()
this.$toast(this.msg)
this.showPannel = false
},
// 点击弹层中的按钮:"立即购买"
handleBuyNow(){
....后面写...
}
ProductDetail
页配置查询参数传参并路由跳转到pay
页
// 点击弹层中的按钮:"立即购买"
/****待优化:可以把两个函数共同有的确认框验证token部分提取封装****/
handleBuyNow () {
if (!this.userInfo.token) { // 不存在:弹出提示框
// Dialog确认框
this.$dialog.confirm({
title: '温馨提示',
message: '此时需要先登录才能继续操作哦',
confirmButtomText: '去登录',
cancelButtonText: '再逛逛'
}).then(() => {
this.$router.replace({
path: '/login',
query: {
backUrl: this.$route.fullPath
}
})
}).catch(() => {})
}
this.$router.push({
path: '/pay',
query: {
mode: 'buyNow',
goodsId: this.goodsId,
goodsSkuId: this.detail.skuList[0].goods_sku_id,
goodsNum: this.addCount
}
})
},
Pay
页调用接口获取数据:用ProductDetail
页传过来的参数作为调用时的传参参数
此时pay页已经渲染了从购物车结算的情况,需对pay页进行修改如下
computed:{
goodsId(){return this.$route.query.goodsId},
goodsNum(){return this.$route.query.goodsNum},
goodsSkuId(){return this.$route.query.goodsSkuId},
}
//methods:
// 获取订单信息
async getOrderList () {
if (this.mode === 'cart') { // 购物车结算
const { data: { order, personal } } = await checkOrder(this.mode, { cartIds: this.cartIds })
this.order = order
this.personal = personal
}
if (this.mode === 'buyNow') { // 立刻购买结算
const { data: { order, personal } } = await checkOrder(this.mode, { goodsId: this.goodsId, goodsNum: this.goodsNum, goodsSkuId: this.goodsSkuId })
this.order = order
this.personal = personal
}
}
- 优化:把详情页中"加入购物车"和"立即购买"的确认框封装起来
使用到了mixin混入
step1:新建src/mixins/loginConfirm.js,
并把确认登录的代码剪切到该文件中
//src/mixins/loginConfirm.js
export default {
methods: {
loginConfirm () { // 登录确认框
if (!this.userInfo.token) { // token不存在==>弹确认框
// 页面中使用Dialog框
this.$dialog.confirm({
title: '温馨提示',
message: '此时需要先登录才能继续操作哦',
confirmButtonText: '去登录',
cancelButtonText: '再逛逛'
})
.then(() => {
this.$router.push({
path: '/login',
query: {
backUrl: this.$router.fullPath
}
})
})
.catch = () => {}
return true// 弹出了
}
return false// 没弹出
}
}
}
step2:导入并在剪切的原位置使用mixiin中封装的方法
//ProductDetail/index.vue
import loginConfirm from '@/mixins/loginConfirm'
export default {
mixin: [loginConfirm], // 里面不要加引号!
methods:{
hendleAddCart(){
if(this.loginConfirm()) true
...
},
hendleBuyNow(){
if(this.loginConfirm()) true
...
}
}
}
5.【提交订单并支付-两种结算】查看文档,封装请求,页面注册事件并调用请求
- 查看文档,确认请求方式,请求路径,请求参数
参数同上(都是分两种情况) - 封装接口
//api/order.js
// 提交订单
export const submitOrder = (mode, obj) => {
return request.post('/checkout/submit', {
mode,
...obj,
/* 非主线业务, */
delivery: 10, // 10代表快速配送,20代表门店自提
couponId: 0, // 优惠券id,传参,默认不使用
isUsePoints: 0, // 使用积分,默认不使用
payType: 10// 余额支付
})
}
. 给页面中的提交订单按钮绑定点击事件,并调用接口
(再加上留言属性remark)
//Pay/index.vue
<!-- 买家留言 -->
<div class="buytips">
<textarea v-model="remark" placeholder="选填:买家留言(50字内)" name="" id="" cols="30" rows="10"></textarea>
</div>
</div>
<!-- 底部提交 -->
<div class="footer-fixed">
<div class="left">实付款:<span>¥999919</span></div>
<div class="tipsbtn" @click='submitOrder'>提交订单</div>
</div>
......
import { checkOrder, submitOrder } from '@/api/order'
......
data:
remark:''
methods:
// 提交订单
async submitOrder () {
if (this.mode === 'cart') { // 购物车结算
await submitOrder(this.mode, {
cartIds: this.cartIds,
remark: this.remark
})
}
if (this.mode === 'buyNow') { // 立刻购买结算
await submitOrder(this.mode, {
goodsId: this.goodsId,
goodsNum: this.goodsNum,
goodsSkuId: this.goodsSkuId,
remark: this.remark
})
}
this.$toast.success('支付成功')
this.$router.replace('/myorder')
}
二.订单管理页的实现
前置要求:
- 在
utils/vant-ui,js
按需引入Tabs和Tab组件 - 创建
components/OrderListItem.vue
子组件
1.静态布局
MyOrder/index.vue
<template>
<div class="order">
<van-nav-bar title="我的订单" left-arrow @click-left="$router.go(-1)" />
<van-tabs v-model="active">
<van-tab title="全部"></van-tab>
<van-tab title="待支付"></van-tab>
<van-tab title="待发货"></van-tab>
<van-tab title="待收货"></van-tab>
<van-tab title="待评价"></van-tab>
</van-tabs>
<OrderListItem></OrderListItem>
</div>
</template>
<script>
import OrderListItem from '@/components/OrderListItem.vue'
export default {
name: 'OrderPage',
components: {
OrderListItem
},
data () {
return {
active: 0
}
}
}
</script>
<style lang="less" scoped>
.order {
background-color: #fafafa;
}
.van-tabs {
position: sticky;
top: 0;
}
</style>
components/OrderListItem.vue
<template>
<div class="order-list-item">
<div class="tit">
<div class="time">2023-07-01 12:02:13</div>
<div class="status">
<span>待支付</span>
</div>
</div>
<div class="list">
<div class="list-item">
<div class="goods-img">
<img src="http://cba.itlike.com/public/uploads/10001/20230321/c4b5c61e46489bb9b9c0630002fbd69e.jpg" alt="">
</div>
<div class="goods-content text-ellipsis-2">
Apple iPhone 14 Pro Max 256G 银色 移动联通电信5G双卡双待手机
</div>
<div class="goods-trade">
<p>¥ 1299.00</p>
<p>x 3</p>
</div>
</div>
<div class="list-item">
<div class="goods-img">
<img src="http://cba.itlike.com/public/uploads/10001/20230321/c4b5c61e46489bb9b9c0630002fbd69e.jpg" alt="">
</div>
<div class="goods-content text-ellipsis-2">
Apple iPhone 14 Pro Max 256G 银色 移动联通电信5G双卡双待手机
</div>
<div class="goods-trade">
<p>¥ 1299.00</p>
<p>x 3</p>
</div>
</div>
<div class="list-item">
<div class="goods-img">
<img src="http://cba.itlike.com/public/uploads/10001/20230321/c4b5c61e46489bb9b9c0630002fbd69e.jpg" alt="">
</div>
<div class="goods-content text-ellipsis-2">
Apple iPhone 14 Pro Max 256G 银色 移动联通电信5G双卡双待手机
</div>
<div class="goods-trade">
<p>¥ 1299.00</p>
<p>x 3</p>
</div>
</div>
</div>
<div class="total">
共12件商品,总金额 ¥29888.00
</div>
<div class="actions">
<span v-if="false">立刻付款</span>
<span v-if="true">申请取消</span>
<span v-if="false">确认收货</span>
<span v-if="false">评价</span>
</div>
</div>
</template>
<script>
export default {
}
</script>
<style lang="less" scoped>
.order-list-item {
margin: 10px auto;
width: 94%;
padding: 15px;
background-color: #ffffff;
box-shadow: 0 0.5px 2px 0 rgba(0,0,0,.05);
border-radius: 8px;
color: #333;
font-size: 13px;
.tit {
height: 24px;
line-height: 24px;
display: flex;
justify-content: space-between;
margin-bottom: 20px;
.status {
color: #fa2209;
}
}
.list-item {
display: flex;
.goods-img {
width: 90px;
height: 90px;
margin: 0px 10px 10px 0;
img {
width: 100%;
height: 100%;
}
}
.goods-content {
flex: 2;
line-height: 18px;
max-height: 36px;
margin-top: 8px;
}
.goods-trade {
flex: 1;
line-height: 18px;
text-align: right;
color: #b39999;
margin-top: 8px;
}
}
.total {
text-align: right;
}
.actions {
text-align: right;
span {
display: inline-block;
height: 28px;
line-height: 28px;
color: #383838;
border: 0.5px solid #a8a8a8;
font-size: 14px;
padding: 0 15px;
border-radius: 5px;
margin: 10px 0;
}
}
}
</style>
2.查看文档并封装接口
//api/order.js
// 【myOrder页】获取订单列表
export const getMyOrderList = (dataType, page) => {
return request.get('/order/list', {
params: {
dataType,
page
}
})
}
3.给tab添加name属性,页面调用接口
<template>
<div class="order">
<van-nav-bar title="我的订单" left-arrow @click-left="$router.go(-1)" />
<!-- sticky:使用粘性定位布局 -->
<van-tabs v-model="active" sticky>
<van-tab name='all' title="全部"></van-tab>
<van-tab name='payment' title="待支付"></van-tab>
<van-tab name='delivery' title="待发货"></van-tab>
<van-tab name='received' title="待收货"></van-tab>
<van-tab name='comment' title="待评价"></van-tab>
</van-tabs>
<OrderListItem></OrderListItem>
</div>
</template>
<script>
import OrderListItem from '@/components/OrderListItem.vue'
import { getMyOrderList } from '@/api/order'
export default {
name: 'OrderPage',
components: {
OrderListItem
},
data () {
return {
active: this.$route.query.dataType || 'all',
page: 1,
list: []
}
},
methods: {
async getMyOrderListMethods () {
const { data: { list } } = await getMyOrderList(this.active, this.page)
list.data.forEach(item => {
item.total_num = 0
item.goods.forEach(goods => {
item.total_num += goods.total_num
})
})
this.list = list.data
}
},
watch: {
active: {
immediate: true,
handler () {
this.getMyOrderListMethods()
}
}
}
}
</script>
4.动态渲染:父传子并渲染OrderListItem子组件
父:MyOrder页
<OrderListItem v-for="item in list" :key="item.order_id" :item="item"></OrderListItem>
子:OrderListItem页
<template>
<div class="order-list-item" v-if="item.order_id">
<div class="tit">
<div class="time">{{ item.create_time }}</div>
<div class="status">
<span>{{ item.state_text }}</span>
</div>
</div>
<div class="list" >
<div class="list-item" v-for="(goods, index) in item.goods" :key="index">
<div class="goods-img">
<img :src="goods.goods_image" alt="">
</div>
<div class="goods-content text-ellipsis-2">
{{ goods.goods_name }}
</div>
<div class="goods-trade">
<p>¥ {{ goods.total_pay_price }}</p>
<p>x {{ goods.total_num }}</p>
</div>
</div>
</div>
<div class="total">
共 {{ item.total_num }} 件商品,总金额 ¥{{ item.total_price }}
</div>
<div class="actions">
<div v-if="item.order_status === 10">
<span v-if="item.pay_status === 10">立刻付款</span>
<span v-else-if="item.delivery_status === 10">申请取消</span>
<span v-else-if="item.delivery_status === 20 || item.delivery_status === 30">确认收货</span>
</div>
<div v-if="item.order_status === 30">
<span>评价</span>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
item: {
type: Object,
default: () => {
return {}
}
}
}
}
</script>
三.个人中心页的实现
个人中心页是嵌套在Layout下的二级路由User.vue
1.查看文档,封装接口
//api/user.js
import request from '@/utils/request'
// 获取个人信息
export const getUserInfoDetail = () => {
request.get('/user/info')
}
2.页面调用,动态渲染
User.vue
完整代码(此处如没有avatar.png本地图片,去网页上找默认用户头像,复制在线URL即可)
<template>
<div class="user">
<div class="head-page" v-if="isLogin">
<div class="head-img">
<img src="https://p1.ssl.qhimgs1.com/sdr/400__/t01d81c9f80f26024b7.jpg" alt="" />
</div>
<div class="info">
<div class="mobile">{{ detail.mobile }}</div>
<div class="vip">
<van-icon name="diamond-o" />
普通会员
</div>
</div>
</div>
<div v-else class="head-page" @click="$router.push('/login')">
<div class="head-img">
<img src="https://p1.ssl.qhimgs1.com/sdr/400__/t01d81c9f80f26024b7.jpg" alt="" />
</div>
<div class="info">
<div class="mobile">未登录</div>
<div class="words">点击登录账号</div>
</div>
</div>
<div class="my-asset">
<div class="asset-left">
<div class="asset-left-item">
<span>{{ detail.pay_money || 0 }}</span>
<span>账户余额</span>
</div>
<div class="asset-left-item">
<span>0</span>
<span>积分</span>
</div>
<div class="asset-left-item">
<span>0</span>
<span>优惠券</span>
</div>
</div>
<div class="asset-right">
<div class="asset-right-item">
<van-icon name="balance-pay" />
<span>我的钱包</span>
</div>
</div>
</div>
<div class="order-navbar">
<div class="order-navbar-item" @click="$router.push('/myorder?dataType=all')">
<van-icon name="balance-list-o" />
<span>全部订单</span>
</div>
<div class="order-navbar-item" @click="$router.push('/myorder?dataType=payment')">
<van-icon name="clock-o" />
<span>待支付</span>
</div>
<div class="order-navbar-item" @click="$router.push('/myorder?dataType=delivery')">
<van-icon name="logistics" />
<span>待发货</span>
</div>
<div class="order-navbar-item" @click="$router.push('/myorder?dataType=received')">
<van-icon name="send-gift-o" />
<span>待收货</span>
</div>
</div>
<div class="service">
<div class="title">我的服务</div>
<div class="content">
<div class="content-item">
<van-icon name="records" />
<span>收货地址</span>
</div>
<div class="content-item">
<van-icon name="gift-o" />
<span>领券中心</span>
</div>
<div class="content-item">
<van-icon name="gift-card-o" />
<span>优惠券</span>
</div>
<div class="content-item">
<van-icon name="question-o" />
<span>我的帮助</span>
</div>
<div class="content-item">
<van-icon name="balance-o" />
<span>我的积分</span>
</div>
<div class="content-item">
<van-icon name="refund-o" />
<span>退换/售后</span>
</div>
</div>
</div>
<div class="logout-btn">
<button>退出登录</button>
</div>
</div>
</template>
<script>
import { getUserInfoDetail } from '@/api/user.js'
export default {
name: 'UserPage',
data () {
return {
detail: {}
}
},
created () {
if (this.isLogin) {
this.getUserInfoDetail()
}
},
computed: {
isLogin () {
return this.$store.getters.token
}
},
methods: {
async getUserInfoDetail () {
const { data: { userInfo } } = await getUserInfoDetail()
this.detail = userInfo
console.log(this.detail)
}
}
}
</script>
<style lang="less" scoped>
.user {
min-height: 100vh;
background-color: #f7f7f7;
padding-bottom: 50px;
}
.head-page {
height: 130px;
background: url("http://cba.itlike.com/public/mweb/static/background/user-header2.png");
background-size: cover;
display: flex;
align-items: center;
.head-img {
width: 50px;
height: 50px;
border-radius: 50%;
overflow: hidden;
margin: 0 10px;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
}
.info {
.mobile {
margin-bottom: 5px;
color: #c59a46;
font-size: 18px;
font-weight: bold;
}
.vip {
display: inline-block;
background-color: #3c3c3c;
padding: 3px 5px;
border-radius: 5px;
color: #e0d3b6;
font-size: 14px;
.van-icon {
font-weight: bold;
color: #ffb632;
}
}
}
.my-asset {
display: flex;
padding: 20px 0;
font-size: 14px;
background-color: #fff;
.asset-left {
display: flex;
justify-content: space-evenly;
flex: 3;
.asset-left-item {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
span:first-child {
margin-bottom: 5px;
color: #ff0000;
font-size: 16px;
}
}
}
.asset-right {
flex: 1;
.asset-right-item {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.van-icon {
font-size: 24px;
margin-bottom: 5px;
}
}
}
}
.order-navbar {
display: flex;
padding: 15px 0;
margin: 10px;
font-size: 14px;
background-color: #fff;
border-radius: 5px;
.order-navbar-item {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 25%;
.van-icon {
font-size: 24px;
margin-bottom: 5px;
}
}
}
.service {
font-size: 14px;
background-color: #fff;
border-radius: 5px;
margin: 10px;
.title {
height: 50px;
line-height: 50px;
padding: 0 15px;
font-size: 16px;
}
.content {
display: flex;
justify-content: flex-start;
flex-wrap: wrap;
font-size: 14px;
background-color: #fff;
border-radius: 5px;
.content-item {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 25%;
margin-bottom: 20px;
.van-icon {
font-size: 24px;
margin-bottom: 5px;
color: #ff3800;
}
}
}
}
.logout-btn {
button {
width: 60%;
margin: 10px auto;
display: block;
font-size: 13px;
color: #616161;
border-radius: 9px;
border: 1px solid #dcdcdc;
padding: 7px 0;
text-align: center;
background-color: #fafafa;
}
}
</style>
3.用户退出登录
思路:用户退出登录时,要把存入的用户信息清空,把购物车商品列表也清空
- 新建module/user.js中的actions方法,异步清空数据
//module/user.js
actions: {
logoutActions (ctx) {
ctx.commit('setUserInfo', {})// 清空用户信息
ctx.commit('/cart/setCartListMethods', [], { root: true })// 清空购物车商品数据
}
}
- 页面"退出登录"按钮中绑定点击事件,事件内调用该actions方法
此处用到了vuex的跨模块访问
<div class="logout-btn">
<button @click="logout">退出登录</button>
</div>
......
logout () {
// 弹出确认框
this.$dialog.confirm({
title: '温馨提示',
message: '你确认要退出么?'
})
.then(() => {
this.$store.diapatch('user/logoutActions')
})
.catch(() => {})
}
四.项目优化和项目打包
1.为什么要打包项目?
Vue脚手架仅在开发过程中协助开发,并不参与上线,
因为脚手架中包含许多开发时的依赖(如node),还有浏览器无法识别的语法(如ES6)
打包的作用就在于,可以:
- 将多个文件雅俗合并成一个文件==>减少请求次数,减轻服务器压力
- 语法降级
- 解析less,sass,typescript语法为css和js代码
2.打包的流程是怎么样的?
vue2项目使用打包命令进行编译,最终生成浏览器能直接识别和运行的网页,被称为需要上线的源码(包括css,img,js文件夹,index.html等)
*上线:打包后的代码上传到运行在公网环境下的服务器中
打包后生成dist文件夹
3.打包的命令和配置有哪些?
命令:
vue脚手架工具提供了打包命令:根目录下执行:yarn build命令,会在项目根目录下自动创建一个文件夹dist,包含了打包后的文件,只需要放到服务器即可
配置:
默认情况下,需要放到服务器根目录打开,如希望双击运行,需要配置publicPath配成相对路径
//vue.config.js
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
publicPath: './',
transpileDependencies: true
})
此时把项目放在任意盘符任意路径下都能运行
4.打包优化:路由懒加载
为什么要打包优化?
当打包构建应用时,js包会变得非常大,影响页面加载,
如果能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,无疑会更高效
如何实现打包优化?
step1:异步组件改造
示例:import Login from "@/view/login"
改成:const login=()=>import('@/views/login')
step2:路由中应用
import router=new VueRouter({
routes:[
...
{path:'productdetail',component:ProductDetail},
{path:'pay',component:Pay}
]
})
step3:重新打包yarn build
如何取舍首次加载的组件?
跟首页相关的自动加载:Layout,Home,Category,Cart,user
,其他的都按需加载
如何验证效果和前后对比?
前:控制台>network>(切换页面不会新加载任何组件,一开始已经加载过了)
后:切换页面,不断新加载js
五.知识点
1. Vue中的mixin
在 Vue 生态中,Mixin 提供了一种非常强大的机制,允许开发者提取公共逻辑并将其注入到任意数量的组件中。无论是数据 (data)、计算属性 (computed)、方法 (methods) 还是生命周期钩子都可以通过 Mixin 共享。
- 特性说明
生命周期钩子合并:当组件和 Mixin 都定义了相同的生命周期钩子时,两者都会被执行,且 Mixin 的钩子优先于组件本身的钩子执行4。
数据冲突解决:如果 Mixin 和组件同时定义了同名的数据属性,则组件自身的定义会覆盖 Mixin 的定义;对于嵌套的对象类型数据,Vue 会对它们进行递归合并
用法说明:
export default {
//此处编写vue组件实例的配置项data,computed,methods等等,通过一定语法,可以直接混入到组件内部
注意事项
//1.若配置项提供了与组件同名的data属性或methods方法(等),那么组件内的优先级比这个mixin的更高
//2.如果编写了生命周期函数,则mixin中的钩子和页面的钩子会用数组管理起来统一执行
}
- 总结
Mixin 在前端开发中扮演着重要角色,尤其是在需要跨组件共享逻辑时显得尤为有用。然而需要注意的是,过度依赖 Mixin 可能会使项目的结构变得难以维护,因此应谨慎权衡其使用的必要性和合理性
2.Vuex的跨模块访问
Vuex 跨模块调用 Action 和 Mutation
在 Vuex 中,跨模块访问是一个常见的需求。可以通过上下文中自带的对象来访问其他模块的内容。例如,在 actions 上下文中可以直接通过 commit 方法指定目标模块名称以及对应的 mutation
一句话总结
action方法在正常的提交mutation语句中,添加一句{root:true}
,代表调用的是全局的mutations方法
(前提是那个子模块开启了命名空间,即该模块的 namespaced 属性为 true)
示例:
//modules/user.js
actions: {
logoutActions (ctx) {
ctx.commit('setUserInfo', {})// 调用本模块mutations方法
ctx.commit('/cart/setCartListMethods', [], { root: true })// 调用cart模块mutations方法
}
}
//modules/cart.js
export default {
namespaced: true,//必须开启命名空间
mutations:{
setCartListMethods(){
...
}
}
}