پرتوپرتو

جدول داده (DataTable)

جدول داده ترکیبی با مرتب‌سازی، صفحه‌بندی، انتخاب ردیف، و حالت‌های بارگذاری و خالی

معرفی

DataTable یک کامپوننت ترکیبی است که جدول، مرتب‌سازی، صفحه‌بندی، انتخاب ردیف، و مدیریت حالت‌های خالی و بارگذاری را در یک API ساده ارائه می‌دهد. این کامپوننت رایج‌ترین الگوی نمایش داده در اپلیکیشن‌های SaaS را پوشش می‌دهد.

چه زمانی استفاده کنیم:

  • لیست داده‌ها با بیش از ۵ آیتم که نیاز به مرتب‌سازی یا صفحه‌بندی دارند
  • جداول با قابلیت انتخاب برای عملیات دسته‌ای
  • هر صفحه لیست در اپلیکیشن (اینفلوئنسرها، کمپین‌ها، گزارش‌ها)

چه زمانی استفاده نکنیم:

  • نمایش ساده ۳-۵ آیتم بدون تعامل — از Table مستقیم استفاده کنید
  • جداول با ساختار بسیار سفارشی — از کامپوننت‌های Table به صورت ترکیبی استفاده کنید
  • نمایش کارتی — از Card استفاده کنید
رضا کریمی۲۳۴٬۰۰۰۲٫۸٪
سارا احمدی۸۷٬۳۰۰۳٫۱٪
امیر رضایی۴۵٬۸۰۰۳٫۵٪
علی محمدی۱۲٬۵۰۰۴٫۲٪
مریم حسینی۵٬۲۰۰۶٫۷٪
import { DataTable } from '@partodata/ui'

زمین بازی

با تغییر تنظیمات زیر، پیش‌نمایش زنده را مشاهده کنید.

زمین بازی
احساس
پست الفاینستاگرام۱۲٬۳۴۵
۷۸٪
پست بتوییتر۸٬۹۰۰
۶۵٪
پست جتلگرام۵٬۴۰۰
۴۲٪
پست داینستاگرام۴٬۲۰۰
۸۸٪
پست هتوییتر۳٬۱۰۰
۳۵٪
تنظیمات
ظاهر
داده
5
چیدمان
حالت
محتوا
کد این نمونه به‌صورت خودکار قابل تولید نیست — برای کد آماده‌ی copy/paste به بخش «استفاده» در بالای صفحه مراجعه کنید.

استفاده پایه

import { DataTable, type DataTableColumn } from '@partodata/ui'

interface Influencer {
  id: number
  name: string
  followers: number
  engagementRate: number
}

const columns: DataTableColumn<Influencer>[] = [
  { id: 'name', header: 'نام', cell: (row) => row.name },
  {
    id: 'followers',
    header: 'دنبال‌کننده‌ها',
    cell: (row) => row.followers.toLocaleString('en-US'),
    align: 'end',
  },
  {
    id: 'engagementRate',
    header: 'نرخ تعامل',
    cell: (row) => `${row.engagementRate}٪`,
    align: 'end',
  },
]

const data: Influencer[] = [
  { id: 1, name: 'محمد رضایی', followers: 125000, engagementRate: 4.2 },
  { id: 2, name: 'سارا احمدی', followers: 89000, engagementRate: 6.8 },
  { id: 3, name: 'علی محمدی', followers: 350000, engagementRate: 2.1 },
]

<DataTable columns={columns} data={data} />

با مرتب‌سازی

'use client'

import { useState } from 'react'
import { DataTable, type DataTableColumn, type SortDirection } from '@partodata/ui'

function SortableExample() {
  const [sortColumn, setSortColumn] = useState<string | null>(null)
  const [sortDirection, setSortDirection] = useState<SortDirection | null>(null)

  return (
    <DataTable
      columns={columns}
      data={data}
      sort={{
        column: sortColumn,
        direction: sortDirection,
        onSort: (col, dir) => {
          setSortColumn(col)
          setSortDirection(dir)
        },
      }}
    />
  )
}

با صفحه‌بندی

<DataTable
  columns={columns}
  data={pageData}
  pagination={{
    currentPage: 2,
    totalPages: 10,
    onPageChange: (page) => setCurrentPage(page),
  }}
/>

نوار صفحه‌بندی کامل

با افزودن pageSize/onPageSizeChange و totalRows، نوار پایین علاوه بر شماره‌ی صفحه‌ها، انتخاب تعداد ردیف در هر صفحه، بازه‌ی نتایج و شمار ردیف‌های انتخاب‌شده را هم نشان می‌دهد. هر سه اختیاری‌اند؛ بدون آن‌ها همان نوار شماره‌ایِ وسط‌چین قبلی رندر می‌شود.

