放心行(约客多 2026/6/22 最新版本改版)
hejianhao
2026-06-24 e1e64c1931dc14072cc04bc6d3ec39177dd5f8b4
feat: 搭建可视化数据监控平台并更新相关资源

- 替换站点图标为logo.jpg,删除旧的logo.png
- 新增近20张静态资源图片,覆盖首页、路由项、图表背景等场景
- 重构axios请求工具,提取基础URL和超时配置为常量
- 新增手机号脱敏、字符串脱敏、时间格式化工具函数
- 实现统计数据大盘、路由项轮播页面,封装通用标题组件TitleBox
- 集成多款ECharts图表实现数据可视化展示
- 新增API请求服务层统一管理后端接口
- 更新路由配置,将默认首页替换为统计大盘页面
- 删除无用的旧空首页组件,调整App.vue大屏适配配置
2个文件已删除
1 文件已重命名
28个文件已添加
5个文件已修改
2263 ■■■■■ 已修改文件
screen/public/index.html 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
screen/public/logo.jpg 补丁 | 查看 | 原始文档 | blame | 历史
screen/public/logo.png 补丁 | 查看 | 原始文档 | blame | 历史
screen/src/App.vue 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
screen/src/assets/img/home/history-icon.png 补丁 | 查看 | 原始文档 | blame | 历史
screen/src/assets/img/home/home-title-bg.png 补丁 | 查看 | 原始文档 | blame | 历史
screen/src/assets/img/home/home-top-bg.png 补丁 | 查看 | 原始文档 | blame | 历史
screen/src/assets/img/home/passenger-rank-1.png 补丁 | 查看 | 原始文档 | blame | 历史
screen/src/assets/img/home/passenger-rank-2.png 补丁 | 查看 | 原始文档 | blame | 历史
screen/src/assets/img/home/passenger-rank-3.png 补丁 | 查看 | 原始文档 | blame | 历史
screen/src/assets/img/home/passenger-rank-4.png 补丁 | 查看 | 原始文档 | blame | 历史
screen/src/assets/img/home/totalOrderChart.png 补丁 | 查看 | 原始文档 | blame | 历史
screen/src/assets/img/route-item/item-bottom-bg-icon.png 补丁 | 查看 | 原始文档 | blame | 历史
screen/src/assets/img/route-item/route-item-top-bg.png 补丁 | 查看 | 原始文档 | blame | 历史
screen/src/components/TitleBox.vue 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
screen/src/router/index.js 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
screen/src/utils/request.js 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
screen/src/utils/time.js 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
screen/src/utils/utils.js 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
screen/src/view/home/index.vue 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
screen/src/view/route-item/index.less 168 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
screen/src/view/route-item/index.vue 221 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
screen/src/view/route-item/service.js 补丁 | 查看 | 原始文档 | blame | 历史
screen/src/view/statistical-table/charts/dayOrderVolume.js 141 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
screen/src/view/statistical-table/charts/newPassenger.js 140 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
screen/src/view/statistical-table/charts/orderVolume.js 124 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
screen/src/view/statistical-table/charts/totalOrder.js 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
screen/src/view/statistical-table/charts/yearTrend.js 128 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
screen/src/view/statistical-table/index.vue 594 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
screen/src/view/statistical-table/styles/day-order-volume.less 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
screen/src/view/statistical-table/styles/layout.less 91 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
screen/src/view/statistical-table/styles/new-passenger.less 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
screen/src/view/statistical-table/styles/order-volume.less 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
screen/src/view/statistical-table/styles/passenger-rank.less 158 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
screen/src/view/statistical-table/styles/total-order.less 144 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
screen/src/view/statistical-table/styles/year-trend.less 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
screen/public/index.html
@@ -6,15 +6,8 @@
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width,initial-scale=1.0">
  <meta name="viewport" content="initial-scale=1.0, user-scalable=no, width=device-width" />
  <script src="https://cache.amap.com/lbs/static/es5.min.js"></script>
  <script type="text/javascript">
    window._AMapSecurityConfig = {
      securityJsCode: "c75eccf0742998b2ea43823e08cacccd",
    };
  </script>
  <script src="https://webapi.amap.com/loader.js"></script>
  <link rel="icon" href="./logo.png">
  <title>放心行</title>
  <link rel="icon" href="./logo.jpg">
   <title>放心行</title>
</head>
<body>
screen/public/logo.jpg
screen/public/logo.png
Binary files differ
screen/src/App.vue
@@ -18,7 +18,6 @@
      dw: 1920,
      el: '#app',
      resize: true,
      ignore: ['#container']
    })
  },
  created () {}
screen/src/assets/img/home/history-icon.png
screen/src/assets/img/home/home-title-bg.png
screen/src/assets/img/home/home-top-bg.png
screen/src/assets/img/home/passenger-rank-1.png
screen/src/assets/img/home/passenger-rank-2.png
screen/src/assets/img/home/passenger-rank-3.png
screen/src/assets/img/home/passenger-rank-4.png
screen/src/assets/img/home/totalOrderChart.png
screen/src/assets/img/route-item/item-bottom-bg-icon.png
screen/src/assets/img/route-item/route-item-top-bg.png
screen/src/components/TitleBox.vue
New file
@@ -0,0 +1,48 @@
<template>
  <div class="title-box">
    <div class="left-slot">
      <slot name="left"></slot>
    </div>
    <div class="right-slot">
      <slot name="right"></slot>
    </div>
  </div>
