diff --git a/openless-all/app/src/pages/Settings.tsx b/openless-all/app/src/pages/Settings.tsx index 5a9aa2f9..58feb58c 100644 --- a/openless-all/app/src/pages/Settings.tsx +++ b/openless-all/app/src/pages/Settings.tsx @@ -353,11 +353,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 | 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(() => { @@ -367,19 +391,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) => { 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; @@ -388,11 +422,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 ( @@ -404,10 +454,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 && ( - )} @@ -421,15 +472,29 @@ function CredentialField({ label, account, placeholder, mono, mask, defaultValue )} - {saved && ( - {t('common.saved')} + {status !== 'idle' && ( + + {status === 'saving' + ? '保存中' + : status === 'saved' + ? t('common.saved') + : status === 'copied' + ? '已复制' + : '操作失败'} + )}