危险

为之则易,不为则难

0%

12_结算

今日目标

✔ 结算。

结算页面布局和路由

  1. 定义组件基础结构,src/views/member/pay/checkout.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
<script lang="ts" setup name="XtxPayCheckoutPage"></script>

<template>
<div class="xtx-pay-checkout-page">
<div class="container">
<XtxBread>
<XtxBreadItem to="/">首页</XtxBreadItem>
<XtxBreadItem to="/cart">购物车</XtxBreadItem>
<XtxBreadItem>填写订单</XtxBreadItem>
</XtxBread>
<div class="wrapper">
<!-- 收货地址 -->
<h3 class="box-title">收货地址</h3>
<div class="box-body">
<div class="address">
<div class="text">
<ul>
<li><span>&ensp;&ensp;人:</span>朱超</li>
<li><span>联系方式:</span>132****2222</li>
<li><span>收货地址:</span>海南省三亚市解放路108号物质大厦1003室</li>
</ul>
<!-- <div class="none">您需要先添加收货地址才可提交订单。</div> -->
</div>
<div class="action">
<XtxButton class="btn">切换地址</XtxButton>
<XtxButton class="btn">添加地址</XtxButton>
</div>
</div>
</div>
<!-- 商品信息 -->
<h3 class="box-title">商品信息</h3>
<div class="box-body">
<table class="goods">
<thead>
<tr>
<th width="520">商品信息</th>
<th width="170">单价</th>
<th width="170">数量</th>
<th width="170">小计</th>
<th width="170">实付</th>
</tr>
</thead>
<tbody>
<tr v-for="item in 4" :key="item">
<td>
<a href="javascript:;" class="info">
<img src="https://yanxuan-item.nosdn.127.net/cd9b2550cde8bdf98c9d083d807474ce.png" alt="" />
<div class="right">
<p>轻巧多用锅雪平锅 麦饭石不粘小奶锅煮锅</p>
<p>颜色:白色 尺寸:10cm 产地:日本</p>
</div>
</a>
</td>
<td>&yen;100.00</td>
<td>2</td>
<td>&yen;200.00</td>
<td>&yen;200.00</td>
</tr>
</tbody>
</table>
</div>
<!-- 配送时间 -->
<h3 class="box-title">配送时间</h3>
<div class="box-body">
<a class="my-btn active" href="javascript:;">不限送货时间:周一至周日</a>
<a class="my-btn" href="javascript:;">工作日送货:周一至周五</a>
<a class="my-btn" href="javascript:;">双休日、假日送货:周六至周日</a>
</div>
<!-- 支付方式 -->
<h3 class="box-title">支付方式</h3>
<div class="box-body">
<a class="my-btn active" href="javascript:;">在线支付</a>
<a class="my-btn" href="javascript:;">货到付款</a>
<span style="color: #999">货到付款需付5元手续费</span>
</div>
<!-- 金额明细 -->
<h3 class="box-title">金额明细</h3>
<div class="box-body">
<div class="total">
<dl>
<dt>商品件数:</dt>
<dd>5件</dd>
</dl>
<dl>
<dt>商品总价:</dt>
<dd>¥5697.00</dd>
</dl>
<dl>
<dt><i></i>费:</dt>
<dd>¥0.00</dd>
</dl>
<dl>
<dt>应付总额:</dt>
<dd class="price">¥5697.00</dd>
</dl>
</div>
</div>
<!-- 提交订单 -->
<div class="submit">
<XtxButton type="primary">提交订单</XtxButton>
</div>
</div>
</div>
</div>
</template>

