Commit ef5a4105 authored by yangjianbo's avatar yangjianbo
Browse files

feat(usage): 添加清理任务与统计过滤

parent 74a3c745
......@@ -1893,7 +1893,43 @@ export default {
cacheCreationTokens: 'Cache Creation Tokens',
cacheReadTokens: 'Cache Read Tokens',
failedToLoad: 'Failed to load usage records',
ipAddress: 'IP'
billingType: 'Billing Type',
allBillingTypes: 'All Billing Types',
billingTypeBalance: 'Balance',
billingTypeSubscription: 'Subscription',
ipAddress: 'IP',
cleanup: {
button: 'Cleanup',
title: 'Cleanup Usage Records',
warning: 'Cleanup is irreversible and will affect historical stats.',
submit: 'Submit Cleanup',
submitting: 'Submitting...',
confirmTitle: 'Confirm Cleanup',
confirmMessage: 'Are you sure you want to submit this cleanup task? This action cannot be undone.',
confirmSubmit: 'Confirm Cleanup',
cancel: 'Cancel',
cancelConfirmTitle: 'Confirm Cancel',
cancelConfirmMessage: 'Are you sure you want to cancel this cleanup task?',
cancelConfirm: 'Confirm Cancel',
cancelSuccess: 'Cleanup task canceled',
cancelFailed: 'Failed to cancel cleanup task',
recentTasks: 'Recent Cleanup Tasks',
loadingTasks: 'Loading tasks...',
noTasks: 'No cleanup tasks yet',
range: 'Range',
deletedRows: 'Deleted',
missingRange: 'Please select a date range',
submitSuccess: 'Cleanup task created',
submitFailed: 'Failed to create cleanup task',
loadFailed: 'Failed to load cleanup tasks',
status: {
pending: 'Pending',
running: 'Running',
succeeded: 'Succeeded',
failed: 'Failed',
canceled: 'Canceled'
}
}
},
// Ops Monitoring
......
......@@ -2041,7 +2041,43 @@ export default {
cacheCreationTokens: '缓存创建 Token',
cacheReadTokens: '缓存读取 Token',
failedToLoad: '加载使用记录失败',
ipAddress: 'IP'
billingType: '计费类型',
allBillingTypes: '全部计费类型',
billingTypeBalance: '钱包余额',
billingTypeSubscription: '订阅套餐',
ipAddress: 'IP',
cleanup: {
button: '清理',
title: '清理使用记录',
warning: '清理不可恢复,且会影响历史统计回看。',
submit: '提交清理',
submitting: '提交中...',
confirmTitle: '确认清理',
confirmMessage: '确定要提交清理任务吗?清理不可恢复。',
confirmSubmit: '确认清理',
cancel: '取消任务',
cancelConfirmTitle: '确认取消',
cancelConfirmMessage: '确定要取消该清理任务吗?',
cancelConfirm: '确认取消',
cancelSuccess: '清理任务已取消',
cancelFailed: '取消清理任务失败',
recentTasks: '最近清理任务',
loadingTasks: '正在加载任务...',
noTasks: '暂无清理任务',
range: '时间范围',
deletedRows: '删除数量',
missingRange: '请选择时间范围',
submitSuccess: '清理任务已创建',
submitFailed: '创建清理任务失败',
loadFailed: '加载清理任务失败',
status: {
pending: '待执行',
running: '执行中',
succeeded: '已完成',
failed: '失败',
canceled: '已取消'
}
}
},
// Ops Monitoring
......
......@@ -618,6 +618,7 @@ export interface UsageLog {
actual_cost: number
rate_multiplier: number
account_rate_multiplier?: number | null
billing_type: number
stream: boolean
duration_ms: number
......@@ -642,6 +643,33 @@ export interface UsageLog {
subscription?: UserSubscription
}
export interface UsageCleanupFilters {
start_time: string
end_time: string
user_id?: number
api_key_id?: number
account_id?: number
group_id?: number
model?: string | null
stream?: boolean | null
billing_type?: number | null
}
export interface UsageCleanupTask {
id: number
status: string
filters: UsageCleanupFilters
created_by: number
deleted_rows: number
error_message?: string | null
canceled_by?: number | null
canceled_at?: string | null
started_at?: string | null
finished_at?: string | null
created_at: string
updated_at: string
}
export interface RedeemCode {
id: number
code: string
......@@ -865,6 +893,7 @@ export interface UsageQueryParams {
group_id?: number
model?: string
stream?: boolean
billing_type?: number | null
start_date?: string
end_date?: string
}
......
......@@ -17,12 +17,19 @@
<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" />
<UsageFilters v-model="filters" v-model:startDate="startDate" v-model:endDate="endDate" :exporting="exporting" @change="applyFilters" @reset="resetFilters" @cleanup="openCleanupDialog" @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" />
</div>
</AppLayout>
<UsageExportProgress :show="exportProgress.show" :progress="exportProgress.progress" :current="exportProgress.current" :total="exportProgress.total" :estimated-time="exportProgress.estimatedTime" @cancel="cancelExport" />
<UsageCleanupDialog
:show="cleanupDialogVisible"
:filters="filters"
:start-date="startDate"
:end-date="endDate"
@close="cleanupDialogVisible = false"
/>
</template>
<script setup lang="ts">
......@@ -33,6 +40,7 @@ import { useAppStore } from '@/stores/app'; import { adminAPI } from '@/api/admi
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 UsageCleanupDialog from '@/components/admin/usage/UsageCleanupDialog.vue'
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'
......@@ -42,6 +50,7 @@ const usageStats = ref<AdminUsageStatsResponse | null>(null); const usageLogs =
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 cleanupDialogVisible = ref(false)
const granularityOptions = computed(() => [{ value: 'day', label: t('admin.dashboard.day') }, { value: 'hour', label: t('admin.dashboard.hour') }])
// Use local timezone to avoid UTC timezone issues
......@@ -53,7 +62,7 @@ const formatLD = (d: Date) => {
}
const now = new Date(); const weekAgo = new Date(); weekAgo.setDate(weekAgo.getDate() - 6)
const startDate = ref(formatLD(weekAgo)); const endDate = ref(formatLD(now))
const filters = ref<AdminUsageQueryParams>({ user_id: undefined, model: undefined, group_id: undefined, start_date: startDate.value, end_date: endDate.value })
const filters = ref<AdminUsageQueryParams>({ user_id: undefined, model: undefined, group_id: undefined, billing_type: null, start_date: startDate.value, end_date: endDate.value })
const pagination = reactive({ page: 1, page_size: 20, total: 0 })
const loadLogs = async () => {
......@@ -67,16 +76,17 @@ const loadStats = async () => { try { const s = await adminAPI.usage.getStats(fi
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, model: filters.value.model, api_key_id: filters.value.api_key_id, account_id: filters.value.account_id, group_id: filters.value.group_id, stream: filters.value.stream }
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, model: params.model, api_key_id: params.api_key_id, account_id: params.account_id, group_id: params.group_id, stream: params.stream })])
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, model: filters.value.model, api_key_id: filters.value.api_key_id, account_id: filters.value.account_id, group_id: filters.value.group_id, stream: filters.value.stream, billing_type: filters.value.billing_type }
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, model: params.model, api_key_id: params.api_key_id, account_id: params.account_id, group_id: params.group_id, stream: params.stream, billing_type: params.billing_type })])
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 resetFilters = () => { startDate.value = formatLD(weekAgo); endDate.value = formatLD(now); filters.value = { start_date: startDate.value, end_date: endDate.value, billing_type: null }; 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()
const openCleanupDialog = () => { cleanupDialogVisible.value = true }
const exportToExcel = async () => {
if (exporting.value) return; exporting.value = true; exportProgress.show = true
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment