From 92653385ea49944e8eaecd33e8a3e502c3e9bf20 Mon Sep 17 00:00:00 2001 From: WangDL Date: Sun, 24 May 2026 11:04:00 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20M1-06=20billing=20page=20=E2=80=94=20AI?= =?UTF-8?q?=20cost=20report=20tab=20+=20CSV=20export?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 --- src/pages/Billing.tsx | 127 +++++++++++++++++++++++++----------------- 1 file changed, 77 insertions(+), 50 deletions(-) diff --git a/src/pages/Billing.tsx b/src/pages/Billing.tsx index 2aa387c..dadfacc 100644 --- a/src/pages/Billing.tsx +++ b/src/pages/Billing.tsx @@ -1,15 +1,14 @@ import { useState } from 'react' import { useQuery, useQueryClient } from '@tanstack/react-query' -import { Card, Row, Col, Statistic, Button, Tag, Space, Typography, App, Table, Modal, Form, Input, Select, DatePicker, InputNumber } from 'antd' -import { DollarOutlined, ReloadOutlined, LinkOutlined, PlusOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons' +import { Card, Row, Col, Statistic, Button, Tag, Space, Typography, App, Table, Modal, Form, Input, Select, DatePicker, InputNumber, Tabs } from 'antd' +import { DollarOutlined, ReloadOutlined, LinkOutlined, PlusOutlined, DeleteOutlined, EditOutlined, CloudOutlined, DownloadOutlined } from '@ant-design/icons' import { getBilling, type BillingInfo } from '@/services/billing-api' import { getCostSummary, createCost, updateCost, deleteCost as deleteCostApi, type CostSummary, type CostItem } from '@/services/costs-api' +import { api } from '@/services/http-client' import dayjs from 'dayjs' const { Text, Title } = Typography -// ── API ── - const CATEGORIES = [ { label: '服务器', value: 'server' }, { label: '域名', value: 'domain' }, @@ -28,8 +27,13 @@ function BillingContent() { const { data: billing } = useQuery({ queryKey: ['billing'], queryFn: getBilling, staleTime: 60_000 }) const { data: costs } = useQuery({ queryKey: ['costs', 'summary'], queryFn: getCostSummary, staleTime: 30_000 }) + const { data: aiReport } = useQuery({ queryKey: ['costs', 'report'], queryFn: (): Promise => api.get('/admin-api/costs/report?days=30'), staleTime: 30_000 }) + const { data: topUsers } = useQuery({ queryKey: ['costs', 'top-users'], queryFn: (): Promise => api.get('/admin-api/costs/top-users?days=30&limit=10'), staleTime: 30_000 }) - const refresh = async () => { setRefreshing(true); await Promise.all([qc.invalidateQueries({ queryKey: ['billing'] }), qc.invalidateQueries({ queryKey: ['costs', 'summary'] })]); setTimeout(() => setRefreshing(false), 800) } + const refresh = async () => { setRefreshing(true); await qc.invalidateQueries({ queryKey: ['billing'] }); await qc.invalidateQueries({ queryKey: ['costs'] }); setTimeout(() => setRefreshing(false), 800) } + + const handleAggregate = async () => { await api.post('/admin-api/costs/aggregate'); message.success('已汇总'); qc.invalidateQueries({ queryKey: ['costs'] }) } + const handleExportCsv = () => { window.open('/admin-api/costs/export-csv?days=30', '_blank') } const saveCost = async (values: any) => { const body = { ...values, purchaseDate: values.purchaseDate?.toISOString(), expiryDate: values.expiryDate?.toISOString() } @@ -64,17 +68,31 @@ function BillingContent() { { title: '金额', dataIndex: 'amount', width: 80, render: (v: number) => `¥${v}` }, ] + const providerCostCols = [ + { title: 'Provider', dataIndex: 'provider', width: 120 }, + { title: '调用量', dataIndex: 'calls', width: 90, align: 'center' as const }, + { title: 'Tokens', dataIndex: 'tokens', width: 100, align: 'center' as const, render: (v: number) => v.toLocaleString() }, + { title: '成本', dataIndex: 'cost', width: 100, align: 'center' as const, render: (v: string) => ${v} }, + ] + + const topUserCols = [ + { title: '用户', dataIndex: 'userId', width: 180, ellipsis: true }, + { title: '调用量', dataIndex: 'calls', width: 80, align: 'center' as const }, + { title: 'Tokens', dataIndex: 'tokens', width: 100, align: 'center' as const, render: (v: number) => v.toLocaleString() }, + { title: '成本', dataIndex: 'cost', width: 100, align: 'center' as const, render: (v: string) => ${v} }, + ] + return (
<DollarOutlined /> 费用总览 +
- {/* API 卡片 */} {providers.map((p: BillingInfo) => ( @@ -86,51 +104,60 @@ function BillingContent() { ))} - {/* 统计数字 */} - - - - - + + + + + + + + {summary.items.map((item: CostItem) => { + const daysLeft = item.expiryDate ? Math.ceil((new Date(item.expiryDate).getTime() - Date.now()) / 86400000) : null + const isExpired = daysLeft !== null && daysLeft <= 0 + return ( + +