diff --git a/backend/app/routers/information.py b/backend/app/routers/information.py index cb7ae31..5ccec6f 100644 --- a/backend/app/routers/information.py +++ b/backend/app/routers/information.py @@ -859,7 +859,7 @@ def get_publisher_info( raise HTTPException(status_code=404, detail="寻配号信息不存在") # 只有匹配者可以看到发布者信息 - if info.matched_user_id != current_user.f99_90_id: + if not info.matched_user_id or info.matched_user_id != current_user.f99_90_id: raise HTTPException(status_code=403, detail="无权访问") # 获取发布者信息 diff --git a/frontend/index.html b/frontend/index.html index 7cebb1a..dafb732 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,7 +4,7 @@ - 甲辰收藏 v1.2.17 + 甲辰收藏 v1.2.19 diff --git a/frontend/src/pages/News.jsx b/frontend/src/pages/News.jsx index 353c23b..a5ff477 100644 --- a/frontend/src/pages/News.jsx +++ b/frontend/src/pages/News.jsx @@ -13,7 +13,7 @@ export default function News() { const [showMatchList, setShowMatchList] = useState(false) const [matchCollections, setMatchCollections] = useState([]) const [seekForm, setSeekForm] = useState({ - edition: '龙钞', type: '', category: '寻号', price: '', matchType: '模糊', features: '', content: '', title: '', contact: '' + edition: '龙钞', type: '', category: '寻号', price: '', matchType: '模糊', features: '', content: '', title: '', contact: (() => { try { const u = JSON.parse(localStorage.getItem('user') || '{}'); return u.phone || '' } catch { return '' } })() || '' }) const [infoList, setInfoList] = useState([]) const [viewMode, setViewMode] = useState('all') @@ -227,12 +227,14 @@ export default function News() { } catch (e) { alert('更新失败') } } - const handleSeekPublish = async () => { - if (!seekForm.contact) { alert('请填写联系方式'); return } + const handleSeekPublish = async () => { console.log('handleSeekPublish called') + const phone = (() => { try { const u = JSON.parse(localStorage.getItem('user') || '{}'); return u.phone || '' } catch { return '' } })() + const finalContact = seekForm.contact || phone + if (!finalContact) { 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 + const content = (seekForm.content ? seekForm.content + '\n' : '') + (seekForm.price ? `价格: ${seekForm.price}` : '') + (seekForm.features ? '\n号码特征: J0' + seekForm.features : '') + '\n联系方式: ' + finalContact // 从edition映射到category const categoryMap = { '龙钞': '龙钞', '马钞': '马钞', '蛇钞': '蛇钞' } const res = await fetch(`${API_BASE}/api/information/`, { @@ -249,7 +251,7 @@ export default function News() { if (data.id || data.code === 0) { alert('发布成功!') setShowSeekPublish(false) - setSeekForm({ edition: '龙钞', type: '', category: '寻号', price: '', matchType: '模糊', features: '', content: '', title: '', contact: '' }) + setSeekForm({ edition: '龙钞', type: '', category: '寻号', price: '', matchType: '模糊', features: '', content: '', title: '', contact: (() => { try { const u = JSON.parse(localStorage.getItem('user') || '{}'); return u.phone || '' } catch { return '' } })() || '' }) fetchInfoList() } else { alert(data.message || '发布失败') } } catch (e) { alert('发布失败: ' + e.message) } diff --git a/frontend/src/pages/News.jsx.bak b/frontend/src/pages/News.jsx.bak new file mode 100644 index 0000000..95e4006 --- /dev/null +++ b/frontend/src/pages/News.jsx.bak @@ -0,0 +1,827 @@ +import React, { useState, useEffect } from 'react' +// 本地获取用户手机号 +const getUserPhone = () => { + try { + const user = JSON.parse(localStorage.getItem('user') || '{}') + console.log('getUserPhone user:', user) + // Try multiple possible field names + return user.phone || user.phoneNumber || user.mobile || user.tel || '' + } catch { return '' } +} + +// 资讯页面 - 展示寻配号和行情信息(所有人可见) +export default function News() { + const [activeTab, setActiveTab] = useState(localStorage.getItem('news_activeTab') || 'seek') + const [showSeekPublish, setShowSeekPublish] = 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 [matchCollections, setMatchCollections] = useState([]) + const [customModal, setCustomModal] = useState({show: false, title: '', content: ''}) + const [seekForm, setSeekForm] = useState({ + edition: '龙钞', type: '', category: '寻号', price: '', matchType: '模糊', features: '', content: '', title: '', contact: getUserPhone() || '' + }) + const [infoList, setInfoList] = useState([]) + const [viewMode, setViewMode] = useState('all') + const [loading, setLoading] = useState(false) + + const API_BASE = 'http://47.103.9.192:3000' + const currentUser = localStorage.getItem('token') ? { id: 'logged_in' } : null + + // 获取资讯列表 + useEffect(() => { + fetchInfoList() + }, [activeTab]) + + // 自动生成寻号标题 + useEffect(() => { + const title = `「寻号 ${seekForm.edition || '龙钞,马钞,蛇钞'} J0${seekForm.features || 'XXXXXXXX'}」` + setSeekForm(prev => ({...prev, title})) + }, [seekForm.edition, seekForm.features]) + + 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 fetchComments = async (infoId) => { + // 每次都重新获取评论 + 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 === '留言成功' || data.message === '评论成功') { + setCommentText('') + // 强制刷新评论列表,先清空缓存 + setComments(prev => { + const newComments = {...prev} + delete newComments[infoId] + return newComments + }) + // 然后重新获取 + setTimeout(() => fetchComments(infoId), 100) + // 确保停留在寻号 tab + if (activeTab !== 'seek') { + setActiveTab('seek') + } + alert('留言成功!') + } else { + alert(data.detail || '留言失败') + } + } catch (e) { + console.error('留言失败:', e) + alert('留言失败,请重试') + } + } + + // 匹配并联系藏友 + 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) // 显示联系方式 + setCustomModal({show: true, title: '匹配成功', content: '已通知发布者,请等待对方联系'}) + fetchInfoList() // 刷新列表 + } + } catch (e) { + alert('操作失败: ' + e.message) + } + } + const fetchMatchedUserInfo = async (infoId) => { + console.log('fetchMatchedUserInfo called with:', infoId) + let token = localStorage.getItem('token') + if (!token) { + // Try to refresh token + const username = localStorage.getItem('username') + const password = localStorage.getItem('password') + if (username && password) { + const form = new FormData() + form.append('username', username) + form.append('password', password) + const loginRes = await fetch(`${API_BASE}/api/auth/login`, { method: 'POST', body: form }) + const loginData = await loginRes.json() + if (loginData.access_token) { + token = loginData.access_token + localStorage.setItem('token', token) + } + } + if (!token) { alert('请先登录'); return } + } + try { + const res = await fetch(`${API_BASE}/api/information/seek/matched-user/${infoId}`, { headers: { Authorization: `Bearer ${token}` } }) + console.log('Matched user response:', res.status) + const data = await res.json() + console.log('Matched user data:', data) + if (data.user_name) setCustomModal({show: true, title: '匹配者信息', content: `用户名: ${data.user_name}\n联系方式: ${data.matched_contact || '未提供'}`}) + else if (data.detail) alert(data.detail) + } catch (e) { console.error(e) } + } + const fetchPublisherInfo = async (infoId) => { + console.log('fetchPublisherInfo called with:', infoId) + const token = localStorage.getItem('token') + if (!token) { alert('请先登录'); return } + try { + const res = await fetch(`${API_BASE}/api/information/seek/publisher/${infoId}`, { headers: { Authorization: `Bearer ${token}` } }) + console.log('Publisher response:', res.status) + const data = await res.json() + console.log('Publisher data:', data) + if (data.user_name) setCustomModal({show: true, title: '发布者信息', content: `用户名: ${data.user_name}\n联系方式: ${data.contact || '未提供'}`}) + else if (data.detail) alert(data.detail) + } catch (e) { console.error(e) } + } + 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) + } + } + + // 获取当前用户ID + const getUserId = () => { + try { + const user = JSON.parse(localStorage.getItem('user') || '{}') + return user.id || user.user_id || user.f99_90_id || null + } catch { return null } + } + const currentUserId = getUserId() + + // 获取我的寻号列表 + const [mySeekList, setMySeekList] = useState([]) + const [myMatchedList, setMyMatchedList] = 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' && item.user_id === currentUserId) : [] + setMySeekList(seeks) + setInfoList(seeks) // 在主列表显示 + } catch (e) { console.error(e) } + } + + // 获取我已匹配的寻号列表 + const fetchMyMatches = async () => { + const token = localStorage.getItem('token') + if (!token) { alert('请先登录'); return } + try { + const res = await fetch(`${API_BASE}/api/information/list?info_type=seek`, { + headers: { Authorization: `Bearer ${token}` } + }) + const data = await res.json() + // 筛选我已匹配的(matched_user_id等于当前用户ID) + const matched = data.filter(item => item.is_matched === 'matched' && item.matched_user_id === currentUserId) + setMyMatchedList(matched) + setInfoList(matched) // 在主列表显示 + } 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 () => { + const phone = getUserPhone() + const finalContact = seekForm.contact || phone || '' + if (!finalContact) { alert('请填写联系方式'); return } + const content = (seekForm.content ? seekForm.content + '\n' : '') + (seekForm.price ? `价格: ${seekForm.price}` : '') + (seekForm.features ? '\n号码特征: J0' + seekForm.features : '') + '\n联系方式: ' + finalContact + try { + const token = localStorage.getItem('token') + // 从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: getUserPhone() || '' }) + 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 ( +
+ {/* Tab导航 */} +
+ {tabs.map(tab => ( +
{ setActiveTab(tab.key); localStorage.setItem('news_activeTab', tab.key); }} + style={{ + padding: '10px 20px', + borderRadius: '8px', + background: activeTab === tab.key ? '#3b82f6' : 'transparent', + color: '#fff', + cursor: 'pointer', + whiteSpace: 'nowrap', + fontSize: '14px' + }} + > + {tab.label} +
+ ))} +
+ + {/* 资讯列表 */} +
+ {showSeekPublish && activeTab === 'seek' && ( +
+
+
+ {['龙钞','马钞','蛇钞'].map(item => { + const isSelected = seekForm.edition ? seekForm.edition.split(',').includes(item) : false + return ( + + ) + })} +
+
+
+ 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' }} /> +
+
+
+ {[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 { 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' }} /> + })} +
+
+ 备注说明:X=任意数字 A=非4 B=非47 C=非347 D=非247 E=非2347 F=非23457 G=非123457 +
+
+
+ setSeekForm({...seekForm, title: e.target.value})} /> +
+
+