Files
hr-position-system/js/utils.js
DonaldFang 方士碩 880c23b844 refactor: 建立核心 JavaScript 模組
- 建立 js 目錄
- 分離核心模組:
  * config.js - API 端點、常數、資料對應表
  * utils.js - XSS 防護、表單工具、Toast、錯誤處理
  * api.js - LLM API、Position API、CSV API

 使用 ES6 Modules 架構

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-05 14:17:41 +08:00

207 lines
6.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

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.

/**
* Utilities - 工具函式
* 包含 XSS 防護、表單欄位工具、UI 回饋工具
*/
import { TOAST_DURATION } from './config.js';
// ==================== XSS 防護工具 ====================
/**
* 消毒 HTML 字串,防止 XSS 攻擊
* @param {string} str - 需要消毒的字串
* @returns {string} - 安全的字串
*/
export function sanitizeHTML(str) {
if (str === null || str === undefined) return '';
const temp = document.createElement('div');
temp.textContent = str;
return temp.innerHTML;
}
/**
* 安全設定元素文字內容
* @param {HTMLElement} element - 目標元素
* @param {string} text - 文字內容
*/
export function safeSetText(element, text) {
if (element) {
element.textContent = text;
}
}
// ==================== 表單欄位工具 ====================
/**
* 只在欄位為空時填入值
* @param {string} elementId - DOM 元素 ID
* @param {string} value - 要填入的值
* @returns {boolean} - 是否有填入值
*/
export function fillIfEmpty(elementId, value) {
const el = document.getElementById(elementId);
if (el && !el.value.trim() && value) {
el.value = value;
return true;
}
return false;
}
/**
* 獲取欄位當前值
* @param {string} elementId - DOM 元素 ID
* @returns {string} - 欄位值(已 trim
*/
export function getFieldValue(elementId) {
const el = document.getElementById(elementId);
return el ? el.value.trim() : '';
}
/**
* 獲取空白欄位列表
* @param {string[]} fieldIds - 欄位 ID 陣列
* @returns {string[]} - 空白欄位 ID 陣列
*/
export function getEmptyFields(fieldIds) {
return fieldIds.filter(id => !getFieldValue(id));
}
// ==================== UI 回饋工具 ====================
/**
* 顯示 Toast 提示訊息
* @param {string} message - 訊息內容
* @param {number} duration - 顯示時長(毫秒),預設 3000
*/
export function showToast(message, duration = TOAST_DURATION) {
const existingToast = document.querySelector('.toast');
if (existingToast) {
existingToast.remove();
}
const toast = document.createElement('div');
toast.className = 'toast';
toast.innerHTML = `
<svg viewBox="0 0 24 24">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
</svg>
<span>${sanitizeHTML(message)}</span>
`;
document.body.appendChild(toast);
setTimeout(() => toast.classList.add('show'), 10);
setTimeout(() => {
toast.classList.remove('show');
setTimeout(() => toast.remove(), 300);
}, duration);
}
/**
* 設定按鈕載入狀態
* @param {HTMLElement} btn - 按鈕元素
* @param {boolean} loading - 是否載入中
*/
export function setButtonLoading(btn, loading) {
if (loading) {
btn.disabled = true;
btn.innerHTML = '<div class="spinner"></div><span>AI 生成中...</span>';
} else {
btn.disabled = false;
btn.innerHTML = '<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg><span>✨ I\'m feeling lucky</span>';
}
}
// ==================== 錯誤處理工具 ====================
/**
* 顯示可複製的錯誤對話框
* @param {Object} options - 錯誤選項
* @param {string} options.title - 錯誤標題
* @param {string} options.message - 錯誤訊息
* @param {string} options.details - 錯誤詳情
* @param {string[]} options.suggestions - 建議列表
*/
export function showCopyableError(options) {
const { title, message, details, suggestions } = options;
// 移除舊的錯誤對話框
const existingModal = document.getElementById('errorModal');
if (existingModal) {
existingModal.remove();
}
const modalHTML = `
<div id="errorModal" class="modal-overlay show">
<div class="modal" style="max-width: 600px;">
<div class="modal-header" style="background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%); color: white;">
<h3>🚨 ${sanitizeHTML(title)}</h3>
<button class="modal-close" onclick="closeErrorModal(this)" style="color: white;">✕</button>
</div>
<div class="modal-body">
<div style="margin-bottom: 16px;">
<strong>錯誤訊息:</strong>
<p style="color: #e74c3c; font-weight: 500;">${sanitizeHTML(message)}</p>
</div>
${details ? `
<div style="margin-bottom: 16px;">
<strong>詳細資訊:</strong>
<pre style="background: #f8f9fa; padding: 12px; border-radius: 6px; overflow-x: auto; font-size: 0.85rem; max-height: 200px;">${sanitizeHTML(details)}</pre>
</div>
` : ''}
${suggestions && suggestions.length > 0 ? `
<div>
<strong>請檢查以下項目:</strong>
<ul style="margin: 8px 0; padding-left: 20px;">
${suggestions.map(s => `<li style="margin: 4px 0;">${sanitizeHTML(s)}</li>`).join('')}
</ul>
</div>
` : ''}
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="copyErrorDetails()">
<svg viewBox="0 0 24 24" style="width: 16px; height: 16px;">
<path fill="currentColor" d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/>
</svg>
複製錯誤詳情
</button>
<button class="btn btn-primary" onclick="closeErrorModal(this)">關閉</button>
</div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', modalHTML);
}
/**
* 關閉錯誤對話框
* @param {HTMLElement} button - 按鈕元素
*/
export function closeErrorModal(button) {
const modal = button.closest('.modal-overlay');
if (modal) {
modal.remove();
}
}
/**
* 複製錯誤詳情到剪貼板
*/
export function copyErrorDetails() {
const modal = document.getElementById('errorModal');
if (!modal) return;
const errorText = modal.querySelector('.modal-body').textContent;
navigator.clipboard.writeText(errorText).then(() => {
showToast('錯誤詳情已複製到剪貼板');
}).catch(err => {
console.error('複製失敗:', err);
});
}
// 將函式掛載到 window 上以便內聯事件處理器使用
if (typeof window !== 'undefined') {
window.closeErrorModal = closeErrorModal;
window.copyErrorDetails = copyErrorDetails;
}