Browse Source

add new tool:svg convert to images

zxlie 6 months ago
parent
commit
768c563c0c

+ 1 - 1
.cursorrules

@@ -11,7 +11,6 @@
 - static/ 目录包含静态资源
 
 # 编码规范
-- 使用ES6+语法
 - 模块化开发,每个功能保持独立
 - 遵循Chrome Extension V3的最佳实践
 - 代码需要清晰的注释和文档
@@ -23,6 +22,7 @@
 - 新增模块需要在manifest.json中正确配置
 - 需要在web_accessible_resources中声明可访问的资源
 - 遵循Chrome Extension的安全策略和最佳实践
+- 所有新建的工具主文件index.html,都需要检查一下是否增加了顶部导航栏,导航栏的样式需要和旧的工具保持一致(具体参考options/index.html中关于导航部分的实现:左侧是图标和工具名称,右侧是打赏按钮)
 
 # 注意事项
 - 权限申请需要最小化原则

+ 8 - 0
apps/background/tools.js

@@ -248,6 +248,14 @@ let toolMap = {
             text: '图表制作工具'
         }]
     },
+    'svg-converter': {
+        name: 'SVG转为图片',
+        tips: '支持SVG文件转换为PNG、JPG、WEBP等格式,可自定义输出尺寸,支持文件拖放和URL导入',
+        menuConfig: [{
+            icon: '⇲',
+            text: 'SVG转图片工具'
+        }]
+    },
 };
 
 export default toolMap;

+ 2 - 2
apps/options/index.html

@@ -140,10 +140,10 @@
                 </div>
                 <div class="settings-body">
                     <div class="donate-content">
-                        <p class="donate-desc">很高兴这个插件能帮助到大家,也感谢大家对这个产品的认可,我尽量抽时间保持更新😄</p>
+                        <p class="donate-desc">{{donate.text}}我会尽量抽时间保持更新😄</p>
                         <div class="donate-qrcode">
                             <img :src="donate.image" alt="微信打赏二维码" class="donate-image">
-                            <p class="donate-text">{{donate.text}}</p>
+                            <p class="donate-text">微信打赏,鼓励升级!</p>
                         </div>
                     </div>
                 </div>

+ 72 - 10
apps/options/market.js

