封装效果
便于快速可选复用定制化交互及主题
封装内容
含左键单击、多选、刷选(圈选)、可配置合并单元格、合并单元格单击事件、右键单击事件等高度定制的交互。
部分v0.1.0代码
useS2Table.ts
import { ref } from "vue";
import { TableSheet, S2Event } from "@antv/s2";
import { debounce } from "lodash";
import { defaultOptions, themeOptions, CustomEvent } from "./config.ts";
import type { Ref } from "vue";
import type {
Config,
Cell,
MergedCell,
State,
DefaultEventConfig,
} from "./types";
import type { S2Theme } from "@antv/s2";
import {
getMergedCellsInfo,
getFilteredCellsByName,
getMergedRowCells,
getRowCells,
getRowCellsByClick,
getRowDataByCells,
} from "./utils.ts";
export function useS2Table(
containerRef: Ref<HTMLElement | null>,
config: Ref<Config>
) {
const s2Ref: Ref<TableSheet | null> = shallowRef(null); // 保存 s2 table 实例
const wrapperSize: Ref<{ width: number; height: number }> = ref({
width: 0,
height: 0,
});
let _config: Config;
// 手动设置尺寸
const resizeTable = (
width: number,
height: number,
reLoadData: boolean = false
) => {
if (s2Ref.value) {
s2Ref.value.changeSheetSize(width, height);
s2Ref.value.render(reLoadData);
}
};
// 强制刷新视图
const renderTable = (reLoadData: boolean = false) => {
if (s2Ref.value) {
resizeTableByConfig(
wrapperSize.value.width,
wrapperSize.value.height,
reLoadData
);
}
};
const resizeTableByConfig = (
width: number,
height: number,
reLoadData: boolean = false
) => {
if (s2Ref.value) {
if (_config.customConfig?.resizeConfig?.calcResizeFunc) {
const size = _config.customConfig?.resizeConfig?.calcResizeFunc(
width,
height
);
s2Ref.value.changeSheetSize(
size?.width || width,
size?.height || height
);
} else {
s2Ref.value.changeSheetSize(width, height);
}
s2Ref.value.render(reLoadData);
}
};
let debounceResize;
const setTheme = (_themeOptions: S2Theme) => {
s2Ref.value && s2Ref.value.setTheme(_themeOptions);
};
const getSelectedCells = () => {
if (s2Ref.value) {
const state = s2Ref.value.interaction.getState();
return state.cells;
}
};
// 用于获取选中的单元格所在的行的原始数据, 默认非响应性, 每行数据多加了个 $rowIndex 索引值
const getSelectedRowsData = (_toRaw: boolean = true) => {
if (s2Ref.value) {
const state = s2Ref.value.interaction.getState();
const ans = getRowDataByCells(state as unknown as State);
return _toRaw ? JSON.parse(JSON.stringify(ans)) : ans;
}
};
const initializeOptions = () => {
_config.options = { ...defaultOptions, ..._config.options };
if (
_config.customConfig?.mergeConfig?.isOn &&
_config.customConfig?.mergeConfig?.mergeKey &&
typeof _config.customConfig?.mergeConfig?.colIndex === "number"
) {
_config.options.mergedCellsInfo = getMergedCellsInfo(
_config.dataConfig.data,
_config.customConfig.mergeConfig.mergeKey,
_config.customConfig?.mergeConfig?.colIndex
);
}
};
const initializeSettings = () => {
const s2 = s2Ref.value;
if (!s2) return;
// 检查配置 看是否启用封装好的交互方法 并执行额外的回调函数
if (
_config.customConfig?.defaultEventsConfig &&
_config.customConfig?.defaultEventsConfig.length > 0
) {
_config.customConfig.defaultEventsConfig.forEach((e) => {
switch (e.eventName) {
// 数据单元格刷选事件
case CustomEvent.DATA_CELL_BRUSH_SELECTION: {
const _event: DefaultEventConfig<CustomEvent.DATA_CELL_BRUSH_SELECTION> =
e;
s2.on(S2Event.DATA_CELL_BRUSH_SELECTION, (targetCells) => {
s2.interaction.changeState(
getRowCells(
s2.interaction.getState() as State,
targetCells as unknown as Cell[]
)
);
_event.callback && _event.callback(targetCells);
});
break;
}
// 数据单元格 click 事件
case CustomEvent.DATA_CELL_CLICK: {
const _event: DefaultEventConfig<CustomEvent.DATA_CELL_CLICK> = e;
s2.on(S2Event.DATA_CELL_CLICK, (event) => {
const cell = s2.getCell(event.target);
s2.interaction.changeState(
getRowCellsByClick(
s2.interaction.getState() as State,
cell as Cell,
event.originalEvent.shiftKey
)
);
const cellRow =
cell.spreadsheet.dataSet.originData[cell.meta.rowIndex];
_event.callback && _event.callback(event, cell, cellRow);
});
break;
}
// 右键单击事件
case CustomEvent.GLOBAL_CONTEXT_MENU: {
const _event: DefaultEventConfig<CustomEvent.GLOBAL_CONTEXT_MENU> =
e;
s2.on(S2Event.GLOBAL_CONTEXT_MENU, (event) => {
const cell = s2.getCell(event.target);
s2.interaction.changeState(
getFilteredCellsByName(
s2.interaction.getState() as State,
cell as unknown as Cell,
e.params?.field || "testStage"
)
);
const cellRow =
cell.spreadsheet.dataSet.originData[cell.meta.rowIndex];
_event.callback && _event.callback(event, cell, cellRow);
});
break;
}
// 监听 mergedCell 的点击事件,自定义点击后的交互操作
case CustomEvent.MERGED_CELLS_CLICK: {
const _event: DefaultEventConfig<CustomEvent.MERGED_CELLS_CLICK> =
e;
s2.on(S2Event.MERGED_CELLS_CLICK, (event) => {
const cell = s2.getCell(event.target);
s2.interaction.changeState(
getMergedRowCells(
cell as unknown as MergedCell,
_config.dataConfig.meta
)
);
const cellRow =
cell.spreadsheet.dataSet.originData[cell.meta.rowIndex];
_event.callback && _event.callback(event, cell, cellRow);
});
break;
}
default:
break;
}
});
}
// 检查配置 监听配置中指定的事件
if (
_config.customConfig?.eventsConfig &&
_config.customConfig?.eventsConfig.length > 0
) {
_config.customConfig?.eventsConfig.forEach((e) => {
s2.on(e.eventName, e.callback);
});
}
};
const initializeTable = () => {
if (containerRef.value && !s2Ref.value) {
s2Ref.value = new TableSheet(
containerRef.value,
_config.dataConfig,
_config.options
);
s2Ref.value.setTheme(themeOptions);
s2Ref.value.render();
// auto resize
// const resizeWrapperElement = typeof _config.customConfig?.resizeConfig?.wrapper === 'string' ?
// document.getElementById(_config.customConfig?.resizeConfig?.wrapper) : _config.customConfig?.resizeConfig?.wrapper;
const wrapperValue = _config.customConfig?.resizeConfig?.wrapper;
const resizeWrapperElement = wrapperValue
? typeof wrapperValue === "string"
? document.getElementById(wrapperValue)
: wrapperValue
: containerRef.value;
if (resizeWrapperElement) {
new ResizeObserver(([entry] = []) => {
const [size] = entry.borderBoxSize || [];
debounceResize(size.inlineSize, size.blockSize);
wrapperSize.value.width = size.inlineSize;
wrapperSize.value.height = size.blockSize;
}).observe(resizeWrapperElement);
}
initializeSettings();
}
};
const initialize = () => {
initializeOptions();
initializeTable();
};
watch(
[config],
([val]) => {
_config = val;
debounceResize = debounce(
resizeTableByConfig,
_config.customConfig?.resizeConfig?.debounceTime || 50
);
initialize();
},
{ immediate: false }
);
return {
s2Ref,
setTheme,
resizeTable,
renderTable,
getSelectedCells,
getSelectedRowsData,
};
}
utils.ts
import {Meta} from "@antv/s2";
import type {InteractionStateInfo} from "@antv/s2";
import type {MergedCell, Cell, CellMeta, State, CellState} from './types';
// 用于初始化时 将指定列 相邻相同内容的单元格合并
export function getMergedCellsInfo(data: Object[], mergeKey: string, colIndex: number) {
let ans: any[] = [];
let k: number = -1;
let lastValue: string = '';
data.forEach((obj: Record<string, any>, index) => {
if (obj[mergeKey] !== lastValue) {
// 单格不变为 "合并单元格" 类型
// if (lastValue !== '' && index - 1 > k) {
// 单格也变为 "合并单元格" 类型
if (lastValue !== '' && index - 1 >= k) {
ans.push(generateArray(k, index - 1, colIndex));
k = index;
lastValue = obj[mergeKey];
} else {
k = index;
lastValue = obj[mergeKey];
}
}
if (index === data.length - 1 && index > k) {
ans.push(generateArray(k, index, colIndex));
}
});
return ans;
}
function generateArray(kA: number, kB: number, colIndex: number) {
const result = [];
for (let i = kA; i <= kB; i++) {
result.push({colIndex: colIndex, rowIndex: i});
}
return result;
}
// 用于获取指定合并单元格所在行的所有单元格
export function getMergedRowCells(mergedCell: MergedCell, columns: Meta[]): InteractionStateInfo {
let ans: CellMeta[] = [];
mergedCell.cells.forEach((cell) => {
let rowIndex = cell.meta.rowIndex;
columns.forEach((column, index) => {
ans.push({
colIndex: index,
rowIndex,
type: 'dataCell',
id: `${rowIndex}-root[&]${column.name}`,
});
})
});
return {
stateName: 'selected',
force: true,
cells: ans,
} as InteractionStateInfo;
}
// 用于过滤单元格组 (保留和指定单元格的指定字段相同的单元格)
export function getFilteredCellsByName(oldState: State, cell: Cell, field: string = 'testStage'): InteractionStateInfo {
let ans: CellMeta[] = [];
let failedRowIndexSet = new Set<number>();
const oldCells = oldState.cells;
const rowIndex = cell.meta.rowIndex;
const data = cell.meta.spreadsheet?.dataSet.displayData;
if (data) {
const filterValue = data[rowIndex][field];
if (filterValue) {
oldCells.forEach((c) => {
if (failedRowIndexSet.has(c.rowIndex) || data[c.rowIndex][field] !== filterValue) {
failedRowIndexSet.add(c.rowIndex);
} else {
ans.push(c);
}
});
}
}
return {
stateName: 'selected',
force: true,
cells: ans,
} as InteractionStateInfo;
}
// 用于获取选中的单元格所在的行的原始数据, 另外每行数据多加了个 $rowIndex 索引值
export function getRowDataByCells(state: State) {
let ans: Object[] = [];
let selectedRowIndexSet: Set<number> = new Set<number>();
const cells: CellState[] = state.cells;
if (state.interactedCells && state.interactedCells.length > 0) {
const originData = state.interactedCells[0].spreadsheet.dataSet.originData;
cells.forEach((cell) => {
selectedRowIndexSet.add(cell.rowIndex);
});
Array.from(selectedRowIndexSet).forEach((rowIndex) => {
originData[rowIndex].$rowIndex = rowIndex;
ans.push(originData[rowIndex]);
});
}
return ans;
}
// 用于将选中的单元格扩展到行粒度(支持刷选/圈选)
export function getRowCells(oldState: State, cells: Cell[]): InteractionStateInfo {
let ans: CellMeta[] = [];
let selectedRowIndexSet: Set<number> = new Set<number>();
const oldCells: CellState[] = oldState.cells;
const columnMetas = cells[0].meta.spreadsheet?.dataSet.meta;
if (columnMetas) {
oldCells.forEach((oldCell) => {
selectedRowIndexSet.add(oldCell.rowIndex);
});
Array.from(selectedRowIndexSet).forEach((rowIndex: number) => {
columnMetas.forEach((column: Meta, index: number) => {
ans.push({
colIndex: index,
rowIndex,
type: 'dataCell',
id: `${rowIndex}-root[&]${column.name}`,
});
});
});
}
return {
stateName: 'selected',
force: true,
cells: ans,
} as InteractionStateInfo;
}
// 用于将选中的单元格扩展到行粒度(支持左键单击、按住 Crtl 或 Shift 左键单击)
export function getRowCellsByClick(oldState: State, cell: Cell, shiftKey: boolean = false): InteractionStateInfo {
let ans: CellMeta[] = [];
let selectedRowIndexSet: Set<number> = new Set<number>();
const oldCells: CellState[] = oldState.cells;
const columnMetas = cell.meta.spreadsheet?.dataSet.meta;
const targetRowIndex = cell.meta.rowIndex;
const targetColIndex = cell.meta.colIndex;
const filteredCells = oldCells.filter((c) => {
return c.rowIndex === targetRowIndex && c.colIndex !== targetColIndex;
// return (c.rowIndex === targetRowIndex && c.colIndex !== targetColIndex) && (c.rowIndex === targetRowIndex && c.colIndex === targetColIndex);
// return !(c.rowIndex === targetRowIndex && c.colIndex === targetColIndex);
});
if (columnMetas) {
oldCells.forEach((oldCell) => {
selectedRowIndexSet.add(oldCell.rowIndex);
});
if (filteredCells.length > 0 && !shiftKey) {
selectedRowIndexSet.delete(targetRowIndex);
} else {
selectedRowIndexSet.add(targetRowIndex);
}
Array.from(selectedRowIndexSet).forEach((rowIndex: number) => {
columnMetas.forEach((column: Meta, index: number) => {
ans.push({
colIndex: index,
rowIndex,
type: 'dataCell',
id: `${rowIndex}-root[&]${column.name}`,
});
});
});
}
return {
stateName: 'selected',
force: true,
cells: ans,
} as InteractionStateInfo;
}
types.d.ts
import { DataCell, S2Event, TableSheet, S2DataConfig } from "@antv/s2";
export type CellMeta = {
colIndex: number;
rowIndex: number;
spreadsheet?: TableSheet;
type?: string;
id?: string;
};
export type Cell = {
meta: CellMeta;
};
export type MergedCell = {
cells: Cell[];
};
export type CellState = {
colIndex: number;
rowIndex: number;
type: string;
id: string;
};
export type State = {
cells: CellState[];
interactedCells?: any[];
[key: any]: any;
};
export type CustomMergeConfig = {
isOn: boolean;
mergeKey?: string;
colIndex?: number;
};
import { CustomEvent } from "./config.ts";
// 回调函数类型映射
type EventCallbackMappings = {
[CustomEvent.MERGED_CELLS_CLICK]: (
event: GraphEvent,
cell: Object,
cellRow: Object
) => void;
[CustomEvent.GLOBAL_CONTEXT_MENU]: (
event: GraphEvent,
cell: Object,
cellRow: Object
) => void;
[CustomEvent.DATA_CELL_CLICK]: (
event: GraphEvent,
cell: DataCell,
cellRow: Object
) => void;
[CustomEvent.DATA_CELL_BRUSH_SELECTION]: (
cells: (DataCell | CellMeta)[]
) => void;
};
// 使用泛型和条件类型定义 DefaultEventConfig
export type DefaultEventConfig<T extends CustomEvent> = {
eventName: T;
isOn: boolean;
params?: { [key: string]: any };
callback?: EventCallbackMappings[T];
};
export type EventConfig = {
eventName: S2Event;
isOn: boolean;
callback: (param) => void;
};
export type ResizeConfig = {
wrapper?: string | HTMLElement; // 可以传元素的 id, 或直接传 DOM 元素, 不指定时默认使用 container
debounceTime?: number;
calcResizeFunc?: (
width: number,
height: number
) => { width: number; height: number };
};
export type CustomConfig = {
mergeConfig?: CustomMergeConfig;
defaultEventsConfig?: DefaultEventConfig[];
eventsConfig?: EventConfig[];
resizeConfig?: ResizeConfig;
};
export type Config = {
dataConfig: S2DataConfig;
options?: S2Options;
customConfig?: CustomConfig;
};
events.ts
import {S2Event, BaseEvent} from '@antv/s2';
export class ContextMenuInteraction extends BaseEvent {
bindEvents() {
this.spreadsheet.on(S2Event.GLOBAL_CONTEXT_MENU, (event) => {
// 禁止弹出右键菜单
event.preventDefault();
});
}
}
config.ts
import type {S2Theme, TextTheme, SplitLine} from '@antv/s2';
import {ContextMenuInteraction} from "@/views/overview/overview-content/cp-pane/events.ts";
const interactionState = {
highlight: {
backgroundColor: '#E7E1F3',
borderColor: '#5D2DCD',
borderWidth: 0
},
hover: {
backgroundColor: '#E7E1F3',
borderColor: '#5D2DCD',
borderWidth: 0
},
hoverFocus: {
backgroundColor: '#E7E1F3',
borderColor: '#5D2DCD',
borderWidth: 0
},
selected: {
backgroundColor: '#EEEEFF',
borderColor: '#5D2DCD',
borderWidth: 0
},
prepareSelect: {
backgroundColor: '#EEEEFF',
borderColor: '#5D2DCD',
borderWidth: 1
}
};
export const themeOptions: S2Theme = {
dataCell: {
cell: {
backgroundColor: "#fff",
crossBackgroundColor: "#fff",
horizontalBorderColor: "#D7D8DC",
verticalBorderColor: "#D7D8DC",
horizontalBorderWidth: 1,
verticalBorderWidth: 1,
interactionState: interactionState,
},
text: {
textAlign: 'center',
textBaseline: 'middle',
} as TextTheme
},
colCell: {
cell: {
backgroundColor: "#E0E0E0",
horizontalBorderColor: "#D7D8DC",
verticalBorderColor: "#D7D8DC",
horizontalBorderWidth: 1,
verticalBorderWidth: 1,
interactionState: interactionState,
}
},
prepareSelectMask: {
backgroundColor: 'rgba(93,45,205,0.3)',
borderColor: '#5D2DCD',
borderWidth: 2,
},
splitLine: {
horizontalBorderColor: "#D7D8DC",
horizontalBorderColorOpacity: 1,
horizontalBorderColorWidth: 1,
verticalBorderColor: "#D7D8DC",
verticalBorderColorOpacity: 1,
verticalBorderColorWidth: 1,
showShadow: false,
} as SplitLine,
};
export const defaultOptions = {
width: 1024,
height: 360,
interaction: {
resize: false,
hoverHighlight: false,
customInteractions: [
{
key: 'ContextMenuInteraction',
interaction: ContextMenuInteraction,
}
],
}
};
export enum CustomEvent {
MERGED_CELLS_CLICK = 'row-cell:click',
GLOBAL_CONTEXT_MENU = 'global:context-menu',
DATA_CELL_CLICK = 'data-cell:click',
DATA_CELL_BRUSH_SELECTION = 'data-cell:brush-selection',
}
Comments NOTHING