文件上传

GitHub
一个用于上传文件的输入元素。

用法

使用 v-model 指令来控制文件上传组件的值。

<script setup lang="ts">
const value = ref(null)
</script>

<template>
  <UFileUpload v-model="value" class="w-96 min-h-48" />
</template>

多选

使用 multiple 属性以允许多选文件。

<template>
  <UFileUpload multiple class="w-96 min-h-48" />
</template>

拖放区

使用 dropzone 属性来启用/禁用拖放区域。默认为 true

<template>
  <UFileUpload :dropzone="false" class="w-96 min-h-48" />
</template>

交互式

使用 interactive 属性来启用/禁用可点击区域。默认为 true

#actions 插槽中添加一个 Button 组件时,这会很有用。
<template>
  <UFileUpload :interactive="false" class="w-96 min-h-48" />
</template>

接受

使用 accept 属性指定输入允许的文件类型。提供一个逗号分隔的列表,包含MIME 类型或文件扩展名(例如,image/png,application/pdf,.jpg)。默认为 *(所有文件类型)。

<template>
  <UFileUpload accept="image/*" class="w-96 min-h-48" />
</template>

标签

使用 label 属性来设置文件上传组件的标签。

将您的图片拖放到此处
<template>
  <UFileUpload label="Drop your image here" class="w-96 min-h-48" />
</template>

描述

使用 description 属性来设置文件上传组件的描述。

将您的图片拖放到此处
SVG, PNG, JPG 或 GIF (最大 2MB)
<template>
  <UFileUpload
    label="Drop your image here"
    description="SVG, PNG, JPG or GIF (max. 2MB)"
    class="w-96 min-h-48"
  />
</template>

Icon

使用 icon 属性来设置文件上传组件的图标。默认为 i-lucide-upload

将您的图片拖放到此处
SVG, PNG, JPG 或 GIF (最大 2MB)
<template>
  <UFileUpload
    icon="i-lucide-image"
    label="Drop your image here"
    description="SVG, PNG, JPG or GIF (max. 2MB)"
    class="w-96 min-h-48"
  />
</template>
您可以在您的 app.config.ts 中通过 ui.icons.upload 键全局自定义此图标。
您可以在您的 vite.config.ts 中通过 ui.icons.upload 键全局自定义此图标。

颜色

使用 color 属性来更改文件上传组件的颜色。

将您的图片拖放到此处
SVG, PNG, JPG 或 GIF (最大 2MB)
<template>
  <UFileUpload
    color="neutral"
    highlight
    label="Drop your image here"
    description="SVG, PNG, JPG or GIF (max. 2MB)"
    class="w-96 min-h-48"
  />
</template>
highlight 属性在此用于显示焦点状态。当发生验证错误时,它会在内部使用。

变体

使用 variant 属性来更改文件上传组件的变体。

<template>
  <UFileUpload variant="button" />
</template>

尺寸

使用 size 属性来更改文件上传组件的大小。

将您的图片拖放到此处
SVG, PNG, JPG 或 GIF (最大 2MB)
<template>
  <UFileUpload
    size="xl"
    variant="area"
    label="Drop your image here"
    description="SVG, PNG, JPG or GIF (max. 2MB)"
  />
</template>

布局

使用 layout 属性来更改文件上传组件中文件的显示方式。默认为 grid

此属性仅当 variantarea 时才有效。
将您的图片拖放到此处
SVG, PNG, JPG 或 GIF (最大 2MB)
<template>
  <UFileUpload
    layout="list"
    multiple
    label="Drop your images here"
    description="SVG, PNG, JPG or GIF (max. 2MB)"
    class="w-96"
    :ui="{
      base: 'min-h-48'
    }"
  />
</template>

位置

使用 position 属性来更改文件上传组件中文件的位置。默认为 outside

