更新首页
This commit is contained in:
@@ -2,6 +2,7 @@ import { Routes, Route, Navigate, useLocation } from 'react-router-dom'
|
||||
import MobileShell from './layouts/MobileShell.jsx'
|
||||
import LoginPage from './pages/LoginPage.jsx'
|
||||
import HomePage from './pages/HomePage.jsx'
|
||||
import NewsDetail from './pages/news/NewsDetail.jsx'
|
||||
import MacroPage from './pages/MacroPage.jsx'
|
||||
import MonitorPage from './pages/MonitorPage.jsx'
|
||||
import MePage from './pages/MePage.jsx'
|
||||
@@ -30,6 +31,7 @@ export default function App() {
|
||||
<Route path="/login" element={<RedirectIfAuthed><LoginPage /></RedirectIfAuthed>} />
|
||||
<Route element={<RequireAuth><MobileShell /></RequireAuth>}>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/news/:id" element={<NewsDetail />} />
|
||||
<Route path="/macro/*" element={<MacroPage />} />
|
||||
<Route path="/monitor" element={<MonitorPage />} />
|
||||
<Route path="/me" element={<MePage />} />
|
||||
|
||||
+36
-9
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -197,6 +197,8 @@ input, textarea, select { font: inherit; }
|
||||
.me-logout { color: var(--color-danger); }
|
||||
/* 首页模块: 报警 + 新闻 */
|
||||
.home-content { padding: 0 0 16px; }
|
||||
.home-content .adm-tabs-content { --content-padding: 0; }
|
||||
.home-content .tab-label { display: inline-flex; align-items: center; gap: 4px; }
|
||||
.home-section { padding: 0 12px 16px; }
|
||||
.section-title { font-size: 16px; font-weight: 700; padding: 14px 4px 8px; color: var(--color-text); }
|
||||
.section-list { display: flex; flex-direction: column; gap: 8px; }
|
||||
@@ -214,4 +216,12 @@ input, textarea, select { font: inherit; }
|
||||
.news-summary { font-size: 12px; color: var(--color-muted); margin-top: 4px; line-height: 1.5; max-height: 3.6em; overflow: hidden; }
|
||||
.news-meta { display: flex; gap: 10px; font-size: 11px; color: var(--color-muted); margin-top: 6px; }
|
||||
.news-rank { color: var(--color-primary); font-weight: 600; }
|
||||
.news-card:active .adm-card-body { background: #f5f5f5; }
|
||||
/* 新闻详情页 */
|
||||
.news-detail { background: #fff; padding: 18px 16px 24px; }
|
||||
.news-detail h1 { font-size: 20px; line-height: 1.35; margin-bottom: 8px; }
|
||||
.news-detail .meta { font-size: 12px; color: var(--color-muted); margin-bottom: 14px; padding-bottom: 10px; border-bottom: 1px solid var(--color-border); display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
|
||||
.news-origin { color: var(--color-primary); margin-left: auto; }
|
||||
.news-content { font-size: 15px; line-height: 1.75; color: #333; white-space: pre-wrap; word-break: break-word; }
|
||||
.news-content-empty { margin-top: 12px; font-size: 12px; color: var(--color-muted); }
|
||||
.scroll-sentinel { text-align: center; padding: 14px 0 4px; color: var(--color-muted); font-size: 12px; }
|
||||
|
||||
Reference in New Issue
Block a user