refactor: 新增 ui.js 和 main.js 模組,啟用 ES6 Modules
新增檔案: - js/ui.js - UI 操作、模組切換、預覽更新、表單資料收集 - js/main.js - 主程式初始化、事件監聽器設置、快捷鍵 更新檔案: - index.html - 引用 ES6 模組 (type="module") 功能: ✅ 模組切換功能 ✅ 標籤頁切換 ✅ 表單欄位監聽 ✅ JSON 預覽更新 ✅ 快捷鍵支援 (Ctrl+S, Ctrl+N) ✅ 用戶信息載入 ✅ 登出功能 注意: - 大部分 JavaScript 代碼仍在 HTML 中(約 2400 行) - 已建立核心模組架構,便於後續逐步遷移 - 使用 ES6 Modules,需要通過 HTTP Server 運行 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
201
js/main.js
Normal file
201
js/main.js
Normal file
@@ -0,0 +1,201 @@
|
||||
/**
|
||||
* Main - 主程式
|
||||
* 初始化應用程式,設定事件監聽器
|
||||
*/
|
||||
|
||||
import { showToast } from './utils.js';
|
||||
import { switchModule, updatePreview, updateCategoryName, updateNatureName, updateJobCategoryName } from './ui.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);
|
||||
});
|
||||
|
||||
// 崗位類別變更
|
||||
const positionCategory = document.getElementById('positionCategory');
|
||||
if (positionCategory) {
|
||||
positionCategory.addEventListener('change', updateCategoryName);
|
||||
}
|
||||
|
||||
// 崗位性質變更
|
||||
const positionNature = document.getElementById('positionNature');
|
||||
if (positionNature) {
|
||||
positionNature.addEventListener('change', updateNatureName);
|
||||
}
|
||||
|
||||
// 職務類別變更
|
||||
const jobCategoryCode = document.getElementById('jobCategoryCode');
|
||||
if (jobCategoryCode) {
|
||||
jobCategoryCode.addEventListener('change', updateJobCategoryName);
|
||||
}
|
||||
|
||||
// Toggle 開關變更
|
||||
const hasAttendanceBonus = document.getElementById('hasAttendanceBonus');
|
||||
if (hasAttendanceBonus) {
|
||||
hasAttendanceBonus.addEventListener('change', function() {
|
||||
const label = document.getElementById('attendanceLabel');
|
||||
if (label) {
|
||||
label.textContent = this.checked ? '是' : '否';
|
||||
}
|
||||
updatePreview();
|
||||
});
|
||||
}
|
||||
|
||||
const hasHousingAllowance = document.getElementById('hasHousingAllowance');
|
||||
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();
|
||||
|
||||
// 初始化預覽
|
||||
updatePreview();
|
||||
|
||||
console.log('✅ HR 系統初始化完成');
|
||||
});
|
||||
|
||||
// ==================== 將函式掛載到 window ====================
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.logout = logout;
|
||||
window.loadUserInfo = loadUserInfo;
|
||||
}
|
||||
291
js/ui.js
Normal file
291
js/ui.js
Normal file
@@ -0,0 +1,291 @@
|
||||
/**
|
||||
* 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: {} };
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集職務表單資料
|
||||
* @returns {Object} - 職務資料
|
||||
*/
|
||||
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('hasAttendanceBonus').checked;
|
||||
data.hasHousingAllowance = document.getElementById('hasHousingAllowance').checked;
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集崗位描述表單資料
|
||||
* @returns {Object} - 崗位描述資料
|
||||
*/
|
||||
export function getJobDescFormData() {
|
||||
const form = document.getElementById('jobDescForm');
|
||||
if (!form) return {};
|
||||
|
||||
const formData = new FormData(form);
|
||||
const data = { basicInfo: {}, positionInfo: {}, responsibilities: {}, requirements: {} };
|
||||
|
||||
// Basic Info
|
||||
['empNo', 'empName', 'positionCode', 'versionDate'].forEach(field => {
|
||||
const el = document.getElementById('jd_' + field);
|
||||
if (el && el.value) data.basicInfo[field] = el.value;
|
||||
});
|
||||
|
||||
// Position Info
|
||||
['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;
|
||||
});
|
||||
|
||||
// Purpose & Responsibilities
|
||||
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;
|
||||
|
||||
// Requirements
|
||||
['education', 'basicSkills', 'professionalKnowledge', 'workExperienceReq', 'otherRequirements'].forEach(field => {
|
||||
const el = document.getElementById('jd_' + field);
|
||||
if (el && el.value) data.requirements[field] = el.value;
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集部門職責表單資料
|
||||
* @returns {Object} - 部門職責資料
|
||||
*/
|
||||
export function getDeptFunctionFormData() {
|
||||
const form = document.getElementById('deptFunctionForm');
|
||||
if (!form) return {};
|
||||
|
||||
const formData = new FormData(form);
|
||||
const data = {};
|
||||
|
||||
const fields = ['deptFunctionCode', 'deptFunctionName', 'deptFunctionBU',
|
||||
'deptFunctionDept', 'deptManager', 'deptMission', 'deptVision',
|
||||
'deptCoreFunctions', 'deptKPIs'];
|
||||
|
||||
fields.forEach(field => {
|
||||
const value = formData.get(field);
|
||||
if (value) data[field] = 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 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 updateJobCategoryName() {
|
||||
const category = document.getElementById('jobCategoryCode').value;
|
||||
document.getElementById('jobCategoryName').value = jobCategoryMap[category] || '';
|
||||
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 changeJobCode() {
|
||||
const currentCode = document.getElementById('jobCode').value;
|
||||
const newCode = prompt('請輸入新的職務編號:', currentCode);
|
||||
if (newCode && newCode !== currentCode) {
|
||||
document.getElementById('jobCode').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);
|
||||
});
|
||||
document.getElementById('majorReq').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;
|
||||
}
|
||||
Reference in New Issue
Block a user