v1.2.42 一尘看板基础版

This commit is contained in:
龙大 2026-04-06 21:17:15 +08:00
parent b2b30cc54e
commit 7ff0ae5c9e
9 changed files with 503 additions and 9 deletions

View File

@ -0,0 +1,37 @@
import os
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.pool import QueuePool
COOLBOT_DB_URL = os.getenv(
"COOLBOT_DB_URL",
"postgresql://coolbot:Coolbot123@pgm-bp1t1008h019ez6cno.pg.rds.aliyuncs.com:5432/coolbot_data"
)
coolbot_engine = create_engine(
COOLBOT_DB_URL,
poolclass=QueuePool,
pool_size=10,
max_overflow=20,
pool_timeout=30,
pool_recycle=1800,
pool_pre_ping=True,
echo=False,
connect_args={
"connect_timeout": 10,
"application_name": "zodiac-coolbot"
}
)
CoolbotSession = sessionmaker(autocommit=False, autoflush=False, bind=coolbot_engine)
def get_coolbot_db():
"""获取coolbot数据库会话"""
db = CoolbotSession()
try:
yield db
except Exception:
db.rollback()
raise
finally:
db.close()

View File

@ -16,6 +16,7 @@ from app.routers import auth, collections, operations
from app.routers import ocr as ocr_router from app.routers import ocr as ocr_router
from app.routers import users as users_router from app.routers import users as users_router
from app.routers import information as information_router from app.routers import information as information_router
from app.routers import yichens as yichens_router
# 版本信息 - 从 config/VERSION 文件读取 # 版本信息 - 从 config/VERSION 文件读取
def get_version(): def get_version():
@ -80,7 +81,8 @@ app.include_router(operations.router)
app.include_router(ocr_router.router) # OCR 识别 app.include_router(ocr_router.router) # OCR 识别
app.include_router(users_router.router) # 当前用户接口 app.include_router(users_router.router) # 当前用户接口
app.include_router(users_router.admin_router) # 管理员用户管理 app.include_router(users_router.admin_router) # 管理员用户管理
app.include_router(information_router.router) # 资讯 app.include_router(information_router.router)
app.include_router(yichens_router.router) # 一尘看板
@app.get("/") @app.get("/")

View File

@ -16,7 +16,7 @@ router = APIRouter(prefix="/api/information", tags=["资讯"])
class InformationCreate(BaseModel): class InformationCreate(BaseModel):
info_type: str # seek-寻配号, deal-成交数据, publish-发布 info_type: str # seek-寻配号, deal-成交数据, publish-发布
title: str title: str
content: Optional[str] = None content: Optional[str]
collection_id: Optional[str] = None collection_id: Optional[str] = None
expect_category: Optional[str] = None expect_category: Optional[str] = None
expect_version: Optional[str] = None expect_version: Optional[str] = None
@ -30,7 +30,7 @@ class InformationCreate(BaseModel):
class InformationUpdate(BaseModel): class InformationUpdate(BaseModel):
title: Optional[str] = None title: Optional[str] = None
content: Optional[str] = None content: Optional[str]
status: Optional[str] = None status: Optional[str] = None
expect_category: Optional[str] = None expect_category: Optional[str] = None
expect_version: Optional[str] = None expect_version: Optional[str] = None

View File

