diff --git a/backend/app/routers/deal.py b/backend/app/routers/deal.py
index 4c71526..8f17a62 100644
--- a/backend/app/routers/deal.py
+++ b/backend/app/routers/deal.py
@@ -10,7 +10,7 @@ from sqlalchemy.orm import Session
# 更新:
from pydantic import BaseModel
# 更新:
-from typing import Optional
+from typing import Optional, List
# 更新:
from datetime import datetime, date
# 更新:
@@ -176,7 +176,7 @@ def generate_deal_no(db: Session):
# 更新:
# ============ API ============
# 更新:
-@router.get("/list", response_model=list[DealInfoResponse])
+@router.get("/list", response_model=List[DealInfoResponse])
# 更新:
def get_deal_list(
# 更新:
diff --git a/backend/app/routers/purchase.py b/backend/app/routers/purchase.py
index eca800b..286b787 100644
--- a/backend/app/routers/purchase.py
+++ b/backend/app/routers/purchase.py
@@ -1,5 +1,5 @@
# purchase - 认购群API路由
-# Version: 0.0.5 (2026-04-29)
+# Version: 0.2.0 (2026-05-01)
# 认购群、成员、藏品管理
from fastapi import APIRouter, Depends, HTTPException, Query
@@ -88,6 +88,11 @@ class MemberResponse(BaseModel):
role: str
status: str
joined_at: Optional[datetime]
+ wechat_name: Optional[str] = None
+ contact: Optional[str] = None
+ purchase_count: Optional[int] = 0
+ paid_amount: Optional[float] = 0
+ notes: Optional[str] = None
# ============ 工具函数 ============
def generate_group_code(db: Session) -> str:
@@ -127,7 +132,7 @@ def get_my_groups(
current_user = Depends(get_current_user),
db: Session = Depends(get_db)
):
- """获取我参与的认购群"""
+ """获取我参与的认购群(仅已批准的)"""
if not current_user:
return []
@@ -296,14 +301,14 @@ def join_group(
if existing:
if existing.status == "approved":
raise HTTPException(status_code=400, detail="您已加入该认购群")
- elif existing.status == "pending":
- raise HTTPException(status_code=400, detail="您已申请加入,等待审批")
- elif existing.status == "rejected":
+ else:
+ # pending/rejected/其他状态:重新激活为pending
existing.status = "pending"
existing.joined_at = datetime.now()
+ existing.user_name = current_user.f01_01_name
group.total_pending += 1
db.commit()
- return {"message": "已重新申请,等待审批"}
+ return {"message": "申请成功,等待审批"}
member = PurchaseMember(
group_id=group_id,
@@ -339,7 +344,12 @@ def get_group_members(
"user_avatar": m.user_avatar,
"role": m.role,
"status": m.status,
- "joined_at": m.joined_at
+ "joined_at": m.joined_at,
+ "wechat_name": m.wechat_name,
+ "contact": m.contact,
+ "purchase_count": m.purchase_count or 0,
+ "paid_amount": float(m.paid_amount or 0),
+ "notes": m.notes
}
for m in members
]
@@ -552,6 +562,7 @@ def delete_group(
return {"message": "认购群已删除"}
class MemberUpdate(BaseModel):
+ user_name: Optional[str] = None
wechat_name: Optional[str] = None
contact: Optional[str] = None
purchase_count: Optional[int] = None
@@ -593,6 +604,8 @@ def update_member(
if member.user_id != current_user.f99_90_id and group.creator_id != current_user.f99_90_id:
raise HTTPException(status_code=403, detail="无权修改")
+ if data.user_name is not None:
+ member.user_name = data.user_name
if data.wechat_name is not None:
member.wechat_name = data.wechat_name
if data.contact is not None:
diff --git a/backend/app/routers/seek.py b/backend/app/routers/seek.py
index 95db0c5..1ea4731 100644
--- a/backend/app/routers/seek.py
+++ b/backend/app/routers/seek.py
@@ -5,15 +5,14 @@
from fastapi import APIRouter, Depends, Query, HTTPException
from sqlalchemy.orm import Session
from pydantic import BaseModel
-from typing import Optional
+from typing import Optional, List
from datetime import datetime
from app.core.database import get_db
from app.core.auth import get_current_user
from app.models.seek_info import SeekInfo
from app.models.models import User, Collection
-from app.routers.information import match_collections_count, match_collections_count_from_coolbot
+from app.routers.information import match_collections_count, match_collections_count_from_coolbot, match_pattern
from app.utils.coolbot_matcher import (
- match_pattern,
match_self_collections_count,
match_collections_count_from_coolbot,
match_collections_list_from_coolbot
@@ -75,7 +74,7 @@ class MatchConfirmRequest(BaseModel):
collection_id: Optional[str] = None
# ============ API ============
-@router.get("/list", response_model=list[SeekInfoResponse])
+@router.get("/list", response_model=List[SeekInfoResponse])
def get_seek_list(
status: str = Query("active"),
page: int = Query(1, ge=1),
diff --git a/backend/app/utils/coolbot_matcher.py b/backend/app/utils/coolbot_matcher.py
index c435b76..d9a6087 100644
--- a/backend/app/utils/coolbot_matcher.py
+++ b/backend/app/utils/coolbot_matcher.py
@@ -1,89 +1,194 @@
# coolbot_matcher - 一尘数据库号码匹配工具
-# Version: 0.0.1 (2026-04-24)
+# Version: 0.0.2 (2026-04-30)
# 描述:从一尘数据库(coolbot_data)查询匹配的藏品号码
+# 更新:号码分类规则修正
-from typing import List
+from typing import List, Optional
from sqlalchemy import text
from sqlalchemy.orm import Session
from app.core.coolbot_db import coolbot_engine
-def match_pattern(col_number: str, pattern: str) -> bool:
- """匹配号码特征模式
+def classify_number(number: str) -> str:
+ """根据号码特征分类
- 通配符规则:
- - X = 任意数字
- - A = 非4
- - B = 非47
- - C = 非347
- - D = 非247
- - E = 非2347
+ 规则(2026-04-30修正版):
+
+ 首先区分标百、标十,单张:
+ - 单张:J后面9位
+ - 标十:J后面8位(最后一位是1)
+ - 标百:J后面7位(最后两位是01)
+
+ 分类优先级:
+ | 类型 | 排除 | 可用数字 | 必须包含 |
+ |------|------|----------|----------|
+ | 通货 | - | - | 4 |
+ | 带7号 | 4 | - | 7 |
+ | 永恒号 | 47 | - | - |
+ | 圆圆号 | 123457 | 0689 | - |
+ | 倒置号 | 23457 | 01689 | 1 |
+ | 金马王 | 12347 | 05689 | 5 |
+ | 金马号 | 2347 | 015689 | 1,5 |
+ | 金山王 | 12457 | 03689 | 3 |
+ | 天马王 | 1247 | 035689 | 3,5 |
+ | 金山号 | 2457 | 013689 | 1,3 |
+ | 天马号 | 247 | 0135689 | 1,3,5 |
+ | 朦胧王 | 13457 | - | - |
+ | 朦胧号 | 3457 | - | - |
+ | 如意号 | 1347 | - | - |
+ | 钻石号 | 347 | - | - |
"""
- if not col_number or not pattern:
- return False
+ if not number:
+ return "未知"
- if len(col_number) != len(pattern):
- return False
+ if number.startswith('J0'):
+ digits = number[2:]
+ elif number.startswith('J'):
+ digits = number[1:]
+ else:
+ return "未知"
- for i, p in enumerate(pattern):
- c = col_number[i]
- if p == 'X':
- continue # 任意数字
- elif p == 'A':
- if c == '4':
- return False
- elif p == 'B':
- if c == '4':
- return False
- if i < len(col_number) - 1 and col_number[i+1] == '7' and pattern[i+1] == 'X':
- 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 == 'L':
- # L = 带4
- if c != '4':
- return False
- elif p == 'N':
- # N = 无4
- if c == '4':
- return False
- else:
- if c != p:
- return False
- return True
+ if len(digits) == 9:
+ d = digits
+ elif len(digits) == 8:
+ d = digits[:8]
+ elif len(digits) == 7:
+ d = digits
+ else:
+ return "未知"
+
+ unique = set(d)
+
+ # 1. 通货:带4
+ if '4' in unique:
+ return "通货"
+
+ # 2. 带7号:无4,有7
+ if '7' in unique:
+ return "带7号"
+
+ # 3. 圆圆号:无123457,0689四个数字任意组合
+ if unique <= {'0', '6', '8', '9'}:
+ return "圆圆号"
+
+ # 4. 永恒号:只有0和1(无47但不符合圆圆号/倒置号)
+ if unique <= {'0', '1'}:
+ return "永恒号"
+
+ # 5. 倒置号:01689组合,必须有1,且包含6/8/9中的至少一个
+ if unique <= {'0', '1', '6', '8', '9'} and '1' in unique and unique & {'6', '8', '9'}:
+ return "倒置号"
+
+ # 6. 金马王:无12347,05689组合,必须有5(排除1)
+ if unique <= {'0', '5', '6', '8', '9'} and '5' in unique and '1' not in unique:
+ return "金马王"
+
+ # 7. 金马号:无2347,015689组合,必须有1和5(包含1)
+ if unique <= {'0', '1', '5', '6', '8', '9'} and '1' in unique and '5' in unique:
+ return "金马号"
+
+ # 8. 金山王:无12457,03689组合,必须有3(排除1和5)
+ if unique <= {'0', '3', '6', '8', '9'} and '3' in unique and '1' not in unique and '5' not in unique:
+ return "金山王"
+
+ # 9. 天马王:无1247,035689组合,必须有3和5(排除1)
+ if unique <= {'0', '3', '5', '6', '8', '9'} and '3' in unique and '5' in unique and '1' not in unique:
+ return "天马王"
+
+ # 10. 金山号:无2457,013689组合,必须有1和3(排除5)
+ if unique <= {'0', '1', '3', '6', '8', '9'} and '1' in unique and '3' in unique and '5' not in unique:
+ return "金山号"
+
+ # 11. 天马号:无247,0135689组合,必须有1、3和5
+ if unique <= {'0', '1', '3', '5', '6', '8', '9'} and '1' in unique and '3' in unique and '5' in unique:
+ return "天马号"
+
+ # 12. 朦胧王:无13457
+ if not unique & {'1', '3', '4', '5', '7'}:
+ return "朦胧王"
+
+ # 13. 朦胧号:无3457
+ if not unique & {'3', '4', '5', '7'}:
+ return "朦胧号"
+
+ # 14. 如意号:无1347
+ if not unique & {'1', '3', '4', '7'}:
+ return "如意号"
+
+ # 15. 钻石号:无347
+ if not unique & {'3', '4', '7'}:
+ return "钻石号"
+
+ # 16. 永恒号兜底:无47
+ if '7' not in unique:
+ return "永恒号"
+
+ return "其他"
-def match_self_collections_count(db: Session, user_id: str, expect_number: str) -> int:
- """根据号码特征计算匹配藏品数量(从用户自有藏品)
+def check_match(col_number: str, expect_number: str, expect_category: str) -> bool:
+ """检查藏品号码是否符合期望的分类"""
+ if not col_number or not expect_category:
+ return False
- Args:
- db: 数据库会话
- user_id: 用户ID
- expect_number: 期望号码,如 "J012345678" (固定J0前缀+8位特征)
+ col_cat = classify_number(col_number)
- Returns:
- 匹配的藏品数量
- """
+ if col_cat == expect_category:
+ return True
+
+ if expect_category == "其他":
+ return check_number_pattern(col_number, expect_number)
+
+ return False
+
+
+def check_number_pattern(col_number: str, expect_number: str) -> bool:
+ """检查号码特征匹配"""
+ if not col_number or not expect_number:
+ return False
+
+ if col_number.startswith('J0'):
+ col_digits = col_number[2:]
+ else:
+ col_digits = col_number[1:] if len(col_number) > 1 else col_number
+
+ if expect_number.startswith('J0'):
+ exp_digits = expect_number[2:]
+ else:
+ exp_digits = expect_number[1:] if len(expect_number) > 1 else expect_number
+
+ min_len = min(len(col_digits), len(exp_digits))
+ return col_digits[:min_len] == exp_digits[:min_len]
+
+
+def get_match_type(expect_number: str) -> str:
+ """判断匹配类型"""
+ if not expect_number:
+ return "unknown"
+
+ if expect_number.startswith('J0'):
+ digits = expect_number[2:]
+ elif expect_number.startswith('J'):
+ digits = expect_number[1:]
+ else:
+ return "unknown"
+
+ if len(digits) == 8:
+ return "ten"
+ elif len(digits) == 7:
+ return "hundred"
+ elif len(digits) == 9:
+ return "single"
+ return "unknown"
+
+
+def match_self_collections_count(db: Session, user_id: str, expect_number: str, expect_category: str) -> int:
+ """计算匹配藏品数量(用户自有藏品)"""
from app.models.models import Collection
- if not expect_number or len(expect_number) != 10:
+ if not expect_number:
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"
@@ -92,81 +197,36 @@ def match_self_collections_count(db: Session, user_id: str, expect_number: str)
count = 0
for c in collections:
number = c.f02_10_prefix_serial or ''
- 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
+ if check_match(number, expect_number, expect_category):
+ count += 1
return count
-def match_collections_count_from_coolbot(expect_number: str) -> int:
- """根据号码特征计算匹配藏品数量(从coolbot_data数据库)
-
- Args:
- expect_number: 期望号码,如 "J012345678" (固定J0前缀+8位特征)
-
- Returns:
- 匹配的藏品数量
- """
- 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:
+def match_collections_count_from_coolbot(expect_number: str, expect_category: str) -> int:
+ """计算匹配藏品数量(一尘数据库)"""
+ if not expect_number:
return 0
query = text("""
SELECT id, crown_code FROM collections
WHERE crown_code IS NOT NULL
AND crown_code != ''
- AND LENGTH(crown_code) >= 10
AND crown_code LIKE 'J0%'
""")
try:
with coolbot_engine.connect() as conn:
result = conn.execute(query)
-
- match_count = 0
- for row in result:
- crown_code = row[1]
- if crown_code and len(crown_code) >= 10:
- col_pattern = crown_code[2:10]
- if match_pattern(col_pattern, pattern):
- match_count += 1
-
- return match_count
+ return sum(1 for row in result if check_match(row[1], expect_number, expect_category))
except Exception as e:
- print(f"Error querying coolbot_data: {e}")
+ print(f"Error: {e}")
return 0
-def match_collections_list_from_coolbot(expect_number: str, limit: int = 20) -> List[dict]:
- """获取匹配的藏品列表(从coolbot_data数据库)
-
- Args:
- expect_number: 期望号码,如 "J012345678"
- limit: 返回的最大数量
-
- Returns:
- 匹配的藏品列表
- """
- if not expect_number or len(expect_number) != 10:
- return []
-
- if not expect_number.startswith('J0'):
- return []
-
- pattern = expect_number[2:] # 后8位
- if not pattern:
+def match_collections_list_from_coolbot(expect_number: str, expect_category: str, limit: int = 20) -> List[dict]:
+ """获取匹配的藏品列表(一尘数据库)"""
+ if not expect_number:
return []
query = text("""
@@ -174,35 +234,29 @@ def match_collections_list_from_coolbot(expect_number: str, limit: int = 20) ->
FROM collections
WHERE crown_code IS NOT NULL
AND crown_code != ''
- AND LENGTH(crown_code) >= 10
AND crown_code LIKE 'J0%'
""")
try:
with coolbot_engine.connect() as conn:
result = conn.execute(query)
-
matched = []
for row in result:
- crown_code = row[3]
- if crown_code and len(crown_code) >= 10:
- col_pattern = crown_code[2:10]
- if match_pattern(col_pattern, pattern):
- matched.append({
- "id": row[0],
- "name": row[1],
- "category": row[2],
- "crown_code": crown_code,
- "price": float(row[4]) if row[4] else None,
- "post_title": row[5],
- "post_url": row[6],
- "author": row[7],
- "post_crawled_at": row[8].isoformat() if row[8] else None
- })
- if len(matched) >= limit:
- break
-
+ if check_match(row[3], expect_number, expect_category):
+ matched.append({
+ "id": row[0],
+ "name": row[1],
+ "category": row[2],
+ "crown_code": row[3],
+ "price": float(row[4]) if row[4] else None,
+ "post_title": row[5],
+ "post_url": row[6],
+ "author": row[7],
+ "post_crawled_at": row[8].isoformat() if row[8] else None
+ })
+ if len(matched) >= limit:
+ break
return matched
except Exception as e:
- print(f"Error querying coolbot_data: {e}")
+ print(f"Error: {e}")
return []
diff --git a/config/CHANGELOG.md b/config/CHANGELOG.md
index 6aedb3f..7459435 100644
--- a/config/CHANGELOG.md
+++ b/config/CHANGELOG.md
@@ -1,10 +1,56 @@
# 版本更新记录
-## v0.1.2 (2026-04-30)
+## v0.1.6 (2026-05-01)
+
+### 前端更新
+- **List.jsx**
+ - 新增:搜索框增强 - 支持搜索冠字号、编号、价格
+ - 新增:版别筛选按钮 - 20版/19版/18版/17版/16版
+ - 新增:包装类型筛选 - 单张/标十/标百
+ - 新增:号码分类筛选 - 圆圆号/倒置号/金马号/天马号/钻石号/永恒号/带7号/带4号
+ - 新增:清除筛选按钮
+ - 优化:筛选结果显示当前已选条件
+
+## v0.1.5 (2026-05-01)
+
+### 前端更新
+- **Purchase.jsx**
+ - 新增:待审批Tab(黄色按钮)- 显示需要审批的成员
+ - 新增:待审批成员列表 - 显示申请人信息+同意/拒绝按钮
+ - 新增:审批后状态 - 变灰+显示已批准/已拒绝
+ - 新增:申请加入后待审批状态 - 列表显示黄色"待审批"
+ - 修复:待审批数量计算准确
+ - 修复:只有群主能看到自己创建的群的待审批
+
+### 后端更新
+- **purchase.py**
+ - 修复:join_group接口处理状态
+ - 新增:MemberResponse扩展字段(wechat_name, contact等)
+
+## v0.1.4 (2026-05-01)
+
+### 前端更新
+- **Purchase.jsx**
+ - 修复:列表隐藏群介绍,点进详情后才显示
+ - 修复:未登录不黑屏
+ - 新增:修改群信息弹窗(群主)
+ - 新增:修改个人信息弹窗(成员)
+ - 修复:冠字号输入框maxLength
+
+## v0.1.3 (2026-04-30)
### 前端更新
- **Home.jsx**
- 修复:我的认购入口字体颜色改为蓝色 #3b82f6
+- **Purchase.jsx**
+ - 新增:权限控制 - 未加入的群只能看到基本信息(群名、编号、群主、介绍)
+ - 新增:申请加入按钮 - 未加入的群可申请加入
+ - 修复:敏感信息(统计、成员、藏品)只有已加入才可见
+ - 修复:user.id改为user.f99_90_id
+
+## v0.1.2 (2026-04-30)
+
+### 前端更新
- **Purchase.jsx**
- 新增:创建群表单6个必填字段
- 认购群名称
diff --git a/config/VERSION.json b/config/VERSION.json
index 1f355bd..df3c1cf 100644
--- a/config/VERSION.json
+++ b/config/VERSION.json
@@ -1,18 +1,19 @@
{
- "version": "0.1.2",
- "updated": "2026-04-30",
+ "version": "0.1.6",
+ "updated": "2026-05-01",
"modules": {
"frontend": {
- "version": "0.1.2",
+ "version": "0.1.6",
"pages": {
"Home": "0.1.0",
- "Purchase": "0.0.2"
+ "Purchase": "0.2.0",
+ "List": "0.1.6"
}
},
"backend": {
- "version": "0.1.2",
+ "version": "0.1.6",
"routers": {
- "purchase": "0.1.0"
+ "purchase": "0.2.0"
},
"models": {
"purchase": "0.1.0"
diff --git a/frontend/src/pages/List.jsx b/frontend/src/pages/List.jsx
index d5b705e..d7f9a13 100644
--- a/frontend/src/pages/List.jsx
+++ b/frontend/src/pages/List.jsx
@@ -1,7 +1,7 @@
/**
* List - 藏品列表页面
- * Version: 0.0.1
- * 更新:
+ * Version: 0.1.6
+ * 更新:新增成交行情页面筛选功能(版别、包装类型、号码分类)
*/
import React, { useState, useEffect } from 'react'
@@ -30,6 +30,10 @@ export default function List() {
const [dealSortOrder, setDealSortOrder] = useState('desc')
const [myDeals, setMyDeals] = useState([])
const [dealsLoading, setDealsLoading] = useState(false)
+ // 行情筛选条件
+ const [dealVersionFilter, setDealVersionFilter] = useState('') // 版别筛选
+ const [dealPackagingFilter, setDealPackagingFilter] = useState('') // 包装类型筛选
+ const [dealNumberCatFilter, setDealNumberCatFilter] = useState('') // 号码分类筛选
useEffect(() => {
// 检查是否管理员
@@ -106,10 +110,33 @@ export default function List() {
// 行情过滤和排序
const filteredDeals = myDeals.filter(deal => {
- if (!dealSearch) return true
- const s = dealSearch.toLowerCase().trim()
- const fields = [deal.title, deal.content, deal.category, deal.packaging, deal.grading_company, deal.grading_score, deal.deal_no, deal.seller, deal.platform, deal.buyer, deal.deal_price?.toString(), deal.deal_date].filter(v => v).map(v => v.toString().toLowerCase())
- return fields.some(f => f.includes(s))
+ // 1. 搜索过滤
+ if (dealSearch) {
+ const s = dealSearch.toLowerCase().trim()
+ const fields = [deal.title, deal.content, deal.category, deal.packaging, deal.grading_company, deal.grading_score, deal.deal_no, deal.seller, deal.platform, deal.buyer, deal.deal_price?.toString(), deal.deal_date].filter(v => v).map(v => v.toString().toLowerCase())
+ if (!fields.some(f => f.includes(s))) return false
+ }
+ // 2. 版别筛选 - 从content中解析
+ if (dealVersionFilter) {
+ let version = ''
+ if (deal.content) {
+ const match = deal.content.match(/版别:\s*([^\n]+)/)
+ if (match) version = match[1].trim()
+ }
+ if (version !== dealVersionFilter) return false
+ }
+ // 3. 包装类型筛选
+ if (dealPackagingFilter && deal.packaging !== dealPackagingFilter) return false
+ // 4. 号码分类筛选 - 从content中解析
+ if (dealNumberCatFilter) {
+ let numberCat = ''
+ if (deal.content) {
+ const match = deal.content.match(/号码分类:\s*([^\n]+)/)
+ if (match) numberCat = match[1].trim()
+ }
+ if (numberCat !== dealNumberCatFilter) return false
+ }
+ return true
}).sort((a, b) => {
let aVal = a[dealSortField] || ''
let bVal = b[dealSortField] || ''
@@ -660,13 +687,13 @@ export default function List() {
{/* 行情搜索框 */}
setDealSearch(e.target.value)}
style={{
width: '100%',
background: 'rgba(255,255,255,0.05)',
- border: '1px solid rgba(255,255,255,0.1)',
+ border: dealSearch ? '1px solid rgba(59, 130, 246, 0.5)' : '1px solid rgba(255,255,255,0.1)',
color: '#fff',
padding: '10px 12px',
borderRadius: '8px',
@@ -675,6 +702,134 @@ export default function List() {
boxSizing: 'border-box'
}}
/>
+ {dealSearch && (
+
+ )}
+
+
+ {/* 筛选条件按钮 */}
+
+ {/* 第一行:版别筛选 */}
+
+ 版别:
+ {[
+ { value: '', label: '全部' },
+ { value: '20', label: '20版' },
+ { value: '19', label: '19版' },
+ { value: '18', label: '18版' },
+ { value: '17', label: '17版' },
+ { value: '16', label: '16版' },
+ ].map(item => (
+
+ ))}
+
+ {/* 第二行:包装类型筛选 */}
+
+ 包装:
+ {[
+ { value: '', label: '全部' },
+ { value: '单张', label: '单张' },
+ { value: '标十', label: '标十' },
+ { value: '标百', label: '标百' },
+ ].map(item => (
+
+ ))}
+
+ {/* 第三行:号码分类筛选 */}
+
+ 号码:
+ {[
+ { value: '', label: '全部' },
+ { value: '圆圆号', label: '圆圆号' },
+ { value: '倒置号', label: '倒置号' },
+ { value: '金马号', label: '金马号' },
+ { value: '天马号', label: '天马号' },
+ { value: '钻石号', label: '钻石号' },
+ { value: '永恒号', label: '永恒号' },
+ { value: '带7号', label: '带7号' },
+ { value: '带4号', label: '带4号' },
+ ].map(item => (
+
+ ))}
+
+ {/* 清除筛选按钮 */}
+ {(dealVersionFilter || dealPackagingFilter || dealNumberCatFilter) && (
+
+ )}
{dealsLoading ? (
@@ -682,7 +837,16 @@ export default function List() {
) : filteredDeals.length === 0 ? (
📊
-
{dealSearch ? '没有匹配的行情' : '暂无行情记录'}
+
+ {(dealSearch || dealVersionFilter || dealPackagingFilter || dealNumberCatFilter)
+ ? '没有匹配的行情'
+ : '暂无行情记录'}
+
+ {(dealSearch || dealVersionFilter || dealPackagingFilter || dealNumberCatFilter) && (
+
+ 已选筛选:{dealSearch && `搜索"${dealSearch}" `}{dealVersionFilter && `版别${dealVersionFilter} `}{dealPackagingFilter && `包装${dealPackagingFilter} `}{dealNumberCatFilter && `号码${dealNumberCatFilter}`}
+
+ )}
) : (
diff --git a/frontend/src/pages/Purchase.jsx b/frontend/src/pages/Purchase.jsx
index 817ec2c..bb082b9 100644
--- a/frontend/src/pages/Purchase.jsx
+++ b/frontend/src/pages/Purchase.jsx
@@ -1,8 +1,8 @@
/**
* Purchase - 我的认购/团购页面
- * Version: 0.0.2 (2026-04-30)
- * 认购群管理、加入、查看详情
- * 更新:创建群表单新增6个字段(群名、介绍、人数、总数、周期、开始日期)
+ * Version: 0.2.0 (2026-05-01)
+ * 认购群管理、加入、待审批
+ * 更新:待审批Tab、成员审批、申请状态显示
*/
import React, { useState, useEffect } from 'react'
@@ -20,6 +20,9 @@ function Purchase() {
const [groupMembers, setGroupMembers] = useState([])
const [groupCollections, setGroupCollections] = useState([])
const [showCollectionForm, setShowCollectionForm] = useState(false)
+ const [editingMember, setEditingMember] = useState(null)
+ const [editingGroup, setEditingGroup] = useState(null)
+ const [viewingMember, setViewingMember] = useState(null)
const [newGroup, setNewGroup] = useState({
name: '',
description: '',
@@ -30,7 +33,10 @@ function Purchase() {
})
const [newCollection, setNewCollection] = useState({ number: 'J0________', price: '' })
const [loading, setLoading] = useState(true)
- const [activeTab, setActiveTab] = useState('my') // my/all
+ const [activeTab, setActiveTab] = useState('my') // my/all/pending
+ const [pendingGroups, setPendingGroups] = useState([]) // 我创建的群中有待审批成员的
+ const [pendingMembersMap, setPendingMembersMap] = useState({}) // 每个群的待审批成员
+ const [processedMembers, setProcessedMembers] = useState({}) // 已审批过的成员
useEffect(() => {
const userData = localStorage.getItem('user')
@@ -47,6 +53,16 @@ function Purchase() {
const fetchGroups = async () => {
setLoading(true)
const token = localStorage.getItem('token')
+ const userData = localStorage.getItem('user')
+
+ // 未登录:直接返回,页面不黑屏
+ if (!token || !userData) {
+ setAllGroups([])
+ setMyGroups([])
+ setLoading(false)
+ return
+ }
+
try {
// 获取我参与的认购群
const myRes = await fetch(`${API_BASE}/api/purchase/my-groups`, {
@@ -59,6 +75,34 @@ function Purchase() {
const allRes = await fetch(`${API_BASE}/api/purchase/groups`)
const allData = await allRes.json()
setAllGroups(allData || [])
+
+ // 获取我创建的群中有待审批成员的(只有我创建的群)
+ const userData = localStorage.getItem('user')
+ const currentUser = userData ? JSON.parse(userData) : null
+
+ const pendingRes = await fetch(`${API_BASE}/api/purchase/my-groups`, {
+ headers: { 'Authorization': `Bearer ${token}` }
+ })
+ const pendingData = await pendingRes.json()
+ // 筛选我是群主且有待审批成员的群
+ const groupsWithPending = (pendingData || []).filter(g => g.creator_id === currentUser?.f99_90_id && g.total_pending > 0)
+ setPendingGroups(groupsWithPending)
+
+ // 获取每个群待审批成员列表
+ const membersMap = {}
+ for (const g of groupsWithPending) {
+ try {
+ const res = await fetch(`${API_BASE}/api/purchase/groups/${g.id}/pending-members`, {
+ headers: { 'Authorization': `Bearer ${token}` }
+ })
+ const data = await res.json()
+ // 确保是数组
+ membersMap[g.id] = Array.isArray(data) ? data : []
+ } catch (e) {
+ membersMap[g.id] = []
+ }
+ }
+ setPendingMembersMap(membersMap)
} catch (e) {
console.error('获取认购群失败:', e)
}
@@ -110,7 +154,7 @@ function Purchase() {
})
const data = await res.json()
if (res.ok) {
- alert('加入成功!')
+ alert(data.message || '已申请,等待群主审批')
fetchGroups()
} else {
alert(data.detail || '加入失败')
@@ -205,6 +249,7 @@ function Purchase() {
if (res.ok) {
alert('提交成功!')
setNewCollection({ number: 'J0________', price: '' })
+ setShowCollectionForm(false)
viewGroupDetail(selectedGroup.id)
fetchGroups()
} else {
@@ -240,6 +285,102 @@ function Purchase() {
}
}
+ const approveMember = async (memberId, groupId, action) => {
+ const token = localStorage.getItem('token')
+ if (!token) return
+
+ try {
+ const res = await fetch(`${API_BASE}/api/purchase/groups/${groupId}/members/${memberId}/approve?action=${action}`, {
+ method: 'POST',
+ headers: { 'Authorization': `Bearer ${token}` }
+ })
+ const data = await res.json()
+ if (res.ok) {
+ alert(action === 'approve' ? '已批准' : '已拒绝')
+ // 标记为已处理
+ const processed = {...processedMembers}
+ processed[memberId] = action
+ setProcessedMembers(processed)
+ // 从pendingMembersMap中移除该成员
+ const newMap = {...pendingMembersMap}
+ newMap[groupId] = (newMap[groupId] || []).filter(m => m.id !== memberId)
+ setPendingMembersMap(newMap)
+ // 如果该群没有更多待审批成员,从pendingGroups中移除
+ if ((newMap[groupId] || []).length === 0) {
+ setPendingGroups(pendingGroups.filter(g => g.id !== groupId))
+ }
+ // 同时刷新整个列表
+ fetchGroups()
+ } else {
+ alert(data.detail || '操作失败')
+ }
+ } catch (e) {
+ console.error('操作失败:', e)
+ alert('操作失败')
+ }
+ }
+
+ const updateMember = async () => {
+ const token = localStorage.getItem('token')
+ if (!token || !editingMember || !selectedGroup) return
+
+ // 找当前用户的成员ID
+ const memberId = groupMembers.find(m => m.user_id === user.f99_90_id)?.id
+ if (!memberId) { alert('您不是该群成员'); return }
+
+ try {
+ const res = await fetch(`${API_BASE}/api/purchase/groups/${selectedGroup.id}/members/${memberId}`, {
+ method: 'PUT',
+ headers: {
+ 'Authorization': `Bearer ${token}`,
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify(editingMember)
+ })
+ const data = await res.json()
+ if (res.ok) {
+ alert('更新成功!')
+ setEditingMember(null)
+ viewGroupDetail(selectedGroup.id)
+ } else {
+ alert(data.detail || '更新失败')
+ }
+ } catch (e) {
+ console.error('更新失败:', e)
+ alert('更新失败')
+ }
+ }
+
+ const updateGroup = async () => {
+ const token = localStorage.getItem('token')
+ if (!token || !editingGroup || !selectedGroup) return
+
+ try {
+ const res = await fetch(`${API_BASE}/api/purchase/groups/${selectedGroup.id}`, {
+ method: 'PUT',
+ headers: {
+ 'Authorization': `Bearer ${token}`,
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify(editingGroup)
+ })
+ const data = await res.json()
+ if (res.ok) {
+ alert('更新成功!')
+ setEditingGroup(null)
+ fetchGroups()
+ // 刷新详情
+ const group = allGroups.find(g => g.id === selectedGroup.id) || myGroups.find(g => g.id === selectedGroup.id)
+ if (group) setSelectedGroup(group)
+ } else {
+ alert(data.detail || '更新失败')
+ }
+ } catch (e) {
+ console.error('更新失败:', e)
+ alert('更新失败')
+ }
+ }
+
const formatMoney = (val) => {
if (!val || val === 0) return '¥0'
return '¥' + parseFloat(val).toLocaleString()
@@ -248,16 +389,18 @@ function Purchase() {
// 渲染认购群卡片
const renderGroupCard = (group, showJoin = false) => {
const isMyGroup = myGroups.some(g => g.id === group.id)
+ const isPendingGroup = pendingGroups.some(g => g.id === group.id) // 待审批
const isClosed = group.status === 'closed'
+ const isApplied = isPendingGroup && !isMyGroup
return (
viewGroupDetail(group.id)}
style={{
- background: isClosed ? 'rgba(80,80,80,0.3)' : isMyGroup ? 'linear-gradient(135deg, rgba(16,185,129,0.15) 0%, rgba(16,185,129,0.05) 100%)' : 'linear-gradient(135deg, rgba(59,130,246,0.15) 0%, rgba(59,130,246,0.05) 100%)',
+ background: isClosed ? 'rgba(80,80,80,0.3)' : isMyGroup ? 'linear-gradient(135deg, rgba(16,185,129,0.15) 0%, rgba(16,185,129,0.05) 100%)' : isApplied ? 'linear-gradient(135deg, rgba(251,191,36,0.15) 0%, rgba(251,191,36,0.05) 100%)' : 'linear-gradient(135deg, rgba(59,130,246,0.15) 0%, rgba(59,130,246,0.05) 100%)',
borderRadius: '12px',
padding: '16px',
- border: isClosed ? '1px solid rgba(100,100,100,0.3)' : isMyGroup ? '1px solid rgba(16,185,129,0.2)' : '1px solid rgba(59,130,246,0.2)',
+ border: isClosed ? '1px solid rgba(100,100,100,0.3)' : isMyGroup ? '1px solid rgba(16,185,129,0.2)' : isApplied ? '1px solid rgba(251,191,36,0.2)' : '1px solid rgba(59,130,246,0.2)',
cursor: 'pointer',
marginBottom: '12px',
opacity: isClosed ? 0.7 : 1
@@ -274,6 +417,8 @@ function Purchase() {
已结束
) : isMyGroup ? (
已加入
+ ) : isApplied ? (
+
待审批
) : showJoin && (
)}
+ {isMyGroup && !isClosed && (
藏品: {group.total_collections}件
总价: {formatMoney(group.total_amount)}
均价: {formatMoney(group.avg_price)}
+ )}
)
}
// 群详情弹窗
if (selectedGroup) {
+ const isMyGroup = myGroups.some(g => g.id === selectedGroup.id)
+ const isClosed = selectedGroup.status === 'closed'
+ const canViewDetails = isMyGroup && !isClosed
+
return (
)}
+
+ {/* 编辑按钮:群主修改群信息,成员修改个人信息 */}
+ {user && (
+
+ )}
- {/* 统计卡片 */}
+ {/* 群介绍 - 所有人都可见 */}
+ {selectedGroup.description && (
+
+
群介绍
+
{selectedGroup.description}
+
+ )}
+
+ {/* 未加入:显示申请加入按钮 */}
+ {!isMyGroup && !isClosed && (
+
+ )}
+
+ {/* 敏感信息:只有已加入的群才可见 */}
+ {canViewDetails ? (
+ <>
参与成员
{groupMembers.map(m => (
-
setViewingMember(m)}
+ style={{
+ background: 'rgba(255,255,255,0.1)',
+ borderRadius: '20px',
+ padding: '6px 12px',
+ display: 'flex',
+ alignItems: 'center',
+ gap: '6px',
+ cursor: 'pointer'
}}>
{m.user_name?.charAt(0) || '?'}
@@ -429,7 +645,7 @@ function Purchase() {
{c.paid ? '✓已结清' : '○未结清'}
- {user && selectedGroup && selectedGroup.creator_id === user.id && (
+ {user && selectedGroup && selectedGroup.creator_id === user.f99_90_id && (
))}
{groupCollections.length === 0 &&
暂无认购藏品
}
- {selectedGroup && user && selectedGroup.creator_id === user.id && selectedGroup.status === 'active' && (
+ {selectedGroup && user && selectedGroup.creator_id === user.f99_90_id && selectedGroup.status === 'active' && (
)}
+ >
+ ) : null}
+
+ {/* 修改成员信息弹窗 */}
+ {editingMember && (
+
+
+
修改个人信息
+
+
用户名
+
setEditingMember({...editingMember, user_name: e.target.value})}
+ placeholder="用户名"
+ style={{ width: '100%', background: 'rgba(255,255,255,0.1)', border: '1px solid rgba(255,255,255,0.2)', borderRadius: '8px', padding: '12px', color: '#fff', fontSize: '14px', marginBottom: '12px' }}
+ />
+
+
微信名
+
setEditingMember({...editingMember, wechat_name: e.target.value})}
+ placeholder="微信名"
+ style={{ width: '100%', background: 'rgba(255,255,255,0.1)', border: '1px solid rgba(255,255,255,0.2)', borderRadius: '8px', padding: '12px', color: '#fff', fontSize: '14px', marginBottom: '12px' }}
+ />
+
+
联系方式
+
setEditingMember({...editingMember, contact: e.target.value})}
+ placeholder="手机号或微信"
+ style={{ width: '100%', background: 'rgba(255,255,255,0.1)', border: '1px solid rgba(255,255,255,0.2)', borderRadius: '8px', padding: '12px', color: '#fff', fontSize: '14px', marginBottom: '12px' }}
+ />
+
+ {/* 群主可以修改的字段 */}
+ {user && selectedGroup && user.f99_90_id === selectedGroup.creator_id && (
+ <>
+
认购数量(仅群主可修改)
+
setEditingMember({...editingMember, purchase_count: parseInt(e.target.value) || 0})}
+ placeholder="认购数量"
+ style={{ width: '100%', background: 'rgba(255,255,255,0.1)', border: '1px solid rgba(255,255,255,0.2)', borderRadius: '8px', padding: '12px', color: '#fff', fontSize: '14px', marginBottom: '12px' }}
+ />
+
+
已付金额(仅群主可修改)
+
setEditingMember({...editingMember, paid_amount: parseFloat(e.target.value) || 0})}
+ placeholder="已付金额"
+ style={{ width: '100%', background: 'rgba(255,255,255,0.1)', border: '1px solid rgba(255,255,255,0.2)', borderRadius: '8px', padding: '12px', color: '#fff', fontSize: '14px', marginBottom: '12px' }}
+ />
+
+
备注(仅群主可修改)
+
+
+ )}
+
+ {/* 查看成员信息弹窗 */}
+ {viewingMember && (
+
+
+
+
+ {viewingMember.user_name}
+
+
+
+
+
+ 微信名
+ {viewingMember.wechat_name || '-'}
+
+
+ 联系方式
+ {viewingMember.contact || '-'}
+
+
+ 角色
+ {viewingMember.role === 'creator' ? '👑 群主' : '成员'}
+
+
+ 状态
+ {viewingMember.status === 'approved' ? '已确认' : '待审核'}
+
+
+ 认购数量
+ {viewingMember.purchase_count || 0}
+
+
+ 已付金额
+ ¥{(viewingMember.paid_amount || 0).toLocaleString()}
+
+
+
+ {viewingMember.notes && (
+
备注
+ )}
+ {viewingMember.notes && (
+
{viewingMember.notes}
+ )}
+
+ {/* 群主可以编辑该成员 */}
+ {user && selectedGroup && user.f99_90_id === selectedGroup.creator_id && viewingMember.role !== 'creator' && (
+
+ )}
+
+
+ )}
+
+ {/* 修改群信息弹窗 */}
+ {editingGroup && (
+
+
+
修改群信息
+
+
群名称
+
setEditingGroup({...editingGroup, name: e.target.value})} placeholder="群名称" style={{ width: '100%', background: 'rgba(255,255,255,0.1)', border: '1px solid rgba(255,255,255,0.2)', borderRadius: '8px', padding: '12px', color: '#fff', fontSize: '14px', marginBottom: '12px' }} />
+
+
群介绍
+
+
+ )}
{/* 编辑弹窗 */}
{editCollection && (
@@ -530,7 +924,7 @@ function Purchase() {
)}
{/* 提交认购藏品表单 - 仅群主可提交 */}
- {selectedGroup && user && selectedGroup.creator_id === user.id && selectedGroup.status === 'active' && showCollectionForm && (
+ {selectedGroup && user && selectedGroup.creator_id === user.f99_90_id && selectedGroup.status === 'active' && showCollectionForm && (
{
- let v = e.target.value
- if (v) {
- v = v.replace(/\D/g, '')
- if (v) {
- const num = newCollection.number || 'J0________'
- const arr = num.split('')
- arr[i+1] = v
- setNewCollection({...newCollection, number: arr.join('')})
- if (i < 8 && v) window.__numberInputs[i+1]?.focus()
- }
+ let v = e.target.value.replace(/\D/g, '').slice(-1)
+ if (v || i === 0) {
+ const num = newCollection.number || 'J0________'
+ const arr = num.split('')
+ arr[i+1] = i === 0 ? '0' : (v || '_')
+ setNewCollection({...newCollection, number: arr.join('')})
+ if (i < 8 && v) window.__numberInputs[i+1]?.focus()
}
}}
onKeyDown={(e) => {
@@ -658,7 +1050,7 @@ function Purchase() {
)}
{/* 非群主提示 */}
- {selectedGroup && user && !(selectedGroup.creator_id === user.id) && selectedGroup.status === 'active' && (
+ {selectedGroup && user && !(selectedGroup.creator_id === user.f99_90_id) && selectedGroup.status === 'active' && (
仅群主可提交认购藏品
@@ -725,11 +1117,53 @@ function Purchase() {
>
全部认购群 ({allGroups.length})
+
{/* 认购群列表 */}
{loading ? (
加载中...
+ ) : activeTab === 'pending' ? (
+
+ {pendingGroups.length === 0 ? (
+
暂无待审批成员
+ ) : pendingGroups.map(group =>
+
+
{group.name} - 待审批成员
+ {(pendingMembersMap[group.id] || []).map(m => {
+ const isProcessed = processedMembers[m.id]
+ return (
+
+
+
{m.user_name}
+
{m.wechat_name || '未填写微信名'} · {m.contact || '未填写联系方式'}
+ {isProcessed &&
已{isProcessed === 'approve' ? '批准' : '拒绝'}
}
+
+ {!isProcessed && (
+
+
+
+
+ )}
+
+ )})}
+
+ )}
+
) : (
{(activeTab === 'my' ? myGroups : allGroups).map(group =>