v0.1.6 - 成交行情页面增强:新增搜索+版别+包装+号码分类筛选

This commit is contained in:
甲辰生产 2026-05-01 10:00:17 +08:00
parent f695dfbfa0
commit 3eb8aac2be
8 changed files with 912 additions and 201 deletions

View File

@ -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(
# 更新:

View File

@ -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:

View File

@ -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),

View File

@ -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. 圆圆号无1234570689四个数字任意组合
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. 金马王无1234705689组合必须有5排除1
if unique <= {'0', '5', '6', '8', '9'} and '5' in unique and '1' not in unique:
return "金马王"
# 7. 金马号无2347015689组合必须有1和5包含1
if unique <= {'0', '1', '5', '6', '8', '9'} and '1' in unique and '5' in unique:
return "金马号"
# 8. 金山王无1245703689组合必须有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. 天马王无1247035689组合必须有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. 金山号无2457013689组合必须有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. 天马号无2470135689组合必须有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 []

View File

@ -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个必填字段
- 认购群名称

View File

@ -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"

View File

@ -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() {
{/* 行情搜索框 */}
<div style={{ marginBottom: '12px' }}>
<input type="text"
placeholder="🔍 搜索行情..."
placeholder="🔍 搜索冠字号、编号、价格..."
value={dealSearch}
onChange={e => 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 && (
<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>
{dealsLoading ? (
@ -682,7 +837,16 @@ export default function List() {
) : filteredDeals.length === 0 ? (
<div style={{ textAlign: 'center', padding: '60px 0' }}>
<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 style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>

View File

@ -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 (
<div
key={group.id}
onClick={() => 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() {
<div style={{ background: '#555', color: '#aaa', borderRadius: '6px', padding: '8px 12px', fontSize: '12px' }}>已结束</div>
) : isMyGroup ? (
<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 && (
<button
onClick={(e) => { e.stopPropagation(); joinGroup(group.id) }}
@ -287,21 +432,27 @@ function Purchase() {
cursor: 'pointer'
}}
>
加入
申请加入
</button>
)}
</div>
{isMyGroup && !isClosed && (
<div style={{ display: 'flex', gap: '16px', marginTop: '12px', color: 'rgba(255,255,255,0.7)', fontSize: '13px' }}>
<span>藏品: {group.total_collections}</span>
<span>总价: {formatMoney(group.total_amount)}</span>
<span>均价: {formatMoney(group.avg_price)}</span>
</div>
)}
</div>
)
}
//
if (selectedGroup) {
const isMyGroup = myGroups.some(g => g.id === selectedGroup.id)
const isClosed = selectedGroup.status === 'closed'
const canViewDetails = isMyGroup && !isClosed
return (
<div style={{
minHeight: '100vh', overflowY: 'auto', WebkitOverflowScrolling: 'touch',
@ -346,9 +497,70 @@ function Purchase() {
结束
</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>
{/* 统计卡片 */}
{/* 群介绍 - 所有人都可见 */}
{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={{
background: 'linear-gradient(135deg, rgba(245,158,11,0.15) 0%, rgba(245,158,11,0.05) 100%)',
borderRadius: '12px',
@ -382,13 +594,17 @@ function Purchase() {
<div style={{ color: 'rgba(255,255,255,0.7)', fontSize: '14px', marginBottom: '12px' }}>参与成员</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
{groupMembers.map(m => (
<div key={m.id} style={{
background: 'rgba(255,255,255,0.1)',
borderRadius: '20px',
padding: '6px 12px',
display: 'flex',
alignItems: 'center',
gap: '6px'
<div
key={m.id}
onClick={() => setViewingMember(m)}
style={{
background: 'rgba(255,255,255,0.1)',
borderRadius: '20px',
padding: '6px 12px',
display: 'flex',
alignItems: 'center',
gap: '6px',
cursor: 'pointer'
}}>
<div style={{ width: '20px', height: '20px', borderRadius: '50%', background: '#3b82f6', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '10px' }}>
{m.user_name?.charAt(0) || '?'}
@ -429,7 +645,7 @@ function Purchase() {
<span style={{ color: c.paid ? '#10b981' : '#ef4444', fontSize: '11px' }}>
{c.paid ? '✓已结清' : '○未结清'}
</span>
{user && selectedGroup && selectedGroup.creator_id === user.id && (
{user && selectedGroup && selectedGroup.creator_id === user.f99_90_id && (
<button
onClick={() => setEditCollection({...c})}
style={{ background: '#3b82f6', color: '#fff', border: 'none', borderRadius: '4px', padding: '4px 8px', fontSize: '10px' }}
@ -440,10 +656,188 @@ function Purchase() {
</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>
)}
</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 && (
@ -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={{
marginTop: '16px',
marginBottom: '80px',
@ -553,18 +947,16 @@ function Purchase() {
if (el) window.__numberInputs = window.__numberInputs || [];
window.__numberInputs[i] = el;
}}
maxLength={1}
defaultValue={i === 0 ? '0' : ''}
onChange={(e) => {
let v = e.target.value
if (v) {
v = v.replace(/\D/g, '')
if (v) {
const num = newCollection.number || 'J0________'
const arr = num.split('')
arr[i+1] = v
setNewCollection({...newCollection, number: arr.join('')})
if (i < 8 && v) window.__numberInputs[i+1]?.focus()
}
let v = e.target.value.replace(/\D/g, '').slice(-1)
if (v || i === 0) {
const num = newCollection.number || 'J0________'
const arr = num.split('')
arr[i+1] = i === 0 ? '0' : (v || '_')
setNewCollection({...newCollection, number: arr.join('')})
if (i < 8 && v) window.__numberInputs[i+1]?.focus()
}
}}
onKeyDown={(e) => {
@ -658,7 +1050,7 @@ function Purchase() {
)}
{/* 非群主提示 */}
{selectedGroup && user && !(selectedGroup.creator_id === user.id) && selectedGroup.status === 'active' && (
{selectedGroup && user && !(selectedGroup.creator_id === user.f99_90_id) && selectedGroup.status === 'active' && (
<div style={{ marginTop: '16px', marginBottom: '80px', background: '#1e293b', borderRadius: '12px', padding: '16px', textAlign: 'center', color: 'rgba(255,255,255,0.5)' }}>
仅群主可提交认购藏品
</div>
@ -725,11 +1117,53 @@ function Purchase() {
>
全部认购群 ({allGroups.length})
</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>
{/* 认购群列表 */}
{loading ? (
<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>
{(activeTab === 'my' ? myGroups : allGroups).map(group =>