@@ -1,4 +1,5 @@
|
||||
import { Outlet } from 'react-router-dom'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Outlet, useLocation } from 'react-router-dom'
|
||||
import { SafeArea } from 'antd-mobile'
|
||||
import BottomNav from '../components/BottomNav.jsx'
|
||||
|
||||
@@ -8,11 +9,23 @@ import BottomNav from '../components/BottomNav.jsx'
|
||||
// - 底部 TabBar: antd-mobile TabBar 自带底部安全区
|
||||
// 解决: 状态栏遮挡/底部 TabBar 遮挡/左右贴边
|
||||
export default function MobileShell() {
|
||||
const location = useLocation()
|
||||
// pathname 变化时给页面容器加动画 class (仅播放入场动画, 不重挂载子树,
|
||||
// 避免破坏 MacroPage 等页内子路由的挂载状态与滚动位置)
|
||||
const [animating, setAnimating] = useState(false)
|
||||
useEffect(() => {
|
||||
setAnimating(true)
|
||||
const t = setTimeout(() => setAnimating(false), 320)
|
||||
return () => clearTimeout(t)
|
||||
}, [location.pathname])
|
||||
|
||||
return (
|
||||
<div className="app-root">
|
||||
<SafeArea position="top" />
|
||||
<main className="app-main">
|
||||
<Outlet />
|
||||
<div className={'page-transition' + (animating ? ' active' : '')}>
|
||||
<Outlet />
|
||||
</div>
|
||||
</main>
|
||||
<BottomNav />
|
||||
</div>
|
||||
|
||||
@@ -51,17 +51,21 @@ export default function MonitorPage() {
|
||||
)
|
||||
}
|
||||
|
||||
// 主机监控列表模块级缓存: 切走再进入时先展示上次数据, 拉取成功后再静默更新
|
||||
let cachedServers = null
|
||||
|
||||
// 主机监控: komari 实时节点列表 (30s 轮询, 静默刷新)
|
||||
function ServerView() {
|
||||
const [items, setItems] = useState(null) // null = 加载中
|
||||
const [items, setItems] = useState(cachedServers) // 有缓存直接先展示, 无缓存为 null 进入加载态
|
||||
const [err, setErr] = useState('')
|
||||
const navigate = useNavigate()
|
||||
|
||||
const load = useCallback((silent) => {
|
||||
if (!silent) { setErr(''); setItems(null) }
|
||||
if (!silent && cachedServers === null) setItems(null) // 仅无缓存时显示加载中
|
||||
if (!silent) setErr('')
|
||||
api('/servers')
|
||||
.then((data) => { setErr(''); setItems(data.items || []) })
|
||||
.catch((e) => { if (!silent) { setErr(e.message); setItems([]) } })
|
||||
.then((data) => { cachedServers = data.items || []; setErr(''); setItems(data.items || []) })
|
||||
.catch((e) => { if (!silent) { setErr(e.message); if (cachedServers === null) setItems([]) } })
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -76,7 +80,7 @@ function ServerView() {
|
||||
{err && (
|
||||
<div className="error-banner">
|
||||
{err}
|
||||
<span className="retry-link" onClick={load}>重试</span>
|
||||
<span className="retry-link" onClick={() => load(false)}>重试</span>
|
||||
</div>
|
||||
)}
|
||||
{items !== null && !err && items.length === 0 && <Empty description="暂无主机" />}
|
||||
|
||||
@@ -13,6 +13,18 @@ function fmtDate(s) {
|
||||
return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate())
|
||||
}
|
||||
|
||||
// 正文规范化: 数据源(妙想 MCP)的段落间用全角空格 \u3000 连接而非换行,
|
||||
// 这里统一转为换行, 保证详情页有段落感; 换行本身存储/传输是完好的
|
||||
function normalizeContent(s) {
|
||||
if (!s) return s
|
||||
return s
|
||||
.replace(/\u3000+/g, '\n') // 连续全角空格(段落分隔) → 换行
|
||||
.replace(/\u00a0/g, ' ') // 不换行空格 → 普通空格
|
||||
.replace(/[ \t]+\n/g, '\n') // 行尾多余空白
|
||||
.replace(/\n{3,}/g, '\n\n') // 压缩连续空行
|
||||
.trim()
|
||||
}
|
||||
|
||||
export default function NewsDetail() {
|
||||
const { id } = useParams()
|
||||
const navigate = useNavigate()
|
||||
@@ -46,7 +58,7 @@ export default function NewsDetail() {
|
||||
)}
|
||||
</div>
|
||||
<div className="news-content">
|
||||
{item.content ? item.content : item.summary}
|
||||
{normalizeContent(item.content ? item.content : item.summary)}
|
||||
{!item.content && <div className="news-content-empty">(本条暂无全文,仅摘要)</div>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+10
-1
@@ -58,6 +58,15 @@ input, textarea, select { font: inherit; }
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
/* 页面切换动画: pathname 变化时整页淡入 + 轻微上移 (由 MobileShell 控制 class) */
|
||||
.page-transition.active {
|
||||
animation: page-in 0.28s ease;
|
||||
}
|
||||
@keyframes page-in {
|
||||
from { opacity: 0; transform: translateY(14px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* 带有 antd-mobile NavBar 的页面 (Home/Monitor/Me/Article) */
|
||||
.page-with-navbar { display: flex; flex-direction: column; min-height: 100%; }
|
||||
.page-with-navbar .adm-error-block { flex: 1; }
|
||||
@@ -214,7 +223,7 @@ input, textarea, select { font: inherit; }
|
||||
.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-summary { font-size: 12px; color: var(--color-muted); margin-top: 4px; line-height: 1.5; word-break: break-word; overflow-wrap: anywhere; }
|
||||
.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; }
|
||||
|
||||
Reference in New Issue
Block a user