feat(seller): 新增移动端卖家中心
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
# Phase 2 移动端卖家中心验收
|
||||
|
||||
> 日期:2026-10-30
|
||||
> 范围:移动端卖家商品列表、详情、发布/编辑入口。
|
||||
|
||||
## 1. 交付内容
|
||||
|
||||
- `/m/seller/listings`:移动端卖家商品列表。
|
||||
- 全部、已上架、待审核、已下架、已完成筛选与数量。
|
||||
- 商品卡片展示编号、状态、审核状态、区服、哈夫币、押金、卖家到手价。
|
||||
- 详情、编辑、提审、下架操作;提审/下架二次确认。
|
||||
- `/m/seller/listings/:id`:移动端商品详情。
|
||||
- `/m/seller/listings/new`、`/m/seller/listings/:id/edit`:复用 PC 发布/编辑表单及整数分/基点定价。
|
||||
- `SellerListingFormPage` / `SellerListingDetailPage` 增加 `mobile` 参数:
|
||||
- 回跳 `/m/seller/listings`。
|
||||
- 编辑/登录回跳使用移动端路径。
|
||||
- 发布成功后的群聊进入 `/m/messages/:id`。
|
||||
- 移动个人中心新增「卖家中心」入口,未登录先登录并回跳。
|
||||
- 新增 `frontend/src/styles/mobile-seller.css`,并加入 `global.css` 分块导入。
|
||||
|
||||
## 2. 验证
|
||||
|
||||
| 检查 | 结果 |
|
||||
|---|---|
|
||||
| `cd frontend && npx tsc --noEmit` | 通过 |
|
||||
| `cd frontend && npm run build` | 通过,主入口 520.27 kB / gzip 167.48 kB |
|
||||
| `GET /m/seller/listings` | 200 |
|
||||
| `GET /m/seller/listings/new` | 200 |
|
||||
| `GET /m/seller/listings/1` | 200 |
|
||||
| `GET /m/seller/listings/1/edit` | 200 |
|
||||
|
||||
## 3. 说明
|
||||
|
||||
- 移动端发布/编辑复用 PC 表单和整数定价工具,避免两套价格算法。
|
||||
- 表单在移动容器下会单列堆叠;复杂字段仍保持完整。
|
||||
- 未执行真实发布写操作,避免污染开发库。
|
||||
+1
-1
@@ -381,7 +381,7 @@ hfb_jysh/
|
||||
|
||||
### Phase 3:C 端 PC + 移动端(3 周)
|
||||
|
||||
> 状态:**进行中(2026-10-30)**。移动端已完成首页、商品列表/详情、下单确认、登录、个人中心、实名认证、订单列表/详情、消息中心与聊天,并接入支付、交接、确认收号、结账、还价、申诉、证据图片上传、订单群聊与客服会话;PC 商品详情、下单、订单中心、操作页、消息中心、聊天页与卖家发布/编辑/审核提交已接通,移动端卖家页面与分销长尾能力待迁移。
|
||||
> 状态:**进行中(2026-10-30)**。移动端已完成首页、商品列表/详情、下单确认、登录、个人中心、实名认证、订单列表/详情、消息中心、聊天与卖家中心,并接入支付、交接、确认收号、结账、还价、申诉、证据图片上传、订单群聊与客服会话;PC 商品详情、下单、订单中心、操作页、消息中心、聊天页与卖家发布/编辑/审核提交已接通,剩余分销长尾能力与真实订单端到端回归。
|
||||
|
||||
- 首页、商品列表、商品详情、撞车商品。
|
||||
- 登录、注册、找回密码、实名认证。
|
||||
|
||||
@@ -84,6 +84,21 @@ const MobileRealnamePage = lazy(() =>
|
||||
default: module.MobileRealnamePage,
|
||||
}))
|
||||
)
|
||||
const MobileSellerListingsPage = lazy(() =>
|
||||
import('@/features/mobile/MobileSellerListingsPage').then(module => ({
|
||||
default: module.MobileSellerListingsPage,
|
||||
}))
|
||||
)
|
||||
const MobileSellerListingDetailPage = lazy(() =>
|
||||
import('@/features/mobile/MobileSellerListingDetailPage').then(module => ({
|
||||
default: module.MobileSellerListingDetailPage,
|
||||
}))
|
||||
)
|
||||
const MobileSellerFormPage = lazy(() =>
|
||||
import('@/features/mobile/MobileSellerFormPage').then(module => ({
|
||||
default: module.MobileSellerFormPage,
|
||||
}))
|
||||
)
|
||||
const AdminLoginPage = lazy(() =>
|
||||
import('@/features/admin/AdminLoginPage').then(module => ({ default: module.AdminLoginPage }))
|
||||
)
|
||||
@@ -203,6 +218,10 @@ export function AppRouter() {
|
||||
<Route path="messages" element={<MobileMessagesPage />} />
|
||||
<Route path="messages/:id" element={<MobileChatPage />} />
|
||||
<Route path="realname" element={<MobileRealnamePage />} />
|
||||
<Route path="seller/listings" element={<MobileSellerListingsPage />} />
|
||||
<Route path="seller/listings/new" element={<MobileSellerFormPage />} />
|
||||
<Route path="seller/listings/:id" element={<MobileSellerListingDetailPage />} />
|
||||
<Route path="seller/listings/:id/edit" element={<MobileSellerFormPage />} />
|
||||
<Route path="profile" element={<MobileProfilePage />} />
|
||||
</Route>
|
||||
|
||||
|
||||
@@ -39,6 +39,13 @@ export function MobileProfilePage() {
|
||||
</List.Item>
|
||||
<List.Item onClick={() => navigate('/m/orders')}>我的订单</List.Item>
|
||||
<List.Item onClick={() => navigate('/m/messages')}>消息中心</List.Item>
|
||||
<List.Item
|
||||
onClick={() =>
|
||||
navigate(user ? '/m/seller/listings' : '/m/login?redirect=/m/seller/listings')
|
||||
}
|
||||
>
|
||||
卖家中心
|
||||
</List.Item>
|
||||
</List>
|
||||
|
||||
<div className="mobile-profile__actions">
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { SellerListingFormPage } from '@/features/seller/SellerListingFormPage'
|
||||
|
||||
export function MobileSellerFormPage() {
|
||||
return (
|
||||
<div className="mobile-seller-host">
|
||||
<SellerListingFormPage mobile />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { SellerListingDetailPage } from '@/features/seller/SellerListingDetailPage'
|
||||
|
||||
export function MobileSellerListingDetailPage() {
|
||||
return (
|
||||
<div className="mobile-seller-host">
|
||||
<SellerListingDetailPage mobile />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -165,13 +165,14 @@ function skinGroupsOf(row: SellerListing) {
|
||||
.filter(group => group.options.length > 0)
|
||||
}
|
||||
|
||||
export function SellerListingDetailPage() {
|
||||
export function SellerListingDetailPage({ mobile = false }: { mobile?: boolean } = {}) {
|
||||
const { id } = useParams()
|
||||
const navigate = useNavigate()
|
||||
const { modal, message } = App.useApp()
|
||||
const queryClient = useQueryClient()
|
||||
const token = useSessionStore(state => state.token)
|
||||
const hydrated = useSessionStore(state => state.hydrated)
|
||||
const basePath = mobile ? '/m/seller/listings' : '/seller/listings'
|
||||
|
||||
const listingQuery = useQuery({
|
||||
queryKey: ['seller-listing', id],
|
||||
@@ -262,7 +263,7 @@ export function SellerListingDetailPage() {
|
||||
<Empty description="登录后查看自己发布的商品">
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => navigate(`/login?redirect=/seller/listings/${id}`)}
|
||||
onClick={() => navigate(`/login?redirect=${basePath}/${id}`)}
|
||||
>
|
||||
去登录
|
||||
</Button>
|
||||
@@ -292,7 +293,7 @@ export function SellerListingDetailPage() {
|
||||
description="可能不是本人发布的商品,或商品已不存在。"
|
||||
action={
|
||||
<Space>
|
||||
<Button icon={<ArrowLeftOutlined />} onClick={() => navigate('/seller/listings')}>
|
||||
<Button icon={<ArrowLeftOutlined />} onClick={() => navigate(basePath)}>
|
||||
返回列表
|
||||
</Button>
|
||||
<Button icon={<ReloadOutlined />} onClick={() => void listingQuery.refetch()}>
|
||||
@@ -341,7 +342,7 @@ export function SellerListingDetailPage() {
|
||||
<main className="seller-page seller-detail-page">
|
||||
<header className="seller-detail-head">
|
||||
<div>
|
||||
<Button type="text" icon={<ArrowLeftOutlined />} onClick={() => navigate('/seller/listings')}>
|
||||
<Button type="text" icon={<ArrowLeftOutlined />} onClick={() => navigate(basePath)}>
|
||||
返回列表
|
||||
</Button>
|
||||
<Typography.Text className="seller-eyebrow">SELLER LISTING · 商品详情</Typography.Text>
|
||||
@@ -359,7 +360,7 @@ export function SellerListingDetailPage() {
|
||||
{canEdit(listing) ? (
|
||||
<Button
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => navigate(`/seller/listings/${listing.id}/edit`)}
|
||||
onClick={() => navigate(`${basePath}/${listing.id}/edit`)}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
|
||||
@@ -189,7 +189,7 @@ function ChoiceField(props: {
|
||||
)
|
||||
}
|
||||
|
||||
export function SellerListingFormPage() {
|
||||
export function SellerListingFormPage({ mobile = false }: { mobile?: boolean } = {}) {
|
||||
const { id } = useParams()
|
||||
const isEdit = Boolean(id)
|
||||
const navigate = useNavigate()
|
||||
@@ -197,6 +197,7 @@ export function SellerListingFormPage() {
|
||||
const queryClient = useQueryClient()
|
||||
const token = useSessionStore(state => state.token)
|
||||
const hydrated = useSessionStore(state => state.hydrated)
|
||||
const basePath = mobile ? '/m/seller/listings' : '/seller/listings'
|
||||
|
||||
const [form, setForm] = useState<PublishForm>(defaultPublishForm)
|
||||
const [quantityValues, setQuantityValues] = useState<Record<string, number>>({})
|
||||
@@ -342,7 +343,7 @@ export function SellerListingFormPage() {
|
||||
if (!isEdit || !listing) return
|
||||
if (listing.status === 'completed') {
|
||||
message.error('该商品订单已完成,不能再次编辑')
|
||||
navigate('/seller/listings')
|
||||
navigate(basePath)
|
||||
}
|
||||
}, [isEdit, listing, message, navigate])
|
||||
|
||||
@@ -737,10 +738,10 @@ export function SellerListingFormPage() {
|
||||
: '发布成功,等待后台审核'
|
||||
)
|
||||
if (!isEdit && listing.listing_group_conversation_id) {
|
||||
navigate(`/messages/${listing.listing_group_conversation_id}`)
|
||||
navigate(`${mobile ? '/m' : ''}/messages/${listing.listing_group_conversation_id}`)
|
||||
return
|
||||
}
|
||||
navigate('/seller/listings')
|
||||
navigate(basePath)
|
||||
},
|
||||
})
|
||||
|
||||
@@ -784,7 +785,7 @@ export function SellerListingFormPage() {
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() =>
|
||||
navigate(`/login?redirect=${isEdit ? `/seller/listings/${id}/edit` : '/seller/listings/new'}`)
|
||||
navigate(`/login?redirect=${isEdit ? `${basePath}/${id}/edit` : `${basePath}/new`}`)
|
||||
}
|
||||
>
|
||||
去登录
|
||||
@@ -805,7 +806,7 @@ export function SellerListingFormPage() {
|
||||
description="可能不是本人发布的商品,或商品已不存在。请返回列表后重试。"
|
||||
action={
|
||||
<Space>
|
||||
<Button icon={<ArrowLeftOutlined />} onClick={() => navigate('/seller/listings')}>
|
||||
<Button icon={<ArrowLeftOutlined />} onClick={() => navigate(basePath)}>
|
||||
返回列表
|
||||
</Button>
|
||||
<Button icon={<ReloadOutlined />} onClick={() => void listingQuery.refetch()}>
|
||||
@@ -833,7 +834,7 @@ export function SellerListingFormPage() {
|
||||
<main className="seller-page seller-form-page">
|
||||
<header className="seller-form-head">
|
||||
<div>
|
||||
<Button type="text" icon={<ArrowLeftOutlined />} onClick={() => navigate('/seller/listings')}>
|
||||
<Button type="text" icon={<ArrowLeftOutlined />} onClick={() => navigate(basePath)}>
|
||||
返回列表
|
||||
</Button>
|
||||
<Typography.Text className="seller-eyebrow">SELLER PUBLISH · 号主发布</Typography.Text>
|
||||
|
||||
@@ -16,4 +16,5 @@
|
||||
@import './mobile-chat.css';
|
||||
@import './pc-messages.css';
|
||||
@import './seller.css';
|
||||
@import './mobile-seller.css';
|
||||
@import './components.css';
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
.mobile-seller {
|
||||
padding: 16px 16px 40px;
|
||||
}
|
||||
|
||||
.mobile-seller__hero {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 20px 18px;
|
||||
border-radius: 12px;
|
||||
background: var(--brand-ink);
|
||||
color: var(--paper-warm);
|
||||
}
|
||||
|
||||
.mobile-seller__eyebrow {
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.2em;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.mobile-seller__hero h1 {
|
||||
margin: 5px 0 6px;
|
||||
font-family: 'Noto Serif SC', 'Songti SC', serif;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.mobile-seller__hero p {
|
||||
margin: 0;
|
||||
color: rgba(246, 242, 232, 0.72);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.mobile-seller__filters {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin: 14px -16px 12px;
|
||||
padding: 0 16px 2px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.mobile-seller__filters button {
|
||||
flex: 0 0 auto;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 7px 12px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 999px;
|
||||
background: var(--paper-warm);
|
||||
color: var(--ink-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.mobile-seller__filters button.is-active {
|
||||
border-color: var(--brand-ink);
|
||||
background: var(--brand-ink);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.mobile-seller__filters em {
|
||||
font-style: normal;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.mobile-seller__list {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.mobile-seller__empty {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
justify-items: center;
|
||||
padding: 24px 0;
|
||||
}
|
||||
|
||||
.mobile-seller-card {
|
||||
padding: 14px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
background: var(--paper-warm);
|
||||
}
|
||||
|
||||
.mobile-seller-card__top,
|
||||
.mobile-seller-card__bottom,
|
||||
.mobile-seller-card__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.mobile-seller-card__no {
|
||||
color: var(--ink-muted);
|
||||
font-family: 'Inter', monospace;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.mobile-seller-card__tags {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.mobile-seller-card h2 {
|
||||
margin: 9px 0 7px;
|
||||
color: var(--ink);
|
||||
font-size: 16px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.mobile-seller-card__meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px 12px;
|
||||
color: var(--ink-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.mobile-seller-card__bottom {
|
||||
margin-top: 10px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px dashed var(--line);
|
||||
}
|
||||
|
||||
.mobile-seller-card__bottom strong {
|
||||
color: var(--brand-seal);
|
||||
font-family: 'Inter', monospace;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.mobile-seller-card__reason {
|
||||
margin: 9px 0 0;
|
||||
color: var(--danger);
|
||||
font-size: 11px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.mobile-seller-card__actions {
|
||||
justify-content: flex-start;
|
||||
margin-top: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* 移动端复用 PC 详情/发布组件时的容器适配 */
|
||||
.mobile-seller-host .seller-page {
|
||||
padding: 14px 12px 28px;
|
||||
}
|
||||
|
||||
.mobile-seller-host .seller-hero,
|
||||
.mobile-seller-host .seller-form-head,
|
||||
.mobile-seller-host .seller-detail-head {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.mobile-seller-host .seller-title {
|
||||
font-size: 22px !important;
|
||||
}
|
||||
|
||||
.mobile-seller-host .seller-stat-strip {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.mobile-seller-host .seller-form-grid,
|
||||
.mobile-seller-host .seller-meta-grid,
|
||||
.mobile-seller-host .seller-agreements,
|
||||
.mobile-seller-host .seller-price-grid,
|
||||
.mobile-seller-host .seller-quantity-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.mobile-seller-host .seller-quantity-head {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mobile-seller-host .seller-summary {
|
||||
position: static;
|
||||
}
|
||||
Reference in New Issue
Block a user