页面调整
This commit is contained in:
@@ -1,16 +1,17 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'
|
||||
import { CapsuleTabs, Card } from 'antd-mobile'
|
||||
import { Card, List, NavBar } from 'antd-mobile'
|
||||
import { api } from '../../api/client.js'
|
||||
import TrendBadge, { calcTrend, lastPoint } from '../../components/TrendBadge.jsx'
|
||||
|
||||
// 经济数据: 顶部 CapsuleTabs 选指标, 下面渲染图表 + 表格
|
||||
// 使用 antd-mobile: CapsuleTabs / Card
|
||||
// 多 groupName 的指标: 在 chip 文本中携带分组前缀以便区分
|
||||
// 经济数据
|
||||
// 列表态 (默认): 列出所有指标, 每行显示最新值 + 趋势徽标, 点击进入详情
|
||||
// 详情态: 顶部 NavBar (返回) + 折线图 + 数据表
|
||||
export default function DataView() {
|
||||
const [indicators, setIndicators] = useState([])
|
||||
const [selected, setSelected] = useState(null)
|
||||
const [err, setErr] = useState('')
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [selected, setSelected] = useState(null) // null = 列表态, 名称 = 详情态
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
@@ -19,7 +20,6 @@ export default function DataView() {
|
||||
.then((data) => {
|
||||
if (cancelled) return
|
||||
setIndicators(data.indicators || [])
|
||||
if (data.indicators && data.indicators[0]) setSelected(data.indicators[0].name)
|
||||
})
|
||||
.catch((e) => !cancelled && setErr(e.message))
|
||||
.finally(() => !cancelled && setLoading(false))
|
||||
@@ -30,65 +30,90 @@ export default function DataView() {
|
||||
if (err) return <div className="error-banner">{err}</div>
|
||||
if (!indicators.length) return <div className="empty">暂无指标数据, 请先执行 npm run db:seed</div>
|
||||
|
||||
// 为 chip label 计算: 多个 group 时显示 "分组 / 标题", 单个 group 只显示标题
|
||||
const groupSet = new Set(indicators.map((i) => i.groupName || ''))
|
||||
const showGroupPrefix = groupSet.size > 1
|
||||
const labelOf = (i) => {
|
||||
const t = (i.data && i.data.title) || i.name
|
||||
if (showGroupPrefix && i.groupName) return i.groupName + ' · ' + t
|
||||
return t
|
||||
// 详情态
|
||||
if (selected) {
|
||||
const cur = indicators.find((i) => i.name === selected)
|
||||
if (cur) return <DataDetail indicator={cur} onBack={() => setSelected(null)} />
|
||||
}
|
||||
|
||||
const cur = indicators.find((i) => i.name === selected)
|
||||
const chartData = cur ? buildMonthlySeries(cur.data) : []
|
||||
// 列表态
|
||||
const labelOf = (i) => (i.data && i.data.title) || i.name
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="list-page">
|
||||
<div className="page-title">经济数据</div>
|
||||
<CapsuleTabs activeKey={selected || undefined} onChange={setSelected}>
|
||||
{indicators.map((i) => (
|
||||
<CapsuleTabs.Tab key={i.name} title={labelOf(i)} />
|
||||
))}
|
||||
</CapsuleTabs>
|
||||
|
||||
{cur && (
|
||||
<div style={{ padding: '0 12px 12px' }}>
|
||||
<Card className="chart-card">
|
||||
<div style={{ fontSize: 12, color: 'var(--color-muted)', marginBottom: 8 }}>
|
||||
{labelOf(cur)} · 单位 {cur.data.unit || '-'} · 来源 {cur.data.source || 'seed'}
|
||||
</div>
|
||||
<div className="chart-wrap">
|
||||
<ResponsiveContainer width="100%" height={260}>
|
||||
<LineChart data={chartData} margin={{ top: 10, right: 16, left: 0, bottom: 0 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="month" tick={{ fontSize: 11 }} />
|
||||
<YAxis tick={{ fontSize: 11 }} />
|
||||
<Tooltip />
|
||||
<Legend />
|
||||
<Line type="monotone" dataKey="value" name={cur.data.unit || '值'} stroke="#1677ff" strokeWidth={2} dot={{ r: 3 }} />
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
<div style={{ maxHeight: 240, overflow: 'auto' }}>
|
||||
<table className="data-table">
|
||||
<thead>
|
||||
<tr><th>月份</th><th style={{ textAlign: 'right' }}>数值</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{chartData.map((r) => (
|
||||
<tr key={r.month}><td>{r.month}</td><td style={{ textAlign: 'right' }}>{r.value}</td></tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
<List>
|
||||
{indicators.map((i) => {
|
||||
const lp = lastPoint(i.data)
|
||||
const { trend, change } = calcTrend(i.data && i.data.rows)
|
||||
const unit = (i.data && i.data.unit) || ''
|
||||
return (
|
||||
<List.Item
|
||||
key={i.name}
|
||||
title={labelOf(i)}
|
||||
description={lp ? <><b>{lp.value}{unit}</b> · {lp.month}</> : '暂无数据'}
|
||||
extra={
|
||||
<div className="row-meta">
|
||||
{lp && Math.abs(change) >= 1 && (
|
||||
<span className="row-change">
|
||||
{change > 0 ? '+' : ''}{change.toFixed(1)}%
|
||||
</span>
|
||||
)}
|
||||
<TrendBadge trend={trend} />
|
||||
</div>
|
||||
}
|
||||
arrowIcon
|
||||
clickable
|
||||
onClick={() => setSelected(i.name)}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</List>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 把 data.rows: [{month, value}, ...] 直接返回, 服务端已经按月排好
|
||||
function buildMonthlySeries(data) {
|
||||
return (data && data.rows) || []
|
||||
// 详情: 折线图 + 数据表
|
||||
function DataDetail({ indicator, onBack }) {
|
||||
const i = indicator
|
||||
const rows = (i.data && i.data.rows) || []
|
||||
const lp = lastPoint(i.data)
|
||||
const { trend } = calcTrend(rows)
|
||||
const title = (i.data && i.data.title) || i.name
|
||||
return (
|
||||
<div className="detail-page">
|
||||
<NavBar onBack={onBack}>{title}</NavBar>
|
||||
<Card className="detail-card">
|
||||
<div className="detail-meta">
|
||||
单位 {i.data.unit || '-'} · 来源 {i.data.source || 'seed'} ·
|
||||
{lp && <> 最新 <b style={{ color: 'var(--color-text)' }}>{lp.value}{i.data.unit || ''}</b> ({lp.month}) </>}
|
||||
<TrendBadge trend={trend} />
|
||||
</div>
|
||||
<div className="chart-wrap">
|
||||
<ResponsiveContainer width="100%" height={260}>
|
||||
<LineChart data={rows} margin={{ top: 10, right: 16, left: 0, bottom: 0 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="month" tick={{ fontSize: 11 }} />
|
||||
<YAxis tick={{ fontSize: 11 }} />
|
||||
<Tooltip />
|
||||
<Legend />
|
||||
<Line type="monotone" dataKey="value" name={i.data.unit || '值'} stroke="#1677ff" strokeWidth={2} dot={{ r: 3 }} />
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
<div style={{ maxHeight: 240, overflow: 'auto' }}>
|
||||
<table className="data-table">
|
||||
<thead>
|
||||
<tr><th>月份</th><th style={{ textAlign: 'right' }}>数值</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map((r) => (
|
||||
<tr key={r.month}><td>{r.month}</td><td style={{ textAlign: 'right' }}>{r.value}</td></tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'
|
||||
import { CapsuleTabs, Card } from 'antd-mobile'
|
||||
import { Card, List, NavBar } from 'antd-mobile'
|
||||
import { api } from '../../api/client.js'
|
||||
import TrendBadge, { calcTrend } from '../../components/TrendBadge.jsx'
|
||||
|
||||
// 策略研究: 顶部 CapsuleTabs 选策略, 下面 4 张图 + 表格
|
||||
// 使用 antd-mobile: CapsuleTabs / Card
|
||||
// 策略研究
|
||||
// 列表态 (默认): 列出所有策略, 每行显示主指标 (债务/GDP) 最新值 + 趋势
|
||||
// 详情态: 顶部 NavBar (返回) + 4 张图 (debt/gdp/debtToGdp/interestToGdp) + 表格
|
||||
const SERIES = [
|
||||
{ key: 'debt', label: '政府债务 (万亿美元)', color: '#8884d8' },
|
||||
{ key: 'gdp', label: 'GDP (万亿美元)', color: '#3cb44b' },
|
||||
@@ -12,21 +14,25 @@ const SERIES = [
|
||||
{ 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 [selected, setSelected] = useState(null)
|
||||
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) return
|
||||
setStrategies(data.strategies || [])
|
||||
if (data.strategies && data.strategies[0]) setSelected(data.strategies[0].name)
|
||||
})
|
||||
.then((data) => { if (!cancelled) setStrategies(data.strategies || []) })
|
||||
.catch((e) => !cancelled && setErr(e.message))
|
||||
.finally(() => !cancelled && setLoading(false))
|
||||
return () => { cancelled = true }
|
||||
@@ -36,67 +42,106 @@ export default function StrategyView() {
|
||||
if (err) return <div className="error-banner">{err}</div>
|
||||
if (!strategies.length) return <div className="empty">暂无策略数据</div>
|
||||
|
||||
const cur = strategies.find((s) => s.name === selected)
|
||||
const rows = cur ? cur.data.rows : []
|
||||
// 详情态
|
||||
if (selected) {
|
||||
const cur = strategies.find((s) => s.name === selected)
|
||||
if (cur) return <StrategyDetail strategy={cur} onBack={() => setSelected(null)} />
|
||||
}
|
||||
|
||||
// 列表态: 主指标 = 债务/GDP, 趋势基于其最近变化
|
||||
return (
|
||||
<div>
|
||||
<div className="list-page">
|
||||
<div className="page-title">策略研究</div>
|
||||
<CapsuleTabs activeKey={selected || undefined} onChange={setSelected}>
|
||||
{strategies.map((s) => (
|
||||
<CapsuleTabs.Tab key={s.name} title={s.description || s.name} />
|
||||
))}
|
||||
</CapsuleTabs>
|
||||
{cur && (
|
||||
<div style={{ padding: '0 12px 12px' }}>
|
||||
<Card className="chart-card">
|
||||
<div style={{ fontSize: 12, color: 'var(--color-muted)', marginBottom: 8 }}>
|
||||
数据 {rows.length} 条 · 来源 {cur.data.source || 'seed'}
|
||||
</div>
|
||||
{SERIES.map((s) => (
|
||||
<div key={s.key} style={{ marginBottom: 12 }}>
|
||||
<div style={{ fontSize: 13, fontWeight: 500, marginBottom: 4 }}>{s.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={s.key} name={s.label} stroke={s.color} strokeWidth={2} dot={{ r: 3 }} />
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
<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>
|
||||
</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>
|
||||
}
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user