Skip to content

状态码显示与过滤方案

背景

ServiceProvidersPage.tsx 中,当前的状态显示和过滤逻辑较为简单:

  • 状态显示:仅显示"有效"(success)或"无效"(error),无法区分具体的HTTP状态码
  • 过滤方式:通过三个按钮(全部/有效/无效)进行过滤,不够灵活

用户希望:

  1. 状态显示改为显示返回的HTTP状态码(如200、401、403等)
  2. 根据状态码分类显示不同颜色
  3. 过滤改为下拉框,支持按具体状态码筛选
  4. 超时状态码为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));
}

实现方案

方案概述

  1. 状态存储改造:将 testStatuses 从存储 'loading' | 'success' | 'error' 改为存储具体的HTTP状态码数字
  2. 状态显示改造:在表格中显示状态码,并根据状态码范围显示不同颜色
  3. 过滤器改造:将过滤器从三个按钮改为下拉框,支持按状态码分类筛选

详细实现

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      │                                          │
└─────────────────┴──────────────────────────────────────────┘

注意事项

  1. 向后兼容:需要确保现有的 testStatuses 逻辑仍然可用,或者提供迁移方案
  2. 性能考虑:状态码存储使用数字类型,比字符串更节省内存
  3. 用户体验:状态码显示应该简洁明了,避免信息过载
  4. 错误处理:超时状态码(0)应该有明确的视觉区分
  5. 过滤灵活性:下拉框应该支持按状态码范围过滤,而不是单个状态码

相关文件

文件用途
src/features/serviceProviders/ServiceProvidersPage.tsx主页面组件
src/features/serviceProviders/ServiceProvidersPage.module.scss样式文件
src/services/api/apiCall.tsAPI调用接口定义
src/i18n/locales/zh-CN.json中文国际化
src/i18n/locales/en.json英文国际化