backup: 完成 HR_position_ 表格前綴重命名與欄位對照表整理
變更內容: - 所有資料表加上 HR_position_ 前綴 - 整理完整欄位顯示名稱與 ID 對照表 - 模組化 JS 檔案 (admin.js, ai.js, csv.js 等) - 專案結構優化 (docs/, scripts/, tests/ 等) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -35,7 +35,11 @@
|
|||||||
"Bash(powershell -Command \"Get-Process python | Stop-Process -Force\")",
|
"Bash(powershell -Command \"Get-Process python | Stop-Process -Force\")",
|
||||||
"Bash(python llm_config.py:*)",
|
"Bash(python llm_config.py:*)",
|
||||||
"Bash(python:*)",
|
"Bash(python:*)",
|
||||||
"Bash(mkdir:*)"
|
"Bash(mkdir:*)",
|
||||||
|
"Bash(del \"d:\\00001_Vibe_coding\\1204剛為\\hierarchy_test.json\")",
|
||||||
|
"Bash(dir:*)",
|
||||||
|
"Bash(echo:*)",
|
||||||
|
"Bash(find:*)"
|
||||||
],
|
],
|
||||||
"deny": [],
|
"deny": [],
|
||||||
"ask": []
|
"ask": []
|
||||||
|
|||||||
5
.cursor/worktrees.json
Normal file
5
.cursor/worktrees.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"setup-worktree": [
|
||||||
|
"npm install"
|
||||||
|
]
|
||||||
|
}
|
||||||
562
SDD_代碼分離優化.md
562
SDD_代碼分離優化.md
@@ -1,567 +1,5 @@
|
|||||||
# 軟體設計文件 (SDD) - 代碼分離優化
|
# 軟體設計文件 (SDD) - 代碼分離優化
|
||||||
|
|
||||||
> **文件版本**: v1.0
|
|
||||||
> **建立日期**: 2024-12-04
|
|
||||||
> **專案名稱**: HR Position Management System
|
|
||||||
> **優化目標**: 將 CSS 和 JavaScript 從 index.html 分離
|
|
||||||
> **狀態**: 📋 待決策階段
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📋 目錄
|
|
||||||
|
|
||||||
1. [專案現況分析](#專案現況分析)
|
|
||||||
2. [優化目標與效益](#優化目標與效益)
|
|
||||||
3. [需要決策的事項](#需要決策的事項)
|
|
||||||
4. [建議的架構方案](#建議的架構方案)
|
|
||||||
5. [分離策略](#分離策略)
|
|
||||||
6. [風險評估](#風險評估)
|
|
||||||
7. [執行計畫](#執行計畫)
|
|
||||||
8. [驗收標準](#驗收標準)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 專案現況分析
|
|
||||||
|
|
||||||
### 📊 當前檔案結構
|
|
||||||
|
|
||||||
```
|
|
||||||
d:\00001_Vibe_coding\1204剛為\
|
|
||||||
├── index.html (約 4,700 行) ⚠️ 包含 HTML + CSS + JavaScript
|
|
||||||
├── login.html (約 470 行) ⚠️ 包含 HTML + CSS + JavaScript
|
|
||||||
├── hierarchical_data.js (已分離) ✅ 組織架構資料
|
|
||||||
├── dropdown_data.js (已分離) ✅ 下拉選單資料
|
|
||||||
├── app.py (Flask 後端)
|
|
||||||
├── .env (環境設定)
|
|
||||||
└── prompt.md (AI Prompt 文件)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 📈 當前 index.html 組成分析
|
|
||||||
|
|
||||||
| 內容類型 | 預估行數 | 佔比 | 說明 |
|
|
||||||
|---------|---------|------|------|
|
|
||||||
| **HTML 結構** | ~1,500 行 | 32% | 5 個主要頁籤的表單結構 |
|
|
||||||
| **CSS 樣式** | ~700 行 | 15% | 內嵌於 `<style>` 標籤中 |
|
|
||||||
| **JavaScript** | ~2,500 行 | 53% | 內嵌於 `<script>` 標籤中 |
|
|
||||||
|
|
||||||
### ⚠️ 當前問題
|
|
||||||
|
|
||||||
1. **維護困難**: 單一檔案過於龐大,難以快速定位問題
|
|
||||||
2. **協作不便**: 多人同時修改容易產生衝突
|
|
||||||
3. **載入效能**: 無法利用瀏覽器快取分離的資源檔案
|
|
||||||
4. **程式碼重用**: CSS 和 JS 無法在其他頁面(如 login.html)重用
|
|
||||||
5. **偵錯困難**: 瀏覽器開發者工具中程式碼不易閱讀
|
|
||||||
6. **版本控制**: Git diff 難以追蹤具體修改內容
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 優化目標與效益
|
|
||||||
|
|
||||||
### 🎯 優化目標
|
|
||||||
|
|
||||||
1. **分離關注點**: HTML 結構、CSS 樣式、JavaScript 邏輯各自獨立
|
|
||||||
2. **提升可維護性**: 每個檔案職責單一,易於理解和修改
|
|
||||||
3. **改善效能**: 利用瀏覽器快取機制,減少重複載入
|
|
||||||
4. **便於協作**: 不同團隊成員可同時修改不同檔案
|
|
||||||
5. **增強可測試性**: JavaScript 獨立後更容易進行單元測試
|
|
||||||
|
|
||||||
### 📈 預期效益
|
|
||||||
|
|
||||||
| 效益項目 | 改善程度 | 說明 |
|
|
||||||
|---------|---------|------|
|
|
||||||
| **維護效率** | ⬆️ 40-60% | 快速定位和修改程式碼 |
|
|
||||||
| **載入速度** | ⬆️ 20-30% | 瀏覽器快取 CSS/JS 檔案 |
|
|
||||||
| **協作效率** | ⬆️ 50-70% | 減少 Git 衝突,平行開發 |
|
|
||||||
| **程式碼重用** | ⬆️ 80-100% | CSS/JS 可在多個頁面共用 |
|
|
||||||
| **偵錯效率** | ⬆️ 30-50% | 獨立檔案,Source Map 支援 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 需要決策的事項
|
|
||||||
|
|
||||||
> 🔔 請針對以下問題做出選擇,我會根據您的決策提供對應的實作方案
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### ❓ 決策 1: CSS 分離策略
|
|
||||||
|
|
||||||
**問題**: 如何分離 CSS 樣式?
|
|
||||||
|
|
||||||
#### 選項 A: 單一 CSS 檔案(簡單方案)✨ 推薦新手
|
|
||||||
```
|
|
||||||
styles/
|
|
||||||
└── main.css (包含所有樣式)
|
|
||||||
```
|
|
||||||
**優點**:
|
|
||||||
- ✅ 結構簡單,易於實作
|
|
||||||
- ✅ 只需引入一個 CSS 檔案
|
|
||||||
- ✅ 適合小型專案
|
|
||||||
|
|
||||||
**缺點**:
|
|
||||||
- ❌ 單一檔案可能過大
|
|
||||||
- ❌ 不同頁面的樣式混在一起
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 選項 B: 模組化 CSS 檔案(推薦方案)✨ 推薦
|
|
||||||
```
|
|
||||||
styles/
|
|
||||||
├── base.css (基礎樣式、CSS Reset、變數)
|
|
||||||
├── layout.css (整體布局、Grid、Flexbox)
|
|
||||||
├── components.css (按鈕、表單、卡片等元件)
|
|
||||||
├── modules.css (5 個頁籤的專屬樣式)
|
|
||||||
└── utilities.css (工具類別、響應式)
|
|
||||||
```
|
|
||||||
**優點**:
|
|
||||||
- ✅ 職責分明,易於維護
|
|
||||||
- ✅ 可按需載入(未來優化)
|
|
||||||
- ✅ 符合業界最佳實踐
|
|
||||||
|
|
||||||
**缺點**:
|
|
||||||
- ❌ 需要管理多個檔案
|
|
||||||
- ❌ 初期設定較複雜
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 選項 C: 按頁籤分離 CSS(精細方案)
|
|
||||||
```
|
|
||||||
styles/
|
|
||||||
├── common.css (共用樣式)
|
|
||||||
├── position-basic.css (崗位基礎資料)
|
|
||||||
├── position-recruit.css (崗位招聘要求)
|
|
||||||
├── job-basic.css (職務基礎資料)
|
|
||||||
├── dept-function.css (部門職責維護)
|
|
||||||
├── job-desc.css (崗位描述)
|
|
||||||
└── position-list.css (崗位清單)
|
|
||||||
```
|
|
||||||
**優點**:
|
|
||||||
- ✅ 極度模組化
|
|
||||||
- ✅ 按需載入效能最佳
|
|
||||||
- ✅ 適合大型專案
|
|
||||||
|
|
||||||
**缺點**:
|
|
||||||
- ❌ 檔案數量多,管理複雜
|
|
||||||
- ❌ 共用樣式可能重複定義
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**您的選擇**: [ ] A / [ ] B / [ ] C / [ ] 其他建議: _______________
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### ❓ 決策 2: JavaScript 分離策略
|
|
||||||
|
|
||||||
**問題**: 如何組織 JavaScript 程式碼?
|
|
||||||
|
|
||||||
#### 選項 A: 功能模組化(推薦方案)✨ 推薦
|
|
||||||
```
|
|
||||||
js/
|
|
||||||
├── config.js (設定檔:API URL、常數)
|
|
||||||
├── utils.js (工具函式:showToast、fillIfEmpty)
|
|
||||||
├── api.js (API 呼叫:callClaudeAPI、fetch 相關)
|
|
||||||
├── ui.js (UI 操作:switchModule、updatePreview)
|
|
||||||
├── form-handlers.js (表單處理:驗證、提交)
|
|
||||||
├── ai-generators.js (AI 生成函式:5 個 generate 函式)
|
|
||||||
├── dropdown.js (下拉選單邏輯:階層式選單)
|
|
||||||
└── main.js (主程式:初始化、事件監聽)
|
|
||||||
```
|
|
||||||
**優點**:
|
|
||||||
- ✅ 職責清晰,易於維護
|
|
||||||
- ✅ 符合 MVC/MVVM 架構思想
|
|
||||||
- ✅ 便於單元測試
|
|
||||||
- ✅ 符合業界最佳實踐
|
|
||||||
|
|
||||||
**缺點**:
|
|
||||||
- ❌ 需要處理模組間依賴關係
|
|
||||||
- ❌ 檔案數量較多
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 選項 B: 按頁籤分離(直觀方案)
|
|
||||||
```
|
|
||||||
js/
|
|
||||||
├── common.js (共用功能)
|
|
||||||
├── position-basic.js (崗位基礎資料)
|
|
||||||
├── position-recruit.js (崗位招聘要求)
|
|
||||||
├── job-basic.js (職務基礎資料)
|
|
||||||
├── dept-function.js (部門職責維護)
|
|
||||||
├── job-desc.js (崗位描述)
|
|
||||||
└── position-list.js (崗位清單)
|
|
||||||
```
|
|
||||||
**優點**:
|
|
||||||
- ✅ 對應頁籤結構,直觀易懂
|
|
||||||
- ✅ 修改特定頁籤功能時容易定位
|
|
||||||
|
|
||||||
**缺點**:
|
|
||||||
- ❌ 共用函式可能在多個檔案重複
|
|
||||||
- ❌ 跨頁籤功能需要協調多個檔案
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 選項 C: 混合方案(平衡方案)✨ 折衷選擇
|
|
||||||
```
|
|
||||||
js/
|
|
||||||
├── core/
|
|
||||||
│ ├── config.js (設定)
|
|
||||||
│ ├── utils.js (工具)
|
|
||||||
│ ├── api.js (API)
|
|
||||||
│ └── ui.js (UI)
|
|
||||||
├── modules/
|
|
||||||
│ ├── position-basic.js
|
|
||||||
│ ├── position-recruit.js
|
|
||||||
│ ├── job-basic.js
|
|
||||||
│ ├── dept-function.js
|
|
||||||
│ ├── job-desc.js
|
|
||||||
│ └── position-list.js
|
|
||||||
└── main.js (主程式)
|
|
||||||
```
|
|
||||||
**優點**:
|
|
||||||
- ✅ 兼具模組化與直觀性
|
|
||||||
- ✅ 核心功能集中管理
|
|
||||||
- ✅ 頁籤功能獨立開發
|
|
||||||
|
|
||||||
**缺點**:
|
|
||||||
- ❌ 目錄結構較複雜
|
|
||||||
- ❌ 需要規劃模組間通訊
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**您的選擇**: [ ] A / [ ] B / [ ] C / [ ] 其他建議: _______________
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### ❓ 決策 3: 模組載入方式
|
|
||||||
|
|
||||||
**問題**: 如何在 HTML 中載入分離後的 JS 檔案?
|
|
||||||
|
|
||||||
#### 選項 A: 傳統 `<script>` 標籤(相容性最佳)✨ 推薦相容
|
|
||||||
```html
|
|
||||||
<script src="js/config.js"></script>
|
|
||||||
<script src="js/utils.js"></script>
|
|
||||||
<script src="js/api.js"></script>
|
|
||||||
<!-- 按順序載入,注意依賴關係 -->
|
|
||||||
```
|
|
||||||
**優點**:
|
|
||||||
- ✅ 相容所有瀏覽器(包含 IE11)
|
|
||||||
- ✅ 不需要打包工具
|
|
||||||
- ✅ 實作簡單
|
|
||||||
|
|
||||||
**缺點**:
|
|
||||||
- ❌ 需要手動管理載入順序
|
|
||||||
- ❌ 全域命名空間污染風險
|
|
||||||
- ❌ 無法使用 `import/export`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 選項 B: ES6 Modules(現代方案)✨ 推薦現代
|
|
||||||
```html
|
|
||||||
<script type="module" src="js/main.js"></script>
|
|
||||||
<!-- main.js 中使用 import 載入其他模組 -->
|
|
||||||
```
|
|
||||||
```javascript
|
|
||||||
// main.js
|
|
||||||
import { showToast } from './utils.js';
|
|
||||||
import { callClaudeAPI } from './api.js';
|
|
||||||
```
|
|
||||||
**優點**:
|
|
||||||
- ✅ 現代瀏覽器原生支援
|
|
||||||
- ✅ 自動處理依賴關係
|
|
||||||
- ✅ 避免全域污染
|
|
||||||
- ✅ 支援按需載入(Tree Shaking)
|
|
||||||
|
|
||||||
**缺點**:
|
|
||||||
- ❌ 不支援 IE11(需要 polyfill)
|
|
||||||
- ❌ 本地開發需要 HTTP Server(不能用 file:// 協議)
|
|
||||||
- ❌ 需要調整現有程式碼結構
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 選項 C: 打包工具(Webpack/Vite)(進階方案)
|
|
||||||
```html
|
|
||||||
<script src="dist/bundle.js"></script>
|
|
||||||
<!-- 所有 JS 打包成單一檔案 -->
|
|
||||||
```
|
|
||||||
**優點**:
|
|
||||||
- ✅ 最佳效能(壓縮、優化)
|
|
||||||
- ✅ 支援所有瀏覽器
|
|
||||||
- ✅ 可使用 npm 套件
|
|
||||||
- ✅ 支援 TypeScript、Babel 等工具
|
|
||||||
|
|
||||||
**缺點**:
|
|
||||||
- ❌ 需要安裝 Node.js 和打包工具
|
|
||||||
- ❌ 開發流程變複雜(需要 build)
|
|
||||||
- ❌ 學習曲線較陡
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**您的選擇**: [ ] A / [ ] B / [ ] C / [ ] 其他建議: _______________
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### ❓ 決策 4: 共用樣式處理
|
|
||||||
|
|
||||||
**問題**: index.html 和 login.html 有部分共用樣式,如何處理?
|
|
||||||
|
|
||||||
#### 選項 A: 建立共用樣式檔(推薦)✨ 推薦
|
|
||||||
```
|
|
||||||
styles/
|
|
||||||
├── common.css (兩個頁面共用的樣式)
|
|
||||||
├── index.css (index.html 專屬)
|
|
||||||
└── login.css (login.html 專屬)
|
|
||||||
```
|
|
||||||
**優點**:
|
|
||||||
- ✅ 避免重複程式碼
|
|
||||||
- ✅ 樣式一致性高
|
|
||||||
- ✅ 修改一處,全站生效
|
|
||||||
|
|
||||||
**缺點**:
|
|
||||||
- ❌ 需要識別哪些是共用樣式
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 選項 B: 各自獨立(簡單方案)
|
|
||||||
```
|
|
||||||
styles/
|
|
||||||
├── index.css (包含所有 index.html 需要的樣式)
|
|
||||||
└── login.css (包含所有 login.html 需要的樣式)
|
|
||||||
```
|
|
||||||
**優點**:
|
|
||||||
- ✅ 實作最簡單
|
|
||||||
- ✅ 兩頁面完全獨立
|
|
||||||
|
|
||||||
**缺點**:
|
|
||||||
- ❌ 樣式重複(如按鈕、表單元件)
|
|
||||||
- ❌ 修改需要同步更新多處
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**您的選擇**: [ ] A / [ ] B / [ ] 其他建議: _______________
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### ❓ 決策 5: CSS 變數與主題管理
|
|
||||||
|
|
||||||
**問題**: 系統使用紫色漸層主題,是否要統一管理?
|
|
||||||
|
|
||||||
#### 選項 A: 使用 CSS 變數(推薦)✨ 推薦
|
|
||||||
```css
|
|
||||||
/* base.css */
|
|
||||||
:root {
|
|
||||||
/* 主題色 */
|
|
||||||
--primary-color: #667eea;
|
|
||||||
--primary-dark: #764ba2;
|
|
||||||
--gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
|
|
||||||
/* 語義化顏色 */
|
|
||||||
--color-success: #10b981;
|
|
||||||
--color-warning: #f59e0b;
|
|
||||||
--color-danger: #ef4444;
|
|
||||||
|
|
||||||
/* 間距 */
|
|
||||||
--spacing-sm: 8px;
|
|
||||||
--spacing-md: 16px;
|
|
||||||
--spacing-lg: 24px;
|
|
||||||
|
|
||||||
/* 字體 */
|
|
||||||
--font-family: 'Microsoft JhengHei', 'Segoe UI', sans-serif;
|
|
||||||
--font-size-base: 14px;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
**優點**:
|
|
||||||
- ✅ 統一管理主題色
|
|
||||||
- ✅ 輕鬆實現換色/Dark Mode
|
|
||||||
- ✅ 語義化命名,易於理解
|
|
||||||
|
|
||||||
**缺點**:
|
|
||||||
- ❌ 不支援 IE11(需要 polyfill)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 選項 B: 直接寫死顏色值(簡單方案)
|
|
||||||
```css
|
|
||||||
.button {
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
**優點**:
|
|
||||||
- ✅ 相容所有瀏覽器
|
|
||||||
- ✅ 不需要額外設定
|
|
||||||
|
|
||||||
**缺點**:
|
|
||||||
- ❌ 修改主題色需要全局搜尋替換
|
|
||||||
- ❌ 無法動態切換主題
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**您的選擇**: [ ] A / [ ] B / [ ] 其他建議: _______________
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### ❓ 決策 6: 執行順序與回滾計畫
|
|
||||||
|
|
||||||
**問題**: 如何進行代碼分離,以降低風險?
|
|
||||||
|
|
||||||
#### 選項 A: 一次性全部分離(快速方案)
|
|
||||||
**執行步驟**:
|
|
||||||
1. 一次性建立所有 CSS/JS 檔案
|
|
||||||
2. 從 index.html 移除所有 `<style>` 和 `<script>`
|
|
||||||
3. 完整測試所有功能
|
|
||||||
|
|
||||||
**優點**:
|
|
||||||
- ✅ 快速完成重構
|
|
||||||
- ✅ 避免中間狀態
|
|
||||||
|
|
||||||
**缺點**:
|
|
||||||
- ❌ 風險較高,問題多時難以回滾
|
|
||||||
- ❌ 測試工作量大
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 選項 B: 分階段漸進式分離(穩健方案)✨ 推薦
|
|
||||||
**階段 1**: CSS 分離(1-2 天)
|
|
||||||
- 建立 CSS 檔案
|
|
||||||
- 測試樣式正確性
|
|
||||||
- Git commit
|
|
||||||
|
|
||||||
**階段 2**: JavaScript 工具函式分離(1-2 天)
|
|
||||||
- 分離 utils.js、api.js
|
|
||||||
- 測試基礎功能
|
|
||||||
- Git commit
|
|
||||||
|
|
||||||
**階段 3**: JavaScript 模組分離(2-3 天)
|
|
||||||
- 按選定策略分離各模組
|
|
||||||
- 逐一測試各頁籤功能
|
|
||||||
- Git commit
|
|
||||||
|
|
||||||
**階段 4**: 優化與收尾(1 天)
|
|
||||||
- 程式碼審查
|
|
||||||
- 效能測試
|
|
||||||
- 文件更新
|
|
||||||
|
|
||||||
**優點**:
|
|
||||||
- ✅ 風險可控,問題易定位
|
|
||||||
- ✅ 每階段可獨立回滾
|
|
||||||
- ✅ 便於測試和驗證
|
|
||||||
|
|
||||||
**缺點**:
|
|
||||||
- ❌ 時間較長
|
|
||||||
- ❌ 可能存在中間狀態
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 選項 C: 建立平行版本(保守方案)
|
|
||||||
**執行步驟**:
|
|
||||||
1. 複製 index.html 為 index-refactor.html
|
|
||||||
2. 在新檔案中進行重構
|
|
||||||
3. 完成後替換原檔案
|
|
||||||
|
|
||||||
**優點**:
|
|
||||||
- ✅ 最安全,隨時可回滾
|
|
||||||
- ✅ 可對比新舊版本
|
|
||||||
|
|
||||||
**缺點**:
|
|
||||||
- ❌ 需要維護兩個版本
|
|
||||||
- ❌ 浪費開發資源
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**您的選擇**: [ ] A / [ ] B / [ ] C / [ ] 其他建議: _______________
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### ❓ 決策 7: 命名規範
|
|
||||||
|
|
||||||
**問題**: 檔案和函式的命名風格?
|
|
||||||
|
|
||||||
#### 選項 A: Kebab-case(檔案)+ camelCase(函式)✨ 推薦
|
|
||||||
```
|
|
||||||
檔案名稱: form-handlers.js, ai-generators.js
|
|
||||||
函式名稱: generatePositionBasic(), showToast()
|
|
||||||
```
|
|
||||||
**優點**:
|
|
||||||
- ✅ 符合業界慣例
|
|
||||||
- ✅ URL 友善(不需要編碼)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 選項 B: camelCase(檔案)+ camelCase(函式)
|
|
||||||
```
|
|
||||||
檔案名稱: formHandlers.js, aiGenerators.js
|
|
||||||
函式名稱: generatePositionBasic(), showToast()
|
|
||||||
```
|
|
||||||
**優點**:
|
|
||||||
- ✅ 與 JavaScript 慣例一致
|
|
||||||
|
|
||||||
**缺點**:
|
|
||||||
- ❌ Windows/Mac 檔案系統可能有大小寫問題
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 選項 C: snake_case(檔案)+ camelCase(函式)
|
|
||||||
```
|
|
||||||
檔案名稱: form_handlers.js, ai_generators.js
|
|
||||||
函式名稱: generatePositionBasic(), showToast()
|
|
||||||
```
|
|
||||||
**優點**:
|
|
||||||
- ✅ Python 開發者熟悉
|
|
||||||
|
|
||||||
**缺點**:
|
|
||||||
- ❌ JavaScript 社群較少使用
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**您的選擇**: [ ] A / [ ] B / [ ] C / [ ] 其他建議: _______________
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### ❓ 決策 8: 瀏覽器相容性
|
|
||||||
|
|
||||||
**問題**: 系統需要支援哪些瀏覽器?
|
|
||||||
|
|
||||||
#### 選項 A: 現代瀏覽器(Chrome/Firefox/Safari/Edge 最新版)✨ 推薦
|
|
||||||
**技術選擇**:
|
|
||||||
- ✅ 可使用 ES6 Modules
|
|
||||||
- ✅ 可使用 CSS Grid、Flexbox
|
|
||||||
- ✅ 可使用 CSS Variables
|
|
||||||
- ✅ 開發效率高
|
|
||||||
|
|
||||||
**支援範圍**:
|
|
||||||
- Chrome 60+
|
|
||||||
- Firefox 60+
|
|
||||||
- Safari 11+
|
|
||||||
- Edge 79+ (Chromium)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 選項 B: 包含 IE11(企業環境)
|
|
||||||
**技術選擇**:
|
|
||||||
- ❌ 需要使用 Babel 轉譯
|
|
||||||
- ❌ 需要 CSS Variables polyfill
|
|
||||||
- ❌ 需要 Flexbox fallback
|
|
||||||
- ❌ 開發成本增加 30-50%
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**您的選擇**: [ ] A / [ ] B / [ ] 其他說明: _______________
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 建議的架構方案
|
|
||||||
|
|
||||||
### 🏆 我的推薦配置(根據專案特性)
|
|
||||||
|
|
||||||
基於您的專案現況,我推薦以下配置:
|
|
||||||
|
|
||||||
```
|
|
||||||
✅ 決策 1: 選項 B - 模組化 CSS 檔案
|
|
||||||
✅ 決策 2: 選項 A - 功能模組化 JavaScript
|
|
||||||
✅ 決策 3: 選項 B - ES6 Modules(需要 HTTP Server)
|
|
||||||
✅ 決策 4: 選項 A - 建立共用樣式檔
|
|
||||||
✅ 決策 5: 選項 A - 使用 CSS 變數
|
|
||||||
✅ 決策 6: 選項 B - 分階段漸進式分離
|
|
||||||
✅ 決策 7: 選項 A - Kebab-case + camelCase
|
|
||||||
✅ 決策 8: 選項 A - 現代瀏覽器
|
|
||||||
```
|
|
||||||
|
|
||||||
### 📁 推薦的最終檔案結構
|
### 📁 推薦的最終檔案結構
|
||||||
|
|
||||||
|
|||||||
@@ -1,485 +0,0 @@
|
|||||||
# 使用者指令記錄
|
|
||||||
|
|
||||||
**專案**: HR Position Management System
|
|
||||||
**日期**: 2024-12-04
|
|
||||||
**版本**: 2.1
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📋 指令時間軸
|
|
||||||
|
|
||||||
### 初始設定階段
|
|
||||||
|
|
||||||
#### 1. 系統基礎建置
|
|
||||||
```
|
|
||||||
提供 SDD.md 文件,要求建立:
|
|
||||||
- .env 文件(包含 MySQL 資料庫設定)
|
|
||||||
- .gitignore 文件
|
|
||||||
- MySQL 資料庫連線設定(mysql.theaken.com:33306, db_A102)
|
|
||||||
- Gitea 版本控制設定(https://gitea.theaken.com/)
|
|
||||||
- LLM API 整合(Gemini, DeepSeek, OpenAI)
|
|
||||||
- 所有頁面/功能的錯誤處理機制
|
|
||||||
```
|
|
||||||
|
|
||||||
**執行結果**: ✅ 完成
|
|
||||||
- 建立 .env 文件
|
|
||||||
- 建立 .gitignore 文件
|
|
||||||
- 建立 database_schema.sql
|
|
||||||
- 建立 llm_config.py
|
|
||||||
- 建立 api_test.html
|
|
||||||
- 建立 error_handler.js
|
|
||||||
- 建立完整的設定文件
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### CORS 錯誤修正階段
|
|
||||||
|
|
||||||
#### 2. 回報 CORS 錯誤
|
|
||||||
```
|
|
||||||
回報錯誤:
|
|
||||||
Access to fetch at 'https://api.anthropic.com/v1/messages' from origin
|
|
||||||
'http://127.0.0.1:5000' has been blocked by CORS policy
|
|
||||||
```
|
|
||||||
|
|
||||||
**執行結果**: ✅ 修正完成
|
|
||||||
- 修改 index.html 的 callClaudeAPI 函數
|
|
||||||
- 建立 app_updated.py 增加 LLM endpoints
|
|
||||||
- 建立 start_server.py 處理 Windows 編碼問題
|
|
||||||
- 建立修正腳本(quick_fix.py, complete_fix.py)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Gemini API 模型設定階段
|
|
||||||
|
|
||||||
#### 3. 指定 Gemini 模型
|
|
||||||
```
|
|
||||||
指令: "我要用gemini-2.5-flash模型"
|
|
||||||
```
|
|
||||||
|
|
||||||
**執行結果**: ✅ 已更改
|
|
||||||
|
|
||||||
#### 4. 強調不要亂改
|
|
||||||
```
|
|
||||||
指令: "你不要亂改!"
|
|
||||||
```
|
|
||||||
|
|
||||||
**背景**: 我建議使用 gemini-1.5-flash,但使用者堅持使用特定版本
|
|
||||||
|
|
||||||
#### 5. 堅持使用指定模型
|
|
||||||
```
|
|
||||||
指令: "有責任我扛,你就是換成gemini-2.5-flash"
|
|
||||||
```
|
|
||||||
|
|
||||||
**執行結果**: ✅ 已修改 llm_config.py 使用 gemini-2.5-flash
|
|
||||||
|
|
||||||
#### 6. 確認當前模型
|
|
||||||
```
|
|
||||||
指令: "你現在用哪個模型"
|
|
||||||
```
|
|
||||||
|
|
||||||
**回覆**: 確認使用 gemini-2.5-flash
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Git 版本控制階段
|
|
||||||
|
|
||||||
#### 7. 推送到 Gitea
|
|
||||||
```
|
|
||||||
指令: "push to gitea"
|
|
||||||
```
|
|
||||||
|
|
||||||
**執行結果**: ✅ 成功推送
|
|
||||||
- 初始化 git repository
|
|
||||||
- 透過 Gitea API 建立 repository
|
|
||||||
- 執行 git commit 和 git push
|
|
||||||
- Repository URL: https://gitea.theaken.com/donald/hr-position-system
|
|
||||||
|
|
||||||
#### 8. 更新 SDD 文件
|
|
||||||
```
|
|
||||||
指令: "更新一份SDD文件,進行版更"
|
|
||||||
```
|
|
||||||
|
|
||||||
**執行結果**: ✅ 完成
|
|
||||||
- 更新 SDD.md 從 v1.0 到 v2.0
|
|
||||||
- 新增變更歷史區塊
|
|
||||||
- 記錄所有功能改進
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 新功能開發階段
|
|
||||||
|
|
||||||
#### 9. CSV 匯入匯出功能
|
|
||||||
```
|
|
||||||
指令: "#在每個頁籤都加入csv匯入匯出功能"
|
|
||||||
```
|
|
||||||
|
|
||||||
**執行結果**: ✅ 完成
|
|
||||||
- ✅ 建立 csv_utils.js 工具模組
|
|
||||||
- ✅ 整合到崗位資料頁籤
|
|
||||||
- ✅ 整合到職務資料頁籤
|
|
||||||
- ✅ 整合到崗位描述頁籤
|
|
||||||
|
|
||||||
**技術規格**:
|
|
||||||
- exportToCSV(): 匯出資料為 CSV 檔案
|
|
||||||
- importFromCSV(): 從 CSV 檔案匯入資料
|
|
||||||
- parseCSV(): 解析 CSV 文字
|
|
||||||
- 支援 UTF-8 BOM
|
|
||||||
- 支援引號和逗號的正確處理
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 10. 新增崗位清單頁籤
|
|
||||||
```
|
|
||||||
指令: "#增加一個崗位清單的頁籤,這個頁籤可以選擇欄位進行排序"
|
|
||||||
```
|
|
||||||
|
|
||||||
**執行結果**: ✅ 完成
|
|
||||||
|
|
||||||
**實作內容**:
|
|
||||||
- ✅ 新增獨立頁籤「崗位清單」
|
|
||||||
- ✅ 顯示所有崗位資料(表格形式)
|
|
||||||
- ✅ 支援點擊欄位標題進行排序
|
|
||||||
- ✅ 支援升序/降序切換(顯示 ^ 和 v 符號)
|
|
||||||
- ✅ 欄位包含:
|
|
||||||
- 崗位編號
|
|
||||||
- 崗位名稱
|
|
||||||
- 事業體
|
|
||||||
- 部門
|
|
||||||
- 崗位類別
|
|
||||||
- 編制人數
|
|
||||||
- 生效日期
|
|
||||||
- ✅ 支援匯出 CSV
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 11. 新增管理者頁面
|
|
||||||
```
|
|
||||||
指令: "#加入管理者頁面,建立使用者清單,清單的欄位包含工號,使用者姓名,
|
|
||||||
email信箱,使用者權限設定(一般使用者/管理者/最高權限管理者)"
|
|
||||||
```
|
|
||||||
|
|
||||||
**執行結果**: ✅ 完成
|
|
||||||
|
|
||||||
**實作內容**:
|
|
||||||
- ✅ 建立新的管理者頁面/頁籤
|
|
||||||
- ✅ 使用者清單欄位:
|
|
||||||
- 工號(Employee ID)
|
|
||||||
- 使用者姓名(User Name)
|
|
||||||
- Email 信箱
|
|
||||||
- 權限等級(三種權限層級)
|
|
||||||
- 建立日期
|
|
||||||
- ✅ 權限標籤顏色區分:
|
|
||||||
- 一般使用者:綠色
|
|
||||||
- 管理者:橘色
|
|
||||||
- 最高權限管理者:紅色
|
|
||||||
- ✅ CRUD 功能:
|
|
||||||
- 新增使用者(彈窗表單)
|
|
||||||
- 編輯使用者
|
|
||||||
- 刪除使用者(最高權限管理者不可刪除)
|
|
||||||
- ✅ 匯出使用者 CSV
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 14. 新增事業體選項和組織單位欄位
|
|
||||||
```
|
|
||||||
指令: "#在每個頁籤都加入事業體選項(SBU,MBU,HQBU,ITBU,HRBU,ACCBU)"
|
|
||||||
指令: "#增加一個處級單位,一個部級單位,一個課級單位"
|
|
||||||
```
|
|
||||||
|
|
||||||
**執行結果**: ✅ 完成
|
|
||||||
|
|
||||||
**實作內容**:
|
|
||||||
- ✅ 事業體下拉選單(6個選項):
|
|
||||||
- SBU - 銷售事業體
|
|
||||||
- MBU - 製造事業體
|
|
||||||
- HQBU - 總部事業體
|
|
||||||
- ITBU - IT事業體
|
|
||||||
- HRBU - HR事業體
|
|
||||||
- ACCBU - 會計事業體
|
|
||||||
- ✅ 處級單位欄位(Division)
|
|
||||||
- ✅ 部級單位欄位(Department)
|
|
||||||
- ✅ 課級單位欄位(Section)
|
|
||||||
- 所有欄位為選填
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 測試階段
|
|
||||||
|
|
||||||
#### 12. 測試所有服務功能
|
|
||||||
```
|
|
||||||
指令: "#測試服務的所有功能是否正常"
|
|
||||||
```
|
|
||||||
|
|
||||||
**執行結果**: ⏳ 待執行
|
|
||||||
|
|
||||||
**測試範圍**:
|
|
||||||
1. Flask 後端 API endpoints
|
|
||||||
- [ ] GET /api/positions
|
|
||||||
- [ ] GET /api/positions/<id>
|
|
||||||
- [ ] POST /api/positions
|
|
||||||
- [ ] GET /api/jobs
|
|
||||||
- [ ] LLM API endpoints
|
|
||||||
2. LLM API 連線測試
|
|
||||||
- [ ] Gemini API (gemini-2.5-flash)
|
|
||||||
- [ ] DeepSeek API
|
|
||||||
- [ ] OpenAI API
|
|
||||||
3. 前端功能測試
|
|
||||||
- [ ] 崗位資料表單(新增/查詢/編輯)
|
|
||||||
- [ ] 職務資料表單
|
|
||||||
- [ ] AI 自動填充功能
|
|
||||||
- [ ] 錯誤訊息顯示與複製
|
|
||||||
- [ ] CSV 匯入匯出(新功能)
|
|
||||||
4. 資料庫連線測試
|
|
||||||
5. Gitea repository 訪問測試
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 13. 建立指令記錄檔
|
|
||||||
```
|
|
||||||
指令: "你建立一個.md檔,記錄所有我在這個對話框裡下的指令"
|
|
||||||
```
|
|
||||||
|
|
||||||
**執行結果**: ✅ 正在建立(此文件)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 15. 建立隨機崗位資料
|
|
||||||
```
|
|
||||||
指令: "隨機建立10筆資料到崗位清單中"
|
|
||||||
選擇: excel_table copy.md 的組織資料(12-71行)
|
|
||||||
```
|
|
||||||
|
|
||||||
**執行結果**: ✅ 完成
|
|
||||||
- 建立 add_random_positions.py 腳本
|
|
||||||
- 從 77 筆組織崗位資料中隨機選取 10 筆
|
|
||||||
- 透過 API 批次建立崗位資料
|
|
||||||
- 自動對應崗位類別、等級、學歷、薪資範圍
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 16. CSV 批次匯入功能
|
|
||||||
```
|
|
||||||
指令: "建立一個<CSV匯入>的按鈕,此按鈕可以批次匯入崗位清單的資料"
|
|
||||||
指令: "建立一個<CSV匯入範本下載>的按紐,此按鈕可下載一個可以被系統讀取的崗位清單範本,具有欄位的表頭"
|
|
||||||
```
|
|
||||||
|
|
||||||
**執行結果**: ✅ 完成
|
|
||||||
|
|
||||||
**後端實作** (app_updated.py):
|
|
||||||
- ✅ GET `/api/positions/csv-template` - 下載崗位資料 CSV 範本
|
|
||||||
- ✅ POST `/api/positions/import-csv` - 批次匯入崗位資料
|
|
||||||
- ✅ GET `/api/jobs/csv-template` - 下載職務資料 CSV 範本
|
|
||||||
- ✅ POST `/api/jobs/import-csv` - 批次匯入職務資料
|
|
||||||
- 支援 UTF-8 BOM 編碼
|
|
||||||
- 完整錯誤驗證與回報
|
|
||||||
|
|
||||||
**前端實作** (index.html):
|
|
||||||
- ✅ 新增「下載範本」按鈕
|
|
||||||
- ✅ 更新 CSV 匯入函數使用 FormData API
|
|
||||||
- ✅ 顯示匯入成功/失敗統計
|
|
||||||
|
|
||||||
**重要修正**:
|
|
||||||
- 修正 Flask 路由順序:CSV 路由必須在動態路由 `<position_id>` 之前
|
|
||||||
- 修正 UTF-8 編碼問題(Windows)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 17. 崗位描述與清單整合
|
|
||||||
```
|
|
||||||
指令: "#在<崗位描述>頁籤,每次當我按下<保存並退出>,或<保存並新增>,資料都會自動新增到<崗位清單>中"
|
|
||||||
指令: "#崗位清單每次click都會自動更新"
|
|
||||||
指令: "#click崗位清單的<檢視>,會開啟<崗位描述>的對應資料"
|
|
||||||
|
|
||||||
進一步clarify:
|
|
||||||
指令: "#<崗位描述>按下<保存>按鈕後,資料會寫入資料庫"
|
|
||||||
指令: "#<崗位清單>會顯示已經建在資料庫中的資料,顯示表頭以及表身"
|
|
||||||
指令: "#<崗位清單>未顯示的表頭可先隱藏"
|
|
||||||
指令: "#<管理者頁面>中,新增一功能,可以匯出完整崗位資料的table"
|
|
||||||
```
|
|
||||||
|
|
||||||
**執行結果**: ✅ 全部完成
|
|
||||||
|
|
||||||
**後端 API 實作** (app_updated.py):
|
|
||||||
1. **崗位描述 API**:
|
|
||||||
- ✅ GET `/api/position-descriptions` - 獲取所有崗位描述
|
|
||||||
- ✅ GET `/api/position-descriptions/<position_code>` - 獲取單一崗位描述
|
|
||||||
- ✅ POST `/api/position-descriptions` - 新增或更新崗位描述
|
|
||||||
- ✅ PUT `/api/position-descriptions/<position_code>` - 更新崗位描述
|
|
||||||
- ✅ DELETE `/api/position-descriptions/<position_code>` - 刪除崗位描述
|
|
||||||
|
|
||||||
2. **崗位清單 API**:
|
|
||||||
- ✅ GET `/api/position-list` - 獲取崗位清單(結合基礎資料與描述)
|
|
||||||
- ✅ GET `/api/position-list/export` - 匯出完整崗位資料為 CSV
|
|
||||||
- 支援分頁和搜尋
|
|
||||||
- 自動合併崗位基礎資料與描述資料
|
|
||||||
|
|
||||||
**前端功能實作** (index.html):
|
|
||||||
1. **崗位描述保存**:
|
|
||||||
- ✅ 更新 `saveJobDescAndExit()` - 保存後切換到崗位清單
|
|
||||||
- ✅ 更新 `saveJobDescAndNew()` - 保存後清空表單
|
|
||||||
- 驗證必填欄位
|
|
||||||
- 顯示成功/失敗訊息
|
|
||||||
|
|
||||||
2. **崗位清單顯示**:
|
|
||||||
- ✅ 實作 `loadPositionList()` - 從 API 載入資料
|
|
||||||
- ✅ 實作 `renderPositionList()` - 渲染表格
|
|
||||||
- ✅ 實作 `viewPositionDesc()` - 檢視崗位描述
|
|
||||||
- ✅ 實作 `switchModule()` - 模組切換函數
|
|
||||||
- ✅ 更新表頭欄位(移除事業體/部門,新增崗位性質/等級)
|
|
||||||
- 自動刷新:切換到崗位清單時自動載入資料
|
|
||||||
|
|
||||||
3. **管理者頁面擴充**:
|
|
||||||
- ✅ 新增「崗位資料管理」區塊
|
|
||||||
- ✅ 實作 `exportCompletePositionData()` - 匯出完整資料
|
|
||||||
- ✅ 實作 `refreshPositionStats()` - 更新統計資料
|
|
||||||
- ✅ 顯示即時統計(總數、已描述、未描述)
|
|
||||||
- 切換到管理者頁面時自動更新統計
|
|
||||||
|
|
||||||
**資料結構**:
|
|
||||||
```typescript
|
|
||||||
interface PositionDescription {
|
|
||||||
id: string;
|
|
||||||
positionCode: string;
|
|
||||||
positionName: string;
|
|
||||||
effectiveDate: string;
|
|
||||||
jobDuties: string;
|
|
||||||
requiredSkills: string;
|
|
||||||
workEnvironment: string;
|
|
||||||
careerPath: string;
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PositionListItem {
|
|
||||||
positionCode: string;
|
|
||||||
positionName: string;
|
|
||||||
positionCategory: string;
|
|
||||||
positionNature: string;
|
|
||||||
headcount: string;
|
|
||||||
positionLevel: string;
|
|
||||||
effectiveDate: string;
|
|
||||||
hasDescription: boolean;
|
|
||||||
jobDuties: string;
|
|
||||||
requiredSkills: string;
|
|
||||||
workEnvironment: string;
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 18. 更新文件並推送 Gitea
|
|
||||||
```
|
|
||||||
指令: "更新SDD文件 and readme. push to gitea"
|
|
||||||
指令: "更新所有相關文件後push to gitea"
|
|
||||||
指令: "更新@/d:/00001_Vibe_coding/1204剛為/USER_COMMANDS_LOG.md , 但排除這個檔案上傳gitea"
|
|
||||||
```
|
|
||||||
|
|
||||||
**執行結果**: ✅ 完成
|
|
||||||
- ✅ 更新 SDD.md 至版本 2.1
|
|
||||||
- ✅ 更新 README.md 至版本 2.1
|
|
||||||
- ✅ 更新 USER_COMMANDS_LOG.md(本文件)
|
|
||||||
- ✅ 更新 .gitignore(排除 USER_COMMANDS_LOG.md)
|
|
||||||
- ✅ 成功推送至 Gitea(commit: b258477)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📊 指令統計
|
|
||||||
|
|
||||||
**總計**: 18 個指令
|
|
||||||
**已完成**: 18 個 ✅
|
|
||||||
**進行中**: 0 個
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔧 技術決策記錄
|
|
||||||
|
|
||||||
### 1. CORS 問題解決方案
|
|
||||||
- **決策**: 使用 Flask 後端作為代理,避免前端直接呼叫外部 API
|
|
||||||
- **原因**: 瀏覽器 CORS 政策限制跨域請求
|
|
||||||
- **實作**: 建立 /api/llm/generate endpoint
|
|
||||||
|
|
||||||
### 2. Gemini 模型版本
|
|
||||||
- **決策**: 使用 gemini-2.5-flash
|
|
||||||
- **原因**: 使用者明確要求並願意承擔責任
|
|
||||||
- **風險**: 該模型可能尚未正式發布
|
|
||||||
|
|
||||||
### 3. 錯誤處理方式
|
|
||||||
- **決策**: 建立可關閉、可複製的錯誤對話框
|
|
||||||
- **原因**: 使用者需要完整查看和複製錯誤訊息
|
|
||||||
- **實作**: showCopyableError() 函數
|
|
||||||
|
|
||||||
### 4. CSV 功能實作
|
|
||||||
- **決策**: 建立獨立的 csv_utils.js 模組
|
|
||||||
- **原因**: 模組化設計,可重複使用於多個頁籤
|
|
||||||
- **優點**: 維護容易,功能統一
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 下一步行動計畫
|
|
||||||
|
|
||||||
### 優先順序 1: 完成 CSV 整合
|
|
||||||
- [ ] 在崗位資料頁籤加入 CSV 按鈕
|
|
||||||
- [ ] 在職務資料頁籤加入 CSV 按鈕
|
|
||||||
- [ ] 在崗位描述頁籤加入 CSV 按鈕
|
|
||||||
- [ ] 測試 CSV 匯入匯出功能
|
|
||||||
|
|
||||||
### 優先順序 2: 建立崗位清單頁籤
|
|
||||||
- [ ] 設計頁籤 UI
|
|
||||||
- [ ] 實作欄位排序功能
|
|
||||||
- [ ] 測試排序功能
|
|
||||||
|
|
||||||
### 優先順序 3: 建立管理者頁面
|
|
||||||
- [ ] 設計資料庫 schema(users 表)
|
|
||||||
- [ ] 建立後端 API(/api/users)
|
|
||||||
- [ ] 建立前端管理介面
|
|
||||||
- [ ] 實作 CRUD 功能
|
|
||||||
- [ ] 加入權限控制
|
|
||||||
|
|
||||||
### 優先順序 4: 全面測試
|
|
||||||
- [ ] 執行所有功能測試
|
|
||||||
- [ ] 修正發現的問題
|
|
||||||
- [ ] 更新文件
|
|
||||||
|
|
||||||
### 優先順序 5: 版本控制
|
|
||||||
- [ ] Commit 新功能
|
|
||||||
- [ ] 更新 SDD 到 v3.0
|
|
||||||
- [ ] Push to Gitea
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📝 備註
|
|
||||||
|
|
||||||
### 系統環境
|
|
||||||
- **作業系統**: Windows
|
|
||||||
- **Python 版本**: 3.x
|
|
||||||
- **資料庫**: MySQL (mysql.theaken.com:33306)
|
|
||||||
- **Git 服務**: Gitea (https://gitea.theaken.com/)
|
|
||||||
- **Flask 端口**: 5000
|
|
||||||
|
|
||||||
### 已知問題
|
|
||||||
1. ✅ CORS 錯誤 - 已修正
|
|
||||||
2. ✅ Windows 編碼錯誤 - 已修正
|
|
||||||
3. ✅ 錯誤對話框無法關閉 - 已修正
|
|
||||||
4. ⚠️ Gemini API Referrer 限制 - 需要使用者自行設定 API Key
|
|
||||||
|
|
||||||
### 重要文件清單
|
|
||||||
1. `.env` - 環境變數設定
|
|
||||||
2. `SDD.md` - 系統設計文件(v2.0)
|
|
||||||
3. `llm_config.py` - LLM API 設定(gemini-2.5-flash)
|
|
||||||
4. `start_server.py` - Flask 伺服器啟動腳本
|
|
||||||
5. `csv_utils.js` - CSV 工具模組
|
|
||||||
6. `error_handler.js` - 錯誤處理模組
|
|
||||||
7. `api_test.html` - API 測試頁面
|
|
||||||
8. `SETUP.md` - 安裝指南
|
|
||||||
9. `CORS_FIX_GUIDE.md` - CORS 修正指南
|
|
||||||
10. `GEMINI_API_FIX.md` - Gemini API 修正指南
|
|
||||||
11. `USER_COMMANDS_LOG.md` - 本文件
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**文件建立時間**: 2024-12-04
|
|
||||||
**最後更新**: 2024-12-04
|
|
||||||
**維護者**: Claude Code
|
|
||||||
**專案狀態**: 🚧 開發中
|
|
||||||
329
app.py
329
app.py
@@ -27,12 +27,23 @@ except ImportError:
|
|||||||
print("Warning: llm_config not found. LLM features will be disabled.")
|
print("Warning: llm_config not found. LLM features will be disabled.")
|
||||||
LLM_ENABLED = False
|
LLM_ENABLED = False
|
||||||
|
|
||||||
|
# Import hierarchy data
|
||||||
|
try:
|
||||||
|
from import_hierarchy_data import import_to_memory
|
||||||
|
hierarchy_data = import_to_memory()
|
||||||
|
HIERARCHY_ENABLED = True
|
||||||
|
print("Hierarchy data loaded successfully.")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Warning: Could not load hierarchy data: {e}")
|
||||||
|
hierarchy_data = None
|
||||||
|
HIERARCHY_ENABLED = False
|
||||||
|
|
||||||
app = Flask(__name__, static_folder='.')
|
app = Flask(__name__, static_folder='.')
|
||||||
app.config['JSON_AS_ASCII'] = False # 確保 JSON 正確處理中文
|
app.config['JSON_AS_ASCII'] = False # 確保 JSON 正確處理中文
|
||||||
CORS(app)
|
CORS(app)
|
||||||
|
|
||||||
# 模擬資料庫 (實際應用中應使用 MySQL/PostgreSQL)
|
# 模擬資料庫 (實際應用中應使用 MySQL/PostgreSQL)
|
||||||
positions_db = {}
|
HR_position_positions_db = {}
|
||||||
|
|
||||||
# 預設崗位資料
|
# 預設崗位資料
|
||||||
default_positions = {
|
default_positions = {
|
||||||
@@ -76,10 +87,10 @@ default_positions = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
positions_db.update(default_positions)
|
HR_position_positions_db.update(default_positions)
|
||||||
|
|
||||||
# 職務資料庫
|
# 職務資料庫
|
||||||
jobs_db = {}
|
HR_position_jobs_db = {}
|
||||||
|
|
||||||
# 預設職務資料
|
# 預設職務資料
|
||||||
default_jobs = {
|
default_jobs = {
|
||||||
@@ -102,10 +113,10 @@ default_jobs = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
jobs_db.update(default_jobs)
|
HR_position_jobs_db.update(default_jobs)
|
||||||
|
|
||||||
# 崗位描述資料庫
|
# 崗位描述資料庫
|
||||||
position_descriptions_db = {}
|
HR_position_descriptions_db = {}
|
||||||
|
|
||||||
# 預設崗位描述資料
|
# 預設崗位描述資料
|
||||||
default_descriptions = {
|
default_descriptions = {
|
||||||
@@ -123,7 +134,7 @@ default_descriptions = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
position_descriptions_db.update(default_descriptions)
|
HR_position_descriptions_db.update(default_descriptions)
|
||||||
|
|
||||||
|
|
||||||
# ==================== 靜態頁面 ====================
|
# ==================== 靜態頁面 ====================
|
||||||
@@ -267,7 +278,7 @@ def import_positions_csv():
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# 檢查是否已存在
|
# 檢查是否已存在
|
||||||
if position_code in positions_db:
|
if position_code in HR_position_positions_db:
|
||||||
errors.append(f"第 {row_num} 列: 崗位編號 {position_code} 已存在")
|
errors.append(f"第 {row_num} 列: 崗位編號 {position_code} 已存在")
|
||||||
error_count += 1
|
error_count += 1
|
||||||
continue
|
continue
|
||||||
@@ -313,7 +324,7 @@ def import_positions_csv():
|
|||||||
'updatedAt': now
|
'updatedAt': now
|
||||||
}
|
}
|
||||||
|
|
||||||
positions_db[position_code] = new_position
|
HR_position_positions_db[position_code] = new_position
|
||||||
success_count += 1
|
success_count += 1
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -349,7 +360,7 @@ def get_positions():
|
|||||||
search = request.args.get('search', '', type=str)
|
search = request.args.get('search', '', type=str)
|
||||||
|
|
||||||
# 過濾搜尋
|
# 過濾搜尋
|
||||||
filtered = list(positions_db.values())
|
filtered = list(HR_position_positions_db.values())
|
||||||
if search:
|
if search:
|
||||||
filtered = [
|
filtered = [
|
||||||
p for p in filtered
|
p for p in filtered
|
||||||
@@ -378,7 +389,7 @@ def get_positions():
|
|||||||
@app.route('/api/positions/<position_id>', methods=['GET'])
|
@app.route('/api/positions/<position_id>', methods=['GET'])
|
||||||
def get_position(position_id):
|
def get_position(position_id):
|
||||||
"""獲取單一崗位資料"""
|
"""獲取單一崗位資料"""
|
||||||
if position_id not in positions_db:
|
if position_id not in HR_position_positions_db:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': False,
|
'success': False,
|
||||||
'error': '找不到該崗位資料'
|
'error': '找不到該崗位資料'
|
||||||
@@ -386,7 +397,7 @@ def get_position(position_id):
|
|||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': True,
|
'success': True,
|
||||||
'data': positions_db[position_id]
|
'data': HR_position_positions_db[position_id]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -425,7 +436,7 @@ def create_position():
|
|||||||
position_code = basic_info['positionCode']
|
position_code = basic_info['positionCode']
|
||||||
|
|
||||||
# 檢查是否已存在
|
# 檢查是否已存在
|
||||||
if position_code in positions_db:
|
if position_code in HR_position_positions_db:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': False,
|
'success': False,
|
||||||
'error': f'崗位編號 {position_code} 已存在'
|
'error': f'崗位編號 {position_code} 已存在'
|
||||||
@@ -441,7 +452,7 @@ def create_position():
|
|||||||
'updatedAt': now
|
'updatedAt': now
|
||||||
}
|
}
|
||||||
|
|
||||||
positions_db[position_code] = new_position
|
HR_position_positions_db[position_code] = new_position
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': True,
|
'success': True,
|
||||||
@@ -465,7 +476,7 @@ def update_position(position_id):
|
|||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
if position_id not in positions_db:
|
if position_id not in HR_position_positions_db:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': False,
|
'success': False,
|
||||||
'error': '找不到該崗位資料'
|
'error': '找不到該崗位資料'
|
||||||
@@ -479,7 +490,7 @@ def update_position(position_id):
|
|||||||
}), 400
|
}), 400
|
||||||
|
|
||||||
# 更新資料
|
# 更新資料
|
||||||
existing = positions_db[position_id]
|
existing = HR_position_positions_db[position_id]
|
||||||
|
|
||||||
if 'basicInfo' in data:
|
if 'basicInfo' in data:
|
||||||
existing['basicInfo'].update(data['basicInfo'])
|
existing['basicInfo'].update(data['basicInfo'])
|
||||||
@@ -505,13 +516,13 @@ def update_position(position_id):
|
|||||||
def delete_position(position_id):
|
def delete_position(position_id):
|
||||||
"""刪除崗位資料"""
|
"""刪除崗位資料"""
|
||||||
try:
|
try:
|
||||||
if position_id not in positions_db:
|
if position_id not in HR_position_positions_db:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': False,
|
'success': False,
|
||||||
'error': '找不到該崗位資料'
|
'error': '找不到該崗位資料'
|
||||||
}), 404
|
}), 404
|
||||||
|
|
||||||
deleted = positions_db.pop(position_id)
|
deleted = HR_position_positions_db.pop(position_id)
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': True,
|
'success': True,
|
||||||
@@ -532,7 +543,7 @@ def change_position_code(position_id):
|
|||||||
Request body: { newCode: "新編號" }
|
Request body: { newCode: "新編號" }
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
if position_id not in positions_db:
|
if position_id not in HR_position_positions_db:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': False,
|
'success': False,
|
||||||
'error': '找不到該崗位資料'
|
'error': '找不到該崗位資料'
|
||||||
@@ -547,19 +558,19 @@ def change_position_code(position_id):
|
|||||||
'error': '請提供新的崗位編號'
|
'error': '請提供新的崗位編號'
|
||||||
}), 400
|
}), 400
|
||||||
|
|
||||||
if new_code in positions_db:
|
if new_code in HR_position_positions_db:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': False,
|
'success': False,
|
||||||
'error': f'崗位編號 {new_code} 已存在'
|
'error': f'崗位編號 {new_code} 已存在'
|
||||||
}), 409
|
}), 409
|
||||||
|
|
||||||
# 更新編號
|
# 更新編號
|
||||||
position = positions_db.pop(position_id)
|
position = HR_position_positions_db.pop(position_id)
|
||||||
position['id'] = new_code
|
position['id'] = new_code
|
||||||
position['basicInfo']['positionCode'] = new_code
|
position['basicInfo']['positionCode'] = new_code
|
||||||
position['updatedAt'] = datetime.now().isoformat()
|
position['updatedAt'] = datetime.now().isoformat()
|
||||||
|
|
||||||
positions_db[new_code] = position
|
HR_position_positions_db[new_code] = position
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': True,
|
'success': True,
|
||||||
@@ -662,7 +673,7 @@ def import_jobs_csv():
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# 檢查是否已存在
|
# 檢查是否已存在
|
||||||
if job_code in jobs_db:
|
if job_code in HR_position_jobs_db:
|
||||||
errors.append(f"第 {row_num} 列: 職務編號 {job_code} 已存在")
|
errors.append(f"第 {row_num} 列: 職務編號 {job_code} 已存在")
|
||||||
error_count += 1
|
error_count += 1
|
||||||
continue
|
continue
|
||||||
@@ -687,7 +698,7 @@ def import_jobs_csv():
|
|||||||
'updatedAt': now
|
'updatedAt': now
|
||||||
}
|
}
|
||||||
|
|
||||||
jobs_db[job_code] = new_job
|
HR_position_jobs_db[job_code] = new_job
|
||||||
success_count += 1
|
success_count += 1
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -725,7 +736,7 @@ def get_jobs():
|
|||||||
category = request.args.get('category', '', type=str)
|
category = request.args.get('category', '', type=str)
|
||||||
|
|
||||||
# 過濾搜尋
|
# 過濾搜尋
|
||||||
filtered = list(jobs_db.values())
|
filtered = list(HR_position_jobs_db.values())
|
||||||
if search:
|
if search:
|
||||||
filtered = [
|
filtered = [
|
||||||
j for j in filtered
|
j for j in filtered
|
||||||
@@ -759,7 +770,7 @@ def get_jobs():
|
|||||||
@app.route('/api/jobs/<job_id>', methods=['GET'])
|
@app.route('/api/jobs/<job_id>', methods=['GET'])
|
||||||
def get_job(job_id):
|
def get_job(job_id):
|
||||||
"""獲取單一職務資料"""
|
"""獲取單一職務資料"""
|
||||||
if job_id not in jobs_db:
|
if job_id not in HR_position_jobs_db:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': False,
|
'success': False,
|
||||||
'error': '找不到該職務資料'
|
'error': '找不到該職務資料'
|
||||||
@@ -767,7 +778,7 @@ def get_job(job_id):
|
|||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': True,
|
'success': True,
|
||||||
'data': jobs_db[job_id]
|
'data': HR_position_jobs_db[job_id]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -814,7 +825,7 @@ def create_job():
|
|||||||
job_code = data['jobCode']
|
job_code = data['jobCode']
|
||||||
|
|
||||||
# 檢查是否已存在
|
# 檢查是否已存在
|
||||||
if job_code in jobs_db:
|
if job_code in HR_position_jobs_db:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': False,
|
'success': False,
|
||||||
'error': f'職務編號 {job_code} 已存在'
|
'error': f'職務編號 {job_code} 已存在'
|
||||||
@@ -829,7 +840,7 @@ def create_job():
|
|||||||
'updatedAt': now
|
'updatedAt': now
|
||||||
}
|
}
|
||||||
|
|
||||||
jobs_db[job_code] = new_job
|
HR_position_jobs_db[job_code] = new_job
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': True,
|
'success': True,
|
||||||
@@ -847,7 +858,7 @@ def create_job():
|
|||||||
def update_job(job_id):
|
def update_job(job_id):
|
||||||
"""更新職務資料"""
|
"""更新職務資料"""
|
||||||
try:
|
try:
|
||||||
if job_id not in jobs_db:
|
if job_id not in HR_position_jobs_db:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': False,
|
'success': False,
|
||||||
'error': '找不到該職務資料'
|
'error': '找不到該職務資料'
|
||||||
@@ -861,7 +872,7 @@ def update_job(job_id):
|
|||||||
}), 400
|
}), 400
|
||||||
|
|
||||||
# 更新資料
|
# 更新資料
|
||||||
existing = jobs_db[job_id]
|
existing = HR_position_jobs_db[job_id]
|
||||||
existing.update(data)
|
existing.update(data)
|
||||||
existing['updatedAt'] = datetime.now().isoformat()
|
existing['updatedAt'] = datetime.now().isoformat()
|
||||||
|
|
||||||
@@ -881,13 +892,13 @@ def update_job(job_id):
|
|||||||
def delete_job(job_id):
|
def delete_job(job_id):
|
||||||
"""刪除職務資料"""
|
"""刪除職務資料"""
|
||||||
try:
|
try:
|
||||||
if job_id not in jobs_db:
|
if job_id not in HR_position_jobs_db:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': False,
|
'success': False,
|
||||||
'error': '找不到該職務資料'
|
'error': '找不到該職務資料'
|
||||||
}), 404
|
}), 404
|
||||||
|
|
||||||
deleted = jobs_db.pop(job_id)
|
deleted = HR_position_jobs_db.pop(job_id)
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': True,
|
'success': True,
|
||||||
@@ -908,7 +919,7 @@ def change_job_code(job_id):
|
|||||||
Request body: { newCode: "新編號" }
|
Request body: { newCode: "新編號" }
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
if job_id not in jobs_db:
|
if job_id not in HR_position_jobs_db:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': False,
|
'success': False,
|
||||||
'error': '找不到該職務資料'
|
'error': '找不到該職務資料'
|
||||||
@@ -923,19 +934,19 @@ def change_job_code(job_id):
|
|||||||
'error': '請提供新的職務編號'
|
'error': '請提供新的職務編號'
|
||||||
}), 400
|
}), 400
|
||||||
|
|
||||||
if new_code in jobs_db:
|
if new_code in HR_position_jobs_db:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': False,
|
'success': False,
|
||||||
'error': f'職務編號 {new_code} 已存在'
|
'error': f'職務編號 {new_code} 已存在'
|
||||||
}), 409
|
}), 409
|
||||||
|
|
||||||
# 更新編號
|
# 更新編號
|
||||||
job = jobs_db.pop(job_id)
|
job = HR_position_jobs_db.pop(job_id)
|
||||||
job['id'] = new_code
|
job['id'] = new_code
|
||||||
job['jobCode'] = new_code
|
job['jobCode'] = new_code
|
||||||
job['updatedAt'] = datetime.now().isoformat()
|
job['updatedAt'] = datetime.now().isoformat()
|
||||||
|
|
||||||
jobs_db[new_code] = job
|
HR_position_jobs_db[new_code] = job
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': True,
|
'success': True,
|
||||||
@@ -1040,14 +1051,14 @@ def get_position_descriptions():
|
|||||||
"""獲取所有崗位描述"""
|
"""獲取所有崗位描述"""
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': True,
|
'success': True,
|
||||||
'data': list(position_descriptions_db.values())
|
'data': list(HR_position_descriptions_db.values())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/position-descriptions/<position_code>', methods=['GET'])
|
@app.route('/api/position-descriptions/<position_code>', methods=['GET'])
|
||||||
def get_position_description(position_code):
|
def get_position_description(position_code):
|
||||||
"""獲取單一崗位描述"""
|
"""獲取單一崗位描述"""
|
||||||
if position_code not in position_descriptions_db:
|
if position_code not in HR_position_descriptions_db:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': False,
|
'success': False,
|
||||||
'error': '找不到該崗位描述'
|
'error': '找不到該崗位描述'
|
||||||
@@ -1055,7 +1066,7 @@ def get_position_description(position_code):
|
|||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': True,
|
'success': True,
|
||||||
'data': position_descriptions_db[position_code]
|
'data': HR_position_descriptions_db[position_code]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -1087,7 +1098,7 @@ def create_position_description():
|
|||||||
}), 400
|
}), 400
|
||||||
|
|
||||||
# 檢查崗位是否存在(暫時註解,允許直接新增描述)
|
# 檢查崗位是否存在(暫時註解,允許直接新增描述)
|
||||||
# if position_code not in positions_db:
|
# if position_code not in HR_position_positions_db:
|
||||||
# return jsonify({
|
# return jsonify({
|
||||||
# 'success': False,
|
# 'success': False,
|
||||||
# 'error': f'崗位編號 {position_code} 不存在,請先建立崗位基礎資料'
|
# 'error': f'崗位編號 {position_code} 不存在,請先建立崗位基礎資料'
|
||||||
@@ -1096,14 +1107,14 @@ def create_position_description():
|
|||||||
now = datetime.now().isoformat()
|
now = datetime.now().isoformat()
|
||||||
|
|
||||||
# 如果已存在則更新,否則新增
|
# 如果已存在則更新,否則新增
|
||||||
if position_code in position_descriptions_db:
|
if position_code in HR_position_descriptions_db:
|
||||||
position_descriptions_db[position_code].update({
|
HR_position_descriptions_db[position_code].update({
|
||||||
**data,
|
**data,
|
||||||
'updatedAt': now
|
'updatedAt': now
|
||||||
})
|
})
|
||||||
message = '崗位描述更新成功'
|
message = '崗位描述更新成功'
|
||||||
else:
|
else:
|
||||||
position_descriptions_db[position_code] = {
|
HR_position_descriptions_db[position_code] = {
|
||||||
'id': position_code,
|
'id': position_code,
|
||||||
**data,
|
**data,
|
||||||
'createdAt': now,
|
'createdAt': now,
|
||||||
@@ -1114,7 +1125,7 @@ def create_position_description():
|
|||||||
return jsonify({
|
return jsonify({
|
||||||
'success': True,
|
'success': True,
|
||||||
'message': message,
|
'message': message,
|
||||||
'data': position_descriptions_db[position_code]
|
'data': HR_position_descriptions_db[position_code]
|
||||||
})
|
})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -1128,7 +1139,7 @@ def create_position_description():
|
|||||||
def update_position_description(position_code):
|
def update_position_description(position_code):
|
||||||
"""更新崗位描述"""
|
"""更新崗位描述"""
|
||||||
try:
|
try:
|
||||||
if position_code not in position_descriptions_db:
|
if position_code not in HR_position_descriptions_db:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': False,
|
'success': False,
|
||||||
'error': '找不到該崗位描述'
|
'error': '找不到該崗位描述'
|
||||||
@@ -1141,7 +1152,7 @@ def update_position_description(position_code):
|
|||||||
'error': '請提供有效的 JSON 資料'
|
'error': '請提供有效的 JSON 資料'
|
||||||
}), 400
|
}), 400
|
||||||
|
|
||||||
position_descriptions_db[position_code].update({
|
HR_position_descriptions_db[position_code].update({
|
||||||
**data,
|
**data,
|
||||||
'updatedAt': datetime.now().isoformat()
|
'updatedAt': datetime.now().isoformat()
|
||||||
})
|
})
|
||||||
@@ -1149,7 +1160,7 @@ def update_position_description(position_code):
|
|||||||
return jsonify({
|
return jsonify({
|
||||||
'success': True,
|
'success': True,
|
||||||
'message': '崗位描述更新成功',
|
'message': '崗位描述更新成功',
|
||||||
'data': position_descriptions_db[position_code]
|
'data': HR_position_descriptions_db[position_code]
|
||||||
})
|
})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -1163,13 +1174,13 @@ def update_position_description(position_code):
|
|||||||
def delete_position_description(position_code):
|
def delete_position_description(position_code):
|
||||||
"""刪除崗位描述"""
|
"""刪除崗位描述"""
|
||||||
try:
|
try:
|
||||||
if position_code not in position_descriptions_db:
|
if position_code not in HR_position_descriptions_db:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': False,
|
'success': False,
|
||||||
'error': '找不到該崗位描述'
|
'error': '找不到該崗位描述'
|
||||||
}), 404
|
}), 404
|
||||||
|
|
||||||
deleted = position_descriptions_db.pop(position_code)
|
deleted = HR_position_descriptions_db.pop(position_code)
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': True,
|
'success': True,
|
||||||
@@ -1201,8 +1212,8 @@ def get_position_list():
|
|||||||
|
|
||||||
# 組合崗位資料和描述
|
# 組合崗位資料和描述
|
||||||
combined_list = []
|
combined_list = []
|
||||||
for position_code, position in positions_db.items():
|
for position_code, position in HR_position_positions_db.items():
|
||||||
description = position_descriptions_db.get(position_code, {})
|
description = HR_position_descriptions_db.get(position_code, {})
|
||||||
|
|
||||||
combined = {
|
combined = {
|
||||||
'positionCode': position_code,
|
'positionCode': position_code,
|
||||||
@@ -1214,7 +1225,7 @@ def get_position_list():
|
|||||||
'effectiveDate': position['basicInfo'].get('effectiveDate', ''),
|
'effectiveDate': position['basicInfo'].get('effectiveDate', ''),
|
||||||
'minEducation': position['recruitInfo'].get('minEducation', ''),
|
'minEducation': position['recruitInfo'].get('minEducation', ''),
|
||||||
'salaryRange': position['recruitInfo'].get('salaryRange', ''),
|
'salaryRange': position['recruitInfo'].get('salaryRange', ''),
|
||||||
'hasDescription': position_code in position_descriptions_db,
|
'hasDescription': position_code in HR_position_descriptions_db,
|
||||||
'jobDuties': description.get('jobDuties', ''),
|
'jobDuties': description.get('jobDuties', ''),
|
||||||
'requiredSkills': description.get('requiredSkills', ''),
|
'requiredSkills': description.get('requiredSkills', ''),
|
||||||
'workEnvironment': description.get('workEnvironment', ''),
|
'workEnvironment': description.get('workEnvironment', ''),
|
||||||
@@ -1262,8 +1273,8 @@ def export_position_list():
|
|||||||
|
|
||||||
# 組合所有崗位資料
|
# 組合所有崗位資料
|
||||||
rows = []
|
rows = []
|
||||||
for position_code, position in positions_db.items():
|
for position_code, position in HR_position_positions_db.items():
|
||||||
description = position_descriptions_db.get(position_code, {})
|
description = HR_position_descriptions_db.get(position_code, {})
|
||||||
|
|
||||||
row = [
|
row = [
|
||||||
position_code,
|
position_code,
|
||||||
@@ -1477,6 +1488,198 @@ def generate_llm_text():
|
|||||||
}), 500
|
}), 500
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== 組織階層 API ====================
|
||||||
|
|
||||||
|
@app.route('/api/hierarchy/business-units', methods=['GET'])
|
||||||
|
def get_business_units():
|
||||||
|
"""獲取所有事業體"""
|
||||||
|
if not HIERARCHY_ENABLED:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'error': '組織階層功能未啟用'
|
||||||
|
}), 503
|
||||||
|
|
||||||
|
business_list = [
|
||||||
|
{'id': v['id'], 'code': v['code'], 'name': v['name']}
|
||||||
|
for v in hierarchy_data['business_units'].values()
|
||||||
|
]
|
||||||
|
business_list.sort(key=lambda x: x['id'])
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'data': business_list
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/hierarchy/divisions', methods=['GET'])
|
||||||
|
def get_divisions():
|
||||||
|
"""獲取處級單位,可按事業體篩選"""
|
||||||
|
if not HIERARCHY_ENABLED:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'error': '組織階層功能未啟用'
|
||||||
|
}), 503
|
||||||
|
|
||||||
|
business_name = request.args.get('business', '', type=str)
|
||||||
|
|
||||||
|
if business_name:
|
||||||
|
# 根據事業體篩選處級單位
|
||||||
|
division_names = hierarchy_data['businessToDivision'].get(business_name, [])
|
||||||
|
division_list = [
|
||||||
|
hierarchy_data['divisions'][name]
|
||||||
|
for name in division_names
|
||||||
|
if name in hierarchy_data['divisions']
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
division_list = list(hierarchy_data['divisions'].values())
|
||||||
|
|
||||||
|
division_list.sort(key=lambda x: x['id'])
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'data': division_list
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/hierarchy/departments', methods=['GET'])
|
||||||
|
def get_departments():
|
||||||
|
"""獲取部級單位,可按處級單位篩選"""
|
||||||
|
if not HIERARCHY_ENABLED:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'error': '組織階層功能未啟用'
|
||||||
|
}), 503
|
||||||
|
|
||||||
|
division_name = request.args.get('division', '', type=str)
|
||||||
|
|
||||||
|
if division_name:
|
||||||
|
# 根據處級單位篩選部級單位
|
||||||
|
dept_names = hierarchy_data['divisionToDepartment'].get(division_name, [])
|
||||||
|
dept_list = [
|
||||||
|
hierarchy_data['departments'][name]
|
||||||
|
for name in dept_names
|
||||||
|
if name in hierarchy_data['departments']
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
dept_list = list(hierarchy_data['departments'].values())
|
||||||
|
|
||||||
|
dept_list.sort(key=lambda x: x['id'])
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'data': dept_list
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/hierarchy/positions', methods=['GET'])
|
||||||
|
def get_hierarchy_positions():
|
||||||
|
"""獲取崗位名稱,可按部級單位篩選"""
|
||||||
|
if not HIERARCHY_ENABLED:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'error': '組織階層功能未啟用'
|
||||||
|
}), 503
|
||||||
|
|
||||||
|
department_name = request.args.get('department', '', type=str)
|
||||||
|
|
||||||
|
if department_name:
|
||||||
|
# 根據部級單位篩選崗位
|
||||||
|
positions = hierarchy_data['departmentToPosition'].get(department_name, [])
|
||||||
|
else:
|
||||||
|
# 返回所有不重複的崗位名稱
|
||||||
|
all_positions = set()
|
||||||
|
for pos_list in hierarchy_data['departmentToPosition'].values():
|
||||||
|
all_positions.update(pos_list)
|
||||||
|
positions = sorted(list(all_positions))
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'data': positions
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/hierarchy/full', methods=['GET'])
|
||||||
|
def get_full_hierarchy():
|
||||||
|
"""獲取完整階層資料"""
|
||||||
|
if not HIERARCHY_ENABLED:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'error': '組織階層功能未啟用'
|
||||||
|
}), 503
|
||||||
|
|
||||||
|
page = request.args.get('page', 1, type=int)
|
||||||
|
size = request.args.get('size', 50, type=int)
|
||||||
|
business = request.args.get('business', '', type=str)
|
||||||
|
division = request.args.get('division', '', type=str)
|
||||||
|
department = request.args.get('department', '', type=str)
|
||||||
|
|
||||||
|
# 篩選資料
|
||||||
|
filtered = hierarchy_data['organization_positions']
|
||||||
|
|
||||||
|
if business:
|
||||||
|
filtered = [r for r in filtered if r['business'] == business]
|
||||||
|
if division:
|
||||||
|
filtered = [r for r in filtered if r['division'] == division]
|
||||||
|
if department:
|
||||||
|
filtered = [r for r in filtered if r['department'] == department]
|
||||||
|
|
||||||
|
# 分頁
|
||||||
|
total = len(filtered)
|
||||||
|
start = (page - 1) * size
|
||||||
|
end = start + size
|
||||||
|
paginated = filtered[start:end]
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'data': paginated,
|
||||||
|
'pagination': {
|
||||||
|
'page': page,
|
||||||
|
'size': size,
|
||||||
|
'total': total,
|
||||||
|
'totalPages': (total + size - 1) // size
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/hierarchy/cascade', methods=['GET'])
|
||||||
|
def get_cascade_data():
|
||||||
|
"""獲取級聯選擇資料"""
|
||||||
|
if not HIERARCHY_ENABLED:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'error': '組織階層功能未啟用'
|
||||||
|
}), 503
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'data': {
|
||||||
|
'businessToDivision': hierarchy_data['businessToDivision'],
|
||||||
|
'divisionToDepartment': hierarchy_data['divisionToDepartment'],
|
||||||
|
'departmentToPosition': hierarchy_data['departmentToPosition']
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/hierarchy/stats', methods=['GET'])
|
||||||
|
def get_hierarchy_stats():
|
||||||
|
"""獲取組織階層統計"""
|
||||||
|
if not HIERARCHY_ENABLED:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'error': '組織階層功能未啟用'
|
||||||
|
}), 503
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'data': {
|
||||||
|
'businessUnitsCount': len(hierarchy_data['business_units']),
|
||||||
|
'divisionsCount': len(hierarchy_data['divisions']),
|
||||||
|
'departmentsCount': len(hierarchy_data['departments']),
|
||||||
|
'organizationPositionsCount': len(hierarchy_data['organization_positions'])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
# ==================== 錯誤處理 ====================
|
# ==================== 錯誤處理 ====================
|
||||||
|
|
||||||
@app.errorhandler(404)
|
@app.errorhandler(404)
|
||||||
@@ -1529,6 +1732,15 @@ if __name__ == '__main__':
|
|||||||
║ GET /api/llm/test-all - 測試所有 API ║
|
║ GET /api/llm/test-all - 測試所有 API ║
|
||||||
║ POST /api/llm/generate - 生成文字 ║
|
║ POST /api/llm/generate - 生成文字 ║
|
||||||
║ ║
|
║ ║
|
||||||
|
║ 組織階層 API: ║
|
||||||
|
║ GET /api/hierarchy/business-units - 獲取事業體 ║
|
||||||
|
║ GET /api/hierarchy/divisions - 獲取處級單位 ║
|
||||||
|
║ GET /api/hierarchy/departments - 獲取部級單位 ║
|
||||||
|
║ GET /api/hierarchy/positions - 獲取崗位名稱 ║
|
||||||
|
║ GET /api/hierarchy/full - 完整階層資料 ║
|
||||||
|
║ GET /api/hierarchy/cascade - 級聯選擇資料 ║
|
||||||
|
║ GET /api/hierarchy/stats - 組織統計 ║
|
||||||
|
║ ║
|
||||||
║ 按 Ctrl+C 停止伺服器 ║
|
║ 按 Ctrl+C 停止伺服器 ║
|
||||||
╚══════════════════════════════════════════════════════════════╝
|
╚══════════════════════════════════════════════════════════════╝
|
||||||
""")
|
""")
|
||||||
@@ -1543,5 +1755,14 @@ if __name__ == '__main__':
|
|||||||
else:
|
else:
|
||||||
print("[!] LLM 功能未啟用 (llm_config.py 未找到)")
|
print("[!] LLM 功能未啟用 (llm_config.py 未找到)")
|
||||||
|
|
||||||
|
if HIERARCHY_ENABLED:
|
||||||
|
print("[OK] 組織階層功能已啟用")
|
||||||
|
print(f" - 事業體: {len(hierarchy_data['business_units'])} 筆")
|
||||||
|
print(f" - 處級單位: {len(hierarchy_data['divisions'])} 筆")
|
||||||
|
print(f" - 部級單位: {len(hierarchy_data['departments'])} 筆")
|
||||||
|
print(f" - 組織崗位關聯: {len(hierarchy_data['organization_positions'])} 筆")
|
||||||
|
else:
|
||||||
|
print("[!] 組織階層功能未啟用")
|
||||||
|
|
||||||
print()
|
print()
|
||||||
app.run(host='0.0.0.0', port=5000, debug=True)
|
app.run(host='0.0.0.0', port=5000, debug=True)
|
||||||
|
|||||||
83
clear_database.sql
Normal file
83
clear_database.sql
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
-- HR Position System - 清空資料庫腳本
|
||||||
|
-- 只清空業務資料,保留表結構和參照代碼
|
||||||
|
-- 執行前請確認備份重要資料
|
||||||
|
|
||||||
|
USE hr_position_system;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 1. 停用外鍵檢查(避免刪除順序問題)
|
||||||
|
-- ============================================================
|
||||||
|
SET FOREIGN_KEY_CHECKS = 0;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 2. 清空業務資料表(保留表結構)
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- 清空組織崗位關聯表
|
||||||
|
TRUNCATE TABLE HR_position_organization_positions;
|
||||||
|
|
||||||
|
-- 清空部級單位表
|
||||||
|
TRUNCATE TABLE HR_position_departments;
|
||||||
|
|
||||||
|
-- 清空處級單位表
|
||||||
|
TRUNCATE TABLE HR_position_divisions;
|
||||||
|
|
||||||
|
-- 清空事業體表
|
||||||
|
TRUNCATE TABLE HR_position_business_units;
|
||||||
|
|
||||||
|
-- 清空崗位描述表
|
||||||
|
TRUNCATE TABLE HR_position_job_descriptions;
|
||||||
|
|
||||||
|
-- 清空職務基礎資料表
|
||||||
|
TRUNCATE TABLE HR_position_jobs;
|
||||||
|
|
||||||
|
-- 清空崗位基礎資料表
|
||||||
|
TRUNCATE TABLE HR_position_positions;
|
||||||
|
|
||||||
|
-- 清空審計日誌表
|
||||||
|
TRUNCATE TABLE HR_position_audit_logs;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 3. 重新啟用外鍵檢查
|
||||||
|
-- ============================================================
|
||||||
|
SET FOREIGN_KEY_CHECKS = 1;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 4. 確認參照代碼表資料保留(不清空)
|
||||||
|
-- ============================================================
|
||||||
|
-- reference_codes 表保留所有參照代碼資料
|
||||||
|
-- 包含:POSITION_CATEGORY, POSITION_NATURE, POSITION_LEVEL,
|
||||||
|
-- JOB_CATEGORY, EDUCATION, SALARY_RANGE, WORK_LOCATION,
|
||||||
|
-- EMP_ATTRIBUTE, RECRUIT_POSITION, TITLE_REQ
|
||||||
|
|
||||||
|
-- 確認參照代碼數量
|
||||||
|
SELECT code_type, COUNT(*) as count
|
||||||
|
FROM HR_position_reference_codes
|
||||||
|
GROUP BY code_type
|
||||||
|
ORDER BY code_type;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 5. 顯示清空結果
|
||||||
|
-- ============================================================
|
||||||
|
SELECT 'HR_position_organization_positions' as table_name, COUNT(*) as row_count FROM HR_position_organization_positions
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'HR_position_departments', COUNT(*) FROM HR_position_departments
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'HR_position_divisions', COUNT(*) FROM HR_position_divisions
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'HR_position_business_units', COUNT(*) FROM HR_position_business_units
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'HR_position_job_descriptions', COUNT(*) FROM HR_position_job_descriptions
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'HR_position_jobs', COUNT(*) FROM HR_position_jobs
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'HR_position_positions', COUNT(*) FROM HR_position_positions
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'HR_position_audit_logs', COUNT(*) FROM HR_position_audit_logs
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'HR_position_reference_codes (保留)', COUNT(*) FROM HR_position_reference_codes;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 完成訊息
|
||||||
|
-- ============================================================
|
||||||
|
SELECT '資料庫清空完成!參照代碼已保留。' as message;
|
||||||
@@ -10,7 +10,7 @@ USE hr_position_system;
|
|||||||
-- ============================================================
|
-- ============================================================
|
||||||
-- Table: positions (崗位基礎資料)
|
-- Table: positions (崗位基礎資料)
|
||||||
-- ============================================================
|
-- ============================================================
|
||||||
CREATE TABLE IF NOT EXISTS positions (
|
CREATE TABLE IF NOT EXISTS HR_position_positions (
|
||||||
id VARCHAR(20) PRIMARY KEY COMMENT '崗位編號',
|
id VARCHAR(20) PRIMARY KEY COMMENT '崗位編號',
|
||||||
position_code VARCHAR(20) NOT NULL UNIQUE COMMENT '崗位編號',
|
position_code VARCHAR(20) NOT NULL UNIQUE COMMENT '崗位編號',
|
||||||
position_name VARCHAR(100) NOT NULL COMMENT '崗位名稱',
|
position_name VARCHAR(100) NOT NULL COMMENT '崗位名稱',
|
||||||
@@ -59,7 +59,7 @@ CREATE TABLE IF NOT EXISTS positions (
|
|||||||
-- ============================================================
|
-- ============================================================
|
||||||
-- Table: jobs (職務基礎資料)
|
-- Table: jobs (職務基礎資料)
|
||||||
-- ============================================================
|
-- ============================================================
|
||||||
CREATE TABLE IF NOT EXISTS jobs (
|
CREATE TABLE IF NOT EXISTS HR_position_jobs (
|
||||||
id VARCHAR(20) PRIMARY KEY COMMENT '職務編號',
|
id VARCHAR(20) PRIMARY KEY COMMENT '職務編號',
|
||||||
job_category_code VARCHAR(4) NOT NULL COMMENT '職務類別編號',
|
job_category_code VARCHAR(4) NOT NULL COMMENT '職務類別編號',
|
||||||
job_category_name VARCHAR(50) COMMENT '職務類別名稱',
|
job_category_name VARCHAR(50) COMMENT '職務類別名稱',
|
||||||
@@ -88,7 +88,7 @@ CREATE TABLE IF NOT EXISTS jobs (
|
|||||||
-- ============================================================
|
-- ============================================================
|
||||||
-- Table: job_descriptions (崗位描述)
|
-- Table: job_descriptions (崗位描述)
|
||||||
-- ============================================================
|
-- ============================================================
|
||||||
CREATE TABLE IF NOT EXISTS job_descriptions (
|
CREATE TABLE IF NOT EXISTS HR_position_job_descriptions (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY COMMENT '主鍵',
|
id INT AUTO_INCREMENT PRIMARY KEY COMMENT '主鍵',
|
||||||
emp_no VARCHAR(20) COMMENT '工號',
|
emp_no VARCHAR(20) COMMENT '工號',
|
||||||
emp_name VARCHAR(50) COMMENT '姓名',
|
emp_name VARCHAR(50) COMMENT '姓名',
|
||||||
@@ -126,13 +126,13 @@ CREATE TABLE IF NOT EXISTS job_descriptions (
|
|||||||
INDEX idx_emp_no (emp_no),
|
INDEX idx_emp_no (emp_no),
|
||||||
INDEX idx_position_code (position_code),
|
INDEX idx_position_code (position_code),
|
||||||
INDEX idx_version_date (version_date),
|
INDEX idx_version_date (version_date),
|
||||||
FOREIGN KEY (position_code) REFERENCES positions(position_code) ON DELETE SET NULL
|
FOREIGN KEY (position_code) REFERENCES HR_position_positions(position_code) ON DELETE SET NULL
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='崗位描述表';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='崗位描述表';
|
||||||
|
|
||||||
-- ============================================================
|
-- ============================================================
|
||||||
-- Table: reference_codes (參照資料代碼表)
|
-- Table: reference_codes (參照資料代碼表)
|
||||||
-- ============================================================
|
-- ============================================================
|
||||||
CREATE TABLE IF NOT EXISTS reference_codes (
|
CREATE TABLE IF NOT EXISTS HR_position_reference_codes (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY COMMENT '主鍵',
|
id INT AUTO_INCREMENT PRIMARY KEY COMMENT '主鍵',
|
||||||
code_type VARCHAR(50) NOT NULL COMMENT '代碼類型',
|
code_type VARCHAR(50) NOT NULL COMMENT '代碼類型',
|
||||||
code_value VARCHAR(10) NOT NULL COMMENT '代碼值',
|
code_value VARCHAR(10) NOT NULL COMMENT '代碼值',
|
||||||
@@ -152,7 +152,7 @@ CREATE TABLE IF NOT EXISTS reference_codes (
|
|||||||
-- ============================================================
|
-- ============================================================
|
||||||
-- Table: audit_logs (審計日誌)
|
-- Table: audit_logs (審計日誌)
|
||||||
-- ============================================================
|
-- ============================================================
|
||||||
CREATE TABLE IF NOT EXISTS audit_logs (
|
CREATE TABLE IF NOT EXISTS HR_position_audit_logs (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY COMMENT '主鍵',
|
id INT AUTO_INCREMENT PRIMARY KEY COMMENT '主鍵',
|
||||||
action VARCHAR(20) NOT NULL COMMENT '操作類型(CREATE/UPDATE/DELETE)',
|
action VARCHAR(20) NOT NULL COMMENT '操作類型(CREATE/UPDATE/DELETE)',
|
||||||
entity_type VARCHAR(50) NOT NULL COMMENT '實體類型',
|
entity_type VARCHAR(50) NOT NULL COMMENT '實體類型',
|
||||||
@@ -174,21 +174,21 @@ CREATE TABLE IF NOT EXISTS audit_logs (
|
|||||||
-- ============================================================
|
-- ============================================================
|
||||||
|
|
||||||
-- 崗位類別 (Position Category)
|
-- 崗位類別 (Position Category)
|
||||||
INSERT INTO reference_codes (code_type, code_value, code_name, sort_order) VALUES
|
INSERT INTO HR_position_reference_codes (code_type, code_value, code_name, sort_order) VALUES
|
||||||
('POSITION_CATEGORY', '01', '技術職', 1),
|
('POSITION_CATEGORY', '01', '技術職', 1),
|
||||||
('POSITION_CATEGORY', '02', '管理職', 2),
|
('POSITION_CATEGORY', '02', '管理職', 2),
|
||||||
('POSITION_CATEGORY', '03', '業務職', 3),
|
('POSITION_CATEGORY', '03', '業務職', 3),
|
||||||
('POSITION_CATEGORY', '04', '行政職', 4);
|
('POSITION_CATEGORY', '04', '行政職', 4);
|
||||||
|
|
||||||
-- 崗位性質 (Position Nature)
|
-- 崗位性質 (Position Nature)
|
||||||
INSERT INTO reference_codes (code_type, code_value, code_name, code_name_en, sort_order) VALUES
|
INSERT INTO HR_position_reference_codes (code_type, code_value, code_name, code_name_en, sort_order) VALUES
|
||||||
('POSITION_NATURE', 'FT', '全職', 'Full-time', 1),
|
('POSITION_NATURE', 'FT', '全職', 'Full-time', 1),
|
||||||
('POSITION_NATURE', 'PT', '兼職', 'Part-time', 2),
|
('POSITION_NATURE', 'PT', '兼職', 'Part-time', 2),
|
||||||
('POSITION_NATURE', 'CT', '約聘', 'Contract', 3),
|
('POSITION_NATURE', 'CT', '約聘', 'Contract', 3),
|
||||||
('POSITION_NATURE', 'IN', '實習', 'Intern', 4);
|
('POSITION_NATURE', 'IN', '實習', 'Intern', 4);
|
||||||
|
|
||||||
-- 崗位級別 (Position Level)
|
-- 崗位級別 (Position Level)
|
||||||
INSERT INTO reference_codes (code_type, code_value, code_name, sort_order) VALUES
|
INSERT INTO HR_position_reference_codes (code_type, code_value, code_name, sort_order) VALUES
|
||||||
('POSITION_LEVEL', 'L1', '基層員工', 1),
|
('POSITION_LEVEL', 'L1', '基層員工', 1),
|
||||||
('POSITION_LEVEL', 'L2', '資深員工', 2),
|
('POSITION_LEVEL', 'L2', '資深員工', 2),
|
||||||
('POSITION_LEVEL', 'L3', '主管', 3),
|
('POSITION_LEVEL', 'L3', '主管', 3),
|
||||||
@@ -198,7 +198,7 @@ INSERT INTO reference_codes (code_type, code_value, code_name, sort_order) VALUE
|
|||||||
('POSITION_LEVEL', 'L7', '總經理', 7);
|
('POSITION_LEVEL', 'L7', '總經理', 7);
|
||||||
|
|
||||||
-- 職務類別 (Job Category)
|
-- 職務類別 (Job Category)
|
||||||
INSERT INTO reference_codes (code_type, code_value, code_name, sort_order) VALUES
|
INSERT INTO HR_position_reference_codes (code_type, code_value, code_name, sort_order) VALUES
|
||||||
('JOB_CATEGORY', 'MGR', '管理職', 1),
|
('JOB_CATEGORY', 'MGR', '管理職', 1),
|
||||||
('JOB_CATEGORY', 'TECH', '技術職', 2),
|
('JOB_CATEGORY', 'TECH', '技術職', 2),
|
||||||
('JOB_CATEGORY', 'SALE', '業務職', 3),
|
('JOB_CATEGORY', 'SALE', '業務職', 3),
|
||||||
@@ -207,7 +207,7 @@ INSERT INTO reference_codes (code_type, code_value, code_name, sort_order) VALUE
|
|||||||
('JOB_CATEGORY', 'PROD', '生產職', 6);
|
('JOB_CATEGORY', 'PROD', '生產職', 6);
|
||||||
|
|
||||||
-- 學歷 (Education)
|
-- 學歷 (Education)
|
||||||
INSERT INTO reference_codes (code_type, code_value, code_name, code_name_en, sort_order) VALUES
|
INSERT INTO HR_position_reference_codes (code_type, code_value, code_name, code_name_en, sort_order) VALUES
|
||||||
('EDUCATION', 'HS', '高中', 'High School', 1),
|
('EDUCATION', 'HS', '高中', 'High School', 1),
|
||||||
('EDUCATION', 'JC', '專科', 'Junior College', 2),
|
('EDUCATION', 'JC', '專科', 'Junior College', 2),
|
||||||
('EDUCATION', 'BA', '大學', 'Bachelor', 3),
|
('EDUCATION', 'BA', '大學', 'Bachelor', 3),
|
||||||
@@ -215,7 +215,7 @@ INSERT INTO reference_codes (code_type, code_value, code_name, code_name_en, sor
|
|||||||
('EDUCATION', 'PHD', '博士', 'PhD', 5);
|
('EDUCATION', 'PHD', '博士', 'PhD', 5);
|
||||||
|
|
||||||
-- 薪酬范圍 (Salary Range)
|
-- 薪酬范圍 (Salary Range)
|
||||||
INSERT INTO reference_codes (code_type, code_value, code_name, sort_order) VALUES
|
INSERT INTO HR_position_reference_codes (code_type, code_value, code_name, sort_order) VALUES
|
||||||
('SALARY_RANGE', 'A', 'A級', 1),
|
('SALARY_RANGE', 'A', 'A級', 1),
|
||||||
('SALARY_RANGE', 'B', 'B級', 2),
|
('SALARY_RANGE', 'B', 'B級', 2),
|
||||||
('SALARY_RANGE', 'C', 'C級', 3),
|
('SALARY_RANGE', 'C', 'C級', 3),
|
||||||
@@ -224,7 +224,7 @@ INSERT INTO reference_codes (code_type, code_value, code_name, sort_order) VALUE
|
|||||||
('SALARY_RANGE', 'N', '面議', 6);
|
('SALARY_RANGE', 'N', '面議', 6);
|
||||||
|
|
||||||
-- 任職地點 (Work Location)
|
-- 任職地點 (Work Location)
|
||||||
INSERT INTO reference_codes (code_type, code_value, code_name, sort_order) VALUES
|
INSERT INTO HR_position_reference_codes (code_type, code_value, code_name, sort_order) VALUES
|
||||||
('WORK_LOCATION', 'HQ', '總部', 1),
|
('WORK_LOCATION', 'HQ', '總部', 1),
|
||||||
('WORK_LOCATION', 'TPE', '台北辦公室', 2),
|
('WORK_LOCATION', 'TPE', '台北辦公室', 2),
|
||||||
('WORK_LOCATION', 'TYC', '桃園廠區', 3),
|
('WORK_LOCATION', 'TYC', '桃園廠區', 3),
|
||||||
@@ -233,7 +233,7 @@ INSERT INTO reference_codes (code_type, code_value, code_name, sort_order) VALUE
|
|||||||
('WORK_LOCATION', 'SZ', '深圳辦公室', 6);
|
('WORK_LOCATION', 'SZ', '深圳辦公室', 6);
|
||||||
|
|
||||||
-- 員工屬性 (Employee Attribute)
|
-- 員工屬性 (Employee Attribute)
|
||||||
INSERT INTO reference_codes (code_type, code_value, code_name, sort_order) VALUES
|
INSERT INTO HR_position_reference_codes (code_type, code_value, code_name, sort_order) VALUES
|
||||||
('EMP_ATTRIBUTE', 'FT', '正式員工', 1),
|
('EMP_ATTRIBUTE', 'FT', '正式員工', 1),
|
||||||
('EMP_ATTRIBUTE', 'CT', '約聘人員', 2),
|
('EMP_ATTRIBUTE', 'CT', '約聘人員', 2),
|
||||||
('EMP_ATTRIBUTE', 'PT', '兼職人員', 3),
|
('EMP_ATTRIBUTE', 'PT', '兼職人員', 3),
|
||||||
@@ -241,7 +241,7 @@ INSERT INTO reference_codes (code_type, code_value, code_name, sort_order) VALUE
|
|||||||
('EMP_ATTRIBUTE', 'DP', '派遣人員', 5);
|
('EMP_ATTRIBUTE', 'DP', '派遣人員', 5);
|
||||||
|
|
||||||
-- 招聘職位 (Recruit Position)
|
-- 招聘職位 (Recruit Position)
|
||||||
INSERT INTO reference_codes (code_type, code_value, code_name, sort_order) VALUES
|
INSERT INTO HR_position_reference_codes (code_type, code_value, code_name, sort_order) VALUES
|
||||||
('RECRUIT_POSITION', 'ENG', '工程師', 1),
|
('RECRUIT_POSITION', 'ENG', '工程師', 1),
|
||||||
('RECRUIT_POSITION', 'MGR', '經理', 2),
|
('RECRUIT_POSITION', 'MGR', '經理', 2),
|
||||||
('RECRUIT_POSITION', 'AST', '助理', 3),
|
('RECRUIT_POSITION', 'AST', '助理', 3),
|
||||||
@@ -249,11 +249,93 @@ INSERT INTO reference_codes (code_type, code_value, code_name, sort_order) VALUE
|
|||||||
('RECRUIT_POSITION', 'SAL', '業務', 5);
|
('RECRUIT_POSITION', 'SAL', '業務', 5);
|
||||||
|
|
||||||
-- 職稱要求 (Title Requirement)
|
-- 職稱要求 (Title Requirement)
|
||||||
INSERT INTO reference_codes (code_type, code_value, code_name, sort_order) VALUES
|
INSERT INTO HR_position_reference_codes (code_type, code_value, code_name, sort_order) VALUES
|
||||||
('TITLE_REQ', 'NONE', '無要求', 1),
|
('TITLE_REQ', 'NONE', '無要求', 1),
|
||||||
('TITLE_REQ', 'CERT', '需證書', 2),
|
('TITLE_REQ', 'CERT', '需證書', 2),
|
||||||
('TITLE_REQ', 'LIC', '需執照', 3);
|
('TITLE_REQ', 'LIC', '需執照', 3);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Table: business_units (事業體)
|
||||||
|
-- ============================================================
|
||||||
|
CREATE TABLE IF NOT EXISTS HR_position_business_units (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY COMMENT '主鍵',
|
||||||
|
business_code VARCHAR(20) NOT NULL UNIQUE COMMENT '事業體代碼',
|
||||||
|
business_name VARCHAR(100) NOT NULL COMMENT '事業體名稱',
|
||||||
|
sort_order INT DEFAULT 0 COMMENT '排序',
|
||||||
|
is_active BOOLEAN DEFAULT TRUE COMMENT '是否啟用',
|
||||||
|
remark VARCHAR(500) COMMENT '備註',
|
||||||
|
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '建立時間',
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',
|
||||||
|
|
||||||
|
INDEX idx_business_name (business_name)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='事業體表';
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Table: divisions (處級單位)
|
||||||
|
-- ============================================================
|
||||||
|
CREATE TABLE IF NOT EXISTS HR_position_divisions (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY COMMENT '主鍵',
|
||||||
|
division_code VARCHAR(20) NOT NULL UNIQUE COMMENT '處級單位代碼',
|
||||||
|
division_name VARCHAR(100) NOT NULL COMMENT '處級單位名稱',
|
||||||
|
business_id INT COMMENT '所屬事業體ID',
|
||||||
|
sort_order INT DEFAULT 0 COMMENT '排序',
|
||||||
|
is_active BOOLEAN DEFAULT TRUE COMMENT '是否啟用',
|
||||||
|
remark VARCHAR(500) COMMENT '備註',
|
||||||
|
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '建立時間',
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',
|
||||||
|
|
||||||
|
INDEX idx_division_name (division_name),
|
||||||
|
INDEX idx_business_id (business_id),
|
||||||
|
FOREIGN KEY (business_id) REFERENCES HR_position_business_units(id) ON DELETE SET NULL
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='處級單位表';
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Table: departments (部級單位)
|
||||||
|
-- ============================================================
|
||||||
|
CREATE TABLE IF NOT EXISTS HR_position_departments (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY COMMENT '主鍵',
|
||||||
|
department_code VARCHAR(20) NOT NULL UNIQUE COMMENT '部級單位代碼',
|
||||||
|
department_name VARCHAR(100) NOT NULL COMMENT '部級單位名稱',
|
||||||
|
division_id INT COMMENT '所屬處級單位ID',
|
||||||
|
sort_order INT DEFAULT 0 COMMENT '排序',
|
||||||
|
is_active BOOLEAN DEFAULT TRUE COMMENT '是否啟用',
|
||||||
|
remark VARCHAR(500) COMMENT '備註',
|
||||||
|
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '建立時間',
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',
|
||||||
|
|
||||||
|
INDEX idx_department_name (department_name),
|
||||||
|
INDEX idx_division_id (division_id),
|
||||||
|
FOREIGN KEY (division_id) REFERENCES HR_position_divisions(id) ON DELETE SET NULL
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='部級單位表';
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Table: organization_positions (組織崗位關聯)
|
||||||
|
-- ============================================================
|
||||||
|
CREATE TABLE IF NOT EXISTS HR_position_organization_positions (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY COMMENT '主鍵',
|
||||||
|
business_id INT NOT NULL COMMENT '事業體ID',
|
||||||
|
division_id INT NOT NULL COMMENT '處級單位ID',
|
||||||
|
department_id INT NOT NULL COMMENT '部級單位ID',
|
||||||
|
position_title VARCHAR(100) NOT NULL COMMENT '崗位名稱',
|
||||||
|
sort_order INT DEFAULT 0 COMMENT '排序',
|
||||||
|
is_active BOOLEAN DEFAULT TRUE COMMENT '是否啟用',
|
||||||
|
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '建立時間',
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',
|
||||||
|
|
||||||
|
INDEX idx_business_id (business_id),
|
||||||
|
INDEX idx_division_id (division_id),
|
||||||
|
INDEX idx_department_id (department_id),
|
||||||
|
INDEX idx_position_title (position_title),
|
||||||
|
UNIQUE KEY uk_org_position (business_id, division_id, department_id, position_title),
|
||||||
|
FOREIGN KEY (business_id) REFERENCES HR_position_business_units(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (division_id) REFERENCES HR_position_divisions(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (department_id) REFERENCES HR_position_departments(id) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='組織崗位關聯表';
|
||||||
|
|
||||||
-- ============================================================
|
-- ============================================================
|
||||||
-- End of Schema
|
-- End of Schema
|
||||||
-- ============================================================
|
-- ============================================================
|
||||||
|
|||||||
183
docs/# 系統表單欄位規範書 v1.1 (UX Optimized).md
Normal file
183
docs/# 系統表單欄位規範書 v1.1 (UX Optimized).md
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
# 系統表單欄位規範書 v1.1 (UX Optimized)
|
||||||
|
|
||||||
|
**設計原則 (UX Design Principles):**
|
||||||
|
1. **Context First**: 先確認組織架構 (BU/Dept),再定義具體內容。
|
||||||
|
2. **Key Data Top**: 核心識別資料 (編號/名稱) 置頂。
|
||||||
|
3. **Group by Type**: 下拉選單 (Select) 集中,開關 (Switch) 集中,長文字 (Textarea) 沉底。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 崗位基礎資料模組 (Position Module)
|
||||||
|
|
||||||
|
**表單代號**: `positionForm`
|
||||||
|
**資料表**: `Position`
|
||||||
|
|
||||||
|
### 1.1 基礎資料頁籤 (tab-position-basic)
|
||||||
|
> **UX 邏輯**: 先選組織(過濾條件) → 填寫核心識別 → 設定分類屬性 → 填寫詳細描述。
|
||||||
|
|
||||||
|
| # | 欄位顯示名稱 | 標準化 HTML ID | 資料庫欄位名稱 | 類型 | 必填 | 備註 |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| **[組織定義]** | | | | | | |
|
||||||
|
| 1 | 事業體 | `pos_businessUnit` | `businessUnit` | select | 否 | 聯動 L1 |
|
||||||
|
| 2 | 處級單位 | `pos_division` | `division` | select | 否 | 聯動 L2 |
|
||||||
|
| 3 | 部級單位 | `pos_department` | `department` | select | 否 | 聯動 L3 |
|
||||||
|
| 4 | 課級單位 | `pos_section` | `section` | text | 否 | - |
|
||||||
|
| **[核心識別]** | | | | | | |
|
||||||
|
| 5 | **崗位編號** | `pos_code` | `positionCode` | text | **是** | 唯一識別 |
|
||||||
|
| 6 | **崗位名稱** | `pos_name` | `positionName` | text | **是** | - |
|
||||||
|
| 7 | 生效日期 | `pos_effectiveDate` | `effectiveDate` | date | 否 | 預設今日 |
|
||||||
|
| **[分類屬性]** | | | | | | |
|
||||||
|
| 8 | 崗位級別 | `pos_level` | `positionLevel` | select | 否 | L1-L7 |
|
||||||
|
| 9 | 崗位類別 | `pos_category` | `positionCategory` | select | 否 | 連動帶出名稱 |
|
||||||
|
| 10 | 崗位類別名稱 | `pos_categoryName` | `positionCategoryName` | text | 否 | Readonly |
|
||||||
|
| 11 | 崗位性質 | `pos_type` | `positionType` | select | 否 | FT/PT/CT... |
|
||||||
|
| 12 | 崗位性質名稱 | `pos_typeName` | `positionTypeName` | text | 否 | Readonly |
|
||||||
|
| 13 | 編制人數 | `pos_headcount` | `headcount` | number | 否 | - |
|
||||||
|
| **[詳細描述]** | | | | | | |
|
||||||
|
| 14 | 崗位描述 | `pos_desc` | `description` | textarea | 否 | rows=6 |
|
||||||
|
| 15 | 崗位備注 | `pos_remark` | `remark` | textarea | 否 | rows=4 |
|
||||||
|
|
||||||
|
### 1.2 招聘要求資料頁籤 (tab-position-recruit)
|
||||||
|
> **UX 邏輯**: 先定義「要招什麼樣的人(Target)」→「硬性條件(Must)」→「軟性技能(Plus)」→「廣告文案(Text)」。
|
||||||
|
|
||||||
|
| # | 欄位顯示名稱 | 標準化 HTML ID | 資料庫欄位名稱 | 類型 | 必填 | 備註 |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| **[招聘職位定義]** | | | | | | |
|
||||||
|
| 1 | 招聘職位代碼 | `rec_position` | `recruitPosition` | select | 否 | ENG, MGR... |
|
||||||
|
| 2 | 對外職稱 | `rec_jobTitle` | `jobTitle` | text | 否 | 顯示在招聘網的名稱 |
|
||||||
|
| 3 | 上級崗位編號 | `rec_superiorCode` | `superiorPositionCode` | text | 否 | - |
|
||||||
|
| 4 | 工作性質 | `rec_jobType` | `jobType` | select | 否 | 招聘用分類 |
|
||||||
|
| **[硬性資格]** | | | | | | |
|
||||||
|
| 5 | 最低學歷 | `rec_eduLevel` | `educationLevel` | select | 否 | - |
|
||||||
|
| 6 | 專業要求 | `rec_majorReq` | `majorRequirements` | text | 否 | Modal 選擇 |
|
||||||
|
| 7 | 工作經驗 | `rec_expYears` | `experienceYears` | select | 否 | - |
|
||||||
|
| 8 | 薪酬范圍 | `rec_salaryRange` | `salaryRange` | select | 否 | - |
|
||||||
|
| 9 | 要求性別 | `rec_gender` | `requiredGender` | select | 否 | - |
|
||||||
|
| 10 | 年齡限制 (Min) | `rec_minAge` | `minAge` | number | 否 | 併排顯示 |
|
||||||
|
| 11 | 年齡限制 (Max) | `rec_maxAge` | `maxAge` | number | 否 | 併排顯示 |
|
||||||
|
| **[技能與證照]** | | | | | | |
|
||||||
|
| 12 | 語言要求 | `rec_langReq` | `langRequirements` | text | 否 | - |
|
||||||
|
| 13 | 證照要求 | `rec_certReq` | `certRequirements` | select | 否 | - |
|
||||||
|
| 14 | 技能要求 | `rec_skillReq` | `skillRequirements` | text | 否 | Tag input |
|
||||||
|
| 15 | 其他要求 | `rec_otherReq` | `otherRequirements` | text | 否 | - |
|
||||||
|
| **[文案描述]** | | | | | | |
|
||||||
|
| 16 | 職位描述 (JD) | `rec_jobDesc` | `recruitJobDesc` | textarea | 否 | 廣告用 |
|
||||||
|
| 17 | 崗位要求 (Req) | `rec_positionReq` | `recruitRequirements` | textarea | 否 | 廣告用 |
|
||||||
|
| 18 | 招聘備注 | `rec_remark` | `recruitRemark` | textarea | 否 | 內部用 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 職務基礎資料模組 (Job Module)
|
||||||
|
|
||||||
|
**表單代號**: `jobForm`
|
||||||
|
**資料表**: `Job`
|
||||||
|
|
||||||
|
> **UX 邏輯**: 先分類 → 再命名 → 設定權限/福利(開關) → 備註。
|
||||||
|
|
||||||
|
| # | 欄位顯示名稱 | 標準化 HTML ID | 資料庫欄位名稱 | 類型 | 必填 | 備註 |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| **[分類與識別]** | | | | | | |
|
||||||
|
| 1 | **職務類別** | `job_category` | `jobCategoryCode` | select | **是** | - |
|
||||||
|
| 2 | 職務類別名稱 | `job_categoryName` | `jobCategoryName` | text | 否 | Readonly |
|
||||||
|
| 3 | **職務編號** | `job_code` | `jobCode` | text | **是** | - |
|
||||||
|
| 4 | **職務名稱** | `job_name` | `jobName` | text | **是** | - |
|
||||||
|
| 5 | 職務英文名稱 | `job_nameEn` | `jobNameEn` | text | 否 | - |
|
||||||
|
| **[屬性設定]** | | | | | | |
|
||||||
|
| 6 | 職務層級 | `job_level` | `jobLevel` | text | 否 | 敏感欄位 |
|
||||||
|
| 7 | 生效日期 | `job_effectiveDate`| `effectiveDate` | date | 否 | - |
|
||||||
|
| 8 | 排列順序 | `job_sortOrder` | `sortOrder` | number | 否 | - |
|
||||||
|
| 9 | 預算編制人數 | `job_headcount` | `headcount` | number | 否 | - |
|
||||||
|
| **[福利開關]** | | | | | | |
|
||||||
|
| 10 | 全勤獎金 | `job_hasAttBonus` | `hasAttendanceBonus` | checkbox| 否 | Toggle Switch |
|
||||||
|
| 11 | 住房補貼 | `job_hasHouseAllow`| `hasHousingAllowance` | checkbox| 否 | Toggle Switch |
|
||||||
|
| **[備註]** | | | | | | |
|
||||||
|
| 12 | 職務備注 | `job_remark` | `remark` | textarea | 否 | - |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 部門職責模組 (DeptFunction Module)
|
||||||
|
|
||||||
|
**表單代號**: `deptFunctionForm`
|
||||||
|
**資料表**: `DeptFunction`
|
||||||
|
|
||||||
|
> **UX 邏輯**: 組織架構 → 核心職責定義 → 管理與規模 → 策略性描述(使命/願景)。
|
||||||
|
|
||||||
|
| # | 欄位顯示名稱 | 標準化 HTML ID | 資料庫欄位名稱 | 類型 | 必填 | 備註 |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| **[基本定義]** | | | | | | |
|
||||||
|
| 1 | **事業體** | `df_businessUnit` | `businessUnit` | select | **是** | - |
|
||||||
|
| 2 | **處級單位** | `df_division` | `division` | select | **是** | - |
|
||||||
|
| 3 | **部級單位** | `df_department` | `department` | select | **是** | - |
|
||||||
|
| 4 | 課級單位 | `df_section` | `section` | text | 否 | - |
|
||||||
|
| 5 | **職責編號** | `df_code` | `dfCode` | text | **是** | - |
|
||||||
|
| 6 | **職責名稱** | `df_name` | `dfName` | text | **是** | - |
|
||||||
|
| **[管理架構]** | | | | | | |
|
||||||
|
| 7 | **對應崗位** | `df_posTitle` | `positionTitle` | select | **是** | 綁定 Position |
|
||||||
|
| 8 | 崗位級別 | `df_posLevel` | `positionLevel` | select | 否 | - |
|
||||||
|
| 9 | 部門主管職稱 | `df_managerTitle` | `managerTitle` | text | 否 | - |
|
||||||
|
| 10 | 人數上限 | `df_headcountLimit`| `headcountLimit` | number | 否 | - |
|
||||||
|
| 11 | **生效日期** | `df_effectiveDate` | `effectiveDate` | date | **是** | - |
|
||||||
|
| 12 | 狀態 | `df_status` | `status` | select | 否 | Active/Inactive |
|
||||||
|
| **[策略職責]** | | | | | | |
|
||||||
|
| 13 | **核心職責** | `df_coreFunc` | `coreFunctions` | textarea | **是** | 重點欄位 (Top) |
|
||||||
|
| 14 | 部門使命 | `df_mission` | `mission` | textarea | 否 | - |
|
||||||
|
| 15 | 部門願景 | `df_vision` | `vision` | textarea | 否 | - |
|
||||||
|
| 16 | KPIs | `df_kpis` | `kpis` | textarea | 否 | - |
|
||||||
|
| 17 | 協作部門 | `df_collab` | `collaboration` | textarea | 否 | - |
|
||||||
|
| 18 | 備注 | `df_remark` | `remark` | textarea | 否 | - |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 崗位描述模組 (JobDescription Module)
|
||||||
|
|
||||||
|
**表單代號**: `jobDescForm`
|
||||||
|
**資料表**: `JobDescription`
|
||||||
|
|
||||||
|
> **UX 邏輯**:
|
||||||
|
> 1. Header: 快速鎖定「這是誰的JD」。
|
||||||
|
> 2. Readonly Area: 顯示「他在哪裡工作」(提供撰寫JD的上下文)。
|
||||||
|
> 3. Reporting: 釐清「上下級關係」。
|
||||||
|
> 4. Main Body: 撰寫「做什麼」(Purpose/Resp)。
|
||||||
|
> 5. Requirements: 撰寫「需要什麼條件」。
|
||||||
|
|
||||||
|
### 4.1 識別與上下文 (Header & Context)
|
||||||
|
|
||||||
|
| # | 欄位顯示名稱 | 標準化 HTML ID | 資料庫欄位名稱 | 類型 | 必填 | 備註 |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| **[員工綁定]** | | | | | | |
|
||||||
|
| 1 | 選擇員工(工號) | `jd_empNo` | `empNo` | text | 否 | 搜尋 Modal |
|
||||||
|
| 2 | 姓名 | `jd_empName` | `empName` | text | 否 | Readonly |
|
||||||
|
| 3 | 版本日期 | `jd_versionDate` | `versionDate` | date | 否 | - |
|
||||||
|
| **[崗位背景 (唯讀)]** | | | | | | |
|
||||||
|
| 4 | 崗位代碼 | `jd_posCode` | `positionCode` | text | 否 | 關聯鍵 |
|
||||||
|
| 5 | 崗位名稱 | `jd_posName` | `positionName` | text | 否 | Readonly |
|
||||||
|
| 6 | 所屬部門 | `jd_department` | `department` | text | 否 | Readonly (組合字串) |
|
||||||
|
| 7 | 任職地點 | `jd_location` | `workLocation` | select | 否 | - |
|
||||||
|
| 8 | 員工屬性 | `jd_empAttr` | `empAttribute` | select | 否 | FT/PT... |
|
||||||
|
| 9 | 部門職責引用 | `jd_dfCode` | `dfCode` | text | 否 | 關聯 DF 模組 |
|
||||||
|
|
||||||
|
### 4.2 匯報關係 (Reporting Lines)
|
||||||
|
|
||||||
|
| # | 欄位顯示名稱 | 標準化 HTML ID | 資料庫欄位名稱 | 類型 | 必填 | 備註 |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| 10 | **直接主管** | `jd_supervisor` | `directSupervisor` | text | 否 | 姓名/職稱 |
|
||||||
|
| 11 | **匯報對象** | `jd_reportTo` | `reportTo` | text | 否 | 系統職務節點 |
|
||||||
|
| 12 | 職等&職務 | `jd_gradeJob` | `positionGradeJob` | text | 否 | HR 用級別 |
|
||||||
|
| 13 | 直接下屬 | `jd_directReports` | `directReports` | text | 否 | 人數或職稱 |
|
||||||
|
|
||||||
|
### 4.3 職責詳情 (Responsibilities)
|
||||||
|
|
||||||
|
| # | 欄位顯示名稱 | 標準化 HTML ID | 資料庫欄位名稱 | 類型 | 必填 | 備註 |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| 14 | 崗位設置目的 | `jd_purpose` | `positionPurpose` | text | 否 | 一句話摘要 |
|
||||||
|
| 15 | **主要職責** | `jd_mainResp` | `mainResponsibilities`| textarea | 否 | 核心內容 (大區塊) |
|
||||||
|
|
||||||
|
### 4.4 任職資格 (Requirements)
|
||||||
|
|
||||||
|
| # | 欄位顯示名稱 | 標準化 HTML ID | 資料庫欄位名稱 | 類型 | 必填 | 備註 |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| 16 | 教育程度 | `jd_eduLevel` | `educationLevel` | text | 否 | - |
|
||||||
|
| 17 | 工作經驗 | `jd_expReq` | `experienceRequirements`| textarea | 否 | - |
|
||||||
|
| 18 | 專業知識 | `jd_proKnowledge` | `professionalKnowledge` | textarea | 否 | - |
|
||||||
|
| 19 | 基本技能 | `jd_basicSkills` | `basicSkills` | textarea | 否 | - |
|
||||||
|
| 20 | 其他要求 | `jd_otherReq` | `otherRequirements` | textarea | 否 | - |
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
# HR 基礎資料維護系統 - 軟體設計文件 (SDD)
|
# HR 基礎資料維護系統 - 軟體設計文件 (SDD)
|
||||||
|
|
||||||
**文件版本**:2.1
|
**文件版本**:3.1
|
||||||
**建立日期**:2024-12-03
|
**建立日期**:2024-12-03
|
||||||
**最後更新**:2024-12-04
|
**最後更新**:2024-12-08
|
||||||
**文件狀態**:Released
|
**文件狀態**:Released
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -15,15 +15,17 @@
|
|||||||
|
|
||||||
### 1.2 範圍
|
### 1.2 範圍
|
||||||
|
|
||||||
本系統涵蓋以下三大功能模組:
|
本系統涵蓋以下功能模組:
|
||||||
|
|
||||||
| 模組 | 功能說明 |
|
| 模組 | 功能說明 |
|
||||||
|------|----------|
|
|------|----------|
|
||||||
| 崗位基礎資料 | 崗位主檔維護,含基礎資料與招聘要求 |
|
| 崗位基礎資料 | 崗位主檔維護,含基礎資料與招聘要求 |
|
||||||
| 職務基礎資料 | 職務類別與屬性設定維護 |
|
| 職務基礎資料 | 職務類別與屬性設定維護 |
|
||||||
|
| 部門職責 | 部門職責定義、使命願景與 KPI 維護 |
|
||||||
| 崗位描述 | 職責描述、崗位要求與任職條件維護 |
|
| 崗位描述 | 職責描述、崗位要求與任職條件維護 |
|
||||||
| 崗位清單 | 顯示所有崗位資料,支援查看描述與匯出 |
|
| 崗位清單 | 顯示所有崗位資料,支援查看描述與匯出 |
|
||||||
| 管理者頁面 | 使用者管理與完整崗位資料匯出 |
|
| 管理者頁面 | 使用者管理與完整崗位資料匯出 |
|
||||||
|
| 組織階層管理 | 事業體、處級單位、部級單位、崗位的層級關聯管理 |
|
||||||
|
|
||||||
### 1.3 參考文件
|
### 1.3 參考文件
|
||||||
|
|
||||||
@@ -37,8 +39,14 @@
|
|||||||
|------|------|
|
|------|------|
|
||||||
| 崗位 (Position) | 組織架構中的職位單位,具有編號、級別、編制人數等屬性 |
|
| 崗位 (Position) | 組織架構中的職位單位,具有編號、級別、編制人數等屬性 |
|
||||||
| 職務 (Job Title) | 職務類別分類,如管理職、技術職、業務職等 |
|
| 職務 (Job Title) | 職務類別分類,如管理職、技術職、業務職等 |
|
||||||
|
| 部門職責 (DeptFunction) | 部門的職責範圍、使命願景與績效指標定義 |
|
||||||
| 崗位描述 (Job Description) | 詳細描述崗位職責、要求與任職條件的文件 |
|
| 崗位描述 (Job Description) | 詳細描述崗位職責、要求與任職條件的文件 |
|
||||||
|
| 事業體 (Business Unit) | 組織最高層級,代表獨立經營單位 |
|
||||||
|
| 處級單位 (Division) | 事業體下的次級組織單位 |
|
||||||
|
| 部級單位 (Department) | 處級單位下的基層組織單位 |
|
||||||
| AI 自動填充 | 利用大型語言模型自動生成表單欄位內容的功能 |
|
| AI 自動填充 | 利用大型語言模型自動生成表單欄位內容的功能 |
|
||||||
|
| 三個錦囊 | AI 智能功能區塊,提供自動補齊、範例模板、驗證檢查 |
|
||||||
|
| LLM | Large Language Model,大型語言模型 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -47,65 +55,117 @@
|
|||||||
### 2.1 整體架構圖
|
### 2.1 整體架構圖
|
||||||
|
|
||||||
```
|
```
|
||||||
┌─────────────────────────────────────────────────────────────────┐
|
┌─────────────────────────────────────────────────────────────────────┐
|
||||||
│ 使用者介面層 (UI Layer) │
|
│ 使用者介面層 (UI Layer) │
|
||||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
|
||||||
│ │ 崗位基礎資料 │ │ 職務基礎資料 │ │ 崗位描述 │ │
|
│ │ 崗位基礎 │ │ 職務基礎 │ │ 部門職責 │ │ 崗位描述 │ │
|
||||||
│ │ 模組 │ │ 模組 │ │ 模組 │ │
|
│ │ 模組 │ │ 模組 │ │ 模組 │ │ 模組 │ │
|
||||||
│ └─────────────┘ └─────────────┘ └─────────────┘ │
|
│ └───────────┘ └───────────┘ └───────────┘ └───────────┘ │
|
||||||
│ │ │
|
│ │
|
||||||
│ ┌──────────────────────────────────────────────────────────┐ │
|
│ ┌───────────────────────────────────────────────────────────────┐ │
|
||||||
│ │ AI 自動填充服務 (Claude API) │ │
|
│ │ 三個錦囊 AI 功能區 │ │
|
||||||
│ └──────────────────────────────────────────────────────────┘ │
|
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
|
||||||
└─────────────────────────────────────────────────────────────────┘
|
│ │ │ 自動補齊 │ │ 範例模板 │ │ 驗證檢查 │ │ │
|
||||||
|
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
|
||||||
|
│ └───────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌───────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ ES6 JavaScript 模組化架構 │ │
|
||||||
|
│ │ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │ │
|
||||||
|
│ │ │ main.js│ │ ui.js │ │ api.js │ │ai-bags │ │utils.js│ │ │
|
||||||
|
│ │ └────────┘ └────────┘ └────────┘ └────────┘ └────────┘ │ │
|
||||||
|
│ └───────────────────────────────────────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────────────┘
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
┌─────────────────────────────────────────────────────────────────┐
|
┌─────────────────────────────────────────────────────────────────────┐
|
||||||
│ 應用服務層 (Application Layer) │
|
│ 應用服務層 (Application Layer) │
|
||||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
│ ┌───────────────────────────────────────────────────────────────┐ │
|
||||||
│ │ Flask RESTful API │ │
|
│ │ Flask RESTful API │ │
|
||||||
│ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ │
|
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
|
||||||
│ │ │ Position │ │ Job │ │ Reference │ │ │
|
│ │ │Position │ │ Job │ │DeptFunc │ │ LLM │ │Hierarchy│ │ │
|
||||||
│ │ │ API │ │ API │ │ API │ │ │
|
│ │ │ API │ │ API │ │ API │ │ API │ │ API │ │ │
|
||||||
│ │ └───────────┘ └───────────┘ └───────────┘ │ │
|
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └─────────┘ │ │
|
||||||
│ └─────────────────────────────────────────────────────────┘ │
|
│ └───────────────────────────────────────────────────────────────┘ │
|
||||||
└─────────────────────────────────────────────────────────────────┘
|
│ │ │
|
||||||
|
│ ┌───────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ LLM 配置模組 (llm_config.py) │ │
|
||||||
|
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
|
||||||
|
│ │ │ Gemini │ │DeepSeek │ │ OpenAI │ │ Ollama │ │ GPT-OSS │ │ │
|
||||||
|
│ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │
|
||||||
|
│ └───────────────────────────────────────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────────────┘
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
┌─────────────────────────────────────────────────────────────────┐
|
┌─────────────────────────────────────────────────────────────────────┐
|
||||||
│ 資料存取層 (Data Layer) │
|
│ 資料存取層 (Data Layer) │
|
||||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
│ ┌───────────────────────────────────────────────────────────────┐ │
|
||||||
│ │ In-Memory Database / Future DB │ │
|
│ │ In-Memory Database / MySQL │ │
|
||||||
│ │ (positions_db, jobs_db, job_desc_db) │ │
|
│ │ ┌─────────────────┐ ┌─────────────────────────────────────┐ │ │
|
||||||
│ └─────────────────────────────────────────────────────────┘ │
|
│ │ │ positions_db │ │ 組織階層資料 │ │ │
|
||||||
└─────────────────────────────────────────────────────────────────┘
|
│ │ │ jobs_db │ │ (business_units, divisions, │ │ │
|
||||||
|
│ │ │ dept_func_db │ │ departments, org_positions) │ │ │
|
||||||
|
│ │ │ job_desc_db │ │ │ │ │
|
||||||
|
│ │ └─────────────────┘ └─────────────────────────────────────┘ │ │
|
||||||
|
│ └───────────────────────────────────────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2.2 技術堆疊
|
### 2.2 技術堆疊
|
||||||
|
|
||||||
| 層級 | 技術選型 | 說明 |
|
| 層級 | 技術選型 | 說明 |
|
||||||
|------|----------|------|
|
|------|----------|------|
|
||||||
| 前端 | HTML5 + CSS3 + JavaScript | 純前端實作,無框架依賴 |
|
| 前端 | HTML5 + CSS3 + ES6 JavaScript | 模組化架構,使用 import/export |
|
||||||
| 樣式 | Custom CSS + Google Fonts | Noto Sans TC 字型、CSS Variables |
|
| 樣式 | CSS Modules + Google Fonts | Noto Sans TC 字型、CSS Variables |
|
||||||
| 後端 | Python Flask | RESTful API 服務 |
|
| 後端 | Python Flask | RESTful API 服務 |
|
||||||
| AI 服務 | Claude API (Anthropic) | 智能表單填充 |
|
| AI 服務 | 多 LLM 支援 | Ollama、Gemini、DeepSeek、OpenAI、GPT-OSS |
|
||||||
| 資料庫 | In-Memory (Dict) | 可擴展至 MySQL/PostgreSQL |
|
| 資料庫 | In-Memory + MySQL | 組織階層資料支援 MySQL 持久化 |
|
||||||
|
|
||||||
### 2.3 部署架構
|
### 2.3 前端模組結構
|
||||||
|
|
||||||
|
| 檔案 | 功能說明 |
|
||||||
|
|------|----------|
|
||||||
|
| js/main.js | 應用程式入口,初始化與事件綁定 |
|
||||||
|
| js/ui.js | UI 互動邏輯,表單處理與頁籤切換 |
|
||||||
|
| js/api.js | API 呼叫封裝,與後端通訊 |
|
||||||
|
| js/ai-bags.js | 三個錦囊 AI 功能實現 |
|
||||||
|
| js/utils.js | 工具函數,Toast、錯誤處理等 |
|
||||||
|
| js/config.js | 配置常數,API 端點等 |
|
||||||
|
|
||||||
|
### 2.4 CSS 模組結構
|
||||||
|
|
||||||
|
| 檔案 | 功能說明 |
|
||||||
|
|------|----------|
|
||||||
|
| styles/base.css | 基礎樣式、CSS 變數、reset |
|
||||||
|
| styles/layout.css | 頁面佈局、header、sidebar |
|
||||||
|
| styles/components.css | 按鈕、表單、卡片、Modal 等元件 |
|
||||||
|
| styles/modules.css | 各模組特定樣式 |
|
||||||
|
| styles/responsive.css | 響應式設計樣式 |
|
||||||
|
|
||||||
|
### 2.5 部署架構
|
||||||
|
|
||||||
```
|
```
|
||||||
┌─────────────────────────────────────────┐
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
│ 使用者瀏覽器 │
|
│ 使用者瀏覽器 │
|
||||||
│ ┌─────────────────────────────────┐ │
|
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||||
│ │ index.html (前端應用) │ │
|
│ │ index.html (前端應用,ES6 Modules) │ │
|
||||||
│ └─────────────────────────────────┘ │
|
│ │ ┌──────────────────────────────────────────────────┐ │ │
|
||||||
└─────────────────────────────────────────┘
|
│ │ │ js/main.js → ui.js → api.js → ai-bags.js │ │ │
|
||||||
|
│ │ └──────────────────────────────────────────────────┘ │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
│ │
|
│ │
|
||||||
▼ ▼
|
▼ ▼
|
||||||
┌───────────────────┐ ┌───────────────────┐
|
┌─────────────────────────┐ ┌────────────────────────────────────┐
|
||||||
│ Flask Server │ │ Claude API │
|
│ Flask Server │ │ LLM API Services │
|
||||||
│ localhost:5000 │ │ api.anthropic │
|
│ localhost:5000 │ │ ┌──────────┐ ┌──────────┐ │
|
||||||
└───────────────────┘ └───────────────────┘
|
│ ┌───────────────────┐ │ │ │ Ollama │ │ Gemini │ │
|
||||||
|
│ │ app.py │ │ │ │localhost │ │ Google │ │
|
||||||
|
│ │ llm_config.py │ │ │ └──────────┘ └──────────┘ │
|
||||||
|
│ └───────────────────┘ │ │ ┌──────────┐ ┌──────────┐ │
|
||||||
|
└─────────────────────────┘ │ │ DeepSeek │ │ OpenAI │ │
|
||||||
|
│ └──────────┘ └──────────┘ │
|
||||||
|
└────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -227,13 +287,51 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 3.3 崗位描述模組
|
### 3.3 部門職責模組
|
||||||
|
|
||||||
#### 3.3.1 模組概述
|
#### 3.3.1 模組概述
|
||||||
|
|
||||||
|
提供部門職責定義維護功能,包含部門使命願景、核心職責與績效指標設定。
|
||||||
|
|
||||||
|
#### 3.3.2 欄位規格
|
||||||
|
|
||||||
|
| 欄位名稱 | 欄位ID | 資料類型 | 必填 | 說明 |
|
||||||
|
|----------|--------|----------|------|------|
|
||||||
|
| 職責編號 | df_code | String(20) | ✓ | 格式 DF-001 |
|
||||||
|
| 職責名稱 | df_name | String(100) | ✓ | |
|
||||||
|
| 事業體 | df_businessUnit | Enum | ✓ | SBU/MBU/TBU |
|
||||||
|
| 處級單位 | df_division | Enum | ✓ | 聯動選項 |
|
||||||
|
| 部級單位 | df_department | Enum | ✓ | 聯動選項 |
|
||||||
|
| 課級單位 | df_section | String(50) | | |
|
||||||
|
| 對應崗位 | df_posTitle | Enum | ✓ | 關聯 Position |
|
||||||
|
| 崗位級別 | df_posLevel | Enum | | L1-L7 |
|
||||||
|
| 部門主管職稱 | df_managerTitle | String(100) | | |
|
||||||
|
| 生效日期 | df_effectiveDate | Date | ✓ | |
|
||||||
|
| 人數上限 | df_headcountLimit | Integer | | |
|
||||||
|
| 狀態 | df_status | Enum | | active/inactive |
|
||||||
|
| 部門使命 | df_mission | Text | | |
|
||||||
|
| 部門願景 | df_vision | Text | | |
|
||||||
|
| 核心職責 | df_coreFunc | Text | ✓ | |
|
||||||
|
| KPIs | df_kpis | Text | | 績效指標 |
|
||||||
|
| 協作部門 | df_collab | Text | | |
|
||||||
|
| 備注 | df_remark | Text | | |
|
||||||
|
|
||||||
|
#### 3.3.3 狀態代碼對照表
|
||||||
|
|
||||||
|
| 代碼 | 名稱 |
|
||||||
|
|------|------|
|
||||||
|
| active | 生效中 |
|
||||||
|
| inactive | 已停用 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.4 崗位描述模組
|
||||||
|
|
||||||
|
#### 3.4.1 模組概述
|
||||||
|
|
||||||
提供完整的崗位描述書 (Job Description) 維護功能,包含崗位基本信息、職責描述與崗位要求三大區塊。
|
提供完整的崗位描述書 (Job Description) 維護功能,包含崗位基本信息、職責描述與崗位要求三大區塊。
|
||||||
|
|
||||||
#### 3.3.2 頂部區域欄位
|
#### 3.4.2 頂部區域欄位
|
||||||
|
|
||||||
| 欄位名稱 | 欄位ID | 資料類型 | 說明 |
|
| 欄位名稱 | 欄位ID | 資料類型 | 說明 |
|
||||||
|----------|--------|----------|------|
|
|----------|--------|----------|------|
|
||||||
@@ -242,7 +340,7 @@
|
|||||||
| 崗位代碼 | jd_positionCode | String(20) | |
|
| 崗位代碼 | jd_positionCode | String(20) | |
|
||||||
| 版本更新日期 | jd_versionDate | Date | |
|
| 版本更新日期 | jd_versionDate | Date | |
|
||||||
|
|
||||||
#### 3.3.3 崗位基本信息區塊
|
#### 3.4.3 崗位基本信息區塊
|
||||||
|
|
||||||
| 欄位名稱 | 欄位ID | 資料類型 | 說明 |
|
| 欄位名稱 | 欄位ID | 資料類型 | 說明 |
|
||||||
|----------|--------|----------|------|
|
|----------|--------|----------|------|
|
||||||
@@ -256,14 +354,14 @@
|
|||||||
| 任職地點 | jd_workLocation | Enum | HQ/TPE/TYC/KHH/SH/SZ |
|
| 任職地點 | jd_workLocation | Enum | HQ/TPE/TYC/KHH/SH/SZ |
|
||||||
| 員工屬性 | jd_empAttribute | Enum | FT/CT/PT/IN/DP |
|
| 員工屬性 | jd_empAttribute | Enum | FT/CT/PT/IN/DP |
|
||||||
|
|
||||||
#### 3.3.4 職責描述區塊
|
#### 3.4.4 職責描述區塊
|
||||||
|
|
||||||
| 欄位名稱 | 欄位ID | 資料類型 | 說明 |
|
| 欄位名稱 | 欄位ID | 資料類型 | 說明 |
|
||||||
|----------|--------|----------|------|
|
|----------|--------|----------|------|
|
||||||
| 崗位設置目的 | jd_positionPurpose | String(500) | 單行文字 |
|
| 崗位設置目的 | jd_positionPurpose | String(500) | 單行文字 |
|
||||||
| 主要崗位職責 | jd_mainResponsibilities | Text | 編號格式 1、2、3、... |
|
| 主要崗位職責 | jd_mainResponsibilities | Text | 編號格式 1、2、3、... |
|
||||||
|
|
||||||
#### 3.3.5 崗位要求區塊
|
#### 3.4.5 崗位要求區塊
|
||||||
|
|
||||||
| 欄位名稱 | 欄位ID | 資料類型 | 說明 |
|
| 欄位名稱 | 欄位ID | 資料類型 | 說明 |
|
||||||
|----------|--------|----------|------|
|
|----------|--------|----------|------|
|
||||||
@@ -273,7 +371,7 @@
|
|||||||
| 工作經驗 | jd_workExperienceReq | Text | |
|
| 工作經驗 | jd_workExperienceReq | Text | |
|
||||||
| 其他 | jd_otherRequirements | Text | |
|
| 其他 | jd_otherRequirements | Text | |
|
||||||
|
|
||||||
#### 3.3.6 任職地點代碼對照表
|
#### 3.4.6 任職地點代碼對照表
|
||||||
|
|
||||||
| 代碼 | 名稱 |
|
| 代碼 | 名稱 |
|
||||||
|------|------|
|
|------|------|
|
||||||
@@ -284,7 +382,7 @@
|
|||||||
| SH | 上海辦公室 |
|
| SH | 上海辦公室 |
|
||||||
| SZ | 深圳辦公室 |
|
| SZ | 深圳辦公室 |
|
||||||
|
|
||||||
#### 3.3.7 員工屬性代碼對照表
|
#### 3.4.7 員工屬性代碼對照表
|
||||||
|
|
||||||
| 代碼 | 名稱 |
|
| 代碼 | 名稱 |
|
||||||
|------|------|
|
|------|------|
|
||||||
@@ -358,6 +456,18 @@
|
|||||||
| GET | `/api/reference/education` | 學歷選項 |
|
| GET | `/api/reference/education` | 學歷選項 |
|
||||||
| GET | `/api/reference/majors` | 專業選項 |
|
| GET | `/api/reference/majors` | 專業選項 |
|
||||||
|
|
||||||
|
#### 4.1.7 組織階層 API
|
||||||
|
|
||||||
|
| 方法 | 端點 | 說明 |
|
||||||
|
|------|------|------|
|
||||||
|
| GET | `/api/hierarchy/business-units` | 獲取所有事業體 |
|
||||||
|
| GET | `/api/hierarchy/divisions` | 獲取處級單位(可按事業體篩選) |
|
||||||
|
| GET | `/api/hierarchy/departments` | 獲取部級單位(可按處級單位篩選) |
|
||||||
|
| GET | `/api/hierarchy/positions` | 獲取崗位名稱(可按部級單位篩選) |
|
||||||
|
| GET | `/api/hierarchy/full` | 獲取完整階層資料(支援分頁) |
|
||||||
|
| GET | `/api/hierarchy/cascade` | 獲取級聯選擇資料 |
|
||||||
|
| GET | `/api/hierarchy/stats` | 獲取組織統計資訊 |
|
||||||
|
|
||||||
### 4.2 API 請求/回應範例
|
### 4.2 API 請求/回應範例
|
||||||
|
|
||||||
#### 4.2.1 新增崗位
|
#### 4.2.1 新增崗位
|
||||||
@@ -448,13 +558,51 @@ GET /api/positions?page=1&size=20&search=工程師
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. AI 自動填充功能設計
|
## 5. AI 智能功能設計
|
||||||
|
|
||||||
### 5.1 功能概述
|
### 5.1 功能概述
|
||||||
|
|
||||||
系統整合 Claude API,提供智能表單填充功能。用戶點擊「✨ I'm feeling lucky」按鈕後,AI 會根據已填寫的內容,自動補充尚未填寫的欄位。
|
系統整合多種 LLM API,提供「三個錦囊」智能功能區塊:
|
||||||
|
|
||||||
### 5.2 填充邏輯
|
| 錦囊 | 功能 | 說明 |
|
||||||
|
|------|------|------|
|
||||||
|
| 自動補齊 | AI 自動填充 | 根據已填內容智能生成其餘欄位 |
|
||||||
|
| 範例模板 | 範例資料生成 | 提供符合行業標準的範例資料 |
|
||||||
|
| 驗證檢查 | 資料驗證建議 | 檢查資料完整性與合理性 |
|
||||||
|
|
||||||
|
### 5.2 支援的 LLM 服務
|
||||||
|
|
||||||
|
| 服務 | 配置鍵 | 預設模型 | 說明 |
|
||||||
|
|------|--------|----------|------|
|
||||||
|
| Ollama | OLLAMA_API_KEY | qwen2.5:3b | 本地部署,無需 API 費用 |
|
||||||
|
| Gemini | GEMINI_API_KEY | gemini-pro | Google AI 服務 |
|
||||||
|
| DeepSeek | DEEPSEEK_API_KEY | deepseek-chat | 中文優化模型 |
|
||||||
|
| OpenAI | OPENAI_API_KEY | gpt-3.5-turbo | OpenAI 官方服務 |
|
||||||
|
| GPT-OSS | GPTOSS_API_KEY | - | 開源替代服務 |
|
||||||
|
|
||||||
|
### 5.3 三個錦囊 UI 設計
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────┐
|
||||||
|
│ 三個錦囊 │
|
||||||
|
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||||
|
│ │ 自動補齊 │ │ 範例模板 │ │ 驗證檢查 │ │
|
||||||
|
│ │ AI │ │ Template │ │ Validate │ │
|
||||||
|
│ │ (Edit) │ │ (Edit) │ │ (Edit) │ │
|
||||||
|
│ └─────────────┘ └─────────────┘ └─────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.4 Prompt 編輯功能
|
||||||
|
|
||||||
|
每個錦囊支援 Prompt 編輯,用戶可自訂 AI 生成邏輯:
|
||||||
|
|
||||||
|
- 標題:錦囊顯示名稱
|
||||||
|
- 副標題:功能簡述
|
||||||
|
- Prompt:發送給 LLM 的指令內容
|
||||||
|
- 儲存至 LocalStorage,持久化設定
|
||||||
|
|
||||||
|
### 5.5 填充邏輯
|
||||||
|
|
||||||
```
|
```
|
||||||
┌─────────────────────────────────────────────────────────┐
|
┌─────────────────────────────────────────────────────────┐
|
||||||
@@ -489,9 +637,9 @@ GET /api/positions?page=1&size=20&search=工程師
|
|||||||
└─────────────────────────────────────────────────────────┘
|
└─────────────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
### 5.3 核心函數
|
### 5.6 核心函數
|
||||||
|
|
||||||
#### 5.3.1 fillIfEmpty
|
#### 5.6.1 fillIfEmpty
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
/**
|
/**
|
||||||
@@ -510,7 +658,7 @@ function fillIfEmpty(elementId, value) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 5.3.2 getEmptyFields
|
#### 5.6.2 getEmptyFields
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
/**
|
/**
|
||||||
@@ -523,28 +671,24 @@ function getEmptyFields(fieldIds) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 5.4 API 呼叫規格
|
### 5.7 API 呼叫規格
|
||||||
|
|
||||||
**Endpoint**
|
**本地 Flask API Endpoint**
|
||||||
```
|
```
|
||||||
POST https://api.anthropic.com/v1/messages
|
POST http://localhost:5000/api/llm/generate
|
||||||
```
|
```
|
||||||
|
|
||||||
**Request Body**
|
**Request Body**
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"model": "claude-sonnet-4-20250514",
|
"api": "ollama",
|
||||||
"max_tokens": 2000,
|
"model": "qwen2.5:3b",
|
||||||
"messages": [
|
"prompt": "Prompt 內容...",
|
||||||
{
|
"max_tokens": 2000
|
||||||
"role": "user",
|
|
||||||
"content": "Prompt 內容..."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 5.5 Prompt 設計範例
|
### 5.8 Prompt 設計範例
|
||||||
|
|
||||||
```
|
```
|
||||||
請為HR崗位管理系統生成崗位基礎資料。請用繁體中文回覆。
|
請為HR崗位管理系統生成崗位基礎資料。請用繁體中文回覆。
|
||||||
@@ -741,6 +885,105 @@ interface PositionListItem {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 7.5 組織階層資料結構
|
||||||
|
|
||||||
|
#### 7.5.1 事業體 (BusinessUnit)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface BusinessUnit {
|
||||||
|
id: number; // 主鍵
|
||||||
|
business_code: string; // 事業體代碼 (BU001, BU002...)
|
||||||
|
business_name: string; // 事業體名稱
|
||||||
|
sort_order: number; // 排序
|
||||||
|
is_active: boolean; // 是否啟用
|
||||||
|
remark: string; // 備註
|
||||||
|
created_at: string; // 建立時間
|
||||||
|
updated_at: string; // 更新時間
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 7.5.2 處級單位 (Division)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface Division {
|
||||||
|
id: number; // 主鍵
|
||||||
|
division_code: string; // 處級單位代碼 (DIV001, DIV002...)
|
||||||
|
division_name: string; // 處級單位名稱
|
||||||
|
business_id: number; // 所屬事業體ID (FK)
|
||||||
|
sort_order: number; // 排序
|
||||||
|
is_active: boolean; // 是否啟用
|
||||||
|
remark: string; // 備註
|
||||||
|
created_at: string; // 建立時間
|
||||||
|
updated_at: string; // 更新時間
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 7.5.3 部級單位 (Department)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface Department {
|
||||||
|
id: number; // 主鍵
|
||||||
|
department_code: string; // 部級單位代碼 (DEPT001, DEPT002...)
|
||||||
|
department_name: string; // 部級單位名稱
|
||||||
|
division_id: number; // 所屬處級單位ID (FK)
|
||||||
|
sort_order: number; // 排序
|
||||||
|
is_active: boolean; // 是否啟用
|
||||||
|
remark: string; // 備註
|
||||||
|
created_at: string; // 建立時間
|
||||||
|
updated_at: string; // 更新時間
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 7.5.4 組織崗位關聯 (OrganizationPosition)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface OrganizationPosition {
|
||||||
|
id: number; // 主鍵
|
||||||
|
business_id: number; // 事業體ID (FK)
|
||||||
|
division_id: number; // 處級單位ID (FK)
|
||||||
|
department_id: number; // 部級單位ID (FK)
|
||||||
|
position_title: string; // 崗位名稱
|
||||||
|
sort_order: number; // 排序
|
||||||
|
is_active: boolean; // 是否啟用
|
||||||
|
created_at: string; // 建立時間
|
||||||
|
updated_at: string; // 更新時間
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 7.5.5 組織階層關係圖
|
||||||
|
|
||||||
|
```
|
||||||
|
事業體 (business_units)
|
||||||
|
│
|
||||||
|
├── 岡山製造事業體
|
||||||
|
│ ├── 生產處 (divisions)
|
||||||
|
│ │ ├── 生產部 (departments)
|
||||||
|
│ │ │ ├── 經副理 (organization_positions)
|
||||||
|
│ │ │ ├── 課長
|
||||||
|
│ │ │ ├── 組長
|
||||||
|
│ │ │ └── 班長
|
||||||
|
│ │ └── 生產企劃部
|
||||||
|
│ │ ├── 經副理
|
||||||
|
│ │ └── 專員
|
||||||
|
│ ├── 封裝工程處
|
||||||
|
│ └── ...
|
||||||
|
│
|
||||||
|
├── 產品事業體
|
||||||
|
│ ├── 先進產品事業處
|
||||||
|
│ └── 成熟產品事業處
|
||||||
|
│
|
||||||
|
└── ...(共 15 個事業體)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 7.5.6 資料統計
|
||||||
|
|
||||||
|
| 層級 | 資料筆數 |
|
||||||
|
|------|----------|
|
||||||
|
| 事業體 | 15 筆 |
|
||||||
|
| 處級單位 | ~45 筆 |
|
||||||
|
| 部級單位 | ~70 筆 |
|
||||||
|
| 組織崗位關聯 | ~350 筆(去重後) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 8. 快捷鍵設計
|
## 8. 快捷鍵設計
|
||||||
@@ -758,9 +1001,35 @@ interface PositionListItem {
|
|||||||
hr_position_form/
|
hr_position_form/
|
||||||
├── index.html # 前端應用主檔
|
├── index.html # 前端應用主檔
|
||||||
├── app.py # Flask 後端 API
|
├── app.py # Flask 後端 API
|
||||||
|
├── llm_config.py # LLM API 配置模組
|
||||||
|
├── database_schema.sql # MySQL 資料庫結構定義
|
||||||
|
├── init_database.py # 資料庫初始化腳本
|
||||||
|
├── import_hierarchy_data.py # 組織階層資料匯入腳本
|
||||||
|
├── hierarchical_data.js # 組織階層原始資料
|
||||||
├── requirements.txt # Python 依賴套件
|
├── requirements.txt # Python 依賴套件
|
||||||
|
├── .env # 環境變數配置 (API Keys)
|
||||||
├── README.md # 使用說明文件
|
├── README.md # 使用說明文件
|
||||||
└── SDD.md # 軟體設計文件(本文件)
|
├── SDD.md # 軟體設計文件
|
||||||
|
├── TDD.md # 測試設計文件
|
||||||
|
│
|
||||||
|
├── js/ # JavaScript 模組
|
||||||
|
│ ├── main.js # 應用程式入口
|
||||||
|
│ ├── ui.js # UI 互動邏輯
|
||||||
|
│ ├── api.js # API 呼叫封裝
|
||||||
|
│ ├── ai-bags.js # 三個錦囊功能
|
||||||
|
│ ├── utils.js # 工具函數
|
||||||
|
│ └── config.js # 配置常數
|
||||||
|
│
|
||||||
|
├── styles/ # CSS 樣式模組
|
||||||
|
│ ├── base.css # 基礎樣式
|
||||||
|
│ ├── layout.css # 頁面佈局
|
||||||
|
│ ├── components.css # UI 元件
|
||||||
|
│ ├── modules.css # 模組樣式
|
||||||
|
│ └── responsive.css # 響應式設計
|
||||||
|
│
|
||||||
|
└── data/ # 資料檔案
|
||||||
|
├── positions.csv # 崗位資料範本
|
||||||
|
└── jobs.csv # 職務資料範本
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -837,6 +1106,8 @@ const i18n = {
|
|||||||
| 1.0 | 2024-12-03 | System | 初始版本,包含三大模組設計與 AI 功能 |
|
| 1.0 | 2024-12-03 | System | 初始版本,包含三大模組設計與 AI 功能 |
|
||||||
| 2.0 | 2024-12-04 | System | 新增 MySQL 資料庫整合、多 LLM API 支援、全局錯誤處理、Gitea 版本控制 |
|
| 2.0 | 2024-12-04 | System | 新增 MySQL 資料庫整合、多 LLM API 支援、全局錯誤處理、Gitea 版本控制 |
|
||||||
| 2.1 | 2024-12-04 | System | 新增崗位描述保存功能、崗位清單模組、管理者頁面匯出功能、CSV 批次匯入 |
|
| 2.1 | 2024-12-04 | System | 新增崗位描述保存功能、崗位清單模組、管理者頁面匯出功能、CSV 批次匯入 |
|
||||||
|
| 3.0 | 2024-12-06 | System | 新增部門職責模組、三個錦囊 AI 功能、ES6 模組化架構、改進 JSON 解析錯誤處理 |
|
||||||
|
| 3.1 | 2024-12-08 | System | 新增組織階層管理模組,包含事業體、處級單位、部級單位、崗位的四層架構;新增 7 個組織階層 API 端點;更新資料庫結構支援組織階層資料 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
1120
docs/SDD_代碼分離優化.md
Normal file
1120
docs/SDD_代碼分離優化.md
Normal file
File diff suppressed because it is too large
Load Diff
496
docs/TDD.md
Normal file
496
docs/TDD.md
Normal file
@@ -0,0 +1,496 @@
|
|||||||
|
# HR 基礎資料維護系統 - 測試設計文件 (TDD)
|
||||||
|
|
||||||
|
**文件版本**:1.1
|
||||||
|
**建立日期**:2024-12-06
|
||||||
|
**最後更新**:2025-12-08
|
||||||
|
**文件狀態**:Draft
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 文件概述
|
||||||
|
|
||||||
|
### 1.1 目的
|
||||||
|
|
||||||
|
本文件為 HR 基礎資料維護系統之測試設計文件 (Test Design Document),描述測試策略、測試範圍、測試案例設計與驗收標準,供測試人員與開發人員參考。
|
||||||
|
|
||||||
|
### 1.2 範圍
|
||||||
|
|
||||||
|
本測試涵蓋以下功能模組:
|
||||||
|
|
||||||
|
| 模組 | 測試重點 |
|
||||||
|
|------|----------|
|
||||||
|
| 崗位基礎資料 | CRUD 操作、欄位驗證、資料聯動 |
|
||||||
|
| 職務基礎資料 | CRUD 操作、類別關聯、Toggle 控件 |
|
||||||
|
| 部門職責 | CRUD 操作、組織層級聯動 |
|
||||||
|
| 崗位描述 | CRUD 操作、版本控制 |
|
||||||
|
| AI 幫我想 | LLM 呼叫、JSON 解析、Prompt 編輯、結果填入 |
|
||||||
|
| 崗位清單 | 列表顯示、搜尋過濾、資料匯出 |
|
||||||
|
| 組織階層 | 事業體/處級/部級單位、級聯選擇、API 查詢 |
|
||||||
|
| CSV 匯入匯出 | 批次匯入、範本下載、資料驗證 |
|
||||||
|
|
||||||
|
### 1.3 參考文件
|
||||||
|
|
||||||
|
- 軟體設計文件 (SDD.md)
|
||||||
|
- API 設計規範
|
||||||
|
- 需求確認文件
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 測試策略
|
||||||
|
|
||||||
|
### 2.1 測試層次
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ E2E 測試 (End-to-End) │
|
||||||
|
│ ┌─────────────────────────────────────────────────┐ │
|
||||||
|
│ │ 整合測試 (Integration) │ │
|
||||||
|
│ │ ┌─────────────────────────────────────────┐ │ │
|
||||||
|
│ │ │ 單元測試 (Unit) │ │ │
|
||||||
|
│ │ │ ┌─────────────────────────────────┐ │ │ │
|
||||||
|
│ │ │ │ 函數 / API / 元件 │ │ │ │
|
||||||
|
│ │ │ └─────────────────────────────────┘ │ │ │
|
||||||
|
│ │ └─────────────────────────────────────────┘ │ │
|
||||||
|
│ └─────────────────────────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 測試類型
|
||||||
|
|
||||||
|
| 類型 | 說明 | 工具 |
|
||||||
|
|------|------|------|
|
||||||
|
| 單元測試 | 測試單一函數或模組 | Jest / pytest |
|
||||||
|
| 整合測試 | 測試模組間互動 | pytest |
|
||||||
|
| API 測試 | 測試 RESTful API | Postman / pytest |
|
||||||
|
| UI 測試 | 測試使用者介面 | 手動測試 / Selenium |
|
||||||
|
| 效能測試 | 測試系統效能 | Apache JMeter |
|
||||||
|
|
||||||
|
### 2.3 測試環境
|
||||||
|
|
||||||
|
| 環境 | 配置 |
|
||||||
|
|------|------|
|
||||||
|
| 前端 | Chrome/Firefox/Edge 最新版 |
|
||||||
|
| 後端 | Python 3.9+, Flask 2.0+ |
|
||||||
|
| LLM | Ollama localhost:11434 |
|
||||||
|
| 資料庫 | In-Memory (測試環境) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 功能測試案例
|
||||||
|
|
||||||
|
### 3.1 崗位基礎資料模組
|
||||||
|
|
||||||
|
#### 3.1.1 新增崗位
|
||||||
|
|
||||||
|
| TC-ID | TC-POS-001 |
|
||||||
|
|-------|------------|
|
||||||
|
| 測試名稱 | 新增崗位 - 正常流程 |
|
||||||
|
| 前置條件 | 系統已啟動,用戶在崗位基礎資料頁面 |
|
||||||
|
| 測試步驟 | 1. 輸入崗位編號 "ENG-001"<br>2. 輸入崗位名稱 "資深軟體工程師"<br>3. 選擇崗位類別 "技術職"<br>4. 點擊「保存」按鈕 |
|
||||||
|
| 預期結果 | 顯示「保存成功」Toast,資料寫入成功 |
|
||||||
|
| 實際結果 | |
|
||||||
|
| 通過/失敗 | |
|
||||||
|
|
||||||
|
| TC-ID | TC-POS-002 |
|
||||||
|
|-------|------------|
|
||||||
|
| 測試名稱 | 新增崗位 - 必填欄位驗證 |
|
||||||
|
| 前置條件 | 系統已啟動,用戶在崗位基礎資料頁面 |
|
||||||
|
| 測試步驟 | 1. 不輸入崗位編號<br>2. 不輸入崗位名稱<br>3. 點擊「保存」按鈕 |
|
||||||
|
| 預期結果 | 顯示錯誤訊息「請填寫必填欄位」 |
|
||||||
|
| 實際結果 | |
|
||||||
|
| 通過/失敗 | |
|
||||||
|
|
||||||
|
| TC-ID | TC-POS-003 |
|
||||||
|
|-------|------------|
|
||||||
|
| 測試名稱 | 新增崗位 - 編號重複檢查 |
|
||||||
|
| 前置條件 | 已存在編號 "ENG-001" 的崗位 |
|
||||||
|
| 測試步驟 | 1. 輸入崗位編號 "ENG-001"<br>2. 輸入其他欄位<br>3. 點擊「保存」按鈕 |
|
||||||
|
| 預期結果 | 顯示錯誤訊息「崗位編號已存在」 |
|
||||||
|
| 實際結果 | |
|
||||||
|
| 通過/失敗 | |
|
||||||
|
|
||||||
|
#### 3.1.2 編輯崗位
|
||||||
|
|
||||||
|
| TC-ID | TC-POS-004 |
|
||||||
|
|-------|------------|
|
||||||
|
| 測試名稱 | 編輯崗位 - 正常流程 |
|
||||||
|
| 前置條件 | 已存在崗位 "ENG-001" |
|
||||||
|
| 測試步驟 | 1. 瀏覽至崗位 "ENG-001"<br>2. 修改崗位名稱為 "高級軟體工程師"<br>3. 點擊「保存」按鈕 |
|
||||||
|
| 預期結果 | 顯示「更新成功」Toast,資料已更新 |
|
||||||
|
| 實際結果 | |
|
||||||
|
| 通過/失敗 | |
|
||||||
|
|
||||||
|
#### 3.1.3 刪除崗位
|
||||||
|
|
||||||
|
| TC-ID | TC-POS-005 |
|
||||||
|
|-------|------------|
|
||||||
|
| 測試名稱 | 刪除崗位 - 正常流程 |
|
||||||
|
| 前置條件 | 已存在崗位 "ENG-001" |
|
||||||
|
| 測試步驟 | 1. 瀏覽至崗位 "ENG-001"<br>2. 點擊「刪除」按鈕<br>3. 確認刪除 |
|
||||||
|
| 預期結果 | 顯示「刪除成功」Toast,資料已移除 |
|
||||||
|
| 實際結果 | |
|
||||||
|
| 通過/失敗 | |
|
||||||
|
|
||||||
|
### 3.2 職務基礎資料模組
|
||||||
|
|
||||||
|
| TC-ID | TC-JOB-001 |
|
||||||
|
|-------|------------|
|
||||||
|
| 測試名稱 | 新增職務 - 正常流程 |
|
||||||
|
| 前置條件 | 系統已啟動,用戶在職務基礎資料頁面 |
|
||||||
|
| 測試步驟 | 1. 選擇職務類別 "技術職"<br>2. 輸入職務編號 "TECH-001"<br>3. 輸入職務名稱 "軟體工程師"<br>4. 點擊「保存」按鈕 |
|
||||||
|
| 預期結果 | 顯示「保存成功」Toast |
|
||||||
|
| 實際結果 | |
|
||||||
|
| 通過/失敗 | |
|
||||||
|
|
||||||
|
| TC-ID | TC-JOB-002 |
|
||||||
|
|-------|------------|
|
||||||
|
| 測試名稱 | Toggle 開關功能 |
|
||||||
|
| 前置條件 | 系統已啟動,用戶在職務基礎資料頁面 |
|
||||||
|
| 測試步驟 | 1. 開啟「是否有全勤獎金」Toggle<br>2. 開啟「是否住房補貼」Toggle<br>3. 保存資料 |
|
||||||
|
| 預期結果 | Toggle 狀態正確保存,重新載入後狀態一致 |
|
||||||
|
| 實際結果 | |
|
||||||
|
| 通過/失敗 | |
|
||||||
|
|
||||||
|
### 3.3 部門職責模組
|
||||||
|
|
||||||
|
| TC-ID | TC-DF-001 |
|
||||||
|
|-------|------------|
|
||||||
|
| 測試名稱 | 新增部門職責 - 正常流程 |
|
||||||
|
| 前置條件 | 系統已啟動,用戶在部門職責頁面 |
|
||||||
|
| 測試步驟 | 1. 輸入職責編號 "DF-001"<br>2. 輸入職責名稱 "研發部門職責"<br>3. 選擇事業體、處級單位、部級單位<br>4. 輸入核心職責<br>5. 點擊「保存」按鈕 |
|
||||||
|
| 預期結果 | 顯示「保存成功」Toast |
|
||||||
|
| 實際結果 | |
|
||||||
|
| 通過/失敗 | |
|
||||||
|
|
||||||
|
| TC-ID | TC-DF-002 |
|
||||||
|
|-------|------------|
|
||||||
|
| 測試名稱 | 組織層級聯動 |
|
||||||
|
| 前置條件 | 系統已啟動 |
|
||||||
|
| 測試步驟 | 1. 選擇事業體 "SBU"<br>2. 觀察處級單位選項變化<br>3. 選擇處級單位<br>4. 觀察部級單位選項變化 |
|
||||||
|
| 預期結果 | 各層級選項正確聯動,無無效選項 |
|
||||||
|
| 實際結果 | |
|
||||||
|
| 通過/失敗 | |
|
||||||
|
|
||||||
|
### 3.4 AI 幫我想功能
|
||||||
|
|
||||||
|
> **說明**:v1.1 版本將三個錦囊按鈕整合為單一「AI 幫我想」按鈕,並提供可編輯的 Prompt 編輯器。
|
||||||
|
|
||||||
|
| TC-ID | TC-AI-001 |
|
||||||
|
|-------|------------|
|
||||||
|
| 測試名稱 | AI 幫我想 - 正常流程 |
|
||||||
|
| 前置條件 | Ollama 服務已啟動,用戶在崗位基礎資料頁面 |
|
||||||
|
| 測試步驟 | 1. 查看預設 Prompt 內容<br>2. 點擊「AI 幫我想」按鈕<br>3. 等待 AI 生成完成 |
|
||||||
|
| 預期結果 | AI 生成資料並填充至表單空白欄位,顯示「AI 生成完成!已填入表單」Toast |
|
||||||
|
| 實際結果 | |
|
||||||
|
| 通過/失敗 | |
|
||||||
|
|
||||||
|
| TC-ID | TC-AI-002 |
|
||||||
|
|-------|------------|
|
||||||
|
| 測試名稱 | Prompt 編輯功能 |
|
||||||
|
| 前置條件 | 系統已啟動 |
|
||||||
|
| 測試步驟 | 1. 在 Prompt 編輯器修改內容<br>2. 點擊「AI 幫我想」按鈕<br>3. 重新整理頁面<br>4. 檢查 Prompt 是否保留 |
|
||||||
|
| 預期結果 | 修改的 Prompt 保存至 LocalStorage,重新整理後恢復 |
|
||||||
|
| 實際結果 | |
|
||||||
|
| 通過/失敗 | |
|
||||||
|
|
||||||
|
| TC-ID | TC-AI-003 |
|
||||||
|
|-------|------------|
|
||||||
|
| 測試名稱 | Prompt 重置功能 |
|
||||||
|
| 前置條件 | 已修改 Prompt 內容 |
|
||||||
|
| 測試步驟 | 1. 修改 Prompt 內容<br>2. 點擊重置按鈕(↺ 圖示) |
|
||||||
|
| 預期結果 | Prompt 恢復為預設值,顯示「已重置為預設 Prompt」Toast |
|
||||||
|
| 實際結果 | |
|
||||||
|
| 通過/失敗 | |
|
||||||
|
|
||||||
|
| TC-ID | TC-AI-004 |
|
||||||
|
|-------|------------|
|
||||||
|
| 測試名稱 | 各模組 AI 功能 |
|
||||||
|
| 前置條件 | Ollama 服務已啟動 |
|
||||||
|
| 測試步驟 | 分別在以下模組測試 AI 功能:<br>1. 崗位基礎資料 (positionBasic)<br>2. 招聘要求 (positionRecruit)<br>3. 職務基礎資料 (jobBasic)<br>4. 部門職責 (deptFunction)<br>5. 崗位描述 (jobDesc) |
|
||||||
|
| 預期結果 | 各模組 AI 均能正常生成對應欄位資料 |
|
||||||
|
| 實際結果 | |
|
||||||
|
| 通過/失敗 | |
|
||||||
|
|
||||||
|
| TC-ID | TC-AI-005 |
|
||||||
|
|-------|------------|
|
||||||
|
| 測試名稱 | JSON 解析錯誤處理 |
|
||||||
|
| 前置條件 | LLM 返回非標準 JSON 格式(含思考過程) |
|
||||||
|
| 測試步驟 | 使用 deepseek-reasoner 等推理模型,觀察 JSON 解析 |
|
||||||
|
| 預期結果 | 系統正確從回應中提取 JSON 物件,忽略思考過程文字 |
|
||||||
|
| 實際結果 | |
|
||||||
|
| 通過/失敗 | |
|
||||||
|
|
||||||
|
| TC-ID | TC-AI-006 |
|
||||||
|
|-------|------------|
|
||||||
|
| 測試名稱 | 按鈕載入狀態 |
|
||||||
|
| 前置條件 | 系統已啟動 |
|
||||||
|
| 測試步驟 | 1. 點擊「AI 幫我想」按鈕<br>2. 觀察按鈕狀態變化 |
|
||||||
|
| 預期結果 | 按鈕顯示 spinner 和「AI 生成中...」文字,完成後恢復原狀 |
|
||||||
|
| 實際結果 | |
|
||||||
|
| 通過/失敗 | |
|
||||||
|
|
||||||
|
| TC-ID | TC-AI-007 |
|
||||||
|
|-------|------------|
|
||||||
|
| 測試名稱 | 空白 Prompt 防護 |
|
||||||
|
| 前置條件 | 系統已啟動 |
|
||||||
|
| 測試步驟 | 1. 清空 Prompt 編輯器<br>2. 點擊「AI 幫我想」按鈕 |
|
||||||
|
| 預期結果 | 顯示錯誤訊息「請輸入 Prompt 指令」 |
|
||||||
|
| 實際結果 | |
|
||||||
|
| 通過/失敗 | |
|
||||||
|
|
||||||
|
### 3.5 CSV 匯入匯出
|
||||||
|
|
||||||
|
| TC-ID | TC-CSV-001 |
|
||||||
|
|-------|------------|
|
||||||
|
| 測試名稱 | 下載 CSV 範本 |
|
||||||
|
| 前置條件 | 系統已啟動 |
|
||||||
|
| 測試步驟 | 1. 點擊「下載崗位 CSV 範本」按鈕 |
|
||||||
|
| 預期結果 | 瀏覽器下載 CSV 檔案,格式正確 |
|
||||||
|
| 實際結果 | |
|
||||||
|
| 通過/失敗 | |
|
||||||
|
|
||||||
|
| TC-ID | TC-CSV-002 |
|
||||||
|
|-------|------------|
|
||||||
|
| 測試名稱 | 匯入崗位 CSV |
|
||||||
|
| 前置條件 | 已準備符合格式的 CSV 檔案 |
|
||||||
|
| 測試步驟 | 1. 點擊「匯入崗位」按鈕<br>2. 選擇 CSV 檔案<br>3. 確認匯入 |
|
||||||
|
| 預期結果 | 顯示匯入結果,資料正確寫入 |
|
||||||
|
| 實際結果 | |
|
||||||
|
| 通過/失敗 | |
|
||||||
|
|
||||||
|
| TC-ID | TC-CSV-003 |
|
||||||
|
|-------|------------|
|
||||||
|
| 測試名稱 | 匯入 CSV - 格式錯誤 |
|
||||||
|
| 前置條件 | CSV 檔案格式不正確 |
|
||||||
|
| 測試步驟 | 1. 選擇格式錯誤的 CSV 檔案<br>2. 嘗試匯入 |
|
||||||
|
| 預期結果 | 顯示錯誤訊息,說明格式問題 |
|
||||||
|
| 實際結果 | |
|
||||||
|
| 通過/失敗 | |
|
||||||
|
|
||||||
|
### 3.6 組織階層模組
|
||||||
|
|
||||||
|
| TC-ID | TC-ORG-001 |
|
||||||
|
|-------|------------|
|
||||||
|
| 測試名稱 | 事業體列表載入 |
|
||||||
|
| 前置條件 | 組織階層資料已匯入 |
|
||||||
|
| 測試步驟 | 1. 開啟部門職責頁面<br>2. 查看事業體下拉選單 |
|
||||||
|
| 預期結果 | 顯示所有事業體選項(SBU, PBU, NBU 等) |
|
||||||
|
| 實際結果 | |
|
||||||
|
| 通過/失敗 | |
|
||||||
|
|
||||||
|
| TC-ID | TC-ORG-002 |
|
||||||
|
|-------|------------|
|
||||||
|
| 測試名稱 | 處級單位級聯選擇 |
|
||||||
|
| 前置條件 | 組織階層資料已匯入 |
|
||||||
|
| 測試步驟 | 1. 選擇事業體 "SBU"<br>2. 觀察處級單位選項 |
|
||||||
|
| 預期結果 | 處級單位僅顯示屬於 SBU 的選項 |
|
||||||
|
| 實際結果 | |
|
||||||
|
| 通過/失敗 | |
|
||||||
|
|
||||||
|
| TC-ID | TC-ORG-003 |
|
||||||
|
|-------|------------|
|
||||||
|
| 測試名稱 | 部級單位級聯選擇 |
|
||||||
|
| 前置條件 | 已選擇事業體和處級單位 |
|
||||||
|
| 測試步驟 | 1. 選擇處級單位<br>2. 觀察部級單位選項 |
|
||||||
|
| 預期結果 | 部級單位僅顯示屬於該處級單位的選項 |
|
||||||
|
| 實際結果 | |
|
||||||
|
| 通過/失敗 | |
|
||||||
|
|
||||||
|
| TC-ID | TC-ORG-004 |
|
||||||
|
|-------|------------|
|
||||||
|
| 測試名稱 | 崗位名稱級聯選擇 |
|
||||||
|
| 前置條件 | 已選擇部級單位 |
|
||||||
|
| 測試步驟 | 1. 選擇部級單位<br>2. 觀察崗位名稱選項 |
|
||||||
|
| 預期結果 | 崗位名稱僅顯示屬於該部級單位的選項 |
|
||||||
|
| 實際結果 | |
|
||||||
|
| 通過/失敗 | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. API 測試案例
|
||||||
|
|
||||||
|
### 4.1 崗位 API
|
||||||
|
|
||||||
|
| TC-ID | 端點 | 方法 | 測試說明 | 預期狀態碼 |
|
||||||
|
|-------|------|------|----------|------------|
|
||||||
|
| TC-API-001 | /api/positions | GET | 查詢所有崗位 | 200 |
|
||||||
|
| TC-API-002 | /api/positions | POST | 新增崗位(正常) | 201 |
|
||||||
|
| TC-API-003 | /api/positions | POST | 新增崗位(缺少必填) | 400 |
|
||||||
|
| TC-API-004 | /api/positions/{id} | GET | 查詢單一崗位 | 200 |
|
||||||
|
| TC-API-005 | /api/positions/{id} | GET | 查詢不存在崗位 | 404 |
|
||||||
|
| TC-API-006 | /api/positions/{id} | PUT | 更新崗位 | 200 |
|
||||||
|
| TC-API-007 | /api/positions/{id} | DELETE | 刪除崗位 | 200 |
|
||||||
|
|
||||||
|
### 4.2 職務 API
|
||||||
|
|
||||||
|
| TC-ID | 端點 | 方法 | 測試說明 | 預期狀態碼 |
|
||||||
|
|-------|------|------|----------|------------|
|
||||||
|
| TC-API-011 | /api/jobs | GET | 查詢所有職務 | 200 |
|
||||||
|
| TC-API-012 | /api/jobs | POST | 新增職務(正常) | 201 |
|
||||||
|
| TC-API-013 | /api/jobs/{id} | PUT | 更新職務 | 200 |
|
||||||
|
| TC-API-014 | /api/jobs/{id} | DELETE | 刪除職務 | 200 |
|
||||||
|
|
||||||
|
### 4.3 LLM API
|
||||||
|
|
||||||
|
| TC-ID | 端點 | 方法 | 測試說明 | 預期狀態碼 |
|
||||||
|
|-------|------|------|----------|------------|
|
||||||
|
| TC-API-021 | /api/llm/generate | POST | LLM 生成(Ollama) | 200 |
|
||||||
|
| TC-API-022 | /api/llm/generate | POST | LLM 生成(無效 API) | 400 |
|
||||||
|
| TC-API-023 | /api/llm/generate | POST | LLM 服務不可用 | 500 |
|
||||||
|
|
||||||
|
### 4.4 組織階層 API
|
||||||
|
|
||||||
|
| TC-ID | 端點 | 方法 | 測試說明 | 預期狀態碼 |
|
||||||
|
|-------|------|------|----------|------------|
|
||||||
|
| TC-API-031 | /api/hierarchy/business-units | GET | 查詢所有事業體 | 200 |
|
||||||
|
| TC-API-032 | /api/hierarchy/divisions | GET | 查詢所有處級單位 | 200 |
|
||||||
|
| TC-API-033 | /api/hierarchy/divisions?business=SBU | GET | 按事業體篩選處級單位 | 200 |
|
||||||
|
| TC-API-034 | /api/hierarchy/departments | GET | 查詢所有部級單位 | 200 |
|
||||||
|
| TC-API-035 | /api/hierarchy/departments?division=技術處 | GET | 按處級篩選部級單位 | 200 |
|
||||||
|
| TC-API-036 | /api/hierarchy/positions | GET | 查詢所有崗位名稱 | 200 |
|
||||||
|
| TC-API-037 | /api/hierarchy/positions?department=研發部 | GET | 按部級篩選崗位 | 200 |
|
||||||
|
| TC-API-038 | /api/hierarchy/full | GET | 查詢完整階層資料 | 200 |
|
||||||
|
| TC-API-039 | /api/hierarchy/full?page=1&size=50 | GET | 分頁查詢階層資料 | 200 |
|
||||||
|
| TC-API-040 | /api/hierarchy/cascade | GET | 查詢級聯選擇資料 | 200 |
|
||||||
|
| TC-API-041 | /api/hierarchy/stats | GET | 查詢組織統計 | 200 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. UI 測試案例
|
||||||
|
|
||||||
|
### 5.1 表單互動
|
||||||
|
|
||||||
|
| TC-ID | TC-UI-001 |
|
||||||
|
|-------|------------|
|
||||||
|
| 測試名稱 | 頁籤切換 |
|
||||||
|
| 測試步驟 | 1. 點擊「招聘要求資料」頁籤<br>2. 點擊「基礎資料」頁籤 |
|
||||||
|
| 預期結果 | 頁籤內容正確切換,動畫流暢 |
|
||||||
|
|
||||||
|
| TC-ID | TC-UI-002 |
|
||||||
|
|-------|------------|
|
||||||
|
| 測試名稱 | 類別聯動顯示 |
|
||||||
|
| 測試步驟 | 1. 選擇崗位類別 "01"<br>2. 觀察崗位類別名稱欄位 |
|
||||||
|
| 預期結果 | 自動顯示「技術職」 |
|
||||||
|
|
||||||
|
| TC-ID | TC-UI-003 |
|
||||||
|
|-------|------------|
|
||||||
|
| 測試名稱 | Toast 通知顯示 |
|
||||||
|
| 測試步驟 | 1. 執行保存操作 |
|
||||||
|
| 預期結果 | 右上角顯示 Toast 通知,3秒後自動消失 |
|
||||||
|
|
||||||
|
### 5.2 響應式設計
|
||||||
|
|
||||||
|
| TC-ID | TC-UI-010 |
|
||||||
|
|-------|------------|
|
||||||
|
| 測試名稱 | 手機版佈局 |
|
||||||
|
| 測試步驟 | 將瀏覽器寬度調整為 375px |
|
||||||
|
| 預期結果 | 表單變為單欄佈局,按鈕仍可操作 |
|
||||||
|
|
||||||
|
| TC-ID | TC-UI-011 |
|
||||||
|
|-------|------------|
|
||||||
|
| 測試名稱 | 平板版佈局 |
|
||||||
|
| 測試步驟 | 將瀏覽器寬度調整為 768px |
|
||||||
|
| 預期結果 | 佈局適當調整,無元素重疊 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 效能測試
|
||||||
|
|
||||||
|
### 6.1 載入效能
|
||||||
|
|
||||||
|
| 測試項目 | 目標值 | 測試方法 |
|
||||||
|
|----------|--------|----------|
|
||||||
|
| 首次載入時間 | < 3秒 | Chrome DevTools |
|
||||||
|
| API 回應時間 | < 500ms | Postman |
|
||||||
|
| LLM 生成時間 | < 30秒 | 實際測試 |
|
||||||
|
|
||||||
|
### 6.2 並發測試
|
||||||
|
|
||||||
|
| 測試項目 | 目標值 |
|
||||||
|
|----------|--------|
|
||||||
|
| 同時連線數 | 50+ |
|
||||||
|
| API 請求/秒 | 100+ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 安全測試
|
||||||
|
|
||||||
|
### 7.1 輸入驗證
|
||||||
|
|
||||||
|
| TC-ID | TC-SEC-001 |
|
||||||
|
|-------|------------|
|
||||||
|
| 測試名稱 | XSS 防護 |
|
||||||
|
| 測試步驟 | 在輸入框輸入 `<script>alert('XSS')</script>` |
|
||||||
|
| 預期結果 | 腳本不執行,內容被轉義或過濾 |
|
||||||
|
|
||||||
|
| TC-ID | TC-SEC-002 |
|
||||||
|
|-------|------------|
|
||||||
|
| 測試名稱 | SQL Injection 防護 |
|
||||||
|
| 測試步驟 | 在輸入框輸入 `'; DROP TABLE positions; --` |
|
||||||
|
| 預期結果 | SQL 不執行,顯示錯誤或過濾 |
|
||||||
|
|
||||||
|
### 7.2 API 安全
|
||||||
|
|
||||||
|
| TC-ID | TC-SEC-010 |
|
||||||
|
|-------|------------|
|
||||||
|
| 測試名稱 | CORS 設定 |
|
||||||
|
| 測試步驟 | 從不同來源發送 API 請求 |
|
||||||
|
| 預期結果 | 僅允許白名單來源 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 測試報告模板
|
||||||
|
|
||||||
|
### 8.1 測試摘要
|
||||||
|
|
||||||
|
| 項目 | 數值 |
|
||||||
|
|------|------|
|
||||||
|
| 總測試案例數 | |
|
||||||
|
| 通過數 | |
|
||||||
|
| 失敗數 | |
|
||||||
|
| 通過率 | |
|
||||||
|
| 測試日期 | |
|
||||||
|
| 測試人員 | |
|
||||||
|
|
||||||
|
### 8.2 缺陷追蹤
|
||||||
|
|
||||||
|
| 缺陷ID | 嚴重度 | 標題 | 狀態 | 負責人 |
|
||||||
|
|--------|--------|------|------|--------|
|
||||||
|
| | | | | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 驗收標準
|
||||||
|
|
||||||
|
### 9.1 功能驗收
|
||||||
|
|
||||||
|
- [ ] 所有 CRUD 操作正常運作
|
||||||
|
- [ ] 三個錦囊 AI 功能可正常生成內容
|
||||||
|
- [ ] CSV 匯入匯出功能完整
|
||||||
|
- [ ] 所有必填欄位驗證正確
|
||||||
|
- [ ] 組織層級聯動正確
|
||||||
|
|
||||||
|
### 9.2 效能驗收
|
||||||
|
|
||||||
|
- [ ] 頁面載入時間 < 3秒
|
||||||
|
- [ ] API 回應時間 < 500ms
|
||||||
|
- [ ] 無明顯 UI 卡頓
|
||||||
|
|
||||||
|
### 9.3 相容性驗收
|
||||||
|
|
||||||
|
- [ ] Chrome 最新版正常
|
||||||
|
- [ ] Firefox 最新版正常
|
||||||
|
- [ ] Edge 最新版正常
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 版本歷史
|
||||||
|
|
||||||
|
| 版本 | 日期 | 作者 | 變更說明 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| 1.0 | 2024-12-06 | System | 初始版本 |
|
||||||
|
| 1.1 | 2025-12-08 | System | 1. 更新 AI 功能測試案例(三個錦囊 → AI 幫我想)<br>2. 新增組織階層模組測試案例<br>3. 新增組織階層 API 測試案例<br>4. 更新 JSON 解析錯誤處理測試(支援推理模型) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**文件結束**
|
||||||
26
docs/建議專案結構.md
Normal file
26
docs/建議專案結構.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
project_name/
|
||||||
|
├── .env # 環境變數(不進版控)
|
||||||
|
├── .env.example # 環境變數範本
|
||||||
|
├── .gitignore
|
||||||
|
├── README.md
|
||||||
|
├── requirements.txt
|
||||||
|
├── app.py # 主程式入口
|
||||||
|
├── config.py # 設定檔
|
||||||
|
├── preview.html # UI 預覽
|
||||||
|
│
|
||||||
|
├── docs/
|
||||||
|
│ ├── SDD.md # 系統設計文件
|
||||||
|
│ ├── security_audit.md # 資安檢視報告
|
||||||
|
│ ├── user_command_log.md # 用戶指令記錄
|
||||||
|
│ ├── CHANGELOG.md # 版本變更紀錄
|
||||||
|
│ └── API_DOC.md # API 文件
|
||||||
|
│
|
||||||
|
├── models/ # 資料庫模型
|
||||||
|
├── routes/ # 路由模組
|
||||||
|
├── services/ # 商業邏輯
|
||||||
|
├── utils/ # 工具函式
|
||||||
|
├── templates/ # HTML 模板
|
||||||
|
└── static/ # 靜態資源
|
||||||
|
├── css/
|
||||||
|
├── js/
|
||||||
|
└── images/
|
||||||
265
docs/需求確認文件.md
Normal file
265
docs/需求確認文件.md
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
# HR 基礎資料維護系統 - 需求確認文件
|
||||||
|
|
||||||
|
**文件版本**:1.0
|
||||||
|
**建立日期**:2024-12-06
|
||||||
|
**最後更新**:2024-12-06
|
||||||
|
**文件狀態**:待確認
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 專案基本資訊
|
||||||
|
|
||||||
|
| 項目 | 內容 |
|
||||||
|
|------|------|
|
||||||
|
| 專案名稱 | HR 基礎資料維護系統 |
|
||||||
|
| 專案目的 | 提供人力資源部門維護崗位、職務、部門職責等基礎資料的工具 |
|
||||||
|
| 目標用戶 | HR 人員、部門主管、系統管理員 |
|
||||||
|
| 開發狀態 | 開發中 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 功能模組確認
|
||||||
|
|
||||||
|
### 2.1 崗位基礎資料模組
|
||||||
|
|
||||||
|
| 功能項目 | 狀態 | 備註 |
|
||||||
|
|----------|------|------|
|
||||||
|
| 新增崗位 | 已實現 | |
|
||||||
|
| 編輯崗位 | 已實現 | |
|
||||||
|
| 刪除崗位 | 已實現 | |
|
||||||
|
| 崗位編號變更 | 已實現 | |
|
||||||
|
| 基礎資料頁籤 | 已實現 | 15 個欄位 |
|
||||||
|
| 招聘要求頁籤 | 已實現 | 18 個欄位 |
|
||||||
|
| 組織層級聯動 | 已實現 | BU → 處 → 部 → 課 |
|
||||||
|
|
||||||
|
**確認問題**:
|
||||||
|
- [ ] 崗位編號格式是否需要強制規範?(目前接受任意格式)
|
||||||
|
- [ ] 是否需要崗位編號自動生成功能?
|
||||||
|
- [ ] 編制人數是否有上限限制?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.2 職務基礎資料模組
|
||||||
|
|
||||||
|
| 功能項目 | 狀態 | 備註 |
|
||||||
|
|----------|------|------|
|
||||||
|
| 新增職務 | 已實現 | |
|
||||||
|
| 編輯職務 | 已實現 | |
|
||||||
|
| 刪除職務 | 已實現 | |
|
||||||
|
| 職務類別聯動 | 已實現 | 6 種類別 |
|
||||||
|
| Toggle 開關 | 已實現 | 全勤獎金、住房補貼 |
|
||||||
|
| 職務層級(保密欄位) | 已實現 | |
|
||||||
|
|
||||||
|
**確認問題**:
|
||||||
|
- [ ] 職務類別是否需要擴充?
|
||||||
|
- [ ] 保密欄位的顯示邏輯是否正確?
|
||||||
|
- [ ] 是否需要職務與崗位的關聯功能?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.3 部門職責模組
|
||||||
|
|
||||||
|
| 功能項目 | 狀態 | 備註 |
|
||||||
|
|----------|------|------|
|
||||||
|
| 新增部門職責 | 已實現 | |
|
||||||
|
| 編輯部門職責 | 已實現 | |
|
||||||
|
| 刪除部門職責 | 已實現 | |
|
||||||
|
| 組織層級聯動 | 已實現 | |
|
||||||
|
| 使命願景維護 | 已實現 | |
|
||||||
|
| 核心職責維護 | 已實現 | |
|
||||||
|
| KPI 維護 | 已實現 | |
|
||||||
|
|
||||||
|
**確認問題**:
|
||||||
|
- [ ] 部門職責是否需要與崗位建立關聯?
|
||||||
|
- [ ] 是否需要審核流程?
|
||||||
|
- [ ] 狀態變更是否需要記錄歷史?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.4 崗位描述模組
|
||||||
|
|
||||||
|
| 功能項目 | 狀態 | 備註 |
|
||||||
|
|----------|------|------|
|
||||||
|
| 新增崗位描述 | 已實現 | |
|
||||||
|
| 編輯崗位描述 | 已實現 | |
|
||||||
|
| 版本管理 | 部分實現 | 僅記錄版本日期 |
|
||||||
|
| 主要職責清單 | 已實現 | 編號格式 |
|
||||||
|
| 崗位要求維護 | 已實現 | |
|
||||||
|
|
||||||
|
**確認問題**:
|
||||||
|
- [ ] 是否需要完整版本歷史記錄?
|
||||||
|
- [ ] 崗位描述是否需要審核流程?
|
||||||
|
- [ ] 是否需要匯出為 PDF/Word 格式?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.5 三個錦囊 AI 功能
|
||||||
|
|
||||||
|
| 功能項目 | 狀態 | 備註 |
|
||||||
|
|----------|------|------|
|
||||||
|
| 自動補齊 | 已實現 | 根據已填內容生成 |
|
||||||
|
| 範例模板 | 已實現 | 生成範例資料 |
|
||||||
|
| 驗證檢查 | 已實現 | 檢查資料完整性 |
|
||||||
|
| Prompt 編輯 | 已實現 | 可自訂 Prompt |
|
||||||
|
| 多 LLM 支援 | 已實現 | Ollama, Gemini, DeepSeek, OpenAI |
|
||||||
|
|
||||||
|
**確認問題**:
|
||||||
|
- [ ] 預設 Prompt 是否符合需求?
|
||||||
|
- [ ] 是否需要限制 LLM 使用次數?
|
||||||
|
- [ ] 是否需要記錄 AI 生成歷史?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.6 崗位清單與匯出
|
||||||
|
|
||||||
|
| 功能項目 | 狀態 | 備註 |
|
||||||
|
|----------|------|------|
|
||||||
|
| 崗位清單顯示 | 已實現 | |
|
||||||
|
| 搜尋過濾 | 已實現 | |
|
||||||
|
| 檢視描述 | 已實現 | |
|
||||||
|
| 匯出 CSV | 已實現 | |
|
||||||
|
| 下載 CSV 範本 | 已實現 | 崗位、職務 |
|
||||||
|
| 匯入 CSV | 已實現 | 崗位、職務 |
|
||||||
|
|
||||||
|
**確認問題**:
|
||||||
|
- [ ] 是否需要 Excel 格式支援?
|
||||||
|
- [ ] 匯出時是否需要選擇欄位?
|
||||||
|
- [ ] 批次匯入的錯誤處理是否符合需求?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 技術規格確認
|
||||||
|
|
||||||
|
### 3.1 前端技術
|
||||||
|
|
||||||
|
| 項目 | 當前實現 | 確認狀態 |
|
||||||
|
|------|----------|----------|
|
||||||
|
| 框架 | 純 HTML5/CSS3/ES6 JavaScript | |
|
||||||
|
| 模組化 | ES6 Modules (import/export) | |
|
||||||
|
| 樣式 | CSS Modules | |
|
||||||
|
| UI 元件 | 自訂元件 | |
|
||||||
|
|
||||||
|
**確認問題**:
|
||||||
|
- [ ] 是否考慮使用前端框架(Vue/React)?
|
||||||
|
- [ ] 是否需要 TypeScript 支援?
|
||||||
|
|
||||||
|
### 3.2 後端技術
|
||||||
|
|
||||||
|
| 項目 | 當前實現 | 確認狀態 |
|
||||||
|
|------|----------|----------|
|
||||||
|
| 框架 | Python Flask | |
|
||||||
|
| API 風格 | RESTful | |
|
||||||
|
| 資料庫 | In-Memory (Dict) | |
|
||||||
|
| LLM 服務 | 多 LLM 支援 | |
|
||||||
|
|
||||||
|
**確認問題**:
|
||||||
|
- [ ] 是否需要遷移至正式資料庫(MySQL/PostgreSQL)?
|
||||||
|
- [ ] 是否需要用戶認證功能?
|
||||||
|
- [ ] 是否需要與現有 HR 系統整合?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 欄位規格確認
|
||||||
|
|
||||||
|
### 4.1 標準化 HTML ID
|
||||||
|
|
||||||
|
系統已規劃欄位 ID 標準化,詳見「更新欄位名稱.md」。
|
||||||
|
|
||||||
|
| 模組 | 前綴 | 欄位數 | 狀態 |
|
||||||
|
|------|------|--------|------|
|
||||||
|
| 崗位基礎資料 | pos_ | 15 | 待實施 |
|
||||||
|
| 招聘要求 | rec_ | 18 | 待實施 |
|
||||||
|
| 職務基礎資料 | job_ | 12 | 待實施 |
|
||||||
|
| 部門職責 | df_ | 18 | 待實施 |
|
||||||
|
| 崗位描述 | jd_ | 16 | 待實施 |
|
||||||
|
|
||||||
|
**確認問題**:
|
||||||
|
- [ ] 是否同意欄位 ID 標準化方案?
|
||||||
|
- [ ] 重命名是否會影響現有資料?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 使用者介面確認
|
||||||
|
|
||||||
|
### 5.1 視覺設計
|
||||||
|
|
||||||
|
| 項目 | 當前狀態 | 確認 |
|
||||||
|
|------|----------|------|
|
||||||
|
| 主色調 | 藍色系 (#1a5276) | |
|
||||||
|
| 字型 | Noto Sans TC | |
|
||||||
|
| 響應式設計 | 支援手機/平板 | |
|
||||||
|
| 深色模式 | 未實現 | |
|
||||||
|
|
||||||
|
**確認問題**:
|
||||||
|
- [ ] 是否需要深色模式?
|
||||||
|
- [ ] 是否需要自訂主題色?
|
||||||
|
|
||||||
|
### 5.2 操作流程
|
||||||
|
|
||||||
|
| 流程 | 當前實現 | 確認 |
|
||||||
|
|------|----------|------|
|
||||||
|
| 新增 → 保存 | 一步完成 | |
|
||||||
|
| 保存並新增 | 支援 | |
|
||||||
|
| 刪除確認 | 彈窗確認 | |
|
||||||
|
| 快捷鍵 | Ctrl+S, Ctrl+N | |
|
||||||
|
|
||||||
|
**確認問題**:
|
||||||
|
- [ ] 是否需要草稿保存功能?
|
||||||
|
- [ ] 是否需要操作歷史(Undo/Redo)?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 待確認事項清單
|
||||||
|
|
||||||
|
### 6.1 優先級高
|
||||||
|
|
||||||
|
| # | 事項 | 負責人 | 截止日期 |
|
||||||
|
|---|------|--------|----------|
|
||||||
|
| 1 | 確認是否需要用戶認證功能 | | |
|
||||||
|
| 2 | 確認資料庫選型(In-Memory vs MySQL) | | |
|
||||||
|
| 3 | 確認欄位 ID 標準化方案 | | |
|
||||||
|
| 4 | 確認 AI 功能的使用政策 | | |
|
||||||
|
|
||||||
|
### 6.2 優先級中
|
||||||
|
|
||||||
|
| # | 事項 | 負責人 | 截止日期 |
|
||||||
|
|---|------|--------|----------|
|
||||||
|
| 5 | 確認 Excel 匯出需求 | | |
|
||||||
|
| 6 | 確認審核流程需求 | | |
|
||||||
|
| 7 | 確認與現有系統整合需求 | | |
|
||||||
|
|
||||||
|
### 6.3 優先級低
|
||||||
|
|
||||||
|
| # | 事項 | 負責人 | 截止日期 |
|
||||||
|
|---|------|--------|----------|
|
||||||
|
| 8 | 確認深色模式需求 | | |
|
||||||
|
| 9 | 確認多語言支援需求 | | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 簽核記錄
|
||||||
|
|
||||||
|
| 角色 | 姓名 | 簽核日期 | 簽核狀態 |
|
||||||
|
|------|------|----------|----------|
|
||||||
|
| 專案負責人 | | | 待簽核 |
|
||||||
|
| 業務代表 | | | 待簽核 |
|
||||||
|
| 技術負責人 | | | 待簽核 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 附錄
|
||||||
|
|
||||||
|
### 附錄 A:相關文件
|
||||||
|
|
||||||
|
- SDD.md - 軟體設計文件
|
||||||
|
- TDD.md - 測試設計文件
|
||||||
|
- 更新欄位名稱.md - 欄位規範書
|
||||||
|
- ID重命名對照表.md - 欄位 ID 對照表
|
||||||
|
|
||||||
|
### 附錄 B:系統截圖
|
||||||
|
|
||||||
|
(請參考系統實際畫面)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**文件結束**
|
||||||
97
hierarchy_test.json
Normal file
97
hierarchy_test.json
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
{
|
||||||
|
"stats": {
|
||||||
|
"business_units": 15,
|
||||||
|
"divisions": 40,
|
||||||
|
"departments": 83,
|
||||||
|
"organization_positions": 237
|
||||||
|
},
|
||||||
|
"business_units": [
|
||||||
|
"半導體事業群",
|
||||||
|
"汽車事業體",
|
||||||
|
"法務室",
|
||||||
|
"岡山製造事業體",
|
||||||
|
"產品事業體",
|
||||||
|
"晶圓三廠",
|
||||||
|
"集團人資行政事業體",
|
||||||
|
"集團財務事業體",
|
||||||
|
"集團會計事業體",
|
||||||
|
"集團資訊事業體",
|
||||||
|
"新創事業體",
|
||||||
|
"稽核室",
|
||||||
|
"總經理室",
|
||||||
|
"總品質事業體",
|
||||||
|
"營業事業體"
|
||||||
|
],
|
||||||
|
"businessToDivision": {
|
||||||
|
"半導體事業群": [
|
||||||
|
"半導體事業群"
|
||||||
|
],
|
||||||
|
"汽車事業體": [
|
||||||
|
"汽車事業體"
|
||||||
|
],
|
||||||
|
"法務室": [
|
||||||
|
"法務室"
|
||||||
|
],
|
||||||
|
"岡山製造事業體": [
|
||||||
|
"生產處",
|
||||||
|
"岡山製造事業體",
|
||||||
|
"封裝工程處",
|
||||||
|
"副總辦公室",
|
||||||
|
"測試工程與研發處",
|
||||||
|
"資材處",
|
||||||
|
"廠務與環安衛管理處"
|
||||||
|
],
|
||||||
|
"產品事業體": [
|
||||||
|
"產品事業體",
|
||||||
|
"先進產品事業處",
|
||||||
|
"成熟產品事業處"
|
||||||
|
],
|
||||||
|
"晶圓三廠": [
|
||||||
|
"晶圓三廠",
|
||||||
|
"製程工程處"
|
||||||
|
],
|
||||||
|
"集團人資行政事業體": [
|
||||||
|
"集團人資行政事業體"
|
||||||
|
],
|
||||||
|
"集團財務事業體": [
|
||||||
|
"集團財務事業體",
|
||||||
|
"岡山強茂財務處"
|
||||||
|
],
|
||||||
|
"集團會計事業體": [
|
||||||
|
"集團會計事業體",
|
||||||
|
"岡山會計處",
|
||||||
|
"集團會計處"
|
||||||
|
],
|
||||||
|
"集團資訊事業體": [
|
||||||
|
"集團資訊事業體",
|
||||||
|
"資安行動小組",
|
||||||
|
"資訊一處",
|
||||||
|
"資訊二處"
|
||||||
|
],
|
||||||
|
"新創事業體": [
|
||||||
|
"新創事業體",
|
||||||
|
"中低壓產品研發處",
|
||||||
|
"研發中心",
|
||||||
|
"高壓產品研發處"
|
||||||
|
],
|
||||||
|
"稽核室": [
|
||||||
|
"稽核室"
|
||||||
|
],
|
||||||
|
"總經理室": [
|
||||||
|
"總經理室",
|
||||||
|
"ESG專案辦公室",
|
||||||
|
"專案管理室"
|
||||||
|
],
|
||||||
|
"總品質事業體": [
|
||||||
|
"總品質事業體"
|
||||||
|
],
|
||||||
|
"營業事業體": [
|
||||||
|
"營業事業體",
|
||||||
|
"商業開發暨市場應用處",
|
||||||
|
"海外銷售事業處",
|
||||||
|
"全球技術服務處",
|
||||||
|
"全球行銷暨業務支援處",
|
||||||
|
"大中華區銷售事業處"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
349
import_hierarchy_data.py
Normal file
349
import_hierarchy_data.py
Normal file
@@ -0,0 +1,349 @@
|
|||||||
|
"""
|
||||||
|
組織階層資料匯入模組
|
||||||
|
從 Markdown 表格解析組織階層資料,供 Flask API 使用
|
||||||
|
"""
|
||||||
|
|
||||||
|
def import_to_memory():
|
||||||
|
"""
|
||||||
|
解析組織階層資料並返回結構化資料
|
||||||
|
Returns: dict 包含所有階層資料
|
||||||
|
"""
|
||||||
|
# 原始資料(從 excel_table copy.md 提取,共 313 筆記錄)
|
||||||
|
raw_data = [
|
||||||
|
("半導體事業群", "半導體事業群", "", "營運長"),
|
||||||
|
("半導體事業群", "半導體事業群", "", "營運長助理"),
|
||||||
|
("汽車事業體", "汽車事業體", "", "副總經理"),
|
||||||
|
("汽車事業體", "汽車事業體", "", "專案經理"),
|
||||||
|
("法務室", "法務室", "", "經副理"),
|
||||||
|
("法務室", "法務室", "", "法務專員"),
|
||||||
|
("法務室", "法務室", "", "專利工程師"),
|
||||||
|
("岡山製造事業體", "生產處", "", "處長"),
|
||||||
|
("岡山製造事業體", "生產處", "", "專員"),
|
||||||
|
("岡山製造事業體", "生產處", "生產部", "經副理"),
|
||||||
|
("岡山製造事業體", "生產處", "生產部", "課長"),
|
||||||
|
("岡山製造事業體", "生產處", "生產部", "組長"),
|
||||||
|
("岡山製造事業體", "生產處", "生產部", "班長"),
|
||||||
|
("岡山製造事業體", "生產處", "生產部", "副班長"),
|
||||||
|
("岡山製造事業體", "生產處", "生產部", "作業員"),
|
||||||
|
("岡山製造事業體", "生產處", "生產企劃部", "經副理"),
|
||||||
|
("岡山製造事業體", "生產處", "生產企劃部", "課長"),
|
||||||
|
("岡山製造事業體", "生產處", "生產企劃部", "專員"),
|
||||||
|
("岡山製造事業體", "生產處", "生產企劃部", "工程師"),
|
||||||
|
("岡山製造事業體", "岡山製造事業體", "岡山品質管制部", "經副理"),
|
||||||
|
("岡山製造事業體", "岡山製造事業體", "岡山品質管制部", "課長"),
|
||||||
|
("岡山製造事業體", "岡山製造事業體", "岡山品質管制部", "工程師"),
|
||||||
|
("岡山製造事業體", "岡山製造事業體", "岡山品質管制部", "組長"),
|
||||||
|
("岡山製造事業體", "岡山製造事業體", "岡山品質管制部", "班長"),
|
||||||
|
("岡山製造事業體", "岡山製造事業體", "岡山品質管制部", "副班長"),
|
||||||
|
("岡山製造事業體", "岡山製造事業體", "岡山品質管制部", "作業員"),
|
||||||
|
("岡山製造事業體", "岡山製造事業體", "岡山品質管制部", "副總經理"),
|
||||||
|
("岡山製造事業體", "岡山製造事業體", "岡山品質管制部", "副總經理助理"),
|
||||||
|
("岡山製造事業體", "封裝工程處", "", "處長"),
|
||||||
|
("岡山製造事業體", "封裝工程處", "", "專員"),
|
||||||
|
("岡山製造事業體", "封裝工程處", "", "工程師"),
|
||||||
|
("岡山製造事業體", "封裝工程處", "製程工程一部", "經副理"),
|
||||||
|
("岡山製造事業體", "封裝工程處", "製程工程二部", "經副理"),
|
||||||
|
("岡山製造事業體", "封裝工程處", "製程工程二部", "課長"),
|
||||||
|
("岡山製造事業體", "封裝工程處", "製程工程二部", "工程師"),
|
||||||
|
("岡山製造事業體", "封裝工程處", "設備一部", "經副理"),
|
||||||
|
("岡山製造事業體", "封裝工程處", "設備二部", "經副理"),
|
||||||
|
("岡山製造事業體", "封裝工程處", "設備二部", "課長"),
|
||||||
|
("岡山製造事業體", "封裝工程處", "設備二部", "工程師"),
|
||||||
|
("岡山製造事業體", "副總辦公室", "工業工程部", "經副理"),
|
||||||
|
("岡山製造事業體", "副總辦公室", "工業工程部", "工程師"),
|
||||||
|
("岡山製造事業體", "副總辦公室", "工業工程部", "課長"),
|
||||||
|
("岡山製造事業體", "副總辦公室", "工業工程部", "副理"),
|
||||||
|
("岡山製造事業體", "測試工程與研發處", "", "處長"),
|
||||||
|
("岡山製造事業體", "測試工程與研發處", "", "專員"),
|
||||||
|
("岡山製造事業體", "測試工程與研發處", "測試工程部", "經副理"),
|
||||||
|
("岡山製造事業體", "測試工程與研發處", "測試工程部", "課長"),
|
||||||
|
("岡山製造事業體", "測試工程與研發處", "測試工程部", "工程師"),
|
||||||
|
("岡山製造事業體", "測試工程與研發處", "新產品導入部", "經副理"),
|
||||||
|
("岡山製造事業體", "測試工程與研發處", "新產品導入部", "專員"),
|
||||||
|
("岡山製造事業體", "測試工程與研發處", "新產品導入部", "工程師"),
|
||||||
|
("岡山製造事業體", "測試工程與研發處", "研發部", "經副理"),
|
||||||
|
("岡山製造事業體", "測試工程與研發處", "研發部", "課長"),
|
||||||
|
("岡山製造事業體", "測試工程與研發處", "研發部", "工程師"),
|
||||||
|
("岡山製造事業體", "測試工程與研發處", "研發部", "專員"),
|
||||||
|
("岡山製造事業體", "資材處", "", "處長"),
|
||||||
|
("岡山製造事業體", "資材處", "採購部", "經副理"),
|
||||||
|
("岡山製造事業體", "資材處", "採購部", "課長"),
|
||||||
|
("岡山製造事業體", "資材處", "採購部", "專員"),
|
||||||
|
("岡山製造事業體", "資材處", "外部資源部", "專員"),
|
||||||
|
("岡山製造事業體", "資材處", "生管部", "經副理"),
|
||||||
|
("岡山製造事業體", "資材處", "生管部", "課長"),
|
||||||
|
("岡山製造事業體", "資材處", "生管部", "專員"),
|
||||||
|
("岡山製造事業體", "資材處", "生管部", "班長"),
|
||||||
|
("岡山製造事業體", "資材處", "生管部", "副班長"),
|
||||||
|
("岡山製造事業體", "資材處", "生管部", "作業員"),
|
||||||
|
("岡山製造事業體", "資材處", "原物料控制部", "經副理"),
|
||||||
|
("岡山製造事業體", "資材處", "原物料控制部", "課長"),
|
||||||
|
("岡山製造事業體", "資材處", "原物料控制部", "專員"),
|
||||||
|
("岡山製造事業體", "資材處", "原物料控制部", "班長"),
|
||||||
|
("岡山製造事業體", "資材處", "原物料控制部", "副班長"),
|
||||||
|
("岡山製造事業體", "資材處", "原物料控制部", "作業員"),
|
||||||
|
("岡山製造事業體", "廠務與環安衛管理處", "", "處長"),
|
||||||
|
("岡山製造事業體", "廠務與環安衛管理處", "", "工程師"),
|
||||||
|
("岡山製造事業體", "廠務與環安衛管理處", "廠務部", "經副理"),
|
||||||
|
("岡山製造事業體", "廠務與環安衛管理處", "廠務部", "課長"),
|
||||||
|
("岡山製造事業體", "廠務與環安衛管理處", "廠務部", "工程師"),
|
||||||
|
("岡山製造事業體", "廠務與環安衛管理處", "廠務部", "專員"),
|
||||||
|
("產品事業體", "產品事業體", "廠務部", "處長"),
|
||||||
|
("產品事業體", "先進產品事業處", "", "處長"),
|
||||||
|
("產品事業體", "先進產品事業處", "產品管理部(APD)", "經副理"),
|
||||||
|
("產品事業體", "先進產品事業處", "產品管理部(APD)", "工程師"),
|
||||||
|
("產品事業體", "成熟產品事業處", "", "處長"),
|
||||||
|
("產品事業體", "成熟產品事業處", "產品管理部(MPD)", "經副理"),
|
||||||
|
("產品事業體", "成熟產品事業處", "產品管理部(MPD)", "專案經副理"),
|
||||||
|
("產品事業體", "成熟產品事業處", "產品管理部(MPD)", "工程師"),
|
||||||
|
("晶圓三廠", "晶圓三廠", "產品管理部(MPD)", "顧問"),
|
||||||
|
("晶圓三廠", "晶圓三廠", "產品管理部(MPD)", "專員"),
|
||||||
|
("晶圓三廠", "晶圓三廠", "品質部", "經副理"),
|
||||||
|
("晶圓三廠", "晶圓三廠", "品質部", "工程師"),
|
||||||
|
("晶圓三廠", "晶圓三廠", "品質部", "作業員"),
|
||||||
|
("晶圓三廠", "晶圓三廠", "製造部", "經副理"),
|
||||||
|
("晶圓三廠", "晶圓三廠", "製造部", "課長"),
|
||||||
|
("晶圓三廠", "晶圓三廠", "製造部", "班長"),
|
||||||
|
("晶圓三廠", "晶圓三廠", "製造部", "副班長"),
|
||||||
|
("晶圓三廠", "晶圓三廠", "製造部", "作業員"),
|
||||||
|
("晶圓三廠", "晶圓三廠", "廠務部(Fab3)", "經副理"),
|
||||||
|
("晶圓三廠", "晶圓三廠", "廠務部(Fab3)", "工程師"),
|
||||||
|
("晶圓三廠", "製程工程處", "工程一部", "經副理"),
|
||||||
|
("晶圓三廠", "製程工程處", "工程一部", "工程師"),
|
||||||
|
("晶圓三廠", "製程工程處", "工程二部", "經副理"),
|
||||||
|
("晶圓三廠", "製程工程處", "工程二部", "工程師"),
|
||||||
|
("晶圓三廠", "製程工程處", "工程三部", "經副理"),
|
||||||
|
("晶圓三廠", "製程工程處", "工程三部", "工程師"),
|
||||||
|
("晶圓三廠", "製程工程處", "製程整合部(Fab3)", "經副理"),
|
||||||
|
("晶圓三廠", "製程工程處", "製程整合部(Fab3)", "工程師"),
|
||||||
|
("集團人資行政事業體", "集團人資行政事業體", "製程整合部(Fab3)", "人資長"),
|
||||||
|
("集團人資行政事業體", "集團人資行政事業體", "行政總務管理部", "經副理"),
|
||||||
|
("集團人資行政事業體", "集團人資行政事業體", "行政總務管理部", "專員"),
|
||||||
|
("集團人資行政事業體", "集團人資行政事業體", "行政總務管理部", "助理"),
|
||||||
|
("集團人資行政事業體", "集團人資行政事業體", "招募任用部", "經副理"),
|
||||||
|
("集團人資行政事業體", "集團人資行政事業體", "招募任用部", "專員"),
|
||||||
|
("集團人資行政事業體", "集團人資行政事業體", "訓練發展部", "經副理"),
|
||||||
|
("集團人資行政事業體", "集團人資行政事業體", "訓練發展部", "專員"),
|
||||||
|
("集團人資行政事業體", "集團人資行政事業體", "薪酬管理部", "經副理"),
|
||||||
|
("集團人資行政事業體", "集團人資行政事業體", "薪酬管理部", "專員"),
|
||||||
|
("集團財務事業體", "集團財務事業體", "薪酬管理部", "財務長"),
|
||||||
|
("集團財務事業體", "岡山強茂財務處", "", "處長"),
|
||||||
|
("集團財務事業體", "岡山強茂財務處", "岡山強茂財務部", "經副理"),
|
||||||
|
("集團財務事業體", "岡山強茂財務處", "岡山強茂財務部", "課長"),
|
||||||
|
("集團財務事業體", "岡山強茂財務處", "岡山強茂財務部", "專員"),
|
||||||
|
("集團財務事業體", "集團財務事業體", "岡山強茂財務部", "專案副理"),
|
||||||
|
("集團會計事業體", "集團會計事業體", "岡山強茂財務部", "會計長"),
|
||||||
|
("集團會計事業體", "岡山會計處", "", "處長"),
|
||||||
|
("集團會計事業體", "岡山會計處", "會計部", "經副理"),
|
||||||
|
("集團會計事業體", "岡山會計處", "會計部", "課長"),
|
||||||
|
("集團會計事業體", "岡山會計處", "會計部", "專員"),
|
||||||
|
("集團會計事業體", "岡山會計處", "管理會計部", "經副理"),
|
||||||
|
("集團會計事業體", "岡山會計處", "管理會計部", "課長"),
|
||||||
|
("集團會計事業體", "岡山會計處", "管理會計部", "專員"),
|
||||||
|
("集團會計事業體", "集團會計處", "", "處長"),
|
||||||
|
("集團會計事業體", "集團會計處", "集團合併報表部", "經副理"),
|
||||||
|
("集團會計事業體", "集團會計處", "集團合併報表部", "專員"),
|
||||||
|
("集團資訊事業體", "集團資訊事業體", "集團合併報表部", "資訊長"),
|
||||||
|
("集團資訊事業體", "資安行動小組", "集團合併報表部", "課長"),
|
||||||
|
("集團資訊事業體", "資訊一處", "應用系統部", "經副理"),
|
||||||
|
("集團資訊事業體", "資訊一處", "應用系統部", "工程師"),
|
||||||
|
("集團資訊事業體", "資訊一處", "電腦整合製造部", "經副理"),
|
||||||
|
("集團資訊事業體", "資訊一處", "電腦整合製造部", "工程師"),
|
||||||
|
("集團資訊事業體", "資訊一處", "系統網路服務部", "經副理"),
|
||||||
|
("集團資訊事業體", "資訊一處", "系統網路服務部", "工程師"),
|
||||||
|
("集團資訊事業體", "資訊二處", "", "處長"),
|
||||||
|
("新創事業體", "新創事業體", "", "處長"),
|
||||||
|
("新創事業體", "新創事業體", "資源管理部", "經副理"),
|
||||||
|
("新創事業體", "新創事業體", "資源管理部", "專員"),
|
||||||
|
("新創事業體", "中低壓產品研發處", "", "經副理"),
|
||||||
|
("新創事業體", "研發中心", "", "工程師"),
|
||||||
|
("新創事業體", "高壓產品研發處", "", "經副理"),
|
||||||
|
("新創事業體", "研發中心", "", "工程師"),
|
||||||
|
("稽核室", "稽核室", "", "主任"),
|
||||||
|
("稽核室", "稽核室", "", "專員"),
|
||||||
|
("總經理室", "總經理室", "", "總裁"),
|
||||||
|
("總經理室", "總經理室", "", "總經理"),
|
||||||
|
("總經理室", "ESG專案辦公室", "", "經副理"),
|
||||||
|
("總經理室", "ESG專案辦公室", "", "課長"),
|
||||||
|
("總經理室", "ESG專案辦公室", "", "專員/工程師"),
|
||||||
|
("總經理室", "專案管理室", "", "副總經理"),
|
||||||
|
("總經理室", "專案管理室", "", "經副理"),
|
||||||
|
("總經理室", "專案管理室", "", "專員/工程師"),
|
||||||
|
("總品質事業體", "總品質事業體", "", "處長"),
|
||||||
|
("總品質事業體", "總品質事業體", "客戶品質管理部", "經副理"),
|
||||||
|
("總品質事業體", "總品質事業體", "客戶品質管理部", "課長"),
|
||||||
|
("總品質事業體", "總品質事業體", "客戶品質管理部", "工程師"),
|
||||||
|
("總品質事業體", "總品質事業體", "客戶品質管理部", "專員"),
|
||||||
|
("總品質事業體", "總品質事業體", "產品品質管理部", "經副理"),
|
||||||
|
("總品質事業體", "總品質事業體", "產品品質管理部", "課長"),
|
||||||
|
("總品質事業體", "總品質事業體", "產品品質管理部", "工程師"),
|
||||||
|
("總品質事業體", "總品質事業體", "產品品質管理部", "班長"),
|
||||||
|
("總品質事業體", "總品質事業體", "產品品質管理部", "作業員"),
|
||||||
|
("總品質事業體", "總品質事業體", "品質系統及客戶工程整合部", "經副理"),
|
||||||
|
("總品質事業體", "總品質事業體", "品質系統及客戶工程整合部", "課長"),
|
||||||
|
("總品質事業體", "總品質事業體", "品質系統及客戶工程整合部", "工程師"),
|
||||||
|
("總品質事業體", "總品質事業體", "封測外包品質管理部", "經副理"),
|
||||||
|
("總品質事業體", "總品質事業體", "封測外包品質管理部", "課長"),
|
||||||
|
("總品質事業體", "總品質事業體", "封測外包品質管理部", "工程師"),
|
||||||
|
("總品質事業體", "總品質事業體", "品質保證部", "經副理"),
|
||||||
|
("總品質事業體", "總品質事業體", "品質保證部", "課長"),
|
||||||
|
("總品質事業體", "總品質事業體", "品質保證部", "工程師"),
|
||||||
|
("總品質事業體", "總品質事業體", "品質保證部", "班長"),
|
||||||
|
("總品質事業體", "總品質事業體", "品質保證部", "副班長"),
|
||||||
|
("總品質事業體", "總品質事業體", "品質保證部", "作業員"),
|
||||||
|
("營業事業體", "營業事業體", "品質保證部", "副總經理"),
|
||||||
|
("營業事業體", "營業事業體", "品質保證部", "副總經理助理"),
|
||||||
|
("營業事業體", "商業開發暨市場應用處", "", "處長"),
|
||||||
|
("營業事業體", "商業開發暨市場應用處", "", "經理"),
|
||||||
|
("營業事業體", "商業開發暨市場應用處", "", "工程師"),
|
||||||
|
("營業事業體", "海外銷售事業處", "", "處長"),
|
||||||
|
("營業事業體", "海外銷售事業處", "日本區暨代工業務部", "經副理"),
|
||||||
|
("營業事業體", "海外銷售事業處", "日本區暨代工業務部", "課長"),
|
||||||
|
("營業事業體", "海外銷售事業處", "日本區暨代工業務部", "專員"),
|
||||||
|
("營業事業體", "海外銷售事業處", "日本區暨代工業務部", "助理"),
|
||||||
|
("營業事業體", "海外銷售事業處", "歐亞區業務部", "經副理"),
|
||||||
|
("營業事業體", "海外銷售事業處", "歐亞區業務部", "課長"),
|
||||||
|
("營業事業體", "海外銷售事業處", "歐亞區業務部", "專員"),
|
||||||
|
("營業事業體", "海外銷售事業處", "歐亞區業務部", "助理"),
|
||||||
|
("營業事業體", "海外銷售事業處", "韓國區業務部-韓國區", "經副理"),
|
||||||
|
("營業事業體", "海外銷售事業處", "韓國區業務部-韓國區", "課長"),
|
||||||
|
("營業事業體", "海外銷售事業處", "韓國區業務部-韓國區", "專員"),
|
||||||
|
("營業事業體", "海外銷售事業處", "韓國區業務部-韓國區", "助理"),
|
||||||
|
("營業事業體", "海外銷售事業處", "韓國區業務部-韓國區", "專案經理"),
|
||||||
|
("營業事業體", "海外銷售事業處", "美洲區業務部", "經副理"),
|
||||||
|
("營業事業體", "海外銷售事業處", "美洲區業務部", "課長"),
|
||||||
|
("營業事業體", "海外銷售事業處", "美洲區業務部", "專員"),
|
||||||
|
("營業事業體", "海外銷售事業處", "美洲區業務部", "助理"),
|
||||||
|
("營業事業體", "全球技術服務處", "", "處長"),
|
||||||
|
("營業事業體", "全球技術服務處", "", "工程師"),
|
||||||
|
("營業事業體", "全球技術服務處", "", "助理"),
|
||||||
|
("營業事業體", "全球技術服務處", "應用工程部(GTS)", "經副理"),
|
||||||
|
("營業事業體", "全球技術服務處", "應用工程部(GTS)", "專案經副理"),
|
||||||
|
("營業事業體", "全球技術服務處", "應用工程部(GTS)", "技術經副理"),
|
||||||
|
("營業事業體", "全球技術服務處", "應用工程部(GTS)", "工程師"),
|
||||||
|
("營業事業體", "全球技術服務處", "系統工程部", "經副理"),
|
||||||
|
("營業事業體", "全球技術服務處", "系統工程部", "工程師"),
|
||||||
|
("營業事業體", "全球技術服務處", "特性測試部", "經副理"),
|
||||||
|
("營業事業體", "全球技術服務處", "特性測試部", "課長"),
|
||||||
|
("營業事業體", "全球技術服務處", "特性測試部", "工程師"),
|
||||||
|
("營業事業體", "全球行銷暨業務支援處", "", "副總經理"),
|
||||||
|
("營業事業體", "全球行銷暨業務支援處", "業務生管部", "經副理"),
|
||||||
|
("營業事業體", "全球行銷暨業務支援處", "業務生管部", "課長"),
|
||||||
|
("營業事業體", "全球行銷暨業務支援處", "業務生管部", "專員"),
|
||||||
|
("營業事業體", "全球行銷暨業務支援處", "市場行銷企劃部", "處長"),
|
||||||
|
("營業事業體", "全球行銷暨業務支援處", "市場行銷企劃部", "經理"),
|
||||||
|
("營業事業體", "全球行銷暨業務支援處", "市場行銷企劃部", "專員"),
|
||||||
|
("營業事業體", "全球行銷暨業務支援處", "MOSFET晶圓採購部", "經副理"),
|
||||||
|
("營業事業體", "全球行銷暨業務支援處", "MOSFET晶圓採購部", "課長"),
|
||||||
|
("營業事業體", "全球行銷暨業務支援處", "MOSFET晶圓採購部", "專員"),
|
||||||
|
("營業事業體", "大中華區銷售事業處", "", "處長"),
|
||||||
|
("營業事業體", "大中華區銷售事業處", "台灣區業務部", "專員"),
|
||||||
|
("營業事業體", "大中華區銷售事業處", "台灣區業務部", "助理"),
|
||||||
|
("營業事業體", "大中華區銷售事業處", "業務一部", "處長/資深經理"),
|
||||||
|
("營業事業體", "大中華區銷售事業處", "業務一部", "經副理"),
|
||||||
|
("營業事業體", "大中華區銷售事業處", "業務一部", "專員"),
|
||||||
|
("營業事業體", "大中華區銷售事業處", "業務一部", "助理"),
|
||||||
|
("營業事業體", "大中華區銷售事業處", "業務二部", "處長/資深經理"),
|
||||||
|
("營業事業體", "大中華區銷售事業處", "業務二部", "經副理"),
|
||||||
|
("營業事業體", "大中華區銷售事業處", "業務二部", "專員"),
|
||||||
|
("營業事業體", "大中華區銷售事業處", "業務二部", "助理"),
|
||||||
|
]
|
||||||
|
|
||||||
|
# 建立結構化資料
|
||||||
|
HR_position_business_units = {} # 事業體
|
||||||
|
HR_position_divisions = {} # 處級單位
|
||||||
|
HR_position_departments = {} # 部級單位
|
||||||
|
HR_position_organization_positions = [] # 完整組織崗位關聯
|
||||||
|
|
||||||
|
# 關聯映射
|
||||||
|
businessToDivision = {} # 事業體 -> 處級單位列表
|
||||||
|
divisionToDepartment = {} # 處級單位 -> 部級單位列表
|
||||||
|
departmentToPosition = {} # 部級單位 -> 崗位列表
|
||||||
|
|
||||||
|
business_id = 1
|
||||||
|
division_id = 1
|
||||||
|
department_id = 1
|
||||||
|
position_id = 1
|
||||||
|
|
||||||
|
for business, division, department, position in raw_data:
|
||||||
|
# 處理事業體
|
||||||
|
if business and business not in HR_position_business_units:
|
||||||
|
HR_position_business_units[business] = {
|
||||||
|
'id': business_id,
|
||||||
|
'code': f'BU{str(business_id).zfill(3)}',
|
||||||
|
'name': business
|
||||||
|
}
|
||||||
|
business_id += 1
|
||||||
|
businessToDivision[business] = []
|
||||||
|
|
||||||
|
# 處理處級單位
|
||||||
|
if division and division not in HR_position_divisions:
|
||||||
|
HR_position_divisions[division] = {
|
||||||
|
'id': division_id,
|
||||||
|
'code': f'DIV{str(division_id).zfill(3)}',
|
||||||
|
'name': division,
|
||||||
|
'business': business
|
||||||
|
}
|
||||||
|
division_id += 1
|
||||||
|
divisionToDepartment[division] = []
|
||||||
|
|
||||||
|
# 建立事業體到處級的關聯
|
||||||
|
if business and division and division not in businessToDivision.get(business, []):
|
||||||
|
if business not in businessToDivision:
|
||||||
|
businessToDivision[business] = []
|
||||||
|
businessToDivision[business].append(division)
|
||||||
|
|
||||||
|
# 處理部級單位(可能為空)
|
||||||
|
dept_key = department if department else f"(直屬){division}"
|
||||||
|
if dept_key not in HR_position_departments:
|
||||||
|
HR_position_departments[dept_key] = {
|
||||||
|
'id': department_id,
|
||||||
|
'code': f'DEPT{str(department_id).zfill(3)}',
|
||||||
|
'name': department if department else '(直屬)',
|
||||||
|
'division': division
|
||||||
|
}
|
||||||
|
department_id += 1
|
||||||
|
departmentToPosition[dept_key] = []
|
||||||
|
|
||||||
|
# 建立處級到部級的關聯
|
||||||
|
if division and dept_key not in divisionToDepartment.get(division, []):
|
||||||
|
if division not in divisionToDepartment:
|
||||||
|
divisionToDepartment[division] = []
|
||||||
|
divisionToDepartment[division].append(dept_key)
|
||||||
|
|
||||||
|
# 處理崗位(去重)
|
||||||
|
if position and position not in departmentToPosition.get(dept_key, []):
|
||||||
|
departmentToPosition[dept_key].append(position)
|
||||||
|
|
||||||
|
# 建立完整組織崗位關聯
|
||||||
|
HR_position_organization_positions.append({
|
||||||
|
'id': position_id,
|
||||||
|
'business': business,
|
||||||
|
'division': division,
|
||||||
|
'department': department if department else '(直屬)',
|
||||||
|
'position': position
|
||||||
|
})
|
||||||
|
position_id += 1
|
||||||
|
|
||||||
|
return {
|
||||||
|
'HR_position_business_units': HR_position_business_units,
|
||||||
|
'HR_position_divisions': HR_position_divisions,
|
||||||
|
'HR_position_departments': HR_position_departments,
|
||||||
|
'HR_position_organization_positions': HR_position_organization_positions,
|
||||||
|
'businessToDivision': businessToDivision,
|
||||||
|
'divisionToDepartment': divisionToDepartment,
|
||||||
|
'departmentToPosition': departmentToPosition
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# 測試
|
||||||
|
data = import_to_memory()
|
||||||
|
print(f"事業體數量: {len(data['HR_position_business_units'])}")
|
||||||
|
print(f"處級單位數量: {len(data['HR_position_divisions'])}")
|
||||||
|
print(f"部級單位數量: {len(data['HR_position_departments'])}")
|
||||||
|
print(f"組織崗位關聯數量: {len(data['HR_position_organization_positions'])}")
|
||||||
|
print()
|
||||||
|
print("事業體列表:")
|
||||||
|
for name, info in data['HR_position_business_units'].items():
|
||||||
|
print(f" - {info['id']}: {name}")
|
||||||
3484
index.html
3484
index.html
File diff suppressed because it is too large
Load Diff
406
js/admin.js
Normal file
406
js/admin.js
Normal file
@@ -0,0 +1,406 @@
|
|||||||
|
/**
|
||||||
|
* 管理功能模組
|
||||||
|
* 處理使用者管理、系統設定和統計功能
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { CSVUtils } from './csv.js';
|
||||||
|
|
||||||
|
const API_BASE_URL = '/api';
|
||||||
|
|
||||||
|
// ==================== 使用者管理 ====================
|
||||||
|
|
||||||
|
let usersData = [
|
||||||
|
{ employeeId: 'A001', name: '系統管理員', email: 'admin@company.com', role: 'superadmin', createdAt: '2024-01-01' },
|
||||||
|
{ employeeId: 'A002', name: '人資主管', email: 'hr_manager@company.com', role: 'admin', createdAt: '2024-01-15' },
|
||||||
|
{ employeeId: 'A003', name: '一般員工', email: 'employee@company.com', role: 'user', createdAt: '2024-02-01' }
|
||||||
|
];
|
||||||
|
let editingUserId = null;
|
||||||
|
|
||||||
|
export function showAddUserModal() {
|
||||||
|
editingUserId = null;
|
||||||
|
document.getElementById('userModalTitle').textContent = '新增使用者';
|
||||||
|
document.getElementById('userEmployeeId').value = '';
|
||||||
|
document.getElementById('userName').value = '';
|
||||||
|
document.getElementById('userEmail').value = '';
|
||||||
|
document.getElementById('userRole').value = '';
|
||||||
|
document.getElementById('userEmployeeId').disabled = false;
|
||||||
|
document.getElementById('userModal').style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function editUser(employeeId) {
|
||||||
|
const user = usersData.find(u => u.employeeId === employeeId);
|
||||||
|
if (!user) return;
|
||||||
|
|
||||||
|
editingUserId = employeeId;
|
||||||
|
document.getElementById('userModalTitle').textContent = '編輯使用者';
|
||||||
|
document.getElementById('userEmployeeId').value = user.employeeId;
|
||||||
|
document.getElementById('userEmployeeId').disabled = true;
|
||||||
|
document.getElementById('userName').value = user.name;
|
||||||
|
document.getElementById('userEmail').value = user.email;
|
||||||
|
document.getElementById('userRole').value = user.role;
|
||||||
|
document.getElementById('userModal').style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function closeUserModal() {
|
||||||
|
document.getElementById('userModal').style.display = 'none';
|
||||||
|
editingUserId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveUser(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const employeeId = document.getElementById('userEmployeeId').value;
|
||||||
|
const name = document.getElementById('userName').value;
|
||||||
|
const email = document.getElementById('userEmail').value;
|
||||||
|
const role = document.getElementById('userRole').value;
|
||||||
|
|
||||||
|
if (!employeeId || !name || !email || !role) {
|
||||||
|
if (typeof showToast === 'function') showToast('請填寫所有必填欄位');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (editingUserId) {
|
||||||
|
const index = usersData.findIndex(u => u.employeeId === editingUserId);
|
||||||
|
if (index > -1) {
|
||||||
|
usersData[index] = { ...usersData[index], name, email, role };
|
||||||
|
if (typeof showToast === 'function') showToast('使用者已更新');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (usersData.some(u => u.employeeId === employeeId)) {
|
||||||
|
if (typeof showToast === 'function') showToast('工號已存在');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
usersData.push({
|
||||||
|
employeeId,
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
role,
|
||||||
|
createdAt: new Date().toISOString().split('T')[0]
|
||||||
|
});
|
||||||
|
if (typeof showToast === 'function') showToast('使用者已新增');
|
||||||
|
}
|
||||||
|
|
||||||
|
closeUserModal();
|
||||||
|
renderUserList();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteUser(employeeId) {
|
||||||
|
if (confirm('確定要刪除此使用者嗎?')) {
|
||||||
|
usersData = usersData.filter(u => u.employeeId !== employeeId);
|
||||||
|
renderUserList();
|
||||||
|
if (typeof showToast === 'function') showToast('使用者已刪除');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderUserList() {
|
||||||
|
const tbody = document.getElementById('userListBody');
|
||||||
|
if (!tbody) return;
|
||||||
|
|
||||||
|
const roleLabels = {
|
||||||
|
'superadmin': { text: '最高權限管理者', color: '#e74c3c' },
|
||||||
|
'admin': { text: '管理者', color: '#f39c12' },
|
||||||
|
'user': { text: '一般使用者', color: '#27ae60' }
|
||||||
|
};
|
||||||
|
|
||||||
|
tbody.innerHTML = usersData.map(user => {
|
||||||
|
const roleInfo = roleLabels[user.role] || { text: user.role, color: '#999' };
|
||||||
|
const isSuperAdmin = user.role === 'superadmin';
|
||||||
|
return `
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 12px; border-bottom: 1px solid #eee;">${sanitizeHTML(user.employeeId)}</td>
|
||||||
|
<td style="padding: 12px; border-bottom: 1px solid #eee;">${sanitizeHTML(user.name)}</td>
|
||||||
|
<td style="padding: 12px; border-bottom: 1px solid #eee;">${sanitizeHTML(user.email)}</td>
|
||||||
|
<td style="padding: 12px; border-bottom: 1px solid #eee;">
|
||||||
|
<span style="background: ${roleInfo.color}; color: white; padding: 4px 8px; border-radius: 4px; font-size: 12px;">${roleInfo.text}</span>
|
||||||
|
</td>
|
||||||
|
<td style="padding: 12px; border-bottom: 1px solid #eee;">${sanitizeHTML(user.createdAt)}</td>
|
||||||
|
<td style="padding: 12px; border-bottom: 1px solid #eee; text-align: center;">
|
||||||
|
<button class="btn btn-secondary" style="padding: 4px 8px; font-size: 12px;" onclick="editUser('${user.employeeId}')">編輯</button>
|
||||||
|
${!isSuperAdmin ? `<button class="btn btn-cancel" style="padding: 4px 8px; font-size: 12px;" onclick="deleteUser('${user.employeeId}')">刪除</button>` : ''}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function exportUsersCSV() {
|
||||||
|
const headers = ['employeeId', 'name', 'email', 'role', 'createdAt'];
|
||||||
|
CSVUtils.exportToCSV(usersData, 'users.csv', headers);
|
||||||
|
if (typeof showToast === 'function') showToast('使用者清單已匯出!');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 用戶信息與登出功能 ====================
|
||||||
|
|
||||||
|
export function loadUserInfo() {
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logout() {
|
||||||
|
if (confirm('確定要登出系統嗎?')) {
|
||||||
|
localStorage.removeItem('currentUser');
|
||||||
|
if (typeof showToast === 'function') showToast('已成功登出系統');
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.href = 'login.html';
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Ollama 模型設定 ====================
|
||||||
|
|
||||||
|
export function saveOllamaModel() {
|
||||||
|
const saveBtn = document.getElementById('saveModelBtn');
|
||||||
|
if (saveBtn) {
|
||||||
|
saveBtn.style.display = 'inline-flex';
|
||||||
|
}
|
||||||
|
hideAllMessages();
|
||||||
|
}
|
||||||
|
|
||||||
|
export 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.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadOllamaModel() {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getOllamaModel() {
|
||||||
|
return localStorage.getItem('ollamaModel') || 'deepseek-reasoner';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hideAllMessages() {
|
||||||
|
const messages = ['connectionSuccess', 'connectionError', 'modelSaveSuccess'];
|
||||||
|
messages.forEach(id => {
|
||||||
|
const elem = document.getElementById(id);
|
||||||
|
if (elem) elem.style.display = 'none';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export 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) {
|
||||||
|
if (typeof showToast === 'function') showToast('請先選擇一個模型');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (testBtn) {
|
||||||
|
testBtn.disabled = true;
|
||||||
|
testBtn.textContent = '測試中...';
|
||||||
|
}
|
||||||
|
|
||||||
|
hideAllMessages();
|
||||||
|
|
||||||
|
try {
|
||||||
|
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>
|
||||||
|
測試連線
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 崗位資料管理功能 ====================
|
||||||
|
|
||||||
|
export async function exportCompletePositionData() {
|
||||||
|
try {
|
||||||
|
if (typeof showToast === 'function') showToast('正在準備匯出資料...');
|
||||||
|
window.location.href = API_BASE_URL + '/position-list/export';
|
||||||
|
setTimeout(() => {
|
||||||
|
if (typeof showToast === 'function') showToast('崗位資料匯出成功!');
|
||||||
|
}, 1000);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('匯出錯誤:', error);
|
||||||
|
alert('匯出失敗: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function refreshPositionStats() {
|
||||||
|
try {
|
||||||
|
if (typeof showToast === 'function') showToast('正在更新統計資料...');
|
||||||
|
|
||||||
|
const response = await fetch(API_BASE_URL + '/position-list');
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
const positions = result.data;
|
||||||
|
const total = positions.length;
|
||||||
|
const described = positions.filter(p => p.hasDescription).length;
|
||||||
|
const undescribed = total - described;
|
||||||
|
|
||||||
|
const totalEl = document.getElementById('totalPositionsCount');
|
||||||
|
const describedEl = document.getElementById('describedPositionsCount');
|
||||||
|
const undescribedEl = document.getElementById('undescribedPositionsCount');
|
||||||
|
|
||||||
|
if (totalEl) totalEl.textContent = total;
|
||||||
|
if (describedEl) describedEl.textContent = described;
|
||||||
|
if (undescribedEl) undescribedEl.textContent = undescribed;
|
||||||
|
|
||||||
|
if (typeof showToast === 'function') showToast('統計資料已更新');
|
||||||
|
} else {
|
||||||
|
alert('更新統計失敗: ' + (result.error || '未知錯誤'));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新統計錯誤:', error);
|
||||||
|
alert('更新統計失敗: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 工具函數 ====================
|
||||||
|
|
||||||
|
function sanitizeHTML(str) {
|
||||||
|
if (str === null || str === undefined) return '';
|
||||||
|
const temp = document.createElement('div');
|
||||||
|
temp.textContent = str;
|
||||||
|
return temp.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 暴露到全域
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.showAddUserModal = showAddUserModal;
|
||||||
|
window.editUser = editUser;
|
||||||
|
window.closeUserModal = closeUserModal;
|
||||||
|
window.saveUser = saveUser;
|
||||||
|
window.deleteUser = deleteUser;
|
||||||
|
window.renderUserList = renderUserList;
|
||||||
|
window.exportUsersCSV = exportUsersCSV;
|
||||||
|
window.logout = logout;
|
||||||
|
window.testOllamaConnection = testOllamaConnection;
|
||||||
|
window.saveOllamaModelWithConfirmation = saveOllamaModelWithConfirmation;
|
||||||
|
window.exportCompletePositionData = exportCompletePositionData;
|
||||||
|
window.refreshPositionStats = refreshPositionStats;
|
||||||
|
}
|
||||||
773
js/ai.js
Normal file
773
js/ai.js
Normal file
@@ -0,0 +1,773 @@
|
|||||||
|
/**
|
||||||
|
* AI 生成功能模組
|
||||||
|
* 處理 LLM API 調用和表單自動填充
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { DEFAULT_AI_PROMPTS } from './prompts.js';
|
||||||
|
|
||||||
|
// ==================== 工具函數 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消毒 HTML 字串,防止 XSS 攻擊
|
||||||
|
*/
|
||||||
|
export function sanitizeHTML(str) {
|
||||||
|
if (str === null || str === undefined) return '';
|
||||||
|
const temp = document.createElement('div');
|
||||||
|
temp.textContent = str;
|
||||||
|
return temp.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安全設定元素文字內容
|
||||||
|
*/
|
||||||
|
export function safeSetText(element, text) {
|
||||||
|
if (element) {
|
||||||
|
element.textContent = text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 只在欄位為空時填入值
|
||||||
|
*/
|
||||||
|
export function fillIfEmpty(elementId, value) {
|
||||||
|
const el = document.getElementById(elementId);
|
||||||
|
if (el && !el.value.trim() && value) {
|
||||||
|
el.value = value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 獲取欄位當前值
|
||||||
|
*/
|
||||||
|
export function getFieldValue(elementId) {
|
||||||
|
const el = document.getElementById(elementId);
|
||||||
|
return el ? el.value.trim() : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 獲取空白欄位列表
|
||||||
|
*/
|
||||||
|
export function getEmptyFields(fieldIds) {
|
||||||
|
return fieldIds.filter(id => !getFieldValue(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 獲取 Ollama 模型選項
|
||||||
|
*/
|
||||||
|
export function getOllamaModel() {
|
||||||
|
const select = document.getElementById('ollamaModel');
|
||||||
|
return select ? select.value : 'llama3.2';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== AI API 調用 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 調用 LLM API
|
||||||
|
*/
|
||||||
|
export async function callClaudeAPI(prompt, api = 'ollama') {
|
||||||
|
try {
|
||||||
|
const requestData = {
|
||||||
|
api: api,
|
||||||
|
prompt: prompt,
|
||||||
|
max_tokens: 2000
|
||||||
|
};
|
||||||
|
|
||||||
|
if (api === 'ollama') {
|
||||||
|
requestData.model = getOllamaModel();
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch("/api/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 調用失敗');
|
||||||
|
}
|
||||||
|
|
||||||
|
let responseText = data.text;
|
||||||
|
|
||||||
|
// 移除 markdown 代碼塊標記
|
||||||
|
responseText = responseText.replace(/```json\n?/g, "").replace(/```\n?/g, "").trim();
|
||||||
|
|
||||||
|
// 嘗試提取 JSON 物件
|
||||||
|
const jsonMatch = responseText.match(/\{[\s\S]*\}/);
|
||||||
|
if (jsonMatch) {
|
||||||
|
const allMatches = responseText.match(/\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}/g);
|
||||||
|
if (allMatches && allMatches.length > 0) {
|
||||||
|
for (let i = allMatches.length - 1; i >= 0; i--) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(allMatches[i]);
|
||||||
|
if (Object.keys(parsed).length > 0) {
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JSON.parse(jsonMatch[0]);
|
||||||
|
} catch (e) {
|
||||||
|
// 繼續嘗試其他方法
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 app.py)',
|
||||||
|
'已在 .env 文件中配置有效的 LLM API Key',
|
||||||
|
'網路連線正常',
|
||||||
|
'確認 Prompt 要求返回純 JSON 格式',
|
||||||
|
'嘗試使用不同的 LLM API (切換到其他模型)',
|
||||||
|
'檢查 LLM 模型是否支援繁體中文'
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 按鈕狀態控制 ====================
|
||||||
|
|
||||||
|
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>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== AI 幫我想功能 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化所有 prompt 編輯器
|
||||||
|
*/
|
||||||
|
export function initializePromptEditors() {
|
||||||
|
const modules = Object.keys(DEFAULT_AI_PROMPTS);
|
||||||
|
modules.forEach(module => {
|
||||||
|
const textarea = document.getElementById(`prompt_${module}`);
|
||||||
|
if (textarea) {
|
||||||
|
const savedPrompt = localStorage.getItem(`aiPrompt_${module}`);
|
||||||
|
textarea.value = savedPrompt || DEFAULT_AI_PROMPTS[module];
|
||||||
|
|
||||||
|
textarea.addEventListener('input', () => {
|
||||||
|
localStorage.setItem(`aiPrompt_${module}`, textarea.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置 prompt 為預設值
|
||||||
|
*/
|
||||||
|
export function resetPromptToDefault(module) {
|
||||||
|
const textarea = document.getElementById(`prompt_${module}`);
|
||||||
|
if (textarea && DEFAULT_AI_PROMPTS[module]) {
|
||||||
|
textarea.value = DEFAULT_AI_PROMPTS[module];
|
||||||
|
localStorage.removeItem(`aiPrompt_${module}`);
|
||||||
|
if (typeof showToast === 'function') {
|
||||||
|
showToast('已重置為預設 Prompt');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 執行 AI 幫我想
|
||||||
|
*/
|
||||||
|
export async function executeAIHelper(module) {
|
||||||
|
const container = document.querySelector(`.ai-helper-container[data-module="${module}"]`);
|
||||||
|
const btn = container.querySelector('.ai-helper-btn');
|
||||||
|
const textarea = document.getElementById(`prompt_${module}`);
|
||||||
|
|
||||||
|
if (!textarea || !textarea.value.trim()) {
|
||||||
|
if (typeof showToast === 'function') {
|
||||||
|
showToast('請輸入 Prompt 指令', 'error');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalHTML = btn.innerHTML;
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.innerHTML = '<div class="spinner"></div><span>AI 生成中...</span>';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const prompt = textarea.value.trim();
|
||||||
|
const data = await callClaudeAPI(prompt);
|
||||||
|
fillFormWithAIHelperResult(module, data);
|
||||||
|
if (typeof showToast === 'function') {
|
||||||
|
showToast('AI 生成完成!已填入表單');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('AI Helper error:', error);
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.innerHTML = originalHTML;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根據模組填入 AI 結果
|
||||||
|
*/
|
||||||
|
export function fillFormWithAIHelperResult(module, data) {
|
||||||
|
const fieldMappings = {
|
||||||
|
positionBasic: {
|
||||||
|
positionCode: 'positionCode',
|
||||||
|
positionName: 'positionName',
|
||||||
|
positionCategory: 'positionCategory',
|
||||||
|
positionNature: 'positionNature',
|
||||||
|
headcount: 'headcount',
|
||||||
|
positionLevel: 'positionLevel',
|
||||||
|
positionDesc: 'positionDesc',
|
||||||
|
positionRemark: 'positionRemark'
|
||||||
|
},
|
||||||
|
positionRecruit: {
|
||||||
|
education: 'recruitEducation',
|
||||||
|
experience: 'recruitExperience',
|
||||||
|
skills: 'recruitSkills',
|
||||||
|
certificates: 'recruitCertificates',
|
||||||
|
languages: 'recruitLanguages',
|
||||||
|
specialRequirements: 'recruitSpecialReq'
|
||||||
|
},
|
||||||
|
jobBasic: {
|
||||||
|
jobCode: 'jobCode',
|
||||||
|
jobName: 'jobName',
|
||||||
|
jobGrade: 'jobGrade',
|
||||||
|
jobCategory: 'jobCategory',
|
||||||
|
careerPath: 'careerPath',
|
||||||
|
jobDesc: 'jobDesc'
|
||||||
|
},
|
||||||
|
deptFunction: {
|
||||||
|
deptCode: 'deptCode',
|
||||||
|
deptName: 'deptName',
|
||||||
|
parentDept: 'parentDept',
|
||||||
|
deptHead: 'deptHead',
|
||||||
|
deptFunction: 'deptFunction',
|
||||||
|
kpi: 'kpi'
|
||||||
|
},
|
||||||
|
jobDesc: {
|
||||||
|
positionName: 'descPositionName',
|
||||||
|
department: 'descDepartment',
|
||||||
|
directSupervisor: 'descDirectSupervisor',
|
||||||
|
positionPurpose: 'descPositionPurpose',
|
||||||
|
mainResponsibilities: 'descMainResponsibilities',
|
||||||
|
education: 'descEducation',
|
||||||
|
basicSkills: 'descBasicSkills',
|
||||||
|
professionalKnowledge: 'descProfessionalKnowledge'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapping = fieldMappings[module];
|
||||||
|
if (!mapping) return;
|
||||||
|
|
||||||
|
let filledCount = 0;
|
||||||
|
for (const [dataKey, elementId] of Object.entries(mapping)) {
|
||||||
|
if (data[dataKey] !== undefined) {
|
||||||
|
const value = Array.isArray(data[dataKey])
|
||||||
|
? data[dataKey].join('\n')
|
||||||
|
: String(data[dataKey]);
|
||||||
|
if (fillIfEmpty(elementId, value)) {
|
||||||
|
filledCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filledCount > 0) {
|
||||||
|
console.log(`[AI Helper] 已填入 ${filledCount} 個欄位`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 錯誤顯示 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 顯示可複製的錯誤訊息
|
||||||
|
*/
|
||||||
|
export 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;
|
||||||
|
">
|
||||||
|
<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;">${sanitizeHTML(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;
|
||||||
|
">×</button>
|
||||||
|
</div>
|
||||||
|
<div style="padding: 25px; overflow-y: auto; flex: 1;">
|
||||||
|
<div style="color: #333; line-height: 1.6; margin-bottom: 20px;">${sanitizeHTML(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;">${sanitizeHTML(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;">🔍 詳細錯誤訊息(點擊展開)</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;">${sanitizeHTML(details)}</pre>
|
||||||
|
</details>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
<div style="padding: 15px 25px; border-top: 1px solid #f0f0f0; display: flex; justify-content: flex-end;">
|
||||||
|
<button onclick="this.closest('[style*=\"position: fixed\"]').remove()" style="
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 25px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 500;
|
||||||
|
">確定</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(modal);
|
||||||
|
|
||||||
|
modal.addEventListener('click', (e) => {
|
||||||
|
if (e.target === modal) {
|
||||||
|
modal.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 各表單 AI 生成函數 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成崗位基礎資料
|
||||||
|
*/
|
||||||
|
export async function generatePositionBasic(event) {
|
||||||
|
const btn = event.target.closest('.ai-generate-btn');
|
||||||
|
|
||||||
|
const allFields = ['positionCode', 'positionName', 'positionCategory', 'positionNature', 'headcount', 'positionLevel', 'positionDesc', 'positionRemark'];
|
||||||
|
const emptyFields = getEmptyFields(allFields);
|
||||||
|
|
||||||
|
if (emptyFields.length === 0) {
|
||||||
|
if (typeof showToast === 'function') showToast('所有欄位都已填寫完成!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setButtonLoading(btn, true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const existingData = {};
|
||||||
|
allFields.forEach(field => {
|
||||||
|
const value = getFieldValue(field);
|
||||||
|
if (value) existingData[field] = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const contextInfo = Object.keys(existingData).length > 0
|
||||||
|
? `\n\n已填寫的資料(請參考這些內容來生成相關的資料):\n${JSON.stringify(existingData, null, 2)}`
|
||||||
|
: '';
|
||||||
|
|
||||||
|
const prompt = `你是專業人資顧問,熟悉半導體製造業的人資所有流程。請生成崗位基礎資料。請用繁體中文回覆。
|
||||||
|
${contextInfo}
|
||||||
|
|
||||||
|
請「只生成」以下這些尚未填寫的欄位:${emptyFields.join(', ')}
|
||||||
|
|
||||||
|
欄位說明:
|
||||||
|
- positionCode: 崗位編號(格式如 ENG-001, MGR-002, SAL-003)
|
||||||
|
- positionName: 崗位名稱
|
||||||
|
- positionCategory: 崗位類別代碼(01=技術職, 02=管理職, 03=業務職, 04=行政職)
|
||||||
|
- positionNature: 崗位性質代碼(FT=全職, PT=兼職, CT=約聘, IN=實習)
|
||||||
|
- headcount: 編制人數(1-10之間的數字字串)
|
||||||
|
- positionLevel: 崗位級別(L1到L7)
|
||||||
|
- positionDesc: 崗位描述(條列式,用換行分隔))
|
||||||
|
- positionRemark: 崗位備注(條列式,用換行分隔)
|
||||||
|
|
||||||
|
重要:請「只」返回純JSON格式,不要有任何解釋、思考過程或額外文字。格式如下:
|
||||||
|
{
|
||||||
|
${emptyFields.map(f => `"${f}": "..."`).join(',\n ')}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const data = await callClaudeAPI(prompt);
|
||||||
|
|
||||||
|
let filledCount = 0;
|
||||||
|
if (fillIfEmpty('positionCode', data.positionCode)) filledCount++;
|
||||||
|
if (fillIfEmpty('positionName', data.positionName)) filledCount++;
|
||||||
|
if (fillIfEmpty('positionCategory', data.positionCategory)) {
|
||||||
|
filledCount++;
|
||||||
|
if (typeof updateCategoryName === 'function') updateCategoryName();
|
||||||
|
}
|
||||||
|
if (fillIfEmpty('positionNature', data.positionNature)) {
|
||||||
|
filledCount++;
|
||||||
|
if (typeof updateNatureName === 'function') updateNatureName();
|
||||||
|
}
|
||||||
|
if (fillIfEmpty('headcount', data.headcount)) filledCount++;
|
||||||
|
if (fillIfEmpty('positionLevel', data.positionLevel)) filledCount++;
|
||||||
|
if (fillIfEmpty('positionDesc', data.positionDesc)) filledCount++;
|
||||||
|
if (fillIfEmpty('positionRemark', data.positionRemark)) filledCount++;
|
||||||
|
|
||||||
|
if (typeof updatePreview === 'function') updatePreview();
|
||||||
|
if (typeof showToast === 'function') showToast(`✨ AI 已補充 ${filledCount} 個欄位!`);
|
||||||
|
} catch (error) {
|
||||||
|
if (typeof showToast === 'function') showToast('生成失敗,請稍後再試');
|
||||||
|
} finally {
|
||||||
|
setButtonLoading(btn, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成招聘要求資料
|
||||||
|
*/
|
||||||
|
export async function generatePositionRecruit(event) {
|
||||||
|
const btn = event.target.closest('.ai-generate-btn');
|
||||||
|
|
||||||
|
const allFields = ['minEducation', 'requiredGender', 'salaryRange', 'workExperience', 'minAge', 'maxAge', 'jobType', 'recruitPosition', 'jobTitle', 'jobDesc', 'positionReq', 'skillReq', 'langReq', 'otherReq'];
|
||||||
|
const emptyFields = getEmptyFields(allFields);
|
||||||
|
|
||||||
|
if (emptyFields.length === 0) {
|
||||||
|
if (typeof showToast === 'function') showToast('所有欄位都已填寫完成!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setButtonLoading(btn, true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const positionName = getFieldValue('positionName') || '一般職位';
|
||||||
|
const existingData = { positionName };
|
||||||
|
allFields.forEach(field => {
|
||||||
|
const value = getFieldValue(field);
|
||||||
|
if (value) existingData[field] = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const prompt = `請生成「${positionName}」的招聘要求資料。請用繁體中文回覆。
|
||||||
|
|
||||||
|
已填寫的資料(請參考這些內容來生成相關的資料):
|
||||||
|
${JSON.stringify(existingData, null, 2)}
|
||||||
|
|
||||||
|
請「只生成」以下這些尚未填寫的欄位:${emptyFields.join(', ')}
|
||||||
|
|
||||||
|
欄位說明:
|
||||||
|
- minEducation: 最低學歷代碼(HS=高中職, JC=專科, BA=大學, MA=碩士, PHD=博士)
|
||||||
|
- requiredGender: 要求性別(空字串=不限, M=男, F=女)
|
||||||
|
- salaryRange: 薪酬范圍代碼(A=30000以下, B=30000-50000, C=50000-80000, D=80000-120000, E=120000以上, N=面議)
|
||||||
|
- workExperience: 工作經驗年數(0=不限, 1, 3, 5, 10)
|
||||||
|
- minAge: 最小年齡(18-30之間的數字字串)
|
||||||
|
- maxAge: 最大年齡(35-55之間的數字字串)
|
||||||
|
- jobType: 工作性質代碼(FT=全職, PT=兼職, CT=約聘, DP=派遣)
|
||||||
|
- recruitPosition: 招聘職位代碼(ENG=工程師, MGR=經理, AST=助理, OP=作業員, SAL=業務)
|
||||||
|
- jobTitle: 職位名稱
|
||||||
|
- jobDesc: 職位描述(2-3句話)
|
||||||
|
- positionReq: 崗位要求(條列式,用換行分隔)
|
||||||
|
- skillReq: 技能要求(條列式,用換行分隔)
|
||||||
|
- langReq: 語言要求(條列式,用換行分隔)
|
||||||
|
- otherReq: 其他要求(條列式,用換行分隔)
|
||||||
|
|
||||||
|
重要:請「只」返回純JSON格式,不要有任何解釋、思考過程或額外文字。格式如下:
|
||||||
|
{
|
||||||
|
${emptyFields.map(f => `"${f}": "..."`).join(',\n ')}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const data = await callClaudeAPI(prompt);
|
||||||
|
|
||||||
|
let filledCount = 0;
|
||||||
|
if (fillIfEmpty('minEducation', data.minEducation)) filledCount++;
|
||||||
|
if (fillIfEmpty('requiredGender', data.requiredGender)) filledCount++;
|
||||||
|
if (fillIfEmpty('salaryRange', data.salaryRange)) filledCount++;
|
||||||
|
if (fillIfEmpty('workExperience', data.workExperience)) filledCount++;
|
||||||
|
if (fillIfEmpty('minAge', data.minAge)) filledCount++;
|
||||||
|
if (fillIfEmpty('maxAge', data.maxAge)) filledCount++;
|
||||||
|
if (fillIfEmpty('jobType', data.jobType)) filledCount++;
|
||||||
|
if (fillIfEmpty('recruitPosition', data.recruitPosition)) filledCount++;
|
||||||
|
if (fillIfEmpty('jobTitle', data.jobTitle)) filledCount++;
|
||||||
|
if (fillIfEmpty('jobDesc', data.jobDesc)) filledCount++;
|
||||||
|
if (fillIfEmpty('positionReq', data.positionReq)) filledCount++;
|
||||||
|
if (fillIfEmpty('skillReq', data.skillReq)) filledCount++;
|
||||||
|
if (fillIfEmpty('langReq', data.langReq)) filledCount++;
|
||||||
|
if (fillIfEmpty('otherReq', data.otherReq)) filledCount++;
|
||||||
|
|
||||||
|
if (typeof updatePreview === 'function') updatePreview();
|
||||||
|
if (typeof showToast === 'function') showToast(`✨ AI 已補充 ${filledCount} 個欄位!`);
|
||||||
|
} catch (error) {
|
||||||
|
if (typeof showToast === 'function') showToast('生成失敗,請稍後再試');
|
||||||
|
} finally {
|
||||||
|
setButtonLoading(btn, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成職務基礎資料
|
||||||
|
*/
|
||||||
|
export async function generateJobBasic(event) {
|
||||||
|
const btn = event.target.closest('.ai-generate-btn');
|
||||||
|
|
||||||
|
const allFields = ['jobCategoryCode', 'jobCode', 'jobName', 'jobNameEn', 'jobHeadcount', 'jobSortOrder', 'jobRemark', 'jobLevel'];
|
||||||
|
const emptyFields = getEmptyFields(allFields);
|
||||||
|
|
||||||
|
const attBonusChk = document.getElementById('job_hasAttBonus');
|
||||||
|
const houseAllowChk = document.getElementById('job_hasHouseAllow');
|
||||||
|
const needCheckboxes = !(attBonusChk?.checked) && !(houseAllowChk?.checked);
|
||||||
|
|
||||||
|
if (emptyFields.length === 0 && !needCheckboxes) {
|
||||||
|
if (typeof showToast === 'function') showToast('所有欄位都已填寫完成!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setButtonLoading(btn, true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const existingData = {};
|
||||||
|
allFields.forEach(field => {
|
||||||
|
const value = getFieldValue(field);
|
||||||
|
if (value) existingData[field] = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const contextInfo = Object.keys(existingData).length > 0
|
||||||
|
? `\n\n已填寫的資料(請參考這些內容來生成相關的資料):\n${JSON.stringify(existingData, null, 2)}`
|
||||||
|
: '';
|
||||||
|
|
||||||
|
const fieldsToGenerate = [...emptyFields];
|
||||||
|
if (needCheckboxes) {
|
||||||
|
fieldsToGenerate.push('hasAttendanceBonus', 'hasHousingAllowance');
|
||||||
|
}
|
||||||
|
|
||||||
|
const prompt = `你是專業人資顧問,熟悉半導體製造業的人資所有流程。請生成職務基礎資料。請用繁體中文回覆。
|
||||||
|
${contextInfo}
|
||||||
|
|
||||||
|
請「只生成」以下這些尚未填寫的欄位:${fieldsToGenerate.join(', ')}
|
||||||
|
|
||||||
|
欄位說明:
|
||||||
|
- jobCategoryCode: 職務類別代碼(MGR=管理職, TECH=技術職, SALE=業務職, ADMIN=行政職, RD=研發職, PROD=生產職)
|
||||||
|
- jobCode: 職務編號(格式如 MGR-001, TECH-002)
|
||||||
|
- jobName: 職務名稱
|
||||||
|
- jobNameEn: 職務英文名稱
|
||||||
|
- jobHeadcount: 編制人數(1-20之間的數字字串)
|
||||||
|
- jobSortOrder: 排列順序(10, 20, 30...的數字字串)
|
||||||
|
- jobRemark: 備注說明
|
||||||
|
- jobLevel: 職務層級(可以是 *保密* 或具體層級)
|
||||||
|
- hasAttendanceBonus: 是否有全勤(true/false)
|
||||||
|
- hasHousingAllowance: 是否住房補貼(true/false)
|
||||||
|
|
||||||
|
重要:請「只」返回純JSON格式,不要有任何解釋、思考過程或額外文字。格式如下:
|
||||||
|
{
|
||||||
|
${fieldsToGenerate.map(f => `"${f}": "..."`).join(',\n ')}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const data = await callClaudeAPI(prompt);
|
||||||
|
|
||||||
|
let filledCount = 0;
|
||||||
|
if (fillIfEmpty('jobCategoryCode', data.jobCategoryCode)) {
|
||||||
|
filledCount++;
|
||||||
|
if (typeof updateJobCategoryName === 'function') updateJobCategoryName();
|
||||||
|
}
|
||||||
|
if (fillIfEmpty('jobCode', data.jobCode)) filledCount++;
|
||||||
|
if (fillIfEmpty('jobName', data.jobName)) filledCount++;
|
||||||
|
if (fillIfEmpty('jobNameEn', data.jobNameEn)) filledCount++;
|
||||||
|
if (fillIfEmpty('jobHeadcount', data.jobHeadcount)) filledCount++;
|
||||||
|
if (fillIfEmpty('jobSortOrder', data.jobSortOrder)) filledCount++;
|
||||||
|
if (fillIfEmpty('jobRemark', data.jobRemark)) filledCount++;
|
||||||
|
if (fillIfEmpty('jobLevel', data.jobLevel)) filledCount++;
|
||||||
|
|
||||||
|
if (needCheckboxes) {
|
||||||
|
const attendanceCheckbox = document.getElementById('job_hasAttBonus');
|
||||||
|
const housingCheckbox = document.getElementById('job_hasHouseAllow');
|
||||||
|
|
||||||
|
if (data.hasAttendanceBonus === true && attendanceCheckbox) {
|
||||||
|
attendanceCheckbox.checked = true;
|
||||||
|
document.getElementById('attendanceLabel').textContent = '是';
|
||||||
|
filledCount++;
|
||||||
|
}
|
||||||
|
if (data.hasHousingAllowance === true && housingCheckbox) {
|
||||||
|
housingCheckbox.checked = true;
|
||||||
|
document.getElementById('housingLabel').textContent = '是';
|
||||||
|
filledCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof updatePreview === 'function') updatePreview();
|
||||||
|
if (typeof showToast === 'function') showToast(`✨ AI 已補充 ${filledCount} 個欄位!`);
|
||||||
|
} catch (error) {
|
||||||
|
if (typeof showToast === 'function') showToast('生成失敗,請稍後再試');
|
||||||
|
} finally {
|
||||||
|
setButtonLoading(btn, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成崗位描述資料
|
||||||
|
*/
|
||||||
|
export async function generateJobDesc(event) {
|
||||||
|
const btn = event.target.closest('.ai-generate-btn');
|
||||||
|
|
||||||
|
const allFields = ['jd_empNo', 'jd_empName', 'jd_positionCode', 'jd_versionDate', 'jd_positionName', 'jd_department', 'jd_positionEffectiveDate', 'jd_directSupervisor', 'jd_directReports', 'jd_workLocation', 'jd_empAttribute', 'jd_positionPurpose', 'jd_mainResponsibilities', 'jd_education', 'jd_basicSkills', 'jd_professionalKnowledge', 'jd_workExperienceReq', 'jd_otherRequirements'];
|
||||||
|
|
||||||
|
const emptyFields = allFields.filter(id => {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
const value = el ? el.value.trim() : '';
|
||||||
|
if (id === 'jd_mainResponsibilities') {
|
||||||
|
return !value || value === '1、\n2、\n3、\n4、' || value === '1、\n2、\n3、\n4、\n5、';
|
||||||
|
}
|
||||||
|
return !value;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (emptyFields.length === 0) {
|
||||||
|
if (typeof showToast === 'function') showToast('所有欄位都已填寫完成!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setButtonLoading(btn, true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const existingData = {};
|
||||||
|
allFields.forEach(field => {
|
||||||
|
const el = document.getElementById(field);
|
||||||
|
const value = el ? el.value.trim() : '';
|
||||||
|
if (value && value !== '1、\n2、\n3、\n4、') {
|
||||||
|
existingData[field.replace('jd_', '')] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const contextInfo = Object.keys(existingData).length > 0
|
||||||
|
? `\n\n已填寫的資料(請參考這些內容來生成相關的資料):\n${JSON.stringify(existingData, null, 2)}`
|
||||||
|
: '';
|
||||||
|
|
||||||
|
const fieldsToGenerate = emptyFields.map(f => f.replace('jd_', ''));
|
||||||
|
|
||||||
|
const prompt = `請生成崗位描述資料。請用繁體中文回覆。
|
||||||
|
${contextInfo}
|
||||||
|
|
||||||
|
請「只生成」以下這些尚未填寫的欄位:${fieldsToGenerate.join(', ')}
|
||||||
|
|
||||||
|
欄位說明:
|
||||||
|
- empNo: 工號(格式如 A001234)
|
||||||
|
- empName: 員工姓名
|
||||||
|
- positionCode: 崗位代碼
|
||||||
|
- versionDate: 版本日期(YYYY-MM-DD格式)
|
||||||
|
- positionName: 崗位名稱
|
||||||
|
- department: 所屬部門
|
||||||
|
- positionEffectiveDate: 崗位生效日期(YYYY-MM-DD格式)
|
||||||
|
- directSupervisor: 直接領導職務
|
||||||
|
- directReports: 直接下級(格式如「工程師 x 5人」)
|
||||||
|
- workLocation: 任職地點代碼(HQ=總部, TPE=台北, TYC=桃園, KHH=高雄, SH=上海, SZ=深圳)
|
||||||
|
- empAttribute: 員工屬性代碼(FT=正式員工, CT=約聘, PT=兼職, IN=實習, DP=派遣)
|
||||||
|
- positionPurpose: 崗位設置目的(1句話說明)
|
||||||
|
- mainResponsibilities: 主要崗位職責(用「1、」「2、」「3、」「4、」「5、」格式,每項換行,用\\n分隔)
|
||||||
|
- education: 教育程度要求(條列式,用換行分隔)
|
||||||
|
- basicSkills: 基本技能要求(條列式,用換行分隔)
|
||||||
|
- professionalKnowledge: 專業知識要求(條列式,用換行分隔)
|
||||||
|
- workExperienceReq: 工作經驗要求(條列式,用換行分隔)
|
||||||
|
- otherRequirements: 其他要求(條列式,用換行分隔)
|
||||||
|
|
||||||
|
重要:請「只」返回純JSON格式,不要有任何解釋、思考過程或額外文字。格式如下:
|
||||||
|
{
|
||||||
|
${fieldsToGenerate.map(f => `"${f}": "..."`).join(',\n ')}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const data = await callClaudeAPI(prompt);
|
||||||
|
|
||||||
|
let filledCount = 0;
|
||||||
|
|
||||||
|
const fieldMapping = {
|
||||||
|
'empNo': 'jd_empNo',
|
||||||
|
'empName': 'jd_empName',
|
||||||
|
'positionCode': 'jd_positionCode',
|
||||||
|
'versionDate': 'jd_versionDate',
|
||||||
|
'positionName': 'jd_positionName',
|
||||||
|
'department': 'jd_department',
|
||||||
|
'positionEffectiveDate': 'jd_positionEffectiveDate',
|
||||||
|
'directSupervisor': 'jd_directSupervisor',
|
||||||
|
'directReports': 'jd_directReports',
|
||||||
|
'workLocation': 'jd_workLocation',
|
||||||
|
'empAttribute': 'jd_empAttribute',
|
||||||
|
'positionPurpose': 'jd_positionPurpose',
|
||||||
|
'mainResponsibilities': 'jd_mainResponsibilities',
|
||||||
|
'education': 'jd_education',
|
||||||
|
'basicSkills': 'jd_basicSkills',
|
||||||
|
'professionalKnowledge': 'jd_professionalKnowledge',
|
||||||
|
'workExperienceReq': 'jd_workExperienceReq',
|
||||||
|
'otherRequirements': 'jd_otherRequirements'
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.keys(fieldMapping).forEach(apiField => {
|
||||||
|
const htmlId = fieldMapping[apiField];
|
||||||
|
if (data[apiField]) {
|
||||||
|
const el = document.getElementById(htmlId);
|
||||||
|
const currentValue = el ? el.value.trim() : '';
|
||||||
|
const isEmpty = !currentValue || currentValue === '1、\n2、\n3、\n4、';
|
||||||
|
if (isEmpty) {
|
||||||
|
el.value = data[apiField];
|
||||||
|
filledCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (typeof updatePreview === 'function') updatePreview();
|
||||||
|
if (typeof showToast === 'function') showToast(`✨ AI 已補充 ${filledCount} 個欄位!`);
|
||||||
|
} catch (error) {
|
||||||
|
if (typeof showToast === 'function') showToast('生成失敗,請稍後再試');
|
||||||
|
} finally {
|
||||||
|
setButtonLoading(btn, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
21
js/api.js
21
js/api.js
@@ -59,8 +59,14 @@ export async function callClaudeAPI(prompt, api = 'ollama') {
|
|||||||
// 清理 JSON 代碼塊標記和其他格式
|
// 清理 JSON 代碼塊標記和其他格式
|
||||||
let responseText = data.text;
|
let responseText = data.text;
|
||||||
|
|
||||||
|
// 移除 DeepSeek-R1 等模型的思考標籤 <think>...</think>
|
||||||
|
responseText = responseText.replace(/<think>[\s\S]*?<\/think>/gi, '');
|
||||||
|
|
||||||
// 移除 Markdown 代碼塊標記
|
// 移除 Markdown 代碼塊標記
|
||||||
responseText = responseText.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim();
|
responseText = responseText.replace(/```json\n?/gi, '').replace(/```\n?/g, '').trim();
|
||||||
|
|
||||||
|
// 移除可能的前導文字(如 "Here is the JSON:" 等)
|
||||||
|
responseText = responseText.replace(/^[\s\S]*?(?=\{)/i, '');
|
||||||
|
|
||||||
// 嘗試提取 JSON 內容(如果包含其他文字)
|
// 嘗試提取 JSON 內容(如果包含其他文字)
|
||||||
// 查找第一個 { 和最後一個 }
|
// 查找第一個 { 和最後一個 }
|
||||||
@@ -77,7 +83,18 @@ export async function callClaudeAPI(prompt, api = 'ollama') {
|
|||||||
} catch (parseError) {
|
} catch (parseError) {
|
||||||
// JSON 解析失敗,拋出更詳細的錯誤
|
// JSON 解析失敗,拋出更詳細的錯誤
|
||||||
console.error('JSON 解析失敗,原始響應:', responseText);
|
console.error('JSON 解析失敗,原始響應:', responseText);
|
||||||
throw new Error(`LLM 返回的內容不是有效的 JSON 格式。\n\n原始響應前 200 字符:\n${responseText.substring(0, 200)}...`);
|
|
||||||
|
// 嘗試修復常見的 JSON 問題
|
||||||
|
try {
|
||||||
|
// 移除控制字符
|
||||||
|
const cleanedText = responseText
|
||||||
|
.replace(/[\x00-\x1F\x7F]/g, '') // 移除控制字符
|
||||||
|
.replace(/,\s*}/g, '}') // 移除末尾逗號
|
||||||
|
.replace(/,\s*]/g, ']'); // 移除陣列末尾逗號
|
||||||
|
return JSON.parse(cleanedText);
|
||||||
|
} catch (retryError) {
|
||||||
|
throw new Error(`LLM 返回的內容不是有效的 JSON 格式。\n\n原始響應前 300 字符:\n${responseText.substring(0, 300)}...`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error calling LLM API:', error);
|
console.error('Error calling LLM API:', error);
|
||||||
|
|||||||
337
js/csv.js
Normal file
337
js/csv.js
Normal file
@@ -0,0 +1,337 @@
|
|||||||
|
/**
|
||||||
|
* CSV 匯入匯出模組
|
||||||
|
* 處理各表單的 CSV 資料交換
|
||||||
|
*/
|
||||||
|
|
||||||
|
const API_BASE_URL = '/api';
|
||||||
|
|
||||||
|
// ==================== CSV 工具函數 ====================
|
||||||
|
|
||||||
|
export const CSVUtils = {
|
||||||
|
/**
|
||||||
|
* 匯出資料到 CSV
|
||||||
|
*/
|
||||||
|
exportToCSV(data, filename, headers) {
|
||||||
|
if (!data || data.length === 0) {
|
||||||
|
console.warn('No data to export');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const csvHeaders = headers || Object.keys(data[0]);
|
||||||
|
const csvRows = data.map(row => {
|
||||||
|
return csvHeaders.map(header => {
|
||||||
|
let value = row[header] !== undefined ? row[header] : '';
|
||||||
|
// 處理包含逗號或換行的值
|
||||||
|
if (typeof value === 'string' && (value.includes(',') || value.includes('\n') || value.includes('"'))) {
|
||||||
|
value = '"' + value.replace(/"/g, '""') + '"';
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}).join(',');
|
||||||
|
});
|
||||||
|
|
||||||
|
const csvContent = '\uFEFF' + [csvHeaders.join(','), ...csvRows].join('\n'); // BOM for UTF-8
|
||||||
|
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = URL.createObjectURL(blob);
|
||||||
|
link.download = filename;
|
||||||
|
link.click();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 從 CSV 匯入資料
|
||||||
|
*/
|
||||||
|
importFromCSV(file, callback) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
const text = e.target.result;
|
||||||
|
const lines = text.split('\n').filter(line => line.trim());
|
||||||
|
if (lines.length < 2) {
|
||||||
|
callback([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = this.parseCSVLine(lines[0]);
|
||||||
|
const data = [];
|
||||||
|
|
||||||
|
for (let i = 1; i < lines.length; i++) {
|
||||||
|
const values = this.parseCSVLine(lines[i]);
|
||||||
|
const row = {};
|
||||||
|
headers.forEach((header, index) => {
|
||||||
|
row[header.trim()] = values[index] ? values[index].trim() : '';
|
||||||
|
});
|
||||||
|
data.push(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(data);
|
||||||
|
};
|
||||||
|
reader.readAsText(file, 'UTF-8');
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 CSV 行(處理引號內的逗號)
|
||||||
|
*/
|
||||||
|
parseCSVLine(line) {
|
||||||
|
const result = [];
|
||||||
|
let current = '';
|
||||||
|
let inQuotes = false;
|
||||||
|
|
||||||
|
for (let i = 0; i < line.length; i++) {
|
||||||
|
const char = line[i];
|
||||||
|
if (char === '"') {
|
||||||
|
if (inQuotes && line[i + 1] === '"') {
|
||||||
|
current += '"';
|
||||||
|
i++;
|
||||||
|
} else {
|
||||||
|
inQuotes = !inQuotes;
|
||||||
|
}
|
||||||
|
} else if (char === ',' && !inQuotes) {
|
||||||
|
result.push(current);
|
||||||
|
current = '';
|
||||||
|
} else {
|
||||||
|
current += char;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.push(current);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================== 崗位資料 CSV ====================
|
||||||
|
|
||||||
|
export function downloadPositionCSVTemplate() {
|
||||||
|
window.location.href = API_BASE_URL + '/positions/csv-template';
|
||||||
|
if (typeof showToast === 'function') showToast('正在下載崗位資料範本...');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function exportPositionsCSV() {
|
||||||
|
const data = [{
|
||||||
|
positionCode: getFieldValue('positionCode'),
|
||||||
|
positionName: getFieldValue('positionName'),
|
||||||
|
positionCategory: getFieldValue('positionCategory'),
|
||||||
|
positionNature: getFieldValue('positionNature'),
|
||||||
|
headcount: getFieldValue('headcount'),
|
||||||
|
positionLevel: getFieldValue('positionLevel'),
|
||||||
|
effectiveDate: getFieldValue('effectiveDate'),
|
||||||
|
positionDesc: getFieldValue('positionDesc'),
|
||||||
|
positionRemark: getFieldValue('positionRemark'),
|
||||||
|
minEducation: getFieldValue('minEducation'),
|
||||||
|
salaryRange: getFieldValue('salaryRange'),
|
||||||
|
workExperience: getFieldValue('workExperience'),
|
||||||
|
minAge: getFieldValue('minAge'),
|
||||||
|
maxAge: getFieldValue('maxAge')
|
||||||
|
}];
|
||||||
|
|
||||||
|
const headers = ['positionCode', 'positionName', 'positionCategory', 'positionNature',
|
||||||
|
'headcount', 'positionLevel', 'effectiveDate', 'positionDesc', 'positionRemark',
|
||||||
|
'minEducation', 'salaryRange', 'workExperience', 'minAge', 'maxAge'];
|
||||||
|
|
||||||
|
CSVUtils.exportToCSV(data, 'positions.csv', headers);
|
||||||
|
if (typeof showToast === 'function') showToast('崗位資料已匯出!');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function importPositionsCSV() {
|
||||||
|
document.getElementById('positionCSVInput').click();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handlePositionCSVImport(event) {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
|
||||||
|
if (typeof showToast === 'function') showToast('正在匯入崗位資料...');
|
||||||
|
|
||||||
|
fetch(API_BASE_URL + '/positions/import-csv', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
let message = data.message;
|
||||||
|
if (data.errors && data.errors.length > 0) {
|
||||||
|
message += '\n\n錯誤詳情:\n' + data.errors.join('\n');
|
||||||
|
}
|
||||||
|
alert(message);
|
||||||
|
} else {
|
||||||
|
alert('匯入失敗: ' + (data.error || '未知錯誤'));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('匯入錯誤:', error);
|
||||||
|
alert('匯入失敗: ' + error.message);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
event.target.value = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 職務資料 CSV ====================
|
||||||
|
|
||||||
|
export function downloadJobCSVTemplate() {
|
||||||
|
window.location.href = API_BASE_URL + '/jobs/csv-template';
|
||||||
|
if (typeof showToast === 'function') showToast('正在下載職務資料範本...');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function exportJobsCSV() {
|
||||||
|
const data = [{
|
||||||
|
jobCategoryCode: getFieldValue('jobCategoryCode'),
|
||||||
|
jobCategoryName: getFieldValue('jobCategoryName'),
|
||||||
|
jobCode: getFieldValue('jobCode'),
|
||||||
|
jobName: getFieldValue('jobName'),
|
||||||
|
jobNameEn: getFieldValue('jobNameEn'),
|
||||||
|
jobEffectiveDate: getFieldValue('jobEffectiveDate'),
|
||||||
|
jobHeadcount: getFieldValue('jobHeadcount'),
|
||||||
|
jobSortOrder: getFieldValue('jobSortOrder'),
|
||||||
|
jobRemark: getFieldValue('jobRemark'),
|
||||||
|
jobLevel: getFieldValue('jobLevel'),
|
||||||
|
hasAttendanceBonus: document.getElementById('job_hasAttBonus')?.checked,
|
||||||
|
hasHousingAllowance: document.getElementById('job_hasHouseAllow')?.checked
|
||||||
|
}];
|
||||||
|
|
||||||
|
const headers = ['jobCategoryCode', 'jobCategoryName', 'jobCode', 'jobName', 'jobNameEn',
|
||||||
|
'jobEffectiveDate', 'jobHeadcount', 'jobSortOrder', 'jobRemark', 'jobLevel',
|
||||||
|
'hasAttendanceBonus', 'hasHousingAllowance'];
|
||||||
|
|
||||||
|
CSVUtils.exportToCSV(data, 'jobs.csv', headers);
|
||||||
|
if (typeof showToast === 'function') showToast('職務資料已匯出!');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function importJobsCSV() {
|
||||||
|
document.getElementById('jobCSVInput').click();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleJobCSVImport(event) {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
|
||||||
|
if (typeof showToast === 'function') showToast('正在匯入職務資料...');
|
||||||
|
|
||||||
|
fetch(API_BASE_URL + '/jobs/import-csv', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
let message = data.message;
|
||||||
|
if (data.errors && data.errors.length > 0) {
|
||||||
|
message += '\n\n錯誤詳情:\n' + data.errors.join('\n');
|
||||||
|
}
|
||||||
|
alert(message);
|
||||||
|
} else {
|
||||||
|
alert('匯入失敗: ' + (data.error || '未知錯誤'));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('匯入錯誤:', error);
|
||||||
|
alert('匯入失敗: ' + error.message);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
event.target.value = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 崗位描述 CSV ====================
|
||||||
|
|
||||||
|
export function exportDescriptionsCSV() {
|
||||||
|
const data = [{
|
||||||
|
descPositionCode: getFieldValue('descPositionCode'),
|
||||||
|
descPositionName: getFieldValue('descPositionName'),
|
||||||
|
descEffectiveDate: getFieldValue('descEffectiveDate'),
|
||||||
|
jobDuties: getFieldValue('jobDuties'),
|
||||||
|
requiredSkills: getFieldValue('requiredSkills'),
|
||||||
|
workEnvironment: getFieldValue('workEnvironment'),
|
||||||
|
careerPath: getFieldValue('careerPath'),
|
||||||
|
descRemark: getFieldValue('descRemark')
|
||||||
|
}];
|
||||||
|
|
||||||
|
const headers = ['descPositionCode', 'descPositionName', 'descEffectiveDate', 'jobDuties',
|
||||||
|
'requiredSkills', 'workEnvironment', 'careerPath', 'descRemark'];
|
||||||
|
|
||||||
|
CSVUtils.exportToCSV(data, 'job_descriptions.csv', headers);
|
||||||
|
if (typeof showToast === 'function') showToast('崗位描述已匯出!');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function importDescriptionsCSV() {
|
||||||
|
document.getElementById('descCSVInput').click();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleDescCSVImport(event) {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
CSVUtils.importFromCSV(file, (data) => {
|
||||||
|
if (data && data.length > 0) {
|
||||||
|
const firstRow = data[0];
|
||||||
|
Object.keys(firstRow).forEach(key => {
|
||||||
|
const element = document.getElementById(key);
|
||||||
|
if (element) {
|
||||||
|
element.value = firstRow[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (typeof showToast === 'function') {
|
||||||
|
showToast(`已匯入 ${data.length} 筆崗位描述資料(顯示第一筆)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
event.target.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 崗位清單 CSV ====================
|
||||||
|
|
||||||
|
export function exportPositionListCSV(positionListData) {
|
||||||
|
if (!positionListData || positionListData.length === 0) {
|
||||||
|
if (typeof showToast === 'function') showToast('請先載入清單資料');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const headers = ['positionCode', 'positionName', 'businessUnit', 'department', 'positionCategory', 'headcount', 'effectiveDate'];
|
||||||
|
CSVUtils.exportToCSV(positionListData, 'position_list.csv', headers);
|
||||||
|
if (typeof showToast === 'function') showToast('崗位清單已匯出!');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 部門職責 CSV ====================
|
||||||
|
|
||||||
|
export function importDeptFunctionCSV() {
|
||||||
|
document.getElementById('deptFunctionCsvInput').click();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleDeptFunctionCSVImport(event, callback) {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
CSVUtils.importFromCSV(file, (data) => {
|
||||||
|
if (data && data.length > 0) {
|
||||||
|
const row = data[0];
|
||||||
|
Object.keys(row).forEach(key => {
|
||||||
|
const el = document.getElementById(key);
|
||||||
|
if (el) el.value = row[key];
|
||||||
|
});
|
||||||
|
if (typeof showToast === 'function') showToast('已匯入 CSV 資料!');
|
||||||
|
if (callback) callback(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
event.target.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function exportDeptFunctionCSV(formData) {
|
||||||
|
const headers = Object.keys(formData);
|
||||||
|
CSVUtils.exportToCSV([formData], 'dept_function.csv', headers);
|
||||||
|
if (typeof showToast === 'function') showToast('部門職責資料已匯出!');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 工具函數 ====================
|
||||||
|
|
||||||
|
function getFieldValue(elementId) {
|
||||||
|
const el = document.getElementById(elementId);
|
||||||
|
return el ? el.value.trim() : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 暴露到全域
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.CSVUtils = CSVUtils;
|
||||||
|
}
|
||||||
238
js/data/hierarchy.js
Normal file
238
js/data/hierarchy.js
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
/**
|
||||||
|
* 組織階層靜態資料模組
|
||||||
|
* 從 Excel 提取的下拉選單資料
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ==================== 下拉選單資料 ====================
|
||||||
|
|
||||||
|
// 事業體
|
||||||
|
export const businessUnits = [
|
||||||
|
'半導體事業群', '汽車事業體', '法務室', '岡山製造事業體', '產品事業體',
|
||||||
|
'晶圓三廠', '集團人資行政事業體', '集團財務事業體', '集團會計事業體',
|
||||||
|
'集團資訊事業體', '新創事業體', '稽核室', '總經理室', '總品質事業體', '營業事業體'
|
||||||
|
];
|
||||||
|
|
||||||
|
// 處級單位
|
||||||
|
export const deptLevel1Units = [
|
||||||
|
'半導體事業群', '汽車事業體', '法務室', '生產處', '岡山製造事業體',
|
||||||
|
'封裝工程處', '副總辦公室', '測試工程與研發處', '資材處', '廠務與環安衛管理處',
|
||||||
|
'產品事業體', '先進產品事業處', '成熟產品事業處', '晶圓三廠', '製程工程處',
|
||||||
|
'集團人資行政事業體', '集團財務事業體', '岡山強茂財務處', '集團會計事業體',
|
||||||
|
'岡山會計處', '集團會計處', '集團資訊事業體', '資安行動小組', '資訊一處',
|
||||||
|
'資訊二處', '新創事業體', '中低壓產品研發處', '研發中心', '高壓產品研發處',
|
||||||
|
'稽核室', '總經理室', 'ESG專案辦公室', '專案管理室', '總品質事業體',
|
||||||
|
'營業事業體', '商業開發暨市場應用處', '海外銷售事業處', '全球技術服務處',
|
||||||
|
'全球行銷暨業務支援處', '大中華區銷售事業處'
|
||||||
|
];
|
||||||
|
|
||||||
|
// 部級單位
|
||||||
|
export const deptLevel2Units = [
|
||||||
|
'生產部', '生產企劃部', '岡山品質管制部', '製程工程一部', '製程工程二部',
|
||||||
|
'設備一部', '設備二部', '工業工程部', '測試工程部', '新產品導入部',
|
||||||
|
'研發部', '採購部', '外部資源部', '生管部', '原物料控制部', '廠務部',
|
||||||
|
'產品管理部(APD)', '產品管理部(MPD)', '品質部', '製造部', '廠務部(Fab3)',
|
||||||
|
'工程一部', '工程二部', '工程三部', '製程整合部(Fab3)', '行政總務管理部',
|
||||||
|
'招募任用部', '訓練發展部', '薪酬管理部', '岡山強茂財務部', '會計部',
|
||||||
|
'管理會計部', '集團合併報表部', '應用系統部', '電腦整合製造部', '系統網路服務部',
|
||||||
|
'資源管理部', '客戶品質管理部', '產品品質管理部', '品質系統及客戶工程整合部',
|
||||||
|
'封測外包品質管理部', '品質保證部', '日本區暨代工業務部', '歐亞區業務部',
|
||||||
|
'韓國區業務部-韓國區', '美洲區業務部', '應用工程部(GTS)', '系統工程部',
|
||||||
|
'特性測試部', '業務生管部', '市場行銷企劃部', 'MOSFET晶圓採購部',
|
||||||
|
'台灣區業務部', '業務一部', '業務二部'
|
||||||
|
];
|
||||||
|
|
||||||
|
// 崗位名稱
|
||||||
|
export const positionNames = [
|
||||||
|
'營運長', '營運長助理', '副總經理', '專案經理', '經副理', '法務專員',
|
||||||
|
'專利工程師', '處長', '專員', '課長', '組長', '班長', '副班長', '作業員',
|
||||||
|
'工程師', '副總經理助理', '副理', '專案經副理', '顧問', '人資長', '助理',
|
||||||
|
'財務長', '專案副理', '會計長', '資訊長', '主任', '總裁', '總經理',
|
||||||
|
'專員/工程師', '經理', '技術經副理', '處長/資深經理'
|
||||||
|
];
|
||||||
|
|
||||||
|
// ==================== 組織階層級聯映射 ====================
|
||||||
|
|
||||||
|
// 事業體 -> 處級單位 (預設資料,可被 API 覆蓋)
|
||||||
|
export let businessToDivision = {
|
||||||
|
'半導體事業群': ['半導體事業群'],
|
||||||
|
'汽車事業體': ['汽車事業體'],
|
||||||
|
'法務室': ['法務室'],
|
||||||
|
'岡山製造事業體': ['生產處', '岡山製造事業體', '封裝工程處', '副總辦公室', '測試工程與研發處', '資材處', '廠務與環安衛管理處'],
|
||||||
|
'產品事業體': ['產品事業體', '先進產品事業處', '成熟產品事業處'],
|
||||||
|
'晶圓三廠': ['晶圓三廠', '製程工程處'],
|
||||||
|
'集團人資行政事業體': ['集團人資行政事業體'],
|
||||||
|
'集團財務事業體': ['集團財務事業體', '岡山強茂財務處'],
|
||||||
|
'集團會計事業體': ['集團會計事業體', '岡山會計處', '集團會計處'],
|
||||||
|
'集團資訊事業體': ['集團資訊事業體', '資安行動小組', '資訊一處', '資訊二處'],
|
||||||
|
'新創事業體': ['新創事業體', '中低壓產品研發處', '研發中心', '高壓產品研發處'],
|
||||||
|
'稽核室': ['稽核室'],
|
||||||
|
'總經理室': ['總經理室', 'ESG專案辦公室', '專案管理室'],
|
||||||
|
'總品質事業體': ['總品質事業體'],
|
||||||
|
'營業事業體': ['營業事業體', '商業開發暨市場應用處', '海外銷售事業處', '全球技術服務處', '全球行銷暨業務支援處', '大中華區銷售事業處']
|
||||||
|
};
|
||||||
|
|
||||||
|
// 處級單位 -> 部級單位 (預設資料,可被 API 覆蓋)
|
||||||
|
export let divisionToDepartment = {
|
||||||
|
'半導體事業群': ['(直屬)'],
|
||||||
|
'汽車事業體': ['(直屬)'],
|
||||||
|
'法務室': ['(直屬)'],
|
||||||
|
'生產處': ['(直屬)', '生產部', '生產企劃部'],
|
||||||
|
'岡山製造事業體': ['岡山品質管制部'],
|
||||||
|
'封裝工程處': ['(直屬)', '製程工程一部', '製程工程二部', '設備一部', '設備二部'],
|
||||||
|
'副總辦公室': ['工業工程部'],
|
||||||
|
'測試工程與研發處': ['(直屬)', '測試工程部', '新產品導入部', '研發部'],
|
||||||
|
'資材處': ['(直屬)', '採購部', '外部資源部', '生管部', '原物料控制部'],
|
||||||
|
'廠務與環安衛管理處': ['(直屬)', '廠務部'],
|
||||||
|
'產品事業體': ['廠務部'],
|
||||||
|
'先進產品事業處': ['(直屬)', '產品管理部(APD)'],
|
||||||
|
'成熟產品事業處': ['(直屬)', '產品管理部(MPD)'],
|
||||||
|
'晶圓三廠': ['產品管理部(MPD)', '品質部', '製造部', '廠務部(Fab3)'],
|
||||||
|
'製程工程處': ['工程一部', '工程二部', '工程三部', '製程整合部(Fab3)'],
|
||||||
|
'集團人資行政事業體': ['製程整合部(Fab3)', '行政總務管理部', '招募任用部', '訓練發展部', '薪酬管理部'],
|
||||||
|
'集團財務事業體': ['薪酬管理部', '岡山強茂財務部'],
|
||||||
|
'岡山強茂財務處': ['(直屬)', '岡山強茂財務部'],
|
||||||
|
'集團會計事業體': ['岡山強茂財務部'],
|
||||||
|
'岡山會計處': ['(直屬)', '會計部', '管理會計部'],
|
||||||
|
'集團會計處': ['(直屬)', '集團合併報表部'],
|
||||||
|
'集團資訊事業體': ['集團合併報表部'],
|
||||||
|
'資安行動小組': ['集團合併報表部'],
|
||||||
|
'資訊一處': ['應用系統部', '電腦整合製造部', '系統網路服務部'],
|
||||||
|
'資訊二處': ['(直屬)'],
|
||||||
|
'新創事業體': ['(直屬)', '資源管理部'],
|
||||||
|
'中低壓產品研發處': ['(直屬)'],
|
||||||
|
'研發中心': ['(直屬)'],
|
||||||
|
'高壓產品研發處': ['(直屬)'],
|
||||||
|
'稽核室': ['(直屬)'],
|
||||||
|
'總經理室': ['(直屬)'],
|
||||||
|
'ESG專案辦公室': ['(直屬)'],
|
||||||
|
'專案管理室': ['(直屬)'],
|
||||||
|
'總品質事業體': ['(直屬)', '客戶品質管理部', '產品品質管理部', '品質系統及客戶工程整合部', '封測外包品質管理部', '品質保證部'],
|
||||||
|
'營業事業體': ['品質保證部'],
|
||||||
|
'商業開發暨市場應用處': ['(直屬)'],
|
||||||
|
'海外銷售事業處': ['(直屬)', '日本區暨代工業務部', '歐亞區業務部', '韓國區業務部-韓國區', '美洲區業務部'],
|
||||||
|
'全球技術服務處': ['(直屬)', '應用工程部(GTS)', '系統工程部', '特性測試部'],
|
||||||
|
'全球行銷暨業務支援處': ['(直屬)', '業務生管部', '市場行銷企劃部', 'MOSFET晶圓採購部'],
|
||||||
|
'大中華區銷售事業處': ['(直屬)', '台灣區業務部', '業務一部', '業務二部']
|
||||||
|
};
|
||||||
|
|
||||||
|
// 部級單位 -> 崗位名稱 (預設資料,可被 API 覆蓋)
|
||||||
|
export let departmentToPosition = {
|
||||||
|
'(直屬)': ['營運長', '營運長助理', '副總經理', '專案經理', '經副理', '法務專員', '專利工程師', '處長', '專員', '工程師', '課長', '專員/工程師', '主任', '總裁', '總經理', '經理', '助理'],
|
||||||
|
'生產部': ['經副理', '課長', '組長', '班長', '副班長', '作業員'],
|
||||||
|
'生產企劃部': ['經副理', '課長', '專員', '工程師'],
|
||||||
|
'岡山品質管制部': ['經副理', '課長', '工程師', '組長', '班長', '副班長', '作業員', '副總經理', '副總經理助理'],
|
||||||
|
'製程工程一部': ['經副理'],
|
||||||
|
'製程工程二部': ['經副理', '課長', '工程師'],
|
||||||
|
'設備一部': ['經副理'],
|
||||||
|
'設備二部': ['經副理', '課長', '工程師'],
|
||||||
|
'工業工程部': ['經副理', '工程師', '課長', '副理'],
|
||||||
|
'測試工程部': ['經副理', '課長', '工程師'],
|
||||||
|
'新產品導入部': ['經副理', '專員', '工程師'],
|
||||||
|
'研發部': ['經副理', '課長', '工程師', '專員'],
|
||||||
|
'採購部': ['經副理', '課長', '專員'],
|
||||||
|
'外部資源部': ['專員'],
|
||||||
|
'生管部': ['經副理', '課長', '專員', '班長', '副班長', '作業員'],
|
||||||
|
'原物料控制部': ['經副理', '課長', '專員', '班長', '副班長', '作業員'],
|
||||||
|
'廠務部': ['經副理', '課長', '工程師', '專員', '處長'],
|
||||||
|
'產品管理部(APD)': ['經副理', '工程師'],
|
||||||
|
'產品管理部(MPD)': ['經副理', '專案經副理', '工程師', '顧問', '專員'],
|
||||||
|
'品質部': ['經副理', '工程師', '作業員'],
|
||||||
|
'製造部': ['經副理', '課長', '班長', '副班長', '作業員'],
|
||||||
|
'廠務部(Fab3)': ['經副理', '工程師'],
|
||||||
|
'工程一部': ['經副理', '工程師'],
|
||||||
|
'工程二部': ['經副理', '工程師'],
|
||||||
|
'工程三部': ['經副理', '工程師'],
|
||||||
|
'製程整合部(Fab3)': ['經副理', '工程師', '人資長'],
|
||||||
|
'行政總務管理部': ['經副理', '專員', '助理'],
|
||||||
|
'招募任用部': ['經副理', '專員'],
|
||||||
|
'訓練發展部': ['經副理', '專員'],
|
||||||
|
'薪酬管理部': ['經副理', '專員', '財務長'],
|
||||||
|
'岡山強茂財務部': ['經副理', '課長', '專員', '專案副理', '會計長'],
|
||||||
|
'會計部': ['經副理', '課長', '專員'],
|
||||||
|
'管理會計部': ['經副理', '課長', '專員'],
|
||||||
|
'集團合併報表部': ['經副理', '專員', '資訊長', '課長'],
|
||||||
|
'應用系統部': ['經副理', '工程師'],
|
||||||
|
'電腦整合製造部': ['經副理', '工程師'],
|
||||||
|
'系統網路服務部': ['經副理', '工程師'],
|
||||||
|
'資源管理部': ['經副理', '專員'],
|
||||||
|
'客戶品質管理部': ['經副理', '課長', '工程師', '專員'],
|
||||||
|
'產品品質管理部': ['經副理', '課長', '工程師', '班長', '作業員'],
|
||||||
|
'品質系統及客戶工程整合部': ['經副理', '課長', '工程師'],
|
||||||
|
'封測外包品質管理部': ['經副理', '課長', '工程師'],
|
||||||
|
'品質保證部': ['經副理', '課長', '工程師', '班長', '副班長', '作業員', '副總經理', '副總經理助理'],
|
||||||
|
'日本區暨代工業務部': ['經副理', '課長', '專員', '助理'],
|
||||||
|
'歐亞區業務部': ['經副理', '課長', '專員', '助理'],
|
||||||
|
'韓國區業務部-韓國區': ['經副理', '課長', '專員', '助理', '專案經理'],
|
||||||
|
'美洲區業務部': ['經副理', '課長', '專員', '助理'],
|
||||||
|
'應用工程部(GTS)': ['經副理', '專案經副理', '技術經副理', '工程師'],
|
||||||
|
'系統工程部': ['經副理', '工程師'],
|
||||||
|
'特性測試部': ['經副理', '課長', '工程師'],
|
||||||
|
'業務生管部': ['經副理', '課長', '專員'],
|
||||||
|
'市場行銷企劃部': ['處長', '經理', '專員'],
|
||||||
|
'MOSFET晶圓採購部': ['經副理', '課長', '專員'],
|
||||||
|
'台灣區業務部': ['專員', '助理'],
|
||||||
|
'業務一部': ['處長/資深經理', '經副理', '專員', '助理'],
|
||||||
|
'業務二部': ['處長/資深經理', '經副理', '專員', '助理']
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================== API 資料載入函數 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 從 API 載入組織階層資料,覆蓋預設資料
|
||||||
|
* @returns {Promise<boolean>} 是否載入成功
|
||||||
|
*/
|
||||||
|
export async function loadHierarchyData() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/hierarchy/cascade');
|
||||||
|
if (response.ok) {
|
||||||
|
const result = await response.json();
|
||||||
|
if (result.success && result.data) {
|
||||||
|
// 更新級聯映射資料
|
||||||
|
if (result.data.businessToDivision) {
|
||||||
|
Object.assign(businessToDivision, result.data.businessToDivision);
|
||||||
|
}
|
||||||
|
if (result.data.divisionToDepartment) {
|
||||||
|
Object.assign(divisionToDepartment, result.data.divisionToDepartment);
|
||||||
|
}
|
||||||
|
if (result.data.departmentToPosition) {
|
||||||
|
Object.assign(departmentToPosition, result.data.departmentToPosition);
|
||||||
|
}
|
||||||
|
console.log('[Hierarchy] 組織階層資料載入成功');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[Hierarchy] 無法從 API 載入組織階層資料,使用預設資料:', error.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 工具函數 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取得指定事業體的處級單位列表
|
||||||
|
* @param {string} businessUnit - 事業體名稱
|
||||||
|
* @returns {string[]} 處級單位列表
|
||||||
|
*/
|
||||||
|
export function getDivisions(businessUnit) {
|
||||||
|
return businessToDivision[businessUnit] || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取得指定處級單位的部級單位列表
|
||||||
|
* @param {string} division - 處級單位名稱
|
||||||
|
* @returns {string[]} 部級單位列表
|
||||||
|
*/
|
||||||
|
export function getDepartments(division) {
|
||||||
|
return divisionToDepartment[division] || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取得指定部級單位的崗位名稱列表
|
||||||
|
* @param {string} department - 部級單位名稱
|
||||||
|
* @returns {string[]} 崗位名稱列表
|
||||||
|
*/
|
||||||
|
export function getPositions(department) {
|
||||||
|
return departmentToPosition[department] || [];
|
||||||
|
}
|
||||||
333
js/dropdowns.js
Normal file
333
js/dropdowns.js
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
/**
|
||||||
|
* 下拉選單模組
|
||||||
|
* 處理階層式下拉選單的連動
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
businessUnits,
|
||||||
|
businessToDivision,
|
||||||
|
divisionToDepartment,
|
||||||
|
departmentToPosition,
|
||||||
|
loadHierarchyData
|
||||||
|
} from './data/hierarchy.js';
|
||||||
|
|
||||||
|
// ==================== 初始化下拉選單 ====================
|
||||||
|
|
||||||
|
export 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>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 初始化崗位描述模組的下拉選單 ==========
|
||||||
|
const jdBusinessUnitSelect = document.getElementById('jd_businessUnit');
|
||||||
|
if (jdBusinessUnitSelect) {
|
||||||
|
jdBusinessUnitSelect.innerHTML = '<option value="">請選擇</option>';
|
||||||
|
businessUnits.forEach(unit => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = unit;
|
||||||
|
option.textContent = unit;
|
||||||
|
jdBusinessUnitSelect.appendChild(option);
|
||||||
|
});
|
||||||
|
jdBusinessUnitSelect.addEventListener('change', onJobDescBusinessUnitChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 崗位描述的處級單位
|
||||||
|
const jdDivisionSelect = document.getElementById('jd_division');
|
||||||
|
if (jdDivisionSelect) {
|
||||||
|
jdDivisionSelect.addEventListener('change', onJobDescDivisionChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 崗位描述的部級單位
|
||||||
|
const jdDepartmentSelect = document.getElementById('jd_department');
|
||||||
|
if (jdDepartmentSelect) {
|
||||||
|
jdDepartmentSelect.addEventListener('change', onJobDescDepartmentChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 初始化部門職責維護模組的下拉選單 ==========
|
||||||
|
const deptFuncBusinessUnitSelect = document.getElementById('deptFunc_businessUnit');
|
||||||
|
if (deptFuncBusinessUnitSelect) {
|
||||||
|
deptFuncBusinessUnitSelect.innerHTML = '<option value="">請選擇</option>';
|
||||||
|
businessUnits.forEach(unit => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = unit;
|
||||||
|
option.textContent = unit;
|
||||||
|
deptFuncBusinessUnitSelect.appendChild(option);
|
||||||
|
});
|
||||||
|
deptFuncBusinessUnitSelect.addEventListener('change', onDeptFuncBusinessUnitChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 部門職責的處級單位
|
||||||
|
const deptFuncDivisionSelect = document.getElementById('deptFunc_division');
|
||||||
|
if (deptFuncDivisionSelect) {
|
||||||
|
deptFuncDivisionSelect.addEventListener('change', onDeptFuncDivisionChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 部門職責的部級單位
|
||||||
|
const deptFuncDepartmentSelect = document.getElementById('deptFunc_department');
|
||||||
|
if (deptFuncDepartmentSelect) {
|
||||||
|
deptFuncDepartmentSelect.addEventListener('change', onDeptFuncDepartmentChange);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 崗位基礎資料維護的連動 ====================
|
||||||
|
|
||||||
|
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 onJobDescBusinessUnitChange(event) {
|
||||||
|
const selectedBusiness = event.target.value;
|
||||||
|
const divisionSelect = document.getElementById('jd_division');
|
||||||
|
const departmentSelect = document.getElementById('jd_department');
|
||||||
|
const positionSelect = document.getElementById('jd_positionTitle');
|
||||||
|
|
||||||
|
if (divisionSelect) divisionSelect.innerHTML = '<option value="">請選擇</option>';
|
||||||
|
if (departmentSelect) departmentSelect.innerHTML = '<option value="">請先選擇處級單位</option>';
|
||||||
|
if (positionSelect) positionSelect.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 onJobDescDivisionChange(event) {
|
||||||
|
const selectedDivision = event.target.value;
|
||||||
|
const departmentSelect = document.getElementById('jd_department');
|
||||||
|
const positionSelect = document.getElementById('jd_positionTitle');
|
||||||
|
|
||||||
|
if (departmentSelect) departmentSelect.innerHTML = '<option value="">請選擇</option>';
|
||||||
|
if (positionSelect) positionSelect.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 onJobDescDepartmentChange(event) {
|
||||||
|
const selectedDepartment = event.target.value;
|
||||||
|
const positionSelect = document.getElementById('jd_positionTitle');
|
||||||
|
|
||||||
|
if (positionSelect) positionSelect.innerHTML = '<option value="">請選擇</option>';
|
||||||
|
|
||||||
|
if (selectedDepartment && departmentToPosition[selectedDepartment]) {
|
||||||
|
const positions = departmentToPosition[selectedDepartment];
|
||||||
|
positions.forEach(position => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = position;
|
||||||
|
option.textContent = position;
|
||||||
|
positionSelect.appendChild(option);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 部門職責維護模組的階層式下拉選單 ====================
|
||||||
|
|
||||||
|
function onDeptFuncBusinessUnitChange(event) {
|
||||||
|
const selectedBusiness = event.target.value;
|
||||||
|
const divisionSelect = document.getElementById('deptFunc_division');
|
||||||
|
const departmentSelect = document.getElementById('deptFunc_department');
|
||||||
|
const positionSelect = document.getElementById('deptFunc_positionTitle');
|
||||||
|
|
||||||
|
if (divisionSelect) divisionSelect.innerHTML = '<option value="">請選擇</option>';
|
||||||
|
if (departmentSelect) departmentSelect.innerHTML = '<option value="">請先選擇處級單位</option>';
|
||||||
|
if (positionSelect) positionSelect.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 onDeptFuncDivisionChange(event) {
|
||||||
|
const selectedDivision = event.target.value;
|
||||||
|
const departmentSelect = document.getElementById('deptFunc_department');
|
||||||
|
const positionSelect = document.getElementById('deptFunc_positionTitle');
|
||||||
|
|
||||||
|
if (departmentSelect) departmentSelect.innerHTML = '<option value="">請選擇</option>';
|
||||||
|
if (positionSelect) positionSelect.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 onDeptFuncDepartmentChange(event) {
|
||||||
|
const selectedDepartment = event.target.value;
|
||||||
|
const positionSelect = document.getElementById('deptFunc_positionTitle');
|
||||||
|
|
||||||
|
if (positionSelect) positionSelect.innerHTML = '<option value="">請選擇</option>';
|
||||||
|
|
||||||
|
if (selectedDepartment && departmentToPosition[selectedDepartment]) {
|
||||||
|
const positions = departmentToPosition[selectedDepartment];
|
||||||
|
positions.forEach(position => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = position;
|
||||||
|
option.textContent = position;
|
||||||
|
positionSelect.appendChild(option);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 部門職責關聯功能 ====================
|
||||||
|
|
||||||
|
// 部門職責資料 (示範用)
|
||||||
|
let deptFunctionData = [];
|
||||||
|
|
||||||
|
export function refreshDeptFunctionList(showMessage = false) {
|
||||||
|
const select = document.getElementById('jd_deptFunction');
|
||||||
|
if (!select) return;
|
||||||
|
|
||||||
|
select.innerHTML = '<option value="">-- 請選擇部門職責 --</option>';
|
||||||
|
|
||||||
|
if (deptFunctionData.length > 0) {
|
||||||
|
deptFunctionData.forEach(df => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = df.deptFunctionCode;
|
||||||
|
option.textContent = `${df.deptFunctionCode} - ${df.deptFunctionName} (${df.deptFunctionDept})`;
|
||||||
|
select.appendChild(option);
|
||||||
|
});
|
||||||
|
if (showMessage && typeof showToast === 'function') {
|
||||||
|
showToast('已載入 ' + deptFunctionData.length + ' 筆部門職責資料');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (showMessage && typeof showToast === 'function') {
|
||||||
|
showToast('尚無部門職責資料,請先建立部門職責');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadDeptFunctionInfo() {
|
||||||
|
const select = document.getElementById('jd_deptFunction');
|
||||||
|
const infoSection = document.getElementById('deptFunctionInfoSection');
|
||||||
|
|
||||||
|
if (!select) return;
|
||||||
|
|
||||||
|
const selectedCode = select.value;
|
||||||
|
|
||||||
|
if (!selectedCode) {
|
||||||
|
if (infoSection) infoSection.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deptFunc = deptFunctionData.find(d => d.deptFunctionCode === selectedCode);
|
||||||
|
|
||||||
|
if (deptFunc) {
|
||||||
|
const codeEl = document.getElementById('jd_deptFunctionCode');
|
||||||
|
const buEl = document.getElementById('jd_deptFunctionBU');
|
||||||
|
const missionEl = document.getElementById('jd_deptMission');
|
||||||
|
const functionsEl = document.getElementById('jd_deptCoreFunctions');
|
||||||
|
const kpisEl = document.getElementById('jd_deptKPIs');
|
||||||
|
|
||||||
|
if (codeEl) codeEl.value = deptFunc.deptFunctionCode || '';
|
||||||
|
if (buEl) buEl.value = deptFunc.deptFunctionBU || '';
|
||||||
|
if (missionEl) missionEl.value = deptFunc.deptMission || '';
|
||||||
|
if (functionsEl) functionsEl.value = deptFunc.deptCoreFunctions || '';
|
||||||
|
if (kpisEl) kpisEl.value = deptFunc.deptKPIs || '';
|
||||||
|
|
||||||
|
const deptInput = document.getElementById('jd_department');
|
||||||
|
if (deptInput && !deptInput.value) {
|
||||||
|
deptInput.value = deptFunc.deptFunctionDept;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (infoSection) infoSection.style.display = 'block';
|
||||||
|
|
||||||
|
if (typeof showToast === 'function') {
|
||||||
|
showToast('已載入部門職責: ' + deptFunc.deptFunctionName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setDeptFunctionData(data) {
|
||||||
|
deptFunctionData = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化時載入階層資料
|
||||||
|
export async function initializeHierarchyDropdowns() {
|
||||||
|
await loadHierarchyData();
|
||||||
|
initializeDropdowns();
|
||||||
|
}
|
||||||
571
js/forms.js
Normal file
571
js/forms.js
Normal file
@@ -0,0 +1,571 @@
|
|||||||
|
/**
|
||||||
|
* 表單邏輯模組
|
||||||
|
* 處理各表單的資料操作、驗證和提交
|
||||||
|
*/
|
||||||
|
|
||||||
|
const API_BASE_URL = '/api';
|
||||||
|
|
||||||
|
// ==================== 常數映射 ====================
|
||||||
|
|
||||||
|
export const categoryMap = { '01': '技術職', '02': '管理職', '03': '業務職', '04': '行政職' };
|
||||||
|
export const natureMap = { 'FT': '全職', 'PT': '兼職', 'CT': '約聘', 'IN': '實習' };
|
||||||
|
export const jobCategoryMap = { 'MGR': '管理職', 'TECH': '技術職', 'SALE': '業務職', 'ADMIN': '行政職', 'RD': '研發職', 'PROD': '生產職' };
|
||||||
|
|
||||||
|
// ==================== 崗位清單全域變數 ====================
|
||||||
|
export let positionListData = [];
|
||||||
|
export let currentSortColumn = '';
|
||||||
|
export let currentSortDirection = 'asc';
|
||||||
|
|
||||||
|
// ==================== 崗位基礎資料表單 ====================
|
||||||
|
|
||||||
|
export function updateCategoryName() {
|
||||||
|
const category = document.getElementById('positionCategory').value;
|
||||||
|
document.getElementById('positionCategoryName').value = categoryMap[category] || '';
|
||||||
|
updatePreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateNatureName() {
|
||||||
|
const nature = document.getElementById('positionNature').value;
|
||||||
|
document.getElementById('positionNatureName').value = natureMap[nature] || '';
|
||||||
|
updatePreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function changePositionCode() {
|
||||||
|
const currentCode = document.getElementById('positionCode').value;
|
||||||
|
const newCode = prompt('請輸入新的崗位編號:', currentCode);
|
||||||
|
if (newCode && newCode !== currentCode) {
|
||||||
|
document.getElementById('positionCode').value = newCode;
|
||||||
|
showToast('崗位編號已更改!');
|
||||||
|
updatePreview();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPositionFormData() {
|
||||||
|
const form = document.getElementById('positionForm');
|
||||||
|
const formData = new FormData(form);
|
||||||
|
const data = { basicInfo: {}, recruitInfo: {} };
|
||||||
|
const basicFields = ['positionCode', 'positionName', 'positionCategory', 'positionCategoryName', 'positionNature', 'positionNatureName', 'headcount', 'positionLevel', 'effectiveDate', 'positionDesc', 'positionRemark'];
|
||||||
|
const recruitFields = ['minEducation', 'requiredGender', 'salaryRange', 'workExperience', 'minAge', 'maxAge', 'jobType', 'recruitPosition', 'jobTitle', 'jobDesc', 'positionReq', 'titleReq', 'majorReq', 'skillReq', 'langReq', 'otherReq', 'superiorPosition', 'recruitRemark'];
|
||||||
|
basicFields.forEach(field => { const value = formData.get(field); if (value) data.basicInfo[field] = value; });
|
||||||
|
recruitFields.forEach(field => { const value = formData.get(field); if (value) data.recruitInfo[field] = value; });
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function savePositionAndExit() {
|
||||||
|
const form = document.getElementById('positionForm');
|
||||||
|
if (!form.checkValidity()) { form.reportValidity(); return; }
|
||||||
|
console.log('Save Position:', getPositionFormData());
|
||||||
|
showToast('崗位資料已保存!');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function savePositionAndNew() {
|
||||||
|
const form = document.getElementById('positionForm');
|
||||||
|
if (!form.checkValidity()) { form.reportValidity(); return; }
|
||||||
|
console.log('Save Position:', getPositionFormData());
|
||||||
|
showToast('崗位資料已保存,請繼續新增!');
|
||||||
|
form.reset();
|
||||||
|
document.getElementById('effectiveDate').value = new Date().toISOString().split('T')[0];
|
||||||
|
updatePreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
export 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cancelPositionForm() {
|
||||||
|
if (confirm('確定要取消嗎?未保存的資料將會遺失。')) {
|
||||||
|
document.getElementById('positionForm').reset();
|
||||||
|
updatePreview();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 職務基礎資料表單 ====================
|
||||||
|
|
||||||
|
export function updateJobCategoryName() {
|
||||||
|
const category = document.getElementById('jobCategoryCode').value;
|
||||||
|
document.getElementById('jobCategoryName').value = jobCategoryMap[category] || '';
|
||||||
|
updatePreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function changeJobCode() {
|
||||||
|
const currentCode = document.getElementById('jobCode').value;
|
||||||
|
const newCode = prompt('請輸入新的職務編號:', currentCode);
|
||||||
|
if (newCode && newCode !== currentCode) {
|
||||||
|
document.getElementById('jobCode').value = newCode;
|
||||||
|
showToast('職務編號已更改!');
|
||||||
|
updatePreview();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getJobFormData() {
|
||||||
|
const form = document.getElementById('jobForm');
|
||||||
|
const formData = new FormData(form);
|
||||||
|
const data = {};
|
||||||
|
const fields = ['jobCategoryCode', 'jobCategoryName', 'jobCode', 'jobName', 'jobNameEn', 'jobEffectiveDate', 'jobHeadcount', 'jobSortOrder', 'jobRemark', 'jobLevel'];
|
||||||
|
fields.forEach(field => { const value = formData.get(field); if (value) data[field] = value; });
|
||||||
|
data.hasAttendanceBonus = document.getElementById('job_hasAttBonus')?.checked || false;
|
||||||
|
data.hasHousingAllowance = document.getElementById('job_hasHouseAllow')?.checked || false;
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveJobToPositionList() {
|
||||||
|
const form = document.getElementById('jobForm');
|
||||||
|
if (!form.checkValidity()) {
|
||||||
|
form.reportValidity();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = getJobFormData();
|
||||||
|
|
||||||
|
if (!formData.jobCode) {
|
||||||
|
alert('請輸入職務代碼');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!formData.jobName) {
|
||||||
|
alert('請輸入職務名稱');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const positionData = {
|
||||||
|
basicInfo: {
|
||||||
|
positionCode: formData.jobCode,
|
||||||
|
positionName: formData.jobName,
|
||||||
|
positionCategory: formData.jobCategoryCode || '',
|
||||||
|
effectiveDate: formData.jobEffectiveDate || new Date().toISOString().split('T')[0],
|
||||||
|
headcount: formData.jobHeadcount || 1,
|
||||||
|
positionLevel: formData.jobLevel || '',
|
||||||
|
positionRemark: formData.jobRemark || ''
|
||||||
|
},
|
||||||
|
recruitInfo: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(API_BASE_URL + '/positions', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(positionData)
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveJobAndExit() {
|
||||||
|
const form = document.getElementById('jobForm');
|
||||||
|
if (!form.checkValidity()) { form.reportValidity(); return; }
|
||||||
|
console.log('Save Job:', getJobFormData());
|
||||||
|
showToast('職務資料已保存!');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveJobAndNew() {
|
||||||
|
const form = document.getElementById('jobForm');
|
||||||
|
if (!form.checkValidity()) { form.reportValidity(); return; }
|
||||||
|
console.log('Save Job:', getJobFormData());
|
||||||
|
showToast('職務資料已保存,請繼續新增!');
|
||||||
|
form.reset();
|
||||||
|
document.getElementById('attendanceLabel').textContent = '否';
|
||||||
|
document.getElementById('housingLabel').textContent = '否';
|
||||||
|
updatePreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cancelJobForm() {
|
||||||
|
if (confirm('確定要取消嗎?未保存的資料將會遺失。')) {
|
||||||
|
document.getElementById('jobForm').reset();
|
||||||
|
document.getElementById('attendanceLabel').textContent = '否';
|
||||||
|
document.getElementById('housingLabel').textContent = '否';
|
||||||
|
updatePreview();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 崗位描述表單 ====================
|
||||||
|
|
||||||
|
export function getJobDescFormData() {
|
||||||
|
const form = document.getElementById('jobDescForm');
|
||||||
|
const formData = new FormData(form);
|
||||||
|
const data = { basicInfo: {}, positionInfo: {}, responsibilities: {}, requirements: {} };
|
||||||
|
|
||||||
|
['empNo', 'empName', 'positionCode', 'versionDate'].forEach(field => {
|
||||||
|
const el = document.getElementById('jd_' + field);
|
||||||
|
if (el && el.value) data.basicInfo[field] = el.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
['positionName', 'department', 'positionEffectiveDate', 'directSupervisor', 'positionGradeJob', 'reportTo', 'directReports', 'workLocation', 'empAttribute'].forEach(field => {
|
||||||
|
const el = document.getElementById('jd_' + field);
|
||||||
|
if (el && el.value) data.positionInfo[field] = el.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const purpose = document.getElementById('jd_positionPurpose');
|
||||||
|
if (purpose && purpose.value) data.responsibilities.positionPurpose = purpose.value;
|
||||||
|
const mainResp = document.getElementById('jd_mainResponsibilities');
|
||||||
|
if (mainResp && mainResp.value) data.responsibilities.mainResponsibilities = mainResp.value;
|
||||||
|
|
||||||
|
['education', 'basicSkills', 'professionalKnowledge', 'workExperienceReq', 'otherRequirements'].forEach(field => {
|
||||||
|
const el = document.getElementById('jd_' + field);
|
||||||
|
if (el && el.value) data.requirements[field] = el.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveJobDescAndExit() {
|
||||||
|
const formData = getJobDescFormData();
|
||||||
|
console.log('Save JobDesc:', formData);
|
||||||
|
|
||||||
|
if (!formData.basicInfo.positionCode) {
|
||||||
|
alert('請輸入崗位代碼');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestData = {
|
||||||
|
positionCode: formData.basicInfo.positionCode,
|
||||||
|
positionName: formData.positionInfo.positionName || '',
|
||||||
|
effectiveDate: formData.positionInfo.positionEffectiveDate || new Date().toISOString().split('T')[0],
|
||||||
|
jobDuties: formData.responsibilities.mainResponsibilities || '',
|
||||||
|
requiredSkills: formData.requirements.basicSkills || '',
|
||||||
|
workEnvironment: formData.positionInfo.workLocation || ''
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(API_BASE_URL + '/position-descriptions', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(requestData)
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
showToast(result.message || '崗位描述已保存!');
|
||||||
|
setTimeout(() => {
|
||||||
|
switchModule('positionlist');
|
||||||
|
}, 1000);
|
||||||
|
} else {
|
||||||
|
alert('保存失敗: ' + (result.error || '未知錯誤'));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存錯誤:', error);
|
||||||
|
alert('保存失敗: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveJobDescAndNew() {
|
||||||
|
const formData = getJobDescFormData();
|
||||||
|
console.log('Save JobDesc:', formData);
|
||||||
|
|
||||||
|
if (!formData.basicInfo.positionCode) {
|
||||||
|
alert('請輸入崗位代碼');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestData = {
|
||||||
|
positionCode: formData.basicInfo.positionCode,
|
||||||
|
positionName: formData.positionInfo.positionName || '',
|
||||||
|
effectiveDate: formData.positionInfo.positionEffectiveDate || new Date().toISOString().split('T')[0],
|
||||||
|
jobDuties: formData.responsibilities.mainResponsibilities || '',
|
||||||
|
requiredSkills: formData.requirements.basicSkills || '',
|
||||||
|
workEnvironment: formData.positionInfo.workLocation || ''
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(API_BASE_URL + '/position-descriptions', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(requestData)
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
showToast(result.message || '崗位描述已保存,請繼續新增!');
|
||||||
|
document.getElementById('jobDescForm').reset();
|
||||||
|
document.getElementById('jd_mainResponsibilities').value = '1、\n2、\n3、\n4、';
|
||||||
|
updatePreview();
|
||||||
|
} else {
|
||||||
|
alert('保存失敗: ' + (result.error || '未知錯誤'));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存錯誤:', error);
|
||||||
|
alert('保存失敗: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveJobDescToPositionList() {
|
||||||
|
const formData = getJobDescFormData();
|
||||||
|
|
||||||
|
if (!formData.basicInfo.positionCode) {
|
||||||
|
alert('請輸入崗位代碼');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const positionData = {
|
||||||
|
basicInfo: {
|
||||||
|
positionCode: formData.basicInfo.positionCode,
|
||||||
|
positionName: formData.positionInfo.positionName || '',
|
||||||
|
effectiveDate: formData.positionInfo.positionEffectiveDate || new Date().toISOString().split('T')[0],
|
||||||
|
positionDesc: formData.responsibilities.mainResponsibilities || '',
|
||||||
|
positionRemark: formData.responsibilities.positionPurpose || ''
|
||||||
|
},
|
||||||
|
recruitInfo: {
|
||||||
|
minEducation: formData.requirements.education || '',
|
||||||
|
skillReq: formData.requirements.basicSkills || '',
|
||||||
|
workExperience: formData.requirements.workExperienceReq || '',
|
||||||
|
otherReq: formData.requirements.otherRequirements || ''
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(API_BASE_URL + '/positions', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(positionData)
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cancelJobDescForm() {
|
||||||
|
if (confirm('確定要取消嗎?未保存的資料將會遺失。')) {
|
||||||
|
document.getElementById('jobDescForm').reset();
|
||||||
|
document.getElementById('jd_mainResponsibilities').value = '1、\n2、\n3、\n4、';
|
||||||
|
updatePreview();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 預覽更新 ====================
|
||||||
|
|
||||||
|
export function updatePreview() {
|
||||||
|
const activeModuleBtn = document.querySelector('.module-btn.active');
|
||||||
|
if (!activeModuleBtn) return;
|
||||||
|
|
||||||
|
const activeModule = activeModuleBtn.dataset.module;
|
||||||
|
let data;
|
||||||
|
if (activeModule === 'position') {
|
||||||
|
data = { module: '崗位基礎資料', ...getPositionFormData() };
|
||||||
|
} else if (activeModule === 'job') {
|
||||||
|
data = { module: '職務基礎資料', ...getJobFormData() };
|
||||||
|
} else {
|
||||||
|
data = { module: '崗位描述', ...getJobDescFormData() };
|
||||||
|
}
|
||||||
|
const previewEl = document.getElementById('jsonPreview');
|
||||||
|
if (previewEl) {
|
||||||
|
previewEl.textContent = JSON.stringify(data, null, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Toast 訊息 ====================
|
||||||
|
|
||||||
|
export function showToast(message) {
|
||||||
|
const toast = document.getElementById('toast');
|
||||||
|
const toastMessage = document.getElementById('toastMessage');
|
||||||
|
if (!toast || !toastMessage) {
|
||||||
|
console.warn('Toast elements not found, creating dynamic toast');
|
||||||
|
const existingToast = document.querySelector('.toast.dynamic-toast');
|
||||||
|
if (existingToast) existingToast.remove();
|
||||||
|
|
||||||
|
const dynamicToast = document.createElement('div');
|
||||||
|
dynamicToast.className = 'toast dynamic-toast show';
|
||||||
|
dynamicToast.innerHTML = `
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>
|
||||||
|
<span>${message}</span>
|
||||||
|
`;
|
||||||
|
document.body.appendChild(dynamicToast);
|
||||||
|
setTimeout(() => {
|
||||||
|
dynamicToast.classList.remove('show');
|
||||||
|
setTimeout(() => dynamicToast.remove(), 300);
|
||||||
|
}, 3000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
toastMessage.textContent = message;
|
||||||
|
toast.classList.add('show');
|
||||||
|
setTimeout(() => toast.classList.remove('show'), 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 模組切換 ====================
|
||||||
|
|
||||||
|
export function switchModule(moduleName) {
|
||||||
|
document.querySelectorAll('.module-btn').forEach(b => {
|
||||||
|
b.classList.remove('active', 'job-active', 'desc-active');
|
||||||
|
});
|
||||||
|
document.querySelectorAll('.module-content').forEach(c => c.classList.remove('active'));
|
||||||
|
|
||||||
|
const targetBtn = document.querySelector(`.module-btn[data-module="${moduleName}"]`);
|
||||||
|
if (targetBtn) {
|
||||||
|
targetBtn.classList.add('active');
|
||||||
|
if (moduleName === 'job') targetBtn.classList.add('job-active');
|
||||||
|
if (moduleName === 'jobdesc') targetBtn.classList.add('desc-active');
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetModule = document.getElementById('module-' + moduleName);
|
||||||
|
if (targetModule) {
|
||||||
|
targetModule.classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (moduleName === 'positionlist') {
|
||||||
|
loadPositionList();
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 崗位清單功能 ====================
|
||||||
|
|
||||||
|
export async function loadPositionList() {
|
||||||
|
try {
|
||||||
|
showToast('正在載入崗位清單...');
|
||||||
|
|
||||||
|
const response = await fetch(API_BASE_URL + '/position-list');
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
positionListData = result.data;
|
||||||
|
renderPositionList();
|
||||||
|
showToast('已載入 ' + positionListData.length + ' 筆崗位資料');
|
||||||
|
} else {
|
||||||
|
alert('載入失敗: ' + (result.error || '未知錯誤'));
|
||||||
|
positionListData = [];
|
||||||
|
renderPositionList();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('載入錯誤:', error);
|
||||||
|
alert('載入失敗: ' + error.message);
|
||||||
|
positionListData = [];
|
||||||
|
renderPositionList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderPositionList() {
|
||||||
|
const tbody = document.getElementById('positionListBody');
|
||||||
|
if (!tbody) return;
|
||||||
|
|
||||||
|
if (positionListData.length === 0) {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="8" style="padding: 40px; text-align: center; color: var(--text-secondary);">沒有資料,請先建立崗位或點擊「載入清單」</td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody.innerHTML = positionListData.map(item => `
|
||||||
|
<tr style="border-bottom: 1px solid #eee;">
|
||||||
|
<td style="padding: 12px;">${sanitizeHTML(item.positionCode)}</td>
|
||||||
|
<td style="padding: 12px;">${sanitizeHTML(item.positionName)}</td>
|
||||||
|
<td style="padding: 12px;">${sanitizeHTML(item.positionCategory || '')}</td>
|
||||||
|
<td style="padding: 12px;">${sanitizeHTML(item.positionNature || '')}</td>
|
||||||
|
<td style="padding: 12px;">${sanitizeHTML(String(item.headcount || ''))}</td>
|
||||||
|
<td style="padding: 12px;">${sanitizeHTML(item.positionLevel || '')}</td>
|
||||||
|
<td style="padding: 12px;">${sanitizeHTML(item.effectiveDate || '')}</td>
|
||||||
|
<td style="padding: 12px; text-align: center;">
|
||||||
|
<button class="btn btn-secondary" style="padding: 4px 8px; font-size: 12px;" onclick="viewPositionDesc('${sanitizeHTML(item.positionCode)}')">檢視</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sortPositionList(column) {
|
||||||
|
if (currentSortColumn === column) {
|
||||||
|
currentSortDirection = currentSortDirection === 'asc' ? 'desc' : 'asc';
|
||||||
|
} else {
|
||||||
|
currentSortColumn = column;
|
||||||
|
currentSortDirection = 'asc';
|
||||||
|
}
|
||||||
|
|
||||||
|
positionListData.sort((a, b) => {
|
||||||
|
let valA = a[column];
|
||||||
|
let valB = b[column];
|
||||||
|
|
||||||
|
if (typeof valA === 'string') {
|
||||||
|
valA = valA.toLowerCase();
|
||||||
|
valB = valB.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valA < valB) return currentSortDirection === 'asc' ? -1 : 1;
|
||||||
|
if (valA > valB) return currentSortDirection === 'asc' ? 1 : -1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.sort-icon').forEach(icon => icon.textContent = '');
|
||||||
|
const currentHeader = document.querySelector(`th[data-sort="${column}"] .sort-icon`);
|
||||||
|
if (currentHeader) {
|
||||||
|
currentHeader.textContent = currentSortDirection === 'asc' ? ' ^' : ' v';
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPositionList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 工具函數 ====================
|
||||||
|
|
||||||
|
function sanitizeHTML(str) {
|
||||||
|
if (str === null || str === undefined) return '';
|
||||||
|
const temp = document.createElement('div');
|
||||||
|
temp.textContent = str;
|
||||||
|
return temp.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 暴露全域函數
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.showToast = showToast;
|
||||||
|
window.updatePreview = updatePreview;
|
||||||
|
window.switchModule = switchModule;
|
||||||
|
window.updateCategoryName = updateCategoryName;
|
||||||
|
window.updateNatureName = updateNatureName;
|
||||||
|
window.updateJobCategoryName = updateJobCategoryName;
|
||||||
|
}
|
||||||
19
js/main.js
19
js/main.js
@@ -89,26 +89,26 @@ function setupFormListeners() {
|
|||||||
field.addEventListener('input', updatePreview);
|
field.addEventListener('input', updatePreview);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 崗位類別變更
|
// 崗位類別變更 (pos_category)
|
||||||
const positionCategory = document.getElementById('positionCategory');
|
const positionCategory = document.getElementById('pos_category');
|
||||||
if (positionCategory) {
|
if (positionCategory) {
|
||||||
positionCategory.addEventListener('change', updateCategoryName);
|
positionCategory.addEventListener('change', updateCategoryName);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 崗位性質變更
|
// 崗位性質變更 (pos_type)
|
||||||
const positionNature = document.getElementById('positionNature');
|
const positionNature = document.getElementById('pos_type');
|
||||||
if (positionNature) {
|
if (positionNature) {
|
||||||
positionNature.addEventListener('change', updateNatureName);
|
positionNature.addEventListener('change', updateNatureName);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 職務類別變更
|
// 職務類別變更 (job_category)
|
||||||
const jobCategoryCode = document.getElementById('jobCategoryCode');
|
const jobCategoryCode = document.getElementById('job_category');
|
||||||
if (jobCategoryCode) {
|
if (jobCategoryCode) {
|
||||||
jobCategoryCode.addEventListener('change', updateJobCategoryName);
|
jobCategoryCode.addEventListener('change', updateJobCategoryName);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toggle 開關變更
|
// Toggle 開關變更 (job_hasAttBonus)
|
||||||
const hasAttendanceBonus = document.getElementById('hasAttendanceBonus');
|
const hasAttendanceBonus = document.getElementById('job_hasAttBonus');
|
||||||
if (hasAttendanceBonus) {
|
if (hasAttendanceBonus) {
|
||||||
hasAttendanceBonus.addEventListener('change', function() {
|
hasAttendanceBonus.addEventListener('change', function() {
|
||||||
const label = document.getElementById('attendanceLabel');
|
const label = document.getElementById('attendanceLabel');
|
||||||
@@ -119,7 +119,8 @@ function setupFormListeners() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasHousingAllowance = document.getElementById('hasHousingAllowance');
|
// Toggle 開關變更 (job_hasHouseAllow)
|
||||||
|
const hasHousingAllowance = document.getElementById('job_hasHouseAllow');
|
||||||
if (hasHousingAllowance) {
|
if (hasHousingAllowance) {
|
||||||
hasHousingAllowance.addEventListener('change', function() {
|
hasHousingAllowance.addEventListener('change', function() {
|
||||||
const label = document.getElementById('housingLabel');
|
const label = document.getElementById('housingLabel');
|
||||||
|
|||||||
88
js/prompts.js
Normal file
88
js/prompts.js
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
/**
|
||||||
|
* AI Prompt 模板模組
|
||||||
|
* 定義各表單的 AI 生成提示詞
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ==================== AI 幫我想 - 預設 Prompt 模板 ====================
|
||||||
|
export const DEFAULT_AI_PROMPTS = {
|
||||||
|
positionBasic: `你是專業人資顧問,熟悉半導體製造業。請生成崗位基礎資料。
|
||||||
|
|
||||||
|
請生成以下欄位:positionCode, positionName, positionCategory, positionNature, headcount, positionLevel, positionDesc, positionRemark
|
||||||
|
|
||||||
|
欄位說明:
|
||||||
|
- positionCode: 崗位編號(格式如 ENG-001, MGR-002)
|
||||||
|
- positionName: 崗位名稱
|
||||||
|
- positionCategory: 崗位類別代碼(01=技術職, 02=管理職, 03=業務職, 04=行政職)
|
||||||
|
- positionNature: 崗位性質代碼(FT=全職, PT=兼職, CT=約聘, IN=實習)
|
||||||
|
- headcount: 編制人數(1-10)
|
||||||
|
- positionLevel: 崗位級別(L1-L7)
|
||||||
|
- positionDesc: 崗位描述(條列式)
|
||||||
|
- positionRemark: 崗位備注
|
||||||
|
|
||||||
|
請用繁體中文,只返回純 JSON 格式,不要有任何其他文字。`,
|
||||||
|
|
||||||
|
positionRecruit: `你是專業人資顧問,熟悉半導體製造業。請生成招聘要求資料。
|
||||||
|
|
||||||
|
請生成以下欄位:minEducation, salaryRange, workExperience, jobType, recruitPosition, jobTitle, positionReq, skillReq, langReq
|
||||||
|
|
||||||
|
欄位說明:
|
||||||
|
- minEducation: 最低學歷代碼(HS=高中職, JC=專科, BA=大學, MA=碩士)
|
||||||
|
- salaryRange: 薪酬范圍代碼(A=30000以下, B=30000-50000, C=50000-80000, D=80000以上, N=面議)
|
||||||
|
- workExperience: 工作經驗年數(0, 1, 3, 5, 10)
|
||||||
|
- jobType: 工作性質代碼(FT=全職, PT=兼職, CT=約聘)
|
||||||
|
- recruitPosition: 招聘職位代碼(ENG=工程師, MGR=經理, AST=助理)
|
||||||
|
- jobTitle: 職位名稱
|
||||||
|
- positionReq: 崗位要求(條列式)
|
||||||
|
- skillReq: 技能要求(條列式)
|
||||||
|
- langReq: 語言要求(條列式)
|
||||||
|
|
||||||
|
請用繁體中文,只返回純 JSON 格式,不要有任何其他文字。`,
|
||||||
|
|
||||||
|
jobBasic: `你是專業人資顧問,熟悉半導體製造業。請生成職務基礎資料。
|
||||||
|
|
||||||
|
請生成以下欄位:jobCode, jobName, jobNameEn, jobCategoryCode, jobLevel, jobHeadcount, jobRemark
|
||||||
|
|
||||||
|
欄位說明:
|
||||||
|
- jobCode: 職務編號(格式如 J001)
|
||||||
|
- jobName: 職務名稱(繁體中文)
|
||||||
|
- jobNameEn: 職務名稱英文
|
||||||
|
- jobCategoryCode: 職務類別代碼(MGR=管理職, TECH=技術職, ADMIN=行政職, SALE=業務職)
|
||||||
|
- jobLevel: 職務級別(J1-J7)
|
||||||
|
- jobHeadcount: 職務人數(1-100)
|
||||||
|
- jobRemark: 職務備註
|
||||||
|
|
||||||
|
請用繁體中文,只返回純 JSON 格式,不要有任何其他文字。`,
|
||||||
|
|
||||||
|
deptFunction: `你是專業人資顧問,熟悉半導體製造業。請生成部門職責資料。
|
||||||
|
|
||||||
|
請生成以下欄位:deptFunctionCode, deptFunctionName, deptFunctionBU, deptFunctionDept, deptManager, deptMission, deptVision, deptCoreFunctions, deptKPIs
|
||||||
|
|
||||||
|
欄位說明:
|
||||||
|
- deptFunctionCode: 部門職責編號(格式如 DF001)
|
||||||
|
- deptFunctionName: 部門職責名稱
|
||||||
|
- deptFunctionBU: 事業單位
|
||||||
|
- deptFunctionDept: 部門名稱
|
||||||
|
- deptManager: 部門主管
|
||||||
|
- deptMission: 部門使命
|
||||||
|
- deptVision: 部門願景
|
||||||
|
- deptCoreFunctions: 核心職能(條列式)
|
||||||
|
- deptKPIs: KPI 指標(條列式)
|
||||||
|
|
||||||
|
請用繁體中文,只返回純 JSON 格式,不要有任何其他文字。`,
|
||||||
|
|
||||||
|
jobDesc: `你是專業人資顧問,熟悉半導體製造業。請生成崗位描述資料。
|
||||||
|
|
||||||
|
請生成以下欄位:positionName, department, directSupervisor, positionPurpose, mainResponsibilities, education, basicSkills, professionalKnowledge
|
||||||
|
|
||||||
|
欄位說明:
|
||||||
|
- positionName: 崗位名稱
|
||||||
|
- department: 所屬部門
|
||||||
|
- directSupervisor: 直屬主管
|
||||||
|
- positionPurpose: 崗位目的
|
||||||
|
- mainResponsibilities: 主要職責(條列式)
|
||||||
|
- education: 學歷要求
|
||||||
|
- basicSkills: 基本技能(條列式)
|
||||||
|
- professionalKnowledge: 專業知識(條列式)
|
||||||
|
|
||||||
|
請用繁體中文,只返回純 JSON 格式,不要有任何其他文字。`
|
||||||
|
};
|
||||||
212
js/ui.js
212
js/ui.js
@@ -49,22 +49,51 @@ export function getPositionFormData() {
|
|||||||
const formData = new FormData(form);
|
const formData = new FormData(form);
|
||||||
const data = { basicInfo: {}, recruitInfo: {} };
|
const data = { basicInfo: {}, recruitInfo: {} };
|
||||||
|
|
||||||
const basicFields = ['positionCode', 'positionName', 'positionCategory', 'positionCategoryName',
|
// 使用新的 pos_ prefix 欄位
|
||||||
'positionNature', 'positionNatureName', 'headcount', 'positionLevel',
|
const basicFieldMapping = {
|
||||||
'effectiveDate', 'positionDesc', 'positionRemark'];
|
'pos_code': 'positionCode',
|
||||||
const recruitFields = ['minEducation', 'requiredGender', 'salaryRange', 'workExperience',
|
'pos_name': 'positionName',
|
||||||
'minAge', 'maxAge', 'jobType', 'recruitPosition', 'jobTitle', 'jobDesc',
|
'pos_category': 'positionCategory',
|
||||||
'positionReq', 'titleReq', 'majorReq', 'skillReq', 'langReq', 'otherReq',
|
'pos_categoryName': 'positionCategoryName',
|
||||||
'superiorPosition', 'recruitRemark'];
|
'pos_type': 'positionType',
|
||||||
|
'pos_typeName': 'positionTypeName',
|
||||||
|
'pos_headcount': 'headcount',
|
||||||
|
'pos_level': 'positionLevel',
|
||||||
|
'pos_effectiveDate': 'effectiveDate',
|
||||||
|
'pos_desc': 'positionDesc',
|
||||||
|
'pos_remark': 'positionRemark'
|
||||||
|
};
|
||||||
|
|
||||||
basicFields.forEach(field => {
|
// 使用新的 rec_ prefix 欄位
|
||||||
const value = formData.get(field);
|
const recruitFieldMapping = {
|
||||||
if (value) data.basicInfo[field] = value;
|
'rec_eduLevel': 'minEducation',
|
||||||
|
'rec_gender': 'requiredGender',
|
||||||
|
'rec_salaryRange': 'salaryRange',
|
||||||
|
'rec_expYears': 'workExperience',
|
||||||
|
'rec_minAge': 'minAge',
|
||||||
|
'rec_maxAge': 'maxAge',
|
||||||
|
'rec_jobType': 'jobType',
|
||||||
|
'rec_position': 'recruitPosition',
|
||||||
|
'rec_jobTitle': 'jobTitle',
|
||||||
|
'rec_jobDesc': 'jobDesc',
|
||||||
|
'rec_positionReq': 'positionReq',
|
||||||
|
'rec_certReq': 'certReq',
|
||||||
|
'rec_majorReq': 'majorReq',
|
||||||
|
'rec_skillReq': 'skillReq',
|
||||||
|
'rec_langReq': 'langReq',
|
||||||
|
'rec_otherReq': 'otherReq',
|
||||||
|
'rec_superiorCode': 'superiorPosition',
|
||||||
|
'rec_remark': 'recruitRemark'
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.entries(basicFieldMapping).forEach(([htmlId, dataKey]) => {
|
||||||
|
const el = document.getElementById(htmlId);
|
||||||
|
if (el && el.value) data.basicInfo[dataKey] = el.value;
|
||||||
});
|
});
|
||||||
|
|
||||||
recruitFields.forEach(field => {
|
Object.entries(recruitFieldMapping).forEach(([htmlId, dataKey]) => {
|
||||||
const value = formData.get(field);
|
const el = document.getElementById(htmlId);
|
||||||
if (value) data.recruitInfo[field] = value;
|
if (el && el.value) data.recruitInfo[dataKey] = el.value;
|
||||||
});
|
});
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
@@ -76,19 +105,31 @@ export function getPositionFormData() {
|
|||||||
*/
|
*/
|
||||||
export function getJobFormData() {
|
export function getJobFormData() {
|
||||||
const form = document.getElementById('jobForm');
|
const form = document.getElementById('jobForm');
|
||||||
const formData = new FormData(form);
|
|
||||||
const data = {};
|
const data = {};
|
||||||
|
|
||||||
const fields = ['jobCategoryCode', 'jobCategoryName', 'jobCode', 'jobName', 'jobNameEn',
|
// 使用新的 job_ prefix 欄位
|
||||||
'jobEffectiveDate', 'jobHeadcount', 'jobSortOrder', 'jobRemark', 'jobLevel'];
|
const fieldMapping = {
|
||||||
|
'job_category': 'jobCategoryCode',
|
||||||
|
'job_categoryName': 'jobCategoryName',
|
||||||
|
'job_code': 'jobCode',
|
||||||
|
'job_name': 'jobName',
|
||||||
|
'job_nameEn': 'jobNameEn',
|
||||||
|
'job_level': 'jobLevel',
|
||||||
|
'job_effectiveDate': 'jobEffectiveDate',
|
||||||
|
'job_sortOrder': 'jobSortOrder',
|
||||||
|
'job_headcount': 'jobHeadcount',
|
||||||
|
'job_remark': 'jobRemark'
|
||||||
|
};
|
||||||
|
|
||||||
fields.forEach(field => {
|
Object.entries(fieldMapping).forEach(([htmlId, dataKey]) => {
|
||||||
const value = formData.get(field);
|
const el = document.getElementById(htmlId);
|
||||||
if (value) data[field] = value;
|
if (el && el.value) data[dataKey] = el.value;
|
||||||
});
|
});
|
||||||
|
|
||||||
data.hasAttendanceBonus = document.getElementById('hasAttendanceBonus').checked;
|
const hasAttBonus = document.getElementById('job_hasAttBonus');
|
||||||
data.hasHousingAllowance = document.getElementById('hasHousingAllowance').checked;
|
const hasHouseAllow = document.getElementById('job_hasHouseAllow');
|
||||||
|
data.hasAttendanceBonus = hasAttBonus ? hasAttBonus.checked : false;
|
||||||
|
data.hasHousingAllowance = hasHouseAllow ? hasHouseAllow.checked : false;
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
@@ -101,33 +142,59 @@ export function getJobDescFormData() {
|
|||||||
const form = document.getElementById('jobDescForm');
|
const form = document.getElementById('jobDescForm');
|
||||||
if (!form) return {};
|
if (!form) return {};
|
||||||
|
|
||||||
const formData = new FormData(form);
|
|
||||||
const data = { basicInfo: {}, positionInfo: {}, responsibilities: {}, requirements: {} };
|
const data = { basicInfo: {}, positionInfo: {}, responsibilities: {}, requirements: {} };
|
||||||
|
|
||||||
// Basic Info
|
// Basic Info - 使用新的 jd_ prefix
|
||||||
['empNo', 'empName', 'positionCode', 'versionDate'].forEach(field => {
|
const basicMapping = {
|
||||||
const el = document.getElementById('jd_' + field);
|
'jd_empNo': 'empNo',
|
||||||
if (el && el.value) data.basicInfo[field] = el.value;
|
'jd_empName': 'empName',
|
||||||
|
'jd_posCode': 'positionCode',
|
||||||
|
'jd_versionDate': 'versionDate'
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.entries(basicMapping).forEach(([htmlId, dataKey]) => {
|
||||||
|
const el = document.getElementById(htmlId);
|
||||||
|
if (el && el.value) data.basicInfo[dataKey] = el.value;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Position Info
|
// Position Info - 使用新的 jd_ prefix
|
||||||
['positionName', 'department', 'positionEffectiveDate', 'directSupervisor',
|
const posInfoMapping = {
|
||||||
'positionGradeJob', 'reportTo', 'directReports', 'workLocation', 'empAttribute'].forEach(field => {
|
'jd_posName': 'positionName',
|
||||||
const el = document.getElementById('jd_' + field);
|
'jd_department': 'department',
|
||||||
if (el && el.value) data.positionInfo[field] = el.value;
|
'jd_posLevel': 'positionLevel',
|
||||||
|
'jd_posEffDate': 'positionEffectiveDate',
|
||||||
|
'jd_supervisor': 'directSupervisor',
|
||||||
|
'jd_gradeJob': 'positionGradeJob',
|
||||||
|
'jd_reportTo': 'reportTo',
|
||||||
|
'jd_directReports': 'directReports',
|
||||||
|
'jd_location': 'workLocation',
|
||||||
|
'jd_empAttr': 'empAttribute'
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.entries(posInfoMapping).forEach(([htmlId, dataKey]) => {
|
||||||
|
const el = document.getElementById(htmlId);
|
||||||
|
if (el && el.value) data.positionInfo[dataKey] = el.value;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Purpose & Responsibilities
|
// Purpose & Responsibilities - 使用新的 jd_ prefix
|
||||||
const purpose = document.getElementById('jd_positionPurpose');
|
const purpose = document.getElementById('jd_purpose');
|
||||||
if (purpose && purpose.value) data.responsibilities.positionPurpose = purpose.value;
|
if (purpose && purpose.value) data.responsibilities.positionPurpose = purpose.value;
|
||||||
|
|
||||||
const mainResp = document.getElementById('jd_mainResponsibilities');
|
const mainResp = document.getElementById('jd_mainResp');
|
||||||
if (mainResp && mainResp.value) data.responsibilities.mainResponsibilities = mainResp.value;
|
if (mainResp && mainResp.value) data.responsibilities.mainResponsibilities = mainResp.value;
|
||||||
|
|
||||||
// Requirements
|
// Requirements - 使用新的 jd_ prefix
|
||||||
['education', 'basicSkills', 'professionalKnowledge', 'workExperienceReq', 'otherRequirements'].forEach(field => {
|
const reqMapping = {
|
||||||
const el = document.getElementById('jd_' + field);
|
'jd_eduLevel': 'education',
|
||||||
if (el && el.value) data.requirements[field] = el.value;
|
'jd_basicSkills': 'basicSkills',
|
||||||
|
'jd_proKnowledge': 'professionalKnowledge',
|
||||||
|
'jd_expReq': 'workExperienceReq',
|
||||||
|
'jd_otherReq': 'otherRequirements'
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.entries(reqMapping).forEach(([htmlId, dataKey]) => {
|
||||||
|
const el = document.getElementById(htmlId);
|
||||||
|
if (el && el.value) data.requirements[dataKey] = el.value;
|
||||||
});
|
});
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
@@ -141,16 +208,33 @@ export function getDeptFunctionFormData() {
|
|||||||
const form = document.getElementById('deptFunctionForm');
|
const form = document.getElementById('deptFunctionForm');
|
||||||
if (!form) return {};
|
if (!form) return {};
|
||||||
|
|
||||||
const formData = new FormData(form);
|
|
||||||
const data = {};
|
const data = {};
|
||||||
|
|
||||||
const fields = ['deptFunctionCode', 'deptFunctionName', 'deptFunctionBU',
|
// 使用新的 df_ prefix 欄位
|
||||||
'deptFunctionDept', 'deptManager', 'deptMission', 'deptVision',
|
const fieldMapping = {
|
||||||
'deptCoreFunctions', 'deptKPIs'];
|
'df_code': 'dfCode',
|
||||||
|
'df_name': 'dfName',
|
||||||
|
'df_businessUnit': 'businessUnit',
|
||||||
|
'df_division': 'division',
|
||||||
|
'df_department': 'department',
|
||||||
|
'df_section': 'section',
|
||||||
|
'df_posTitle': 'positionTitle',
|
||||||
|
'df_posLevel': 'positionLevel',
|
||||||
|
'df_managerTitle': 'managerTitle',
|
||||||
|
'df_effectiveDate': 'effectiveDate',
|
||||||
|
'df_headcountLimit': 'headcountLimit',
|
||||||
|
'df_status': 'status',
|
||||||
|
'df_mission': 'mission',
|
||||||
|
'df_vision': 'vision',
|
||||||
|
'df_coreFunc': 'coreFunctions',
|
||||||
|
'df_kpis': 'kpis',
|
||||||
|
'df_collab': 'collaboration',
|
||||||
|
'df_remark': 'remark'
|
||||||
|
};
|
||||||
|
|
||||||
fields.forEach(field => {
|
Object.entries(fieldMapping).forEach(([htmlId, dataKey]) => {
|
||||||
const value = formData.get(field);
|
const el = document.getElementById(htmlId);
|
||||||
if (value) data[field] = value;
|
if (el && el.value) data[dataKey] = el.value;
|
||||||
});
|
});
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
@@ -192,8 +276,11 @@ export function updatePreview() {
|
|||||||
* 更新崗位類別中文名稱
|
* 更新崗位類別中文名稱
|
||||||
*/
|
*/
|
||||||
export function updateCategoryName() {
|
export function updateCategoryName() {
|
||||||
const category = document.getElementById('positionCategory').value;
|
const categoryEl = document.getElementById('pos_category');
|
||||||
document.getElementById('positionCategoryName').value = categoryMap[category] || '';
|
const categoryNameEl = document.getElementById('pos_categoryName');
|
||||||
|
if (categoryEl && categoryNameEl) {
|
||||||
|
categoryNameEl.value = categoryMap[categoryEl.value] || '';
|
||||||
|
}
|
||||||
updatePreview();
|
updatePreview();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,8 +288,11 @@ export function updateCategoryName() {
|
|||||||
* 更新崗位性質中文名稱
|
* 更新崗位性質中文名稱
|
||||||
*/
|
*/
|
||||||
export function updateNatureName() {
|
export function updateNatureName() {
|
||||||
const nature = document.getElementById('positionNature').value;
|
const typeEl = document.getElementById('pos_type');
|
||||||
document.getElementById('positionNatureName').value = natureMap[nature] || '';
|
const typeNameEl = document.getElementById('pos_typeName');
|
||||||
|
if (typeEl && typeNameEl) {
|
||||||
|
typeNameEl.value = natureMap[typeEl.value] || '';
|
||||||
|
}
|
||||||
updatePreview();
|
updatePreview();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,8 +300,11 @@ export function updateNatureName() {
|
|||||||
* 更新職務類別中文名稱
|
* 更新職務類別中文名稱
|
||||||
*/
|
*/
|
||||||
export function updateJobCategoryName() {
|
export function updateJobCategoryName() {
|
||||||
const category = document.getElementById('jobCategoryCode').value;
|
const categoryEl = document.getElementById('job_category');
|
||||||
document.getElementById('jobCategoryName').value = jobCategoryMap[category] || '';
|
const categoryNameEl = document.getElementById('job_categoryName');
|
||||||
|
if (categoryEl && categoryNameEl) {
|
||||||
|
categoryNameEl.value = jobCategoryMap[categoryEl.value] || '';
|
||||||
|
}
|
||||||
updatePreview();
|
updatePreview();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,10 +312,12 @@ export function updateJobCategoryName() {
|
|||||||
* 修改崗位編號
|
* 修改崗位編號
|
||||||
*/
|
*/
|
||||||
export function changePositionCode() {
|
export function changePositionCode() {
|
||||||
const currentCode = document.getElementById('positionCode').value;
|
const codeEl = document.getElementById('pos_code');
|
||||||
|
if (!codeEl) return;
|
||||||
|
const currentCode = codeEl.value;
|
||||||
const newCode = prompt('請輸入新的崗位編號:', currentCode);
|
const newCode = prompt('請輸入新的崗位編號:', currentCode);
|
||||||
if (newCode && newCode !== currentCode) {
|
if (newCode && newCode !== currentCode) {
|
||||||
document.getElementById('positionCode').value = newCode;
|
codeEl.value = newCode;
|
||||||
showToast('崗位編號已更改!');
|
showToast('崗位編號已更改!');
|
||||||
updatePreview();
|
updatePreview();
|
||||||
}
|
}
|
||||||
@@ -232,10 +327,12 @@ export function changePositionCode() {
|
|||||||
* 修改職務編號
|
* 修改職務編號
|
||||||
*/
|
*/
|
||||||
export function changeJobCode() {
|
export function changeJobCode() {
|
||||||
const currentCode = document.getElementById('jobCode').value;
|
const codeEl = document.getElementById('job_code');
|
||||||
|
if (!codeEl) return;
|
||||||
|
const currentCode = codeEl.value;
|
||||||
const newCode = prompt('請輸入新的職務編號:', currentCode);
|
const newCode = prompt('請輸入新的職務編號:', currentCode);
|
||||||
if (newCode && newCode !== currentCode) {
|
if (newCode && newCode !== currentCode) {
|
||||||
document.getElementById('jobCode').value = newCode;
|
codeEl.value = newCode;
|
||||||
showToast('職務編號已更改!');
|
showToast('職務編號已更改!');
|
||||||
updatePreview();
|
updatePreview();
|
||||||
}
|
}
|
||||||
@@ -271,7 +368,10 @@ export function confirmMajor() {
|
|||||||
document.querySelectorAll('#majorModal input[type="checkbox"]:checked').forEach(cb => {
|
document.querySelectorAll('#majorModal input[type="checkbox"]:checked').forEach(cb => {
|
||||||
selected.push(cb.value);
|
selected.push(cb.value);
|
||||||
});
|
});
|
||||||
document.getElementById('majorReq').value = selected.join(', ');
|
const majorReqEl = document.getElementById('rec_majorReq');
|
||||||
|
if (majorReqEl) {
|
||||||
|
majorReqEl.value = selected.join(', ');
|
||||||
|
}
|
||||||
closeMajorModal();
|
closeMajorModal();
|
||||||
updatePreview();
|
updatePreview();
|
||||||
}
|
}
|
||||||
|
|||||||
36
js/utils.js
36
js/utils.js
@@ -132,39 +132,39 @@ export function showCopyableError(options) {
|
|||||||
|
|
||||||
const modalHTML = `
|
const modalHTML = `
|
||||||
<div id="errorModal" class="modal-overlay show">
|
<div id="errorModal" class="modal-overlay show">
|
||||||
<div class="modal" style="max-width: 600px;">
|
<div class="modal" style="max-width: min(90vw, 500px); max-height: 85vh; display: flex; flex-direction: column;">
|
||||||
<div class="modal-header" style="background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%); color: white;">
|
<div class="modal-header" style="background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%); color: white; flex-shrink: 0;">
|
||||||
<h3>🚨 ${sanitizeHTML(title)}</h3>
|
<h3 style="font-size: 1rem;">🚨 ${sanitizeHTML(title)}</h3>
|
||||||
<button class="modal-close" onclick="closeErrorModal(this)" style="color: white;">✕</button>
|
<button class="modal-close" onclick="closeErrorModal(this)" style="color: white;">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body" style="overflow-y: auto; flex: 1; padding: 16px;">
|
||||||
<div style="margin-bottom: 16px;">
|
<div style="margin-bottom: 12px;">
|
||||||
<strong>錯誤訊息:</strong>
|
<strong style="font-size: 0.9rem;">錯誤訊息:</strong>
|
||||||
<p style="color: #e74c3c; font-weight: 500;">${sanitizeHTML(message)}</p>
|
<p style="color: #e74c3c; font-weight: 500; font-size: 0.85rem; word-break: break-word;">${sanitizeHTML(message)}</p>
|
||||||
</div>
|
</div>
|
||||||
${details ? `
|
${details ? `
|
||||||
<div style="margin-bottom: 16px;">
|
<div style="margin-bottom: 12px;">
|
||||||
<strong>詳細資訊:</strong>
|
<strong style="font-size: 0.9rem;">詳細資訊:</strong>
|
||||||
<pre style="background: #f8f9fa; padding: 12px; border-radius: 6px; overflow-x: auto; font-size: 0.85rem; max-height: 200px;">${sanitizeHTML(details)}</pre>
|
<pre style="background: #f8f9fa; padding: 10px; border-radius: 6px; overflow-x: auto; font-size: 0.75rem; max-height: 120px; white-space: pre-wrap; word-break: break-all;">${sanitizeHTML(details)}</pre>
|
||||||
</div>
|
</div>
|
||||||
` : ''}
|
` : ''}
|
||||||
${suggestions && suggestions.length > 0 ? `
|
${suggestions && suggestions.length > 0 ? `
|
||||||
<div>
|
<div>
|
||||||
<strong>請檢查以下項目:</strong>
|
<strong style="font-size: 0.9rem;">請檢查以下項目:</strong>
|
||||||
<ul style="margin: 8px 0; padding-left: 20px;">
|
<ul style="margin: 6px 0; padding-left: 18px; font-size: 0.85rem;">
|
||||||
${suggestions.map(s => `<li style="margin: 4px 0;">${sanitizeHTML(s)}</li>`).join('')}
|
${suggestions.map(s => `<li style="margin: 3px 0;">${sanitizeHTML(s)}</li>`).join('')}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
` : ''}
|
` : ''}
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer" style="flex-shrink: 0; padding: 12px 16px;">
|
||||||
<button class="btn btn-secondary" onclick="copyErrorDetails()">
|
<button class="btn btn-secondary" onclick="copyErrorDetails()" style="font-size: 0.85rem; padding: 8px 12px;">
|
||||||
<svg viewBox="0 0 24 24" style="width: 16px; height: 16px;">
|
<svg viewBox="0 0 24 24" style="width: 14px; height: 14px;">
|
||||||
<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"/>
|
<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>
|
</svg>
|
||||||
複製錯誤詳情
|
複製
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-primary" onclick="closeErrorModal(this)">關閉</button>
|
<button class="btn btn-primary" onclick="closeErrorModal(this)" style="font-size: 0.85rem; padding: 8px 16px;">關閉</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -355,7 +355,8 @@ class LLMConfig:
|
|||||||
"temperature": 0.7
|
"temperature": 0.7
|
||||||
}
|
}
|
||||||
|
|
||||||
response = requests.post(url, json=data, headers=headers, timeout=60, verify=False)
|
# 增加逾時時間到 180 秒,處理大模型較慢的情況
|
||||||
|
response = requests.post(url, json=data, headers=headers, timeout=180, verify=False)
|
||||||
|
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
result = response.json()
|
result = response.json()
|
||||||
@@ -364,6 +365,10 @@ class LLMConfig:
|
|||||||
else:
|
else:
|
||||||
return False, f"生成失敗 (HTTP {response.status_code}): {response.text}"
|
return False, f"生成失敗 (HTTP {response.status_code}): {response.text}"
|
||||||
|
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
return False, f"連線逾時 (180秒),伺服器可能過載或網路不穩定。建議:1) 稍後重試 2) 使用本地 Ollama (localhost:11434) 3) 切換到 Gemini API"
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
return False, "無法連接到 Ollama 伺服器,請確認伺服器地址正確且服務已啟動"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return False, f"錯誤: {str(e)}"
|
return False, f"錯誤: {str(e)}"
|
||||||
|
|
||||||
@@ -395,7 +400,8 @@ class LLMConfig:
|
|||||||
"temperature": 0.7
|
"temperature": 0.7
|
||||||
}
|
}
|
||||||
|
|
||||||
response = requests.post(url, json=data, headers=headers, timeout=60, verify=False)
|
# 增加逾時時間到 180 秒
|
||||||
|
response = requests.post(url, json=data, headers=headers, timeout=180, verify=False)
|
||||||
|
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
result = response.json()
|
result = response.json()
|
||||||
@@ -404,6 +410,10 @@ class LLMConfig:
|
|||||||
else:
|
else:
|
||||||
return False, f"生成失敗 (HTTP {response.status_code}): {response.text}"
|
return False, f"生成失敗 (HTTP {response.status_code}): {response.text}"
|
||||||
|
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
return False, f"連線逾時 (180秒),伺服器可能過載或網路不穩定。建議:1) 稍後重試 2) 使用本地 Ollama (localhost:11434) 3) 切換到 Gemini API"
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
return False, "無法連接到 GPT-OSS 伺服器,請確認伺服器地址正確且服務已啟動"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return False, f"錯誤: {str(e)}"
|
return False, f"錯誤: {str(e)}"
|
||||||
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
你好!謝謝你的問候。作為一個人工智能助手,我沒有情緒或身體感受,但我的程式運行得很順利,隨時準備為你提供幫助~ 你呢?今天過得如何?有什麼我可以為你解答的嗎? 😊
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"error":"\u627e\u4e0d\u5230\u8a72\u5d17\u4f4d\u8cc7\u6599","success":false}
|
|
||||||
|
Can't render this file because it contains an unexpected character in line 1 and column 2.
|
387
scripts/import_hierarchy_data.py
Normal file
387
scripts/import_hierarchy_data.py
Normal file
@@ -0,0 +1,387 @@
|
|||||||
|
"""
|
||||||
|
匯入組織階層資料到資料庫
|
||||||
|
從 hierarchical_data.js 讀取資料並匯入 MySQL
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import json
|
||||||
|
import pymysql
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Load environment variables
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
# 從 hierarchical_data.js 解析資料
|
||||||
|
def parse_hierarchical_data():
|
||||||
|
"""解析 hierarchical_data.js 檔案"""
|
||||||
|
js_file = os.path.join(os.path.dirname(__file__), 'hierarchical_data.js')
|
||||||
|
|
||||||
|
with open(js_file, 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# 提取 businessToDivision
|
||||||
|
business_match = re.search(r'const businessToDivision = ({[\s\S]*?});', content)
|
||||||
|
business_to_division = json.loads(business_match.group(1).replace("'", '"')) if business_match else {}
|
||||||
|
|
||||||
|
# 提取 divisionToDepartment
|
||||||
|
division_match = re.search(r'const divisionToDepartment = ({[\s\S]*?});', content)
|
||||||
|
division_to_department = json.loads(division_match.group(1).replace("'", '"')) if division_match else {}
|
||||||
|
|
||||||
|
# 提取 departmentToPosition
|
||||||
|
dept_match = re.search(r'const departmentToPosition = ({[\s\S]*?});', content)
|
||||||
|
department_to_position = json.loads(dept_match.group(1).replace("'", '"')) if dept_match else {}
|
||||||
|
|
||||||
|
# 提取 fullHierarchyData
|
||||||
|
hierarchy_match = re.search(r'const fullHierarchyData = (\[[\s\S]*?\]);', content)
|
||||||
|
full_hierarchy_data = json.loads(hierarchy_match.group(1)) if hierarchy_match else []
|
||||||
|
|
||||||
|
return {
|
||||||
|
'businessToDivision': business_to_division,
|
||||||
|
'divisionToDepartment': division_to_department,
|
||||||
|
'departmentToPosition': department_to_position,
|
||||||
|
'fullHierarchyData': full_hierarchy_data
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def generate_code(prefix, index):
|
||||||
|
"""生成代碼"""
|
||||||
|
return f"{prefix}{index:03d}"
|
||||||
|
|
||||||
|
|
||||||
|
def import_to_database():
|
||||||
|
"""匯入資料到資料庫"""
|
||||||
|
|
||||||
|
# Database connection parameters
|
||||||
|
db_config = {
|
||||||
|
'host': os.getenv('DB_HOST', 'localhost'),
|
||||||
|
'port': int(os.getenv('DB_PORT', 3306)),
|
||||||
|
'user': os.getenv('DB_USER', 'root'),
|
||||||
|
'password': os.getenv('DB_PASSWORD', ''),
|
||||||
|
'database': os.getenv('DB_NAME', 'hr_position_system'),
|
||||||
|
'charset': 'utf8mb4',
|
||||||
|
'cursorclass': pymysql.cursors.DictCursor
|
||||||
|
}
|
||||||
|
|
||||||
|
print("=" * 60)
|
||||||
|
print("組織階層資料匯入工具")
|
||||||
|
print("=" * 60)
|
||||||
|
print()
|
||||||
|
|
||||||
|
# 解析 JS 資料
|
||||||
|
print("步驟 1: 解析 hierarchical_data.js...")
|
||||||
|
data = parse_hierarchical_data()
|
||||||
|
|
||||||
|
print(f" - 事業體數量: {len(data['businessToDivision'])}")
|
||||||
|
print(f" - 處級單位對應數: {len(data['divisionToDepartment'])}")
|
||||||
|
print(f" - 部級單位對應數: {len(data['departmentToPosition'])}")
|
||||||
|
print(f" - 完整階層記錄數: {len(data['fullHierarchyData'])}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 連接資料庫
|
||||||
|
print("步驟 2: 連接資料庫...")
|
||||||
|
connection = pymysql.connect(**db_config)
|
||||||
|
cursor = connection.cursor()
|
||||||
|
print(" 連接成功")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# 先建立資料表(如果不存在)
|
||||||
|
print("步驟 3: 確認資料表存在...")
|
||||||
|
create_tables_sql = """
|
||||||
|
-- 事業體表
|
||||||
|
CREATE TABLE IF NOT EXISTS business_units (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
business_code VARCHAR(20) NOT NULL UNIQUE,
|
||||||
|
business_name VARCHAR(100) NOT NULL,
|
||||||
|
sort_order INT DEFAULT 0,
|
||||||
|
is_active BOOLEAN DEFAULT TRUE,
|
||||||
|
remark VARCHAR(500),
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
INDEX idx_business_name (business_name)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- 處級單位表
|
||||||
|
CREATE TABLE IF NOT EXISTS divisions (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
division_code VARCHAR(20) NOT NULL UNIQUE,
|
||||||
|
division_name VARCHAR(100) NOT NULL,
|
||||||
|
business_id INT,
|
||||||
|
sort_order INT DEFAULT 0,
|
||||||
|
is_active BOOLEAN DEFAULT TRUE,
|
||||||
|
remark VARCHAR(500),
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
INDEX idx_division_name (division_name),
|
||||||
|
INDEX idx_business_id (business_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- 部級單位表
|
||||||
|
CREATE TABLE IF NOT EXISTS departments (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
department_code VARCHAR(20) NOT NULL UNIQUE,
|
||||||
|
department_name VARCHAR(100) NOT NULL,
|
||||||
|
division_id INT,
|
||||||
|
sort_order INT DEFAULT 0,
|
||||||
|
is_active BOOLEAN DEFAULT TRUE,
|
||||||
|
remark VARCHAR(500),
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
INDEX idx_department_name (department_name),
|
||||||
|
INDEX idx_division_id (division_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- 組織崗位關聯表
|
||||||
|
CREATE TABLE IF NOT EXISTS organization_positions (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
business_id INT NOT NULL,
|
||||||
|
division_id INT NOT NULL,
|
||||||
|
department_id INT NOT NULL,
|
||||||
|
position_title VARCHAR(100) NOT NULL,
|
||||||
|
sort_order INT DEFAULT 0,
|
||||||
|
is_active BOOLEAN DEFAULT TRUE,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
INDEX idx_business_id (business_id),
|
||||||
|
INDEX idx_division_id (division_id),
|
||||||
|
INDEX idx_department_id (department_id),
|
||||||
|
INDEX idx_position_title (position_title),
|
||||||
|
UNIQUE KEY uk_org_position (business_id, division_id, department_id, position_title)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
"""
|
||||||
|
|
||||||
|
for statement in create_tables_sql.split(';'):
|
||||||
|
if statement.strip():
|
||||||
|
try:
|
||||||
|
cursor.execute(statement)
|
||||||
|
except Exception as e:
|
||||||
|
pass # 表已存在時忽略錯誤
|
||||||
|
|
||||||
|
connection.commit()
|
||||||
|
print(" 資料表已確認")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# 清空現有資料
|
||||||
|
print("步驟 4: 清空現有資料...")
|
||||||
|
cursor.execute("SET FOREIGN_KEY_CHECKS = 0")
|
||||||
|
cursor.execute("TRUNCATE TABLE organization_positions")
|
||||||
|
cursor.execute("TRUNCATE TABLE departments")
|
||||||
|
cursor.execute("TRUNCATE TABLE divisions")
|
||||||
|
cursor.execute("TRUNCATE TABLE business_units")
|
||||||
|
cursor.execute("SET FOREIGN_KEY_CHECKS = 1")
|
||||||
|
connection.commit()
|
||||||
|
print(" 資料已清空")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# 匯入事業體
|
||||||
|
print("步驟 5: 匯入事業體...")
|
||||||
|
business_id_map = {}
|
||||||
|
business_list = list(data['businessToDivision'].keys())
|
||||||
|
|
||||||
|
for idx, business_name in enumerate(business_list, 1):
|
||||||
|
code = generate_code('BU', idx)
|
||||||
|
cursor.execute(
|
||||||
|
"INSERT INTO business_units (business_code, business_name, sort_order) VALUES (%s, %s, %s)",
|
||||||
|
(code, business_name, idx)
|
||||||
|
)
|
||||||
|
business_id_map[business_name] = cursor.lastrowid
|
||||||
|
|
||||||
|
connection.commit()
|
||||||
|
print(f" 匯入 {len(business_list)} 筆事業體")
|
||||||
|
|
||||||
|
# 匯入處級單位
|
||||||
|
print("步驟 6: 匯入處級單位...")
|
||||||
|
division_id_map = {}
|
||||||
|
division_idx = 0
|
||||||
|
|
||||||
|
for business_name, divisions in data['businessToDivision'].items():
|
||||||
|
business_id = business_id_map.get(business_name)
|
||||||
|
for division_name in divisions:
|
||||||
|
if division_name not in division_id_map:
|
||||||
|
division_idx += 1
|
||||||
|
code = generate_code('DIV', division_idx)
|
||||||
|
cursor.execute(
|
||||||
|
"INSERT INTO divisions (division_code, division_name, business_id, sort_order) VALUES (%s, %s, %s, %s)",
|
||||||
|
(code, division_name, business_id, division_idx)
|
||||||
|
)
|
||||||
|
division_id_map[division_name] = cursor.lastrowid
|
||||||
|
|
||||||
|
connection.commit()
|
||||||
|
print(f" 匯入 {division_idx} 筆處級單位")
|
||||||
|
|
||||||
|
# 匯入部級單位
|
||||||
|
print("步驟 7: 匯入部級單位...")
|
||||||
|
department_id_map = {}
|
||||||
|
dept_idx = 0
|
||||||
|
|
||||||
|
for division_name, departments in data['divisionToDepartment'].items():
|
||||||
|
division_id = division_id_map.get(division_name)
|
||||||
|
for dept_name in departments:
|
||||||
|
if dept_name not in department_id_map:
|
||||||
|
dept_idx += 1
|
||||||
|
code = generate_code('DEPT', dept_idx)
|
||||||
|
cursor.execute(
|
||||||
|
"INSERT INTO departments (department_code, department_name, division_id, sort_order) VALUES (%s, %s, %s, %s)",
|
||||||
|
(code, dept_name, division_id, dept_idx)
|
||||||
|
)
|
||||||
|
department_id_map[dept_name] = cursor.lastrowid
|
||||||
|
|
||||||
|
connection.commit()
|
||||||
|
print(f" 匯入 {dept_idx} 筆部級單位")
|
||||||
|
|
||||||
|
# 匯入組織崗位關聯
|
||||||
|
print("步驟 8: 匯入組織崗位關聯...")
|
||||||
|
position_count = 0
|
||||||
|
inserted_combinations = set()
|
||||||
|
|
||||||
|
for record in data['fullHierarchyData']:
|
||||||
|
business_name = record.get('business')
|
||||||
|
division_name = record.get('division')
|
||||||
|
department_name = record.get('department')
|
||||||
|
position_title = record.get('position')
|
||||||
|
|
||||||
|
business_id = business_id_map.get(business_name)
|
||||||
|
division_id = division_id_map.get(division_name)
|
||||||
|
department_id = department_id_map.get(department_name)
|
||||||
|
|
||||||
|
if not all([business_id, division_id, department_id, position_title]):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 避免重複插入
|
||||||
|
combination_key = (business_id, division_id, department_id, position_title)
|
||||||
|
if combination_key in inserted_combinations:
|
||||||
|
continue
|
||||||
|
|
||||||
|
inserted_combinations.add(combination_key)
|
||||||
|
|
||||||
|
try:
|
||||||
|
cursor.execute(
|
||||||
|
"""INSERT INTO organization_positions
|
||||||
|
(business_id, division_id, department_id, position_title, sort_order)
|
||||||
|
VALUES (%s, %s, %s, %s, %s)""",
|
||||||
|
(business_id, division_id, department_id, position_title, position_count + 1)
|
||||||
|
)
|
||||||
|
position_count += 1
|
||||||
|
except pymysql.err.IntegrityError:
|
||||||
|
pass # 重複記錄跳過
|
||||||
|
|
||||||
|
connection.commit()
|
||||||
|
print(f" 匯入 {position_count} 筆組織崗位關聯")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# 顯示統計
|
||||||
|
print("=" * 60)
|
||||||
|
print("匯入完成!")
|
||||||
|
print("=" * 60)
|
||||||
|
print()
|
||||||
|
|
||||||
|
# 查詢統計
|
||||||
|
cursor.execute("SELECT COUNT(*) as cnt FROM business_units")
|
||||||
|
business_count = cursor.fetchone()['cnt']
|
||||||
|
|
||||||
|
cursor.execute("SELECT COUNT(*) as cnt FROM divisions")
|
||||||
|
division_count = cursor.fetchone()['cnt']
|
||||||
|
|
||||||
|
cursor.execute("SELECT COUNT(*) as cnt FROM departments")
|
||||||
|
dept_count = cursor.fetchone()['cnt']
|
||||||
|
|
||||||
|
cursor.execute("SELECT COUNT(*) as cnt FROM organization_positions")
|
||||||
|
org_pos_count = cursor.fetchone()['cnt']
|
||||||
|
|
||||||
|
print(f"資料庫統計:")
|
||||||
|
print(f" - 事業體: {business_count} 筆")
|
||||||
|
print(f" - 處級單位: {division_count} 筆")
|
||||||
|
print(f" - 部級單位: {dept_count} 筆")
|
||||||
|
print(f" - 組織崗位關聯: {org_pos_count} 筆")
|
||||||
|
|
||||||
|
cursor.close()
|
||||||
|
connection.close()
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n錯誤: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def import_to_memory():
|
||||||
|
"""匯入資料到記憶體(用於 Flask 應用)"""
|
||||||
|
|
||||||
|
# 解析 JS 資料
|
||||||
|
data = parse_hierarchical_data()
|
||||||
|
|
||||||
|
# 建立記憶體資料結構
|
||||||
|
business_units = {}
|
||||||
|
divisions = {}
|
||||||
|
departments = {}
|
||||||
|
organization_positions = []
|
||||||
|
|
||||||
|
business_idx = 0
|
||||||
|
division_idx = 0
|
||||||
|
dept_idx = 0
|
||||||
|
|
||||||
|
# 處理事業體
|
||||||
|
for business_name in data['businessToDivision'].keys():
|
||||||
|
business_idx += 1
|
||||||
|
business_units[business_name] = {
|
||||||
|
'id': business_idx,
|
||||||
|
'code': generate_code('BU', business_idx),
|
||||||
|
'name': business_name
|
||||||
|
}
|
||||||
|
|
||||||
|
# 處理處級單位
|
||||||
|
division_id_map = {}
|
||||||
|
for business_name, division_list in data['businessToDivision'].items():
|
||||||
|
for div_name in division_list:
|
||||||
|
if div_name not in division_id_map:
|
||||||
|
division_idx += 1
|
||||||
|
division_id_map[div_name] = division_idx
|
||||||
|
divisions[div_name] = {
|
||||||
|
'id': division_idx,
|
||||||
|
'code': generate_code('DIV', division_idx),
|
||||||
|
'name': div_name,
|
||||||
|
'business': business_name
|
||||||
|
}
|
||||||
|
|
||||||
|
# 處理部級單位
|
||||||
|
dept_id_map = {}
|
||||||
|
for div_name, dept_list in data['divisionToDepartment'].items():
|
||||||
|
for dept_name in dept_list:
|
||||||
|
if dept_name not in dept_id_map:
|
||||||
|
dept_idx += 1
|
||||||
|
dept_id_map[dept_name] = dept_idx
|
||||||
|
departments[dept_name] = {
|
||||||
|
'id': dept_idx,
|
||||||
|
'code': generate_code('DEPT', dept_idx),
|
||||||
|
'name': dept_name,
|
||||||
|
'division': div_name
|
||||||
|
}
|
||||||
|
|
||||||
|
# 處理組織崗位關聯
|
||||||
|
seen = set()
|
||||||
|
for record in data['fullHierarchyData']:
|
||||||
|
key = (record['business'], record['division'], record['department'], record['position'])
|
||||||
|
if key not in seen:
|
||||||
|
seen.add(key)
|
||||||
|
organization_positions.append({
|
||||||
|
'business': record['business'],
|
||||||
|
'division': record['division'],
|
||||||
|
'department': record['department'],
|
||||||
|
'position': record['position']
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
'business_units': business_units,
|
||||||
|
'divisions': divisions,
|
||||||
|
'departments': departments,
|
||||||
|
'organization_positions': organization_positions,
|
||||||
|
'businessToDivision': data['businessToDivision'],
|
||||||
|
'divisionToDepartment': data['divisionToDepartment'],
|
||||||
|
'departmentToPosition': data['departmentToPosition']
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import_to_database()
|
||||||
@@ -32,8 +32,8 @@ cors_origins = os.getenv('CORS_ORIGINS', 'http://localhost:5000,http://127.0.0.1
|
|||||||
CORS(app, origins=cors_origins)
|
CORS(app, origins=cors_origins)
|
||||||
|
|
||||||
# 模擬資料庫
|
# 模擬資料庫
|
||||||
positions_db = {}
|
HR_position_positions_db = {}
|
||||||
jobs_db = {}
|
HR_position_jobs_db = {}
|
||||||
|
|
||||||
# 預設崗位資料
|
# 預設崗位資料
|
||||||
default_positions = {
|
default_positions = {
|
||||||
@@ -76,7 +76,7 @@ default_positions = {
|
|||||||
"updatedAt": "2024-01-01T00:00:00"
|
"updatedAt": "2024-01-01T00:00:00"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
positions_db.update(default_positions)
|
HR_position_positions_db.update(default_positions)
|
||||||
|
|
||||||
# 預設職務資料
|
# 預設職務資料
|
||||||
default_jobs = {
|
default_jobs = {
|
||||||
@@ -98,7 +98,7 @@ default_jobs = {
|
|||||||
"updatedAt": "2024-01-01T00:00:00"
|
"updatedAt": "2024-01-01T00:00:00"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
jobs_db.update(default_jobs)
|
HR_position_jobs_db.update(default_jobs)
|
||||||
|
|
||||||
# ==================== 靜態頁面 ====================
|
# ==================== 靜態頁面 ====================
|
||||||
|
|
||||||
@@ -118,7 +118,7 @@ def get_positions():
|
|||||||
size = request.args.get('size', 20, type=int)
|
size = request.args.get('size', 20, type=int)
|
||||||
search = request.args.get('search', '', type=str)
|
search = request.args.get('search', '', type=str)
|
||||||
|
|
||||||
filtered = list(positions_db.values())
|
filtered = list(HR_position_positions_db.values())
|
||||||
if search:
|
if search:
|
||||||
filtered = [p for p in filtered
|
filtered = [p for p in filtered
|
||||||
if search.lower() in p['basicInfo'].get('positionCode', '').lower()
|
if search.lower() in p['basicInfo'].get('positionCode', '').lower()
|
||||||
@@ -142,9 +142,9 @@ def get_positions():
|
|||||||
|
|
||||||
@app.route('/api/positions/<position_id>', methods=['GET'])
|
@app.route('/api/positions/<position_id>', methods=['GET'])
|
||||||
def get_position(position_id):
|
def get_position(position_id):
|
||||||
if position_id not in positions_db:
|
if position_id not in HR_position_positions_db:
|
||||||
return jsonify({'success': False, 'error': '找不到該崗位資料'}), 404
|
return jsonify({'success': False, 'error': '找不到該崗位資料'}), 404
|
||||||
return jsonify({'success': True, 'data': positions_db[position_id]})
|
return jsonify({'success': True, 'data': HR_position_positions_db[position_id]})
|
||||||
|
|
||||||
@app.route('/api/positions', methods=['POST'])
|
@app.route('/api/positions', methods=['POST'])
|
||||||
def create_position():
|
def create_position():
|
||||||
@@ -160,7 +160,7 @@ def create_position():
|
|||||||
return jsonify({'success': False, 'error': '崗位名稱為必填欄位'}), 400
|
return jsonify({'success': False, 'error': '崗位名稱為必填欄位'}), 400
|
||||||
|
|
||||||
position_code = basic_info['positionCode']
|
position_code = basic_info['positionCode']
|
||||||
if position_code in positions_db:
|
if position_code in HR_position_positions_db:
|
||||||
return jsonify({'success': False, 'error': f'崗位編號 {position_code} 已存在'}), 409
|
return jsonify({'success': False, 'error': f'崗位編號 {position_code} 已存在'}), 409
|
||||||
|
|
||||||
now = datetime.now().isoformat()
|
now = datetime.now().isoformat()
|
||||||
@@ -171,7 +171,7 @@ def create_position():
|
|||||||
'createdAt': now,
|
'createdAt': now,
|
||||||
'updatedAt': now
|
'updatedAt': now
|
||||||
}
|
}
|
||||||
positions_db[position_code] = new_position
|
HR_position_positions_db[position_code] = new_position
|
||||||
return jsonify({'success': True, 'message': '崗位資料新增成功', 'data': new_position}), 201
|
return jsonify({'success': True, 'message': '崗位資料新增成功', 'data': new_position}), 201
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'success': False, 'error': f'新增失敗: {str(e)}'}), 500
|
return jsonify({'success': False, 'error': f'新增失敗: {str(e)}'}), 500
|
||||||
|
|||||||
@@ -14,7 +14,39 @@
|
|||||||
--green: #27ae60;
|
--green: #27ae60;
|
||||||
--green-dark: #1e8449;
|
--green-dark: #1e8449;
|
||||||
|
|
||||||
|
/* 模組專屬色彩 */
|
||||||
|
/* 崗位基礎資料 - 藍色 (Primary) */
|
||||||
|
--module-position: #1a5276;
|
||||||
|
--module-position-dark: #0e3a53;
|
||||||
|
--module-position-shadow: rgba(26, 82, 118, 0.4);
|
||||||
|
|
||||||
|
/* 職務基礎資料 - 橘色 */
|
||||||
|
--module-job: #e67e22;
|
||||||
|
--module-job-dark: #d35400;
|
||||||
|
--module-job-shadow: rgba(230, 126, 34, 0.4);
|
||||||
|
|
||||||
|
/* 部門職責 - 紫色 */
|
||||||
|
--module-deptfunc: #8b5cf6;
|
||||||
|
--module-deptfunc-dark: #6d28d9;
|
||||||
|
--module-deptfunc-shadow: rgba(139, 92, 246, 0.4);
|
||||||
|
|
||||||
|
/* 崗位描述 - 綠色 */
|
||||||
|
--module-desc: #27ae60;
|
||||||
|
--module-desc-dark: #1e8449;
|
||||||
|
--module-desc-shadow: rgba(39, 174, 96, 0.4);
|
||||||
|
|
||||||
|
/* 崗位清單 - 青色 */
|
||||||
|
--module-list: #14b8a6;
|
||||||
|
--module-list-dark: #0d9488;
|
||||||
|
--module-list-shadow: rgba(20, 184, 166, 0.4);
|
||||||
|
|
||||||
|
/* 管理者頁面 - 玫紅色 */
|
||||||
|
--module-admin: #f43f5e;
|
||||||
|
--module-admin-dark: #e11d48;
|
||||||
|
--module-admin-shadow: rgba(244, 63, 94, 0.4);
|
||||||
|
|
||||||
/* 背景色 */
|
/* 背景色 */
|
||||||
|
--bg: #f4f6f9;
|
||||||
--bg-main: #f4f6f9;
|
--bg-main: #f4f6f9;
|
||||||
--bg-card: #ffffff;
|
--bg-card: #ffffff;
|
||||||
--border: #d5dbdf;
|
--border: #d5dbdf;
|
||||||
|
|||||||
@@ -142,17 +142,25 @@ select {
|
|||||||
|
|
||||||
/* ==================== Buttons ==================== */
|
/* ==================== Buttons ==================== */
|
||||||
.btn {
|
.btn {
|
||||||
padding: 12px 24px;
|
padding: 10px 20px;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 6px;
|
border-radius: 8px;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
font-size: 0.9rem;
|
font-size: 0.875rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-decoration: none;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:active {
|
||||||
|
transform: scale(0.98);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
@@ -162,8 +170,9 @@ select {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary:hover {
|
.btn-primary:hover {
|
||||||
transform: translateY(-1px);
|
transform: translateY(-2px);
|
||||||
box-shadow: 0 4px 12px rgba(26, 82, 118, 0.4);
|
box-shadow: 0 4px 16px rgba(26, 82, 118, 0.4);
|
||||||
|
filter: brightness(1.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary {
|
.btn-secondary {
|
||||||
@@ -173,28 +182,107 @@ select {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary:hover {
|
.btn-secondary:hover {
|
||||||
background: rgba(26, 82, 118, 0.05);
|
background: var(--primary);
|
||||||
|
color: white;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success {
|
||||||
|
background: linear-gradient(135deg, #27ae60 0%, #1e8449 100%);
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 2px 8px rgba(39, 174, 96, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 16px rgba(39, 174, 96, 0.4);
|
||||||
|
filter: brightness(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-warning {
|
||||||
|
background: linear-gradient(135deg, #f39c12 0%, #d68910 100%);
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 2px 8px rgba(243, 156, 18, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-warning:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 16px rgba(243, 156, 18, 0.4);
|
||||||
|
filter: brightness(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 2px 8px rgba(231, 76, 60, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 16px rgba(231, 76, 60, 0.4);
|
||||||
|
filter: brightness(1.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-cancel {
|
.btn-cancel {
|
||||||
background: #f0f3f5;
|
background: #f0f3f5;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-cancel:hover {
|
.btn-cancel:hover {
|
||||||
background: #e5e9ec;
|
background: #e5e9ec;
|
||||||
|
color: var(--text-primary);
|
||||||
|
border-color: #d0d5da;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 特殊漸層按鈕 - 儲存至崗位清單 */
|
||||||
|
.btn-gradient-purple {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-gradient-purple:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 16px rgba(102, 126, 234, 0.4);
|
||||||
|
filter: brightness(1.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn svg {
|
.btn svg {
|
||||||
width: 18px;
|
width: 18px;
|
||||||
height: 18px;
|
height: 18px;
|
||||||
fill: currentColor;
|
fill: currentColor;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 小尺寸按鈕 */
|
||||||
|
.btn-sm {
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm svg {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 大尺寸按鈕 */
|
||||||
|
.btn-lg {
|
||||||
|
padding: 14px 28px;
|
||||||
|
font-size: 1rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-lg svg {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-lookup {
|
.btn-lookup {
|
||||||
padding: 10px 16px;
|
padding: 10px 16px;
|
||||||
border: 1.5px solid var(--border);
|
border: 1.5px solid var(--border);
|
||||||
border-radius: 6px;
|
border-radius: 8px;
|
||||||
background: #f8fafc;
|
background: #f8fafc;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
@@ -207,17 +295,19 @@ select {
|
|||||||
background: var(--primary);
|
background: var(--primary);
|
||||||
color: white;
|
color: white;
|
||||||
border-color: var(--primary);
|
border-color: var(--primary);
|
||||||
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-icon {
|
.btn-icon {
|
||||||
width: 38px;
|
width: 38px;
|
||||||
height: 38px;
|
height: 38px;
|
||||||
|
padding: 0;
|
||||||
border: 1.5px solid var(--border);
|
border: 1.5px solid var(--border);
|
||||||
border-radius: 6px;
|
border-radius: 8px;
|
||||||
background: #f8fafc;
|
background: #f8fafc;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
@@ -227,6 +317,7 @@ select {
|
|||||||
background: var(--primary);
|
background: var(--primary);
|
||||||
color: white;
|
color: white;
|
||||||
border-color: var(--primary);
|
border-color: var(--primary);
|
||||||
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-icon svg {
|
.btn-icon svg {
|
||||||
@@ -265,84 +356,139 @@ select {
|
|||||||
fill: currentColor;
|
fill: currentColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==================== AI Bags (Three Fortune Bags) ==================== */
|
/* ==================== AI Helper (AI 幫我想) ==================== */
|
||||||
.ai-bags-container {
|
.ai-helper-container {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: repeat(3, 1fr);
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 12px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 20px;
|
||||||
|
padding: 16px;
|
||||||
|
background: linear-gradient(135deg, rgba(155, 89, 182, 0.08) 0%, rgba(142, 68, 173, 0.05) 100%);
|
||||||
|
border: 1px solid rgba(155, 89, 182, 0.2);
|
||||||
|
border-radius: var(--radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ai-bag {
|
.ai-helper-btn {
|
||||||
position: relative;
|
display: flex;
|
||||||
padding: 8px 12px;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 14px 24px;
|
||||||
background: linear-gradient(135deg, #9b59b6 0%, #8e44ad 100%);
|
background: linear-gradient(135deg, #9b59b6 0%, #8e44ad 100%);
|
||||||
border-radius: 6px;
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
box-shadow: 0 4px 15px rgba(155, 89, 182, 0.3);
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
box-shadow: 0 2px 8px rgba(155, 89, 182, 0.3);
|
|
||||||
text-align: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ai-bag:hover {
|
.ai-helper-btn:hover {
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
box-shadow: 0 4px 12px rgba(155, 89, 182, 0.5);
|
box-shadow: 0 6px 20px rgba(155, 89, 182, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ai-bag:active {
|
.ai-helper-btn:active {
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ai-bag .bag-icon {
|
.ai-helper-btn:disabled {
|
||||||
font-size: 1rem;
|
|
||||||
margin-bottom: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-bag .bag-title {
|
|
||||||
color: white;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
margin-bottom: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-bag .bag-subtitle {
|
|
||||||
color: rgba(255, 255, 255, 0.8);
|
|
||||||
font-size: 0.65rem;
|
|
||||||
margin-top: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-bag .bag-edit-btn {
|
|
||||||
position: absolute;
|
|
||||||
top: 4px;
|
|
||||||
right: 4px;
|
|
||||||
background: rgba(255, 255, 255, 0.2);
|
|
||||||
border: none;
|
|
||||||
border-radius: 3px;
|
|
||||||
padding: 2px 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 0.7rem;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-bag .bag-edit-btn:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.3);
|
|
||||||
transform: scale(1.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-bag.loading {
|
|
||||||
pointer-events: none;
|
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ai-bag .spinner {
|
.ai-helper-btn svg {
|
||||||
width: 14px;
|
flex-shrink: 0;
|
||||||
height: 14px;
|
}
|
||||||
|
|
||||||
|
/* Prompt Editor */
|
||||||
|
.ai-prompt-editor {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-label {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-reset-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
padding: 0;
|
||||||
|
background: rgba(155, 89, 182, 0.1);
|
||||||
|
border: 1px solid rgba(155, 89, 182, 0.2);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #8e44ad;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-reset-btn:hover {
|
||||||
|
background: rgba(155, 89, 182, 0.2);
|
||||||
|
border-color: rgba(155, 89, 182, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-family: 'Monaco', 'Consolas', monospace;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
resize: vertical;
|
||||||
|
background: white;
|
||||||
|
color: var(--text-primary);
|
||||||
|
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #9b59b6;
|
||||||
|
box-shadow: 0 0 0 3px rgba(155, 89, 182, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-textarea::placeholder {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* AI Helper Loading State */
|
||||||
|
.ai-helper-container.loading .ai-helper-btn {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-helper-container.loading .ai-helper-btn::after {
|
||||||
|
content: '';
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||||
border-top-color: white;
|
border-top-color: white;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
animation: spin 0.8s linear infinite;
|
animation: spin 0.8s linear infinite;
|
||||||
margin: 4px auto;
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Legacy: Keep old styles for backward compatibility */
|
||||||
|
.ai-bags-container {
|
||||||
|
display: none; /* Hidden - replaced by ai-helper-container */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==================== Old AI Button (Deprecated) ==================== */
|
/* ==================== Old AI Button (Deprecated) ==================== */
|
||||||
@@ -640,3 +786,369 @@ select {
|
|||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
max-height: 300px;
|
max-height: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==================== Responsive Design for Components ==================== */
|
||||||
|
|
||||||
|
/* Large Tablets (1024px) */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.form-grid {
|
||||||
|
gap: 16px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-grid.three-cols {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn {
|
||||||
|
padding: 14px 24px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-bags-container {
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tablets (768px) */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
/* Tabs */
|
||||||
|
.tabs {
|
||||||
|
overflow-x: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn {
|
||||||
|
padding: 12px 16px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content {
|
||||||
|
padding: 20px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form Grid */
|
||||||
|
.form-grid,
|
||||||
|
.form-grid.three-cols {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form Actions */
|
||||||
|
.form-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
width: 100%;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons .btn {
|
||||||
|
flex: 1;
|
||||||
|
min-width: calc(50% - 6px);
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
.btn {
|
||||||
|
padding: 10px 16px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-buttons {
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-btn {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* AI Bags */
|
||||||
|
.ai-bags-container {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-bag {
|
||||||
|
padding: 6px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-bag .bag-title {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-bag .bag-subtitle {
|
||||||
|
font-size: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-bag .bag-icon {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toggle */
|
||||||
|
.toggle-group {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-switch {
|
||||||
|
width: 46px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-slider:before {
|
||||||
|
height: 18px;
|
||||||
|
width: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-switch input:checked + .toggle-slider:before {
|
||||||
|
transform: translateX(22px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Checkbox */
|
||||||
|
.checkbox-group {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal */
|
||||||
|
.modal {
|
||||||
|
width: 95%;
|
||||||
|
max-width: none;
|
||||||
|
margin: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header h3 {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
padding: 12px 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer .btn {
|
||||||
|
flex: 1;
|
||||||
|
min-width: calc(50% - 6px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toast */
|
||||||
|
.toast {
|
||||||
|
top: 12px;
|
||||||
|
right: 12px;
|
||||||
|
left: 12px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast svg {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Data Preview */
|
||||||
|
.data-preview {
|
||||||
|
padding: 16px;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-preview pre {
|
||||||
|
padding: 12px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
max-height: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile Phones (480px) */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
/* Tabs */
|
||||||
|
.tab-btn {
|
||||||
|
padding: 10px 12px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content {
|
||||||
|
padding: 16px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form */
|
||||||
|
.form-group label {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"],
|
||||||
|
input[type="number"],
|
||||||
|
input[type="date"],
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-wrapper {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-lookup {
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form Actions */
|
||||||
|
.form-actions {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons .btn {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
.btn {
|
||||||
|
padding: 10px 14px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon {
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* AI Bags */
|
||||||
|
.ai-bags-container {
|
||||||
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-bag {
|
||||||
|
padding: 6px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-bag .bag-icon {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-bag .bag-title {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-bag .bag-subtitle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-bag .bag-edit-btn {
|
||||||
|
padding: 1px 3px;
|
||||||
|
font-size: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* AI Generate Button */
|
||||||
|
.ai-generate-btn {
|
||||||
|
padding: 10px 16px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toggle */
|
||||||
|
.toggle-label {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Checkbox */
|
||||||
|
.checkbox-item {
|
||||||
|
padding: 8px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-item input {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-item label {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal */
|
||||||
|
.modal {
|
||||||
|
margin: 8px;
|
||||||
|
width: calc(100% - 16px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header h3 {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
padding: 10px 12px;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer .btn {
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toast */
|
||||||
|
.toast {
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
left: 8px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast svg {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Data Preview */
|
||||||
|
.data-preview {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-preview h4 {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-preview pre {
|
||||||
|
padding: 10px;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
max-height: 150px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,25 +1,37 @@
|
|||||||
/**
|
/**
|
||||||
* Layout Styles - 布局樣式
|
* Layout Styles - 布局樣式
|
||||||
* 包含容器、整體布局、Header、模組切換器
|
* Two-Column Layout: 左側導航 + 右側內容
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* ==================== Container ==================== */
|
/* ==================== Main Layout ==================== */
|
||||||
.app-container {
|
.app-container {
|
||||||
max-width: 1200px;
|
display: flex;
|
||||||
margin: 0 auto;
|
min-height: 100vh;
|
||||||
padding: 24px;
|
background: var(--bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==================== User Info Bar ==================== */
|
/* ==================== Sidebar (左側導航欄) ==================== */
|
||||||
.user-info-bar {
|
.sidebar {
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
width: 240px;
|
||||||
border-radius: var(--radius);
|
min-width: 240px;
|
||||||
padding: 12px 20px;
|
background: linear-gradient(180deg, #1a1f2e 0%, #2d3748 100%);
|
||||||
margin-bottom: 20px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
flex-direction: column;
|
||||||
align-items: center;
|
position: fixed;
|
||||||
box-shadow: var(--shadow);
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 100;
|
||||||
|
box-shadow: 4px 0 20px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==================== User Info (側邊欄頂部) ==================== */
|
||||||
|
.user-info-bar {
|
||||||
|
padding: 20px 16px;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-info {
|
.user-info {
|
||||||
@@ -30,117 +42,147 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.user-avatar {
|
.user-avatar {
|
||||||
width: 40px;
|
width: 44px;
|
||||||
height: 40px;
|
height: 44px;
|
||||||
border-radius: 50%;
|
border-radius: 10px;
|
||||||
background: rgba(255, 255, 255, 0.2);
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-details {
|
.user-details {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-name {
|
.user-name {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 1rem;
|
font-size: 0.95rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-role {
|
.user-role {
|
||||||
font-size: 0.85rem;
|
font-size: 0.75rem;
|
||||||
opacity: 0.9;
|
opacity: 0.7;
|
||||||
|
color: #a0aec0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logout-btn {
|
.logout-btn {
|
||||||
padding: 8px 20px;
|
width: 100%;
|
||||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
padding: 10px 16px;
|
||||||
border-radius: 6px;
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||||
background: rgba(255, 255, 255, 0.1);
|
border-radius: 8px;
|
||||||
color: #ffffff;
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
color: #e2e8f0;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
font-size: 0.9rem;
|
font-size: 0.85rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
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 {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.module-btn {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 200px;
|
|
||||||
padding: 14px 20px;
|
|
||||||
border: 2px solid var(--border);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
background: var(--bg-card);
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 10px;
|
gap: 8px;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-btn:hover {
|
||||||
|
background: rgba(239, 68, 68, 0.2);
|
||||||
|
border-color: rgba(239, 68, 68, 0.4);
|
||||||
|
color: #fca5a5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-btn svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==================== Module Selector (垂直導航選單) ==================== */
|
||||||
|
.module-selector {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 16px 12px;
|
||||||
|
gap: 6px;
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 14px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: transparent;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #a0aec0;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-btn:hover {
|
.module-btn:hover {
|
||||||
border-color: var(--primary-light);
|
background: rgba(255, 255, 255, 0.08);
|
||||||
color: var(--primary);
|
color: #e2e8f0;
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: var(--shadow);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-btn.active {
|
.module-btn.active {
|
||||||
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
|
background: linear-gradient(135deg, var(--module-position) 0%, var(--module-position-dark) 100%);
|
||||||
border-color: var(--primary);
|
|
||||||
color: white;
|
color: white;
|
||||||
box-shadow: 0 4px 15px rgba(26, 82, 118, 0.3);
|
box-shadow: 0 4px 12px var(--module-position-shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-btn.active.job-active {
|
.module-btn.active.job-active {
|
||||||
background: linear-gradient(135deg, var(--accent) 0%, #d35400 100%);
|
background: linear-gradient(135deg, var(--module-job) 0%, var(--module-job-dark) 100%);
|
||||||
border-color: var(--accent);
|
box-shadow: 0 4px 12px var(--module-job-shadow);
|
||||||
box-shadow: 0 4px 15px rgba(230, 126, 34, 0.3);
|
}
|
||||||
|
|
||||||
|
.module-btn.active.deptfunc-active {
|
||||||
|
background: linear-gradient(135deg, var(--module-deptfunc) 0%, var(--module-deptfunc-dark) 100%);
|
||||||
|
box-shadow: 0 4px 12px var(--module-deptfunc-shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-btn.active.desc-active {
|
.module-btn.active.desc-active {
|
||||||
background: linear-gradient(135deg, var(--green) 0%, var(--green-dark) 100%);
|
background: linear-gradient(135deg, var(--module-desc) 0%, var(--module-desc-dark) 100%);
|
||||||
border-color: var(--green);
|
box-shadow: 0 4px 12px var(--module-desc-shadow);
|
||||||
box-shadow: 0 4px 15px rgba(39, 174, 96, 0.3);
|
}
|
||||||
|
|
||||||
|
.module-btn.active.list-active {
|
||||||
|
background: linear-gradient(135deg, var(--module-list) 0%, var(--module-list-dark) 100%);
|
||||||
|
box-shadow: 0 4px 12px var(--module-list-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-btn.active.admin-active {
|
||||||
|
background: linear-gradient(135deg, var(--module-admin) 0%, var(--module-admin-dark) 100%);
|
||||||
|
box-shadow: 0 4px 12px var(--module-admin-shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-btn svg {
|
.module-btn svg {
|
||||||
width: 22px;
|
width: 20px;
|
||||||
height: 22px;
|
height: 20px;
|
||||||
fill: currentColor;
|
fill: currentColor;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==================== Main Content (右側內容區) ==================== */
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 240px;
|
||||||
|
padding: 24px 32px;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: var(--bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-content {
|
.module-content {
|
||||||
@@ -159,17 +201,29 @@
|
|||||||
gap: 16px;
|
gap: 16px;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
padding: 20px 28px;
|
padding: 20px 28px;
|
||||||
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
|
background: linear-gradient(135deg, var(--module-position) 0%, var(--module-position-dark) 100%);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
box-shadow: var(--shadow);
|
box-shadow: var(--shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-header.job-header {
|
.app-header.job-header {
|
||||||
background: linear-gradient(135deg, var(--accent) 0%, #d35400 100%);
|
background: linear-gradient(135deg, var(--module-job) 0%, var(--module-job-dark) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header.deptfunc-header {
|
||||||
|
background: linear-gradient(135deg, var(--module-deptfunc) 0%, var(--module-deptfunc-dark) 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-header.desc-header {
|
.app-header.desc-header {
|
||||||
background: linear-gradient(135deg, var(--green) 0%, var(--green-dark) 100%);
|
background: linear-gradient(135deg, var(--module-desc) 0%, var(--module-desc-dark) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header.list-header {
|
||||||
|
background: linear-gradient(135deg, var(--module-list) 0%, var(--module-list-dark) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header.admin-header {
|
||||||
|
background: linear-gradient(135deg, var(--module-admin) 0%, var(--module-admin-dark) 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-header .icon {
|
.app-header .icon {
|
||||||
@@ -255,3 +309,214 @@
|
|||||||
height: 1px;
|
height: 1px;
|
||||||
background: linear-gradient(to right, var(--border), transparent);
|
background: linear-gradient(to right, var(--border), transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==================== Sidebar Footer ==================== */
|
||||||
|
.sidebar-footer {
|
||||||
|
padding: 16px;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #64748b;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==================== Responsive Design ==================== */
|
||||||
|
|
||||||
|
/* Large Tablets & Small Desktops (1024px) */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.sidebar {
|
||||||
|
width: 200px;
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
margin-left: 200px;
|
||||||
|
padding: 20px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header {
|
||||||
|
padding: 16px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header h1 {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tablets (768px) */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.app-container {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 100%;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding: 12px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info-bar {
|
||||||
|
width: 100%;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-btn {
|
||||||
|
width: auto;
|
||||||
|
padding: 8px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-selector {
|
||||||
|
width: 100%;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding: 8px 0;
|
||||||
|
gap: 8px;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-btn {
|
||||||
|
flex: 1;
|
||||||
|
min-width: calc(33.33% - 8px);
|
||||||
|
padding: 10px;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-btn span {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
margin-left: 0;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header {
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header .icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header .icon svg {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header h1 {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header .subtitle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-footer {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-body {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile Phones (480px) */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.sidebar {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info-bar {
|
||||||
|
padding: 8px;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-name {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-role {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-btn {
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-btn span {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-selector {
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-btn {
|
||||||
|
min-width: calc(50% - 6px);
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-btn svg {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header {
|
||||||
|
padding: 12px;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header .icon {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header .icon svg {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header h1 {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-box {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
padding: 10px 12px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-body {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,25 +3,334 @@
|
|||||||
* 包含各個頁籤/模組的特定樣式需求
|
* 包含各個頁籤/模組的特定樣式需求
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/* ==================== 共用模組樣式 ==================== */
|
||||||
|
|
||||||
|
/* 模組頂部工具列 */
|
||||||
|
.module-toolbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
background: #f8fafc;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-toolbar-left,
|
||||||
|
.module-toolbar-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-toolbar h2 {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--primary);
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-toolbar .hint {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
/* ==================== 崗位基礎資料模組 ==================== */
|
/* ==================== 崗位基礎資料模組 ==================== */
|
||||||
/* (目前使用共用元件,無特殊樣式) */
|
/* CSV 按鈕區塊 */
|
||||||
|
.csv-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
/* ==================== 職務基礎資料模組 ==================== */
|
/* ==================== 職務基礎資料模組 ==================== */
|
||||||
/* (目前使用共用元件,無特殊樣式) */
|
/* (使用共用元件) */
|
||||||
|
|
||||||
/* ==================== 部門職責模組 ==================== */
|
/* ==================== 部門職責模組 ==================== */
|
||||||
/* (目前使用共用元件,無特殊樣式) */
|
/* (使用共用元件) */
|
||||||
|
|
||||||
/* ==================== 崗位描述模組 ==================== */
|
/* ==================== 崗位描述模組 ==================== */
|
||||||
/* (目前使用共用元件,無特殊樣式) */
|
/* (使用共用元件) */
|
||||||
|
|
||||||
/* ==================== 崗位清單模組 ==================== */
|
/* ==================== 崗位清單模組 ==================== */
|
||||||
/* (目前使用共用元件,無特殊樣式) */
|
.position-list-module .module-toolbar {
|
||||||
|
background: linear-gradient(135deg, rgba(20, 184, 166, 0.05) 0%, rgba(13, 148, 136, 0.05) 100%);
|
||||||
|
border-color: rgba(20, 184, 166, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 資料表格容器 */
|
||||||
|
.data-table-container {
|
||||||
|
overflow-x: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 資料表格 */
|
||||||
|
.data-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
background: white;
|
||||||
|
min-width: 700px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table thead {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table th {
|
||||||
|
padding: 14px 16px;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
background: var(--primary);
|
||||||
|
color: white;
|
||||||
|
border-bottom: 2px solid var(--primary-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table th.sortable {
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table th.sortable:hover {
|
||||||
|
background: var(--primary-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table th .sort-icon {
|
||||||
|
margin-left: 6px;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table td {
|
||||||
|
padding: 12px 16px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table tbody tr:hover {
|
||||||
|
background: rgba(41, 128, 185, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table tbody tr:last-child td {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 空資料提示 */
|
||||||
|
.data-table .empty-row td {
|
||||||
|
padding: 48px 20px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 操作按鈕欄 */
|
||||||
|
.data-table .action-cell {
|
||||||
|
text-align: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table .action-cell .btn {
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
margin: 0 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 崗位清單專用表格顏色 - 青色 */
|
||||||
|
.position-list-table thead {
|
||||||
|
background: linear-gradient(135deg, var(--module-list) 0%, var(--module-list-dark) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.position-list-table th {
|
||||||
|
background: transparent;
|
||||||
|
border-bottom-color: var(--module-list-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.position-list-table th.sortable:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
/* ==================== 管理者頁面模組 ==================== */
|
/* ==================== 管理者頁面模組 ==================== */
|
||||||
/* (目前使用共用元件,無特殊樣式) */
|
.admin-module .module-toolbar {
|
||||||
|
background: linear-gradient(135deg, rgba(244, 63, 94, 0.05) 0%, rgba(225, 29, 72, 0.05) 100%);
|
||||||
|
border-color: rgba(244, 63, 94, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/* 管理者表格專用顏色 - 玫紅色 */
|
||||||
* 注意:如果未來各模組需要特定樣式,可在此添加
|
.admin-table thead {
|
||||||
* 例如:特殊的表格樣式、卡片佈局等
|
background: linear-gradient(135deg, var(--module-admin) 0%, var(--module-admin-dark) 100%);
|
||||||
*/
|
}
|
||||||
|
|
||||||
|
.admin-table th {
|
||||||
|
background: transparent;
|
||||||
|
border-bottom-color: var(--module-admin-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 權限標籤 */
|
||||||
|
.permission-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.permission-badge.admin-highest {
|
||||||
|
background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.permission-badge.admin {
|
||||||
|
background: linear-gradient(135deg, #f39c12 0%, #d68910 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.permission-badge.user {
|
||||||
|
background: linear-gradient(135deg, #27ae60 0%, #1e8449 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.permission-badge.viewer {
|
||||||
|
background: linear-gradient(135deg, #3498db 0%, #2980b9 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==================== 響應式設計 - 模組專屬 ==================== */
|
||||||
|
|
||||||
|
/* Large Tablets (1024px) */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.module-toolbar {
|
||||||
|
padding: 14px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table {
|
||||||
|
min-width: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table th,
|
||||||
|
.data-table td {
|
||||||
|
padding: 12px 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tablets (768px) */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.module-toolbar {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-toolbar-left,
|
||||||
|
.module-toolbar-right {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-toolbar h2 {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-toolbar .hint {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.csv-buttons {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table {
|
||||||
|
min-width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table th,
|
||||||
|
.data-table td {
|
||||||
|
padding: 10px 12px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table .action-cell .btn {
|
||||||
|
padding: 5px 10px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 隱藏較不重要的欄位 */
|
||||||
|
.data-table .hide-tablet {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.permission-badge {
|
||||||
|
padding: 3px 8px;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile Phones (480px) */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.module-toolbar {
|
||||||
|
padding: 10px;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-toolbar-left,
|
||||||
|
.module-toolbar-right {
|
||||||
|
width: 100%;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-toolbar-left .btn,
|
||||||
|
.module-toolbar-right .btn {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.csv-buttons {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.csv-buttons .btn {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table {
|
||||||
|
min-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table th,
|
||||||
|
.data-table td {
|
||||||
|
padding: 8px 10px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table th {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table .action-cell {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table .action-cell .btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 隱藏更多欄位 */
|
||||||
|
.data-table .hide-mobile {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,56 +1,244 @@
|
|||||||
/**
|
/**
|
||||||
* Utilities Styles - 工具類別與響應式設計
|
* Utilities Styles - 工具類別與額外響應式設計
|
||||||
* 包含快速工具類別、響應式斷點
|
* 包含快速工具類別、表格響應式、額外斷點調整
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* ==================== Utility Classes ==================== */
|
/* ==================== Utility Classes ==================== */
|
||||||
/* (可按需添加工具類別,如 .mt-1, .text-center 等) */
|
|
||||||
|
|
||||||
/* ==================== Responsive Design ==================== */
|
/* Display */
|
||||||
|
.d-none { display: none !important; }
|
||||||
|
.d-block { display: block !important; }
|
||||||
|
.d-flex { display: flex !important; }
|
||||||
|
.d-grid { display: grid !important; }
|
||||||
|
|
||||||
|
/* Flexbox */
|
||||||
|
.flex-wrap { flex-wrap: wrap !important; }
|
||||||
|
.flex-column { flex-direction: column !important; }
|
||||||
|
.justify-center { justify-content: center !important; }
|
||||||
|
.justify-between { justify-content: space-between !important; }
|
||||||
|
.align-center { align-items: center !important; }
|
||||||
|
.gap-1 { gap: 4px !important; }
|
||||||
|
.gap-2 { gap: 8px !important; }
|
||||||
|
.gap-3 { gap: 12px !important; }
|
||||||
|
.gap-4 { gap: 16px !important; }
|
||||||
|
|
||||||
|
/* Margin */
|
||||||
|
.mt-0 { margin-top: 0 !important; }
|
||||||
|
.mt-1 { margin-top: 4px !important; }
|
||||||
|
.mt-2 { margin-top: 8px !important; }
|
||||||
|
.mt-3 { margin-top: 12px !important; }
|
||||||
|
.mt-4 { margin-top: 16px !important; }
|
||||||
|
.mb-0 { margin-bottom: 0 !important; }
|
||||||
|
.mb-1 { margin-bottom: 4px !important; }
|
||||||
|
.mb-2 { margin-bottom: 8px !important; }
|
||||||
|
.mb-3 { margin-bottom: 12px !important; }
|
||||||
|
.mb-4 { margin-bottom: 16px !important; }
|
||||||
|
|
||||||
|
/* Padding */
|
||||||
|
.p-0 { padding: 0 !important; }
|
||||||
|
.p-1 { padding: 4px !important; }
|
||||||
|
.p-2 { padding: 8px !important; }
|
||||||
|
.p-3 { padding: 12px !important; }
|
||||||
|
.p-4 { padding: 16px !important; }
|
||||||
|
|
||||||
|
/* Text */
|
||||||
|
.text-center { text-align: center !important; }
|
||||||
|
.text-left { text-align: left !important; }
|
||||||
|
.text-right { text-align: right !important; }
|
||||||
|
.text-sm { font-size: 0.875rem !important; }
|
||||||
|
.text-xs { font-size: 0.75rem !important; }
|
||||||
|
.text-muted { color: var(--text-secondary) !important; }
|
||||||
|
.text-primary { color: var(--primary) !important; }
|
||||||
|
.text-danger { color: var(--danger) !important; }
|
||||||
|
.text-success { color: var(--success) !important; }
|
||||||
|
.fw-bold { font-weight: 600 !important; }
|
||||||
|
.fw-normal { font-weight: 400 !important; }
|
||||||
|
|
||||||
|
/* Width */
|
||||||
|
.w-100 { width: 100% !important; }
|
||||||
|
.w-auto { width: auto !important; }
|
||||||
|
|
||||||
|
/* Overflow */
|
||||||
|
.overflow-auto { overflow: auto !important; }
|
||||||
|
.overflow-hidden { overflow: hidden !important; }
|
||||||
|
.overflow-x-auto { overflow-x: auto !important; }
|
||||||
|
|
||||||
|
/* Visibility */
|
||||||
|
.visible { visibility: visible !important; }
|
||||||
|
.invisible { visibility: hidden !important; }
|
||||||
|
|
||||||
|
/* ==================== Table Responsive ==================== */
|
||||||
|
.table-responsive {
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-responsive table {
|
||||||
|
min-width: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==================== Position List Specific ==================== */
|
||||||
|
.position-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.position-table th,
|
||||||
|
.position-table td {
|
||||||
|
padding: 12px 16px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.position-table th {
|
||||||
|
background: #f8fafc;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.position-table td {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.position-table tr:hover {
|
||||||
|
background: rgba(41, 128, 185, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==================== Responsive Hide/Show ==================== */
|
||||||
|
@media (min-width: 769px) {
|
||||||
|
.hide-desktop { display: none !important; }
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
/* Form Grid */
|
.hide-tablet { display: none !important; }
|
||||||
.form-grid,
|
|
||||||
.form-grid.three-cols {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Form Actions */
|
@media (max-width: 480px) {
|
||||||
.form-actions {
|
.hide-mobile { display: none !important; }
|
||||||
flex-direction: column;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-buttons {
|
@media (min-width: 481px) {
|
||||||
width: 100%;
|
.show-mobile-only { display: none !important; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==================== Extra Responsive Adjustments ==================== */
|
||||||
|
|
||||||
|
/* Tablets (768px) */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
/* Table */
|
||||||
|
.position-table th,
|
||||||
|
.position-table td {
|
||||||
|
padding: 10px 12px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Utility overrides */
|
||||||
|
.text-sm { font-size: 0.8rem !important; }
|
||||||
|
.text-xs { font-size: 0.7rem !important; }
|
||||||
|
|
||||||
|
/* Form wrapper for side-by-side elements */
|
||||||
|
.input-with-button {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.input-with-button .btn-lookup {
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Module Selector */
|
|
||||||
.module-selector {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.module-btn {
|
|
||||||
min-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* User Info Bar */
|
|
||||||
.user-info-bar {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-info {
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logout-btn {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mobile Phones (480px) */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
/* Table */
|
||||||
|
.position-table th,
|
||||||
|
.position-table td {
|
||||||
|
padding: 8px 10px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stack buttons on very small screens */
|
||||||
|
.btn-group-vertical {
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-group-vertical .btn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide less important table columns */
|
||||||
|
.position-table .hide-xs {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==================== Print Styles ==================== */
|
||||||
|
@media print {
|
||||||
|
.sidebar,
|
||||||
|
.logout-btn,
|
||||||
|
.module-selector,
|
||||||
|
.form-actions,
|
||||||
|
.ai-bags-container,
|
||||||
|
.ai-generate-btn,
|
||||||
|
.nav-buttons,
|
||||||
|
.toast,
|
||||||
|
.modal-overlay {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
margin-left: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-container {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-card {
|
||||||
|
box-shadow: none;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content {
|
||||||
|
display: block !important;
|
||||||
|
page-break-inside: avoid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==================== Safe Area for notched devices ==================== */
|
||||||
|
@supports (padding: max(0px)) {
|
||||||
|
.sidebar {
|
||||||
|
padding-left: max(12px, env(safe-area-inset-left));
|
||||||
|
padding-right: max(12px, env(safe-area-inset-right));
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
padding-left: max(16px, env(safe-area-inset-left));
|
||||||
|
padding-right: max(16px, env(safe-area-inset-right));
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.sidebar {
|
||||||
|
padding-bottom: max(12px, env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==================== Reduced Motion ==================== */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
animation-duration: 0.01ms !important;
|
||||||
|
animation-iteration-count: 1 !important;
|
||||||
|
transition-duration: 0.01ms !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,314 +0,0 @@
|
|||||||
<EFBFBD>Ʒ~<7E><>,<EFBFBD>B<EFBFBD>O,<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>W<EFBFBD><EFBFBD>,<EFBFBD>^<5E><><EFBFBD>W<EFBFBD><57>
|
|
||||||
<EFBFBD>b<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ʒ~<7E>s,<EFBFBD>b<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ʒ~<7E>s,<EFBFBD>b<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ʒ~<7E>s,<EFBFBD><EFBFBD><EFBFBD>B<EFBFBD><EFBFBD>
|
|
||||||
,,,<EFBFBD><EFBFBD><EFBFBD>B<EFBFBD><EFBFBD><EFBFBD>U<EFBFBD>z
|
|
||||||
<EFBFBD>T<EFBFBD><EFBFBD><EFBFBD>Ʒ~<7E><>,<EFBFBD>T<EFBFBD><EFBFBD><EFBFBD>Ʒ~<7E><>,<EFBFBD>T<EFBFBD><EFBFBD><EFBFBD>Ʒ~<7E><>,<EFBFBD><EFBFBD><EFBFBD>`<60>g<EFBFBD>z
|
|
||||||
,,,<EFBFBD>M<EFBFBD>g<EFBFBD>z
|
|
||||||
<EFBFBD>k<EFBFBD>ȫ<EFBFBD>,<EFBFBD>k<EFBFBD>ȫ<EFBFBD>,<EFBFBD>k<EFBFBD>ȫ<EFBFBD>,<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,,,<EFBFBD>k<EFBFBD>ȱM<EFBFBD><EFBFBD>
|
|
||||||
,,,<EFBFBD>M<EFBFBD>Q<EFBFBD>u<EFBFBD>{<7B>v
|
|
||||||
<EFBFBD><EFBFBD><EFBFBD>s<EFBFBD>s<EFBFBD>y<EFBFBD>Ʒ~<7E><>,<EFBFBD>Ͳ<EFBFBD><EFBFBD>B,<EFBFBD>Ͳ<EFBFBD><EFBFBD>B,<EFBFBD>B<EFBFBD><EFBFBD>
|
|
||||||
,,,<EFBFBD>M<EFBFBD><EFBFBD>
|
|
||||||
,,<EFBFBD>Ͳ<EFBFBD><EFBFBD><EFBFBD>,<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,,<EFBFBD>Ͳ<EFBFBD><EFBFBD><EFBFBD>,<EFBFBD>Ҫ<EFBFBD>
|
|
||||||
,,,<EFBFBD>ժ<EFBFBD>
|
|
||||||
,,,<EFBFBD>Z<EFBFBD><EFBFBD>
|
|
||||||
,,,<EFBFBD>ƯZ<EFBFBD><EFBFBD>
|
|
||||||
,,,<EFBFBD>@<40>~<7E><>
|
|
||||||
,,<EFBFBD>Ͳ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>,<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,,,<EFBFBD>Ҫ<EFBFBD>
|
|
||||||
,,,<EFBFBD>M<EFBFBD><EFBFBD>
|
|
||||||
,,,<EFBFBD>u<EFBFBD>{<7B>v
|
|
||||||
,<EFBFBD><EFBFBD><EFBFBD>s<EFBFBD>s<EFBFBD>y<EFBFBD>Ʒ~<7E><>,<EFBFBD><EFBFBD><EFBFBD>s<EFBFBD>~<7E><><EFBFBD>ި,<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,,<EFBFBD>ʸ˫~<7E><><EFBFBD>ި<EFBFBD><DEA8><EFBFBD>,<EFBFBD>Ҫ<EFBFBD>
|
|
||||||
,,,<EFBFBD>u<EFBFBD>{<7B>v
|
|
||||||
,,,<EFBFBD>ժ<EFBFBD>
|
|
||||||
,,,<EFBFBD>Z<EFBFBD><EFBFBD>
|
|
||||||
,,,<EFBFBD>ƯZ<EFBFBD><EFBFBD>
|
|
||||||
,,,<EFBFBD>@<40>~<7E><>
|
|
||||||
,,<EFBFBD>~<7E><><EFBFBD>ި<EFBFBD><DEA8><EFBFBD><EFBFBD>X<EFBFBD><58>,<EFBFBD>Ҫ<EFBFBD>
|
|
||||||
,,,<EFBFBD>u<EFBFBD>{<7B>v
|
|
||||||
,,<EFBFBD><EFBFBD><EFBFBD>s<EFBFBD>s<EFBFBD>y<EFBFBD>Ʒ~<7E><>,<EFBFBD><EFBFBD><EFBFBD>`<60>g<EFBFBD>z
|
|
||||||
,,,<EFBFBD><EFBFBD><EFBFBD>`<60>g<EFBFBD>z<EFBFBD>U<EFBFBD>z
|
|
||||||
,<EFBFBD>ʸˤu<EFBFBD>{<7B>B,<EFBFBD>ʸˤu<EFBFBD>{<7B>B,<EFBFBD>B<EFBFBD><EFBFBD>
|
|
||||||
,,,<EFBFBD>M<EFBFBD><EFBFBD>
|
|
||||||
,,,<EFBFBD>u<EFBFBD>{<7B>v
|
|
||||||
,,<EFBFBD>s<EFBFBD>{<7B>u<EFBFBD>{<7B>@<40><>,<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,,<EFBFBD>s<EFBFBD>{<7B>u<EFBFBD>{<7B>G<EFBFBD><47>,<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,,<EFBFBD>W<EFBFBD><EFBFBD><EFBFBD>Z<EFBFBD>u<EFBFBD>u<EFBFBD>{<7B><>,<EFBFBD>Ҫ<EFBFBD>
|
|
||||||
,,,<EFBFBD>u<EFBFBD>{<7B>v
|
|
||||||
,,<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>I<EFBFBD><EFBFBD><EFBFBD>u<EFBFBD>{<7B><>,<EFBFBD>Ҫ<EFBFBD>
|
|
||||||
,,,<EFBFBD>u<EFBFBD>{<7B>v
|
|
||||||
,,<EFBFBD><EFBFBD><EFBFBD>L~<7E>s<EFBFBD>}<7D>u<EFBFBD>{<7B><>,<EFBFBD>Ҫ<EFBFBD>
|
|
||||||
,,,<EFBFBD>u<EFBFBD>{<7B>v
|
|
||||||
,,<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>q<EFBFBD><EFBFBD><EFBFBD>u<EFBFBD>{<7B><>,<EFBFBD>Ҫ<EFBFBD>
|
|
||||||
,,,<EFBFBD>u<EFBFBD>{<7B>v
|
|
||||||
,,<EFBFBD>]<5D>Ƥ@<40><>,<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,,<EFBFBD>]<5D>ƤG<C6A4><47>,<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,,<EFBFBD>]<5D>Ƥ@<40><>,<EFBFBD>Ҫ<EFBFBD>
|
|
||||||
,,,<EFBFBD>u<EFBFBD>{<7B>v
|
|
||||||
,,<EFBFBD>]<5D>ƤG<C6A4><47>,<EFBFBD>Ҫ<EFBFBD>
|
|
||||||
,,,<EFBFBD>u<EFBFBD>{<7B>v
|
|
||||||
,,<EFBFBD>]<5D>ƤT<C6A4><54>,<EFBFBD>Ҫ<EFBFBD>
|
|
||||||
,,,<EFBFBD>u<EFBFBD>{<7B>v
|
|
||||||
,<EFBFBD><EFBFBD><EFBFBD>`<60>줽<EFBFBD><ECA4BD>,<EFBFBD>u<EFBFBD>~<7E>u<EFBFBD>{<7B><>,<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,,,<EFBFBD>u<EFBFBD>{<7B>v
|
|
||||||
,,<EFBFBD>u<EFBFBD>~<7E>u<EFBFBD>{<7B><>,<EFBFBD>Ҫ<EFBFBD>
|
|
||||||
,,,<EFBFBD>u<EFBFBD>{<7B>v
|
|
||||||
,,<EFBFBD>M<EFBFBD>z,<EFBFBD>Ʋz
|
|
||||||
,,,<EFBFBD>u<EFBFBD>{<7B>v
|
|
||||||
,<EFBFBD><EFBFBD><EFBFBD>դu<EFBFBD>{<7B>P<EFBFBD><50><EFBFBD>o<EFBFBD>B,<EFBFBD><EFBFBD><EFBFBD>դu<EFBFBD>{<7B>P<EFBFBD><50><EFBFBD>o<EFBFBD>B,<EFBFBD>B<EFBFBD><EFBFBD>
|
|
||||||
,,,<EFBFBD>M<EFBFBD><EFBFBD>
|
|
||||||
,,<EFBFBD><EFBFBD><EFBFBD>դu<EFBFBD>{<7B><>,<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,,<EFBFBD>]<5D>ƽ<EFBFBD>,<EFBFBD>Ҫ<EFBFBD>
|
|
||||||
,,,<EFBFBD>u<EFBFBD>{<7B>v
|
|
||||||
,,<EFBFBD><EFBFBD><EFBFBD>ս<EFBFBD>,<EFBFBD>Ҫ<EFBFBD>
|
|
||||||
,,,<EFBFBD>u<EFBFBD>{<7B>v
|
|
||||||
,,<EFBFBD>s<EFBFBD><EFBFBD><EFBFBD>~<7E>ɤJ<C9A4><4A>,<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,,,<EFBFBD>M<EFBFBD><EFBFBD>
|
|
||||||
,,,<EFBFBD>u<EFBFBD>{<7B>v
|
|
||||||
,,<EFBFBD><EFBFBD><EFBFBD>o<EFBFBD><EFBFBD>,<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,,<EFBFBD>ʸ˧N<EFBFBD><EFBFBD>,<EFBFBD>Ҫ<EFBFBD>
|
|
||||||
,,,<EFBFBD>u<EFBFBD>{<7B>v
|
|
||||||
,,<EFBFBD>]<5D>p<EFBFBD><70><EFBFBD><EFBFBD><EFBFBD><EFBFBD>,<EFBFBD>Ҫ<EFBFBD>
|
|
||||||
,,,<EFBFBD>u<EFBFBD>{<7B>v
|
|
||||||
,,,<EFBFBD>M<EFBFBD><EFBFBD>
|
|
||||||
,<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>B,<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>B,<EFBFBD>B<EFBFBD><EFBFBD>
|
|
||||||
,,<EFBFBD><EFBFBD><EFBFBD>ʳ<EFBFBD>,<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,,<EFBFBD><EFBFBD><EFBFBD>ʤ@<40><>,<EFBFBD>Ҫ<EFBFBD>
|
|
||||||
,,,<EFBFBD>M<EFBFBD><EFBFBD>
|
|
||||||
,,<EFBFBD><EFBFBD><EFBFBD>ʤG<EFBFBD><EFBFBD>,<EFBFBD>Ҫ<EFBFBD>
|
|
||||||
,,,<EFBFBD>M<EFBFBD><EFBFBD>
|
|
||||||
,,<EFBFBD>~<7E><><EFBFBD>귽<EFBFBD><EAB7BD>,<EFBFBD>M<EFBFBD><EFBFBD>
|
|
||||||
,,<EFBFBD>ͺ<EFBFBD>,<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,,<EFBFBD>Ͳ<EFBFBD><EFBFBD>Ƶ{<7B><>,<EFBFBD>Ҫ<EFBFBD>
|
|
||||||
,,,<EFBFBD>M<EFBFBD><EFBFBD>
|
|
||||||
,,<EFBFBD><EFBFBD><EFBFBD>~<7E><>,<EFBFBD>Ҫ<EFBFBD>
|
|
||||||
,,,<EFBFBD>Z<EFBFBD><EFBFBD>
|
|
||||||
,,,<EFBFBD>ƯZ<EFBFBD><EFBFBD>
|
|
||||||
,,,<EFBFBD>@<40>~<7E><>
|
|
||||||
,,<EFBFBD>쪫<EFBFBD>Ʊ<EFBFBD><EFBFBD>,<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,,<EFBFBD><EFBFBD><EFBFBD>Ʊ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>,<EFBFBD>Ҫ<EFBFBD>
|
|
||||||
,,,<EFBFBD>M<EFBFBD><EFBFBD>
|
|
||||||
,,<EFBFBD>쪫<EFBFBD>ƭ<EFBFBD>,<EFBFBD>Z<EFBFBD><EFBFBD>
|
|
||||||
,,,<EFBFBD>ƯZ<EFBFBD><EFBFBD>
|
|
||||||
,,,<EFBFBD>@<40>~<7E><>
|
|
||||||
,<EFBFBD>t<EFBFBD>ȻP<EFBFBD><EFBFBD><EFBFBD>w<EFBFBD>úz<EFBFBD>B,<EFBFBD>t<EFBFBD>ȻP<EFBFBD><EFBFBD><EFBFBD>w<EFBFBD>úz<EFBFBD>B,<EFBFBD>B<EFBFBD><EFBFBD>
|
|
||||||
,,,<EFBFBD>u<EFBFBD>{<7B>v
|
|
||||||
,,<EFBFBD>t<EFBFBD>ȳ<EFBFBD>,<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,,<EFBFBD>t<EFBFBD>Ƚ<EFBFBD>,<EFBFBD>Ҫ<EFBFBD>
|
|
||||||
,,,<EFBFBD>u<EFBFBD>{<7B>v
|
|
||||||
,,<EFBFBD>w<EFBFBD>ä<EFBFBD><EFBFBD><EFBFBD>,<EFBFBD>Ҫ<EFBFBD>
|
|
||||||
,,,<EFBFBD>u<EFBFBD>{<7B>v
|
|
||||||
,,,<EFBFBD>M<EFBFBD><EFBFBD>
|
|
||||||
,,<EFBFBD><EFBFBD><EFBFBD>ʤƽ<EFBFBD>,<EFBFBD>Ҫ<EFBFBD>
|
|
||||||
,,,<EFBFBD>u<EFBFBD>{<7B>v
|
|
||||||
<EFBFBD><EFBFBD><EFBFBD>~<7E>Ʒ~<7E><>,<EFBFBD><EFBFBD><EFBFBD>~<7E>Ʒ~<7E><>,<EFBFBD><EFBFBD><EFBFBD>~<7E>Ʒ~<7E><>,<EFBFBD>B<EFBFBD><EFBFBD>
|
|
||||||
,<EFBFBD><EFBFBD><EFBFBD>i<EFBFBD><EFBFBD><EFBFBD>~<7E>Ʒ~<7E>B,<EFBFBD><EFBFBD><EFBFBD>i<EFBFBD><EFBFBD><EFBFBD>~<7E>Ʒ~<7E>B,<EFBFBD>B<EFBFBD><EFBFBD>
|
|
||||||
,,<EFBFBD><EFBFBD><EFBFBD>~<7E>z<DEB2><7A>(APD),<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,,<EFBFBD><EFBFBD><EFBFBD>~<7E>u<EFBFBD>{(APD),<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,,,<EFBFBD>u<EFBFBD>{<7B>v
|
|
||||||
,,<EFBFBD><EFBFBD><EFBFBD>~<7E>z(APD),<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,,,<EFBFBD>u<EFBFBD>{<7B>v
|
|
||||||
,<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>~<7E>Ʒ~<7E>B,<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>~<7E>Ʒ~<7E>B,<EFBFBD>B<EFBFBD><EFBFBD>
|
|
||||||
,,<EFBFBD><EFBFBD><EFBFBD>~<7E>z<DEB2><7A>(MPD),<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,,<EFBFBD><EFBFBD><EFBFBD>~<7E>u<EFBFBD>{(MPD),<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,,,<EFBFBD>M<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,,,<EFBFBD>u<EFBFBD>{<7B>v
|
|
||||||
,,<EFBFBD><EFBFBD><EFBFBD>~<7E>z(MPD),<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,,,<EFBFBD>M<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,,,<EFBFBD>u<EFBFBD>{<7B>v
|
|
||||||
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>T<EFBFBD>t,<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>T<EFBFBD>t,<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>T<EFBFBD>t,<EFBFBD>U<EFBFBD><EFBFBD>
|
|
||||||
,,,<EFBFBD>M<EFBFBD><EFBFBD>
|
|
||||||
,,<EFBFBD>~<7E>賡,<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,,,<EFBFBD>u<EFBFBD>{<7B>v
|
|
||||||
,,,<EFBFBD>@<40>~<7E><>
|
|
||||||
,,<EFBFBD>s<EFBFBD>y<EFBFBD><EFBFBD>,<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,,,<EFBFBD>Ҫ<EFBFBD>
|
|
||||||
,,,<EFBFBD>Z<EFBFBD><EFBFBD>
|
|
||||||
,,,<EFBFBD>ƯZ<EFBFBD><EFBFBD>
|
|
||||||
,,,<EFBFBD>@<40>~<7E><>
|
|
||||||
,,<EFBFBD>t<EFBFBD>ȳ<EFBFBD>(Fab3),<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,,,<EFBFBD>u<EFBFBD>{<7B>v
|
|
||||||
,<EFBFBD>s<EFBFBD>{<7B>u<EFBFBD>{<7B>B,<EFBFBD>u<EFBFBD>{<7B>@<40><>,<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,,,<EFBFBD>u<EFBFBD>{<7B>v
|
|
||||||
,,<EFBFBD>u<EFBFBD>{<7B>G<EFBFBD><47>,<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,,,<EFBFBD>u<EFBFBD>{<7B>v
|
|
||||||
,,<EFBFBD>u<EFBFBD>{<7B>T<EFBFBD><54>,<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,,,<EFBFBD>u<EFBFBD>{<7B>v
|
|
||||||
,,<EFBFBD>s<EFBFBD>{<7B><><EFBFBD>X<EFBFBD><58>(Fab3),<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,,,<EFBFBD>u<EFBFBD>{<7B>v
|
|
||||||
<EFBFBD><EFBFBD><EFBFBD>ΤH<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>F<EFBFBD>Ʒ~<7E><>,<EFBFBD><EFBFBD><EFBFBD>ΤH<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>F<EFBFBD>Ʒ~<7E><>,<EFBFBD><EFBFBD><EFBFBD>ΤH<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>F<EFBFBD>Ʒ~<7E><>,<EFBFBD>H<EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
|
||||||
,,<EFBFBD><EFBFBD><EFBFBD>F<EFBFBD>`<60>Ⱥz<DEB2><7A>,<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,,,<EFBFBD>M<EFBFBD><EFBFBD>
|
|
||||||
,,,<EFBFBD>U<EFBFBD>z
|
|
||||||
,,<EFBFBD>۶ҥ<EFBFBD><EFBFBD>γ<EFBFBD>,<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,,,<EFBFBD>M<EFBFBD><EFBFBD>
|
|
||||||
,,<EFBFBD>V<EFBFBD>m<EFBFBD>o<EFBFBD>i<EFBFBD><EFBFBD>,<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,,,<EFBFBD>M<EFBFBD><EFBFBD>
|
|
||||||
,,<EFBFBD>~<7E>S<EFBFBD>z<DEB2><7A>,<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,,,<EFBFBD>M<EFBFBD><EFBFBD>
|
|
||||||
<EFBFBD><EFBFBD><EFBFBD>ΰ]<5D>ȨƷ~<7E><>,<EFBFBD><EFBFBD><EFBFBD>ΰ]<5D>ȨƷ~<7E><>,<EFBFBD><EFBFBD><EFBFBD>ΰ]<5D>ȨƷ~<7E><>,<EFBFBD>]<5D>Ȫ<EFBFBD>
|
|
||||||
,<EFBFBD><EFBFBD><EFBFBD>s<EFBFBD>j<EFBFBD>Z<EFBFBD>]<5D>ȳB,<EFBFBD><EFBFBD><EFBFBD>s<EFBFBD>j<EFBFBD>Z<EFBFBD>]<5D>ȳB,<EFBFBD>B<EFBFBD><EFBFBD>
|
|
||||||
,,<EFBFBD><EFBFBD><EFBFBD>s<EFBFBD>j<EFBFBD>Z<EFBFBD>]<5D>ȳ<EFBFBD>,<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,,<EFBFBD><EFBFBD><EFBFBD>s<EFBFBD>j<EFBFBD>Z<EFBFBD>]<5D>Ƚ<EFBFBD>,<EFBFBD>Ҫ<EFBFBD>
|
|
||||||
,,,<EFBFBD>M<EFBFBD><EFBFBD>
|
|
||||||
,<EFBFBD><EFBFBD><EFBFBD>ΰ]<5D>ȨƷ~<7E><>,<EFBFBD><EFBFBD><EFBFBD>Χ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>H<EFBFBD><EFBFBD><EFBFBD>Y,<EFBFBD>M<EFBFBD>װƲz
|
|
||||||
<EFBFBD><EFBFBD><EFBFBD>η|<7C>p<EFBFBD>Ʒ~<7E><>,<EFBFBD><EFBFBD><EFBFBD>η|<7C>p<EFBFBD>Ʒ~<7E><>,<EFBFBD><EFBFBD><EFBFBD>η|<7C>p<EFBFBD>Ʒ~<7E><>,<EFBFBD>|<7C>p<EFBFBD><70>
|
|
||||||
,<EFBFBD><EFBFBD><EFBFBD>s<EFBFBD>|<7C>p<EFBFBD>B,<EFBFBD><EFBFBD><EFBFBD>s<EFBFBD>|<7C>p<EFBFBD>B,<EFBFBD>B<EFBFBD><EFBFBD>
|
|
||||||
,<EFBFBD><EFBFBD><EFBFBD>s<EFBFBD>|<7C>p<EFBFBD>B,<EFBFBD>|<7C>p<EFBFBD><70>,<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,<EFBFBD><EFBFBD><EFBFBD>s<EFBFBD>|<7C>p<EFBFBD>B,<EFBFBD>|<7C>p<EFBFBD><70>,<EFBFBD>Ҫ<EFBFBD>
|
|
||||||
,<EFBFBD><EFBFBD><EFBFBD>s<EFBFBD>|<7C>p<EFBFBD>B,,<EFBFBD>M<EFBFBD><EFBFBD>
|
|
||||||
,<EFBFBD><EFBFBD><EFBFBD>s<EFBFBD>|<7C>p<EFBFBD>B,<EFBFBD>|<7C>Ƚ<EFBFBD>,<EFBFBD>Ҫ<EFBFBD>
|
|
||||||
,<EFBFBD><EFBFBD><EFBFBD>s<EFBFBD>|<7C>p<EFBFBD>B,,<EFBFBD>M<EFBFBD><EFBFBD>
|
|
||||||
,<EFBFBD><EFBFBD><EFBFBD>s<EFBFBD>|<7C>p<EFBFBD>B,<EFBFBD>z<EFBFBD>|<7C>p<EFBFBD><70>,<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,<EFBFBD><EFBFBD><EFBFBD>s<EFBFBD>|<7C>p<EFBFBD>B,<EFBFBD>ʸ˺z<EFBFBD>|<7C>p<EFBFBD><70>,<EFBFBD>Ҫ<EFBFBD>
|
|
||||||
,<EFBFBD><EFBFBD><EFBFBD>s<EFBFBD>|<7C>p<EFBFBD>B,,<EFBFBD>M<EFBFBD><EFBFBD>
|
|
||||||
,<EFBFBD><EFBFBD><EFBFBD>s<EFBFBD>|<7C>p<EFBFBD>B,<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>z<EFBFBD>|<7C>p<EFBFBD><70>,<EFBFBD>Ҫ<EFBFBD>
|
|
||||||
,<EFBFBD><EFBFBD><EFBFBD>s<EFBFBD>|<7C>p<EFBFBD>B,,<EFBFBD>M<EFBFBD><EFBFBD>
|
|
||||||
,<EFBFBD><EFBFBD><EFBFBD>η|<7C>p<EFBFBD>B,<EFBFBD><EFBFBD><EFBFBD>η|<7C>p<EFBFBD>B,<EFBFBD>B<EFBFBD><EFBFBD>
|
|
||||||
,<EFBFBD><EFBFBD><EFBFBD>η|<7C>p<EFBFBD>B,<EFBFBD><EFBFBD><EFBFBD>ΦX<EFBFBD>ֳ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>,<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,<EFBFBD><EFBFBD><EFBFBD>η|<7C>p<EFBFBD>B,,<EFBFBD>M<EFBFBD><EFBFBD>
|
|
||||||
<EFBFBD><EFBFBD><EFBFBD>θ<EFBFBD><EFBFBD>T<EFBFBD>Ʒ~<7E><>,<EFBFBD><EFBFBD><EFBFBD>θ<EFBFBD><EFBFBD>T<EFBFBD>Ʒ~<7E><>,<EFBFBD><EFBFBD><EFBFBD>θ<EFBFBD><EFBFBD>T<EFBFBD>Ʒ~<7E><>,<EFBFBD><EFBFBD><EFBFBD>T<EFBFBD><EFBFBD>
|
|
||||||
,<EFBFBD><EFBFBD><EFBFBD>w<EFBFBD><EFBFBD><EFBFBD>ʤp<EFBFBD><EFBFBD>,<EFBFBD><EFBFBD><EFBFBD>w<EFBFBD><EFBFBD><EFBFBD>ʽ<EFBFBD>,<EFBFBD>Ҫ<EFBFBD>
|
|
||||||
,<EFBFBD><EFBFBD><EFBFBD>T<EFBFBD>@<40>B,<EFBFBD><EFBFBD><EFBFBD>Ψt<EFBFBD>γ<EFBFBD>,<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,<EFBFBD><EFBFBD><EFBFBD>T<EFBFBD>@<40>B,,<EFBFBD>u<EFBFBD>{<7B>v
|
|
||||||
,<EFBFBD><EFBFBD><EFBFBD>T<EFBFBD>@<40>B,<EFBFBD>q<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>X<EFBFBD>s<EFBFBD>y<EFBFBD><EFBFBD>,<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,<EFBFBD><EFBFBD><EFBFBD>T<EFBFBD>@<40>B,,<EFBFBD>u<EFBFBD>{<7B>v
|
|
||||||
,<EFBFBD><EFBFBD><EFBFBD>T<EFBFBD>@<40>B,<EFBFBD>t<EFBFBD>κ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>A<EFBFBD>ȳ<EFBFBD>,<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,<EFBFBD><EFBFBD><EFBFBD>T<EFBFBD>@<40>B,,<EFBFBD>u<EFBFBD>{<7B>v
|
|
||||||
,<EFBFBD><EFBFBD><EFBFBD>T<EFBFBD>G<EFBFBD>B,<EFBFBD><EFBFBD><EFBFBD>T<EFBFBD>G<EFBFBD>B,<EFBFBD>B<EFBFBD><EFBFBD>
|
|
||||||
<EFBFBD>s<EFBFBD>ШƷ~<7E><>,<EFBFBD>s<EFBFBD>ШƷ~<7E><>,<EFBFBD>s<EFBFBD>ШƷ~<7E><>,<EFBFBD>B<EFBFBD><EFBFBD>
|
|
||||||
,<EFBFBD>s<EFBFBD>ШƷ~<7E><>,<EFBFBD>귽<EFBFBD>z<EFBFBD><EFBFBD>,<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,<EFBFBD>s<EFBFBD>ШƷ~<7E><>,,<EFBFBD>M<EFBFBD><EFBFBD>
|
|
||||||
,<EFBFBD><EFBFBD><EFBFBD>o<EFBFBD><EFBFBD><EFBFBD><EFBFBD>,<EFBFBD><EFBFBD><EFBFBD>C<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>~<7E><><EFBFBD>o<EFBFBD>B,<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,<EFBFBD><EFBFBD><EFBFBD>o<EFBFBD><EFBFBD><EFBFBD><EFBFBD>,,<EFBFBD>u<EFBFBD>{<7B>v
|
|
||||||
,<EFBFBD><EFBFBD><EFBFBD>o<EFBFBD><EFBFBD><EFBFBD><EFBFBD>,<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>~<7E><><EFBFBD>o<EFBFBD>B,<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,<EFBFBD><EFBFBD><EFBFBD>o<EFBFBD><EFBFBD><EFBFBD><EFBFBD>,,<EFBFBD>u<EFBFBD>{<7B>v
|
|
||||||
<EFBFBD>]<5D>֫<EFBFBD>,<EFBFBD>]<5D>֫<EFBFBD>,<EFBFBD>]<5D>֫<EFBFBD>,<EFBFBD>D<EFBFBD><EFBFBD>
|
|
||||||
,,,<EFBFBD>M<EFBFBD><EFBFBD>
|
|
||||||
<EFBFBD>`<60>g<EFBFBD>z<EFBFBD><7A>,<EFBFBD>`<60>g<EFBFBD>z<EFBFBD><7A>,<EFBFBD>`<60>g<EFBFBD>z<EFBFBD><7A>,<EFBFBD>`<60><>
|
|
||||||
,,,<EFBFBD>`<60>g<EFBFBD>z
|
|
||||||
,ESG<EFBFBD>M<EFBFBD>줽<EFBFBD><EFBFBD>,ESG<EFBFBD>M<EFBFBD>줽<EFBFBD><EFBFBD>,<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,,<EFBFBD><EFBFBD><EFBFBD>ҥ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>p<EFBFBD><EFBFBD>,<EFBFBD>Ҫ<EFBFBD>
|
|
||||||
,,,<EFBFBD>M<EFBFBD><EFBFBD>/<2F>u<EFBFBD>{<7B>v
|
|
||||||
,,<EFBFBD><EFBFBD><EFBFBD>|<7C><><EFBFBD><EFBFBD><EFBFBD>p<EFBFBD><70>,<EFBFBD>Ҫ<EFBFBD>
|
|
||||||
,,,<EFBFBD>M<EFBFBD><EFBFBD>/<2F>u<EFBFBD>{<7B>v
|
|
||||||
,,<EFBFBD><EFBFBD><EFBFBD>q<EFBFBD>v<EFBFBD>z<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>p<EFBFBD><EFBFBD>,<EFBFBD>Ҫ<EFBFBD>
|
|
||||||
,,,<EFBFBD>M<EFBFBD><EFBFBD>/<2F>u<EFBFBD>{<7B>v
|
|
||||||
,<EFBFBD>M<EFBFBD>z<EFBFBD><EFBFBD>,<EFBFBD>M<EFBFBD>z<EFBFBD><EFBFBD>,<EFBFBD><EFBFBD><EFBFBD>`<60>g<EFBFBD>z
|
|
||||||
,,<EFBFBD>M<EFBFBD>z,<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,,,<EFBFBD>M<EFBFBD><EFBFBD>/<2F>u<EFBFBD>{<7B>v
|
|
||||||
,,PVS<EFBFBD>M<EFBFBD><EFBFBD>,<EFBFBD>M<EFBFBD><EFBFBD>/<2F>u<EFBFBD>{<7B>v
|
|
||||||
<EFBFBD>`<60>~<7E><><EFBFBD>Ʒ~<7E><>,<EFBFBD>`<60>~<7E><><EFBFBD>Ʒ~<7E><>,<EFBFBD>`<60>~<7E><><EFBFBD>Ʒ~<7E><>,<EFBFBD>B<EFBFBD><EFBFBD>
|
|
||||||
,,<EFBFBD>Ȥ<EFBFBD><EFBFBD>~<7E><><EFBFBD>z<DEB2><7A>,<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,,<EFBFBD>Ȥ<EFBFBD><EFBFBD>~<7E><><EFBFBD>u<EFBFBD>{<7B><>,<EFBFBD>Ҫ<EFBFBD>
|
|
||||||
,,,<EFBFBD>u<EFBFBD>{<7B>v
|
|
||||||
,,,<EFBFBD>M<EFBFBD><EFBFBD>
|
|
||||||
,,<EFBFBD>~<7E><><EFBFBD>u<EFBFBD>{<7B><>,<EFBFBD>Ҫ<EFBFBD>
|
|
||||||
,,,<EFBFBD>u<EFBFBD>{<7B>v
|
|
||||||
,,<EFBFBD><EFBFBD><EFBFBD>~<7E>~<7E><><EFBFBD>z<DEB2><7A>,<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,,<EFBFBD>s<EFBFBD><EFBFBD><EFBFBD>~<7E>~<7E><><EFBFBD>z<DEB2><7A>,<EFBFBD>Ҫ<EFBFBD>
|
|
||||||
,,,<EFBFBD>u<EFBFBD>{<7B>v
|
|
||||||
,,<EFBFBD>ܧ<EFBFBD><EFBFBD>z<EFBFBD><EFBFBD>,<EFBFBD>Ҫ<EFBFBD>
|
|
||||||
,,,<EFBFBD>u<EFBFBD>{<7B>v
|
|
||||||
,,<EFBFBD>Ȥ<EFBFBD><EFBFBD>䴩<EFBFBD>u<EFBFBD>{<7B><>,<EFBFBD>Ҫ<EFBFBD>
|
|
||||||
,,,<EFBFBD>u<EFBFBD>{<7B>v
|
|
||||||
,,,<EFBFBD>Z<EFBFBD><EFBFBD>
|
|
||||||
,,,<EFBFBD>@<40>~<7E><>
|
|
||||||
,,<EFBFBD>~<7E><><EFBFBD>t<EFBFBD>ΤΫȤ<CEAB><C8A4>u<EFBFBD>{<7B><><EFBFBD>X<EFBFBD><58>,<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,,<EFBFBD>Ȥ<EFBFBD><EFBFBD>u<EFBFBD>{<7B><><EFBFBD>X<EFBFBD><58>,<EFBFBD>Ҫ<EFBFBD>
|
|
||||||
,,,<EFBFBD>u<EFBFBD>{<7B>v
|
|
||||||
,,<EFBFBD>~<7E><><EFBFBD>t<EFBFBD>ν<EFBFBD>,<EFBFBD>Ҫ<EFBFBD>
|
|
||||||
,,,<EFBFBD>u<EFBFBD>{<7B>v
|
|
||||||
,,<EFBFBD>ʴ<EFBFBD><EFBFBD>~<7E>]<5D>~<7E><><EFBFBD>z<DEB2><7A>,<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,,,<EFBFBD>Ҫ<EFBFBD>
|
|
||||||
,,,<EFBFBD>u<EFBFBD>{<7B>v
|
|
||||||
,,<EFBFBD>~<7E><><EFBFBD>O<EFBFBD>ҳ<EFBFBD>,<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,,<EFBFBD><EFBFBD><EFBFBD>Ĥ<EFBFBD><EFBFBD>R<EFBFBD><EFBFBD>,<EFBFBD>Ҫ<EFBFBD>
|
|
||||||
,,,<EFBFBD>u<EFBFBD>{<7B>v
|
|
||||||
,,<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ӻz<EFBFBD><EFBFBD>,<EFBFBD>Ҫ<EFBFBD>
|
|
||||||
,,,<EFBFBD>u<EFBFBD>{<7B>v
|
|
||||||
,,,<EFBFBD>Z<EFBFBD><EFBFBD>
|
|
||||||
,,,<EFBFBD>ƯZ<EFBFBD><EFBFBD>
|
|
||||||
,,,<EFBFBD>@<40>~<7E><>
|
|
||||||
,,<EFBFBD>H<EFBFBD><EFBFBD><EFBFBD>ʫO<EFBFBD>ҽ<EFBFBD>,<EFBFBD>Ҫ<EFBFBD>
|
|
||||||
,,,<EFBFBD>u<EFBFBD>{<7B>v
|
|
||||||
,,,<EFBFBD>Z<EFBFBD><EFBFBD>
|
|
||||||
,,,<EFBFBD>ƯZ<EFBFBD><EFBFBD>
|
|
||||||
,,,<EFBFBD>@<40>~<7E><>
|
|
||||||
,,<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ӻz<EFBFBD><EFBFBD>,<EFBFBD>Ҫ<EFBFBD>
|
|
||||||
,,,<EFBFBD>u<EFBFBD>{<7B>v
|
|
||||||
<EFBFBD><EFBFBD><EFBFBD>~<7E>Ʒ~<7E><>,<EFBFBD><EFBFBD><EFBFBD>~<7E>Ʒ~<7E><>,<EFBFBD><EFBFBD><EFBFBD>~<7E>Ʒ~<7E><>,<EFBFBD><EFBFBD><EFBFBD>`<60>g<EFBFBD>z
|
|
||||||
,,,<EFBFBD><EFBFBD><EFBFBD>`<60>g<EFBFBD>z<EFBFBD>U<EFBFBD>z
|
|
||||||
,<EFBFBD>ӷ~<7E>}<7D>o<EFBFBD>[<5B><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>γB,<EFBFBD>ӷ~<7E>}<7D>o<EFBFBD>[<5B><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>γB,<EFBFBD>B<EFBFBD><EFBFBD>
|
|
||||||
,,,<EFBFBD>g<EFBFBD>z
|
|
||||||
,,,<EFBFBD>u<EFBFBD>{<7B>v
|
|
||||||
,<EFBFBD><EFBFBD><EFBFBD>~<7E>P<EFBFBD><50><EFBFBD>Ʒ~<7E>B,<EFBFBD><EFBFBD><EFBFBD>~<7E>P<EFBFBD><50><EFBFBD>Ʒ~<7E>B,<EFBFBD>B<EFBFBD><EFBFBD>
|
|
||||||
,,<EFBFBD>饻<EFBFBD>Ϻ[<5B>N<EFBFBD>u<EFBFBD>~<7E>ȳ<EFBFBD>,<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,,<EFBFBD>饻<EFBFBD><EFBFBD>,<EFBFBD>Ҫ<EFBFBD>
|
|
||||||
,,,<EFBFBD>M<EFBFBD><EFBFBD>
|
|
||||||
,,,<EFBFBD>U<EFBFBD>z
|
|
||||||
,,<EFBFBD>N<EFBFBD>u,<EFBFBD>Ҫ<EFBFBD>
|
|
||||||
,,,<EFBFBD>M<EFBFBD><EFBFBD>
|
|
||||||
,,,<EFBFBD>U<EFBFBD>z
|
|
||||||
,,<EFBFBD>ڨȰϷ~<7E>ȳ<EFBFBD>,<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,,,<EFBFBD>U<EFBFBD>z
|
|
||||||
,,<EFBFBD>ڬw,<EFBFBD>Ҫ<EFBFBD>
|
|
||||||
,,,<EFBFBD>M<EFBFBD><EFBFBD>
|
|
||||||
,,,<EFBFBD>U<EFBFBD>z
|
|
||||||
,,<EFBFBD>n<EFBFBD><EFBFBD>,<EFBFBD>Ҫ<EFBFBD>
|
|
||||||
,,,<EFBFBD>M<EFBFBD><EFBFBD>
|
|
||||||
,,,<EFBFBD>U<EFBFBD>z
|
|
||||||
,,<EFBFBD>F<EFBFBD><EFBFBD>,<EFBFBD>Ҫ<EFBFBD>
|
|
||||||
,,,<EFBFBD>M<EFBFBD><EFBFBD>
|
|
||||||
,,,<EFBFBD>U<EFBFBD>z
|
|
||||||
,,<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ϸ~<7E>ȳ<EFBFBD>-<2D><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>,<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,,,<EFBFBD>Ҫ<EFBFBD>
|
|
||||||
,,,<EFBFBD>M<EFBFBD><EFBFBD>
|
|
||||||
,,,<EFBFBD>U<EFBFBD>z
|
|
||||||
,,<EFBFBD><EFBFBD><EFBFBD>y<EFBFBD><EFBFBD><EFBFBD>ϫȤ<EFBFBD><EFBFBD>z,<EFBFBD>M<EFBFBD>g<EFBFBD>z
|
|
||||||
,,,<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,,<EFBFBD><EFBFBD><EFBFBD>w<EFBFBD>Ϸ~<7E>ȳ<EFBFBD>,<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,,,<EFBFBD>Ҫ<EFBFBD>
|
|
||||||
,,,<EFBFBD>M<EFBFBD><EFBFBD>
|
|
||||||
,,,<EFBFBD>U<EFBFBD>z
|
|
||||||
,<EFBFBD><EFBFBD><EFBFBD>y<EFBFBD>N<EFBFBD>A<EFBFBD>ȳB,<EFBFBD><EFBFBD><EFBFBD>y<EFBFBD>N<EFBFBD>A<EFBFBD>ȳB,<EFBFBD>B<EFBFBD><EFBFBD>
|
|
||||||
,,,<EFBFBD>u<EFBFBD>{<7B>v
|
|
||||||
,,,<EFBFBD>U<EFBFBD>z
|
|
||||||
,,<EFBFBD><EFBFBD><EFBFBD>Τu<EFBFBD>{<7B><>(GTS),<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,,,<EFBFBD>M<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,,,<EFBFBD>N<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,,,<EFBFBD>u<EFBFBD>{<7B>v
|
|
||||||
,,<EFBFBD>t<EFBFBD>Τu<EFBFBD>{<7B><>,<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,,,<EFBFBD>u<EFBFBD>{<7B>v
|
|
||||||
,,<EFBFBD>S<EFBFBD>ʴ<EFBFBD><EFBFBD>ճ<EFBFBD>,<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,,<EFBFBD>S<EFBFBD>ʴ<EFBFBD><EFBFBD>ս<EFBFBD>,<EFBFBD>Ҫ<EFBFBD>
|
|
||||||
,,,<EFBFBD>u<EFBFBD>{<7B>v
|
|
||||||
,<EFBFBD><EFBFBD><EFBFBD>y<EFBFBD><EFBFBD><EFBFBD>P<EFBFBD>[<5B>~<7E>Ȥ䴩<C8A4>B,<EFBFBD><EFBFBD><EFBFBD>y<EFBFBD><EFBFBD><EFBFBD>P<EFBFBD>[<5B>~<7E>Ȥ䴩<C8A4>B,<EFBFBD><EFBFBD><EFBFBD>`<60>g<EFBFBD>z
|
|
||||||
,,<EFBFBD>~<7E>ȥͺ<CDBA>,<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,,<EFBFBD>~<7E>ȥͺ<CDBA>,<EFBFBD>Ҫ<EFBFBD>
|
|
||||||
,,,<EFBFBD>M<EFBFBD><EFBFBD>
|
|
||||||
,,<EFBFBD><EFBFBD><EFBFBD>y&<26><><EFBFBD><EFBFBD>,<EFBFBD>Ҫ<EFBFBD>
|
|
||||||
,,,<EFBFBD>M<EFBFBD><EFBFBD>
|
|
||||||
,,<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>P<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>,<EFBFBD>B<EFBFBD><EFBFBD>
|
|
||||||
,,,<EFBFBD>g<EFBFBD>z
|
|
||||||
,,,<EFBFBD>M<EFBFBD><EFBFBD>
|
|
||||||
,,<EFBFBD><EFBFBD><EFBFBD>P<EFBFBD><EFBFBD><EFBFBD>s,<EFBFBD>M<EFBFBD><EFBFBD>
|
|
||||||
,,<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Բ<EFBFBD>,<EFBFBD>M<EFBFBD><EFBFBD>
|
|
||||||
,,MOSFET<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʳ<EFBFBD>,<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,,,<EFBFBD>Ҫ<EFBFBD>
|
|
||||||
,,,<EFBFBD>M<EFBFBD><EFBFBD>
|
|
||||||
,<EFBFBD>j<EFBFBD><EFBFBD><EFBFBD>ذϾP<EFBFBD><EFBFBD><EFBFBD>Ʒ~<7E>B,<EFBFBD>j<EFBFBD><EFBFBD><EFBFBD>ذϾP<EFBFBD><EFBFBD><EFBFBD>Ʒ~<7E>B,<EFBFBD>B<EFBFBD><EFBFBD>
|
|
||||||
,,<EFBFBD>x<EFBFBD>W<EFBFBD>Ϸ~<7E>ȳ<EFBFBD>,<EFBFBD>M<EFBFBD><EFBFBD>
|
|
||||||
,,,<EFBFBD>U<EFBFBD>z
|
|
||||||
,,<EFBFBD>~<7E>Ȥ@<40><>,<EFBFBD>B<EFBFBD><EFBFBD>/<2F><><EFBFBD>`<60>g<EFBFBD>z
|
|
||||||
,,,<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,,,<EFBFBD>M<EFBFBD><EFBFBD>
|
|
||||||
,,,<EFBFBD>U<EFBFBD>z
|
|
||||||
,,<EFBFBD>~<7E>ȤG<C8A4><47>,<EFBFBD>B<EFBFBD><EFBFBD>/<2F><><EFBFBD>`<60>g<EFBFBD>z
|
|
||||||
,,,<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,,,<EFBFBD>M<EFBFBD><EFBFBD>
|
|
||||||
,,,<EFBFBD>U<EFBFBD>z
|
|
||||||
,,HH<EFBFBD>M<EFBFBD>ײ<EFBFBD>,<EFBFBD>g<EFBFBD>Ʋz
|
|
||||||
,,,<EFBFBD>M<EFBFBD><EFBFBD>
|
|
||||||
,,,<EFBFBD>U<EFBFBD>z
|
|
||||||
|
Reference in New Issue
Block a user