ServiceProvidersPage 新增「刷新」和「新建提供商」按钮方案
问题背景
当前 ServiceProvidersPage 页面缺少两个常用操作入口:
- 刷新按钮:当后端配置变更(如手动编辑了配置文件、其他终端操作等)后,用户需要手动刷新页面才能看到最新数据。
- 新建提供商按钮:当前只能通过编辑已有提供商来管理配置,无法直接在页面上新建一个全新的 OpenAI 兼容提供商。
而 ProvidersWorkbenchPage(ai-providers 页面)已有这两个功能,其实现可作为参考。
现有实现分析
ProvidersWorkbenchPage 的刷新实现
tsx
// ProvidersWorkbenchPage.tsx:91-96
const handleRefresh = useCallback(async () => {
await Promise.allSettled([
workbench.refetch(), // 重新拉取提供商配置
refreshRecentRequests().catch(() => undefined), // 刷新最近请求数据
]);
}, [refreshRecentRequests, workbench]);刷新按钮位于 ProviderHeaderCard 中,点击后调用 onRefresh 回调,有 loading 旋转动画。
ProvidersWorkbenchPage 的新建实现
tsx
// ProvidersWorkbenchPage.tsx:225-234
const openCreate = useCallback(() => {
setSheetState({ open: true, brand: 'openaiCompatibility', mode: 'create', resource: null });
}, [activeBrand, groups]);新建按钮点击后打开 ProviderSheet(mode='create'),表单使用 BaseProviderForm 渲染,提交后调用 workbench.createProvider()。
ServiceProvidersPage 现有机制
fetchProviders:已有的数据加载函数sheetOpen/editTarget:已有的 Sheet 状态(仅用于编辑模式)BaseProviderForm:已有的表单组件(支持 create 和 edit 两种 mode)providersApi:saveOpenAIProviders()和updateOpenAIProvider()已支持保存
实现方案
一、新增刷新功能
1.1 新增 refreshing 状态
在 ServiceProvidersPage.tsx:292 附近添加:
tsx
const [refreshing, setRefreshing] = useState(false);1.2 新增 handleRefresh 函数
在 fetchProviders 之后(约第 321 行)添加:
tsx
const handleRefresh = useCallback(async () => {
if (refreshing) return;
setRefreshing(true);
try {
const data = await providersApi.getOpenAIProviders();
setProviders(data);
// 保留当前选中,如果当前选中的 provider 已不存在则自动选第一个
setSelectedName((prev) => {
if (prev && data.some((p) => p.name === prev)) return prev;
return data.length > 0 ? data[0].name : null;
});
showNotification(t('serviceProviders.toast.refreshed', 'Data refreshed'), 'success');
} catch {
showNotification(t('serviceProviders.toast.refreshFailed', 'Refresh failed'), 'error');
} finally {
setRefreshing(false);
}
}, [refreshing, showNotification, t]);与 fetchProviders 的区别:
- 不设置
setLoading(true)(不会显示全屏加载态,避免闪烁) - 不清空选中的
selectedName,而是验证后保留 - 刷新完成后显示 toast 通知
1.3 UI 中添加刷新按钮
在页面标题区域(第 1071 行 <h2> 附近)改为:
tsx
<div className={styles.headerRow}>
<h2 className={styles.title}>{t('serviceProviders.header.title')}</h2>
<div className={styles.headerActions}>
<button
type="button"
className={styles.headerBtn}
onClick={() => void handleRefresh()}
disabled={refreshing}
>
{refreshing ? (
<IconLoader2 size={16} className={styles.spin} />
) : (
<IconRefreshCw size={16} />
)}
<span>{refreshing
? t('serviceProviders.actions.syncing', 'Syncing...')
: t('serviceProviders.actions.refresh', 'Refresh')
}</span>
</button>
<button
type="button"
className={styles.headerBtn}
onClick={openCreateSheet}
>
<IconPlus size={16} />
<span>{t('serviceProviders.actions.newProvider', 'New Provider')}</span>
</button>
</div>
</div>二、新增新建提供商功能
2.1 将 Sheet 状态改为支持 create/edit 两种模式
当前的 editTarget + sheetOpen 只支持编辑模式。需要引入 sheetMode:
tsx
const [sheetMode, setSheetMode] = useState<'create' | 'edit'>('edit');2.2 新增 openCreateSheet 函数
tsx
const openCreateSheet = useCallback(() => {
setSheetMode('create');
setEditTarget(null);
setSheetOpen(true);
}, []);2.3 修改 openEditSheet 设置 mode
tsx
const openEditSheet = useCallback(
(provider: OpenAIProviderConfig) => {
setSheetMode('edit');
setProviders((prev) => {
const idx = prev.indexOf(provider);
setEditTarget({ provider, providerIdx: idx });
return prev;
});
setSheetOpen(true);
},
[]
);2.4 新增新建提交处理函数 handleCreateSubmit
tsx
const handleCreateSubmit = useCallback(
async (input: ProviderEntryFormInput) => {
try {
const apiKeyEntries: ApiKeyEntry[] = (input.apiKeyEntries ?? [])
.map((e) => ({
apiKey: (e.apiKey ?? '').trim() || (e.existingApiKey ?? '').trim(),
proxyUrl: e.proxyUrl || undefined,
authIndex: e.authIndex || undefined,
}))
.filter((e) => !!e.apiKey);
const models = (input.models ?? [])
.filter((m) => m.name.trim())
.map((m) => ({
name: m.name.trim(),
...(m.alias?.trim() ? { alias: m.alias.trim() } : {}),
}));
const headers: Record<string, string> = {};
(input.headers ?? []).forEach((h) => {
if (h.key.trim()) headers[h.key.trim()] = h.value;
});
const newProvider: OpenAIProviderConfig = {
name: input.name?.trim() || 'new-provider',
baseUrl: (input.baseUrl?.trim() || '').replace(/\/+$/, ''),
apiKeyEntries,
models: models.length ? models : undefined,
headers: Object.keys(headers).length ? headers : undefined,
testModel: input.testModel?.trim() || undefined,
prefix: input.prefix?.trim() || undefined,
disabled: input.disabled ?? false,
};
// 追加到列表末尾并保存
const nextList = [...providers, newProvider];
await providersApi.saveOpenAIProviders(nextList);
setProviders(nextList);
setSelectedName(newProvider.name);
setSheetOpen(false);
showNotification(t('serviceProviders.toast.createSuccess', 'Provider created'), 'success');
} catch {
showNotification(t('serviceProviders.toast.createFailed', 'Creation failed'), 'error');
}
},
[providers, showNotification, t]
);2.5 修改 Sheet 渲染逻辑,支持 create 模式
将原来的 {editTarget && (...)} 改为:
tsx
{(sheetMode === 'edit' ? editTarget : true) && (
<Sheet
open={sheetOpen}
onClose={() => setSheetOpen(false)}
title={sheetMode === 'create'
? t('providersPage.form.createEyebrow')
: t('providersPage.form.editEyebrow')
}
footer={
<>
<button ... onClick={() => setSheetOpen(false)}>
{t('providersPage.actions.cancel')}
</button>
<button type="submit" form={editFormId} ...>
{t('providersPage.actions.save')}
</button>
</>
}
>
<BaseProviderForm
brand="openaiCompatibility"
resource={sheetMode === 'edit' && editTarget
? buildEditResource(editTarget.provider, editTarget.providerIdx)
: null
}
mode={sheetMode}
mutating={editSubmitting}
formId={editFormId}
onSubmit={async (input) => {
setEditSubmitting(true);
try {
if (sheetMode === 'create') {
await handleCreateSubmit(input);
} else {
await handleFormSubmit(input);
}
} finally {
setEditSubmitting(false);
}
}}
/>
</Sheet>
)}三、i18n 翻译键
需要在所有语言文件中添加以下翻译键:
en.json:
json
"serviceProviders": {
"actions": {
"refresh": "Refresh",
"syncing": "Syncing...",
"newProvider": "New Provider"
},
"toast": {
"refreshed": "Data refreshed",
"refreshFailed": "Refresh failed",
"createSuccess": "Provider created",
"createFailed": "Creation failed"
}
}zh-CN.json:
json
"serviceProviders": {
"actions": {
"refresh": "刷新",
"syncing": "同步中...",
"newProvider": "新建提供商"
},
"toast": {
"refreshed": "数据已刷新",
"refreshFailed": "刷新失败",
"createSuccess": "提供商已创建",
"createFailed": "创建失败"
}
}同样需要在 ru.json 和 zh-TW.json 中添加对应翻译。
四、样式
在 ServiceProvidersPage.module.scss 中添加:
scss
.headerRow {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.headerActions {
display: flex;
gap: 8px;
}
.headerBtn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 14px;
font-size: 13px;
font-weight: 500;
border-radius: 6px;
border: 1px solid var(--border-color, #ddd);
background: var(--bg-primary);
color: var(--text-primary);
cursor: pointer;
transition: background 0.15s, border-color 0.15s;
&:hover:not(:disabled) {
background: var(--hover-bg, #f5f5f5);
border-color: var(--primary-color);
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
span.spin {
animation: spin 1s linear infinite;
}
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}五、实施步骤清单
| 步骤 | 操作 | 涉及文件 |
|---|---|---|
| 1 | 新增 IconPlus、IconRefreshCw 导入 | ServiceProvidersPage.tsx |
| 2 | 新增 refreshing、sheetMode 状态 | ServiceProvidersPage.tsx |
| 3 | 新增 handleRefresh 回调 | ServiceProvidersPage.tsx |
| 4 | 新增 openCreateSheet、handleCreateSubmit 回调 | ServiceProvidersPage.tsx |
| 5 | 修改 openEditSheet 添加 setSheetMode('edit') | ServiceProvidersPage.tsx |
| 6 | 修改页面标题区域为 flex 布局并添加两个按钮 | ServiceProvidersPage.tsx |
| 7 | 修改 Sheet 渲染逻辑支持 create 模式 | ServiceProvidersPage.tsx |
| 8 | 添加 i18n 翻译键(4 个语言文件) | en.json、zh-CN.json、zh-TW.json、ru.json |
| 9 | 添加 SCSS 样式 | ServiceProvidersPage.module.scss |
| 10 | 测试刷新和新建功能 | - |