Skip to content

密钥管理页面禁用提供商逻辑

背景

ServiceProvidersPage.tsx 中,导航栏(侧边栏)显示所有服务提供商,但缺少对禁用(disabled)提供商的管理功能。

用户需求:

  1. 在导航栏增加一个切换功能,可以选择显示或隐藏禁用的提供商
  2. 移动逻辑保持不变,表格的移动功能仍然可以将密钥移动到禁用的提供商
  3. 在主面板中,为选中的提供商提供启用/禁用的开关控件

现状分析

当前实现

  • 服务提供商配置中包含 disabled 字段(参考 buildEditResource 函数)
  • 在表单提交时支持 disabled 字段(参考 handleFormSubmit 函数)
  • 侧边栏通过 filteredProviders 进行搜索过滤,但不支持按禁用状态过滤
  • 移动下拉菜单 MoveDropdownBatchMoveDropdown 包含了所有提供商(禁用的也能移动过去),但没有视觉区分

相关代码位置

功能文件位置
状态定义ServiceProvidersPage.tsx
侧边栏过滤filteredProviders
移动下拉菜单MoveDropdown
表单提交handleFormSubmit
编辑面板第 1019-1186 行(panelHeader)

实现方案

方案概述

  1. 导航栏切换:在侧边栏顶部添加一个 ToggleSwitch 开关,用于控制是否显示禁用的提供商
  2. 侧边栏过滤逻辑:修改 filteredProviders 根据 showDisabledProviders 状态过滤提供商
  3. 移动下拉菜单:修改 allProviders 参数传递逻辑,确保移动功能仍然支持禁用提供商(保持现有行为)
  4. 面板启用/禁用开关:在主面板的工具栏区域添加 ToggleSwitch 控件,支持快速启用/禁用当前选中的提供商

详细实现

1. 添加状态变量

在组件顶部添加新的状态变量:

typescript
const [showDisabledProviders, setShowDisabledProviders] = useState(true);

2. 修改侧边栏过滤逻辑

typescript
const filteredProviders = useMemo(() => {
  let result = providers;
  
  // 根据搜索过滤
  if (sidebarSearch.trim()) {
    const q = sidebarSearch.toLowerCase();
    result = result.filter(
      (p) => p.name.toLowerCase().includes(q) || (p.baseUrl ?? '').toLowerCase().includes(q)
    );
  }
  
  // 根据禁用状态过滤
  if (!showDisabledProviders) {
    result = result.filter((p) => !p.disabled);
  }
  
  return result;
}, [providers, sidebarSearch, showDisabledProviders]);

3. 侧边栏切换控件

在侧边栏添加 ToggleSwitch 控件:

tsx
<div className={styles.sidebarToggle}>
  <span className={styles.sidebarToggleLabel}>
    {t('serviceProviders.sidebar.showDisabled')}
  </span>
  <ToggleSwitch
    checked={showDisabledProviders}
    onChange={setShowDisabledProviders}
    ariaLabel={t('serviceProviders.sidebar.showDisabled')}
  />
</div>

4. 侧边栏条目样式

为禁用的提供商条目添加视觉区分样式:

scss
// ServiceProvidersPage.module.scss
.sidebarItemDisabled {
  opacity: 0.5;
  background-color: var(--muted);
}

并在侧边栏渲染时添加条件样式:

tsx
<button
  className={`${styles.sidebarItem}${
    p.name === selectedName ? ` ${styles.sidebarItemActive}` : ''
  }${p.disabled ? ` ${styles.sidebarItemDisabled}` : ''}`}
  // ...
>

5. 面板启用/禁用开关

在面板工具栏区域添加启用/禁用开关:

tsx
<div className={styles.panelDisableToggle}>
  <span className={styles.panelDisableLabel}>
    {selectedProvider.disabled 
      ? t('serviceProviders.panel.disabled')
      : t('serviceProviders.panel.enabled')
    }
  </span>
  <ToggleSwitch
    checked={!selectedProvider.disabled}
    onChange={async (checked) => {
      const next: OpenAIProviderConfig = {
        ...selectedProvider,
        disabled: !checked,
      };
      const all = [...providers];
      all[providers.indexOf(selectedProvider)] = next;
      await providersApi.saveOpenAIProviders(all);
      setProviders(all);
      showNotification(
        checked 
          ? t('serviceProviders.toast.enabled')
          : t('serviceProviders.toast.disabled'),
        'success'
      );
    }}
    ariaLabel={t('serviceProviders.panel.toggleDisable')}
  />
</div>

6. 移动逻辑保持不变

移动下拉菜单 MoveDropdownBatchMoveDropdownallProviders 仍然传入完整的 providers 数组,不需要修改:

tsx
<MoveDropdown
  provider={selectedProvider}
  allProviders={providers}  // 保持传入所有提供商,包括禁用的
  onMove={(target) => handleMove(selectedProvider, realIdx, target)}
  disabled={testingAny}
/>

国际化文本

需要在 zh-CN.json 中添加以下文本:

json
{
  "serviceProviders": {
    "sidebar": {
      "showDisabled": "显示禁用提供商",
      "baseUrl": "服务地址",
      "searchPlaceholder": "搜索地址..."
    },
    "panel": {
      "enabled": "已启用",
      "disabled": "已禁用",
      "toggleDisable": "切换提供商启用状态"
    },
    "toast": {
      "enabled": "提供商已启用",
      "disabled": "提供商已禁用"
    }
  }
}

UI 布局建议

┌────────────────────────────────────────────────────────────┐
│  提供商管理                                                  │
├─────────────────┬──────────────────────────────────────────┤
│ [显示禁用提供商 ☑] │  toolbar area                          │
│ ┌─────────────┐ │  [搜索] [全部/有效/无效] [启用/禁用开关]   │
│ │ 🔍 搜索...  │ │                                          │
│ ├─────────────┤ │  ┌────────────────────────────────────┐  │
│ │ Provider A  │ │  │ # | API Key | Proxy | Status | Act  │  │
│ │ api.example │ │  ├────────────────────────────────────┤  │
│ └─────────────┘ │  │ 1 | sk-***... | —    | ✅ 有效 | 🗘  │  │
│ │ Provider B  │ │  │ 2 | sk-***... | —    | ❌ 无效 | 🗘  │  │
│ │ api.test.co │ │  └────────────────────────────────────┘  │
│ └─────────────┘ │                                          │
│ │ Provider C*  │ │                                          │
│ │ (已禁用)     │ │                                          │
│ └─────────────┘ │                                          │
└─────────────────┴──────────────────────────────────────────┘

注意事项

  1. 移动功能不限制:表格的移动功能仍然可以将密钥移动到禁用的提供商,这是保持现有行为的设计决策
  2. 视觉区分:禁用的提供商在侧边栏应该有视觉区分(如降低透明度、灰色背景等)
  3. 自动选中:当用户隐藏禁用的提供商时,如果当前选中的是禁用的提供商,应该自动切换到列表中的第一个非禁用提供商
  4. API 调用:启用/禁用状态变更需要调用 saveOpenAIProviders 保存到后端

相关文件

文件用途
src/features/serviceProviders/ServiceProvidersPage.tsx主页面组件
src/features/serviceProviders/ServiceProvidersPage.module.scss样式文件
src/components/ui/ToggleSwitch.tsxToggleSwitch 组件
src/i18n/locales/zh-CN.json中文国际化
src/i18n/locales/en.json英文国际化