feat(listings): 统一人脸情况文案并优化详情页布局

This commit is contained in:
2026-11-12 18:28:37 +08:00
parent 766369075b
commit 0359fd62cb
10 changed files with 126 additions and 34 deletions
@@ -407,7 +407,7 @@ export function AdminListingDetailPage() {
<div className="admin-listing-grid">
<Card title="账号属性" size="small" className="admin-detail-card admin-detail-card--wide">
<Descriptions column={3} size="small">
<Descriptions column={{ xs: 1, sm: 2, md: 3 }} size="small">
{assetRows.map(row => (
<Descriptions.Item key={row.label} label={row.label}>
{row.value}
@@ -291,7 +291,6 @@ export function AdminListingReviewDetailPage() {
meta={
listing ? (
<Space size={6} wrap>
{listing.review_status === 'pending' ? <Tag color="gold">待审核</Tag> : null}
<StatusTag meta={listingStatusMeta(listing.status)} />
<StatusTag meta={listingReviewStatusMeta(listing.review_status)} />
</Space>
@@ -389,7 +388,7 @@ export function AdminListingReviewDetailPage() {
size="small"
className="admin-detail-card admin-detail-card--wide"
>
<Descriptions column={3} size="small">
<Descriptions column={{ xs: 1, sm: 2, md: 3 }} size="small">
{assetRows.map(row => (
<Descriptions.Item key={row.label} label={row.label}>
{row.value}
@@ -712,7 +712,7 @@ export function AdminOrderDetailPage() {
<div className="admin-order-grid">
<Card title="订单概览" size="small" className="admin-detail-card">
<Descriptions column={2} size="small">
<Descriptions column={{ xs: 1, sm: 2 }} size="small">
<Descriptions.Item label="订单编号">
<span className="admin-mono">{order.order_no}</span>
</Descriptions.Item>
@@ -734,7 +734,7 @@ export function AdminOrderDetailPage() {
</Card>
<Card title="用户与时间" size="small" className="admin-detail-card">
<Descriptions column={2} size="small">
<Descriptions column={{ xs: 1, sm: 2 }} size="small">
<Descriptions.Item label="租客">
{formatText(order.renter_phone)} · ID {order.renter_id}
</Descriptions.Item>
@@ -759,7 +759,7 @@ export function AdminOrderDetailPage() {
</Card>
<Card title="账号来源与处理方式" size="small" className="admin-detail-card">
<Descriptions column={2} size="small">
<Descriptions column={{ xs: 1, sm: 2 }} size="small">
<Descriptions.Item label="账号来源">
{accountSourceLabel(order.account_source)}
</Descriptions.Item>
@@ -809,7 +809,7 @@ export function AdminOrderDetailPage() {
/>
}
>
<Descriptions column={2} size="small">
<Descriptions column={{ xs: 1, sm: 2 }} size="small">
<Descriptions.Item label="退款金额">
{moneyOrDash(refundStatus?.refund_amount_cent)}
</Descriptions.Item>
@@ -869,7 +869,7 @@ export function AdminOrderDetailPage() {
/>
}
>
<Descriptions column={2} size="small">
<Descriptions column={{ xs: 1, sm: 2 }} size="small">
<Descriptions.Item label="待打款金额">
{formatMoney(order.offline_settlement_amount_cent)}
</Descriptions.Item>
@@ -895,7 +895,7 @@ export function AdminOrderDetailPage() {
<StatusTag meta={readStatusMeta(checkoutStatusMap, order.checkout.status)} />
}
>
<Descriptions column={3} size="small">
<Descriptions column={{ xs: 1, sm: 2, md: 3 }} size="small">
<Descriptions.Item label="结算租金">
{formatMoney(order.checkout.display_amount_cent ?? 0)}
</Descriptions.Item>
@@ -950,7 +950,7 @@ export function AdminOrderDetailPage() {
{order.active_dispute ? (
<Card title="进行中的申诉" size="small" className="admin-detail-card">
<Descriptions column={2} size="small">
<Descriptions column={{ xs: 1, sm: 2 }} size="small">
<Descriptions.Item label="申诉单号">#{order.active_dispute.id}</Descriptions.Item>
<Descriptions.Item label="类型">
{formatText(order.active_dispute.type)}
@@ -1024,7 +1024,7 @@ export function AdminOrderDetailPage() {
</Space>
}
>
<Descriptions column={2} size="small">
<Descriptions column={{ xs: 1, sm: 2 }} size="small">
<Descriptions.Item label="账号 ID">
{snapshotText(snapshot.account_id)}
</Descriptions.Item>
@@ -1,5 +1,10 @@
import type { AdminListing } from '@/features/admin/types'
import { listingResources, type ListingResourceRow } from '@/features/listings/labels'
import {
faceOwnerText,
listingResources,
listingStatusMeta,
type ListingResourceRow,
} from '@/features/listings/labels'
import { formatCoinM, formatMoney } from '@/shared/utils/format'
import {
@@ -67,7 +72,7 @@ export function adminListingAssetRows(listing: AdminListing): ListingRow[] {
{ label: '解锁德穆兰', value: listingAssetText(listing, 'unlock_demulan') },
{ label: '凌晨可响应', value: listingAssetText(listing, 'early_morning_response') },
{ label: '封禁记录', value: listingAssetText(listing, 'ban_record') },
{ label: '人脸情况', value: listingAssetText(listing, 'face_owner') },
{ label: '人脸情况', value: faceOwnerText(listing.asset_summary) || '—' },
{ label: '预计可租', value: duration.days },
]
}
@@ -96,15 +101,7 @@ export function adminListingOwnerRows(listing: AdminListing): ListingRow[] {
{ label: '哈夫币', value: formatCoinM(listing.haf_coin_amount) },
{ label: '每日损耗', value: duration.dailyLoss || '—' },
{ label: '预计可租', value: duration.days },
{
label: '商品状态',
value:
listing.status === 'published'
? '已上架'
: listing.status === 'rented'
? '租用中'
: listing.status,
},
{ label: '商品状态', value: listingStatusMeta(listing.status).label },
]
}
@@ -0,0 +1,37 @@
import { describe, expect, it } from 'vitest'
import { orderFundRows } from '@/features/admin/orderDetailModel'
import type { AdminOrderDetail } from '@/features/admin/types'
function order(patch: Partial<AdminOrderDetail> = {}): AdminOrderDetail {
return {
status: 'completed',
pure_coin_original_amount_cent: 0,
extra_item_original_amount_cent: 0,
rent_discount_amount_cent: 0,
actual_pure_coin_amount_cent: 0,
actual_pure_coin_discount_cent: 0,
growth_points_basis_cent: 0,
rent_amount_cent: 0,
owner_rent_amount_cent: 0,
platform_fee_cent: 0,
deposit_amount_cent: 0,
deposit_original_amount_cent: 0,
deposit_waived_amount_cent: 0,
...patch,
} as AdminOrderDetail
}
describe('orderFundRows', () => {
it('优惠为 0 时不产生负零金额', () => {
const rows = orderFundRows(order())
const discount = rows.find(row => row.label === '最终实际纯币优惠')
expect(discount?.amountCent).toBe(0)
expect(Object.is(discount?.amountCent, -0)).toBe(false)
})
it('有优惠时展示为负数,便于区分扣减', () => {
const rows = orderFundRows(order({ actual_pure_coin_discount_cent: 940 }))
expect(rows.find(row => row.label === '最终实际纯币优惠')?.amountCent).toBe(-940)
})
})
@@ -90,7 +90,10 @@ export function orderFundRows(order: AdminOrderDetail): OrderFundRow[] {
{ label: '原始租金', amountCent: numberOf(order.rent_original_amount_cent) },
{
label: `${order.renter_growth_level_name || '成长等级'}优惠`,
amountCent: -numberOf(order.rent_discount_amount_cent),
amountCent:
numberOf(order.rent_discount_amount_cent) > 0
? -numberOf(order.rent_discount_amount_cent)
: 0,
}
)
}
@@ -102,7 +105,10 @@ export function orderFundRows(order: AdminOrderDetail): OrderFundRow[] {
},
{
label: '最终实际纯币优惠',
amountCent: -numberOf(order.actual_pure_coin_discount_cent),
amountCent:
numberOf(order.actual_pure_coin_discount_cent) > 0
? -numberOf(order.actual_pure_coin_discount_cent)
: 0,
},
{
label: '积分计算纯币基数',
@@ -1,6 +1,7 @@
import { describe, expect, it } from 'vitest'
import {
faceOwnerText,
listingPriceDetail,
listingRatioText,
listingResources,
@@ -149,3 +150,21 @@ describe('resourceBadgeText', () => {
)
})
})
describe('faceOwnerText', () => {
it('把是/否映射为本人/非本人', () => {
expect(faceOwnerText({ face_owner: '是' })).toBe('本人')
expect(faceOwnerText({ face_owner: '否' })).toBe('非本人')
})
it('兼容已映射文案与空值', () => {
expect(faceOwnerText({ face_owner: '本人' })).toBe('本人')
expect(faceOwnerText({ face_owner: '非本人' })).toBe('非本人')
expect(faceOwnerText({ face_owner: ' ' })).toBe('')
expect(faceOwnerText(null)).toBe('')
})
it('未知文案原样保留', () => {
expect(faceOwnerText({ face_owner: '需配合人脸' })).toBe('需配合人脸')
})
})
+5 -1
View File
@@ -91,7 +91,11 @@ export function onlineTimeText(summary?: Record<string, unknown> | null): string
export function faceOwnerText(summary?: Record<string, unknown> | null): string {
const value = summary?.face_owner
if (typeof value !== 'string') return ''
return value.trim()
const text = value.trim()
if (!text) return ''
if (text === '是' || text === '本人' || text === '有') return '本人'
if (text === '否' || text === '非本人' || text === '无') return '非本人'
return text
}
export function banRecordText(summary?: Record<string, unknown> | null): string {
@@ -153,10 +153,11 @@ function ChoiceField(props: {
required?: boolean
value: string
options: string[]
optionLabels?: Record<string, string>
placeholder?: string
onChange: (value: string) => void
}) {
const { label, required, value, options, placeholder, onChange } = props
const { label, required, value, options, optionLabels, placeholder, onChange } = props
return (
<div className="seller-field">
<span className="seller-field__label">
@@ -182,7 +183,7 @@ function ChoiceField(props: {
onChange(option)
}}
>
{option}
{optionLabels?.[option] ?? option}
</button>
)
})}
@@ -1183,6 +1184,7 @@ export function SellerListingFormPage({ mobile = false }: { mobile?: boolean } =
label="人脸情况"
value={form.face_owner}
options={options.face_options}
optionLabels={{ 是: '本人', 否: '非本人' }}
onChange={value => updateForm('face_owner', value)}
/>
<ChoiceField
+35 -7
View File
@@ -92,16 +92,24 @@
}
.admin-screenshot-groups {
display: flex;
flex-direction: column;
gap: 16px;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
gap: 14px;
}
.admin-screenshot-group {
min-width: 0;
padding: 12px;
border: 1px solid var(--line);
border-radius: 10px;
background: var(--paper-warm);
}
.admin-screenshot-group__title {
display: flex;
align-items: baseline;
gap: 8px;
margin-bottom: 8px;
margin-bottom: 10px;
color: var(--ink);
font-size: 13px;
font-weight: 600;
@@ -114,9 +122,14 @@
}
.admin-screenshot-grid {
display: flex;
flex-wrap: wrap;
gap: 10px;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
gap: 8px;
}
.admin-screenshot-grid .ant-image {
width: 100% !important;
height: auto !important;
}
.admin-screenshot-grid .ant-image,
@@ -125,6 +138,12 @@
overflow: hidden;
}
.admin-screenshot-grid img {
width: 100% !important;
height: 96px !important;
object-fit: cover;
}
.admin-price-pipeline {
display: grid;
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr) auto minmax(0, 1fr);
@@ -288,4 +307,13 @@
.admin-price-preview {
grid-template-columns: minmax(0, 1fr);
}
.admin-screenshot-groups {
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
.admin-screenshot-grid img {
height: 78px !important;
}
}