在 Vue3 + TypeScript 项目开发中,DOM 打印、列表自动滚动、ECharts 数据可视化、TSX 自定义指令是高频刚需场景。本文整合企业级实战代码,封装通用方案,支持直接复制使用,完美适配后台管理系统、数据大屏、报表打印等业务场景。

一、Vue3+TS 实现 DOM 精准打印(带自定义页眉页脚 / 分页)

基于 print-js 实现 DOM 元素打印,支持自定义页眉页脚、A4 纸张、强制分页、样式隔离,完美解决 Vue 项目打印样式错乱、页眉页脚不生效问题。

核心代码

// 安装依赖:npm install print-js
import print from 'print-js'

// 调用打印方法
const handlePrint = () => {
  print({
    printable: 'print-section', // 目标打印DOM的id
    type: 'html',
    // 自定义打印样式:页眉、页脚、分页、边距
    style: `
      body {
        counter-reset: page 0;
      }
      /* 打印页面配置 */
      @page {
        size: A4;
        margin: 60px 80px 55px; /* 上下边距预留页眉页脚空间 */
        
        /* 左上角页眉 */
        @top-left {
          margin-bottom: 20px;
          margin-top: 22px;
          content: "学生健康体质监测";
          font-size: 11px;
          color: #46474c;
          border-bottom: 1px solid #b8becc;
          padding-bottom: 8px;
          font-weight: 400;
          font-family: Microsoft YaHei UI;
          letter-spacing: 0.02px;
          box-sizing: border-box;
        }
        
        /* 右下角页脚(自动页码) */
        @bottom-right {
          margin-bottom: 25px;
          content: "第 " counter(page) " 页";
          font-size: 9px;
          color: #5d6067;
          border-top: 1px solid #b8becc;
          padding-top: 6px;
          font-family: Microsoft YaHei UI;
          box-sizing: border-box;
        }
      }
      /* 打印容器样式 */
      #print-section {
        width: 100%;
        padding: 0;
      }
      /* 强制分页类(给需要分页的元素添加) */
      .page-break-always {
        page-break-after: always !important;
        break-after: page !important;
      }
      /* 禁止分页类(避免内容被截断) */
      .page-break-avoid {
        page-break-after: avoid !important;
        break-after: avoid !important;
      }
    `,
    scanStyles: false, // 关闭全局样式扫描,避免样式冲突
  })
}

使用说明

  1. 给需要打印的 DOM 设置 id="print-section"
  2. 需要强制分页的元素添加类名 page-break-always
  3. 表格 / 模块避免被分页截断添加类名 page-break-avoid
  4. 支持自定义页眉文字、页脚页码、边距、字体样式

二、Vue3+TS 手动实现表格无限自动滚动(悬浮暂停 / 底部回顶)

纯原生 JS 实现列表 / 表格自动滚动、鼠标悬浮暂停、滚动到底部暂停回顶,无第三方依赖,性能优异,适配数据大屏、排名列表等场景。

1. 定义响应式变量

import { ref, onMounted, onUnmounted } from 'vue'

// 滚动定时器
const scrollInterval = ref<number | null>(null)
// 鼠标悬浮状态
const isHovered = ref(false)
// 初始化延迟定时器
const initTimer = ref<number | null>(null)
// 底部暂停定时器
const bottomPauseTimer = ref<number | null>(null)
// 是否滚动到底部
const isAtBottom = ref(false)
// 是否正在滚动
const isScrolling = ref(false)
// 滚动容器DOM引用
const contentBoxRef = ref<HTMLDivElement | null>(null)

2. 模板结构

<template>
  <div
    class="content-box"
    ref="contentBoxRef"
    @mouseenter="isHovered = true"
    @mouseleave="isHovered = false"
  >
    <div 
      v-for="(item, index) in props.dataList" 
      :key="index" 
      class="content-item"
    >
      <template v-for="(row, number) in props.headerList" :key="row.key">
        <div class="row-item" :style="getRowItemStyle(row)">
          <img 
            v-if="!number" 
            class="ranking-item" 
            :src="getImg(index)" 
            alt="排位" 
          />
          <span class="ellipsis-text">{{ item[row.key] }}</span>
        </div>
      </template>
    </div>
  </div>
</template>

3. 滚动核心逻辑

/**
 * 启动自动滚动(初始延迟3秒)
 */
const startAutoScroll = () => {
  if (scrollInterval.value || isScrolling.value) return
  isScrolling.value = false
  // 初始延迟3秒后开始滚动
  initTimer.value = window.setTimeout(() => {
    isScrolling.value = true
    doScroll()
  }, 3000)
}

/**
 * 执行滚动逻辑
 */
