Files
hr-position-system/error_handler.js
DonaldFang 方士碩 29c1633e49 Initial commit: HR Position System
- Database schema with MySQL support
- LLM API integration (Gemini 2.5 Flash, DeepSeek, OpenAI)
- Error handling with copyable error messages
- CORS fix for API calls
- Complete setup documentation

🤖 Generated with Claude Code
https://claude.com/claude-code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 00:46:53 +08:00

560 lines
17 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Global Error Handler for HR Position System
* Provides unified error handling and user-friendly error dialogs
*/
class ErrorHandler {
constructor() {
this.init();
}
init() {
// Create error modal container if not exists
if (!document.getElementById('error-modal-container')) {
const container = document.createElement('div');
container.id = 'error-modal-container';
document.body.appendChild(container);
}
// Add error handler CSS
this.injectStyles();
// Setup global error handlers
this.setupGlobalHandlers();
}
injectStyles() {
if (document.getElementById('error-handler-styles')) {
return;
}
const style = document.createElement('style');
style.id = 'error-handler-styles';
style.textContent = `
.error-modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
animation: fadeIn 0.3s ease;
}
.error-modal {
background: white;
border-radius: 12px;
padding: 0;
max-width: 500px;
width: 90%;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
animation: slideDown 0.3s ease;
overflow: hidden;
}
.error-modal-header {
padding: 20px;
border-bottom: 2px solid #f0f0f0;
display: flex;
align-items: center;
gap: 15px;
}
.error-modal-header.error {
background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
color: white;
border-bottom-color: #c0392b;
}
.error-modal-header.warning {
background: linear-gradient(135deg, #f39c12 0%, #e67e22 100%);
color: white;
border-bottom-color: #e67e22;
}
.error-modal-header.info {
background: linear-gradient(135deg, #3498db 0%, #2980b9 100%);
color: white;
border-bottom-color: #2980b9;
}
.error-modal-header.success {
background: linear-gradient(135deg, #27ae60 0%, #229954 100%);
color: white;
border-bottom-color: #229954;
}
.error-icon {
font-size: 2rem;
line-height: 1;
}
.error-title {
flex: 1;
font-size: 1.3rem;
font-weight: 600;
margin: 0;
}
.error-modal-body {
padding: 25px;
max-height: 400px;
overflow-y: auto;
}
.error-message {
color: #333;
line-height: 1.6;
margin-bottom: 15px;
font-size: 1rem;
}
.error-details {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 6px;
padding: 15px;
margin-top: 15px;
font-family: 'Courier New', monospace;
font-size: 0.85rem;
color: #666;
max-height: 200px;
overflow-y: auto;
white-space: pre-wrap;
word-break: break-word;
}
.error-modal-footer {
padding: 15px 25px;
border-top: 1px solid #f0f0f0;
display: flex;
justify-content: flex-end;
gap: 10px;
}
.error-btn {
padding: 10px 25px;
border: none;
border-radius: 6px;
font-size: 0.95rem;
font-weight: 500;
cursor: pointer;
transition: all 0.3s;
font-family: 'Noto Sans TC', sans-serif;
}
.error-btn-primary {
background: #3498db;
color: white;
}
.error-btn-primary:hover {
background: #2980b9;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(52, 152, 219, 0.3);
}
.error-btn-secondary {
background: #95a5a6;
color: white;
}
.error-btn-secondary:hover {
background: #7f8c8d;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideDown {
from {
transform: translateY(-50px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.error-toast {
position: fixed;
top: 20px;
right: 20px;
background: white;
border-radius: 8px;
padding: 15px 20px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
display: flex;
align-items: center;
gap: 12px;
z-index: 10001;
animation: slideInRight 0.3s ease;
max-width: 400px;
}
.error-toast.error {
border-left: 4px solid #e74c3c;
}
.error-toast.warning {
border-left: 4px solid #f39c12;
}
.error-toast.info {
border-left: 4px solid #3498db;
}
.error-toast.success {
border-left: 4px solid #27ae60;
}
.error-toast-icon {
font-size: 1.5rem;
}
.error-toast-content {
flex: 1;
}
.error-toast-title {
font-weight: 600;
margin-bottom: 5px;
color: #333;
}
.error-toast-message {
font-size: 0.9rem;
color: #666;
}
.error-toast-close {
background: none;
border: none;
font-size: 1.5rem;
color: #999;
cursor: pointer;
padding: 0;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: all 0.2s;
}
.error-toast-close:hover {
background: #f0f0f0;
color: #666;
}
@keyframes slideInRight {
from {
transform: translateX(400px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideOutRight {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(400px);
opacity: 0;
}
}
`;
document.head.appendChild(style);
}
setupGlobalHandlers() {
// Handle uncaught errors
window.addEventListener('error', (event) => {
this.handleError({
title: '執行錯誤',
message: event.message,
details: `檔案: ${event.filename}\n行號: ${event.lineno}:${event.colno}\n錯誤: ${event.error?.stack || event.message}`
});
});
// Handle unhandled promise rejections
window.addEventListener('unhandledrejection', (event) => {
this.handleError({
title: 'Promise 錯誤',
message: '發生未處理的 Promise 錯誤',
details: event.reason?.stack || event.reason
});
});
}
/**
* Show error modal
* @param {Object} options - Error options
* @param {string} options.title - Error title
* @param {string} options.message - Error message
* @param {string} options.details - Error details (optional)
* @param {string} options.type - Error type: error, warning, info, success (default: error)
* @param {Function} options.onClose - Callback when modal closes
*/
showError(options) {
const {
title = '錯誤',
message = '發生未知錯誤',
details = null,
type = 'error',
onClose = null
} = options;
const icons = {
error: '❌',
warning: '⚠️',
info: '',
success: '✅'
};
const container = document.getElementById('error-modal-container');
const modal = document.createElement('div');
modal.className = 'error-modal-overlay';
modal.innerHTML = `
<div class="error-modal">
<div class="error-modal-header ${type}">
<div class="error-icon">${icons[type]}</div>
<h3 class="error-title">${this.escapeHtml(title)}</h3>
</div>
<div class="error-modal-body">
<div class="error-message">${this.escapeHtml(message)}</div>
${details ? `<div class="error-details">${this.escapeHtml(details)}</div>` : ''}
</div>
<div class="error-modal-footer">
<button class="error-btn error-btn-primary" onclick="window.errorHandler.closeModal(this)">
確定
</button>
</div>
</div>
`;
container.appendChild(modal);
// Store close callback
modal._onClose = onClose;
// Close on overlay click
modal.addEventListener('click', (e) => {
if (e.target === modal) {
this.closeModal(modal.querySelector('.error-btn'));
}
});
}
/**
* Show toast notification
* @param {Object} options - Toast options
* @param {string} options.title - Toast title
* @param {string} options.message - Toast message
* @param {string} options.type - Toast type: error, warning, info, success (default: info)
* @param {number} options.duration - Duration in ms (default: 3000)
*/
showToast(options) {
const {
title = '',
message = '',
type = 'info',
duration = 3000
} = options;
const icons = {
error: '❌',
warning: '⚠️',
info: '',
success: '✅'
};
const toast = document.createElement('div');
toast.className = `error-toast ${type}`;
toast.innerHTML = `
<div class="error-toast-icon">${icons[type]}</div>
<div class="error-toast-content">
${title ? `<div class="error-toast-title">${this.escapeHtml(title)}</div>` : ''}
<div class="error-toast-message">${this.escapeHtml(message)}</div>
</div>
<button class="error-toast-close" onclick="this.parentElement.remove()">×</button>
`;
document.body.appendChild(toast);
// Auto remove after duration
if (duration > 0) {
setTimeout(() => {
toast.style.animation = 'slideOutRight 0.3s ease';
setTimeout(() => toast.remove(), 300);
}, duration);
}
}
closeModal(button) {
const modal = button.closest('.error-modal-overlay');
if (modal) {
const onClose = modal._onClose;
modal.style.animation = 'fadeOut 0.3s ease';
setTimeout(() => {
modal.remove();
if (onClose && typeof onClose === 'function') {
onClose();
}
}, 300);
}
}
/**
* Handle API errors
* @param {Error} error - Error object
* @param {Object} options - Additional options
*/
async handleAPIError(error, options = {}) {
const { showModal = true, showToast = false } = options;
let message = '發生 API 錯誤';
let details = null;
if (error.response) {
// Server responded with error
const data = error.response.data;
message = data.error || data.message || `HTTP ${error.response.status} 錯誤`;
details = JSON.stringify(data, null, 2);
} else if (error.request) {
// No response received
message = '無法連接到伺服器,請檢查網路連線';
details = error.message;
} else {
// Request setup error
message = error.message;
details = error.stack;
}
if (showModal) {
this.showError({
title: 'API 錯誤',
message,
details,
type: 'error'
});
}
if (showToast) {
this.showToast({
title: 'API 錯誤',
message,
type: 'error'
});
}
}
/**
* Handle general errors
* @param {Object} options - Error options
*/
handleError(options) {
console.error('[ErrorHandler]', options);
this.showError(options);
}
/**
* Escape HTML to prevent XSS
* @param {string} text - Text to escape
* @returns {string} - Escaped text
*/
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
/**
* Confirm dialog
* @param {Object} options - Confirm options
* @param {string} options.title - Title
* @param {string} options.message - Message
* @param {string} options.confirmText - Confirm button text
* @param {string} options.cancelText - Cancel button text
* @param {Function} options.onConfirm - Confirm callback
* @param {Function} options.onCancel - Cancel callback
*/
confirm(options) {
const {
title = '確認',
message = '確定要執行此操作嗎?',
confirmText = '確定',
cancelText = '取消',
onConfirm = null,
onCancel = null
} = options;
const container = document.getElementById('error-modal-container');
const modal = document.createElement('div');
modal.className = 'error-modal-overlay';
modal.innerHTML = `
<div class="error-modal">
<div class="error-modal-header info">
<div class="error-icon">❓</div>
<h3 class="error-title">${this.escapeHtml(title)}</h3>
</div>
<div class="error-modal-body">
<div class="error-message">${this.escapeHtml(message)}</div>
</div>
<div class="error-modal-footer">
<button class="error-btn error-btn-secondary" data-action="cancel">
${this.escapeHtml(cancelText)}
</button>
<button class="error-btn error-btn-primary" data-action="confirm">
${this.escapeHtml(confirmText)}
</button>
</div>
</div>
`;
container.appendChild(modal);
// Handle button clicks
modal.querySelectorAll('[data-action]').forEach(btn => {
btn.addEventListener('click', () => {
const action = btn.getAttribute('data-action');
modal.remove();
if (action === 'confirm' && onConfirm) {
onConfirm();
} else if (action === 'cancel' && onCancel) {
onCancel();
}
});
});
}
}
// Initialize global error handler
window.errorHandler = new ErrorHandler();
// Export for module usage
if (typeof module !== 'undefined' && module.exports) {
module.exports = ErrorHandler;
}