mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-13 15:57:01 +08:00
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:
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
Reference in New Issue
Block a user