likes
comments
collection
share

最简Vue3实战:做我女朋友好不好

作者站长头像
站长
· 阅读数 16

前言碎语

在一种莫名力量的推动下,每年会有好多个情人节,但是七夕,应该还是我们最传统的一个。在鹊桥相会的重要日子里,你需要来一个Vue3的实战么?

你说啥,想要和女朋友一起过七夕,但是不知道她愿不愿意?那就花费10分钟时间来搞定这个最简Vue3实战吧,还可以从容地发给她:做我女朋友好不好?

完整项目体验地址:train.lovetime.top/qixi/

最简Vue3实战:做我女朋友好不好

项目奠基

因为是最简实战,所以我们这个项目比较简单,用不上routerpinia这些,但是我们也不能太草率,用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不再支持webpackrequire加载模式,我们简单封装一下new URL()方法来引入使用的静态资源。

const getResourceUrl = (name, ext = "png") => {
  return new URL(`./assets/${name}.${ext}`, import.meta.url).href;
};

getResourceUrl()方法支持传入文件名和后缀格式。

核心实现

由于我们的功能比较简单,直接在App.vue里面写起来。

变量赋值

我们首先通过refgetResourceUrl来定义赋值一下需要使用的变量和资源。

<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

最后,把创建好的花瓣元素插入到花瓣列表,并且在花瓣动效结束后移除它。

当然,我们还是要通过定时器setIntervalonMounted周期挂载并持续创建花瓣,在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>

写在最后

其实,还有好多细节需要优化的,比如素材的选择、比如表白语录、比如为了花瓣不遮挡按钮事件直接放在了下层等等,主要还是七夕马上就要到了,担心影响大家表白~

祝大家七夕快乐~