refactor(web): 拆分 PlatformRock 大屏为容器 + 9 个子组件(1306 → 最大 335 行)
- PlatformRock.tsx 335 行:保留全部 state、loadData/loadTokens、useEffect、 handler 与顶层布局;默认导出名不变,App.tsx 的 lazy 导入未改 - platformRock/ 子目录:tokenColumns 329、RockStrategyCard 267、 PurchaseTokenModal 183、RockTokenTable 156、RockMetricCards 124、 VoucherPreviewModal 95、RockTokenFilterBar 76、UidEditModal 47、types.ts 6 state 归属与更新时机、请求次数与参数、useEffect 依赖语义均未改变;子组件 props 显式声明类型,无 any;行级异步 handler 随表格下沉,state setter 经显式类型 props 传入。 验证: - SSR 快照 renderToString 拆分前后字节一致(40920 bytes) - jsdom 交互差分:拆分前后 23 个检查点的 DOM 快照与 API 调用日志完全一致 (覆盖采购、UID 修正、解冻、释放、熔断、筛选/搜索/翻页、保存配置、连通性测试) - 原文件应保留的 230 行代码逐字命中
This commit is contained in:
+84
-1055
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,183 @@
|
||||
import { Button, Col, Divider, InputNumber, Modal, Row, Space } from 'antd'
|
||||
import { ShoppingCartOutlined } from '@ant-design/icons'
|
||||
import type { RockStats } from '../../api'
|
||||
|
||||
export interface PurchaseTokenModalProps {
|
||||
open: boolean
|
||||
stats: RockStats | null
|
||||
purchaseCount: number
|
||||
purchasing: boolean
|
||||
maxPurchasable: number
|
||||
upstreamPoints: number
|
||||
estimatedCost: number
|
||||
isPointsInsufficient: boolean
|
||||
onPurchaseCountChange: (value: number) => void
|
||||
onCancel: () => void
|
||||
onConfirm: (count: number) => void
|
||||
}
|
||||
|
||||
export function PurchaseTokenModal({
|
||||
open,
|
||||
stats,
|
||||
purchaseCount,
|
||||
purchasing,
|
||||
maxPurchasable,
|
||||
upstreamPoints,
|
||||
estimatedCost,
|
||||
isPointsInsufficient,
|
||||
onPurchaseCountChange,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
}: PurchaseTokenModalProps) {
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
<Space>
|
||||
<ShoppingCartOutlined style={{ color: '#2563eb' }} />
|
||||
<span>手动采购通用黑色 Token (0666.pro)</span>
|
||||
</Space>
|
||||
}
|
||||
open={open}
|
||||
onCancel={onCancel}
|
||||
footer={[
|
||||
<Button key="cancel" onClick={onCancel}>
|
||||
取消
|
||||
</Button>,
|
||||
<Button
|
||||
key="submit"
|
||||
type="primary"
|
||||
icon={<ShoppingCartOutlined />}
|
||||
loading={purchasing}
|
||||
disabled={isPointsInsufficient || purchaseCount <= 0}
|
||||
onClick={() => onConfirm(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) => onPurchaseCountChange(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={() => onPurchaseCountChange(num)}
|
||||
disabled={maxPurchasable > 0 && num > maxPurchasable}
|
||||
>
|
||||
{num} 个
|
||||
</Button>
|
||||
))}
|
||||
{maxPurchasable > 0 && (
|
||||
<Button
|
||||
size="small"
|
||||
type={purchaseCount === maxPurchasable ? 'primary' : 'dashed'}
|
||||
onClick={() => onPurchaseCountChange(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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import { Col, Row, Tag } from 'antd'
|
||||
import {
|
||||
CheckCircleOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
FireOutlined,
|
||||
ShoppingCartOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import type { RockStats } from '../../api'
|
||||
|
||||
export interface RockMetricCardsProps {
|
||||
stats: RockStats | null
|
||||
availableStock: number
|
||||
warningStock: number
|
||||
isStockLow: boolean
|
||||
hasMismatch: boolean
|
||||
upstreamPoints: number
|
||||
maxPurchasable: number
|
||||
onOpenPurchase: (count: number) => void
|
||||
}
|
||||
|
||||
export function RockMetricCards({
|
||||
stats,
|
||||
availableStock,
|
||||
warningStock,
|
||||
isStockLow,
|
||||
hasMismatch,
|
||||
upstreamPoints,
|
||||
maxPurchasable,
|
||||
onOpenPurchase,
|
||||
}: RockMetricCardsProps) {
|
||||
return (
|
||||
<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={() => onOpenPurchase(maxPurchasable > 0 ? Math.min(50, maxPurchasable) : 10)}
|
||||
style={{ fontWeight: 600, color: '#2563eb', cursor: 'pointer' }}
|
||||
title="点击快速打开手动采购弹窗"
|
||||
>
|
||||
约可采购 {maxPurchasable} 个 ➔
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Divider,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Row,
|
||||
Space,
|
||||
Switch,
|
||||
Tag,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from 'antd'
|
||||
import type { FormInstance } from 'antd'
|
||||
import {
|
||||
ControlOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
KeyOutlined,
|
||||
SafetyCertificateOutlined,
|
||||
ThunderboltOutlined,
|
||||
} from '@ant-design/icons'
|
||||
|
||||
const { Text } = Typography
|
||||
|
||||
export interface RockStrategyCardProps {
|
||||
form: FormInstance
|
||||
autoDelivery: boolean
|
||||
testingConn: boolean
|
||||
savingConfig: boolean
|
||||
onToggleAutoDelivery: (checked: boolean) => void
|
||||
onTestConnection: () => void
|
||||
onSaveConfig: () => void
|
||||
}
|
||||
|
||||
export function RockStrategyCard({
|
||||
form,
|
||||
autoDelivery,
|
||||
testingConn,
|
||||
savingConfig,
|
||||
onToggleAutoDelivery,
|
||||
onTestConnection,
|
||||
onSaveConfig,
|
||||
}: RockStrategyCardProps) {
|
||||
return (
|
||||
<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={onToggleAutoDelivery}
|
||||
checkedChildren="开启"
|
||||
unCheckedChildren="熔断"
|
||||
style={{
|
||||
backgroundColor: autoDelivery ? '#16a34a' : '#dc2626',
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
size="small"
|
||||
danger={autoDelivery}
|
||||
type={autoDelivery ? 'default' : 'primary'}
|
||||
onClick={() => onToggleAutoDelivery(!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={onTestConnection}
|
||||
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={onSaveConfig}
|
||||
loading={savingConfig}
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%)',
|
||||
minWidth: 130,
|
||||
}}
|
||||
>
|
||||
保存策略配置
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { Badge, Button, Card, Col, Input, Radio, Row, Space, Typography } from 'antd'
|
||||
import { ReloadOutlined, SearchOutlined } from '@ant-design/icons'
|
||||
import type { RockStats } from '../../api'
|
||||
|
||||
const { Text } = Typography
|
||||
|
||||
export interface RockTokenFilterBarProps {
|
||||
stats: RockStats | null
|
||||
searchKeyword: string
|
||||
filterStatus: string
|
||||
totalTokens: number
|
||||
loadingTokens: boolean
|
||||
onSearchKeywordChange: (value: string) => void
|
||||
onFilterStatusChange: (value: string) => void
|
||||
onRefresh: () => void
|
||||
}
|
||||
|
||||
export function RockTokenFilterBar({
|
||||
stats,
|
||||
searchKeyword,
|
||||
filterStatus,
|
||||
totalTokens,
|
||||
loadingTokens,
|
||||
onSearchKeywordChange,
|
||||
onFilterStatusChange,
|
||||
onRefresh,
|
||||
}: RockTokenFilterBarProps) {
|
||||
return (
|
||||
<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) => onSearchKeywordChange(e.target.value)}
|
||||
onPressEnter={onRefresh}
|
||||
style={{ width: 340 }}
|
||||
allowClear
|
||||
/>
|
||||
<Radio.Group
|
||||
value={filterStatus}
|
||||
onChange={(e) => {
|
||||
onFilterStatusChange(e.target.value)
|
||||
}}
|
||||
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={onRefresh} 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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
import type { Dispatch, SetStateAction } from 'react'
|
||||
import { Table, message } from 'antd'
|
||||
import type { RockTokenItem } from '../../api'
|
||||
import { rockApi } from '../../api'
|
||||
import { generateVoucherDataURL, copyVoucherDataUrlToClipboard } from '../../utils/voucher'
|
||||
import { formatDateTime } from '../../utils/time'
|
||||
import { buildTokenColumns } from './tokenColumns'
|
||||
import type { UidEditState } from './types'
|
||||
|
||||
export interface RockTokenTableProps {
|
||||
tokens: RockTokenItem[]
|
||||
loadingTokens: boolean
|
||||
page: number
|
||||
pageSize: number
|
||||
totalTokens: number
|
||||
copyingVoucherId: number | null
|
||||
setCopyingVoucherId: Dispatch<SetStateAction<number | null>>
|
||||
setPreviewVoucherUrl: Dispatch<SetStateAction<string>>
|
||||
setPreviewVoucherOpen: Dispatch<SetStateAction<boolean>>
|
||||
onEditUid: (state: UidEditState) => void
|
||||
onPageChange: (page: number, pageSize: number) => void
|
||||
onReloadData: () => void
|
||||
onReloadTokens: () => void
|
||||
}
|
||||
|
||||
export function RockTokenTable({
|
||||
tokens,
|
||||
loadingTokens,
|
||||
page,
|
||||
pageSize,
|
||||
totalTokens,
|
||||
copyingVoucherId,
|
||||
setCopyingVoucherId,
|
||||
setPreviewVoucherUrl,
|
||||
setPreviewVoucherOpen,
|
||||
onEditUid,
|
||||
onPageChange,
|
||||
onReloadData,
|
||||
onReloadTokens,
|
||||
}: RockTokenTableProps) {
|
||||
// 人工重置解冻 Mismatch Token
|
||||
const handleResetToken = async (id: number) => {
|
||||
try {
|
||||
await rockApi.resetToken(id)
|
||||
message.success('已解除绑定异常,重置为可用库存')
|
||||
onReloadData()
|
||||
onReloadTokens()
|
||||
} catch (e) {
|
||||
message.error('重置失败: ' + (e instanceof Error ? e.message : '未知错误'))
|
||||
}
|
||||
}
|
||||
|
||||
// 买家未扫码绑定时,把占用中的提货链接放回库存
|
||||
const handleReleaseToken = async (id: number) => {
|
||||
try {
|
||||
await rockApi.releaseToken(id)
|
||||
message.success('已释放回可用库存,该提货链接可以再次分配')
|
||||
onReloadData()
|
||||
onReloadTokens()
|
||||
} 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 = buildTokenColumns({
|
||||
copyingVoucherId,
|
||||
onCopyVoucher: handleCopyVoucher,
|
||||
onPreviewVoucher: handlePreviewVoucher,
|
||||
onEditUid,
|
||||
onResetToken: handleResetToken,
|
||||
onReleaseToken: handleReleaseToken,
|
||||
})
|
||||
|
||||
return (
|
||||
<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) => {
|
||||
onPageChange(p, ps)
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Input, Modal } from 'antd'
|
||||
import type { UidEditState } from './types'
|
||||
|
||||
export interface UidEditModalProps {
|
||||
uidEdit: UidEditState | null
|
||||
savingUid: boolean
|
||||
onValueChange: (value: string) => void
|
||||
onConfirm: () => void
|
||||
onCancel: () => void
|
||||
}
|
||||
|
||||
export function UidEditModal({
|
||||
uidEdit,
|
||||
savingUid,
|
||||
onValueChange,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
}: UidEditModalProps) {
|
||||
return (
|
||||
<Modal
|
||||
title="修改买家填写的 UID"
|
||||
open={Boolean(uidEdit)}
|
||||
okText="保存"
|
||||
cancelText="取消"
|
||||
confirmLoading={savingUid}
|
||||
onOk={() => void onConfirm()}
|
||||
onCancel={onCancel}
|
||||
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) => onValueChange(event.target.value.replace(/\D/g, '').slice(0, 15))}
|
||||
onPressEnter={() => void onConfirm()}
|
||||
/>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import { Button, Modal, Space, Typography, message } from 'antd'
|
||||
import { CopyOutlined, DownloadOutlined, SafetyCertificateOutlined } from '@ant-design/icons'
|
||||
import { copyVoucherDataUrlToClipboard } from '../../utils/voucher'
|
||||
|
||||
const { Text } = Typography
|
||||
|
||||
export interface VoucherPreviewModalProps {
|
||||
open: boolean
|
||||
previewVoucherUrl: string
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export function VoucherPreviewModal({
|
||||
open,
|
||||
previewVoucherUrl,
|
||||
onClose,
|
||||
}: VoucherPreviewModalProps) {
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
<Space>
|
||||
<SafetyCertificateOutlined style={{ color: '#10b981' }} />
|
||||
<span>发货到账存证凭据 (高清预览)</span>
|
||||
</Space>
|
||||
}
|
||||
open={open}
|
||||
onCancel={onClose}
|
||||
footer={[
|
||||
<Button key="close" onClick={onClose}>
|
||||
关闭
|
||||
</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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
import { Button, Popconfirm, Space, Tag, Tooltip, Typography } from 'antd'
|
||||
import { CopyOutlined, EyeOutlined, UndoOutlined } from '@ant-design/icons'
|
||||
import type { ColumnsType } from 'antd/es/table'
|
||||
import type { RockTokenItem } from '../../api'
|
||||
import type { UidEditState } from './types'
|
||||
|
||||
const { Text } = Typography
|
||||
|
||||
export interface TokenColumnHandlers {
|
||||
copyingVoucherId: number | null
|
||||
onCopyVoucher: (record: RockTokenItem) => void
|
||||
onPreviewVoucher: (record: RockTokenItem) => void
|
||||
onEditUid: (state: UidEditState) => void
|
||||
onResetToken: (id: number) => void
|
||||
onReleaseToken: (id: number) => void
|
||||
}
|
||||
|
||||
export function buildTokenColumns({
|
||||
copyingVoucherId,
|
||||
onCopyVoucher,
|
||||
onPreviewVoucher,
|
||||
onEditUid,
|
||||
onResetToken,
|
||||
onReleaseToken,
|
||||
}: TokenColumnHandlers): ColumnsType<RockTokenItem> {
|
||||
return [
|
||||
{
|
||||
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={() => onPreviewVoucher(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={() => onCopyVoucher(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={() => onPreviewVoucher(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={() =>
|
||||
onEditUid({
|
||||
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={() => onResetToken(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={() => onReleaseToken(record.id)}
|
||||
>
|
||||
<Button size="small" icon={<UndoOutlined />}>
|
||||
释放回库存
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
)
|
||||
}
|
||||
return <Text type="secondary">-</Text>
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export interface UidEditState {
|
||||
id: number
|
||||
value: string
|
||||
scanned: string
|
||||
roleName: string
|
||||
}
|
||||
Reference in New Issue
Block a user