- A+
? 点击这里 获取国庆头像。通过 Vue3 + Vant(组件库) + Vite 构建。
应用的流程
在做之前,先思考它的流程:
- 上传头像
- 选择头像模板
- 保存新头像
进入应用放出节日祝福与简单的使用介绍。
开始仅显示一个上传头像按钮。这样做的好处是,直接固定第一步操作。如果将模板和保存按钮显示出来需要增加代码做一些判断。如果开始就将头像模板与保存按钮显示,可能让人无从下手。
上传头像后直接将模板与保存按钮显示出来。在代码中默认选中今年最流行的模板,直接点击保存按钮将下载这个头像。开屏也进行了选择模板的提示,如果点击一个头像模板,将进行切换,通过点击保存按钮保存它。
最后,点击保存显示烟花动画。
代码实现
头像上传
<div class="uploader-wrap" :style="{ marginTop: fileList.length === 0 ? '220px' : '80px' }" > <Uploader v-model="fileList" preview-size="120px" class="uploader" :deletable="true" :max-count="1" :after-read="afterRead" :before-delete="handleDelete" /> </div>
开始,仅显示一个上传按钮,但希望它出现在屏幕靠中间位置,便于点击。通过 vue 样式绑定即可。
.uploader-wrap { display: flex; justify-content: center; transition: 0.3s margin; }
在样式代码中给它添加 transition(过渡效果)。
const fileList = ref([]) const base64 = ref('') const afterRead = file => buildBase64(file.file) const buildBase64 = file => { const reader = new FileReader() reader.readAsDataURL(file) reader.onload = () => { base64.value = reader.result } } const handleDelete = () => { base64.value = '' fileList.value = [] }
在 JS 中,通过 afterRead
获取上传的文件,并将它转化为 Base64 编码,以便在后面将它在头像模板中再次显示。通过 handleDelete
将 响应属性 base64 与 fileList 的值清空。由于上传按钮位置通过 vue 样式绑定,当 fileList 为空时,上传按钮会再次回到屏幕靠中间位置。
模板列表
<div class="preview-list"> <div class="preview-item" v-if="fileList.length !== 0" v-for="(num, index) in 6" :id="`item-${num}`" :key="index" @click="handleSelect" > <img class="preview-item__img" :src="base64" alt="avatar" v-show="base64 !== ''" /> <img class="preview-item__modification" :src="getImageUrl(num)" alt="avatar" /> </div> </div>
通过 v-if 控制列表显示。给每个列表项一个唯一的 id,以便区分它们。
.preview-list { display: grid; grid-template-rows: repeat(2, 1fr); grid-template-columns: repeat(3, 1fr); row-gap: 14px; column-gap: 8px; margin: 40px 0 60px; .preview-item { justify-self: center; // ... img { display: block; position: absolute; top: 0; right: 0; bottom: 0; left: 0; width: 100%; height: 100%; } } }
在样式中,直接通过 grid 生成一个两行三列的布局。由于给图片的设定宽度小于网格宽度,通过 justify-self: center
使每个项水平居中。通过定位将两个 img 重叠显示。如果使用两个 div,并分别给它们添加背景图片(Base64)配合 Vue3 CSS 样式变量注入将非常香,但生成 canvas 后将变得模糊。
const getImageUrl = name => new URL(`./assets/${name}.png`, import.meta.url).href const selectedId = ref('item-1') const handleSelect = e => { selectedId.value = e.currentTarget.id Toast.success('选择成功') }
在 JS 中,由于 vue 无法在动态绑定 src 至本地文件,在 webpack 中使用 require 动态引入文件即可。 在 vite 中,通过 vite 特有的 import.meta.url
来获取。原因是,可以通过动态绑定到 "/src/assets/ ${name}.png" 来显示出头像模板,但这在生产环境中无法被加载(资产被打包到 dist 或其他目录下,assets 不在 src 下了)。如果是 src 静态的,直接通过绑定 "./assets/${name}.png" 即可, 开发环境输出到浏览器时,被编译为 "/src/assets/name.png",在生产环境也能被正确显示,另外也可以通过将静态头像模板图片放到根目录下的 public 来解决,在 vite build
时,public 会被复制到输出文件夹(ourDir)下。
保存头像
<Button v-if="fileList.length !== 0" :loading="downloading" @click="handleSave" type="primary" loading-text="保存中..." block color="#FA9935" >保存</Button >
template 中,使用 vant 组件库加载按钮,通过点击触发 handleSave
函数保存头像。
const downloading = ref(false) const handleSave = () => { if (fileList.value.length === 0) { Toast.fail('请先上传头像') return } downloading.value = true html2canvas(document.querySelector(`#${selectedId.value}`), { scale: 4, allowTaint: true, dpi: window.devicePixelRatio * 2, }).then(canvas => { // downloadjs(canvas.toDataURL(), '头像.png', 'image/png') saveAs(canvas.toDataURL(), 'image.jpg') // const dataUrl = canvas.toDataURL() // const a = document.createElement('a') // a.download = 'avatar.png' // a.href = dataUrl // a.click() downloading.value = false Toast.success('保存成功') showFireworks.value = true setTimeout(() => { showFireworks.value = false }, 3000) }) }
在 handleSave
函数中,开始将 downloading 的值更新为 true,按钮这时为 loading 状态。通过 html2canvas.js 将选中的头像模板转为 canvas,在结果中通过 canvas.toDataURL()
获取 canvas dataUrl,在通过 file-saver 将头像保存。通过 HTML a[download]
与 download.js 都不能很好的处理移动端兼容问题。最后,就是显示烟花彩蛋了,在烟花动画播放完毕再将它关闭。
烟花效果与开屏提示
以上就是主要代码了,下面分别是烟花效果与开屏提示组件的代码片段,如果你对它们感兴趣可以通过点击展开查看它们。
Blessing.vue
<script setup> import { ref } from 'vue' const show = ref(true) </script> <template> <van-dialog v-model:show="show" title="Hi,国庆节快乐" theme="round-button" confirmButtonText="好" > <div class="blessing"> <ol> <li>? 上传头像</li> <li>? 选择一个模板</li> <li>? 保存新头像</li> </ol> <p>——来自 <a href="https://www.cnblogs.com/guangzan/">@guangzan</a></p> </div> </van-dialog> </template> <style lang="scss"> .blessing { padding: 20px; h4 { margin-bottom: 16px; } p { margin: 4px 0; font-size: 12px; color: #666; text-align: right; } ol { margin-left: 20px; list-style: auto; li { margin-bottom: 4px; } } a { color: #b8251b; } } </style>
Congratulate.vue
<template> <div class="container"> <div class="wrapper"> <div class="confetti-201"></div> <div class="confetti-200"></div> <div class="confetti-199"></div> <div class="confetti-198"></div> <div class="confetti-197"></div> <div class="confetti-196"></div> <div class="confetti-195"></div> <div class="confetti-194"></div> <div class="confetti-193"></div> <div class="confetti-192"></div> <div class="confetti-191"></div> <div class="confetti-190"></div> <div class="confetti-189"></div> <div class="confetti-188"></div> <div class="confetti-187"></div> <div class="confetti-186"></div> <div class="confetti-185"></div> <div class="confetti-184"></div> <div class="confetti-183"></div> <div class="confetti-182"></div> <div class="confetti-181"></div> <div class="confetti-180"></div> <div class="confetti-179"></div> <div class="confetti-178"></div> <div class="confetti-177"></div> <div class="confetti-176"></div> <div class="confetti-175"></div> <div class="confetti-174"></div> <div class="confetti-173"></div> <div class="confetti-172"></div> <div class="confetti-171"></div> <div class="confetti-170"></div> <div class="confetti-169"></div> <div class="confetti-168"></div> <div class="confetti-167"></div> <div class="confetti-166"></div> <div class="confetti-165"></div> <div class="confetti-164"></div> <div class="confetti-163"></div> <div class="confetti-162"></div> <div class="confetti-161"></div> <div class="confetti-160"></div> <div class="confetti-159"></div> <div class="confetti-158"></div> <div class="confetti-157"></div> <div class="confetti-156"></div> <div class="confetti-155"></div> <div class="confetti-154"></div> <div class="confetti-153"></div> <div class="confetti-152"></div> <div class="confetti-151"></div> <div class="confetti-150"></div> <div class="confetti-149"></div> <div class="confetti-148"></div> <div class="confetti-147"></div> <div class="confetti-146"></div> <div class="confetti-145"></div> <div class="confetti-144"></div> <div class="confetti-143"></div> <div class="confetti-142"></div> <div class="confetti-141"></div> <div class="confetti-140"></div> <div class="confetti-139"></div> <div class="confetti-138"></div> <div class="confetti-137"></div> <div class="confetti-136"></div> <div class="confetti-135"></div> <div class="confetti-134"></div> <div class="confetti-133"></div> <div class="confetti-132"></div> <div class="confetti-131"></div> <div class="confetti-130"></div> <div class="confetti-129"></div> <div class="confetti-128"></div> <div class="confetti-127"></div> <div class="confetti-126"></div> <div class="confetti-125"></div> <div class="confetti-124"></div> <div class="confetti-123"></div> <div class="confetti-122"></div> <div class="confetti-121"></div> <div class="confetti-120"></div> <div class="confetti-119"></div> <div class="confetti-118"></div> <div class="confetti-117"></div> <div class="confetti-116"></div> <div class="confetti-115"></div> <div class="confetti-114"></div> <div class="confetti-113"></div> <div class="confetti-112"></div> <div class="confetti-111"></div> <div class="confetti-110"></div> <div class="confetti-109"></div> <div class="confetti-108"></div> <div class="confetti-107"></div> <div class="confetti-106"></div> <div class="confetti-105"></div> <div class="confetti-104"></div> <div class="confetti-103"></div> <div class="confetti-102"></div> <div class="confetti-101"></div> <div class="confetti-100"></div> <div class="confetti-99"></div> <div class="confetti-98"></div> <div class="confetti-97"></div> <div class="confetti-96"></div> <div class="confetti-95"></div> <div class="confetti-94"></div> <div class="confetti-93"></div> <div class="confetti-92"></div> <div class="confetti-91"></div> <div class="confetti-90"></div> <div class="confetti-89"></div> <div class="confetti-88"></div> <div class="confetti-87"></div> <div class="confetti-86"></div> <div class="confetti-85"></div> <div class="confetti-84"></div> <div class="confetti-83"></div> <div class="confetti-82"></div> <div class="confetti-81"></div> <div class="confetti-80"></div> <div class="confetti-79"></div> <div class="confetti-78"></div> <div class="confetti-77"></div> <div class="confetti-76"></div> <div class="confetti-75"></div> <div class="confetti-74"></div> <div class="confetti-73"></div> <div class="confetti-72"></div> <div class="confetti-71"></div> <div class="confetti-70"></div> <div class="confetti-69"></div> <div class="confetti-68"></div> <div class="confetti-67"></div> <div class="confetti-66"></div> <div class="confetti-65"></div> <div class="confetti-64"></div> <div class="confetti-63"></div> <div class="confetti-62"></div> <div class="confetti-61"></div> <div class="confetti-60"></div> <div class="confetti-59"></div> <div class="confetti-58"></div> <div class="confetti-57"></div> <div class="confetti-56"></div> <div class="confetti-55"></div> <div class="confetti-54"></div> <div class="confetti-53"></div> <div class="confetti-52"></div> <div class="confetti-51"></div> <div class="confetti-50"></div> <div class="confetti-49"></div> <div class="confetti-48"></div> <div class="confetti-47"></div> <div class="confetti-46"></div> <div class="confetti-45"></div> <div class="confetti-44"></div> <div class="confetti-43"></div> <div class="confetti-42"></div> <div class="confetti-41"></div> <div class="confetti-40"></div> <div class="confetti-39"></div> <div class="confetti-38"></div> <div class="confetti-37"></div> <div class="confetti-36"></div> <div class="confetti-35"></div> <div class="confetti-34"></div> <div class="confetti-33"></div> <div class="confetti-32"></div> <div class="confetti-31"></div> <div class="confetti-30"></div> <div class="confetti-29"></div> <div class="confetti-28"></div> <div class="confetti-27"></div> <div class="confetti-26"></div> <div class="confetti-25"></div> <div class="confetti-24"></div> <div class="confetti-23"></div> <div class="confetti-22"></div> <div class="confetti-21"></div> <div class="confetti-20"></div> <div class="confetti-19"></div> <div class="confetti-18"></div> <div class="confetti-17"></div> <div class="confetti-16"></div> <div class="confetti-15"></div> <div class="confetti-14"></div> <div class="confetti-13"></div> <div class="confetti-12"></div> <div class="confetti-11"></div> <div class="confetti-10"></div> <div class="confetti-9"></div> <div class="confetti-8"></div> <div class="confetti-7"></div> <div class="confetti-6"></div> <div class="confetti-5"></div> <div class="confetti-4"></div> <div class="confetti-3"></div> <div class="confetti-2"></div> <div class="confetti-1"></div> <div class="confetti-0"></div> </div> </div> </template> <style scoped lang="scss"> .container { position: absolute; top: 0; bottom: 0; right: 0; left: 0; overflow: hidden; } .wrapper { position: relative; height: 100vh; display: flex; flex-wrap: wrap; } .logo { display: flex; justify-content: center; margin: auto; } [class|='confetti'] { position: absolute; } $colors: (#d13447, #ffbf00, #263672); @for $i from 0 through 200 { $w: random(8); $l: random(100); .confetti-#{$i} { width: #{$w}px; height: #{$w * 0.4}px; background-color: nth($colors, random(3)); top: -10%; left: unquote($l + '%'); opacity: random() + 0.5; transform: rotate(#{random() * 360}deg); animation: drop-#{$i} unquote(4 + random() + 's') unquote(random() + 's'); } @keyframes drop-#{$i} { 100% { top: 110%; left: unquote($l + random(15) + '%'); } } } </style>