Files
order_site/apps/frontend/src/components/admin/ConsignmentEvidenceGallery.tsx
T
yml2213 ca04e09deb 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 张。临时数据已清理。
2026-09-17 11:22:59 +08:00

93 lines
2.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}