import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import * as Clipboard from 'expo-clipboard'; import { Stack, useLocalSearchParams } from 'expo-router'; import { useMemo, useState } from 'react'; import { Pressable, ScrollView, Text, TextInput, View } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { LineTrendChart } from '@/src/components/line-trend-chart'; import { getDashboardSnapshot, getUsageStats, getUser, listUserApiKeys, updateUserBalance } from '@/src/services/admin'; import type { AdminApiKey, BalanceOperation } from '@/src/types/admin'; const colors = { page: '#f4efe4', card: '#fbf8f2', text: '#16181a', subtext: '#6f665c', border: '#e7dfcf', primary: '#1d5f55', dark: '#1b1d1f', errorBg: '#f7e1d6', errorText: '#a4512b', muted: '#f7f1e6', }; type RangeKey = '24h' | '7d' | '30d'; const RANGE_OPTIONS: Array<{ key: RangeKey; label: string }> = [ { key: '24h', label: '24H' }, { key: '7d', label: '7D' }, { key: '30d', label: '30D' }, ]; function getDateRange(rangeKey: RangeKey) { const end = new Date(); const start = new Date(); if (rangeKey === '24h') { start.setHours(end.getHours() - 23, 0, 0, 0); } else if (rangeKey === '30d') { start.setDate(end.getDate() - 29); } else { start.setDate(end.getDate() - 6); } const toDate = (value: Date) => value.toISOString().slice(0, 10); return { start_date: toDate(start), end_date: toDate(end), granularity: rangeKey === '24h' ? ('hour' as const) : ('day' as const), }; } function getErrorMessage(error: unknown) { if (error instanceof Error && error.message) { switch (error.message) { case 'BASE_URL_REQUIRED': return '请先到服务器页填写服务地址。'; case 'ADMIN_API_KEY_REQUIRED': return '请先到服务器页填写 Admin Token。'; default: return error.message; } } return '加载失败,请稍后重试。'; } function formatMoney(value?: number | null) { return `$${Number(value ?? 0).toFixed(2)}`; } function formatUsageCost(stats?: { total_account_cost?: number | null; total_actual_cost?: number | null; total_cost?: number | null }) { const value = Number(stats?.total_account_cost ?? stats?.total_actual_cost ?? stats?.total_cost ?? 0); return `$${value.toFixed(4)}`; } function formatTokenValue(value?: number | null) { const number = Number(value ?? 0); if (number >= 1_000_000_000) return `${(number / 1_000_000_000).toFixed(2)}B`; if (number >= 1_000_000) return `${(number / 1_000_000).toFixed(2)}M`; if (number >= 1_000) return `${(number / 1_000).toFixed(2)}K`; return new Intl.NumberFormat('en-US').format(number); } function formatQuota(quotaUsed?: number | null, quota?: number | null) { const used = Number(quotaUsed ?? 0); const limit = Number(quota ?? 0); if (limit <= 0) { return '∞'; } return `${used} / ${limit}`; } function formatTime(value?: string | null) { if (!value) return '--'; const date = new Date(value); if (Number.isNaN(date.getTime())) return value; const year = date.getFullYear(); const month = `${date.getMonth() + 1}`.padStart(2, '0'); const day = `${date.getDate()}`.padStart(2, '0'); const hours = `${date.getHours()}`.padStart(2, '0'); const minutes = `${date.getMinutes()}`.padStart(2, '0'); return `${year}-${month}-${day} ${hours}:${minutes}`; } function Section({ title, children }: { title: string; children: React.ReactNode }) { return ( {title} {children} ); } function GridField({ label, value }: { label: string; value: string }) { return ( {label} {value} ); } function MetricCard({ label, value }: { label: string; value: string }) { return ( {label} {value} ); } function StatusBadge({ text }: { text: string }) { const normalized = text.toLowerCase(); const backgroundColor = normalized === 'active' ? '#dff4ea' : normalized === 'inactive' ? '#ece5da' : '#f7e1d6'; const color = normalized === 'active' ? '#17663f' : normalized === 'inactive' ? '#6f665c' : '#a4512b'; return ( {text} ); } function CopyInlineButton({ copied, onPress }: { copied: boolean; onPress: () => void }) { return ( {copied ? '已复制' : '复制'} ); } function KeyItem({ item, copied, onCopy }: { item: AdminApiKey; copied: boolean; onCopy: () => void }) { return ( {item.name || `Key #${item.id}`} {item.group?.name || '未分组'} {item.key || '--'} 已用额度 {formatQuota(item.quota_used, item.quota)} 最后使用时间 {formatTime(item.last_used_at || item.updated_at || item.created_at)} ); } export default function UserDetailScreen() { const { id } = useLocalSearchParams<{ id: string }>(); const userId = Number(id); const queryClient = useQueryClient(); const [operation, setOperation] = useState('add'); const [amount, setAmount] = useState('10'); const [notes, setNotes] = useState(''); const [formError, setFormError] = useState(null); const [searchText, setSearchText] = useState(''); const [copiedKeyId, setCopiedKeyId] = useState(null); const [rangeKey, setRangeKey] = useState('7d'); const range = getDateRange(rangeKey); const userQuery = useQuery({ queryKey: ['user', userId], queryFn: () => getUser(userId), enabled: Number.isFinite(userId), }); const apiKeysQuery = useQuery({ queryKey: ['user-api-keys', userId], queryFn: () => listUserApiKeys(userId), enabled: Number.isFinite(userId), }); const usageStatsQuery = useQuery({ queryKey: ['usage-stats', 'user', userId, rangeKey, range.start_date, range.end_date], queryFn: () => getUsageStats({ ...range, user_id: userId }), enabled: Number.isFinite(userId), }); const usageSnapshotQuery = useQuery({ queryKey: ['usage-snapshot', 'user', userId, rangeKey, range.start_date, range.end_date, range.granularity], queryFn: () => getDashboardSnapshot({ ...range, user_id: userId, include_stats: false, include_trend: true, include_model_stats: false, include_group_stats: false, include_users_trend: false, }), enabled: Number.isFinite(userId), }); ; ; const balanceMutation = useMutation({ mutationFn: (payload: { amount: number; notes?: string; operation: BalanceOperation }) => updateUserBalance(userId, { balance: payload.amount, notes: payload.notes, operation: payload.operation, }), onSuccess: () => { setFormError(null); setAmount('10'); setNotes(''); queryClient.invalidateQueries({ queryKey: ['user', userId] }); queryClient.invalidateQueries({ queryKey: ['users'] }); }, onError: (error) => setFormError(getErrorMessage(error)), }); const user = userQuery.data; const apiKeys = apiKeysQuery.data?.items ?? []; const filteredApiKeys = useMemo(() => { const keyword = searchText.trim().toLowerCase(); return apiKeys.filter((item) => { const haystack = [item.name, item.key, item.group?.name].filter(Boolean).join(' ').toLowerCase(); return keyword ? haystack.includes(keyword) : true; }); }, [apiKeys, searchText]); const trendPoints = (usageSnapshotQuery.data?.trend ?? []).map((item) => ({ label: rangeKey === '24h' ? item.date.slice(11, 13) : item.date.slice(5, 10), value: item.total_tokens, })); function submitBalance() { const numericAmount = Number(amount); if (!amount.trim()) { setFormError('请输入金额。'); return; } if (!Number.isFinite(numericAmount) || numericAmount < 0) { setFormError('金额格式不正确。'); return; } balanceMutation.mutate({ amount: numericAmount, notes: notes.trim() || undefined, operation, }); } async function copyKey(item: AdminApiKey) { await Clipboard.setStringAsync(item.key || ''); setCopiedKeyId(item.id); setTimeout(() => { setCopiedKeyId((current) => (current === item.id ? null : current)); }, 1500); } return ( <> {userQuery.isLoading ? (
正在加载用户详情...
) : null} {userQuery.error ? (
用户信息加载失败 {getErrorMessage(userQuery.error)}
) : null} {user ? (
邮箱 {user.email || '--'} 最后使用时间 {formatTime(user.last_used_at || user.updated_at || user.created_at)}
) : null}
{RANGE_OPTIONS.map((item) => { const active = item.key === rangeKey; return ( setRangeKey(item.key)} style={{ backgroundColor: active ? colors.primary : colors.muted, borderRadius: 999, paddingHorizontal: 12, paddingVertical: 8, borderWidth: 1, borderColor: active ? colors.primary : colors.border, }} > {item.label} ); })} {usageStatsQuery.data ? ( 输入 {formatTokenValue(usageStatsQuery.data.total_input_tokens)} · 输出 {formatTokenValue(usageStatsQuery.data.total_output_tokens)} ) : null} {usageStatsQuery.isLoading ? 正在加载用量统计... : null} {usageStatsQuery.error ? ( 用量统计加载失败 {getErrorMessage(usageStatsQuery.error)} ) : null} {!usageSnapshotQuery.isLoading && trendPoints.length > 1 ? ( formatTokenValue(value)} compact /> ) : null} {usageSnapshotQuery.isLoading ? 正在加载趋势图... : null} {usageSnapshotQuery.error ? ( 趋势加载失败 {getErrorMessage(usageSnapshotQuery.error)} ) : null}
{apiKeysQuery.isLoading ? 正在加载 API Keys... : null} {apiKeysQuery.error ? ( API Keys 加载失败 {getErrorMessage(apiKeysQuery.error)} ) : null} {!apiKeysQuery.isLoading && !apiKeysQuery.error ? ( filteredApiKeys.length > 0 ? ( {filteredApiKeys.map((item) => ( copyKey(item)} /> ))} ) : ( 当前筛选条件下没有 Key。 ) ) : null}
{([ { label: '充值', value: 'add' }, { label: '扣减', value: 'subtract' }, { label: '设为', value: 'set' }, ] as const).map((item) => { const active = operation === item.value; return ( setOperation(item.value)} style={{ flex: 1, backgroundColor: active ? colors.primary : colors.muted, borderRadius: 12, paddingVertical: 12, alignItems: 'center', borderWidth: 1, borderColor: active ? colors.primary : colors.border, }} > {item.label} ); })} {formError ? ( {formError} ) : null} {balanceMutation.isPending ? '提交中...' : '确认提交'}
); }