@ -0,0 +1,181 @@
from fastapi import APIRouter, Depends, Query
from sqlalchemy import func, text
from sqlalchemy.orm import Session
from pydantic import BaseModel
from typing import Optional, List
from datetime import datetime, date
from app.core.coolbot_db import get_coolbot_db
router = APIRouter(prefix="/api/yichens", tags=["一尘看板"])
# ============ 数据模型 ============
class YichensPostStats(BaseModel):
total_posts: int
total_deals: int # 出售
total_wants: int # 求购
total_replies: int
total_views: int
avg_price: Optional[float]
class CategoryStat(BaseModel):
category: str
count: int
class PostItem(BaseModel):
post_id: str
title: str
category: Optional[str]
post_type: str
price: Optional[float]
author_username: str
post_time: str
reply_count: int
view_count: int
url: Optional[str]
class UserStat(BaseModel):
total_users: int
new_users_today: int
sellers: int
class UserItem(BaseModel):
user_id: str
username: str
avatar_url: Optional[str]
credit_level: Optional[str]
credit_score: Optional[int]
post_count: int
is_seller: bool
registration_date: Optional[str]
# ============ 统计接口 ============
@router.get("/stats/posts", response_model=YichensPostStats)
def get_post_stats(days: int = Query(30, ge=1, le=365), db: Session = Depends(get_coolbot_db)):
"""获取帖子统计"""
result = db.execute(text("""
SELECT
COUNT(*) as total_posts,
COUNT(*) FILTER (WHERE post_type = 'deal') as total_deals,
COUNT(*) FILTER (WHERE post_type = 'want') as total_wants,
COALESCE(SUM(reply_count), 0) as total_replies,
COALESCE(SUM(view_count), 0) as total_views,
AVG(price) as avg_price
FROM yichens_posts
WHERE post_time >= NOW() - INTERVAL '1 day' * :days
"""), {"days": days}).fetchone()
return YichensPostStats(
total_posts=result[0] or 0,
total_deals=result[1] or 0,
total_wants=result[2] or 0,
total_replies=result[3] or 0,
total_views=result[4] or 0,
avg_price=float(result[5]) if result[5] else None
)
@router.get("/stats/categories", response_model=List[CategoryStat])
def get_category_stats(days: int = Query(30, ge=1, le=365), db: Session = Depends(get_coolbot_db)):
"""按分类统计帖子数量"""
results = db.execute(text("""
SELECT category, COUNT(*) as count
FROM yichens_posts
WHERE post_time >= NOW() - INTERVAL '1 day' * :days
GROUP BY category
ORDER BY count DESC
"""), {"days": days}).fetchall()
return [CategoryStat(category=r[0] or "未知", count=r[1]) for r in results]
@router.get("/stats/users", response_model=UserStat)
def get_user_stats(db: Session = Depends(get_coolbot_db)):
"""获取用户统计"""
result = db.execute(text("""
SELECT
COUNT(*) as total_users,
COUNT(*) FILTER (WHERE created_at::date = CURRENT_DATE) as new_users_today,
COUNT(*) FILTER (WHERE is_seller = true) as sellers
FROM yichens_users
""")).fetchone()
return UserStat(
total_users=result[0] or 0,
new_users_today=result[1] or 0,
sellers=result[2] or 0
)
@router.get("/posts", response_model=List[PostItem])
def get_posts(
limit: int = Query(20, ge=1, le=100),
offset: int = Query(0, ge=0),
category: Optional[str] = None,
post_type: Optional[str] = None,
db: Session = Depends(get_coolbot_db)
):
"""获取帖子列表"""
query = """
SELECT post_id, title, category, post_type, price,
author_username, post_time, reply_count, view_count, url
FROM yichens_posts
WHERE 1=1
"""
params = {"limit": limit, "offset": offset}
if category:
query += " AND category = :category"
params["category"] = category
if post_type:
query += " AND post_type = :post_type"
params["post_type"] = post_type
query += " ORDER BY post_time DESC LIMIT :limit OFFSET :offset"
results = db.execute(text(query), params).fetchall()
return [PostItem(
post_id=r[0],
title=r[1] or "",
category=r[2],
post_type=r[3] or "",
price=float(r[4]) if r[4] else None,
author_username=r[5] or "",
post_time=str(r[6]) if r[6] else "",
reply_count=r[7] or 0,
view_count=r[8] or 0,
url=r[9]
) for r in results]
@router.get("/users", response_model=List[UserItem])
def get_users(
limit: int = Query(20, ge=1, le=100),
offset: int = Query(0, ge=0),
is_seller: Optional[bool] = None,
db: Session = Depends(get_coolbot_db)
):
"""获取用户列表"""
query = """
SELECT user_id, username, avatar_url, credit_level, credit_score,
post_count, is_seller, registration_date
FROM yichens_users
WHERE 1=1
"""
params = {"limit": limit, "offset": offset}
if is_seller is not None:
query += " AND is_seller = :is_seller"
params["is_seller"] = is_seller
query += " ORDER BY post_count DESC LIMIT :limit OFFSET :offset"
results = db.execute(text(query), params).fetchall()
return [UserItem(
user_id=r[0],
username=r[1] or "",
avatar_url=r[2],
credit_level=r[3],
credit_score=r[4],
post_count=r[5] or 0,
is_seller=r[6] or False,
registration_date=str(r[7]) if r[7] else None
) for r in results]

