File Upload
在队列或附件工作区管理文件、预览、进度与重试操作。
适用场景
- 当界面需要以下行为时使用 File Upload:在队列或附件工作区管理文件、预览、进度与重试操作。
安装
选择安装方式,用法示例将同步更新导入路径。
$ bunx --bun shadcn-vue@2.8.2 add https://orvui.efu.me/r/file-upload.json https://orvui.efu.me/r/attachment-upload.json
首次使用时,将安装的 src/components/orvui/styles.css 导入项目样式入口。
示例
Attachment Upload
attachment-upload.vue管理混合附件,查看预览、进度、重试操作与图片详情。
Attachments:
- launch-brief.pdfUpload failed31 MB
- orange-flowers.jpg9.3 MB
- 0:120:48
packages/vue/src/blocks/file-upload/examples/attachment-upload.vue
<script setup lang="ts">
import { onBeforeUnmount, ref, watch } from "vue";
import { AttachmentUpload, type AttachmentUploadItem } from "@/components/orvui/blocks/file-upload";
const items = ref<AttachmentUploadItem[]>([
{
id: "brief",
name: "launch-brief.pdf",
kind: "file",
size: 32400000,
href: "data:application/pdf,orvui%20launch%20brief",
status: "failed",
error: "Upload failed",
},
{
id: "flowers",
name: "orange-flowers.jpg",
kind: "image",
size: 9800000,
previewUrl:
"https://images.unsplash.com/photo-1490750967868-88aa4486c946?auto=format&fit=crop&w=1200&q=85",
},
{
id: "voice-note",
name: "launch-note.m4a",
kind: "audio",
currentTime: 12,
duration: 48,
},
]);
const playing = ref<string>(),
jobs = new Set<ReturnType<typeof setTimeout>>();
let clock: ReturnType<typeof setInterval> | undefined;
watch(playing, (id) => {
clearInterval(clock);
if (id)
clock = setInterval(() => {
items.value = items.value.map((item) =>
item.id === id && item.duration
? {
...item,
currentTime: Math.min(item.duration, (item.currentTime ?? 0) + 1),
}
: item,
);
}, 1000);
});
watch(items, (current) => {
const active = current.find((item) => item.id === playing.value);
if (active?.duration && (active.currentTime ?? 0) >= active.duration)
playing.value = undefined;
});
function retry(item: AttachmentUploadItem) {
items.value = items.value.map((entry) =>
entry.id === item.id
? { ...entry, status: "uploading", error: undefined }
: entry,
);
for (const [delay, status] of [
[900, "complete"],
[1900, "idle"],
] as const) {
const timer = setTimeout(() => {
jobs.delete(timer);
items.value = items.value.map((entry) =>
entry.id === item.id ? { ...entry, status } : entry,
);
}, delay);
jobs.add(timer);
}
}
onBeforeUnmount(() => {
clearInterval(clock);
for (const timer of jobs) clearTimeout(timer);
jobs.clear();
});
</script>
<template>
<div class="w-full max-w-2xl px-3 py-6 sm:px-6">
<AttachmentUpload
v-model="items"
@retry="retry"
:playing-id="playing"
@audio-toggle="playing = playing === $event.id ? undefined : $event.id"
attachments-label="Attachments:"
/>
</div>
</template>
packages/vue/src/blocks/file-upload/src/AttachmentUpload.vue
Upload Queue
upload-queue.vue将选择或拖入的文件加入队列,并分别管理上传进度。
Upload package
1 of 3 files ready
brand-assets.zip
ZIP · 18 MB
Uploadedrelease-cut.mov
MOV · 80 MB
Uploadingvendor-contract.pdf
PDF · 2.7 MB · Connection lost
Failed
packages/vue/src/blocks/file-upload/examples/upload-queue.vue
<script setup lang="ts">
import { RotateCcw } from "lucide-vue-next";
import { onBeforeUnmount, onMounted, ref } from "vue";
import {
FileUpload,
type FileUploadItem,
type FileUploadVariant,
} from "@/components/orvui/blocks/file-upload";
const initial: FileUploadItem[] = [
{
id: "brand-assets",
name: "brand-assets.zip",
size: 18400000,
type: "application/zip",
progress: 100,
status: "success",
},
{
id: "release-video",
name: "release-cut.mov",
size: 84200000,
type: "video/quicktime",
progress: 58,
status: "uploading",
},
{
id: "contracts",
name: "vendor-contract.pdf",
size: 2800000,
type: "application/pdf",
progress: 32,
status: "error",
error: "Connection lost",
},
];
function useUploadDemo() {
const items = ref(initial),
variant = ref<FileUploadVariant>("centered");
const jobs = new Map<string, ReturnType<typeof setInterval>>();
function stop(id: string) {
const job = jobs.get(id);
if (job !== undefined) clearInterval(job);
jobs.delete(id);
}
function start(id: string) {
stop(id);
const advance = () => {
const at = items.value.findIndex((entry) => entry.id === id);
if (at < 0 || items.value[at].status !== "uploading") {
stop(id);
return;
}
const previous = items.value[at];
let progress = (previous.progress ?? 0) + 7;
progress = Math.min(100, progress + Math.random() * 12);
const updated: FileUploadItem = {
...previous,
progress,
status: progress === 100 ? "success" : "uploading",
};
const snapshot = Array.from(items.value);
snapshot[at] = updated;
items.value = snapshot;
if (updated.status === "success") stop(id);
};
jobs.set(id, setInterval(advance, 520));
}
function reset() {
for (const id of jobs.keys()) stop(id);
items.value = initial;
start("release-video");
}
onMounted(() => start("release-video"));
onBeforeUnmount(() => {
for (const id of jobs.keys()) stop(id);
});
return { items, variant, start, stop, reset };
}
const { items, variant, start, stop, reset } = useUploadDemo();
const choices = [
{ id: "centered", label: "Centered" },
{ id: "default", label: "Row" },
] as const;
</script>
<template>
<div class="flex min-h-[30rem] w-full items-center justify-center">
<div
class="w-full max-w-md rounded-[2rem] border border-border bg-background p-3"
>
<div class="mb-3 flex flex-wrap items-center justify-between gap-2 px-1">
<div>
<p class="text-sm font-semibold text-foreground">Upload package</p>
<p class="text-xs text-muted-foreground">
{{ items.filter((item) => item.status === "success").length }} of
{{ items.length }} files ready
</p>
</div>
<div class="flex items-center gap-1.5">
<div class="flex rounded-full border border-border bg-muted p-1">
<button
v-for="choice in choices"
:key="choice.id"
type="button"
@click="variant = choice.id"
:data-selected="choice.id === variant"
class="h-7 rounded-full px-3 text-xs font-medium text-muted-foreground transition-[background-color,color,transform] duration-150 hover:text-foreground active:scale-95 data-[selected=true]:bg-background data-[selected=true]:text-foreground"
>
{{ choice.label }}
</button>
</div>
<button
type="button"
@click="reset"
class="grid h-9 w-9 place-items-center rounded-full border border-border text-muted-foreground transition-colors hover:text-foreground active:scale-95"
aria-label="Reset upload queue"
>
<RotateCcw class="h-3.5 w-3.5" />
</button>
</div>
</div>
<FileUpload
v-model="items"
:variant="variant"
@files-added="(added) => added.forEach((item) => start(item.id))"
@retry="start($event.id)"
@remove="stop($event.id)"
:max-files="5"
:title="
variant === 'centered' ? 'Drop files to upload' : 'Drop release files'
"
description="PDF, images, video or zipped assets"
/>
</div>
</div>
</template>
packages/vue/src/blocks/file-upload/src/FileUpload.vue
状态与事件
需要由表单、路由或父组件管理时,绑定下列状态并接收对应更新。v-model 是属性与 update 事件的简写;值的类型应与 API 一致。
| 组件 | Vue 绑定 | 更新事件 |
|---|---|---|
FileUpload | v-modelFileUploadItem[] | update:modelValue |
AttachmentUpload | v-modelAttachmentUploadItem[] | update:modelValue |
可选受控属性未传入时,按组件文档的默认行为工作;不要用固定的 false 或空值代替未传入。异步请求和持久化在应用中处理,再把结果传回组件。
API 参考
Attachment Upload
modelValue?AttachmentUploadItem[]—value?AttachmentUploadItem[]—defaultValue?AttachmentUploadItem[]() => []playingId?string—accept?string—multiple?booleantruemaxFiles?number12maxFileSize?number500 * 1024 * 1024disabled?booleanfalsetitle?string"Drag and drop or browse files"description?string—attachmentsLabel?string"Attachments"className?string—classNames?AttachmentUploadClassNames—onRetry?(item: AttachmentUploadItem) => void—事件
update:modelValue[items: AttachmentUploadItem[]]@update:modelValuevalueChange[items: AttachmentUploadItem[]]@valueChangefilesAdded[items: AttachmentUploadItem[], files: File[]]@filesAddedfilesRejected[files: File[], reason: AttachmentRejectReason]@filesRejectedremove[item: AttachmentUploadItem]@removeretry[item: AttachmentUploadItem]@retryaudioToggle[item: AttachmentUploadItem]@audioToggleUpload Queue
modelValue?FileUploadItem[]—value?FileUploadItem[]—defaultValue?FileUploadItem[]() => []accept?string—multiple?booleantruemaxFiles?number—disabled?booleanfalsevariant?"default" | "centered""default"title?string"Drop files here"description?string"Add files to the upload queue"browseLabel?string"Browse"className?string—classNames?FileUploadClassNames—事件
update:modelValue[items: FileUploadItem[]]@update:modelValuevalueChange[items: FileUploadItem[]]@valueChangefilesAdded[items: FileUploadItem[], files: File[]]@filesAddedremove[item: FileUploadItem]@removeretry[item: FileUploadItem]@retryreject[files: File[]]@reject无障碍
为交互控件提供明确的无障碍名称。保留文档中的键盘路径、可见焦点与减少动态效果状态;自定义插槽应继续保留所替代部件的语义。
常见问题
- 如何配置 File Upload?
- 通过 modelValue、value、defaultValue、accept、multiple、maxFiles、disabled、variant、title、description、browseLabel、className、classNames 配置 File Upload;API 表列出其类型与默认值。
- 如何连接到应用状态?
- 监听 update:modelValue、valueChange、filesAdded、remove、retry、reject,将组件连接到应用状态。
- 可以在服务端渲染吗?
- 可以在 Vue SSR 应用中正常渲染。服务端与客户端的初始 props 应保持一致;浏览器测量与交互资源在挂载后启动。
请留意
部分组件参考了开源作品,相关署名与独立许可请参阅 NOTICE。
更新于
来源与许可 ↗MIT · 0.1.1