سفارشها، کارِ گروهی
پیشنمایش تمامصفحهسگمنتِ وضعیت با شمار، جدولِ فشرده با انتخابِ همه و نیمهانتخاب، و نوارِ اقدامِ گروهی (ارسال، چاپِ برچسب) که جای ابزارها را میگیرد.
۱. نصب کامپوننتها
این بلاک از ۹ آیتم دیگ استفاده میکند: badgebuttoncheckboxcontrol-sizeiconspersiantabletoasttoggle-group
پکیجمنیجر پیشفرض Node.jsنصب سریع با لینک سخت و دیسک مشترکYarn نسخهٔ ۲ به بالا (Berry)رانتایم و پکیجمنیجر Bun
npx digdesign@latest add https://docs.digdesign.ir/r/badge.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/checkbox.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/table.json https://docs.digdesign.ir/r/toast.json https://docs.digdesign.ir/r/toggle-group.jsonpnpm dlx digdesign@latest add https://docs.digdesign.ir/r/badge.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/checkbox.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/table.json https://docs.digdesign.ir/r/toast.json https://docs.digdesign.ir/r/toggle-group.jsonyarn dlx digdesign@latest add https://docs.digdesign.ir/r/badge.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/checkbox.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/table.json https://docs.digdesign.ir/r/toast.json https://docs.digdesign.ir/r/toggle-group.jsonbunx --bun digdesign@latest add https://docs.digdesign.ir/r/badge.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/checkbox.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/table.json https://docs.digdesign.ir/r/toast.json https://docs.digdesign.ir/r/toggle-group.json۲. کد صفحه
کد را در app/<route>/page.tsx بگذارید و متنها، داده و هندلرها را با محصول خودتان عوض کنید.
orders-bulk.tsx
"use client"
import * as React from "react"
import { DownloadIcon, FilterIcon } from "@/components/icons"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { toast } from "@/components/ui/toast"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
import { formatCurrency, toFaDigits } from "@/lib/persian"
type Status = "pending" | "paid" | "shipped"
const STATUS: Record<Status, { label: string; color: "warning" | "info" | "success" }> = {
pending: { label: "در انتظارِ پرداخت", color: "warning" },
paid: { label: "پرداختشده", color: "info" },
shipped: { label: "ارسالشده", color: "success" },
}
const ORDERS: { id: string; customer: string; city: string; total: number; status: Status }[] = [
{ id: "1101", customer: "آرمیتا رستگار", city: "تهران", total: 2_450_000, status: "paid" },
{ id: "1100", customer: "کیان مرادی", city: "اصفهان", total: 1_265_000, status: "paid" },
{ id: "1099", customer: "نیلوفر احمدی", city: "شیراز", total: 5_100_000, status: "pending" },
{ id: "1098", customer: "سارا محمدی", city: "تهران", total: 789_000, status: "shipped" },
{ id: "1097", customer: "امیر رضایی", city: "مشهد", total: 3_320_000, status: "paid" },
]
/**
* سفارشها برای کارِ گروهی: سگمنتِ وضعیت با شمار، جدولِ فشرده با چکباکس و «انتخابِ همه»
* (حالتِ نیمهانتخاب هم)، و نوارِ اقدامِ گروهی که با انتخاب جای ابزارها را میگیرد: ارسال،
* چاپِ برچسب، خروجی؛ پس از اقدام انتخاب پاک و نتیجه گزارش میشود.
*/
export default function OrdersBulkBlock() {
const [orders, setOrders] = React.useState(ORDERS)
const [filter, setFilter] = React.useState<Status | "all">("all")
const [selected, setSelected] = React.useState<string[]>([])
const visible = orders.filter((order) => filter === "all" || order.status === filter)
const checkedCount = visible.filter((order) => selected.includes(order.id)).length
const allState = checkedCount === 0 ? false : checkedCount === visible.length ? true : "indeterminate"
const count = (value: Status | "all") => orders.filter((order) => value === "all" || order.status === value).length
function ship() {
setOrders((list) => list.map((order) => (selected.includes(order.id) ? { ...order, status: "shipped" } : order)))
toast.success(`${toFaDigits(selected.length)} سفارش «ارسالشده» شد.`)
setSelected([])
}
return (
<ControlSizeProvider size="xl" asChild>
<main className="mx-auto flex w-full max-w-5xl flex-col gap-8 p-4 sm:p-6">
<header className="flex flex-col gap-1">
<h1 className="text-2xl font-bold">سفارشها</h1>
<p className="text-muted-foreground">چند سفارش را انتخاب کن و یکجا جلو ببر.</p>
</header>
<div className="flex flex-col gap-4">
{selected.length > 0 ? (
<div className="flex min-h-12 flex-wrap items-center justify-between gap-3 rounded-xl bg-primary-subtle px-4 py-2" aria-live="polite">
<span className="text-sm font-medium">{toFaDigits(selected.length)} سفارش انتخاب شده</span>
<div className="flex flex-wrap gap-2">
<Button onClick={ship}>علامتِ ارسال</Button>
<Button variant="outline" className="bg-card" onClick={() => toast.success("برچسبها آمادهٔ چاپ است.")}>
چاپِ برچسب
</Button>
<Button variant="ghost" onClick={() => setSelected([])}>
لغوِ انتخاب
</Button>
</div>
</div>
) : (
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="max-w-full overflow-x-auto">
<ToggleGroup type="single" variant="segmented" value={filter} onValueChange={(value) => value && setFilter(value as Status | "all")} aria-label="وضعیت">
{(["all", "pending", "paid", "shipped"] as const).map((value) => (
<ToggleGroupItem key={value} value={value} className="gap-2 whitespace-nowrap">
{value === "all" ? "همه" : STATUS[value].label}
<span className="text-xs text-muted-foreground tabular-nums">{toFaDigits(count(value))}</span>
</ToggleGroupItem>
))}
</ToggleGroup>
</div>
<div className="flex gap-2">
<Button variant="outline">
<FilterIcon />
فیلتر
</Button>
<Button variant="outline" onClick={() => toast.success("خروجی اکسل آماده شد.")}>
<DownloadIcon />
خروجی
</Button>
</div>
</div>
)}
{/* موبایل: کارتِ هر سفارش با تیک؛ کلِ کارت ناحیهٔ لمس است. */}
<ul className="flex flex-col divide-y divide-border rounded-xl border border-border bg-card md:hidden">
{visible.map((order) => {
const checked = selected.includes(order.id)
return (
<li key={order.id}>
<label className={`flex cursor-pointer items-center gap-3 p-4 ${checked ? "bg-primary-subtle" : ""}`}>
<Checkbox checked={checked} onCheckedChange={(value) => setSelected((list) => (value ? [...list, order.id] : list.filter((id) => id !== order.id)))} aria-label={`انتخابِ سفارشِ ${toFaDigits(order.id)}`} />
<span className="flex min-w-0 flex-1 flex-col gap-1">
<span className="flex items-center justify-between gap-2">
<span className="font-bold">{order.customer}</span>
<Badge variant="soft" color={STATUS[order.status].color}>
{STATUS[order.status].label}
</Badge>
</span>
<span className="text-sm text-muted-foreground tabular-nums">
#{toFaDigits(order.id)} · {order.city} · {formatCurrency(order.total)}
</span>
</span>
</label>
</li>
)
})}
</ul>
<div className="hidden rounded-xl border border-border bg-card md:block">
<Table className="min-w-[36rem]">
<TableHeader className="bg-subtle">
<TableRow className="hover:bg-subtle">
<TableHead className="w-12">
<Checkbox checked={allState} onCheckedChange={(checked) => setSelected(checked === true ? visible.map((order) => order.id) : [])} aria-label="انتخابِ همه" />
</TableHead>
<TableHead>سفارش</TableHead>
<TableHead>مشتری</TableHead>
<TableHead>شهر</TableHead>
<TableHead className="text-end">مبلغ</TableHead>
<TableHead>وضعیت</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{visible.map((order) => {
const checked = selected.includes(order.id)
return (
<TableRow key={order.id} data-state={checked ? "selected" : undefined}>
<TableCell>
<Checkbox checked={checked} onCheckedChange={(value) => setSelected((list) => (value ? [...list, order.id] : list.filter((id) => id !== order.id)))} aria-label={`انتخابِ سفارشِ ${toFaDigits(order.id)}`} />
</TableCell>
<TableCell className="font-medium">
<bdi dir="ltr">#{toFaDigits(order.id)}</bdi>
</TableCell>
<TableCell>{order.customer}</TableCell>
<TableCell className="text-muted-foreground">{order.city}</TableCell>
<TableCell className="text-end tabular-nums">{formatCurrency(order.total)}</TableCell>
<TableCell>
<Badge variant="soft" color={STATUS[order.status].color}>
{STATUS[order.status].label}
</Badge>
</TableCell>
</TableRow>
)
})}
</TableBody>
</Table>
</div>
</div>
</main>
</ControlSizeProvider>
)
}