修复用户发布下架后审核还有的bug
This commit is contained in:
@@ -254,11 +254,12 @@ func (h *Handler) Offline(c *gin.Context) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := h.service.Offline(ownerID, id); err != nil {
|
item, err := h.service.Offline(ownerID, id)
|
||||||
|
if err != nil {
|
||||||
writeListingError(c, err)
|
writeListingError(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
response.OK(c, gin.H{"offline": true})
|
response.OK(c, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) ListPublic(c *gin.Context) {
|
func (h *Handler) ListPublic(c *gin.Context) {
|
||||||
|
|||||||
@@ -252,7 +252,7 @@ func (r *Repository) SubmitReview(ownerID uint64, listingID uint64, reviewRequir
|
|||||||
func (r *Repository) ListPendingReview() ([]ListingDTO, error) {
|
func (r *Repository) ListPendingReview() ([]ListingDTO, error) {
|
||||||
var rows []listingRow
|
var rows []listingRow
|
||||||
err := r.baseQuery().
|
err := r.baseQuery().
|
||||||
Where("l.review_status = ?", "pending").
|
Where("l.review_status = ? AND l.status <> ?", "pending", "offline").
|
||||||
Order("l.updated_at ASC, l.id ASC").
|
Order("l.updated_at ASC, l.id ASC").
|
||||||
Limit(200).
|
Limit(200).
|
||||||
Scan(&rows).Error
|
Scan(&rows).Error
|
||||||
@@ -612,8 +612,9 @@ func (r *Repository) Reject(listingID uint64, req ReviewRequest) (*ListingDTO, e
|
|||||||
return dto, err
|
return dto, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repository) Offline(ownerID uint64, listingID uint64) error {
|
func (r *Repository) Offline(ownerID uint64, listingID uint64) (*ListingDTO, error) {
|
||||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
var dto *ListingDTO
|
||||||
|
err := r.db.Transaction(func(tx *gorm.DB) error {
|
||||||
listing, account, err := r.findOwnedForUpdate(tx, ownerID, listingID)
|
listing, account, err := r.findOwnedForUpdate(tx, ownerID, listingID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -622,12 +623,20 @@ func (r *Repository) Offline(ownerID uint64, listingID uint64) error {
|
|||||||
return ErrListingLocked
|
return ErrListingLocked
|
||||||
}
|
}
|
||||||
listing.Status = "offline"
|
listing.Status = "offline"
|
||||||
|
listing.ReviewStatus = "none"
|
||||||
|
listing.ReviewReason = "号主已手动下架"
|
||||||
|
listing.PublishedAt = nil
|
||||||
account.Status = "offline"
|
account.Status = "offline"
|
||||||
if err := tx.Save(account).Error; err != nil {
|
if err := tx.Save(account).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return tx.Save(listing).Error
|
if err := tx.Save(listing).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dto = toDTO(*account, *listing)
|
||||||
|
return nil
|
||||||
})
|
})
|
||||||
|
return dto, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repository) ListPublic(query PublicListQuery) (*PublicListResult, error) {
|
func (r *Repository) ListPublic(query PublicListQuery) (*PublicListResult, error) {
|
||||||
@@ -1382,6 +1391,7 @@ func applySellerListingPrice(item *ListingDTO) {
|
|||||||
func (row listingRow) toDTO() ListingDTO {
|
func (row listingRow) toDTO() ListingDTO {
|
||||||
assetSummary := decodeAssetSummary(row.AssetSummary)
|
assetSummary := decodeAssetSummary(row.AssetSummary)
|
||||||
screenshotURLS := cleanScreenshotURLs(decodeScreenshots(row.ScreenshotURLS))
|
screenshotURLS := cleanScreenshotURLs(decodeScreenshots(row.ScreenshotURLS))
|
||||||
|
reviewStatus, reviewReason := normalizedReviewState(row.Status, row.ReviewStatus, row.ReviewReason)
|
||||||
return ListingDTO{
|
return ListingDTO{
|
||||||
ID: row.ID,
|
ID: row.ID,
|
||||||
AccountID: row.AccountID,
|
AccountID: row.AccountID,
|
||||||
@@ -1403,8 +1413,8 @@ func (row listingRow) toDTO() ListingDTO {
|
|||||||
IsAccelerated: isAcceleratedSale(assetSummary),
|
IsAccelerated: isAcceleratedSale(assetSummary),
|
||||||
InTransaction: row.InTransaction,
|
InTransaction: row.InTransaction,
|
||||||
Status: row.Status,
|
Status: row.Status,
|
||||||
ReviewStatus: row.ReviewStatus,
|
ReviewStatus: reviewStatus,
|
||||||
ReviewReason: row.ReviewReason,
|
ReviewReason: reviewReason,
|
||||||
PublishedAt: row.PublishedAt,
|
PublishedAt: row.PublishedAt,
|
||||||
CreatedAt: row.CreatedAt,
|
CreatedAt: row.CreatedAt,
|
||||||
UpdatedAt: row.UpdatedAt,
|
UpdatedAt: row.UpdatedAt,
|
||||||
@@ -1414,6 +1424,7 @@ func (row listingRow) toDTO() ListingDTO {
|
|||||||
func toDTO(account model.GameAccount, listing model.RentalListing) *ListingDTO {
|
func toDTO(account model.GameAccount, listing model.RentalListing) *ListingDTO {
|
||||||
assetSummary := decodeAssetSummary(account.AssetSummary)
|
assetSummary := decodeAssetSummary(account.AssetSummary)
|
||||||
screenshotURLS := cleanScreenshotURLs(decodeScreenshots(account.ScreenshotURLS))
|
screenshotURLS := cleanScreenshotURLs(decodeScreenshots(account.ScreenshotURLS))
|
||||||
|
reviewStatus, reviewReason := normalizedReviewState(listing.Status, listing.ReviewStatus, listing.ReviewReason)
|
||||||
return &ListingDTO{
|
return &ListingDTO{
|
||||||
ID: listing.ID,
|
ID: listing.ID,
|
||||||
AccountID: account.ID,
|
AccountID: account.ID,
|
||||||
@@ -1433,14 +1444,21 @@ func toDTO(account model.GameAccount, listing model.RentalListing) *ListingDTO {
|
|||||||
IsAccelerated: isAcceleratedSale(assetSummary),
|
IsAccelerated: isAcceleratedSale(assetSummary),
|
||||||
InTransaction: listing.InTransaction,
|
InTransaction: listing.InTransaction,
|
||||||
Status: listing.Status,
|
Status: listing.Status,
|
||||||
ReviewStatus: listing.ReviewStatus,
|
ReviewStatus: reviewStatus,
|
||||||
ReviewReason: listing.ReviewReason,
|
ReviewReason: reviewReason,
|
||||||
PublishedAt: listing.PublishedAt,
|
PublishedAt: listing.PublishedAt,
|
||||||
CreatedAt: listing.CreatedAt,
|
CreatedAt: listing.CreatedAt,
|
||||||
UpdatedAt: listing.UpdatedAt,
|
UpdatedAt: listing.UpdatedAt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func normalizedReviewState(status string, reviewStatus string, reviewReason string) (string, string) {
|
||||||
|
if status == "offline" {
|
||||||
|
return "none", reviewReason
|
||||||
|
}
|
||||||
|
return reviewStatus, reviewReason
|
||||||
|
}
|
||||||
|
|
||||||
func marshalScreenshots(urls []string) (datatypes.JSON, error) {
|
func marshalScreenshots(urls []string) (datatypes.JSON, error) {
|
||||||
cleaned := cleanScreenshotURLs(urls)
|
cleaned := cleanScreenshotURLs(urls)
|
||||||
if len(cleaned) > 12 {
|
if len(cleaned) > 12 {
|
||||||
|
|||||||
@@ -266,9 +266,9 @@ func (s *Service) Reject(id uint64, req ReviewRequest) (*ListingDTO, error) {
|
|||||||
return s.repo.Reject(id, req)
|
return s.repo.Reject(id, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) Offline(ownerID uint64, id uint64) error {
|
func (s *Service) Offline(ownerID uint64, id uint64) (*ListingDTO, error) {
|
||||||
if s.repo == nil {
|
if s.repo == nil {
|
||||||
return ErrDependencyUnavailable
|
return nil, ErrDependencyUnavailable
|
||||||
}
|
}
|
||||||
return s.repo.Offline(ownerID, id)
|
return s.repo.Offline(ownerID, id)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ export async function submitListingReview(id: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function offlineListing(id: number) {
|
export async function offlineListing(id: number) {
|
||||||
const { data } = await apiClient.delete<ApiResponse<{ offline: boolean }>>(`/listings/${id}`)
|
const { data } = await apiClient.delete<ApiResponse<Listing>>(`/listings/${id}`)
|
||||||
return data.data
|
return data.data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,30 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ElMessage } from 'element-plus'
|
import { Plus, Refresh } from '@element-plus/icons-vue'
|
||||||
import { onMounted, ref } from 'vue'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
|
||||||
import { fetchSellerListings, offlineListing, submitListingReview, type Listing } from '@/features/listings'
|
import { fetchSellerListings, offlineListing, submitListingReview, type Listing } from '@/features/listings'
|
||||||
import { listingReviewStatusLabel, listingStatusLabel } from '@/utils/statusLabels'
|
import { listingReviewStatusLabel, listingStatusLabel } from '@/utils/statusLabels'
|
||||||
import { getListingSellerPrice } from '@/utils/listingDisplay'
|
import { getListingSellerPrice } from '@/utils/listingDisplay'
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
const submittingID = ref<number | null>(null)
|
||||||
|
const offliningID = ref<number | null>(null)
|
||||||
const listings = ref<Listing[]>([])
|
const listings = ref<Listing[]>([])
|
||||||
|
const statusFilter = ref('all')
|
||||||
|
|
||||||
|
const displayListings = computed(() => {
|
||||||
|
if (statusFilter.value === 'all') return listings.value
|
||||||
|
if (statusFilter.value === 'pending') return listings.value.filter((item) => isPendingReview(item))
|
||||||
|
return listings.value.filter((item) => item.status === statusFilter.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const stats = 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 },
|
||||||
|
])
|
||||||
|
|
||||||
onMounted(loadListings)
|
onMounted(loadListings)
|
||||||
|
|
||||||
@@ -20,67 +37,431 @@ async function loadListings() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function submitReview(id: number) {
|
async function submitReview(row: Listing) {
|
||||||
const listing = await submitListingReview(id)
|
submittingID.value = row.id
|
||||||
|
try {
|
||||||
|
const listing = await submitListingReview(row.id)
|
||||||
|
replaceListing(listing)
|
||||||
ElMessage.success(
|
ElMessage.success(
|
||||||
listing.status === 'published' && listing.review_status === 'approved'
|
listing.status === 'published' && listing.review_status === 'approved'
|
||||||
? '已上架'
|
? '已上架'
|
||||||
: '已提交审核,等待后台处理',
|
: '已提交审核,等待后台处理',
|
||||||
)
|
)
|
||||||
await loadListings()
|
} finally {
|
||||||
|
submittingID.value = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function offline(id: number) {
|
async function offline(row: Listing) {
|
||||||
await offlineListing(id)
|
try {
|
||||||
|
await ElMessageBox.confirm(`确认下架「${row.title}」吗?下架后后台待审列表会同步移除。`, '下架确认', {
|
||||||
|
confirmButtonText: '确认下架',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
offliningID.value = row.id
|
||||||
|
try {
|
||||||
|
const listing = await offlineListing(row.id)
|
||||||
|
replaceListing(listing)
|
||||||
ElMessage.success('已下架')
|
ElMessage.success('已下架')
|
||||||
await loadListings()
|
} 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) {
|
function listingPrice(row: Listing) {
|
||||||
return `¥${Math.round(getListingSellerPrice(row))}`
|
return `¥${Math.round(getListingSellerPrice(row))}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function coinText(row: Listing) {
|
||||||
|
const coinM = Number(row.haf_coin_amount || 0) / 1000000
|
||||||
|
return Number.isInteger(coinM) ? `${coinM}M` : `${coinM.toFixed(2)}M`
|
||||||
|
}
|
||||||
|
|
||||||
|
function canSubmit(row: Listing) {
|
||||||
|
return !isPendingReview(row) && row.status !== 'rented' && row.status !== 'published'
|
||||||
|
}
|
||||||
|
|
||||||
|
function canOffline(row: Listing) {
|
||||||
|
return row.status !== 'rented' && row.status !== 'offline'
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusTone(status: string) {
|
||||||
|
const tones: Record<string, string> = {
|
||||||
|
published: 'success',
|
||||||
|
draft: 'info',
|
||||||
|
offline: 'muted',
|
||||||
|
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 isPendingReview(row: Listing) {
|
||||||
|
return row.status !== 'offline' && row.review_status === 'pending'
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="page">
|
<section class="page seller-listings-page">
|
||||||
<div class="page-header page-header-row">
|
<div class="seller-hero">
|
||||||
<div>
|
<div>
|
||||||
<p class="eyebrow">Seller</p>
|
<p class="eyebrow">Seller</p>
|
||||||
<h1>我的发布</h1>
|
<h1>我的发布</h1>
|
||||||
<p>管理账号发布、审核状态、上架和下架。</p>
|
<p>查看账号状态、审核进度,并管理上架与下架。</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="hero-actions">
|
||||||
|
<el-button :icon="Refresh" :loading="loading" @click="loadListings">刷新</el-button>
|
||||||
<RouterLink to="/seller/listings/create">
|
<RouterLink to="/seller/listings/create">
|
||||||
<el-button type="primary">发布账号</el-button>
|
<el-button type="primary" :icon="Plus">发布账号</el-button>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<el-table v-loading="loading" class="table-panel" :data="listings">
|
<div class="seller-stats">
|
||||||
<el-table-column prop="title" label="标题" min-width="180" />
|
<button
|
||||||
<el-table-column prop="server_region" label="区服" width="120" />
|
v-for="item in stats"
|
||||||
<el-table-column prop="haf_coin_amount" label="哈夫币" width="120" />
|
:key="item.key"
|
||||||
<el-table-column label="价格" width="100">
|
type="button"
|
||||||
<template #default="{ row }">{{ listingPrice(row) }}</template>
|
class="stat-card"
|
||||||
</el-table-column>
|
:class="{ active: statusFilter === item.key }"
|
||||||
<el-table-column prop="deposit_amount" label="押金" width="100" />
|
@click="statusFilter = item.key"
|
||||||
<el-table-column label="状态" width="110">
|
>
|
||||||
<template #default="{ row }">{{ listingStatusLabel(row.status) }}</template>
|
<span>{{ item.label }}</span>
|
||||||
</el-table-column>
|
<strong>{{ item.value }}</strong>
|
||||||
<el-table-column label="审核" width="110">
|
</button>
|
||||||
<template #default="{ row }">{{ listingReviewStatusLabel(row.review_status) }}</template>
|
</div>
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="review_reason" label="审核原因" min-width="160" show-overflow-tooltip />
|
<div v-loading="loading" class="listing-card-panel">
|
||||||
<el-table-column label="操作" width="190">
|
<el-empty v-if="!displayListings.length" description="暂无符合条件的发布" />
|
||||||
<template #default="{ row }">
|
|
||||||
|
<article v-for="item in displayListings" :key="item.id" class="listing-card">
|
||||||
|
<div class="listing-main">
|
||||||
|
<div class="listing-title-row">
|
||||||
|
<h3>{{ item.title }}</h3>
|
||||||
|
<div class="listing-tags">
|
||||||
|
<span class="status-pill" :class="statusTone(item.status)">{{ listingStatusLabel(item.status) }}</span>
|
||||||
|
<span class="status-pill" :class="reviewTone(effectiveReviewStatus(item))">
|
||||||
|
{{ listingReviewStatusLabel(effectiveReviewStatus(item)) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</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>¥{{ item.deposit_amount }}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p v-if="item.review_reason" class="review-reason">{{ item.review_reason }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="listing-actions">
|
||||||
<el-button
|
<el-button
|
||||||
size="small"
|
:disabled="!canSubmit(item)"
|
||||||
:disabled="row.review_status === 'pending' || row.status === 'rented' || row.status === 'published'"
|
:loading="submittingID === item.id"
|
||||||
@click="submitReview(row.id)"
|
@click="submitReview(item)"
|
||||||
>
|
>
|
||||||
提审
|
提审
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button size="small" type="danger" :disabled="row.status === 'rented'" @click="offline(row.id)">下架</el-button>
|
<el-button
|
||||||
</template>
|
type="danger"
|
||||||
</el-table-column>
|
plain
|
||||||
</el-table>
|
:disabled="!canOffline(item)"
|
||||||
|
:loading="offliningID === item.id"
|
||||||
|
@click="offline(item)"
|
||||||
|
>
|
||||||
|
下架
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.seller-listings-page {
|
||||||
|
display: grid;
|
||||||
|
gap: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seller-hero {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.seller-hero h1 {
|
||||||
|
margin: 0;
|
||||||
|
color: #0f172a;
|
||||||
|
font-size: 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seller-hero p:last-child {
|
||||||
|
margin: 10px 0 0;
|
||||||
|
color: #64748b;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seller-stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
min-height: 76px;
|
||||||
|
padding: 16px 18px;
|
||||||
|
border: 1px solid #e6ebf2;
|
||||||
|
border-radius: 18px;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card strong {
|
||||||
|
color: #0f172a;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card.active,
|
||||||
|
.stat-card:hover {
|
||||||
|
border-color: #409eff;
|
||||||
|
box-shadow: 0 12px 28px rgba(64, 158, 255, 0.14);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.listing-card-panel {
|
||||||
|
display: grid;
|
||||||
|
min-height: 220px;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 10px 28px rgba(15, 23, 42, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.listing-main {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listing-title-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listing-title-row h3 {
|
||||||
|
min-width: 0;
|
||||||
|
margin: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
color: #1f2937;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.45;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listing-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-pill {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 24px;
|
||||||
|
padding: 0 9px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #eef2f7;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-pill.success {
|
||||||
|
background: #ecfdf3;
|
||||||
|
color: #15803d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-pill.warning {
|
||||||
|
background: #fff7ed;
|
||||||
|
color: #c2410c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-pill.danger {
|
||||||
|
background: #fef2f2;
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-pill.info {
|
||||||
|
background: #eff6ff;
|
||||||
|
color: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-pill.muted {
|
||||||
|
background: #f1f5f9;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listing-meta-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(110px, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listing-meta-grid div {
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listing-meta-grid span {
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listing-meta-grid strong {
|
||||||
|
color: #334155;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-text {
|
||||||
|
color: #f97316 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.review-reason {
|
||||||
|
margin: 12px 0 0;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #fff7ed;
|
||||||
|
color: #b45309;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listing-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
@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) {
|
||||||
|
.seller-hero {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seller-stats,
|
||||||
|
.listing-meta-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listing-actions {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user