• 博客园logo
  • 会员
  • 周边
  • 新闻
  • 博问
  • 闪存
  • 赞助商
  • Chat2DB
    • 搜索
      所有博客
    • 搜索
      当前博客
  • 写随笔 我的博客 短消息 简洁模式
    用户头像
    我的博客 我的园子 账号设置 会员中心 简洁模式 ... 退出登录
    注册 登录
老司机快发车
记录自己遇到的一些问题,以便后期查看
博客园    首页    新随笔    联系   管理    订阅  订阅

VxeTable 手写实现区域复制、粘贴功能(非Pro或付费插件实现)

此组件是用Cursor生成的,VxeTable版本对应4.19.16,其它版本未试过。

VxeTableAreaClip

为 vxe-grid 表格提供 Excel 风格的区域框选、复制与粘贴 能力。通过包裹 vxe-grid,用户可像操作电子表格一样拖拽选区,并使用 Ctrl+C / Ctrl+V(macOS 为 Cmd+C / Cmd+V)在单元格间批量复制数据。

依赖

  • Vue 3
  • vxe-table(vxe-grid)

将整个 VxeTableAreaClip 目录复制到目标项目的组件目录即可使用,无额外 npm 依赖。

功能特性

  • 鼠标拖拽框选矩形区域,支持 Shift + 点击 扩展选区
  • 选区高亮显示(锚点单元格与选中区域样式区分)
  • 复制为 TSV(制表符分隔)格式写入系统剪贴板,可直接粘贴到 Excel
  • 从剪贴板粘贴 TSV 数据,按锚点单元格向右、向下填充
  • 支持固定列(left / right)场景下的单元格定位
  • 可配置单元格是否允许选中 / 粘贴
  • 支持自定义复制格式化与粘贴解析(如 ID ↔ 名称互转)
  • 编辑控件(input、select 等)内保留原生文本选中行为

目录结构

VxeTableAreaClip/
├── index.vue                  # 组件入口,包裹 slot 并绑定事件
├── useVxeTableAreaClip.js     # 核心 composable(选区、复制、粘贴逻辑)
├── utils.js                   # TSV 序列化 / 解析、选区归一化工具
├── vxe-table-area-clip.scss   # 选区高亮与 user-select 样式
└── README.md                  # 本说明文件

基本用法

将 vxe-grid 作为默认插槽内容包裹在组件内,并通过 grid-ref 传入表格实例引用。

<template>
  <VxeTableAreaClip
    ref="clipRef"
    :grid-ref="getGridRef"
    :can-select-cell="canClipCell"
    :can-paste-cell="canClipCell"
    :copy-formatter="clipCopyFormatter"
    :paste-parser="clipPasteParser"
  >
    <vxe-grid ref="gridRef" v-bind="gridOptions" />
  </VxeTableAreaClip>
</template>

<script setup>
import { ref } from 'vue'
import VxeTableAreaClip from './components/VxeTableAreaClip/index.vue'

const gridRef = ref()
const clipRef = ref()

/** 返回 vxe-grid 实例(推荐 getter 函数,避免实例未挂载时取值为空) */
function getGridRef() {
  return gridRef.value
}

/** 仅数据行且存在 field 的列允许框选 */
function canClipCell({ row, column }) {
  return !row._isGroup && !!column?.field
}

/** 复制时将内部值转为可读文本 */
function clipCopyFormatter(row, column, value) {
  if (value == null || value === '') return ''
  if (column.field === 'status') {
    return value === '1' ? '启用' : '禁用'
  }
  return String(value)
}

/** 粘贴时将可读文本解析回内部值 */
function clipPasteParser(row, column, text) {
  const raw = text == null ? '' : String(text).trim()
  if (raw === '') return null
  if (column.field === 'status') {
    if (raw === '启用' || raw === '1') return '1'
    if (raw === '禁用' || raw === '0') return '0'
  }
  return raw
}
</script>

