fix: improve UI state updates after form submissions

- StrategyStudioPage: auto-select new strategy after creation, clear selection after deletion
- ModelConfigModal: add loading state and async onSave handling
- ExchangeConfigModal: add loading state to prevent duplicate submissions
This commit is contained in:
tinkle-community
2025-12-08 11:21:09 +08:00
parent 4a0f56f1ee
commit 10047577e1
3 changed files with 106 additions and 66 deletions

View File

@@ -84,6 +84,9 @@ export function ExchangeConfigModal({
null | 'hyperliquid' | 'aster' | 'lighter' null | 'hyperliquid' | 'aster' | 'lighter'
>(null) >(null)
// 保存中状态
const [isSaving, setIsSaving] = useState(false)
// 获取当前编辑的交易所信息 // 获取当前编辑的交易所信息
const selectedExchange = allExchanges?.find( const selectedExchange = allExchanges?.find(
(e) => e.id === selectedExchangeId (e) => e.id === selectedExchangeId
@@ -218,59 +221,64 @@ export function ExchangeConfigModal({
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault() e.preventDefault()
if (!selectedExchangeId) return if (!selectedExchangeId || isSaving) return
// 根据交易所类型验证不同字段 setIsSaving(true)
if (selectedExchange?.id === 'binance') { try {
if (!apiKey.trim() || !secretKey.trim()) return // 根据交易所类型验证不同字段
await onSave(selectedExchangeId, apiKey.trim(), secretKey.trim(), '', testnet) if (selectedExchange?.id === 'binance') {
} else if (selectedExchange?.id === 'okx') { if (!apiKey.trim() || !secretKey.trim()) return
if (!apiKey.trim() || !secretKey.trim() || !passphrase.trim()) return await onSave(selectedExchangeId, apiKey.trim(), secretKey.trim(), '', testnet)
await onSave(selectedExchangeId, apiKey.trim(), secretKey.trim(), passphrase.trim(), testnet) } else if (selectedExchange?.id === 'okx') {
} else if (selectedExchange?.id === 'hyperliquid') { if (!apiKey.trim() || !secretKey.trim() || !passphrase.trim()) return
if (!apiKey.trim() || !hyperliquidWalletAddr.trim()) return // 验证私钥和钱包地址 await onSave(selectedExchangeId, apiKey.trim(), secretKey.trim(), passphrase.trim(), testnet)
await onSave( } else if (selectedExchange?.id === 'hyperliquid') {
selectedExchangeId, if (!apiKey.trim() || !hyperliquidWalletAddr.trim()) return // 验证私钥和钱包地址
apiKey.trim(), await onSave(
'', selectedExchangeId,
'', apiKey.trim(),
testnet, '',
hyperliquidWalletAddr.trim() '',
) testnet,
} else if (selectedExchange?.id === 'aster') { hyperliquidWalletAddr.trim()
if (!asterUser.trim() || !asterSigner.trim() || !asterPrivateKey.trim()) )
return } else if (selectedExchange?.id === 'aster') {
await onSave( if (!asterUser.trim() || !asterSigner.trim() || !asterPrivateKey.trim())
selectedExchangeId, return
'', await onSave(
'', selectedExchangeId,
'', '',
testnet, '',
undefined, '',
asterUser.trim(), testnet,
asterSigner.trim(), undefined,
asterPrivateKey.trim() asterUser.trim(),
) asterSigner.trim(),
} else if (selectedExchange?.id === 'lighter') { asterPrivateKey.trim()
if (!lighterWalletAddr.trim() || !lighterPrivateKey.trim()) return )
await onSave( } else if (selectedExchange?.id === 'lighter') {
selectedExchangeId, if (!lighterWalletAddr.trim() || !lighterPrivateKey.trim()) return
lighterPrivateKey.trim(), await onSave(
'', selectedExchangeId,
'', lighterPrivateKey.trim(),
testnet, '',
lighterWalletAddr.trim(), '',
undefined, testnet,
undefined, lighterWalletAddr.trim(),
undefined, undefined,
lighterWalletAddr.trim(), undefined,
lighterPrivateKey.trim(), undefined,
lighterApiKeyPrivateKey.trim() lighterWalletAddr.trim(),
) lighterPrivateKey.trim(),
} else { lighterApiKeyPrivateKey.trim()
// 默认情况其他CEX交易所 )
if (!apiKey.trim() || !secretKey.trim()) return } else {
await onSave(selectedExchangeId, apiKey.trim(), secretKey.trim(), '', testnet) // 默认情况其他CEX交易所
if (!apiKey.trim() || !secretKey.trim()) return
await onSave(selectedExchangeId, apiKey.trim(), secretKey.trim(), '', testnet)
}
} finally {
setIsSaving(false)
} }
} }
@@ -1000,6 +1008,7 @@ export function ExchangeConfigModal({
<button <button
type="submit" type="submit"
disabled={ disabled={
isSaving ||
!selectedExchange || !selectedExchange ||
(selectedExchange.id === 'binance' && (selectedExchange.id === 'binance' &&
(!apiKey.trim() || !secretKey.trim())) || (!apiKey.trim() || !secretKey.trim())) ||
@@ -1029,7 +1038,7 @@ export function ExchangeConfigModal({
className="flex-1 px-4 py-2 rounded text-sm font-semibold disabled:opacity-50" className="flex-1 px-4 py-2 rounded text-sm font-semibold disabled:opacity-50"
style={{ background: '#F0B90B', color: '#000' }} style={{ background: '#F0B90B', color: '#000' }}
> >
{t('saveConfig', language)} {isSaving ? t('saving', language) || '保存中...' : t('saveConfig', language)}
</button> </button>
</div> </div>
</form> </form>

View File

@@ -14,8 +14,8 @@ interface ModelConfigModalProps {
apiKey: string, apiKey: string,
baseUrl?: string, baseUrl?: string,
modelName?: string modelName?: string
) => void ) => Promise<void>
onDelete: (modelId: string) => void onDelete: (modelId: string) => Promise<void>
onClose: () => void onClose: () => void
language: Language language: Language
} }
@@ -48,16 +48,23 @@ export function ModelConfigModal({
} }
}, [editingModelId, selectedModel]) }, [editingModelId, selectedModel])
const handleSubmit = (e: React.FormEvent) => { const [isSaving, setIsSaving] = useState(false)
e.preventDefault()
if (!selectedModelId || !apiKey.trim()) return
onSave( const handleSubmit = async (e: React.FormEvent) => {
selectedModelId, e.preventDefault()
apiKey.trim(), if (!selectedModelId || !apiKey.trim() || isSaving) return
baseUrl.trim() || undefined,
modelName.trim() || undefined setIsSaving(true)
) try {
await onSave(
selectedModelId,
apiKey.trim(),
baseUrl.trim() || undefined,
modelName.trim() || undefined
)
} finally {
setIsSaving(false)
}
} }
// 可选择的模型列表(所有支持的模型) // 可选择的模型列表(所有支持的模型)
@@ -277,11 +284,11 @@ export function ModelConfigModal({
</button> </button>
<button <button
type="submit" type="submit"
disabled={!selectedModel || !apiKey.trim()} disabled={!selectedModel || !apiKey.trim() || isSaving}
className="flex-1 px-4 py-2 rounded text-sm font-semibold disabled:opacity-50" className="flex-1 px-4 py-2 rounded text-sm font-semibold disabled:opacity-50"
style={{ background: '#F0B90B', color: '#000' }} style={{ background: '#F0B90B', color: '#000' }}
> >
{t('saveConfig', language)} {isSaving ? t('saving', language) || '保存中...' : t('saveConfig', language)}
</button> </button>
</div> </div>
</form> </form>

View File

@@ -168,7 +168,25 @@ export function StrategyStudioPage() {
}), }),
}) })
if (!response.ok) throw new Error('Failed to create strategy') if (!response.ok) throw new Error('Failed to create strategy')
const result = await response.json()
await fetchStrategies() await fetchStrategies()
// Auto-select the newly created strategy
if (result.id) {
const now = new Date().toISOString()
const newStrategy = {
id: result.id,
name: language === 'zh' ? '新策略' : 'New Strategy',
description: '',
is_active: false,
is_default: false,
config: defaultConfig,
created_at: now,
updated_at: now,
}
setSelectedStrategy(newStrategy)
setEditingConfig(defaultConfig)
setHasChanges(false)
}
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error') setError(err instanceof Error ? err.message : 'Unknown error')
} }
@@ -195,6 +213,12 @@ export function StrategyStudioPage() {
}) })
if (!response.ok) throw new Error('Failed to delete strategy') if (!response.ok) throw new Error('Failed to delete strategy')
notify.success(language === 'zh' ? '策略已删除' : 'Strategy deleted') notify.success(language === 'zh' ? '策略已删除' : 'Strategy deleted')
// Clear selection if deleted strategy was selected
if (selectedStrategy?.id === id) {
setSelectedStrategy(null)
setEditingConfig(null)
setHasChanges(false)
}
await fetchStrategies() await fetchStrategies()
} catch (err) { } catch (err) {
const errorMsg = err instanceof Error ? err.message : 'Unknown error' const errorMsg = err instanceof Error ? err.message : 'Unknown error'