From f84a85d3c1c8013e0dd945514ea3f1c13e86703c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=B2=E8=BE=B0=E7=94=9F=E4=BA=A7?= Date: Fri, 27 Mar 2026 16:39:36 +0800 Subject: [PATCH] =?UTF-8?q?v1.2.18=20=E5=AF=BB=E5=8F=B7=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=9F=BA=E7=A1=80=E7=89=88=EF=BC=9A=E5=AF=BB=E9=85=8D=E5=8F=B7?= =?UTF-8?q?=E5=8F=91=E5=B8=83=E3=80=81=E8=87=AA=E5=8A=A8=E9=85=8D=E5=8F=B7?= =?UTF-8?q?=E5=8C=B9=E9=85=8D=E3=80=81=E6=88=91=E7=9A=84=E5=AF=BB=E5=8F=B7?= =?UTF-8?q?=E5=88=97=E8=A1=A8=E7=BC=96=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- VERSION | 3 +- backend/app/core/auth.py | 16 +- backend/app/routers/collections.py | 5 + backend/app/routers/information.py | 203 +++++++++++++-- frontend/index.html | 2 +- frontend/src/pages/List.jsx | 23 +- frontend/src/pages/News.jsx | 393 ++++++++++++++++++++++++++--- 7 files changed, 567 insertions(+), 78 deletions(-) diff --git a/VERSION b/VERSION index f9d405b..efe97d8 100644 --- a/VERSION +++ b/VERSION @@ -1 +1,2 @@ -VERSION=1.2.14 +VERSION=1.2.18 +# 2026-03-27 寻号功能基础版:寻配号发布、自动配号匹配、我的寻号列表编辑 diff --git a/backend/app/core/auth.py b/backend/app/core/auth.py index 360b5bf..602723a 100644 --- a/backend/app/core/auth.py +++ b/backend/app/core/auth.py @@ -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 diff --git a/backend/app/routers/collections.py b/backend/app/routers/collections.py index fd741e0..bf48d73 100644 --- a/backend/app/routers/collections.py +++ b/backend/app/routers/collections.py @@ -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: diff --git a/backend/app/routers/information.py b/backend/app/routers/information.py index 1b56533..ef65024 100644 --- a/backend/app/routers/information.py +++ b/backend/app/routers/information.py @@ -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( diff --git a/frontend/index.html b/frontend/index.html index 7cebb1a..9190919 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,7 +4,7 @@ - 甲辰收藏 v1.2.17 + 甲辰收藏 v1.2.18 diff --git a/frontend/src/pages/List.jsx b/frontend/src/pages/List.jsx index 0078943..fa4fed1 100644 --- a/frontend/src/pages/List.jsx +++ b/frontend/src/pages/List.jsx @@ -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 }) => ( -
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 }) => ( +
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行:编号 + 冠字号 + 包装(蓝色) | 状态 + 持仓类型 + 珍惜度(紫色) */}
@@ -583,7 +592,7 @@ export default function List() { ) : viewMode === 'grid' ? (
{filteredCollections.map(item => ( -
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' }}> +
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' }}>
🐉
{item.code || '-'}
{formatPrefixSerial(item.prefixSerial)}
@@ -595,7 +604,7 @@ export default function List() { ))}
) : ( - filteredCollections.map(item => ) + filteredCollections.map(item => ) )}
diff --git a/frontend/src/pages/News.jsx b/frontend/src/pages/News.jsx index f49ea06..231066a 100644 --- a/frontend/src/pages/News.jsx +++ b/frontend/src/pages/News.jsx @@ -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() {
{[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 { 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 { 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' }} /> })}
-
X=任意数字 A=非4 B=非47 C=非347 D=非247 E=非2347 F=非12347 F=非23457 G=非123457
+
+ 备注说明:X=任意数字 A=非4 B=非47 C=非347 D=非247 E=非2347 F=非23457 G=非123457 +