问题:一件寄卖有两类凭证,客服界面只看得见第一类,还是 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 张。临时数据已清理。
98 lines
3.7 KiB
TypeScript
98 lines
3.7 KiB
TypeScript
/**
|
||
* 寄卖凭证(提报凭证 / 赠送凭证)的纯展示逻辑。
|
||
*
|
||
* 客服要在列表和详情里一句话看清「赠送凭证交没交、交了几张、验没验」,
|
||
* 所以把文案规则集中在这里。刻意写成纯函数:不依赖 React / antd,
|
||
* 也不使用 `@/` 别名,方便 `node --experimental-strip-types --test` 直接跑单测。
|
||
*/
|
||
import type { GiftEvidenceReviewStatus } from '../types/consignment'
|
||
|
||
export type { GiftEvidenceReviewStatus }
|
||
|
||
export type GiftEvidenceBadge = {
|
||
/** 标签文案 */
|
||
text: string
|
||
/** antd Tag 的 color 取值 */
|
||
color: string
|
||
}
|
||
|
||
export type GiftEvidenceSummaryInput = {
|
||
status: GiftEvidenceReviewStatus
|
||
/** 赠送凭证图片(赠送完成截图) */
|
||
images: string[]
|
||
/** 凭证提交时间(ISO 字符串,可能为空) */
|
||
submittedAt?: string
|
||
}
|
||
|
||
/**
|
||
* 赠送凭证状态 → 客服标签。
|
||
* 后端以后新增状态时不能崩:未知状态一律回退成「未提交」。
|
||
*/
|
||
export function resolveGiftEvidenceBadge(status: GiftEvidenceReviewStatus): GiftEvidenceBadge {
|
||
switch (status) {
|
||
case 'pending':
|
||
return { text: '待客服验收', color: 'gold' }
|
||
case 'accepted':
|
||
return { text: '已验收', color: 'green' }
|
||
case 'submitted':
|
||
return { text: '已提交', color: 'blue' }
|
||
default:
|
||
return { text: '未提交', color: 'default' }
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 给客服看的一句话赠送凭证说明:
|
||
* - `未提交`
|
||
* - `已提交 3 张,待客服验收`
|
||
* - `已验收(3 张)· 2026-09-17 10:00`
|
||
* - 有图但没时间 / 时间非法:省略 ` · 时间` 部分
|
||
* - 已提交但 0 张图:`已提交,无截图`(带状态后缀时如 `已提交,无截图,待客服验收`)
|
||
* - `none`(以及未来新增状态)只看成「还没交」:即使残留了时间也不拼进文案,避免自相矛盾
|
||
*/
|
||
export function describeGiftEvidence(input: GiftEvidenceSummaryInput): string {
|
||
const count = countEvidenceImages(input?.images)
|
||
const time = formatEvidenceTime(input?.submittedAt)
|
||
|
||
switch (input?.status) {
|
||
case 'pending':
|
||
return appendEvidenceTime(
|
||
count > 0 ? `已提交 ${count} 张,待客服验收` : '已提交,无截图,待客服验收',
|
||
time,
|
||
)
|
||
case 'accepted':
|
||
return appendEvidenceTime(count > 0 ? `已验收(${count} 张)` : '已验收,无截图', time)
|
||
case 'submitted':
|
||
return appendEvidenceTime(count > 0 ? `已提交 ${count} 张` : '已提交,无截图', time)
|
||
default:
|
||
return '未提交'
|
||
}
|
||
}
|
||
|
||
/** 追加提交时间;没有有效时间就原样返回(不留下孤零零的分隔符) */
|
||
function appendEvidenceTime(text: string, time: string): string {
|
||
if (!time) return text
|
||
// 以全角右括号收尾的短句(如「已验收(3 张)」)直接紧凑接 ·,其余用空格分隔更易读
|
||
return text.endsWith(')') ? `${text}· ${time}` : `${text} · ${time}`
|
||
}
|
||
|
||
/** 统计有效图片张数:空字符串 / 空白串不算凭证 */
|
||
function countEvidenceImages(images?: string[] | null): number {
|
||
if (!Array.isArray(images)) return 0
|
||
return images.filter((src) => String(src ?? '').trim()).length
|
||
}
|
||
|
||
/** 凭证提交时间 → `YYYY-MM-DD HH:mm`;空值或非法时间返回空串(调用方直接省略这段) */
|
||
function formatEvidenceTime(submittedAt?: string | null): string {
|
||
const normalized = String(submittedAt ?? '').trim()
|
||
if (!normalized) return ''
|
||
|
||
const date = new Date(normalized)
|
||
if (Number.isNaN(date.getTime())) return ''
|
||
|
||
const pad = (value: number) => String(value).padStart(2, '0')
|
||
const datePart = `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
|
||
const timePart = `${pad(date.getHours())}:${pad(date.getMinutes())}`
|
||
return `${datePart} ${timePart}`
|
||
}
|