Browse Source

改成接口获取数据

gcz 3 weeks ago
parent
commit
651bb77dc9
11 changed files with 1754 additions and 457 deletions
  1. 8 0
      .env
  2. 10 0
      .env.development
  3. 11 0
      .env.production
  4. 11 0
      .env.test
  5. 1 0
      .gitignore
  6. 2 1
      package.json
  7. 2 2
      src/api/statistics.js
  8. 2 2
      src/utils/request.js
  9. 652 0
      src/views/Statistics copy.vue
  10. 1030 442
      src/views/Statistics.vue
  11. 25 10
      vite.config.js

+ 8 - 0
.env

@@ -0,0 +1,8 @@
+# 接口基础路径
+VITE_API_BASE_URL=/fhss-api
+
+# 项目标题
+VITE_APP_TITLE=AI助手
+
+# 接口超时时间
+VITE_API_TIMEOUT=15000 

+ 10 - 0
.env.development

@@ -0,0 +1,10 @@
+# 开发环境
+NODE_ENV=development
+
+# 接口基础路径
+VITE_API_BASE_URL=/fhss-api
+
+# 是否开启代理
+VITE_USE_PROXY=true
+VITE_PROXY_TARGET=https://fhsstatistic.dev.dazesoft.cn/fhss-api
+# VITE_PROXY_TARGET=http://172.16.90.111:8080

+ 11 - 0
.env.production

@@ -0,0 +1,11 @@
+# 生产环境
+NODE_ENV=production
+
+# 接口基础路径
+VITE_API_BASE_URL=https://fhsadmin.dev.dazesoft.cn/fhss-api
+
+# 是否删除console
+VITE_DROP_CONSOLE=true
+
+# 是否启用gzip压缩
+VITE_USE_COMPRESSION=true 

+ 11 - 0
.env.test

@@ -0,0 +1,11 @@
+# 测试环境
+NODE_ENV=test
+
+# 接口基础路径
+VITE_API_BASE_URL=http://test-api.example.com
+
+# 是否删除console
+VITE_DROP_CONSOLE=false
+
+# 是否启用gzip压缩
+VITE_USE_COMPRESSION=true 

+ 1 - 0
.gitignore

@@ -3,6 +3,7 @@ node_modules/
 dist/
 dist-prod/
 dist-staging/
+dist-test/
 npm-debug.log*
 yarn-debug.log*
 yarn-error.log*

+ 2 - 1
package.json

