页面调整

This commit is contained in:
cheney
2026-08-06 11:26:54 +08:00
parent 7f72bf809b
commit 7fcd0c9569
6 changed files with 314 additions and 221 deletions
+83 -58
View File
@@ -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>
)
}