feat: 新增崗位描述與清單整合功能 v2.1

主要功能更新:
- 崗位描述保存功能:保存後資料寫入資料庫
- 崗位清單自動刷新:切換模組時自動載入最新資料
- 崗位清單檢視功能:點擊「檢視」按鈕載入對應描述
- 管理者頁面擴充:新增崗位資料管理與匯出功能
- CSV 批次匯入:支援崗位與職務資料批次匯入

後端 API 新增:
- Position Description CRUD APIs
- Position List Query & Export APIs
- CSV Template Download & Import APIs

文件更新:
- SDD.md 更新至版本 2.1
- README.md 更新功能說明與版本歷史

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-04 12:46:36 +08:00
parent d17af39bf4
commit b2584772c4
31 changed files with 6795 additions and 365 deletions

155
fix_cors.js Normal file
View File

@@ -0,0 +1,155 @@
/**
* CORS 錯誤修正
* 將直接調用 Claude API 改為通過後端 Flask API 調用
*
* 使用方法:
* 1. 在 index.html 中找到 callClaudeAPI 函數
* 2. 將其替換為下面的新版本
*/
// ==================== 修正後的 AI Generation Functions ====================
/**
* 調用後端 LLM API 生成文字
* @param {string} prompt - 提示詞
* @param {string} api - API 名稱 (gemini, deepseek, openai)
* @returns {Promise<Object>} - 生成的 JSON 數據
*/
async function callClaudeAPI(prompt, api = 'gemini') {
try {
// 調用後端 Flask API而不是直接調用 Claude API
const response = await fetch("/api/llm/generate", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
api: api, // 使用 Gemini 作為默認
prompt: prompt,
max_tokens: 2000
})
});
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;
// 移除可能的 markdown 代碼塊標記
responseText = responseText.replace(/```json\n?/g, "").replace(/```\n?/g, "").trim();
// 解析 JSON
return JSON.parse(responseText);
} catch (error) {
console.error("Error calling LLM API:", error);
// 使用全局錯誤處理器顯示錯誤
if (window.errorHandler) {
window.errorHandler.showError({
title: 'AI 生成錯誤',
message: error.message || '調用 AI API 時發生錯誤',
type: 'error',
details: error.stack
});
} else {
alert(`AI 生成錯誤: ${error.message}`);
}
throw error;
}
}
/**
* 設置按鈕載入狀態
* @param {HTMLElement} btn - 按鈕元素
* @param {boolean} loading - 是否載入中
*/
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>';
}
}
// ==================== 選擇 LLM API 提供者 ====================
/**
* 讓用戶選擇使用哪個 LLM API
* @returns {Promise<string>} - 選擇的 API 名稱
*/
async function selectLLMProvider() {
// 獲取可用的 API 列表
try {
const response = await fetch('/api/llm/config');
const config = await response.json();
const enabledAPIs = [];
for (const [key, value] of Object.entries(config)) {
if (value.enabled) {
enabledAPIs.push({
key: key,
name: value.name
});
}
}
if (enabledAPIs.length === 0) {
throw new Error('沒有可用的 LLM API請先配置 API Key');
}
// 如果只有一個 API直接使用
if (enabledAPIs.length === 1) {
return enabledAPIs[0].key;
}
// 多個 API 時,使用第一個(默認 Gemini
return enabledAPIs[0].key;
} catch (error) {
console.error('無法獲取 LLM 配置:', error);
// 默認使用 Gemini
return 'gemini';
}
}
/**
* 增強版的 callClaudeAPI - 自動選擇最佳 API
* @param {string} prompt - 提示詞
* @returns {Promise<Object>} - 生成的 JSON 數據
*/
async function callAIAPI(prompt) {
const api = await selectLLMProvider();
return callClaudeAPI(prompt, api);
}
// ==================== 使用示例 ====================
/*
// 原來的調用方式(會導致 CORS 錯誤):
const result = await callClaudeAPI(prompt);
// 修正後的調用方式 1使用默認 Gemini
const result = await callClaudeAPI(prompt, 'gemini');
// 修正後的調用方式 2使用 DeepSeek
const result = await callClaudeAPI(prompt, 'deepseek');
// 修正後的調用方式 3使用 OpenAI
const result = await callClaudeAPI(prompt, 'openai');
// 修正後的調用方式 4自動選擇最佳 API
const result = await callAIAPI(prompt);
*/