<DataTable
  columns={columns}
  data={pageData}
  selection={{ selectedRows, onSelectionChange: setSelectedRows, getRowKey: (row) => row.id }}
  pagination={{
    currentPage,
    totalPages,
    onPageChange: setCurrentPage,
    pageSize,
    onPageSizeChange: setPageSize,
    pageSizeOptions: [10, 20, 50, 100],
    totalRows: 1240,
  }}
/>

totalRows از سرور می‌آید

این جدول سرور-صفحه‌بندی است و فقط یک صفحه ردیف در دست دارد؛ پس نمی‌تواند تعداد کل را حدس بزند. اگر totalRows را ندهید، بازه‌ی «۱–۲۰ از ۱٬۲۴۰» رندر نمی‌شود.


با انتخاب ردیف

'use client'

import { useState } from 'react'

function SelectableExample() {
  const [selected, setSelected] = useState<Set<number>>(new Set())

  return (
    <DataTable
      columns={columns}
      data={data}
      selection={{
        selectedRows: selected,
        onSelectionChange: setSelected,
        getRowKey: (row) => row.id,
      }}
    />
  )
}

حالت بارگذاری

<DataTable columns={columns} data={[]} isLoading loadingRows={5} />

حالت خالی

import { Empty, EmptyIcon, EmptyTitle, EmptyDescription, Button } from '@partodata/ui'
import { SearchX } from 'lucide-react'
;<DataTable
  columns={columns}
  data={[]}
  emptyState={
    <Empty className="border-0">
      <EmptyIcon>
        <SearchX className="size-6" />
      </EmptyIcon>
      <EmptyTitle>اینفلوئنسری یافت نشد</EmptyTitle>
      <EmptyDescription>فیلترهای خود را تغییر دهید یا اینفلوئنسر جدید اضافه کنید</EmptyDescription>
    </Empty>
  }
/>

ترکیب کامل

'use client'

import { useState } from 'react'
import {
  DataTable,
  type DataTableColumn,
  type SortDirection,
  Avatar,
  Badge,
  SocialPlatformBadge,
} from '@partodata/ui'

function FullExample() {
  const [page, setPage] = useState(1)
  const [sortCol, setSortCol] = useState<string | null>(null)
  const [sortDir, setSortDir] = useState<SortDirection | null>(null)
  const [selected, setSelected] = useState<Set<number>>(new Set())

  const columns: DataTableColumn<Influencer>[] = [
    {
      id: 'name',
      header: 'نام',
      sortable: true,
      cell: (row) => (
        <div className="flex items-center gap-2">
          <Avatar src={row.avatar} fallback={row.name[0]} size="sm" />
          <span className="font-medium">{row.name}</span>
        </div>
      ),
    },
    {
      id: 'platform',
      header: 'پلتفرم',
      cell: (row) => <SocialPlatformBadge platform={row.platform} size="sm" />,
    },
    {
      id: 'followers',
      header: 'دنبال‌کننده‌ها',
      sortable: true,
      align: 'end',
      cell: (row) => row.followers.toLocaleString('en-US'),
    },
    {
      id: 'status',
      header: 'وضعیت',
      cell: (row) => (
        <Badge variant={row.active ? 'success' : 'default'} size="sm">
          {row.active ? 'فعال' : 'غیرفعال'}
        </Badge>
      ),
    },
  ]

  return (
    <DataTable
      columns={columns}
      data={influencers}
      size="sm"
      striped
      sort={{
        column: sortCol,
        direction: sortDir,
        onSort: (col, dir) => {
          setSortCol(col)
          setSortDir(dir)
        },
      }}
      selection={{
        selectedRows: selected,
        onSelectionChange: setSelected,
        getRowKey: (row) => row.id,
      }}
      pagination={{
        currentPage: page,
        totalPages: 12,
        onPageChange: setPage,
      }}
      resultCount="۱۴۲ اینفلوئنسر"
    />
  )
}

کلیک روی ردیف

با onRowClick، ردیف‌های بدنه تعاملی می‌شوند: فوکوس‌پذیر با کیبورد (Tab) و قابل فعال‌سازی با Enter/Space، و کلیک/فعال‌سازی این callback را با (row, rowIndex) صدا می‌زند. برای «کلیک ردیف → صفحهٔ جزئیات» مناسب است. کنترل‌های تعاملی داخل سلول (دکمه، منو) رفتار خودشان را حفظ می‌کنند.

<DataTable columns={columns} data={campaigns} onRowClick={(row) => router.push(`/campaigns/${row.id}`)} />

