大屏可视化效果实现记录

5,090 阅读5分钟

适配及响应式处理

  1. 一次搞懂数据大屏适配方案 (vw vh、rem、scale)

  2. 处理scale方案下有空白问题

效果实现

Echarts线图线条渐变色及区域渐变

  • 效果图

image.png

  • 关注点

    1. 线条颜色渐变
    2. 线条含有阴影
    3. 区域填充色渐变
  • 配置项

series:[{
  data: [820, 932, 901, 934, 1290, 1330, 1320],
  type: 'line',
  smooth: false,
  lineStyle: {
    normal: {
      // 1. 设置线条渐变色
      color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
        {
          offset: 0,
          color: '#FDFDFF',
        },
        {
          offset: 0.3,
          color: '#6EA4F8',
        },
        {
          offset: 0.6,
          color: '#7DA0E0',
        }, {
          offset: 1,
          color: '#679BF0',
        },
      ]), 
      width: 3,
      // 2. 设置线条阴影
      shadowColor: '#2E4F84',
      shadowOffsetY: 15,
      shadowOffsetX: 5,
      shadowBlur: 3,
    },
  },
  // 3. 设置区域填充渐变:渐变色设置文档 https://echarts.apache.org/zh/option.html#color
  areaStyle: {
    color: {
      type: 'linear',
      x: 0,
      y: 0,
      x2: 0,
      y2: 1,
      colorStops: [
        {
          offset: 0,
          color: 'rgba(36,173,254, 0.5)',
        }, {
          offset: 1,
          color: 'rgba(52,112,252, 0.1)',
        },
      ],
    },
  },
}]

Echarts外环饼图

  • 效果图

image 1.png

  • 关注点

    1. 内圈含有间隔数据
    2. 外圈效果
  • 配置项

// 数据处理
// 间隔空白数据
const gapData = {
  name: '',
  value: 20,
  itemStyle: {
    color: 'transparent', // 颜色设置为透明数据
  },
};

// 计算饼图渲染数据
const seriesData = [];
[
  { value: 1048, name: 'Search Engine' },
  { value: 735, name: 'Direct' },
  { value: 580, name: 'Email' },
  { value: 484, name: 'Union Ads' },
  { value: 300, name: 'Video Ads' }
].forEach((item) => {
  seriesData.push(item);
  seriesData.push(gapData);
});
// 图表配置项
series: [
  // 内圆环配置项
  {
    data: seriesData,
    roundCap: true,
    center: ['50%', '50%'],
    radius: ['50%', '60%'],
    label: {
      show: false,
      position: 'center',
    },
  },
  // 外圆环配置项
  {
    type: 'pie',
    name: '旋转圆',
    silent: true,
    center: ['50%', '50%'],
    radius: ['70%', '69%'],
    hoverAnimation: false,
    startAngle: 50,
    // Notes:这里的数据根据要展示的外环段数及长短自定义设置
    data:[120, 40, 120, 40, 120, 40].map((item, index) => ({
      value: item,
      name: '',
      itemStyle: {
        color: index % 2 === 0 ? '#5999E1' : 'transparent',
        shadowBlur: 20,
        shadowColor: '#86C6FD',
      },
    })),
    label: {
      normal: {
        show: false,
      },
    },
    labelLine: {
      normal: {
        show: false,
      },
    },
  }
],

Echarts 渐变色柱状图

  • 效果图

image 2.png

  • 关注点

    1. 柱体颜色渐变
  • 配置项

  series: [
    {
      data: [120, 200, 150, 80, 70, 110, 130],
      type: 'bar',
      // 设置柱体颜色渐变
      itemStyle: {
        normal: {
          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
            {
              offset: 1,
              color: '#20517E',
              opacity: 0.85,
            },
            {
              offset: 0,
              color: '#3FC0F7',
              opacity: 0.79,
            },
          ]),
        },
      },
      label: {
        show: true,
        color:'#3FC0F7',
        fontSize: 12,
        position: 'outside', 
      },
    }
  ]

Echarts含图片标签渐变色柱状图

  • 效果图

image 3.png

  • 关注项

    1. 渐变色柱体
    2. 高亮结尾
    3. 数据标签含背景图
  • 配置项

