Skip to content
本页目录

9. 结算

9.1 结算页

9.1.1 静态页面构建

地址区域

vue
<!-- 地址区域 -->
<view class="address-wrap" @click="goAddress">
    <!-- 正常信息 -->
    <template v-if="address.userName">
        <!-- 姓名/手机号信息 -->
        <view class="name">
            <text>{{ address.userName }}</text>
            <text>{{ address.userPhone }}</text>
        </view>
        <!-- 地址信息 -->
        <view class="address">{{ addressInfo }}</view>
    </template>

    <!-- 空信息 -->
    <view class="empty" v-else>
        <van-icon class="warning" name="warning" />
        <text>请选择收货地址</text>
    </view>
    <!-- 右箭头图标 -->
    <van-icon class="arrow" name="arrow" />
</view>

商品清单区域

vue
<!-- 商品清单区域 -->
<view class="goods-wrap">
    <view class="goods-item" v-for="item in cartList" :key="item.cartItemId">
        <view class="goods-img"><image :src="item.goodsCoverImg" mode=""></image></view>
        <view class="goods-desc">
            <view class="goods-title">
                <text>{{ item.goodsName }}</text>
                <text>x{{ item.goodsCount }}</text>
            </view>
            <view class="goods-btn">
                <view class="price">¥{{ item.sellingPrice }}</view>
            </view>
        </view>
    </view>
</view>

支付区域

vue
<!-- 支付区域 -->
<view class="pay-wrap">
    <view class="price">
        <text>商品金额</text>
        <text>¥{{ totol }}</text>
    </view>
    <van-button @click="handleCreateOrder" class="pay-btn" color="#1baeae" type="primary" block>生成订单</van-button>
</view>

9.1.2 UI样式

scss

