134 lines
5.1 KiB
React
134 lines
5.1 KiB
React
import { useEffect, useState, useCallback } from 'react'
|
||
import { useParams, useNavigate } from 'react-router-dom'
|
||
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, lastPoint } from '../../components/TrendBadge.jsx'
|
||
import useRefreshOnVisible from '../../hooks/useRefreshOnVisible.js'
|
||
|
||
// 经济数据
|
||
// 列表态 (默认): 列出所有指标, 每行显示最新值 + 趋势徽标, 点击进入详情
|
||
// 详情态: 顶部 NavBar (返回) + 折线图 + 数据表
|
||
// 频率中文标签 (对应 Indicator.frequency)
|
||
const FREQ_LABEL = { year: '年', quarter: '季', month: '月', day: '日' }
|
||
|
||
// 统一数据行的时间字段: 兼容 {month}/{year}/{period} 三种存储形状
|
||
function periodOf(r) {
|
||
return (r && (r.period || r.month || r.year)) || ''
|
||
}
|
||
|
||
export default function DataView() {
|
||
const [indicators, setIndicators] = useState([])
|
||
const [err, setErr] = useState('')
|
||
const [loading, setLoading] = useState(true)
|
||
// 路由驱动: /macro/data 列表, /macro/data/:name 详情, 浏览器/手机返回键天然可用
|
||
const { name } = useParams()
|
||
const navigate = useNavigate()
|
||
|
||
const load = useCallback(() => {
|
||
setLoading(true)
|
||
api('/indicators')
|
||
.then((data) => { setErr(''); setIndicators(data.indicators || []) })
|
||
.catch((e) => setErr(e.message))
|
||
.finally(() => setLoading(false))
|
||
}, [])
|
||
|
||
useEffect(() => { load() }, [load])
|
||
// 后台回前台时重新拉取
|
||
useRefreshOnVisible(load)
|
||
|
||
if (loading) return <div className="loading">加载中...</div>
|
||
if (err) return <div className="error-banner">{err}</div>
|
||
if (!indicators.length) return <div className="empty">暂无指标数据, 请先执行 npm run db:seed</div>
|
||
|
||
// 详情态 (路径 /macro/data/:name)
|
||
if (name) {
|
||
const cur = indicators.find((i) => i.name === name)
|
||
if (cur) return <DataDetail indicator={cur} onBack={() => navigate('/macro/data')} />
|
||
}
|
||
|
||
// 列表态
|
||
const labelOf = (i) => i.title || i.name
|
||
|
||
return (
|
||
<div className="list-page">
|
||
<div className="page-title">经济数据</div>
|
||
<List>
|
||
{indicators.map((i) => {
|
||
const lp = lastPoint({ rows: i.points || [] })
|
||
const { trend, change } = calcTrend(i.points || [])
|
||
const unit = i.unit || ''
|
||
return (
|
||
<List.Item
|
||
key={i.name}
|
||
title={labelOf(i)}
|
||
description={lp ? <><b>{lp.value}{unit}</b> · {periodOf(lp)}</> : '暂无数据'}
|
||
extra={<TrendBadge trend={trend} />}
|
||
|
||
arrowIcon
|
||
clickable
|
||
onClick={() => navigate('/macro/data/' + i.name)}
|
||
/>
|
||
)
|
||
})}
|
||
</List>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// 详情: 折线图 + 数据表
|
||
function DataDetail({ indicator, onBack }) {
|
||
const i = indicator
|
||
const rows = (i.points || [])
|
||
// 图表 X 轴统一用 period 字段
|
||
const chartRows = rows.map((r) => ({ ...r, period: periodOf(r) }))
|
||
// 表格按时间反序: 最新在前
|
||
const tableRows = [...rows].reverse()
|
||
const [showAll, setShowAll] = useState(false)
|
||
const visibleRows = showAll ? tableRows : tableRows.slice(0, 10)
|
||
const lp = lastPoint({ rows })
|
||
const { trend } = calcTrend(rows)
|
||
const title = i.title || i.name
|
||
return (
|
||
<div className="detail-page">
|
||
<NavBar onBack={onBack}>{title}</NavBar>
|
||
<Card className="detail-card">
|
||
<div className="detail-meta">
|
||
单位 {i.unit || '-'} · 频率 {FREQ_LABEL[i.frequency] || i.frequency || '-'} · 来源 {i.source || 'seed'} ·
|
||
{lp && <> 最新 <b style={{ color: 'var(--color-text)' }}>{lp.value}{i.unit || ''}</b> ({periodOf(lp)}) </>}
|
||
<TrendBadge trend={trend} />
|
||
</div>
|
||
<div className="chart-wrap">
|
||
<ResponsiveContainer width="100%" height={260}>
|
||
<LineChart data={chartRows} margin={{ top: 10, right: 16, left: 0, bottom: 0 }}>
|
||
<CartesianGrid strokeDasharray="3 3" />
|
||
<XAxis dataKey="period" tick={{ fontSize: 11 }} />
|
||
<YAxis tick={{ fontSize: 11 }} />
|
||
<Tooltip />
|
||
<Legend />
|
||
<Line type="monotone" dataKey="value" name={i.unit || '值'} stroke="#1677ff" strokeWidth={2} dot={{ r: 3 }} />
|
||
</LineChart>
|
||
</ResponsiveContainer>
|
||
</div>
|
||
<div className={'data-table-wrap' + (showAll ? ' scroll' : '')}>
|
||
<table className="data-table">
|
||
<thead>
|
||
<tr><th>月份</th><th style={{ textAlign: 'right' }}>数值</th></tr>
|
||
</thead>
|
||
<tbody>
|
||
{visibleRows.map((r) => (
|
||
<tr key={periodOf(r)}><td>{periodOf(r)}</td><td style={{ textAlign: 'right' }}>{r.value}</td></tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
{tableRows.length > 10 && !showAll && (
|
||
<div className="table-more" onClick={() => setShowAll(true)}>
|
||
展开全部(共 {tableRows.length} 行)
|
||
</div>
|
||
)}
|
||
</div>
|
||
</Card>
|
||
</div>
|
||
)
|
||
}
|