節日禮物待簽收,開箱國慶頭像

🎁 點擊這裡 獲取國慶頭像。通過 Vue3 + Vant(組件庫) + Vite 構建。

應用的流程

在做之前,先思考它的流程:

  1. 上傳頭像
  2. 選擇頭像模板
  3. 保存新頭像

進入應用放出節日祝福與簡單的使用介紹。

image

開始僅顯示一個上傳頭像按鈕。這樣做的好處是,直接固定第一步操作。如果將模板和保存按鈕顯示出來需要增加代碼做一些判斷。如果開始就將頭像模板與保存按鈕顯示,可能讓人無從下手。

image

上傳頭像後直接將模板與保存按鈕顯示出來。在代碼中默認選中今年最流行的模板,直接點擊保存按鈕將下載這個頭像。開屏也進行了選擇模板的提示,如果點擊一個頭像模板,將進行切換,通過點擊保存按鈕保存它。

image

最後,點擊保存顯示煙花動畫。

image

代碼實現

頭像上傳

<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="//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>

鏈接