注意:包裹容器需有明确高度(如 height: 100% 或 flex: 1),否则表格区域可能无法正常展示与框选。

Props

参数 类型 默认值 说明
gridRef Object | Function null vxe-grid 实例引用。支持直接传 ref 对象,或传返回实例的 getter 函数(推荐)
canSelectCell Function () => true 判断单元格是否允许被选入选区。参数:{ row, column }
canPasteCell Function () => true 判断单元格是否允许接收粘贴。参数:{ row, column, value }
copyFormatter Function null 复制时格式化单元格值。参数:(row, column, value) => string。未传时默认 String(value)
pasteParser Function null 粘贴时解析剪贴板文本。参数:(row, column, text) => any。未传时原样写入

Events

事件名 参数 说明
selection-change range: { r1, r2, c1, c2 } 选区变化时触发,表示归一化后的行列索引范围
pasted { changes: Array<{ row, column, value }> } 粘贴完成后触发,包含所有被修改的单元格

暴露方法

通过 ref 可调用以下方法(defineExpose):

方法 说明
remount() 卸载并重新挂载事件监听。表格列变更、数据重载后若框选失效,应调用此方法
refreshHighlight() 仅刷新选区高亮,不重新绑定事件

典型调用时机——数据加载完成后:

import { nextTick } from 'vue'

nextTick(() => {
  gridRef.value?.recalculate?.()
  clipRef.value?.remount?.()
})

交互说明

操作 行为
单击单元格 以该单元格为锚点,选区收缩为单格
拖拽 框选矩形区域
Shift + 单击 以已有锚点为起点,扩展选区至目标单元格
Ctrl+C / Cmd+C 复制选区内容到剪贴板(TSV 格式)
Ctrl+V / Cmd+V 从剪贴板粘贴,以锚点单元格为左上角向右、向下填充
单元格内编辑 点击 input / select 等编辑控件时,不触发框选,保留原生输入行为

自定义复制 / 粘贴

当列存储的是 ID、枚举值等内部数据,而用户期望在 Excel 中看到可读文本时,需实现 copyFormatter 与 pasteParser 配对使用。

复制:将 categoryId: 3 格式化为 "电子产品"
粘贴:将 "电子产品" 解析回 3

未实现 pasteParser 时,粘贴的是原始文本;未实现 copyFormatter 时,复制的是单元格原始值。

注意事项

  1. grid-ref 推荐使用 getter 函数(如 :grid-ref="getGridRef"),确保表格挂载后能取到最新实例。
  2. 仅 field 不为空且 visible !== false 的列参与复制粘贴。
  3. 粘贴直接修改行对象(row[field] = value),并调用 vxe-grid 的 updateStatus 刷新编辑状态;不会自动触发保存,需由业务页面自行处理。
  4. 剪贴板读写依赖浏览器权限(navigator.clipboard),非 HTTPS 或权限被拒时复制/粘贴可能静默失败。
  5. 表格列动态变更后(如切换查询条件重载 columns),需调用 clipRef.remount() 重新绑定事件。
  6. 树形表格中,通常通过 canSelectCell 限制仅数据行可参与框选,分组行、汇总行等应排除在外。

移植说明

  1. 将 VxeTableAreaClip 整个目录复制到目标项目。
  2. 按目标项目的路径别名调整 import 路径(示例中使用相对路径 ./components/VxeTableAreaClip/index.vue)。
  3. 确保目标项目已安装并注册 vxe-table。
  4. 若目标项目使用不同的 CSS 变量体系,可按需调整 vxe-table-area-clip.scss 中的高亮颜色(默认使用 --el-color-primary)。

代码

点击查看 index.vue 代码
<template>
  <div ref="wrapRef" class="vxe-table-area-clip-wrap">
    <slot/>
  </div>
</template>

<script setup>
import {nextTick, onBeforeUnmount, onMounted, ref, watch} from 'vue'
import {useVxeTableAreaClip} from './useVxeTableAreaClip'
import './vxe-table-area-clip.scss'

