123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218 |
- /**
- * FeHelper Full Page Capture
- * @author FeHelper
- * @version 1.0.1
- */
- window.screenshotContentScript = function () {
- // 存储截图数据的数组
- let screenshots = [];
- // 定义最大尺寸限制常量
- const MAX_PRIMARY_DIMENSION = 50000 * 2,
- MAX_SECONDARY_DIMENSION = 20000 * 2,
- MAX_AREA = MAX_PRIMARY_DIMENSION * MAX_SECONDARY_DIMENSION;
- // 保存原始页面标题
- const pageOriginalTitle = document.title;
- // 是否正在截图中
- let isCapturing = false;
- // 取消截图的标志
- let isCancelled = false;
- // 定义全局变量以存储原始滚动位置
- let originalScrollLeft = 0;
- let originalScrollTop = 0;
- /**
- * URL合法性校验
- * @param {string} url - 要检查的URL
- * @returns {boolean} - URL是否合法
- */
- function isValidUrl(url) {
- // 允许的URL模式
- const matches = ['http://*/*', 'https://*/*', 'ftp://*/*', 'file://*/*'];
- // 不允许的URL模式
- const noMatches = [/^https?:\/\/chrome\.google\.com\/.*$/];
- // 先检查不允许的URL
- for (let i = 0; i < noMatches.length; i++) {
- if (noMatches[i].test(url)) {
- return false;
- }
- }
-
- // 再检查允许的URL
- for (let i = 0; i < matches.length; i++) {
- const pattern = matches[i].replace(/\*/g, '.*');
- const regex = new RegExp('^' + pattern + '$');
- if (regex.test(url)) {
- return true;
- }
- }
-
- return false;
- }
- /**
- * 释放canvas资源
- * @param {Array} canvasList - 要释放的canvas列表
- */
- function releaseCanvasResources(canvasList) {
- if (!canvasList || !canvasList.length) return;
-
- canvasList.forEach(item => {
- if (item.ctx) {
- item.ctx.clearRect(0, 0, item.canvas.width, item.canvas.height);
- }
- if (item.canvas) {
- item.canvas.width = 0;
- item.canvas.height = 0;
- }
- });
- }
- /**
- * 初始化截图canvas
- * @param {number} totalWidth - 总宽度
- * @param {number} totalHeight - 总高度
- * @returns {Array} - 初始化的canvas数组
- * @private
- */
- function _initScreenshots(totalWidth, totalHeight) {
- // 检查尺寸是否超过限制
- const badSize = (totalHeight > MAX_PRIMARY_DIMENSION ||
- totalWidth > MAX_PRIMARY_DIMENSION ||
- totalHeight * totalWidth > MAX_AREA);
- const biggerWidth = totalWidth > totalHeight;
-
- // 计算每个分块的最大尺寸
- const maxWidth = (!badSize ? totalWidth :
- (biggerWidth ? MAX_PRIMARY_DIMENSION : MAX_SECONDARY_DIMENSION));
- const maxHeight = (!badSize ? totalHeight :
- (biggerWidth ? MAX_SECONDARY_DIMENSION : MAX_PRIMARY_DIMENSION));
-
- // 计算分块数量
- const numCols = Math.ceil(totalWidth / maxWidth);
- const numRows = Math.ceil(totalHeight / maxHeight);
-
- // 创建结果数组
- const result = [];
- let canvasIndex = 0;
- // 创建所有需要的canvas
- for (let row = 0; row < numRows; row++) {
- for (let col = 0; col < numCols; col++) {
- const canvas = document.createElement('canvas');
- canvas.width = (col === numCols - 1 ? totalWidth % maxWidth || maxWidth : maxWidth);
- canvas.height = (row === numRows - 1 ? totalHeight % maxHeight || maxHeight : maxHeight);
- const left = col * maxWidth;
- const top = row * maxHeight;
- result.push({
- canvas: canvas,
- ctx: canvas.getContext('2d'),
- index: canvasIndex,
- left: left,
- right: left + canvas.width,
- top: top,
- bottom: top + canvas.height
- });
- canvasIndex++;
- }
- }
- return result;
- }
- /**
- * 从截屏中筛选有效数据
- * @param {number} imgLeft - 图像左边界
- * @param {number} imgTop - 图像上边界
- * @param {number} imgWidth - 图像宽度
- * @param {number} imgHeight - 图像高度
- * @param {Array} screenshotList - 截图列表
- * @returns {Array} - 筛选后的截图列表
- * @private
- */
- function _filterScreenshots(imgLeft, imgTop, imgWidth, imgHeight, screenshotList) {
- // 计算图像边界
- const imgRight = imgLeft + imgWidth;
- const imgBottom = imgTop + imgHeight;
-
- // 筛选与当前区域重叠的截图
- return screenshotList.filter(screenshot =>
- imgLeft < screenshot.right &&
- imgRight > screenshot.left &&
- imgTop < screenshot.bottom &&
- imgBottom > screenshot.top
- );
- }
- /**
- * 添加截图到canvas
- * @param {Object} data - 截图数据
- * @param {string} uri - 图片URI
- */
- function addScreenShot(data, uri) {
- // 如果已取消截图,不处理
- if (isCancelled) return;
-
- const image = new Image();
-
- // 图片加载错误处理
- image.onerror = function() {
- captureConfig.fail('图片加载失败');
- releaseResources();
- };
- image.onload = function() {
- try {
- data.image = {width: image.width, height: image.height};
- // 调整缩放比例
- if (data.windowWidth !== image.width) {
- const scale = image.width / data.windowWidth;
- data.x *= scale;
- data.y *= scale;
- data.totalWidth *= scale;
- data.totalHeight *= scale;
- }
- // 如果是第一张截图,初始化canvas
- if (!screenshots.length) {
- screenshots = _initScreenshots(data.totalWidth, data.totalHeight);
- }
- // 获取与当前区域重叠的canvas并绘制图像
- const matchingScreenshots = _filterScreenshots(
- data.x, data.y, image.width, image.height, screenshots
- );
-
- matchingScreenshots.forEach(screenshot => {
- screenshot.ctx.drawImage(
- image,
- data.x - screenshot.left,
- data.y - screenshot.top
- );
- });
- // 如果是最后一步,调用成功回调
- if (data.complete === 1) {
- captureConfig.success(data);
- isCapturing = false;
- }
- } catch (e) {
- captureConfig.fail('处理截图时出错: ' + e.message);
- releaseResources();
- }
- };
-
- // 设置图片源
- image.src = uri;
- }
- /**
- * 释放所有资源
- */
- function releaseResources() {
- releaseCanvasResources(screenshots);
- screenshots = [];
- isCapturing = false;
- isCancelled = false;
- }
- /**
- * 创建截图进度UI
- * @param {string} [text='正在截取网页...'] 显示的文本
- * @returns {HTMLElement} 创建的进度UI元素
- */
- function createProgressUI(text = '正在截取网页...') {
- // 先检查是否已存在
- let progressContainer = document.getElementById('fehelper-screenshot-progress');
- if (progressContainer) {
- progressContainer.querySelector('.fh-progress-text').textContent = text;
- progressContainer.style.display = 'flex';
- return progressContainer;
- }
- // 创建进度UI容器
- progressContainer = document.createElement('div');
- progressContainer.id = 'fehelper-screenshot-progress';
- progressContainer.setAttribute('data-fh-ui', 'true');
- progressContainer.className = 'fehelper-ui-element';
- progressContainer.style.cssText = `
- position: fixed;
- bottom: 20px;
- left: 50%;
- transform: translateX(-50%);
- background-color: rgba(0, 0, 0, 0.7);
- color: white;
- padding: 10px 20px;
- border-radius: 5px;
- z-index: 10000000;
- font-size: 14px;
- display: flex;
- flex-direction: column;
- align-items: center;
- box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
- width: 300px;
- `;
- // 创建文本元素
- const textElement = document.createElement('div');
- textElement.className = 'fh-progress-text';
- textElement.textContent = text;
- textElement.style.cssText = 'margin-bottom: 10px; width: 100%; text-align: center;';
- progressContainer.appendChild(textElement);
- // 创建进度条容器
- const progressBarContainer = document.createElement('div');
- progressBarContainer.style.cssText = `
- width: 100%;
- height: 10px;
- background-color: rgba(255, 255, 255, 0.2);
- border-radius: 5px;
- overflow: hidden;
- margin-bottom: 10px;
- `;
- progressContainer.appendChild(progressBarContainer);
- // 创建进度条
- const progressBar = document.createElement('div');
- progressBar.className = 'fh-progress-bar';
- progressBar.style.cssText = `
- height: 100%;
- width: 0%;
- background-color: #4CAF50;
- border-radius: 5px;
- transition: width 0.3s;
- `;
- progressBarContainer.appendChild(progressBar);
- // 创建百分比文本
- const percentText = document.createElement('div');
- percentText.className = 'fh-progress-percent';
- percentText.textContent = '0%';
- percentText.style.cssText = 'margin-bottom: 10px; font-size: 12px;';
- progressContainer.appendChild(percentText);
- // 创建取消按钮
- const cancelButton = document.createElement('button');
- cancelButton.textContent = '取消';
- cancelButton.className = 'fh-progress-cancel';
- cancelButton.style.cssText = `
- background-color: #f44336;
- border: none;
- color: white;
- padding: 5px 10px;
- border-radius: 3px;
- cursor: pointer;
- font-size: 12px;
- `;
- cancelButton.onclick = () => {
- if (window._fh_screenshot_cancel_callback && typeof window._fh_screenshot_cancel_callback === 'function') {
- window._fh_screenshot_cancel_callback();
- }
- progressContainer.style.display = 'none';
- };
- progressContainer.appendChild(cancelButton);
- document.body.appendChild(progressContainer);
- return progressContainer;
- }
-
- /**
- * 更新截图进度UI
- * @param {number} percent 进度百分比(0-1)
- * @param {string} [text] 可选的文本更新
- */
- function updateProgressUI(percent, text) {
- const progressContainer = document.getElementById('fehelper-screenshot-progress');
- if (!progressContainer) return;
- const progressBar = progressContainer.querySelector('.fh-progress-bar');
- const percentText = progressContainer.querySelector('.fh-progress-percent');
-
- if (progressBar) {
- const percentage = Math.min(Math.max(percent * 100, 0), 100);
- progressBar.style.width = `${percentage}%`;
- }
-
- if (percentText) {
- percentText.textContent = `${Math.round(percent * 100)}%`;
- }
- if (text) {
- const textElement = progressContainer.querySelector('.fh-progress-text');
- if (textElement) {
- textElement.textContent = text;
- }
- }
- }
-
- /**
- * 取消截图操作
- */
- function cancelCapture() {
- if (isCapturing) {
- isCancelled = true;
- isCapturing = false;
-
- // 移除进度UI
- const progressContainer = document.getElementById('fehelper-screenshot-progress');
- if (progressContainer) {
- progressContainer.style.display = 'none';
- }
-
- // 恢复原始状态
- cleanup && typeof cleanup === 'function' && cleanup();
-
- return true;
- }
- return false;
- }
- /**
- * 隐藏所有FeHelper UI元素
- * 隐藏所有带有 fehelper-ui-element 类或 data-fh-ui 属性的元素
- * @returns {Object} 隐藏元素的原始显示状态
- */
- function hideFeHelperUI() {
- const uiElements = document.querySelectorAll('.fehelper-ui-element, [data-fh-ui="true"]');
- const originalDisplays = {};
- uiElements.forEach((element, index) => {
- // Ensure unique ID for lookup later
- if (!element.id) {
- // Assign a temporary, identifiable ID
- element.id = `fh-temp-id-${Math.random().toString(36).substring(2, 9)}`;
- }
- const id = element.id;
- originalDisplays[id] = element.style.display;
- element.style.display = 'none';
- });
- window._fh_original_displays = originalDisplays; // Store globally
- return originalDisplays; // Keep return for compatibility if needed elsewhere
- }
-
- /**
- * 显示所有FeHelper UI元素
- * 恢复所有带有 fehelper-ui-element 类或 data-fh-ui 属性的元素的显示状态
- * @param {Object} originalDisplays 原始显示状态对象
- */
- function showFeHelperUI(originalDisplays) { // Accept argument
- originalDisplays = originalDisplays || window._fh_original_displays || {}; // Use passed or global
- const uiElements = document.querySelectorAll('.fehelper-ui-element, [data-fh-ui="true"]');
- uiElements.forEach((element) => {
- const id = element.id;
- if (id && id in originalDisplays) {
- element.style.display = originalDisplays[id];
- } else {
- // Default restoration if ID mismatch or not found (less reliable)
- element.style.display = '';
- }
- // Remove temporary ID if added and it wasn't originally present
- if (id && id.startsWith('fh-temp-id-') && !(id in (window._fh_original_displays || {}))) {
- element.removeAttribute('id');
- }
- });
- }
- /**
- * 隐藏固定定位和粘性定位的元素
- * @returns {Array} 被隐藏元素的原始样式信息,用于恢复
- */
- function hideFixedElements() {
- const fixedElements = [];
-
- // 查找所有fixed和sticky定位的元素
- const elements = document.querySelectorAll('*');
- elements.forEach(el => {
- const style = window.getComputedStyle(el);
- if (style && (style.position === 'fixed' || style.position === 'sticky')) {
- // 排除FeHelper自己的UI元素
- if (el.classList.contains('fehelper-ui-element') ||
- el.hasAttribute('data-fh-ui') ||
- el.id === 'fehelper-screenshot-progress' ||
- el.closest('#fehelper_screenshot_container')) {
- return;
- }
-
- // 保存原始样式
- fixedElements.push({
- element: el,
- originalDisplay: el.style.display,
- originalVisibility: el.style.visibility,
- originalOpacity: el.style.opacity
- });
-
- // 隐藏元素
- el.style.visibility = 'hidden';
- el.style.opacity = '0';
- }
- });
-
- window._fh_fixed_elements = fixedElements;
- return fixedElements;
- }
- /**
- * 恢复之前隐藏的固定定位和粘性定位元素
- */
- function showFixedElements() {
- const fixedElements = window._fh_fixed_elements || [];
- fixedElements.forEach(item => {
- const el = item.element;
- if (el) {
- // 恢复原始样式 - 修正:恢复 visibility 和 opacity
- el.style.visibility = item.originalVisibility !== undefined ? item.originalVisibility : '';
- el.style.opacity = item.originalOpacity !== undefined ? item.originalOpacity : '';
- // Display 属性通常不需要在隐藏/显示 fixed 元素时修改,注释掉以防干扰
- // if (item.originalDisplay !== undefined) {
- // el.style.display = item.originalDisplay;
- // }
- }
- });
- window._fh_fixed_elements = null; // 清理存储的元素
- }
- // 定义全局的 cleanup 函数
- function cleanup() {
- // 恢复滚动位置
- window.scrollTo(originalScrollLeft, originalScrollTop);
- // 恢复UI显示 - 传递存储的原始状态
- showFeHelperUI(window._fh_original_displays || {});
- showFixedElements();
- // 隐藏进度UI
- const progressContainer = document.getElementById('fehelper-screenshot-progress');
- if (progressContainer) {
- progressContainer.style.display = 'none';
- }
- // 重新显示工具条
- const screenshotContainer = document.getElementById('fehelper_screenshot_container');
- if (screenshotContainer) {
- screenshotContainer.style.display = 'block';
- }
- window._fh_screenshot_in_progress = false;
- window._fh_screenshot_cancel_callback = null;
- window._fh_fixed_elements = null; // Clear fixed elements cache
- window._fh_original_displays = null; // Clear UI display cache
- }
- // 配置项
- const captureConfig = {
- // 成功回调
- success: function(data) {
- try {
- // 构造正确的数据格式
- const screenshotData = {
- filename: buildFilenameFromUrl(),
- screenshots: screenshots.map(ss => {
- // 确保使用toDataURL生成png格式的图像
- const dataUri = ss.canvas.toDataURL('image/png');
-
- return {
- dataUri: dataUri, // 保持与showResult函数期望的属性名一致
- index: ss.index,
- row: Math.floor(ss.top / (ss.bottom - ss.top || 1)),
- col: Math.floor(ss.left / (ss.right - ss.left || 1)),
- left: ss.left,
- top: ss.top,
- right: ss.right,
- bottom: ss.bottom,
- width: ss.right - ss.left,
- height: ss.bottom - ss.top
- };
- }),
- totalWidth: data.totalWidth,
- totalHeight: data.totalHeight
- };
-
- // 使用正确的消息类型和thing
- chrome.runtime.sendMessage({
- type: 'fh-dynamic-any-thing',
- thing: 'page-screenshot-done',
- params: screenshotData
- }, function(response) {
- // 恢复页面标题并移除进度条
- document.title = pageOriginalTitle;
- const progressBar = document.getElementById('fehelper_screenshot_progress');
- if (progressBar) {
- progressBar.remove();
- }
-
- // 释放资源
- setTimeout(() => {
- releaseResources();
- }, 1000);
- });
- } catch (e) {
- captureConfig.fail('处理截图结果时出错: ' + e.message);
- }
- },
- // 失败回调
- fail: function(reason) {
- // 恢复页面标题
- document.title = pageOriginalTitle;
-
- // 移除进度条
- const progressBar = document.getElementById('fehelper_screenshot_progress');
- if (progressBar) {
- progressBar.remove();
- }
-
- // 显示错误消息
- const errorMsg = reason && reason.message || reason || '截图失败,请刷新页面重试!';
-
- // 创建错误提示UI
- const errorDiv = document.createElement('div');
- errorDiv.style.cssText = 'position:fixed;left:20%;top:20%;right:20%;z-index:1000001;padding:20px;background:rgba(255,0,0,0.7);color:#fff;text-align:center;border-radius:5px;';
- errorDiv.textContent = errorMsg;
-
- // 添加关闭按钮
- const closeButton = document.createElement('button');
- closeButton.textContent = '关闭';
- closeButton.style.cssText = 'margin-top:10px;padding:5px 15px;background:#fff;color:#000;border:none;border-radius:3px;cursor:pointer;';
- closeButton.onclick = function() {
- errorDiv.remove();
- };
-
- errorDiv.appendChild(document.createElement('br'));
- errorDiv.appendChild(closeButton);
- document.body.appendChild(errorDiv);
-
- // 自动关闭
- setTimeout(() => {
- if (document.body.contains(errorDiv)) {
- errorDiv.remove();
- }
- }, 5000);
-
- // 释放资源
- releaseResources();
- },
- // 进度回调
- progress: function(complete) {
- if (isCancelled) return false;
-
- // 更新进度条
- updateProgressUI(complete);
-
- // 更新页面标题
- const percent = parseInt(complete * 100, 10) + '%';
- document.title = `截图进度:${percent}...`;
- if (percent === '100%') {
- setTimeout(() => {
- document.title = pageOriginalTitle;
- }, 800);
- }
- return true;
- }
- };
- /**
- * 计算数组中的最大值
- * @param {Array} nums - 数字数组
- * @returns {number} - 最大值
- */
- function max(nums) {
- return Math.max(...nums.filter(Boolean));
- }
- /**
- * 执行可视区域截图
- */
- async function captureVisible() {
- if (window._fh_screenshot_in_progress) {
- return;
- }
-
- window._fh_screenshot_in_progress = true;
-
- // 保存原始滚动位置
- originalScrollTop = window.scrollY || document.documentElement.scrollTop;
- originalScrollLeft = window.scrollX || document.documentElement.scrollLeft;
-
- // 隐藏工具条
- let screenshotContainer = document.getElementById('fehelper_screenshot_container');
- if (screenshotContainer) {
- screenshotContainer.style.display = 'none';
- }
-
- const progressUI = createProgressUI('正在截取可视区域...');
-
- window._fh_screenshot_cancel_callback = cleanup;
-
- // 隐藏干扰UI
- hideFeHelperUI();
- hideFixedElements();
-
- // 等待DOM更新
- setTimeout(() => {
- updateProgressUI(0.3, '正在截取可视区域...');
-
- // 从background.js导入MSG_TYPE不现实,这里直接使用特定值
- chrome.runtime.sendMessage({
- type: 'fh-screenshot-capture-visible'
- }, response => {
- if (response) {
- // 加载截图并处理
- const img = new Image();
- img.onload = function() {
- updateProgressUI(0.8, '正在处理截图...');
-
- // 创建与index.js中showResult函数期望的格式一致的数据
- const screenshotData = {
- filename: buildFilenameFromUrl(),
- screenshots: [{
- dataUri: response,
- index: 0,
- row: 0,
- col: 0,
- left: 0,
- top: 0,
- right: img.width,
- bottom: img.height,
- width: img.width,
- height: img.height
- }],
- totalWidth: img.width,
- totalHeight: img.height
- };
-
- // 使用统一的消息格式发送数据,确保只发送一次
- chrome.runtime.sendMessage({
- type: 'fh-dynamic-any-thing',
- thing: 'page-screenshot-done',
- params: screenshotData
- }, function(resp) {
- updateProgressUI(1, '截图完成');
- setTimeout(cleanup, 500);
- });
- };
-
- img.onerror = function(e) {
- updateProgressUI(1, '截图加载失败');
- setTimeout(cleanup, 500);
- };
-
- img.src = response;
-
- // 如果图片加载时间过长,设置超时
- setTimeout(() => {
- if (!img.complete) {
- cleanup();
- }
- }, 3000);
- } else {
- const errorMessage = '截图失败,请刷新页面重试!';
- updateProgressUI(1, '截图失败');
- setTimeout(() => {
- alert(errorMessage);
- cleanup();
- }, 500);
- }
- });
- }, 100);
- }
- /**
- * 执行全页面截图
- */
- async function captureFullPage() {
- if (window._fh_screenshot_in_progress) {
- return;
- }
- window._fh_screenshot_in_progress = true;
- window._fh_screenshot_canceled = false; // 重命名 cancel 标志以避免冲突
- // 保存原始滚动位置
- originalScrollTop = window.scrollY || document.documentElement.scrollTop;
- originalScrollLeft = window.scrollX || document.documentElement.scrollLeft;
- // 计算页面尺寸
- const pageWidth = Math.max(
- document.documentElement.scrollWidth,
- document.body.scrollWidth,
- document.documentElement.offsetWidth,
- document.body.offsetWidth
- );
- const pageHeight = Math.max(
- document.documentElement.scrollHeight,
- document.body.scrollHeight,
- document.documentElement.offsetHeight,
- document.body.offsetHeight
- );
- // 获取视窗尺寸
- const windowWidth = window.innerWidth;
- const windowHeight = window.innerHeight;
- // 使用视窗尺寸作为滚动步长,并计算大致步数用于UI显示
- const totalSteps = Math.ceil(pageHeight / windowHeight) * Math.ceil(pageWidth / windowWidth);
- // 隐藏工具条
- let screenshotContainer = document.getElementById('fehelper_screenshot_container');
- if (screenshotContainer) {
- screenshotContainer.style.display = 'none';
- }
- // 创建进度UI和取消回调
- const progressUI = createProgressUI(`正在截取全页面 (共${totalSteps}步)...`);
- window._fh_screenshot_cancel_callback = () => {
- window._fh_screenshot_canceled = true; // Set the cancel flag
- cleanup(); // Execute cleanup
- };
- // 隐藏其他干扰UI(但不隐藏固定元素)
- hideFeHelperUI(); // 保存原始UI状态
- // 创建存储截图数据的数组和已捕获位置的集合
- const screenshots = [];
- const capturedPositions = new Set();
- let currentStep = 0; // 用于进度显示
- // 使用异步函数和循环进行截图
- setTimeout(async () => {
- try {
- // 垂直滚动循环
- for (let y = 0; ; y += windowHeight) {
- // 水平滚动循环
- for (let x = 0; ; x += windowWidth) {
- if (window._fh_screenshot_canceled) { cleanup(); return; }
- // 计算目标滚动位置,限制在页面边界内
- const targetX = Math.min(x, pageWidth - windowWidth);
- const targetY = Math.min(y, pageHeight - windowHeight);
- // 滚动到目标位置
- window.scrollTo(targetX, targetY);
- // 等待滚动和页面重绘完成
- await new Promise(resolve => setTimeout(resolve, 300));
- if (window._fh_screenshot_canceled) { cleanup(); return; }
- // 获取滚动后的实际位置
- const actualX = window.scrollX || document.documentElement.scrollLeft;
- const actualY = window.scrollY || document.documentElement.scrollTop;
- const posKey = `${actualX},${actualY}`;
- // 如果这个精确位置已经截取过,则跳过
- if (capturedPositions.has(posKey)) {
- // 如果已到达水平末端,跳出内层循环
- if (x >= pageWidth - windowWidth) break;
- continue; // 继续内层循环的下一个x值
- }
- capturedPositions.add(posKey); // 记录新的已截取位置
- // 判断是否为首屏(实际滚动位置为0,0)
- const isFirstScreen = (actualX === 0 && actualY === 0);
- // 非首屏时隐藏固定元素
- if (!isFirstScreen) {
- hideFixedElements();
- // 等待隐藏生效
- await new Promise(resolve => setTimeout(resolve, 50));
- }
- currentStep++;
- updateProgressUI(Math.min(0.9, currentStep / totalSteps), `正在截取第 ${currentStep}/${totalSteps} 部分...`);
- // 执行截图API调用
- let response;
- try {
- response = await new Promise((resolve, reject) => {
- chrome.runtime.sendMessage({ type: 'fh-screenshot-capture-visible' }, res => {
- if (chrome.runtime.lastError) {
- reject(new Error(chrome.runtime.lastError.message || '截图通讯错误'));
- } else if (res) {
- resolve(res);
- } else {
- reject(new Error('截图失败,未收到数据'));
- }
- });
- // 添加超时处理
- setTimeout(() => reject(new Error('截图超时')), 5000);
- });
- } catch (error) {
- // 非首屏时尝试恢复固定元素
- if (!isFirstScreen && window._fh_fixed_elements) {
- showFixedElements();
- }
- captureFailureHandler(error.message || '截图API调用失败');
- return; // 中断截图流程
- }
- if (window._fh_screenshot_canceled) {
- if (!isFirstScreen) showFixedElements(); // Make sure to show elements if cancelled here
- cleanup();
- return;
- }
- // 非首屏时恢复固定元素显示
- if (!isFirstScreen) {
- showFixedElements();
- }
- console.log(`截图成功 [${currentStep}]: 位置(${actualX},${actualY}), 数据长度: ${response.length}`);
- screenshots.push({
- dataUrl: response,
- x: actualX,
- y: actualY,
- width: windowWidth, // 截图是基于视窗尺寸的
- height: windowHeight,
- // 保留 row/col 供可能的调试或兼容性需求,但去重和排序基于 x, y
- row: Math.round(actualY / windowHeight),
- col: Math.round(actualX / windowWidth)
- });
- // 如果当前水平位置已覆盖页面宽度,结束内层循环
- if (x >= pageWidth - windowWidth) break;
- } // 结束内层循环 (x)
- // 如果当前垂直位置已覆盖页面高度,结束外层循环
- if (y >= pageHeight - windowHeight) break;
- } // 结束外层循环 (y)
- // 所有截图完成后,滚动回页面顶部
- window.scrollTo(0, 0);
- await new Promise(resolve => setTimeout(resolve, 100)); // 等待滚动完成
- // 调用完成处理函数
- finishCapture(screenshots, pageWidth, pageHeight);
- } catch (error) {
- // 捕获循环中的意外错误
- captureFailureHandler(error.message || '截图过程中断');
- }
- }, 200); // 初始延迟,等待UI隐藏生效
- /**
- * 完成所有截图后处理 - 修改为接受参数
- * @param {Array} capturedScreenshots 捕获到的截图数组
- * @param {number} finalWidth 最终页面宽度
- * @param {number} finalHeight 最终页面高度
- */
- function finishCapture(capturedScreenshots, finalWidth, finalHeight) {
- if (window._fh_screenshot_canceled) {
- return; // 如果在 finishCapture 前取消,则不继续
- }
- updateProgressUI(0.95, '正在处理截图...');
- if (!capturedScreenshots || capturedScreenshots.length === 0) {
- updateProgressUI(1, '截图失败: 没有截取到任何内容');
- setTimeout(cleanup, 1000);
- return;
- }
- console.log(`处理截图,共 ${capturedScreenshots.length} 张,页面尺寸: ${finalWidth}x${finalHeight}`);
- // 基于精确的捕获滚动坐标 (x, y) 进行去重
- const screenshotMap = new Map();
- capturedScreenshots.forEach(ss => {
- const key = `${ss.x},${ss.y}`;
- if (!screenshotMap.has(key)) {
- screenshotMap.set(key, ss);
- } else {
- console.log(`发现重复精确位置的截图: (${ss.x},${ss.y}),保留第一个`);
- }
- });
- // 将 Map 转换回数组,并按 Y 坐标优先,然后 X 坐标排序
- const uniqueScreenshots = Array.from(screenshotMap.values()).sort((a, b) => {
- if (a.y !== b.y) return a.y - b.y;
- return a.x - b.x;
- });
- console.log(`去重后共 ${uniqueScreenshots.length} 张有效截图(原 ${capturedScreenshots.length} 张)`);
- // 准备发送到后台的数据格式
- let mappedScreenshots = uniqueScreenshots.map((ss, index) => ({
- dataUri: ss.dataUrl, // 确保属性名是 dataUri
- x: ss.x,
- y: ss.y,
- width: ss.width, // 使用捕获时的视窗尺寸
- height: ss.height,
- index: index, // 添加索引供后台使用
- // 添加兼容性字段(如果后台拼接逻辑需要)
- row: ss.row,
- col: ss.col,
- left: ss.x,
- top: ss.y,
- right: ss.x + ss.width,
- bottom: ss.y + ss.height
- }));
- console.log('准备发送所有截图分片到后台:', {
- screenshots: mappedScreenshots.length,
- pageWidth: finalWidth,
- pageHeight: finalHeight
- });
- const screenshotData = {
- filename: buildFilenameFromUrl(),
- screenshots: mappedScreenshots,
- totalWidth: finalWidth,
- totalHeight: finalHeight
- };
- // 发送消息到后台处理
- chrome.runtime.sendMessage({
- type: 'fh-dynamic-any-thing',
- thing: 'page-screenshot-done',
- params: screenshotData
- }, function(resp) {
- if (chrome.runtime.lastError) {
- updateProgressUI(1, '发送结果失败');
- } else {
- updateProgressUI(1, '截图完成');
- }
- // 确保滚动位置在 cleanup 前恢复
- window.scrollTo(originalScrollLeft, originalScrollTop);
- setTimeout(cleanup, 500);
- });
- }
- }
- /**
- * 执行截图操作
- * @param {Object} params
- * @param {String} params.captureType 截图类型:'visible'或'whole'
- */
- function goCapture(params) {
- // 如果正在截图中,则忽略新的请求
- if (isCapturing) {
- return;
- }
- // 如果不是http/https,就不处理
- if (!isValidUrl(location.href)) {
- alert('截图功能仅支持HTTP/HTTPS协议的网页!');
- return;
- }
- isCapturing = true;
- isCancelled = false;
-
- // console.log('开始执行截图,模式:' + params.captureType);
-
- // 根据截图模式执行不同的操作
- if (params.captureType === 'visible') {
- captureVisible()
- .then(result => {
- if (result && result.success) {
- // console.log('可视区域截图完成');
- }
- })
- .catch(error => {
- // console.error('截图失败:', error);
- alert('截图失败: ' + (error.message || '未知错误'));
- })
- .finally(() => {
- // 确保截图状态重置
- isCapturing = false;
-
- // 确保进度UI被移除
- const progressUI = document.getElementById('fehelper_screenshot_progress');
- if (progressUI) {
- progressUI.remove();
- }
- });
- } else {
- captureFullPage()
- .then(result => {
- if (result && result.success) {
- // console.log('全页面截图完成');
- }
- })
- .catch(error => {
- // console.error('截图失败:', error);
- alert('截图失败: ' + (error.message || '未知错误'));
- })
- .finally(() => {
- // 确保截图状态重置
- isCapturing = false;
-
- // 确保进度UI被移除
- const progressUI = document.getElementById('fehelper_screenshot_progress');
- if (progressUI) {
- progressUI.remove();
- }
- });
- }
- }
- /**
- * 创建截图选择UI
- */
- window.screenshotNoPage = function() {
- // console.log('FeHelper: 截图工具触发');
- // 如果正在截图,不创建新UI
- if (isCapturing) {
- alert('正在截图中,请等待当前操作完成');
- return;
- }
-
- try {
- // 先检查是否已存在截图UI
- const existingUI = document.getElementById('fehelper_screenshot');
- if (existingUI) {
- existingUI.remove();
- }
-
- // 创建一个独立的div作为容器
- const container = document.createElement('div');
- container.id = 'fehelper_screenshot_container';
- container.className = 'fehelper-ui-element'; // 添加类名,便于统一隐藏
- container.style.cssText = 'position:fixed;left:0;top:0;right:0;z-index:10000000;';
-
- // 设置内部HTML
- container.innerHTML = `
- <div id="fehelper_screenshot" style="position:fixed;left:0;top:0;right:0;z-index:1000000;padding:15px;background:rgba(0,0,0,0.8);color:#fff;text-align:center;">
- <h3 style="margin:0 0 10px 0;font-size:16px;">FeHelper 网页截图工具</h3>
- <button id="btnVisible" style="margin:0 10px;padding:8px 15px;border-radius:4px;border:none;background:#4CAF50;color:#fff;cursor:pointer;font-size:14px;">可视区域截图</button>
- <button id="btnWhole" style="margin:0 10px;padding:8px 15px;border-radius:4px;border:none;background:#2196F3;color:#fff;cursor:pointer;font-size:14px;">全网页截图</button>
- <button id="btnClose" style="margin:0 10px;padding:8px 15px;border-radius:4px;border:none;background:#f44336;color:#fff;cursor:pointer;font-size:14px;">关闭</button>
- </div>
- `;
-
- // 确保DOM已准备好
- if (!document.body) {
- // console.error('FeHelper截图:document.body不存在,无法添加截图UI');
- // 尝试等待DOM加载完成
- const checkBodyInterval = setInterval(() => {
- if (document.body) {
- clearInterval(checkBodyInterval);
- document.body.appendChild(container);
- bindEvents(container);
- }
- }, 100);
-
- // 超时处理
- setTimeout(() => {
- clearInterval(checkBodyInterval);
- alert('页面DOM未准备好,无法启动截图工具');
- }, 5000);
-
- return;
- }
-
- // 添加到document.body
- document.body.appendChild(container);
-
- // 绑定事件
- bindEvents(container);
-
- // console.log('FeHelper截图UI已添加到页面');
-
- } catch (error) {
- // console.error('FeHelper截图UI创建失败:', error);
- alert('截图工具启动失败:' + error.message);
- }
- };
-
- /**
- * 绑定截图UI的事件
- */
- function bindEvents(container) {
- const btnVisible = document.getElementById('btnVisible');
- const btnWhole = document.getElementById('btnWhole');
- const btnClose = document.getElementById('btnClose');
-
- if (btnVisible) {
- btnVisible.onclick = function() {
- container.remove();
- goCapture({captureType: 'visible'});
- };
- }
-
- if (btnWhole) {
- btnWhole.onclick = function() {
- container.remove();
- goCapture({captureType: 'whole'});
- };
- }
-
- if (btnClose) {
- btnClose.onclick = function() {
- container.remove();
- };
- }
-
- // 添加自动关闭
- setTimeout(() => {
- if (container && document.body.contains(container)) {
- container.remove();
- }
- }, 30000);
- }
- // 添加键盘快捷键支持
- document.addEventListener('keydown', function(e) {
- // ESC键取消截图
- if (e.key === 'Escape' && isCapturing) {
- cancelCapture();
- }
- });
-
- // 添加消息监听,支持通过消息触发截图功能
- chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
- if (request.type === 'fh-screenshot-start') {
- // console.log('FeHelper: 收到截图请求');
- window.screenshotNoPage();
- sendResponse({success: true});
- return true;
- }
- });
-
- // 初始化
- // console.log('FeHelper: 截图功能已加载');
- /**
- * 根据网页URL生成默认文件名
- * @returns {string} - 生成的文件名
- */
- function buildFilenameFromUrl() {
- let name = location.href.split('?')[0].split('#')[0];
- if (name) {
- name = name
- .replace(/^https?:\/\//, '')
- .replace(/[^A-z0-9]+/g, '-')
- .replace(/-+/g, '-')
- .replace(/^[_\-]+/, '')
- .replace(/[_\-]+$/, '');
- name = '-' + name;
- } else {
- name = '';
- }
- return 'fehelper' + name + '-' + Date.now() + '.png';
- }
- // 在截图失败的回调中,确保错误信息被正确记录,并释放资源
- function captureFailureHandler(reason) {
- // console.error('截图失败:', reason);
- updateProgressUI(1, '截图失败');
- setTimeout(() => {
- const errorMsg = reason && reason.message || reason || '截图失败,请刷新页面重试!';
- alert(errorMsg);
- cleanup();
- }, 500);
- }
- };
|