此属性仅当 variantarealayoutlist 时才有效。
将您的图片拖放到此处
SVG, PNG, JPG 或 GIF (最大 2MB)
<template>
  <UFileUpload
    position="inside"
    layout="list"
    multiple
    label="Drop your images here"
    description="SVG, PNG, JPG or GIF (max. 2MB)"
    class="w-96"
    :ui="{
      base: 'min-h-48'
    }"
  />
</template>

示例

带表单验证

您可以在 FormFormField 组件中使用文件上传组件来处理验证和错误。

JPG, GIF 或 PNG。最大 2MB。

<script setup lang="ts">
import * as z from 'zod'
import type { FormSubmitEvent } from '@nuxt/ui'

const MAX_FILE_SIZE = 2 * 1024 * 1024 // 2MB
const MIN_DIMENSIONS = { width: 200, height: 200 }
const MAX_DIMENSIONS = { width: 4096, height: 4096 }
const ACCEPTED_IMAGE_TYPES = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp']

const formatBytes = (bytes: number, decimals = 2) => {
  if (bytes === 0) return '0 Bytes'
  const k = 1024
  const dm = decimals < 0 ? 0 : decimals
  const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
  const i = Math.floor(Math.log(bytes) / Math.log(k))
  return Number.parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]
}

const schema = z.object({
  image: z
    .instanceof(File, {
      message: 'Please select an image file.'
    })
    .refine((file) => file.size <= MAX_FILE_SIZE, {
      message: `The image is too large. Please choose an image smaller than ${formatBytes(MAX_FILE_SIZE)}.`
    })
    .refine((file) => ACCEPTED_IMAGE_TYPES.includes(file.type), {
      message: 'Please upload a valid image file (JPEG, PNG, or WebP).'
    })
    .refine(
      (file) =>
        new Promise((resolve) => {
          const reader = new FileReader()
          reader.onload = (e) => {
            const img = new Image()
            img.onload = () => {
              const meetsDimensions =
                img.width >= MIN_DIMENSIONS.width &&
                img.height >= MIN_DIMENSIONS.height &&
                img.width <= MAX_DIMENSIONS.width &&
                img.height <= MAX_DIMENSIONS.height
              resolve(meetsDimensions)
            }
            img.src = e.target?.result as string
          }
          reader.readAsDataURL(file)
        }),
      {
        message: `The image dimensions are invalid. Please upload an image between ${MIN_DIMENSIONS.width}x${MIN_DIMENSIONS.height} and ${MAX_DIMENSIONS.width}x${MAX_DIMENSIONS.height} pixels.`
      }
    )
})

type schema = z.output<typeof schema>

const state = reactive<Partial<schema>>({
  image: undefined
})

async function onSubmit(event: FormSubmitEvent<schema>) {
  console.log(event.data)
}
</script>

<template>
  <UForm :schema="schema" :state="state" class="space-y-4 w-96" @submit="onSubmit">
    <UFormField name="image" label="Image" description="JPG, GIF or PNG. 2MB Max.">
      <UFileUpload v-model="state.image" accept="image/*" class="min-h-48" />
    </UFormField>

    <UButton type="submit" label="Submit" color="neutral" />
  </UForm>
</template>

带默认插槽

您可以使用默认插槽来创建您自己的文件上传组件。

JPG, GIF 或 PNG。最大 1MB。

<script setup lang="ts">
import * as z from 'zod'
import type { FormSubmitEvent } from '@nuxt/ui'

const MAX_FILE_SIZE = 2 * 1024 * 1024 // 2MB
const MIN_DIMENSIONS = { width: 200, height: 200 }
const MAX_DIMENSIONS = { width: 4096, height: 4096 }
const ACCEPTED_IMAGE_TYPES = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp']

const formatBytes = (bytes: number, decimals = 2) => {
  if (bytes === 0) return '0 Bytes'
  const k = 1024
  const dm = decimals < 0 ? 0 : decimals
  const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
  const i = Math.floor(Math.log(bytes) / Math.log(k))
  return Number.parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]
}

