|
|
@@ -0,0 +1,1262 @@
|
|
|
+<template>
|
|
|
+ <div class="dashboard-container">
|
|
|
+ <div class="header">
|
|
|
+ <div class="header-title-wrap">
|
|
|
+ <h1>优惠券运营大屏</h1>
|
|
|
+ <p v-if="storeName" class="header-sub">{{ storeName }}</p>
|
|
|
+ </div>
|
|
|
+ <div class="time-info">
|
|
|
+ <span class="month-label">客户</span>
|
|
|
+ <el-select
|
|
|
+ v-model="selectedTenantId"
|
|
|
+ size="small"
|
|
|
+ placeholder="请选择客户"
|
|
|
+ class="dark-select tenant-select"
|
|
|
+ popper-class="dark-select-popper"
|
|
|
+ filterable
|
|
|
+ clearable
|
|
|
+ :loading="tenantListLoading"
|
|
|
+ :disabled="tenantListLoading && !tenantList.length"
|
|
|
+ @change="handleTenantChange"
|
|
|
+ >
|
|
|
+ <el-option
|
|
|
+ v-for="item in tenantList"
|
|
|
+ :key="String(item.tenantId)"
|
|
|
+ :label="item.tenantName"
|
|
|
+ :value="item.tenantId"
|
|
|
+ />
|
|
|
+ </el-select>
|
|
|
+ <span class="month-label">统计月份</span>
|
|
|
+ <el-date-picker
|
|
|
+ v-model="statsMonth"
|
|
|
+ type="month"
|
|
|
+ size="small"
|
|
|
+ value-format="yyyy-MM"
|
|
|
+ clearable
|
|
|
+ placeholder="全部"
|
|
|
+ class="dark-month-picker"
|
|
|
+ popper-class="dark-select-popper"
|
|
|
+ @change="handleStatsMonthChange"
|
|
|
+ />
|
|
|
+ <span>数据更新时间:{{ updateTime }}</span>
|
|
|
+ <el-button type="text" @click="refreshData" :loading="refreshing">
|
|
|
+ {{ refreshing ? '刷新中...' : '刷新' }}
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="core-metrics">
|
|
|
+ <el-row :gutter="16" class="metrics-row">
|
|
|
+ <el-col :xs="24" :sm="12" :lg="6">
|
|
|
+ <metric-card
|
|
|
+ :title="'领取张数(' + statsMonthLabel + ')'"
|
|
|
+ :value="kpis.totalReceived"
|
|
|
+ icon="el-icon-download"
|
|
|
+ color="#67C23A"
|
|
|
+ suffix="张"
|
|
|
+ :loading="loading"
|
|
|
+ />
|
|
|
+ </el-col>
|
|
|
+ <el-col :xs="24" :sm="12" :lg="6">
|
|
|
+ <metric-card
|
|
|
+ :title="'已使用张数(' + statsMonthLabel + ')'"
|
|
|
+ :value="kpis.totalUsed"
|
|
|
+ icon="el-icon-circle-check"
|
|
|
+ color="#409EFF"
|
|
|
+ suffix="张"
|
|
|
+ :loading="loading"
|
|
|
+ />
|
|
|
+ </el-col>
|
|
|
+ <el-col :xs="24" :sm="12" :lg="6">
|
|
|
+ <metric-card
|
|
|
+ :title="'已支付红包金额(' + statsMonthLabel + ')'"
|
|
|
+ :value="kpis.paidRedPacketAmount"
|
|
|
+ icon="el-icon-money"
|
|
|
+ color="#E6A23C"
|
|
|
+ prefix="¥"
|
|
|
+ :formatter="formatCurrency"
|
|
|
+ :loading="loading"
|
|
|
+ />
|
|
|
+ </el-col>
|
|
|
+ <el-col :xs="24" :sm="12" :lg="6">
|
|
|
+ <metric-card
|
|
|
+ :title="'待支付占用(' + statsMonthLabel + ')'"
|
|
|
+ :value="kpis.unpaidRedPacketAmount"
|
|
|
+ icon="el-icon-warning"
|
|
|
+ color="#909399"
|
|
|
+ prefix="¥"
|
|
|
+ :formatter="formatCurrency"
|
|
|
+ :loading="loading"
|
|
|
+ />
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="chart-section">
|
|
|
+ <el-row :gutter="20">
|
|
|
+ <el-col :span="12">
|
|
|
+ <div class="chart-card">
|
|
|
+ <div class="chart-header chart-header--col">
|
|
|
+ <div>
|
|
|
+ <h3>红包金额({{ statsMonthLabel }})</h3>
|
|
|
+ <p v-if="!statsMonth" class="chart-desc">统计范围:{{ statsMonthLabel }}</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="chart-container chart-container--echarts">
|
|
|
+ <div ref="amountStatusChart" class="chart-instance"></div>
|
|
|
+ <div v-show="trendLoading" class="chart-loading chart-loading--overlay">
|
|
|
+ <i class="el-icon-loading"></i>
|
|
|
+ <span>加载中...</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="12">
|
|
|
+ <div class="chart-card">
|
|
|
+ <div class="chart-header chart-header--col">
|
|
|
+ <div>
|
|
|
+ <h3>领取 / 使用张数({{ statsMonthLabel }} · 按日)</h3>
|
|
|
+ <p v-if="!statsMonth" class="chart-desc">统计范围:{{ statsMonthLabel }}</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="chart-container chart-container--echarts">
|
|
|
+ <div ref="receiveUseChart" class="chart-instance"></div>
|
|
|
+ <div v-show="chartLoading" class="chart-loading chart-loading--overlay">
|
|
|
+ <i class="el-icon-loading"></i>
|
|
|
+ <span>加载中...</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+
|
|
|
+ <el-row :gutter="20" style="margin-top: 20px;">
|
|
|
+ <el-col :span="24">
|
|
|
+ <div class="chart-card">
|
|
|
+ <div class="chart-header">
|
|
|
+ <div>
|
|
|
+ <h3>使用排行</h3>
|
|
|
+ <p class="chart-desc">按红包金额合计(已付+待付)降序 · 统计范围:{{ statsMonthLabel }} · 与上方一致</p>
|
|
|
+ </div>
|
|
|
+ <el-button type="text" :loading="exportRankLoading" @click="exportUsageRank">
|
|
|
+ {{ exportRankLoading ? '导出中...' : '导出' }}
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ <el-table
|
|
|
+ :data="usageRank"
|
|
|
+ style="width: 100%"
|
|
|
+ height="320"
|
|
|
+ border
|
|
|
+ class="dark-table"
|
|
|
+ v-loading="tableLoading"
|
|
|
+ element-loading-text="数据加载中"
|
|
|
+ element-loading-background="rgba(0, 0, 0, 0.5)"
|
|
|
+ >
|
|
|
+ <el-table-column type="index" label="#" width="48" align="center" />
|
|
|
+ <el-table-column
|
|
|
+ prop="name"
|
|
|
+ label="客户/业务员"
|
|
|
+ min-width="160"
|
|
|
+ show-overflow-tooltip
|
|
|
+ tooltip-class="dark-tooltip-popper"
|
|
|
+ />
|
|
|
+ <el-table-column
|
|
|
+ prop="salesCompanyName"
|
|
|
+ label="公司名称"
|
|
|
+ min-width="160"
|
|
|
+ show-overflow-tooltip
|
|
|
+ tooltip-class="dark-tooltip-popper"
|
|
|
+ />
|
|
|
+ <el-table-column prop="usedCount" label="订单数" width="88" align="right" />
|
|
|
+ <el-table-column prop="usedCouponCount" label="用券张数" width="100" align="right" />
|
|
|
+ <el-table-column prop="paidRedPacketAmount" min-width="112" align="right">
|
|
|
+ <template slot="header">
|
|
|
+ <el-tooltip
|
|
|
+ content="订单已支付对应的红包金额"
|
|
|
+ placement="top"
|
|
|
+ popper-class="dark-tooltip-popper"
|
|
|
+ >
|
|
|
+ <span class="rank-col-head">已用金额</span>
|
|
|
+ </el-tooltip>
|
|
|
+ </template>
|
|
|
+ <template slot-scope="scope">
|
|
|
+ ¥{{ formatNumber(scope.row.paidRedPacketAmount) }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="unpaidRedPacketAmount" min-width="100" align="right">
|
|
|
+ <template slot="header">
|
|
|
+ <el-tooltip
|
|
|
+ content="订单未付、已用券占用的红包金额"
|
|
|
+ placement="top"
|
|
|
+ popper-class="dark-tooltip-popper"
|
|
|
+ >
|
|
|
+ <span class="rank-col-head">待付占用</span>
|
|
|
+ </el-tooltip>
|
|
|
+ </template>
|
|
|
+ <template slot-scope="scope">
|
|
|
+ ¥{{ formatNumber(scope.row.unpaidRedPacketAmount) }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import * as echarts from 'echarts'
|
|
|
+import {
|
|
|
+ getCouponScreenOverview,
|
|
|
+ getCouponAcquireUseTrend,
|
|
|
+ getCustomerList,
|
|
|
+ exportUsageRank as exportUsageRankApi
|
|
|
+} from '@/api/redPacket/screen'
|
|
|
+
|
|
|
+/** 可选租户下拉 + yearMonth;不传 corpsId;选中租户时请求带 tenantId */
|
|
|
+export default {
|
|
|
+ name: 'CouponScreenDashboard',
|
|
|
+
|
|
|
+ components: {
|
|
|
+ MetricCard: {
|
|
|
+ props: ['title', 'value', 'icon', 'color', 'trend', 'prefix', 'suffix', 'formatter', 'loading'],
|
|
|
+ template: `
|
|
|
+ <div class="metric-card" :style="{borderLeftColor: color}">
|
|
|
+ <div class="metric-icon" :style="{color: color}">
|
|
|
+ <i :class="icon"></i>
|
|
|
+ </div>
|
|
|
+ <div class="metric-content">
|
|
|
+ <div class="metric-title">{{ title }}</div>
|
|
|
+ <div class="metric-value">
|
|
|
+ <span v-if="prefix">{{ prefix }}</span>
|
|
|
+ <template v-if="loading">
|
|
|
+ <i class="el-icon-loading"></i> 加载中...
|
|
|
+ </template>
|
|
|
+ <template v-else>
|
|
|
+ {{ formatter ? formatter(value) : value }}
|
|
|
+ <span v-if="suffix">{{ suffix }}</span>
|
|
|
+ </template>
|
|
|
+ </div>
|
|
|
+ <div class="metric-trend" v-if="trend !== undefined && !loading">
|
|
|
+ <i :class="trend >= 0 ? 'el-icon-top' : 'el-icon-bottom'"
|
|
|
+ :style="{color: trend >= 0 ? '#67C23A' : '#F56C6C'}"></i>
|
|
|
+ <span :style="{color: trend >= 0 ? '#67C23A' : '#F56C6C'}">
|
|
|
+ {{ Math.abs(trend) }}%
|
|
|
+ </span>
|
|
|
+ <span class="trend-text">较上期</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ `
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ storeName: '',
|
|
|
+ updateTime: '',
|
|
|
+ statsMonth: null,
|
|
|
+
|
|
|
+ tenantList: [],
|
|
|
+ selectedTenantId: null,
|
|
|
+ pendingTenantId: null,
|
|
|
+ tenantListLoading: false,
|
|
|
+
|
|
|
+ loading: false,
|
|
|
+ refreshing: false,
|
|
|
+ chartLoading: false,
|
|
|
+ trendLoading: false,
|
|
|
+ tableLoading: false,
|
|
|
+
|
|
|
+ kpis: {
|
|
|
+ totalReceived: 0,
|
|
|
+ totalUsed: 0,
|
|
|
+ paidRedPacketAmount: 0,
|
|
|
+ unpaidRedPacketAmount: 0
|
|
|
+ },
|
|
|
+
|
|
|
+ overviewPayload: null,
|
|
|
+ trendSeries: [],
|
|
|
+ usageRank: [],
|
|
|
+
|
|
|
+ amountStatusChart: null,
|
|
|
+ receiveUseChart: null,
|
|
|
+
|
|
|
+ autoRefreshTimer: null,
|
|
|
+ resizeTimer: null,
|
|
|
+
|
|
|
+ exportRankLoading: false
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ computed: {
|
|
|
+ statsMonthLabel() {
|
|
|
+ if (!this.statsMonth) return '全部'
|
|
|
+ const parts = String(this.statsMonth).split('-')
|
|
|
+ if (parts.length < 2) return this.statsMonth
|
|
|
+ return `${parts[0]}年${Number(parts[1])}月`
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ mounted() {
|
|
|
+ const q = this.$route.query || {}
|
|
|
+ if (q.storeName) {
|
|
|
+ try {
|
|
|
+ this.storeName = decodeURIComponent(String(q.storeName))
|
|
|
+ } catch (e) {
|
|
|
+ this.storeName = String(q.storeName)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ const ym = q.yearMonth != null ? String(q.yearMonth) : ''
|
|
|
+ if (ym === '' || ym === 'all') {
|
|
|
+ this.statsMonth = null
|
|
|
+ } else if (/^\d{4}-\d{2}$/.test(ym)) {
|
|
|
+ this.statsMonth = ym
|
|
|
+ }
|
|
|
+ if (q.tenantId != null && q.tenantId !== '') {
|
|
|
+ this.pendingTenantId = q.tenantId
|
|
|
+ }
|
|
|
+ this.loadTenantList().then(() => {
|
|
|
+ this.initDashboard()
|
|
|
+ this.startAutoRefresh()
|
|
|
+ })
|
|
|
+ window.addEventListener('resize', this.handleResize)
|
|
|
+ },
|
|
|
+
|
|
|
+ beforeDestroy() {
|
|
|
+ this.stopAutoRefresh()
|
|
|
+ window.removeEventListener('resize', this.handleResize)
|
|
|
+ this.destroyCharts()
|
|
|
+ },
|
|
|
+
|
|
|
+ methods: {
|
|
|
+ screenRequestBody() {
|
|
|
+ const body = {
|
|
|
+ ...(this.statsMonth ? { yearMonth: this.statsMonth } : {})
|
|
|
+ }
|
|
|
+ if (this.selectedTenantId != null && this.selectedTenantId !== '') {
|
|
|
+ body.tenantId = this.selectedTenantId
|
|
|
+ }
|
|
|
+ return body
|
|
|
+ },
|
|
|
+
|
|
|
+ async loadTenantList() {
|
|
|
+ this.tenantListLoading = true
|
|
|
+ try {
|
|
|
+ const res = await getCustomerList()
|
|
|
+ const raw = res && res.data && res.data.data !== undefined ? res.data.data : res && res.data
|
|
|
+ const list = Array.isArray(raw) ? raw : []
|
|
|
+ this.tenantList = list
|
|
|
+ if (this.pendingTenantId != null) {
|
|
|
+ const want = String(this.pendingTenantId)
|
|
|
+ const hit = list.find(t => String(t.tenantId) === want)
|
|
|
+ this.selectedTenantId = hit ? hit.tenantId : null
|
|
|
+ this.pendingTenantId = null
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.warn('租户列表加载失败', e)
|
|
|
+ this.tenantList = []
|
|
|
+ } finally {
|
|
|
+ this.tenantListLoading = false
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ handleTenantChange() {
|
|
|
+ this.initDashboard()
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 接口无数据或失败时的空结构 */
|
|
|
+ emptyOverview() {
|
|
|
+ return {
|
|
|
+ storeName: this.storeName || '',
|
|
|
+ kpis: {
|
|
|
+ totalReceived: 0,
|
|
|
+ totalUsed: 0,
|
|
|
+ paidRedPacketAmount: 0,
|
|
|
+ unpaidRedPacketAmount: 0
|
|
|
+ },
|
|
|
+ trend: [],
|
|
|
+ usageRank: []
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 排行行:已付 / 待付;若仅返回 redPacketAmount 则全部计入已付(兼容旧接口)
|
|
|
+ */
|
|
|
+ normalizeUsageRankList(list) {
|
|
|
+ if (!Array.isArray(list)) return []
|
|
|
+ return list.map(row => {
|
|
|
+ if (!row || typeof row !== 'object') return row
|
|
|
+ const hasSplit =
|
|
|
+ row.paidRedPacketAmount != null ||
|
|
|
+ row.usedPaidAmount != null ||
|
|
|
+ row.paidAmount != null ||
|
|
|
+ row.unpaidRedPacketAmount != null ||
|
|
|
+ row.pendingPaymentRedPacketAmount != null ||
|
|
|
+ row.unpaidAmount != null
|
|
|
+ let paid = 0
|
|
|
+ let unpaid = 0
|
|
|
+ if (hasSplit) {
|
|
|
+ paid = Number(
|
|
|
+ row.paidRedPacketAmount != null
|
|
|
+ ? row.paidRedPacketAmount
|
|
|
+ : row.usedPaidAmount != null
|
|
|
+ ? row.usedPaidAmount
|
|
|
+ : row.paidAmount != null
|
|
|
+ ? row.paidAmount
|
|
|
+ : 0
|
|
|
+ )
|
|
|
+ unpaid = Number(
|
|
|
+ row.unpaidRedPacketAmount != null
|
|
|
+ ? row.unpaidRedPacketAmount
|
|
|
+ : row.pendingPaymentRedPacketAmount != null
|
|
|
+ ? row.pendingPaymentRedPacketAmount
|
|
|
+ : row.unpaidAmount != null
|
|
|
+ ? row.unpaidAmount
|
|
|
+ : 0
|
|
|
+ )
|
|
|
+ } else {
|
|
|
+ paid = Number(row.redPacketAmount != null ? row.redPacketAmount : 0)
|
|
|
+ unpaid = 0
|
|
|
+ }
|
|
|
+ const orderCount = Number(
|
|
|
+ row.orderCount != null ? row.orderCount : row.orderNum != null ? row.orderNum : 0
|
|
|
+ )
|
|
|
+ const usedCouponCount = Number(
|
|
|
+ row.usedCouponCount != null
|
|
|
+ ? row.usedCouponCount
|
|
|
+ : row.usedCount != null
|
|
|
+ ? row.usedCount
|
|
|
+ : 0
|
|
|
+ )
|
|
|
+ return {
|
|
|
+ ...row,
|
|
|
+ paidRedPacketAmount: paid,
|
|
|
+ unpaidRedPacketAmount: unpaid,
|
|
|
+ orderCount,
|
|
|
+ usedCouponCount
|
|
|
+ }
|
|
|
+ })
|
|
|
+ },
|
|
|
+
|
|
|
+ extractTrendList(payload) {
|
|
|
+ if (!payload) return []
|
|
|
+ if (Array.isArray(payload)) return payload
|
|
|
+ if (typeof payload !== 'object') return []
|
|
|
+ const v =
|
|
|
+ payload.trend ||
|
|
|
+ payload.dailyTrend ||
|
|
|
+ payload.trendList ||
|
|
|
+ payload.dayList ||
|
|
|
+ payload.list
|
|
|
+ return Array.isArray(v) ? v : []
|
|
|
+ },
|
|
|
+
|
|
|
+ normalizeOverview(raw) {
|
|
|
+ if (!raw || typeof raw !== 'object') return this.emptyOverview()
|
|
|
+ const k = raw.kpis || {}
|
|
|
+ const unpaid =
|
|
|
+ k.unpaidRedPacketAmount != null
|
|
|
+ ? Number(k.unpaidRedPacketAmount)
|
|
|
+ : Number(k.pendingPaymentRedPacketAmount || k.unpaidAmount || 0)
|
|
|
+ const trend = this.extractTrendList(raw)
|
|
|
+ return {
|
|
|
+ storeName: raw.storeName || this.storeName,
|
|
|
+ kpis: {
|
|
|
+ totalReceived: Number(k.totalReceived) || 0,
|
|
|
+ totalUsed: Number(k.totalUsed) || 0,
|
|
|
+ paidRedPacketAmount: Number(k.paidRedPacketAmount != null ? k.paidRedPacketAmount : k.paidAmount) || 0,
|
|
|
+ unpaidRedPacketAmount: unpaid
|
|
|
+ },
|
|
|
+ trend,
|
|
|
+ usageRank: this.normalizeUsageRankList(raw.usageRank || raw.userRank || [])
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ applyOverview(data) {
|
|
|
+ this.overviewPayload = data
|
|
|
+ if (data.storeName) this.storeName = data.storeName
|
|
|
+ this.kpis = data.kpis
|
|
|
+ this.trendSeries = data.trend || []
|
|
|
+ this.usageRank = this.normalizeUsageRankList(data.usageRank || [])
|
|
|
+ },
|
|
|
+
|
|
|
+ async initDashboard() {
|
|
|
+ this.loading = true
|
|
|
+ this.chartLoading = true
|
|
|
+ this.trendLoading = true
|
|
|
+ this.tableLoading = true
|
|
|
+ this.updateTime = this.formatDate(new Date())
|
|
|
+
|
|
|
+ let payload = null
|
|
|
+ try {
|
|
|
+ const res = await getCouponScreenOverview(this.screenRequestBody())
|
|
|
+ const body = res && res.data && res.data.data !== undefined ? res.data.data : res && res.data
|
|
|
+ payload = this.normalizeOverview(body)
|
|
|
+ } catch (e) {
|
|
|
+ console.warn('优惠券大屏总览接口请求失败', e)
|
|
|
+ payload = this.emptyOverview()
|
|
|
+ }
|
|
|
+
|
|
|
+ this.applyOverview(payload)
|
|
|
+
|
|
|
+ this.$nextTick(() => {
|
|
|
+ this.initCharts()
|
|
|
+ this.updateAmountStatusTrendChart(this.trendSeries)
|
|
|
+ this.updateReceiveUseTrendChart(this.trendSeries)
|
|
|
+ })
|
|
|
+
|
|
|
+ try {
|
|
|
+ await this.loadTrendOnly()
|
|
|
+ } catch (e) {
|
|
|
+ console.warn('趋势接口失败,沿用总览中的趋势', e)
|
|
|
+ }
|
|
|
+
|
|
|
+ this.loading = false
|
|
|
+ this.chartLoading = false
|
|
|
+ this.trendLoading = false
|
|
|
+ this.tableLoading = false
|
|
|
+ this.$nextTick(() => {
|
|
|
+ this.resizeCharts()
|
|
|
+ })
|
|
|
+ },
|
|
|
+
|
|
|
+ async loadTrendOnly() {
|
|
|
+ this.trendLoading = true
|
|
|
+ try {
|
|
|
+ const res = await getCouponAcquireUseTrend(this.screenRequestBody())
|
|
|
+ const body = res && res.data && res.data.data !== undefined ? res.data.data : res && res.data
|
|
|
+ const list = this.extractTrendList(body)
|
|
|
+ if (list && list.length) {
|
|
|
+ this.trendSeries = list
|
|
|
+ this.updateAmountStatusTrendChart(list)
|
|
|
+ this.updateReceiveUseTrendChart(list)
|
|
|
+ }
|
|
|
+ } finally {
|
|
|
+ this.trendLoading = false
|
|
|
+ this.$nextTick(() => {
|
|
|
+ this.resizeCharts()
|
|
|
+ })
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ initCharts() {
|
|
|
+ this.initAmountStatusChart()
|
|
|
+ this.initReceiveUseChart()
|
|
|
+ },
|
|
|
+
|
|
|
+ moneyTooltipFormatter(params) {
|
|
|
+ if (!params || !params.length) return ''
|
|
|
+ const ax = params[0].axisValue != null ? params[0].axisValue : params[0].name
|
|
|
+ let html = ax + '<br/>'
|
|
|
+ params.forEach(p => {
|
|
|
+ const v = Number(p.value)
|
|
|
+ const txt = isNaN(v)
|
|
|
+ ? p.value
|
|
|
+ : '¥' + v.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
|
|
+ html += `${p.marker}${p.seriesName}:${txt}<br/>`
|
|
|
+ })
|
|
|
+ return html
|
|
|
+ },
|
|
|
+
|
|
|
+ initAmountStatusChart() {
|
|
|
+ if (!this.$refs.amountStatusChart) return
|
|
|
+ if (this.amountStatusChart) this.amountStatusChart.dispose()
|
|
|
+ this.amountStatusChart = echarts.init(this.$refs.amountStatusChart)
|
|
|
+ this.amountStatusChart.setOption({
|
|
|
+ backgroundColor: 'transparent',
|
|
|
+ tooltip: {
|
|
|
+ trigger: 'axis',
|
|
|
+ backgroundColor: 'rgba(0,0,0,0.85)',
|
|
|
+ borderColor: '#333',
|
|
|
+ textStyle: { color: '#fff' },
|
|
|
+ formatter: p => this.moneyTooltipFormatter(p)
|
|
|
+ },
|
|
|
+ legend: {
|
|
|
+ data: ['已支付红包', '待支付占用'],
|
|
|
+ textStyle: { color: '#fff' },
|
|
|
+ top: 8
|
|
|
+ },
|
|
|
+ grid: { left: '3%', right: '4%', bottom: '3%', top: '18%', containLabel: true },
|
|
|
+ xAxis: {
|
|
|
+ type: 'category',
|
|
|
+ data: [],
|
|
|
+ axisLine: { lineStyle: { color: '#666' } },
|
|
|
+ axisLabel: { color: '#ccc' }
|
|
|
+ },
|
|
|
+ yAxis: {
|
|
|
+ type: 'value',
|
|
|
+ axisLine: { lineStyle: { color: '#666' } },
|
|
|
+ axisLabel: {
|
|
|
+ color: '#ccc',
|
|
|
+ formatter: val => {
|
|
|
+ const n = Number(val)
|
|
|
+ if (n >= 10000) return (n / 10000).toFixed(1) + '万'
|
|
|
+ return n
|
|
|
+ }
|
|
|
+ },
|
|
|
+ splitLine: { lineStyle: { color: '#444', type: 'dashed' } }
|
|
|
+ },
|
|
|
+ series: [
|
|
|
+ {
|
|
|
+ name: '已支付红包',
|
|
|
+ type: 'bar',
|
|
|
+ stack: 'amt',
|
|
|
+ data: [],
|
|
|
+ itemStyle: { color: '#67C23A' }
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: '待支付占用',
|
|
|
+ type: 'bar',
|
|
|
+ stack: 'amt',
|
|
|
+ data: [],
|
|
|
+ itemStyle: { color: '#909399' }
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ })
|
|
|
+ },
|
|
|
+
|
|
|
+ trendItemDayLabel(item) {
|
|
|
+ const s = String(
|
|
|
+ item.date != null
|
|
|
+ ? item.date
|
|
|
+ : item.statDate != null
|
|
|
+ ? item.statDate
|
|
|
+ : item.day != null
|
|
|
+ ? item.day
|
|
|
+ : item.bizDate != null
|
|
|
+ ? item.bizDate
|
|
|
+ : ''
|
|
|
+ )
|
|
|
+ if (s.length >= 10) return s.substring(5, 10)
|
|
|
+ return s.length >= 5 ? s.substring(5) : s
|
|
|
+ },
|
|
|
+
|
|
|
+ updateAmountStatusTrendChart(list) {
|
|
|
+ if (!this.amountStatusChart || !list || !list.length) return
|
|
|
+ const dates = list.map(item => this.trendItemDayLabel(item))
|
|
|
+ const paid = list.map(item =>
|
|
|
+ Number(
|
|
|
+ item.paidRedPacketAmount != null
|
|
|
+ ? item.paidRedPacketAmount
|
|
|
+ : item.paidAmount != null
|
|
|
+ ? item.paidAmount
|
|
|
+ : 0
|
|
|
+ )
|
|
|
+ )
|
|
|
+ const unpaid = list.map(item =>
|
|
|
+ Number(
|
|
|
+ item.unpaidRedPacketAmount != null
|
|
|
+ ? item.unpaidRedPacketAmount
|
|
|
+ : item.pendingPaymentRedPacketAmount != null
|
|
|
+ ? item.pendingPaymentRedPacketAmount
|
|
|
+ : item.unpaidAmount != null
|
|
|
+ ? item.unpaidAmount
|
|
|
+ : 0
|
|
|
+ )
|
|
|
+ )
|
|
|
+ this.amountStatusChart.setOption({
|
|
|
+ xAxis: { data: dates },
|
|
|
+ series: [{ data: paid }, { data: unpaid }]
|
|
|
+ })
|
|
|
+ },
|
|
|
+
|
|
|
+ initReceiveUseChart() {
|
|
|
+ if (!this.$refs.receiveUseChart) return
|
|
|
+ if (this.receiveUseChart) this.receiveUseChart.dispose()
|
|
|
+ this.receiveUseChart = echarts.init(this.$refs.receiveUseChart)
|
|
|
+ this.receiveUseChart.setOption({
|
|
|
+ backgroundColor: 'transparent',
|
|
|
+ tooltip: {
|
|
|
+ trigger: 'axis',
|
|
|
+ backgroundColor: 'rgba(0,0,0,0.85)',
|
|
|
+ textStyle: { color: '#fff' }
|
|
|
+ },
|
|
|
+ legend: {
|
|
|
+ data: ['领取张数', '使用张数'],
|
|
|
+ textStyle: { color: '#fff' },
|
|
|
+ top: 8
|
|
|
+ },
|
|
|
+ grid: { left: '3%', right: '4%', bottom: '3%', top: '18%', containLabel: true },
|
|
|
+ xAxis: {
|
|
|
+ type: 'category',
|
|
|
+ data: [],
|
|
|
+ axisLine: { lineStyle: { color: '#666' } },
|
|
|
+ axisLabel: { color: '#ccc' }
|
|
|
+ },
|
|
|
+ yAxis: {
|
|
|
+ type: 'value',
|
|
|
+ axisLine: { lineStyle: { color: '#666' } },
|
|
|
+ axisLabel: { color: '#ccc' },
|
|
|
+ splitLine: { lineStyle: { color: '#444', type: 'dashed' } },
|
|
|
+ minInterval: 1
|
|
|
+ },
|
|
|
+ series: [
|
|
|
+ {
|
|
|
+ name: '领取张数',
|
|
|
+ type: 'line',
|
|
|
+ smooth: true,
|
|
|
+ data: [],
|
|
|
+ itemStyle: { color: '#67C23A' }
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: '使用张数',
|
|
|
+ type: 'line',
|
|
|
+ smooth: true,
|
|
|
+ data: [],
|
|
|
+ itemStyle: { color: '#409EFF' }
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ })
|
|
|
+ },
|
|
|
+
|
|
|
+ updateReceiveUseTrendChart(list) {
|
|
|
+ if (!this.receiveUseChart || !list || !list.length) return
|
|
|
+ const dates = list.map(item => this.trendItemDayLabel(item))
|
|
|
+ const recv = list.map(item => Number(item.receivedCount != null ? item.receivedCount : item.acquire || 0))
|
|
|
+ const used = list.map(item => Number(item.usedCount != null ? item.usedCount : item.use || 0))
|
|
|
+ this.receiveUseChart.setOption({
|
|
|
+ xAxis: { data: dates },
|
|
|
+ series: [{ data: recv }, { data: used }]
|
|
|
+ })
|
|
|
+ },
|
|
|
+
|
|
|
+ async refreshData() {
|
|
|
+ try {
|
|
|
+ this.refreshing = true
|
|
|
+ await this.initDashboard()
|
|
|
+ this.$message.success('数据刷新成功')
|
|
|
+ } catch (error) {
|
|
|
+ console.error('刷新数据失败:', error)
|
|
|
+ this.$message.error('刷新数据失败')
|
|
|
+ } finally {
|
|
|
+ this.refreshing = false
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ handleStatsMonthChange() {
|
|
|
+ this.initDashboard()
|
|
|
+ },
|
|
|
+
|
|
|
+ startAutoRefresh() {
|
|
|
+ this.autoRefreshTimer = setInterval(() => {
|
|
|
+ this.refreshData()
|
|
|
+ }, 5 * 60 * 1000)
|
|
|
+ },
|
|
|
+
|
|
|
+ stopAutoRefresh() {
|
|
|
+ if (this.autoRefreshTimer) {
|
|
|
+ clearInterval(this.autoRefreshTimer)
|
|
|
+ this.autoRefreshTimer = null
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ handleResize() {
|
|
|
+ if (this.resizeTimer) clearTimeout(this.resizeTimer)
|
|
|
+ this.resizeTimer = setTimeout(() => this.resizeCharts(), 300)
|
|
|
+ },
|
|
|
+
|
|
|
+ resizeCharts() {[
|
|
|
+ this.amountStatusChart,
|
|
|
+ this.receiveUseChart
|
|
|
+ ].forEach(chart => {
|
|
|
+ if (chart) {
|
|
|
+ try {
|
|
|
+ chart.resize()
|
|
|
+ } catch (e) {
|
|
|
+ console.error(e)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+ },
|
|
|
+
|
|
|
+ destroyCharts() {[
|
|
|
+ this.amountStatusChart,
|
|
|
+ this.receiveUseChart
|
|
|
+ ].forEach(chart => {
|
|
|
+ if (chart) {
|
|
|
+ try {
|
|
|
+ chart.dispose()
|
|
|
+ } catch (e) {
|
|
|
+ console.error(e)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+ },
|
|
|
+
|
|
|
+ formatCurrency(value) {
|
|
|
+ if (value == null || value === '') return '0.00'
|
|
|
+ const numValue = Number(value)
|
|
|
+ if (isNaN(numValue)) return '0.00'
|
|
|
+ if (numValue >= 100000000) {
|
|
|
+ return (numValue / 100000000).toFixed(2) + '亿'
|
|
|
+ }
|
|
|
+ if (numValue >= 10000) {
|
|
|
+ return (numValue / 10000).toFixed(2) + '万'
|
|
|
+ }
|
|
|
+ return numValue.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
|
|
+ },
|
|
|
+
|
|
|
+ formatNumber(value) {
|
|
|
+ if (value == null || value === '') return '0.00'
|
|
|
+ const numValue = Number(value)
|
|
|
+ if (isNaN(numValue)) return '0.00'
|
|
|
+ return numValue.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
|
|
+ },
|
|
|
+
|
|
|
+ formatDate(date) {
|
|
|
+ return date.toLocaleString('zh-CN', {
|
|
|
+ year: 'numeric',
|
|
|
+ month: '2-digit',
|
|
|
+ day: '2-digit',
|
|
|
+ hour: '2-digit',
|
|
|
+ minute: '2-digit',
|
|
|
+ second: '2-digit'
|
|
|
+ })
|
|
|
+ },
|
|
|
+
|
|
|
+ async exportUsageRank() {
|
|
|
+ if (this.exportRankLoading) return
|
|
|
+ this.exportRankLoading = true
|
|
|
+ try {
|
|
|
+ const res = await exportUsageRankApi(this.screenRequestBody())
|
|
|
+ const blob = res && res.data
|
|
|
+ if (!(blob instanceof Blob)) {
|
|
|
+ this.$message.warning('导出失败:未返回文件')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (blob.size === 0) {
|
|
|
+ this.$message.warning('导出失败:文件为空')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (blob.type && blob.type.indexOf('application/json') !== -1) {
|
|
|
+ const text = await blob.text()
|
|
|
+ try {
|
|
|
+ const j = JSON.parse(text)
|
|
|
+ this.$message.error(j.msg || j.message || '导出失败')
|
|
|
+ } catch (e) {
|
|
|
+ this.$message.error('导出失败')
|
|
|
+ }
|
|
|
+ return
|
|
|
+ }
|
|
|
+ let filename = `使用排行_${Date.now()}.xlsx`
|
|
|
+ const cd = res.headers && (res.headers['content-disposition'] || res.headers['Content-Disposition'])
|
|
|
+ if (cd && typeof cd === 'string') {
|
|
|
+ const m = /filename\*?=(?:UTF-8'')?([^;\n]+)/i.exec(cd)
|
|
|
+ if (m && m[1]) {
|
|
|
+ try {
|
|
|
+ filename = decodeURIComponent(m[1].trim().replace(/^["']|["']$/g, ''))
|
|
|
+ } catch (e) {
|
|
|
+ filename = m[1].trim().replace(/^["']|["']$/g, '')
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ const url = window.URL.createObjectURL(blob)
|
|
|
+ const a = document.createElement('a')
|
|
|
+ a.href = url
|
|
|
+ a.download = filename
|
|
|
+ a.click()
|
|
|
+ window.URL.revokeObjectURL(url)
|
|
|
+ this.$message.success('导出成功')
|
|
|
+ } catch (e) {
|
|
|
+ console.warn('使用排行导出失败', e)
|
|
|
+ this.$message.error('导出请求失败')
|
|
|
+ } finally {
|
|
|
+ this.exportRankLoading = false
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.chart-loading {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ height: 100%;
|
|
|
+ min-height: 200px;
|
|
|
+ color: rgba(255, 255, 255, 0.7);
|
|
|
+}
|
|
|
+
|
|
|
+.chart-loading i {
|
|
|
+ font-size: 24px;
|
|
|
+ margin-bottom: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.chart-container--echarts {
|
|
|
+ position: relative;
|
|
|
+ min-height: 300px;
|
|
|
+}
|
|
|
+
|
|
|
+.chart-container--echarts .chart-instance {
|
|
|
+ height: 300px;
|
|
|
+ width: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.chart-loading--overlay {
|
|
|
+ position: absolute;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ top: 0;
|
|
|
+ bottom: 0;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ background: rgba(15, 22, 41, 0.55);
|
|
|
+ z-index: 2;
|
|
|
+ margin-bottom: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.dashboard-container {
|
|
|
+ padding: 20px;
|
|
|
+ background: linear-gradient(145deg, #0f1629 0%, #1a2744 45%, #243352 100%);
|
|
|
+ min-height: 100vh;
|
|
|
+ color: #fff;
|
|
|
+}
|
|
|
+
|
|
|
+.header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 24px;
|
|
|
+ padding-bottom: 16px;
|
|
|
+ border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
|
|
+}
|
|
|
+
|
|
|
+.header-title-wrap {
|
|
|
+ min-width: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.header h1 {
|
|
|
+ margin: 0;
|
|
|
+ font-size: 26px;
|
|
|
+ font-weight: 600;
|
|
|
+ letter-spacing: 2px;
|
|
|
+ background: linear-gradient(90deg, #e8f4ff, #7eb6ff);
|
|
|
+ -webkit-background-clip: text;
|
|
|
+ -webkit-text-fill-color: transparent;
|
|
|
+}
|
|
|
+
|
|
|
+.header-sub {
|
|
|
+ margin: 8px 0 0;
|
|
|
+ font-size: 14px;
|
|
|
+ color: rgba(255, 255, 255, 0.75);
|
|
|
+}
|
|
|
+
|
|
|
+.header-sub.muted {
|
|
|
+ color: rgba(255, 255, 255, 0.45);
|
|
|
+ font-size: 13px;
|
|
|
+}
|
|
|
+
|
|
|
+.time-info {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 15px;
|
|
|
+ flex-wrap: wrap;
|
|
|
+}
|
|
|
+
|
|
|
+.month-label {
|
|
|
+ font-size: 13px;
|
|
|
+ color: rgba(255, 255, 255, 0.65);
|
|
|
+}
|
|
|
+
|
|
|
+::v-deep .tenant-select.el-select {
|
|
|
+ min-width: 200px;
|
|
|
+}
|
|
|
+
|
|
|
+::v-deep .dark-month-picker.el-date-editor.el-input {
|
|
|
+ width: 130px;
|
|
|
+}
|
|
|
+
|
|
|
+::v-deep .dark-month-picker .el-input__inner {
|
|
|
+ background-color: rgba(0, 0, 0, 0.25) !important;
|
|
|
+ border-color: rgba(255, 255, 255, 0.15) !important;
|
|
|
+ color: #fff !important;
|
|
|
+}
|
|
|
+
|
|
|
+::v-deep .dark-month-picker .el-input__prefix {
|
|
|
+ color: rgba(255, 255, 255, 0.65);
|
|
|
+}
|
|
|
+
|
|
|
+.core-metrics {
|
|
|
+ margin-bottom: 24px;
|
|
|
+}
|
|
|
+
|
|
|
+.metrics-row {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+}
|
|
|
+
|
|
|
+.metric-card {
|
|
|
+ background: rgba(255, 255, 255, 0.06);
|
|
|
+ backdrop-filter: blur(10px);
|
|
|
+ border-radius: 10px;
|
|
|
+ padding: 20px 16px;
|
|
|
+ border-left: 4px solid;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ min-height: 112px;
|
|
|
+ margin-bottom: 16px;
|
|
|
+ transition: all 0.25s;
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+.metric-card:hover {
|
|
|
+ transform: translateY(-3px);
|
|
|
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
|
|
|
+}
|
|
|
+
|
|
|
+.metric-icon {
|
|
|
+ font-size: 32px;
|
|
|
+ margin-right: 14px;
|
|
|
+ opacity: 0.85;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.metric-content {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.metric-title {
|
|
|
+ font-size: 13px;
|
|
|
+ color: rgba(255, 255, 255, 0.65);
|
|
|
+ margin-bottom: 6px;
|
|
|
+ white-space: nowrap;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+}
|
|
|
+
|
|
|
+.metric-value {
|
|
|
+ font-size: 24px;
|
|
|
+ font-weight: bold;
|
|
|
+ margin-bottom: 4px;
|
|
|
+ line-height: 1.2;
|
|
|
+ white-space: nowrap;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+}
|
|
|
+
|
|
|
+.metric-trend {
|
|
|
+ font-size: 12px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.trend-text {
|
|
|
+ color: rgba(255, 255, 255, 0.55);
|
|
|
+}
|
|
|
+
|
|
|
+.chart-section {
|
|
|
+ margin-top: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.chart-card {
|
|
|
+ background: rgba(255, 255, 255, 0.05);
|
|
|
+ backdrop-filter: blur(10px);
|
|
|
+ border-radius: 10px;
|
|
|
+ padding: 18px;
|
|
|
+ height: 100%;
|
|
|
+ box-sizing: border-box;
|
|
|
+ border: 1px solid rgba(255, 255, 255, 0.06);
|
|
|
+}
|
|
|
+
|
|
|
+.chart-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 14px;
|
|
|
+}
|
|
|
+
|
|
|
+.chart-header--col {
|
|
|
+ align-items: flex-start;
|
|
|
+}
|
|
|
+
|
|
|
+.chart-header h3 {
|
|
|
+ margin: 0 0 4px 0;
|
|
|
+ font-size: 15px;
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+
|
|
|
+.chart-desc {
|
|
|
+ margin: 0;
|
|
|
+ font-size: 12px;
|
|
|
+ color: rgba(255, 255, 255, 0.45);
|
|
|
+ line-height: 1.4;
|
|
|
+}
|
|
|
+
|
|
|
+.rank-col-head {
|
|
|
+ cursor: help;
|
|
|
+ border-bottom: 1px dashed rgba(255, 255, 255, 0.35);
|
|
|
+}
|
|
|
+
|
|
|
+.chart-controls {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.chart-container {
|
|
|
+ height: calc(100% - 36px);
|
|
|
+}
|
|
|
+
|
|
|
+::v-deep .dark-table {
|
|
|
+ background: transparent !important;
|
|
|
+}
|
|
|
+
|
|
|
+::v-deep .dark-table th,
|
|
|
+::v-deep .dark-table tr {
|
|
|
+ background: rgba(255, 255, 255, 0.04) !important;
|
|
|
+ color: #fff !important;
|
|
|
+}
|
|
|
+
|
|
|
+::v-deep .dark-table td {
|
|
|
+ border-bottom: 1px solid rgba(255, 255, 255, 0.08) !important;
|
|
|
+ color: #e8e8e8 !important;
|
|
|
+}
|
|
|
+
|
|
|
+/* 行悬停:覆盖 Element 默认浅灰高亮,与大屏暗色一致 */
|
|
|
+::v-deep .dark-table .el-table__body tr:hover > td {
|
|
|
+ background-color: rgba(255, 255, 255, 0.07) !important;
|
|
|
+}
|
|
|
+
|
|
|
+::v-deep .dark-table .el-table__body tr.hover-row > td {
|
|
|
+ background-color: rgba(255, 255, 255, 0.07) !important;
|
|
|
+}
|
|
|
+
|
|
|
+::v-deep .dark-table .el-table__fixed-body-wrapper tr:hover > td,
|
|
|
+::v-deep .dark-table .el-table__fixed-body-wrapper tr.hover-row > td {
|
|
|
+ background-color: rgba(255, 255, 255, 0.07) !important;
|
|
|
+}
|
|
|
+
|
|
|
+::v-deep .dark-table::before {
|
|
|
+ background-color: rgba(255, 255, 255, 0.08) !important;
|
|
|
+}
|
|
|
+
|
|
|
+::v-deep .dark-select .el-input__inner {
|
|
|
+ background-color: rgba(0, 0, 0, 0.25) !important;
|
|
|
+ border-color: rgba(255, 255, 255, 0.15) !important;
|
|
|
+ color: #fff !important;
|
|
|
+}
|
|
|
+
|
|
|
+::v-deep .dark-select .el-input__suffix {
|
|
|
+ color: rgba(255, 255, 255, 0.7) !important;
|
|
|
+}
|
|
|
+
|
|
|
+@media (max-width: 768px) {
|
|
|
+ .header {
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: flex-start;
|
|
|
+ }
|
|
|
+
|
|
|
+ .time-info {
|
|
|
+ margin-top: 10px;
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|
|
|
+
|
|
|
+<!-- 下拉/日期面板挂在 body,scoped 无法命中,需单独写 -->
|
|
|
+<style>
|
|
|
+.dark-select-popper.el-select-dropdown {
|
|
|
+ background-color: rgba(15, 22, 41, 0.98) !important;
|
|
|
+ border: 1px solid rgba(255, 255, 255, 0.12) !important;
|
|
|
+ box-shadow: 0 8px 28px rgba(0, 0, 0, 0.45) !important;
|
|
|
+}
|
|
|
+
|
|
|
+.dark-select-popper .el-scrollbar,
|
|
|
+.dark-select-popper .el-scrollbar__wrap,
|
|
|
+.dark-select-popper .el-select-dropdown__list {
|
|
|
+ background-color: transparent !important;
|
|
|
+}
|
|
|
+
|
|
|
+.dark-select-popper .el-select-dropdown__item {
|
|
|
+ color: #e8e8e8 !important;
|
|
|
+ background-color: transparent !important;
|
|
|
+}
|
|
|
+
|
|
|
+.dark-select-popper .el-select-dropdown__item.hover,
|
|
|
+.dark-select-popper .el-select-dropdown__item:hover {
|
|
|
+ background-color: rgba(255, 255, 255, 0.1) !important;
|
|
|
+}
|
|
|
+
|
|
|
+.dark-select-popper .el-select-dropdown__item.selected {
|
|
|
+ background-color: rgba(64, 158, 255, 0.28) !important;
|
|
|
+ color: #a8d0ff !important;
|
|
|
+}
|
|
|
+
|
|
|
+.dark-select-popper .el-select-dropdown__item.is-disabled {
|
|
|
+ color: rgba(255, 255, 255, 0.35) !important;
|
|
|
+}
|
|
|
+
|
|
|
+.dark-select-popper .el-select-dropdown__empty {
|
|
|
+ color: rgba(255, 255, 255, 0.5) !important;
|
|
|
+ background-color: transparent !important;
|
|
|
+}
|
|
|
+
|
|
|
+/* 月份选择器面板(同 popper-class) */
|
|
|
+.dark-select-popper.el-picker-panel {
|
|
|
+ color: #e8e8e8 !important;
|
|
|
+ background-color: rgba(15, 22, 41, 0.98) !important;
|
|
|
+ border: 1px solid rgba(255, 255, 255, 0.12) !important;
|
|
|
+ box-shadow: 0 8px 28px rgba(0, 0, 0, 0.45) !important;
|
|
|
+}
|
|
|
+
|
|
|
+.dark-select-popper .el-picker-panel__body,
|
|
|
+.dark-select-popper .el-date-picker__header,
|
|
|
+.dark-select-popper .el-date-picker__header-label,
|
|
|
+.dark-select-popper .el-picker-panel__icon-btn {
|
|
|
+ color: #e8e8e8 !important;
|
|
|
+}
|
|
|
+
|
|
|
+.dark-select-popper .el-month-table td .cell {
|
|
|
+ color: #e8e8e8 !important;
|
|
|
+}
|
|
|
+
|
|
|
+.dark-select-popper .el-month-table td .cell:hover {
|
|
|
+ color: #79bbff !important;
|
|
|
+}
|
|
|
+
|
|
|
+.dark-select-popper .el-month-table td.current .cell,
|
|
|
+.dark-select-popper .el-month-table td.today .cell {
|
|
|
+ color: #79bbff !important;
|
|
|
+}
|
|
|
+
|
|
|
+/* 表格单元格省略号悬浮、表头问号:与深色大屏一致,避免白底刺眼 */
|
|
|
+.dark-tooltip-popper.el-tooltip__popper {
|
|
|
+ background: rgba(15, 22, 41, 0.98) !important;
|
|
|
+ color: #e8e8e8 !important;
|
|
|
+ border: 1px solid rgba(255, 255, 255, 0.14) !important;
|
|
|
+ box-shadow: 0 6px 20px rgba(0, 0, 0, 0.45) !important;
|
|
|
+ max-width: min(480px, 90vw);
|
|
|
+}
|
|
|
+
|
|
|
+.dark-tooltip-popper.el-tooltip__popper .el-tooltip__popper__inner {
|
|
|
+ color: #e8e8e8 !important;
|
|
|
+ background: transparent !important;
|
|
|
+}
|
|
|
+
|
|
|
+.dark-tooltip-popper.el-tooltip__popper[x-placement^='top'] .popper__arrow,
|
|
|
+.dark-tooltip-popper.el-tooltip__popper[x-placement^='top'] .popper__arrow::after {
|
|
|
+ border-top-color: rgba(15, 22, 41, 0.98) !important;
|
|
|
+}
|
|
|
+
|
|
|
+.dark-tooltip-popper.el-tooltip__popper[x-placement^='bottom'] .popper__arrow,
|
|
|
+.dark-tooltip-popper.el-tooltip__popper[x-placement^='bottom'] .popper__arrow::after {
|
|
|
+ border-bottom-color: rgba(15, 22, 41, 0.98) !important;
|
|
|
+}
|
|
|
+
|
|
|
+.dark-tooltip-popper.el-tooltip__popper[x-placement^='left'] .popper__arrow,
|
|
|
+.dark-tooltip-popper.el-tooltip__popper[x-placement^='left'] .popper__arrow::after {
|
|
|
+ border-left-color: rgba(15, 22, 41, 0.98) !important;
|
|
|
+}
|
|
|
+
|
|
|
+.dark-tooltip-popper.el-tooltip__popper[x-placement^='right'] .popper__arrow,
|
|
|
+.dark-tooltip-popper.el-tooltip__popper[x-placement^='right'] .popper__arrow::after {
|
|
|
+ border-right-color: rgba(15, 22, 41, 0.98) !important;
|
|
|
+}
|
|
|
+</style>
|