index.js 48 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285
  1. /**
  2. * FeHelper SVG转图片工具
  3. * 实现SVG到图片格式的转换
  4. */
  5. new Vue({
  6. el: '#pageContainer',
  7. data: {
  8. // 工具类型
  9. toolName: 'SVG转图片',
  10. // SVG转图片相关
  11. previewSrc: '',
  12. convertedSrc: '',
  13. outputFormat: 'png',
  14. outputWidth: 0,
  15. outputHeight: 0,
  16. imgWidth: 128,
  17. imgHeight: 128,
  18. originalWidth: 0,
  19. originalHeight: 0,
  20. // 通用
  21. error: '',
  22. // SVG转图片相关数据
  23. svgSource: '',
  24. svgPreviewSrc: '',
  25. svgFile: null,
  26. svgInfo: {
  27. dimensions: '0 x 0',
  28. fileSize: '0 KB',
  29. fileType: 'SVG'
  30. },
  31. // 输出图片相关数据
  32. imgSource: '',
  33. imgPreviewSrc: '',
  34. imgInfo: {
  35. dimensions: '0 x 0',
  36. fileSize: '0 KB',
  37. fileType: 'PNG',
  38. comparison: {
  39. ratio: 0,
  40. isIncrease: false
  41. }
  42. },
  43. // 错误信息
  44. errorMsg: '',
  45. // SVG相关状态
  46. svgDimensions: { width: 0, height: 0 },
  47. svgFileSize: 0,
  48. svgFileName: '',
  49. svgError: '',
  50. // 图片相关状态
  51. imgDimensions: { width: 0, height: 0 },
  52. imgFileSize: 0,
  53. imgFileName: '',
  54. // 转换选项
  55. outputWidth: 0,
  56. outputHeight: 0,
  57. // 转换结果
  58. convertedFileSize: 0,
  59. // 加载状态
  60. isProcessing: false,
  61. processingProgress: 0,
  62. processingMessage: '',
  63. // 文件大小比较
  64. sizeComparison: {
  65. difference: 0,
  66. percentage: 0,
  67. isIncrease: false
  68. }
  69. },
  70. computed: {
  71. // 是否展示SVG预览
  72. showSvgPreview() {
  73. return this.svgSource && !this.svgError;
  74. },
  75. // 是否展示图片预览
  76. showImgPreview() {
  77. return this.imgSource && !this.imgError;
  78. },
  79. // 是否展示转换结果
  80. showResult() {
  81. return this.convertedSrc && !this.svgError && !this.imgError;
  82. },
  83. // 是否可以转换
  84. canConvert() {
  85. return this.svgSource;
  86. },
  87. // 下载文件名称
  88. downloadFileName() {
  89. const baseName = this.svgFileName.replace(/\.svg$/i, '') || 'converted';
  90. return `${baseName}.${this.outputFormat}`;
  91. },
  92. // 格式化后的SVG文件大小
  93. formattedSvgFileSize() {
  94. return this.formatFileSize(this.svgFileSize);
  95. },
  96. // 格式化后的图片文件大小
  97. formattedImgFileSize() {
  98. return this.formatFileSize(this.imgFileSize);
  99. },
  100. // 格式化后的转换结果文件大小
  101. formattedConvertedFileSize() {
  102. return this.formatFileSize(this.convertedFileSize);
  103. },
  104. // 格式化后的文件大小差异
  105. formattedSizeDifference() {
  106. return this.formatFileSize(Math.abs(this.sizeComparison.difference));
  107. },
  108. // 文件大小变化的百分比
  109. sizeChangeText() {
  110. if (this.sizeComparison.percentage === 0) return '无变化';
  111. const sign = this.sizeComparison.isIncrease ? '增加' : '减少';
  112. return `${sign} ${this.sizeComparison.percentage}%`;
  113. },
  114. // 文件大小变化的CSS类
  115. sizeChangeClass() {
  116. if (this.sizeComparison.percentage === 0) return 'size-same';
  117. return this.sizeComparison.isIncrease ? 'size-increase' : 'size-decrease';
  118. }
  119. },
  120. mounted: function() {
  121. // 监听paste事件
  122. document.addEventListener('paste', this.pasteSvg, false);
  123. // 初始化拖放功能
  124. this.initDragAndDrop();
  125. // 确保文件上传元素可用 - 使用多层保障确保DOM完全加载
  126. // 1. 首先使用Vue的nextTick
  127. this.$nextTick(() => {
  128. // 2. 然后添加一个短暂的延时确保DOM完全渲染
  129. setTimeout(() => {
  130. this.ensureFileInputsAvailable();
  131. }, 300);
  132. });
  133. // 3. 添加一个额外的保障,如果页面已完全加载则立即执行,否则等待加载完成
  134. if (document.readyState === 'complete') {
  135. this.ensureFileInputsAvailable();
  136. } else {
  137. window.addEventListener('load', () => {
  138. this.ensureFileInputsAvailable();
  139. });
  140. }
  141. },
  142. watch: {
  143. previewSrc: function(newVal) {
  144. // 确保DOM元素存在再进行操作
  145. if (this.$refs.panelBox) {
  146. if (newVal && newVal.length > 0) {
  147. this.$refs.panelBox.classList.add('has-image');
  148. } else {
  149. this.$refs.panelBox.classList.remove('has-image');
  150. }
  151. }
  152. },
  153. svgPreviewSrc: function(newVal) {
  154. // 使用选择器直接获取元素,避免依赖ref
  155. const svgPanel = document.querySelector('.mod-svg-converter .x-panel');
  156. if (svgPanel) {
  157. if (newVal && newVal.length > 0) {
  158. svgPanel.classList.add('has-image');
  159. } else {
  160. svgPanel.classList.remove('has-image');
  161. }
  162. }
  163. },
  164. imgPreviewSrc: function(newVal) {
  165. // 使用选择器直接获取元素
  166. const imgPanel = document.querySelector('.mod-svg-converter .result-zone img');
  167. if (imgPanel && imgPanel.parentNode) {
  168. if (newVal && newVal.length > 0) {
  169. imgPanel.parentNode.classList.add('has-image');
  170. } else {
  171. imgPanel.parentNode.classList.remove('has-image');
  172. }
  173. }
  174. }
  175. },
  176. methods: {
  177. /**
  178. * 重置状态
  179. */
  180. resetState: function() {
  181. this.previewSrc = '';
  182. this.convertedSrc = '';
  183. this.imgPreviewSrc = '';
  184. this.error = '';
  185. this.outputWidth = 0;
  186. this.outputHeight = 0;
  187. this.originalWidth = 0;
  188. this.originalHeight = 0;
  189. // 移除has-image类
  190. if (this.$refs.panelBox) {
  191. this.$refs.panelBox.classList.remove('has-image');
  192. }
  193. this.svgSource = '';
  194. this.svgPreviewSrc = '';
  195. this.svgFile = null;
  196. this.imgSource = '';
  197. this.imgPreviewSrc = '';
  198. this.errorMsg = '';
  199. this.svgInfo = {
  200. dimensions: '0 x 0',
  201. fileSize: '0 KB',
  202. fileType: 'SVG'
  203. };
  204. this.imgInfo = {
  205. dimensions: '0 x 0',
  206. fileSize: '0 KB',
  207. fileType: this.outputFormat.toUpperCase(),
  208. comparison: {
  209. ratio: 0,
  210. isIncrease: false
  211. }
  212. };
  213. },
  214. /**
  215. * 确保文件输入元素可用并正确绑定
  216. */
  217. ensureFileInputsAvailable() {
  218. // 使用setTimeout确保DOM已经完全加载
  219. setTimeout(() => {
  220. // 检查SVG文件上传元素
  221. let svgFileInput = document.getElementById('svgFile');
  222. if (!svgFileInput) {
  223. // 优先从Vue ref中获取
  224. if (this.$refs.svgFile) {
  225. svgFileInput = this.$refs.svgFile;
  226. console.log('从Vue ref中找到SVG文件上传元素');
  227. } else {
  228. console.warn('SVG文件上传元素不存在,创建新元素');
  229. svgFileInput = document.createElement('input');
  230. svgFileInput.type = 'file';
  231. svgFileInput.id = 'svgFile';
  232. svgFileInput.accept = '.svg';
  233. svgFileInput.style.display = 'none';
  234. document.body.appendChild(svgFileInput);
  235. // 绑定上传事件
  236. svgFileInput.addEventListener('change', (event) => {
  237. this.uploadSvgFile(event);
  238. });
  239. }
  240. }
  241. }, 100); // 100ms延迟,确保DOM已完全加载
  242. },
  243. /**
  244. * 重置SVG文件(SVG转图片模式)
  245. */
  246. resetSvgFile() {
  247. try {
  248. // 1. 首先尝试使用Vue的refs
  249. if (this.$refs.svgFile) {
  250. this.$refs.svgFile.click();
  251. return;
  252. }
  253. // 2. 然后尝试使用ID查询
  254. const fileInput = document.getElementById('svgFile');
  255. if (fileInput) {
  256. fileInput.click();
  257. return;
  258. }
  259. // 3. 如果上述方法都失败,确保文件输入元素可用
  260. this.ensureFileInputsAvailable();
  261. // 4. 再次尝试
  262. const newFileInput = document.getElementById('svgFile');
  263. if (newFileInput) {
  264. newFileInput.click();
  265. return;
  266. }
  267. // 5. 最后的备用方案
  268. console.warn('无法找到SVG文件上传元素,使用临时元素');
  269. this.createTemporaryFileInput('.svg', this.uploadSvgFile.bind(this));
  270. } catch (error) {
  271. console.error('SVG文件选择器点击失败:', error);
  272. this.createTemporaryFileInput('.svg', this.uploadSvgFile.bind(this));
  273. }
  274. },
  275. /**
  276. * 创建临时文件输入元素并触发点击
  277. * @param {string} acceptType - 接受的文件类型
  278. * @param {Function} changeHandler - 文件变化处理函数
  279. */
  280. createTemporaryFileInput(acceptType, changeHandler) {
  281. const tempInput = document.createElement('input');
  282. tempInput.type = 'file';
  283. tempInput.accept = acceptType;
  284. tempInput.style.display = 'none';
  285. tempInput.addEventListener('change', (event) => {
  286. if (changeHandler) {
  287. changeHandler(event);
  288. }
  289. // 使用后移除临时元素
  290. document.body.removeChild(tempInput);
  291. });
  292. document.body.appendChild(tempInput);
  293. tempInput.click();
  294. },
  295. /**
  296. * 弹出文件选择对话框 - SVG
  297. */
  298. upload: function(event) {
  299. event.preventDefault();
  300. this.$refs.fileBox.click();
  301. },
  302. /**
  303. * 加载SVG文件
  304. */
  305. loadSvg: function() {
  306. if (this.$refs.fileBox.files.length) {
  307. const file = this.$refs.fileBox.files[0];
  308. this.processSvgFile(file);
  309. this.$refs.fileBox.value = '';
  310. }
  311. },
  312. /**
  313. * 处理SVG文件
  314. */
  315. processSvgFile: function(file) {
  316. // 检查文件大小
  317. const MAX_SVG_SIZE = 5 * 1024 * 1024; // 5MB
  318. if (file.size > MAX_SVG_SIZE) {
  319. // 显示文件大小警告
  320. this.showFileSizeWarning(file, 'svg');
  321. return;
  322. }
  323. // 显示加载状态
  324. this.isProcessing = true;
  325. this.processingMessage = '正在处理SVG文件...';
  326. this.processingProgress = 20;
  327. // 使用全局加载函数作为备份
  328. if (window.showLoading) {
  329. window.showLoading('正在处理SVG文件...');
  330. }
  331. // 读取文件内容
  332. FileUtils.readAsText(file, (svgContent, error) => {
  333. if (error) {
  334. this.handleError(error, 'svgUpload');
  335. if (window.hideLoading) window.hideLoading();
  336. return;
  337. }
  338. try {
  339. // 验证SVG内容
  340. if (!svgContent.includes('<svg') || !svgContent.includes('</svg>')) {
  341. this.handleError('无效的SVG文件,缺少SVG标签', 'svgUpload');
  342. if (window.hideLoading) window.hideLoading();
  343. return;
  344. }
  345. this.processingProgress = 50;
  346. // 解析SVG获取尺寸
  347. const parser = new DOMParser();
  348. const svgDoc = parser.parseFromString(svgContent, 'image/svg+xml');
  349. const parserError = svgDoc.querySelector('parsererror');
  350. if (parserError) {
  351. this.handleError('SVG解析失败,文件可能损坏或格式不正确', 'svgUpload');
  352. if (window.hideLoading) window.hideLoading();
  353. return;
  354. }
  355. // 提取尺寸信息
  356. const svgElement = svgDoc.documentElement;
  357. let width = svgElement.getAttribute('width');
  358. let height = svgElement.getAttribute('height');
  359. if (!width || !height || width.includes('%') || height.includes('%')) {
  360. const viewBox = svgElement.getAttribute('viewBox');
  361. if (viewBox) {
  362. const viewBoxValues = viewBox.split(/\s+|,/);
  363. if (viewBoxValues.length >= 4) {
  364. width = parseFloat(viewBoxValues[2]);
  365. height = parseFloat(viewBoxValues[3]);
  366. }
  367. }
  368. } else {
  369. width = parseFloat(width);
  370. height = parseFloat(height);
  371. }
  372. // 保存SVG信息
  373. this.svgDimensions = {
  374. width: width || 300,
  375. height: height || 150
  376. };
  377. this.svgSource = svgContent;
  378. this.svgFile = file;
  379. this.svgFileName = file.name;
  380. this.svgFileSize = file.size;
  381. this.processingProgress = 70;
  382. // 更新SVG信息
  383. this.svgInfo.dimensions = `${this.svgDimensions.width} x ${this.svgDimensions.height}`;
  384. this.svgInfo.fileSize = this.formatFileSize(file.size);
  385. this.svgInfo.fileType = 'SVG';
  386. // 创建DataURL预览
  387. const reader = new FileReader();
  388. reader.onload = (e) => {
  389. this.svgPreviewSrc = e.target.result;
  390. // 完成处理
  391. this.processingProgress = 100;
  392. setTimeout(() => {
  393. this.isProcessing = false;
  394. if (window.hideLoading) window.hideLoading();
  395. }, 300);
  396. };
  397. reader.onerror = () => {
  398. this.handleError('读取SVG文件失败', 'svgUpload');
  399. if (window.hideLoading) window.hideLoading();
  400. };
  401. reader.readAsDataURL(file);
  402. } catch (err) {
  403. this.handleError(err, 'svgUpload');
  404. if (window.hideLoading) window.hideLoading();
  405. }
  406. });
  407. },
  408. /**
  409. * 粘贴SVG内容
  410. * @param {ClipboardEvent} event - 粘贴事件对象
  411. */
  412. pasteSvg(event) {
  413. // 显示加载状态
  414. this.isProcessing = true;
  415. this.processingMessage = '正在处理粘贴内容...';
  416. this.processingProgress = 20;
  417. if (event && event.clipboardData) {
  418. // 从事件中获取剪贴板数据
  419. const items = event.clipboardData.items || {};
  420. let hasSvgContent = false;
  421. // 处理文本内容,可能是SVG代码或URL
  422. for (let i = 0; i < items.length; i++) {
  423. const item = items[i];
  424. if (item.type === 'text/plain') {
  425. item.getAsString((text) => {
  426. this.processingProgress = 40;
  427. const trimmedText = text.trim();
  428. // 检查是否是SVG内容
  429. if (trimmedText.startsWith('<svg') || trimmedText.startsWith('<?xml') && trimmedText.includes('<svg')) {
  430. hasSvgContent = true;
  431. this.processSvgText(trimmedText);
  432. }
  433. // 检查是否是URL
  434. else if (trimmedText.startsWith('http') &&
  435. (trimmedText.toLowerCase().endsWith('.svg') ||
  436. trimmedText.toLowerCase().includes('.svg?'))) {
  437. hasSvgContent = true;
  438. this.loadSvgFromUrl();
  439. } else {
  440. this.handleError('剪贴板中没有SVG内容', 'svgUpload');
  441. }
  442. });
  443. break;
  444. }
  445. }
  446. // 处理SVG图像文件
  447. if (!hasSvgContent) {
  448. for (let i = 0; i < items.length; i++) {
  449. const item = items[i];
  450. if (item.type.indexOf('image/svg+xml') !== -1) {
  451. const file = item.getAsFile();
  452. if (file) {
  453. this.processingProgress = 60;
  454. hasSvgContent = true;
  455. this.uploadSvgFile({ target: { files: [file] } });
  456. break;
  457. }
  458. }
  459. }
  460. }
  461. // 如果没有找到SVG内容
  462. if (!hasSvgContent) {
  463. this.handleError('剪贴板中没有SVG内容或无法访问剪贴板', 'svgUpload');
  464. }
  465. } else {
  466. this.handleError('无法访问剪贴板,请直接选择文件上传', 'svgUpload');
  467. }
  468. },
  469. /**
  470. * 处理粘贴的SVG文本内容
  471. * @param {string} svgText - SVG文本内容
  472. */
  473. processSvgText(svgText) {
  474. try {
  475. this.processingProgress = 60;
  476. // 验证SVG内容
  477. if (!svgText.includes('<svg') || !svgText.includes('</svg>')) {
  478. this.handleError('无效的SVG内容,缺少SVG标签', 'svgUpload');
  479. return;
  480. }
  481. // 解析SVG获取尺寸
  482. const parser = new DOMParser();
  483. const svgDoc = parser.parseFromString(svgText, 'image/svg+xml');
  484. const parserError = svgDoc.querySelector('parsererror');
  485. if (parserError) {
  486. this.handleError('SVG解析失败,内容可能损坏或格式不正确', 'svgUpload');
  487. return;
  488. }
  489. // 创建SVG文件
  490. const blob = new Blob([svgText], {type: 'image/svg+xml'});
  491. const file = new File([blob], 'pasted.svg', {type: 'image/svg+xml'});
  492. // 计算SVG尺寸
  493. const svgElement = svgDoc.documentElement;
  494. let width = svgElement.getAttribute('width');
  495. let height = svgElement.getAttribute('height');
  496. if (!width || !height || width.includes('%') || height.includes('%')) {
  497. const viewBox = svgElement.getAttribute('viewBox');
  498. if (viewBox) {
  499. const viewBoxValues = viewBox.split(/\s+|,/);
  500. if (viewBoxValues.length >= 4) {
  501. width = parseFloat(viewBoxValues[2]);
  502. height = parseFloat(viewBoxValues[3]);
  503. }
  504. }
  505. } else {
  506. width = parseFloat(width);
  507. height = parseFloat(height);
  508. }
  509. // 保存SVG信息
  510. this.svgDimensions = {
  511. width: width || 300,
  512. height: height || 150
  513. };
  514. this.svgSource = svgText;
  515. this.svgFile = file;
  516. this.svgFileName = 'pasted.svg';
  517. this.svgFileSize = blob.size;
  518. // 更新SVG信息
  519. this.svgInfo.dimensions = `${this.svgDimensions.width} x ${this.svgDimensions.height}`;
  520. this.svgInfo.fileSize = this.formatFileSize(blob.size);
  521. this.processingProgress = 80;
  522. // 创建SVG预览
  523. const reader = new FileReader();
  524. reader.onload = (e) => {
  525. this.svgPreviewSrc = e.target.result;
  526. // 完成处理
  527. this.processingProgress = 100;
  528. setTimeout(() => {
  529. this.isProcessing = false;
  530. }, 300);
  531. };
  532. reader.onerror = () => {
  533. this.handleError('读取SVG内容失败', 'svgUpload');
  534. };
  535. reader.readAsDataURL(file);
  536. } catch (err) {
  537. this.handleError(err, 'svgUpload');
  538. }
  539. },
  540. /**
  541. * 从URL加载SVG
  542. */
  543. loadSvgFromUrl() {
  544. const url = prompt('请输入SVG文件的URL:');
  545. if (!url) return;
  546. // 显示加载状态
  547. this.isProcessing = true;
  548. this.processingMessage = '正在从URL加载SVG...';
  549. this.processingProgress = 20;
  550. // 验证URL
  551. if (!url.trim().startsWith('http')) {
  552. this.handleError('URL格式不正确,请输入以http或https开头的有效URL', 'urlLoad');
  553. return;
  554. }
  555. // 模拟进度更新
  556. const progressInterval = setInterval(() => {
  557. if (this.processingProgress < 80) {
  558. this.processingProgress += 10;
  559. }
  560. }, 300);
  561. fetch(url)
  562. .then(response => {
  563. if (!response.ok) {
  564. throw new Error(`获取文件失败,服务器返回状态码: ${response.status}`);
  565. }
  566. this.processingProgress = 90;
  567. return response.text();
  568. })
  569. .then(svgContent => {
  570. clearInterval(progressInterval);
  571. // 验证是否为SVG内容
  572. if (!svgContent.includes('<svg') || !svgContent.includes('</svg>')) {
  573. throw new Error('URL返回的内容不是有效的SVG格式');
  574. }
  575. // 创建SVG Blob
  576. const blob = new Blob([svgContent], {type: 'image/svg+xml'});
  577. const file = new File([blob], 'fromURL.svg', {type: 'image/svg+xml'});
  578. // 解析SVG获取尺寸信息
  579. const parser = new DOMParser();
  580. const svgDoc = parser.parseFromString(svgContent, 'image/svg+xml');
  581. const svgElement = svgDoc.documentElement;
  582. // 获取宽高
  583. let width = svgElement.getAttribute('width');
  584. let height = svgElement.getAttribute('height');
  585. if (!width || !height || width.includes('%') || height.includes('%')) {
  586. const viewBox = svgElement.getAttribute('viewBox');
  587. if (viewBox) {
  588. const viewBoxValues = viewBox.split(/\s+|,/);
  589. if (viewBoxValues.length >= 4) {
  590. width = parseFloat(viewBoxValues[2]);
  591. height = parseFloat(viewBoxValues[3]);
  592. }
  593. }
  594. } else {
  595. width = parseFloat(width);
  596. height = parseFloat(height);
  597. }
  598. // 保存宽高信息
  599. this.svgDimensions = {
  600. width: width || 300,
  601. height: height || 150
  602. };
  603. // 保存文件
  604. this.svgFile = file;
  605. this.svgFileName = url.split('/').pop() || 'fromURL.svg';
  606. this.svgFileSize = blob.size;
  607. this.svgSource = svgContent;
  608. // 更新SVG信息
  609. this.svgInfo.dimensions = `${this.svgDimensions.width} x ${this.svgDimensions.height}`;
  610. this.svgInfo.fileSize = this.formatFileSize(blob.size);
  611. // 创建预览
  612. const reader = new FileReader();
  613. reader.onload = (e) => {
  614. this.svgPreviewSrc = e.target.result;
  615. // 完成加载
  616. this.processingProgress = 100;
  617. setTimeout(() => {
  618. this.isProcessing = false;
  619. }, 300);
  620. };
  621. reader.onerror = () => {
  622. this.handleError('读取SVG文件失败', 'urlLoad');
  623. };
  624. reader.readAsDataURL(blob);
  625. })
  626. .catch(error => {
  627. clearInterval(progressInterval);
  628. this.handleError('加载SVG失败: ' + error.message, 'urlLoad');
  629. });
  630. },
  631. /**
  632. * 重置输出尺寸为原始尺寸
  633. */
  634. resetSize: function() {
  635. this.outputWidth = this.originalWidth;
  636. this.outputHeight = this.originalHeight;
  637. },
  638. /**
  639. * 将SVG转换为图片
  640. */
  641. convertSvg() {
  642. if (!this.svgPreviewSrc) return;
  643. // 设置处理状态
  644. this.isProcessing = true;
  645. this.processingMessage = '正在转换SVG...';
  646. this.processingProgress = 0;
  647. // 模拟进度更新
  648. const progressInterval = setInterval(() => {
  649. if (this.processingProgress < 90) {
  650. this.processingProgress += 10;
  651. }
  652. }, 200);
  653. // 创建图像对象
  654. const img = new Image();
  655. img.onload = () => {
  656. try {
  657. const canvas = document.createElement('canvas');
  658. // 设置输出尺寸
  659. canvas.width = this.imgWidth || this.svgDimensions.width || img.width;
  660. canvas.height = this.imgHeight || this.svgDimensions.height || img.height;
  661. const ctx = canvas.getContext('2d');
  662. // 绘制白色背景(对于JPG格式)
  663. if (this.outputFormat === 'jpg') {
  664. ctx.fillStyle = '#FFFFFF';
  665. ctx.fillRect(0, 0, canvas.width, canvas.height);
  666. }
  667. // 绘制SVG
  668. ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
  669. // 转换为数据URL
  670. const dataURL = canvas.toDataURL('image/' + this.outputFormat, 0.95);
  671. this.imgPreviewSrc = dataURL;
  672. // 计算转换后文件大小
  673. const convertedSize = Math.round(dataURL.length * 0.75);
  674. // 进度100%
  675. this.processingProgress = 100;
  676. // 延迟一点关闭加载状态,让用户看到100%
  677. setTimeout(() => {
  678. clearInterval(progressInterval);
  679. this.isProcessing = false;
  680. // 更新图片信息
  681. this.imgInfo.dimensions = `${canvas.width} x ${canvas.height}`;
  682. this.imgInfo.fileSize = this.formatFileSize(convertedSize);
  683. this.imgInfo.fileType = this.outputFormat.toUpperCase();
  684. // 计算大小比较
  685. if (this.svgFileSize > 0) {
  686. const difference = convertedSize - this.svgFileSize;
  687. const percentage = Math.round((Math.abs(difference) / this.svgFileSize) * 100);
  688. this.imgInfo.comparison.ratio = percentage;
  689. this.imgInfo.comparison.isIncrease = difference > 0;
  690. }
  691. }, 500);
  692. } catch (error) {
  693. clearInterval(progressInterval);
  694. this.isProcessing = false;
  695. this.errorMsg = '转换失败: ' + error.message;
  696. }
  697. };
  698. img.onerror = () => {
  699. clearInterval(progressInterval);
  700. this.isProcessing = false;
  701. this.errorMsg = '加载SVG图像失败,请检查SVG文件是否有效';
  702. };
  703. img.src = this.svgPreviewSrc;
  704. },
  705. /**
  706. * 下载转换后的图片
  707. */
  708. downloadImage() {
  709. if (!this.imgPreviewSrc) {
  710. return;
  711. }
  712. // 获取当前时间戳
  713. const timestamp = new Date().getTime();
  714. const formattedDate = new Date().toISOString().replace(/:/g, '-').substring(0, 19);
  715. // 创建下载链接
  716. const link = document.createElement('a');
  717. // 基础文件名(移除.svg扩展名)
  718. let fileName = this.svgFileName.replace(/\.svg$/i, '') || 'converted';
  719. // 添加时间戳到文件名
  720. fileName += '_' + formattedDate;
  721. // 添加扩展名
  722. link.download = fileName + '.' + this.outputFormat;
  723. link.href = this.imgPreviewSrc;
  724. link.click();
  725. },
  726. /**
  727. * 初始化拖放功能
  728. */
  729. initDragAndDrop() {
  730. const svgDropZone = document.querySelector('.x-panel');
  731. if (svgDropZone) {
  732. // 监听拖拽 - SVG
  733. svgDropZone.addEventListener('drop', (event) => {
  734. event.preventDefault();
  735. event.stopPropagation();
  736. let files = event.dataTransfer.files;
  737. if (files.length) {
  738. if (/svg/.test(files[0].type)) {
  739. this.processSvgFile(files[0]);
  740. } else {
  741. this.errorMsg = '请选择SVG文件!';
  742. }
  743. }
  744. }, false);
  745. // 监听拖拽阻止默认行为 - SVG
  746. svgDropZone.addEventListener('dragover', (event) => {
  747. event.preventDefault();
  748. event.stopPropagation();
  749. }, false);
  750. }
  751. },
  752. /**
  753. * 计算并设置SVG文件信息
  754. */
  755. calculateSvgInfo(svgContent, fileSize) {
  756. // 设置文件大小
  757. this.svgInfo.fileSize = this.formatFileSize(fileSize);
  758. // 尝试从SVG内容解析尺寸
  759. const widthMatch = svgContent.match(/width="([^"]+)"/);
  760. const heightMatch = svgContent.match(/height="([^"]+)"/);
  761. if (widthMatch && heightMatch) {
  762. let width = widthMatch[1];
  763. let height = heightMatch[1];
  764. // 如果尺寸带有单位,尝试转换为像素
  765. if (isNaN(parseFloat(width))) {
  766. width = '自适应';
  767. }
  768. if (isNaN(parseFloat(height))) {
  769. height = '自适应';
  770. }
  771. if (width !== '自适应' && height !== '自适应') {
  772. this.svgInfo.dimensions = `${width} x ${height}`;
  773. } else {
  774. // 获取viewBox尺寸作为备选
  775. const viewBoxMatch = svgContent.match(/viewBox="([^"]+)"/);
  776. if (viewBoxMatch) {
  777. const viewBox = viewBoxMatch[1].split(' ');
  778. if (viewBox.length === 4) {
  779. this.svgInfo.dimensions = `${viewBox[2]} x ${viewBox[3]}`;
  780. }
  781. }
  782. }
  783. }
  784. },
  785. /**
  786. * 更新图片信息
  787. */
  788. updateImageInfo(dataUrl, originalSize) {
  789. // 计算文件大小
  790. fetch(dataUrl)
  791. .then(res => res.blob())
  792. .then(blob => {
  793. const img = new Image();
  794. img.onload = () => {
  795. // 设置尺寸
  796. this.imgInfo.dimensions = `${img.width} x ${img.height}`;
  797. // 设置文件类型
  798. this.imgInfo.fileType = this.outputFormat.toUpperCase();
  799. // 设置文件大小
  800. const fileSize = blob.size;
  801. this.imgInfo.fileSize = this.formatFileSize(fileSize);
  802. // 计算大小比较
  803. const originalSizeNum = this.parseFileSize(originalSize);
  804. if (originalSizeNum > 0) {
  805. const ratio = ((fileSize / originalSizeNum) * 100 - 100).toFixed(1);
  806. this.imgInfo.comparison.ratio = Math.abs(ratio);
  807. this.imgInfo.comparison.isIncrease = ratio > 0;
  808. }
  809. };
  810. img.src = dataUrl;
  811. });
  812. },
  813. /**
  814. * 格式化文件大小
  815. */
  816. formatFileSize(bytes) {
  817. if (bytes === 0) return '0 KB';
  818. const k = 1024;
  819. const sizes = ['B', 'KB', 'MB', 'GB'];
  820. const i = Math.floor(Math.log(bytes) / Math.log(k));
  821. return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
  822. },
  823. /**
  824. * 解析文件大小字符串为字节数
  825. */
  826. parseFileSize(sizeStr) {
  827. if (!sizeStr || typeof sizeStr !== 'string') return 0;
  828. const parts = sizeStr.split(' ');
  829. if (parts.length !== 2) return 0;
  830. const size = parseFloat(parts[0]);
  831. const unit = parts[1];
  832. switch (unit) {
  833. case 'Bytes':
  834. return size;
  835. case 'KB':
  836. return size * 1024;
  837. case 'MB':
  838. return size * 1024 * 1024;
  839. case 'GB':
  840. return size * 1024 * 1024 * 1024;
  841. default:
  842. return 0;
  843. }
  844. },
  845. /**
  846. * 显示文件大小警告
  847. */
  848. showFileSizeWarning: function(file, fileType) {
  849. const fileSizeMB = (file.size / (1024 * 1024)).toFixed(2);
  850. let warningMessage = '';
  851. const maxSizeLimit = '5MB';
  852. warningMessage = `您上传的SVG文件大小为 ${fileSizeMB}MB,超过了建议的最大大小 ${maxSizeLimit}。过大的文件可能导致浏览器性能问题或转换失败。是否继续处理?`;
  853. // 创建警告对话框容器
  854. const warningContainer = document.createElement('div');
  855. warningContainer.className = 'warning-dialog';
  856. warningContainer.style.position = 'fixed';
  857. warningContainer.style.top = '0';
  858. warningContainer.style.left = '0';
  859. warningContainer.style.width = '100%';
  860. warningContainer.style.height = '100%';
  861. warningContainer.style.backgroundColor = 'rgba(0, 0, 0, 0.5)';
  862. warningContainer.style.display = 'flex';
  863. warningContainer.style.justifyContent = 'center';
  864. warningContainer.style.alignItems = 'center';
  865. warningContainer.style.zIndex = '9999';
  866. // 创建对话框内容
  867. const dialogBox = document.createElement('div');
  868. dialogBox.className = 'warning-dialog-box';
  869. dialogBox.style.backgroundColor = '#fff';
  870. dialogBox.style.borderRadius = '8px';
  871. dialogBox.style.padding = '20px';
  872. dialogBox.style.maxWidth = '500px';
  873. dialogBox.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.2)';
  874. // 创建标题
  875. const title = document.createElement('h3');
  876. title.textContent = '文件大小警告';
  877. title.style.color = '#e74c3c';
  878. title.style.marginTop = '0';
  879. // 创建警告内容
  880. const content = document.createElement('p');
  881. content.textContent = warningMessage;
  882. content.style.marginBottom = '20px';
  883. // 创建按钮容器
  884. const btnContainer = document.createElement('div');
  885. btnContainer.style.display = 'flex';
  886. btnContainer.style.justifyContent = 'flex-end';
  887. btnContainer.style.gap = '10px';
  888. // 取消按钮
  889. const cancelBtn = document.createElement('button');
  890. cancelBtn.className = 'btn-cancel';
  891. cancelBtn.textContent = '取消';
  892. cancelBtn.style.padding = '8px 16px';
  893. cancelBtn.style.border = 'none';
  894. cancelBtn.style.borderRadius = '4px';
  895. cancelBtn.style.backgroundColor = '#e0e0e0';
  896. cancelBtn.style.cursor = 'pointer';
  897. cancelBtn.onclick = () => {
  898. document.body.removeChild(warningContainer);
  899. this.isProcessing = false;
  900. this.processingProgress = 0;
  901. };
  902. // 继续按钮
  903. const continueBtn = document.createElement('button');
  904. continueBtn.className = 'btn-continue';
  905. continueBtn.textContent = '继续处理';
  906. continueBtn.style.padding = '8px 16px';
  907. continueBtn.style.border = 'none';
  908. continueBtn.style.borderRadius = '4px';
  909. continueBtn.style.backgroundColor = '#3498db';
  910. continueBtn.style.color = '#fff';
  911. continueBtn.style.cursor = 'pointer';
  912. continueBtn.onclick = () => {
  913. document.body.removeChild(warningContainer);
  914. this.processSvgFile(file);
  915. };
  916. // 组装对话框
  917. btnContainer.appendChild(cancelBtn);
  918. btnContainer.appendChild(continueBtn);
  919. dialogBox.appendChild(title);
  920. dialogBox.appendChild(content);
  921. dialogBox.appendChild(btnContainer);
  922. warningContainer.appendChild(dialogBox);
  923. // 添加到页面
  924. document.body.appendChild(warningContainer);
  925. // 播放警告提示音
  926. this.playSound('warning');
  927. },
  928. /**
  929. * 播放提示音
  930. */
  931. playSound: function(type) {
  932. try {
  933. let soundUrl = '';
  934. if (type === 'error') {
  935. soundUrl = chrome.runtime.getURL('static/audio/error.mp3');
  936. } else if (type === 'warning') {
  937. soundUrl = chrome.runtime.getURL('static/audio/warning.mp3');
  938. } else if (type === 'success') {
  939. soundUrl = chrome.runtime.getURL('static/audio/success.mp3');
  940. }
  941. if (soundUrl) {
  942. const audio = new Audio(soundUrl);
  943. audio.volume = 0.5;
  944. audio.play().catch(e => console.warn('无法播放提示音:', e));
  945. }
  946. } catch (err) {
  947. console.warn('播放提示音失败:', err);
  948. }
  949. },
  950. /**
  951. * 统一错误处理方法
  952. * @param {Error|string} error - 错误对象或错误消息
  953. * @param {string} type - 错误类型 'svgConvert'|'imgConvert'
  954. * @param {Object} options - 额外选项
  955. */
  956. handleError(error, type, options = {}) {
  957. // 默认选项
  958. const defaultOptions = {
  959. isWarning: false,
  960. clearAfter: 0
  961. };
  962. // 合并选项
  963. const finalOptions = {...defaultOptions, ...options};
  964. // 获取错误消息
  965. const message = error instanceof Error ? error.message : error;
  966. // 根据类型设置错误消息
  967. if (type === 'svgConvert') {
  968. this.errorMsg = finalOptions.isWarning ? message : '转换失败: ' + message;
  969. } else {
  970. this.errorMsg = finalOptions.isWarning ? message : '转换失败: ' + message;
  971. }
  972. // 记录到控制台
  973. if (finalOptions.isWarning) {
  974. console.warn(message);
  975. } else {
  976. console.error(message);
  977. }
  978. // 关闭加载状态
  979. this.isProcessing = false;
  980. // 如果设置了自动清除
  981. if (finalOptions.clearAfter > 0) {
  982. setTimeout(() => {
  983. this.errorMsg = '';
  984. }, finalOptions.clearAfter);
  985. }
  986. },
  987. /**
  988. * 处理SVG文件上传
  989. * @param {Event} event - 文件上传事件
  990. */
  991. uploadSvgFile(event) {
  992. if (event.target.files && event.target.files.length > 0) {
  993. const file = event.target.files[0];
  994. if (file.type === 'image/svg+xml' || file.name.toLowerCase().endsWith('.svg')) {
  995. this.processSvgFile(file);
  996. } else {
  997. this.errorMsg = '请选择SVG格式的文件';
  998. }
  999. // 清空文件输入,确保可以重复上传相同文件
  1000. event.target.value = '';
  1001. }
  1002. },
  1003. /**
  1004. * 处理图片文件上传
  1005. * @param {Event} event - 文件上传事件
  1006. */
  1007. uploadImageFile(event) {
  1008. if (event.target.files && event.target.files.length > 0) {
  1009. const file = event.target.files[0];
  1010. if (file.type.startsWith('image/') && file.type !== 'image/svg+xml') {
  1011. this.processImageFile(file);
  1012. } else {
  1013. this.imgError = '请选择非SVG格式的图片文件';
  1014. }
  1015. // 清空文件输入,确保可以重复上传相同文件
  1016. event.target.value = '';
  1017. }
  1018. },
  1019. /**
  1020. * 更新SVG结果信息
  1021. * @param {string} svg - 生成的SVG内容
  1022. */
  1023. updateSvgResultInfo(svg) {
  1024. if (!svg) return;
  1025. try {
  1026. // 计算SVG大小
  1027. const svgSize = new Blob([svg]).size;
  1028. const formattedSvgSize = this.formatFileSize(svgSize);
  1029. // 获取原始图片大小(如果有)
  1030. let origImgSize = 0;
  1031. if (this.originalImgSize) {
  1032. origImgSize = this.originalImgSize;
  1033. } else if (this.originalImgBlob) {
  1034. origImgSize = this.originalImgBlob.size;
  1035. }
  1036. // 计算大小比较
  1037. let sizeComparison = '';
  1038. if (origImgSize > 0) {
  1039. const sizeRatio = (svgSize / origImgSize) * 100;
  1040. if (sizeRatio < 100) {
  1041. sizeComparison = `SVG比原图小 ${(100 - sizeRatio).toFixed(1)}%`;
  1042. } else if (sizeRatio > 100) {
  1043. sizeComparison = `SVG比原图大 ${(sizeRatio - 100).toFixed(1)}%`;
  1044. } else {
  1045. sizeComparison = '文件大小相同';
  1046. }
  1047. }
  1048. // 从SVG中提取宽度和高度
  1049. let width = 0;
  1050. let height = 0;
  1051. // 提取width和height属性
  1052. const widthMatch = svg.match(/width="([^"]+)"/);
  1053. const heightMatch = svg.match(/height="([^"]+)"/);
  1054. if (widthMatch && heightMatch) {
  1055. width = parseInt(widthMatch[1], 10);
  1056. height = parseInt(heightMatch[1], 10);
  1057. } else {
  1058. // 尝试从viewBox提取尺寸
  1059. const viewBoxMatch = svg.match(/viewBox="([^"]+)"/);
  1060. if (viewBoxMatch) {
  1061. const viewBoxParts = viewBoxMatch[1].split(/\s+/);
  1062. if (viewBoxParts.length === 4) {
  1063. width = parseInt(viewBoxParts[2], 10);
  1064. height = parseInt(viewBoxParts[3], 10);
  1065. }
  1066. }
  1067. }
  1068. // 更新SVG信息
  1069. this.svgInfo = {
  1070. size: formattedSvgSize,
  1071. originalSize: this.formatFileSize(origImgSize),
  1072. sizeComparison: sizeComparison,
  1073. dimensions: width && height ? `${width} × ${height}` : '未知'
  1074. };
  1075. } catch (e) {
  1076. console.error('更新SVG信息出错:', e);
  1077. this.svgInfo = {
  1078. size: this.formatFileSize(new Blob([svg]).size),
  1079. dimensions: '解析出错'
  1080. };
  1081. }
  1082. },
  1083. /**
  1084. * 打开打赏弹窗
  1085. */
  1086. openDonateModal: function (event) {
  1087. event.preventDefault();
  1088. event.stopPropagation();
  1089. chrome.runtime.sendMessage({
  1090. type: 'fh-dynamic-any-thing',
  1091. thing: 'open-donate-modal',
  1092. params: {
  1093. toolName: 'svg-converter'
  1094. }
  1095. });
  1096. },
  1097. /**
  1098. * 打开工具市场
  1099. */
  1100. openOptionsPage: function (event) {
  1101. event.preventDefault();
  1102. event.stopPropagation();
  1103. chrome.runtime.openOptionsPage();
  1104. }
  1105. }
  1106. });