状态码显示与过滤方案
背景
在 ServiceProvidersPage.tsx 中,当前的状态显示和过滤逻辑较为简单:
- 状态显示:仅显示"有效"(success)或"无效"(error),无法区分具体的HTTP状态码
- 过滤方式:通过三个按钮(全部/有效/无效)进行过滤,不够灵活
用户希望:
- 状态显示改为显示返回的HTTP状态码(如200、401、403等)
- 根据状态码分类显示不同颜色
- 过滤改为下拉框,支持按具体状态码筛选
- 超时状态码为0,显示灰色
现状分析
当前状态存储
typescript
// ServiceProvidersPage.tsx:284-286
const [testStatuses, setTestStatuses] = useState<
Record<string, 'loading' | 'success' | 'error' | undefined>
>({});
const [testErrors, setTestErrors] = useState<Record<string, string>>({});当前状态渲染
typescript
// ServiceProvidersPage.tsx:1268-1291
<TableCell>
<span className={styles.statusCell}>
{status === 'loading' && (
<span className={`${styles.statusBadge} ${styles.statusLoading}`}>
<IconLoader2 size={12} className="spin" />
{t('serviceProviders.status.testing', 'Testing...')}
</span>
)}
{status === 'success' && (
<span className={`${styles.statusBadge} ${styles.statusSuccess}`}>
<IconCheckCircle2 size={12} />
{t('serviceProviders.status.valid')}
</span>
)}
{status === 'error' && (
<span className={`${styles.statusBadge} ${styles.statusError}`} title={error}>
<IconAlertTriangle size={12} />
{t('serviceProviders.status.invalid')}
</span>
)}
{!status && <span className={styles.statusIdle}>—</span>}
</span>
</TableCell>当前过滤逻辑
typescript
// ServiceProvidersPage.tsx:380-388
const filteredEntries = useMemo(() => {
if (!selectedProvider) return [];
let entries = selectedProvider.apiKeyEntries ?? [];
if (filter !== 'all') {
entries = entries.filter((e) => {
const k = keyOf(selectedProvider, (selectedProvider.apiKeyEntries ?? []).indexOf(e));
const st = testStatuses[k];
return filter === 'valid' ? st === 'success' : st === 'error';
});
}
return entries;
}, [selectedProvider, search, filter, testStatuses]);API调用返回
typescript
// src/services/api/apiCall.ts:16-17
export interface ApiCallResult<T = unknown> {
statusCode: number;
header: Record<string, string[]>;
body?: T;
}在 testEntry 函数中(第764行):
typescript
if (result.statusCode < 200 || result.statusCode >= 300) {
throw new Error(getApiCallErrorMessage(result));
}实现方案
方案概述
- 状态存储改造:将
testStatuses从存储'loading' | 'success' | 'error'改为存储具体的HTTP状态码数字 - 状态显示改造:在表格中显示状态码,并根据状态码范围显示不同颜色
- 过滤器改造:将过滤器从三个按钮改为下拉框,支持按状态码分类筛选
详细实现
1. 状态存储改造
typescript
// 新增状态码存储
const [testStatusCodes, setTestStatusCodes] = useState<
Record<string, number | undefined>
>({});
// 修改 testEntry 函数
const testEntry = useCallback(
async (provider: OpenAIProviderConfig, entry: ApiKeyEntry) => {
const k = keyOf(provider, entry);
setTestStatusCodes((prev) => ({ ...prev, [k]: undefined })); // undefined 表示 loading
setTestErrors((prev) => {
const n = { ...prev };
delete n[k];
return n;
});
try {
// ... 请求逻辑 ...
const result = await apiCallApi.request(/* ... */);
// 存储状态码
setTestStatusCodes((prev) => ({ ...prev, [k]: result.statusCode }));
if (result.statusCode < 200 || result.statusCode >= 300) {
setTestErrors((prev) => ({ ...prev, [k]: getApiCallErrorMessage(result) }));
}
} catch (err) {
// 超时或其他错误,设置状态码为0
setTestStatusCodes((prev) => ({ ...prev, [k]: 0 }));
const msg = err instanceof Error ? err.message : 'Unknown error';
setTestErrors((prev) => ({ ...prev, [k]: msg }));
}
},
[]
);2. 状态码颜色分类
typescript
// 状态码颜色映射
const getStatusCodeColor = (code: number | undefined): string => {
if (code === undefined) return 'loading'; // 加载中
if (code === 0) return 'timeout'; // 超时,灰色
if (code >= 200 && code < 300) return 'success'; // 成功,绿色
if (code >= 300 && code < 400) return 'redirect'; // 重定向,蓝色
if (code >= 400 && code < 500) return 'client-error'; // 客户端错误,橙色
if (code >= 500) return 'server-error'; // 服务端错误,红色
return 'unknown'; // 未知,灰色
};
// 状态码分类标签
const getStatusCodeCategory = (code: number | undefined): string => {
if (code === undefined) return 'loading';
if (code === 0) return 'timeout';
if (code >= 200 && code < 300) return 'success';
if (code >= 300 && code < 400) return 'redirect';
if (code >= 400 && code < 500) return 'client-error';
if (code >= 500) return 'server-error';
return 'unknown';
};3. 状态码颜色样式
scss
// ServiceProvidersPage.module.scss
.statusBadge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
border: 1px solid transparent;
}
.statusLoading {
color: var(--primary-color);
border-color: var(--primary-color-20);
}
.statusCodeSuccess {
color: var(--success-color);
background-color: var(--success-color-10);
border-color: var(--success-color-30);
}
.statusCodeRedirect {
color: var(--info-color);
background-color: var(--info-color-10);
border-color: var(--info-color-30);
}
.statusCodeClientError {
color: var(--warning-color);
background-color: var(--warning-color-10);
border-color: var(--warning-color-30);
}
.statusCodeServerError {
color: var(--error-color);
background-color: var(--error-color-10);
border-color: var(--error-color-30);
}
.statusCodeTimeout {
color: var(--muted-foreground);
background-color: var(--muted);
border-color: var(--border-color);
}
.statusCodeUnknown {
color: var(--muted-foreground);
background-color: var(--muted);
border-color: var(--border-color);
}4. 状态码显示组件
tsx
// 状态码显示
const renderStatusCode = (code: number | undefined, error?: string) => {
if (code === undefined) {
return (
<span className={`${styles.statusBadge} ${styles.statusLoading}`}>
<IconLoader2 size={12} className="spin" />
{t('serviceProviders.status.testing', 'Testing...')}
</span>
);
}
const colorClass = getStatusCodeColor(code);
const displayCode = code === 0 ? 'Timeout' : code.toString();
return (
<span
className={`${styles.statusBadge} ${styles[`statusCode${colorClass.charAt(0).toUpperCase() + colorClass.slice(1)}`]}`}
title={error || ''}
>
{displayCode}
</span>
);
};5. 过滤器改造
typescript
// 状态码分类选项
const statusCodeCategories = [
{ value: 'all', label: t('serviceProviders.filter.all') },
{ value: 'loading', label: t('serviceProviders.filter.loading') },
{ value: 'success', label: t('serviceProviders.filter.success') },
{ value: 'redirect', label: t('serviceProviders.filter.redirect') },
{ value: 'client-error', label: t('serviceProviders.filter.clientError') },
{ value: 'server-error', label: t('serviceProviders.filter.serverError') },
{ value: 'timeout', label: t('serviceProviders.filter.timeout') },
];
// 过滤逻辑
const filteredEntries = useMemo(() => {
if (!selectedProvider) return [];
let entries = selectedProvider.apiKeyEntries ?? [];
// 搜索过滤
if (search) {
const q = search.toLowerCase();
entries = entries.filter((e) => (e.apiKey ?? '').toLowerCase().includes(q));
}
// 状态码过滤
if (filter !== 'all') {
entries = entries.filter((e) => {
const k = keyOf(selectedProvider, (selectedProvider.apiKeyEntries ?? []).indexOf(e));
const code = testStatusCodes[k];
const category = getStatusCodeCategory(code);
return category === filter;
});
}
return entries;
}, [selectedProvider, search, filter, testStatusCodes]);6. 下拉框UI
tsx
<div className={styles.filterGroup}>
<select
className={styles.filterSelect}
value={filter}
onChange={(e) => setFilter(e.target.value)}
>
{statusCodeCategories.map((cat) => (
<option key={cat.value} value={cat.value}>
{cat.label}
</option>
))}
</select>
</div>国际化文本
json
{
"serviceProviders": {
"filter": {
"all": "全部状态",
"loading": "测试中",
"success": "2xx 成功",
"redirect": "3xx 重定向",
"clientError": "4xx 客户端错误",
"serverError": "5xx 服务端错误",
"timeout": "超时 (0)"
},
"status": {
"testing": "测试中...",
"timeout": "超时"
}
}
}UI 布局示例
┌────────────────────────────────────────────────────────────┐
│ 提供商管理 │
├─────────────────┬──────────────────────────────────────────┤
│ 侧边栏 │ [搜索] [状态码过滤 ▼] [其他操作按钮] │
│ │ │
│ Provider A │ # | API Key | Proxy | Status | Act │
│ ─────────────── │ ──┼────────────┼───────┼────────┼────── │
│ api.example.com │ 1 | sk-***... | — | 200 ✅ | 🗘 🗑 │
│ │ 2 | sk-***... | — | 401 ❌ | 🗘 🗑 │
│ Provider B │ 3 | sk-***... | — | 0 ⏱️ | 🗘 🗑 │
│ ─────────────── │ 4 | sk-***... | — | 500 ❌ | 🗘 🗑 │
│ api.test.co │ │
└─────────────────┴──────────────────────────────────────────┘注意事项
- 向后兼容:需要确保现有的
testStatuses逻辑仍然可用,或者提供迁移方案 - 性能考虑:状态码存储使用数字类型,比字符串更节省内存
- 用户体验:状态码显示应该简洁明了,避免信息过载
- 错误处理:超时状态码(0)应该有明确的视觉区分
- 过滤灵活性:下拉框应该支持按状态码范围过滤,而不是单个状态码