feat(rock): 支持手动采购指定数量通用 Token 弹窗与实时积分试算
1. 新增手动采购 Modal 弹窗,支持自由输入指定数量(InputNumber)与快捷胶囊选择(10/20/50/100/全部可购); 2. 动态联动 0666.pro 实时积分余额,提供单价折算(21点/个)、预计消耗与结余防超支校验; 3. PageHeader 采购按钮与指标看板无缝唤起采购弹窗,采购完成后自动关闭并刷新数据。
This commit is contained in:
@@ -46,6 +46,8 @@ export default function PlatformRock() {
|
|||||||
const [testingConn, setTestingConn] = useState(false)
|
const [testingConn, setTestingConn] = useState(false)
|
||||||
const [savingConfig, setSavingConfig] = useState(false)
|
const [savingConfig, setSavingConfig] = useState(false)
|
||||||
const [purchasing, setPurchasing] = useState(false)
|
const [purchasing, setPurchasing] = useState(false)
|
||||||
|
const [purchaseModalOpen, setPurchaseModalOpen] = useState(false)
|
||||||
|
const [purchaseCount, setPurchaseCount] = useState(50)
|
||||||
|
|
||||||
// Token 列表与筛选
|
// Token 列表与筛选
|
||||||
const [tokens, setTokens] = useState<RockTokenItem[]>([])
|
const [tokens, setTokens] = useState<RockTokenItem[]>([])
|
||||||
@@ -169,12 +171,22 @@ export default function PlatformRock() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 批量采购 50 个
|
const openPurchaseModal = (count = 50) => {
|
||||||
const handlePurchase = async (count = 50) => {
|
setPurchaseCount(count)
|
||||||
|
setPurchaseModalOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 采购指定数量 Token
|
||||||
|
const handlePurchase = async (count: number) => {
|
||||||
|
if (!count || count <= 0) {
|
||||||
|
message.warning('请输入有效的采购数量')
|
||||||
|
return
|
||||||
|
}
|
||||||
setPurchasing(true)
|
setPurchasing(true)
|
||||||
try {
|
try {
|
||||||
const res = await rockApi.purchaseTokens(count)
|
const res = await rockApi.purchaseTokens(count)
|
||||||
message.success(res.message || `成功采购 ${count} 个通用黑色 Token!`)
|
message.success(res.message || `成功采购 ${count} 个通用黑色 Token!`)
|
||||||
|
setPurchaseModalOpen(false)
|
||||||
loadData()
|
loadData()
|
||||||
loadTokens()
|
loadTokens()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -371,6 +383,10 @@ export default function PlatformRock() {
|
|||||||
const warningStock = config?.token_warning_stock ?? 10
|
const warningStock = config?.token_warning_stock ?? 10
|
||||||
const isStockLow = availableStock <= warningStock
|
const isStockLow = availableStock <= warningStock
|
||||||
const hasMismatch = (stats?.mismatch_stock ?? 0) > 0
|
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 (
|
return (
|
||||||
<div style={{ padding: '0 4px' }}>
|
<div style={{ padding: '0 4px' }}>
|
||||||
@@ -382,14 +398,14 @@ export default function PlatformRock() {
|
|||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<ShoppingCartOutlined />}
|
icon={<ShoppingCartOutlined />}
|
||||||
onClick={() => handlePurchase(50)}
|
onClick={() => openPurchaseModal(50)}
|
||||||
loading={purchasing}
|
loading={purchasing}
|
||||||
style={{
|
style={{
|
||||||
background: 'linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%)',
|
background: 'linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%)',
|
||||||
boxShadow: '0 2px 4px rgba(37,99,235,0.2)',
|
boxShadow: '0 2px 4px rgba(37,99,235,0.2)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
一键采购 50 个通用 Token
|
采购通用 Token
|
||||||
</Button>
|
</Button>
|
||||||
<Button icon={<ReloadOutlined />} onClick={loadData} loading={loadingStats}>
|
<Button icon={<ReloadOutlined />} onClick={loadData} loading={loadingStats}>
|
||||||
刷新状态
|
刷新状态
|
||||||
@@ -474,12 +490,18 @@ export default function PlatformRock() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="metric-card-value" style={{ color: '#7c3aed' }}>
|
<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>
|
<span style={{ fontSize: 13, fontWeight: 500, marginLeft: 4, color: '#64748b' }}>点</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="metric-card-footer">
|
<div className="metric-card-footer">
|
||||||
<span>账号: {stats?.username || '未同步'}</span>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user