فیلتر facetedِ ستون‌محور

DataTableFacetedFilter یک چندانتخابیِ Popover + Command است برای enumهای محدودِ دامنه — پلتفرم، احساس، موضع، وضعیت، شدت، رده‌ی تعامل. کنارِ هر گزینه، شمارِ نتایج نمایش داده می‌شود.

5 نتیجه
نامپلتفرمپست‌ها
علی محمدیاینستاگرام128
سارا احمدیتلگرام64
رضا کریمیاینستاگرام91
مریم حسینیایکس37
امیر رضاییتلگرام12
'use client'
import { DataTable, DataTableFacetedFilter } from '@partodata/ui'

const [platforms, setPlatforms] = useState<string[]>([])

<DataTableFacetedFilter
  title="پلتفرم"
  options={[
    { value: 'instagram', label: 'اینستاگرام' },
    { value: 'telegram', label: 'تلگرام' },
    { value: 'x', label: 'ایکس' },
  ]}
  selected={platforms}
  onSelectedChange={setPlatforms}
  facets={{ instagram: 1240, telegram: 87, x: 12 }}
/>

شمارش‌ها را سرور می‌دهد

facets را مصرف‌کننده پاس می‌دهد، چون DataTable دقیقاً همان dataای را رندر می‌کند که گرفته (سرور-صفحه‌بندی). شمارشی که از صفحه‌ی جاری حساب شود دروغ است. اگر facets ندهید، گزینه‌ها بدون عدد رندر می‌شوند.

نوار ابزار کامل (جست‌وجو + فیلترها + خروجی)

پرتو کامپوننتِ جداگانه‌ای به‌نامِ DataTableToolbar ندارد و لازم هم نداردFilterBar دقیقاً همین کار را می‌کند. نوار ابزارِ متعارف از قطعاتِ موجود سرِ هم می‌شود:

قطعهنقش
FilterBarظرفِ عمودی (نوار + جدول)
FilterBarRowردیفِ کنترل‌ها (flex-wrap)
SearchInput + useDebounceجست‌وجوی متنیِ debounce‌شده
DataTableFacetedFilterفیلترِ facet برای هر enum
FilterBarClearریست — فقط وقتی فیلتری فعال است
FilterBarActionsبا ms-auto به انتهای خط می‌راند
DataTableColumnVisibilityToggle / DataTableExportButtonنمایانیِ ستون و خروجی
5 نتیجه
نامپلتفرماحساسپست‌ها
علی محمدیاینستاگراممثبت128
سارا احمدیتلگرامخنثی64
رضا کریمیاینستاگراممنفی91
مریم حسینیایکسمثبت37
امیر رضاییتلگراممثبت12
'use client'
import {
  DataTable,
  DataTableFacetedFilter,
  DataTableColumnVisibilityToggle,
  DataTableExportButton,
  FilterBar,
  FilterBarRow,
  FilterBarActions,
  FilterBarClear,
  SearchInput,
  useDebounce,
} from '@partodata/ui'

const [query, setQuery] = useState('')
const debouncedQuery = useDebounce(query, 300)
const [platforms, setPlatforms] = useState<string[]>([])
const [visible, setVisible] = useState({})
const hasFilters = !!query || platforms.length > 0

// در محصول واقعی، debouncedQuery و platforms را به سرور می‌فرستید
// و سرور صفحه‌ی نتایج + facetها را برمی‌گرداند.

;<FilterBar>
  <FilterBarRow>
    <SearchInput
      value={query}
      onChange={(e) => setQuery(e.target.value)}
      onClear={() => setQuery('')}
      placeholder="جست‌وجوی نام"
      wrapperClassName="w-44"
    />
    <DataTableFacetedFilter
      title="پلتفرم"
      options={platformOptions}
      selected={platforms}
      onSelectedChange={setPlatforms}
      facets={facetCounts}
    />
    {hasFilters && (
      <FilterBarClear
        onClear={() => {
          setQuery('')
          setPlatforms([])
        }}
      />
    )}
    <FilterBarActions>
      <DataTableColumnVisibilityToggle columns={columns} visibility={{ visible, onVisibilityChange: setVisible }} />
      <DataTableExportButton columns={columns} data={rows} filename="influencers.csv" />
    </FilterBarActions>
  </FilterBarRow>

  <DataTable columns={columns} data={rows} columnVisibility={{ visible, onVisibilityChange: setVisible }} />
</FilterBar>

چرا کامپوننت جدید نساختیم

شادسی‌ان یک DataTableToolbar دارد، ولی آن فقط یک wrapperِ چیدمانی است و FilterBar ما همان نقش را با RTL و i18nِ درست بازی می‌کند. افزودنِ کامپوننتِ هم‌کار، فقط بدهیِ تکراری می‌ساخت.

