寻号功能开发:

1. 发布寻号面板:去掉类型、匹配度字段,标题移到正文前面
2. 寻号列表:联系方式默认***隐藏,增加匹配状态按钮
3. 增加匹配并联系藏友、留言藏友按钮
4. 实现留言功能(所有人可见)
5. 后端:添加is_matched字段和新API
This commit is contained in:
甲辰生产 2026-03-28 08:24:08 +08:00
parent f84a85d3c1
commit ce08c68d04
8 changed files with 883 additions and 35 deletions

View File

@ -182,6 +182,12 @@ class Information(Base):
# 状态: active-有效, closed-已关闭, expired-已过期 # 状态: active-有效, closed-已关闭, expired-已过期
status = Column(String(20), default="active", index=True) status = Column(String(20), default="active", index=True)
# 匹配状态: pending-尚未匹配, matched-已经匹配
is_matched = Column(String(20), default="pending", index=True)
# 匹配的用户ID当用户愿意交换联系方式时
matched_user_id = Column(String(36), ForeignKey("users.f99_90_id", ondelete="SET NULL"), nullable=True)
# 浏览/联系次数 # 浏览/联系次数
view_count = Column(Integer, default=0) view_count = Column(Integer, default=0)
contact_count = Column(Integer, default=0) contact_count = Column(Integer, default=0)
@ -193,6 +199,31 @@ class Information(Base):
collection = relationship("Collection") collection = relationship("Collection")
# 资讯评论/留言
class InformationComment(Base):
__tablename__ = "information_comments"
id = Column(String(36), primary_key=True, default=generate_uuid)
information_id = Column(String(36), ForeignKey("information.id", ondelete="CASCADE"), nullable=False, index=True)
user_id = Column(String(36), ForeignKey("users.f99_90_id", ondelete="CASCADE"), nullable=False, index=True)
content = Column(Text, nullable=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
user = relationship("User")
# 资讯联系方式查看记录
class InformationContactView(Base):
__tablename__ = "information_contact_views"
id = Column(String(36), primary_key=True, default=generate_uuid)
information_id = Column(String(36), ForeignKey("information.id", ondelete="CASCADE"), nullable=False, index=True)
viewer_id = Column(String(36), ForeignKey("users.f99_90_id", ondelete="CASCADE"), nullable=False, index=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
viewer = relationship("User")
# 资讯关联用户 (收藏/点赞) # 资讯关联用户 (收藏/点赞)
class InformationLike(Base): class InformationLike(Base):
__tablename__ = "information_likes" __tablename__ = "information_likes"

View File

@ -681,3 +681,82 @@ def get_my_information(
"created_at": i.created_at.isoformat() if i.created_at else None "created_at": i.created_at.isoformat() if i.created_at else None
} for i in infos] } for i in infos]
} }
# ============ 匹配寻号 ============
class MatchSeekRequest(BaseModel):
info_id: str
collection_id: Optional[str] = None
@router.post("/seek/match-confirm")
def match_seek(
request: MatchSeekRequest,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""确认匹配寻号 - 用户愿意交换联系方式给发布者"""
info = db.query(Information).filter(
Information.id == request.info_id,
Information.info_type == "seek",
Information.status == "active"
).first()
if not info:
raise HTTPException(status_code=404, detail="寻配号信息不存在")
# 更新匹配状态
info.is_matched = "matched"
info.matched_user_id = current_user.f99_90_id
# 更新发布寻号者的内容,显示有藏品被匹配
original_content = info.content or ""
# 添加匹配信息藏品被XX藏友匹配联系方式为xxx
match_info = f"\n藏品被{current_user.f01_01_name}匹配成功!联系方式:{request.collection_id or '已交换'}"
info.content = original_content + match_info
db.commit()
return {"message": "匹配成功,已通知发布者", "is_matched": "matched"}
# ============ 添加留言 ============
class CommentRequest(BaseModel):
information_id: str
content: str
@router.post("/comment")
def add_comment(
request: CommentRequest,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""添加留言"""
info = db.query(Information).filter(
Information.id == request.information_id
).first()
if not info:
raise HTTPException(status_code=404, detail="资讯不存在")
# 创建留言
from app.models.models import InformationComment
comment = InformationComment(
information_id=request.information_id,
user_id=current_user.f99_90_id,
content=request.content
)
db.add(comment)
db.commit()
return {
"message": "留言成功",
"comment": {
"id": comment.id,
"content": comment.content,
"user_name": current_user.f01_01_name,
"user_avatar": current_user.avatar,
"created_at": comment.created_at
}
}

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">
<title>甲辰收藏 v1.2.18</title> <title>甲辰收藏 v1.2.17</title>
<!-- Favicon --> <!-- Favicon -->
<link rel="icon" type="image/x-icon" href="/static/icons/favicon.ico" /> <link rel="icon" type="image/x-icon" href="/static/icons/favicon.ico" />

View File

@ -5,6 +5,11 @@ export default function News() {
const [activeTab, setActiveTab] = useState('deal') const [activeTab, setActiveTab] = useState('deal')
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 [showCommentId, setShowCommentId] = useState(null) // ID
const [commentText, setCommentText] = useState('') //
const [comments, setComments] = useState({}) //
const [matchedStatus, setMatchedStatus] = useState({}) //
const [showMatchList, setShowMatchList] = useState(false) const [showMatchList, setShowMatchList] = useState(false)
const [matchCollections, setMatchCollections] = useState([]) const [matchCollections, setMatchCollections] = useState([])
const [seekForm, setSeekForm] = useState({ const [seekForm, setSeekForm] = useState({
@ -14,6 +19,7 @@ export default function News() {
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const API_BASE = localStorage.getItem('API_BASE') || '' const API_BASE = localStorage.getItem('API_BASE') || ''
const currentUser = localStorage.getItem('token') ? { id: 'logged_in' } : null
// //
useEffect(() => { useEffect(() => {
@ -22,9 +28,9 @@ export default function News() {
// //
useEffect(() => { useEffect(() => {
const title = `「寻号 ${seekForm.edition || '龙钞,马钞,蛇钞'} ${seekForm.type || '不限'} J0${seekForm.features || 'XXXXXXXX'} ${seekForm.matchType}` const title = `「寻号 ${seekForm.edition || '龙钞,马钞,蛇钞'} J0${seekForm.features || 'XXXXXXXX'}`
setSeekForm(prev => ({...prev, title})) setSeekForm(prev => ({...prev, title}))
}, [seekForm.edition, seekForm.type, seekForm.features, seekForm.matchType]) }, [seekForm.edition, seekForm.features])
const fetchInfoList = async () => { const fetchInfoList = async () => {
setLoading(true) setLoading(true)
@ -46,6 +52,63 @@ export default function News() {
} }
// //
//
const fetchComments = async (infoId) => {
if (comments[infoId]) return; //
try {
const res = await fetch(`${API_BASE}/api/information/comments/${infoId}`)
const data = await res.json()
setComments(prev => ({...prev, [infoId]: data || []}))
} catch (e) {
console.error('获取评论失败:', e)
}
}
//
const addComment = async (infoId) => {
if (!commentText.trim()) { alert('请输入评论内容'); return }
const token = localStorage.getItem('token')
if (!token) { alert('请先登录'); return }
try {
const res = await fetch(`${API_BASE}/api/information/comment`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ information_id: infoId, content: commentText })
})
const data = await res.json()
if (data.message === '留言成功') {
setCommentText('')
setComments(prev => ({...prev, [infoId]: [...(prev[infoId] || []), data.comment]}))
alert('留言成功!')
}
} catch (e) {
alert('留言失败: ' + e.message)
}
}
//
const matchAndContact = async (infoId) => {
const token = localStorage.getItem('token')
if (!token) { alert('请先登录'); return }
try {
const res = await fetch(`${API_BASE}/api/information/seek/match-confirm`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ info_id: infoId })
})
const data = await res.json()
if (data.message === '匹配成功,已通知发布者') {
setMatchedStatus(prev => ({...prev, [infoId]: 'matched'}))
setShowContactId(infoId) //
alert('匹配成功!')
fetchInfoList() //
}
} catch (e) {
alert('操作失败: ' + e.message)
}
}
const fetchMatchCollections = async (infoId) => { const fetchMatchCollections = async (infoId) => {
const token = localStorage.getItem('token') const token = localStorage.getItem('token')
if (!token) { alert('请先登录'); return } if (!token) { alert('请先登录'); return }
@ -191,34 +254,10 @@ export default function News() {
})} })}
</div> </div>
</div> </div>
<div style={{ marginBottom: '12px' }}><label style={{ color: '#94a3b8', fontSize: '12px' }}>类型</label>
<div style={{ display: 'flex', gap: '8px', marginTop: '6px' }}>
{['标百','标十','单张'].map(e => (
<button key={e} onClick={() => setSeekForm({...seekForm, type: e})}
style={{ flex: 1, padding: '8px', borderRadius: '6px', border: seekForm.type===e?'2px solid #10b981':'1px solid #334155', background: seekForm.type===e?'rgba(16,185,129,0.2)':'#0f172a', color: '#fff', fontSize: '12px' }}>{e}</button>
))}
</div>
</div>
<div style={{ marginBottom: '12px' }}><label style={{ color: '#94a3b8', fontSize: '12px' }}>分类</label>
<div style={{ display: 'flex', gap: '8px', marginTop: '6px' }}>
{['求购','出售','寻号'].map(e => (
<button key={e} onClick={() => setSeekForm({...seekForm, category: e})}
style={{ flex: 1, padding: '8px', borderRadius: '6px', border: seekForm.category===e?'2px solid #10b981':'1px solid #334155', background: seekForm.category===e?'rgba(16,185,129,0.2)':'#0f172a', color: '#fff', fontSize: '12px' }}>{e}</button>
))}
</div>
</div>
<div style={{ marginBottom: '12px' }}><label style={{ color: '#94a3b8', fontSize: '12px' }}>价格选填</label> <div style={{ marginBottom: '12px' }}><label style={{ color: '#94a3b8', fontSize: '12px' }}>价格选填</label>
<input type="text" value={seekForm.price} onChange={(e) => setSeekForm({...seekForm, price: e.target.value})} placeholder="请输入价格" <input type="text" value={seekForm.price} onChange={(e) => setSeekForm({...seekForm, price: e.target.value})} placeholder="请输入价格"
style={{ width: '100%', padding: '8px', marginTop: '6px', background: '#0f172a', border: '1px solid #334155', borderRadius: '6px', color: '#fff', fontSize: '13px', boxSizing: 'border-box' }} /> style={{ width: '100%', padding: '8px', marginTop: '6px', background: '#0f172a', border: '1px solid #334155', borderRadius: '6px', color: '#fff', fontSize: '13px', boxSizing: 'border-box' }} />
</div> </div>
<div style={{ marginBottom: '12px' }}><label style={{ color: '#94a3b8', fontSize: '12px' }}>匹配度</label>
<div style={{ display: 'flex', gap: '8px', marginTop: '6px' }}>
{['精准','模糊'].map(e => (
<button key={e} onClick={() => setSeekForm({...seekForm, matchType: e})}
style={{ flex: 1, padding: '8px', borderRadius: '6px', border: seekForm.matchType===e?'2px solid #10b981':'1px solid #334155', background: seekForm.matchType===e?'rgba(16,185,129,0.2)':'#0f172a', color: '#fff', fontSize: '12px' }}>{e}</button>
))}
</div>
</div>
<div style={{ marginBottom: '8px' }}><label style={{ color: '#94a3b8', fontSize: '12px' }}>号码特征10</label> <div style={{ marginBottom: '8px' }}><label style={{ color: '#94a3b8', fontSize: '12px' }}>号码特征10</label>
<div style={{ display: 'flex', gap: '4px', marginTop: '6px', flexWrap: 'wrap' }}> <div style={{ display: 'flex', gap: '4px', marginTop: '6px', flexWrap: 'wrap' }}>
{[0,1,2,3,4,5,6,7,8,9].map(i => { {[0,1,2,3,4,5,6,7,8,9].map(i => {
@ -260,6 +299,10 @@ export default function News() {
备注说明X=任意数字 A=非4 B=非47 C=非347 D=非247 E=非2347 F=非23457 G=非123457 备注说明X=任意数字 A=非4 B=非47 C=非347 D=非247 E=非2347 F=非23457 G=非123457
</div> </div>
</div> </div>
<div style={{ marginBottom: '12px' }}><label style={{ color: '#94a3b8', fontSize: '12px' }}>标题自动生成</label>
<input type="text" value={seekForm.title} readOnly
style={{ width: '100%', padding: '8px', marginTop: '6px', background: '#0f172a', border: '1px solid #334155', borderRadius: '6px', color: '#fff', fontSize: '13px', boxSizing: 'border-box' }} onChange={(e) => setSeekForm({...seekForm, title: e.target.value})} />
</div>
<div style={{ marginBottom: '12px' }}><label style={{ color: '#94a3b8', fontSize: '12px' }}>正文</label> <div style={{ marginBottom: '12px' }}><label style={{ color: '#94a3b8', fontSize: '12px' }}>正文</label>
<textarea value={seekForm.content || ''} onChange={(e) => setSeekForm({...seekForm, content: e.target.value})} placeholder="请输入详细信息..." <textarea value={seekForm.content || ''} onChange={(e) => setSeekForm({...seekForm, content: e.target.value})} placeholder="请输入详细信息..."
style={{ width: '100%', padding: '8px', marginTop: '6px', background: '#0f172a', border: '1px solid #334155', borderRadius: '6px', color: '#fff', fontSize: '13px', boxSizing: 'border-box', resize: 'vertical', minHeight: '60px' }} /> style={{ width: '100%', padding: '8px', marginTop: '6px', background: '#0f172a', border: '1px solid #334155', borderRadius: '6px', color: '#fff', fontSize: '13px', boxSizing: 'border-box', resize: 'vertical', minHeight: '60px' }} />
@ -268,10 +311,6 @@ export default function News() {
<input type="text" value={seekForm.contact || ''} onChange={(e) => setSeekForm({...seekForm, contact: e.target.value})} placeholder="请输入手机号或微信" <input type="text" value={seekForm.contact || ''} onChange={(e) => setSeekForm({...seekForm, contact: e.target.value})} placeholder="请输入手机号或微信"
style={{ width: '100%', padding: '8px', marginTop: '6px', background: '#0f172a', border: '1px solid #334155', borderRadius: '6px', color: '#fff', fontSize: '13px', boxSizing: 'border-box' }} /> style={{ width: '100%', padding: '8px', marginTop: '6px', background: '#0f172a', border: '1px solid #334155', borderRadius: '6px', color: '#fff', fontSize: '13px', boxSizing: 'border-box' }} />
</div> </div>
<div style={{ marginBottom: '12px' }}><label style={{ color: '#94a3b8', fontSize: '12px' }}>标题自动生成</label>
<input type="text" value={seekForm.title} readOnly
style={{ width: '100%', padding: '8px', marginTop: '6px', background: '#0f172a', border: '1px solid #334155', borderRadius: '6px', color: '#fff', fontSize: '13px', boxSizing: 'border-box' }} onChange={(e) => setSeekForm({...seekForm, title: e.target.value})} />
</div>
<div style={{ display: 'flex', gap: '12px' }}> <div style={{ display: 'flex', gap: '12px' }}>
<button onClick={handleSeekPublish} style={{ flex: 1, padding: '12px', background: '#10b981', border: 'none', borderRadius: '8px', color: '#fff', fontSize: '14px', fontWeight: 'bold', cursor: 'pointer' }}>发布寻号</button> <button onClick={handleSeekPublish} style={{ flex: 1, padding: '12px', background: '#10b981', border: 'none', borderRadius: '8px', color: '#fff', fontSize: '14px', fontWeight: 'bold', cursor: 'pointer' }}>发布寻号</button>
<button onClick={() => setShowSeekPublish(false)} style={{ flex: 1, padding: '12px', background: 'transparent', border: '1px solid #334155', borderRadius: '8px', color: '#94a3b8', fontSize: '14px', cursor: 'pointer' }}>取消</button> <button onClick={() => setShowSeekPublish(false)} style={{ flex: 1, padding: '12px', background: 'transparent', border: '1px solid #334155', borderRadius: '8px', color: '#94a3b8', fontSize: '14px', cursor: 'pointer' }}>取消</button>
@ -381,10 +420,124 @@ export default function News() {
{cleanContent} {cleanContent}
</div> </div>
)} )}
{/* 联系方式 */} {/* 联系方式 - 默认隐藏,显示*** */}
{contact && ( {contact && (
<div style={{ color: '#f97316', fontSize: '13px', padding: '8px', background: 'rgba(249,115,22,0.1)', borderRadius: '6px' }}> <div style={{ marginBottom: '8px' }}>
联系方式{contact} <div style={{ color: '#94a3b8', fontSize: '12px', marginBottom: '4px' }}>联系方式</div>
<div style={{ color: '#f97316', fontSize: '13px', padding: '8px', background: 'rgba(249,115,22,0.1)', borderRadius: '6px' }}>
联系方式{showContactId === item.id ? contact : '***'}
</div>
{/* 是否匹配按钮 - 仅对已登录用户显示 */}
{currentUser && (
<div style={{ marginTop: '8px' }}>
<span style={{
padding: '4px 12px',
borderRadius: '4px',
fontSize: '12px',
background: (item.is_matched === 'matched' || matchedStatus[item.id] === 'matched') ? '#10b981' : '#6b7280',
color: '#fff'
}}>
{(item.is_matched === 'matched' || matchedStatus[item.id] === 'matched') ? '已经匹配' : '尚未匹配'}
</span>
</div>
)}
{/* 匹配并联系藏友 / 留言藏友 按钮 - 有匹配藏品才可点击 */}
{currentUser && (
<div style={{ display: 'flex', gap: '8px', marginTop: '8px' }}>
<button
onClick={() => {
if (item.matched_count > 0) {
matchAndContact(item.id)
} else {
alert('暂无匹配藏品,无法匹配')
}
}}
disabled={item.matched_count === 0}
style={{
flex: 1,
padding: '8px 12px',
borderRadius: '6px',
border: 'none',
fontSize: '12px',
background: item.matched_count > 0 ? '#3b82f6' : '#4b5563',
color: '#fff',
cursor: item.matched_count > 0 ? 'pointer' : 'not-allowed',
opacity: item.matched_count > 0 ? 1 : 0.5
}}
>
匹配并联系藏友
</button>
<button
onClick={() => {
setShowCommentId(showCommentId === item.id ? null : item.id)
fetchComments(item.id)
}}
style={{
flex: 1,
padding: '8px 12px',
borderRadius: '6px',
border: '1px solid #334155',
fontSize: '12px',
background: '#1e293b',
color: '#fff',
cursor: 'pointer'
}}
>
留言藏友
</button>
</div>
)}
{/* 留言区域 */}
{showCommentId === item.id && (
<div style={{ marginTop: '8px', padding: '12px', background: '#0f172a', borderRadius: '6px' }}>
{/* 评论列表 */}
{comments[item.id] && comments[item.id].length > 0 && (
<div style={{ marginBottom: '8px' }}>
{comments[item.id].map((c, idx) => (
<div key={c.id || idx} style={{ padding: '8px 0', borderBottom: '1px solid #334155' }}>
<div style={{ color: '#3b82f6', fontSize: '12px', marginBottom: '4px' }}>{c.user_name || '匿名用户'}</div>
<div style={{ color: '#e2e8f0', fontSize: '13px' }}>{c.content}</div>
</div>
))}
</div>
)}
{/* 评论输入框 */}
<div style={{ display: 'flex', gap: '8px' }}>
<input
type="text"
value={commentText}
onChange={(e) => setCommentText(e.target.value)}
placeholder="输入留言..."
style={{
flex: 1,
padding: '8px',
background: '#1e293b',
border: '1px solid #334155',
borderRadius: '4px',
color: '#fff',
fontSize: '13px'
}}
/>
<button
onClick={() => addComment(item.id)}
style={{
padding: '8px 16px',
background: '#10b981',
border: 'none',
borderRadius: '4px',
color: '#fff',
fontSize: '13px',
cursor: 'pointer'
}}
>
发送
</button>
</div>
</div>
)}
</div> </div>
)} )}
</div> </div>

View File

@ -0,0 +1,554 @@
import React, { useState, useEffect } from 'react'
// 资讯页面 - 展示寻配号和行情信息(所有人可见)
export default function News() {
const [activeTab, setActiveTab] = useState('deal')
const [showSeekPublish, setShowSeekPublish] = useState(false)
const [showMySeeks, setShowMySeeks] = useState(false) // 我的寻号
const [showMatchList, setShowMatchList] = useState(false)
const [matchCollections, setMatchCollections] = useState([])
const [seekForm, setSeekForm] = useState({
edition: '龙钞', type: '', category: '寻号', price: '', matchType: '模糊', features: '', content: '', title: '', contact: ''
})
const [infoList, setInfoList] = useState([])
const [loading, setLoading] = useState(false)
const API_BASE = localStorage.getItem('API_BASE') || ''
// 获取资讯列表
useEffect(() => {
fetchInfoList()
}, [activeTab])
// 自动生成寻号标题
useEffect(() => {
const title = `「寻号 ${seekForm.edition || '龙钞,马钞,蛇钞'} ${seekForm.type || '不限'} J0${seekForm.features || 'XXXXXXXX'} ${seekForm.matchType}」`
setSeekForm(prev => ({...prev, title}))
}, [seekForm.edition, seekForm.type, seekForm.features, seekForm.matchType])
const fetchInfoList = async () => {
setLoading(true)
try {
// 寻配号对所有人公开,无需登录即可查看
const token = localStorage.getItem('token')
// 根据tab获取不同类型的数据
const type = activeTab === 'seek' ? 'seek' : 'deal'
// 始终传递token以便获取准确的matched_count
const headers = token ? { Authorization: `Bearer ${token}` } : {}
const res = await fetch(`${API_BASE}/api/information/list?info_type=${type}`, { headers })
const data = await res.json()
console.log('资讯列表:', data)
setInfoList(data || [])
} catch (e) {
console.error(e)
}
setLoading(false)
}
// 获取匹配藏品列表
const fetchMatchCollections = async (infoId) => {
const token = localStorage.getItem('token')
if (!token) { alert('请先登录'); return }
try {
const res = await fetch(`${API_BASE}/api/information/seek/match?info_id=${infoId}`, {
headers: { Authorization: `Bearer ${token}` }
})
const data = await res.json()
console.log('匹配结果:', data)
console.log('匹配数量:', data.matched_count)
console.log('藏品列表:', data.collections)
setMatchCollections(data.collections || [])
setShowMatchList(true)
} catch (e) {
console.error('获取匹配藏品失败:', e)
alert('获取匹配藏品失败: ' + e.message)
}
}
// 获取我的寻号列表
const [mySeekList, setMySeekList] = useState([])
const [editingSeek, setEditingSeek] = useState(null)
const fetchMySeeks = async () => {
const token = localStorage.getItem('token')
if (!token) { alert('请先登录'); return }
try {
const res = await fetch(`${API_BASE}/api/information/my/list`, {
headers: { Authorization: `Bearer ${token}` }
})
const data = await res.json()
// 过滤出寻号类型的
const seeks = Array.isArray(data) ? data.filter(item => item.info_type === 'seek') : []
setMySeekList(seeks)
} catch (e) { console.error(e) }
}
// 更新寻号
const handleUpdateSeek = async () => {
if (!editingSeek) return
const token = localStorage.getItem('token')
try {
// 解析正文中的号码特征和联系方式
const content = (editingSeek.content || '') + '\n联系方式: ' + editingSeek.contact
await fetch(`${API_BASE}/api/information/${editingSeek.id}`, {
method: 'PUT',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
title: editingSeek.title,
content,
expect_category: editingSeek.edition,
expect_number: editingSeek.features ? 'J0' + editingSeek.features : null
})
})
setEditingSeek(null)
fetchMySeeks()
} catch (e) { alert('更新失败') }
}
const handleSeekPublish = async () => {
if (!seekForm.contact) { alert('请填写联系方式'); return }
// 类型改为非必选
try {
const token = localStorage.getItem('token')
const content = (seekForm.content ? seekForm.content + '\n' : '') + (seekForm.price ? `价格: ${seekForm.price}` : '') + (seekForm.features ? '\n号码特征: J0' + seekForm.features : '') + '\n联系方式: ' + seekForm.contact
// 从edition映射到category
const categoryMap = { '龙钞': '龙钞', '马钞': '马钞', '蛇钞': '蛇钞' }
const res = await fetch(`${API_BASE}/api/information/`, {
method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
title: seekForm.title,
content,
info_type: 'seek',
expect_category: seekForm.edition,
expect_number: seekForm.features ? 'J0' + seekForm.features : None
})
})
const data = await res.json()
if (data.id || data.code === 0) {
alert('发布成功!')
setShowSeekPublish(false)
setSeekForm({ edition: '龙钞', type: '', category: '寻号', price: '', matchType: '模糊', features: '', content: '', title: '', contact: '' })
fetchInfoList()
} else { alert(data.message || '发布失败') }
} catch (e) { alert('发布失败: ' + e.message) }
}
// Tab切换
const tabs = [
{ key: 'seek', label: '🔍 寻配号' },
{ key: 'deal', label: '💰 成交行情' }
]
const formatDate = (dateStr) => {
if (!dateStr) return '-'
const date = new Date(dateStr)
return `${date.getMonth() + 1}/${date.getDate()}`
}
return (
<div style={{ minHeight: '100vh', background: '#0f172a', paddingBottom: '60px' }}>
{/* Tab导航 */}
<div style={{ display: 'flex', padding: '12px 16px', background: '#1e293b', gap: '8px', overflowX: 'auto' }}>
{tabs.map(tab => (
<div
key={tab.key}
onClick={() => setActiveTab(tab.key)}
style={{
padding: '10px 20px',
borderRadius: '8px',
background: activeTab === tab.key ? '#3b82f6' : 'transparent',
color: '#fff',
cursor: 'pointer',
whiteSpace: 'nowrap',
fontSize: '14px'
}}
>
{tab.label}
</div>
))}
</div>
{/* 资讯列表 */}
<div style={{ padding: '16px' }}>
{showSeekPublish && activeTab === 'seek' && (
<div style={{ background: '#1e293b', borderRadius: '12px', padding: '16px', marginBottom: '16px' }}>
<div style={{ marginBottom: '12px' }}><label style={{ color: '#94a3b8', fontSize: '12px' }}>版别</label>
<div style={{ display: 'flex', gap: '8px', marginTop: '6px' }}>
{['龙钞','马钞','蛇钞'].map(item => {
const isSelected = seekForm.edition ? seekForm.edition.split(',').includes(item) : false
return (
<button key={item} onClick={() => {
const eds = seekForm.edition ? seekForm.edition.split(',').filter(x => x) : []
if (isSelected) {
const idx = eds.indexOf(item)
if (idx > -1) eds.splice(idx, 1)
} else {
eds.push(item)
}
setSeekForm({...seekForm, edition: eds.join(',')})
}}
style={{ flex: 1, padding: '8px', borderRadius: '6px', border: isSelected?'2px solid #10b981':'1px solid #334155', background: isSelected?'rgba(16,185,129,0.2)':'#0f172a', color: '#fff', fontSize: '12px' }}>{item}</button>
)
})}
</div>
</div>
<div style={{ marginBottom: '12px' }}><label style={{ color: '#94a3b8', fontSize: '12px' }}>类型</label>
<div style={{ display: 'flex', gap: '8px', marginTop: '6px' }}>
{['标百','标十','单张'].map(e => (
<button key={e} onClick={() => setSeekForm({...seekForm, type: e})}
style={{ flex: 1, padding: '8px', borderRadius: '6px', border: seekForm.type===e?'2px solid #10b981':'1px solid #334155', background: seekForm.type===e?'rgba(16,185,129,0.2)':'#0f172a', color: '#fff', fontSize: '12px' }}>{e}</button>
))}
</div>
</div>
<div style={{ marginBottom: '12px' }}><label style={{ color: '#94a3b8', fontSize: '12px' }}>分类</label>
<div style={{ display: 'flex', gap: '8px', marginTop: '6px' }}>
{['求购','出售','寻号'].map(e => (
<button key={e} onClick={() => setSeekForm({...seekForm, category: e})}
style={{ flex: 1, padding: '8px', borderRadius: '6px', border: seekForm.category===e?'2px solid #10b981':'1px solid #334155', background: seekForm.category===e?'rgba(16,185,129,0.2)':'#0f172a', color: '#fff', fontSize: '12px' }}>{e}</button>
))}
</div>
</div>
<div style={{ marginBottom: '12px' }}><label style={{ color: '#94a3b8', fontSize: '12px' }}>价格(选填)</label>
<input type="text" value={seekForm.price} onChange={(e) => setSeekForm({...seekForm, price: e.target.value})} placeholder="请输入价格"
style={{ width: '100%', padding: '8px', marginTop: '6px', background: '#0f172a', border: '1px solid #334155', borderRadius: '6px', color: '#fff', fontSize: '13px', boxSizing: 'border-box' }} />
</div>
<div style={{ marginBottom: '12px' }}><label style={{ color: '#94a3b8', fontSize: '12px' }}>匹配度</label>
<div style={{ display: 'flex', gap: '8px', marginTop: '6px' }}>
{['精准','模糊'].map(e => (
<button key={e} onClick={() => setSeekForm({...seekForm, matchType: e})}
style={{ flex: 1, padding: '8px', borderRadius: '6px', border: seekForm.matchType===e?'2px solid #10b981':'1px solid #334155', background: seekForm.matchType===e?'rgba(16,185,129,0.2)':'#0f172a', color: '#fff', fontSize: '12px' }}>{e}</button>
))}
</div>
</div>
<div style={{ marginBottom: '8px' }}><label style={{ color: '#94a3b8', fontSize: '12px' }}>号码特征10位</label>
<div style={{ display: 'flex', gap: '4px', marginTop: '6px', flexWrap: 'wrap' }}>
{[0,1,2,3,4,5,6,7,8,9].map(i => {
const isFixed = i < 2
const char = i === 0 ? 'J' : (i === 1 ? '0' : (seekForm.features[i - 2] || ''))
const isDigit = /[0-9]/.test(char)
const letterColor = isFixed ? '#10b981' : (isDigit ? '#fff' : ({ X: '#94a3b8', A: '#f472b6', B: '#60a5fa', C: '#34d399', D: '#fbbf24', E: '#a78bfa', F: '#fb923c', G: '#22d3ee' }[char] || '#fff'))
return <input key={i} ref={el => { if (el) el.dataset.index = i }} type="text" maxLength={1}
value={char}
readOnly={isFixed}
onChange={(e) => {
if (i < 2) return
const val = e.target.value.toUpperCase().replace(/[^0-9XABCFG]/g, '')
const newFeatures = (seekForm.features || '').split('')
while (newFeatures.length < 8) newFeatures.push('')
newFeatures[i - 2] = val
setSeekForm({...seekForm, features: newFeatures.join('')})
// 自动跳转下一个
if (val && i < 9) {
setTimeout(() => {
const nextInput = document.querySelector(`input[data-index="${i+1}"]`)
if (nextInput) nextInput.focus()
}, 50)
}
}}
onKeyDown={(e) => {
// 按Delete或Backspace且当前为空时跳回前一个
if ((e.key === 'Backspace' || e.key === 'Delete') && !char && i > 2) {
setTimeout(() => {
const prevInput = document.querySelector(`input[data-index="${i-1}"]`)
if (prevInput) prevInput.focus()
}, 50)
}
}}
style={{ width: '32px', height: '36px', textAlign: 'center', padding: '6px', background: '#0f172a', border: '1px solid #334155', borderRadius: '4px', color: letterColor, fontSize: '14px', boxSizing: 'border-box' }} />
})}
</div>
<div style={{ color: '#64748b', fontSize: '10px', marginTop: '6px' }}>
备注说明X=任意数字 A=非4 B=非47 C=非347 D=非247 E=非2347 F=非23457 G=非123457
</div>
</div>
<div style={{ marginBottom: '12px' }}><label style={{ color: '#94a3b8', fontSize: '12px' }}>正文</label>
<textarea value={seekForm.content || ''} onChange={(e) => setSeekForm({...seekForm, content: e.target.value})} placeholder="请输入详细信息..."
style={{ width: '100%', padding: '8px', marginTop: '6px', background: '#0f172a', border: '1px solid #334155', borderRadius: '6px', color: '#fff', fontSize: '13px', boxSizing: 'border-box', resize: 'vertical', minHeight: '60px' }} />
</div>
<div style={{ marginBottom: '12px' }}><label style={{ color: '#ef4444', fontSize: '12px' }}>联系方式 *</label>
<input type="text" value={seekForm.contact || ''} onChange={(e) => setSeekForm({...seekForm, contact: e.target.value})} placeholder="请输入手机号或微信"
style={{ width: '100%', padding: '8px', marginTop: '6px', background: '#0f172a', border: '1px solid #334155', borderRadius: '6px', color: '#fff', fontSize: '13px', boxSizing: 'border-box' }} />
</div>
<div style={{ marginBottom: '12px' }}><label style={{ color: '#94a3b8', fontSize: '12px' }}>标题(自动生成)</label>
<input type="text" value={seekForm.title} readOnly
style={{ width: '100%', padding: '8px', marginTop: '6px', background: '#0f172a', border: '1px solid #334155', borderRadius: '6px', color: '#fff', fontSize: '13px', boxSizing: 'border-box' }} onChange={(e) => setSeekForm({...seekForm, title: e.target.value})} />
</div>
<div style={{ display: 'flex', gap: '12px' }}>
<button onClick={handleSeekPublish} style={{ flex: 1, padding: '12px', background: '#10b981', border: 'none', borderRadius: '8px', color: '#fff', fontSize: '14px', fontWeight: 'bold', cursor: 'pointer' }}>发布寻号</button>
<button onClick={() => setShowSeekPublish(false)} style={{ flex: 1, padding: '12px', background: 'transparent', border: '1px solid #334155', borderRadius: '8px', color: '#94a3b8', fontSize: '14px', cursor: 'pointer' }}>取消</button>
</div>
</div>
)}
<h3 style={{ color: '#fff', marginBottom: '16px' }}>
{activeTab === 'seek' ? '🔍 寻配号信息' : '💰 成交行情信息'}
{activeTab === 'seek' && (
<div style={{ display: 'flex', gap: '8px', marginLeft: 'auto' }}>
<button onClick={() => setShowSeekPublish(!showSeekPublish)} style={{ padding: '6px 12px', background: showSeekPublish ? '#6b7280' : '#10b981', border: 'none', borderRadius: '6px', color: '#fff', fontSize: '12px', cursor: 'pointer' }}>
'+ 发布寻号'
</button>
<button onClick={() => { setShowMySeeks(true); fetchMySeeks(); }} style={{ padding: '6px 12px', background: '#3b82f6', border: 'none', borderRadius: '6px', color: '#fff', fontSize: '12px', cursor: 'pointer' }}>
我的寻号
</button>
</div>
)}
</h3>
{loading ? (
<div style={{ color: '#94a3b8', textAlign: 'center', padding: '40px' }}>加载中...</div>
) : infoList.length === 0 ? (
<div style={{ color: '#94a3b8', textAlign: 'center', padding: '40px' }}>
暂无{activeTab === 'seek' ? '寻配号' : '成交行情'}信息
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
{infoList.map(item => {
// 解析正文中的号码特征和联系方式
const content = item.content || ''
const featuresMatch = content.match(/号码特征[:\s]*(.+?)(?:\n|$)/)
const contactMatch = content.match(/联系方式[:\s]*(.+?)(?:\n|$)/)
const features = featuresMatch ? featuresMatch[1].trim() : ''
const contact = contactMatch ? contactMatch[1].trim() : ''
// 去除正文中的价格、号码特征、联系方式
const cleanContent = content.replace(/价格[:\s]*.+?(\n|$)/g, '').replace(/号码特征[:\s]*.+?(\n|$)/g, '').replace(/联系方式[:\s]*.+?(\n|$)/g, '').trim()
return (
<div key={item.id} style={{ background: '#1e293b', borderRadius: '12px', padding: '16px' }}>
{/* 标题 */}
<div style={{ color: '#fbbf24', fontSize: '15px', fontWeight: '600', marginBottom: '8px' }}>
{item.title}
</div>
{/* 创建日期 + 用户名 */}
<div style={{ color: '#64748b', fontSize: '12px', marginBottom: '8px' }}>
📅 {formatDate(item.created_at)} &nbsp;|&nbsp; 👤 {item.user_name || '匿名用户'}
</div>
{/* 号码特征 */}
{features && (
<div style={{ marginBottom: '8px' }}>
<span style={{ color: '#94a3b8', fontSize: '13px' }}>号码特征:</span>
<span style={{ fontFamily: 'monospace', fontSize: '14px' }}>
{/* 如果features已包含J0则不再重复添加 */}
{features.startsWith('J0') ? (
features.split('').map((char, i) => {
const isDigit = /[0-9]/.test(char)
const letterColor = isDigit ? '#fff' : ({ X: '#94a3b8', A: '#f472b6', B: '#60a5fa', C: '#34d399', D: '#fbbf24', E: '#a78bfa', F: '#fb923c', G: '#22d3ee' }[char] || '#fff')
return <span key={i} style={{ color: i < 2 ? '#10b981' : letterColor }}>{char}</span>
})
) : (
<>
{'J0'.split('').map((char, i) => (
<span key={i} style={{ color: '#10b981' }}>{char}</span>
))}
{features.split('').map((char, i) => {
const isDigit = /[0-9]/.test(char)
const letterColor = isDigit ? '#fff' : ({ X: '#94a3b8', A: '#f472b6', B: '#60a5fa', C: '#34d399', D: '#fbbf24', E: '#a78bfa', F: '#fb923c', G: '#22d3ee' }[char] || '#fff')
return <span key={i + 2} style={{ color: letterColor }}>{char}</span>
})}
</>
)}
</span>
{/* 动态显示用到的字母说明 */}
{features.match(/[XABCFG]/) && (
<div style={{ color: '#64748b', fontSize: '10px', marginTop: '4px' }}>
备注说明:{(() => {
const used = []
if (features.includes('X')) used.push('X=任意数字')
if (features.includes('A')) used.push('A=非4')
if (features.includes('B')) used.push('B=非47')
if (features.includes('C')) used.push('C=非347')
if (features.includes('D')) used.push('D=非247')
if (features.includes('E')) used.push('E=非2347')
if (features.includes('F')) used.push('F=非23457')
if (features.includes('G')) used.push('G=非123457')
return used.join(' | ')
})()}
</div>
)}
</div>
)}
{/* 配号结果 - 仅寻配号显示 */}
{activeTab === 'seek' && (
<div style={{ marginBottom: '8px' }}>
<span
style={{ color: '#10b981', fontSize: '13px', fontWeight: 'bold', cursor: 'pointer' }}
onClick={() => fetchMatchCollections(item.id)}
>
配号结果: {item.matched_count || 0}条藏品匹配成功
</span>
</div>
)}
{/* 正文 */}
{cleanContent && (
<div style={{ color: '#e2e8f0', fontSize: '14px', lineHeight: '1.6', marginBottom: '8px' }}>
{cleanContent}
</div>
)}
{/* 联系方式 */}
{contact && (
<div style={{ color: '#f97316', fontSize: '13px', padding: '8px', background: 'rgba(249,115,22,0.1)', borderRadius: '6px' }}>
联系方式:{contact}
</div>
)}
</div>
)
})}
</div>
)}
</div>
{/* 匹配藏品列表弹窗 */}
{showMatchList && (
<div style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(0,0,0,0.8)', zIndex: 1000, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<div style={{ background: '#1e293b', borderRadius: '12px', padding: '20px', width: '90%', maxHeight: '80vh', overflow: 'auto' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
<h3 style={{ color: '#fff', margin: 0 }}>匹配藏品清单</h3>
<button onClick={() => setShowMatchList(false)} style={{ background: 'transparent', border: 'none', color: '#94a3b8', fontSize: '20px', cursor: 'pointer' }}>×</button>
</div>
{matchCollections.length === 0 ? (
<div style={{ color: '#94a3b8', textAlign: 'center', padding: '20px' }}>暂无匹配藏品</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
{matchCollections.map(c => (
<div
key={c.id}
style={{ background: '#0f172a', borderRadius: '8px', padding: '12px', cursor: 'pointer' }}
onClick={() => {
setShowMatchList(false)
// 跳转到藏品列表页并定位到该藏品
window.location.hash = `#/list?filter=id&value=${c.id}`
}}
>
<div style={{ color: '#e2e8f0', fontSize: '13px' }}>
<span style={{ color: '#94a3b8' }}>编号: </span>{c.code || c.id.substring(0,8)}
</div>
<div style={{ color: '#e2e8f0', fontSize: '13px', marginTop: '4px' }}>
<span style={{ color: '#94a3b8' }}>冠字号: </span>{c.number}
</div>
<div style={{ color: '#e2e8f0', fontSize: '13px', marginTop: '4px' }}>
<span style={{ color: '#94a3b8' }}>状态: </span>{c.status === 'in_collection' ? '持仓中' : c.status}
</div>
</div>
))}
</div>
)}
</div>
</div>
)}
{/* 我的寻号弹窗 */}
{showMySeeks && (
<div style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(0,0,0,0.8)', zIndex: 1000, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<div style={{ background: '#1e293b', borderRadius: '12px', padding: '20px', width: '90%', maxHeight: '80vh', overflow: 'auto' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
<h3 style={{ color: '#fff', margin: 0 }}>我发布的寻号</h3>
<button onClick={() => setShowMySeeks(false)} style={{ background: 'transparent', border: 'none', color: '#94a3b8', fontSize: '20px', cursor: 'pointer' }}>×</button>
</div>
{mySeekList.length === 0 ? (
<div style={{ color: '#94a3b8', textAlign: 'center', padding: '20px' }}>暂无发布的寻号</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
{mySeekList.map(item => (
<div key={item.id} style={{ background: '#0f172a', borderRadius: '8px', padding: '12px' }}>
{editingSeek && editingSeek.id === item.id ? (
// 编辑模式
<div>
<div style={{ marginBottom: '8px' }}>
<label style={{ color: '#94a3b8', fontSize: '12px' }}>版别</label>
<select
value={editingSeek.edition || '龙钞'}
onChange={(e) => setEditingSeek({...editingSeek, edition: e.target.value})}
style={{ width: '100%', padding: '8px', background: '#1e293b', border: '1px solid #334155', borderRadius: '4px', color: '#fff', marginTop: '4px' }}
>
<option value="龙钞">龙钞</option>
<option value="马钞">马钞</option>
<option value="蛇钞">蛇钞</option>
</select>
</div>
<div style={{ marginBottom: '8px' }}>
<label style={{ color: '#94a3b8', fontSize: '12px' }}>类型</label>
<div style={{ display: 'flex', gap: '8px', marginTop: '4px' }}>
{['标百', '标十', '单张'].map(t => (
<button
key={t}
onClick={() => setEditingSeek({...editingSeek, type: t})}
style={{ flex: 1, padding: '8px', background: editingSeek.type === t ? '#10b981' : '#1e293b', border: '1px solid #334155', borderRadius: '4px', color: '#fff', cursor: 'pointer' }}
>{t}</button>
))}
</div>
</div>
<div style={{ marginBottom: '8px' }}>
<label style={{ color: '#94a3b8', fontSize: '12px' }}>匹配度</label>
<div style={{ display: 'flex', gap: '8px', marginTop: '4px' }}>
<button
onClick={() => setEditingSeek({...editingSeek, matchType: '精准'})}
style={{ flex: 1, padding: '8px', background: editingSeek.matchType === '精准' ? '#10b981' : '#1e293b', border: '1px solid #334155', borderRadius: '4px', color: '#fff', cursor: 'pointer' }}
>精准</button>
<button
onClick={() => setEditingSeek({...editingSeek, matchType: '模糊'})}
style={{ flex: 1, padding: '8px', background: editingSeek.matchType === '模糊' ? '#10b981' : '#1e293b', border: '1px solid #334155', borderRadius: '4px', color: '#fff', cursor: 'pointer' }}
>模糊</button>
</div>
</div>
<div style={{ marginBottom: '8px' }}>
<label style={{ color: '#94a3b8', fontSize: '12px' }}>价格(选填)</label>
<input
value={editingSeek.price || ''}
onChange={(e) => setEditingSeek({...editingSeek, price: e.target.value})}
placeholder="期望价格"
style={{ width: '100%', padding: '8px', background: '#1e293b', border: '1px solid #334155', borderRadius: '4px', color: '#fff', marginTop: '4px' }}
/>
</div>
<div style={{ marginBottom: '8px' }}>
<label style={{ color: '#94a3b8', fontSize: '12px' }}>正文</label>
<textarea
value={editingSeek.content || ''}
onChange={(e) => setEditingSeek({...editingSeek, content: e.target.value})}
style={{ width: '100%', padding: '8px', background: '#1e293b', border: '1px solid #334155', borderRadius: '4px', color: '#fff', minHeight: '60px', marginTop: '4px' }}
/>
</div>
<div style={{ marginBottom: '8px' }}>
<label style={{ color: '#94a3b8', fontSize: '12px' }}>联系方式(必填)</label>
<input
value={editingSeek.contact || ''}
onChange={(e) => setEditingSeek({...editingSeek, contact: e.target.value})}
placeholder="手机号或微信"
style={{ width: '100%', padding: '8px', background: '#1e293b', border: '1px solid #334155', borderRadius: '4px', color: '#fff', marginTop: '4px' }}
/>
</div>
<div style={{ display: 'flex', gap: '8px' }}>
<button onClick={handleUpdateSeek} style={{ flex: 1, padding: '10px', background: '#10b981', border: 'none', borderRadius: '6px', color: '#fff', cursor: 'pointer' }}>保存</button>
<button onClick={() => setEditingSeek(null)} style={{ flex: 1, padding: '10px', background: '#6b7280', border: 'none', borderRadius: '6px', color: '#fff', cursor: 'pointer' }}>取消</button>
</div>
</div>
) : (
// 显示模式
<div>
<div style={{ color: '#fbbf24', fontSize: '14px', fontWeight: '500', marginBottom: '4px' }}>{item.title}</div>
<div style={{ color: '#64748b', fontSize: '12px', marginBottom: '4px' }}>创建于: {new Date(item.created_at).toLocaleDateString()}</div>
<div style={{ color: '#10b981', fontSize: '12px', marginBottom: '8px' }}>配号结果: {item.matched_count || 0}条藏品匹配成功</div>
<button onClick={() => {
// 解析内容提取字段
const content = item.content || ''
const contactMatch = content.match(/联系方式[:\s]*(.+?)$/m)
const priceMatch = content.match(/价格[:\s]*(.+?)$/m)
setEditingSeek({
id: item.id,
title: item.title,
content: content.split('\n联系方式:')[0],
edition: item.expect_category || '龙钞',
type: item.title.match(/标百|标十|单张/)?.[0] || '标百',
matchType: item.title.includes('精准') ? '精准' : '模糊',
price: priceMatch ? priceMatch[1].trim() : '',
contact: contactMatch ? contactMatch[1].trim() : ''
})
}} style={{ padding: '4px 8px', background: '#3b82f6', border: 'none', borderRadius: '4px', color: '#fff', fontSize: '12px', cursor: 'pointer' }}>编辑</button>
</div>
)}
</div>
))}
</div>
)}
</div>
</div>
)}
</div>
)
}

View File

@ -0,0 +1,31 @@
import React, { useState, useEffect } from 'react'
// -
export default function News() {
const [activeTab, setActiveTab] = useState('deal')
const [showSeekPublish, setShowSeekPublish] = useState(false)
const [showMySeeks, setShowMySeeks] = useState(false)
const [showMatchList, setShowMatchList] = useState(false)
const [matchCollections, setMatchCollections] = useState([])
const [listFilter, setListFilter] = useState('')
const [currentUserId, setCurrentUserId] = useState('')
const [matchedItems, setMatchedItems] = useState(() => {
try { return JSON.parse(localStorage.getItem('matchedSeekItems') || '{}') } catch { return {} }
})
const [showCommentId, setShowCommentId] = useState(null)
const [commentText, setCommentText] = useState('')
const [comments, setComments] = useState({})
useEffect(() => {
const token = localStorage.getItem('token')
if (token) { try { const payload = JSON.parse(atob(token.split('.')[1])); setCurrentUserId(payload.sub) } catch {} }
}, [])
const API_BASE = 'http://47.103.9.192:3000'
return (
<div>
<h1>News Page TEST</h1>
</div>
)
}

1
frontend/src/test.js Normal file
View File

@ -0,0 +1 @@
export const TEST_CONST = "test_value_12345";

View File

@ -53,6 +53,5 @@ export default defineConfig({
} }
}, },
// 禁用缓存 // 禁用缓存
manifest: true
} }
}) })