更换 UI

This commit is contained in:
Cheney
2026-08-06 09:32:15 +08:00
parent 28d1b5d1c6
commit 7f72bf809b
21 changed files with 767 additions and 2277 deletions
+16 -11
View File
@@ -1,8 +1,10 @@
import { useEffect, useState } from 'react'
import { useParams, useNavigate, Link } from 'react-router-dom'
import { useParams, useNavigate } from 'react-router-dom'
import { NavBar } from 'antd-mobile'
import { api } from '../../api/client.js'
// 文章详情: /macro/articles/:slug
// 使用 antd-mobile NavBar 提供原生风格的返回按钮
export default function ArticleDetail() {
const { slug } = useParams()
const navigate = useNavigate()
@@ -20,16 +22,19 @@ export default function ArticleDetail() {
return () => { cancelled = true }
}, [slug])
if (loading) return <div className="loading">加载中...</div>
if (err) return <div className="error-banner">{err}</div>
if (!article) return <div className="empty">文章不存在</div>
return (
<div className="article-detail">
<div className="article-back" onClick={() => navigate(-1)} role="button">‹ 返回</div>
<h1>{article.title}</h1>
<div className="meta">{article.date}</div>
<div className="article-content" dangerouslySetInnerHTML={{ __html: article.contentHtml }} />
<div className="article-page">
<NavBar onBack={() => navigate(-1)}>文章详情</NavBar>
{loading && <div className="loading">加载中...</div>}
{err && <div className="error-banner">{err}</div>}
{!loading && !err && !article && <div className="empty">文章不存在</div>}
{!loading && !err && article && (
<div className="article-detail">
<h1>{article.title}</h1>
<div className="meta">{article.date}</div>
<div className="article-content" dangerouslySetInnerHTML={{ __html: article.contentHtml }} />
</div>
)}
</div>
)
}
}
+20 -9
View File
@@ -1,12 +1,15 @@
import { useEffect, useState } from 'react'
import { Link } from 'react-router-dom'
import { useNavigate } from 'react-router-dom'
import { List } from 'antd-mobile'
import { api } from '../../api/client.js'
// 经济文章: 列表, 点进 /macro/articles/:slug
// 使用 antd-mobile List 仿原生设置/通讯录样式
export default function ArticlesView() {
const [articles, setArticles] = useState([])
const [err, setErr] = useState('')
const [loading, setLoading] = useState(true)
const navigate = useNavigate()
useEffect(() => {
let cancelled = false
@@ -25,15 +28,23 @@ export default function ArticlesView() {
return (
<div>
<div className="page-title">经济文章</div>
<div className="list">
<List className="articles-list" mode="card">
{articles.map((a) => (
<Link key={a.id} to={`/macro/articles/${encodeURIComponent(a.slug)}`} className="list-item">
<div className="list-item-title">{a.title}</div>
<div className="list-item-meta">{a.date}</div>
<div className="list-item-excerpt">{a.excerpt}</div>
</Link>
<List.Item
key={a.id}
title={a.title}
description={
<>
<div className="list-item-meta">{a.date}</div>
<div className="list-item-excerpt">{a.excerpt}</div>
</>
}
arrowIcon
clickable
onClick={() => navigate('/macro/articles/' + encodeURIComponent(a.slug))}
/>
))}
</div>
</List>
</div>
)
}
}
+23 -38
View File
@@ -1,8 +1,11 @@
import { useEffect, useState } from 'react'
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'
import { CapsuleTabs, Card } from 'antd-mobile'
import { api } from '../../api/client.js'
// 经济数据: 从 /api/indicators 拉所有指标, 顶上一个 chip 行切换, 下面渲染图表+表格
// 经济数据: 顶部 CapsuleTabs 选指标, 下面渲染图表 + 表格
// 使用 antd-mobile: CapsuleTabs / Card
// 多 groupName 的指标: 在 chip 文本中携带分组前缀以便区分
export default function DataView() {
const [indicators, setIndicators] = useState([])
const [selected, setSelected] = useState(null)
@@ -27,37 +30,32 @@ 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>
// 按 groupName 分组, chip 行按组排列
const groups = groupBy(indicators, 'groupName')
// 为 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
}
const cur = indicators.find((i) => i.name === selected)
const chartData = cur ? buildMonthlySeries(cur.data) : []
return (
<div>
<div className="page-title">经济数据</div>
{Object.entries(groups).map(([groupName, list]) => (
<div key={groupName}>
<div className="section-title">{groupName}</div>
<div className="chip-row">
{list.map((i) => (
<button
key={i.name}
className={'chip' + (selected === i.name ? ' active' : '')}
onClick={() => setSelected(i.name)}
>
{shortTitle(i)}
</button>
))}
</div>
</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' }}>
<div className="card">
<div style={{ fontWeight: 600, marginBottom: 4 }}>{cur.data.title || cur.name}</div>
<Card className="chart-card">
<div style={{ fontSize: 12, color: 'var(--color-muted)', marginBottom: 8 }}>
单位 {cur.data.unit || '-'} · 来源 {cur.data.source || 'seed'}
{labelOf(cur)} · 单位 {cur.data.unit || '-'} · 来源 {cur.data.source || 'seed'}
</div>
<div className="chart-wrap">
<ResponsiveContainer width="100%" height={260}>
@@ -67,7 +65,7 @@ export default function DataView() {
<YAxis tick={{ fontSize: 11 }} />
<Tooltip />
<Legend />
<Line type="monotone" dataKey="value" name={cur.data.unit || '值'} stroke="#2563eb" strokeWidth={2} dot={{ r: 3 }} />
<Line type="monotone" dataKey="value" name={cur.data.unit || '值'} stroke="#1677ff" strokeWidth={2} dot={{ r: 3 }} />
</LineChart>
</ResponsiveContainer>
</div>
@@ -83,27 +81,14 @@ export default function DataView() {
</tbody>
</table>
</div>
</div>
</Card>
</div>
)}
</div>
)
}
function groupBy(arr, key) {
return arr.reduce((acc, x) => {
const k = x[key] || '其它'
;(acc[k] = acc[k] || []).push(x)
return acc
}, {})
}
function shortTitle(i) {
if (i.data && i.data.title) return i.data.title
return i.name
}
// 把 data.rows: [{month, value}, ...] 直接返回, 服务端已经按月排好
function buildMonthlySeries(data) {
return (data && data.rows) || []
}
}
+13 -19
View File
@@ -1,13 +1,15 @@
import { useEffect, useState } from 'react'
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'
import { CapsuleTabs, Card } from 'antd-mobile'
import { api } from '../../api/client.js'
// 策略研究: 顶部 chip 行选策略, 下面显示 4 张图 (debt / gdp / debtToGdp / interestToGdp) + 表格
// 策略研究: 顶部 CapsuleTabs 选策略, 下面 4 张图 + 表格
// 使用 antd-mobile: CapsuleTabs / Card
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' },
{ key: 'debt', label: '政府债务 (万亿美元)', color: '#8884d8' },
{ key: 'gdp', label: 'GDP (万亿美元)', color: '#3cb44b' },
{ key: 'debtToGdp', label: '债务/GDP (%)', color: '#4363d8' },
{ key: 'interestToGdp', label: '债务利息/GDP (%)', color: '#f58231' },
]
export default function StrategyView() {
@@ -40,22 +42,14 @@ export default function StrategyView() {
return (
<div>
<div className="page-title">策略研究</div>
<div className="chip-row" style={{ paddingLeft: 12 }}>
<CapsuleTabs activeKey={selected || undefined} onChange={setSelected}>
{strategies.map((s) => (
<button
key={s.name}
className={'chip' + (selected === s.name ? ' active' : '')}
onClick={() => setSelected(s.name)}
>
{s.description || s.name}
</button>
<CapsuleTabs.Tab key={s.name} title={s.description || s.name} />
))}
</div>
</CapsuleTabs>
{cur && (
<div style={{ padding: '0 12px 12px' }}>
<div className="card">
<div style={{ fontWeight: 600, marginBottom: 4 }}>{cur.description || cur.name}</div>
<Card className="chart-card">
<div style={{ fontSize: 12, color: 'var(--color-muted)', marginBottom: 8 }}>
数据 {rows.length} 条 · 来源 {cur.data.source || 'seed'}
</div>
@@ -100,9 +94,9 @@ export default function StrategyView() {
</tbody>
</table>
</div>
</div>
</Card>
</div>
)}
</div>
)
}
}