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:
yml2213
2026-09-17 11:22:59 +08:00
parent 44de11a3a7
commit ca04e09deb
10 changed files with 849 additions and 41 deletions
@@ -0,0 +1,92 @@
import { Image, Typography } from 'antd'
import type { CSSProperties } from 'react'
type ConsignmentEvidenceGalleryProps = {
/** 凭证图片地址列表;空数组 / 空值时渲染 emptyText */
images?: string[] | null
/** 没有凭证时的占位文案(灰色小字) */
emptyText?: string
/** 列表里最多铺几张缩略图,超出的用「+N」覆盖层提示 */
max?: number
/** 缩略图边长(px) */
size?: number
}
const DEFAULT_MAX = 3
const DEFAULT_SIZE = 52
/**
* 寄卖凭证画廊:把一组凭证图渲染成「缩略图 + 点击看大图」。
*
* 客服原来只能看到一张 48x48 的小图,既看不清是什么、也看不到全部;
* 这里统一成缩略图 + `共 N 张` 提示,点击任意缩略图即可放大预览**全部**图片
* (预览项来自 `items`,所以被 `max` 折叠的图在大图预览里依然可以左右切换)。
*/
export default function ConsignmentEvidenceGallery({
images,
emptyText = '无',
max = DEFAULT_MAX,
size = DEFAULT_SIZE,
}: ConsignmentEvidenceGalleryProps) {
const list = normalizeImages(images)
if (list.length === 0) {
return (
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{emptyText}
</Typography.Text>
)
}
const visibleCount = Math.max(1, Math.min(max, list.length))
const visible = list.slice(0, visibleCount)
const hiddenCount = list.length - visible.length
return (
<Image.PreviewGroup items={list}>
<div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 6 }}>
{visible.map((src, index) => (
<div key={`${index}-${src}`} style={{ position: 'relative', lineHeight: 0 }}>
<Image
src={src}
width={size}
height={size}
style={{ objectFit: 'cover', borderRadius: 6, border: '1px solid #e5e7eb' }}
/>
{hiddenCount > 0 && index === visible.length - 1 ? (
<span style={MORE_OVERLAY_STYLE} title={`还有 ${hiddenCount} 张,点击看全部`}>
+{hiddenCount}
</span>
) : null}
</div>
))}
{hiddenCount > 0 ? (
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
共 {list.length} 张
</Typography.Text>
) : null}
</div>
</Image.PreviewGroup>
)
}
/** 「+N」覆盖层:不吃鼠标事件,点击仍然穿透到缩略图打开大图预览 */
const MORE_OVERLAY_STYLE: CSSProperties = {
position: 'absolute',
inset: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 6,
background: 'rgba(0, 0, 0, 0.45)',
color: '#fff',
fontSize: 13,
fontWeight: 600,
lineHeight: 1,
pointerEvents: 'none',
}
function normalizeImages(images?: string[] | null) {
if (!Array.isArray(images)) return []
return images.map((src) => String(src ?? '').trim()).filter(Boolean)
}