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:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user