HiTable Data Grid
HiTable Data Grid Component
Section titled “HiTable Data Grid Component”A flexible, feature-rich data table component built with Vue 3, TypeScript, and TanStack Table. The component provides a clean API with extensive customization options while maintaining excellent performance for enterprise applications.
✨ Key Features
Section titled “✨ Key Features”🔄 Sorting & Filtering
Single and multi-column sorting with global search and column-specific filters
📄 Pagination
Configurable page sizes, navigation controls, and page information display
✅ Row Selection
Single and multi-row selection with select all functionality and bulk actions
👁️ Column Visibility
Dynamic show/hide columns with user preference persistence
📊 Export Integration
Seamless integration with HiExport for CSV exports with multiple options
🎨 Customizable UI
Responsive design, dark mode support, loading states, and custom styling
🚀 Basic Usage
Section titled “🚀 Basic Usage”<template> <HiTable :data="users" :columns="columns" /></template>
<script setup lang="ts">import { HiTable } from '@/components/HiTable'import type { ColumnDef } from '@tanstack/vue-table'
interface User { id: string name: string email: string role: string}
const users = ref<User[]>([])
const columns: ColumnDef<User>[] = [ { accessorKey: 'name', header: 'Name', }, { accessorKey: 'email', header: 'Email', }, { accessorKey: 'role', header: 'Role', },]</script>
<template> <HiTable :data="products" :columns="columns" :config="tableConfig" :loading="loading" /></template>
<script setup lang="ts">import type { TableConfig } from '@/components/HiTable/types'
const tableConfig: TableConfig = { pagination: { pageSize: 20, showPageInfo: true, }, sorting: { enabled: true, multiSort: true, }, filtering: { searchPlaceholder: 'Search products...', searchColumn: 'name', }, rowSelection: { enabled: true, }, styling: { compact: true, headerClass: 'font-semibold', }, emptyState: { message: 'No products found. Try adjusting your search.', },}</script>
⚙️ Configuration Options
Section titled “⚙️ Configuration Options”TableConfig Interface
Section titled “TableConfig Interface”The main configuration object for customizing table behavior:
Property | Type | Default | Description |
---|---|---|---|
pagination.enabled | boolean | true | Enable/disable pagination |
pagination.pageSize | number | 10 | Initial page size |
pagination.showPageInfo | boolean | true | Show “X-Y of Z” info |
sorting.enabled | boolean | true | Enable/disable sorting |
sorting.multiSort | boolean | false | Allow multi-column sorting |
filtering.enabled | boolean | true | Enable/disable filtering |
filtering.searchable | boolean | true | Show global search input |
rowSelection.enabled | boolean | false | Enable row selection |
columnVisibility.enabled | boolean | true | Enable column visibility toggle |
expandable.enabled | boolean | false | Enable row expansion |
🎯 Advanced Features
Section titled “🎯 Advanced Features”Custom Column Definitions
Section titled “Custom Column Definitions”<script setup lang="ts">import { Checkbox } from '@/components/ui/checkbox'
const columns: ColumnDef<User>[] = [ // Selection column { id: 'select', header: ({ table }) => h(Checkbox, { checked: table.getIsAllPageRowsSelected(), 'onUpdate:checked': (value) => table.toggleAllPageRowsSelected(!!value), }), cell: ({ row }) => h(Checkbox, { checked: row.getIsSelected(), 'onUpdate:checked': (value) => row.toggleSelected(!!value), }), enableSorting: false, enableHiding: false, }, // ... other columns]</script>
<script setup lang="ts">import { Badge } from '@/components/ui/badge'import { Button } from '@/components/ui/button'
const columns: ColumnDef<Product>[] = [ { accessorKey: 'name', header: 'Product Name', meta: { displayName: 'Product' }, }, { accessorKey: 'status', header: 'Status', cell: ({ row }) => { const status = row.getValue('status') as string return h(Badge, { variant: status === 'active' ? 'default' : 'secondary' }, () => status) }, }, { id: 'actions', header: 'Actions', cell: ({ row }) => h(Button, { variant: 'ghost', size: 'sm', onClick: () => editProduct(row.original.id) }, () => 'Edit'), enableSorting: false, },]</script>
Custom Slots
Section titled “Custom Slots”The HiTable component provides several slots for customization:
<template> <HiTable :data="orders" :columns="columns" :config="config" > <!-- Custom toolbar actions --> <template #toolbar-actions="{table}"> <Button @click="exportData" variant="outline" size="sm" > Export CSV </Button> <Button @click="refreshData" variant="outline" size="sm" > Refresh </Button> </template>
<!-- Custom toolbar filters --> <template #toolbar-filters="{table}"> <Select @update:model-value="filterByStatus"> <SelectTrigger class="w-40"> <SelectValue placeholder="Filter by status" /> </SelectTrigger> <SelectContent> <SelectItem value="all">All Status</SelectItem> <SelectItem value="pending">Pending</SelectItem> <SelectItem value="completed">Completed</SelectItem> </SelectContent> </Select> </template> </HiTable></template>
<template> <HiTable :data="users" :columns="columns"> <!-- Custom expanded row content --> <template #expanded-row="{row, data}"> <div class="p-4 bg-gray-50 dark:bg-gray-900"> <h4 class="font-semibold mb-2">User Details</h4> <div class="grid grid-cols-2 gap-4"> <div> <p><strong>Phone:</strong> {{ data.phone }}</p> <p><strong>Department:</strong> {{ data.department }}</p> </div> <div> <p><strong>Last Login:</strong> {{ formatDate(data.lastLogin) }}</p> <p><strong>Created:</strong> {{ formatDate(data.createdAt) }}</p> </div> </div> </div> </template>
<!-- Custom empty state --> <template #empty-state> <div class="text-center py-8"> <img src="/empty-users.svg" alt="No users" class="mx-auto mb-4" /> <h3 class="text-lg font-semibold">No users found</h3> <p class="text-muted-foreground">Start by creating your first user</p> <Button @click="createUser" class="mt-4"> Create User </Button> </div> </template> </HiTable></template>
📤 Export Integration with HiExport
Section titled “📤 Export Integration with HiExport”HiTable seamlessly integrates with the HiExport component for comprehensive data export functionality.
<template> <HiTable :data="users" :columns="columns" :config="tableConfig" > <template #toolbar-actions="{table}"> <HiExport :table="table" :original-data="users" :columns="exportColumns" filename="users-export" button-text="Export Data" /> </template> </HiTable></template>
<script setup lang="ts">import { HiExport } from '@/components/HiExport'import type { ExportColumn } from '@/components/HiExport/types'
const exportColumns: ExportColumn[] = [ { key: 'id', header: 'User ID' }, { key: 'name', header: 'Full Name' }, { key: 'email', header: 'Email Address' }, { key: 'role', header: 'User Role', accessor: (user) => user.role.toUpperCase() },]</script>
<template> <HiTable :data="products" :columns="columns"> <template #toolbar-actions="{table}"> <HiExport :table="table" :original-data="products" :columns="exportColumns" :config="exportConfig" filename="products-export" button-text="Export" button-variant="outline" button-size="sm" /> </template> </HiTable></template>
<script setup lang="ts">import type { ExportConfig } from '@/components/HiExport/types'
const exportConfig: ExportConfig = { includeHeaders: true, delimiter: ',', encoding: 'utf-8', dateFormat: 'YYYY-MM-DD', transform: (data) => { return data.map(item => ({ ...item, price: `$${item.price.toFixed(2)}`, category: item.category.name })) }}
const exportColumns: ExportColumn[] = [ { key: 'sku', header: 'Product SKU' }, { key: 'name', header: 'Product Name' }, { key: 'price', header: 'Price (USD)' }, { key: 'category', header: 'Category', accessor: (product) => product.category.name },]</script>
Export Types Available
Section titled “Export Types Available”Export All - Exports all original data regardless of filters or pagination
Export Selected - Exports only selected rows (requires row selection)
Export Current Page - Exports data visible on current page
Export Filtered - Exports all data matching current filters
🏗️ Component Architecture & Development Practices
Section titled “🏗️ Component Architecture & Development Practices”Internal Architecture Analysis
Section titled “Internal Architecture Analysis”The HiTable component follows a modular architecture pattern that promotes maintainability and extensibility:
The component is built using a composition-based architecture:
src/components/HiTable/├── index.vue # Main component entry point├── composables/│ └── useTable.ts # Core table logic & state management├── components/│ ├── TableContent.vue # Table rendering & virtualization│ ├── TableToolbar.vue # Search, filters, column visibility│ └── TablePagination.vue # Pagination controls└── types/ └── index.ts # TypeScript type definitions
Key Design Principles:
- Separation of Concerns: Each sub-component handles specific functionality
- Composable Logic: Business logic isolated in reusable composables
- Type Safety: Comprehensive TypeScript interfaces for all configurations
- Slot-based Extensibility: Multiple slots for custom content injection
// useTable.ts - Core state management patternexport function useTableInstance<T>( props: HiTableProps<T>, config: TableConfig = {}) { // Reactive state for table features const sorting = ref<SortingState>([]) const columnFilters = ref<ColumnFiltersState>([]) const columnVisibility = ref<VisibilityState>({}) const rowSelection = ref({}) const expanded = ref<ExpandedState>({})
// Configuration merging with sensible defaults const mergedConfig = computed(() => ({ ...defaultConfig, ...config, // Deep merge for nested options pagination: { ...defaultConfig.pagination, ...config.pagination }, // ... other feature configs }))
// TanStack Table instance with all features const table = useVueTable({ get data() { return props.data }, get columns() { return props.columns }, getCoreRowModel: getCoreRowModel(), getPaginationRowModel: getPaginationRowModel(), getSortedRowModel: getSortedRowModel(), getFilteredRowModel: getFilteredRowModel(), getExpandedRowModel: getExpandedRowModel(), // State binding onSortingChange: updaterOrValue => { sorting.value = valueUpdater(updaterOrValue, sorting.value) }, // ... other state handlers })
return { table, config: mergedConfig }}
Custom Extension Patterns
Section titled “Custom Extension Patterns”You can create reusable column definitions for common patterns. Here’s an example approach:
// Example: Creating reusable column patternsimport { h, type ColumnDef } from '@tanstack/vue-table'import { Button } from '@/components/ui/button'import { Badge } from '@/components/ui/badge'import { Checkbox } from '@/components/ui/checkbox'
// Example: Selection column patternconst selectionColumn: ColumnDef<User> = { id: 'select', header: ({ table }) => h(Checkbox, { checked: table.getIsAllPageRowsSelected(), 'onUpdate:checked': (value) => table.toggleAllPageRowsSelected(!!value), ariaLabel: 'Select all rows' }), cell: ({ row }) => h(Checkbox, { checked: row.getIsSelected(), 'onUpdate:checked': (value) => row.toggleSelected(!!value), ariaLabel: `Select row ${row.index + 1}` }), enableSorting: false, enableHiding: false,}
// Example: Status badge column patternconst statusColumn: ColumnDef<User> = { accessorKey: 'status', header: 'Status', cell: ({ getValue }) => { const status = getValue() as string const variants: Record<string, string> = { active: 'default', inactive: 'secondary', pending: 'outline' } return h(Badge, { variant: variants[status] || 'secondary' }, () => status) },}
// Example: Actions column patternconst actionsColumn: ColumnDef<User> = { id: 'actions', header: 'Actions', cell: ({ row }) => { return h('div', { class: 'flex gap-2' }, [ h(Button, { variant: 'ghost', size: 'sm', onClick: () => console.log('Edit', row.original) }, () => 'Edit'), h(Button, { variant: 'ghost', size: 'sm', onClick: () => console.log('Delete', row.original) }, () => 'Delete') ]) }, enableSorting: false,}
API Integration with Loading States:
<template> <HiTable :data="users" :columns="columns" :config="tableConfig" :loading="isLoading" > <template #toolbar-actions="{ table }"> <Button @click="refetchData" :disabled="isLoading"> <RefreshCw :class="{ 'animate-spin': isLoading }" class="h-4 w-4 mr-2" /> {{ isLoading ? 'Refreshing...' : 'Refresh' }} </Button> </template> </HiTable></template>
<script setup lang="ts">// Reactive data fetching patternconst users = ref<User[]>([])const isLoading = ref(false)const error = ref<string | null>(null)
// API integration composableconst { data, loading, error: apiError, refetch } = useAsyncData( 'users', () => fetchUsers(), { transform: (data: APIResponse<User[]>) => data.items, onError: (err) => { console.error('Failed to fetch users:', err) toast.error('Failed to load user data') } })
// Sync reactive statewatchEffect(() => { users.value = data.value || [] isLoading.value = loading.value error.value = apiError.value?.message || null})
// Manual refresh with optimistic updatesconst refetchData = async () => { try { await refetch() toast.success('Data refreshed successfully') } catch (err) { toast.error('Failed to refresh data') }}
// Server-side pagination patternconst handlePageChange = async (page: number, pageSize: number) => { try { const response = await fetchUsers({ page, limit: pageSize }) users.value = response.items // Update pagination state } catch (err) { toast.error('Failed to load page') }}</script>
Memory Management and Performance:
<script setup lang="ts">// ✅ Good: Memoize column definitionsconst columns = shallowRef<ColumnDef<User>[]>([ { accessorKey: 'name', header: 'Name', // Memoize expensive cell renderers cell: ({ getValue }) => { const name = getValue() as string return h('span', { class: 'font-medium' }, name) } }, { accessorKey: 'email', header: 'Email', // Use stable references for better performance meta: { className: 'text-muted-foreground' } }])
// ✅ Good: Debounce search inputconst searchTerm = ref('')const debouncedSearch = useDebounceFn((value: string) => { // Apply search filter to table tableRef.value?.table.setGlobalFilter(value)}, 300)
// Watch search term changeswatch(searchTerm, (newValue) => { debouncedSearch(newValue)})
// ✅ Good: Cleanup watchers and subscriptionsonUnmounted(() => { // Clean up any subscriptions or timers debouncedSearch.cancel()})
// ❌ Avoid: Creating columns in computed (causes re-renders)// const columns = computed(() => [...]) // Don't do this
// ❌ Avoid: Heavy operations in cell renderers// cell: ({ row }) => {// // Heavy computation on every render// const expensiveData = heavyCalculation(row.original)// return h('span', expensiveData)// }
// ✅ Better: Pre-compute data or use computed propertiesconst processedData = computed(() => rawData.value.map(item => ({ ...item, computedField: heavyCalculation(item) // Computed once })))</script>
Error Handling and Resilience
Section titled “Error Handling and Resilience”// types/tableData.ts - Data validation schemasimport { z } from 'zod'
// Define data schemas for type safetyconst UserSchema = z.object({ id: z.string().min(1), name: z.string().min(2), email: z.string().email(), role: z.enum(['admin', 'user', 'moderator']), status: z.enum(['active', 'inactive', 'suspended']), createdAt: z.string().datetime(),})
type User = z.infer<typeof UserSchema>
// Data validation utilityexport function validateTableData<T>(data: unknown[], schema: z.ZodSchema<T>): T[] { const validatedData: T[] = [] const errors: string[] = []
data.forEach((item, index) => { try { const validItem = schema.parse(item) validatedData.push(validItem) } catch (error) { if (error instanceof z.ZodError) { errors.push(`Row ${index + 1}: ${error.errors.map(e => e.message).join(', ')}`) } } })
if (errors.length > 0) { console.warn('Data validation errors:', errors) // Optionally show user notification toast.warning(`${errors.length} rows have validation errors`) }
return validatedData}
// Usage in componentconst safeUserData = computed(() => validateTableData(rawUserData.value, UserSchema))
<template> <div> <!-- Error state handling --> <div v-if="error" class="rounded-lg border border-red-200 bg-red-50 p-4"> <div class="flex"> <AlertCircle class="h-5 w-5 text-red-400" /> <div class="ml-3"> <h3 class="text-sm font-medium text-red-800">Data Loading Error</h3> <p class="mt-1 text-sm text-red-700">{{ error }}</p> <div class="mt-4"> <Button @click="retryLoad" variant="outline" size="sm"> Try Again </Button> </div> </div> </div> </div>
<!-- Table with fallback data --> <HiTable v-else :data="tableData" :columns="columns" :config="tableConfig" :loading="isLoading" > <!-- Custom empty state for different scenarios --> <template #empty-state> <div class="text-center py-12"> <template v-if="isFiltered"> <SearchX class="mx-auto h-12 w-12 text-muted-foreground" /> <h3 class="mt-4 text-lg font-semibold">No results found</h3> <p class="mt-2 text-muted-foreground"> No data matches your current filters. </p> <Button @click="clearFilters" class="mt-4" variant="outline"> Clear Filters </Button> </template> <template v-else> <Database class="mx-auto h-12 w-12 text-muted-foreground" /> <h3 class="mt-4 text-lg font-semibold">No data available</h3> <p class="mt-2 text-muted-foreground"> Get started by adding your first entry. </p> <Button @click="createNew" class="mt-4"> Add New Item </Button> </template> </div> </template> </HiTable> </div></template>
<script setup lang="ts">import { AlertCircle, SearchX, Database } from 'lucide-vue-next'
const error = ref<string | null>(null)const isLoading = ref(false)const isFiltered = computed(() => { // Check if any filters are applied return tableRef.value?.table.getState().columnFilters.length > 0 || tableRef.value?.table.getState().globalFilter})
const retryLoad = async () => { error.value = null await loadData()}
const clearFilters = () => { tableRef.value?.table.resetColumnFilters() tableRef.value?.table.setGlobalFilter('')}
const createNew = () => { // Navigate to create form or open modal router.push('/users/create')}
// Graceful error handlingconst loadData = async () => { try { isLoading.value = true error.value = null const data = await fetchUsers() users.value = data } catch (err) { error.value = err instanceof Error ? err.message : 'An unexpected error occurred' console.error('Failed to load table data:', err) } finally { isLoading.value = false }}</script>
🔧 Advanced Programmatic Control
Section titled “🔧 Advanced Programmatic Control”Access table methods through a template ref for programmatic control:
<template> <div> <div class="mb-4 space-x-2"> <Button @click="selectAll">Select All</Button> <Button @click="clearSelection">Clear Selection</Button> <Button @click="resetFilters">Reset Filters</Button> </div>
<HiTable ref="tableRef" :data="data" :columns="columns" :config="config" /> </div></template>
<script setup lang="ts">const tableRef = ref()
const selectAll = () => { tableRef.value?.table.toggleAllRowsSelected(true)}
const clearSelection = () => { tableRef.value?.table.resetRowSelection()}
const resetFilters = () => { tableRef.value?.table.resetColumnFilters()}
// Advanced state managementconst tableState = computed(() => { if (!tableRef.value?.table) return null
const table = tableRef.value.table return { selectedCount: table.getSelectedRowModel().rows.length, totalRows: table.getRowModel().rows.length, currentPage: table.getState().pagination.pageIndex + 1, pageSize: table.getState().pagination.pageSize, totalPages: table.getPageCount(), hasFilters: table.getState().columnFilters.length > 0, sorting: table.getState().sorting, isAllSelected: table.getIsAllPageRowsSelected(), }})
// Bulk operations with selected rowsconst performBulkAction = async (action: 'delete' | 'export' | 'update', data?: any) => { const selectedRows = tableRef.value?.table.getSelectedRowModel().rows if (!selectedRows?.length) { toast.warning('Please select rows to perform this action') return }
const selectedData = selectedRows.map(row => row.original)
try { switch (action) { case 'delete': await bulkDeleteUsers(selectedData.map(user => user.id)) toast.success(`Deleted ${selectedData.length} users`) break case 'export': await exportUsersToCSV(selectedData) toast.success('Export completed') break case 'update': await bulkUpdateUsers(selectedData.map(user => ({ ...user, ...data }))) toast.success(`Updated ${selectedData.length} users`) break }
// Clear selection after action tableRef.value?.table.resetRowSelection() // Refresh data await refetchData() } catch (error) { toast.error(`Failed to ${action} selected rows`) console.error(`Bulk ${action} error:`, error) }}
// Advanced filtering and searchconst applyAdvancedFilter = (filters: Record<string, any>) => { const table = tableRef.value?.table if (!table) return
// Clear existing filters table.resetColumnFilters()
// Apply new filters Object.entries(filters).forEach(([columnId, value]) => { if (value !== undefined && value !== null && value !== '') { table.getColumn(columnId)?.setFilterValue(value) } })}
// Export current view dataconst exportCurrentView = () => { const table = tableRef.value?.table if (!table) return
// Get filtered and sorted data const currentData = table.getFilteredRowModel().rows.map(row => row.original)
// Export using HiExport or custom logic exportData(currentData, { filename: `users-filtered-${new Date().toISOString().split('T')[0]}`, includeHeaders: true })}
// Real-time data synchronizationconst syncInterval = ref<NodeJS.Timeout | null>(null)
const startRealtimeSync = (intervalMs = 30000) => { if (syncInterval.value) return
syncInterval.value = setInterval(async () => { try { // Fetch latest data without showing loading state const latestData = await fetchUsers({ silent: true })
// Check if data has changed if (JSON.stringify(latestData) !== JSON.stringify(users.value)) { users.value = latestData toast.info('Data updated automatically') } } catch (error) { console.error('Real-time sync error:', error) } }, intervalMs)}
const stopRealtimeSync = () => { if (syncInterval.value) { clearInterval(syncInterval.value) syncInterval.value = null }}
// Cleanup on unmountonUnmounted(() => { stopRealtimeSync()})</script>
📚 API Reference
Section titled “📚 API Reference”Prop | Type | Default | Description |
---|---|---|---|
data | T[] | - | Array of data objects to display |
columns | ColumnDef<T>[] | - | Column definitions |
config | TableConfig |
| Table configuration options |
toolbarConfig | ToolbarConfig | - | Toolbar-specific configuration |
paginationConfig | PaginationConfig | - | Pagination-specific configuration |
loading | boolean | false | Show loading state |
class | string | - | Additional CSS classes |
Slot | Props | Description |
---|---|---|
toolbar-actions | {table} | Custom actions in the toolbar |
toolbar-filters | {table} | Custom filters in the toolbar |
expanded-row | {row, data} | Custom content for expanded rows |
empty-state | - | Custom empty state content |
Methods (via ref)
Section titled “Methods (via ref)”Method | Description |
---|---|
table.getSelectedRowModel() | Get selected rows |
table.resetRowSelection() | Clear row selection |
table.resetColumnFilters() | Clear all filters |
table.resetSorting() | Clear sorting |
table.setPageSize(size) | Set page size |
table.setPageIndex(index) | Go to specific page |
🎨 Styling & Theming
Section titled “🎨 Styling & Theming”The component uses Tailwind CSS classes and supports extensive customization:
Theme Configuration
Section titled “Theme Configuration”const tableConfig: TableConfig = { styling: { headerClass: 'bg-gray-100 dark:bg-gray-800 font-semibold', cellClass: 'text-sm', rowClass: 'hover:bg-gray-50 dark:hover:bg-gray-800', tableClass: 'border-collapse', containerClass: 'rounded-lg border', compact: false, // Use compact padding }}
CSS Custom Properties
Section titled “CSS Custom Properties”/* Override theme colors */.hi-table { --table-header-bg: theme('colors.gray.100'); --table-row-hover: theme('colors.gray.50'); --table-border: theme('colors.gray.200');}
.dark .hi-table { --table-header-bg: theme('colors.gray.800'); --table-row-hover: theme('colors.gray.800'); --table-border: theme('colors.gray.700');}
🚀 Performance Tips
Section titled “🚀 Performance Tips”Memory Management
Section titled “Memory Management”<script setup lang="ts">// ✅ Good: Memoize columnsconst columns = shallowRef<ColumnDef<User>[]>([ { accessorKey: 'name', header: 'Name' }, { accessorKey: 'email', header: 'Email' },])
// ✅ Good: Debounce searchconst debouncedSearch = useDebounceFn((value: string) => { // Apply search filter}, 300)
// ❌ Avoid: Creating columns in template// const columns = computed(() => [...])</script>
🔗 Integration Examples
Section titled “🔗 Integration Examples”Real-world Implementation
Section titled “Real-world Implementation”<template> <div class="space-y-6"> <div class="flex items-center justify-between"> <div> <h1 class="text-2xl font-semibold">User Management</h1> <p class="text-sm text-muted-foreground"> Manage and export user data </p> </div> </div>
<BaseCard> <div class="p-6"> <HiTable :data="users" :columns="columns" :config="tableConfig" :loading="loading" > <template #toolbar-actions="{table}"> <HiExport :table="table" :original-data="users" :columns="exportColumns" filename="users-export" button-text="Export" /> <Button @click="refreshData" variant="outline" size="sm"> <RefreshCw class="h-4 w-4 mr-2" /> Refresh </Button> </template>
<template #expanded-row="{ data }"> <div class="p-4 bg-gray-50 dark:bg-gray-900 rounded-lg"> <div class="grid grid-cols-2 gap-4"> <div> <p><strong>Phone:</strong> {{ data.phone }}</p> <p><strong>Department:</strong> {{ data.department }}</p> </div> <div> <p><strong>Last Login:</strong> {{ formatDate(data.lastLogin) }}</p> <p><strong>Created:</strong> {{ formatDate(data.createdAt) }}</p> </div> </div> </div> </template> </HiTable> </div> </BaseCard> </div></template>