feat(admin): 优化后台宽屏布局与商品管理列表
This commit is contained in:
@@ -1,6 +1,16 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import { keepPreviousData, useQuery } from '@tanstack/react-query'
|
||||
import { Alert, Button, Input, Select, Space, Tag, Typography, type TableColumnsType } from 'antd'
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Input,
|
||||
InputNumber,
|
||||
Select,
|
||||
Space,
|
||||
Tag,
|
||||
Typography,
|
||||
type TableColumnsType,
|
||||
} from 'antd'
|
||||
import { EyeOutlined, ReloadOutlined } from '@ant-design/icons'
|
||||
import { useSearchParams } from 'react-router-dom'
|
||||
|
||||
@@ -10,6 +20,23 @@ 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,
|
||||
@@ -17,108 +44,287 @@ import {
|
||||
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 { formatDateTime, formatMoney, formatText } from '@/shared/utils/format'
|
||||
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,
|
||||
mode: ListingMode
|
||||
canView: boolean
|
||||
): TableColumnsType<AdminListing> {
|
||||
return [
|
||||
{
|
||||
title: '商品',
|
||||
dataIndex: 'title',
|
||||
width: 260,
|
||||
title: '商品编码',
|
||||
key: 'listing_no',
|
||||
width: 118,
|
||||
fixed: 'left',
|
||||
render: (_, row) => (
|
||||
<div className="admin-stack">
|
||||
<div className="admin-listing-code">
|
||||
{canView ? (
|
||||
<button type="button" className="admin-order-link" onClick={() => onOpen(row)}>
|
||||
{row.title || '未命名商品'}
|
||||
{listingCodeText(row)}
|
||||
</button>
|
||||
) : (
|
||||
<strong>{row.title || '未命名商品'}</strong>
|
||||
<strong>{listingCodeText(row)}</strong>
|
||||
)}
|
||||
<span className="admin-mono">{row.listing_no}</span>
|
||||
<span>
|
||||
{formatText(row.server_region)} · {formatText(row.login_platform)}
|
||||
<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: 'owner',
|
||||
width: 170,
|
||||
render: (_, row) => (
|
||||
<div className="admin-stack">
|
||||
<span>{formatText(row.owner_phone)}</span>
|
||||
<span>
|
||||
{row.owner_nickname || '未设置昵称'} · ID {row.owner_id}
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
title: '地区',
|
||||
key: 'region',
|
||||
width: 92,
|
||||
render: (_, row) => <EllipsisCell value={listingRegionText(row)} />,
|
||||
},
|
||||
{
|
||||
title: '价格',
|
||||
key: 'price',
|
||||
width: 140,
|
||||
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) => (
|
||||
<div className="admin-amount">
|
||||
<strong>{formatMoney(row.price_cent)}</strong>
|
||||
<span>押金 {formatMoney(row.deposit_amount_cent)}</span>
|
||||
<span>哈夫币 {row.haf_coin_amount.toLocaleString('zh-CN')}</span>
|
||||
</div>
|
||||
<span className="admin-money">{listingPureCoinPriceText(row)}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '商品状态',
|
||||
dataIndex: 'status',
|
||||
width: 120,
|
||||
render: value => <StatusTag meta={listingStatusMeta(value)} />,
|
||||
},
|
||||
{
|
||||
title: '审核状态',
|
||||
dataIndex: 'review_status',
|
||||
width: 120,
|
||||
render: value => <StatusTag meta={listingReviewStatusMeta(value)} />,
|
||||
},
|
||||
{
|
||||
title: '审核意见',
|
||||
dataIndex: 'review_reason',
|
||||
width: 200,
|
||||
ellipsis: true,
|
||||
render: value =>
|
||||
value ? (
|
||||
<Typography.Text type="secondary">{value}</Typography.Text>
|
||||
) : (
|
||||
<Typography.Text type="secondary">—</Typography.Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: mode === 'review' ? '提交时间' : '更新时间',
|
||||
dataIndex: mode === 'review' ? 'created_at' : 'updated_at',
|
||||
width: 170,
|
||||
render: value => formatDateTime(value),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
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={() => onOpen(row)}>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={event => {
|
||||
event.stopPropagation()
|
||||
onOpen(row)
|
||||
}}
|
||||
>
|
||||
详情
|
||||
</Button>
|
||||
) : (
|
||||
@@ -146,12 +352,32 @@ export function AdminListingsPage({ mode = 'all' }: { mode?: ListingMode }) {
|
||||
review_status: isReviewMode ? 'pending' : '',
|
||||
}))
|
||||
const [page, setPage] = useState(1)
|
||||
const [pageSize, setPageSize] = useState(20)
|
||||
const [pageSize, setPageSize] = useState(10)
|
||||
const [detailId, setDetailId] = useState<number | null>(null)
|
||||
const [detailOpen, setDetailOpen] = useState(false)
|
||||
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 }),
|
||||
@@ -171,12 +397,19 @@ export function AdminListingsPage({ mode = 'all' }: { mode?: ListingMode }) {
|
||||
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.status])
|
||||
}, [
|
||||
isReviewMode,
|
||||
rawItems,
|
||||
applied.keyword,
|
||||
applied.owner_id,
|
||||
applied.status,
|
||||
])
|
||||
|
||||
const total = isReviewMode ? reviewFiltered.length : listQuery.data?.total ?? 0
|
||||
const items = isReviewMode
|
||||
@@ -188,6 +421,10 @@ export function AdminListingsPage({ mode = 'all' }: { mode?: ListingMode }) {
|
||||
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
|
||||
setDetailId(id)
|
||||
@@ -195,7 +432,7 @@ export function AdminListingsPage({ mode = 'all' }: { mode?: ListingMode }) {
|
||||
}
|
||||
|
||||
const columns = useMemo(
|
||||
() => buildColumns(row => openDetail(row.id), canView, mode),
|
||||
() => buildColumns(row => openDetail(row.id), canView),
|
||||
[canView, mode]
|
||||
)
|
||||
|
||||
@@ -219,21 +456,43 @@ export function AdminListingsPage({ mode = 'all' }: { mode?: ListingMode }) {
|
||||
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">
|
||||
<div className="admin-page admin-listings-page">
|
||||
<AdminPageHeader
|
||||
eyebrow={isReviewMode ? 'JIEYOU OPS · 审核' : 'JIEYOU OPS · 商品'}
|
||||
title={isReviewMode ? '商品审核' : '商品管理'}
|
||||
description={
|
||||
isReviewMode
|
||||
? '按提交顺序处理待审商品,核对账号、价格与截图后再上架。'
|
||||
: '查看全量商品、号主、价格、状态与审核意见;点击标题查看详情。'
|
||||
: '查看商品编码、地区、上号方式、账号资产、号主备注、租金押金、比例和预计租期。'
|
||||
}
|
||||
meta={
|
||||
<Space size={8} wrap>
|
||||
@@ -249,20 +508,95 @@ export function AdminListingsPage({ mode = 'all' }: { mode?: ListingMode }) {
|
||||
}
|
||||
/>
|
||||
|
||||
<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={300}>
|
||||
<FilterField label="编号" width={240}>
|
||||
<Input
|
||||
allowClear
|
||||
value={draft.keyword}
|
||||
placeholder="商品编号 / 标题 / 号主手机号"
|
||||
placeholder="商品编号 / 订单编号"
|
||||
onChange={event => patchFilter({ keyword: event.target.value })}
|
||||
/>
|
||||
</FilterField>
|
||||
<FilterField label="商品状态" width={150}>
|
||||
<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}
|
||||
@@ -271,17 +605,60 @@ export function AdminListingsPage({ mode = 'all' }: { mode?: ListingMode }) {
|
||||
onChange={value => patchFilter({ status: value ?? '' })}
|
||||
/>
|
||||
</FilterField>
|
||||
{isReviewMode ? null : (
|
||||
<FilterField label="审核状态" width={150}>
|
||||
<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.review_status || undefined}
|
||||
placeholder="全部审核"
|
||||
options={listingReviewStatusOptions}
|
||||
onChange={value => patchFilter({ review_status: value ?? '' })}
|
||||
value={draft.insurance || undefined}
|
||||
placeholder="保险"
|
||||
options={insuranceOptions.map(value => ({
|
||||
value,
|
||||
label: insuranceLabel(value),
|
||||
}))}
|
||||
onChange={value => patchFilter({ insurance: value ?? '' })}
|
||||
/>
|
||||
</FilterField>
|
||||
)}
|
||||
<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 ? (
|
||||
@@ -306,6 +683,7 @@ export function AdminListingsPage({ mode = 'all' }: { mode?: ListingMode }) {
|
||||
{isPending ? '加载中' : `第 ${page} 页 · 当前 ${items.length} 条`}
|
||||
</Typography.Text>
|
||||
}
|
||||
className="admin-listings-table"
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
dataSource={items}
|
||||
|
||||
Reference in New Issue
Block a user