diff --git a/frontend/src/components/GlobalConfig.jsx b/frontend/src/components/GlobalConfig.jsx index 90a886f..77101ad 100644 --- a/frontend/src/components/GlobalConfig.jsx +++ b/frontend/src/components/GlobalConfig.jsx @@ -4,6 +4,147 @@ import { api } from '../services/api' import './GlobalConfig.css' import './ConfigPanel.css' // 复用 ConfigPanel 的样式 +// 复用 ConfigPanel 的 ConfigItem 组件 +const ConfigItem = ({ label, config, onUpdate, disabled }) => { + const isPercentKey = label.includes('PERCENT') || label.includes('PCT') + const PCT_LIKE_KEYS = new Set([ + 'LIMIT_ORDER_OFFSET_PCT', + 'ENTRY_MAX_DRIFT_PCT_TRENDING', + 'ENTRY_MAX_DRIFT_PCT_RANGING', + ]) + const isPctLike = PCT_LIKE_KEYS.has(label) + const isRatioPercentKey = isPercentKey && !isPctLike + + const formatPercent = (n) => { + if (typeof n !== 'number' || isNaN(n)) return '' + return n.toFixed(4).replace(/\.?0+$/, '') + } + + const getInitialDisplayValue = (val) => { + if (config.type === 'number' && isPercentKey) { + if (val === null || val === undefined || val === '') { + return '' + } + const numVal = typeof val === 'string' ? parseFloat(val) : val + if (isNaN(numVal)) { + return '' + } + if (isPctLike) { + const pctNum = numVal <= 0.05 ? numVal * 100 : numVal + return formatPercent(pctNum) + } + const percent = numVal <= 1 ? numVal * 100 : numVal + return formatPercent(percent) + } + return val === null || val === undefined ? '' : val + } + + const [value, setValue] = useState(config.value) + const [localValue, setLocalValue] = useState(getInitialDisplayValue(config.value)) + const [isEditing, setIsEditing] = useState(false) + + useEffect(() => { + setValue(config.value) + setIsEditing(false) + setLocalValue(getInitialDisplayValue(config.value)) + }, [config.value]) + + const handleChange = (newValue) => { + setLocalValue(newValue) + setIsEditing(true) + } + + const handleBlur = () => { + if (!isEditing) return + let finalValue = localValue + if (config.type === 'number') { + if (isPercentKey) { + const numVal = parseFloat(localValue) + if (isNaN(numVal)) { + setLocalValue(getInitialDisplayValue(config.value)) + setIsEditing(false) + return + } + if (isPctLike) { + finalValue = numVal <= 1 ? numVal / 100 : numVal / 100 + } else { + finalValue = numVal <= 100 ? numVal / 100 : numVal / 100 + } + } else { + finalValue = parseFloat(localValue) + if (isNaN(finalValue)) { + setLocalValue(getInitialDisplayValue(config.value)) + setIsEditing(false) + return + } + } + } else if (config.type === 'boolean') { + finalValue = localValue === 'true' || localValue === true + } else { + finalValue = localValue + } + onUpdate(finalValue) + setIsEditing(false) + } + + const displayValue = isEditing ? localValue : getInitialDisplayValue(config.value) + + return ( +
+
+ +
+
+ {config.type === 'boolean' ? ( + + ) : ( + { + let newValue = e.target.value + if (config.type === 'number') { + if (isPercentKey) { + const numValue = parseFloat(newValue) + const maxAllowed = isPctLike ? 1 : 100 + if (newValue !== '' && !isNaN(numValue) && (numValue < 0 || numValue > maxAllowed)) { + return + } + } + } + handleChange(newValue) + }} + onBlur={handleBlur} + onKeyPress={(e) => { + if (e.key === 'Enter') { + handleBlur() + } + }} + disabled={disabled} + style={{ width: '100%', padding: '8px', border: '1px solid #ddd', borderRadius: '4px' }} + /> + )} + {isPercentKey && %} +
+ {config.description && ( +
{config.description}
+ )} +
+ ) +} + const GlobalConfig = ({ currentUser }) => { const [users, setUsers] = useState([]) const [accounts, setAccounts] = useState([]) @@ -908,6 +1049,80 @@ const GlobalConfig = ({ currentUser }) => { )} + {/* 全局策略配置项编辑(仅管理员) */} + {isAdmin && ( +
+
+

全局策略配置

+

+ 修改全局策略配置,所有普通用户账号将使用这些配置(风险旋钮除外) +

+
+ {Object.keys(configs).length > 0 ? ( + (() => { + const configCategories = { + 'scan': '市场扫描', + 'position': '仓位控制', + 'risk': '风险控制', + 'strategy': '策略参数', + } + return Object.entries(configCategories).map(([category, label]) => { + const categoryConfigs = Object.entries(configs).filter(([key, config]) => { + if (config.category !== category) return false + // 排除风险旋钮(这些由用户自己控制) + const RISK_KNOBS_KEYS = ['MIN_MARGIN_USDT', 'MIN_POSITION_PERCENT', 'MAX_POSITION_PERCENT', + 'MAX_TOTAL_POSITION_PERCENT', 'AUTO_TRADE_ENABLED', 'MAX_OPEN_POSITIONS', 'MAX_DAILY_ENTRIES'] + if (RISK_KNOBS_KEYS.includes(key)) return false + // 排除API密钥(在账号管理中) + if (key === 'BINANCE_API_KEY' || key === 'BINANCE_API_SECRET' || key === 'USE_TESTNET') return false + return true + }) + if (categoryConfigs.length === 0) return null + return ( +
+

{label}

+
+ {categoryConfigs.map(([key, config]) => ( + { + try { + setSaving(true) + setMessage('') + const globalAccountId = parseInt(String(configMeta.global_strategy_account_id), 10) || 1 + await api.updateGlobalConfigsBatch([{ + key, + value, + type: config.type, + category: config.category, + description: config.description + }], globalAccountId) + setMessage(`已更新 ${key}`) + await loadConfigs() + } catch (error) { + setMessage('更新配置失败: ' + error.message) + } finally { + setSaving(false) + } + }} + disabled={saving} + /> + ))} +
+
+ ) + }) + })() + ) : ( +
+ {loading ? '加载配置中...' : '暂无配置项'} +
+ )} +
+ )} + {/* 预设方案快速切换(仅管理员 + 全局策略账号) */} {isAdmin && isGlobalStrategyAccount && (