513 lines
12 KiB
Vue
513 lines
12 KiB
Vue
<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>
|