refactor: improve cloud variable records page with standard DataTable layout

- Add statistics cards (total records, variable key, max records, scope)
- Use standard DataTable component with toolbar template
- Add BulkActions component for batch operations
- Add DataTableViewOptions for column visibility control
- Dynamic columns are generated from record data fields
- Add missing i18n keys (totalRecords, maxRecordsLabel, unlimited)
- Match styling with other admin data table pages
This commit is contained in:
2026-05-07 23:40:14 +08:00
parent 74d030f846
commit de2c455e79
3 changed files with 142 additions and 66 deletions
@@ -8,9 +8,11 @@ import { useRoute, useRouter } from 'vue-router'
import { toast } from 'vue-sonner'
import ConfirmDialog from '@/components/confirm-dialog.vue'
import BulkActions from '@/components/data-table/bulk-actions.vue'
import DataTable from '@/components/data-table/data-table.vue'
import { SelectColumn } from '@/components/data-table/table-columns'
import { generateVueTable } from '@/components/data-table/use-generate-vue-table'
import DataTableViewOptions from '@/components/data-table/view-options.vue'
import { BasicPage } from '@/components/global-layout'
import Badge from '@/components/ui/badge/Badge.vue'
import Button from '@/components/ui/button/Button.vue'
@@ -61,7 +63,7 @@ const dynamicKeys = computed(() => {
Object.keys(record.data).forEach(key => keys.add(key))
}
})
return Array.from(keys)
return Array.from(keys).sort()
})
const columns = computed<ColumnDef<VariableRecord>[]>(() => {
@@ -105,7 +107,7 @@ const columns = computed<ColumnDef<VariableRecord>[]>(() => {
if (typeof value === 'boolean') {
return h(Badge, { variant: value ? 'default' : 'secondary' }, () => String(value))
}
return h('span', { class: 'truncate max-w-xs', title: String(value) }, String(value))
return h('span', { class: 'truncate max-w-xs block', title: String(value) }, String(value))
},
size: 150,
})
@@ -134,13 +136,7 @@ const columns = computed<ColumnDef<VariableRecord>[]>(() => {
try {
const date = new Date(createdAt)
if (Number.isNaN(date.getTime())) return '-'
return h('span', { class: 'text-muted-foreground text-xs' }, date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
}))
return h('span', { class: 'text-muted-foreground text-xs' }, date.toLocaleString())
}
catch {
return '-'
@@ -188,6 +184,21 @@ const table = generateVueTable<VariableRecord>({
serverPagination: serverPagination.value,
})
const columnLabels = computed(() => {
const labels: Record<string, string> = {
select: 'admin.cloudVariables.select',
id: 'ID',
created_at: 'admin.cloudVariables.records.createdAt',
}
if (variable.value?.scope === 'user') {
labels.user = 'admin.cloudVariables.records.user'
}
dynamicKeys.value.forEach((key) => {
labels[`data_${key}`] = key
})
return labels
})
watch([page, pageSize], () => {
fetchRecords()
})
@@ -203,7 +214,7 @@ async function fetchVariable() {
}
}
catch (error) {
console.error('获取变量信息失败:', error)
console.error('Fetch variable failed:', error)
}
}
@@ -224,7 +235,7 @@ async function fetchRecords() {
total.value = data?.total || 0
}
catch (error) {
console.error('获取记录失败:', error)
console.error('Fetch records failed:', error)
}
finally {
loading.value = false
@@ -306,63 +317,122 @@ onMounted(async () => {
]"
sticky
>
<div class="space-y-4">
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<div class="relative">
<Search class="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
v-model="searchQuery"
:placeholder="t('admin.cloudVariables.records.searchPlaceholder')"
class="pl-8 w-[250px] h-9"
@keydown.enter="handleSearch"
/>
</div>
<Input
v-model="startDate"
type="date"
class="w-36 h-9"
:placeholder="t('admin.cloudVariables.records.startDate')"
/>
<Input
v-model="endDate"
type="date"
class="w-36 h-9"
:placeholder="t('admin.cloudVariables.records.endDate')"
/>
<Button variant="outline" size="sm" class="h-9" @click="handleSearch">
<Search class="h-4 w-4 mr-1" />
{{ t('admin.cloudVariables.records.search') }}
</Button>
</div>
<div class="flex items-center gap-2">
<Button
variant="destructive"
size="sm"
:disabled="table.getSelectedRowModel().rows.length === 0"
@click="openBatchDeleteDialog"
>
<Trash2 class="h-4 w-4 mr-1" />
{{ t('admin.cloudVariables.records.batchDelete') }}
<span v-if="table.getSelectedRowModel().rows.length > 0" class="ml-1">
({{ table.getSelectedRowModel().rows.length }})
</span>
</Button>
</div>
<div class="space-y-6">
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('admin.cloudVariables.records.totalRecords') }}
</UiCardTitle>
<List class="size-4 text-muted-foreground" />
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
{{ total }}
</div>
</UiCardContent>
</UiCard>
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('admin.cloudVariables.types.stream') }}
</UiCardTitle>
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
{{ variable?.key || '-' }}
</div>
</UiCardContent>
</UiCard>
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('admin.cloudVariables.records.maxRecordsLabel') }}
</UiCardTitle>
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
{{ variable?.max_records || t('admin.cloudVariables.records.unlimited') }}
</div>
</UiCardContent>
</UiCard>
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<UiCardTitle class="text-sm font-medium">
{{ t('admin.cloudVariables.columns.scope') }}
</UiCardTitle>
</UiCardHeader>
<UiCardContent>
<div class="text-2xl font-bold">
{{ variable?.scope === 'user' ? t('admin.cloudVariables.userScope') : t('admin.cloudVariables.appScope') }}
</div>
</UiCardContent>
</UiCard>
</div>
<div v-if="variable" class="flex items-center gap-4 text-sm text-muted-foreground">
<div class="flex items-center gap-1.5">
<List class="h-4 w-4" />
<span>{{ t('admin.cloudVariables.types.stream') }}</span>
</div>
<span>·</span>
<span>{{ t('admin.cloudVariables.records.total', { count: total }) }}</span>
<span v-if="variable.max_records > 0">·</span>
<span v-if="variable.max_records > 0">{{ t('admin.cloudVariables.records.maxRecords', { count: variable.max_records }) }}</span>
</div>
<DataTable :columns="columns" :data="records" :loading="loading" :table="table" :server-pagination="serverPagination" @refresh="fetchRecords" />
<UiCard>
<UiCardContent class="p-6">
<DataTable :columns="columns" :data="records" :loading="loading" :table="table" :server-pagination="serverPagination" @refresh="fetchRecords">
<template #toolbar>
<div class="space-y-3">
<div class="flex items-center justify-end">
<UiButton
variant="destructive"
size="sm"
:disabled="table.getSelectedRowModel().rows.length === 0"
@click="openBatchDeleteDialog"
>
<Trash2 class="h-4 w-4 mr-1" />
{{ t('admin.cloudVariables.records.batchDelete') }}
<span v-if="table.getSelectedRowModel().rows.length > 0" class="ml-1">
({{ table.getSelectedRowModel().rows.length }})
</span>
</UiButton>
</div>
<BulkActions :table="table" entity-name="records">
<UiButton variant="destructive" size="sm" @click="openBatchDeleteDialog">
<Trash2 class="h-4 w-4 mr-1" />
{{ t('admin.cloudVariables.records.batchDelete') }}
</UiButton>
</BulkActions>
<div class="flex flex-wrap items-center justify-between gap-2">
<div class="flex flex-wrap items-center gap-2">
<div class="relative">
<Search class="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
v-model="searchQuery"
:placeholder="t('admin.cloudVariables.records.searchPlaceholder')"
class="pl-8 w-[250px] h-9"
@keydown.enter="handleSearch"
/>
</div>
<Input
v-model="startDate"
type="date"
class="w-36 h-9"
:placeholder="t('admin.cloudVariables.records.startDate')"
/>
<Input
v-model="endDate"
type="date"
class="w-36 h-9"
:placeholder="t('admin.cloudVariables.records.endDate')"
/>
<Button variant="outline" size="sm" class="h-9" @click="handleSearch">
<Search class="h-4 w-4 mr-1" />
{{ t('admin.cloudVariables.records.search') }}
</Button>
</div>
<DataTableViewOptions :table="table" :column-labels="columnLabels" />
</div>
</div>
</template>
</DataTable>
</UiCardContent>
</UiCard>
</div>
<ConfirmDialog
+3
View File
@@ -2059,7 +2059,10 @@
"startDate": "Start Date",
"endDate": "End Date",
"total": "{count} records total",
"totalRecords": "Total Records",
"maxRecords": "Max {count} records",
"maxRecordsLabel": "Max Records",
"unlimited": "Unlimited",
"notStreamType": "This variable is not a record type",
"deleteRecord": "Delete Record",
"deleteConfirm": "Are you sure you want to delete this record?",
+3
View File
@@ -2048,7 +2048,10 @@
"startDate": "开始日期",
"endDate": "结束日期",
"total": "共 {count} 条记录",
"totalRecords": "总记录数",
"maxRecords": "最大 {count} 条",
"maxRecordsLabel": "最大记录数",
"unlimited": "不限制",
"notStreamType": "该变量不是记录类型",
"deleteRecord": "删除记录",
"deleteConfirm": "确定要删除该条记录吗?",