</template>
<script>
export default {
  name: 'TitleBox'
}
</script>
<style lang="less" scoped>
.title-box {
  width: 100%;
  height: 50px;
  background: url('~@/assets/img/home/home-title-bg.png') no-repeat center/100% 100%;
  display: flex;
  justify-content: space-between;
  align-items: center;
  box-sizing: border-box;
}
.left-slot {
  margin-top: 6px;
  margin-left: 20px;
  font-family: PangMenZhengDao;
  font-size: 30px;
  font-weight: normal;
  line-height: normal;
  letter-spacing: 0em;
  background: linear-gradient(180deg, #ffffff 0%, #2bbaf8 100%);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
  text-fill-color: transparent;
}
.right-slot {
  margin-right: 18px;
  flex-shrink: 0;
}
</style>
screen/src/router/index.js
@@ -4,13 +4,21 @@
Vue.use(Router)
export const constantRoutes = [
  // {
  //   path: '/',
  //   component: () => import('../view/route-item/index.vue'),
  //   meta: {
  //     title: '放心出行智慧平台'
  //   }
  // },
  {
    // path: '/statistical-table',
    path: '/',
    component: () => import('../view/home/index.vue'),
    component: () => import('../view/statistical-table/index.vue'),
    meta: {
      title: '首页'
      title: '放心行可视化数据平台'
    }
  }
  },
]
const createRouter = () => new Router({
screen/src/utils/request.js
@@ -1,10 +1,22 @@
import axios from 'axios'
import { Message } from 'element-ui'
/**
 * 基础请求 URL
 * @type {string}
 */
const BASE_URL = process.env.VUE_APP_BASE_URL
/**
 * 默认请求超时时间(毫秒)
 * @type {number}
 */
const TIMEOUT = 30000
const service = axios.create({
  baseURL: process.env.VUE_APP_BASE_URL,
  baseURL: BASE_URL,
  withCredentials: false,
  timeout: 30000
  timeout: TIMEOUT
})
// 请求拦截
screen/src/utils/time.js
New file
@@ -0,0 +1,26 @@
/**
 * 格式化当前时间,返回包含日期、星期、时间的结构化对象
 * @returns {Object} 包含以下属性的对象:
 * - currentDate {string} 当前日期,格式为 YYYY-MM-DD
 * - currentDay {string} 当前星期,例如 '星期一'
 * - currentTime {string} 当前时间,格式为 HH:mm:ss
 */
export function formatTime() {
  const now = new Date();
  const year = now.getFullYear();
  const month = String(now.getMonth() + 1).padStart(2, '0');
  const date = String(now.getDate()).padStart(2, '0');
  const days = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
  const currentDay = days[now.getDay()];
  const hours = String(now.getHours()).padStart(2, '0');
  const minutes = String(now.getMinutes()).padStart(2, '0');
  const seconds = String(now.getSeconds()).padStart(2, '0');
  return {
    currentDate: `${year}-${month}-${date}`,
    currentDay,
    currentTime: `${hours}:${minutes}:${seconds}`
  };
}
screen/src/utils/utils.js
@@ -1,3 +1,8 @@
/**
 * 手机号脱敏处理(隐藏中间四位)
 * @param {string} phoneNumber - 需要脱敏的手机号码
 * @returns {string} 脱敏后的手机号码,例如 '138****1234'
 */
export const maskPhoneNumber = (phoneNumber) => {
  if (!phoneNumber || phoneNumber === '-') return '-'
  let str = ''
@@ -7,6 +12,11 @@
  return str
}
/**
 * 字符串脱敏处理(仅保留第一位,后面用星号代替)
 * @param {string} strName - 需要脱敏的字符串
 * @returns {string} 脱敏后的字符串,例如 '张***'
 */
export const maskString = (strName) => {
  if (!strName || strName === '-' || strName.length === 0) return '-'
  let str = ''
screen/src/view/home/index.vue
File was deleted
screen/src/view/route-item/index.less
New file
@@ -0,0 +1,168 @@
.page-container {
  width: 1920px;
  height: 1080px;
  background: url('~@/assets/img/route-item/route-bg.png') no-repeat center/cover;
  position: relative;
  overflow: hidden;
}
.top-bar {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 97px;
  background: url('~@/assets/img/route-item/route-item-top-bg.png') no-repeat center/100% 100%;
}
.top-title {
  position: absolute;
  top: 6px;
  left: 50%;
  transform: translateX(-50%);
  font-family: PangMenZhengDao;
  line-height: normal;
  font-size: 48px;
  letter-spacing: 0.14em;
  color: #FFFFFF;
  text-shadow: 0px 4px 4px rgba(17, 143, 215, 0.49);
}
.top-right {
  position: absolute;
  top: 4px;
  right: 30px;
  display: flex;
  gap: 30px;
  span {
    font-family: ABeeZee;
    font-size: 16px;
    line-height: 24px;
    font-variation-settings: "opsz" auto;
    background: linear-gradient(180deg, #ffffff 35%, #008bfd 100%), #45A2FF;
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
    background-clip: text;
    text-fill-color: transparent;
  }
}
.main-content {
  position: absolute;
  bottom: 336px;
  left: 0;
  width: 100%;
  height: 456px;
  display: flex;
  justify-content: center;
  align-items: center;
}
.arrow-box {
  width: 300px;
  height: 456px;
  position: absolute;
  top: 0;
  display: flex;
  align-items: center;
  z-index: 10;
  cursor: pointer;
}
.left-arrow-box {
  left: 0;
  background: linear-gradient(90deg, #000f29 34%, #000f2900 99%);
  justify-content: flex-start;
}
.right-arrow-box {
  right: 0;
  background: linear-gradient(270deg, #000f29 34%, #000f2900 99%);
  justify-content: flex-end;
}
.arrow-icon {
  width: 100px;
  height: 100px;
}
.items-container {
  position: relative;
  width: 100%;
  height: 100%;
  z-index: 5;
}
.item {
  position: absolute;
  top: 50%;
  background: url('~@/assets/img/route-item/no-active-item.png') no-repeat center/100% 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  cursor: pointer;
  transition: all 0.5s ease;
  &.active {
    background: url('~@/assets/img/route-item/active-item.png') no-repeat center/100% 100%;
  }
  &.no-transition {
    transition: none !important;
  }
}
.item-icons {
  position: relative;
  width: 100%;
}
.icon-1 {
  position: absolute;
  width: 106px;
  height: 107px;
  top: 106px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 2;
  transition: all 0.5s ease;
}
.icon-2 {
  position: absolute;
  width: 248px;
  height: 248px;
  top: 35px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 1;
  transition: all 0.5s ease;
}
.item-name-container {
  position: absolute;
  bottom: 78px;
  display: flex;
  flex-direction: column;
  align-items: center;
  transition: all 0.5s ease;
  .item-name {
    font-family: Alibaba-PuHuiTi-R;
    font-size: 36px;
    font-weight: 800;
    line-height: normal;
    text-align: center;
    letter-spacing: 0px;
    color: #FFFFFF;
  }
  .icon-3{
    margin-top: 10px;
    height: 6px;
    width: 141px;
    transition: all 0.5s ease;
  }
}
screen/src/view/route-item/index.vue
New file
@@ -0,0 +1,221 @@
<template>
  <div class="page-container">
    <!-- 导航栏 -->
    <div class="top-bar">
      <div class="top-title">放心出行智慧平台</div>
      <div class="top-right">
        <span>{{ currentDate }}</span>
        <span>{{ currentDay }}</span>
        <span>{{ currentTime }}</span>
      </div>
    </div>
    <!-- 主内容 -->
    <div class="main-content">
      <!-- 左箭头盒子 -->
      <div class="arrow-box left-arrow-box" @click="prev">
        <img src="~@/assets/img/route-item/left-change-icon.png" class="arrow-icon" />
      </div>
      <!-- 轮播图容器 -->
      <div class="items-container">
        <div
          v-for="item in carouselItems"
          :key="item.id"
          :class="['item', { active: item.diff === 0, 'no-transition': wrappingIndices.includes(item.index) }]"
          :style="item.style"
          @click="selectItem(item.index)"
        >
          <div class="item-icons">
            <img src="~@/assets/img/route-item/item-top-bg-icon-2.png" class="icon-2" :style="item.icon2Style" />
            <img src="~@/assets/img/route-item/item-top-bg-icon-1.png" class="icon-1" :style="item.icon1Style" />
          </div>
          <div class="item-name-container" :style="item.nameContainerStyle">
            <div class="item-name" :style="item.nameStyle">{{ item.name }}</div>
            <img src="~@/assets/img/route-item/item-bottom-bg-icon.png" class="icon-3" :style="item.icon3Style" />
          </div>
        </div>
      </div>
      <!-- 右箭头盒子 -->
      <div class="arrow-box right-arrow-box" @click="next">
        <img src="~@/assets/img/route-item/right-change-icon.png" class="arrow-icon" />
      </div>
    </div>
  </div>
</template>
<script>
import { formatTime } from '@/utils/time';
export default {
  name: "RouteItem",
  data() {
    return {
      items: [
        { id: 1, name: '导航名称' },
        { id: 2, name: '导航名称' },
        { id: 3, name: '导航名称' },
        { id: 4, name: '导航名称' },
        { id: 5, name: '导航名称' },
      ],
      activeIndex: 2, // 默认中间一项为选中态 (0, 1, 2, 3, 4)
      wrappingIndices: [], // 记录正在飞越边界的元素,取消过渡动画
      currentDate: '',
      currentDay: '',
      currentTime: '',
      timer: null
    };
  },
  computed: {
    carouselItems() {
      return this.items.map((item, index) => {
        let diff = index - this.activeIndex;
        if (diff < -2) diff += 5;
        if (diff > 2) diff -= 5;
        let left, width, height, zIndex, opacity = 1;
        // 基础尺寸(中心项)
        const baseWidth = 348;
        const baseHeight = 450;
        let scale = 1;
        if (diff === 0) {
          left = '50%'; zIndex = 3;
          scale = 1;
        } else if (diff === -1 || diff === 1) {
          left = diff === -1 ? 'calc(50% - 380px)' : 'calc(50% + 380px)';
          zIndex = 2;
          scale = 332 / baseWidth; // 约 0.954
        } else if (diff === -2 || diff === 2) {
          left = diff === -2 ? 'calc(50% - 733px)' : 'calc(50% + 733px)';
          zIndex = 1;
          scale = 294 / baseWidth; // 约 0.8448
        }
        // 根据比例计算所有内部元素的尺寸和位置
        width = baseWidth * scale;
        height = baseHeight * scale;
        // icon-2 原尺寸 249
        const icon2Size = 249 * scale;
        // icon-1 原尺寸 110 x 107
        const icon1Width = 110 * scale;
        const icon1Height = 107 * scale;
        // 如果想让非中心项的 icon 有微小的上移(模拟视觉重心变化)
        // 假设中间时 icon-1 top 为 106px,icon-2 top 为 35px,两边依次上移
        const offsetTop = (1 - scale) * 40; // 这个可调系数用来控制往两边递减时的上移幅度
        // 分别计算两个 icon 的动态 top
        const icon1Top = `${106 * scale - offsetTop}px`;
        const icon2Top = `${35 * scale - offsetTop}px`;
        // item-name-container 距离底部
        const nameBottom = 78 * scale;
        // 名字字体大小
        const nameFontSize = 36 * scale;
        // 底部 icon-3 的尺寸,原图大小大概可以缩放
        const icon3Width = 141 * scale;
        // 当元素正在“瞬移”到另一侧准备进场时,将其设为透明,避免视觉上突然出现
        if (this.wrappingIndices.includes(index)) {
          opacity = 0;
        }
        return {
          ...item,
          index,
          diff,
          style: {
            left,
            width: `${width}px`,
            height: `${height}px`,
            zIndex,
            opacity,
            transform: 'translate(-50%, -50%)',
          },
          icon1Style: {
            width: `${icon1Width}px`,
            height: `${icon1Height}px`,
            top: icon1Top
          },
          icon2Style: {
            width: `${icon2Size}px`,
            height: `${icon2Size}px`,
            top: icon2Top
          },
          nameContainerStyle: {
            bottom: `${nameBottom}px`
          },
          nameStyle: {
            fontSize: `${nameFontSize}px`
          },
          icon3Style: {
            width: `${icon3Width}px`
          }
        };
      });
    }
  },
  created() {
    this.updateTime();
    this.timer = setInterval(this.updateTime, 1000);
  },
  beforeDestroy() {
    if (this.timer) {
      clearInterval(this.timer);
    }
  },
  methods: {
    updateTime() {
      const timeData = formatTime();
      this.currentDate = timeData.currentDate;
      this.currentDay = timeData.currentDay;
      this.currentTime = timeData.currentTime;
    },
    prev() {
      let target = this.activeIndex - 1;
      if (target < 0) target = 4;
      this.selectItem(target);
    },
    next() {
      let target = this.activeIndex + 1;
      if (target > 4) target = 0;
      this.selectItem(target);
    },
    selectItem(targetIndex) {
      if (targetIndex === this.activeIndex) return;
      let wrappers = [];
      this.items.forEach((item, index) => {
        let oldDiff = index - this.activeIndex;
        if (oldDiff < -2) oldDiff += 5;
        if (oldDiff > 2) oldDiff -= 5;
        let newDiff = index - targetIndex;
        if (newDiff < -2) newDiff += 5;
        if (newDiff > 2) newDiff -= 5;
        // 判断是否跨越了边界(即从一端飞到另一端)
        if (Math.abs(oldDiff - newDiff) > 2) {
          wrappers.push(index);
        }
      });
      this.wrappingIndices = wrappers;
      this.activeIndex = targetIndex;
      // 稍微加长等待时间,确保浏览器绘制透明的瞬移状态,再恢复过渡动画和透明度
      setTimeout(() => {
        this.wrappingIndices = [];
      }, 50);
    }
  }
};
</script>
<style lang="less" scoped src="./index.less"></style>
screen/src/view/route-item/service.js
screen/src/view/statistical-table/charts/dayOrderVolume.js
New file
@@ -0,0 +1,141 @@
import * as echarts from "echarts";
/**
 * 初始化当日各时段订单情况折线图
 * @param {HTMLElement} chartDom - 图表容器 DOM 元素
 * @param {Array<string>} xData - X 轴类目数据(例如:小时列表)
 * @param {Array<number>} seriesData - 折线图对应的数值数据
 * @returns {echarts.ECharts} ECharts 实例
 */
export function initDayOrderVolumeChart(chartDom, xData, seriesData) {
  if (!chartDom) return null;
  const myChart = echarts.init(chartDom);
  const option = {
    grid: {
      top: "40px",
      left: "20px",
      right: "20px",
      bottom: "20px",
      containLabel: true,
    },
    title: {
      text: "订单量",
      textStyle: {
        fontSize: 13,
        color: "#B4C0CC",
        fontFamily: "Alibaba PuHuiTi 2.0"
      },
      left: "15px",
      top: "10px",
    },
    tooltip: {
      trigger: "axis",
      axisPointer: {
        type: "line",
        lineStyle: {
          color: "rgba(255, 218, 107, 0.5)",
          type: "dashed"
        }
      },
      backgroundColor: "#0b1e53",
      borderColor: "rgba(173, 198, 255, 0.502)",
      textStyle: {
        color: "#E7F9FF",
      },
      formatter: function (params) {
        const data = params[0];
        return `
          <div>
            ${data.name}时
            <div style="display:flex;align-items:center;">
              <span style="display:inline-block;margin-right:4px;border-radius:10px;width:10px;height:10px;background-color:#FFDA6B;"></span>
              <p style="color:#fff;margin-top:4px;">订单量</p>
            </div>
            <span style="font-size: 18px; font-weight: bold; margin-top: 4px; display: block;color:rgba(193, 234, 248, 1);">${data.value}</span>
          </div>
        `;
      }
    },
    xAxis: {
      type: "category",
      data: xData,
      boundaryGap: false,
      axisLine: {
        lineStyle: {
          color: "rgba(180, 192, 204, 0.2)",
        },
      },
      axisTick: {
        show: false,
      },
      axisLabel: {
        color: "#B4C0CC",
        fontFamily: "Alibaba PuHuiTi 2.0",
        fontSize: 12,
        margin: 15
      },
      splitLine: {
        show: false
      }
    },
    yAxis: {
      type: "value",
      min: 0,
      max: 250,
      interval: 50,
      splitLine: {
        show: false
      },
      axisLine: {
        show: false,
      },
      axisTick: {
        show: false,
      },
      axisLabel: {
        color: "#B4C0CC",
        fontFamily: "Alibaba PuHuiTi 2.0",
        fontSize: 12,
        margin: 15
      },
    },
    series: [
      {
        name: "订单量",
        type: "line",
        smooth: true,
        symbol: "circle",
        symbolSize: 8,
        showSymbol: true,
        itemStyle: {
          color: "#FFDA6B",
          borderColor: "#0b1e53",
          borderWidth: 2,
        },
        lineStyle: {
          width: 3,
          color: "#FFDA6B",
        },
        areaStyle: {
          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
            { offset: 0, color: "rgba(255, 218, 107, 0.2)" },
            { offset: 1, color: "rgba(255, 218, 107, 0)" }
          ])
        },
        emphasis: {
          focus: "series",
          itemStyle: {
            color: "#FFDA6B",
            borderColor: "#0b1e53",
            borderWidth: 3,
          },
        },
        data: seriesData,
      }
    ],
  };
  myChart.setOption(option);
  return myChart;
}
screen/src/view/statistical-table/charts/newPassenger.js
New file
@@ -0,0 +1,140 @@
import * as echarts from "echarts";
/**
 * 初始化新增乘客柱状图
 * @param {HTMLElement} chartDom - 图表容器 DOM 元素
 * @param {Array<string>} xData - X 轴类目数据(例如:月份或日期列表)
 * @param {Array<number>} seriesData - 柱状图对应的数值数据
 * @returns {echarts.ECharts} ECharts 实例
 */
export function initNewPassengerChart(chartDom, xData, seriesData) {
  if (!chartDom) return null;
  const myChart = echarts.init(chartDom);
  // 辅助背景柱的数据,始终等于 Y 轴的 max (50)
  const bgData = new Array(seriesData.length).fill(50);
  const option = {
    grid: {
      top: "30px",
      left: "20px",
      right: "20px",
      bottom: "20px",
      containLabel: true,
    },
    tooltip: {
      trigger: "axis",
      axisPointer: {
        type: "shadow",
      },
      backgroundColor: "#0b1e53",
      borderColor: "rgba(173, 198, 255, 0.502)",
      textStyle: {
        color: "#E7F9FF",
      },
    },
    xAxis: {
      type: "category",
      data: xData,
      axisLine: {
        show: false,
      },
      axisTick: {
        show: false,
      },
      axisLabel: {
        color: "#B4C0CC",
        fontFamily: "Alibaba-PuHuiTi-R",
        fontSize: 12
      },
    },
    yAxis: {
      type: "value",
      min: 0,
      max: 50,
      interval: 10,
      splitLine: {
        show: false,
      },
      axisLine: {
        show: false,
      },
      axisTick: {
        show: false,
      },
      axisLabel: {
        color: "#B4C0CC",
        fontFamily: "Alibaba-PuHuiTi-R",
        fontSize: 12
      },
    },
    series: [
      {
        name: "新增乘客",
        type: "bar",
        barWidth: 10,
        // 渐变色:从浅蓝到深蓝 (注意 echarts 的 LinearGradient 坐标系:0,0,0,1 表示从上到下)
        // 设计要求:background: linear-gradient(180deg, #0055ff00 1%, #0783fa 100%);
        itemStyle: {
          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
            { offset: 0, color: "rgba(0, 85, 255, 0)" }, // #0055ff00 顶部全透明
            { offset: 1, color: "#0783fa" }              // 底部蓝色
          ])
        },
        // 防止鼠标悬浮时颜色变化或者其他默认高亮行为影响视觉
        emphasis: {
          itemStyle: {
            color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
              { offset: 0, color: "rgba(0, 85, 255, 0)" },
              { offset: 1, color: "#0783fa" }
            ])
          }
        },
        // 顶部的小横线模拟:可以通过一个独立的 scatter 或者配置顶部的 markPoint/markLine,这里为了简单完美,直接利用 bar 本身和背景 bar 来组合,或者在上面盖一层
        data: seriesData,
      },
      // 这个用于渲染顶部的横线
      {
        name: "顶部横线",
        type: "pictorialBar",
        symbol: "rect",
        symbolSize: [10, 4], // 宽10,与主柱子 barWidth 一致,高2
        symbolPosition: "end", // 定位到柱子的顶端
        symbolOffset: [-2, -2], // 强制向上偏移2个像素(等于横线高度),使其彻底贴在柱子最上方的边缘外侧
        itemStyle: {
          color: "#15b1ff",
        },
        // 防止鼠标悬浮时图形变大
        emphasis: {
          scale: false,
          itemStyle: {
            color: "#15b1ff",
          }
        },
        tooltip: {
          show: false // 禁用顶部横线在 tooltip 中的显示
        },
        data: seriesData,
        z: 10
      },
      // 这个用于渲染底部的灰色柱子背景(整列阴影)
      {
        name: "背景阴影",
        type: "bar",
        barWidth: 20,
        barGap: "-150%", // 让背景阴影和主柱状图重叠并稍微宽一点
        itemStyle: {
          color: "rgba(255, 255, 255, 0.05)",
        },
        data: bgData,
        z: -1,
        tooltip: {
          show: false
        }
      }
    ],
  };
  myChart.setOption(option);
  return myChart;
}
screen/src/view/statistical-table/charts/orderVolume.js
New file
@@ -0,0 +1,124 @@
import * as echarts from "echarts";
/**
 * 初始化订单量柱状图(分段渐变样式)
 * @param {HTMLElement} chartDom - 图表容器 DOM 元素
 * @param {Array<string>} xData - X 轴类目数据(例如:月份或日期列表)
 * @param {Array<number>} seriesData - 柱状图对应的数值数据
 * @returns {echarts.ECharts} ECharts 实例
 */
