更新首页

This commit is contained in:
cheney
2026-08-06 16:41:47 +08:00
parent 547181d333
commit 4d02439ecb
32 changed files with 5470 additions and 93 deletions
+36 -9
View File
@@ -1,6 +1,8 @@
import { useEffect, useState, useRef, useCallback } from 'react'
import { NavBar, List, Card, Empty, DotLoading } from 'antd-mobile'
import { useNavigate } from 'react-router-dom'
import { NavBar, Card, Empty, DotLoading, Badge } from 'antd-mobile'
import { api } from '../api/client.js'
import SwipeTabs from '../components/SwipeTabs.jsx'
// 首页: 报警模块 + 新闻模块
// - 报警: World Bank 与妙想 MCP 跨源同 (指标, 周期) 数据差异超阈值 (按时间倒序)
@@ -22,41 +24,62 @@ function fmtDate(s) {
}
export default function HomePage() {
const [active, setActive] = useState('news') // 默认选中新闻
const [alertCount, setAlertCount] = useState(0)
const handleAlertCount = useCallback((n) => setAlertCount(n), [])
const tabs = [
{
key: 'alerts',
label: (
<span className="tab-label">
报警
{alertCount > 0 && <Badge content={alertCount} />}
</span>
),
render: () => <AlertSection onCount={handleAlertCount} />,
},
{ key: 'news', label: '新闻', render: () => <NewsSection /> },
]
return (
<div className="page-with-navbar">
<NavBar back={null}>首页</NavBar>
<div className="home-content">
<AlertSection />
<NewsSection />
<SwipeTabs tabs={tabs} active={active} onChange={setActive} />
</div>
</div>
)
}
// 报警模块
function AlertSection() {
function AlertSection({ onCount }) {
const [items, setItems] = useState([])
const [loading, setLoading] = useState(false)
const [done, setDone] = useState(false)
const cursorRef = useRef(null)
const sentinelRef = useRef(null)
const inFlightRef = useRef(false)
const fetchPage = useCallback(async () => {
if (loading || done) return
if (inFlightRef.current || done) return
inFlightRef.current = true
setLoading(true)
try {
const url = '/alerts?limit=10' + (cursorRef.current ? '&cursor=' + cursorRef.current : '')
const data = await api(url)
setItems((prev) => prev.concat(data.items || []))
onCount?.(data.total ?? (data.items?.length || 0))
cursorRef.current = data.nextCursor
if (!data.nextCursor) setDone(true)
} catch (e) {
console.error('alerts load error', e)
setDone(true)
} finally {
inFlightRef.current = false
setLoading(false)
}
}, [loading, done])
}, [done, onCount])
useEffect(() => { fetchPage() }, [])
useEffect(() => {
@@ -106,9 +129,12 @@ function NewsSection() {
const [done, setDone] = useState(false)
const cursorRef = useRef(null)
const sentinelRef = useRef(null)
const inFlightRef = useRef(false)
const navigate = useNavigate()
const fetchPage = useCallback(async () => {
if (loading || done) return
if (inFlightRef.current || done) return
inFlightRef.current = true
setLoading(true)
try {
const url = '/news?limit=10' + (cursorRef.current ? '&cursor=' + encodeURIComponent(cursorRef.current) : '')
@@ -120,9 +146,10 @@ function NewsSection() {
console.error('news load error', e)
setDone(true)
} finally {
inFlightRef.current = false
setLoading(false)
}
}, [loading, done])
}, [done])
useEffect(() => { fetchPage() }, [])
useEffect(() => {
@@ -144,7 +171,7 @@ function NewsSection() {
) : (
<div className="section-list">
{items.map((n) => (
<Card key={n.id} className="news-card">
<Card key={n.id} className="news-card" onClick={() => navigate('/news/' + n.id)}>
<div className="news-title">{n.title}</div>
{n.summary && <div className="news-summary">{n.summary}</div>}
<div className="news-meta">
+56
View File
@@ -0,0 +1,56 @@
import { useEffect, useState } from 'react'
import { useParams, useNavigate } from 'react-router-dom'
import { NavBar } from 'antd-mobile'
import { api } from '../../api/client.js'
// 新闻详情: /news/:id
// 正文来自 News.content (MCP 返回的完整正文), 旧数据可能无 content, 兜底显示摘要
function fmtDate(s) {
if (!s) return ''
const d = new Date(s)
if (isNaN(d.getTime())) return s
const pad = (n) => String(n).padStart(2, '0')
return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate())
}
export default function NewsDetail() {
const { id } = useParams()
const navigate = useNavigate()
const [item, setItem] = useState(null)
const [err, setErr] = useState('')
const [loading, setLoading] = useState(true)
useEffect(() => {
let cancelled = false
setLoading(true)
api('/news/' + encodeURIComponent(id))
.then((data) => { if (!cancelled) setItem(data.item) })
.catch((e) => !cancelled && setErr(e.message))
.finally(() => !cancelled && setLoading(false))
return () => { cancelled = true }
}, [id])
return (
<div className="article-page">
<NavBar onBack={() => navigate(-1)}>新闻详情</NavBar>
{loading && <div className="loading">加载中...</div>}
{err && <div className="error-banner">{err}</div>}
{!loading && !err && !item && <div className="empty">新闻不存在</div>}
{!loading && !err && item && (
<div className="news-detail">
<h1>{item.title}</h1>
<div className="meta">
{item.source || '未知来源'} · {fmtDate(item.publishedAt)} · #{item.impactRank}
{/^https?:\/\//i.test(item.url || '') && (
<a className="news-origin" href={item.url} target="_blank" rel="noreferrer">原文链接</a>
)}
</div>
<div className="news-content">
{item.content ? item.content : item.summary}
{!item.content && <div className="news-content-empty">(本条暂无全文,仅摘要)</div>}
</div>
</div>
)}
</div>
)
}