feat: 优化后台商品管理与移动端个人中心

This commit is contained in:
yml2213
2026-11-05 19:10:48 +08:00
parent f01329e049
commit 53574236ef
5 changed files with 1042 additions and 446 deletions
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { computed } from 'vue'
import { ArrowLeft, ArrowRight, DArrowLeft, DArrowRight } from '@element-plus/icons-vue'
import { computed, ref, watch } from 'vue'
interface Props {
currentPage: number
@@ -23,6 +24,18 @@ const currentCount = computed(() => {
const start = (props.currentPage - 1) * props.pageSize
return Math.min(props.pageSize, props.total - start)
})
const jumpPage = ref<number | undefined>(props.currentPage)
watch(
() => props.currentPage,
page => {
jumpPage.value = page
}
)
watch(totalPages, pages => {
if (jumpPage.value && jumpPage.value > pages) jumpPage.value = pages
})
function handleSizeChange(size: number) {
emit('update:pageSize', size)
@@ -36,36 +49,120 @@ function prevPage() {
emit('pageChange')
}
function firstPage() {
if (props.currentPage <= 1) return
emit('update:currentPage', 1)
emit('pageChange')
}
function nextPage() {
if (props.currentPage >= totalPages.value) return
emit('update:currentPage', props.currentPage + 1)
emit('pageChange')
}
function lastPage() {
if (props.currentPage >= totalPages.value) return
emit('update:currentPage', totalPages.value)
emit('pageChange')
}
function jumpToPage() {
const requested = Number(jumpPage.value)
if (!Number.isFinite(requested)) {
jumpPage.value = props.currentPage
return
}
const page = Math.min(Math.max(Math.trunc(requested), 1), totalPages.value)
jumpPage.value = page
if (page === props.currentPage) return
emit('update:currentPage', page)
emit('pageChange')
}
</script>
<template>
<div class="table-pagination">
<span class="pagination-summary">当前页 {{ currentCount }} 条,共 {{ total }} 条</span>
<span class="pagination-summary">
共 <strong>{{ total }}</strong> 条
<em>本页 {{ currentCount }} 条</em>
</span>
<div class="pagination-controls">
<span class="pagination-size-label">每页</span>
<el-select
:model-value="pageSize"
class="page-size-select"
size="small"
:disabled="loading"
@update:model-value="handleSizeChange"
>
<el-option :value="10" label="10条" />
<el-option :value="20" label="20条" />
<el-option :value="50" label="50条" />
<el-option :value="100" label="100条" />
<el-option :value="10" label="10 条" />
<el-option :value="20" label="20 条" />
<el-option :value="50" label="50 条" />
<el-option :value="100" label="100 条" />
</el-select>
<span class="pagination-page">{{ currentPage }}/{{ totalPages }}页</span>
<el-button size="small" :disabled="currentPage <= 1 || loading" @click="prevPage"
>上页</el-button
>
<el-button size="small" :disabled="currentPage >= totalPages || loading" @click="nextPage"
>下页</el-button
>
<div class="pagination-nav">
<el-button
class="pagination-nav-btn"
size="small"
:icon="DArrowLeft"
:disabled="currentPage <= 1 || loading"
aria-label="第一页"
@click="firstPage"
/>
<el-button
class="pagination-nav-btn"
size="small"
:icon="ArrowLeft"
:disabled="currentPage <= 1 || loading"
aria-label="上一页"
@click="prevPage"
/>
<span class="pagination-page">
<strong>{{ currentPage }}</strong>
<i>/</i>
{{ totalPages }}
</span>
<el-button
class="pagination-nav-btn"
size="small"
:icon="ArrowRight"
:disabled="currentPage >= totalPages || loading"
aria-label="下一页"
@click="nextPage"
/>
<el-button
class="pagination-nav-btn"
size="small"
:icon="DArrowRight"
:disabled="currentPage >= totalPages || loading"
aria-label="最后一页"
@click="lastPage"
/>
</div>
<div class="pagination-jump">
<span>前往</span>
<el-input-number
v-model="jumpPage"
class="pagination-jump-input"
size="small"
:min="1"
:max="totalPages"
:precision="0"
:controls="false"
:disabled="loading || totalPages <= 1"
@keyup.enter="jumpToPage"
/>
<span>页</span>
<el-button
class="pagination-jump-btn"
size="small"
:disabled="loading || totalPages <= 1"
@click="jumpToPage"
>
跳转
</el-button>
</div>
</div>
</div>
</template>
@@ -11,6 +11,7 @@ import {
type AdminListingQuery,
type Listing,
} from '@/features/listings'
import AdminTablePagination from '@/features/admin/components/AdminTablePagination.vue'
import { useAdminTable } from '@/features/admin/composables/useAdminTable'
import { adminPath } from '@/shared/utils/adminPath'
import { formatCentWithSymbol } from '@/shared/utils/money'
@@ -79,14 +80,6 @@ const {
const listings = computed(() => listingPage.value.items)
const totalListings = computed(() => listingPage.value.total)
const publishedCount = computed(
() => listings.value.filter(item => item.status === 'published').length
)
const rentedCount = computed(() => listings.value.filter(item => item.status === 'rented').length)
const pendingCount = computed(
() => listings.value.filter(item => item.review_status === 'pending').length
)
const totalPages = computed(() => Math.max(1, Math.ceil(totalListings.value / pageSize.value)))
const insuranceOptions = computed(() =>
publishOptions.value.insurance_options.length
? publishOptions.value.insurance_options
@@ -372,36 +365,6 @@ function levelSlotLabel(value: string) {
return value ? value.replace(/级$/, '') : '-'
}
function setStatusFilter(status: string) {
filters.status = (filters.status === status ? '' : status) as AdminListingQuery['status']
filters.review_status = ''
void queryListings()
}
function setReviewStatusFilter(reviewStatus: string) {
filters.review_status = (
filters.review_status === reviewStatus ? '' : reviewStatus
) as AdminListingQuery['review_status']
filters.status = ''
void queryListings()
}
async function handlePageSizeChange(size: number) {
pageSize.value = size
currentPage.value = 1
await loadListings()
}
async function prevPage() {
currentPage.value = Math.max(1, currentPage.value - 1)
await loadListings()
}
async function nextPage() {
currentPage.value = Math.min(totalPages.value, currentPage.value + 1)
await loadListings()
}
async function copyTableScreenshot() {
try {
const blob = await createTableScreenshotBlob()
@@ -690,43 +653,25 @@ function formatQuantity(value: number) {
</div>
<div class="listing-control-panel">
<!-- 统计指标行 -->
<div class="listing-stats-row">
<div
class="stat-card clickable"
:class="{ active: !filters.status && !filters.review_status }"
@click="resetFilters"
>
<span class="stat-label">全部商品</span>
<strong class="stat-value">{{ totalListings }}</strong>
<div class="listing-filter-toolbar">
<div class="listing-filter-summary">
<strong>筛选条件</strong>
<span>
共 <b>{{ totalListings }}</b> 件商品
</span>
</div>
<div
class="stat-card clickable"
:class="{ active: filters.status === 'published' }"
@click="setStatusFilter('published')"
>
<span class="stat-label">已上架</span>
<strong class="stat-value">{{ publishedCount }}</strong>
</div>
<div
class="stat-card clickable"
:class="{ active: filters.status === 'rented' }"
@click="setStatusFilter('rented')"
>
<span class="stat-label">已锁定</span>
<strong class="stat-value">{{ rentedCount }}</strong>
</div>
<div
class="stat-card clickable"
:class="{ active: filters.review_status === 'pending' }"
@click="setReviewStatusFilter('pending')"
>
<span class="stat-label">待审核</span>
<strong class="stat-value">{{ pendingCount }}</strong>
<div class="listing-action-strip">
<el-button @click="resetFilters">重置</el-button>
<el-button type="primary" :icon="Search" :loading="loading" @click="queryListings"
>查询</el-button
>
<el-button :disabled="!listings.length" @click="copyTableScreenshot">复制截图</el-button>
<el-button :disabled="!listings.length" @click="downloadTableScreenshot"
>下载截图</el-button
>
</div>
</div>
<!-- 筛选和操作行 -->
<div class="listing-filter-row">
<el-form class="listing-filter-bar" label-position="top">
<el-form-item class="filter-keyword" label="编号">
@@ -813,7 +758,7 @@ function formatQuantity(value: number) {
<el-option label="极品专区" value="premium" />
</el-select>
</el-form-item>
<el-form-item class="filter-select" label="资产">
<el-form-item class="filter-select filter-asset" label="资产">
<el-popover
:visible="activeFilterPopover === 'asset'"
trigger="click"
@@ -980,16 +925,6 @@ function formatQuantity(value: number) {
</el-popover>
</el-form-item>
</el-form>
<div class="listing-action-strip">
<el-button @click="resetFilters">重置</el-button>
<el-button type="primary" :icon="Search" :loading="loading" @click="queryListings"
>查询</el-button
>
<el-button :disabled="!listings.length" @click="copyTableScreenshot">复制截图</el-button>
<el-button :disabled="!listings.length" @click="downloadTableScreenshot"
>下载截图</el-button
>
</div>
</div>
</div>
@@ -1095,29 +1030,13 @@ function formatQuantity(value: number) {
</el-table-column>
</el-table>
<div class="table-pagination">
<span class="pagination-summary"
>当前页 {{ listings.length }} 条,共 {{ totalListings }} 条</span
>
<div class="pagination-controls">
<span class="pagination-size-label">每页</span>
<el-select
:model-value="pageSize"
class="page-size-select"
size="small"
@update:model-value="handlePageSizeChange"
>
<el-option :value="10" label="10条" />
<el-option :value="20" label="20条" />
<el-option :value="50" label="50条" />
<el-option :value="100" label="100条" />
</el-select>
<span class="pagination-page">{{ currentPage }}/{{ totalPages }}页</span>
<el-button size="small" :disabled="currentPage <= 1" @click="prevPage">上页</el-button>
<el-button size="small" :disabled="currentPage >= totalPages" @click="nextPage"
>下页</el-button
>
</div>
</div>
<AdminTablePagination
v-if="totalListings > 0"
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:total="totalListings"
:loading="loading"
@page-change="loadListings"
/>
</section>
</template>
@@ -12,31 +12,6 @@
background: linear-gradient(180deg, #fff8f3 0%, #ffffff 82%);
}
.hero-top-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 14px;
}
.hero-title {
display: grid;
gap: 3px;
}
.app-title {
font-size: 17px;
font-weight: 900;
color: #111827;
letter-spacing: 0;
}
.hero-title small {
color: #8b95a5;
font-size: 10px;
font-weight: 600;
}
.hero-setting-btn {
display: grid;
width: 34px;
@@ -953,6 +928,528 @@
min-width: 132px;
}
/* ========== Compact mobile account center ========== */
.profile-hero {
position: relative;
padding: 15px 16px 14px;
border: 0;
border-radius: 0 0 18px 18px;
background: linear-gradient(165deg, #fff8f1 0%, #ffffff 76%);
box-shadow: 0 5px 18px rgba(154, 89, 25, 0.045);
}
.hero-setting-btn {
position: absolute;
top: 14px;
right: 16px;
z-index: 1;
width: 32px;
height: 32px;
border-color: #f2dfd0;
}
.profile-hero-content {
padding-right: 38px;
}
.avatar-circle {
width: 62px;
height: 62px;
border: 2px solid #ffffff;
box-shadow:
0 0 0 1px #f0d8c6,
0 5px 12px rgba(185, 104, 34, 0.1);
}
.user-nickname {
font-size: 18px;
}
.user-meta {
margin-top: 4px;
font-size: 11px;
}
.owner-quality-tag {
border-color: #edcd91;
background: #fff3da !important;
color: #995b08 !important;
}
.hero-status-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-top: 13px;
padding-top: 11px;
border-top: 1px solid #f4e9df;
}
.hero-level-pill,
.hero-benefits-btn {
display: inline-flex;
align-items: center;
min-height: 28px;
border: 0;
background: transparent;
}
.hero-level-pill {
min-width: 0;
gap: 5px;
padding: 0 9px;
border: 1px solid #f1dccb;
border-radius: 999px;
background: rgba(255, 255, 255, 0.82);
}
.hero-level-pill span {
color: #8d7c70;
font-size: 9px;
}
.hero-level-pill strong {
overflow: hidden;
color: #b65308;
font-size: 12px;
text-overflow: ellipsis;
white-space: nowrap;
}
.hero-level-pill .van-icon {
flex: 0 0 auto;
color: #b98e6d;
}
.hero-level-pill.is-store {
border-color: #ccebd9;
background: #f3fbf6;
}
.hero-level-pill.is-store strong {
color: #13864c;
}
.hero-benefits-btn {
flex: 0 0 auto;
gap: 2px;
padding: 0;
color: #7e8794;
font-size: 10px;
font-weight: 700;
}
.asset-overview-card {
display: grid;
grid-template-columns: minmax(0, 0.95fr) minmax(0, 1.05fr);
margin: 12px 16px 0;
overflow: hidden;
border: 1px solid #eceff3;
border-radius: 14px;
background: #ffffff;
box-shadow: 0 4px 14px rgba(15, 23, 42, 0.03);
}
.asset-points {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: center;
gap: 9px;
min-width: 0;
padding: 13px 11px;
border: 0;
border-right: 1px solid #f0f2f5;
background: linear-gradient(135deg, #fff9f3 0%, #ffffff 88%);
text-align: left;
}
.asset-icon {
display: grid;
width: 34px;
height: 34px;
flex: 0 0 auto;
place-items: center;
border-radius: 10px;
}
.points-icon {
background: #ffeadc;
color: #ef6200;
}
.asset-copy {
display: grid;
min-width: 0;
gap: 1px;
}
.asset-copy small {
color: #8b95a3;
font-size: 9px;
font-weight: 700;
}
.asset-copy strong {
color: #252d3b;
font-size: 18px;
line-height: 1.15;
}
.asset-copy em {
overflow: hidden;
color: #9aa2ae;
font-size: 8px;
font-style: normal;
text-overflow: ellipsis;
white-space: nowrap;
}
.asset-arrow {
color: #b9aea3;
}
.asset-wallet {
display: grid;
align-content: center;
gap: 6px;
min-width: 0;
padding: 12px 13px;
background: #ffffff;
}
.asset-wallet-head,
.asset-wallet-main {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.asset-wallet-head > span {
color: #8b95a3;
font-size: 9px;
font-weight: 700;
}
.asset-wallet-head button {
padding: 0;
border: 0;
background: transparent;
color: #b27035;
font-size: 9px;
font-weight: 700;
}
.asset-wallet-main > strong {
min-width: 0;
overflow: hidden;
color: #222a37;
font-size: 19px;
line-height: 1.2;
text-overflow: ellipsis;
white-space: nowrap;
}
.asset-withdraw {
flex: 0 0 auto;
min-height: 26px;
padding: 0 9px;
border: 1px solid #f3c8aa;
border-radius: 999px;
background: #fff8f2;
color: #d95b00;
font-size: 10px;
font-weight: 800;
}
.quality-entry-card {
display: grid;
gap: 9px;
margin: 12px 16px 0;
padding: 12px 13px;
border: 1px solid #eee2d3;
border-radius: 14px;
background: linear-gradient(135deg, #fffaf4 0%, #ffffff 74%);
box-shadow: 0 4px 14px rgba(134, 85, 34, 0.025);
}
.quality-entry-card.is-active {
border-color: #e6c17c;
background: linear-gradient(135deg, #fff8e9 0%, #ffffff 74%);
}
.quality-entry-head,
.quality-entry-title,
.quality-entry-actions,
.quality-entry-active {
display: flex;
align-items: center;
}
.quality-entry-head {
justify-content: space-between;
gap: 10px;
}
.quality-entry-title {
min-width: 0;
gap: 9px;
}
.quality-entry-icon {
display: grid;
width: 30px;
height: 30px;
flex: 0 0 auto;
place-items: center;
border: 1px solid #efd7b9;
border-radius: 9px;
background: #fff1df;
color: #cf670c;
}
.quality-entry-copy {
display: grid;
min-width: 0;
gap: 1px;
}
.quality-entry-copy span {
color: #8c7c6e;
font-size: 9px;
font-weight: 700;
}
.quality-entry-copy strong {
overflow: hidden;
color: #2d3340;
font-size: 13px;
text-overflow: ellipsis;
white-space: nowrap;
}
.quality-entry-card :deep(.van-tag) {
flex: 0 0 auto;
font-size: 9px;
}
.quality-entry-summary {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 6px 10px;
padding-top: 8px;
border-top: 1px solid #f3eadf;
}
.quality-entry-summary span {
color: #877a6e;
font-size: 9px;
}
.quality-entry-summary button {
display: inline-flex;
align-items: center;
gap: 2px;
margin-left: auto;
padding: 0;
border: 0;
background: transparent;
color: #bd681b;
font-size: 9px;
font-weight: 700;
}
.quality-entry-actions,
.quality-entry-active {
justify-content: space-between;
gap: 8px;
padding-top: 8px;
border-top: 1px solid #f3eadf;
}
.quality-entry-actions > span,
.quality-entry-active > span {
min-width: 0;
color: #8a8178;
font-size: 9px;
line-height: 1.45;
}
.quality-entry-actions > div {
display: flex;
flex: 0 0 auto;
gap: 5px;
}
.quality-entry-actions :deep(.van-button) {
height: 28px;
padding: 0 9px;
font-size: 9px;
font-weight: 800;
}
.quality-cancel-link {
flex: 0 0 auto;
min-height: 26px;
padding: 0 8px;
border: 1px solid #f4c4cb;
border-radius: 999px;
background: #fff7f8;
color: #c63d50;
font-size: 9px;
font-weight: 800;
}
.quality-cancel-link:disabled {
opacity: 0.45;
}
.service-panel,
.more-services-card {
margin: 12px 16px 0;
border: 1px solid #eceff3;
border-radius: 14px;
background: #ffffff;
box-shadow: 0 4px 14px rgba(15, 23, 42, 0.03);
}
.service-panel {
padding: 13px 14px;
}
.service-section-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-bottom: 10px;
}
.service-section-head h3 {
margin: 0;
color: #202733;
font-size: 13px;
}
.service-section-head button {
display: inline-flex;
align-items: center;
gap: 1px;
padding: 0;
border: 0;
background: transparent;
color: #8a929f;
font-size: 9px;
}
.service-grid {
display: grid;
align-items: start;
}
.service-grid.five {
grid-template-columns: repeat(5, minmax(0, 1fr));
}
.service-grid.three {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.service-item {
display: grid;
justify-items: center;
gap: 5px;
min-width: 0;
}
.service-item:active .service-icon {
transform: scale(0.92);
}
.service-icon {
display: grid;
width: 36px;
height: 36px;
place-items: center;
border-radius: 10px;
transition: transform 0.15s ease;
}
.service-icon.warning {
background: #fff5de;
color: #d89000;
}
.service-icon.info {
background: #edf4ff;
color: #3178d9;
}
.service-icon.primary {
background: #eef4ff;
color: #1d6dd1;
}
.service-icon.success {
background: #ebf9f3;
color: #159a6b;
}
.service-icon.orange {
background: #fff0e7;
color: #e96508;
}
.service-icon.gold {
background: #fff6e6;
color: #c78a21;
}
.service-icon.teal {
background: #e9f8f7;
color: #13938d;
}
.service-item > span {
overflow: hidden;
max-width: 100%;
color: #4a5361;
font-size: 10px;
font-weight: 600;
text-overflow: ellipsis;
white-space: nowrap;
}
.service-divider {
height: 1px;
margin: 13px 0;
background: #f0f2f5;
}
.more-services-card {
overflow: hidden;
margin-bottom: 14px;
}
.more-services-title {
padding: 13px 14px 8px;
color: #4e5867;
font-size: 11px;
font-weight: 800;
}
.more-services-card :deep(.van-cell) {
padding: 11px 14px;
font-size: 13px;
}
.more-services-card :deep(.van-cell__left-icon) {
margin-right: 9px;
color: #9a7454;
}
/* ========== 响应式适配 ========== */
@media (min-width: 520px) {
.mobile-profile-shell {
@@ -139,6 +139,7 @@ const settingsGroups = [
title: '账号与安全',
items: [
{ label: '资料更改', icon: 'edit', action: 'profile' },
{ label: '修改密码', icon: 'lock', action: 'password' },
{ label: '实名认证', icon: 'idcard', action: 'realname' },
{ label: '注销账号', icon: 'delete-o', action: 'cancel-account' },
],
@@ -328,6 +329,9 @@ function onSettingClick(action: string) {
case 'profile':
openProfileEditor()
break
case 'password':
openPasswordEditor()
break
case 'realname':
router.push('/m/realname')
break
@@ -464,15 +468,14 @@ function resolveAvatarURL(url: string | undefined | null) {
<template v-if="isLoggedIn">
<!-- Hero 顶部账户概览 -->
<section class="profile-hero">
<div class="hero-top-row">
<div class="hero-title">
<span class="app-title">个人中心</span>
<small>账户、成长与权益管理</small>
</div>
<button class="hero-setting-btn" @click="showSettings = true">
<van-icon name="setting-o" :size="20" color="#374151" />
</button>
</div>
<button
class="hero-setting-btn"
type="button"
aria-label="打开设置"
@click="showSettings = true"
>
<van-icon name="setting-o" :size="20" color="#374151" />
</button>
<div class="profile-hero-content">
<div class="avatar-wrap">
<div class="avatar-circle">
@@ -518,200 +521,196 @@ function resolveAvatarURL(url: string | undefined | null) {
<p class="user-meta">ID: {{ displayId }} · {{ maskedPhone }}</p>
</div>
</div>
<div class="hero-status-row">
<button
v-if="!siteStore.isDistribution"
type="button"
class="hero-level-pill"
@click="router.push('/m/benefits')"
>
<span>成长等级</span>
<strong>{{ session.renterGrowthLevelName }}</strong>
<van-icon name="arrow" :size="10" />
</button>
<div v-else class="hero-level-pill is-store">
<span>所属商城</span>
<strong>{{ siteStore.siteName || '官方商城' }}</strong>
</div>
<button type="button" class="hero-benefits-btn" @click="router.push('/m/benefits')">
VIP 与权益
<van-icon name="arrow" :size="10" />
</button>
</div>
</section>
<section v-if="siteStore.isDistribution" class="growth-card is-distribution">
<div>
<span>所属商城</span>
<strong>{{ siteStore.siteName || '官方商城' }}</strong>
</div>
<div>
<span>会员特权</span>
<strong>渠道直租专享</strong>
</div>
</section>
<section v-else class="growth-card">
<div>
<span>成长等级</span>
<strong>{{ session.renterGrowthLevelName }}</strong>
</div>
<div>
<span>累计积分</span>
<strong>{{ session.renterGrowthPoints }}</strong>
</div>
<button type="button" class="growth-more" @click="router.push('/m/benefits')">
<span>查看 VIP 与优质号主权益</span>
<van-icon name="arrow" :size="11" />
<section class="asset-overview-card">
<button type="button" class="asset-points" @click="router.push('/m/points-mall')">
<span class="asset-icon points-icon">
<van-icon name="point-gift-o" :size="20" />
</span>
<span class="asset-copy">
<small>可用积分</small>
<strong>{{ session.pointsAvailable }}</strong>
<em>累计 {{ session.renterGrowthPoints }}</em>
</span>
<van-icon name="arrow" :size="10" class="asset-arrow" />
</button>
<div class="asset-wallet">
<div class="asset-wallet-head">
<span>账户余额</span>
<button type="button" @click="openLedgers">明细</button>
</div>
<div class="asset-wallet-main">
<strong>¥{{ formatCent(availableBalanceCent) }}</strong>
<button type="button" class="asset-withdraw" @click="handleWithdraw">提现</button>
</div>
</div>
</section>
<section class="quality-owner-card" :class="{ 'is-active': qualityOwnerStatus?.active }">
<div class="quality-owner-heading">
<div class="quality-owner-title">
<span class="quality-owner-icon">
<van-icon name="award-o" :size="18" />
<section class="quality-entry-card" :class="{ 'is-active': qualityOwnerStatus?.active }">
<div class="quality-entry-head">
<div class="quality-entry-title">
<span class="quality-entry-icon">
<van-icon name="award-o" :size="16" />
</span>
<div>
<div class="quality-entry-copy">
<span>优质号主</span>
<strong>{{ qualityOwnerStatus?.active ? '已开通' : '解锁极品专区发布权限' }}</strong>
<strong>{{
qualityOwnerStatus?.active ? '已开通极品专区权益' : '解锁极品专区发布权限'
}}</strong>
</div>
</div>
<van-tag :type="qualityOwnerStatus?.active ? 'success' : 'default'" round>
{{ qualityOwnerStatus?.active ? '优质号主' : '未开通' }}
{{ qualityOwnerStatus?.active ? '已开通' : '未开通' }}
</van-tag>
</div>
<div class="quality-owner-info">
<div>
<span>保证金</span>
<strong>¥{{ formatCent(qualityOwnerStatus?.required_deposit_cent || 20000) }}</strong>
</div>
<div>
<span>极品专区保险</span>
<strong
>{{
((qualityOwnerStatus?.premium_insurance_fee_bps || 400) / 100).toFixed(1)
}}%</strong
>
</div>
</div>
<div v-if="qualityOwnerStatus?.active" class="quality-owner-footer">
<p>
<template v-if="qualityOwnerStatus.active_premium_listing_count > 0">
极品专区还有 {{ qualityOwnerStatus.active_premium_listing_count }}
个进行中账号,需处理后可取消。
</template>
<template v-else-if="qualityOwnerStatus.deposit_source === 'manual'">
已由后台开通优质号主,可随时取消。
</template>
<template v-else>保证金已冻结,取消后退回可用余额。</template>
</p>
<van-button
size="small"
round
plain
type="danger"
class="quality-cancel-btn"
:loading="qualityOwnerCancelling"
:disabled="qualityOwnerStatus.active_premium_listing_count > 0"
@click="handleCancelQualityOwner"
>
取消资格
</van-button>
</div>
<div v-else class="quality-owner-actions">
<van-button
block
round
type="primary"
color="#7c3aed"
:loading="qualityOwnerLoading"
:disabled="
(qualityOwnerStatus?.available_balance_cent || 0) <
(qualityOwnerStatus?.required_deposit_cent || 20000)
"
@click="handleActivateQualityOwner"
>
余额支付开通
</van-button>
<van-button
block
round
plain
type="primary"
:loading="qualityOwnerOnlineLoading"
@click="handleOnlineQualityOwnerPayment"
>
微信 / 支付宝支付开通
</van-button>
<p>开通后可发布到极品专区,并享受该专区专享保险费率。</p>
</div>
</section>
<section class="mobile-points-entry" @click="router.push('/m/points-mall')">
<div class="points-entry-left">
<span class="points-entry-icon">
<van-icon name="point-gift-o" :size="22" color="#ff6a00" />
<div class="quality-entry-summary">
<span>保证金 ¥{{ formatCent(qualityOwnerStatus?.required_deposit_cent || 20000) }}</span>
<span>
专区保险
{{ ((qualityOwnerStatus?.premium_insurance_fee_bps || 400) / 100).toFixed(1) }}%
</span>
<div class="points-entry-copy">
<strong>{{ session.pointsAvailable }} 可用积分</strong>
<small>积分商城 · {{ session.renterGrowthLevelName }}等级</small>
</div>
</div>
<van-icon name="arrow" />
</section>
<!-- 余额卡片 - 银行卡质感暗色卡片 -->
<div class="balance-card">
<div class="balance-row">
<div class="balance-info">
<span class="balance-label">账户可用余额</span>
<strong class="balance-value">¥{{ formatCent(availableBalanceCent) }}</strong>
</div>
<button class="withdraw-btn" @click="handleWithdraw">提现</button>
</div>
<div class="balance-footer">
<button class="detail-btn" @click="openLedgers">
查看收支明细 <van-icon name="arrow" :size="10" />
<button type="button" @click="router.push('/m/benefits')">
权益说明 <van-icon name="arrow" :size="9" />
</button>
</div>
</div>
<!-- 买家服务面板 -->
<div class="menu-card">
<div class="card-header">
<h3>买家服务</h3>
<span class="see-all" @click="goOrders('all')">
全部订单 <van-icon name="arrow" :size="10" />
<div v-if="qualityOwnerStatus?.active" class="quality-entry-active">
<span>
<template v-if="qualityOwnerStatus.active_premium_listing_count > 0">
还有 {{ qualityOwnerStatus.active_premium_listing_count }} 个进行中专区账号
</template>
<template v-else-if="qualityOwnerStatus.deposit_source === 'manual'">
当前资格由后台开通
</template>
<template v-else>保证金已冻结,取消后自动退回余额</template>
</span>
<button
type="button"
class="quality-cancel-link"
:disabled="
qualityOwnerCancelling || qualityOwnerStatus.active_premium_listing_count > 0
"
@click="handleCancelQualityOwner"
>
{{ qualityOwnerCancelling ? '取消中...' : '取消资格' }}
</button>
</div>
<div class="grid-menu col-5">
<div class="grid-item" @click="goOrders('pending_payment')">
<div class="icon-wrap warning"><van-icon name="bill-o" :size="22" /></div>
<span>待支付</span>
</div>
<div class="grid-item" @click="goOrders('pending_handoff')">
<div class="icon-wrap info"><van-icon name="logistics" :size="22" /></div>
<span>待交接</span>
</div>
<div class="grid-item" @click="goOrders('renting')">
<div class="icon-wrap primary"><van-icon name="play-circle-o" :size="22" /></div>
<span>使用中</span>
</div>
<div class="grid-item" @click="goOrders('completed')">
<div class="icon-wrap success"><van-icon name="smile-o" :size="22" /></div>
<span>已完成</span>
</div>
<div class="grid-item" @click="router.push('/m/listings/collections')">
<div class="icon-wrap orange"><van-icon name="star-o" :size="22" /></div>
<span>我的足迹</span>
</div>
</div>
</div>
<!-- 卖家服务面板 -->
<div class="menu-card">
<div class="card-header">
<h3>卖家服务</h3>
</div>
<div class="grid-menu col-3">
<div class="grid-item" @click="router.push('/m/seller/listings/create')">
<div class="icon-wrap orange"><van-icon name="plus" :size="22" /></div>
<span>发布商品</span>
</div>
<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>
<div class="grid-item" @click="router.push('/m/wallet/withdrawal')">
<div class="icon-wrap teal"><van-icon name="balance-o" :size="22" /></div>
<span>提现/账单</span>
<div v-else class="quality-entry-actions">
<span> 可用余额 ¥{{ formatCent(qualityOwnerStatus?.available_balance_cent || 0) }} </span>
<div>
<van-button
size="small"
round
type="primary"
color="#ff6a00"
:loading="qualityOwnerLoading"
:disabled="
(qualityOwnerStatus?.available_balance_cent || 0) <
(qualityOwnerStatus?.required_deposit_cent || 20000)
"
@click="handleActivateQualityOwner"
>
余额开通
</van-button>
<van-button
size="small"
round
plain
type="primary"
color="#a45b16"
:loading="qualityOwnerOnlineLoading"
@click="handleOnlineQualityOwnerPayment"
>
在线支付
</van-button>
</div>
</div>
</div>
</section>
<!-- 消息与公告 -->
<div class="menu-card cell-card">
<div class="cell-card-title">消息与公告</div>
<section class="service-panel">
<div class="service-section">
<div class="service-section-head">
<h3>买家服务</h3>
<button type="button" @click="goOrders('all')">
全部订单 <van-icon name="arrow" :size="10" />
</button>
</div>
<div class="service-grid five">
<div class="service-item" @click="goOrders('pending_payment')">
<div class="service-icon warning"><van-icon name="bill-o" :size="20" /></div>
<span>待支付</span>
</div>
<div class="service-item" @click="goOrders('pending_handoff')">
<div class="service-icon info"><van-icon name="logistics" :size="20" /></div>
<span>待交接</span>
</div>
<div class="service-item" @click="goOrders('renting')">
<div class="service-icon primary"><van-icon name="play-circle-o" :size="20" /></div>
<span>使用中</span>
</div>
<div class="service-item" @click="goOrders('completed')">
<div class="service-icon success"><van-icon name="smile-o" :size="20" /></div>
<span>已完成</span>
</div>
<div class="service-item" @click="router.push('/m/listings/collections')">
<div class="service-icon orange"><van-icon name="star-o" :size="20" /></div>
<span>我的足迹</span>
</div>
</div>
</div>
<div class="service-divider"></div>
<div class="service-section">
<div class="service-section-head">
<h3>卖家服务</h3>
<button type="button" @click="router.push('/m/seller/listings')">
商品管理 <van-icon name="arrow" :size="10" />
</button>
</div>
<div class="service-grid three">
<div class="service-item" @click="router.push('/m/seller/listings/create')">
<div class="service-icon orange"><van-icon name="plus" :size="20" /></div>
<span>发布商品</span>
</div>
<div class="service-item" @click="router.push('/m/seller/listings')">
<div class="service-icon gold"><van-icon name="shop-o" :size="20" /></div>
<span>我的商品</span>
</div>
<div class="service-item" @click="router.push('/m/wallet/withdrawal')">
<div class="service-icon teal"><van-icon name="balance-o" :size="20" /></div>
<span>提现/账单</span>
</div>
</div>
</div>
</section>
<section class="more-services-card">
<div class="more-services-title">消息、账户与服务</div>
<van-cell-group :border="false">
<van-cell title="消息中心" icon="chat-o" is-link to="/m/messages" />
<van-cell title="站内信" icon="envelop-o" is-link to="/m/notifications">
@@ -722,22 +721,6 @@ function resolveAvatarURL(url: string | undefined | null) {
</template>
</van-cell>
<van-cell title="公告中心" icon="volume-o" is-link to="/m/announcements" />
</van-cell-group>
</div>
<!-- 账户与常用服务 -->
<div class="menu-card cell-card">
<div class="cell-card-title">账户与服务</div>
<van-cell-group :border="false">
<van-cell title="资料修改" icon="edit" is-link @click="openProfileEditor" />
<van-cell title="修改密码" icon="lock" is-link @click="openPasswordEditor" />
<van-cell title="实名认证" icon="idcard" is-link to="/m/realname">
<template #value>
<span :class="isRealnameVerified ? 'verified-color' : 'unverified-color'">
{{ isRealnameVerified ? '已认证' : isRealnamePending ? '审核中' : '未认证' }}
</span>
</template>
</van-cell>
<van-cell
title="邀请返利"
icon="gold-coin-o"
@@ -745,11 +728,10 @@ function resolveAvatarURL(url: string | undefined | null) {
to="/m/invitations"
:value="inviteRateText || '阶梯返利'"
/>
<van-cell title="权益中心" icon="medal-o" is-link to="/m/benefits" />
<van-cell title="租后须知" icon="info-o" is-link @click="openPostRentalNotice" />
<van-cell title="系统设置" icon="setting-o" is-link @click="showSettings = true" />
<van-cell title="资料与安全" icon="setting-o" is-link @click="showSettings = true" />
</van-cell-group>
</div>
</section>
</template>
<MobilePayWaySelectPopup
+204 -103
View File
@@ -727,94 +727,71 @@ h1 {
.listing-control-panel {
display: flex;
flex-direction: column;
gap: 16px;
gap: 14px;
margin-bottom: 16px;
border-radius: 12px;
border: 1px solid #e7ebf1;
border-radius: 8px;
background: #ffffff;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
padding: 16px 18px 18px;
box-shadow: none;
}
/* 统计卡片行 */
.listing-stats-row {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
}
.stat-card {
.listing-filter-toolbar {
display: flex;
flex-direction: column;
gap: 8px;
padding: 16px 18px;
border: 1px solid #e8edf4;
border-radius: 10px;
background: #ffffff;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
transition: all 0.2s ease;
align-items: center;
justify-content: space-between;
gap: 16px;
padding-bottom: 14px;
border-bottom: 1px solid #eef1f5;
}
.stat-card.clickable {
cursor: pointer;
.listing-filter-summary {
display: flex;
align-items: baseline;
gap: 10px;
min-width: 0;
}
.stat-card.clickable:hover {
border-color: #1477ff;
box-shadow: 0 2px 8px rgba(20, 119, 255, 0.15);
transform: translateY(-1px);
}
.stat-card.active {
border-color: #1477ff;
background: #1477ff;
box-shadow: 0 2px 8px rgba(20, 119, 255, 0.25);
}
.stat-card .stat-label {
color: #6b7785;
font-size: 13px;
font-weight: 600;
line-height: 1;
}
.stat-card.active .stat-label {
color: #ffffff;
}
.stat-card .stat-value {
color: #17233d;
font-size: 28px;
.listing-filter-summary strong {
color: #1f2a3d;
font-size: 15px;
font-weight: 800;
line-height: 1;
letter-spacing: -0.5px;
}
.stat-card.active .stat-value {
color: #ffffff;
.listing-filter-summary span {
color: #8b95a4;
font-size: 12px;
}
.listing-filter-summary b {
color: #2d5bd1;
font-weight: 800;
}
/* 筛选和操作行 */
.listing-filter-row {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: end;
gap: 16px 24px;
padding-top: 16px;
border-top: 1px solid #f0f3f7;
min-width: 0;
}
.listing-filter-bar {
display: grid;
grid-template-columns: minmax(220px, 1.2fr) minmax(150px, 0.78fr) minmax(220px, 1fr) minmax(
220px,
1fr
);
grid-template-columns: repeat(8, minmax(0, 1fr));
align-items: end;
gap: 12px 16px;
gap: 12px;
margin: 0;
min-width: 0;
}
.listing-filter-bar .filter-keyword {
grid-column: span 3;
}
.listing-filter-bar .filter-range,
.listing-filter-bar .filter-asset,
.listing-filter-bar .filter-skin {
grid-column: span 2;
}
.listing-filter-bar .el-form-item {
margin-bottom: 0;
min-width: 0;
@@ -879,7 +856,6 @@ h1 {
align-items: center;
gap: 8px;
flex-shrink: 0;
padding-bottom: 0;
white-space: nowrap;
}
@@ -1212,98 +1188,223 @@ h1 {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
border: none;
background: #ffffff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.table-pagination {
justify-content: flex-end;
margin-top: 12px;
border-radius: 12px;
padding: 12px 16px;
flex-wrap: wrap;
gap: 10px 18px;
box-sizing: border-box;
width: 100%;
margin-top: 12px;
padding: 10px 14px;
border: 1px solid #e7ebf1;
border-radius: 8px;
background: #ffffff;
box-shadow: none;
color: #66728f;
font-size: 12px;
font-weight: 600;
}
.pagination-summary {
display: flex;
align-items: center;
gap: 7px;
min-width: 0;
white-space: nowrap;
}
.pagination-summary strong {
color: #263247;
font-size: 13px;
font-weight: 800;
}
.pagination-summary em {
padding-left: 8px;
border-left: 1px solid #e8ecf2;
color: #99a2af;
font-size: 11px;
font-style: normal;
}
.pagination-controls {
display: flex;
align-items: center;
gap: 6px;
flex-shrink: 0;
white-space: nowrap;
flex-wrap: wrap;
gap: 8px;
margin-left: auto;
}
.pagination-summary,
.pagination-page,
.pagination-size-label {
flex-shrink: 0;
white-space: nowrap;
}
.page-size-select {
width: 86px;
flex: 0 0 86px;
width: 82px;
flex: 0 0 82px;
}
.page-size-select .el-select__wrapper {
min-height: 30px;
padding: 4px 8px;
min-height: 28px;
border-radius: 5px;
padding: 2px 7px;
}
.page-size-select .el-select__selected-item {
min-width: max-content;
}
.admin-listings-page .table-pagination {
margin-top: auto;
}
.pagination-controls .el-button--small {
min-width: 44px;
padding: 5px 8px;
.pagination-nav {
display: flex;
align-items: center;
gap: 4px;
}
.pagination-controls .el-button + .el-button {
margin-left: 0;
}
.pagination-nav-btn.el-button {
width: 28px;
min-width: 28px;
height: 28px;
padding: 0;
border-color: #e2e7ee;
border-radius: 5px;
color: #66728a;
}
.pagination-nav-btn.el-button:hover:not(.is-disabled) {
border-color: #9db6ed;
background: #f4f8ff;
color: #2c61d5;
}
.pagination-page {
display: inline-flex;
align-items: baseline;
justify-content: center;
gap: 3px;
min-width: 56px;
padding: 0 6px;
color: #8d97a6;
font-size: 11px;
white-space: nowrap;
}
.pagination-page strong {
color: #2b5fd2;
font-size: 13px;
font-weight: 800;
}
.pagination-page i {
color: #c0c6cf;
font-style: normal;
}
.pagination-jump {
display: flex;
align-items: center;
gap: 5px;
padding-left: 8px;
border-left: 1px solid #e8ecf2;
color: #818b9a;
white-space: nowrap;
}
.pagination-jump-input {
width: 52px;
}
.pagination-jump-input .el-input__wrapper {
min-height: 28px;
padding: 2px 6px;
border-radius: 5px;
}
.pagination-jump-input .el-input__inner {
text-align: center;
}
.pagination-jump-btn.el-button {
height: 28px;
padding: 0 9px;
border-color: #dce3ec;
border-radius: 5px;
color: #3f5f97;
font-weight: 700;
}
.admin-listings-page .table-pagination {
margin-top: auto;
}
@media (max-width: 720px) {
.table-pagination {
align-items: flex-start;
flex-direction: column;
}
.pagination-controls {
justify-content: flex-end;
width: 100%;
margin-left: 0;
}
.pagination-jump {
padding-left: 0;
border-left: 0;
}
}
@media (max-width: 1280px) {
.listing-filter-row {
grid-template-columns: 1fr;
.listing-filter-toolbar {
align-items: flex-start;
flex-wrap: wrap;
}
.listing-filter-bar {
grid-template-columns: repeat(2, minmax(180px, 1fr));
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.listing-action-strip {
justify-content: flex-end;
flex-wrap: wrap;
}
}
@media (max-width: 980px) {
.listing-control-panel {
grid-template-columns: 1fr;
align-items: stretch;
.listing-filter-bar {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.listing-filter-row {
grid-template-columns: 1fr;
.listing-filter-bar .filter-keyword {
grid-column: span 2;
}
.listing-action-strip {
justify-content: flex-start;
}
}
@media (max-width: 640px) {
.listing-control-panel {
padding: 14px;
}
.listing-filter-summary {
display: grid;
gap: 3px;
}
.listing-filter-bar {
grid-template-columns: 1fr;
}
.listing-action-strip {
justify-content: flex-start;
flex-wrap: wrap;
.listing-filter-bar .filter-keyword,
.listing-filter-bar .filter-range,
.listing-filter-bar .filter-asset,
.listing-filter-bar .filter-skin {
grid-column: span 1;
}
}