// ========================================
// 報到系統 - 共用 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');
})();