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