diff --git a/src-tauri/src/commands/token_commands.rs b/src-tauri/src/commands/token_commands.rs index bd95837..05502ca 100644 --- a/src-tauri/src/commands/token_commands.rs +++ b/src-tauri/src/commands/token_commands.rs @@ -4,7 +4,8 @@ use ::duckcoding::models::provider::Provider; use ::duckcoding::models::remote_token::{ - CreateRemoteTokenRequest, RemoteToken, RemoteTokenGroup, UpdateRemoteTokenRequest, + CreateRemoteTokenRequest, RemoteToken, RemoteTokenGroup, TokenListData, + UpdateRemoteTokenRequest, }; use ::duckcoding::services::profile_manager::types::TokenImportStatus; use ::duckcoding::services::{ @@ -27,11 +28,18 @@ pub async fn check_token_import_status( .map_err(|e| e.to_string()) } -/// 获取指定供应商的远程令牌列表 +/// 获取指定供应商的远程令牌列表(支持分页) #[tauri::command] -pub async fn fetch_provider_tokens(provider: Provider) -> Result, String> { +pub async fn fetch_provider_tokens( + provider: Provider, + page: i32, + page_size: i32, +) -> Result { let client = NewApiClient::new(provider).map_err(|e| e.to_string())?; - client.list_tokens().await.map_err(|e| e.to_string()) + client + .list_tokens(page, page_size) + .await + .map_err(|e| e.to_string()) } /// 获取指定供应商的令牌分组列表 diff --git a/src-tauri/src/models/remote_token.rs b/src-tauri/src/models/remote_token.rs index f051d3e..3149254 100644 --- a/src-tauri/src/models/remote_token.rs +++ b/src-tauri/src/models/remote_token.rs @@ -119,7 +119,7 @@ pub struct NewApiResponse { } /// NEW API 令牌列表响应的 data 部分 -#[derive(Debug, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct TokenListData { pub page: i32, pub page_size: i32, diff --git a/src-tauri/src/services/new_api/client.rs b/src-tauri/src/services/new_api/client.rs index 29dc95e..60f03b4 100644 --- a/src-tauri/src/services/new_api/client.rs +++ b/src-tauri/src/services/new_api/client.rs @@ -49,9 +49,14 @@ impl NewApiClient { headers } - /// 获取所有远程令牌列表 - pub async fn list_tokens(&self) -> Result> { - let url = format!("{}/api/token", self.base_url()); + /// 获取远程令牌列表(支持分页) + pub async fn list_tokens(&self, page: i32, page_size: i32) -> Result { + let url = format!( + "{}/api/token?p={}&page_size={}", + self.base_url(), + page, + page_size + ); let response = self .client .get(&url) @@ -82,14 +87,19 @@ impl NewApiClient { } // 标准化 API Key,确保所有令牌都有 sk- 前缀 - let mut tokens = api_response.data.map(|d| d.items).unwrap_or_default(); - for token in &mut tokens { + let mut data = api_response.data.unwrap_or_else(|| TokenListData { + page, + page_size, + total: 0, + items: vec![], + }); + for token in &mut data.items { if !token.key.starts_with("sk-") { token.key = format!("sk-{}", token.key); } } - Ok(tokens) + Ok(data) } /// 获取所有令牌分组 diff --git a/src/lib/tauri-commands/token.ts b/src/lib/tauri-commands/token.ts index ad265fd..03192b0 100644 --- a/src/lib/tauri-commands/token.ts +++ b/src/lib/tauri-commands/token.ts @@ -9,14 +9,19 @@ import type { RemoteToken, RemoteTokenGroup, TokenImportStatus, + TokenListResponse, UpdateRemoteTokenRequest, } from '@/types/remote-token'; /** - * 获取指定供应商的远程令牌列表 + * 获取指定供应商的远程令牌列表(支持分页) */ -export async function fetchProviderTokens(provider: Provider): Promise { - return invoke('fetch_provider_tokens', { provider }); +export async function fetchProviderTokens( + provider: Provider, + page = 1, + pageSize = 10, +): Promise { + return invoke('fetch_provider_tokens', { provider, page, pageSize }); } /** diff --git a/src/pages/ProfileManagementPage/components/ImportFromProviderDialog.tsx b/src/pages/ProfileManagementPage/components/ImportFromProviderDialog.tsx index a656960..76cd500 100644 --- a/src/pages/ProfileManagementPage/components/ImportFromProviderDialog.tsx +++ b/src/pages/ProfileManagementPage/components/ImportFromProviderDialog.tsx @@ -157,7 +157,7 @@ export const ImportFromProviderDialog = forwardRef< setLoadingTokens(true); const result = await fetchProviderTokens(provider); // 自动为没有 sk- 前缀的令牌添加前缀 - const normalizedTokens = result.map((token) => ({ + const normalizedTokens = result.items.map((token: RemoteToken) => ({ ...token, key: token.key.startsWith('sk-') ? token.key : `sk-${token.key}`, })); @@ -325,7 +325,7 @@ export const ImportFromProviderDialog = forwardRef< // 重新加载令牌列表并获取最新数据 const updatedTokens = await fetchProviderTokens(selectedProvider); // 自动为没有 sk- 前缀的令牌添加前缀 - const normalizedTokens = updatedTokens.map((token) => ({ + const normalizedTokens = updatedTokens.items.map((token: RemoteToken) => ({ ...token, key: token.key.startsWith('sk-') ? token.key : `sk-${token.key}`, })); @@ -337,7 +337,7 @@ export const ImportFromProviderDialog = forwardRef< ? result.api_key : `sk-${result.api_key}`; if (normalizedTokens.length > 0) { - const newToken = normalizedTokens.find((t) => t.key === normalizedApiKey); + const newToken = normalizedTokens.find((t: RemoteToken) => t.key === normalizedApiKey); if (newToken) { setTokenId(newToken.id); } else { @@ -531,15 +531,15 @@ export const ImportFromProviderDialog = forwardRef< // 重新获取令牌列表 const updatedTokens = await fetchProviderTokens(selectedProvider); // 自动为没有 sk- 前缀的令牌添加前缀 - const normalizedTokens = updatedTokens.map((token) => ({ + const normalizedTokens = updatedTokens.items.map((token: RemoteToken) => ({ ...token, key: token.key.startsWith('sk-') ? token.key : `sk-${token.key}`, })); // 按 ID 降序排序,找到名称匹配的第一个(最新创建的) const sortedTokens = normalizedTokens - .filter((t) => t.name === newTokenName) - .sort((a, b) => b.id - a.id); + .filter((t: RemoteToken) => t.name === newTokenName) + .sort((a: RemoteToken, b: RemoteToken) => b.id - a.id); if (sortedTokens.length === 0) { toast({ diff --git a/src/pages/ProfileManagementPage/components/TokenDetailCard.tsx b/src/pages/ProfileManagementPage/components/TokenDetailCard.tsx index 4a9553b..3217857 100644 --- a/src/pages/ProfileManagementPage/components/TokenDetailCard.tsx +++ b/src/pages/ProfileManagementPage/components/TokenDetailCard.tsx @@ -30,10 +30,10 @@ export function TokenDetailCard({ token, group }: TokenDetailCardProps) { }; /** - * 格式化额度(microdollars 转美元) + * 格式化额度(500000 = $1) */ - const formatQuota = (microdollars: number): string => { - return `$${(microdollars / 1000000).toFixed(2)}`; + const formatQuota = (quota: number): string => { + return `$${(quota / 500000).toFixed(2)}`; }; return ( @@ -57,20 +57,16 @@ export function TokenDetailCard({ token, group }: TokenDetailCardProps) { - {/* 剩余额度 */} + {/* 剩余额度 / 总额度 */}
- 剩余额度: + 额度: - {token.unlimited_quota ? '无限' : formatQuota(token.remain_quota)} + {token.unlimited_quota + ? '无限' + : `${formatQuota(token.remain_quota)} / ${formatQuota(token.remain_quota + token.used_quota)}`}
- {/* 已用额度 */} -
- 已用额度: - {formatQuota(token.used_quota)} -
- {/* 过期时间 */}
过期时间: diff --git a/src/pages/ProviderManagementPage/components/ImportTokenDialog.tsx b/src/pages/ProviderManagementPage/components/ImportTokenDialog.tsx index 37be58c..9008884 100644 --- a/src/pages/ProviderManagementPage/components/ImportTokenDialog.tsx +++ b/src/pages/ProviderManagementPage/components/ImportTokenDialog.tsx @@ -211,9 +211,11 @@ export function ImportTokenDialog({ {token.group}
- 剩余额度: + 额度: - {token.unlimited_quota ? '无限' : `$${(token.remain_quota / 1000000).toFixed(2)}`} + {token.unlimited_quota + ? '无限' + : `$${(token.remain_quota / 500000).toFixed(2)} / $${((token.remain_quota + token.used_quota) / 500000).toFixed(2)}`}
diff --git a/src/pages/ProviderManagementPage/components/RemoteTokenManagement.tsx b/src/pages/ProviderManagementPage/components/RemoteTokenManagement.tsx index d98ec56..6c8db30 100644 --- a/src/pages/ProviderManagementPage/components/RemoteTokenManagement.tsx +++ b/src/pages/ProviderManagementPage/components/RemoteTokenManagement.tsx @@ -23,7 +23,16 @@ import { AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'; -import { Loader2, Plus, Download, Trash2, RefreshCw, Pencil } from 'lucide-react'; +import { + Loader2, + Plus, + Download, + Trash2, + RefreshCw, + Pencil, + ChevronLeft, + ChevronRight, +} from 'lucide-react'; import type { Provider } from '@/types/provider'; import type { RemoteToken } from '@/types/remote-token'; import { TOKEN_STATUS_TEXT, TOKEN_STATUS_VARIANT, TokenStatus } from '@/types/remote-token'; @@ -39,6 +48,8 @@ interface RemoteTokenManagementProps { /** * 远程令牌管理组件 */ +const PAGE_SIZE = 10; + export function RemoteTokenManagement({ provider }: RemoteTokenManagementProps) { const { toast } = useToast(); const [tokens, setTokens] = useState([]); @@ -52,28 +63,43 @@ export function RemoteTokenManagement({ provider }: RemoteTokenManagementProps) const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [deletingToken, setDeletingToken] = useState(null); const [deleting, setDeleting] = useState(false); + const [page, setPage] = useState(1); + const [total, setTotal] = useState(0); + + const totalPages = Math.ceil(total / PAGE_SIZE); /** * 加载令牌列表 */ - const loadTokens = useCallback(async () => { - setLoading(true); - setError(null); - try { - const tokensResult = await fetchProviderTokens(provider); - setTokens(tokensResult); - } catch (err) { - const errorMsg = err instanceof Error ? err.message : String(err); - setError(errorMsg); - toast({ - title: '加载失败', - description: errorMsg, - variant: 'destructive', - }); - } finally { - setLoading(false); + const loadTokens = useCallback( + async (targetPage: number = page) => { + setLoading(true); + setError(null); + try { + const result = await fetchProviderTokens(provider, targetPage, PAGE_SIZE); + setTokens(result.items); + setTotal(result.total); + setPage(result.page); + } catch (err) { + const errorMsg = err instanceof Error ? err.message : String(err); + setError(errorMsg); + toast({ + title: '加载失败', + description: errorMsg, + variant: 'destructive', + }); + } finally { + setLoading(false); + } + }, + [provider, page, toast], + ); + + const handlePageChange = (newPage: number) => { + if (newPage >= 1 && newPage <= totalPages) { + loadTokens(newPage); } - }, [provider, toast]); + }; /** * 打开删除确认对话框 @@ -98,7 +124,11 @@ export function RemoteTokenManagement({ provider }: RemoteTokenManagementProps) }); setDeleteDialogOpen(false); setDeletingToken(null); - await loadTokens(); + if (tokens.length === 1 && page > 1) { + await loadTokens(page - 1); + } else { + await loadTokens(); + } } catch (err) { const errorMsg = err instanceof Error ? err.message : String(err); toast({ @@ -146,19 +176,22 @@ export function RemoteTokenManagement({ provider }: RemoteTokenManagementProps) }; /** - * 格式化额度 + * 格式化额度(剩余/总额) */ - const formatQuota = (quota: number, unlimited: boolean) => { + const formatQuota = (remainQuota: number, usedQuota: number, unlimited: boolean) => { if (unlimited) return '无限'; - return `$${(quota / 1000000).toFixed(2)}`; + const remain = (remainQuota / 500000).toFixed(2); + const total = ((remainQuota + usedQuota) / 500000).toFixed(2); + return `$${remain} / $${total}`; }; /** * 组件加载时获取令牌列表 */ useEffect(() => { - loadTokens(); - }, [loadTokens]); + loadTokens(1); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [provider]); return (
@@ -195,77 +228,108 @@ export function RemoteTokenManagement({ provider }: RemoteTokenManagementProps)

暂无令牌,请点击「创建令牌」按钮添加

) : ( -
- - - - 名称 - 分组 - 剩余额度 - 状态 - 过期时间 - 操作 - - - - {tokens.map((token) => ( - - {/* 名称 */} - {token.name} +
+
+
+ + + 名称 + 分组 + 额度 + 状态 + 过期时间 + 操作 + + + + {tokens.map((token) => ( + + {/* 名称 */} + {token.name} - {/* 分组 */} - {token.group} + {/* 分组 */} + {token.group} - {/* 剩余额度 */} - - {formatQuota(token.remain_quota, token.unlimited_quota)} - + {/* 额度 */} + + {formatQuota(token.remain_quota, token.used_quota, token.unlimited_quota)} + - {/* 状态 */} - - - {TOKEN_STATUS_TEXT[token.status as TokenStatus]} - - + {/* 状态 */} + + + {TOKEN_STATUS_TEXT[token.status as TokenStatus]} + + - {/* 过期时间 */} - - {formatTimestamp(token.expired_time)} - + {/* 过期时间 */} + + {formatTimestamp(token.expired_time)} + - {/* 操作 */} - -
- - - -
-
-
- ))} -
-
+ {/* 操作 */} + +
+ + + +
+
+ + ))} + + +
+ + {/* 分页控件 */} + {totalPages > 1 && ( +
+ + 共 {total} 条记录,第 {page} / {totalPages} 页 + +
+ + +
+
+ )} )} diff --git a/src/types/remote-token.ts b/src/types/remote-token.ts index 491dc19..4723991 100644 --- a/src/types/remote-token.ts +++ b/src/types/remote-token.ts @@ -129,3 +129,13 @@ export interface TokenImportStatus { /** 已导入的 Profile 名称(如果已导入) */ imported_profile_name?: string; } + +/** + * 令牌列表分页响应 + */ +export interface TokenListResponse { + page: number; + page_size: number; + total: number; + items: RemoteToken[]; +}