const schema = z.object({
  avatar: z
    .instanceof(File, {
      message: 'Please select an image file.'
    })
    .refine((file) => file.size <= MAX_FILE_SIZE, {
      message: `The image is too large. Please choose an image smaller than ${formatBytes(MAX_FILE_SIZE)}.`
    })
    .refine((file) => ACCEPTED_IMAGE_TYPES.includes(file.type), {
      message: 'Please upload a valid image file (JPEG, PNG, or WebP).'
    })
    .refine(
      (file) =>
        new Promise((resolve) => {
          const reader = new FileReader()
          reader.onload = (e) => {
            const img = new Image()
            img.onload = () => {
              const meetsDimensions =
                img.width >= MIN_DIMENSIONS.width &&
                img.height >= MIN_DIMENSIONS.height &&
                img.width <= MAX_DIMENSIONS.width &&
                img.height <= MAX_DIMENSIONS.height
              resolve(meetsDimensions)
            }
            img.src = e.target?.result as string
          }
          reader.readAsDataURL(file)
        }),
      {
        message: `The image dimensions are invalid. Please upload an image between ${MIN_DIMENSIONS.width}x${MIN_DIMENSIONS.height} and ${MAX_DIMENSIONS.width}x${MAX_DIMENSIONS.height} pixels.`
      }
    )
})

type schema = z.output<typeof schema>

const state = reactive<Partial<schema>>({
  avatar: undefined
})

function createObjectUrl(file: File): string {
  return URL.createObjectURL(file)
}

async function onSubmit(event: FormSubmitEvent<schema>) {
  console.log(event.data)
}
</script>

<template>
  <UForm :schema="schema" :state="state" class="space-y-4 w-64" @submit="onSubmit">
    <UFormField name="avatar" label="Avatar" description="JPG, GIF or PNG. 1MB Max.">
      <UFileUpload v-slot="{ open, removeFile }" v-model="state.avatar" accept="image/*">
        <div class="flex flex-wrap items-center gap-3">
          <UAvatar
            size="lg"
            :src="state.avatar ? createObjectUrl(state.avatar) : undefined"
            icon="i-lucide-image"
          />

          <UButton
            :label="state.avatar ? 'Change image' : 'Upload image'"
            color="neutral"
            variant="outline"
            @click="open()"
          />
        </div>

        <p v-if="state.avatar" class="text-xs text-muted mt-1.5">
          {{ state.avatar.name }}

          <UButton
            label="Remove"
            color="error"
            variant="link"
            size="xs"
            class="p-0"
            @click="removeFile()"
          />
        </p>
      </UFileUpload>
    </UFormField>

    <UButton type="submit" label="Submit" color="neutral" />
  </UForm>
</template>

带 files-bottom 插槽

您可以使用 files-bottom 插槽在文件列表下方添加一个 Button 按钮,例如用于移除所有文件。

将您的图片拖放到此处
SVG, PNG, JPG 或 GIF (最大 2MB)
<script setup lang="ts">
const value = ref<File[]>([])
</script>

<template>
  <UFileUpload
    v-model="value"
    icon="i-lucide-image"
    label="Drop your images here"
    description="SVG, PNG, JPG or GIF (max. 2MB)"
    layout="list"
    multiple
    :interactive="false"
    class="w-96 min-h-48"
  >
    <template #actions="{ open }">
      <UButton
        label="Select images"
        icon="i-lucide-upload"
        color="neutral"
        variant="outline"
        @click="open()"
      />
    </template>

    <template #files-bottom="{ removeFile, files }">
      <UButton
        v-if="files?.length"
        label="Remove all files"
        color="neutral"
        @click="removeFile()"
      />
    </template>
  </UFileUpload>
</template>
在此示例中,interactive 属性设置为 false 以防止默认的可点击区域。

带 files-top 插槽

您可以使用 files-top 插槽在文件列表上方添加一个 Button 按钮,例如用于添加新文件。

将您的图片拖放到此处
SVG, PNG, JPG 或 GIF (最大 2MB)
<script setup lang="ts">
const value = ref<File[]>([])
</script>