@@ -6,7 +6,7 @@ import Settings from './settings.js';
 const TOOL_CATEGORIES = [
     { key: 'dev', name: '开发工具类', tools: ['json-format', 'json-diff', 'code-beautify', 'code-compress', 'postman', 'websocket', 'regexp'] },
     { key: 'encode', name: '编解码转换类', tools: ['en-decode', 'trans-radix', 'timestamp', 'trans-color'] },
-    { key: 'image', name: '图像处理类', tools: ['qr-code', 'image-base64', 'screenshot', 'color-picker'] },
+    { key: 'image', name: '图像处理类', tools: ['qr-code', 'image-base64', 'svg-converter', 'chart-maker' ,'screenshot', 'color-picker'] },
     { key: 'productivity', name: '效率工具类', tools: ['aiagent', 'sticky-notes', 'html2markdown', 'page-monkey'] },
     { key: 'calculator', name: '计算工具类', tools: ['crontab', 'loan-rate', 'password'] },
     { key: 'other', name: '其他工具', tools: [] }
@@ -20,7 +20,7 @@ new Vue({
         searchKey: '',
         currentCategory: '',
         sortType: 'default',
-        viewMode: 'grid', // 默认网格视图
+        viewMode: 'list', // 默认网格视图
         categories: TOOL_CATEGORIES,
         favorites: new Set(),
         recentUsed: [],
@@ -46,7 +46,7 @@ new Vue({
         // 打赏相关
         showDonateModal: false,
         donate: {
-            text: '微信打赏!鼓励升级!',
+            text: '感谢你对FeHelper的认可和支持!',
             image: './donate.jpeg'
         },
 
@@ -72,6 +72,9 @@ new Vue({
         this.checkBrowserType();
         // 检查版本更新
         this.checkVersionUpdate();
+        
+        // 检查URL中是否有donate_from参数
+        this.checkDonateParam();
     },
 
     computed: {
@@ -716,8 +719,22 @@ new Vue({
                         if(pt > 100) {
                             clearInterval(ptInterval);
                             elProgress.textContent = ``;
+                            
+                            // 在进度条完成后显示安装成功的通知
+                            this.showInPageNotification({
+                                message: `${this.originalTools[toolKey].name} 安装成功!`,
+                                type: 'success',
+                                duration: 3000
+                            });
                         }
                     }, 100);
+                } else {
+                    // 如果没有进度条元素,直接显示通知
+                    this.showInPageNotification({
+                        message: `${this.originalTools[toolKey].name} 安装成功!`,
+                        type: 'success',
+                        duration: 3000
+                    });
                 }
                 
                 // 更新原始数据和当前活动数据
@@ -745,13 +762,6 @@ new Vue({
                     showTips: true
                 });
                 
-                // 显示安装成功的通知
-                this.showInPageNotification({
-                    message: `${this.originalTools[toolKey].name} 安装成功!`,
-                    type: 'success',
-                    duration: 3000
-                });
-                
             } catch (error) {
                 console.error('安装工具失败:', error);
                 
@@ -1143,6 +1153,58 @@ new Vue({
                 }, 1000);
             }
         },
+
+        // 检查URL中的donate_from参数并显示打赏弹窗
+        checkDonateParam() {
+            try {
+                const urlParams = new URLSearchParams(window.location.search);
+                const donateFrom = urlParams.get('donate_from');
+                
+                if (donateFrom) {
+                    console.log('检测到打赏来源参数:', donateFrom);
+                    
+                    // 记录打赏来源
+                    chrome.storage.local.set({
+                        'fehelper_donate_from': donateFrom,
+                        'fehelper_donate_time': Date.now()
+                    });
+                    
+                    // 等待工具数据加载完成
+                    this.$nextTick(() => {
+                        // 在所有工具中查找匹配项
+                        let matchedTool = null;
+                        
+                        // 首先尝试直接匹配工具key
+                        if (this.originalTools && this.originalTools[donateFrom]) {
+                            matchedTool = this.originalTools[donateFrom];
+                        } else if (this.originalTools) {
+                            // 如果没有直接匹配,尝试在所有工具中查找部分匹配
+                            for (const [key, tool] of Object.entries(this.originalTools)) {
+                                if (key.includes(donateFrom) || donateFrom.includes(key) ||
+                                    (tool.name && tool.name.includes(donateFrom)) || 
+                                    (donateFrom && donateFrom.includes(tool.name))) {
+                                    matchedTool = tool;
+                                    break;
+                                }
+                            }
+                        }
+                        
+                        // 更新打赏文案
+                        if (matchedTool) {
+                            this.donate.text = `看起来【${matchedTool.name}】工具帮助到了你,感谢你的认可!`;
+                        } else {
+                            // 没有匹配到特定工具,使用通用文案
+                            this.donate.text = `感谢你对FeHelper的认可和支持!`;
+                        }
+                        
+                        // 显示打赏弹窗
+                        this.showDonateModal = true;
+                    });
+                }
+            } catch (error) {
+                console.error('处理打赏参数时出错:', error);
+            }
+        },
     },
 
     watch: {

+ 686 - 0
apps/svg-converter/index.css

@@ -0,0 +1,686 @@
+@import url("../static/css/bootstrap.min.css");
+
+/* 基础样式 */
+:root {
+    --primary-color: #4285f4;
+    --hover-color: #3367d6;
+    --error-color: #f44336;
+    --success-color: #4caf50;
+    --panel-bg: #f5f5f5;
+    --border-color: #ddd;
+}
+
+body {
+    font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
+    margin: 0;
+    padding: 0;
+    color: #333;
+    background-color: #f8f9fa;
+}
+
+#pageContainer {
+    width: 100%;
+    min-height: 100vh;
+}
+
+/* 导航栏样式 */
+.fe-mod-head {
+    background-color: #3c4043;
+    color: white;
+    padding: 0;
+    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.mod-head-inner {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 2px 10px;
+    max-width: 1400px;
+    margin: 0 auto;
+}
+
+.mod-head-title {
+    display: flex;
+    align-items: center;
+}
+
+/* FeHelper品牌样式 */
+.navbar-brand {
+    display: flex;
+    align-items: center;
+}
+
+.navbar-brand img {
+    width: 24px;
+    height: 24px;
+    margin-right: 8px;
+}
+
+.brand-text {
+    font-size: 18px;
+    font-weight: bold;
+    color: #fff;
+    margin-right: 10px;
+}
+
+.brand-subtitle {
+    font-size: 16px;
+    color: #f0f0f0;
+    font-weight: normal;
+    border-left: 1px solid rgba(255, 255, 255, 0.3);
+    padding-left: 10px;
+}
+
+/* 打赏按钮样式 */
+.donate-link {
+    display: flex;
+    align-items: center;
+    color: white;
+    text-decoration: none;
+    padding: 6px 12px;
+    background-color: #ea4335;
+    border-radius: 4px;
+    transition: background-color 0.3s;
+    margin-left: 10px;
+}
+
+.donate-link:hover {
+    background-color: #d62516;
+    text-decoration: none;
+    color: white;
+}
+
+.nav-icon {
+    margin-right: 5px;
+    font-style: normal;
+    display: inline-block;
+}
+
+.mod-head-title h1 {
+    font-size: 1.5rem;
+    margin: 0;
+    padding: 0;
+    font-weight: 500;
+}
+
+.back-link {
+    display: inline-block;
+    width: 24px;
+    height: 24px;
+    margin-right: 12px;
+    background: url("../static/img/back-arrow.png") no-repeat center;
+    background-size: contain;
+    opacity: 0.8;
+    transition: opacity 0.3s;
+}
+
+.back-link:hover {
+    opacity: 1;
+}
+
+.mod-head-actions {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+}
+
+.mod-head-btn {
+    padding: 8px 15px;
+    background-color: #4285f4;
+    color: white;
+    border: none;
+    border-radius: 4px;
+    cursor: pointer;
+    font-size: 14px;
+    transition: background-color 0.3s;
+    text-decoration: none;
+    display: inline-flex;
+    align-items: center;
+}
+
+.mod-head-btn:hover {
+    background-color: #3367d6;
+    color: white;
+    text-decoration: none;
+}
+
+.mod-head-btn.donate {
+    background-color: #ea4335;
+}
+
+.mod-head-btn.donate:hover {
+    background-color: #d62516;
+}
+
+/* 工具栏样式 */
+.mod-toolbar {
+    background: #fff;
+    padding: 10px 20px;
+    border-bottom: 1px solid var(--border-color);
+}
+
+.toolbar-inner {
+    display: flex;
+    justify-content: space-between;
+}
+
+.tool-group {
+    display: flex;
+    gap: 10px;
+}
+
+/* 主体内容样式 */
+.mod-main {
+    padding: 20px;
+    max-width: 1400px;
+    margin: 0 auto;
+}
+
+.main-inner {
+    background: #fff;
+    border-radius: 4px;
+    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+    padding: 20px;
+}
+
+/* 表格样式 */
+.mod-table {
+    width: 100%;
+    border-collapse: separate;
+    border-spacing: 15px;
+}
+
+.mod-table td {
+    vertical-align: top;
+    width: 30%;
+}
+
+.mod-table td.converter-col {
+    width: 10%;
+}
+
+/* 面板样式 */
+.x-panel {
+    border: 1px solid var(--border-color);
+    border-radius: 4px;
+    background: var(--panel-bg);
+    margin-bottom: 15px;
+    overflow: hidden;
+}
+
+.panel-title {
+    background: #e9ecef;
+    padding: 10px 15px;
+    font-weight: bold;
+    border-bottom: 1px solid var(--border-color);
+}
+
+.panel-content {
+    padding: 15px;
+    min-height: 200px;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+}
+
+/* 上传区域样式 */
+.upload-zone {
+    text-align: center;
+    min-height: 180px;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+}
+
+.upload-zone p {
+    color: #666;
+    line-height: 1.6;
+}
+
+.btn-upload {
+    display: inline-block;
+    padding: 6px 12px;
+    margin: 5px;
+    background-color: var(--primary-color);
+    color: white;
+    border-radius: 4px;
+    cursor: pointer;
+    transition: background-color 0.3s;
+}
+
+.btn-upload:hover {
+    background-color: var(--hover-color);
+}
+
+.load-url, .paste-content {
+    color: var(--primary-color);
+    text-decoration: underline;
+    cursor: pointer;
+    margin: 0 10px;
+}
+
+/* 图片与SVG预览样式 */
+.svg-preview, .img-preview {
+    max-width: 100%;
+    max-height: 300px;
+    margin-left: auto;
+    margin-right: auto;
+    display: block;
+}
+
+.preview-container {
+    position: relative;
+    width: 100%;
+    text-align: center;
+}
+
+.preview-actions {
+    margin-top: 10px;
+    display: flex;
+    justify-content: center;
+}
+
+.btn-change {
+    background-color: #5a6268;
+    padding: 5px 10px;
+    font-size: 14px;
+}
+
+.btn-change:hover {
+    background-color: #444a4e;
+}
+
+.svg-result {
+    width: 100%;
+    height: 300px;
+    overflow: auto;
+    background-color: transparent;
+}
+
+.svg-result svg {
+    max-width: 100%;
+    max-height: 280px;
+    display: block;
+    margin: 0 auto;
+}
+
+/* 面板有图像时移除背景图 */
+.x-panel.has-image {
+    background-image: none;
+}
+
+/* 转换器列样式 */
+.converter-box {
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+    height: 100%;
+    min-height: 200px;
+}
+
+.converter-inputs {
+    width: 100%;
+    margin-bottom: 20px;
+}
+
+.input-group {
+    margin-bottom: 10px;
+    display: flex;
+    align-items: center;
+}
+
+.input-group label {
+    width: 100px;
+    text-align: right;
+    padding-right: 10px;
+}
+
+.input-group input, .input-group select {
+    flex: 1;
+    padding: 6px;
+    border: 1px solid var(--border-color);
+    border-radius: 4px;
+}
+
+/* 转换按钮样式 */
+.btn-convert {
+    display: block;
+    width: 100%;
+    padding: 10px 15px;
+    background-color: var(--primary-color);
+    color: white;
+    font-weight: bold;
+    border: none;
+    border-radius: 4px;
+    cursor: pointer;
+    transition: background-color 0.3s;
+    margin-top: 15px;
+}
+
+.btn-convert:hover {
+    background-color: var(--hover-color);
+}
+
+/* 操作按钮容器 */
+.action-buttons {
+    display: flex;
+    gap: 10px;
+    margin-top: 15px;
+}
+
+.action-buttons .btn-convert {
+    flex: 3;
+    margin-top: 0;
+}
+
+.action-buttons .btn-reset {
+    flex: 2;
+    background-color: #e0e0e0;
+    color: #333;
+    font-weight: bold;
+    padding: 10px 15px;
+    border: none;
+    border-radius: 4px;
+    cursor: pointer;
+    transition: background-color 0.3s;
+}
+
+.action-buttons .btn-reset:hover {
+    background-color: #c0c0c0;
+}
+
+/* 一般按钮样式 */
+.btn {
+    padding: 8px 15px;
+    background-color: var(--primary-color);
+    color: white;
+    font-weight: 500;
+    border: none;
+    border-radius: 4px;
+    cursor: pointer;
+    text-decoration: none;
+    transition: background-color 0.3s;
+}
+
+.btn:hover {
+    background-color: var(--hover-color);
+}
+
+.download-btn {
+    text-align: center;
+    margin-top: 15px;
+}
+
+/* 结果区域样式 */
+.result-zone {
+    text-align: center;
+    min-height: 180px;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+}
+
+.result-zone p {
+    color: #666;
+}
+
+/* 错误信息样式 */
+.error-msg {
+    background-color: #ffebee;
+    color: #d32f2f;
+    padding: 10px 15px;
+    border-radius: 4px;
+    margin: 10px 0;
+    border-left: 4px solid #d32f2f;
+    font-size: 14px;
+    line-height: 1.5;
+}
+
+/* 警告信息样式 */
+.warning-msg {
+    background-color: #fff8e1;
+    color: #f57c00;
+    padding: 10px 15px;
+    border-radius: 4px;
+    margin: 10px 0;
+    border-left: 4px solid #f57c00;
+    font-size: 14px;
+    line-height: 1.5;
+}
+
+.error-msg ul, .warning-msg ul {
+    margin: 5px 0 0 20px;
+    padding: 0;
+}
+
+.error-suggestion, .warning-suggestion {
+    font-style: italic;
+    color: #666;
+    margin-top: 5px;
+}
+
+/* 文件信息面板样式 */
+.file-info-panel {
+    border: 1px solid var(--border-color);
+    border-radius: 4px;
+    padding: 15px;
+    margin-bottom: 15px;
+    background-color: #fff;
+}
+
+.file-info-panel h3 {
+    margin-top: 0;
+    margin-bottom: 10px;
+    font-size: 16px;
+    color: #333;
+    border-bottom: 1px solid #eee;
+    padding-bottom: 8px;
+}
+
+.info-table {
+    width: 100%;
+    border-collapse: collapse;
+}
+
+.info-table td {
+    padding: 6px 0;
+    border: none;
+}
+
+.info-label {
+    font-weight: bold;
+    width: 100px;
+    color: #666;
+}
+
+.size-increase {
+    color: var(--error-color);
+}
+
+.size-decrease {
+    color: var(--success-color);
+}
+
+/* 自适应样式 */
+@media (max-width: 992px) {
+    .mod-table {
+        display: block;
+    }
+    
+    .mod-table tr {
+        display: flex;
+        flex-direction: column;
+    }
+    
+    .mod-table td {
+        width: 100%;
+        display: block;
+    }
+    
+    .converter-box {
+        flex-direction: row;
+        min-height: auto;
+        padding: 15px 0;
+    }
+    
+    .converter-inputs {
+        margin-bottom: 0;
+        margin-right: 15px;
+    }
+}
+
+/* 加载指示器样式 */
+.loading-overlay {
+    position: fixed;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    background: rgba(0, 0, 0, 0.5);
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    z-index: 1000;
+}
+
+.loading-container {
+    background: white;
+    padding: 20px;
+    border-radius: 4px;
+    text-align: center;
+    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
+    max-width: 80%;
+    width: 300px;
+}
+
+.loading-message {
+    margin: 10px 0;
+    color: #333;
+    font-size: 16px;
+}
+
+.loading-spinner {
+    border: 3px solid #f3f3f3;
+    border-top: 3px solid #4285f4;
+    border-radius: 50%;
+    width: 30px;
+    height: 30px;
+    margin: 0 auto;
+    animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+    0% { transform: rotate(0deg); }
+    100% { transform: rotate(360deg); }
+}
+
+.progress-bar {
+    height: 10px;
+    background: #f3f3f3;
+    border-radius: 5px;
+    margin-top: 10px;
+    overflow: hidden;
+}
+
+.progress-fill {
+    height: 100%;
+    background: #4285f4;
+    transition: width 0.3s;
+}
+
+/* 文件大小警告样式 */
+.file-size-warning {
+    position: fixed;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%);
+    background: white;
+    padding: 20px;
+    border-radius: 4px;
+    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
+    z-index: 1100;
+    max-width: 90%;
+    width: 400px;
+}
+
+.file-size-warning p:first-child {
+    font-weight: bold;
+    font-size: 18px;
+    margin-top: 0;
+}
+
+.warning-actions {
+    display: flex;
+    justify-content: flex-end;
+    margin-top: 20px;
+}
+
+.warning-actions .btn {
+    margin-left: 10px;
+}
+
+.btn-cancel {
+    background-color: #f5f5f5;
+    color: #333;
+}
+
+.btn-continue {
+    background-color: #ff5722;
+    color: white;
+}
+
+/* 文件大小警告对话框样式 */
+.warning-dialog {
+    position: fixed;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%);
+    background-color: #fff;
+    border-radius: 8px;
+    box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
+    padding: 20px;
+    z-index: 9999;
+    min-width: 300px;
+    max-width: 450px;
+}
+
+.warning-content {
+    margin-bottom: 20px;
+    font-size: 14px;
+    line-height: 1.5;
+    color: #333;
+}
+
+.warning-actions {
+    display: flex;
+    justify-content: flex-end;
+    gap: 10px;
+}
+
+.btn-cancel {
+    background-color: #f0f0f0;
+    color: #333;
+    border: 1px solid #ddd;
+}
+
+.btn-continue {
+    background-color: #F44336;
+    color: white;
+    border: 1px solid #D32F2F;
+}
+
+.btn {
+    padding: 8px 16px;
+    border-radius: 4px;
+    cursor: pointer;
+    font-size: 14px;
+    transition: background-color 0.2s;
+}
+
+.btn:hover {
+    opacity: 0.9;
+} 

+ 192 - 0
apps/svg-converter/index.html

@@ -0,0 +1,192 @@
+<!DOCTYPE HTML>
+<html lang="zh-CN">
+    <head>
+        <title>SVG转图片工具</title>
+        <meta charset="UTF-8">
+        <meta http-equiv="X-UA-Compatible" content="IE=edge">
+        <meta name="viewport" content="width=device-width, initial-scale=1.0">
+        <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:;">
+        <link rel="shortcut icon" href="../static/img/favicon.ico">
+        <link rel="stylesheet" href="index.css" />
+        <script type="text/javascript" src="../static/vendor/evalCore.min.js"></script>
+        <script type="text/javascript" src="../static/vendor/vue/vue.js"></script>
+        <style type="text/css">
+            <!--避免页面闪烁-->
+            [v-cloak] {
+                display: none;
+            }
+        </style>
+    </head>
+    <body>
+        <div id="pageContainer" v-cloak>
+            <div class="fe-mod-head">
+                <div class="mod-head-inner">
+                    <div class="mod-head-title">
+                        <div class="navbar-brand">
+                            <img src="../static/img/fe-16.png" alt="fehelper"/> 
+                            <span class="brand-text">FeHelper</span>
+                            <span class="brand-subtitle">SVG转图片工具</span>
+                        </div>
+                    </div>
+                    <div class="mod-head-actions">
+                        <a href="#" @click="openDonateModal" class="nav-item donate-link" v-if="typeof openDonateModal === 'function'">
+                            <i class="nav-icon">❤</i>
+                            <span>打赏鼓励</span>
+                        </a>
+                    </div>
+                </div>
+            </div>
+            <div class="fe-mod-bd">
+                <div class="mod-main">
+                    <div class="main-inner">
+                        <!-- SVG转图片界面 -->
+                        <div class="mod-svg-converter">
+                            <table class="mod-table">
+                                <tr>
+                                    <td>
+                                        <div ref="panelBox" class="x-panel" :class="{'has-image': svgPreviewSrc}">
+                                            <div class="panel-title">SVG源文件</div>
+                                            <div class="panel-content">
+                                                <div class="upload-zone" v-if="!svgPreviewSrc">
+                                                    <p>
+                                                        将SVG文件拖放到此处,或
+                                                        <input type="file" accept=".svg" @change="uploadSvgFile" id="svgFile" ref="svgFile" style="display: none;">
+                                                        <label for="svgFile" class="btn-upload">点击上传</label> <br/>
+                                                        <a href="javascript:;" @click="loadSvgFromUrl" class="load-url">从URL加载</a>
+                                                        <a href="javascript:;" id="pasteSvg" @click="pasteSvg" class="paste-content">粘贴</a>
+                                                    </p>
+                                                </div>
+                                                <div v-if="svgPreviewSrc">
+                                                    <div class="upload-zone">
+                                                        <div class="preview-container">
+                                                            <img :src="svgPreviewSrc" id="svgPreview" class="svg-preview">
+                                                        </div>
+                                                    </div>
+                                                    <div class="preview-actions">
+                                                        <button type="button" class="btn btn-change" @click="resetSvgFile">更换文件</button>
+                                                    </div>
+                                                </div>
+                                            </div>
+                                        </div>
+                                        <!-- 添加文件信息面板 -->
+                                        <div v-if="svgPreviewSrc" class="file-info-panel">
+                                            <h3>SVG文件信息</h3>
+                                            <table class="info-table">
+                                                <tr>
+                                                    <td class="info-label">尺寸:</td>
+                                                    <td>{{svgInfo.dimensions}}</td>
+                                                </tr>
+                                                <tr>
+                                                    <td class="info-label">文件大小:</td>
+                                                    <td>{{svgInfo.fileSize}}</td>
+                                                </tr>
+                                                <tr>
+                                                    <td class="info-label">文件类型:</td>
+                                                    <td>{{svgInfo.fileType}}</td>
+                                                </tr>
+                                            </table>
+                                        </div>
+                                    </td>
+                                    <td class="converter-col">
+                                        <div class="converter-box">
+                                            <div class="converter-inputs">
+                                                <div class="input-group">
+                                                    <label>输出格式:</label>
+                                                    <select v-model="outputFormat">
+                                                        <option value="png">PNG</option>
+                                                        <option value="jpg">JPG</option>
+                                                        <option value="webp">WEBP</option>
+                                                    </select>
+                                                </div>
+                                                <div class="input-group">
+                                                    <label>宽度(px):</label>
+                                                    <input type="number" v-model.number="imgWidth" placeholder="自动" min="0">
+                                                </div>
+                                                <div class="input-group">
+                                                    <label>高度(px):</label>
+                                                    <input type="number" v-model.number="imgHeight" placeholder="自动" min="0">
+                                                </div>
+                                            </div>
+                                            <div class="action-buttons">
+                                                <button type="button" class="btn btn-convert" @click="convertSvg">转换 &gt;</button>
+                                                <button type="button" class="btn btn-reset" @click="resetState">重置</button>
+                                            </div>
+
+                                            <div v-if="errorMsg" class="error-msg">{{errorMsg}}</div>
+                                        </div>
+                                    </td>
+                                    <td>
+                                        <div class="x-panel" :class="{'has-image': imgPreviewSrc}">
+                                            <div class="panel-title">转换后图片</div>
+                                            <div class="panel-content">
+                                                <div class="result-zone">
+                                                    <p v-if="!imgPreviewSrc">转换后的图片将显示在这里</p>
+                                                    <img v-if="imgPreviewSrc" :src="imgPreviewSrc" id="imgPreview" class="img-preview">
+                                                </div>
+                                                <div v-if="imgPreviewSrc" class="download-btn">
+                                                    <button type="button" class="btn" @click="downloadImage">下载图片</button>
+                                                </div>
+                                            </div>
+                                        </div>
+                                        <!-- 添加文件信息面板 -->
+                                        <div v-if="imgPreviewSrc" class="file-info-panel">
+                                            <h3>图片文件信息</h3>
+                                            <table class="info-table">
+                                                <tr>
+                                                    <td class="info-label">尺寸:</td>
+                                                    <td>{{imgInfo.dimensions}}</td>
+                                                </tr>
+                                                <tr>
+                                                    <td class="info-label">文件大小:</td>
+                                                    <td>{{imgInfo.fileSize}}</td>
+                                                </tr>
+                                                <tr>
+                                                    <td class="info-label">文件类型:</td>
+                                                    <td>{{imgInfo.fileType}}</td>
+                                                </tr>
+                                                <tr v-if="imgInfo.comparison.ratio > 0">
+                                                    <td class="info-label">大小比较:</td>
+                                                    <td>
+                                                        <span :class="{'size-increase': imgInfo.comparison.isIncrease, 'size-decrease': !imgInfo.comparison.isIncrease}">
+                                                            {{imgInfo.comparison.isIncrease ? '增加' : '减少'}} {{imgInfo.comparison.ratio}}%
+                                                        </span>
+                                                    </td>
+                                                </tr>
+                                            </table>
+                                        </div>
+                                    </td>
+                                </tr>
+                            </table>
+                        </div>
+                    </div>
+                </div>
+            </div>
+            
+            <!-- 添加加载状态指示器 -->
+            <div class="loading-overlay" v-if="isProcessing">
+                <div class="loading-container">
+                    <div class="loading-spinner"></div>
+                    <div class="loading-message">{{ processingMessage }}</div>
+                    <div class="progress-bar" v-if="processingProgress > 0">
+                        <div class="progress-fill" :style="{width: processingProgress + '%'}"></div>
+                    </div>
+                </div>
+            </div>
+        </div>
+
+        <!-- 添加一个静态的加载指示器作为备份 -->
+        <div id="static-loading" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.7); z-index: 9999; justify-content: center; align-items: center;">
+            <div style="background-color: white; padding: 20px; border-radius: 5px; text-align: center; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);">
+                <div style="width: 40px; height: 40px; border: 3px solid #f3f3f3; border-radius: 50%; border-top: 3px solid #3498db; animation: spin 1s linear infinite; margin: 0 auto;"></div>
+                <div style="margin-top: 15px; font-size: 16px;" id="static-loading-message">正在处理,请稍候...</div>
+            </div>
+        </div>
+
+        <!-- 引入工具模块 -->
+        <script src="../static/vendor/jquery/jquery-3.3.1.min.js"></script>
+        <script src="modules/file-utils.js"></script>
+        <script src="modules/svg-to-image.js"></script>
+        <script type="text/javascript" src="index.js"></script>
+        <script src="modules/loading-helper.js"></script>
+    </body>
+</html> 

+ 1261 - 0
apps/svg-converter/index.js

@@ -0,0 +1,1261 @@
+/**
+ * FeHelper SVG转图片工具
+ * 实现SVG到图片格式的转换
+ */
+
+new Vue({
+    el: '#pageContainer',
+    data: {
+        // 工具类型
+        toolName: 'SVG转图片',
+        
+        // SVG转图片相关
+        previewSrc: '',
+        convertedSrc: '',
+        outputFormat: 'png',
+        outputWidth: 0,
+        outputHeight: 0,
+        imgWidth: 128,
+        imgHeight: 128,
+        originalWidth: 0,
+        originalHeight: 0,
+        
+        // 通用
+        error: '',
+        
+        // SVG转图片相关数据
+        svgSource: '',
+        svgPreviewSrc: '',
+        svgFile: null,
+        svgInfo: {
+            dimensions: '0 x 0',
+            fileSize: '0 KB',
+            fileType: 'SVG'
+        },
+        
+        // 输出图片相关数据
+        imgSource: '',
+        imgPreviewSrc: '',
+        imgInfo: {
+            dimensions: '0 x 0',
+            fileSize: '0 KB',
+            fileType: 'PNG',
+            comparison: {
+                ratio: 0,
+                isIncrease: false
+            }
+        },
+        
+        // 错误信息
+        errorMsg: '',
+        
+        // SVG相关状态
+        svgDimensions: { width: 0, height: 0 },
+        svgFileSize: 0,
+        svgFileName: '',
+        svgError: '',
+        
+        // 图片相关状态
+        imgDimensions: { width: 0, height: 0 },
+        imgFileSize: 0,
+        imgFileName: '',
+        
+        // 转换选项
+        outputWidth: 0,
+        outputHeight: 0,
+        
+        // 转换结果
+        convertedFileSize: 0,
+        
+        // 加载状态
+        isProcessing: false,
+        processingProgress: 0,
+        processingMessage: '',
+        
+        // 文件大小比较
+        sizeComparison: {
+            difference: 0,
+            percentage: 0,
+            isIncrease: false
+        }
+    },
+
+    computed: {
+        // 是否展示SVG预览
+        showSvgPreview() {
+            return this.svgSource && !this.svgError;
+        },
+        
+        // 是否展示图片预览
+        showImgPreview() {
+            return this.imgSource && !this.imgError;
+        },
+        
+        // 是否展示转换结果
+        showResult() {
+            return this.convertedSrc && !this.svgError && !this.imgError;
+        },
+        
+        // 是否可以转换
+        canConvert() {
+            return this.svgSource;
+        },
+        
+        // 下载文件名称
+        downloadFileName() {
+            const baseName = this.svgFileName.replace(/\.svg$/i, '') || 'converted';
+            return `${baseName}.${this.outputFormat}`;
+        },
+        
+        // 格式化后的SVG文件大小
+        formattedSvgFileSize() {
+            return this.formatFileSize(this.svgFileSize);
+        },
+        
+        // 格式化后的图片文件大小
+        formattedImgFileSize() {
+            return this.formatFileSize(this.imgFileSize);
+        },
+        
+        // 格式化后的转换结果文件大小
+        formattedConvertedFileSize() {
+            return this.formatFileSize(this.convertedFileSize);
+        },
+        
+        // 格式化后的文件大小差异
+        formattedSizeDifference() {
+            return this.formatFileSize(Math.abs(this.sizeComparison.difference));
+        },
+        
+        // 文件大小变化的百分比
+        sizeChangeText() {
+            if (this.sizeComparison.percentage === 0) return '无变化';
+            
+            const sign = this.sizeComparison.isIncrease ? '增加' : '减少';
+            return `${sign} ${this.sizeComparison.percentage}%`;
+        },
+        
+        // 文件大小变化的CSS类
+        sizeChangeClass() {
+            if (this.sizeComparison.percentage === 0) return 'size-same';
+            return this.sizeComparison.isIncrease ? 'size-increase' : 'size-decrease';
+        }
+    },
+
+    mounted: function() {
+        // 监听paste事件
+        document.addEventListener('paste', this.pasteSvg, false);
+
+        // 初始化拖放功能
+        this.initDragAndDrop();
+        
+        // 确保文件上传元素可用 - 使用多层保障确保DOM完全加载
+        // 1. 首先使用Vue的nextTick
+        this.$nextTick(() => {
+            // 2. 然后添加一个短暂的延时确保DOM完全渲染
+            setTimeout(() => {
+                this.ensureFileInputsAvailable();
+            }, 300);
+        });
+        
+        // 3. 添加一个额外的保障,如果页面已完全加载则立即执行,否则等待加载完成
+        if (document.readyState === 'complete') {
+            this.ensureFileInputsAvailable();
+        } else {
+            window.addEventListener('load', () => {
+                this.ensureFileInputsAvailable();
+            });
+        }
+    },
+    
+    watch: {
+        previewSrc: function(newVal) {
+            // 确保DOM元素存在再进行操作
+            if (this.$refs.panelBox) {
+                if (newVal && newVal.length > 0) {
+                    this.$refs.panelBox.classList.add('has-image');
+                } else {
+                    this.$refs.panelBox.classList.remove('has-image');
+                }
+            }
+        },
+        svgPreviewSrc: function(newVal) {
+            // 使用选择器直接获取元素,避免依赖ref
+            const svgPanel = document.querySelector('.mod-svg-converter .x-panel');
+            if (svgPanel) {
+                if (newVal && newVal.length > 0) {
+                    svgPanel.classList.add('has-image');
+                } else {
+                    svgPanel.classList.remove('has-image');
+                }
+            }
+        },
+        imgPreviewSrc: function(newVal) {
+            // 使用选择器直接获取元素
+            const imgPanel = document.querySelector('.mod-svg-converter .result-zone img');
+            if (imgPanel && imgPanel.parentNode) {
+                if (newVal && newVal.length > 0) {
+                    imgPanel.parentNode.classList.add('has-image');
+                } else {
+                    imgPanel.parentNode.classList.remove('has-image');
+                }
+            }
+        }
+    },
+
+    methods: {
+        /**
+         * 重置状态
+         */
+        resetState: function() {
+            this.previewSrc = '';
+            this.convertedSrc = '';
+            this.imgPreviewSrc = '';
+            this.error = '';
+            this.outputWidth = 0;
+            this.outputHeight = 0;
+            this.originalWidth = 0;
+            this.originalHeight = 0;
+            
+            // 移除has-image类
+            if (this.$refs.panelBox) {
+                this.$refs.panelBox.classList.remove('has-image');
+            }
+
+            this.svgSource = '';
+            this.svgPreviewSrc = '';
+            this.svgFile = null;
+            this.imgSource = '';
+            this.imgPreviewSrc = '';
+            this.errorMsg = '';
+            this.svgInfo = {
+                dimensions: '0 x 0',
+                fileSize: '0 KB',
+                fileType: 'SVG'
+            };
+            this.imgInfo = {
+                dimensions: '0 x 0',
+                fileSize: '0 KB',
+                fileType: this.outputFormat.toUpperCase(),
+                comparison: {
+                    ratio: 0,
+                    isIncrease: false
+                }
+            };
+        },
+        
+        /**
+         * 确保文件输入元素可用并正确绑定
+         */
+        ensureFileInputsAvailable() {
+            // 使用setTimeout确保DOM已经完全加载
+            setTimeout(() => {
+                // 检查SVG文件上传元素
+                let svgFileInput = document.getElementById('svgFile');
+                if (!svgFileInput) {
+                    // 优先从Vue ref中获取
+                    if (this.$refs.svgFile) {
+                        svgFileInput = this.$refs.svgFile;
+                        console.log('从Vue ref中找到SVG文件上传元素');
+                    } else {
+                        console.warn('SVG文件上传元素不存在,创建新元素');
+                        svgFileInput = document.createElement('input');
+                        svgFileInput.type = 'file';
+                        svgFileInput.id = 'svgFile';
+                        svgFileInput.accept = '.svg';
+                        svgFileInput.style.display = 'none';
+                        document.body.appendChild(svgFileInput);
+                        
+                        // 绑定上传事件
+                        svgFileInput.addEventListener('change', (event) => {
+                            this.uploadSvgFile(event);
+                        });
+                    }
+                }
+            }, 100); // 100ms延迟,确保DOM已完全加载
+        },
+        
+        /**
+         * 重置SVG文件(SVG转图片模式)
+         */
+        resetSvgFile() {
+            try {
+                // 1. 首先尝试使用Vue的refs
+                if (this.$refs.svgFile) {
+                    this.$refs.svgFile.click();
+                    return;
+                }
+                
+                // 2. 然后尝试使用ID查询
+                const fileInput = document.getElementById('svgFile');
+                if (fileInput) {
+                    fileInput.click();
+                    return;
+                }
+                
+                // 3. 如果上述方法都失败,确保文件输入元素可用
+                this.ensureFileInputsAvailable();
+                
+                // 4. 再次尝试
+                const newFileInput = document.getElementById('svgFile');
+                if (newFileInput) {
+                    newFileInput.click();
+                    return;
+                }
+                
+                // 5. 最后的备用方案
+                console.warn('无法找到SVG文件上传元素,使用临时元素');
+                this.createTemporaryFileInput('.svg', this.uploadSvgFile.bind(this));
+            } catch (error) {
+                console.error('SVG文件选择器点击失败:', error);
+                this.createTemporaryFileInput('.svg', this.uploadSvgFile.bind(this));
+            }
+        },
+        
+        /**
+         * 创建临时文件输入元素并触发点击
+         * @param {string} acceptType - 接受的文件类型
+         * @param {Function} changeHandler - 文件变化处理函数
+         */
+        createTemporaryFileInput(acceptType, changeHandler) {
+            const tempInput = document.createElement('input');
+            tempInput.type = 'file';
+            tempInput.accept = acceptType;
+            tempInput.style.display = 'none';
+            
+            tempInput.addEventListener('change', (event) => {
+                if (changeHandler) {
+                    changeHandler(event);
+                }
+                // 使用后移除临时元素
+                document.body.removeChild(tempInput);
+            });
+            
+            document.body.appendChild(tempInput);
+            tempInput.click();
+        },
+        
+        /**
+         * 弹出文件选择对话框 - SVG
+         */
+        upload: function(event) {
+            event.preventDefault();
+            this.$refs.fileBox.click();
+        },
+        
+        /**
+         * 加载SVG文件
+         */
+        loadSvg: function() {
+            if (this.$refs.fileBox.files.length) {
+                const file = this.$refs.fileBox.files[0];
+                this.processSvgFile(file);
+                this.$refs.fileBox.value = '';
+            }
+        },
+        
+        /**
+         * 处理SVG文件
+         */
+        processSvgFile: function(file) {
+            // 检查文件大小
+            const MAX_SVG_SIZE = 5 * 1024 * 1024; // 5MB
+            if (file.size > MAX_SVG_SIZE) {
+                // 显示文件大小警告
+                this.showFileSizeWarning(file, 'svg');
+                return;
+            }
+            
+            // 显示加载状态
+            this.isProcessing = true;
+            this.processingMessage = '正在处理SVG文件...';
+            this.processingProgress = 20;
+            
+            // 使用全局加载函数作为备份
+            if (window.showLoading) {
+                window.showLoading('正在处理SVG文件...');
+            }
+            
+            // 读取文件内容
+            FileUtils.readAsText(file, (svgContent, error) => {
+                if (error) {
+                    this.handleError(error, 'svgUpload');
+                    if (window.hideLoading) window.hideLoading();
+                    return;
+                }
+                
+                try {
+                    // 验证SVG内容
+                    if (!svgContent.includes('<svg') || !svgContent.includes('</svg>')) {
+                        this.handleError('无效的SVG文件,缺少SVG标签', 'svgUpload');
+                        if (window.hideLoading) window.hideLoading();
+                        return;
+                    }
+                    
+                    this.processingProgress = 50;
+                    
+                    // 解析SVG获取尺寸
+                    const parser = new DOMParser();
+                    const svgDoc = parser.parseFromString(svgContent, 'image/svg+xml');
+                    const parserError = svgDoc.querySelector('parsererror');
+                        
+                    if (parserError) {
+                        this.handleError('SVG解析失败,文件可能损坏或格式不正确', 'svgUpload');
+                        if (window.hideLoading) window.hideLoading();
+                        return;
+                    }
+                    
+                    // 提取尺寸信息
+                    const svgElement = svgDoc.documentElement;
+                    let width = svgElement.getAttribute('width');
+                    let height = svgElement.getAttribute('height');
+                    
+                    if (!width || !height || width.includes('%') || height.includes('%')) {
+                        const viewBox = svgElement.getAttribute('viewBox');
+                        if (viewBox) {
+                            const viewBoxValues = viewBox.split(/\s+|,/);
+                            if (viewBoxValues.length >= 4) {
+                                width = parseFloat(viewBoxValues[2]);
+                                height = parseFloat(viewBoxValues[3]);
+                            }
+                        }
+                    } else {
+                        width = parseFloat(width);
+                        height = parseFloat(height);
+                    }
+                    
+                    // 保存SVG信息
+                    this.svgDimensions = {
+                        width: width || 300,
+                        height: height || 150
+                    };
+                    this.svgSource = svgContent;
+                    this.svgFile = file;
+                    this.svgFileName = file.name;
+                    this.svgFileSize = file.size;
+                    
+                    this.processingProgress = 70;
+                    
+                    // 更新SVG信息
+                    this.svgInfo.dimensions = `${this.svgDimensions.width} x ${this.svgDimensions.height}`;
+                    this.svgInfo.fileSize = this.formatFileSize(file.size);
+                    this.svgInfo.fileType = 'SVG';
+                    
+                    // 创建DataURL预览
+                    const reader = new FileReader();
+                    reader.onload = (e) => {
+                        this.svgPreviewSrc = e.target.result;
+                        
+                        // 完成处理
+                        this.processingProgress = 100;
+                        setTimeout(() => {
+                            this.isProcessing = false;
+                            if (window.hideLoading) window.hideLoading();
+                        }, 300);
+                    };
+                    
+                    reader.onerror = () => {
+                        this.handleError('读取SVG文件失败', 'svgUpload');
+                        if (window.hideLoading) window.hideLoading();
+                    };
+                    
+                    reader.readAsDataURL(file);
+                } catch (err) {
+                    this.handleError(err, 'svgUpload');
+                    if (window.hideLoading) window.hideLoading();
+                }
+            });
+        },
+        
+        /**
+         * 粘贴SVG内容
+         * @param {ClipboardEvent} event - 粘贴事件对象
+         */
+        pasteSvg(event) {
+            // 显示加载状态
+            this.isProcessing = true;
+            this.processingMessage = '正在处理粘贴内容...';
+            this.processingProgress = 20;
+            
+            if (event && event.clipboardData) {
+                // 从事件中获取剪贴板数据
+                const items = event.clipboardData.items || {};
+                let hasSvgContent = false;
+                
+                // 处理文本内容,可能是SVG代码或URL
+                for (let i = 0; i < items.length; i++) {
+                    const item = items[i];
+                    
+                    if (item.type === 'text/plain') {
+                        item.getAsString((text) => {
+                            this.processingProgress = 40;
+                            const trimmedText = text.trim();
+                            
+                            // 检查是否是SVG内容
+                            if (trimmedText.startsWith('<svg') || trimmedText.startsWith('<?xml') && trimmedText.includes('<svg')) {
+                                hasSvgContent = true;
+                                this.processSvgText(trimmedText);
+                            } 
+                            // 检查是否是URL
+                            else if (trimmedText.startsWith('http') && 
+                                    (trimmedText.toLowerCase().endsWith('.svg') || 
+                                     trimmedText.toLowerCase().includes('.svg?'))) {
+                                hasSvgContent = true;
+                                this.loadSvgFromUrl();
+                            } else {
+                                this.handleError('剪贴板中没有SVG内容', 'svgUpload');
+                            }
+                        });
+                        break;
+                    }
+                }
+                
+                // 处理SVG图像文件
+                if (!hasSvgContent) {
+                    for (let i = 0; i < items.length; i++) {
+                        const item = items[i];
+                        
+                        if (item.type.indexOf('image/svg+xml') !== -1) {
+                            const file = item.getAsFile();
+                            if (file) {
+                                this.processingProgress = 60;
+                                hasSvgContent = true;
+                                this.uploadSvgFile({ target: { files: [file] } });
+                                break;
+                            }
+                        }
+                    }
+                }
+                
+                // 如果没有找到SVG内容
+                if (!hasSvgContent) {
+                    this.handleError('剪贴板中没有SVG内容或无法访问剪贴板', 'svgUpload');
+                }
+            } else {
+                this.handleError('无法访问剪贴板,请直接选择文件上传', 'svgUpload');
+            }
+        },
+        
+        /**
+         * 处理粘贴的SVG文本内容
+         * @param {string} svgText - SVG文本内容
+         */
+        processSvgText(svgText) {
+            try {
+                this.processingProgress = 60;
+                
+                // 验证SVG内容
+                if (!svgText.includes('<svg') || !svgText.includes('</svg>')) {
+                    this.handleError('无效的SVG内容,缺少SVG标签', 'svgUpload');
+                    return;
+                }
+                
+                // 解析SVG获取尺寸
+                const parser = new DOMParser();
+                const svgDoc = parser.parseFromString(svgText, 'image/svg+xml');
+                const parserError = svgDoc.querySelector('parsererror');
+                    
+                if (parserError) {
+                    this.handleError('SVG解析失败,内容可能损坏或格式不正确', 'svgUpload');
+                    return;
+                }
+                
+                // 创建SVG文件
+                const blob = new Blob([svgText], {type: 'image/svg+xml'});
+                const file = new File([blob], 'pasted.svg', {type: 'image/svg+xml'});
+                
+                // 计算SVG尺寸
+                const svgElement = svgDoc.documentElement;
+                let width = svgElement.getAttribute('width');
+                let height = svgElement.getAttribute('height');
+                
+                if (!width || !height || width.includes('%') || height.includes('%')) {
+                    const viewBox = svgElement.getAttribute('viewBox');
+                    if (viewBox) {
+                        const viewBoxValues = viewBox.split(/\s+|,/);
+                        if (viewBoxValues.length >= 4) {
+                            width = parseFloat(viewBoxValues[2]);
+                            height = parseFloat(viewBoxValues[3]);
+                        }
+                    }
+                } else {
+                    width = parseFloat(width);
+                    height = parseFloat(height);
+                }
+                
+                // 保存SVG信息
+                this.svgDimensions = {
+                    width: width || 300,
+                    height: height || 150
+                };
+                this.svgSource = svgText;
+                this.svgFile = file;
+                this.svgFileName = 'pasted.svg';
+                this.svgFileSize = blob.size;
+                
+                // 更新SVG信息
+                this.svgInfo.dimensions = `${this.svgDimensions.width} x ${this.svgDimensions.height}`;
+                this.svgInfo.fileSize = this.formatFileSize(blob.size);
+                
+                this.processingProgress = 80;
+                
+                // 创建SVG预览
+                const reader = new FileReader();
+                reader.onload = (e) => {
+                    this.svgPreviewSrc = e.target.result;
+                    
+                    // 完成处理
+                    this.processingProgress = 100;
+                    setTimeout(() => {
+                        this.isProcessing = false;
+                    }, 300);
+                };
+                
+                reader.onerror = () => {
+                    this.handleError('读取SVG内容失败', 'svgUpload');
+                };
+                
+                reader.readAsDataURL(file);
+            } catch (err) {
+                this.handleError(err, 'svgUpload');
+            }
+        },
+        
+        /**
+         * 从URL加载SVG
+         */
+        loadSvgFromUrl() {
+            const url = prompt('请输入SVG文件的URL:');
+            if (!url) return;
+            
+            // 显示加载状态
+            this.isProcessing = true;
+            this.processingMessage = '正在从URL加载SVG...';
+            this.processingProgress = 20;
+            
+            // 验证URL
+            if (!url.trim().startsWith('http')) {
+                this.handleError('URL格式不正确,请输入以http或https开头的有效URL', 'urlLoad');
+                return;
+            }
+            
+            // 模拟进度更新
+            const progressInterval = setInterval(() => {
+                if (this.processingProgress < 80) {
+                    this.processingProgress += 10;
+                }
+            }, 300);
+            
+            fetch(url)
+                .then(response => {
+                    if (!response.ok) {
+                        throw new Error(`获取文件失败,服务器返回状态码: ${response.status}`);
+                    }
+                    this.processingProgress = 90;
+                    return response.text();
+                })
+                .then(svgContent => {
+                    clearInterval(progressInterval);
+                    
+                    // 验证是否为SVG内容
+                    if (!svgContent.includes('<svg') || !svgContent.includes('</svg>')) {
+                        throw new Error('URL返回的内容不是有效的SVG格式');
+                    }
+                    
+                    // 创建SVG Blob
+                    const blob = new Blob([svgContent], {type: 'image/svg+xml'});
+                    const file = new File([blob], 'fromURL.svg', {type: 'image/svg+xml'});
+                    
+                    // 解析SVG获取尺寸信息
+                    const parser = new DOMParser();
+                    const svgDoc = parser.parseFromString(svgContent, 'image/svg+xml');
+                    const svgElement = svgDoc.documentElement;
+                    
+                    // 获取宽高
+                    let width = svgElement.getAttribute('width');
+                    let height = svgElement.getAttribute('height');
+                    
+                    if (!width || !height || width.includes('%') || height.includes('%')) {
+                        const viewBox = svgElement.getAttribute('viewBox');
+                        if (viewBox) {
+                            const viewBoxValues = viewBox.split(/\s+|,/);
+                            if (viewBoxValues.length >= 4) {
+                                width = parseFloat(viewBoxValues[2]);
+                                height = parseFloat(viewBoxValues[3]);
+                            }
+                        }
+                    } else {
+                        width = parseFloat(width);
+                        height = parseFloat(height);
+                    }
+                    
+                    // 保存宽高信息
+                    this.svgDimensions = {
+                        width: width || 300,
+                        height: height || 150
+                    };
+                    
+                    // 保存文件
+                    this.svgFile = file;
+                    this.svgFileName = url.split('/').pop() || 'fromURL.svg';
+                    this.svgFileSize = blob.size;
+                    this.svgSource = svgContent;
+                    
+                    // 更新SVG信息
+                    this.svgInfo.dimensions = `${this.svgDimensions.width} x ${this.svgDimensions.height}`;
+                    this.svgInfo.fileSize = this.formatFileSize(blob.size);
+                    
+                    // 创建预览
+                    const reader = new FileReader();
+                    reader.onload = (e) => {
+                        this.svgPreviewSrc = e.target.result;
+                        
+                        // 完成加载
+                        this.processingProgress = 100;
+                        setTimeout(() => {
+                            this.isProcessing = false;
+                        }, 300);
+                    };
+                    
+                    reader.onerror = () => {
+                        this.handleError('读取SVG文件失败', 'urlLoad');
+                    };
+                    
+                    reader.readAsDataURL(blob);
+                })
+                .catch(error => {
+                    clearInterval(progressInterval);
+                    this.handleError('加载SVG失败: ' + error.message, 'urlLoad');
+                });
+        },
+        
+        /**
+         * 重置输出尺寸为原始尺寸
+         */
+        resetSize: function() {
+            this.outputWidth = this.originalWidth;
+            this.outputHeight = this.originalHeight;
+        },
+        
+        /**
+         * 将SVG转换为图片
+         */
+        convertSvg() {
+            if (!this.svgPreviewSrc) return;
+            
+            // 设置处理状态
+            this.isProcessing = true;
+            this.processingMessage = '正在转换SVG...';
+            this.processingProgress = 0;
+            
+            // 模拟进度更新
+            const progressInterval = setInterval(() => {
+                if (this.processingProgress < 90) {
+                    this.processingProgress += 10;
+                }
+            }, 200);
+            
+            // 创建图像对象
+            const img = new Image();
+            
+            img.onload = () => {
+                try {
+                    const canvas = document.createElement('canvas');
+                    
+                    // 设置输出尺寸
+                    canvas.width = this.imgWidth || this.svgDimensions.width || img.width;
+                    canvas.height = this.imgHeight || this.svgDimensions.height || img.height;
+                    
+                    const ctx = canvas.getContext('2d');
+                    
+                    // 绘制白色背景(对于JPG格式)
+                    if (this.outputFormat === 'jpg') {
+                        ctx.fillStyle = '#FFFFFF';
+                        ctx.fillRect(0, 0, canvas.width, canvas.height);
+                    }
+                    
+                    // 绘制SVG
+                    ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
+                    
+                    // 转换为数据URL
+                    const dataURL = canvas.toDataURL('image/' + this.outputFormat, 0.95);
+                    this.imgPreviewSrc = dataURL;
+                    
+                    // 计算转换后文件大小
+                    const convertedSize = Math.round(dataURL.length * 0.75);
+                    
+                    // 进度100%
+                    this.processingProgress = 100;
+                    
+                    // 延迟一点关闭加载状态,让用户看到100%
+                    setTimeout(() => {
+                        clearInterval(progressInterval);
+                        this.isProcessing = false;
+                        
+                        // 更新图片信息
+                        this.imgInfo.dimensions = `${canvas.width} x ${canvas.height}`;
+                        this.imgInfo.fileSize = this.formatFileSize(convertedSize);
+                        this.imgInfo.fileType = this.outputFormat.toUpperCase();
+                        
+                        // 计算大小比较
+                        if (this.svgFileSize > 0) {
+                            const difference = convertedSize - this.svgFileSize;
+                            const percentage = Math.round((Math.abs(difference) / this.svgFileSize) * 100);
+                            
+                            this.imgInfo.comparison.ratio = percentage;
+                            this.imgInfo.comparison.isIncrease = difference > 0;
+                        }
+                    }, 500);
+                } catch (error) {
+                    clearInterval(progressInterval);
+                    this.isProcessing = false;
+                    this.errorMsg = '转换失败: ' + error.message;
+                }
+            };
+            
+            img.onerror = () => {
+                clearInterval(progressInterval);
+                this.isProcessing = false;
+                this.errorMsg = '加载SVG图像失败,请检查SVG文件是否有效';
+            };
+            
+            img.src = this.svgPreviewSrc;
+        },
+        
+        /**
+         * 下载转换后的图片
+         */
+        downloadImage() {
+            if (!this.imgPreviewSrc) {
+                return;
+            }
+            
+            // 获取当前时间戳
+            const timestamp = new Date().getTime();
+            const formattedDate = new Date().toISOString().replace(/:/g, '-').substring(0, 19);
+            
+            // 创建下载链接
+            const link = document.createElement('a');
+            
+            // 基础文件名(移除.svg扩展名)
+            let fileName = this.svgFileName.replace(/\.svg$/i, '') || 'converted';
+            
+            // 添加时间戳到文件名
+            fileName += '_' + formattedDate;
+            
+            // 添加扩展名
+            link.download = fileName + '.' + this.outputFormat;
+            link.href = this.imgPreviewSrc;
+            link.click();
+        },
+        
+        /**
+         * 初始化拖放功能
+         */
+        initDragAndDrop() {
+            const svgDropZone = document.querySelector('.x-panel');
+            
+            if (svgDropZone) {
+                // 监听拖拽 - SVG
+                svgDropZone.addEventListener('drop', (event) => {
+                    event.preventDefault();
+                    event.stopPropagation();
+                    
+                    let files = event.dataTransfer.files;
+                    if (files.length) {
+                        if (/svg/.test(files[0].type)) {
+                            this.processSvgFile(files[0]);
+                        } else {
+                            this.errorMsg = '请选择SVG文件!';
+                        }
+                    }
+                }, false);
+
+                // 监听拖拽阻止默认行为 - SVG
+                svgDropZone.addEventListener('dragover', (event) => {
+                    event.preventDefault();
+                    event.stopPropagation();
+                }, false);
+            }
+        },
+        
+        /**
+         * 计算并设置SVG文件信息
+         */
+        calculateSvgInfo(svgContent, fileSize) {
+            // 设置文件大小
+            this.svgInfo.fileSize = this.formatFileSize(fileSize);
+            
+            // 尝试从SVG内容解析尺寸
+            const widthMatch = svgContent.match(/width="([^"]+)"/);
+            const heightMatch = svgContent.match(/height="([^"]+)"/);
+            
+            if (widthMatch && heightMatch) {
+                let width = widthMatch[1];
+                let height = heightMatch[1];
+                
+                // 如果尺寸带有单位,尝试转换为像素
+                if (isNaN(parseFloat(width))) {
+                    width = '自适应';
+                }
+                if (isNaN(parseFloat(height))) {
+                    height = '自适应';
+                }
+                
+                if (width !== '自适应' && height !== '自适应') {
+                    this.svgInfo.dimensions = `${width} x ${height}`;
+                } else {
+                    // 获取viewBox尺寸作为备选
+                    const viewBoxMatch = svgContent.match(/viewBox="([^"]+)"/);
+                    if (viewBoxMatch) {
+                        const viewBox = viewBoxMatch[1].split(' ');
+                        if (viewBox.length === 4) {
+                            this.svgInfo.dimensions = `${viewBox[2]} x ${viewBox[3]}`;
+                        }
+                    }
+                }
+            }
+        },
+        
+        /**
+         * 更新图片信息
+         */
+        updateImageInfo(dataUrl, originalSize) {
+            // 计算文件大小
+            fetch(dataUrl)
+                .then(res => res.blob())
+                .then(blob => {
+                    const img = new Image();
+                    img.onload = () => {
+                        // 设置尺寸
+                        this.imgInfo.dimensions = `${img.width} x ${img.height}`;
+                        
+                        // 设置文件类型
+                        this.imgInfo.fileType = this.outputFormat.toUpperCase();
+                        
+                        // 设置文件大小
+                        const fileSize = blob.size;
+                        this.imgInfo.fileSize = this.formatFileSize(fileSize);
+                        
+                        // 计算大小比较
+                        const originalSizeNum = this.parseFileSize(originalSize);
+                        if (originalSizeNum > 0) {
+                            const ratio = ((fileSize / originalSizeNum) * 100 - 100).toFixed(1);
+                            this.imgInfo.comparison.ratio = Math.abs(ratio);
+                            this.imgInfo.comparison.isIncrease = ratio > 0;
+                        }
+                    };
+                    img.src = dataUrl;
+                });
+        },
+        
+        /**
+         * 格式化文件大小
+         */
+        formatFileSize(bytes) {
+            if (bytes === 0) return '0 KB';
+            
+            const k = 1024;
+            const sizes = ['B', 'KB', 'MB', 'GB'];
+            const i = Math.floor(Math.log(bytes) / Math.log(k));
+            
+            return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
+        },
+        
+        /**
+         * 解析文件大小字符串为字节数
+         */
+        parseFileSize(sizeStr) {
+            if (!sizeStr || typeof sizeStr !== 'string') return 0;
+            
+            const parts = sizeStr.split(' ');
+            if (parts.length !== 2) return 0;
+            
+            const size = parseFloat(parts[0]);
+            const unit = parts[1];
+            
+            switch (unit) {
+                case 'Bytes':
+                    return size;
+                case 'KB':
+                    return size * 1024;
+                case 'MB':
+                    return size * 1024 * 1024;
+                case 'GB':
+                    return size * 1024 * 1024 * 1024;
+                default:
+                    return 0;
+            }
+        },
+        
+        /**
+         * 显示文件大小警告
+         */
+        showFileSizeWarning: function(file, fileType) {
+            const fileSizeMB = (file.size / (1024 * 1024)).toFixed(2);
+            let warningMessage = '';
+            
+            const maxSizeLimit = '5MB';
+            warningMessage = `您上传的SVG文件大小为 ${fileSizeMB}MB,超过了建议的最大大小 ${maxSizeLimit}。过大的文件可能导致浏览器性能问题或转换失败。是否继续处理?`;
+            
+            // 创建警告对话框容器
+            const warningContainer = document.createElement('div');
+            warningContainer.className = 'warning-dialog';
+            warningContainer.style.position = 'fixed';
+            warningContainer.style.top = '0';
+            warningContainer.style.left = '0';
+            warningContainer.style.width = '100%';
+            warningContainer.style.height = '100%';
+            warningContainer.style.backgroundColor = 'rgba(0, 0, 0, 0.5)';
+            warningContainer.style.display = 'flex';
+            warningContainer.style.justifyContent = 'center';
+            warningContainer.style.alignItems = 'center';
+            warningContainer.style.zIndex = '9999';
+            
+            // 创建对话框内容
+            const dialogBox = document.createElement('div');
+            dialogBox.className = 'warning-dialog-box';
+            dialogBox.style.backgroundColor = '#fff';
+            dialogBox.style.borderRadius = '8px';
+            dialogBox.style.padding = '20px';
+            dialogBox.style.maxWidth = '500px';
+            dialogBox.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.2)';
+            
+            // 创建标题
+            const title = document.createElement('h3');
+            title.textContent = '文件大小警告';
+            title.style.color = '#e74c3c';
+            title.style.marginTop = '0';
+            
+            // 创建警告内容
+            const content = document.createElement('p');
+            content.textContent = warningMessage;
+            content.style.marginBottom = '20px';
+            
+            // 创建按钮容器
+            const btnContainer = document.createElement('div');
+            btnContainer.style.display = 'flex';
+            btnContainer.style.justifyContent = 'flex-end';
+            btnContainer.style.gap = '10px';
+            
+            // 取消按钮
+            const cancelBtn = document.createElement('button');
+            cancelBtn.className = 'btn-cancel';
+            cancelBtn.textContent = '取消';
+            cancelBtn.style.padding = '8px 16px';
+            cancelBtn.style.border = 'none';
+            cancelBtn.style.borderRadius = '4px';
+            cancelBtn.style.backgroundColor = '#e0e0e0';
+            cancelBtn.style.cursor = 'pointer';
+            cancelBtn.onclick = () => {
+                document.body.removeChild(warningContainer);
+                this.isProcessing = false;
+                this.processingProgress = 0;
+            };
+            
+            // 继续按钮
+            const continueBtn = document.createElement('button');
+            continueBtn.className = 'btn-continue';
+            continueBtn.textContent = '继续处理';
+            continueBtn.style.padding = '8px 16px';
+            continueBtn.style.border = 'none';
+            continueBtn.style.borderRadius = '4px';
+            continueBtn.style.backgroundColor = '#3498db';
+            continueBtn.style.color = '#fff';
+            continueBtn.style.cursor = 'pointer';
+            continueBtn.onclick = () => {
+                document.body.removeChild(warningContainer);
+                this.processSvgFile(file);
+            };
+            
+            // 组装对话框
+            btnContainer.appendChild(cancelBtn);
+            btnContainer.appendChild(continueBtn);
+            dialogBox.appendChild(title);
+            dialogBox.appendChild(content);
+            dialogBox.appendChild(btnContainer);
+            warningContainer.appendChild(dialogBox);
+            
+            // 添加到页面
+            document.body.appendChild(warningContainer);
+            
+            // 播放警告提示音
+            this.playSound('warning');
+        },
+        
+        /**
+         * 播放提示音
+         */
+        playSound: function(type) {
+            try {
+                let soundUrl = '';
+                
+                if (type === 'error') {
+                    soundUrl = chrome.runtime.getURL('static/audio/error.mp3');
+                } else if (type === 'warning') {
+                    soundUrl = chrome.runtime.getURL('static/audio/warning.mp3');
+                } else if (type === 'success') {
+                    soundUrl = chrome.runtime.getURL('static/audio/success.mp3');
+                }
+                
+                if (soundUrl) {
+                    const audio = new Audio(soundUrl);
+                    audio.volume = 0.5;
+                    audio.play().catch(e => console.warn('无法播放提示音:', e));
+                }
+            } catch (err) {
+                console.warn('播放提示音失败:', err);
+            }
+        },
+        
+        /**
+         * 统一错误处理方法
+         * @param {Error|string} error - 错误对象或错误消息
+         * @param {string} type - 错误类型 'svgConvert'|'imgConvert'
+         * @param {Object} options - 额外选项
+         */
+        handleError(error, type, options = {}) {
+            // 默认选项
+            const defaultOptions = {
+                isWarning: false,
+                clearAfter: 0
+            };
+            
+            // 合并选项
+            const finalOptions = {...defaultOptions, ...options};
+            
+            // 获取错误消息
+            const message = error instanceof Error ? error.message : error;
+            
+            // 根据类型设置错误消息
+            if (type === 'svgConvert') {
+                this.errorMsg = finalOptions.isWarning ? message : '转换失败: ' + message;
+            } else {
+                this.errorMsg = finalOptions.isWarning ? message : '转换失败: ' + message;
+            }
+            
+            // 记录到控制台
+            if (finalOptions.isWarning) {
+                console.warn(message);
+            } else {
+                console.error(message);
+            }
+            
+            // 关闭加载状态
+            this.isProcessing = false;
+            
+            // 如果设置了自动清除
+            if (finalOptions.clearAfter > 0) {
+                setTimeout(() => {
+                    this.errorMsg = '';
+                }, finalOptions.clearAfter);
+            }
+        },
+        
+        /**
+         * 处理SVG文件上传
+         * @param {Event} event - 文件上传事件
+         */
+        uploadSvgFile(event) {
+            if (event.target.files && event.target.files.length > 0) {
+                const file = event.target.files[0];
+                if (file.type === 'image/svg+xml' || file.name.toLowerCase().endsWith('.svg')) {
+                    this.processSvgFile(file);
+                } else {
+                    this.errorMsg = '请选择SVG格式的文件';
+                }
+                // 清空文件输入,确保可以重复上传相同文件
+                event.target.value = '';
+            }
+        },
+        
+        /**
+         * 处理图片文件上传
+         * @param {Event} event - 文件上传事件
+         */
+        uploadImageFile(event) {
+            if (event.target.files && event.target.files.length > 0) {
+                const file = event.target.files[0];
+                if (file.type.startsWith('image/') && file.type !== 'image/svg+xml') {
+                    this.processImageFile(file);
+                } else {
+                    this.imgError = '请选择非SVG格式的图片文件';
+                }
+                // 清空文件输入,确保可以重复上传相同文件
+                event.target.value = '';
+            }
+        },
+        
+        /**
+         * 更新SVG结果信息
+         * @param {string} svg - 生成的SVG内容
+         */
+        updateSvgResultInfo(svg) {
+            if (!svg) return;
+            
+            try {
+                // 计算SVG大小
+                const svgSize = new Blob([svg]).size;
+                const formattedSvgSize = this.formatFileSize(svgSize);
+                
+                // 获取原始图片大小(如果有)
+                let origImgSize = 0;
+                if (this.originalImgSize) {
+                    origImgSize = this.originalImgSize;
+                } else if (this.originalImgBlob) {
+                    origImgSize = this.originalImgBlob.size;
+                }
+                
+                // 计算大小比较
+                let sizeComparison = '';
+                if (origImgSize > 0) {
+                    const sizeRatio = (svgSize / origImgSize) * 100;
+                    if (sizeRatio < 100) {
+                        sizeComparison = `SVG比原图小 ${(100 - sizeRatio).toFixed(1)}%`;
+                    } else if (sizeRatio > 100) {
+                        sizeComparison = `SVG比原图大 ${(sizeRatio - 100).toFixed(1)}%`;
+                    } else {
+                        sizeComparison = '文件大小相同';
+                    }
+                }
+                
+                // 从SVG中提取宽度和高度
+                let width = 0;
+                let height = 0;
+                
+                // 提取width和height属性
+                const widthMatch = svg.match(/width="([^"]+)"/);
+                const heightMatch = svg.match(/height="([^"]+)"/);
+                
+                if (widthMatch && heightMatch) {
+                    width = parseInt(widthMatch[1], 10);
+                    height = parseInt(heightMatch[1], 10);
+                } else {
+                    // 尝试从viewBox提取尺寸
+                    const viewBoxMatch = svg.match(/viewBox="([^"]+)"/);
+                    if (viewBoxMatch) {
+                        const viewBoxParts = viewBoxMatch[1].split(/\s+/);
+                        if (viewBoxParts.length === 4) {
+                            width = parseInt(viewBoxParts[2], 10);
+                            height = parseInt(viewBoxParts[3], 10);
+                        }
+                    }
+                }
+                
+                // 更新SVG信息
+                this.svgInfo = {
+                    size: formattedSvgSize,
+                    originalSize: this.formatFileSize(origImgSize),
+                    sizeComparison: sizeComparison,
+                    dimensions: width && height ? `${width} × ${height}` : '未知'
+                };
+            } catch (e) {
+                console.error('更新SVG信息出错:', e);
+                this.svgInfo = {
+                    size: this.formatFileSize(new Blob([svg]).size),
+                    dimensions: '解析出错'
+                };
+            }
+        }
+    }
+});

