import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { zodResolver } from '@hookform/resolvers/zod'; import * as Clipboard from 'expo-clipboard'; import { ArrowLeftRight, ChevronLeft, Copy, KeyRound, Search, Wallet } from 'lucide-react-native'; import { router, useLocalSearchParams } from 'expo-router'; import { useMemo, useState } from 'react'; import { Controller, useForm } from 'react-hook-form'; import { Pressable, Text, TextInput, View } from 'react-native'; import { z } from 'zod'; import { DetailRow } from '@/src/components/detail-row'; import { ListCard } from '@/src/components/list-card'; import { formatDisplayTime } from '@/src/lib/formatters'; import { ScreenShell } from '@/src/components/screen-shell'; import { useDebouncedValue } from '@/src/hooks/use-debounced-value'; import { getUser, getUserUsage, listUserApiKeys, updateUserBalance } from '@/src/services/admin'; import type { BalanceOperation } from '@/src/types/admin'; const schema = z.object({ amount: z.string().min(1, '请输入金额'), notes: z.string().optional(), }); type FormValues = z.infer; export default function UserDetailScreen() { const { id } = useLocalSearchParams<{ id: string }>(); const userId = Number(id); const queryClient = useQueryClient(); const [operation, setOperation] = useState('add'); const [keySearchText, setKeySearchText] = useState(''); const [statusFilter, setStatusFilter] = useState<'all' | 'active' | 'inactive'>('all'); const [copiedKeyId, setCopiedKeyId] = useState(null); const keySearch = useDebouncedValue(keySearchText.trim().toLowerCase(), 250); const { control, handleSubmit, reset, formState } = useForm({ resolver: zodResolver(schema), defaultValues: { amount: '10', notes: '', }, }); const userQuery = useQuery({ queryKey: ['user', userId], queryFn: () => getUser(userId), enabled: Number.isFinite(userId), }); const usageQuery = useQuery({ queryKey: ['user-usage', userId], queryFn: () => getUserUsage(userId), enabled: Number.isFinite(userId), }); const apiKeysQuery = useQuery({ queryKey: ['user-api-keys', userId], queryFn: () => listUserApiKeys(userId), enabled: Number.isFinite(userId), }); const balanceMutation = useMutation({ mutationFn: (values: FormValues & { operation: BalanceOperation }) => updateUserBalance(userId, { balance: Number(values.amount), operation: values.operation, notes: values.notes, }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['user', userId] }); queryClient.invalidateQueries({ queryKey: ['users'] }); reset({ amount: '10', notes: '' }); }, }); const user = userQuery.data; const usage = usageQuery.data; const apiKeys = apiKeysQuery.data?.items ?? []; const filteredApiKeys = useMemo( () => apiKeys.filter((item) => { const matchesStatus = statusFilter === 'all' ? true : item.status === statusFilter; const matchesSearch = !keySearch ? true : [item.name, item.key, item.group?.name] .filter(Boolean) .join(' ') .toLowerCase() .includes(keySearch); return matchesStatus && matchesSearch; }), [apiKeys, keySearch, statusFilter] ); function maskKey(value: string) { if (!value || value.length < 16) { return value; } return `${value.slice(0, 8)}••••••${value.slice(-8)}`; } async function copyKey(keyId: number, value: string) { await Clipboard.setStringAsync(value); setCopiedKeyId(keyId); setTimeout(() => setCopiedKeyId((current) => (current === keyId ? null : current)), 1600); } return ( 用户余额、用量与密钥概览。} variant="minimal" right={ router.back()}> } > {(['all', 'active', 'inactive'] as const).map((item) => ( setStatusFilter(item)} > {item === 'all' ? '全部' : item === 'active' ? '启用' : '停用'} ))} {filteredApiKeys.map((item) => ( {item.name} {item.status} {maskKey(item.key)} copyKey(item.id, item.key)} > {copiedKeyId === item.id ? '已复制到剪贴板' : `最近使用 ${formatDisplayTime(item.last_used_at)}`} 已用 ${Number(item.quota_used ?? 0).toFixed(2)} / 配额 {item.quota ? `$${Number(item.quota).toFixed(2)}` : '无限制'} 分组 {item.group?.name || '未绑定'} · 5h 用量 {Number(item.usage_5h ?? 0).toFixed(2)} ))} {filteredApiKeys.length === 0 ? 当前筛选条件下没有 API 密钥。 : null} ( )} /> {(['add', 'subtract', 'set'] as BalanceOperation[]).map((item) => ( setOperation(item)} > {item === 'add' ? '增加' : item === 'subtract' ? '扣减' : '设为'} ))} ( )} /> {formState.errors.amount ? {formState.errors.amount.message} : null} balanceMutation.mutate({ ...values, operation }))} > {balanceMutation.isPending ? '提交中...' : operation === 'add' ? '增加余额' : operation === 'subtract' ? '扣减余额' : '设置余额'} ); }