import { useQuery } from '@tanstack/react-query';
import { router } from 'expo-router';
import { useMemo, useState } from 'react';
import { FlatList, Pressable, RefreshControl, Text, TextInput, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useDebouncedValue } from '@/src/hooks/use-debounced-value';
import { queryClient } from '@/src/lib/query-client';
import { getUser, listUserApiKeys, listUsers } from '@/src/services/admin';
import { adminConfigState } from '@/src/store/admin-config';
import type { AdminUser } from '@/src/types/admin';
const { useSnapshot } = require('valtio/react');
const colors = {
page: '#f4efe4',
card: '#fbf8f2',
mutedCard: '#f1ece2',
primary: '#1d5f55',
text: '#16181a',
subtext: '#6f665c',
dangerBg: '#fbf1eb',
danger: '#c25d35',
accentBg: '#efe4cf',
accentText: '#8c5a22',
};
type SortOrder = 'desc' | 'asc';
function formatBalance(value?: number) {
if (typeof value !== 'number' || Number.isNaN(value)) return '$0.00';
return `$${value.toFixed(2)}`;
}
function formatActivityTime(value?: string) {
if (!value) return '时间未知';
const date = new Date(value);
if (Number.isNaN(date.getTime())) return '时间未知';
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return `${year}/${month}/${day} ${hours}:${minutes}:${seconds}`;
}
function toTimeValue(value?: string | null) {
if (!value) return 0;
const time = new Date(value).getTime();
return Number.isNaN(time) ? 0 : time;
}
function getTimeValue(user: AdminUser) {
return toTimeValue(user.last_used_at) || toTimeValue(user.updated_at) || toTimeValue(user.created_at) || user.id || 0;
}
function getUserNameLabel(user: AdminUser) {
if (user.username?.trim()) return user.username.trim();
if (user.notes?.trim()) return user.notes.trim();
return user.email.split('@')[0] || '未命名';
}
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 '当前无法加载页面数据,请检查服务地址、Token 和网络。';
}
function MetricTile({ title, value, tone = 'default' }: { title: string; value: string; tone?: 'default' | 'accent' }) {
const backgroundColor = tone === 'accent' ? colors.accentBg : colors.mutedCard;
const valueColor = tone === 'accent' ? colors.accentText : colors.text;
return (
{title}
{value}
);
}
function UserCard({ user }: { user: AdminUser }) {
const isAdmin = user.role?.trim().toLowerCase() === 'admin';
const statusLabel = `${isAdmin ? 'admin · ' : ''}${user.status || 'active'}`;
return (
{user.email}
最近使用 {formatActivityTime(user.last_used_at || user.updated_at || user.created_at)}
{statusLabel}
);
}
export default function UsersScreen() {
const config = useSnapshot(adminConfigState);
const hasAccount = Boolean(config.baseUrl.trim());
const [searchText, setSearchText] = useState('');
const [sortOrder, setSortOrder] = useState('desc');
const debouncedSearchText = useDebouncedValue(searchText, 250);
const usersQuery = useQuery({
queryKey: ['users', debouncedSearchText],
queryFn: () => listUsers(debouncedSearchText),
enabled: hasAccount,
});
const users = useMemo(() => {
const items = [...(usersQuery.data?.items ?? [])];
items.sort((left, right) => {
const value = getTimeValue(left) - getTimeValue(right);
return sortOrder === 'desc' ? -value : value;
});
return items;
}, [sortOrder, usersQuery.data?.items]);
const errorMessage = getErrorMessage(usersQuery.error);
return (
用户
查看用户列表并进入详情页管理账号。
setSortOrder((value) => (value === 'desc' ? 'asc' : 'desc'))}
style={{ backgroundColor: colors.card, borderRadius: 16, paddingHorizontal: 14, paddingVertical: 14, minWidth: 92, alignItems: 'center' }}
>
时间
{sortOrder === 'desc' ? '倒序' : '正序'}
{!hasAccount ? (
未连接服务器
请先到“服务器”页完成连接,再查看用户列表。
router.push('/settings')}
>
去配置服务器
) : usersQuery.isLoading ? (
正在加载用户
已连接服务器,正在拉取用户列表。
) : usersQuery.error ? (
加载失败
{errorMessage}
) : (
`${item.id}`}
showsVerticalScrollIndicator={false}
refreshControl={ void usersQuery.refetch()} tintColor="#1d5f55" />}
contentContainerStyle={{ paddingBottom: 8, gap: 12, flexGrow: users.length === 0 ? 1 : 0 }}
ListEmptyComponent={
暂无用户
当前搜索条件下没有匹配结果,可以修改关键词后重试。
}
renderItem={({ item }) => (
{
void queryClient.prefetchQuery({ queryKey: ['user', item.id], queryFn: () => getUser(item.id) });
void queryClient.prefetchQuery({ queryKey: ['user-api-keys', item.id], queryFn: () => listUserApiKeys(item.id) });
router.push(`/users/${item.id}`);
}}
>
)}
/>
)}
);
}