feat(frontend): 为管理端用量页面添加列显示设置
This commit is contained in:
@@ -17,8 +17,43 @@
|
||||
<TokenUsageTrend :trend-data="trendData" :loading="chartsLoading" />
|
||||
</div>
|
||||
</div>
|
||||
<UsageFilters v-model="filters" v-model:startDate="startDate" v-model:endDate="endDate" :exporting="exporting" @change="applyFilters" @refresh="refreshData" @reset="resetFilters" @cleanup="openCleanupDialog" @export="exportToExcel" />
|
||||
<UsageTable :data="usageLogs" :loading="loading" />
|
||||
<UsageFilters v-model="filters" v-model:startDate="startDate" v-model:endDate="endDate" :exporting="exporting" @change="applyFilters" @refresh="refreshData" @reset="resetFilters" @cleanup="openCleanupDialog" @export="exportToExcel">
|
||||
<template #after-reset>
|
||||
<div class="relative" ref="columnDropdownRef">
|
||||
<button
|
||||
@click="showColumnDropdown = !showColumnDropdown"
|
||||
class="btn btn-secondary px-2 md:px-3"
|
||||
:title="t('admin.users.columnSettings')"
|
||||
>
|
||||
<svg class="h-4 w-4 md:mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 4.5v15m6-15v15m-10.875 0h15.75c.621 0 1.125-.504 1.125-1.125V5.625c0-.621-.504-1.125-1.125-1.125H4.125C3.504 4.5 3 5.004 3 5.625v12.75c0 .621.504 1.125 1.125 1.125z" />
|
||||
</svg>
|
||||
<span class="hidden md:inline">{{ t('admin.users.columnSettings') }}</span>
|
||||
</button>
|
||||
<div
|
||||
v-if="showColumnDropdown"
|
||||
class="absolute right-0 top-full z-50 mt-1 max-h-80 w-48 overflow-y-auto rounded-lg border border-gray-200 bg-white py-1 shadow-lg dark:border-dark-600 dark:bg-dark-800"
|
||||
>
|
||||
<button
|
||||
v-for="col in toggleableColumns"
|
||||
:key="col.key"
|
||||
@click="toggleColumn(col.key)"
|
||||
class="flex w-full items-center justify-between px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-dark-700"
|
||||
>
|
||||
<span>{{ col.label }}</span>
|
||||
<Icon
|
||||
v-if="isColumnVisible(col.key)"
|
||||
name="check"
|
||||
size="sm"
|
||||
class="text-primary-500"
|
||||
:stroke-width="2"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</UsageFilters>
|
||||
<UsageTable :data="usageLogs" :loading="loading" :columns="visibleColumns" />
|
||||
<Pagination v-if="pagination.total > 0" :page="pagination.page" :total="pagination.total" :page-size="pagination.page_size" @update:page="handlePageChange" @update:pageSize="handlePageSizeChange" />
|
||||
</div>
|
||||
</AppLayout>
|
||||
@@ -43,6 +78,7 @@ import UsageStatsCards from '@/components/admin/usage/UsageStatsCards.vue'; impo
|
||||
import UsageTable from '@/components/admin/usage/UsageTable.vue'; import UsageExportProgress from '@/components/admin/usage/UsageExportProgress.vue'
|
||||
import UsageCleanupDialog from '@/components/admin/usage/UsageCleanupDialog.vue'
|
||||
import ModelDistributionChart from '@/components/charts/ModelDistributionChart.vue'; import TokenUsageTrend from '@/components/charts/TokenUsageTrend.vue'
|
||||
import Icon from '@/components/icons/Icon.vue'
|
||||
import type { AdminUsageLog, TrendDataPoint, ModelStat } from '@/types'; import type { AdminUsageStatsResponse, AdminUsageQueryParams } from '@/api/admin/usage'
|
||||
|
||||
const { t } = useI18n()
|
||||
@@ -141,6 +177,77 @@ const exportToExcel = async () => {
|
||||
finally { if(exportAbortController === c) { exportAbortController = null; exporting.value = false; exportProgress.show = false } }
|
||||
}
|
||||
|
||||
onMounted(() => { loadLogs(); loadStats(); loadChartData() })
|
||||
onUnmounted(() => { abortController?.abort(); exportAbortController?.abort() })
|
||||
// Column visibility
|
||||
const ALWAYS_VISIBLE = ['user', 'created_at']
|
||||
const DEFAULT_HIDDEN_COLUMNS = ['reasoning_effort', 'user_agent']
|
||||
const HIDDEN_COLUMNS_KEY = 'usage-hidden-columns'
|
||||
|
||||
const allColumns = computed(() => [
|
||||
{ key: 'user', label: t('admin.usage.user'), sortable: false },
|
||||
{ key: 'api_key', label: t('usage.apiKeyFilter'), sortable: false },
|
||||
{ key: 'account', label: t('admin.usage.account'), sortable: false },
|
||||
{ key: 'model', label: t('usage.model'), sortable: true },
|
||||
{ key: 'reasoning_effort', label: t('usage.reasoningEffort'), sortable: false },
|
||||
{ key: 'group', label: t('admin.usage.group'), sortable: false },
|
||||
{ key: 'stream', label: t('usage.type'), sortable: false },
|
||||
{ key: 'tokens', label: t('usage.tokens'), sortable: false },
|
||||
{ key: 'cost', label: t('usage.cost'), sortable: false },
|
||||
{ key: 'first_token', label: t('usage.firstToken'), sortable: false },
|
||||
{ key: 'duration', label: t('usage.duration'), sortable: false },
|
||||
{ key: 'created_at', label: t('usage.time'), sortable: true },
|
||||
{ key: 'user_agent', label: t('usage.userAgent'), sortable: false },
|
||||
{ key: 'ip_address', label: t('admin.usage.ipAddress'), sortable: false }
|
||||
])
|
||||
|
||||
const hiddenColumns = reactive<Set<string>>(new Set())
|
||||
|
||||
const toggleableColumns = computed(() =>
|
||||
allColumns.value.filter(col => !ALWAYS_VISIBLE.includes(col.key))
|
||||
)
|
||||
|
||||
const visibleColumns = computed(() =>
|
||||
allColumns.value.filter(col =>
|
||||
ALWAYS_VISIBLE.includes(col.key) || !hiddenColumns.has(col.key)
|
||||
)
|
||||
)
|
||||
|
||||
const isColumnVisible = (key: string) => !hiddenColumns.has(key)
|
||||
|
||||
const toggleColumn = (key: string) => {
|
||||
if (hiddenColumns.has(key)) {
|
||||
hiddenColumns.delete(key)
|
||||
} else {
|
||||
hiddenColumns.add(key)
|
||||
}
|
||||
try {
|
||||
localStorage.setItem(HIDDEN_COLUMNS_KEY, JSON.stringify([...hiddenColumns]))
|
||||
} catch (e) {
|
||||
console.error('Failed to save columns:', e)
|
||||
}
|
||||
}
|
||||
|
||||
const loadSavedColumns = () => {
|
||||
try {
|
||||
const saved = localStorage.getItem(HIDDEN_COLUMNS_KEY)
|
||||
if (saved) {
|
||||
(JSON.parse(saved) as string[]).forEach(key => hiddenColumns.add(key))
|
||||
} else {
|
||||
DEFAULT_HIDDEN_COLUMNS.forEach(key => hiddenColumns.add(key))
|
||||
}
|
||||
} catch {
|
||||
DEFAULT_HIDDEN_COLUMNS.forEach(key => hiddenColumns.add(key))
|
||||
}
|
||||
}
|
||||
|
||||
const showColumnDropdown = ref(false)
|
||||
const columnDropdownRef = ref<HTMLElement | null>(null)
|
||||
|
||||
const handleColumnClickOutside = (event: MouseEvent) => {
|
||||
if (columnDropdownRef.value && !columnDropdownRef.value.contains(event.target as HTMLElement)) {
|
||||
showColumnDropdown.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => { loadLogs(); loadStats(); loadChartData(); loadSavedColumns(); document.addEventListener('click', handleColumnClickOutside) })
|
||||
onUnmounted(() => { abortController?.abort(); exportAbortController?.abort(); document.removeEventListener('click', handleColumnClickOutside) })
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user