View File

@ -1 +1 @@
VERSION=1.2.41 VERSION=1.2.42

View File

@ -9,7 +9,7 @@ export default function Info() {
} }
// tab / // tab /
const [activeTab, setActiveTab] = useState('manage') const [activeTab, setActiveTab] = useState('yichens')
const [showPublish, setShowPublish] = useState(false) const [showPublish, setShowPublish] = useState(false)
const [publishType, setPublishType] = useState('deal') // 'seek' 'deal' const [publishType, setPublishType] = useState('deal') // 'seek' 'deal'
const [myList, setMyList] = useState([]) const [myList, setMyList] = useState([])
@ -227,7 +227,8 @@ export default function Info() {
<div style={{ position: 'sticky', top: 0, background: 'rgba(15,23,42,0.95)', backdropFilter: 'blur(10px)', padding: '16px 20px', borderBottom: '1px solid #1e293b', zIndex: 100 }}> <div style={{ position: 'sticky', top: 0, background: 'rgba(15,23,42,0.95)', backdropFilter: 'blur(10px)', padding: '16px 20px', borderBottom: '1px solid #1e293b', zIndex: 100 }}>
<div style={{ display: 'flex', gap: '8px', background: '#1e293b', borderRadius: '12px', padding: '4px' }}> <div style={{ display: 'flex', gap: '8px', background: '#1e293b', borderRadius: '12px', padding: '4px' }}>
{[ {[
{ key: 'manage', label: '📋 发布管理' } { key: 'manage', label: '📋 发布管理' },
{ key: 'yichens', label: '📊 一尘看板' }
].map(tab => ( ].map(tab => (
<div <div
key={tab.key} key={tab.key}
@ -672,6 +673,165 @@ export default function Info() {
</div> </div>
</div> </div>
)} )}
{/* 一尘看板页 */}
{activeTab === 'yichens' && (
<YichensBoard />
)}
</div>
)
}
// ============ ============
function YichensBoard() {
const [stats, setStats] = useState({ posts: null, categories: [], users: null })
const [posts, setPosts] = useState([])
const [loading, setLoading] = useState(false)
const [expandedPosts, setExpandedPosts] = useState({})
const [postTypeFilter, setPostTypeFilter] = useState('all')
const API_BASE = localStorage.getItem('API_BASE') || ''
useEffect(() => {
fetchStats()
fetchPosts()
}, [])
const fetchStats = async () => {
try {
const [postsRes, catRes, usersRes] = await Promise.all([
fetch(`${API_BASE}/api/yichens/stats/posts?days=30`),
fetch(`${API_BASE}/api/yichens/stats/categories?days=30`),
fetch(`${API_BASE}/api/yichens/stats/users`)
])
const postsData = await postsRes.json()
const catData = await catRes.json()
const usersData = await usersRes.json()
setStats({ posts: postsData, categories: catData, users: usersData })
} catch (e) {
console.error('获取统计失败:', e)
}
}
const fetchPosts = async () => {
setLoading(true)
try {
let url = `${API_BASE}/api/yichens/posts?limit=50`
if (postTypeFilter !== 'all') url += `&post_type=${postTypeFilter}`
const res = await fetch(url)
const data = await res.json()
setPosts(data || [])
} catch (e) {
console.error('获取帖子失败:', e)
}
setLoading(false)
}
const togglePostExpand = (postId) => {
setExpandedPosts(prev => ({ ...prev, [postId]: !prev[postId] }))
}
const filteredPosts = postTypeFilter === 'all'
? posts
: posts.filter(p => p.post_type === postTypeFilter)
return (
<div style={{ padding: '0' }}>
{stats.posts && (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '12px', marginBottom: '20px' }}>
<div style={{ background: 'linear-gradient(145deg, #1f2937 0%, #111827 100%)', borderRadius: '12px', padding: '16px', border: '1px solid #374151' }}>
<div style={{ color: '#9ca3af', fontSize: '12px', marginBottom: '4px' }}>30天帖子</div>
<div style={{ color: '#fff', fontSize: '24px', fontWeight: 'bold' }}>{stats.posts.total_posts}</div>
<div style={{ color: '#6b7280', fontSize: '11px' }}>出售{stats.posts.total_deals} 求购{stats.posts.total_wants}</div>
</div>
<div style={{ background: 'linear-gradient(145deg, #1f2937 0%, #111827 100%)', borderRadius: '12px', padding: '16px', border: '1px solid #374151' }}>
<div style={{ color: '#9ca3af', fontSize: '12px', marginBottom: '4px' }}>浏览量</div>
<div style={{ color: '#fff', fontSize: '24px', fontWeight: 'bold' }}>{(stats.posts.total_views / 10000).toFixed(1)}</div>
<div style={{ color: '#6b7280', fontSize: '11px' }}>回复{stats.posts.total_replies}</div>
</div>
<div style={{ background: 'linear-gradient(145deg, #1f2937 0%, #111827 100%)', borderRadius: '12px', padding: '16px', border: '1px solid #374151' }}>
<div style={{ color: '#9ca3af', fontSize: '12px', marginBottom: '4px' }}>用户数</div>
<div style={{ color: '#fff', fontSize: '24px', fontWeight: 'bold' }}>{stats.users?.total_users || 0}</div>
<div style={{ color: '#6b7280', fontSize: '11px' }}>今日新增{stats.users?.new_users_today || 0}</div>
</div>
</div>
)}
<div style={{ background: 'linear-gradient(145deg, #1f2937 0%, #111827 100%)', borderRadius: '12px', padding: '16px', marginBottom: '20px', border: '1px solid #374151' }}>
<div style={{ color: '#f9fafb', fontSize: '14px', fontWeight: '600', marginBottom: '12px' }}>📊 分类统计</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
{stats.categories.map(cat => (
<span key={cat.category} style={{ padding: '4px 12px', background: 'rgba(59,130,246,0.2)', borderRadius: '20px', color: '#93c5fd', fontSize: '12px' }}>
{cat.category} ({cat.count})
</span>
))}
</div>
</div>
<div style={{ display: 'flex', gap: '8px', marginBottom: '16px' }}>
{[
{ key: 'all', label: '全部' },
{ key: 'deal', label: '出售' },
{ key: 'want', label: '求购' }
].map(ft => (
<button
key={ft.key}
onClick={() => { setPostTypeFilter(ft.key); fetchPosts() }}
style={{
padding: '8px 16px',
borderRadius: '8px',
border: 'none',
background: postTypeFilter === ft.key ? 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)' : '#374151',
color: '#fff',
cursor: 'pointer',
fontSize: '13px'
}}
>
{ft.label}
</button>
))}
</div>
{loading ? (
<div style={{ textAlign: 'center', color: '#9ca3af', padding: '40px' }}>加载中...</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
{filteredPosts.map(post => (
<div key={post.post_id} style={{ background: 'linear-gradient(145deg, #1f2937 0%, #111827 100%)', borderRadius: '12px', padding: '16px', border: '1px solid #374151' }}>
<div onClick={() => togglePostExpand(post.post_id)} style={{ cursor: 'pointer' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px' }}>
<span style={{ color: '#fff', fontSize: '15px', fontWeight: '500' }}>{post.title || '无标题'}</span>
<span style={{ color: post.post_type === 'deal' ? '#10b981' : post.post_type === 'want' ? '#f59e0b' : '#9ca3af', fontSize: '12px' }}>
{post.post_type === 'deal' ? '出售' : post.post_type === 'want' ? '求购' : '普通'}
</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', color: '#9ca3af', fontSize: '12px' }}>
<span>{post.author_username || '未知'}</span>
<span>{post.post_time ? post.post_time.substring(0, 16) : ''}</span>
</div>
</div>
{expandedPosts[post.post_id] && (
<div style={{ marginTop: '12px', paddingTop: '12px', borderTop: '1px solid #374151' }}>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '8px', fontSize: '13px' }}>
<div><span style={{ color: '#6b7280' }}>分类: </span><span style={{ color: '#fff' }}>{post.category || '-'}</span></div>
<div><span style={{ color: '#6b7280' }}>价格: </span><span style={{ color: '#10b981' }}>{post.price ? post.price.toLocaleString() + '元' : '待询'}</span></div>
<div><span style={{ color: '#6b7280' }}>浏览: </span><span style={{ color: '#fff' }}>{post.view_count || 0}</span></div>
<div><span style={{ color: '#6b7280' }}>回复: </span><span style={{ color: '#fff' }}>{post.reply_count || 0}</span></div>
</div>
{post.url && (
<a href={post.url} target="_blank" rel="noopener noreferrer" style={{ display: 'inline-block', marginTop: '12px', padding: '8px 16px', background: 'rgba(59,130,246,0.2)', borderRadius: '8px', color: '#60a5fa', fontSize: '13px', textDecoration: 'none' }}>
查看原帖
</a>
)}
</div>
)}
</div>
))}
</div>
)}
</div> </div>
) )
} }

