mirror of
https://gitee.com/wanwujie/sub2api-mobile
synced 2026-04-07 17:00:21 +08:00
feat: bootstrap v2 admin app tabs
This commit is contained in:
15
src/components/detail-row.tsx
Normal file
15
src/components/detail-row.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Text, View } from 'react-native';
|
||||
|
||||
type DetailRowProps = {
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export function DetailRow({ label, value }: DetailRowProps) {
|
||||
return (
|
||||
<View className="flex-row items-start justify-between gap-4 border-b border-[#eee6d7] py-3 last:border-b-0">
|
||||
<Text className="text-sm text-[#7d7468]">{label}</Text>
|
||||
<Text className="max-w-[62%] text-right text-sm font-medium text-[#16181a]">{value}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
70
src/components/line-trend-chart.tsx
Normal file
70
src/components/line-trend-chart.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import { Text, View } from 'react-native';
|
||||
import Svg, { Defs, LinearGradient, Path, Stop } from 'react-native-svg';
|
||||
|
||||
type Point = {
|
||||
label: string;
|
||||
value: number;
|
||||
};
|
||||
|
||||
type LineTrendChartProps = {
|
||||
points: Point[];
|
||||
color?: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
formatValue?: (value: number) => string;
|
||||
};
|
||||
|
||||
export function LineTrendChart({
|
||||
points,
|
||||
color = '#1d5f55',
|
||||
title,
|
||||
subtitle,
|
||||
formatValue = (value) => `${value}`,
|
||||
}: LineTrendChartProps) {
|
||||
const width = 320;
|
||||
const height = 160;
|
||||
const maxValue = Math.max(...points.map((point) => point.value), 1);
|
||||
const minValue = Math.min(...points.map((point) => point.value), 0);
|
||||
const range = Math.max(maxValue - minValue, 1);
|
||||
|
||||
const line = points
|
||||
.map((point, index) => {
|
||||
const x = (index / Math.max(points.length - 1, 1)) * width;
|
||||
const y = height - ((point.value - minValue) / range) * (height - 18) - 12;
|
||||
|
||||
return `${index === 0 ? 'M' : 'L'} ${x} ${y}`;
|
||||
})
|
||||
.join(' ');
|
||||
|
||||
const area = `${line} L ${width} ${height} L 0 ${height} Z`;
|
||||
const latest = points[points.length - 1]?.value ?? 0;
|
||||
|
||||
return (
|
||||
<View className="rounded-[28px] bg-[#fbf8f2] p-5">
|
||||
<Text className="text-xs uppercase tracking-[1.6px] text-[#7d7468]">{title}</Text>
|
||||
<Text className="mt-2 text-3xl font-bold text-[#16181a]">{formatValue(latest)}</Text>
|
||||
<Text className="mt-1 text-sm text-[#7d7468]">{subtitle}</Text>
|
||||
|
||||
<View className="mt-5 overflow-hidden rounded-[20px] bg-[#f4efe4] p-3">
|
||||
<Svg width="100%" height={height} viewBox={`0 0 ${width} ${height}`}>
|
||||
<Defs>
|
||||
<LinearGradient id="trendFill" x1="0" x2="0" y1="0" y2="1">
|
||||
<Stop offset="0%" stopColor={color} stopOpacity="0.28" />
|
||||
<Stop offset="100%" stopColor={color} stopOpacity="0.02" />
|
||||
</LinearGradient>
|
||||
</Defs>
|
||||
<Path d={area} fill="url(#trendFill)" />
|
||||
<Path d={line} fill="none" stroke={color} strokeWidth="3" strokeLinejoin="round" strokeLinecap="round" />
|
||||
</Svg>
|
||||
|
||||
<View className="mt-3 flex-row justify-between">
|
||||
{points.map((point) => (
|
||||
<Text key={point.label} className="text-xs text-[#7d7468]">
|
||||
{point.label}
|
||||
</Text>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
33
src/components/list-card.tsx
Normal file
33
src/components/list-card.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import type { LucideIcon } from 'lucide-react-native';
|
||||
import type { ReactNode } from 'react';
|
||||
import { Text, View } from 'react-native';
|
||||
|
||||
type ListCardProps = {
|
||||
title: string;
|
||||
meta?: string;
|
||||
badge?: string;
|
||||
children?: ReactNode;
|
||||
icon?: LucideIcon;
|
||||
};
|
||||
|
||||
export function ListCard({ title, meta, badge, children, icon: Icon }: ListCardProps) {
|
||||
return (
|
||||
<View className="rounded-[24px] bg-[#fbf8f2] p-4">
|
||||
<View className="flex-row items-start justify-between gap-3">
|
||||
<View className="flex-1">
|
||||
<View className="flex-row items-center gap-2">
|
||||
{Icon ? <Icon color="#7d7468" size={16} /> : null}
|
||||
<Text className="text-lg font-semibold text-[#16181a]">{title}</Text>
|
||||
</View>
|
||||
{meta ? <Text className="mt-1 text-sm text-[#7d7468]">{meta}</Text> : null}
|
||||
</View>
|
||||
{badge ? (
|
||||
<View className="rounded-full bg-[#e7dfcf] px-3 py-1">
|
||||
<Text className="text-xs font-semibold uppercase tracking-[1.2px] text-[#5d564d]">{badge}</Text>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
{children ? <View className="mt-4">{children}</View> : null}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
28
src/components/screen-shell.tsx
Normal file
28
src/components/screen-shell.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { PropsWithChildren, ReactNode } from 'react';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { ScrollView, Text, View } from 'react-native';
|
||||
|
||||
type ScreenShellProps = PropsWithChildren<{
|
||||
title: string;
|
||||
subtitle: string;
|
||||
right?: ReactNode;
|
||||
}>;
|
||||
|
||||
export function ScreenShell({ title, subtitle, right, children }: ScreenShellProps) {
|
||||
return (
|
||||
<SafeAreaView className="flex-1 bg-[#f4efe4]">
|
||||
<ScrollView className="flex-1" contentContainerClassName="px-5 pb-24">
|
||||
<View className="mt-4 rounded-[24px] border border-[#e6dece] bg-[#fbf8f2] px-4 py-4">
|
||||
<View className="flex-row items-start justify-between gap-4">
|
||||
<View className="flex-1">
|
||||
<Text className="text-[24px] font-bold tracking-tight text-[#16181a]">{title}</Text>
|
||||
<Text className="mt-1 text-sm leading-6 text-[#7d7468]">{subtitle}</Text>
|
||||
</View>
|
||||
{right}
|
||||
</View>
|
||||
</View>
|
||||
<View className="mt-4 gap-4">{children}</View>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
33
src/components/stat-card.tsx
Normal file
33
src/components/stat-card.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import type { LucideIcon } from 'lucide-react-native';
|
||||
import { TrendingDown, TrendingUp } from 'lucide-react-native';
|
||||
import { Text, View } from 'react-native';
|
||||
|
||||
type StatCardProps = {
|
||||
label: string;
|
||||
value: string;
|
||||
tone?: 'light' | 'dark';
|
||||
trend?: 'up' | 'down';
|
||||
icon?: LucideIcon;
|
||||
};
|
||||
|
||||
export function StatCard({ label, value, tone = 'light', trend, icon: Icon }: StatCardProps) {
|
||||
const dark = tone === 'dark';
|
||||
const TrendIcon = trend === 'up' ? TrendingUp : trend === 'down' ? TrendingDown : null;
|
||||
|
||||
return (
|
||||
<View className={dark ? 'rounded-[24px] bg-[#1d5f55] p-4' : 'rounded-[24px] bg-[#fbf8f2] p-4'}>
|
||||
<View className="flex-row items-center justify-between gap-3">
|
||||
<Text className={dark ? 'text-xs uppercase tracking-[1.5px] text-[#d8efe7]' : 'text-xs uppercase tracking-[1.5px] text-[#7d7468]'}>
|
||||
{label}
|
||||
</Text>
|
||||
<View className="flex-row items-center gap-2">
|
||||
{TrendIcon ? <TrendIcon color={dark ? '#d8efe7' : '#7d7468'} size={14} /> : null}
|
||||
{Icon ? <Icon color={dark ? '#d8efe7' : '#7d7468'} size={14} /> : null}
|
||||
</View>
|
||||
</View>
|
||||
<Text className={dark ? 'mt-3 text-3xl font-bold text-white' : 'mt-3 text-3xl font-bold text-[#16181a]'}>
|
||||
{value}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
2
src/global.css
Normal file
2
src/global.css
Normal file
@@ -0,0 +1,2 @@
|
||||
@import 'tailwindcss';
|
||||
@import 'uniwind';
|
||||
56
src/lib/admin-fetch.ts
Normal file
56
src/lib/admin-fetch.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { adminConfigState } from '@/src/store/admin-config';
|
||||
import type { ApiEnvelope } from '@/src/types/admin';
|
||||
|
||||
function isProxyBaseUrl(baseUrl: string) {
|
||||
return /localhost:8787$/.test(baseUrl) || /127\.0\.0\.1:8787$/.test(baseUrl);
|
||||
}
|
||||
|
||||
export function isLocalProxyBaseUrl(baseUrl: string) {
|
||||
return isProxyBaseUrl(baseUrl);
|
||||
}
|
||||
|
||||
export async function adminFetch<T>(
|
||||
path: string,
|
||||
init: RequestInit = {},
|
||||
options?: { idempotencyKey?: string }
|
||||
): Promise<T> {
|
||||
const baseUrl = adminConfigState.baseUrl.trim().replace(/\/$/, '');
|
||||
const adminApiKey = adminConfigState.adminApiKey.trim();
|
||||
|
||||
if (!baseUrl) {
|
||||
throw new Error('BASE_URL_REQUIRED');
|
||||
}
|
||||
|
||||
if (!adminApiKey && !isProxyBaseUrl(baseUrl)) {
|
||||
throw new Error('ADMIN_API_KEY_REQUIRED');
|
||||
}
|
||||
|
||||
const headers = new Headers(init.headers);
|
||||
headers.set('Content-Type', 'application/json');
|
||||
if (adminApiKey) {
|
||||
headers.set('x-api-key', adminApiKey);
|
||||
}
|
||||
|
||||
if (options?.idempotencyKey) {
|
||||
headers.set('Idempotency-Key', options.idempotencyKey);
|
||||
}
|
||||
|
||||
const response = await fetch(`${baseUrl}${path}`, {
|
||||
...init,
|
||||
headers,
|
||||
});
|
||||
|
||||
let json: ApiEnvelope<T>;
|
||||
|
||||
try {
|
||||
json = (await response.json()) as ApiEnvelope<T>;
|
||||
} catch {
|
||||
throw new Error('INVALID_SERVER_RESPONSE');
|
||||
}
|
||||
|
||||
if (!response.ok || json.code !== 0) {
|
||||
throw new Error(json.reason || json.message || 'REQUEST_FAILED');
|
||||
}
|
||||
|
||||
return json.data as T;
|
||||
}
|
||||
11
src/lib/query-client.ts
Normal file
11
src/lib/query-client.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { QueryClient } from '@tanstack/react-query';
|
||||
|
||||
export const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: 1,
|
||||
staleTime: 30_000,
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
129
src/services/admin.ts
Normal file
129
src/services/admin.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import { adminFetch } from '@/src/lib/admin-fetch';
|
||||
import type {
|
||||
AccountTodayStats,
|
||||
AdminAccount,
|
||||
AdminApiKey,
|
||||
AdminGroup,
|
||||
AdminSettings,
|
||||
AdminUser,
|
||||
BalanceOperation,
|
||||
DashboardModelStats,
|
||||
DashboardStats,
|
||||
DashboardTrend,
|
||||
PaginatedData,
|
||||
UserUsageSummary,
|
||||
} from '@/src/types/admin';
|
||||
|
||||
function buildQuery(params: Record<string, string | number | boolean | undefined>) {
|
||||
const query = new URLSearchParams();
|
||||
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
query.set(key, String(value));
|
||||
}
|
||||
});
|
||||
|
||||
const value = query.toString();
|
||||
|
||||
return value ? `?${value}` : '';
|
||||
}
|
||||
|
||||
export function getDashboardStats() {
|
||||
return adminFetch<DashboardStats>('/api/v1/admin/dashboard/stats');
|
||||
}
|
||||
|
||||
export function getAdminSettings() {
|
||||
return adminFetch<AdminSettings>('/api/v1/admin/settings');
|
||||
}
|
||||
|
||||
export function getDashboardTrend(params: {
|
||||
start_date: string;
|
||||
end_date: string;
|
||||
granularity?: 'day' | 'hour';
|
||||
account_id?: number;
|
||||
group_id?: number;
|
||||
user_id?: number;
|
||||
}) {
|
||||
return adminFetch<DashboardTrend>(`/api/v1/admin/dashboard/trend${buildQuery(params)}`);
|
||||
}
|
||||
|
||||
export function getDashboardModels(params: { start_date: string; end_date: string }) {
|
||||
return adminFetch<DashboardModelStats>(`/api/v1/admin/dashboard/models${buildQuery(params)}`);
|
||||
}
|
||||
|
||||
export function listUsers(search = '') {
|
||||
return adminFetch<PaginatedData<AdminUser>>(
|
||||
`/api/v1/admin/users${buildQuery({ page: 1, page_size: 20, search: search.trim() })}`
|
||||
);
|
||||
}
|
||||
|
||||
export function getUser(userId: number) {
|
||||
return adminFetch<AdminUser>(`/api/v1/admin/users/${userId}`);
|
||||
}
|
||||
|
||||
export function getUserUsage(userId: number, period: 'day' | 'week' | 'month' = 'month') {
|
||||
return adminFetch<UserUsageSummary>(`/api/v1/admin/users/${userId}/usage${buildQuery({ period })}`);
|
||||
}
|
||||
|
||||
export function listUserApiKeys(userId: number) {
|
||||
return adminFetch<PaginatedData<AdminApiKey>>(`/api/v1/admin/users/${userId}/api-keys${buildQuery({ page: 1, page_size: 100 })}`);
|
||||
}
|
||||
|
||||
export function updateUserBalance(
|
||||
userId: number,
|
||||
body: { balance: number; operation: BalanceOperation; notes?: string }
|
||||
) {
|
||||
return adminFetch<AdminUser>(
|
||||
`/api/v1/admin/users/${userId}/balance`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
},
|
||||
{
|
||||
idempotencyKey: `user-balance-${userId}-${Date.now()}`,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function listGroups(search = '') {
|
||||
return adminFetch<PaginatedData<AdminGroup>>(
|
||||
`/api/v1/admin/groups${buildQuery({ page: 1, page_size: 20, search: search.trim() })}`
|
||||
);
|
||||
}
|
||||
|
||||
export function getGroup(groupId: number) {
|
||||
return adminFetch<AdminGroup>(`/api/v1/admin/groups/${groupId}`);
|
||||
}
|
||||
|
||||
export function listAccounts(search = '') {
|
||||
return adminFetch<PaginatedData<AdminAccount>>(
|
||||
`/api/v1/admin/accounts${buildQuery({ page: 1, page_size: 20, search: search.trim() })}`
|
||||
);
|
||||
}
|
||||
|
||||
export function getAccount(accountId: number) {
|
||||
return adminFetch<AdminAccount>(`/api/v1/admin/accounts/${accountId}`);
|
||||
}
|
||||
|
||||
export function getAccountTodayStats(accountId: number) {
|
||||
return adminFetch<AccountTodayStats>(`/api/v1/admin/accounts/${accountId}/today-stats`);
|
||||
}
|
||||
|
||||
export function testAccount(accountId: number) {
|
||||
return adminFetch(`/api/v1/admin/accounts/${accountId}/test`, {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
export function refreshAccount(accountId: number) {
|
||||
return adminFetch(`/api/v1/admin/accounts/${accountId}/refresh`, {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
export function setAccountSchedulable(accountId: number, schedulable: boolean) {
|
||||
return adminFetch<AdminAccount>(`/api/v1/admin/accounts/${accountId}/schedulable`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ schedulable }),
|
||||
});
|
||||
}
|
||||
71
src/store/admin-config.ts
Normal file
71
src/store/admin-config.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import * as SecureStore from 'expo-secure-store';
|
||||
const { proxy } = require('valtio');
|
||||
|
||||
const BASE_URL_KEY = 'sub2api_base_url';
|
||||
const ADMIN_KEY_KEY = 'sub2api_admin_api_key';
|
||||
|
||||
export function getDefaultAdminConfig() {
|
||||
return {
|
||||
baseUrl: '',
|
||||
adminApiKey: '',
|
||||
};
|
||||
}
|
||||
|
||||
async function getItem(key: string) {
|
||||
if (typeof window !== 'undefined') {
|
||||
if (typeof localStorage === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return localStorage.getItem(key);
|
||||
}
|
||||
|
||||
return SecureStore.getItemAsync(key);
|
||||
}
|
||||
|
||||
async function setItem(key: string, value: string) {
|
||||
if (typeof window !== 'undefined') {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.setItem(key, value);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await SecureStore.setItemAsync(key, value);
|
||||
}
|
||||
|
||||
export const adminConfigState = proxy({
|
||||
...getDefaultAdminConfig(),
|
||||
hydrated: false,
|
||||
saving: false,
|
||||
});
|
||||
|
||||
export async function hydrateAdminConfig() {
|
||||
const defaults = getDefaultAdminConfig();
|
||||
const [baseUrl, adminApiKey] = await Promise.all([
|
||||
getItem(BASE_URL_KEY),
|
||||
getItem(ADMIN_KEY_KEY),
|
||||
]);
|
||||
|
||||
adminConfigState.baseUrl = baseUrl ?? defaults.baseUrl;
|
||||
adminConfigState.adminApiKey = adminApiKey ?? defaults.adminApiKey;
|
||||
|
||||
adminConfigState.hydrated = true;
|
||||
}
|
||||
|
||||
export async function saveAdminConfig(input: { baseUrl: string; adminApiKey: string }) {
|
||||
adminConfigState.saving = true;
|
||||
|
||||
const nextBaseUrl = input.baseUrl.trim().replace(/\/$/, '');
|
||||
const nextAdminApiKey = input.adminApiKey.trim();
|
||||
|
||||
await Promise.all([
|
||||
setItem(BASE_URL_KEY, nextBaseUrl),
|
||||
setItem(ADMIN_KEY_KEY, nextAdminApiKey),
|
||||
]);
|
||||
|
||||
adminConfigState.baseUrl = nextBaseUrl;
|
||||
adminConfigState.adminApiKey = nextAdminApiKey;
|
||||
adminConfigState.saving = false;
|
||||
}
|
||||
174
src/types/admin.ts
Normal file
174
src/types/admin.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
export type ApiEnvelope<T> = {
|
||||
code: number;
|
||||
message: string;
|
||||
reason?: string;
|
||||
metadata?: Record<string, string>;
|
||||
data?: T;
|
||||
};
|
||||
|
||||
export type PaginatedData<T> = {
|
||||
items: T[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
pages: number;
|
||||
};
|
||||
|
||||
export type DashboardStats = {
|
||||
total_users: number;
|
||||
today_new_users: number;
|
||||
active_users: number;
|
||||
total_api_keys: number;
|
||||
active_api_keys: number;
|
||||
total_accounts: number;
|
||||
normal_accounts: number;
|
||||
error_accounts: number;
|
||||
total_requests: number;
|
||||
total_cost: number;
|
||||
total_tokens: number;
|
||||
today_requests: number;
|
||||
today_cost: number;
|
||||
today_tokens: number;
|
||||
today_input_tokens?: number;
|
||||
today_output_tokens?: number;
|
||||
today_cache_read_tokens?: number;
|
||||
rpm: number;
|
||||
tpm: number;
|
||||
};
|
||||
|
||||
export type TrendPoint = {
|
||||
date: string;
|
||||
requests: number;
|
||||
input_tokens: number;
|
||||
output_tokens: number;
|
||||
cache_creation_tokens: number;
|
||||
cache_read_tokens: number;
|
||||
total_tokens: number;
|
||||
cost: number;
|
||||
actual_cost: number;
|
||||
};
|
||||
|
||||
export type DashboardTrend = {
|
||||
start_date: string;
|
||||
end_date: string;
|
||||
granularity: 'day' | 'hour' | string;
|
||||
trend: TrendPoint[];
|
||||
};
|
||||
|
||||
export type ModelStat = {
|
||||
model: string;
|
||||
requests: number;
|
||||
input_tokens: number;
|
||||
output_tokens: number;
|
||||
cache_creation_tokens: number;
|
||||
cache_read_tokens: number;
|
||||
total_tokens: number;
|
||||
cost: number;
|
||||
actual_cost: number;
|
||||
};
|
||||
|
||||
export type DashboardModelStats = {
|
||||
start_date: string;
|
||||
end_date: string;
|
||||
models: ModelStat[];
|
||||
};
|
||||
|
||||
export type AdminSettings = {
|
||||
site_name?: string;
|
||||
[key: string]: string | number | boolean | null | string[] | undefined;
|
||||
};
|
||||
|
||||
export type AdminUser = {
|
||||
id: number;
|
||||
email: string;
|
||||
username?: string | null;
|
||||
balance?: number;
|
||||
concurrency?: number;
|
||||
status?: string;
|
||||
role?: string;
|
||||
current_concurrency?: number;
|
||||
notes?: string | null;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
};
|
||||
|
||||
export type UserUsageSummary = {
|
||||
total_requests?: number;
|
||||
total_tokens?: number;
|
||||
total_cost?: number;
|
||||
requests?: number;
|
||||
tokens?: number;
|
||||
cost?: number;
|
||||
[key: string]: string | number | boolean | null | undefined;
|
||||
};
|
||||
|
||||
export type AdminApiKey = {
|
||||
id: number;
|
||||
user_id: number;
|
||||
key: string;
|
||||
name: string;
|
||||
group_id?: number | null;
|
||||
status: string;
|
||||
quota: number;
|
||||
quota_used: number;
|
||||
last_used_at?: string | null;
|
||||
expires_at?: string | null;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
usage_5h?: number;
|
||||
usage_1d?: number;
|
||||
usage_7d?: number;
|
||||
group?: AdminGroup;
|
||||
user?: {
|
||||
id: number;
|
||||
email?: string;
|
||||
username?: string | null;
|
||||
};
|
||||
};
|
||||
|
||||
export type BalanceOperation = 'set' | 'add' | 'subtract';
|
||||
|
||||
export type AdminGroup = {
|
||||
id: number;
|
||||
name: string;
|
||||
description?: string | null;
|
||||
platform: string;
|
||||
rate_multiplier?: number;
|
||||
is_exclusive?: boolean;
|
||||
status?: string;
|
||||
subscription_type?: string;
|
||||
daily_limit_usd?: number | null;
|
||||
weekly_limit_usd?: number | null;
|
||||
monthly_limit_usd?: number | null;
|
||||
account_count?: number;
|
||||
sort_order?: number;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
};
|
||||
|
||||
export type AccountTodayStats = {
|
||||
requests: number;
|
||||
tokens: number;
|
||||
cost: number;
|
||||
standard_cost?: number;
|
||||
user_cost?: number;
|
||||
};
|
||||
|
||||
export type AdminAccount = {
|
||||
id: number;
|
||||
name: string;
|
||||
platform: string;
|
||||
type: string;
|
||||
status?: string;
|
||||
schedulable?: boolean;
|
||||
priority?: number;
|
||||
concurrency?: number;
|
||||
current_concurrency?: number;
|
||||
rate_multiplier?: number;
|
||||
error_message?: string;
|
||||
updated_at?: string;
|
||||
last_used_at?: string | null;
|
||||
group_ids?: number[];
|
||||
groups?: AdminGroup[];
|
||||
extra?: Record<string, string | number | boolean | null>;
|
||||
};
|
||||
9
src/uniwind-env.d.ts
vendored
Normal file
9
src/uniwind-env.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
import 'react-native';
|
||||
import 'react-native-safe-area-context';
|
||||
import 'uniwind/types';
|
||||
|
||||
declare module 'react-native-safe-area-context' {
|
||||
interface NativeSafeAreaViewProps {
|
||||
className?: string;
|
||||
}
|
||||
}
|
||||
10
src/uniwind-types.d.ts
vendored
Normal file
10
src/uniwind-types.d.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
// NOTE: This file is generated by uniwind and it should not be edited manually.
|
||||
/// <reference types="uniwind/types" />
|
||||
|
||||
declare module 'uniwind' {
|
||||
export interface UniwindConfig {
|
||||
themes: readonly ['light', 'dark']
|
||||
}
|
||||
}
|
||||
|
||||
export {}
|
||||
Reference in New Issue
Block a user