fix(settings): 防止凭据字段清空已有密钥 (#49)

Co-authored-by: Cooper-X-Oak <Cooper-X-Oak@users.noreply.github.com>
This commit is contained in:
Cooper OakandCooper-X-Oak authored and GitHub committed 2026-04-30 14:16:58 +08:00
1 parent 3d6870822f
commit bb7300ee85
1 file changed
+75 -10
+75 -10
View File
@@ -349,11 +349,35 @@ function CredentialField({ label, account, placeholder, mono, mask, defaultValue
const { t } = useTranslation();
const [value, setValue] = useState('');
const [revealed, setRevealed] = useState(false);
const [saved, setSaved] = useState(false);
const [loaded, setLoaded] = useState(false);
const [dirty, setDirty] = useState(false);
const [status, setStatus] = useState<'idle' | 'saving' | 'saved' | 'saveError' | 'copied' | 'copyError'>('idle');
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
useEffect(() => {
readCredential(account).then(v => setValue(v ?? ''));
let cancelled = false;
setLoaded(false);
setDirty(false);
setStatus('idle');
if (debounceRef.current) {
clearTimeout(debounceRef.current);
debounceRef.current = null;
}
readCredential(account)
.then(v => {
if (cancelled) return;
setValue(v ?? '');
setLoaded(true);
})
.catch(error => {
if (cancelled) return;
console.error('[settings] failed to read credential', account, error);
setLoaded(true);
setStatus('saveError');
});
return () => {
cancelled = true;
};
}, [account]);
useEffect(() => {
@@ -363,19 +387,29 @@ function CredentialField({ label, account, placeholder, mono, mask, defaultValue
}, []);
const save = async (v: string) => {
await setCredential(account, v);
setSaved(true);
setTimeout(() => setSaved(false), 1200);
if (!loaded) return;
setStatus('saving');
try {
await setCredential(account, v);
setDirty(false);
setStatus('saved');
} catch (error) {
console.error('[settings] failed to save credential', account, error);
setStatus('saveError');
}
window.setTimeout(() => setStatus('idle'), 1600);
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const v = e.target.value;
setValue(v);
setDirty(true);
if (debounceRef.current) clearTimeout(debounceRef.current);
debounceRef.current = setTimeout(() => save(v), 300);
};
const onBlur = () => {
if (!loaded || !dirty) return;
if (debounceRef.current) {
clearTimeout(debounceRef.current);
debounceRef.current = null;
@@ -384,11 +418,27 @@ function CredentialField({ label, account, placeholder, mono, mask, defaultValue
};
const fillDefault = async () => {
if (!defaultValue) return;
if (!loaded || !defaultValue) return;
setValue(defaultValue);
setDirty(true);
await save(defaultValue);
};
const onCopy = async () => {
if (!value) return;
try {
if (!navigator.clipboard?.writeText) {
throw new Error('Clipboard API unavailable');
}
await navigator.clipboard.writeText(value);
setStatus('copied');
} catch (error) {
console.error('[settings] failed to copy credential', account, error);
setStatus('copyError');
}
window.setTimeout(() => setStatus('idle'), 1600);
};
const inputType = mask && !revealed ? 'password' : 'text';
return (
@@ -400,10 +450,11 @@ function CredentialField({ label, account, placeholder, mono, mask, defaultValue
placeholder={placeholder}
onChange={handleChange}
onBlur={onBlur}
disabled={!loaded}
style={{ ...inputStyle, fontFamily: mono ? 'var(--ol-font-mono)' : 'inherit' }}
/>
{defaultValue && !value && (
<button onClick={fillDefault} title={t('settings.providers.fillDefault')} style={iconBtnStyle}>
<button onClick={fillDefault} title={t('settings.providers.fillDefault')} style={iconBtnStyle} disabled={!loaded}>
<Icon name="check" size={13} />
</button>
)}
@@ -417,15 +468,29 @@ function CredentialField({ label, account, placeholder, mono, mask, defaultValue
</button>
)}
<button
onClick={() => navigator.clipboard?.writeText(value)}
onClick={onCopy}
title={t('common.copy')}
style={iconBtnStyle}
disabled={!value}
>
<Icon name="copy" size={14} />
</button>
{saved && (
<span style={{ fontSize: 11, color: 'var(--ol-ok)', whiteSpace: 'nowrap' }}>{t('common.saved')}</span>
{status !== 'idle' && (
<span
style={{
fontSize: 11,
color: status.endsWith('Error') ? 'var(--ol-warn)' : 'var(--ol-ok)',
whiteSpace: 'nowrap',
}}
>
{status === 'saving'
? '保存中'
: status === 'saved'
? t('common.saved')
: status === 'copied'
? '已复制'
: '操作失败'}
</span>
)}
</div>
</SettingRow>