feat: add dedicated create-user and create-account admin flows

This commit is contained in:
xuhongbin
2026-03-09 19:06:02 +08:00
parent 58393a6730
commit 293b62b444
13 changed files with 1576 additions and 57 deletions

View File

@@ -198,7 +198,17 @@ export default function AccountsScreen() {
<ScreenShell
title="账号管理"
subtitle="看单账号状态、并发、最近使用和异常信息。"
titleAside={<Text className="text-[11px] text-[#a2988a]"></Text>}
titleAside={(
<View className="flex-row items-center gap-2">
<Text className="text-[11px] text-[#a2988a]"></Text>
<Pressable
onPress={() => router.push('/accounts/create')}
className="h-8 w-8 items-center justify-center rounded-[10px] bg-[#1d5f55]"
>
<Text className="text-xl leading-5 text-white">+</Text>
</Pressable>
</View>
)}
variant="minimal"
scroll={false}
>

View File

@@ -8,7 +8,7 @@ import { BarChartCard } from '@/src/components/bar-chart-card';
import { formatTokenValue } from '@/src/lib/formatters';
import { DonutChartCard } from '@/src/components/donut-chart-card';
import { LineTrendChart } from '@/src/components/line-trend-chart';
import { getAdminSettings, getDashboardModels, getDashboardStats, getDashboardTrend, listAllAccounts } from '@/src/services/admin';
import { getAdminSettings, getDashboardModels, getDashboardStats, getDashboardTrend, listAccounts } from '@/src/services/admin';
import { adminConfigState, hasAuthenticatedAdminSession } from '@/src/store/admin-config';
const { useSnapshot } = require('valtio/react');
@@ -172,23 +172,37 @@ export default function MonitorScreen() {
const [rangeKey, setRangeKey] = useState<RangeKey>('7d');
const range = useMemo(() => getDateRange(rangeKey), [rangeKey]);
const statsQuery = useQuery({ queryKey: ['monitor-stats'], queryFn: getDashboardStats, enabled: hasAccount });
const settingsQuery = useQuery({ queryKey: ['admin-settings'], queryFn: getAdminSettings, enabled: hasAccount });
const accountPageSize = Math.max(statsQuery.data?.total_accounts ?? 20, 20);
const accountsQuery = useQuery({
queryKey: ['monitor-accounts', accountPageSize],
queryFn: () => listAllAccounts(''),
const statsQuery = useQuery({
queryKey: ['monitor-stats'],
queryFn: getDashboardStats,
enabled: hasAccount,
staleTime: 60_000,
});
const settingsQuery = useQuery({
queryKey: ['admin-settings'],
queryFn: getAdminSettings,
enabled: hasAccount,
staleTime: 120_000,
});
const accountsQuery = useQuery({
queryKey: ['monitor-accounts'],
queryFn: () => listAccounts(''),
enabled: hasAccount,
staleTime: 60_000,
});
const trendQuery = useQuery({
queryKey: ['monitor-trend', rangeKey, range.start_date, range.end_date, range.granularity],
queryFn: () => getDashboardTrend(range),
enabled: hasAccount,
staleTime: 60_000,
placeholderData: (previousData) => previousData,
});
const modelsQuery = useQuery({
queryKey: ['monitor-models', rangeKey, range.start_date, range.end_date],
queryFn: () => getDashboardModels(range),
enabled: hasAccount,
staleTime: 60_000,
placeholderData: (previousData) => previousData,
});
function refetchAll() {
@@ -381,22 +395,11 @@ export default function MonitorScreen() {
formatValue={formatCompactNumber}
/>
<Section title="趋势摘要" subtitle="图表 + 最近几个统计点的请求、Token 和成本变化">
<Section title="趋势摘要" subtitle="最近几个统计点的请求、Token 和成本变化">
{latestTrendPoints.length === 0 ? (
<Text style={{ fontSize: 14, color: colors.subtext }}></Text>
) : (
<View style={{ gap: 12 }}>
{throughputPoints.length > 1 ? (
<LineTrendChart
title="摘要 Token 趋势"
subtitle="最近统计点的 Token 变化"
points={throughputPoints.slice(-6)}
color="#a34d2d"
formatValue={formatTokenDisplay}
compact
/>
) : null}
<View style={{ gap: 10 }}>
{latestTrendPoints.map((point) => (
<View key={point.date} style={{ backgroundColor: colors.mutedCard, borderRadius: 14, padding: 12 }}>

View File

@@ -109,6 +109,7 @@ export default function SettingsScreen() {
const [connectionState, setConnectionState] = useState<ConnectionState>('idle');
const [connectionMessage, setConnectionMessage] = useState('');
const [isRefreshing, setIsRefreshing] = useState(false);
const [showAdminKey, setShowAdminKey] = useState(false);
const { control, handleSubmit, formState, reset } = useForm<FormValues>({
resolver: zodResolver(schema),
defaultValues: {
@@ -227,15 +228,32 @@ export default function SettingsScreen() {
control={control}
name="adminApiKey"
render={({ field: { onChange, value } }) => (
<TextInput
value={value}
onChangeText={onChange}
placeholder="admin-xxxxxxxx"
placeholderTextColor="#9b9081"
autoCapitalize="none"
autoCorrect={false}
style={{ backgroundColor: colors.mutedCard, borderRadius: 16, paddingHorizontal: 16, paddingVertical: 14, fontSize: 16, color: colors.text }}
/>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
<TextInput
value={value}
onChangeText={onChange}
placeholder="admin-xxxxxxxx"
placeholderTextColor="#9b9081"
autoCapitalize="none"
autoCorrect={false}
secureTextEntry={!showAdminKey}
style={{
flex: 1,
backgroundColor: colors.mutedCard,
borderRadius: 16,
paddingHorizontal: 16,
paddingVertical: 14,
fontSize: 16,
color: colors.text,
}}
/>
<Pressable
onPress={() => setShowAdminKey((value) => !value)}
style={{ backgroundColor: colors.border, borderRadius: 12, paddingHorizontal: 12, paddingVertical: 10 }}
>
<Text style={{ fontSize: 12, fontWeight: '700', color: '#4e463e' }}>{showAdminKey ? '隐藏' : '显示'}</Text>
</Pressable>
</View>
)}
/>
</View>

View File

@@ -130,7 +130,7 @@ function UserCard({ user, usage }: { user: AdminUser; usage?: UsageStats }) {
<Text numberOfLines={1} style={{ fontSize: 16, fontWeight: '800', color: colors.text }}>{user.email}</Text>
<Text style={{ marginTop: 4, fontSize: 12, color: colors.subtext }}>使 {formatActivityTime(user.last_used_at || user.updated_at || user.created_at)}</Text>
</View>
<View style={{ alignSelf: 'flex-start', backgroundColor: user.status === 'inactive' ? '#cfc5b7' : colors.primary, borderRadius: 999, paddingHorizontal: 10, paddingVertical: 6 }}>
<View style={{ alignSelf: 'flex-start', backgroundColor: user.status === 'inactive' || user.status === 'disabled' ? '#cfc5b7' : colors.primary, borderRadius: 999, paddingHorizontal: 10, paddingVertical: 6 }}>
<Text style={{ fontSize: 10, fontWeight: '700', color: '#fff' }}>{statusLabel}</Text>
</View>
</View>
@@ -187,9 +187,24 @@ export default function UsersScreen() {
return (
<SafeAreaView style={{ flex: 1, backgroundColor: colors.page }}>
<View style={{ flex: 1, paddingHorizontal: 16, paddingTop: 14 }}>
<View style={{ marginBottom: 10 }}>
<Text style={{ fontSize: 28, fontWeight: '700', color: colors.text }}></Text>
<Text style={{ marginTop: 4, fontSize: 12, color: '#8a8072' }}></Text>
<View style={{ marginBottom: 10, flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', gap: 12 }}>
<View style={{ flex: 1 }}>
<Text style={{ fontSize: 28, fontWeight: '700', color: colors.text }}></Text>
<Text style={{ marginTop: 4, fontSize: 12, color: '#8a8072' }}></Text>
</View>
<Pressable
onPress={() => router.push('/users/create-user')}
style={{
width: 40,
height: 40,
borderRadius: 12,
backgroundColor: colors.primary,
alignItems: 'center',
justifyContent: 'center',
}}
>
<Text style={{ color: '#fff', fontSize: 24, lineHeight: 24, fontWeight: '500' }}>+</Text>
</Pressable>
</View>
<View style={{ flexDirection: 'row', gap: 10, alignItems: 'center' }}>