简介
基于vue3做的一个小玩意,点击标签加入购物车,话不多说,先献丑上全部的代码(其中大部分class用的是自己公共样式,但不影响功能),然后再分别细细讲解一下个人的小思路。
代码
<template>
<div class="flex flex-column position-relative">
<div class="flex justify-center align-center mt-1">
<el-icon :size="20"><Bowl /></el-icon>
<span class="ml-1 font-weight-bold">好的,今天我们来做菜!</span>
</div>
<!-- 关键代码-->
<transition
appear
@before-appear="beforeEnter"
@after-appear="afterEnter"
v-for="(item, index) in showBall.boolean"
:key="index"
>
<div class="ball rounded"
:class="[showBall.flag == 'vegetable' ? 'vegetable-item' : showBall.flag == 'meat' ? 'meat-item' : showBall.flag == 'food' ? 'food-item' : 'kitchenware-item' ]"
v-if="item">
{{showBall.ball.name}}
</div>
</transition>
<!-- 蔬菜-->
<div class="font-weight-bold font-lg flex justify-center">{{data.name}}</div>
<div class="flex flex-row justify-center mt-1">
<div v-for="(item, i) in data.vegetable" :key="item.id" class="vegetable boxShadow">
<div class="vegetable-item rounded" @click="addCart(item, 'vegetable')" :ref="el => { if(el) itemRefs.ref[item.id] = el }">
{{ item.name }}
</div>
</div>
</div>
<!-- 肉-->
<div class="font-weight-bold font-lg flex justify-center mt-2">🥩 肉肉们</div>
<div class="flex flex-row justify-center mt-1">
<div v-for="(item, i) in data.meat" :key="item.id" class="meat boxShadow">
<div class="meat-item rounded" @click="addCart(item, 'meat')" :ref="el => { if(el) itemRefs.ref[item.id] = el }">{{ item.name }}</div>
</div>
</div>
<!-- 主食-->
<div class="font-weight-bold font-lg flex justify-center mt-2">🍚 主食也要一起下锅吗?(不选也行)</div>
<div class="flex flex-row justify-center mt-1">
<div v-for="(item, i) in data.food" :key="item.id" class="food boxShadow">
<div class="food-item rounded" @click="addCart(item, 'food')" :ref="el => { if(el) itemRefs.ref[item.id] = el }">{{ item.name }}</div>
</div>
</div>
<!-- 厨具-->
<div class="font-weight-bold font-lg flex justify-center mt-2">🍳 再选一下厨具</div>
<div class="flex flex-row justify-center mt-1">
<div v-for="(item, i) in data.kitchenware" :key="item.id" class="kitchenware boxShadow">
<div class="kitchenware-item rounded" @click="addCart(item, 'kitchenware')" :ref="el => { if(el) itemRefs.ref[item.id] = el }">{{ item.name }}</div>
</div>
</div>
<div ref="card" class="cart-icon">🛒</div>
</div>
</template>
<script setup lang='ts'>
// 关于ref和reactive我已经全局导入了,没全局的这里补一下 import { ref, reactive } from 'vue';
// 为了让你们更好区分,画dom部分方法我就用function,购物车部分用()=>
let data = reactive({
name: '🥘 先选一下食材',
vegetable: [],
meat: [],
food: [],
kitchenware: [],
})
function getVegetable() {
data.vegetable = [
{ name: '🥔土豆', id: 'td' },
{ name: '🥕胡萝卜', id: 'hlb' },
{ name: '🥦花菜', id: 'hc' },
{ name: '🥣白萝卜', id: 'blb' },
{ name: '🥒西葫芦', id: 'xhl' },
{ name: '🍅番茄', id: 'fq' },
{ name: '🥬芹菜', id: 'qc' },
{ name: '🥒黄瓜', id: 'hg' },
{ name: '🧅洋葱', id: 'yc' },
{ name: '🥫莴笋', id: 'ws' },
{ name: '🍄菌菇', id: 'jg' },
{ name: '🍆茄子', id: 'qz' },
{ name: '🍲豆腐', id: 'df' },
{ name: '🥗包菜', id: 'bc' },
{ name: '🥬白菜', id: 'bc' },
]
}
function getMeat() {
data.meat = [
{ name: '🥓午餐肉', id: 'wcr' },
{ name: '🌭香肠', id: 'xc' },
{ name: '🥓腊肠', id: 'lr' },
{ name: '🐤鸡肉', id: 'jr' },
{ name: '🐷猪肉', id: 'zr' },
{ name: '🐣鸡蛋', id: 'jd' },
{ name: '🍤虾', id: 'x' },
{ name: '🐮牛肉', id: 'nr' },
{ name: '🦴骨头', id: 'gt' },
{ name: '🐟鱼', id: 'y' },
]
}
function getFood() {
data.food = [
{ name: '🍜面食', id: 'ms' },
{ name: '🍞面包', id: 'mb' },
{ name: '🍚米', id: 'm' },
{ name: '🍜方便面', id: 'fbm' },
]
}
function getKitchenware() {
data.kitchenware = [
{ name: '😉烤箱', id: 'kx' },
{ name: '😋空气炸锅', id: 'kqzg' },
{ name: '😃微波炉', id: 'wbl' },
{ name: '😊电饭煲', id: 'dfb' },
{ name: '😎一口能炒又能煮的大锅', id: 'dg' },
]
}
let itemRefs = reactive({
ref: [], //所有商品的dom结构
left: 0, // 当前购物车的left
top: 0,
})
let card = ref(null) // 购物车的位置信息
let showBall = reactive({
boolean: [], // 控制商品是否显示
ball: {}, // 商品数据
flag: null, // 商品class
})
// 动画开始了
const beforeEnter = (el)=>{
el.style.transform = `translate3d(${itemRefs.left}px,${itemRefs.top}px,0)`
}
// 动画结束了
const afterEnter = (el)=>{
let {left, top} = card.value.getBoundingClientRect() // 获取购物车位置
el.style.transform = `translate3d(${left}px,${top}px,0)`
//增加移动路径
el.style.transition = 'transform .55s cubic-bezier(0.3,-0.25,0.7,-0.15)'
el.style.transition = 'transform .55s linear'
showBall.boolean = showBall.boolean.map(item => false) // 隐藏商品
}
const addCart = (item, flag)=>{ //点击商品按钮后,获取其位置信息
showBall.ball = item; //
showBall.boolean.push('true');
showBall.flag = flag;
itemRefs.left = itemRefs.ref[item.id].getBoundingClientRect().left;
itemRefs.top = itemRefs.ref[item.id].getBoundingClientRect().top;
}
function init() {
getVegetable();
getMeat();
getFood();
getKitchenware();
}
init()
</script>
<style scoped lang="scss">
.vegetable, .meat, .food, .kitchenware{
margin: 0 5px;
&-item{
background-color: #ddf2e7;
color: #166534;
padding: 2px 5px;
font-size: 14px;
cursor: pointer;
}
&-active {
background-color: #2cab5b;
}
}
.meat-item{
background-color: rgb(244, 227, 230);
color: #bf3e3c;
}
.kitchenware-item{
background-color: #f1f1f4;
color: #000;
}
.food-item{
background-color: #f5efd3;
color: #c69254;
}
.boxShadow{
box-shadow: 0px 2px 2px 0px rgba(0, 0, 0, 0.15);
}
/* 商品选项 */
.ball{
max-width: 65px;
position: fixed;
top: 0px;
left: 0px
}
/* 购物车*/
.cart-icon {
position: fixed; /* 或根据需要调整 */
bottom: 50px; /* 或根据需要调整 */
right: 50px; /* 或根据需要调整 */
font-size: 2em; /* 根据需要调整大小 */
}
</style>
思路
- 先创建选项,以蔬菜为例(具体代码上面已全部贴出)
<div v-for="(item, i) in data.vegetable" :key="item.id" class="vegetable boxShadow">
<div class="vegetable-item rounded" @click="addCart(item, 'vegetable')" :ref="el => { if(el) itemRefs.ref[item.id] = el }">
{{ item.name }}
</div>
</div>
给每个蔬菜标签设置对应id的ref并存入itemRefs(:ref="el => { if(el) itemRefs.ref[item.id] = el }"),点击addCart通过id方便获取选中dom的具体位置(left,top)
// 创建itemRefs存储dom
let itemRefs = reactive({
ref: [], // 所有蔬菜的ref
left: 0, // 当前购物车的left
top: 0,
})
const addCart = (item, flag)=>{ //点击蔬菜按钮后,获取其位置信息
...// 获取蔬菜标签详细信息代码,上面完整代码有,这里不展示
itemRefs.left = itemRefs.ref[item.id].getBoundingClientRect().left; // 通过id获取left
itemRefs.top = itemRefs.ref[item.id].getBoundingClientRect().top;// 通过id获取top
}
2.加入购物车通过vue3的内置组件transition来完成,@before-appear获取点击前的位置,并在蔬菜标签位置生成一个同款商品,@after-appear获取购物车的位置,进行一个简单路径移动就完成了
<transition
appear
@before-appear="beforeEnter"
@after-appear="afterEnter"
v-for="(item, index) in showBall.boolean"
:key="index"
>
<!-- 和选中蔬菜标签同款样式的标签-->
<div class="ball rounded"
:class="[showBall.flag == 'vegetable' ? 'vegetable-item' : showBall.flag == 'meat' ? 'meat-item' : showBall.flag == 'food' ? 'food-item' : 'kitchenware-item' ]"
v-if="item">
{{showBall.ball.name}}
</div>
</transition>
<!-- 购物车-->
<div ref="card" class="cart-icon">🛒</div>
let card = ref(null) // 购物车的位置信息
let showBall = reactive({
boolean: [], // 控制商品是否显示
ball: {}, // 商品数据
flag: null, // 商品class
})
// 动画开始了,将商品位置设置成前面点击蔬菜时获取的位置
const beforeEnter = (el)=>{
el.style.transform = `translate3d(${itemRefs.left}px,${itemRefs.top}px,0)`
}
// 动画结束了
const afterEnter = (el)=>{
let {left, top} = card.value.getBoundingClientRect() // 获取购物车位置
el.style.transform = `translate3d(${left}px,${top}px,0)`
//增加移动路径
el.style.transition = 'transform .55s cubic-bezier(0.3,-0.25,0.7,-0.15)'
el.style.transition = 'transform .55s linear'
showBall.boolean = showBall.boolean.map(item => false) // 隐藏商品
}