@@ -0,0 +1,291 @@
import { useState } from 'react'
import { useMutation , useQuery , useQueryClient } from '@tanstack/react-query'
import { Button , Dialog , Empty , ErrorBlock , Skeleton , Tag , Toast } from 'antd-mobile'
import { AddOutline , RightOutline } from 'antd-mobile-icons'
import { useNavigate } from 'react-router-dom'
import { BrandLogo } from '@/components/BrandLogo'
import { listingReviewStatusMeta , listingStatusMeta } from '@/features/listings/labels'
import {
fetchSellerListings ,
offlineListing ,
submitListingReview ,
} from '@/features/seller/api'
import type { SellerListing } from '@/features/seller/types'
import { useSessionStore } from '@/stores/session'
import { formatMoney } from '@/shared/utils/format'
type Filter = 'all' | 'published' | 'pending' | 'offline' | 'completed'
const filters : Array < { key : Filter ; label : string } > = [
{ key : 'all' , label : '全部' } ,
{ key : 'published' , label : '已上架' } ,
{ key : 'pending' , label : '待审核' } ,
{ key : 'offline' , label : '已下架' } ,
{ key : 'completed' , label : '已完成' } ,
]
function isTerminal ( item : SellerListing ) {
return item . status === 'rented' || item . status === 'completed' || item . status === 'sealed'
}
function isPending ( item : SellerListing ) {
return ! isTerminal ( item ) && item . status !== 'offline' && item . review_status === 'pending'
}
function canEdit ( item : SellerListing ) {
return ! isTerminal ( item ) && ! isPending ( item ) && item . status !== 'published'
}
function canSubmit ( item : SellerListing ) {
return canEdit ( item )
}
function canOffline ( item : SellerListing ) {
return ! isTerminal ( item ) && item . status !== 'offline'
}
function sellerPriceCent ( item : SellerListing ) {
const breakdown = item . asset_summary ? . price_breakdown
if ( breakdown && typeof breakdown === 'object' ) {
const record = breakdown as Record < string , unknown >
const cent = Number ( record . seller_total_price_cent )
if ( Number . isFinite ( cent ) && cent > 0 ) return cent
const yuan = Number ( record . seller_total_price )
if ( Number . isFinite ( yuan ) && yuan > 0 ) return Math . round ( yuan * 100 )
}
return item . price_cent
}
function coinText ( item : SellerListing ) {
const coinM = Number ( item . haf_coin_amount || 0 ) / 1 _000_000
if ( ! coinM ) return '—'
return Number . isInteger ( coinM ) ? ` ${ coinM } M ` : ` ${ coinM . toFixed ( 2 ) } M `
}
function statusColor ( tone : string ) {
if ( tone === 'success' ) return 'success'
if ( tone === 'gold' || tone === 'warning' ) return 'warning'
if ( tone === 'red' || tone === 'danger' ) return 'danger'
return 'default'
}
export function MobileSellerListingsPage() {
const navigate = useNavigate ( )
const queryClient = useQueryClient ( )
const token = useSessionStore ( state = > state . token )
const hydrated = useSessionStore ( state = > state . hydrated )
const [ filter , setFilter ] = useState < Filter > ( 'all' )
const [ actingId , setActingId ] = useState < number | null > ( null )
const listingsQuery = useQuery ( {
queryKey : [ 'seller-listings' ] ,
queryFn : fetchSellerListings ,
enabled : Boolean ( token ) ,
} )
const submitMutation = useMutation ( {
mutationFn : submitListingReview ,
onSuccess : async ( ) = > {
await queryClient . invalidateQueries ( { queryKey : [ 'seller-listings' ] } )
Toast . show ( { icon : 'success' , content : '已提交审核' } )
} ,
} )
const offlineMutation = useMutation ( {
mutationFn : offlineListing ,
onSuccess : async ( ) = > {
await queryClient . invalidateQueries ( { queryKey : [ 'seller-listings' ] } )
Toast . show ( { icon : 'success' , content : '已下架' } )
} ,
} )
const listings = listingsQuery . data ? ? [ ]
const display = listings . filter ( item = > {
if ( filter === 'all' ) return true
if ( filter === 'pending' ) return isPending ( item )
return item . status === filter
} )
const submitReview = async ( item : SellerListing ) = > {
const confirmed = await Dialog . confirm ( {
title : '提交审核' ,
content : ` 确认提交「 ${ item . title } 」进入后台审核吗? ` ,
confirmText : '提交审核' ,
} )
if ( ! confirmed ) return
setActingId ( item . id )
try {
await submitMutation . mutateAsync ( item . id )
} catch ( error ) {
Toast . show ( {
icon : 'fail' ,
content : error instanceof Error ? error . message : '提审失败' ,
} )
} finally {
setActingId ( null )
}
}
const offline = async ( item : SellerListing ) = > {
const confirmed = await Dialog . confirm ( {
title : '下架确认' ,
content : ` 确认下架「 ${ item . title } 」吗? ` ,
confirmText : '确认下架' ,
} )
if ( ! confirmed ) return
setActingId ( item . id )
try {
await offlineMutation . mutateAsync ( item . id )
} catch ( error ) {
Toast . show ( {
icon : 'fail' ,
content : error instanceof Error ? error . message : '下架失败' ,
} )
} finally {
setActingId ( null )
}
}
if ( ! hydrated ) {
return (
< div className = "mobile-seller" >
< Skeleton.Title animated / >
< Skeleton.Paragraph lineCount = { 6 } animated / >
< / div >
)
}
if ( ! token ) {
return (
< div className = "mobile-seller" >
< div className = "mobile-orders__login" >
< BrandLogo height = { 40 } variant = "seal" / >
< h2 > 登 录 后 管 理 商 品 < / h2 >
< p > 发 布 、 提 审 与 下 架 只 对 当 前 账 号 开 放 。 < / p >
< Button block color = "primary" onClick = { ( ) = > navigate ( '/m/login?redirect=/m/seller/listings' ) } >
去 登 录
< / Button >
< / div >
< / div >
)
}
return (
< div className = "mobile-seller" >
< section className = "mobile-seller__hero" >
< div >
< span className = "mobile-seller__eyebrow" > SELLER · 号 主 柜 台 < / span >
< h1 > 我 的 商 品 < / h1 >
< p > 查 看 审 核 进 度 , 管 理 上 架 与 下 架 。 < / p >
< / div >
< Button color = "primary" onClick = { ( ) = > navigate ( '/m/seller/listings/new' ) } >
< AddOutline / > 发 布
< / Button >
< / section >
< div className = "mobile-seller__filters" >
{ filters . map ( item = > (
< button
type = "button"
key = { item . key }
className = { filter === item . key ? 'is-active' : '' }
onClick = { ( ) = > setFilter ( item . key ) }
>
{ item . label }
< em >
{ item . key === 'all'
? listings.length
: item.key === 'pending'
? listings . filter ( isPending ) . length
: listings.filter ( row = > row . status === item . key ) . length }
< / em >
< / button >
) ) }
< / div >
{ listingsQuery . isPending ? (
< Skeleton.Paragraph lineCount = { 8 } animated / >
) : listingsQuery . isError ? (
< ErrorBlock status = "disconnected" title = "商品没有加载出来" >
< Button size = "small" onClick = { ( ) = > void listingsQuery . refetch ( ) } >
重 新 加 载
< / Button >
< / ErrorBlock >
) : display . length === 0 ? (
< div className = "mobile-seller__empty" >
< Empty description = "暂无符合条件的商品" / >
< Button color = "primary" onClick = { ( ) = > navigate ( '/m/seller/listings/new' ) } >
去 发 布
< / Button >
< / div >
) : (
< div className = "mobile-seller__list" >
{ display . map ( item = > {
const status = listingStatusMeta ( item . status )
const review = listingReviewStatusMeta ( item . review_status )
return (
< article
className = "mobile-seller-card"
key = { item . id }
onClick = { ( ) = > navigate ( ` /m/seller/listings/ ${ item . id } ` ) }
>
< div className = "mobile-seller-card__top" >
< span className = "mobile-seller-card__no" > { item . listing_no || ` # ${ item . id } ` } < / span >
< div className = "mobile-seller-card__tags" >
< Tag color = { statusColor ( status . tone ) } > { status . label } < / Tag >
{ item . status !== 'completed' ? (
< Tag color = { statusColor ( review . tone ) } > { review . label } < / Tag >
) : null }
< / div >
< / div >
< h2 > { item . title || '未命名账号' } < / h2 >
< div className = "mobile-seller-card__meta" >
< span > { item . server_region || '—' } < / span >
< span > { coinText ( item ) } < / span >
< span > 押 金 { formatMoney ( item . deposit_amount_cent ) } < / span >
< / div >
< div className = "mobile-seller-card__bottom" >
< strong > { formatMoney ( sellerPriceCent ( item ) ) } < / strong >
< RightOutline / >
< / div >
{ item . review_status === 'rejected' && item . review_reason ? (
< p className = "mobile-seller-card__reason" > 驳 回 : { item . review_reason } < / p >
) : null }
< div className = "mobile-seller-card__actions" onClick = { event = > event . stopPropagation ( ) } >
< Button
size = "mini"
fill = "outline"
disabled = { ! canEdit ( item ) }
onClick = { ( ) = > navigate ( ` /m/seller/listings/ ${ item . id } /edit ` ) }
>
编 辑
< / Button >
< Button
size = "mini"
fill = "outline"
disabled = { ! canSubmit ( item ) || actingId === item . id }
loading = { actingId === item . id && submitMutation . isPending }
onClick = { ( ) = > void submitReview ( item ) }
>
提 审
< / Button >
< Button
size = "mini"
color = "danger"
fill = "outline"
disabled = { ! canOffline ( item ) || actingId === item . id }
loading = { actingId === item . id && offlineMutation . isPending }
onClick = { ( ) = > void offline ( item ) }
>
下 架
< / Button >
< / div >
< / article >
)
} ) }
< / div >
) }
< / div >
)
}