Files
hfb_sys/frontend/src/views/mobile/MobileSellerListingsView.vue
T

513 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>