feat(delivery-voucher): 优化发货凭据为一键复制深海蓝高清图并补齐商品本地图片
1. 管理后台 Rock 表格优化:将原「官方发货凭证(FulfillId)」列升级为「发货结果图片」列,提供一键「复制结果图」与「高清预览」Modal,点击即可自动生成凭据大图并直接写入剪贴板(支持在微信/QQ客服聊天框中直接 Ctrl+V 粘贴图片); 2. 商品图片本地缓存与展示修复:从官方 HAR 提取黑色 ROCK 长袖 T 恤与蓝色运动服上衣高清立绘(rock_black_tshirt.webp / rock_blue_sportswear.webp),优化 productImage 自动识别与 Delivery 提货页图片自适应居中,彻底消除灰色占位块; 3. 标杆级发货凭据绘制重构:参考腾讯官方电竞风格设计,重构 640x960 视网膜双倍高清 Canvas 凭据绘制(深海蓝微光渐变背景、顶部微光加粗「兑换成功」、正圆形双层光环立绘、详细角色区服参数对齐、底部翡翠绿发光胶囊按钮),全方位超越 0666 官方粗糙原图; 4. 履约结果字段持久化:后端发货成功后同步持久化角色名、游戏大区与 UID 至 fulfillment_orders 结果报文,确保提货页区服与角色信息完整呈现。
This commit is contained in:
@@ -431,7 +431,7 @@ export default function Delivery() {
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 16, marginBottom: 20, paddingBottom: 16, borderBottom: '1px solid #e2e8f0', flexWrap: 'wrap' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 16, flex: 1, minWidth: 220 }}>
|
||||
{(() => {
|
||||
const imgSrc = resolveProductImage({ sku: order.product?.sku, url: order.product?.image })
|
||||
const imgSrc = resolveProductImage({ sku: order.product?.sku, name: order.product?.name, url: order.product?.image })
|
||||
if (!imgSrc) {
|
||||
return (
|
||||
<div style={{ width: 72, height: 72, background: '#e2e8f0', borderRadius: 8, display: 'grid', placeItems: 'center', color: '#64748b' }}>
|
||||
@@ -445,7 +445,13 @@ export default function Delivery() {
|
||||
width={72}
|
||||
height={72}
|
||||
alt={order.product?.name || '商品'}
|
||||
style={{ objectFit: 'cover', borderRadius: 8, border: '1px solid #e2e8f0' }}
|
||||
style={{
|
||||
objectFit: 'contain',
|
||||
background: '#f8fafc',
|
||||
borderRadius: 8,
|
||||
border: '1px solid #e2e8f0',
|
||||
padding: 4,
|
||||
}}
|
||||
fallback="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='72' height='72'%3E%3Crect fill='%23e2e8f0' width='72' height='72'/%3E%3C/svg%3E"
|
||||
/>
|
||||
)
|
||||
@@ -492,7 +498,9 @@ export default function Delivery() {
|
||||
{order.role_name || '-'}
|
||||
</Typography.Text>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="区服信息">{order.game_channel || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="区服信息">
|
||||
{order.game_channel || (order.game_uid?.length === 10 ? '微信区' : '手Q / 微信区')}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="下单时间">{formatDateTime(order.created_at)}</Descriptions.Item>
|
||||
<Descriptions.Item label="完成时间">
|
||||
{order.shipped_at ? formatDateTime(order.shipped_at) : '已完成'}
|
||||
|
||||
@@ -23,7 +23,10 @@ import {
|
||||
import {
|
||||
CheckCircleOutlined,
|
||||
ControlOutlined,
|
||||
CopyOutlined,
|
||||
DownloadOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
EyeOutlined,
|
||||
FireOutlined,
|
||||
KeyOutlined,
|
||||
ReloadOutlined,
|
||||
@@ -36,6 +39,8 @@ import {
|
||||
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
|
||||
|
||||
@@ -49,6 +54,11 @@ export default function PlatformRock() {
|
||||
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)
|
||||
@@ -208,6 +218,68 @@ export default function PlatformRock() {
|
||||
}
|
||||
}
|
||||
|
||||
// 复制/生成发货凭证图片
|
||||
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 dataURL = await generateVoucherDataURL({
|
||||
productName,
|
||||
productSKU: sku,
|
||||
roleName: record.actual_role_name || '-',
|
||||
roleUID: record.expected_uid || record.actual_role_id || '-',
|
||||
channel,
|
||||
currencyText: '幸运币 x 10',
|
||||
redeemedAt: timeStr ? formatDateTime(timeStr) : formatDateTime(new Date().toISOString()),
|
||||
voucherNo: record.upstream_fulfill_id ? String(record.upstream_fulfill_id) : (record.assigned_order_no || undefined),
|
||||
})
|
||||
|
||||
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 dataURL = await generateVoucherDataURL({
|
||||
productName,
|
||||
productSKU: sku,
|
||||
roleName: record.actual_role_name || '-',
|
||||
roleUID: record.expected_uid || record.actual_role_id || '-',
|
||||
channel,
|
||||
currencyText: '幸运币 x 10',
|
||||
redeemedAt: timeStr ? formatDateTime(timeStr) : formatDateTime(new Date().toISOString()),
|
||||
voucherNo: record.upstream_fulfill_id ? String(record.upstream_fulfill_id) : (record.assigned_order_no || undefined),
|
||||
})
|
||||
|
||||
setPreviewVoucherUrl(dataURL)
|
||||
setPreviewVoucherOpen(true)
|
||||
} catch (e) {
|
||||
message.error('生成凭证失败: ' + (e instanceof Error ? e.message : '未知错误'))
|
||||
}
|
||||
}
|
||||
|
||||
const columns: ColumnsType<RockTokenItem> = [
|
||||
{
|
||||
title: 'ID / 提取码',
|
||||
@@ -333,24 +405,59 @@ export default function PlatformRock() {
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '官方发货凭证 (FulfillId)',
|
||||
dataIndex: 'upstream_fulfill_id',
|
||||
width: 180,
|
||||
render: (fulfillId, record) =>
|
||||
fulfillId ? (
|
||||
<Space direction="vertical" size={2}>
|
||||
<Text code copyable strong style={{ color: '#059669', fontSize: 12 }}>
|
||||
{fulfillId}
|
||||
</Text>
|
||||
<div>
|
||||
<Tag color="default" style={{ fontSize: 11 }}>
|
||||
{record.target_skin || '和平精英道具'}
|
||||
title: '发货结果图片',
|
||||
key: 'voucher',
|
||||
width: 200,
|
||||
render: (_, record) => {
|
||||
if (record.status !== 'redeemed' && !record.upstream_fulfill_id) {
|
||||
return <Text type="secondary">-</Text>
|
||||
}
|
||||
const isCopying = copyingVoucherId === record.id
|
||||
return (
|
||||
<Space direction="vertical" size={4} style={{ width: '100%' }}>
|
||||
<Space size={6}>
|
||||
<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,
|
||||
}}
|
||||
>
|
||||
复制结果图
|
||||
</Button>
|
||||
<Tooltip title="查看高清深海蓝发货凭据">
|
||||
<Button
|
||||
size="small"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={() => handlePreviewVoucher(record)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<Tag color="cyan" style={{ fontSize: 11, margin: 0 }}>
|
||||
{record.target_skin || '黑色ROCK长袖T恤'}
|
||||
</Tag>
|
||||
{record.upstream_fulfill_id && (
|
||||
<Text
|
||||
type="secondary"
|
||||
copyable={{ text: String(record.upstream_fulfill_id) }}
|
||||
style={{ fontSize: 11, fontFamily: 'monospace' }}
|
||||
title={`官方凭据号: ${record.upstream_fulfill_id}`}
|
||||
>
|
||||
{record.upstream_fulfill_id}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
</Space>
|
||||
) : (
|
||||
<Text type="secondary">-</Text>
|
||||
),
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
@@ -948,6 +1055,82 @@ export default function PlatformRock() {
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user