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>
This commit is contained in:
270
js/api.js
Normal file
270
js/api.js
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
/**
|
||||||
|
* API - API 呼叫函式
|
||||||
|
* 包含所有與後端 API 的通訊邏輯
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { API_BASE_URL } from './config.js';
|
||||||
|
import { showCopyableError } from './utils.js';
|
||||||
|
|
||||||
|
// ==================== LLM API ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 獲取當前選擇的 Ollama 模型
|
||||||
|
* @returns {string} - 模型名稱
|
||||||
|
*/
|
||||||
|
function getOllamaModel() {
|
||||||
|
return localStorage.getItem('selectedOllamaModel') || 'deepseek-r1:8b';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 調用 Claude/Ollama LLM API
|
||||||
|
* @param {string} prompt - Prompt 內容
|
||||||
|
* @param {string} api - API 類型('ollama' 或其他)
|
||||||
|
* @returns {Promise<Object>} - 解析後的 JSON 回應
|
||||||
|
*/
|
||||||
|
export async function callClaudeAPI(prompt, api = 'ollama') {
|
||||||
|
try {
|
||||||
|
// 準備請求資料
|
||||||
|
const requestData = {
|
||||||
|
api: api,
|
||||||
|
prompt: prompt,
|
||||||
|
max_tokens: 2000
|
||||||
|
};
|
||||||
|
|
||||||
|
// 如果使用 Ollama API,加入選擇的模型
|
||||||
|
if (api === 'ollama') {
|
||||||
|
requestData.model = getOllamaModel();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 調用後端 Flask API,避免 CORS 錯誤
|
||||||
|
const response = await fetch(`${API_BASE_URL}/llm/generate`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(requestData)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
throw new Error(errorData.error || `API 請求失敗: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!data.success) {
|
||||||
|
throw new Error(data.error || 'API 調用失敗');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理 JSON 代碼塊標記
|
||||||
|
let responseText = data.text;
|
||||||
|
responseText = responseText.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim();
|
||||||
|
return JSON.parse(responseText);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error calling LLM API:', error);
|
||||||
|
|
||||||
|
// 嘗試解析更詳細的錯誤訊息
|
||||||
|
let errorDetails = error.message;
|
||||||
|
try {
|
||||||
|
const errorJson = JSON.parse(error.message);
|
||||||
|
errorDetails = JSON.stringify(errorJson, null, 2);
|
||||||
|
} catch (e) {
|
||||||
|
// 不是 JSON,使用原始訊息
|
||||||
|
}
|
||||||
|
|
||||||
|
// 創建可複製的錯誤對話框
|
||||||
|
showCopyableError({
|
||||||
|
title: 'AI 生成錯誤',
|
||||||
|
message: error.message,
|
||||||
|
details: errorDetails,
|
||||||
|
suggestions: [
|
||||||
|
'Flask 後端已啟動 (python start_server.py)',
|
||||||
|
'已在 .env 文件中配置有效的 LLM API Key',
|
||||||
|
'網路連線正常',
|
||||||
|
'嘗試使用不同的 LLM API (DeepSeek 或 OpenAI)'
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Position API ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存崗位至崗位清單
|
||||||
|
* @param {Object} positionData - 崗位資料
|
||||||
|
* @returns {Promise<Object>} - API 回應
|
||||||
|
*/
|
||||||
|
export async function savePositionToList(positionData) {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/positions`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(positionData)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
throw new Error(errorData.error || '保存失敗');
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 載入崗位清單
|
||||||
|
* @returns {Promise<Array>} - 崗位清單
|
||||||
|
*/
|
||||||
|
export async function loadPositionList() {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/position-list`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('載入崗位清單失敗');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return data.data || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 獲取單一崗位描述
|
||||||
|
* @param {string} positionCode - 崗位編號
|
||||||
|
* @returns {Promise<Object>} - 崗位描述資料
|
||||||
|
*/
|
||||||
|
export async function getPositionDescription(positionCode) {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/position-descriptions/${positionCode}`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status === 404) {
|
||||||
|
return null; // 未找到描述
|
||||||
|
}
|
||||||
|
throw new Error('載入崗位描述失敗');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return data.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存崗位描述
|
||||||
|
* @param {Object} descData - 崗位描述資料
|
||||||
|
* @returns {Promise<Object>} - API 回應
|
||||||
|
*/
|
||||||
|
export async function savePositionDescription(descData) {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/position-descriptions`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(descData)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
throw new Error(errorData.error || '保存崗位描述失敗');
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== CSV API ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下載崗位 CSV 範本
|
||||||
|
*/
|
||||||
|
export async function downloadPositionCSVTemplate() {
|
||||||
|
window.location.href = `${API_BASE_URL}/positions/csv-template`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下載職務 CSV 範本
|
||||||
|
*/
|
||||||
|
export async function downloadJobCSVTemplate() {
|
||||||
|
window.location.href = `${API_BASE_URL}/jobs/csv-template`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 匯入崗位 CSV
|
||||||
|
* @param {File} file - CSV 檔案
|
||||||
|
* @returns {Promise<Object>} - API 回應
|
||||||
|
*/
|
||||||
|
export async function importPositionsCSV(file) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
|
||||||
|
const response = await fetch(`${API_BASE_URL}/positions/import-csv`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
throw new Error(errorData.error || 'CSV 匯入失敗');
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 匯入職務 CSV
|
||||||
|
* @param {File} file - CSV 檔案
|
||||||
|
* @returns {Promise<Object>} - API 回應
|
||||||
|
*/
|
||||||
|
export async function importJobsCSV(file) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
|
||||||
|
const response = await fetch(`${API_BASE_URL}/jobs/import-csv`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
throw new Error(errorData.error || 'CSV 匯入失敗');
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 匯出完整崗位資料
|
||||||
|
*/
|
||||||
|
export async function exportCompletePositionData() {
|
||||||
|
window.location.href = `${API_BASE_URL}/position-list/export`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Ollama Connection Test ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 測試 Ollama 連線
|
||||||
|
* @returns {Promise<boolean>} - 連線是否成功
|
||||||
|
*/
|
||||||
|
export async function testOllamaConnection() {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/llm/generate`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
api: 'ollama',
|
||||||
|
model: getOllamaModel(),
|
||||||
|
prompt: '請回答:「連線測試成功」',
|
||||||
|
max_tokens: 50
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return data.success === true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ollama 連線測試失敗:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
82
js/config.js
Normal file
82
js/config.js
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
/**
|
||||||
|
* Configuration - 設定檔
|
||||||
|
* 包含 API 端點、常數定義、資料對應表
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ==================== API Configuration ====================
|
||||||
|
export const API_BASE_URL = '/api';
|
||||||
|
|
||||||
|
// ==================== 下拉選單資料 (從 Excel 提取) ====================
|
||||||
|
|
||||||
|
// 事業體
|
||||||
|
export const businessUnits = [
|
||||||
|
'半導體事業群', '汽車事業體', '法務室', '岡山製造事業體', '產品事業體',
|
||||||
|
'晶圓三廠', '集團人資行政事業體', '集團財務事業體', '集團會計事業體',
|
||||||
|
'集團資訊事業體', '新創事業體', '稽核室', '總經理室', '總品質事業體', '營業事業體'
|
||||||
|
];
|
||||||
|
|
||||||
|
// 處級單位
|
||||||
|
export const deptLevel1Units = [
|
||||||
|
'半導體事業群', '汽車事業體', '法務室', '生產處', '岡山製造事業體', '封裝工程處',
|
||||||
|
'副總辦公室', '測試工程與研發處', '資材處', '廠務與環安衛管理處', '產品事業體',
|
||||||
|
'先進產品事業處', '成熟產品事業處', '晶圓三廠', '製程工程處', '集團人資行政事業體',
|
||||||
|
'集團財務事業體', '岡山強茂財務處', '集團會計事業體', '岡山會計處', '集團會計處',
|
||||||
|
'集團資訊事業體', '資安行動小組', '資訊一處', '資訊二處', '新創事業體',
|
||||||
|
'中低壓產品研發處', '研發中心', '高壓產品研發處', '稽核室', '總經理室',
|
||||||
|
'ESG專案辦公室', '專案管理室', '總品質事業體', '營業事業體', '商業開發暨市場應用處',
|
||||||
|
'海外銷售事業處', '全球技術服務處', '全球行銷暨業務支援處', '大中華區銷售事業處'
|
||||||
|
];
|
||||||
|
|
||||||
|
// 部級單位
|
||||||
|
export const deptLevel2Units = [
|
||||||
|
'生產部', '生產企劃部', '岡山品質管制部', '製程工程一部', '製程工程二部', '設備一部',
|
||||||
|
'設備二部', '工業工程部', '測試工程部', '新產品導入部', '研發部', '採購部',
|
||||||
|
'外部資源部', '生管部', '原物料控制部', '廠務部', '產品管理部(APD)', '產品管理部(MPD)',
|
||||||
|
'品質部', '製造部', '廠務部(Fab3)', '工程一部', '工程二部', '工程三部',
|
||||||
|
'製程整合部(Fab3)', '行政總務管理部', '招募任用部', '訓練發展部', '薪酬管理部',
|
||||||
|
'岡山強茂財務部', '會計部', '管理會計部', '集團合併報表部', '應用系統部',
|
||||||
|
'電腦整合製造部', '系統網路服務部', '資源管理部', '客戶品質管理部', '產品品質管理部',
|
||||||
|
'品質系統及客戶工程整合部', '封測外包品質管理部', '品質保證部', '日本區暨代工業務部',
|
||||||
|
'歐亞區業務部', '韓國區業務部-韓國區', '美洲區業務部', '應用工程部(GTS)', '系統工程部',
|
||||||
|
'特性測試部', '業務生管部', '市場行銷企劃部', 'MOSFET晶圓採購部', '台灣區業務部',
|
||||||
|
'業務一部', '業務二部'
|
||||||
|
];
|
||||||
|
|
||||||
|
// 崗位名稱
|
||||||
|
export const positionNames = [
|
||||||
|
'營運長', '營運長助理', '副總經理', '專案經理', '經副理', '法務專員', '專利工程師',
|
||||||
|
'處長', '專員', '課長', '組長', '班長', '副班長', '作業員', '工程師', '副總經理助理',
|
||||||
|
'副理', '專案經副理', '顧問', '人資長', '助理', '財務長', '專案副理', '會計長',
|
||||||
|
'資訊長', '主任', '總裁', '總經理', '專員/工程師', '經理', '技術經副理', '處長/資深經理'
|
||||||
|
];
|
||||||
|
|
||||||
|
// ==================== 資料對應表 ====================
|
||||||
|
|
||||||
|
// 崗位類別代碼對應中文名稱
|
||||||
|
export const categoryMap = {
|
||||||
|
'01': '技術職',
|
||||||
|
'02': '管理職',
|
||||||
|
'03': '業務職',
|
||||||
|
'04': '行政職'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 崗位性質代碼對應中文名稱
|
||||||
|
export const natureMap = {
|
||||||
|
'FT': '全職',
|
||||||
|
'PT': '兼職',
|
||||||
|
'CT': '約聘',
|
||||||
|
'IN': '實習'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 職務類別代碼對應中文名稱
|
||||||
|
export const jobCategoryMap = {
|
||||||
|
'MGR': '管理職',
|
||||||
|
'TECH': '技術職',
|
||||||
|
'SALE': '業務職',
|
||||||
|
'ADMIN': '行政職',
|
||||||
|
'RD': '研發職',
|
||||||
|
'PROD': '生產職'
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================== Toast 持續時間 ====================
|
||||||
|
export const TOAST_DURATION = 3000;
|
||||||
206
js/utils.js
Normal file
206
js/utils.js
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user