feat(admin): 彻底优化任务列表与详情界面,集成到账存证凭单与联盟直充面板

- 优化任务列表角色列:支持从 affiliateDash 及 claimIdentity 中自动提取并展示角色名与 UID(含一键复制)
- 列表操作栏支持一键查看到账存证凭单,支持直接复制图片发给买家或下载保存
- 任务详情页全面现代化重构:
  - 新增「兑换成功 · 官方直充到账存证凭据」大卡片,实时生成 640x960 Retina 高清卡片,支持一键写入系统剪贴板(微信/QQ/客服窗口直接 Ctrl+V 发送)、下载 PNG 大图与全屏预览
  - 新增「联盟直充履约 (affiliate_dash)」专属面板,结构化展示上游订单号、直充状态、点数积分、核销状态及供应商回执
  - 优化凭据生成器 (voucher.ts):强制优先读取本地高清商品图,彻底消除上游图片文字水印与圆环破图问题
- 验证方式:npm run build & npm test (133 项单测全量通过)
This commit is contained in:
yml2213
2026-10-10 21:04:01 +08:00
parent 215f3351ba
commit f3f2550b19
7 changed files with 1136 additions and 228 deletions
+190 -12
View File
@@ -1,6 +1,8 @@
import {
CloseCircleOutlined,
CopyOutlined,
DownloadOutlined,
FileImageOutlined,
ReloadOutlined,
SearchOutlined,
UserSwitchOutlined,
@@ -11,9 +13,12 @@ import {
Button,
Card,
Form,
Image,
Input,
Modal,
Select,
Space,
Spin,
Table,
Tag,
Tooltip,
@@ -43,6 +48,11 @@ import {
resolveAdminPageSize,
} from '@/utils/admin-pagination'
import { formatAdminDateTime } from '@/utils/admin-time'
import {
copyVoucherDataUrlToClipboard,
downloadVoucherDataUrl,
generateVoucherDataURL,
} from '@/utils/voucher'
type TaskFilterForm = {
keyword?: string
@@ -68,10 +78,66 @@ export default function AdminTasksPage() {
const [searchParams, setSearchParams] = useSearchParams()
const [actionLoadingId, setActionLoadingId] = useState<number | null>(null)
const [lastClaimUrl, setLastClaimUrl] = useState('')
const [voucherModal, setVoucherModal] = useState<{
visible: boolean
task: AdminTaskListItem | null
dataUrl: string
loading: boolean
copying: boolean
}>({
visible: false,
task: null,
dataUrl: '',
loading: false,
copying: false,
})
const page = Number(searchParams.get('page') || 1) || 1
const pageSize = resolveAdminPageSize(searchParams.get('pageSize'), ADMIN_DEFAULT_PAGE_SIZE)
const canManageTaskLifecycle = hasAdminRole('operator')
const canCloseTasks = hasAdminRole('support')
const openVoucherModal = async (row: AdminTaskListItem) => {
setVoucherModal({
visible: true,
task: row,
dataUrl: '',
loading: true,
copying: false,
})
try {
const url = await generateVoucherDataURL({
productName: row.skuName,
productSKU: row.skuCode,
roleName: row.roleName,
roleUID: row.roleId,
redeemedAt: row.redeemedAt || row.updatedAt,
voucherNo: row.taskNo,
})
setVoucherModal((prev) => ({ ...prev, dataUrl: url, loading: false }))
} catch {
message.error('生成凭据图片失败')
setVoucherModal((prev) => ({ ...prev, loading: false }))
}
}
const handleCopyVoucher = async () => {
if (!voucherModal.dataUrl) return
setVoucherModal((prev) => ({ ...prev, copying: true }))
const success = await copyVoucherDataUrlToClipboard(voucherModal.dataUrl)
setVoucherModal((prev) => ({ ...prev, copying: false }))
if (success) {
message.success('凭据图片已复制到剪贴板,可在微信/QQ/客服聊天框直接 Ctrl+V 粘贴!')
} else {
message.warning('浏览器限制直接复制图片,请使用下方“下载凭据”按钮保存!')
}
}
const handleDownloadVoucher = () => {
if (!voucherModal.dataUrl) return
const filename = `和平精英_${voucherModal.task?.roleName || '直充'}_${voucherModal.task?.taskNo || '凭据'}.png`
downloadVoucherDataUrl(voucherModal.dataUrl, filename)
message.success('已开始下载凭据图片')
}
const filters = {
keyword:
searchParams.get('keyword') ||
@@ -222,16 +288,43 @@ export default function AdminTasksPage() {
),
},
{
title: '角色',
minWidth: 140,
render: (_, row) => (
<div className="cell-stack">
<span>{row.roleName || '-'}</span>
{row.roleId || row.loginType ? (
<span className="muted">{row.roleId || row.loginType}</span>
) : null}
</div>
),
title: '角色信息',
minWidth: 160,
render: (_, row) => {
const hasRole = Boolean(row.roleName && row.roleName !== '-')
const hasUid = Boolean(row.roleId && row.roleId !== '-')
if (!hasRole && !hasUid) {
return <span className="muted">-</span>
}
return (
<div className="cell-stack">
{hasRole ? (
<span style={{ fontWeight: 600, color: '#1e293b' }}>{row.roleName}</span>
) : null}
{hasUid ? (
<Space size={4}>
<span className="muted" style={{ fontFamily: 'monospace', fontSize: 12 }}>
{row.roleId}
</span>
<Button
type="text"
size="small"
icon={<CopyOutlined />}
title="复制角色 UID"
style={{ padding: 0, height: 'auto', color: '#64748b' }}
onClick={(e) => {
e.stopPropagation()
navigator.clipboard
.writeText(row.roleId)
.then(() => message.success('已复制 UID: ' + row.roleId))
.catch(() => message.error('复制失败'))
}}
/>
</Space>
) : null}
</div>
)
},
},
{
title: '更新时间',
@@ -244,7 +337,7 @@ export default function AdminTasksPage() {
},
{
title: '操作',
minWidth: 200,
minWidth: 220,
fixed: 'right',
render: (_, row) => {
const rowLoading = actionLoadingId === row.taskId
@@ -259,7 +352,11 @@ export default function AdminTasksPage() {
canManageTaskLifecycle &&
!['redeemed', 'closed', 'completed', 'manual_review'].includes(row.status)
const canClose = canCloseTasks && !['redeemed', 'closed'].includes(row.status)
const hasActions = canRetry || canManualReview || canClose
const hasVoucher =
['redeemed', 'delivered', 'completed', 'success'].includes(row.status) ||
row.deliveryStatus === 'delivered' ||
Boolean(row.roleName || row.roleId)
const hasActions = canRetry || canManualReview || canClose || hasVoucher
return (
<div className="cell-stack">
@@ -270,6 +367,18 @@ export default function AdminTasksPage() {
) : null}
{hasActions ? (
<Space size={[6, 6]} wrap>
{hasVoucher ? (
<Button
size="small"
type="primary"
ghost
icon={<FileImageOutlined />}
title="查看到账存证凭据"
onClick={() => openVoucherModal(row)}
>
凭据
</Button>
) : null}
{canRetry ? (
<Button
size="small"
@@ -487,6 +596,75 @@ export default function AdminTasksPage() {
})}
/>
</Card>
<Modal
title={
<Space>
<FileImageOutlined style={{ color: '#0284c7' }} />
<span>发货存证凭据 · 官方直充凭单</span>
</Space>
}
open={voucherModal.visible}
footer={
<Space wrap style={{ width: '100%', justifyContent: 'space-between' }}>
<Button
onClick={() => {
if (voucherModal.task?.taskId) {
navigate(`/admin/tasks/${voucherModal.task.taskId}`)
}
}}
>
查看任务详情
</Button>
<Space>
<Button
icon={<CopyOutlined />}
loading={voucherModal.copying}
disabled={!voucherModal.dataUrl || voucherModal.loading}
onClick={handleCopyVoucher}
>
复制图片
</Button>
<Button
type="primary"
icon={<DownloadOutlined />}
disabled={!voucherModal.dataUrl || voucherModal.loading}
onClick={handleDownloadVoucher}
>
下载大图
</Button>
</Space>
</Space>
}
onCancel={() => setVoucherModal((prev) => ({ ...prev, visible: false }))}
width={420}
centered
styles={{ body: { textAlign: 'center', padding: '16px 0' } }}
>
{voucherModal.loading ? (
<div style={{ padding: '60px 0' }}>
<Spin tip="正在生成高清存证大图..." />
</div>
) : voucherModal.dataUrl ? (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 12 }}>
<Image
src={voucherModal.dataUrl}
alt="发货存证凭据"
style={{
width: '100%',
maxWidth: 320,
borderRadius: 12,
boxShadow: '0 8px 30px rgba(0, 0, 0, 0.25)',
}}
/>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
提示:点击“复制图片”后可直接在微信/QQ客服聊天窗口中粘贴发给买家。
</Typography.Text>
</div>
) : (
<div style={{ padding: '40px 0', color: '#94a3b8' }}>暂无凭据生成</div>
)}
</Modal>
</section>
)
}