mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-15 00:36:56 +08:00
feat: improve model/exchange deletion and selection logic
- Add soft delete support for AI models * Add deleted field to ai_models table * Implement soft delete with sensitive data cleanup * Filter deleted records in queries - Replace browser confirm dialogs with custom styled modals * Create DeleteConfirmModal component with Binance theme * Add proper warning messages and icons * Improve user experience with consistent styling - Fix duplicate model/exchange display in selection dropdowns * Use supportedModels/supportedExchanges for modal selectors * Use configuredModels/configuredExchanges for panel display * Remove redundant selectableModels/selectableExchanges logic - Enhance data management * Refresh data after deletion operations * Proper state cleanup after modal operations * Clear sensitive data during soft delete 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: tinkle-community <tinklefund@gmail.com>
This commit is contained in:
@@ -140,8 +140,9 @@ func (d *PostgreSQLDatabase) GetAIModels(userID string) ([]*AIModelConfig, error
|
|||||||
SELECT id, user_id, name, provider, enabled, api_key,
|
SELECT id, user_id, name, provider, enabled, api_key,
|
||||||
COALESCE(custom_api_url, '') as custom_api_url,
|
COALESCE(custom_api_url, '') as custom_api_url,
|
||||||
COALESCE(custom_model_name, '') as custom_model_name,
|
COALESCE(custom_model_name, '') as custom_model_name,
|
||||||
|
COALESCE(deleted, FALSE) as deleted,
|
||||||
created_at, updated_at
|
created_at, updated_at
|
||||||
FROM ai_models WHERE user_id = $1 ORDER BY id
|
FROM ai_models WHERE user_id = $1 AND COALESCE(deleted, FALSE) = FALSE ORDER BY id
|
||||||
`, userID)
|
`, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -152,10 +153,11 @@ func (d *PostgreSQLDatabase) GetAIModels(userID string) ([]*AIModelConfig, error
|
|||||||
models := make([]*AIModelConfig, 0)
|
models := make([]*AIModelConfig, 0)
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var model AIModelConfig
|
var model AIModelConfig
|
||||||
|
var deleted bool // 临时变量,用于读取 deleted 字段但不保存到结构体
|
||||||
err := rows.Scan(
|
err := rows.Scan(
|
||||||
&model.ID, &model.UserID, &model.Name, &model.Provider,
|
&model.ID, &model.UserID, &model.Name, &model.Provider,
|
||||||
&model.Enabled, &model.APIKey, &model.CustomAPIURL, &model.CustomModelName,
|
&model.Enabled, &model.APIKey, &model.CustomAPIURL, &model.CustomModelName,
|
||||||
&model.CreatedAt, &model.UpdatedAt,
|
&deleted, &model.CreatedAt, &model.UpdatedAt,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -168,7 +170,59 @@ func (d *PostgreSQLDatabase) GetAIModels(userID string) ([]*AIModelConfig, error
|
|||||||
|
|
||||||
// UpdateAIModel 更新AI模型配置,如果不存在则创建用户特定配置
|
// UpdateAIModel 更新AI模型配置,如果不存在则创建用户特定配置
|
||||||
func (d *PostgreSQLDatabase) UpdateAIModel(userID, id string, enabled bool, apiKey, customAPIURL, customModelName string) error {
|
func (d *PostgreSQLDatabase) UpdateAIModel(userID, id string, enabled bool, apiKey, customAPIURL, customModelName string) error {
|
||||||
// 先尝试精确匹配 ID(新版逻辑,支持多个相同 provider 的模型)
|
log.Printf("🔧 UpdateAIModel: userID=%s, id=%s, enabled=%v", userID, id, enabled)
|
||||||
|
|
||||||
|
// 检查是否为删除操作(API Key 为空且 enabled 为 false 表示删除)
|
||||||
|
isDelete := !enabled && apiKey == "" && customAPIURL == "" && customModelName == ""
|
||||||
|
|
||||||
|
if isDelete {
|
||||||
|
// 执行软删除:标记为已删除并清空敏感数据
|
||||||
|
// 先尝试精确匹配 ID
|
||||||
|
var existingID string
|
||||||
|
err := d.db.QueryRow(`
|
||||||
|
SELECT id FROM ai_models WHERE user_id = $1 AND id = $2 LIMIT 1
|
||||||
|
`, userID, id).Scan(&existingID)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
// 找到了现有配置(精确匹配 ID),标记为删除并清空敏感数据
|
||||||
|
_, err = d.db.Exec(`
|
||||||
|
UPDATE ai_models SET enabled = FALSE, deleted = TRUE, api_key = '', custom_api_url = '', custom_model_name = '', updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = $1 AND user_id = $2
|
||||||
|
`, existingID, userID)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("❌ UpdateAIModel: 标记删除失败: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Printf("🗑️ UpdateAIModel: 已标记删除用户 %s 的模型配置 %s", userID, existingID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID 不存在,尝试兼容旧逻辑:将 id 作为 provider 查找
|
||||||
|
provider := id
|
||||||
|
err = d.db.QueryRow(`
|
||||||
|
SELECT id FROM ai_models WHERE user_id = $1 AND provider = $2 LIMIT 1
|
||||||
|
`, userID, provider).Scan(&existingID)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
// 找到了现有配置(通过 provider 匹配),标记为删除并清空敏感数据
|
||||||
|
_, err = d.db.Exec(`
|
||||||
|
UPDATE ai_models SET enabled = FALSE, deleted = TRUE, api_key = '', custom_api_url = '', custom_model_name = '', updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = $1 AND user_id = $2
|
||||||
|
`, existingID, userID)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("❌ UpdateAIModel: 标记删除失败: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Printf("🗑️ UpdateAIModel: 已标记删除用户 %s 的模型配置 %s (通过provider匹配)", userID, existingID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 没有找到配置,返回成功(幂等性)
|
||||||
|
log.Printf("ℹ️ UpdateAIModel: 模型配置不存在,跳过删除: %s", id)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启用模型的情况:先尝试精确匹配 ID(新版逻辑,支持多个相同 provider 的模型)
|
||||||
var existingID string
|
var existingID string
|
||||||
err := d.db.QueryRow(`
|
err := d.db.QueryRow(`
|
||||||
SELECT id FROM ai_models WHERE user_id = $1 AND id = $2 LIMIT 1
|
SELECT id FROM ai_models WHERE user_id = $1 AND id = $2 LIMIT 1
|
||||||
@@ -177,7 +231,7 @@ func (d *PostgreSQLDatabase) UpdateAIModel(userID, id string, enabled bool, apiK
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
// 找到了现有配置(精确匹配 ID),更新它
|
// 找到了现有配置(精确匹配 ID),更新它
|
||||||
_, err = d.db.Exec(`
|
_, err = d.db.Exec(`
|
||||||
UPDATE ai_models SET enabled = $1, api_key = $2, custom_api_url = $3, custom_model_name = $4, updated_at = CURRENT_TIMESTAMP
|
UPDATE ai_models SET enabled = $1, api_key = $2, custom_api_url = $3, custom_model_name = $4, deleted = FALSE, updated_at = CURRENT_TIMESTAMP
|
||||||
WHERE id = $5 AND user_id = $6
|
WHERE id = $5 AND user_id = $6
|
||||||
`, enabled, apiKey, customAPIURL, customModelName, existingID, userID)
|
`, enabled, apiKey, customAPIURL, customModelName, existingID, userID)
|
||||||
return err
|
return err
|
||||||
@@ -193,7 +247,7 @@ func (d *PostgreSQLDatabase) UpdateAIModel(userID, id string, enabled bool, apiK
|
|||||||
// 找到了现有配置(通过 provider 匹配,兼容旧版),更新它
|
// 找到了现有配置(通过 provider 匹配,兼容旧版),更新它
|
||||||
log.Printf("⚠️ 使用旧版 provider 匹配更新模型: %s -> %s", provider, existingID)
|
log.Printf("⚠️ 使用旧版 provider 匹配更新模型: %s -> %s", provider, existingID)
|
||||||
_, err = d.db.Exec(`
|
_, err = d.db.Exec(`
|
||||||
UPDATE ai_models SET enabled = $1, api_key = $2, custom_api_url = $3, custom_model_name = $4, updated_at = CURRENT_TIMESTAMP
|
UPDATE ai_models SET enabled = $1, api_key = $2, custom_api_url = $3, custom_model_name = $4, deleted = FALSE, updated_at = CURRENT_TIMESTAMP
|
||||||
WHERE id = $5 AND user_id = $6
|
WHERE id = $5 AND user_id = $6
|
||||||
`, enabled, apiKey, customAPIURL, customModelName, existingID, userID)
|
`, enabled, apiKey, customAPIURL, customModelName, existingID, userID)
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ CREATE TABLE IF NOT EXISTS ai_models (
|
|||||||
api_key TEXT DEFAULT '',
|
api_key TEXT DEFAULT '',
|
||||||
custom_api_url TEXT DEFAULT '',
|
custom_api_url TEXT DEFAULT '',
|
||||||
custom_model_name TEXT DEFAULT '',
|
custom_model_name TEXT DEFAULT '',
|
||||||
|
deleted BOOLEAN DEFAULT FALSE,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||||
@@ -167,6 +168,9 @@ INSERT INTO system_config (key, value) VALUES
|
|||||||
('jwt_secret', '')
|
('jwt_secret', '')
|
||||||
ON CONFLICT (key) DO NOTHING;
|
ON CONFLICT (key) DO NOTHING;
|
||||||
|
|
||||||
|
-- 数据库迁移:添加 deleted 字段到现有 ai_models 表
|
||||||
|
ALTER TABLE ai_models ADD COLUMN IF NOT EXISTS deleted BOOLEAN DEFAULT FALSE;
|
||||||
|
|
||||||
-- 创建索引
|
-- 创建索引
|
||||||
CREATE INDEX IF NOT EXISTS idx_ai_models_user_id ON ai_models(user_id);
|
CREATE INDEX IF NOT EXISTS idx_ai_models_user_id ON ai_models(user_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_exchanges_user_id ON exchanges(user_id);
|
CREATE INDEX IF NOT EXISTS idx_exchanges_user_id ON exchanges(user_id);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect, useMemo } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
import useSWR from 'swr'
|
import useSWR from 'swr'
|
||||||
import { api } from '../lib/api'
|
import { api } from '../lib/api'
|
||||||
import type {
|
import type {
|
||||||
@@ -58,6 +58,8 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
|||||||
const [showModelModal, setShowModelModal] = useState(false)
|
const [showModelModal, setShowModelModal] = useState(false)
|
||||||
const [showExchangeModal, setShowExchangeModal] = useState(false)
|
const [showExchangeModal, setShowExchangeModal] = useState(false)
|
||||||
const [showSignalSourceModal, setShowSignalSourceModal] = useState(false)
|
const [showSignalSourceModal, setShowSignalSourceModal] = useState(false)
|
||||||
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
|
||||||
|
const [deleteTarget, setDeleteTarget] = useState<{type: 'model' | 'exchange', id: string} | null>(null)
|
||||||
const [editingModel, setEditingModel] = useState<string | null>(null)
|
const [editingModel, setEditingModel] = useState<string | null>(null)
|
||||||
const [editingExchange, setEditingExchange] = useState<string | null>(null)
|
const [editingExchange, setEditingExchange] = useState<string | null>(null)
|
||||||
const [editingTrader, setEditingTrader] = useState<any>(null)
|
const [editingTrader, setEditingTrader] = useState<any>(null)
|
||||||
@@ -135,20 +137,6 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
|||||||
const configuredModels = allModels || []
|
const configuredModels = allModels || []
|
||||||
const configuredExchanges = allExchanges || []
|
const configuredExchanges = allExchanges || []
|
||||||
|
|
||||||
const selectableModels = useMemo(() => {
|
|
||||||
return (supportedModels || []).map((model) => {
|
|
||||||
const configured = allModels?.find((m) => m.id === model.id)
|
|
||||||
return configured ? { ...model, ...configured } : model
|
|
||||||
})
|
|
||||||
}, [supportedModels, allModels])
|
|
||||||
|
|
||||||
const selectableExchanges = useMemo(() => {
|
|
||||||
return (supportedExchanges || []).map((exchange) => {
|
|
||||||
const configured = allExchanges?.find((e) => e.id === exchange.id)
|
|
||||||
return configured ? { ...exchange, ...configured } : exchange
|
|
||||||
})
|
|
||||||
}, [supportedExchanges, allExchanges])
|
|
||||||
|
|
||||||
// 只在创建交易员时使用已启用且配置完整的
|
// 只在创建交易员时使用已启用且配置完整的
|
||||||
const enabledModels = allModels?.filter((m) => m.enabled && m.apiKey) || []
|
const enabledModels = allModels?.filter((m) => m.enabled && m.apiKey) || []
|
||||||
const enabledExchanges =
|
const enabledExchanges =
|
||||||
@@ -313,8 +301,6 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleDeleteModelConfig = async (modelId: string) => {
|
const handleDeleteModelConfig = async (modelId: string) => {
|
||||||
if (!confirm(t('confirmDeleteModel', language))) return
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const updatedModels =
|
const updatedModels =
|
||||||
allModels?.map((m) =>
|
allModels?.map((m) =>
|
||||||
@@ -344,15 +330,31 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await api.updateModelConfigs(request)
|
await api.updateModelConfigs(request)
|
||||||
setAllModels(updatedModels)
|
|
||||||
|
// 重新获取用户配置以确保数据同步
|
||||||
|
const refreshedModels = await api.getModelConfigs()
|
||||||
|
setAllModels(refreshedModels)
|
||||||
|
|
||||||
setShowModelModal(false)
|
setShowModelModal(false)
|
||||||
setEditingModel(null)
|
setEditingModel(null)
|
||||||
|
setShowDeleteConfirm(false)
|
||||||
|
setDeleteTarget(null)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to delete model config:', error)
|
console.error('Failed to delete model config:', error)
|
||||||
alert(t('deleteConfigFailed', language))
|
alert(t('deleteConfigFailed', language))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleConfirmDelete = () => {
|
||||||
|
if (!deleteTarget) return
|
||||||
|
|
||||||
|
if (deleteTarget.type === 'model') {
|
||||||
|
handleDeleteModelConfig(deleteTarget.id)
|
||||||
|
} else if (deleteTarget.type === 'exchange') {
|
||||||
|
handleDeleteExchangeConfig(deleteTarget.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleSaveModelConfig = async (
|
const handleSaveModelConfig = async (
|
||||||
modelId: string,
|
modelId: string,
|
||||||
apiKey: string,
|
apiKey: string,
|
||||||
@@ -427,8 +429,6 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleDeleteExchangeConfig = async (exchangeId: string) => {
|
const handleDeleteExchangeConfig = async (exchangeId: string) => {
|
||||||
if (!confirm(t('confirmDeleteExchange', language))) return
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const request = {
|
const request = {
|
||||||
exchanges: {
|
exchanges: {
|
||||||
@@ -451,6 +451,8 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
|||||||
setAllExchanges(refreshed)
|
setAllExchanges(refreshed)
|
||||||
setShowExchangeModal(false)
|
setShowExchangeModal(false)
|
||||||
setEditingExchange(null)
|
setEditingExchange(null)
|
||||||
|
setShowDeleteConfirm(false)
|
||||||
|
setDeleteTarget(null)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to delete exchange config:', error)
|
console.error('Failed to delete exchange config:', error)
|
||||||
alert(t('deleteExchangeConfigFailed', language))
|
alert(t('deleteExchangeConfigFailed', language))
|
||||||
@@ -1043,15 +1045,18 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
|||||||
{/* Model Configuration Modal */}
|
{/* Model Configuration Modal */}
|
||||||
{showModelModal && (
|
{showModelModal && (
|
||||||
<ModelConfigModal
|
<ModelConfigModal
|
||||||
allModels={selectableModels}
|
supportedModels={supportedModels}
|
||||||
configuredModels={allModels}
|
configuredModels={allModels}
|
||||||
editingModelId={editingModel}
|
editingModelId={editingModel}
|
||||||
onSave={handleSaveModelConfig}
|
onSave={handleSaveModelConfig}
|
||||||
onDelete={handleDeleteModelConfig}
|
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setShowModelModal(false)
|
setShowModelModal(false)
|
||||||
setEditingModel(null)
|
setEditingModel(null)
|
||||||
}}
|
}}
|
||||||
|
onDelete={(modelId) => {
|
||||||
|
setDeleteTarget({ type: 'model', id: modelId })
|
||||||
|
setShowDeleteConfirm(true)
|
||||||
|
}}
|
||||||
language={language}
|
language={language}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -1059,15 +1064,18 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
|||||||
{/* Exchange Configuration Modal */}
|
{/* Exchange Configuration Modal */}
|
||||||
{showExchangeModal && (
|
{showExchangeModal && (
|
||||||
<ExchangeConfigModal
|
<ExchangeConfigModal
|
||||||
allExchanges={selectableExchanges}
|
supportedExchanges={supportedExchanges}
|
||||||
configuredExchanges={allExchanges}
|
configuredExchanges={allExchanges}
|
||||||
editingExchangeId={editingExchange}
|
editingExchangeId={editingExchange}
|
||||||
onSave={handleSaveExchangeConfig}
|
onSave={handleSaveExchangeConfig}
|
||||||
onDelete={handleDeleteExchangeConfig}
|
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setShowExchangeModal(false)
|
setShowExchangeModal(false)
|
||||||
setEditingExchange(null)
|
setEditingExchange(null)
|
||||||
}}
|
}}
|
||||||
|
onDelete={(exchangeId) => {
|
||||||
|
setDeleteTarget({ type: 'exchange', id: exchangeId })
|
||||||
|
setShowDeleteConfirm(true)
|
||||||
|
}}
|
||||||
language={language}
|
language={language}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -1082,6 +1090,27 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
|||||||
language={language}
|
language={language}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Delete Confirmation Modal */}
|
||||||
|
{showDeleteConfirm && deleteTarget && (
|
||||||
|
<DeleteConfirmModal
|
||||||
|
isOpen={showDeleteConfirm}
|
||||||
|
title={deleteTarget.type === 'model'
|
||||||
|
? t('confirmDeleteModel', language)
|
||||||
|
: t('confirmDeleteExchange', language)
|
||||||
|
}
|
||||||
|
message={deleteTarget.type === 'model'
|
||||||
|
? t('deleteModelWarning', language)
|
||||||
|
: t('deleteExchangeWarning', language)
|
||||||
|
}
|
||||||
|
onConfirm={handleConfirmDelete}
|
||||||
|
onCancel={() => {
|
||||||
|
setShowDeleteConfirm(false)
|
||||||
|
setDeleteTarget(null)
|
||||||
|
}}
|
||||||
|
language={language}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1255,17 +1284,80 @@ function SignalSourceModal({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete Confirmation Modal Component
|
||||||
|
function DeleteConfirmModal({
|
||||||
|
isOpen,
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
onConfirm,
|
||||||
|
onCancel,
|
||||||
|
language,
|
||||||
|
}: {
|
||||||
|
isOpen: boolean
|
||||||
|
title: string
|
||||||
|
message: string
|
||||||
|
onConfirm: () => void
|
||||||
|
onCancel: () => void
|
||||||
|
language: Language
|
||||||
|
}) {
|
||||||
|
if (!isOpen) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||||
|
<div
|
||||||
|
className="bg-gray-800 rounded-lg p-6 w-full max-w-md relative"
|
||||||
|
style={{ background: '#1E2329' }}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3 mb-4">
|
||||||
|
<div
|
||||||
|
className="w-8 h-8 rounded-full flex items-center justify-center"
|
||||||
|
style={{ background: 'rgba(246, 70, 93, 0.1)' }}
|
||||||
|
>
|
||||||
|
<AlertTriangle className="w-5 h-5" style={{ color: '#F6465D' }} />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-bold" style={{ color: '#EAECEF' }}>
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-sm mb-6" style={{ color: '#848E9C' }}>
|
||||||
|
{message}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onCancel}
|
||||||
|
className="flex-1 px-4 py-2 rounded text-sm font-semibold transition-all hover:scale-105"
|
||||||
|
style={{ background: '#2B3139', color: '#848E9C' }}
|
||||||
|
>
|
||||||
|
{t('cancel', language)}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onConfirm}
|
||||||
|
className="flex-1 px-4 py-2 rounded text-sm font-semibold transition-all hover:scale-105"
|
||||||
|
style={{ background: '#F6465D', color: '#fff' }}
|
||||||
|
>
|
||||||
|
{t('delete', language)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Model Configuration Modal Component
|
// Model Configuration Modal Component
|
||||||
function ModelConfigModal({
|
function ModelConfigModal({
|
||||||
allModels,
|
supportedModels,
|
||||||
configuredModels,
|
configuredModels,
|
||||||
editingModelId,
|
editingModelId,
|
||||||
onSave,
|
onSave,
|
||||||
onDelete,
|
|
||||||
onClose,
|
onClose,
|
||||||
|
onDelete,
|
||||||
language,
|
language,
|
||||||
}: {
|
}: {
|
||||||
allModels: AIModel[]
|
supportedModels: AIModel[]
|
||||||
configuredModels: AIModel[]
|
configuredModels: AIModel[]
|
||||||
editingModelId: string | null
|
editingModelId: string | null
|
||||||
onSave: (
|
onSave: (
|
||||||
@@ -1274,8 +1366,8 @@ function ModelConfigModal({
|
|||||||
baseUrl?: string,
|
baseUrl?: string,
|
||||||
modelName?: string
|
modelName?: string
|
||||||
) => void
|
) => void
|
||||||
onDelete: (modelId: string) => void
|
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
|
onDelete: (modelId: string) => void
|
||||||
language: Language
|
language: Language
|
||||||
}) {
|
}) {
|
||||||
const [selectedModelId, setSelectedModelId] = useState(editingModelId || '')
|
const [selectedModelId, setSelectedModelId] = useState(editingModelId || '')
|
||||||
@@ -1283,10 +1375,10 @@ function ModelConfigModal({
|
|||||||
const [baseUrl, setBaseUrl] = useState('')
|
const [baseUrl, setBaseUrl] = useState('')
|
||||||
const [modelName, setModelName] = useState('')
|
const [modelName, setModelName] = useState('')
|
||||||
|
|
||||||
// 获取当前编辑的模型信息 - 编辑时从已配置的模型中查找,新建时从所有支持的模型中查找
|
// 获取当前编辑的模型信息 - 编辑时从已配置的模型中查找,新建时从支持的模型中查找
|
||||||
const selectedModel = editingModelId
|
const selectedModel = editingModelId
|
||||||
? configuredModels?.find((m) => m.id === selectedModelId)
|
? configuredModels?.find((m) => m.id === selectedModelId) // 编辑:从已配置中获取完整信息
|
||||||
: allModels?.find((m) => m.id === selectedModelId)
|
: supportedModels?.find((m) => m.id === selectedModelId) // 新建:从支持列表获取基本信息
|
||||||
|
|
||||||
// 如果是编辑现有模型,初始化API Key、Base URL和Model Name
|
// 如果是编辑现有模型,初始化API Key、Base URL和Model Name
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -1309,8 +1401,8 @@ function ModelConfigModal({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 可选择的模型列表(所有支持的模型)
|
// 可选择的模型列表:直接使用系统支持的模型
|
||||||
const availableModels = allModels || []
|
const availableModels = supportedModels || []
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||||
@@ -1327,14 +1419,10 @@ function ModelConfigModal({
|
|||||||
{editingModelId && (
|
{editingModelId && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => onDelete(editingModelId)}
|
||||||
if (confirm(t('confirmDeleteModel', language))) {
|
|
||||||
onDelete(editingModelId)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="p-2 rounded hover:bg-red-100 transition-colors"
|
className="p-2 rounded hover:bg-red-100 transition-colors"
|
||||||
style={{ background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }}
|
style={{ background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }}
|
||||||
title={t('deleteConfigFailed', language)}
|
title={t('deleteModel', language)}
|
||||||
>
|
>
|
||||||
<Trash2 className="w-4 h-4" />
|
<Trash2 className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
@@ -1528,15 +1616,15 @@ function ModelConfigModal({
|
|||||||
|
|
||||||
// Exchange Configuration Modal Component
|
// Exchange Configuration Modal Component
|
||||||
function ExchangeConfigModal({
|
function ExchangeConfigModal({
|
||||||
allExchanges,
|
supportedExchanges,
|
||||||
configuredExchanges,
|
configuredExchanges,
|
||||||
editingExchangeId,
|
editingExchangeId,
|
||||||
onSave,
|
onSave,
|
||||||
onDelete,
|
|
||||||
onClose,
|
onClose,
|
||||||
|
onDelete,
|
||||||
language,
|
language,
|
||||||
}: {
|
}: {
|
||||||
allExchanges: Exchange[]
|
supportedExchanges: Exchange[]
|
||||||
configuredExchanges: Exchange[]
|
configuredExchanges: Exchange[]
|
||||||
editingExchangeId: string | null
|
editingExchangeId: string | null
|
||||||
onSave: (
|
onSave: (
|
||||||
@@ -1549,8 +1637,8 @@ function ExchangeConfigModal({
|
|||||||
asterSigner?: string,
|
asterSigner?: string,
|
||||||
asterPrivateKey?: string
|
asterPrivateKey?: string
|
||||||
) => Promise<void>
|
) => Promise<void>
|
||||||
onDelete: (exchangeId: string) => void
|
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
|
onDelete: (exchangeId: string) => void
|
||||||
language: Language
|
language: Language
|
||||||
}) {
|
}) {
|
||||||
const [selectedExchangeId, setSelectedExchangeId] = useState(
|
const [selectedExchangeId, setSelectedExchangeId] = useState(
|
||||||
@@ -1581,10 +1669,10 @@ function ExchangeConfigModal({
|
|||||||
|
|
||||||
// 获取当前选择的交易所信息
|
// 获取当前选择的交易所信息
|
||||||
// 编辑模式:从 configuredExchanges 查找(包含用户配置的 apiKey、secretKey 等)
|
// 编辑模式:从 configuredExchanges 查找(包含用户配置的 apiKey、secretKey 等)
|
||||||
// 新增模式:从 allExchanges 查找(系统支持的交易所列表)
|
// 新增模式:从 supportedExchanges 查找(系统支持的交易所列表)
|
||||||
const selectedExchange = editingExchangeId
|
const selectedExchange = editingExchangeId
|
||||||
? configuredExchanges?.find(e => e.id === selectedExchangeId)
|
? configuredExchanges?.find(e => e.id === selectedExchangeId)
|
||||||
: allExchanges?.find(e => e.id === selectedExchangeId);
|
: supportedExchanges?.find(e => e.id === selectedExchangeId);
|
||||||
|
|
||||||
// 如果是编辑现有交易所,初始化表单数据
|
// 如果是编辑现有交易所,初始化表单数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -1622,6 +1710,9 @@ function ExchangeConfigModal({
|
|||||||
}
|
}
|
||||||
}, [selectedExchangeId])
|
}, [selectedExchangeId])
|
||||||
|
|
||||||
|
// 可选择的交易所列表:直接使用系统支持的交易所
|
||||||
|
const availableExchanges = supportedExchanges || []
|
||||||
|
|
||||||
const handleCopyIP = (ip: string) => {
|
const handleCopyIP = (ip: string) => {
|
||||||
navigator.clipboard.writeText(ip).then(() => {
|
navigator.clipboard.writeText(ip).then(() => {
|
||||||
setCopiedIP(true)
|
setCopiedIP(true)
|
||||||
@@ -1693,17 +1784,13 @@ function ExchangeConfigModal({
|
|||||||
{editingExchangeId && (
|
{editingExchangeId && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => onDelete(editingExchangeId)}
|
||||||
if (confirm(t('confirmDeleteExchange', language))) {
|
|
||||||
onDelete(editingExchangeId)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="p-2 rounded hover:bg-red-100 transition-colors"
|
className="p-2 rounded hover:bg-red-100 transition-colors"
|
||||||
style={{
|
style={{
|
||||||
background: 'rgba(246, 70, 93, 0.1)',
|
background: 'rgba(246, 70, 93, 0.1)',
|
||||||
color: '#F6465D',
|
color: '#F6465D',
|
||||||
}}
|
}}
|
||||||
title={t('deleteConfigFailed', language)}
|
title={t('deleteExchange', language)}
|
||||||
>
|
>
|
||||||
<Trash2 className="w-4 h-4" />
|
<Trash2 className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
@@ -1732,7 +1819,7 @@ function ExchangeConfigModal({
|
|||||||
required
|
required
|
||||||
>
|
>
|
||||||
<option value="">{t('pleaseSelectExchange', language)}</option>
|
<option value="">{t('pleaseSelectExchange', language)}</option>
|
||||||
{(allExchanges || []).map((exchange) => (
|
{availableExchanges.map((exchange) => (
|
||||||
<option key={exchange.id} value={exchange.id}>
|
<option key={exchange.id} value={exchange.id}>
|
||||||
{getShortName(exchange.name)} ({exchange.type.toUpperCase()}
|
{getShortName(exchange.name)} ({exchange.type.toUpperCase()}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -313,6 +313,13 @@ export const translations = {
|
|||||||
deleteExchangeConfigFailed: 'Failed to delete exchange configuration',
|
deleteExchangeConfigFailed: 'Failed to delete exchange configuration',
|
||||||
saveSignalSourceFailed: 'Failed to save signal source configuration',
|
saveSignalSourceFailed: 'Failed to save signal source configuration',
|
||||||
|
|
||||||
|
// Delete Confirmation
|
||||||
|
delete: 'Delete',
|
||||||
|
deleteModel: 'Delete Model',
|
||||||
|
deleteExchange: 'Delete Exchange',
|
||||||
|
deleteModelWarning: 'This will remove the AI model configuration. Traders using this model will not work properly.',
|
||||||
|
deleteExchangeWarning: 'This will remove the exchange configuration. Traders using this exchange will not be able to trade.',
|
||||||
|
|
||||||
// Login & Register
|
// Login & Register
|
||||||
login: 'Sign In',
|
login: 'Sign In',
|
||||||
register: 'Sign Up',
|
register: 'Sign Up',
|
||||||
@@ -784,6 +791,13 @@ export const translations = {
|
|||||||
deleteExchangeConfigFailed: '删除交易所配置失败',
|
deleteExchangeConfigFailed: '删除交易所配置失败',
|
||||||
saveSignalSourceFailed: '保存信号源配置失败',
|
saveSignalSourceFailed: '保存信号源配置失败',
|
||||||
|
|
||||||
|
// Delete Confirmation
|
||||||
|
delete: '删除',
|
||||||
|
deleteModel: '删除模型',
|
||||||
|
deleteExchange: '删除交易所',
|
||||||
|
deleteModelWarning: '这将删除AI模型配置。使用此模型的交易员将无法正常工作。',
|
||||||
|
deleteExchangeWarning: '这将删除交易所配置。使用此交易所的交易员将无法进行交易。',
|
||||||
|
|
||||||
// Login & Register
|
// Login & Register
|
||||||
login: '登录',
|
login: '登录',
|
||||||
register: '注册',
|
register: '注册',
|
||||||
|
|||||||
Reference in New Issue
Block a user