优化了 pc 和移动端我的商品界面 ui
This commit is contained in:
@@ -0,0 +1,580 @@
|
||||
<script setup lang="ts">
|
||||
import { showDialog, showToast } from 'vant'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import {
|
||||
fetchSellerListings,
|
||||
offlineListing,
|
||||
submitListingReview,
|
||||
type Listing,
|
||||
} from '@/features/listings'
|
||||
import { formatCent, formatMoneyWithSymbol } from '@/shared/utils/money'
|
||||
import { listingReviewStatusLabel, listingStatusLabel } from '@/shared/utils/statusLabels'
|
||||
import { formatListingCode, getListingSellerPrice } from '@/shared/utils/listingDisplay'
|
||||
import MobileBottomNav from '@/components/MobileBottomNav.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
const submittingID = ref<number | null>(null)
|
||||
const offliningID = ref<number | null>(null)
|
||||
const listings = ref<Listing[]>([])
|
||||
const activeTab = ref('all')
|
||||
|
||||
const statusTabs = computed(() => [
|
||||
{ key: 'all', label: '全部', value: listings.value.length },
|
||||
{
|
||||
key: 'published',
|
||||
label: '已上架',
|
||||
value: listings.value.filter(item => item.status === 'published').length,
|
||||
},
|
||||
{
|
||||
key: 'pending',
|
||||
label: '待审核',
|
||||
value: listings.value.filter(item => isPendingReview(item)).length,
|
||||
},
|
||||
{
|
||||
key: 'offline',
|
||||
label: '已下架',
|
||||
value: listings.value.filter(item => item.status === 'offline').length,
|
||||
},
|
||||
{
|
||||
key: 'completed',
|
||||
label: '已完成',
|
||||
value: listings.value.filter(item => item.status === 'completed').length,
|
||||
},
|
||||
])
|
||||
|
||||
const displayListings = computed(() => {
|
||||
if (activeTab.value === 'all') return listings.value
|
||||
if (activeTab.value === 'pending') return listings.value.filter(item => isPendingReview(item))
|
||||
return listings.value.filter(item => item.status === activeTab.value)
|
||||
})
|
||||
|
||||
onMounted(loadListings)
|
||||
|
||||
async function loadListings() {
|
||||
loading.value = true
|
||||
try {
|
||||
listings.value = await fetchSellerListings()
|
||||
} catch {
|
||||
showToast({ message: '加载失败,请稍后重试', icon: 'warning-o' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function submitReview(row: Listing) {
|
||||
submittingID.value = row.id
|
||||
try {
|
||||
const listing = await submitListingReview(row.id)
|
||||
replaceListing(listing)
|
||||
showToast({
|
||||
message:
|
||||
listing.status === 'published' && listing.review_status === 'approved'
|
||||
? '已上架'
|
||||
: '已提交审核,等待后台处理',
|
||||
icon: 'passed',
|
||||
})
|
||||
} catch {
|
||||
showToast({ message: '提交失败,请稍后重试', icon: 'cross' })
|
||||
} finally {
|
||||
submittingID.value = null
|
||||
}
|
||||
}
|
||||
|
||||
async function offline(row: Listing) {
|
||||
try {
|
||||
await showDialog({
|
||||
title: '下架确认',
|
||||
message: `确认下架「${row.title}」吗?下架后后台待审列表会同步移除。`,
|
||||
showCancelButton: true,
|
||||
confirmButtonText: '确认下架',
|
||||
cancelButtonText: '取消',
|
||||
})
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
offliningID.value = row.id
|
||||
try {
|
||||
const listing = await offlineListing(row.id)
|
||||
replaceListing(listing)
|
||||
showToast({ message: '已下架', icon: 'passed' })
|
||||
} catch {
|
||||
showToast({ message: '下架失败,请稍后重试', icon: 'cross' })
|
||||
} finally {
|
||||
offliningID.value = null
|
||||
}
|
||||
}
|
||||
|
||||
function replaceListing(next: Listing) {
|
||||
const index = listings.value.findIndex(item => item.id === next.id)
|
||||
if (index >= 0) {
|
||||
listings.value.splice(index, 1, next)
|
||||
} else {
|
||||
listings.value.unshift(next)
|
||||
}
|
||||
}
|
||||
|
||||
function listingPrice(row: Listing) {
|
||||
return formatMoneyWithSymbol(getListingSellerPrice(row))
|
||||
}
|
||||
|
||||
async function copyListingCode(row: Listing) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(formatListingCode(row))
|
||||
showToast({ message: '商品编号已复制', icon: 'passed' })
|
||||
} catch {
|
||||
showToast({ message: '复制失败', icon: 'cross' })
|
||||
}
|
||||
}
|
||||
|
||||
function coinText(row: Listing) {
|
||||
const coinM = Number(row.haf_coin_amount || 0) / 1000000
|
||||
return Number.isInteger(coinM) ? `${coinM}M` : `${coinM.toFixed(2)}M`
|
||||
}
|
||||
|
||||
function goCreate() {
|
||||
router.push('/m/seller/listings/create')
|
||||
}
|
||||
|
||||
function goEdit(row: Listing) {
|
||||
router.push(`/m/seller/listings/${row.id}/edit`)
|
||||
}
|
||||
|
||||
function canSubmit(row: Listing) {
|
||||
return !isTerminalListing(row) && !isPendingReview(row) && row.status !== 'published'
|
||||
}
|
||||
|
||||
function canEdit(row: Listing) {
|
||||
return !isTerminalListing(row) && !isPendingReview(row) && row.status !== 'published'
|
||||
}
|
||||
|
||||
function canOffline(row: Listing) {
|
||||
return !isTerminalListing(row) && row.status !== 'offline'
|
||||
}
|
||||
|
||||
function statusTone(status: string) {
|
||||
const tones: Record<string, string> = {
|
||||
published: 'success',
|
||||
draft: 'info',
|
||||
offline: 'muted',
|
||||
completed: 'success',
|
||||
rented: 'warning',
|
||||
abnormal: 'danger',
|
||||
}
|
||||
return tones[status] || 'info'
|
||||
}
|
||||
|
||||
function reviewTone(status: string) {
|
||||
const tones: Record<string, string> = {
|
||||
approved: 'success',
|
||||
pending: 'warning',
|
||||
rejected: 'danger',
|
||||
none: 'muted',
|
||||
}
|
||||
return tones[status] || 'info'
|
||||
}
|
||||
|
||||
function effectiveReviewStatus(row: Listing) {
|
||||
return row.status === 'offline' ? 'none' : row.review_status
|
||||
}
|
||||
|
||||
function showReviewStatus(row: Listing) {
|
||||
return row.status !== 'completed'
|
||||
}
|
||||
|
||||
function isTerminalListing(row: Listing) {
|
||||
return row.status === 'rented' || row.status === 'completed'
|
||||
}
|
||||
|
||||
function isPendingReview(row: Listing) {
|
||||
return !isTerminalListing(row) && row.status !== 'offline' && row.review_status === 'pending'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="mobile-seller-listings">
|
||||
<!-- 顶部导航 -->
|
||||
<header class="page-header">
|
||||
<button class="back-btn" @click="router.back()">
|
||||
<van-icon name="arrow-left" :size="20" />
|
||||
</button>
|
||||
<h1>我的商品</h1>
|
||||
<button class="refresh-btn" :class="{ spinning: loading }" @click="loadListings">
|
||||
<van-icon name="replay" :size="18" />
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<!-- 状态筛选条 -->
|
||||
<van-tabs
|
||||
v-model:active="activeTab"
|
||||
class="custom-tabs"
|
||||
line-width="20px"
|
||||
line-height="3px"
|
||||
color="#ff6a00"
|
||||
title-active-color="#ff6a00"
|
||||
title-inactive-color="#6b7280"
|
||||
:border="false"
|
||||
swipeable
|
||||
animated
|
||||
>
|
||||
<van-tab
|
||||
v-for="tab in statusTabs"
|
||||
:key="tab.key"
|
||||
:name="tab.key"
|
||||
:title="tab.value > 0 ? `${tab.label} ${tab.value}` : tab.label"
|
||||
/>
|
||||
</van-tabs>
|
||||
|
||||
<!-- 商品列表 -->
|
||||
<section class="listing-list">
|
||||
<van-loading v-if="loading" class="center-loading" size="24px" vertical>加载中...</van-loading>
|
||||
|
||||
<div v-else-if="displayListings.length === 0" class="empty-state-wrap">
|
||||
<van-empty description="暂无相关商品" image="search" />
|
||||
<van-button round type="primary" class="empty-publish-btn" @click="goCreate">
|
||||
去发布账号
|
||||
</van-button>
|
||||
</div>
|
||||
|
||||
<div v-else v-for="item in displayListings" :key="item.id" class="listing-card">
|
||||
<!-- 卡片头:编号 + 状态 -->
|
||||
<div class="card-header">
|
||||
<button class="listing-code-chip" type="button" @click="copyListingCode(item)">
|
||||
编号 {{ formatListingCode(item) }}
|
||||
<van-icon name="description" :size="12" />
|
||||
</button>
|
||||
<div class="status-tags">
|
||||
<span class="status-badge" :class="statusTone(item.status)">
|
||||
{{ listingStatusLabel(item.status) }}
|
||||
</span>
|
||||
<span
|
||||
v-if="showReviewStatus(item)"
|
||||
class="status-badge"
|
||||
:class="reviewTone(effectiveReviewStatus(item))"
|
||||
>
|
||||
{{ listingReviewStatusLabel(effectiveReviewStatus(item)) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 卡片体 -->
|
||||
<h3 class="listing-title">{{ item.title }}</h3>
|
||||
|
||||
<div class="meta-grid">
|
||||
<div class="meta-item">
|
||||
<span class="meta-label">区服</span>
|
||||
<span class="meta-val">{{ item.server_region || '-' }}</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span class="meta-label">哈夫币</span>
|
||||
<span class="meta-val">{{ coinText(item) }}</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span class="meta-label">价格</span>
|
||||
<span class="meta-val price">{{ listingPrice(item) }}</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span class="meta-label">押金</span>
|
||||
<span class="meta-val">¥{{ formatCent(item.deposit_amount_cent) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p v-if="item.review_reason" class="review-reason">
|
||||
<van-icon name="info-o" :size="13" />
|
||||
{{ item.review_reason }}
|
||||
</p>
|
||||
|
||||
<!-- 卡片底:操作 -->
|
||||
<div class="card-footer">
|
||||
<van-button
|
||||
v-if="canEdit(item)"
|
||||
size="small"
|
||||
plain
|
||||
round
|
||||
icon="edit"
|
||||
class="action-btn"
|
||||
@click="goEdit(item)"
|
||||
>
|
||||
编辑
|
||||
</van-button>
|
||||
<van-button
|
||||
v-if="canSubmit(item)"
|
||||
size="small"
|
||||
plain
|
||||
round
|
||||
type="primary"
|
||||
class="action-btn"
|
||||
:loading="submittingID === item.id"
|
||||
@click="submitReview(item)"
|
||||
>
|
||||
提审
|
||||
</van-button>
|
||||
<van-button
|
||||
v-if="canOffline(item)"
|
||||
size="small"
|
||||
plain
|
||||
round
|
||||
type="danger"
|
||||
class="action-btn"
|
||||
:loading="offliningID === item.id"
|
||||
@click="offline(item)"
|
||||
>
|
||||
下架
|
||||
</van-button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<MobileBottomNav />
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.mobile-seller-listings {
|
||||
min-height: 100dvh;
|
||||
background: #f6f8fa;
|
||||
padding-bottom: calc(64px + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
/* ========== 顶部导航 ========== */
|
||||
.page-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
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 {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.back-btn,
|
||||
.refresh-btn {
|
||||
display: grid;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
place-items: center;
|
||||
border: none;
|
||||
background: none;
|
||||
color: #374151;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.refresh-btn.spinning :deep(.van-icon) {
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== Vant Tabs ========== */
|
||||
.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-list {
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.center-loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 60px 0;
|
||||
}
|
||||
|
||||
.empty-state-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 40px 0;
|
||||
}
|
||||
|
||||
.empty-publish-btn {
|
||||
margin-top: 4px;
|
||||
padding: 0 28px;
|
||||
background: linear-gradient(135deg, #ff8c00, #ff5f00);
|
||||
border: none;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* ========== 商品卡片 ========== */
|
||||
.listing-card {
|
||||
background: #ffffff;
|
||||
border-radius: 16px;
|
||||
margin-bottom: 14px;
|
||||
padding: 16px;
|
||||
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);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.listing-code-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 9px;
|
||||
border: 1px solid #dbeafe;
|
||||
border-radius: 999px;
|
||||
background: #eff6ff;
|
||||
color: #2563eb;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.status-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 3px 9px;
|
||||
border-radius: 8px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
background: #eef2f7;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.status-badge.success {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
color: #15803d;
|
||||
}
|
||||
|
||||
.status-badge.warning {
|
||||
background: rgba(217, 119, 6, 0.1);
|
||||
color: #c2410c;
|
||||
}
|
||||
|
||||
.status-badge.danger {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.status-badge.info {
|
||||
background: rgba(37, 99, 235, 0.1);
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.status-badge.muted {
|
||||
background: #f1f5f9;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.listing-title {
|
||||
margin: 12px 0 0;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.meta-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
padding: 12px 14px;
|
||||
background: #f9fafb;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.meta-label {
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.meta-val {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.meta-val.price {
|
||||
color: #ff5f00;
|
||||
font-size: 15px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.review-reason {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 5px;
|
||||
margin: 12px 0 0;
|
||||
padding: 9px 12px;
|
||||
border-radius: 10px;
|
||||
background: rgba(217, 119, 6, 0.08);
|
||||
color: #b45309;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
margin-top: 14px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #f3f4f6;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
height: 32px !important;
|
||||
padding: 0 16px !important;
|
||||
font-size: 13px !important;
|
||||
font-weight: 700 !important;
|
||||
}
|
||||
</style>
|
||||
@@ -188,9 +188,8 @@ function isPendingReview(row: Listing) {
|
||||
<template>
|
||||
<section class="page seller-listings-page">
|
||||
<div class="seller-hero">
|
||||
<div>
|
||||
<p class="eyebrow">Seller</p>
|
||||
<h1>我的发布</h1>
|
||||
<div class="hero-text">
|
||||
<h1>我的商品</h1>
|
||||
<p>查看账号状态、审核进度,并管理上架与下架。</p>
|
||||
</div>
|
||||
<div class="hero-actions">
|
||||
@@ -206,11 +205,11 @@ function isPendingReview(row: Listing) {
|
||||
v-for="item in stats"
|
||||
:key="item.key"
|
||||
type="button"
|
||||
class="stat-card"
|
||||
class="stat-chip"
|
||||
:class="{ active: statusFilter === item.key }"
|
||||
@click="statusFilter = item.key"
|
||||
>
|
||||
<span>{{ item.label }}</span>
|
||||
{{ item.label }}
|
||||
<strong>{{ item.value }}</strong>
|
||||
</button>
|
||||
</div>
|
||||
@@ -219,51 +218,48 @@ function isPendingReview(row: Listing) {
|
||||
<el-empty v-if="!displayListings.length" description="暂无符合条件的发布" />
|
||||
|
||||
<article v-for="item in displayListings" :key="item.id" class="listing-card">
|
||||
<div class="listing-main">
|
||||
<div class="listing-title-row">
|
||||
<div class="listing-title-main">
|
||||
<button class="listing-code-chip" type="button" @click="copyListingCode(item)">
|
||||
编号 {{ formatListingCode(item) }}
|
||||
<el-icon><CopyDocument /></el-icon>
|
||||
</button>
|
||||
<h3>{{ item.title }}</h3>
|
||||
</div>
|
||||
<div class="listing-tags">
|
||||
<span class="status-pill" :class="statusTone(item.status)">{{
|
||||
listingStatusLabel(item.status)
|
||||
}}</span>
|
||||
<span
|
||||
v-if="showReviewStatus(item)"
|
||||
class="status-pill"
|
||||
:class="reviewTone(effectiveReviewStatus(item))"
|
||||
>
|
||||
{{ listingReviewStatusLabel(effectiveReviewStatus(item)) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="listing-card-head">
|
||||
<button class="listing-code-chip" type="button" @click="copyListingCode(item)">
|
||||
编号 {{ formatListingCode(item) }}
|
||||
<el-icon><CopyDocument /></el-icon>
|
||||
</button>
|
||||
<div class="listing-tags">
|
||||
<span class="status-pill" :class="statusTone(item.status)">{{
|
||||
listingStatusLabel(item.status)
|
||||
}}</span>
|
||||
<span
|
||||
v-if="showReviewStatus(item)"
|
||||
class="status-pill"
|
||||
:class="reviewTone(effectiveReviewStatus(item))"
|
||||
>
|
||||
{{ listingReviewStatusLabel(effectiveReviewStatus(item)) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="listing-meta-grid">
|
||||
<div>
|
||||
<span>区服</span>
|
||||
<strong>{{ item.server_region || '-' }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>哈夫币</span>
|
||||
<strong>{{ coinText(item) }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>价格</span>
|
||||
<strong class="price-text">{{ listingPrice(item) }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>押金</span>
|
||||
<strong>¥{{ formatCent(item.deposit_amount_cent) }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p v-if="item.review_reason" class="review-reason">{{ item.review_reason }}</p>
|
||||
</div>
|
||||
|
||||
<h3 class="listing-title">{{ item.title }}</h3>
|
||||
|
||||
<div class="listing-meta-grid">
|
||||
<div>
|
||||
<span>区服</span>
|
||||
<strong>{{ item.server_region || '-' }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>哈夫币</span>
|
||||
<strong>{{ coinText(item) }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>价格</span>
|
||||
<strong class="price-text">{{ listingPrice(item) }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>押金</span>
|
||||
<strong>¥{{ formatCent(item.deposit_amount_cent) }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p v-if="item.review_reason" class="review-reason">{{ item.review_reason }}</p>
|
||||
|
||||
<div class="listing-actions">
|
||||
<RouterLink v-if="canEdit(item)" :to="editPath(item)">
|
||||
<el-button :icon="Edit">编辑</el-button>
|
||||
@@ -293,31 +289,31 @@ function isPendingReview(row: Listing) {
|
||||
<style scoped>
|
||||
.seller-listings-page {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.seller-hero {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
padding: 26px 28px;
|
||||
border: 1px solid #e7edf7;
|
||||
border-radius: 22px;
|
||||
background: linear-gradient(135deg, #ffffff 0%, #f7fbff 58%, #eef6ff 100%);
|
||||
box-shadow: 0 14px 36px rgba(15, 23, 42, 0.06);
|
||||
padding: 18px 22px;
|
||||
border: 1px solid #eef1f6;
|
||||
border-radius: 14px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.seller-hero h1 {
|
||||
.hero-text h1 {
|
||||
margin: 0;
|
||||
color: #0f172a;
|
||||
font-size: 34px;
|
||||
font-size: 22px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.seller-hero p:last-child {
|
||||
margin: 10px 0 0;
|
||||
color: #64748b;
|
||||
font-weight: 700;
|
||||
.hero-text p {
|
||||
margin: 6px 0 0;
|
||||
color: #94a3b8;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
@@ -328,102 +324,111 @@ function isPendingReview(row: Listing) {
|
||||
}
|
||||
|
||||
.seller-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
display: flex;
|
||||
.stat-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-height: 76px;
|
||||
padding: 16px 18px;
|
||||
gap: 6px;
|
||||
padding: 7px 14px;
|
||||
border: 1px solid #e6ebf2;
|
||||
border-radius: 18px;
|
||||
border-radius: 999px;
|
||||
background: #fff;
|
||||
color: #64748b;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.04);
|
||||
transition:
|
||||
transform 0.18s ease,
|
||||
border-color 0.18s ease,
|
||||
box-shadow 0.18s ease;
|
||||
}
|
||||
|
||||
.stat-card span {
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
border-color 0.15s ease,
|
||||
color 0.15s ease,
|
||||
background 0.15s ease;
|
||||
}
|
||||
|
||||
.stat-card strong {
|
||||
.stat-chip strong {
|
||||
color: #0f172a;
|
||||
font-size: 24px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.stat-card.active,
|
||||
.stat-card:hover {
|
||||
.stat-chip:hover {
|
||||
border-color: #cdddf5;
|
||||
}
|
||||
|
||||
.stat-chip.active {
|
||||
border-color: #409eff;
|
||||
box-shadow: 0 12px 28px rgba(64, 158, 255, 0.14);
|
||||
transform: translateY(-1px);
|
||||
background: #ecf5ff;
|
||||
color: #1d6fd0;
|
||||
}
|
||||
|
||||
.stat-chip.active strong {
|
||||
color: #1d6fd0;
|
||||
}
|
||||
|
||||
.listing-card-panel {
|
||||
display: grid;
|
||||
min-height: 220px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.listing-card-panel:not(:has(.el-empty)) {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
|
||||
gap: 14px;
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.listing-card {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 18px;
|
||||
align-items: center;
|
||||
padding: 18px;
|
||||
border: 1px solid #e7edf5;
|
||||
border-radius: 18px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 16px 18px;
|
||||
border: 1px solid #eef1f6;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
box-shadow: 0 10px 28px rgba(15, 23, 42, 0.05);
|
||||
transition:
|
||||
border-color 0.15s ease,
|
||||
box-shadow 0.15s ease;
|
||||
}
|
||||
|
||||
.listing-main {
|
||||
min-width: 0;
|
||||
.listing-card:hover {
|
||||
border-color: #d7e3f4;
|
||||
box-shadow: 0 6px 20px rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
|
||||
.listing-title-row {
|
||||
.listing-card-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.listing-title-main {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.listing-code-chip {
|
||||
display: inline-flex;
|
||||
width: fit-content;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 4px 9px;
|
||||
border: 1px solid #dbeafe;
|
||||
border-radius: 999px;
|
||||
background: #eff6ff;
|
||||
color: #2563eb;
|
||||
padding: 3px 8px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
background: #f8fafc;
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.listing-title-row h3 {
|
||||
.listing-code-chip:hover {
|
||||
border-color: #cbd5e1;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.listing-title {
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
color: #1f2937;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
line-height: 1.45;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
@@ -439,13 +444,13 @@ function isPendingReview(row: Listing) {
|
||||
.status-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 24px;
|
||||
min-height: 22px;
|
||||
padding: 0 9px;
|
||||
border-radius: 999px;
|
||||
background: #eef2f7;
|
||||
border-radius: 6px;
|
||||
background: #f1f5f9;
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.status-pill.success {
|
||||
@@ -475,42 +480,45 @@ function isPendingReview(row: Listing) {
|
||||
|
||||
.listing-meta-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(110px, 1fr));
|
||||
gap: 10px;
|
||||
margin-top: 14px;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px 18px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 10px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.listing-meta-grid div {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 12px;
|
||||
background: #f8fafc;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.listing-meta-grid span {
|
||||
color: #94a3b8;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.listing-meta-grid strong {
|
||||
color: #334155;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.price-text {
|
||||
color: #f97316 !important;
|
||||
font-size: 15px !important;
|
||||
}
|
||||
|
||||
.review-reason {
|
||||
margin: 12px 0 0;
|
||||
padding: 10px 12px;
|
||||
border-radius: 12px;
|
||||
margin: 0;
|
||||
padding: 9px 12px;
|
||||
border-radius: 8px;
|
||||
background: #fff7ed;
|
||||
color: #b45309;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
font-weight: 600;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
@@ -518,46 +526,19 @@ function isPendingReview(row: Listing) {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
margin-top: auto;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #f1f5f9;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.seller-hero,
|
||||
.listing-title-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.listing-card {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.hero-actions,
|
||||
.listing-actions,
|
||||
.listing-tags {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.seller-stats {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.listing-meta-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 560px) {
|
||||
@media (max-width: 768px) {
|
||||
.seller-hero {
|
||||
padding: 20px;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.seller-stats,
|
||||
.listing-meta-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.listing-actions {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
.hero-actions {
|
||||
justify-content: stretch;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -118,7 +118,7 @@ export const mobileRoutes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/m/seller/listings',
|
||||
name: 'mobile-seller-listings',
|
||||
component: () => import('@/features/seller/views/SellerListingsView.vue'),
|
||||
component: () => import('@/features/seller/views/MobileSellerListingsView.vue'),
|
||||
meta: { layout: 'blank', requiresAuth: true },
|
||||
},
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user