Files
affiliate_dash/frontend/src/pages/PlatformRock.tsx
T

1307 lines
48 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useCallback, useEffect, useState } from 'react'
import {
Badge,
Button,
Card,
Col,
Divider,
Form,
Input,
InputNumber,
Modal,
Popconfirm,
Row,
Radio,
Space,
Switch,
Table,
Tag,
Tooltip,
Typography,
message,
} from 'antd'
import {
CheckCircleOutlined,
ControlOutlined,
CopyOutlined,
DownloadOutlined,
ExclamationCircleOutlined,
EyeOutlined,
FireOutlined,
KeyOutlined,
ReloadOutlined,
SafetyCertificateOutlined,
SearchOutlined,
ShoppingCartOutlined,
ThunderboltOutlined,
UndoOutlined,
} from '@ant-design/icons'
import type { ColumnsType } from 'antd/es/table'
import { rockApi, type RockConfig, type RockStats, type RockTokenItem } from '../api'
import { PageHeader } from '../components/PageHeader'
import { generateVoucherDataURL, copyVoucherDataUrlToClipboard } from '../utils/voucher'
import { formatDateTime } from '../utils/time'
const { Text } = Typography
export default function PlatformRock() {
const [config, setConfig] = useState<RockConfig | null>(null)
const [stats, setStats] = useState<RockStats | null>(null)
const [loadingStats, setLoadingStats] = useState(false)
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)
// 凭证图片操作状态
const [copyingVoucherId, setCopyingVoucherId] = useState<number | null>(null)
const [previewVoucherUrl, setPreviewVoucherUrl] = useState('')
const [previewVoucherOpen, setPreviewVoucherOpen] = useState(false)
// Token 列表与筛选
const [tokens, setTokens] = useState<RockTokenItem[]>([])
const [totalTokens, setTotalTokens] = useState(0)
const [page, setPage] = useState(1)
const [pageSize, setPageSize] = useState(20)
const [filterStatus, setFilterStatus] = useState<string>('all')
const [searchKeyword, setSearchKeyword] = useState('')
const [loadingTokens, setLoadingTokens] = useState(false)
const [form] = Form.useForm()
const [uidEdit, setUidEdit] = useState<{ id: number; value: string; scanned: string; roleName: string } | null>(null)
const [savingUid, setSavingUid] = useState(false)
const loadData = useCallback(async () => {
setLoadingStats(true)
try {
const [cfg, st] = await Promise.all([rockApi.getConfig(), rockApi.getStats()])
setConfig(cfg)
setStats(st)
form.setFieldsValue({
auto_delivery_enabled: cfg.auto_delivery_enabled,
base_url: cfg.base_url,
bearer_token: cfg.bearer_token,
min_points_warning: cfg.min_points_warning,
token_warning_stock: cfg.token_warning_stock,
auto_purchase_enabled: cfg.auto_purchase_enabled,
auto_purchase_count: cfg.auto_purchase_count,
purchase_cooldown_minutes: cfg.purchase_cooldown_minutes,
})
} catch (e) {
message.error('加载 Rock 平台配置失败: ' + (e instanceof Error ? e.message : '未知错误'))
} finally {
setLoadingStats(false)
}
}, [form])
const loadTokens = useCallback(async () => {
setLoadingTokens(true)
try {
const res = await rockApi.listTokens({
page,
page_size: pageSize,
status: filterStatus === 'all' ? undefined : filterStatus,
search: searchKeyword || undefined,
})
setTokens(res.list || [])
setTotalTokens(res.total || 0)
} catch (e) {
message.error('加载库存列表失败: ' + (e instanceof Error ? e.message : '未知错误'))
} finally {
setLoadingTokens(false)
}
}, [page, pageSize, filterStatus, searchKeyword])
useEffect(() => {
loadData()
}, [loadData])
useEffect(() => {
loadTokens()
}, [loadTokens])
// 一键切换自动发货总开关 (Kill Switch)
const handleToggleAutoDelivery = async (checked: boolean) => {
Modal.confirm({
title: checked ? '确认开启 Rock 自动发货通道?' : '确认一键关闭 Rock 自动发货通道(软熔断)?',
content: checked
? '开启后,黑色 ROCK 与蓝色运动服两款商品将恢复 0666.pro 全自动反代发货。'
: '关闭后,两款商品将脱离自动发货通道,进单将转交人工打手大厅处理,防止错单穿透。',
okText: checked ? '确认开启' : '立即熔断关闭',
cancelText: '取消',
okButtonProps: {
danger: !checked,
},
onOk: async () => {
try {
const updated = await rockApi.updateConfig({ auto_delivery_enabled: checked })
setConfig(updated)
message.success(checked ? '自动发货通道已开启' : '自动发货通道已成功熔断关闭')
loadData()
} catch (e) {
message.error('更新总开关失败: ' + (e instanceof Error ? e.message : '未知错误'))
}
},
})
}
// 测试连通性
const handleTestConnection = async () => {
const bearerToken = form.getFieldValue('bearer_token')
if (!bearerToken) {
message.warning('请先输入 Bearer Token')
return
}
setTestingConn(true)
try {
const res = await rockApi.testConnection(bearerToken)
message.success(`连通成功!商户:${res.user?.username || '正常'},可用积分:${res.user?.points || 0}`)
loadData()
} catch (e) {
message.error('连通性测试失败:' + (e instanceof Error ? e.message : '未知错误'))
} finally {
setTestingConn(false)
}
}
// 保存策略配置
const handleSaveConfig = async () => {
try {
const values = await form.validateFields()
setSavingConfig(true)
const updated = await rockApi.updateConfig(values)
setConfig(updated)
message.success('配置已成功保存生效')
loadData()
} catch (e) {
if (e instanceof Error) {
message.error('保存失败: ' + e.message)
}
} finally {
setSavingConfig(false)
}
}
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) {
message.error('采购失败: ' + (e instanceof Error ? e.message : '未知错误'))
} finally {
setPurchasing(false)
}
}
const handleCorrectUid = async () => {
if (!uidEdit) {
return
}
const nextUid = uidEdit.value.trim()
if (!/^\d{1,15}$/.test(nextUid)) {
message.warning('请填写 15 位以内的纯数字 UID')
return
}
setSavingUid(true)
try {
const res = await rockApi.correctExpectedUid(uidEdit.id, nextUid)
message.success(res.message || '已更新买家 UID')
setUidEdit(null)
loadData()
loadTokens()
} catch (e) {
message.error('修改 UID 失败: ' + (e instanceof Error ? e.message : '未知错误'))
} finally {
setSavingUid(false)
}
}
// 人工重置解冻 Mismatch Token
const handleResetToken = async (id: number) => {
try {
await rockApi.resetToken(id)
message.success('已解除绑定异常,重置为可用库存')
loadData()
loadTokens()
} catch (e) {
message.error('重置失败: ' + (e instanceof Error ? e.message : '未知错误'))
}
}
// 买家未扫码绑定时,把占用中的提货链接放回库存
const handleReleaseToken = async (id: number) => {
try {
await rockApi.releaseToken(id)
message.success('已释放回可用库存,该提货链接可以再次分配')
loadData()
loadTokens()
} catch (e) {
message.error('释放失败: ' + (e instanceof Error ? e.message : '未知错误'))
}
}
// 复制/生成发货凭证图片
const handleCopyVoucher = async (record: RockTokenItem) => {
setCopyingVoucherId(record.id)
try {
const isRockBlue = record.target_skin?.includes('蓝')
const sku = isRockBlue ? 'rock_blue_sportswear' : 'rock_black_tshirt'
const productName = record.target_skin || (isRockBlue ? '蓝色运动服上衣' : '黑色ROCK长袖T恤')
const channel = record.actual_channel || record.region || '安卓-微信'
const timeStr = record.redeemed_at || record.bound_at || record.created_at
const cleanRole = (record.actual_role_name || '').trim()
const dataURL = await generateVoucherDataURL({
productName,
productSKU: sku,
roleName: cleanRole && cleanRole !== '-' ? cleanRole : '',
roleUID: record.expected_uid || record.actual_role_id || '-',
channel,
currencyText: '幸运币 x 10',
redeemedAt: timeStr ? formatDateTime(timeStr) : formatDateTime(new Date().toISOString()),
})
const copied = await copyVoucherDataUrlToClipboard(dataURL)
if (copied) {
message.success('发货结果图片已复制到剪贴板!可直接在微信/QQ聊天框粘贴 (Ctrl+V)')
} else {
setPreviewVoucherUrl(dataURL)
setPreviewVoucherOpen(true)
message.info('已生成凭据图片,已为您打开预览窗口,支持直接保存或右键复制')
}
} catch (e) {
message.error('生成凭证失败: ' + (e instanceof Error ? e.message : '未知错误'))
} finally {
setCopyingVoucherId(null)
}
}
const handlePreviewVoucher = async (record: RockTokenItem) => {
try {
const isRockBlue = record.target_skin?.includes('蓝')
const sku = isRockBlue ? 'rock_blue_sportswear' : 'rock_black_tshirt'
const productName = record.target_skin || (isRockBlue ? '蓝色运动服上衣' : '黑色ROCK长袖T恤')
const channel = record.actual_channel || record.region || '安卓-微信'
const timeStr = record.redeemed_at || record.bound_at || record.created_at
const cleanRole = (record.actual_role_name || '').trim()
const dataURL = await generateVoucherDataURL({
productName,
productSKU: sku,
roleName: cleanRole && cleanRole !== '-' ? cleanRole : '',
roleUID: record.expected_uid || record.actual_role_id || '-',
channel,
currencyText: '幸运币 x 10',
redeemedAt: timeStr ? formatDateTime(timeStr) : formatDateTime(new Date().toISOString()),
})
setPreviewVoucherUrl(dataURL)
setPreviewVoucherOpen(true)
} catch (e) {
message.error('生成凭证失败: ' + (e instanceof Error ? e.message : '未知错误'))
}
}
const columns: ColumnsType<RockTokenItem> = [
{
title: '0666 编号 / 提取码',
dataIndex: 'upstream_token_id',
width: 250,
render: (_, record) => (
<Space direction="vertical" size={2}>
<Text
copyable={record.upstream_token_id ? { text: record.upstream_token_id } : false}
style={{ fontWeight: 600 }}
>
0666 #{record.upstream_token_id || '-'}
</Text>
<Text
copyable={record.redeem_code ? { text: record.redeem_code } : false}
type="secondary"
style={{ fontSize: 12, fontFamily: 'monospace' }}
>
{record.redeem_code || '-'}
</Text>
<Text type="secondary" style={{ fontSize: 11 }}>本地 #{record.id}</Text>
</Space>
),
},
{
title: '规格与适用范围',
dataIndex: 'color',
width: 150,
render: (_, record) => (
<Space direction="vertical" size={2}>
<Tag color="geekblue" style={{ borderRadius: 4, fontWeight: 500 }}>
通用黑色 Token
</Tag>
<Text type="secondary" style={{ fontSize: 12 }}>
{record.region || 'Q/V区通用 · 自动分发'}
</Text>
</Space>
),
},
{
title: '库存状态',
dataIndex: 'status',
width: 140,
render: (status) => {
switch (status) {
case 'available':
return (
<Tag color="success" style={{ fontWeight: 600, padding: '2px 8px' }}>
● 可用库存
</Tag>
)
case 'using':
return (
<Tag color="processing" style={{ fontWeight: 600, padding: '2px 8px' }}>
● 扫码使用中
</Tag>
)
case 'bound':
return (
<Tag color="cyan" style={{ fontWeight: 600, padding: '2px 8px' }}>
● 已授权绑定
</Tag>
)
case 'redeemed':
return (
<Tag color="purple" style={{ fontWeight: 600, padding: '2px 8px' }}>
● 已交付发货
</Tag>
)
case 'mismatch':
return (
<Tag color="error" style={{ fontWeight: 600, padding: '2px 8px', border: '1px solid #ff4d4f' }}>
⚠️ 绑定错误 (阻断)
</Tag>
)
default:
return <Tag>{status}</Tag>
}
},
},
{
title: '关联业务工单',
dataIndex: 'assigned_order_no',
width: 220,
render: (orderNo, record) => {
if (!orderNo) {
return <Text type="secondary">-</Text>
}
return (
<Space direction="vertical" size={3}>
{/* 系统履约工单号 */}
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
<Text
code
copyable={{ text: orderNo }}
style={{ fontWeight: 600, color: '#1d4ed8', fontSize: 12 }}
title="系统履约工单号"
>
{orderNo}
</Text>
</div>
{/* 创建订单时的商户单号 */}
{record.client_order_no ? (
<div style={{ display: 'flex', alignItems: 'center', gap: 4, fontSize: 12 }}>
<span style={{ color: '#64748b' }}>商户:</span>
<Text
copyable={{ text: record.client_order_no }}
type="secondary"
style={{ fontFamily: 'monospace', fontSize: 12 }}
title="创建订单时的商户单号 (ClientOrderNo)"
>
{record.client_order_no}
</Text>
</div>
) : null}
</Space>
)
},
},
{
title: '预期 UID vs 扫码角色 (风控门禁)',
width: 250,
render: (_, record) => {
const isMismatch = record.status === 'mismatch'
return (
<div
style={{
padding: isMismatch ? '6px 10px' : '0',
background: isMismatch ? '#fff1f0' : 'transparent',
borderRadius: isMismatch ? 6 : 0,
border: isMismatch ? '1px dashed #ffa39e' : 'none',
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 12 }}>
<span style={{ color: '#64748b' }}>预期 UID:</span>
<Text strong style={{ color: '#0f172a' }}>{record.expected_uid || '-'}</Text>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 12, marginTop: 2 }}>
<span style={{ color: '#64748b' }}>扫码反查:</span>
<Text type={isMismatch ? 'danger' : undefined} strong={isMismatch}>
{record.actual_role_name ? `${record.actual_role_name} (${record.actual_role_id || '-'})` : '-'}
</Text>
</div>
{isMismatch && record.mismatch_reason && (
<div style={{ marginTop: 4 }}>
<Tooltip title={record.mismatch_reason}>
<Tag color="error" style={{ fontSize: 11, margin: 0, maxWidth: 220, overflow: 'hidden', textOverflow: 'ellipsis' }}>
{record.mismatch_reason}
</Tag>
</Tooltip>
</div>
)}
</div>
)
},
},
{
title: '发货结果图片',
key: 'voucher',
width: 240,
render: (_, record) => {
if (record.status !== 'redeemed' && !record.upstream_fulfill_id) {
return <Text type="secondary">-</Text>
}
const isCopying = copyingVoucherId === record.id
const isRockBlue = record.target_skin?.includes('蓝')
const sku = isRockBlue ? 'rock_blue_sportswear' : 'rock_black_tshirt'
const imgSrc = `/images/products/${sku}.webp`
return (
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
{/* 商品缩略图:鼠标悬浮微放、点击快速预览高清大图凭据 */}
<Tooltip title="点击预览深海蓝发货凭据大图">
<div
onClick={() => handlePreviewVoucher(record)}
style={{
width: 44,
height: 44,
flexShrink: 0,
borderRadius: 8,
border: '1px solid #e2e8f0',
background: '#f8fafc',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
overflow: 'hidden',
boxShadow: '0 1px 3px rgba(0,0,0,0.06)',
transition: 'all 0.2s ease',
}}
onMouseEnter={(e) => {
e.currentTarget.style.transform = 'scale(1.08)'
e.currentTarget.style.borderColor = '#93c5fd'
e.currentTarget.style.boxShadow = '0 3px 8px rgba(37,99,235,0.2)'
}}
onMouseLeave={(e) => {
e.currentTarget.style.transform = 'scale(1)'
e.currentTarget.style.borderColor = '#e2e8f0'
e.currentTarget.style.boxShadow = '0 1px 3px rgba(0,0,0,0.06)'
}}
>
<img
src={imgSrc}
alt={record.target_skin || '道具'}
style={{ width: '100%', height: '100%', objectFit: 'contain', padding: 2 }}
/>
</div>
</Tooltip>
{/* 操作按钮与商品徽章 */}
<Space direction="vertical" size={3}>
<Space size={4}>
<Button
size="small"
type="primary"
icon={<CopyOutlined />}
loading={isCopying}
onClick={() => handleCopyVoucher(record)}
style={{
background: 'linear-gradient(135deg, #059669 0%, #10b981 100%)',
borderColor: '#059669',
boxShadow: '0 1px 2px rgba(16, 185, 129, 0.2)',
fontSize: 12,
fontWeight: 500,
padding: '0 8px',
}}
>
复制结果图
</Button>
<Tooltip title="查看高清发货凭据大图">
<Button
size="small"
icon={<EyeOutlined />}
onClick={() => handlePreviewVoucher(record)}
/>
</Tooltip>
</Space>
<div>
<Tag color="cyan" style={{ fontSize: 11, margin: 0 }}>
{record.target_skin || '黑色ROCK长袖T恤'}
</Tag>
</div>
</Space>
</div>
)
},
},
{
title: '操作',
key: 'action',
width: 168,
fixed: 'right',
render: (_, record) => {
if (record.status === 'mismatch') {
return (
<Space direction="vertical" size={4}>
<Button
size="small"
type="primary"
onClick={() =>
setUidEdit({
id: record.id,
value: record.actual_role_id || record.expected_uid || '',
scanned: record.actual_role_id || '',
roleName: record.actual_role_name || '',
})
}
>
修改 UID
</Button>
<Popconfirm
title="确定解冻重置该 Token?"
description="重置后将清除原订单绑定与错误标记,重新释放为可用通用库存。已扫码的链接放回库存后,其他订单领走就会废掉。"
okText="确认解冻"
cancelText="取消"
onConfirm={() => handleResetToken(record.id)}
>
<Button size="small" danger ghost icon={<UndoOutlined />}>
解冻重置
</Button>
</Popconfirm>
</Space>
)
}
if (record.status === 'using' && !record.bound_at && !record.actual_role_id) {
return (
<Popconfirm
title="释放这条未绑定的提货链接?"
description="仅在买家没有扫码绑定时可以释放。系统会先向上游确认;确认未绑定后,链接回到可用库存,可以分给下一单。请先确认该订单已退款。原发货页若仍打开,买家再次获取二维码会重新占用一条库存。"
okText="确认释放"
cancelText="取消"
onConfirm={() => handleReleaseToken(record.id)}
>
<Button size="small" icon={<UndoOutlined />}>
释放回库存
</Button>
</Popconfirm>
)
}
return <Text type="secondary">-</Text>
},
},
]
const autoDelivery = config?.auto_delivery_enabled ?? true
const availableStock = stats?.available_stock ?? 0
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' }}>
<PageHeader
title="Rock 发货平台 (0666.pro) 自动发货与库存风控"
subtitle="和平精英「黑色ROCK长袖T恤」与「蓝色运动服上衣」通用库存池、上游采购、UID强一致防错门禁与软熔断机制"
extra={
<Space size="middle">
<Button
type="primary"
icon={<ShoppingCartOutlined />}
onClick={() => openPurchaseModal(50)}
loading={purchasing}
style={{
background: 'linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%)',
boxShadow: '0 2px 4px rgba(37,99,235,0.2)',
}}
>
采购通用 Token
</Button>
<Button icon={<ReloadOutlined />} onClick={loadData} loading={loadingStats}>
刷新状态
</Button>
</Space>
}
/>
{/* 顶部高质感指标卡片 */}
<Row gutter={[16, 16]} style={{ marginBottom: 20 }}>
<Col xs={24} sm={12} lg={6}>
<div className="metric-card-box">
<div className="metric-card-header">
<span className="metric-card-title">可用通用库存 (Available)</span>
<div className={`metric-card-icon ${isStockLow ? 'metric-card-icon--rose' : ''}`}>
<ShoppingCartOutlined />
</div>
</div>
<div className="metric-card-value" style={{ color: isStockLow ? '#e11d48' : '#0f172a' }}>
{availableStock}
<span style={{ fontSize: 13, fontWeight: 500, marginLeft: 4, color: '#64748b' }}>个</span>
</div>
<div className="metric-card-footer">
<span>警戒水位: {warningStock} 个</span>
{isStockLow ? (
<Tag color="error" style={{ margin: 0, borderRadius: 4 }}>库存紧缺</Tag>
) : (
<span style={{ color: '#10b981', fontWeight: 600 }}>储备充足</span>
)}
</div>
</div>
</Col>
<Col xs={24} sm={12} lg={6}>
<div className="metric-card-box">
<div className="metric-card-header">
<span className="metric-card-title">绑定错误阻断 (Mismatch)</span>
<div className={`metric-card-icon ${hasMismatch ? 'metric-card-icon--rose' : ''}`}>
<ExclamationCircleOutlined />
</div>
</div>
<div className="metric-card-value" style={{ color: hasMismatch ? '#e11d48' : '#0f172a' }}>
{stats?.mismatch_stock ?? 0}
<span style={{ fontSize: 13, fontWeight: 500, marginLeft: 4, color: '#64748b' }}>个</span>
</div>
<div className="metric-card-footer">
<span>{hasMismatch ? 'UID强一致校验拦截保护' : '暂无异常阻断'}</span>
{hasMismatch ? (
<Tag color="error" style={{ margin: 0, borderRadius: 4 }}>需核对处理</Tag>
) : (
<span style={{ color: '#10b981', fontWeight: 600 }}>状态正常</span>
)}
</div>
</div>
</Col>
<Col xs={24} sm={12} lg={6}>
<div className="metric-card-box">
<div className="metric-card-header">
<span className="metric-card-title">已履约交付 (Redeemed)</span>
<div className="metric-card-icon metric-card-icon--emerald">
<CheckCircleOutlined />
</div>
</div>
<div className="metric-card-value">
{stats?.redeemed_stock ?? 0}
<span style={{ fontSize: 13, fontWeight: 500, marginLeft: 4, color: '#64748b' }}>个</span>
</div>
<div className="metric-card-footer">
<span>当前扫码中</span>
<span style={{ fontWeight: 600, color: '#2563eb' }}>{stats?.using_stock ?? 0} 个</span>
</div>
</div>
</Col>
<Col xs={24} sm={12} lg={6}>
<div className="metric-card-box">
<div className="metric-card-header">
<span className="metric-card-title">0666.pro 账号积分</span>
<div className="metric-card-icon metric-card-icon--purple">
<FireOutlined />
</div>
</div>
<div className="metric-card-value" style={{ color: '#7c3aed' }}>
{upstreamPoints.toFixed(1)}
<span style={{ fontSize: 13, fontWeight: 500, marginLeft: 4, color: '#64748b' }}>点</span>
</div>
<div className="metric-card-footer">
<span>账号: {stats?.username || '未同步'}</span>
<a
onClick={() => openPurchaseModal(maxPurchasable > 0 ? Math.min(50, maxPurchasable) : 10)}
style={{ fontWeight: 600, color: '#2563eb', cursor: 'pointer' }}
title="点击快速打开手动采购弹窗"
>
约可采购 {maxPurchasable} 个 ➔
</a>
</div>
</div>
</Col>
</Row>
{/* 核心控制与上游策略看板 */}
<Card
style={{
marginBottom: 20,
borderRadius: 12,
border: '1px solid #e2e8f0',
boxShadow: 'var(--shadow-sm)',
}}
styles={{ body: { padding: '20px 24px' } }}
>
{/* 通道状态与软熔断横幅 */}
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '14px 18px',
borderRadius: 8,
marginBottom: 20,
background: autoDelivery ? '#f0fdf4' : '#fef2f2',
border: autoDelivery ? '1px solid #bbf7d0' : '1px solid #fecaca',
transition: 'all 0.3s ease',
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<div
style={{
width: 38,
height: 38,
borderRadius: 8,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 18,
background: autoDelivery ? '#dcfce7' : '#fee2e2',
color: autoDelivery ? '#16a34a' : '#dc2626',
}}
>
{autoDelivery ? <SafetyCertificateOutlined /> : <ControlOutlined />}
</div>
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<strong style={{ fontSize: 14, color: autoDelivery ? '#166534' : '#991b1b' }}>
{autoDelivery
? '自动发货通道正常运转 (Active)'
: '自动发货已全局软熔断 (Kill Switch Engaged)'}
</strong>
<Tag color={autoDelivery ? 'success' : 'error'} style={{ borderRadius: 10 }}>
{autoDelivery ? '反代履约中' : '已转交人工打手'}
</Tag>
</div>
<div style={{ fontSize: 12, color: autoDelivery ? '#15803d' : '#b91c1c', marginTop: 2 }}>
{autoDelivery
? '和平精英「黑色 ROCK 长袖」与「蓝色运动服」开启 0666.pro 全自动反代发货,UID 强一致安全门禁全面守护。'
: '两款商品已彻底切断上游 0666.pro 自动发货通道,所有新订单均保留在待发货状态,由人工接单平台处理。'}
</div>
</div>
</div>
<Space size="middle">
<Switch
checked={autoDelivery}
onChange={handleToggleAutoDelivery}
checkedChildren="开启"
unCheckedChildren="熔断"
style={{
backgroundColor: autoDelivery ? '#16a34a' : '#dc2626',
}}
/>
<Button
size="small"
danger={autoDelivery}
type={autoDelivery ? 'default' : 'primary'}
onClick={() => handleToggleAutoDelivery(!autoDelivery)}
>
{autoDelivery ? '一键紧急熔断' : '恢复自动发货'}
</Button>
</Space>
</div>
{/* 策略表单 */}
<Form form={form} layout="vertical" autoComplete="off">
{/* 阻断浏览器对凭据配置项的强制账密自动填充 (Honeypot) */}
<div style={{ position: 'absolute', top: -9999, left: -9999, width: 0, height: 0, overflow: 'hidden' }} aria-hidden="true">
<input type="text" name="chrome_prevent_autofill_username" tabIndex={-1} autoComplete="username" />
<input type="password" name="chrome_prevent_autofill_password" tabIndex={-1} autoComplete="current-password" />
</div>
<Row gutter={24}>
<Col xs={24} lg={12}>
<div
style={{
background: '#f8fafc',
padding: '16px 18px',
borderRadius: 10,
border: '1px solid #f1f5f9',
height: '100%',
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 16 }}>
<KeyOutlined style={{ color: '#2563eb', fontSize: 16 }} />
<strong style={{ fontSize: 14, color: '#0f172a' }}>0666.pro 采购认证凭据</strong>
</div>
<Form.Item name="base_url" label="上游平台接口地址 (Base URL)" style={{ marginBottom: 14 }}>
<Input
placeholder="https://0666.pro"
autoComplete="off"
name="upstream_base_url"
id="upstream_base_url"
/>
</Form.Item>
<Form.Item
name="bearer_token"
label={
<Space>
<span>上游商户 Bearer Token (yd_token)</span>
<Tooltip title="0666.pro 采用无密码架构,直接从网页 localStorage 提取 64 位十六进制 Token 即可">
<ExclamationCircleOutlined style={{ color: '#94a3b8' }} />
</Tooltip>
</Space>
}
style={{ marginBottom: 12 }}
>
<Input.Password
placeholder="粘贴 64 位十六进制 Bearer Token"
autoComplete="new-password"
name="upstream_bearer_token"
id="upstream_bearer_token"
addonAfter={
<Button
type="link"
size="small"
onClick={handleTestConnection}
loading={testingConn}
style={{ padding: '0 4px', height: 'auto' }}
>
测试连通性
</Button>
}
/>
</Form.Item>
<div
style={{
background: '#eff6ff',
border: '1px dashed #bfdbfe',
borderRadius: 6,
padding: '8px 12px',
fontSize: 12,
color: '#1e40af',
}}
>
💡 <strong>极简提取指引:</strong>登录 0666.pro 网页后,按 F12 打开控制台,在 Console 执行{' '}
<Text code copyable={{ text: "localStorage.getItem('yd_token')" }}>
localStorage.getItem('yd_token')
</Text>{' '}
即可一键复制。
</div>
</div>
</Col>
<Col xs={24} lg={12}>
<div
style={{
background: '#f8fafc',
padding: '16px 18px',
borderRadius: 10,
border: '1px solid #f1f5f9',
height: '100%',
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 16 }}>
<ThunderboltOutlined style={{ color: '#7c3aed', fontSize: 16 }} />
<strong style={{ fontSize: 14, color: '#0f172a' }}>库存风控与自动补货策略</strong>
</div>
<Row gutter={16}>
<Col span={12}>
<Form.Item name="token_warning_stock" label="库存告警安全水位">
<InputNumber min={1} max={500} style={{ width: '100%' }} addonAfter="个" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="auto_purchase_enabled" label="自动补货触发开关" valuePropName="checked">
<Switch checkedChildren="开启自动补货" unCheckedChildren="关闭" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="auto_purchase_count" label="单次自动补货数量">
<InputNumber min={5} max={200} style={{ width: '100%' }} addonAfter="个" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="purchase_cooldown_minutes" label="补货防抖冷却时间">
<InputNumber min={1} max={60} style={{ width: '100%' }} addonAfter="分钟" />
</Form.Item>
</Col>
</Row>
</div>
</Col>
</Row>
<Divider style={{ margin: '18px 0 16px 0' }} />
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<Button
type="primary"
onClick={handleSaveConfig}
loading={savingConfig}
style={{
background: 'linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%)',
minWidth: 130,
}}
>
保存策略配置
</Button>
</div>
</Form>
</Card>
{/* 筛选与搜索工具条(对齐标杆页面) */}
<Card
styles={{ body: { padding: '16px 20px' } }}
style={{ marginBottom: 16, borderRadius: 10, border: '1px solid #e2e8f0' }}
>
<Row gutter={[16, 12]} align="middle" justify="space-between">
<Col xs={24} md={16}>
<Space size="middle" wrap>
<Input
placeholder="搜索 0666 编号 / 提取码 / 订单号 / UID"
prefix={<SearchOutlined style={{ color: '#94a3b8' }} />}
value={searchKeyword}
onChange={(e) => setSearchKeyword(e.target.value)}
onPressEnter={loadTokens}
style={{ width: 340 }}
allowClear
/>
<Radio.Group
value={filterStatus}
onChange={(e) => {
setFilterStatus(e.target.value)
setPage(1)
}}
buttonStyle="solid"
>
<Radio.Button value="all">全部</Radio.Button>
<Radio.Button value="available">可用</Radio.Button>
<Radio.Button value="using">使用中</Radio.Button>
<Radio.Button value="bound">已绑定</Radio.Button>
<Radio.Button value="redeemed">已发货</Radio.Button>
<Radio.Button value="mismatch">
<Badge count={stats?.mismatch_stock ?? 0} offset={[6, -2]} size="small">
<span>绑定错误</span>
</Badge>
</Radio.Button>
</Radio.Group>
<Button icon={<ReloadOutlined />} onClick={loadTokens} loading={loadingTokens}>
刷新
</Button>
</Space>
</Col>
<Col xs={24} md={8} style={{ textAlign: 'right' }}>
<Text type="secondary" style={{ fontSize: 13 }}>
共检索到 <strong style={{ color: '#0f172a' }}>{totalTokens}</strong> 条库存记录
</Text>
</Col>
</Row>
</Card>
{/* 主库存与错单审计表格 */}
<Table
rowKey="id"
columns={columns}
dataSource={tokens}
loading={loadingTokens}
scroll={{ x: 1100 }}
pagination={{
current: page,
pageSize,
total: totalTokens,
showSizeChanger: true,
pageSizeOptions: ['10', '20', '50', '100'],
showTotal: (total) => `共 ${total} 条库存记录`,
onChange: (p, ps) => {
setPage(p)
setPageSize(ps)
},
}}
/>
{/* 手动采购指定数量 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>
<Modal
title="修改买家填写的 UID"
open={Boolean(uidEdit)}
okText="保存"
cancelText="取消"
confirmLoading={savingUid}
onOk={() => void handleCorrectUid()}
onCancel={() => setUidEdit(null)}
destroyOnClose
>
<p style={{ marginTop: 0, color: '#64748b' }}>
提货号继续留在原订单上,不会放回库存。改成扫码 UID 后,绑定阻断会解除。
</p>
{uidEdit?.scanned ? (
<p>
扫码角色:{uidEdit.roleName ? `「${uidEdit.roleName}」` : ''} {uidEdit.scanned}
</p>
) : null}
<Input
value={uidEdit?.value || ''}
maxLength={15}
placeholder="买家游戏 UID"
onChange={(event) =>
setUidEdit((current) =>
current
? { ...current, value: event.target.value.replace(/\D/g, '').slice(0, 15) }
: current,
)
}
onPressEnter={() => void handleCorrectUid()}
/>
</Modal>
{/* 发货结果图片高清预览弹窗 */}
<Modal
title={
<Space>
<SafetyCertificateOutlined style={{ color: '#10b981' }} />
<span>发货到账存证凭据 (高清预览)</span>
</Space>
}
open={previewVoucherOpen}
onCancel={() => setPreviewVoucherOpen(false)}
footer={[
<Button key="close" onClick={() => setPreviewVoucherOpen(false)}>
关闭
</Button>,
<Button
key="download"
icon={<DownloadOutlined />}
onClick={() => {
if (!previewVoucherUrl) return
const link = document.createElement('a')
link.href = previewVoucherUrl
link.download = `发货凭证_${Date.now()}.png`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
message.success('凭据图片已下载')
}}
>
下载凭证图
</Button>,
<Button
key="copy"
type="primary"
icon={<CopyOutlined />}
style={{
background: 'linear-gradient(135deg, #059669 0%, #10b981 100%)',
borderColor: '#059669',
}}
onClick={async () => {
if (!previewVoucherUrl) return
const copied = await copyVoucherDataUrlToClipboard(previewVoucherUrl)
if (copied) {
message.success('发货结果图片已复制到剪贴板!可直接在聊天窗粘贴 (Ctrl+V)')
} else {
message.info('请右键下方图片选择「复制图片」')
}
}}
>
复制图片到剪贴板
</Button>,
]}
width={420}
destroyOnClose
centered
>
<div style={{ textAlign: 'center', padding: '12px 0' }}>
{previewVoucherUrl ? (
<img
src={previewVoucherUrl}
alt="发货凭证大图"
style={{
maxWidth: '100%',
maxHeight: 520,
borderRadius: 16,
boxShadow: '0 8px 30px rgba(0, 0, 0, 0.25)',
objectFit: 'contain',
}}
/>
) : (
<div style={{ padding: 40, color: '#94a3b8' }}>正在生成高清凭据图...</div>
)}
<div style={{ marginTop: 12, fontSize: 12, color: '#64748b' }}>
💡 提示:点击「复制图片到剪贴板」后,可直接在微信/QQ等客服聊天窗口按 <Text code>Ctrl + V</Text> 发送。
</div>
</div>
</Modal>
</div>
)
}