1307 lines
48 KiB
TypeScript
1307 lines
48 KiB
TypeScript
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>
|
||
)
|
||
}
|
||
|
||
|