const props = defineProps({
  /** vxe-grid 实例引用,支持 ref 对象或返回实例的 getter 函数 */
  gridRef: {
    type: [Object, Function],
    default: null
  },
  /** 判断单元格是否允许被选入选区,参数为 { row, column } */
  canSelectCell: {
    type: Function,
    default: () => true
  },
  /** 判断单元格是否允许接收粘贴,参数为 { row, column, value } */
  canPasteCell: {
    type: Function,
    default: () => true
  },
  /** 复制时将单元格值格式化为剪贴板文本,参数为 (row, column, value) */
  copyFormatter: {
    type: Function,
    default: null
  },
  /** 粘贴时将剪贴板文本解析为单元格值,参数为 (row, column, text) */
  pasteParser: {
    type: Function,
    default: null
  }
})

/** 组件事件定义 */
const emit = defineEmits(['pasted', 'selection-change'])

/** 包裹容器 DOM 引用 */
const wrapRef = ref()

/** 区域复制粘贴 composable 实例 */
const clip = useVxeTableAreaClip(() => resolveGridRef(props.gridRef), {
  canSelectCell: (...args) => props.canSelectCell(...args),
  canPasteCell: (...args) => props.canPasteCell(...args),
  copyFormatter: (...args) => (props.copyFormatter || ((row, column, value) => (value == null ? '' : String(value))))(...args),
  pasteParser: (...args) => (props.pasteParser || ((row, column, text) => text))(...args),
  onSelectionChange: range => emit('selection-change', range),
  onPasted: payload => emit('pasted', payload)
})

/** 解析 gridRef(支持 ref 对象或 getter 函数) */
function resolveGridRef(gridRef) {
  const raw = typeof gridRef === 'function' ? gridRef() : gridRef
  return raw?.value ?? raw
}

/** 卸载并重新挂载剪贴板事件监听 */
function remountClip() {
  clip.unmount()
  const unbind = clip.mount(wrapRef.value)
  clip.setCleanup(unbind)
}

watch(() => resolveGridRef(props.gridRef), (val) => {
  if (val) nextTick(remountClip)
})

onMounted(() => {
  nextTick(remountClip)
})

onBeforeUnmount(() => {
  clip.unmount()
})

defineExpose({refreshHighlight: clip.refreshHighlight, remount: remountClip})
</script>

<style scoped>
.vxe-table-area-clip-wrap {
  outline: none;
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
}
</style>

点击查看 useVxeTableAreaClip.js 代码
import { ref } from 'vue'
import { normalizeRange, parseTsv, serializeTsv } from './utils'

/** 选区单元格高亮 CSS 类名 */
const SELECTED = 'vxe-area-clip--selected'
/** 选区锚点单元格高亮 CSS 类名 */
const ANCHOR = 'vxe-area-clip--anchor'

/** 从 grid 引用解析 vxe-table 实例 */
function getVxeTable(gridRef) {
  const inst = typeof gridRef === 'function' ? gridRef() : gridRef?.value ?? gridRef
  if (!inst) return null
  return inst.$refs?.xTable || inst
}

/** 从 td 元素提取列 ID(colid) */
function extractColId(td) {
  if (!td) return null
  const cls = typeof td.className === 'string' ? td.className : td.classList?.toString?.() || ''
  const fromClass = cls.match(/\bcol_\d+\b/)?.[0]
  if (fromClass) return fromClass
  return td.getAttribute?.('colid') || null
}

/** 获取列的固定方向类型(left / right / 空) */
function getColumnFixedType(column) {
  const fixed = column?.fixed
  if (fixed === true || fixed === 'left') return 'left'
  if (fixed === 'right') return 'right'
  return ''
}