export function initOrderVolumeChart(chartDom, xData, seriesData) {
  if (!chartDom) return null;
  const myChart = echarts.init(chartDom);
  const option = {
    grid: {
      top: "40px",
      left: "20px",
      right: "20px",
      bottom: "20px",
      containLabel: true,
    },
    title: {
      text: "订单量",
      textStyle: {
        fontSize: 13,
        color: "#B4C0CC",
        fontFamily: "Alibaba-PuHuiTi-R"
      },
      left: "15px",
      top: "10px",
    },
    tooltip: {
      trigger: "axis",
      axisPointer: {
        type: "shadow",
      },
      backgroundColor: "#0b1e53",
      borderColor: "rgba(173, 198, 255, 0.502)",
      textStyle: {
        color: "#E7F9FF",
      },
      formatter: function (params) {
        const data = params[0];
        return `
          <div>
            ${data.name}
            <div style="display:flex;align-items:center;">
              <span style="display:inline-block;margin-right:4px;border-radius:10px;width:10px;height:10px;background-color:#15b1ff;"></span>
              <p style="color:#fff;margin-top:4px;">订单量</p>
            </div>
            <span style="font-size: 18px; font-weight: bold; margin-top: 4px; display: block;color:rgba(193, 234, 248, 1);">${data.value}</span>
          </div>
        `;
      }
    },
    xAxis: {
      type: "category",
      data: xData,
      axisLine: {
        show: false
      },
      axisTick: {
        show: false,
      },
      axisLabel: {
        color: "#B4C0CC",
        fontFamily: "Alibaba-PuHuiTi-R",
        fontSize: 12,
        margin: 15
      },
    },
    yAxis: {
      type: "value",
      min: 0,
      max: 250,
      interval: 50,
      splitLine: {
        show: false // 设计图中Y轴没有横向网格线
      },
      axisLine: {
        show: false,
      },
      axisTick: {
        show: false,
      },
      axisLabel: {
        color: "#B4C0CC",
        fontFamily: "Alibaba-PuHuiTi-R",
        fontSize: 12,
        margin: 15
      },
    },
    series: [
      {
        name: "订单量",
        type: "pictorialBar",
        symbol: "rect",
        symbolRepeat: true,
        symbolSize: [16, 4], // 宽度16,高度4,形成长条分段
        symbolMargin: 2, // 分段之间的间隔
        itemStyle: {
          // 渐变色:从浅蓝到深蓝 (注意 echarts 的 LinearGradient 坐标系:0,0,0,1 表示从上到下)
          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
            { offset: 0, color: "#00FFFF" },
            { offset: 1, color: "#0055FF" }
          ])
        },
        emphasis: {
          itemStyle: {
            color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
              { offset: 0, color: "#00FFFF" },
              { offset: 1, color: "#0055FF" }
            ])
          }
        },
        data: seriesData,
        z: 10
      }
    ],
  };
  myChart.setOption(option);
  return myChart;
}
screen/src/view/statistical-table/charts/totalOrder.js
New file
@@ -0,0 +1,53 @@
import * as echarts from "echarts";
/**
 * 初始化总体订单统计饼图
 * @param {HTMLElement} chartDom - 图表容器 DOM 元素
 * @param {Array} orderStats - 订单统计数据源
 * @returns {echarts.ECharts} ECharts 实例
 */
