拼圖按鈕 button
拚完所有拼圖才能按的按鈕 ◝( ゚ ∀ ゚ )◟
技術關鍵字
| 名稱 | 描述 |
|---|---|
| Pointer 事件 | 偵測滑鼠或觸控點移動、點擊、懸停等等事件,取得座標、目標等等資訊 |
| SVG Mask | SVG 遮罩效果,用於控制元素的顯示區域,可以實現複雜的形狀切割和遮罩 |
| JS 動畫 | 基於 JavaScript 實現的動畫,達成更複雜、精準的動畫控制,常見套件有 GSAP、anime.js 等 |
使用範例
基本用法
拖動碎片,完成拼圖吧
查看範例原始碼
vue
<template>
<div class="w-full flex flex-col gap-4 border border-gray-200 rounded-xl p-6">
<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(欸?
客訴回饋單
若您有任何疑慮,請在此填寫您的意見,我們將會在第一時間處理您的問題。
為防止濫用,完成拼圖後才能送出 (*´∀`)~♥
查看範例原始碼
vue
<template>
<div class="relative w-full flex flex-col gap-10 border border-gray-200 rounded-xl p-6">
<div class="flex flex-col gap-3">
<div class="text-3xl font-bold">
{{ t('complaintFeedbackForm') }}
</div>
<div class="text-sm text-gray-500">
{{ 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">
{{ 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 秒內完成拼圖後才能點擊送出 (*´∀`)~♥
路人:「這連真人都防住啦!ლ(´口`ლ)」
查看範例原始碼
vue
<template>
<div class="relative w-full flex flex-col gap-10 border border-gray-200 rounded-xl p-6">
<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">
選擇「鱈魚亞洲演唱會」門票:
</div>
<div class="text-sm text-gray-500">
時間:2030/02/30 03:00<br>
地點:台灣海峽 - 24°19'22.3"N 119°58'43.3"E
</div>
<div class="grid grid-cols-1 gap-3">
<label
v-for="ticket in ticketOptions"
: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 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">
為防止濫用,請在 {{ 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">
購買
</button>
</btn-jigsaw-puzzle>
</div>
<div class="text-center text-xs text-red-500">
{{ isActive ? `剩餘 ${timerCount} 秒` : ' ' }}
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useIntervalFn } from '@vueuse/core'
import { 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 ticketOptions = ref([
{
id: 't1',
area: '搖滾區',
price: 5200,
note: '在喇叭上,保證超搖滾',
},
{
id: 't2',
area: 'VIP 站區',
price: 4200,
note: '提供錨,不怕嗨到浮出水面',
},
{
id: 't3',
area: '奇怪視線區',
price: 3200,
note: '會看到很多不該看的東西',
},
])
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('此票已售完,請重新選擇')
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 Mask 實作拼圖分割,並使用 Pointer 事件實作拖曳效果。
原始碼
API
Props
interface Props {
/** 按鈕內文字 */
label?: string;
/** 是否停用 */
disabled?: boolean;
/** 同 CSS z-index */
zIndex?: number | string;
/** 同 html tabindex */
tabindex?: number | string;
/** 拼圖行數 */
rowCount?: number;
/** 拼圖列數 */
colCount?: number;
}Emits
const emit = defineEmits<{
/** 開始拖動 */
dragStart: [piece: Piece, evt: PointerEvent];
dragging: [piece: Piece, evt: PointerEvent];
dragStop: [piece: Piece, evt: PointerEvent];
completed: [];
click: [];
}>()Methods
defineExpose({
/** 打散拼圖 */
scatter,
/** 自動完成 */
autoComplete,
})Slots
defineSlots<{
/** 按鈕 */
default?: (params: { isAllCompleted: boolean }) => unknown;
}>()