option = {
  backgroundColor:'#17243A',
  tooltip: {
    trigger: 'axis',
    axisPointer: {
      type: 'shadow'
    }
  },
  grid: {
    left: '3%',
    right: '4%',
    bottom: '3%',
    containLabel: true
  },
  xAxis: {
    type: 'value',
    boundaryGap: [0, 0.01]
  },
  yAxis: [
    {
      inverse: true,
      axisLabel: {
      color: '#ADCBE9',
      fontSize: 20,
      formatter: (value) => {
        if (value.length < 8) {
          return value;
        }
        return `${value.substring(0, 8)}...`;
      },
    },
    axisLine: {
      lineStyle: {
        color: 'transparent',
      },
    },
    data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
  }, {
      inverse: true,
      axisTick: 'none',
      axisLine: 'none',
      axisLabel: {
        show: true,
        fontSize: 20,
        fontWeight: 'bold',
        color: '#BFD1E3',
        padding: [5, 12, 5, 12],
        backgroundColor: {
           image: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAAAWCAYAAAA/45nkAAAACXBIWXMAAAsTAAALEwEAmpwYAAABAElEQVRoge3ZwQoBURTG8XOvKzGzmnQ3SpKJrMZb2HsQL+MRLCkb5RHsbNVspGwkRYamplipaSiUOodzZnl/d/FvZvPVqP5geoXMs18ve0l8ibLnfq1SDlebXfZc7LWZQtHxqq1x1g0AwDZcdKmG/4sl8SW6v+e7HfJ2qCnEcTaNHcDdNAAA1TgOlqv77TnVuH+3ODpO9LuXxb5vSXw+PXwAKnFcTH9yWez7JjMU2WSGYs9Q7ADuprEDuJvMUGSTGYpsMkORTWYooplCydW2EYwoxnEwr9ocaVDKpRjHxZ6uICpxHExmKLIZAADrd2bpwzD175Jq+C+abQRjUMpJ+w1co8ZyUwNCuQAAAABJRU5ErkJggg==',
        },
       },
      data: [120, 200, 150, 80, 70, 110, 130],
    },
  ],
  series: [
        {
          type: 'pictorialBar',
          symbol: 'image://data:image/png;base64,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',
      symbolOffset: [20, -5],
      symbolSize: [40, 40],
      symbolPosition: 'end',
      z: 12,
      data: [120, 200, 150, 80, 70, 110, 130],
    }, {
      name: '',
      type: 'bar',
      showBackground: true,
      yAxisIndex: 0,
      barWidth: 7,
      barBorderRadius: 10,
      data: [120, 200, 150, 80, 70, 110, 130].map((value, index) => ({
        value,
        itemStyle: {
          normal: {
            color: {
              type: 'linear',
              x: 0,
              y: 0,
              x2: 1,
              y2: 0,
              colorStops: [
                {
                  offset: 0,
                  color: '#2F3E56',
                },
                {
                  offset: 1,
                  color:'#7BB1EE',
                },
              ],
            },
          },
        },
      })),
    },
  ]
};

Echarts立体柱状图

  • 效果图