<template>
  <UFileUpload
    v-model="value"
    icon="i-lucide-image"
    label="Drop your images here"
    description="SVG, PNG, JPG or GIF (max. 2MB)"
    layout="grid"
    multiple
    :interactive="false"
    class="w-96 min-h-48"
  >
    <template #actions="{ open }">
      <UButton
        label="Select images"
        icon="i-lucide-upload"
        color="neutral"
        variant="outline"
        @click="open()"
      />
    </template>

    <template #files-top="{ open, files }">
      <div v-if="files?.length" class="mb-2 flex items-center justify-between">
        <p class="font-bold">Files ({{ files?.length }})</p>

        <UButton
          icon="i-lucide-plus"
          label="Add more"
          color="neutral"
          variant="outline"
          class="-my-2"
          @click="open()"
        />
      </div>
    </template>
  </UFileUpload>
</template>

API

属性

属性默认值类型
as

'div'

any

此组件应渲染为的元素或组件。

id

string

name

string

图标

appConfig.ui.icons.upload

string

要显示的图标。

label

string

description

string

color

'primary'

"error" | "primary" | "secondary" | "success" | "info" | "warning" | "neutral"

variant

'area'

"button" | "area"

button 变体仅在 multiplefalse 时可用。

尺寸

'md'

"xs" | "sm" | "md" | "lg" | "xl"

layout

'grid'

"list" | "grid"

文件显示布局。仅当 variantarea 时有效。

位置

'outside'

"inside" | "outside"

文件的位置。仅当 variantarealayoutlist 时有效。

高亮

boolean

高亮环形颜色,如同焦点状态。

accept

'*'

string

指定输入允许的文件类型。提供一个逗号分隔的 MIME 类型或文件扩展名列表(例如,“image/png,application/pdf,.jpg”)。

multiple

false

boolean

reset

false

boolean

当对话框打开时重置文件输入。

dropzone

true

boolean

创建一个允许用户将文件拖放到其上的区域。

interactive

true

boolean

当用户点击拖放区时,使其具有交互性。

required

boolean

disabled

boolean

fileIcon

appConfig.ui.icons.file

string

要为文件显示的图标。

fileDelete

boolean | Partial<ButtonProps>

配置文件的删除按钮。当 layoutgrid 时,默认值为 { color: 'neutral', variant: 'solid', size: 'xs' }。当 layoutlist 时,默认值为 { color: 'neutral', variant: 'link' }

fileDeleteIcon

appConfig.ui.icons.close

string

用于删除文件的图标。

modelValue

null | File | File[]

ui

{ root?: ClassNameValue; base?: ClassNameValue; wrapper?: ClassNameValue; icon?: ClassNameValue; avatar?: ClassNameValue; label?: ClassNameValue; description?: ClassNameValue; actions?: ClassNameValue; files?: ClassNameValue; file?: ClassNameValue; fileLeadingAvatar?: ClassNameValue; fileWrapper?: ClassNameValue; fileName?: ClassNameValue; fileSize?: ClassNameValue; fileTrailingButton?: ClassNameValue; }

插槽

插槽类型
default

{ open: (localOptions?: Partial<UseFileDialogOptions> | undefined) => void; removeFile: (index?: number | undefined) => void; }

前置

{}

label

{}

description

{}

actions

{ files?: FileUploadFiles<boolean> | undefined; open: (localOptions?: Partial<UseFileDialogOptions> | undefined) => void; removeFile: (index?: number | undefined) => void; }

files

{ files?: FileUploadFiles<boolean> | undefined; }

files-top

{ files?: FileUploadFiles<boolean> | undefined; open: (localOptions?: Partial<UseFileDialogOptions> | undefined) => void; removeFile: (index?: number | undefined) => void; }

files-bottom

{ files?: FileUploadFiles<boolean> | undefined; open: (localOptions?: Partial<UseFileDialogOptions> | undefined) => void; removeFile: (index?: number | undefined) => void; }

