Admin 元件
Image Upload
單張圖片上傳。
安裝
Terminal
pnpm dlx shadcn-vue add @admin/image-uploadimport { AdminImageUpload, AdminImageUploadLoading, AdminImageUploadMessage, AdminImageUploadPlaceholder, AdminImageUploadPreview, AdminImageUploadTrigger,} from '@/components/admin/image-upload'用法
<script setup lang="ts">import { ref } from 'vue'import { AdminImageUpload, AdminImageUploadLoading, AdminImageUploadMessage, AdminImageUploadPlaceholder, AdminImageUploadPreview, AdminImageUploadTrigger} from '@/components/admin/image-upload'const value = ref<File | null>(null)const status = ref<'uploading' | 'failed' | undefined>(undefined)const isError = ref(false)function handleReset() { value.value = null status.value = undefined isError.value = false}</script><template> <div class="flex flex-col gap-4"> <admin-image-upload v-model="value" :is-error="isError" :status="status"> <admin-image-upload-trigger> <admin-image-upload-placeholder /> <admin-image-upload-preview /> <admin-image-upload-loading /> </admin-image-upload-trigger> <admin-image-upload-message /> </admin-image-upload> <div class="flex flex-wrap gap-2 typo-content-medium"> <button class="rounded-sm border border-text-30 px-2 py-1" type="button" @click="handleReset"> Reset </button> <button class="rounded-sm border border-text-30 px-2 py-1" type="button" @click="status = status === 'uploading' ? undefined : 'uploading'" > Toggle uploading </button> <button class="rounded-sm border border-text-30 px-2 py-1" type="button" @click="status = status === 'failed' ? undefined : 'failed'" > Toggle failed </button> <button class="rounded-sm border border-text-30 px-2 py-1" type="button" @click="isError = !isError" > Toggle is-error </button> </div> </div></template>場景
與表單驗證整合
<script setup lang="ts">import { useForm, Field as VeeField } from 'vee-validate'import { ref } from 'vue'import { AdminField, AdminFieldContent, AdminFieldLabel } from '@/components/admin/field'import { AdminImageUpload, AdminImageUploadLoading, AdminImageUploadMessage, AdminImageUploadPlaceholder, AdminImageUploadPreview, AdminImageUploadTrigger} from '@/components/admin/image-upload'// canonical split:欄位值 cover 是「已持久化的 URL」(送出值),// 元件 v-model 綁的 file 只承載使用者新選、尚未上傳完成的本地 File。const { handleSubmit, values, setFieldValue } = useForm<{ cover: string | null}>({ initialValues: { cover: null }})const file = ref<File | null>(null)const status = ref<'uploading' | 'failed' | undefined>(undefined)const lastReject = ref<string | undefined>(undefined)function validateCover(value: unknown) { return value ? true : '請上傳商品圖片'}// 5MB 上限function validateFile(f: File): true | string { if (f.size > 5 * 1024 * 1024) { return '檔案大小超過 5MB' } return true}// 模擬 upload:1.2 秒後隨機成功 / 失敗async function fakeUpload(payload: { file: File }) { status.value = 'uploading' await new Promise((resolve) => setTimeout(resolve, 1200)) if (Math.random() < 0.3) { status.value = 'failed' return } // 成功:把回傳的持久化 URL 存進表單欄位、清掉本地選檔,preview 改吃 :src setFieldValue('cover', URL.createObjectURL(payload.file)) file.value = null status.value = undefined}function onFileReject(payload: { reason: string; file?: File }) { lastReject.value = payload.reason}const onSubmit = handleSubmit((submitted) => { // eslint-disable-next-line no-alert alert(`已送出,cover = ${submitted.cover ?? 'null'}`)})function handleReset() { setFieldValue('cover', null) file.value = null status.value = undefined lastReject.value = undefined}</script><template> <form class="flex w-full max-w-sm flex-col gap-4" @submit.prevent="onSubmit"> <vee-field v-slot="{ errorMessage }" name="cover" :rules="validateCover"> <admin-field> <admin-field-label class="w-26 shrink-0 py-2.5">商品圖片</admin-field-label> <admin-field-content class="min-h-full min-w-0"> <admin-image-upload v-model="file" accept="image/*" :is-error="!!errorMessage" :on-file-validate="validateFile" :status="status" @file-reject="onFileReject" @upload="fakeUpload" > <admin-image-upload-trigger> <admin-image-upload-placeholder v-if="!values.cover" /> <admin-image-upload-preview :src="values.cover" /> <admin-image-upload-loading /> </admin-image-upload-trigger> <admin-image-upload-message /> </admin-image-upload> </admin-field-content> </admin-field> </vee-field> <div class="flex gap-2"> <button class="rounded-sm border border-text-30 px-3 py-1.5 typo-content-medium" type="submit" > Submit </button> <button class="rounded-sm border border-text-30 px-3 py-1.5 typo-content-medium" type="button" @click="handleReset" > Reset </button> </div> <p class="typo-caption-small text-text-60"> Cover (persisted): {{ values.cover ?? 'null' }} <br /> Local file: {{ file ? '[File]' : 'null' }} <br /> Status: {{ status ?? '—' }} <br /> Last reject: {{ lastReject ?? '—' }} </p> </form></template>以 vee-validate 的
<Field>提供驗證,將errorMessage是否存在透過:is-error傳入元件即可;錯誤文字交由元件內建的AdminImageUploadMessage顯示,無需額外渲染admin-field-error,避免訊息重複。v-model 綁的是使用者新選的
File。已上傳完成的 URL 另外存一個 state,上傳成功後餵給AdminImageUploadPreview的:src。
顯示既有圖片
編輯表單的既有圖片(或上傳完成的持久化 URL)不進 v-model,改由 AdminImageUploadPreview 的 src 顯示。有選檔(File)時 File 優先,否則 fallback src。因為此時 v-model 為 null,需以 v-if gate 掉 AdminImageUploadPlaceholder,避免與既有圖重疊:
<template> <admin-image-upload v-model="file"> <admin-image-upload-trigger> <admin-image-upload-placeholder v-if="!existingSrc" /> <admin-image-upload-preview :src="existingSrc" /> <admin-image-upload-loading /> </admin-image-upload-trigger> <admin-image-upload-message /> </admin-image-upload></template>API 參考
組成
<template> <admin-image-upload v-model="image"> <admin-image-upload-trigger> <admin-image-upload-placeholder /> <admin-image-upload-preview /> <admin-image-upload-loading /> </admin-image-upload-trigger> <admin-image-upload-message /> </admin-image-upload></template>admin-image-upload
Props
| 名稱 | 型別 | 預設值 | 說明 |
|---|---|---|---|
disabled | boolean | undefined | false | 設為 `true` 時禁用上傳互動 |
isError | boolean | undefined | false | 設為 `true` 時套用錯誤狀態樣式 |
status | "uploading" | "failed" | undefined | — | 外部驅動之上傳狀態:`uploading` 顯示載入態、`failed` 顯示錯誤態 |
accept | string | undefined | "image/*" | 可接受之檔案類型(同原生 `<input type="file">` accept) |
onFileValidate | ((file: File, ctx: { index: 0; accepted: 0; total: 0; }) => string | true | Promise<string | true>) | undefined | — | 自訂單檔驗證 hook;回傳 `true` 為通過,回傳字串為錯誤訊息 |
class | ClassValue | — | 根元素之自訂 class |
modelValue | File | null | undefined | null | — |
Events
| 名稱 | Payload | 說明 |
|---|---|---|
update:modelValue | File | null | — |
fileReject | { reason: string; file?: File | undefined; } | 檔案被拒(驗證失敗或拋錯)時觸發 |
upload | { file: File; } | 檔案通過驗證、modelValue 已更新後觸發 |
Slots
| 名稱 | Bindings | 說明 |
|---|---|---|
default | {} | — |
Expose
| 名稱 | 簽名 | 說明 |
|---|---|---|
openFileDialog | () => void | 程式化開啟系統檔案選擇對話框(disabled 時 no-op) |
clearRejectMessage | () => void | 清掉上一次拒絕的訊息 |
clear | () => void | 把 v-model 設回 null 並清掉 reject 訊息 |
admin-image-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-image-upload-loading
Props
| 名稱 | 型別 | 預設值 | 說明 |
|---|---|---|---|
class | ClassValue | — | — |
admin-image-upload-message
Props
| 名稱 | 型別 | 預設值 | 說明 |
|---|---|---|---|
class | ClassValue | — | — |
admin-image-upload-placeholder
Props
| 名稱 | 型別 | 預設值 | 說明 |
|---|---|---|---|
class | ClassValue | — | — |
admin-image-upload-preview
Props
| 名稱 | 型別 | 預設值 | 說明 |
|---|---|---|---|
src | string | null | undefined | — | 既有/已持久化圖片的唯讀顯示來源;有選檔(`File`)時 `File` 優先 |
class | ClassValue | — | — |
注意事項
- v-model 只認
File | null:既有/已上傳的 URL 字串不進 v-model,改由AdminImageUploadPreview的:src顯示(見「顯示既有圖片」)。 - CDN 同 URL 不同檔:若上傳成功後 backend 回傳的 URL 與舊圖完全相同(CDN 快取),
<img>不會自動重抓。父層應在傳給:src的 URL 後加 query string(例如?v=timestamp)強制更新。 - 元件不負責上傳:
@upload只通知「使用者選了新檔且通過 validate」,實際 fetch / FormData / endpoint 全由父層處理。