// ======================================== // 報到系統 - 共用 JavaScript 函式庫 // 檔案位置:Scripts/shared.js // ======================================== // // 提供的全域服務: // - $api : API 呼叫服務 (定義於 line 21) // - $message : 訊息提示服務 (定義於 line 101) // - $format : 顯示格式化服務 (定義於 line 223) // - $dataConvert : 資料轉換服務 (處理 /Date()/ 等資料來源格式) // - $navigate : 頁面導航服務 // // 使用方式: // 在 Vue 實例內:this.$api.login(...), this.$format.date(...), this.$dataConvert.formatDate(...) // 在 Vue 實例外:window.$api.login(...), window.$format.date(...), window.$dataConvert.formatDate(...) // // 向後相容舊 API:apiCall.login(...), message.success(...), navigate.to(...) // // ======================================== (() => { 'use strict'; // ========================================== // Loading 遮罩服務 // 定義位置:Scripts/shared.js (line 21-70) // ========================================== const LoadingService = { overlay: null, counter: 0, /** * 顯示 Loading 遮罩 * @定義位置 Scripts/shared.js line 29 */ show() { this.counter++; if (!this.overlay) { this.overlay = document.createElement('div'); this.overlay.id = 'api-loading-overlay'; this.overlay.innerHTML = `
`; document.body.appendChild(this.overlay); } this.overlay.style.display = 'flex'; }, /** * 隱藏 Loading 遮罩 * @定義位置 Scripts/shared.js line 51 */ hide() { this.counter--; if (this.counter <= 0) { this.counter = 0; if (this.overlay) { this.overlay.style.display = 'none'; } } }, /** * 強制隱藏 Loading 遮罩 * @定義位置 Scripts/shared.js line 66 */ forceHide() { this.counter = 0; if (this.overlay) { this.overlay.style.display = 'none'; } } }; // ========================================== // API 服務 // 定義位置:Scripts/shared.js (line 76-162) // ========================================== const ApiService = { // API 基礎路徑 baseUrl: API_BASE_URL, homeUrl: HOME_URL, /** * 呼叫 ASMX WebMethod * @定義位置 Scripts/shared.js line 33 * @param {string} serviceName - 服務名稱 (如 'LoginService') * @param {string} methodName - 方法名稱 (如 'Login') * @param {object} data - 傳送的資料 * @returns {Promise} 回傳 Promise 物件 * * @example * $api.call('LoginService', 'Login', { username: 'admin', password: '123' }) * .then(response => console.log(response)) * .catch(error => console.error(error)); */ call(serviceName, methodName, data = {}) { LoadingService.show(); return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); const url = `${this.baseUrl}${serviceName}.asmx/${methodName}`; xhr.open('POST', url, true); xhr.setRequestHeader('Content-Type', 'application/json; charset=utf-8'); // ✨ 關鍵修正: 確保傳送 Cookie xhr.withCredentials = true; xhr.onload = () => { LoadingService.hide(); if (xhr.status === 200) { try { const response = JSON.parse(xhr.responseText); resolve(response.d || response); } catch (e) { reject({ Success: false, Message: '解析回應失敗' }); } } else if (xhr.status === 401) { // ✨ 處理 401 錯誤 reject({ Success: false, Message: '未授權,請重新登入' }); // 可選: 自動導向登入頁 // window.location.href = '/Login.aspx'; } else { reject({ Success: false, Message: `HTTP Error: ${xhr.status}` }); } }; xhr.onerror = () => { LoadingService.hide(); reject({ Success: false, Message: '網路錯誤' }); }; xhr.send(JSON.stringify(data)); }); }, /** * 登入 * @定義位置 Scripts/shared.js line 78 * @param {string} username - 使用者帳號 * @param {string} password - 密碼 * @param {boolean} rememberMe - 是否記住我 * @returns {Promise} * * @example * $api.login('admin', 'admin123', true) * .then(response => { * if (response.Success) { * console.log('登入成功:', response.UserName); * } * }); */ login(username, password, rememberMe = false) { return this.call('LoginService', 'Login', { username, password, rememberMe }); }, /** * 登出 * @定義位置 Scripts/shared.js line 101 * @returns {Promise} * * @example * $api.logout().then(response => console.log('已登出')); */ logout() { return this.call('LoginService', 'Logout'); }, /** * 檢查 Session 狀態 * @定義位置 Scripts/shared.js line 112 * @returns {Promise} * * @example * $api.checkSession().then(response => { * if (response.IsLoggedIn) { * console.log('已登入:', response.UserName); * } * }); */ checkSession() { return this.call('LoginService', 'CheckSession'); } }; // ========================================== // 訊息服務 // 定義位置:Scripts/shared.js (line 133-208) // ========================================== const MessageService = { container: null, /** * 初始化訊息容器 * @定義位置 Scripts/shared.js line 141 */ initContainer() { if (!this.container) { this.container = document.createElement('div'); this.container.id = 'message-container'; this.container.style.cssText = 'position: fixed; top: 20px; right: 20px; z-index: 1050; pointer-events: none; display: flex; flex-direction: column; gap: 10px; max-width: 400px;'; document.body.appendChild(this.container); } }, /** * 顯示訊息提示 * @定義位置 Scripts/shared.js line 153 * @param {string} text - 訊息文字 * @param {string} type - 訊息類型 (success/danger/warning/info) * @param {number} duration - 顯示時間 (毫秒,預設 3000) * * @example * $message.show('操作成功', 'success', 3000); */ show(text, type, duration = 3000) { this.initContainer(); const alertDiv = document.createElement('div'); alertDiv.className = `alert alert-${type}`; alertDiv.style.cssText = 'position: relative; min-width: 300px; animation: slideIn 0.3s; box-shadow: 0 4px 12px rgba(0,0,0,0.15); margin: 0; pointer-events: auto; padding-right: 40px; display: flex; align-items: center;'; const textSpan = document.createElement('span'); textSpan.textContent = text; textSpan.style.cssText = 'flex: 1;'; alertDiv.appendChild(textSpan); const closeBtn = document.createElement('button'); closeBtn.innerHTML = '×'; closeBtn.style.cssText = 'position: absolute; right: 10px; top: 50%; transform: translateY(-50%); background: none; border: none; font-size: 24px; line-height: 1; cursor: pointer; opacity: 0.5; padding: 0; width: 20px; height: 20px;'; closeBtn.onmouseover = () => closeBtn.style.opacity = '1'; closeBtn.onmouseout = () => closeBtn.style.opacity = '0.5'; closeBtn.onclick = () => this.removeMessage(alertDiv); alertDiv.appendChild(closeBtn); this.container.appendChild(alertDiv); if (duration > 0) { setTimeout(() => { this.removeMessage(alertDiv); }, duration); } }, /** * 移除訊息 * @定義位置 Scripts/shared.js line 193 * @param {HTMLElement} alertDiv - 要移除的訊息元素 */ removeMessage(alertDiv) { if (!alertDiv || !alertDiv.parentNode) return; alertDiv.style.animation = 'slideOut 0.3s'; setTimeout(() => { if (alertDiv.parentNode) { alertDiv.parentNode.removeChild(alertDiv); } }, 300); }, /** * 顯示成功訊息 (綠色) * @定義位置 Scripts/shared.js line 171 * @param {string} text - 訊息文字 * @param {number} duration - 顯示時間 (毫秒) * * @example * $message.success('儲存成功'); */ success(text, duration) { this.show(text, 'success', duration); }, /** * 顯示錯誤訊息 (紅色) * @定義位置 Scripts/shared.js line 183 * @param {string} text - 訊息文字 * @param {number} duration - 顯示時間 (毫秒) * * @example * $message.error('操作失敗'); */ error(text, duration) { this.show(text, 'danger', duration); }, /** * 顯示警告訊息 (黃色) * @定義位置 Scripts/shared.js line 195 * @param {string} text - 訊息文字 * @param {number} duration - 顯示時間 (毫秒) * * @example * $message.warning('請注意'); */ warning(text, duration) { this.show(text, 'warning', duration); }, /** * 顯示資訊訊息 (藍色) * @定義位置 Scripts/shared.js line 207 * @param {string} text - 訊息文字 * @param {number} duration - 顯示時間 (毫秒) * * @example * $message.info('系統提示'); */ info(text, duration) { this.show(text, 'info', duration); } }; // ========================================== // 顯示格式化服務(用於 UI 顯示,不處理資料來源格式轉換) // 定義位置:Scripts/shared.js // ========================================== const FormatService = { /** * 格式化日期(顯示用) * @param {string|Date} dateString - 日期字串或 Date 物件 * @returns {string} "YYYY-MM-DD" * * @example * this.$format.date('2025-06-15T08:30:00'); // "2025-06-15" */ date(dateString) { if (!dateString) return ''; const date = new Date(dateString); if (isNaN(date.getTime())) return ''; const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; }, /** * 格式化時間(顯示用,只取 HH:mm) * @param {string|Date} dateString - 日期字串或 Date 物件 * @returns {string} "HH:mm" * * @example * this.$format.time('2025-06-15T08:30:00'); // "08:30" */ time(dateString) { if (!dateString) return ''; const date = new Date(dateString); if (isNaN(date.getTime())) return ''; const hour = String(date.getHours()).padStart(2, '0'); const minute = String(date.getMinutes()).padStart(2, '0'); return `${hour}:${minute}`; }, /** * 格式化日期時間(顯示用) * @param {string|Date} dateString - 日期字串或 Date 物件 * @returns {string} "YYYY-MM-DD HH:mm:ss" * * @example * this.$format.dateTime('2025-06-15T08:30:00'); // "2025-06-15 08:30:00" */ dateTime(dateString) { if (!dateString) return ''; const date = new Date(dateString); if (isNaN(date.getTime())) return ''; const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); const hour = String(date.getHours()).padStart(2, '0'); const minute = String(date.getMinutes()).padStart(2, '0'); const second = String(date.getSeconds()).padStart(2, '0'); return `${year}-${month}-${day} ${hour}:${minute}:${second}`; }, /** * 格式化日期時間供 input[type="datetime-local"] 使用 * @param {string|Date} dateString - 日期字串或 Date 物件 * @returns {string} "YYYY-MM-DDTHH:mm" * * @example * this.$format.dateTimeForInput('2025-06-15T08:30:00'); // "2025-06-15T08:30" */ dateTimeForInput(dateString) { if (!dateString) return ''; const date = new Date(dateString); if (isNaN(date.getTime())) return ''; const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); const hour = String(date.getHours()).padStart(2, '0'); const minute = String(date.getMinutes()).padStart(2, '0'); return `${year}-${month}-${day}T${hour}:${minute}`; } }; // ========================================== // 資料轉換服務(用於處理 /Date(...)/ 等資料來源格式) // 定義位置:Scripts/shared.js // ========================================== const DataConvertionService = { /** * 格式化日期 * @定義位置 Scripts/shared.js line 231 * @param {string|Date} dateStr - 日期字串或 Date 物件 * @param {string} separator - 日期分隔符,預設為 "-" * @returns {string} 格式化後的日期字串 * * @example * $dataConvert.formatDate('/Date(1764622110737)/', '-'); // "2025-12-31" * $dataConvert.formatDate(new Date(), '/'); // "2025/12/31" */ formatDate(dateStr, separator = '-') { if (!dateStr) return ''; // 處理 ASP.NET ASMX 的日期格式 /Date(1764622110737)/ if (typeof dateStr === 'string' && dateStr.indexOf('/Date(') === 0) { const timestamp = parseInt(dateStr.match(/-?\d+/)[0]); const date = new Date(timestamp); const year = String(date.getFullYear()).padStart(4, '0'); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); return year + separator + month + separator + day; } // 如果已經是一般的 Date 物件或其他格式 const date = new Date(dateStr); if (!isNaN(date.getTime())) { const year = String(date.getFullYear()).padStart(4, '0'); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); return year + separator + month + separator + day; } return dateStr; }, /** * 格式化日期時間 * @定義位置 Scripts/shared.js line 269 * @param {string|Date} dateStr - 日期字串或 Date 物件 * @param {string} separator - 日期分隔符,預設為 "-" * @returns {string} 格式化後的日期時間字串 * * @example * $dataConvert.formatDateTime('/Date(1764622110737)/'); // "2025-12-31 14:35:10" */ formatDateTime(dateStr, separator = '-') { if (!dateStr) return ''; let date; // 處理 ASP.NET ASMX 的日期格式 /Date(1764622110737)/ if (typeof dateStr === 'string' && dateStr.indexOf('/Date(') === 0) { const timestamp = parseInt(dateStr.match(/-?\d+/)[0]); date = new Date(timestamp); } else { date = new Date(dateStr); } if (isNaN(date.getTime())) { return dateStr; } const year = String(date.getFullYear()).padStart(4, '0'); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); const hours = String(date.getHours()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, '0'); const seconds = String(date.getSeconds()).padStart(2, '0'); return year + separator + month + separator + day + ' ' + hours + ':' + minutes + ':' + seconds; }, /** * 遍歷物件或陣列,自動轉換所有符合 ASP.NET ASMX 日期格式的屬性 * @定義位置 Scripts/shared.js line 308 * @param {Object|Array} obj - 要處理的物件或陣列 * @param {string} separator - 日期分隔符,預設為 "-" * @returns {Object|Array} 處理後的物件或陣列 * * @example * const data = { createDate: '/Date(1764622110737)/', items: [{date: '/Date(1764622110737)/'}] }; * $dataConvert.convertDatesInObject(data); // 自動轉換所有日期欄位 */ convertDatesInObject(obj, separator = '-') { if (!obj) return obj; // 處理陣列 if (Array.isArray(obj)) { return obj.map(item => this.convertDatesInObject(item, separator)); } // 處理物件 if (typeof obj === 'object') { const result = {}; for (const key in obj) { if (obj.hasOwnProperty(key)) { const value = obj[key]; // 檢查是否為 ASP.NET ASMX 日期格式 if (typeof value === 'string' && value.indexOf('/Date(') === 0) { result[key] = this.formatDateTime(value, separator); } // 遞迴處理巢狀物件或陣列 else if (value !== null && typeof value === 'object') { result[key] = this.convertDatesInObject(value, separator); } else { result[key] = value; } } } return result; } return obj; } }; // ========================================== // 導航服務 // 定義位置:Scripts/shared.js (line 347-379) // ========================================== const NavigateService = { /** * 導航到指定 URL * @定義位置 Scripts/shared.js line 230 * @param {string} url - 目標 URL * * @example * $navigate.to('/Forms/ActivityList.aspx'); */ to(url) { window.location.href = url; }, /** * 導航到登入頁 * @定義位置 Scripts/shared.js line 241 * * @example * $navigate.toLogin(); */ toLogin() { this.to(`${HOME_URL}Login.aspx`); }, /** * 導航到首頁 * @定義位置 Scripts/shared.js line 251 * * @example * $navigate.toHome(); */ toHome() { this.to(`${HOME_URL}Forms/Home.aspx`); } }; // ========================================== // 初始化動畫樣式 // 定義位置:Scripts/shared.js (line 386-454) // ========================================== (() => { if (!document.getElementById('shared-styles')) { const style = document.createElement('style'); style.id = 'shared-styles'; style.textContent = ` /* Message 動畫 */ @keyframes slideIn { from { transform: translateX(400px); opacity: 0; } to { transform: translateX(0); opacity: 1; } } @keyframes slideOut { from { transform: translateX(0); opacity: 1; } to { transform: translateX(400px); opacity: 0; } } /* Loading 遮罩樣式 */ #api-loading-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0); display: none; justify-content: center; align-items: center; z-index: 9999; } #api-loading-overlay .spinner-container { text-align: center; } #api-loading-overlay .spinner { border: 4px solid rgba(255, 255, 255, 0.3); border-left-color: #c41e3a; border-radius: 50%; width: 50px; height: 50px; animation: spin 1s linear infinite; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } `; document.head.appendChild(style); } })(); // ========================================== // 匯出到全域 // 定義位置:Scripts/shared.js (line 409-430) // ========================================== // 新版 API(建議使用,符合 Vue 慣例) window.$api = ApiService; window.$message = MessageService; window.$format = FormatService; window.$dataConvert = DataConvertionService; window.$navigate = NavigateService; window.$loading = LoadingService; // 舊版 API(向後相容,未來版本可能移除) window.apiCall = ApiService; window.message = MessageService; window.navigate = NavigateService; // ========================================== // Vue Plugin (自動安裝) // 定義位置:Scripts/shared.js (line 433-445) // ========================================== // 延遲註冊 Vue plugin,確保 Vue 已載入 function installVuePlugin() { if (typeof window.Vue !== 'undefined' && window.Vue.prototype) { window.Vue.prototype.$api = ApiService; window.Vue.prototype.$message = MessageService; window.Vue.prototype.$format = FormatService; window.Vue.prototype.$dataConvert = DataConvertionService; window.Vue.prototype.$navigate = NavigateService; window.Vue.prototype.$loading = LoadingService; console.log('[Shared.js] ? Vue plugin installed'); console.log(' Use in Vue: this.$api, this.$message, this.$format, this.$dataConvert, this.$navigate, this.$loading'); return true; } return false; } // 立即嘗試安裝 if (!installVuePlugin()) { // 如果失敗,等待 DOM 載入後再試 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', installVuePlugin); } else { // DOM 已載入,等待一下再試 setTimeout(installVuePlugin, 0); } } console.log('[Shared.js] ? Services loaded from Scripts/shared.js'); console.log(' Global API: window.$api, window.$message, window.$format, window.$dataConvert, window.$navigate, window.$loading'); console.log(' Legacy API: window.apiCall, window.message, window.navigate'); })();