feat: 实现移动端我的商品管理列表,支持分类筛选、一键下架及重新提审

This commit is contained in:
yml
2026-05-25 09:31:54 +08:00
parent de4a4ac50e
commit e1ad98582b
3 changed files with 519 additions and 1 deletions
+6
View File
@@ -67,4 +67,10 @@ export const mobileRoutes: RouteRecordRaw[] = [
component: () => import('@/views/mobile/MobileSellerListingCreateView.vue'), component: () => import('@/views/mobile/MobileSellerListingCreateView.vue'),
meta: { layout: 'blank', requiresAuth: true, requiresRealname: true }, meta: { layout: 'blank', requiresAuth: true, requiresRealname: true },
}, },
{
path: '/m/seller/listings',
name: 'mobile-seller-listings',
component: () => import('@/views/mobile/MobileSellerListingsView.vue'),
meta: { layout: 'blank', requiresAuth: true },
},
] ]
@@ -325,7 +325,7 @@ function resolveAvatarURL(url: string | undefined | null) {
<div class="icon-wrap orange"><van-icon name="plus" :size="22" /></div> <div class="icon-wrap orange"><van-icon name="plus" :size="22" /></div>
<span>发布商品</span> <span>发布商品</span>
</div> </div>
<div class="grid-item" @click="showToast('卖家商品管理开发中')"> <div class="grid-item" @click="router.push('/m/seller/listings')">
<div class="icon-wrap purple"><van-icon name="shop-o" :size="22" /></div> <div class="icon-wrap purple"><van-icon name="shop-o" :size="22" /></div>
<span>我的商品</span> <span>我的商品</span>
</div> </div>
@@ -0,0 +1,512 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { showToast, showDialog } from 'vant'
import { fetchSellerListings, offlineListing, submitListingReview, type Listing } from '@/api/listings'
import { fetchFileBlobByURL } from '@/api/files'
import { listingReviewStatusLabel, listingStatusLabel } from '@/utils/statusLabels'
const router = useRouter()
const loading = ref(false)
const rawListings = ref<Listing[]>([])
const activeTab = ref('all')
interface DisplayListing extends Listing {
resolvedCover: string
updating: boolean
}
const displayListings = ref<DisplayListing[]>([])
onMounted(() => {
loadListings()
})
async function loadListings() {
loading.value = true
try {
const items = await fetchSellerListings()
displayListings.value = items.map(item => ({
...item,
resolvedCover: '',
updating: false
}))
// 异步解析图片,私有图片使用 Blob 加载,公有图片直接加载
displayListings.value.forEach((dl) => {
const coverUrl = dl.cover_url
if (!coverUrl) return
if (!coverUrl.includes('/api/files/object')) {
dl.resolvedCover = coverUrl
return
}
fetchFileBlobByURL(coverUrl)
.then(blob => {
dl.resolvedCover = URL.createObjectURL(blob)
})
.catch(() => {
dl.resolvedCover = '' // 失败则不展示
})
})
} catch {
showToast({ message: '获取商品列表失败', icon: 'cross' })
} finally {
loading.value = false
}
}
// 状态过滤逻辑
const filteredListings = computed(() => {
const tab = activeTab.value
if (tab === 'all') return displayListings.value
if (tab === 'reviewing') {
return displayListings.value.filter(item => item.review_status === 'pending')
}
if (tab === 'active') {
return displayListings.value.filter(item => item.status === 'published' && item.review_status === 'approved')
}
if (tab === 'offline') {
return displayListings.value.filter(item => item.status === 'offline' || item.review_status === 'rejected')
}
return displayListings.value
})
// 提审操作
async function handleSubmitReview(item: DisplayListing) {
item.updating = true
try {
const res = await submitListingReview(item.id)
showToast({
message: res.status === 'published' && res.review_status === 'approved' ? '已成功上架' : '已提交审核',
icon: 'passed'
})
await loadListings()
} catch (error) {
showToast({ message: '提审失败,请稍后重试', icon: 'cross' })
} finally {
item.updating = false
}
}
// 下架操作
async function handleOffline(item: DisplayListing) {
showDialog({
title: '下架确认',
message: '确定要下架该商品吗?下架后买家将无法搜索或租用该商品。',
showCancelButton: true
})
.then(async () => {
item.updating = true
try {
await offlineListing(item.id)
showToast({ message: '商品已下架', icon: 'passed' })
await loadListings()
} catch {
showToast({ message: '下架失败,请稍后重试', icon: 'cross' })
} finally {
item.updating = false
}
})
.catch(() => {})
}
function getStatusBadgeClass(item: Listing) {
if (item.review_status === 'pending') return 'badge-reviewing'
if (item.review_status === 'rejected') return 'badge-rejected'
if (item.status === 'published') return 'badge-active'
if (item.status === 'offline') return 'badge-offline'
if (item.status === 'rented') return 'badge-rented'
return 'badge-offline'
}
function getStatusText(item: Listing) {
if (item.review_status === 'pending') return '审核中'
if (item.review_status === 'rejected') return '审核被拒'
if (item.status === 'published') return '已上架'
if (item.status === 'offline') return '已下架'
if (item.status === 'rented') return '使用中'
return listingStatusLabel(item.status)
}
function goDetail(item: Listing) {
if (item.status === 'published' && item.review_status === 'approved') {
router.push(`/m/listings/${item.id}`)
} else {
showToast('该商品当前状态不支持预览')
}
}
function goBack() {
router.push('/m/profile')
}
</script>
<template>
<main class="mobile-seller-listings">
<!-- 顶部导航栏 -->
<header class="page-header">
<button class="back-btn" @click="goBack">
<van-icon name="arrow-left" :size="20" />
</button>
<h1>我的商品</h1>
<button class="header-action-btn" @click="router.push('/m/seller/listings/create')">
<van-icon name="plus" :size="18" />
<span>发布</span>
</button>
</header>
<!-- 状态切换 Tab -->
<van-tabs v-model:active="activeTab" class="custom-tabs" sticky>
<van-tab title="全部" name="all" />
<van-tab title="审核中" name="reviewing" />
<van-tab title="展示中" name="active" />
<van-tab title="已下架" name="offline" />
</van-tabs>
<!-- 商品列表 -->
<section class="listing-container">
<van-loading v-if="loading" class="center-loading" vertical>加载列表中...</van-loading>
<van-empty
v-else-if="filteredListings.length === 0"
description="暂无发布的商品记录"
class="empty-state"
/>
<div v-else class="listings-list">
<div
v-for="item in filteredListings"
:key="item.id"
class="listing-card"
@click="goDetail(item)"
>
<div class="card-content">
<!-- 封面图 -->
<div class="cover-wrap">
<img v-if="item.resolvedCover" :src="item.resolvedCover" alt="" class="cover-img" />
<div v-else class="cover-placeholder">
<van-icon name="photo-o" :size="24" />
</div>
</div>
<!-- 右侧详情 -->
<div class="info-wrap">
<div class="title-row">
<span class="game-tag">三角洲行动</span>
<span class="status-badge" :class="getStatusBadgeClass(item)">
{{ getStatusText(item) }}
</span>
</div>
<h3 class="listing-title">{{ item.title }}</h3>
<p class="listing-meta">{{ item.server_region }} · {{ item.login_platform }}</p>
<div class="price-row">
<span class="price-val">¥{{ item.price.toFixed(2) }}<small>/小时</small></span>
<span class="deposit-val">押金: ¥{{ item.deposit_amount.toFixed(2) }}</span>
</div>
</div>
</div>
<!-- 驳回原因展示 -->
<div v-if="item.review_status === 'rejected' && item.review_reason" class="reason-alert">
<van-icon name="warning-o" />
<span>拒绝原因{{ item.review_reason }}</span>
</div>
<!-- 操作栏 -->
<div class="card-actions" @click.stop>
<div class="time-box">
<span>更新时间: {{ item.updated_at.split('T')[0] }}</span>
</div>
<div class="btns-box">
<van-button
v-if="item.status === 'published'"
size="small"
type="danger"
plain
round
class="action-btn"
:loading="item.updating"
@click="handleOffline(item)"
>
下架
</van-button>
<van-button
v-if="item.status === 'offline' || item.review_status === 'rejected'"
size="small"
type="primary"
round
class="action-btn"
:loading="item.updating"
@click="handleSubmitReview(item)"
>
重新上架
</van-button>
</div>
</div>
</div>
</div>
</section>
</main>
</template>
<style scoped>
.mobile-seller-listings {
min-height: 100dvh;
background: #f6f8fa;
padding-bottom: calc(20px + env(safe-area-inset-bottom));
}
/* ========== 顶部导航 ========== */
.page-header {
position: sticky;
top: 0;
z-index: 100;
display: flex;
align-items: center;
justify-content: space-between;
height: 48px;
padding: 0 12px;
background: rgba(255, 255, 255, 0.94);
backdrop-filter: blur(10px);
border-bottom: 1px solid rgba(243, 244, 246, 0.8);
}
.page-header h1 {
margin: 0;
font-size: 17px;
font-weight: 700;
color: #111827;
}
.back-btn {
display: grid;
width: 36px;
height: 36px;
place-items: center;
border: none;
background: none;
color: #374151;
cursor: pointer;
}
.header-action-btn {
display: flex;
align-items: center;
gap: 2px;
border: none;
background: none;
color: #ff6a00;
font-size: 13px;
font-weight: 700;
cursor: pointer;
padding: 0 8px;
}
/* ========== Tab 栏样式 ========== */
.custom-tabs {
position: sticky;
top: 48px;
z-index: 99;
background: rgba(255, 255, 255, 0.94);
backdrop-filter: blur(10px);
border-bottom: 1px solid rgba(243, 244, 246, 0.6);
}
:deep(.van-tabs__nav) {
background: transparent;
padding-bottom: 4px;
}
:deep(.van-tab) {
font-size: 13px;
font-weight: 600;
}
/* ========== 列表内容 ========== */
.listing-container {
padding: 14px 16px;
}
.center-loading {
display: flex;
justify-content: center;
align-items: center;
padding: 60px 0;
}
.empty-state {
padding: 40px 0;
}
.listings-list {
display: flex;
flex-direction: column;
gap: 12px;
}
/* ========== 商品卡片 ========== */
.listing-card {
background: #ffffff;
border-radius: 16px;
padding: 14px;
box-shadow: 0 4px 18px rgba(0, 0, 0, 0.02), 0 1px 4px rgba(0, 0, 0, 0.02);
border: 1px solid rgba(243, 244, 246, 0.9);
cursor: pointer;
display: flex;
flex-direction: column;
}
.card-content {
display: flex;
gap: 12px;
}
.cover-wrap {
position: relative;
width: 80px;
height: 80px;
border-radius: 12px;
overflow: hidden;
flex-shrink: 0;
background: #f3f4f6;
border: 1px solid #f3f4f6;
}
.cover-img {
width: 100%;
height: 100%;
object-fit: cover;
}
.cover-placeholder {
width: 100%;
height: 100%;
display: grid;
place-items: center;
color: #9ca3af;
}
.info-wrap {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.title-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 4px;
}
.game-tag {
font-size: 10px;
color: #1477ff;
background: rgba(20, 119, 255, 0.08);
padding: 2px 6px;
border-radius: 6px;
font-weight: 700;
}
.status-badge {
font-size: 10px;
padding: 2px 6px;
border-radius: 6px;
font-weight: 700;
}
/* 状态颜色 */
.badge-reviewing { color: #f59e0b; background: rgba(245, 158, 11, 0.08); }
.badge-rejected { color: #ef4444; background: rgba(239, 68, 68, 0.08); }
.badge-active { color: #10b981; background: rgba(16, 185, 129, 0.08); }
.badge-offline { color: #6b7280; background: rgba(107, 114, 128, 0.08); }
.badge-rented { color: #2563eb; background: rgba(37, 99, 235, 0.08); }
.listing-title {
margin: 0 0 4px;
font-size: 14px;
font-weight: 700;
color: #111827;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
}
.listing-meta {
margin: 0 0 6px;
font-size: 11px;
color: #6b7280;
}
.price-row {
display: flex;
align-items: baseline;
justify-content: space-between;
}
.price-val {
font-size: 16px;
font-weight: 800;
color: #ff5f00;
}
.price-val small {
font-size: 10px;
font-weight: 500;
}
.deposit-val {
font-size: 10px;
color: #9ca3af;
}
/* ========== 驳回警告栏 ========== */
.reason-alert {
display: flex;
align-items: center;
gap: 6px;
margin-top: 10px;
padding: 8px 12px;
border-radius: 10px;
background: #fef2f2;
border: 1px solid #fee2e2;
color: #dc2626;
font-size: 11px;
}
.reason-alert :deep(.van-icon) {
font-size: 13px;
}
/* ========== 操作栏 ========== */
.card-actions {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 12px;
padding-top: 10px;
border-top: 1px solid #f3f4f6;
}
.time-box {
font-size: 10px;
color: #9ca3af;
}
.btns-box {
display: flex;
gap: 8px;
}
.action-btn {
height: 26px !important;
padding: 0 12px !important;
font-size: 11px !important;
font-weight: 700 !important;
}
</style>