const doScroll = () => {
  if (scrollInterval.value) return
  scrollInterval.value = window.setInterval(() => {
    if (!contentBoxRef.value || isHovered.value || !isScrolling.value) return
    const { scrollTop, scrollHeight, clientHeight } = contentBoxRef.value
    // 判断是否可滚动、是否触底
    const canScroll = scrollHeight > clientHeight
    const reachedBottom = scrollTop + clientHeight >= scrollHeight - 1

    if (canScroll) {
      // 触底逻辑:暂停3秒后回到顶部重新滚动
      if (reachedBottom && !isAtBottom.value) {
        isAtBottom.value = true
        isScrolling.value = false
        clearInterval(scrollInterval.value!)
        scrollInterval.value = null

        bottomPauseTimer.value = window.setTimeout(() => {
          contentBoxRef.value!.scrollTop = 0
          isAtBottom.value = false
          isScrolling.value = false
          startAutoScroll()
        }, 3000)
      } 
      // 正常滚动
      else if (!reachedBottom) {
        contentBoxRef.value.scrollBy({
          top: 1,
          behavior: 'smooth',
        })
      }
    }
  }, 50) as unknown as number
}

/**
 * 停止所有滚动,清除定时器(防止内存泄漏)
 */
const stopAllScroll = () => {
  initTimer.value && clearTimeout(initTimer.value)
  bottomPauseTimer.value && clearTimeout(bottomPauseTimer.value)
  scrollInterval.value && clearInterval(scrollInterval.value)
  
  initTimer.value = null
  bottomPauseTimer.value = null
  scrollInterval.value = null
  isScrolling.value = false
  isAtBottom.value = false
}

// 生命周期:挂载启动、卸载销毁
onMounted(() => startAutoScroll())
onUnmounted(() => stopAllScroll())

核心特性

  1. 鼠标悬浮自动暂停,离开恢复滚动;
  2. 初始延迟 3 秒,滚动到底部暂停 3 秒后回顶;
  3. 平滑滚动,无卡顿,性能占用极低;
  4. 完整定时器销毁,避免内存泄漏。

三、Vue3+TS+ECharts 封装通用折线图(渐变 / 空状态 / 自适应)

封装高可用折线图组件,支持渐变填充、自定义样式、空数据状态、响应式渲染,统一 ECharts 初始化逻辑。

1. 通用 ECharts 初始化封装(全局复用)

// src/utils/echart.ts
import * as echarts from 'echarts'

/**
 * 统一初始化ECharts实例
 * @param echartsInstance echarts对象
 * @param id DOM节点ID
 * @param renderer 渲染方式
 * @returns ECharts实例
 */
export function getInitecharts(
  echartsInstance: typeof echarts,
  id: string,
  renderer: 'canvas' | 'svg' = 'canvas',
): echarts.ECharts {
  const dom = document.getElementById(id)
  if (!dom) throw new Error(`DOM节点 ${id} 不存在`)
  return echartsInstance.init(dom, null, {
    renderer,
    useDirtyRect: false,
  })
}

2. 折线图组件代码

<script setup lang="ts" name="RepsSingleLineChart">
import * as echarts from 'echarts'
import { ref, reactive, watch, onMounted, nextTick } from 'vue'
import { getInitecharts } from '@/utils/echart'
import type { EChartsOption } from 'echarts'

// Props类型定义
interface Props {
  id: string
  dataObj: Record<string, unknown> | null
}
const props = defineProps<Props>()

// ECharts实例
let myChart: echarts.ECharts | null = null
// 空数据提示
const emptyObj = reactive<Record<string, string>>({
  dashboardPtglHyxxs: '暂无学校数据',
  dashboardPtglRzqys: '暂无企业数据',
})

