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