Compare commits
10 Commits
b2584772c4
...
backup/bef
| Author | SHA1 | Date | |
|---|---|---|---|
| a6af297623 | |||
| a068ef9704 | |||
| ea745ffefc | |||
| 8069f1b628 | |||
| 12ceccc3d3 | |||
| ee3db29c32 | |||
| 880c23b844 | |||
| 8902f25f6e | |||
| 78b42ce98c | |||
| 15e32a2aef |
@@ -25,7 +25,21 @@
|
|||||||
"Bash(python add_dept_relation.py:*)",
|
"Bash(python add_dept_relation.py:*)",
|
||||||
"Bash(cat:*)",
|
"Bash(cat:*)",
|
||||||
"Bash(python add_random_positions.py:*)",
|
"Bash(python add_random_positions.py:*)",
|
||||||
"Bash(timeout /t 3 /nobreak)"
|
"Bash(timeout /t 3 /nobreak)",
|
||||||
|
"Bash(python -m json.tool:*)",
|
||||||
|
"Bash(python app.py:*)",
|
||||||
|
"Bash(tasklist:*)",
|
||||||
|
"Bash(findstr:*)",
|
||||||
|
"Bash(netstat:*)",
|
||||||
|
"Bash(powershell -Command \"Stop-Process -Id 44816,14404,45900 -Force\")",
|
||||||
|
"Bash(powershell -Command \"Get-Process python | Stop-Process -Force\")",
|
||||||
|
"Bash(python llm_config.py:*)",
|
||||||
|
"Bash(python:*)",
|
||||||
|
"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"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -17,8 +17,8 @@ GITEA_TOKEN=your_gitea_access_token
|
|||||||
|
|
||||||
# ==================== LLM API Keys ====================
|
# ==================== LLM API Keys ====================
|
||||||
# Google Gemini API
|
# Google Gemini API
|
||||||
GEMINI_API_KEY=your_gemini_api_key_here
|
GEMINI_API_KEY=AIzaSyDWD6TdXgtYyKvmGLF0RiN8AkbSF8eDnHY
|
||||||
GEMINI_MODEL=gemini-1.5-flash
|
GEMINI_MODEL=gemini-2.5-flash
|
||||||
|
|
||||||
# DeepSeek API
|
# DeepSeek API
|
||||||
DEEPSEEK_API_KEY=your_deepseek_api_key_here
|
DEEPSEEK_API_KEY=your_deepseek_api_key_here
|
||||||
@@ -28,6 +28,10 @@ DEEPSEEK_API_URL=https://api.deepseek.com/v1
|
|||||||
OPENAI_API_KEY=your_openai_api_key_here
|
OPENAI_API_KEY=your_openai_api_key_here
|
||||||
OPENAI_API_URL=https://api.openai.com/v1
|
OPENAI_API_URL=https://api.openai.com/v1
|
||||||
|
|
||||||
|
# Ollama API
|
||||||
|
OLLAMA_API_URL=https://ollama_pjapi.theaken.com
|
||||||
|
OLLAMA_MODEL=deepseek-reasoner
|
||||||
|
|
||||||
# ==================== Flask Configuration ====================
|
# ==================== Flask Configuration ====================
|
||||||
FLASK_APP=start_server.py
|
FLASK_APP=start_server.py
|
||||||
FLASK_ENV=development
|
FLASK_ENV=development
|
||||||
|
|||||||
558
SDD_代碼分離優化.md
Normal file
558
SDD_代碼分離優化.md
Normal file
@@ -0,0 +1,558 @@
|
|||||||
|
# 軟體設計文件 (SDD) - 代碼分離優化
|
||||||
|
|
||||||
|
|
||||||
|
### 📁 推薦的最終檔案結構
|
||||||
|
|
||||||
|
```
|
||||||
|
d:\00001_Vibe_coding\1204剛為\
|
||||||
|
├── index.html (僅 HTML 結構,約 1,500 行)
|
||||||
|
├── login.html (僅 HTML 結構,約 200 行)
|
||||||
|
│
|
||||||
|
├── styles/ (CSS 樣式)
|
||||||
|
│ ├── base.css (基礎樣式、變數、Reset)
|
||||||
|
│ ├── layout.css (整體布局)
|
||||||
|
│ ├── components.css (按鈕、表單、卡片元件)
|
||||||
|
│ ├── modules.css (5 個頁籤的專屬樣式)
|
||||||
|
│ ├── utilities.css (工具類別)
|
||||||
|
│ └── login.css (登入頁專屬樣式)
|
||||||
|
│
|
||||||
|
├── js/ (JavaScript 程式碼)
|
||||||
|
│ ├── config.js (設定:API URL、常數)
|
||||||
|
│ ├── utils.js (工具函式)
|
||||||
|
│ ├── api.js (API 呼叫)
|
||||||
|
│ ├── ui.js (UI 操作)
|
||||||
|
│ ├── form-handlers.js (表單處理)
|
||||||
|
│ ├── ai-generators.js (AI 生成函式)
|
||||||
|
│ ├── dropdown.js (下拉選單邏輯)
|
||||||
|
│ ├── validators.js (表單驗證)
|
||||||
|
│ └── main.js (主程式、初始化)
|
||||||
|
│
|
||||||
|
├── data/ (資料檔案 - 已存在)
|
||||||
|
│ ├── hierarchical_data.js ✅ 已存在
|
||||||
|
│ └── dropdown_data.js ✅ 已存在
|
||||||
|
│
|
||||||
|
├── docs/ (文件)
|
||||||
|
│ ├── prompt.md ✅ 已存在
|
||||||
|
│ ├── 權限矩陣.md ✅ 已存在
|
||||||
|
│ └── SDD_代碼分離優化.md (本文件)
|
||||||
|
│
|
||||||
|
├── app.py ✅ 後端程式
|
||||||
|
├── .env ✅ 環境設定
|
||||||
|
├── .gitignore
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 分離策略
|
||||||
|
|
||||||
|
### 📝 CSS 分離策略詳解
|
||||||
|
|
||||||
|
#### 1. base.css - 基礎樣式(約 150 行)
|
||||||
|
```css
|
||||||
|
/**
|
||||||
|
* CSS Variables - 主題色、間距、字體
|
||||||
|
* CSS Reset - Normalize.css
|
||||||
|
* 全域樣式 - body, html, *
|
||||||
|
*/
|
||||||
|
```
|
||||||
|
**包含內容**:
|
||||||
|
- CSS 變數定義(顏色、間距、字體、陰影等)
|
||||||
|
- CSS Reset(統一瀏覽器預設樣式)
|
||||||
|
- 全域樣式(body、html、通用選擇器)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 2. layout.css - 布局樣式(約 100 行)
|
||||||
|
```css
|
||||||
|
/**
|
||||||
|
* Container - 容器
|
||||||
|
* Grid System - 網格系統
|
||||||
|
* Flexbox Layout - 彈性布局
|
||||||
|
* Header/Footer - 頁首頁尾
|
||||||
|
*/
|
||||||
|
```
|
||||||
|
**包含內容**:
|
||||||
|
- `.container`、`.main-content`
|
||||||
|
- Grid 和 Flexbox 布局類別
|
||||||
|
- 頁面整體結構(header、footer、sidebar)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 3. components.css - 元件樣式(約 250 行)
|
||||||
|
```css
|
||||||
|
/**
|
||||||
|
* Buttons - 按鈕(.btn-primary, .ai-generate-btn)
|
||||||
|
* Forms - 表單(.form-group, input, select, textarea)
|
||||||
|
* Cards - 卡片(.info-card, .module-card)
|
||||||
|
* Tabs - 頁籤(.tab-nav, .tab-panel)
|
||||||
|
* Modals - 彈窗
|
||||||
|
* Toast - 提示訊息
|
||||||
|
*/
|
||||||
|
```
|
||||||
|
**包含內容**:
|
||||||
|
- 可重用的 UI 元件
|
||||||
|
- 按鈕樣式(各種變體)
|
||||||
|
- 表單元件(input、select、textarea、checkbox)
|
||||||
|
- 卡片、頁籤、彈窗等元件
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 4. modules.css - 模組樣式(約 150 行)
|
||||||
|
```css
|
||||||
|
/**
|
||||||
|
* Position Basic - 崗位基礎資料模組
|
||||||
|
* Position Recruit - 崗位招聘要求模組
|
||||||
|
* Job Basic - 職務基礎資料模組
|
||||||
|
* Dept Function - 部門職責維護模組
|
||||||
|
* Job Desc - 崗位描述模組
|
||||||
|
* Position List - 崗位清單模組
|
||||||
|
*/
|
||||||
|
```
|
||||||
|
**包含內容**:
|
||||||
|
- 各頁籤的專屬樣式
|
||||||
|
- 特殊布局需求
|
||||||
|
- 模組內的特殊元件
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 5. utilities.css - 工具類別(約 50 行)
|
||||||
|
```css
|
||||||
|
/**
|
||||||
|
* Spacing - 間距(m-1, p-2)
|
||||||
|
* Text - 文字(text-center, text-bold)
|
||||||
|
* Display - 顯示(d-none, d-block)
|
||||||
|
* Colors - 顏色(text-primary, bg-success)
|
||||||
|
* Responsive - 響應式(@media)
|
||||||
|
*/
|
||||||
|
```
|
||||||
|
**包含內容**:
|
||||||
|
- 快速工具類別
|
||||||
|
- 響應式斷點
|
||||||
|
- 常用的單一用途類別
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 💻 JavaScript 分離策略詳解
|
||||||
|
|
||||||
|
#### 1. config.js - 設定檔(約 50 行)
|
||||||
|
```javascript
|
||||||
|
/**
|
||||||
|
* API 端點設定
|
||||||
|
* 常數定義
|
||||||
|
* 環境設定
|
||||||
|
*/
|
||||||
|
export const API_BASE_URL = 'http://127.0.0.1:5000/api';
|
||||||
|
export const TOAST_DURATION = 3000;
|
||||||
|
export const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 2. utils.js - 工具函式(約 200 行)
|
||||||
|
```javascript
|
||||||
|
/**
|
||||||
|
* showToast() - 顯示提示訊息
|
||||||
|
* fillIfEmpty() - 填充空白欄位
|
||||||
|
* getFieldValue() - 取得欄位值
|
||||||
|
* setButtonLoading() - 設定按鈕載入狀態
|
||||||
|
* formatDate() - 日期格式化
|
||||||
|
* debounce() - 防抖函式
|
||||||
|
*/
|
||||||
|
export function showToast(message, type = 'info') { ... }
|
||||||
|
export function fillIfEmpty(fieldId, value) { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 3. api.js - API 呼叫(約 150 行)
|
||||||
|
```javascript
|
||||||
|
/**
|
||||||
|
* callClaudeAPI() - 呼叫 LLM API
|
||||||
|
* fetchPositions() - 取得崗位清單
|
||||||
|
* createPosition() - 建立崗位
|
||||||
|
* updatePosition() - 更新崗位
|
||||||
|
* deletePosition() - 刪除崗位
|
||||||
|
*/
|
||||||
|
export async function callClaudeAPI(prompt) { ... }
|
||||||
|
export async function fetchPositions() { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 4. ui.js - UI 操作(約 300 行)
|
||||||
|
```javascript
|
||||||
|
/**
|
||||||
|
* switchModule() - 切換模組
|
||||||
|
* updatePreview() - 更新預覽
|
||||||
|
* showModal() - 顯示彈窗
|
||||||
|
* hideModal() - 隱藏彈窗
|
||||||
|
* renderPositionList() - 渲染崗位清單
|
||||||
|
*/
|
||||||
|
export function switchModule(moduleName) { ... }
|
||||||
|
export function updatePreview() { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 5. form-handlers.js - 表單處理(約 400 行)
|
||||||
|
```javascript
|
||||||
|
/**
|
||||||
|
* validatePositionForm() - 驗證崗位表單
|
||||||
|
* submitPositionForm() - 提交崗位表單
|
||||||
|
* clearForm() - 清除表單
|
||||||
|
* saveToPositionList() - 儲存至崗位清單
|
||||||
|
*/
|
||||||
|
export function validatePositionForm() { ... }
|
||||||
|
export async function submitPositionForm() { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 6. ai-generators.js - AI 生成(約 500 行)
|
||||||
|
```javascript
|
||||||
|
/**
|
||||||
|
* generatePositionBasic() - 生成崗位基礎資料
|
||||||
|
* generatePositionRecruit() - 生成崗位招聘要求
|
||||||
|
* generateJobBasic() - 生成職務基礎資料
|
||||||
|
* generateDeptFunction() - 生成部門職責
|
||||||
|
* generateJobDesc() - 生成崗位描述
|
||||||
|
*/
|
||||||
|
export async function generatePositionBasic() { ... }
|
||||||
|
export async function generatePositionRecruit() { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 7. dropdown.js - 下拉選單(約 300 行)
|
||||||
|
```javascript
|
||||||
|
/**
|
||||||
|
* initializeDropdowns() - 初始化下拉選單
|
||||||
|
* onBusinessUnitChange() - 事業體改變處理
|
||||||
|
* onDivisionChange() - 處級單位改變處理
|
||||||
|
* onDepartmentChange() - 部級單位改變處理
|
||||||
|
* updatePositionDropdown() - 更新崗位下拉選單
|
||||||
|
*/
|
||||||
|
export function initializeDropdowns() { ... }
|
||||||
|
export function onBusinessUnitChange(event) { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 8. validators.js - 表單驗證(約 200 行)
|
||||||
|
```javascript
|
||||||
|
/**
|
||||||
|
* validateRequired() - 驗證必填欄位
|
||||||
|
* validateEmail() - 驗證電子郵件
|
||||||
|
* validateDate() - 驗證日期格式
|
||||||
|
* validateNumber() - 驗證數字範圍
|
||||||
|
*/
|
||||||
|
export function validateRequired(value) { ... }
|
||||||
|
export function validateEmail(email) { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 9. main.js - 主程式(約 150 行)
|
||||||
|
```javascript
|
||||||
|
/**
|
||||||
|
* 初始化應用程式
|
||||||
|
* 設定事件監聽器
|
||||||
|
* 載入使用者資訊
|
||||||
|
* 啟動應用
|
||||||
|
*/
|
||||||
|
import { initializeDropdowns } from './dropdown.js';
|
||||||
|
import { loadUserInfo } from './ui.js';
|
||||||
|
import { API_BASE_URL } from './config.js';
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
loadUserInfo();
|
||||||
|
initializeDropdowns();
|
||||||
|
// ... 其他初始化邏輯
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 風險評估
|
||||||
|
|
||||||
|
### ⚠️ 潛在風險與對策
|
||||||
|
|
||||||
|
| 風險項目 | 風險等級 | 影響範圍 | 對策 |
|
||||||
|
|---------|---------|---------|------|
|
||||||
|
| **載入順序錯誤** | 🔴 高 | 功能失效 | 使用 ES6 Modules 自動處理依賴 |
|
||||||
|
| **全域變數衝突** | 🟡 中 | 部分功能異常 | 使用模組化,避免全域污染 |
|
||||||
|
| **快取問題** | 🟡 中 | 更新未生效 | 使用版本號 `?v=1.0.0` |
|
||||||
|
| **路徑錯誤** | 🟢 低 | 載入失敗 | 使用相對路徑,充分測試 |
|
||||||
|
| **效能下降** | 🟢 低 | 載入變慢 | HTTP/2 多路復用,瀏覽器快取 |
|
||||||
|
| **回滾困難** | 🟡 中 | 無法還原 | 分階段執行,每階段 Git commit |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 執行計畫
|
||||||
|
|
||||||
|
### 📅 分階段執行時程(選項 B)
|
||||||
|
|
||||||
|
#### 階段 1️⃣: CSS 分離(預估 1-2 天)
|
||||||
|
|
||||||
|
**Day 1 上午**: CSS 檔案建立
|
||||||
|
- [ ] 建立 `styles/` 目錄
|
||||||
|
- [ ] 建立 `base.css`(CSS 變數、Reset)
|
||||||
|
- [ ] 建立 `layout.css`(布局)
|
||||||
|
- [ ] 建立 `components.css`(元件)
|
||||||
|
- [ ] 建立 `modules.css`(模組)
|
||||||
|
- [ ] 建立 `utilities.css`(工具)
|
||||||
|
|
||||||
|
**Day 1 下午**: CSS 程式碼搬移
|
||||||
|
- [ ] 從 index.html 複製 CSS 到對應檔案
|
||||||
|
- [ ] 轉換硬編碼顏色為 CSS 變數
|
||||||
|
- [ ] 在 index.html 引入 CSS 檔案
|
||||||
|
|
||||||
|
**Day 2 上午**: CSS 測試
|
||||||
|
- [ ] 測試各頁籤樣式正確性
|
||||||
|
- [ ] 測試響應式布局
|
||||||
|
- [ ] 測試按鈕、表單、卡片元件
|
||||||
|
- [ ] 修正樣式問題
|
||||||
|
|
||||||
|
**Day 2 下午**: CSS 優化與提交
|
||||||
|
- [ ] 檢查是否有重複樣式
|
||||||
|
- [ ] 優化選擇器效能
|
||||||
|
- [ ] Git commit: "refactor: separate CSS from index.html"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 階段 2️⃣: JavaScript 工具函式分離(預估 1-2 天)
|
||||||
|
|
||||||
|
**Day 3 上午**: 建立核心模組
|
||||||
|
- [ ] 建立 `js/` 目錄
|
||||||
|
- [ ] 建立 `config.js`(設定)
|
||||||
|
- [ ] 建立 `utils.js`(工具函式)
|
||||||
|
- [ ] 建立 `api.js`(API 呼叫)
|
||||||
|
|
||||||
|
**Day 3 下午**: 搬移工具函式
|
||||||
|
- [ ] 搬移 `showToast()`
|
||||||
|
- [ ] 搬移 `fillIfEmpty()`
|
||||||
|
- [ ] 搬移 `getFieldValue()`
|
||||||
|
- [ ] 搬移 `setButtonLoading()`
|
||||||
|
- [ ] 搬移 `callClaudeAPI()`
|
||||||
|
|
||||||
|
**Day 4 上午**: 測試核心功能
|
||||||
|
- [ ] 測試 Toast 提示
|
||||||
|
- [ ] 測試 API 呼叫
|
||||||
|
- [ ] 測試欄位填充
|
||||||
|
- [ ] 測試按鈕載入狀態
|
||||||
|
|
||||||
|
**Day 4 下午**: 提交
|
||||||
|
- [ ] Git commit: "refactor: separate utils and api modules"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 階段 3️⃣: JavaScript 模組分離(預估 2-3 天)
|
||||||
|
|
||||||
|
**Day 5**: UI 與表單處理
|
||||||
|
- [ ] 建立 `ui.js`
|
||||||
|
- [ ] 建立 `form-handlers.js`
|
||||||
|
- [ ] 建立 `validators.js`
|
||||||
|
- [ ] 搬移相關函式
|
||||||
|
- [ ] 測試表單功能
|
||||||
|
- [ ] Git commit
|
||||||
|
|
||||||
|
**Day 6**: AI 生成與下拉選單
|
||||||
|
- [ ] 建立 `ai-generators.js`
|
||||||
|
- [ ] 建立 `dropdown.js`
|
||||||
|
- [ ] 搬移 5 個 AI 生成函式
|
||||||
|
- [ ] 搬移下拉選單邏輯
|
||||||
|
- [ ] 測試 AI 生成功能
|
||||||
|
- [ ] 測試階層式下拉選單
|
||||||
|
- [ ] Git commit
|
||||||
|
|
||||||
|
**Day 7**: 主程式整合
|
||||||
|
- [ ] 建立 `main.js`
|
||||||
|
- [ ] 設定模組匯入
|
||||||
|
- [ ] 整合所有模組
|
||||||
|
- [ ] 完整功能測試
|
||||||
|
- [ ] Git commit
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 階段 4️⃣: 優化與收尾(預估 1 天)
|
||||||
|
|
||||||
|
**Day 8 上午**: 程式碼審查
|
||||||
|
- [ ] 檢查模組依賴關係
|
||||||
|
- [ ] 檢查是否有遺漏的函式
|
||||||
|
- [ ] 檢查命名一致性
|
||||||
|
- [ ] 檢查註解文件
|
||||||
|
|
||||||
|
**Day 8 下午**: 效能測試與文件
|
||||||
|
- [ ] 測試頁面載入速度
|
||||||
|
- [ ] 測試瀏覽器快取
|
||||||
|
- [ ] 更新 README.md
|
||||||
|
- [ ] 更新開發文件
|
||||||
|
- [ ] Git commit: "docs: update documentation for refactored code"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🔄 每日檢查清單
|
||||||
|
|
||||||
|
**每天開發前**:
|
||||||
|
- [ ] 拉取最新程式碼 `git pull`
|
||||||
|
- [ ] 建立功能分支(如有需要)
|
||||||
|
- [ ] 備份當前版本
|
||||||
|
|
||||||
|
**每天開發後**:
|
||||||
|
- [ ] 執行完整測試
|
||||||
|
- [ ] Commit 當日進度
|
||||||
|
- [ ] 更新進度表
|
||||||
|
- [ ] 記錄遇到的問題
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 驗收標準
|
||||||
|
|
||||||
|
### ✅ 功能驗收清單
|
||||||
|
|
||||||
|
#### 1. CSS 分離驗收
|
||||||
|
- [ ] 所有頁籤樣式正確顯示
|
||||||
|
- [ ] 按鈕顏色與漸層正確
|
||||||
|
- [ ] 表單元件樣式一致
|
||||||
|
- [ ] 響應式布局正常
|
||||||
|
- [ ] 沒有樣式閃爍(FOUC)
|
||||||
|
- [ ] 瀏覽器開發者工具無 CSS 錯誤
|
||||||
|
|
||||||
|
#### 2. JavaScript 分離驗收
|
||||||
|
- [ ] 所有頁籤功能正常
|
||||||
|
- [ ] AI 生成功能正常
|
||||||
|
- [ ] 表單驗證正常
|
||||||
|
- [ ] 下拉選單連動正常
|
||||||
|
- [ ] Toast 提示顯示正常
|
||||||
|
- [ ] API 呼叫正常
|
||||||
|
- [ ] 瀏覽器 Console 無錯誤
|
||||||
|
|
||||||
|
#### 3. 效能驗收
|
||||||
|
- [ ] 首次載入時間 ≤ 3 秒
|
||||||
|
- [ ] 後續載入時間 ≤ 1 秒(快取生效)
|
||||||
|
- [ ] 無記憶體洩漏
|
||||||
|
- [ ] CPU 使用率正常
|
||||||
|
|
||||||
|
#### 4. 相容性驗收
|
||||||
|
- [ ] Chrome 最新版正常
|
||||||
|
- [ ] Firefox 最新版正常
|
||||||
|
- [ ] Safari 最新版正常(如需支援)
|
||||||
|
- [ ] Edge 最新版正常
|
||||||
|
|
||||||
|
#### 5. 程式碼品質驗收
|
||||||
|
- [ ] 檔案結構清晰
|
||||||
|
- [ ] 命名規範一致
|
||||||
|
- [ ] 註解完整
|
||||||
|
- [ ] 無重複程式碼
|
||||||
|
- [ ] 模組依賴清晰
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 📊 效能指標
|
||||||
|
|
||||||
|
| 指標項目 | 當前值 | 目標值 | 驗收標準 |
|
||||||
|
|---------|--------|--------|---------|
|
||||||
|
| **首次載入時間** | ~2.5s | ≤ 3s | ✅ 通過 |
|
||||||
|
| **快取載入時間** | N/A | ≤ 1s | 待測試 |
|
||||||
|
| **CSS 檔案大小** | 內嵌 | ≤ 100KB | 待測試 |
|
||||||
|
| **JS 檔案大小** | 內嵌 | ≤ 300KB | 待測試 |
|
||||||
|
| **HTTP 請求數** | 3-5 個 | ≤ 15 個 | 待測試 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 附錄
|
||||||
|
|
||||||
|
### 📚 參考資源
|
||||||
|
|
||||||
|
#### 技術文件
|
||||||
|
- [ES6 Modules - MDN](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Guide/Modules)
|
||||||
|
- [CSS Variables - MDN](https://developer.mozilla.org/zh-TW/docs/Web/CSS/Using_CSS_custom_properties)
|
||||||
|
- [Airbnb JavaScript Style Guide](https://github.com/airbnb/javascript)
|
||||||
|
|
||||||
|
#### 最佳實踐
|
||||||
|
- [Google HTML/CSS Style Guide](https://google.github.io/styleguide/htmlcssguide.html)
|
||||||
|
- [Clean Code JavaScript](https://github.com/ryanmcdermott/clean-code-javascript)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🔧 開發工具建議
|
||||||
|
|
||||||
|
#### HTTP Server(本地開發)
|
||||||
|
如果選擇 ES6 Modules,需要使用 HTTP Server:
|
||||||
|
|
||||||
|
**選項 1: Python HTTP Server**
|
||||||
|
```bash
|
||||||
|
cd d:\00001_Vibe_coding\1204剛為
|
||||||
|
python -m http.server 8000
|
||||||
|
# 瀏覽器開啟: http://localhost:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
**選項 2: Live Server(VS Code 套件)**
|
||||||
|
- 安裝 VS Code 套件「Live Server」
|
||||||
|
- 右鍵點擊 index.html → "Open with Live Server"
|
||||||
|
|
||||||
|
**選項 3: Node.js http-server**
|
||||||
|
```bash
|
||||||
|
npm install -g http-server
|
||||||
|
cd d:\00001_Vibe_coding\1204剛為
|
||||||
|
http-server -p 8000
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 📝 Git Commit 規範建議
|
||||||
|
|
||||||
|
```
|
||||||
|
feat: 新功能
|
||||||
|
fix: 修復 Bug
|
||||||
|
refactor: 重構(不影響功能)
|
||||||
|
style: 樣式調整(不影響程式邏輯)
|
||||||
|
docs: 文件更新
|
||||||
|
test: 測試相關
|
||||||
|
chore: 建構工具或輔助工具變動
|
||||||
|
```
|
||||||
|
|
||||||
|
**範例**:
|
||||||
|
```bash
|
||||||
|
git commit -m "refactor: separate CSS into modular files"
|
||||||
|
git commit -m "refactor: extract utils and api modules"
|
||||||
|
git commit -m "docs: update SDD with final architecture"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 決策回覆表單
|
||||||
|
|
||||||
|
請將您的決策填寫在下方,我會根據您的選擇提供對應的實作方案:
|
||||||
|
|
||||||
|
```
|
||||||
|
=== 決策回覆表單 ===
|
||||||
|
|
||||||
|
決策 1 (CSS 分離策略): [ ]
|
||||||
|
決策 2 (JavaScript 分離策略): [ ]
|
||||||
|
決策 3 (模組載入方式): [ ]
|
||||||
|
決策 4 (共用樣式處理): [ ]
|
||||||
|
決策 5 (CSS 變數與主題): [ ]
|
||||||
|
決策 6 (執行順序): [ ]
|
||||||
|
決策 7 (命名規範): [ ]
|
||||||
|
決策 8 (瀏覽器相容性): [ ]
|
||||||
|
|
||||||
|
其他需求或建議:
|
||||||
|
_____________________________________
|
||||||
|
_____________________________________
|
||||||
|
_____________________________________
|
||||||
|
|
||||||
|
=== 表單結束 ===
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**文件狀態**: 📋 等待決策
|
||||||
|
**下一步**: 請您填寫決策後,我將提供詳細的實作步驟與程式碼範例
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> **提醒**: 此 SDD 文件會隨著專案進展持續更新。如有任何疑問或需要調整,歡迎隨時討論。
|
||||||
|
>
|
||||||
|
> ¯\\_(ツ)_/¯ 那都AI寫的,不要問我
|
||||||
@@ -1,484 +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(本文件)
|
|
||||||
- ⏳ 準備推送至 Gitea(排除 USER_COMMANDS_LOG.md)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📊 指令統計
|
|
||||||
|
|
||||||
**總計**: 18 個指令
|
|
||||||
**已完成**: 17 個
|
|
||||||
**進行中**: 1 個(推送到 Gitea)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔧 技術決策記錄
|
|
||||||
|
|
||||||
### 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
|
|
||||||
**專案狀態**: 🚧 開發中
|
|
||||||
390
app.py
390
app.py
@@ -27,11 +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 正確處理中文
|
||||||
CORS(app)
|
CORS(app)
|
||||||
|
|
||||||
# 模擬資料庫 (實際應用中應使用 MySQL/PostgreSQL)
|
# 模擬資料庫 (實際應用中應使用 MySQL/PostgreSQL)
|
||||||
positions_db = {}
|
HR_position_positions_db = {}
|
||||||
|
|
||||||
# 預設崗位資料
|
# 預設崗位資料
|
||||||
default_positions = {
|
default_positions = {
|
||||||
@@ -75,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 = {
|
||||||
@@ -101,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 = {
|
||||||
@@ -122,23 +134,51 @@ default_descriptions = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
position_descriptions_db.update(default_descriptions)
|
HR_position_descriptions_db.update(default_descriptions)
|
||||||
|
|
||||||
|
|
||||||
# ==================== 靜態頁面 ====================
|
# ==================== 靜態頁面 ====================
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
def index():
|
def index():
|
||||||
"""返回主頁面"""
|
"""返回登入頁面"""
|
||||||
|
return send_from_directory('.', 'login.html')
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/index.html')
|
||||||
|
def main_app():
|
||||||
|
"""返回主應用頁面"""
|
||||||
return send_from_directory('.', 'index.html')
|
return send_from_directory('.', 'index.html')
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/login.html')
|
||||||
|
def login_page():
|
||||||
|
"""返回登入頁面"""
|
||||||
|
return send_from_directory('.', 'login.html')
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api-test')
|
@app.route('/api-test')
|
||||||
def api_test_page():
|
def api_test_page():
|
||||||
"""返回 API 測試頁面"""
|
"""返回 API 測試頁面"""
|
||||||
return send_from_directory('.', 'api_test.html')
|
return send_from_directory('.', 'api_test.html')
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/<path:filename>')
|
||||||
|
def serve_static(filename):
|
||||||
|
"""服務靜態文件 (JS, SVG, CSS, etc.)"""
|
||||||
|
# 只服務特定類型的文件,避免安全問題
|
||||||
|
allowed_extensions = {'.js', '.svg', '.css', '.png', '.jpg', '.jpeg', '.gif', '.ico', '.md'}
|
||||||
|
file_ext = os.path.splitext(filename)[1].lower()
|
||||||
|
|
||||||
|
if file_ext in allowed_extensions or filename.endswith('.html'):
|
||||||
|
try:
|
||||||
|
return send_from_directory('.', filename)
|
||||||
|
except:
|
||||||
|
return "File not found", 404
|
||||||
|
else:
|
||||||
|
return "File type not allowed", 403
|
||||||
|
|
||||||
|
|
||||||
# ==================== 崗位資料 API ====================
|
# ==================== 崗位資料 API ====================
|
||||||
|
|
||||||
# CSV 路由必須在 <position_id> 路由之前定義
|
# CSV 路由必須在 <position_id> 路由之前定義
|
||||||
@@ -238,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
|
||||||
@@ -284,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:
|
||||||
@@ -320,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
|
||||||
@@ -349,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': '找不到該崗位資料'
|
||||||
@@ -357,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]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -396,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} 已存在'
|
||||||
@@ -412,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,
|
||||||
@@ -436,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': '找不到該崗位資料'
|
||||||
@@ -450,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'])
|
||||||
@@ -476,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,
|
||||||
@@ -503,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': '找不到該崗位資料'
|
||||||
@@ -518,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,
|
||||||
@@ -633,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
|
||||||
@@ -658,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:
|
||||||
@@ -696,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
|
||||||
@@ -730,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': '找不到該職務資料'
|
||||||
@@ -738,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]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -785,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} 已存在'
|
||||||
@@ -800,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,
|
||||||
@@ -818,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': '找不到該職務資料'
|
||||||
@@ -832,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()
|
||||||
|
|
||||||
@@ -852,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,
|
||||||
@@ -879,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': '找不到該職務資料'
|
||||||
@@ -894,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,
|
||||||
@@ -1011,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': '找不到該崗位描述'
|
||||||
@@ -1026,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]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -1034,7 +1074,15 @@ def get_position_description(position_code):
|
|||||||
def create_position_description():
|
def create_position_description():
|
||||||
"""新增或更新崗位描述"""
|
"""新增或更新崗位描述"""
|
||||||
try:
|
try:
|
||||||
data = request.get_json()
|
# 直接從 request.data 讀取並使用 UTF-8 解碼
|
||||||
|
try:
|
||||||
|
if request.data:
|
||||||
|
data = json.loads(request.data.decode('utf-8'))
|
||||||
|
else:
|
||||||
|
data = request.get_json()
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
# 如果 UTF-8 解碼失敗,嘗試其他編碼
|
||||||
|
data = json.loads(request.data.decode('utf-8', errors='replace'))
|
||||||
|
|
||||||
if not data:
|
if not data:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
@@ -1049,24 +1097,24 @@ def create_position_description():
|
|||||||
'error': '崗位編號為必填欄位'
|
'error': '崗位編號為必填欄位'
|
||||||
}), 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} 不存在,請先建立崗位基礎資料'
|
||||||
}), 404
|
# }), 404
|
||||||
|
|
||||||
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,
|
||||||
@@ -1077,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:
|
||||||
@@ -1091,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': '找不到該崗位描述'
|
||||||
@@ -1104,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()
|
||||||
})
|
})
|
||||||
@@ -1112,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:
|
||||||
@@ -1126,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,
|
||||||
@@ -1164,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,
|
||||||
@@ -1177,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', ''),
|
||||||
@@ -1225,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,
|
||||||
@@ -1372,7 +1420,7 @@ def generate_llm_text():
|
|||||||
"""
|
"""
|
||||||
使用 LLM API 生成文字
|
使用 LLM API 生成文字
|
||||||
Request body: {
|
Request body: {
|
||||||
"api": "gemini" | "deepseek" | "openai",
|
"api": "gemini" | "deepseek" | "openai" | "ollama",
|
||||||
"prompt": "提示詞",
|
"prompt": "提示詞",
|
||||||
"max_tokens": 2000
|
"max_tokens": 2000
|
||||||
}
|
}
|
||||||
@@ -1392,7 +1440,7 @@ def generate_llm_text():
|
|||||||
'error': '請提供有效的 JSON 資料'
|
'error': '請提供有效的 JSON 資料'
|
||||||
}), 400
|
}), 400
|
||||||
|
|
||||||
api_name = data.get('api', 'gemini')
|
api_name = data.get('api', 'ollama') # 預設使用 Ollama
|
||||||
prompt = data.get('prompt', '')
|
prompt = data.get('prompt', '')
|
||||||
max_tokens = data.get('max_tokens', 2000)
|
max_tokens = data.get('max_tokens', 2000)
|
||||||
|
|
||||||
@@ -1410,6 +1458,12 @@ def generate_llm_text():
|
|||||||
elif api_name == 'openai':
|
elif api_name == 'openai':
|
||||||
model = data.get('model', 'gpt-3.5-turbo')
|
model = data.get('model', 'gpt-3.5-turbo')
|
||||||
success, result = llm_config.generate_text_openai(prompt, model, max_tokens)
|
success, result = llm_config.generate_text_openai(prompt, model, max_tokens)
|
||||||
|
elif api_name == 'ollama':
|
||||||
|
model = data.get('model') # 從請求中獲取模型,如果沒有則使用預設值
|
||||||
|
success, result = llm_config.generate_text_ollama(prompt, max_tokens, model)
|
||||||
|
elif api_name == 'gptoss':
|
||||||
|
model = data.get('model') # 從請求中獲取模型,如果沒有則使用預設值
|
||||||
|
success, result = llm_config.generate_text_gptoss(prompt, max_tokens, model)
|
||||||
else:
|
else:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': False,
|
'success': False,
|
||||||
@@ -1434,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)
|
||||||
@@ -1486,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 停止伺服器 ║
|
||||||
╚══════════════════════════════════════════════════════════════╝
|
╚══════════════════════════════════════════════════════════════╝
|
||||||
""")
|
""")
|
||||||
@@ -1500,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;
|
||||||
13
data/dropdown_data.js
Normal file
13
data/dropdown_data.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
// 自動生成的下拉選單資料
|
||||||
|
|
||||||
|
// 事業體
|
||||||
|
const businessUnits = ['半導體事業群', '汽車事業體', '法務室', '岡山製造事業體', '產品事業體', '晶圓三廠', '集團人資行政事業體', '集團財務事業體', '集團會計事業體', '集團資訊事業體', '新創事業體', '稽核室', '總經理室', '總品質事業體', '營業事業體'];
|
||||||
|
|
||||||
|
// 處級單位
|
||||||
|
const deptLevel1Units = ['半導體事業群', '汽車事業體', '法務室', '生產處', '岡山製造事業體', '封裝工程處', '副總辦公室', '測試工程與研發處', '資材處', '廠務與環安衛管理處', '產品事業體', '先進產品事業處', '成熟產品事業處', '晶圓三廠', '製程工程處', '集團人資行政事業體', '集團財務事業體', '岡山強茂財務處', '集團會計事業體', '岡山會計處', '集團會計處', '集團資訊事業體', '資安行動小組', '資訊一處', '資訊二處', '新創事業體', '中低壓產品研發處', '研發中心', '高壓產品研發處', '稽核室', '總經理室', 'ESG專案辦公室', '專案管理室', '總品質事業體', '營業事業體', '商業開發暨市場應用處', '海外銷售事業處', '全球技術服務處', '全球行銷暨業務支援處', '大中華區銷售事業處'];
|
||||||
|
|
||||||
|
// 部級單位
|
||||||
|
const deptLevel2Units = ['生產部', '生產企劃部', '岡山品質管制部', '製程工程一部', '製程工程二部', '設備一部', '設備二部', '工業工程部', '測試工程部', '新產品導入部', '研發部', '採購部', '外部資源部', '生管部', '原物料控制部', '廠務部', '產品管理部(APD)', '產品管理部(MPD)', '品質部', '製造部', '廠務部(Fab3)', '工程一部', '工程二部', '工程三部', '製程整合部(Fab3)', '行政總務管理部', '招募任用部', '訓練發展部', '薪酬管理部', '岡山強茂財務部', '會計部', '管理會計部', '集團合併報表部', '應用系統部', '電腦整合製造部', '系統網路服務部', '資源管理部', '客戶品質管理部', '產品品質管理部', '品質系統及客戶工程整合部', '封測外包品質管理部', '品質保證部', '日本區暨代工業務部', '歐亞區業務部', '韓國區業務部-韓國區', '美洲區業務部', '應用工程部(GTS)', '系統工程部', '特性測試部', '業務生管部', '市場行銷企劃部', 'MOSFET晶圓採購部', '台灣區業務部', '業務一部', '業務二部'];
|
||||||
|
|
||||||
|
// 崗位名稱
|
||||||
|
const positionNames = ['營運長', '營運長助理', '副總經理', '專案經理', '經副理', '法務專員', '專利工程師', '處長', '專員', '課長', '組長', '班長', '副班長', '作業員', '工程師', '副總經理助理', '副理', '專案經副理', '顧問', '人資長', '助理', '財務長', '專案副理', '會計長', '資訊長', '主任', '總裁', '總經理', '專員/工程師', '經理', '技術經副理', '處長/資深經理'];
|
||||||
2066
data/hierarchical_data.js
Normal file
2066
data/hierarchical_data.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||||
-- ============================================================
|
-- ============================================================
|
||||||
|
|||||||
19
deepseek_reasoner_output.txt
Normal file
19
deepseek_reasoner_output.txt
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
人工智能(AI)就像教電腦模仿人類的思考或行為能力。簡單來說,它是讓機器能夠:
|
||||||
|
|
||||||
|
1. **學習**:從大量數據或經驗中自己找出規律(例如:辨識貓的照片)。
|
||||||
|
2. **判斷**:根據學習到的資訊做出決策(例如:推薦你喜歡的影片)。
|
||||||
|
3. **解決問題**:處理複雜任務,如下棋、翻譯語言,甚至開車(自動駕駛)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 生活中的例子:
|
||||||
|
- **手機語音助手**(如 Siri)能聽懂你的問題並回答。
|
||||||
|
- **社群媒體** 自動標註照片中的人臉。
|
||||||
|
- **地圖軟體** 根據交通狀況規劃最快路線。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 核心概念:
|
||||||
|
AI 不是真的擁有「智慧」,而是透過數學模型和大量資料訓練出的「模擬智能」。目前常見的 AI 通常專精於特定任務(例如:只會下圍棋的 AlphaGo),還無法像人類一樣全面思考。
|
||||||
|
|
||||||
|
AI 正在快速發展,未來可能會更深入影響生活、工作和醫療等領域,但也需要關注相關的倫理與安全問題哦! 😊
|
||||||
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 | 否 | - |
|
||||||
286
docs/ID重命名對照表.md
Normal file
286
docs/ID重命名對照表.md
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
# HTML ID 重命名對照表
|
||||||
|
|
||||||
|
## 變更概覽
|
||||||
|
- **總計需變更:72 個**
|
||||||
|
- **無需變更:13 個**
|
||||||
|
- **總欄位數:85 個**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 模組 1: 崗位基礎資料 - 基礎資料頁籤
|
||||||
|
**需變更:15 / 15**
|
||||||
|
|
||||||
|
| 舊 ID | 新 ID | 欄位名稱 | 備註 |
|
||||||
|
|-------|-------|---------|------|
|
||||||
|
| `businessUnit` | `pos_businessUnit` | 事業體 (Business Unit) | |
|
||||||
|
| `division` | `pos_division` | 處級單位 (Division) | |
|
||||||
|
| `department` | `pos_department` | 部級單位 (Department) | |
|
||||||
|
| `section` | `pos_section` | 課級單位 (Section) | |
|
||||||
|
| `positionCode` | `pos_code` | 崗位編號 | 必填欄位 |
|
||||||
|
| `effectiveDate` | `pos_effectiveDate` | 生效日期 | |
|
||||||
|
| `positionName` | `pos_name` | 崗位名稱 | 必填欄位 |
|
||||||
|
| `positionLevel` | `pos_level` | 崗位級別 | |
|
||||||
|
| `positionCategory` | `pos_category` | 崗位類別 | onchange 事件 |
|
||||||
|
| `positionCategoryName` | `pos_categoryName` | 崗位類別名稱 | readonly |
|
||||||
|
| `positionNature` | `pos_type` | 崗位性質 | onchange 事件,注意:資料庫改為 positionType |
|
||||||
|
| `positionNatureName` | `pos_typeName` | 崗位性質名稱 | readonly,資料庫改為 positionTypeName |
|
||||||
|
| `headcount` | `pos_headcount` | 編制人數 | |
|
||||||
|
| `positionDesc` | `pos_desc` | 崗位描述 | 資料庫改為 description |
|
||||||
|
| `positionRemark` | `pos_remark` | 崗位備注 | 資料庫改為 remark |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 模組 2: 崗位基礎資料 - 招聘要求頁籤
|
||||||
|
**需變更:18 / 18**
|
||||||
|
|
||||||
|
| 舊 ID | 新 ID | 欄位名稱 | 備註 |
|
||||||
|
|-------|-------|---------|------|
|
||||||
|
| `minEducation` | `rec_eduLevel` | 最低學歷 | 資料庫改為 educationLevel |
|
||||||
|
| `requiredGender` | `rec_gender` | 要求性別 | |
|
||||||
|
| `salaryRange` | `rec_salaryRange` | 薪酬范圍 | |
|
||||||
|
| `workExperience` | `rec_expYears` | 工作經驗 | 資料庫改為 experienceYears |
|
||||||
|
| `minAge` | `rec_minAge` | 最小年齡 | |
|
||||||
|
| `maxAge` | `rec_maxAge` | 最大年齡 | |
|
||||||
|
| `jobType` | `rec_jobType` | 工作性質 | |
|
||||||
|
| `recruitPosition` | `rec_position` | 招聘職位 | 資料庫改為 recruitPosition |
|
||||||
|
| `jobTitle` | `rec_jobTitle` | 職位名稱(對外) | |
|
||||||
|
| `superiorPosition` | `rec_superiorCode` | 上級崗位編號 | 資料庫改為 superiorPositionCode |
|
||||||
|
| `jobDesc` | `rec_jobDesc` | 職位描述(JD) | 資料庫改為 recruitJobDesc |
|
||||||
|
| `positionReq` | `rec_positionReq` | 崗位要求(Req) | 資料庫改為 recruitRequirements |
|
||||||
|
| `titleReq` | `rec_certReq` | 職稱要求 | 語義更正為「證照要求」,資料庫改為 certRequirements |
|
||||||
|
| `majorReq` | `rec_majorReq` | 專業要求 | 資料庫改為 majorRequirements |
|
||||||
|
| `skillReq` | `rec_skillReq` | 技能要求 | 資料庫改為 skillRequirements |
|
||||||
|
| `langReq` | `rec_langReq` | 語言要求 | 資料庫改為 langRequirements |
|
||||||
|
| `otherReq` | `rec_otherReq` | 其他要求 | 資料庫改為 otherRequirements |
|
||||||
|
| `recruitRemark` | `rec_remark` | 招聘備注 | 資料庫改為 recruitRemark |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 模組 3: 職務基礎資料
|
||||||
|
**需變更:12 / 12**
|
||||||
|
|
||||||
|
| 舊 ID | 新 ID | 欄位名稱 | 備註 |
|
||||||
|
|-------|-------|---------|------|
|
||||||
|
| `jobCategoryCode` | `job_category` | 職務類別編號 | 必填欄位,onchange 事件 |
|
||||||
|
| `jobCategoryName` | `job_categoryName` | 職務類別名稱 | readonly |
|
||||||
|
| `jobCode` | `job_code` | 職務編號 | 必填欄位 |
|
||||||
|
| `jobName` | `job_name` | 職務名稱 | 必填欄位 |
|
||||||
|
| `jobNameEn` | `job_nameEn` | 職務英文 | |
|
||||||
|
| `jobEffectiveDate` | `job_effectiveDate` | 生效日期 | 資料庫改為 effectiveDate |
|
||||||
|
| `jobLevel` | `job_level` | 職務層級 | 敏感欄位 |
|
||||||
|
| `jobHeadcount` | `job_headcount` | 編制人數 | 資料庫改為 headcount |
|
||||||
|
| `jobSortOrder` | `job_sortOrder` | 排列順序 | 資料庫改為 sortOrder |
|
||||||
|
| `hasAttendanceBonus` | `job_hasAttBonus` | 全勤獎金 | Toggle Switch |
|
||||||
|
| `hasHousingAllowance` | `job_hasHouseAllow` | 住房補貼 | Toggle Switch |
|
||||||
|
| `jobRemark` | `job_remark` | 職務備注 | 資料庫改為 remark |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 模組 4: 部門職責
|
||||||
|
**需變更:19 / 19**
|
||||||
|
|
||||||
|
| 舊 ID | 新 ID | 欄位名稱 | 備註 |
|
||||||
|
|-------|-------|---------|------|
|
||||||
|
| `deptFunctionCode` | `df_code` | 部門職責編號 | 必填欄位,資料庫改為 dfCode |
|
||||||
|
| `deptFunctionName` | `df_name` | 部門職責名稱 | 必填欄位,資料庫改為 dfName |
|
||||||
|
| `deptFunctionBU` | `df_businessUnit` | 事業體 (第1個) | 必填欄位,已合併重複欄位 |
|
||||||
|
| `deptFunc_businessUnit` | `df_businessUnit` | 事業體 (第2個) | **合併為同一欄位** |
|
||||||
|
| `deptFunc_division` | `df_division` | 處級單位 | 必填欄位 |
|
||||||
|
| `deptFunc_department` | `df_department` | 部級單位 | 必填欄位 |
|
||||||
|
| `deptFunc_section` | `df_section` | 課級單位 | |
|
||||||
|
| `deptFunc_positionTitle` | `df_posTitle` | 崗位名稱 | 必填欄位,資料庫改為 positionTitle |
|
||||||
|
| `deptFunc_positionLevel` | `df_posLevel` | 崗位級別 | 資料庫保持 positionLevel |
|
||||||
|
| `deptManager` | `df_managerTitle` | 部門主管職稱 | 資料庫改為 managerTitle |
|
||||||
|
| `deptFunctionEffectiveDate` | `df_effectiveDate` | 生效日期 | 必填欄位,資料庫改為 effectiveDate |
|
||||||
|
| `deptHeadcount` | `df_headcountLimit` | 部門人數上限 | 資料庫改為 headcountLimit |
|
||||||
|
| `deptStatus` | `df_status` | 部門狀態 | 資料庫改為 status |
|
||||||
|
| `deptMission` | `df_mission` | 部門使命 | 資料庫改為 mission |
|
||||||
|
| `deptVision` | `df_vision` | 部門願景 | 資料庫改為 vision |
|
||||||
|
| `deptCoreFunctions` | `df_coreFunc` | 核心職責 | 必填欄位,資料庫改為 coreFunctions |
|
||||||
|
| `deptKPIs` | `df_kpis` | 關鍵績效指標 | 資料庫改為 kpis |
|
||||||
|
| `deptCollaboration` | `df_collab` | 協作部門 | 資料庫改為 collaboration |
|
||||||
|
| `deptFunctionRemark` | `df_remark` | 備注 | 資料庫改為 remark |
|
||||||
|
|
||||||
|
**特別注意:** 原本有兩個事業體欄位 (`deptFunctionBU` 和 `deptFunc_businessUnit`),新規範已合併為單一欄位 `df_businessUnit`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 模組 5: 崗位描述 - 基本信息
|
||||||
|
**需變更:3 / 4**
|
||||||
|
|
||||||
|
| 舊 ID | 新 ID | 欄位名稱 | 備註 |
|
||||||
|
|-------|-------|---------|------|
|
||||||
|
| `jd_empNo` | `jd_empNo` | 工號 | ✓ 無需變更 |
|
||||||
|
| `jd_empName` | `jd_empName` | 姓名 | ✓ 無需變更 |
|
||||||
|
| `jd_positionCode` | `jd_posCode` | 崗位代碼 | |
|
||||||
|
| `jd_versionDate` | `jd_versionDate` | 版本更新日期 | ✓ 無需變更 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 模組 6: 崗位描述 - 崗位基本信息
|
||||||
|
**需變更:10 / 14**
|
||||||
|
|
||||||
|
| 舊 ID | 新 ID | 欄位名稱 | 備註 |
|
||||||
|
|-------|-------|---------|------|
|
||||||
|
| `jd_positionName` | `jd_posName` | 崗位名稱 | |
|
||||||
|
| `jd_businessUnit` | `jd_businessUnit` | 事業體 | ✓ 無需變更 |
|
||||||
|
| `jd_division` | `jd_division` | 處級單位 | ✓ 無需變更 |
|
||||||
|
| `jd_department` | `jd_department` | 部級單位 | ✓ 無需變更 |
|
||||||
|
| `jd_section` | `jd_section` | 課級單位 | ✓ 無需變更 |
|
||||||
|
| `jd_positionTitle` | `(刪除)` | 崗位名稱 (重複) | 與 positionName 重複,建議刪除 |
|
||||||
|
| `jd_positionLevel` | `jd_posLevel` | 崗位級別 | |
|
||||||
|
| `jd_positionEffectiveDate` | `jd_posEffDate` | 崗位生效日期 | |
|
||||||
|
| `jd_directSupervisor` | `jd_supervisor` | 直接領導職務 | |
|
||||||
|
| `jd_positionGradeJob` | `jd_gradeJob` | 崗位職等&職務 | Modal 選擇器 |
|
||||||
|
| `jd_reportTo` | `jd_reportTo` | 匯報對象職務 | ✓ 無需變更 |
|
||||||
|
| `jd_directReports` | `jd_directReports` | 直接下級 | ✓ 無需變更 |
|
||||||
|
| `jd_workLocation` | `jd_location` | 任職地點 | |
|
||||||
|
| `jd_empAttribute` | `jd_empAttr` | 員工屬性 | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 模組 7: 崗位描述 - 部門職責資訊
|
||||||
|
**需變更:3 / 5**
|
||||||
|
|
||||||
|
| 舊 ID | 新 ID | 欄位名稱 | 備註 |
|
||||||
|
|-------|-------|---------|------|
|
||||||
|
| `jd_deptFunctionCode` | `jd_dfCode` | 部門職責編號 | readonly |
|
||||||
|
| `jd_deptFunctionBU` | `(保留)` | 事業體 | 建議保留或改為 jd_dfBU |
|
||||||
|
| `jd_deptMission` | `jd_deptMission` | 部門使命 | ✓ 無需變更 |
|
||||||
|
| `jd_deptCoreFunctions` | `jd_deptCoreFunctions` | 部門核心職責 | ✓ 無需變更 |
|
||||||
|
| `jd_deptKPIs` | `jd_deptKPIs` | 部門 KPIs | ✓ 無需變更 |
|
||||||
|
|
||||||
|
**說明:** 這些是 readonly 欄位,自動帶入,可保持現有命名以維持與來源資料的一致性。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 模組 8: 崗位描述 - 職責描述
|
||||||
|
**需變更:1 / 2**
|
||||||
|
|
||||||
|
| 舊 ID | 新 ID | 欄位名稱 | 備註 |
|
||||||
|
|-------|-------|---------|------|
|
||||||
|
| `jd_positionPurpose` | `jd_purpose` | 崗位設置目的 | |
|
||||||
|
| `jd_mainResponsibilities` | `jd_mainResp` | 主要崗位職責 | Numbered textarea |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 模組 9: 崗位描述 - 崗位要求
|
||||||
|
**需變更:5 / 5**
|
||||||
|
|
||||||
|
| 舊 ID | 新 ID | 欄位名稱 | 備註 |
|
||||||
|
|-------|-------|---------|------|
|
||||||
|
| `jd_education` | `jd_eduLevel` | 教育程度 | |
|
||||||
|
| `jd_basicSkills` | `jd_basicSkills` | 基本技能 | ✓ 無需變更 |
|
||||||
|
| `jd_professionalKnowledge` | `jd_proKnowledge` | 專業知識 | |
|
||||||
|
| `jd_workExperienceReq` | `jd_expReq` | 工作經驗 | |
|
||||||
|
| `jd_otherRequirements` | `jd_otherReq` | 其他 | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 重要變更說明
|
||||||
|
|
||||||
|
### 1. 前綴標準化
|
||||||
|
所有欄位統一採用模組前綴:
|
||||||
|
- `pos_` - 崗位基礎資料
|
||||||
|
- `rec_` - 招聘要求
|
||||||
|
- `job_` - 職務基礎資料
|
||||||
|
- `df_` - 部門職責
|
||||||
|
- `jd_` - 崗位描述
|
||||||
|
|
||||||
|
### 2. 命名簡化
|
||||||
|
- `positionCode` → `pos_code`
|
||||||
|
- `positionName` → `pos_name`
|
||||||
|
- `positionCategory` → `pos_category`
|
||||||
|
- `positionNature` → `pos_type` (語義更精確)
|
||||||
|
- `hasAttendanceBonus` → `job_hasAttBonus` (縮寫簡化)
|
||||||
|
|
||||||
|
### 3. 語義改善
|
||||||
|
- `titleReq` → `rec_certReq` (職稱要求 → 證照要求)
|
||||||
|
- `positionNature` → `pos_type` (性質 → 類型,更符合英文語義)
|
||||||
|
- `workExperience` → `rec_expYears` (經驗年數更明確)
|
||||||
|
|
||||||
|
### 4. 資料庫欄位同步變更
|
||||||
|
部分 HTML ID 變更時,資料庫欄位名稱也需要對應修改:
|
||||||
|
- `positionNature` → `positionType`
|
||||||
|
- `positionDesc` → `description`
|
||||||
|
- `positionRemark` → `remark`
|
||||||
|
- `minEducation` → `educationLevel`
|
||||||
|
|
||||||
|
### 5. 重複欄位合併
|
||||||
|
部門職責模組中的兩個事業體欄位:
|
||||||
|
- 原:`deptFunctionBU` 和 `deptFunc_businessUnit`
|
||||||
|
- 新:統一為 `df_businessUnit`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 實施建議
|
||||||
|
|
||||||
|
### 階段 1: 資料庫遷移
|
||||||
|
1. 執行資料庫欄位重命名 SQL 腳本
|
||||||
|
2. 更新所有 ORM Model 定義
|
||||||
|
3. 更新 API 回應的欄位名稱
|
||||||
|
|
||||||
|
### 階段 2: 前端更新
|
||||||
|
1. 全局搜尋替換 HTML ID
|
||||||
|
2. 更新所有 JavaScript 選擇器
|
||||||
|
3. 更新 CSS 樣式選擇器
|
||||||
|
4. 更新表單驗證邏輯
|
||||||
|
|
||||||
|
### 階段 3: 測試驗證
|
||||||
|
1. 單元測試:驗證所有表單欄位綁定
|
||||||
|
2. 整合測試:驗證資料儲存與讀取
|
||||||
|
3. E2E 測試:完整流程測試
|
||||||
|
4. 回歸測試:確保無遺漏欄位
|
||||||
|
|
||||||
|
### 階段 4: 文檔更新
|
||||||
|
1. API 文檔更新
|
||||||
|
2. 資料字典更新
|
||||||
|
3. 開發指南更新
|
||||||
|
4. 使用者手冊更新(如有影響)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 風險評估
|
||||||
|
|
||||||
|
### 高風險變更 (必填欄位)
|
||||||
|
- `positionCode` → `pos_code`
|
||||||
|
- `positionName` → `pos_name`
|
||||||
|
- `jobCode` → `job_code`
|
||||||
|
- `jobName` → `job_name`
|
||||||
|
- `deptFunctionCode` → `df_code`
|
||||||
|
|
||||||
|
**建議:** 優先測試這些欄位的儲存與驗證邏輯。
|
||||||
|
|
||||||
|
### 中風險變更 (聯動欄位)
|
||||||
|
- 組織結構欄位:`businessUnit`, `division`, `department`, `section`
|
||||||
|
- 自動帶出欄位:`positionCategoryName`, `positionNatureName`
|
||||||
|
|
||||||
|
**建議:** 仔細測試聯動邏輯和 onchange 事件。
|
||||||
|
|
||||||
|
### 低風險變更 (選填欄位)
|
||||||
|
- 備注類欄位
|
||||||
|
- 描述類欄位
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 檢查清單
|
||||||
|
|
||||||
|
- [ ] 資料庫遷移腳本已準備
|
||||||
|
- [ ] Model 定義已更新
|
||||||
|
- [ ] HTML 表單 ID 已更新
|
||||||
|
- [ ] JavaScript 選擇器已更新
|
||||||
|
- [ ] CSS 樣式已更新
|
||||||
|
- [ ] API 端點已更新
|
||||||
|
- [ ] 驗證邏輯已更新
|
||||||
|
- [ ] 單元測試已通過
|
||||||
|
- [ ] 整合測試已通過
|
||||||
|
- [ ] E2E 測試已通過
|
||||||
|
- [ ] 文檔已更新
|
||||||
|
- [ ] 程式碼審查已完成
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**文檔版本:** v1.0
|
||||||
|
**建立日期:** 2025-12-06
|
||||||
|
**最後更新:** 2025-12-06
|
||||||
|
**維護者:** 系統開發團隊
|
||||||
@@ -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. 快捷鍵設計
|
||||||
@@ -756,11 +999,37 @@ interface PositionListItem {
|
|||||||
|
|
||||||
```
|
```
|
||||||
hr_position_form/
|
hr_position_form/
|
||||||
├── index.html # 前端應用主檔
|
├── index.html # 前端應用主檔
|
||||||
├── app.py # Flask 後端 API
|
├── app.py # Flask 後端 API
|
||||||
├── requirements.txt # Python 依賴套件
|
├── llm_config.py # LLM API 配置模組
|
||||||
├── README.md # 使用說明文件
|
├── database_schema.sql # MySQL 資料庫結構定義
|
||||||
└── SDD.md # 軟體設計文件(本文件)
|
├── init_database.py # 資料庫初始化腳本
|
||||||
|
├── import_hierarchy_data.py # 組織階層資料匯入腳本
|
||||||
|
├── hierarchical_data.js # 組織階層原始資料
|
||||||
|
├── requirements.txt # Python 依賴套件
|
||||||
|
├── .env # 環境變數配置 (API Keys)
|
||||||
|
├── README.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 解析錯誤處理測試(支援推理模型) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**文件結束**
|
||||||
1140
docs/Test Driven Development.md
Normal file
1140
docs/Test Driven Development.md
Normal file
File diff suppressed because it is too large
Load Diff
758
docs/prompt.md
Normal file
758
docs/prompt.md
Normal file
@@ -0,0 +1,758 @@
|
|||||||
|
# HR 崗位管理系統 - AI 生成功能 Prompt 說明文件
|
||||||
|
|
||||||
|
> **文件版本**: v1.0
|
||||||
|
> **最後更新**: 2024-12-04
|
||||||
|
> **維護者**: AI(所以有問題真的不要問我)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 目錄
|
||||||
|
|
||||||
|
1. [總覽](#總覽)
|
||||||
|
2. [頁籤 1: 崗位基礎資料維護](#頁籤-1-崗位基礎資料維護)
|
||||||
|
3. [頁籤 2: 崗位招聘要求](#頁籤-2-崗位招聘要求)
|
||||||
|
4. [頁籤 3: 職務基礎資料](#頁籤-3-職務基礎資料)
|
||||||
|
5. [頁籤 4: 部門職責維護](#頁籤-4-部門職責維護)
|
||||||
|
6. [頁籤 5: 崗位描述 (JD)](#頁籤-5-崗位描述-jd)
|
||||||
|
7. [如何修改 Prompt](#如何修改-prompt)
|
||||||
|
8. [Prompt 設計原則](#prompt-設計原則)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 總覽
|
||||||
|
|
||||||
|
系統中共有 **5 個頁籤**提供 AI 自動生成功能,每個頁籤都有一個 "✨ I'm feeling lucky" 按鈕。
|
||||||
|
|
||||||
|
### 🎯 核心運作原理
|
||||||
|
|
||||||
|
1. **智能空白檢測**: 系統會檢測哪些欄位是空白的
|
||||||
|
2. **上下文感知**: 將已填寫的欄位作為上下文傳給 LLM
|
||||||
|
3. **精準生成**: 只生成尚未填寫的欄位
|
||||||
|
4. **JSON 格式回傳**: 要求 LLM 返回結構化的 JSON 資料
|
||||||
|
5. **自動填充**: 解析 JSON 並填入對應欄位
|
||||||
|
|
||||||
|
### 🔗 對應函式與程式碼位置
|
||||||
|
|
||||||
|
| 頁籤名稱 | 函式名稱 | 程式碼位置 | 按鈕 ID/onclick |
|
||||||
|
|---------|---------|-----------|----------------|
|
||||||
|
| 崗位基礎資料維護 | `generatePositionBasic()` | [index.html:2179](index.html#L2179) | `onclick="generatePositionBasic()"` |
|
||||||
|
| 崗位招聘要求 | `generatePositionRecruit()` | [index.html:2253](index.html#L2253) | `onclick="generatePositionRecruit()"` |
|
||||||
|
| 職務基礎資料 | `generateJobBasic()` | [index.html:2330](index.html#L2330) | `onclick="generateJobBasic()"` |
|
||||||
|
| 部門職責維護 | `generateDeptFunction()` | [index.html:3778](index.html#L3778) | `onclick="generateDeptFunction()"` |
|
||||||
|
| 崗位描述 (JD) | `generateJobDesc()` | [index.html:2425](index.html#L2425) | `onclick="generateJobDesc()"` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 頁籤 1: 崗位基礎資料維護
|
||||||
|
|
||||||
|
### 📍 函式位置
|
||||||
|
- **檔案**: `index.html`
|
||||||
|
- **行數**: 2179-2251
|
||||||
|
- **函式**: `async function generatePositionBasic()`
|
||||||
|
|
||||||
|
### 📝 完整 Prompt
|
||||||
|
|
||||||
|
```
|
||||||
|
請為HR崗位管理系統生成崗位基礎資料。請用繁體中文回覆。
|
||||||
|
|
||||||
|
[如果有已填寫的資料,會附加此段]
|
||||||
|
已填寫的資料(請參考這些內容來生成相關的資料):
|
||||||
|
{JSON格式的已填寫資料}
|
||||||
|
|
||||||
|
請「只生成」以下這些尚未填寫的欄位:[動態欄位列表]
|
||||||
|
|
||||||
|
欄位說明:
|
||||||
|
- 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: 崗位描述(2-3句話描述工作內容)
|
||||||
|
- positionRemark: 崗位備注(可選的補充說明)
|
||||||
|
|
||||||
|
請直接返回JSON格式,只包含需要生成的欄位,不要有任何其他文字:
|
||||||
|
{
|
||||||
|
"positionCode": "...",
|
||||||
|
"positionName": "...",
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🎯 Prompt 設計依據
|
||||||
|
|
||||||
|
1. **上下文感知**: 如果使用者已填寫部分欄位,這些資料會被傳入作為參考
|
||||||
|
2. **精準指令**: 明確告知只生成「尚未填寫」的欄位,避免覆蓋已有資料
|
||||||
|
3. **格式規範**: 提供詳細的欄位格式說明和代碼對照表
|
||||||
|
4. **結構化輸出**: 要求返回純 JSON,方便程式解析
|
||||||
|
|
||||||
|
### 📦 處理的欄位
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const allFields = [
|
||||||
|
'positionCode', // 崗位編號
|
||||||
|
'positionName', // 崗位名稱
|
||||||
|
'positionCategory', // 崗位類別代碼
|
||||||
|
'positionNature', // 崗位性質代碼
|
||||||
|
'headcount', // 編制人數
|
||||||
|
'positionLevel', // 崗位級別
|
||||||
|
'positionDesc', // 崗位描述
|
||||||
|
'positionRemark' // 崗位備注
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🔧 如何修改此 Prompt
|
||||||
|
|
||||||
|
在 [index.html:2205-2223](index.html#L2205) 找到以下程式碼:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const prompt = `請為HR崗位管理系統生成崗位基礎資料。請用繁體中文回覆。
|
||||||
|
${contextInfo}
|
||||||
|
|
||||||
|
請「只生成」以下這些尚未填寫的欄位:${emptyFields.join(', ')}
|
||||||
|
|
||||||
|
欄位說明:
|
||||||
|
- positionCode: 崗位編號(格式如 ENG-001, MGR-002, SAL-003)
|
||||||
|
...
|
||||||
|
`;
|
||||||
|
```
|
||||||
|
|
||||||
|
**修改建議**:
|
||||||
|
- 如果要改變生成風格:修改第一句的指令(例如:「請以專業正式的語氣生成...」)
|
||||||
|
- 如果要新增欄位規則:在「欄位說明」中添加新的規範
|
||||||
|
- 如果要調整格式:修改格式範例(如改變編號規則)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 頁籤 2: 崗位招聘要求
|
||||||
|
|
||||||
|
### 📍 函式位置
|
||||||
|
- **檔案**: `index.html`
|
||||||
|
- **行數**: 2253-2328
|
||||||
|
- **函式**: `async function generatePositionRecruit()`
|
||||||
|
|
||||||
|
### 📝 完整 Prompt
|
||||||
|
|
||||||
|
```
|
||||||
|
請為HR崗位管理系統生成「{崗位名稱}」的招聘要求資料。請用繁體中文回覆。
|
||||||
|
|
||||||
|
已填寫的資料(請參考這些內容來生成相關的資料):
|
||||||
|
{JSON格式的已填寫資料,包含 positionName}
|
||||||
|
|
||||||
|
請「只生成」以下這些尚未填寫的欄位:[動態欄位列表]
|
||||||
|
|
||||||
|
欄位說明:
|
||||||
|
- 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格式,只包含需要生成的欄位:
|
||||||
|
{
|
||||||
|
"minEducation": "...",
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🎯 Prompt 設計依據
|
||||||
|
|
||||||
|
1. **職位名稱作為核心上下文**: 使用第一個頁籤的崗位名稱作為生成依據
|
||||||
|
2. **跨頁籤資料引用**: 會從「崗位基礎資料」頁籤讀取 `positionName`
|
||||||
|
3. **招聘專用代碼**: 提供完整的學歷、薪資、經驗等代碼對照
|
||||||
|
|
||||||
|
### 📦 處理的欄位
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const allFields = [
|
||||||
|
'minEducation', // 最低學歷
|
||||||
|
'requiredGender', // 要求性別
|
||||||
|
'salaryRange', // 薪酬范圍
|
||||||
|
'workExperience', // 工作經驗年數
|
||||||
|
'minAge', // 最小年齡
|
||||||
|
'maxAge', // 最大年齡
|
||||||
|
'jobType', // 工作性質
|
||||||
|
'recruitPosition', // 招聘職位
|
||||||
|
'jobTitle', // 職位名稱
|
||||||
|
'jobDesc', // 職位描述
|
||||||
|
'positionReq', // 崗位要求
|
||||||
|
'skillReq', // 技能要求
|
||||||
|
'langReq', // 語言要求
|
||||||
|
'otherReq' // 其他要求
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🔧 如何修改此 Prompt
|
||||||
|
|
||||||
|
在 [index.html:2275-2301](index.html#L2275) 找到程式碼。
|
||||||
|
|
||||||
|
**修改建議**:
|
||||||
|
- **調整薪資範圍**: 修改 `salaryRange` 的代碼對照(例如增加更高薪資級別)
|
||||||
|
- **新增性別選項**: 如果需要更多性別選項,在 `requiredGender` 中添加
|
||||||
|
- **調整經驗年限**: 修改 `workExperience` 的可用選項
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 頁籤 3: 職務基礎資料
|
||||||
|
|
||||||
|
### 📍 函式位置
|
||||||
|
- **檔案**: `index.html`
|
||||||
|
- **行數**: 2330-2423
|
||||||
|
- **函式**: `async function generateJobBasic()`
|
||||||
|
|
||||||
|
### 📝 完整 Prompt
|
||||||
|
|
||||||
|
```
|
||||||
|
請為HR職務管理系統生成職務基礎資料。請用繁體中文回覆。
|
||||||
|
|
||||||
|
[如果有已填寫的資料,會附加此段]
|
||||||
|
已填寫的資料(請參考這些內容來生成相關的資料):
|
||||||
|
{JSON格式的已填寫資料}
|
||||||
|
|
||||||
|
請「只生成」以下這些尚未填寫的欄位:[動態欄位列表,可能包含 checkbox]
|
||||||
|
|
||||||
|
欄位說明:
|
||||||
|
- 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格式,只包含需要生成的欄位:
|
||||||
|
{
|
||||||
|
"jobCategoryCode": "...",
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🎯 Prompt 設計依據
|
||||||
|
|
||||||
|
1. **職務 vs 崗位**: 這個頁籤處理的是「職務」(Job),與「崗位」(Position)不同
|
||||||
|
2. **Checkbox 處理**: 特殊處理 `hasAttendanceBonus` 和 `hasHousingAllowance` 兩個布林值欄位
|
||||||
|
3. **排序欄位**: `jobSortOrder` 使用 10 的倍數,方便後續插入新職務
|
||||||
|
|
||||||
|
### 📦 處理的欄位
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const allFields = [
|
||||||
|
'jobCategoryCode', // 職務類別代碼
|
||||||
|
'jobCode', // 職務編號
|
||||||
|
'jobName', // 職務名稱
|
||||||
|
'jobNameEn', // 職務英文名稱
|
||||||
|
'jobHeadcount', // 編制人數
|
||||||
|
'jobSortOrder', // 排列順序
|
||||||
|
'jobRemark', // 備注說明
|
||||||
|
'jobLevel' // 職務層級
|
||||||
|
];
|
||||||
|
|
||||||
|
// 額外處理的 checkbox
|
||||||
|
const checkboxes = [
|
||||||
|
'hasAttendanceBonus', // 是否有全勤
|
||||||
|
'hasHousingAllowance' // 是否住房補貼
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🔧 如何修改此 Prompt
|
||||||
|
|
||||||
|
在 [index.html:2362-2382](index.html#L2362) 找到程式碼。
|
||||||
|
|
||||||
|
**修改建議**:
|
||||||
|
- **新增職務類別**: 在 `jobCategoryCode` 中添加新的類別代碼
|
||||||
|
- **調整編號格式**: 修改 `jobCode` 的格式範例
|
||||||
|
- **修改保密設定**: 調整 `jobLevel` 的說明(如不允許保密)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 頁籤 4: 部門職責維護
|
||||||
|
|
||||||
|
### 📍 函式位置
|
||||||
|
- **檔案**: `index.html`
|
||||||
|
- **行數**: 3778-3839
|
||||||
|
- **函式**: `function generateDeptFunction()`
|
||||||
|
|
||||||
|
### 📝 完整 Prompt
|
||||||
|
|
||||||
|
```
|
||||||
|
請為HR部門職責管理系統生成部門職責資料。請用繁體中文回覆。
|
||||||
|
|
||||||
|
[如果有已填寫的資料,會附加此段]
|
||||||
|
已填寫的資料(請參考這些內容來生成相關的資料):
|
||||||
|
{JSON格式的已填寫資料}
|
||||||
|
|
||||||
|
請「只生成」以下這些尚未填寫的欄位:[動態欄位列表]
|
||||||
|
|
||||||
|
欄位說明:
|
||||||
|
- deptFunctionCode: 部門職責編號(格式如 DF-001, DF-002)
|
||||||
|
- deptFunctionName: 部門職責名稱(例如:軟體研發部職責)
|
||||||
|
- deptFunctionBU: 事業體代碼(SBU/MBU/HQBU/ITBU/HRBU/ACCBU 之一)
|
||||||
|
- deptFunctionDept: 部門名稱
|
||||||
|
- deptManager: 部門主管職稱
|
||||||
|
- deptMission: 部門使命(使用「•」開頭的條列式,2-3項)
|
||||||
|
- deptVision: 部門願景(使用「•」開頭的條列式,1-2項)
|
||||||
|
- deptCoreFunctions: 核心職責(使用「•」開頭的條列式,4-6項)
|
||||||
|
- deptKPIs: 關鍵績效指標(使用「•」開頭的條列式,3-4項)
|
||||||
|
|
||||||
|
請直接返回JSON格式,只包含需要生成的欄位,不要有任何其他文字:
|
||||||
|
{
|
||||||
|
"deptFunctionCode": "...",
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🎯 Prompt 設計依據
|
||||||
|
|
||||||
|
1. **條列式格式**: 特別要求使用 `•` 開頭的條列式,符合部門職責文件慣例
|
||||||
|
2. **數量控制**: 明確指定每個欄位的條列項目數量(例如使命 2-3 項)
|
||||||
|
3. **事業體代碼**: 提供固定的事業體代碼選項
|
||||||
|
4. **管理導向**: 專注於部門管理層面的使命、願景、職責、KPI
|
||||||
|
|
||||||
|
### 📦 處理的欄位
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const allFields = [
|
||||||
|
'deptFunctionCode', // 部門職責編號
|
||||||
|
'deptFunctionName', // 部門職責名稱
|
||||||
|
'deptFunctionBU', // 事業體代碼
|
||||||
|
'deptFunctionDept', // 部門名稱
|
||||||
|
'deptManager', // 部門主管職稱
|
||||||
|
'deptMission', // 部門使命(條列式)
|
||||||
|
'deptVision', // 部門願景(條列式)
|
||||||
|
'deptCoreFunctions', // 核心職責(條列式)
|
||||||
|
'deptKPIs' // 關鍵績效指標(條列式)
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🔧 如何修改此 Prompt
|
||||||
|
|
||||||
|
在 [index.html:3800-3819](index.html#L3800) 找到程式碼。
|
||||||
|
|
||||||
|
**修改建議**:
|
||||||
|
- **新增事業體**: 在 `deptFunctionBU` 中添加新的事業體代碼
|
||||||
|
- **調整條列數量**: 修改各欄位的條列項目數量要求
|
||||||
|
- **改變條列符號**: 將 `•` 改為其他符號(如 `1.`, `-`, `★`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 頁籤 5: 崗位描述 (JD)
|
||||||
|
|
||||||
|
### 📍 函式位置
|
||||||
|
- **檔案**: `index.html`
|
||||||
|
- **行數**: 2425-2541
|
||||||
|
- **函式**: `async function generateJobDesc()`
|
||||||
|
|
||||||
|
### 📝 完整 Prompt
|
||||||
|
|
||||||
|
```
|
||||||
|
請為HR崗位描述管理系統生成崗位描述資料。請用繁體中文回覆。
|
||||||
|
|
||||||
|
[如果有已填寫的資料,會附加此段]
|
||||||
|
已填寫的資料(請參考這些內容來生成相關的資料):
|
||||||
|
{JSON格式的已填寫資料,欄位名稱已移除 jd_ 前綴}
|
||||||
|
|
||||||
|
請「只生成」以下這些尚未填寫的欄位:[動態欄位列表]
|
||||||
|
|
||||||
|
欄位說明:
|
||||||
|
- 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格式,只包含需要生成的欄位:
|
||||||
|
{
|
||||||
|
"empNo": "...",
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🎯 Prompt 設計依據
|
||||||
|
|
||||||
|
1. **最複雜的表單**: 包含最多欄位(18 個),涵蓋完整的 JD 內容
|
||||||
|
2. **日期格式規範**: 明確要求 YYYY-MM-DD 格式
|
||||||
|
3. **職責編號格式**: 特別指定使用「1、」「2、」格式,並用 `\n` 換行
|
||||||
|
4. **地點代碼對照**: 提供台灣與中國大陸的辦公室代碼
|
||||||
|
5. **欄位名稱映射**: 程式中會將 `jd_` 前綴移除後再傳給 API
|
||||||
|
|
||||||
|
### 📦 處理的欄位
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
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' // 其他要求
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🔧 如何修改此 Prompt
|
||||||
|
|
||||||
|
在 [index.html:2464-2492](index.html#L2464) 找到程式碼。
|
||||||
|
|
||||||
|
**特別注意**: 這個模組有欄位名稱映射機制([index.html:2499-2518](index.html#L2499)),修改欄位時需要同時更新 `fieldMapping` 物件。
|
||||||
|
|
||||||
|
**修改建議**:
|
||||||
|
- **新增辦公室地點**: 在 `workLocation` 中添加新的辦公室代碼
|
||||||
|
- **調整職責數量**: 修改 `mainResponsibilities` 的編號範圍(如改為 1-10)
|
||||||
|
- **新增員工屬性**: 在 `empAttribute` 中添加新的員工類型
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 如何修改 Prompt
|
||||||
|
|
||||||
|
### 📝 通用修改步驟
|
||||||
|
|
||||||
|
所有 "I'm Feeling Lucky" 按鈕的 Prompt 都遵循相同的修改流程:
|
||||||
|
|
||||||
|
#### 步驟 1: 找到對應函式
|
||||||
|
|
||||||
|
使用上方表格找到要修改的函式位置,例如:
|
||||||
|
```
|
||||||
|
崗位基礎資料維護 → index.html:2179
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 步驟 2: 找到 prompt 變數
|
||||||
|
|
||||||
|
在函式中搜尋 `const prompt =` 或 `const prompt = \``:
|
||||||
|
```javascript
|
||||||
|
const prompt = `請為HR崗位管理系統生成崗位基礎資料。請用繁體中文回覆。
|
||||||
|
...
|
||||||
|
`;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 步驟 3: 修改 Prompt 內容
|
||||||
|
|
||||||
|
根據需求修改:
|
||||||
|
|
||||||
|
**A. 修改生成風格**
|
||||||
|
```javascript
|
||||||
|
// 修改前
|
||||||
|
const prompt = `請為HR崗位管理系統生成崗位基礎資料。請用繁體中文回覆。
|
||||||
|
|
||||||
|
// 修改後(加入語氣要求)
|
||||||
|
const prompt = `請以專業正式且友善的語氣,為HR崗位管理系統生成崗位基礎資料。請用繁體中文回覆。
|
||||||
|
```
|
||||||
|
|
||||||
|
**B. 修改欄位說明**
|
||||||
|
```javascript
|
||||||
|
// 修改前
|
||||||
|
- positionCode: 崗位編號(格式如 ENG-001, MGR-002, SAL-003)
|
||||||
|
|
||||||
|
// 修改後(改變編號規則)
|
||||||
|
- positionCode: 崗位編號(格式:部門縮寫-年份-流水號,如 ENG-2024-001)
|
||||||
|
```
|
||||||
|
|
||||||
|
**C. 新增欄位規則**
|
||||||
|
```javascript
|
||||||
|
// 在「欄位說明」區塊新增
|
||||||
|
- positionPriority: 優先級(HIGH=高, MID=中, LOW=低)
|
||||||
|
```
|
||||||
|
|
||||||
|
**D. 調整輸出格式**
|
||||||
|
```javascript
|
||||||
|
// 修改前
|
||||||
|
請直接返回JSON格式,只包含需要生成的欄位,不要有任何其他文字:
|
||||||
|
|
||||||
|
// 修改後(要求更多資訊)
|
||||||
|
請直接返回JSON格式,只包含需要生成的欄位。每個欄位請加上「_note」後綴提供生成理由:
|
||||||
|
{
|
||||||
|
"positionCode": "ENG-001",
|
||||||
|
"positionCode_note": "根據技術職的慣例生成",
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 步驟 4: 測試修改結果
|
||||||
|
|
||||||
|
1. 儲存 `index.html` 檔案
|
||||||
|
2. 重新整理瀏覽器頁面(Ctrl+F5 強制重新整理)
|
||||||
|
3. 點擊對應頁籤的 "✨ I'm feeling lucky" 按鈕
|
||||||
|
4. 檢查生成的內容是否符合預期
|
||||||
|
|
||||||
|
### ⚠️ 修改時的注意事項
|
||||||
|
|
||||||
|
1. **保持 JSON 格式要求**: 必須要求 LLM 返回純 JSON,否則程式解析會失敗
|
||||||
|
2. **不要移除欄位動態列表**: `${emptyFields.join(', ')}` 這段必須保留
|
||||||
|
3. **維持上下文機制**: `${contextInfo}` 這段是自動填入已有資料的機制,不要刪除
|
||||||
|
4. **注意反引號**: Prompt 使用反引號 `` ` `` 包裹,內部不可再使用反引號
|
||||||
|
5. **測試跨欄位引用**: 有些頁籤會引用其他頁籤的資料(如招聘要求引用崗位名稱)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prompt 設計原則
|
||||||
|
|
||||||
|
### 🎨 系統採用的 Prompt 設計原則
|
||||||
|
|
||||||
|
#### 1. **智能空白檢測**
|
||||||
|
```javascript
|
||||||
|
const emptyFields = getEmptyFields(allFields);
|
||||||
|
```
|
||||||
|
- 只生成尚未填寫的欄位
|
||||||
|
- 避免覆蓋使用者已輸入的資料
|
||||||
|
- 提升生成效率
|
||||||
|
|
||||||
|
#### 2. **上下文感知生成**
|
||||||
|
```javascript
|
||||||
|
const contextInfo = Object.keys(existingData).length > 0
|
||||||
|
? `\n\n已填寫的資料(請參考這些內容來生成相關的資料):\n${JSON.stringify(existingData, null, 2)}`
|
||||||
|
: '';
|
||||||
|
```
|
||||||
|
- 將已填寫的資料作為上下文
|
||||||
|
- 讓 LLM 生成與現有資料一致的內容
|
||||||
|
- 提升資料連貫性
|
||||||
|
|
||||||
|
#### 3. **結構化輸出**
|
||||||
|
```javascript
|
||||||
|
const prompt = `...
|
||||||
|
請直接返回JSON格式,只包含需要生成的欄位,不要有任何其他文字:
|
||||||
|
{
|
||||||
|
${emptyFields.map(f => `"${f}": "..."`).join(',\n ')}
|
||||||
|
}`;
|
||||||
|
```
|
||||||
|
- 要求 LLM 返回純 JSON
|
||||||
|
- 提供 JSON 格式範本
|
||||||
|
- 方便程式解析
|
||||||
|
|
||||||
|
#### 4. **詳細的欄位說明**
|
||||||
|
```javascript
|
||||||
|
欄位說明:
|
||||||
|
- positionCode: 崗位編號(格式如 ENG-001, MGR-002, SAL-003)
|
||||||
|
- positionCategory: 崗位類別代碼(01=技術職, 02=管理職, 03=業務職, 04=行政職)
|
||||||
|
```
|
||||||
|
- 提供完整的代碼對照表
|
||||||
|
- 說明格式規範和範例
|
||||||
|
- 減少生成錯誤
|
||||||
|
|
||||||
|
#### 5. **繁體中文優先**
|
||||||
|
```javascript
|
||||||
|
const prompt = `請為HR崗位管理系統生成崗位基礎資料。請用繁體中文回覆。
|
||||||
|
```
|
||||||
|
- 明確要求使用繁體中文
|
||||||
|
- 避免出現簡體字或英文
|
||||||
|
|
||||||
|
### 🚀 進階 Prompt 技巧
|
||||||
|
|
||||||
|
#### 技巧 1: 加入企業特色
|
||||||
|
```javascript
|
||||||
|
// 在 prompt 開頭加入
|
||||||
|
const prompt = `你是一位專業的HR顧問,熟悉台灣半導體產業的人力資源管理。
|
||||||
|
請為HR崗位管理系統生成崗位基礎資料。請用繁體中文回覆。
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 技巧 2: 加入範例
|
||||||
|
```javascript
|
||||||
|
// 在欄位說明後加入
|
||||||
|
生成範例:
|
||||||
|
{
|
||||||
|
"positionCode": "ENG-001",
|
||||||
|
"positionName": "前端工程師",
|
||||||
|
"positionDesc": "負責開發和維護公司網站前端功能,提升使用者體驗。"
|
||||||
|
}
|
||||||
|
|
||||||
|
請參考以上範例,生成類似格式的資料。
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 技巧 3: 加入條件邏輯
|
||||||
|
```javascript
|
||||||
|
// 根據職位類型調整 prompt
|
||||||
|
const isManagerPosition = existingData.positionCategory === '02';
|
||||||
|
const extraInstruction = isManagerPosition
|
||||||
|
? '\n特別注意:這是管理職,請強調領導能力和團隊管理經驗。'
|
||||||
|
: '';
|
||||||
|
|
||||||
|
const prompt = `請為HR崗位管理系統生成崗位基礎資料。請用繁體中文回覆。${extraInstruction}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 技巧 4: 加入驗證規則
|
||||||
|
```javascript
|
||||||
|
const prompt = `...
|
||||||
|
請確保生成的資料符合以下規則:
|
||||||
|
1. 崗位編號必須以部門代碼開頭
|
||||||
|
2. 編制人數必須為正整數
|
||||||
|
3. 崗位描述長度在 20-100 字之間
|
||||||
|
4. 所有代碼必須從提供的選項中選擇
|
||||||
|
...`;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 偵錯與問題排查
|
||||||
|
|
||||||
|
### 常見問題 1: LLM 返回格式錯誤
|
||||||
|
|
||||||
|
**現象**: 點擊按鈕後出現「生成失敗,請稍後再試」
|
||||||
|
|
||||||
|
**可能原因**:
|
||||||
|
- LLM 返回的不是純 JSON 格式
|
||||||
|
- JSON 中包含多餘的文字說明
|
||||||
|
- JSON 格式不正確(缺少逗號、括號等)
|
||||||
|
|
||||||
|
**解決方法**:
|
||||||
|
在 Prompt 中加強格式要求:
|
||||||
|
```javascript
|
||||||
|
const prompt = `...
|
||||||
|
請直接返回JSON格式,不要包含任何markdown標記(如 \`\`\`json),不要有任何其他文字說明:
|
||||||
|
{
|
||||||
|
${emptyFields.map(f => `"${f}": "..."`).join(',\n ')}
|
||||||
|
}`;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 常見問題 2: 生成內容不符合預期
|
||||||
|
|
||||||
|
**現象**: 生成的內容格式正確,但內容不理想
|
||||||
|
|
||||||
|
**解決方法**:
|
||||||
|
1. 檢查上下文資料是否正確傳遞
|
||||||
|
2. 增加更詳細的欄位說明
|
||||||
|
3. 提供具體範例
|
||||||
|
4. 調整 Prompt 語氣和指令
|
||||||
|
|
||||||
|
### 常見問題 3: 部分欄位未填充
|
||||||
|
|
||||||
|
**現象**: 只填充了部分欄位,其他欄位仍為空
|
||||||
|
|
||||||
|
**可能原因**:
|
||||||
|
- LLM 返回的 JSON 缺少某些欄位
|
||||||
|
- 欄位名稱不匹配(大小寫、前綴問題)
|
||||||
|
|
||||||
|
**解決方法**:
|
||||||
|
檢查欄位映射邏輯,特別是 `generateJobDesc()` 函式中的 `fieldMapping`:
|
||||||
|
```javascript
|
||||||
|
const fieldMapping = {
|
||||||
|
'empNo': 'jd_empNo',
|
||||||
|
'empName': 'jd_empName',
|
||||||
|
...
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 參考資源
|
||||||
|
|
||||||
|
### 相關檔案
|
||||||
|
|
||||||
|
- **主程式**: [index.html](index.html) - 包含所有 AI 生成函式
|
||||||
|
- **LLM API 配置**: [llm_config.py](llm_config.py) - LLM 模型設定
|
||||||
|
- **環境變數**: [.env](.env) - API Key 和模型設定
|
||||||
|
|
||||||
|
### 相關函式
|
||||||
|
|
||||||
|
- **`callClaudeAPI(prompt)`**: 呼叫 LLM API 的核心函式
|
||||||
|
- **`getEmptyFields(allFields)`**: 檢測空白欄位
|
||||||
|
- **`fillIfEmpty(fieldId, value)`**: 只填充空白欄位
|
||||||
|
- **`setButtonLoading(btn, isLoading)`**: 設定按鈕載入狀態
|
||||||
|
|
||||||
|
### LLM 模型設定
|
||||||
|
|
||||||
|
系統支援多種 LLM 模型,在 `.env` 檔案中設定:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Gemini API Configuration
|
||||||
|
GEMINI_API_KEY=your_api_key
|
||||||
|
GEMINI_MODEL=gemini-1.5-flash
|
||||||
|
|
||||||
|
# DeepSeek API Configuration
|
||||||
|
DEEPSEEK_API_KEY=your_deepseek_api_key
|
||||||
|
DEEPSEEK_API_URL=https://api.deepseek.com/v1
|
||||||
|
|
||||||
|
# OpenAI API Configuration
|
||||||
|
OPENAI_API_KEY=your_openai_api_key
|
||||||
|
OPENAI_API_URL=https://api.openai.com/v1
|
||||||
|
|
||||||
|
# Ollama API Configuration
|
||||||
|
OLLAMA_API_URL=https://ollama_pjapi.theaken.com
|
||||||
|
OLLAMA_MODEL=deepseek-reasoner
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ 最佳實踐
|
||||||
|
|
||||||
|
### ✨ 撰寫好的 Prompt 的建議
|
||||||
|
|
||||||
|
1. **明確的指令**: 清楚說明要生成什麼類型的資料
|
||||||
|
2. **詳細的格式說明**: 提供完整的代碼對照表和格式範例
|
||||||
|
3. **上下文資訊**: 包含已填寫的資料作為參考
|
||||||
|
4. **結構化輸出**: 要求返回 JSON 格式,方便解析
|
||||||
|
5. **語言偏好**: 明確指定使用繁體中文
|
||||||
|
6. **錯誤處理**: 加入驗證規則,減少生成錯誤
|
||||||
|
|
||||||
|
### 📝 Prompt 維護建議
|
||||||
|
|
||||||
|
1. **版本控制**: 重大修改前先備份原 Prompt
|
||||||
|
2. **測試驗證**: 每次修改後都要測試所有情境
|
||||||
|
3. **文件更新**: 修改後同步更新此說明文件
|
||||||
|
4. **使用者回饋**: 根據實際使用情況調整 Prompt
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 總結
|
||||||
|
|
||||||
|
### 快速參考表
|
||||||
|
|
||||||
|
| 頁籤 | 函式 | 程式碼行數 | 主要用途 | 欄位數量 |
|
||||||
|
|-----|------|-----------|---------|---------|
|
||||||
|
| 崗位基礎資料 | `generatePositionBasic()` | 2179-2251 | 生成崗位基本資訊 | 8 |
|
||||||
|
| 崗位招聘要求 | `generatePositionRecruit()` | 2253-2328 | 生成招聘需求 | 14 |
|
||||||
|
| 職務基礎資料 | `generateJobBasic()` | 2330-2423 | 生成職務資訊 | 8+2 checkbox |
|
||||||
|
| 部門職責維護 | `generateDeptFunction()` | 3778-3839 | 生成部門職責 | 9 |
|
||||||
|
| 崗位描述 (JD) | `generateJobDesc()` | 2425-2541 | 生成完整JD | 18 |
|
||||||
|
|
||||||
|
### 核心機制
|
||||||
|
|
||||||
|
1. **智能檢測**: 自動識別空白欄位
|
||||||
|
2. **上下文感知**: 參考已填寫資料生成
|
||||||
|
3. **結構化輸出**: JSON 格式便於解析
|
||||||
|
4. **安全填充**: 只填充空白欄位,不覆蓋現有資料
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> **維護提醒**: 當系統新增或修改欄位時,記得同步更新:
|
||||||
|
> 1. HTML 表單欄位
|
||||||
|
> 2. JavaScript 函式中的 `allFields` 陣列
|
||||||
|
> 3. Prompt 中的欄位說明
|
||||||
|
> 4. 此說明文件
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**文件結束** | 有問題請找 AI,不要找我 ¯\\_(ツ)_/¯
|
||||||
263
docs/三個錦囊設計.md
Normal file
263
docs/三個錦囊設計.md
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
# 三個錦囊功能設計
|
||||||
|
|
||||||
|
## 功能概述
|
||||||
|
將原本的單一 "I'm feeling lucky" 按鈕改為三個可自定義的 AI 生成按鈕("三個錦囊")。
|
||||||
|
|
||||||
|
## UI 設計
|
||||||
|
|
||||||
|
### 視覺效果
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ 🎁 錦囊一 🎁 錦囊二 🎁 錦囊三 │
|
||||||
|
│ [標題] [標題] [標題] │
|
||||||
|
│ ⚙️ 編輯 ⚙️ 編輯 ⚙️ 編輯 │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 互動流程
|
||||||
|
1. **點擊錦囊按鈕** → 使用預設或自定義的 prompt 調用 AI
|
||||||
|
2. **點擊編輯圖示** → 彈出對話框,顯示並可編輯當前 prompt
|
||||||
|
3. **Prompt 儲存** → 儲存至 localStorage(按模組區分)
|
||||||
|
|
||||||
|
## 資料結構
|
||||||
|
|
||||||
|
### LocalStorage 結構
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
"prompts": {
|
||||||
|
"positionBasic": {
|
||||||
|
"bag1": { "title": "簡化版", "prompt": "..." },
|
||||||
|
"bag2": { "title": "標準版", "prompt": "..." },
|
||||||
|
"bag3": { "title": "詳細版", "prompt": "..." }
|
||||||
|
},
|
||||||
|
"positionRecruit": {
|
||||||
|
"bag1": { "title": "一般需求", "prompt": "..." },
|
||||||
|
"bag2": { "title": "高階需求", "prompt": "..." },
|
||||||
|
"bag3": { "title": "專業需求", "prompt": "..." }
|
||||||
|
},
|
||||||
|
"jobBasic": { ... },
|
||||||
|
"deptFunction": { ... },
|
||||||
|
"jobDesc": { ... }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 預設 Prompt 模板
|
||||||
|
|
||||||
|
### 崗位基礎資料 - 基礎資料頁籤
|
||||||
|
|
||||||
|
#### 錦囊一:簡化版(僅填必填欄位)
|
||||||
|
```
|
||||||
|
你是專業人資顧問,熟悉半導體製造業。請生成崗位基礎資料(僅必填欄位)。
|
||||||
|
|
||||||
|
已填寫的資料:{existingData}
|
||||||
|
需要生成的欄位:positionCode, positionName
|
||||||
|
|
||||||
|
請用繁體中文,返回 JSON 格式。
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 錦囊二:標準版(常用欄位)
|
||||||
|
```
|
||||||
|
你是專業人資顧問,熟悉半導體製造業。請生成崗位基礎資料(標準版)。
|
||||||
|
|
||||||
|
已填寫的資料:{existingData}
|
||||||
|
需要生成的欄位:positionCode, positionName, positionCategory, positionLevel, headcount
|
||||||
|
|
||||||
|
欄位說明:
|
||||||
|
- positionCode: 崗位編號(格式如 ENG-001)
|
||||||
|
- positionName: 崗位名稱
|
||||||
|
- positionCategory: 崗位類別代碼(01=技術職, 02=管理職, 03=業務職, 04=行政職)
|
||||||
|
- positionLevel: 崗位級別(L1-L7)
|
||||||
|
- headcount: 編制人數(1-10)
|
||||||
|
|
||||||
|
請用繁體中文,返回 JSON 格式。
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 錦囊三:詳細版(所有欄位)
|
||||||
|
```
|
||||||
|
你是專業人資顧問,熟悉半導體製造業的人資所有流程。請生成完整的崗位基礎資料。
|
||||||
|
|
||||||
|
已填寫的資料:{existingData}
|
||||||
|
需要生成的欄位: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 格式,不要有任何其他文字。
|
||||||
|
```
|
||||||
|
|
||||||
|
### 崗位基礎資料 - 招聘要求頁籤
|
||||||
|
|
||||||
|
#### 錦囊一:基本需求
|
||||||
|
```
|
||||||
|
請生成「{positionName}」的基本招聘要求。
|
||||||
|
|
||||||
|
已填寫的資料:{existingData}
|
||||||
|
需要生成的欄位:minEducation, workExperience, jobType, jobTitle
|
||||||
|
|
||||||
|
請用繁體中文,返回 JSON 格式。
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 錦囊二:標準需求
|
||||||
|
```
|
||||||
|
請生成「{positionName}」的標準招聘要求。
|
||||||
|
|
||||||
|
已填寫的資料:{existingData}
|
||||||
|
需要生成的欄位:minEducation, salaryRange, workExperience, jobType, recruitPosition, jobTitle, positionReq, skillReq
|
||||||
|
|
||||||
|
欄位說明:
|
||||||
|
- minEducation: 最低學歷代碼(HS=高中職, JC=專科, BA=大學, MA=碩士, PHD=博士)
|
||||||
|
- salaryRange: 薪酬范圍代碼(A=30000以下, B=30000-50000, C=50000-80000, D=80000-120000, E=120000以上, N=面議)
|
||||||
|
- workExperience: 工作經驗年數(0=不限, 1, 3, 5, 10)
|
||||||
|
- jobType: 工作性質代碼(FT=全職, PT=兼職, CT=約聘, DP=派遣)
|
||||||
|
- recruitPosition: 招聘職位代碼(ENG=工程師, MGR=經理, AST=助理, OP=作業員, SAL=業務)
|
||||||
|
- positionReq: 崗位要求(條列式,用換行分隔)
|
||||||
|
- skillReq: 技能要求(條列式,用換行分隔)
|
||||||
|
|
||||||
|
請用繁體中文,返回 JSON 格式。
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 錦囊三:完整需求
|
||||||
|
```
|
||||||
|
請生成「{positionName}」的完整招聘要求資料。
|
||||||
|
|
||||||
|
已填寫的資料:{existingData}
|
||||||
|
需要生成所有空白欄位。
|
||||||
|
|
||||||
|
(完整欄位說明同標準版,包含所有 18 個欄位)
|
||||||
|
|
||||||
|
請用繁體中文,返回 JSON 格式。
|
||||||
|
```
|
||||||
|
|
||||||
|
## 實作步驟
|
||||||
|
|
||||||
|
### 1. HTML 結構更新
|
||||||
|
將單一按鈕:
|
||||||
|
```html
|
||||||
|
<button type="button" class="ai-generate-btn" onclick="generatePositionBasic()">
|
||||||
|
<span>✨ I'm feeling lucky</span>
|
||||||
|
</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
改為三個錦囊:
|
||||||
|
```html
|
||||||
|
<div class="ai-bags-container">
|
||||||
|
<div class="ai-bag" data-bag="1" onclick="executeAIBag('positionBasic', 1)">
|
||||||
|
<div class="bag-icon">🎁</div>
|
||||||
|
<div class="bag-title" id="bag1-title-positionBasic">錦囊一</div>
|
||||||
|
<button class="bag-edit-btn" onclick="editBagPrompt(event, 'positionBasic', 1)">⚙️</button>
|
||||||
|
</div>
|
||||||
|
<div class="ai-bag" data-bag="2" onclick="executeAIBag('positionBasic', 2)">
|
||||||
|
<div class="bag-icon">🎁</div>
|
||||||
|
<div class="bag-title" id="bag2-title-positionBasic">錦囊二</div>
|
||||||
|
<button class="bag-edit-btn" onclick="editBagPrompt(event, 'positionBasic', 2)">⚙️</button>
|
||||||
|
</div>
|
||||||
|
<div class="ai-bag" data-bag="3" onclick="executeAIBag('positionBasic', 3)">
|
||||||
|
<div class="bag-icon">🎁</div>
|
||||||
|
<div class="bag-title" id="bag3-title-positionBasic">錦囊三</div>
|
||||||
|
<button class="bag-edit-btn" onclick="editBagPrompt(event, 'positionBasic', 3)">⚙️</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. CSS 樣式
|
||||||
|
```css
|
||||||
|
.ai-bags-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-bag {
|
||||||
|
position: relative;
|
||||||
|
padding: 20px;
|
||||||
|
background: linear-gradient(135deg, #9b59b6 0%, #8e44ad 100%);
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 4px 15px rgba(155, 89, 182, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-bag:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 6px 20px rgba(155, 89, 182, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-bag .bag-icon {
|
||||||
|
font-size: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-bag .bag-title {
|
||||||
|
color: white;
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-bag .bag-edit-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-bag .bag-edit-btn:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. JavaScript 函式
|
||||||
|
|
||||||
|
#### 執行 AI 錦囊
|
||||||
|
```javascript
|
||||||
|
async function executeAIBag(module, bagNumber) {
|
||||||
|
const prompts = getModulePrompts(module);
|
||||||
|
const bagPrompt = prompts[`bag${bagNumber}`];
|
||||||
|
|
||||||
|
// 使用 prompt 調用 AI
|
||||||
|
await callClaudeAPI(bagPrompt.prompt);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 編輯 Prompt
|
||||||
|
```javascript
|
||||||
|
function editBagPrompt(event, module, bagNumber) {
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
// 顯示編輯對話框
|
||||||
|
showPromptEditModal(module, bagNumber);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 優點
|
||||||
|
|
||||||
|
1. ✅ **靈活性高** - 用戶可自定義每個錦囊的用途
|
||||||
|
2. ✅ **效率提升** - 三個預設選項涵蓋不同需求場景
|
||||||
|
3. ✅ **學習友好** - 顯示 prompt 有助於理解 AI 運作
|
||||||
|
4. ✅ **可擴展** - 未來可增加更多錦囊或分享功能
|
||||||
|
|
||||||
|
## 待實作功能
|
||||||
|
|
||||||
|
- [ ] HTML 結構更新(5 個模組)
|
||||||
|
- [ ] CSS 樣式新增
|
||||||
|
- [ ] JavaScript 函式實作
|
||||||
|
- [ ] LocalStorage 管理
|
||||||
|
- [ ] 編輯 Modal 對話框
|
||||||
|
- [ ] 預設 Prompt 初始化
|
||||||
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/
|
||||||
176
docs/更新欄位名稱.md
Normal file
176
docs/更新欄位名稱.md
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
# 系統表單欄位規範書 (Standardized Field Specifications)
|
||||||
|
|
||||||
|
## 1. 命名規範與前綴定義 (Naming Conventions)
|
||||||
|
|
||||||
|
為了確保系統一致性,HTML 元素 ID 採用 `[模組前綴]_[標準欄位名]` 的命名方式。
|
||||||
|
|
||||||
|
| 模組名稱 | 模組前綴 (Prefix) | 說明 |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| **崗位管理 (Position)** | `pos_` | 崗位基礎資料 |
|
||||||
|
| **招聘條件 (Recruit)** | `rec_` | 崗位內的招聘頁籤 |
|
||||||
|
| **職務管理 (Job)** | `job_` | 全公司通用的職務定義 |
|
||||||
|
| **部門職責 (DeptFunc)** | `df_` | 部門功能與職責定義 |
|
||||||
|
| **崗位描述 (JobDesc)** | `jd_` | 最終的 JD 產出表單 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 崗位基礎資料模組 (Position Module)
|
||||||
|
|
||||||
|
**表單代號**: `positionForm`
|
||||||
|
**資料表**: `Position`
|
||||||
|
|
||||||
|
### 2.1 基礎資料頁籤 (tab-position-basic)
|
||||||
|
|
||||||
|
| # | 欄位顯示名稱 | 標準化 HTML ID | 資料庫欄位名稱 | 類型 | 必填 | 預設 | 備註 |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| 1 | 事業體 | `pos_businessUnit` | `businessUnit` | select | 否 | - | SBU, MBU... (聯動L1) |
|
||||||
|
| 2 | 處級單位 | `pos_division` | `division` | select | 否 | - | (聯動L2) |
|
||||||
|
| 3 | 部級單位 | `pos_department` | `department` | select | 否 | - | (聯動L3) |
|
||||||
|
| 4 | 課級單位 | `pos_section` | `section` | text | 否 | - | - |
|
||||||
|
| 5 | **崗位編號** | `pos_code` | `positionCode` | text | **是** | - | 唯一識別碼 (PK) |
|
||||||
|
| 6 | 生效日期 | `pos_effectiveDate` | `effectiveDate` | date | 否 | Today | - |
|
||||||
|
| 7 | **崗位名稱** | `pos_name` | `positionName` | text | **是** | - | - |
|
||||||
|
| 8 | 崗位級別 | `pos_level` | `positionLevel` | select | 否 | - | L1-L7 |
|
||||||
|
| 9 | 崗位類別 | `pos_category` | `positionCategory` | select | 否 | - | onchange 觸發 |
|
||||||
|
| 10 | 崗位類別名稱 | `pos_categoryName` | `positionCategoryName` | text | 否 | - | readonly |
|
||||||
|
| 11 | 崗位性質 | `pos_type` | `positionType` | select | 否 | - | FT, PT, CT, IN |
|
||||||
|
| 12 | 崗位性質名稱 | `pos_typeName` | `positionTypeName` | text | 否 | - | readonly |
|
||||||
|
| 13 | 編制人數 | `pos_headcount` | `headcount` | number | 否 | 0 | min=0 |
|
||||||
|
| 14 | 崗位描述 | `pos_desc` | `description` | textarea | 否 | - | rows=6 |
|
||||||
|
| 15 | 崗位備注 | `pos_remark` | `remark` | textarea | 否 | - | rows=6 |
|
||||||
|
|
||||||
|
### 2.2 招聘要求資料頁籤 (tab-position-recruit)
|
||||||
|
|
||||||
|
| # | 欄位顯示名稱 | 標準化 HTML ID | 資料庫欄位名稱 | 類型 | 必填 | 預設 | 備註 |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| 1 | 最低學歷 | `rec_eduLevel` | `educationLevel` | select | 否 | - | HS, BA, MA, PHD |
|
||||||
|
| 2 | 要求性別 | `rec_gender` | `requiredGender` | select | 否 | Any | M, F, Any |
|
||||||
|
| 3 | 薪酬范圍 | `rec_salaryRange` | `salaryRange` | select | 否 | - | A-E, Negotiable |
|
||||||
|
| 4 | 工作經驗 | `rec_expYears` | `experienceYears` | select | 否 | - | 0, 1, 3, 5, 10+ |
|
||||||
|
| 5 | 最小年齡 | `rec_minAge` | `minAge` | number | 否 | - | min=18 |
|
||||||
|
| 6 | 最大年齡 | `rec_maxAge` | `maxAge` | number | 否 | - | max=65 |
|
||||||
|
| 7 | 工作性質 | `rec_jobType` | `jobType` | select | 否 | - | 招聘用性質分類 |
|
||||||
|
| 8 | 招聘職位 | `rec_position` | `recruitPosition` | select | 否 | - | ENG, MGR... |
|
||||||
|
| 9 | 職位名稱(對外) | `rec_jobTitle` | `jobTitle` | text | 否 | - | 對外招聘用Title |
|
||||||
|
| 10 | 上級崗位編號 | `rec_superiorCode` | `superiorPositionCode` | text | 否 | - | - |
|
||||||
|
| 11 | 職位描述(JD) | `rec_jobDesc` | `recruitJobDesc` | textarea | 否 | - | 招聘廣告用 |
|
||||||
|
| 12 | 崗位要求(Req) | `rec_positionReq` | `recruitRequirements` | textarea | 否 | - | 招聘廣告用 |
|
||||||
|
| 13 | 證照要求 | `rec_certReq` | `certRequirements` | select | 否 | - | - |
|
||||||
|
| 14 | 專業要求 | `rec_majorReq` | `majorRequirements` | text | 否 | - | Modal選擇 |
|
||||||
|
| 15 | 技能要求 | `rec_skillReq` | `skillRequirements` | text | 否 | - | Tags input |
|
||||||
|
| 16 | 語言要求 | `rec_langReq` | `langRequirements` | text | 否 | - | - |
|
||||||
|
| 17 | 其他要求 | `rec_otherReq` | `otherRequirements` | text | 否 | - | - |
|
||||||
|
| 18 | 招聘備注 | `rec_remark` | `recruitRemark` | textarea | 否 | - | - |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 職務基礎資料模組 (Job Module)
|
||||||
|
|
||||||
|
**表單代號**: `jobForm`
|
||||||
|
**資料表**: `Job`
|
||||||
|
|
||||||
|
| # | 欄位顯示名稱 | 標準化 HTML ID | 資料庫欄位名稱 | 類型 | 必填 | 預設 | 備註 |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| 1 | **職務類別編號** | `job_category` | `jobCategoryCode` | select | **是** | - | onchange 觸發 |
|
||||||
|
| 2 | 職務類別名稱 | `job_categoryName` | `jobCategoryName` | text | 否 | - | readonly |
|
||||||
|
| 3 | **職務編號** | `job_code` | `jobCode` | text | **是** | - | 唯一識別碼 |
|
||||||
|
| 4 | **職務名稱** | `job_name` | `jobName` | text | **是** | - | - |
|
||||||
|
| 5 | 職務英文 | `job_nameEn` | `jobNameEn` | text | 否 | - | - |
|
||||||
|
| 6 | 生效日期 | `job_effectiveDate`| `effectiveDate` | date | 否 | - | - |
|
||||||
|
| 7 | 職務層級 | `job_level` | `jobLevel` | text | 否 | *保密* | **敏感欄位** |
|
||||||
|
| 8 | 編制人數 | `job_headcount` | `headcount` | number | 否 | - | - |
|
||||||
|
| 9 | 排列順序 | `job_sortOrder` | `sortOrder` | number | 否 | - | - |
|
||||||
|
| 10 | 全勤獎金 | `job_hasAttBonus` | `hasAttendanceBonus` | checkbox| 否 | false| Toggle Switch |
|
||||||
|
| 11 | 住房補貼 | `job_hasHouseAllow`| `hasHousingAllowance` | checkbox| 否 | false| Toggle Switch |
|
||||||
|
| 12 | 職務備注 | `job_remark` | `remark` | textarea | 否 | - | - |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 部門職責模組 (DeptFunction Module)
|
||||||
|
|
||||||
|
**表單代號**: `deptFunctionForm`
|
||||||
|
**資料表**: `DeptFunction`
|
||||||
|
|
||||||
|
| # | 欄位顯示名稱 | 標準化 HTML ID | 資料庫欄位名稱 | 類型 | 必填 | 預設 | 備註 |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| 1 | **職責編號** | `df_code` | `dfCode` | text | **是** | - | DF-001 |
|
||||||
|
| 2 | **職責名稱** | `df_name` | `dfName` | text | **是** | - | - |
|
||||||
|
| 3 | **事業體** | `df_businessUnit` | `businessUnit` | select | **是** | - | (已合併重複欄位) |
|
||||||
|
| 4 | **處級單位** | `df_division` | `division` | select | **是** | - | - |
|
||||||
|
| 5 | **部級單位** | `df_department` | `department` | select | **是** | - | - |
|
||||||
|
| 6 | 課級單位 | `df_section` | `section` | text | 否 | - | - |
|
||||||
|
| 7 | **對應崗位** | `df_posTitle` | `positionTitle` | select | **是** | - | 關聯 Position |
|
||||||
|
| 8 | 崗位級別 | `df_posLevel` | `positionLevel` | select | 否 | - | 自動帶出或指定 |
|
||||||
|
| 9 | 部門主管職稱 | `df_managerTitle` | `managerTitle` | text | 否 | - | - |
|
||||||
|
| 10 | **生效日期** | `df_effectiveDate` | `effectiveDate` | date | **是** | - | - |
|
||||||
|
| 11 | 人數上限 | `df_headcountLimit`| `headcountLimit` | number | 否 | - | - |
|
||||||
|
| 12 | 狀態 | `df_status` | `status` | select | 否 | active | - |
|
||||||
|
| 13 | 部門使命 | `df_mission` | `mission` | textarea | 否 | - | - |
|
||||||
|
| 14 | 部門願景 | `df_vision` | `vision` | textarea | 否 | - | - |
|
||||||
|
| 15 | **核心職責** | `df_coreFunc` | `coreFunctions` | textarea | **是** | - | - |
|
||||||
|
| 16 | KPIs | `df_kpis` | `kpis` | textarea | 否 | - | - |
|
||||||
|
| 17 | 協作部門 | `df_collab` | `collaboration` | textarea | 否 | - | - |
|
||||||
|
| 18 | 備注 | `df_remark` | `remark` | textarea | 否 | - | - |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 崗位描述模組 (JobDescription Module)
|
||||||
|
|
||||||
|
**表單代號**: `jobDescForm`
|
||||||
|
**資料表**: `JobDescription` (部分欄位為 View)
|
||||||
|
|
||||||
|
### 5.1 基本信息 (Header)
|
||||||
|
|
||||||
|
| # | 欄位顯示名稱 | 標準化 HTML ID | 資料庫欄位名稱 | 類型 | 必填 | 備註 |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| 1 | 工號 | `jd_empNo` | `empNo` | text | 否 | Search Modal |
|
||||||
|
| 2 | 姓名 | `jd_empName` | `empName` | text | 否 | Readonly |
|
||||||
|
| 3 | 崗位代碼 | `jd_posCode` | `positionCode` | text | 否 | 關聯鍵 |
|
||||||
|
| 4 | 版本日期 | `jd_versionDate` | `versionDate` | date | 否 | - |
|
||||||
|
|
||||||
|
### 5.2 崗位資訊 (Position Info - Readonly/Derived)
|
||||||
|
|
||||||
|
| # | 欄位顯示名稱 | 標準化 HTML ID | 資料庫欄位名稱 | 類型 | 必填 | 備註 |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| 1 | 崗位名稱 | `jd_posName` | `positionName` | text | 否 | - |
|
||||||
|
| 2 | 事業體 | `jd_businessUnit` | `businessUnit` | select | 否 | - |
|
||||||
|
| 3 | 處級單位 | `jd_division` | `division` | select | 否 | - |
|
||||||
|
| 4 | 部級單位 | `jd_department` | `department` | select | 否 | - |
|
||||||
|
| 5 | 課級單位 | `jd_section` | `section` | text | 否 | - |
|
||||||
|
| 6 | 崗位級別 | `jd_posLevel` | `positionLevel` | select | 否 | - |
|
||||||
|
| 7 | 生效日期 | `jd_posEffDate` | `positionEffectiveDate`| date | 否 | - |
|
||||||
|
| 8 | **直接主管** | `jd_supervisor` | `directSupervisor` | text | 否 | - |
|
||||||
|
| 9 | 職等&職務 | `jd_gradeJob` | `positionGradeJob` | text | 否 | Modal |
|
||||||
|
| 10 | **匯報對象** | `jd_reportTo` | `reportTo` | text | 否 | Modal |
|
||||||
|
| 11 | 直接下屬 | `jd_directReports` | `directReports` | text | 否 | - |
|
||||||
|
| 12 | 任職地點 | `jd_location` | `workLocation` | select | 否 | - |
|
||||||
|
| 13 | 員工屬性 | `jd_empAttr` | `empAttribute` | select | 否 | - |
|
||||||
|
|
||||||
|
### 5.3 職責與要求 (Details)
|
||||||
|
|
||||||
|
| # | 欄位顯示名稱 | 標準化 HTML ID | 資料庫欄位名稱 | 類型 | 必填 | 備註 |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| 1 | 部門職責代碼 | `jd_dfCode` | `dfCode` | text | 否 | 關聯 DeptFunction |
|
||||||
|
| 2 | 崗位設置目的 | `jd_purpose` | `positionPurpose` | text | 否 | - |
|
||||||
|
| 3 | **主要職責** | `jd_mainResp` | `mainResponsibilities`| textarea | 否 | 編號清單 |
|
||||||
|
| 4 | 教育程度 | `jd_eduLevel` | `educationLevel` | text | 否 | - |
|
||||||
|
| 5 | 基本技能 | `jd_basicSkills` | `basicSkills` | textarea | 否 | - |
|
||||||
|
| 6 | 專業知識 | `jd_proKnowledge` | `professionalKnowledge` | textarea | 否 | - |
|
||||||
|
| 7 | 工作經驗 | `jd_expReq` | `experienceRequirements`| textarea | 否 | - |
|
||||||
|
| 8 | 其他要求 | `jd_otherReq` | `otherRequirements` | textarea | 否 | - |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 共用設定與資料字典
|
||||||
|
|
||||||
|
### 6.1 模態框 (Modals)
|
||||||
|
* `MajorModal` (專業要求)
|
||||||
|
* `EmpSearchModal` (員工搜索)
|
||||||
|
* `OrgSearchModal` (組織搜索)
|
||||||
|
* `GradeJobModal` (職等職務)
|
||||||
|
* `ReportToModal` (匯報對象)
|
||||||
|
|
||||||
|
### 6.2 特殊控件
|
||||||
|
* **Toggle Switch**: 用於所有布林值 (Boolean) 欄位。
|
||||||
|
* **Numbered Textarea**: 用於 `mainResponsibilities`,自動產生序號。
|
||||||
|
* **Cascading Selects**: 組織層級 (BU -> Div -> Dept -> Section) 統一使用標準聯動邏輯。
|
||||||
236
docs/權限矩陣.md
Normal file
236
docs/權限矩陣.md
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
# 系統權限矩陣 - 那都AI寫的,不要問我
|
||||||
|
|
||||||
|
## 系統概述
|
||||||
|
本系統為「人力資源崗位管理系統」(HR Position Management System),採用三級權限架構設計,確保資料安全性與操作權限的合理分配。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 角色定義
|
||||||
|
|
||||||
|
### 1. 一般使用者 (User)
|
||||||
|
- **測試帳號**: A003 / employee
|
||||||
|
- **使用對象**: 一般員工、HR專員
|
||||||
|
- **主要職責**: 查詢崗位資訊、建立崗位描述、查看部門職責
|
||||||
|
|
||||||
|
### 2. 管理者 (Admin)
|
||||||
|
- **測試帳號**: A002 / hr_manager
|
||||||
|
- **使用對象**: 部門主管、HR經理
|
||||||
|
- **主要職責**: 管理部門職責、審核崗位資料、匯出報表
|
||||||
|
|
||||||
|
### 3. 最高管理者 (Super Admin)
|
||||||
|
- **測試帳號**: A001 / admin
|
||||||
|
- **使用對象**: 系統管理員、HR總監
|
||||||
|
- **主要職責**: 系統設定、使用者管理、完整權限控制
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 功能權限矩陣
|
||||||
|
|
||||||
|
| 功能模組 | 功能項目 | 一般使用者 | 管理者 | 最高管理者 | 說明 |
|
||||||
|
|---------|---------|:---------:|:-----:|:---------:|------|
|
||||||
|
| **崗位管理** | 查看崗位清單 | ✅ | ✅ | ✅ | 所有角色可查看 |
|
||||||
|
| | 搜尋/篩選崗位 | ✅ | ✅ | ✅ | 所有角色可搜尋 |
|
||||||
|
| | 查看崗位詳情 | ✅ | ✅ | ✅ | 所有角色可查看詳情 |
|
||||||
|
| | 建立新崗位 | ❌ | ✅ | ✅ | 需要管理權限 |
|
||||||
|
| | 編輯崗位資訊 | ❌ | ✅ | ✅ | 需要管理權限 |
|
||||||
|
| | 刪除崗位 | ❌ | ❌ | ✅ | 僅最高管理者 |
|
||||||
|
| **職位描述 (JD)** | 查看 JD | ✅ | ✅ | ✅ | 所有角色可查看 |
|
||||||
|
| | 建立 JD | ✅ | ✅ | ✅ | 所有角色可建立 |
|
||||||
|
| | 編輯自己的 JD | ✅ | ✅ | ✅ | 可編輯自己建立的 |
|
||||||
|
| | 編輯所有 JD | ❌ | ✅ | ✅ | 管理者以上 |
|
||||||
|
| | 刪除 JD | ❌ | ✅ | ✅ | 管理者以上 |
|
||||||
|
| | 使用 AI 生成 JD | ✅ | ✅ | ✅ | 所有角色可使用 AI |
|
||||||
|
| **部門職責** | 查看部門職責 | ✅ | ✅ | ✅ | 所有角色可查看 |
|
||||||
|
| | 建立部門職責 | ❌ | ✅ | ✅ | 管理者以上 |
|
||||||
|
| | 編輯部門職責 | ❌ | ✅ | ✅ | 管理者以上 |
|
||||||
|
| | 刪除部門職責 | ❌ | ❌ | ✅ | 僅最高管理者 |
|
||||||
|
| | 匯出部門職責 | ✅ | ✅ | ✅ | 所有角色可匯出 |
|
||||||
|
| **崗位清單** | 查看清單 | ✅ | ✅ | ✅ | 所有角色可查看 |
|
||||||
|
| | 篩選/排序 | ✅ | ✅ | ✅ | 所有角色可使用 |
|
||||||
|
| | 匯出 CSV | ✅ | ✅ | ✅ | 所有角色可匯出 |
|
||||||
|
| | 批量操作 | ❌ | ✅ | ✅ | 管理者以上 |
|
||||||
|
| **報表匯出** | 匯出基本報表 | ✅ | ✅ | ✅ | 所有角色可匯出 |
|
||||||
|
| | 匯出完整資料 | ❌ | ✅ | ✅ | 管理者以上 |
|
||||||
|
| | 匯出統計報表 | ❌ | ✅ | ✅ | 管理者以上 |
|
||||||
|
| **系統管理** | 查看系統設定 | ❌ | ❌ | ✅ | 僅最高管理者 |
|
||||||
|
| | 修改系統設定 | ❌ | ❌ | ✅ | 僅最高管理者 |
|
||||||
|
| | LLM 模型設定 | ❌ | ❌ | ✅ | 僅最高管理者 |
|
||||||
|
| | 測試 API 連線 | ❌ | ❌ | ✅ | 僅最高管理者 |
|
||||||
|
| **使用者管理** | 查看使用者清單 | ❌ | ❌ | ✅ | 僅最高管理者 |
|
||||||
|
| | 新增使用者 | ❌ | ❌ | ✅ | 僅最高管理者 |
|
||||||
|
| | 編輯使用者 | ❌ | ❌ | ✅ | 僅最高管理者 |
|
||||||
|
| | 刪除使用者 | ❌ | ❌ | ✅ | 僅最高管理者 |
|
||||||
|
| | 修改權限 | ❌ | ❌ | ✅ | 僅最高管理者 |
|
||||||
|
| **AI 功能** | 使用 AI 生成 | ✅ | ✅ | ✅ | 所有角色可使用 |
|
||||||
|
| | 選擇 AI 模型 | ❌ | ❌ | ✅ | 僅最高管理者設定 |
|
||||||
|
| | 查看 AI 使用記錄 | ❌ | ✅ | ✅ | 管理者以上 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 資料訪問權限
|
||||||
|
|
||||||
|
### 資料可見性
|
||||||
|
|
||||||
|
| 資料類型 | 一般使用者 | 管理者 | 最高管理者 |
|
||||||
|
|---------|:---------:|:-----:|:---------:|
|
||||||
|
| 所有崗位資料 | ✅ 唯讀 | ✅ 可編輯 | ✅ 完全控制 |
|
||||||
|
| 部門職責資料 | ✅ 唯讀 | ✅ 可編輯 | ✅ 完全控制 |
|
||||||
|
| 自己建立的 JD | ✅ 可編輯 | ✅ 可編輯 | ✅ 可編輯 |
|
||||||
|
| 他人建立的 JD | ✅ 唯讀 | ✅ 可編輯 | ✅ 可編輯 |
|
||||||
|
| 使用者資料 | ❌ | ❌ | ✅ 完全控制 |
|
||||||
|
| 系統設定 | ❌ | ❌ | ✅ 完全控制 |
|
||||||
|
| 操作日誌 | ❌ | ✅ 唯讀 | ✅ 完全控制 |
|
||||||
|
|
||||||
|
### 資料操作權限
|
||||||
|
|
||||||
|
| 操作類型 | 一般使用者 | 管理者 | 最高管理者 |
|
||||||
|
|---------|:---------:|:-----:|:---------:|
|
||||||
|
| **C**reate (新增) | 僅 JD | 崗位、部門職責、JD | 所有資料 |
|
||||||
|
| **R**ead (讀取) | 基本資料 | 包含統計資料 | 所有資料 |
|
||||||
|
| **U**pdate (更新) | 僅自己的 JD | 大部分資料 | 所有資料 |
|
||||||
|
| **D**elete (刪除) | ❌ | 部分資料 | 所有資料 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 頁面/模組訪問權限
|
||||||
|
|
||||||
|
| 頁面模組 | 一般使用者 | 管理者 | 最高管理者 |
|
||||||
|
|---------|:---------:|:-----:|:---------:|
|
||||||
|
| 🏠 首頁 (登入頁) | ✅ | ✅ | ✅ |
|
||||||
|
| 📝 崗位說明書管理 | ✅ | ✅ | ✅ |
|
||||||
|
| 🎯 部門職責管理 | ✅ 唯讀 | ✅ | ✅ |
|
||||||
|
| 📋 崗位清單 | ✅ | ✅ | ✅ |
|
||||||
|
| ⚙️ 管理者頁面 | ❌ | ⚠️ 部分功能 | ✅ |
|
||||||
|
|
||||||
|
### 管理者頁面功能細分
|
||||||
|
|
||||||
|
| 管理者頁面功能 | 一般使用者 | 管理者 | 最高管理者 |
|
||||||
|
|--------------|:---------:|:-----:|:---------:|
|
||||||
|
| 使用者管理 | ❌ | ❌ | ✅ |
|
||||||
|
| LLM 模型設定 | ❌ | ❌ | ✅ |
|
||||||
|
| 崗位資料管理 | ❌ | ✅ 唯讀 | ✅ |
|
||||||
|
| 匯出完整資料 | ❌ | ✅ | ✅ |
|
||||||
|
| 查看統計資訊 | ❌ | ✅ | ✅ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 特殊權限說明
|
||||||
|
|
||||||
|
### 1. AI 功能使用
|
||||||
|
|
||||||
|
所有角色都可以使用 AI 生成功能,但有以下限制:
|
||||||
|
|
||||||
|
- **一般使用者**: 可使用 AI 生成 JD,但每日限額 50 次
|
||||||
|
- **管理者**: 可使用 AI 生成,每日限額 200 次
|
||||||
|
- **最高管理者**: 無限制,且可設定使用的 AI 模型
|
||||||
|
|
||||||
|
### 2. 匯出功能
|
||||||
|
|
||||||
|
| 匯出類型 | 一般使用者 | 管理者 | 最高管理者 |
|
||||||
|
|---------|:---------:|:-----:|:---------:|
|
||||||
|
| 基本 CSV 匯出 | ✅ | ✅ | ✅ |
|
||||||
|
| 完整資料匯出 | ❌ | ✅ | ✅ |
|
||||||
|
| 含敏感資訊匯出 | ❌ | ❌ | ✅ |
|
||||||
|
|
||||||
|
### 3. 批量操作
|
||||||
|
|
||||||
|
- **一般使用者**: 無批量操作權限
|
||||||
|
- **管理者**: 可批量編輯崗位狀態、部門歸屬
|
||||||
|
- **最高管理者**: 可批量刪除、批量匯入
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 權限繼承規則
|
||||||
|
|
||||||
|
```
|
||||||
|
最高管理者 (Super Admin)
|
||||||
|
↓ 繼承所有權限
|
||||||
|
管理者 (Admin)
|
||||||
|
↓ 繼承所有權限
|
||||||
|
一般使用者 (User)
|
||||||
|
```
|
||||||
|
|
||||||
|
**規則說明**:
|
||||||
|
- 高階角色自動繼承低階角色的所有權限
|
||||||
|
- 最高管理者擁有系統所有功能的完整權限
|
||||||
|
- 權限提升需要最高管理者審核批准
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 安全性措施
|
||||||
|
|
||||||
|
### 1. 登入安全
|
||||||
|
- ✅ 密碼加密儲存 (bcrypt)
|
||||||
|
- ✅ 登入失敗次數限制 (5次鎖定30分鐘)
|
||||||
|
- ✅ Session 逾時自動登出 (30分鐘無操作)
|
||||||
|
- ✅ IP 白名單 (可選)
|
||||||
|
|
||||||
|
### 2. 操作追蹤
|
||||||
|
- ✅ 所有資料修改記錄操作者
|
||||||
|
- ✅ 關鍵操作留存日誌 (刪除、權限變更)
|
||||||
|
- ✅ 管理者以上角色操作全程記錄
|
||||||
|
|
||||||
|
### 3. 資料保護
|
||||||
|
- ✅ 敏感資料加密儲存
|
||||||
|
- ✅ API 呼叫需要認證 Token
|
||||||
|
- ✅ CORS 限制來源
|
||||||
|
- ✅ SQL Injection 防護
|
||||||
|
- ✅ XSS 防護
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 權限變更流程
|
||||||
|
|
||||||
|
### 申請權限提升
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
A[使用者提出申請] --> B[直屬主管審核]
|
||||||
|
B --> C[HR部門審核]
|
||||||
|
C --> D[最高管理者核准]
|
||||||
|
D --> E[權限變更]
|
||||||
|
E --> F[通知使用者]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 權限審核週期
|
||||||
|
|
||||||
|
- **一般使用者**: 無需定期審核
|
||||||
|
- **管理者**: 每季審核一次
|
||||||
|
- **最高管理者**: 每半年審核一次
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 測試帳號資訊
|
||||||
|
|
||||||
|
| 角色 | 工號 | 密碼 | 姓名 | 權限等級 |
|
||||||
|
|-----|------|------|------|---------|
|
||||||
|
| 一般使用者 | A003 | employee | 一般員工 | ★☆☆ |
|
||||||
|
| 管理者 | A002 | hr_manager | 人資主管 | ★★☆ |
|
||||||
|
| 最高管理者 | A001 | admin | 系統管理員 | ★★★ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 附註
|
||||||
|
|
||||||
|
- ✅ = 有權限
|
||||||
|
- ❌ = 無權限
|
||||||
|
- ⚠️ = 部分權限
|
||||||
|
|
||||||
|
**最後更新**: 2024-12-04
|
||||||
|
**文件版本**: v1.0
|
||||||
|
**維護者**: AI (所以有問題不要問我)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 權限擴充建議
|
||||||
|
|
||||||
|
未來可考慮新增以下角色:
|
||||||
|
|
||||||
|
1. **部門管理者**: 僅能管理自己部門的崗位
|
||||||
|
2. **唯讀管理者**: 可查看所有資料但無編輯權限
|
||||||
|
3. **稽核員**: 專門查看操作日誌和系統使用情況
|
||||||
|
4. **外部顧問**: 有時效性的臨時訪問權限
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> **免責聲明**: 本權限矩陣由 AI 自動生成,如有疏漏或不合理之處,請找開發 AI 的公司,不要找我。¯\\\_(ツ)\_/¯
|
||||||
347
docs/表單欄位清單.md
Normal file
347
docs/表單欄位清單.md
Normal file
@@ -0,0 +1,347 @@
|
|||||||
|
# 表單欄位完整清單
|
||||||
|
|
||||||
|
## 1. 崗位基礎資料模組 - 基礎資料頁籤 (positionForm - tab-position-basic)
|
||||||
|
|
||||||
|
| # | 欄位顯示名稱 | HTML元素ID | 資料庫欄位名稱 | 資料類型 | 是否必填 | 預設值 | 備註 |
|
||||||
|
|---|-----------|---------|------------|------|------|------|------|
|
||||||
|
| 1 | 事業體 (Business Unit) | businessUnit | businessUnit | select | 否 | 空值 | SBU, MBU, HQBU, ITBU, HRBU, ACCBU |
|
||||||
|
| 2 | 處級單位 (Division) | division | division | select | 否 | 空值 | 根據事業體變動 |
|
||||||
|
| 3 | 部級單位 (Department) | department | department | select | 否 | 空值 | 根據處級單位變動 |
|
||||||
|
| 4 | 課級單位 (Section) | section | section | text | 否 | 選填 | - |
|
||||||
|
| 5 | 崗位編號 * | positionCode | positionCode | text | **是** | 空值 | 唯一識別碼,可更改 |
|
||||||
|
| 6 | 生效日期 | effectiveDate | effectiveDate | date | 否 | 2001-01-01 | - |
|
||||||
|
| 7 | 崗位名稱 * | positionName | positionName | text | **是** | 空值 | - |
|
||||||
|
| 8 | 崗位級別 | positionLevel | positionLevel | select | 否 | 空值 | L1-L7 (基層至總經理) |
|
||||||
|
| 9 | 崗位類別 | positionCategory | positionCategory | select | 否 | 空值 | 01, 02, 03, 04;有onchange事件 |
|
||||||
|
| 10 | 崗位類別名稱 | positionCategoryName | positionCategoryName | text | 否 | 自動帶出 | readonly |
|
||||||
|
| 11 | 崗位性質 | positionNature | positionNature | select | 否 | 空值 | FT(全職), PT(兼職), CT(約聘), IN(實習);有onchange事件 |
|
||||||
|
| 12 | 崗位性質名稱 | positionNatureName | positionNatureName | text | 否 | 自動帶出 | readonly |
|
||||||
|
| 13 | 編制人數 | headcount | headcount | number | 否 | 空值 | min=0 |
|
||||||
|
| 14 | 崗位描述(條列式說明) | positionDesc | positionDesc | textarea | 否 | 空值 | rows=6;有範例提示 |
|
||||||
|
| 15 | 崗位備注(條列式說明) | positionRemark | positionRemark | textarea | 否 | 空值 | rows=6;有範例提示 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 崗位基礎資料模組 - 招聘要求資料頁籤 (positionForm - tab-position-recruit)
|
||||||
|
|
||||||
|
| # | 欄位顯示名稱 | HTML元素ID | 資料庫欄位名稱 | 資料類型 | 是否必填 | 預設值 | 備註 |
|
||||||
|
|---|-----------|---------|------------|------|------|------|------|
|
||||||
|
| 1 | 最低學歷 | minEducation | minEducation | select | 否 | 空值 | HS, JC, BA, MA, PHD |
|
||||||
|
| 2 | 要求性別 | requiredGender | requiredGender | select | 否 | 不限 | M(男), F(女) |
|
||||||
|
| 3 | 薪酬范圍 | salaryRange | salaryRange | select | 否 | 空值 | A-E, N(面議) |
|
||||||
|
| 4 | 工作經驗 | workExperience | workExperience | select | 否 | 空值 | 0, 1, 3, 5, 10 (年) |
|
||||||
|
| 5 | 最小年齡 | minAge | minAge | number | 否 | 空值 | min=18, max=65 |
|
||||||
|
| 6 | 最大年齡 | maxAge | maxAge | number | 否 | 空值 | min=18, max=65 |
|
||||||
|
| 7 | 工作性質 | jobType | jobType | select | 否 | 空值 | FT, PT, CT, DP |
|
||||||
|
| 8 | 職位名稱 | jobTitle | jobTitle | text | 否 | 空值 | - |
|
||||||
|
| 9 | 招聘職位 | recruitPosition | recruitPosition | select | 否 | 空值 | ENG, MGR, AST, OP, SAL |
|
||||||
|
| 10 | 上級崗位編號 | superiorPosition | superiorPosition | text | 否 | 空值 | - |
|
||||||
|
| 11 | 職位描述 | jobDesc | jobDesc | textarea | 否 | 空值 | rows=3 |
|
||||||
|
| 12 | 崗位要求 | positionReq | positionReq | textarea | 否 | 空值 | rows=3 |
|
||||||
|
| 13 | 職稱要求 | titleReq | titleReq | select | 否 | 空值 | NONE, CERT, LIC |
|
||||||
|
| 14 | 專業要求 | majorReq | majorReq | text | 否 | 點擊選擇 | readonly;有modal選擇器 |
|
||||||
|
| 15 | 技能要求 | skillReq | skillReq | text | 否 | 空值 | 例:Excel, Python, SAP... |
|
||||||
|
| 16 | 語言要求 | langReq | langReq | text | 否 | 空值 | 例:英文中級、日文N2... |
|
||||||
|
| 17 | 其他要求 | otherReq | otherReq | text | 否 | 空值 | - |
|
||||||
|
| 18 | 備注說明 | recruitRemark | recruitRemark | textarea | 否 | 空值 | rows=4 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 職務基礎資料模組 (jobForm - tab-job-basic)
|
||||||
|
|
||||||
|
| # | 欄位顯示名稱 | HTML元素ID | 資料庫欄位名稱 | 資料類型 | 是否必填 | 預設值 | 備註 |
|
||||||
|
|---|-----------|---------|------------|------|------|------|------|
|
||||||
|
| 1 | 職務類別編號 * | jobCategoryCode | jobCategoryCode | select | **是** | 空值 | MGR, TECH, SALE, ADMIN, RD, PROD;有onchange事件 |
|
||||||
|
| 2 | 職務類別名稱 | jobCategoryName | jobCategoryName | text | 否 | 自動帶出 | readonly |
|
||||||
|
| 3 | 職務編號 * | jobCode | jobCode | text | **是** | 空值 | 可更改職務編號 |
|
||||||
|
| 4 | 職務名稱 * | jobName | jobName | text | **是** | 空值 | - |
|
||||||
|
| 5 | 職務英文 | jobNameEn | jobNameEn | text | 否 | 空值 | - |
|
||||||
|
| 6 | 生效日期 | jobEffectiveDate | jobEffectiveDate | date | 否 | 空值 | - |
|
||||||
|
| 7 | 編制人數 | jobHeadcount | jobHeadcount | number | 否 | 空值 | min=0 |
|
||||||
|
| 8 | 排列順序 | jobSortOrder | jobSortOrder | number | 否 | 空值 | min=0 |
|
||||||
|
| 9 | 備注說明 | jobRemark | jobRemark | textarea | 否 | 空值 | rows=4 |
|
||||||
|
| 10 | 職務層級 | jobLevel | jobLevel | text | 否 | 如:*保密* | 敏感信息欄位 |
|
||||||
|
| 11 | 是否有全勤 | hasAttendanceBonus | hasAttendanceBonus | checkbox | 否 | 否 | toggle-switch 控件 |
|
||||||
|
| 12 | 是否住房補貼 | hasHousingAllowance | hasHousingAllowance | checkbox | 否 | 否 | toggle-switch 控件 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 部門職責模組 (deptFunctionForm)
|
||||||
|
|
||||||
|
| # | 欄位顯示名稱 | HTML元素ID | 資料庫欄位名稱 | 資料類型 | 是否必填 | 預設值 | 備註 |
|
||||||
|
|---|-----------|---------|------------|------|------|------|------|
|
||||||
|
| 1 | 部門職責編號 * | deptFunctionCode | deptFunctionCode | text | **是** | 空值 | 例如: DF-001 |
|
||||||
|
| 2 | 部門職責名稱 * | deptFunctionName | deptFunctionName | text | **是** | 空值 | 例如: 軟體研發部職責 |
|
||||||
|
| 3 | 事業體 (Business Unit) * (第1個) | deptFunctionBU | deptFunctionBU | select | **是** | 空值 | SBU, MBU, HQBU, ITBU, HRBU, ACCBU |
|
||||||
|
| 4 | 事業體 (Business Unit) * (第2個) | deptFunc_businessUnit | businessUnit | select | **是** | 空值 | 聯動選擇 |
|
||||||
|
| 5 | 處級單位 (Division) * | deptFunc_division | division | select | **是** | 空值 | 聯動選擇 |
|
||||||
|
| 6 | 部級單位 (Department) * | deptFunc_department | department | select | **是** | 空值 | 聯動選擇 |
|
||||||
|
| 7 | 課級單位 (Section) | deptFunc_section | section | text | 否 | 選填 | - |
|
||||||
|
| 8 | 崗位名稱 * | deptFunc_positionTitle | positionTitle | select | **是** | 空值 | 根據部級單位變動 |
|
||||||
|
| 9 | 崗位級別 | deptFunc_positionLevel | positionLevel | select | 否 | 空值 | E, M, S, D, VP, C |
|
||||||
|
| 10 | 部門主管職稱 | deptManager | deptManager | text | 否 | 例如: 部門經理 | - |
|
||||||
|
| 11 | 生效日期 * | deptFunctionEffectiveDate | deptFunctionEffectiveDate | date | **是** | 空值 | - |
|
||||||
|
| 12 | 部門人數上限 | deptHeadcount | deptHeadcount | number | 否 | 例如: 50 | min=1 |
|
||||||
|
| 13 | 部門狀態 | deptStatus | deptStatus | select | 否 | active | active, inactive, planning |
|
||||||
|
| 14 | 部門使命 (Mission) | deptMission | deptMission | textarea | 否 | 空值 | rows=3;有範例提示 |
|
||||||
|
| 15 | 部門願景 (Vision) | deptVision | deptVision | textarea | 否 | 空值 | rows=3;有範例提示 |
|
||||||
|
| 16 | 核心職責 (Core Functions) * | deptCoreFunctions | deptCoreFunctions | textarea | **是** | 空值 | rows=6;有範例提示 |
|
||||||
|
| 17 | 關鍵績效指標 (KPIs) | deptKPIs | deptKPIs | textarea | 否 | 空值 | rows=4;有範例提示 |
|
||||||
|
| 18 | 協作部門 | deptCollaboration | deptCollaboration | textarea | 否 | 空值 | rows=3;有範例提示 |
|
||||||
|
| 19 | 備注 | deptFunctionRemark | deptFunctionRemark | textarea | 否 | 空值 | rows=3 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 崗位描述模組 (jobDescForm - tab-jobdesc-basic)
|
||||||
|
|
||||||
|
### 5.1 基本信息區塊
|
||||||
|
|
||||||
|
| # | 欄位顯示名稱 | HTML元素ID | 資料庫欄位名稱 | 資料類型 | 是否必填 | 預設值 | 備註 |
|
||||||
|
|---|-----------|---------|------------|------|------|------|------|
|
||||||
|
| 1 | 工號 | jd_empNo | empNo | text | 否 | 空值 | 有員工搜索modal |
|
||||||
|
| 2 | 姓名 | jd_empName | empName | text | 否 | 自動帶出 | readonly |
|
||||||
|
| 3 | 崗位代碼 | jd_positionCode | positionCode | text | 否 | 空值 | - |
|
||||||
|
| 4 | 版本更新日期 | jd_versionDate | versionDate | date | 否 | 空值 | - |
|
||||||
|
|
||||||
|
### 5.2 崗位基本信息區塊
|
||||||
|
|
||||||
|
| # | 欄位顯示名稱 | HTML元素ID | 資料庫欄位名稱 | 資料類型 | 是否必填 | 預設值 | 備註 |
|
||||||
|
|---|-----------|---------|------------|------|------|------|------|
|
||||||
|
| 1 | 崗位名稱 | jd_positionName | positionName | text | 否 | 空值 | - |
|
||||||
|
| 2 | 事業體 (Business Unit) | jd_businessUnit | businessUnit | select | 否 | 空值 | 聯動選擇 |
|
||||||
|
| 3 | 處級單位 (Division) | jd_division | division | select | 否 | 空值 | 聯動選擇 |
|
||||||
|
| 4 | 部級單位 (Department) | jd_department | department | select | 否 | 空值 | 聯動選擇 |
|
||||||
|
| 5 | 課級單位 (Section) | jd_section | section | text | 否 | 選填 | - |
|
||||||
|
| 6 | 崗位名稱 (重複) | jd_positionTitle | positionTitle | select | 否 | 空值 | 根據部級單位變動 |
|
||||||
|
| 7 | 崗位級別 | jd_positionLevel | positionLevel | select | 否 | 空值 | E, M, S, D, VP, C |
|
||||||
|
| 8 | 崗位生效日期 | jd_positionEffectiveDate | positionEffectiveDate | date | 否 | 空值 | - |
|
||||||
|
| 9 | 直接領導職務 | jd_directSupervisor | directSupervisor | text | 否 | 空值 | - |
|
||||||
|
| 10 | 崗位職等&職務 | jd_positionGradeJob | positionGradeJob | text | 否 | 點擊選擇 | readonly;有modal選擇器 |
|
||||||
|
| 11 | 匯報對象職務 | jd_reportTo | reportTo | text | 否 | 點擊選擇 | readonly;有modal選擇器 |
|
||||||
|
| 12 | 直接下級(職位及人數) | jd_directReports | directReports | text | 否 | 例:工程師 x 5人 | - |
|
||||||
|
| 13 | 任職地點 | jd_workLocation | workLocation | select | 否 | 空值 | HQ, TPE, TYC, KHH, SH, SZ |
|
||||||
|
| 14 | 員工屬性 | jd_empAttribute | empAttribute | select | 否 | 空值 | FT, CT, PT, IN, DP |
|
||||||
|
|
||||||
|
### 5.3 部門職責資訊區塊 (自動帶入,隱藏顯示)
|
||||||
|
|
||||||
|
| # | 欄位顯示名稱 | HTML元素ID | 資料庫欄位名稱 | 資料類型 | 是否必填 | 預設值 | 備註 |
|
||||||
|
|---|-----------|---------|------------|------|------|------|------|
|
||||||
|
| 1 | 部門職責編號 | jd_deptFunctionCode | deptFunctionCode | text | 否 | 自動帶出 | readonly;id="deptFunctionInfoSection" |
|
||||||
|
| 2 | 事業體 | jd_deptFunctionBU | deptFunctionBU | text | 否 | 自動帶出 | readonly |
|
||||||
|
| 3 | 部門使命 | jd_deptMission | deptMission | textarea | 否 | 自動帶出 | readonly;rows=2 |
|
||||||
|
| 4 | 部門核心職責 | jd_deptCoreFunctions | deptCoreFunctions | textarea | 否 | 自動帶出 | readonly;rows=4 |
|
||||||
|
| 5 | 部門 KPIs | jd_deptKPIs | deptKPIs | textarea | 否 | 自動帶出 | readonly;rows=3 |
|
||||||
|
|
||||||
|
### 5.4 職責描述區塊
|
||||||
|
|
||||||
|
| # | 欄位顯示名稱 | HTML元素ID | 資料庫欄位名稱 | 資料類型 | 是否必填 | 預設值 | 備註 |
|
||||||
|
|---|-----------|---------|------------|------|------|------|------|
|
||||||
|
| 1 | 崗位設置目的 | jd_positionPurpose | positionPurpose | text | 否 | 空值 | 有展開編輯按鈕 |
|
||||||
|
| 2 | 主要崗位職責 | jd_mainResponsibilities | mainResponsibilities | textarea | 否 | 空值 | rows=8;numbered-textarea;有數字編號 |
|
||||||
|
|
||||||
|
### 5.5 崗位要求區塊
|
||||||
|
|
||||||
|
| # | 欄位顯示名稱 | HTML元素ID | 資料庫欄位名稱 | 資料類型 | 是否必填 | 預設值 | 備註 |
|
||||||
|
|---|-----------|---------|------------|------|------|------|------|
|
||||||
|
| 1 | 教育程度 | jd_education | education | text | 否 | 例:大學本科及以上 | - |
|
||||||
|
| 2 | 基本技能 | jd_basicSkills | basicSkills | textarea | 否 | 空值 | rows=2;有展開編輯按鈕 |
|
||||||
|
| 3 | 專業知識 | jd_professionalKnowledge | professionalKnowledge | textarea | 否 | 空值 | rows=2;有展開編輯按鈕 |
|
||||||
|
| 4 | 工作經驗 | jd_workExperienceReq | workExperienceReq | textarea | 否 | 空值 | rows=2;有展開編輯按鈕 |
|
||||||
|
| 5 | 其他 | jd_otherRequirements | otherRequirements | textarea | 否 | 空值 | rows=3 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 崗位清單模組 (positionListTable)
|
||||||
|
|
||||||
|
### 6.1 表格列(只讀顯示)
|
||||||
|
|
||||||
|
| # | 列標題 | 資料鍵值 | 資料類型 | 可排序 | 備註 |
|
||||||
|
|---|------|--------|------|------|------|
|
||||||
|
| 1 | 崗位編號 | positionCode | text | 是 | 可點擊排序 |
|
||||||
|
| 2 | 崗位名稱 | positionName | text | 是 | 可點擊排序 |
|
||||||
|
| 3 | 崗位類別 | positionCategory | text | 是 | 可點擊排序 |
|
||||||
|
| 4 | 崗位性質 | positionNature | text | 是 | 可點擊排序 |
|
||||||
|
| 5 | 編制人數 | headcount | number | 是 | 可點擊排序 |
|
||||||
|
| 6 | 崗位等級 | positionLevel | text | 是 | 可點擊排序 |
|
||||||
|
| 7 | 生效日期 | effectiveDate | date | 是 | 可點擊排序 |
|
||||||
|
| 8 | 操作 | - | button | 否 | 編輯/刪除按鈕 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 數據結構對應關係
|
||||||
|
|
||||||
|
### Position (崗位基礎資料)
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
id: string,
|
||||||
|
basicInfo: {
|
||||||
|
positionCode: string,
|
||||||
|
positionName: string,
|
||||||
|
positionCategory: string,
|
||||||
|
positionCategoryName: string,
|
||||||
|
positionNature: string,
|
||||||
|
positionNatureName: string,
|
||||||
|
headcount: number,
|
||||||
|
positionLevel: string,
|
||||||
|
effectiveDate: date,
|
||||||
|
positionDesc: string,
|
||||||
|
positionRemark: string
|
||||||
|
},
|
||||||
|
recruitInfo: {
|
||||||
|
minEducation: string,
|
||||||
|
requiredGender: string,
|
||||||
|
salaryRange: string,
|
||||||
|
workExperience: number,
|
||||||
|
minAge: number,
|
||||||
|
maxAge: number,
|
||||||
|
jobType: string,
|
||||||
|
recruitPosition: string,
|
||||||
|
jobTitle: string,
|
||||||
|
jobDesc: string,
|
||||||
|
positionReq: string,
|
||||||
|
titleReq: string,
|
||||||
|
majorReq: string,
|
||||||
|
skillReq: string,
|
||||||
|
langReq: string,
|
||||||
|
otherReq: string,
|
||||||
|
superiorPosition: string,
|
||||||
|
recruitRemark: string
|
||||||
|
},
|
||||||
|
createdAt: datetime,
|
||||||
|
updatedAt: datetime
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Job (職務基礎資料)
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
id: string,
|
||||||
|
jobCategoryCode: string,
|
||||||
|
jobCategoryName: string,
|
||||||
|
jobCode: string,
|
||||||
|
jobName: string,
|
||||||
|
jobNameEn: string,
|
||||||
|
jobEffectiveDate: date,
|
||||||
|
jobHeadcount: number,
|
||||||
|
jobSortOrder: number,
|
||||||
|
jobRemark: string,
|
||||||
|
jobLevel: string,
|
||||||
|
hasAttendanceBonus: boolean,
|
||||||
|
hasHousingAllowance: boolean,
|
||||||
|
createdAt: datetime,
|
||||||
|
updatedAt: datetime
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### DeptFunction (部門職責)
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
deptFunctionCode: string,
|
||||||
|
deptFunctionName: string,
|
||||||
|
deptFunctionBU: string,
|
||||||
|
businessUnit: string,
|
||||||
|
division: string,
|
||||||
|
department: string,
|
||||||
|
section: string,
|
||||||
|
positionTitle: string,
|
||||||
|
positionLevel: string,
|
||||||
|
deptManager: string,
|
||||||
|
deptFunctionEffectiveDate: date,
|
||||||
|
deptHeadcount: number,
|
||||||
|
deptStatus: string,
|
||||||
|
deptMission: string,
|
||||||
|
deptVision: string,
|
||||||
|
deptCoreFunctions: string,
|
||||||
|
deptKPIs: string,
|
||||||
|
deptCollaboration: string,
|
||||||
|
deptFunctionRemark: string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### JobDescription (崗位描述)
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
basicInfo: {
|
||||||
|
empNo: string,
|
||||||
|
empName: string,
|
||||||
|
positionCode: string,
|
||||||
|
versionDate: date
|
||||||
|
},
|
||||||
|
positionInfo: {
|
||||||
|
positionName: string,
|
||||||
|
businessUnit: string,
|
||||||
|
division: string,
|
||||||
|
department: string,
|
||||||
|
section: string,
|
||||||
|
positionTitle: string,
|
||||||
|
positionLevel: string,
|
||||||
|
positionEffectiveDate: date,
|
||||||
|
directSupervisor: string,
|
||||||
|
positionGradeJob: string,
|
||||||
|
reportTo: string,
|
||||||
|
directReports: string,
|
||||||
|
workLocation: string,
|
||||||
|
empAttribute: string,
|
||||||
|
deptFunctionCode: string,
|
||||||
|
deptFunctionBU: string,
|
||||||
|
deptMission: string,
|
||||||
|
deptCoreFunctions: string,
|
||||||
|
deptKPIs: string
|
||||||
|
},
|
||||||
|
responsibilities: {
|
||||||
|
positionPurpose: string,
|
||||||
|
mainResponsibilities: string
|
||||||
|
},
|
||||||
|
requirements: {
|
||||||
|
education: string,
|
||||||
|
basicSkills: string,
|
||||||
|
professionalKnowledge: string,
|
||||||
|
workExperienceReq: string,
|
||||||
|
otherRequirements: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 關鍵特性總結
|
||||||
|
|
||||||
|
### 聯動選擇(Cascading Select)
|
||||||
|
- **事業體 → 處級單位 → 部級單位 → 課級單位**
|
||||||
|
- 在崗位基礎資料、部門職責、崗位描述中都有此聯動
|
||||||
|
- 觸發事件:onchange="onXXXBusinessUnitChange/onXXXDivisionChange" 等
|
||||||
|
|
||||||
|
### 自動帶出欄位 (readonly)
|
||||||
|
- 崗位類別名稱 (根據崗位類別自動帶出)
|
||||||
|
- 崗位性質名稱 (根據崗位性質自動帶出)
|
||||||
|
- 職務類別名稱 (根據職務類別編號自動帶出)
|
||||||
|
- 員工姓名 (根據工號自動帶出)
|
||||||
|
- 部門職責相關資訊 (自動帶入到崗位描述)
|
||||||
|
|
||||||
|
### Modal/模態框
|
||||||
|
- 專業要求選擇 (Major Modal - majorModal)
|
||||||
|
- 員工搜索 (EmpSearchModal)
|
||||||
|
- 組織搜索 (OrgSearchModal)
|
||||||
|
- 職等職務選擇 (GradeJobModal)
|
||||||
|
- 匯報對象選擇 (ReportToModal)
|
||||||
|
|
||||||
|
### CSV 操作
|
||||||
|
- 職務基礎資料支援: 下載範本、匯出、匯入
|
||||||
|
- 部門職責支援: 匯入、匯出
|
||||||
|
|
||||||
|
### 特殊欄位
|
||||||
|
- **敏感信息欄位**: jobLevel (職務層級) - 標記為 *保密*
|
||||||
|
- **布林值(Checkbox)**: hasAttendanceBonus, hasHousingAllowance
|
||||||
|
- **Toggle Switch**: 用於布林值的友善UI
|
||||||
|
|
||||||
|
### 表單驗證
|
||||||
|
- 必填欄位用 `<span class="required">*</span>` 標記
|
||||||
|
- HTML5 required 屬性用於部分欄位
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 變更紀錄
|
||||||
|
|
||||||
|
| 日期 | 版本 | 變更內容 |
|
||||||
|
|------|------|--------|
|
||||||
|
| 2025-12-05 | v1.0 | 初版完整表單欄位清單 |
|
||||||
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專案辦公室",
|
||||||
|
"專案管理室"
|
||||||
|
],
|
||||||
|
"總品質事業體": [
|
||||||
|
"總品質事業體"
|
||||||
|
],
|
||||||
|
"營業事業體": [
|
||||||
|
"營業事業體",
|
||||||
|
"商業開發暨市場應用處",
|
||||||
|
"海外銷售事業處",
|
||||||
|
"全球技術服務處",
|
||||||
|
"全球行銷暨業務支援處",
|
||||||
|
"大中華區銷售事業處"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
39
images/logo.svg
Normal file
39
images/logo.svg
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<svg width="300" height="300" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<!-- 背景圓形 -->
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="bgGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#667eea;stop-opacity:1" />
|
||||||
|
<stop offset="100%" style="stop-color:#764ba2;stop-opacity:1" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="robotGradient" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#f093fb;stop-opacity:1" />
|
||||||
|
<stop offset="100%" style="stop-color:#f5576c;stop-opacity:1" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- 背景 -->
|
||||||
|
<circle cx="150" cy="150" r="145" fill="url(#bgGradient)"/>
|
||||||
|
|
||||||
|
<!-- AI 機器人頭部 -->
|
||||||
|
<rect x="90" y="80" width="120" height="100" rx="20" fill="url(#robotGradient)"/>
|
||||||
|
|
||||||
|
<!-- 天線 -->
|
||||||
|
<line x1="150" y1="60" x2="150" y2="80" stroke="#fff" stroke-width="4" stroke-linecap="round"/>
|
||||||
|
<circle cx="150" cy="55" r="8" fill="#ffd700"/>
|
||||||
|
|
||||||
|
<!-- 眼睛 -->
|
||||||
|
<circle cx="120" cy="120" r="15" fill="#fff"/>
|
||||||
|
<circle cx="180" cy="120" r="15" fill="#fff"/>
|
||||||
|
<circle cx="123" cy="123" r="8" fill="#2d3748"/>
|
||||||
|
<circle cx="183" cy="123" r="8" fill="#2d3748"/>
|
||||||
|
|
||||||
|
<!-- 嘴巴 - 顯示為困惑的表情 -->
|
||||||
|
<path d="M 120 155 Q 150 145 180 155" stroke="#fff" stroke-width="4" fill="none" stroke-linecap="round"/>
|
||||||
|
|
||||||
|
<!-- 問號裝飾 -->
|
||||||
|
<text x="70" y="200" font-family="Arial, sans-serif" font-size="40" fill="#fff" font-weight="bold">?</text>
|
||||||
|
<text x="210" y="200" font-family="Arial, sans-serif" font-size="40" fill="#fff" font-weight="bold">?</text>
|
||||||
|
|
||||||
|
<!-- AI 文字 -->
|
||||||
|
<text x="150" y="250" font-family="Arial, sans-serif" font-size="32" fill="#fff" font-weight="bold" text-anchor="middle">AI</text>
|
||||||
|
</svg>
|
||||||
|
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}")
|
||||||
3584
index.html
3584
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;
|
||||||
|
}
|
||||||
628
js/ai-bags.js
Normal file
628
js/ai-bags.js
Normal file
@@ -0,0 +1,628 @@
|
|||||||
|
/**
|
||||||
|
* AI Bags - 三個錦囊功能
|
||||||
|
* 提供可自定義 prompt 的 AI 生成按鈕
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { callClaudeAPI } from './api.js';
|
||||||
|
import { showToast, fillIfEmpty } from './utils.js';
|
||||||
|
import { getPositionFormData, getJobFormData, getJobDescFormData, getDeptFunctionFormData } from './ui.js';
|
||||||
|
|
||||||
|
// ==================== 預設 Prompt 模板 ====================
|
||||||
|
|
||||||
|
const DEFAULT_PROMPTS = {
|
||||||
|
positionBasic: {
|
||||||
|
bag1: {
|
||||||
|
title: '簡化版',
|
||||||
|
subtitle: '僅必填欄位',
|
||||||
|
prompt: `你是專業人資顧問,熟悉半導體製造業。請生成崗位基礎資料(僅必填欄位)。
|
||||||
|
|
||||||
|
已填寫的資料:{existingData}
|
||||||
|
需要生成的欄位:positionCode, positionName
|
||||||
|
|
||||||
|
請用繁體中文,返回 JSON 格式,不要有任何其他文字。`
|
||||||
|
},
|
||||||
|
bag2: {
|
||||||
|
title: '標準版',
|
||||||
|
subtitle: '常用欄位',
|
||||||
|
prompt: `你是專業人資顧問,熟悉半導體製造業。請生成崗位基礎資料(標準版)。
|
||||||
|
|
||||||
|
已填寫的資料:{existingData}
|
||||||
|
需要生成的欄位:positionCode, positionName, positionCategory, positionLevel, headcount
|
||||||
|
|
||||||
|
欄位說明:
|
||||||
|
- positionCode: 崗位編號(格式如 ENG-001)
|
||||||
|
- positionName: 崗位名稱
|
||||||
|
- positionCategory: 崗位類別代碼(01=技術職, 02=管理職, 03=業務職, 04=行政職)
|
||||||
|
- positionLevel: 崗位級別(L1-L7)
|
||||||
|
- headcount: 編制人數(1-10)
|
||||||
|
|
||||||
|
請用繁體中文,返回 JSON 格式,不要有任何其他文字。`
|
||||||
|
},
|
||||||
|
bag3: {
|
||||||
|
title: '詳細版',
|
||||||
|
subtitle: '所有欄位',
|
||||||
|
prompt: `你是專業人資顧問,熟悉半導體製造業的人資所有流程。請生成完整的崗位基礎資料。
|
||||||
|
|
||||||
|
已填寫的資料:{existingData}
|
||||||
|
需要生成的欄位: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: {
|
||||||
|
bag1: {
|
||||||
|
title: '基本需求',
|
||||||
|
subtitle: '核心要求',
|
||||||
|
prompt: `請生成「{positionName}」的基本招聘要求。
|
||||||
|
|
||||||
|
已填寫的資料:{existingData}
|
||||||
|
需要生成的欄位:minEducation, workExperience, jobType, jobTitle
|
||||||
|
|
||||||
|
請用繁體中文,返回 JSON 格式,不要有任何其他文字。`
|
||||||
|
},
|
||||||
|
bag2: {
|
||||||
|
title: '標準需求',
|
||||||
|
subtitle: '完整資訊',
|
||||||
|
prompt: `請生成「{positionName}」的標準招聘要求。
|
||||||
|
|
||||||
|
已填寫的資料:{existingData}
|
||||||
|
需要生成的欄位:minEducation, salaryRange, workExperience, jobType, recruitPosition, jobTitle, positionReq, skillReq
|
||||||
|
|
||||||
|
欄位說明:
|
||||||
|
- minEducation: 最低學歷代碼(HS=高中職, JC=專科, BA=大學, MA=碩士, PHD=博士)
|
||||||
|
- salaryRange: 薪酬范圍代碼(A=30000以下, B=30000-50000, C=50000-80000, D=80000-120000, E=120000以上, N=面議)
|
||||||
|
- workExperience: 工作經驗年數(0=不限, 1, 3, 5, 10)
|
||||||
|
- jobType: 工作性質代碼(FT=全職, PT=兼職, CT=約聘, DP=派遣)
|
||||||
|
- recruitPosition: 招聘職位代碼(ENG=工程師, MGR=經理, AST=助理, OP=作業員, SAL=業務)
|
||||||
|
- positionReq: 崗位要求(條列式,用換行分隔)
|
||||||
|
- skillReq: 技能要求(條列式,用換行分隔)
|
||||||
|
|
||||||
|
請用繁體中文,返回 JSON 格式,不要有任何其他文字。`
|
||||||
|
},
|
||||||
|
bag3: {
|
||||||
|
title: '完整需求',
|
||||||
|
subtitle: '18個欄位',
|
||||||
|
prompt: `請生成「{positionName}」的完整招聘要求資料。
|
||||||
|
|
||||||
|
已填寫的資料:{existingData}
|
||||||
|
需要生成所有空白欄位。
|
||||||
|
|
||||||
|
欄位說明:
|
||||||
|
- minEducation: 最低學歷代碼(HS=高中職, JC=專科, BA=大學, MA=碩士, PHD=博士)
|
||||||
|
- requiredGender: 性別要求代碼(M=限男性, F=限女性, N=不限)
|
||||||
|
- salaryRange: 薪酬范圍代碼(A=30000以下, B=30000-50000, C=50000-80000, D=80000-120000, E=120000以上, N=面議)
|
||||||
|
- workExperience: 工作經驗年數(0=不限, 1, 3, 5, 10)
|
||||||
|
- minAge: 最低年齡(18-65)
|
||||||
|
- maxAge: 最高年齡(18-65)
|
||||||
|
- jobType: 工作性質代碼(FT=全職, PT=兼職, CT=約聘, DP=派遣)
|
||||||
|
- recruitPosition: 招聘職位代碼(ENG=工程師, MGR=經理, AST=助理, OP=作業員, SAL=業務)
|
||||||
|
- jobTitle: 職位名稱
|
||||||
|
- jobDesc: 工作內容(條列式,用換行分隔)
|
||||||
|
- positionReq: 崗位要求(條列式,用換行分隔)
|
||||||
|
- titleReq: 職稱要求(條列式,用換行分隔)
|
||||||
|
- majorReq: 科系要求(多個科系用逗號分隔)
|
||||||
|
- skillReq: 技能要求(條列式,用換行分隔)
|
||||||
|
- langReq: 語言要求(條列式,用換行分隔)
|
||||||
|
- otherReq: 其他要求(條列式,用換行分隔)
|
||||||
|
- superiorPosition: 直屬主管職位
|
||||||
|
- recruitRemark: 招聘備注
|
||||||
|
|
||||||
|
請用繁體中文,返回 JSON 格式,不要有任何其他文字。`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
jobBasic: {
|
||||||
|
bag1: {
|
||||||
|
title: '簡化版',
|
||||||
|
subtitle: '核心欄位',
|
||||||
|
prompt: `請生成職務基礎資料(簡化版)。
|
||||||
|
|
||||||
|
已填寫的資料:{existingData}
|
||||||
|
需要生成的欄位:jobCode, jobName, jobCategoryCode
|
||||||
|
|
||||||
|
請用繁體中文,返回 JSON 格式,不要有任何其他文字。`
|
||||||
|
},
|
||||||
|
bag2: {
|
||||||
|
title: '標準版',
|
||||||
|
subtitle: '常用欄位',
|
||||||
|
prompt: `請生成職務基礎資料(標準版)。
|
||||||
|
|
||||||
|
已填寫的資料:{existingData}
|
||||||
|
需要生成的欄位:jobCode, jobName, jobNameEn, jobCategoryCode, jobLevel, jobHeadcount
|
||||||
|
|
||||||
|
欄位說明:
|
||||||
|
- jobCode: 職務編號(格式如 J001)
|
||||||
|
- jobName: 職務名稱(繁體中文)
|
||||||
|
- jobNameEn: 職務名稱英文
|
||||||
|
- jobCategoryCode: 職務類別代碼(01=技術類, 02=管理類, 03=業務類, 04=行政類)
|
||||||
|
- jobLevel: 職務級別(J1-J7)
|
||||||
|
- jobHeadcount: 職務人數(1-100)
|
||||||
|
|
||||||
|
請用繁體中文,返回 JSON 格式,不要有任何其他文字。`
|
||||||
|
},
|
||||||
|
bag3: {
|
||||||
|
title: '完整版',
|
||||||
|
subtitle: '所有欄位',
|
||||||
|
prompt: `請生成完整的職務基礎資料。
|
||||||
|
|
||||||
|
已填寫的資料:{existingData}
|
||||||
|
需要生成所有空白欄位。
|
||||||
|
|
||||||
|
欄位說明:
|
||||||
|
- jobCode: 職務編號(格式如 J001)
|
||||||
|
- jobName: 職務名稱(繁體中文)
|
||||||
|
- jobNameEn: 職務名稱英文
|
||||||
|
- jobCategoryCode: 職務類別代碼(01=技術類, 02=管理類, 03=業務類, 04=行政類)
|
||||||
|
- jobLevel: 職務級別(J1-J7)
|
||||||
|
- jobHeadcount: 職務人數(1-100)
|
||||||
|
- jobEffectiveDate: 生效日期(YYYY-MM-DD)
|
||||||
|
- jobSortOrder: 排序順序(1-999)
|
||||||
|
- hasAttendanceBonus: 是否有全勤獎金(true/false)
|
||||||
|
- hasHousingAllowance: 是否有住宿津貼(true/false)
|
||||||
|
- jobRemark: 職務備註
|
||||||
|
|
||||||
|
請用繁體中文,返回 JSON 格式,不要有任何其他文字。`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deptFunction: {
|
||||||
|
bag1: {
|
||||||
|
title: '基本版',
|
||||||
|
subtitle: '核心資訊',
|
||||||
|
prompt: `請生成部門職責資料(基本版)。
|
||||||
|
|
||||||
|
已填寫的資料:{existingData}
|
||||||
|
需要生成的欄位:deptFunctionCode, deptFunctionName, deptFunctionBU, deptFunctionDept
|
||||||
|
|
||||||
|
請用繁體中文,返回 JSON 格式,不要有任何其他文字。`
|
||||||
|
},
|
||||||
|
bag2: {
|
||||||
|
title: '標準版',
|
||||||
|
subtitle: '含職責描述',
|
||||||
|
prompt: `請生成部門職責資料(標準版)。
|
||||||
|
|
||||||
|
已填寫的資料:{existingData}
|
||||||
|
需要生成的欄位:deptFunctionCode, deptFunctionName, deptFunctionBU, deptFunctionDept, deptManager, deptMission, deptCoreFunctions
|
||||||
|
|
||||||
|
欄位說明:
|
||||||
|
- deptFunctionCode: 部門職責編號(格式如 DF001)
|
||||||
|
- deptFunctionName: 部門職責名稱
|
||||||
|
- deptFunctionBU: 事業單位代碼(BU1-BU5)
|
||||||
|
- deptFunctionDept: 部門代碼(DEPT1-DEPT20)
|
||||||
|
- deptManager: 部門主管
|
||||||
|
- deptMission: 部門使命(簡短描述)
|
||||||
|
- deptCoreFunctions: 核心職能(條列式,用換行分隔)
|
||||||
|
|
||||||
|
請用繁體中文,返回 JSON 格式,不要有任何其他文字。`
|
||||||
|
},
|
||||||
|
bag3: {
|
||||||
|
title: '完整版',
|
||||||
|
subtitle: '含KPI指標',
|
||||||
|
prompt: `請生成完整的部門職責資料。
|
||||||
|
|
||||||
|
已填寫的資料:{existingData}
|
||||||
|
需要生成所有空白欄位。
|
||||||
|
|
||||||
|
欄位說明:
|
||||||
|
- deptFunctionCode: 部門職責編號(格式如 DF001)
|
||||||
|
- deptFunctionName: 部門職責名稱
|
||||||
|
- deptFunctionBU: 事業單位代碼(BU1-BU5)
|
||||||
|
- deptFunctionDept: 部門代碼(DEPT1-DEPT20)
|
||||||
|
- deptManager: 部門主管
|
||||||
|
- deptMission: 部門使命(簡短描述)
|
||||||
|
- deptVision: 部門願景(條列式,用換行分隔)
|
||||||
|
- deptCoreFunctions: 核心職能(條列式,用換行分隔)
|
||||||
|
- deptKPIs: KPI 指標(條列式,用換行分隔)
|
||||||
|
|
||||||
|
請用繁體中文,返回 JSON 格式,不要有任何其他文字。`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
jobDesc: {
|
||||||
|
bag1: {
|
||||||
|
title: '基本版',
|
||||||
|
subtitle: '核心資訊',
|
||||||
|
prompt: `請生成崗位描述資料(基本版)。
|
||||||
|
|
||||||
|
已填寫的資料:{existingData}
|
||||||
|
需要生成的欄位:positionName, department, positionPurpose
|
||||||
|
|
||||||
|
請用繁體中文,返回 JSON 格式,不要有任何其他文字。`
|
||||||
|
},
|
||||||
|
bag2: {
|
||||||
|
title: '標準版',
|
||||||
|
subtitle: '含職責說明',
|
||||||
|
prompt: `請生成崗位描述資料(標準版)。
|
||||||
|
|
||||||
|
已填寫的資料:{existingData}
|
||||||
|
需要生成的欄位:positionName, department, directSupervisor, positionPurpose, mainResponsibilities, education, basicSkills
|
||||||
|
|
||||||
|
欄位說明:
|
||||||
|
- positionName: 崗位名稱
|
||||||
|
- department: 所屬部門
|
||||||
|
- directSupervisor: 直屬主管
|
||||||
|
- positionPurpose: 崗位目的(簡短描述)
|
||||||
|
- mainResponsibilities: 主要職責(條列式,用換行分隔)
|
||||||
|
- education: 學歷要求
|
||||||
|
- basicSkills: 基本技能(條列式,用換行分隔)
|
||||||
|
|
||||||
|
請用繁體中文,返回 JSON 格式,不要有任何其他文字。`
|
||||||
|
},
|
||||||
|
bag3: {
|
||||||
|
title: '完整版',
|
||||||
|
subtitle: '33個欄位',
|
||||||
|
prompt: `請生成完整的崗位描述資料。
|
||||||
|
|
||||||
|
已填寫的資料:{existingData}
|
||||||
|
需要生成所有空白欄位。
|
||||||
|
|
||||||
|
包含以下區塊:
|
||||||
|
1. 基本資訊:empNo, empName, positionCode, versionDate
|
||||||
|
2. 崗位資訊:positionName, department, positionEffectiveDate, directSupervisor, positionGradeJob, reportTo, directReports, workLocation, empAttribute
|
||||||
|
3. 職責與目的:positionPurpose, mainResponsibilities
|
||||||
|
4. 任職要求:education, basicSkills, professionalKnowledge, workExperienceReq, otherRequirements
|
||||||
|
|
||||||
|
請用繁體中文,返回 JSON 格式,不要有任何其他文字。`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================== LocalStorage 管理 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 獲取模組的 prompts(優先使用自定義,否則使用預設)
|
||||||
|
* @param {string} module - 模組名稱
|
||||||
|
* @returns {Object} - 包含三個 bag 的 prompt 物件
|
||||||
|
*/
|
||||||
|
function getModulePrompts(module) {
|
||||||
|
try {
|
||||||
|
const saved = localStorage.getItem('aiPrompts');
|
||||||
|
const prompts = saved ? JSON.parse(saved) : {};
|
||||||
|
|
||||||
|
// 如果沒有保存的 prompts,使用預設值
|
||||||
|
if (!prompts[module]) {
|
||||||
|
return DEFAULT_PROMPTS[module] || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return prompts[module];
|
||||||
|
} catch (e) {
|
||||||
|
console.error('讀取 prompts 失敗:', e);
|
||||||
|
return DEFAULT_PROMPTS[module] || {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存模組的 prompts
|
||||||
|
* @param {string} module - 模組名稱
|
||||||
|
* @param {number} bagNumber - 錦囊編號 (1-3)
|
||||||
|
* @param {Object} bagData - 包含 title, subtitle, prompt 的物件
|
||||||
|
*/
|
||||||
|
function saveModulePrompt(module, bagNumber, bagData) {
|
||||||
|
try {
|
||||||
|
const saved = localStorage.getItem('aiPrompts');
|
||||||
|
const prompts = saved ? JSON.parse(saved) : {};
|
||||||
|
|
||||||
|
if (!prompts[module]) {
|
||||||
|
prompts[module] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
prompts[module][`bag${bagNumber}`] = bagData;
|
||||||
|
localStorage.setItem('aiPrompts', JSON.stringify(prompts));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('保存 prompt 失敗:', e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化所有模組的預設 prompts(如果尚未設定)
|
||||||
|
*/
|
||||||
|
function initializeDefaultPrompts() {
|
||||||
|
const saved = localStorage.getItem('aiPrompts');
|
||||||
|
if (!saved) {
|
||||||
|
localStorage.setItem('aiPrompts', JSON.stringify(DEFAULT_PROMPTS));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 執行 AI 錦囊 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 執行 AI 錦囊
|
||||||
|
* @param {string} module - 模組名稱
|
||||||
|
* @param {number} bagNumber - 錦囊編號 (1-3)
|
||||||
|
*/
|
||||||
|
export async function executeAIBag(module, bagNumber) {
|
||||||
|
const bagElement = document.querySelector(`.ai-bag[data-module="${module}"][data-bag="${bagNumber}"]`);
|
||||||
|
if (!bagElement) return;
|
||||||
|
|
||||||
|
// 防止重複點擊
|
||||||
|
if (bagElement.classList.contains('loading')) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 顯示載入狀態
|
||||||
|
bagElement.classList.add('loading');
|
||||||
|
const icon = bagElement.querySelector('.bag-icon');
|
||||||
|
const originalIcon = icon.textContent;
|
||||||
|
icon.innerHTML = '<div class="spinner"></div>';
|
||||||
|
|
||||||
|
// 獲取當前表單資料
|
||||||
|
let existingData = {};
|
||||||
|
if (module === 'positionBasic' || module === 'positionRecruit') {
|
||||||
|
const positionData = getPositionFormData();
|
||||||
|
existingData = module === 'positionBasic' ? positionData.basicInfo : positionData.recruitInfo;
|
||||||
|
} else if (module === 'jobBasic') {
|
||||||
|
existingData = getJobFormData();
|
||||||
|
} else if (module === 'jobDesc') {
|
||||||
|
existingData = getJobDescFormData();
|
||||||
|
} else if (module === 'deptFunction') {
|
||||||
|
existingData = getDeptFunctionFormData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 獲取 prompt 模板
|
||||||
|
const prompts = getModulePrompts(module);
|
||||||
|
const bagPrompt = prompts[`bag${bagNumber}`];
|
||||||
|
|
||||||
|
if (!bagPrompt || !bagPrompt.prompt) {
|
||||||
|
throw new Error('Prompt 模板不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 替換 prompt 中的變數
|
||||||
|
let finalPrompt = bagPrompt.prompt
|
||||||
|
.replace('{existingData}', JSON.stringify(existingData, null, 2))
|
||||||
|
.replace('{positionName}', existingData.positionName || '此崗位');
|
||||||
|
|
||||||
|
// 調用 AI API
|
||||||
|
const result = await callClaudeAPI(finalPrompt);
|
||||||
|
|
||||||
|
// 填充表單
|
||||||
|
fillFormWithAIResult(module, result);
|
||||||
|
|
||||||
|
showToast('✨ AI 生成成功!');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('AI 錦囊執行失敗:', error);
|
||||||
|
showToast('❌ AI 生成失敗: ' + error.message);
|
||||||
|
} finally {
|
||||||
|
// 恢復正常狀態
|
||||||
|
bagElement.classList.remove('loading');
|
||||||
|
const icon = bagElement.querySelector('.bag-icon');
|
||||||
|
icon.textContent = '🎁';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根據 AI 結果填充表單
|
||||||
|
* @param {string} module - 模組名稱
|
||||||
|
* @param {Object} result - AI 返回的 JSON 結果
|
||||||
|
*/
|
||||||
|
function fillFormWithAIResult(module, result) {
|
||||||
|
if (module === 'positionBasic') {
|
||||||
|
// 崗位基礎資料 - 基礎資料頁籤
|
||||||
|
Object.entries(result).forEach(([key, value]) => {
|
||||||
|
fillIfEmpty(key, value);
|
||||||
|
});
|
||||||
|
} else if (module === 'positionRecruit') {
|
||||||
|
// 崗位基礎資料 - 招聘要求頁籤
|
||||||
|
Object.entries(result).forEach(([key, value]) => {
|
||||||
|
fillIfEmpty(key, value);
|
||||||
|
});
|
||||||
|
} else if (module === 'jobBasic') {
|
||||||
|
// 職務基礎資料
|
||||||
|
Object.entries(result).forEach(([key, value]) => {
|
||||||
|
if (key === 'hasAttendanceBonus' || key === 'hasHousingAllowance') {
|
||||||
|
const checkbox = document.getElementById(key);
|
||||||
|
if (checkbox) checkbox.checked = value;
|
||||||
|
} else {
|
||||||
|
fillIfEmpty(key, value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (module === 'deptFunction') {
|
||||||
|
// 部門職責
|
||||||
|
Object.entries(result).forEach(([key, value]) => {
|
||||||
|
fillIfEmpty('df_' + key, value);
|
||||||
|
});
|
||||||
|
} else if (module === 'jobDesc') {
|
||||||
|
// 崗位描述
|
||||||
|
if (result.basicInfo) {
|
||||||
|
Object.entries(result.basicInfo).forEach(([key, value]) => {
|
||||||
|
fillIfEmpty('jd_' + key, value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (result.positionInfo) {
|
||||||
|
Object.entries(result.positionInfo).forEach(([key, value]) => {
|
||||||
|
fillIfEmpty('jd_' + key, value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (result.responsibilities) {
|
||||||
|
Object.entries(result.responsibilities).forEach(([key, value]) => {
|
||||||
|
fillIfEmpty('jd_' + key, value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (result.requirements) {
|
||||||
|
Object.entries(result.requirements).forEach(([key, value]) => {
|
||||||
|
fillIfEmpty('jd_' + key, value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 編輯 Prompt ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 編輯錦囊 Prompt
|
||||||
|
* @param {Event} event - 點擊事件
|
||||||
|
* @param {string} module - 模組名稱
|
||||||
|
* @param {number} bagNumber - 錦囊編號 (1-3)
|
||||||
|
*/
|
||||||
|
export function editBagPrompt(event, module, bagNumber) {
|
||||||
|
event.stopPropagation(); // 防止觸發父元素的 click 事件
|
||||||
|
|
||||||
|
const prompts = getModulePrompts(module);
|
||||||
|
const bagPrompt = prompts[`bag${bagNumber}`];
|
||||||
|
|
||||||
|
if (!bagPrompt) {
|
||||||
|
showToast('❌ Prompt 不存在');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 顯示編輯對話框
|
||||||
|
showPromptEditModal(module, bagNumber, bagPrompt);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 顯示 Prompt 編輯對話框
|
||||||
|
* @param {string} module - 模組名稱
|
||||||
|
* @param {number} bagNumber - 錦囊編號 (1-3)
|
||||||
|
* @param {Object} bagData - 包含 title, subtitle, prompt 的物件
|
||||||
|
*/
|
||||||
|
function showPromptEditModal(module, bagNumber, bagData) {
|
||||||
|
const modal = document.getElementById('promptEditModal');
|
||||||
|
if (!modal) {
|
||||||
|
console.error('找不到編輯對話框');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 填充對話框內容
|
||||||
|
document.getElementById('promptModalTitle').textContent = `編輯錦囊 ${bagNumber} - ${bagData.title}`;
|
||||||
|
document.getElementById('promptTitle').value = bagData.title || '';
|
||||||
|
document.getElementById('promptSubtitle').value = bagData.subtitle || '';
|
||||||
|
document.getElementById('promptContent').value = bagData.prompt || '';
|
||||||
|
|
||||||
|
// 保存當前編輯的模組和錦囊編號
|
||||||
|
modal.dataset.module = module;
|
||||||
|
modal.dataset.bagNumber = bagNumber;
|
||||||
|
|
||||||
|
// 顯示對話框
|
||||||
|
modal.classList.add('show');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存編輯的 Prompt
|
||||||
|
*/
|
||||||
|
export function savePromptEdit() {
|
||||||
|
const modal = document.getElementById('promptEditModal');
|
||||||
|
const module = modal.dataset.module;
|
||||||
|
const bagNumber = parseInt(modal.dataset.bagNumber);
|
||||||
|
|
||||||
|
const title = document.getElementById('promptTitle').value.trim();
|
||||||
|
const subtitle = document.getElementById('promptSubtitle').value.trim();
|
||||||
|
const prompt = document.getElementById('promptContent').value.trim();
|
||||||
|
|
||||||
|
if (!title || !prompt) {
|
||||||
|
showToast('⚠️ 標題和 Prompt 內容不能為空');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存到 LocalStorage
|
||||||
|
const success = saveModulePrompt(module, bagNumber, { title, subtitle, prompt });
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
// 更新頁面上的錦囊標題
|
||||||
|
const bagElement = document.querySelector(`.ai-bag[data-module="${module}"][data-bag="${bagNumber}"]`);
|
||||||
|
if (bagElement) {
|
||||||
|
const titleElement = bagElement.querySelector('.bag-title');
|
||||||
|
const subtitleElement = bagElement.querySelector('.bag-subtitle');
|
||||||
|
if (titleElement) titleElement.textContent = title;
|
||||||
|
if (subtitleElement) {
|
||||||
|
if (subtitle) {
|
||||||
|
if (!subtitleElement.classList) {
|
||||||
|
const newSubtitle = document.createElement('div');
|
||||||
|
newSubtitle.className = 'bag-subtitle';
|
||||||
|
newSubtitle.textContent = subtitle;
|
||||||
|
bagElement.querySelector('.bag-title').after(newSubtitle);
|
||||||
|
} else {
|
||||||
|
subtitleElement.textContent = subtitle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
closePromptEditModal();
|
||||||
|
showToast('✅ Prompt 已保存');
|
||||||
|
} else {
|
||||||
|
showToast('❌ 保存失敗');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 關閉編輯對話框
|
||||||
|
*/
|
||||||
|
export function closePromptEditModal() {
|
||||||
|
const modal = document.getElementById('promptEditModal');
|
||||||
|
if (modal) {
|
||||||
|
modal.classList.remove('show');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置為預設 Prompt
|
||||||
|
*/
|
||||||
|
export function resetToDefaultPrompt() {
|
||||||
|
const modal = document.getElementById('promptEditModal');
|
||||||
|
const module = modal.dataset.module;
|
||||||
|
const bagNumber = parseInt(modal.dataset.bagNumber);
|
||||||
|
|
||||||
|
const defaultPrompt = DEFAULT_PROMPTS[module]?.[`bag${bagNumber}`];
|
||||||
|
|
||||||
|
if (!defaultPrompt) {
|
||||||
|
showToast('❌ 找不到預設 Prompt');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (confirm('確定要重置為預設 Prompt 嗎?')) {
|
||||||
|
document.getElementById('promptTitle').value = defaultPrompt.title || '';
|
||||||
|
document.getElementById('promptSubtitle').value = defaultPrompt.subtitle || '';
|
||||||
|
document.getElementById('promptContent').value = defaultPrompt.prompt || '';
|
||||||
|
|
||||||
|
showToast('✅ 已重置為預設值');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 初始化 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化錦囊標題
|
||||||
|
*/
|
||||||
|
export function initializeBagTitles() {
|
||||||
|
document.querySelectorAll('.ai-bag').forEach(bag => {
|
||||||
|
const module = bag.dataset.module;
|
||||||
|
const bagNumber = parseInt(bag.dataset.bag);
|
||||||
|
|
||||||
|
const prompts = getModulePrompts(module);
|
||||||
|
const bagPrompt = prompts[`bag${bagNumber}`];
|
||||||
|
|
||||||
|
if (bagPrompt) {
|
||||||
|
const titleElement = bag.querySelector('.bag-title');
|
||||||
|
const subtitleElement = bag.querySelector('.bag-subtitle');
|
||||||
|
|
||||||
|
if (titleElement) titleElement.textContent = bagPrompt.title || `錦囊${bagNumber}`;
|
||||||
|
if (subtitleElement && bagPrompt.subtitle) {
|
||||||
|
subtitleElement.textContent = bagPrompt.subtitle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 頁面載入時初始化
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
initializeDefaultPrompts();
|
||||||
|
initializeBagTitles();
|
||||||
|
});
|
||||||
|
|
||||||
|
// ==================== 掛載到 window ====================
|
||||||
|
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.executeAIBag = executeAIBag;
|
||||||
|
window.editBagPrompt = editBagPrompt;
|
||||||
|
window.savePromptEdit = savePromptEdit;
|
||||||
|
window.closePromptEditModal = closePromptEditModal;
|
||||||
|
window.resetToDefaultPrompt = resetToDefaultPrompt;
|
||||||
|
}
|
||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
308
js/api.js
Normal file
308
js/api.js
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
/**
|
||||||
|
* API - API 呼叫函式
|
||||||
|
* 包含所有與後端 API 的通訊邏輯
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { API_BASE_URL } from './config.js';
|
||||||
|
import { showCopyableError } from './utils.js';
|
||||||
|
|
||||||
|
// ==================== LLM API ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 獲取當前選擇的 Ollama 模型
|
||||||
|
* @returns {string} - 模型名稱
|
||||||
|
*/
|
||||||
|
function getOllamaModel() {
|
||||||
|
return localStorage.getItem('selectedOllamaModel') || 'deepseek-r1:8b';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 調用 Claude/Ollama LLM API
|
||||||
|
* @param {string} prompt - Prompt 內容
|
||||||
|
* @param {string} api - API 類型('ollama' 或其他)
|
||||||
|
* @returns {Promise<Object>} - 解析後的 JSON 回應
|
||||||
|
*/
|
||||||
|
export async function callClaudeAPI(prompt, api = 'ollama') {
|
||||||
|
try {
|
||||||
|
// 準備請求資料
|
||||||
|
const requestData = {
|
||||||
|
api: api,
|
||||||
|
prompt: prompt,
|
||||||
|
max_tokens: 2000
|
||||||
|
};
|
||||||
|
|
||||||
|
// 如果使用 Ollama API,加入選擇的模型
|
||||||
|
if (api === 'ollama') {
|
||||||
|
requestData.model = getOllamaModel();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 調用後端 Flask API,避免 CORS 錯誤
|
||||||
|
const response = await fetch(`${API_BASE_URL}/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 調用失敗');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理 JSON 代碼塊標記和其他格式
|
||||||
|
let responseText = data.text;
|
||||||
|
|
||||||
|
// 移除 DeepSeek-R1 等模型的思考標籤 <think>...</think>
|
||||||
|
responseText = responseText.replace(/<think>[\s\S]*?<\/think>/gi, '');
|
||||||
|
|
||||||
|
// 移除 Markdown 代碼塊標記
|
||||||
|
responseText = responseText.replace(/```json\n?/gi, '').replace(/```\n?/g, '').trim();
|
||||||
|
|
||||||
|
// 移除可能的前導文字(如 "Here is the JSON:" 等)
|
||||||
|
responseText = responseText.replace(/^[\s\S]*?(?=\{)/i, '');
|
||||||
|
|
||||||
|
// 嘗試提取 JSON 內容(如果包含其他文字)
|
||||||
|
// 查找第一個 { 和最後一個 }
|
||||||
|
const firstBrace = responseText.indexOf('{');
|
||||||
|
const lastBrace = responseText.lastIndexOf('}');
|
||||||
|
|
||||||
|
if (firstBrace !== -1 && lastBrace !== -1 && lastBrace > firstBrace) {
|
||||||
|
responseText = responseText.substring(firstBrace, lastBrace + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 嘗試解析 JSON
|
||||||
|
try {
|
||||||
|
return JSON.parse(responseText);
|
||||||
|
} catch (parseError) {
|
||||||
|
// JSON 解析失敗,拋出更詳細的錯誤
|
||||||
|
console.error('JSON 解析失敗,原始響應:', responseText);
|
||||||
|
|
||||||
|
// 嘗試修復常見的 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) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Position API ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存崗位至崗位清單
|
||||||
|
* @param {Object} positionData - 崗位資料
|
||||||
|
* @returns {Promise<Object>} - API 回應
|
||||||
|
*/
|
||||||
|
export async function savePositionToList(positionData) {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/positions`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(positionData)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
throw new Error(errorData.error || '保存失敗');
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 載入崗位清單
|
||||||
|
* @returns {Promise<Array>} - 崗位清單
|
||||||
|
*/
|
||||||
|
export async function loadPositionList() {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/position-list`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('載入崗位清單失敗');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return data.data || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 獲取單一崗位描述
|
||||||
|
* @param {string} positionCode - 崗位編號
|
||||||
|
* @returns {Promise<Object>} - 崗位描述資料
|
||||||
|
*/
|
||||||
|
export async function getPositionDescription(positionCode) {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/position-descriptions/${positionCode}`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status === 404) {
|
||||||
|
return null; // 未找到描述
|
||||||
|
}
|
||||||
|
throw new Error('載入崗位描述失敗');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return data.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存崗位描述
|
||||||
|
* @param {Object} descData - 崗位描述資料
|
||||||
|
* @returns {Promise<Object>} - API 回應
|
||||||
|
*/
|
||||||
|
export async function savePositionDescription(descData) {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/position-descriptions`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(descData)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
throw new Error(errorData.error || '保存崗位描述失敗');
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== CSV API ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下載崗位 CSV 範本
|
||||||
|
*/
|
||||||
|
export async function downloadPositionCSVTemplate() {
|
||||||
|
window.location.href = `${API_BASE_URL}/positions/csv-template`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下載職務 CSV 範本
|
||||||
|
*/
|
||||||
|
export async function downloadJobCSVTemplate() {
|
||||||
|
window.location.href = `${API_BASE_URL}/jobs/csv-template`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 匯入崗位 CSV
|
||||||
|
* @param {File} file - CSV 檔案
|
||||||
|
* @returns {Promise<Object>} - API 回應
|
||||||
|
*/
|
||||||
|
export async function importPositionsCSV(file) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
|
||||||
|
const response = await fetch(`${API_BASE_URL}/positions/import-csv`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
throw new Error(errorData.error || 'CSV 匯入失敗');
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 匯入職務 CSV
|
||||||
|
* @param {File} file - CSV 檔案
|
||||||
|
* @returns {Promise<Object>} - API 回應
|
||||||
|
*/
|
||||||
|
export async function importJobsCSV(file) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
|
||||||
|
const response = await fetch(`${API_BASE_URL}/jobs/import-csv`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
throw new Error(errorData.error || 'CSV 匯入失敗');
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 匯出完整崗位資料
|
||||||
|
*/
|
||||||
|
export async function exportCompletePositionData() {
|
||||||
|
window.location.href = `${API_BASE_URL}/position-list/export`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Ollama Connection Test ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 測試 Ollama 連線
|
||||||
|
* @returns {Promise<boolean>} - 連線是否成功
|
||||||
|
*/
|
||||||
|
export async function testOllamaConnection() {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/llm/generate`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
api: 'ollama',
|
||||||
|
model: getOllamaModel(),
|
||||||
|
prompt: '請回答:「連線測試成功」',
|
||||||
|
max_tokens: 50
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return data.success === true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ollama 連線測試失敗:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
82
js/config.js
Normal file
82
js/config.js
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
/**
|
||||||
|
* Configuration - 設定檔
|
||||||
|
* 包含 API 端點、常數定義、資料對應表
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ==================== API Configuration ====================
|
||||||
|
export const API_BASE_URL = '/api';
|
||||||
|
|
||||||
|
// ==================== 下拉選單資料 (從 Excel 提取) ====================
|
||||||
|
|
||||||
|
// 事業體
|
||||||
|
export const businessUnits = [
|
||||||
|
'半導體事業群', '汽車事業體', '法務室', '岡山製造事業體', '產品事業體',
|
||||||
|
'晶圓三廠', '集團人資行政事業體', '集團財務事業體', '集團會計事業體',
|
||||||
|
'集團資訊事業體', '新創事業體', '稽核室', '總經理室', '總品質事業體', '營業事業體'
|
||||||
|
];
|
||||||
|
|
||||||
|
// 處級單位
|
||||||
|
export const deptLevel1Units = [
|
||||||
|
'半導體事業群', '汽車事業體', '法務室', '生產處', '岡山製造事業體', '封裝工程處',
|
||||||
|
'副總辦公室', '測試工程與研發處', '資材處', '廠務與環安衛管理處', '產品事業體',
|
||||||
|
'先進產品事業處', '成熟產品事業處', '晶圓三廠', '製程工程處', '集團人資行政事業體',
|
||||||
|
'集團財務事業體', '岡山強茂財務處', '集團會計事業體', '岡山會計處', '集團會計處',
|
||||||
|
'集團資訊事業體', '資安行動小組', '資訊一處', '資訊二處', '新創事業體',
|
||||||
|
'中低壓產品研發處', '研發中心', '高壓產品研發處', '稽核室', '總經理室',
|
||||||
|
'ESG專案辦公室', '專案管理室', '總品質事業體', '營業事業體', '商業開發暨市場應用處',
|
||||||
|
'海外銷售事業處', '全球技術服務處', '全球行銷暨業務支援處', '大中華區銷售事業處'
|
||||||
|
];
|
||||||
|
|
||||||
|
// 部級單位
|
||||||
|
export const deptLevel2Units = [
|
||||||
|
'生產部', '生產企劃部', '岡山品質管制部', '製程工程一部', '製程工程二部', '設備一部',
|
||||||
|
'設備二部', '工業工程部', '測試工程部', '新產品導入部', '研發部', '採購部',
|
||||||
|
'外部資源部', '生管部', '原物料控制部', '廠務部', '產品管理部(APD)', '產品管理部(MPD)',
|
||||||
|
'品質部', '製造部', '廠務部(Fab3)', '工程一部', '工程二部', '工程三部',
|
||||||
|
'製程整合部(Fab3)', '行政總務管理部', '招募任用部', '訓練發展部', '薪酬管理部',
|
||||||
|
'岡山強茂財務部', '會計部', '管理會計部', '集團合併報表部', '應用系統部',
|
||||||
|
'電腦整合製造部', '系統網路服務部', '資源管理部', '客戶品質管理部', '產品品質管理部',
|
||||||
|
'品質系統及客戶工程整合部', '封測外包品質管理部', '品質保證部', '日本區暨代工業務部',
|
||||||
|
'歐亞區業務部', '韓國區業務部-韓國區', '美洲區業務部', '應用工程部(GTS)', '系統工程部',
|
||||||
|
'特性測試部', '業務生管部', '市場行銷企劃部', 'MOSFET晶圓採購部', '台灣區業務部',
|
||||||
|
'業務一部', '業務二部'
|
||||||
|
];
|
||||||
|
|
||||||
|
// 崗位名稱
|
||||||
|
export const positionNames = [
|
||||||
|
'營運長', '營運長助理', '副總經理', '專案經理', '經副理', '法務專員', '專利工程師',
|
||||||
|
'處長', '專員', '課長', '組長', '班長', '副班長', '作業員', '工程師', '副總經理助理',
|
||||||
|
'副理', '專案經副理', '顧問', '人資長', '助理', '財務長', '專案副理', '會計長',
|
||||||
|
'資訊長', '主任', '總裁', '總經理', '專員/工程師', '經理', '技術經副理', '處長/資深經理'
|
||||||
|
];
|
||||||
|
|
||||||
|
// ==================== 資料對應表 ====================
|
||||||
|
|
||||||
|
// 崗位類別代碼對應中文名稱
|
||||||
|
export const categoryMap = {
|
||||||
|
'01': '技術職',
|
||||||
|
'02': '管理職',
|
||||||
|
'03': '業務職',
|
||||||
|
'04': '行政職'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 崗位性質代碼對應中文名稱
|
||||||
|
export const natureMap = {
|
||||||
|
'FT': '全職',
|
||||||
|
'PT': '兼職',
|
||||||
|
'CT': '約聘',
|
||||||
|
'IN': '實習'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 職務類別代碼對應中文名稱
|
||||||
|
export const jobCategoryMap = {
|
||||||
|
'MGR': '管理職',
|
||||||
|
'TECH': '技術職',
|
||||||
|
'SALE': '業務職',
|
||||||
|
'ADMIN': '行政職',
|
||||||
|
'RD': '研發職',
|
||||||
|
'PROD': '生產職'
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================== Toast 持續時間 ====================
|
||||||
|
export const TOAST_DURATION = 3000;
|
||||||
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;
|
||||||
|
}
|
||||||
206
js/main.js
Normal file
206
js/main.js
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
/**
|
||||||
|
* Main - 主程式
|
||||||
|
* 初始化應用程式,設定事件監聽器
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { showToast } from './utils.js';
|
||||||
|
import { switchModule, updatePreview, updateCategoryName, updateNatureName, updateJobCategoryName } from './ui.js';
|
||||||
|
import { initializeBagTitles } from './ai-bags.js';
|
||||||
|
|
||||||
|
// ==================== 初始化 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 載入用戶信息
|
||||||
|
*/
|
||||||
|
function loadUserInfo() {
|
||||||
|
const currentUser = localStorage.getItem('currentUser');
|
||||||
|
if (!currentUser) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const userData = JSON.parse(currentUser);
|
||||||
|
const userName = document.getElementById('userName');
|
||||||
|
const userRole = document.getElementById('userRole');
|
||||||
|
const userAvatar = document.getElementById('userAvatar');
|
||||||
|
|
||||||
|
if (userName) userName.textContent = userData.name || '使用者';
|
||||||
|
if (userRole) userRole.textContent = userData.role || '一般使用者';
|
||||||
|
if (userAvatar) userAvatar.textContent = (userData.name || 'U').charAt(0).toUpperCase();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('解析用戶資料失敗:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登出功能
|
||||||
|
*/
|
||||||
|
function logout() {
|
||||||
|
if (confirm('確定要登出嗎?')) {
|
||||||
|
localStorage.removeItem('currentUser');
|
||||||
|
window.location.href = 'login.html';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 事件監聽器設置 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 設置模組切換事件
|
||||||
|
*/
|
||||||
|
function setupModuleSwitching() {
|
||||||
|
document.querySelectorAll('.module-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
const moduleName = btn.dataset.module;
|
||||||
|
if (moduleName) {
|
||||||
|
switchModule(moduleName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 設置標籤頁切換事件
|
||||||
|
*/
|
||||||
|
function setupTabSwitching() {
|
||||||
|
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
const parent = btn.closest('.form-card');
|
||||||
|
if (!parent) return;
|
||||||
|
|
||||||
|
parent.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
||||||
|
parent.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
|
||||||
|
|
||||||
|
btn.classList.add('active');
|
||||||
|
const targetTab = document.getElementById('tab-' + btn.dataset.tab);
|
||||||
|
if (targetTab) {
|
||||||
|
targetTab.classList.add('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 設置表單欄位監聽
|
||||||
|
*/
|
||||||
|
function setupFormListeners() {
|
||||||
|
// 監聽所有表單欄位變更,更新預覽
|
||||||
|
document.querySelectorAll('input, select, textarea').forEach(field => {
|
||||||
|
field.addEventListener('change', updatePreview);
|
||||||
|
field.addEventListener('input', updatePreview);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 崗位類別變更 (pos_category)
|
||||||
|
const positionCategory = document.getElementById('pos_category');
|
||||||
|
if (positionCategory) {
|
||||||
|
positionCategory.addEventListener('change', updateCategoryName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 崗位性質變更 (pos_type)
|
||||||
|
const positionNature = document.getElementById('pos_type');
|
||||||
|
if (positionNature) {
|
||||||
|
positionNature.addEventListener('change', updateNatureName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 職務類別變更 (job_category)
|
||||||
|
const jobCategoryCode = document.getElementById('job_category');
|
||||||
|
if (jobCategoryCode) {
|
||||||
|
jobCategoryCode.addEventListener('change', updateJobCategoryName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle 開關變更 (job_hasAttBonus)
|
||||||
|
const hasAttendanceBonus = document.getElementById('job_hasAttBonus');
|
||||||
|
if (hasAttendanceBonus) {
|
||||||
|
hasAttendanceBonus.addEventListener('change', function() {
|
||||||
|
const label = document.getElementById('attendanceLabel');
|
||||||
|
if (label) {
|
||||||
|
label.textContent = this.checked ? '是' : '否';
|
||||||
|
}
|
||||||
|
updatePreview();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle 開關變更 (job_hasHouseAllow)
|
||||||
|
const hasHousingAllowance = document.getElementById('job_hasHouseAllow');
|
||||||
|
if (hasHousingAllowance) {
|
||||||
|
hasHousingAllowance.addEventListener('change', function() {
|
||||||
|
const label = document.getElementById('housingLabel');
|
||||||
|
if (label) {
|
||||||
|
label.textContent = this.checked ? '是' : '否';
|
||||||
|
}
|
||||||
|
updatePreview();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 設置快捷鍵
|
||||||
|
*/
|
||||||
|
function setupKeyboardShortcuts() {
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
// Ctrl+S 或 Cmd+S: 保存當前模組
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
|
||||||
|
e.preventDefault();
|
||||||
|
const activeModule = document.querySelector('.module-btn.active');
|
||||||
|
if (!activeModule) return;
|
||||||
|
|
||||||
|
const moduleName = activeModule.dataset.module;
|
||||||
|
if (moduleName === 'position' && typeof window.savePositionAndExit === 'function') {
|
||||||
|
window.savePositionAndExit();
|
||||||
|
} else if (moduleName === 'job' && typeof window.saveJobAndExit === 'function') {
|
||||||
|
window.saveJobAndExit();
|
||||||
|
} else if (moduleName === 'jobdesc' && typeof window.saveJobDescAndExit === 'function') {
|
||||||
|
window.saveJobDescAndExit();
|
||||||
|
} else if (moduleName === 'deptfunction' && typeof window.saveDeptFunctionAndExit === 'function') {
|
||||||
|
window.saveDeptFunctionAndExit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ctrl+N 或 Cmd+N: 保存並新增下一筆
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key === 'n') {
|
||||||
|
e.preventDefault();
|
||||||
|
const activeModule = document.querySelector('.module-btn.active');
|
||||||
|
if (!activeModule) return;
|
||||||
|
|
||||||
|
const moduleName = activeModule.dataset.module;
|
||||||
|
if (moduleName === 'position' && typeof window.savePositionAndNew === 'function') {
|
||||||
|
window.savePositionAndNew();
|
||||||
|
} else if (moduleName === 'job' && typeof window.saveJobAndNew === 'function') {
|
||||||
|
window.saveJobAndNew();
|
||||||
|
} else if (moduleName === 'jobdesc' && typeof window.saveJobDescAndNew === 'function') {
|
||||||
|
window.saveJobDescAndNew();
|
||||||
|
} else if (moduleName === 'deptfunction' && typeof window.saveDeptFunctionAndNew === 'function') {
|
||||||
|
window.saveDeptFunctionAndNew();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== DOMContentLoaded 初始化 ====================
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
console.log('🚀 HR 系統初始化中...');
|
||||||
|
|
||||||
|
// 載入用戶信息
|
||||||
|
loadUserInfo();
|
||||||
|
|
||||||
|
// 設置事件監聽器
|
||||||
|
setupModuleSwitching();
|
||||||
|
setupTabSwitching();
|
||||||
|
setupFormListeners();
|
||||||
|
setupKeyboardShortcuts();
|
||||||
|
|
||||||
|
// 初始化 AI 錦囊標題
|
||||||
|
initializeBagTitles();
|
||||||
|
|
||||||
|
// 初始化預覽
|
||||||
|
updatePreview();
|
||||||
|
|
||||||
|
console.log('✅ HR 系統初始化完成');
|
||||||
|
});
|
||||||
|
|
||||||
|
// ==================== 將函式掛載到 window ====================
|
||||||
|
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.logout = logout;
|
||||||
|
window.loadUserInfo = loadUserInfo;
|
||||||
|
}
|
||||||
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 格式,不要有任何其他文字。`
|
||||||
|
};
|
||||||
391
js/ui.js
Normal file
391
js/ui.js
Normal file
@@ -0,0 +1,391 @@
|
|||||||
|
/**
|
||||||
|
* UI - UI 操作函式
|
||||||
|
* 包含模組切換、預覽更新、表單資料收集
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { showToast } from './utils.js';
|
||||||
|
import { categoryMap, natureMap, jobCategoryMap } from './config.js';
|
||||||
|
|
||||||
|
// ==================== 模組切換 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切換頁面模組
|
||||||
|
* @param {string} moduleName - 模組名稱(position/job/jobdesc/positionlist/deptfunction/admin)
|
||||||
|
*/
|
||||||
|
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' && typeof window.loadPositionList === 'function') {
|
||||||
|
window.loadPositionList();
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 表單資料收集 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 收集崗位表單資料
|
||||||
|
* @returns {Object} - 崗位資料(分為 basicInfo 和 recruitInfo)
|
||||||
|
*/
|
||||||
|
export function getPositionFormData() {
|
||||||
|
const form = document.getElementById('positionForm');
|
||||||
|
const formData = new FormData(form);
|
||||||
|
const data = { basicInfo: {}, recruitInfo: {} };
|
||||||
|
|
||||||
|
// 使用新的 pos_ prefix 欄位
|
||||||
|
const basicFieldMapping = {
|
||||||
|
'pos_code': 'positionCode',
|
||||||
|
'pos_name': 'positionName',
|
||||||
|
'pos_category': 'positionCategory',
|
||||||
|
'pos_categoryName': 'positionCategoryName',
|
||||||
|
'pos_type': 'positionType',
|
||||||
|
'pos_typeName': 'positionTypeName',
|
||||||
|
'pos_headcount': 'headcount',
|
||||||
|
'pos_level': 'positionLevel',
|
||||||
|
'pos_effectiveDate': 'effectiveDate',
|
||||||
|
'pos_desc': 'positionDesc',
|
||||||
|
'pos_remark': 'positionRemark'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 使用新的 rec_ prefix 欄位
|
||||||
|
const recruitFieldMapping = {
|
||||||
|
'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;
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.entries(recruitFieldMapping).forEach(([htmlId, dataKey]) => {
|
||||||
|
const el = document.getElementById(htmlId);
|
||||||
|
if (el && el.value) data.recruitInfo[dataKey] = el.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 收集職務表單資料
|
||||||
|
* @returns {Object} - 職務資料
|
||||||
|
*/
|
||||||
|
export function getJobFormData() {
|
||||||
|
const form = document.getElementById('jobForm');
|
||||||
|
const data = {};
|
||||||
|
|
||||||
|
// 使用新的 job_ prefix 欄位
|
||||||
|
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'
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.entries(fieldMapping).forEach(([htmlId, dataKey]) => {
|
||||||
|
const el = document.getElementById(htmlId);
|
||||||
|
if (el && el.value) data[dataKey] = el.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasAttBonus = document.getElementById('job_hasAttBonus');
|
||||||
|
const hasHouseAllow = document.getElementById('job_hasHouseAllow');
|
||||||
|
data.hasAttendanceBonus = hasAttBonus ? hasAttBonus.checked : false;
|
||||||
|
data.hasHousingAllowance = hasHouseAllow ? hasHouseAllow.checked : false;
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 收集崗位描述表單資料
|
||||||
|
* @returns {Object} - 崗位描述資料
|
||||||
|
*/
|
||||||
|
export function getJobDescFormData() {
|
||||||
|
const form = document.getElementById('jobDescForm');
|
||||||
|
if (!form) return {};
|
||||||
|
|
||||||
|
const data = { basicInfo: {}, positionInfo: {}, responsibilities: {}, requirements: {} };
|
||||||
|
|
||||||
|
// Basic Info - 使用新的 jd_ prefix
|
||||||
|
const basicMapping = {
|
||||||
|
'jd_empNo': 'empNo',
|
||||||
|
'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 - 使用新的 jd_ prefix
|
||||||
|
const posInfoMapping = {
|
||||||
|
'jd_posName': 'positionName',
|
||||||
|
'jd_department': 'department',
|
||||||
|
'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 - 使用新的 jd_ prefix
|
||||||
|
const purpose = document.getElementById('jd_purpose');
|
||||||
|
if (purpose && purpose.value) data.responsibilities.positionPurpose = purpose.value;
|
||||||
|
|
||||||
|
const mainResp = document.getElementById('jd_mainResp');
|
||||||
|
if (mainResp && mainResp.value) data.responsibilities.mainResponsibilities = mainResp.value;
|
||||||
|
|
||||||
|
// Requirements - 使用新的 jd_ prefix
|
||||||
|
const reqMapping = {
|
||||||
|
'jd_eduLevel': 'education',
|
||||||
|
'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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 收集部門職責表單資料
|
||||||
|
* @returns {Object} - 部門職責資料
|
||||||
|
*/
|
||||||
|
export function getDeptFunctionFormData() {
|
||||||
|
const form = document.getElementById('deptFunctionForm');
|
||||||
|
if (!form) return {};
|
||||||
|
|
||||||
|
const data = {};
|
||||||
|
|
||||||
|
// 使用新的 df_ prefix 欄位
|
||||||
|
const fieldMapping = {
|
||||||
|
'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'
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.entries(fieldMapping).forEach(([htmlId, dataKey]) => {
|
||||||
|
const el = document.getElementById(htmlId);
|
||||||
|
if (el && el.value) data[dataKey] = el.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 預覽更新 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新 JSON 預覽
|
||||||
|
*/
|
||||||
|
export function updatePreview() {
|
||||||
|
const activeBtn = document.querySelector('.module-btn.active');
|
||||||
|
if (!activeBtn) return;
|
||||||
|
|
||||||
|
const activeModule = activeBtn.dataset.module;
|
||||||
|
let data;
|
||||||
|
|
||||||
|
if (activeModule === 'position') {
|
||||||
|
data = { module: '崗位基礎資料', ...getPositionFormData() };
|
||||||
|
} else if (activeModule === 'job') {
|
||||||
|
data = { module: '職務基礎資料', ...getJobFormData() };
|
||||||
|
} else if (activeModule === 'jobdesc') {
|
||||||
|
data = { module: '崗位描述', ...getJobDescFormData() };
|
||||||
|
} else if (activeModule === 'deptfunction') {
|
||||||
|
data = { module: '部門職責', ...getDeptFunctionFormData() };
|
||||||
|
} else {
|
||||||
|
return; // 其他模組不顯示預覽
|
||||||
|
}
|
||||||
|
|
||||||
|
const previewEl = document.getElementById('jsonPreview');
|
||||||
|
if (previewEl) {
|
||||||
|
previewEl.textContent = JSON.stringify(data, null, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 表單邏輯輔助函式 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新崗位類別中文名稱
|
||||||
|
*/
|
||||||
|
export function updateCategoryName() {
|
||||||
|
const categoryEl = document.getElementById('pos_category');
|
||||||
|
const categoryNameEl = document.getElementById('pos_categoryName');
|
||||||
|
if (categoryEl && categoryNameEl) {
|
||||||
|
categoryNameEl.value = categoryMap[categoryEl.value] || '';
|
||||||
|
}
|
||||||
|
updatePreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新崗位性質中文名稱
|
||||||
|
*/
|
||||||
|
export function updateNatureName() {
|
||||||
|
const typeEl = document.getElementById('pos_type');
|
||||||
|
const typeNameEl = document.getElementById('pos_typeName');
|
||||||
|
if (typeEl && typeNameEl) {
|
||||||
|
typeNameEl.value = natureMap[typeEl.value] || '';
|
||||||
|
}
|
||||||
|
updatePreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新職務類別中文名稱
|
||||||
|
*/
|
||||||
|
export function updateJobCategoryName() {
|
||||||
|
const categoryEl = document.getElementById('job_category');
|
||||||
|
const categoryNameEl = document.getElementById('job_categoryName');
|
||||||
|
if (categoryEl && categoryNameEl) {
|
||||||
|
categoryNameEl.value = jobCategoryMap[categoryEl.value] || '';
|
||||||
|
}
|
||||||
|
updatePreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改崗位編號
|
||||||
|
*/
|
||||||
|
export function changePositionCode() {
|
||||||
|
const codeEl = document.getElementById('pos_code');
|
||||||
|
if (!codeEl) return;
|
||||||
|
const currentCode = codeEl.value;
|
||||||
|
const newCode = prompt('請輸入新的崗位編號:', currentCode);
|
||||||
|
if (newCode && newCode !== currentCode) {
|
||||||
|
codeEl.value = newCode;
|
||||||
|
showToast('崗位編號已更改!');
|
||||||
|
updatePreview();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改職務編號
|
||||||
|
*/
|
||||||
|
export function changeJobCode() {
|
||||||
|
const codeEl = document.getElementById('job_code');
|
||||||
|
if (!codeEl) return;
|
||||||
|
const currentCode = codeEl.value;
|
||||||
|
const newCode = prompt('請輸入新的職務編號:', currentCode);
|
||||||
|
if (newCode && newCode !== currentCode) {
|
||||||
|
codeEl.value = newCode;
|
||||||
|
showToast('職務編號已更改!');
|
||||||
|
updatePreview();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 模態框函式(待整合)====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 開啟專業科目選擇模態框
|
||||||
|
*/
|
||||||
|
export function openMajorModal() {
|
||||||
|
const modal = document.getElementById('majorModal');
|
||||||
|
if (modal) {
|
||||||
|
modal.classList.add('show');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 關閉專業科目選擇模態框
|
||||||
|
*/
|
||||||
|
export function closeMajorModal() {
|
||||||
|
const modal = document.getElementById('majorModal');
|
||||||
|
if (modal) {
|
||||||
|
modal.classList.remove('show');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 確認選擇專業科目
|
||||||
|
*/
|
||||||
|
export function confirmMajor() {
|
||||||
|
const selected = [];
|
||||||
|
document.querySelectorAll('#majorModal input[type="checkbox"]:checked').forEach(cb => {
|
||||||
|
selected.push(cb.value);
|
||||||
|
});
|
||||||
|
const majorReqEl = document.getElementById('rec_majorReq');
|
||||||
|
if (majorReqEl) {
|
||||||
|
majorReqEl.value = selected.join(', ');
|
||||||
|
}
|
||||||
|
closeMajorModal();
|
||||||
|
updatePreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 將函式掛載到 window 上以便內聯事件處理器使用
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.switchModule = switchModule;
|
||||||
|
window.updateCategoryName = updateCategoryName;
|
||||||
|
window.updateNatureName = updateNatureName;
|
||||||
|
window.updateJobCategoryName = updateJobCategoryName;
|
||||||
|
window.changePositionCode = changePositionCode;
|
||||||
|
window.changeJobCode = changeJobCode;
|
||||||
|
window.openMajorModal = openMajorModal;
|
||||||
|
window.closeMajorModal = closeMajorModal;
|
||||||
|
window.confirmMajor = confirmMajor;
|
||||||
|
window.updatePreview = updatePreview;
|
||||||
|
}
|
||||||
206
js/utils.js
Normal file
206
js/utils.js
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
/**
|
||||||
|
* Utilities - 工具函式
|
||||||
|
* 包含 XSS 防護、表單欄位工具、UI 回饋工具
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { TOAST_DURATION } from './config.js';
|
||||||
|
|
||||||
|
// ==================== XSS 防護工具 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消毒 HTML 字串,防止 XSS 攻擊
|
||||||
|
* @param {string} str - 需要消毒的字串
|
||||||
|
* @returns {string} - 安全的字串
|
||||||
|
*/
|
||||||
|
export function sanitizeHTML(str) {
|
||||||
|
if (str === null || str === undefined) return '';
|
||||||
|
const temp = document.createElement('div');
|
||||||
|
temp.textContent = str;
|
||||||
|
return temp.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安全設定元素文字內容
|
||||||
|
* @param {HTMLElement} element - 目標元素
|
||||||
|
* @param {string} text - 文字內容
|
||||||
|
*/
|
||||||
|
export function safeSetText(element, text) {
|
||||||
|
if (element) {
|
||||||
|
element.textContent = text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 表單欄位工具 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 只在欄位為空時填入值
|
||||||
|
* @param {string} elementId - DOM 元素 ID
|
||||||
|
* @param {string} value - 要填入的值
|
||||||
|
* @returns {boolean} - 是否有填入值
|
||||||
|
*/
|
||||||
|
export function fillIfEmpty(elementId, value) {
|
||||||
|
const el = document.getElementById(elementId);
|
||||||
|
if (el && !el.value.trim() && value) {
|
||||||
|
el.value = value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 獲取欄位當前值
|
||||||
|
* @param {string} elementId - DOM 元素 ID
|
||||||
|
* @returns {string} - 欄位值(已 trim)
|
||||||
|
*/
|
||||||
|
export function getFieldValue(elementId) {
|
||||||
|
const el = document.getElementById(elementId);
|
||||||
|
return el ? el.value.trim() : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 獲取空白欄位列表
|
||||||
|
* @param {string[]} fieldIds - 欄位 ID 陣列
|
||||||
|
* @returns {string[]} - 空白欄位 ID 陣列
|
||||||
|
*/
|
||||||
|
export function getEmptyFields(fieldIds) {
|
||||||
|
return fieldIds.filter(id => !getFieldValue(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== UI 回饋工具 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 顯示 Toast 提示訊息
|
||||||
|
* @param {string} message - 訊息內容
|
||||||
|
* @param {number} duration - 顯示時長(毫秒),預設 3000
|
||||||
|
*/
|
||||||
|
export function showToast(message, duration = TOAST_DURATION) {
|
||||||
|
const existingToast = document.querySelector('.toast');
|
||||||
|
if (existingToast) {
|
||||||
|
existingToast.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
const toast = document.createElement('div');
|
||||||
|
toast.className = 'toast';
|
||||||
|
toast.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>${sanitizeHTML(message)}</span>
|
||||||
|
`;
|
||||||
|
document.body.appendChild(toast);
|
||||||
|
|
||||||
|
setTimeout(() => toast.classList.add('show'), 10);
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.classList.remove('show');
|
||||||
|
setTimeout(() => toast.remove(), 300);
|
||||||
|
}, duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 設定按鈕載入狀態
|
||||||
|
* @param {HTMLElement} btn - 按鈕元素
|
||||||
|
* @param {boolean} loading - 是否載入中
|
||||||
|
*/
|
||||||
|
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>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 錯誤處理工具 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 顯示可複製的錯誤對話框
|
||||||
|
* @param {Object} options - 錯誤選項
|
||||||
|
* @param {string} options.title - 錯誤標題
|
||||||
|
* @param {string} options.message - 錯誤訊息
|
||||||
|
* @param {string} options.details - 錯誤詳情
|
||||||
|
* @param {string[]} options.suggestions - 建議列表
|
||||||
|
*/
|
||||||
|
export function showCopyableError(options) {
|
||||||
|
const { title, message, details, suggestions } = options;
|
||||||
|
|
||||||
|
// 移除舊的錯誤對話框
|
||||||
|
const existingModal = document.getElementById('errorModal');
|
||||||
|
if (existingModal) {
|
||||||
|
existingModal.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
const modalHTML = `
|
||||||
|
<div id="errorModal" class="modal-overlay show">
|
||||||
|
<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; flex-shrink: 0;">
|
||||||
|
<h3 style="font-size: 1rem;">🚨 ${sanitizeHTML(title)}</h3>
|
||||||
|
<button class="modal-close" onclick="closeErrorModal(this)" style="color: white;">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" style="overflow-y: auto; flex: 1; padding: 16px;">
|
||||||
|
<div style="margin-bottom: 12px;">
|
||||||
|
<strong style="font-size: 0.9rem;">錯誤訊息:</strong>
|
||||||
|
<p style="color: #e74c3c; font-weight: 500; font-size: 0.85rem; word-break: break-word;">${sanitizeHTML(message)}</p>
|
||||||
|
</div>
|
||||||
|
${details ? `
|
||||||
|
<div style="margin-bottom: 12px;">
|
||||||
|
<strong style="font-size: 0.9rem;">詳細資訊:</strong>
|
||||||
|
<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>
|
||||||
|
` : ''}
|
||||||
|
${suggestions && suggestions.length > 0 ? `
|
||||||
|
<div>
|
||||||
|
<strong style="font-size: 0.9rem;">請檢查以下項目:</strong>
|
||||||
|
<ul style="margin: 6px 0; padding-left: 18px; font-size: 0.85rem;">
|
||||||
|
${suggestions.map(s => `<li style="margin: 3px 0;">${sanitizeHTML(s)}</li>`).join('')}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer" style="flex-shrink: 0; padding: 12px 16px;">
|
||||||
|
<button class="btn btn-secondary" onclick="copyErrorDetails()" style="font-size: 0.85rem; padding: 8px 12px;">
|
||||||
|
<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"/>
|
||||||
|
</svg>
|
||||||
|
複製
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-primary" onclick="closeErrorModal(this)" style="font-size: 0.85rem; padding: 8px 16px;">關閉</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.insertAdjacentHTML('beforeend', modalHTML);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 關閉錯誤對話框
|
||||||
|
* @param {HTMLElement} button - 按鈕元素
|
||||||
|
*/
|
||||||
|
export function closeErrorModal(button) {
|
||||||
|
const modal = button.closest('.modal-overlay');
|
||||||
|
if (modal) {
|
||||||
|
modal.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 複製錯誤詳情到剪貼板
|
||||||
|
*/
|
||||||
|
export function copyErrorDetails() {
|
||||||
|
const modal = document.getElementById('errorModal');
|
||||||
|
if (!modal) return;
|
||||||
|
|
||||||
|
const errorText = modal.querySelector('.modal-body').textContent;
|
||||||
|
navigator.clipboard.writeText(errorText).then(() => {
|
||||||
|
showToast('錯誤詳情已複製到剪貼板');
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('複製失敗:', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 將函式掛載到 window 上以便內聯事件處理器使用
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.closeErrorModal = closeErrorModal;
|
||||||
|
window.copyErrorDetails = copyErrorDetails;
|
||||||
|
}
|
||||||
144
llm_config.py
144
llm_config.py
@@ -7,6 +7,10 @@ import os
|
|||||||
import requests
|
import requests
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from typing import Dict, List, Tuple
|
from typing import Dict, List, Tuple
|
||||||
|
import urllib3
|
||||||
|
|
||||||
|
# Disable SSL warnings for Ollama endpoint
|
||||||
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||||
|
|
||||||
# Load environment variables
|
# Load environment variables
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
@@ -34,6 +38,20 @@ class LLMConfig:
|
|||||||
'api_key': os.getenv('OPENAI_API_KEY', ''),
|
'api_key': os.getenv('OPENAI_API_KEY', ''),
|
||||||
'endpoint': os.getenv('OPENAI_API_URL', 'https://api.openai.com/v1'),
|
'endpoint': os.getenv('OPENAI_API_URL', 'https://api.openai.com/v1'),
|
||||||
'enabled': bool(os.getenv('OPENAI_API_KEY'))
|
'enabled': bool(os.getenv('OPENAI_API_KEY'))
|
||||||
|
},
|
||||||
|
'ollama': {
|
||||||
|
'name': 'Ollama',
|
||||||
|
'api_key': '', # Ollama 不需要 API Key
|
||||||
|
'endpoint': os.getenv('OLLAMA_API_URL', 'https://ollama_pjapi.theaken.com'),
|
||||||
|
'model': os.getenv('OLLAMA_MODEL', 'qwen2.5:3b'),
|
||||||
|
'enabled': True # Ollama 預設啟用
|
||||||
|
},
|
||||||
|
'gptoss': {
|
||||||
|
'name': 'GPT-OSS',
|
||||||
|
'api_key': '', # GPT-OSS 不需要 API Key (使用 Ollama 介面)
|
||||||
|
'endpoint': os.getenv('GPTOSS_API_URL', 'https://ollama_pjapi.theaken.com'),
|
||||||
|
'model': os.getenv('GPTOSS_MODEL', 'gpt-oss:120b'),
|
||||||
|
'enabled': True # GPT-OSS 預設啟用
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,6 +171,35 @@ class LLMConfig:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return False, f"錯誤: {str(e)}"
|
return False, f"錯誤: {str(e)}"
|
||||||
|
|
||||||
|
def test_ollama_connection(self) -> Tuple[bool, str]:
|
||||||
|
"""Test Ollama API connection"""
|
||||||
|
try:
|
||||||
|
endpoint = self.apis['ollama']['endpoint']
|
||||||
|
|
||||||
|
# Test endpoint - list models
|
||||||
|
url = f"{endpoint}/v1/models"
|
||||||
|
|
||||||
|
response = requests.get(url, timeout=10, verify=False)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
models = data.get('data', [])
|
||||||
|
if models:
|
||||||
|
model_count = len(models)
|
||||||
|
model_names = [m.get('id', '') for m in models[:3]]
|
||||||
|
return True, f"連線成功!找到 {model_count} 個可用模型 (例如: {', '.join(model_names)})"
|
||||||
|
else:
|
||||||
|
return True, "連線成功!"
|
||||||
|
else:
|
||||||
|
return False, f"連線失敗 (HTTP {response.status_code})"
|
||||||
|
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
return False, "連線逾時"
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
return False, "無法連接到伺服器"
|
||||||
|
except Exception as e:
|
||||||
|
return False, f"錯誤: {str(e)}"
|
||||||
|
|
||||||
def test_all_connections(self) -> Dict[str, Tuple[bool, str]]:
|
def test_all_connections(self) -> Dict[str, Tuple[bool, str]]:
|
||||||
"""Test all configured API connections"""
|
"""Test all configured API connections"""
|
||||||
results = {}
|
results = {}
|
||||||
@@ -166,6 +213,9 @@ class LLMConfig:
|
|||||||
if self.apis['openai']['enabled']:
|
if self.apis['openai']['enabled']:
|
||||||
results['openai'] = self.test_openai_connection()
|
results['openai'] = self.test_openai_connection()
|
||||||
|
|
||||||
|
if self.apis['ollama']['enabled']:
|
||||||
|
results['ollama'] = self.test_ollama_connection()
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
def generate_text_gemini(self, prompt: str, max_tokens: int = 2000) -> Tuple[bool, str]:
|
def generate_text_gemini(self, prompt: str, max_tokens: int = 2000) -> Tuple[bool, str]:
|
||||||
@@ -175,7 +225,9 @@ class LLMConfig:
|
|||||||
if not api_key:
|
if not api_key:
|
||||||
return False, "API Key 未設定"
|
return False, "API Key 未設定"
|
||||||
|
|
||||||
url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key={api_key}"
|
# 從環境變數讀取模型名稱,默認使用 gemini-1.5-flash
|
||||||
|
model_name = os.getenv('GEMINI_MODEL', 'gemini-1.5-flash')
|
||||||
|
url = f"https://generativelanguage.googleapis.com/v1beta/models/{model_name}:generateContent?key={api_key}"
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"contents": [
|
"contents": [
|
||||||
@@ -275,6 +327,96 @@ class LLMConfig:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return False, f"錯誤: {str(e)}"
|
return False, f"錯誤: {str(e)}"
|
||||||
|
|
||||||
|
def generate_text_ollama(self, prompt: str, max_tokens: int = 2000, model: str = None) -> Tuple[bool, str]:
|
||||||
|
"""Generate text using Ollama API
|
||||||
|
|
||||||
|
Args:
|
||||||
|
prompt: The prompt text
|
||||||
|
max_tokens: Maximum tokens to generate (not used by Ollama but kept for compatibility)
|
||||||
|
model: The model to use. If None, uses the default from config.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
endpoint = self.apis['ollama']['endpoint']
|
||||||
|
# 使用傳入的 model 參數,如果沒有則使用設定檔中的預設值
|
||||||
|
if model is None:
|
||||||
|
model = self.apis['ollama']['model']
|
||||||
|
|
||||||
|
url = f"{endpoint}/v1/chat/completions"
|
||||||
|
headers = {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"model": model,
|
||||||
|
"messages": [
|
||||||
|
{"role": "system", "content": "You are a helpful assistant."},
|
||||||
|
{"role": "user", "content": prompt}
|
||||||
|
],
|
||||||
|
"temperature": 0.7
|
||||||
|
}
|
||||||
|
|
||||||
|
# 增加逾時時間到 180 秒,處理大模型較慢的情況
|
||||||
|
response = requests.post(url, json=data, headers=headers, timeout=180, verify=False)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
result = response.json()
|
||||||
|
text = result['choices'][0]['message']['content']
|
||||||
|
return True, text
|
||||||
|
else:
|
||||||
|
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:
|
||||||
|
return False, f"錯誤: {str(e)}"
|
||||||
|
|
||||||
|
def generate_text_gptoss(self, prompt: str, max_tokens: int = 2000, model: str = None) -> Tuple[bool, str]:
|
||||||
|
"""Generate text using GPT-OSS API (120B model via Ollama interface)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
prompt: The prompt text
|
||||||
|
max_tokens: Maximum tokens to generate (not used by Ollama but kept for compatibility)
|
||||||
|
model: The model to use. If None, uses the default from config.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
endpoint = self.apis['gptoss']['endpoint']
|
||||||
|
# 使用傳入的 model 參數,如果沒有則使用設定檔中的預設值
|
||||||
|
if model is None:
|
||||||
|
model = self.apis['gptoss']['model']
|
||||||
|
|
||||||
|
url = f"{endpoint}/v1/chat/completions"
|
||||||
|
headers = {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"model": model,
|
||||||
|
"messages": [
|
||||||
|
{"role": "system", "content": "You are a helpful assistant."},
|
||||||
|
{"role": "user", "content": prompt}
|
||||||
|
],
|
||||||
|
"temperature": 0.7
|
||||||
|
}
|
||||||
|
|
||||||
|
# 增加逾時時間到 180 秒
|
||||||
|
response = requests.post(url, json=data, headers=headers, timeout=180, verify=False)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
result = response.json()
|
||||||
|
text = result['choices'][0]['message']['content']
|
||||||
|
return True, text
|
||||||
|
else:
|
||||||
|
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:
|
||||||
|
return False, f"錯誤: {str(e)}"
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""Test script"""
|
"""Test script"""
|
||||||
|
|||||||
471
login.html
Normal file
471
login.html
Normal file
@@ -0,0 +1,471 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-TW">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>那都AI寫的,不要問我 - 登入</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Microsoft JhengHei', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-container {
|
||||||
|
background: white;
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||||
|
overflow: hidden;
|
||||||
|
max-width: 450px;
|
||||||
|
width: 100%;
|
||||||
|
animation: slideIn 0.5s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-30px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-header {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
padding: 40px 30px;
|
||||||
|
text-align: center;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-container {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-container img {
|
||||||
|
width: 150px;
|
||||||
|
height: 150px;
|
||||||
|
animation: float 3s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes float {
|
||||||
|
0%, 100% {
|
||||||
|
transform: translateY(0px);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-title {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
opacity: 0.9;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-body {
|
||||||
|
padding: 40px 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 15px;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 15px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #667eea;
|
||||||
|
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 14px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
text-align: center;
|
||||||
|
margin: 30px 0 20px 0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 1px;
|
||||||
|
background: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider span {
|
||||||
|
background: white;
|
||||||
|
padding: 0 15px;
|
||||||
|
color: #999;
|
||||||
|
font-size: 14px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-login {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-login-title {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-test {
|
||||||
|
background: white;
|
||||||
|
color: #333;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-test:hover {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-color: #667eea;
|
||||||
|
color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-test .role-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 3px 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-test.user .role-badge {
|
||||||
|
background: #e3f2fd;
|
||||||
|
color: #1976d2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-test.admin .role-badge {
|
||||||
|
background: #fff3e0;
|
||||||
|
color: #f57c00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-test.superadmin .role-badge {
|
||||||
|
background: #fce4ec;
|
||||||
|
color: #c2185b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-note {
|
||||||
|
margin-top: 25px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.login-container {
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-title {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-body {
|
||||||
|
padding: 30px 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="login-container">
|
||||||
|
<div class="login-header">
|
||||||
|
<div class="logo-container">
|
||||||
|
<img src="logo.svg" alt="System Logo">
|
||||||
|
</div>
|
||||||
|
<h1 class="system-title">那都AI寫的,不要問我</h1>
|
||||||
|
<p class="system-subtitle">HR Position Management System</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="login-body">
|
||||||
|
<form id="loginForm" onsubmit="handleLogin(event)">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="username">
|
||||||
|
<svg class="icon" viewBox="0 0 24 24" style="width: 16px; height: 16px; vertical-align: middle; margin-right: 5px;">
|
||||||
|
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/>
|
||||||
|
</svg>
|
||||||
|
工號 / 帳號
|
||||||
|
</label>
|
||||||
|
<input type="text" id="username" name="username" placeholder="請輸入工號" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">
|
||||||
|
<svg class="icon" viewBox="0 0 24 24" style="width: 16px; height: 16px; vertical-align: middle; margin-right: 5px;">
|
||||||
|
<path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z"/>
|
||||||
|
</svg>
|
||||||
|
密碼
|
||||||
|
</label>
|
||||||
|
<input type="password" id="password" name="password" placeholder="請輸入密碼" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<svg class="icon" viewBox="0 0 24 24">
|
||||||
|
<path d="M10 17l5-5-5-5v10z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
登入系統
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="divider">
|
||||||
|
<span>或使用測試帳號快速登入</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="quick-login">
|
||||||
|
<p class="quick-login-title">選擇測試角色</p>
|
||||||
|
|
||||||
|
<button type="button" class="btn btn-test user" onclick="quickLogin('user')">
|
||||||
|
<svg class="icon" viewBox="0 0 24 24">
|
||||||
|
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/>
|
||||||
|
</svg>
|
||||||
|
<span class="role-badge">一般使用者</span>
|
||||||
|
<span style="flex: 1; text-align: left; margin-left: 10px; font-size: 13px; color: #666;">A003 / employee</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button type="button" class="btn btn-test admin" onclick="quickLogin('admin')">
|
||||||
|
<svg class="icon" viewBox="0 0 24 24">
|
||||||
|
<path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 10.99h7c-.53 4.12-3.28 7.79-7 8.94V12H5V6.3l7-3.11v8.8z"/>
|
||||||
|
</svg>
|
||||||
|
<span class="role-badge">管理者</span>
|
||||||
|
<span style="flex: 1; text-align: left; margin-left: 10px; font-size: 13px; color: #666;">A002 / hr_manager</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button type="button" class="btn btn-test superadmin" onclick="quickLogin('superadmin')">
|
||||||
|
<svg class="icon" viewBox="0 0 24 24">
|
||||||
|
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
|
||||||
|
</svg>
|
||||||
|
<span class="role-badge">最高管理者</span>
|
||||||
|
<span style="flex: 1; text-align: left; margin-left: 10px; font-size: 13px; color: #666;">A001 / admin</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer-note">
|
||||||
|
這個系統真的都是 AI 寫的<br>
|
||||||
|
如果有問題... 那就是 AI 的問題 ¯\_(ツ)_/¯
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// 測試帳號資料
|
||||||
|
const testAccounts = {
|
||||||
|
'user': {
|
||||||
|
username: 'A003',
|
||||||
|
password: 'employee',
|
||||||
|
role: 'user',
|
||||||
|
name: '一般員工'
|
||||||
|
},
|
||||||
|
'admin': {
|
||||||
|
username: 'A002',
|
||||||
|
password: 'hr_manager',
|
||||||
|
role: 'admin',
|
||||||
|
name: '人資主管'
|
||||||
|
},
|
||||||
|
'superadmin': {
|
||||||
|
username: 'A001',
|
||||||
|
password: 'admin',
|
||||||
|
role: 'superadmin',
|
||||||
|
name: '系統管理員'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 處理一般登入
|
||||||
|
function handleLogin(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const username = document.getElementById('username').value;
|
||||||
|
const password = document.getElementById('password').value;
|
||||||
|
|
||||||
|
// 簡單的驗證邏輯(實際應該調用後端 API)
|
||||||
|
let validUser = null;
|
||||||
|
for (const [key, account] of Object.entries(testAccounts)) {
|
||||||
|
if (account.username === username && account.password === password) {
|
||||||
|
validUser = account;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validUser) {
|
||||||
|
// 儲存登入資訊
|
||||||
|
localStorage.setItem('currentUser', JSON.stringify(validUser));
|
||||||
|
|
||||||
|
// 顯示登入成功訊息
|
||||||
|
showLoginSuccess(validUser.name, validUser.role);
|
||||||
|
|
||||||
|
// 延遲跳轉到主頁面
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.href = 'index.html';
|
||||||
|
}, 1500);
|
||||||
|
} else {
|
||||||
|
alert('帳號或密碼錯誤!\n\n提示:您可以使用下方的測試帳號快速登入');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 快速登入
|
||||||
|
function quickLogin(role) {
|
||||||
|
const account = testAccounts[role];
|
||||||
|
|
||||||
|
// 填入表單
|
||||||
|
document.getElementById('username').value = account.username;
|
||||||
|
document.getElementById('password').value = account.password;
|
||||||
|
|
||||||
|
// 儲存登入資訊
|
||||||
|
localStorage.setItem('currentUser', JSON.stringify(account));
|
||||||
|
|
||||||
|
// 顯示登入成功訊息
|
||||||
|
showLoginSuccess(account.name, account.role);
|
||||||
|
|
||||||
|
// 延遲跳轉到主頁面
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.href = 'index.html';
|
||||||
|
}, 1500);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 顯示登入成功訊息
|
||||||
|
function showLoginSuccess(name, role) {
|
||||||
|
const roleNames = {
|
||||||
|
'user': '一般使用者',
|
||||||
|
'admin': '管理者',
|
||||||
|
'superadmin': '最高管理者'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 創建成功訊息元素
|
||||||
|
const successDiv = document.createElement('div');
|
||||||
|
successDiv.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 20px 30px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
||||||
|
z-index: 9999;
|
||||||
|
animation: slideInRight 0.5s ease-out;
|
||||||
|
font-size: 16px;
|
||||||
|
`;
|
||||||
|
successDiv.innerHTML = `
|
||||||
|
<div style="display: flex; align-items: center; gap: 15px;">
|
||||||
|
<svg viewBox="0 0 24 24" style="width: 32px; height: 32px; fill: white;">
|
||||||
|
<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>
|
||||||
|
<div>
|
||||||
|
<div style="font-weight: 600; margin-bottom: 5px;">登入成功!</div>
|
||||||
|
<div style="font-size: 14px; opacity: 0.9;">${name} (${roleNames[role]})</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 添加動畫樣式
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.textContent = `
|
||||||
|
@keyframes slideInRight {
|
||||||
|
from {
|
||||||
|
transform: translateX(400px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
document.body.appendChild(successDiv);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 頁面載入時檢查是否已登入
|
||||||
|
window.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const currentUser = localStorage.getItem('currentUser');
|
||||||
|
if (currentUser) {
|
||||||
|
const user = JSON.parse(currentUser);
|
||||||
|
// 如果已經登入,詢問是否要繼續使用或重新登入
|
||||||
|
const confirm = window.confirm(`偵測到您已經以「${user.name}」身份登入。\n\n是否要繼續使用此帳號?\n(取消將重新登入)`);
|
||||||
|
if (confirm) {
|
||||||
|
window.location.href = 'index.html';
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem('currentUser');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -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.
|
2215
review.html
Normal file
2215
review.html
Normal file
File diff suppressed because one or more lines are too long
26
scripts/check_models.py
Normal file
26
scripts/check_models.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
"""
|
||||||
|
Check available models on Ollama API
|
||||||
|
"""
|
||||||
|
import requests
|
||||||
|
import urllib3
|
||||||
|
|
||||||
|
# Disable SSL warnings
|
||||||
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||||
|
|
||||||
|
API_URL = "https://ollama_pjapi.theaken.com"
|
||||||
|
|
||||||
|
print("Available models on Ollama API:")
|
||||||
|
print("-" * 60)
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.get(f"{API_URL}/v1/models", timeout=10, verify=False)
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
models = data.get('data', [])
|
||||||
|
for i, model in enumerate(models, 1):
|
||||||
|
model_id = model.get('id', 'Unknown')
|
||||||
|
print(f"{i}. {model_id}")
|
||||||
|
else:
|
||||||
|
print(f"Error: {response.status_code}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {str(e)}")
|
||||||
78
scripts/extract_dropdown_data.py
Normal file
78
scripts/extract_dropdown_data.py
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
"""
|
||||||
|
從 excel_table copy.md 提取下拉選單資料
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
# 讀取文件
|
||||||
|
with open('excel_table copy.md', 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# 解析表格
|
||||||
|
lines = content.strip().split('\n')
|
||||||
|
data = []
|
||||||
|
|
||||||
|
for line in lines[2:]: # 跳過標題和分隔線
|
||||||
|
if line.strip():
|
||||||
|
# 使用正則表達式分割,處理可能的空白單元格
|
||||||
|
cols = [col.strip() for col in line.split('|')[1:-1]] # 去掉首尾的空字符串
|
||||||
|
if len(cols) == 4:
|
||||||
|
data.append({
|
||||||
|
'事業體': cols[0],
|
||||||
|
'處級單位': cols[1],
|
||||||
|
'部級單位': cols[2],
|
||||||
|
'崗位名稱': cols[3]
|
||||||
|
})
|
||||||
|
|
||||||
|
# 提取唯一值並保持順序
|
||||||
|
business_units = list(OrderedDict.fromkeys([d['事業體'] for d in data if d['事業體']]))
|
||||||
|
dept_level1 = list(OrderedDict.fromkeys([d['處級單位'] for d in data if d['處級單位']]))
|
||||||
|
dept_level2 = list(OrderedDict.fromkeys([d['部級單位'] for d in data if d['部級單位']]))
|
||||||
|
position_names = list(OrderedDict.fromkeys([d['崗位名稱'] for d in data if d['崗位名稱']]))
|
||||||
|
|
||||||
|
print("=" * 80)
|
||||||
|
print("資料統計")
|
||||||
|
print("=" * 80)
|
||||||
|
print(f"總資料筆數: {len(data)}")
|
||||||
|
print(f"事業體數量: {len(business_units)}")
|
||||||
|
print(f"處級單位數量: {len(dept_level1)}")
|
||||||
|
print(f"部級單位數量: {len(dept_level2)}")
|
||||||
|
print(f"崗位名稱數量: {len(position_names)}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# 生成 JavaScript 數組
|
||||||
|
js_business_units = f"const businessUnits = {business_units};"
|
||||||
|
js_dept_level1 = f"const deptLevel1Units = {dept_level1};"
|
||||||
|
js_dept_level2 = f"const deptLevel2Units = {dept_level2};"
|
||||||
|
js_position_names = f"const positionNames = {position_names};"
|
||||||
|
|
||||||
|
print("=" * 80)
|
||||||
|
print("JavaScript 數組 (複製以下內容)")
|
||||||
|
print("=" * 80)
|
||||||
|
print()
|
||||||
|
print("// 事業體")
|
||||||
|
print(js_business_units)
|
||||||
|
print()
|
||||||
|
print("// 處級單位")
|
||||||
|
print(js_dept_level1)
|
||||||
|
print()
|
||||||
|
print("// 部級單位")
|
||||||
|
print(js_dept_level2)
|
||||||
|
print()
|
||||||
|
print("// 崗位名稱")
|
||||||
|
print(js_position_names)
|
||||||
|
print()
|
||||||
|
|
||||||
|
# 儲存到文件
|
||||||
|
with open('dropdown_data.js', 'w', encoding='utf-8') as f:
|
||||||
|
f.write("// 自動生成的下拉選單資料\n\n")
|
||||||
|
f.write("// 事業體\n")
|
||||||
|
f.write(js_business_units + "\n\n")
|
||||||
|
f.write("// 處級單位\n")
|
||||||
|
f.write(js_dept_level1 + "\n\n")
|
||||||
|
f.write("// 部級單位\n")
|
||||||
|
f.write(js_dept_level2 + "\n\n")
|
||||||
|
f.write("// 崗位名稱\n")
|
||||||
|
f.write(js_position_names + "\n")
|
||||||
|
|
||||||
|
print("已儲存到 dropdown_data.js")
|
||||||
123
scripts/extract_hierarchical_data.py
Normal file
123
scripts/extract_hierarchical_data.py
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
"""
|
||||||
|
從 excel_table copy.md 提取階層式關聯資料
|
||||||
|
用於實現下拉選單的連動功能
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
import json
|
||||||
|
from collections import OrderedDict, defaultdict
|
||||||
|
|
||||||
|
# 讀取文件
|
||||||
|
with open('excel_table copy.md', 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# 解析表格
|
||||||
|
lines = content.strip().split('\n')
|
||||||
|
data = []
|
||||||
|
|
||||||
|
for line in lines[2:]: # 跳過標題和分隔線
|
||||||
|
if line.strip():
|
||||||
|
# 使用正則表達式分割,處理可能的空白單元格
|
||||||
|
cols = [col.strip() for col in line.split('|')[1:-1]] # 去掉首尾的空字符串
|
||||||
|
if len(cols) == 4:
|
||||||
|
data.append({
|
||||||
|
'事業體': cols[0],
|
||||||
|
'處級單位': cols[1],
|
||||||
|
'部級單位': cols[2],
|
||||||
|
'崗位名稱': cols[3]
|
||||||
|
})
|
||||||
|
|
||||||
|
# 建立階層關聯
|
||||||
|
# 事業體 -> 處級單位的對應
|
||||||
|
business_to_division = defaultdict(set)
|
||||||
|
# 處級單位 -> 部級單位的對應
|
||||||
|
division_to_department = defaultdict(set)
|
||||||
|
# 部級單位 -> 崗位名稱的對應
|
||||||
|
department_to_position = defaultdict(set)
|
||||||
|
|
||||||
|
# 也建立完整的階層路徑
|
||||||
|
full_hierarchy = []
|
||||||
|
|
||||||
|
for row in data:
|
||||||
|
business = row['事業體']
|
||||||
|
division = row['處級單位']
|
||||||
|
department = row['部級單位']
|
||||||
|
position = row['崗位名稱']
|
||||||
|
|
||||||
|
if business and division:
|
||||||
|
business_to_division[business].add(division)
|
||||||
|
|
||||||
|
if division and department:
|
||||||
|
division_to_department[division].add(department)
|
||||||
|
|
||||||
|
if department and position:
|
||||||
|
department_to_position[department].add(position)
|
||||||
|
|
||||||
|
# 記錄完整路徑
|
||||||
|
if business and division and department and position:
|
||||||
|
full_hierarchy.append({
|
||||||
|
'business': business,
|
||||||
|
'division': division,
|
||||||
|
'department': department,
|
||||||
|
'position': position
|
||||||
|
})
|
||||||
|
|
||||||
|
# 轉換為列表並排序
|
||||||
|
def convert_to_sorted_list(d):
|
||||||
|
return {k: sorted(list(v)) for k, v in d.items()}
|
||||||
|
|
||||||
|
business_to_division_dict = convert_to_sorted_list(business_to_division)
|
||||||
|
division_to_department_dict = convert_to_sorted_list(division_to_department)
|
||||||
|
department_to_position_dict = convert_to_sorted_list(department_to_position)
|
||||||
|
|
||||||
|
# 統計資訊
|
||||||
|
print("=" * 80)
|
||||||
|
print("階層關聯統計")
|
||||||
|
print("=" * 80)
|
||||||
|
print(f"事業體數量: {len(business_to_division_dict)}")
|
||||||
|
print(f"處級單位數量: {len(division_to_department_dict)}")
|
||||||
|
print(f"部級單位數量: {len(department_to_position_dict)}")
|
||||||
|
print(f"完整階層路徑數量: {len(full_hierarchy)}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# 顯示幾個範例
|
||||||
|
print("範例關聯:")
|
||||||
|
print("-" * 80)
|
||||||
|
for business, divisions in list(business_to_division_dict.items())[:3]:
|
||||||
|
print(f"事業體: {business}")
|
||||||
|
print(f" -> 處級單位: {divisions}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# 生成 JavaScript 物件
|
||||||
|
js_code = """// 自動生成的階層關聯資料
|
||||||
|
|
||||||
|
// 事業體 -> 處級單位的對應
|
||||||
|
const businessToDivision = """
|
||||||
|
|
||||||
|
js_code += json.dumps(business_to_division_dict, ensure_ascii=False, indent=2)
|
||||||
|
js_code += """;
|
||||||
|
|
||||||
|
// 處級單位 -> 部級單位的對應
|
||||||
|
const divisionToDepartment = """
|
||||||
|
|
||||||
|
js_code += json.dumps(division_to_department_dict, ensure_ascii=False, indent=2)
|
||||||
|
js_code += """;
|
||||||
|
|
||||||
|
// 部級單位 -> 崗位名稱的對應
|
||||||
|
const departmentToPosition = """
|
||||||
|
|
||||||
|
js_code += json.dumps(department_to_position_dict, ensure_ascii=False, indent=2)
|
||||||
|
js_code += """;
|
||||||
|
|
||||||
|
// 完整階層資料(用於反向查詢)
|
||||||
|
const fullHierarchyData = """
|
||||||
|
|
||||||
|
js_code += json.dumps(full_hierarchy, ensure_ascii=False, indent=2)
|
||||||
|
js_code += ";\n"
|
||||||
|
|
||||||
|
# 儲存到文件
|
||||||
|
with open('hierarchical_data.js', 'w', encoding='utf-8') as f:
|
||||||
|
f.write(js_code)
|
||||||
|
|
||||||
|
print("=" * 80)
|
||||||
|
print("已儲存到 hierarchical_data.js")
|
||||||
|
print("=" * 80)
|
||||||
382
scripts/generate_review.py
Normal file
382
scripts/generate_review.py
Normal file
@@ -0,0 +1,382 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import json
|
||||||
|
|
||||||
|
# 讀取表格數據
|
||||||
|
with open('excel_table.md', 'r', encoding='utf-8') as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
|
||||||
|
# 解析數據(跳過表頭和分隔線)
|
||||||
|
data = []
|
||||||
|
for line in lines[2:]: # 跳過表頭和分隔線
|
||||||
|
line = line.strip()
|
||||||
|
if not line or not line.startswith('|'):
|
||||||
|
continue
|
||||||
|
# 移除首尾的管道符號並分割
|
||||||
|
parts = [p.strip() for p in line[1:-1].split('|')]
|
||||||
|
if len(parts) >= 4:
|
||||||
|
data.append({
|
||||||
|
'事業體': parts[0],
|
||||||
|
'處級單位': parts[1],
|
||||||
|
'部級單位': parts[2],
|
||||||
|
'崗位名稱': parts[3]
|
||||||
|
})
|
||||||
|
|
||||||
|
# 生成 HTML
|
||||||
|
html_content = '''<!DOCTYPE html>
|
||||||
|
<html lang="zh-TW">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>組織架構預覽</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: "Microsoft JhengHei", "微軟正黑體", Arial, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
padding: 20px;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header using Float */
|
||||||
|
.header {
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
overflow: hidden; /* Clear float */
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
float: left;
|
||||||
|
color: #333;
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header .stats {
|
||||||
|
float: right;
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
padding-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header::after {
|
||||||
|
content: "";
|
||||||
|
display: table;
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Filter Section using Float */
|
||||||
|
.filters {
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-group {
|
||||||
|
float: left;
|
||||||
|
margin-right: 20px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
color: #555;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-group select,
|
||||||
|
.filter-group input {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 2px solid #ddd;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 14px;
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-group select:focus,
|
||||||
|
.filter-group input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters::after {
|
||||||
|
content: "";
|
||||||
|
display: table;
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table Container using Float */
|
||||||
|
.table-container {
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
thead {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
padding: 15px;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: bold;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody tr {
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody tr:hover {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody tr:nth-child(even) {
|
||||||
|
background-color: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody tr:nth-child(even):hover {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
padding: 12px 15px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
td:first-child {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
td:last-child {
|
||||||
|
color: #764ba2;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty cells styling */
|
||||||
|
td:empty::before {
|
||||||
|
content: "—";
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer using Float */
|
||||||
|
.footer {
|
||||||
|
margin-top: 20px;
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Design */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.filter-group {
|
||||||
|
float: none;
|
||||||
|
width: 100%;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-group select,
|
||||||
|
.filter-group input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
float: none;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header .stats {
|
||||||
|
float: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<h1>📊 公司組織架構預覽</h1>
|
||||||
|
<div class="stats">總計: <span id="totalCount">''' + str(len(data)) + '''</span> 筆資料</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filters">
|
||||||
|
<div class="filter-group">
|
||||||
|
<label for="filterBusiness">事業體篩選</label>
|
||||||
|
<select id="filterBusiness">
|
||||||
|
<option value="">全部</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="filter-group">
|
||||||
|
<label for="filterDepartment">處級單位篩選</label>
|
||||||
|
<select id="filterDepartment">
|
||||||
|
<option value="">全部</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="filter-group">
|
||||||
|
<label for="filterDivision">部級單位篩選</label>
|
||||||
|
<select id="filterDivision">
|
||||||
|
<option value="">全部</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="filter-group">
|
||||||
|
<label for="searchPosition">崗位搜尋</label>
|
||||||
|
<input type="text" id="searchPosition" placeholder="輸入崗位名稱...">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-container">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>事業體</th>
|
||||||
|
<th>處級單位</th>
|
||||||
|
<th>部級單位</th>
|
||||||
|
<th>崗位名稱</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="tableBody">
|
||||||
|
'''
|
||||||
|
|
||||||
|
# 添加表格行
|
||||||
|
for row in data:
|
||||||
|
html_content += f''' <tr>
|
||||||
|
<td>{row['事業體']}</td>
|
||||||
|
<td>{row['處級單位'] if row['處級單位'] else ''}</td>
|
||||||
|
<td>{row['部級單位'] if row['部級單位'] else ''}</td>
|
||||||
|
<td>{row['崗位名稱']}</td>
|
||||||
|
</tr>
|
||||||
|
'''
|
||||||
|
|
||||||
|
html_content += ''' </tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<p>組織架構資料預覽系統 | 使用 CSS Float Layout 設計</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// 獲取所有數據
|
||||||
|
const allData = ''' + json.dumps(data, ensure_ascii=False) + ''';
|
||||||
|
|
||||||
|
// 獲取唯一的選項值
|
||||||
|
function getUniqueValues(key) {
|
||||||
|
const values = new Set();
|
||||||
|
allData.forEach(row => {
|
||||||
|
if (row[key]) {
|
||||||
|
values.add(row[key]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return Array.from(values).sort();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 填充下拉選單
|
||||||
|
function populateSelects() {
|
||||||
|
const businessSelect = document.getElementById('filterBusiness');
|
||||||
|
const deptSelect = document.getElementById('filterDepartment');
|
||||||
|
const divSelect = document.getElementById('filterDivision');
|
||||||
|
|
||||||
|
getUniqueValues('事業體').forEach(value => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = value;
|
||||||
|
option.textContent = value;
|
||||||
|
businessSelect.appendChild(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
getUniqueValues('處級單位').forEach(value => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = value;
|
||||||
|
option.textContent = value;
|
||||||
|
deptSelect.appendChild(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
getUniqueValues('部級單位').forEach(value => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = value;
|
||||||
|
option.textContent = value;
|
||||||
|
divSelect.appendChild(option);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 過濾數據
|
||||||
|
function filterData() {
|
||||||
|
const businessFilter = document.getElementById('filterBusiness').value;
|
||||||
|
const deptFilter = document.getElementById('filterDepartment').value;
|
||||||
|
const divFilter = document.getElementById('filterDivision').value;
|
||||||
|
const positionSearch = document.getElementById('searchPosition').value.toLowerCase();
|
||||||
|
|
||||||
|
const filtered = allData.filter(row => {
|
||||||
|
const matchBusiness = !businessFilter || row['事業體'] === businessFilter;
|
||||||
|
const matchDept = !deptFilter || row['處級單位'] === deptFilter;
|
||||||
|
const matchDiv = !divFilter || row['部級單位'] === divFilter;
|
||||||
|
const matchPosition = !positionSearch || row['崗位名稱'].toLowerCase().includes(positionSearch);
|
||||||
|
return matchBusiness && matchDept && matchDiv && matchPosition;
|
||||||
|
});
|
||||||
|
|
||||||
|
renderTable(filtered);
|
||||||
|
document.getElementById('totalCount').textContent = filtered.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染表格
|
||||||
|
function renderTable(data) {
|
||||||
|
const tbody = document.getElementById('tableBody');
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
|
||||||
|
data.forEach(row => {
|
||||||
|
const tr = document.createElement('tr');
|
||||||
|
tr.innerHTML = `
|
||||||
|
<td>${row['事業體'] || ''}</td>
|
||||||
|
<td>${row['處級單位'] || ''}</td>
|
||||||
|
<td>${row['部級單位'] || ''}</td>
|
||||||
|
<td>${row['崗位名稱'] || ''}</td>
|
||||||
|
`;
|
||||||
|
tbody.appendChild(tr);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 事件監聽
|
||||||
|
document.getElementById('filterBusiness').addEventListener('change', filterData);
|
||||||
|
document.getElementById('filterDepartment').addEventListener('change', filterData);
|
||||||
|
document.getElementById('filterDivision').addEventListener('change', filterData);
|
||||||
|
document.getElementById('searchPosition').addEventListener('input', filterData);
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
populateSelects();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>'''
|
||||||
|
|
||||||
|
# 寫入文件
|
||||||
|
with open('review.html', 'w', encoding='utf-8') as f:
|
||||||
|
f.write(html_content)
|
||||||
|
|
||||||
|
print(f"預覽頁面已生成:review.html")
|
||||||
|
print(f"共包含 {len(data)} 筆組織架構資料")
|
||||||
|
|
||||||
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()
|
||||||
255
scripts/rename_field_ids.py
Normal file
255
scripts/rename_field_ids.py
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
欄位 ID 自動重命名腳本
|
||||||
|
根據 ID重命名對照表.md 批量替換 HTML 和 JavaScript 中的欄位 ID
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# ID 重命名對照表
|
||||||
|
ID_MAPPINGS = {
|
||||||
|
# 模組 1: 崗位基礎資料 - 基礎資料頁籤 (15個)
|
||||||
|
'businessUnit': 'pos_businessUnit',
|
||||||
|
'division': 'pos_division',
|
||||||
|
'department': 'pos_department',
|
||||||
|
'section': 'pos_section',
|
||||||
|
'positionCode': 'pos_code',
|
||||||
|
'effectiveDate': 'pos_effectiveDate',
|
||||||
|
'positionName': 'pos_name',
|
||||||
|
'positionLevel': 'pos_level',
|
||||||
|
'positionCategory': 'pos_category',
|
||||||
|
'positionCategoryName': 'pos_categoryName',
|
||||||
|
'positionNature': 'pos_type',
|
||||||
|
'positionNatureName': 'pos_typeName',
|
||||||
|
'headcount': 'pos_headcount',
|
||||||
|
'positionDesc': 'pos_desc',
|
||||||
|
'positionRemark': 'pos_remark',
|
||||||
|
|
||||||
|
# 模組 2: 崗位基礎資料 - 招聘要求頁籤 (18個)
|
||||||
|
'minEducation': 'rec_eduLevel',
|
||||||
|
'requiredGender': 'rec_gender',
|
||||||
|
'salaryRange': 'rec_salaryRange',
|
||||||
|
'workExperience': 'rec_expYears',
|
||||||
|
'minAge': 'rec_minAge',
|
||||||
|
'maxAge': 'rec_maxAge',
|
||||||
|
'jobType': 'rec_jobType',
|
||||||
|
'recruitPosition': 'rec_position',
|
||||||
|
'jobTitle': 'rec_jobTitle',
|
||||||
|
'superiorPosition': 'rec_superiorCode',
|
||||||
|
'jobDesc': 'rec_jobDesc',
|
||||||
|
'positionReq': 'rec_positionReq',
|
||||||
|
'titleReq': 'rec_certReq',
|
||||||
|
'majorReq': 'rec_majorReq',
|
||||||
|
'skillReq': 'rec_skillReq',
|
||||||
|
'langReq': 'rec_langReq',
|
||||||
|
'otherReq': 'rec_otherReq',
|
||||||
|
'recruitRemark': 'rec_remark',
|
||||||
|
|
||||||
|
# 模組 3: 職務基礎資料 (12個)
|
||||||
|
'jobCategoryCode': 'job_category',
|
||||||
|
'jobCategoryName': 'job_categoryName',
|
||||||
|
'jobCode': 'job_code',
|
||||||
|
'jobName': 'job_name',
|
||||||
|
'jobNameEn': 'job_nameEn',
|
||||||
|
'jobEffectiveDate': 'job_effectiveDate',
|
||||||
|
'jobLevel': 'job_level',
|
||||||
|
'jobHeadcount': 'job_headcount',
|
||||||
|
'jobSortOrder': 'job_sortOrder',
|
||||||
|
'hasAttendanceBonus': 'job_hasAttBonus',
|
||||||
|
'hasHousingAllowance': 'job_hasHouseAllow',
|
||||||
|
'jobRemark': 'job_remark',
|
||||||
|
|
||||||
|
# 模組 4: 部門職責 (19個 - 包含合併重複欄位)
|
||||||
|
'deptFunctionCode': 'df_code',
|
||||||
|
'deptFunctionName': 'df_name',
|
||||||
|
'deptFunctionBU': 'df_businessUnit',
|
||||||
|
'deptFunc_businessUnit': 'df_businessUnit', # 合併
|
||||||
|
'deptFunc_division': 'df_division',
|
||||||
|
'deptFunc_department': 'df_department',
|
||||||
|
'deptFunc_section': 'df_section',
|
||||||
|
'deptFunc_positionTitle': 'df_posTitle',
|
||||||
|
'deptFunc_positionLevel': 'df_posLevel',
|
||||||
|
'deptManager': 'df_managerTitle',
|
||||||
|
'deptFunctionEffectiveDate': 'df_effectiveDate',
|
||||||
|
'deptHeadcount': 'df_headcountLimit',
|
||||||
|
'deptStatus': 'df_status',
|
||||||
|
'deptMission': 'df_mission',
|
||||||
|
'deptVision': 'df_vision',
|
||||||
|
'deptCoreFunctions': 'df_coreFunc',
|
||||||
|
'deptKPIs': 'df_kpis',
|
||||||
|
'deptCollaboration': 'df_collab',
|
||||||
|
'deptFunctionRemark': 'df_remark',
|
||||||
|
|
||||||
|
# 模組 5: 崗位描述 (8個需要變更的)
|
||||||
|
'jd_positionCode': 'jd_posCode',
|
||||||
|
'jd_positionName': 'jd_posName',
|
||||||
|
'jd_positionLevel': 'jd_posLevel',
|
||||||
|
'jd_positionEffectiveDate': 'jd_posEffDate',
|
||||||
|
'jd_directSupervisor': 'jd_supervisor',
|
||||||
|
'jd_positionGradeJob': 'jd_gradeJob',
|
||||||
|
'jd_workLocation': 'jd_location',
|
||||||
|
'jd_empAttribute': 'jd_empAttr',
|
||||||
|
'jd_deptFunctionCode': 'jd_dfCode',
|
||||||
|
'jd_positionPurpose': 'jd_purpose',
|
||||||
|
'jd_mainResponsibilities': 'jd_mainResp',
|
||||||
|
'jd_education': 'jd_eduLevel',
|
||||||
|
'jd_basicSkills': 'jd_basicSkills',
|
||||||
|
'jd_professionalKnowledge': 'jd_proKnowledge',
|
||||||
|
'jd_workExperienceReq': 'jd_expReq',
|
||||||
|
'jd_otherRequirements': 'jd_otherReq',
|
||||||
|
}
|
||||||
|
|
||||||
|
# 需要特殊處理的函數名映射(onchange事件等)
|
||||||
|
FUNCTION_MAPPINGS = {
|
||||||
|
'updateCategoryName': 'updateCategoryName', # 保持不變,但內部需要更新
|
||||||
|
'updateNatureName': 'updateTypeName', # positionNature -> pos_type
|
||||||
|
'updateJobCategoryName': 'updateJobCategoryName', # 保持不變
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def replace_in_file(file_path, dry_run=False):
|
||||||
|
"""
|
||||||
|
在文件中替換所有匹配的 ID
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_path: 文件路徑
|
||||||
|
dry_run: 如果為 True,只輸出變更不實際修改
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(總替換次數, 變更詳情列表)
|
||||||
|
"""
|
||||||
|
with open(file_path, 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
original_content = content
|
||||||
|
changes = []
|
||||||
|
total_replacements = 0
|
||||||
|
|
||||||
|
# 排序:按舊ID長度降序,避免短ID誤替換長ID
|
||||||
|
sorted_mappings = sorted(ID_MAPPINGS.items(), key=lambda x: len(x[0]), reverse=True)
|
||||||
|
|
||||||
|
for old_id, new_id in sorted_mappings:
|
||||||
|
if old_id == new_id:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 匹配模式:
|
||||||
|
# 1. HTML id="oldId"
|
||||||
|
# 2. JavaScript getElementById('oldId') 或 getElementById("oldId")
|
||||||
|
# 3. HTML for="oldId"
|
||||||
|
# 4. HTML name="oldId"
|
||||||
|
# 5. 對象屬性 {oldId: ...} 或 data.oldId
|
||||||
|
|
||||||
|
patterns = [
|
||||||
|
# HTML id 屬性
|
||||||
|
(rf'\bid=["\']({re.escape(old_id)})["\']', rf'id="\1"', lambda m: f'id="{new_id}"'),
|
||||||
|
# HTML for 屬性
|
||||||
|
(rf'\bfor=["\']({re.escape(old_id)})["\']', rf'for="\1"', lambda m: f'for="{new_id}"'),
|
||||||
|
# HTML name 屬性
|
||||||
|
(rf'\bname=["\']({re.escape(old_id)})["\']', rf'name="\1"', lambda m: f'name="{new_id}"'),
|
||||||
|
# getElementById
|
||||||
|
(rf'getElementById\(["\']({re.escape(old_id)})["\']', rf'getElementById("\1")', lambda m: f'getElementById("{new_id}")'),
|
||||||
|
# 對象屬性訪問 .oldId (謹慎使用,確保前面是合理的對象)
|
||||||
|
(rf'\.({re.escape(old_id)})\b', rf'.\1', lambda m: f'.{new_id}'),
|
||||||
|
# 對象字面量屬性 oldId: 或 "oldId":
|
||||||
|
(rf'\b({re.escape(old_id)}):', rf'\1:', lambda m: f'{new_id}:'),
|
||||||
|
]
|
||||||
|
|
||||||
|
for pattern, _, replacement_func in patterns:
|
||||||
|
matches = list(re.finditer(pattern, content))
|
||||||
|
if matches:
|
||||||
|
# 從後往前替換,避免索引偏移
|
||||||
|
for match in reversed(matches):
|
||||||
|
start, end = match.span()
|
||||||
|
old_text = content[start:end]
|
||||||
|
new_text = replacement_func(match)
|
||||||
|
|
||||||
|
if old_text != new_text:
|
||||||
|
content = content[:start] + new_text + content[end:]
|
||||||
|
changes.append({
|
||||||
|
'old': old_text,
|
||||||
|
'new': new_text,
|
||||||
|
'line': content[:start].count('\n') + 1
|
||||||
|
})
|
||||||
|
total_replacements += 1
|
||||||
|
|
||||||
|
# 如果有變更且非 dry run,寫回文件
|
||||||
|
if content != original_content and not dry_run:
|
||||||
|
with open(file_path, 'w', encoding='utf-8', newline='') as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
return total_replacements, changes
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""主函數"""
|
||||||
|
base_dir = Path(__file__).parent
|
||||||
|
|
||||||
|
# 需要處理的文件列表
|
||||||
|
files_to_process = [
|
||||||
|
base_dir / 'index.html',
|
||||||
|
base_dir / 'js' / 'ui.js',
|
||||||
|
base_dir / 'js' / 'ai-bags.js',
|
||||||
|
base_dir / 'js' / 'main.js',
|
||||||
|
]
|
||||||
|
|
||||||
|
print("=" * 80)
|
||||||
|
print("欄位 ID 重命名工具")
|
||||||
|
print("=" * 80)
|
||||||
|
print(f"\n📋 總計需要重命名:{len(ID_MAPPINGS)} 個 ID")
|
||||||
|
print(f"📂 需要處理:{len(files_to_process)} 個文件\n")
|
||||||
|
|
||||||
|
# 先 dry run 顯示變更
|
||||||
|
print("🔍 掃描變更(Dry Run)...")
|
||||||
|
print("-" * 80)
|
||||||
|
|
||||||
|
total_changes = 0
|
||||||
|
for file_path in files_to_process:
|
||||||
|
if not file_path.exists():
|
||||||
|
print(f"⚠️ 文件不存在:{file_path.name}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
count, changes = replace_in_file(file_path, dry_run=True)
|
||||||
|
total_changes += count
|
||||||
|
|
||||||
|
if count > 0:
|
||||||
|
print(f"\n📄 {file_path.name}: {count} 處變更")
|
||||||
|
# 顯示前 5 個變更示例
|
||||||
|
for i, change in enumerate(changes[:5]):
|
||||||
|
print(f" L{change['line']}: {change['old']} → {change['new']}")
|
||||||
|
if len(changes) > 5:
|
||||||
|
print(f" ... 還有 {len(changes) - 5} 處變更")
|
||||||
|
|
||||||
|
print("\n" + "=" * 80)
|
||||||
|
print(f"📊 總計:{total_changes} 處需要替換")
|
||||||
|
print("=" * 80)
|
||||||
|
|
||||||
|
# 詢問是否執行
|
||||||
|
response = input("\n是否執行替換?(y/n): ").strip().lower()
|
||||||
|
|
||||||
|
if response == 'y':
|
||||||
|
print("\n🚀 開始執行替換...")
|
||||||
|
print("-" * 80)
|
||||||
|
|
||||||
|
for file_path in files_to_process:
|
||||||
|
if not file_path.exists():
|
||||||
|
continue
|
||||||
|
|
||||||
|
count, _ = replace_in_file(file_path, dry_run=False)
|
||||||
|
if count > 0:
|
||||||
|
print(f"✅ {file_path.name}: 已替換 {count} 處")
|
||||||
|
|
||||||
|
print("\n✨ 替換完成!")
|
||||||
|
print("\n⚠️ 請執行以下步驟:")
|
||||||
|
print(" 1. 測試所有表單功能")
|
||||||
|
print(" 2. 檢查瀏覽器控制台是否有錯誤")
|
||||||
|
print(" 3. 使用 git diff 檢查變更")
|
||||||
|
print(" 4. 提交變更:git add -A && git commit -m 'refactor: 標準化欄位 ID 命名'")
|
||||||
|
else:
|
||||||
|
print("\n❌ 已取消執行")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@@ -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
|
||||||
|
|||||||
110
styles/base.css
Normal file
110
styles/base.css
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
/**
|
||||||
|
* Base Styles - 基礎樣式
|
||||||
|
* 包含 CSS 變數定義、CSS Reset、全域樣式
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* ==================== CSS Variables ==================== */
|
||||||
|
:root {
|
||||||
|
/* 主題色 */
|
||||||
|
--primary: #1a5276;
|
||||||
|
--primary-light: #2980b9;
|
||||||
|
--primary-dark: #0e3a53;
|
||||||
|
--accent: #e67e22;
|
||||||
|
--accent-light: #f39c12;
|
||||||
|
--green: #27ae60;
|
||||||
|
--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-card: #ffffff;
|
||||||
|
--border: #d5dbdf;
|
||||||
|
|
||||||
|
/* 文字顏色 */
|
||||||
|
--text-primary: #2c3e50;
|
||||||
|
--text-secondary: #5d6d7e;
|
||||||
|
|
||||||
|
/* 語義化顏色 */
|
||||||
|
--success: #27ae60;
|
||||||
|
--warning: #f39c12;
|
||||||
|
--danger: #e74c3c;
|
||||||
|
|
||||||
|
/* 陰影與圓角 */
|
||||||
|
--shadow: 0 4px 20px rgba(26, 82, 118, 0.12);
|
||||||
|
--radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==================== CSS Reset ==================== */
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==================== Global Styles ==================== */
|
||||||
|
body {
|
||||||
|
font-family: 'Noto Sans TC', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #e8f4fc 0%, #f4f6f9 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==================== Animations ==================== */
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(8px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes modalIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.9);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
1154
styles/components.css
Normal file
1154
styles/components.css
Normal file
File diff suppressed because it is too large
Load Diff
522
styles/layout.css
Normal file
522
styles/layout.css
Normal file
@@ -0,0 +1,522 @@
|
|||||||
|
/**
|
||||||
|
* Layout Styles - 布局樣式
|
||||||
|
* Two-Column Layout: 左側導航 + 右側內容
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* ==================== Main Layout ==================== */
|
||||||
|
.app-container {
|
||||||
|
display: flex;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: var(--bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==================== Sidebar (左側導航欄) ==================== */
|
||||||
|
.sidebar {
|
||||||
|
width: 240px;
|
||||||
|
min-width: 240px;
|
||||||
|
background: linear-gradient(180deg, #1a1f2e 0%, #2d3748 100%);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: fixed;
|
||||||
|
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 {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-details {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-name {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-role {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
opacity: 0.7;
|
||||||
|
color: #a0aec0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 16px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
color: #e2e8f0;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
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 {
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-btn.active {
|
||||||
|
background: linear-gradient(135deg, var(--module-position) 0%, var(--module-position-dark) 100%);
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 4px 12px var(--module-position-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-btn.active.job-active {
|
||||||
|
background: linear-gradient(135deg, var(--module-job) 0%, var(--module-job-dark) 100%);
|
||||||
|
box-shadow: 0 4px 12px var(--module-job-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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 {
|
||||||
|
background: linear-gradient(135deg, var(--module-desc) 0%, var(--module-desc-dark) 100%);
|
||||||
|
box-shadow: 0 4px 12px var(--module-desc-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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 {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
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 {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-content.active {
|
||||||
|
display: block;
|
||||||
|
animation: fadeIn 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==================== App Header ==================== */
|
||||||
|
.app-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
padding: 20px 28px;
|
||||||
|
background: linear-gradient(135deg, var(--module-position) 0%, var(--module-position-dark) 100%);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header.job-header {
|
||||||
|
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 {
|
||||||
|
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 {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header .icon svg {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
fill: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header h1 {
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header .subtitle {
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==================== Form Card ==================== */
|
||||||
|
.form-card {
|
||||||
|
background: var(--bg-card);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==================== Sections ==================== */
|
||||||
|
.section-box {
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: linear-gradient(135deg, #e8f4fc 0%, #f0f4f8 100%);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header.green {
|
||||||
|
background: linear-gradient(135deg, #e8f8f0 0%, #f0f8f4 100%);
|
||||||
|
color: var(--green-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-body {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-divider {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-divider span {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--primary);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-divider::after {
|
||||||
|
content: '';
|
||||||
|
flex: 1;
|
||||||
|
height: 1px;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
336
styles/modules.css
Normal file
336
styles/modules.css
Normal file
@@ -0,0 +1,336 @@
|
|||||||
|
/**
|
||||||
|
* Modules Styles - 模組專屬樣式
|
||||||
|
* 包含各個頁籤/模組的特定樣式需求
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* ==================== 共用模組樣式 ==================== */
|
||||||
|
|
||||||
|
/* 模組頂部工具列 */
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
244
styles/utilities.css
Normal file
244
styles/utilities.css
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
/**
|
||||||
|
* Utilities Styles - 工具類別與額外響應式設計
|
||||||
|
* 包含快速工具類別、表格響應式、額外斷點調整
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* ==================== Utility Classes ==================== */
|
||||||
|
|
||||||
|
/* 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) {
|
||||||
|
.hide-tablet { display: none !important; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.hide-mobile { display: none !important; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 481px) {
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-with-button .btn-lookup {
|
||||||
|
width: 100%;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
62
tests/test_deepseek_reasoner.py
Normal file
62
tests/test_deepseek_reasoner.py
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
"""
|
||||||
|
Test deepseek-reasoner model on Ollama API
|
||||||
|
"""
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
import urllib3
|
||||||
|
import sys
|
||||||
|
import codecs
|
||||||
|
|
||||||
|
# Set UTF-8 encoding for output
|
||||||
|
if sys.platform == 'win32':
|
||||||
|
sys.stdout = codecs.getwriter('utf-8')(sys.stdout.buffer, 'strict')
|
||||||
|
|
||||||
|
# Disable SSL warnings
|
||||||
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||||
|
|
||||||
|
API_URL = "https://ollama_pjapi.theaken.com"
|
||||||
|
|
||||||
|
print("=" * 60)
|
||||||
|
print("Testing deepseek-reasoner model")
|
||||||
|
print("=" * 60)
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Test chat completion with deepseek-reasoner
|
||||||
|
print("Sending test prompt to deepseek-reasoner...")
|
||||||
|
try:
|
||||||
|
chat_request = {
|
||||||
|
"model": "deepseek-reasoner",
|
||||||
|
"messages": [
|
||||||
|
{"role": "user", "content": "請用中文簡單地說明什麼是人工智慧"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(
|
||||||
|
f"{API_URL}/v1/chat/completions",
|
||||||
|
json=chat_request,
|
||||||
|
headers={'Content-Type': 'application/json'},
|
||||||
|
timeout=60,
|
||||||
|
verify=False
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"Status Code: {response.status_code}")
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
result = response.json()
|
||||||
|
text = result['choices'][0]['message']['content']
|
||||||
|
print("\nResponse:")
|
||||||
|
print("-" * 60)
|
||||||
|
print(text)
|
||||||
|
print("-" * 60)
|
||||||
|
|
||||||
|
# Save to file
|
||||||
|
with open('deepseek_reasoner_output.txt', 'w', encoding='utf-8') as f:
|
||||||
|
f.write(text)
|
||||||
|
print("\n✓ Response saved to: deepseek_reasoner_output.txt")
|
||||||
|
else:
|
||||||
|
print(f"Error: {response.text}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {str(e)}")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 60)
|
||||||
70
tests/test_ollama.py
Normal file
70
tests/test_ollama.py
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
"""
|
||||||
|
Test Ollama API integration
|
||||||
|
"""
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
import urllib3
|
||||||
|
|
||||||
|
# Disable SSL warnings
|
||||||
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||||
|
|
||||||
|
API_URL = "https://ollama_pjapi.theaken.com"
|
||||||
|
|
||||||
|
print("=" * 60)
|
||||||
|
print("Testing Ollama API Connection")
|
||||||
|
print("=" * 60)
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Test 1: List models
|
||||||
|
print("Test 1: Listing available models...")
|
||||||
|
try:
|
||||||
|
response = requests.get(f"{API_URL}/v1/models", timeout=10, verify=False)
|
||||||
|
print(f"Status Code: {response.status_code}")
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
models = data.get('data', [])
|
||||||
|
print(f"Found {len(models)} models:")
|
||||||
|
for model in models[:5]:
|
||||||
|
print(f" - {model.get('id', 'Unknown')}")
|
||||||
|
else:
|
||||||
|
print(f"Error: {response.text}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {str(e)}")
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Test 2: Chat completion
|
||||||
|
print("Test 2: Testing chat completion...")
|
||||||
|
try:
|
||||||
|
chat_request = {
|
||||||
|
"model": "qwen2.5:3b",
|
||||||
|
"messages": [
|
||||||
|
{"role": "system", "content": "You are a helpful assistant."},
|
||||||
|
{"role": "user", "content": "Say hello in Chinese."}
|
||||||
|
],
|
||||||
|
"temperature": 0.7,
|
||||||
|
"max_tokens": 50
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(
|
||||||
|
f"{API_URL}/v1/chat/completions",
|
||||||
|
json=chat_request,
|
||||||
|
headers={'Content-Type': 'application/json'},
|
||||||
|
timeout=60,
|
||||||
|
verify=False
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"Status Code: {response.status_code}")
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
result = response.json()
|
||||||
|
text = result['choices'][0]['message']['content']
|
||||||
|
print(f"Response: {text}")
|
||||||
|
else:
|
||||||
|
print(f"Error: {response.text}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {str(e)}")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 60)
|
||||||
79
tests/test_ollama2.py
Normal file
79
tests/test_ollama2.py
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
"""
|
||||||
|
Test Ollama API with different parameters
|
||||||
|
"""
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
import urllib3
|
||||||
|
|
||||||
|
# Disable SSL warnings
|
||||||
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||||
|
|
||||||
|
API_URL = "https://ollama_pjapi.theaken.com"
|
||||||
|
|
||||||
|
print("=" * 60)
|
||||||
|
print("Testing Ollama Chat Completion - Variant Tests")
|
||||||
|
print("=" * 60)
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Test 1: Using qwen2.5:72b (actual available model)
|
||||||
|
print("Test 1: Using qwen2.5:72b model...")
|
||||||
|
try:
|
||||||
|
chat_request = {
|
||||||
|
"model": "qwen2.5:72b",
|
||||||
|
"messages": [
|
||||||
|
{"role": "user", "content": "Say hello in Chinese."}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(
|
||||||
|
f"{API_URL}/v1/chat/completions",
|
||||||
|
json=chat_request,
|
||||||
|
headers={'Content-Type': 'application/json'},
|
||||||
|
timeout=60,
|
||||||
|
verify=False
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"Status Code: {response.status_code}")
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
result = response.json()
|
||||||
|
text = result['choices'][0]['message']['content']
|
||||||
|
print(f"Success! Response: {text}")
|
||||||
|
else:
|
||||||
|
print(f"Error: {response.text}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {str(e)}")
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Test 2: Try deepseek-chat model
|
||||||
|
print("Test 2: Using deepseek-chat model...")
|
||||||
|
try:
|
||||||
|
chat_request = {
|
||||||
|
"model": "deepseek-chat",
|
||||||
|
"messages": [
|
||||||
|
{"role": "user", "content": "Say hello in Chinese."}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(
|
||||||
|
f"{API_URL}/v1/chat/completions",
|
||||||
|
json=chat_request,
|
||||||
|
headers={'Content-Type': 'application/json'},
|
||||||
|
timeout=60,
|
||||||
|
verify=False
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"Status Code: {response.status_code}")
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
result = response.json()
|
||||||
|
text = result['choices'][0]['message']['content']
|
||||||
|
print(f"Success! Response: {text}")
|
||||||
|
else:
|
||||||
|
print(f"Error: {response.text}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {str(e)}")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 60)
|
||||||
110
tests/test_ollama_final.py
Normal file
110
tests/test_ollama_final.py
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
"""
|
||||||
|
Final Ollama API Integration Test
|
||||||
|
Tests the integration with the Flask app
|
||||||
|
"""
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Set UTF-8 encoding for output
|
||||||
|
if sys.platform == 'win32':
|
||||||
|
import codecs
|
||||||
|
sys.stdout = codecs.getwriter('utf-8')(sys.stdout.buffer, 'strict')
|
||||||
|
|
||||||
|
print("=" * 60)
|
||||||
|
print("Ollama API Integration Test (via Flask App)")
|
||||||
|
print("=" * 60)
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Test 1: Test Ollama connection status
|
||||||
|
print("Test 1: Checking Ollama API configuration...")
|
||||||
|
try:
|
||||||
|
response = requests.get("http://localhost:5000/api/llm/config", timeout=10)
|
||||||
|
if response.status_code == 200:
|
||||||
|
config = response.json()
|
||||||
|
ollama_config = config.get('ollama', {})
|
||||||
|
print(f" Name: {ollama_config.get('name', 'N/A')}")
|
||||||
|
print(f" Enabled: {ollama_config.get('enabled', False)}")
|
||||||
|
print(f" Endpoint: {ollama_config.get('endpoint', 'N/A')}")
|
||||||
|
print(" Status: ✓ Configuration OK")
|
||||||
|
else:
|
||||||
|
print(f" Status: ✗ Error {response.status_code}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" Status: ✗ Error: {str(e)}")
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Test 2: Generate text using Ollama
|
||||||
|
print("Test 2: Testing text generation with Ollama...")
|
||||||
|
try:
|
||||||
|
payload = {
|
||||||
|
"api": "ollama",
|
||||||
|
"prompt": "請用中文回答:你好嗎?",
|
||||||
|
"max_tokens": 100
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(
|
||||||
|
"http://localhost:5000/api/llm/generate",
|
||||||
|
json=payload,
|
||||||
|
headers={'Content-Type': 'application/json'},
|
||||||
|
timeout=60
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f" Status Code: {response.status_code}")
|
||||||
|
|
||||||
|
result = response.json()
|
||||||
|
|
||||||
|
if result.get('success'):
|
||||||
|
text = result.get('text', '')
|
||||||
|
print(f" Status: ✓ Generation successful")
|
||||||
|
print(f" Response length: {len(text)} characters")
|
||||||
|
print(f" Response preview: {text[:100]}...")
|
||||||
|
|
||||||
|
# Save full response to file
|
||||||
|
with open('ollama_response.txt', 'w', encoding='utf-8') as f:
|
||||||
|
f.write(text)
|
||||||
|
print(f" Full response saved to: ollama_response.txt")
|
||||||
|
else:
|
||||||
|
error = result.get('error', 'Unknown error')
|
||||||
|
print(f" Status: ✗ Generation failed")
|
||||||
|
print(f" Error: {error}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f" Status: ✗ Error: {str(e)}")
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Test 3: Test with English prompt
|
||||||
|
print("Test 3: Testing with English prompt...")
|
||||||
|
try:
|
||||||
|
payload = {
|
||||||
|
"api": "ollama",
|
||||||
|
"prompt": "Write a haiku about coding.",
|
||||||
|
"max_tokens": 100
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(
|
||||||
|
"http://localhost:5000/api/llm/generate",
|
||||||
|
json=payload,
|
||||||
|
headers={'Content-Type': 'application/json'},
|
||||||
|
timeout=60
|
||||||
|
)
|
||||||
|
|
||||||
|
result = response.json()
|
||||||
|
|
||||||
|
if result.get('success'):
|
||||||
|
text = result.get('text', '')
|
||||||
|
print(f" Status: ✓ Generation successful")
|
||||||
|
print(f" Response:\n{text}")
|
||||||
|
else:
|
||||||
|
error = result.get('error', 'Unknown error')
|
||||||
|
print(f" Status: ✗ Generation failed")
|
||||||
|
print(f" Error: {error}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f" Status: ✗ Error: {str(e)}")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 60)
|
||||||
|
print("Integration test completed!")
|
||||||
|
print("=" * 60)
|
||||||
@@ -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