亂丟的清單 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;
}