+ 126 - 0
apps/svg-converter/modules/file-utils.js

@@ -0,0 +1,126 @@
+/**
+ * 文件处理工具类
+ * 提供文件读取、下载等通用功能
+ */
+(function(window) {
+    'use strict';
+
+    class FileUtils {
+        /**
+         * 读取文件内容为DataURL
+         * @param {File} file - 要读取的文件对象
+         * @param {Function} callback - 回调函数,接收读取的数据
+         */
+        static readAsDataURL(file, callback) {
+            const reader = new FileReader();
+            reader.onload = function(e) {
+                callback(e.target.result);
+            };
+            reader.onerror = function(e) {
+                console.error('文件读取失败:', e);
+                callback(null, e);
+            };
+            reader.readAsDataURL(file);
+        }
+
+        /**
+         * 读取文件内容为文本
+         * @param {File} file - 要读取的文件对象
+         * @param {Function} callback - 回调函数,接收读取的文本
+         */
+        static readAsText(file, callback) {
+            const reader = new FileReader();
+            reader.onload = function(e) {
+                callback(e.target.result);
+            };
+            reader.onerror = function(e) {
+                console.error('文件读取失败:', e);
+                callback(null, e);
+            };
+            reader.readAsText(file);
+        }
+
+        /**
+         * 下载数据为文件
+         * @param {string} content - 要下载的内容
+         * @param {string} fileName - 文件名
+         * @param {string} mimeType - MIME类型
+         */
+        static download(content, fileName, mimeType) {
+            // 创建blob对象
+            const blob = new Blob([content], { type: mimeType });
+            
+            // 创建用于下载的元素
+            const link = document.createElement('a');
+            link.href = URL.createObjectURL(blob);
+            link.download = fileName;
+            
+            // 触发点击
+            document.body.appendChild(link);
+            link.click();
+            
+            // 清理
+            document.body.removeChild(link);
+            setTimeout(() => {
+                URL.revokeObjectURL(link.href);
+            }, 100);
+        }
+
+        /**
+         * 下载DataURL为文件
+         * @param {string} dataUrl - 数据URL
+         * @param {string} fileName - 文件名
+         */
+        static downloadDataURL(dataUrl, fileName) {
+            const link = document.createElement('a');
+            link.href = dataUrl;
+            link.download = fileName;
+            
+            // 触发点击
+            document.body.appendChild(link);
+            link.click();
+            
+            // 清理
+            document.body.removeChild(link);
+        }
+
+        /**
+         * 从URL获取文件名
+         * @param {string} url - URL
+         * @returns {string} 文件名
+         */
+        static getFileNameFromUrl(url) {
+            try {
+                const urlObj = new URL(url);
+                const pathSegments = urlObj.pathname.split('/');
+                return pathSegments[pathSegments.length - 1] || 'downloaded-file';
+            } catch (e) {
+                return 'downloaded-file';
+            }
+        }
+
+        /**
+         * 格式化字节大小为可读的字符串
+         * @param {number} bytes - 字节数
+         * @returns {string} 格式化后的字符串
+         */
+        static formatFileSize(bytes) {
+            if (isNaN(bytes)) {
+                return '未知大小';
+            }
+            
+            const units = ['B', 'KB', 'MB', 'GB', 'TB'];
+            let unitIndex = 0;
+            let size = bytes;
+            
+            while (size >= 1024 && unitIndex < units.length - 1) {
+                size /= 1024;
+                unitIndex++;
+            }
+            
+            return size.toFixed(2) + ' ' + units[unitIndex];
+        }
+    }
+
+    window.FileUtils = FileUtils;
+})(window); 