file

{ file: File; index: number; }

file-leading

{ file: File; index: number; }

file-name

{ file: File; index: number; }

file-size

{ file: File; index: number; }

file-trailing

{ file: File; index: number; }

事件

事件类型
change

[event: Event]

update:modelValue

[payload: File | File[] | null]

update:modelValue

[value: File | File[] | null | undefined]

可访问属性

通过模板引用访问组件时,您可以使用以下内容:

名称类型
inputRefRef<HTMLInputElement | null>
dropzoneRefRef<HTMLDivElement | null>

主题

app.config.ts
export default defineAppConfig({
  ui: {
    fileUpload: {
      slots: {
        root: 'relative flex flex-col',
        base: [
          'w-full flex-1 bg-default border border-default flex flex-col gap-2 items-stretch justify-center rounded-lg focus-visible:outline-2',
          'transition-[background]'
        ],
        wrapper: 'flex flex-col items-center justify-center text-center',
        icon: 'shrink-0',
        avatar: 'shrink-0',
        label: 'font-medium text-default mt-2',
        description: 'text-muted mt-1',
        actions: 'flex flex-wrap gap-1.5 shrink-0 mt-4',
        files: '',
        file: 'relative',
        fileLeadingAvatar: 'shrink-0',
        fileWrapper: 'flex flex-col min-w-0',
        fileName: 'text-default truncate',
        fileSize: 'text-muted truncate',
        fileTrailingButton: ''
      },
      variants: {
        color: {
          primary: '',
          secondary: '',
          success: '',
          info: '',
          warning: '',
          error: '',
          neutral: ''
        },
        variant: {
          area: {
            wrapper: 'px-4 py-3',
            base: 'p-4'
          },
          button: {}
        },
        size: {
          xs: {
            base: 'text-xs',
            icon: 'size-4',
            file: 'text-xs px-2 py-1 gap-1',
            fileWrapper: 'flex-row gap-1'
          },
          sm: {
            base: 'text-xs',
            icon: 'size-4',
            file: 'text-xs px-2.5 py-1.5 gap-1.5',
            fileWrapper: 'flex-row gap-1'
          },
          md: {
            base: 'text-sm',
            icon: 'size-5',
            file: 'text-xs px-2.5 py-1.5 gap-1.5'
          },
          lg: {
            base: 'text-sm',
            icon: 'size-5',
            file: 'text-sm px-3 py-2 gap-2',
            fileSize: 'text-xs'
          },
          xl: {
            base: 'text-base',
            icon: 'size-6',
            file: 'text-sm px-3 py-2 gap-2'
          }
        },
        layout: {
          list: {
            root: 'gap-2 items-start',
            files: 'flex flex-col w-full gap-2',
            file: 'min-w-0 flex items-center border border-default rounded-md w-full',
            fileTrailingButton: 'ms-auto'
          },
          grid: {
            fileWrapper: 'hidden',
            fileLeadingAvatar: 'size-full rounded-lg',
            fileTrailingButton: 'absolute -top-1.5 -end-1.5 p-0 rounded-full border-2 border-bg'
          }
        },
        position: {
          inside: '',
          outside: ''
        },
        dropzone: {
          true: 'border-dashed data-[dragging=true]:bg-elevated/25'
        },
        interactive: {
          true: ''
        },
        highlight: {
          true: ''
        },
        multiple: {
          true: ''
        },
        disabled: {
          true: 'cursor-not-allowed opacity-75'
        }
      },
      compoundVariants: [
        {
          color: 'primary',
          class: 'focus-visible:outline-primary'
        },
        {
          color: 'primary',
          highlight: true,
          class: 'border-primary'
        },
        {
          color: 'neutral',
          class: 'focus-visible:outline-inverted'
        },
        {
          color: 'neutral',
          highlight: true,
          class: 'border-inverted'
        },
        {
          size: 'xs',
          layout: 'list',
          class: {
            fileTrailingButton: '-me-1'
          }
        },
        {
          size: 'sm',
          layout: 'list',
          class: {
            fileTrailingButton: '-me-1.5'
          }
        },
        {
          size: 'md',
          layout: 'list',
          class: {
            fileTrailingButton: '-me-1.5'
          }
        },
        {
          size: 'lg',
          layout: 'list',
          class: {
            fileTrailingButton: '-me-2'
          }
        },
        {
          size: 'xl',
          layout: 'list',
          class: {
            fileTrailingButton: '-me-2'
          }
        },
        {
          variant: 'button',
          size: 'xs',
          class: {
            base: 'p-1'
          }
        },
        {
          variant: 'button',
          size: 'sm',
          class: {
            base: 'p-1.5'
          }
        },
        {
          variant: 'button',
          size: 'md',
          class: {
            base: 'p-1.5'
          }
        },
        {
          variant: 'button',
          size: 'lg',
          class: {
            base: 'p-2'
          }
        },
        {
          variant: 'button',
          size: 'xl',
          class: {
            base: 'p-2'
          }
        },
        {
          layout: 'grid',
          multiple: true,
          class: {
            files: 'grid grid-cols-2 md:grid-cols-3 gap-4 w-full',
            file: 'p-0 aspect-square'
          }
        },
        {
          layout: 'grid',
          multiple: false,
          class: {
            file: 'absolute inset-0 p-0'
          }
        },
        {
          interactive: true,
          disabled: false,
          class: 'hover:bg-elevated/25'
        }
      ],
      defaultVariants: {
        color: 'primary',
        variant: 'area',
        size: 'md'
      }
    }
  }
})
vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'

