689 lines
21 KiB
TypeScript
689 lines
21 KiB
TypeScript
import { useMemo, useState } from 'react'
|
|
import { keepPreviousData, useQuery } from '@tanstack/react-query'
|
|
import {
|
|
Alert,
|
|
Button,
|
|
Input,
|
|
InputNumber,
|
|
Select,
|
|
Space,
|
|
Tag,
|
|
Typography,
|
|
type TableColumnsType,
|
|
} from 'antd'
|
|
import { EyeOutlined, ReloadOutlined } from '@ant-design/icons'
|
|
import { useSearchParams, useNavigate } from 'react-router-dom'
|
|
|
|
import { fetchAdminListings, fetchAdminPendingListings } from '@/features/admin/api'
|
|
import { AdminPageHeader } from '@/features/admin/components/AdminPageHeader'
|
|
import { DataTable } from '@/features/admin/components/DataTable'
|
|
import { FilterBar, FilterField } from '@/features/admin/components/FilterBar'
|
|
import { StatusTag } from '@/features/admin/components/StatusTag'
|
|
import {
|
|
listingAccountLevel,
|
|
listingAllHeroText,
|
|
listingAssetText,
|
|
listingCharacterSkinText,
|
|
listingCodeText,
|
|
listingCoinText,
|
|
listingLevelText,
|
|
listingOwnerRemark,
|
|
listingPureCoinPriceText,
|
|
listingRatioText,
|
|
listingRegionText,
|
|
listingRentalDuration,
|
|
listingRentDepositText,
|
|
listingResourceText,
|
|
listingSkinGroupText,
|
|
} from '@/features/admin/listingDisplay'
|
|
import {
|
|
listingReviewStatusMeta,
|
|
listingReviewStatusOptions,
|
|
listingStatusMeta,
|
|
listingStatusOptions,
|
|
} from '@/features/listings/labels'
|
|
import type { AdminListing, AdminListingQuery } from '@/features/admin/types'
|
|
import { emptyListingPublishOptions, fetchListingPublishOptions } from '@/features/seller/options'
|
|
import { useAdminSessionStore } from '@/stores/adminSession'
|
|
import { getAdminPath } from '@/shared/api/adminClient'
|
|
import { formatText } from '@/shared/utils/format'
|
|
|
|
type ListingMode = 'all' | 'review'
|
|
|
|
const emptyFilters: AdminListingQuery = {
|
|
keyword: '',
|
|
owner_id: undefined,
|
|
status: '',
|
|
review_status: '',
|
|
insurance: '',
|
|
stamina: '',
|
|
load: '',
|
|
skin_group: '',
|
|
skin_name: '',
|
|
min_coin: undefined,
|
|
max_coin: undefined,
|
|
min_deposit: undefined,
|
|
max_deposit: undefined,
|
|
}
|
|
|
|
const fallbackInsuranceOptions = ['2*1', '2*2', '2*3', '3*3']
|
|
const fallbackLevelOptions = ['1级', '2级', '3级', '4级', '5级', '6级', '7级']
|
|
|
|
function insuranceLabel(value: string) {
|
|
const parts = value.split('*').map(Number)
|
|
if (parts.length === 2 && parts.every(part => Number.isFinite(part) && part > 0)) {
|
|
return `${parts[0] * parts[1]}格`
|
|
}
|
|
return value
|
|
}
|
|
|
|
function levelLabel(value: string) {
|
|
return value.replace(/级$/, '')
|
|
}
|
|
|
|
function EllipsisCell({ value }: { value: string }) {
|
|
return (
|
|
<span className="admin-listings-ellipsis" title={value}>
|
|
{value}
|
|
</span>
|
|
)
|
|
}
|
|
|
|
function RangeFilter({
|
|
min,
|
|
max,
|
|
minPlaceholder,
|
|
maxPlaceholder,
|
|
step,
|
|
precision,
|
|
onMinChange,
|
|
onMaxChange,
|
|
}: {
|
|
min?: number
|
|
max?: number
|
|
minPlaceholder: string
|
|
maxPlaceholder: string
|
|
step: number
|
|
precision: number
|
|
onMinChange: (value?: number) => void
|
|
onMaxChange: (value?: number) => void
|
|
}) {
|
|
return (
|
|
<div className="admin-range-filter">
|
|
<InputNumber
|
|
min={0}
|
|
step={step}
|
|
precision={precision}
|
|
controls={false}
|
|
placeholder={minPlaceholder}
|
|
value={min}
|
|
onChange={value => onMinChange(typeof value === 'number' ? value : undefined)}
|
|
/>
|
|
<span className="admin-range-filter__sep">-</span>
|
|
<InputNumber
|
|
min={0}
|
|
step={step}
|
|
precision={precision}
|
|
controls={false}
|
|
placeholder={maxPlaceholder}
|
|
value={max}
|
|
onChange={value => onMaxChange(typeof value === 'number' ? value : undefined)}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function buildColumns(
|
|
onOpen: (row: AdminListing) => void,
|
|
canView: boolean
|
|
): TableColumnsType<AdminListing> {
|
|
return [
|
|
{
|
|
title: '商品编码',
|
|
key: 'listing_no',
|
|
width: 118,
|
|
fixed: 'left',
|
|
render: (_, row) => (
|
|
<div className="admin-listing-code">
|
|
{canView ? (
|
|
<button type="button" className="admin-order-link" onClick={() => onOpen(row)}>
|
|
{listingCodeText(row)}
|
|
</button>
|
|
) : (
|
|
<strong>{listingCodeText(row)}</strong>
|
|
)}
|
|
<span className="admin-listing-code__tags">
|
|
<StatusTag meta={listingStatusMeta(row.status)} />
|
|
{row.review_status !== 'approved' ? (
|
|
<StatusTag meta={listingReviewStatusMeta(row.review_status)} />
|
|
) : null}
|
|
</span>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
title: '地区',
|
|
key: 'region',
|
|
width: 92,
|
|
render: (_, row) => <EllipsisCell value={listingRegionText(row)} />,
|
|
},
|
|
{
|
|
title: '上号方式',
|
|
dataIndex: 'login_platform',
|
|
width: 82,
|
|
render: value => <EllipsisCell value={formatText(value)} />,
|
|
},
|
|
{
|
|
title: '哈夫币(M)',
|
|
key: 'coin',
|
|
width: 88,
|
|
align: 'right',
|
|
render: (_, row) => <strong className="admin-listings-number">{listingCoinText(row)}</strong>,
|
|
},
|
|
{
|
|
title: '保险',
|
|
key: 'insurance',
|
|
width: 62,
|
|
align: 'center',
|
|
render: (_, row) => listingAssetText(row, 'season_insurance'),
|
|
},
|
|
{
|
|
title: '体力(级)',
|
|
key: 'stamina',
|
|
width: 74,
|
|
align: 'center',
|
|
render: (_, row) => listingLevelText(row, 'stamina_level'),
|
|
},
|
|
{
|
|
title: '负重(级)',
|
|
key: 'load',
|
|
width: 74,
|
|
align: 'center',
|
|
render: (_, row) => listingLevelText(row, 'load_level'),
|
|
},
|
|
{
|
|
title: '账号等级',
|
|
key: 'account_level',
|
|
width: 78,
|
|
align: 'center',
|
|
render: (_, row) => listingAccountLevel(row),
|
|
},
|
|
{
|
|
title: '绝密KD',
|
|
key: 'secret_kd',
|
|
width: 72,
|
|
align: 'center',
|
|
render: (_, row) => listingAssetText(row, 'secret_kd'),
|
|
},
|
|
{
|
|
title: 'awm子弹',
|
|
key: 'awm',
|
|
width: 76,
|
|
align: 'center',
|
|
render: (_, row) => listingResourceText(row, 'awmAmmo'),
|
|
},
|
|
{
|
|
title: '六头',
|
|
key: 'helmet6',
|
|
width: 54,
|
|
align: 'center',
|
|
render: (_, row) => listingResourceText(row, 'helmet6'),
|
|
},
|
|
{
|
|
title: '六甲',
|
|
key: 'armor6',
|
|
width: 54,
|
|
align: 'center',
|
|
render: (_, row) => listingResourceText(row, 'armor6'),
|
|
},
|
|
{
|
|
title: '特殊刀皮',
|
|
key: 'melee',
|
|
width: 126,
|
|
render: (_, row) => <EllipsisCell value={listingSkinGroupText(row, 'melee')} />,
|
|
},
|
|
{
|
|
title: '人物红皮/人物金皮/武器皮肤',
|
|
key: 'skins',
|
|
width: 300,
|
|
render: (_, row) => <EllipsisCell value={listingCharacterSkinText(row)} />,
|
|
},
|
|
{
|
|
title: '全英雄',
|
|
key: 'all_hero',
|
|
width: 72,
|
|
align: 'center',
|
|
render: (_, row) => listingAllHeroText(row),
|
|
},
|
|
{
|
|
title: '号主备注',
|
|
key: 'description',
|
|
width: 190,
|
|
render: (_, row) => <EllipsisCell value={listingOwnerRemark(row)} />,
|
|
},
|
|
{
|
|
title: '租客纯币价格',
|
|
key: 'pure_coin_price',
|
|
width: 118,
|
|
align: 'right',
|
|
render: (_, row) => <span className="admin-money">{listingPureCoinPriceText(row)}</span>,
|
|
},
|
|
{
|
|
title: '租期',
|
|
key: 'rental_duration',
|
|
width: 96,
|
|
align: 'center',
|
|
render: (_, row) => {
|
|
const duration = listingRentalDuration(row)
|
|
return (
|
|
<div className="admin-listing-duration">
|
|
<strong>{duration.days}</strong>
|
|
{duration.dailyLoss ? <small>{duration.dailyLoss}</small> : null}
|
|
</div>
|
|
)
|
|
},
|
|
},
|
|
{
|
|
title: '租金/押金',
|
|
key: 'rent_deposit',
|
|
width: 142,
|
|
align: 'right',
|
|
render: (_, row) => (
|
|
<span className="admin-listing-money-pair">{listingRentDepositText(row)}</span>
|
|
),
|
|
},
|
|
{
|
|
title: '比例',
|
|
key: 'ratio',
|
|
width: 70,
|
|
align: 'center',
|
|
render: (_, row) => <span className="admin-listings-ratio">{listingRatioText(row)}</span>,
|
|
},
|
|
{
|
|
title: '详情',
|
|
key: 'actions',
|
|
width: 74,
|
|
fixed: 'right',
|
|
align: 'center',
|
|
render: (_, row) =>
|
|
canView ? (
|
|
<Button
|
|
type="link"
|
|
size="small"
|
|
icon={<EyeOutlined />}
|
|
onClick={event => {
|
|
event.stopPropagation()
|
|
onOpen(row)
|
|
}}
|
|
>
|
|
详情
|
|
</Button>
|
|
) : (
|
|
<Typography.Text type="secondary">无权限</Typography.Text>
|
|
),
|
|
},
|
|
]
|
|
}
|
|
|
|
export function AdminListingsPage({ mode = 'all' }: { mode?: ListingMode }) {
|
|
const isReviewMode = mode === 'review'
|
|
const [searchParams] = useSearchParams()
|
|
const initialOwnerId = Number(searchParams.get('owner_id')) || undefined
|
|
const initialKeyword = searchParams.get('keyword')?.trim() ?? ''
|
|
const [draft, setDraft] = useState<AdminListingQuery>(() => ({
|
|
...emptyFilters,
|
|
keyword: initialKeyword,
|
|
owner_id: initialOwnerId,
|
|
review_status: isReviewMode ? 'pending' : '',
|
|
}))
|
|
const [applied, setApplied] = useState<AdminListingQuery>(() => ({
|
|
...emptyFilters,
|
|
keyword: initialKeyword,
|
|
owner_id: initialOwnerId,
|
|
review_status: isReviewMode ? 'pending' : '',
|
|
}))
|
|
const [page, setPage] = useState(1)
|
|
const [pageSize, setPageSize] = useState(10)
|
|
const navigate = useNavigate()
|
|
const permissions = useAdminSessionStore(state => state.admin?.permissions)
|
|
const canView = Boolean(permissions?.includes('*') || permissions?.includes('listing:view'))
|
|
|
|
const optionsQuery = useQuery({
|
|
queryKey: ['listing-publish-options'],
|
|
queryFn: fetchListingPublishOptions,
|
|
staleTime: 5 * 60_000,
|
|
})
|
|
const publishOptions = optionsQuery.data ?? emptyListingPublishOptions
|
|
const insuranceOptions = publishOptions.insurance_options.length
|
|
? publishOptions.insurance_options
|
|
: fallbackInsuranceOptions
|
|
const levelOptions = publishOptions.level_options.length
|
|
? publishOptions.level_options
|
|
: fallbackLevelOptions
|
|
const skinOptions = publishOptions.skin_groups.map(group => ({
|
|
label: group.title,
|
|
options: group.options.map(name => ({ value: name, label: name })),
|
|
}))
|
|
const selectedSkinNames = draft.skin_name
|
|
? draft.skin_name
|
|
.split(',')
|
|
.map(name => name.trim())
|
|
.filter(Boolean)
|
|
: []
|
|
|
|
const listQuery = useQuery({
|
|
queryKey: ['admin-listings', applied, page, pageSize],
|
|
queryFn: () => fetchAdminListings({ ...applied, page, page_size: pageSize }),
|
|
enabled: !isReviewMode,
|
|
placeholderData: keepPreviousData,
|
|
})
|
|
const pendingQuery = useQuery({
|
|
queryKey: ['admin-pending-listings'],
|
|
queryFn: fetchAdminPendingListings,
|
|
enabled: isReviewMode,
|
|
})
|
|
|
|
const rawItems = useMemo(
|
|
() => (isReviewMode ? (pendingQuery.data ?? []) : (listQuery.data?.items ?? [])),
|
|
[isReviewMode, pendingQuery.data, listQuery.data?.items]
|
|
)
|
|
|
|
const reviewFiltered = useMemo(() => {
|
|
if (!isReviewMode) return rawItems
|
|
const keyword = (applied.keyword ?? '').toLowerCase()
|
|
return rawItems.filter(item => {
|
|
if (applied.status && item.status !== applied.status) return false
|
|
if (applied.owner_id && item.owner_id !== applied.owner_id) return false
|
|
if (!keyword) return true
|
|
return [item.title, item.listing_no, item.owner_phone, item.owner_nickname]
|
|
.filter(Boolean)
|
|
.some(value => String(value).toLowerCase().includes(keyword))
|
|
})
|
|
}, [isReviewMode, rawItems, applied.keyword, applied.owner_id, applied.status])
|
|
|
|
const total = isReviewMode ? reviewFiltered.length : (listQuery.data?.total ?? 0)
|
|
const items = isReviewMode
|
|
? reviewFiltered.slice((page - 1) * pageSize, page * pageSize)
|
|
: rawItems
|
|
const isFetching = isReviewMode ? pendingQuery.isFetching : listQuery.isFetching
|
|
const isPending = isReviewMode ? pendingQuery.isPending : listQuery.isPending
|
|
const isError = isReviewMode ? pendingQuery.isError : listQuery.isError
|
|
const error = isReviewMode ? pendingQuery.error : listQuery.error
|
|
const refetch = () => (isReviewMode ? pendingQuery.refetch() : listQuery.refetch())
|
|
|
|
const publishedCount = items.filter(item => item.status === 'published').length
|
|
const rentedCount = items.filter(item => item.status === 'rented').length
|
|
const pendingReviewCount = items.filter(item => item.review_status === 'pending').length
|
|
|
|
const openDetail = (id: number) => {
|
|
if (!canView) return
|
|
navigate(getAdminPath(isReviewMode ? `/listings/review/${id}` : `/listings/${id}`))
|
|
}
|
|
|
|
const columns = buildColumns(row => openDetail(row.id), canView)
|
|
|
|
const patchFilter = (patch: Partial<AdminListingQuery>) => {
|
|
setDraft(current => ({ ...current, ...patch }))
|
|
}
|
|
|
|
const applyFilters = () => {
|
|
setApplied({
|
|
...draft,
|
|
keyword: draft.keyword?.trim() ?? '',
|
|
review_status: draft.review_status || (isReviewMode ? 'pending' : ''),
|
|
})
|
|
setPage(1)
|
|
}
|
|
|
|
const resetFilters = () => {
|
|
const next = { ...emptyFilters, review_status: isReviewMode ? 'pending' : '' }
|
|
setDraft(next)
|
|
setApplied(next)
|
|
setPage(1)
|
|
}
|
|
|
|
const setStatusFilter = (status: string) => {
|
|
const nextStatus = applied.status === status ? '' : status
|
|
const next = { ...applied, status: nextStatus, review_status: '' }
|
|
setDraft(next)
|
|
setApplied(next)
|
|
setPage(1)
|
|
}
|
|
|
|
const setReviewStatusFilter = (reviewStatus: string) => {
|
|
const nextReviewStatus = isReviewMode
|
|
? 'pending'
|
|
: applied.review_status === reviewStatus
|
|
? ''
|
|
: reviewStatus
|
|
const next = { ...applied, status: '', review_status: nextReviewStatus }
|
|
setDraft(next)
|
|
setApplied(next)
|
|
setPage(1)
|
|
}
|
|
|
|
const hasActiveFilters = Object.entries(draft).some(
|
|
([key, value]) => !(isReviewMode && key === 'review_status') && value !== '' && value != null
|
|
)
|
|
const errorText = error instanceof Error ? error.message : '请稍后重试或重新登录后台。'
|
|
const allActive = !applied.status && !applied.review_status
|
|
const reviewModeActive = applied.review_status === 'pending' && !applied.status
|
|
|
|
return (
|
|
<div className="admin-page admin-listings-page">
|
|
<AdminPageHeader
|
|
eyebrow={isReviewMode ? 'JIEYOU OPS · 审核' : 'JIEYOU OPS · 商品'}
|
|
title={isReviewMode ? '商品审核' : '商品管理'}
|
|
description={
|
|
isReviewMode
|
|
? '按提交顺序处理待审商品,核对账号、价格与截图后再上架。'
|
|
: '查看商品编码、地区、上号方式、账号资产、号主备注、租金押金、比例和预计租期。'
|
|
}
|
|
meta={
|
|
<Space size={8} wrap>
|
|
<Tag color="geekblue">共 {total} 件</Tag>
|
|
{isReviewMode ? <Tag color="gold">待审核队列</Tag> : null}
|
|
{isFetching && !isPending ? <Tag color="processing">正在更新</Tag> : null}
|
|
</Space>
|
|
}
|
|
extra={
|
|
<Button icon={<ReloadOutlined />} onClick={() => void refetch()} loading={isFetching}>
|
|
刷新数据
|
|
</Button>
|
|
}
|
|
/>
|
|
|
|
<section className="admin-listing-overview">
|
|
<button
|
|
type="button"
|
|
className={`admin-listing-stat${allActive ? ' is-active' : ''}`}
|
|
onClick={resetFilters}
|
|
>
|
|
<span>全部商品</span>
|
|
<strong>{total}</strong>
|
|
<small>当前筛选结果</small>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className={`admin-listing-stat is-published${applied.status === 'published' ? ' is-active' : ''}`}
|
|
onClick={() => setStatusFilter('published')}
|
|
>
|
|
<span>已上架</span>
|
|
<strong>{publishedCount}</strong>
|
|
<small>当前页</small>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className={`admin-listing-stat is-rented${applied.status === 'rented' ? ' is-active' : ''}`}
|
|
onClick={() => setStatusFilter('rented')}
|
|
>
|
|
<span>已锁定</span>
|
|
<strong>{rentedCount}</strong>
|
|
<small>当前页</small>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className={`admin-listing-stat is-review${reviewModeActive ? ' is-active' : ''}`}
|
|
onClick={() => setReviewStatusFilter('pending')}
|
|
>
|
|
<span>待审核</span>
|
|
<strong>{pendingReviewCount}</strong>
|
|
<small>当前页</small>
|
|
</button>
|
|
</section>
|
|
|
|
<FilterBar
|
|
onSearch={applyFilters}
|
|
onReset={hasActiveFilters ? resetFilters : undefined}
|
|
loading={isFetching}
|
|
>
|
|
<FilterField label="编号" width={240}>
|
|
<Input
|
|
allowClear
|
|
value={draft.keyword}
|
|
placeholder="商品编号 / 订单编号"
|
|
onChange={event => patchFilter({ keyword: event.target.value })}
|
|
/>
|
|
</FilterField>
|
|
<FilterField label="号主 ID" width={130}>
|
|
<Input
|
|
allowClear
|
|
inputMode="numeric"
|
|
value={draft.owner_id ?? ''}
|
|
placeholder="筛选号主"
|
|
onChange={event => {
|
|
const value = event.target.value.replace(/\D/g, '')
|
|
patchFilter({ owner_id: value ? Number(value) : undefined })
|
|
}}
|
|
/>
|
|
</FilterField>
|
|
<FilterField label="纯币(M)" width={260}>
|
|
<RangeFilter
|
|
min={draft.min_coin}
|
|
max={draft.max_coin}
|
|
minPlaceholder="最低哈夫币"
|
|
maxPlaceholder="最高哈夫币"
|
|
step={10}
|
|
precision={1}
|
|
onMinChange={value => patchFilter({ min_coin: value })}
|
|
onMaxChange={value => patchFilter({ max_coin: value })}
|
|
/>
|
|
</FilterField>
|
|
<FilterField label="押金(元)" width={260}>
|
|
<RangeFilter
|
|
min={draft.min_deposit}
|
|
max={draft.max_deposit}
|
|
minPlaceholder="最低押金"
|
|
maxPlaceholder="最高押金"
|
|
step={100}
|
|
precision={1}
|
|
onMinChange={value => patchFilter({ min_deposit: value })}
|
|
onMaxChange={value => patchFilter({ max_deposit: value })}
|
|
/>
|
|
</FilterField>
|
|
<FilterField label="商品状态" width={142}>
|
|
<Select
|
|
allowClear
|
|
value={draft.status || undefined}
|
|
placeholder="全部状态"
|
|
options={listingStatusOptions}
|
|
onChange={value => patchFilter({ status: value ?? '' })}
|
|
/>
|
|
</FilterField>
|
|
<FilterField label="审核状态" width={142}>
|
|
<Select
|
|
allowClear
|
|
value={draft.review_status || undefined}
|
|
placeholder="全部审核"
|
|
options={listingReviewStatusOptions}
|
|
onChange={value => patchFilter({ review_status: value ?? '' })}
|
|
/>
|
|
</FilterField>
|
|
<FilterField label="资产" width={360}>
|
|
<div className="admin-triple-filter">
|
|
<Select
|
|
allowClear
|
|
value={draft.insurance || undefined}
|
|
placeholder="保险"
|
|
options={insuranceOptions.map(value => ({
|
|
value,
|
|
label: insuranceLabel(value),
|
|
}))}
|
|
onChange={value => patchFilter({ insurance: value ?? '' })}
|
|
/>
|
|
<Select
|
|
allowClear
|
|
value={draft.stamina || undefined}
|
|
placeholder="体力"
|
|
options={levelOptions.map(value => ({
|
|
value,
|
|
label: levelLabel(value),
|
|
}))}
|
|
onChange={value => patchFilter({ stamina: value ?? '' })}
|
|
/>
|
|
<Select
|
|
allowClear
|
|
value={draft.load || undefined}
|
|
placeholder="负重"
|
|
options={levelOptions.map(value => ({
|
|
value,
|
|
label: levelLabel(value),
|
|
}))}
|
|
onChange={value => patchFilter({ load: value ?? '' })}
|
|
/>
|
|
</div>
|
|
</FilterField>
|
|
<FilterField label="皮肤" width={230}>
|
|
<Select
|
|
mode="multiple"
|
|
allowClear
|
|
maxTagCount="responsive"
|
|
value={selectedSkinNames}
|
|
placeholder="全部皮肤"
|
|
options={skinOptions}
|
|
onChange={values => patchFilter({ skin_name: values.join(','), skin_group: '' })}
|
|
/>
|
|
</FilterField>
|
|
</FilterBar>
|
|
|
|
{isError ? (
|
|
<Alert
|
|
type="error"
|
|
showIcon
|
|
className="admin-table-alert"
|
|
message="商品列表没有加载出来"
|
|
description={errorText}
|
|
action={
|
|
<Button size="small" onClick={() => void refetch()}>
|
|
重试
|
|
</Button>
|
|
}
|
|
/>
|
|
) : null}
|
|
|
|
<DataTable<AdminListing>
|
|
title={<span>{isReviewMode ? '待审核商品' : '商品列表'}</span>}
|
|
extra={
|
|
<Typography.Text type="secondary">
|
|
{isPending ? '加载中' : `第 ${page} 页 · 当前 ${items.length} 条`}
|
|
</Typography.Text>
|
|
}
|
|
className="admin-listings-table"
|
|
rowKey="id"
|
|
columns={columns}
|
|
dataSource={items}
|
|
loading={isFetching}
|
|
total={total}
|
|
page={page}
|
|
pageSize={pageSize}
|
|
onPageChange={(nextPage, nextPageSize) => {
|
|
setPage(nextPageSize !== pageSize ? 1 : nextPage)
|
|
setPageSize(nextPageSize)
|
|
}}
|
|
onRowClick={canView ? row => openDetail(row.id) : undefined}
|
|
emptyDescription={isReviewMode ? '没有待审核商品' : '没有匹配的商品'}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|