diff --git a/src/mcp_feedback_enhanced/web/locales/en/translation.json b/src/mcp_feedback_enhanced/web/locales/en/translation.json index 5b119f6..1ecbabf 100644 --- a/src/mcp_feedback_enhanced/web/locales/en/translation.json +++ b/src/mcp_feedback_enhanced/web/locales/en/translation.json @@ -320,6 +320,7 @@ "settings": { "title": "Image Settings", "sizeLimit": "Image Size Limit", + "sizeLimitDesc": "Set the maximum file size limit for uploaded images", "sizeLimitOptions": { "unlimited": "Unlimited", "1mb": "1MB", diff --git a/src/mcp_feedback_enhanced/web/locales/zh-CN/translation.json b/src/mcp_feedback_enhanced/web/locales/zh-CN/translation.json index 7fe305e..936476a 100644 --- a/src/mcp_feedback_enhanced/web/locales/zh-CN/translation.json +++ b/src/mcp_feedback_enhanced/web/locales/zh-CN/translation.json @@ -320,6 +320,7 @@ "settings": { "title": "图片设置", "sizeLimit": "图片大小限制", + "sizeLimitDesc": "设定上传图片的最大文件大小限制", "sizeLimitOptions": { "unlimited": "无限制", "1mb": "1MB", diff --git a/src/mcp_feedback_enhanced/web/locales/zh-TW/translation.json b/src/mcp_feedback_enhanced/web/locales/zh-TW/translation.json index ef5786c..0ada157 100644 --- a/src/mcp_feedback_enhanced/web/locales/zh-TW/translation.json +++ b/src/mcp_feedback_enhanced/web/locales/zh-TW/translation.json @@ -325,6 +325,7 @@ "settings": { "title": "圖片設定", "sizeLimit": "圖片大小限制", + "sizeLimitDesc": "設定上傳圖片的最大檔案大小限制", "sizeLimitOptions": { "unlimited": "無限制", "1mb": "1MB", diff --git a/src/mcp_feedback_enhanced/web/static/css/styles.css b/src/mcp_feedback_enhanced/web/static/css/styles.css index f297ba1..8ab7189 100644 --- a/src/mcp_feedback_enhanced/web/static/css/styles.css +++ b/src/mcp_feedback_enhanced/web/static/css/styles.css @@ -554,6 +554,100 @@ body { line-height: 1.4; } +.settings-card .setting-warning { + font-size: 12px; + color: var(--warning-color); + margin-top: 4px; + font-weight: 500; +} + +/* 圖片大小限制選擇器樣式 */ +.image-size-limit-selector { + display: flex; + align-items: center; +} + +.image-size-limit-select { + background: var(--bg-primary); + border: 1px solid var(--border-color); + border-radius: 6px; + color: var(--text-primary); + padding: 8px 12px; + font-size: 14px; + min-width: 120px; + cursor: pointer; + transition: all 0.3s ease; +} + +.image-size-limit-select:hover { + border-color: var(--accent-color); + background: var(--bg-tertiary); +} + +.image-size-limit-select:focus { + outline: none; + border-color: var(--accent-color); + box-shadow: 0 0 0 2px rgba(0, 122, 204, 0.2); +} + +/* Base64 切換器容器樣式 */ +.base64-toggle-container { + display: flex; + align-items: center; +} + +/* 現代化切換開關樣式 */ +.toggle-switch { + position: relative; + display: inline-block; + width: 50px; + height: 24px; + cursor: pointer; +} + +.toggle-input { + opacity: 0; + width: 0; + height: 0; +} + +.toggle-slider { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: var(--border-color); + border-radius: 24px; + transition: all 0.3s ease; + display: flex; + align-items: center; +} + +.toggle-slider:before { + content: ""; + position: absolute; + height: 18px; + width: 18px; + left: 3px; + background: white; + border-radius: 50%; + transition: all 0.3s ease; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); +} + +.toggle-input:checked + .toggle-slider { + background: var(--accent-color); +} + +.toggle-input:checked + .toggle-slider:before { + transform: translateX(26px); +} + +.toggle-switch:hover .toggle-slider { + box-shadow: 0 0 0 2px rgba(0, 122, 204, 0.2); +} + /* 佈局模式選擇器 */ .layout-mode-selector { display: flex; @@ -1102,106 +1196,9 @@ body { transform: scale(1.1); } -/* 圖片設定區域樣式 */ -.image-settings-details { - background: var(--bg-tertiary); - border: 1px solid var(--border-color); - border-radius: 6px; - margin-bottom: 8px; - overflow: hidden; - transition: all 0.3s ease; -} -.image-settings-details:hover { - border-color: rgba(0, 122, 204, 0.5); - box-shadow: 0 2px 8px rgba(0, 122, 204, 0.1); -} -.image-settings-summary { - padding: 8px 12px; - background: var(--bg-secondary); - cursor: pointer; - font-size: 13px; - font-weight: 500; - color: var(--text-primary); - border: none; - outline: none; - transition: all 0.3s ease; - list-style: none; - display: flex; - align-items: center; - justify-content: space-between; - position: relative; - user-select: none; -} -.image-settings-summary:hover { - background: var(--surface-color); - color: var(--accent-color); -} - -.image-settings-summary::-webkit-details-marker { - display: none; -} - -/* 箭頭指示器 */ -.image-settings-summary::after { - content: '▼'; - font-size: 10px; - color: var(--text-secondary); - transition: all 0.3s ease; - transform-origin: center; - display: inline-block; - margin-left: auto; -} - -.image-settings-summary:hover::after { - color: var(--accent-color); -} - -/* 展開狀態的箭頭 */ -.image-settings-details[open] .image-settings-summary::after { - transform: rotate(180deg); - color: var(--accent-color); -} - -/* 展開狀態的 summary 樣式 */ -.image-settings-details[open] .image-settings-summary { - background: var(--surface-color); - color: var(--accent-color); - border-bottom: 1px solid var(--border-color); -} - -.image-settings-content { - padding: 12px; - background: var(--bg-tertiary); - animation: slideDown 0.3s ease-out; -} - -/* 展開內容的滑入動畫 */ -@keyframes slideDown { - from { - opacity: 0; - transform: translateY(-10px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -/* 圖片設定行 */ -.image-setting-row { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 8px; - gap: 12px; -} - -.image-setting-row:last-child { - margin-bottom: 0; -} .image-setting-label { font-size: 12px; diff --git a/src/mcp_feedback_enhanced/web/static/js/modules/file-upload-manager.js b/src/mcp_feedback_enhanced/web/static/js/modules/file-upload-manager.js new file mode 100644 index 0000000..a7b3159 --- /dev/null +++ b/src/mcp_feedback_enhanced/web/static/js/modules/file-upload-manager.js @@ -0,0 +1,543 @@ +/** + * 現代化檔案上傳管理器 + * 使用事件委託模式,避免重複事件監聽器問題 + */ + +(function() { + 'use strict'; + + // 確保命名空間存在 + if (!window.MCPFeedback) { + window.MCPFeedback = {}; + } + + /** + * 檔案上傳管理器建構函數 + */ + function FileUploadManager(options) { + options = options || {}; + + // 配置選項 + this.maxFileSize = options.maxFileSize || 0; // 0 表示無限制 + this.enableBase64Detail = options.enableBase64Detail || false; + this.acceptedTypes = options.acceptedTypes || 'image/*'; + this.maxFiles = options.maxFiles || 10; + + // 狀態管理 + this.files = []; + this.isInitialized = false; + this.debounceTimeout = null; + this.lastClickTime = 0; + this.isProcessingClick = false; + this.lastClickTime = 0; + + // 事件回調 + this.onFileAdd = options.onFileAdd || null; + this.onFileRemove = options.onFileRemove || null; + this.onSettingsChange = options.onSettingsChange || null; + + // 綁定方法上下文 + this.handleDelegatedEvent = this.handleDelegatedEvent.bind(this); + this.handleGlobalPaste = this.handleGlobalPaste.bind(this); + + console.log('📁 FileUploadManager 初始化完成'); + } + + /** + * 初始化檔案上傳管理器 + */ + FileUploadManager.prototype.initialize = function() { + if (this.isInitialized) { + console.warn('⚠️ FileUploadManager 已經初始化過了'); + return; + } + + this.setupEventDelegation(); + this.setupGlobalPasteHandler(); + this.isInitialized = true; + + console.log('✅ FileUploadManager 事件委託設置完成'); + }; + + /** + * 設置事件委託 + * 使用單一事件監聽器處理所有檔案上傳相關事件 + */ + FileUploadManager.prototype.setupEventDelegation = function() { + // 移除舊的事件監聽器 + document.removeEventListener('click', this.handleDelegatedEvent); + document.removeEventListener('dragover', this.handleDelegatedEvent); + document.removeEventListener('dragleave', this.handleDelegatedEvent); + document.removeEventListener('drop', this.handleDelegatedEvent); + document.removeEventListener('change', this.handleDelegatedEvent); + + // 設置新的事件委託 + document.addEventListener('click', this.handleDelegatedEvent); + document.addEventListener('dragover', this.handleDelegatedEvent); + document.addEventListener('dragleave', this.handleDelegatedEvent); + document.addEventListener('drop', this.handleDelegatedEvent); + document.addEventListener('change', this.handleDelegatedEvent); + }; + + /** + * 處理委託事件 + */ + FileUploadManager.prototype.handleDelegatedEvent = function(event) { + const target = event.target; + + // 處理檔案移除按鈕點擊 + const removeBtn = target.closest('.image-remove-btn'); + if (removeBtn) { + event.preventDefault(); + event.stopPropagation(); + this.handleRemoveFile(removeBtn); + return; + } + + // 處理檔案輸入變更 + if (target.type === 'file' && event.type === 'change') { + this.handleFileInputChange(target, event); + return; + } + + // 處理上傳區域事件 - 只處理直接點擊上傳區域的情況 + const uploadArea = target.closest('.image-upload-area'); + if (uploadArea && event.type === 'click') { + // 確保不是點擊 input 元素本身 + if (target.type === 'file') { + return; + } + + // 確保不是點擊預覽圖片或移除按鈕 + if (target.closest('.image-preview-item') || target.closest('.image-remove-btn')) { + return; + } + + this.handleUploadAreaClick(uploadArea, event); + return; + } + + // 處理拖放事件 + if (uploadArea && (event.type === 'dragover' || event.type === 'dragleave' || event.type === 'drop')) { + switch (event.type) { + case 'dragover': + this.handleDragOver(uploadArea, event); + break; + case 'dragleave': + this.handleDragLeave(uploadArea, event); + break; + case 'drop': + this.handleDrop(uploadArea, event); + break; + } + } + }; + + /** + * 處理上傳區域點擊(使用防抖機制) + */ + FileUploadManager.prototype.handleUploadAreaClick = function(uploadArea, event) { + event.preventDefault(); + event.stopPropagation(); + + // 強力防抖機制 - 防止無限循環 + const now = Date.now(); + if (this.lastClickTime && (now - this.lastClickTime) < 500) { + console.log('🚫 防抖:忽略重複點擊,間隔:', now - this.lastClickTime, 'ms'); + return; + } + this.lastClickTime = now; + + // 如果已經有待處理的點擊,忽略新的點擊 + if (this.isProcessingClick) { + console.log('🚫 正在處理點擊,忽略新的點擊'); + return; + } + + this.isProcessingClick = true; + + const fileInput = uploadArea.querySelector('input[type="file"]'); + if (fileInput) { + console.log('🖱️ 觸發檔案選擇:', fileInput.id); + + // 重置 input 值以確保可以重複選擇同一檔案 + fileInput.value = ''; + + // 使用 setTimeout 確保在下一個事件循環中執行,避免事件冒泡問題 + const self = this; + setTimeout(function() { + try { + fileInput.click(); + console.log('✅ 檔案選擇對話框已觸發'); + } catch (error) { + console.error('❌ 檔案選擇對話框觸發失敗:', error); + } finally { + // 重置處理狀態 + setTimeout(function() { + self.isProcessingClick = false; + }, 100); + } + }, 50); + } else { + this.isProcessingClick = false; + } + }; + + /** + * 處理檔案輸入變更 + */ + FileUploadManager.prototype.handleFileInputChange = function(fileInput, event) { + const files = event.target.files; + if (files && files.length > 0) { + console.log('📁 檔案選擇變更:', files.length, '個檔案'); + this.processFiles(Array.from(files), fileInput); + } + }; + + /** + * 處理拖放事件 + */ + FileUploadManager.prototype.handleDragOver = function(uploadArea, event) { + event.preventDefault(); + uploadArea.classList.add('dragover'); + }; + + FileUploadManager.prototype.handleDragLeave = function(uploadArea, event) { + event.preventDefault(); + // 只有當滑鼠真正離開上傳區域時才移除樣式 + if (!uploadArea.contains(event.relatedTarget)) { + uploadArea.classList.remove('dragover'); + } + }; + + FileUploadManager.prototype.handleDrop = function(uploadArea, event) { + event.preventDefault(); + uploadArea.classList.remove('dragover'); + + const files = event.dataTransfer.files; + if (files && files.length > 0) { + console.log('📁 拖放檔案:', files.length, '個檔案'); + this.processFiles(Array.from(files), uploadArea.querySelector('input[type="file"]')); + } + }; + + /** + * 處理檔案移除 + */ + FileUploadManager.prototype.handleRemoveFile = function(removeBtn) { + const index = parseInt(removeBtn.dataset.index); + if (!isNaN(index) && index >= 0 && index < this.files.length) { + const removedFile = this.files.splice(index, 1)[0]; + console.log('🗑️ 移除檔案:', removedFile.name); + + this.updateAllPreviews(); + + if (this.onFileRemove) { + this.onFileRemove(removedFile, index); + } + } + }; + + /** + * 設置全域剪貼板貼上處理 + */ + FileUploadManager.prototype.setupGlobalPasteHandler = function() { + document.removeEventListener('paste', this.handleGlobalPaste); + document.addEventListener('paste', this.handleGlobalPaste); + }; + + /** + * 處理全域剪貼板貼上 + */ + FileUploadManager.prototype.handleGlobalPaste = function(event) { + const items = event.clipboardData.items; + const imageFiles = []; + + for (let i = 0; i < items.length; i++) { + const item = items[i]; + if (item.type.indexOf('image') !== -1) { + const file = item.getAsFile(); + if (file) { + imageFiles.push(file); + } + } + } + + if (imageFiles.length > 0) { + event.preventDefault(); + console.log('📋 剪貼板貼上圖片:', imageFiles.length, '個檔案'); + this.processFiles(imageFiles); + } + }; + + /** + * 處理檔案 + */ + FileUploadManager.prototype.processFiles = function(files, sourceInput) { + const validFiles = []; + + for (let i = 0; i < files.length; i++) { + const file = files[i]; + + // 檢查檔案類型 + if (!file.type.startsWith('image/')) { + console.warn('⚠️ 跳過非圖片檔案:', file.name); + continue; + } + + // 檢查檔案大小 + if (this.maxFileSize > 0 && file.size > this.maxFileSize) { + const sizeLimit = this.formatFileSize(this.maxFileSize); + console.warn('⚠️ 檔案過大:', file.name, '超過限制', sizeLimit); + this.showMessage('圖片大小超過限制 (' + sizeLimit + '): ' + file.name, 'warning'); + continue; + } + + // 檢查檔案數量限制 + if (this.files.length + validFiles.length >= this.maxFiles) { + console.warn('⚠️ 檔案數量超過限制:', this.maxFiles); + this.showMessage('最多只能上傳 ' + this.maxFiles + ' 個檔案', 'warning'); + break; + } + + validFiles.push(file); + } + + // 處理有效檔案 + if (validFiles.length > 0) { + this.addFiles(validFiles); + } + }; + + /** + * 添加檔案到列表 + */ + FileUploadManager.prototype.addFiles = function(files) { + const promises = files.map(file => this.fileToBase64(file)); + + const self = this; + Promise.all(promises) + .then(function(base64Results) { + base64Results.forEach(function(base64, index) { + const file = files[index]; + const fileData = { + name: file.name, + size: file.size, + type: file.type, + data: base64, + timestamp: Date.now() + }; + + self.files.push(fileData); + console.log('✅ 檔案已添加:', file.name); + + if (self.onFileAdd) { + self.onFileAdd(fileData); + } + }); + + self.updateAllPreviews(); + }) + .catch(function(error) { + console.error('❌ 檔案處理失敗:', error); + self.showMessage('檔案處理失敗,請重試', 'error'); + }); + }; + + /** + * 將檔案轉換為 Base64 + */ + FileUploadManager.prototype.fileToBase64 = function(file) { + return new Promise(function(resolve, reject) { + const reader = new FileReader(); + reader.onload = function() { + resolve(reader.result.split(',')[1]); + }; + reader.onerror = reject; + reader.readAsDataURL(file); + }); + }; + + /** + * 更新所有預覽容器 + */ + FileUploadManager.prototype.updateAllPreviews = function() { + const previewContainers = document.querySelectorAll('.image-preview-container'); + const self = this; + + previewContainers.forEach(function(container) { + self.updatePreviewContainer(container); + }); + + this.updateFileCount(); + console.log('🖼️ 已更新', previewContainers.length, '個預覽容器'); + }; + + /** + * 更新單個預覽容器 + */ + FileUploadManager.prototype.updatePreviewContainer = function(container) { + container.innerHTML = ''; + + const self = this; + this.files.forEach(function(file, index) { + const previewElement = self.createPreviewElement(file, index); + container.appendChild(previewElement); + }); + }; + + /** + * 創建預覽元素 + */ + FileUploadManager.prototype.createPreviewElement = function(file, index) { + const preview = document.createElement('div'); + preview.className = 'image-preview-item'; + + // 圖片元素 + const img = document.createElement('img'); + img.src = 'data:' + file.type + ';base64,' + file.data; + img.alt = file.name; + img.title = file.name + ' (' + this.formatFileSize(file.size) + ')'; + + // 檔案資訊 + const info = document.createElement('div'); + info.className = 'image-info'; + + const name = document.createElement('div'); + name.className = 'image-name'; + name.textContent = file.name; + + const size = document.createElement('div'); + size.className = 'image-size'; + size.textContent = this.formatFileSize(file.size); + + // 移除按鈕 + const removeBtn = document.createElement('button'); + removeBtn.className = 'image-remove-btn'; + removeBtn.textContent = '×'; + removeBtn.title = '移除圖片'; + removeBtn.dataset.index = index; + removeBtn.setAttribute('aria-label', '移除圖片 ' + file.name); + + // 組裝元素 + info.appendChild(name); + info.appendChild(size); + preview.appendChild(img); + preview.appendChild(info); + preview.appendChild(removeBtn); + + return preview; + }; + + /** + * 更新檔案計數顯示 + */ + FileUploadManager.prototype.updateFileCount = function() { + const count = this.files.length; + const countElements = document.querySelectorAll('.image-count'); + + countElements.forEach(function(element) { + element.textContent = count > 0 ? '(' + count + ')' : ''; + }); + + // 更新上傳區域狀態 + const uploadAreas = document.querySelectorAll('.image-upload-area'); + uploadAreas.forEach(function(area) { + if (count > 0) { + area.classList.add('has-images'); + } else { + area.classList.remove('has-images'); + } + }); + }; + + /** + * 格式化檔案大小 + */ + FileUploadManager.prototype.formatFileSize = function(bytes) { + if (bytes === 0) return '0 Bytes'; + + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; + }; + + /** + * 顯示訊息 + */ + FileUploadManager.prototype.showMessage = function(message, type) { + // 使用現有的 Utils.showMessage 如果可用 + if (window.MCPFeedback && window.MCPFeedback.Utils && window.MCPFeedback.Utils.showMessage) { + const messageType = type === 'warning' ? window.MCPFeedback.Utils.CONSTANTS.MESSAGE_WARNING : + type === 'error' ? window.MCPFeedback.Utils.CONSTANTS.MESSAGE_ERROR : + window.MCPFeedback.Utils.CONSTANTS.MESSAGE_INFO; + window.MCPFeedback.Utils.showMessage(message, messageType); + } else { + // 後備方案 + console.log('[' + type.toUpperCase() + ']', message); + alert(message); + } + }; + + /** + * 更新設定 + */ + FileUploadManager.prototype.updateSettings = function(settings) { + this.maxFileSize = settings.imageSizeLimit || 0; + this.enableBase64Detail = settings.enableBase64Detail || false; + + console.log('⚙️ FileUploadManager 設定已更新:', { + maxFileSize: this.maxFileSize, + enableBase64Detail: this.enableBase64Detail + }); + }; + + /** + * 獲取檔案列表 + */ + FileUploadManager.prototype.getFiles = function() { + return this.files.slice(); // 返回副本 + }; + + /** + * 清空所有檔案 + */ + FileUploadManager.prototype.clearFiles = function() { + this.files = []; + this.updateAllPreviews(); + console.log('🗑️ 已清空所有檔案'); + }; + + /** + * 清理資源 + */ + FileUploadManager.prototype.cleanup = function() { + // 移除事件監聽器 + document.removeEventListener('click', this.handleDelegatedEvent); + document.removeEventListener('dragover', this.handleDelegatedEvent); + document.removeEventListener('dragleave', this.handleDelegatedEvent); + document.removeEventListener('drop', this.handleDelegatedEvent); + document.removeEventListener('change', this.handleDelegatedEvent); + document.removeEventListener('paste', this.handleGlobalPaste); + + // 清理防抖計時器 + if (this.debounceTimeout) { + clearTimeout(this.debounceTimeout); + this.debounceTimeout = null; + } + + // 清空檔案 + this.clearFiles(); + + this.isInitialized = false; + console.log('🧹 FileUploadManager 資源已清理'); + }; + + // 將 FileUploadManager 加入命名空間 + window.MCPFeedback.FileUploadManager = FileUploadManager; + + console.log('✅ FileUploadManager 模組載入完成'); + +})(); diff --git a/src/mcp_feedback_enhanced/web/static/js/modules/image-handler.js b/src/mcp_feedback_enhanced/web/static/js/modules/image-handler.js index 5bb58cc..2d3571c 100644 --- a/src/mcp_feedback_enhanced/web/static/js/modules/image-handler.js +++ b/src/mcp_feedback_enhanced/web/static/js/modules/image-handler.js @@ -17,30 +17,38 @@ */ function ImageHandler(options) { options = options || {}; - - this.images = []; + this.imageSizeLimit = options.imageSizeLimit || 0; this.enableBase64Detail = options.enableBase64Detail || false; this.layoutMode = options.layoutMode || 'combined-vertical'; this.currentImagePrefix = ''; - - // UI 元素 - this.imageInput = null; - this.imageUploadArea = null; - this.imagePreviewContainer = null; + + // UI 元素(保留用於設定同步) this.imageSizeLimitSelect = null; this.enableBase64DetailCheckbox = null; - - // 事件處理器 - this.imageChangeHandler = null; - this.imageClickHandler = null; - this.imageDragOverHandler = null; - this.imageDragLeaveHandler = null; - this.imageDropHandler = null; - this.pasteHandler = null; - + // 回調函數 this.onSettingsChange = options.onSettingsChange || null; + + // 創建檔案上傳管理器 + const self = this; + this.fileUploadManager = new window.MCPFeedback.FileUploadManager({ + maxFileSize: this.imageSizeLimit, + enableBase64Detail: this.enableBase64Detail, + onFileAdd: function(fileData) { + console.log('📁 檔案已添加:', fileData.name); + }, + onFileRemove: function(fileData, index) { + console.log('🗑️ 檔案已移除:', fileData.name); + }, + onSettingsChange: function() { + if (self.onSettingsChange) { + self.onSettingsChange(); + } + } + }); + + console.log('🖼️ ImageHandler 建構函數初始化完成'); } /** @@ -48,148 +56,46 @@ */ ImageHandler.prototype.init = function() { console.log('🖼️ 開始初始化圖片處理功能...'); - - this.initImageElements(); - this.setupImageEventListeners(); - this.setupGlobalPasteHandler(); - + + // 初始化設定元素 + this.initImageSettingsElements(); + + // 初始化檔案上傳管理器 + this.fileUploadManager.initialize(); + console.log('✅ 圖片處理功能初始化完成'); }; /** * 動態初始化圖片相關元素 */ - ImageHandler.prototype.initImageElements = function() { - const prefix = this.layoutMode && this.layoutMode.startsWith('combined') ? 'combined' : 'feedback'; - - console.log('🖼️ 初始化圖片元素,使用前綴: ' + prefix); - - this.imageInput = Utils.safeQuerySelector('#' + prefix + 'ImageInput') || - Utils.safeQuerySelector('#imageInput'); - this.imageUploadArea = Utils.safeQuerySelector('#' + prefix + 'ImageUploadArea') || - Utils.safeQuerySelector('#imageUploadArea'); - this.imagePreviewContainer = Utils.safeQuerySelector('#' + prefix + 'ImagePreviewContainer') || - Utils.safeQuerySelector('#imagePreviewContainer'); - this.imageSizeLimitSelect = Utils.safeQuerySelector('#' + prefix + 'ImageSizeLimit') || - Utils.safeQuerySelector('#imageSizeLimit'); - this.enableBase64DetailCheckbox = Utils.safeQuerySelector('#' + prefix + 'EnableBase64Detail') || - Utils.safeQuerySelector('#enableBase64Detail'); - - this.currentImagePrefix = prefix; - - if (!this.imageInput || !this.imageUploadArea) { - console.warn('⚠️ 圖片元素初始化失敗 - imageInput: ' + !!this.imageInput + ', imageUploadArea: ' + !!this.imageUploadArea); - } else { - console.log('✅ 圖片元素初始化成功 - 前綴: ' + prefix); - } - }; + ImageHandler.prototype.initImageSettingsElements = function() { + // 查找設定頁籤中的圖片設定元素 + this.imageSizeLimitSelect = Utils.safeQuerySelector('#settingsImageSizeLimit'); + this.enableBase64DetailCheckbox = Utils.safeQuerySelector('#settingsEnableBase64Detail'); - /** - * 設置圖片事件監聽器 - */ - ImageHandler.prototype.setupImageEventListeners = function() { - if (!this.imageInput || !this.imageUploadArea) { - console.warn('⚠️ 缺少必要的圖片元素,跳過事件監聽器設置'); - return; - } - - console.log('🖼️ 設置圖片事件監聽器 - imageInput: ' + this.imageInput.id + ', imageUploadArea: ' + this.imageUploadArea.id); - - // 移除舊的事件監聽器 - this.removeImageEventListeners(); - - const self = this; - - // 文件選擇事件 - this.imageChangeHandler = function(e) { - console.log('📁 文件選擇事件觸發 - input: ' + e.target.id + ', files: ' + e.target.files.length); - self.handleFileSelect(e.target.files); - }; - this.imageInput.addEventListener('change', this.imageChangeHandler); - - // 點擊上傳區域 - this.imageClickHandler = function(e) { - e.preventDefault(); - e.stopPropagation(); - - if (self.imageInput) { - console.log('🖱️ 點擊上傳區域 - 觸發 input: ' + self.imageInput.id); - self.imageInput.click(); - } - }; - this.imageUploadArea.addEventListener('click', this.imageClickHandler); - - // 拖放事件 - this.imageDragOverHandler = function(e) { - e.preventDefault(); - self.imageUploadArea.classList.add('dragover'); - }; - this.imageUploadArea.addEventListener('dragover', this.imageDragOverHandler); - - this.imageDragLeaveHandler = function(e) { - e.preventDefault(); - self.imageUploadArea.classList.remove('dragover'); - }; - this.imageUploadArea.addEventListener('dragleave', this.imageDragLeaveHandler); - - this.imageDropHandler = function(e) { - e.preventDefault(); - self.imageUploadArea.classList.remove('dragover'); - self.handleFileSelect(e.dataTransfer.files); - }; - this.imageUploadArea.addEventListener('drop', this.imageDropHandler); - - // 初始化圖片設定事件 + // 初始化設定事件監聽器 this.initImageSettings(); + + console.log('✅ 圖片設定元素初始化完成'); }; - /** - * 設置全域剪貼板貼上事件 - */ - ImageHandler.prototype.setupGlobalPasteHandler = function() { - if (this.pasteHandler) { - return; // 已經設置過了 - } - const self = this; - this.pasteHandler = function(e) { - const items = e.clipboardData.items; - for (let i = 0; i < items.length; i++) { - const item = items[i]; - if (item.type.indexOf('image') !== -1) { - e.preventDefault(); - const file = item.getAsFile(); - self.handleFileSelect([file]); - break; - } - } - }; - - document.addEventListener('paste', this.pasteHandler); - console.log('✅ 全域剪貼板貼上事件已設置'); - }; + + /** - * 移除圖片事件監聽器 + * 移除圖片設定事件監聽器 */ - ImageHandler.prototype.removeImageEventListeners = function() { - if (this.imageInput && this.imageChangeHandler) { - this.imageInput.removeEventListener('change', this.imageChangeHandler); + ImageHandler.prototype.removeImageSettingsListeners = function() { + if (this.imageSizeLimitSelect && this.imageSizeLimitChangeHandler) { + this.imageSizeLimitSelect.removeEventListener('change', this.imageSizeLimitChangeHandler); + this.imageSizeLimitChangeHandler = null; } - - if (this.imageUploadArea) { - if (this.imageClickHandler) { - this.imageUploadArea.removeEventListener('click', this.imageClickHandler); - } - if (this.imageDragOverHandler) { - this.imageUploadArea.removeEventListener('dragover', this.imageDragOverHandler); - } - if (this.imageDragLeaveHandler) { - this.imageUploadArea.removeEventListener('dragleave', this.imageDragLeaveHandler); - } - if (this.imageDropHandler) { - this.imageUploadArea.removeEventListener('drop', this.imageDropHandler); - } + + if (this.enableBase64DetailCheckbox && this.enableBase64DetailChangeHandler) { + this.enableBase64DetailCheckbox.removeEventListener('change', this.enableBase64DetailChangeHandler); + this.enableBase64DetailChangeHandler = null; } }; @@ -198,238 +104,47 @@ */ ImageHandler.prototype.initImageSettings = function() { const self = this; - + + // 移除舊的設定事件監聽器 + this.removeImageSettingsListeners(); + if (this.imageSizeLimitSelect) { - this.imageSizeLimitSelect.addEventListener('change', function(e) { + this.imageSizeLimitChangeHandler = function(e) { self.imageSizeLimit = parseInt(e.target.value); if (self.onSettingsChange) { self.onSettingsChange(); } - }); + }; + this.imageSizeLimitSelect.addEventListener('change', this.imageSizeLimitChangeHandler); } if (this.enableBase64DetailCheckbox) { - this.enableBase64DetailCheckbox.addEventListener('change', function(e) { + this.enableBase64DetailChangeHandler = function(e) { self.enableBase64Detail = e.target.checked; if (self.onSettingsChange) { self.onSettingsChange(); } - }); - } - }; - - /** - * 處理文件選擇 - */ - ImageHandler.prototype.handleFileSelect = function(files) { - for (let i = 0; i < files.length; i++) { - const file = files[i]; - if (file.type.startsWith('image/')) { - this.addImage(file); - } - } - }; - - /** - * 添加圖片 - */ - ImageHandler.prototype.addImage = function(file) { - // 檢查文件大小 - if (this.imageSizeLimit > 0 && file.size > this.imageSizeLimit) { - Utils.showMessage('圖片大小超過限制 (' + Utils.formatFileSize(this.imageSizeLimit) + ')', Utils.CONSTANTS.MESSAGE_WARNING); - return; - } - - const self = this; - this.fileToBase64(file) - .then(function(base64) { - const imageData = { - name: file.name, - size: file.size, - type: file.type, - data: base64 - }; - - self.images.push(imageData); - self.updateImagePreview(); - }) - .catch(function(error) { - console.error('圖片處理失敗:', error); - Utils.showMessage('圖片處理失敗,請重試', Utils.CONSTANTS.MESSAGE_ERROR); - }); - }; - - /** - * 將文件轉換為 Base64 - */ - ImageHandler.prototype.fileToBase64 = function(file) { - return new Promise(function(resolve, reject) { - const reader = new FileReader(); - reader.onload = function() { - resolve(reader.result.split(',')[1]); }; - reader.onerror = reject; - reader.readAsDataURL(file); - }); - }; - - /** - * 更新圖片預覽 - */ - ImageHandler.prototype.updateImagePreview = function() { - const previewContainers = [ - Utils.safeQuerySelector('#feedbackImagePreviewContainer'), - Utils.safeQuerySelector('#combinedImagePreviewContainer'), - this.imagePreviewContainer - ].filter(function(container) { - return container !== null; - }); - - if (previewContainers.length === 0) { - console.warn('⚠️ 沒有找到圖片預覽容器'); - return; + this.enableBase64DetailCheckbox.addEventListener('change', this.enableBase64DetailChangeHandler); } - - console.log('🖼️ 更新 ' + previewContainers.length + ' 個圖片預覽容器'); - - const self = this; - previewContainers.forEach(function(container) { - container.innerHTML = ''; - - self.images.forEach(function(image, index) { - const preview = self.createImagePreviewElement(image, index); - container.appendChild(preview); - }); - }); - - this.updateImageCount(); }; - /** - * 創建圖片預覽元素 - */ - ImageHandler.prototype.createImagePreviewElement = function(image, index) { - const self = this; - - // 創建圖片預覽項目容器 - const preview = document.createElement('div'); - preview.className = 'image-preview-item'; - preview.style.cssText = 'position: relative; display: inline-block;'; - // 創建圖片元素 - const img = document.createElement('img'); - img.src = 'data:' + image.type + ';base64,' + image.data; - img.alt = image.name; - img.style.cssText = 'width: 80px; height: 80px; object-fit: cover; display: block; border-radius: 6px;'; - // 創建圖片信息容器 - const imageInfo = document.createElement('div'); - imageInfo.className = 'image-info'; - imageInfo.style.cssText = ` - position: absolute; bottom: 0; left: 0; right: 0; - background: rgba(0, 0, 0, 0.7); color: white; padding: 4px; - font-size: 10px; line-height: 1.2; - `; - const imageName = document.createElement('div'); - imageName.className = 'image-name'; - imageName.textContent = image.name; - imageName.style.cssText = 'font-weight: bold; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;'; - - const imageSize = document.createElement('div'); - imageSize.className = 'image-size'; - imageSize.textContent = Utils.formatFileSize(image.size); - imageSize.style.cssText = 'font-size: 9px; opacity: 0.8;'; - - // 創建刪除按鈕 - const removeBtn = document.createElement('button'); - removeBtn.className = 'image-remove-btn'; - removeBtn.textContent = '×'; - removeBtn.title = '移除圖片'; - removeBtn.style.cssText = ` - position: absolute; top: -8px; right: -8px; width: 20px; height: 20px; - border-radius: 50%; background: #f44336; color: white; border: none; - cursor: pointer; font-size: 12px; font-weight: bold; - display: flex; align-items: center; justify-content: center; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); transition: all 0.3s ease; z-index: 10; - `; - - // 添加刪除按鈕懸停效果 - removeBtn.addEventListener('mouseenter', function() { - removeBtn.style.background = '#d32f2f'; - removeBtn.style.transform = 'scale(1.1)'; - }); - removeBtn.addEventListener('mouseleave', function() { - removeBtn.style.background = '#f44336'; - removeBtn.style.transform = 'scale(1)'; - }); - - // 添加刪除功能 - removeBtn.addEventListener('click', function(e) { - e.preventDefault(); - e.stopPropagation(); - self.removeImage(index); - }); - - // 組裝元素 - imageInfo.appendChild(imageName); - imageInfo.appendChild(imageSize); - preview.appendChild(img); - preview.appendChild(imageInfo); - preview.appendChild(removeBtn); - - return preview; - }; /** - * 更新圖片計數顯示 + * 獲取圖片數據 */ - ImageHandler.prototype.updateImageCount = function() { - const count = this.images.length; - const countElements = document.querySelectorAll('.image-count'); - - countElements.forEach(function(element) { - element.textContent = count > 0 ? '(' + count + ')' : ''; - }); - - // 更新上傳區域的顯示狀態 - const uploadAreas = [ - Utils.safeQuerySelector('#feedbackImageUploadArea'), - Utils.safeQuerySelector('#combinedImageUploadArea') - ].filter(function(area) { - return area !== null; - }); - - uploadAreas.forEach(function(area) { - if (count > 0) { - area.classList.add('has-images'); - } else { - area.classList.remove('has-images'); - } - }); - }; - - /** - * 移除圖片 - */ - ImageHandler.prototype.removeImage = function(index) { - this.images.splice(index, 1); - this.updateImagePreview(); + ImageHandler.prototype.getImages = function() { + return this.fileUploadManager.getFiles(); }; /** * 清空所有圖片 */ ImageHandler.prototype.clearImages = function() { - this.images = []; - this.updateImagePreview(); - }; - - /** - * 獲取圖片數據 - */ - ImageHandler.prototype.getImages = function() { - return Utils.deepClone(this.images); + this.fileUploadManager.clearFiles(); }; /** @@ -437,19 +152,13 @@ */ ImageHandler.prototype.reinitialize = function(layoutMode) { console.log('🔄 重新初始化圖片處理功能...'); - + this.layoutMode = layoutMode; - this.removeImageEventListeners(); - this.initImageElements(); - - if (this.imageUploadArea && this.imageInput) { - this.setupImageEventListeners(); - console.log('✅ 圖片處理功能重新初始化完成'); - } else { - console.warn('⚠️ 圖片處理重新初始化失敗 - 缺少必要元素'); - } - - this.updateImagePreview(); + + // 重新初始化設定元素 + this.initImageSettingsElements(); + + console.log('✅ 圖片處理功能重新初始化完成'); }; /** @@ -458,7 +167,13 @@ ImageHandler.prototype.updateSettings = function(settings) { this.imageSizeLimit = settings.imageSizeLimit || 0; this.enableBase64Detail = settings.enableBase64Detail || false; - + + // 更新檔案上傳管理器設定 + this.fileUploadManager.updateSettings({ + imageSizeLimit: this.imageSizeLimit, + enableBase64Detail: this.enableBase64Detail + }); + // 同步到 UI 元素 if (this.imageSizeLimitSelect) { this.imageSizeLimitSelect.value = this.imageSizeLimit.toString(); @@ -472,14 +187,8 @@ * 清理資源 */ ImageHandler.prototype.cleanup = function() { - this.removeImageEventListeners(); - - if (this.pasteHandler) { - document.removeEventListener('paste', this.pasteHandler); - this.pasteHandler = null; - } - - this.clearImages(); + this.removeImageSettingsListeners(); + this.fileUploadManager.cleanup(); }; // 將 ImageHandler 加入命名空間 diff --git a/src/mcp_feedback_enhanced/web/static/js/modules/settings-manager.js b/src/mcp_feedback_enhanced/web/static/js/modules/settings-manager.js index 400bc96..6ac8e33 100644 --- a/src/mcp_feedback_enhanced/web/static/js/modules/settings-manager.js +++ b/src/mcp_feedback_enhanced/web/static/js/modules/settings-manager.js @@ -369,15 +369,22 @@ * 應用圖片設定 */ SettingsManager.prototype.applyImageSettings = function() { + // 更新所有圖片大小限制選擇器(包括設定頁籤中的) const imageSizeLimitSelects = document.querySelectorAll('[id$="ImageSizeLimit"]'); imageSizeLimitSelects.forEach(function(select) { select.value = this.currentSettings.imageSizeLimit.toString(); }.bind(this)); + // 更新所有 Base64 相容模式複選框(包括設定頁籤中的) const enableBase64DetailCheckboxes = document.querySelectorAll('[id$="EnableBase64Detail"]'); enableBase64DetailCheckboxes.forEach(function(checkbox) { checkbox.checked = this.currentSettings.enableBase64Detail; }.bind(this)); + + console.log('圖片設定已應用到 UI:', { + imageSizeLimit: this.currentSettings.imageSizeLimit, + enableBase64Detail: this.currentSettings.enableBase64Detail + }); }; @@ -424,6 +431,26 @@ }); }); + // 圖片設定 - 大小限制選擇器 + const settingsImageSizeLimit = Utils.safeQuerySelector('#settingsImageSizeLimit'); + if (settingsImageSizeLimit) { + settingsImageSizeLimit.addEventListener('change', function(e) { + const value = parseInt(e.target.value); + self.set('imageSizeLimit', value); + console.log('圖片大小限制已更新:', value); + }); + } + + // 圖片設定 - Base64 相容模式切換器 + const settingsEnableBase64Detail = Utils.safeQuerySelector('#settingsEnableBase64Detail'); + if (settingsEnableBase64Detail) { + settingsEnableBase64Detail.addEventListener('change', function(e) { + const value = e.target.checked; + self.set('enableBase64Detail', value); + console.log('Base64 相容模式已更新:', value); + }); + } + // 重置設定 const resetBtn = Utils.safeQuerySelector('#resetSettingsBtn'); if (resetBtn) { @@ -435,7 +462,6 @@ }); } - }; // 將 SettingsManager 加入命名空間 diff --git a/src/mcp_feedback_enhanced/web/templates/components/image-upload.html b/src/mcp_feedback_enhanced/web/templates/components/image-upload.html index 24dd397..0b6d1a7 100644 --- a/src/mcp_feedback_enhanced/web/templates/components/image-upload.html +++ b/src/mcp_feedback_enhanced/web/templates/components/image-upload.html @@ -19,34 +19,7 @@ {% set min_height = min_height or "120px" %} {% set upload_text = upload_text or "📎 點擊選擇圖片或拖放圖片到此處
支援 PNG、JPG、JPEG、GIF、BMP、WebP 等格式" %} - -
-
- ⚙️ 圖片設定 -
-
- - -
-
- - ⚠️ 會增加傳輸量 -
-
- 啟用後會在文字中包含完整的 Base64 圖片資料,提升部分 AI 模型的相容性 -
-
-
-
+
diff --git a/src/mcp_feedback_enhanced/web/templates/feedback.html b/src/mcp_feedback_enhanced/web/templates/feedback.html index 7199fe6..d11d35d 100644 --- a/src/mcp_feedback_enhanced/web/templates/feedback.html +++ b/src/mcp_feedback_enhanced/web/templates/feedback.html @@ -743,6 +743,46 @@
+ +
+
+

🖼️ 圖片設定

+
+
+
+
+
圖片大小限制
+
+ 設定上傳圖片的最大檔案大小限制 +
+
+
+ +
+
+
+
+
Base64 相容模式
+
+ 啟用後會在文字中包含完整的 Base64 圖片資料,提升與某些 AI 模型的相容性 +
+
⚠️ 會增加傳輸量
+
+
+ +
+
+
+
+
@@ -894,6 +934,7 @@ +