|
@@ -1,652 +1,1240 @@
|
|
|
<template>
|
|
|
- <div class="statistics-container">
|
|
|
- <!-- 页面标题 -->
|
|
|
- <h1 class="page-title">辽宁凤凰山景区票务统计</h1>
|
|
|
-
|
|
|
- <!-- 日期选择器 -->
|
|
|
- <el-card class="date-selector-card">
|
|
|
- <div class="date-selector-wrapper">
|
|
|
- <span class="date-label">选择日期:</span>
|
|
|
- <el-date-picker
|
|
|
- v-model="selectedDate"
|
|
|
- type="date"
|
|
|
- placeholder="选择日期"
|
|
|
- :shortcuts="dateShortcuts"
|
|
|
- @change="handleDateChange"
|
|
|
- class="date-picker"
|
|
|
- />
|
|
|
+ <div class="ticket-statistics-container">
|
|
|
+ <el-config-provider :locale="zhCn">
|
|
|
+ <div class="dashboard-header">
|
|
|
+ <div class="header-content">
|
|
|
+ <h1 class="page-title">辽宁凤凰山景区票务统计</h1>
|
|
|
+ <!-- <div class="subtitle">实时数据分析平台</div> -->
|
|
|
+ </div>
|
|
|
</div>
|
|
|
- </el-card>
|
|
|
-
|
|
|
- <!-- 统计数据卡片网格 -->
|
|
|
- <div class="statistics-grid">
|
|
|
- <!-- 模块1:售票数据 -->
|
|
|
- <el-card class="statistics-card sales-card" :body-style="{ padding: '0' }">
|
|
|
- <template #header>
|
|
|
- <div class="card-header">
|
|
|
- <span class="card-title">当日售票数据</span>
|
|
|
- <el-tag type="success" size="small">{{ dayjs(selectedDate).format('YYYY年MM月DD日') }}</el-tag>
|
|
|
- </div>
|
|
|
- </template>
|
|
|
- <div class="card-content">
|
|
|
- <!-- 线上售票数据 -->
|
|
|
- <div class="data-section">
|
|
|
- <h3 class="section-title"><i class="el-icon-mobile-phone"></i> 线上售票</h3>
|
|
|
- <div class="data-list">
|
|
|
- <div class="data-item" v-for="(item, index) in onlineData" :key="index">
|
|
|
- <span class="label">{{ item.label }}</span>
|
|
|
- <span class="value highlight-value">{{ item.value }}</span>
|
|
|
+
|
|
|
+ <!-- 日期选择器 -->
|
|
|
+ <div class="date-picker-container">
|
|
|
+ <el-card shadow="hover" class="date-picker-card">
|
|
|
+ <!-- <div class="date-picker-header">
|
|
|
+ <i class="el-icon-date"></i>
|
|
|
+ <span>选择日期</span>
|
|
|
+ </div> -->
|
|
|
+ <el-date-picker
|
|
|
+ v-model="selectedDate"
|
|
|
+ type="date"
|
|
|
+ placeholder="选择日期"
|
|
|
+ :shortcuts="dateShortcuts"
|
|
|
+ @change="handleDateChange"
|
|
|
+ :size="isMobile ? 'small' : 'default'"
|
|
|
+ style="width: 100%;"
|
|
|
+ class="custom-date-picker"
|
|
|
+ />
|
|
|
+ </el-card>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 数据概览卡片 -->
|
|
|
+ <div class="statistics-overview">
|
|
|
+ <el-row :gutter="20">
|
|
|
+ <el-col :xs="12" :sm="6" :md="6" :lg="6">
|
|
|
+ <el-card shadow="hover" class="overview-card sales-card">
|
|
|
+ <div class="card-icon">
|
|
|
+ <i class="el-icon-money"></i>
|
|
|
</div>
|
|
|
- <div class="total-item">
|
|
|
- <span class="label">线上总计</span>
|
|
|
- <span class="value total-value">{{ getTotalOnline() }}</span>
|
|
|
+ <div class="card-content">
|
|
|
+ <div class="card-title">总销售额</div>
|
|
|
+ <div class="card-value">¥{{ totalSales.toLocaleString() }}</div>
|
|
|
</div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 线下售票数据 -->
|
|
|
- <div class="data-section">
|
|
|
- <h3 class="section-title"><i class="el-icon-office-building"></i> 线下售票</h3>
|
|
|
- <div class="data-list">
|
|
|
- <div class="data-item" v-for="(item, index) in offlineData" :key="index">
|
|
|
- <span class="label">{{ item.label }}</span>
|
|
|
- <span class="value highlight-value">{{ item.value }}</span>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ <el-col :xs="12" :sm="6" :md="6" :lg="6">
|
|
|
+ <el-card shadow="hover" class="overview-card reserved-card">
|
|
|
+ <div class="card-content">
|
|
|
+ <div class="card-title">总预定人数</div>
|
|
|
+ <div class="card-value">{{ totalReservedPeople.toLocaleString() }}</div>
|
|
|
</div>
|
|
|
- <div class="total-item">
|
|
|
- <span class="label">线下总计</span>
|
|
|
- <span class="value total-value">{{ getTotalOffline() }}</span>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ <el-col :xs="12" :sm="6" :md="6" :lg="6">
|
|
|
+ <el-card shadow="hover" class="overview-card verified-card">
|
|
|
+ <div class="card-content">
|
|
|
+ <div class="card-title">总验证人数</div>
|
|
|
+ <div class="card-value">{{ totalVerifiedPeople.toLocaleString() }}</div>
|
|
|
</div>
|
|
|
- </div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ <el-col :xs="12" :sm="6" :md="6" :lg="6">
|
|
|
+ <el-card shadow="hover" class="overview-card refund-card">
|
|
|
+ <div class="card-content">
|
|
|
+ <div class="card-title">退票率</div>
|
|
|
+ <div class="card-value">{{ refundRate.toFixed(2) }}%</div>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 线上售票数据 -->
|
|
|
+ <div class="data-section">
|
|
|
+ <div class="section-header">
|
|
|
+ <h2 class="section-title">线上售票数据</h2>
|
|
|
+ <div class="section-actions">
|
|
|
+ <el-button type="primary" size="small" icon="el-icon-refresh" circle @click="handleDateChange"></el-button>
|
|
|
</div>
|
|
|
</div>
|
|
|
- </el-card>
|
|
|
-
|
|
|
- <!-- 模块2:检票数据 -->
|
|
|
- <el-card class="statistics-card check-card" :body-style="{ padding: '0' }">
|
|
|
- <template #header>
|
|
|
- <div class="card-header">
|
|
|
- <span class="card-title">当日检票数据</span>
|
|
|
- <el-tag type="warning" size="small">{{ dayjs(selectedDate).format('YYYY年MM月DD日') }}</el-tag>
|
|
|
- </div>
|
|
|
- </template>
|
|
|
- <div class="card-content">
|
|
|
- <div class="check-data-container">
|
|
|
- <!-- 检票数据图表 -->
|
|
|
- <div class="check-chart">
|
|
|
- <div class="chart-placeholder">
|
|
|
- <!-- 这里可以放置一个饼图或柱状图 -->
|
|
|
- <div class="chart-circle" v-for="(item, index) in checkData" :key="index"
|
|
|
- :style="{
|
|
|
- width: `${getPercentage(item.value, getTotalCheck())}%`,
|
|
|
- backgroundColor: getColorByIndex(index)
|
|
|
- }">
|
|
|
- {{ item.label }}
|
|
|
+
|
|
|
+ <!-- 电脑端表格 -->
|
|
|
+ <div v-if="!isMobile" class="desktop-table">
|
|
|
+ <el-table
|
|
|
+ :data="onlineData"
|
|
|
+ style="width: 100%"
|
|
|
+ :border="true"
|
|
|
+ stripe
|
|
|
+ highlight-current-row
|
|
|
+ class="custom-table"
|
|
|
+ >
|
|
|
+ <el-table-column prop="name" label="分销商" width="120">
|
|
|
+ <template #default="scope">
|
|
|
+ <div class="table-cell-with-icon">
|
|
|
+ <!-- <span class="distributor-icon" :class="getDistributorIconClass(scope.row.name)"></span> -->
|
|
|
+ <span>{{ scope.row.name }}</span>
|
|
|
</div>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="reservedCount" label="预定数" width="100" />
|
|
|
+ <el-table-column prop="verifiedCount" label="验证数" width="100" />
|
|
|
+ <el-table-column prop="refundCount" label="退票数" width="100" />
|
|
|
+ <el-table-column prop="reservedPeople" label="预定人数" width="100" />
|
|
|
+ <el-table-column prop="verifiedPeople" label="验证人数" width="100" />
|
|
|
+ <el-table-column prop="refundPeople" label="退票人数" width="100" />
|
|
|
+ <el-table-column prop="salesAmount" label="销售金额">
|
|
|
+ <template #default="scope">
|
|
|
+ <span class="sales-amount-cell">¥{{ scope.row.salesAmount.toLocaleString() }}</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 手机端卡片 -->
|
|
|
+ <div v-else class="mobile-cards">
|
|
|
+ <el-card
|
|
|
+ v-for="(item, index) in onlineData"
|
|
|
+ :key="'online-' + index"
|
|
|
+ class="mobile-data-card"
|
|
|
+ shadow="hover"
|
|
|
+ >
|
|
|
+ <div class="card-header">
|
|
|
+ <div class="distributor-badge" :class="getDistributorClass(item.name)">
|
|
|
+ <!-- <span class="distributor-icon" :class="getDistributorIconClass(item.name)"></span> -->
|
|
|
+ <h3>{{ item.name }}</h3>
|
|
|
</div>
|
|
|
</div>
|
|
|
-
|
|
|
- <!-- 检票数据列表 -->
|
|
|
- <div class="data-list">
|
|
|
- <div class="data-item" v-for="(item, index) in checkData" :key="index">
|
|
|
- <div class="label-with-color">
|
|
|
- <span class="color-dot" :style="{ backgroundColor: getColorByIndex(index) }"></span>
|
|
|
- <span class="label">{{ item.label }}</span>
|
|
|
- </div>
|
|
|
- <span class="value highlight-value">{{ item.value }}</span>
|
|
|
+ <div class="card-grid">
|
|
|
+ <div class="grid-item">
|
|
|
+ <div class="item-label">预定数</div>
|
|
|
+ <div class="item-value">{{ item.reservedCount }}</div>
|
|
|
+ </div>
|
|
|
+ <div class="grid-item">
|
|
|
+ <div class="item-label">验证数</div>
|
|
|
+ <div class="item-value">{{ item.verifiedCount }}</div>
|
|
|
+ </div>
|
|
|
+ <div class="grid-item">
|
|
|
+ <div class="item-label">退票数</div>
|
|
|
+ <div class="item-value">{{ item.refundCount }}</div>
|
|
|
+ </div>
|
|
|
+ <div class="grid-item">
|
|
|
+ <div class="item-label">预定人数</div>
|
|
|
+ <div class="item-value">{{ item.reservedPeople }}</div>
|
|
|
+ </div>
|
|
|
+ <div class="grid-item">
|
|
|
+ <div class="item-label">验证人数</div>
|
|
|
+ <div class="item-value">{{ item.verifiedPeople }}</div>
|
|
|
</div>
|
|
|
- <div class="total-item">
|
|
|
- <span class="label">检票总计</span>
|
|
|
- <span class="value total-value">{{ getTotalCheck() }}</span>
|
|
|
+ <div class="grid-item">
|
|
|
+ <div class="item-label">退票人数</div>
|
|
|
+ <div class="item-value">{{ item.refundPeople }}</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </el-card>
|
|
|
-
|
|
|
- <!-- 模块3:收入数据 -->
|
|
|
- <el-card class="statistics-card income-card" :body-style="{ padding: '0' }">
|
|
|
- <template #header>
|
|
|
- <div class="card-header">
|
|
|
- <span class="card-title">当日收入</span>
|
|
|
- <el-tag type="primary" size="small">{{ dayjs(selectedDate).format('YYYY年MM月DD日') }}</el-tag>
|
|
|
- </div>
|
|
|
- </template>
|
|
|
- <div class="card-content">
|
|
|
- <!-- 总收入展示 -->
|
|
|
- <div class="total-income">
|
|
|
- <div class="income-label">总收入</div>
|
|
|
- <div class="income-amount">
|
|
|
- <span class="income-icon">¥</span>
|
|
|
- {{ formatCurrency(incomeData.total) }}
|
|
|
+ <div class="sales-amount">
|
|
|
+ <span class="amount-label">销售金额</span>
|
|
|
+ <span class="amount-value">¥{{ item.salesAmount.toLocaleString() }}</span>
|
|
|
</div>
|
|
|
-
|
|
|
+ </el-card>
|
|
|
+ </div>
|
|
|
+ <!-- 如果没数据 -->
|
|
|
+ <div v-if="onlineData.length === 0" class="no-data-container">
|
|
|
+ <el-empty description="暂无数据"></el-empty>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 线上数据图表 -->
|
|
|
+ <!-- <div class="chart-container">
|
|
|
+ <el-tabs v-model="onlineChartType" @tab-click="handleChartTypeChange" class="custom-tabs">
|
|
|
+ <el-tab-pane label="销售金额" name="salesAmount"></el-tab-pane>
|
|
|
+ <el-tab-pane label="预定人数" name="reservedPeople"></el-tab-pane>
|
|
|
+ <el-tab-pane label="验证人数" name="verifiedPeople"></el-tab-pane>
|
|
|
+ </el-tabs>
|
|
|
+ <div ref="onlineChartRef" class="chart"></div>
|
|
|
+ </div> -->
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 线下购票数据 -->
|
|
|
+ <div class="data-section">
|
|
|
+ <div class="section-header">
|
|
|
+ <h2 class="section-title">线下购票数据</h2>
|
|
|
+ <div class="section-actions">
|
|
|
+ <el-button type="primary" size="small" icon="el-icon-refresh" circle @click="handleDateChange"></el-button>
|
|
|
</div>
|
|
|
-
|
|
|
- <!-- 收入明细 -->
|
|
|
- <div class="income-details">
|
|
|
- <div class="data-item" v-for="(item, index) in incomeData.details" :key="index">
|
|
|
- <span class="label">{{ item.label }}</span>
|
|
|
- <span class="value income-value">¥{{ formatCurrency(item.value) }}</span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 电脑端表格 -->
|
|
|
+ <div v-if="!isMobile" class="desktop-table">
|
|
|
+ <el-table
|
|
|
+ :data="offlineData"
|
|
|
+ style="width: 100%"
|
|
|
+ :border="true"
|
|
|
+ stripe
|
|
|
+ highlight-current-row
|
|
|
+ class="custom-table"
|
|
|
+ >
|
|
|
+ <el-table-column prop="type" label="客户类型" width="120">
|
|
|
+ <template #default="scope">
|
|
|
+ <div class="table-cell-with-icon">
|
|
|
+ <!-- <span class="customer-icon" :class="getCustomerIconClass(scope.row.type)"></span> -->
|
|
|
+ <span>{{ scope.row.type }}</span>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="reservedCount" label="预定数" width="100" />
|
|
|
+ <el-table-column prop="verifiedCount" label="验证数" width="100" />
|
|
|
+ <el-table-column prop="refundCount" label="退票数" width="100" />
|
|
|
+ <el-table-column prop="reservedPeople" label="预定人数" width="100" />
|
|
|
+ <el-table-column prop="verifiedPeople" label="验证人数" width="100" />
|
|
|
+ <el-table-column prop="refundPeople" label="退票人数" width="100" />
|
|
|
+ <el-table-column prop="salesAmount" label="销售金额">
|
|
|
+ <template #default="scope">
|
|
|
+ <span class="sales-amount-cell">¥{{ scope.row.salesAmount.toLocaleString() }}</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 手机端卡片 -->
|
|
|
+ <div v-else class="mobile-cards">
|
|
|
+ <el-card
|
|
|
+ v-for="(item, index) in offlineData"
|
|
|
+ :key="'offline-' + index"
|
|
|
+ class="mobile-data-card"
|
|
|
+ shadow="hover"
|
|
|
+ >
|
|
|
+ <div class="card-header">
|
|
|
+ <div class="customer-badge" :class="getCustomerClass(item.type)">
|
|
|
+ <!-- <span class="customer-icon" :class="getCustomerIconClass(item.type)"></span> -->
|
|
|
+ <h3>{{ item.type }}</h3>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
-
|
|
|
- <!-- 收入占比条 -->
|
|
|
- <div class="income-ratio-bar">
|
|
|
- <div class="ratio-segment online-segment"
|
|
|
- :style="{ width: `${(incomeData.details[0].value / incomeData.total) * 100}%` }">
|
|
|
- {{ Math.round((incomeData.details[0].value / incomeData.total) * 100) }}%
|
|
|
+ <div class="card-grid">
|
|
|
+ <div class="grid-item">
|
|
|
+ <div class="item-label">预定数</div>
|
|
|
+ <div class="item-value">{{ item.reservedCount }}</div>
|
|
|
</div>
|
|
|
- <div class="ratio-segment offline-segment"
|
|
|
- :style="{ width: `${(incomeData.details[1].value / incomeData.total) * 100}%` }">
|
|
|
- {{ Math.round((incomeData.details[1].value / incomeData.total) * 100) }}%
|
|
|
+ <div class="grid-item">
|
|
|
+ <div class="item-label">验证数</div>
|
|
|
+ <div class="item-value">{{ item.verifiedCount }}</div>
|
|
|
</div>
|
|
|
- </div>
|
|
|
- <div class="ratio-legend">
|
|
|
- <div class="legend-item">
|
|
|
- <span class="color-dot online-dot"></span>
|
|
|
- <span>线上收入</span>
|
|
|
+ <div class="grid-item">
|
|
|
+ <div class="item-label">退票数</div>
|
|
|
+ <div class="item-value">{{ item.refundCount }}</div>
|
|
|
</div>
|
|
|
- <div class="legend-item">
|
|
|
- <span class="color-dot offline-dot"></span>
|
|
|
- <span>线下收入</span>
|
|
|
+ <div class="grid-item">
|
|
|
+ <div class="item-label">预定人数</div>
|
|
|
+ <div class="item-value">{{ item.reservedPeople }}</div>
|
|
|
+ </div>
|
|
|
+ <div class="grid-item">
|
|
|
+ <div class="item-label">验证人数</div>
|
|
|
+ <div class="item-value">{{ item.verifiedPeople }}</div>
|
|
|
+ </div>
|
|
|
+ <div class="grid-item">
|
|
|
+ <div class="item-label">退票人数</div>
|
|
|
+ <div class="item-value">{{ item.refundPeople }}</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
- </div>
|
|
|
+ <div class="sales-amount">
|
|
|
+ <span class="amount-label">销售金额</span>
|
|
|
+ <span class="amount-value">¥{{ item.salesAmount.toLocaleString() }}</span>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </div>
|
|
|
+ <!-- 如果没数据 -->
|
|
|
+ <div v-if="offlineData.length === 0" class="no-data-container">
|
|
|
+ <el-empty description="暂无数据"></el-empty>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 线下数据图表 -->
|
|
|
+ <!-- <div class="chart-container">
|
|
|
+ <el-tabs v-model="offlineChartType" @tab-click="handleChartTypeChange" class="custom-tabs">
|
|
|
+ <el-tab-pane label="销售金额" name="salesAmount"></el-tab-pane>
|
|
|
+ <el-tab-pane label="预定人数" name="reservedPeople"></el-tab-pane>
|
|
|
+ <el-tab-pane label="验证人数" name="verifiedPeople"></el-tab-pane>
|
|
|
+ </el-tabs>
|
|
|
+ <div ref="offlineChartRef" class="chart"></div>
|
|
|
+ </div> -->
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="dashboard-footer">
|
|
|
+ <div class="footer-content">
|
|
|
+ <p>© {{ new Date().getFullYear() }} 辽宁凤凰山景区 - 票务管理系统</p>
|
|
|
</div>
|
|
|
- </el-card>
|
|
|
- </div>
|
|
|
+ </div>
|
|
|
+ </el-config-provider>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
-import { ref, onMounted, computed } from 'vue'
|
|
|
+import { ref, computed, onMounted, nextTick, watch } from 'vue'
|
|
|
+import { zhCn } from 'element-plus/es/locale/index'
|
|
|
+import * as echarts from 'echarts/core'
|
|
|
+import { BarChart, PieChart } from 'echarts/charts'
|
|
|
+import {
|
|
|
+ TitleComponent,
|
|
|
+ TooltipComponent,
|
|
|
+ LegendComponent,
|
|
|
+ GridComponent
|
|
|
+} from 'echarts/components'
|
|
|
+import { CanvasRenderer } from 'echarts/renderers'
|
|
|
+import { ElMessage } from 'element-plus'
|
|
|
+
|
|
|
import { getStatisticsData } from '@/api/statistics'
|
|
|
-import dayjs from 'dayjs'
|
|
|
|
|
|
-// 日期选择
|
|
|
+// 注册必须的组件
|
|
|
+echarts.use([
|
|
|
+ TitleComponent,
|
|
|
+ TooltipComponent,
|
|
|
+ LegendComponent,
|
|
|
+ GridComponent,
|
|
|
+ BarChart,
|
|
|
+ PieChart,
|
|
|
+ CanvasRenderer
|
|
|
+])
|
|
|
+
|
|
|
+// 响应式判断
|
|
|
+const isMobile = ref(false)
|
|
|
+const checkMobile = () => {
|
|
|
+ isMobile.value = window.innerWidth < 768
|
|
|
+}
|
|
|
+
|
|
|
+// 日期选择器相关
|
|
|
const selectedDate = ref(new Date())
|
|
|
+
|
|
|
const dateShortcuts = [
|
|
|
{
|
|
|
text: '今天',
|
|
|
- value: new Date(),
|
|
|
+ value: new Date()
|
|
|
},
|
|
|
{
|
|
|
text: '昨天',
|
|
|
value: () => {
|
|
|
const date = new Date()
|
|
|
- date.setTime(date.getTime() - 3600 * 1000 * 24)
|
|
|
+ date.setDate(date.getDate() - 1)
|
|
|
return date
|
|
|
- },
|
|
|
+ }
|
|
|
},
|
|
|
{
|
|
|
- text: '上周同日',
|
|
|
+ text: '一周前',
|
|
|
value: () => {
|
|
|
const date = new Date()
|
|
|
- date.setTime(date.getTime() - 3600 * 1000 * 24 * 7)
|
|
|
+ date.setDate(date.getDate() - 7)
|
|
|
return date
|
|
|
- },
|
|
|
- },
|
|
|
+ }
|
|
|
+ }
|
|
|
]
|
|
|
|
|
|
-// 模拟数据
|
|
|
-const onlineData = ref([
|
|
|
- { label: '小程序', value: 0 },
|
|
|
- { label: '抖音', value: 0 },
|
|
|
- { label: '美团', value: 0 },
|
|
|
- { label: '携程', value: 0 }
|
|
|
-])
|
|
|
+// 模拟数据 - 线上售票
|
|
|
+const onlineData = ref([])
|
|
|
|
|
|
-const offlineData = ref([
|
|
|
- { label: '窗口1', value: 0 },
|
|
|
- { label: '窗口2', value: 0 }
|
|
|
-])
|
|
|
+// 模拟数据 - 线下购票
|
|
|
+const offlineData = ref([])
|
|
|
|
|
|
-const checkData = ref([
|
|
|
- { label: '年卡', value: 0 },
|
|
|
- { label: '普通票', value: 0 },
|
|
|
- { label: '优惠票', value: 0 }
|
|
|
-])
|
|
|
+// 计算总数据
|
|
|
+const totalSales = computed(() => {
|
|
|
+ const onlineSales = onlineData.value.reduce((sum, item) => sum + item.salesAmount, 0)
|
|
|
+ const offlineSales = offlineData.value.reduce((sum, item) => sum + item.salesAmount, 0)
|
|
|
+ return onlineSales + offlineSales
|
|
|
+})
|
|
|
|
|
|
-const incomeData = ref({
|
|
|
- total: 0,
|
|
|
- details: [
|
|
|
- { label: '线上收入', value: 0 },
|
|
|
- { label: '线下收入', value: 0 }
|
|
|
- ]
|
|
|
+const totalReservedPeople = computed(() => {
|
|
|
+ const onlinePeople = onlineData.value.reduce((sum, item) => sum + item.reservedPeople, 0)
|
|
|
+ const offlinePeople = offlineData.value.reduce((sum, item) => sum + item.reservedPeople, 0)
|
|
|
+ return onlinePeople + offlinePeople
|
|
|
})
|
|
|
|
|
|
-// 计算总数的方法
|
|
|
-const getTotalOnline = () => {
|
|
|
- return onlineData.value.reduce((sum, item) => sum + item.value, 0)
|
|
|
+const totalVerifiedPeople = computed(() => {
|
|
|
+ const onlinePeople = onlineData.value.reduce((sum, item) => sum + item.verifiedPeople, 0)
|
|
|
+ const offlinePeople = offlineData.value.reduce((sum, item) => sum + item.verifiedPeople, 0)
|
|
|
+ return onlinePeople + offlinePeople
|
|
|
+})
|
|
|
+
|
|
|
+const totalRefundPeople = computed(() => {
|
|
|
+ const onlinePeople = onlineData.value.reduce((sum, item) => sum + item.refundPeople, 0)
|
|
|
+ const offlinePeople = offlineData.value.reduce((sum, item) => sum + item.refundPeople, 0)
|
|
|
+ return onlinePeople + offlinePeople
|
|
|
+})
|
|
|
+
|
|
|
+const refundRate = computed(() => {
|
|
|
+ if (totalReservedPeople.value === 0) return 0
|
|
|
+ return (totalRefundPeople.value / totalReservedPeople.value) * 100
|
|
|
+})
|
|
|
+
|
|
|
+
|
|
|
+// 图表相关
|
|
|
+const onlineChartRef = ref(null)
|
|
|
+const offlineChartRef = ref(null)
|
|
|
+let onlineChart = null
|
|
|
+let offlineChart = null
|
|
|
+const onlineChartType = ref('salesAmount')
|
|
|
+const offlineChartType = ref('salesAmount')
|
|
|
+
|
|
|
+// 处理日期变化
|
|
|
+const handleDateChange = async () => {
|
|
|
+ try {
|
|
|
+ const formatDateTime = (date, isEndTime = false) => {
|
|
|
+ const year = date.getFullYear()
|
|
|
+ const month = String(date.getMonth() + 1).padStart(2, '0')
|
|
|
+ const day = String(date.getDate()).padStart(2, '0')
|
|
|
+ return `${year}-${month}-${day} ${isEndTime ? '23:59:59' : '00:00:00'}`
|
|
|
+ }
|
|
|
+
|
|
|
+ const params = {
|
|
|
+ beginTime: formatDateTime(selectedDate.value),
|
|
|
+ endTime: formatDateTime(selectedDate.value, true)
|
|
|
+ }
|
|
|
+ // console.log('params',params)
|
|
|
+
|
|
|
+ const response = await getStatisticsData(params)
|
|
|
+
|
|
|
+ console.log('response',response)
|
|
|
+
|
|
|
+ if (response.code === 200) {
|
|
|
+ // 更新线上数据
|
|
|
+ onlineData.value = response.data.onlineData.chartData.map(item => ({
|
|
|
+ name: item.productName,
|
|
|
+ reservedCount: item.totalNum,
|
|
|
+ verifiedCount: item.useNum,
|
|
|
+ refundCount: item.backNum,
|
|
|
+ reservedPeople: item.totalPeopleNum,
|
|
|
+ verifiedPeople: item.usePeopleNum,
|
|
|
+ refundPeople: item.backPeopleNum,
|
|
|
+ salesAmount: item.settlementPrice
|
|
|
+ }))
|
|
|
+
|
|
|
+ console.log('onlineData',onlineData.value)
|
|
|
+
|
|
|
+ // 更新线下数据
|
|
|
+ offlineData.value = response.data.offData.offDataList.map(item => ({
|
|
|
+ type: item.productName,
|
|
|
+ reservedCount: item.totalNum,
|
|
|
+ verifiedCount: item.useNum,
|
|
|
+ refundCount: item.backNum,
|
|
|
+ reservedPeople: item.totalPeopleNum,
|
|
|
+ verifiedPeople: item.usePeopleNum,
|
|
|
+ refundPeople: item.backPeopleNum,
|
|
|
+ salesAmount: item.settlementPrice
|
|
|
+ }))
|
|
|
+
|
|
|
+ // 更新图表
|
|
|
+ nextTick(() => {
|
|
|
+ updateCharts()
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ ElMessage.error(response.msg || '获取数据失败')
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取统计数据失败:', error)
|
|
|
+ ElMessage.error('获取统计数据失败')
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
-const getTotalOffline = () => {
|
|
|
- return offlineData.value.reduce((sum, item) => sum + item.value, 0)
|
|
|
+// 处理图表类型变化
|
|
|
+const handleChartTypeChange = () => {
|
|
|
+ nextTick(() => {
|
|
|
+ updateCharts()
|
|
|
+ })
|
|
|
}
|
|
|
|
|
|
-const getTotalCheck = () => {
|
|
|
- return checkData.value.reduce((sum, item) => sum + item.value, 0)
|
|
|
+// 获取分销商图标类名
|
|
|
+const getDistributorIconClass = (name) => {
|
|
|
+ switch (name) {
|
|
|
+ case '抖音':
|
|
|
+ return 'icon-douyin'
|
|
|
+ case '美团':
|
|
|
+ return 'icon-meituan'
|
|
|
+ case '携程':
|
|
|
+ return 'icon-xiecheng'
|
|
|
+ case '小程序':
|
|
|
+ return 'icon-xiaochengxu'
|
|
|
+ default:
|
|
|
+ return ''
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
-// 格式化货币
|
|
|
-const formatCurrency = (value) => {
|
|
|
- return value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
|
|
+// 获取分销商类名
|
|
|
+const getDistributorClass = (name) => {
|
|
|
+ switch (name) {
|
|
|
+ case '抖音':
|
|
|
+ return 'douyin-badge'
|
|
|
+ case '美团':
|
|
|
+ return 'meituan-badge'
|
|
|
+ case '携程':
|
|
|
+ return 'xiecheng-badge'
|
|
|
+ case '小程序':
|
|
|
+ return 'xiaochengxu-badge'
|
|
|
+ default:
|
|
|
+ return ''
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
-// 获取百分比
|
|
|
-const getPercentage = (value, total) => {
|
|
|
- if (total === 0) return 0
|
|
|
- return Math.max(10, Math.round((value / total) * 100))
|
|
|
+// 获取客户类型图标类名
|
|
|
+const getCustomerIconClass = (type) => {
|
|
|
+ switch (type) {
|
|
|
+ case '散客':
|
|
|
+ return 'icon-individual'
|
|
|
+ case '团体':
|
|
|
+ return 'icon-group'
|
|
|
+ default:
|
|
|
+ return ''
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
-// 根据索引获取颜色
|
|
|
-const getColorByIndex = (index) => {
|
|
|
- const colors = ['#409EFF', '#67C23A', '#E6A23C', '#F56C6C', '#909399']
|
|
|
- return colors[index % colors.length]
|
|
|
+// 获取客户类型类名
|
|
|
+const getCustomerClass = (type) => {
|
|
|
+ switch (type) {
|
|
|
+ case '散客':
|
|
|
+ return 'individual-badge'
|
|
|
+ case '团体':
|
|
|
+ return 'group-badge'
|
|
|
+ default:
|
|
|
+ return ''
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
-// 获取统计数据
|
|
|
-const fetchData = async () => {
|
|
|
- try {
|
|
|
- const date = dayjs(selectedDate.value).format('YYYY-MM-DD')
|
|
|
- // 这里先使用模拟数据
|
|
|
- const mockData = {
|
|
|
- online: {
|
|
|
- miniProgram: 150,
|
|
|
- douyin: 200,
|
|
|
- meituan: 180,
|
|
|
- ctrip: 120
|
|
|
+// 初始化图表
|
|
|
+const initCharts = () => {
|
|
|
+ if (onlineChartRef.value) {
|
|
|
+ onlineChart = echarts.init(onlineChartRef.value)
|
|
|
+ }
|
|
|
+
|
|
|
+ if (offlineChartRef.value) {
|
|
|
+ offlineChart = echarts.init(offlineChartRef.value)
|
|
|
+ }
|
|
|
+
|
|
|
+ updateCharts()
|
|
|
+
|
|
|
+ window.addEventListener('resize', () => {
|
|
|
+ onlineChart?.resize()
|
|
|
+ offlineChart?.resize()
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// 更新图表
|
|
|
+const updateCharts = () => {
|
|
|
+ // 线上数据图表
|
|
|
+ if (onlineChart) {
|
|
|
+ const chartData = onlineData.value.map(item => ({
|
|
|
+ name: item.name,
|
|
|
+ value: item[onlineChartType.value]
|
|
|
+ }))
|
|
|
+
|
|
|
+ const colors = ['#FF4B91', '#FFCD4B', '#4BC3FF', '#4BFFB4']
|
|
|
+
|
|
|
+ onlineChart.setOption({
|
|
|
+ title: {
|
|
|
+ text: `线上售票${getChartTitle(onlineChartType.value)}分布`,
|
|
|
+ left: 'center',
|
|
|
+ textStyle: {
|
|
|
+ fontSize: 16,
|
|
|
+ fontWeight: 'normal'
|
|
|
+ }
|
|
|
},
|
|
|
- offline: {
|
|
|
- window1: 300,
|
|
|
- window2: 250
|
|
|
+ tooltip: {
|
|
|
+ trigger: 'item',
|
|
|
+ formatter: params => {
|
|
|
+ if (onlineChartType.value === 'salesAmount') {
|
|
|
+ return `${params.name}: ¥${params.value.toLocaleString()}`
|
|
|
+ }
|
|
|
+ return `${params.name}: ${params.value.toLocaleString()}人`
|
|
|
+ },
|
|
|
+ backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
|
|
+ borderColor: '#eee',
|
|
|
+ borderWidth: 1,
|
|
|
+ textStyle: {
|
|
|
+ color: '#333'
|
|
|
+ },
|
|
|
+ extraCssText: 'box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);'
|
|
|
},
|
|
|
- check: {
|
|
|
- yearCard: 50,
|
|
|
- normal: 800,
|
|
|
- discount: 200
|
|
|
+ legend: {
|
|
|
+ orient: 'horizontal',
|
|
|
+ bottom: 'bottom',
|
|
|
+ icon: 'circle',
|
|
|
+ itemWidth: 10,
|
|
|
+ itemHeight: 10,
|
|
|
+ textStyle: {
|
|
|
+ fontSize: 12
|
|
|
+ }
|
|
|
},
|
|
|
- income: {
|
|
|
- total: 50000,
|
|
|
- online: 30000,
|
|
|
- offline: 20000
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // 更新数据
|
|
|
- onlineData.value = [
|
|
|
- { label: '小程序', value: mockData.online.miniProgram },
|
|
|
- { label: '抖音', value: mockData.online.douyin },
|
|
|
- { label: '美团', value: mockData.online.meituan },
|
|
|
- { label: '携程', value: mockData.online.ctrip }
|
|
|
- ]
|
|
|
-
|
|
|
- offlineData.value = [
|
|
|
- { label: '窗口1', value: mockData.offline.window1 },
|
|
|
- { label: '窗口2', value: mockData.offline.window2 }
|
|
|
- ]
|
|
|
-
|
|
|
- checkData.value = [
|
|
|
- { label: '年卡', value: mockData.check.yearCard },
|
|
|
- { label: '普通票', value: mockData.check.normal },
|
|
|
- { label: '优惠票', value: mockData.check.discount }
|
|
|
- ]
|
|
|
-
|
|
|
- incomeData.value = {
|
|
|
- total: mockData.income.total,
|
|
|
- details: [
|
|
|
- { label: '线上收入', value: mockData.income.online },
|
|
|
- { label: '线下收入', value: mockData.income.offline }
|
|
|
- ]
|
|
|
- }
|
|
|
- } catch (error) {
|
|
|
- console.error('获取数据失败:', error)
|
|
|
+ series: [
|
|
|
+ {
|
|
|
+ name: getChartTitle(onlineChartType.value),
|
|
|
+ type: 'pie',
|
|
|
+ radius: ['40%', '70%'],
|
|
|
+ avoidLabelOverlap: false,
|
|
|
+ itemStyle: {
|
|
|
+ borderRadius: 10,
|
|
|
+ borderColor: '#fff',
|
|
|
+ borderWidth: 2
|
|
|
+ },
|
|
|
+ label: {
|
|
|
+ show: false,
|
|
|
+ position: 'center'
|
|
|
+ },
|
|
|
+ emphasis: {
|
|
|
+ label: {
|
|
|
+ show: true,
|
|
|
+ fontSize: 16,
|
|
|
+ fontWeight: 'bold'
|
|
|
+ },
|
|
|
+ itemStyle: {
|
|
|
+ shadowBlur: 10,
|
|
|
+ shadowOffsetX: 0,
|
|
|
+ shadowColor: 'rgba(0, 0, 0, 0.5)'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ labelLine: {
|
|
|
+ show: false
|
|
|
+ },
|
|
|
+ data: chartData
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ color: colors,
|
|
|
+ animation: true,
|
|
|
+ animationDuration: 1000,
|
|
|
+ animationEasing: 'cubicOut',
|
|
|
+ animationDelay: idx => idx * 100
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ // 线下数据图表
|
|
|
+ if (offlineChart) {
|
|
|
+ const chartData = offlineData.value.map(item => ({
|
|
|
+ name: item.type,
|
|
|
+ value: item[offlineChartType.value]
|
|
|
+ }))
|
|
|
+
|
|
|
+ offlineChart.setOption({
|
|
|
+ title: {
|
|
|
+ text: `线下购票${getChartTitle(offlineChartType.value)}分布`,
|
|
|
+ left: 'center',
|
|
|
+ textStyle: {
|
|
|
+ fontSize: 16,
|
|
|
+ fontWeight: 'normal'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ tooltip: {
|
|
|
+ trigger: 'axis',
|
|
|
+ axisPointer: {
|
|
|
+ type: 'shadow'
|
|
|
+ },
|
|
|
+ formatter: params => {
|
|
|
+ const param = params[0]
|
|
|
+ if (offlineChartType.value === 'salesAmount') {
|
|
|
+ return `${param.name}: ¥${param.value.toLocaleString()}`
|
|
|
+ }
|
|
|
+ return `${param.name}: ${param.value.toLocaleString()}人`
|
|
|
+ },
|
|
|
+ backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
|
|
+ borderColor: '#eee',
|
|
|
+ borderWidth: 1,
|
|
|
+ textStyle: {
|
|
|
+ color: '#333'
|
|
|
+ },
|
|
|
+ extraCssText: 'box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);'
|
|
|
+ },
|
|
|
+ xAxis: {
|
|
|
+ type: 'category',
|
|
|
+ data: chartData.map(item => item.name),
|
|
|
+ axisLine: {
|
|
|
+ lineStyle: {
|
|
|
+ color: '#ddd'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ axisTick: {
|
|
|
+ alignWithLabel: true
|
|
|
+ }
|
|
|
+ },
|
|
|
+ yAxis: {
|
|
|
+ type: 'value',
|
|
|
+ axisLine: {
|
|
|
+ show: false
|
|
|
+ },
|
|
|
+ axisTick: {
|
|
|
+ show: false
|
|
|
+ },
|
|
|
+ splitLine: {
|
|
|
+ lineStyle: {
|
|
|
+ color: '#eee'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ axisLabel: {
|
|
|
+ formatter: value => {
|
|
|
+ if (offlineChartType.value === 'salesAmount') {
|
|
|
+ return `¥${value}`
|
|
|
+ }
|
|
|
+ return value
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ series: [
|
|
|
+ {
|
|
|
+ name: getChartTitle(offlineChartType.value),
|
|
|
+ type: 'bar',
|
|
|
+ barWidth: '60%',
|
|
|
+ data: chartData.map(item => item.value),
|
|
|
+ itemStyle: {
|
|
|
+ color: function(params) {
|
|
|
+ const colorList = ['#36CFFF', '#36FFB5']
|
|
|
+ return colorList[params.dataIndex % colorList.length]
|
|
|
+ },
|
|
|
+ borderRadius: [5, 5, 0, 0]
|
|
|
+ },
|
|
|
+ emphasis: {
|
|
|
+ itemStyle: {
|
|
|
+ shadowBlur: 10,
|
|
|
+ shadowOffsetX: 0,
|
|
|
+ shadowColor: 'rgba(0, 0, 0, 0.5)'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ animation: true,
|
|
|
+ animationDuration: 1000,
|
|
|
+ animationEasing: 'cubicOut',
|
|
|
+ animationDelay: idx => idx * 100
|
|
|
+ })
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-const handleDateChange = () => {
|
|
|
- fetchData()
|
|
|
+// 获取图表标题
|
|
|
+const getChartTitle = (type) => {
|
|
|
+ switch (type) {
|
|
|
+ case 'salesAmount':
|
|
|
+ return '销售金额'
|
|
|
+ case 'reservedPeople':
|
|
|
+ return '预定人数'
|
|
|
+ case 'verifiedPeople':
|
|
|
+ return '验证人数'
|
|
|
+ default:
|
|
|
+ return ''
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
+// 生命周期钩子
|
|
|
onMounted(() => {
|
|
|
- fetchData()
|
|
|
+ checkMobile()
|
|
|
+ window.addEventListener('resize', checkMobile)
|
|
|
+
|
|
|
+ // 初始化加载数据
|
|
|
+ handleDateChange()
|
|
|
+
|
|
|
+ nextTick(() => {
|
|
|
+ initCharts()
|
|
|
+ })
|
|
|
+})
|
|
|
+
|
|
|
+// 监听移动设备状态变化,重新初始化图表
|
|
|
+watch(isMobile, () => {
|
|
|
+ nextTick(() => {
|
|
|
+ if (onlineChart) {
|
|
|
+ onlineChart.dispose()
|
|
|
+ }
|
|
|
+ if (offlineChart) {
|
|
|
+ offlineChart.dispose()
|
|
|
+ }
|
|
|
+ initCharts()
|
|
|
+ })
|
|
|
})
|
|
|
</script>
|
|
|
|
|
|
<style scoped>
|
|
|
-/* 整体容器样式 */
|
|
|
-.statistics-container {
|
|
|
- padding: 24px;
|
|
|
- max-width: 1280px;
|
|
|
+.ticket-statistics-container {
|
|
|
+ padding: 0;
|
|
|
+ max-width: 1200px;
|
|
|
margin: 0 auto;
|
|
|
+ font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
|
|
background-color: #f5f7fa;
|
|
|
min-height: 100vh;
|
|
|
}
|
|
|
|
|
|
-/* 页面标题 */
|
|
|
+.dashboard-header {
|
|
|
+ background: linear-gradient(135deg, #1e88e5 0%, #1565c0 100%);
|
|
|
+ color: white;
|
|
|
+ padding: 24px 20px;
|
|
|
+ border-radius: 0 0 20px 20px;
|
|
|
+ margin-bottom: 20px;
|
|
|
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
|
|
+}
|
|
|
+
|
|
|
+.header-content {
|
|
|
+ max-width: 1160px;
|
|
|
+ margin: 0 auto;
|
|
|
+}
|
|
|
+
|
|
|
.page-title {
|
|
|
- font-size: 28px;
|
|
|
- color: #303133;
|
|
|
text-align: center;
|
|
|
- margin-bottom: 24px;
|
|
|
+ color: white;
|
|
|
+ margin-bottom: 8px;
|
|
|
+ font-size: 28px;
|
|
|
font-weight: 600;
|
|
|
- position: relative;
|
|
|
- padding-bottom: 12px;
|
|
|
+ text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
|
|
}
|
|
|
|
|
|
-.page-title::after {
|
|
|
- content: '';
|
|
|
- position: absolute;
|
|
|
- bottom: 0;
|
|
|
- left: 50%;
|
|
|
- transform: translateX(-50%);
|
|
|
- width: 80px;
|
|
|
- height: 3px;
|
|
|
- background: linear-gradient(90deg, #409EFF, #67C23A);
|
|
|
- border-radius: 3px;
|
|
|
+.subtitle {
|
|
|
+ text-align: center;
|
|
|
+ color: rgba(255, 255, 255, 0.8);
|
|
|
+ font-size: 16px;
|
|
|
}
|
|
|
|
|
|
-/* 日期选择器 */
|
|
|
-.date-selector-card {
|
|
|
+.date-picker-container {
|
|
|
+ padding: 0 20px;
|
|
|
margin-bottom: 24px;
|
|
|
- border-radius: 8px;
|
|
|
- box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
|
|
}
|
|
|
|
|
|
-.date-selector-wrapper {
|
|
|
+.date-picker-card {
|
|
|
+ border-radius: 12px;
|
|
|
+ overflow: hidden;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.date-picker-card:hover {
|
|
|
+ transform: translateY(-2px);
|
|
|
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
|
|
+}
|
|
|
+
|
|
|
+.date-picker-header {
|
|
|
display: flex;
|
|
|
- justify-content: flex-end;
|
|
|
align-items: center;
|
|
|
+ margin-bottom: 12px;
|
|
|
+ font-weight: 500;
|
|
|
+ color: #1e88e5;
|
|
|
}
|
|
|
|
|
|
-.date-label {
|
|
|
- margin-right: 10px;
|
|
|
- font-weight: 500;
|
|
|
- color: #606266;
|
|
|
- font-size: 14px;
|
|
|
+.date-picker-header i {
|
|
|
+ margin-right: 8px;
|
|
|
}
|
|
|
|
|
|
-.date-picker {
|
|
|
- width: 220px;
|
|
|
+.custom-date-picker {
|
|
|
+ width: 100%;
|
|
|
}
|
|
|
|
|
|
-/* 统计卡片网格 */
|
|
|
-.statistics-grid {
|
|
|
- display: grid;
|
|
|
- grid-template-columns: repeat(auto-fit, minmax(380px, 1fr));
|
|
|
- gap: 24px;
|
|
|
+.statistics-overview {
|
|
|
+ padding: 0 20px;
|
|
|
+ margin-bottom: 24px;
|
|
|
}
|
|
|
|
|
|
-/* 统计卡片通用样式 */
|
|
|
-.statistics-card {
|
|
|
- border-radius: 8px;
|
|
|
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
|
|
|
- transition: transform 0.3s, box-shadow 0.3s;
|
|
|
+.overview-card {
|
|
|
+ border-radius: 12px;
|
|
|
overflow: hidden;
|
|
|
+ height: 100%;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: row;
|
|
|
+ align-items: center;
|
|
|
+ padding: 16px;
|
|
|
}
|
|
|
|
|
|
-.statistics-card:hover {
|
|
|
- transform: translateY(-5px);
|
|
|
+.overview-card:hover {
|
|
|
+ transform: translateY(-3px);
|
|
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
|
|
}
|
|
|
|
|
|
-.card-header {
|
|
|
- display: flex;
|
|
|
- justify-content: space-between;
|
|
|
- align-items: center;
|
|
|
- padding: 16px 20px;
|
|
|
- /*border-bottom: 1px solid #EBEEF5;*/
|
|
|
+.sales-card {
|
|
|
+ background: linear-gradient(135deg, #ff9a9e 0%, #fad0c4 100%);
|
|
|
+}
|
|
|
+
|
|
|
+.reserved-card {
|
|
|
+ background: linear-gradient(135deg, #a1c4fd 0%, #c2e9fb 100%);
|
|
|
+}
|
|
|
+
|
|
|
+.verified-card {
|
|
|
+ background: linear-gradient(135deg, #84fab0 0%, #8fd3f4 100%);
|
|
|
+}
|
|
|
+
|
|
|
+.refund-card {
|
|
|
+ background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%);
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+.card-content {
|
|
|
+ flex: 1;
|
|
|
}
|
|
|
|
|
|
.card-title {
|
|
|
- font-size: 18px;
|
|
|
+ font-size: 14px;
|
|
|
+ color: rgba(255, 255, 255, 0.8);
|
|
|
+ margin-bottom: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.card-value {
|
|
|
+ font-size: 24px;
|
|
|
font-weight: 600;
|
|
|
- color: #303133;
|
|
|
+ color: white;
|
|
|
+ margin-bottom: 8px;
|
|
|
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
|
|
}
|
|
|
|
|
|
-.card-content {
|
|
|
+
|
|
|
+.data-section {
|
|
|
+ margin: 0 20px 32px;
|
|
|
+ background-color: #fff;
|
|
|
+ border-radius: 12px;
|
|
|
padding: 20px;
|
|
|
+ box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
|
|
+ transition: all 0.3s ease;
|
|
|
}
|
|
|
|
|
|
-/* 数据部分通用样式 */
|
|
|
-.data-section {
|
|
|
- margin-bottom: 24px;
|
|
|
+.data-section:hover {
|
|
|
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
|
|
}
|
|
|
|
|
|
-.data-section:last-child {
|
|
|
- margin-bottom: 0;
|
|
|
+.section-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 16px;
|
|
|
+ padding-bottom: 12px;
|
|
|
+ border-bottom: 1px solid #ebeef5;
|
|
|
}
|
|
|
|
|
|
.section-title {
|
|
|
- font-size: 16px;
|
|
|
- color: #606266;
|
|
|
- margin-bottom: 16px;
|
|
|
- font-weight: 500;
|
|
|
+ font-size: 18px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #303133;
|
|
|
+ margin: 0;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
- /* gap: 8px; */
|
|
|
}
|
|
|
|
|
|
-.data-list {
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- /* gap: 12px; */
|
|
|
+.section-title::before {
|
|
|
+ content: '';
|
|
|
+ display: inline-block;
|
|
|
+ width: 4px;
|
|
|
+ height: 18px;
|
|
|
+ background: #1e88e5;
|
|
|
+ margin-right: 8px;
|
|
|
+ border-radius: 2px;
|
|
|
}
|
|
|
|
|
|
-/* 只展示需要修改的部分 */
|
|
|
-.data-item {
|
|
|
- display: flex;
|
|
|
- justify-content: space-between;
|
|
|
- padding: 14px 0;
|
|
|
- /* border-bottom: 1px dashed #EBEEF5; */
|
|
|
+.desktop-table {
|
|
|
+ margin-bottom: 24px;
|
|
|
}
|
|
|
|
|
|
-.data-item + .data-item {
|
|
|
- border-top: 1px dashed #EBEEF5;
|
|
|
+.custom-table {
|
|
|
+ border-radius: 8px;
|
|
|
+ overflow: hidden;
|
|
|
}
|
|
|
|
|
|
-/* 修改总计项样式,使其独立于 data-item */
|
|
|
-.total-item {
|
|
|
+.table-cell-with-icon {
|
|
|
display: flex;
|
|
|
- justify-content: space-between;
|
|
|
- margin-top: 8px;
|
|
|
- padding-top: 12px;
|
|
|
- padding-bottom: 8px;
|
|
|
- border-top: 1px solid #EBEEF5;
|
|
|
+ align-items: center;
|
|
|
}
|
|
|
|
|
|
-.label {
|
|
|
- color: #606266;
|
|
|
- font-size: 14px;
|
|
|
+.distributor-icon, .customer-icon {
|
|
|
+ display: inline-block;
|
|
|
+ width: 24px;
|
|
|
+ height: 24px;
|
|
|
+ margin-right: 8px;
|
|
|
+ background-color: #f0f2f5;
|
|
|
+ border-radius: 4px;
|
|
|
+ position: relative;
|
|
|
}
|
|
|
|
|
|
-.value {
|
|
|
- font-weight: 500;
|
|
|
- color: #303133;
|
|
|
- font-size: 14px;
|
|
|
+.icon-douyin::before {
|
|
|
+ content: '抖';
|
|
|
+ position: absolute;
|
|
|
+ top: 50%;
|
|
|
+ left: 50%;
|
|
|
+ transform: translate(-50%, -50%);
|
|
|
+ color: #ff4b91;
|
|
|
+ font-weight: bold;
|
|
|
+ font-size: 12px;
|
|
|
}
|
|
|
|
|
|
-.highlight-value {
|
|
|
- color: #409EFF;
|
|
|
- font-weight: 600;
|
|
|
+.icon-meituan::before {
|
|
|
+ content: '美';
|
|
|
+ position: absolute;
|
|
|
+ top: 50%;
|
|
|
+ left: 50%;
|
|
|
+ transform: translate(-50%, -50%);
|
|
|
+ color: #ffcd4b;
|
|
|
+ font-weight: bold;
|
|
|
+ font-size: 12px;
|
|
|
}
|
|
|
|
|
|
-.total-value {
|
|
|
- font-size: 18px;
|
|
|
- font-weight: 700;
|
|
|
- color: #303133;
|
|
|
+.icon-xiecheng::before {
|
|
|
+ content: '携';
|
|
|
+ position: absolute;
|
|
|
+ top: 50%;
|
|
|
+ left: 50%;
|
|
|
+ transform: translate(-50%, -50%);
|
|
|
+ color: #4bc3ff;
|
|
|
+ font-weight: bold;
|
|
|
+ font-size: 12px;
|
|
|
}
|
|
|
|
|
|
-/* 售票卡片特殊样式 */
|
|
|
-.sales-card {
|
|
|
- border-top: 4px solid #67C23A;
|
|
|
+.icon-xiaochengxu::before {
|
|
|
+ content: '小';
|
|
|
+ position: absolute;
|
|
|
+ top: 50%;
|
|
|
+ left: 50%;
|
|
|
+ transform: translate(-50%, -50%);
|
|
|
+ color: #4bffb4;
|
|
|
+ font-weight: bold;
|
|
|
+ font-size: 12px;
|
|
|
}
|
|
|
|
|
|
-/* 检票卡片特殊样式 */
|
|
|
-.check-card {
|
|
|
- border-top: 4px solid #E6A23C;
|
|
|
+.icon-individual::before {
|
|
|
+ content: '散';
|
|
|
+ position: absolute;
|
|
|
+ top: 50%;
|
|
|
+ left: 50%;
|
|
|
+ transform: translate(-50%, -50%);
|
|
|
+ color: #36cfff;
|
|
|
+ font-weight: bold;
|
|
|
+ font-size: 12px;
|
|
|
}
|
|
|
|
|
|
-.check-data-container {
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- gap: 20px;
|
|
|
+.icon-group::before {
|
|
|
+ content: '团';
|
|
|
+ position: absolute;
|
|
|
+ top: 50%;
|
|
|
+ left: 50%;
|
|
|
+ transform: translate(-50%, -50%);
|
|
|
+ color: #36ffb5;
|
|
|
+ font-weight: bold;
|
|
|
+ font-size: 12px;
|
|
|
}
|
|
|
|
|
|
-.check-chart {
|
|
|
- margin-bottom: 16px;
|
|
|
+.sales-amount-cell {
|
|
|
+ color: #f56c6c;
|
|
|
+ font-weight: 500;
|
|
|
}
|
|
|
|
|
|
-.chart-placeholder {
|
|
|
+.mobile-cards {
|
|
|
display: flex;
|
|
|
- height: 40px;
|
|
|
- border-radius: 20px;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 16px;
|
|
|
+ margin-bottom: 24px;
|
|
|
+}
|
|
|
+
|
|
|
+.mobile-data-card {
|
|
|
+ width: 100%;
|
|
|
+ border-radius: 12px;
|
|
|
overflow: hidden;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.mobile-data-card:hover {
|
|
|
+ transform: translateY(-2px);
|
|
|
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
|
|
|
+}
|
|
|
+
|
|
|
+.card-header {
|
|
|
margin-bottom: 16px;
|
|
|
}
|
|
|
|
|
|
-.chart-circle {
|
|
|
- height: 100%;
|
|
|
- display: flex;
|
|
|
+.distributor-badge, .customer-badge {
|
|
|
+ display: inline-flex;
|
|
|
align-items: center;
|
|
|
- justify-content: center;
|
|
|
- color: white;
|
|
|
- font-size: 12px;
|
|
|
- font-weight: 500;
|
|
|
- transition: all 0.3s;
|
|
|
+ padding: 6px 12px;
|
|
|
+ border-radius: 20px;
|
|
|
+ background-color: #f5f7fa;
|
|
|
}
|
|
|
|
|
|
-.label-with-color {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 8px;
|
|
|
+.douyin-badge {
|
|
|
+ background-color: rgba(255, 75, 145, 0.1);
|
|
|
+ color: #ff4b91;
|
|
|
}
|
|
|
|
|
|
-.color-dot {
|
|
|
- width: 12px;
|
|
|
- height: 12px;
|
|
|
- border-radius: 50%;
|
|
|
- display: inline-block;
|
|
|
+.meituan-badge {
|
|
|
+ background-color: rgba(255, 205, 75, 0.1);
|
|
|
+ color: #ffcd4b;
|
|
|
}
|
|
|
|
|
|
-/* 收入卡片特殊样式 */
|
|
|
-.income-card {
|
|
|
- border-top: 4px solid #409EFF;
|
|
|
+.xiecheng-badge {
|
|
|
+ background-color: rgba(75, 195, 255, 0.1);
|
|
|
+ color: #4bc3ff;
|
|
|
}
|
|
|
|
|
|
-.total-income {
|
|
|
- text-align: center;
|
|
|
- padding: 24px 0;
|
|
|
- margin-bottom: 24px;
|
|
|
- background: linear-gradient(135deg, #ecf5ff, #f0f9eb);
|
|
|
- border-radius: 8px;
|
|
|
- position: relative;
|
|
|
+.xiaochengxu-badge {
|
|
|
+ background-color: rgba(75, 255, 180, 0.1);
|
|
|
+ color: #4bffb4;
|
|
|
}
|
|
|
|
|
|
-.income-icon {
|
|
|
- font-size: 24px;
|
|
|
- color: #409EFF;
|
|
|
- margin-bottom: 8px;
|
|
|
- font-weight: bold;
|
|
|
+.individual-badge {
|
|
|
+ background-color: rgba(54, 207, 255, 0.1);
|
|
|
+ color: #36cfff;
|
|
|
}
|
|
|
|
|
|
-.income-amount {
|
|
|
- font-size: 32px;
|
|
|
- font-weight: 700;
|
|
|
- color: #409EFF;
|
|
|
- margin-bottom: 8px;
|
|
|
+.group-badge {
|
|
|
+ background-color: rgba(54, 255, 181, 0.1);
|
|
|
+ color: #36ffb5;
|
|
|
}
|
|
|
|
|
|
-.income-label {
|
|
|
- font-size: 14px;
|
|
|
- color: #606266;
|
|
|
+.card-grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(3, 1fr);
|
|
|
+ gap: 16px;
|
|
|
+ margin-bottom: 16px;
|
|
|
}
|
|
|
|
|
|
-.income-details {
|
|
|
- margin-top: 16px;
|
|
|
+.grid-item {
|
|
|
+ text-align: center;
|
|
|
+ padding: 8px;
|
|
|
+ border-radius: 8px;
|
|
|
+ background-color: #f5f7fa;
|
|
|
+ transition: all 0.3s ease;
|
|
|
}
|
|
|
|
|
|
-.income-value {
|
|
|
- color: #67C23A;
|
|
|
- font-weight: 600;
|
|
|
+.grid-item:hover {
|
|
|
+ background-color: #e6f7ff;
|
|
|
+ transform: translateY(-2px);
|
|
|
}
|
|
|
|
|
|
-.income-ratio-bar {
|
|
|
- height: 24px;
|
|
|
- display: flex;
|
|
|
- border-radius: 12px;
|
|
|
- overflow: hidden;
|
|
|
- margin: 16px 0;
|
|
|
+.item-label {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #909399;
|
|
|
+ margin-bottom: 4px;
|
|
|
}
|
|
|
|
|
|
-.ratio-segment {
|
|
|
- height: 100%;
|
|
|
+.item-value {
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #303133;
|
|
|
+}
|
|
|
+
|
|
|
+.sales-amount {
|
|
|
display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
align-items: center;
|
|
|
- justify-content: center;
|
|
|
- color: white;
|
|
|
- font-size: 12px;
|
|
|
- font-weight: 500;
|
|
|
- transition: width 0.5s;
|
|
|
+ padding: 12px;
|
|
|
+ border-radius: 8px;
|
|
|
+ background-color: #fff9f9;
|
|
|
+ border: 1px dashed #ffcdd2;
|
|
|
}
|
|
|
|
|
|
-.online-segment {
|
|
|
- background-color: #409EFF;
|
|
|
+.amount-label {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #606266;
|
|
|
}
|
|
|
|
|
|
-.offline-segment {
|
|
|
- background-color: #67C23A;
|
|
|
+.amount-value {
|
|
|
+ font-size: 18px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #f56c6c;
|
|
|
}
|
|
|
|
|
|
-.ratio-legend {
|
|
|
- display: flex;
|
|
|
- justify-content: center;
|
|
|
- gap: 24px;
|
|
|
- margin-top: 8px;
|
|
|
+.chart-container {
|
|
|
+ margin-top: 24px;
|
|
|
}
|
|
|
|
|
|
-.legend-item {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 6px;
|
|
|
- font-size: 12px;
|
|
|
+.custom-tabs :deep(.el-tabs__nav-wrap::after) {
|
|
|
+ height: 1px;
|
|
|
+ background-color: #ebeef5;
|
|
|
+}
|
|
|
+
|
|
|
+.custom-tabs :deep(.el-tabs__item) {
|
|
|
+ font-size: 14px;
|
|
|
color: #606266;
|
|
|
+ padding: 0 16px;
|
|
|
}
|
|
|
|
|
|
-.online-dot {
|
|
|
- background-color: #409EFF;
|
|
|
+.custom-tabs :deep(.el-tabs__item.is-active) {
|
|
|
+ color: #1e88e5;
|
|
|
+ font-weight: 500;
|
|
|
}
|
|
|
|
|
|
-.offline-dot {
|
|
|
- background-color: #67C23A;
|
|
|
+.custom-tabs :deep(.el-tabs__active-bar) {
|
|
|
+ background-color: #1e88e5;
|
|
|
+ height: 3px;
|
|
|
+ border-radius: 3px;
|
|
|
}
|
|
|
|
|
|
-/* 响应式设计 */
|
|
|
-@media (max-width: 1200px) {
|
|
|
- .statistics-grid {
|
|
|
- grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
|
|
- }
|
|
|
+.chart {
|
|
|
+ height: 300px;
|
|
|
+ width: 100%;
|
|
|
+ margin-top: 16px;
|
|
|
}
|
|
|
|
|
|
-@media (max-width: 768px) {
|
|
|
- .statistics-container {
|
|
|
- padding: 16px;
|
|
|
+.dashboard-footer {
|
|
|
+ background-color: #f5f7fa;
|
|
|
+ padding: 20px;
|
|
|
+ text-align: center;
|
|
|
+ color: #909399;
|
|
|
+ font-size: 12px;
|
|
|
+ border-top: 1px solid #ebeef5;
|
|
|
+ margin-top: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 响应式调整 */
|
|
|
+@media (max-width: 767px) {
|
|
|
+ .ticket-statistics-container {
|
|
|
+ padding: 0;
|
|
|
}
|
|
|
|
|
|
- .statistics-grid {
|
|
|
- grid-template-columns: 1fr;
|
|
|
- gap: 16px;
|
|
|
+ .dashboard-header {
|
|
|
+ padding: 16px;
|
|
|
+ border-radius: 0 0 16px 16px;
|
|
|
}
|
|
|
|
|
|
.page-title {
|
|
|
- font-size: 24px;
|
|
|
+ font-size: 20px;
|
|
|
+ margin-bottom: 4px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .subtitle {
|
|
|
+ font-size: 14px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .date-picker-container,
|
|
|
+ .statistics-overview {
|
|
|
+ padding: 0 12px;
|
|
|
+ margin-bottom: 16px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .data-section {
|
|
|
+ margin: 0 12px 24px;
|
|
|
+ padding: 16px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .card-value {
|
|
|
+ font-size: 20px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .chart {
|
|
|
+ height: 250px;
|
|
|
}
|
|
|
|
|
|
- .card-title {
|
|
|
+ .section-title {
|
|
|
font-size: 16px;
|
|
|
}
|
|
|
|
|
|
- .income-amount {
|
|
|
- font-size: 28px;
|
|
|
+ .card-grid {
|
|
|
+ grid-template-columns: repeat(2, 1fr);
|
|
|
+ gap: 12px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .grid-item {
|
|
|
+ padding: 6px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .item-value {
|
|
|
+ font-size: 14px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .amount-value {
|
|
|
+ font-size: 16px;
|
|
|
}
|
|
|
}
|
|
|
</style>
|