diff --git a/docs/Phase2-移动端卖家中心验收.md b/docs/Phase2-移动端卖家中心验收.md
new file mode 100644
index 0000000..b4ab126
--- /dev/null
+++ b/docs/Phase2-移动端卖家中心验收.md
@@ -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 表单和整数定价工具,避免两套价格算法。
+- 表单在移动容器下会单列堆叠;复杂字段仍保持完整。
+- 未执行真实发布写操作,避免污染开发库。
diff --git a/docs/项目计划.md b/docs/项目计划.md
index 50e4277..e62521d 100644
--- a/docs/项目计划.md
+++ b/docs/项目计划.md
@@ -381,7 +381,7 @@ hfb_jysh/
### Phase 3:C 端 PC + 移动端(3 周)
-> 状态:**进行中(2026-10-30)**。移动端已完成首页、商品列表/详情、下单确认、登录、个人中心、实名认证、订单列表/详情、消息中心与聊天,并接入支付、交接、确认收号、结账、还价、申诉、证据图片上传、订单群聊与客服会话;PC 商品详情、下单、订单中心、操作页、消息中心、聊天页与卖家发布/编辑/审核提交已接通,移动端卖家页面与分销长尾能力待迁移。
+> 状态:**进行中(2026-10-30)**。移动端已完成首页、商品列表/详情、下单确认、登录、个人中心、实名认证、订单列表/详情、消息中心、聊天与卖家中心,并接入支付、交接、确认收号、结账、还价、申诉、证据图片上传、订单群聊与客服会话;PC 商品详情、下单、订单中心、操作页、消息中心、聊天页与卖家发布/编辑/审核提交已接通,剩余分销长尾能力与真实订单端到端回归。
- 首页、商品列表、商品详情、撞车商品。
- 登录、注册、找回密码、实名认证。
diff --git a/frontend/src/app/AppRouter.tsx b/frontend/src/app/AppRouter.tsx
index c4a7731..af4b0d8 100644
--- a/frontend/src/app/AppRouter.tsx
+++ b/frontend/src/app/AppRouter.tsx
@@ -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() {
} />
} />
} />
+ } />
+ } />
+ } />
+ } />
} />
diff --git a/frontend/src/features/mobile/MobileProfilePage.tsx b/frontend/src/features/mobile/MobileProfilePage.tsx
index b318d88..08f8dcc 100644
--- a/frontend/src/features/mobile/MobileProfilePage.tsx
+++ b/frontend/src/features/mobile/MobileProfilePage.tsx
@@ -39,6 +39,13 @@ export function MobileProfilePage() {
navigate('/m/orders')}>我的订单
navigate('/m/messages')}>消息中心
+
+ navigate(user ? '/m/seller/listings' : '/m/login?redirect=/m/seller/listings')
+ }
+ >
+ 卖家中心
+
diff --git a/frontend/src/features/mobile/MobileSellerFormPage.tsx b/frontend/src/features/mobile/MobileSellerFormPage.tsx
new file mode 100644
index 0000000..11b107f
--- /dev/null
+++ b/frontend/src/features/mobile/MobileSellerFormPage.tsx
@@ -0,0 +1,9 @@
+import { SellerListingFormPage } from '@/features/seller/SellerListingFormPage'
+
+export function MobileSellerFormPage() {
+ return (
+
+
+
+ )
+}
diff --git a/frontend/src/features/mobile/MobileSellerListingDetailPage.tsx b/frontend/src/features/mobile/MobileSellerListingDetailPage.tsx
new file mode 100644
index 0000000..ba11331
--- /dev/null
+++ b/frontend/src/features/mobile/MobileSellerListingDetailPage.tsx
@@ -0,0 +1,9 @@
+import { SellerListingDetailPage } from '@/features/seller/SellerListingDetailPage'
+
+export function MobileSellerListingDetailPage() {
+ return (
+
+
+
+ )
+}
diff --git a/frontend/src/features/mobile/MobileSellerListingsPage.tsx b/frontend/src/features/mobile/MobileSellerListingsPage.tsx
new file mode 100644
index 0000000..83d4e99
--- /dev/null
+++ b/frontend/src/features/mobile/MobileSellerListingsPage.tsx
@@ -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
+ 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('all')
+ const [actingId, setActingId] = useState(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 (
+
+
+
+
+ )
+ }
+
+ if (!token) {
+ return (
+
+
+
+
登录后管理商品
+
发布、提审与下架只对当前账号开放。
+
+
+
+ )
+ }
+
+ return (
+
+
+
+
SELLER · 号主柜台
+
我的商品
+
查看审核进度,管理上架与下架。
+
+
+
+
+
+ {filters.map(item => (
+
+ ))}
+
+
+ {listingsQuery.isPending ? (
+
+ ) : listingsQuery.isError ? (
+
+
+
+ ) : display.length === 0 ? (
+
+
+
+
+ ) : (
+
+ {display.map(item => {
+ const status = listingStatusMeta(item.status)
+ const review = listingReviewStatusMeta(item.review_status)
+ return (
+
navigate(`/m/seller/listings/${item.id}`)}
+ >
+
+
{item.listing_no || `#${item.id}`}
+
+ {status.label}
+ {item.status !== 'completed' ? (
+ {review.label}
+ ) : null}
+
+
+ {item.title || '未命名账号'}
+
+ {item.server_region || '—'}
+ {coinText(item)}
+ 押金 {formatMoney(item.deposit_amount_cent)}
+
+
+ {formatMoney(sellerPriceCent(item))}
+
+
+ {item.review_status === 'rejected' && item.review_reason ? (
+ 驳回:{item.review_reason}
+ ) : null}
+ event.stopPropagation()}>
+
+
+
+
+
+ )
+ })}
+
+ )}
+
+ )
+}
diff --git a/frontend/src/features/seller/SellerListingDetailPage.tsx b/frontend/src/features/seller/SellerListingDetailPage.tsx
index 62997df..b9b99a1 100644
--- a/frontend/src/features/seller/SellerListingDetailPage.tsx
+++ b/frontend/src/features/seller/SellerListingDetailPage.tsx
@@ -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() {
@@ -292,7 +293,7 @@ export function SellerListingDetailPage() {
description="可能不是本人发布的商品,或商品已不存在。"
action={
- } onClick={() => navigate('/seller/listings')}>
+ } onClick={() => navigate(basePath)}>
返回列表
} onClick={() => void listingQuery.refetch()}>
@@ -341,7 +342,7 @@ export function SellerListingDetailPage() {
-
} onClick={() => navigate('/seller/listings')}>
+
} onClick={() => navigate(basePath)}>
返回列表
SELLER LISTING · 商品详情
@@ -359,7 +360,7 @@ export function SellerListingDetailPage() {
{canEdit(listing) ? (
}
- onClick={() => navigate(`/seller/listings/${listing.id}/edit`)}
+ onClick={() => navigate(`${basePath}/${listing.id}/edit`)}
>
编辑
diff --git a/frontend/src/features/seller/SellerListingFormPage.tsx b/frontend/src/features/seller/SellerListingFormPage.tsx
index 4ed5464..a1995a0 100644
--- a/frontend/src/features/seller/SellerListingFormPage.tsx
+++ b/frontend/src/features/seller/SellerListingFormPage.tsx
@@ -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
(defaultPublishForm)
const [quantityValues, setQuantityValues] = useState>({})
@@ -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() {
} onClick={() => void listingQuery.refetch()}>
@@ -833,7 +834,7 @@ export function SellerListingFormPage() {
- } onClick={() => navigate('/seller/listings')}>
+ } onClick={() => navigate(basePath)}>
返回列表
SELLER PUBLISH · 号主发布
diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css
index 564536a..2e753bd 100644
--- a/frontend/src/styles/global.css
+++ b/frontend/src/styles/global.css
@@ -16,4 +16,5 @@
@import './mobile-chat.css';
@import './pc-messages.css';
@import './seller.css';
+@import './mobile-seller.css';
@import './components.css';
diff --git a/frontend/src/styles/mobile-seller.css b/frontend/src/styles/mobile-seller.css
new file mode 100644
index 0000000..0a2c025
--- /dev/null
+++ b/frontend/src/styles/mobile-seller.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;
+}