image 4.png

  • 关注点

    1. 三面立体
    2. 柱体渐变
  • 配置项

    // 自定义图形
    // 绘制左侧面
    export const CubeLeft = echarts.graphic.extendShape({
      shape: {
        x: 0,
        y: 0,
      },
      buildPath (ctx, shape) {
        const xAxisPoint = shape.xAxisPoint;
        const c0 = [shape.x, shape.y];
        const c1 = [shape.x - offsetX, shape.y - offsetY];
        const c2 = [xAxisPoint[0] - offsetX, xAxisPoint[1] - offsetY];
        const c3 = [xAxisPoint[0], xAxisPoint[1]];
        ctx.moveTo(c0[0], c0[1]).lineTo(c1[0], c1[1]).lineTo(c2[0], c2[1]).lineTo(c3[0], c3[1])
          .closePath();
      },
    });
    
    // 绘制右侧面
    export const CubeRight = echarts.graphic.extendShape({
      shape: {
        x: 0,
        y: 0,
      },
      buildPath (ctx, shape) {
        const xAxisPoint = shape.xAxisPoint;
        const c1 = [shape.x, shape.y];
        const c2 = [xAxisPoint[0], xAxisPoint[1]];
        const c3 = [xAxisPoint[0] + offsetX, xAxisPoint[1] - offsetY];
        const c4 = [shape.x + offsetX, shape.y - offsetY];
        ctx.moveTo(c1[0], c1[1]).lineTo(c2[0], c2[1]).lineTo(c3[0], c3[1]).lineTo(c4[0], c4[1])
          .closePath();
      },
    });
    // 绘制顶面
    export const CubeTop = echarts.graphic.extendShape({
      shape: {
        x: 0,
        y: 0,
      },
      buildPath (ctx, shape) {
        const c1 = [shape.x, shape.y];
        const c2 = [shape.x + offsetX, shape.y - offsetY]; // 右点
        const c3 = [shape.x, shape.y - offsetX];
        const c4 = [shape.x - offsetX, shape.y - offsetY];
        ctx.moveTo(c1[0], c1[1]).lineTo(c2[0], c2[1]).lineTo(c3[0], c3[1]).lineTo(c4[0], c4[1])
          .closePath();
      },
    });
    
    function getRenderItem(param, type) {
      const colorList = ['#66C9F2', '#80D1CD', '#9BD977'];
      const color = colorList[param.dataIndex % 3];
      const rgba = color16ToRGBA(color, type === 'top' ? 0.6 : 0.01);
      return {
        fill: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
          {
            offset: 1,
            color: rgba,
          },
          {
            offset: 0,
            color,
          },
        ]),
      };
    }
    
    // 图表配置项
    config = {
      xAxis: {
        axisLine: {
          lineStyle: {
            color: 'transparent',
          },
        },
        axisLabel: {
          color: '#B1CBD8',
          fontSize: 20,
        },
      },
      yAxis: {
        show: false,
        splitLine: {
          show: false,
        },
      },
      series:[
        {
          type: 'custom',
          // 使用自定义的图形进行绘制
          renderItem: (params, api) => {
            const location = api.coord([api.value(0), api.value(1)]);
            return {
              type: 'group',
              children: [
                {
                  type: 'CubeLeft', // 绘制左侧面
                  shape: {
                    api,
                    xValue: api.value(0),
                    yValue: api.value(1),
                    x: location[0],
                    y: location[1],
                    xAxisPoint: api.coord([api.value(0), 0]),
                  },
                  style: {
                    ...getRenderItem(params),
                  },
                },
                {
                  type: 'CubeRight', // 绘制右侧面
                  shape: {
                    api,
                    xValue: api.value(0),
                    yValue: api.value(1),
                    x: location[0],
                    y: location[1],
                    xAxisPoint: api.coord([api.value(0), 0]),
                  },
                  style: {
                    ...getRenderItem(params),
                  },
                },
                {
                  type: 'CubeTop', // 绘制顶层
                  shape: {
                    api,
                    xValue: api.value(0),
                    yValue: api.value(1),
                    x: location[0],
                    y: location[1],
                    xAxisPoint: api.coord([api.value(0), 0]),
                  },
                  style: {
                    ...getRenderItem(params, 'top'),
                  },
                },
              ],
            };
          },
          data: [120, 200, 150, 80, 70, 110, 130],
        },
        {
          type: 'bar',
          label: {
            normal: {
              show: true,
              position: 'top',
              formatter: e => `${e.value}%`,
              fontSize: 15,
              color: '#fff',
              offset: [0, -15],
            },
          },
          itemStyle: {
            color: 'transparent',
          },
          tooltip: {},
          data: [120, 200, 150, 80, 70, 110, 130],
        },
      ]
    }
    

CSS旋转圆动画效果

  • 效果图

iShot_2024-11-20_11.15.37.gif

<div class="value">
  <span>{{ item.value }}</span>
  <span class="unit">%</span>
</div>
/** 定义旋转动画 **/
@keyframes rotate {
  100% {
    transform: rotate(360deg);
  }
}

.value {
  width: 9vh;
  height: 9vh;
  line-height: 9vh;
  text-align: center;
  position: relative;
  margin: auto;
  border-radius: 50%;
  /** 设置元素背景径向渐变色 **/
  background: radial-gradient(50% 50% at 50% 50%, rgba(12, 27, 48, 0.1) 0%, rgba(12, 27, 48, 0.1) 49%, rgba(116, 217, 229, 0.1) 98%);
  text-align: center;

  .unit {
    font-size: 1.4vh;
    position: absolute;
    margin-top: 3px;
    margin-left: 3px;
  }

  /** 添加外环元素 **/
  &::before,
  &::after {
    content: "";
    position: absolute;
    top: -1.5vh;
    left: -1.5vh;
    bottom: -1.5vh;
    right: -1.5vh;
    border-radius: 50%;
    border-top: 3px solid #58A7B4;
    /** 为外环元素添加旋转动画 **/
    animation: rotate 6s infinite linear;
  }

  /** 第二个半圆添加动画延迟3S,使两个动画可以交替执行 **/
  &::after {
    animation-delay: 3s;
  }
}

