feat(consignment): 客服端凭证链可视化(提报凭证 + 赠送凭证)
问题:一件寄卖有两类凭证,客服界面只看得见第一类,还是 48x48 一张缩略图, 看不出是主页还是物品;第二类(证明"这件货真送出去了"的赠送完成截图)只在工单验收界面, 客服要证明赠送是否完成得自己跨模块翻,没有任何地方把两类凭证并排呈现。 - 两类凭证分别在哪里: 提报凭证 = 提报表 screenshots_json(卖家提报时的游戏主页截图,含物品/仓库详情) 赠送凭证 = 绑定工单的验收资料 work_orders.acceptance_json(冷却结束后上传、客服凭此验收) - 后端把凭证链从客服两个列表/详情接口一起下发:提报表/库存 → 结果库存 → 绑定工单, 新增 submissionScreenshots / giftEvidenceImages / giftEvidenceNote / giftEvidenceSubmittedAt / giftWorkOrderNo / giftWorkOrderStatus / giftEvidenceReviewStatus(none|pending|accepted|submitted); 赠送凭证图从 imageUrls 与 files[].url|mediumUrl|thumbnailUrl 两处取并去重 - 提报凭证口径统一:有来源提报单用提报单主页截图,客服手工入库现货回退库存自身截图, 前端只看 submissionScreenshots 一个字段,不会拿错 - 前端新增共享组件 ConsignmentEvidenceGallery(缩略图 + 大图预览 + 共 N 张, 预览用 Image.PreviewGroup items,被折叠的图在大图里仍能翻看);审核面板把「凭据截图」 拆成「提报凭证(游戏主页截图)」与「赠送凭证」(带待验收/已验收 Tag + 关联工单 Tooltip), 仓库面板新增「凭证」列并分组展示,两个详情弹窗同口径(含备注/提交时间/工单号/一句话说明) 真实库验证:赠送凭证解析在构造的工单验收资料上取到 3 张图(imageUrls 1 + files 2)并识别 pending_acceptance → 待客服验收;提报凭证在真实数据上取到 1 张。临时数据已清理。
This commit is contained in:
@@ -1,8 +1,5 @@
|
||||
import {
|
||||
CheckCircleOutlined,
|
||||
CopyOutlined,
|
||||
EyeOutlined,
|
||||
FileImageOutlined,
|
||||
PlusOutlined,
|
||||
ReloadOutlined,
|
||||
RollbackOutlined,
|
||||
@@ -18,7 +15,6 @@ import {
|
||||
Col,
|
||||
Descriptions,
|
||||
Form,
|
||||
Image,
|
||||
Input,
|
||||
InputNumber,
|
||||
Modal,
|
||||
@@ -36,6 +32,7 @@ import {
|
||||
import type { TableColumnsType } from 'antd'
|
||||
import { useState } from 'react'
|
||||
|
||||
import ConsignmentEvidenceGallery from '@/components/admin/ConsignmentEvidenceGallery'
|
||||
import {
|
||||
archiveAdminConsignmentInventory,
|
||||
createAdminConsignmentInventory,
|
||||
@@ -46,9 +43,11 @@ import {
|
||||
updateAdminConsignmentInventory,
|
||||
} from '@/services/admin/consignment'
|
||||
import { fetchAdminWorkerUsers } from '@/services/admin/worker-platform'
|
||||
import type { ConsignmentInventory } from '@/types/consignment'
|
||||
import type { ConsignmentInventory, GiftEvidenceReviewStatus } from '@/types/consignment'
|
||||
import { fetchAdminSession } from '@/services/admin/auth'
|
||||
import { resolveAdminPermission } from '@/utils/admin-auth'
|
||||
import { describeGiftEvidence, resolveGiftEvidenceBadge } from '@/utils/consignment-evidence'
|
||||
import { formatDateTime } from '@/utils/date-time'
|
||||
|
||||
const { Text } = Typography
|
||||
|
||||
@@ -92,6 +91,9 @@ const BUYER_COLORS: Record<string, string> = {
|
||||
客服B: 'magenta',
|
||||
}
|
||||
|
||||
/** 「凭证」列里两组的灰字小标题 */
|
||||
const EVIDENCE_CAPTION_STYLE = { fontSize: 12, color: '#8c8c8c' } as const
|
||||
|
||||
/**
|
||||
* 寄卖单号 = 来源提报单号(卖家提报时生成的提报单号就是唯一寄卖单号)。
|
||||
* 客服手工入库的现货没有来源提报,回退显示内部仓库号,保证客服对单口径一致。
|
||||
@@ -100,6 +102,33 @@ function resolveConsignmentNo(record: ConsignmentInventory) {
|
||||
return record.sourceSubmissionNo || record.inventoryNo
|
||||
}
|
||||
|
||||
/** 关联工单状态文案(后端 giftWorkOrderStatus:in_progress / pending_acceptance / accepted 等) */
|
||||
const GIFT_WORK_ORDER_STATUS_LABELS: Record<string, string> = {
|
||||
in_progress: '履约中',
|
||||
pending_acceptance: '待客服验收',
|
||||
accepted: '已验收',
|
||||
}
|
||||
|
||||
function formatGiftWorkOrderStatus(status?: string) {
|
||||
const normalized = String(status || '').trim()
|
||||
if (!normalized) return '状态未知'
|
||||
return GIFT_WORK_ORDER_STATUS_LABELS[normalized] || normalized
|
||||
}
|
||||
|
||||
/**
|
||||
* 赠送凭证为空时的占位文案:客服要能一眼看出「为什么还没有赠送凭证」,
|
||||
* 所以按有没有匹配到工单讲清原因,不能只写一个「无」。
|
||||
*/
|
||||
function resolveGiftEvidenceEmptyText(record: {
|
||||
giftEvidenceReviewStatus?: GiftEvidenceReviewStatus
|
||||
giftWorkOrderNo?: string
|
||||
}) {
|
||||
if (record.giftEvidenceReviewStatus === 'none') {
|
||||
return record.giftWorkOrderNo ? '待上传赠送截图' : '未匹配工单,待赠送'
|
||||
}
|
||||
return '未上传赠送截图'
|
||||
}
|
||||
|
||||
export default function ConsignmentInventoryPanel() {
|
||||
const { message } = App.useApp()
|
||||
|
||||
@@ -471,6 +500,52 @@ export default function ConsignmentInventoryPanel() {
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '凭证',
|
||||
key: 'evidence',
|
||||
width: 190,
|
||||
render: (_, record) => {
|
||||
const badge = resolveGiftEvidenceBadge(record.giftEvidenceReviewStatus ?? 'none')
|
||||
return (
|
||||
<Space direction="vertical" size={6}>
|
||||
<div>
|
||||
<div style={EVIDENCE_CAPTION_STYLE}>提报凭证</div>
|
||||
<ConsignmentEvidenceGallery
|
||||
images={record.submissionScreenshots}
|
||||
emptyText="无提报凭证"
|
||||
max={2}
|
||||
size={48}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div style={EVIDENCE_CAPTION_STYLE}>赠送凭证</div>
|
||||
<ConsignmentEvidenceGallery
|
||||
images={record.giftEvidenceImages}
|
||||
emptyText={resolveGiftEvidenceEmptyText(record)}
|
||||
max={2}
|
||||
size={48}
|
||||
/>
|
||||
<Space size={4} wrap style={{ marginTop: 2 }}>
|
||||
<Tag color={badge.color} style={{ marginInlineEnd: 0 }}>
|
||||
{badge.text}
|
||||
</Tag>
|
||||
{record.giftWorkOrderNo ? (
|
||||
<Tooltip
|
||||
title={`关联工单 ${record.giftWorkOrderNo}(工单状态:${formatGiftWorkOrderStatus(
|
||||
record.giftWorkOrderStatus,
|
||||
)})`}
|
||||
>
|
||||
<Tag color="default" style={{ marginInlineEnd: 0, cursor: 'help' }}>
|
||||
关联工单
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</Space>
|
||||
</div>
|
||||
</Space>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '允许匹配',
|
||||
key: 'allowMatching',
|
||||
@@ -596,6 +671,9 @@ export default function ConsignmentInventoryPanel() {
|
||||
},
|
||||
]
|
||||
|
||||
// 详情弹窗里的赠送凭证标签(未选记录时给默认值,Modal 未打开也不会取到 undefined)
|
||||
const viewingGiftBadge = resolveGiftEvidenceBadge(viewingItem?.giftEvidenceReviewStatus ?? 'none')
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
{/* 统计概览卡片 */}
|
||||
@@ -783,6 +861,7 @@ export default function ConsignmentInventoryPanel() {
|
||||
columns={columns}
|
||||
dataSource={items}
|
||||
loading={inventoriesQuery.isLoading}
|
||||
scroll={{ x: 1590 }}
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize,
|
||||
@@ -1279,6 +1358,62 @@ export default function ConsignmentInventoryPanel() {
|
||||
<Descriptions.Item label="卖家收款信息" span={2}>
|
||||
<Text copyable>{viewingItem.sellerPaymentInfo || '-'}</Text>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="提报凭证" span={2}>
|
||||
<Space direction="vertical" size={6} style={{ width: '100%' }}>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
游戏主页截图(含物品/仓库详情)
|
||||
</Text>
|
||||
<ConsignmentEvidenceGallery
|
||||
images={viewingItem.submissionScreenshots}
|
||||
emptyText="无提报凭证"
|
||||
max={8}
|
||||
size={80}
|
||||
/>
|
||||
</Space>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="赠送凭证" span={2}>
|
||||
<Space direction="vertical" size={6} style={{ width: '100%' }}>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
赠送完成截图(客服凭此验收)
|
||||
</Text>
|
||||
<Space size={8} wrap align="center">
|
||||
<ConsignmentEvidenceGallery
|
||||
images={viewingItem.giftEvidenceImages}
|
||||
emptyText={resolveGiftEvidenceEmptyText(viewingItem)}
|
||||
max={8}
|
||||
size={80}
|
||||
/>
|
||||
<Tag color={viewingGiftBadge.color}>{viewingGiftBadge.text}</Tag>
|
||||
</Space>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{describeGiftEvidence({
|
||||
status: viewingItem.giftEvidenceReviewStatus ?? 'none',
|
||||
images: viewingItem.giftEvidenceImages ?? [],
|
||||
submittedAt: viewingItem.giftEvidenceSubmittedAt,
|
||||
})}
|
||||
</Text>
|
||||
</Space>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="赠送备注" span={2}>
|
||||
{viewingItem.giftEvidenceNote || '-'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="凭证提交时间">
|
||||
{viewingItem.giftEvidenceSubmittedAt
|
||||
? formatDateTime(viewingItem.giftEvidenceSubmittedAt)
|
||||
: '-'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="关联工单">
|
||||
{viewingItem.giftWorkOrderNo ? (
|
||||
<Space size={4} wrap>
|
||||
<Text copyable>{viewingItem.giftWorkOrderNo}</Text>
|
||||
<Tag color="default">
|
||||
{formatGiftWorkOrderStatus(viewingItem.giftWorkOrderStatus)}
|
||||
</Tag>
|
||||
</Space>
|
||||
) : (
|
||||
<Text type="secondary">未匹配工单</Text>
|
||||
)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="登录方式">
|
||||
{(viewingItem.credentialsJson as any)?.loginMethod || '-'}
|
||||
</Descriptions.Item>
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
Col,
|
||||
Descriptions,
|
||||
Form,
|
||||
Image,
|
||||
Input,
|
||||
InputNumber,
|
||||
Modal,
|
||||
@@ -23,12 +22,14 @@ import {
|
||||
Space,
|
||||
Table,
|
||||
Tag,
|
||||
Tooltip,
|
||||
Typography,
|
||||
App,
|
||||
} from 'antd'
|
||||
import type { TableColumnsType } from 'antd'
|
||||
import { useState } from 'react'
|
||||
|
||||
import ConsignmentEvidenceGallery from '@/components/admin/ConsignmentEvidenceGallery'
|
||||
import {
|
||||
approveAdminConsignmentSubmission,
|
||||
fetchAdminConsignmentSubmissions,
|
||||
@@ -36,11 +37,40 @@ import {
|
||||
} from '@/services/admin/consignment'
|
||||
import { fetchAdminSession } from '@/services/admin/auth'
|
||||
import { fetchAdminWorkerUsers } from '@/services/admin/worker-platform'
|
||||
import type { ConsignmentSubmission } from '@/types/consignment'
|
||||
import type { ConsignmentSubmission, GiftEvidenceReviewStatus } from '@/types/consignment'
|
||||
import { resolveAdminPermission } from '@/utils/admin-auth'
|
||||
import { describeGiftEvidence, resolveGiftEvidenceBadge } from '@/utils/consignment-evidence'
|
||||
import { formatDateTime } from '@/utils/date-time'
|
||||
|
||||
const { Text } = Typography
|
||||
|
||||
/** 关联工单状态文案(后端 giftWorkOrderStatus:in_progress / pending_acceptance / accepted 等) */
|
||||
const GIFT_WORK_ORDER_STATUS_LABELS: Record<string, string> = {
|
||||
in_progress: '履约中',
|
||||
pending_acceptance: '待客服验收',
|
||||
accepted: '已验收',
|
||||
}
|
||||
|
||||
function formatGiftWorkOrderStatus(status?: string) {
|
||||
const normalized = String(status || '').trim()
|
||||
if (!normalized) return '状态未知'
|
||||
return GIFT_WORK_ORDER_STATUS_LABELS[normalized] || normalized
|
||||
}
|
||||
|
||||
/**
|
||||
* 赠送凭证为空时的占位文案:客服要能一眼看出「为什么还没有赠送凭证」,
|
||||
* 所以按有没有匹配到工单讲清原因,不能只写一个「无」。
|
||||
*/
|
||||
function resolveGiftEvidenceEmptyText(record: {
|
||||
giftEvidenceReviewStatus?: GiftEvidenceReviewStatus
|
||||
giftWorkOrderNo?: string
|
||||
}) {
|
||||
if (record.giftEvidenceReviewStatus === 'none') {
|
||||
return record.giftWorkOrderNo ? '待上传赠送截图' : '未匹配工单,待赠送'
|
||||
}
|
||||
return '未上传赠送截图'
|
||||
}
|
||||
|
||||
export default function ConsignmentSubmissionsPanel() {
|
||||
const { message } = App.useApp()
|
||||
|
||||
@@ -240,21 +270,45 @@ export default function ConsignmentSubmissionsPanel() {
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '凭据截图',
|
||||
key: 'screens',
|
||||
width: 110,
|
||||
title: '提报凭证(游戏主页截图)',
|
||||
key: 'submissionScreenshots',
|
||||
width: 170,
|
||||
render: (_, record) => (
|
||||
<ConsignmentEvidenceGallery
|
||||
images={record.submissionScreenshots}
|
||||
emptyText="提报时未上传主页截图"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '赠送凭证',
|
||||
key: 'giftEvidence',
|
||||
width: 170,
|
||||
render: (_, record) => {
|
||||
const screens = Array.isArray(record.screenshotsJson) ? record.screenshotsJson : []
|
||||
if (screens.length === 0) return <Text type="secondary">无截图</Text>
|
||||
const badge = resolveGiftEvidenceBadge(record.giftEvidenceReviewStatus ?? 'none')
|
||||
return (
|
||||
<Image.PreviewGroup>
|
||||
<Image
|
||||
src={screens[0]}
|
||||
width={48}
|
||||
height={48}
|
||||
style={{ objectFit: 'cover', borderRadius: 4 }}
|
||||
<Space direction="vertical" size={2}>
|
||||
<ConsignmentEvidenceGallery
|
||||
images={record.giftEvidenceImages}
|
||||
emptyText={resolveGiftEvidenceEmptyText(record)}
|
||||
/>
|
||||
</Image.PreviewGroup>
|
||||
<Space size={4} wrap>
|
||||
<Tag color={badge.color} style={{ marginInlineEnd: 0 }}>
|
||||
{badge.text}
|
||||
</Tag>
|
||||
{record.giftWorkOrderNo ? (
|
||||
<Tooltip
|
||||
title={`关联工单 ${record.giftWorkOrderNo}(工单状态:${formatGiftWorkOrderStatus(
|
||||
record.giftWorkOrderStatus,
|
||||
)})`}
|
||||
>
|
||||
<Tag color="default" style={{ marginInlineEnd: 0, cursor: 'help' }}>
|
||||
关联工单
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</Space>
|
||||
</Space>
|
||||
)
|
||||
},
|
||||
},
|
||||
@@ -330,6 +384,11 @@ export default function ConsignmentSubmissionsPanel() {
|
||||
},
|
||||
]
|
||||
|
||||
// 详情弹窗里的赠送凭证标签(未选记录时给默认值,Modal 未打开也不会取到 undefined)
|
||||
const viewingGiftBadge = resolveGiftEvidenceBadge(
|
||||
viewingSubmission?.giftEvidenceReviewStatus ?? 'none',
|
||||
)
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
<Card size="small">
|
||||
@@ -392,6 +451,7 @@ export default function ConsignmentSubmissionsPanel() {
|
||||
columns={submissionColumns}
|
||||
dataSource={submissions}
|
||||
loading={submissionsQuery.isLoading}
|
||||
scroll={{ x: 1490 }}
|
||||
pagination={{
|
||||
current: submissionPage,
|
||||
pageSize: submissionPageSize,
|
||||
@@ -651,24 +711,60 @@ export default function ConsignmentSubmissionsPanel() {
|
||||
<Descriptions.Item label="卖家收款方式" span={2}>
|
||||
<Text copyable>{viewingSubmission.sellerPaymentInfo || '-'}</Text>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="凭据截图" span={2}>
|
||||
{Array.isArray(viewingSubmission.screenshotsJson) &&
|
||||
viewingSubmission.screenshotsJson.length > 0 ? (
|
||||
<Image.PreviewGroup>
|
||||
<Space wrap>
|
||||
{viewingSubmission.screenshotsJson.map((src, i) => (
|
||||
<Image
|
||||
key={i}
|
||||
src={src}
|
||||
width={80}
|
||||
height={80}
|
||||
style={{ objectFit: 'cover', borderRadius: 4 }}
|
||||
/>
|
||||
))}
|
||||
</Space>
|
||||
</Image.PreviewGroup>
|
||||
<Descriptions.Item label="提报凭证" span={2}>
|
||||
<Space direction="vertical" size={6} style={{ width: '100%' }}>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
游戏主页截图(含物品/仓库详情)
|
||||
</Text>
|
||||
<ConsignmentEvidenceGallery
|
||||
images={viewingSubmission.submissionScreenshots}
|
||||
emptyText="提报时未上传游戏主页截图"
|
||||
max={8}
|
||||
size={80}
|
||||
/>
|
||||
</Space>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="赠送凭证" span={2}>
|
||||
<Space direction="vertical" size={6} style={{ width: '100%' }}>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
赠送完成截图(客服凭此验收)
|
||||
</Text>
|
||||
<Space size={8} wrap align="center">
|
||||
<ConsignmentEvidenceGallery
|
||||
images={viewingSubmission.giftEvidenceImages}
|
||||
emptyText={resolveGiftEvidenceEmptyText(viewingSubmission)}
|
||||
max={8}
|
||||
size={80}
|
||||
/>
|
||||
<Tag color={viewingGiftBadge.color}>{viewingGiftBadge.text}</Tag>
|
||||
</Space>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{describeGiftEvidence({
|
||||
status: viewingSubmission.giftEvidenceReviewStatus ?? 'none',
|
||||
images: viewingSubmission.giftEvidenceImages ?? [],
|
||||
submittedAt: viewingSubmission.giftEvidenceSubmittedAt,
|
||||
})}
|
||||
</Text>
|
||||
</Space>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="赠送备注" span={2}>
|
||||
{viewingSubmission.giftEvidenceNote || '-'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="凭证提交时间">
|
||||
{viewingSubmission.giftEvidenceSubmittedAt
|
||||
? formatDateTime(viewingSubmission.giftEvidenceSubmittedAt)
|
||||
: '-'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="关联工单">
|
||||
{viewingSubmission.giftWorkOrderNo ? (
|
||||
<Space size={4} wrap>
|
||||
<Text copyable>{viewingSubmission.giftWorkOrderNo}</Text>
|
||||
<Tag color="default">
|
||||
{formatGiftWorkOrderStatus(viewingSubmission.giftWorkOrderStatus)}
|
||||
</Tag>
|
||||
</Space>
|
||||
) : (
|
||||
<Text type="secondary">无截图</Text>
|
||||
<Text type="secondary">未匹配工单</Text>
|
||||
)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="审核状态">
|
||||
|
||||
Reference in New Issue
Block a user