diff --git a/src/components/ProviderForm.tsx b/src/components/ProviderForm.tsx index 7b2dbe5..a961d59 100644 --- a/src/components/ProviderForm.tsx +++ b/src/components/ProviderForm.tsx @@ -219,6 +219,9 @@ const ProviderForm: React.FC = ({ const [codexBaseUrl, setCodexBaseUrl] = useState(""); const [isCodexTemplateModalOpen, setIsCodexTemplateModalOpen] = useState(false); + // 端点测速弹窗状态 + const [isEndpointModalOpen, setIsEndpointModalOpen] = useState(false); + const [isCodexEndpointModalOpen, setIsCodexEndpointModalOpen] = useState(false); // -1 表示自定义,null 表示未选择,>= 0 表示预设索引 const [selectedCodexPreset, setSelectedCodexPreset] = useState( showPresets && isCodex ? -1 : null @@ -1117,10 +1120,6 @@ const ProviderForm: React.FC = ({ // 综合判断是否应该显示 Kimi 模型选择器 const shouldShowKimiSelector = isKimiPreset || isEditingKimi; - // 判断是否显示基础 URL 输入框(仅自定义模式显示) - const showBaseUrlInput = - !isCodex && shouldShowSpeedTest; - const claudeSpeedTestEndpoints = useMemo(() => { if (isCodex) return []; const map = new Map(); @@ -1571,23 +1570,22 @@ const ProviderForm: React.FC = ({ )} {!isCodex && shouldShowSpeedTest && ( - - )} - - {/* 基础 URL 输入框 - 仅在自定义模式下显示 */} - {!isCodex && showBaseUrlInput && (
- +
+ + +
= ({
)} + {/* 端点测速弹窗 - Claude */} + {!isCodex && shouldShowSpeedTest && isEndpointModalOpen && ( + setIsEndpointModalOpen(false)} + /> + )} + {!isCodex && shouldShowKimiSelector && ( = ({ )} {isCodex && shouldShowSpeedTest && ( +
+
+ + +
+ handleCodexBaseUrlChange(e.target.value)} + placeholder="https://your-api-endpoint.com/v1" + autoComplete="off" + className="w-full px-3 py-2 border border-gray-200 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:focus:ring-blue-400/20 focus:border-blue-500 dark:focus:border-blue-400 transition-colors" + /> +
+ )} + + {/* 端点测速弹窗 - Codex */} + {isCodex && shouldShowSpeedTest && isCodexEndpointModalOpen && ( setIsCodexEndpointModalOpen(false)} /> )} diff --git a/src/components/ProviderForm/EndpointSpeedTest.tsx b/src/components/ProviderForm/EndpointSpeedTest.tsx index ee373df..67ab5ac 100644 --- a/src/components/ProviderForm/EndpointSpeedTest.tsx +++ b/src/components/ProviderForm/EndpointSpeedTest.tsx @@ -1,5 +1,6 @@ import React, { useCallback, useEffect, useMemo, useState } from "react"; -import { Zap, Loader2, Plus, Trash2, AlertCircle, Check } from "lucide-react"; +import { Zap, Loader2, Plus, X, AlertCircle } from "lucide-react"; +import { isLinux } from "../../lib/platform"; import type { AppType } from "../../lib/tauri-api"; @@ -16,6 +17,7 @@ interface EndpointSpeedTestProps { onChange: (url: string) => void; initialEndpoints: EndpointCandidate[]; visible?: boolean; + onClose: () => void; } interface EndpointEntry extends EndpointCandidate { @@ -80,6 +82,7 @@ const EndpointSpeedTest: React.FC = ({ onChange, initialEndpoints, visible = true, + onClose, }) => { const [entries, setEntries] = useState(() => buildInitialEntries(initialEndpoints, value), @@ -185,12 +188,12 @@ const EndpointSpeedTest: React.FC = ({ try { parsed = new URL(candidate); } catch { - setAddError("URL 格式不正确,请确认包含 http(s) 前缀"); + setAddError("URL 格式不正确"); return; } if (!parsed.protocol.startsWith("http")) { - setAddError("仅支持 HTTP/HTTPS 地址"); + setAddError("仅支持 HTTP/HTTPS"); return; } @@ -240,12 +243,12 @@ const EndpointSpeedTest: React.FC = ({ const runSpeedTest = useCallback(async () => { const urls = entries.map((entry) => entry.url); if (urls.length === 0) { - setLastError("请先添加至少一个地址再进行测速"); + setLastError("请先添加端点"); return; } if (typeof window === "undefined" || !window.api?.testApiEndpoints) { - setLastError("测速功能仅在桌面应用中可用"); + setLastError("测速功能不可用"); return; } @@ -268,7 +271,7 @@ const EndpointSpeedTest: React.FC = ({ ...entry, latency: null, status: undefined, - error: "未返回测速结果", + error: "未返回结果", }; } return { @@ -307,170 +310,222 @@ const EndpointSpeedTest: React.FC = ({ [normalizedSelected, onChange], ); + // 支持按下 ESC 关闭弹窗 + useEffect(() => { + const onKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape") { + e.preventDefault(); + onClose(); + } + }; + window.addEventListener("keydown", onKeyDown); + return () => window.removeEventListener("keydown", onKeyDown); + }, [onClose]); + if (!visible) { return null; } return ( -
-
-
-

- 节点测速 +
{ + if (e.target === e.currentTarget) onClose(); + }} + > + {/* Backdrop */} +
+ + {/* Modal */} +
+ {/* Header */} +
+

+ 请求地址管理

-

- 添加多个端点后可一键测速,自动选取延迟最低的地址 -

-
-
-
-
- {hasEndpoints ? ( -
- {sortedEntries.map((entry) => { - const isSelected = normalizedSelected === entry.url; - const latency = entry.latency; - const statusBadge = - latency !== null - ? latency <= 100 - ? "text-green-600 dark:text-green-400" - : latency <= 300 - ? "text-amber-600 dark:text-amber-400" - : "text-red-600 dark:text-red-400" - : "text-gray-500 dark:text-gray-400"; - - return ( -
+ {/* 测速控制栏 */} +
+
+ {entries.length} 个端点 +
+
+ + +
+
+ + {/* 添加输入 */} +
+
+ setCustomUrl(event.target.value)} + onKeyDown={(event) => { + if (event.key === "Enter") { + event.preventDefault(); + handleAddEndpoint(); + } + }} + className="flex-1 rounded-md border border-gray-200 bg-white px-3 py-1.5 text-sm text-gray-900 placeholder-gray-400 transition focus:border-gray-400 focus:outline-none dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500 dark:focus:border-gray-600" + /> + +
+ {addError && ( +
+ + {addError} +
+ )} +
+ + {/* 端点列表 */} + {hasEndpoints ? ( +
+ {sortedEntries.map((entry, index) => { + const isSelected = normalizedSelected === entry.url; + const latency = entry.latency; + + return ( +
handleSelect(entry.url)} + className={`group flex cursor-pointer items-center justify-between px-3 py-2.5 transition ${ + isSelected + ? "bg-gray-100 dark:bg-gray-800" + : "bg-white hover:bg-gray-50 dark:bg-gray-900 dark:hover:bg-gray-850" + } ${index > 0 ? "border-t border-gray-100 dark:border-gray-800" : ""}`} + > +
+ {/* 选择指示器 */} +
+ + {/* 内容 */} +
+
+ + {entry.label} + +
+
+ {entry.url} +
+
+
+ + {/* 右侧信息 */}
- - {entry.label || "候选节点"} - - {isSelected && ( - - 已选中 - + {latency !== null ? ( +
+
+ {latency}ms +
+
+ ) : isTesting ? ( + + ) : entry.error ? ( +
失败
+ ) : ( +
+ )} + + {entry.isCustom && ( + )}
- - {entry.url} -
- -
-
- {latency !== null ? ( - {latency} ms - ) : isTesting ? ( - 等待结果 - ) : entry.error ? ( - - - 失败 - - ) : ( - 未测速 - )} -
- {entry.isCustom && ( - - )} -
-
- ); - })} -
- ) : ( -
- 暂无可测速的地址,请先添加至少一个请求地址 -
- )} + ); + })} +
+ ) : ( +
+ 暂无端点 +
+ )} -
-
- setCustomUrl(event.target.value)} - onKeyDown={(event) => { - if (event.key === "Enter") { - event.preventDefault(); - handleAddEndpoint(); - } - }} - className="flex-1 rounded-md border border-gray-200 px-3 py-2 text-sm text-gray-800 shadow-sm transition focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-400/20 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100" - /> + {/* 错误提示 */} + {lastError && ( +
+ + {lastError} +
+ )} +
+ + {/* Footer */} +
- {addError && ( -

{addError}

- )}
- - {lastError && ( -

- - {lastError} -

- )}
); };