Skip to content

亂丟的清單 list

資料被刪除時,對應的項目會被蛋雕!⎝(・∀´・⎝)

TIP

感謝 Line 社群「新手導向前端學習讀書會」的孟德大大提供靈感 (*´∀`)~♥

使用範例

基本用法

可以調整丟掉方式,不管是拋出、漂浮,還是要甩在臉上都可以。(ゝ∀・)b

寫 1 分鐘程式,就有 60 秒過去
人和程式,一個能跑就行
要改得太多了,不如改天吧
寫程式的時候像神,debug 的時候像鬼
程式如詩寫,美到無人解
程式碼會說話,只是你聽不懂它在罵你
查看範例原始碼
vue
<template>
  <div class="flex flex-col gap-4">
    <div class="h-[50vh] flex flex-col items-center">
      <list-throw
        v-slot="{ item }"
        :items
        class="h-full w-full flex flex-col gap-2 overflow-auto border rounded bg-gray-50 p-4 md:w-1/2"
        :class="{ 'bg-gray-900': isDark }"
        throw-item-class="z-50"
        :leave-type
      >
        <div
          class="border rounded bg-white p-2 px-3 text-center"
          :class="{ 'bg-gray-700': isDark }"
        >
          {{ item }}
        </div>
      </list-throw>
    </div>

    <div class="flex gap-4">
      <base-btn
        label="增加"
        class="flex-1"
        @click="add"
      />
      <base-btn
        label="減少"
        class="flex-1"
        @click="remove"
      />
    </div>

    <select-stepper
      v-model="leaveType"
      label="丟法"
      class="w-full"
      :options="leaveTypeOptions"
    />
  </div>
</template>

<script setup lang="ts">
import type { ComponentProps } from 'vue-component-type-helpers'
import { throttle } from 'lodash-es'
import { clone, pipe, shuffle } from 'remeda'
import { useData } from 'vitepress'
import { ref } from 'vue'
import BaseBtn from '../../base-btn.vue'
import SelectStepper from '../../select-stepper.vue'
import ListThrow from '../list-throw.vue'

const { isDark } = useData()

type Props = ComponentProps<typeof ListThrow>
type LayoutType = NonNullable<Props['leaveType']>

const SENTENCE_LIST = [
  '寫 1 分鐘程式,就有 60 秒過去',
  '人和程式,一個能跑就行',
  '要改得太多了,不如改天吧',
  '寫程式的時候像神,debug 的時候像鬼',
  '程式如詩寫,美到無人解',
  '程式碼會說話,只是你聽不懂它在罵你',
  '開發軟體和建造教堂非常像,完工後我們開始祈禱',
  '殺一個軟體工程師不需用槍,改三次需求即可',
  'Java 與 JavaScript 的關係,就如同狗和熱狗',
]

function createSentenceGetter() {
  let list = clone(SENTENCE_LIST)

  return () => {
    if (list.length === 0) {
      list = pipe(
        SENTENCE_LIST,
        clone(),
        shuffle(),
      )
    }

    return list.shift()
  }
}

const getSentence = createSentenceGetter()

const items = ref([
  getSentence(),
  getSentence(),
  getSentence(),
  getSentence(),
  getSentence(),
  getSentence(),
])

const add = throttle(() => {
  items.value.push(getSentence())
}, 100)

const remove = throttle(() => {
  const index = Math.floor(Math.random() * items.value.length)
  items.value.splice(index, 1)
}, 100)

const leaveType = ref<LayoutType>('throw')
const leaveTypeOptions: LayoutType[] = [
  'throw',
  'space',
  'fling',
  'hurl',
]
</script>

代辦清單

一個很有個性的代辦清單。( •̀ ω •́ )✧

(嘗試輸入鱈魚看看)

1.
2.
3.
4.
查看範例原始碼
vue
<template>
  <div class="w-full flex flex-col items-center gap-4">
    <div class="w-full flex flex-col gap-4 md:w-1/2">
      <base-input
        v-model="text"
        label="輸入事項"
        class=""
        @keydown.enter="add"
      />

      <base-btn
        label="新增"
        @click="add()"
      />

      <div class="mt-4 h-[50vh] flex flex-col items-center">
        <list-throw
          v-slot="{ item, index }"
          :items
          class="h-full w-full flex flex-col gap-2 overflow-auto border rounded bg-gray-50 p-4"
          :class="{ 'bg-gray-900': isDark }"
          throw-item-class="z-50"
          :get-key="prop('id')"
          :leave-type
        >
          <div
            class="flex items-center gap-2 border rounded bg-white p-2 px-3"
            :class="{ 'bg-gray-700': isDark }"
          >
            <base-checkbox
              v-model="item.done"
              class="pt-[2px]"
              @click="check(item)"
            />
            {{ index + 1 }}.

            <div class="flex-1">
              <input
                v-model="item.text"
                type="text"
                class="input w-full"
                @input="clearText(item)"
              >
            </div>

            <div
              class="cursor-pointer select-none"
              @click="remove(index)"
            >

            </div>
          </div>
        </list-throw>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import type { ComponentProps } from 'vue-component-type-helpers'
import { promiseTimeout } from '@vueuse/core'
import { nanoid } from 'nanoid'
import { prop } from 'remeda'
import { useData } from 'vitepress'
import { nextTick, ref } from 'vue'
import BaseBtn from '../../base-btn.vue'
import BaseCheckbox from '../../base-checkbox.vue'
import BaseInput from '../../base-input.vue'
import ListThrow from '../list-throw.vue'

const { isDark } = useData()

type Props = ComponentProps<typeof ListThrow>
type LayoutType = NonNullable<Props['leaveType']>

interface Item {
  id: string;
  done: boolean;
  text: string;
}

const leaveType = ref<LayoutType>('throw')
const items = ref<Item[]>([
  {
    id: nanoid(),
    done: false,
    text: '分享酷酷的元件',
  },
  {
    id: nanoid(),
    done: false,
    text: '打勾飄走',
  },
  {
    id: nanoid(),
    done: false,
    text: '刪除丟掉',
  },
  {
    id: nanoid(),
    done: false,
    text: '清空砸你臉',
  },
])

async function check(item: Item) {
  leaveType.value = 'space'
  await nextTick()

  items.value = items.value.filter(({ id }) => id !== item.id)
}

async function clearText(item: Item) {
  if (item.text !== '') {
    return
  }
  leaveType.value = 'hurl'
  await nextTick()

  items.value = items.value.filter(({ id }) => id !== item.id)
}

const text = ref('')
async function add() {
  const value = text.value.trim()
  text.value = ''

  if (!value) {
    return
  }

  items.value.push({
    id: nanoid(),
    done: false,
    text: value,
  })

  if (/cod|鱈魚/.test(value)) {
    leaveType.value = 'fling'
    await promiseTimeout(400)
    items.value = items.value.filter(({ text }) => !/cod|鱈魚/.test(text))
  }
}

async function remove(i: number) {
  leaveType.value = 'throw'
  await nextTick()

  items.value.splice(i, 1)
}
</script>

<style lang="sass" scoped>
.input
  border-bottom: 1px solid #DDD
</style>

購物車

毛病很多的購物車 ...( 0 д0)

商品
購物車
查看範例原始碼
vue
<template>
  <div class="flex flex-col items-center gap-4">
    <div class="h-[70vh] flex flex-col gap-4 p-6">
      <div class="text-sm opacity-60">
        商品
      </div>

      <div class="flex gap-2">
        <base-btn
          v-for="product in products"
          :key="product.id"
          :label="product.name"
          @click="addToCart(product)"
        />
      </div>

      <div class="mt-2 text-sm opacity-60">
        購物車
      </div>

      <list-throw
        v-slot="{ item, index }"
        :items="cartItems"
        class="h-full w-full flex flex-col gap-4 overflow-auto border rounded bg-gray-50 p-6"
        :class="{ 'bg-gray-900': isDark }"
        throw-item-class="z-50"
        :get-key="prop('id')"
        :leave-type
      >
        <div
          class="flex items-center gap-4 border rounded bg-white p-4"
          :class="{ 'bg-gray-900': isDark }"
        >
          <span class="font-semibold">
            {{ index + 1 }}. {{ item.name }}
          </span>
          <span class="ml-auto">
            x{{ item.quantity }}
          </span>

          <div
            class="ml-4 cursor-pointer select-none text-red-500 hover:text-red-700"
            @click="remove(index)"
          >

          </div>
        </div>
      </list-throw>
    </div>

    <div class="fixed bottom-4 right-4 flex flex-col gap-2">
      <transition-group
        name="opacity"
        tag="div"
        class="flex flex-col gap-2"
      >
        <div
          v-for="(message, index) in messages"
          :key="index"
          class="message w-[14rem] rounded bg-blue-500 p-3 text-center text-white"
          :class="message.type"
        >
          {{ message.value }}
        </div>
      </transition-group>
    </div>
  </div>
</template>

<script setup lang="ts">
import type { ComponentProps } from 'vue-component-type-helpers'
import { promiseTimeout } from '@vueuse/core'
import { nanoid } from 'nanoid'
import { prop } from 'remeda'
import { useData } from 'vitepress'
import { nextTick, ref } from 'vue'
import BaseBtn from '../../base-btn.vue'
import ListThrow from '../list-throw.vue'

const { isDark } = useData()

type Props = ComponentProps<typeof ListThrow>

interface Product {
  id: string;
  name: string;
}

interface CartItem extends Product {
  quantity: number;
}

interface Message {
  id: string;
  value: string;
  type: 'info' | 'error';
}

const products = ref<Product[]>([
  { id: nanoid(), name: '鱈魚' },
  { id: nanoid(), name: '飲料' },
  { id: nanoid(), name: '薯條' },
  { id: nanoid(), name: '炸雞' },
])

const cartItems = ref<CartItem[]>([])
const leaveType = ref<Props['leaveType']>('throw')
const messages = ref<Message[]>([])

async function checkCart() {
  leaveType.value = 'fling'

  // 不要魚
  if (cartItems.value.some((item) => item.name === '鱈魚')) {
    await promiseTimeout(300)
    addMessage('我討厭魚', 'error')
    cartItems.value = cartItems.value.filter((item) => item.name !== '鱈魚')
  }

  // 冷熱不要放在一起
  if (cartItems.value.some((item) => item.name === '飲料') && cartItems.value.length >= 2) {
    await promiseTimeout(300)
    addMessage('冷熱放一起,看了不爽', 'error')
    cartItems.value = cartItems.value.filter((item) => item.name !== '飲料')
  }

  // 東西超過 5 個
  if (cartItems.value.some((item) => item.quantity > 4)) {
    addMessage('太多了很重', 'error')
    await nextTick()
    cartItems.value = cartItems.value.filter((item) => item.quantity <= 4)
  }
}

function addToCart(product: Product) {
  const existingItem = cartItems.value.find((item) => item.id === product.id)
  if (existingItem) {
    existingItem.quantity++
  }
  else {
    cartItems.value.push({ ...product, quantity: 1 })
  }

  checkCart()
}

function addMessage(
  message: string,
  type: Message['type'] = 'info',
) {
  messages.value.push({
    id: nanoid(),
    value: message,
    type,
  })

  if (messages.value.length > 5) {
    messages.value.shift()
    return
  }

  setTimeout(() => {
    messages.value.shift()
  }, 3000)
}

async function remove(index: number) {
  const target = cartItems.value[index]
  if (target?.name.includes('薯條')) {
    addMessage('薯條很讚,不準丟', 'error')
    return
  }

  leaveType.value = 'throw'
  await nextTick()
  cartItems.value.splice(index, 1)
}
</script>

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

.message
  &.info
    background-color: #5998ff
  &.error
    background-color: #f24e4e
</style>

原理

基於 Vue 內建之 transition-group 元件

這裡有個有趣的小細節,即使父元素加上 overflow: hidden,被丟掉的子元素還是可以超出父元素範圍。

實現方式其實很簡單,就是在 onLeave 時,將被丟掉的元素 clone 一份,放到 document.body 上產生動畫這樣,非常樸實無華。

原始碼

API

Props

interface Props {
  tag?: string;
  items: Item[];
  getKey?: ((item: Item, index: number) => PropertyKey);
  throwItemClass?: string;

  enterFromClass?: string;
  enterToClass?: string;

  leaveType?: 'throw' | 'space' | 'fling' | 'hurl';
}

Emits

interface Emits { }

Methods

interface Expose { }

Slots

interface Slots {
  default?: (data: {
    item: Item;
    index: number;
  }) => unknown;
}

v0.35.6