export function initTotalOrderChart(chartDom, orderStats) {
  if (!chartDom) return null;
  const myChart = echarts.init(chartDom);
  const option = {
    // 通过配置默认的 legend 来配合 dispatchAction 控制扇区隐藏显示
    legend: {
      show: false, // 隐藏原生 legend
      selected: orderStats.reduce((acc, cur) => {
        acc[cur.name] = cur.selected !== false;
        return acc;
      }, {})
    },
    series: [
      {
        name: "订单统计",
        type: "pie",
        radius: ["35%", "85%%"],
        center: ["50%", "50%"],
        avoidLabelOverlap: false,
        label: {
          show: false,
          position: 'center'
        },
        emphasis: {
          label: {
            show: false
          }
        },
        labelLine: {
          show: false
        },
        data: orderStats.map(item => ({
          value: item.value,
          name: item.name,
          itemStyle: { color: item.color }
        }))
      }
    ]
  };
  myChart.setOption(option);
  return myChart;
}
screen/src/view/statistical-table/charts/yearTrend.js
New file
@@ -0,0 +1,128 @@
import * as echarts from "echarts";
/**
 * 初始化年度趋势图表
 * @param {HTMLElement} chartDom - 图表容器 DOM 元素
 * @returns {echarts.ECharts} ECharts 实例
 */