جدول ویژگی‌ها

DataTable

Prop

Type

DataTableColumn

ویژگینوعتوضیح
idstringکلید یکتای ستون
headerReactNodeعنوان ستون
cell(row, index) => ReactNodeتابع رندر سلول
sortablebooleanقابلیت مرتب‌سازی
align"start" | "center" | "end"تراز ستون
pinned"start" | "end"سنجاق‌کردن ستون (چسبان هنگام اسکرول افقی)
defaultVisiblebooleanبا false تا فعال‌شدن مخفی است
hideablebooleanبا false ستونِ ساختاری است و در منوی نمایانیِ ستون نمی‌آید (پیش‌فرض true)
classNamestringکلاس اضافی

DataTablePagination

Prop

Type

DataTableFacetedFilter

Prop

Type

راهنمای استفاده

بکنید

  • از DataTable برای لیست داده‌ها با بیش از ۵ آیتم که نیاز به مرتب‌سازی یا صفحه‌بندی دارند استفاده کنید - همیشه emptyState سفارشی با پیام فارسی مناسب ارائه دهید - برای جداول با انتخاب ردیف، getRowKey یکتا تعریف کنید

نکنید

  • برای نمایش ساده ۳-۵ آیتم بدون تعامل از DataTable استفاده نکنید — از Table استفاده کنید - ستون‌های غیرضروری اضافه نکنید — اطلاعات باید قابل اسکن باشند - برای نمایش کارتی از جدول استفاده نکنید — از Card در grid layout استفاده کنید

دسترسی‌پذیری

  • ستون‌های قابل مرتب‌سازی از aria-sort استفاده می‌کنند
  • چک‌باکس‌ها دارای aria-label فارسی هستند
  • حالت بارگذاری از اسکلتون بصری استفاده می‌کند
  • ساختار جدول از تگ‌های معنایی HTML (table, thead, tbody, th, td) پیروی می‌کند

تعامل با کیبورد

  • Tab: حرکت بین عناصر تعاملی (چک‌باکس‌ها، دکمه‌های مرتب‌سازی، صفحه‌بندی) - Space: انتخاب ردیف (چک‌باکس) - Enter: فعال‌سازی مرتب‌سازی ستون

v2 features (نمایان‌سازی ستون، expansion، pinning، export)

'use client'
import {
  DataTable,
  DataTableColumnVisibilityToggle,
  DataTableExportButton,
} from '@partodata/ui'

const [visibility, setVisibility] = useState({})
const [expandedRows, setExpandedRows] = useState(new Set<number>())

<div className="flex items-center justify-end gap-2">
  <DataTableColumnVisibilityToggle
    columns={columns}
    visibility={{ visible: visibility, onVisibilityChange: setVisibility }}
  />
  <DataTableExportButton columns={columns} data={rows} filename="pages.csv" />
</div>

<DataTable
  columns={[
    { id: 'name', header: 'نام', cell: (r) => r.name, pinned: 'start', width: 200 },
    { id: 'engagement', header: 'تعامل', cell: (r) => r.engagement },
    { id: 'notes', header: 'یادداشت', cell: (r) => r.notes, defaultVisible: false },
  ]}
  data={rows}
  columnVisibility={{ visible: visibility, onVisibilityChange: setVisibility }}
  expansion={{
    expandedRows,
    onExpandedRowsChange: setExpandedRows,
    renderExpandedRow: (r) => <div className="p-4 text-sm">{r.detailsHtml}</div>,
  }}
/>
  • columnVisibility (4.2) — show/hide columns via a checkbox dropdown. defaultVisible: false on a column hides it until toggled on.
  • expansion (4.5) — adds a chevron column; clicking expands a full-width detail row below.
  • pinned: 'start' | 'end' (4.6) — sticky columns که در زمان horizontal scroll قابل دیدن می‌مانند. RTL-aware via CSS logical inset.
  • width: number — عرض ثابت پیکسل برای layout header (همراه با pinning مفید است).
  • <DataTableExportButton> (4.9) — dropdown با CSV download + TSV-clipboard. exportValue در ستون برای cellهای React-node serialize را override می‌کند.

کامپوننت‌های مرتبط

  • Table — اگر داده‌های شما ساده هستند و نیاز به مرتب‌سازی یا صفحه‌بندی ندارند، از Table استفاده کنید
  • Card — اگر نمایش کارتی مناسب‌تر از جدول است، از Card در grid layout استفاده کنید
  • DataTableCells — renderer های مخصوص cell جدول (sparkline، trend، status، …)