// 图表配置项
const option = ref<EChartsOption>({
  grid: {
    top: '30px',
    left: '17px',
    right: '20px',
    bottom: '9px',
    containLabel: true,
  },
  xAxis: {
    type: 'category',
    boundaryGap: false,
    data: ['2025.01', '2025.02', '2025.03', '2025.04', '2025.05', '2025.06'],
    axisTick: { show: false },
    axisLine: {
      lineStyle: { color: 'rgba(153,204,255,0.50)', width: 1 }
    },
    axisLabel: { color: '#DAECFF', fontSize: 14 },
    splitLine: {
      lineStyle: { color: 'rgba(153,204,255,0.20)', type: 'dashed' }
    }
  },
  yAxis: {
    type: 'value',
    axisLabel: { color: 'rgba(218,236,255,0.60)', fontSize: 14 },
    axisLine: { show: false },
    axisTick: { show: false },
    splitLine: {
      lineStyle: { color: 'rgba(153,204,255,0.20)', type: 'dashed' }
    }
  },
  series: [
    {
      data: [820, 932, 0, 0, 1290, 1330],
      type: 'line',
      symbol: 'circle',
      symbolSize: 6,
      label: {
        show: true,
        position: 'top',
        color: '#ffffff',
        fontSize: 14,
        fontWeight: 'bold'
      },
      // 渐变填充
      areaStyle: {
        color: {
          type: 'linear',
          x: 0, y: 0, x2: 0, y2: 1,
          colorStops: [
            { offset: 0, color: 'rgba(0,230,191,0.30)' },
            { offset: 1, color: 'rgba(0,230,191,0.10)' }
          ]
        }
      },
      lineStyle: { color: '#00E5BF', width: 2 },
      itemStyle: {
        color: '#00e5bf',
        borderColor: 'rgba(0,229,191,0.40)',
        borderWidth: 3
      }
    }
  ]
})

// 渲染图表
const getRender = () => {
  myChart?.dispose()
  myChart = getInitecharts(echarts, props.id)

  nextTick(() => {
    if (!myChart) return
    // 有数据渲染图表
    if (props.dataObj && Object.keys(props.dataObj).length) {
      myChart.setOption(option.value)
    } 
    // 无数据显示空状态
    else {
      myChart.setOption({
        tooltip: { show: false },
        graphic: {
          elements: [{
            type: 'group', left: 'center', top: 'center', silent: true,
            children: [
              { type: 'image', style: { image: '/src/assets/img/table-empty.png', width: 88, height: 88 } },
              { type: 'text', style: { text: emptyObj[props.id], fontSize: 14, fill: 'rgba(255,255,255,.8)' } }
            ]
          }]
        }
      })
    }
  })
}

// 监听数据变化
watch(() => props.dataObj, () => getRender(), { deep: true })
onMounted(() => getRender())
</script>

<template>
  <div :id="props.id" class="line-chart" />
</template>

<style lang="less" scoped>
.line-chart {
  width: 100%;
  height: 100%;
}
</style>

四、Vue3+TS+ECharts 自定义柱状图(分段柱状 / 空状态)

使用 ECharts custom 自定义渲染,实现分段式柱状图(多矩形堆叠效果),支持数值显示、渐变样式、空数据占位。

完整组件代码

<script setup lang="ts" name="RepsSingleBarChart">
import * as echarts from 'echarts'
import { ref, reactive, watch, onMounted, nextTick } from 'vue'
import { getInitecharts } from '@/utils/echart'

interface Props {
  id: string
  dataObj: Record<string, unknown> | null
}
const props = defineProps<Props>()

// 自定义配置类型
interface SeriesOption {
  type: string
  data: number[]
  renderItem?: (params: any, api: any) => any
  encode?: { x: number; y: number }
}

let myChart: echarts.ECharts | null = null
const emptyObj = reactive<Record<string, string>>({
  dashboardPtglRzxxs: '暂无入驻学校',
})

const option = ref<{
  tooltip?: any; grid?: any; xAxis?: any; yAxis?: any; series?: SeriesOption[]
}>({
  tooltip: { show: false },
  grid: { top: '31px', left: '17px', right: '21px', bottom: '9px', containLabel: true },
  xAxis: [{
    type: 'category',
    data: ['2025.01', '2025.02', '2025.03', '2025.04', '2025.05', '2025.06'],
    axisTick: { show: false },
    axisLine: { lineStyle: { color: 'rgba(153,204,255,0.50)' } },
    axisLabel: { color: '#DAECFF', fontSize: 14 },
    splitLine: { lineStyle: { color: 'rgba(153,204,255,0.20)', type: 'dashed' } }
  }],
  yAxis: [{
    type: 'value',
    axisLabel: { color: 'rgba(218,236,255,0.60)', fontSize: 14 },
    axisLine: { show: false },
    axisTick: { show: false },
    splitLine: { lineStyle: { color: 'rgba(153,204,255,0.20)', type: 'dashed' } }
  }],
  // 自定义分段柱状图
  series: [{
    type: 'custom',
    data: [10, 0, 200, 334, 390, 330],
    encode: { x: 0, y: 1 },
    renderItem: (_, api) => {
      const gap = 4        // 分段间距
      const segWidth = 22  // 分段宽度
      const segHeight = 6  // 分段高度
      const base = api.coord([api.value(0), 0])
      const value = api.coord([api.value(0), api.value(1)])
      let segCount = Math.round((base[1] - value[1]) / (segHeight + gap))
      const number = api.value(1)
      let children: any[] = []

      segCount === 0 && number && (segCount = 1)

      // 生成分段矩形
      for (let i = 0; i < segCount; i++) {
        const y = base[1] - segHeight - i * (segHeight + gap)
        children.push({
          type: 'rect',
          shape: { x: value[0] - segWidth / 2, y, width: segWidth, height: segHeight },
          style: {
            fill: new echarts.graphic.LinearGradient(0,0,1,0, [
              { offset:0, color:'#00d4ff' },
              { offset:1, color:'#00aaff' }
            ])
          }
        })
      }

      // 数值为0时特殊处理
      if (segCount === 0) {
        children = [
          { type: 'rect', shape: { x: value[0]-11, y: base[1]-1, width:22, height:1 }, style: { fill:'#00d4ff' } },
          { type: 'text', style: { text: '0', fill:'#fff', fontSize:14, textAlign:'center' }, position: [value[0], base[1]-24] }
        ]
      } else {
        // 显示数值
        children.push({
          type: 'text',
          style: { text: number.toString(), fill: '#fff', fontSize:14, fontWeight:700, textAlign:'center' },
          position: [value[0], base[1] - (segHeight+gap)*segCount - 20],
          z:10
        })
      }

      return { type: 'group', children, silent: true }
    }
  }]
})

