diff --git a/backend/app/routers/purchase.py b/backend/app/routers/purchase.py index a5478da..22c9322 100644 --- a/backend/app/routers/purchase.py +++ b/backend/app/routers/purchase.py @@ -36,17 +36,19 @@ class PurchaseGroupResponse(BaseModel): total_collections: int total_confirmed: int total_amount: float - avg_price: float + avg_price: Optional[float] = None created_at: Optional[datetime] class PurchaseCollectionCreate(BaseModel): - collection_name: str - number: str - grading: str - boss_name: str - price: float - source: str - paid: bool = False + collection_name: Optional[str] = None + collection_code: Optional[str] = None + number: Optional[str] = None + collection_code: Optional[str] = None + grading: Optional[str] = None + boss_name: Optional[str] = None + price: Optional[float] = None + source: Optional[str] = None + paid: Optional[bool] = False class PurchaseCollectionUpdate(BaseModel): collection_name: Optional[str] = None @@ -65,11 +67,12 @@ class PurchaseCollectionResponse(BaseModel): user_name: Optional[str] collection_name: Optional[str] collection_code: Optional[str] - number: str - grading: str - boss_name: str - price: float - source: str + number: Optional[str] = None + collection_code: Optional[str] = None + grading: Optional[str] = None + boss_name: Optional[str] = None + price: Optional[float] = None + source: Optional[str] = None paid: bool status: str submit_date: Optional[datetime] @@ -123,12 +126,18 @@ def get_my_groups( db: Session = Depends(get_db) ): """获取我参与的认购群""" + if not current_user: + return [] + member_groups = db.query(PurchaseMember).filter( PurchaseMember.user_id == current_user.f99_90_id, PurchaseMember.status == "approved" ).all() group_ids = [m.group_id for m in member_groups] + if not group_ids: + return [] + groups = db.query(PurchaseGroup).filter( PurchaseGroup.id.in_(group_ids) ).all() @@ -370,16 +379,8 @@ def add_collection( if not current_user: raise HTTPException(status_code=401, detail="请先登录") - if not data.number: - raise HTTPException(status_code=400, detail="冠字号必填") - if not data.grading: - raise HTTPException(status_code=400, detail="评级必填") - if not data.boss_name: - raise HTTPException(status_code=400, detail="认购老板必填") - if not data.price: - raise HTTPException(status_code=400, detail="价格必填") - if not data.source: - raise HTTPException(status_code=400, detail="来源必填") + # All fields now optional - frontend will send what it has + # No validation needed as all fields are Optional in schema group = db.query(PurchaseGroup).filter(PurchaseGroup.id == group_id).first() if not group: diff --git a/config/VERSION.json b/config/VERSION.json index 6b8738e..2facab0 100644 --- a/config/VERSION.json +++ b/config/VERSION.json @@ -6,7 +6,7 @@ "version": "0.1.1", "pages": { "Home": "0.1.0", - "Purchase": "0.1.1" + "Purchase": "0.1.2" } }, "backend": { diff --git a/frontend/src/pages/Purchase.jsx b/frontend/src/pages/Purchase.jsx index f0ec259..84b2dfc 100644 --- a/frontend/src/pages/Purchase.jsx +++ b/frontend/src/pages/Purchase.jsx @@ -15,11 +15,12 @@ function Purchase() { const [allGroups, setAllGroups] = useState([]) const [showCreate, setShowCreate] = useState(false) const [selectedGroup, setSelectedGroup] = useState(null) + const [editCollection, setEditCollection] = useState(null) const [groupMembers, setGroupMembers] = useState([]) const [groupCollections, setGroupCollections] = useState([]) const [showCollectionForm, setShowCollectionForm] = useState(false) const [newGroup, setNewGroup] = useState({ name: '', description: '' }) - const [newCollection, setNewCollection] = useState({ collection_name: '', collection_code: '', number: '', price: '' }) + const [newCollection, setNewCollection] = useState({ number: 'J0________', price: '' }) const [loading, setLoading] = useState(true) const [activeTab, setActiveTab] = useState('my') // my/all @@ -132,12 +133,43 @@ function Purchase() { } } + const updateCollection = async () => { + const token = localStorage.getItem('token') + if (!token || !editCollection) { return } + + try { + const res = await fetch(`${API_BASE}/api/purchase/groups/${selectedGroup.id}/collections/${editCollection.id}`, { + method: 'PUT', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + grading: editCollection.grading, + boss_name: editCollection.boss_name, + price: editCollection.price ? parseFloat(editCollection.price) : null, + source: editCollection.source, + paid: editCollection.paid + }) + }) + if (res.ok) { + alert('更新成功!') + setEditCollection(null) + viewGroupDetail(selectedGroup.id) + } else { + alert('更新失败') + } + } catch (e) { + console.error('更新失败:', e) + } + } + const submitCollection = async () => { const token = localStorage.getItem('token') if (!token) { alert('请先登录'); return } if (!selectedGroup) return - if (!newCollection.collection_name || !newCollection.price) { - alert('请填写藏品名称和价格'); return + if (!newCollection.number || !newCollection.price) { + alert('请填写完整信息'); return } try { @@ -148,16 +180,18 @@ function Purchase() { 'Content-Type': 'application/json' }, body: JSON.stringify({ - collection_name: newCollection.collection_name, - collection_code: newCollection.collection_code, number: newCollection.number, - price: parseFloat(newCollection.price) + grading: newCollection.grading, + boss_name: newCollection.boss_name, + price: parseFloat(newCollection.price), + source: newCollection.source, + paid: newCollection.paid || false }) }) const data = await res.json() if (res.ok) { alert('提交成功!') - setNewCollection({ collection_name: '', collection_code: '', number: '', price: '' }) + setNewCollection({ number: 'J0________', price: '' }) viewGroupDetail(selectedGroup.id) fetchGroups() } else { @@ -362,19 +396,33 @@ function Purchase() { background: 'rgba(255,255,255,0.05)', borderRadius: '8px', padding: '12px', - marginBottom: '8px', - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center' + marginBottom: '8px' }}> -
-
{c.collection_name}
-
- {c.number} · {c.user_name} +
+
+ {c.collection_code} + {c.grading}
+
{formatMoney(c.price)}
-
- {formatMoney(c.price)} +
+ 冠字号: {c.number} +
+
+
+ 老板: {c.boss_name} · 来源: {c.source || '-'} +
+
+ + {c.paid ? '✓已结清' : '○未结清'} + + {user && selectedGroup && selectedGroup.creator_id === user.id && ( + + )} +
))} @@ -384,6 +432,90 @@ function Purchase() { )}
+ {/* 编辑弹窗 */} + {editCollection && ( +
+
+
编辑: {editCollection.collection_code}
+
冠字号
+
+ J + {[0,1,2,3,4,5,6,7,8].map(i => ( + { + if (el) window.__editNumberInputs = window.__editNumberInputs || []; + window.__editNumberInputs[i] = el; + }} + maxLength={1} + value={editCollection.number ? editCollection.number[i+1] || '' : ''} + onChange={(e) => { + const v = e.target.value.slice(-1) + if (v) { + const num = editCollection.number || 'J0________' + const arr = num.split('') + arr[i+1] = v + setEditCollection({...editCollection, number: arr.join('')}) + if (i < 8) window.__editNumberInputs?.[i+1]?.focus() + } + }} + onKeyDown={(e) => { + if (e.key === 'Backspace' && !editCollection.number?.[i+1] && i > 0) window.__editNumberInputs?.[i]?.focus() + }} + placeholder="_" + style={{ flex: 1, background: 'rgba(255,255,255,0.1)', border: '1px solid rgba(255,255,255,0.2)', borderRadius: '0', padding: '10px 2px', color: '#fff', fontSize: '12px', textAlign: 'center', minWidth: '20px' }} + /> + ))} +
+ +
评级
+
+ + setEditCollection({...editCollection, price: e.target.value})} placeholder="输入价格" type="number" style={{ background: 'rgba(255,255,255,0.1)', border: '1px solid rgba(255,255,255,0.2)', borderRadius: '6px', padding: '10px', color: '#fff' }} /> +
+ +
认购老板
+
+ + +
+ +
状态
+ +
+ + +
+
+
+ )} + {/* 提交认购藏品表单 - 仅群主可提交 */} {selectedGroup && user && selectedGroup.creator_id === user.id && selectedGroup.status === 'active' && showCollectionForm && (
-
提交认购藏品
-
- setNewCollection({...newCollection, collection_name: e.target.value})} - placeholder="藏品名称" - style={{ - background: 'rgba(255,255,255,0.1)', - border: '1px solid rgba(255,255,255,0.2)', - borderRadius: '6px', - padding: '10px', - color: '#fff', - fontSize: '13px' - }} - /> - setNewCollection({...newCollection, number: e.target.value})} - placeholder="冠字号" - style={{ - background: 'rgba(255,255,255,0.1)', - border: '1px solid rgba(255,255,255,0.2)', - borderRadius: '6px', - padding: '10px', - color: '#fff', - fontSize: '13px' - }} - /> +
提交认购藏品
+ + {/* 冠字号行 */} +
+
冠字号 (J0开头8位)
+
+ J + {[0,1,2,3,4,5,6,7,8].map(i => ( + { + if (el) window.__numberInputs = window.__numberInputs || []; + window.__numberInputs[i] = el; + }} + 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() + } + } + }} + onKeyDown={(e) => { + if (e.key === 'Backspace' && (!newCollection.number || !newCollection.number[i+1]) && i > 0) + window.__numberInputs[i]?.focus() + }} + style={{ flex: 1, background: 'rgba(255,255,255,0.1)', border: '1px solid rgba(255,255,255,0.2)', borderRadius: '0', padding: '10px 2px', color: '#fff', fontSize: '13px', textAlign: 'center', minWidth: '20px' }} + /> + ))} +
-
- setNewCollection({...newCollection, collection_code: e.target.value})} - placeholder="编号(可选)" - style={{ - background: 'rgba(255,255,255,0.1)', - border: '1px solid rgba(255,255,255,0.2)', - borderRadius: '6px', - padding: '10px', - color: '#fff', - fontSize: '13px' - }} - /> + + {/* 评级行 */} +
+
评级
+ +
+ + {/* 认购老板行 */} +
+
认购老板
+ +
+ + {/* 价格行 */} +
+
价格 (元)
setNewCollection({...newCollection, price: e.target.value})} - placeholder="价格" + placeholder="输入价格" type="number" - style={{ - background: 'rgba(255,255,255,0.1)', - border: '1px solid rgba(255,255,255,0.2)', - borderRadius: '6px', - padding: '10px', - color: '#fff', - fontSize: '13px' - }} + style={{ width: '100%', background: 'rgba(255,255,255,0.1)', border: '1px solid rgba(255,255,255,0.2)', borderRadius: '6px', padding: '10px', color: '#fff', fontSize: '13px' }} />
+ + {/* 来源行 */} +
+
来源
+ +
+ + {/* 结清行 */} +
+ +
+