v1.2.18 寻号功能基础版:寻配号发布、自动配号匹配、我的寻号列表编辑
This commit is contained in:
parent
18b6cfbc1d
commit
f84a85d3c1
3
VERSION
3
VERSION
|
|
@ -1 +1,2 @@
|
|||
VERSION=1.2.14
|
||||
VERSION=1.2.18
|
||||
# 2026-03-27 寻号功能基础版:寻配号发布、自动配号匹配、我的寻号列表编辑
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
// 如果有id参数,直接用id筛选(后端精确匹配)
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// 从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' })
|
||||
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) => {
|
||||
// 按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=非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)} | 👤 {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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue