| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765 |
- // Excel风格数据表格组件
- document.addEventListener('DOMContentLoaded', function() {
- // 初始化数据表格
- initDataTables();
- });
- /**
- * 初始化所有数据表格
- */
- function initDataTables() {
- // 初始化拟合数据表格
- initFitDataTable();
- // 初始化回归分析表格
- initRegressionTable();
- }
- /**
- * 初始化拟合数据表格
- */
- function initFitDataTable() {
- const tableContainer = document.querySelector('#fit-tab .table-container');
- if (!tableContainer) return;
- // 清空现有内容
- tableContainer.innerHTML = '';
- // 创建Excel风格表格
- const excelTable = createExcelTable('fit-excel-table', 2); // X和Y两列
- tableContainer.appendChild(excelTable);
- // 添加示例数据
- const fitTable = document.getElementById('fit-excel-table');
- addExcelRow(fitTable, [1, 2]);
- addExcelRow(fitTable, [2, 4]);
- addExcelRow(fitTable, [3, 9]);
- // 添加CSV粘贴支持
- enableCSVPaste(fitTable);
- // 添加表格操作按钮
- updateTableActions('fit-tab', fitTable);
- }
- /**
- * 初始化回归分析表格
- */
- function initRegressionTable() {
- // 表格模式
- const tableContainer = document.querySelector('#table-input-mode .table-container');
- if (tableContainer) {
- // 清空现有内容
- tableContainer.innerHTML = '';
- // 创建Excel风格表格
- const excelTable = createExcelTable('regression-excel-table', 3); // X1, X2和Y三列
- tableContainer.appendChild(excelTable);
- // 添加示例数据
- const regressionTable = document.getElementById('regression-excel-table');
- addExcelRow(regressionTable, [1, 2, 3]);
- addExcelRow(regressionTable, [4, 5, 6]);
- // 添加CSV粘贴支持
- enableCSVPaste(regressionTable);
- // 添加表格操作按钮
- updateTableActions('regression-tab', regressionTable);
- }
- // CSV粘贴模式
- const csvTextarea = document.getElementById('regression-data');
- if (csvTextarea) {
- // 为CSV文本区域添加粘贴处理
- csvTextarea.addEventListener('paste', handleCSVPaste);
- }
- // 切换输入模式按钮
- const tableModeBtn = document.getElementById('table-mode-btn');
- const csvModeBtn = document.getElementById('csv-mode-btn');
- const tableInputMode = document.getElementById('table-input-mode');
- const csvInputMode = document.getElementById('csv-input-mode');
- if (tableModeBtn && csvModeBtn) {
- tableModeBtn.addEventListener('click', function() {
- tableModeBtn.classList.add('active');
- csvModeBtn.classList.remove('active');
- tableInputMode.classList.add('active');
- csvInputMode.classList.remove('active');
- });
- csvModeBtn.addEventListener('click', function() {
- csvModeBtn.classList.add('active');
- tableModeBtn.classList.remove('active');
- csvInputMode.classList.add('active');
- tableInputMode.classList.remove('active');
- });
- }
- }
- /**
- * 创建Excel风格表格
- * @param {string} id - 表格ID
- * @param {number} columnCount - 初始列数
- * @returns {HTMLElement} - 创建的表格元素
- */
- function createExcelTable(id, columnCount) {
- const table = document.createElement('div');
- table.id = id;
- table.className = 'excel-table';
- // 创建表头
- const header = document.createElement('div');
- header.className = 'excel-header';
- // 添加空白角落单元格
- const cornerCell = document.createElement('div');
- cornerCell.className = 'excel-corner-cell';
- header.appendChild(cornerCell);
- // 添加列标题 (A, B, C...)
- for (let i = 0; i < columnCount; i++) {
- const columnHeader = document.createElement('div');
- columnHeader.className = 'excel-column-header';
- // 创建列标题和删除按钮的容器
- const columnHeaderContent = document.createElement('div');
- columnHeaderContent.className = 'column-header-content';
- columnHeaderContent.textContent = String.fromCharCode(65 + i); // A, B, C...
- // 创建删除列按钮
- const deleteColBtn = document.createElement('div');
- deleteColBtn.className = 'delete-col-btn';
- deleteColBtn.innerHTML = '×';
- deleteColBtn.title = '删除列';
- deleteColBtn.addEventListener('click', function(e) {
- e.stopPropagation();
- // 获取当前列的索引,而不是使用创建时的索引
- const currentHeaders = table.querySelectorAll('.excel-column-header');
- const currentIndex = Array.from(currentHeaders).indexOf(columnHeader);
- deleteExcelColumn(table, currentIndex);
- });
- columnHeader.appendChild(columnHeaderContent);
- columnHeader.appendChild(deleteColBtn);
- header.appendChild(columnHeader);
- }
- // 添加添加列按钮
- const addColumnBtn = document.createElement('div');
- addColumnBtn.className = 'excel-add-column';
- addColumnBtn.innerHTML = '+';
- addColumnBtn.title = '添加列';
- addColumnBtn.addEventListener('click', function() {
- addExcelColumn(table);
- });
- header.appendChild(addColumnBtn);
- table.appendChild(header);
- // 创建表格内容区域
- const body = document.createElement('div');
- body.className = 'excel-body';
- table.appendChild(body);
- // 添加添加行按钮
- const footer = document.createElement('div');
- footer.className = 'excel-footer';
- const addRowBtn = document.createElement('div');
- addRowBtn.className = 'excel-add-row';
- addRowBtn.innerHTML = '+';
- addRowBtn.title = '添加行';
- addRowBtn.addEventListener('click', function() {
- addExcelRow(table);
- });
- footer.appendChild(addRowBtn);
- table.appendChild(footer);
- return table;
- }
- /**
- * 添加Excel表格行
- * @param {HTMLElement} table - 表格元素
- * @param {Array} values - 行数据值
- * @returns {HTMLElement} - 创建的行元素
- */
- function addExcelRow(table, values = []) {
- const body = table.querySelector('.excel-body');
- const rowIndex = body.children.length;
- const row = document.createElement('div');
- row.className = 'excel-row';
- row.dataset.index = rowIndex;
- // 添加行号和删除行按钮
- const rowHeader = document.createElement('div');
- rowHeader.className = 'excel-row-header';
- // 创建行号和删除按钮的容器
- const rowHeaderContent = document.createElement('div');
- rowHeaderContent.className = 'row-header-content';
- rowHeaderContent.textContent = rowIndex + 1;
- // 创建删除行按钮
- const deleteRowBtn = document.createElement('div');
- deleteRowBtn.className = 'delete-row-btn';
- deleteRowBtn.innerHTML = '×';
- deleteRowBtn.title = '删除行';
- deleteRowBtn.addEventListener('click', function(e) {
- e.stopPropagation();
- deleteExcelRow(table, rowIndex);
- });
- rowHeader.appendChild(rowHeaderContent);
- rowHeader.appendChild(deleteRowBtn);
- row.appendChild(rowHeader);
- // 获取列数
- const columnCount = table.querySelector('.excel-header').children.length - 2; // 减去角落单元格和添加列按钮
- // 添加单元格
- for (let i = 0; i < columnCount; i++) {
- const cell = document.createElement('div');
- cell.className = 'excel-cell';
- cell.dataset.row = rowIndex;
- cell.dataset.col = i;
- cell.contentEditable = true;
- // 设置初始值
- if (values && values[i] !== undefined) {
- cell.textContent = values[i];
- }
- // 添加单元格事件
- cell.addEventListener('focus', function() {
- this.classList.add('active');
- });
- cell.addEventListener('blur', function() {
- this.classList.remove('active');
- });
- cell.addEventListener('keydown', function(e) {
- handleCellKeydown(e, table, this);
- });
- row.appendChild(cell);
- }
- body.appendChild(row);
- return row;
- }
- /**
- * 添加Excel表格列
- * @param {HTMLElement} table - 表格元素
- */
- function addExcelColumn(table) {
- // 更新表头
- const header = table.querySelector('.excel-header');
- const columnCount = header.children.length - 2; // 减去角落单元格和添加列按钮
- // 在添加列按钮前插入新列标题
- const columnHeader = document.createElement('div');
- columnHeader.className = 'excel-column-header';
- // 创建列标题和删除按钮的容器
- const columnHeaderContent = document.createElement('div');
- columnHeaderContent.className = 'column-header-content';
- columnHeaderContent.textContent = String.fromCharCode(65 + columnCount); // A, B, C...
- // 创建删除列按钮
- const deleteColBtn = document.createElement('div');
- deleteColBtn.className = 'delete-col-btn';
- deleteColBtn.innerHTML = '×';
- deleteColBtn.title = '删除列';
- deleteColBtn.addEventListener('click', function(e) {
- e.stopPropagation();
- // 获取当前列的索引,而不是使用创建时的索引
- const currentHeaders = table.querySelectorAll('.excel-column-header');
- const currentIndex = Array.from(currentHeaders).indexOf(columnHeader);
- deleteExcelColumn(table, currentIndex);
- });
- columnHeader.appendChild(columnHeaderContent);
- columnHeader.appendChild(deleteColBtn);
- header.insertBefore(columnHeader, header.lastChild);
- // 为每一行添加新单元格
- const rows = table.querySelectorAll('.excel-row');
- rows.forEach((row, rowIndex) => {
- const cell = document.createElement('div');
- cell.className = 'excel-cell';
- cell.dataset.row = rowIndex;
- cell.dataset.col = columnCount;
- cell.contentEditable = true;
- // 添加单元格事件
- cell.addEventListener('focus', function() {
- this.classList.add('active');
- });
- cell.addEventListener('blur', function() {
- this.classList.remove('active');
- });
- cell.addEventListener('keydown', function(e) {
- handleCellKeydown(e, table, this);
- });
- row.appendChild(cell);
- });
- }
- /**
- * 处理单元格键盘事件
- * @param {Event} e - 键盘事件
- * @param {HTMLElement} table - 表格元素
- * @param {HTMLElement} cell - 当前单元格
- */
- function handleCellKeydown(e, table, cell) {
- const row = parseInt(cell.dataset.row);
- const col = parseInt(cell.dataset.col);
- // 按Tab键移动到下一个单元格
- if (e.key === 'Tab') {
- e.preventDefault();
- const nextCell = e.shiftKey ?
- findCell(table, row, col - 1) :
- findCell(table, row, col + 1);
- if (nextCell) {
- nextCell.focus();
- } else if (!e.shiftKey) {
- // 如果是最后一个单元格,添加新行并聚焦第一个单元格
- const newRow = addExcelRow(table);
- const firstCell = newRow.querySelector('.excel-cell');
- if (firstCell) firstCell.focus();
- }
- }
- // 按Enter键移动到下一行
- if (e.key === 'Enter') {
- e.preventDefault();
- const nextCell = e.shiftKey ?
- findCell(table, row - 1, col) :
- findCell(table, row + 1, col);
- if (nextCell) {
- nextCell.focus();
- } else if (!e.shiftKey) {
- // 如果是最后一行,添加新行并聚焦相同列的单元格
- const newRow = addExcelRow(table);
- const sameColCell = newRow.querySelector(`.excel-cell[data-col="${col}"]`);
- if (sameColCell) sameColCell.focus();
- }
- }
- // 按方向键移动
- if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
- e.preventDefault();
- let nextRow = row;
- let nextCol = col;
- if (e.key === 'ArrowUp') nextRow--;
- if (e.key === 'ArrowDown') nextRow++;
- if (e.key === 'ArrowLeft') nextCol--;
- if (e.key === 'ArrowRight') nextCol++;
- const nextCell = findCell(table, nextRow, nextCol);
- if (nextCell) nextCell.focus();
- }
- }
- /**
- * 查找表格中的单元格
- * @param {HTMLElement} table - 表格元素
- * @param {number} row - 行索引
- * @param {number} col - 列索引
- * @returns {HTMLElement|null} - 找到的单元格或null
- */
- function findCell(table, row, col) {
- if (row < 0 || col < 0) return null;
- return table.querySelector(`.excel-cell[data-row="${row}"][data-col="${col}"]`);
- }
- /**
- * 启用CSV粘贴功能
- * @param {HTMLElement} table - 表格元素
- */
- function enableCSVPaste(table) {
- table.addEventListener('paste', function(e) {
- e.preventDefault();
- // 获取粘贴的文本
- const clipboardData = e.clipboardData || window.clipboardData;
- const pastedData = clipboardData.getData('text');
- // 处理CSV数据
- processCSVData(table, pastedData);
- });
- }
- /**
- * 处理CSV数据
- * @param {HTMLElement} table - 表格元素
- * @param {string} csvData - CSV格式的数据
- */
- function processCSVData(table, csvData) {
- // 清空现有数据
- const body = table.querySelector('.excel-body');
- body.innerHTML = '';
- // 解析CSV数据
- const rows = csvData.trim().split(/\r?\n/);
- const maxCols = rows.reduce((max, row) => {
- const cols = row.split(/[,\t]/);
- return Math.max(max, cols.length);
- }, 0);
- // 调整列数
- adjustColumnCount(table, maxCols);
- // 添加数据行
- rows.forEach(row => {
- const values = row.split(/[,\t]/);
- addExcelRow(table, values);
- });
- }
- /**
- * 调整表格列数
- * @param {HTMLElement} table - 表格元素
- * @param {number} columnCount - 目标列数
- */
- function adjustColumnCount(table, columnCount) {
- const header = table.querySelector('.excel-header');
- const currentColumnCount = header.children.length - 2; // 减去角落单元格和添加列按钮
- // 如果需要添加列
- for (let i = currentColumnCount; i < columnCount; i++) {
- addExcelColumn(table);
- }
- }
- /**
- * 处理CSV文本区域的粘贴事件
- * @param {Event} e - 粘贴事件
- */
- function handleCSVPaste(e) {
- // 获取粘贴的文本
- const clipboardData = e.clipboardData || window.clipboardData;
- const pastedData = clipboardData.getData('text');
- // 如果是CSV格式,直接替换文本区域内容
- if (pastedData.includes(',') || pastedData.includes('\t')) {
- e.preventDefault();
- e.target.value = pastedData;
- }
- }
- /**
- * 更新表格操作按钮
- * @param {string} tabId - 标签页ID
- * @param {HTMLElement} table - 表格元素
- */
- function updateTableActions(tabId, table) {
- const actionsContainer = document.querySelector(`#${tabId} .table-actions`);
- if (!actionsContainer) return;
- // 清空现有按钮
- const buttonsContainer = actionsContainer.querySelector('div');
- if (buttonsContainer) {
- buttonsContainer.innerHTML = '';
- // 添加行按钮
- const addRowBtn = document.createElement('button');
- addRowBtn.id = tabId === 'fit-tab' ? 'add-row' : 'add-regression-row';
- addRowBtn.textContent = '添加行';
- addRowBtn.addEventListener('click', function() {
- addExcelRow(table);
- });
- buttonsContainer.appendChild(addRowBtn);
- // 添加列按钮(仅回归分析)
- if (tabId === 'regression-tab') {
- const addColBtn = document.createElement('button');
- addColBtn.id = 'add-regression-column';
- addColBtn.textContent = '添加变量';
- addColBtn.addEventListener('click', function() {
- addExcelColumn(table);
- });
- buttonsContainer.appendChild(addColBtn);
- }
- // 导入导出按钮(仅拟合数据)
- if (tabId === 'fit-tab') {
- const importBtn = document.createElement('button');
- importBtn.id = 'import-data';
- importBtn.textContent = '导入数据';
- importBtn.addEventListener('click', function() {
- // 创建隐藏的文件输入
- const fileInput = document.createElement('input');
- fileInput.type = 'file';
- fileInput.accept = '.csv,.txt';
- fileInput.style.display = 'none';
- document.body.appendChild(fileInput);
- fileInput.addEventListener('change', function() {
- const file = this.files[0];
- if (file) {
- const reader = new FileReader();
- reader.onload = function(e) {
- processCSVData(table, e.target.result);
- };
- reader.readAsText(file);
- }
- document.body.removeChild(fileInput);
- });
- fileInput.click();
- });
- buttonsContainer.appendChild(importBtn);
- const exportBtn = document.createElement('button');
- exportBtn.id = 'export-data';
- exportBtn.textContent = '导出数据';
- exportBtn.addEventListener('click', function() {
- exportTableData(table);
- });
- buttonsContainer.appendChild(exportBtn);
- }
- }
- }
- /**
- * 导出表格数据为CSV
- * @param {HTMLElement} table - 表格元素
- */
- function exportTableData(table) {
- const rows = table.querySelectorAll('.excel-row');
- let csvContent = '';
- rows.forEach(row => {
- const cells = row.querySelectorAll('.excel-cell');
- const rowData = Array.from(cells).map(cell => {
- // 处理CSV特殊字符
- let value = cell.textContent.trim();
- if (value.includes(',') || value.includes('"') || value.includes('\n')) {
- value = '"' + value.replace(/"/g, '""') + '"';
- }
- return value;
- }).join(',');
- csvContent += rowData + '\n';
- });
- // 创建下载链接
- const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
- const url = URL.createObjectURL(blob);
- const link = document.createElement('a');
- link.setAttribute('href', url);
- link.setAttribute('download', 'data.csv');
- link.style.display = 'none';
- document.body.appendChild(link);
- link.click();
- document.body.removeChild(link);
- }
- /**
- * 获取表格数据
- * @param {HTMLElement} table - 表格元素
- * @returns {Array} - 表格数据数组
- */
- function getTableData(table) {
- const rows = table.querySelectorAll('.excel-row');
- const data = [];
- rows.forEach(row => {
- const cells = row.querySelectorAll('.excel-cell');
- const rowData = Array.from(cells).map(cell => {
- const value = cell.textContent.trim();
- return isNaN(value) ? value : parseFloat(value);
- });
- data.push(rowData);
- });
- return data;
- }
- /**
- * 删除Excel表格行
- * @param {HTMLElement} table - 表格元素
- * @param {number} rowIndex - 要删除的行索引
- */
- function deleteExcelRow(table, rowIndex) {
- const body = table.querySelector('.excel-body');
- const rows = body.querySelectorAll('.excel-row');
- // 确保行索引有效
- if (rowIndex < 0 || rowIndex >= rows.length) return;
- // 删除指定行
- body.removeChild(rows[rowIndex]);
- // 更新剩余行的索引和行号
- const remainingRows = body.querySelectorAll('.excel-row');
- remainingRows.forEach((row, idx) => {
- row.dataset.index = idx;
- const rowHeaderContent = row.querySelector('.row-header-content');
- if (rowHeaderContent) {
- rowHeaderContent.textContent = idx + 1;
- }
- // 更新单元格的行索引
- const cells = row.querySelectorAll('.excel-cell');
- cells.forEach(cell => {
- cell.dataset.row = idx;
- });
- });
- }
- /**
- * 删除Excel表格列
- * @param {HTMLElement} table - 表格元素
- * @param {number} colIndex - 要删除的列索引
- */
- function deleteExcelColumn(table, colIndex) {
- // 确保至少保留一列
- const header = table.querySelector('.excel-header');
- const columnCount = header.children.length - 2; // 减去角落单元格和添加列按钮
- if (columnCount <= 1) {
- alert('表格至少需要保留一列');
- return;
- }
- // 删除列标题
- const columnHeaders = header.querySelectorAll('.excel-column-header');
- if (colIndex < columnHeaders.length) {
- header.removeChild(columnHeaders[colIndex]);
- }
- // 删除每行中的对应单元格
- const rows = table.querySelectorAll('.excel-row');
- rows.forEach(row => {
- const cells = row.querySelectorAll('.excel-cell');
- if (colIndex < cells.length) {
- row.removeChild(cells[colIndex]);
- }
- // 更新剩余单元格的列索引
- const remainingCells = row.querySelectorAll('.excel-cell');
- remainingCells.forEach((cell, idx) => {
- cell.dataset.col = idx;
- });
- });
- // 更新列标题 - 重新分配A, B, C...标签
- const updatedHeaders = header.querySelectorAll('.excel-column-header');
- updatedHeaders.forEach((header, idx) => {
- const headerContent = header.querySelector('.column-header-content');
- if (headerContent) {
- headerContent.textContent = String.fromCharCode(65 + idx); // A, B, C...
- }
- });
- }
- /**
- * 从Excel表格获取数据点
- * @param {HTMLElement} table - 表格元素
- * @returns {Array} - 数据点数组,每个数据点包含x和y属性
- */
- function getDataPointsFromExcelTable(table) {
- const rows = table.querySelectorAll('.excel-row');
- const dataPoints = [];
- // 检查表格是否有足够的列
- const firstRow = table.querySelector('.excel-row');
- if (!firstRow) return dataPoints;
- const cellCount = firstRow.querySelectorAll('.excel-cell').length;
- if (cellCount < 2) {
- console.warn('表格列数不足,无法提取X和Y值');
- return dataPoints;
- }
- rows.forEach(row => {
- const cells = row.querySelectorAll('.excel-cell');
- if (cells.length >= 2) {
- // 始终使用前两列作为X和Y值,无论它们的列标题是什么
- const xValue = parseFloat(cells[0].textContent.trim());
- const yValue = parseFloat(cells[1].textContent.trim());
- // 只添加有效的数据点
- if (!isNaN(xValue) && !isNaN(yValue)) {
- dataPoints.push({ x: xValue, y: yValue });
- }
- }
- });
- return dataPoints;
- }
- // 添加CSS样式
- document.addEventListener('DOMContentLoaded', function() {
- // 添加删除按钮样式
- const style = document.createElement('style');
- style.textContent = `
- .excel-row-header, .excel-column-header {
- position: relative;
- }
- .row-header-content, .column-header-content {
- display: inline-block;
- width: 100%;
- text-align: center;
- }
- .delete-row-btn, .delete-col-btn {
- position: absolute;
- display: none;
- cursor: pointer;
- color: #ff4d4f;
- font-weight: bold;
- font-size: 12px;
- width: 16px;
- height: 16px;
- line-height: 14px;
- text-align: center;
- border-radius: 50%;
- background-color: #fff;
- border: 1px solid #ff4d4f;
- }
- .delete-row-btn {
- right: 2px;
- top: 50%;
- transform: translateY(-50%);
- }
- .delete-col-btn {
- top: 2px;
- right: 50%;
- transform: translateX(50%);
- }
- .excel-row-header:hover .delete-row-btn,
- .excel-column-header:hover .delete-col-btn {
- display: block;
- }
- `;
- document.head.appendChild(style);
- });
- // 导出函数供其他模块使用
- window.dataTable = {
- getTableData: getTableData,
- processCSVData: processCSVData,
- getDataPointsFromExcelTable: getDataPointsFromExcelTable
- };
|