@@ -5,7 +5,8 @@
   "type": "module",
   "scripts": {
     "dev": "vite",
-    "build": "vite build",
+    "build:test": "vite build --mode development",
+    "build:prod": "vite build --mode production",
     "preview": "vite preview"
   },
   "dependencies": {

+ 2 - 2
src/api/statistics.js

@@ -3,8 +3,8 @@ import request from '@/utils/request'
 // 获取统计数据
 export function getStatisticsData(date) {
   return request({
-    url: '/statistics',
+    url: '/order/orderReport/ticket',
     method: 'get',
-    params: { date }
+    params: date
   })
 } 

+ 2 - 2
src/utils/request.js

@@ -3,7 +3,7 @@ import { ElMessage } from 'element-plus'
 
 // 创建 axios 实例
 const service = axios.create({
-  baseURL: '/api', // 基础URL
+  baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
   timeout: 10000, // 请求超时时间
   headers: {
     'Content-Type': 'application/json'
@@ -28,7 +28,7 @@ service.interceptors.response.use(
     const res = response.data
     // 这里可以根据后端返回的状态码进行判断
     if (res.code === 200) {
-      return res.data
+      return res
     } else {
       ElMessage.error(res.message || '请求失败')
       return Promise.reject(new Error(res.message || '请求失败'))

+ 652 - 0
src/views/Statistics copy.vue

@@ -0,0 +1,652 @@
+<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>
+    </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>
+              <div class="total-item">
+                <span class="label">线上总计</span>
+                <span class="value total-value">{{ getTotalOnline() }}</span>
+              </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>
+              </div>
+              <div class="total-item">
+                <span class="label">线下总计</span>
+                <span class="value total-value">{{ getTotalOffline() }}</span>
+              </div>
+            </div>
+          </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>
+              </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>
+              <div class="total-item">
+                <span class="label">检票总计</span>
+                <span class="value total-value">{{ getTotalCheck() }}</span>
+              </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>
+            
+          </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 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>
+              <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>
+            </div>
+            <div class="ratio-legend">
+              <div class="legend-item">
+                <span class="color-dot online-dot"></span>
+                <span>线上收入</span>
+              </div>
+              <div class="legend-item">
+                <span class="color-dot offline-dot"></span>
+                <span>线下收入</span>
+              </div>
+            </div>
+          </div>
+        </div>
+      </el-card>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted, computed } from 'vue'
+import { getStatisticsData } from '@/api/statistics'
+import dayjs from 'dayjs'
+
+// 日期选择
+const selectedDate = ref(new Date())
+const dateShortcuts = [
+  {
+    text: '今天',
+    value: new Date(),
+  },
+  {
+    text: '昨天',
+    value: () => {
+      const date = new Date()
+      date.setTime(date.getTime() - 3600 * 1000 * 24)
+      return date
+    },
+  },
+  {
+    text: '上周同日',
+    value: () => {
+      const date = new Date()
+      date.setTime(date.getTime() - 3600 * 1000 * 24 * 7)
+      return date
+    },
+  },
+]
+
+// 模拟数据
+const onlineData = ref([
+  { label: '小程序', value: 0 },
+  { label: '抖音', value: 0 },
+  { label: '美团', value: 0 },
+  { label: '携程', value: 0 }
+])
+
+const offlineData = ref([
+  { label: '窗口1', value: 0 },
+  { label: '窗口2', value: 0 }
+])
+
+const checkData = ref([
+  { label: '年卡', value: 0 },
+  { label: '普通票', value: 0 },
+  { label: '优惠票', value: 0 }
+])
+
+const incomeData = ref({
+  total: 0,
+  details: [
+    { label: '线上收入', value: 0 },
+    { label: '线下收入', value: 0 }
+  ]
+})
+
+// 计算总数的方法
+const getTotalOnline = () => {
+  return onlineData.value.reduce((sum, item) => sum + item.value, 0)
+}
+
+const getTotalOffline = () => {
+  return offlineData.value.reduce((sum, item) => sum + item.value, 0)
+}
+
+const getTotalCheck = () => {
+  return checkData.value.reduce((sum, item) => sum + item.value, 0)
+}
+
+// 格式化货币
+const formatCurrency = (value) => {
+  return value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
+}
+
+// 获取百分比
+const getPercentage = (value, total) => {
+  if (total === 0) return 0
+  return Math.max(10, Math.round((value / total) * 100))
+}
+
+// 根据索引获取颜色
+const getColorByIndex = (index) => {
+  const colors = ['#409EFF', '#67C23A', '#E6A23C', '#F56C6C', '#909399']
+  return colors[index % colors.length]
+}
+
+// 获取统计数据
+const fetchData = async () => {
+  try {
+    const date = dayjs(selectedDate.value).format('YYYY-MM-DD')
+    // 这里先使用模拟数据
+    const mockData = {
+      online: {
+        miniProgram: 150,
+        douyin: 200,
+        meituan: 180,
+        ctrip: 120
+      },
+      offline: {
+        window1: 300,
+        window2: 250
+      },
+      check: {
+        yearCard: 50,
+        normal: 800,
+        discount: 200
+      },
+      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)
+  }
+}
+
+const handleDateChange = () => {
+  fetchData()
+}
+
+onMounted(() => {
+  fetchData()
+})
+</script>
+
+<style scoped>
+/* 整体容器样式 */
+.statistics-container {
+  padding: 24px;
+  max-width: 1280px;
+  margin: 0 auto;
+  background-color: #f5f7fa;
+  min-height: 100vh;
+}
+
+/* 页面标题 */
+.page-title {
+  font-size: 28px;
+  color: #303133;
+  text-align: center;
+  margin-bottom: 24px;
+  font-weight: 600;
+  position: relative;
+  padding-bottom: 12px;
+}
+
+.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;
+}
+
+/* 日期选择器 */
+.date-selector-card {
+  margin-bottom: 24px;
+  border-radius: 8px;
+  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
+}
+
+.date-selector-wrapper {
+  display: flex;
+  justify-content: flex-end;
+  align-items: center;
+}
+
+.date-label {
+  margin-right: 10px;
+  font-weight: 500;
+  color: #606266;
+  font-size: 14px;
+}
+
+.date-picker {
+  width: 220px;
+}
+
+/* 统计卡片网格 */
+.statistics-grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fit, minmax(380px, 1fr));
+  gap: 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;
+  overflow: hidden;
+}
+
+.statistics-card:hover {
+  transform: translateY(-5px);
+  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;*/
+}
+
+.card-title {
+  font-size: 18px;
+  font-weight: 600;
+  color: #303133;
+}
+
+.card-content {
+  padding: 20px;
+}
+
+/* 数据部分通用样式 */
+.data-section {
+  margin-bottom: 24px;
+}
+
+.data-section:last-child {
+  margin-bottom: 0;
+}
+
+.section-title {
+  font-size: 16px;
+  color: #606266;
+  margin-bottom: 16px;
+  font-weight: 500;
+  display: flex;
+  align-items: center;
+  /* gap: 8px; */
+}
+
+.data-list {
+  display: flex;
+  flex-direction: column;
+  /* gap: 12px; */
+}
+
+/* 只展示需要修改的部分 */
+.data-item {
+  display: flex;
+  justify-content: space-between;
+  padding: 14px 0;
+  /* border-bottom: 1px dashed #EBEEF5; */
+}
+
+.data-item + .data-item {
+  border-top: 1px dashed #EBEEF5;
+}
+
+/* 修改总计项样式,使其独立于 data-item */
+.total-item {
+  display: flex;
+  justify-content: space-between;
+  margin-top: 8px;
+  padding-top: 12px;
+  padding-bottom: 8px;
+  border-top: 1px solid #EBEEF5;
+}
+
+.label {
+  color: #606266;
+  font-size: 14px;
+}
+
+.value {
+  font-weight: 500;
+  color: #303133;
+  font-size: 14px;
+}
+
+.highlight-value {
+  color: #409EFF;
+  font-weight: 600;
+}
+
+.total-value {
+  font-size: 18px;
+  font-weight: 700;
+  color: #303133;
+}
+
+/* 售票卡片特殊样式 */
+.sales-card {
+  border-top: 4px solid #67C23A;
+}
+
+/* 检票卡片特殊样式 */
+.check-card {
+  border-top: 4px solid #E6A23C;
+}
+
+.check-data-container {
+  display: flex;
+  flex-direction: column;
+  gap: 20px;
+}
+
+.check-chart {
+  margin-bottom: 16px;
+}
+
+.chart-placeholder {
+  display: flex;
+  height: 40px;
+  border-radius: 20px;
+  overflow: hidden;
+  margin-bottom: 16px;
+}
+
+.chart-circle {
+  height: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: white;
+  font-size: 12px;
+  font-weight: 500;
+  transition: all 0.3s;
+}
+
+.label-with-color {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.color-dot {
+  width: 12px;
+  height: 12px;
+  border-radius: 50%;
+  display: inline-block;
+}
+
+/* 收入卡片特殊样式 */
+.income-card {
+  border-top: 4px solid #409EFF;
+}
+
+.total-income {
+  text-align: center;
+  padding: 24px 0;
+  margin-bottom: 24px;
+  background: linear-gradient(135deg, #ecf5ff, #f0f9eb);
+  border-radius: 8px;
+  position: relative;
+}
+
+.income-icon {
+  font-size: 24px;
+  color: #409EFF;
+  margin-bottom: 8px;
+  font-weight: bold;
+}
+
+.income-amount {
+  font-size: 32px;
+  font-weight: 700;
+  color: #409EFF;
+  margin-bottom: 8px;
+}
+
+.income-label {
+  font-size: 14px;
+  color: #606266;
+}
+
+.income-details {
+  margin-top: 16px;
+}
+
+.income-value {
+  color: #67C23A;
+  font-weight: 600;
+}
+
+.income-ratio-bar {
+  height: 24px;
+  display: flex;
+  border-radius: 12px;
+  overflow: hidden;
+  margin: 16px 0;
+}
+
+.ratio-segment {
+  height: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: white;
+  font-size: 12px;
+  font-weight: 500;
+  transition: width 0.5s;
+}
+
+.online-segment {
+  background-color: #409EFF;
+}
+
+.offline-segment {
+  background-color: #67C23A;
+}
+
+.ratio-legend {
+  display: flex;
+  justify-content: center;
+  gap: 24px;
+  margin-top: 8px;
+}
+
+.legend-item {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  font-size: 12px;
+  color: #606266;
+}
+
+.online-dot {
+  background-color: #409EFF;
+}
+
+.offline-dot {
+  background-color: #67C23A;
+}
+
+/* 响应式设计 */
+@media (max-width: 1200px) {
+  .statistics-grid {
+    grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
+  }
+}
+
+@media (max-width: 768px) {
+  .statistics-container {
+    padding: 16px;
+  }
+  
+  .statistics-grid {
+    grid-template-columns: 1fr;
+    gap: 16px;
+  }
+  
+  .page-title {
+    font-size: 24px;
+  }
+  
+  .card-title {
+    font-size: 16px;
+  }
+  
+  .income-amount {
+    font-size: 28px;
+  }
+}
+</style>

+ 1030 - 442
src/views/Statistics.vue

@@ -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>

+ 25 - 10
vite.config.js

@@ -1,16 +1,31 @@
-import { defineConfig } from 'vite'
+import { defineConfig, loadEnv } from 'vite'
 import vue from '@vitejs/plugin-vue'
 import path from 'path'
 
-export default defineConfig({
-  plugins: [vue()],
-  resolve: {
-    alias: {
-      '@': path.resolve(__dirname, './src')
+export default defineConfig(({ mode }) => {
+  const env = loadEnv(mode, process.cwd())
+  const { VITE_USE_PROXY, VITE_API_BASE_URL, VITE_PROXY_TARGET } = env
+  
+  return {
+    plugins: [vue()],
+    resolve: {
+      alias: {
+        '@': path.resolve(__dirname, './src')
+      }
+    },
+    build: {
+      outDir: mode === 'production' ? 'dist-prod' : 'dist-test'
+    },
+    server: {
+      port: 3000,
+      open: true,
+      proxy: VITE_USE_PROXY === 'true' ? {
+        [VITE_API_BASE_URL]: {
+          target: VITE_PROXY_TARGET,
+          changeOrigin: true,
+          rewrite: (path) => path.replace(new RegExp('^' + VITE_API_BASE_URL), '')
+        }
+      } : {}
     }
-  },
-  server: {
-    port: 3000,
-    open: true
   }
 })