chore: 首次簽入 Thinkyu ASP.NET 專案
- 加入 Visual Studio / ASP.NET .gitignore - 排除建置輸出、IDE 設定、NuGet packages、大型 MSI 安裝檔
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
||||
Reference in New Issue
Block a user