Skip to content

主動的開關 toggle

停用時切換狀態會和你唱反調。( ´థ౪థ)

靈感來自 Useless machine,這個小廢物可是 Maker 的浪漫。(´,,•ω•,,)

至於為甚麼要用貓手,因為貓手是我想的到最欠揍的小手手。ヾ(◍'౪`◍)ノ゙

使用範例

基本用法

開關停用時,切換開關會被貓貓手切回來。(◜௰◝)y

查看範例原始碼
vue
<template>
  <div class="w-full flex flex-col gap-4 border border-gray-300 p-6">
    <base-checkbox
      v-model="disabled"
      label="停用開關"
      class="border rounded p-4"
    />

    <div class="flex flex-1 items-center justify-center">
      <toggle-proactive
        v-model="value"
        :disabled="disabled"
      />
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import BaseCheckbox from '../../base-checkbox.vue'
import ToggleProactive from '../toggle-proactive.vue'

const disabled = ref(false)
const value = ref(false)
</script>

元件參數

樣式可隨意調整

查看範例原始碼
vue
<template>
  <div class="w-full flex flex-col items-center gap-10 border border-gray-300 p-8">
    <toggle-proactive
      :model-value="false"
      disabled
      size="3rem"
      fur-color="#DFC57B"
      pad-color="#FFF"
    />

    <toggle-proactive
      :model-value="true"
      disabled
      size="6rem"
      track-inactive-class="bg-red-400"
      track-active-class="bg-[#DFDFDF]"
      fur-color="#8D6F64"
      pad-color="#000"
    />

    <toggle-proactive
      :model-value="false"
      disabled
      size="4rem"
      track-active-class="bg-[#7DDAEA]"
      fur-color="#F3F2F2"
    />
  </div>
</template>

<script setup lang="ts">
import ToggleProactive from '../toggle-proactive.vue'
</script>

使用須知

讓使用者心甘情願(?閱讀須知。(´,,•ω•,,)

閱讀率 100% 時才能切換開關,否則會被強制回復。

🐟 鱈魚使用須知

📌 重要聲明

本指南適用於任何與鱈魚相關的活動,例如食用、觀賞、聊天、或試圖與其建立深厚友誼(不建議)。

🍽️ 食用須知

  • 請確保鱈魚已煮熟,除非你是北極熊。
  • 如果你發現鱈魚在盤子上對你微笑,請確認你沒有嗑藥。
  • 鱈魚富含不可名狀物質,吃多了可能出現幻覺。

🐠 觀賞須知

  • 鱈魚外觀樸素,請勿種族歧視。
  • 請勿在水族館對著鱈魚說「你好肥」,牠們也有自尊心。

💬 與鱈魚溝通須知

  • 鱈魚不會講話,請不要對牠進行長篇演講。
  • 如果鱈魚對你點頭,請不要高興得太早,牠可能只是因為水流晃動。
  • 與鱈魚進行心靈溝通時,請確保你沒有餓過頭導致出現幻覺。

🚨 禁忌事項

  1. 請勿將鱈魚放入洗衣機,可能也洗不乾淨。
  2. 請勿將鱈魚作為武器使用,除非已事先凍成冰塊。
  3. 請勿遛鱈魚,因為他不會走路

🎉 結語

請以尊重與幽默的態度對待鱈魚,如有任何不滿,請記得它只是一隻魚

歡迎提出 MR 補充以上須知

閱讀率:0.0%

UI:「這裡不應該用 toggle 吧! Σ(ˊДˋ;)」

查看範例原始碼
vue
<template>
  <div class="relative w-full flex flex-center flex-col gap-4 border border-gray-300 p-6">
    <div class="h-[40vh] overflow-y-auto border rounded-xl p-4">
      <h1>🐟 鱈魚使用須知</h1>

      <h2 :ref="titleRefList.set">
        📌 重要聲明
      </h2>
      <p>本指南適用於任何與鱈魚相關的活動,例如食用、觀賞、聊天、或試圖與其建立深厚友誼(不建議)。</p>

      <h2 :ref="titleRefList.set">
        🍽️ 食用須知
      </h2>
      <ul>
        <li>請確保鱈魚已煮熟,除非你是北極熊。</li>
        <li>如果你發現鱈魚在盤子上對你微笑,請確認你沒有嗑藥。</li>
        <li>鱈魚富含不可名狀物質,吃多了可能出現幻覺。</li>
      </ul>

      <h2 :ref="titleRefList.set">
        🐠 觀賞須知
      </h2>
      <ul>
        <li>鱈魚外觀樸素,請勿種族歧視。</li>
        <li>請勿在水族館對著鱈魚說「你好肥」,牠們也有自尊心。</li>
      </ul>

      <h2 :ref="titleRefList.set">
        💬 與鱈魚溝通須知
      </h2>
      <ul>
        <li>鱈魚不會講話,請不要對牠進行長篇演講。</li>
        <li>如果鱈魚對你點頭,請不要高興得太早,牠可能只是因為水流晃動。</li>
        <li>與鱈魚進行心靈溝通時,請確保你沒有餓過頭導致出現幻覺。</li>
      </ul>

      <h2 :ref="titleRefList.set">
        🚨 禁忌事項
      </h2>
      <ol>
        <li>請勿將鱈魚放入洗衣機,可能也洗不乾淨。</li>
        <li>請勿將鱈魚作為武器使用,除非已事先凍成冰塊。</li>
        <li>請勿遛鱈魚,因為他不會走路</li>
      </ol>

      <h2 :ref="titleRefList.set">
        🎉 結語
      </h2>
      <p>請以尊重與幽默的態度對待鱈魚,如有任何不滿,請記得它只是一隻魚</p>
      <p>歡迎提出 MR 補充以上須知</p>
    </div>

    閱讀率:{{ readRate.toFixed(1) }}%

    <label class="w-full flex-center cursor-pointer gap-6 border rounded-xl px-8 py-4 text-lg">
      我已詳閱以上須知

      <toggle-proactive
        v-model="value"
        :disabled
        size="2rem"
      />
    </label>

    <transition name="opacity">
      <div
        v-if="value"
        class="absolute inset-0 z-[40] flex flex-col items-center justify-center gap-6 rounded-xl bg-[#c7f6ff] bg-opacity-90"
      >
        <span class="text-xl tracking-wide">
          感謝您的閱讀!(*´∀`)~♥
        </span>
      </div>
    </transition>
  </div>
</template>

<script setup lang="ts">
import type { ComputedRef } from 'vue'
import { useTemplateRefsList } from '@vueuse/core'
import { map, pipe } from 'remeda'
import { computed, onMounted, ref } from 'vue'
import { useElementVisibilityTime } from '../../../composables/use-element-visibility-time'
import ToggleProactive from '../toggle-proactive.vue'

const titleRefList = useTemplateRefsList<HTMLElement>()

const timeList = ref<ComputedRef<number>[]>([])
onMounted(() => {
  timeList.value = titleRefList.value.map((el) => {
    const { totalVisibleTime } = useElementVisibilityTime(el)
    return totalVisibleTime
  })
})

/** 最小閱讀時間 */
const MIN_READ_MS = 1000

/** 閱讀率 */
const readRate = computed(() => pipe(
  timeList.value,
  map((time) => time.value > MIN_READ_MS ? MIN_READ_MS : time.value),
  (timeList) => {
    const total = timeList.reduce((acc, time) => acc + time, 0)
    if (total === 0)
      return 0

    return total / (MIN_READ_MS * timeList.length) * 100
  },
))

const disabled = computed(() => readRate.value < 100)
const value = ref(false)
</script>

<style lang="sass" scoped>
.opacity-enter-active, .opacity-leave-active
  transition-duration: 0.4s
.opacity-enter-from, .opacity-leave-to
  opacity: 0 !important
</style>

不可能的事

可愛又不失禮貌地打槍客戶。ヾ(◍'౪`◍)ノ゙

查看範例原始碼
vue
<template>
  <div class="w-full flex-center border border-gray-300 p-10">
    <div class="flex flex-col gap-4">
      <label
        v-for="state in stateList"
        :key="state.label"
        class="flex cursor-pointer items-center justify-end gap-5"
      >
        <div class="text-2xl">
          {{ state.label }}
        </div>

        <toggle-proactive
          ref="toggleRefList"
          v-model="state.value"
          v-bind="colorData"
          size="3.5rem"
        />
      </label>
    </div>
  </div>
</template>

<script setup lang="ts">
import { useCycleList } from '@vueuse/core'
import { pipe, reduce, sample } from 'remeda'
import { computed, ref, watch } from 'vue'
import ToggleProactive from '../toggle-proactive.vue'

interface State {
  value: boolean;
  label: string;
}

type Toggle = InstanceType<typeof ToggleProactive>

const toggleRefList = ref<Toggle[]>()

const {
  state: colorData,
  next: nextColor,
} = useCycleList([
  {
    furColor: '#7DDAEA',
    padColor: '#000',
  },
  {
    furColor: '#FAFAFA',
    padColor: '#FFA5A5',
  },
  {
    furColor: '#DFC57B',
    padColor: '#000',
  },
  {
    furColor: '#8D6F64',
    padColor: '#FFA5A5',
  },
  {
    furColor: '#444',
    padColor: '#FFA5A5',
  },
  {
    furColor: '#F3F2F2',
    padColor: '#000',
  },
])

const stateList = ref<State[]>([
  {
    label: '要快',
    value: false,
  },
  {
    label: '要好',
    value: false,
  },
  {
    label: '要便宜',
    value: false,
  },
])
const booleanList = computed(
  () => stateList.value.map((state) => state.value),
)

watch(booleanList, (value, oldValue) => {
  const allTrue = value.every((v) => v)
  if (!allTrue) {
    return
  }

  const targetIndex = pipe(
    oldValue,
    /** 排除最後一個切換的開關 */
    reduce(
      (acc: number[], boolValue, i) => boolValue ? [...acc, i] : acc,
      [],
    ),
    sample(1),
    ([i]) => i ?? 0,
  )

  nextColor()

  toggleRefList.value?.[targetIndex]?.toggle()
})
</script>

原理

注意!Σ(ˊДˋ;)

請不要將 overflow 設定為 hidden,否則貓貓手會被狠心切割

利用 anime.js 製作 svg 動畫。

裡面有個小細節,就是貓手一開始藏在 toggle 背後(手臂與手肘都在背後),播放切換動畫時,手臂一樣在 toggle 背後,但是手肘會伸到 toggle 前面。

SVG 內的物件為甚麼有辦法忽然變換堆疊順序?大家來猜猜看如何實現這個效果。(´,,•ω•,,)

賣個關子,想知道的話可以看看原始碼或留言讓我知道你的猜想。( ´ ▽ ` )ノ

原始碼

API

Props

interface Props {
  modelValue: boolean;
  disabled?: boolean;
  /** @default '4rem' */
  size?: string;

  /** @default 'rounded-full' */
  trackClass?: string;
  /** @default 'bg-[#DFDFDF]' */
  trackInactiveClass?: string;
  /** @default 'bg-green-500' */
  trackActiveClass?: string;
  /** @default 'bg-white' */
  thumbClass?: string;
  /** @default '' */
  thumbInactiveClass?: string;
  /** @default '' */
  thumbActiveClass?: string;

  /** @default '#444' */
  furColor?: string;
  /** @default '#FFA5A5' */
  padColor?: string;
}

Emits

interface Emits {
  'update:modelValue': [value: boolean];
}

Methods

interface Expose {
  /** 觸發切換動畫 */
  toggle: () => Promise<void>;
}

v0.28.0