<style scoped lang="less">
.wrapper {
background: #fff;
padding: 0 20px;
.box-title {
font-size: 16px;
font-weight: normal;
padding-left: 10px;
line-height: 70px;
border-bottom: 1px solid #f5f5f5;
}
.box-body {
padding: 20px 0;
}
}
.address {
border: 1px solid #f5f5f5;
display: flex;
align-items: center;
.text {
flex: 1;
min-height: 90px;
display: flex;
align-items: center;
.none {
line-height: 90px;
color: #999;
text-align: center;
width: 100%;
}
> ul {
flex: 1;
padding: 20px;
li {
line-height: 30px;
span {
color: #999;
margin-right: 5px;
> i {
width: 0.5em;
display: inline-block;
}
}
}
}
> a {
color: @xtxColor;
width: 160px;
text-align: center;
height: 90px;
line-height: 90px;
border-right: 1px solid #f5f5f5;
}
}
.action {
width: 420px;
text-align: center;
.btn {
width: 140px;
height: 46px;
line-height: 44px;
font-size: 14px;
&:first-child {
margin-right: 10px;
}
}
}
}
.goods {
width: 100%;
border-collapse: collapse;
border-spacing: 0;
.info {
display: flex;
text-align: left;
img {
width: 70px;
height: 70px;
margin-right: 20px;
}
.right {
line-height: 24px;
p {
&:last-child {
color: #999;
}
}
}
}
tr {
th {
background: #f5f5f5;
font-weight: normal;
}
td,
th {
text-align: center;
padding: 20px;
border-bottom: 1px solid #f5f5f5;
&:first-child {
border-left: 1px solid #f5f5f5;
}
&:last-child {
border-right: 1px solid #f5f5f5;
}
}
}
}
.my-btn {
width: 228px;
height: 50px;
border: 1px solid #e4e4e4;
text-align: center;
line-height: 48px;
margin-right: 25px;
color: #666666;
display: inline-block;
&.active,
&:hover {
border-color: @xtxColor;
}
}
.total {
dl {
display: flex;
justify-content: flex-end;
line-height: 50px;
dt {
i {
display: inline-block;
width: 2em;
}
}
dd {
width: 240px;
text-align: right;
padding-right: 70px;
&.price {
font-size: 20px;
color: @priceColor;
}
}
}
}
.submit {
text-align: right;
padding: 60px;
border-top: 1px solid #f5f5f5;
}
// 对话框地址列表
.xtx-dialog {
.addressWrapper {
max-height: 500px;
overflow-y: auto;
}
.text {
flex: 1;
min-height: 90px;
display: flex;
align-items: center;
&.item {
border: 1px solid #f5f5f5;
margin-bottom: 10px;
cursor: pointer;
&.active,
&:hover {
border-color: @xtxColor;
background: lighten(@xtxColor, 50%);
}
> ul {
padding: 10px;
font-size: 14px;
line-height: 30px;
}
}
}
}
</style>
  1. 配置路由,src/router/index.ts
1
2
3
4
{
path: '/member/checkout',
component: () => import('@/views/member/pay/checkout.vue')
}
  1. 给结算按钮注册点击事件,views/cart/index.vue
1
<XtxButton type="primary" @click="goCheckout">下单结算</XtxButton>
1
2
3
4
5
6
7
8
// 跳转到结算页
const router = useRouter()
const goCheckout = () => {
if (cart.selectedList.length === 0) {
return Message.warning('请至少选择一件商品')
}
router.push('/member/checkout')
}

拦截未登录的用户

对于结算页面,没有登录的用户是无法访问的。

  1. 拦截未登录的用户,src/router/index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 配置路由导航守卫,拦截 /member 开头的所有的地址
router.beforeEach((to, from, next) => {
// 判断用户登没登录
const { cart } = useStore()
if (cart.isLogin) {
next()
} else {
if (to.path.includes('/member')) {
next({
path: '/login',
})
} else {
next()
}
}
})
  1. 增加回跳的 url 地址。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
router.beforeEach((to, from, next) => {
// 判断用户登没登录
const { cart } = useStore()
if (cart.isLogin) {
next()
} else {
if (to.path.includes('/member')) {
next({
path: '/login',
query: {
// #mark
redirectUrl: to.fullPath,
},
})
} else {
next()
}
}
})
  1. 修改登录成功的逻辑,views/login/components/login-form.vue
1
2
3
4
// 通过第三方登录,跳转到首页
Message.success('登录成功')
const redirectUrl = (route.query.redirectUrl as string) || '/'
router.push(redirectUrl)

对话框组件封装

