2025-06-10 07:19:47 +08:00
|
|
|
|
/**
|
|
|
|
|
* MCP Feedback Enhanced - 工具模組
|
|
|
|
|
* ================================
|
|
|
|
|
*
|
|
|
|
|
* 提供共用的工具函數和常數定義
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
(function() {
|
|
|
|
|
'use strict';
|
|
|
|
|
|
|
|
|
|
// 確保命名空間存在
|
|
|
|
|
window.MCPFeedback = window.MCPFeedback || {};
|
2025-06-13 06:47:11 +08:00
|
|
|
|
window.MCPFeedback.Utils = window.MCPFeedback.Utils || {};
|
2025-06-10 07:19:47 +08:00
|
|
|
|
|
|
|
|
|
/**
|
2025-06-13 06:47:11 +08:00
|
|
|
|
* 工具函數模組 - 擴展現有的 Utils 物件
|
2025-06-10 07:19:47 +08:00
|
|
|
|
*/
|
2025-06-13 06:47:11 +08:00
|
|
|
|
Object.assign(window.MCPFeedback.Utils, {
|
2025-06-10 07:19:47 +08:00
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 格式化檔案大小
|
|
|
|
|
* @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} 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;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
2025-06-13 13:43:27 +08:00
|
|
|
|
/**
|
|
|
|
|
* 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;
|
|
|
|
|
},
|
|
|
|
|
|
2025-06-10 07:19:47 +08:00
|
|
|
|
/**
|
|
|
|
|
* 常數定義
|
|
|
|
|
*/
|
|
|
|
|
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'
|
|
|
|
|
}
|
2025-06-13 06:47:11 +08:00
|
|
|
|
});
|
2025-06-10 07:19:47 +08:00
|
|
|
|
|
|
|
|
|
console.log('✅ Utils 模組載入完成');
|
|
|
|
|
|
|
|
|
|
})();
|