feat: 新增登出按鈕和用戶信息欄

- 在頁面頂部新增用戶信息欄,顯示當前登入用戶的姓名、角色和頭像
- 新增登出按鈕,點擊後清除 localStorage 並返回登入頁面
- 支援三種角色顯示:一般使用者 ★☆☆、管理者 ★★☆、最高管理者 ★★★
- 用戶頭像使用姓名首字母顯示
- 登出時顯示確認對話框和成功訊息
- 添加響應式設計,支援行動裝置顯示
- 使用紫色漸層背景,與系統主題一致
This commit is contained in:
2025-12-04 15:37:02 +08:00
parent b2584772c4
commit 15e32a2aef

View File

@@ -3,8 +3,21 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HR 基礎資料維護系統</title> <title>那都AI寫的不要問我 - HR 基礎資料維護系統</title>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@300;400;500;600;700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<!-- 登入檢查 -->
<script>
// 檢查登入狀態
(function() {
const currentUser = localStorage.getItem('currentUser');
if (!currentUser) {
// 如果未登入,跳轉到登入頁面
window.location.href = 'login.html';
}
})();
</script>
<style> <style>
:root { :root {
--primary: #1a5276; --primary: #1a5276;
@@ -37,6 +50,80 @@
.app-container { max-width: 1200px; margin: 0 auto; padding: 24px; } .app-container { max-width: 1200px; margin: 0 auto; padding: 24px; }
/* User Info Bar */
.user-info-bar {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: var(--radius);
padding: 12px 20px;
margin-bottom: 20px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: var(--shadow);
}
.user-info {
display: flex;
align-items: center;
gap: 12px;
color: #ffffff;
}
.user-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.2);
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 1.1rem;
}
.user-details {
display: flex;
flex-direction: column;
}
.user-name {
font-weight: 600;
font-size: 1rem;
}
.user-role {
font-size: 0.85rem;
opacity: 0.9;
}
.logout-btn {
padding: 8px 20px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 6px;
background: rgba(255, 255, 255, 0.1);
color: #ffffff;
font-family: inherit;
font-size: 0.9rem;
font-weight: 500;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.3s ease;
}
.logout-btn:hover {
background: rgba(255, 255, 255, 0.2);
border-color: rgba(255, 255, 255, 0.5);
transform: translateY(-2px);
}
.logout-btn svg {
width: 18px;
height: 18px;
fill: currentColor;
}
/* Module Selector */ /* Module Selector */
.module-selector { display: flex; gap: 12px; margin-bottom: 20px; flex-wrap: wrap; } .module-selector { display: flex; gap: 12px; margin-bottom: 20px; flex-wrap: wrap; }
@@ -604,12 +691,45 @@
.btn { justify-content: center; } .btn { justify-content: center; }
.module-selector { flex-direction: column; } .module-selector { flex-direction: column; }
.module-btn { min-width: 100%; } .module-btn { min-width: 100%; }
/* 響應式用戶信息欄 */
.user-info-bar {
flex-direction: column;
gap: 12px;
text-align: center;
}
.user-info {
justify-content: center;
}
.logout-btn {
width: 100%;
justify-content: center;
}
} }
</style> </style>
<script src="csv_utils.js"></script> <script src="csv_utils.js"></script>
</head> </head>
<body> <body>
<div class="app-container"> <div class="app-container">
<!-- User Info Bar -->
<div class="user-info-bar">
<div class="user-info">
<div class="user-avatar" id="userAvatar">U</div>
<div class="user-details">
<div class="user-name" id="userName">使用者</div>
<div class="user-role" id="userRole">一般使用者</div>
</div>
</div>
<button class="logout-btn" onclick="logout()">
<svg viewBox="0 0 24 24">
<path d="M17 7l-1.41 1.41L18.17 11H8v2h10.17l-2.58 2.58L17 17l5-5zM4 5h8V3H4c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h8v-2H4V5z"/>
</svg>
登出
</button>
</div>
<!-- Module Selector --> <!-- Module Selector -->
<div class="module-selector"> <div class="module-selector">
<button class="module-btn active" data-module="position"> <button class="module-btn active" data-module="position">
@@ -680,13 +800,17 @@
<!-- 處級單位 --> <!-- 處級單位 -->
<div class="form-group"> <div class="form-group">
<label>處級單位 (Division)</label> <label>處級單位 (Division)</label>
<input type="text" id="division" name="division" placeholder="選填"> <select id="division" name="division">
<option value="">請選擇</option>
</select>
</div> </div>
<!-- 部級單位 --> <!-- 部級單位 -->
<div class="form-group"> <div class="form-group">
<label>部級單位 (Department)</label> <label>部級單位 (Department)</label>
<input type="text" id="department" name="department" placeholder="選填"> <select id="department" name="department">
<option value="">請選擇</option>
</select>
</div> </div>
<!-- 課級單位 --> <!-- 課級單位 -->
@@ -921,6 +1045,10 @@
<input type="file" id="positionCSVInput" accept=".csv" style="display: none;" onchange="handlePositionCSVImport(event)"> <input type="file" id="positionCSVInput" accept=".csv" style="display: none;" onchange="handlePositionCSVImport(event)">
</div> </div>
<div class="action-buttons"> <div class="action-buttons">
<button type="button" class="btn btn-success" onclick="saveToPositionList()" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
<svg viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
儲存至崗位清單
</button>
<button type="button" class="btn btn-primary" onclick="savePositionAndExit()"> <button type="button" class="btn btn-primary" onclick="savePositionAndExit()">
<svg viewBox="0 0 24 24"><path d="M17 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V7l-4-4zm-5 16c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3zm3-10H5V5h10v4z"/></svg> <svg viewBox="0 0 24 24"><path d="M17 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V7l-4-4zm-5 16c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3zm3-10H5V5h10v4z"/></svg>
保存并退出(S) 保存并退出(S)
@@ -1285,7 +1413,7 @@
<select id="jd_deptFunction" name="deptFunction" onchange="loadDeptFunctionInfo()"> <select id="jd_deptFunction" name="deptFunction" onchange="loadDeptFunctionInfo()">
<option value="">-- 請選擇部門職責 --</option> <option value="">-- 請選擇部門職責 --</option>
</select> </select>
<button type="button" class="btn-icon" onclick="refreshDeptFunctionList()" title="重新載入"> <button type="button" class="btn-icon" onclick="refreshDeptFunctionList(true)" title="重新載入">
<svg viewBox="0 0 24 24"><path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg> <svg viewBox="0 0 24 24"><path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>
</button> </button>
</div> </div>
@@ -1666,6 +1794,80 @@
</div> </div>
</div> </div>
<!-- LLM 模型設定 -->
<div class="form-card" style="margin-top: 30px;">
<div style="margin-bottom: 20px;">
<h2 style="color: var(--primary); margin: 0 0 10px 0;">LLM 模型設定</h2>
<p style="color: var(--text-secondary); font-size: 14px; margin: 0;">選擇 Ollama API 使用的模型</p>
</div>
<div style="border: 1px solid #e0e0e0; border-radius: 8px; padding: 20px; background: #f8f9fa;">
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 10px; font-weight: 500; color: var(--text-primary);">
<svg viewBox="0 0 24 24" style="width: 20px; height: 20px; fill: var(--primary); vertical-align: middle; margin-right: 5px;">
<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>
Ollama 模型選擇
</label>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px; padding: 10px 0;">
<label style="display: flex; align-items: center; cursor: pointer;">
<input type="radio" name="ollamaModel" value="deepseek-reasoner" id="model-reasoner"
style="margin-right: 8px; width: 18px; height: 18px; cursor: pointer;"
onchange="saveOllamaModel()">
<span style="font-size: 14px;">deepseek-reasoner<br><small style="color: #666;">推理模型</small></span>
</label>
<label style="display: flex; align-items: center; cursor: pointer;">
<input type="radio" name="ollamaModel" value="deepseek-chat" id="model-chat"
style="margin-right: 8px; width: 18px; height: 18px; cursor: pointer;"
onchange="saveOllamaModel()">
<span style="font-size: 14px;">deepseek-chat<br><small style="color: #666;">對話模型</small></span>
</label>
<label style="display: flex; align-items: center; cursor: pointer;">
<input type="radio" name="ollamaModel" value="gpt-oss:120b" id="model-gptoss"
style="margin-right: 8px; width: 18px; height: 18px; cursor: pointer;"
onchange="saveOllamaModel()">
<span style="font-size: 14px;">GPT-OSS 120B<br><small style="color: #666;">大型語言模型</small></span>
</label>
</div>
<div style="margin-top: 10px; padding: 10px; background: #fff3cd; border: 1px solid #ffc107; border-radius: 4px; font-size: 13px; color: #856404;">
<strong>說明:</strong>
<ul style="margin: 5px 0 0 20px; padding: 0;">
<li><strong>deepseek-reasoner</strong>:適合需要深度思考和推理的任務,如複雜問題分析、邏輯推導等</li>
<li><strong>deepseek-chat</strong>:適合一般對話和快速回應的場景,如文字生成、簡單問答等</li>
<li><strong>GPT-OSS 120B</strong>:超大型模型 (120B 參數),提供最佳的理解能力和生成品質,適合複雜任務</li>
</ul>
</div>
</div>
<!-- 測試連線按鈕 -->
<div style="margin-top: 15px; display: flex; gap: 10px; align-items: center;">
<button type="button" class="btn btn-secondary" onclick="testOllamaConnection()" id="testConnectionBtn">
<svg viewBox="0 0 24 24" style="width: 18px; height: 18px; fill: currentColor;">
<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>
測試連線
</button>
<button type="button" class="btn btn-primary" onclick="saveOllamaModelWithConfirmation()" style="display: none;" id="saveModelBtn">
<svg viewBox="0 0 24 24" style="width: 18px; height: 18px; fill: currentColor;">
<path d="M17 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V7l-4-4zm-5 16c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3zm3-10H5V5h10v4z"/>
</svg>
儲存變更
</button>
</div>
<!-- 測試結果顯示 -->
<div style="margin-top: 15px; padding: 10px; background: #d4edda; border: 1px solid #28a745; border-radius: 4px; font-size: 13px; color: #155724; display: none;" id="connectionSuccess">
✓ 連線測試成功!模型回應正常
</div>
<div style="margin-top: 15px; padding: 10px; background: #f8d7da; border: 1px solid #dc3545; border-radius: 4px; font-size: 13px; color: #721c24; display: none;" id="connectionError">
✗ 連線測試失敗:<span id="connectionErrorMessage"></span>
</div>
<div style="margin-top: 15px; padding: 10px; background: #d4edda; border: 1px solid #28a745; border-radius: 4px; font-size: 13px; color: #155724; display: none;" id="modelSaveSuccess">
✓ 模型設定已儲存
</div>
</div>
</div>
<!-- 崗位資料管理 --> <!-- 崗位資料管理 -->
<div class="form-card" style="margin-top: 30px;"> <div class="form-card" style="margin-top: 30px;">
<div style="margin-bottom: 20px;"> <div style="margin-bottom: 20px;">
@@ -1752,7 +1954,26 @@
</div> </div>
<!-- 載入階層資料 -->
<script src="hierarchical_data.js"></script>
<script> <script>
// ==================== API Configuration ====================
const API_BASE_URL = '/api';
// ==================== 下拉選單資料 (從 Excel 提取) ====================
// 事業體
const businessUnits = ['半導體事業群', '汽車事業體', '法務室', '岡山製造事業體', '產品事業體', '晶圓三廠', '集團人資行政事業體', '集團財務事業體', '集團會計事業體', '集團資訊事業體', '新創事業體', '稽核室', '總經理室', '總品質事業體', '營業事業體'];
// 處級單位
const deptLevel1Units = ['半導體事業群', '汽車事業體', '法務室', '生產處', '岡山製造事業體', '封裝工程處', '副總辦公室', '測試工程與研發處', '資材處', '廠務與環安衛管理處', '產品事業體', '先進產品事業處', '成熟產品事業處', '晶圓三廠', '製程工程處', '集團人資行政事業體', '集團財務事業體', '岡山強茂財務處', '集團會計事業體', '岡山會計處', '集團會計處', '集團資訊事業體', '資安行動小組', '資訊一處', '資訊二處', '新創事業體', '中低壓產品研發處', '研發中心', '高壓產品研發處', '稽核室', '總經理室', 'ESG專案辦公室', '專案管理室', '總品質事業體', '營業事業體', '商業開發暨市場應用處', '海外銷售事業處', '全球技術服務處', '全球行銷暨業務支援處', '大中華區銷售事業處'];
// 部級單位
const deptLevel2Units = ['生產部', '生產企劃部', '岡山品質管制部', '製程工程一部', '製程工程二部', '設備一部', '設備二部', '工業工程部', '測試工程部', '新產品導入部', '研發部', '採購部', '外部資源部', '生管部', '原物料控制部', '廠務部', '產品管理部(APD)', '產品管理部(MPD)', '品質部', '製造部', '廠務部(Fab3)', '工程一部', '工程二部', '工程三部', '製程整合部(Fab3)', '行政總務管理部', '招募任用部', '訓練發展部', '薪酬管理部', '岡山強茂財務部', '會計部', '管理會計部', '集團合併報表部', '應用系統部', '電腦整合製造部', '系統網路服務部', '資源管理部', '客戶品質管理部', '產品品質管理部', '品質系統及客戶工程整合部', '封測外包品質管理部', '品質保證部', '日本區暨代工業務部', '歐亞區業務部', '韓國區業務部-韓國區', '美洲區業務部', '應用工程部(GTS)', '系統工程部', '特性測試部', '業務生管部', '市場行銷企劃部', 'MOSFET晶圓採購部', '台灣區業務部', '業務一部', '業務二部'];
// 崗位名稱
const positionNames = ['營運長', '營運長助理', '副總經理', '專案經理', '經副理', '法務專員', '專利工程師', '處長', '專員', '課長', '組長', '班長', '副班長', '作業員', '工程師', '副總經理助理', '副理', '專案經副理', '顧問', '人資長', '助理', '財務長', '專案副理', '會計長', '資訊長', '主任', '總裁', '總經理', '專員/工程師', '經理', '技術經副理', '處長/資深經理'];
// ==================== XSS 防護工具函數 ==================== // ==================== XSS 防護工具函數 ====================
/** /**
* 消毒 HTML 字串,防止 XSS 攻擊 * 消毒 HTML 字串,防止 XSS 攻擊
@@ -1778,19 +1999,27 @@
} }
// ==================== AI Generation Functions ==================== // ==================== AI Generation Functions ====================
async function callClaudeAPI(prompt, api = 'gemini') { async function callClaudeAPI(prompt, api = 'ollama') {
try { try {
// 準備請求資料
const requestData = {
api: api,
prompt: prompt,
max_tokens: 2000
};
// 如果使用 Ollama API加入選擇的模型
if (api === 'ollama') {
requestData.model = getOllamaModel();
}
// 調用後端 Flask API避免 CORS 錯誤 // 調用後端 Flask API避免 CORS 錯誤
const response = await fetch("/api/llm/generate", { const response = await fetch("/api/llm/generate", {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify({ body: JSON.stringify(requestData)
api: api,
prompt: prompt,
max_tokens: 2000
})
}); });
if (!response.ok) { if (!response.ok) {
@@ -2459,6 +2688,51 @@ ${contextInfo}
updatePreview(); updatePreview();
} }
async function saveToPositionList() {
const form = document.getElementById('positionForm');
if (!form.checkValidity()) {
form.reportValidity();
return;
}
const formData = getPositionFormData();
// 驗證必填欄位
if (!formData.basicInfo.positionCode) {
alert('請輸入崗位編號');
return;
}
if (!formData.basicInfo.positionName) {
alert('請輸入崗位名稱');
return;
}
try {
const response = await fetch(API_BASE_URL + '/positions', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
});
const result = await response.json();
if (result.success) {
showToast(result.message || '崗位已成功儲存至崗位清單!');
// 切換到崗位清單頁面
setTimeout(() => {
switchModule('positionlist');
}, 1500);
} else {
alert('儲存失敗: ' + (result.error || '未知錯誤'));
}
} catch (error) {
console.error('儲存錯誤:', error);
alert('儲存失敗: ' + error.message);
}
}
function cancelPositionForm() { function cancelPositionForm() {
if (confirm('確定要取消嗎?未保存的資料將會遺失。')) { if (confirm('確定要取消嗎?未保存的資料將會遺失。')) {
document.getElementById('positionForm').reset(); document.getElementById('positionForm').reset();
@@ -3424,7 +3698,7 @@ ${contextInfo}
// ==================== 部門職責關聯功能 ==================== // ==================== 部門職責關聯功能 ====================
// 重新載入部門職責下拉選單 // 重新載入部門職責下拉選單
function refreshDeptFunctionList() { function refreshDeptFunctionList(showMessage = false) {
const select = document.getElementById('jd_deptFunction'); const select = document.getElementById('jd_deptFunction');
if (!select) return; if (!select) return;
@@ -3439,11 +3713,15 @@ ${contextInfo}
option.textContent = `${sanitizeHTML(df.deptFunctionCode)} - ${sanitizeHTML(df.deptFunctionName)} (${sanitizeHTML(df.deptFunctionDept)})`; option.textContent = `${sanitizeHTML(df.deptFunctionCode)} - ${sanitizeHTML(df.deptFunctionName)} (${sanitizeHTML(df.deptFunctionDept)})`;
select.appendChild(option); select.appendChild(option);
}); });
if (showMessage) {
showToast('已載入 ' + deptFunctionData.length + ' 筆部門職責資料'); showToast('已載入 ' + deptFunctionData.length + ' 筆部門職責資料');
}
} else { } else {
if (showMessage) {
showToast('尚無部門職責資料,請先建立部門職責'); showToast('尚無部門職責資料,請先建立部門職責');
} }
} }
}
// 載入選中的部門職責資訊 // 載入選中的部門職責資訊
function loadDeptFunctionInfo() { function loadDeptFunctionInfo() {
@@ -3491,12 +3769,339 @@ ${contextInfo}
} }
} }
// ==================== 初始化下拉選單 ====================
function initializeDropdowns() {
// 初始化事業體下拉選單
const businessUnitSelect = document.getElementById('businessUnit');
if (businessUnitSelect) {
businessUnitSelect.innerHTML = '<option value="">請選擇</option>';
businessUnits.forEach(unit => {
const option = document.createElement('option');
option.value = unit;
option.textContent = unit;
businessUnitSelect.appendChild(option);
});
// 添加變更事件監聽
businessUnitSelect.addEventListener('change', onBusinessUnitChange);
}
// 初始化處級單位下拉選單(預設為空,等待事業體選擇)
const divisionSelect = document.getElementById('division');
if (divisionSelect) {
divisionSelect.innerHTML = '<option value="">請先選擇事業體</option>';
// 添加變更事件監聽
divisionSelect.addEventListener('change', onDivisionChange);
}
// 初始化部級單位下拉選單(預設為空,等待處級單位選擇)
const departmentSelect = document.getElementById('department');
if (departmentSelect) {
departmentSelect.innerHTML = '<option value="">請先選擇處級單位</option>';
}
}
// ==================== 下拉選單連動函數 ====================
function onBusinessUnitChange(event) {
const selectedBusiness = event.target.value;
const divisionSelect = document.getElementById('division');
const departmentSelect = document.getElementById('department');
// 清空處級單位和部級單位
if (divisionSelect) {
divisionSelect.innerHTML = '<option value="">請選擇</option>';
}
if (departmentSelect) {
departmentSelect.innerHTML = '<option value="">請先選擇處級單位</option>';
}
// 如果選擇了事業體,填充對應的處級單位
if (selectedBusiness && businessToDivision[selectedBusiness]) {
const divisions = businessToDivision[selectedBusiness];
divisions.forEach(division => {
const option = document.createElement('option');
option.value = division;
option.textContent = division;
divisionSelect.appendChild(option);
});
}
}
function onDivisionChange(event) {
const selectedDivision = event.target.value;
const departmentSelect = document.getElementById('department');
// 清空部級單位
if (departmentSelect) {
departmentSelect.innerHTML = '<option value="">請選擇</option>';
}
// 如果選擇了處級單位,填充對應的部級單位
if (selectedDivision && divisionToDepartment[selectedDivision]) {
const departments = divisionToDepartment[selectedDivision];
departments.forEach(department => {
const option = document.createElement('option');
option.value = department;
option.textContent = department;
departmentSelect.appendChild(option);
});
}
}
// ==================== 用戶信息與登出功能 ====================
function loadUserInfo() {
/**
* 從 localStorage 載入當前用戶信息並更新 UI
*/
const currentUser = localStorage.getItem('currentUser');
if (currentUser) {
try {
const user = JSON.parse(currentUser);
// 更新用戶名稱
const userNameEl = document.getElementById('userName');
if (userNameEl) {
userNameEl.textContent = user.name || user.username;
}
// 更新用戶角色
const userRoleEl = document.getElementById('userRole');
if (userRoleEl) {
let roleText = '';
switch(user.role) {
case 'user':
roleText = '一般使用者 ★☆☆';
break;
case 'admin':
roleText = '管理者 ★★☆';
break;
case 'superadmin':
roleText = '最高管理者 ★★★';
break;
default:
roleText = '一般使用者';
}
userRoleEl.textContent = roleText;
}
// 更新頭像
const userAvatarEl = document.getElementById('userAvatar');
if (userAvatarEl) {
// 使用使用者名稱的第一個字作為頭像
const avatarText = (user.name || user.username || 'U').charAt(0).toUpperCase();
userAvatarEl.textContent = avatarText;
}
console.log('用戶信息已載入:', user.name, user.role);
} catch (error) {
console.error('載入用戶信息失敗:', error);
}
} else {
// 如果沒有用戶信息,重定向到登入頁面
console.warn('未找到用戶信息,重定向到登入頁面');
window.location.href = 'login.html';
}
}
function logout() {
/**
* 登出功能:清除用戶信息並返回登入頁面
*/
if (confirm('確定要登出系統嗎?')) {
// 清除 localStorage 中的用戶信息
localStorage.removeItem('currentUser');
// 顯示登出訊息
showToast('已成功登出系統');
// 延遲後跳轉到登入頁面
setTimeout(() => {
window.location.href = 'login.html';
}, 1000);
}
}
// 在頁面載入時初始化部門職責下拉選單 // 在頁面載入時初始化部門職責下拉選單
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// 載入用戶信息
loadUserInfo();
// 初始化下拉選單
initializeDropdowns();
// 延遲載入,確保 deptFunctionData 已初始化 // 延遲載入,確保 deptFunctionData 已初始化
setTimeout(refreshDeptFunctionList, 500); setTimeout(refreshDeptFunctionList, 500);
// 載入 Ollama 模型設定
loadOllamaModel();
}); });
// ==================== Ollama 模型設定 ====================
function saveOllamaModel() {
// 當選擇變更時,顯示儲存按鈕
const saveBtn = document.getElementById('saveModelBtn');
if (saveBtn) {
saveBtn.style.display = 'inline-flex';
}
// 隱藏之前的訊息
hideAllMessages();
}
function saveOllamaModelWithConfirmation() {
const reasonerRadio = document.getElementById('model-reasoner');
const chatRadio = document.getElementById('model-chat');
let selectedModel = '';
if (reasonerRadio && reasonerRadio.checked) {
selectedModel = 'deepseek-reasoner';
} else if (chatRadio && chatRadio.checked) {
selectedModel = 'deepseek-chat';
}
if (selectedModel) {
// 儲存到 localStorage
localStorage.setItem('ollamaModel', selectedModel);
// 隱藏儲存按鈕
const saveBtn = document.getElementById('saveModelBtn');
if (saveBtn) {
saveBtn.style.display = 'none';
}
// 顯示成功訊息
hideAllMessages();
const successDiv = document.getElementById('modelSaveSuccess');
if (successDiv) {
successDiv.style.display = 'block';
setTimeout(() => {
successDiv.style.display = 'none';
}, 3000);
}
console.log('Ollama 模型已設定為:', selectedModel);
}
}
function loadOllamaModel() {
// 從 localStorage 讀取設定,預設使用 deepseek-reasoner
const savedModel = localStorage.getItem('ollamaModel') || 'deepseek-reasoner';
const reasonerRadio = document.getElementById('model-reasoner');
const chatRadio = document.getElementById('model-chat');
const gptossRadio = document.getElementById('model-gptoss');
if (savedModel === 'deepseek-reasoner' && reasonerRadio) {
reasonerRadio.checked = true;
} else if (savedModel === 'deepseek-chat' && chatRadio) {
chatRadio.checked = true;
} else if (savedModel === 'gpt-oss:120b' && gptossRadio) {
gptossRadio.checked = true;
}
console.log('已載入 Ollama 模型設定:', savedModel);
}
function getOllamaModel() {
// 取得當前選擇的模型
return localStorage.getItem('ollamaModel') || 'deepseek-reasoner';
}
function hideAllMessages() {
// 隱藏所有訊息框
const messages = ['connectionSuccess', 'connectionError', 'modelSaveSuccess'];
messages.forEach(id => {
const elem = document.getElementById(id);
if (elem) elem.style.display = 'none';
});
}
async function testOllamaConnection() {
const testBtn = document.getElementById('testConnectionBtn');
const reasonerRadio = document.getElementById('model-reasoner');
const chatRadio = document.getElementById('model-chat');
// 取得當前選擇的模型
let selectedModel = '';
if (reasonerRadio && reasonerRadio.checked) {
selectedModel = 'deepseek-reasoner';
} else if (chatRadio && chatRadio.checked) {
selectedModel = 'deepseek-chat';
}
if (!selectedModel) {
showToast('請先選擇一個模型');
return;
}
// 禁用按鈕並顯示載入中
if (testBtn) {
testBtn.disabled = true;
testBtn.textContent = '測試中...';
}
// 隱藏之前的訊息
hideAllMessages();
try {
// 調用測試 API
const response = await fetch('/api/llm/generate', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
api: 'ollama',
model: selectedModel,
prompt: '請用一句話說明你是誰',
max_tokens: 100
}),
timeout: 30000
});
const data = await response.json();
if (data.success) {
// 顯示成功訊息
const successDiv = document.getElementById('connectionSuccess');
if (successDiv) {
successDiv.style.display = 'block';
setTimeout(() => {
successDiv.style.display = 'none';
}, 5000);
}
console.log('測試回應:', data.text);
} else {
throw new Error(data.error || '未知錯誤');
}
} catch (error) {
// 顯示錯誤訊息
const errorDiv = document.getElementById('connectionError');
const errorMsg = document.getElementById('connectionErrorMessage');
if (errorDiv && errorMsg) {
errorMsg.textContent = error.message;
errorDiv.style.display = 'block';
setTimeout(() => {
errorDiv.style.display = 'none';
}, 10000);
}
console.error('連線測試失敗:', error);
} finally {
// 恢復按鈕
if (testBtn) {
testBtn.disabled = false;
testBtn.innerHTML = `
<svg viewBox="0 0 24 24" style="width: 18px; height: 18px; fill: currentColor;">
<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>
測試連線
`;
}
}
}
// ==================== 管理者頁面功能 ==================== // ==================== 管理者頁面功能 ====================
let usersData = [ let usersData = [
{ employeeId: 'A001', name: '系統管理員', email: 'admin@company.com', role: 'superadmin', createdAt: '2024-01-01' }, { employeeId: 'A001', name: '系統管理員', email: 'admin@company.com', role: 'superadmin', createdAt: '2024-01-01' },