export function initYearTrendChart(chartDom) {
  if (!chartDom) return null;
  const myChart = echarts.init(chartDom);
  const option = {
    grid: {
      top: "55px",
      left: "20px",
      right: "20px",
      bottom: "20px",
      containLabel: true,
    },
    title: {
      text: "成功订单(万)",
      textStyle: {
        fontSize: 13,
        color: "#B4C0CC",
      },
      left: "15px",
      top: "15px",
    },
    tooltip: {
      trigger: "axis",
      backgroundColor: "#0b1e53",
      borderColor: "rgba(173, 198, 255, 0.502)",
      padding: [12, 12, 7, 12],
      textStyle: {
        fontSize: 16,
        color: "#E7F9FF",
      },
      formatter: function (params) {
        const data = params[0];
        return `
          <div>
            2026年${data.name}月
            <div style="display:flex;align-items:center;">
              <span style="display:inline-block;margin-right:4px;border-radius:10px;width:10px;height:10px;background-color:#66FFFF;"></span>
              <p style="color:#fff;margin-top:4px;">成功订单</p>
            </div>
            <span style="font-size: 18px; font-weight: bold; margin-top: 4px; display: block;color:rgba(193, 234, 248, 1);">${data.value}万</span>
          </div>
        `;
      },
      axisPointer: {
        type: "none",
      },
    },
    xAxis: {
      type: "category",
      data: ["01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12"],
      axisLine: {
        lineStyle: {
          color: "#242D34",
        },
      },
      axisTick: {
        show: false,
      },
      axisLabel: {
        color: "#B4C0CC",
      },
    },
    yAxis: {
      type: "value",
      min: 0,
      max: 5000,
      interval: 1000,
      splitLine: {
        show: true,
        lineStyle: {
          type: "dashed",
          color: "rgba(180, 192, 204, 0.2)",
        },
      },
      axisLine: {
        show: false,
      },
      axisTick: {
        show: false,
      },
      axisLabel: {
        color: "#B4C0CC",
        formatter: "{value}",
      },
      nameGap: 15,
    },
    series: [
      {
        name: "成功订单",
        type: "line",
        smooth: true,
        symbol: "emptyCircle",
        symbolSize: 6,
        showSymbol: false,
        itemStyle: {
          color: "#66FFFF",
          borderColor: "#66FFFF",
          borderWidth: 2,
        },
        lineStyle: {
          width: 4,
          color: "#66FFFF",
          cap: "round",
          join: "round",
        },
        emphasis: {
          focus: "series",
          itemStyle: {
            color: "#0D274D",
            borderColor: "#66FFFF",
            borderWidth: 2,
          },
        },
        data: [2500, 3500, 2300, 3000, 2500, 3300, 1600, 2000, 1600, 3000, 1600, 2500],
      },
    ],
  };
  myChart.setOption(option);
  return myChart;
}
screen/src/view/statistical-table/index.vue
New file
@@ -0,0 +1,594 @@
<template>
  <div class="page-container">
    <!-- 顶部导航栏 -->
    <div class="top-bar">
      <!-- 左侧图标 -->
      <div class="top-left">
        <div class="icon-circle">
          <img src="~@/assets/img/home/home-icon.png" class="home-icon" />
        </div>
        <div class="icon-circle">
          <img src="~@/assets/img/home/exit-icon.png" class="exit-icon" />
        </div>
      </div>
      <!-- 中间标题 -->
      <div class="top-title">放心行可视化数据平台</div>
      <!-- 右侧时间 -->
      <div class="top-right">
        <span>{{ currentDate }}</span>
        <span>{{ currentDay }}</span>
        <span>{{ currentTime }}</span>
      </div>
    </div>
    <!-- 内容区 -->
    <div class="content-container">
      <!-- 左侧 -->
      <div class="left-content">
        <!-- 年度趋势 -->
        <div class="year-trend">
          <TitleBox>
            <template #left>年度趋势</template>
            <template #right>
              <div class="date-picker-box">
                <img src="~@/assets/img/home/date-icon.png" class="date-icon" />
                <span>2026</span>
              </div>
            </template>
          </TitleBox>
          <div class="chart-container" ref="yearTrendChart"></div>
        </div>
        <!-- 总体订单统计 -->
        <div class="total-order">
          <TitleBox>
            <template #left>总体订单统计</template>
            <template #right>
              <div class="date-range-picker-box">
                <div class="date-item">
                  <img
                    src="~@/assets/img/home/date-icon.png"
                    class="date-icon"
                  />
                  <span>2026-01-30</span>
                </div>
                <div class="date-item">
                  <img
                    src="~@/assets/img/home/date-icon.png"
                    class="date-icon"
                  />
                  <span>2026-05-30</span>
                </div>
              </div>
            </template>
          </TitleBox>
          <div class="pie-chart-wrapper">
            <div class="pie-chart-bg">
              <div class="pie-chart-container" ref="totalOrderChart"></div>
              <div class="chart-title">
                <div class="title">12345</div>
                <div class="subtitle">总订单</div>
              </div>
            </div>
            <div class="legend-container">
              <div
                class="legend-item"
                v-for="(item, index) in orderStats"
                :key="index"
                @click="toggleLegend(item)"
                :style="{ opacity: item.selected === false ? 0.5 : 1 }"
              >
                <div class="legend-title-wrap">
                  <span
                    class="dot"
                    :style="{
                      backgroundColor:
                        item.selected === false ? '#ccc' : item.color,
                    }"
                  ></span>
                  <span class="name">{{ item.name }}</span>
                </div>
                <div class="legend-value-wrap">
                  <span>{{ item.value }}</span>
                  <span class="percent">({{ item.percent }})</span>
                </div>
              </div>
            </div>
          </div>
        </div>
        <!-- 新增乘客 -->
        <div class="new-passenger">
          <TitleBox>
            <template #left>新增乘客</template>
            <template #right>
              <div class="filter-btn-group">
                <div
                  class="filter-btn"
                  :class="{ active: newPassengerFilter === 'day' }"
                  @click="newPassengerFilter = 'day'"
                >
                  日新增
                </div>
                <div
                  class="filter-btn"
                  :class="{ active: newPassengerFilter === 'month' }"
                  @click="newPassengerFilter = 'month'"
                >
                  月新增
                </div>
              </div>
            </template>
          </TitleBox>
          <div class="chart-container" ref="newPassengerChart"></div>
        </div>
      </div>
      <!-- 中间内容 -->
      <div class="middle-content">
        <!-- 订单量 -->
        <div class="order-volume">
          <TitleBox>
            <template #left>订单量</template>
            <template #right>
              <div class="filter-btn-group">
                <div
                  class="filter-btn"
                  :class="{ active: orderVolumeFilter === 'day' }"
                  @click="orderVolumeFilter = 'day'"
                >
                  日订单量
                </div>
                <div
                  class="filter-btn"
                  :class="{ active: orderVolumeFilter === 'month' }"
                  @click="orderVolumeFilter = 'month'"
                >
                  月订单量
                </div>
              </div>
            </template>
          </TitleBox>
          <div class="chart-container" ref="orderVolumeChart"></div>
        </div>
        <!-- 当日各时段订单情况 -->
        <div class="day-order-volume">
          <TitleBox>
            <template #left>当日各时段订单情况</template>
            <template #right>
              <div class="time-picker-box">
                <img src="~@/assets/img/home/history-icon.png" class="time-icon" />
                <span>16~24时</span>
              </div>
            </template>
          </TitleBox>
          <div class="chart-container" ref="dayOrderVolumeChart"></div>
        </div>
      </div>
      <!-- 右侧 -->
      <div class="right-content">
        <!-- 司机接单排名 -->
        <div class="driver-order-rank"></div>
        <!-- 乘客排名 -->
        <!-- 乘客排名 -->
        <div class="passenger-rank">
          <TitleBox>
            <template #left>乘客排名</template>
            <template #right>
              <div class="filter-btn-group">
                <div
                  class="filter-btn"
                  :class="{ active: passengerRankFilter === 'day' }"
                  @click="passengerRankFilter = 'day'"
                >
                  今日
                </div>
                <div
                  class="filter-btn"
                  :class="{ active: passengerRankFilter === 'week' }"
                  @click="passengerRankFilter = 'week'"
                >
                  本周
                </div>
                <div
                  class="filter-btn"
                  :class="{ active: passengerRankFilter === 'month' }"
                  @click="passengerRankFilter = 'month'"
                >
                  本月
                </div>
              </div>
            </template>
          </TitleBox>
          <div class="rank-list-container">
            <div class="rank-header">
              <span class="header-index"></span>
              <span class="header-phone">乘客电话</span>
              <span class="header-count">叫车次数</span>
              <span class="header-orders">完成订单数</span>
            </div>
            <div class="rank-list">
              <div
                class="rank-item"
                v-for="(item, index) in passengerRankList"
                :key="index"
                :class="'bg-' + (index < 3 ? index + 1 : 4)"
              >
                <div class="rank-index-wrapper">
                  TOP <span class="rank-number" :class="'rank-' + (index < 3 ? index + 1 : 4)">{{ index + 1 }}</span>
                </div>
                <div class="item-phone">{{ item.phone }}</div>
                <div class="item-count">{{ item.count }}</div>
                <div class="item-orders">{{ item.orders }}</div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>
