This commit is contained in:
薇薇安 2026-01-22 17:11:21 +08:00
parent 5717614f61
commit 9ed4d4259a

View File

@ -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">