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:
Lawrence Liu
2025-11-12 09:34:29 +08:00
committed by GitHub
parent e1f48e4d9b
commit 33b46e8092
3 changed files with 85 additions and 27 deletions

View File

@@ -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),

View File

@@ -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()
}, },

View File

@@ -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,9 +584,10 @@ 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 className="flex items-center gap-3">
<div <div
className="w-10 h-10 rounded-xl flex items-center justify-center" className="w-10 h-10 rounded-xl flex items-center justify-center"
style={{ style={{
@@ -594,6 +609,32 @@ export default function TraderDashboard() {
</div> </div>
</div> </div>
{/* 显示数量选择器 */}
<div className="flex items-center gap-2">
<span className="text-xs" style={{ color: '#848E9C' }}>
{language === 'zh' ? '显示' : 'Show'}:
</span>
<select
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
className="space-y-4 overflow-y-auto pr-2" className="space-y-4 overflow-y-auto pr-2"
style={{ maxHeight: 'calc(100vh - 280px)' }} style={{ maxHeight: 'calc(100vh - 280px)' }}