feat(listings): 统一人脸情况文案并优化详情页布局
This commit is contained in:
@@ -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('需配合人脸')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user