Files
hfb_sys/frontend/src/features/admin/views/AdminDashboardView.vue
T

1561 lines
40 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 { computed } from 'vue'
import {
Bell,
Bottom,
ChatDotRound,
DataLine,
Document,
Grid,
Loading,
Operation,
Refresh,
ScaleToOriginal,
ShoppingBag,
Shop,
Tickets,
Top,
User,
Van,
Wallet,
Warning,
} from '@element-plus/icons-vue'
import {
fetchAdminDashboard,
type AdminDashboard,
type ListingChannelDayStats,
type ListingDayStats,
type ListingUploaderDayStats,
} from '@/features/admin/api/adminDashboard'
import { useAdminTable } from '@/features/admin/composables/useAdminTable'
import { formatCentWithSymbol } from '@/shared/utils/money'
import { adminPath } from '@/shared/utils/adminPath'
import { disputeStatusLabel, orderStatusLabel } from '@/shared/utils/statusLabels'
import { formatDateTime } from '@/shared/utils/time'
const {
loading,
error,
data: dashboard,
load: loadDashboard,
} = useAdminTable<AdminDashboard>({
fetchFn: fetchAdminDashboard,
})
const listingTrend = computed(() => dashboard.value?.listing_daily?.trend ?? [])
const listingChannelTrend = computed(() => dashboard.value?.listing_daily?.channel_trend ?? [])
const listingUploaderTrend = computed(() => dashboard.value?.listing_daily?.uploader_trend ?? [])
const listingToday = computed(
() =>
dashboard.value?.listing_daily?.today ?? {
date: '',
published_count: 0,
active_offline_count: 0,
trade_leave_count: 0,
}
)
const channelDisplayOrder = ['咸鱼', '淘宝', '京东', 'QQ', '微信', '自定义', '站内发布', '未填写']
const listingTodayChannels = computed<ListingChannelDayStats[]>(() => {
const date = listingToday.value.date
const rows = dashboard.value?.listing_daily?.today_channels ?? []
const byChannel = new Map(rows.map(row => [row.source_channel || '未填写', row]))
const ordered = channelDisplayOrder.map(channel => ({
date,
source_channel: channel,
published_count: byChannel.get(channel)?.published_count ?? 0,
active_offline_count: byChannel.get(channel)?.active_offline_count ?? 0,
trade_leave_count: byChannel.get(channel)?.trade_leave_count ?? 0,
}))
const extras = rows.filter(row => !channelDisplayOrder.includes(row.source_channel || '未填写'))
return [...ordered, ...extras]
})
const listingTodayUploaders = computed<ListingUploaderDayStats[]>(() => {
const date = listingToday.value.date
return (dashboard.value?.listing_daily?.today_uploaders ?? [])
.filter(row => row.date === date)
.sort(
(a, b) => b.upload_count - a.upload_count || a.uploader_name.localeCompare(b.uploader_name)
)
})
function leaveCount(row: ListingChannelDayStats | ListingDayStats) {
return Number(row.active_offline_count || 0) + Number(row.trade_leave_count || 0)
}
function trendMax(getter: (row: ListingDayStats) => number) {
const values = listingTrend.value.map(getter)
return Math.max(1, ...values, 0)
}
function barHeight(value: number, max: number) {
if (max <= 0) return '8%'
return `${Math.max(8, Math.round((value / max) * 100))}%`
}
function shortDate(date: string) {
if (!date || date.length < 10) return date
return date.slice(5)
}
</script>
<template>
<div class="finance-page-container">
<!-- 头部卡片 -->
<header class="page-header-card">
<div class="header-main">
<div class="title-group">
<div class="eyebrow-tag">
<el-icon><DataLine /></el-icon>
<span>DASHBOARD</span>
</div>
<h1 class="page-title">后台仪表盘</h1>
<p class="page-subtitle">实时监控平台运营核心数据,洞察商品流转趋势与待办事项。</p>
</div>
<div class="toolbar-actions">
<el-button :icon="Refresh" :loading="loading" @click="loadDashboard">
刷新数据
</el-button>
</div>
</div>
</header>
<!-- 加载与错误状态 -->
<div v-if="loading && !dashboard" class="state-card loading-state">
<el-icon class="is-loading"><Loading /></el-icon>
<span>加载仪表盘数据中...</span>
</div>
<div v-else-if="error && !dashboard" class="state-card error-state">
<el-icon><Warning /></el-icon>
<span>{{ error }}</span>
<el-button type="primary" @click="loadDashboard">重试</el-button>
</div>
<!-- 核心指标卡片 -->
<section v-if="dashboard" class="summary-metric-grid">
<div class="metric-card metric-card--users">
<div class="metric-icon-wrap indigo">
<el-icon><User /></el-icon>
</div>
<div class="metric-info">
<span class="metric-label">用户总数</span>
<strong class="metric-val indigo-val">{{ dashboard.metrics.total_users }}</strong>
<span class="metric-sub-pill info">实名 {{ dashboard.metrics.verified_users }} 人</span>
</div>
</div>
<div class="metric-card metric-card--listings">
<div class="metric-icon-wrap emerald">
<el-icon><ShoppingBag /></el-icon>
</div>
<div class="metric-info">
<span class="metric-label">发布商品</span>
<strong class="metric-val emerald-val">{{ dashboard.metrics.total_listings }}</strong>
<span class="metric-sub-pill success"
>已上架 {{ dashboard.metrics.published_listings }} 个</span
>
</div>
</div>
<div class="metric-card metric-card--orders">
<div class="metric-icon-wrap amber">
<el-icon><Document /></el-icon>
</div>
<div class="metric-info">
<span class="metric-label">订单总数</span>
<strong class="metric-val amber-val">{{ dashboard.metrics.total_orders }}</strong>
<span class="metric-sub-pill warning"
>使用中 {{ dashboard.metrics.renting_orders }} 个</span
>
</div>
</div>
<div class="metric-card metric-card--flow">
<div class="metric-icon-wrap rose">
<el-icon><Wallet /></el-icon>
</div>
<div class="metric-info">
<span class="metric-label">今日流水</span>
<strong class="metric-val rose-val">
{{ formatCentWithSymbol(dashboard.metrics.today_ledger_amount_cent) }}
</strong>
<span class="metric-sub-pill danger"
>今日订单 {{ dashboard.metrics.today_orders }} 个</span
>
</div>
</div>
</section>
<!-- 账号上下架:今日三组 + 近 7 日趋势 -->
<section v-if="dashboard" class="section-card">
<div class="section-header-row">
<div class="section-header-title">
<el-icon class="header-icon"><Shop /></el-icon>
<span>账号上下架趋势</span>
<span class="header-badge"
>按北京时间自然日 · 近 {{ dashboard.listing_daily.days }} 日</span
>
</div>
</div>
<!-- 今日指标与柱状趋势 -->
<div class="listing-daily-grid">
<div class="listing-stat-card tone-publish">
<div class="listing-stat-head">
<div class="listing-stat-icon">
<el-icon><Top /></el-icon>
</div>
<div class="listing-stat-meta">
<span>今日上架</span>
<strong>{{ listingToday.published_count }}</strong>
</div>
</div>
<p class="listing-stat-hint">免审发布 / 审核通过 / 取消订单恢复上架</p>
<div class="mini-bars">
<div
v-for="row in listingTrend"
:key="`pub-${row.date}`"
class="mini-bar-col"
:title="`${row.date}:${row.published_count}`"
>
<div
class="mini-bar publish"
:style="{
height: barHeight(
row.published_count,
trendMax(r => r.published_count)
),
}"
/>
<em>{{ shortDate(row.date) }}</em>
<b>{{ row.published_count }}</b>
</div>
</div>
</div>
<div class="listing-stat-card tone-offline">
<div class="listing-stat-head">
<div class="listing-stat-icon">
<el-icon><Bottom /></el-icon>
</div>
<div class="listing-stat-meta">
<span>今日主动下架</span>
<strong>{{ listingToday.active_offline_count }}</strong>
</div>
</div>
<p class="listing-stat-hint">号主手动下架 + 后台管理下架</p>
<div class="mini-bars">
<div
v-for="row in listingTrend"
:key="`off-${row.date}`"
class="mini-bar-col"
:title="`${row.date}:${row.active_offline_count}`"
>
<div
class="mini-bar offline"
:style="{
height: barHeight(
row.active_offline_count,
trendMax(r => r.active_offline_count)
),
}"
/>
<em>{{ shortDate(row.date) }}</em>
<b>{{ row.active_offline_count }}</b>
</div>
</div>
</div>
<div class="listing-stat-card tone-trade">
<div class="listing-stat-head">
<div class="listing-stat-icon">
<el-icon><Van /></el-icon>
</div>
<div class="listing-stat-meta">
<span>今日交易离架</span>
<strong>{{ listingToday.trade_leave_count }}</strong>
</div>
</div>
<p class="listing-stat-hint">租出 / 售出完成 / 封存 / 订单仲裁归档</p>
<div class="mini-bars">
<div
v-for="row in listingTrend"
:key="`trade-${row.date}`"
class="mini-bar-col"
:title="`${row.date}:${row.trade_leave_count}`"
>
<div
class="mini-bar trade"
:style="{
height: barHeight(
row.trade_leave_count,
trendMax(r => r.trade_leave_count)
),
}"
/>
<em>{{ shortDate(row.date) }}</em>
<b>{{ row.trade_leave_count }}</b>
</div>
</div>
</div>
</div>
<!-- 渠道总览 -->
<div class="channel-summary-title">今日各渠道上下架汇总</div>
<div class="listing-channel-summary">
<div
v-for="row in listingTodayChannels"
:key="`today-channel-${row.source_channel}`"
class="listing-channel-item"
>
<div class="listing-channel-item-head">
<span class="channel-name">{{ row.source_channel || '未填写' }}</span>
<strong class="channel-total">{{ row.published_count + leaveCount(row) }}</strong>
</div>
<div class="listing-channel-counts">
<span class="count-tag publish">上架 {{ row.published_count }}</span>
<span class="count-tag offline">主动下架 {{ row.active_offline_count }}</span>
<span class="count-tag trade">交易离架 {{ row.trade_leave_count }}</span>
</div>
</div>
</div>
<div v-if="listingTodayUploaders.length" class="listing-uploader-block">
<div class="channel-summary-title">今日外部上传账号统计</div>
<div class="listing-uploader-summary">
<div
v-for="row in listingTodayUploaders"
:key="`today-uploader-${row.uploader_id}-${row.uploader_name}`"
class="listing-uploader-item"
>
<span class="uploader-name">{{ row.uploader_name || '未填写' }}</span>
<strong class="uploader-total">{{ row.upload_count }}</strong>
<span class="uploader-unit">个账号</span>
</div>
</div>
</div>
<!-- 7日上下架趋势明细表 -->
<div class="table-container trend-table-wrap">
<el-table class="custom-enterprise-table" :data="listingTrend" size="small" stripe>
<el-table-column prop="date" label="统计日期" min-width="120" />
<el-table-column prop="published_count" label="上架量" width="120" align="center">
<template #default="{ row }">
<span class="trend-val text-emerald">+{{ row.published_count }}</span>
</template>
</el-table-column>
<el-table-column prop="active_offline_count" label="主动下架" width="130" align="center">
<template #default="{ row }">
<span class="trend-val text-amber">-{{ row.active_offline_count }}</span>
</template>
</el-table-column>
<el-table-column prop="trade_leave_count" label="交易离架" width="130" align="center">
<template #default="{ row }">
<span class="trend-val text-blue">-{{ row.trade_leave_count }}</span>
</template>
</el-table-column>
<el-table-column label="合计离架" width="120" align="center">
<template #default="{ row }">
<strong class="trend-val text-slate">{{
row.active_offline_count + row.trade_leave_count
}}</strong>
</template>
</el-table-column>
</el-table>
</div>
<!-- 近 7 日渠道明细表 -->
<div v-if="listingChannelTrend.length" class="listing-channel-block">
<div class="channel-summary-title">近 7 日各渠道每日明细</div>
<div class="table-container">
<el-table class="custom-enterprise-table" :data="listingChannelTrend" size="small" stripe>
<el-table-column prop="date" label="统计日期" min-width="120" />
<el-table-column prop="source_channel" label="渠道来源" min-width="140">
<template #default="{ row }">
<el-tag size="small" effect="plain" class="channel-badge">
{{ row.source_channel || '-' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="published_count" label="上架" width="110" align="center">
<template #default="{ row }">
<span class="trend-val text-emerald">+{{ row.published_count }}</span>
</template>
</el-table-column>
<el-table-column
prop="active_offline_count"
label="主动下架"
width="120"
align="center"
>
<template #default="{ row }">
<span class="trend-val text-amber">-{{ row.active_offline_count }}</span>
</template>
</el-table-column>
<el-table-column prop="trade_leave_count" label="交易离架" width="120" align="center">
<template #default="{ row }">
<span class="trend-val text-blue">-{{ row.trade_leave_count }}</span>
</template>
</el-table-column>
<el-table-column label="合计离架" width="110" align="center">
<template #default="{ row }">
<strong class="trend-val text-slate">{{ leaveCount(row) }}</strong>
</template>
</el-table-column>
</el-table>
</div>
</div>
<div v-if="listingUploaderTrend.length" class="listing-uploader-block">
<div class="channel-summary-title">近 7 日客服上传账号明细</div>
<div class="table-container">
<el-table
class="custom-enterprise-table"
:data="listingUploaderTrend"
size="small"
stripe
>
<el-table-column prop="date" label="统计日期" min-width="120" />
<el-table-column prop="uploader_name" label="客服" min-width="140">
<template #default="{ row }">{{ row.uploader_name || '未填写' }}</template>
</el-table-column>
<el-table-column prop="upload_count" label="上传账号数" width="130" align="center" />
</el-table>
</div>
</div>
</section>
<!-- 待处理事项 & 快捷入口 -->
<div v-if="dashboard" class="dashboard-panels-grid">
<!-- 待处理事项 -->
<section class="section-card panel-card">
<div class="panel-header-row">
<div class="panel-header-title">
<el-icon class="panel-icon amber-icon"><Bell /></el-icon>
<span>待处理事项</span>
</div>
<span
class="pending-total-badge"
:class="{
'has-pending':
dashboard.pending.disputes +
dashboard.pending.listing_reviews +
dashboard.pending.pending_handoffs +
dashboard.pending.pending_return_confirms >
0,
}"
>
{{
dashboard.pending.disputes +
dashboard.pending.listing_reviews +
dashboard.pending.pending_handoffs +
dashboard.pending.pending_return_confirms
}}
项待办
</span>
</div>
<div class="pending-list-group">
<RouterLink class="pending-item-card" :to="adminPath('disputes')">
<div class="pending-info">
<span class="pending-label">待仲裁申诉</span>
<span class="pending-desc">需要人工介入处理的用户争议</span>
</div>
<span
class="pending-badge"
:class="dashboard.pending.disputes > 0 ? 'badge-urgent' : 'badge-zero'"
>
{{ dashboard.pending.disputes }}
</span>
</RouterLink>
<RouterLink class="pending-item-card" :to="adminPath('listings/review')">
<div class="pending-info">
<span class="pending-label">待审核商品</span>
<span class="pending-desc">卖家新提交上架的账号资产审核</span>
</div>
<span
class="pending-badge"
:class="dashboard.pending.listing_reviews > 0 ? 'badge-urgent' : 'badge-zero'"
>
{{ dashboard.pending.listing_reviews }}
</span>
</RouterLink>
<div class="pending-item-card static-card">
<div class="pending-info">
<span class="pending-label">待交接订单</span>
<span class="pending-desc">等待卖家确认交接发货</span>
</div>
<span
class="pending-badge"
:class="dashboard.pending.pending_handoffs > 0 ? 'badge-urgent' : 'badge-zero'"
>
{{ dashboard.pending.pending_handoffs }}
</span>
</div>
<div class="pending-item-card static-card">
<div class="pending-info">
<span class="pending-label">待结账确认</span>
<span class="pending-desc">租期结束等待双方确认结算</span>
</div>
<span
class="pending-badge"
:class="dashboard.pending.pending_return_confirms > 0 ? 'badge-urgent' : 'badge-zero'"
>
{{ dashboard.pending.pending_return_confirms }}
</span>
</div>
</div>
</section>
<!-- 快捷入口 -->
<section class="section-card panel-card">
<div class="panel-header-row">
<div class="panel-header-title">
<el-icon class="panel-icon blue-icon"><Grid /></el-icon>
<span>快捷业务导航</span>
</div>
</div>
<div class="quick-nav-grid">
<RouterLink class="quick-tile" :to="adminPath('users')">
<div class="quick-icon-wrap indigo">
<el-icon><User /></el-icon>
</div>
<span class="quick-title">用户管理</span>
</RouterLink>
<RouterLink class="quick-tile" :to="adminPath('orders')">
<div class="quick-icon-wrap blue">
<el-icon><Tickets /></el-icon>
</div>
<span class="quick-title">订单管理</span>
</RouterLink>
<RouterLink class="quick-tile" :to="adminPath('disputes')">
<div class="quick-icon-wrap rose">
<el-icon><ScaleToOriginal /></el-icon>
</div>
<span class="quick-title">仲裁中心</span>
</RouterLink>
<RouterLink class="quick-tile" :to="adminPath('listings')">
<div class="quick-icon-wrap emerald">
<el-icon><Shop /></el-icon>
</div>
<span class="quick-title">商品管理</span>
</RouterLink>
<RouterLink class="quick-tile" :to="adminPath('chats')">
<div class="quick-icon-wrap teal">
<el-icon><ChatDotRound /></el-icon>
</div>
<span class="quick-title">客服群聊</span>
</RouterLink>
<RouterLink class="quick-tile" :to="adminPath('system-configs')">
<div class="quick-icon-wrap slate">
<el-icon><Operation /></el-icon>
</div>
<span class="quick-title">系统配置</span>
</RouterLink>
</div>
</section>
</div>
<!-- 最近订单表格 -->
<section v-if="dashboard" class="section-card table-section-card">
<div class="table-card-header">
<div class="table-header-title">
<el-icon class="header-icon"><Tickets /></el-icon>
<span>最近订单动态</span>
</div>
<RouterLink :to="adminPath('orders')" class="view-all-link">查看全部订单 →</RouterLink>
</div>
<div class="table-container">
<el-table class="custom-enterprise-table" :data="dashboard.recent_orders" stripe>
<el-table-column prop="order_no" label="订单号" min-width="210">
<template #default="{ row }">
<RouterLink :to="adminPath(`orders/${row.id}`)" class="order-link">
{{ row.order_no }}
</RouterLink>
</template>
</el-table-column>
<el-table-column prop="title" label="商品名称" min-width="190" show-overflow-tooltip />
<el-table-column label="订单状态" width="120" align="center">
<template #default="{ row }">
<el-tag
:type="
row.status === 'completed'
? 'success'
: row.status === 'renting'
? 'primary'
: 'warning'
"
size="small"
effect="plain"
class="status-tag"
>
{{ orderStatusLabel(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="rent_amount_cent" label="租金金额" min-width="130" align="right">
<template #default="{ row }">
<strong class="amount-val">{{ formatCentWithSymbol(row.rent_amount_cent) }}</strong>
</template>
</el-table-column>
<el-table-column label="创建时间" min-width="170">
<template #default="{ row }">
<span class="time-cell">{{ formatDateTime(row.created_at) }}</span>
</template>
</el-table-column>
</el-table>
</div>
</section>
<!-- 最近申诉表格 -->
<section
v-if="dashboard && dashboard.recent_disputes.length > 0"
class="section-card table-section-card"
>
<div class="table-card-header">
<div class="table-header-title">
<el-icon class="header-icon rose-icon"><Warning /></el-icon>
<span>最近申诉记录</span>
</div>
<RouterLink :to="adminPath('disputes')" class="view-all-link">前往仲裁中心 →</RouterLink>
</div>
<div class="table-container">
<el-table class="custom-enterprise-table" :data="dashboard.recent_disputes" stripe>
<el-table-column prop="order_no" label="订单号" min-width="210">
<template #default="{ row }">
<span class="mono-code">{{ row.order_no }}</span>
</template>
</el-table-column>
<el-table-column prop="title" label="商品名称" min-width="190" show-overflow-tooltip />
<el-table-column prop="type" label="申诉类型" width="140">
<template #default="{ row }">
<span class="dispute-type">{{ row.type }}</span>
</template>
</el-table-column>
<el-table-column label="状态" width="120" align="center">
<template #default="{ row }">
<el-tag
:type="row.status === 'resolved' ? 'success' : 'danger'"
size="small"
effect="plain"
class="status-tag"
>
{{ disputeStatusLabel(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="创建时间" min-width="170">
<template #default="{ row }">
<span class="time-cell">{{ formatDateTime(row.created_at) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="90" align="center">
<template #default>
<RouterLink :to="adminPath('disputes')">
<el-button size="small" type="primary" link>处理</el-button>
</RouterLink>
</template>
</el-table-column>
</el-table>
</div>
</section>
</div>
</template>
<style scoped>
.finance-page-container {
display: flex;
flex-direction: column;
gap: 20px;
padding: 0 4px 32px;
}
/* ================= 头部卡片 ================= */
.page-header-card {
background: #ffffff;
border-radius: 16px;
padding: 24px 28px;
box-shadow:
0 1px 3px rgba(15, 23, 42, 0.04),
0 4px 12px rgba(15, 23, 42, 0.02);
border: 1px solid rgba(226, 232, 240, 0.8);
}
.header-main {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
}
.title-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.eyebrow-tag {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.8px;
color: #3b82f6;
background: #eff6ff;
padding: 3px 10px;
border-radius: 20px;
width: fit-content;
}
.page-title {
margin: 0;
font-size: 26px;
font-weight: 800;
color: #0f172a;
letter-spacing: -0.5px;
}
.page-subtitle {
margin: 0;
font-size: 13.5px;
color: #64748b;
line-height: 1.5;
}
.toolbar-actions {
display: flex;
align-items: center;
gap: 10px;
}
/* ================= 状态卡片 ================= */
.state-card {
background: #ffffff;
border-radius: 16px;
padding: 48px 24px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 16px;
border: 1px solid rgba(226, 232, 240, 0.8);
}
.loading-state {
color: #64748b;
}
.loading-state .el-icon {
font-size: 36px;
color: #3b82f6;
}
.error-state {
color: #e11d48;
}
.error-state .el-icon {
font-size: 36px;
}
/* ================= 核心指标卡片 ================= */
.summary-metric-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
gap: 16px;
}
.metric-card {
background: #ffffff;
border-radius: 16px;
padding: 20px 22px;
border: 1px solid rgba(226, 232, 240, 0.8);
box-shadow:
0 1px 3px rgba(15, 23, 42, 0.04),
0 4px 12px rgba(15, 23, 42, 0.02);
display: flex;
align-items: center;
gap: 16px;
position: relative;
overflow: hidden;
transition:
transform 0.2s ease,
box-shadow 0.2s ease;
}
.metric-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.08);
}
.metric-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
}
.metric-card--users::before {
background: linear-gradient(90deg, #6366f1, #818cf8);
}
.metric-card--listings::before {
background: linear-gradient(90deg, #10b981, #34d399);
}
.metric-card--orders::before {
background: linear-gradient(90deg, #f59e0b, #fbbf24);
}
.metric-card--flow::before {
background: linear-gradient(90deg, #f43f5e, #fb7185);
}
.metric-icon-wrap {
width: 48px;
height: 48px;
border-radius: 14px;
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
flex-shrink: 0;
}
.metric-icon-wrap.indigo {
background: #eef2ff;
color: #6366f1;
}
.metric-icon-wrap.emerald {
background: #ecfdf5;
color: #10b981;
}
.metric-icon-wrap.amber {
background: #fffbeb;
color: #f59e0b;
}
.metric-icon-wrap.rose {
background: #fff1f2;
color: #f43f5e;
}
.metric-icon-wrap.blue {
background: #eff6ff;
color: #3b82f6;
}
.metric-icon-wrap.teal {
background: #f0fdfa;
color: #0d9488;
}
.metric-icon-wrap.slate {
background: #f1f5f9;
color: #64748b;
}
.metric-info {
display: flex;
flex-direction: column;
gap: 4px;
}
.metric-label {
font-size: 13px;
font-weight: 500;
color: #64748b;
}
.metric-val {
font-size: 26px;
font-weight: 800;
color: #0f172a;
letter-spacing: -0.5px;
line-height: 1.2;
}
.indigo-val {
color: #4f46e5;
}
.emerald-val {
color: #059669;
}
.amber-val {
color: #d97706;
}
.rose-val {
color: #e11d48;
}
.metric-sub-pill {
display: inline-flex;
align-items: center;
font-size: 11.5px;
font-weight: 600;
padding: 2px 8px;
border-radius: 4px;
width: fit-content;
}
.metric-sub-pill.info {
background: #eef2ff;
color: #4f46e5;
}
.metric-sub-pill.success {
background: #f0fdf4;
color: #16a34a;
}
.metric-sub-pill.warning {
background: #fffbeb;
color: #d97706;
}
.metric-sub-pill.danger {
background: #fff1f2;
color: #e11d48;
}
/* ================= 通用 Section 卡片 ================= */
.section-card {
background: #ffffff;
border-radius: 16px;
padding: 22px 24px;
border: 1px solid rgba(226, 232, 240, 0.8);
box-shadow:
0 1px 3px rgba(15, 23, 42, 0.04),
0 4px 12px rgba(15, 23, 42, 0.02);
}
.section-header-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
}
.section-header-title {
display: flex;
align-items: center;
gap: 10px;
font-size: 17px;
font-weight: 700;
color: #1e293b;
}
.header-icon {
color: #3b82f6;
font-size: 20px;
}
.header-icon.rose-icon {
color: #f43f5e;
}
.header-badge {
font-size: 12px;
font-weight: 500;
color: #64748b;
background: #f1f5f9;
padding: 3px 10px;
border-radius: 12px;
}
/* ================= 上下架 3 组 Mini-Bars ================= */
.listing-daily-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 16px;
margin-bottom: 20px;
}
.listing-stat-card {
padding: 20px;
border-radius: 14px;
background: #f8fafc;
border: 1px solid #e2e8f0;
transition: all 0.2s ease;
}
.listing-stat-card:hover {
background: #ffffff;
box-shadow: 0 4px 16px rgba(15, 23, 42, 0.06);
border-color: #cbd5e1;
}
.listing-stat-head {
display: flex;
align-items: center;
gap: 12px;
}
.listing-stat-icon {
width: 40px;
height: 40px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
flex-shrink: 0;
}
.tone-publish .listing-stat-icon {
background: #ecfdf5;
color: #10b981;
}
.tone-offline .listing-stat-icon {
background: #fffbeb;
color: #f59e0b;
}
.tone-trade .listing-stat-icon {
background: #eff6ff;
color: #3b82f6;
}
.listing-stat-meta span {
display: block;
font-size: 12.5px;
font-weight: 500;
color: #64748b;
}
.listing-stat-meta strong {
display: block;
font-size: 24px;
font-weight: 800;
color: #0f172a;
line-height: 1.1;
margin-top: 2px;
}
.listing-stat-hint {
margin: 10px 0 16px;
font-size: 11.5px;
color: #94a3b8;
line-height: 1.4;
}
.mini-bars {
display: grid;
grid-template-columns: repeat(7, minmax(0, 1fr));
gap: 8px;
align-items: end;
min-height: 90px;
padding-top: 8px;
border-top: 1px dashed #e2e8f0;
}
.mini-bar-col {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
min-width: 0;
}
.mini-bar {
width: 100%;
max-width: 20px;
border-radius: 4px 4px 1px 1px;
min-height: 6px;
transition: height 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.mini-bar.publish {
background: linear-gradient(180deg, #34d399, #10b981);
}
.mini-bar.offline {
background: linear-gradient(180deg, #fbbf24, #f59e0b);
}
.mini-bar.trade {
background: linear-gradient(180deg, #60a5fa, #3b82f6);
}
.mini-bar-col em {
font-style: normal;
font-size: 10px;
color: #94a3b8;
}
.mini-bar-col b {
font-size: 11px;
color: #1e293b;
font-weight: 700;
}
/* ================= 渠道总览 ================= */
.channel-summary-title {
font-size: 14px;
font-weight: 700;
color: #334155;
margin: 16px 0 10px;
}
.listing-channel-summary {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 12px;
margin-bottom: 20px;
}
.listing-channel-item {
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 12px;
padding: 14px;
transition: all 0.2s ease;
}
.listing-channel-item:hover {
background: #ffffff;
border-color: #cbd5e1;
box-shadow: 0 2px 10px rgba(15, 23, 42, 0.05);
}
.listing-channel-item-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-bottom: 8px;
}
.channel-name {
font-size: 13.5px;
font-weight: 700;
color: #1e293b;
}
.channel-total {
font-size: 18px;
font-weight: 800;
color: #3b82f6;
}
.listing-channel-counts {
display: flex;
flex-direction: column;
gap: 4px;
}
.count-tag {
font-size: 11.5px;
font-weight: 600;
}
.count-tag.publish {
color: #059669;
}
.count-tag.offline {
color: #d97706;
}
.count-tag.trade {
color: #2563eb;
}
.trend-table-wrap {
margin-bottom: 20px;
}
.listing-channel-block {
margin-top: 20px;
}
.listing-uploader-block {
margin-top: 20px;
}
.listing-uploader-summary {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 12px;
margin-bottom: 20px;
}
.listing-uploader-item {
display: flex;
align-items: baseline;
gap: 8px;
border: 1px solid #dbeafe;
border-radius: 10px;
background: #eff6ff;
padding: 14px;
}
.uploader-name {
overflow: hidden;
color: #1e3a8a;
font-size: 13.5px;
font-weight: 700;
text-overflow: ellipsis;
white-space: nowrap;
}
.uploader-total {
margin-left: auto;
color: #2563eb;
font-size: 20px;
font-weight: 800;
}
.uploader-unit {
color: #64748b;
font-size: 11.5px;
white-space: nowrap;
}
.channel-badge {
font-weight: 600;
}
.trend-val {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-weight: 700;
font-size: 12.5px;
}
.text-emerald {
color: #059669;
}
.text-amber {
color: #d97706;
}
.text-blue {
color: #2563eb;
}
.text-slate {
color: #475569;
}
/* ================= 待处理事项 & 快捷入口 ================= */
.dashboard-panels-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 20px;
}
.panel-card {
display: flex;
flex-direction: column;
}
.panel-header-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.panel-header-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 16px;
font-weight: 700;
color: #1e293b;
}
.panel-icon {
font-size: 18px;
}
.amber-icon {
color: #f59e0b;
}
.blue-icon {
color: #3b82f6;
}
.pending-total-badge {
font-size: 12px;
font-weight: 600;
color: #64748b;
background: #f1f5f9;
padding: 3px 10px;
border-radius: 20px;
}
.pending-total-badge.has-pending {
color: #e11d48;
background: #fff1f2;
}
.pending-list-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.pending-item-card {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 12px;
text-decoration: none;
transition: all 0.2s ease;
}
.pending-item-card:hover:not(.static-card) {
background: #ffffff;
border-color: #cbd5e1;
box-shadow: 0 2px 10px rgba(15, 23, 42, 0.05);
transform: translateX(2px);
}
.pending-info {
display: flex;
flex-direction: column;
gap: 2px;
}
.pending-label {
font-size: 13.5px;
font-weight: 700;
color: #1e293b;
}
.pending-desc {
font-size: 11.5px;
color: #94a3b8;
}
.pending-badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 28px;
height: 28px;
padding: 0 8px;
border-radius: 14px;
font-size: 13px;
font-weight: 800;
}
.badge-urgent {
background: #fff1f2;
color: #e11d48;
border: 1px solid #fecdd3;
}
.badge-zero {
background: #f1f5f9;
color: #94a3b8;
}
/* ================= 快捷入口 ================= */
.quick-nav-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.quick-tile {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
padding: 18px 12px;
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 14px;
text-decoration: none;
transition: all 0.2s ease;
}
.quick-tile:hover {
background: #ffffff;
border-color: #cbd5e1;
box-shadow: 0 4px 16px rgba(15, 23, 42, 0.06);
transform: translateY(-2px);
}
.quick-icon-wrap {
width: 44px;
height: 44px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
}
.quick-icon-wrap.indigo {
background: #eef2ff;
color: #6366f1;
}
.quick-icon-wrap.blue {
background: #eff6ff;
color: #3b82f6;
}
.quick-icon-wrap.rose {
background: #fff1f2;
color: #f43f5e;
}
.quick-icon-wrap.emerald {
background: #ecfdf5;
color: #10b981;
}
.quick-icon-wrap.teal {
background: #f0fdfa;
color: #0d9488;
}
.quick-icon-wrap.slate {
background: #f1f5f9;
color: #64748b;
}
.quick-title {
font-size: 12.5px;
font-weight: 700;
color: #334155;
}
/* ================= 表格容器与通用组件 ================= */
.table-section-card {
padding-bottom: 16px;
}
.table-card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.table-header-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 16px;
font-weight: 700;
color: #1e293b;
}
.view-all-link {
font-size: 12.5px;
font-weight: 600;
color: #3b82f6;
text-decoration: none;
}
.view-all-link:hover {
text-decoration: underline;
}
.table-container {
border-radius: 12px;
overflow: hidden;
border: 1px solid #e2e8f0;
}
.custom-enterprise-table :deep(.el-table__header-wrapper th) {
background-color: #f8fafc !important;
color: #475569;
font-size: 13px;
font-weight: 600;
padding: 12px 0;
border-bottom: 1px solid #e2e8f0;
}
.custom-enterprise-table :deep(.el-table__body-wrapper td) {
padding: 12px 0;
font-size: 13px;
color: #334155;
}
.order-link {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 12.5px;
font-weight: 600;
color: #3b82f6;
text-decoration: none;
}
.order-link:hover {
text-decoration: underline;
}
.mono-code {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 12px;
color: #64748b;
}
.status-tag {
font-weight: 600;
}
.amount-val {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 13.5px;
font-weight: 700;
color: #0f172a;
}
.time-cell {
font-size: 12px;
color: #64748b;
}
.dispute-type {
font-size: 12.5px;
color: #475569;
}
@media (max-width: 1100px) {
.listing-daily-grid {
grid-template-columns: 1fr;
}
.dashboard-panels-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 768px) {
.quick-nav-grid {
grid-template-columns: repeat(2, 1fr);
}
}
</style>