feat: 实现移动端我的商品管理列表,支持分类筛选、一键下架及重新提审
This commit is contained in:
@@ -67,4 +67,10 @@ export const mobileRoutes: RouteRecordRaw[] = [
|
||||
component: () => import('@/views/mobile/MobileSellerListingCreateView.vue'),
|
||||
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>
|
||||
<span>发布商品</span>
|
||||
</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>
|
||||
<span>我的商品</span>
|
||||
</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>
|
||||
Reference in New Issue
Block a user