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

252
improve_error_display.py Normal file
View File

@@ -0,0 +1,252 @@
"""
改進錯誤訊息顯示 - 使錯誤訊息可完整顯示和複製
"""
with open('index.html', 'r', encoding='utf-8') as f:
content = f.read()
# 備份
with open('index.html.backup2', 'w', encoding='utf-8') as f:
f.write(content)
# 找到錯誤處理的 alert 並替換為更好的顯示方式
old_error_handling = ''' } catch (error) {
console.error("Error calling LLM API:", error);
alert(`AI 生成錯誤: ${error.message}\\n\\n請確保\\n1. Flask 後端已啟動 (python app_updated.py)\\n2. 已在 .env 文件中配置 LLM API Key\\n3. 網路連線正常`);
throw error;
}'''
new_error_handling = ''' } catch (error) {
console.error("Error calling LLM API:", error);
// 嘗試解析更詳細的錯誤訊息
let errorDetails = error.message;
try {
// 如果錯誤訊息是 JSON 格式,嘗試美化顯示
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;
}'''
# 替換
new_content = content.replace(old_error_handling, new_error_handling)
if new_content == content:
print("WARNING: Pattern not found, content not changed")
else:
print("SUCCESS: Error handling improved")
# 添加 showCopyableError 函數(如果還沒有)
if 'function showCopyableError' not in new_content:
# 在 </script> 前添加新函數
error_display_function = '''
// 顯示可複製的錯誤訊息
function showCopyableError(options) {
const { title, message, details, suggestions } = options;
// 創建對話框
const modal = document.createElement('div');
modal.style.cssText = `
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
animation: fadeIn 0.3s;
`;
modal.innerHTML = `
<div style="
background: white;
border-radius: 12px;
max-width: 600px;
width: 90%;
max-height: 80vh;
overflow: hidden;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
display: flex;
flex-direction: column;
">
<!-- Header -->
<div style="
background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
color: white;
padding: 20px;
display: flex;
align-items: center;
gap: 15px;
">
<span style="font-size: 2rem;">❌</span>
<h3 style="margin: 0; font-size: 1.3rem; flex: 1;">${title}</h3>
<button onclick="this.closest('[style*=\\'position: fixed\\']').remove()" style="
background: rgba(255,255,255,0.2);
border: none;
color: white;
width: 30px;
height: 30px;
border-radius: 50%;
cursor: pointer;
font-size: 1.2rem;
display: flex;
align-items: center;
justify-content: center;
">×</button>
</div>
<!-- Body -->
<div style="
padding: 25px;
overflow-y: auto;
flex: 1;
">
<div style="
color: #333;
line-height: 1.6;
margin-bottom: 20px;
font-size: 1rem;
">${message}</div>
${suggestions && suggestions.length > 0 ? `
<div style="
background: #fff3cd;
border: 1px solid #ffc107;
border-radius: 6px;
padding: 15px;
margin-bottom: 20px;
">
<strong style="color: #856404; display: block; margin-bottom: 10px;">💡 請確保:</strong>
<ul style="margin: 0; padding-left: 20px; color: #856404;">
${suggestions.map(s => `<li style="margin: 5px 0;">${s}</li>`).join('')}
</ul>
</div>
` : ''}
${details ? `
<details style="
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 6px;
padding: 15px;
">
<summary style="
cursor: pointer;
font-weight: 600;
color: #495057;
user-select: none;
margin-bottom: 10px;
">🔍 詳細錯誤訊息(點擊展開)</summary>
<pre id="errorDetailsText" style="
background: white;
padding: 15px;
border-radius: 4px;
overflow-x: auto;
font-size: 0.85rem;
color: #666;
margin: 10px 0 0 0;
white-space: pre-wrap;
word-break: break-word;
max-height: 300px;
overflow-y: auto;
">${details}</pre>
<button onclick="copyErrorDetails()" style="
background: #007bff;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
margin-top: 10px;
font-size: 0.9rem;
">📋 複製錯誤訊息</button>
</details>
` : ''}
</div>
<!-- Footer -->
<div style="
padding: 15px 25px;
border-top: 1px solid #f0f0f0;
display: flex;
justify-content: flex-end;
gap: 10px;
">
<button onclick="this.closest('[style*=\\'position: fixed\\']').remove()" style="
background: #007bff;
color: white;
border: none;
padding: 10px 25px;
border-radius: 6px;
cursor: pointer;
font-size: 0.95rem;
font-weight: 500;
">確定</button>
</div>
</div>
`;
document.body.appendChild(modal);
// 點擊背景關閉
modal.addEventListener('click', (e) => {
if (e.target === modal) {
modal.remove();
}
});
}
// 複製錯誤訊息到剪貼板
function copyErrorDetails() {
const text = document.getElementById('errorDetailsText').textContent;
navigator.clipboard.writeText(text).then(() => {
alert('錯誤訊息已複製到剪貼板!');
}).catch(err => {
// Fallback: 選取文字
const range = document.createRange();
range.selectNode(document.getElementById('errorDetailsText'));
window.getSelection().removeAllRanges();
window.getSelection().addRange(range);
try {
document.execCommand('copy');
alert('錯誤訊息已複製到剪貼板!');
} catch (e) {
alert('複製失敗,請手動選取並複製');
}
});
}
'''
new_content = new_content.replace(' </script>', error_display_function + '\n </script>')
print("Added showCopyableError function")
# 寫回
with open('index.html', 'w', encoding='utf-8') as f:
f.write(new_content)
print("\nDone! Improvements:")
print("1. Error messages now show in a modal dialog")
print("2. Full error details are expandable")
print("3. Error details can be copied to clipboard")
print("4. Better formatting and readability")
print("\nPlease reload the page (Ctrl+F5) to see the changes")