CSS元素浮动漂浮效果

  • 效果图

iShot_2024-11-20_14.05.01.gif

  • 实现
/** 定义浮动动画 **/
@keyframes float {
  0% {
    transform: translateY(0);
  }

  50% {
    transform: translateY(-20px);
  }

  100% {
    transform: translateY(0);
  }
}

/** 为元素整体添加动画 **/
.indicator{
  ...其他样式项
  animation: float 3s infinite ease-in-out;
  
  /** 往后每个元素的动画执行延迟2s,保证不同的漂浮幅度 **/
  &.indicator2{
     animation-delay: 2s;
  }
  
  &.indicator3{
     animation-delay: 4s;
  }
  
  &.indicator4{
     animation-delay: 6s;
  }
}

字体渐变色

  • 效果

image 5.png

span {
  /** 设置字体的背景色为径向渐变色 **/
  background: linear-gradient(180deg, #F5F5F5 0%, #7EB8E6 100%);
  /** 将背景作用区域更新为文本,背景被裁剪为文字的形状 **/
  background-clip: text;
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  /** 文字本身设置为透明色 **/
  color: transparent;
}

其他

动态渲染大屏模块

  • 背景

    一般大屏页面会显示多个小模块,在不同的场景下分别配置显示哪几个。当前由接口提供一组要渲染的模块值,需要根据接口动态设置要渲染的内容

  • 实现方式

    由Vue component动态组件进行渲染

    1. 从接口获取一组模块名称:comList

    2. 对comList进行遍历,使用component :is 进行匹配渲染

    3. 注意组件的name名称,使用:is匹配时,需要字段值于之一致

    <template v-for="name in comList">
      <component :is="name" :key="name"/>
    </template>
    

定时器更新图表数据

  • 背景

    所有图表数据量较多,不宜一次性展示全部,而是分组进行循环展示。即每次展示5条数据,间隔一定时间后切换至下5条数据,以此循环。

  • 实现方式

    在顶层App.vue组件中,开启一个定时器,并使用 moduleTimerCount 字段记录当前的组别数,按时间间隔更新该字段。并在子组件中监听该字段,该字段变化时计算当前子组件需要显示的数据条数,并更细图表数据。

    1. 声明 moduleTimerCount 变量
    data(){
      return {
        moduleTimerCount:0
      }
    }
    
    1. 开启一个定时器

    使用 setTimeout 模拟 setInterval 定时器(相比于setInterval,setTimeout每次执行完当前次任务后才会执行下一次任务,不存在任务堆积问题,每次执行完后自行清理、独立调用,内存泄露的风险较低)。

    image 6.png

    ```JavaScript
    function openModuleRefresh(delay) {
      const execute = () => {
        this.moduleTimerCount += 1;
        if (moduleRefreshTime) {
          clearTimeout(moduleRefreshTime);
        }
        moduleRefreshTime = setTimeout(execute, delay * 1000);
      };
    
      setTimeout(execute, delay * 1000); // 首次延迟执行
    },
    ```
    

    3. 子组件监听字段变化

    ```JavaScript
      watch: {
        moduleTimerCount(value) {
          if (dataList) {
            // 当前接口数据的数据长度
            const dataLength = dataList.length;
            // 每5个分一组,计算组别数
            const totalGroup = Math.ceil(dataLength / 5);
            // 计算当前组别数,使用 moduleTimerCount 值对组别数取余,保证获取的当前组别不会超过总组别数
            this.chartGroupIndex = value % totalGroup;
            // 计算当前的数据,由组别数获取当前组的数据索引
            const startIndex = this.chartGroupIndex * 5;
            let endIndex = (this.chartGroupIndex + 1) * 5;
            if (endIndex >= echartsData.data.length) {
              endIndex = echartsData.data.length;
            }
            // 根据索引截取数据
            const renderChartData = echartsData.slice(startIndex, endIndex);
          }
        },
      },
    ```