620 lines
22 KiB
JavaScript
620 lines
22 KiB
JavaScript
// ========================================
|
||
// 活動參加人員 - 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();
|
||
}
|
||
}
|
||
});
|
||
})();
|