Merge remote-tracking branch 'origin/pr/292' into dev

# Conflicts:
#	src/features/providers/sheets/forms/BaseProviderForm.tsx
This commit is contained in:
LTbinglingfeng
2026-05-27 23:45:21 +08:00
6 changed files with 136 additions and 28 deletions
@@ -4,6 +4,8 @@ import {
IconAlertTriangle,
IconCheckCircle2,
IconDownload,
IconEye,
IconEyeOff,
IconLoader2,
IconPlus,
IconX,
@@ -133,7 +135,8 @@ function buildInitialForm(
const disabled = hasDisableAllModelsRule(cfg.excludedModels);
const excludedList = stripDisableAllRule(cfg.excludedModels);
return {
apiKey: '',
// Populate apiKey from resource.raw in edit mode so the field is not empty
apiKey: cfg.apiKey ?? '',
name: '',
baseUrl: cfg.baseUrl ?? '',
proxyUrl: cfg.proxyUrl ?? '',
@@ -211,6 +214,20 @@ export function BaseProviderForm({
JSON.stringify(buildInitialForm(brand, resource, mode))
);
const [error, setError] = useState<string | null>(null);
const [showPasswords, setShowPasswords] = useState<Set<number>>(new Set());
const [showSingleApiKey, setShowSingleApiKey] = useState(false);
const togglePasswordVisibility = (idx: number) => {
setShowPasswords((prev) => {
const next = new Set(prev);
if (next.has(idx)) {
next.delete(idx);
} else {
next.add(idx);
}
return next;
});
};
const isDirty = useMemo(
() => JSON.stringify(form) !== initialFormSignature,
@@ -454,19 +471,43 @@ export function BaseProviderForm({
<label className={styles.label} htmlFor={`${fid}-apiKey`}>
{t('providersPage.form.apiKey')}
</label>
<input
id={`${fid}-apiKey`}
className={styles.input}
type="password"
value={form.apiKey}
onChange={(e) => updateField('apiKey', e.target.value)}
placeholder={
mode === 'edit'
? t('providersPage.form.apiKeyEditPlaceholder')
: t('providersPage.form.apiKeyCreatePlaceholder')
}
disabled={mutating}
/>
<div className={styles.passwordField}>
<input
id={`${fid}-apiKey`}
className={styles.passwordInput}
type={showSingleApiKey ? 'text' : 'password'}
value={form.apiKey}
onChange={(e) => updateField('apiKey', e.target.value)}
placeholder={
mode === 'edit'
? t('providersPage.form.apiKeyEditPlaceholder')
: t('providersPage.form.apiKeyCreatePlaceholder')
}
disabled={mutating}
/>
<button
type="button"
className={styles.passwordToggle}
onClick={() => setShowSingleApiKey((v) => !v)}
disabled={mutating}
aria-label={
showSingleApiKey
? t('providersPage.form.hideApiKey')
: t('providersPage.form.showApiKey')
}
title={
showSingleApiKey
? t('providersPage.form.hideApiKey')
: t('providersPage.form.showApiKey')
}
>
{showSingleApiKey ? (
<IconEyeOff size={16} />
) : (
<IconEye size={16} />
)}
</button>
</div>
</div>
) : null}
@@ -709,21 +750,45 @@ export function BaseProviderForm({
<label className={styles.label}>
{t('providersPage.form.apiKey')}
</label>
<input
className={styles.input}
type="password"
value={entry.apiKey}
onChange={(e) =>
updateField(
'apiKeyEntries',
apiKeyEntries.map((it, i) =>
i === realIdx ? { ...it, apiKey: e.target.value } : it
<div className={styles.passwordField}>
<input
className={styles.passwordInput}
type={showPasswords.has(realIdx) ? 'text' : 'password'}
value={entry.apiKey}
onChange={(e) =>
updateField(
'apiKeyEntries',
apiKeyEntries.map((it, i) =>
i === realIdx ? { ...it, apiKey: e.target.value } : it
)
)
)
}
disabled={mutating}
placeholder={t('providersPage.form.apiKeyCreatePlaceholder')}
/>
}
disabled={mutating}
placeholder={t('providersPage.form.apiKeyCreatePlaceholder')}
/>
<button
type="button"
className={styles.passwordToggle}
onClick={() => togglePasswordVisibility(realIdx)}
disabled={mutating}
aria-label={
showPasswords.has(realIdx)
? t('providersPage.form.hideApiKey')
: t('providersPage.form.showApiKey')
}
title={
showPasswords.has(realIdx)
? t('providersPage.form.hideApiKey')
: t('providersPage.form.showApiKey')
}
>
{showPasswords.has(realIdx) ? (
<IconEyeOff size={16} />
) : (
<IconEye size={16} />
)}
</button>
</div>
</div>
<div className={styles.field}>
<label className={styles.label}>
@@ -494,6 +494,41 @@
}
}
.passwordField {
position: relative;
display: flex;
align-items: center;
}
.passwordInput {
@extend .input;
padding-right: 36px;
}
.passwordToggle {
position: absolute;
right: 8px;
display: flex;
align-items: center;
justify-content: center;
padding: 4px;
border: none;
background: transparent;
color: var(--text-tertiary);
cursor: pointer;
border-radius: var(--radius-sm);
transition: color $transition-fast;
&:hover:not(:disabled) {
color: var(--text-primary);
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
.entriesList {
display: flex;
flex-direction: column;
+2
View File
@@ -1313,6 +1313,8 @@
"apiKeyEntriesSection": "API key entries",
"apiKeyEntry": "Key #{{index}}",
"addApiKeyEntry": "Add key entry",
"showApiKey": "Show API key",
"hideApiKey": "Hide API key",
"validation": {
"nameRequired": "Name is required",
"apiKeyRequired": "At least one API key is required",
+2
View File
@@ -1310,6 +1310,8 @@
"apiKeyEntriesSection": "API-ключи",
"apiKeyEntry": "Ключ #{{index}}",
"addApiKeyEntry": "Добавить ключ",
"showApiKey": "Показать ключ API",
"hideApiKey": "Скрыть ключ API",
"validation": {
"nameRequired": "Название обязательно",
"apiKeyRequired": "Нужен хотя бы один API-ключ",
+2
View File
@@ -1313,6 +1313,8 @@
"apiKeyEntriesSection": "API 密钥条目",
"apiKeyEntry": "密钥 #{{index}}",
"addApiKeyEntry": "添加密钥条目",
"showApiKey": "显示密钥",
"hideApiKey": "隐藏密钥",
"validation": {
"nameRequired": "名称必填",
"apiKeyRequired": "至少填写一个 API 密钥",
+2
View File
@@ -1339,6 +1339,8 @@
"apiKeyEntriesSection": "API 金鑰條目",
"apiKeyEntry": "金鑰 #{{index}}",
"addApiKeyEntry": "新增金鑰條目",
"showApiKey": "顯示金鑰",
"hideApiKey": "隱藏金鑰",
"validation": {
"nameRequired": "名稱必填",
"apiKeyRequired": "至少填寫一個 API 金鑰",