更新首页

This commit is contained in:
cheney
2026-08-06 15:39:59 +08:00
parent ec15f40323
commit 547181d333
23 changed files with 3248 additions and 19 deletions
+157 -9
View File
@@ -1,16 +1,164 @@
import { ErrorBlock, NavBar } from 'antd-mobile'
import { useEffect, useState, useRef, useCallback } from 'react'
import { NavBar, List, Card, Empty, DotLoading } from 'antd-mobile'
import { api } from '../api/client.js'
// 首页: 报警模块 + 新闻模块
// - 报警: World Bank 与妙想 MCP 跨源同 (指标, 周期) 数据差异超阈值 (按时间倒序)
// - 新闻: mx_finance_search_news 每日 3 点拉取国内外经济政治新闻 (按时间倒序)
// - 两个模块均无限滚动, 触底自动加载下一页
function fmtDateTime(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()) + ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes())
}
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())
}
// 首页: 暂留占位, 后续接入监控告警 / 个性化推荐
// 使用 antd-mobile ErrorBlock 仿原生空状态页
export default function HomePage() {
return (
<div className="page-with-navbar">
<NavBar back={null}>首页</NavBar>
<ErrorBlock
status="empty"
title="首页"
description="后续接入: 监控告警 / 个性化推荐 / 收藏的指标"
/>
<div className="home-content">
<AlertSection />
<NewsSection />
</div>
</div>
)
}
}
// 报警模块
function AlertSection() {
const [items, setItems] = useState([])
const [loading, setLoading] = useState(false)
const [done, setDone] = useState(false)
const cursorRef = useRef(null)
const sentinelRef = useRef(null)
const fetchPage = useCallback(async () => {
if (loading || done) return
setLoading(true)
try {
const url = '/alerts?limit=10' + (cursorRef.current ? '&cursor=' + cursorRef.current : '')
const data = await api(url)
setItems((prev) => prev.concat(data.items || []))
cursorRef.current = data.nextCursor
if (!data.nextCursor) setDone(true)
} catch (e) {
console.error('alerts load error', e)
setDone(true)
} finally {
setLoading(false)
}
}, [loading, done])
useEffect(() => { fetchPage() }, [])
useEffect(() => {
if (done || items.length === 0) return
const el = sentinelRef.current
if (!el) return
const io = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) fetchPage()
}, { rootMargin: '200px' })
io.observe(el)
return () => io.disconnect()
}, [items.length, done, fetchPage])
return (
<section className="home-section">
<div className="section-title">报警</div>
{items.length === 0 && !loading ? (
<Empty description="暂无报警" />
) : (
<div className="section-list">
{items.map((a) => (
<Card key={a.id} className="alert-card">
<div className="alert-title">
<span className="alert-name">{a.indicatorName}</span>
<span className="alert-period">{a.period}</span>
</div>
<div className="alert-diff">
{a.sourceA} = {a.valueA} vs {a.sourceB} = {a.valueB}
<span className="alert-ratio"> 差异 {(a.diffRatio * 100).toFixed(1)}%</span>
</div>
<div className="alert-time">{fmtDateTime(a.createdAt)}</div>
</Card>
))}
<div ref={sentinelRef} className="scroll-sentinel">
{loading ? <DotLoading /> : (done ? '— 已加载全部 —' : '')}
</div>
</div>
)}
</section>
)
}
// 新闻模块
function NewsSection() {
const [items, setItems] = useState([])
const [loading, setLoading] = useState(false)
const [done, setDone] = useState(false)
const cursorRef = useRef(null)
const sentinelRef = useRef(null)
const fetchPage = useCallback(async () => {
if (loading || done) return
setLoading(true)
try {
const url = '/news?limit=10' + (cursorRef.current ? '&cursor=' + encodeURIComponent(cursorRef.current) : '')
const data = await api(url)
setItems((prev) => prev.concat(data.items || []))
cursorRef.current = data.nextCursor
if (!data.nextCursor) setDone(true)
} catch (e) {
console.error('news load error', e)
setDone(true)
} finally {
setLoading(false)
}
}, [loading, done])
useEffect(() => { fetchPage() }, [])
useEffect(() => {
if (done || items.length === 0) return
const el = sentinelRef.current
if (!el) return
const io = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) fetchPage()
}, { rootMargin: '200px' })
io.observe(el)
return () => io.disconnect()
}, [items.length, done, fetchPage])
return (
<section className="home-section">
<div className="section-title">新闻</div>
{items.length === 0 && !loading ? (
<Empty description="暂无新闻" />
) : (
<div className="section-list">
{items.map((n) => (
<Card key={n.id} className="news-card">
<div className="news-title">{n.title}</div>
{n.summary && <div className="news-summary">{n.summary}</div>}
<div className="news-meta">
<span className="news-source">{n.source || '未知来源'}</span>
<span className="news-time">{fmtDate(n.publishedAt)}</span>
<span className="news-rank">#{n.impactRank}</span>
</div>
</Card>
))}
<div ref={sentinelRef} className="scroll-sentinel">
{loading ? <DotLoading /> : (done ? '— 已加载全部 —' : '')}
</div>
</div>
)}
</section>
)
}
+21 -1
View File
@@ -194,4 +194,24 @@ input, textarea, select { font: inherit; }
.me-email { font-size: 13px; opacity: .85; }
.me-list { margin-top: 12px; }
.me-list .adm-list-item { padding-left: 16px; padding-right: 16px; }
.me-logout { color: var(--color-danger); }
.me-logout { color: var(--color-danger); }
/* 首页模块: 报警 + 新闻 */
.home-content { padding: 0 0 16px; }
.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; }
.alert-card.adm-card { border-radius: 10px; margin: 0; }
.alert-card .adm-card-body { padding: 10px 12px; }
.alert-title { display: flex; justify-content: space-between; font-weight: 600; font-size: 14px; }
.alert-name { color: var(--color-danger); }
.alert-period { color: var(--color-muted); font-weight: 400; }
.alert-diff { font-size: 12px; color: var(--color-text); margin-top: 4px; }
.alert-ratio { color: var(--color-danger); font-weight: 600; margin-left: 6px; }
.alert-time { font-size: 11px; color: var(--color-muted); margin-top: 4px; }
.news-card.adm-card { border-radius: 10px; margin: 0; }
.news-card .adm-card-body { padding: 10px 12px; }
.news-title { font-size: 14px; font-weight: 600; line-height: 1.4; color: var(--color-text); }
.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; }
.scroll-sentinel { text-align: center; padding: 14px 0 4px; color: var(--color-muted); font-size: 12px; }