+
{canView ? (
) : (
- {row.title || '未命名商品'}
+ {listingCodeText(row)}
)}
- {row.listing_no}
-
- {formatText(row.server_region)} · {formatText(row.login_platform)}
+
+
+ {row.review_status !== 'approved' ? (
+
+ ) : null}
),
},
{
- title: '号主',
- key: 'owner',
- width: 170,
- render: (_, row) => (
-
- {formatText(row.owner_phone)}
-
- {row.owner_nickname || '未设置昵称'} · ID {row.owner_id}
-
-
- ),
+ title: '地区',
+ key: 'region',
+ width: 92,
+ render: (_, row) =>
,
},
{
- title: '价格',
- key: 'price',
- width: 140,
+ title: '上号方式',
+ dataIndex: 'login_platform',
+ width: 82,
+ render: value =>
,
+ },
+ {
+ title: '哈夫币(M)',
+ key: 'coin',
+ width: 88,
+ align: 'right',
+ render: (_, row) =>
{listingCoinText(row)},
+ },
+ {
+ 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) =>
,
+ },
+ {
+ title: '人物红皮/人物金皮/武器皮肤',
+ key: 'skins',
+ width: 300,
+ render: (_, row) =>
,
+ },
+ {
+ title: '全英雄',
+ key: 'all_hero',
+ width: 72,
+ align: 'center',
+ render: (_, row) => listingAllHeroText(row),
+ },
+ {
+ title: '号主备注',
+ key: 'description',
+ width: 190,
+ render: (_, row) =>
,
+ },
+ {
+ title: '租客纯币价格',
+ key: 'pure_coin_price',
+ width: 118,
align: 'right',
render: (_, row) => (
-
- {formatMoney(row.price_cent)}
- 押金 {formatMoney(row.deposit_amount_cent)}
- 哈夫币 {row.haf_coin_amount.toLocaleString('zh-CN')}
-
+
{listingPureCoinPriceText(row)}
),
},
{
- title: '商品状态',
- dataIndex: 'status',
- width: 120,
- render: value =>
,
- },
- {
- title: '审核状态',
- dataIndex: 'review_status',
- width: 120,
- render: value =>
,
- },
- {
- title: '审核意见',
- dataIndex: 'review_reason',
- width: 200,
- ellipsis: true,
- render: value =>
- value ? (
-
{value}
- ) : (
-
—
- ),
- },
- {
- 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 (
+
+ {duration.days}
+ {duration.dailyLoss ? {duration.dailyLoss} : null}
+
+ )
+ },
+ },
+ {
+ title: '租金/押金',
+ key: 'rent_deposit',
+ width: 142,
+ align: 'right',
+ render: (_, row) => (
+
{listingRentDepositText(row)}
+ ),
+ },
+ {
+ title: '比例',
+ key: 'ratio',
+ width: 70,
+ align: 'center',
+ render: (_, row) => (
+
+ {listingRatioText(row)}
+
+ ),
+ },
+ {
+ title: '详情',
+ key: 'actions',
+ width: 74,
fixed: 'right',
+ align: 'center',
render: (_, row) =>
canView ? (
-
} onClick={() => onOpen(row)}>
+
}
+ onClick={event => {
+ event.stopPropagation()
+ onOpen(row)
+ }}
+ >
详情
) : (
@@ -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
(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 (
-
+
@@ -249,20 +508,95 @@ export function AdminListingsPage({ mode = 'all' }: { mode?: ListingMode }) {
}
/>
+
+
+
+
+
+
+
-
+
patchFilter({ keyword: event.target.value })}
/>
-
+
+ {
+ const value = event.target.value.replace(/\D/g, '')
+ patchFilter({ owner_id: value ? Number(value) : undefined })
+ }}
+ />
+
+
+ patchFilter({ min_coin: value })}
+ onMaxChange={value => patchFilter({ max_coin: value })}
+ />
+
+
+ patchFilter({ min_deposit: value })}
+ onMaxChange={value => patchFilter({ max_deposit: value })}
+ />
+
+
- {isReviewMode ? null : (
-
+
+
+
+
+
+
+ patchFilter({ skin_name: values.join(','), skin_group: '' })}
+ />
+
{isError ? (
@@ -306,6 +683,7 @@ export function AdminListingsPage({ mode = 'all' }: { mode?: ListingMode }) {
{isPending ? '加载中' : `第 ${page} 页 · 当前 ${items.length} 条`}
}
+ className="admin-listings-table"
rowKey="id"
columns={columns}
dataSource={items}
diff --git a/frontend/src/features/admin/listingDisplay.ts b/frontend/src/features/admin/listingDisplay.ts
new file mode 100644
index 0000000..46397ad
--- /dev/null
+++ b/frontend/src/features/admin/listingDisplay.ts
@@ -0,0 +1,158 @@
+import type { AdminListing } from '@/features/admin/types'
+import { formatCoinM, formatMoney } from '@/shared/utils/format'
+
+type JsonRecord = Record
+
+function recordOf(value: unknown): JsonRecord {
+ return typeof value === 'object' && value !== null ? (value as JsonRecord) : {}
+}
+
+function textOf(value: unknown) {
+ return value == null || value === '' ? '' : String(value)
+}
+
+function numberOf(value: unknown) {
+ const parsed = Number(value)
+ return Number.isFinite(parsed) ? parsed : 0
+}
+
+function compactNumber(value: number) {
+ const rounded = Math.round(value * 10) / 10
+ return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1)
+}
+
+export function listingCodeText(row: AdminListing) {
+ return row.listing_no || `SP${String(row.id).padStart(6, '0')}`
+}
+
+export function listingCoinText(row: AdminListing) {
+ return formatCoinM(row.haf_coin_amount)
+}
+
+export function listingRegionText(row: AdminListing) {
+ const regions = row.asset_summary?.common_regions
+ if (Array.isArray(regions)) {
+ const names = regions.filter(
+ (region): region is string => typeof region === 'string' && Boolean(region.trim())
+ )
+ if (names.length > 0) return names.join('、')
+ }
+ return row.server_region || '—'
+}
+
+export function listingAssetText(row: AdminListing, key: string) {
+ const value = row.asset_summary?.[key]
+ if (value == null || value === '') return '—'
+ if (Array.isArray(value)) {
+ const names = value.filter(
+ (item): item is string => typeof item === 'string' && Boolean(item.trim())
+ )
+ return names.length > 0 ? names.join('、') : '—'
+ }
+ if (typeof value === 'object') return '—'
+ return String(value)
+}
+
+export function listingLevelText(row: AdminListing, key: string) {
+ const value = listingAssetText(row, key)
+ return value === '—' ? value : value.replace(/级$/, '')
+}
+
+export function listingQuantity(row: AdminListing, key: string) {
+ const resources = row.asset_summary?.resources
+ if (!Array.isArray(resources)) return 0
+ const match = resources.find(item => {
+ const resource = recordOf(item)
+ return textOf(resource.key) === key
+ })
+ return numberOf(recordOf(match).quantity)
+}
+
+export function listingResourceText(row: AdminListing, key: string) {
+ const quantity = listingQuantity(row, key)
+ return quantity > 0 ? compactNumber(quantity) : '—'
+}
+
+export function listingSkinGroupText(row: AdminListing, groupKey: string) {
+ const groups = recordOf(row.asset_summary?.skin_groups)
+ const names = groups[groupKey]
+ if (!Array.isArray(names)) return '—'
+ const values = names.filter(
+ (name): name is string => typeof name === 'string' && Boolean(name.trim())
+ )
+ return values.length > 0 ? values.join('、') : '—'
+}
+
+export function listingCharacterSkinText(row: AdminListing) {
+ const groups = [
+ { label: '红皮', key: 'operatorRed' },
+ { label: '金皮', key: 'operatorGold' },
+ { label: '武器', key: 'weapon' },
+ ]
+ const parts = groups
+ .map(group => {
+ const names = listingSkinGroupText(row, group.key)
+ return names === '—' ? '' : `${group.label}:${names}`
+ })
+ .filter(Boolean)
+ return parts.length > 0 ? parts.join(' / ') : '—'
+}
+
+export function listingAllHeroText(row: AdminListing) {
+ const value =
+ listingAssetText(row, 'all_hero') !== '—'
+ ? listingAssetText(row, 'all_hero')
+ : listingAssetText(row, 'all_heroes')
+ return value === '是' || value === '否' ? value : '—'
+}
+
+export function listingOwnerRemark(row: AdminListing) {
+ return row.description?.trim() || '—'
+}
+
+export function listingPureCoinPrice(row: AdminListing) {
+ const summary = row.asset_summary ?? {}
+ const breakdown = recordOf(summary.price_breakdown)
+ const buyerCent = numberOf(breakdown.buyer_coin_base_price_cent)
+ if (buyerCent > 0) return Math.round(buyerCent)
+
+ const buyerYuan = numberOf(breakdown.buyer_coin_base_price)
+ if (buyerYuan > 0) return Math.round(buyerYuan * 100)
+
+ const consumableCent =
+ numberOf(breakdown.consumable_price_cent) || numberOf(breakdown.consumable_price) * 100
+ return Math.max(0, Math.round(row.price_cent - consumableCent))
+}
+
+export function listingPureCoinPriceText(row: AdminListing) {
+ const cents = listingPureCoinPrice(row)
+ return cents > 0 ? formatMoney(cents) : '—'
+}
+
+export function listingRentalDuration(row: AdminListing) {
+ const coinM = numberOf(row.haf_coin_amount) / 1_000_000
+ const dailyLoss = numberOf(row.asset_summary?.daily_loss_m)
+ if (coinM <= 0 || dailyLoss <= 0) {
+ return { days: '—', dailyLoss: '' }
+ }
+ return {
+ days: `${Math.max(1, Math.ceil(coinM / dailyLoss))}天`,
+ dailyLoss: `日耗 ${compactNumber(dailyLoss)}M`,
+ }
+}
+
+export function listingRentDepositText(row: AdminListing) {
+ return `${formatMoney(row.price_cent)} / ${formatMoney(row.deposit_amount_cent)}`
+}
+
+export function listingRatioText(row: AdminListing) {
+ const ratio = numberOf(row.asset_summary?.publish_ratio)
+ if (ratio <= 0) return '—'
+ return `1:${compactNumber(ratio)}`
+}
+
+export function listingAccountLevel(row: AdminListing) {
+ const fireLevel = listingAssetText(row, 'fire_level')
+ if (fireLevel !== '—') return fireLevel
+ return row.rank_level || '—'
+}
diff --git a/frontend/src/features/admin/types.ts b/frontend/src/features/admin/types.ts
index ac9b885..a77d686 100644
--- a/frontend/src/features/admin/types.ts
+++ b/frontend/src/features/admin/types.ts
@@ -265,6 +265,15 @@ export interface AdminListingQuery {
status?: string
review_status?: string
owner_id?: number
+ insurance?: string
+ stamina?: string
+ load?: string
+ skin_group?: string
+ skin_name?: string
+ min_coin?: number
+ max_coin?: number
+ min_deposit?: number
+ max_deposit?: number
}
/** 后台商品列表与详情共用 ListingDTO,字段完整但截图等资源按商品状态脱敏。 */
diff --git a/frontend/src/styles/admin-shell.css b/frontend/src/styles/admin-shell.css
index a70d4d5..6ff9dd7 100644
--- a/frontend/src/styles/admin-shell.css
+++ b/frontend/src/styles/admin-shell.css
@@ -221,12 +221,14 @@
}
.admin-content {
- padding: 24px;
+ width: 100%;
+ padding: 20px 22px 32px;
}
.admin-page {
- max-width: 1600px;
- margin: 0 auto;
+ width: 100%;
+ max-width: none;
+ margin: 0;
}
.admin-page__head {
@@ -234,7 +236,7 @@
align-items: flex-end;
justify-content: space-between;
gap: 24px;
- margin-bottom: 24px;
+ margin-bottom: 18px;
}
.admin-page__head-main {
@@ -1177,7 +1179,7 @@
/* 工作台 */
.dash-page {
- max-width: 1320px;
+ max-width: none;
}
.dash-head {
@@ -1879,3 +1881,285 @@
}
}
+/* 商品管理 */
+.admin-listings-page {
+ max-width: none;
+}
+
+.admin-listing-overview {
+ display: grid;
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ gap: 12px;
+ margin-bottom: 18px;
+}
+
+.admin-listing-stat {
+ position: relative;
+ display: flex;
+ min-height: 104px;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 4px;
+ overflow: hidden;
+ padding: 15px 17px;
+ border: 1px solid var(--line);
+ border-radius: 10px;
+ background: var(--paper-warm);
+ text-align: left;
+ cursor: pointer;
+ transition:
+ transform 0.16s ease,
+ border-color 0.16s ease,
+ box-shadow 0.16s ease;
+}
+
+.admin-listing-stat::before {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ width: 3px;
+ background: var(--brand-ink);
+ content: '';
+ opacity: 0.35;
+}
+
+.admin-listing-stat:hover {
+ border-color: var(--brand-ink);
+ box-shadow: 0 8px 20px rgba(30, 77, 66, 0.07);
+ transform: translateY(-1px);
+}
+
+.admin-listing-stat > span {
+ color: var(--ink-muted);
+ font-size: 12px;
+ font-weight: 600;
+}
+
+.admin-listing-stat > strong {
+ color: var(--brand-ink);
+ font-family: 'Inter', 'DIN Alternate', sans-serif;
+ font-size: 30px;
+ line-height: 1.1;
+}
+
+.admin-listing-stat > small {
+ color: var(--ink-muted);
+ font-size: 10px;
+}
+
+.admin-listing-stat.is-published::before {
+ background: #0f7b6c;
+}
+
+.admin-listing-stat.is-rented::before {
+ background: #b45309;
+}
+
+.admin-listing-stat.is-review::before {
+ background: #b91c1c;
+}
+
+.admin-listing-stat.is-active {
+ border-color: var(--brand-ink);
+ background: var(--brand-ink);
+ box-shadow: 0 10px 24px rgba(30, 77, 66, 0.16);
+}
+
+.admin-listing-stat.is-active::before {
+ width: 0;
+}
+
+.admin-listing-stat.is-active > span,
+.admin-listing-stat.is-active > strong,
+.admin-listing-stat.is-active > small {
+ color: #fff;
+}
+
+.admin-listing-stat.is-active > span,
+.admin-listing-stat.is-active > small {
+ opacity: 0.78;
+}
+
+.admin-listings-page .admin-filter-bar__fields {
+ align-items: flex-end;
+}
+
+.admin-range-filter {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
+ align-items: center;
+ gap: 6px;
+ width: 100%;
+}
+
+.admin-range-filter .ant-input-number {
+ width: 100%;
+}
+
+.admin-range-filter__sep {
+ color: var(--ink-muted);
+ font-size: 11px;
+}
+
+.admin-triple-filter {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 6px;
+ width: 100%;
+}
+
+.admin-triple-filter .ant-select {
+ min-width: 0;
+ width: 100%;
+}
+
+.admin-listings-table .ant-card-body {
+ padding-top: 10px;
+}
+
+.admin-listings-table .ant-table {
+ font-size: 11px;
+}
+
+.admin-listings-table .ant-table-thead > tr > th {
+ padding: 8px 6px !important;
+ background: #f7f3ea !important;
+ color: var(--ink-secondary);
+ font-size: 10px;
+ font-weight: 600;
+ letter-spacing: 0;
+ white-space: nowrap;
+}
+
+.admin-listings-table .ant-table-tbody > tr > td {
+ padding: 8px 6px !important;
+ vertical-align: top;
+}
+
+.admin-listings-table .ant-table-tbody > tr:nth-child(even) > td {
+ background: #fcfbf7;
+}
+
+.admin-listings-table .ant-table-tbody > tr:hover > td,
+.admin-listings-table .ant-table-tbody > tr.ant-table-row.admin-table__row--clickable:hover > td {
+ background: #f1f6f2 !important;
+}
+
+.admin-listings-table .ant-table-cell-fix-left,
+.admin-listings-table .ant-table-cell-fix-right {
+ background: var(--paper-warm);
+}
+
+.admin-listings-table .ant-table-thead .ant-table-cell-fix-left,
+.admin-listings-table .ant-table-thead .ant-table-cell-fix-right {
+ background: #f7f3ea !important;
+}
+
+.admin-listing-code {
+ display: flex;
+ min-width: 96px;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 5px;
+}
+
+.admin-listing-code .admin-order-link {
+ max-width: 108px;
+ overflow: hidden;
+ font-size: 11px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.admin-listing-code__tags {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 2px;
+}
+
+.admin-listing-code__tags .admin-status-tag {
+ min-height: 17px;
+ margin: 0;
+ padding-inline: 5px;
+ font-size: 9px;
+ line-height: 15px;
+}
+
+.admin-listings-ellipsis {
+ display: block;
+ max-width: 100%;
+ overflow: hidden;
+ color: var(--ink-secondary);
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.admin-listings-number {
+ color: var(--brand-ink);
+ font-family: 'Inter', 'DIN Alternate', sans-serif;
+ font-size: 11px;
+ font-weight: 600;
+}
+
+.admin-listing-duration {
+ display: flex;
+ flex-direction: column;
+ gap: 1px;
+ align-items: center;
+}
+
+.admin-listing-duration strong {
+ color: var(--brand-ink);
+ font-size: 11px;
+}
+
+.admin-listing-duration small {
+ color: var(--ink-muted);
+ font-size: 9px;
+ white-space: nowrap;
+}
+
+.admin-listing-money-pair {
+ color: var(--brand-ink);
+ font-family: 'Inter', monospace;
+ font-size: 10px;
+ white-space: nowrap;
+}
+
+.admin-listings-ratio {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 1px 5px;
+ border-radius: 999px;
+ background: #eef2ec;
+ color: var(--brand-ink);
+ font-family: 'Inter', monospace;
+ font-size: 10px;
+ font-weight: 600;
+ white-space: nowrap;
+}
+
+.admin-listings-time {
+ color: var(--ink-muted);
+ font-size: 9px;
+ white-space: nowrap;
+}
+
+@media (max-width: 1100px) {
+ .admin-listing-overview {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+}
+
+@media (max-width: 640px) {
+ .admin-listing-overview {
+ grid-template-columns: 1fr;
+ }
+
+ .admin-triple-filter {
+ grid-template-columns: 1fr;
+ }
+}
+