merge: 合并远程分支并修复代码冲突
合并了远程分支 cb72262 的功能更新,同时保留了 ESLint 修复:
**冲突解决详情:**
1. AccountTableFilters.vue
- ✅ 保留 emit 模式修复(避免 vue/no-mutating-props 错误)
- ✅ 添加第三个筛选器 type(账户类型)
- ✅ 新增 antigravity 平台和 inactive 状态选项
2. UserBalanceModal.vue
- ✅ 保留 console.error 错误日志
- ✅ 添加输入验证(金额校验、余额不足检查)
- ✅ 使用 appStore.showError 向用户显示友好错误
3. AccountsView.vue
- ✅ 保留所有 console.error 错误日志(避免 no-empty 错误)
- ✅ 使用新 API:clearRateLimit 和 setSchedulable
4. UsageView.vue
- ✅ 添加 console.error 错误日志
- ✅ 添加图表功能(模型分布、使用趋势)
- ✅ 添加粒度选择(按天/按小时)
- ✅ 保留 XLSX 动态导入优化
**测试结果:**
- ✅ Go tests: PASS
- ✅ golangci-lint: 0 issues
- ✅ ESLint: 0 errors
- ✅ TypeScript: PASS
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,21 @@
|
||||
<AppLayout>
|
||||
<div class="space-y-6">
|
||||
<UsageStatsCards :stats="usageStats" />
|
||||
<!-- Charts Section -->
|
||||
<div class="space-y-4">
|
||||
<div class="card p-4">
|
||||
<div class="flex items-center gap-4">
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">{{ t('admin.dashboard.granularity') }}:</span>
|
||||
<div class="w-28">
|
||||
<Select v-model="granularity" :options="granularityOptions" @change="loadChartData" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||||
<ModelDistributionChart :model-stats="modelStats" :loading="chartsLoading" />
|
||||
<TokenUsageTrend :trend-data="trendData" :loading="chartsLoading" />
|
||||
</div>
|
||||
</div>
|
||||
<UsageFilters v-model="filters" v-model:startDate="startDate" v-model:endDate="endDate" :exporting="exporting" @change="applyFilters" @reset="resetFilters" @export="exportToExcel" />
|
||||
<UsageTable :data="usageLogs" :loading="loading" />
|
||||
<Pagination v-if="pagination.total > 0" :page="pagination.page" :total="pagination.total" :page-size="pagination.page_size" @update:page="handlePageChange" @update:pageSize="handlePageSizeChange" />
|
||||
@@ -11,19 +26,24 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, onUnmounted } from 'vue'
|
||||
import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { saveAs } from 'file-saver'
|
||||
import { useAppStore } from '@/stores/app'; import { adminAPI } from '@/api/admin'; import { adminUsageAPI } from '@/api/admin/usage'
|
||||
import AppLayout from '@/components/layout/AppLayout.vue'; import Pagination from '@/components/common/Pagination.vue'
|
||||
import AppLayout from '@/components/layout/AppLayout.vue'; import Pagination from '@/components/common/Pagination.vue'; import Select from '@/components/common/Select.vue'
|
||||
import UsageStatsCards from '@/components/admin/usage/UsageStatsCards.vue'; import UsageFilters from '@/components/admin/usage/UsageFilters.vue'
|
||||
import UsageTable from '@/components/admin/usage/UsageTable.vue'; import UsageExportProgress from '@/components/admin/usage/UsageExportProgress.vue'
|
||||
import type { UsageLog } from '@/types'; import type { AdminUsageStatsResponse, AdminUsageQueryParams } from '@/api/admin/usage'
|
||||
import ModelDistributionChart from '@/components/charts/ModelDistributionChart.vue'; import TokenUsageTrend from '@/components/charts/TokenUsageTrend.vue'
|
||||
import type { UsageLog, TrendDataPoint, ModelStat } from '@/types'; import type { AdminUsageStatsResponse, AdminUsageQueryParams } from '@/api/admin/usage'
|
||||
|
||||
const { t } = useI18n()
|
||||
const appStore = useAppStore()
|
||||
const usageStats = ref<AdminUsageStatsResponse | null>(null); const usageLogs = ref<UsageLog[]>([]); const loading = ref(false); const exporting = ref(false)
|
||||
const trendData = ref<TrendDataPoint[]>([]); const modelStats = ref<ModelStat[]>([]); const chartsLoading = ref(false); const granularity = ref<'day' | 'hour'>('day')
|
||||
let abortController: AbortController | null = null; let exportAbortController: AbortController | null = null
|
||||
const exportProgress = reactive({ show: false, progress: 0, current: 0, total: 0, estimatedTime: '' })
|
||||
|
||||
const granularityOptions = computed(() => [{ value: 'day', label: t('admin.dashboard.day') }, { value: 'hour', label: t('admin.dashboard.hour') }])
|
||||
const formatLD = (d: Date) => d.toISOString().split('T')[0]
|
||||
const now = new Date(); const weekAgo = new Date(Date.now() - 6 * 86400000)
|
||||
const startDate = ref(formatLD(weekAgo)); const endDate = ref(formatLD(now))
|
||||
@@ -38,8 +58,16 @@ const loadLogs = async () => {
|
||||
} catch (error: any) { if(error?.name !== 'AbortError') console.error('Failed to load usage logs:', error) } finally { if(abortController === c) loading.value = false }
|
||||
}
|
||||
const loadStats = async () => { try { const s = await adminAPI.usage.getStats(filters.value); usageStats.value = s } catch (error) { console.error('Failed to load usage stats:', error) } }
|
||||
const applyFilters = () => { pagination.page = 1; loadLogs(); loadStats() }
|
||||
const resetFilters = () => { startDate.value = formatLD(weekAgo); endDate.value = formatLD(now); filters.value = { start_date: startDate.value, end_date: endDate.value }; applyFilters() }
|
||||
const loadChartData = async () => {
|
||||
chartsLoading.value = true
|
||||
try {
|
||||
const params = { start_date: filters.value.start_date || startDate.value, end_date: filters.value.end_date || endDate.value, granularity: granularity.value, user_id: filters.value.user_id }
|
||||
const [trendRes, modelRes] = await Promise.all([adminAPI.dashboard.getUsageTrend(params), adminAPI.dashboard.getModelStats({ start_date: params.start_date, end_date: params.end_date, user_id: params.user_id })])
|
||||
trendData.value = trendRes.trend || []; modelStats.value = modelRes.models || []
|
||||
} catch (error) { console.error('Failed to load chart data:', error) } finally { chartsLoading.value = false }
|
||||
}
|
||||
const applyFilters = () => { pagination.page = 1; loadLogs(); loadStats(); loadChartData() }
|
||||
const resetFilters = () => { startDate.value = formatLD(weekAgo); endDate.value = formatLD(now); filters.value = { start_date: startDate.value, end_date: endDate.value }; granularity.value = 'day'; applyFilters() }
|
||||
const handlePageChange = (p: number) => { pagination.page = p; loadLogs() }
|
||||
const handlePageSizeChange = (s: number) => { pagination.page_size = s; pagination.page = 1; loadLogs() }
|
||||
const cancelExport = () => exportAbortController?.abort()
|
||||
@@ -63,10 +91,10 @@ const exportToExcel = async () => {
|
||||
saveAs(new Blob([XLSX.write(wb, { bookType: 'xlsx', type: 'array' })], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }), `usage_${Date.now()}.xlsx`)
|
||||
appStore.showSuccess('Export Success')
|
||||
}
|
||||
} catch { appStore.showError('Export Failed') }
|
||||
} catch (error) { console.error('Failed to export:', error); appStore.showError('Export Failed') }
|
||||
finally { if(exportAbortController === c) { exportAbortController = null; exporting.value = false; exportProgress.show = false } }
|
||||
}
|
||||
|
||||
onMounted(() => { loadLogs(); loadStats() })
|
||||
onMounted(() => { loadLogs(); loadStats(); loadChartData() })
|
||||
onUnmounted(() => { abortController?.abort(); exportAbortController?.abort() })
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user