/** 判断 body wrapper 是否匹配指定固定列类型 */
function matchBodyWrapper(body, fixedType) {
  if (!body?.classList) return false
  if (fixedType === 'left') return body.classList.contains('fixed-left--wrapper')
  if (fixedType === 'right') return body.classList.contains('fixed-right--wrapper')
  return body.classList.contains('body--wrapper')
    && !body.classList.contains('fixed-left--wrapper')
    && !body.classList.contains('fixed-right--wrapper')
}

/** 判断 td 是否为可见的数据单元格 */
function isVisibleBodyTd(td) {
  return td && !td.classList.contains('fixed--hidden')
}

/** 从 td 元素提取行 ID(rowid) */
function extractRowId(td) {
  const rowidEl = td?.querySelector?.('[rowid]') || td?.closest?.('tr')?.querySelector?.('[rowid]')
  return rowidEl?.getAttribute('rowid') || null
}

/** 根据鼠标事件解析对应的表格单元格信息 */
function resolveCellFromEvent(event, gridRef) {
  const $table = getVxeTable(gridRef)
  if (!$table) return null
  let td = event.target.closest('td')
  if (!td) return null
  const colid = extractColId(td)
  if (!colid) return null
  const column = $table.getColumnById?.(colid)
  if (!column?.field) return null
  const rowid = extractRowId(td)
  if (!rowid) return null
  const row = $table.getRowById?.(rowid)
  if (!row) return null
  if (!isVisibleBodyTd(td)) {
    const visibleTd = findTd($table, row, column.field)
    if (visibleTd) td = visibleTd
  }
  return { row, column, field: column.field, td, tr: td.closest('tr') }
}

/** 获取可参与复制粘贴的可见列列表 */
function getClipColumns(gridRef) {
  const $table = getVxeTable(gridRef)
  if (!$table) return []
  return ($table.getColumns?.() || []).filter(c => c.field && c.visible !== false)
}

/** 获取当前可见行数据 */
function getVisibleRows(gridRef) {
  const $table = getVxeTable(gridRef)
  return $table?.getTableData?.().visibleData ?? []
}

/** 根据行列索引解析单元格信息 */
function resolveCellByIndex(gridRef, rowIndex, colIndex, clipColumns, visibleRows) {
  const row = visibleRows[rowIndex]
  const column = clipColumns[colIndex]
  if (!row || !column?.field) return null
  return { row, column, field: column.field }
}

/** 在指定 body 区域内查找可见的 td 元素 */
function findTdInBody(body, rowid, colid) {
  const markers = body.querySelectorAll(`[rowid="${rowid}"]`)
  for (const marker of markers) {
    const tr = marker.closest('tr')
    const td = tr?.querySelector(`td.${colid}`)
    if (isVisibleBodyTd(td)) return td
  }
  return null
}

/** 根据行和字段名查找对应的可见 td 元素 */
function findTd($table, row, field) {
  const column = $table.getColumnByField?.(field)
  const colid = column?.id
  const rowid = row?.rowKey
  if (!colid || !rowid) return null
  const fixedType = getColumnFixedType(column)
  const wrappers = $table.$el?.querySelectorAll('.vxe-table--body-wrapper') || []
  for (const body of wrappers) {
    if (!matchBodyWrapper(body, fixedType)) continue
    const td = findTdInBody(body, rowid, colid)
    if (td) return td
  }
  for (const body of wrappers) {
    const td = findTdInBody(body, rowid, colid)
    if (td) return td
  }
  return null
}

