v0.1.2 - 认购功能完善(冠字号J0输入框、编辑弹窗、字段完整)

This commit is contained in:
甲辰生产 2026-04-30 00:39:55 +08:00
parent 5a5ec0ee2c
commit a31d6cc153
3 changed files with 277 additions and 103 deletions

View File

@ -36,17 +36,19 @@ class PurchaseGroupResponse(BaseModel):
total_collections: int total_collections: int
total_confirmed: int total_confirmed: int
total_amount: float total_amount: float
avg_price: float avg_price: Optional[float] = None
created_at: Optional[datetime] created_at: Optional[datetime]
class PurchaseCollectionCreate(BaseModel): class PurchaseCollectionCreate(BaseModel):
collection_name: str collection_name: Optional[str] = None
number: str collection_code: Optional[str] = None
grading: str number: Optional[str] = None
boss_name: str collection_code: Optional[str] = None
price: float grading: Optional[str] = None
source: str boss_name: Optional[str] = None
paid: bool = False price: Optional[float] = None
source: Optional[str] = None
paid: Optional[bool] = False
class PurchaseCollectionUpdate(BaseModel): class PurchaseCollectionUpdate(BaseModel):
collection_name: Optional[str] = None collection_name: Optional[str] = None
@ -65,11 +67,12 @@ class PurchaseCollectionResponse(BaseModel):
user_name: Optional[str] user_name: Optional[str]
collection_name: Optional[str] collection_name: Optional[str]
collection_code: Optional[str] collection_code: Optional[str]
number: str number: Optional[str] = None
grading: str collection_code: Optional[str] = None
boss_name: str grading: Optional[str] = None
price: float boss_name: Optional[str] = None
source: str price: Optional[float] = None
source: Optional[str] = None
paid: bool paid: bool
status: str status: str
submit_date: Optional[datetime] submit_date: Optional[datetime]
@ -123,12 +126,18 @@ def get_my_groups(
db: Session = Depends(get_db) db: Session = Depends(get_db)
): ):
"""获取我参与的认购群""" """获取我参与的认购群"""
if not current_user:
return []
member_groups = db.query(PurchaseMember).filter( member_groups = db.query(PurchaseMember).filter(
PurchaseMember.user_id == current_user.f99_90_id, PurchaseMember.user_id == current_user.f99_90_id,
PurchaseMember.status == "approved" PurchaseMember.status == "approved"
).all() ).all()
group_ids = [m.group_id for m in member_groups] group_ids = [m.group_id for m in member_groups]
if not group_ids:
return []
groups = db.query(PurchaseGroup).filter( groups = db.query(PurchaseGroup).filter(
PurchaseGroup.id.in_(group_ids) PurchaseGroup.id.in_(group_ids)
).all() ).all()
@ -370,16 +379,8 @@ def add_collection(
if not current_user: if not current_user:
raise HTTPException(status_code=401, detail="请先登录") raise HTTPException(status_code=401, detail="请先登录")
if not data.number: # All fields now optional - frontend will send what it has
raise HTTPException(status_code=400, detail="冠字号必填") # No validation needed as all fields are Optional in schema
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="来源必填")
group = db.query(PurchaseGroup).filter(PurchaseGroup.id == group_id).first() group = db.query(PurchaseGroup).filter(PurchaseGroup.id == group_id).first()
if not group: if not group:

View File