<script>
import { formatTime } from "@/utils/time";
import TitleBox from "@/components/TitleBox.vue";
import { initYearTrendChart } from "./charts/yearTrend";
import { initTotalOrderChart } from "./charts/totalOrder";
import { initNewPassengerChart } from "./charts/newPassenger";
import { initOrderVolumeChart } from "./charts/orderVolume";
import { initDayOrderVolumeChart } from "./charts/dayOrderVolume";
export default {
  name: "StatisticalTable",
  components: {
    TitleBox,
  },
  props: {},
  data() {
    return {
      /** @type {string} 当前日期 */
      currentDate: "",
      /** @type {string} 当前星期 */
      currentDay: "",
      /** @type {string} 当前时间 */
      currentTime: "",
      /** @type {number|null} 时间更新定时器 ID */
      timer: null,
      /** @type {Array} 总体订单统计图例数据源 */
      orderStats: [
        {
          name: "乘客取消",
          value: 748,
          percent: "23%",
          color: "#00FFFF",
          selected: true,
        },
        {
          name: "完成订单",
          value: 680,
          percent: "13%",
          color: "#1B82FF",
          selected: true,
        },
        {
          name: "司机取消",
          value: 548,
          percent: "11%",
          color: "#FFD159",
          selected: true,
        },
        {
          name: "超时订单",
          value: 548,
          percent: "11%",
          color: "#FF7F59",
          selected: true,
        },
        {
          name: "无效订单",
          value: 548,
          percent: "11%",
          color: "#7359FF",
          selected: true,
        },
        {
          name: "未处理订单",
          value: 548,
          percent: "11%",
          color: "#59FF59",
          selected: true,
        },
      ],
      /** @type {string} 新增乘客筛选类型: 'day' 日新增 | 'month' 月新增 */
      newPassengerFilter: "month",
      /** @type {string} 订单量筛选类型: 'day' 日订单量 | 'month' 月订单量 */
      orderVolumeFilter: "month",
      /** @type {string} 乘客排名筛选类型: 'day' | 'week' | 'month' */
      passengerRankFilter: "day",
      /** @type {Array} 乘客排名数据 */
      passengerRankList: [],
      /** @type {Object|null} 年度趋势图表实例 */
      yearTrendChartInstance: null,
      /** @type {Object|null} 总体订单图表实例 */
      totalOrderChartInstance: null,
      /** @type {Object|null} 新增乘客图表实例 */
      newPassengerChartInstance: null,
      /** @type {Object|null} 订单量图表实例 */
      orderVolumeChartInstance: null,
      /** @type {Object|null} 当日各时段订单情况图表实例 */
      dayOrderVolumeChartInstance: null,
    };
  },
  computed: {},
  watch: {
    /**
     * 监听新增乘客的筛选条件,改变时重新渲染图表
     */
    newPassengerFilter() {
      this.initNewPassengerChart();
    },
    /**
     * 监听订单量的筛选条件,改变时重新渲染图表
     */
    orderVolumeFilter() {
      this.initOrderVolumeChart();
    },
    /**
     * 监听乘客排名的筛选条件,改变时更新数据
     */
    passengerRankFilter() {
      this.fetchPassengerRankData();
    }
  },
  created() {
    this.updateTime();
    this.timer = setInterval(this.updateTime, 1000);
  },
  beforeDestroy() {
    if (this.timer) {
      clearInterval(this.timer);
    }
    if (this.yearTrendChartInstance) {
      this.yearTrendChartInstance.dispose();
    }
    if (this.totalOrderChartInstance) {
      this.totalOrderChartInstance.dispose();
    }
    if (this.newPassengerChartInstance) {
      this.newPassengerChartInstance.dispose();
    }
    if (this.orderVolumeChartInstance) {
      this.orderVolumeChartInstance.dispose();
    }
    if (this.dayOrderVolumeChartInstance) {
      this.dayOrderVolumeChartInstance.dispose();
    }
  },
  mounted() {
    this.initChart();
    this.initTotalOrderChart();
    this.initNewPassengerChart();
    this.initOrderVolumeChart();
    this.initDayOrderVolumeChart();
    this.fetchPassengerRankData();
    window.addEventListener("resize", this.handleResize);
  },
  destroyed() {
    window.removeEventListener("resize", this.handleResize);
  },
  methods: {
    /**
     * 更新顶部时间显示
     */
    updateTime() {
      const timeData = formatTime();
      this.currentDate = timeData.currentDate;
      this.currentDay = timeData.currentDay;
      this.currentTime = timeData.currentTime;
    },
    /**
     * 处理窗口改变大小事件,重置所有图表大小
     */
    handleResize() {
      if (this.yearTrendChartInstance) {
        this.yearTrendChartInstance.resize();
      }
      if (this.totalOrderChartInstance) {
        this.totalOrderChartInstance.resize();
      }
      if (this.newPassengerChartInstance) {
        this.newPassengerChartInstance.resize();
      }
      if (this.orderVolumeChartInstance) {
        this.orderVolumeChartInstance.resize();
      }
      if (this.dayOrderVolumeChartInstance) {
        this.dayOrderVolumeChartInstance.resize();
      }
    },
    /**
     * 初始化年度趋势图表
     */
    initChart() {
      if (this.yearTrendChartInstance) {
        this.yearTrendChartInstance.dispose();
      }
      this.yearTrendChartInstance = initYearTrendChart(
        this.$refs.yearTrendChart
      );
    },
    /**
     * 初始化总体订单图表
     */
    initTotalOrderChart() {
      if (this.totalOrderChartInstance) {
        this.totalOrderChartInstance.dispose();
      }
      this.totalOrderChartInstance = initTotalOrderChart(
        this.$refs.totalOrderChart,
        this.orderStats
      );
    },
    /**
     * 切换图例显示/隐藏
     * @param {Object} item - 被点击的图例对象
     */
    toggleLegend(item) {
      if (item.selected === undefined) {
        this.$set(item, "selected", false);
      } else {
        item.selected = !item.selected;
      }
      // 触发 echarts 内部的 action 隐藏或显示对应扇区
      if (this.totalOrderChartInstance) {
        this.totalOrderChartInstance.dispatchAction({
          type: "legendToggleSelect",
          name: item.name,
        });
      }
    },
    /**
     * 初始化/更新新增乘客图表数据
     */
    initNewPassengerChart() {
      if (this.newPassengerChartInstance) {
        this.newPassengerChartInstance.dispose();
      }
      let xData = [];
      let seriesData = [];
      if (this.newPassengerFilter === "month") {
        // 月新增:生成当前年 1-12 月
        xData = Array.from({ length: 12 }, (_, i) => `${i + 1}月`);
        // 模拟 12 个月的随机数据
        seriesData = Array.from(
          { length: 12 },
          () => Math.floor(Math.random() * 40) + 10
        );
      } else {
        // 日新增:生成当前月的所有日
        const now = new Date();
        const year = now.getFullYear();
        const month = now.getMonth() + 1;
        const daysInMonth = new Date(year, month, 0).getDate(); // 获取当月天数
        xData = Array.from({ length: daysInMonth }, (_, i) => `${i + 1}日`);
        // 模拟每天的随机数据
        seriesData = Array.from(
          { length: daysInMonth },
          () => Math.floor(Math.random() * 40) + 10
        );
      }
      this.newPassengerChartInstance = initNewPassengerChart(
        this.$refs.newPassengerChart,
        xData,
        seriesData
      );
    },
    /**
     * 初始化/更新订单量图表数据
     */
    initOrderVolumeChart() {
      if (this.orderVolumeChartInstance) {
        this.orderVolumeChartInstance.dispose();
      }
      let xData = [];
      let seriesData = [];
      if (this.orderVolumeFilter === "month") {
        // 月新增:生成当前年 1-12 月
        xData = Array.from(
          { length: 12 },
          (_, i) => `${String(i + 1).padStart(2, "0")}`
        );
        // 模拟数据
        seriesData = [
          178, 145, 160, 115, 225, 145, 105, 245, 178, 105, 150, 75,
        ];
      } else {
        // 日新增:生成当前月的所有日
        const now = new Date();
        const year = now.getFullYear();
        const month = now.getMonth() + 1;
        const daysInMonth = new Date(year, month, 0).getDate();
        xData = Array.from(
          { length: daysInMonth },
          (_, i) => `${String(i + 1).padStart(2, "0")}`
        );
        seriesData = Array.from(
          { length: daysInMonth },
          () => Math.floor(Math.random() * 200) + 50
        );
      }
      this.orderVolumeChartInstance = initOrderVolumeChart(
        this.$refs.orderVolumeChart,
        xData,
        seriesData
      );
    },
    /**
     * 初始化当日各时段订单情况图表数据
     */
    initDayOrderVolumeChart() {
      if (this.dayOrderVolumeChartInstance) {
        this.dayOrderVolumeChartInstance.dispose();
      }
      const xData = [
        "01",
        "02",
        "03",
        "04",
        "05",
        "06",
        "07",
        "08",
        "09",
        "10",
        "11",
        "12",
      ];
      const seriesData = [
        85, 130, 185, 145, 120, 180, 170, 160, 215, 120, 85, 85,
      ];
      this.dayOrderVolumeChartInstance = initDayOrderVolumeChart(
        this.$refs.dayOrderVolumeChart,
        xData,
        seriesData
      );
    },
    /**
     * 获取乘客排名数据
     */
    fetchPassengerRankData() {
      // 模拟接口请求数据
      let multiplier = this.passengerRankFilter === 'month' ? 30 : this.passengerRankFilter === 'week' ? 7 : 1;
      this.passengerRankList = Array.from({ length: 8 }, (_, i) => ({
        phone: '15215268013',
        count: Math.floor((88 - i * 5) * multiplier),
        orders: Math.floor((335 - i * 15) * multiplier)
      }));
    }
  },
};
</script>
<style lang='less' scoped>
@import "./styles/layout.less";
@import "./styles/year-trend.less";
@import "./styles/total-order.less";
@import "./styles/new-passenger.less";
@import "./styles/order-volume.less";
@import "./styles/day-order-volume.less";
@import "./styles/passenger-rank.less";
</style>
screen/src/view/statistical-table/styles/day-order-volume.less
New file
@@ -0,0 +1,43 @@
.day-order-volume {
  width: 838px;
  height: 465px;
  margin-top: 20px;
  background: linear-gradient(142deg, #0066ff1a -1%, #0066ff1a 38%, #009bff00 130%);
  box-sizing: border-box;
  border: 1px solid;
  border-image: linear-gradient(4deg, #007be178 2%, #00000000 101%) 1;
  position: relative;
  display: flex;
  flex-direction: column;
}
.time-picker-box {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 12px;
  box-sizing: border-box;
  cursor: pointer;
  position: relative;
  border-radius: 4px;
  border: 1px solid transparent;
  background-clip: padding-box, border-box;
  background-origin: padding-box, border-box;
  background-image: linear-gradient(#0B1E53, #0B1E53), linear-gradient(0deg, #498dc4 0%, #cbdeff 50%, #498dc4 100%);
  .time-icon {
    width: 14px;
    height: 14px;
  }
  span {
    color: rgba(95, 178, 255, 1);
    font-size: 14px;
    font-family: "Alibaba PuHuiTi 2.0";
  }
}
.chart-container {
  flex: 1;
  width: 100%;
}
screen/src/view/statistical-table/styles/layout.less
New file
@@ -0,0 +1,91 @@
.page-container {
  width: 100%;
  height: 100%;
  background: url('~@/assets/img/home/home-bg.png') no-repeat center/cover;
  position: relative;
  overflow: hidden;
  background-color: #03122d;
}
.top-bar {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100px;
  background: url('~@/assets/img/home/home-top-bg.png') no-repeat center/100% 100%;
}
.top-left {
  position: absolute;
  top: 24px;
  left: 30px;
  display: flex;
  gap: 24px;
}
.icon-circle {
  width: 24px;
  height: 24px;
  border-radius: 50%;
  background: linear-gradient(90deg, #3572ed 0%, #1b2a6f 51%, #223b8b 100%);
  display: flex;
  justify-content: center;
  align-items: center;
  cursor: pointer;
  img {
    width: 12px;
    height: 12px;
    object-fit: contain;
  }
}
.top-title {
  position: absolute;
  top: 6px;
  left: 50%;
  transform: translateX(-50%);
  font-family: PangMenZhengDao;
  line-height: normal;
  font-size: 48px;
  letter-spacing: 0.14em;
  color: #FFFFFF;
  text-shadow: 0px 4px 4px rgba(17, 143, 215, 0.49);
}
.top-right {
  position: absolute;
  top: 20px;
  right: 30px;
  display: flex;
  gap: 30px;
  span {
    font-family: ABeeZee;
    font-size: 16px;
    line-height: 24px;
    font-variation-settings: "opsz" auto;
    background: linear-gradient(180deg, #ffffff 35%, #008bfd 100%), #45A2FF;
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
    background-clip: text;
    text-fill-color: transparent;
  }
}
.content-container {
  width: 100%;
  height: calc(100% - 80px);
  position: absolute;
  top: 80px;
  left: 0;
  padding: 20px 30px;
  box-sizing: border-box;
  display: flex;
  justify-content: space-between;
}
.left-content {
  width: 491px;
}
screen/src/view/statistical-table/styles/new-passenger.less
New file
@@ -0,0 +1,56 @@
.new-passenger {
  width: 491px;
  height: 290px;
  margin-top: 20px;
  background: linear-gradient(142deg, #0066ff1a -1%, #0066ff1a 38%, #009bff00 130%);
  box-sizing: border-box;
  border: 1px solid;
  border-image: linear-gradient(4deg, #007be178 2%, #00000000 101%) 1;
  position: relative;
  display: flex;
  flex-direction: column;
}
.filter-btn-group {
  display: flex;
  gap: 8px;
  .filter-btn {
    flex-shrink: 0;
    padding: 4px 8px;
    background: #04325B;
    border-radius: 4px;
    border: 1px solid #04325B;
    display: flex;
    justify-content: center;
    align-items: center;
    font-family: Alibaba-PuHuiTi-R;
    font-size: 14px;
    color: rgba(255, 255, 255, .8);
    cursor: pointer;
    transition: all 0.3s;
    &:hover {
      color: #FFFFFF;
      border-radius: 4px;
      border: 1px solid transparent;
      background-clip: padding-box, border-box;
      background-origin: padding-box, border-box;
      background-image: linear-gradient(#0259A2, #0259A2), linear-gradient(0deg, #498dc4 0%, #cbdeff 50%, #498dc4 100%);
    }
    &.active {
      color: #FFFFFF;
      border-radius: 4px;
      border: 1px solid transparent;
      background-clip: padding-box, border-box;
      background-origin: padding-box, border-box;
      background-image: linear-gradient(#0259A2, #0259A2), linear-gradient(0deg, #498dc4 0%, #cbdeff 50%, #498dc4 100%);
    }
  }
}
.chart-container {
  flex: 1;
  width: 100%;
}
screen/src/view/statistical-table/styles/order-volume.less
New file
@@ -0,0 +1,16 @@
.order-volume {
  width: 838px;
  height: 465px;
  background: linear-gradient(142deg, #0066ff1a -1%, #0066ff1a 38%, #009bff00 130%);
  box-sizing: border-box;
  border: 1px solid;
  border-image: linear-gradient(4deg, #007be178 2%, #00000000 101%) 1;
  position: relative;
  display: flex;
  flex-direction: column;
}
.chart-container {
  flex: 1;
  width: 100%;
}
screen/src/view/statistical-table/styles/passenger-rank.less
New file
@@ -0,0 +1,158 @@
.passenger-rank {
    width: 491px;
    height: 465px;
    margin-top: 20px;
    background: linear-gradient(142deg, #0066ff1a -1%, #0066ff1a 38%, #009bff00 130%);
    box-sizing: border-box;
    border: 1px solid;
    border-image: linear-gradient(4deg, #007be178 2%, #00000000 101%) 1;
    position: relative;
    display: flex;
    flex-direction: column;
}
.rank-list-container {
    flex: 1;
    width: 100%;
    padding: 20px 0px;
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    .rank-header {
        display: flex;
        border-bottom: 1px solid rgba(255, 255, 255, 0.1);
        font-family: Alibaba PuHuiTi 2.0;
        font-size: 14px;
        color: #B4C0CC;
        .header-index {
            width: 105px;
        }
        .header-phone{
            width: 93px;
            text-align: center;
        }
        .header-count{
            margin-left: 55px;
            margin-right: 63px;
        }
    }
    .rank-list {
        flex: 1;
        overflow-y: auto;
        margin-top: 10px;
        /* 自定义滚动条样式 */
        &::-webkit-scrollbar {
            width: 4px;
        }
        &::-webkit-scrollbar-thumb {
            background: rgba(0, 102, 255, 0.5);
            border-radius: 2px;
        }
        .rank-item {
            display: flex;
            align-items: center;
            height: 36px;
            margin-bottom: 8px;
            padding-left: 29px;
            box-sizing: border-box;
            /* 控制整行的背景图 */
            background: url('~@/assets/img/home/passenger-rank-4.png') no-repeat center/100% 100%;
            &.bg-1 {
                background: url('~@/assets/img/home/passenger-rank-1.png') no-repeat center/100% 100%;
            }
            &.bg-2 {
                background: url('~@/assets/img/home/passenger-rank-2.png') no-repeat center/100% 100%;
            }
            &.bg-3 {
                background: url('~@/assets/img/home/passenger-rank-3.png') no-repeat center/100% 100%;
            }
            &.bg-4 {
                background: url('~@/assets/img/home/passenger-rank-4.png') no-repeat center/100% 100%;
            }
            .rank-index-wrapper {
                width: 76px;
                display: flex;
                align-items: center;
                font-size: 14px;
                color: #FFFFFF;
                gap: 10px;
                .rank-number {
                    display: flex;
                    justify-content: center;
                    align-items: center;
                    width: 24px;
                    height: 24px;
                    font-size: 18px;
                    box-sizing: border-box;
                    color: #FFFFFF;
                    &.rank-1 {
                        background: rgba(233, 183, 1, 0.1);
                        border: 1.68px solid;
                        border-image: linear-gradient(180deg, #e9b701 0%, #e9b70100 10%, #e9b70100 50%, #e9b70100 89%, #e9b701 100%) 1.68;
                        box-shadow: -3px 0px 7px 0px rgba(110, 88, 9, 0.5), inset 0px 0px 10px 0px rgba(233, 183, 1, 0.5);
                    }
                    &.rank-2 {
                        background: rgba(14, 209, 163, 0.1);
                        border: 1.68px solid;
                        border-image: linear-gradient(180deg, #0ed1a3 0%, #0ed1a300 9%, #0ed1a300 50%, #0ed1a300 90%, #0ed1a3 100%) 1.68;
                        box-shadow: -3px 0px 7px 0px rgba(5, 121, 93, 0.5), inset 0px 0px 10px 0px rgba(14, 209, 163, 0.5);
                    }
                    &.rank-3 {
                        background: rgba(0, 137, 255, 0.1);
                        border: 1.68px solid;
                        border-image: linear-gradient(180deg, #0089ff 0%, #0089ff00 9%, #0089ff00 52%, #0089ff00 90%, #0089ff 100%) 1.68;
                        box-shadow: -3px 0px 7px 0px rgba(3, 47, 73, 0.5), inset 0px 0px 10px 0px rgba(0, 137, 255, 0.5);
                    }
                    &.rank-4 {
                        background: rgba(14, 191, 218, 0.1);
                        border: 1.68px solid;
                        border-image: linear-gradient(180deg, #0ebfda 0%, #0ebfda00 9%, #0ebfda00 52%, #0ebfda00 90%, #0ebfda 100%) 1.68;
                        box-shadow: -3px 0px 7px 0px rgba(0, 51, 63, 0.5), inset 0px 0px 10px 0px rgba(14, 191, 218, 0.5);
                    }
                }
            }
            .item-phone {
                flex: 1.5;
                font-family: Alibaba PuHuiTi 2.0;
                font-size: 16px;
                color: #FFFFFF;
            }
            .item-count {
                flex: 1;
                font-family: Alibaba PuHuiTi 2.0;
                font-size: 16px;
                font-weight: bold;
                color: #FFFFFF;
            }
            .item-orders {
                flex: 1;
                font-family: Alibaba PuHuiTi 2.0;
                font-size: 16px;
                font-weight: bold;
                color: #FFFFFF;
            }
        }
    }
}
screen/src/view/statistical-table/styles/total-order.less
New file
@@ -0,0 +1,144 @@
.total-order {
    width: 491px;
    height: 290px;
    margin-top: 20px;
    background: linear-gradient(142deg, #0066ff1a -1%, #0066ff1a 38%, #009bff00 130%);
    box-sizing: border-box;
    border: 1px solid;
    border-image: linear-gradient(4deg, #007be178 2%, #00000000 101%) 1;
    position: relative;
    display: flex;
    flex-direction: column;
}
.date-range-picker-box {
    display: flex;
    align-items: center;
    gap: 16px;
    padding: 4px 12px;
    box-sizing: border-box;
    cursor: pointer;
    border-radius: 4px;
    border: 1px solid transparent;
    background-clip: padding-box, border-box;
    background-origin: padding-box, border-box;
    background-image: linear-gradient(#0B1E53, #0B1E53), linear-gradient(0deg, #498dc4 0%, #cbdeff 50%, #498dc4 100%);
    .date-item {
        display: flex;
        align-items: center;
        gap: 8px;
        .date-icon {
            width: 14px;
            height: 14px;
        }
        span {
            color: rgba(95, 178, 255, 1);
            font-size: 14px;
            font-family: "Alibaba-PuHuiTi-R";
        }
    }
}
.pie-chart-wrapper {
    flex: 1;
    width: 100%;
    display: flex;
    align-items: center;
    padding-top: 10px;
    .pie-chart-bg {
        width: 200px;
        height: 200px;
        margin-left: 20px;
        background: url('~@/assets/img/home/totalOrderChart.png') no-repeat center/contain;
        position: relative;
        display: flex;
        justify-content: center;
        align-items: center;
    }
    .pie-chart-container {
        width: 100%;
        height: 100%;
    }
    .chart-title {
        position: absolute;
        top: 50%;
        left: 50%;
        width: 104px;
        height: 104px;
        transform: translate(-50%, -50%);
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        background: rgba(0, 19, 66, .8);
        border-radius: 50%;
        .title {
            color: #fff;
            font-size: 24px;
            font-family: PangMenZhengDao;
        }
        .subtitle {
            line-height: 20px;
            color: #B4C0CC;
            font-size: 12px;
            font-family: Alibaba-PuHuiTi-R;
        }
    }
    .legend-container {
        flex: 1;
        display: flex;
        flex-wrap: wrap;
        align-content: center;
        padding-left: 35px;
        gap: 20px 0;
        .legend-item {
            width: 50%;
            display: flex;
            flex-direction: column;
            cursor: pointer;
            transition: opacity 0.3s;
            .legend-title-wrap {
                display: flex;
                align-items: center;
                gap: 6px;
                .dot {
                    width: 6px;
                    height: 6px;
                    border-radius: 50%;
                    transition: background-color 0.3s;
                }
                .name {
                    font-family: Alibaba-PuHuiTi-R;
                    font-size: 14px;
                    color: #B4C0CC;
                }
            }
            .legend-value-wrap {
                margin-top: 4px;
                padding-left: 12px;
                font-family: Alibaba-PuHuiTi-R;
                font-size: 16px;
                font-weight: bold;
                color: #FFFFFF;
                .percent {
                    margin-left: 4px;
                }
            }
        }
    }
}
screen/src/view/statistical-table/styles/year-trend.less
New file
@@ -0,0 +1,42 @@
.year-trend {
  width: 491px;
  height: 335px;
  background: linear-gradient(142deg, #0066ff1a -1%, #0066ff1a 38%, #009bff00 130%);
  box-sizing: border-box;
  border: 1px solid;
  border-image: linear-gradient(4deg, #007be178 2%, #00000000 101%) 1;
  position: relative;
  display: flex;
  flex-direction: column;
}
.date-picker-box {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 12px;
  background: #0B1E53;
  box-sizing: border-box;
  cursor: pointer;
  border-radius: 4px;
  border: 1px solid transparent;
  background-clip: padding-box, border-box;
  background-origin: padding-box, border-box;
  background-image: linear-gradient(#0B1E53, #0B1E53), linear-gradient(0deg, #498dc4 0%, #cbdeff 50%, #498dc4 100%);
  .date-icon {
    width: 14px;
    height: 14px;
  }
  span {
    color: #5FB2FF;
    font-size: 14px;
    font-family: "Alibaba-PuHuiTi-R";
  }
}
.chart-container {
  flex: 1;
  width: 100%;
}