diff --git a/console/src/api/modules/auth.ts b/console/src/api/modules/auth.ts index 5abcbc4..f9608ac 100644 --- a/console/src/api/modules/auth.ts +++ b/console/src/api/modules/auth.ts @@ -356,6 +356,15 @@ export const authApi = { // ---- 计量:余额 / 本月用量(按模型) ---- getOpcComputeBalance: () => request<{ id: number; quota: number; used_quota: number }>("/opc/compute/balance"), getOpcComputeUsage: () => request<{ month: string; cost_quota: number; input_tokens: number; output_tokens: number; items: { model_name: string; requests: number; input_tokens: number; output_tokens: number; cost_quota: number }[] }>("/opc/compute/usage"), + getOpcComputeUsageLogs: (page: number = 1, pageSize: number = 20) => + request<{ items: { id: string; time: string; model: string; type: string; input_tokens: number; output_tokens: number; cost: number; status: string }[]; total: number; page: number; page_size: number }>(`/opc/compute/usage/logs?page=${page}&page_size=${pageSize}`), + getOpcComputeUsageTrend: (days: number = 7) => + request<{ + trend: { date: string; cost: number; tokens: number }[]; + model_distribution: { name: string; value: number; cost: number; color: string }[]; + hourly_distribution: { hour: string; cost: number }[]; + summary: { total_cost: number; total_tokens: number; total_requests: number; days: number }; + }>(`/opc/compute/usage/trend?days=${days}`), // ---- 算力充值(微信支付,native 扫码) ---- getOpcRechargePackages: () => request<{ enabled: boolean; items: { id: string; amount: number; bonus: number; label: string; quota_micro: number }[]; min_yuan: number; max_yuan: number }>("/opc/compute/recharge/packages"), // client_type="mp":桌面端展示小程序码,微信扫码在小程序内完成支付(与扫码登录同构) diff --git a/console/src/pages/portal/Opc/Compute.tsx b/console/src/pages/portal/Opc/Compute.tsx index 9dfbe5a..bb08f27 100644 --- a/console/src/pages/portal/Opc/Compute.tsx +++ b/console/src/pages/portal/Opc/Compute.tsx @@ -56,43 +56,6 @@ function seriesOf(id: string): string { const SERIES_COLORS = ["#6284ff", "#8f7bff", "#3fb68b", "#e8830c", "#e0526e", "#38a8e0", "#9a7bd8"]; const seriesColor = (s: string) => SERIES_COLORS[[...s].reduce((a, ch) => a + ch.charCodeAt(0), 0) % SERIES_COLORS.length]; -// 模拟消耗数据(实际项目中应从API获取) -const MOCK_USAGE_TREND = [ - { date: "09-01", cost: 12.5, tokens: 125000 }, - { date: "09-02", cost: 8.3, tokens: 83000 }, - { date: "09-03", cost: 15.7, tokens: 157000 }, - { date: "09-04", cost: 6.2, tokens: 62000 }, - { date: "09-05", cost: 18.9, tokens: 189000 }, - { date: "09-06", cost: 11.4, tokens: 114000 }, - { date: "09-07", cost: 9.8, tokens: 98000 }, -]; - -const MOCK_MODEL_DISTRIBUTION = [ - { name: "GPT-4o", value: 35, color: "#6284ff" }, - { name: "Claude 3.5", value: 28, color: "#8f7bff" }, - { name: "DeepSeek", value: 18, color: "#3fb68b" }, - { name: "通义千问", value: 12, color: "#e8830c" }, - { name: "其他", value: 7, color: "#e0526e" }, -]; - -const MOCK_HOURLY_DISTRIBUTION = [ - { hour: "00", cost: 2 }, { hour: "02", cost: 1 }, { hour: "04", cost: 0.5 }, - { hour: "06", cost: 1 }, { hour: "08", cost: 5 }, { hour: "10", cost: 12 }, - { hour: "12", cost: 8 }, { hour: "14", cost: 15 }, { hour: "16", cost: 18 }, - { hour: "18", cost: 10 }, { hour: "20", cost: 6 }, { hour: "22", cost: 3 }, -]; - -const MOCK_USAGE_RECORDS = [ - { id: "u001", time: "2026-09-05 16:32:18", model: "gpt-4o", type: "聊天", inputTokens: 8520, outputTokens: 2180, cost: 0.125, status: "成功" }, - { id: "u002", time: "2026-09-05 15:45:22", model: "claude-3-5-sonnet", type: "聊天", inputTokens: 12300, outputTokens: 3450, cost: 0.158, status: "成功" }, - { id: "u003", time: "2026-09-05 14:20:15", model: "deepseek-chat", type: "聊天", inputTokens: 5600, outputTokens: 1800, cost: 0.028, status: "成功" }, - { id: "u004", time: "2026-09-05 11:08:33", model: "qwen-vl-max", type: "视觉", inputTokens: 15800, outputTokens: 2200, cost: 0.089, status: "成功" }, - { id: "u005", time: "2026-09-05 10:15:47", model: "text-embedding-3-large", type: "嵌入", inputTokens: 50000, outputTokens: 0, cost: 0.065, status: "成功" }, - { id: "u006", time: "2026-09-04 22:30:12", model: "gpt-4o", type: "聊天", inputTokens: 9800, outputTokens: 2500, cost: 0.142, status: "成功" }, - { id: "u007", time: "2026-09-04 18:45:55", model: "whisper-1", type: "音频", inputTokens: 0, outputTokens: 3200, cost: 0.032, status: "成功" }, - { id: "u008", time: "2026-09-04 16:20:33", model: "claude-3-5-sonnet", type: "聊天", inputTokens: 18500, outputTokens: 4200, cost: 0.215, status: "失败" }, -]; - // SVG 折线图组件 function LineChart({ data, height = 180 }: { data: { date: string; cost: number }[]; height?: number }) { const maxCost = Math.max(...data.map((d) => d.cost), 1); @@ -221,6 +184,16 @@ export default function OpcCompute() { const [payOrder, setPayOrder] = useState<{ order_no: string; code_url?: string; qr_image?: string; status: string } | null>(null); const [payPolling, setPayPolling] = useState(false); + // 消耗统计真实数据 + const [usageTrend, setUsageTrend] = useState<{ date: string; cost: number; tokens: number }[]>([]); + const [modelDistribution, setModelDistribution] = useState<{ name: string; value: number; cost: number; color: string }[]>([]); + const [hourlyDistribution, setHourlyDistribution] = useState<{ hour: string; cost: number }[]>([]); + const [usageSummary, setUsageSummary] = useState<{ total_cost: number; total_tokens: number; total_requests: number; days: number } | null>(null); + const [usageLogs, setUsageLogs] = useState<{ id: string; time: string; model: string; type: string; input_tokens: number; output_tokens: number; cost: number; status: string }[]>([]); + const [usageLogsTotal, setUsageLogsTotal] = useState(0); + const [usageLogsPage, setUsageLogsPage] = useState(1); + const [usageLogsLoading, setUsageLogsLoading] = useState(false); + const load = useCallback(() => { authApi.getOpcComputeBase().then(setBase).catch(() => {}); authApi.getOpcComputeModels().then((r) => setModels(r.items ?? [])).catch(() => {}); @@ -228,9 +201,32 @@ export default function OpcCompute() { authApi.getOpcComputeBalance().then(setBalance).catch(() => {}); authApi.getOpcRechargePackages().then(setPackages).catch(() => {}); authApi.getOpcRechargeOrders().then((r) => setOrders(r.items ?? [])).catch(() => {}); + // 加载消耗趋势统计 + authApi.getOpcComputeUsageTrend(7).then((r) => { + setUsageTrend(r.trend || []); + setModelDistribution(r.model_distribution || []); + setHourlyDistribution(r.hourly_distribution || []); + setUsageSummary(r.summary || null); + }).catch(() => {}); + }, []); + + // 加载消耗明细(分页) + const loadUsageLogs = useCallback((page: number = 1) => { + setUsageLogsLoading(true); + authApi.getOpcComputeUsageLogs(page, 20).then((r) => { + setUsageLogs(r.items || []); + setUsageLogsTotal(r.total || 0); + setUsageLogsPage(page); + }).catch(() => { + setUsageLogs([]); + setUsageLogsTotal(0); + }).finally(() => { + setUsageLogsLoading(false); + }); }, []); useEffect(() => { load(); }, [load]); + useEffect(() => { loadUsageLogs(1); }, [loadUsageLogs]); const create = useCallback(async () => { setLoading(true); @@ -276,17 +272,18 @@ export default function OpcCompute() { .map(([series, items]) => ({ series, items: items.sort((a, b) => (a.id || "").localeCompare(b.id || "")) })); }, [models, cat]); - const totalCost = MOCK_USAGE_TREND.reduce((sum, d) => sum + d.cost, 0); - const totalTokens = MOCK_USAGE_TREND.reduce((sum, d) => sum + d.tokens, 0); + const totalCost = usageSummary?.total_cost ?? usageTrend.reduce((sum, d) => sum + d.cost, 0); + const totalTokens = usageSummary?.total_tokens ?? usageTrend.reduce((sum, d) => sum + d.tokens, 0); + const totalRequests = usageSummary?.total_requests ?? 0; const usageColumns = [ - { title: "时间", dataIndex: "time", key: "time", width: 180, render: (v: string) => {v} }, - { title: "模型", dataIndex: "model", key: "model", render: (v: string) => {v} }, - { title: "类型", dataIndex: "type", key: "type", render: (v: string) => {v} }, - { title: "输入Tokens", dataIndex: "inputTokens", key: "inputTokens", render: (v: number) => {v.toLocaleString()} }, - { title: "输出Tokens", dataIndex: "outputTokens", key: "outputTokens", render: (v: number) => {v.toLocaleString()} }, - { title: "费用", dataIndex: "cost", key: "cost", render: (v: number) => ¥{v.toFixed(4)} }, - { title: "状态", dataIndex: "status", key: "status", render: (v: string) => {v} }, + { title: "时间", dataIndex: "time", key: "time", width: 180, render: (v: string) => {v || "-"} }, + { title: "模型", dataIndex: "model", key: "model", render: (v: string) => v ? {v} : "-" }, + { title: "类型", dataIndex: "type", key: "type", render: (v: string) => v ? {v} : "-" }, + { title: "输入Tokens", dataIndex: "input_tokens", key: "input_tokens", render: (v: number) => {(v || 0).toLocaleString()} }, + { title: "输出Tokens", dataIndex: "output_tokens", key: "output_tokens", render: (v: number) => {(v || 0).toLocaleString()} }, + { title: "费用", dataIndex: "cost", key: "cost", render: (v: number) => ¥{(v || 0).toFixed(4)} }, + { title: "状态", dataIndex: "status", key: "status", render: (v: string) => {v === "success" ? "成功" : v || "未知"} }, ]; return ( @@ -388,19 +385,19 @@ export default function OpcCompute() { 本周 - + 0 ? usageTrend : [{ date: "暂无", cost: 0, tokens: 0 }]} height={200} />
日均消耗
-
¥{(totalCost / 7).toFixed(2)}
+
¥{(totalCost / Math.max(usageTrend.length, 1)).toFixed(2)}
最高单日
-
¥{Math.max(...MOCK_USAGE_TREND.map(d => d.cost)).toFixed(2)}
+
¥{usageTrend.length > 0 ? Math.max(...usageTrend.map(d => d.cost)).toFixed(2) : "0.00"}
最低单日
-
¥{Math.min(...MOCK_USAGE_TREND.map(d => d.cost)).toFixed(2)}
+
¥{usageTrend.length > 0 ? Math.min(...usageTrend.filter(d => d.cost > 0).map(d => d.cost)).toFixed(2) : "0.00"}
@@ -412,7 +409,7 @@ export default function OpcCompute() {
模型调用分布
- + 0 ? modelDistribution : [{ name: "暂无数据", value: 100, color: "#ccc" }]} size={150} /> @@ -425,9 +422,14 @@ export default function OpcCompute() { 按时段 - + 0 ? hourlyDistribution : [{ hour: "00", cost: 0 }]} height={180} />
- 高峰时段:14:00-18:00,占全天消耗的 52% + {hourlyDistribution.length > 0 ? (() => { + const maxHour = hourlyDistribution.reduce((max, d) => d.cost > max.cost ? d : max, hourlyDistribution[0]); + const peakStart = Math.max(0, parseInt(maxHour.hour) - 2); + const peakEnd = Math.min(23, parseInt(maxHour.hour) + 2); + return `高峰时段:${peakStart}:00-${peakEnd}:00`; + })() : "暂无数据"}
@@ -499,13 +501,13 @@ export default function OpcCompute() {
今日消耗
-
¥0.856
- +
¥{(usageTrend.length > 0 ? usageTrend[usageTrend.length - 1].cost : 0).toFixed(3)}
+ 0 ? usageTrend[usageTrend.length - 1].cost : 0) / Math.max(totalCost, 1)) * 100)} showInfo={false} strokeColor={{ from: "#667eea", to: "#764ba2" }} style={{ marginTop: 8 }} />
-
本月消耗
+
近7天消耗
¥{totalCost.toFixed(2)}
@@ -513,7 +515,7 @@ export default function OpcCompute() {
调用次数
-
1,286
+
{totalRequests.toLocaleString()}
@@ -526,16 +528,24 @@ export default function OpcCompute() { 消耗明细 - - + `共 ${total} 条记录` }} + loading={usageLogsLoading} + pagination={{ + current: usageLogsPage, + pageSize: 20, + total: usageLogsTotal, + showSizeChanger: false, + showTotal: (total) => `共 ${total} 条记录`, + onChange: (page) => loadUsageLogs(page), + }} size="middle" + locale={{ emptyText: "暂无消耗记录" }} />