fix:Rock Token列表ID合并与列宽优化

This commit is contained in:
2026-11-08 14:42:42 +08:00
parent d6530b47f4
commit 852debb635
4 changed files with 164 additions and 174 deletions
@@ -275,7 +275,7 @@ export function RockTokenTable({
columns={columns}
dataSource={tokens}
loading={loadingTokens}
scroll={{ x: 1700 }}
scroll={{ x: 1100 }}
rowSelection={{
selectedRowKeys: selectedTokenIds,
preserveSelectedRowKeys: true,
@@ -17,6 +17,7 @@ import type { ColumnsType } from 'antd/es/table'
import { UndoOutlined } from '@ant-design/icons'
import { rockApi, type RockTokenItem, type RockTokenRestoreResult } from '../../api'
import { formatDateTime } from '../../utils/time'
import { TokenInfoCell } from './tokenColumns'
const { Text } = Typography
@@ -181,43 +182,11 @@ export function TokenRecycleBinDrawer({
).length
const columns: ColumnsType<RockTokenItem> = [
{ title: 'ID', dataIndex: 'id', width: 80 },
{
title: '0666 编号 / 兑换码',
dataIndex: 'upstream_token_id',
width: 360,
onCell: () => ({ style: { whiteSpace: 'nowrap' } }),
render: (_, record) => {
const text = `${record.upstream_token_id || '-'}/${record.redeem_code || '-'}`
// 复制按钮只复制兑换码,编号不参与复制
const copyText = (record.redeem_code || '').trim()
return (
<Text
copyable={copyText ? { text: copyText } : false}
style={{ fontSize: 12, fontFamily: 'monospace', fontWeight: 600, whiteSpace: 'nowrap' }}
>
{text}
</Text>
)
},
},
{
title: '包ID / 提取码',
width: 260,
render: (_, record) => {
const packageText = record.package_id ? String(record.package_id) : '-'
const text = `${packageText}/${record.token || '-'}`
// 复制按钮只复制提取码,包 ID 不参与复制
const copyText = (record.token || '').trim()
return (
<Text
copyable={copyText ? { text: copyText } : false}
style={{ fontSize: 12, fontFamily: 'monospace', fontWeight: 600, wordBreak: 'break-all' }}
>
{text}
</Text>
)
},
title: 'ID / Token(编号/兑换码 · 包ID/提取码)',
key: 'token_info',
width: 330,
render: (_, record) => <TokenInfoCell record={record} />,
},
{
title: '状态',
@@ -230,19 +199,19 @@ export function TokenRecycleBinDrawer({
{
title: '订单号',
dataIndex: 'assigned_order_no',
width: 200,
width: 180,
render: (value?: string) => value || '-',
},
{
title: '角色',
dataIndex: 'actual_role_name',
width: 140,
width: 130,
render: (value?: string) => value || '-',
},
{
title: '删除时间',
dataIndex: 'deleted_at',
width: 170,
width: 160,
render: (value?: string | null) => formatDateTime(value),
},
{
@@ -347,7 +316,7 @@ export function TokenRecycleBinDrawer({
columns={columns}
dataSource={rows}
loading={loading}
scroll={{ x: 1560 }}
scroll={{ x: 1160 }}
rowSelection={{
selectedRowKeys: selectedIds,
onChange: (keys, selected) => {
@@ -6,6 +6,7 @@ import {
DeleteOutlined,
EditOutlined,
ExclamationCircleOutlined,
MessageOutlined,
StopOutlined,
} from '@ant-design/icons'
import type { RockTokenItem } from '../../api'
@@ -16,9 +17,16 @@ export interface TokenRemarkCellProps {
record: RockTokenItem
onUpdateRemark: (id: number, remark: string, status?: string) => void
onMarkAbnormal: (id: number, remark?: string) => void
// 紧凑模式:表格里只渲染一个图标按钮,备注内容通过悬浮提示和弹层查看
compact?: boolean
}
export function TokenRemarkCell({ record, onUpdateRemark, onMarkAbnormal }: TokenRemarkCellProps) {
export function TokenRemarkCell({
record,
onUpdateRemark,
onMarkAbnormal,
compact,
}: TokenRemarkCellProps) {
const [open, setOpen] = useState(false)
const [inputValue, setInputValue] = useState(record.remark || '')
@@ -165,6 +173,31 @@ export function TokenRemarkCell({ record, onUpdateRemark, onMarkAbnormal }: Toke
const isAbnormalOrReported =
record.status === 'abnormal' || (record.remark && record.remark.includes('上报'))
if (compact) {
const hasRemark = Boolean(record.remark)
return (
<Popover
content={content}
trigger="click"
open={open}
onOpenChange={(v) => {
setOpen(v)
if (v) setInputValue(record.remark || '')
}}
placement="bottomLeft"
>
<Button
size="small"
type={hasRemark ? 'primary' : 'text'}
danger={hasRemark && Boolean(isAbnormalOrReported)}
icon={hasRemark ? <MessageOutlined /> : <EditOutlined />}
title={hasRemark ? `备注:${record.remark}(点击修改)` : '添加备注'}
style={{ width: 26, height: 26, padding: 0, fontSize: 12 }}
/>
</Popover>
)
}
if (!record.remark) {
return (
<Popover
+120 -132
View File
@@ -1,5 +1,13 @@
import { Button, Popconfirm, Space, Tag, Tooltip, Typography } from 'antd'
import { CopyOutlined, ExportOutlined, EyeOutlined, StopOutlined, SyncOutlined, UndoOutlined } from '@ant-design/icons'
import { Button, Dropdown, Popconfirm, Space, Spin, Tag, Tooltip, Typography } from 'antd'
import {
CopyOutlined,
ExportOutlined,
EyeOutlined,
FileTextOutlined,
StopOutlined,
SyncOutlined,
UndoOutlined,
} from '@ant-design/icons'
import type { ColumnsType } from 'antd/es/table'
import type { RockTokenItem } from '../../api'
import { formatDateTime } from '../../utils/time'
@@ -19,6 +27,48 @@ function usingElapsedDays(usingAt?: string): number | null {
return diff > 0 ? diff / MS_PER_DAY : 0
}
// 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 (
<Space direction="vertical" size={1} style={{ width: '100%', lineHeight: 1.45 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 4, minWidth: 0 }}>
<span style={{ flexShrink: 0, fontSize: 10, color: '#94a3b8' }}>#{record.id}</span>
<Text
copyable={redeemCode ? { text: redeemCode, tooltips: ['复制兑换码', '已复制兑换码'] } : false}
style={{
fontSize: 11,
fontFamily: 'monospace',
fontWeight: 600,
color: '#0f172a',
whiteSpace: 'nowrap',
}}
>
{tokenNo}/{redeemCode || '-'}
</Text>
</div>
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 4, minWidth: 0 }}>
<span style={{ flexShrink: 0, fontSize: 10, color: '#94a3b8', lineHeight: '18px' }}>包ID</span>
<Text
copyable={extractCode ? { text: extractCode, tooltips: ['复制提取码', '已复制提取码'] } : false}
style={{
fontSize: 11,
fontFamily: 'monospace',
color: '#64748b',
wordBreak: 'break-all',
}}
>
{packageNo}/{extractCode || '-'}
</Text>
</div>
</Space>
)
}
export interface TokenColumnHandlers {
copyingVoucherId: number | null
usingWarnDays: number
@@ -56,62 +106,25 @@ export function buildTokenColumns({
}: TokenColumnHandlers): ColumnsType<RockTokenItem> {
return [
{
title: 'ID',
dataIndex: 'id',
width: 80,
align: 'right',
render: (id: number) => (
<Text style={{ fontSize: 12, fontFamily: 'monospace', color: '#64748b' }}>{id}</Text>
),
},
{
title: '0666 编号 / 兑换码',
dataIndex: 'upstream_token_id',
width: 360,
onCell: () => ({ style: { whiteSpace: 'nowrap' } }),
render: (_, record) => {
const text = `${record.upstream_token_id || '-'}/${record.redeem_code || '-'}`
// 复制按钮只复制兑换码,编号不参与复制
const copyText = (record.redeem_code || '').trim()
return (
<Text
copyable={copyText ? { text: copyText } : false}
style={{ fontSize: 12, fontFamily: 'monospace', fontWeight: 600, whiteSpace: 'nowrap' }}
>
{text}
</Text>
)
},
},
{
title: '包ID / 提取码',
width: 260,
render: (_, record) => {
const packageText = record.package_id ? String(record.package_id) : '-'
const text = `${packageText}/${record.token || '-'}`
// 复制按钮只复制提取码,包 ID 不参与复制
const copyText = (record.token || '').trim()
return (
<Text
copyable={copyText ? { text: copyText } : false}
style={{ fontSize: 12, fontFamily: 'monospace', fontWeight: 600, wordBreak: 'break-all' }}
>
{text}
</Text>
)
},
title: 'ID / Token(编号/兑换码 · 包ID/提取码)',
key: 'token_info',
width: 330,
render: (_, record) => <TokenInfoCell record={record} />,
},
{
title: '规格与适用范围',
dataIndex: 'color',
width: 150,
width: 120,
render: (_, record) => (
<Space direction="vertical" size={2}>
<Tag color="geekblue" style={{ borderRadius: 4, fontWeight: 500 }}>
<Tag
color="geekblue"
style={{ borderRadius: 4, fontWeight: 500, fontSize: 11, padding: '0 4px', margin: 0 }}
>
通用黑色 Token
</Tag>
<Text type="secondary" style={{ fontSize: 12 }}>
{record.region || 'Q/V区通用 · 自动分发'}
<Text type="secondary" style={{ fontSize: 11 }}>
{record.region || 'Q/V区通用'}
</Text>
</Space>
),
@@ -119,7 +132,7 @@ export function buildTokenColumns({
{
title: '库存状态',
dataIndex: 'status',
width: 170,
width: 165,
render: (status, record) => {
switch (status) {
case 'available':
@@ -193,7 +206,7 @@ export function buildTokenColumns({
},
{
title: '流转时间',
width: 210,
width: 180,
render: (_, record) => {
const usingDays = record.status === 'using' ? usingElapsedDays(record.using_at) : null
const usingColor =
@@ -248,7 +261,7 @@ export function buildTokenColumns({
{
title: '关联业务工单',
dataIndex: 'assigned_order_no',
width: 220,
width: 168,
render: (orderNo, record) => {
if (!orderNo) {
return <Text type="secondary">-</Text>
@@ -286,7 +299,7 @@ export function buildTokenColumns({
},
{
title: '预期 UID vs 扫码角色 (风控门禁)',
width: 250,
width: 255,
render: (_, record) => {
const isMismatch = record.status === 'mismatch'
return (
@@ -323,19 +336,22 @@ export function buildTokenColumns({
},
{
title: '备注',
width: 200,
width: 44,
align: 'center',
render: (_, record) => (
<TokenRemarkCell
record={record}
onUpdateRemark={onUpdateRemark}
onMarkAbnormal={onMarkAbnormal}
compact
/>
),
},
{
title: '发货结果图片',
title: '凭证',
key: 'voucher',
width: 240,
width: 56,
align: 'center',
render: (_, record) => {
if (record.status !== 'redeemed' && !record.upstream_fulfill_id) {
return <Text type="secondary">-</Text>
@@ -346,91 +362,63 @@ export function buildTokenColumns({
const imgSrc = `/images/products/${sku}.webp`
return (
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
{/* 商品缩略图:鼠标悬浮微放、点击快速预览高清大图凭据 */}
<Tooltip title="点击预览深海蓝发货凭据大图">
<div
onClick={() => onPreviewVoucher(record)}
style={{
width: 44,
height: 44,
flexShrink: 0,
borderRadius: 8,
border: '1px solid #e2e8f0',
background: '#f8fafc',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
overflow: 'hidden',
boxShadow: '0 1px 3px rgba(0,0,0,0.06)',
transition: 'all 0.2s ease',
}}
onMouseEnter={(e) => {
e.currentTarget.style.transform = 'scale(1.08)'
e.currentTarget.style.borderColor = '#93c5fd'
e.currentTarget.style.boxShadow = '0 3px 8px rgba(37,99,235,0.2)'
}}
onMouseLeave={(e) => {
e.currentTarget.style.transform = 'scale(1)'
e.currentTarget.style.borderColor = '#e2e8f0'
e.currentTarget.style.boxShadow = '0 1px 3px rgba(0,0,0,0.06)'
}}
>
<Dropdown
trigger={['click']}
menu={{
items: [
{ key: 'copy', icon: <CopyOutlined />, label: '复制发货结果图' },
{ key: 'preview', icon: <EyeOutlined />, label: '预览高清大图' },
],
onClick: ({ key }) => {
if (key === 'copy') onCopyVoucher(record)
if (key === 'preview') onPreviewVoucher(record)
},
}}
>
<div
title={`${record.target_skin || '黑色ROCK长袖T恤'} · 点击复制或预览发货凭据`}
style={{
width: 34,
height: 34,
borderRadius: 8,
border: '1px solid #e2e8f0',
background: '#f8fafc',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
overflow: 'hidden',
margin: '0 auto',
}}
>
{isCopying ? (
<Spin size="small" />
) : (
<img
src={imgSrc}
alt={record.target_skin || '道具'}
style={{ width: '100%', height: '100%', objectFit: 'contain', padding: 2 }}
/>
</div>
</Tooltip>
{/* 操作按钮与商品徽章 */}
<Space direction="vertical" size={3}>
<Space size={4}>
<Button
size="small"
type="primary"
icon={<CopyOutlined />}
loading={isCopying}
onClick={() => onCopyVoucher(record)}
style={{
background: 'linear-gradient(135deg, #059669 0%, #10b981 100%)',
borderColor: '#059669',
boxShadow: '0 1px 2px rgba(16, 185, 129, 0.2)',
fontSize: 12,
fontWeight: 500,
padding: '0 8px',
}}
>
复制结果图
</Button>
<Tooltip title="查看高清发货凭据大图">
<Button
size="small"
icon={<EyeOutlined />}
onClick={() => onPreviewVoucher(record)}
/>
</Tooltip>
</Space>
<div>
<Tag color="cyan" style={{ fontSize: 11, margin: 0 }}>
{record.target_skin || '黑色ROCK长袖T恤'}
</Tag>
</div>
</Space>
</div>
)}
</div>
</Dropdown>
)
},
},
{
title: '原始数据',
width: 100,
title: '原始',
width: 44,
align: 'center',
render: (_, record) =>
record.raw_data ? (
<Button size="small" onClick={() => onViewRawData(record)}>
查看
</Button>
<Tooltip title="查看原始数据">
<Button
size="small"
type="text"
icon={<FileTextOutlined />}
onClick={() => onViewRawData(record)}
/>
</Tooltip>
) : (
<Text type="secondary">-</Text>
),
@@ -438,7 +426,7 @@ export function buildTokenColumns({
{
title: '操作',
key: 'action',
width: 200,
width: 140,
fixed: 'right',
render: (_, record) => {
const refreshButton = (