export default defineConfig({
  plugins: [
    vue(),
    ui({
      ui: {
        fileUpload: {
          slots: {
            root: 'relative flex flex-col',
            base: [
              'w-full flex-1 bg-default border border-default flex flex-col gap-2 items-stretch justify-center rounded-lg focus-visible:outline-2',
              'transition-[background]'
            ],
            wrapper: 'flex flex-col items-center justify-center text-center',
            icon: 'shrink-0',
            avatar: 'shrink-0',
            label: 'font-medium text-default mt-2',
            description: 'text-muted mt-1',
            actions: 'flex flex-wrap gap-1.5 shrink-0 mt-4',
            files: '',
            file: 'relative',
            fileLeadingAvatar: 'shrink-0',
            fileWrapper: 'flex flex-col min-w-0',
            fileName: 'text-default truncate',
            fileSize: 'text-muted truncate',
            fileTrailingButton: ''
          },
          variants: {
            color: {
              primary: '',
              secondary: '',
              success: '',
              info: '',
              warning: '',
              error: '',
              neutral: ''
            },
            variant: {
              area: {
                wrapper: 'px-4 py-3',
                base: 'p-4'
              },
              button: {}
            },
            size: {
              xs: {
                base: 'text-xs',
                icon: 'size-4',
                file: 'text-xs px-2 py-1 gap-1',
                fileWrapper: 'flex-row gap-1'
              },
              sm: {
                base: 'text-xs',
                icon: 'size-4',
                file: 'text-xs px-2.5 py-1.5 gap-1.5',
                fileWrapper: 'flex-row gap-1'
              },
              md: {
                base: 'text-sm',
                icon: 'size-5',
                file: 'text-xs px-2.5 py-1.5 gap-1.5'
              },
              lg: {
                base: 'text-sm',
                icon: 'size-5',
                file: 'text-sm px-3 py-2 gap-2',
                fileSize: 'text-xs'
              },
              xl: {
                base: 'text-base',
                icon: 'size-6',
                file: 'text-sm px-3 py-2 gap-2'
              }
            },
            layout: {
              list: {
                root: 'gap-2 items-start',
                files: 'flex flex-col w-full gap-2',
                file: 'min-w-0 flex items-center border border-default rounded-md w-full',
                fileTrailingButton: 'ms-auto'
              },
              grid: {
                fileWrapper: 'hidden',
                fileLeadingAvatar: 'size-full rounded-lg',
                fileTrailingButton: 'absolute -top-1.5 -end-1.5 p-0 rounded-full border-2 border-bg'
              }
            },
            position: {
              inside: '',
              outside: ''
            },
            dropzone: {
              true: 'border-dashed data-[dragging=true]:bg-elevated/25'
            },
            interactive: {
              true: ''
            },
            highlight: {
              true: ''
            },
            multiple: {
              true: ''
            },
            disabled: {
              true: 'cursor-not-allowed opacity-75'
            }
          },
          compoundVariants: [
            {
              color: 'primary',
              class: 'focus-visible:outline-primary'
            },
            {
              color: 'primary',
              highlight: true,
              class: 'border-primary'
            },
            {
              color: 'neutral',
              class: 'focus-visible:outline-inverted'
            },
            {
              color: 'neutral',
              highlight: true,
              class: 'border-inverted'
            },
            {
              size: 'xs',
              layout: 'list',
              class: {
                fileTrailingButton: '-me-1'
              }
            },
            {
              size: 'sm',
              layout: 'list',
              class: {
                fileTrailingButton: '-me-1.5'
              }
            },
            {
              size: 'md',
              layout: 'list',
              class: {
                fileTrailingButton: '-me-1.5'
              }
            },
            {
              size: 'lg',
              layout: 'list',
              class: {
                fileTrailingButton: '-me-2'
              }
            },
            {
              size: 'xl',
              layout: 'list',
              class: {
                fileTrailingButton: '-me-2'
              }
            },
            {
              variant: 'button',
              size: 'xs',
              class: {
                base: 'p-1'
              }
            },
            {
              variant: 'button',
              size: 'sm',
              class: {
                base: 'p-1.5'
              }
            },
            {
              variant: 'button',
              size: 'md',
              class: {
                base: 'p-1.5'
              }
            },
            {
              variant: 'button',
              size: 'lg',
              class: {
                base: 'p-2'
              }
            },
            {
              variant: 'button',
              size: 'xl',
              class: {
                base: 'p-2'
              }
            },
            {
              layout: 'grid',
              multiple: true,
              class: {
                files: 'grid grid-cols-2 md:grid-cols-3 gap-4 w-full',
                file: 'p-0 aspect-square'
              }
            },
            {
              layout: 'grid',
              multiple: false,
              class: {
                file: 'absolute inset-0 p-0'
              }
            },
            {
              interactive: true,
              disabled: false,
              class: 'hover:bg-elevated/25'
            }
          ],
          defaultVariants: {
            color: 'primary',
            variant: 'area',
            size: 'md'
          }
        }
      }
    })
  ]
})
vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import uiPro from '@nuxt/ui-pro/vite'

