mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-04 11:30:58 +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,
|
||||
COALESCE(custom_api_url, '') as custom_api_url,
|
||||
COALESCE(custom_model_name, '') as custom_model_name,
|
||||
COALESCE(deleted, FALSE) as deleted,
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -152,10 +153,11 @@ func (d *PostgreSQLDatabase) GetAIModels(userID string) ([]*AIModelConfig, error
|
||||
models := make([]*AIModelConfig, 0)
|
||||
for rows.Next() {
|
||||
var model AIModelConfig
|
||||
var deleted bool // 临时变量,用于读取 deleted 字段但不保存到结构体
|
||||
err := rows.Scan(
|
||||
&model.ID, &model.UserID, &model.Name, &model.Provider,
|
||||
&model.Enabled, &model.APIKey, &model.CustomAPIURL, &model.CustomModelName,
|
||||
&model.CreatedAt, &model.UpdatedAt,
|
||||
&deleted, &model.CreatedAt, &model.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -168,7 +170,59 @@ func (d *PostgreSQLDatabase) GetAIModels(userID string) ([]*AIModelConfig, error
|
||||
|
||||
// UpdateAIModel 更新AI模型配置,如果不存在则创建用户特定配置
|
||||
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
|
||||
err := d.db.QueryRow(`
|
||||
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 {
|
||||
// 找到了现有配置(精确匹配 ID),更新它
|
||||
_, 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
|
||||
`, enabled, apiKey, customAPIURL, customModelName, existingID, userID)
|
||||
return err
|
||||
@@ -193,7 +247,7 @@ func (d *PostgreSQLDatabase) UpdateAIModel(userID, id string, enabled bool, apiK
|
||||
// 找到了现有配置(通过 provider 匹配,兼容旧版),更新它
|
||||
log.Printf("⚠️ 使用旧版 provider 匹配更新模型: %s -> %s", provider, existingID)
|
||||
_, 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
|
||||
`, enabled, apiKey, customAPIURL, customModelName, existingID, userID)
|
||||
return err
|
||||
|
||||
@@ -22,6 +22,7 @@ CREATE TABLE IF NOT EXISTS ai_models (
|
||||
api_key TEXT DEFAULT '',
|
||||
custom_api_url TEXT DEFAULT '',
|
||||
custom_model_name TEXT DEFAULT '',
|
||||
deleted BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
@@ -167,6 +168,9 @@ INSERT INTO system_config (key, value) VALUES
|
||||
('jwt_secret', '')
|
||||
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_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 { api } from '../lib/api'
|
||||
import type {
|
||||
@@ -58,6 +58,8 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
const [showModelModal, setShowModelModal] = useState(false)
|
||||
const [showExchangeModal, setShowExchangeModal] = 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 [editingExchange, setEditingExchange] = useState<string | null>(null)
|
||||
const [editingTrader, setEditingTrader] = useState<any>(null)
|
||||
@@ -135,20 +137,6 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
const configuredModels = allModels || []
|
||||
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 enabledExchanges =
|
||||
@@ -313,8 +301,6 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
}
|
||||
|
||||
const handleDeleteModelConfig = async (modelId: string) => {
|
||||
if (!confirm(t('confirmDeleteModel', language))) return
|
||||
|
||||
try {
|
||||
const updatedModels =
|
||||
allModels?.map((m) =>
|
||||
@@ -344,15 +330,31 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
}
|
||||
|
||||
await api.updateModelConfigs(request)
|
||||
setAllModels(updatedModels)
|
||||
|
||||
// 重新获取用户配置以确保数据同步
|
||||
const refreshedModels = await api.getModelConfigs()
|
||||
setAllModels(refreshedModels)
|
||||
|
||||
setShowModelModal(false)
|
||||
setEditingModel(null)
|
||||
setShowDeleteConfirm(false)
|
||||
setDeleteTarget(null)
|
||||
} catch (error) {
|
||||
console.error('Failed to delete model config:', error)
|
||||
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 (
|
||||
modelId: string,
|
||||
apiKey: string,
|
||||
@@ -427,8 +429,6 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
}
|
||||
|
||||
const handleDeleteExchangeConfig = async (exchangeId: string) => {
|
||||
if (!confirm(t('confirmDeleteExchange', language))) return
|
||||
|
||||
try {
|
||||
const request = {
|
||||
exchanges: {
|
||||
@@ -451,6 +451,8 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
setAllExchanges(refreshed)
|
||||
setShowExchangeModal(false)
|
||||
setEditingExchange(null)
|
||||
setShowDeleteConfirm(false)
|
||||
setDeleteTarget(null)
|
||||
} catch (error) {
|
||||
console.error('Failed to delete exchange config:', error)
|
||||
alert(t('deleteExchangeConfigFailed', language))
|
||||
@@ -1043,15 +1045,18 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
{/* Model Configuration Modal */}
|
||||
{showModelModal && (
|
||||
<ModelConfigModal
|
||||
allModels={selectableModels}
|
||||
supportedModels={supportedModels}
|
||||
configuredModels={allModels}
|
||||
editingModelId={editingModel}
|
||||
onSave={handleSaveModelConfig}
|
||||
onDelete={handleDeleteModelConfig}
|
||||
onClose={() => {
|
||||
setShowModelModal(false)
|
||||
setEditingModel(null)
|
||||
}}
|
||||
onDelete={(modelId) => {
|
||||
setDeleteTarget({ type: 'model', id: modelId })
|
||||
setShowDeleteConfirm(true)
|
||||
}}
|
||||
language={language}
|
||||
/>
|
||||
)}
|
||||
@@ -1059,15 +1064,18 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
{/* Exchange Configuration Modal */}
|
||||
{showExchangeModal && (
|
||||
<ExchangeConfigModal
|
||||
allExchanges={selectableExchanges}
|
||||
supportedExchanges={supportedExchanges}
|
||||
configuredExchanges={allExchanges}
|
||||
editingExchangeId={editingExchange}
|
||||
onSave={handleSaveExchangeConfig}
|
||||
onDelete={handleDeleteExchangeConfig}
|
||||
onClose={() => {
|
||||
setShowExchangeModal(false)
|
||||
setEditingExchange(null)
|
||||
}}
|
||||
onDelete={(exchangeId) => {
|
||||
setDeleteTarget({ type: 'exchange', id: exchangeId })
|
||||
setShowDeleteConfirm(true)
|
||||
}}
|
||||
language={language}
|
||||
/>
|
||||
)}
|
||||
@@ -1082,6 +1090,27 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
function ModelConfigModal({
|
||||
allModels,
|
||||
supportedModels,
|
||||
configuredModels,
|
||||
editingModelId,
|
||||
onSave,
|
||||
onDelete,
|
||||
onClose,
|
||||
onDelete,
|
||||
language,
|
||||
}: {
|
||||
allModels: AIModel[]
|
||||
supportedModels: AIModel[]
|
||||
configuredModels: AIModel[]
|
||||
editingModelId: string | null
|
||||
onSave: (
|
||||
@@ -1274,8 +1366,8 @@ function ModelConfigModal({
|
||||
baseUrl?: string,
|
||||
modelName?: string
|
||||
) => void
|
||||
onDelete: (modelId: string) => void
|
||||
onClose: () => void
|
||||
onDelete: (modelId: string) => void
|
||||
language: Language
|
||||
}) {
|
||||
const [selectedModelId, setSelectedModelId] = useState(editingModelId || '')
|
||||
@@ -1283,10 +1375,10 @@ function ModelConfigModal({
|
||||
const [baseUrl, setBaseUrl] = useState('')
|
||||
const [modelName, setModelName] = useState('')
|
||||
|
||||
// 获取当前编辑的模型信息 - 编辑时从已配置的模型中查找,新建时从所有支持的模型中查找
|
||||
// 获取当前编辑的模型信息 - 编辑时从已配置的模型中查找,新建时从支持的模型中查找
|
||||
const selectedModel = editingModelId
|
||||
? configuredModels?.find((m) => m.id === selectedModelId)
|
||||
: allModels?.find((m) => m.id === selectedModelId)
|
||||
? configuredModels?.find((m) => m.id === selectedModelId) // 编辑:从已配置中获取完整信息
|
||||
: supportedModels?.find((m) => m.id === selectedModelId) // 新建:从支持列表获取基本信息
|
||||
|
||||
// 如果是编辑现有模型,初始化API Key、Base URL和Model Name
|
||||
useEffect(() => {
|
||||
@@ -1309,8 +1401,8 @@ function ModelConfigModal({
|
||||
)
|
||||
}
|
||||
|
||||
// 可选择的模型列表(所有支持的模型)
|
||||
const availableModels = allModels || []
|
||||
// 可选择的模型列表:直接使用系统支持的模型
|
||||
const availableModels = supportedModels || []
|
||||
|
||||
return (
|
||||
<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 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (confirm(t('confirmDeleteModel', language))) {
|
||||
onDelete(editingModelId)
|
||||
}
|
||||
}}
|
||||
onClick={() => onDelete(editingModelId)}
|
||||
className="p-2 rounded hover:bg-red-100 transition-colors"
|
||||
style={{ background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }}
|
||||
title={t('deleteConfigFailed', language)}
|
||||
title={t('deleteModel', language)}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
@@ -1528,15 +1616,15 @@ function ModelConfigModal({
|
||||
|
||||
// Exchange Configuration Modal Component
|
||||
function ExchangeConfigModal({
|
||||
allExchanges,
|
||||
supportedExchanges,
|
||||
configuredExchanges,
|
||||
editingExchangeId,
|
||||
onSave,
|
||||
onDelete,
|
||||
onClose,
|
||||
onDelete,
|
||||
language,
|
||||
}: {
|
||||
allExchanges: Exchange[]
|
||||
supportedExchanges: Exchange[]
|
||||
configuredExchanges: Exchange[]
|
||||
editingExchangeId: string | null
|
||||
onSave: (
|
||||
@@ -1549,8 +1637,8 @@ function ExchangeConfigModal({
|
||||
asterSigner?: string,
|
||||
asterPrivateKey?: string
|
||||
) => Promise<void>
|
||||
onDelete: (exchangeId: string) => void
|
||||
onClose: () => void
|
||||
onDelete: (exchangeId: string) => void
|
||||
language: Language
|
||||
}) {
|
||||
const [selectedExchangeId, setSelectedExchangeId] = useState(
|
||||
@@ -1581,10 +1669,10 @@ function ExchangeConfigModal({
|
||||
|
||||
// 获取当前选择的交易所信息
|
||||
// 编辑模式:从 configuredExchanges 查找(包含用户配置的 apiKey、secretKey 等)
|
||||
// 新增模式:从 allExchanges 查找(系统支持的交易所列表)
|
||||
// 新增模式:从 supportedExchanges 查找(系统支持的交易所列表)
|
||||
const selectedExchange = editingExchangeId
|
||||
? configuredExchanges?.find(e => e.id === selectedExchangeId)
|
||||
: allExchanges?.find(e => e.id === selectedExchangeId);
|
||||
: supportedExchanges?.find(e => e.id === selectedExchangeId);
|
||||
|
||||
// 如果是编辑现有交易所,初始化表单数据
|
||||
useEffect(() => {
|
||||
@@ -1622,6 +1710,9 @@ function ExchangeConfigModal({
|
||||
}
|
||||
}, [selectedExchangeId])
|
||||
|
||||
// 可选择的交易所列表:直接使用系统支持的交易所
|
||||
const availableExchanges = supportedExchanges || []
|
||||
|
||||
const handleCopyIP = (ip: string) => {
|
||||
navigator.clipboard.writeText(ip).then(() => {
|
||||
setCopiedIP(true)
|
||||
@@ -1693,17 +1784,13 @@ function ExchangeConfigModal({
|
||||
{editingExchangeId && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (confirm(t('confirmDeleteExchange', language))) {
|
||||
onDelete(editingExchangeId)
|
||||
}
|
||||
}}
|
||||
onClick={() => onDelete(editingExchangeId)}
|
||||
className="p-2 rounded hover:bg-red-100 transition-colors"
|
||||
style={{
|
||||
background: 'rgba(246, 70, 93, 0.1)',
|
||||
color: '#F6465D',
|
||||
}}
|
||||
title={t('deleteConfigFailed', language)}
|
||||
title={t('deleteExchange', language)}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
@@ -1732,7 +1819,7 @@ function ExchangeConfigModal({
|
||||
required
|
||||
>
|
||||
<option value="">{t('pleaseSelectExchange', language)}</option>
|
||||
{(allExchanges || []).map((exchange) => (
|
||||
{availableExchanges.map((exchange) => (
|
||||
<option key={exchange.id} value={exchange.id}>
|
||||
{getShortName(exchange.name)} ({exchange.type.toUpperCase()}
|
||||
)
|
||||
|
||||
@@ -313,6 +313,13 @@ export const translations = {
|
||||
deleteExchangeConfigFailed: 'Failed to delete exchange 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: 'Sign In',
|
||||
register: 'Sign Up',
|
||||
@@ -784,6 +791,13 @@ export const translations = {
|
||||
deleteExchangeConfigFailed: '删除交易所配置失败',
|
||||
saveSignalSourceFailed: '保存信号源配置失败',
|
||||
|
||||
// Delete Confirmation
|
||||
delete: '删除',
|
||||
deleteModel: '删除模型',
|
||||
deleteExchange: '删除交易所',
|
||||
deleteModelWarning: '这将删除AI模型配置。使用此模型的交易员将无法正常工作。',
|
||||
deleteExchangeWarning: '这将删除交易所配置。使用此交易所的交易员将无法进行交易。',
|
||||
|
||||
// Login & Register
|
||||
login: '登录',
|
||||
register: '注册',
|
||||
|
||||
Reference in New Issue
Block a user