mirror of
https://github.com/Minidoracat/mcp-feedback-enhanced.git
synced 2025-07-27 02:22:26 +08:00
374 lines
13 KiB
JavaScript
374 lines
13 KiB
JavaScript
/**
|
||
* MCP Feedback Enhanced - 工具模組
|
||
* ================================
|
||
*
|
||
* 提供共用的工具函數和常數定義
|
||
*/
|
||
|
||
(function() {
|
||
'use strict';
|
||
|
||
// 確保命名空間存在
|
||
window.MCPFeedback = window.MCPFeedback || {};
|
||
window.MCPFeedback.Utils = window.MCPFeedback.Utils || {};
|
||
|
||
/**
|
||
* 工具函數模組 - 擴展現有的 Utils 物件
|
||
*/
|
||
Object.assign(window.MCPFeedback.Utils, {
|
||
|
||
/**
|
||
* 格式化檔案大小
|
||
* @param {number} bytes - 位元組數
|
||
* @returns {string} 格式化後的檔案大小
|
||
*/
|
||
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];
|
||
},
|
||
|
||
/**
|
||
* 生成唯一 ID
|
||
* @param {string} prefix - ID 前綴
|
||
* @returns {string} 唯一 ID
|
||
*/
|
||
generateId: function(prefix) {
|
||
prefix = prefix || 'id';
|
||
return prefix + '_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
||
},
|
||
|
||
/**
|
||
* 深度複製物件
|
||
* @param {Object} obj - 要複製的物件
|
||
* @returns {Object} 複製後的物件
|
||
*/
|
||
deepClone: function(obj) {
|
||
if (obj === null || typeof obj !== 'object') return obj;
|
||
if (obj instanceof Date) return new Date(obj.getTime());
|
||
if (obj instanceof Array) return obj.map(item => this.deepClone(item));
|
||
if (typeof obj === 'object') {
|
||
const clonedObj = {};
|
||
for (const key in obj) {
|
||
if (obj.hasOwnProperty(key)) {
|
||
clonedObj[key] = this.deepClone(obj[key]);
|
||
}
|
||
}
|
||
return clonedObj;
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 防抖函數
|
||
* @param {Function} func - 要防抖的函數
|
||
* @param {number} wait - 等待時間(毫秒)
|
||
* @returns {Function} 防抖後的函數
|
||
*/
|
||
debounce: function(func, wait) {
|
||
let timeout;
|
||
return function executedFunction() {
|
||
const later = () => {
|
||
clearTimeout(timeout);
|
||
func.apply(this, arguments);
|
||
};
|
||
clearTimeout(timeout);
|
||
timeout = setTimeout(later, wait);
|
||
};
|
||
},
|
||
|
||
/**
|
||
* 節流函數
|
||
* @param {Function} func - 要節流的函數
|
||
* @param {number} limit - 限制時間(毫秒)
|
||
* @returns {Function} 節流後的函數
|
||
*/
|
||
throttle: function(func, limit) {
|
||
let inThrottle;
|
||
return function() {
|
||
const args = arguments;
|
||
const context = this;
|
||
if (!inThrottle) {
|
||
func.apply(context, args);
|
||
inThrottle = true;
|
||
setTimeout(() => inThrottle = false, limit);
|
||
}
|
||
};
|
||
},
|
||
|
||
/**
|
||
* 安全的 JSON 解析
|
||
* @param {string} jsonString - JSON 字串
|
||
* @param {*} defaultValue - 預設值
|
||
* @returns {*} 解析結果或預設值
|
||
*/
|
||
safeJsonParse: function(jsonString, defaultValue) {
|
||
try {
|
||
return JSON.parse(jsonString);
|
||
} catch (error) {
|
||
console.warn('JSON 解析失敗:', error);
|
||
return defaultValue;
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 檢查元素是否存在
|
||
* @param {string} selector - CSS 選擇器
|
||
* @returns {boolean} 元素是否存在
|
||
*/
|
||
elementExists: function(selector) {
|
||
return document.querySelector(selector) !== null;
|
||
},
|
||
|
||
/**
|
||
* 從右側截斷路徑,保留最後幾個目錄層級
|
||
* @param {string} path - 完整路徑
|
||
* @param {number} maxLevels - 保留的最大目錄層級數(默認2)
|
||
* @param {number} maxLength - 最大顯示長度(默認40)
|
||
* @returns {object} 包含 truncated(截斷後的路徑)和 isTruncated(是否被截斷)
|
||
*/
|
||
truncatePathFromRight: function(path, maxLevels, maxLength) {
|
||
maxLevels = maxLevels || 2;
|
||
maxLength = maxLength || 40;
|
||
|
||
if (!path || typeof path !== 'string') {
|
||
return { truncated: path || '', isTruncated: false };
|
||
}
|
||
|
||
// 如果路徑長度小於最大長度,直接返回
|
||
if (path.length <= maxLength) {
|
||
return { truncated: path, isTruncated: false };
|
||
}
|
||
|
||
// 統一路徑分隔符為反斜線(Windows風格)
|
||
const normalizedPath = path.replace(/\//g, '\\');
|
||
|
||
// 分割路徑
|
||
const parts = normalizedPath.split('\\').filter(part => part.length > 0);
|
||
|
||
if (parts.length <= maxLevels) {
|
||
return { truncated: normalizedPath, isTruncated: false };
|
||
}
|
||
|
||
// 取最後幾個層級
|
||
const lastParts = parts.slice(-maxLevels);
|
||
const truncatedPath = '...' + '\\' + lastParts.join('\\');
|
||
|
||
return {
|
||
truncated: truncatedPath,
|
||
isTruncated: true
|
||
};
|
||
},
|
||
|
||
/**
|
||
* 複製文字到剪貼板(統一的複製功能)
|
||
* @param {string} text - 要複製的文字
|
||
* @param {string} successMessage - 成功提示訊息
|
||
* @param {string} errorMessage - 錯誤提示訊息
|
||
* @returns {Promise<boolean>} 複製是否成功
|
||
*/
|
||
copyToClipboard: function(text, successMessage, errorMessage) {
|
||
successMessage = successMessage || '已複製到剪貼板';
|
||
errorMessage = errorMessage || '複製失敗';
|
||
|
||
return new Promise(function(resolve) {
|
||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||
// 使用現代 Clipboard API
|
||
navigator.clipboard.writeText(text).then(function() {
|
||
if (window.MCPFeedback && window.MCPFeedback.Utils && window.MCPFeedback.Utils.showMessage) {
|
||
window.MCPFeedback.Utils.showMessage(successMessage, window.MCPFeedback.Utils.CONSTANTS.MESSAGE_SUCCESS);
|
||
}
|
||
resolve(true);
|
||
}).catch(function(err) {
|
||
console.error('Clipboard API 複製失敗:', err);
|
||
// 回退到舊方法
|
||
const success = window.MCPFeedback.Utils.fallbackCopyToClipboard(text);
|
||
if (success) {
|
||
if (window.MCPFeedback && window.MCPFeedback.Utils && window.MCPFeedback.Utils.showMessage) {
|
||
window.MCPFeedback.Utils.showMessage(successMessage, window.MCPFeedback.Utils.CONSTANTS.MESSAGE_SUCCESS);
|
||
}
|
||
resolve(true);
|
||
} else {
|
||
if (window.MCPFeedback && window.MCPFeedback.Utils && window.MCPFeedback.Utils.showMessage) {
|
||
window.MCPFeedback.Utils.showMessage(errorMessage, window.MCPFeedback.Utils.CONSTANTS.MESSAGE_ERROR);
|
||
}
|
||
resolve(false);
|
||
}
|
||
});
|
||
} else {
|
||
// 直接使用回退方法
|
||
const success = window.MCPFeedback.Utils.fallbackCopyToClipboard(text);
|
||
if (success) {
|
||
if (window.MCPFeedback && window.MCPFeedback.Utils && window.MCPFeedback.Utils.showMessage) {
|
||
window.MCPFeedback.Utils.showMessage(successMessage, window.MCPFeedback.Utils.CONSTANTS.MESSAGE_SUCCESS);
|
||
}
|
||
resolve(true);
|
||
} else {
|
||
if (window.MCPFeedback && window.MCPFeedback.Utils && window.MCPFeedback.Utils.showMessage) {
|
||
window.MCPFeedback.Utils.showMessage(errorMessage, window.MCPFeedback.Utils.CONSTANTS.MESSAGE_ERROR);
|
||
}
|
||
resolve(false);
|
||
}
|
||
}
|
||
});
|
||
},
|
||
|
||
/**
|
||
* 回退的複製到剪貼板方法
|
||
* @param {string} text - 要複製的文字
|
||
* @returns {boolean} 複製是否成功
|
||
*/
|
||
fallbackCopyToClipboard: function(text) {
|
||
try {
|
||
const textArea = document.createElement('textarea');
|
||
textArea.value = text;
|
||
textArea.style.position = 'fixed';
|
||
textArea.style.left = '-999999px';
|
||
textArea.style.top = '-999999px';
|
||
document.body.appendChild(textArea);
|
||
textArea.focus();
|
||
textArea.select();
|
||
|
||
const successful = document.execCommand('copy');
|
||
document.body.removeChild(textArea);
|
||
|
||
return successful;
|
||
} catch (err) {
|
||
console.error('回退複製方法失敗:', err);
|
||
return false;
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 安全的元素查詢
|
||
* @param {string} selector - CSS 選擇器
|
||
* @param {Element} context - 查詢上下文(可選)
|
||
* @returns {Element|null} 找到的元素或 null
|
||
*/
|
||
safeQuerySelector: function(selector, context) {
|
||
try {
|
||
const root = context || document;
|
||
return root.querySelector(selector);
|
||
} catch (error) {
|
||
console.warn('元素查詢失敗:', selector, error);
|
||
return null;
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 顯示訊息提示
|
||
* @param {string} message - 訊息內容
|
||
* @param {string} type - 訊息類型 (success, error, warning, info)
|
||
* @param {number} duration - 顯示時間(毫秒)
|
||
*/
|
||
showMessage: function(message, type, duration) {
|
||
type = type || 'info';
|
||
duration = duration || 3000;
|
||
|
||
// 創建訊息元素
|
||
const messageDiv = document.createElement('div');
|
||
messageDiv.className = 'message message-' + type;
|
||
messageDiv.style.cssText = `
|
||
position: fixed;
|
||
top: 80px;
|
||
right: 20px;
|
||
z-index: 1001;
|
||
padding: 12px 20px;
|
||
background: var(--${type === 'error' ? 'error' : type === 'warning' ? 'warning' : 'success'}-color, #4CAF50);
|
||
color: white;
|
||
border-radius: 6px;
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||
max-width: 300px;
|
||
word-wrap: break-word;
|
||
transition: opacity 0.3s ease;
|
||
`;
|
||
messageDiv.textContent = message;
|
||
|
||
document.body.appendChild(messageDiv);
|
||
|
||
// 自動移除
|
||
setTimeout(() => {
|
||
if (messageDiv.parentNode) {
|
||
messageDiv.style.opacity = '0';
|
||
setTimeout(() => {
|
||
if (messageDiv.parentNode) {
|
||
messageDiv.parentNode.removeChild(messageDiv);
|
||
}
|
||
}, 300);
|
||
}
|
||
}, duration);
|
||
},
|
||
|
||
/**
|
||
* 檢查 WebSocket 是否可用
|
||
* @returns {boolean} WebSocket 是否可用
|
||
*/
|
||
isWebSocketSupported: function() {
|
||
return 'WebSocket' in window;
|
||
},
|
||
|
||
/**
|
||
* 檢查 localStorage 是否可用
|
||
* @returns {boolean} localStorage 是否可用
|
||
*/
|
||
isLocalStorageSupported: function() {
|
||
try {
|
||
const test = '__localStorage_test__';
|
||
localStorage.setItem(test, test);
|
||
localStorage.removeItem(test);
|
||
return true;
|
||
} catch (e) {
|
||
return false;
|
||
}
|
||
},
|
||
|
||
/**
|
||
* HTML 轉義函數
|
||
* @param {string} text - 要轉義的文字
|
||
* @returns {string} 轉義後的文字
|
||
*/
|
||
escapeHtml: function(text) {
|
||
if (typeof text !== 'string') {
|
||
return text;
|
||
}
|
||
|
||
const div = document.createElement('div');
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
},
|
||
|
||
/**
|
||
* 常數定義
|
||
*/
|
||
CONSTANTS: {
|
||
// WebSocket 狀態
|
||
WS_CONNECTING: 0,
|
||
WS_OPEN: 1,
|
||
WS_CLOSING: 2,
|
||
WS_CLOSED: 3,
|
||
|
||
// 回饋狀態
|
||
FEEDBACK_WAITING: 'waiting_for_feedback',
|
||
FEEDBACK_SUBMITTED: 'feedback_submitted',
|
||
FEEDBACK_PROCESSING: 'processing',
|
||
|
||
// 預設設定
|
||
DEFAULT_HEARTBEAT_FREQUENCY: 30000,
|
||
DEFAULT_TAB_HEARTBEAT_FREQUENCY: 5000,
|
||
DEFAULT_RECONNECT_DELAY: 1000,
|
||
MAX_RECONNECT_ATTEMPTS: 5,
|
||
TAB_EXPIRED_THRESHOLD: 30000,
|
||
|
||
// 訊息類型
|
||
MESSAGE_SUCCESS: 'success',
|
||
MESSAGE_ERROR: 'error',
|
||
MESSAGE_WARNING: 'warning',
|
||
MESSAGE_INFO: 'info'
|
||
}
|
||
});
|
||
|
||
console.log('✅ Utils 模組載入完成');
|
||
|
||
})();
|