import { useCallback, useState } from 'react' import { Alert, Button, Empty, Input, Table, Tag, message } from 'antd' import { SearchOutlined } from '@ant-design/icons' import { rockApi, type RockTokenItem, type RockTokenStatusLog } from '../api' import { PageHeader } from '../components/PageHeader' import { formatDateTime } from '../utils/time' import { copyVoucherDataUrlToClipboard, generateVoucherDataURL } from '../utils/voucher' import { buildTokenColumns } from './platformRock/tokenColumns' import { TokenStatusLogDrawer } from './platformRock/TokenStatusLogDrawer' import { VoucherPreviewModal } from './platformRock/VoucherPreviewModal' export default function SupportRockTokens() { const [search, setSearch] = useState('') const [query, setQuery] = useState('') const [searched, setSearched] = useState(false) const [tokens, setTokens] = useState([]) const [total, setTotal] = useState(0) const [page, setPage] = useState(1) const [pageSize, setPageSize] = useState(20) const [loading, setLoading] = useState(false) const [statusLogToken, setStatusLogToken] = useState(null) const [copyingVoucherId, setCopyingVoucherId] = useState(null) const [openingUpstreamId, setOpeningUpstreamId] = useState(null) const [previewVoucherUrl, setPreviewVoucherUrl] = useState('') const [previewVoucherOpen, setPreviewVoucherOpen] = useState(false) const fetchTokens = useCallback( async (keyword: string, nextPage: number, nextSize: number) => { const trimmed = keyword.trim() if (!trimmed) return setLoading(true) try { const res = await rockApi.listSupportBoundTokens({ page: nextPage, page_size: nextSize, search: trimmed, }) setTokens(res.list || []) setTotal(res.total || 0) } catch (e) { setTokens([]) setTotal(0) message.error('查询失败: ' + (e instanceof Error ? e.message : '未知错误')) } finally { setLoading(false) } }, [], ) const handleSearch = () => { const trimmed = search.trim() if (!trimmed) { message.warning('请输入包ID、上游编号、提取码、兑换码、工单号、商户单号或角色 ID 后再查询') return } setQuery(trimmed) setPage(1) setSearched(true) void fetchTokens(trimmed, 1, pageSize) } const handleSearchChange = (value: string) => { setSearch(value) if (!value.trim()) { // 清空输入框时回到未查询状态,不自动加载数据。 setSearch('') setQuery('') setSearched(false) setTokens([]) setTotal(0) setPage(1) } } const handlePageChange = (nextPage: number, nextSize: number) => { if (!query) return setPage(nextPage) setPageSize(nextSize) void fetchTokens(query, nextPage, nextSize) } const handleOpenUpstream = async (id: number) => { // 先同步打开空白页,避免异步请求后 window.open 被浏览器拦截。 const win = window.open('about:blank', '_blank') setOpeningUpstreamId(id) try { const res = await rockApi.getTokenPackageLink(id) if (!win) { message.warning('浏览器拦截了新窗口,请允许弹窗后重试') return } win.opener = null win.location.href = res.url } catch (e) { win?.close() message.error('获取上游包链接失败: ' + (e instanceof Error ? e.message : '未知错误')) } finally { setOpeningUpstreamId(null) } } const loadStatusLogs = useCallback( async (tokenId: number): Promise => { const res = await rockApi.listSupportTokenStatusLogs(tokenId) return res.list || [] }, [], ) const handleCopyVoucher = async (record: RockTokenItem) => { setCopyingVoucherId(record.id) try { const isRockBlue = record.target_skin?.includes('蓝') const sku = isRockBlue ? 'rock_blue_sportswear' : 'rock_black_tshirt' const productName = record.target_skin || (isRockBlue ? '蓝色运动服上衣' : '黑色ROCK长袖T恤') const channel = record.actual_channel || record.region || '安卓-微信' const timeStr = record.redeemed_at || record.bound_at || record.created_at const cleanRole = (record.actual_role_name || '').trim() const dataURL = await generateVoucherDataURL({ productName, productSKU: sku, roleName: cleanRole && cleanRole !== '-' ? cleanRole : '', roleUID: record.expected_uid || record.actual_role_id || '-', channel, currencyText: '幸运币 x 10', redeemedAt: timeStr ? formatDateTime(timeStr) : formatDateTime(new Date().toISOString()), }) const copied = await copyVoucherDataUrlToClipboard(dataURL) if (copied) { message.success('发货结果图片已复制到剪贴板!可直接在微信/QQ聊天框粘贴 (Ctrl+V)') } else { setPreviewVoucherUrl(dataURL) setPreviewVoucherOpen(true) message.info('已生成凭据图片,已为您打开预览窗口,支持直接保存或右键复制') } } catch (e) { message.error('生成凭证失败: ' + (e instanceof Error ? e.message : '未知错误')) } finally { setCopyingVoucherId(null) } } const handlePreviewVoucher = async (record: RockTokenItem) => { try { const isRockBlue = record.target_skin?.includes('蓝') const sku = isRockBlue ? 'rock_blue_sportswear' : 'rock_black_tshirt' const productName = record.target_skin || (isRockBlue ? '蓝色运动服上衣' : '黑色ROCK长袖T恤') const channel = record.actual_channel || record.region || '安卓-微信' const timeStr = record.redeemed_at || record.bound_at || record.created_at const cleanRole = (record.actual_role_name || '').trim() const dataURL = await generateVoucherDataURL({ productName, productSKU: sku, roleName: cleanRole && cleanRole !== '-' ? cleanRole : '', roleUID: record.expected_uid || record.actual_role_id || '-', channel, currencyText: '幸运币 x 10', redeemedAt: timeStr ? formatDateTime(timeStr) : formatDateTime(new Date().toISOString()), }) setPreviewVoucherUrl(dataURL) setPreviewVoucherOpen(true) } catch (e) { message.error('生成凭证失败: ' + (e instanceof Error ? e.message : '未知错误')) } } const columns = buildTokenColumns({ copyingVoucherId, usingWarnDays: 3, usingDangerDays: 5, boundWarnDays: 2, readOnly: true, refreshingTokenId: null, onRefreshBinding: () => {}, openingUpstreamId, onOpenUpstream: handleOpenUpstream, onCopyVoucher: handleCopyVoucher, onPreviewVoucher: handlePreviewVoucher, onEditUid: () => {}, onViewRawData: () => {}, onResetToken: () => {}, onReleaseToken: () => {}, onViewStatusLogs: setStatusLogToken, onUpdateRemark: () => {}, onMarkAbnormal: () => {}, }) return (
共 {total} 条 : undefined} />
handleSearchChange(e.target.value)} onPressEnter={handleSearch} placeholder="包ID / 上游编号 / 提取码 / 兑换码 / 工单号 / 商户单号 / 角色 ID / 角色名" style={{ flex: '1 1 360px', minWidth: 0 }} />
{!searched ? (
为避免一次展示过多数据,本页面不会默认加载绑定中的 Token 列表。
) : ( `共 ${count} 条绑定中 Token`, onChange: handlePageChange, }} /> )} setStatusLogToken(null)} loadLogs={loadStatusLogs} /> setPreviewVoucherOpen(false)} /> ) }