Admin 元件

Image Upload

單張圖片上傳。

安裝

Terminal
pnpm dlx shadcn-vue add @admin/image-upload
import {  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

名稱型別預設值說明
disabledboolean | undefinedfalse設為 `true` 時禁用上傳互動
isErrorboolean | undefinedfalse設為 `true` 時套用錯誤狀態樣式
status"uploading" | "failed" | undefined—外部驅動之上傳狀態:`uploading` 顯示載入態、`failed` 顯示錯誤態
acceptstring | undefined"image/*"可接受之檔案類型(同原生 `<input type="file">` accept)
onFileValidate((file: File, ctx: { index: 0; accepted: 0; total: 0; }) => string | true | Promise<string | true>) | undefined—自訂單檔驗證 hook;回傳 `true` 為通過,回傳字串為錯誤訊息
classClassValue—根元素之自訂 class
modelValueFile | null | undefinednull—

Events

名稱Payload說明
update:modelValueFile | 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

名稱型別預設值說明
asChildboolean | 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.
asglobalThis.Component | AsTag | undefined"button"The element or component this component should render as. Can be overwritten by `asChild`.
classClassValue——

Slots

名稱Bindings說明
default{}—

admin-image-upload-loading

Props

名稱型別預設值說明
classClassValue——

admin-image-upload-message

Props

名稱型別預設值說明
classClassValue——

admin-image-upload-placeholder

Props

名稱型別預設值說明
classClassValue——

admin-image-upload-preview

Props

名稱型別預設值說明
srcstring | null | undefined—既有/已持久化圖片的唯讀顯示來源;有選檔(`File`)時 `File` 優先
classClassValue——

注意事項

  • 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 全由父層處理。

更新紀錄

  • BREAKING refactor!: v-model 型別收窄為 File | null20ff217
  • fix: 修正 trigger 鍵盤重複開啟對話框並集中 sourceType2bc1911