🎨 將工作區的圖片設定轉移到設定頁籤

This commit is contained in:
Minidoracat 2025-06-13 15:20:26 +08:00
parent 7dc9aa510a
commit e8d4068229
9 changed files with 786 additions and 494 deletions

View File

@ -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",

View File

@ -320,6 +320,7 @@
"settings": {
"title": "图片设置",
"sizeLimit": "图片大小限制",
"sizeLimitDesc": "设定上传图片的最大文件大小限制",
"sizeLimitOptions": {
"unlimited": "无限制",
"1mb": "1MB",

View File

@ -325,6 +325,7 @@
"settings": {
"title": "圖片設定",
"sizeLimit": "圖片大小限制",
"sizeLimitDesc": "設定上傳圖片的最大檔案大小限制",
"sizeLimitOptions": {
"unlimited": "無限制",
"1mb": "1MB",

View File

@ -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;

View File

@ -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 模組載入完成');
})();

View File

@ -18,29 +18,37 @@
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 建構函數初始化完成');
}
/**
@ -49,9 +57,11 @@
ImageHandler.prototype.init = function() {
console.log('🖼️ 開始初始化圖片處理功能...');
this.initImageElements();
this.setupImageEventListeners();
this.setupGlobalPasteHandler();
// 初始化設定元素
this.initImageSettingsElements();
// 初始化檔案上傳管理器
this.fileUploadManager.initialize();
console.log('✅ 圖片處理功能初始化完成');
};
@ -59,137 +69,33 @@
/**
* 動態初始化圖片相關元素
*/
ImageHandler.prototype.initImageElements = function() {
const prefix = this.layoutMode && this.layoutMode.startsWith('combined') ? 'combined' : 'feedback';
ImageHandler.prototype.initImageSettingsElements = function() {
// 查找設定頁籤中的圖片設定元素
this.imageSizeLimitSelect = Utils.safeQuerySelector('#settingsImageSizeLimit');
this.enableBase64DetailCheckbox = Utils.safeQuerySelector('#settingsEnableBase64Detail');
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.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;
}
};
@ -199,237 +105,46 @@
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();
};
/**
@ -439,17 +154,11 @@
console.log('🔄 重新初始化圖片處理功能...');
this.layoutMode = layoutMode;
this.removeImageEventListeners();
this.initImageElements();
if (this.imageUploadArea && this.imageInput) {
this.setupImageEventListeners();
console.log('✅ 圖片處理功能重新初始化完成');
} else {
console.warn('⚠️ 圖片處理重新初始化失敗 - 缺少必要元素');
}
// 重新初始化設定元素
this.initImageSettingsElements();
this.updateImagePreview();
console.log('✅ 圖片處理功能重新初始化完成');
};
/**
@ -459,6 +168,12 @@
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 加入命名空間

View File

@ -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 加入命名空間

View File

@ -19,34 +19,7 @@
{% set min_height = min_height or "120px" %}
{% set upload_text = upload_text or "📎 點擊選擇圖片或拖放圖片到此處<br><small>支援 PNG、JPG、JPEG、GIF、BMP、WebP 等格式</small>" %}
<!-- 圖片設定區域 -->
<div class="input-group" style="margin-bottom: 12px;">
<details class="image-settings-details">
<summary class="image-settings-summary" data-i18n="images.settings.title">⚙️ 圖片設定</summary>
<div class="image-settings-content">
<div class="image-setting-row">
<label class="image-setting-label" data-i18n="images.settings.sizeLimit">圖片大小限制:</label>
<select id="{{ id_prefix }}ImageSizeLimit" class="image-setting-select">
<option value="0" data-i18n="images.settings.sizeLimitOptions.unlimited">無限制</option>
<option value="1048576" data-i18n="images.settings.sizeLimitOptions.1mb">1MB</option>
<option value="3145728" data-i18n="images.settings.sizeLimitOptions.3mb">3MB</option>
<option value="5242880" data-i18n="images.settings.sizeLimitOptions.5mb">5MB</option>
</select>
</div>
<div class="image-setting-row">
<label class="image-setting-checkbox-container">
<input type="checkbox" id="{{ id_prefix }}EnableBase64Detail" class="image-setting-checkbox">
<span class="image-setting-checkmark"></span>
<span class="image-setting-label" data-i18n="images.settings.base64Detail">Base64 相容模式</span>
</label>
<small class="image-setting-help" data-i18n="images.settings.base64Warning">⚠️ 會增加傳輸量</small>
</div>
<div class="image-setting-help-text" data-i18n="images.settings.base64DetailHelp">
啟用後會在文字中包含完整的 Base64 圖片資料,提升部分 AI 模型的相容性
</div>
</div>
</details>
</div>
<div class="input-group">
<label class="input-label" data-i18n="feedback.imageLabel">{{ label_text }}</label>

View File

@ -743,6 +743,46 @@
</div>
</div>
<!-- 圖片設定卡片 -->
<div class="settings-card">
<div class="settings-card-header">
<h3 class="settings-card-title" data-i18n="images.settings.title">🖼️ 圖片設定</h3>
</div>
<div class="settings-card-body">
<div class="setting-item">
<div class="setting-info">
<div class="setting-label" data-i18n="images.settings.sizeLimit">圖片大小限制</div>
<div class="setting-description" data-i18n="images.settings.sizeLimitDesc">
設定上傳圖片的最大檔案大小限制
</div>
</div>
<div class="image-size-limit-selector">
<select id="settingsImageSizeLimit" class="image-size-limit-select">
<option value="0" data-i18n="images.settings.sizeLimitOptions.unlimited">無限制</option>
<option value="1048576" data-i18n="images.settings.sizeLimitOptions.1mb">1MB</option>
<option value="3145728" data-i18n="images.settings.sizeLimitOptions.3mb">3MB</option>
<option value="5242880" data-i18n="images.settings.sizeLimitOptions.5mb">5MB</option>
</select>
</div>
</div>
<div class="setting-item">
<div class="setting-info">
<div class="setting-label" data-i18n="images.settings.base64Detail">Base64 相容模式</div>
<div class="setting-description" data-i18n="images.settings.base64DetailHelp">
啟用後會在文字中包含完整的 Base64 圖片資料,提升與某些 AI 模型的相容性
</div>
<div class="setting-warning" data-i18n="images.settings.base64Warning">⚠️ 會增加傳輸量</div>
</div>
<div class="base64-toggle-container">
<label class="toggle-switch">
<input type="checkbox" id="settingsEnableBase64Detail" class="toggle-input">
<span class="toggle-slider"></span>
</label>
</div>
</div>
</div>
</div>
<!-- 提示詞管理卡片 -->
<div class="settings-card">
<div class="settings-card-header">
@ -894,6 +934,7 @@
<script src="/static/js/modules/websocket-manager.js?v=2025010510"></script>
<script src="/static/js/modules/connection-monitor.js?v=2025010510"></script>
<script src="/static/js/modules/session-manager.js?v=2025010510"></script>
<script src="/static/js/modules/file-upload-manager.js?v=2025010510"></script>
<script src="/static/js/modules/image-handler.js?v=2025010510"></script>
<script src="/static/js/modules/settings-manager.js?v=2025010510"></script>
<script src="/static/js/modules/ui-manager.js?v=2025010510"></script>