page {
	background-color: #f9f9f9;
}
.address-wrap {
	margin-bottom: 40rpx;
	background-color: #fff;
	position: relative;
	font-size: 28rpx;
	padding: 30rpx;
	color: #222333;
	.empty {
		margin: 40rpx 0;
		font-size: 30rpx;
		.warning {
			margin-right: 20rpx;
		}
	}
	.name,
	.address {
		margin: 20rpx 0;
		text {
			margin-right: 6rpx;
		}
	}
	.arrow {
		position: absolute;
		right: 20rpx;
		top: 50%;
		transform: translateY(-50%);
		font-size: 40rpx;
	}
	&::before {
		content: '';
		position: absolute;
		right: 0;
		bottom: 0;
		left: 0;
		height: 4rpx;
		background-image: repeating-linear-gradient(-45deg, #ff6c6c 0, #ff6c6c 20%, transparent 0, transparent 25%, #1989fa 0, #1989fa 45%, transparent 0, transparent 50%);
		background-size: 160rpx;
	}
}

.goods-wrap {
	padding-bottom: 230rpx;
	.goods-item {
		padding: 20rpx;
		background-color: #fff;
		display: flex;
		.goods-img {
			image {
				@include wd(200rpx);
			}
		}
		.goods-desc {
			@include fj();
			flex-direction: column;
			flex: 1;
			padding: 40rpx;
			.goods-title {
				@include fj();
			}
			.goods-btn {
				@include fj();
				.price {
					font-size: 32rpx;
					color: red;
					line-height: 56rpx;
				}
			}
		}
	}
}

.pay-wrap {
	position: fixed;
	bottom: 0;
	left: 0;
	right: 0;
	width: 100%;
	background: #fff;
	padding: 20rpx 0;
	border-top: 2rpx solid #e9e9e9;
	box-sizing: border-box;
	.price {
		@include fj();
		padding: 0 5%;
		margin: 20rpx 0;
		font-size: 28rpx;
		text:nth-child(2) {
			color: red;
			font-size: 36rpx;
		}
	}
	.pay-btn .van-button {
		width: 90%;
		margin: 0 auto;
	}
}

.pay-view {
	width: 90%;
	margin: 0 auto;
	padding-top: 100rpx;
	.van-button {
		margin-bottom: 20rpx;
	}
}

9.1.3 生成对应订单信息

  • 获取 store中选中商品信息 需要获取选中商品

    js
    const { data: list } = await getByCartItemIds({ cartItemIds: cartStore.cartItemIds })
    
  • 页面显示时加载数据

    js
    onShow(() => {
    	// 初始化数据
    	init()
    })
    
  • 完整init函数

    js
    // 初始化数据
    const init = async () => {
    	try {
    		// 1. 加载数据
    		uni.showLoading({ title: '加载中...', mask: true })
    		// 2. 查询数据 【订单信息 / 地址信息】
    		const { data: list } = await getByCartItemIds({ cartItemIds: cartStore.cartItemIds })
    		const { data: address } = cartStore.addressId ? await getAddressDetail(cartStore.addressId) : await getDefaultAddress()
    		state.cartList = list
    		state.address = address || {}
    		uni.hideLoading()
    		console.log(address)
    	} catch (e) {
    		// 3. 处理错误
    		state.address = {}
    	}
    }
    
  • 计算商品价格

    js
    // 总价格
    const totol = computed(() => state.cartList.reduce((sum, item) => sum + item.goodsCount * item.sellingPrice, 0))
    

9.2 地址页

页面逻辑: 用户可自行选择 地址 默认加载用户默认地址 若用户选择地址则加载选择地址信息

9.2.1 地址列表

页面静态结构

vue
<view class="address-box">
    <!-- 地址列表区域 -->
    <view class="address-list">
        <view class="address-item" v-for="item in addressList" :key="item.addressId">
            <view class="content" @click="select(item.addressId)">
                <view class="name">{{ item.userName }} {{ item.userPhone }} <van-tag v-if="item.defaultFlag" type="danger" round>默认</van-tag></view>
                <view class="address">{{ item.provinceName }} {{ item.cityName }} {{ item.regionName }} {{ item.detailAddress }}</view>
            </view>
            <van-icon name="edit" class="address-edit" @click="onEdit(item.addressId)"/>
        </view>
    </view>

    <!-- 新增地址区域 -->
    <view class="address-bottom" @click="onAdd"><van-button block round color="#1baeae">新增地址</van-button></view>
</view>

页面UI样式

scss
.address-list {
	box-sizing: border-box;
	height: 100%;
	padding: 24rpx 24rpx 160rpx;
	.address-item:not(:last-child) {
		margin-bottom: 24rpx;
	}
	.address-item {
	    padding: 24rpx;
	    background-color: #fff;
	    border-radius: 16rpx;
		position: relative;
		display: flex;
		box-sizing: border-box;
		width: 100%;
		overflow: hidden;
		color: #232323;
		background-color: #FFF;
		font-size: 28rpx;
		line-height: 48rpx;
		.content {
			flex: 1;
			padding-right: 88rpx;
			.name {
				display: flex;
				align-items: center;
				margin-bottom: 16rpx;
				font-size: 32rpx;
				line-height: 44rpx;
				.van-tag {
					margin-left: 16rpx;
					padding-top: 0;
					padding-bottom: 0;
					flex: 0;
					line-height: 34rpx !important;
					transform: translateY(-2rpx);
				
				}
			}
			.address {
				color: #323233;
				font-size: 26rpx;
				line-height: 36rpx;
			}
		}
		.address-edit {
			position: absolute;
			top: 50%;
			right: 32rpx;
			color: #969799;
			font-size: 40rpx;
			transform: translateY(-50%);
		}
	}
}
.address-bottom {
	position: fixed;
	bottom: 0;
	left: 0;
	z-index: 999;
	box-sizing: border-box;
	width: 100%;
	padding: 0 32rpx;
	background-color: #fff;
	.van-button {
		margin: 10rpx 0;
	}
}

页面逻辑

  1. 页面显示时加载我的地址列表

    js
    // 页面显示
    onShow(async () => {
    	// 加载我的地址列表
    	const { data } = await getAddressList()
    	state.addressList = data
    })
    
  2. 设置新增/编辑地址跳转

    js
    // * 新增地址
    const onAdd = () => {
    	uni.navigateTo({ url: '/subpkg/address-edit/address-edit?type=add' })
    }
    
    // * 修改地址
    const onEdit = (id) => {
    	uni.navigateTo({ url: `/subpkg/address-edit/address-edit?type=edit&addressId=${id}` })
    }
    
  3. 设置选中地址

    js
    // * 选择地址
    const select = (id) => {
    	// 来源为mine我的 则不进行跳转
    	if (state.from === 'mine') return
    	cartStore.addressId = id
    	uni.navigateBack({ url: `/subpkg/createOrder/createOrder` })
    }
    

    将选中的地址保存到Store中 供结算页使用

9.2.2 地址编辑

页面结构

vue
<view class="address-edit">
		<view class="content">
			<!-- 输入框组 -->
			<van-cell-group>
				<van-field data-name="userName" :value="formInput.userName" @change="handleValue" label="姓名" placeholder="收货人姓名" clearable />

				<van-field data-name="userPhone" :value="formInput.userPhone" @change="handleValue" type="number" label="电话" placeholder="收货人手机号" clearable />

				<van-cell title="地区">
					<picker mode="region" :value="region" @change="changeRegion">
						<view v-if="!region.length" class="city">选择省 / 市 / 区</view>
						<view v-else class="city2">{{ region.join('/') }}</view>
					</picker>
				</van-cell>

				<van-field
					data-name="detailAddress"
					:value="formInput.detailAddress"
					@change="handleValue"
					type="textarea"
					label="详细地址"
					placeholder="街道门排, 楼层房间号等信息"
					clearable
				/>

				<van-cell class="default" title="设为默认收货地址"><switch :checked="formInput.defaultFlag" @change="changeDefault" /></van-cell>
			</van-cell-group>

			<van-button :loading="loading" color="#1baeae" round block @click="onSave">保存</van-button>

			<van-button v-if="state.type === 'edit'" round block @click="onDelete">删除</van-button>
		</view>
	</view>
  • 输入框使用vant输入框 进行UI布局
  • 地址 省市区选择 使用原生Picker组件

页面UI样式

scss
.content {
	padding: 24rpx;
	.van-cell__title {
		max-width: 180rpx !important;
		color: #646566;
		margin-right: 24rpx;
	}
	.default .van-cell__title {
		max-width: 100% !important;
	}
	.city {
		color: #ccc;
		text-align: left;
	}
	.city2 {
		color: #000;
		text-align: left;
	}
	switch {
		transform: scale(0.8);
	}
	.van-button {
		margin-top: 36rpx;
	}
}

页面逻辑

完成基础版本的双向绑定

  • 输入框双向绑定

    js
    // 输入框双向绑定 **
    const handleValue = e => {
    	const { name } = e.target.dataset
    	const value = e.detail
    	 state.formInput[name] = value
    }
    
  • 省市区

    js
    // 选择省市区
    const changeRegion = e => {
    	const value = e.detail.value
    	state.region = value
    	state.formInput.provinceName = value[0]
    	state.formInput.cityName = value[1]
    	state.formInput.regionName = value[2]
    }
    
  • 选择默认

    js
    // 选择默认
    const changeDefault = e => {
    	const isCheck = e.detail.value
    	state.formInput.defaultFlag = isCheck ? 1 : 0
    }
    

**页面初始化加载地址信息 **

js
// 页面加载
onLoad(async (message) => {
	// 1. 获取参数
	const { type, addressId } = message
	uni.setNavigationBarTitle({ title: type === 'add' ? '新增地址' : '修改地址' })
	state.type = type
	// 2. 初始化地址
	if (type !== 'add') {
		const { data } = await getAddressDetail(addressId)
		state.formInput = data
		state.region = [data.provinceName, data.cityName, data.regionName]
	}
})

保存地址

js
// * 保存地址
const onSave = async () => {
	// 判断值的有效性
	const { userName, userPhone, detailAddress } = state.formInput
	if (!userName.trim().length) return uni.$Toast('姓名不能为空')
	if (!/1[3456789]\d{9}/.test(userPhone)) return uni.$Toast('请填写正确的手机号')
	if (!state.region.length) return uni.$Toast('请选择省市区')
	if (detailAddress.length < 6) return uni.$Toast('详情地址最少6个字符')
	state.loading = true
	try{
		await state.type === 'add' ? addAddress(state.formInput) : EditAddress(state.formInput)
		uni.$Toast('保存成功')
		setTimeout(() => {
			uni.navigateBack()
		}, 500)
		state.loading = false
	}catch(e){
		state.loading = false
	}
}

删除地址

js
// * 删除地址
const onDelete = async () => {
	await DeleteAddress(state.formInput.addressId)
	uni.$Toast('删除成功')
	setTimeout(() => {
	uni.navigateBack()
	}, 500)
}

9.3 结算支付

结算弹窗层

vue
<!-- 弹出层 支付框 -->
<van-popup :show="showPay" closeable position="bottom" custom-style="height: 30%" @close="closePay">
    <view class="pay-view">
        <!-- 微信 2 其他 1 -->
        <van-button color="#4fc08d" block @click="handlePayOrder(2)">微信支付</van-button>
        <van-button color="#1989fa" block @click="handlePayOrder(1)">其他支付</van-button>
    </view>
</van-popup>

UI样式

scss
.pay-view {
	width: 90%;
	margin: 0 auto;
	padding-top: 100rpx;
	.van-button {
		margin-bottom: 20rpx;
	}
}

结算逻辑

无论是否支付 点击生成订单后 都将跳转订单页

js

// 生成订单
const handleCreateOrder = async () => {
	// 校验是否有地址
	if (!state.address.addressId) return uni.$Toast('请先选择收货地址')
	
	// 1. 准备参数
	const params = {
		addressId: state.address.addressId,
		cartItemIds: state.cartList.map(item => item.cartItemId)
	}
	// 2. 发起请求
	const { data } = await createOrder(params)
	console.log(data)
	state.orderNo = data
	state.showPay = true
}

// 支付订单
const handlePayOrder = async (type) => {
	await payOrder({ orderNo: state.orderNo, payType: type })
	uni.$Toast('支付成功')
	setTimeout(() => {
		uni.redirectTo({ url: '/subpkg/order/order' })
	}, 500)
}

// 关闭支付
const closePay = () => {
	uni.$Toast('你关闭了支付')
	state.showPay = false
	uni.redirectTo({ url: '/subpkg/order/order' })
}