148 lines
5.6 KiB
JavaScript
148 lines
5.6 KiB
JavaScript
import { useEffect, useState } from 'react'
|
|
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'
|
|
import { Card, List, NavBar } from 'antd-mobile'
|
|
import { api } from '../../api/client.js'
|
|
import TrendBadge, { calcTrend } from '../../components/TrendBadge.jsx'
|
|
|
|
// 策略研究
|
|
// 列表态 (默认): 列出所有策略, 每行显示主指标 (债务/GDP) 最新值 + 趋势
|
|
// 详情态: 顶部 NavBar (返回) + 4 张图 (debt/gdp/debtToGdp/interestToGdp) + 表格
|
|
const SERIES = [
|
|
{ key: 'debt', label: '政府债务 (万亿美元)', color: '#8884d8' },
|
|
{ key: 'gdp', label: 'GDP (万亿美元)', color: '#3cb44b' },
|
|
{ key: 'debtToGdp', label: '债务/GDP (%)', color: '#4363d8' },
|
|
{ key: 'interestToGdp', label: '债务利息/GDP (%)', color: '#f58231' },
|
|
]
|
|
|
|
// 取最后一个有效数据点
|
|
function lastRow(rows) {
|
|
for (let i = rows.length - 1; i >= 0; i--) {
|
|
if (rows[i] && typeof rows[i].debtToGdp === 'number') return rows[i]
|
|
}
|
|
return null
|
|
}
|
|
|
|
export default function StrategyView() {
|
|
const [strategies, setStrategies] = useState([])
|
|
const [err, setErr] = useState('')
|
|
const [loading, setLoading] = useState(true)
|
|
const [selected, setSelected] = useState(null)
|
|
|
|
useEffect(() => {
|
|
let cancelled = false
|
|
setLoading(true)
|
|
api('/strategies')
|
|
.then((data) => { if (!cancelled) setStrategies(data.strategies || []) })
|
|
.catch((e) => !cancelled && setErr(e.message))
|
|
.finally(() => !cancelled && setLoading(false))
|
|
return () => { cancelled = true }
|
|
}, [])
|
|
|
|
if (loading) return <div className="loading">加载中...</div>
|
|
if (err) return <div className="error-banner">{err}</div>
|
|
if (!strategies.length) return <div className="empty">暂无策略数据</div>
|
|
|
|
// 详情态
|
|
if (selected) {
|
|
const cur = strategies.find((s) => s.name === selected)
|
|
if (cur) return <StrategyDetail strategy={cur} onBack={() => setSelected(null)} />
|
|
}
|
|
|
|
// 列表态: 主指标 = 债务/GDP, 趋势基于其最近变化
|
|
return (
|
|
<div className="list-page">
|
|
<div className="page-title">策略研究</div>
|
|
<List>
|
|
{strategies.map((s) => {
|
|
const rows = (s.data && s.data.rows) || []
|
|
const last = lastRow(rows)
|
|
const { trend, change } = calcTrend(rows)
|
|
return (
|
|
<List.Item
|
|
key={s.name}
|
|
title={s.description || s.name}
|
|
description={last
|
|
? <><b>{last.debtToGdp}%</b> · 债务利息/GDP {last.interestToGdp}% · {last.year}</>
|
|
: '暂无数据'}
|
|
extra={
|
|
<div className="row-meta">
|
|
{last && Math.abs(change) >= 1 && (
|
|
<span className="row-change">
|
|
{change > 0 ? '+' : ''}{change.toFixed(1)}%
|
|
</span>
|
|
)}
|
|
<TrendBadge trend={trend} />
|
|
</div>
|
|
}
|
|
arrowIcon
|
|
clickable
|
|
onClick={() => setSelected(s.name)}
|
|
/>
|
|
)
|
|
})}
|
|
</List>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// 详情: 4 张图 + 表格
|
|
function StrategyDetail({ strategy, onBack }) {
|
|
const s = strategy
|
|
const rows = (s.data && s.data.rows) || []
|
|
const last = lastRow(rows)
|
|
const { trend } = calcTrend(rows)
|
|
return (
|
|
<div className="detail-page">
|
|
<NavBar onBack={onBack}>{s.description || s.name}</NavBar>
|
|
<Card className="detail-card">
|
|
<div className="detail-meta">
|
|
数据 {rows.length} 条 · 来源 {s.data.source || 'seed'} ·
|
|
{last && <> 最新 <b style={{ color: 'var(--color-text)' }}>{last.year}</b> 债务/GDP <b style={{ color: 'var(--color-text)' }}>{last.debtToGdp}%</b> </>}
|
|
<TrendBadge trend={trend} />
|
|
</div>
|
|
{SERIES.map((s2) => (
|
|
<div key={s2.key} style={{ marginBottom: 12 }}>
|
|
<div style={{ fontSize: 13, fontWeight: 500, marginBottom: 4 }}>{s2.label}</div>
|
|
<div className="chart-wrap">
|
|
<ResponsiveContainer width="100%" height={220}>
|
|
<LineChart data={rows} margin={{ top: 8, right: 12, left: 0, bottom: 0 }}>
|
|
<CartesianGrid strokeDasharray="3 3" />
|
|
<XAxis dataKey="year" tick={{ fontSize: 11 }} />
|
|
<YAxis tick={{ fontSize: 11 }} />
|
|
<Tooltip />
|
|
<Legend />
|
|
<Line type="monotone" dataKey={s2.key} name={s2.label} stroke={s2.color} strokeWidth={2} dot={{ r: 3 }} />
|
|
</LineChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
</div>
|
|
))}
|
|
<div style={{ maxHeight: 220, overflow: 'auto', marginTop: 8 }}>
|
|
<table className="data-table">
|
|
<thead>
|
|
<tr>
|
|
<th>年份</th>
|
|
<th style={{ textAlign: 'right' }}>债务</th>
|
|
<th style={{ textAlign: 'right' }}>GDP</th>
|
|
<th style={{ textAlign: 'right' }}>债务/GDP</th>
|
|
<th style={{ textAlign: 'right' }}>利息/GDP</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{rows.map((r) => (
|
|
<tr key={r.year}>
|
|
<td>{r.year}</td>
|
|
<td style={{ textAlign: 'right' }}>{r.debt}</td>
|
|
<td style={{ textAlign: 'right' }}>{r.gdp}</td>
|
|
<td style={{ textAlign: 'right' }}>{r.debtToGdp}</td>
|
|
<td style={{ textAlign: 'right' }}>{r.interestToGdp}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
)
|
|
}
|