import { Button, Dropdown, Popconfirm, Space, Tag, Tooltip, Typography } from 'antd' import { CopyOutlined, ExportOutlined, EyeOutlined, FileTextOutlined, HistoryOutlined, MessageOutlined, StopOutlined, SyncOutlined, UndoOutlined, } from '@ant-design/icons' import type { ColumnsType } from 'antd/es/table' import type { RockTokenItem } from '../../api' import { formatDateTime } from '../../utils/time' import { TokenRemarkCell } from './TokenRemarkCell' import type { UidEditState } from './types' const { Text } = Typography const MS_PER_DAY = 24 * 60 * 60 * 1000 // 满 24 小时算 1 天:超时预警按实际时长计算,不按自然日跨天。 function elapsedDaysSince(value?: string): number | null { if (!value) return null const startedAt = new Date(value).getTime() if (Number.isNaN(startedAt)) return null const diff = Date.now() - startedAt return diff > 0 ? diff / MS_PER_DAY : 0 } // 展示用天数:向下取整到 1 位,避免 1.99 天被四舍五入成 2.0 天而误以为已超时。 function floorDays(days: number): number { return Math.floor(days * 10) / 10 } // 优先使用后端返回的服务端天数,旧接口没有该字段时回退到本地计算。 function serverElapsedDays(serverDays: number | undefined, at?: string): number | null { if (typeof serverDays === 'number') return serverDays return elapsedDaysSince(at) } // Token 主信息:编号/兑换码 与 包ID/提取码 上下两行展示, // 两个复制按钮各自只复制兑换码 / 提取码,编号与包 ID 不参与复制。 export function TokenInfoCell({ record }: { record: RockTokenItem }) { const redeemCode = (record.redeem_code || '').trim() const extractCode = (record.token || '').trim() const tokenNo = record.upstream_token_id || '-' const packageNo = record.package_id ? String(record.package_id) : '-' return (
#{record.id} {tokenNo}/{redeemCode || '-'}
包ID {packageNo}/{extractCode || '-'}
) } export interface TokenColumnHandlers { copyingVoucherId: number | null usingWarnDays: number usingDangerDays: number boundWarnDays: number readOnly?: boolean refreshingTokenId: number | null onRefreshBinding: (id: number) => void openingUpstreamId: number | null onOpenUpstream: (id: number) => void onCopyVoucher: (record: RockTokenItem) => void onPreviewVoucher: (record: RockTokenItem) => void onEditUid: (state: UidEditState) => void onViewRawData: (record: RockTokenItem) => void onResetToken: (id: number) => void onReleaseToken: (record: RockTokenItem) => void onViewStatusLogs: (record: RockTokenItem) => void onUpdateRemark: (id: number, remark: string, status?: string) => void onMarkAbnormal: (id: number, remark?: string) => void } export function buildTokenColumns({ copyingVoucherId, usingWarnDays, usingDangerDays, boundWarnDays, readOnly, refreshingTokenId, onRefreshBinding, openingUpstreamId, onOpenUpstream, onCopyVoucher, onPreviewVoucher, onEditUid, onViewRawData, onResetToken, onReleaseToken, onViewStatusLogs, onUpdateRemark, onMarkAbnormal, }: TokenColumnHandlers): ColumnsType { return [ { title: 'ID / Token(编号/兑换码 · 包ID/提取码)', key: 'token_info', width: 320, render: (_, record) => , }, { title: '规格与适用范围', dataIndex: 'color', width: 120, render: (_, record) => ( 通用黑色 Token {record.region || 'Q/V区通用'} ), }, { title: '库存状态', dataIndex: 'status', width: 165, render: (status, record) => { switch (status) { case 'available': return ( ● 可用库存 ) case 'using': { const days = serverElapsedDays(record.using_days, record.using_at) const usingOverdue = record.using_overdue ?? (days !== null && days >= usingWarnDays) const usingDanger = record.using_danger ?? (days !== null && days >= usingDangerDays) const displayDays = days === null ? 0 : floorDays(days) if (usingDanger) { return ( ⚠️ 使用中超时 {displayDays.toFixed(1)} 天 ) } if (usingOverdue) { return ( ● 扫码使用中 · {displayDays.toFixed(1)} 天 ) } return ( ● 扫码使用中 ) } case 'bound': { const boundDays = serverElapsedDays(record.bound_days, record.bound_at) const boundOverdue = record.bound_overdue ?? (boundDays !== null && boundDays >= boundWarnDays) if (boundOverdue) { const displayDays = boundDays === null ? 0 : floorDays(boundDays) return ( ⏱ 绑定超时 {displayDays.toFixed(1)} 天 ) } return ( ● 已授权绑定 ) } case 'redeemed': return ( ● 已交付发货 ) case 'mismatch': return ( ⚠️ 绑定错误 (阻断) ) case 'abnormal': return ( 🚫 已上报失效 ) default: return {status} } }, }, { title: '流转时间', width: 180, render: (_, record) => { const usingDays = record.status === 'using' ? serverElapsedDays(record.using_days, record.using_at) : null const boundDays = record.status === 'bound' ? serverElapsedDays(record.bound_days, record.bound_at) : null const usingOverdue = record.using_overdue ?? (usingDays !== null && usingDays >= usingWarnDays) const usingDanger = record.using_danger ?? (usingDays !== null && usingDays >= usingDangerDays) const boundOverdue = record.bound_overdue ?? (boundDays !== null && boundDays >= boundWarnDays) const usingColor = usingDays === null ? '#64748b' : usingDanger ? '#dc2626' : usingOverdue ? '#d97706' : '#0891b2' return (
入库: {formatDateTime(record.created_at)}
{record.status === 'using' && record.using_at && (
⏳ 使用中: {formatDateTime(record.using_at)} {usingDays !== null ? `(${floorDays(usingDays).toFixed(1)} 天)` : ''}
)} {record.mismatch_at && (
⚠️ 绑错: {formatDateTime(record.mismatch_at)}
)} {record.redeemed_at && (
✅ 发货: {formatDateTime(record.redeemed_at)}
)} {!record.redeemed_at && record.bound_at && (
🔗 绑定: {formatDateTime(record.bound_at)} {boundDays !== null ? `(${floorDays(boundDays).toFixed(1)} 天)` : ''}
)} {!record.bound_at && !record.mismatch_at && record.claimed_at && (
📦 提货: {formatDateTime(record.claimed_at)}
)}
) }, }, { title: '关联业务工单', dataIndex: 'assigned_order_no', width: 158, render: (orderNo, record) => { if (!orderNo) { return - } return ( {/* 系统履约工单号 */}
{orderNo}
{/* 创建订单时的商户单号 */} {record.client_order_no ? (
商户: {record.client_order_no}
) : null}
) }, }, { title: '预期 UID vs 扫码角色 (风控门禁)', width: 255, render: (_, record) => { const isMismatch = record.status === 'mismatch' return (
预期 UID: {record.expected_uid || '-'}
扫码反查: {record.actual_role_name ? `${record.actual_role_name} (${record.actual_role_id || '-'})` : '-'}
{isMismatch && record.mismatch_reason && (
{record.mismatch_reason}
)}
) }, }, { title: '备注', width: 44, align: 'center', render: (_, record) => readOnly ? ( record.remark ? ( {record.remark} } > ) : ( - ) ) : ( ), }, { title: '凭证', key: 'voucher', width: 88, align: 'center', render: (_, record) => { if (record.status !== 'redeemed' && !record.upstream_fulfill_id) { return - } const isCopying = copyingVoucherId === record.id const isRockBlue = record.target_skin?.includes('蓝') const sku = isRockBlue ? 'rock_blue_sportswear' : 'rock_black_tshirt' const imgSrc = `/images/products/${sku}.webp` return (
, label: '复制发货结果图' }, { key: 'preview', icon: , label: '预览高清大图' }, ], onClick: ({ key }) => { if (key === 'copy') onCopyVoucher(record) if (key === 'preview') onPreviewVoucher(record) }, }} >
{record.target_skin
) }, }, ...(readOnly ? [] : [ { title: '原始', width: 44, align: 'center' as const, render: (_: unknown, record: RockTokenItem) => record.raw_data ? ( ) const upstreamDisabled = !record.package_id const upstreamTip = upstreamDisabled ? '该 Token 未关联上游打包记录,无法跳转上游' : record.package_share_code ? '' : '本地暂无包分享码,点击时会自动向上游反查' const upstreamButton = ( ) const historyButton = ( ) if (readOnly) { return ( {upstreamButton} {historyButton} ) } if (record.status === 'mismatch') { return ( {refreshButton} {upstreamButton} onMarkAbnormal(record.id, '绑定错误-已上报上游 (已失效)')} > onResetToken(record.id)} > {historyButton} ) } if (record.status === 'abnormal') { return ( 已失效作废 {upstreamButton} onResetToken(record.id)} > {historyButton} ) } if (record.status === 'using') { return ( {refreshButton} {upstreamButton} {historyButton} ) } if (record.status === 'bound') { return ( {refreshButton} {upstreamButton} {historyButton} ) } return ( {upstreamButton} {historyButton} ) }, }, ] }