/** vxe-table 区域复制粘贴 composable */
export function useVxeTableAreaClip(gridRef, options = {}) {
  const {
    canSelectCell = () => true,
    canPasteCell = () => true,
    copyFormatter = (row, column, value) => (value == null ? '' : String(value)),
    pasteParser = (row, column, text) => text,
    onSelectionChange,
    onPasted
  } = options

  /** 选区锚点单元格索引 */
  const anchor = ref(null)
  /** 选区焦点单元格索引 */
  const focus = ref(null)
  /** 是否正在拖拽框选 */
  const isDragging = ref(false)
  /** 键盘事件聚焦元素 */
  let focusEl = null
  /** 事件解绑清理函数 */
  let cleanup = null

  /** 清除所有选区高亮样式 */
  function clearHighlight() {
    const $table = getVxeTable(gridRef)
    $table?.$el?.querySelectorAll(`.${SELECTED}, .${ANCHOR}`).forEach(el => {
      el.classList.remove(SELECTED, ANCHOR)
    })
  }

  /** 根据当前锚点与焦点重新绘制选区高亮 */
  function applyHighlight() {
    clearHighlight()
    const range = normalizeRange(anchor.value, focus.value)
    if (!range) return
    const $table = getVxeTable(gridRef)
    if (!$table) return
    const clipColumns = getClipColumns(gridRef)
    const visibleRows = getVisibleRows(gridRef)
    for (let r = range.r1; r <= range.r2; r++) {
      for (let c = range.c1; c <= range.c2; c++) {
        const cell = resolveCellByIndex(gridRef, r, c, clipColumns, visibleRows)
        if (!cell || !canSelectCell({ row: cell.row, column: cell.column })) continue
        const td = findTd($table, cell.row, cell.field)
        if (!td) continue
        td.classList.add(SELECTED)
        if (anchor.value && r === anchor.value.rowIndex && c === anchor.value.colIndex) {
          td.classList.add(ANCHOR)
        }
      }
    }
    onSelectionChange?.(range)
  }

  /** 将单元格信息转换为行列索引 */
  function cellToIndex(cell) {
    const clipColumns = getClipColumns(gridRef)
    const visibleRows = getVisibleRows(gridRef)
    const rowIndex = visibleRows.findIndex(r => r.rowKey === cell.row.rowKey)
    const colIndex = clipColumns.findIndex(c => c.field === cell.field)
    if (rowIndex < 0 || colIndex < 0) return null
    return { rowIndex, colIndex }
  }

  /** 清除浏览器原生文本选区 */
  function clearNativeSelection() {
    window.getSelection?.()?.removeAllRanges?.()
  }

  /** 判断事件目标是否为可编辑控件 */
  function isEditableTarget(target) {
    return !!target?.closest?.('input, textarea, select, .vxe-input, .vxe-select, .vxe-tree-select, .vxe-number-input, .vxe-textarea')
  }

  /** 阻止表格区域的原生文本选中 */
  function onSelectStart(e) {
    if (isEditableTarget(e.target)) return
    const cell = resolveCellFromEvent(e, gridRef)
    if (!cell) return
    if (!canSelectCell({ row: cell.row, column: cell.column })) return
    e.preventDefault()
  }

  /** 鼠标按下时初始化或扩展选区 */
  function onMouseDown(e) {
    if (e.button !== 0) return
    if (isEditableTarget(e.target)) return
    const cell = resolveCellFromEvent(e, gridRef)
    if (!cell) return
    if (!canSelectCell({ row: cell.row, column: cell.column })) return
    const idx = cellToIndex(cell)
    if (!idx) return
    clearNativeSelection()
    focusEl?.focus?.({ preventScroll: true })
    if (e.shiftKey && anchor.value) {
      focus.value = idx
      applyHighlight()
      return
    }
    anchor.value = idx
    focus.value = idx
    isDragging.value = true
    document.addEventListener('mousemove', onDocumentMouseMove, { passive: false })
    applyHighlight()
  }

  /** 鼠标移动时更新选区焦点 */
  function onMouseMove(e) {
    if (!isDragging.value) return
    e.preventDefault()
    clearNativeSelection()
    const cell = resolveCellFromEvent(e, gridRef)
    if (!cell) return
    if (!canSelectCell({ row: cell.row, column: cell.column })) return
    const idx = cellToIndex(cell)
    if (!idx) return
    focus.value = idx
    applyHighlight()
  }

  /** document 级鼠标移动代理(拖拽框选) */
  function onDocumentMouseMove(e) {
    if (!isDragging.value) return
    onMouseMove(e)
  }

  /** 鼠标松开时结束拖拽框选 */
  function onMouseUp() {
    if (isDragging.value) {
      document.removeEventListener('mousemove', onDocumentMouseMove)
      clearNativeSelection()
    }
    isDragging.value = false
  }

  /** 构建当前选区的复制矩阵 */
  function buildCopyMatrix() {
    const range = normalizeRange(anchor.value, focus.value)
    if (!range) return null
    const clipColumns = getClipColumns(gridRef)
    const visibleRows = getVisibleRows(gridRef)
    const matrix = []
    for (let r = range.r1; r <= range.r2; r++) {
      const rowArr = []
      for (let c = range.c1; c <= range.c2; c++) {
        const cell = resolveCellByIndex(gridRef, r, c, clipColumns, visibleRows)
        if (!cell || !canSelectCell({ row: cell.row, column: cell.column })) {
          rowArr.push('')
          continue
        }
        rowArr.push(copyFormatter(cell.row, cell.column, cell.row[cell.field]))
      }
      matrix.push(rowArr)
    }
    return matrix
  }

  /** 将当前选区内容复制到剪贴板 */
  async function copySelection() {
    const matrix = buildCopyMatrix()
    if (!matrix?.length) return
    const text = serializeTsv(matrix)
    try {
      await navigator.clipboard.writeText(text)
    } catch {
    }
  }

  /** 从剪贴板读取内容并粘贴到选区 */
  async function pasteSelection() {
    if (!anchor.value) return
    let text = ''
    try {
      text = await navigator.clipboard.readText()
    } catch {
      return
    }
    const matrix = parseTsv(text)
    if (!matrix.length) return
    const clipColumns = getClipColumns(gridRef)
    const visibleRows = getVisibleRows(gridRef)
    const $table = getVxeTable(gridRef)
    const changes = []
    for (let ri = 0; ri < matrix.length; ri++) {
      for (let ci = 0; ci < matrix[ri].length; ci++) {
        const rowIndex = anchor.value.rowIndex + ri
        const colIndex = anchor.value.colIndex + ci
        const cell = resolveCellByIndex(gridRef, rowIndex, colIndex, clipColumns, visibleRows)
        if (!cell) continue
        const raw = matrix[ri][ci]
        const value = pasteParser(cell.row, cell.column, raw)
        if (!canPasteCell({ row: cell.row, column: cell.column, value })) continue
        cell.row[cell.field] = value
        changes.push({ row: cell.row, column: cell.column, value })
      }
    }
    if (changes.length && $table?.updateStatus) {
      changes.forEach(({ row }) => $table.updateStatus({ row }))
    }
    onPasted?.({ changes })
    applyHighlight()
  }

  /** 处理 Ctrl/Cmd+C/V 快捷键 */
  function onKeyDown(e) {
    if (e.target.closest('input, textarea, select, .vxe-input, .vxe-select, .vxe-tree-select, .vxe-number-input, .vxe-textarea')) return
    if (!anchor.value) return
    const mod = e.ctrlKey || e.metaKey
    if (!mod) return
    if (e.key === 'c' || e.key === 'C') {
      e.preventDefault()
      copySelection()
    } else if (e.key === 'v' || e.key === 'V') {
      e.preventDefault()
      pasteSelection()
    }
  }

  /** 阻止表格区域的原生拖拽行为 */
  function onDragStart(e) {
    if (isEditableTarget(e.target)) return
    const cell = resolveCellFromEvent(e, gridRef)
    if (!cell) return
    if (!canSelectCell({ row: cell.row, column: cell.column })) return
    e.preventDefault()
  }

  /** 绑定鼠标、键盘等事件监听 */
  function bindEvents(rootEl, wrapEl) {
    if (!rootEl) return
    focusEl = wrapEl || rootEl
    rootEl.addEventListener('mousedown', onMouseDown)
    rootEl.addEventListener('mousemove', onMouseMove, { passive: false })
    rootEl.addEventListener('selectstart', onSelectStart)
    rootEl.addEventListener('dragstart', onDragStart)
    document.addEventListener('mouseup', onMouseUp)
    document.addEventListener('keydown', onKeyDown)
  }

  /** 解绑所有事件监听 */
  function unbindEvents(rootEl) {
    if (!rootEl) return
    rootEl.removeEventListener('mousedown', onMouseDown)
    rootEl.removeEventListener('mousemove', onMouseMove)
    rootEl.removeEventListener('selectstart', onSelectStart)
    rootEl.removeEventListener('dragstart', onDragStart)
    document.removeEventListener('mousemove', onDocumentMouseMove)
    document.removeEventListener('mouseup', onMouseUp)
    document.removeEventListener('keydown', onKeyDown)
    isDragging.value = false
    focusEl = null
  }

  /** 挂载事件监听并返回解绑函数 */
  function mount(rootEl) {
    const el = rootEl?.value || rootEl
    if (!el) return
    el.setAttribute('tabindex', '0')
    const $table = getVxeTable(gridRef)
    const tableRoot = $table?.$el || el
    bindEvents(tableRoot, el)
    return () => unbindEvents(tableRoot)
  }

  /** 刷新选区高亮(表格数据或列变化后调用) */
  function refreshHighlight() {
    applyHighlight()
  }

  return {
    anchor,
    focus,
    mount,
    unmount: () => {
      cleanup?.()
      cleanup = null
      clearHighlight()
    },
    refreshHighlight,
    setCleanup: fn => { cleanup = fn },
    remount: (rootEl) => {
      cleanup?.()
      cleanup = mount(rootEl)
    }
  }
}

