每條游來游去的小魚,都是正在瀏覽這一頁的訪客,有一條是你自己 ( ´ ▽ ` )ノ 覺得干擾閱讀的話,可以把小魚關掉。
OSU 打擊鈕
button如同 OSU 遊戲的打擊鈕,看準時機點擊按鈕吧!(ゝ∀・)b
至於甚麼是 OSU 遊戲,可以來這裡看看。
| 名稱 | 描述 |
|---|---|
| Pointer 事件 | 偵測滑鼠或觸控點移動、點擊、懸停等等事件,取得座標、目標等等資訊 |
| JS 動畫 | 基於 JavaScript 實現的動畫,達成更複雜、精準的動畫控制,常見套件有 GSAP、anime.js 等 |
| Anime.js | 輕量級 JavaScript 動畫函式庫 |
觸碰後會出現接近圈圈,在圈圈大小與原本按鈕大小一致時,給他用力點下去!੭ ˙ᗜ˙ )੭
<template> <div class="w-full flex flex-col gap-4 example-wrap"> <div class="flex justify-center"> <btn-osu-hit label="Submit" /> </div> </div></template>
<script setup lang="ts">import BtnOsuHit from '../btn-osu-hit.vue'</script>Perfect 判定才會觸發事件,同時縮短反應時間,氣死你同事吧 ᕕ( ゚ ∀。)ᕗ
真的不訂飲料?( ・ิω・ิ)貼心小提醒,您即將損失:
- 一杯好喝的飲料
- 悠閒的午休時光
- 美好的心情
操作已完成 (╥ω╥`)
<template> <div class="relative flex flex-col gap-3 border border-red-300 rounded-lg bg-red-100/50 p-6"> <span class="text-xl text-red-800 font-bold"> {{ t('dontOrderDrink') }} </span>
<span class="mt-2 text-red-700"> {{ t('reminder') }} </span>
<ul class="list-disc list-inside text-sm text-red-600 !m-0"> <li>{{ t('loss1') }}</li> <li>{{ t('loss2') }}</li> <li>{{ t('loss3') }}</li> </ul>
<div class="flex justify-end gap-4"> <base-btn :label="t('cancelBtn')" />
<btn-osu-hit :reaction-time="400" @perfect="handleClick" > <base-btn :label="t('confirmBtn')" class="text-white active:scale-95 !border-none !bg-red-600" /> </btn-osu-hit> </div>
<div class="pointer-events-none absolute inset-0 flex items-center justify-center bg-black/90 duration-500" :class="done ? ' opacity-100' : 'opacity-0'" > <span class="text-2xl text-white font-bold"> {{ t('doneTip') }} </span> </div> </div></template>
<script setup lang="ts">import { ref } from 'vue'import { useI18n } from 'vue-i18n'import BaseBtn from '../../base-btn.vue'import BtnOsuHit from '../btn-osu-hit.vue'
const { t } = useI18n()
const done = ref(false)function handleClick() { done.value = true setTimeout(() => { done.value = false }, 2000)}</script>來挑戰看看吧!ヾ(◍'౪`◍)ノ゙
照著節奏依序按下按鈕
全 Perfect 判定即可解鎖 9 折優惠券!੭ ˙ᗜ˙ )੭
✨ 領取優惠券
<template> <div class="w-full flex flex-col gap-10 example-wrap"> <div class="flex flex-col gap-1 text-center text-sm"> <div> {{ t('tip') }} </div> <div> {{ t('tip2') }} </div> </div>
<div class="flex flex-col items-center justify-between gap-4 sm:flex-row"> <btn-osu-hit v-for="item, i in btnList" :key="i" :ref="(ref: any) => item.ref = ref" v-model:hit-result="item.hitResult" v-model:is-start="item.isStart" :label="item.label" class="duration-500" :class="{ 'opacity-10 pointer-events-none': !item.isStart && i !== 0, }" :enable-enter-start="i === 0" @start="start(i)" /> </div>
<div class="coupon-btn" :class="{ '!cursor-not-allowed opacity-20': !allPerfect }" @click="handleCouponClick" > {{ t('couponBtn') }} </div> </div></template>
<script setup lang="ts">import type { ComponentProps } from 'vue-component-type-helpers'import { promiseTimeout } from '@vueuse/core'import { computed, ref } from 'vue'import { useI18n } from 'vue-i18n'import BtnOsuHit from '../btn-osu-hit.vue'
type BtnProps = ComponentProps<typeof BtnOsuHit>
const { t } = useI18n()
const btnList = ref<Array<{ label: string; isStart: boolean; hitResult: BtnProps['hitResult']; ref?: InstanceType<typeof BtnOsuHit>;}>>([ { label: t('perfect'), isStart: false, hitResult: undefined }, { label: t('click'), isStart: false, hitResult: undefined }, { label: t('just'), isStart: false, hitResult: undefined }, { label: t('unlock'), isStart: false, hitResult: undefined }, { label: t('coupon'), isStart: false, hitResult: undefined },])
const anyStarted = computed( () => btnList.value.some(({ isStart }) => isStart),)
const allPerfect = computed( () => !btnList.value.some(({ hitResult }) => hitResult !== 'perfect'),)
function start(index: number) { if (index !== 0 || anyStarted.value) { return }
btnList.value.forEach(async (item, i) => { item.hitResult = undefined
if (i === 0) { return }
await promiseTimeout(i * 1000) item.ref?.startBeat() })}
function handleCouponClick() { if (!allPerfect.value) { return }
window.open('https://165.npa.gov.tw/', '_blank')}</script>
<style scoped>.coupon-btn { display: inline-block; padding: 12px 24px; background: linear-gradient(135deg, #ff7a00, #ff3d00); color: #fff; font-size: 30px; font-weight: bold; border-radius: 12px; text-align: center; cursor: pointer; transition: 0.25s; box-shadow: 0 4px 10px rgba(255, 100, 0, 0.4); user-select: none;}
.coupon-btn:hover { transform: translateY(-3px); box-shadow: 0 6px 14px rgba(255, 100, 0, 0.5);}
.coupon-btn:active { transform: translateY(0); box-shadow: 0 3px 8px rgba(255, 100, 0, 0.35);}</style>使用 slot 可以調整判定結果顯示內容
客訴回饋單
若您有任何疑慮,請在此填寫您的意見,我們將會在第一時間處理您的問題。
連擊「解鎖」按鈕即可解鎖客訴回饋單
20 combo 即可解鎖!您一定可以的!(ゝ∀・)b
<template> <div class="relative w-full flex flex-col gap-10 example-wrap"> <div class="flex flex-col gap-3" :class="{ 'opacity-40 pointer-events-none': !isUnlock }" > <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 :placeholder="t('inputPlaceholder')" class="min-h-[30vh] w-full" /> </div> <base-btn :label="t('submitBtn')" @click="handleSubmit" /> </div>
<div v-if="!isUnlock" class="mask absolute inset-0 flex flex-col items-center justify-center gap-5 rounded-xl" > <div class="flex flex-col gap-3 text-center"> <div class="text-xl font-bold"> {{ t('unlockTip') }} </div> <div class="text-sm opacity-50"> {{ t('unlockTip2', { unlockComboCount: UNLOCK_COMBO_COUNT }) }} </div> </div>
<btn-osu-hit ref="btnRef" v-model:is-start="isStart" :enable-enter-start="!isStart" :label="t('unlockBtn')" v-on="eventMap" > <template #perfect> <div class="result-text pointer-events-none absolute inset-0 flex-center text-3xl font-bold"> {{ comboCount }} combo! </div> </template>
<template #great> <div class="result-text pointer-events-none absolute inset-0 flex-center text-3xl font-bold"> {{ comboCount }} combo! </div> </template> </btn-osu-hit> </div> </div></template>
<script setup lang="ts">import { promiseTimeout, whenever } from '@vueuse/core'import { ref, useTemplateRef } from 'vue'import { useI18n } from 'vue-i18n'import BaseBtn from '../../base-btn.vue'import BtnOsuHit from '../btn-osu-hit.vue'
const UNLOCK_COMBO_COUNT = 20
const { t } = useI18n()const btnRef = useTemplateRef('btnRef')const comboCount = ref(0)const isUnlock = ref(false)whenever( () => comboCount.value >= UNLOCK_COMBO_COUNT, () => isUnlock.value = true,)
const isStart = ref(false)
const eventMap = { async perfect() { comboCount.value++ await promiseTimeout(500) btnRef.value?.startBeat() }, async great() { comboCount.value++ await promiseTimeout(500) btnRef.value?.startBeat() }, good() { comboCount.value = 0 }, miss() { comboCount.value = 0 },}
function handleSubmit() { // eslint-disable-next-line no-alert alert(t('thanks'))}</script>
<style scoped lang="sass">.result-text animation: result-text 0.6s forwards -webkit-text-stroke: 5px white paint-order: stroke fill text-wrap: nowrap color: #FFD700 -webkit-text-stroke: 5px #f77816 font-weight: 900
@keyframes result-text 0% opacity: 0 transform: scale(0.8) 10%, 50% opacity: 1 transform: scale(1) 100% opacity: 0
.mask backdrop-filter: blur(2px)</style>以 Anime.js 實作動畫並在點擊後依照縮放數值判斷打擊結果。
interface Props { label?: string; /** 反應時間,單位:毫秒 */ reactionTime?: number; /** pointer enter 是否觸發 start */ enableEnterStart?: boolean;}interface Emits { start: []; perfect: []; great: []; good: []; miss: [];}Methods
Section titled “Methods”interface Expose { /** 開始打擊,會出現 Approach Circle */ startBeat: () => void;}interface Slots { default?: () => unknown; perfect?: () => unknown; great?: () => unknown; good?: () => unknown; miss?: () => unknown;}