我正在参加「创意开发 投稿大赛」详情请看:掘金创意开发大赛来了!
前言碎语
在一种莫名力量的推动下,每年会有好多个情人节,但是七夕,应该还是我们最传统的一个。在鹊桥相会的重要日子里,你需要来一个Vue3的实战么?
你说啥,想要和女朋友一起过七夕,但是不知道她愿不愿意?那就花费10分钟时间来搞定这个最简Vue3实战吧,还可以从容地发给她:做我女朋友好不好?
完整项目体验地址:train.lovetime.top/qixi/
项目奠基
因为是最简实战,所以我们这个项目比较简单,用不上router、pinia这些,但是我们也不能太草率,用pnpm来玩一玩吧。
pnpm create vite ./qixi-girlfriend
按照提示操作即可,当然我们要记得选择Vue模板,也可以直接指定模板的方式来玩:
pnpm create vite ./qixi-girlfriend --template vue
进入项目,安装基础依赖,这都是老一套流程,不再baba赘述:
cd qixi-girlfriend
pnpm install
pnpm run dev
项目进行时
捋一捋流程
(1) 初始状态下呢,我们要展示“做我女朋友好不好”的主题,表明一下自己的心声。
(2) 展示同意Yes和拒绝No的按钮。
(3) 点击Yes按钮同意时,说明女朋友同意啦,展示比心状态,并说出你的诺言。
(4) 点击No按钮拒绝时,赶紧拯救一下自己。一条条插入你表白的理由,如果理由展示完了,展示可怜的状态。
静态资源的加载
由于vite不再支持webpack的require加载模式,我们简单封装一下new URL()方法来引入使用的静态资源。
const getResourceUrl = (name, ext = "png") => {
return new URL(`./assets/${name}.${ext}`, import.meta.url).href;
};
getResourceUrl()方法支持传入文件名和后缀格式。
核心实现
由于我们的功能比较简单,直接在App.vue里面写起来。
变量赋值
我们首先通过ref和getResourceUrl来定义赋值一下需要使用的变量和资源。
<script setup>
import { ref } from "vue";
const refuseNum = ref(0);
const isDecisionShow = ref(true);
const isAgreeShow = ref(false);
const title = ref("做我女朋友好不好");
const initText = ref(
"承蒙你的出现,够我喜欢好多年,我希望,以后你能用我的名字拒绝所有人"
);
const benefitText = ref([
"你是我拔掉氧气罐都想吻的人",
"你是我跑完8000米还想拥抱的人",
"你是我自罚三杯都不肯开口的秘密",
"你是我赴汤蹈火都不肯放下的执着",
"你是我电量只剩1%也想回信息的人",
"你是我穷极一生不想醒来的梦",
]);
const resultText =
"遇见你是我所有美好故事的开始,所以,请别放开我的手,也别缺席我的将来,因为一辈子和你在一起才叫将来";
const exhibitionText = ref([initText]);
const winkImg = getResourceUrl("wink", "gif");
const bgImg = getResourceUrl("bg", "jpg");
const kelianImg = getResourceUrl("emoji_kelian", "jpg");
const bixinImg = getResourceUrl("emoji_bixin", "jpg");
</script>
在template模板中使用这些变量
<template>
<div class="container">
<img class="bg" :src="bgImg" />
<div class="lover">
<div class="express">
<h1>{{ title }}<span>💕</span></h1>
<div class="wink">
<img :src="winkImg" />
</div>
<p v-for="(text, index) in exhibitionText" :key="index">{{ text }}<span>💕</span></p>
</div>
<div class="pray" v-show="!isDecisionShow" @click="onPray">
<img :src="kelianImg" />
<p>请告诉我Yes!</p>
<span class="pray-close">×</span>
</div>
<div class="decision" v-show="isDecisionShow">
<div class="decision-btn refuse" @click="onRefuse">No<span>💔</span></div>
<div class="decision-btn" @click="onAgree">Yes<span>❤️</span></div>
</div>
<div class="agree-wrapper" v-show="isAgreeShow">
<div class="agree">
<img :src="bixinImg" />
<p>太好了,O(∩_∩)O哈哈~</p>
<p>{{ agreeText }}<span class="agree-cursor" style="color: #f44336">❤</span></p>
</div>
</div>
</div>
</div>
</template>
定义方法
当点击No按钮拒绝时,把下一条表白话语插入到展示列表中,并对拒绝次数进行累加。若表白话语展示完了,展示可怜状态并隐藏决策按钮;点击可怜状态时立马恢复。
const onRefuse = () => {
console.log("onRefuse", refuseNum.value);
if (refuseNum.value < benefitText.value.length) {
exhibitionText.value.push(benefitText.value[refuseNum.value]);
refuseNum.value++;
} else {
isDecisionShow.value = false;
}
};
const onPray = () => {
isDecisionShow.value = true;
};
当点击Yes按钮同意时,展示比心状态,并且实现一个简陋版的打字效果,展示你的诺言。
const onAgree = () => {
isAgreeShow.value = true;
onTyped();
};
const onTyped = () => {
let index = 0;
const typedTime = setInterval(() => {
agreeText.value = resultText.substring(0, index++);
}, 150);
if (index >= resultText.length - 1) {
clearInterval(typedTime);
}
};
好啦,到这里,我们核心的功能已经实现啦。
Emm,但是总感觉有那么点点不够唯美,我们来加上花瓣飘飘的动效吧。
动效加持
说起来动效,想起前一段和PixiJS一起食用的GSAP,详见历史文章。那我们先安装依赖,当然为了让每一片花瓣都唯一,我们再安装一下nanoid帮助生成唯一ID吧。
pnpm install gsap
pnpm install nanoid
# 为了使用sass的习惯,补充安装一下sass到devDependencies
pnpm install sass -D
创建花瓣
首先,我们来准备一组花瓣素材,不同样式的花瓣让我们在素材方面省了不少事(当然,其实我们也是可以直接用css来画的)~
然后,在创建花瓣的时候,根据可视屏幕的宽高来随机一下花瓣初始位置,以及配合GSAP动效的目标位置和动画时长duration。
最后,把创建好的花瓣元素插入到花瓣列表,并且在花瓣动效结束后移除它。
当然,我们还是要通过定时器setInterval在onMounted周期挂载并持续创建花瓣,在onUnmounted周期移除定时器。
<script setup>
import { ref, onMounted, onUnmounted } from "vue";
// 为了使ID符合规则且够简单,我们自定义一下ID的生成规则
import { customAlphabet } from "nanoid";
const nanoid = customAlphabet("abcdefghijklmn", 6);
// 花瓣素材
const petalImgs = [
"icon_petal_1",
"icon_petal_2",
"icon_petal_3",
"icon_petal_4",
"icon_petal_5",
"icon_petal_6",
"icon_petal_7",
"icon_petal_8",
];
const petalList = ref([]);
const visualWidth = window.innerWidth;
const visualHeight = window.innerHeight;
// 创建花瓣元素
const createPetalBox = () => {
// 配合getResourceUrl()方法随机获取素材*1
const currentPetal = getResourceUrl(petalImgs[Math.floor(Math.random() * 8)]);
// 初始的随机位置
const petalLeft = Math.random() * visualWidth;
// 初始透明度
const randomOpacity = Math.random();
const petalOpacity =
randomOpacity < 0.5 ? randomOpacity + 0.5 : randomOpacity;
// 动效结束的随机位置
const petalEndLeft = petalLeft - 100 + Math.random() * 500;
const petalEndTop = visualHeight + 40;
// 动效时长
const duration = Math.floor(
(visualHeight * 10 + Math.random() * 5000) / 1000
);
const currentStyle = {
left: petalLeft,
opacity: petalOpacity,
};
const petal = {
id: nanoid(),
url: currentPetal,
style: currentStyle,
end: {
duration,
left: petalEndLeft,
top: petalEndTop,
},
};
petalList.value.push(petal);
};
// 动效结束后移除当前花瓣元素
const removeHandler = (id) => {
petalList.value.splice(
petalList.value.findIndex((petal) => petal.id === id),
1
);
};
// 定义花瓣定时器
const petalTimer = ref(null);
const petalHandler = () => {
petalTimer.value = setInterval(createPetalBox, 500);
};
onMounted(() => {
petalHandler();
});
onUnmounted(() => {
clearInterval(petalTimer);
});
</script>
创建自定义组件
接下来我们创建一个自定义组件WePetal,可以在组件挂载完成后,通过props传递的元素ID绑定GSAP动效,并在动态结束后通知父组件。
<template>
<img :id="petal.id" class="petal" :src="petal.url" :style="petal.style" />
</template>
<script setup>
import { onMounted } from "vue";
import gsap from "gsap";
const props = defineProps({
petal: {
type: Object,
default() {
return {};
},
},
});
const emit = defineEmits(['remove'])
onMounted(() => {
const { id, end } = props.petal;
// 通过唯一ID来绑定动效
gsap.to(`#${id}`, {
...end,
onComplete: () => {
emit('remove', id)
}
});
});
</script>
<style lang="scss" scoped>
.petal {
width: 24px;
height: 24px;
position: absolute;
top: -40px;
left: 0;
opacity: 1;
z-index: 99;
}
</style>
调用子组件
现在,我们就可以在App.vue中引入子组件并且使用啦。
<template>
<!--...-->
<div class="petal-box">
<WePetal
v-for="petal in petalList"
:key="petal.id"
:petal="petal"
@remove="removeHandler"
/>
</div>
</template>
写在最后
其实,还有好多细节需要优化的,比如素材的选择、比如表白语录、比如为了花瓣不遮挡按钮事件直接放在了下层等等,主要还是七夕马上就要到了,担心影响大家表白~
码上掘金的Vue代码块感觉还是不够灵活,摸索了好久,最后决定只在上面码一个核心实现,完整代码请查看github。
祝大家七夕快乐~