Admin 元件
File Upload
多檔上傳。拖曳、驗證、File 與 URL 並存;上傳由父層監聽 upload 事件處理。
安裝
Terminal
pnpm dlx shadcn-vue add @admin/file-uploadimport { AdminFileUpload, AdminFileUploadClear, AdminFileUploadDropzone, AdminFileUploadItem, AdminFileUploadItemDelete, AdminFileUploadItemMetadata, AdminFileUploadItemPreview, AdminFileUploadItemProgress, AdminFileUploadList, AdminFileUploadTrigger,} from '@/components/admin/file-upload'用法
<script setup lang="ts">import type { AdminFileUploadListItem } from '@/components/admin/file-upload'import { IconRefresh } from '@tabler/icons-vue'import { ref } from 'vue'import { cn } from '@/lib/utils'import { AdminFileUpload, AdminFileUploadClear, AdminFileUploadDropzone, AdminFileUploadItem, AdminFileUploadItemDelete, AdminFileUploadItemMetadata, AdminFileUploadItemPreview, AdminFileUploadItemProgress, AdminFileUploadList, AdminFileUploadTrigger} from '@/components/admin/file-upload'import { AdminNotification, errorNotification } from '@/components/admin/notification'const items = ref<AdminFileUploadListItem[]>([])const MAX_FILES = 5const MAX_SIZE = 5 * 1024 * 1024function validate(file: File, ctx: { total: number }): true | string { if (ctx.total >= MAX_FILES) { return `最多 ${MAX_FILES} 個檔案` } if (file.size > MAX_SIZE) { return `檔案超過大小上限(5 MB)` } return true}function onFileReject({ reason, file }: { reason: string; file?: File }) { errorNotification('檔案無法加入', file ? `${file.name}:${reason}` : reason)}async function handleUpload({ id }: { id: string; file: File }) { const findIndex = () => items.value.findIndex((it) => it.id === id) const patch = (p: Partial<AdminFileUploadListItem>) => { const i = findIndex() if (i === -1) { return } const copy = [...items.value] copy[i] = { ...copy[i]!, ...p } items.value = copy } const willFail = Math.random() < 0.4 const failAt = 50 patch({ status: 'uploading', progress: 0, errorMessage: undefined }) for (let p = 25; p <= 100; p += 25) { await new Promise((r) => setTimeout(r, 180)) if (willFail && p >= failAt) { patch({ status: 'failed', errorMessage: '網路錯誤,請重試' }) return } patch({ progress: p }) } patch({ status: 'success', progress: 100 })}function retry(item: AdminFileUploadListItem) { if (item.source instanceof File) { handleUpload({ id: item.id, file: item.source }) }}</script><template> <admin-notification /> <admin-file-upload v-model="items" class="w-full max-w-lg" :on-file-validate="validate" @file-reject="onFileReject" @upload="handleUpload" > <admin-file-upload-dropzone> <p class="typo-title-small text-text-90">拖曳檔案到此或選取檔案</p> <admin-file-upload-trigger :class=" cn(` rounded-lg border border-primary-50 px-3 py-2 typo-caption-small text-primary-50 hover:bg-primary-10 `) " > 選擇檔案 </admin-file-upload-trigger> </admin-file-upload-dropzone> <admin-file-upload-clear class="self-start" label="清除全部" /> <admin-file-upload-list v-if="items.length"> <admin-file-upload-item v-for="it in items" :key="it.id" :item-id="it.id"> <admin-file-upload-item-preview /> <div class="flex min-w-0 flex-1 flex-col gap-2"> <admin-file-upload-item-metadata> <p v-if="it.status === 'failed' && it.errorMessage" class="typo-caption-small text-error-50" > {{ it.errorMessage }} </p> </admin-file-upload-item-metadata> <admin-file-upload-item-progress :labels="{ uploading: '上傳中', success: '上傳成功', failed: '上傳失敗' }" /> </div> <button v-if="it.status === 'failed'" aria-label="重試上傳" type="button" :class=" cn(` flex size-9 shrink-0 items-center justify-center rounded-lg text-text-50 hover:bg-bg-70 hover:text-text-90 `) " @click="retry(it)" > <icon-refresh class="size-5" /> </button> <admin-file-upload-item-delete label="移除檔案" /> </admin-file-upload-item> </admin-file-upload-list> </admin-file-upload></template>場景
僅按鈕、不要 Dropzone
<script setup lang="ts">import type { AdminFileUploadListItem } from '@/components/admin/file-upload'import { ref } from 'vue'import { cn } from '@/lib/utils'import { AdminFileUpload, AdminFileUploadClear, AdminFileUploadItem, AdminFileUploadItemDelete, AdminFileUploadItemMetadata, AdminFileUploadItemPreview, AdminFileUploadList, AdminFileUploadTrigger} from '@/components/admin/file-upload'const items = ref<AdminFileUploadListItem[]>([])</script><template> <admin-file-upload v-model="items" class="w-full max-w-lg" :multiple="false"> <div class="flex flex-wrap items-center gap-2"> <admin-file-upload-trigger :class=" cn(` rounded-lg bg-primary-50 px-3 py-2 typo-caption-small text-text-10 hover:opacity-90 `) " > 從電腦上傳 </admin-file-upload-trigger> <span class="typo-caption-small text-text-50">(此 demo 不含 Dropzone,僅 Trigger)</span> </div> <admin-file-upload-clear v-if="items.length" class="self-start" label="清除全部" /> <admin-file-upload-list v-if="items.length" class="mt-2"> <admin-file-upload-item v-for="it in items" :key="it.id" :item-id="it.id"> <admin-file-upload-item-preview /> <admin-file-upload-item-metadata /> <admin-file-upload-item-delete label="移除檔案" /> </admin-file-upload-item> </admin-file-upload-list> </admin-file-upload></template>程式寫入 URL(不經檔案選擇器)
由素材庫、過往上傳紀錄等來源直接寫入 URL 項目,跳過檔案選擇器流程。
<script setup lang="ts">import type { AdminFileUploadListItem } from '@/components/admin/file-upload'import { ref } from 'vue'import { cn } from '@/lib/utils'import { AdminFileUpload, AdminFileUploadClear, AdminFileUploadItem, AdminFileUploadItemDelete, AdminFileUploadItemMetadata, AdminFileUploadItemPreview, AdminFileUploadList} from '@/components/admin/file-upload'const items = ref<AdminFileUploadListItem[]>([])function addRemoteImage() { items.value = [ ...items.value, { id: globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(16).slice(2)}`, source: 'https://picsum.photos/id/237/200/200', status: 'success', progress: 100 } ]}</script><template> <admin-file-upload v-model="items" class="w-full max-w-lg"> <p class="typo-caption-small text-text-50"> 模擬「素材庫」或其它來源:由外層按鈕直接寫入 URL 項目(不經檔案選擇器)。 </p> <button type="button" :class=" cn(` rounded-lg border border-primary-50 px-3 py-2 typo-caption-small text-primary-50 hover:bg-primary-10 `) " @click="addRemoteImage" > 新增一筆圖片 URL </button> <admin-file-upload-clear class="self-start" label="清除全部" /> <admin-file-upload-list v-if="items.length" class="mt-2"> <admin-file-upload-item v-for="it in items" :key="it.id" :item-id="it.id"> <admin-file-upload-item-preview /> <admin-file-upload-item-metadata> <p class="typo-caption-small text-text-50">此列為 slot:可放影片時長、上傳日期等</p> </admin-file-upload-item-metadata> <admin-file-upload-item-delete label="移除檔案" /> </admin-file-upload-item> </admin-file-upload-list> </admin-file-upload></template>Type-safe 列表項目
AdminFileUpload 已是 generic(TItem extends AdminFileUploadListItem),以子型別的 ref 綁定 v-model 即可讓列表項目攜帶業務欄位(如後端回傳的資產 id),update:modelValue payload 同步收斂為該子型別。
元件自行新增的項目(使用者選檔)僅含 base 欄位(id/source);自訂欄位由呼叫端於 upload 事件後自行補寫。
需開啟 strictVModel
型別檢查需開啟 strictVModel(若已開啟 strictTemplates 則已包含)。設定見 Type-safe。下列同時含合法綁定與刻意錯誤綁定;錯誤處以 @vue-expect-error 標示。
<script setup lang="ts">import type { AdminFileUploadListItem } from '@/components/admin/file-upload'import { ref } from 'vue'import { AdminFileUpload, AdminFileUploadDropzone, AdminFileUploadItem, AdminFileUploadItemDelete, AdminFileUploadItemMetadata, AdminFileUploadItemPreview, AdminFileUploadList, AdminFileUploadTrigger} from '@/components/admin/file-upload'interface AttachmentItem extends AdminFileUploadListItem { /** 上傳完成後由後端回傳之資產 id,由呼叫端於 upload 事件後補寫 */ assetId?: string}const items = ref<AttachmentItem[]>([])const wrongItems = ref<{ id: string }[]>([])</script><template> <div class="flex w-full max-w-md flex-col gap-6"> <admin-file-upload v-model="items"> <admin-file-upload-dropzone> <admin-file-upload-trigger>選擇檔案</admin-file-upload-trigger> </admin-file-upload-dropzone> <admin-file-upload-list> <admin-file-upload-item v-for="it in items" :key="it.id" :item-id="it.id"> <admin-file-upload-item-preview /> <admin-file-upload-item-metadata /> <admin-file-upload-item-delete /> </admin-file-upload-item> </admin-file-upload-list> </admin-file-upload> <!-- @vue-expect-error 項目缺 source 必要欄位,不符 AdminFileUploadListItem 約束 --> <admin-file-upload v-model="wrongItems" /> </div></template>API 參考
組成
<template> <admin-file-upload v-model="items" :on-file-validate="validate" @upload="handleUpload"> <admin-file-upload-dropzone> <admin-file-upload-trigger>選擇檔案</admin-file-upload-trigger> </admin-file-upload-dropzone> <admin-file-upload-clear /> <admin-file-upload-list> <admin-file-upload-item v-for="it in items" :key="it.id" :item-id="it.id"> <admin-file-upload-item-preview /> <admin-file-upload-item-metadata /> <admin-file-upload-item-progress /> <admin-file-upload-item-delete /> </admin-file-upload-item> </admin-file-upload-list> </admin-file-upload></template>admin-file-upload
Props
| 名稱 | 型別 | 預設值 | 說明 |
|---|---|---|---|
class | ClassValue | — | 根元素之自訂 class |
disabled | boolean | undefined | false | 設為 `true` 時禁用整個上傳互動 |
invalid | boolean | undefined | false | 設為 `true` 時套用錯誤狀態樣式 |
multiple | boolean | undefined | true | 設為 `false` 時限制單檔上傳 |
accept | string | undefined | — | 可接受之檔案類型(同原生 `<input type="file">` accept) |
onFileValidate | ((file: File, ctx: { index: number; accepted: number; total: number; }) => string | true | Promise<string | true>) | undefined | — | 自訂單檔驗證 hook;回傳 `true` 為通過,回傳字串為錯誤訊息 |
modelValue | TItem[] | undefined | [] | — |
Events
| 名稱 | Payload | 說明 |
|---|---|---|
fileReject | { reason: string; file?: File | undefined; } | 檔案被拒絕時觸發(驗證失敗或被 `onFileValidate` 拒收) |
upload | { id: string; file: File; } | 檔案被接受並加入列表後觸發,可用於啟動實際上傳流程 |
update:modelValue | TItem[] | — |
Slots
| 名稱 | Bindings | 說明 |
|---|---|---|
default | {} | — |
admin-file-upload-trigger
Props
| 名稱 | 型別 | 預設值 | 說明 |
|---|---|---|---|
asChild | boolean | undefined | — | Change the default rendered element for the one passed as a child, merging their props and behavior. Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details. |
as | globalThis.Component | AsTag | undefined | "button" | The element or component this component should render as. Can be overwritten by `asChild`. |
class | ClassValue | — | — |
Slots
| 名稱 | Bindings | 說明 |
|---|---|---|
default | {} | — |
admin-file-upload-list
Props
| 名稱 | 型別 | 預設值 | 說明 |
|---|---|---|---|
orientation | "horizontal" | "vertical" | undefined | "vertical" | — |
class | ClassValue | — | — |
Slots
| 名稱 | Bindings | 說明 |
|---|---|---|
default | {} | — |
admin-file-upload-item
Props
| 名稱 | 型別 | 預設值 | 說明 |
|---|---|---|---|
itemId* | string | — | — |
class | ClassValue | — | — |
* 代表必填
Slots
| 名稱 | Bindings | 說明 |
|---|---|---|
default | {} | — |
admin-file-upload-clear
Props
| 名稱 | 型別 | 預設值 | 說明 |
|---|---|---|---|
asChild | boolean | undefined | — | Change the default rendered element for the one passed as a child, merging their props and behavior. Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details. |
as | globalThis.Component | AsTag | undefined | "button" | The element or component this component should render as. Can be overwritten by `asChild`. |
class | ClassValue | — | — |
forceMount | boolean | undefined | false | — |
label | string | undefined | "Clear all" | — |
Slots
| 名稱 | Bindings | 說明 |
|---|---|---|
default | {} | — |
admin-file-upload-item-delete
Props
| 名稱 | 型別 | 預設值 | 說明 |
|---|---|---|---|
asChild | boolean | undefined | — | Change the default rendered element for the one passed as a child, merging their props and behavior. Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details. |
as | globalThis.Component | AsTag | undefined | "button" | The element or component this component should render as. Can be overwritten by `asChild`. |
class | ClassValue | — | — |
label | string | undefined | "Remove file" | — |
Slots
| 名稱 | Bindings | 說明 |
|---|---|---|
default | {} | — |
admin-file-upload-item-progress
Props
| 名稱 | 型別 | 預設值 | 說明 |
|---|---|---|---|
class | ClassValue | — | — |
labels | Partial<Record<FileUploadStatus, string>> | undefined | — | — |
Slots
| 名稱 | Bindings | 說明 |
|---|---|---|
status | { label: string; status: FileUploadStatus; } | — |
icon | { status: FileUploadStatus; } | — |
admin-file-upload-dropzone
Props
| 名稱 | 型別 | 預設值 | 說明 |
|---|---|---|---|
class | ClassValue | — | — |
Slots
| 名稱 | Bindings | 說明 |
|---|---|---|
default | {} | — |
admin-file-upload-item-metadata
Props
| 名稱 | 型別 | 預設值 | 說明 |
|---|---|---|---|
class | ClassValue | — | — |
Slots
| 名稱 | Bindings | 說明 |
|---|---|---|
default | {} | — |
admin-file-upload-item-preview
Props
| 名稱 | 型別 | 預設值 | 說明 |
|---|---|---|---|
class | ClassValue | — | — |
Slots
| 名稱 | Bindings | 說明 |
|---|---|---|
default | { item: AdminFileUploadListItem | undefined; } | — |
注意事項
- 自訂子元件可從
@/components/admin/file-uploadimportuseFileUploadRoot()/useFileUploadItem()取得內部狀態。 useFileUploadRoot()的modelValue為寬型別AdminFileUploadListItem[];泛型收斂僅作用於 root 的v-model與update:modelValue。- 單張圖片欄位使用
admin-image-upload,多檔、列表進度、拖曳區使用本元件族。