export default useVxeTableAreaClip

点击查看 utils.js 代码
/** 将锚点与焦点单元格索引归一化为矩形选区(r1/r2/c1/c2) */
export function normalizeRange(anchor, focus) {
  if (!anchor || !focus) return null
  return {
    r1: Math.min(anchor.rowIndex, focus.rowIndex),
    r2: Math.max(anchor.rowIndex, focus.rowIndex),
    c1: Math.min(anchor.colIndex, focus.colIndex),
    c2: Math.max(anchor.colIndex, focus.colIndex)
  }
}

/** 将二维矩阵序列化为制表符分隔的 TSV 文本 */
export function serializeTsv(matrix) {
  return matrix.map(row => row.map(v => v == null ? '' : String(v)).join('\t')).join('\n')
}

/** 将剪贴板 TSV 文本解析为二维矩阵 */
export function parseTsv(text) {
  if (!text) return []
  const lines = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n').split('\n')
  while (lines.length && lines[lines.length - 1] === '') {
    lines.pop()
  }
  return lines.map(line => line.split('\t'))
}

点击查看 vxe-table-area-clip.scss 代码
.vxe-table-area-clip-wrap {
  user-select: none;
  -webkit-user-select: none;

  .vxe-table,
  .vxe-cell,
  .vxe-cell--label {
    user-select: none;
    -webkit-user-select: none;
  }

  input,
  textarea,
  select,
  .vxe-input--inner,
  .vxe-number-input--inner,
  .vxe-textarea--inner {
    user-select: text;
    -webkit-user-select: text;
  }
}

.vxe-area-clip--selected {
  background-color: rgba(64, 158, 255, 0.18) !important;
  box-shadow: inset 0 0 0 1px var(--el-color-primary);
}

.vxe-area-clip--anchor {
  background-color: rgba(64, 158, 255, 0.28) !important;
}

posted @ 2026-07-07 17:50  老司机快发车  阅读(11)  评论(0)    收藏  举报
刷新页面返回顶部
博客园  ©  2004-2026
浙公网安备 33010602011771号 浙ICP备2021040463号-3