Files
sryang 577060bc78 chore: 首次簽入 Thinkyu ASP.NET 專案
- 加入 Visual Studio / ASP.NET .gitignore
- 排除建置輸出、IDE 設定、NuGet packages、大型 MSI 安裝檔
2026-09-10 09:42:37 +08:00

683 lines
25 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ========================================
// 報到系統 - 共用 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(...)
//
// 向後相容舊 APIapiCall.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 = `
<div class="spinner-container">
<div class="spinner"></div>
</div>
`;
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<LoginResponse>}
*
* @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<LogoutResponse>}
*
* @example
* $api.logout().then(response => console.log('已登出'));
*/
logout() {
return this.call('LoginService', 'Logout');
},
/**
* 檢查 Session 狀態
* @定義位置 Scripts/shared.js line 112
* @returns {Promise<CheckSessionResponse>}
*
* @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 = '&times;';
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');
})();