export default defineConfig({
  plugins: [
    vue(),
    uiPro({
      ui: {
        fileUpload: {
          slots: {
            root: 'relative flex flex-col',
            base: [
              'w-full flex-1 bg-default border border-default flex flex-col gap-2 items-stretch justify-center rounded-lg focus-visible:outline-2',
              'transition-[background]'
            ],
            wrapper: 'flex flex-col items-center justify-center text-center',
            icon: 'shrink-0',
            avatar: 'shrink-0',
            label: 'font-medium text-default mt-2',
            description: 'text-muted mt-1',
            actions: 'flex flex-wrap gap-1.5 shrink-0 mt-4',
            files: '',
            file: 'relative',
            fileLeadingAvatar: 'shrink-0',
            fileWrapper: 'flex flex-col min-w-0',
            fileName: 'text-default truncate',
            fileSize: 'text-muted truncate',
            fileTrailingButton: ''
          },
          variants: {
            color: {
              primary: '',
              secondary: '',
              success: '',
              info: '',
              warning: '',
              error: '',
              neutral: ''
            },
            variant: {
              area: {
                wrapper: 'px-4 py-3',
                base: 'p-4'
              },
              button: {}
            },
            size: {
              xs: {
                base: 'text-xs',
                icon: 'size-4',
                file: 'text-xs px-2 py-1 gap-1',
                fileWrapper: 'flex-row gap-1'
              },
              sm: {
                base: 'text-xs',
                icon: 'size-4',
                file: 'text-xs px-2.5 py-1.5 gap-1.5',
                fileWrapper: 'flex-row gap-1'
              },
              md: {
                base: 'text-sm',
                icon: 'size-5',
                file: 'text-xs px-2.5 py-1.5 gap-1.5'
              },
              lg: {
                base: 'text-sm',
                icon: 'size-5',
                file: 'text-sm px-3 py-2 gap-2',
                fileSize: 'text-xs'
              },
              xl: {
                base: 'text-base',
                icon: 'size-6',
                file: 'text-sm px-3 py-2 gap-2'
              }
            },
            layout: {
              list: {
                root: 'gap-2 items-start',
                files: 'flex flex-col w-full gap-2',
                file: 'min-w-0 flex items-center border border-default rounded-md w-full',
                fileTrailingButton: 'ms-auto'
              },
              grid: {
                fileWrapper: 'hidden',
                fileLeadingAvatar: 'size-full rounded-lg',
                fileTrailingButton: 'absolute -top-1.5 -end-1.5 p-0 rounded-full border-2 border-bg'
              }
            },
            position: {
              inside: '',
              outside: ''
            },
            dropzone: {
              true: 'border-dashed data-[dragging=true]:bg-elevated/25'
            },
            interactive: {
              true: ''
            },
            highlight: {
              true: ''
            },
            multiple: {
              true: ''
            },
            disabled: {
              true: 'cursor-not-allowed opacity-75'
            }
          },
          compoundVariants: [
            {
              color: 'primary',
              class: 'focus-visible:outline-primary'
            },
            {
              color: 'primary',
              highlight: true,
              class: 'border-primary'
            },
            {
              color: 'neutral',
              class: 'focus-visible:outline-inverted'
            },
            {
              color: 'neutral',
              highlight: true,
              class: 'border-inverted'
            },
            {
              size: 'xs',
              layout: 'list',
              class: {
                fileTrailingButton: '-me-1'
              }
            },
            {
              size: 'sm',
              layout: 'list',
              class: {
                fileTrailingButton: '-me-1.5'
              }
            },
            {
              size: 'md',
              layout: 'list',
              class: {
                fileTrailingButton: '-me-1.5'
              }
            },
            {
              size: 'lg',
              layout: 'list',
              class: {
                fileTrailingButton: '-me-2'
              }
            },
            {
              size: 'xl',
              layout: 'list',
              class: {
                fileTrailingButton: '-me-2'
              }
            },
            {
              variant: 'button',
              size: 'xs',
              class: {
                base: 'p-1'
              }
            },
            {
              variant: 'button',
              size: 'sm',
              class: {
                base: 'p-1.5'
              }
            },
            {
              variant: 'button',
              size: 'md',
              class: {
                base: 'p-1.5'
              }
            },
            {
              variant: 'button',
              size: 'lg',
              class: {
                base: 'p-2'
              }
            },
            {
              variant: 'button',
              size: 'xl',
              class: {
                base: 'p-2'
              }
            },
            {
              layout: 'grid',
              multiple: true,
              class: {
                files: 'grid grid-cols-2 md:grid-cols-3 gap-4 w-full',
                file: 'p-0 aspect-square'
              }
            },
            {
              layout: 'grid',
              multiple: false,
              class: {
                file: 'absolute inset-0 p-0'
              }
            },
            {
              interactive: true,
              disabled: false,
              class: 'hover:bg-elevated/25'
            }
          ],
          defaultVariants: {
            color: 'primary',
            variant: 'area',
            size: 'md'
          }
        }
      }
    })
  ]
})
为便于阅读,compoundVariants 中的某些颜色已省略。请在 GitHub 上查看源代码。