+ 52 - 0
apps/svg-converter/modules/loading-helper.js

@@ -0,0 +1,52 @@
+/**
+ * 加载指示器助手
+ * 解决CSP安全策略问题,将内联脚本提取到外部文件
+ */
+
+// 为了确保加载指示器始终可用,添加一个全局函数
+window.showLoading = function(message) {
+    // 尝试使用Vue的方式
+    try {
+        if (window.vueApp && window.vueApp.isProcessing !== undefined) {
+            window.vueApp.isProcessing = true;
+            window.vueApp.processingMessage = message || '正在处理,请稍候...';
+        } else {
+            throw new Error('Vue实例不可用');
+        }
+    } catch (e) {
+        // 如果Vue方式失败,使用静态指示器
+        var loadingEl = document.getElementById('static-loading');
+        var messageEl = document.getElementById('static-loading-message');
+        if (messageEl) messageEl.textContent = message || '正在处理,请稍候...';
+        if (loadingEl) loadingEl.style.display = 'flex';
+    }
+};
+
+window.hideLoading = function() {
+    // 尝试使用Vue的方式
+    try {
+        if (window.vueApp && window.vueApp.isProcessing !== undefined) {
+            window.vueApp.isProcessing = false;
+        } else {
+            throw new Error('Vue实例不可用');
+        }
+    } catch (e) {
+        // 如果Vue方式失败,使用静态指示器
+        var loadingEl = document.getElementById('static-loading');
+        if (loadingEl) loadingEl.style.display = 'none';
+    }
+};
+
+// 暴露Vue实例到全局,以便于调试和访问
+document.addEventListener('DOMContentLoaded', function() {
+    setTimeout(function() {
+        try {
+            var vueInstance = document.getElementById('pageContainer').__vue__;
+            if (vueInstance) {
+                window.vueApp = vueInstance;
+            }
+        } catch (e) {
+            console.warn('无法获取Vue实例:', e);
+        }
+    }, 500);
+}); 

