趣购

介绍

在线购物几乎已经是现代生活必备的一环,每年的 618,双 11 都是购物的狂欢。我们几乎可以在线上购物商城买到一切日常所需。

本题需要在已提供的基础项目中,使用 Web 原生拖拽事件实现在线购物的功能。

准备

开始答题前,需要先打开本题的项目代码文件夹,目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
├── images
│ ├── book.jpeg
│ ├── box.jpeg
│ ├── paper.jpeg
│ ├── trolley.jpeg
│ └── tv.jpg
├── index.html
├── effect.gif
├── js
│ ├── http-vue-loader.js
│ └── vue.min.js
└── trolley.vue

其中:

  • index.html 是主页面。
  • effect.gif 是最终实现的效果图。
  • images 文件夹提供的页面所需要的商品图片。
  • js/vue.min.jsjs/http-vue-loader.js 是 vue 库相关文件。
  • trolley.vue 是需要补充代码的组件文件。

注意:打开环境后发现缺少项目代码,请复制下述命令至命令行进行下载。

1
2
cd /home/project
wget https://labfile.oss.aliyuncs.com/courses/18164/08.zip && unzip 08.zip && rm 08.zip

在浏览器中预览 index.html 页面,显示如下所示:

初始效果

目标

请在 trolley.vue 文件中的 TODO 部分补全代码:

  • 用鼠标按下拖动图片到购物车(即 id="trolley" 的节点)中,然后松开鼠标,购物车会添加拖动的商品,并显示购物车商品数量。
  • 下方(即 class="result" 的方框)会显示购物车中商品的详情,详情以商品名 x 数量的形式展示,商品之间通过空格间隔。
  • 下方(即 class="result" 的方框)同时还会显示购物车中商品的总价。

交互效果

完成后的效果见文件夹下面的 gif 图,图片名称为 effect.gif(提示:可以通过 VS Code 或者浏览器预览 gif 图片):

完成效果

题目会用到的拖拽 API 参考:

全局属性 draggable 是一个枚举类型的属性,用于标识元素是否允许使用拖放操作拖动。它的取值如下:

  • true:表示元素可以被拖动。
  • false:表示元素不可以被拖动。

带有属性 draggable 的可拖放元素可用的拖放事件 api 如下:

拖动事件:

事件 事件处理程序 触发时刻
drag ondrag 当拖拽元素或选中的文本时触发。
dragstart ondragstart 当用户开始拖拽一个元素或选中的文本时触发
dragend ondragend 当拖拽操作结束时触发 (比如松开鼠标按键或敲“Esc”键)

放置事件:

事件 事件处理程序 触发时刻
dragenter ondragenter 当拖拽元素或选中的文本到一个可释放目标时触发
dragleave ondragleave 当拖拽元素或选中的文本离开一个可释放目标时触发。
dragover ondragover 当元素或选中的文本被拖到一个可释放目标上时触发(每 100 毫秒触发一次)
drop ondrop 当元素或选中的文本在可释放目标上被释放时触发,想要 ondrop 能正确触发,有时需要在前置 dragover 事件中禁用默认行为

每个 drag event 都有一个 dataTransfer 属性,其中保存着事件的数据。这个属性(DataTransfer 对象)也有管理拖拽数据的方法。setData() 方法为拖拽数据添加一个项,其用法如下所示:

1
2
3
4
5
function dragstart_handler(ev) {
// 添加拖拽数据,key 可以为任意字符串
ev.dataTransfer.setData("key", "value");
const data = ev.dataTransfer.getData("key");
}

规定

  • 请勿修改 trolley.vue 文件外的任何内容。
  • 请严格按照考试步骤操作,切勿修改考试默认提供项目中的文件名称、文件夹路径、class 名、id 名、图片名等,以免造成无法判题通过。
  • 满足需求后,保持 Web 服务处于可以正常访问状态,点击「提交检测」系统会自动检测。

判分标准

  • 本题完全实现题目目标得满分,否则得 0 分。

总通过次数: 519 | 总提交次数: 623 | 通过率: 83.3%

难度: 中等 标签: 2022, 省模拟赛, Web 前端, JavaScript

题解

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
<!-- TODO: 补充拖拽事件,请不要改动任何 id 属性 -->
<template>
<div class="container">
<div class="good-list">
<div v-for="good in goods" :key="good.name" class="good">
<img
:src="good.cover"
:data-name="good.name"
draggable
@dragstart="handleDrag(good, $event)"
/>
<span>{{ good.name }}</span>
<span>¥{{ good.price }}</span>
</div>
</div>
<div id="trolley" class="trolley" @dragover.prevent @drop="handleDrop">
<span id="bought" class="bought" v-if="bought.length !== 0">{{
bought.length
}}</span>
<img src="./images/trolley.jpeg" />
</div>
<div class="result">
<div>
购物车商品:<span id="goods">{{ goodsDetail }}</span>
</div>
<div>
购物车商品总计:<span id="total">{{ totalPrice }}</span>
</div>
</div>
</div>
</template>
<style>
.container {
width: 650px;
position: relative;
height: 600px;
margin: 10px auto;
display: flex;
flex-direction: column;
}
.good-list {
width: 100%;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
flex-grow: 1;
}
.good {
width: 150px;
height: 160px;
border: 1px solid rgb(52, 52, 52);
border-radius: 5px;
padding: 5px;
}
.good:hover {
border: 2px solid rgb(52, 52, 52);
}

.good img {
width: 100%;
height: 120px;
}
.trolley {
position: absolute;
height: 60px;
width: 60px;
border-radius: 50%;
overflow: hidden;
border: 1px solid #f4f4f4;
background-color: #f4f4f4;
display: flex;
align-items: center;
justify-content: center;
right: 0;
bottom: 100px;
}

.trolley img {
width: 40px;
height: 40px;
pointer-events: none;
}

.bought {
width: 16px;
height: 16px;
background-color: crimson;
color: white;
position: absolute;
right: 8px;
top: 10px;
border-radius: 50%;
text-align: center;
line-height: 15px;
pointer-events: none;
}
.result {
width: 100%;
min-height: 80px;
border: 1px solid;
margin-top: 20px;
padding: 10px;
}
</style>
<script>
module.exports = {
data() {
return {
goods: [
{
name: "畅销书",
price: 30,
cover: "./images/book.jpeg",
},
{
name: "收纳箱",
price: 60,
cover: "./images/box.jpeg",
},
{
name: "纸巾",
price: 20,
cover: "./images/paper.jpeg",
},
{
name: "电视",
price: 1000,
cover: "./images/tv.jpg",
},
],
bought: [],
};
},
// TODO: 请补充实现代码
computed: {
totalPrice() {
return this.bought.reduce((t, c) => t + c.price, 0);
},
goodsDetail() {
return [
...new Set(
this.bought.map(
(v, i, a) => `${v.name}*${a.filter((j) => j.name == v.name).length}`
)
),
].join(" ");
},
},
methods: {
handleDrag(good, e) {
e.dataTransfer.setData("good", JSON.stringify(good));
},
handleDrop(e) {
this.bought.push(JSON.parse(e.dataTransfer.getData("good")));
},
},
};
</script>