// 渲染逻辑与折线图一致
const getRender = () => {
  myChart?.dispose()
  myChart = getInitecharts(echarts, props.id)
  nextTick(() => {
    if (!myChart) return
    props.dataObj && Object.keys(props.dataObj).length
      ? myChart.setOption(option.value)
      : myChart.setOption({ /* 空状态配置 */ })
  })
}

watch(() => props.dataObj, () => getRender(), { deep: true })
onMounted(() => getRender())
</script>

<template>
  <div :id="props.id" class="bar-chart" />
</template>

<style lang="less" scoped>
.bar-chart { width: 100%; height: 100%; }
</style>

五、Vue3 TSX/JSX 中使用自定义指令(权限指令实战)

Vue3 中TSX/JSX 不支持 v - 指令语法,需通过 withDirectives + resolveDirective 实现自定义指令(如权限控制、按钮禁用等)。

实战代码(权限指令)

import { 
  createVNode, h, withDirectives, resolveDirective 
} from 'vue'
import { Button, Modal, message } from 'ant-design-vue'
import { ExclamationCircleOutlined } from '@ant-design/icons-vue'

// 解析自定义指令(如权限指令 hasPermi)
const permission = resolveDirective('hasPermi')

// 表格列渲染
const columns = [
  {
    title: '操作',
    customRender: ({ record }) => {
      return h('div', [
        // 普通按钮
        h(Button, {
          type: 'link',
          onClick: () => {
            roleDetailProps.roleId = record.roleId
            roleDetailProps.visible = true
          }
        }, () => '详情'),

        // 带权限指令的按钮
        withDirectives(
          h(Button, {
            type: 'link',
            onClick: () => {
              userAssignmentProps.roleId = record.roleId
              userAssignmentProps.visible = true
            }
          }, () => '用户分配'),
          // 绑定指令:[指令实例, 指令值]
          [[permission, 'szjz_qxgl_yyjsqx_yhfp']]
        ),

        // 删除按钮(权限+二次确认)
        withDirectives(
          h(Button, {
            type: 'link',
            danger: true,
            onClick: () => {
              Modal.confirm({
                title: `是否删除${record.name}?`,
                icon: createVNode(ExclamationCircleOutlined),
                onOk: () => {
                  return axios.removePrivilegeRole(record.id).then(res => {
                    message.success('删除成功')
                    InstanceType.reload()
                  })
                }
              })
            }
          }, () => '删除'),
          [[permission, 'szjz_qxgl_yyjsqx_sc']]
        )
      ])
    }
  }
]

核心语法

// 格式:withDirectives(渲染的VNode, [[指令, 参数]])
withDirectives(h(Button, {}, () => '按钮'), [[指令实例, '指令参数']])

总结

本文覆盖 Vue3+TS 项目五大高频实战场景,所有代码均为企业级生产可用版本:

  1. DOM 打印:自定义页眉页脚、分页、样式隔离,解决打印痛点;
  2. 自动滚动:纯原生实现,悬浮暂停、触底回顶,性能拉满;
  3. ECharts 折线图:渐变、空状态、统一封装,开箱即用;
  4. 自定义柱状图:分段渲染、数值显示、视觉效果拉满;
  5. TSX 自定义指令:权限控制实战,解决 TSX 指令不生效问题。

代码可直接复制到项目中使用,适配 Vue3 + TypeScript + Vite 全场景,欢迎收藏、转发、交流!

更多推荐