mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-15 08:46:58 +08:00
feat: Add decision limit selector with 5/10/20/50 options (#638)
## Summary Allow users to select the number of decision records to display (5/10/20/50) in the Web UI, with persistent storage in localStorage. ## Changes ### Backend - api/server.go: Add 'limit' query parameter support to /api/decisions/latest - Default: 5 (maintains current behavior) - Max: 50 (prevents excessive data loading) - Fully backward compatible ### Frontend - web/src/lib/api.ts: Update getLatestDecisions() to accept limit parameter - web/src/pages/TraderDashboard.tsx: - Add decisionLimit state management with localStorage persistence - Add dropdown selector UI (5/10/20/50 options) - Pass limit to API calls and update SWR cache key ## Time Coverage - 5 records = 15 minutes (default, quick check) - 10 records = 30 minutes (short-term review) - 20 records = 1 hour (medium-term analysis) - 50 records = 2.5 hours (deep pattern analysis)
This commit is contained in:
@@ -1448,7 +1448,15 @@ func (s *Server) handleLatestDecisions(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
records, err := trader.GetDecisionLogger().GetLatestRecords(5)
|
// 从 query 参数读取 limit,默认 5,最大 50
|
||||||
|
limit := 5
|
||||||
|
if limitStr := c.Query("limit"); limitStr != "" {
|
||||||
|
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 50 {
|
||||||
|
limit = l
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
records, err := trader.GetDecisionLogger().GetLatestRecords(limit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
"error": fmt.Sprintf("获取决策日志失败: %v", err),
|
"error": fmt.Sprintf("获取决策日志失败: %v", err),
|
||||||
|
|||||||
@@ -261,12 +261,21 @@ export const api = {
|
|||||||
return res.json()
|
return res.json()
|
||||||
},
|
},
|
||||||
|
|
||||||
// 获取最新决策(支持trader_id)
|
// 获取最新决策(支持trader_id和limit参数)
|
||||||
async getLatestDecisions(traderId?: string): Promise<DecisionRecord[]> {
|
async getLatestDecisions(
|
||||||
const url = traderId
|
traderId?: string,
|
||||||
? `${API_BASE}/decisions/latest?trader_id=${traderId}`
|
limit: number = 5
|
||||||
: `${API_BASE}/decisions/latest`
|
): Promise<DecisionRecord[]> {
|
||||||
const res = await httpClient.get(url, getAuthHeaders())
|
const params = new URLSearchParams()
|
||||||
|
if (traderId) {
|
||||||
|
params.append('trader_id', traderId)
|
||||||
|
}
|
||||||
|
params.append('limit', limit.toString())
|
||||||
|
|
||||||
|
const res = await httpClient.get(
|
||||||
|
`${API_BASE}/decisions/latest?${params}`,
|
||||||
|
getAuthHeaders()
|
||||||
|
)
|
||||||
if (!res.ok) throw new Error('获取最新决策失败')
|
if (!res.ok) throw new Error('获取最新决策失败')
|
||||||
return res.json()
|
return res.json()
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -54,6 +54,18 @@ export default function TraderDashboard() {
|
|||||||
)
|
)
|
||||||
const [lastUpdate, setLastUpdate] = useState<string>('--:--:--')
|
const [lastUpdate, setLastUpdate] = useState<string>('--:--:--')
|
||||||
|
|
||||||
|
// 决策记录数量选择(从 localStorage 读取,默认 5)
|
||||||
|
const [decisionLimit, setDecisionLimit] = useState<number>(() => {
|
||||||
|
const saved = localStorage.getItem('decisionLimit')
|
||||||
|
return saved ? parseInt(saved, 10) : 5
|
||||||
|
})
|
||||||
|
|
||||||
|
// 当 limit 变化时保存到 localStorage
|
||||||
|
const handleLimitChange = (newLimit: number) => {
|
||||||
|
setDecisionLimit(newLimit)
|
||||||
|
localStorage.setItem('decisionLimit', newLimit.toString())
|
||||||
|
}
|
||||||
|
|
||||||
// 获取trader列表(仅在用户登录时)
|
// 获取trader列表(仅在用户登录时)
|
||||||
const { data: traders, error: tradersError } = useSWR<TraderInfo[]>(
|
const { data: traders, error: tradersError } = useSWR<TraderInfo[]>(
|
||||||
user && token ? 'traders' : null,
|
user && token ? 'traders' : null,
|
||||||
@@ -111,8 +123,10 @@ export default function TraderDashboard() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const { data: decisions } = useSWR<DecisionRecord[]>(
|
const { data: decisions } = useSWR<DecisionRecord[]>(
|
||||||
selectedTraderId ? `decisions/latest-${selectedTraderId}` : null,
|
selectedTraderId
|
||||||
() => api.getLatestDecisions(selectedTraderId),
|
? `decisions/latest-${selectedTraderId}-${decisionLimit}`
|
||||||
|
: null,
|
||||||
|
() => api.getLatestDecisions(selectedTraderId, decisionLimit),
|
||||||
{
|
{
|
||||||
refreshInterval: 30000,
|
refreshInterval: 30000,
|
||||||
revalidateOnFocus: false,
|
revalidateOnFocus: false,
|
||||||
@@ -570,27 +584,54 @@ export default function TraderDashboard() {
|
|||||||
style={{ animationDelay: '0.2s' }}
|
style={{ animationDelay: '0.2s' }}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="flex items-center gap-3 mb-5 pb-4 border-b"
|
className="flex items-center justify-between mb-5 pb-4 border-b"
|
||||||
style={{ borderColor: '#2B3139' }}
|
style={{ borderColor: '#2B3139' }}
|
||||||
>
|
>
|
||||||
<div
|
<div className="flex items-center gap-3">
|
||||||
className="w-10 h-10 rounded-xl flex items-center justify-center"
|
<div
|
||||||
style={{
|
className="w-10 h-10 rounded-xl flex items-center justify-center"
|
||||||
background: 'linear-gradient(135deg, #6366F1 0%, #8B5CF6 100%)',
|
style={{
|
||||||
boxShadow: '0 4px 14px rgba(99, 102, 241, 0.4)',
|
background: 'linear-gradient(135deg, #6366F1 0%, #8B5CF6 100%)',
|
||||||
}}
|
boxShadow: '0 4px 14px rgba(99, 102, 241, 0.4)',
|
||||||
>
|
}}
|
||||||
<Brain className="w-5 h-5" style={{ color: '#FFFFFF' }} />
|
>
|
||||||
|
<Brain className="w-5 h-5" style={{ color: '#FFFFFF' }} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-bold" style={{ color: '#EAECEF' }}>
|
||||||
|
{t('recentDecisions', language)}
|
||||||
|
</h2>
|
||||||
|
{decisions && decisions.length > 0 && (
|
||||||
|
<div className="text-xs" style={{ color: '#848E9C' }}>
|
||||||
|
{t('lastCycles', language, { count: decisions.length })}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<h2 className="text-xl font-bold" style={{ color: '#EAECEF' }}>
|
{/* 显示数量选择器 */}
|
||||||
{t('recentDecisions', language)}
|
<div className="flex items-center gap-2">
|
||||||
</h2>
|
<span className="text-xs" style={{ color: '#848E9C' }}>
|
||||||
{decisions && decisions.length > 0 && (
|
{language === 'zh' ? '显示' : 'Show'}:
|
||||||
<div className="text-xs" style={{ color: '#848E9C' }}>
|
</span>
|
||||||
{t('lastCycles', language, { count: decisions.length })}
|
<select
|
||||||
</div>
|
value={decisionLimit}
|
||||||
)}
|
onChange={(e) => handleLimitChange(parseInt(e.target.value, 10))}
|
||||||
|
className="rounded px-2 py-1 text-xs font-medium cursor-pointer transition-colors"
|
||||||
|
style={{
|
||||||
|
background: '#1E2329',
|
||||||
|
border: '1px solid #2B3139',
|
||||||
|
color: '#EAECEF',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value={5}>5</option>
|
||||||
|
<option value={10}>10</option>
|
||||||
|
<option value={20}>20</option>
|
||||||
|
<option value={50}>50</option>
|
||||||
|
</select>
|
||||||
|
<span className="text-xs" style={{ color: '#848E9C' }}>
|
||||||
|
{language === 'zh' ? '条' : ''}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user