From 3eb8aac2bef90cde4d5bb215a6227f2f1d9c995a 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, 1 May 2026 10:00:17 +0800 Subject: [PATCH] =?UTF-8?q?v0.1.6=20-=20=E6=88=90=E4=BA=A4=E8=A1=8C?= =?UTF-8?q?=E6=83=85=E9=A1=B5=E9=9D=A2=E5=A2=9E=E5=BC=BA=EF=BC=9A=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E6=90=9C=E7=B4=A2+=E7=89=88=E5=88=AB+=E5=8C=85?= =?UTF-8?q?=E8=A3=85+=E5=8F=B7=E7=A0=81=E5=88=86=E7=B1=BB=E7=AD=9B?= =?UTF-8?q?=E9=80=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/routers/deal.py | 4 +- backend/app/routers/purchase.py | 27 +- backend/app/routers/seek.py | 7 +- backend/app/utils/coolbot_matcher.py | 338 ++++++++++-------- config/CHANGELOG.md | 48 ++- config/VERSION.json | 13 +- frontend/src/pages/List.jsx | 182 +++++++++- frontend/src/pages/Purchase.jsx | 494 +++++++++++++++++++++++++-- 8 files changed, 912 insertions(+), 201 deletions(-) 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' }} + /> + +
备注(仅群主可修改)
+