جدول Table
جدول بتهيئة الأعمدة. يدعم الفرز، وتثبيت الأعمدة، وخطوط الحمار الوحشي، وفتحات الخلايا، إضافة إلى التمرير الافتراضي للبيانات الضخمة (دوال نقية في core تحسب النافذة المرئية، مطوَّرة داخليًا دون أي مكتبة).
الاستخدام الأساسي
<script setup>
const cols = [
{ prop: 'name', label: 'الاسم', width: 140 },
{ prop: 'role', label: 'المسؤولية' },
{ prop: 'city', label: 'المدينة', width: 120 },
]
const rows = [
{ id: 1, name: 'شين يان', role: 'هيكلة', city: 'هانغتشو' },
{ id: 2, name: 'سو وان', role: 'واجهة أمامية', city: 'سوتشو' },
{ id: 3, name: 'لو لي', role: 'تصميم', city: 'بيجينغ' },
]
</script>
<template>
<LhTable :data="rows" :columns="cols" />
</template><script>
export default {
data() {
return {
cols: [
{ prop: 'name', label: 'الاسم', width: 140 },
{ prop: 'role', label: 'المسؤولية' },
{ prop: 'city', label: 'المدينة', width: 120 },
],
rows: [
{ id: 1, name: 'شين يان', role: 'هيكلة', city: 'هانغتشو' },
{ id: 2, name: 'سو وان', role: 'واجهة أمامية', city: 'سوتشو' },
{ id: 3, name: 'لو لي', role: 'تصميم', city: 'بيجينغ' },
],
}
},
}
</script>
<template>
<LhTable :data="rows" :columns="cols" />
</template>الفرز وتثبيت الأعمدة
<script setup>
const sortCols = [
{ prop: 'name', label: 'الاسم', width: 140, fixed: 'left' },
{ prop: 'age', label: 'العمر', width: 100, sortable: true, align: 'center' },
{ prop: 'role', label: 'المسؤولية' },
{ prop: 'city', label: 'المدينة', width: 120 },
]
</script>
<template>
<LhTable :data="rows" :columns="sortCols" stripe border size="small" />
</template><script>
export default {
data() {
return {
sortCols: [
{ prop: 'name', label: 'الاسم', width: 140, fixed: 'left' },
{ prop: 'age', label: 'العمر', width: 100, sortable: true, align: 'center' },
{ prop: 'role', label: 'المسؤولية' },
{ prop: 'city', label: 'المدينة', width: 120 },
],
}
},
}
</script>
<template>
<LhTable :data="rows" :columns="sortCols" stripe border size="small" />
</template>التمرير الافتراضي (بيانات ضخمة)
يُفعَّل بضبط height: لا يُعرض سوى الصفوف داخل النافذة المرئية، ويجب أن يتطابق rowHeight مع ارتفاع الصف الفعلي.
<script setup lang="ts">
const cols = [
{ prop: 'name', label: 'الاسم', width: 140 },
{ prop: 'role', label: 'المسؤولية' },
{ prop: 'city', label: 'المدينة', width: 120 },
]
const bigRows = Array.from({ length: 1000 }, (_, i) => ({
id: i + 1,
name: `الصف ${i + 1}`,
role: i % 2 === 0 ? 'هيكلة' : 'واجهة أمامية',
city: 'هانغتشو',
}))
</script>
<template>
<LhTable :data="bigRows" :columns="cols" :height="240" :row-height="48" />
</template><script>
export default {
data() {
return {
cols: [
{ prop: 'name', label: 'الاسم', width: 140 },
{ prop: 'role', label: 'المسؤولية' },
{ prop: 'city', label: 'المدينة', width: 120 },
],
bigRows: Array.from({ length: 1000 }, (_, i) => ({
id: i + 1,
name: `الصف ${i + 1}`,
role: i % 2 === 0 ? 'هيكلة' : 'واجهة أمامية',
city: 'هانغتشو',
})),
}
},
}
</script>
<template>
<LhTable :data="bigRows" :columns="cols" :height="240" :row-height="48" />
</template>خلية مخصَّصة
<script setup lang="ts">
const rows = [
{ id: 1, name: 'شين يان', age: 34, role: 'هيكلة', city: 'هانغتشو' },
{ id: 2, name: 'سو وان', age: 28, role: 'واجهة أمامية', city: 'سوتشو' },
{ id: 3, name: 'لو لي', age: 31, role: 'تصميم', city: 'بيجينغ' },
]
const slotCols = [
{ prop: 'name', label: 'الاسم', width: 140 },
{ prop: 'role', label: 'المسؤولية', slot: 'role' },
]
</script>
<template>
<!-- تخصيص عمود المسؤولية عبر الفتحة -->
<LhTable :data="rows" :columns="slotCols">
<template #role="{ value }">
<LhTag :type="value === 'هيكلة' ? 'primary' : 'info'">{{ value }}</LhTag>
</template>
</LhTable>
</template><script>
export default {
data() {
return {
rows: [
{ id: 1, name: 'شين يان', age: 34, role: 'هيكلة', city: 'هانغتشو' },
{ id: 2, name: 'سو وان', age: 28, role: 'واجهة أمامية', city: 'سوتشو' },
{ id: 3, name: 'لو لي', age: 31, role: 'تصميم', city: 'بيجينغ' },
],
slotCols: [
{ prop: 'name', label: 'الاسم', width: 140 },
{ prop: 'role', label: 'المسؤولية', slot: 'role' },
],
}
},
}
</script>
<template>
<!-- تخصيص عمود المسؤولية عبر الفتحة -->
<LhTable :data="rows" :columns="slotCols">
<template #role="{ value }">
<LhTag :type="value === 'هيكلة' ? 'primary' : 'info'">{{ value }}</LhTag>
</template>
</LhTable>
</template>رؤوس متعددة المستويات
بتداخل children في تهيئة الأعمدة تحصل على رؤوس متعددة المستويات، بشكل غير محدود؛ ويُحاذا الجدول وفق الأعمدة الورقية.
<script setup>
const nestedCols = [
{ prop: 'name', label: 'الاسم', width: 140, fixed: 'left' },
{
label: 'معلومات المشروع',
children: [
{ prop: 'role', label: 'المسؤولية' },
{ prop: 'city', label: 'المدينة', width: 120 },
],
},
{ prop: 'age', label: 'العمر', sortable: true, align: 'center' },
]
</script>
<template>
<LhTable :data="rows" :columns="nestedCols" border />
</template><script>
export default {
data() {
return {
nestedCols: [
{ prop: 'name', label: 'الاسم', width: 140, fixed: 'left' },
{
label: 'معلومات المشروع',
children: [
{ prop: 'role', label: 'المسؤولية' },
{ prop: 'city', label: 'المدينة', width: 120 },
],
},
{ prop: 'age', label: 'العمر', sortable: true, align: 'center' },
],
}
},
}
</script>
<template>
<LhTable :data="rows" :columns="nestedCols" border />
</template>التصفية
عند ضبط filters في تهيئة العمود يظهر مُطلِّق التصفية في الرأس؛ وبعد تحديد الخيارات يُطبَّق عند النقر على «تأكيد» ويُفرَّغ عند النقر على «إعادة ضبط». يجعل filterMultiple: false الاختيار أحاديًا، ويُخصِّص filterMethod منطق الحكم؛ والاستخدام الموجَّه في المثال أدناه.
التصفية الحالية: {}
<script setup>
import { ref } from 'vue'
const filteredValues = ref({})
const filterCols = [
{ prop: 'name', label: 'الاسم', width: 140 },
{
prop: 'role', label: 'المسؤولية', filters: [
{ text: 'هيكلة', value: 'هيكلة' },
{ text: 'واجهة أمامية', value: 'واجهة أمامية' },
{ text: 'تصميم', value: 'تصميم' },
],
},
]
function onFilterChange({ prop, values }) {
console.log('filterChange', prop, values)
}
</script>
<template>
<!-- تصفية موجَّهة -->
<LhTable
:data="rows"
:columns="filterCols"
v-model:filtered-values="filteredValues"
@filter-change="onFilterChange"
/>
</template><script>
export default {
data() {
return {
filteredValues: {},
filterCols: [
{ prop: 'name', label: 'الاسم', width: 140 },
{
prop: 'role', label: 'المسؤولية', filters: [
{ text: 'هيكلة', value: 'هيكلة' },
{ text: 'واجهة أمامية', value: 'واجهة أمامية' },
{ text: 'تصميم', value: 'تصميم' },
],
},
],
}
},
methods: {
onFilterChange({ prop, values }) {
console.log('filterChange', prop, values)
},
},
}
</script>
<template>
<!-- تصفية موجَّهة -->
<LhTable
:data="rows"
:columns="filterCols"
v-model:filtered-values="filteredValues"
@filter-change="onFilterChange"
/>
</template>سحب عرض العمود
فعِّل resizable (المفتاح الرئيسي) وعلِّم العمود بـ resizable: true (عمود ورقة وعرضه رقمي). يظهر مقبض السحب على حافة الرأس، وبعد التركيز يضيف ←/→ في كل مرة ±10px، بحد أدنى للحجم 40px.
آخر تعديل: (بلا تعديل)
<script setup>
import { ref } from 'vue'
const resizeLog = ref('(بلا تعديل)')
const resizeCols = [
{ prop: 'name', label: 'الاسم', width: 140, resizable: true, fixed: 'left' },
{ prop: 'role', label: 'المسؤولية' },
{ prop: 'city', label: 'المدينة', width: 120, resizable: true },
]
function onColumnResize({ prop, width }) {
resizeLog.value = `${prop} → ${width}px`
}
</script>
<template>
<!-- سحب عرض العمود -->
<LhTable :data="rows" :columns="resizeCols" resizable border @column-resize="onColumnResize" />
</template><script>
export default {
data() {
return {
resizeLog: '(بلا تعديل)',
resizeCols: [
{ prop: 'name', label: 'الاسم', width: 140, resizable: true, fixed: 'left' },
{ prop: 'role', label: 'المسؤولية' },
{ prop: 'city', label: 'المدينة', width: 120, resizable: true },
],
}
},
methods: {
onColumnResize({ prop, width }) {
this.resizeLog = `${prop} → ${width}px`
},
},
}
</script>
<template>
<!-- سحب عرض العمود -->
<LhTable :data="rows" :columns="resizeCols" resizable border @column-resize="onColumnResize" />
</template>مفتاح الصف، والارتفاع الأقصى، والفرز الافتراضي
يُحدِّد row-key مفتاحًا فريدًا للصف (مُوصى به)؛ وبعد تحديد max-height يلتصق الرأس بالأعلى ويتمرر جسم الجدول؛ ويُخصِّص sorter دالة الفرز؛ ويضبط default-sort-prop / default-sort-order الفرز الابتدائي؛ ويضبط default-filtered-values التصفية الابتدائية.
<script setup>
const sorterCols = [
{ prop: 'name', label: 'الاسم', width: 140 },
{ prop: 'age', label: 'العمر', sortable: true, sorter: (a, b) => a.age - b.age },
{ prop: 'role', label: 'المسؤولية' },
]
</script>
<template>
<LhTable
:data="rows"
:columns="sorterCols"
row-key="name"
max-height="200"
default-sort-prop="age"
default-sort-order="desc"
/>
</template><script>
export default {
data() {
return {
sorterCols: [
{ prop: 'name', label: 'الاسم', width: 140 },
{ prop: 'age', label: 'العمر', sortable: true, sorter: (a, b) => a.age - b.age },
{ prop: 'role', label: 'المسؤولية' },
],
}
},
}
</script>
<template>
<LhTable
:data="rows"
:columns="sorterCols"
row-key="name"
max-height="200"
default-sort-prop="age"
default-sort-order="desc"
/>
</template>API
LhTable Props
| اسم الخاصية | الوصف | النوع | الافتراضي |
|---|---|---|---|
| data | بيانات الصفوف | [LhTableRow](#type-LhTableRow)[] | — |
| columns | تهيئة الأعمدة | [LhTableColumn](#type-LhTableColumn)[] | — |
| height | ارتفاع الجدول (يُفعِّل التمرير الافتراضي عند ضبطه؛ والرقم يعني px) | string | number | — |
| maxHeight | أقصى ارتفاع في الوضع غير الافتراضي | string | number | — |
| rowHeight | ارتفاع صف التمرير الافتراضي (يجب أن يتطابق مع ارتفاع الصف الفعلي) | number | [LH_TABLE_ROW_HEIGHT](#const-LH_TABLE_ROW_HEIGHT) |
| stripe | خطوط الحمار الوحشي | boolean | false |
| border | حدود الخلايا | boolean | false |
| size | الحجم | "small" | "default" | "large" | 'default' |
| rowKey | اسم حقل مفتاح الصف | string | 'id' |
| defaultSortProp | عمود الفرز الافتراضي | string | — |
| defaultSortOrder | اتجاه الفرز الافتراضي | "asc" | "desc" | — |
| sorter | دالة فرز مخصَّصة | (a: LhTableRow, b: LhTableRow) => number | — |
| resizable | تفعيل سحب عرض العمود (يتطلَّب أيضًا resizable على مستوى العمود) | boolean | false |
| defaultFilteredValues | حالة التصفية الافتراضية (غير موجَّهة، المفاتيح هي prop الأعمدة) | { [x: string]: (string | number)[]; } | () => ({}) |
| filteredValues | حالة التصفية الموجَّهة (المفاتيح هي prop الأعمدة) | { [x: string]: (string | number)[]; } | — |
| sx | خصائص التخطيط السريع sx | LhSxProps | — |
Events
| اسم الحدث | الوصف | وسائط الاستدعاء |
|---|---|---|
| sortChange | يُطلق عند تغيّر الفرز (عند تغيير العمود يبدأ من asc من جديد، وعلى نفس العمود يدور asc → desc → إلغاء) | (payload: { prop: string, order: 'asc' | 'desc' | undefined }) |
| rowClick | يُطلق عند النقر على أحد الصفوف | (row: LhTableRow, index: number) |
| filterChange | يُطلق بعد إرسال «تأكيد» أو «إعادة ضبط» التصفية | (payload: { prop: string, values: (string | number)[] }) |
| update:filteredValues | يُطلق عند تغيّر حالة التصفية (v-model:filtered-values) | (values: Record<string, (string | number)[]>) |
| columnResize | يُطلق بعد اكتمال ضبط عرض العمود (انتهاء السحب أو ←/→ من لوحة المفاتيح) | (payload: { prop: string, width: number }) |
الفتحات: empty، و<column.slot> (مع البارامترات { row, column, index, value }).
LhTableColumn
| الحقل | الوصف | النوع |
|---|---|---|
| prop | اسم حقل البيانات | string |
| label | عنوان العمود | string |
| width | عرض العمود (الرقم يعني px، ويُقسَّم بالتساوي إذا لم يُضبط) | number | string |
| align | المحاذاة | 'left' | 'center' | 'right' |
| fixed | تثبيت العمود (يتطلَّب width) | 'left' | 'right' |
| sortable | قابل للفرز | boolean |
| slot | اسم فتحة الخلية المخصَّصة | string |
| formatter | تنسيق القيمة | (row, value) => string |
| children | الأعمدة الفرعية (ضبطها يُنشئ رؤوس متعددة المستويات) | LhTableColumn[] |
| filters | خيارات تصفية العمود (فعّالة للأعمدة الورقية فقط) | LhTableFilterOption[] |
| filterMethod | منطق تصفية مخصَّص (الافتراضي مقارنة prop بالمساواة التامة) | (value, row) => boolean |
| filterMultiple | هل لوحة التصفية متعددة الاختيار | boolean |
| resizable | يمكن سحب عرض هذا العمود (يجب أن يكون عمودًا ورقيًا بـ width رقمي) | boolean |
إمكانية الوصول
role="grid"+aria-rowcount/aria-colcount؛ والصفrow، ورأس العمودcolumnheader، والخليةgridcell- يكشف رأس العمود القابل للفرز عن
aria-sort(none/ascending/descending)، ومُطلِّق الفرز<button>حقيقي - الأعمدة المثبَّتة تستخدم
position: stickyفي CSS (دون حسابات التصاق مخصَّصة) - مُطلِّق التصفية
aria-haspopup+aria-expanded، واللوحةrole="group" - مقبض عرض العمود
role="separator"، قابل للتركيز ويُضبَط بـ ←/→ بمقدار ±10px
تعريفات الأنواع
بنى TypeScript التي يصدّرها هذا المكوّن للخارج (مستخرجة تلقائيًا من packages/components/table/src/types.ts و@lh-ui/share/core/*؛ انقر كل عنصر لتوسيع تعريفه الكامل؛ لأوصاف الخصائص انظر جدول Props أعلاه):
LhTableProps
export interface LhTableProps {
/** بيانات الصفوف */
data: LhTableRow[]
/** تهيئة الأعمدة */
columns: LhTableColumn[]
/** ارتفاع الجدول (يُفعِّل التمرير الافتراضي عند ضبطه؛ والرقم يعني px) */
height?: number | string
/** أقصى ارتفاع في الوضع غير الافتراضي */
maxHeight?: number | string
/** ارتفاع صف التمرير الافتراضي (يجب أن يتطابق مع ارتفاع الصف الفعلي) */
rowHeight?: number
/** خطوط الحمار الوحشي */
stripe?: boolean
/** حدود الخلايا */
border?: boolean
/** الحجم */
size?: 'small' | 'default' | 'large'
/** اسم حقل مفتاح الصف */
rowKey?: string
/** عمود الفرز الافتراضي */
defaultSortProp?: string
/** اتجاه الفرز الافتراضي */
defaultSortOrder?: LhTableSortOrder
/** دالة فرز مخصَّصة */
sorter?: (a: LhTableRow, b: LhTableRow) => number
/** تفعيل سحب عرض العمود (يتطلَّب أيضًا resizable على مستوى العمود، ويجب أن يكون width العمود رقميًا) */
resizable?: boolean
/** حالة التصفية الافتراضية (غير موجَّهة، المفاتيح هي prop الأعمدة) */
defaultFilteredValues?: Record<string, (string | number)[]>
/** حالة التصفية الموجَّهة (المفاتيح هي prop الأعمدة) */
filteredValues?: Record<string, (string | number)[]>
/** خصائص التخطيط السريع sx */
sx?: LhSxProps
}LhTableSortOrder (مأخوذ من @lh-ui/share/core/table)
/** اتجاه الفرز */
export type LhTableSortOrder = 'asc' | 'desc'LhTableRow (مأخوذ من @lh-ui/share/core/table)
/** نوع بيانات الصف */
export type LhTableRow = Record<string, any>LhTableColumn (مأخوذ من @lh-ui/share/core/table)
/** تهيئة العمود */
export interface LhTableColumn {
/** اسم حقل البيانات */
prop: string
/** عنوان العمود */
label: string
/** عرض العمود (الرقم يعني px) */
width?: number | string
/** المحاذاة */
align?: LhTableAlign
/** تثبيت العمود (يتطلَّب width) */
fixed?: 'left' | 'right'
/** قابل للفرز */
sortable?: boolean
/** اسم فتحة الخلية المخصَّصة */
slot?: string
/** تنسيق القيمة */
formatter?: (row: LhTableRow, value: any) => string
/** الأعمدة الفرعية (ضبطها يُنشئ رؤوس متعددة المستويات، بشكل غير محدود) */
children?: LhTableColumn[]
/** خيارات تصفية العمود (فعّالة للأعمدة الورقية فقط) */
filters?: LhTableFilterOption[]
/** منطق تصفية مخصَّص (الافتراضي مقارنة prop بالمساواة التامة) */
filterMethod?: (value: string | number, row: LhTableRow) => boolean
/** هل لوحة التصفية متعددة الاختيار (الافتراضي true) */
filterMultiple?: boolean
/** يمكن سحب عرض هذا العمود (يجب أن يكون عمودًا ورقيًا بـ width رقمي) */
resizable?: boolean
}LhTableFilterOption (مأخوذ من @lh-ui/share/core/table)
/** خيار التصفية */
export interface LhTableFilterOption {
/** نص الخيار */
text: string
/** قيمة الخيار (تُستخدم في مقارنة filterMethod) */
value: string | number
}LhTableAlign (مأخوذ من @lh-ui/share/core/table)
/** محاذاة الخلية */
export type LhTableAlign = 'left' | 'center' | 'right'الثوابت
الثوابت العامة التي يستخدمها هذا المكوّن كالتالي (يمكن النقر على الاسم نفسه في عمود «النوع / الافتراضي» في جدول Props للقفز إلى هنا):
LH_TABLE_ROW_HEIGHT (مأخوذ من @lh-ui/share/core/table)
/** ارتفاع الصف الافتراضي للتمرير الافتراضي */
export const LH_TABLE_ROW_HEIGHT = 48LH_TABLE_OVERSCAN (مأخوذ من @lh-ui/share/core/table)
/** عدد الصفوف الإضافية المعروضة أعلى النافذة وأسفلها (احتياطي شاشة بيضاء عند التمرير) */
export const LH_TABLE_OVERSCAN = 3LH_TABLE_MIN_COL_WIDTH (مأخوذ من @lh-ui/share/core/table)
// ============================================================================
// سحب عرض العمود
// ============================================================================
/** الحد الأدنى للسحب (px)، وأضيق من ذلك لا تتسع الخلية للمحتوى */
export const LH_TABLE_MIN_COL_WIDTH = 40