// ==UserScript== // @name ADMS复制 // @namespace anker-dms-export // @version 2.6 // @description 在结果集工具栏内嵌入复制按钮,可选含表头、纯文本或CSV格式;不含表头时自动排除序号列;重新查询后按钮自动补回 // @match https://dms.anker-in.com/* // @grant GM_setClipboard // ==/UserScript== (function () { 'use strict'; const sleep = ms => new Promise(r => setTimeout(r, ms)); function csvEscape(s) { s = String(s ?? ''); if (/[",\n]/.test(s)) return '"' + s.replace(/"/g, '""') + '"'; return s; } function splitGridsByHeader(scope) { const grids = Array.from(scope.querySelectorAll('[role="grid"]')); const blocks = grids.map(g => { const inner = g.querySelector('.ReactVirtualized__Grid__innerScrollContainer') || g; const cells = inner.children.length ? Array.from(inner.children) : [g]; const tops = new Set(cells.map(c => c.style.top || '0px')); return { el: g, isHeader: tops.size <= 1, cells }; }); return { headerBlocks: blocks.filter(b => b.isHeader), dataBlocks: blocks.filter(b => !b.isHeader) }; } function subHeadersOf(block) { return block.cells .slice() .sort((a, b) => parseFloat(a.style.left || 0) - parseFloat(b.style.left || 0)) .map(c => c.textContent.trim()); } function blockToColumns(cellMap) { const rowsMap = {}; Object.entries(cellMap).forEach(([key, text]) => { const idx = key.lastIndexOf('_'); const top = key.slice(0, idx), left = key.slice(idx + 1); (rowsMap[top] ||= {})[left] = text; }); const sortedTops = Object.keys(rowsMap).sort((a, b) => parseFloat(a) - parseFloat(b)); const leftsSet = new Set(); Object.values(rowsMap).forEach(r => Object.keys(r).forEach(l => leftsSet.add(l))); const sortedLefts = Array.from(leftsSet).sort((a, b) => parseFloat(a) - parseFloat(b)); return { colCount: sortedLefts.length, matrix: sortedTops.map(top => sortedLefts.map(left => rowsMap[top][left] ?? '')) }; } function formatOutput(headers, rows, includeHeader, format) { if (format === 'csv') { const lines = []; if (includeHeader) lines.push(headers.map(csvEscape).join(',')); rows.forEach(r => lines.push(r.map(csvEscape).join(','))); return lines.join('\n'); } const lines = []; if (includeHeader) lines.push(headers.join('\t')); rows.forEach(r => lines.push(r.join('\t'))); return lines.join('\n'); } function toast(msg) { const t = document.createElement('div'); t.textContent = msg; Object.assign(t.style, { position: 'fixed', bottom: '20px', right: '20px', zIndex: 999999, background: '#333', color: '#fff', padding: '8px 14px', borderRadius: '4px', fontSize: '13px' }); document.body.appendChild(t); setTimeout(() => t.remove(), 2500); } async function runExport(scope, includeHeader, format) { let { headerBlocks, dataBlocks } = splitGridsByHeader(scope); if (!dataBlocks.length) { alert('当前结果集没有找到虚拟表格'); return; } // 序号列跟随"包含表头"开关:不含表头时才剔除序号列 if (!includeHeader) { const rowNumIdx = headerBlocks.findIndex(hb => { const texts = subHeadersOf(hb); return texts.length === 1 && texts[0] === '序号'; }); if (rowNumIdx !== -1) { headerBlocks = headerBlocks.filter((_, i) => i !== rowNumIdx); dataBlocks = dataBlocks.filter((_, i) => i !== rowNumIdx); } } const scrollers = dataBlocks.map(b => b.el.closest('.ReactVirtualized__Grid') || b.el); const maxScroll = Math.max(...scrollers.map(s => s.scrollHeight - s.clientHeight)); const step = Math.max(50, Math.min(...scrollers.map(s => s.clientHeight)) - 20); const cellMaps = dataBlocks.map(() => ({})); function captureAll() { dataBlocks.forEach((b, i) => { const inner = b.el.querySelector('.ReactVirtualized__Grid__innerScrollContainer') || b.el; Array.from(inner.children).forEach(c => { cellMaps[i][`${c.style.top}_${c.style.left}`] = c.textContent; }); }); } captureAll(); for (let t = step; t <= maxScroll + step; t += step) { scrollers.forEach(s => { s.scrollTop = t; s.dispatchEvent(new Event('scroll', { bubbles: true })); }); await sleep(60); captureAll(); } const blockResults = cellMaps.map(blockToColumns); const rowCount = Math.max(0, ...blockResults.map(r => r.matrix.length)); const rows = []; for (let r = 0; r < rowCount; r++) { let row = []; blockResults.forEach(br => { row = row.concat(br.matrix[r] || new Array(br.colCount).fill('')); }); rows.push(row); } const headers = []; headerBlocks.forEach(hb => headers.push(...subHeadersOf(hb))); const output = formatOutput(headers, rows, includeHeader, format); GM_setClipboard(output); toast(`已复制 ${rows.length} 行到剪贴板`); } function openConfigPopup(anchorBtn, scope) { document.querySelectorAll('.vg-export-popup').forEach(p => p.remove()); const rect = anchorBtn.getBoundingClientRect(); const panel = document.createElement('div'); panel.className = 'vg-export-popup'; Object.assign(panel.style, { position: 'fixed', top: `${rect.bottom + 4}px`, left: `${rect.left}px`, zIndex: 999999, background: '#fff', border: '1px solid #d9d9d9', borderRadius: '6px', padding: '10px', boxShadow: '0 2px 8px rgba(0,0,0,.15)', fontSize: '13px', width: '190px' }); panel.innerHTML = `