fix bugs
All checks were successful
Docker Build and Push / build-image (push) Successful in 3m2s

This commit is contained in:
cheney 2026-08-06 17:43:21 +08:00
parent f2e53df25b
commit 6bacda05f8
6 changed files with 86 additions and 9 deletions

33
scripts/diag-news.cjs Normal file
View File

@ -0,0 +1,33 @@
// 临时诊断: 检查 News 表 content/summary 的换行与长度情况 (用完即删)
require('dotenv').config()
const mysql = require('mysql2/promise')
function parseUrl(url) {
const u = new URL(url)
return { host: u.hostname, port: Number(u.port) || 3306, user: decodeURIComponent(u.username), password: decodeURIComponent(u.password), database: u.pathname.replace(/^\//, '') }
}
async function main() {
const conn = await mysql.createConnection(parseUrl(process.env.DATABASE_URL))
const [rows] = await conn.execute(
'SELECT id, title, summary, content, CHAR_LENGTH(content) AS clen, CHAR_LENGTH(summary) AS slen FROM `News` ORDER BY id DESC LIMIT 10'
)
console.log('总条数检查:')
const [[cnt]] = await conn.execute('SELECT COUNT(*) AS c, SUM(content IS NULL) AS nullContent FROM `News`')
console.log(JSON.stringify(cnt))
for (const r of rows) {
const content = r.content || ''
const summary = r.summary || ''
const lf = (content.match(/\n/g) || []).length
const cr = (content.match(/\r/g) || []).length
const sLf = (summary.match(/\n/g) || []).length
console.log('---')
console.log('id=' + r.id + ' clen=' + r.clen + ' slen=' + r.slen + ' contentLF=' + lf + ' contentCR=' + cr + ' summaryLF=' + sLf)
console.log('title: ' + r.title)
console.log('summary: ' + JSON.stringify(summary.slice(0, 120)))
console.log('content(hex首200): ' + content.slice(0, 200).replace(/[^\x20-\x7E\u4e00-\u9fa5]/g, (c) => '\\x' + c.charCodeAt(0).toString(16)))
console.log('content(带转义首300): ' + JSON.stringify(content.slice(0, 300)))
}
await conn.end()
}
main().catch((e) => { console.error(e); process.exit(1) })

View File

@ -55,6 +55,12 @@ function parseNewsSheet(sheet, impactBase = 0) {
const publishedAt = parsePublishedAt(r[2])
if (!publishedAt) continue
const body = String(r[1] || '').trim()
// 数据源段落间用全角空格连接, 入库前统一转成换行, 保证详情页有段落感
.replace(/\u3000+/g, '\n')
.replace(/\u00a0/g, ' ')
.replace(/[ \t]+\n/g, '\n')
.replace(/\n{3,}/g, '\n\n')
.trim()
out.push({
title,
summary: body.slice(0, 180),

View File

@ -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>

View File

@ -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="暂无主机" />}

View File

@ -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>

View File

@ -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; }