v0.1.6 - 成交行情页面增强:新增搜索+版别+包装+号码分类筛选
This commit is contained in:
parent
f695dfbfa0
commit
3eb8aac2be
|
|
@ -10,7 +10,7 @@ from sqlalchemy.orm import Session
|
||||||
# 更新:
|
# 更新:
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
# 更新:
|
# 更新:
|
||||||
from typing import Optional
|
from typing import Optional, List
|
||||||
# 更新:
|
# 更新:
|
||||||
from datetime import datetime, date
|
from datetime import datetime, date
|
||||||
# 更新:
|
# 更新:
|
||||||
|
|
@ -176,7 +176,7 @@ def generate_deal_no(db: Session):
|
||||||
# 更新:
|
# 更新:
|
||||||
# ============ API ============
|
# ============ API ============
|
||||||
# 更新:
|
# 更新:
|
||||||
@router.get("/list", response_model=list[DealInfoResponse])
|
@router.get("/list", response_model=List[DealInfoResponse])
|
||||||
# 更新:
|
# 更新:
|
||||||
def get_deal_list(
|
def get_deal_list(
|
||||||
# 更新:
|
# 更新:
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
# purchase - 认购群API路由
|
# purchase - 认购群API路由
|
||||||
# Version: 0.0.5 (2026-04-29)
|
# Version: 0.2.0 (2026-05-01)
|
||||||
# 认购群、成员、藏品管理
|
# 认购群、成员、藏品管理
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
|
|
@ -88,6 +88,11 @@ class MemberResponse(BaseModel):
|
||||||
role: str
|
role: str
|
||||||
status: str
|
status: str
|
||||||
joined_at: Optional[datetime]
|
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:
|
def generate_group_code(db: Session) -> str:
|
||||||
|
|
@ -127,7 +132,7 @@ def get_my_groups(
|
||||||
current_user = Depends(get_current_user),
|
current_user = Depends(get_current_user),
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
"""获取我参与的认购群"""
|
"""获取我参与的认购群(仅已批准的)"""
|
||||||
if not current_user:
|
if not current_user:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
@ -296,14 +301,14 @@ def join_group(
|
||||||
if existing:
|
if existing:
|
||||||
if existing.status == "approved":
|
if existing.status == "approved":
|
||||||
raise HTTPException(status_code=400, detail="您已加入该认购群")
|
raise HTTPException(status_code=400, detail="您已加入该认购群")
|
||||||
elif existing.status == "pending":
|
else:
|
||||||
raise HTTPException(status_code=400, detail="您已申请加入,等待审批")
|
# pending/rejected/其他状态:重新激活为pending
|
||||||
elif existing.status == "rejected":
|
|
||||||
existing.status = "pending"
|
existing.status = "pending"
|
||||||
existing.joined_at = datetime.now()
|
existing.joined_at = datetime.now()
|
||||||
|
existing.user_name = current_user.f01_01_name
|
||||||
group.total_pending += 1
|
group.total_pending += 1
|
||||||
db.commit()
|
db.commit()
|
||||||
return {"message": "已重新申请,等待审批"}
|
return {"message": "申请成功,等待审批"}
|
||||||
|
|
||||||
member = PurchaseMember(
|
member = PurchaseMember(
|
||||||
group_id=group_id,
|
group_id=group_id,
|
||||||
|
|
@ -339,7 +344,12 @@ def get_group_members(
|
||||||
"user_avatar": m.user_avatar,
|
"user_avatar": m.user_avatar,
|
||||||
"role": m.role,
|
"role": m.role,
|
||||||
"status": m.status,
|
"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
|
for m in members
|
||||||
]
|
]
|
||||||
|
|
@ -552,6 +562,7 @@ def delete_group(
|
||||||
return {"message": "认购群已删除"}
|
return {"message": "认购群已删除"}
|
||||||
|
|
||||||
class MemberUpdate(BaseModel):
|
class MemberUpdate(BaseModel):
|
||||||
|
user_name: Optional[str] = None
|
||||||
wechat_name: Optional[str] = None
|
wechat_name: Optional[str] = None
|
||||||
contact: Optional[str] = None
|
contact: Optional[str] = None
|
||||||
purchase_count: Optional[int] = 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:
|
if member.user_id != current_user.f99_90_id and group.creator_id != current_user.f99_90_id:
|
||||||
raise HTTPException(status_code=403, detail="无权修改")
|
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:
|
if data.wechat_name is not None:
|
||||||
member.wechat_name = data.wechat_name
|
member.wechat_name = data.wechat_name
|
||||||
if data.contact is not None:
|
if data.contact is not None:
|
||||||
|
|
|
||||||
|
|
@ -5,15 +5,14 @@
|
||||||
from fastapi import APIRouter, Depends, Query, HTTPException
|
from fastapi import APIRouter, Depends, Query, HTTPException
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from typing import Optional
|
from typing import Optional, List
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from app.core.database import get_db
|
from app.core.database import get_db
|
||||||
from app.core.auth import get_current_user
|
from app.core.auth import get_current_user
|
||||||
from app.models.seek_info import SeekInfo
|
from app.models.seek_info import SeekInfo
|
||||||
from app.models.models import User, Collection
|
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 (
|
from app.utils.coolbot_matcher import (
|
||||||
match_pattern,
|
|
||||||
match_self_collections_count,
|
match_self_collections_count,
|
||||||
match_collections_count_from_coolbot,
|
match_collections_count_from_coolbot,
|
||||||
match_collections_list_from_coolbot
|
match_collections_list_from_coolbot
|
||||||
|
|
@ -75,7 +74,7 @@ class MatchConfirmRequest(BaseModel):
|
||||||
collection_id: Optional[str] = None
|
collection_id: Optional[str] = None
|
||||||
|
|
||||||
# ============ API ============
|
# ============ API ============
|
||||||
@router.get("/list", response_model=list[SeekInfoResponse])
|
@router.get("/list", response_model=List[SeekInfoResponse])
|
||||||
def get_seek_list(
|
def get_seek_list(
|
||||||
status: str = Query("active"),
|
status: str = Query("active"),
|
||||||
page: int = Query(1, ge=1),
|
page: int = Query(1, ge=1),
|
||||||
|
|
|
||||||
|
|
@ -1,89 +1,194 @@
|
||||||
# coolbot_matcher - 一尘数据库号码匹配工具
|
# coolbot_matcher - 一尘数据库号码匹配工具
|
||||||
# Version: 0.0.1 (2026-04-24)
|
# Version: 0.0.2 (2026-04-30)
|
||||||
# 描述:从一尘数据库(coolbot_data)查询匹配的藏品号码
|
# 描述:从一尘数据库(coolbot_data)查询匹配的藏品号码
|
||||||
|
# 更新:号码分类规则修正
|
||||||
|
|
||||||
from typing import List
|
from typing import List, Optional
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from app.core.coolbot_db import coolbot_engine
|
from app.core.coolbot_db import coolbot_engine
|
||||||
|
|
||||||
|
|
||||||
def match_pattern(col_number: str, pattern: str) -> bool:
|
def classify_number(number: str) -> str:
|
||||||
"""匹配号码特征模式
|
"""根据号码特征分类
|
||||||
|
|
||||||
通配符规则:
|
规则(2026-04-30修正版):
|
||||||
- X = 任意数字
|
|
||||||
- A = 非4
|
首先区分标百、标十,单张:
|
||||||
- B = 非47
|
- 单张:J后面9位
|
||||||
- C = 非347
|
- 标十:J后面8位(最后一位是1)
|
||||||
- D = 非247
|
- 标百:J后面7位(最后两位是01)
|
||||||
- E = 非2347
|
|
||||||
|
分类优先级:
|
||||||
|
| 类型 | 排除 | 可用数字 | 必须包含 |
|
||||||
|
|------|------|----------|----------|
|
||||||
|
| 通货 | - | - | 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:
|
if not number:
|
||||||
return False
|
return "未知"
|
||||||
|
|
||||||
if len(col_number) != len(pattern):
|
if number.startswith('J0'):
|
||||||
return False
|
digits = number[2:]
|
||||||
|
elif number.startswith('J'):
|
||||||
for i, p in enumerate(pattern):
|
digits = number[1:]
|
||||||
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:
|
else:
|
||||||
if c != p:
|
return "未知"
|
||||||
|
|
||||||
|
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 check_match(col_number: str, expect_number: str, expect_category: str) -> bool:
|
||||||
|
"""检查藏品号码是否符合期望的分类"""
|
||||||
|
if not col_number or not expect_category:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
col_cat = classify_number(col_number)
|
||||||
|
|
||||||
|
if col_cat == expect_category:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
if expect_category == "其他":
|
||||||
|
return check_number_pattern(col_number, expect_number)
|
||||||
|
|
||||||
def match_self_collections_count(db: Session, user_id: str, expect_number: str) -> int:
|
return False
|
||||||
"""根据号码特征计算匹配藏品数量(从用户自有藏品)
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: 数据库会话
|
|
||||||
user_id: 用户ID
|
|
||||||
expect_number: 期望号码,如 "J012345678" (固定J0前缀+8位特征)
|
|
||||||
|
|
||||||
Returns:
|
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
|
from app.models.models import Collection
|
||||||
|
|
||||||
if not expect_number or len(expect_number) != 10:
|
if not expect_number:
|
||||||
return 0
|
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(
|
collections = db.query(Collection).filter(
|
||||||
Collection.f99_91_user_id == user_id,
|
Collection.f99_91_user_id == user_id,
|
||||||
Collection.f01_04_status == "in_collection"
|
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
|
count = 0
|
||||||
for c in collections:
|
for c in collections:
|
||||||
number = c.f02_10_prefix_serial or ''
|
number = c.f02_10_prefix_serial or ''
|
||||||
if len(number) >= 10 and number.startswith('J0'):
|
if check_match(number, expect_number, expect_category):
|
||||||
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
|
count += 1
|
||||||
|
|
||||||
return count
|
return count
|
||||||
|
|
||||||
|
|
||||||
def match_collections_count_from_coolbot(expect_number: str) -> int:
|
def match_collections_count_from_coolbot(expect_number: str, expect_category: str) -> int:
|
||||||
"""根据号码特征计算匹配藏品数量(从coolbot_data数据库)
|
"""计算匹配藏品数量(一尘数据库)"""
|
||||||
|
if not expect_number:
|
||||||
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:
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
query = text("""
|
query = text("""
|
||||||
SELECT id, crown_code FROM collections
|
SELECT id, crown_code FROM collections
|
||||||
WHERE crown_code IS NOT NULL
|
WHERE crown_code IS NOT NULL
|
||||||
AND crown_code != ''
|
AND crown_code != ''
|
||||||
AND LENGTH(crown_code) >= 10
|
|
||||||
AND crown_code LIKE 'J0%'
|
AND crown_code LIKE 'J0%'
|
||||||
""")
|
""")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with coolbot_engine.connect() as conn:
|
with coolbot_engine.connect() as conn:
|
||||||
result = conn.execute(query)
|
result = conn.execute(query)
|
||||||
|
return sum(1 for row in result if check_match(row[1], expect_number, expect_category))
|
||||||
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
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error querying coolbot_data: {e}")
|
print(f"Error: {e}")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def match_collections_list_from_coolbot(expect_number: str, limit: int = 20) -> List[dict]:
|
def match_collections_list_from_coolbot(expect_number: str, expect_category: str, limit: int = 20) -> List[dict]:
|
||||||
"""获取匹配的藏品列表(从coolbot_data数据库)
|
"""获取匹配的藏品列表(一尘数据库)"""
|
||||||
|
if not expect_number:
|
||||||
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:
|
|
||||||
return []
|
return []
|
||||||
|
|
||||||
query = text("""
|
query = text("""
|
||||||
|
|
@ -174,25 +234,20 @@ def match_collections_list_from_coolbot(expect_number: str, limit: int = 20) ->
|
||||||
FROM collections
|
FROM collections
|
||||||
WHERE crown_code IS NOT NULL
|
WHERE crown_code IS NOT NULL
|
||||||
AND crown_code != ''
|
AND crown_code != ''
|
||||||
AND LENGTH(crown_code) >= 10
|
|
||||||
AND crown_code LIKE 'J0%'
|
AND crown_code LIKE 'J0%'
|
||||||
""")
|
""")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with coolbot_engine.connect() as conn:
|
with coolbot_engine.connect() as conn:
|
||||||
result = conn.execute(query)
|
result = conn.execute(query)
|
||||||
|
|
||||||
matched = []
|
matched = []
|
||||||
for row in result:
|
for row in result:
|
||||||
crown_code = row[3]
|
if check_match(row[3], expect_number, expect_category):
|
||||||
if crown_code and len(crown_code) >= 10:
|
|
||||||
col_pattern = crown_code[2:10]
|
|
||||||
if match_pattern(col_pattern, pattern):
|
|
||||||
matched.append({
|
matched.append({
|
||||||
"id": row[0],
|
"id": row[0],
|
||||||
"name": row[1],
|
"name": row[1],
|
||||||
"category": row[2],
|
"category": row[2],
|
||||||
"crown_code": crown_code,
|
"crown_code": row[3],
|
||||||
"price": float(row[4]) if row[4] else None,
|
"price": float(row[4]) if row[4] else None,
|
||||||
"post_title": row[5],
|
"post_title": row[5],
|
||||||
"post_url": row[6],
|
"post_url": row[6],
|
||||||
|
|
@ -201,8 +256,7 @@ def match_collections_list_from_coolbot(expect_number: str, limit: int = 20) ->
|
||||||
})
|
})
|
||||||
if len(matched) >= limit:
|
if len(matched) >= limit:
|
||||||
break
|
break
|
||||||
|
|
||||||
return matched
|
return matched
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error querying coolbot_data: {e}")
|
print(f"Error: {e}")
|
||||||
return []
|
return []
|
||||||
|
|
|
||||||
|
|
@ -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**
|
- **Home.jsx**
|
||||||
- 修复:我的认购入口字体颜色改为蓝色 #3b82f6
|
- 修复:我的认购入口字体颜色改为蓝色 #3b82f6
|
||||||
|
- **Purchase.jsx**
|
||||||
|
- 新增:权限控制 - 未加入的群只能看到基本信息(群名、编号、群主、介绍)
|
||||||
|
- 新增:申请加入按钮 - 未加入的群可申请加入
|
||||||
|
- 修复:敏感信息(统计、成员、藏品)只有已加入才可见
|
||||||
|
- 修复:user.id改为user.f99_90_id
|
||||||
|
|
||||||
|
## v0.1.2 (2026-04-30)
|
||||||
|
|
||||||
|
### 前端更新
|
||||||
- **Purchase.jsx**
|
- **Purchase.jsx**
|
||||||
- 新增:创建群表单6个必填字段
|
- 新增:创建群表单6个必填字段
|
||||||
- 认购群名称
|
- 认购群名称
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,19 @@
|
||||||
{
|
{
|
||||||
"version": "0.1.2",
|
"version": "0.1.6",
|
||||||
"updated": "2026-04-30",
|
"updated": "2026-05-01",
|
||||||
"modules": {
|
"modules": {
|
||||||
"frontend": {
|
"frontend": {
|
||||||
"version": "0.1.2",
|
"version": "0.1.6",
|
||||||
"pages": {
|
"pages": {
|
||||||
"Home": "0.1.0",
|
"Home": "0.1.0",
|
||||||
"Purchase": "0.0.2"
|
"Purchase": "0.2.0",
|
||||||
|
"List": "0.1.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"backend": {
|
"backend": {
|
||||||
"version": "0.1.2",
|
"version": "0.1.6",
|
||||||
"routers": {
|
"routers": {
|
||||||
"purchase": "0.1.0"
|
"purchase": "0.2.0"
|
||||||
},
|
},
|
||||||
"models": {
|
"models": {
|
||||||
"purchase": "0.1.0"
|
"purchase": "0.1.0"
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* List - 藏品列表页面
|
* List - 藏品列表页面
|
||||||
* Version: 0.0.1
|
* Version: 0.1.6
|
||||||
* 更新:
|
* 更新:新增成交行情页面筛选功能(版别、包装类型、号码分类)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
|
|
@ -30,6 +30,10 @@ export default function List() {
|
||||||
const [dealSortOrder, setDealSortOrder] = useState('desc')
|
const [dealSortOrder, setDealSortOrder] = useState('desc')
|
||||||
const [myDeals, setMyDeals] = useState([])
|
const [myDeals, setMyDeals] = useState([])
|
||||||
const [dealsLoading, setDealsLoading] = useState(false)
|
const [dealsLoading, setDealsLoading] = useState(false)
|
||||||
|
// 行情筛选条件
|
||||||
|
const [dealVersionFilter, setDealVersionFilter] = useState('') // 版别筛选
|
||||||
|
const [dealPackagingFilter, setDealPackagingFilter] = useState('') // 包装类型筛选
|
||||||
|
const [dealNumberCatFilter, setDealNumberCatFilter] = useState('') // 号码分类筛选
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 检查是否管理员
|
// 检查是否管理员
|
||||||
|
|
@ -106,10 +110,33 @@ export default function List() {
|
||||||
|
|
||||||
// 行情过滤和排序
|
// 行情过滤和排序
|
||||||
const filteredDeals = myDeals.filter(deal => {
|
const filteredDeals = myDeals.filter(deal => {
|
||||||
if (!dealSearch) return true
|
// 1. 搜索过滤
|
||||||
|
if (dealSearch) {
|
||||||
const s = dealSearch.toLowerCase().trim()
|
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())
|
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))
|
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) => {
|
}).sort((a, b) => {
|
||||||
let aVal = a[dealSortField] || ''
|
let aVal = a[dealSortField] || ''
|
||||||
let bVal = b[dealSortField] || ''
|
let bVal = b[dealSortField] || ''
|
||||||
|
|
@ -660,13 +687,13 @@ export default function List() {
|
||||||
{/* 行情搜索框 */}
|
{/* 行情搜索框 */}
|
||||||
<div style={{ marginBottom: '12px' }}>
|
<div style={{ marginBottom: '12px' }}>
|
||||||
<input type="text"
|
<input type="text"
|
||||||
placeholder="🔍 搜索行情..."
|
placeholder="🔍 搜索冠字号、编号、价格..."
|
||||||
value={dealSearch}
|
value={dealSearch}
|
||||||
onChange={e => setDealSearch(e.target.value)}
|
onChange={e => setDealSearch(e.target.value)}
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
background: 'rgba(255,255,255,0.05)',
|
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',
|
color: '#fff',
|
||||||
padding: '10px 12px',
|
padding: '10px 12px',
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
|
|
@ -675,6 +702,134 @@ export default function List() {
|
||||||
boxSizing: 'border-box'
|
boxSizing: 'border-box'
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{dealSearch && (
|
||||||
|
<button
|
||||||
|
onClick={() => setDealSearch('')}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
right: '24px',
|
||||||
|
top: '50%',
|
||||||
|
transform: 'translateY(-50%)',
|
||||||
|
background: 'rgba(255,255,255,0.1)',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '50%',
|
||||||
|
width: '24px',
|
||||||
|
height: '24px',
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: '12px',
|
||||||
|
cursor: 'pointer'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 筛选条件按钮 */}
|
||||||
|
<div style={{ marginBottom: '12px', padding: '10px', background: 'rgba(255,255,255,0.03)', borderRadius: '8px' }}>
|
||||||
|
{/* 第一行:版别筛选 */}
|
||||||
|
<div style={{ marginBottom: '8px' }}>
|
||||||
|
<span style={{ color: '#94a3b8', fontSize: '11px', marginRight: '8px' }}>版别:</span>
|
||||||
|
{[
|
||||||
|
{ value: '', label: '全部' },
|
||||||
|
{ value: '20', label: '20版' },
|
||||||
|
{ value: '19', label: '19版' },
|
||||||
|
{ value: '18', label: '18版' },
|
||||||
|
{ value: '17', label: '17版' },
|
||||||
|
{ value: '16', label: '16版' },
|
||||||
|
].map(item => (
|
||||||
|
<button key={item.value} onClick={() => setDealVersionFilter(item.value)}
|
||||||
|
style={{
|
||||||
|
marginRight: '6px',
|
||||||
|
marginBottom: '4px',
|
||||||
|
padding: '4px 10px',
|
||||||
|
borderRadius: '6px',
|
||||||
|
fontSize: '11px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
background: dealVersionFilter === item.value ? '#fbbf24' : 'rgba(255,255,255,0.08)',
|
||||||
|
color: dealVersionFilter === item.value ? '#1e293b' : '#94a3b8',
|
||||||
|
border: dealVersionFilter === item.value ? 'none' : '1px solid rgba(255,255,255,0.1)'
|
||||||
|
}}>
|
||||||
|
{item.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{/* 第二行:包装类型筛选 */}
|
||||||
|
<div style={{ marginBottom: '8px' }}>
|
||||||
|
<span style={{ color: '#94a3b8', fontSize: '11px', marginRight: '8px' }}>包装:</span>
|
||||||
|
{[
|
||||||
|
{ value: '', label: '全部' },
|
||||||
|
{ value: '单张', label: '单张' },
|
||||||
|
{ value: '标十', label: '标十' },
|
||||||
|
{ value: '标百', label: '标百' },
|
||||||
|
].map(item => (
|
||||||
|
<button key={item.value} onClick={() => setDealPackagingFilter(item.value)}
|
||||||
|
style={{
|
||||||
|
marginRight: '6px',
|
||||||
|
marginBottom: '4px',
|
||||||
|
padding: '4px 10px',
|
||||||
|
borderRadius: '6px',
|
||||||
|
fontSize: '11px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
background: dealPackagingFilter === item.value ? '#a78bfa' : 'rgba(255,255,255,0.08)',
|
||||||
|
color: dealPackagingFilter === item.value ? '#fff' : '#94a3b8',
|
||||||
|
border: dealPackagingFilter === item.value ? 'none' : '1px solid rgba(255,255,255,0.1)'
|
||||||
|
}}>
|
||||||
|
{item.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{/* 第三行:号码分类筛选 */}
|
||||||
|
<div>
|
||||||
|
<span style={{ color: '#94a3b8', fontSize: '11px', marginRight: '8px' }}>号码:</span>
|
||||||
|
{[
|
||||||
|
{ value: '', label: '全部' },
|
||||||
|
{ value: '圆圆号', label: '圆圆号' },
|
||||||
|
{ value: '倒置号', label: '倒置号' },
|
||||||
|
{ value: '金马号', label: '金马号' },
|
||||||
|
{ value: '天马号', label: '天马号' },
|
||||||
|
{ value: '钻石号', label: '钻石号' },
|
||||||
|
{ value: '永恒号', label: '永恒号' },
|
||||||
|
{ value: '带7号', label: '带7号' },
|
||||||
|
{ value: '带4号', label: '带4号' },
|
||||||
|
].map(item => (
|
||||||
|
<button key={item.value} onClick={() => setDealNumberCatFilter(item.value)}
|
||||||
|
style={{
|
||||||
|
marginRight: '6px',
|
||||||
|
marginBottom: '4px',
|
||||||
|
padding: '4px 10px',
|
||||||
|
borderRadius: '6px',
|
||||||
|
fontSize: '11px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
background: dealNumberCatFilter === item.value ? '#22c55e' : 'rgba(255,255,255,0.08)',
|
||||||
|
color: dealNumberCatFilter === item.value ? '#fff' : '#94a3b8',
|
||||||
|
border: dealNumberCatFilter === item.value ? 'none' : '1px solid rgba(255,255,255,0.1)'
|
||||||
|
}}>
|
||||||
|
{item.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{/* 清除筛选按钮 */}
|
||||||
|
{(dealVersionFilter || dealPackagingFilter || dealNumberCatFilter) && (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setDealVersionFilter('')
|
||||||
|
setDealPackagingFilter('')
|
||||||
|
setDealNumberCatFilter('')
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
marginTop: '8px',
|
||||||
|
padding: '4px 10px',
|
||||||
|
borderRadius: '6px',
|
||||||
|
fontSize: '11px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
background: 'rgba(239, 68, 68, 0.2)',
|
||||||
|
color: '#ef4444',
|
||||||
|
border: 'none'
|
||||||
|
}}>
|
||||||
|
✕ 清除筛选
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{dealsLoading ? (
|
{dealsLoading ? (
|
||||||
|
|
@ -682,7 +837,16 @@ export default function List() {
|
||||||
) : filteredDeals.length === 0 ? (
|
) : filteredDeals.length === 0 ? (
|
||||||
<div style={{ textAlign: 'center', padding: '60px 0' }}>
|
<div style={{ textAlign: 'center', padding: '60px 0' }}>
|
||||||
<div style={{ fontSize: '50px', opacity: 0.3 }}>📊</div>
|
<div style={{ fontSize: '50px', opacity: 0.3 }}>📊</div>
|
||||||
<div style={{ color: '#64748b', marginTop: '16px' }}>{dealSearch ? '没有匹配的行情' : '暂无行情记录'}</div>
|
<div style={{ color: '#64748b', marginTop: '16px' }}>
|
||||||
|
{(dealSearch || dealVersionFilter || dealPackagingFilter || dealNumberCatFilter)
|
||||||
|
? '没有匹配的行情'
|
||||||
|
: '暂无行情记录'}
|
||||||
|
</div>
|
||||||
|
{(dealSearch || dealVersionFilter || dealPackagingFilter || dealNumberCatFilter) && (
|
||||||
|
<div style={{ color: '#64748b', fontSize: '11px', marginTop: '8px' }}>
|
||||||
|
已选筛选:{dealSearch && `搜索"${dealSearch}" `}{dealVersionFilter && `版别${dealVersionFilter} `}{dealPackagingFilter && `包装${dealPackagingFilter} `}{dealNumberCatFilter && `号码${dealNumberCatFilter}`}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
/**
|
/**
|
||||||
* Purchase - 我的认购/团购页面
|
* Purchase - 我的认购/团购页面
|
||||||
* Version: 0.0.2 (2026-04-30)
|
* Version: 0.2.0 (2026-05-01)
|
||||||
* 认购群管理、加入、查看详情
|
* 认购群管理、加入、待审批
|
||||||
* 更新:创建群表单新增6个字段(群名、介绍、人数、总数、周期、开始日期)
|
* 更新:待审批Tab、成员审批、申请状态显示
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
|
|
@ -20,6 +20,9 @@ function Purchase() {
|
||||||
const [groupMembers, setGroupMembers] = useState([])
|
const [groupMembers, setGroupMembers] = useState([])
|
||||||
const [groupCollections, setGroupCollections] = useState([])
|
const [groupCollections, setGroupCollections] = useState([])
|
||||||
const [showCollectionForm, setShowCollectionForm] = useState(false)
|
const [showCollectionForm, setShowCollectionForm] = useState(false)
|
||||||
|
const [editingMember, setEditingMember] = useState(null)
|
||||||
|
const [editingGroup, setEditingGroup] = useState(null)
|
||||||
|
const [viewingMember, setViewingMember] = useState(null)
|
||||||
const [newGroup, setNewGroup] = useState({
|
const [newGroup, setNewGroup] = useState({
|
||||||
name: '',
|
name: '',
|
||||||
description: '',
|
description: '',
|
||||||
|
|
@ -30,7 +33,10 @@ function Purchase() {
|
||||||
})
|
})
|
||||||
const [newCollection, setNewCollection] = useState({ number: 'J0________', price: '' })
|
const [newCollection, setNewCollection] = useState({ number: 'J0________', price: '' })
|
||||||
const [loading, setLoading] = useState(true)
|
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(() => {
|
useEffect(() => {
|
||||||
const userData = localStorage.getItem('user')
|
const userData = localStorage.getItem('user')
|
||||||
|
|
@ -47,6 +53,16 @@ function Purchase() {
|
||||||
const fetchGroups = async () => {
|
const fetchGroups = async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
const token = localStorage.getItem('token')
|
const token = localStorage.getItem('token')
|
||||||
|
const userData = localStorage.getItem('user')
|
||||||
|
|
||||||
|
// 未登录:直接返回,页面不黑屏
|
||||||
|
if (!token || !userData) {
|
||||||
|
setAllGroups([])
|
||||||
|
setMyGroups([])
|
||||||
|
setLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 获取我参与的认购群
|
// 获取我参与的认购群
|
||||||
const myRes = await fetch(`${API_BASE}/api/purchase/my-groups`, {
|
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 allRes = await fetch(`${API_BASE}/api/purchase/groups`)
|
||||||
const allData = await allRes.json()
|
const allData = await allRes.json()
|
||||||
setAllGroups(allData || [])
|
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) {
|
} catch (e) {
|
||||||
console.error('获取认购群失败:', e)
|
console.error('获取认购群失败:', e)
|
||||||
}
|
}
|
||||||
|
|
@ -110,7 +154,7 @@ function Purchase() {
|
||||||
})
|
})
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
alert('加入成功!')
|
alert(data.message || '已申请,等待群主审批')
|
||||||
fetchGroups()
|
fetchGroups()
|
||||||
} else {
|
} else {
|
||||||
alert(data.detail || '加入失败')
|
alert(data.detail || '加入失败')
|
||||||
|
|
@ -205,6 +249,7 @@ function Purchase() {
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
alert('提交成功!')
|
alert('提交成功!')
|
||||||
setNewCollection({ number: 'J0________', price: '' })
|
setNewCollection({ number: 'J0________', price: '' })
|
||||||
|
setShowCollectionForm(false)
|
||||||
viewGroupDetail(selectedGroup.id)
|
viewGroupDetail(selectedGroup.id)
|
||||||
fetchGroups()
|
fetchGroups()
|
||||||
} else {
|
} 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) => {
|
const formatMoney = (val) => {
|
||||||
if (!val || val === 0) return '¥0'
|
if (!val || val === 0) return '¥0'
|
||||||
return '¥' + parseFloat(val).toLocaleString()
|
return '¥' + parseFloat(val).toLocaleString()
|
||||||
|
|
@ -248,16 +389,18 @@ function Purchase() {
|
||||||
// 渲染认购群卡片
|
// 渲染认购群卡片
|
||||||
const renderGroupCard = (group, showJoin = false) => {
|
const renderGroupCard = (group, showJoin = false) => {
|
||||||
const isMyGroup = myGroups.some(g => g.id === group.id)
|
const isMyGroup = myGroups.some(g => g.id === group.id)
|
||||||
|
const isPendingGroup = pendingGroups.some(g => g.id === group.id) // 待审批
|
||||||
const isClosed = group.status === 'closed'
|
const isClosed = group.status === 'closed'
|
||||||
|
const isApplied = isPendingGroup && !isMyGroup
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={group.id}
|
key={group.id}
|
||||||
onClick={() => viewGroupDetail(group.id)}
|
onClick={() => viewGroupDetail(group.id)}
|
||||||
style={{
|
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',
|
borderRadius: '12px',
|
||||||
padding: '16px',
|
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',
|
cursor: 'pointer',
|
||||||
marginBottom: '12px',
|
marginBottom: '12px',
|
||||||
opacity: isClosed ? 0.7 : 1
|
opacity: isClosed ? 0.7 : 1
|
||||||
|
|
@ -274,6 +417,8 @@ function Purchase() {
|
||||||
<div style={{ background: '#555', color: '#aaa', borderRadius: '6px', padding: '8px 12px', fontSize: '12px' }}>已结束</div>
|
<div style={{ background: '#555', color: '#aaa', borderRadius: '6px', padding: '8px 12px', fontSize: '12px' }}>已结束</div>
|
||||||
) : isMyGroup ? (
|
) : isMyGroup ? (
|
||||||
<div style={{ background: '#10b981', color: '#fff', borderRadius: '6px', padding: '8px 16px', fontSize: '13px' }}>已加入</div>
|
<div style={{ background: '#10b981', color: '#fff', borderRadius: '6px', padding: '8px 16px', fontSize: '13px' }}>已加入</div>
|
||||||
|
) : isApplied ? (
|
||||||
|
<div style={{ background: '#fbbf24', color: '#000', borderRadius: '6px', padding: '8px 16px', fontSize: '13px' }}>待审批</div>
|
||||||
) : showJoin && (
|
) : showJoin && (
|
||||||
<button
|
<button
|
||||||
onClick={(e) => { e.stopPropagation(); joinGroup(group.id) }}
|
onClick={(e) => { e.stopPropagation(); joinGroup(group.id) }}
|
||||||
|
|
@ -287,21 +432,27 @@ function Purchase() {
|
||||||
cursor: 'pointer'
|
cursor: 'pointer'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
加入
|
申请加入
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{isMyGroup && !isClosed && (
|
||||||
<div style={{ display: 'flex', gap: '16px', marginTop: '12px', color: 'rgba(255,255,255,0.7)', fontSize: '13px' }}>
|
<div style={{ display: 'flex', gap: '16px', marginTop: '12px', color: 'rgba(255,255,255,0.7)', fontSize: '13px' }}>
|
||||||
<span>藏品: {group.total_collections}件</span>
|
<span>藏品: {group.total_collections}件</span>
|
||||||
<span>总价: {formatMoney(group.total_amount)}</span>
|
<span>总价: {formatMoney(group.total_amount)}</span>
|
||||||
<span>均价: {formatMoney(group.avg_price)}</span>
|
<span>均价: {formatMoney(group.avg_price)}</span>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 群详情弹窗
|
// 群详情弹窗
|
||||||
if (selectedGroup) {
|
if (selectedGroup) {
|
||||||
|
const isMyGroup = myGroups.some(g => g.id === selectedGroup.id)
|
||||||
|
const isClosed = selectedGroup.status === 'closed'
|
||||||
|
const canViewDetails = isMyGroup && !isClosed
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
minHeight: '100vh', overflowY: 'auto', WebkitOverflowScrolling: 'touch',
|
minHeight: '100vh', overflowY: 'auto', WebkitOverflowScrolling: 'touch',
|
||||||
|
|
@ -346,9 +497,70 @@ function Purchase() {
|
||||||
结束
|
结束
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 编辑按钮:群主修改群信息,成员修改个人信息 */}
|
||||||
|
{user && (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
if (selectedGroup.creator_id === user.f99_90_id) {
|
||||||
|
// 群主:修改群信息
|
||||||
|
setEditingGroup({
|
||||||
|
name: selectedGroup.name,
|
||||||
|
description: selectedGroup.description || '',
|
||||||
|
cycle_months: selectedGroup.cycle_months || '',
|
||||||
|
total_members: selectedGroup.total_members || '',
|
||||||
|
total_collections: selectedGroup.total_collections || ''
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// 成员:修改个人信息(包含群主可修改的字段)
|
||||||
|
const member = groupMembers.find(m => m.user_id === user.f99_90_id)
|
||||||
|
setEditingMember({
|
||||||
|
user_name: user.f01_01_name,
|
||||||
|
wechat_name: member?.wechat_name || '',
|
||||||
|
contact: member?.contact || '',
|
||||||
|
purchase_count: member?.purchase_count || 0,
|
||||||
|
paid_amount: member?.paid_amount || 0,
|
||||||
|
notes: member?.notes || ''
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
background: '#3b82f6',
|
||||||
|
color: '#fff',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '6px',
|
||||||
|
padding: '8px 12px',
|
||||||
|
fontSize: '13px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
marginLeft: '8px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{selectedGroup.creator_id === user.f99_90_id ? '修改群信息' : '修改我的信息'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 统计卡片 */}
|
{/* 群介绍 - 所有人都可见 */}
|
||||||
|
{selectedGroup.description && (
|
||||||
|
<div style={{ marginBottom: '16px', padding: '12px', background: 'rgba(255,255,255,0.05)', borderRadius: '8px' }}>
|
||||||
|
<div style={{ color: 'rgba(255,255,255,0.7)', fontSize: '12px', marginBottom: '4px' }}>群介绍</div>
|
||||||
|
<div style={{ color: '#fff', fontSize: '14px' }}>{selectedGroup.description}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 未加入:显示申请加入按钮 */}
|
||||||
|
{!isMyGroup && !isClosed && (
|
||||||
|
<button
|
||||||
|
onClick={() => joinGroup(selectedGroup.id)}
|
||||||
|
style={{ width: '100%', background: '#3b82f6', color: '#fff', border: 'none', borderRadius: '8px', padding: '14px', fontSize: '16px', fontWeight: 'bold', cursor: 'pointer', marginBottom: '20px' }}
|
||||||
|
>
|
||||||
|
申请加入该认购群
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 敏感信息:只有已加入的群才可见 */}
|
||||||
|
{canViewDetails ? (
|
||||||
|
<>
|
||||||
<div style={{
|
<div style={{
|
||||||
background: 'linear-gradient(135deg, rgba(245,158,11,0.15) 0%, rgba(245,158,11,0.05) 100%)',
|
background: 'linear-gradient(135deg, rgba(245,158,11,0.15) 0%, rgba(245,158,11,0.05) 100%)',
|
||||||
borderRadius: '12px',
|
borderRadius: '12px',
|
||||||
|
|
@ -382,13 +594,17 @@ function Purchase() {
|
||||||
<div style={{ color: 'rgba(255,255,255,0.7)', fontSize: '14px', marginBottom: '12px' }}>参与成员</div>
|
<div style={{ color: 'rgba(255,255,255,0.7)', fontSize: '14px', marginBottom: '12px' }}>参与成员</div>
|
||||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
|
||||||
{groupMembers.map(m => (
|
{groupMembers.map(m => (
|
||||||
<div key={m.id} style={{
|
<div
|
||||||
|
key={m.id}
|
||||||
|
onClick={() => setViewingMember(m)}
|
||||||
|
style={{
|
||||||
background: 'rgba(255,255,255,0.1)',
|
background: 'rgba(255,255,255,0.1)',
|
||||||
borderRadius: '20px',
|
borderRadius: '20px',
|
||||||
padding: '6px 12px',
|
padding: '6px 12px',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: '6px'
|
gap: '6px',
|
||||||
|
cursor: 'pointer'
|
||||||
}}>
|
}}>
|
||||||
<div style={{ width: '20px', height: '20px', borderRadius: '50%', background: '#3b82f6', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '10px' }}>
|
<div style={{ width: '20px', height: '20px', borderRadius: '50%', background: '#3b82f6', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '10px' }}>
|
||||||
{m.user_name?.charAt(0) || '?'}
|
{m.user_name?.charAt(0) || '?'}
|
||||||
|
|
@ -429,7 +645,7 @@ function Purchase() {
|
||||||
<span style={{ color: c.paid ? '#10b981' : '#ef4444', fontSize: '11px' }}>
|
<span style={{ color: c.paid ? '#10b981' : '#ef4444', fontSize: '11px' }}>
|
||||||
{c.paid ? '✓已结清' : '○未结清'}
|
{c.paid ? '✓已结清' : '○未结清'}
|
||||||
</span>
|
</span>
|
||||||
{user && selectedGroup && selectedGroup.creator_id === user.id && (
|
{user && selectedGroup && selectedGroup.creator_id === user.f99_90_id && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setEditCollection({...c})}
|
onClick={() => setEditCollection({...c})}
|
||||||
style={{ background: '#3b82f6', color: '#fff', border: 'none', borderRadius: '4px', padding: '4px 8px', fontSize: '10px' }}
|
style={{ background: '#3b82f6', color: '#fff', border: 'none', borderRadius: '4px', padding: '4px 8px', fontSize: '10px' }}
|
||||||
|
|
@ -440,10 +656,188 @@ function Purchase() {
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{groupCollections.length === 0 && <div style={{ color: 'rgba(255,255,255,0.3)', fontSize: '13px', textAlign: 'center', padding: '20px' }}>暂无认购藏品</div>}
|
{groupCollections.length === 0 && <div style={{ color: 'rgba(255,255,255,0.3)', fontSize: '13px', textAlign: 'center', padding: '20px' }}>暂无认购藏品</div>}
|
||||||
{selectedGroup && user && selectedGroup.creator_id === user.id && selectedGroup.status === 'active' && (
|
{selectedGroup && user && selectedGroup.creator_id === user.f99_90_id && selectedGroup.status === 'active' && (
|
||||||
<button onClick={() => setShowCollectionForm(!showCollectionForm)} style={{ marginTop: '12px', width: '100%', background: showCollectionForm ? '#ef4444' : '#3b82f6', color: '#fff', border: 'none', borderRadius: '8px', padding: '12px', fontSize: '14px', fontWeight: 'bold', cursor: 'pointer' }}>{showCollectionForm ? '收起' : '录入认购藏品'}</button>
|
<button onClick={() => setShowCollectionForm(!showCollectionForm)} style={{ marginTop: '12px', width: '100%', background: showCollectionForm ? '#ef4444' : '#3b82f6', color: '#fff', border: 'none', borderRadius: '8px', padding: '12px', fontSize: '14px', fontWeight: 'bold', cursor: 'pointer' }}>{showCollectionForm ? '收起' : '录入认购藏品'}</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{/* 修改成员信息弹窗 */}
|
||||||
|
{editingMember && (
|
||||||
|
<div style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(0,0,0,0.8)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000 }}>
|
||||||
|
<div style={{ background: '#1e293b', borderRadius: '12px', padding: '20px', width: '90%', maxWidth: '400px' }}>
|
||||||
|
<div style={{ color: '#fff', fontSize: '16px', marginBottom: '16px', fontWeight: 'bold' }}>修改个人信息</div>
|
||||||
|
|
||||||
|
<div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '4px' }}>用户名</div>
|
||||||
|
<input
|
||||||
|
value={editingMember.user_name || ''}
|
||||||
|
onChange={(e) => 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' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '4px' }}>微信名</div>
|
||||||
|
<input
|
||||||
|
value={editingMember.wechat_name || ''}
|
||||||
|
onChange={(e) => 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' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '4px' }}>联系方式</div>
|
||||||
|
<input
|
||||||
|
value={editingMember.contact || ''}
|
||||||
|
onChange={(e) => 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 && (
|
||||||
|
<>
|
||||||
|
<div style={{ fontSize: '12px', color: '#fbbf24', marginBottom: '4px' }}>认购数量(仅群主可修改)</div>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={editingMember.purchase_count || 0}
|
||||||
|
onChange={(e) => 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' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div style={{ fontSize: '12px', color: '#fbbf24', marginBottom: '4px' }}>已付金额(仅群主可修改)</div>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={editingMember.paid_amount || 0}
|
||||||
|
onChange={(e) => 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' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div style={{ fontSize: '12px', color: '#fbbf24', marginBottom: '4px' }}>备注(仅群主可修改)</div>
|
||||||
|
<textarea
|
||||||
|
value={editingMember.notes || ''}
|
||||||
|
onChange={(e) => setEditingMember({...editingMember, notes: e.target.value})}
|
||||||
|
placeholder="备注"
|
||||||
|
rows={3}
|
||||||
|
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', resize: 'none' }}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', gap: '8px' }}>
|
||||||
|
<button
|
||||||
|
onClick={() => setEditingMember(null)}
|
||||||
|
style={{ flex: 1, background: 'rgba(255,255,255,0.1)', color: '#fff', border: 'none', borderRadius: '8px', padding: '12px', fontSize: '14px', cursor: 'pointer' }}
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={updateMember}
|
||||||
|
style={{ flex: 1, background: '#3b82f6', color: '#fff', border: 'none', borderRadius: '8px', padding: '12px', fontSize: '14px', fontWeight: 'bold', cursor: 'pointer' }}
|
||||||
|
>
|
||||||
|
保存
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 查看成员信息弹窗 */}
|
||||||
|
{viewingMember && (
|
||||||
|
<div style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(0,0,0,0.8)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000 }}>
|
||||||
|
<div style={{ background: '#1e293b', borderRadius: '12px', padding: '20px', width: '90%', maxWidth: '400px' }}>
|
||||||
|
<div style={{ color: '#fff', fontSize: '18px', marginBottom: '16px', fontWeight: 'bold', textAlign: 'center', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
|
<span></span>
|
||||||
|
<span>{viewingMember.user_name}</span>
|
||||||
|
<button onClick={() => setViewingMember(null)} style={{ background: 'transparent', border: 'none', color: '#fff', fontSize: '20px', cursor: 'pointer' }}>×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ background: 'rgba(255,255,255,0.05)', borderRadius: '8px', padding: '12px', marginBottom: '12px' }}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '8px' }}>
|
||||||
|
<span style={{ color: 'rgba(255,255,255,0.5)', fontSize: '12px' }}>微信名</span>
|
||||||
|
<span style={{ color: '#fff', fontSize: '14px' }}>{viewingMember.wechat_name || '-'}</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '8px' }}>
|
||||||
|
<span style={{ color: 'rgba(255,255,255,0.5)', fontSize: '12px' }}>联系方式</span>
|
||||||
|
<span style={{ color: '#fff', fontSize: '14px' }}>{viewingMember.contact || '-'}</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '8px' }}>
|
||||||
|
<span style={{ color: 'rgba(255,255,255,0.5)', fontSize: '12px' }}>角色</span>
|
||||||
|
<span style={{ color: viewingMember.role === 'creator' ? '#fbbf24' : '#fff', fontSize: '14px' }}>{viewingMember.role === 'creator' ? '👑 群主' : '成员'}</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '8px' }}>
|
||||||
|
<span style={{ color: 'rgba(255,255,255,0.5)', fontSize: '12px' }}>状态</span>
|
||||||
|
<span style={{ color: viewingMember.status === 'approved' ? '#10b981' : '#fbbf24', fontSize: '14px' }}>{viewingMember.status === 'approved' ? '已确认' : '待审核'}</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '8px' }}>
|
||||||
|
<span style={{ color: 'rgba(255,255,255,0.5)', fontSize: '12px' }}>认购数量</span>
|
||||||
|
<span style={{ color: '#fff', fontSize: '14px' }}>{viewingMember.purchase_count || 0}</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||||
|
<span style={{ color: 'rgba(255,255,255,0.5)', fontSize: '12px' }}>已付金额</span>
|
||||||
|
<span style={{ color: '#10b981', fontSize: '14px' }}>¥{(viewingMember.paid_amount || 0).toLocaleString()}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{viewingMember.notes && (
|
||||||
|
<div style={{ color: 'rgba(255,255,255,0.5)', fontSize: '12px', marginBottom: '4px' }}>备注</div>
|
||||||
|
)}
|
||||||
|
{viewingMember.notes && (
|
||||||
|
<div style={{ color: '#fff', fontSize: '13px', background: 'rgba(255,255,255,0.05)', borderRadius: '8px', padding: '8px' }}>{viewingMember.notes}</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 群主可以编辑该成员 */}
|
||||||
|
{user && selectedGroup && user.f99_90_id === selectedGroup.creator_id && viewingMember.role !== 'creator' && (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setViewingMember(null)
|
||||||
|
setEditingMember({
|
||||||
|
user_name: viewingMember.user_name,
|
||||||
|
wechat_name: viewingMember.wechat_name || '',
|
||||||
|
contact: viewingMember.contact || '',
|
||||||
|
purchase_count: viewingMember.purchase_count || 0,
|
||||||
|
paid_amount: viewingMember.paid_amount || 0,
|
||||||
|
notes: viewingMember.notes || ''
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
style={{ marginTop: '16px', width: '100%', background: '#3b82f6', color: '#fff', border: 'none', borderRadius: '8px', padding: '12px', fontSize: '14px', fontWeight: 'bold', cursor: 'pointer' }}
|
||||||
|
>
|
||||||
|
编辑该成员信息
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 修改群信息弹窗 */}
|
||||||
|
{editingGroup && (
|
||||||
|
<div style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(0,0,0,0.8)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000 }}>
|
||||||
|
<div style={{ background: '#1e293b', borderRadius: '12px', padding: '20px', width: '90%', maxWidth: '400px' }}>
|
||||||
|
<div style={{ color: '#fff', fontSize: '16px', marginBottom: '16px', fontWeight: 'bold' }}>修改群信息</div>
|
||||||
|
|
||||||
|
<div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '4px' }}>群名称</div>
|
||||||
|
<input value={editingGroup.name || ''} onChange={(e) => 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' }} />
|
||||||
|
|
||||||
|
<div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '4px' }}>群介绍</div>
|
||||||
|
<textarea value={editingGroup.description || ''} onChange={(e) => setEditingGroup({...editingGroup, description: e.target.value})} placeholder="群介绍" rows={3} 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', resize: 'none' }} />
|
||||||
|
|
||||||
|
<div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '4px' }}>参与人数</div>
|
||||||
|
<input type="number" value={editingGroup.total_members || ''} onChange={(e) => setEditingGroup({...editingGroup, total_members: 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' }} />
|
||||||
|
|
||||||
|
<div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '4px' }}>总认购数</div>
|
||||||
|
<input type="number" value={editingGroup.total_collections || ''} onChange={(e) => setEditingGroup({...editingGroup, total_collections: 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' }} />
|
||||||
|
|
||||||
|
<div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '4px' }}>认购周期(月)</div>
|
||||||
|
<input type="number" value={editingGroup.cycle_months || ''} onChange={(e) => setEditingGroup({...editingGroup, cycle_months: 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: '16px' }} />
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', gap: '8px' }}>
|
||||||
|
<button onClick={() => setEditingGroup(null)} style={{ flex: 1, background: 'rgba(255,255,255,0.1)', color: '#fff', border: 'none', borderRadius: '8px', padding: '12px', fontSize: '14px', cursor: 'pointer' }}>取消</button>
|
||||||
|
<button onClick={updateGroup} style={{ flex: 1, background: '#3b82f6', color: '#fff', border: 'none', borderRadius: '8px', padding: '12px', fontSize: '14px', fontWeight: 'bold', cursor: 'pointer' }}>保存</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 编辑弹窗 */}
|
{/* 编辑弹窗 */}
|
||||||
{editCollection && (
|
{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 && (
|
||||||
<div style={{
|
<div style={{
|
||||||
marginTop: '16px',
|
marginTop: '16px',
|
||||||
marginBottom: '80px',
|
marginBottom: '80px',
|
||||||
|
|
@ -553,19 +947,17 @@ function Purchase() {
|
||||||
if (el) window.__numberInputs = window.__numberInputs || [];
|
if (el) window.__numberInputs = window.__numberInputs || [];
|
||||||
window.__numberInputs[i] = el;
|
window.__numberInputs[i] = el;
|
||||||
}}
|
}}
|
||||||
|
maxLength={1}
|
||||||
defaultValue={i === 0 ? '0' : ''}
|
defaultValue={i === 0 ? '0' : ''}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
let v = e.target.value
|
let v = e.target.value.replace(/\D/g, '').slice(-1)
|
||||||
if (v) {
|
if (v || i === 0) {
|
||||||
v = v.replace(/\D/g, '')
|
|
||||||
if (v) {
|
|
||||||
const num = newCollection.number || 'J0________'
|
const num = newCollection.number || 'J0________'
|
||||||
const arr = num.split('')
|
const arr = num.split('')
|
||||||
arr[i+1] = v
|
arr[i+1] = i === 0 ? '0' : (v || '_')
|
||||||
setNewCollection({...newCollection, number: arr.join('')})
|
setNewCollection({...newCollection, number: arr.join('')})
|
||||||
if (i < 8 && v) window.__numberInputs[i+1]?.focus()
|
if (i < 8 && v) window.__numberInputs[i+1]?.focus()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === 'Backspace' && (!newCollection.number || !newCollection.number[i+1]) && i > 0)
|
if (e.key === 'Backspace' && (!newCollection.number || !newCollection.number[i+1]) && i > 0)
|
||||||
|
|
@ -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' && (
|
||||||
<div style={{ marginTop: '16px', marginBottom: '80px', background: '#1e293b', borderRadius: '12px', padding: '16px', textAlign: 'center', color: 'rgba(255,255,255,0.5)' }}>
|
<div style={{ marginTop: '16px', marginBottom: '80px', background: '#1e293b', borderRadius: '12px', padding: '16px', textAlign: 'center', color: 'rgba(255,255,255,0.5)' }}>
|
||||||
仅群主可提交认购藏品
|
仅群主可提交认购藏品
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -725,11 +1117,53 @@ function Purchase() {
|
||||||
>
|
>
|
||||||
全部认购群 ({allGroups.length})
|
全部认购群 ({allGroups.length})
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('pending')}
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
background: activeTab === 'pending' ? '#fbbf24' : 'rgba(255,255,255,0.1)',
|
||||||
|
color: activeTab === 'pending' ? '#000' : '#fff',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '8px',
|
||||||
|
padding: '12px',
|
||||||
|
fontSize: '14px',
|
||||||
|
cursor: 'pointer'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
待审批 {pendingGroups.length > 0 && `(${Object.values(pendingMembersMap).reduce((a, arr) => a + (Array.isArray(arr) ? arr.length : 0), 0)})`}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 认购群列表 */}
|
{/* 认购群列表 */}
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div style={{ textAlign: 'center', padding: '40px', color: 'rgba(255,255,255,0.5)' }}>加载中...</div>
|
<div style={{ textAlign: 'center', padding: '40px', color: 'rgba(255,255,255,0.5)' }}>加载中...</div>
|
||||||
|
) : activeTab === 'pending' ? (
|
||||||
|
<div>
|
||||||
|
{pendingGroups.length === 0 ? (
|
||||||
|
<div style={{ textAlign: 'center', padding: '40px', color: 'rgba(255,255,255,0.3)' }}>暂无待审批成员</div>
|
||||||
|
) : pendingGroups.map(group =>
|
||||||
|
<div key={group.id} style={{ background: 'rgba(251,191,36,0.15)', borderRadius: '12px', padding: '16px', marginBottom: '16px' }}>
|
||||||
|
<div style={{ color: '#fff', fontSize: '16px', fontWeight: 'bold', marginBottom: '12px' }}>{group.name} - 待审批成员</div>
|
||||||
|
{(pendingMembersMap[group.id] || []).map(m => {
|
||||||
|
const isProcessed = processedMembers[m.id]
|
||||||
|
return (
|
||||||
|
<div key={m.id} style={{ background: isProcessed ? 'rgba(100,100,100,0.3)' : 'rgba(255,255,255,0.05)', borderRadius: '8px', padding: '12px', marginBottom: '8px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', opacity: isProcessed ? 0.6 : 1 }}>
|
||||||
|
<div>
|
||||||
|
<div style={{ color: '#fff', fontSize: '14px', fontWeight: 'bold' }}>{m.user_name}</div>
|
||||||
|
<div style={{ color: 'rgba(255,255,255,0.5)', fontSize: '12px', marginTop: '4px' }}>{m.wechat_name || '未填写微信名'} · {m.contact || '未填写联系方式'}</div>
|
||||||
|
{isProcessed && <div style={{ color: '#fbbf24', fontSize: '12px', marginTop: '4px' }}>已{isProcessed === 'approve' ? '批准' : '拒绝'}</div>}
|
||||||
|
</div>
|
||||||
|
{!isProcessed && (
|
||||||
|
<div style={{ display: 'flex', gap: '8px' }}>
|
||||||
|
<button onClick={() => approveMember(m.id, group.id, 'reject')} style={{ background: '#ef4444', color: '#fff', border: 'none', borderRadius: '6px', padding: '8px 16px', fontSize: '12px', cursor: 'pointer' }}>拒绝</button>
|
||||||
|
<button onClick={() => approveMember(m.id, group.id, 'approve')} style={{ background: '#10b981', color: '#fff', border: 'none', borderRadius: '6px', padding: '8px 16px', fontSize: '12px', fontWeight: 'bold', cursor: 'pointer' }}>同意</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div>
|
<div>
|
||||||
{(activeTab === 'my' ? myGroups : allGroups).map(group =>
|
{(activeTab === 'my' ? myGroups : allGroups).map(group =>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue