خلافی خودرو
پیشنمایش تمامصفحهاستعلام خلافی با پلاک و کد ملی، جدول تخلفات و پرداخت.
۱. نصب کامپوننتها
این بلاک از ۹ آیتم دیگ استفاده میکند: buttoncheckboxcontrol-sizeempty-statelabellicense-platepersiantext-fieldtoast
پکیجمنیجر پیشفرض Node.jsنصب سریع با لینک سخت و دیسک مشترکYarn نسخهٔ ۲ به بالا (Berry)رانتایم و پکیجمنیجر Bun
npx digdesign@latest add 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/empty-state.json https://docs.digdesign.ir/r/label.json https://docs.digdesign.ir/r/license-plate.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/text-field.json https://docs.digdesign.ir/r/toast.jsonpnpm dlx digdesign@latest add 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/empty-state.json https://docs.digdesign.ir/r/label.json https://docs.digdesign.ir/r/license-plate.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/text-field.json https://docs.digdesign.ir/r/toast.jsonyarn dlx digdesign@latest add 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/empty-state.json https://docs.digdesign.ir/r/label.json https://docs.digdesign.ir/r/license-plate.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/text-field.json https://docs.digdesign.ir/r/toast.jsonbunx --bun digdesign@latest add 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/empty-state.json https://docs.digdesign.ir/r/label.json https://docs.digdesign.ir/r/license-plate.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/text-field.json https://docs.digdesign.ir/r/toast.json۲. کد صفحه
کد را در app/<route>/page.tsx بگذارید و متنها، داده و هندلرها را با محصول خودتان عوض کنید.
vehicle-fines.tsx
"use client"
import * as React from "react"
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { EmptyState, EmptyStateDescription, EmptyStateTitle } from "@/components/ui/empty-state"
import { Label } from "@/components/ui/label"
import { LicensePlate } from "@/components/ui/license-plate"
import { TextField } from "@/components/ui/text-field"
import { toast } from "@/components/ui/toast"
import { formatCurrency, formatJalaliDateTime, isValidNationalId, toFaDigits } from "@/lib/persian"
const FINES = [
{ id: "f1", title: "عبور از چراغ قرمز", place: "تهران، میدان ونک", date: "2026-09-12T18:20:00", amount: 1_000_000, camera: true },
{ id: "f2", title: "سرعتِ غیرمجاز", place: "بزرگراه همت، شرق به غرب", date: "2026-09-05T07:42:00", amount: 600_000, camera: true },
{ id: "f3", title: "توقف ممنوع", place: "تهران، خیابان ولیعصر", date: "2026-08-30T11:05:00", amount: 300_000, camera: false },
]
/**
* استعلامِ خلافی: فرمِ پلاک و کد ملی در ستونِ کنار؛ نتیجه با کارتِ تیرهٔ مجموع، فهرستِ
* تخلفها با انتخابِ چندتایی، و پرداختِ فقط موارد انتخابشده. اگر خلافی نبود، حالتِ خالی.
*/
export default function VehicleFinesBlock() {
const [loading, setLoading] = React.useState(false)
const [result, setResult] = React.useState<typeof FINES | null>(null)
const [selected, setSelected] = React.useState<string[]>([])
const total = result?.filter((fine) => selected.includes(fine.id)).reduce((sum, fine) => sum + fine.amount, 0) ?? 0
async function inquire(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault()
setLoading(true)
await new Promise((resolve) => setTimeout(resolve, 900))
setLoading(false)
setResult(FINES)
setSelected(FINES.map((fine) => fine.id))
}
return (
<ControlSizeProvider size="xl" asChild>
<main className="mx-auto grid grid-cols-1 w-full max-w-5xl items-start gap-8 p-4 sm:p-6 lg:grid-cols-[22rem_minmax(0,1fr)]">
<form onSubmit={inquire} className="flex flex-col gap-6 rounded-xl border border-border bg-card p-6">
<div className="flex flex-col gap-1">
<h1 className="text-2xl font-bold">استعلامِ خلافی</h1>
<p className="text-sm text-muted-foreground">پلاک و کد ملی مالک را وارد کنید.</p>
</div>
<div className="flex flex-col gap-2">
<Label id="plate-label">پلاک خودرو</Label>
<LicensePlate aria-labelledby="plate-label" name="plate" required onValueChange={() => setResult(null)} />
</div>
<TextField
label="کد ملی مالک"
name="nationalId"
inputMode="numeric"
dir="ltr"
maxLength={10}
required
validate={(value) => (isValidNationalId(value) ? undefined : value.length === 10 ? "کد ملی معتبر نیست." : "کد ملی باید ۱۰ رقم باشد.")}
/>
<Button type="submit" fullWidth loading={loading} className="mt-2">
استعلام
</Button>
</form>
{!result ? (
<EmptyState className="rounded-xl bg-subtle py-16">
<EmptyStateTitle>هنوز استعلامی نگرفتهاید</EmptyStateTitle>
<EmptyStateDescription>بعد از استعلام، تخلفها همینجا میآیند و میتوانید یکجا پرداخت کنید.</EmptyStateDescription>
</EmptyState>
) : (
<section aria-label="نتیجهٔ استعلام" className="flex flex-col gap-6">
<div className="flex flex-wrap items-end justify-between gap-4 rounded-xl bg-inverse p-6 text-[var(--dig-white)] [--muted-foreground:var(--dig-neutral-400)]">
<div className="flex flex-col gap-1">
<span className="text-sm text-muted-foreground">{toFaDigits(result.length)} تخلفِ پرداختنشده</span>
<span className="text-3xl font-black tabular-nums">{formatCurrency(result.reduce((sum, fine) => sum + fine.amount, 0))}</span>
</div>
<span className="text-sm text-muted-foreground">بهروز تا امروز</span>
</div>
<ul className="flex flex-col gap-3">
{result.map((fine) => (
<li key={fine.id} className="flex items-start gap-3 rounded-xl border border-border p-4 transition-colors has-[[data-state=checked]]:border-primary has-[[data-state=checked]]:bg-primary-subtle">
<Checkbox
id={`fine-${fine.id}`}
checked={selected.includes(fine.id)}
onCheckedChange={(checked) => setSelected((list) => (checked ? [...list, fine.id] : list.filter((id) => id !== fine.id)))}
className="mt-1"
/>
<Label htmlFor={`fine-${fine.id}`} className="flex flex-1 flex-col items-start gap-1">
<span className="font-semibold">{fine.title}</span>
<span className="text-sm font-normal text-muted-foreground">
{formatJalaliDateTime(fine.date)} · {fine.place}
</span>
{fine.camera && <span className="text-xs font-normal text-brand-secondary">ثبت با دوربین؛ تصویر در جزئیات</span>}
</Label>
<span className="font-bold tabular-nums">{formatCurrency(fine.amount)}</span>
</li>
))}
</ul>
<div className="flex flex-wrap items-center justify-between gap-4 border-t border-border pt-8">
<span className="text-sm text-muted-foreground">{toFaDigits(selected.length)} مورد انتخاب شده</span>
<Button disabled={selected.length === 0} onClick={() => toast.success("پرداخت انجام شد.")}>
پرداختِ {formatCurrency(total)}
</Button>
</div>
</section>
)}
</main>
</ControlSizeProvider>
)
}