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