Skip to content

React 剪贴板

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

装载

tsx
import { VirtTableReact, vtCellSelection, vtClipboard } from '@virt-table/react';

// 插件实例只能建一次:不放进 useMemo,每次渲染都会新建插件对象
const plugins = React.useMemo(() => [vtCellSelection(), vtClipboard()], []);

vtClipboard 声明了 requires: ['vtCellSelection']——没装框选插件时它会被跳过并告警,不会静默失效。

自定义 payload

tsx
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 在建表时生效,运行时替换该字段不会重新装载。所以回调里要读的开关量走 useRef(示例中的「携带结构化 payload」),否则拿到的是建表那一刻的闭包值。

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

示例

微应用尚未挂载。

源码

点击查看源码
tsx
import React from 'react';
import {
  VirtTableReact,
  type ReactTableColumn,
  type VirtTableRef,
  vtClipboard,
  vtCellSelection,
} from '@virt-table/react';
import { faker } from '@faker-js/faker';

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: ReactTableColumn<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' },
];

export default function ClipboardTable() {
  const tableRef = React.useRef<VirtTableRef>(null);
  const [status, setStatus] = React.useState('等待复制/粘贴…');
  /** 开启后复制会额外写入自定义 MIME 的结构化数据,粘贴时优先用它 */
  const [usePayload, setUsePayload] = React.useState(false);
  const list = React.useMemo(makeList, []);
  // 插件在建表时装载一次,回调里要读到最新开关值
  const payloadRef = React.useRef(usePayload);
  payloadRef.current = usePayload;

  // 插件实例只能建一次:放进 useMemo,否则每次渲染都会新建插件对象
  const plugins = React.useMemo(
    () => [
      vtCellSelection(),
      vtClipboard<Row>({
        mimeType: 'application/x-vt-demo+json',

        onCopy: (ctx) => {
          const size = `${ctx.rows.length} 行 × ${ctx.columnIndexes.length} 列`;
          if (!payloadRef.current) {
            setStatus(`已复制 ${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]),
          };
          setStatus(`已复制 ${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 写回
            setStatus(`粘贴 ${ctx.rows.length} 行(默认 TSV 解析)`);
            return false;
          }
          setStatus(
            `粘贴 ${payload.rowCount} 行 × ${payload.colCount} 列(走结构化 payload)`,
          );
          return false; // 本示例仍交回默认写回,只是演示 payload 已拿到
        },
      }),
    ],
    [],
  );

  return (
    <div className="demo-wrapper">
      <h3 className="demo-title">React 剪贴板</h3>
      <div className="demo-hint">
        拖选一片单元格 → <b>Ctrl/Cmd+C</b> 复制(TSV,可粘到 Excel);选中目标左上角单元格 →{' '}
        <b>Ctrl/Cmd+V</b> 粘贴。粘贴行数超出数据时会自动补空行。
      </div>
      <div className="virt-table-controls">
        <label>
          <input
            type="checkbox"
            checked={usePayload}
            onChange={(e) => {
              setUsePayload(e.target.checked);
              setStatus(e.target.checked ? '复制将携带结构化 payload' : '复制只写 TSV');
            }}
          />
          携带结构化 payload(自定义 MIME,保留原始类型)
        </label>
        <button
          type="button"
          className="virt-table-btn"
          onClick={() => {
            tableRef.current?.setList(makeList());
            tableRef.current?.clearCellSelection();
            setStatus('数据已重置');
          }}
        >
          重置数据
        </button>
      </div>
      <div className="status-text">{status}</div>
      <div style={{ width: 760, height: 420 }} className="demo-container">
        <VirtTableReact
          ref={tableRef}
          columns={columns}
          options={{ list, itemKey: 'id', estimatedSize: 40, buffer: 6, border: true, plugins }}
        />
      </div>
    </div>
  );
}