每條游來游去的小魚,都是正在瀏覽這一頁的訪客,有一條是你自己 ( ´ ▽ ` )ノ 覺得干擾閱讀的話,可以把小魚關掉。
拼圖按鈕
button拚完所有拼圖才能按的按鈕 ◝( ゚ ∀ ゚ )◟
| 名稱 | 描述 |
|---|---|
| Pointer 事件 | 偵測滑鼠或觸控點移動、點擊、懸停等等事件,取得座標、目標等等資訊 |
| JS 動畫 | 基於 JavaScript 實現的動畫,達成更複雜、精準的動畫控制,常見套件有 GSAP、anime.js 等 |
| Anime.js | 輕量級 JavaScript 動畫函式庫 |
| SVG | 可在 DOM 中使用的圖形格式,適合複雜圖形和動畫,配合 Vue v-bind 可以實現基於資料的動態效果 |
拖動碎片,完成拼圖吧
<template> <div class="w-full flex flex-col gap-4 example-wrap"> <div class="flex justify-center"> <btn-jigsaw-puzzle :label="t('clickMe')" @click="handleClick" /> </div> </div></template>
<script setup lang="ts">import { useI18n } from 'vue-i18n'import BtnJigsawPuzzle from '../btn-jigsaw-puzzle.vue'
const { t } = useI18n()
function handleClick() { // eslint-disable-next-line no-alert alert(t('congratulations'))}</script>阻擋機器人和討厭的客訴吧!(・∀・)9(欸?
客訴回饋單
若您有任何疑慮,請在此填寫您的意見,我們將會在第一時間處理您的問題。
為防止濫用,完成拼圖後才能送出 (*´∀`)~♥
<template> <div class="relative w-full flex flex-col gap-10 example-wrap"> <div class="flex flex-col gap-3"> <div class="text-3xl font-bold"> {{ t('complaintFeedbackForm') }} </div> <div class="text-sm text-gray-500 dark:text-gray-400"> {{ t('complaintFeedbackFormTip') }} </div>
<div class="flex justify-center border border-gray-200 rounded-lg p-3"> <textarea v-model="text" :placeholder="t('inputPlaceholder')" class="min-h-[30vh] w-full" /> </div>
<div class="w-full flex justify-center"> <btn-jigsaw-puzzle ref="jigsawPuzzleRef" :row-count="3" :col-count="4" @click="handleSubmit" > <button class="btn w-full select-none rounded p-3 px-20 text-3xl"> {{ t('submitBtn') }} </button> </btn-jigsaw-puzzle> </div>
<div class="mt-2 text-center text-xs text-gray-500 dark:text-gray-400"> {{ t('jigsawPuzzleTip') }} </div> </div> </div></template>
<script setup lang="ts">import { ref, useTemplateRef } from 'vue'import { useI18n } from 'vue-i18n'import BtnJigsawPuzzle from '../btn-jigsaw-puzzle.vue'
const { t } = useI18n()
const jigsawPuzzleRef = useTemplateRef('jigsawPuzzleRef')
const text = ref('')function handleSubmit() { // eslint-disable-next-line no-alert alert(t('thanks'))
text.value = '' jigsawPuzzleRef.value?.scatter()}</script>
<style scoped lang="sass">.btn background-color: light-dark(#444, #EEE) color: light-dark(#EEE, #444) transition-duration: 0.2s &:active transition-duration: 0.1s transform: scale(0.98)</style>防止壞壞的搶票機器人吧!(⌐■_■)✧
選擇「鱈魚亞洲演唱會」門票:
時間:2030/02/30 03:00
地點:台灣海峽 - 24°19'22.3"N 119°58'43.3"E
地點:台灣海峽 - 24°19'22.3"N 119°58'43.3"E
為防止濫用,請在 4 秒內完成拼圖後才能點擊送出 (*´∀`)~♥
路人:「這連真人都防住啦!ლ(´口`ლ)」
查看範例原始碼
<template> <div class="relative w-full flex flex-col gap-10 example-wrap"> <div class="flex flex-col gap-3"> <div class="max-w-xl w-full flex flex-col gap-3"> <div class="mb-1 text-lg font-semibold"> {{ t('selectTicket') }} </div>
<div class="text-sm text-gray-500 dark:text-gray-400"> {{ t('eventTime') }}<br> {{ t('eventLocation') }} </div>
<div class="grid grid-cols-1 gap-3"> <label v-for="ticket in ticketOptionList" :key="ticket.id" class="cursor-pointer" > <input v-model="selectedTicketId" type="radio" class="peer hidden" :value="ticket.id" >
<div class="w-full flex flex-col gap-1 border rounded-lg px-4 py-3 text-sm transition-all hover:border-blue-400 peer-checked:border-blue-500 peer-checked:bg-blue-50" > <div class="flex items-center justify-between"> <div class="text-lg text-gray-500 dark:text-gray-400 font-black"> {{ ticket.area }} </div> <div class="text-base font-bold"> NT$ {{ ticket.price.toLocaleString() }} </div> </div>
<div v-if="ticket.note" class="text-xs text-amber-600" > {{ ticket.note }} </div> </div> </label> </div> </div>
<div class="py-2 text-center text-gray-500 dark:text-gray-400"> {{ t('timerTip', { seconds: MAX_TIMER_COUNT }) }} </div>
<div class="w-full flex justify-center duration-300" :class="{ 'opacity-50 pointer-events-none': !selectedTicketId }" > <btn-jigsaw-puzzle ref="jigsawPuzzleRef" @drag-start="handleDragStart" @click="handleSubmit" > <button class="btn w-full select-none rounded p-3 px-6 text-3xl"> {{ t('buyButton') }} </button> </btn-jigsaw-puzzle> </div>
<div class="text-center text-xs text-red-500"> {{ isActive ? t('remainingTime', { seconds: timerCount }) : ' ' }} </div> </div> </div></template>
<script setup lang="ts">import { useIntervalFn } from '@vueuse/core'import { computed, ref, useTemplateRef } from 'vue'import { useI18n } from 'vue-i18n'import BtnJigsawPuzzle from '../btn-jigsaw-puzzle.vue'
const { t } = useI18n()
const MAX_TIMER_COUNT = 4
const jigsawPuzzleRef = useTemplateRef('jigsawPuzzleRef')
const timerCount = ref(MAX_TIMER_COUNT)const { isActive, pause, resume } = useIntervalFn(() => { timerCount.value--
if (timerCount.value <= 0) { reset() }}, 1000, { immediate: false,})
const ticketOptionList = computed(() => [ { id: 't1', area: t('rockZone'), price: 5200, note: t('rockZoneNote'), }, { id: 't2', area: t('vipZone'), price: 4200, note: t('vipZoneNote'), }, { id: 't3', area: t('weirdZone'), price: 3200, note: t('weirdZoneNote'), },])const selectedTicketId = ref('')
function reset() { selectedTicketId.value = '' jigsawPuzzleRef.value?.scatter() pause() timerCount.value = MAX_TIMER_COUNT}
function handleDragStart() { if (isActive.value) { return } resume()}function handleSubmit() { // eslint-disable-next-line no-alert alert(t('soldOut')) reset()}</script>
<style scoped lang="sass">.btn background-color: light-dark(#444, #EEE) color: light-dark(#EEE, #444) transition-duration: 0.2s &:active transition-duration: 0.1s transform: scale(0.98)</style>使用 SVG clipPath 實作拼圖分割,並使用 Pointer 事件實作拖曳效果。
interface Props { /** 按鈕內文字 */ label?: string; /** 是否停用 */ disabled?: boolean; /** 同 CSS z-index */ zIndex?: number | string; /** 同 html tabindex */ tabindex?: number | string;
/** 拼圖行數 */ rowCount?: number; /** 拼圖列數 */ colCount?: number;}const emit = defineEmits<{ /** 開始拖動 */ dragStart: [piece: Piece, evt: PointerEvent]; dragging: [piece: Piece, evt: PointerEvent]; dragStop: [piece: Piece, evt: PointerEvent]; completed: []; click: [];}>()Methods
Section titled “Methods”interface Expose { /** 打散拼圖 */ scatter: () => void; /** 自動完成 */ autoComplete: () => void;}defineSlots<{ /** 按鈕 */ default?: (params: { isAllCompleted: boolean }) => unknown;}>()