孤灯
收藏于2025-09-25
// ==UserScript==
// @name JSONRESPONES to Excel (优化版)
// @namespace http://tampermonkey.net/
// @version 2.999
// @description 增强版JSON转Excel工具,优化预览界面交互逻辑,支持单元格单击选行/取消取行、行列选择互斥、ctr+单击多选单元格、双击复制单元格、右键清除选择等功能
// @author HELEI6
// @match https://10.242.33.155/*
// @grant GM_addStyle
// @grant GM_registerMenuCommand
// @grant unsafeWindow
// @grant GM_xmlhttpRequest
// @require https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js
// ==/UserScript==
(function () {
'use strict';
// 添加全局样式 (使用原生方式替代GM_addStyle以兼容360浏览器)
function addGlobalStyle(css) {
const style = document.createElement('style');
style.type = 'text/css';
style.innerHTML = css;
document.head.appendChild(style);
// 调试信息
console.log('样式已添加到页面:', style);
return style;
}
addGlobalStyle(`
.json-to-excel-btn {
position: fixed !important;
bottom: 20px !important;
right: 20px !important;
z-index: 100000 !important; /* 降低z-index值以兼容更多浏览器 */
background-color: #4CAF50 !important;
color: white !important;
border: none !important;
border-radius: 50% !important;
width: 50px !important;
height: 50px !important;
font-size: 24px !important;
cursor: pointer !important;
box-shadow: 0 2px 10px rgba(0,0,0,0.2) !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
opacity: 1 !important;
visibility: visible !important;
pointer-events: auto !important;
user-select: none !important;
touch-action: none !important;
-webkit-user-select: none !important;
-moz-user-select: none !important;
-webkit-transform: translateZ(0) !important;
transform: translateZ(0) !important;
}
@keyframes pulse {
0% { transform: scale(1); box-shadow: 0 0 0 0 rgba(76, 175, 80, 0.7); }
70% { transform: scale(1.05); box-shadow: 0 0 0 10px rgba(76, 175, 80, 0); }
100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(76, 175, 80, 0); }
}
.json-to-excel-btn:hover {
transform: scale(1.1);
box-shadow: 0 4px 15px rgba(0,0,0,0.3);
animation: none;
}
.json-to-excel-btn.dragging {
animation: none;
box-shadow: 0 4px 8px rgba(0,0,0,0.4);
z-index: 100000 !important;
}
.json-to-excel-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5);
z-index: 100000;
display: none;
align-items: center;
justify-content: center;
animation: fadeIn 0.3s;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.json-to-excel-content {
background-color: white;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0,0,0,0.2);
width: 90%;
max-width: 1600px;
max-height: 90vh;
overflow: auto;
position: relative;
animation: slideUp 0.4s;
}
@keyframes slideUp {
from { transform: translateY(20px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.json-to-excel-header {
padding: 16px 20px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
background-color: #f8f9fa;
border-radius: 8px 8px 0 0;
cursor: move;
}
.json-to-excel-title {
font-size: 18px;
font-weight: bold;
}
.json-to-excel-close {
font-size: 24px;
cursor: pointer;
color: #888;
transition: color 0.2s;
}
.json-to-excel-close:hover {
color: #333;
}
.json-to-excel-body {
padding: 20px;
height: calc(100% - 100px);
overflow: auto;
}
.json-to-excel-footer {
padding: 16px 20px;
border-top: 1px solid #eee;
display: flex;
justify-content: flex-end;
background-color: #f8f9fa;
border-radius: 0 0 8px 8px;
}
.json-to-excel-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 20px;
}
.json-to-excel-table th,
.json-to-excel-table td {
padding: 10px;
border: 1px solid #ddd;
text-align: left;
}
.json-to-excel-table th {
background-color: #f2f2f2;
font-weight: bold;
position: relative;
cursor: move;
}
.json-to-excel-table th.dragging {
opacity: 0.5;
}
.json-to-excel-table tr:hover {
background-color: #f5f5f5;
}
.json-to-excel-table tr.selected {
background-color: #e3f2fd;
}
.json-to-excel-btn-secondary {
background-color: #f1f1f1;
color: black;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
margin-right: 10px;
transition: background-color 0.2s;
}
.json-to-excel-btn-secondary:hover {
background-color: #e0e0e0;
}
.json-to-excel-btn-primary {
background-color: #4CAF50;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s;
}
.json-to-excel-btn-primary:hover {
background-color: #388e3c;
}
.json-to-excel-btn-danger {
background-color: #f44336;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s;
}
.json-to-excel-btn-danger:hover {
background-color: #d32f2f;
}
.json-to-excel-status {
margin-top: 10px;
padding: 8px;
border-radius: 4px;
animation: fadeIn 0.5s;
}
.json-to-excel-status.success {
background-color: #e8f5e9;
color: #2e7d32;
}
.json-to-excel-status.error {
background-color: #ffebee;
color: #c62828;
}
.json-to-excel-network-btn {
background-color: #2196F3;
color: white;
border: none;
padding: 4px 8px;
border-radius: 3px;
cursor: pointer;
font-size: 12px;
margin-left: 5px;
transition: background-color 0.2s;
}
.json-to-excel-network-btn:hover {
background-color: #0d47a1;
}
/* 调试样式 */
.json-to-excel-debug {
position: fixed;
top: 10px;
right: 10px;
background-color: rgba(0,0,0,0.7);
color: white;
padding: 5px 10px;
border-radius: 4px;
font-size: 12px;
z-index: 99999;
}
/* 网络请求指示器 */
.json-to-excel-request-indicator {
position: fixed;
top: 10px;
left: 10px;
background-color: #2196F3;
color: white;
padding: 5px 10px;
border-radius: 4px;
font-size: 12px;
z-index: 99999;
opacity: 0;
transition: opacity 0.3s;
}
/* 数据预览表格 */
.json-to-excel-preview-table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
font-size: 14px;
}
.json-to-excel-preview-table th,
.json-to-excel-preview-table td {
padding: 8px;
border: 1px solid #ddd;
text-align: left;
position: relative;
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.json-to-excel-preview-table th {
background-color: #f2f2f2 !important; /* 确保背景色不透明 */
transition: all 0.1s ease; /* 添加过渡效果 */
position: sticky !important;
top: 0 !important;
z-index: 30 !important; /* 进一步提高z-index确保覆盖 */
}
.json-to-excel-preview-table th:hover {
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-word;
white-space: normal;
box-shadow: 0 2px 8px rgba(0,0,0,0.15); /* 增强阴影效果 */
z-index: 40 !important; /* 悬停时进一步提高z-index */
}
.json-to-excel-preview-table td:hover {
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-word;
white-space: normal;
position: relative;
z-index: 1;
}
.json-to-excel-preview-table th {
background-color: #f2f2f2;
font-weight: bold;
position: sticky;
top: 0;
z-index: 10;
cursor: move;
}
.json-to-excel-preview-table th.dragging {
opacity: 0.6;
z-index: 10;
}
.json-to-excel-preview-table th.drag-placeholder {
visibility: hidden;
}
.json-to-excel-preview-table tr:nth-child(even) {
background-color: #f9f9f9;
}
.json-to-excel-preview-table tr:hover {
background-color: #f5f5f5;
}
.json-to-excel-preview-table tr.selected {
background-color: #bbdefb;
}
.json-to-excel-copy-btn {
margin-top: 10px;
background-color: #2196F3;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s;
}
.json-to-excel-copy-btn:hover {
background-color: #0d47a1;
}
/* 列选择样式 */
.selected-column {
background-color: #e0f7fa !important;
}
/* 排序箭头样式 */
.sort-container {
display: inline-flex;
flex-direction: column;
margin-left: 5px;
vertical-align: middle;
}
.sort-arrow {
width: 0;
height: 0;
border-left: 9px solid transparent;
border-right: 9px solid transparent;
cursor: pointer;
opacity: 0.5;
transition: opacity 0.2s;
}
.sort-arrow:hover {
opacity: 1;
}
.sort-arrow-up {
border-bottom: 9px solid #333;
margin-bottom: 3px;
}
.sort-arrow-down {
border-top: 9px solid #333;
}
.sort-active {
opacity: 1;
}
/* 新增水平滚动容器样式 */
.json-to-excel-preview-container {
position: relative;
width: 100%;
overflow: hidden;
margin-top: 10px;
}
.json-to-excel-preview-scroll {
width: 100%;
overflow-x: auto;
overflow-y: auto;
max-height: 500px;
border: 1px solid #e0e0e0;
border-radius: 4px;
margin-top: 5px;
}
.json-to-excel-preview-table {
min-width: max-content;
table-layout: auto;
white-space: nowrap;
}
.json-to-excel-cell-selected {
background-color: #bbdefb;
border: 2px solid #2196F3;
outline: none;
}
.json-to-excel-dbl-selected {
background-color: #90caf9;
border: 2px solid #0d47a1;
outline: none;
}
/* 列拖动辅助线 */
.column-drag-guide {
position: absolute;
top: 0;
bottom: 0;
width: 2px;
background-color: #2196F3;
z-index: 20;
display: none;
}
/* 复制选中状态提示 */
.copy-hint {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
background-color: rgba(0, 0, 0, 0.7);
color: white;
padding: 8px 16px;
border-radius: 4px;
font-size: 14px;
z-index: 100000;
animation: fadeInOut 2s ease-in-out forwards;
}
@keyframes fadeInOut {
0%, 100% { opacity: 0; }
10%, 90% { opacity: 1; }
}
`);
// 存储捕获的JSON请求
let capturedRequests = [];
let requestCount = 0;
let currentPreviewIndex = -1;
let selectedColumns = new Set(); // 存储选中的列索引
let currentSortField = null;
let currentSortDirection = 'asc';
let draggedColumnIndex = -1; // 拖动中的列索引
let draggedColumnElement = null; // 拖动中的列元素
let isDraggingColumn = false; // 是否正在拖动列
let isDraggingButton = false; // 是否正在拖动按钮
let buttonPosition = { x: 0, y: 0 }; // 按钮初始位置
let mousePosition = { x: 0, y: 0 }; // 鼠标位置
let selectedRows = new Set(); // 存储选中的行索引
let selectedCells = new Set(); // 存储选中的单元格 (row, col)
let doubleClickCells = null; // 双击选中的单元格范围 [start, end]
let modalPosition = { x: 0, y: 0 }; // 模态框位置
let isDraggingModal = false; // 是否正在拖动模态框
let isDoubleClickSelecting = false; // 是否正在进行双击选择
let lastDoubleClickTime = 0; // 上次双击时间
let lastDoubleClickCell = null; // 上次双击的单元格
let isCtrlPressed = false; // Ctrl键是否按下
let isShiftPressed = false; // Shift键是否按下
let lastSelectedRowIndex = null; // 上次选中的行索引
let lastSelectedCellKey = null; // 上次选中的单元格键
let isCellClickHandled = false; // 标记单元格点击是否已处理
// 创建UI元素
function createUI() {
// 添加主按钮
const mainBtn = document.createElement('button');
mainBtn.className = 'json-to-excel-btn';
mainBtn.innerHTML = '';
mainBtn.title = 'JSON to Excel';
// 跨浏览器兼容代码
mainBtn.style.display = 'block';
mainBtn.style.opacity = '1';
mainBtn.style.visibility = 'visible';
document.body.appendChild(mainBtn);
// 添加调试信息
console.log('按钮已创建并添加到页面:', mainBtn);
setTimeout(() => {
const computedStyle = window.getComputedStyle(mainBtn);
console.log('按钮状态检查:', {
display: computedStyle.display,
opacity: computedStyle.opacity,
visibility: computedStyle.visibility,
zIndex: computedStyle.zIndex,
position: computedStyle.position,
bottom: computedStyle.bottom,
right: computedStyle.right
});
// 如果按钮不可见,尝试强制显示
if (computedStyle.display === 'none' || computedStyle.visibility === 'hidden' || computedStyle.opacity === '0') {
console.warn('按钮不可见,尝试强制显示...');
mainBtn.style.display = 'flex';
mainBtn.style.visibility = 'visible';
mainBtn.style.opacity = '1';
console.log('强制显示后状态:', {
display: window.getComputedStyle(mainBtn).display,
opacity: window.getComputedStyle(mainBtn).opacity,
visibility: window.getComputedStyle(mainBtn).visibility
});
}
}, 500);
// 添加调试信息
const debugInfo = document.createElement('div');
debugInfo.className = 'json-to-excel-debug';
debugInfo.textContent = 'JSON to Excel 已加载';
document.body.appendChild(debugInfo);
// 添加请求指示器
const requestIndicator = document.createElement('div');
requestIndicator.className = 'json-to-excel-request-indicator';
requestIndicator.textContent = '0 个 JSON 请求';
document.body.appendChild(requestIndicator);
// 添加列拖动辅助线
const dragGuide = document.createElement('div');
dragGuide.className = 'column-drag-guide';
document.body.appendChild(dragGuide);
// 添加模态框
const modal = document.createElement('div');
modal.className = 'json-to-excel-modal';
modal.id = 'jsonToExcelModal';
modal.style.left = '50%';
modal.style.top = '50%';
modal.style.transform = 'translate(-50%, -50%)';
modal.innerHTML = `
<div class="json-to-excel-content" id="modalContent">
<div class="json-to-excel-header" id="modalHeader">
<div class="json-to-excel-title">JSON 转 Excel 工具</div>
<div class="json-to-excel-close" id="closeBtn">×</div>
</div>
<div class="json-to-excel-body">
<div style="display: flex; justify-content: space-between; margin-bottom: 10px;">
<h4>捕获的JSON请求</h4>
<button id="deleteAllBtn" class="json-to-excel-btn-danger">全部删除</button>
</div>
<table class="json-to-excel-table">
<thead>
<tr>
<th>URL</th>
<th>类型</th>
<th>大小</th>
<th>操作</th>
</tr>
</thead>
<tbody id="requestTableBody">
<tr><td colspan="4" style="text-align: center;">暂无捕获的JSON请求</td></tr>
</tbody>
</table>
<!-- 将按钮提前到数据预览上面 -->
<div id="buttonContainer" style="display: flex; justify-content: space-between; margin-bottom: 10px;">
<button id="copySelectedBtn" class="json-to-excel-copy-btn">复制选中列</button>
<button id="copyRowBtn" class="json-to-excel-copy-btn">复制选中行</button>
<button id="copyCellsBtn" class="json-to-excel-copy-btn">复制选中单元格</button>
<button id="clearSelectionBtn" class="json-to-excel-copy-btn">清除所有选中</button>
<button id="copyTableBtn" class="json-to-excel-copy-btn">全部复制</button>
<button id="exportSelectedBtn" class="json-to-excel-btn-primary">导出 Excel</button>
</div>
<div id="previewContainer" style="display: none;">
<h4>数据预览</h4>
<div id="dataPreview" class="json-to-excel-status success"></div>
<div class="json-to-excel-preview-container">
<div class="json-to-excel-preview-scroll" id="previewTableContainer"></div>
</div>
</div>
<div id="statusMessage" class="json-to-excel-status"></div>
</div>
<div class="json-to-excel-footer" id="modalFooter">
<button id="closeModal" class="json-to-excel-btn-secondary">关闭</button>
</div>
</div>
`;
document.body.appendChild(modal);
// 添加事件监听
mainBtn.addEventListener('mousedown', startButtonDrag);
mainBtn.addEventListener('touchstart', startButtonDrag, { passive: false });
// 模态框拖拽
document.getElementById('modalHeader').addEventListener('mousedown', startModalDrag);
document.getElementById('modalHeader').addEventListener('touchstart', startModalDrag, { passive: false });
mainBtn.addEventListener('click', (e) => {
if (!isDraggingButton) {
updateRequestTable();
modal.style.display = 'flex';
}
e.stopPropagation();
});
const closeBtn = document.getElementById('closeBtn');
closeBtn.addEventListener('click', closeModal);
document.getElementById('closeModal').addEventListener('click', closeModal);
// 点击模态框外部关闭
modal.addEventListener('click', (e) => {
if (e.target === modal) {
closeModal();
}
});
// 全部删除按钮
document.getElementById('deleteAllBtn').addEventListener('click', () => {
if (confirm('确定要删除所有记录吗?')) {
capturedRequests = [];
requestCount = 0;
currentPreviewIndex = -1;
selectedColumns.clear();
selectedRows.clear();
selectedCells.clear();
doubleClickCells = null;
requestIndicator.textContent = '0 个 JSON 请求';
updateRequestTable();
document.getElementById('previewContainer').style.display = 'none';
showStatus('已删除所有记录', 'success');
}
});
// 复制按钮事件
document.getElementById('copySelectedBtn').addEventListener('click', copySelectedColumns);
document.getElementById('copyRowBtn').addEventListener('click', copySelectedRows);
document.getElementById('copyCellsBtn').addEventListener('click', copySelectedCells);
document.getElementById('clearSelectionBtn').addEventListener('click', clearAllSelections);
document.getElementById('copyTableBtn').addEventListener('click', () => copyFullTable(true));
// 为导出Excel按钮添加默认事件处理器,确保即使未预览也能尝试导出
const exportSelectedBtn = document.getElementById('exportSelectedBtn');
exportSelectedBtn.addEventListener('click', function() {
// 检查是否有预览数据
if (currentPreviewIndex >= 0 && capturedRequests[currentPreviewIndex]) {
// 如果有预览数据,使用预览数据
exportData(capturedRequests[currentPreviewIndex].data);
} else if (capturedRequests.length > 0) {
// 如果没有预览数据但有捕获的请求,使用最后一条请求
exportData(capturedRequests[capturedRequests.length - 1].data);
} else {
showCopyHint('没有可用的数据请求');
}
});
// 监听键盘事件
document.addEventListener('keydown', handleKeyDown);
document.addEventListener('keyup', handleKeyUp);
// 3秒后隐藏调试信息
setTimeout(() => {
debugInfo.style.display = 'none';
}, 3000);
// 添加右键点击清除所有选中的功能
document.addEventListener('contextmenu', (e) => {
if (document.getElementById('jsonToExcelModal').style.display === 'flex') {
e.preventDefault();
clearAllSelections();
}
});
return { modal, requestIndicator, mainBtn, dragGuide };
}
// 处理键盘按下事件
function handleKeyDown(e) {
if (e.key === 'Control' || e.key === 'Meta') {
isCtrlPressed = true;
} else if (e.key === 'Shift') {
isShiftPressed = true;
} else if (e.key === 'c' && isCtrlPressed) {
const modal = document.getElementById('jsonToExcelModal');
if (modal && modal.style.display === 'flex') {
// 仅在预览模态框显示时执行自定义复制
e.preventDefault();
handleCopyShortcut();
}
}
}
// 处理键盘释放事件
function handleKeyUp(e) {
if (e.key === 'Control' || e.key === 'Meta') {
isCtrlPressed = false;
} else if (e.key === 'Shift') {
isShiftPressed = false;
}
}
// 处理Ctrl+C快捷键
function handleCopyShortcut() {
if (selectedColumns.size > 0) {
copySelectedColumns(false); // 不包含标题行
} else if (selectedRows.size > 0) {
copySelectedRows(false); // 确保能复制所有选中行
} else if (selectedCells.size > 0) {
copySelectedCells();
} else {
const table = document.querySelector('.json-to-excel-preview-table');
if (table) {
copyFullTable();
}
}
}
// 显示复制提示
function showCopyHint(message) {
// 移除已有的提示
const existingHint = document.querySelector('.copy-hint');
if (existingHint) {
document.body.removeChild(existingHint);
}
// 创建新提示
const hint = document.createElement('div');
hint.className = 'copy-hint';
hint.textContent = message;
document.body.appendChild(hint);
// 2秒后移除提示
setTimeout(() => {
if (document.body.contains(hint)) {
document.body.removeChild(hint);
}
}, 2000);
}
// 关闭模态框
function closeModal() {
const modal = document.getElementById('jsonToExcelModal');
modal.style.display = 'none';
document.getElementById('previewContainer').style.display = 'none';
currentPreviewIndex = -1;
selectedColumns.clear();
selectedRows.clear();
selectedCells.clear();
doubleClickCells = null;
currentSortField = null;
currentSortDirection = 'asc';
isDoubleClickSelecting = false;
lastDoubleClickCell = null;
}
// 更新请求表格
function updateRequestTable() {
const tableBody = document.getElementById('requestTableBody');
tableBody.innerHTML = '';
if (capturedRequests.length === 0) {
tableBody.innerHTML = '<tr><td colspan="4" style="text-align: center;">暂无捕获的JSON请求</td></tr>';
return;
}
capturedRequests.forEach((req, index) => {
const row = document.createElement('tr');
row.innerHTML = `
<td>${req.url.substring(0, 80)}${req.url.length > 80 ? '...' : ''}</td>
<td>${req.contentType}</td>
<td>${formatBytes(req.size)}</td>
<td>
<button class="json-to-excel-btn-secondary view-btn" data-index="${index}">查看</button>
<button class="json-to-excel-btn-primary export-btn" data-index="${index}">导出</button>
<button class="json-to-excel-btn-danger delete-btn" data-index="${index}">删除</button>
</td>
`;
tableBody.appendChild(row);
});
// 添加查看、导出和删除按钮事件
document.querySelectorAll('.view-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const index = parseInt(e.target.getAttribute('data-index'));
currentPreviewIndex = index;
previewData(capturedRequests[index].data);
});
});
document.querySelectorAll('.export-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const index = parseInt(e.target.getAttribute('data-index'));
exportData(capturedRequests[index].data);
});
});
document.querySelectorAll('.delete-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const index = parseInt(e.target.getAttribute('data-index'));
if (confirm('确定要删除这条记录吗?')) {
capturedRequests.splice(index, 1);
requestCount = capturedRequests.length;
// 如果删除的是当前预览的请求,关闭预览
if (index === currentPreviewIndex) {
document.getElementById('previewContainer').style.display = 'none';
currentPreviewIndex = -1;
} else if (index < currentPreviewIndex) {
// 更新当前预览索引
currentPreviewIndex--;
}
updateRequestTable();
showStatus('已删除所选记录', 'success');
}
});
});
}
// 查找最内层的data数组
function findInnermostData(data) {
let deepestData = null;
function dfs(obj) {
if (!obj || typeof obj !== 'object') return;
// 发现data属性且为数组,且元素为对象(可能包含字段键值对)
if (obj.hasOwnProperty('data') && Array.isArray(obj.data)) {
const currentData = obj.data;
if (currentData.length > 0 && typeof currentData[0] === 'object' && currentData[0] !== null) {
deepestData = currentData; // 记录当前data数组
dfs(currentData[0]); // 继续深入检查元素是否有更深的data
}
}
// 递归处理数组或对象的所有值
if (Array.isArray(obj)) {
obj.forEach(item => dfs(item));
} else {
Object.values(obj).forEach(value => dfs(value));
}
}
dfs(data);
return deepestData;
}
// 检测并提取数据数组
function detectDataArray(data) {
// 尝试查找最内层的用户数据数组
const innermostData = findInnermostData(data);
if (innermostData !== null) {
return innermostData;
}
// 备用逻辑:检查常见的嵌套结构
if (data && typeof data === 'object') {
// 检查是否有data属性
if (Array.isArray(data.data)) {
return data.data;
}
// 检查是否有多层嵌套的data
if (data.data && typeof data.data === 'object') {
if (Array.isArray(data.data.data)) {
return data.data.data;
}
if (data.data.data && typeof data.data.data === 'object') {
if (Array.isArray(data.data.data.data)) {
return data.data.data.data;
}
}
}
// 检查是否有列表或records属性
if (Array.isArray(data.list)) {
return data.list;
}
if (Array.isArray(data.records)) {
return data.records;
}
// 检查是否有items或results属性
if (Array.isArray(data.items)) {
return data.items;
}
if (Array.isArray(data.results)) {
return data.results;
}
// 检查对象的所有值,看是否有数组
const values = Object.values(data);
const arrayValue = values.find(v => Array.isArray(v) && v.length > 0);
if (arrayValue) {
return arrayValue;
}
}
// 如果找不到数组,返回空数组
return [];
}
// 预览数据
function previewData(data) {
const previewContainer = document.getElementById('previewContainer');
const dataPreview = document.getElementById('dataPreview');
const previewTableContainer = document.getElementById('previewTableContainer');
const exportBtn = document.getElementById('exportSelectedBtn');
try {
const innerData = detectDataArray(data);
if (innerData.length === 0) {
throw new Error('无法找到有效的数据数组');
}
const count = innerData.length;
const allFields = [];
// 提取所有字段
innerData.forEach(item => {
if (typeof item === 'object' && item !== null) {
Object.keys(item).forEach(key => {
if (!allFields.includes(key)) {
allFields.push(key);
}
});
}
});
dataPreview.textContent = `数据预览:共 ${count} 条记录,包含字段:${allFields.join(', ')}`;
previewContainer.style.display = 'block';
// 生成预览表格
generatePreviewTable(innerData, allFields);
// 保存当前数据供导出使用
exportBtn.onclick = () => exportData(innerData);
} catch (error) {
dataPreview.textContent = `预览失败:${error.message}`;
dataPreview.className = 'json-to-excel-status error';
previewContainer.style.display = 'block';
}
}
// 生成预览表格(支持列选择、排序和拖动)
function generatePreviewTable(data, fields) {
const tableContainer = document.getElementById('previewTableContainer');
tableContainer.innerHTML = '';
if (data.length === 0) {
tableContainer.innerHTML = '<p>没有数据可预览</p>';
return;
}
const table = document.createElement('table');
table.className = 'json-to-excel-preview-table';
// 创建表头
const thead = document.createElement('thead');
const headerRow = document.createElement('tr');
fields.forEach((field, colIndex) => {
const th = document.createElement('th');
th.textContent = field;
th.dataset.field = field;
th.dataset.colIndex = colIndex;
th.setAttribute('draggable', 'true');
// 添加排序箭头
const sortContainer = document.createElement('div');
sortContainer.className = 'sort-container';
const upArrow = document.createElement('div');
upArrow.className = 'sort-arrow sort-arrow-up';
upArrow.dataset.field = field;
upArrow.dataset.direction = 'asc';
const downArrow = document.createElement('div');
downArrow.className = 'sort-arrow sort-arrow-down';
downArrow.dataset.field = field;
downArrow.dataset.direction = 'desc';
sortContainer.appendChild(upArrow);
sortContainer.appendChild(downArrow);
th.appendChild(sortContainer);
// 设置当前排序状态
if (field === currentSortField) {
if (currentSortDirection === 'asc') {
upArrow.classList.add('sort-active');
} else {
downArrow.classList.add('sort-active');
}
}
// 添加列选择功能
th.addEventListener('click', function (e) {
// 阻止事件冒泡到父元素
e.stopPropagation();
e.preventDefault();
// 检查是否点击了排序箭头
if (e.target.closest('.sort-arrow')) {
return;
}
// 行和列选择互斥逻辑
if (selectedRows.size > 0) {
selectedRows.clear();
updateRowSelectionStyles();
}
const colIndex = parseInt(this.dataset.colIndex);
// 切换列选择状态
toggleColumnSelection(colIndex, this, table);
}, false); // 使用冒泡阶段监听
// 添加排序功能
upArrow.addEventListener('click', function () {
const field = this.dataset.field;
sortTable(data, field, 'asc');
currentSortField = field;
currentSortDirection = 'asc';
generatePreviewTable(data, fields);
});
downArrow.addEventListener('click', function () {
const field = this.dataset.field;
sortTable(data, field, 'desc');
currentSortField = field;
currentSortDirection = 'desc';
generatePreviewTable(data, fields);
});
headerRow.appendChild(th);
});
thead.appendChild(headerRow);
table.appendChild(thead);
// 创建表格内容
const tbody = document.createElement('tbody');
data.forEach((item, rowIndex) => {
const row = document.createElement('tr');
row.dataset.rowIndex = rowIndex;
// 行点击选择
row.addEventListener('click', function (e) {
if (e.target.closest('button')) return; // 跳过按钮点击
// 如果按下Ctrl键,允许切换行选择状态
if (isCtrlPressed) {
toggleRowSelection(rowIndex);
return;
}
// 如果按下Shift键,从上次选中行到当前行连续选择
if (isShiftPressed && lastSelectedRowIndex !== null) {
const start = Math.min(lastSelectedRowIndex, rowIndex);
const end = Math.max(lastSelectedRowIndex, rowIndex);
// 清除之前的选择
selectedRows.clear();
// 连续选择
for (let i = start; i <= end; i++) {
selectedRows.add(i);
}
// 更新行样式
updateRowSelectionStyles();
return;
}
// 普通点击,单选行
selectedRows.clear();
selectedRows.add(rowIndex);
lastSelectedRowIndex = rowIndex;
// 更新行样式
updateRowSelectionStyles();
});
// 行双击选择
row.addEventListener('dblclick', function (e) {
if (e.target.closest('button')) return; // 跳过按钮点击
// 双击选中整行
selectedRows.clear();
selectedRows.add(rowIndex);
// 更新行样式
updateRowSelectionStyles();
// 显示复制提示
showCopyHint('双击选中整行,按Ctrl+C复制');
});
// 添加单元格点击事件
fields.forEach((field, colIndex) => {
const cell = document.createElement('td');
// 处理单元格值
let value = item[field] !== undefined ? item[field] : '';
// 处理嵌套对象
if (typeof value === 'object' && value !== null) {
value = JSON.stringify(value);
}
// 去除值中的多余空格
if (typeof value === 'string') {
value = value.trim();
}
cell.textContent = value;
cell.dataset.rowIndex = rowIndex;
cell.dataset.colIndex = colIndex;
// 单元格点击选择(选中或取消选中所在行)
// 阻止鼠标按下时的原生选择行为
cell.addEventListener('mousedown', function (e) {
e.stopPropagation();
e.preventDefault();
});
cell.addEventListener('click', function (e) {
e.stopPropagation();
e.preventDefault(); // 阻止原生选择行为
isCellClickHandled = false;
// 如果按下Ctrl键,允许选择单元格
if (isCtrlPressed) {
handleCtrlCellClick(e, cell, rowIndex, colIndex);
return;
} else if (isShiftPressed && lastSelectedCell) {
// Shift+点击,选择范围单元格
const [lastRow, lastCol] = lastSelectedCell.split(',').map(Number);
const startRow = Math.min(lastRow, rowIndex);
const endRow = Math.max(lastRow, rowIndex);
const startCol = Math.min(lastCol, colIndex);
const endCol = Math.max(lastCol, colIndex);
// 清除之前的选中
selectedCells.forEach(key => {
const [r, c] = key.split(',').map(Number);
const oldCell = table.querySelector(`td[data-row-index="${r}"][data-col-index="${c}"]`);
if (oldCell) {
oldCell.classList.remove('json-to-excel-cell-selected');
}
});
selectedCells.clear();
// 选中范围内的所有单元格
for (let r = startRow; r <= endRow; r++) {
for (let c = startCol; c <= endCol; c++) {
const rangeCell = table.querySelector(`td[data-row-index="${r}"][data-col-index="${c}"]`);
if (rangeCell) {
const cellKey = `${r},${c}`;
selectedCells.add(cellKey);
rangeCell.classList.add('json-to-excel-cell-selected');
}
}
}
return;
}
// 行和列选择互斥逻辑
if (selectedColumns.size > 0) {
selectedColumns.clear();
clearColumnSelectionStyles(table);
}
// 处理单元格点击选中行
handleCellClick(rowIndex, cell);
});
// 单元格双击复制内容
cell.addEventListener('dblclick', function (e) {
e.stopPropagation();
const cellValue = this.textContent.trim();
navigator.clipboard.writeText(cellValue)
.then(() => {
showCopyHint(`已复制单元格内容: ${cellValue.substring(0, 20)}${cellValue.length > 20 ? '...' : ''}`);
})
.catch(err => {
console.error('复制失败: ', err);
showCopyHint('复制失败,请手动复制');
});
});
row.appendChild(cell);
});
tbody.appendChild(row);
});
table.appendChild(tbody);
tableContainer.appendChild(table);
// 添加列拖动功能
const headers = table.querySelectorAll('th');
headers.forEach((header, index) => {
// 拖动开始
header.addEventListener('dragstart', function (e) {
isDraggingColumn = true;
draggedColumnIndex = index;
draggedColumnElement = this;
// 设置拖动数据
e.dataTransfer.setData('text/plain', index);
// 延迟设置样式,避免影响拖动
setTimeout(() => {
this.classList.add('dragging');
}, 0);
// 隐藏辅助线
document.querySelector('.column-drag-guide').style.display = 'none';
});
// 拖动结束
header.addEventListener('dragend', function () {
isDraggingColumn = false;
this.classList.remove('dragging');
// 隐藏辅助线
document.querySelector('.column-drag-guide').style.display = 'none';
});
// 拖动经过
header.addEventListener('dragover', function (e) {
e.preventDefault();
if (isDraggingColumn) {
// 获取当前列的位置信息
const rect = this.getBoundingClientRect();
const tableRect = table.getBoundingClientRect();
// 计算辅助线位置
const guide = document.querySelector('.column-drag-guide');
guide.style.display = 'block';
// 根据鼠标位置决定是左侧还是右侧
const mouseX = e.clientX;
const columnMiddle = rect.left + rect.width / 2;
if (mouseX < columnMiddle) {
// 左侧
guide.style.left = `${rect.left - tableRect.left}px`;
this.dataset.insertBefore = 'true';
} else {
// 右侧
guide.style.left = `${rect.right - tableRect.left}px`;
this.dataset.insertBefore = 'false';
}
// 设置允许放置
e.dataTransfer.dropEffect = 'move';
}
});
// 拖动离开
header.addEventListener('dragleave', function () {
if (isDraggingColumn) {
// 隐藏辅助线
document.querySelector('.column-drag-guide').style.display = 'none';
this.dataset.insertBefore = '';
}
});
// 放置
header.addEventListener('drop', function (e) {
e.preventDefault();
if (isDraggingColumn) {
const targetIndex = index;
const sourceIndex = parseInt(e.dataTransfer.getData('text/plain'));
// 检查是否需要移动
if (sourceIndex !== targetIndex) {
// 获取插入方向
const insertBefore = this.dataset.insertBefore === 'true';
// 重新排序表头
const newFields = [...fields];
const fieldToMove = newFields[sourceIndex];
// 移除要移动的字段
newFields.splice(sourceIndex, 1);
// 插入到新位置
if (insertBefore) {
newFields.splice(targetIndex, 0, fieldToMove);
} else {
newFields.splice(targetIndex + 1, 0, fieldToMove);
}
// 更新字段顺序
fields = newFields;
// 重新生成表格
generatePreviewTable(data, fields);
// 显示操作成功提示
showCopyHint(`已将列 "${fieldToMove}" 移动到新位置`);
}
// 重置状态
this.dataset.insertBefore = '';
}
});
});
// 初始化列选择状态
selectedColumns.forEach(colIndex => {
const th = table.querySelector(`th[data-col-index="${colIndex}"]`);
if (th) {
th.classList.add('selected-column');
// 添加对应列的所有单元格的选中样式
const cells = table.querySelectorAll(`td[data-col-index="${colIndex}"]`);
cells.forEach(cell => {
cell.classList.add('selected-column');
});
}
});
// 初始化行选择状态
selectedRows.forEach(rowIndex => {
const tr = table.querySelector(`tr[data-row-index="${rowIndex}"]`);
if (tr) {
tr.classList.add('selected');
}
});
// 初始化单元格选择状态
selectedCells.forEach(cellKey => {
const [rowIndex, colIndex] = cellKey.split(',').map(Number);
const cell = table.querySelector(`td[data-row-index="${rowIndex}"][data-col-index="${colIndex}"]`);
if (cell) {
cell.classList.add('json-to-excel-cell-selected');
}
});
// 初始化双击选择状态
if (doubleClickCells) {
const startRow = Math.min(doubleClickCells.start.row, doubleClickCells.end.row);
const endRow = Math.max(doubleClickCells.start.row, doubleClickCells.end.row);
const startCol = Math.min(doubleClickCells.start.col, doubleClickCells.end.col);
const endCol = Math.max(doubleClickCells.start.col, doubleClickCells.end.col);
for (let r = startRow; r <= endRow; r++) {
for (let c = startCol; c <= endCol; c++) {
const cell = table.querySelector(`td[data-row-index="${r}"][data-col-index="${c}"]`);
if (cell) {
cell.classList.add('json-to-excel-dbl-selected');
}
}
}
}
}
// 处理Ctrl键按下时的单元格点击
// 记录最后选中的单元格
let lastSelectedCell = null;
function handleCtrlCellClick(e, cell, rowIndex, colIndex) {
const cellKey = `${rowIndex},${colIndex}`;
if (selectedCells.has(cellKey)) {
selectedCells.delete(cellKey);
cell.classList.remove('json-to-excel-cell-selected');
// 如果删除的是最后选中的单元格,则清除记录
if (lastSelectedCell === cellKey) {
lastSelectedCell = null;
}
} else {
selectedCells.add(cellKey);
cell.classList.add('json-to-excel-cell-selected');
// 更新最后选中的单元格
lastSelectedCell = cellKey;
}
}
// 处理单元格点击(选中行)
function handleCellClick(rowIndex, cell) {
if (selectedRows.has(rowIndex)) {
// 已选中,取消选择
selectedRows.delete(rowIndex);
cell.closest('tr').classList.remove('selected');
} else {
// 未选中,选中此行
selectedRows.clear();
selectedRows.add(rowIndex);
cell.closest('tr').classList.add('selected');
}
lastSelectedRowIndex = rowIndex;
}
// 切换列选择状态
function toggleColumnSelection(colIndex, thElement, table) {
if (selectedColumns.has(colIndex)) {
selectedColumns.delete(colIndex);
thElement.classList.remove('selected-column');
// 移除对应列的所有单元格的选中样式
const cells = table.querySelectorAll(`td[data-col-index="${colIndex}"]`);
cells.forEach(cell => {
cell.classList.remove('selected-column');
});
} else {
selectedColumns.add(colIndex);
thElement.classList.add('selected-column');
// 添加对应列的所有单元格的选中样式
const cells = table.querySelectorAll(`td[data-col-index="${colIndex}"]`);
cells.forEach(cell => {
cell.classList.add('selected-column');
});
}
}
// 清除列选择样式
function clearColumnSelectionStyles(table) {
const headers = table.querySelectorAll('th');
headers.forEach(th => {
th.classList.remove('selected-column');
});
const cells = table.querySelectorAll('td');
cells.forEach(cell => {
cell.classList.remove('selected-column');
});
}
// 切换行选择状态
function toggleRowSelection(rowIndex) {
if (selectedRows.has(rowIndex)) {
selectedRows.delete(rowIndex);
} else {
selectedRows.add(rowIndex);
}
// 更新行样式
updateRowSelectionStyles();
}
// 更新行选择样式
function updateRowSelectionStyles() {
const rows = document.querySelectorAll('.json-to-excel-preview-table tbody tr');
rows.forEach(row => {
const rowIndex = parseInt(row.dataset.rowIndex);
if (selectedRows.has(rowIndex)) {
row.classList.add('selected');
} else {
row.classList.remove('selected');
}
});
}
// 排序表格
function sortTable(data, field, direction) {
data.sort((a, b) => {
const valueA = a[field] !== undefined ? a[field] : '';
const valueB = b[field] !== undefined ? b[field] : '';
// 处理不同类型的数据
if (typeof valueA === 'number' && typeof valueB === 'number') {
return direction === 'asc' ? valueA - valueB : valueB - valueA;
}
// 转换为字符串进行比较
const strA = String(valueA).toLowerCase();
const strB = String(valueB).toLowerCase();
return direction === 'asc' ? strA.localeCompare(strB) : strB.localeCompare(strA);
});
}
// 复制选中的列(不带标题行)
function copySelectedColumns(includeHeader = false) {
if (selectedColumns.size === 0) {
showCopyHint('没有选中的列');
return;
}
const table = document.querySelector('.json-to-excel-preview-table');
if (!table) return;
const headers = Array.from(table.querySelectorAll('th'));
const rows = Array.from(table.querySelectorAll('tbody tr'));
// 获取选中列的索引并排序
const sortedColIndices = Array.from(selectedColumns).sort((a, b) => a - b);
// 生成CSV内容
let csvContent = '';
// 可选:添加表头
if (includeHeader) {
const selectedHeaders = sortedColIndices.map(colIndex => {
return headers[colIndex].textContent.trim();
});
csvContent += selectedHeaders.join('\t') + '\n';
}
// 添加数据行
rows.forEach(row => {
const cells = Array.from(row.querySelectorAll('td'));
const rowData = sortedColIndices.map(colIndex => {
return cells[colIndex] ? cells[colIndex].textContent.trim() : '';
});
csvContent += rowData.join('\t') + '\n';
});
// 复制到剪贴板
navigator.clipboard.writeText(csvContent)
.then(() => {
showCopyHint(`已复制 ${selectedColumns.size} 列到剪贴板`);
})
.catch(err => {
console.error('复制失败: ', err);
showCopyHint('复制失败,请手动复制');
});
}
// 复制选中的行(不带标题行)
function copySelectedRows(includeHeader = false) {
// 重新获取当前选中的行元素(解决索引失效问题)
const selectedRows = Array.from(
document.querySelectorAll('.json-to-excel-preview-table tr.selected')
);
if (selectedRows.length === 0) {
showCopyHint('没有选中的行');
return;
}
const table = document.querySelector('.json-to-excel-preview-table');
if (!table) return;
const headers = Array.from(table.querySelectorAll('th'));
// 生成 CSV 内容
let csvContent = '';
// 添加表头(可选)
if (includeHeader) {
const headerValues = headers.map(header => header.textContent.trim());
csvContent += headerValues.join('\t') + '\n';
}
// 处理每行数据
selectedRows.forEach(row => {
const cells = Array.from(row.querySelectorAll('td'));
const rowData = cells.map(cell => {
let value = cell.textContent.trim();
// 处理特殊字符:换行符和双引号
if (value.includes('\n') || value.includes('"')) {
value = `"${value.replace(/"/g, '""')}"`; // 转义双引号
}
return value;
});
csvContent += rowData.join('\t') + '\n'; // 用制表符分隔列
});
// 复制到剪贴板
navigator.clipboard.writeText(csvContent)
.then(() => {
showCopyHint(`已复制 ${selectedRows.length} 行到剪贴板`);
})
.catch(err => {
console.error('复制失败', err);
showCopyHint('复制失败,请手动复制');
});
}
// 复制选中的单元格
function copySelectedCells() {
if (selectedCells.size === 0) {
showCopyHint('没有选中的单元格');
return;
}
const table = document.querySelector('.json-to-excel-preview-table');
if (!table) return;
// 提取选中单元格的坐标并排序
const cellCoords = Array.from(selectedCells).map(key => {
const [rowIndex, colIndex] = key.split(',').map(Number);
return { rowIndex, colIndex };
});
// 按行和列排序
cellCoords.sort((a, b) => {
if (a.rowIndex !== b.rowIndex) {
return a.rowIndex - b.rowIndex;
}
return a.colIndex - b.colIndex;
});
// 找出最大行索引和列索引
const maxRowIndex = Math.max(...cellCoords.map(coord => coord.rowIndex));
const maxColIndex = Math.max(...cellCoords.map(coord => coord.colIndex));
// 创建二维数组存储数据
const dataMatrix = Array(maxRowIndex + 1).fill().map(() => Array(maxColIndex + 1).fill(''));
// 填充数据
cellCoords.forEach(coord => {
const cell = table.querySelector(`td[data-row-index="${coord.rowIndex}"][data-col-index="${coord.colIndex}"]`);
if (cell) {
dataMatrix[coord.rowIndex][coord.colIndex] = cell.textContent.trim();
}
});
// 生成CSV内容(不带标题行)
let csvContent = '';
// 添加数据行
dataMatrix.forEach(row => {
csvContent += row.join('\t') + '\n';
});
// 复制到剪贴板
navigator.clipboard.writeText(csvContent)
.then(() => {
showCopyHint(`已复制 ${selectedCells.size} 个单元格到剪贴板`);
})
.catch(err => {
console.error('复制失败: ', err);
showCopyHint('复制失败,请手动复制');
});
}
// 清除所有选中的函数
function clearAllSelections() {
selectedColumns.clear();
selectedRows.clear();
selectedCells.clear();
doubleClickCells = null;
currentSortField = null;
currentSortDirection = 'asc';
isDoubleClickSelecting = false;
lastDoubleClickCell = null;
// 更新表格样式
const table = document.querySelector('.json-to-excel-preview-table');
if (table) {
table.querySelectorAll('th').forEach(th => th.classList.remove('selected-column'));
table.querySelectorAll('td').forEach(td => {
td.classList.remove('selected-column', 'json-to-excel-cell-selected', 'json-to-excel-dbl-selected');
});
table.querySelectorAll('tr').forEach(tr => tr.classList.remove('selected'));
}
showCopyHint('已清除所有选中');
}
// 复制整个表格(不带标题行)
function copyFullTable(includeHeader = false) {
const table = document.querySelector('.json-to-excel-preview-table');
if (!table) {
showCopyHint('没有表格数据可复制');
return;
}
const headers = Array.from(table.querySelectorAll('th'));
const rows = Array.from(table.querySelectorAll('tbody tr'));
// 生成CSV内容
let csvContent = '';
// 可选:添加表头
if (includeHeader) {
const headerValues = headers.map(header => header.textContent.trim());
csvContent += headerValues.join('\t') + '\n';
}
// 添加数据行
rows.forEach(row => {
const cells = Array.from(row.querySelectorAll('td'));
const rowData = cells.map(cell => cell.textContent.trim());
csvContent += rowData.join('\t') + '\n';
});
// 复制到剪贴板
navigator.clipboard.writeText(csvContent)
.then(() => {
showCopyHint(`已复制 ${rows.length} 行数据到剪贴板`);
})
.catch(err => {
console.error('复制失败: ', err);
showCopyHint('复制失败,请手动复制');
});
}
// 导出数据到Excel
function exportData(data) {
// 首先确保我们使用的是最内层的data数组,无论数据来源如何
const finalData = detectDataArray(data);
console.log('准备导出数据,确保使用最内层data,数据条数:', finalData.length);
// 如果没有有效的数据数组,显示错误并返回
if (finalData.length === 0) {
showCopyHint('没有数据可导出');
return;
}
// 检查是否有预览表格,有则优先使用表格内容(已经经过排序筛选的)
const table = document.querySelector('.json-to-excel-preview-table');
if (table && table.rows.length > 1) {
// 从表格中读取数据,确保导出的是预览中显示的内容
try {
console.log('从预览表格中导出数据(已排序筛选)');
// 检查XLSX库是否已加载
if (typeof XLSX === 'undefined') {
console.error('XLSX库未加载');
showCopyHint('导出失败: XLSX库未加载');
return;
}
// 获取表头
const headers = Array.from(table.querySelectorAll('thead th')).map(th => th.textContent.trim());
// 准备导出的数据(包含表头)
const wsData = [headers];
// 获取表格中的数据行
const rows = Array.from(table.querySelectorAll('tbody tr'));
rows.forEach(row => {
const cells = Array.from(row.querySelectorAll('td'));
const rowData = cells.map(cell => {
let value = cell.textContent.trim();
// 处理嵌套对象格式
if (value.startsWith('{') && value.endsWith('}') || value.startsWith('[') && value.endsWith(']')) {
try {
// 尝试解析JSON字符串
JSON.parse(value);
} catch (e) {
// 不是有效的JSON,保持原样
}
}
return value;
});
wsData.push(rowData);
});
console.log('Excel数据准备完成,行数:', wsData.length, '列数:', wsData[0].length);
// 创建工作表
const ws = XLSX.utils.aoa_to_sheet(wsData);
// 设置列宽自适应(优化)
const wscols = headers.map(field => {
// 估计字段名长度作为基础宽度
const fieldLength = field.length;
// 返回配置对象,width表示列宽(字符数)
return { wch: Math.max(fieldLength + 2, 10) }; // 至少10个字符宽度
});
ws['!cols'] = wscols;
// 创建工作簿
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, "Sheet1");
// 生成Excel文件
const wbout = XLSX.write(wb, {
bookType: 'xlsx',
type: 'array',
bookSST: false,
cellStyles: false
});
// 创建下载链接
const blob = new Blob([wbout], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
// 生成文件名
const date = new Date();
let fileName = `导出数据_${date.getFullYear()}${(date.getMonth() + 1).toString().padStart(2, '0')}${date.getDate().toString().padStart(2, '0')}_${date.getTime()}`;
a.download = `${fileName}.xlsx`;
a.style.display = 'none';
// 添加到DOM并触发下载
document.body.appendChild(a);
setTimeout(() => {
try {
a.click();
console.log('Excel文件下载已触发');
showCopyHint('导出Excel成功');
} catch (clickError) {
console.error('触发下载失败:', clickError);
showCopyHint('导出失败: 无法触发下载');
}
// 清理资源
setTimeout(() => {
try {
document.body.removeChild(a);
URL.revokeObjectURL(url);
console.log('下载资源已清理');
} catch (cleanupError) {
console.error('清理资源失败:', cleanupError);
}
}, 1000);
}, 100);
return;
} catch (tableExportError) {
console.error('从表格导出失败,尝试使用最内层数据导出:', tableExportError);
// 如果从表格导出失败,回退到使用最内层数据
}
}
// 回退方案:使用最内层数据导出
// 确保 finalData 是数组类型(应该已经是数组,但做个双重保障)
const exportDataArray = Array.isArray(finalData) ? finalData : [finalData];
try {
console.log('使用最内层数据导出Excel,数据条数:', exportDataArray.length);
// 检查XLSX库是否已加载
if (typeof XLSX === 'undefined') {
console.error('XLSX库未加载');
showCopyHint('导出失败: XLSX库未加载');
return;
}
// 提取所有字段
const allFields = [];
exportDataArray.forEach(item => {
if (typeof item === 'object' && item !== null) {
Object.keys(item).forEach(key => {
if (!allFields.includes(key)) {
allFields.push(key);
}
});
}
});
// 准备导出的数据
const wsData = [allFields]; // 第一行是表头
exportDataArray.forEach(item => {
const row = allFields.map(field => {
let value = item[field] !== undefined ? item[field] : '';
// 处理嵌套对象
if (typeof value === 'object' && value !== null) {
value = JSON.stringify(value);
}
// 去除值中的多余空格
if (typeof value === 'string') {
value = value.trim();
}
return value;
});
wsData.push(row);
});
console.log('Excel数据准备完成,行数:', wsData.length, '列数:', wsData[0].length);
// 创建工作表
const ws = XLSX.utils.aoa_to_sheet(wsData);
// 设置列宽自适应(优化)
const wscols = allFields.map(field => {
// 估计字段名长度作为基础宽度
const fieldLength = field.length;
// 返回配置对象,width表示列宽(字符数)
return { wch: Math.max(fieldLength + 2, 10) }; // 至少10个字符宽度
});
ws['!cols'] = wscols;
// 创建工作簿
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, "Sheet1");
// 生成Excel文件(使用更可靠的参数配置)
const wbout = XLSX.write(wb, {
bookType: 'xlsx',
type: 'array',
bookSST: false, // 不生成共享字符串表
cellStyles: false // 不包含单元格样式
});
console.log('Excel文件生成成功,大小:', wbout.length, '字节');
// 创建下载链接 - 使用正确的MIME类型
const blob = new Blob([wbout], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
// 生成文件名
const date = new Date();
let fileName = `导出数据_${date.getFullYear()}${(date.getMonth() + 1).toString().padStart(2, '0')}${date.getDate().toString().padStart(2, '0')}_${date.getTime()}`;
a.download = `${fileName}.xlsx`;
a.style.display = 'none'; // 隐藏链接元素
// 触发下载 - 使用更可靠的方式
document.body.appendChild(a);
// 使用setTimeout确保DOM已更新
setTimeout(() => {
try {
a.click();
console.log('Excel文件下载已触发');
showCopyHint('导出Excel成功');
} catch (clickError) {
console.error('触发下载失败:', clickError);
showCopyHint('导出失败: 无法触发下载');
}
// 清理资源 - 延迟更长时间以确保下载完成
setTimeout(() => {
try {
document.body.removeChild(a);
URL.revokeObjectURL(url);
console.log('下载资源已清理');
} catch (cleanupError) {
console.error('清理资源失败:', cleanupError);
}
}, 1000);
}, 100);
} catch (error) {
console.error('导出Excel时发生错误:', error);
console.error('错误详情:', error.stack);
showCopyHint(`导出失败: ${error.message || '未知错误'}`);
}
}
// 显示状态消息
function showStatus(message, type = 'info') {
const statusElement = document.getElementById('statusMessage');
statusElement.textContent = message;
statusElement.className = `json-to-excel-status ${type}`;
// 3秒后自动隐藏
setTimeout(() => {
statusElement.textContent = '';
statusElement.className = 'json-to-excel-status';
}, 3000);
}
// 格式化字节大小
function formatBytes(bytes, decimals = 2) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
// 按钮拖动功能
function startButtonDrag(e) {
isDraggingButton = true;
const btn = e.target.closest('.json-to-excel-btn');
btn.classList.add('dragging');
const rect = btn.getBoundingClientRect();
buttonPosition.x = rect.left;
buttonPosition.y = rect.top;
let clientX, clientY;
if (e.type === 'mousedown') {
clientX = e.clientX;
clientY = e.clientY;
} else {
clientX = e.touches[0].clientX;
clientY = e.touches[0].clientY;
}
mousePosition.x = clientX;
mousePosition.y = clientY;
document.addEventListener('mousemove', moveButton);
document.addEventListener('touchmove', moveButton, { passive: false });
document.addEventListener('mouseup', stopButtonDrag);
document.addEventListener('touchend', stopButtonDrag);
e.preventDefault();
}
function moveButton(e) {
if (!isDraggingButton) return;
let clientX, clientY;
if (e.type === 'mousemove') {
clientX = e.clientX;
clientY = e.clientY;
} else {
clientX = e.touches[0].clientX;
clientY = e.touches[0].clientY;
}
const dx = clientX - mousePosition.x;
const dy = clientY - mousePosition.y;
const btn = document.querySelector('.json-to-excel-btn');
btn.style.left = `${buttonPosition.x + dx}px`;
btn.style.top = `${buttonPosition.y + dy}px`;
btn.style.bottom = 'auto';
btn.style.right = 'auto';
// 确保按钮不会移出可视区域
const btnRect = btn.getBoundingClientRect();
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
if (btnRect.left < 0) {
btn.style.left = '0px';
}
if (btnRect.right > windowWidth) {
btn.style.left = `${windowWidth - btnRect.width}px`;
}
if (btnRect.top < 0) {
btn.style.top = '0px';
}
if (btnRect.bottom > windowHeight) {
btn.style.top = `${windowHeight - btnRect.height}px`;
}
e.preventDefault();
}
function stopButtonDrag() {
isDraggingButton = false;
const btn = document.querySelector('.json-to-excel-btn');
btn.classList.remove('dragging');
document.removeEventListener('mousemove', moveButton);
document.removeEventListener('touchmove', moveButton);
document.removeEventListener('mouseup', stopButtonDrag);
document.removeEventListener('touchend', stopButtonDrag);
}
// 模态框拖动功能
function startModalDrag(e) {
isDraggingModal = true;
const modal = document.getElementById('modalContent');
const rect = modal.getBoundingClientRect();
modalPosition.x = rect.left;
modalPosition.y = rect.top;
let clientX, clientY;
if (e.type === 'mousedown') {
clientX = e.clientX;
clientY = e.clientY;
} else {
clientX = e.touches[0].clientX;
clientY = e.touches[0].clientY;
}
mousePosition.x = clientX;
mousePosition.y = clientY;
document.addEventListener('mousemove', moveModal);
document.addEventListener('touchmove', moveModal, { passive: false });
document.addEventListener('mouseup', stopModalDrag);
document.addEventListener('touchend', stopModalDrag);
e.preventDefault();
}
function moveModal(e) {
if (!isDraggingModal) return;
let clientX, clientY;
if (e.type === 'mousemove') {
clientX = e.clientX;
clientY = e.clientY;
} else {
clientX = e.touches[0].clientX;
clientY = e.touches[0].clientY;
}
const dx = clientX - mousePosition.x;
const dy = clientY - mousePosition.y;
const modal = document.getElementById('modalContent');
modal.style.left = `${modalPosition.x + dx}px`;
modal.style.top = `${modalPosition.y + dy}px`;
modal.style.transform = 'none';
// 确保模态框不会移出可视区域
const modalRect = modal.getBoundingClientRect();
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
if (modalRect.left < 0) {
modal.style.left = '0px';
}
if (modalRect.right > windowWidth) {
modal.style.left = `${windowWidth - modalRect.width}px`;
}
if (modalRect.top < 0) {
modal.style.top = '0px';
}
if (modalRect.bottom > windowHeight) {
modal.style.top = `${windowHeight - modalRect.height}px`;
}
e.preventDefault();
}
function stopModalDrag() {
isDraggingModal = false;
document.removeEventListener('mousemove', moveModal);
document.removeEventListener('touchmove', moveModal);
document.removeEventListener('mouseup', stopModalDrag);
document.removeEventListener('touchend', stopModalDrag);
}
// 捕获网络请求
function captureNetworkRequests() {
const originalXHROpen = XMLHttpRequest.prototype.open;
const originalFetch = window.fetch;
// 重写XMLHttpRequest
XMLHttpRequest.prototype.open = function (method, url, async, user, password) {
this._url = url;
this._method = method;
this._startTime = new Date().getTime();
const originalSend = this.send;
this.send = function (data) {
this._requestData = data;
this.addEventListener('load', function () {
handleResponse(this);
});
originalSend.call(this, data);
};
return originalXHROpen.call(this, method, url, async, user, password);
};
// 重写fetch
window.fetch = function (input, init) {
const url = typeof input === 'string' ? input : input.url;
const method = (init && init.method) || 'GET';
const startTime = new Date().getTime();
const requestData = (init && init.body) || null;
// 只监听指定的URL
if (!url.includes('/api/data/batch-exec/batch-sql-execution')) {
return originalFetch(input, init);
}
return originalFetch(input, init)
.then(response => {
const responseClone = response.clone();
const endTime = new Date().getTime();
// 只处理JSON响应
if (response.headers.get('Content-Type') &&
response.headers.get('Content-Type').includes('application/json')) {
responseClone.text().then(text => {
try {
const jsonData = JSON.parse(text);
const size = text.length;
capturedRequests.push({
url: url,
method: method,
contentType: 'JSON',
size: size,
data: jsonData,
timestamp: new Date().toISOString()
});
requestCount = capturedRequests.length;
updateRequestIndicator();
} catch (error) {
console.log('不是有效的JSON响应');
}
});
}
return response;
})
.catch(error => {
console.error('Fetch error:', error);
throw error;
});
};
// 处理XMLHttpRequest响应
function handleResponse(xhr) {
// 只监听指定的URL
if (!xhr._url.includes('/api/data/batch-exec/batch-sql-execution')) {
return;
}
if (xhr.status === 200 &&
xhr.getResponseHeader('Content-Type') &&
xhr.getResponseHeader('Content-Type').includes('application/json')) {
try {
const jsonData = JSON.parse(xhr.responseText);
const size = xhr.responseText.length;
capturedRequests.push({
url: xhr._url,
method: xhr._method,
contentType: 'JSON',
size: size,
data: jsonData,
timestamp: new Date().toISOString()
});
requestCount = capturedRequests.length;
updateRequestIndicator();
} catch (error) {
console.log('不是有效的JSON响应');
}
}
};
// 更新请求指示器
function updateRequestIndicator() {
const indicator = document.querySelector('.json-to-excel-request-indicator');
if (indicator) {
indicator.textContent = `${requestCount} 个 JSON 请求`;
indicator.style.opacity = '1';
// 3秒后淡出
setTimeout(() => {
indicator.style.opacity = '0';
}, 3000);
}
}
}
// 初始化
function init() {
const { modal, requestIndicator, mainBtn, dragGuide } = createUI();
captureNetworkRequests();
}
// 启动脚本
init();
})();