v1.2.59 - 恢复一尘看板前端页面
This commit is contained in:
parent
2ea781deba
commit
0e89408c93
|
|
@ -9,6 +9,7 @@ import Info from './pages/Info'
|
||||||
import Login from './pages/Login'
|
import Login from './pages/Login'
|
||||||
import Detail from './pages/Detail'
|
import Detail from './pages/Detail'
|
||||||
import Edit from './pages/Edit'
|
import Edit from './pages/Edit'
|
||||||
|
import YichensBoard from './pages/YichensBoard'
|
||||||
import Admin from './pages/Admin'
|
import Admin from './pages/Admin'
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
|
|
@ -35,6 +36,7 @@ export default function App() {
|
||||||
if (basePath === '/stats') return <Stats />
|
if (basePath === '/stats') return <Stats />
|
||||||
if (basePath === '/list') return <List />
|
if (basePath === '/list') return <List />
|
||||||
if (basePath === '/add') return <Add />
|
if (basePath === '/add') return <Add />
|
||||||
|
if (basePath === '/yichens') return <YichensBoard />
|
||||||
if (basePath === '/login') return <Login />
|
if (basePath === '/login') return <Login />
|
||||||
if (basePath === '/settings') return <Settings />
|
if (basePath === '/settings') return <Settings />
|
||||||
if (basePath === '/admin') return <Admin />
|
if (basePath === '/admin') return <Admin />
|
||||||
|
|
@ -91,6 +93,7 @@ export default function App() {
|
||||||
{ path: '/news', icon: '📰', label: '资讯' },
|
{ path: '/news', icon: '📰', label: '资讯' },
|
||||||
{ path: '/stats', icon: '📊', label: '统计' },
|
{ path: '/stats', icon: '📊', label: '统计' },
|
||||||
{ path: '/list', icon: '📚', label: '藏品' },
|
{ path: '/list', icon: '📚', label: '藏品' },
|
||||||
|
{ path: '/yichens', icon: '🐉', label: '一尘' },
|
||||||
{ path: '/add', icon: '🎯', label: '添加' },
|
{ path: '/add', icon: '🎯', label: '添加' },
|
||||||
...(canAccessInfo ? [{ path: '/info', icon: '📝', label: '信息' }] : []),
|
...(canAccessInfo ? [{ path: '/info', icon: '📝', label: '信息' }] : []),
|
||||||
...(isAdmin ? [{ path: '/admin', icon: '⚙️', label: '管理' }] : [])
|
...(isAdmin ? [{ path: '/admin', icon: '⚙️', label: '管理' }] : [])
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,218 @@
|
||||||
|
import React, { useState, useEffect } from 'react'
|
||||||
|
|
||||||
|
export default function YichensBoard() {
|
||||||
|
const [todayStats, setTodayStats] = useState({ total: 0, deals: 0, wants: 0, others: 0 })
|
||||||
|
const [todayCategory, setTodayCategory] = useState([])
|
||||||
|
const [posts, setPosts] = useState([])
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [expandedPosts, setExpandedPosts] = useState({})
|
||||||
|
const [postTypeFilter, setPostTypeFilter] = useState('all')
|
||||||
|
const [categoryFilter, setCategoryFilter] = useState('')
|
||||||
|
const [page, setPage] = useState(1)
|
||||||
|
const [totalPosts, setTotalPosts] = useState(0)
|
||||||
|
const [searchKeyword, setSearchKeyword] = useState('')
|
||||||
|
const API_BASE = localStorage.getItem('API_BASE') || ''
|
||||||
|
|
||||||
|
const today = new Date()
|
||||||
|
const dateStr = today.getFullYear() + '/' + (today.getMonth() + 1) + '/' + today.getDate()
|
||||||
|
|
||||||
|
useEffect(() => { fetchTodayStats(); fetchPosts(1, '') }, [])
|
||||||
|
|
||||||
|
const fetchTodayStats = async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(API_BASE + '/api/yichens/stats/today')
|
||||||
|
setTodayStats(await res.json())
|
||||||
|
} catch(e) { console.error(e) }
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchTodayCategory = async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(API_BASE + '/api/yichens/stats/today-category')
|
||||||
|
setTodayCategory(await res.json())
|
||||||
|
} catch(e) { console.error(e) }
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchPosts = async (p, cat) => {
|
||||||
|
setLoading(true)
|
||||||
|
const currentPage = p !== undefined ? p : page
|
||||||
|
const currentCat = cat !== undefined ? cat : categoryFilter
|
||||||
|
|
||||||
|
let url = API_BASE + '/api/yichens/posts?limit=390&offset=' + ((currentPage - 1) * 390)
|
||||||
|
if (postTypeFilter === 'deal') url += '&post_type=deal'
|
||||||
|
else if (postTypeFilter === 'want') url += '&post_type=want'
|
||||||
|
else if (postTypeFilter === 'other') url += '&post_type=normal'
|
||||||
|
try {
|
||||||
|
const res = await fetch(url)
|
||||||
|
let data = await res.json() || []
|
||||||
|
if (currentCat) {
|
||||||
|
if (currentCat === '龙') {
|
||||||
|
data = data.filter(p => p.category && (p.category.includes('龙') || p.category.includes('纪念钞')))
|
||||||
|
} else if (currentCat === '蛇') {
|
||||||
|
data = data.filter(p => p.category && p.category.includes('蛇'))
|
||||||
|
} else if (currentCat === '马') {
|
||||||
|
data = data.filter(p => p.category && p.category.includes('马'))
|
||||||
|
} else if (currentCat === '其他') {
|
||||||
|
data = data.filter(p => p.category && !p.category.includes('龙') && !p.category.includes('纪念钞') && !p.category.includes('蛇') && !p.category.includes('马'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 搜索关键词过滤 - 强匹配(完全包含)
|
||||||
|
if (searchKeyword) {
|
||||||
|
const kw = searchKeyword.trim()
|
||||||
|
if (kw) {
|
||||||
|
data = data.filter(p =>
|
||||||
|
(p.title && p.title.includes(kw)) ||
|
||||||
|
(p.content && p.content.includes(kw)) ||
|
||||||
|
(p.category && p.category.includes(kw)) ||
|
||||||
|
(p.contact && p.contact.includes(kw))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setPosts(data)
|
||||||
|
setTotalPosts(todayStats.total || 0)
|
||||||
|
} catch { setPosts([]) }
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => { if (todayStats.total > 0) fetchTodayCategory() }, [todayStats])
|
||||||
|
|
||||||
|
// 搜索关键词变化时重新获取数据
|
||||||
|
useEffect(() => {
|
||||||
|
setPage(1)
|
||||||
|
fetchPosts(1, '')
|
||||||
|
}, [searchKeyword])
|
||||||
|
|
||||||
|
const togglePost = id => setExpandedPosts(p => ({ ...p, [id]: !p[id] }))
|
||||||
|
|
||||||
|
const StatCard = ({ label, value, color, onClick }) => (
|
||||||
|
<div onClick={onClick} style={{
|
||||||
|
background: 'linear-gradient(145deg, #1f2937 0%, #111827 100%)',
|
||||||
|
borderRadius: 12, padding: '12px 8px', border: '1px solid #374151',
|
||||||
|
cursor: onClick ? 'pointer' : 'default',
|
||||||
|
textAlign: 'center'
|
||||||
|
}}>
|
||||||
|
<div style={{ color: '#9ca3af', fontSize: 11, marginBottom: 4 }}>{label}</div>
|
||||||
|
<div style={{ color: color || '#fff', fontSize: 20, fontWeight: 'bold' }}>{value}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '0' }}>
|
||||||
|
<div style={{ marginBottom: 16 }}>
|
||||||
|
<div style={{ color: '#f9fafb', fontSize: 14, fontWeight: 600, marginBottom: 10 }}>
|
||||||
|
📈 连体钞/纪念钞 <span style={{ color: '#9ca3af', fontWeight: 400 }}>{dateStr}</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 8 }}>
|
||||||
|
<StatCard label='全部' value={todayStats.total} color='#3b82f6' onClick={() => { setPostTypeFilter('all'); setCategoryFilter(''); setPage(1); fetchPosts(1, '') }} />
|
||||||
|
<StatCard label='出售' value={todayStats.deals} color='#10b981' onClick={() => { setPostTypeFilter('deal'); setCategoryFilter(''); setPage(1); fetchPosts(1, '') }} />
|
||||||
|
<StatCard label='求购' value={todayStats.wants} color='#f59e0b' onClick={() => { setPostTypeFilter('want'); setCategoryFilter(''); setPage(1); fetchPosts(1, '') }} />
|
||||||
|
<StatCard label='其他' value={todayStats.others || 0} color='#8b5cf6' onClick={() => { setPostTypeFilter('other'); setCategoryFilter(''); setPage(1); fetchPosts(1, '') }} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ background: 'linear-gradient(145deg, #1f2937 0%, #111827 100%)', borderRadius: 12, padding: 12, marginBottom: 20, border: '1px solid #374151' }}>
|
||||||
|
<div style={{ color: '#f9fafb', fontSize: 14, fontWeight: 600, marginBottom: 8 }}>📊 今日分类统计</div>
|
||||||
|
<div style={{ display: 'flex', gap: 6, overflowX: 'auto', paddingBottom: 4 }}>
|
||||||
|
{todayCategory.map(cat => (
|
||||||
|
<span key={cat.category} style={{ padding: '4px 10px', background: 'rgba(59,130,246,0.2)', borderRadius: 16, color: '#93c5fd', fontSize: 11, whiteSpace: 'nowrap' }}>
|
||||||
|
{cat.category} ({cat.count})
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 搜索框 */}
|
||||||
|
<div style={{ marginBottom: 12 }}>
|
||||||
|
<div style={{ display: 'flex', gap: 8 }}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="搜索标题、内容、分类..."
|
||||||
|
value={searchKeyword}
|
||||||
|
onChange={(e) => { setSearchKeyword(e.target.value); setPage(1); fetchPosts(1, '') }}
|
||||||
|
style={{ flex: 1, padding: '10px 14px', borderRadius: 8, border: '1px solid #374151', background: '#1f2937', color: '#fff', fontSize: 13, outline: 'none' }}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => { setSearchKeyword(''); setPage(1); fetchPosts(1, '') }}
|
||||||
|
style={{ padding: '10px 16px', borderRadius: 8, border: 'none', background: '#4b5563', color: '#fff', cursor: 'pointer', fontSize: 13 }}>
|
||||||
|
清空
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{searchKeyword && <div style={{ color: '#9ca3af', fontSize: 12, marginTop: 6 }}>搜索: "{searchKeyword}",找到 {posts.length} 条结果</div>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', gap: 8, marginBottom: 12 }}>
|
||||||
|
{[{key:'all',label:'全部'},{key:'deal',label:'出售'},{key:'want',label:'求购'},{key:'other',label:'其他'}].map(k => (
|
||||||
|
<button key={k.key} onClick={() => { setPostTypeFilter(k.key==='other'?'other':k.key); setCategoryFilter(''); setPage(1); fetchPosts(1, '') }}
|
||||||
|
style={{ padding: '8px 16px', borderRadius: 8, border: 'none',
|
||||||
|
background: postTypeFilter===k.key ? 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)' : '#374151', color: '#fff', cursor: 'pointer', fontSize: 13 }}>
|
||||||
|
{k.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', gap: 8, marginBottom: 16 }}>
|
||||||
|
{[{key:'',label:'全部'},{key:'龙',label:'龙钞'},{key:'蛇',label:'蛇钞'},{key:'马',label:'马钞'},{key:'其他',label:'其他'}].map(k => (
|
||||||
|
<button key={k.key} onClick={() => { setCategoryFilter(k.key); setPage(1); fetchPosts(1, k.key) }}
|
||||||
|
style={{ padding: '8px 16px', borderRadius: 8, border: 'none',
|
||||||
|
background: categoryFilter===k.key ? 'linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%)' : '#374151', color: '#fff', cursor: 'pointer', fontSize: 13 }}>
|
||||||
|
{k.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{loading ? <div style={{textAlign:'center',color:'#9ca3af',padding:40}}>加载中...</div> : (
|
||||||
|
<div>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||||
|
{posts.map(post => (
|
||||||
|
<div key={post.post_id} style={{ background: 'linear-gradient(145deg, #1f2937 0%, #111827 100%)', borderRadius: 12, padding: 16, border: '1px solid #374151' }}>
|
||||||
|
<div onClick={() => togglePost(post.post_id)} style={{ cursor: 'pointer' }}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
|
||||||
|
<span style={{ color: '#fff', fontSize: 15, fontWeight: 500, flex: 1 }}>{post.title||'无标题'}</span>
|
||||||
|
<div style={{ display: 'flex', gap: 4 }}>
|
||||||
|
<span style={{ color: post.post_type==='deal'?'#10b981':post.post_type==='want'?'#f59e0b':post.post_type==='normal'?'#8b5cf6':'#9ca3af', fontSize: 12 }}>
|
||||||
|
{post.post_type==='deal'?'出售':post.post_type==='want'?'求购':post.post_type==='normal'?'其他':'普通'}
|
||||||
|
</span>
|
||||||
|
<span style={{ padding: '2px 8px', borderRadius: 4, fontSize: 11, background: post.category?.includes('龙') ? 'rgba(251,191,36,0.4)' : post.category?.includes('马') ? 'rgba(180,83,9,0.4)' : post.category?.includes('蛇') ? 'rgba(249,168,212,0.4)' : 'rgba(139,92,246,0.4)', color: post.category?.includes('龙') ? '#fde047' : post.category?.includes('马') ? '#d97706' : post.category?.includes('蛇') ? '#fbcfe8' : '#c4b5fd' }}>
|
||||||
|
{post.category || '-'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', color: '#9ca3af', fontSize: 12 }}>
|
||||||
|
<span>{post.author_username||'未知'}</span>
|
||||||
|
<span>{post.post_time?.substring(0,16)||''}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{expandedPosts[post.post_id] && post.content && (
|
||||||
|
<div style={{ marginTop: 12, paddingTop: 12, borderTop: '1px solid #374151' }}>
|
||||||
|
<div style={{ color: '#e2e8f0', fontSize: 13, lineHeight: 1.6, whiteSpace: 'pre-wrap', wordBreak: 'break-word', maxHeight: 300, overflowY: 'auto' }}>
|
||||||
|
{post.content}
|
||||||
|
</div>
|
||||||
|
{post.url && <a href={post.url} target='_blank' rel='noopener noreferrer' style={{ display:'inline-block', marginTop:12, padding:'8px 16px', background:'rgba(59,130,246,0.2)', borderRadius:8, color:'#60a5fa', textDecoration:'none', fontSize:13 }}>查看原帖</a>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 分页按钮移到页面底部 */}
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '12px 16px', background: '#1e293b', borderRadius: 12, marginTop: 16 }}>
|
||||||
|
<div style={{ color: '#9ca3af', fontSize: 12 }}>
|
||||||
|
共 {totalPosts} 条帖子,{Math.ceil(totalPosts / 390)} 页,当前第 {page} 页
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', gap: 8 }}>
|
||||||
|
{page > 1 ? (
|
||||||
|
<button onClick={() => { const newPage = page - 1; setPage(newPage); fetchPosts(newPage, categoryFilter) }} style={{ padding: '6px 12px', borderRadius: 6, border: 'none', background: '#3b82f6', color: '#fff', cursor: 'pointer', fontSize: 12 }}>上一页</button>
|
||||||
|
) : (
|
||||||
|
<span style={{ padding: '6px 12px', borderRadius: 6, background: '#374151', color: '#6b7280', fontSize: 12 }}>上一页</span>
|
||||||
|
)}
|
||||||
|
{posts.length >= 390 ? (
|
||||||
|
<button onClick={() => { const newPage = page + 1; setPage(newPage); fetchPosts(newPage, categoryFilter) }} style={{ padding: '6px 12px', borderRadius: 6, border: 'none', background: '#3b82f6', color: '#fff', cursor: 'pointer', fontSize: 12 }}>下一页</button>
|
||||||
|
) : (
|
||||||
|
<span style={{ padding: '6px 12px', borderRadius: 6, background: '#374151', color: '#6b7280', fontSize: 12 }}>下一页</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue