變更內容: - 所有資料表加上 HR_position_ 前綴 - 整理完整欄位顯示名稱與 ID 對照表 - 模組化 JS 檔案 (admin.js, ai.js, csv.js 等) - 專案結構優化 (docs/, scripts/, tests/ 等) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
207 lines
7.4 KiB
JavaScript
207 lines
7.4 KiB
JavaScript
/**
|
||
* 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: min(90vw, 500px); max-height: 85vh; display: flex; flex-direction: column;">
|
||
<div class="modal-header" style="background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%); color: white; flex-shrink: 0;">
|
||
<h3 style="font-size: 1rem;">🚨 ${sanitizeHTML(title)}</h3>
|
||
<button class="modal-close" onclick="closeErrorModal(this)" style="color: white;">✕</button>
|
||
</div>
|
||
<div class="modal-body" style="overflow-y: auto; flex: 1; padding: 16px;">
|
||
<div style="margin-bottom: 12px;">
|
||
<strong style="font-size: 0.9rem;">錯誤訊息:</strong>
|
||
<p style="color: #e74c3c; font-weight: 500; font-size: 0.85rem; word-break: break-word;">${sanitizeHTML(message)}</p>
|
||
</div>
|
||
${details ? `
|
||
<div style="margin-bottom: 12px;">
|
||
<strong style="font-size: 0.9rem;">詳細資訊:</strong>
|
||
<pre style="background: #f8f9fa; padding: 10px; border-radius: 6px; overflow-x: auto; font-size: 0.75rem; max-height: 120px; white-space: pre-wrap; word-break: break-all;">${sanitizeHTML(details)}</pre>
|
||
</div>
|
||
` : ''}
|
||
${suggestions && suggestions.length > 0 ? `
|
||
<div>
|
||
<strong style="font-size: 0.9rem;">請檢查以下項目:</strong>
|
||
<ul style="margin: 6px 0; padding-left: 18px; font-size: 0.85rem;">
|
||
${suggestions.map(s => `<li style="margin: 3px 0;">${sanitizeHTML(s)}</li>`).join('')}
|
||
</ul>
|
||
</div>
|
||
` : ''}
|
||
</div>
|
||
<div class="modal-footer" style="flex-shrink: 0; padding: 12px 16px;">
|
||
<button class="btn btn-secondary" onclick="copyErrorDetails()" style="font-size: 0.85rem; padding: 8px 12px;">
|
||
<svg viewBox="0 0 24 24" style="width: 14px; height: 14px;">
|
||
<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)" style="font-size: 0.85rem; padding: 8px 16px;">關閉</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;
|
||
}
|