@ -6,7 +6,7 @@
"version": "0.1.1", "version": "0.1.1",
"pages": { "pages": {
"Home": "0.1.0", "Home": "0.1.0",
"Purchase": "0.1.1" "Purchase": "0.1.2"
} }
}, },
"backend": { "backend": {

View File

@ -15,11 +15,12 @@ function Purchase() {
const [allGroups, setAllGroups] = useState([]) const [allGroups, setAllGroups] = useState([])
const [showCreate, setShowCreate] = useState(false) const [showCreate, setShowCreate] = useState(false)
const [selectedGroup, setSelectedGroup] = useState(null) const [selectedGroup, setSelectedGroup] = useState(null)
const [editCollection, setEditCollection] = useState(null)
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 [newGroup, setNewGroup] = useState({ name: '', description: '' }) 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 [loading, setLoading] = useState(true)
const [activeTab, setActiveTab] = useState('my') // my/all 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 submitCollection = async () => {
const token = localStorage.getItem('token') const token = localStorage.getItem('token')
if (!token) { alert('请先登录'); return } if (!token) { alert('请先登录'); return }
if (!selectedGroup) return if (!selectedGroup) return
if (!newCollection.collection_name || !newCollection.price) { if (!newCollection.number || !newCollection.price) {
alert('请填写藏品名称和价格'); return alert('请填写完整信息'); return
} }
try { try {
@ -148,16 +180,18 @@ function Purchase() {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify({ body: JSON.stringify({
collection_name: newCollection.collection_name,
collection_code: newCollection.collection_code,
number: newCollection.number, 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() const data = await res.json()
if (res.ok) { if (res.ok) {
alert('提交成功!') alert('提交成功!')
setNewCollection({ collection_name: '', collection_code: '', number: '', price: '' }) setNewCollection({ number: 'J0________', price: '' })
viewGroupDetail(selectedGroup.id) viewGroupDetail(selectedGroup.id)
fetchGroups() fetchGroups()
} else { } else {
@ -362,19 +396,33 @@ function Purchase() {
background: 'rgba(255,255,255,0.05)', background: 'rgba(255,255,255,0.05)',
borderRadius: '8px', borderRadius: '8px',
padding: '12px', padding: '12px',
marginBottom: '8px', marginBottom: '8px'
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}> }}>
<div> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '6px' }}>
<div style={{ color: '#fff', fontSize: '14px' }}>{c.collection_name}</div> <div>
<div style={{ color: 'rgba(255,255,255,0.5)', fontSize: '12px' }}> <span style={{ color: '#fff', fontSize: '14px', fontWeight: 'bold' }}>{c.collection_code}</span>
{c.number} · {c.user_name} <span style={{ color: '#fbbf24', fontSize: '12px', marginLeft: '8px' }}>{c.grading}</span>
</div> </div>
<div style={{ color: '#34d399', fontSize: '16px', fontWeight: 'bold' }}>{formatMoney(c.price)}</div>
</div> </div>
<div style={{ color: '#34d399', fontSize: '16px', fontWeight: 'bold' }}> <div style={{ color: 'rgba(255,255,255,0.7)', fontSize: '12px', marginBottom: '4px' }}>
{formatMoney(c.price)} 冠字号: {c.number}
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div style={{ color: 'rgba(255,255,255,0.6)', fontSize: '11px' }}>
老板: {c.boss_name} · 来源: {c.source || '-'}
</div>
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
<span style={{ color: c.paid ? '#10b981' : '#ef4444', fontSize: '11px' }}>
{c.paid ? '✓已结清' : '○未结清'}
</span>
{user && selectedGroup && selectedGroup.creator_id === user.id && (
<button
onClick={() => setEditCollection({...c})}
style={{ background: '#3b82f6', color: '#fff', border: 'none', borderRadius: '4px', padding: '4px 8px', fontSize: '10px' }}
>编辑</button>
)}
</div>
</div> </div>
</div> </div>
))} ))}
@ -384,6 +432,90 @@ function Purchase() {
)} )}
</div> </div>
{/* 编辑弹窗 */}
{editCollection && (
<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' }}>编辑: {editCollection.collection_code}</div>
<div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '4px' }}>冠字号</div>
<div style={{ display: 'flex', gap: '1px', marginBottom: '8px' }}>
<span style={{ background: 'rgba(255,255,255,0.1)', border: '1px solid rgba(255,255,255,0.2)', borderRadius: '4px 0 0 4px', padding: '10px 4px', color: '#fff', fontWeight: 'bold', fontSize: '12px' }}>J</span>
{[0,1,2,3,4,5,6,7,8].map(i => (
<input
key={i}
ref={el => {
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' }}
/>
))}
</div>
<div style={{ marginBottom: '6px', color: 'rgba(255,255,255,0.7)', fontSize: '12px' }}>评级</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '8px', marginBottom: '12px' }}>
<select value={editCollection.grading || ''} onChange={(e) => setEditCollection({...editCollection, grading: e.target.value})} style={{ background: 'rgba(255,255,255,0.1)', border: '1px solid rgba(255,255,255,0.2)', borderRadius: '6px', padding: '10px', color: '#fff' }}>
<option value="">选择评级</option>
<option value="ACG 67+">ACG 67+</option>
<option value="PMG 67+">PMG 67+</option>
<option value="ACG 68+">ACG 68+</option>
<option value="PMG 68+">PMG 68+</option>
<option value="ACG 69+">ACG 69+</option>
<option value="PMG 69+">PMG 69+</option>
<option value="ACG 70+">ACG 70+</option>
<option value="PMG 70+">PMG 70+</option>
<option value="ACG 67+">ACG 67+</option>
<option value="ACG 68+">ACG 68+</option>
<option value="ACG 69+">ACG 69+</option>
<option value="ACG 70+">ACG 70+</option>
</select>
<input value={editCollection.price || ''} onChange={(e) => 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' }} />
</div>
<div style={{ marginBottom: '6px', color: 'rgba(255,255,255,0.7)', fontSize: '12px' }}>认购老板</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '8px', marginBottom: '12px' }}>
<select value={editCollection.boss_name || ''} onChange={(e) => setEditCollection({...editCollection, boss_name: e.target.value})} style={{ background: 'rgba(255,255,255,0.1)', border: '1px solid rgba(255,255,255,0.2)', borderRadius: '6px', padding: '10px', color: '#fff' }}>
<option value="">选择老板</option>
{groupMembers.map(m => <option key={m.id} value={m.user_name}>{m.user_name}</option>)}
</select>
<select value={editCollection.source || ''} onChange={(e) => setEditCollection({...editCollection, source: e.target.value})} style={{ background: 'rgba(255,255,255,0.1)', border: '1px solid rgba(255,255,255,0.2)', borderRadius: '6px', padding: '10px', color: '#fff' }}>
<option value="">选择来源</option>
<option value="一尘">一尘</option>
<option value="直播">直播</option>
<option value="群友">群友</option>
<option value="其他">其他</option>
</select>
</div>
<div style={{ marginBottom: '6px', color: 'rgba(255,255,255,0.7)', fontSize: '12px' }}>状态</div>
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', color: '#fff', marginBottom: '16px' }}>
<input type="checkbox" checked={editCollection.paid || false} onChange={(e) => setEditCollection({...editCollection, paid: e.target.checked})} />
认购款结清
</label>
<div style={{ display: 'flex', gap: '8px' }}>
<button onClick={() => setEditCollection(null)} style={{ flex: 1, background: '#64748b', color: '#fff', border: 'none', borderRadius: '8px', padding: '12px' }}>取消</button>
<button onClick={updateCollection} style={{ flex: 1, background: '#3b82f6', color: '#fff', border: 'none', borderRadius: '8px', padding: '12px' }}>保存</button>
</div>
</div>
</div>
)}
{/* 提交认购藏品表单 - 仅群主可提交 */} {/* 提交认购藏品表单 - 仅群主可提交 */}
{selectedGroup && user && selectedGroup.creator_id === user.id && selectedGroup.status === 'active' && showCollectionForm && ( {selectedGroup && user && selectedGroup.creator_id === user.id && selectedGroup.status === 'active' && showCollectionForm && (
<div style={{ <div style={{
@ -394,77 +526,118 @@ function Purchase() {
padding: '16px', padding: '16px',
border: '1px solid rgba(255,255,255,0.1)' border: '1px solid rgba(255,255,255,0.1)'
}}> }}>
<div style={{ color: '#fff', fontSize: '14px', marginBottom: '12px' }}>提交认购藏品</div> <div style={{ color: '#fff', fontSize: '16px', marginBottom: '16px', fontWeight: 'bold', textAlign: 'center' }}>提交认购藏品</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '8px', marginBottom: '12px' }}>
<input {/* 冠字号行 */}
value={newCollection.collection_name} <div style={{ marginBottom: '12px' }}>
onChange={(e) => setNewCollection({...newCollection, collection_name: e.target.value})} <div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '4px' }}>冠字号 (J0开头8位)</div>
placeholder="藏品名称" <div style={{ display: 'flex', gap: '1px' }}>
style={{ <span style={{ background: 'rgba(255,255,255,0.1)', border: '1px solid rgba(255,255,255,0.2)', borderRadius: '4px 0 0 4px', padding: '10px 6px', color: '#fff', fontWeight: 'bold', fontSize: '14px' }}>J</span>
background: 'rgba(255,255,255,0.1)', {[0,1,2,3,4,5,6,7,8].map(i => (
border: '1px solid rgba(255,255,255,0.2)', <input
borderRadius: '6px', key={i}
padding: '10px', ref={el => {
color: '#fff', if (el) window.__numberInputs = window.__numberInputs || [];
fontSize: '13px' window.__numberInputs[i] = el;
}} }}
/> defaultValue={i === 0 ? '0' : ''}
<input onChange={(e) => {
value={newCollection.number} let v = e.target.value
onChange={(e) => setNewCollection({...newCollection, number: e.target.value})} if (v) {
placeholder="冠字号" v = v.replace(/\D/g, '')
style={{ if (v) {
background: 'rgba(255,255,255,0.1)', const num = newCollection.number || 'J0________'
border: '1px solid rgba(255,255,255,0.2)', const arr = num.split('')
borderRadius: '6px', arr[i+1] = v
padding: '10px', setNewCollection({...newCollection, number: arr.join('')})
color: '#fff', if (i < 8 && v) window.__numberInputs[i+1]?.focus()
fontSize: '13px' }
}} }
/> }}
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' }}
/>
))}
</div>
</div> </div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '8px', marginBottom: '12px' }}>
<input {/* 评级行 */}
value={newCollection.collection_code} <div style={{ marginBottom: '12px' }}>
onChange={(e) => setNewCollection({...newCollection, collection_code: e.target.value})} <div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '4px' }}>评级</div>
placeholder="编号(可选)" <select
style={{ value={newCollection.grading || ''}
background: 'rgba(255,255,255,0.1)', onChange={(e) => setNewCollection({...newCollection, grading: e.target.value})}
border: '1px solid rgba(255,255,255,0.2)', 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' }}
borderRadius: '6px', >
padding: '10px', <option value="" style={{color:'#666'}}>选择评级</option>
color: '#fff', <option value="ACG 67+">ACG 67+</option>
fontSize: '13px' <option value="ACG 68+">ACG 68+</option>
}} <option value="ACG 69+">ACG 69+</option>
/> <option value="ACG 70+">ACG 70+</option>
<option value="PMG 67+">PMG 67+</option>
<option value="PMG 68+">PMG 68+</option>
<option value="PMG 69+">PMG 69+</option>
<option value="PMG 70+">PMG 70+</option>
</select>
</div>
{/* 认购老板行 */}
<div style={{ marginBottom: '12px' }}>
<div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '4px' }}>认购老板</div>
<select
value={newCollection.boss_name || ''}
onChange={(e) => setNewCollection({...newCollection, boss_name: e.target.value})}
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' }}
>
<option value="" style={{color:'#666'}}>选择认购老板</option>
{groupMembers.filter(m => m.status === 'approved').map(m => (
<option key={m.id} value={m.user_name}>{m.user_name}</option>
))}
</select>
</div>
{/* 价格行 */}
<div style={{ marginBottom: '12px' }}>
<div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '4px' }}>价格 ()</div>
<input <input
value={newCollection.price} value={newCollection.price}
onChange={(e) => setNewCollection({...newCollection, price: e.target.value})} onChange={(e) => setNewCollection({...newCollection, price: e.target.value})}
placeholder="价格" placeholder="输入价格"
type="number" type="number"
style={{ 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' }}
background: 'rgba(255,255,255,0.1)',
border: '1px solid rgba(255,255,255,0.2)',
borderRadius: '6px',
padding: '10px',
color: '#fff',
fontSize: '13px'
}}
/> />
</div> </div>
{/* 来源行 */}
<div style={{ marginBottom: '12px' }}>
<div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '4px' }}>来源</div>
<select
value={newCollection.source || ''}
onChange={(e) => setNewCollection({...newCollection, source: e.target.value})}
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' }}
>
<option value="" style={{color:'#666'}}>选择来源</option>
<option value="一尘">一尘</option>
<option value="直播">直播</option>
<option value="群友">群友</option>
<option value="其他">其他</option>
</select>
</div>
{/* 结清行 */}
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', color: '#fff', fontSize: '13px' }}>
<input type="checkbox" checked={newCollection.paid || false} onChange={(e) => setNewCollection({...newCollection, paid: e.target.checked})} />
认购款已结清
</label>
</div>
<button <button
onClick={submitCollection} onClick={submitCollection}
style={{ style={{ width: '100%', background: '#3b82f6', color: '#fff', border: 'none', borderRadius: '8px', padding: '14px', fontSize: '14px', fontWeight: 'bold', cursor: 'pointer' }}
width: '100%',
background: '#3b82f6',
color: '#fff',
border: 'none',
borderRadius: '8px',
padding: '12px',
fontSize: '14px',
fontWeight: 'bold',
cursor: 'pointer'
}}
> >
提交认购 提交认购
</button> </button>