目的:实现一个对话框组件可设置标题,动态插入内容,动态插入底部操作按钮,打开关闭功能。

image-20210804181049576

基本结构

  1. 定义一个基础布局,src/components/dialog/index.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
<script lang="ts" setup name="XtxDialog"></script>
<template>
<div class="xtx-dialog">
<div class="wrapper">
<div class="header">
<h3>切换收货地址</h3>
<a href="JavaScript:;" class="iconfont icon-close-new"></a>
</div>
<div class="body">对话框内容</div>
<div class="footer">
<XtxButton type="gray" style="margin-right: 20px">取消</XtxButton>
<XtxButton type="primary">确认</XtxButton>
</div>
</div>
</div>
</template>

<style scoped lang="less">
.xtx-dialog {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 8887;
background: rgba(0, 0, 0, 0.5);
// background: rgba(0, 0, 0, 0);
// &.fade {
// transition: all 0.4s;
// background: rgba(0, 0, 0, 0.5);
// }
.wrapper {
width: 600px;
background: #fff;
border-radius: 4px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
// transform: translate(-50%, -60%);
// opacity: 0;
// &.fade {
// transition: all 0.4s;
// transform: translate(-50%, -50%);
// opacity: 1;
// }
.body {
padding: 20px 40px;
font-size: 16px;
.icon-warning {
color: @priceColor;
margin-right: 3px;
font-size: 16px;
}
}
.footer {
text-align: center;
padding: 10px 0 30px 0;
}
.header {
position: relative;
height: 70px;
line-height: 70px;
padding: 0 20px;
border-bottom: 1px solid #f5f5f5;
h3 {
font-weight: normal;
font-size: 18px;
}
a {
position: absolute;
right: 25px;
top: 25px;
font-size: 24px;
width: 20px;
height: 20px;
line-height: 20px;
text-align: center;
color: #999;
&:hover {
color: #666;
}
}
}
}
}
</style>
  1. 需要全局注册,components/index.ts
1
2
import XtxDialog from '@/components/dialog/index.vue'
app.component('XtxDialog', XtxDialog)

global.d.ts

1
2
3
4
5
6
7
import XtxDialog from '@/components/dialog/index.vue'
declare module 'vue' {
export interface GlobalComponents {
XtxDialog: typeof XtxDialog
}
}
export {}

src/views/playground

1
<XtxDialog />

标题控制

  1. 实现设置标题,src/components/dialog/index.vue
1
2
3
4
5
6
defineProps({
title: {
type: String,
default: '',
},
})
1
2
3
4
<div class="header">
<h3>{{ title }}</h3>
<a href="JavaScript:;" class="iconfont icon-close-new"></a>
</div>
  1. src/views/playground/index.vue
1
<XtxDialog title="切换收货地址" />

插槽的使用

  1. 实现插入内容,src/components/dialog/index.vue
1
2
3
<div class="body">
<slot />
</div>
  1. src/views/playground/index.vue
1
2
3
<XtxDialog title="切换收货地址">
<h3>对话框内容</h3>
</XtxDialog>
  1. 底部具名插槽,src/components/dialog/index.vue
1
<div class="footer"><slot name="footer" /></div>
  1. src/views/playground/index.vue
1
2
3
4
5
6
7
<XtxDialog title="切换收货地址">
对话框内容
<template #footer>
<XtxButton type="gray" style="margin-right:20px">取消</XtxButton>
<XtxButton type="primary">确认</XtxButton>
</template>
</XtxDialog>

实现打开关闭功能

  1. 使用,src/views/playground/index
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script setup lang="ts">
import { ref } from 'vue'

const visible = ref(false)
</script>
<template>
<button @click="visible = true">显示</button>
<XtxDialog title="切换收货地址" v-model:visible="visible">
<h3>对话框内容</h3>
<template #footer>
<XtxButton type="gray" style="margin-right: 20px" @click="visible = false">取消</XtxButton>
<XtxButton type="primary" @click="visible = false">确认</XtxButton>
</template>
</XtxDialog>
</template>
  1. 定义,src/components/dialog/index.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