+ 153 - 0
apps/svg-converter/modules/svg-to-image.js

@@ -0,0 +1,153 @@
+/**
+ * SVG到图片转换工具
+ * 负责将SVG转换为PNG、JPEG等格式
+ */
+(function(window) {
+    'use strict';
+
+    class SvgToImage {
+        /**
+         * 将SVG转换为图像
+         * @param {string} svgText - SVG文本内容或者dataURI
+         * @param {number} width - 输出图像宽度
+         * @param {number} height - 输出图像高度
+         * @param {string} format - 输出格式 (png, jpeg, webp)
+         * @param {Function} callback - 回调函数,接收数据URL
+         */
+        static convert(svgText, width, height, format, callback) {
+            // 判断是否为SVG Data URI
+            let svgContent = svgText;
+            if (svgText.startsWith('data:image/svg+xml;')) {
+                try {
+                    // 从Data URI中提取SVG内容
+                    const base64Part = svgText.split(',')[1];
+                    svgContent = this._decodeBase64(base64Part);
+                } catch (e) {
+                    console.error('解析SVG Data URI失败:', e);
+                    callback(null, new Error('解析SVG Data URI失败'));
+                    return;
+                }
+            }
+
+            // 创建SVG的安全Blob URL
+            try {
+                const svgBlob = new Blob([svgContent], { type: 'image/svg+xml;charset=utf-8' });
+                const svgUrl = URL.createObjectURL(svgBlob);
+
+                // 创建图像元素加载SVG
+                const img = new Image();
+                img.onload = function() {
+                    // SVG加载完成后,在Canvas上绘制
+                    const canvas = document.createElement('canvas');
+                    
+                    // 设置Canvas尺寸
+                    canvas.width = width || img.width;
+                    canvas.height = height || img.height;
+                    
+                    const ctx = canvas.getContext('2d');
+                    
+                    // 绘制白色背景(针对JPEG和WEBP格式,因为它们不支持透明度)
+                    if (format === 'jpeg' || format === 'webp') {
+                        ctx.fillStyle = '#FFFFFF';
+                        ctx.fillRect(0, 0, canvas.width, canvas.height);
+                    }
+                    
+                    // 绘制SVG
+                    ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
+                    
+                    // 释放Blob URL
+                    URL.revokeObjectURL(svgUrl);
+                    
+                    // 转换为data URL并返回
+                    let mimeType;
+                    switch (format) {
+                        case 'jpeg':
+                            mimeType = 'image/jpeg';
+                            break;
+                        case 'webp':
+                            mimeType = 'image/webp';
+                            break;
+                        case 'png':
+                        default:
+                            mimeType = 'image/png';
+                            break;
+                    }
+                    
+                    const dataURL = canvas.toDataURL(mimeType, 0.95);
+                    callback(dataURL);
+                };
+                
+                img.onerror = function(err) {
+                    URL.revokeObjectURL(svgUrl);
+                    console.error('SVG加载失败:', err);
+                    callback(null, new Error('SVG加载失败'));
+                };
+                
+                img.src = svgUrl;
+            } catch (e) {
+                console.error('创建SVG Blob失败:', e);
+                callback(null, new Error('创建SVG Blob失败'));
+            }
+        }
+
+        /**
+         * 从SVG的Data URI中提取尺寸信息
+         * @param {string} svgDataURI - SVG的Data URI
+         * @returns {Object} 包含width和height的对象
+         */
+        static getSvgDimensions(svgDataURI) {
+            try {
+                let svgContent;
+                if (svgDataURI.startsWith('data:image/svg+xml;')) {
+                    const base64Part = svgDataURI.split(',')[1];
+                    svgContent = this._decodeBase64(base64Part);
+                } else {
+                    svgContent = svgDataURI;
+                }
+                
+                // 使用正则表达式提取宽度和高度
+                const widthMatch = svgContent.match(/width="([^"]+)"/);
+                const heightMatch = svgContent.match(/height="([^"]+)"/);
+                
+                // 如果找不到特定属性,尝试从viewBox中提取
+                if (!widthMatch || !heightMatch) {
+                    const viewBoxMatch = svgContent.match(/viewBox="([^"]+)"/);
+                    if (viewBoxMatch) {
+                        const viewBox = viewBoxMatch[1].split(' ');
+                        if (viewBox.length >= 4) {
+                            return {
+                                width: parseFloat(viewBox[2]),
+                                height: parseFloat(viewBox[3])
+                            };
+                        }
+                    }
+                }
+                
+                return {
+                    width: widthMatch ? parseFloat(widthMatch[1]) : 300,
+                    height: heightMatch ? parseFloat(heightMatch[1]) : 150
+                };
+            } catch (e) {
+                console.error('解析SVG尺寸失败:', e);
+                return { width: 300, height: 150 }; // 默认尺寸
+            }
+        }
+
+        /**
+         * 安全地解码Base64编码的SVG内容
+         * @param {string} base64String - Base64编码的字符串
+         * @returns {string} 解码后的SVG内容
+         * @private
+         */
+        static _decodeBase64(base64String) {
+            try {
+                return atob(base64String);
+            } catch (e) {
+                // 如果解码失败,尝试URL解码后再解码
+                return atob(decodeURIComponent(base64String));
+            }
+        }
+    }
+
+    window.SvgToImage = SvgToImage;
+})(window);