a
This commit is contained in:
parent
5717614f61
commit
9ed4d4259a
|
|
@ -4,6 +4,147 @@ import { api } from '../services/api'
|
||||||
import './GlobalConfig.css'
|
import './GlobalConfig.css'
|
||||||
import './ConfigPanel.css' // 复用 ConfigPanel 的样式
|
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 GlobalConfig = ({ currentUser }) => {
|
||||||
const [users, setUsers] = useState([])
|
const [users, setUsers] = useState([])
|
||||||
const [accounts, setAccounts] = useState([])
|
const [accounts, setAccounts] = useState([])
|
||||||
|
|
@ -908,6 +1049,80 @@ const GlobalConfig = ({ currentUser }) => {
|
||||||
</section>
|
</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 && (
|
{isAdmin && isGlobalStrategyAccount && (
|
||||||
<section className="global-section preset-section">
|
<section className="global-section preset-section">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user