View File

@ -1,4 +1,5 @@
import React, { useState, useEffect } from 'react' import React, { useState, useEffect } from 'react'
import YichensBoard from './YichensBoard'
// - // -
const getUserPhone = () => { const getUserPhone = () => {
@ -22,7 +23,7 @@ const getStoredUserId = () => {
// - // -
export default function News() { export default function News() {
const [activeTab, setActiveTab] = useState(localStorage.getItem('news_activeTab') || 'seek') const [activeTab, setActiveTab] = useState(localStorage.getItem('news_activeTab') || 'yichen')
const [showSeekPublish, setShowSeekPublish] = useState(false) const [showSeekPublish, setShowSeekPublish] = useState(false)
const [showMySeeks, setShowMySeeks] = useState(false) // const [showMySeeks, setShowMySeeks] = useState(false) //
const [showContactId, setShowContactId] = useState(null) // ID const [showContactId, setShowContactId] = useState(null) // ID
@ -455,7 +456,7 @@ export default function News() {
</div> </div>
) : ( ) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
{infoList.map(item => { {activeTab === 'yichen' ? <YichensBoard /> : infoList.map(item => {
// //
const content = item.content || '' const content = item.content || ''
const featuresMatch = content.match(/号码特征[:\s]*(.+?)(?:\n|$)/) const featuresMatch = content.match(/号码特征[:\s]*(.+?)(?:\n|$)/)

View File

@ -0,0 +1,113 @@
import React, { useState, useEffect } from 'react'
export default function YichensBoard() {
const [stats, setStats] = useState({ posts: null, categories: [], users: null })
const [posts, setPosts] = useState([])
const [loading, setLoading] = useState(false)
const [expandedPosts, setExpandedPosts] = useState({})
const [postTypeFilter, setPostTypeFilter] = useState('all')
const API_BASE = localStorage.getItem('API_BASE') || ''
useEffect(() => { fetchStats(); fetchPosts() }, [])
const fetchStats = async () => {
try {
const [p, c, u] = await Promise.all([
fetch(`${API_BASE}/api/yichens/stats/posts?days=30`),
fetch(`${API_BASE}/api/yichens/stats/categories?days=30`),
fetch(`${API_BASE}/api/yichens/stats/users`)
])
setStats({ posts: await p.json(), categories: await c.json(), users: await u.json() })
} catch(e) { console.error(e) }
}
const fetchPosts = async () => {
setLoading(true)
let url = `${API_BASE}/api/yichens/posts?limit=50`
if (postTypeFilter !== 'all') url += `&post_type=${postTypeFilter}`
try {
const res = await fetch(url)
setPosts((await res.json()) || [])
} catch { setPosts([]) }
setLoading(false)
}
const togglePost = id => setExpandedPosts(p => ({ ...p, [id]: !p[id] }))
const filtered = postTypeFilter === 'all' ? posts : posts.filter(p => p.post_type === postTypeFilter)
return (
<div style={{ padding: '0' }}>
{stats.posts && (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3,1fr)', gap: 12, marginBottom: 20 }}>
<div style={{ background: 'linear-gradient(145deg, #1f2937 0%, #111827 100%)', borderRadius: 12, padding: 16, border: '1px solid #374151' }}>
<div style={{ color: '#9ca3af', fontSize: 12, marginBottom: 4 }}>30天帖子</div>
<div style={{ color: '#fff', fontSize: 24, fontWeight: 'bold' }}>{stats.posts.total_posts}</div>
<div style={{ color: '#6b7280', fontSize: 11 }}>出售{stats.posts.total_deals} 求购{stats.posts.total_wants}</div>
</div>
<div style={{ background: 'linear-gradient(145deg, #1f2937 0%, #111827 100%)', borderRadius: 12, padding: 16, border: '1px solid #374151' }}>
<div style={{ color: '#9ca3af', fontSize: 12, marginBottom: 4 }}>浏览量</div>
<div style={{ color: '#fff', fontSize: 24, fontWeight: 'bold' }}>{(stats.posts.total_views/10000).toFixed(1)}</div>
<div style={{ color: '#6b7280', fontSize: 11 }}>回复{stats.posts.total_replies}</div>
</div>
<div style={{ background: 'linear-gradient(145deg, #1f2937 0%, #111827 100%)', borderRadius: 12, padding: 16, border: '1px solid #374151' }}>
<div style={{ color: '#9ca3af', fontSize: 12, marginBottom: 4 }}>用户数</div>
<div style={{ color: '#fff', fontSize: 24, fontWeight: 'bold' }}>{stats.users?.total_users||0}</div>
<div style={{ color: '#6b7280', fontSize: 11 }}>今日新增{stats.users?.new_users_today||0}</div>
</div>
</div>
)}
<div style={{ background: 'linear-gradient(145deg, #1f2937 0%, #111827 100%)', borderRadius: 12, padding: 16, marginBottom: 20, border: '1px solid #374151' }}>
<div style={{ color: '#f9fafb', fontSize: 14, fontWeight: 600, marginBottom: 12 }}>📊 分类统计</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
{stats.categories.map(cat => (
<span key={cat.category} style={{ padding: '4px 12px', background: 'rgba(59,130,246,0.2)', borderRadius: 20, color: '#93c5fd', fontSize: 12 }}>
{cat.category} ({cat.count})
</span>
))}
</div>
</div>
<div style={{ display: 'flex', gap: 8, marginBottom: 16 }}>
{[{key:'all',label:'全部'},{key:'deal',label:'出售'},{key:'want',label:'求购'}].map(k => (
<button key={k.key} onClick={() => { setPostTypeFilter(k.key); fetchPosts() }}
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>
{loading ? <div style={{textAlign:'center',color:'#9ca3af',padding:40}}>加载中...</div> :
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
{filtered.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 }}>{post.title||'无标题'}</span>
<span style={{ color: post.post_type==='deal'?'#10b981':post.post_type==='want'?'#f59e0b':'#9ca3af', fontSize: 12 }}>
{post.post_type==='deal'?'出售':post.post_type==='want'?'求购':'普通'}
</span>
</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] && (
<div style={{ marginTop: 12, paddingTop: 12, borderTop: '1px solid #374151' }}>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8, fontSize: 13 }}>
<div><span style={{color:'#6b7280'}}>分类: </span><span style={{color:'#fff'}}>{post.category||'-'}</span></div>
<div><span style={{color:'#6b7280'}}>价格: </span><span style={{color:'#10b981'}}>{post.price?post.price.toLocaleString()+'元':'待询'}</span></div>
<div><span style={{color:'#6b7280'}}>浏览: </span><span style={{color:'#fff'}}>{post.view_count||0}</span></div>
<div><span style={{color:'#6b7280'}}>回复: </span><span style={{color:'#fff'}}>{post.reply_count||0}</span></div>
</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>
)
}