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:
@@ -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
|
||||
|
||||
@@ -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?",
|
||||
|
||||
@@ -2048,7 +2048,10 @@
|
||||
"startDate": "开始日期",
|
||||
"endDate": "结束日期",
|
||||
"total": "共 {count} 条记录",
|
||||
"totalRecords": "总记录数",
|
||||
"maxRecords": "最大 {count} 条",
|
||||
"maxRecordsLabel": "最大记录数",
|
||||
"unlimited": "不限制",
|
||||
"notStreamType": "该变量不是记录类型",
|
||||
"deleteRecord": "删除记录",
|
||||
"deleteConfirm": "确定要删除该条记录吗?",
|
||||
|
||||
Reference in New Issue
Block a user