a
This commit is contained in:
parent
5717614f61
commit
9ed4d4259a
|
|
@ -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 (
|
||||
<div className="config-item">
|
||||
<div className="config-item-header">
|
||||
<label>{label}</label>
|
||||
</div>
|
||||
<div className="config-input-wrapper">
|
||||
{config.type === 'boolean' ? (
|
||||
<select
|
||||
value={String(config.value || false)}
|
||||
onChange={(e) => {
|
||||
const boolValue = e.target.value === 'true'
|
||||
onUpdate(boolValue)
|
||||
}}
|
||||
disabled={disabled}
|
||||
style={{ width: '100%', padding: '8px', border: '1px solid #ddd', borderRadius: '4px' }}
|
||||
>
|
||||
<option value="true">是</option>
|
||||
<option value="false">否</option>
|
||||
</select>
|
||||
) : (
|
||||
<input
|
||||
type="text"
|
||||
inputMode={config.type === 'number' ? 'decimal' : 'text'}
|
||||
value={displayValue === '' ? '' : String(displayValue)}
|
||||
onChange={(e) => {
|
||||
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 && <span style={{ marginLeft: '8px', color: '#666' }}>%</span>}
|
||||
</div>
|
||||
{config.description && (
|
||||
<div style={{ fontSize: '12px', color: '#666', marginTop: '4px' }}>{config.description}</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const GlobalConfig = ({ currentUser }) => {
|
||||
const [users, setUsers] = useState([])
|
||||
const [accounts, setAccounts] = useState([])
|
||||
|
|
@ -908,6 +1049,80 @@ const GlobalConfig = ({ currentUser }) => {
|
|||
</section>
|
||||
)}
|
||||
|
||||
{/* 全局策略配置项编辑(仅管理员) */}
|
||||
{isAdmin && (
|
||||
<section className="global-section config-section">
|
||||
<div className="section-header">
|
||||
<h3>全局策略配置</h3>
|
||||
<p style={{ fontSize: '14px', color: '#666', marginTop: '8px' }}>
|
||||
修改全局策略配置,所有普通用户账号将使用这些配置(风险旋钮除外)
|
||||
</p>
|
||||
</div>
|
||||
{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 (
|
||||
<div key={category} style={{ marginBottom: '24px' }}>
|
||||
<h4 style={{ marginBottom: '12px', fontSize: '16px', fontWeight: '600' }}>{label}</h4>
|
||||
<div className="config-grid" style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))', gap: '16px' }}>
|
||||
{categoryConfigs.map(([key, config]) => (
|
||||
<ConfigItem
|
||||
key={key}
|
||||
label={key}
|
||||
config={config}
|
||||
onUpdate={async (value) => {
|
||||
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}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
})()
|
||||
) : (
|
||||
<div style={{ padding: '20px', textAlign: 'center', color: '#666' }}>
|
||||
{loading ? '加载配置中...' : '暂无配置项'}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* 预设方案快速切换(仅管理员 + 全局策略账号) */}
|
||||
{isAdmin && isGlobalStrategyAccount && (
|
||||
<section className="global-section preset-section">
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user