Skip to content

Vue 剪贴板

vtClipboard 插件提供:把当前框选区域复制成 TSV(可直接粘到 Excel),也支持从外部粘贴写回。插件与框架无关,@virt-table/vue 直接透传 vanilla 的实现与类型。

装载

ts
import { VirtTableVue, vtCellSelection, vtClipboard } from '@virt-table/vue';

const options = {
  list,
  itemKey: 'id',
  estimatedSize: 40,
  // vtClipboard 声明了 requires: ['vtCellSelection'],缺了它会被跳过并告警
  plugins: [vtCellSelection(), vtClipboard()],
};

自定义 payload

ts
vtClipboard<Row>({
  mimeType: 'application/x-vt-demo+json',
  onCopy: (ctx) => ({ text: ctx.rows.map((l) => l.join('\t')).join('\n'), payload }),
  onPaste: (ctx) => false,  // 返回 false 交回插件走默认 TSV 写回
});

插件在构造时装载

plugins 在建表时生效,运行时替换该字段不会重新装载。所以回调里要读的开关量放在 ref 里(示例中的「携带结构化 payload」),而不是靠重建插件。

完整语义(默认 TSV 规则、自动补空行、焦点让位、与编辑浮层的关系)见 Vanilla · 剪贴板

示例

微应用尚未挂载。

源码

点击查看源码
vue
<template>
  <div class="demo-wrapper">
    <h3 class="demo-title">Vue 剪贴板</h3>
    <div class="demo-hint">
      拖选一片单元格 → <b>Ctrl/Cmd+C</b> 复制(TSV,可粘到 Excel); 选中目标左上角单元格 →
      <b>Ctrl/Cmd+V</b> 粘贴。粘贴行数超出数据时会自动补空行。
    </div>
    <div class="virt-table-controls">
      <label>
        <input v-model="usePayload" type="checkbox" />
        携带结构化 payload(自定义 MIME,保留原始类型)
      </label>
      <button type="button" class="virt-table-btn" @click="reset">重置数据</button>
    </div>
    <div class="status-text">{{ status }}</div>
    <div style="width: 760px; height: 420px" class="demo-container">
      <VirtTableVue ref="tableRef" :columns="columns" :options="options" />
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, watch } from 'vue';
import {
  VirtTableVue,
  type VueTableColumn,
  vtClipboard,
  vtCellSelection,
} from '@virt-table/vue';
import { faker } from '@faker-js/faker';
import type { VirtTableVueInstance } from '../../virt-table-ref';

interface Row extends Record<string, unknown> {
  id: number;
  name: string;
  dept: string;
  city: string;
  score: number;
}

const depts = ['工程部', '设计部', '市场部', '财务部'];

const makeList = (): Row[] =>
  Array.from({ length: 200 }, (_, i) => ({
    id: i + 1,
    name: faker.person.fullName(),
    dept: depts[i % depts.length]!,
    city: faker.location.city(),
    score: faker.number.int({ min: 0, max: 100 }),
  }));

/** 结构化 payload:演示自定义 MIME 往返(表内复制粘贴可保留原始类型) */
interface DemoPayload {
  kind: 'vt-demo';
  rowCount: number;
  colCount: number;
  cells: unknown[][];
}

const columns: VueTableColumn<Row>[] = [
  { key: 'id', title: 'ID', width: 70 },
  { key: 'name', title: '姓名', width: 170 },
  { key: 'dept', title: '部门', width: 130 },
  { key: 'city', title: '城市', width: 150 },
  { key: 'score', title: '分数', width: 100, align: 'right' },
];

const tableRef = ref<VirtTableVueInstance | null>(null);
const status = ref('等待复制/粘贴…');
/** 开启后复制会额外写入自定义 MIME 的结构化数据,粘贴时优先用它 */
const usePayload = ref(false);

const options = {
  list: makeList(),
  itemKey: 'id',
  estimatedSize: 40,
  buffer: 6,
  border: true,
  // 剪贴板以框选区域为单位,必须同时启用框选
  plugins: [
    vtCellSelection(),
    vtClipboard<Row>({
      mimeType: 'application/x-vt-demo+json',

      onCopy: (ctx) => {
        const size = `${ctx.rows.length} 行 × ${ctx.columnIndexes.length} 列`;
        if (!usePayload.value) {
          status.value = `已复制 ${size}(默认 TSV)`;
          return; // 返回 undefined → 用插件默认的 TSV
        }
        const payload: DemoPayload = {
          kind: 'vt-demo',
          rowCount: ctx.rows.length,
          colCount: ctx.columnIndexes.length,
          cells: ctx.rows.map((line) => [...line]),
        };
        status.value = `已复制 ${size}(TSV + 结构化 payload)`;
        return { text: ctx.rows.map((l) => l.join('\t')).join('\n'), payload };
      },

      onPaste: (ctx) => {
        const payload = ctx.payload as DemoPayload | null;
        if (!payload || payload.kind !== 'vt-demo') {
          // 没有自定义 payload(例如从 Excel 粘来)→ 交回插件走默认 TSV 写回
          status.value = `粘贴 ${ctx.rows.length} 行(默认 TSV 解析)`;
          return false;
        }
        status.value = `粘贴 ${payload.rowCount} 行 × ${payload.colCount} 列(走结构化 payload)`;
        return false; // 本示例仍交回默认写回,只是演示 payload 已拿到
      },
    }),
  ],
};

watch(usePayload, (on) => {
  status.value = on ? '复制将携带结构化 payload' : '复制只写 TSV';
});

const reset = () => {
  tableRef.value?.setList(makeList());
  tableRef.value?.clearCellSelection();
  status.value = '数据已重置';
};
</script>