feat: 新增登出按鈕和用戶信息欄
- 在頁面頂部新增用戶信息欄,顯示當前登入用戶的姓名、角色和頭像 - 新增登出按鈕,點擊後清除 localStorage 並返回登入頁面 - 支援三種角色顯示:一般使用者 ★☆☆、管理者 ★★☆、最高管理者 ★★★ - 用戶頭像使用姓名首字母顯示 - 登出時顯示確認對話框和成功訊息 - 添加響應式設計,支援行動裝置顯示 - 使用紫色漸層背景,與系統主題一致
This commit is contained in:
627
index.html
627
index.html
@@ -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' },
|
||||||
|
|||||||
Reference in New Issue
Block a user