chore: 首次簽入 Thinkyu ASP.NET 專案

- 加入 Visual Studio / ASP.NET .gitignore
- 排除建置輸出、IDE 設定、NuGet packages、大型 MSI 安裝檔
This commit is contained in:
2026-09-10 09:42:37 +08:00
commit 577060bc78
2496 changed files with 501389 additions and 0 deletions
+650
View File
@@ -0,0 +1,650 @@
// ========================================
// 活動管理 - Vue.js 應用程式
// 檔案位置:Scripts/ClassesList.js
// ========================================
(() => {
'use strict';
new Vue({
el: '#classesApp',
data: {
// API
apiName: 'ClassesService',
// 搜尋條件
search: {
courseCode: '',
courseName: '',
courseDate1: '',
courseDate2: '',
courseLocation: '',
projectID: '',
projectName: '',
closed: ''
},
// 搜尋結果
classes: [],
loading: false,
// 分頁
currentPage: 1,
pageSize: 20,
totalCount: 0,
// 專案自動完成
projectSuggestions: [],
showProjectSuggestions: false,
projectSearchTimeout: null,
// 彈窗
showModal: false,
isNewMode: true,
editItem: {
ID: 0,
CourseCode: '',
CourseName: '',
CourseDate: '',
CourseLocation: '',
StartTime: '',
EndTime: '',
Teacher: '',
projectID: '',
projectName: '',
NotificationTitle: '',
NotificationTemplate: '',
AgreedTerms: '',
Closed: false,
DB_APPNO: 0
},
// 編輯模式專案自動完成
editProjectSuggestions: [],
showEditProjectSuggestions: false,
editProjectSearchTimeout: null,
// 驗證錯誤訊息
validationErrors: {
CourseName: '',
CourseDate: '',
StartTime: '',
EndTime: '',
Teacher: '',
CourseLocation: '',
NotificationTitle: '',
NotificationTemplate: ''
}
},
computed: {
totalPages() {
return Math.ceil(this.totalCount / this.pageSize);
},
generatePreview() {
if (!this.editItem.NotificationTemplate) {
return '';
}
let preview = this.editItem.NotificationTemplate;
// 替換模板變數
preview = preview.replace(/{課程日期}/g, this.editItem.CourseDate ? this.editItem.CourseDate : '(課程日期未設定)');
preview = preview.replace(/{課程名稱}/g, this.editItem.CourseName || '(課程名稱未設定)');
preview = preview.replace(/{課程地點}/g, this.editItem.CourseLocation || '(課程地點未設定)');
preview = preview.replace(/{開始時間}/g, this.editItem.StartTime || '(開始時間未設定)');
preview = preview.replace(/{結束時間}/g, this.editItem.EndTime || '(結束時間未設定)');
preview = preview.replace(/{講師}/g, this.editItem.Teacher || '(講師未設定)');
preview = preview.replace(/{學員姓名}/g, '(學員姓名)');
return preview;
}
},
mounted() {
console.log('[ClassesList] Vue app mounted');
// 初始載入資料
this.performSearch();
},
methods: {
/**
* 執行搜尋
*/
async performSearch() {
this.currentPage = 1;
await this.loadClasses();
},
/**
* 載入活動資料
*/
async loadClasses() {
this.loading = true;
try {
const response = await this.$api.call(this.apiName, 'SearchClasses', {
pageIndex: this.currentPage,
pageSize: this.pageSize,
courseCode: this.search.courseCode,
courseName: this.search.courseName,
courseDate1: this.search.courseDate1 || null,
courseDate2: this.search.courseDate2 || null,
courseLocation: this.search.courseLocation,
projectID: this.search.projectID,
closed: this.search.closed
});
if (response.Success) {
this.classes = (response.Data || []);
this.totalCount = response.TotalCount || 0;
console.log('[ClassesList] 載入成功,共 ' + this.totalCount + ' 筆');
} else {
this.$message.error(response.Message || '查詢失敗');
this.classes = [];
this.totalCount = 0;
}
} catch (error) {
console.error('[ClassesList] 載入錯誤:', error);
this.$message.error('載入資料發生錯誤');
this.classes = [];
this.totalCount = 0;
} finally {
this.loading = false;
}
},
/**
* 清除搜尋條件
*/
clearSearch() {
this.search = {
courseCode: '',
courseName: '',
courseDate1: '',
courseDate2: '',
courseLocation: '',
projectID: '',
projectName: '',
closed: ''
};
this.projectSuggestions = [];
this.showProjectSuggestions = false;
this.performSearch();
},
/**
* 換頁
*/
goToPage(page) {
if (page < 1 || page > this.totalPages) return;
this.currentPage = page;
this.loadClasses();
},
/**
* 格式化專案資訊 (編號 - 名稱)
*/
formatProjectInfo(item) {
if (!item.ProjectID) return '';
return item.ProjectName
? `${item.ProjectID} - ${item.ProjectName}`
: item.ProjectID;
},
/**
* 開啟新增彈窗
*/
openAddModal() {
this.isNewMode = true;
this.editItem = {
ID: 0,
CourseCode: '(自動產生)',
CourseName: '',
CourseDate: '',
CourseLocation: '',
StartTime: '',
EndTime: '',
Teacher: '',
projectID: '',
projectName: '',
NotificationTitle: '',
NotificationTemplate: '',
AgreedTerms: '',
Closed: false,
DB_APPNO: 0
};
this.editProjectSuggestions = [];
this.showEditProjectSuggestions = false;
this.clearValidationErrors();
this.showModal = true;
},
/**
* 開啟更正彈窗
*/
openEditModal(item) {
this.isNewMode = false;
const projectDisplay = item.ProjectID && item.ProjectName
? `${item.ProjectID} - ${item.ProjectName}`
: (item.ProjectID || '');
this.editItem = {
ID: item.ID,
CourseCode: item.CourseCode,
CourseName: item.CourseName,
CourseDate: this.$format.date(item.CourseDate),
CourseLocation: item.CourseLocation || '',
StartTime: item.StartTime || '',
EndTime: item.EndTime || '',
Teacher: item.Teacher || '',
projectID: item.ProjectID || '',
projectName: projectDisplay,
NotificationTitle: item.NotificationTitle || '',
NotificationTemplate: item.NotificationTemplate || '',
AgreedTerms: item.AgreedTerms || '',
Closed: item.Closed,
DB_APPNO: item.DB_APPNO
};
this.editProjectSuggestions = [];
this.showEditProjectSuggestions = false;
this.clearValidationErrors();
this.showModal = true;
},
/**
* 關閉彈窗
*/
closeModal() {
this.showModal = false;
},
/**
* 插入模板文字
*/
insertTemplate(text) {
const textarea = this.$refs.templateTextarea;
if (!textarea) return;
const startPos = textarea.selectionStart;
const endPos = textarea.selectionEnd;
const currentValue = this.editItem.NotificationTemplate || '';
// 在游標位置插入文字
this.editItem.NotificationTemplate =
currentValue.substring(0, startPos) +
text +
currentValue.substring(endPos);
// 設定游標位置
this.$nextTick(() => {
textarea.selectionStart = textarea.selectionEnd = startPos + text.length;
textarea.focus();
});
},
/**
* 清除驗證錯誤訊息
*/
clearValidationErrors() {
this.validationErrors = {
CourseName: '',
CourseDate: '',
StartTime: '',
EndTime: '',
Teacher: '',
CourseLocation: '',
NotificationTitle: '',
NotificationTemplate: ''
};
},
/**
* 搜尋專案 (自動完成)
*/
searchProjects() {
// 清除之前的超時
if (this.projectSearchTimeout) {
clearTimeout(this.projectSearchTimeout);
}
const keyword = this.search.projectName.trim();
// 如果輸入為空,清空建議和ProjectID
if (!keyword) {
this.projectSuggestions = [];
this.search.projectID = '';
return;
}
// 延遲搜尋以避免過多API呼叫
this.projectSearchTimeout = setTimeout(async () => {
try {
const response = await this.$api.call(this.apiName, 'SearchProjects', {
keyword: keyword
});
if (response.Success) {
this.projectSuggestions = response.Data || [];
this.showProjectSuggestions = this.projectSuggestions.length > 0;
} else {
this.projectSuggestions = [];
this.showProjectSuggestions = false;
}
} catch (error) {
console.error('[ClassesList] 搜尋專案錯誤:', error);
this.projectSuggestions = [];
this.showProjectSuggestions = false;
}
}, 300);
},
/**
* 選擇專案
*/
selectProject(suggestion) {
this.search.projectID = suggestion.Value;
this.search.projectName = `${suggestion.Value} - ${suggestion.Name}`;
this.projectSuggestions = [];
this.showProjectSuggestions = false;
},
/**
* 隱藏專案建議 (延遲隱藏以允許點擊)
*/
hideProjectSuggestions() {
setTimeout(() => {
this.showProjectSuggestions = false;
}, 200);
},
/**
* 搜尋編輯模式的專案 (自動完成)
*/
searchEditProjectSuggestions() {
// 清除之前的超時
if (this.editProjectSearchTimeout) {
clearTimeout(this.editProjectSearchTimeout);
}
const keyword = this.editItem.projectName.trim();
// 如果輸入為空,清空建議和ProjectID
if (!keyword) {
this.editProjectSuggestions = [];
this.editItem.projectID = '';
return;
}
// 延遲搜尋以避免過多API呼叫
this.editProjectSearchTimeout = setTimeout(async () => {
try {
const response = await this.$api.call(this.apiName, 'SearchProjects', {
keyword: keyword
});
if (response.Success) {
this.editProjectSuggestions = response.Data || [];
this.showEditProjectSuggestions = this.editProjectSuggestions.length > 0;
} else {
this.editProjectSuggestions = [];
this.showEditProjectSuggestions = false;
}
} catch (error) {
console.error('[ClassesList] 搜尋編輯模式專案錯誤:', error);
this.editProjectSuggestions = [];
this.showEditProjectSuggestions = false;
}
}, 300);
},
/**
* 選擇編輯模式的專案
*/
selectEditProject(suggestion) {
this.editItem.projectID = suggestion.Value;
this.editItem.projectName = `${suggestion.Value} - ${suggestion.Name}`;
this.editProjectSuggestions = [];
this.showEditProjectSuggestions = false;
},
/**
* 隱藏編輯模式的專案建議 (延遲隱藏以允許點擊)
*/
hideEditProjectSuggestions() {
setTimeout(() => {
this.showEditProjectSuggestions = false;
}, 200);
},
/**
* 驗證輸入
*/
validateInput() {
this.clearValidationErrors();
let isValid = true;
// 驗證課程名稱
if (!this.editItem.CourseName || this.editItem.CourseName.trim() === '') {
this.validationErrors.CourseName = '課程名稱為必填欄位';
isValid = false;
} else if (this.editItem.CourseName.length > 100) {
this.validationErrors.CourseName = '課程名稱最長 100 個字元';
isValid = false;
}
// 驗證課程日期
if (!this.editItem.CourseDate) {
this.validationErrors.CourseDate = '課程日期為必填欄位';
isValid = false;
}
// 驗證開始時間
if (!this.editItem.StartTime || this.editItem.StartTime.trim() === '') {
this.validationErrors.StartTime = '開始時間為必填欄位';
isValid = false;
}
// 驗證結束時間
if (!this.editItem.EndTime || this.editItem.EndTime.trim() === '') {
this.validationErrors.EndTime = '結束時間為必填欄位';
isValid = false;
}
// 驗證講師
if (!this.editItem.Teacher || this.editItem.Teacher.trim() === '') {
this.validationErrors.Teacher = '講師為必填欄位';
isValid = false;
}
// 驗證通知標題
if (this.editItem.NotificationTitle && this.editItem.NotificationTitle.length > 100) {
this.validationErrors.NotificationTitle = '通知標題最長 100 個字元';
isValid = false;
}
// 驗證通知模板
if (this.editItem.NotificationTemplate && this.editItem.NotificationTemplate.length > 500) {
this.validationErrors.NotificationTemplate = '通知模板最長 500 個字元';
isValid = false;
}
return isValid;
},
/**
* 儲存活動
*/
async saveClasses() {
if (!this.validateInput()) return;
try {
// 準備資料
const item = {
ID: this.editItem.ID,
CourseCode: this.editItem.CourseCode,
CourseName: this.editItem.CourseName.trim(),
CourseDate: this.editItem.CourseDate,
CourseLocation: this.editItem.CourseLocation,
StartTime: this.editItem.StartTime,
EndTime: this.editItem.EndTime,
Teacher: this.editItem.Teacher,
ProjectID: this.editItem.projectID,
NotificationTitle: this.editItem.NotificationTitle,
NotificationTemplate: this.editItem.NotificationTemplate,
AgreedTerms: this.editItem.AgreedTerms,
Closed: this.editItem.Closed,
DB_APPNO: this.editItem.DB_APPNO
};
const response = await this.$api.call(this.apiName, 'SaveClasses', {
isNew: this.isNewMode,
item: item
});
if (response.Success) {
this.$message.success(response.Message || '儲存成功');
this.closeModal();
this.loadClasses();
} else {
this.$message.error(response.Message || '儲存失敗');
}
} catch (error) {
console.error('[ClassesList] 儲存錯誤:', error);
this.$message.error('儲存發生錯誤');
}
},
/**
* 儲存後繼續新增
*/
async saveAndContinue() {
if (!this.validateInput()) return;
try {
// 準備資料
const item = {
ID: 0,
CourseCode: '',
CourseName: this.editItem.CourseName.trim(),
CourseDate: this.editItem.CourseDate,
CourseLocation: this.editItem.CourseLocation,
StartTime: this.editItem.StartTime,
EndTime: this.editItem.EndTime,
Teacher: this.editItem.Teacher,
ProjectID: this.editItem.projectID,
NotificationTitle: this.editItem.NotificationTitle,
NotificationTemplate: this.editItem.NotificationTemplate,
AgreedTerms: this.editItem.AgreedTerms,
Closed: this.editItem.Closed,
DB_APPNO: 0
};
const response = await this.$api.call(this.apiName, 'SaveClasses', {
isNew: true,
item: item
});
if (response.Success) {
this.$message.success(response.Message || '儲存成功');
// 清除輸入內容,重新進入新增模式
this.editItem = {
ID: 0,
CourseCode: '(自動產生)',
CourseName: '',
CourseDate: '',
CourseLocation: '',
StartTime: '',
EndTime: '',
Teacher: '',
projectID: '',
projectName: '',
NotificationTitle: '',
NotificationTemplate: '',
AgreedTerms: '',
Closed: false,
DB_APPNO: 0
};
this.loadClasses();
} else {
this.$message.error(response.Message || '儲存失敗');
}
} catch (error) {
console.error('[ClassesList] 儲存錯誤:', error);
this.$message.error('儲存發生錯誤');
}
},
/**
* 刪除活動
*/
async deleteClasses(item) {
if (!confirm(`確定要刪除活動「${item.CourseName}」嗎?`)) {
return;
}
try {
const response = await this.$api.call(this.apiName, 'DeleteClasses', {
id: item.ID,
dbAppNo: item.DB_APPNO
});
if (response.Success) {
this.$message.success(response.Message || '刪除成功');
this.loadClasses();
} else {
this.$message.error(response.Message || '刪除失敗');
}
} catch (error) {
console.error('[ClassesList] 刪除錯誤:', error);
this.$message.error('刪除發生錯誤');
}
},
/**
* 匯出活動
*/
async performExport() {
try {
// 驗證必要條件:課程日期(起)和課程日期(迄)為必填
if (!this.search.courseDate1 || !this.search.courseDate2) {
this.$message.warning('課程日期(起)和課程日期(迄)為必填欄位');
return;
}
const response = await this.$api.call(this.apiName, 'ExportClassesList', {
courseDate1: this.search.courseDate1 || null,
courseDate2: this.search.courseDate2 || null,
projectID: this.search.projectID || null
});
if (response.Success) {
// 取得檔案資訊
const fileData = response.Data;
const fileName = fileData.FileName;
const fileBase64 = fileData.FileBase64;
const fileSize = fileData.FileSize;
// 轉換 Base64 為 Blob
const binaryString = atob(fileBase64);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
const blob = new Blob([bytes], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
// 建立下載連結
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = fileName;
// 執行下載
document.body.appendChild(link);
link.click();
// 清理資源
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
this.$message.success(`匯出成功 (${(fileSize / 1024).toFixed(2)} KB)`);
console.log('[ClassesList] 匯出成功:', fileName);
} else {
this.$message.error(response.Message || '匯出失敗');
}
} catch (error) {
console.error('[ClassesList] 匯出錯誤:', error);
this.$message.error('匯出發生錯誤');
}
}
}
});
})();
+256
View File
@@ -0,0 +1,256 @@
// 課程報表 - Vue.js 應用程式
// 檔案位置:Scripts/ClassesReport.js
// ========================================
(() => {
'use strict';
new Vue({
el: '#classesReportApp',
data: {
// API
apiName: 'ClassesService',
// 搜尋條件
search: {
courseCode: '',
courseName: '',
courseDate1: '',
courseDate2: '',
courseLocation: '',
projectID: '',
projectName: '',
closed: '',
timeFormat: 'datetime'
},
// 搜尋結果
classes: [],
loading: false,
// 分頁
currentPage: 1,
pageSize: 20,
totalCount: 0,
// 專案自動完成
projectSuggestions: [],
showProjectSuggestions: false,
projectSearchTimeout: null,
// 匯出
exporting: false,
exportingId: 0
},
computed: {
totalPages() {
return Math.ceil(this.totalCount / this.pageSize);
}
},
mounted() {
console.log('[ClassesReport] Vue app mounted');
// 初始載入資料
this.performSearch();
},
methods: {
/**
* 執行搜尋
*/
async performSearch() {
this.currentPage = 1;
await this.fetchClasses();
},
/**
* 查詢課程列表
*/
async fetchClasses() {
this.loading = true;
try {
const response = await this.$api.call(this.apiName, 'SearchClasses', {
pageIndex: this.currentPage,
pageSize: this.pageSize,
courseCode: this.search.courseCode,
courseName: this.search.courseName,
courseDate1: this.search.courseDate1 || null,
courseDate2: this.search.courseDate2 || null,
courseLocation: this.search.courseLocation,
projectID: this.search.projectID,
closed: this.search.closed === '' ? null : this.search.closed === 'true'
});
if (response.Success) {
this.classes = response.Data || [];
this.totalCount = response.TotalCount || 0;
console.log('[ClassesReport] 載入成功,共 ' + this.totalCount + ' 筆');
} else {
this.$message.error(response.Message || '查詢失敗');
this.classes = [];
this.totalCount = 0;
}
} catch (error) {
console.error('[ClassesReport] 載入錯誤:', error);
this.$message.error('載入資料發生錯誤');
this.classes = [];
this.totalCount = 0;
} finally {
this.loading = false;
}
},
/**
* 清除搜尋條件
*/
clearSearch() {
this.search = {
courseCode: '',
courseName: '',
courseDate1: '',
courseDate2: '',
courseLocation: '',
projectID: '',
projectName: '',
closed: '',
timeFormat: 'datetime'
};
this.performSearch();
},
/**
* 搜尋專案
*/
searchProjects(event) {
const query = this.search.projectName.trim();
if (query.length === 0) {
this.projectSuggestions = [];
return;
}
clearTimeout(this.projectSearchTimeout);
this.projectSearchTimeout = setTimeout(async () => {
try {
const response = await this.$api.call(this.apiName, 'SearchProjects', {
keyword: query
});
if (response.Success) {
this.projectSuggestions = response.Data || [];
} else {
this.projectSuggestions = [];
}
} catch (error) {
console.error('[ClassesReport] 搜尋專案出錯:', error);
this.projectSuggestions = [];
}
}, 300);
},
/**
* 選擇專案
*/
selectProject(project) {
this.search.projectID = project.Value;
this.search.projectName = project.Value + ' ' + project.Name;
this.projectSuggestions = [];
this.showProjectSuggestions = false;
},
/**
* 隱藏專案建議
*/
hideProjectSuggestions() {
setTimeout(() => {
this.showProjectSuggestions = false;
}, 200);
},
/**
* 上一頁
*/
previousPage() {
if (this.currentPage > 1) {
this.currentPage--;
this.fetchClasses();
}
},
/**
* 下一頁
*/
nextPage() {
if (this.currentPage < this.totalPages) {
this.currentPage++;
this.fetchClasses();
}
},
/**
* 匯出報表
*/
async exportReport(courseID) {
if (this.exporting) {
return;
}
this.exporting = true;
this.exportingId = courseID;
try {
const response = await this.$api.call(this.apiName, 'ExportClassesReport', {
classesID: courseID,
timeFormat: this.search.timeFormat
});
if (response.Success) {
// 下載檔案
const fileBase64 = response.Data.FileBase64;
const fileName = response.Data.FileName;
const byteCharacters = atob(fileBase64);
const byteNumbers = new Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
const blob = new Blob([byteArray], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
// 建立下載連結
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = fileName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
this.$message.success('匯出成功');
} else {
this.$message.error('匯出失敗:' + response.Message);
}
} catch (error) {
console.error('[ClassesReport] 匯出報表出錯:', error);
this.$message.error('匯出報表出錯:' + error.message);
} finally {
this.exporting = false;
this.exportingId = 0;
}
},
/**
* 呼叫後端 WebService
*/
async callWebService(methodName, params) {
const url = `${this.apiName}.asmx/${methodName}`;
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(params)
};
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
}
}
});
})();
@@ -0,0 +1,528 @@
(() => {
'use strict';
new Vue({
el: '#messageAccountManageApp',
data: {
// API
apiName: 'MessageAccountManage',
// 搜尋條件
search: {
projectName: '',
platformCode: ''
},
// 搜尋結果
accountList: [],
loading: false,
// 分頁
currentPage: 1,
pageSize: 10,
totalCount: 0,
// 搜尋專案自動完成
projectSuggestions: [],
showProjectSuggestions: false,
projectSearchTimeout: null,
// 彈窗
showModal: false,
isNewMode: true,
editItem: {
ID: 0,
ProjectID: '',
MMSAccount: '',
MMSPassword: '',
PlatformCode: '',
projectName: '',
DB_APPNO: 0
},
// 編輯模式專案自動完成
editProjectSuggestions: [],
showEditProjectSuggestions: false,
editProjectSearchTimeout: null,
// 驗證錯誤訊息
validationErrors: {
ProjectID: '',
MMSAccount: '',
MMSPassword: ''
},
// 編輯狀態
showPassword: false,
// 平台
platforms: [],
currentPlatform: null,
showPlatformSettings: false,
showSwitchPanel: false,
switchPlatformCode: ''
},
computed: {
totalPages() {
return Math.ceil(this.totalCount / this.pageSize);
}
},
mounted() {
console.log('[MessageAccountManage] Vue app mounted');
// 初始載入資料
this.performSearch();
this.loadPlatforms();
this.loadCurrentPlatform();
},
methods: {
/**
* 依 PlatformCode 取得平台顯示名稱
*/
getPlatformName(platformCode) {
if (!platformCode) return '系統預設';
const p = this.platforms.find(function(x) { return x.PlatformCode === platformCode; });
return p ? p.PlatformName + '' + p.PlatformCode + '' : platformCode;
},
/**
* 執行搜尋
*/
async performSearch() {
this.currentPage = 1;
await this.loadAccounts();
},
/**
* 載入簡訊帳號資料
*/
async loadAccounts() {
this.loading = true;
try {
const response = await this.$api.call(this.apiName, 'Search', {
pageNo: this.currentPage,
pageSize: this.pageSize,
projectID: this.search.projectID || '',
platformCode: this.search.platformCode || ''
});
if (response.Success) {
this.accountList = response.Data || [];
this.totalCount = response.TotalCount || 0;
// 初始化密碼隱藏狀態 (使用 $set 確保響應式)
this.accountList.forEach(item => {
this.$set(item, 'showPassword', false);
});
console.log('[MessageAccountManage] 載入成功,共 ' + this.totalCount + ' 筆');
} else {
this.$message.error(response.Message || '查詢失敗');
this.accountList = [];
this.totalCount = 0;
}
} catch (error) {
console.error('[MessageAccountManage] 載入錯誤:', error);
this.$message.error('載入資料發生錯誤');
this.accountList = [];
this.totalCount = 0;
} finally {
this.loading = false;
}
},
/**
* 清除搜尋條件
*/
clearSearch() {
this.search = {
projectName: '',
platformCode: ''
};
this.projectSuggestions = [];
this.showProjectSuggestions = false;
this.performSearch();
},
/**
* 搜尋計畫 (搜尋區自動完成)
*/
searchProjects() {
// 清除之前的超時
if (this.projectSearchTimeout) {
clearTimeout(this.projectSearchTimeout);
}
const keyword = this.search.projectName.trim();
// 如果輸入為空,清空建議和ProjectID
if (!keyword) {
this.projectSuggestions = [];
this.search.projectID = '';
return;
}
// 延遲搜尋以避免過多API呼叫
this.projectSearchTimeout = setTimeout(async () => {
try {
const response = await this.$api.call(this.apiName, 'SearchProjects', {
keyword: keyword
});
if (response.Success) {
this.projectSuggestions = response.Data || [];
this.showProjectSuggestions = this.projectSuggestions.length > 0;
} else {
this.projectSuggestions = [];
this.showProjectSuggestions = false;
}
} catch (error) {
console.error('[MessageAccountManage] 搜尋計畫錯誤:', error);
this.projectSuggestions = [];
this.showProjectSuggestions = false;
}
}, 300);
},
/**
* 選擇計畫 (搜尋區)
*/
selectProject(suggestion) {
this.search.projectID = suggestion.Value;
this.search.projectName = `${suggestion.Value} - ${suggestion.Name}`;
this.projectSuggestions = [];
this.showProjectSuggestions = false;
},
/**
* 隱藏計畫建議 (搜尋區 - 延遲隱藏以允許點擊)
*/
hideProjectSuggestions() {
setTimeout(() => {
this.showProjectSuggestions = false;
}, 200);
},
/**
* 搜尋編輯模式的計畫 (自動完成)
*/
searchEditProjectSuggestions() {
// 清除之前的超時
if (this.editProjectSearchTimeout) {
clearTimeout(this.editProjectSearchTimeout);
}
const keyword = this.editItem.projectName.trim();
// 如果輸入為空,清空建議和ProjectID
if (!keyword) {
this.editProjectSuggestions = [];
this.editItem.ProjectID = '';
return;
}
// 延遲搜尋以避免過多API呼叫
this.editProjectSearchTimeout = setTimeout(async () => {
try {
const response = await this.$api.call(this.apiName, 'SearchProjects', {
keyword: keyword
});
if (response.Success) {
this.editProjectSuggestions = response.Data || [];
this.showEditProjectSuggestions = this.editProjectSuggestions.length > 0;
} else {
this.editProjectSuggestions = [];
this.showEditProjectSuggestions = false;
}
} catch (error) {
console.error('[MessageAccountManage] 搜尋編輯模式計畫錯誤:', error);
this.editProjectSuggestions = [];
this.showEditProjectSuggestions = false;
}
}, 300);
},
/**
* 選擇編輯模式的計畫
*/
selectEditProject(suggestion) {
this.editItem.ProjectID = suggestion.Value;
this.editItem.projectName = `${suggestion.Value} - ${suggestion.Name}`;
this.editProjectSuggestions = [];
this.showEditProjectSuggestions = false;
},
/**
* 隱藏編輯模式的計畫建議 (延遲隱藏以允許點擊)
*/
hideEditProjectSuggestions() {
setTimeout(() => {
this.showEditProjectSuggestions = false;
}, 200);
},
/**
* 換頁
*/
goToPage(page) {
if (page < 1 || page > this.totalPages) return;
this.currentPage = page;
this.loadAccounts();
},
/**
* 上一頁
*/
previousPage() {
if (this.currentPage > 1) {
this.goToPage(this.currentPage - 1);
}
},
/**
* 下一頁
*/
nextPage() {
if (this.currentPage < this.totalPages) {
this.goToPage(this.currentPage + 1);
}
},
/**
* 開啟新增彈窗
*/
openAddModal() {
this.isNewMode = true;
this.editItem = {
ID: 0,
ProjectID: '',
MMSAccount: '',
MMSPassword: '',
PlatformCode: this.currentPlatform ? this.currentPlatform.PlatformCode : '',
projectName: '',
DB_APPNO: 0
};
this.showEditProjectSuggestions = false;
this.showPassword = false;
this.clearValidationErrors();
this.showModal = true;
},
/**
* 開啟編輯彈窗
*/
openEditModal(item) {
this.isNewMode = false;
this.editItem = {
ID: item.ID,
ProjectID: item.ProjectID,
MMSAccount: item.MMSAccount,
MMSPassword: item.MMSPassword,
PlatformCode: item.PlatformCode || '',
projectName: item.ProjectName ? `${item.ProjectID} - ${item.ProjectName}` : item.ProjectID,
DB_APPNO: item.DB_APPNO
};
this.editProjectSuggestions = [];
this.showEditProjectSuggestions = false;
this.showPassword = false;
this.clearValidationErrors();
this.showModal = true;
},
/**
* 關閉彈窗
*/
closeModal() {
this.showModal = false;
},
/**
* 清除驗證錯誤訊息
*/
clearValidationErrors() {
this.validationErrors = {
ProjectID: '',
MMSAccount: '',
MMSPassword: ''
};
},
/**
* 驗證輸入
*/
validateInput() {
this.clearValidationErrors();
let isValid = true;
// 驗證計畫代號
if (!this.editItem.ProjectID || this.editItem.ProjectID.trim() === '') {
this.validationErrors.ProjectID = '計畫代號為必填欄位';
isValid = false;
} else if (this.editItem.ProjectID.length > 20) {
this.validationErrors.ProjectID = '計畫代號最長 20 個字元';
isValid = false;
}
// 驗證簡訊帳號
if (!this.editItem.MMSAccount || this.editItem.MMSAccount.trim() === '') {
this.validationErrors.MMSAccount = '簡訊帳號為必填欄位';
isValid = false;
} else if (this.editItem.MMSAccount.length > 50) {
this.validationErrors.MMSAccount = '簡訊帳號最長 50 個字元';
isValid = false;
}
// 驗證簡訊密碼
if (!this.editItem.MMSPassword || this.editItem.MMSPassword.trim() === '') {
this.validationErrors.MMSPassword = '簡訊密碼為必填欄位';
isValid = false;
} else if (this.editItem.MMSPassword.length > 50) {
this.validationErrors.MMSPassword = '簡訊密碼最長 50 個字元';
isValid = false;
}
return isValid;
},
/**
* 儲存簡訊帳號
*/
async saveAccount() {
if (!this.validateInput()) return;
try {
// 準備資料
const item = {
ID: this.editItem.ID,
ProjectID: this.editItem.ProjectID.trim(),
MMSAccount: this.editItem.MMSAccount.trim(),
MMSPassword: this.editItem.MMSPassword.trim(),
PlatformCode: this.editItem.PlatformCode.trim(),
DB_APPNO: this.editItem.DB_APPNO
};
const response = await this.$api.call(this.apiName, 'Save', {
isNew: this.isNewMode,
item: item
});
if (response.Success) {
this.$message.success(response.Message || '儲存成功');
this.closeModal();
this.loadAccounts();
} else {
this.$message.error(response.Message || '儲存失敗');
}
} catch (error) {
console.error('[MessageAccountManage] 儲存錯誤:', error);
this.$message.error('儲存發生錯誤');
}
},
/**
* 刪除簡訊帳號
*/
async deleteAccount(item) {
if (!confirm(`確定要刪除計畫「${item.ProjectID}」的簡訊帳號設定嗎?`)) {
return;
}
try {
const response = await this.$api.call(this.apiName, 'Delete', {
id: item.ID,
dbAppNo: item.DB_APPNO
});
if (response.Success) {
this.$message.success(response.Message || '刪除成功');
this.loadAccounts();
} else {
this.$message.error(response.Message || '刪除失敗');
}
} catch (error) {
console.error('[MessageAccountManage] 刪除錯誤:', error);
this.$message.error('刪除發生錯誤');
}
},
/**
* 切換密碼顯示/隱藏
*/
togglePasswordDisplay(item) {
this.$set(item, 'showPassword', !item.showPassword);
},
/**
* 切換編輯模式密碼顯示/隱藏
*/
toggleEditPasswordDisplay() {
this.showPassword = !this.showPassword;
},
/**
* 載入所有平台清單
*/
async loadPlatforms() {
try {
const res = await this.$api.call(this.apiName, 'GetPlatforms', {});
if (res.Success && res.Data) {
this.platforms = res.Data.map(p => Object.assign({ showPwd: false }, p));
if (!this.switchPlatformCode && this.platforms.length > 0) {
this.switchPlatformCode = this.platforms[0].PlatformCode;
}
}
} catch (e) {
console.error('[loadPlatforms]', e);
}
},
/**
* 載入目前預設平台
*/
async loadCurrentPlatform() {
try {
const res = await this.$api.call(this.apiName, 'GetCurrentPlatform', {});
if (res.Success) this.currentPlatform = res.Data;
} catch (e) {
console.error('[loadCurrentPlatform]', e);
}
},
/**
* 儲存單一平台帳號設定
*/
async savePlatformSetting(platform) {
try {
const res = await this.$api.call(this.apiName, 'SavePlatform', {
item: {
ID: platform.ID,
PlatformCode: platform.PlatformCode,
PlatformName: platform.PlatformName,
Account: platform.Account,
Password: platform.Password,
Remark: platform.Remark
}
});
if (res.Success) {
this.$message.success(res.Message || '儲存成功');
} else {
this.$message.error(res.Message || '儲存失敗');
}
} catch (e) {
console.error('[savePlatformSetting]', e);
this.$message.error('儲存發生錯誤');
}
},
/**
* 切換預設平台
*/
async setCurrentPlatform() {
try {
const res = await this.$api.call(this.apiName, 'SetCurrentPlatform', { platformCode: this.switchPlatformCode });
if (res.Success) {
this.$message.success(res.Message || '切換成功');
this.showSwitchPanel = false;
await this.loadCurrentPlatform();
} else {
this.$message.error(res.Message || '切換失敗');
}
} catch (e) {
console.error('[setCurrentPlatform]', e);
this.$message.error('切換發生錯誤');
}
}
}
});
})();
+195
View File
@@ -0,0 +1,195 @@
// ========================================
// 群組人員管理 - Vue.js 應用
// 檔案位置:Scripts/RoleManage.js
// ========================================
(function() {
'use strict';
// 等待 DOM 載入完成
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initRoleManageApp);
} else {
initRoleManageApp();
}
function initRoleManageApp() {
new Vue({
el: '#roleManageApp',
data: {
groupList: [
{ id: 'SG01', name: '系統管理群組', description: '擁有系統最高權限,可管理所有功能' },
{ id: 'SG02', name: '客戶管理群組', description: '可管理活動及參加人員' },
{ id: 'SG03', name: '現場工作群組', description: '可進行現場報到作業' },
{ id: 'SG04', name: '查詢群組', description: '僅可查詢報到資料' }
],
selectedGroup: null,
groupMembers: [],
searchKeyword: '',
searchResults: [],
selectedMembers: [],
selectedEmployees: [],
loadingMembers: false,
loadingSearch: false
},
mounted: function() {
console.log('[RoleManage.js] Vue instance mounted');
if (this.groupList.length > 0) {
this.selectGroup(this.groupList[0]);
}
},
methods: {
selectGroup: async function(group) {
this.selectedGroup = group;
this.selectedMembers = [];
await this.loadGroupMembers();
},
loadGroupMembers: async function() {
if (!this.selectedGroup) return;
this.loadingMembers = true;
try {
var response = await this.$api.call('RoleManageService', 'GetGroupMembers', {
groupID: this.selectedGroup.id
});
if (response.Success) {
this.groupMembers = response.Data || [];
console.log('[RoleManage] 載入群組成員: ' + this.groupMembers.length + ' 位');
} else {
this.$message.error(response.Message);
this.groupMembers = [];
}
} catch (error) {
console.error('[RoleManage] 載入群組成員失敗:', error);
this.$message.error('載入群組成員失敗');
this.groupMembers = [];
} finally {
this.loadingMembers = false;
}
},
searchEmployees: async function() {
if (!this.searchKeyword.trim()) {
this.$message.warning('請輸入搜尋關鍵字');
return;
}
this.loadingSearch = true;
this.selectedEmployees = [];
try {
var response = await this.$api.call('RoleManageService', 'SearchEmployees', {
keyword: this.searchKeyword
});
if (response.Success) {
this.searchResults = response.Data || [];
console.log('[RoleManage] 搜尋結果: ' + this.searchResults.length + ' 位');
} else {
this.$message.error(response.Message);
this.searchResults = [];
}
} catch (error) {
console.error('[RoleManage] 搜尋員工失敗:', error);
this.$message.error('搜尋員工失敗');
this.searchResults = [];
} finally {
this.loadingSearch = false;
}
},
addEmployeesToGroup: async function() {
if (!this.selectedGroup) {
this.$message.warning('請先選擇群組');
return;
}
if (this.selectedEmployees.length === 0) {
this.$message.warning('請選擇要加入的員工');
return;
}
try {
var response = await this.$api.call('RoleManageService', 'AddMemberToGroup', {
groupID: this.selectedGroup.id,
employeeIDs: this.selectedEmployees
});
if (response.Success) {
this.$message.success(response.Message);
this.selectedEmployees = [];
await this.loadGroupMembers();
await this.searchEmployees();
} else {
this.$message.error(response.Message);
}
} catch (error) {
console.error('[RoleManage] 加入群組失敗:', error);
this.$message.error('加入群組失敗');
}
},
removeMembersFromGroup: async function() {
if (!this.selectedGroup) {
this.$message.warning('請先選擇群組');
return;
}
if (this.selectedMembers.length === 0) {
this.$message.warning('請選擇要移除的成員');
return;
}
if (!confirm('確定要從「' + this.selectedGroup.name + '」移除 ' + this.selectedMembers.length + ' 位成員嗎?')) {
return;
}
try {
var response = await this.$api.call('RoleManageService', 'RemoveMemberFromGroup', {
groupID: this.selectedGroup.id,
employeeIDs: this.selectedMembers
});
if (response.Success) {
this.$message.success(response.Message);
this.selectedMembers = [];
await this.loadGroupMembers();
} else {
this.$message.error(response.Message);
}
} catch (error) {
console.error('[RoleManage] 移除群組失敗:', error);
this.$message.error('移除群組失敗');
}
},
toggleMemberSelection: function(employeeId) {
var index = this.selectedMembers.indexOf(employeeId);
if (index > -1) {
this.selectedMembers.splice(index, 1);
} else {
this.selectedMembers.push(employeeId);
}
},
toggleEmployeeSelection: function(employeeId) {
var index = this.selectedEmployees.indexOf(employeeId);
if (index > -1) {
this.selectedEmployees.splice(index, 1);
} else {
this.selectedEmployees.push(employeeId);
}
},
isSelectedMember: function(employeeId) {
return this.selectedMembers.indexOf(employeeId) > -1;
},
isSelectedEmployee: function(employeeId) {
return this.selectedEmployees.indexOf(employeeId) > -1;
}
}
});
}
})();
+184
View File
@@ -0,0 +1,184 @@
// ========================================
// 匯出參加人員 - Vue.js 應用程式
// 檔案位置:Scripts/SignUpExport.js
// ========================================
(() => {
'use strict';
// 檔案下載工具 - 使用後端生成的 Base64
const FileDownloader = {
downloadFromBase64: function(base64String, fileName) {
const link = document.createElement('a');
link.href = 'data:application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;base64,' + base64String;
link.download = fileName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
};
new Vue({
el: '#signUpExportApp',
data: {
// API
apiNameClasses: 'ClassesService',
apiNameSignUp: 'SignUpService',
// 活動列表
courses: [],
selectedCourseID: '',
// 包含已結案
includeClosed: false,
// 簽到退時間格式
timeFormat: 'datetime',
// 匯出資料
signupData: null,
exporting: false,
exportMessage: '',
exportSuccess: false
},
watch: {
selectedCourseID: function(newVal) {
if (newVal) {
this.loadExportData();
} else {
this.signupData = null;
}
},
includeClosed() {
this.loadCourses();
},
timeFormat() {
if (this.selectedCourseID) {
this.loadExportData();
}
}
},
mounted() {
console.log('[SignUpExport] Vue app mounted');
// 初始載入活動清單
this.loadCourses();
},
methods: {
/**
* 載入活動清單
*/
async loadCourses() {
try {
const response = await this.$api.call(this.apiNameClasses, 'SearchClasses', {
pageIndex: 1,
pageSize: 1000,
courseCode: '',
courseName: '',
courseDate1: null,
courseDate2: null,
courseLocation: '',
projectID: '',
closed: this.includeClosed ? null : false
});
if (response.Success) {
const convertedData = this.$dataConvert.convertDatesInObject(response.Data || []);
this.courses = convertedData;
console.log('[SignUpExport] 載入活動清單成功');
} else {
this.$message.error(response.Message || '載入活動清單失敗');
this.courses = [];
}
} catch (error) {
console.error('[SignUpExport] 載入活動清單錯誤:', error);
this.$message.error('載入活動清單發生錯誤');
this.courses = [];
}
},
/**
* 當活動選擇改變時載入資料
*/
async loadExportData() {
if (!this.selectedCourseID) {
this.signupData = null;
return;
}
try {
const response = await this.$api.call(this.apiNameSignUp, 'ExportSignUps', {
courseID: this.selectedCourseID,
timeFormat: this.timeFormat
});
if (response.Success) {
this.signupData = response.Data;
this.exportMessage = '';
console.log('[SignUpExport] 載入匯出資料成功');
} else {
this.$message.error(response.Message || '載入資料失敗');
this.signupData = null;
}
} catch (error) {
console.error('[SignUpExport] 載入資料錯誤:', error);
this.$message.error('載入資料發生錯誤');
this.signupData = null;
}
},
/**
* 執行匯出 - 使用後端 NPOI 生成的 Excel 檔案
*/
async executeExport() {
if (!this.signupData) {
this.$message.warning('沒有可匯出的資料');
return;
}
this.exporting = true;
this.exportMessage = '';
try {
// signupData 已經包含後端生成的 Base64 檔案
const fileBase64 = this.signupData.FileBase64;
const fileName = this.signupData.FileName;
if (!fileBase64 || !fileName) {
this.$message.error('缺少匯出資料');
return;
}
// 使用後端生成的檔案進行下載
FileDownloader.downloadFromBase64(fileBase64, fileName);
this.exportSuccess = true;
this.exportMessage = `成功匯出 ${this.signupData.DataCount} 筆資料`;
this.$message.success('匯出成功');
} catch (error) {
console.error('[SignUpExport] 匯出錯誤:', error);
this.exportSuccess = false;
this.exportMessage = '匯出失敗:' + error.message;
this.$message.error('匯出發生錯誤');
} finally {
this.exporting = false;
}
},
/**
* 清除選擇
*/
clearSelection() {
this.selectedCourseID = '';
this.signupData = null;
this.exportMessage = '';
},
/**
* 格式化檔案大小
*/
formatFileSize(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
}
}
});
})();
+299
View File
@@ -0,0 +1,299 @@
// ========================================
// 匯入參加人員 - Vue.js 應用程式
// 檔案位置:Scripts/SignUpImport.js
// ========================================
(() => {
'use strict';
new Vue({
el: '#signUpImportApp',
data: {
// API
apiNameClasses: 'ClassesService',
apiNameSignUp: 'SignUpService',
// 活動列表
courses: [],
selectedCourseID: '',
// 包含已結案
includeClosed: false,
// 檔案上傳
selectedFile: null,
isDragging: false,
// 預覽資料
previewData: [],
validationErrors: [],
// 匯入選項
clearBefore: true,
// 匯入狀態
importing: false,
importResult: null
},
computed: {
validRowCount() {
return this.previewData.filter(item => item.errors.length === 0).length;
}
},
mounted() {
console.log('[SignUpImport] Vue app mounted');
// 初始載入活動清單
this.loadCourses();
},
methods: {
/**
* 載入活動清單
*/
async loadCourses() {
try {
const response = await this.$api.call(this.apiNameClasses, 'SearchClasses', {
pageIndex: 1,
pageSize: 1000,
courseCode: '',
courseName: '',
courseDate1: null,
courseDate2: null,
courseLocation: '',
projectID: '',
closed: this.includeClosed ? null : false
});
if (response.Success) {
const convertedData = this.$dataConvert.convertDatesInObject(response.Data || []);
this.courses = convertedData;
console.log('[SignUpImport] 載入活動清單成功');
} else {
this.$message.error(response.Message || '載入活動清單失敗');
this.courses = [];
}
} catch (error) {
console.error('[SignUpImport] 載入活動清單錯誤:', error);
this.$message.error('載入活動清單發生錯誤');
this.courses = [];
}
},
/**
* 檔案被選擇
*/
onFileSelected(event) {
const file = event.target.files[0];
if (file) {
this.processFile(file);
}
},
/**
* 檔案被拖放
*/
onFileDrop(event) {
this.isDragging = false;
const files = event.dataTransfer.files;
if (files.length > 0) {
this.processFile(files[0]);
}
},
/**
* 處理檔案 - 上傳至後端進行解析和驗證
*/
async processFile(file) {
// 驗證檔案類型
if (!file.name.toLowerCase().endsWith('.xlsx')) {
this.$message.error('只支援 .xlsx 格式的 Excel 檔案');
return;
}
if (!this.selectedCourseID) {
this.$message.warning('請先選擇活動');
return;
}
this.selectedFile = file;
this.previewData = [];
this.validationErrors = [];
try {
// 建立 FormData 物件用於上傳檔案
const formData = new FormData();
formData.append('file', file);
// 呼叫後端 ProcessImportFile 方法
const response = await this.callProcessImportFile(formData);
if (response.Success) {
const data = response.Data;
this.previewData = this.convertBackendDataToPreviewFormat(data.PreviewData);
this.validationErrors = data.ValidationErrors || [];
this.$message.success(`成功讀取 ${data.ValidRowCount} 列有效資料`);
} else {
this.$message.error('處理檔案失敗:' + response.Message);
this.previewData = [];
this.validationErrors = [];
}
} catch (error) {
console.error('[SignUpImport] 處理檔案錯誤:', error);
this.$message.error('處理檔案發生錯誤:' + error.message);
this.previewData = [];
this.validationErrors = [];
}
},
/**
* 呼叫後端 ProcessImportFile 方法
*/
async callProcessImportFile(formData) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
// 使用 FileUploadHandler.ashx 處理檔案上傳
const url = `${this.$api.baseUrl}FileUploadHandler.ashx?courseID=${this.selectedCourseID}&action=ProcessImportFile`;
xhr.onload = () => {
try {
if (xhr.status !== 200) {
reject(new Error(`HTTP ${xhr.status}: ${xhr.statusText}`));
return;
}
let response = JSON.parse(xhr.responseText);
resolve(response);
} catch (error) {
reject(new Error('解析響應失敗:' + error.message + '\n回應:' + xhr.responseText));
}
};
xhr.onerror = () => reject(new Error('檔案上傳失敗'));
xhr.onprogress = (event) => {
if (event.lengthComputable) {
// 可以在此處更新上傳進度
const percentComplete = (event.loaded / event.total) * 100;
console.log(`上傳進度: ${percentComplete.toFixed(2)}%`);
}
};
xhr.open('POST', url);
// 不要設定 Content-Type,讓瀏覽器自動設定 multipart/form-data
xhr.send(formData);
});
},
/**
* 將後端資料轉換為前端預覽格式
*/
convertBackendDataToPreviewFormat(backendData) {
if (!Array.isArray(backendData)) {
return [];
}
return backendData.map(item => ({
Name: item.Name || '',
Mobile: item.Mobile || '',
IDType: item.IDType || '',
IDNumber: item.IDNumber || '',
MealType: item.MealType || '',
Email: item.Email || '',
LossJobTimes: item.LossJobTimes || '',
Gender: item.Gender || '',
SignUpType: item.SignUpType || '',
errors: Array.isArray(item.Errors) ? item.Errors : []
}));
},
/**
* 執行匯入 - 確認驗證無誤的資料並送交後端入庫
*/
async executeImport() {
if (!this.selectedCourseID) {
this.$message.warning('請先選擇活動');
return;
}
if (this.validRowCount === 0) {
this.$message.error('沒有有效的資料可匯入');
return;
}
// 準備要匯入的資料(只包括有效的列)
const itemsToImport = this.previewData
.filter(item => item.errors.length === 0)
.map(item => ({
ID: 0,
CourseID: this.selectedCourseID,
Name: item.Name,
Mobile: item.Mobile,
IDType: item.IDType || '',
IDNumber: item.IDNumber ? item.IDNumber.trim() : null,
MealType: item.MealType || '',
Email: item.Email ? item.Email.trim() : null,
SignUpType: item.SignUpType || '',
Gender: item.Gender || '',
LossJobTimes: item.LossJobTimes || '',
QRCode: null,
CheckInTime: null,
CheckOutTime: null,
DB_APPNO: 0
}));
this.importing = true;
this.importResult = null;
try {
const response = await this.$api.call(this.apiNameSignUp, 'ConfirmImport', {
courseID: this.selectedCourseID,
items: itemsToImport,
clearBefore: this.clearBefore
});
this.importResult = {
success: response.Success,
message: response.Message,
details: response.Data
};
if (response.Success) {
this.$message.success('匯入成功');
} else {
this.$message.error('匯入部分失敗:' + response.Message);
}
} catch (error) {
console.error('[SignUpImport] 匯入錯誤:', error);
this.$message.error('匯入發生錯誤');
this.importResult = {
success: false,
message: '匯入發生錯誤:' + error.message
};
} finally {
this.importing = false;
}
},
/**
* 重新選擇檔案
*/
resetUpload() {
this.selectedFile = null;
this.previewData = [];
this.validationErrors = [];
this.importResult = null;
this.clearBefore = true;
if (this.$refs.fileInput) {
this.$refs.fileInput.value = '';
}
},
/**
* 格式化檔案大小
*/
formatFileSize(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
}
},
watch: {
includeClosed() {
this.loadCourses();
}
}
});
})();
+619
View File
@@ -0,0 +1,619 @@
// ========================================
// 活動參加人員 - Vue.js 應用程式
// 檔案位置:Scripts/SignUpList.js
// ========================================
(() => {
'use strict';
new Vue({
el: '#signUpApp',
data: {
// API
apiNameClasses: 'ClassesService',
apiNameSignUp: 'SignUpService',
// 活動列表
courses: [],
selectedCourseID: '',
// 搜尋條件
search: {
seqNo: null,
name: '',
mobile: '',
idType: '',
idNumber: '',
idNumberEmpty: false,
mealType: '',
email: '',
emailEmpty: false
},
// 包含已結案
includeClosed: false,
// 搜尋結果
signups: [],
loading: false,
// 分頁
currentPage: 1,
pageSize: 20,
totalCount: 0,
// 勾選功能
selectedIds: [], // 已選人員 ID 清單(跨分頁保留)
selectAllCurrentPage: false, // 當前頁全選狀態
// 簡訊發送
sendingSmsBatch: false, // 發送中狀態
// 彈窗
showModal: false,
isNewMode: true,
editItem: {
ID: 0,
CourseID: 0,
SeqNo: 0,
Name: '',
Mobile: '',
SignUpType: '',
CheckInTime: '',
CheckOutTime: '',
IDType: '',
IDNumber: '',
MealType: '',
Email: '',
Gender: '',
LossJobTimes: 0,
DB_APPNO: 0
},
// 驗證錯誤訊息
validationErrors: {
Name: '',
Mobile: '',
IDNumber: '',
Email: '',
LossJobTimes: ''
}
},
computed: {
totalPages() {
return Math.ceil(this.totalCount / this.pageSize);
},
hasIndeterminate() {
// 當前頁部分已選且不是全選時顯示 indeterminate 狀態
const currentPageSelectedCount = this.signups.filter(item =>
this.selectedIds.includes(item.ID)
).length;
return currentPageSelectedCount > 0 && currentPageSelectedCount < this.signups.length;
}
},
mounted() {
console.log('[SignUpList] Vue app mounted');
// 初始載入活動清單
this.loadCourses();
},
methods: {
/**
* 載入活動清單
*/
async loadCourses() {
try {
const response = await this.$api.call(this.apiNameClasses, 'SearchClasses', {
pageIndex: 1,
pageSize: 1000,
courseCode: '',
courseName: '',
courseDate1: null,
courseDate2: null,
courseLocation: '',
projectID: '',
closed: this.includeClosed ? null : false
});
if (response.Success) {
const convertedData = this.$dataConvert.convertDatesInObject(response.Data || []);
this.courses = convertedData;
console.log('[SignUpList] 載入活動清單成功,共 ' + this.courses.length + ' 筆');
} else {
this.$message.error(response.Message || '載入活動清單失敗');
this.courses = [];
}
} catch (error) {
console.error('[SignUpList] 載入活動清單錯誤:', error);
this.$message.error('載入活動清單發生錯誤');
this.courses = [];
}
},
/**
* 活動選擇改變
*/
onCourseSelected() {
this.clearSearch();
this.clearSmsSelection(); // 活動改變時清除已選
if (this.selectedCourseID) {
this.currentPage = 1;
this.loadSignUps();
}
},
/**
* 執行搜尋
*/
async performSearch() {
if (!this.selectedCourseID) {
this.$message.warning('請先選擇活動');
return;
}
this.currentPage = 1;
await this.loadSignUps();
},
/**
* 載入參加人員資料
*/
async loadSignUps() {
if (!this.selectedCourseID) return;
this.loading = true;
try {
const response = await this.$api.call(this.apiNameSignUp, 'SearchSignUpsAdvanced', {
pageIndex: this.currentPage,
pageSize: this.pageSize,
courseID: this.selectedCourseID,
seqNo: this.search.seqNo,
name: this.search.name,
mobile: this.search.mobile,
idType: this.search.idType,
idNumber: this.search.idNumber,
idNumberEmpty: this.search.idNumberEmpty,
mealType: this.search.mealType,
email: this.search.email,
emailEmpty: this.search.emailEmpty
});
if (response.Success) {
const convertedData = this.$dataConvert.convertDatesInObject(response.Data || []);
this.signups = convertedData;
this.totalCount = response.TotalCount || 0;
console.log('[SignUpList] 載入成功,共 ' + this.totalCount + ' 筆');
console.log('[SignUpList] ', convertedData);
// 更新當前頁全選狀態
this.updateSelectAllState();
} else {
this.$message.error(response.Message || '查詢失敗');
this.signups = [];
this.totalCount = 0;
}
} catch (error) {
console.error('[SignUpList] 載入錯誤:', error);
this.$message.error('載入資料發生錯誤');
this.signups = [];
this.totalCount = 0;
} finally {
this.loading = false;
}
},
/**
* 清除搜尋條件
*/
clearSearch() {
this.search = {
seqNo: null,
name: '',
mobile: '',
idType: '',
idNumber: '',
idNumberEmpty: false,
mealType: '',
email: '',
emailEmpty: false
};
this.includeClosed = false;
this.clearSmsSelection();
},
/**
* 換頁
*/
goToPage(page) {
if (page < 1 || page > this.totalPages) return;
this.currentPage = page;
this.loadSignUps();
},
/**
* 前往第一頁
*/
goToFirstPage() {
this.currentPage = 1;
this.loadSignUps();
},
/**
* 前往最後一頁
*/
goToLastPage() {
this.currentPage = this.totalPages;
this.loadSignUps();
},
/**
* 分頁大小改變
*/
onPageSizeChanged() {
this.currentPage = 1;
this.loadSignUps();
},
/**
* 開啟新增彈窗
*/
openAddModal() {
this.isNewMode = true;
this.editItem = {
ID: 0,
CourseID: this.selectedCourseID,
SeqNo: 0,
Name: '',
Mobile: '',
SignUpType: '現場',
CheckInTime: '',
CheckOutTime: '',
IDType: '',
IDNumber: '',
MealType: '',
Email: '',
Gender: '',
LossJobTimes: 0,
DB_APPNO: 0
};
this.clearValidationErrors();
this.showModal = true;
},
/**
* 開啟更正彈窗
*/
openEditModal(item) {
this.isNewMode = false;
this.editItem = {
ID: item.ID,
CourseID: item.CourseID,
SeqNo: item.SeqNo,
Name: item.Name,
Mobile: item.Mobile,
SignUpType: item.SignUpType || '',
CheckInTime: item.CheckInTime,
CheckOutTime: item.CheckOutTime,
IDType: item.IDType || '',
IDNumber: item.IDNumber || '',
MealType: item.MealType || '',
Email: item.Email || '',
Gender: item.Gender || '',
LossJobTimes: item.LossJobTimes || 0,
DB_APPNO: item.DB_APPNO
};
this.clearValidationErrors();
this.showModal = true;
},
/**
* 關閉彈窗
*/
closeModal() {
this.showModal = false;
},
/**
* 清除驗證錯誤訊息
*/
clearValidationErrors() {
this.validationErrors = {
Name: '',
Mobile: '',
IDNumber: '',
Email: '',
LossJobTimes: ''
};
},
/**
* 驗證電子郵件格式
*/
validateEmail(email) {
if (!email || email.trim() === '') {
return true; // 電子郵件非必填,空值視為有效
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email.trim());
},
/**
* 驗證身分證字號格式和檢查碼
* 台灣身分證字號格式:首位為大寫英文字母,後跟9位數字
* 檢查碼算法:
* 1. 首字母轉換為數字(A=10, B=11, ...Z=35
* 2. 首字母對應的十位數字乘以1,個位數字乘以9
* 3. 後續8位數字分別乘以8,7,6,5,4,3,2
* 4. 最後一位為檢查碼,乘以1
* 5. 所有乘積相加,模10後的結果應該為0
*/
validateIDNumber(idNumber) {
if (!idNumber || idNumber.trim() === '') {
return true; // 身分證字號非必填,空值視為有效
}
idNumber = idNumber.trim().toUpperCase();
// 基本格式驗證:首位為大寫英文字母,後跟9位數字
const idRegex = /^[A-Z]\d{9}$/;
if (!idRegex.test(idNumber)) {
return false;
}
// 檢查碼驗證
const letterToNumber = {
'A': 10, 'B': 11, 'C': 12, 'D': 13, 'E': 14,
'F': 15, 'G': 16, 'H': 17, 'I': 34, 'J': 18,
'K': 19, 'L': 20, 'M': 21, 'N': 22, 'O': 35,
'P': 23, 'Q': 24, 'R': 25, 'S': 26, 'T': 27,
'U': 28, 'V': 29, 'W': 32, 'X': 30, 'Y': 31,
'Z': 33
};
const firstLetter = idNumber[0];
const firstLetterNum = letterToNumber[firstLetter];
// 將首字母對應的數字分成十位和個位
const letterTens = Math.floor(firstLetterNum / 10);
const letterOnes = firstLetterNum % 10;
// 計算檢查碼
let sum = letterTens * 1 + letterOnes * 9;
// 第2-9位數字分別乘以8,7,6,5,4,3,2
const weights = [8, 7, 6, 5, 4, 3, 2, 1];
for (let i = 0; i < 8; i++) {
sum += parseInt(idNumber[i + 1]) * weights[i];
}
// 第10位為檢查碼,乘以1
sum += parseInt(idNumber[9]) * 1;
// 檢查碼驗證:總和模10應該為0
return (sum % 10) === 0;
},
/**
* 驗證輸入
*/
validateInput() {
this.clearValidationErrors();
let isValid = true;
// 驗證姓名
if (!this.editItem.Name || this.editItem.Name.trim() === '') {
this.validationErrors.Name = '姓名為必填欄位';
isValid = false;
} else if (this.editItem.Name.length > 100) {
this.validationErrors.Name = '姓名最長 100 個字元';
isValid = false;
}
// 驗證手機號
if (!this.editItem.Mobile || this.editItem.Mobile.trim() === '') {
this.validationErrors.Mobile = '手機號為必填欄位';
isValid = false;
} else if (this.editItem.Mobile.length > 20) {
this.validationErrors.Mobile = '手機號最長 20 個字元';
isValid = false;
}
// 驗證身分證字號(如果有填寫)
if (this.editItem.IDNumber && this.editItem.IDNumber.trim() !== '') {
if (!this.validateIDNumber(this.editItem.IDNumber)) {
this.validationErrors.IDNumber = '身分證字號無效(格式應為:大寫英文字母 + 9位數字,且檢查碼需正確,例如:A123456789';
isValid = false;
}
}
// 驗證電子郵件(如果有填寫)
if (this.editItem.Email && this.editItem.Email.trim() !== '') {
if (!this.validateEmail(this.editItem.Email)) {
this.validationErrors.Email = '電子郵件格式不正確(例如:user@example.com';
isValid = false;
}
}
// 驗證失業給付認定次數(如果有填寫)
if (this.editItem.LossJobTimes !== null && this.editItem.LossJobTimes !== '' && this.editItem.LossJobTimes !== undefined) {
if (isNaN(this.editItem.LossJobTimes) || this.editItem.LossJobTimes < 0) {
this.validationErrors.LossJobTimes = '失業給付認定次數必須為非負整數';
isValid = false;
}
}
return isValid;
},
/**
* 儲存參加人員
*/
async saveSignUp() {
if (!this.validateInput()) return;
try {
// 準備資料
const item = {
ID: this.editItem.ID,
CourseID: this.editItem.CourseID,
SeqNo: this.editItem.SeqNo,
Name: this.editItem.Name.trim(),
Mobile: this.editItem.Mobile.trim(),
SignUpType: this.editItem.SignUpType,
CheckInTime: this.editItem.CheckInTime,
CheckOutTime: this.editItem.CheckOutTime,
IDType: this.editItem.IDType,
IDNumber: this.editItem.IDNumber ? this.editItem.IDNumber.trim() : null,
MealType: this.editItem.MealType,
Email: this.editItem.Email ? this.editItem.Email.trim() : null,
Gender: this.editItem.Gender,
LossJobTimes: this.editItem.LossJobTimes !== null && this.editItem.LossJobTimes !== '' ? this.editItem.LossJobTimes : null,
DB_APPNO: this.editItem.DB_APPNO
};
console.debug('[saveSignUp]', item);
const response = await this.$api.call(this.apiNameSignUp, 'SaveSignUp', {
isNew: this.isNewMode,
item: item
});
if (response.Success) {
this.$message.success(response.Message || '儲存成功');
this.closeModal();
this.loadSignUps();
} else {
this.$message.error(response.Message || '儲存失敗');
}
} catch (error) {
console.error('[SignUpList] 儲存錯誤:', error);
this.$message.error('儲存發生錯誤');
}
},
/**
* 刪除參加人員
*/
async deleteSignUp(item) {
if (!confirm(`確定要刪除「${item.Name}」的報名資料嗎?`)) {
return;
}
try {
const response = await this.$api.call(this.apiNameSignUp, 'DeleteSignUp', {
id: item.ID,
dbAppNo: item.DB_APPNO
});
if (response.Success) {
this.$message.success(response.Message || '刪除成功');
// 從已選清單中移除
const index = this.selectedIds.indexOf(item.ID);
if (index > -1) {
this.selectedIds.splice(index, 1);
}
this.loadSignUps();
} else {
this.$message.error(response.Message || '刪除失敗');
}
} catch (error) {
console.error('[SignUpList] 刪除錯誤:', error);
this.$message.error('刪除發生錯誤');
}
},
/**
* 全選/反選當前頁
*/
toggleSelectAll() {
if (this.selectAllCurrentPage) {
// 全選當前頁
this.signups.forEach(item => {
if (!this.selectedIds.includes(item.ID)) {
this.selectedIds.push(item.ID);
}
});
} else {
// 反選當前頁
this.signups.forEach(item => {
const index = this.selectedIds.indexOf(item.ID);
if (index > -1) {
this.selectedIds.splice(index, 1);
}
});
}
},
/**
* 更新全選狀態 - 根據當前頁已選情況
*/
updateSelectAllState() {
const currentPageSelectedCount = this.signups.filter(item =>
this.selectedIds.includes(item.ID)
).length;
this.selectAllCurrentPage = currentPageSelectedCount === this.signups.length && this.signups.length > 0;
},
/**
* 清除已選人員
*/
clearSmsSelection() {
this.selectedIds = [];
this.selectAllCurrentPage = false;
},
/**
* 批發簡訊
*/
async sendSmsBatch() {
if (this.selectedIds.length === 0) {
this.$message.warning('請先勾選要發送的人員');
return;
}
if (!confirm(`確定要向 ${this.selectedIds.length} 位人員發送簡訊嗎?`)) {
return;
}
this.sendingSmsBatch = true;
try {
const response = await this.$api.call(this.apiNameSignUp, 'SendSmsBatch', {
courseID: this.selectedCourseID,
signupIds: this.selectedIds
});
if (response.Success) {
this.$message.success(response.Message || '簡訊發送成功');
this.clearSmsSelection();
} else {
this.$message.error(response.Message || '簡訊發送失敗');
}
} catch (error) {
console.error('[SignUpList] 發送簡訊錯誤:', error);
this.$message.error('發送簡訊發生錯誤');
} finally {
this.sendingSmsBatch = false;
}
},
/**
* 查詢餘額
*/
async checkBalance() {
try {
const response = await this.$api.call(this.apiNameSignUp, 'CheckBalance');
if (response.Success) {
this.$message.success(response.Message || `餘額尚有 ${response.Data.Balance}`);
} else {
this.$message.error(response.Message || '餘額查詢失敗');
}
} catch (error) {
console.error('[SignUpList] 查詢餘額錯誤:', error);
this.$message.error('查詢餘額發生錯誤');
} finally {
this.sendingSmsBatch = false;
}
}
},
watch: {
includeClosed() {
this.loadCourses();
}
}
});
})();
+682
View File
@@ -0,0 +1,682 @@
// ========================================
// 報到系統 - 共用 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');
})();
+174
View File
@@ -0,0 +1,174 @@
// ========================================
// Site.Master 選單管理 - 使用 sessionStorage 保持狀態
// ========================================
/**
* 切換側邊欄展開/收合
*/
function toggleSidebar() {
var gridContainer = document.querySelector('.grid-container');
gridContainer.classList.toggle('sidebar-collapsed');
// 儲存側邊欄狀態到 sessionStorage
var isCollapsed = gridContainer.classList.contains('sidebar-collapsed');
sessionStorage.setItem('sidebarCollapsed', isCollapsed);
}
/**
* 切換子選單展開/收合
* @param {Event} event - 點擊事件
* @param {string} menuId - 子選單的 ID
*/
function toggleSubmenu(event, menuId) {
event.preventDefault();
var submenu = document.getElementById(menuId);
var menuLink = event.currentTarget;
var arrow = menuLink.querySelector('.menu-arrow');
if (submenu.style.display === 'block') {
submenu.style.display = 'none';
arrow.textContent = '▼';
} else {
// 關閉其他子選單
var allSubmenus = document.querySelectorAll('.submenu');
var allArrows = document.querySelectorAll('.menu-arrow');
allSubmenus.forEach(function (sm) { sm.style.display = 'none'; });
allArrows.forEach(function (arr) { arr.textContent = '▼'; });
submenu.style.display = 'block';
arrow.textContent = '▲';
}
// 儲存所有子選單的展開狀態到 sessionStorage
saveSubmenuStates();
}
/**
* 儲存所有子選單的展開狀態
*/
function saveSubmenuStates() {
var states = {};
var allSubmenus = document.querySelectorAll('.submenu');
allSubmenus.forEach(function (submenu) {
states[submenu.id] = submenu.style.display === 'block';
});
sessionStorage.setItem('submenuStates', JSON.stringify(states));
}
/**
* 恢復所有子選單的展開狀態
* @returns {boolean} 是否有恢復任何選單狀態
*/
function restoreSubmenuStates() {
var savedStates = sessionStorage.getItem('submenuStates');
if (savedStates) {
try {
var states = JSON.parse(savedStates);
var hasOpenMenu = false;
// 恢復每個子選單的狀態
for (var menuId in states) {
var submenu = document.getElementById(menuId);
if (submenu && states[menuId]) {
submenu.style.display = 'block';
hasOpenMenu = true;
// 更新箭頭方向
var menuItem = submenu.closest('.menu-item');
if (menuItem) {
var arrow = menuItem.querySelector('.menu-arrow');
if (arrow) {
arrow.textContent = '▲';
}
}
}
}
return hasOpenMenu;
} catch (e) {
console.error('恢復選單狀態失敗:', e);
return false;
}
}
return false;
}
/**
* 恢復側邊欄收合狀態
*/
function restoreSidebarState() {
var isCollapsed = sessionStorage.getItem('sidebarCollapsed') === 'true';
if (isCollapsed) {
var gridContainer = document.querySelector('.grid-container');
gridContainer.classList.add('sidebar-collapsed');
}
}
/**
* 初始化選單狀態
*/
function initializeMenuState() {
// 恢復側邊欄狀態
restoreSidebarState();
// 嘗試恢復子選單狀態
var hasRestoredState = restoreSubmenuStates();
// 如果沒有儲存的狀態,則展開第一個選單(預設行為)
if (!hasRestoredState) {
var firstSubmenu = document.getElementById('menu1');
if (firstSubmenu) {
firstSubmenu.style.display = 'block';
var firstArrow = document.querySelector('.menu-list .menu-item:first-child .menu-arrow');
if (firstArrow) {
firstArrow.textContent = '▲';
}
// 儲存預設狀態
saveSubmenuStates();
}
}
}
// ========================================
// 主版面 - 登出功能
// 使用 Scripts/shared.js 提供的服務
// ========================================
/**
* 處理登出
* 使用 Scripts/shared.js -> $api.logout
* 使用 Scripts/shared.js -> $message
* 使用 Scripts/shared.js -> $navigate
*/
async function handleLogout() {
if (!confirm('確定要登出嗎?')) {
return;
}
console.log('[Site.Master] 開始登出');
try {
const response = await window.$api.logout();
if (response.Success) {
console.log('[Site.Master] 登出成功');
window.$message.success('登出成功');
setTimeout(() => window.$navigate.toLogin(), 500);
} else {
console.warn('[Site.Master] 登出失敗: ' + response.Message);
window.$message.error('登出失敗');
}
} catch (error) {
console.error('[Site.Master] 登出錯誤:', error);
// 即使發生錯誤也導向登入頁
window.$navigate.toLogin();
}
}
// ========================================
// 頁面載入時初始化
// ========================================
window.addEventListener('load', function () {
initializeMenuState();
});
File diff suppressed because it is too large Load Diff
+11
View File
File diff suppressed because one or more lines are too long