refactor: migrate all tables to DataTable component

- Migrate pages/events/index.vue to use DataTable
- Migrate pages/license/index.vue feature permissions table to DataTable
- Migrate components/user/* tables to DataTable (account, policies, groups)
- Migrate components/user-group/* tables to DataTable (policies, members, set-policies-mutiple)
- Replace native table elements and raw Table components with shared DataTable
- Implement search functionality using DataTable filter capabilities
- Ensure consistent table rendering across the application
This commit is contained in:
overtrue
2025-10-31 21:34:12 +08:00
parent 08aa11e754
commit 8b871ccbca
8 changed files with 347 additions and 284 deletions
+27 -26
View File
@@ -71,38 +71,22 @@
</CardContent>
</Card>
<Table class="overflow-hidden rounded-lg border">
<TableHeader>
<TableRow>
<TableHead>{{ t('Name') }}</TableHead>
</TableRow>
</TableHeader>
<TableBody v-if="filteredMembers.length">
<TableRow v-for="member in filteredMembers" :key="member">
<TableCell class="font-medium">{{ member }}</TableCell>
</TableRow>
</TableBody>
<TableBody v-else>
<TableRow>
<TableCell class="text-center" colspan="1">
<p class="py-6 text-sm text-muted-foreground">{{ t('No Data') }}</p>
</TableCell>
</TableRow>
</TableBody>
</Table>
<DataTable :table="table" />
</div>
</template>
<script setup lang="ts">
import { Icon } from '#components'
import DataTable from '@/components/data-table/data-table.vue'
import { useDataTable } from '@/components/data-table/useDataTable'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card, CardContent } from '@/components/ui/card'
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command'
import { Field, FieldContent, FieldLabel } from '@/components/ui/field'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { computed, onMounted, ref, watch } from 'vue'
import type { ColumnDef } from '@tanstack/vue-table'
import { computed, h, onMounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
interface UserOption {
@@ -141,11 +125,28 @@ const userLabelMap = computed(() => {
const selectedUserLabels = computed(() => members.value.map(value => userLabelMap.value[value] ?? value))
const filteredMembers = computed(() => {
const keyword = searchTerm.value.trim().toLowerCase()
const source = props.group?.members ?? []
if (!keyword) return source
return source.filter(item => item.toLowerCase().includes(keyword))
interface MemberItem {
name: string
}
const membersData = computed<MemberItem[]>(() => (props.group?.members ?? []).map(name => ({ name })))
const columns: ColumnDef<MemberItem>[] = [
{
id: 'name',
header: () => t('Name'),
cell: ({ row }) => h('span', { class: 'font-medium' }, row.original.name),
},
]
const { table } = useDataTable<MemberItem>({
data: membersData,
columns,
getRowId: row => row.name,
})
watch(searchTerm, value => {
table.getColumn('name')?.setFilterValue(value || undefined)
})
watch(
+27 -26
View File
@@ -67,37 +67,21 @@
</div>
</div>
<Table class="overflow-hidden rounded-lg border">
<TableHeader>
<TableRow>
<TableHead>{{ t('Name') }}</TableHead>
</TableRow>
</TableHeader>
<TableBody v-if="filteredPolicies.length">
<TableRow v-for="policy in filteredPolicies" :key="policy">
<TableCell class="font-medium">{{ policy }}</TableCell>
</TableRow>
</TableBody>
<TableBody v-else>
<TableRow>
<TableCell class="text-center" colspan="1">
<p class="py-6 text-sm text-muted-foreground">{{ t('No Data') }}</p>
</TableCell>
</TableRow>
</TableBody>
</Table>
<DataTable :table="table" />
</div>
</template>
<script setup lang="ts">
import { Icon } from '#components'
import DataTable from '@/components/data-table/data-table.vue'
import { useDataTable } from '@/components/data-table/useDataTable'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command'
import { Field, FieldContent, FieldLabel } from '@/components/ui/field'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { computed, onMounted, ref, watch } from 'vue'
import type { ColumnDef } from '@tanstack/vue-table'
import { computed, h, onMounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
const { listPolicies, setUserOrGroupPolicy } = usePolicies()
@@ -124,11 +108,28 @@ const currentPolicies = computed(() => {
return props.group.policy.split(',').map(item => item.trim()).filter(Boolean)
})
const filteredPolicies = computed(() => {
const keyword = searchTerm.value.trim().toLowerCase()
const source = currentPolicies.value
if (!keyword) return source
return source.filter(policy => policy.toLowerCase().includes(keyword))
interface PolicyItem {
name: string
}
const policiesData = computed<PolicyItem[]>(() => currentPolicies.value.map(name => ({ name })))
const columns: ColumnDef<PolicyItem>[] = [
{
id: 'name',
header: () => t('Name'),
cell: ({ row }) => h('span', { class: 'font-medium' }, row.original.name),
},
]
const { table } = useDataTable<PolicyItem>({
data: policiesData,
columns,
getRowId: row => row.name,
})
watch(searchTerm, value => {
table.getColumn('name')?.setFilterValue(value || undefined)
})
const policyLabelMap = computed(() => {
+43 -29
View File
@@ -10,42 +10,18 @@
</Button>
</div>
<Table class="overflow-hidden rounded-lg border">
<TableHeader>
<TableRow>
<TableHead class="w-12">
<Checkbox :checked="allVisibleSelected" :indeterminate="checkedKeys.length > 0 && !allVisibleSelected"
@update:checked="(value: boolean | 'indeterminate') => toggleSelectAll(value === true)" />
</TableHead>
<TableHead>{{ t('Name') }}</TableHead>
</TableRow>
</TableHeader>
<TableBody v-if="filteredPolicies.length">
<TableRow v-for="policy in filteredPolicies" :key="policy.name">
<TableCell>
<Checkbox :checked="isSelected(policy.name)" @update:checked="(value: boolean | 'indeterminate') => toggleSelection(policy.name, value === true)" />
</TableCell>
<TableCell class="font-medium">{{ policy.name }}</TableCell>
</TableRow>
</TableBody>
<TableBody v-else>
<TableRow>
<TableCell class="text-center" colspan="2">
<p class="py-6 text-sm text-muted-foreground">{{ t('No Data') }}</p>
</TableCell>
</TableRow>
</TableBody>
</Table>
<DataTable :table="table" />
</div>
</Modal>
</template>
<script setup lang="ts">
import DataTable from '@/components/data-table/data-table.vue'
import { useDataTable } from '@/components/data-table/useDataTable'
import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { computed, ref } from 'vue'
import type { ColumnDef } from '@tanstack/vue-table'
import { computed, h, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import Modal from '~/components/modal.vue'
@@ -102,6 +78,44 @@ const toggleSelectAll = (checked: boolean) => {
}
}
const columns: ColumnDef<{ name: string; content: unknown }>[] = [
{
id: 'select',
header: () =>
h(Checkbox, {
checked: allVisibleSelected.value,
indeterminate: checkedKeys.value.length > 0 && !allVisibleSelected.value,
onUpdateChecked: (value: boolean | 'indeterminate') => toggleSelectAll(value === true),
}),
enableSorting: false,
cell: ({ row }) =>
h(Checkbox, {
checked: isSelected(row.original.name),
onUpdateChecked: (value: boolean | 'indeterminate') => toggleSelection(row.original.name, value === true),
}),
meta: { maxWidth: '3rem' },
},
{
id: 'name',
header: () => t('Name'),
cell: ({ row }) => h('span', { class: 'font-medium' }, row.original.name),
filterFn: (row, _columnId, filterValue) => {
if (!filterValue) return true
return row.original.name.toLowerCase().includes(String(filterValue).toLowerCase())
},
},
]
const { table } = useDataTable<{ name: string; content: unknown }>({
data: filteredPolicies,
columns,
getRowId: row => row.name,
})
watch(searchTerm, value => {
table.getColumn('name')?.setFilterValue(value || undefined)
})
const loadPolicies = async () => {
try {
const res = await listPolicies()
+77 -51
View File
@@ -12,51 +12,7 @@
</Button>
</div>
<Table class="overflow-hidden rounded-lg border">
<TableHeader>
<TableRow>
<TableHead class="text-center">{{ t('Access Key') }}</TableHead>
<TableHead class="text-center">{{ t('Expiration') }}</TableHead>
<TableHead class="text-center">{{ t('Status') }}</TableHead>
<TableHead class="text-center">{{ t('Name') }}</TableHead>
<TableHead class="w-32 text-center">{{ t('Actions') }}</TableHead>
</TableRow>
</TableHeader>
<TableBody v-if="filteredAccounts.length">
<TableRow v-for="account in filteredAccounts" :key="account.accessKey">
<TableCell class="text-center font-medium">{{ account.accessKey }}</TableCell>
<TableCell class="text-center">{{ formatExpiration(account.expiration) }}</TableCell>
<TableCell class="text-center">
<span class="text-sm">
{{ account.accountStatus === 'on' ? t('Available') : t('Disabled') }}
</span>
</TableCell>
<TableCell class="text-center">{{ account.name || '-' }}</TableCell>
<TableCell>
<div class="flex justify-center gap-2">
<Button variant="outline" size="sm" class="h-8 w-8 p-0" @click="openEditItem(account)">
<Icon class="size-4" name="ri:edit-2-line" />
</Button>
<Button
variant="outline"
size="sm"
class="h-8 w-8 p-0 text-destructive border-destructive"
@click="confirmDelete(account)"
>
<Icon class="size-4" name="ri:delete-bin-5-line" />
</Button>
</div>
</TableCell>
</TableRow>
</TableBody>
<TableBody v-else>
<TableRow>
<TableCell class="text-center" colspan="5">
<p class="py-6 text-sm text-muted-foreground">{{ t('No Data') }}</p>
</TableCell>
</TableRow>
</TableBody>
</Table>
<DataTable :table="table" />
</div>
</template>
@@ -134,13 +90,15 @@ import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Icon } from '#components'
import DataTable from '@/components/data-table/data-table.vue'
import { useDataTable } from '@/components/data-table/useDataTable'
import DateTimePicker from '@/components/datetime-picker.vue'
import { Field, FieldContent, FieldLabel } from '@/components/ui/field'
import { Switch } from '@/components/ui/switch'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { Textarea } from '@/components/ui/textarea'
import type { ColumnDef } from '@tanstack/vue-table'
import dayjs from 'dayjs'
import { computed, reactive, ref, watch } from 'vue'
import { computed, h, reactive, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useDialog, useMessage } from '~/composables/ui'
import { makeRandomString } from '~/utils/functions'
@@ -183,10 +141,78 @@ const parentPolicy = ref('')
const minExpiry = computed(() => new Date().toISOString())
const filteredAccounts = computed(() => {
const keyword = searchTerm.value.trim().toLowerCase()
if (!keyword) return listData.value
return listData.value.filter((item: any) => item.accessKey.toLowerCase().includes(keyword))
interface AccountItem {
accessKey: string
expiration?: string
accountStatus?: string
name?: string
[key: string]: unknown
}
const columns: ColumnDef<AccountItem>[] = [
{
id: 'accessKey',
header: () => t('Access Key'),
cell: ({ row }) => h('span', { class: 'text-center font-medium' }, row.original.accessKey),
},
{
id: 'expiration',
header: () => t('Expiration'),
cell: ({ row }) => h('span', { class: 'text-center' }, formatExpiration(row.original.expiration)),
},
{
id: 'status',
header: () => t('Status'),
cell: ({ row }) =>
h(
'span',
{ class: 'text-sm text-center' },
row.original.accountStatus === 'on' ? t('Available') : t('Disabled')
),
},
{
id: 'name',
header: () => t('Name'),
cell: ({ row }) => h('span', { class: 'text-center' }, row.original.name || '-'),
},
{
id: 'actions',
header: () => t('Actions'),
enableSorting: false,
cell: ({ row }) =>
h('div', { class: 'flex justify-center gap-2' }, [
h(
Button,
{
variant: 'outline',
size: 'sm',
class: 'h-8 w-8 p-0',
onClick: () => openEditItem(row.original),
},
() => h(Icon, { class: 'size-4', name: 'ri:edit-2-line' })
),
h(
Button,
{
variant: 'outline',
size: 'sm',
class: 'h-8 w-8 p-0 text-destructive border-destructive',
onClick: () => confirmDelete(row.original),
},
() => h(Icon, { class: 'size-4', name: 'ri:delete-bin-5-line' })
),
]),
},
]
const { table } = useDataTable<AccountItem>({
data: listData as any,
columns,
getRowId: row => row.accessKey,
})
watch(searchTerm, value => {
table.getColumn('accessKey')?.setFilterValue(value || undefined)
})
const formatExpiration = (value?: string) => {
+27 -26
View File
@@ -53,37 +53,21 @@
</div>
</div>
<Table class="overflow-hidden rounded-lg border">
<TableHeader>
<TableRow>
<TableHead>{{ t('Name') }}</TableHead>
</TableRow>
</TableHeader>
<TableBody v-if="filteredGroups.length">
<TableRow v-for="item in filteredGroups" :key="item">
<TableCell class="font-medium">{{ item }}</TableCell>
</TableRow>
</TableBody>
<TableBody v-else>
<TableRow>
<TableCell class="text-center" colspan="1">
<p class="py-6 text-sm text-muted-foreground">{{ t('No Data') }}</p>
</TableCell>
</TableRow>
</TableBody>
</Table>
<DataTable :table="table" />
</div>
</template>
<script setup lang="ts">
import { Icon } from '#components'
import DataTable from '@/components/data-table/data-table.vue'
import { useDataTable } from '@/components/data-table/useDataTable'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command'
import { Field, FieldContent, FieldLabel } from '@/components/ui/field'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { computed, onMounted, ref, watch } from 'vue'
import type { ColumnDef } from '@tanstack/vue-table'
import { computed, h, onMounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
@@ -108,11 +92,28 @@ const submitting = ref(false)
const currentGroups = computed(() => props.user.memberOf ?? [])
const filteredGroups = computed(() => {
const keyword = searchTerm.value.trim().toLowerCase()
const source = currentGroups.value
if (!keyword) return source
return source.filter(item => item.toLowerCase().includes(keyword))
interface GroupItem {
name: string
}
const groupsData = computed<GroupItem[]>(() => currentGroups.value.map(name => ({ name })))
const columns: ColumnDef<GroupItem>[] = [
{
id: 'name',
header: () => t('Name'),
cell: ({ row }) => h('span', { class: 'font-medium' }, row.original.name),
},
]
const { table } = useDataTable<GroupItem>({
data: groupsData,
columns,
getRowId: row => row.name,
})
watch(searchTerm, value => {
table.getColumn('name')?.setFilterValue(value || undefined)
})
const groupLabelMap = computed(() => {
+27 -26
View File
@@ -53,37 +53,21 @@
</div>
</div>
<Table class="overflow-hidden rounded-lg border">
<TableHeader>
<TableRow>
<TableHead>{{ t('Name') }}</TableHead>
</TableRow>
</TableHeader>
<TableBody v-if="filteredPolicies.length">
<TableRow v-for="policy in filteredPolicies" :key="policy">
<TableCell class="font-medium">{{ policy }}</TableCell>
</TableRow>
</TableBody>
<TableBody v-else>
<TableRow>
<TableCell class="text-center" colspan="1">
<p class="py-6 text-sm text-muted-foreground">{{ t('No Data') }}</p>
</TableCell>
</TableRow>
</TableBody>
</Table>
<DataTable :table="table" />
</div>
</template>
<script setup lang="ts">
import { Icon } from '#components'
import DataTable from '@/components/data-table/data-table.vue'
import { useDataTable } from '@/components/data-table/useDataTable'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command'
import { Field, FieldContent, FieldLabel } from '@/components/ui/field'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { computed, onMounted, ref, watch } from 'vue'
import type { ColumnDef } from '@tanstack/vue-table'
import { computed, h, onMounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
const { listPolicies, setUserOrGroupPolicy } = usePolicies()
@@ -108,11 +92,28 @@ const submitting = ref(false)
const currentPolicies = computed(() => props.user.policy ?? [])
const filteredPolicies = computed(() => {
const keyword = searchTerm.value.trim().toLowerCase()
const source = currentPolicies.value
if (!keyword) return source
return source.filter(item => item.toLowerCase().includes(keyword))
interface PolicyItem {
name: string
}
const policiesData = computed<PolicyItem[]>(() => currentPolicies.value.map(name => ({ name })))
const columns: ColumnDef<PolicyItem>[] = [
{
id: 'name',
header: () => t('Name'),
cell: ({ row }) => h('span', { class: 'font-medium' }, row.original.name),
},
]
const { table } = useDataTable<PolicyItem>({
data: policiesData,
columns,
getRowId: row => row.name,
})
watch(searchTerm, value => {
table.getColumn('name')?.setFilterValue(value || undefined)
})
const policyLabelMap = computed(() => {