1
0

content-script.js 46 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218
  1. /**
  2. * FeHelper Full Page Capture
  3. * @author FeHelper
  4. * @version 1.0.1
  5. */
  6. window.screenshotContentScript = function () {
  7. // 存储截图数据的数组
  8. let screenshots = [];
  9. // 定义最大尺寸限制常量
  10. const MAX_PRIMARY_DIMENSION = 50000 * 2,
  11. MAX_SECONDARY_DIMENSION = 20000 * 2,
  12. MAX_AREA = MAX_PRIMARY_DIMENSION * MAX_SECONDARY_DIMENSION;
  13. // 保存原始页面标题
  14. const pageOriginalTitle = document.title;
  15. // 是否正在截图中
  16. let isCapturing = false;
  17. // 取消截图的标志
  18. let isCancelled = false;
  19. // 定义全局变量以存储原始滚动位置
  20. let originalScrollLeft = 0;
  21. let originalScrollTop = 0;
  22. /**
  23. * URL合法性校验
  24. * @param {string} url - 要检查的URL
  25. * @returns {boolean} - URL是否合法
  26. */
  27. function isValidUrl(url) {
  28. // 允许的URL模式
  29. const matches = ['http://*/*', 'https://*/*', 'ftp://*/*', 'file://*/*'];
  30. // 不允许的URL模式
  31. const noMatches = [/^https?:\/\/chrome\.google\.com\/.*$/];
  32. // 先检查不允许的URL
  33. for (let i = 0; i < noMatches.length; i++) {
  34. if (noMatches[i].test(url)) {
  35. return false;
  36. }
  37. }
  38. // 再检查允许的URL
  39. for (let i = 0; i < matches.length; i++) {
  40. const pattern = matches[i].replace(/\*/g, '.*');
  41. const regex = new RegExp('^' + pattern + '$');
  42. if (regex.test(url)) {
  43. return true;
  44. }
  45. }
  46. return false;
  47. }
  48. /**
  49. * 释放canvas资源
  50. * @param {Array} canvasList - 要释放的canvas列表
  51. */
  52. function releaseCanvasResources(canvasList) {
  53. if (!canvasList || !canvasList.length) return;
  54. canvasList.forEach(item => {
  55. if (item.ctx) {
  56. item.ctx.clearRect(0, 0, item.canvas.width, item.canvas.height);
  57. }
  58. if (item.canvas) {
  59. item.canvas.width = 0;
  60. item.canvas.height = 0;
  61. }
  62. });
  63. }
  64. /**
  65. * 初始化截图canvas
  66. * @param {number} totalWidth - 总宽度
  67. * @param {number} totalHeight - 总高度
  68. * @returns {Array} - 初始化的canvas数组
  69. * @private
  70. */
  71. function _initScreenshots(totalWidth, totalHeight) {
  72. // 检查尺寸是否超过限制
  73. const badSize = (totalHeight > MAX_PRIMARY_DIMENSION ||
  74. totalWidth > MAX_PRIMARY_DIMENSION ||
  75. totalHeight * totalWidth > MAX_AREA);
  76. const biggerWidth = totalWidth > totalHeight;
  77. // 计算每个分块的最大尺寸
  78. const maxWidth = (!badSize ? totalWidth :
  79. (biggerWidth ? MAX_PRIMARY_DIMENSION : MAX_SECONDARY_DIMENSION));
  80. const maxHeight = (!badSize ? totalHeight :
  81. (biggerWidth ? MAX_SECONDARY_DIMENSION : MAX_PRIMARY_DIMENSION));
  82. // 计算分块数量
  83. const numCols = Math.ceil(totalWidth / maxWidth);
  84. const numRows = Math.ceil(totalHeight / maxHeight);
  85. // 创建结果数组
  86. const result = [];
  87. let canvasIndex = 0;
  88. // 创建所有需要的canvas
  89. for (let row = 0; row < numRows; row++) {
  90. for (let col = 0; col < numCols; col++) {
  91. const canvas = document.createElement('canvas');
  92. canvas.width = (col === numCols - 1 ? totalWidth % maxWidth || maxWidth : maxWidth);
  93. canvas.height = (row === numRows - 1 ? totalHeight % maxHeight || maxHeight : maxHeight);
  94. const left = col * maxWidth;
  95. const top = row * maxHeight;
  96. result.push({
  97. canvas: canvas,
  98. ctx: canvas.getContext('2d'),
  99. index: canvasIndex,
  100. left: left,
  101. right: left + canvas.width,
  102. top: top,
  103. bottom: top + canvas.height
  104. });
  105. canvasIndex++;
  106. }
  107. }
  108. return result;
  109. }
  110. /**
  111. * 从截屏中筛选有效数据
  112. * @param {number} imgLeft - 图像左边界
  113. * @param {number} imgTop - 图像上边界
  114. * @param {number} imgWidth - 图像宽度
  115. * @param {number} imgHeight - 图像高度
  116. * @param {Array} screenshotList - 截图列表
  117. * @returns {Array} - 筛选后的截图列表
  118. * @private
  119. */
  120. function _filterScreenshots(imgLeft, imgTop, imgWidth, imgHeight, screenshotList) {
  121. // 计算图像边界
  122. const imgRight = imgLeft + imgWidth;
  123. const imgBottom = imgTop + imgHeight;
  124. // 筛选与当前区域重叠的截图
  125. return screenshotList.filter(screenshot =>
  126. imgLeft < screenshot.right &&
  127. imgRight > screenshot.left &&
  128. imgTop < screenshot.bottom &&
  129. imgBottom > screenshot.top
  130. );
  131. }
  132. /**
  133. * 添加截图到canvas
  134. * @param {Object} data - 截图数据
  135. * @param {string} uri - 图片URI
  136. */
  137. function addScreenShot(data, uri) {
  138. // 如果已取消截图,不处理
  139. if (isCancelled) return;
  140. const image = new Image();
  141. // 图片加载错误处理
  142. image.onerror = function() {
  143. captureConfig.fail('图片加载失败');
  144. releaseResources();
  145. };
  146. image.onload = function() {
  147. try {
  148. data.image = {width: image.width, height: image.height};
  149. // 调整缩放比例
  150. if (data.windowWidth !== image.width) {
  151. const scale = image.width / data.windowWidth;
  152. data.x *= scale;
  153. data.y *= scale;
  154. data.totalWidth *= scale;
  155. data.totalHeight *= scale;
  156. }
  157. // 如果是第一张截图,初始化canvas
  158. if (!screenshots.length) {
  159. screenshots = _initScreenshots(data.totalWidth, data.totalHeight);
  160. }
  161. // 获取与当前区域重叠的canvas并绘制图像
  162. const matchingScreenshots = _filterScreenshots(
  163. data.x, data.y, image.width, image.height, screenshots
  164. );
  165. matchingScreenshots.forEach(screenshot => {
  166. screenshot.ctx.drawImage(
  167. image,
  168. data.x - screenshot.left,
  169. data.y - screenshot.top
  170. );
  171. });
  172. // 如果是最后一步,调用成功回调
  173. if (data.complete === 1) {
  174. captureConfig.success(data);
  175. isCapturing = false;
  176. }
  177. } catch (e) {
  178. captureConfig.fail('处理截图时出错: ' + e.message);
  179. releaseResources();
  180. }
  181. };
  182. // 设置图片源
  183. image.src = uri;
  184. }
  185. /**
  186. * 释放所有资源
  187. */
  188. function releaseResources() {
  189. releaseCanvasResources(screenshots);
  190. screenshots = [];
  191. isCapturing = false;
  192. isCancelled = false;
  193. }
  194. /**
  195. * 创建截图进度UI
  196. * @param {string} [text='正在截取网页...'] 显示的文本
  197. * @returns {HTMLElement} 创建的进度UI元素
  198. */
  199. function createProgressUI(text = '正在截取网页...') {
  200. // 先检查是否已存在
  201. let progressContainer = document.getElementById('fehelper-screenshot-progress');
  202. if (progressContainer) {
  203. progressContainer.querySelector('.fh-progress-text').textContent = text;
  204. progressContainer.style.display = 'flex';
  205. return progressContainer;
  206. }
  207. // 创建进度UI容器
  208. progressContainer = document.createElement('div');
  209. progressContainer.id = 'fehelper-screenshot-progress';
  210. progressContainer.setAttribute('data-fh-ui', 'true');
  211. progressContainer.className = 'fehelper-ui-element';
  212. progressContainer.style.cssText = `
  213. position: fixed;
  214. bottom: 20px;
  215. left: 50%;
  216. transform: translateX(-50%);
  217. background-color: rgba(0, 0, 0, 0.7);
  218. color: white;
  219. padding: 10px 20px;
  220. border-radius: 5px;
  221. z-index: 10000000;
  222. font-size: 14px;
  223. display: flex;
  224. flex-direction: column;
  225. align-items: center;
  226. box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
  227. width: 300px;
  228. `;
  229. // 创建文本元素
  230. const textElement = document.createElement('div');
  231. textElement.className = 'fh-progress-text';
  232. textElement.textContent = text;
  233. textElement.style.cssText = 'margin-bottom: 10px; width: 100%; text-align: center;';
  234. progressContainer.appendChild(textElement);
  235. // 创建进度条容器
  236. const progressBarContainer = document.createElement('div');
  237. progressBarContainer.style.cssText = `
  238. width: 100%;
  239. height: 10px;
  240. background-color: rgba(255, 255, 255, 0.2);
  241. border-radius: 5px;
  242. overflow: hidden;
  243. margin-bottom: 10px;
  244. `;
  245. progressContainer.appendChild(progressBarContainer);
  246. // 创建进度条
  247. const progressBar = document.createElement('div');
  248. progressBar.className = 'fh-progress-bar';
  249. progressBar.style.cssText = `
  250. height: 100%;
  251. width: 0%;
  252. background-color: #4CAF50;
  253. border-radius: 5px;
  254. transition: width 0.3s;
  255. `;
  256. progressBarContainer.appendChild(progressBar);
  257. // 创建百分比文本
  258. const percentText = document.createElement('div');
  259. percentText.className = 'fh-progress-percent';
  260. percentText.textContent = '0%';
  261. percentText.style.cssText = 'margin-bottom: 10px; font-size: 12px;';
  262. progressContainer.appendChild(percentText);
  263. // 创建取消按钮
  264. const cancelButton = document.createElement('button');
  265. cancelButton.textContent = '取消';
  266. cancelButton.className = 'fh-progress-cancel';
  267. cancelButton.style.cssText = `
  268. background-color: #f44336;
  269. border: none;
  270. color: white;
  271. padding: 5px 10px;
  272. border-radius: 3px;
  273. cursor: pointer;
  274. font-size: 12px;
  275. `;
  276. cancelButton.onclick = () => {
  277. if (window._fh_screenshot_cancel_callback && typeof window._fh_screenshot_cancel_callback === 'function') {
  278. window._fh_screenshot_cancel_callback();
  279. }
  280. progressContainer.style.display = 'none';
  281. };
  282. progressContainer.appendChild(cancelButton);
  283. document.body.appendChild(progressContainer);
  284. return progressContainer;
  285. }
  286. /**
  287. * 更新截图进度UI
  288. * @param {number} percent 进度百分比(0-1)
  289. * @param {string} [text] 可选的文本更新
  290. */
  291. function updateProgressUI(percent, text) {
  292. const progressContainer = document.getElementById('fehelper-screenshot-progress');
  293. if (!progressContainer) return;
  294. const progressBar = progressContainer.querySelector('.fh-progress-bar');
  295. const percentText = progressContainer.querySelector('.fh-progress-percent');
  296. if (progressBar) {
  297. const percentage = Math.min(Math.max(percent * 100, 0), 100);
  298. progressBar.style.width = `${percentage}%`;
  299. }
  300. if (percentText) {
  301. percentText.textContent = `${Math.round(percent * 100)}%`;
  302. }
  303. if (text) {
  304. const textElement = progressContainer.querySelector('.fh-progress-text');
  305. if (textElement) {
  306. textElement.textContent = text;
  307. }
  308. }
  309. }
  310. /**
  311. * 取消截图操作
  312. */
  313. function cancelCapture() {
  314. if (isCapturing) {
  315. isCancelled = true;
  316. isCapturing = false;
  317. // 移除进度UI
  318. const progressContainer = document.getElementById('fehelper-screenshot-progress');
  319. if (progressContainer) {
  320. progressContainer.style.display = 'none';
  321. }
  322. // 恢复原始状态
  323. cleanup && typeof cleanup === 'function' && cleanup();
  324. return true;
  325. }
  326. return false;
  327. }
  328. /**
  329. * 隐藏所有FeHelper UI元素
  330. * 隐藏所有带有 fehelper-ui-element 类或 data-fh-ui 属性的元素
  331. * @returns {Object} 隐藏元素的原始显示状态
  332. */
  333. function hideFeHelperUI() {
  334. const uiElements = document.querySelectorAll('.fehelper-ui-element, [data-fh-ui="true"]');
  335. const originalDisplays = {};
  336. uiElements.forEach((element, index) => {
  337. // Ensure unique ID for lookup later
  338. if (!element.id) {
  339. // Assign a temporary, identifiable ID
  340. element.id = `fh-temp-id-${Math.random().toString(36).substring(2, 9)}`;
  341. }
  342. const id = element.id;
  343. originalDisplays[id] = element.style.display;
  344. element.style.display = 'none';
  345. });
  346. window._fh_original_displays = originalDisplays; // Store globally
  347. return originalDisplays; // Keep return for compatibility if needed elsewhere
  348. }
  349. /**
  350. * 显示所有FeHelper UI元素
  351. * 恢复所有带有 fehelper-ui-element 类或 data-fh-ui 属性的元素的显示状态
  352. * @param {Object} originalDisplays 原始显示状态对象
  353. */
  354. function showFeHelperUI(originalDisplays) { // Accept argument
  355. originalDisplays = originalDisplays || window._fh_original_displays || {}; // Use passed or global
  356. const uiElements = document.querySelectorAll('.fehelper-ui-element, [data-fh-ui="true"]');
  357. uiElements.forEach((element) => {
  358. const id = element.id;
  359. if (id && id in originalDisplays) {
  360. element.style.display = originalDisplays[id];
  361. } else {
  362. // Default restoration if ID mismatch or not found (less reliable)
  363. element.style.display = '';
  364. }
  365. // Remove temporary ID if added and it wasn't originally present
  366. if (id && id.startsWith('fh-temp-id-') && !(id in (window._fh_original_displays || {}))) {
  367. element.removeAttribute('id');
  368. }
  369. });
  370. }
  371. /**
  372. * 隐藏固定定位和粘性定位的元素
  373. * @returns {Array} 被隐藏元素的原始样式信息,用于恢复
  374. */
  375. function hideFixedElements() {
  376. const fixedElements = [];
  377. // 查找所有fixed和sticky定位的元素
  378. const elements = document.querySelectorAll('*');
  379. elements.forEach(el => {
  380. const style = window.getComputedStyle(el);
  381. if (style && (style.position === 'fixed' || style.position === 'sticky')) {
  382. // 排除FeHelper自己的UI元素
  383. if (el.classList.contains('fehelper-ui-element') ||
  384. el.hasAttribute('data-fh-ui') ||
  385. el.id === 'fehelper-screenshot-progress' ||
  386. el.closest('#fehelper_screenshot_container')) {
  387. return;
  388. }
  389. // 保存原始样式
  390. fixedElements.push({
  391. element: el,
  392. originalDisplay: el.style.display,
  393. originalVisibility: el.style.visibility,
  394. originalOpacity: el.style.opacity
  395. });
  396. // 隐藏元素
  397. el.style.visibility = 'hidden';
  398. el.style.opacity = '0';
  399. }
  400. });
  401. window._fh_fixed_elements = fixedElements;
  402. return fixedElements;
  403. }
  404. /**
  405. * 恢复之前隐藏的固定定位和粘性定位元素
  406. */
  407. function showFixedElements() {
  408. const fixedElements = window._fh_fixed_elements || [];
  409. fixedElements.forEach(item => {
  410. const el = item.element;
  411. if (el) {
  412. // 恢复原始样式 - 修正:恢复 visibility 和 opacity
  413. el.style.visibility = item.originalVisibility !== undefined ? item.originalVisibility : '';
  414. el.style.opacity = item.originalOpacity !== undefined ? item.originalOpacity : '';
  415. // Display 属性通常不需要在隐藏/显示 fixed 元素时修改,注释掉以防干扰
  416. // if (item.originalDisplay !== undefined) {
  417. // el.style.display = item.originalDisplay;
  418. // }
  419. }
  420. });
  421. window._fh_fixed_elements = null; // 清理存储的元素
  422. }
  423. // 定义全局的 cleanup 函数
  424. function cleanup() {
  425. // 恢复滚动位置
  426. window.scrollTo(originalScrollLeft, originalScrollTop);
  427. // 恢复UI显示 - 传递存储的原始状态
  428. showFeHelperUI(window._fh_original_displays || {});
  429. showFixedElements();
  430. // 隐藏进度UI
  431. const progressContainer = document.getElementById('fehelper-screenshot-progress');
  432. if (progressContainer) {
  433. progressContainer.style.display = 'none';
  434. }
  435. // 重新显示工具条
  436. const screenshotContainer = document.getElementById('fehelper_screenshot_container');
  437. if (screenshotContainer) {
  438. screenshotContainer.style.display = 'block';
  439. }
  440. window._fh_screenshot_in_progress = false;
  441. window._fh_screenshot_cancel_callback = null;
  442. window._fh_fixed_elements = null; // Clear fixed elements cache
  443. window._fh_original_displays = null; // Clear UI display cache
  444. }
  445. // 配置项
  446. const captureConfig = {
  447. // 成功回调
  448. success: function(data) {
  449. try {
  450. // 构造正确的数据格式
  451. const screenshotData = {
  452. filename: buildFilenameFromUrl(),
  453. screenshots: screenshots.map(ss => {
  454. // 确保使用toDataURL生成png格式的图像
  455. const dataUri = ss.canvas.toDataURL('image/png');
  456. return {
  457. dataUri: dataUri, // 保持与showResult函数期望的属性名一致
  458. index: ss.index,
  459. row: Math.floor(ss.top / (ss.bottom - ss.top || 1)),
  460. col: Math.floor(ss.left / (ss.right - ss.left || 1)),
  461. left: ss.left,
  462. top: ss.top,
  463. right: ss.right,
  464. bottom: ss.bottom,
  465. width: ss.right - ss.left,
  466. height: ss.bottom - ss.top
  467. };
  468. }),
  469. totalWidth: data.totalWidth,
  470. totalHeight: data.totalHeight
  471. };
  472. // 使用正确的消息类型和thing
  473. chrome.runtime.sendMessage({
  474. type: 'fh-dynamic-any-thing',
  475. thing: 'page-screenshot-done',
  476. params: screenshotData
  477. }, function(response) {
  478. // 恢复页面标题并移除进度条
  479. document.title = pageOriginalTitle;
  480. const progressBar = document.getElementById('fehelper_screenshot_progress');
  481. if (progressBar) {
  482. progressBar.remove();
  483. }
  484. // 释放资源
  485. setTimeout(() => {
  486. releaseResources();
  487. }, 1000);
  488. });
  489. } catch (e) {
  490. captureConfig.fail('处理截图结果时出错: ' + e.message);
  491. }
  492. },
  493. // 失败回调
  494. fail: function(reason) {
  495. // 恢复页面标题
  496. document.title = pageOriginalTitle;
  497. // 移除进度条
  498. const progressBar = document.getElementById('fehelper_screenshot_progress');
  499. if (progressBar) {
  500. progressBar.remove();
  501. }
  502. // 显示错误消息
  503. const errorMsg = reason && reason.message || reason || '截图失败,请刷新页面重试!';
  504. // 创建错误提示UI
  505. const errorDiv = document.createElement('div');
  506. 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;';
  507. errorDiv.textContent = errorMsg;
  508. // 添加关闭按钮
  509. const closeButton = document.createElement('button');
  510. closeButton.textContent = '关闭';
  511. closeButton.style.cssText = 'margin-top:10px;padding:5px 15px;background:#fff;color:#000;border:none;border-radius:3px;cursor:pointer;';
  512. closeButton.onclick = function() {
  513. errorDiv.remove();
  514. };
  515. errorDiv.appendChild(document.createElement('br'));
  516. errorDiv.appendChild(closeButton);
  517. document.body.appendChild(errorDiv);
  518. // 自动关闭
  519. setTimeout(() => {
  520. if (document.body.contains(errorDiv)) {
  521. errorDiv.remove();
  522. }
  523. }, 5000);
  524. // 释放资源
  525. releaseResources();
  526. },
  527. // 进度回调
  528. progress: function(complete) {
  529. if (isCancelled) return false;
  530. // 更新进度条
  531. updateProgressUI(complete);
  532. // 更新页面标题
  533. const percent = parseInt(complete * 100, 10) + '%';
  534. document.title = `截图进度:${percent}...`;
  535. if (percent === '100%') {
  536. setTimeout(() => {
  537. document.title = pageOriginalTitle;
  538. }, 800);
  539. }
  540. return true;
  541. }
  542. };
  543. /**
  544. * 计算数组中的最大值
  545. * @param {Array} nums - 数字数组
  546. * @returns {number} - 最大值
  547. */
  548. function max(nums) {
  549. return Math.max(...nums.filter(Boolean));
  550. }
  551. /**
  552. * 执行可视区域截图
  553. */
  554. async function captureVisible() {
  555. if (window._fh_screenshot_in_progress) {
  556. return;
  557. }
  558. window._fh_screenshot_in_progress = true;
  559. // 保存原始滚动位置
  560. originalScrollTop = window.scrollY || document.documentElement.scrollTop;
  561. originalScrollLeft = window.scrollX || document.documentElement.scrollLeft;
  562. // 隐藏工具条
  563. let screenshotContainer = document.getElementById('fehelper_screenshot_container');
  564. if (screenshotContainer) {
  565. screenshotContainer.style.display = 'none';
  566. }
  567. const progressUI = createProgressUI('正在截取可视区域...');
  568. window._fh_screenshot_cancel_callback = cleanup;
  569. // 隐藏干扰UI
  570. hideFeHelperUI();
  571. hideFixedElements();
  572. // 等待DOM更新
  573. setTimeout(() => {
  574. updateProgressUI(0.3, '正在截取可视区域...');
  575. // 从background.js导入MSG_TYPE不现实,这里直接使用特定值
  576. chrome.runtime.sendMessage({
  577. type: 'fh-screenshot-capture-visible'
  578. }, response => {
  579. if (response) {
  580. // 加载截图并处理
  581. const img = new Image();
  582. img.onload = function() {
  583. updateProgressUI(0.8, '正在处理截图...');
  584. // 创建与index.js中showResult函数期望的格式一致的数据
  585. const screenshotData = {
  586. filename: buildFilenameFromUrl(),
  587. screenshots: [{
  588. dataUri: response,
  589. index: 0,
  590. row: 0,
  591. col: 0,
  592. left: 0,
  593. top: 0,
  594. right: img.width,
  595. bottom: img.height,
  596. width: img.width,
  597. height: img.height
  598. }],
  599. totalWidth: img.width,
  600. totalHeight: img.height
  601. };
  602. // 使用统一的消息格式发送数据,确保只发送一次
  603. chrome.runtime.sendMessage({
  604. type: 'fh-dynamic-any-thing',
  605. thing: 'page-screenshot-done',
  606. params: screenshotData
  607. }, function(resp) {
  608. updateProgressUI(1, '截图完成');
  609. setTimeout(cleanup, 500);
  610. });
  611. };
  612. img.onerror = function(e) {
  613. updateProgressUI(1, '截图加载失败');
  614. setTimeout(cleanup, 500);
  615. };
  616. img.src = response;
  617. // 如果图片加载时间过长,设置超时
  618. setTimeout(() => {
  619. if (!img.complete) {
  620. cleanup();
  621. }
  622. }, 3000);
  623. } else {
  624. const errorMessage = '截图失败,请刷新页面重试!';
  625. updateProgressUI(1, '截图失败');
  626. setTimeout(() => {
  627. alert(errorMessage);
  628. cleanup();
  629. }, 500);
  630. }
  631. });
  632. }, 100);
  633. }
  634. /**
  635. * 执行全页面截图
  636. */
  637. async function captureFullPage() {
  638. if (window._fh_screenshot_in_progress) {
  639. return;
  640. }
  641. window._fh_screenshot_in_progress = true;
  642. window._fh_screenshot_canceled = false; // 重命名 cancel 标志以避免冲突
  643. // 保存原始滚动位置
  644. originalScrollTop = window.scrollY || document.documentElement.scrollTop;
  645. originalScrollLeft = window.scrollX || document.documentElement.scrollLeft;
  646. // 计算页面尺寸
  647. const pageWidth = Math.max(
  648. document.documentElement.scrollWidth,
  649. document.body.scrollWidth,
  650. document.documentElement.offsetWidth,
  651. document.body.offsetWidth
  652. );
  653. const pageHeight = Math.max(
  654. document.documentElement.scrollHeight,
  655. document.body.scrollHeight,
  656. document.documentElement.offsetHeight,
  657. document.body.offsetHeight
  658. );
  659. // 获取视窗尺寸
  660. const windowWidth = window.innerWidth;
  661. const windowHeight = window.innerHeight;
  662. // 使用视窗尺寸作为滚动步长,并计算大致步数用于UI显示
  663. const totalSteps = Math.ceil(pageHeight / windowHeight) * Math.ceil(pageWidth / windowWidth);
  664. // 隐藏工具条
  665. let screenshotContainer = document.getElementById('fehelper_screenshot_container');
  666. if (screenshotContainer) {
  667. screenshotContainer.style.display = 'none';
  668. }
  669. // 创建进度UI和取消回调
  670. const progressUI = createProgressUI(`正在截取全页面 (共${totalSteps}步)...`);
  671. window._fh_screenshot_cancel_callback = () => {
  672. window._fh_screenshot_canceled = true; // Set the cancel flag
  673. cleanup(); // Execute cleanup
  674. };
  675. // 隐藏其他干扰UI(但不隐藏固定元素)
  676. hideFeHelperUI(); // 保存原始UI状态
  677. // 创建存储截图数据的数组和已捕获位置的集合
  678. const screenshots = [];
  679. const capturedPositions = new Set();
  680. let currentStep = 0; // 用于进度显示
  681. // 使用异步函数和循环进行截图
  682. setTimeout(async () => {
  683. try {
  684. // 垂直滚动循环
  685. for (let y = 0; ; y += windowHeight) {
  686. // 水平滚动循环
  687. for (let x = 0; ; x += windowWidth) {
  688. if (window._fh_screenshot_canceled) { cleanup(); return; }
  689. // 计算目标滚动位置,限制在页面边界内
  690. const targetX = Math.min(x, pageWidth - windowWidth);
  691. const targetY = Math.min(y, pageHeight - windowHeight);
  692. // 滚动到目标位置
  693. window.scrollTo(targetX, targetY);
  694. // 等待滚动和页面重绘完成
  695. await new Promise(resolve => setTimeout(resolve, 300));
  696. if (window._fh_screenshot_canceled) { cleanup(); return; }
  697. // 获取滚动后的实际位置
  698. const actualX = window.scrollX || document.documentElement.scrollLeft;
  699. const actualY = window.scrollY || document.documentElement.scrollTop;
  700. const posKey = `${actualX},${actualY}`;
  701. // 如果这个精确位置已经截取过,则跳过
  702. if (capturedPositions.has(posKey)) {
  703. // 如果已到达水平末端,跳出内层循环
  704. if (x >= pageWidth - windowWidth) break;
  705. continue; // 继续内层循环的下一个x值
  706. }
  707. capturedPositions.add(posKey); // 记录新的已截取位置
  708. // 判断是否为首屏(实际滚动位置为0,0)
  709. const isFirstScreen = (actualX === 0 && actualY === 0);
  710. // 非首屏时隐藏固定元素
  711. if (!isFirstScreen) {
  712. hideFixedElements();
  713. // 等待隐藏生效
  714. await new Promise(resolve => setTimeout(resolve, 50));
  715. }
  716. currentStep++;
  717. updateProgressUI(Math.min(0.9, currentStep / totalSteps), `正在截取第 ${currentStep}/${totalSteps} 部分...`);
  718. // 执行截图API调用
  719. let response;
  720. try {
  721. response = await new Promise((resolve, reject) => {
  722. chrome.runtime.sendMessage({ type: 'fh-screenshot-capture-visible' }, res => {
  723. if (chrome.runtime.lastError) {
  724. reject(new Error(chrome.runtime.lastError.message || '截图通讯错误'));
  725. } else if (res) {
  726. resolve(res);
  727. } else {
  728. reject(new Error('截图失败,未收到数据'));
  729. }
  730. });
  731. // 添加超时处理
  732. setTimeout(() => reject(new Error('截图超时')), 5000);
  733. });
  734. } catch (error) {
  735. // 非首屏时尝试恢复固定元素
  736. if (!isFirstScreen && window._fh_fixed_elements) {
  737. showFixedElements();
  738. }
  739. captureFailureHandler(error.message || '截图API调用失败');
  740. return; // 中断截图流程
  741. }
  742. if (window._fh_screenshot_canceled) {
  743. if (!isFirstScreen) showFixedElements(); // Make sure to show elements if cancelled here
  744. cleanup();
  745. return;
  746. }
  747. // 非首屏时恢复固定元素显示
  748. if (!isFirstScreen) {
  749. showFixedElements();
  750. }
  751. console.log(`截图成功 [${currentStep}]: 位置(${actualX},${actualY}), 数据长度: ${response.length}`);
  752. screenshots.push({
  753. dataUrl: response,
  754. x: actualX,
  755. y: actualY,
  756. width: windowWidth, // 截图是基于视窗尺寸的
  757. height: windowHeight,
  758. // 保留 row/col 供可能的调试或兼容性需求,但去重和排序基于 x, y
  759. row: Math.round(actualY / windowHeight),
  760. col: Math.round(actualX / windowWidth)
  761. });
  762. // 如果当前水平位置已覆盖页面宽度,结束内层循环
  763. if (x >= pageWidth - windowWidth) break;
  764. } // 结束内层循环 (x)
  765. // 如果当前垂直位置已覆盖页面高度,结束外层循环
  766. if (y >= pageHeight - windowHeight) break;
  767. } // 结束外层循环 (y)
  768. // 所有截图完成后,滚动回页面顶部
  769. window.scrollTo(0, 0);
  770. await new Promise(resolve => setTimeout(resolve, 100)); // 等待滚动完成
  771. // 调用完成处理函数
  772. finishCapture(screenshots, pageWidth, pageHeight);
  773. } catch (error) {
  774. // 捕获循环中的意外错误
  775. captureFailureHandler(error.message || '截图过程中断');
  776. }
  777. }, 200); // 初始延迟,等待UI隐藏生效
  778. /**
  779. * 完成所有截图后处理 - 修改为接受参数
  780. * @param {Array} capturedScreenshots 捕获到的截图数组
  781. * @param {number} finalWidth 最终页面宽度
  782. * @param {number} finalHeight 最终页面高度
  783. */
  784. function finishCapture(capturedScreenshots, finalWidth, finalHeight) {
  785. if (window._fh_screenshot_canceled) {
  786. return; // 如果在 finishCapture 前取消,则不继续
  787. }
  788. updateProgressUI(0.95, '正在处理截图...');
  789. if (!capturedScreenshots || capturedScreenshots.length === 0) {
  790. updateProgressUI(1, '截图失败: 没有截取到任何内容');
  791. setTimeout(cleanup, 1000);
  792. return;
  793. }
  794. console.log(`处理截图,共 ${capturedScreenshots.length} 张,页面尺寸: ${finalWidth}x${finalHeight}`);
  795. // 基于精确的捕获滚动坐标 (x, y) 进行去重
  796. const screenshotMap = new Map();
  797. capturedScreenshots.forEach(ss => {
  798. const key = `${ss.x},${ss.y}`;
  799. if (!screenshotMap.has(key)) {
  800. screenshotMap.set(key, ss);
  801. } else {
  802. console.log(`发现重复精确位置的截图: (${ss.x},${ss.y}),保留第一个`);
  803. }
  804. });
  805. // 将 Map 转换回数组,并按 Y 坐标优先,然后 X 坐标排序
  806. const uniqueScreenshots = Array.from(screenshotMap.values()).sort((a, b) => {
  807. if (a.y !== b.y) return a.y - b.y;
  808. return a.x - b.x;
  809. });
  810. console.log(`去重后共 ${uniqueScreenshots.length} 张有效截图(原 ${capturedScreenshots.length} 张)`);
  811. // 准备发送到后台的数据格式
  812. let mappedScreenshots = uniqueScreenshots.map((ss, index) => ({
  813. dataUri: ss.dataUrl, // 确保属性名是 dataUri
  814. x: ss.x,
  815. y: ss.y,
  816. width: ss.width, // 使用捕获时的视窗尺寸
  817. height: ss.height,
  818. index: index, // 添加索引供后台使用
  819. // 添加兼容性字段(如果后台拼接逻辑需要)
  820. row: ss.row,
  821. col: ss.col,
  822. left: ss.x,
  823. top: ss.y,
  824. right: ss.x + ss.width,
  825. bottom: ss.y + ss.height
  826. }));
  827. console.log('准备发送所有截图分片到后台:', {
  828. screenshots: mappedScreenshots.length,
  829. pageWidth: finalWidth,
  830. pageHeight: finalHeight
  831. });
  832. const screenshotData = {
  833. filename: buildFilenameFromUrl(),
  834. screenshots: mappedScreenshots,
  835. totalWidth: finalWidth,
  836. totalHeight: finalHeight
  837. };
  838. // 发送消息到后台处理
  839. chrome.runtime.sendMessage({
  840. type: 'fh-dynamic-any-thing',
  841. thing: 'page-screenshot-done',
  842. params: screenshotData
  843. }, function(resp) {
  844. if (chrome.runtime.lastError) {
  845. updateProgressUI(1, '发送结果失败');
  846. } else {
  847. updateProgressUI(1, '截图完成');
  848. }
  849. // 确保滚动位置在 cleanup 前恢复
  850. window.scrollTo(originalScrollLeft, originalScrollTop);
  851. setTimeout(cleanup, 500);
  852. });
  853. }
  854. }
  855. /**
  856. * 执行截图操作
  857. * @param {Object} params
  858. * @param {String} params.captureType 截图类型:'visible'或'whole'
  859. */
  860. function goCapture(params) {
  861. // 如果正在截图中,则忽略新的请求
  862. if (isCapturing) {
  863. return;
  864. }
  865. // 如果不是http/https,就不处理
  866. if (!isValidUrl(location.href)) {
  867. alert('截图功能仅支持HTTP/HTTPS协议的网页!');
  868. return;
  869. }
  870. isCapturing = true;
  871. isCancelled = false;
  872. // console.log('开始执行截图,模式:' + params.captureType);
  873. // 根据截图模式执行不同的操作
  874. if (params.captureType === 'visible') {
  875. captureVisible()
  876. .then(result => {
  877. if (result && result.success) {
  878. // console.log('可视区域截图完成');
  879. }
  880. })
  881. .catch(error => {
  882. // console.error('截图失败:', error);
  883. alert('截图失败: ' + (error.message || '未知错误'));
  884. })
  885. .finally(() => {
  886. // 确保截图状态重置
  887. isCapturing = false;
  888. // 确保进度UI被移除
  889. const progressUI = document.getElementById('fehelper_screenshot_progress');
  890. if (progressUI) {
  891. progressUI.remove();
  892. }
  893. });
  894. } else {
  895. captureFullPage()
  896. .then(result => {
  897. if (result && result.success) {
  898. // console.log('全页面截图完成');
  899. }
  900. })
  901. .catch(error => {
  902. // console.error('截图失败:', error);
  903. alert('截图失败: ' + (error.message || '未知错误'));
  904. })
  905. .finally(() => {
  906. // 确保截图状态重置
  907. isCapturing = false;
  908. // 确保进度UI被移除
  909. const progressUI = document.getElementById('fehelper_screenshot_progress');
  910. if (progressUI) {
  911. progressUI.remove();
  912. }
  913. });
  914. }
  915. }
  916. /**
  917. * 创建截图选择UI
  918. */
  919. window.screenshotNoPage = function() {
  920. // console.log('FeHelper: 截图工具触发');
  921. // 如果正在截图,不创建新UI
  922. if (isCapturing) {
  923. alert('正在截图中,请等待当前操作完成');
  924. return;
  925. }
  926. try {
  927. // 先检查是否已存在截图UI
  928. const existingUI = document.getElementById('fehelper_screenshot');
  929. if (existingUI) {
  930. existingUI.remove();
  931. }
  932. // 创建一个独立的div作为容器
  933. const container = document.createElement('div');
  934. container.id = 'fehelper_screenshot_container';
  935. container.className = 'fehelper-ui-element'; // 添加类名,便于统一隐藏
  936. container.style.cssText = 'position:fixed;left:0;top:0;right:0;z-index:10000000;';
  937. // 设置内部HTML
  938. container.innerHTML = `
  939. <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;">
  940. <h3 style="margin:0 0 10px 0;font-size:16px;">FeHelper 网页截图工具</h3>
  941. <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>
  942. <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>
  943. <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>
  944. </div>
  945. `;
  946. // 确保DOM已准备好
  947. if (!document.body) {
  948. // console.error('FeHelper截图:document.body不存在,无法添加截图UI');
  949. // 尝试等待DOM加载完成
  950. const checkBodyInterval = setInterval(() => {
  951. if (document.body) {
  952. clearInterval(checkBodyInterval);
  953. document.body.appendChild(container);
  954. bindEvents(container);
  955. }
  956. }, 100);
  957. // 超时处理
  958. setTimeout(() => {
  959. clearInterval(checkBodyInterval);
  960. alert('页面DOM未准备好,无法启动截图工具');
  961. }, 5000);
  962. return;
  963. }
  964. // 添加到document.body
  965. document.body.appendChild(container);
  966. // 绑定事件
  967. bindEvents(container);
  968. // console.log('FeHelper截图UI已添加到页面');
  969. } catch (error) {
  970. // console.error('FeHelper截图UI创建失败:', error);
  971. alert('截图工具启动失败:' + error.message);
  972. }
  973. };
  974. /**
  975. * 绑定截图UI的事件
  976. */
  977. function bindEvents(container) {
  978. const btnVisible = document.getElementById('btnVisible');
  979. const btnWhole = document.getElementById('btnWhole');
  980. const btnClose = document.getElementById('btnClose');
  981. if (btnVisible) {
  982. btnVisible.onclick = function() {
  983. container.remove();
  984. goCapture({captureType: 'visible'});
  985. };
  986. }
  987. if (btnWhole) {
  988. btnWhole.onclick = function() {
  989. container.remove();
  990. goCapture({captureType: 'whole'});
  991. };
  992. }
  993. if (btnClose) {
  994. btnClose.onclick = function() {
  995. container.remove();
  996. };
  997. }
  998. // 添加自动关闭
  999. setTimeout(() => {
  1000. if (container && document.body.contains(container)) {
  1001. container.remove();
  1002. }
  1003. }, 30000);
  1004. }
  1005. // 添加键盘快捷键支持
  1006. document.addEventListener('keydown', function(e) {
  1007. // ESC键取消截图
  1008. if (e.key === 'Escape' && isCapturing) {
  1009. cancelCapture();
  1010. }
  1011. });
  1012. // 添加消息监听,支持通过消息触发截图功能
  1013. chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
  1014. if (request.type === 'fh-screenshot-start') {
  1015. // console.log('FeHelper: 收到截图请求');
  1016. window.screenshotNoPage();
  1017. sendResponse({success: true});
  1018. return true;
  1019. }
  1020. });
  1021. // 初始化
  1022. // console.log('FeHelper: 截图功能已加载');
  1023. /**
  1024. * 根据网页URL生成默认文件名
  1025. * @returns {string} - 生成的文件名
  1026. */
  1027. function buildFilenameFromUrl() {
  1028. let name = location.href.split('?')[0].split('#')[0];
  1029. if (name) {
  1030. name = name
  1031. .replace(/^https?:\/\//, '')
  1032. .replace(/[^A-z0-9]+/g, '-')
  1033. .replace(/-+/g, '-')
  1034. .replace(/^[_\-]+/, '')
  1035. .replace(/[_\-]+$/, '');
  1036. name = '-' + name;
  1037. } else {
  1038. name = '';
  1039. }
  1040. return 'fehelper' + name + '-' + Date.now() + '.png';
  1041. }
  1042. // 在截图失败的回调中,确保错误信息被正确记录,并释放资源
  1043. function captureFailureHandler(reason) {
  1044. // console.error('截图失败:', reason);
  1045. updateProgressUI(1, '截图失败');
  1046. setTimeout(() => {
  1047. const errorMsg = reason && reason.message || reason || '截图失败,请刷新页面重试!';
  1048. alert(errorMsg);
  1049. cleanup();
  1050. }, 500);
  1051. }
  1052. };