+
{{ col.value }}
diff --git a/packages/opendesign/src/table/__demo__/TableSpan.vue b/packages/opendesign/src/table/__demo__/TableSpan.vue
index 71ca586d3..24b68203b 100644
--- a/packages/opendesign/src/table/__demo__/TableSpan.vue
+++ b/packages/opendesign/src/table/__demo__/TableSpan.vue
@@ -38,13 +38,19 @@ function cellSpanFn(rowIdx: number, colIdx: number) {
colspan: 2,
};
}
+ if (rowIdx === 4 && colIdx === 3) {
+ return {
+ colspan: 3,
+ rowspan: 3,
+ };
+ }
}
单元格合并
- name: {{ row.name }}
+ name: {{ row.name }}
diff --git a/packages/opendesign/src/table/style/style.scss b/packages/opendesign/src/table/style/style.scss
index a8092a66a..3a50b745b 100644
--- a/packages/opendesign/src/table/style/style.scss
+++ b/packages/opendesign/src/table/style/style.scss
@@ -30,17 +30,9 @@
line-height: var(--table-text-height);
height: var(--table-cell-height);
}
-
- tbody tr {
- @include hover {
- &:hover {
- background-color: var(--table-row-hover);
- }
- }
- &:active {
- background-color: var(--table-row-active);
- }
- }
+}
+.o-table-highlight {
+ background-color: var(--table-row-hover);
}
.o-table-wrap {
border-radius: var(--table-radius);
@@ -64,11 +56,14 @@
.o-table-border-all,
.o-table-border-frame,
.o-table-border-row {
- tr.last {
+ tr.o-row-last {
td {
border-bottom-color: transparent;
}
}
+ td.o-cell-last-row {
+ border-bottom-color: transparent;
+ }
}
.o-table-border-all,
@@ -77,7 +72,7 @@
td {
border-right: var(--table-border);
- &.last {
+ &.o-cell-last-col {
border-right-color: transparent;
}
}
diff --git a/packages/opendesign/src/table/useTableMeta.ts b/packages/opendesign/src/table/useTableMeta.ts
new file mode 100644
index 000000000..5792e421f
--- /dev/null
+++ b/packages/opendesign/src/table/useTableMeta.ts
@@ -0,0 +1,291 @@
+import { shallowRef, onBeforeUnmount, type Ref } from 'vue';
+import { resolveHtmlElement } from '../_utils/vue-utils';
+import { debounce } from '../_utils/helper';
+
+type CellT = {
+ el: HTMLTableCellElement;
+ // 包含
+ colStart: number;
+ rowStart: number;
+ // 不包含
+ colEnd: number;
+ rowEnd: number;
+ /** 该单元格是否为最后一列 */
+ lastCol: boolean;
+ /** 该单元格是否为最后一行 */
+ lastRow: boolean;
+ // eslint-disable-next-line no-use-before-define
+ section: TableSection;
+};
+type TableSection = {
+ data: (CellT | null)[][];
+ totalCols: number;
+ totalRows: number;
+ rows: HTMLTableRowElement[];
+ sectionEl: HTMLTableSectionElement;
+ scope: 'head' | 'body' | 'foot';
+};
+type TableMetaOptions = {
+ /** 通过 class 在 td 或 th 元素上标记是否为最后一行 */
+ markCellLastRow?: boolean | string;
+ /** 通过 class 在 td 或 th 元素上标记是否为最后一列 */
+ markCellLastCol?: boolean | string;
+ /** 通过 class 在 tr 元素上标记是否为最后一行 */
+ markRowLast?: boolean | string;
+ /** 最后一列的标记是否区分不同的 section */
+ splitBySection?: boolean;
+};
+
+export const DEFAULT_CELL_COL_MARKER = 'o-cell-last-col';
+export const DEFAULT_CELL_ROW_MARKER = 'o-cell-last-row';
+export const DEFAULT_ROW_MARKER = 'o-row-last';
+
+function fillGrid(grid: (CellT | null)[][], cellMeta: CellT) {
+ for (let r = cellMeta.rowStart; r < cellMeta.rowEnd; r++) {
+ if (!grid[r]) grid[r] = [];
+ for (let c = cellMeta.colStart; c < cellMeta.colEnd; c++) {
+ grid[r][c] = cellMeta;
+ }
+ }
+}
+// 处理表格数据
+function processSection(section: HTMLTableSectionElement, scope: 'head' | 'body' | 'foot', cellMap: WeakMap): TableSection {
+ const rows = section.rows;
+ const grid: (CellT | null)[][] = [];
+ const rtn: TableSection = {
+ data: grid,
+ totalRows: 0,
+ totalCols: 0,
+ rows: Array.from(rows),
+ sectionEl: section,
+ scope,
+ };
+ let maxCols = 0;
+
+ // 初始化网格
+ for (let i = 0; i < rows.length; i++) {
+ grid.push([]);
+ }
+
+ // 放置所有单元格并计算最大列数
+ for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {
+ const row = rows[rowIndex];
+ let colIndex = 0;
+
+ for (const cell of Array.from(row.cells)) {
+ const colspan = cell.colSpan || 1;
+ const rowspan = cell.rowSpan || 1;
+ while (grid[rowIndex][colIndex] !== undefined) {
+ colIndex++;
+ }
+
+ // 创建单元格元数据
+ const cellMeta: CellT = {
+ el: cell,
+ colStart: colIndex,
+ rowStart: rowIndex,
+ colEnd: colIndex + colspan,
+ rowEnd: rowIndex + rowspan,
+ lastCol: false,
+ lastRow: false,
+ section: rtn,
+ };
+
+ // 将单元格放入映射表
+ cellMap.set(cell, cellMeta);
+
+ // 在网格中占据单元格的位置
+ fillGrid(grid, cellMeta);
+ maxCols = Math.max(maxCols, cellMeta.colEnd);
+
+ // 移动到下一个可用列位置
+ colIndex += colspan;
+ }
+ }
+ rtn.totalCols = maxCols;
+ rtn.totalRows = rows.length;
+ return rtn;
+}
+// 标准化表格数据,使它们具有相同的列数
+function normalizeSection(section: TableSection, maxCols: number) {
+ // 扩展每行到最大列数
+ section.data.forEach((row) => {
+ if (row.length < maxCols) {
+ row.push(...Array(maxCols - row.length).fill(null));
+ }
+ });
+ section.totalCols = maxCols;
+ return section;
+}
+function markCellEl(cell: CellT, colMarker: string | false | undefined, rowMarker: string | false | undefined) {
+ if (colMarker) {
+ if (cell.lastCol) {
+ cell.el.classList.add(colMarker);
+ } else {
+ cell.el.classList.remove(colMarker);
+ }
+ }
+ if (rowMarker) {
+ if (cell.lastRow) {
+ cell.el.classList.add(rowMarker);
+ } else {
+ cell.el.classList.remove(rowMarker);
+ }
+ }
+}
+function markSection(
+ section: TableSection,
+ isLastSection: boolean,
+ marker: { cellColMarker?: string | false; cellRowMarker?: string | false; rowMarker?: string | false }
+) {
+ const { totalCols, totalRows, data } = section;
+ for (let rowIndex = 0; rowIndex < totalRows; rowIndex++) {
+ const isLastRow = rowIndex === totalRows - 1 && isLastSection;
+ const rowEl = section.rows[rowIndex];
+ if (marker.rowMarker && rowEl) {
+ if (isLastRow) {
+ rowEl.classList.add(marker.rowMarker);
+ } else {
+ rowEl.classList.remove(marker.rowMarker);
+ }
+ }
+ for (let colIndex = 0; colIndex < totalCols; colIndex++) {
+ const cell = data[rowIndex][colIndex];
+ if (cell && rowIndex === cell.rowStart && colIndex === cell.colStart) {
+ // 每个 cell 只处理一次
+ if (cell.colEnd === totalCols) {
+ cell.lastCol = true;
+ }
+ if (cell.rowEnd === totalRows && isLastSection) {
+ cell.lastRow = true;
+ }
+ markCellEl(cell, marker.cellColMarker, marker.cellRowMarker);
+ }
+ }
+ }
+}
+function markTable(sections: Array, options: TableMetaOptions) {
+ const { markCellLastCol, markCellLastRow, markRowLast, splitBySection } = options;
+ const marker = {
+ cellColMarker: markCellLastCol === true ? DEFAULT_CELL_COL_MARKER : markCellLastCol,
+ cellRowMarker: markCellLastRow === true ? DEFAULT_CELL_ROW_MARKER : markCellLastRow,
+ rowMarker: markRowLast === true ? DEFAULT_ROW_MARKER : markRowLast,
+ };
+
+ const validSections = sections.filter(Boolean) as TableSection[];
+ const lastSectionIdx = validSections.length - 1;
+ validSections.forEach((section, sectionIndex) => {
+ const isLastSection = splitBySection || sectionIndex === lastSectionIdx;
+ markSection(section, isLastSection, marker);
+ });
+}
+const processTable = (el: HTMLTableElement, cellMap: WeakMap, options: TableMetaOptions) => {
+ let head = undefined;
+ let foot = undefined;
+ let maxCols = 0;
+ // 处理表格数据
+ if (el.tHead) {
+ head = processSection(el.tHead, 'head', cellMap);
+ maxCols = Math.max(maxCols, head.totalCols);
+ }
+ const bodies = Array.from(el.tBodies).map((tbody) => {
+ const body = processSection(tbody, 'body', cellMap);
+ maxCols = Math.max(maxCols, body.totalCols);
+ return body;
+ });
+ if (el.tFoot) {
+ foot = processSection(el.tFoot, 'foot', cellMap);
+ maxCols = Math.max(maxCols, foot.totalCols);
+ }
+ // 标准化表格数据(处理异常表格)
+ if (head) {
+ normalizeSection(head, maxCols);
+ }
+ bodies.map((section) => normalizeSection(section, maxCols));
+ if (foot) {
+ normalizeSection(foot, maxCols);
+ }
+ // 标记表格
+ markTable([head, ...bodies, foot], options);
+ return {
+ head,
+ bodies,
+ foot,
+ };
+};
+function isSpanChange(record: MutationRecord) {
+ // 不需要检测 record.attributeName,已经在 observe.attributeFilter 中过滤
+ return record.type === 'attributes' && record.target instanceof HTMLTableCellElement;
+}
+function isTableChildChange(record: MutationRecord) {
+ if (record.type === 'childList') {
+ for (const node of record.addedNodes) {
+ if (node instanceof HTMLTableCellElement || node instanceof HTMLTableRowElement || node instanceof HTMLTableSectionElement) {
+ return true;
+ }
+ }
+ for (const node of record.removedNodes) {
+ if (node instanceof HTMLTableCellElement || node instanceof HTMLTableRowElement || node instanceof HTMLTableSectionElement) {
+ return true;
+ }
+ }
+ }
+ return false;
+}
+function shouldRefactorTableMeta(records: MutationRecord[]) {
+ for (const record of records) {
+ if (isSpanChange(record)) {
+ // td 或 th 的 colspan 或 rowspan 属性被修改
+ return true;
+ }
+ if (isTableChildChange(record)) {
+ // td, th, tr, thead, tbody, tfoot 被添加或删除(被移动等于先删除再添加)
+ return true;
+ }
+ }
+ return false;
+}
+export function useTableMeta(elRef: HTMLTableElement | Ref, options: TableMetaOptions = {}) {
+ const cellMap = new WeakMap();
+ const head = shallowRef();
+ const bodies = shallowRef();
+ const foot = shallowRef();
+ let mutationObserver: MutationObserver | null = null;
+
+ const updateMeta = (el: HTMLTableElement) => {
+ const _rtn = processTable(el, cellMap, options);
+ head.value = _rtn.head;
+ bodies.value = _rtn.bodies;
+ foot.value = _rtn.foot;
+ };
+
+ resolveHtmlElement(elRef).then((el) => {
+ if (!(el instanceof HTMLTableElement)) {
+ return;
+ }
+ updateMeta(el);
+ const deBounceUpdateMeta = debounce(updateMeta.bind(null, el), 16, false);
+ mutationObserver = new MutationObserver((mRecords) => {
+ if (shouldRefactorTableMeta(mRecords)) {
+ deBounceUpdateMeta();
+ }
+ });
+ mutationObserver.observe(el, { childList: true, subtree: true, attributes: true, attributeFilter: ['colspan', 'rowspan'] });
+ });
+
+ onBeforeUnmount(() => {
+ mutationObserver?.disconnect();
+ });
+
+ // 获取单元格元数据的函数
+ function getMeta(cellEl: HTMLTableCellElement): CellT | null {
+ return cellMap.get(cellEl) || null;
+ }
+
+ return {
+ head,
+ bodies,
+ foot,
+ getMeta,
+ };
+}
--
Gitee
From d12ac7d6c18c5b76582d2e749cbd942bb6bbb2cb Mon Sep 17 00:00:00 2001
From: sakurayinfei <970412446@qq.com>
Date: Fri, 31 Oct 2025 10:47:10 +0800
Subject: [PATCH 2/2] =?UTF-8?q?fix(table):=20=E4=BC=98=E5=8C=96=E9=AB=98?=
=?UTF-8?q?=E4=BA=AE=E6=A0=B7=E5=BC=8F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
packages/opendesign/src/table/OTable.vue | 29 ++++++++++---------
.../opendesign/src/table/style/style.scss | 5 +++-
2 files changed, 20 insertions(+), 14 deletions(-)
diff --git a/packages/opendesign/src/table/OTable.vue b/packages/opendesign/src/table/OTable.vue
index 9db604617..217a20991 100644
--- a/packages/opendesign/src/table/OTable.vue
+++ b/packages/opendesign/src/table/OTable.vue
@@ -43,16 +43,21 @@ const tableEl = ref();
// 导致无法通过tableData计算元数据以及无法通过vue模板语法设置类名
const tableMeta = useTableMeta(tableEl, { markCellLastCol: true, markCellLastRow: true, markRowLast: true });
-const highlightedCells: Array = [];
+const highlightedDoms: Array = [];
let highlightTrigger: HTMLTableCellElement | null = null;
const clearHighlight = () => {
- highlightedCells.forEach((cell) => {
- cell.classList.remove('o-table-highlight');
+ highlightedDoms.forEach((cell) => {
+ cell.classList.remove('o-table-hover');
+ cell.classList.remove('o-table-active');
});
- highlightedCells.length = 0;
+ highlightedDoms.length = 0;
highlightTrigger = null;
};
-const highlightCell = (cell: HTMLTableCellElement) => {
+const applyHighlight = (dom: HTMLTableRowElement | HTMLTableCellElement, className: string) => {
+ highlightedDoms.push(dom);
+ dom.classList.add(className);
+};
+const highlightTable = (cell: HTMLTableCellElement, type: 'hover' | 'active') => {
if (highlightTrigger === cell) {
// 避免重复添加高亮样式
return;
@@ -66,22 +71,20 @@ const highlightCell = (cell: HTMLTableCellElement) => {
const section = cellMeta.section;
const rowEl = section.rows[cellMeta.rowStart];
const rowSpan = cellMeta.el.rowSpan;
- highlightedCells.push(rowEl);
- rowEl.classList.add('o-table-highlight');
+ const className = `o-table-${type}`;
+ applyHighlight(rowEl, className);
if (rowSpan === 1) {
const rows = section.data[cellMeta.rowStart];
rows.forEach((item) => {
if (item && item.el.parentElement !== rowEl) {
- highlightedCells.push(item.el);
- item.el.classList.add('o-table-highlight');
+ applyHighlight(item.el, className);
}
});
} else {
for (let i = cellMeta.rowStart + 1; i < cellMeta.rowEnd; i++) {
const rowElItem = section.rows[i];
if (rowElItem) {
- highlightedCells.push(rowElItem);
- rowElItem.classList.add('o-table-highlight');
+ applyHighlight(rowElItem, className);
}
}
}
@@ -101,14 +104,14 @@ const handleMouseOver = (e: MouseEvent) => {
if (!target || !isHoverDevice) {
return;
}
- highlightCell(target);
+ highlightTable(target, 'hover');
};
const handleTouchStart = (e: TouchEvent) => {
const target = getTdEl(e.target);
if (!target) {
return;
}
- highlightCell(target);
+ highlightTable(target, 'active');
};
diff --git a/packages/opendesign/src/table/style/style.scss b/packages/opendesign/src/table/style/style.scss
index 3a50b745b..6e32df41c 100644
--- a/packages/opendesign/src/table/style/style.scss
+++ b/packages/opendesign/src/table/style/style.scss
@@ -31,9 +31,12 @@
height: var(--table-cell-height);
}
}
-.o-table-highlight {
+.o-table-hover {
background-color: var(--table-row-hover);
}
+.o-table-active {
+ background-color: var(--table-row-active);
+}
.o-table-wrap {
border-radius: var(--table-radius);
overflow: hidden;
--
Gitee
|