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 4920c28cc6
commit 9d721621f2
3 changed files with 85 additions and 27 deletions

View File

@@ -1448,7 +1448,15 @@ func (s *Server) handleLatestDecisions(c *gin.Context) {
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 {
c.JSON(http.StatusInternalServerError, gin.H{
"error": fmt.Sprintf("获取决策日志失败: %v", err),

View File

@@ -261,12 +261,21 @@ export const api = {
return res.json()
},
// 获取最新决策支持trader_id
async getLatestDecisions(traderId?: string): Promise<DecisionRecord[]> {
const url = traderId
? `${API_BASE}/decisions/latest?trader_id=${traderId}`
: `${API_BASE}/decisions/latest`
const res = await httpClient.get(url, getAuthHeaders())
// 获取最新决策支持trader_id和limit参数
async getLatestDecisions(
traderId?: string,
limit: number = 5
): Promise<DecisionRecord[]> {
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('获取最新决策失败')
return res.json()
},

View File

@@ -54,6 +54,18 @@ export default function TraderDashboard() {
)
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列表仅在用户登录时
const { data: traders, error: tradersError } = useSWR<TraderInfo[]>(
user && token ? 'traders' : null,
@@ -111,8 +123,10 @@ export default function TraderDashboard() {
)
const { data: decisions } = useSWR<DecisionRecord[]>(
selectedTraderId ? `decisions/latest-${selectedTraderId}` : null,
() => api.getLatestDecisions(selectedTraderId),
selectedTraderId
? `decisions/latest-${selectedTraderId}-${decisionLimit}`
: null,
() => api.getLatestDecisions(selectedTraderId, decisionLimit),
{
refreshInterval: 30000,
revalidateOnFocus: false,
@@ -570,27 +584,54 @@ export default function TraderDashboard() {
style={{ animationDelay: '0.2s' }}
>
<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' }}
>
<div
className="w-10 h-10 rounded-xl flex items-center justify-center"
style={{
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' }} />
<div className="flex items-center gap-3">
<div
className="w-10 h-10 rounded-xl flex items-center justify-center"
style={{
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' }} />
</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>
<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 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>