v1.2.18 寻号功能基础版:寻配号发布、自动配号匹配、我的寻号列表编辑

This commit is contained in:
甲辰生产 2026-03-27 16:39:36 +08:00
parent 18b6cfbc1d
commit f84a85d3c1
7 changed files with 567 additions and 78 deletions

View File

@ -1 +1,2 @@
VERSION=1.2.14
VERSION=1.2.18
# 2026-03-27 寻号功能基础版:寻配号发布、自动配号匹配、我的寻号列表编辑

View File

@ -57,14 +57,10 @@ def decode_access_token(token: str) -> Optional[dict]:
def get_current_user(
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
db: Session = Depends(lambda: SessionLocal())
) -> User:
"""获取当前用户"""
) -> Optional[User]:
"""获取当前用户可返回None"""
if not credentials:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="未提供认证信息",
headers={"WWW-Authenticate": "Bearer"},
)
return None
token = credentials.credentials
payload = decode_access_token(token)
@ -86,10 +82,6 @@ def get_current_user(
user = db.query(User).filter(User.f99_90_id == user_id).first()
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="用户不存在",
headers={"WWW-Authenticate": "Bearer"},
)
return None
return user

View File

@ -105,6 +105,7 @@ def get_next_code(
@router.get("")
def get_collections(
id: str = Query(None, description="filter by collection id"),
category: Optional[str] = None,
status: Optional[str] = None,
search: Optional[str] = None,
@ -143,6 +144,10 @@ def get_collections(
if user_id:
query = query.filter(Collection.f99_91_user_id == user_id)
# 按ID精确筛选
if id:
query = query.filter(Collection.f99_90_id == id)
if category:
query = query.filter(Collection.f01_03_category == category)
if status:

View File

@ -69,6 +69,8 @@ class InformationResponse(BaseModel):
collection_category: Optional[str] = None
collection_version: Optional[str] = None
collection_number: Optional[str] = None
# 匹配数量(我的藏品中满足条件的数量)
matched_count: Optional[int] = 0
class Config:
from_attributes = True
@ -81,9 +83,10 @@ def get_information_list(
status: str = Query("active", description="状态: active/closed/expired"),
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""获取资讯列表"""
"""获取资讯列表(公开,无需登录)"""
query = db.query(Information).options(
joinedload(Information.user),
joinedload(Information.collection)
@ -102,6 +105,11 @@ def get_information_list(
# 转换结果
result = []
for item in items:
# 计算匹配数量仅对seek类型且用户登录时
matched_count = 0
if item.info_type == 'seek' and item.expect_number and current_user:
matched_count = match_collections_count(db, current_user.f99_90_id, item.expect_number)
result.append(InformationResponse(
id=item.id,
user_id=item.user_id,
@ -127,11 +135,98 @@ def get_information_list(
collection_category=item.collection.f01_03_category if item.collection else None,
collection_version=item.collection.f02_11_version if item.collection else None,
collection_number=item.collection.f02_10_prefix_serial if item.collection else None,
matched_count=matched_count,
))
return result
def match_collections_count(db: Session, user_id: str, expect_number: str) -> int:
"""根据号码特征计算匹配藏品数量"""
if not expect_number or len(expect_number) != 10:
return 0
# 固定前缀
if not expect_number.startswith('J0'):
return 0
pattern = expect_number[2:] # 后8位
if not pattern:
return 0
# 获取用户所有藏品
collections = db.query(Collection).filter(
Collection.f99_91_user_id == user_id,
Collection.f01_04_status == "in_collection"
).all()
count = 0
for c in collections:
number = c.f02_10_prefix_serial or ''
# 去掉J0前缀后取前8位
if len(number) >= 10 and number.startswith('J0'):
col_pattern = number[2:10]
if match_pattern(col_pattern, pattern):
count += 1
elif len(number) >= 8:
col_pattern = number[:8]
if match_pattern(col_pattern, pattern):
count += 1
return count
def match_pattern(col_number: str, pattern: str) -> bool:
"""匹配号码特征模式"""
# X = 任意数字
# A = 非4
# B = 非47
# C = 非347
# D = 非247
# E = 非2347
# F = 非23457
# G = 非123457
col_num = col_number[2:] if col_number.startswith('J0') else col_number # 去掉J0前缀
for i, p in enumerate(pattern):
if i >= len(col_num):
return False
c = col_num[i]
if p == 'X':
if not c.isdigit():
return False
elif p == 'A':
if c == '4':
return False
elif p == 'B':
if c in '47':
return False
elif p == 'C':
if c in '347':
return False
elif p == 'D':
if c in '247':
return False
elif p == 'E':
if c in '2347':
return False
elif p == 'F':
if c in '23457':
return False
elif p == 'G':
if c in '123457':
return False
else:
# 数字或字母必须完全匹配
if p != c:
return False
return True
# 获取单条资讯
@router.get("/{info_id}", response_model=InformationResponse)
def get_information(
@ -333,10 +428,13 @@ def delete_information(
@router.get("/seek/match")
def get_seek_match(
info_id: str,
current_user: User = Depends(get_current_user),
current_user: Optional[User] = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""获取符合条件的我的藏品推荐"""
if not current_user:
raise HTTPException(status_code=401, detail="请先登录")
info = db.query(Information).filter(
Information.id == info_id,
Information.info_type == "seek"
@ -345,45 +443,108 @@ def get_seek_match(
if not info:
raise HTTPException(status_code=404, detail="寻配号信息不存在")
# 查询当前用户的藏品,匹配条件
query = db.query(Collection).filter(
# 获取用户所有藏品
collections = db.query(Collection).filter(
Collection.f99_91_user_id == current_user.f99_90_id,
Collection.f01_04_status == "in_collection"
)
).all()
if info.expect_category:
query = query.filter(Collection.f01_03_category == info.expect_category)
if info.expect_version:
query = query.filter(Collection.f02_11_version == info.expect_version)
if info.expect_packaging:
query = query.filter(Collection.f02_12_packaging == info.expect_packaging)
if info.expect_number:
query = query.filter(Collection.f02_10_prefix_serial.contains(info.expect_number))
if info.expect_price_min:
query = query.filter(Collection.f05_40_cost_price >= info.expect_price_min)
if info.expect_price_max:
query = query.filter(Collection.f05_40_cost_price <= info.expect_price_max)
# 去掉版别筛选,因为藏品分类和发布需求的版别不同
# if info.expect_category:
# collections = [c for c in collections if c.f01_03_category == info.expect_category]
matched_collections = query.all()
# 按号码特征模式匹配
matched = []
if info.expect_number and len(info.expect_number) == 10:
pattern = info.expect_number[2:] # 后8位
for c in collections:
number = c.f02_10_prefix_serial or ''
# 去掉J0前缀后取前8位
if len(number) >= 10 and number.startswith('J0'):
col_pattern = number[2:10] # 取J0后面的8位
if match_pattern(col_pattern, pattern):
matched.append(c)
elif len(number) >= 8:
col_pattern = number[:8] # 取前8位
if match_pattern(col_pattern, pattern):
matched.append(c)
else:
matched = collections
return {
"info_id": info_id,
"matched_count": len(matched_collections),
"matched_count": len(matched),
"collections": [
{
"id": c.f99_90_id,
"code": c.f01_02_code or '',
"name": c.f01_01_name,
"number": c.f02_10_prefix_serial,
"status": c.f01_04_status,
"category": c.f01_03_category,
"version": c.f02_11_version,
"packaging": c.f02_12_packaging,
"number": c.f02_10_prefix_serial,
"cost_price": c.f05_40_cost_price,
}
for c in matched_collections
for c in matched
]
}
# 我的寻号列表
@router.get("/my-seeks")
def get_my_seeks(
current_user: Optional[User] = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""获取当前用户发布的所有寻号信息"""
if not current_user:
raise HTTPException(status_code=401, detail="请先登录")
items = db.query(Information).filter(
Information.user_id == current_user.f99_90_id,
Information.info_type == "seek",
Information.status == "active"
).order_by(Information.created_at.desc()).all()
result = []
for item in items:
# 计算匹配数量
matched_count = 0
if item.expect_number:
matched_count = match_collections_count(db, current_user.f99_90_id, item.expect_number)
result.append(InformationResponse(
id=item.id,
user_id=item.user_id,
info_type=item.info_type,
title=item.title,
content=item.content,
collection_id=item.collection_id,
expect_category=item.expect_category,
expect_version=item.expect_version,
expect_packaging=item.expect_packaging,
expect_number=item.expect_number,
expect_price_min=item.expect_price_min,
expect_price_max=item.expect_price_max,
deal_price=item.deal_price,
deal_date=item.deal_date,
status=item.status,
view_count=item.view_count,
contact_count=item.contact_count,
created_at=item.created_at,
user_name=item.user.f01_01_name if item.user else None,
user_avatar=item.user.avatar if item.user else None,
collection_name=item.collection.f01_01_name if item.collection else None,
collection_category=item.collection.f01_03_category if item.collection else None,
collection_version=item.collection.f02_11_version if item.collection else None,
collection_number=item.collection.f02_10_prefix_serial if item.collection else None,
matched_count=matched_count,
))
return result
# 成交数据统计
@router.get("/deal/stats")
def get_deal_stats(

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8">
<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">
<title>甲辰收藏 v1.2.17</title>
<title>甲辰收藏 v1.2.18</title>
<!-- Favicon -->
<link rel="icon" type="image/x-icon" href="/static/icons/favicon.ico" />

View File

@ -3,12 +3,13 @@ import { APP_VERSION } from '../config/version'
export default function List() {
const [collections, setCollections] = useState([])
const [highlightId, setHighlightId] = useState('')
const [loading, setLoading] = useState(true)
const [filter, setFilter] = useState('')
const [filterType, setFilterType] = useState('')
const urlParams = new URLSearchParams(window.location.hash.split('?')[1] || '')
const urlUserId = urlParams.get('userId') || ''
const [userIdFilter, setUserIdFilter] = useState(urlUserId)
const urlId = urlParams.get('id') || ''
const [userIdFilter, setUserIdFilter] = useState(urlParams.get('userId') || '')
const [sortField, setSortField] = useState('createdAt')
const [sortOrder, setSortOrder] = useState('desc')
const [viewMode, setViewMode] = useState('list')
@ -31,6 +32,9 @@ export default function List() {
// hash
const handleHashChange = () => {
const params = new URLSearchParams(window.location.hash.split('?')[1] || '')
// id -
const idParam = params.get('id') || ''
setHighlightId(idParam)
// filter=category&value= filter=category=
let filterTypeParam = params.get('filter') || ''
let valueParam = params.get('value') || ''
@ -67,11 +71,15 @@ export default function List() {
try {
// URL
const params = new URLSearchParams(window.location.hash.split('?')[1] || '')
const urlId = params.get('id') || ''
const urlFilterType = params.get('filter') || ''
const urlFilterValue = params.get('value') ? decodeURIComponent(params.get('value')) : ''
let api = '/api/collections?page=' + page + '&limit=100&sortBy=' + sortField + '&sortOrder=' + sortOrder
if (urlFilterType && urlFilterValue) {
// idid
if (urlId) {
api += '&id=' + encodeURIComponent(urlId)
} else if (urlFilterType && urlFilterValue) {
api += '&' + urlFilterType + '=' + encodeURIComponent(urlFilterValue)
}
const res = await fetch(api, {
@ -101,6 +109,7 @@ export default function List() {
if (filterType && filter) {
const fieldMap = {
id: 'id',
status: 'status',
category: 'category',
packaging: 'packaging',
@ -406,8 +415,8 @@ export default function List() {
return { bg: 'linear-gradient(135deg, #fbbf24 0%, #f59e0b 100%)', color: '#1e293b' }
}
const ListItem = ({ item }) => (
<div onClick={() => goDetail(item.id)} style={{ background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.05)', borderRadius: '12px', padding: '10px', marginBottom: '8px', cursor: 'pointer' }}>
const ListItem = ({ item, highlight }) => (
<div onClick={() => goDetail(item.id)} style={{ background: 'rgba(255,255,255,0.03)', border: highlight ? '2px solid #10b981' : '1px solid rgba(255,255,255,0.05)', borderRadius: '12px', padding: '10px', marginBottom: '8px', cursor: 'pointer', boxShadow: highlight ? '0 0 10px rgba(16,185,129,0.3)' : 'none' }}>
{/* 第1行编号 + 冠字号 + 包装(蓝色) | 状态 + 持仓类型 + 珍惜度(紫色) */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '4px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '6px', flexWrap: 'wrap' }}>
@ -583,7 +592,7 @@ export default function List() {
) : viewMode === 'grid' ? (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: '12px' }}>
{filteredCollections.map(item => (
<div key={item.id} onClick={() => goDetail(item.id)} style={{ background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.05)', borderRadius: '12px', padding: '12px', cursor: 'pointer' }}>
<div key={item.id} onClick={() => goDetail(item.id)} style={{ background: 'rgba(255,255,255,0.03)', border: item.id === highlightId ? '2px solid #10b981' : '1px solid rgba(255,255,255,0.05)', borderRadius: '12px', padding: '12px', cursor: 'pointer', boxShadow: item.id === highlightId ? '0 0 10px rgba(16,185,129,0.3)' : 'none' }}>
<div style={{ height: '80px', background: 'rgba(255,255,255,0.03)', borderRadius: '8px', display: 'flex', alignItems: 'center', justifyContent: 'flex-start', fontSize: '32px', marginBottom: '10px' }}>🐉</div>
<div style={{ color: '#fff', fontSize: '11px', fontWeight: 'normal' }}>{item.code || '-'}</div>
<div style={{ color: '#fbbf24', fontSize: '11px', fontFamily: 'monospace', marginTop: '2px' }}>{formatPrefixSerial(item.prefixSerial)}</div>
@ -595,7 +604,7 @@ export default function List() {
))}
</div>
) : (
filteredCollections.map(item => <ListItem key={item.id} item={item} />)
filteredCollections.map(item => <ListItem key={item.id} item={item} highlight={item.id === highlightId} />)
)}
</div>
</div>

View File

@ -4,8 +4,11 @@ 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: 'J0', content: '', title: '', contact: ''
edition: '龙钞', type: '', category: '寻号', price: '', matchType: '模糊', features: '', content: '', title: '', contact: ''
})
const [infoList, setInfoList] = useState([])
const [loading, setLoading] = useState(false)
@ -19,20 +22,22 @@ export default function News() {
//
useEffect(() => {
const title = `「寻号 ${seekForm.edition || '龙钞,马钞,蛇钞'} ${seekForm.type} ${seekForm.features || 'J0'} ${seekForm.matchType}`
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'
const res = await fetch(`${API_BASE}/api/information/list?info_type=${type}`, {
headers: { Authorization: `Bearer ${token}` }
})
// 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)
@ -40,20 +45,88 @@ export default function News() {
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号码特征: ' + seekForm.features : '') + '\n联系方式: ' + seekForm.contact
const content = (seekForm.content ? seekForm.content + '\n' : '') + (seekForm.price ? `价格: ${seekForm.price}` : '') + (seekForm.features ? '\n号码特征: J0' + seekForm.features : '') + '\n联系方式: ' + seekForm.contact
// editioncategory
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' })
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: 'J0', content: '', title: '', contact: '' })
setSeekForm({ edition: '龙钞', type: '', category: '寻号', price: '', matchType: '模糊', features: '', content: '', title: '', contact: '' })
fetchInfoList()
} else { alert(data.message || '发布失败') }
} catch (e) { alert('发布失败: ' + e.message) }
@ -150,28 +223,42 @@ export default function News() {
<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 placeholder = isFixed ? (i === 0 ? 'J' : '0') : 'X'
return <input key={i} ref={el => { if (el) el.dataset.index = i }} type="text" maxLength={1}
value={isFixed ? placeholder : (seekForm.features?.[i] || '')}
readOnly={isFixed}
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 arr = (seekForm.features || 'J0XXXXXXXX').split('')
arr[i] = e.target.value.toUpperCase()
setSeekForm({...seekForm, features: arr.join('')})
//
if (e.target.value && i < 9) {
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()
}, 0)
}, 50)
}
}}
placeholder={placeholder}
style={{ width: '32px', height: '36px', textAlign: 'center', padding: '6px', background: '#0f172a', border: '1px solid #334155', borderRadius: '4px', color: isFixed ? '#10b981' : '#fff', fontSize: '14px', boxSizing: 'border-box' }} />
}}
onKeyDown={(e) => {
// DeleteBackspace
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=非12347 F=非23457 G=非123457</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="请输入详细信息..."
@ -198,6 +285,9 @@ export default function News() {
<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>
@ -210,24 +300,255 @@ export default function News() {
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
{infoList.map(item => (
<div key={item.id} style={{ background: '#1e293b', borderRadius: '12px', padding: '16px' }}>
<div style={{ color: '#fbbf24', fontSize: '15px', fontWeight: '500', marginBottom: '8px' }}>
{item.title}
</div>
<div style={{ color: '#94a3b8', fontSize: '12px', marginBottom: '8px' }}>
{formatDate(item.created_at)} | {item.info_source || '系统'}
</div>
{item.content && (
<div style={{ color: '#e2e8f0', fontSize: '14px', lineHeight: '1.6' }}>
{item.content}
{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>
))}
{/* 创建日期 + 用户名 */}
<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>
)
}