<script lang="ts" setup name="XtxDialog">
import { ref, watch } from 'vue'
import { onClickOutside } from '@vueuse/core'

const props = defineProps({
title: {
type: String,
default: '',
},
// #1
visible: {
type: Boolean,
default: false,
},
})
// #2
const emit = defineEmits<{
(e: 'update:visible', visible: boolean): void
}>()

// #3 控制动画
const show = ref(false)
watch(
() => props.visible,
(value) => {
setTimeout(() => {
show.value = value
}, 20)
},
{
immediate: true,
}
)
const close = () => {
emit('update:visible', false)
}

const target = ref(null)
onClickOutside(target, () => {
close()
})
</script>
<template>
<!-- #4 -->
<div class="xtx-dialog" v-if="visible" :class="{ fade: show }">
<!-- #5 -->
<div class="wrapper" ref="target" :class="{ fade: show }">
<div class="header">
<h3>{{ title }}</h3>
<a href="JavaScript:;" class="iconfont icon-close-new" @click="close"></a>
</div>
<div class="body">
<slot />
</div>
<div class="footer">
<slot name="footer" />
</div>
</div>
</div>
</template>

<style scoped lang="less">
.xtx-dialog {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 8887;
// background: rgba(0, 0, 0, 0.5);
background: rgba(0, 0, 0, 0);
&.fade {
transition: all 0.4s;
background: rgba(0, 0, 0, 0.5);
}
.wrapper {
width: 600px;
background: #fff;
border-radius: 4px;
position: absolute;
top: 50%;
left: 50%;
// transform: translate(-50%, -50%);
transform: translate(-50%, -60%);
opacity: 0;
&.fade {
transition: all 0.4s;
transform: translate(-50%, -50%);
opacity: 1;
}
.body {
padding: 20px 40px;
font-size: 16px;
.icon-warning {
color: @priceColor;
margin-right: 3px;
font-size: 16px;
}
}
.footer {
text-align: center;
padding: 10px 0 30px 0;
}
.header {
position: relative;
height: 70px;
line-height: 70px;
padding: 0 20px;
border-bottom: 1px solid #f5f5f5;
h3 {
font-weight: normal;
font-size: 18px;
}
a {
position: absolute;
right: 25px;
top: 25px;
font-size: 24px;
width: 20px;
height: 20px;
line-height: 20px;
text-align: center;
color: #999;
&:hover {
color: #666;
}
}
}
}
}
</style>

Teleport 组件的使用

https://staging-cn.vuejs.org/guide/built-ins/teleport.html#teleport

views/playground/index.vue

1
2
3
4
5
6
7
8
9
<Teleport to="body">
<XtxDialog title="添加地址" v-model:visible="visible">
<div class="form">我是一个表单</div>
<template #footer>
<XtxButton type="gray" style="margin-right: 20px" @click="visible = false"> 取消 </XtxButton>
<XtxButton type="primary" @click="visible = false">确认</XtxButton>
</template>
</XtxDialog>
</Teleport>

结算模块快速实现

结算模块的核心知识点,就是对话框组件封装,其他部分业务,我们可以快速实现,拷贝结算模块代码到项目中。

  1. components/switch 组件 => 需要全局注册。

  2. checkout.vuecomponents => 直接覆盖。

  3. store/checkout 模块 => 需要挂载。

提交订单功能

  1. 注册事件。
1
2
3
4
<!-- 提交订单 -->
<div class="submit">
<XtxButton type="primary" @click="submitCheckout">提交订单</XtxButton>
</div>
  1. 核心逻辑。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const submitCheckout = async () => {
// 如果地址为空,不能提交订单
if (!checkout.showAddress) {
return Message.warning('请选择收货地址')
}

// 发送请求,下单
const res = await request.post('/member/order', {
goods: checkout.checkoutInfo.goods.map((item) => {
return {
skuId: item.skuId,
count: item.count,
}
}),
addressId: checkout.showAddress.id,
})

// 成功提醒用户
Message({ type: 'success', text: '下单成功~' })
// 重新获取购物车列表
cart.getCartList()
// 跳转到支付页面
router.push('/member/pay')
}