refactor: convert endpoint speed test to modal dialog
- Transform EndpointSpeedTest component into a modal dialog - Add "Advanced" button next to base URL input to open modal - Support ESC key and backdrop click to close modal - Apply Linear design principles: minimal styling, clean layout - Remove unused showBaseUrlInput variable - Implement same modal pattern for both Claude and Codex
This commit is contained in:
@@ -219,6 +219,9 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
const [codexBaseUrl, setCodexBaseUrl] = useState("");
|
const [codexBaseUrl, setCodexBaseUrl] = useState("");
|
||||||
const [isCodexTemplateModalOpen, setIsCodexTemplateModalOpen] =
|
const [isCodexTemplateModalOpen, setIsCodexTemplateModalOpen] =
|
||||||
useState(false);
|
useState(false);
|
||||||
|
// 端点测速弹窗状态
|
||||||
|
const [isEndpointModalOpen, setIsEndpointModalOpen] = useState(false);
|
||||||
|
const [isCodexEndpointModalOpen, setIsCodexEndpointModalOpen] = useState(false);
|
||||||
// -1 表示自定义,null 表示未选择,>= 0 表示预设索引
|
// -1 表示自定义,null 表示未选择,>= 0 表示预设索引
|
||||||
const [selectedCodexPreset, setSelectedCodexPreset] = useState<number | null>(
|
const [selectedCodexPreset, setSelectedCodexPreset] = useState<number | null>(
|
||||||
showPresets && isCodex ? -1 : null
|
showPresets && isCodex ? -1 : null
|
||||||
@@ -1117,10 +1120,6 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
// 综合判断是否应该显示 Kimi 模型选择器
|
// 综合判断是否应该显示 Kimi 模型选择器
|
||||||
const shouldShowKimiSelector = isKimiPreset || isEditingKimi;
|
const shouldShowKimiSelector = isKimiPreset || isEditingKimi;
|
||||||
|
|
||||||
// 判断是否显示基础 URL 输入框(仅自定义模式显示)
|
|
||||||
const showBaseUrlInput =
|
|
||||||
!isCodex && shouldShowSpeedTest;
|
|
||||||
|
|
||||||
const claudeSpeedTestEndpoints = useMemo<EndpointCandidate[]>(() => {
|
const claudeSpeedTestEndpoints = useMemo<EndpointCandidate[]>(() => {
|
||||||
if (isCodex) return [];
|
if (isCodex) return [];
|
||||||
const map = new Map<string, EndpointCandidate>();
|
const map = new Map<string, EndpointCandidate>();
|
||||||
@@ -1571,23 +1570,22 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{!isCodex && shouldShowSpeedTest && (
|
{!isCodex && shouldShowSpeedTest && (
|
||||||
<EndpointSpeedTest
|
|
||||||
appType={appType}
|
|
||||||
value={baseUrl}
|
|
||||||
onChange={handleBaseUrlChange}
|
|
||||||
initialEndpoints={claudeSpeedTestEndpoints}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 基础 URL 输入框 - 仅在自定义模式下显示 */}
|
|
||||||
{!isCodex && showBaseUrlInput && (
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label
|
<div className="flex items-center justify-between">
|
||||||
htmlFor="baseUrl"
|
<label
|
||||||
className="block text-sm font-medium text-gray-900 dark:text-gray-100"
|
htmlFor="baseUrl"
|
||||||
>
|
className="block text-sm font-medium text-gray-900 dark:text-gray-100"
|
||||||
请求地址
|
>
|
||||||
</label>
|
请求地址
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsEndpointModalOpen(true)}
|
||||||
|
className="text-xs text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 transition-colors"
|
||||||
|
>
|
||||||
|
高级
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<input
|
<input
|
||||||
type="url"
|
type="url"
|
||||||
id="baseUrl"
|
id="baseUrl"
|
||||||
@@ -1605,6 +1603,18 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 端点测速弹窗 - Claude */}
|
||||||
|
{!isCodex && shouldShowSpeedTest && isEndpointModalOpen && (
|
||||||
|
<EndpointSpeedTest
|
||||||
|
appType={appType}
|
||||||
|
value={baseUrl}
|
||||||
|
onChange={handleBaseUrlChange}
|
||||||
|
initialEndpoints={claudeSpeedTestEndpoints}
|
||||||
|
visible={isEndpointModalOpen}
|
||||||
|
onClose={() => setIsEndpointModalOpen(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{!isCodex && shouldShowKimiSelector && (
|
{!isCodex && shouldShowKimiSelector && (
|
||||||
<KimiModelSelector
|
<KimiModelSelector
|
||||||
apiKey={apiKey}
|
apiKey={apiKey}
|
||||||
@@ -1650,11 +1660,43 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{isCodex && shouldShowSpeedTest && (
|
{isCodex && shouldShowSpeedTest && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label
|
||||||
|
htmlFor="codexBaseUrl"
|
||||||
|
className="block text-sm font-medium text-gray-900 dark:text-gray-100"
|
||||||
|
>
|
||||||
|
请求地址
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsCodexEndpointModalOpen(true)}
|
||||||
|
className="text-xs text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 transition-colors"
|
||||||
|
>
|
||||||
|
高级
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
id="codexBaseUrl"
|
||||||
|
value={codexBaseUrl}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 端点测速弹窗 - Codex */}
|
||||||
|
{isCodex && shouldShowSpeedTest && isCodexEndpointModalOpen && (
|
||||||
<EndpointSpeedTest
|
<EndpointSpeedTest
|
||||||
appType={appType}
|
appType={appType}
|
||||||
value={codexBaseUrl}
|
value={codexBaseUrl}
|
||||||
onChange={handleCodexBaseUrlChange}
|
onChange={handleCodexBaseUrlChange}
|
||||||
initialEndpoints={codexSpeedTestEndpoints}
|
initialEndpoints={codexSpeedTestEndpoints}
|
||||||
|
visible={isCodexEndpointModalOpen}
|
||||||
|
onClose={() => setIsCodexEndpointModalOpen(false)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
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";
|
import type { AppType } from "../../lib/tauri-api";
|
||||||
|
|
||||||
@@ -16,6 +17,7 @@ interface EndpointSpeedTestProps {
|
|||||||
onChange: (url: string) => void;
|
onChange: (url: string) => void;
|
||||||
initialEndpoints: EndpointCandidate[];
|
initialEndpoints: EndpointCandidate[];
|
||||||
visible?: boolean;
|
visible?: boolean;
|
||||||
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EndpointEntry extends EndpointCandidate {
|
interface EndpointEntry extends EndpointCandidate {
|
||||||
@@ -80,6 +82,7 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
|
|||||||
onChange,
|
onChange,
|
||||||
initialEndpoints,
|
initialEndpoints,
|
||||||
visible = true,
|
visible = true,
|
||||||
|
onClose,
|
||||||
}) => {
|
}) => {
|
||||||
const [entries, setEntries] = useState<EndpointEntry[]>(() =>
|
const [entries, setEntries] = useState<EndpointEntry[]>(() =>
|
||||||
buildInitialEntries(initialEndpoints, value),
|
buildInitialEntries(initialEndpoints, value),
|
||||||
@@ -185,12 +188,12 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
|
|||||||
try {
|
try {
|
||||||
parsed = new URL(candidate);
|
parsed = new URL(candidate);
|
||||||
} catch {
|
} catch {
|
||||||
setAddError("URL 格式不正确,请确认包含 http(s) 前缀");
|
setAddError("URL 格式不正确");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!parsed.protocol.startsWith("http")) {
|
if (!parsed.protocol.startsWith("http")) {
|
||||||
setAddError("仅支持 HTTP/HTTPS 地址");
|
setAddError("仅支持 HTTP/HTTPS");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,12 +243,12 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
|
|||||||
const runSpeedTest = useCallback(async () => {
|
const runSpeedTest = useCallback(async () => {
|
||||||
const urls = entries.map((entry) => entry.url);
|
const urls = entries.map((entry) => entry.url);
|
||||||
if (urls.length === 0) {
|
if (urls.length === 0) {
|
||||||
setLastError("请先添加至少一个地址再进行测速");
|
setLastError("请先添加端点");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof window === "undefined" || !window.api?.testApiEndpoints) {
|
if (typeof window === "undefined" || !window.api?.testApiEndpoints) {
|
||||||
setLastError("测速功能仅在桌面应用中可用");
|
setLastError("测速功能不可用");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,7 +271,7 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
|
|||||||
...entry,
|
...entry,
|
||||||
latency: null,
|
latency: null,
|
||||||
status: undefined,
|
status: undefined,
|
||||||
error: "未返回测速结果",
|
error: "未返回结果",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
@@ -307,170 +310,222 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
|
|||||||
[normalizedSelected, onChange],
|
[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) {
|
if (!visible) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-4 space-y-3 rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/40">
|
<div
|
||||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
className="fixed inset-0 z-50 flex items-center justify-center"
|
||||||
<div>
|
onMouseDown={(e) => {
|
||||||
<h3 className="text-sm font-semibold text-gray-800 dark:text-gray-200">
|
if (e.target === e.currentTarget) onClose();
|
||||||
节点测速
|
}}
|
||||||
|
>
|
||||||
|
{/* Backdrop */}
|
||||||
|
<div
|
||||||
|
className={`absolute inset-0 bg-black/50 dark:bg-black/70${
|
||||||
|
isLinux() ? "" : " backdrop-blur-sm"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Modal */}
|
||||||
|
<div className="relative bg-white dark:bg-gray-900 rounded-lg shadow-lg w-full max-w-2xl mx-4 max-h-[80vh] overflow-hidden flex flex-col">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-800">
|
||||||
|
<h3 className="text-base font-medium text-gray-900 dark:text-gray-100">
|
||||||
|
请求地址管理
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
|
||||||
添加多个端点后可一键测速,自动选取延迟最低的地址
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<label className="flex items-center gap-2 text-xs text-gray-600 dark:text-gray-400">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={autoSelect}
|
|
||||||
onChange={(event) => setAutoSelect(event.target.checked)}
|
|
||||||
className="rounded border-gray-300 text-blue-500 focus:ring-blue-400"
|
|
||||||
/>
|
|
||||||
自动选择最快节点
|
|
||||||
</label>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={runSpeedTest}
|
onClick={onClose}
|
||||||
disabled={isTesting || entries.length === 0}
|
className="p-1 text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md transition-colors"
|
||||||
className="flex items-center gap-2 rounded-md bg-blue-500 px-3 py-1.5 text-sm text-white transition hover:bg-blue-600 disabled:cursor-not-allowed disabled:opacity-60"
|
aria-label="关闭"
|
||||||
>
|
>
|
||||||
{isTesting ? (
|
<X size={16} />
|
||||||
<>
|
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
|
||||||
测速中...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Zap className="h-4 w-4" />
|
|
||||||
开始测速
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{hasEndpoints ? (
|
{/* Content */}
|
||||||
<div className="space-y-2">
|
<div className="flex-1 overflow-auto px-6 py-4 space-y-4">
|
||||||
{sortedEntries.map((entry) => {
|
{/* 测速控制栏 */}
|
||||||
const isSelected = normalizedSelected === entry.url;
|
<div className="flex items-center justify-between">
|
||||||
const latency = entry.latency;
|
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
const statusBadge =
|
{entries.length} 个端点
|
||||||
latency !== null
|
</div>
|
||||||
? latency <= 100
|
<div className="flex items-center gap-3">
|
||||||
? "text-green-600 dark:text-green-400"
|
<label className="flex items-center gap-1.5 text-xs text-gray-600 dark:text-gray-400">
|
||||||
: latency <= 300
|
<input
|
||||||
? "text-amber-600 dark:text-amber-400"
|
type="checkbox"
|
||||||
: "text-red-600 dark:text-red-400"
|
checked={autoSelect}
|
||||||
: "text-gray-500 dark:text-gray-400";
|
onChange={(event) => setAutoSelect(event.target.checked)}
|
||||||
|
className="h-3.5 w-3.5 rounded border-gray-300 dark:border-gray-600"
|
||||||
return (
|
/>
|
||||||
<div
|
自动选择
|
||||||
key={entry.id}
|
</label>
|
||||||
className={`flex items-start justify-between gap-2 rounded-lg border px-3 py-2 text-sm transition ${
|
<button
|
||||||
isSelected
|
type="button"
|
||||||
? "border-green-400 bg-green-50 dark:border-green-500 dark:bg-green-900/30"
|
onClick={runSpeedTest}
|
||||||
: "border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800"
|
disabled={isTesting || !hasEndpoints}
|
||||||
}`}
|
className="flex h-7 items-center gap-1.5 rounded-md bg-gray-900 px-2.5 text-xs font-medium text-white transition hover:bg-gray-800 disabled:cursor-not-allowed disabled:opacity-40 dark:bg-gray-100 dark:text-gray-900 dark:hover:bg-gray-200"
|
||||||
>
|
>
|
||||||
<label className="flex flex-1 cursor-pointer items-start gap-2">
|
{isTesting ? (
|
||||||
<input
|
<>
|
||||||
type="radio"
|
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||||
name="endpoint-speedtest"
|
测速中
|
||||||
checked={isSelected}
|
</>
|
||||||
onChange={() => handleSelect(entry.url)}
|
) : (
|
||||||
className="mt-1 h-4 w-4 border-gray-300 text-green-500 focus:ring-green-400"
|
<>
|
||||||
/>
|
<Zap className="h-3.5 w-3.5" />
|
||||||
<div className="flex flex-1 flex-col gap-1">
|
测速
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 添加输入 */}
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
value={customUrl}
|
||||||
|
placeholder="https://api.example.com"
|
||||||
|
onChange={(event) => 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"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleAddEndpoint}
|
||||||
|
className="flex h-8 w-8 items-center justify-center rounded-md border border-gray-200 transition hover:border-gray-300 hover:bg-gray-50 dark:border-gray-700 dark:hover:border-gray-600 dark:hover:bg-gray-800"
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4 text-gray-600 dark:text-gray-400" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{addError && (
|
||||||
|
<div className="flex items-center gap-1.5 text-xs text-red-600 dark:text-red-400">
|
||||||
|
<AlertCircle className="h-3 w-3" />
|
||||||
|
{addError}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 端点列表 */}
|
||||||
|
{hasEndpoints ? (
|
||||||
|
<div className="space-y-px overflow-hidden rounded-md border border-gray-200 dark:border-gray-700">
|
||||||
|
{sortedEntries.map((entry, index) => {
|
||||||
|
const isSelected = normalizedSelected === entry.url;
|
||||||
|
const latency = entry.latency;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={entry.id}
|
||||||
|
onClick={() => 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" : ""}`}
|
||||||
|
>
|
||||||
|
<div className="flex min-w-0 flex-1 items-center gap-3">
|
||||||
|
{/* 选择指示器 */}
|
||||||
|
<div
|
||||||
|
className={`h-1.5 w-1.5 flex-shrink-0 rounded-full transition ${
|
||||||
|
isSelected
|
||||||
|
? "bg-gray-900 dark:bg-gray-100"
|
||||||
|
: "bg-gray-300 dark:bg-gray-700"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 内容 */}
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||||
|
{entry.label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-0.5 truncate text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{entry.url}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 右侧信息 */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="font-medium text-gray-800 dark:text-gray-100">
|
{latency !== null ? (
|
||||||
{entry.label || "候选节点"}
|
<div className="text-right">
|
||||||
</span>
|
<div className="font-mono text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||||
{isSelected && (
|
{latency}ms
|
||||||
<span className="flex items-center gap-1 text-xs text-green-600 dark:text-green-400">
|
</div>
|
||||||
<Check className="h-3 w-3" /> 已选中
|
</div>
|
||||||
</span>
|
) : isTesting ? (
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin text-gray-400" />
|
||||||
|
) : entry.error ? (
|
||||||
|
<div className="text-xs text-gray-400">失败</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-xs text-gray-400">—</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{entry.isCustom && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleRemoveEndpoint(entry);
|
||||||
|
}}
|
||||||
|
className="opacity-0 transition hover:text-red-600 group-hover:opacity-100 dark:hover:text-red-400"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<span className="break-all text-xs text-gray-500 dark:text-gray-400">
|
|
||||||
{entry.url}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</label>
|
);
|
||||||
<div className="flex items-center gap-3">
|
})}
|
||||||
<div className="text-xs font-mono">
|
</div>
|
||||||
{latency !== null ? (
|
) : (
|
||||||
<span className={statusBadge}>{latency} ms</span>
|
<div className="rounded-md border border-dashed border-gray-200 bg-gray-50 py-8 text-center text-xs text-gray-500 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-400">
|
||||||
) : isTesting ? (
|
暂无端点
|
||||||
<span className="text-gray-400">等待结果</span>
|
</div>
|
||||||
) : entry.error ? (
|
)}
|
||||||
<span className="flex items-center gap-1 text-red-500">
|
|
||||||
<AlertCircle className="h-3 w-3" />
|
|
||||||
失败
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span className="text-gray-400">未测速</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{entry.isCustom && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => handleRemoveEndpoint(entry)}
|
|
||||||
className="rounded-md p-1 text-red-500 transition hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-900/30"
|
|
||||||
title="删除该地址"
|
|
||||||
>
|
|
||||||
<Trash2 className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="rounded-md border border-dashed border-gray-300 bg-white p-4 text-center text-xs text-gray-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400">
|
|
||||||
暂无可测速的地址,请先添加至少一个请求地址
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div>
|
{/* 错误提示 */}
|
||||||
<div className="flex gap-2">
|
{lastError && (
|
||||||
<input
|
<div className="flex items-center gap-1.5 text-xs text-red-600 dark:text-red-400">
|
||||||
type="url"
|
<AlertCircle className="h-3 w-3" />
|
||||||
value={customUrl}
|
{lastError}
|
||||||
placeholder="https://example.com/claude"
|
</div>
|
||||||
onChange={(event) => setCustomUrl(event.target.value)}
|
)}
|
||||||
onKeyDown={(event) => {
|
</div>
|
||||||
if (event.key === "Enter") {
|
|
||||||
event.preventDefault();
|
{/* Footer */}
|
||||||
handleAddEndpoint();
|
<div className="flex items-center justify-end px-6 py-4 border-t border-gray-200 dark:border-gray-800 bg-gray-50 dark:bg-gray-800">
|
||||||
}
|
|
||||||
}}
|
|
||||||
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"
|
|
||||||
/>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleAddEndpoint}
|
onClick={onClose}
|
||||||
className="flex items-center gap-1 rounded-md bg-green-500 px-3 py-1.5 text-sm text-white transition hover:bg-green-600"
|
className="px-4 py-2 text-sm font-medium text-gray-900 dark:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md transition-colors"
|
||||||
>
|
>
|
||||||
<Plus className="h-4 w-4" />
|
完成
|
||||||
添加
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{addError && (
|
|
||||||
<p className="mt-1 text-xs text-red-500">{addError}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{lastError && (
|
|
||||||
<p className="text-xs text-red-500">
|
|
||||||
<AlertCircle className="mr-1 inline h-3 w-3 align-middle" />
|
|
||||||
{lastError}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user