feat(rock): 支持手动采购指定数量通用 Token 弹窗与实时积分试算

1. 新增手动采购 Modal 弹窗,支持自由输入指定数量(InputNumber)与快捷胶囊选择(10/20/50/100/全部可购);
2. 动态联动 0666.pro 实时积分余额,提供单价折算(21点/个)、预计消耗与结余防超支校验;
3. PageHeader 采购按钮与指标看板无缝唤起采购弹窗,采购完成后自动关闭并刷新数据。
This commit is contained in:
yml2213
2026-10-05 15:54:27 +08:00
parent 7e5cbaf65e
commit befe50b3e5
+180 -6
View File
@@ -46,6 +46,8 @@ export default function PlatformRock() {
const [testingConn, setTestingConn] = useState(false)
const [savingConfig, setSavingConfig] = useState(false)
const [purchasing, setPurchasing] = useState(false)
const [purchaseModalOpen, setPurchaseModalOpen] = useState(false)
const [purchaseCount, setPurchaseCount] = useState(50)
// Token 列表与筛选
const [tokens, setTokens] = useState<RockTokenItem[]>([])
@@ -169,12 +171,22 @@ export default function PlatformRock() {
}
}
// 批量采购 50 个
const handlePurchase = async (count = 50) => {
const openPurchaseModal = (count = 50) => {
setPurchaseCount(count)
setPurchaseModalOpen(true)
}
// 采购指定数量 Token
const handlePurchase = async (count: number) => {
if (!count || count <= 0) {
message.warning('请输入有效的采购数量')
return
}
setPurchasing(true)
try {
const res = await rockApi.purchaseTokens(count)
message.success(res.message || `成功采购 ${count} 个通用黑色 Token!`)
setPurchaseModalOpen(false)
loadData()
loadTokens()
} catch (e) {
@@ -371,6 +383,10 @@ export default function PlatformRock() {
const warningStock = config?.token_warning_stock ?? 10
const isStockLow = availableStock <= warningStock
const hasMismatch = (stats?.mismatch_stock ?? 0) > 0
const upstreamPoints = stats?.upstream_points ?? 0
const maxPurchasable = Math.max(0, Math.floor(upstreamPoints / 21))
const estimatedCost = (purchaseCount || 0) * 21
const isPointsInsufficient = estimatedCost > upstreamPoints
return (
<div style={{ padding: '0 4px' }}>
@@ -382,14 +398,14 @@ export default function PlatformRock() {
<Button
type="primary"
icon={<ShoppingCartOutlined />}
onClick={() => handlePurchase(50)}
onClick={() => openPurchaseModal(50)}
loading={purchasing}
style={{
background: 'linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%)',
boxShadow: '0 2px 4px rgba(37,99,235,0.2)',
}}
>
一键采购 50 个通用 Token
采购通用 Token
</Button>
<Button icon={<ReloadOutlined />} onClick={loadData} loading={loadingStats}>
刷新状态
@@ -474,12 +490,18 @@ export default function PlatformRock() {
</div>
</div>
<div className="metric-card-value" style={{ color: '#7c3aed' }}>
{(stats?.upstream_points ?? 0).toFixed(1)}
{upstreamPoints.toFixed(1)}
<span style={{ fontSize: 13, fontWeight: 500, marginLeft: 4, color: '#64748b' }}>点</span>
</div>
<div className="metric-card-footer">
<span>账号: {stats?.username || '未同步'}</span>
<span>约可采购 <strong style={{ color: '#0f172a' }}>{Math.floor((stats?.upstream_points ?? 0) / 21)}</strong> 个</span>
<a
onClick={() => openPurchaseModal(maxPurchasable > 0 ? Math.min(50, maxPurchasable) : 10)}
style={{ fontWeight: 600, color: '#2563eb', cursor: 'pointer' }}
title="点击快速打开手动采购弹窗"
>
约可采购 {maxPurchasable} 个 ➔
</a>
</div>
</div>
</Col>
@@ -761,7 +783,159 @@ export default function PlatformRock() {
},
}}
/>
{/* 手动采购指定数量 Token 弹窗 */}
<Modal
title={
<Space>
<ShoppingCartOutlined style={{ color: '#2563eb' }} />
<span>手动采购通用黑色 Token (0666.pro)</span>
</Space>
}
open={purchaseModalOpen}
onCancel={() => setPurchaseModalOpen(false)}
footer={[
<Button key="cancel" onClick={() => setPurchaseModalOpen(false)}>
取消
</Button>,
<Button
key="submit"
type="primary"
icon={<ShoppingCartOutlined />}
loading={purchasing}
disabled={isPointsInsufficient || purchaseCount <= 0}
onClick={() => handlePurchase(purchaseCount)}
style={{
background: isPointsInsufficient
? undefined
: 'linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%)',
}}
>
确认采购 {purchaseCount} 个 ({estimatedCost} 点)
</Button>,
]}
destroyOnClose
width={500}
>
<div style={{ marginTop: 16 }}>
{/* 账户与资产概览 */}
<div
style={{
background: '#f8fafc',
border: '1px solid #e2e8f0',
borderRadius: 8,
padding: '12px 16px',
marginBottom: 18,
}}
>
<Row gutter={16}>
<Col span={12}>
<div style={{ fontSize: 12, color: '#64748b' }}>0666.pro 采购商户</div>
<div style={{ fontSize: 15, fontWeight: 600, color: '#0f172a', marginTop: 2 }}>
{stats?.username || '正常商户'}
</div>
</Col>
<Col span={12}>
<div style={{ fontSize: 12, color: '#64748b' }}>当前实时积分余额</div>
<div style={{ fontSize: 15, fontWeight: 700, color: '#7c3aed', marginTop: 2 }}>
{upstreamPoints.toFixed(1)} 点
</div>
</Col>
</Row>
<Divider style={{ margin: '10px 0' }} />
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 12, color: '#64748b' }}>
<span>采购单价:<strong style={{ color: '#0f172a' }}>21 点 / 个</strong></span>
<span>最大可采购:<strong style={{ color: '#2563eb' }}>{maxPurchasable} 个</strong></span>
</div>
</div>
{/* 采购数量输入 */}
<div style={{ marginBottom: 14 }}>
<div style={{ fontWeight: 600, marginBottom: 8, color: '#0f172a' }}>
采购数量:
</div>
<InputNumber
min={1}
max={maxPurchasable > 0 ? maxPurchasable : 10000}
value={purchaseCount}
onChange={(val) => setPurchaseCount(val || 1)}
style={{ width: '100%' }}
addonAfter="个 Token"
size="large"
/>
</div>
{/* 快捷数量选择胶囊 */}
<div style={{ marginBottom: 18 }}>
<div style={{ fontSize: 12, color: '#64748b', marginBottom: 8 }}>快捷选择:</div>
<Space wrap size={[8, 8]}>
{[10, 20, 50, 100].map((num) => (
<Button
key={num}
size="small"
type={purchaseCount === num ? 'primary' : 'default'}
onClick={() => setPurchaseCount(num)}
disabled={maxPurchasable > 0 && num > maxPurchasable}
>
{num} 个
</Button>
))}
{maxPurchasable > 0 && (
<Button
size="small"
type={purchaseCount === maxPurchasable ? 'primary' : 'dashed'}
onClick={() => setPurchaseCount(maxPurchasable)}
style={{ color: '#2563eb', borderColor: '#bfdbfe' }}
>
全部可购 ({maxPurchasable} 个)
</Button>
)}
</Space>
</div>
{/* 实时消耗与结余试算 */}
<div
style={{
background: isPointsInsufficient ? '#fef2f2' : '#f0fdf4',
border: `1px solid ${isPointsInsufficient ? '#fecaca' : '#bbf7d0'}`,
borderRadius: 8,
padding: '12px 16px',
fontSize: 13,
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ color: isPointsInsufficient ? '#991b1b' : '#166534' }}>
本次采购消耗积分:
</span>
<strong style={{ color: isPointsInsufficient ? '#dc2626' : '#16a34a', fontSize: 16 }}>
{estimatedCost} 点
</strong>
</div>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginTop: 6,
fontSize: 12,
}}
>
<span style={{ color: isPointsInsufficient ? '#b91c1c' : '#15803d' }}>
{isPointsInsufficient
? '⚠️ 账户积分不足,请先充值或减少采购数量'
: '采购后预计剩余积分:'}
</span>
{!isPointsInsufficient && (
<span style={{ color: '#64748b' }}>
{(upstreamPoints - estimatedCost).toFixed(1)} 点
</span>
)}
</div>
</div>
</div>
</Modal>
</div>
)
}