بارگذاری فایل
پیشنمایش تمامصفحهناحیهٔ رهاکردن، کارتِ هر فایل با برچسبِ نوع، پیشرفت و درصد، لغو حینِ بارگذاری، خطا با تلاشِ دوباره، وارد کردن از لینک، قالبِ نمونه و ثبت پس از اتمام.
۱. نصب کامپوننتها
این بلاک از ۹ آیتم دیگ استفاده میکند: buttoncardcontrol-sizefile-uploadiconsinputlabelpersiantoast
پکیجمنیجر پیشفرض Node.jsنصب سریع با لینک سخت و دیسک مشترکYarn نسخهٔ ۲ به بالا (Berry)رانتایم و پکیجمنیجر Bun
npx digdesign@latest add https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/card.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/file-upload.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/input.json https://docs.digdesign.ir/r/label.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/toast.jsonpnpm dlx digdesign@latest add https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/card.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/file-upload.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/input.json https://docs.digdesign.ir/r/label.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/toast.jsonyarn dlx digdesign@latest add https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/card.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/file-upload.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/input.json https://docs.digdesign.ir/r/label.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/toast.jsonbunx --bun digdesign@latest add https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/card.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/file-upload.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/input.json https://docs.digdesign.ir/r/label.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/toast.json۲. کد صفحه
کد را در app/<route>/page.tsx بگذارید و متنها، داده و هندلرها را با محصول خودتان عوض کنید.
upload-files.tsx
"use client"
import * as React from "react"
import { DownloadIcon, GlobeIcon } from "@/components/icons"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { FileTypeIcon, FileUpload, type FileUploadHandler } from "@/components/ui/file-upload"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { toast } from "@/components/ui/toast"
import { toFaDigits } from "@/lib/persian"
type UploadState = "uploading" | "done" | "error"
/**
* بارگذاری شبیهسازیشده؛ در پروژهٔ واقعی همان fetch یا XHR با onProgress است.
* فایلی که اسمش «ناقص» دارد وسطِ راه خطا میدهد تا حالتِ «تلاشِ دوباره» دیده شود.
*/
const simulate: FileUploadHandler = (file, { onProgress, signal }) =>
new Promise<void>((resolve, reject) => {
let progress = 0
const fails = file.name.includes("ناقص")
const timer = setInterval(() => {
progress += 6 + Math.random() * 12
if (fails && progress >= 50) {
clearInterval(timer)
reject(new Error("اتصال قطع شد."))
return
}
onProgress(Math.min(progress, 100))
if (progress >= 100) {
clearInterval(timer)
resolve()
}
}, 220)
signal.addEventListener("abort", () => clearInterval(timer))
})
/** نامِ فایل از انتهای مسیرِ لینک؛ اگر نبود، یک نامِ عمومی. */
function nameFromUrl(url: URL) {
const last = decodeURIComponent(url.pathname.split("/").filter(Boolean).pop() ?? "")
return last.includes(".") ? last : `${url.hostname}.pdf`
}
/**
* صفحهٔ بارگذاری فایل: ناحیهٔ رهاکردن، فهرستِ کارتی فایلها با پیشرفت و وضعیت،
* وارد کردن از لینک، دانلودِ قالبِ نمونه و دکمههای انصراف و ثبت. «ثبت» فقط وقتی
* فعال است که دستکم یک فایل کامل بارگذاری شده و هیچ فایلی در راه نیست.
*/
export default function UploadFilesBlock() {
const uploaderRef = React.useRef<HTMLDivElement>(null)
const [files, setFiles] = React.useState<File[]>([])
const [states, setStates] = React.useState(() => new Map<File, UploadState>())
const [link, setLink] = React.useState("")
const [linkError, setLinkError] = React.useState("")
const [round, setRound] = React.useState(0)
const mark = (file: File, state: UploadState | undefined) =>
setStates((current) => {
const next = new Map(current)
if (state) next.set(file, state)
else next.delete(file)
return next
})
// وضعیتِ هر فایل را کنارِ خودِ FileUpload نگه میداریم تا دکمهٔ ثبت بداند کی مجاز است.
const upload: FileUploadHandler = async (file, context) => {
mark(file, "uploading")
try {
await simulate(file, context)
if (!context.signal.aborted) mark(file, "done")
} catch (error) {
if (!context.signal.aborted) mark(file, "error")
throw error
}
}
const changeFiles = (next: File[]) => {
setFiles(next)
setStates((current) => new Map([...current].filter(([file]) => next.includes(file))))
}
const uploading = [...states.values()].filter((state) => state === "uploading").length
const done = [...states.values()].filter((state) => state === "done").length
const importLink = (event: React.FormEvent) => {
event.preventDefault()
let url: URL
try {
url = new URL(link.trim())
if (!/^https?:$/.test(url.protocol)) throw new Error()
} catch {
setLinkError("یک لینکِ کامل با https:// وارد کنید.")
return
}
setLinkError("")
setLink("")
// در محصولِ واقعی سرور فایل را از لینک میگیرد؛ اینجا همان مسیرِ انتخابِ فایل را طی میکند.
const input = uploaderRef.current?.querySelector<HTMLInputElement>('input[type="file"]')
if (!input || typeof DataTransfer === "undefined") return
const transfer = new DataTransfer()
transfer.items.add(new File(["نمونه"], nameFromUrl(url), { type: "application/pdf" }))
input.files = transfer.files
input.dispatchEvent(new Event("change", { bubbles: true }))
}
const reset = () => {
setFiles([])
setStates(new Map())
setRound((value) => value + 1)
}
const submit = () => {
toast.success(`${toFaDigits(done)} فایل ثبت شد.`)
reset()
}
return (
<ControlSizeProvider size="xl" asChild>
<main className="mx-auto flex w-full max-w-xl flex-col p-4 sm:p-6">
<Card>
<CardHeader>
<CardTitle asChild className="text-lg">
<h1>بارگذاری فایلها</h1>
</CardTitle>
<CardDescription>مدارک پروژه را اضافه کنید؛ پس از اتمام بارگذاری، ثبت کنید.</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-6">
{/* قالبِ نمونه اول میآید تا کاربر پیش از انتخابِ فایل، ساختارِ درست را داشته باشد. */}
<div className="flex items-center gap-3 rounded-xl bg-muted p-3">
<FileTypeIcon kind="XLSX" className="size-11" />
<span className="flex min-w-0 flex-1 flex-col gap-0.5">
<span className="text-sm font-medium">قالب نمونه</span>
<span className="text-xs text-muted-foreground">برای وارد کردن فهرست، فایل را با همین ستونها پر کنید.</span>
</span>
<Button variant="outline" asChild>
<a href="data:text/csv;charset=utf-8,%EF%BB%BF%D9%86%D8%A7%D9%85,%D8%AA%D9%88%D8%B6%DB%8C%D8%AD" download="قالب-نمونه.csv">
<DownloadIcon />
دانلود
</a>
</Button>
</div>
<div ref={uploaderRef}>
<FileUpload
key={round}
name="files"
multiple
maxFiles={8}
maxSize={25 * 1024 * 1024}
accept=".pdf,.doc,.docx,.xls,.xlsx,.csv,.zip,image/*"
hint="PDF، ورد، اکسل، ZIP یا تصویر؛ هر فایل تا ۲۵ مگابایت"
files={files}
onFilesChange={changeFiles}
onUpload={upload}
/>
</div>
<form onSubmit={importLink} className="flex flex-col gap-2" noValidate>
<Label htmlFor="upload-link">وارد کردن از لینک</Label>
<div className="flex gap-2">
<Input
id="upload-link"
dir="ltr"
inputMode="url"
placeholder="https://example.com/file.pdf"
value={link}
onChange={(event) => setLink(event.target.value)}
aria-invalid={linkError ? true : undefined}
aria-describedby={linkError ? "upload-link-error" : undefined}
startContent={<GlobeIcon />}
className="flex-1"
/>
<Button type="submit" variant="outline" disabled={!link.trim()}>
افزودن
</Button>
</div>
{linkError && (
<p id="upload-link-error" className="text-xs text-destructive" role="alert">
{linkError}
</p>
)}
</form>
</CardContent>
{/* خطِ جداکننده و فاصلهٔ بیشتر، دکمههای پایانی را از گروههای محتوا جدا میکند. */}
<CardFooter className="flex-col-reverse items-stretch gap-3 border-t sm:flex-row sm:justify-end">
<ControlSizeProvider size="xl">
<Button variant="outline" onClick={reset} disabled={files.length === 0}>
انصراف
</Button>
<Button onClick={submit} disabled={done === 0 || uploading > 0}>
{uploading > 0 ? "منتظر پایان بارگذاری…" : done > 0 ? `ثبت ${toFaDigits(done)} فایل` : "ثبت فایلها"}
</Button>
</ControlSizeProvider>
</CardFooter>
</Card>
</main>
</ControlSizeProvider>
)
}