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 { toast } from 'vue-sonner'
import ConfirmDialog from '@/components/confirm-dialog.vue' 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 DataTable from '@/components/data-table/data-table.vue'
import { SelectColumn } from '@/components/data-table/table-columns' import { SelectColumn } from '@/components/data-table/table-columns'
import { generateVueTable } from '@/components/data-table/use-generate-vue-table' 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 { BasicPage } from '@/components/global-layout'
import Badge from '@/components/ui/badge/Badge.vue' import Badge from '@/components/ui/badge/Badge.vue'
import Button from '@/components/ui/button/Button.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)) Object.keys(record.data).forEach(key => keys.add(key))
} }
}) })
return Array.from(keys) return Array.from(keys).sort()
}) })
const columns = computed<ColumnDef<VariableRecord>[]>(() => { const columns = computed<ColumnDef<VariableRecord>[]>(() => {
@@ -105,7 +107,7 @@ const columns = computed<ColumnDef<VariableRecord>[]>(() => {
if (typeof value === 'boolean') { if (typeof value === 'boolean') {
return h(Badge, { variant: value ? 'default' : 'secondary' }, () => String(value)) 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, size: 150,
}) })
@@ -134,13 +136,7 @@ const columns = computed<ColumnDef<VariableRecord>[]>(() => {
try { try {
const date = new Date(createdAt) const date = new Date(createdAt)
if (Number.isNaN(date.getTime())) return '-' if (Number.isNaN(date.getTime())) return '-'
return h('span', { class: 'text-muted-foreground text-xs' }, date.toLocaleString('zh-CN', { return h('span', { class: 'text-muted-foreground text-xs' }, date.toLocaleString())
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
}))
} }
catch { catch {
return '-' return '-'
@@ -188,6 +184,21 @@ const table = generateVueTable<VariableRecord>({
serverPagination: serverPagination.value, 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], () => { watch([page, pageSize], () => {
fetchRecords() fetchRecords()
}) })
@@ -203,7 +214,7 @@ async function fetchVariable() {
} }
} }
catch (error) { catch (error) {
console.error('获取变量信息失败:', error) console.error('Fetch variable failed:', error)
} }
} }
@@ -224,7 +235,7 @@ async function fetchRecords() {
total.value = data?.total || 0 total.value = data?.total || 0
} }
catch (error) { catch (error) {
console.error('获取记录失败:', error) console.error('Fetch records failed:', error)
} }
finally { finally {
loading.value = false loading.value = false
@@ -306,63 +317,122 @@ onMounted(async () => {
]" ]"
sticky sticky
> >
<div class="space-y-4"> <div class="space-y-6">
<div class="flex items-center justify-between"> <div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<div class="flex items-center gap-2"> <UiCard>
<div class="relative"> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<Search class="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" /> <UiCardTitle class="text-sm font-medium">
<Input {{ t('admin.cloudVariables.records.totalRecords') }}
v-model="searchQuery" </UiCardTitle>
:placeholder="t('admin.cloudVariables.records.searchPlaceholder')" <List class="size-4 text-muted-foreground" />
class="pl-8 w-[250px] h-9" </UiCardHeader>
@keydown.enter="handleSearch" <UiCardContent>
/> <div class="text-2xl font-bold">
</div> {{ total }}
<Input </div>
v-model="startDate" </UiCardContent>
type="date" </UiCard>
class="w-36 h-9"
:placeholder="t('admin.cloudVariables.records.startDate')" <UiCard>
/> <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
<Input <UiCardTitle class="text-sm font-medium">
v-model="endDate" {{ t('admin.cloudVariables.types.stream') }}
type="date" </UiCardTitle>
class="w-36 h-9" </UiCardHeader>
:placeholder="t('admin.cloudVariables.records.endDate')" <UiCardContent>
/> <div class="text-2xl font-bold">
<Button variant="outline" size="sm" class="h-9" @click="handleSearch"> {{ variable?.key || '-' }}
<Search class="h-4 w-4 mr-1" /> </div>
{{ t('admin.cloudVariables.records.search') }} </UiCardContent>
</Button> </UiCard>
</div>
<div class="flex items-center gap-2"> <UiCard>
<Button <UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
variant="destructive" <UiCardTitle class="text-sm font-medium">
size="sm" {{ t('admin.cloudVariables.records.maxRecordsLabel') }}
:disabled="table.getSelectedRowModel().rows.length === 0" </UiCardTitle>
@click="openBatchDeleteDialog" </UiCardHeader>
> <UiCardContent>
<Trash2 class="h-4 w-4 mr-1" /> <div class="text-2xl font-bold">
{{ t('admin.cloudVariables.records.batchDelete') }} {{ variable?.max_records || t('admin.cloudVariables.records.unlimited') }}
<span v-if="table.getSelectedRowModel().rows.length > 0" class="ml-1"> </div>
({{ table.getSelectedRowModel().rows.length }}) </UiCardContent>
</span> </UiCard>
</Button>
</div> <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>
<div v-if="variable" class="flex items-center gap-4 text-sm text-muted-foreground"> <UiCard>
<div class="flex items-center gap-1.5"> <UiCardContent class="p-6">
<List class="h-4 w-4" /> <DataTable :columns="columns" :data="records" :loading="loading" :table="table" :server-pagination="serverPagination" @refresh="fetchRecords">
<span>{{ t('admin.cloudVariables.types.stream') }}</span> <template #toolbar>
</div> <div class="space-y-3">
<span>·</span> <div class="flex items-center justify-end">
<span>{{ t('admin.cloudVariables.records.total', { count: total }) }}</span> <UiButton
<span v-if="variable.max_records > 0">·</span> variant="destructive"
<span v-if="variable.max_records > 0">{{ t('admin.cloudVariables.records.maxRecords', { count: variable.max_records }) }}</span> size="sm"
</div> :disabled="table.getSelectedRowModel().rows.length === 0"
@click="openBatchDeleteDialog"
<DataTable :columns="columns" :data="records" :loading="loading" :table="table" :server-pagination="serverPagination" @refresh="fetchRecords" /> >
<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> </div>
<ConfirmDialog <ConfirmDialog
+3
View File
@@ -2059,7 +2059,10 @@
"startDate": "Start Date", "startDate": "Start Date",
"endDate": "End Date", "endDate": "End Date",
"total": "{count} records total", "total": "{count} records total",
"totalRecords": "Total Records",
"maxRecords": "Max {count} records", "maxRecords": "Max {count} records",
"maxRecordsLabel": "Max Records",
"unlimited": "Unlimited",
"notStreamType": "This variable is not a record type", "notStreamType": "This variable is not a record type",
"deleteRecord": "Delete Record", "deleteRecord": "Delete Record",
"deleteConfirm": "Are you sure you want to delete this record?", "deleteConfirm": "Are you sure you want to delete this record?",
+3
View File
@@ -2048,7 +2048,10 @@
"startDate": "开始日期", "startDate": "开始日期",
"endDate": "结束日期", "endDate": "结束日期",
"total": "共 {count} 条记录", "total": "共 {count} 条记录",
"totalRecords": "总记录数",
"maxRecords": "最大 {count} 条", "maxRecords": "最大 {count} 条",
"maxRecordsLabel": "最大记录数",
"unlimited": "不限制",
"notStreamType": "该变量不是记录类型", "notStreamType": "该变量不是记录类型",
"deleteRecord": "删除记录", "deleteRecord": "删除记录",
"deleteConfirm": "确定要删除该条记录吗?", "deleteConfirm": "确定要删除该条记录吗?",