chore: 首次簽入 Thinkyu ASP.NET 專案
- 加入 Visual Studio / ASP.NET .gitignore - 排除建置輸出、IDE 設定、NuGet packages、大型 MSI 安裝檔
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
/**
|
||||
* 使用 ApiMixin 的示例
|
||||
* 这个文件展示了如何简化 領據處理.js 中的 API 调用
|
||||
*/
|
||||
|
||||
// 原来的写法(冗长):
|
||||
save() {
|
||||
// ... 验证代码 ...
|
||||
|
||||
axios({
|
||||
method: 'post',
|
||||
url: `${API_BASE}/Save`,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
},
|
||||
data: JSON.stringify(data)
|
||||
})
|
||||
.then(response => {
|
||||
const result = response.data.d || response.data;
|
||||
if (result.Success) {
|
||||
this.ShowSuccess(result.Message);
|
||||
// ... 处理成功逻辑 ...
|
||||
} else {
|
||||
this.ShowError('儲存失敗: ' + result.Message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Save error:', error);
|
||||
this.ShowError('儲存發生錯誤: ' + (error.message || '請檢查網路連線'));
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
// 使用 ApiMixin 的新写法(简洁):
|
||||
async save() {
|
||||
if (!this.validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
|
||||
try {
|
||||
const data = {
|
||||
isNew: this.mode === 'add',
|
||||
mainItem: this.mainData,
|
||||
laborItem: this.laborData,
|
||||
travelItem: this.travelData
|
||||
};
|
||||
|
||||
const result = await this.callApiWithErrorMsg('領據處理', 'Save', data, '儲存');
|
||||
|
||||
this.ShowSuccess(result.Message);
|
||||
// 返回查看模式
|
||||
this.mode = 'view';
|
||||
// 更新编号和数据
|
||||
if (this.mainData.編號 === '' && result.Data) {
|
||||
this.mainData.編號 = result.Data;
|
||||
this.編號 = result.Data;
|
||||
}
|
||||
this.backupData();
|
||||
} catch (error) {
|
||||
console.error('Save error:', error);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
// 其他 API 调用示例:
|
||||
|
||||
// 原来的 loadDropdownList:
|
||||
async loadDropdownList(key) {
|
||||
let value = null;
|
||||
await axios({
|
||||
method: 'post',
|
||||
url: `${API_BASE}/GetDropdownLists`,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
},
|
||||
data: JSON.stringify({ key: key })
|
||||
})
|
||||
.then(response => {
|
||||
const result = response.data.d || response.data;
|
||||
if (result.Success) {
|
||||
value = result.Data || [];
|
||||
} else {
|
||||
console.error('取得下拉選單資料失敗:', result.Message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('載入下拉選單失敗:', error);
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
// 使用 mixin 的新写法:
|
||||
async loadDropdownList(key) {
|
||||
try {
|
||||
const result = await this.callApi('領據處理', 'GetDropdownLists', { key });
|
||||
return result.Data || [];
|
||||
} catch (error) {
|
||||
console.error('載入下拉選單失敗:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
// searchRecipient 的对比:
|
||||
|
||||
// 原来的写法:
|
||||
searchRecipient() {
|
||||
const key = this.mainData.領款人姓名;
|
||||
const identity = this.mainData.身分別;
|
||||
|
||||
if (key && key.length > 0) {
|
||||
if (!identity) {
|
||||
this.ShowWarning('請先選擇身分別');
|
||||
this.recipientList = [];
|
||||
this.showRecipientDropdown = false;
|
||||
return;
|
||||
}
|
||||
|
||||
axios({
|
||||
method: 'post',
|
||||
url: `${API_BASE}/SearchRecipient`,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
},
|
||||
data: JSON.stringify({ key, identity })
|
||||
})
|
||||
.then(response => {
|
||||
const result = response.data.d || response.data;
|
||||
if (result.Success) {
|
||||
this.recipientList = result.Data || [];
|
||||
this.showRecipientDropdown = this.recipientList.length > 0;
|
||||
} else {
|
||||
this.recipientList = [];
|
||||
this.showRecipientDropdown = false;
|
||||
this.ShowWarning(result.Message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('查詢領款人失敗:', error);
|
||||
});
|
||||
} else {
|
||||
this.recipientList = [];
|
||||
this.showRecipientDropdown = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 使用 mixin 的新写法:
|
||||
async searchRecipient() {
|
||||
const key = this.mainData.領款人姓名;
|
||||
const identity = this.mainData.身分別;
|
||||
|
||||
if (!key || key.length === 0) {
|
||||
this.recipientList = [];
|
||||
this.showRecipientDropdown = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!identity) {
|
||||
this.ShowWarning('請先選擇身分別');
|
||||
this.recipientList = [];
|
||||
this.showRecipientDropdown = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.callApi('領據處理', 'SearchRecipient', { key, identity });
|
||||
this.recipientList = result.Data || [];
|
||||
this.showRecipientDropdown = this.recipientList.length > 0;
|
||||
} catch (error) {
|
||||
console.error('查詢領款人失敗:', error);
|
||||
this.recipientList = [];
|
||||
this.showRecipientDropdown = false;
|
||||
this.ShowWarning(error.message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* API Mixin - 统一的 API 调用管理
|
||||
* 用法: 在 Vue 组件中引入此 mixin,然后使用 this.callApi(endpoint, functionName, data)
|
||||
*/
|
||||
const ApiMixin = {
|
||||
methods: {
|
||||
/**
|
||||
* 调用 API 的通用方法
|
||||
* @param {string} endpoint - API 端点(.asmx 文件名,不需要扩展名),如 '領據處理'
|
||||
* @param {string} functionName - 方法名称,如 'Save', 'GetOne'
|
||||
* @param {object} data - 要传送的数据对象
|
||||
* @returns {Promise} 返回 Promise,包含 API 响应数据
|
||||
*/
|
||||
callApi(endpoint, functionName, data = {}) {
|
||||
console.log(`callApi [${endpoint}/${functionName}]`, data);
|
||||
return axios({
|
||||
method: 'post',
|
||||
url: `${API_BASE}/${endpoint}.asmx/${functionName}`,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
},
|
||||
data: JSON.stringify(data)
|
||||
})
|
||||
.then(response => {
|
||||
const result = response.data.d || response.data;
|
||||
if (result.Success) {
|
||||
return Promise.resolve(result);
|
||||
} else {
|
||||
return Promise.reject(new Error(result.Message || '操作失败'));
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(`API 調用失敗 [${endpoint}/${functionName}]:`, error);
|
||||
return Promise.reject(error);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 调用 API 并显示错误消息的便捷方法
|
||||
* @param {string} endpoint - API 端点
|
||||
* @param {string} functionName - 方法名称
|
||||
* @param {object} data - 数据对象
|
||||
* @param {string} errorPrefix - 错误消息前缀
|
||||
* @returns {Promise}
|
||||
*/
|
||||
callApiWithErrorMsg(endpoint, functionName, data = {}, errorPrefix = '操作') {
|
||||
return this.callApi(endpoint, functionName, data)
|
||||
.catch(error => {
|
||||
this.ShowError(`${errorPrefix}失敗: ${error.message || '請檢查網路連線'}`);
|
||||
return Promise.reject(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
Vendored
+3
File diff suppressed because one or more lines are too long
@@ -0,0 +1,35 @@
|
||||
// Vue Mixin - Base64 工具
|
||||
// 提供 Base64 字串轉換為 Blob 的功能
|
||||
|
||||
var Base64Mixin = {
|
||||
methods: {
|
||||
/**
|
||||
* 將 Base64 字串轉換為 Blob 物件
|
||||
* @param {string} base64 - Base64 編碼字串
|
||||
* @returns {Blob} Excel 格式的 Blob 物件
|
||||
*/
|
||||
Base64ToBlob: function(base64) {
|
||||
try {
|
||||
if (!base64 || typeof base64 !== 'string') {
|
||||
throw new Error('Invalid input: Base64 string is required');
|
||||
}
|
||||
|
||||
var cleanBase64 = base64.replace(/\s/g, '');
|
||||
|
||||
if (!cleanBase64 || !/^[A-Za-z0-9+/]*={0,2}$/.test(cleanBase64)) {
|
||||
throw new Error('Invalid Base64 string format');
|
||||
}
|
||||
|
||||
var binaryString = atob(cleanBase64);
|
||||
var bytes = new Uint8Array(binaryString.length);
|
||||
for (var i = 0; i < binaryString.length; i++) {
|
||||
bytes[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
return new Blob([bytes], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
|
||||
} catch (error) {
|
||||
console.error('Base64ToBlob error:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
+2657
File diff suppressed because it is too large
Load Diff
Vendored
+11008
File diff suppressed because it is too large
Load Diff
+5
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+18706
File diff suppressed because it is too large
Load Diff
+13
File diff suppressed because one or more lines are too long
@@ -0,0 +1,198 @@
|
||||
# Vue Date Mixin 使用說明
|
||||
|
||||
## 概述
|
||||
`vue-date-mixin.js` 是一個 Vue.js mixin,用於處理 ASP.NET ASMX Web Service 回傳的日期格式 `/Date(timestamp)/`。
|
||||
|
||||
## 功能
|
||||
|
||||
### 1. FormatDate(dateStr, separator)
|
||||
將日期格式化為指定分隔符的格式。
|
||||
|
||||
**參數:**
|
||||
- `dateStr`: 日期字串或 Date 物件
|
||||
- `separator`: 日期分隔符,預設為 `"-"`
|
||||
|
||||
**回傳值:**
|
||||
- 格式化後的日期字串
|
||||
|
||||
**範例:**
|
||||
```javascript
|
||||
this.FormatDate('/Date(1764622110737)/'); // 回傳: "2025-12-02" (使用預設 "-")
|
||||
this.FormatDate('/Date(1764622110737)/', '/'); // 回傳: "2025/12/02"
|
||||
this.FormatDate('/Date(1764622110737)/', '.'); // 回傳: "2025.12.02"
|
||||
this.FormatDate(new Date(), '-'); // 回傳: "2025-01-15"
|
||||
```
|
||||
|
||||
### 2. ConvertDatesInObject(obj, separator)
|
||||
遍歷物件或陣列,自動轉換所有符合 ASP.NET ASMX 日期格式的屬性。
|
||||
|
||||
**參數:**
|
||||
- `obj`: 要處理的物件或陣列
|
||||
- `separator`: 日期分隔符,預設為 `"-"`
|
||||
|
||||
**回傳值:**
|
||||
- 處理後的物件或陣列(所有日期欄位已轉換)
|
||||
|
||||
**範例:**
|
||||
```javascript
|
||||
// 使用預設分隔符 "-"
|
||||
const item = {
|
||||
編號: 1,
|
||||
生效日期: '/Date(1764622110737)/',
|
||||
備註: '測試'
|
||||
};
|
||||
const converted = this.ConvertDatesInObject(item);
|
||||
// converted.生效日期 = "2025-12-02"
|
||||
|
||||
// 使用自訂分隔符 "/"
|
||||
const converted2 = this.ConvertDatesInObject(item, '/');
|
||||
// converted2.生效日期 = "2025/12/02"
|
||||
|
||||
// 處理陣列
|
||||
const items = [
|
||||
{ 編號: 1, 生效日期: '/Date(1764622110737)/' },
|
||||
{ 編號: 2, 生效日期: '/Date(1764708510737)/' }
|
||||
];
|
||||
const converted = this.ConvertDatesInObject(items, '-');
|
||||
// 所有項目的生效日期都會被轉換為 "YYYY-MM-DD" 格式
|
||||
```
|
||||
|
||||
### 3. FormatDateTime(dateStr, separator)
|
||||
將日期時間格式化為指定分隔符的格式。
|
||||
|
||||
**參數:**
|
||||
- `dateStr`: 日期字串或 Date 物件
|
||||
- `separator`: 日期分隔符,預設為 `"-"`
|
||||
|
||||
**回傳值:**
|
||||
- 格式化後的日期時間字串
|
||||
|
||||
**範例:**
|
||||
```javascript
|
||||
this.FormatDateTime('/Date(1764622110737)/'); // 回傳: "2025-12-02 14:35:10"
|
||||
this.FormatDateTime('/Date(1764622110737)/', '/'); // 回傳: "2025/12/02 14:35:10"
|
||||
```
|
||||
|
||||
## 使用方式
|
||||
|
||||
### 1. 引入 Mixin
|
||||
在 `.ascx.cs` 檔案中註冊腳本:
|
||||
```csharp
|
||||
Page.ClientScript.RegisterClientScriptInclude("DateMixin",
|
||||
ParentPage.ResolveClientUrl("~/Scripts/vue-date-mixin.js"));
|
||||
```
|
||||
|
||||
### 2. 在 Vue 實例中使用
|
||||
```javascript
|
||||
function CreateApp() {
|
||||
app = new Vue({
|
||||
el: "#APP",
|
||||
mixins: [DateMixin], // 加入 mixin
|
||||
data: {
|
||||
Items: []
|
||||
},
|
||||
methods: {
|
||||
SelectPage() {
|
||||
axios.post('/Services/XXX.asmx/SelectPage', data)
|
||||
.then(response => {
|
||||
const result = response.data.d || response.data;
|
||||
|
||||
// 自動轉換所有日期欄位 (使用預設分隔符 "-")
|
||||
const items = this.ConvertDatesInObject(result.data || []);
|
||||
this.Items = items;
|
||||
|
||||
// 或使用自訂分隔符 "/"
|
||||
// const items = this.ConvertDatesInObject(result.data || [], '/');
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## 日期分隔符選項
|
||||
|
||||
### 常用分隔符
|
||||
- `"-"` (預設): 2025-12-02
|
||||
- `"/"`: 2025/12/02
|
||||
- `"."`: 2025.12.02
|
||||
- `""` (空字串): 20251202
|
||||
|
||||
### 使用建議
|
||||
- **ISO 8601 標準**: 使用 `"-"` (例: 2025-12-02)
|
||||
- **台灣慣用**: 使用 `"/"` (例: 2025/12/02)
|
||||
- **歐洲慣用**: 使用 `"."` (例: 2025.12.02)
|
||||
- **HTML5 date input**: 使用 `"-"` (必須是 YYYY-MM-DD 格式)
|
||||
|
||||
## 支援的日期格式
|
||||
|
||||
### 輸入格式
|
||||
- ASP.NET ASMX 格式: `/Date(1764622110737)/`
|
||||
- 標準 JavaScript Date 物件
|
||||
- 任何可被 `new Date()` 解析的字串
|
||||
|
||||
### 輸出格式
|
||||
- FormatDate: `YYYY{sep}MM{sep}DD` (例: 2025-12-02, 2025/12/02)
|
||||
- FormatDateTime: `YYYY{sep}MM{sep}DD HH:mm:ss` (例: 2025-12-02 14:35:10)
|
||||
|
||||
## 注意事項
|
||||
|
||||
1. **預設分隔符**: 如果不提供 `separator` 參數,預設使用 `"-"`
|
||||
2. **自動遞迴處理**: `ConvertDatesInObject` 會自動處理巢狀物件和陣列
|
||||
3. **不改變原物件**: 方法會回傳新的物件,不會修改原始資料
|
||||
4. **安全性**: 包含空值檢查和錯誤處理
|
||||
5. **效能**: 只處理符合 `/Date(` 格式的字串,避免不必要的轉換
|
||||
|
||||
## 完整範例
|
||||
|
||||
```javascript
|
||||
let app = null;
|
||||
|
||||
function CreateApp() {
|
||||
app = new Vue({
|
||||
el: "#APP",
|
||||
mixins: [DateMixin],
|
||||
data: {
|
||||
Items: []
|
||||
},
|
||||
methods: {
|
||||
LoadData() {
|
||||
axios.post('/Services/MyService.asmx/GetData', {})
|
||||
.then(response => {
|
||||
const result = response.data.d;
|
||||
|
||||
if (result.success) {
|
||||
// 使用預設分隔符 "-" (適用於 HTML5 date input)
|
||||
this.Items = this.ConvertDatesInObject(result.data);
|
||||
|
||||
// 或使用 "/" 分隔符 (適用於顯示)
|
||||
// this.Items = this.ConvertDatesInObject(result.data, '/');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
FormatSingleDate() {
|
||||
// 格式化單一日期
|
||||
const dateStr = '/Date(1764622110737)/';
|
||||
console.log(this.FormatDate(dateStr)); // "2025-12-02"
|
||||
console.log(this.FormatDate(dateStr, '/')); // "2025/12/02"
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## HTML5 Date Input 整合
|
||||
|
||||
當使用 HTML5 的 `<input type="date">` 時,**必須**使用 `"-"` 分隔符:
|
||||
|
||||
```html
|
||||
<input type="date" v-model="EditingItem.生效日期"/>
|
||||
```
|
||||
|
||||
```javascript
|
||||
// 載入時使用 "-" 分隔符
|
||||
this.Items = this.ConvertDatesInObject(result.data, '-');
|
||||
|
||||
// 或省略參數使用預設值 "-"
|
||||
this.Items = this.ConvertDatesInObject(result.data);
|
||||
@@ -0,0 +1,127 @@
|
||||
// Vue Mixin - 日期轉換工具
|
||||
// 用於處理 ASP.NET ASMX Web Service 回傳的日期格式
|
||||
|
||||
var DateMixin = {
|
||||
methods: {
|
||||
/**
|
||||
* 格式化日期
|
||||
* @param {string|Date} dateStr - 日期字串或 Date 物件
|
||||
* @param {string} separator - 日期分隔符,預設為 "-"
|
||||
* @returns {string} 格式化後的日期字串
|
||||
*/
|
||||
FormatDate: function(dateStr, separator) {
|
||||
if (!dateStr) return '';
|
||||
|
||||
// 預設分隔符為 "-"
|
||||
if (separator === undefined) {
|
||||
separator = '-';
|
||||
}
|
||||
|
||||
// 處理 ASP.NET ASMX 的日期格式 /Date(1764622110737)/
|
||||
if (typeof dateStr === 'string' && dateStr.indexOf('/Date(') === 0) {
|
||||
var timestamp = parseInt(dateStr.match(/-?\d+/)[0]);
|
||||
var date = new Date(timestamp);
|
||||
var year = String(date.getFullYear()).padStart(4, '0');
|
||||
var month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
var day = String(date.getDate()).padStart(2, '0');
|
||||
return year + separator + month + separator + day;
|
||||
}
|
||||
|
||||
// 如果已經是一般的 Date 物件或其他格式
|
||||
var date = new Date(dateStr);
|
||||
if (!isNaN(date.getTime())) {
|
||||
var year = String(date.getFullYear()).padStart(4, '0');
|
||||
var month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
var day = String(date.getDate()).padStart(2, '0');
|
||||
return year + separator + month + separator + day;
|
||||
}
|
||||
|
||||
return dateStr;
|
||||
},
|
||||
|
||||
/**
|
||||
* 遍歷物件或陣列,自動轉換所有符合 ASP.NET ASMX 日期格式的屬性
|
||||
* @param {Object|Array} obj - 要處理的物件或陣列
|
||||
* @param {string} separator - 日期分隔符,預設為 "-"
|
||||
* @returns {Object|Array} 處理後的物件或陣列
|
||||
*/
|
||||
ConvertDatesInObject: function(obj, separator) {
|
||||
if (!obj) return obj;
|
||||
|
||||
// 預設分隔符為 "-"
|
||||
if (separator === undefined) {
|
||||
separator = '-';
|
||||
}
|
||||
|
||||
// 處理陣列
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map(function(item) {
|
||||
return this.ConvertDatesInObject(item, separator);
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
// 處理物件
|
||||
if (typeof obj === 'object') {
|
||||
var result = {};
|
||||
for (var key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
var value = obj[key];
|
||||
|
||||
// 檢查是否為 ASP.NET ASMX 日期格式
|
||||
if (typeof value === 'string' && value.indexOf('/Date(') === 0) {
|
||||
result[key] = this.FormatDate(value, separator);
|
||||
}
|
||||
// 遞迴處理巢狀物件或陣列
|
||||
else if (value !== null && typeof value === 'object') {
|
||||
result[key] = this.ConvertDatesInObject(value, separator);
|
||||
}
|
||||
else {
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
return obj;
|
||||
},
|
||||
|
||||
/**
|
||||
* 格式化日期時間
|
||||
* @param {string|Date} dateStr - 日期字串或 Date 物件
|
||||
* @param {string} separator - 日期分隔符,預設為 "-"
|
||||
* @returns {string} 格式化後的日期時間字串
|
||||
*/
|
||||
FormatDateTime: function(dateStr, separator) {
|
||||
if (!dateStr) return '';
|
||||
|
||||
// 預設分隔符為 "-"
|
||||
if (separator === undefined) {
|
||||
separator = '-';
|
||||
}
|
||||
|
||||
var date;
|
||||
|
||||
// 處理 ASP.NET ASMX 的日期格式 /Date(1764622110737)/
|
||||
if (typeof dateStr === 'string' && dateStr.indexOf('/Date(') === 0) {
|
||||
var timestamp = parseInt(dateStr.match(/-?\d+/)[0]);
|
||||
date = new Date(timestamp);
|
||||
} else {
|
||||
date = new Date(dateStr);
|
||||
}
|
||||
|
||||
if (isNaN(date.getTime())) {
|
||||
return dateStr;
|
||||
}
|
||||
|
||||
var year = String(date.getFullYear()).padStart(4, '0');
|
||||
var month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
var day = String(date.getDate()).padStart(2, '0');
|
||||
var hours = String(date.getHours()).padStart(2, '0');
|
||||
var minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
var seconds = String(date.getSeconds()).padStart(2, '0');
|
||||
|
||||
return year + separator + month + separator + day + ' ' + hours + ':' + minutes + ':' + seconds;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,98 @@
|
||||
// Vue Mixin - 訊息提示功能
|
||||
// 提供浮動訊息提示 (Toast) 功能
|
||||
|
||||
// 訊息類型常數
|
||||
var ToastType = {
|
||||
SUCCESS: 'success',
|
||||
ERROR: 'error',
|
||||
WARNING: 'warning',
|
||||
INFO: 'info'
|
||||
};
|
||||
|
||||
var ToastMixin = {
|
||||
data: function() {
|
||||
return {
|
||||
// 訊息提示相關
|
||||
ShowMessage: false,
|
||||
MessageText: "",
|
||||
MessageType: ToastType.SUCCESS,
|
||||
|
||||
// 預設顯示時長(ms)
|
||||
defaultDuration: 5000,
|
||||
|
||||
// 將常數暴露給 Vue 實例使用
|
||||
ToastType: ToastType
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 顯示訊息提示
|
||||
* @param {string} message - 訊息內容
|
||||
* @param {string} type - 訊息類型: ToastType.SUCCESS, ToastType.ERROR, ToastType.WARNING, ToastType.INFO
|
||||
* @param {number} duration - 顯示時長(毫秒),預設 3000ms
|
||||
*/
|
||||
ShowToast: function(message, type, duration) {
|
||||
if (type === undefined) {
|
||||
type = ToastType.SUCCESS;
|
||||
}
|
||||
if (duration === undefined) {
|
||||
duration = this.defaultDuration;
|
||||
}
|
||||
|
||||
this.MessageText = message;
|
||||
this.MessageType = type;
|
||||
this.ShowMessage = true;
|
||||
|
||||
var self = this;
|
||||
setTimeout(function() {
|
||||
self.ShowMessage = false;
|
||||
}, duration);
|
||||
},
|
||||
|
||||
/**
|
||||
* 顯示成功訊息
|
||||
* @param {string} message - 訊息內容
|
||||
* @param {number} duration - 顯示時長(毫秒),預設 3000ms
|
||||
*/
|
||||
ShowSuccess: function(message, duration) {
|
||||
this.ShowToast(message, ToastType.SUCCESS, duration);
|
||||
},
|
||||
|
||||
/**
|
||||
* 顯示錯誤訊息
|
||||
* @param {string} message - 訊息內容
|
||||
* @param {number} duration - 顯示時長(毫秒),預設 3000ms
|
||||
*/
|
||||
ShowError: function(message, duration) {
|
||||
this.ShowToast(message, ToastType.ERROR, duration);
|
||||
},
|
||||
|
||||
/**
|
||||
* 顯示警告訊息
|
||||
* @param {string} message - 訊息內容
|
||||
* @param {number} duration - 顯示時長(毫秒),預設 4000ms
|
||||
*/
|
||||
ShowWarning: function(message, duration) {
|
||||
if (duration === undefined) {
|
||||
duration = this.defaultDuration;
|
||||
}
|
||||
this.ShowToast(message, ToastType.WARNING, duration);
|
||||
},
|
||||
|
||||
/**
|
||||
* 顯示資訊訊息
|
||||
* @param {string} message - 訊息內容
|
||||
* @param {number} duration - 顯示時長(毫秒),預設 3000ms
|
||||
*/
|
||||
ShowInfo: function(message, duration) {
|
||||
this.ShowToast(message, ToastType.INFO, duration);
|
||||
},
|
||||
|
||||
/**
|
||||
* 隱藏訊息提示
|
||||
*/
|
||||
HideToast: function() {
|
||||
this.ShowMessage = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
Vendored
+11
File diff suppressed because one or more lines are too long
@@ -0,0 +1,9 @@
|
||||
if (typeof(Sys.Browser.WebKit) == "undefined") {
|
||||
Sys.Browser.WebKit = {};
|
||||
}
|
||||
|
||||
if (navigator.userAgent.indexOf("WebKit/") > -1) {
|
||||
Sys.Browser.agent = Sys.Browser.WebKit;
|
||||
Sys.Browser.version = parseFloat(navigator.userAgent.match(/Webkit\/(\d+(\.\d+)?)/)[1]);
|
||||
Sys.Browser.name = "WebKit";
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
let app = null;
|
||||
|
||||
let V人員類別 = null;
|
||||
let V講師編號 = null;
|
||||
let V憑證類別 = [];
|
||||
let V扣繳類別 = [];
|
||||
let V銀行分行 = [];
|
||||
|
||||
function CreatePaymentSettingApp() {
|
||||
const loadMarker = document.getElementById('PaymentSettingAPP');
|
||||
if (loadMarker) {
|
||||
DoCreatePaymentSettingApp();
|
||||
}
|
||||
}
|
||||
|
||||
function DoCreatePaymentSettingApp() {
|
||||
app = new Vue({
|
||||
el: "#PaymentSettingAPP",
|
||||
mixins: [DateMixin, ToastMixin, ApiMixin],
|
||||
data: {
|
||||
// 分頁相關
|
||||
PageIndex: 1,
|
||||
PageSize: 50,
|
||||
Total: 0,
|
||||
Items: [],
|
||||
|
||||
// 輸入參數
|
||||
人員類別: "",
|
||||
講師編號: "",
|
||||
|
||||
// 下拉選單資料
|
||||
憑證類別Options: [],
|
||||
扣繳類別Options: [],
|
||||
|
||||
// 新增/編輯狀態
|
||||
Appending: false,
|
||||
Editing: false,
|
||||
|
||||
// 銀行分行浮動選單相關
|
||||
銀行分行: [],
|
||||
ShowBankDropdown: false,
|
||||
FilteredBankBranches: [],
|
||||
|
||||
// 編輯用空白物件範本
|
||||
EmptyItem: {
|
||||
編號: 0,
|
||||
講師_非講師編號: "",
|
||||
生效日期: "",
|
||||
憑證類別: "",
|
||||
扣繳類別: "",
|
||||
匯款戶名: "",
|
||||
匯款帳號: "",
|
||||
銀行分行: "",
|
||||
銀行分行代碼: "",
|
||||
備註: ""
|
||||
},
|
||||
|
||||
// 目前正在編輯的項目
|
||||
EditingItem: {},
|
||||
|
||||
// 編輯表單錯誤訊息
|
||||
EditErrorMessages: {
|
||||
講師_非講師編號: "",
|
||||
生效日期: "",
|
||||
憑證類別: "",
|
||||
扣繳類別: "",
|
||||
匯款戶名: "",
|
||||
匯款帳號: "",
|
||||
銀行分行: "",
|
||||
銀行分行代碼: "",
|
||||
備註: "",
|
||||
Clear() {
|
||||
this.講師_非講師編號 = "";
|
||||
this.生效日期 = "";
|
||||
this.憑證類別 = "";
|
||||
this.扣繳類別 = "";
|
||||
this.匯款戶名 = "";
|
||||
this.匯款帳號 = "";
|
||||
this.銀行分行 = "";
|
||||
this.銀行分行代碼 = "";
|
||||
this.備註 = "";
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
// 從全域變數取得初始資料
|
||||
this.人員類別 = V人員類別;
|
||||
this.講師編號 = V講師編號;
|
||||
this.講師姓名 = V講師姓名;
|
||||
this.憑證類別Options = V憑證類別;
|
||||
this.扣繳類別Options = V扣繳類別;
|
||||
this.銀行分行 = V銀行分行;
|
||||
},
|
||||
mounted() {
|
||||
this.SelectPage();
|
||||
},
|
||||
methods: {
|
||||
// === 資料載入方法 ===
|
||||
async SelectPage() {
|
||||
try {
|
||||
// 使用 ASMX Web Service
|
||||
const result = await this.callApiWithErrorMsg('付款設定', 'SelectPage', {
|
||||
人員類別: this.人員類別,
|
||||
講師編號: this.講師編號,
|
||||
PageIndex: this.PageIndex,
|
||||
PageSize: this.PageSize
|
||||
}, '載入資料');
|
||||
const items = this.ConvertDatesInObject(result.Data || [], '-');
|
||||
this.Items = items;
|
||||
this.Total = result.count || 0;
|
||||
} catch (error) {
|
||||
console.error('SelectPage error:', error);
|
||||
}
|
||||
},
|
||||
|
||||
// === 新增/編輯/刪除方法 ===
|
||||
Append() {
|
||||
this.EditingItem = JSON.parse(JSON.stringify(this.EmptyItem));
|
||||
this.EditingItem.講師_非講師編號 = this.講師編號;
|
||||
this.EditingItem.備註 = '更新內容:'
|
||||
this.Appending = true;
|
||||
},
|
||||
|
||||
Edit(item, index) {
|
||||
this.EditingItem = JSON.parse(JSON.stringify(item));
|
||||
this.EditingItem.Index = index;
|
||||
this.Editing = true;
|
||||
},
|
||||
|
||||
async Save() {
|
||||
this.EditErrorMessages.Clear();
|
||||
|
||||
let messages = [];
|
||||
// 驗證資料
|
||||
if (!this.EditingItem.生效日期) {
|
||||
this.EditErrorMessages.生效日期 = '請填寫';
|
||||
messages.push('請填寫生效日期');
|
||||
}
|
||||
if (!this.EditingItem.憑證類別) {
|
||||
this.EditErrorMessages.憑證類別 = '請選擇';
|
||||
messages.push('請選擇憑證類別');
|
||||
}
|
||||
if (!this.EditingItem.扣繳類別) {
|
||||
this.EditErrorMessages.扣繳類別 = '請選擇';
|
||||
messages.push('請選擇扣繳類別');
|
||||
}
|
||||
if (!this.EditingItem.備註) {
|
||||
this.EditErrorMessages.備註 = '請填寫';
|
||||
messages.push('請填寫備註');
|
||||
}
|
||||
if (this.EditingItem.備註.length > 100) {
|
||||
this.EditErrorMessages.備註 = '最多 100 個字';
|
||||
messages.push('備註最長 100 個字');
|
||||
}
|
||||
|
||||
if (messages.length > 0) {
|
||||
this.ShowWarning(messages.join('、'));
|
||||
return;
|
||||
}
|
||||
|
||||
const isNew = this.Appending;
|
||||
|
||||
try {
|
||||
await this.callApiWithErrorMsg('付款設定', 'Save',
|
||||
{
|
||||
itemStr: JSON.stringify(this.EditingItem),
|
||||
isNew: isNew
|
||||
},
|
||||
'儲存'
|
||||
);
|
||||
this.ShowSuccess('儲存成功');
|
||||
this.Appending = false;
|
||||
this.Editing = false;
|
||||
this.EditingItem = {};
|
||||
this.SelectPage(); // 重新載入資料
|
||||
} catch (error) {
|
||||
console.error('付款設定 Save error:', error);
|
||||
this.ShowError('儲存時發生錯誤: ' + (error.message || '請檢查網路連線'));
|
||||
}
|
||||
},
|
||||
|
||||
Cancel() {
|
||||
this.EditingItem = {};
|
||||
this.Appending = false;
|
||||
this.Editing = false;
|
||||
},
|
||||
|
||||
async Delete(item, index) {
|
||||
if (!confirm('確定要刪除此筆資料?')) return;
|
||||
|
||||
try {
|
||||
await this.callApiWithErrorMsg('付款設定', 'Delete',
|
||||
{
|
||||
編號: item.編號,
|
||||
DB_APPNO: item.DB_APPNO
|
||||
},
|
||||
'儲存'
|
||||
);
|
||||
this.ShowSuccess('刪除成功');
|
||||
this.SelectPage(); // 重新載入資料
|
||||
} catch (error) {
|
||||
console.error('付款設定 Delete error:', error);
|
||||
this.ShowError('刪除時發生錯誤: ' + (error.message || '請檢查網路連線'));
|
||||
}
|
||||
},
|
||||
|
||||
GetOptionName(options, value) {
|
||||
const option = options.find(o => o.Value === value);
|
||||
return option ? option.Name : '';
|
||||
},
|
||||
|
||||
// === 銀行分行浮動選單方法 ===
|
||||
FilterBankBranch() {
|
||||
const keyword = this.EditingItem.銀行分行代碼;
|
||||
if (!keyword || keyword.trim() === '') {
|
||||
this.FilteredBankBranches = this.銀行分行.slice(0, 20);
|
||||
return;
|
||||
}
|
||||
|
||||
const lowerKeyword = keyword.toLowerCase();
|
||||
this.FilteredBankBranches = this.銀行分行.filter(branch => {
|
||||
return branch.Value.toLowerCase().includes(lowerKeyword) ||
|
||||
branch.Name.toLowerCase().includes(lowerKeyword);
|
||||
}).slice(0, 20);
|
||||
},
|
||||
|
||||
ShowBankBranchList() {
|
||||
this.FilterBankBranch();
|
||||
this.ShowBankDropdown = true;
|
||||
},
|
||||
|
||||
HideBankBranchList() {
|
||||
setTimeout(() => {
|
||||
this.ShowBankDropdown = false;
|
||||
}, 200);
|
||||
},
|
||||
|
||||
SelectBankBranch(branch) {
|
||||
this.EditingItem.銀行分行代碼 = branch.Value;
|
||||
this.EditingItem.銀行分行 = branch.Name;
|
||||
this.ShowBankDropdown = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,455 @@
|
||||
function CreateTrainFareApp() {
|
||||
const loadMarker = document.getElementById('TrainFareAPP');
|
||||
if (loadMarker) {
|
||||
DoCreateTrainFareApp();
|
||||
}
|
||||
}
|
||||
|
||||
function DoCreateTrainFareApp() {
|
||||
trainFareApp = new Vue({
|
||||
el: "#TrainFareAPP",
|
||||
mixins: [DateMixin, ToastMixin, ApiMixin],
|
||||
data: {
|
||||
versions: [],
|
||||
selectedVersionId: '',
|
||||
isEditMode: false,
|
||||
|
||||
originStations: [],
|
||||
destStations: [],
|
||||
fareData: {},
|
||||
editFareData: {},
|
||||
|
||||
loading: false
|
||||
},
|
||||
mounted() {
|
||||
this.LoadVersions();
|
||||
},
|
||||
methods: {
|
||||
async LoadVersions() {
|
||||
try {
|
||||
const result = await this.callApiWithErrorMsg('火車票價維護', 'GetVersions', {}, '載入版本清單');
|
||||
this.versions = result.Data || [];
|
||||
if (!this.selectedVersionId && this.versions.length > 0) {
|
||||
this.selectedVersionId = this.versions[0].Value;
|
||||
this.LoadFareTable();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('LoadVersions error:', error);
|
||||
}
|
||||
},
|
||||
|
||||
async LoadFareTable() {
|
||||
if (!this.selectedVersionId) {
|
||||
this.originStations = [];
|
||||
this.destStations = [];
|
||||
this.fareData = {};
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.loading = true;
|
||||
const result = await this.callApiWithErrorMsg(
|
||||
'火車票價維護',
|
||||
'GetFareTable',
|
||||
{ versionId: this.selectedVersionId },
|
||||
'載入票價表'
|
||||
);
|
||||
|
||||
const data = result.Data || {};
|
||||
this.originStations = data.originStations || [];
|
||||
this.destStations = data.destStations || [];
|
||||
this.fareData = data.fares || {};
|
||||
this.editFareData = JSON.parse(JSON.stringify(this.fareData));
|
||||
this.isEditMode = false;
|
||||
} catch (error) {
|
||||
console.error('LoadFareTable error:', error);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
EnterEditMode() {
|
||||
this.editFareData = JSON.parse(JSON.stringify(this.fareData));
|
||||
this.isEditMode = true;
|
||||
},
|
||||
|
||||
CancelEdit() {
|
||||
this.editFareData = {};
|
||||
this.isEditMode = false;
|
||||
},
|
||||
|
||||
async SaveChanges() {
|
||||
if (!this.selectedVersionId) {
|
||||
this.ShowWarning('請先選擇版本');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.loading = true;
|
||||
await this.callApiWithErrorMsg(
|
||||
'火車票價維護',
|
||||
'SaveFareTable',
|
||||
{
|
||||
versionId: this.selectedVersionId,
|
||||
fares: JSON.stringify(this.editFareData)
|
||||
},
|
||||
'儲存'
|
||||
);
|
||||
this.ShowSuccess('儲存成功');
|
||||
this.isEditMode = false;
|
||||
this.LoadVersions();
|
||||
this.LoadFareTable();
|
||||
} catch (error) {
|
||||
console.error('SaveChanges error:', error);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async DeleteVersion() {
|
||||
if (!this.selectedVersionId) {
|
||||
this.ShowWarning('請先選擇版本');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm(`確定要刪除版本 ${this.selectedVersionId} 嗎?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.loading = true;
|
||||
await this.callApiWithErrorMsg(
|
||||
'火車票價維護',
|
||||
'DeleteVersion',
|
||||
{ versionId: this.selectedVersionId },
|
||||
'刪除'
|
||||
);
|
||||
this.ShowSuccess('版本刪除成功');
|
||||
this.selectedVersionId = '';
|
||||
this.LoadVersions();
|
||||
this.LoadFareTable();
|
||||
} catch (error) {
|
||||
console.error('DeleteVersion error:', error);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
GetFare(origin, destination) {
|
||||
const key = `${origin}_${destination}`;
|
||||
return this.fareData[key] || '';
|
||||
},
|
||||
|
||||
GetFareInput(origin, destination) {
|
||||
if (origin === destination) return null;
|
||||
const key = `${origin}_${destination}`;
|
||||
return this.editFareData[key] || null;
|
||||
},
|
||||
|
||||
SetFareInput(origin, destination, value) {
|
||||
if (origin === destination) return;
|
||||
const key = `${origin}_${destination}`;
|
||||
// 轉換為整數,空值或非數字時設為 0
|
||||
const numValue = parseInt(value) || 0;
|
||||
this.$set(this.editFareData, key, numValue);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function CreateTrainFareImportExportApp() {
|
||||
const loadMarker = document.getElementById('TrainFareImportExportAPP');
|
||||
if (loadMarker) {
|
||||
DoCreateTrainFareImportExportApp();
|
||||
}
|
||||
}
|
||||
|
||||
function DoCreateTrainFareImportExportApp() {
|
||||
const importExportApp = new Vue({
|
||||
el: "#TrainFareImportExportAPP",
|
||||
mixins: [DateMixin, ToastMixin, ApiMixin, Base64Mixin],
|
||||
data: {
|
||||
activeTab: 'export',
|
||||
versions: [],
|
||||
exportVersionId: '',
|
||||
exportResponse: null,
|
||||
|
||||
selectedFile: null,
|
||||
uploadedFilePath: null,
|
||||
importPreviewData: {},
|
||||
importOriginStations: [],
|
||||
importDestStations: [],
|
||||
checkResult: null,
|
||||
|
||||
loading: false
|
||||
},
|
||||
computed: {
|
||||
isImportButtonDisabled() {
|
||||
// 匯入按鈕禁用條件:
|
||||
// 1. 沒有上傳檔案
|
||||
// 2. 沒有驗證結果
|
||||
// 3. 驗證失敗
|
||||
// 4. 驗證未完成(success 不為 true)
|
||||
return !this.uploadedFilePath ||
|
||||
!this.checkResult ||
|
||||
!this.checkResult.success;
|
||||
},
|
||||
exportOriginStations() {
|
||||
return this.exportResponse?.Data?.originStations || [];
|
||||
},
|
||||
exportDestStations() {
|
||||
return this.exportResponse?.Data?.destStations || [];
|
||||
},
|
||||
exportData() {
|
||||
return this.exportResponse?.Data?.fares || {};
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.LoadVersions();
|
||||
},
|
||||
watch: {
|
||||
exportVersionId() {
|
||||
// 版本選擇改變時,重置匯出數據
|
||||
this.exportResponse = null;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async LoadVersions() {
|
||||
try {
|
||||
const result = await this.callApiWithErrorMsg('火車票價維護', 'GetVersions', {}, '載入版本清單');
|
||||
this.versions = result.Data || [];
|
||||
} catch (error) {
|
||||
console.error('LoadVersions error:', error);
|
||||
}
|
||||
},
|
||||
|
||||
async DownloadTemplate() {
|
||||
try {
|
||||
const result = await this.callApiWithErrorMsg(
|
||||
'火車票價維護',
|
||||
'GetExcelTemplate',
|
||||
{},
|
||||
'下載範本'
|
||||
);
|
||||
|
||||
// 下載 Excel 檔案
|
||||
const excelData = result.Data;
|
||||
if (!excelData) {
|
||||
this.ShowError('下載失敗:未取得範本檔案資料');
|
||||
return;
|
||||
}
|
||||
|
||||
const blob = this.Base64ToBlob(excelData);
|
||||
const link = document.createElement('a');
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.download = '火車票價_匯入範本.xlsx';
|
||||
link.click();
|
||||
|
||||
this.ShowSuccess('範本下載成功');
|
||||
} catch (error) {
|
||||
console.error('DownloadTemplate error:', error);
|
||||
}
|
||||
},
|
||||
|
||||
async ExportData() {
|
||||
if (!this.exportVersionId) {
|
||||
this.ShowWarning('請選擇版本');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.loading = true;
|
||||
const result = await this.callApiWithErrorMsg(
|
||||
'火車票價維護',
|
||||
'ExportFareData',
|
||||
{ versionId: this.exportVersionId },
|
||||
'匯出'
|
||||
);
|
||||
|
||||
// 儲存完整響應以供計算屬性使用
|
||||
this.exportResponse = result;
|
||||
|
||||
// 下載 Excel 檔案
|
||||
const excelData = result.ExcelData;
|
||||
if (!excelData) {
|
||||
this.ShowError('匯出失敗:未取得 Excel 檔案資料');
|
||||
return;
|
||||
}
|
||||
|
||||
const blob = this.Base64ToBlob(excelData);
|
||||
const link = document.createElement('a');
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.download = `火車票價_版本${this.exportVersionId}_${new Date().getTime()}.xlsx`;
|
||||
link.click();
|
||||
|
||||
this.ShowSuccess('匯出成功');
|
||||
} catch (error) {
|
||||
console.error('ExportData error:', error);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
OnFileSelected(event) {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
this.selectedFile = file;
|
||||
this.checkResult = null;
|
||||
this.importPreviewData = {};
|
||||
this.importOriginStations = [];
|
||||
this.importDestStations = [];
|
||||
|
||||
this.UploadFile(file);
|
||||
},
|
||||
|
||||
UploadFile(file) {
|
||||
if (!file) {
|
||||
this.ShowError('請選擇檔案');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
// 使用 .ashx Handler 上傳檔案
|
||||
const uploadUrl = `${API_BASE}/TrainFareUploadHandler.ashx`;
|
||||
|
||||
fetch(uploadUrl, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return response.text();
|
||||
})
|
||||
.then(text => {
|
||||
try {
|
||||
const result = JSON.parse(text);
|
||||
|
||||
if (result && result.Success) {
|
||||
const uploadedFilePath = result.Data;
|
||||
this.ShowSuccess('檔案上傳成功');
|
||||
// 立即驗證上傳的檔案
|
||||
this.ValidateUploadedFile(uploadedFilePath);
|
||||
} else {
|
||||
const errorMsg = result ? result.Message : '上傳失敗';
|
||||
this.ShowError('上傳失敗:' + errorMsg);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Parse response error:', e);
|
||||
console.error('Response text:', text);
|
||||
this.ShowError('檔案上傳失敗:回應格式錯誤');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Upload error:', error);
|
||||
this.ShowError('檔案上傳失敗:' + error.message);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('UploadFile error:', error);
|
||||
this.ShowError('檔案上傳出錯:' + error.message);
|
||||
}
|
||||
},
|
||||
|
||||
async ValidateUploadedFile(filePath) {
|
||||
try {
|
||||
const result = await this.callApiWithErrorMsg(
|
||||
'火車票價維護',
|
||||
'ValidateImportFile',
|
||||
{ filePath: filePath },
|
||||
'驗證檔案'
|
||||
);
|
||||
|
||||
this.checkResult = {
|
||||
success: result.Success,
|
||||
errors: result.Data?.Errors || []
|
||||
};
|
||||
|
||||
if (this.checkResult.success) {
|
||||
// 解析預覽資料
|
||||
this.uploadedFilePath = filePath;
|
||||
this.importPreviewData = result.Data?.Fares || {};
|
||||
this.importOriginStations = result.Data?.OriginStations || [];
|
||||
this.importDestStations = result.Data?.DestStations || [];
|
||||
this.ShowSuccess('檔案驗證成功');
|
||||
} else {
|
||||
this.ShowError('檔案驗證失敗');
|
||||
this.uploadedFilePath = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('ValidateUploadedFile error:', error);
|
||||
this.uploadedFilePath = null;
|
||||
}
|
||||
},
|
||||
|
||||
CancelImport() {
|
||||
this.selectedFile = null;
|
||||
this.uploadedFilePath = null;
|
||||
this.checkResult = null;
|
||||
this.importPreviewData = {};
|
||||
this.importOriginStations = [];
|
||||
this.importDestStations = [];
|
||||
const fileInput = document.querySelector('input[type="file"]');
|
||||
if (fileInput) {
|
||||
fileInput.value = '';
|
||||
}
|
||||
},
|
||||
|
||||
async ImportData() {
|
||||
// 匯入按鈕已被禁用,不應該執行到這裡
|
||||
// 但仍保留安全檢查作為雙重保障
|
||||
if (!this.uploadedFilePath || !this.checkResult || !this.checkResult.success) {
|
||||
this.ShowWarning('請先上傳並驗證檔案');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.loading = true;
|
||||
const result = await this.callApiWithErrorMsg(
|
||||
'火車票價維護',
|
||||
'ImportFareData',
|
||||
{
|
||||
mode: 'new',
|
||||
versionId: null,
|
||||
filePath: this.uploadedFilePath
|
||||
},
|
||||
'匯入'
|
||||
);
|
||||
|
||||
this.ShowSuccess('匯入成功');
|
||||
this.CancelImport();
|
||||
|
||||
// 更新版本清單
|
||||
if (trainFareApp) {
|
||||
trainFareApp.LoadVersions();
|
||||
}
|
||||
this.LoadVersions();
|
||||
} catch (error) {
|
||||
console.error('ImportData error:', error);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
GetExportFare(origin, destination) {
|
||||
const key = `${origin}_${destination}`;
|
||||
return this.exportData[key] || '';
|
||||
},
|
||||
|
||||
GetImportFare(origin, destination) {
|
||||
const key = `${origin}_${destination}`;
|
||||
return this.importPreviewData[key] || '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//// 頁面載入時建立 Vue 應用
|
||||
//document.addEventListener('DOMContentLoaded', function () {
|
||||
// CreateTrainFareApp();
|
||||
// CreateTrainFareImportExportApp();
|
||||
//});
|
||||
@@ -0,0 +1,641 @@
|
||||
let taxReceiptApp = null;
|
||||
|
||||
function CreateTaxReceiptApp() {
|
||||
const loadMarker = document.getElementById('taxReceiptApp');
|
||||
if (loadMarker) {
|
||||
DoCreateTaxReceiptApp();
|
||||
}
|
||||
}
|
||||
|
||||
function DoCreateTaxReceiptApp() {
|
||||
taxReceiptApp = new Vue({
|
||||
el: '#taxReceiptApp',
|
||||
mixins: [ToastMixin, ApiMixin],
|
||||
data: {
|
||||
activeTab: 0,
|
||||
currentYear: '',
|
||||
currentClosingMonth: '',
|
||||
newClosingMonth: '',
|
||||
loading: false,
|
||||
message: '',
|
||||
messageType: 'info',
|
||||
summaryTable: {
|
||||
startDate: '',
|
||||
endDate: '',
|
||||
identity: '',
|
||||
paymentMethod: '',
|
||||
identityOptions: [],
|
||||
paymentMethodOptions: [],
|
||||
dataList: [],
|
||||
loading: false,
|
||||
message: '',
|
||||
messageType: 'info'
|
||||
},
|
||||
detailTable: {
|
||||
startDate: '',
|
||||
endDate: '',
|
||||
identity: '',
|
||||
certificateType: '',
|
||||
identityOptions: [],
|
||||
certificateTypeOptions: [],
|
||||
dataList: [],
|
||||
selectAll: false,
|
||||
senderEmail: '',
|
||||
emailSubject: '',
|
||||
emailBody: '',
|
||||
loading: false,
|
||||
message: '',
|
||||
messageType: 'info',
|
||||
emailMessage: '',
|
||||
emailMessageType: 'info',
|
||||
previewData: [],
|
||||
showEmailPreview: false
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.initializeData();
|
||||
this.loadSummaryTableDropdowns();
|
||||
this.loadDetailTableDropdowns();
|
||||
// 確保預覽窗口初始為關閉狀態
|
||||
this.detailTable.showEmailPreview = false;
|
||||
this.detailTable.previewData = [];
|
||||
},
|
||||
mounted() {
|
||||
this.loadClosingMonthData();
|
||||
// 再次確保預覽窗口初始為關閉狀態
|
||||
this.$nextTick(() => {
|
||||
this.detailTable.showEmailPreview = false;
|
||||
this.detailTable.previewData = [];
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
initializeData() {
|
||||
// 取得現行年度
|
||||
const now = new Date();
|
||||
this.currentYear = now.getFullYear().toString();
|
||||
},
|
||||
|
||||
async loadClosingMonthData() {
|
||||
try {
|
||||
const result = await this.callApiWithErrorMsg(
|
||||
'領據處理',
|
||||
'GetClosingMonth',
|
||||
{},
|
||||
'載入關帳年月資料'
|
||||
);
|
||||
|
||||
if (result && result.Data) {
|
||||
this.currentClosingMonth = result.Data.currentClosingMonth || '';
|
||||
// 初始化新關帳年月為目前月份
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0');
|
||||
this.newClosingMonth = `${year}-${month}`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('載入關帳年月失敗:', error);
|
||||
this.showMessage('載入關帳年月失敗: ' + (error.message || '請檢查網路連線'), 'danger');
|
||||
}
|
||||
},
|
||||
|
||||
validateMonth() {
|
||||
if (!this.newClosingMonth) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 驗證格式 YYYY-MM
|
||||
const pattern = /^\d{4}-\d{2}$/;
|
||||
if (!pattern.test(this.newClosingMonth)) {
|
||||
this.showMessage('年月格式不正確,請使用 YYYY-MM 格式', 'danger');
|
||||
this.newClosingMonth = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// 驗證年份
|
||||
const [year, month] = this.newClosingMonth.split('-');
|
||||
const yearNum = parseInt(year);
|
||||
const monthNum = parseInt(month);
|
||||
|
||||
// 月份範圍驗證
|
||||
if (monthNum < 1 || monthNum > 12) {
|
||||
this.showMessage('月份必須介於 01 到 12 之間', 'danger');
|
||||
this.newClosingMonth = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// 年份不能超過現在
|
||||
const currentYear = new Date().getFullYear();
|
||||
if (yearNum > currentYear) {
|
||||
this.showMessage('年份不能超過現在', 'danger');
|
||||
this.newClosingMonth = '';
|
||||
return;
|
||||
}
|
||||
},
|
||||
|
||||
async saveClosingMonth() {
|
||||
if (!this.newClosingMonth) {
|
||||
this.showMessage('請輸入調整的關帳年月', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// 再次驗證
|
||||
this.validateMonth();
|
||||
if (!this.newClosingMonth) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 確認操作
|
||||
if (!confirm(`確認要將領據關帳年月調整為 ${this.newClosingMonth} 嗎?\n此操作將影響領據的修改權限。`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
|
||||
try {
|
||||
const params = {
|
||||
closingMonth: this.newClosingMonth
|
||||
};
|
||||
|
||||
const result = await this.callApiWithErrorMsg(
|
||||
'領據處理',
|
||||
'UpdateClosingMonth',
|
||||
params,
|
||||
'保存關帳年月'
|
||||
);
|
||||
|
||||
if (result && result.Success) {
|
||||
this.showMessage('關帳年月已成功更新', 'success');
|
||||
this.currentClosingMonth = this.newClosingMonth;
|
||||
// 清除輸入欄位
|
||||
setTimeout(() => {
|
||||
this.clearForm();
|
||||
}, 1500);
|
||||
} else {
|
||||
this.showMessage(result.Message || '保存失敗,請稍後重試', 'danger');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存失敗:', error);
|
||||
this.showMessage('保存失敗: ' + (error.message || '請檢查網路連線'), 'danger');
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
clearForm() {
|
||||
this.newClosingMonth = '';
|
||||
this.message = '';
|
||||
},
|
||||
|
||||
showMessage(text, type = 'info') {
|
||||
this.message = text;
|
||||
this.messageType = type;
|
||||
|
||||
// 3 秒後自動隱藏訊息
|
||||
if (type === 'success' || type === 'info') {
|
||||
setTimeout(() => {
|
||||
this.message = '';
|
||||
}, 3000);
|
||||
}
|
||||
},
|
||||
|
||||
async loadSummaryTableDropdowns() {
|
||||
try {
|
||||
// 加載身分別選項
|
||||
const identityResult = await this.callApiWithErrorMsg(
|
||||
'領據處理',
|
||||
'GetDropdownLists',
|
||||
{ key: '身分別' },
|
||||
'載入身分別選項'
|
||||
);
|
||||
if (identityResult && identityResult.Data) {
|
||||
this.summaryTable.identityOptions = identityResult.Data.filter(item => item.Value !== '');
|
||||
}
|
||||
|
||||
// 加載付款方式選項
|
||||
const paymentResult = await this.callApiWithErrorMsg(
|
||||
'領據處理',
|
||||
'GetDropdownLists',
|
||||
{ key: '付款方式' },
|
||||
'載入付款方式選項'
|
||||
);
|
||||
if (paymentResult && paymentResult.Data) {
|
||||
this.summaryTable.paymentMethodOptions = paymentResult.Data.filter(item => item.Value !== '');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加載下拉清單失敗:', error);
|
||||
}
|
||||
},
|
||||
|
||||
async searchSummaryTable() {
|
||||
this.summaryTable.loading = true;
|
||||
this.summaryTable.message = '';
|
||||
this.summaryTable.dataList = [];
|
||||
|
||||
try {
|
||||
const params = {
|
||||
起日: this.summaryTable.startDate ? this.parseDate(this.summaryTable.startDate) : null,
|
||||
迄日: this.summaryTable.endDate ? this.parseDate(this.summaryTable.endDate) : null,
|
||||
身分別: this.summaryTable.identity || null,
|
||||
付款方式: this.summaryTable.paymentMethod || null,
|
||||
輸出方式: 'DATA'
|
||||
};
|
||||
|
||||
const result = await this.callApiWithErrorMsg(
|
||||
'領據處理',
|
||||
'SearchSummaryTable',
|
||||
params,
|
||||
'查詢個人費用彙整表'
|
||||
);
|
||||
|
||||
if (result && result.Success) {
|
||||
this.summaryTable.dataList = result.Data || [];
|
||||
if (this.summaryTable.dataList.length === 0) {
|
||||
this.summaryTable.message = '查詢完成,但未找到符合條件的資料';
|
||||
this.summaryTable.messageType = 'info';
|
||||
} else {
|
||||
this.summaryTable.message = `查詢完成,共找到 ${this.summaryTable.dataList.length} 筆資料`;
|
||||
this.summaryTable.messageType = 'success';
|
||||
}
|
||||
} else {
|
||||
this.summaryTable.message = result.Message || '查詢失敗,請稍後重試';
|
||||
this.summaryTable.messageType = 'danger';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('查詢失敗:', error);
|
||||
this.summaryTable.message = '查詢失敗: ' + (error.message || '請檢查網路連線');
|
||||
this.summaryTable.messageType = 'danger';
|
||||
} finally {
|
||||
this.summaryTable.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async exportSummaryTable() {
|
||||
if (this.summaryTable.dataList.length === 0) {
|
||||
this.summaryTable.message = '沒有資料可以匯出,請先執行查詢';
|
||||
this.summaryTable.messageType = 'warning';
|
||||
return;
|
||||
}
|
||||
|
||||
this.summaryTable.loading = true;
|
||||
this.summaryTable.message = '';
|
||||
|
||||
try {
|
||||
const params = {
|
||||
起日: this.summaryTable.startDate ? this.parseDate(this.summaryTable.startDate) : null,
|
||||
迄日: this.summaryTable.endDate ? this.parseDate(this.summaryTable.endDate) : null,
|
||||
身分別: this.summaryTable.identity || null,
|
||||
付款方式: this.summaryTable.paymentMethod || null,
|
||||
輸出方式: 'EXPORT'
|
||||
};
|
||||
|
||||
const result = await this.callApiWithErrorMsg(
|
||||
'領據處理',
|
||||
'SearchSummaryTable',
|
||||
params,
|
||||
'匯出個人費用彙整表'
|
||||
);
|
||||
|
||||
if (result && result.Success && result.Data) {
|
||||
// 下載 Excel 檔案
|
||||
const excelData = result.Data;
|
||||
if (excelData.excelData && excelData.fileName) {
|
||||
this.downloadExcel(excelData.excelData, excelData.fileName);
|
||||
this.summaryTable.message = '匯出成功';
|
||||
this.summaryTable.messageType = 'success';
|
||||
} else {
|
||||
this.summaryTable.message = '匯出失敗:無法取得檔案';
|
||||
this.summaryTable.messageType = 'danger';
|
||||
}
|
||||
} else {
|
||||
this.summaryTable.message = result.Message || '匯出失敗,請稍後重試';
|
||||
this.summaryTable.messageType = 'danger';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('匯出失敗:', error);
|
||||
this.summaryTable.message = '匯出失敗: ' + (error.message || '請檢查網路連線');
|
||||
this.summaryTable.messageType = 'danger';
|
||||
} finally {
|
||||
this.summaryTable.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
parseDate(dateString) {
|
||||
if (!dateString) return null;
|
||||
const date = new Date(dateString);
|
||||
return date.toISOString().split('T')[0];
|
||||
},
|
||||
|
||||
formatCurrency(value) {
|
||||
if (!value) return '0';
|
||||
return new Intl.NumberFormat('zh-TW', {
|
||||
style: 'currency',
|
||||
currency: 'TWD',
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0
|
||||
}).format(value);
|
||||
},
|
||||
|
||||
downloadExcel(excelBase64, fileName) {
|
||||
const binaryString = atob(excelBase64);
|
||||
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();
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(link);
|
||||
},
|
||||
|
||||
async loadDetailTableDropdowns() {
|
||||
try {
|
||||
// 加載身分別選項
|
||||
const identityResult = await this.callApiWithErrorMsg(
|
||||
'領據處理',
|
||||
'GetDropdownLists',
|
||||
{ key: '身分別' },
|
||||
'載入身分別選項'
|
||||
);
|
||||
if (identityResult && identityResult.Data) {
|
||||
this.detailTable.identityOptions = identityResult.Data.filter(item => item.Value !== '');
|
||||
}
|
||||
|
||||
// 加載憑證類別選項
|
||||
const certResult = await this.callApiWithErrorMsg(
|
||||
'領據處理',
|
||||
'GetDropdownLists',
|
||||
{ key: '憑證類別' },
|
||||
'載入憑證類別選項'
|
||||
);
|
||||
if (certResult && certResult.Data) {
|
||||
this.detailTable.certificateTypeOptions = certResult.Data.filter(item => item.Value !== '');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加載下拉清單失敗:', error);
|
||||
}
|
||||
},
|
||||
|
||||
async searchDetailTable() {
|
||||
this.detailTable.loading = true;
|
||||
this.detailTable.message = '';
|
||||
this.detailTable.dataList = [];
|
||||
this.detailTable.selectAll = false;
|
||||
|
||||
try {
|
||||
const params = {
|
||||
起日: this.detailTable.startDate ? this.parseDate(this.detailTable.startDate) : null,
|
||||
迄日: this.detailTable.endDate ? this.parseDate(this.detailTable.endDate) : null,
|
||||
身分別: this.detailTable.identity || null,
|
||||
憑證類別: this.detailTable.certificateType || null,
|
||||
輸出方式: 'DATA'
|
||||
};
|
||||
|
||||
const result = await this.callApiWithErrorMsg(
|
||||
'領據處理',
|
||||
'SearchDetailTable',
|
||||
params,
|
||||
'查詢扣繳明細'
|
||||
);
|
||||
|
||||
if (result && result.Success) {
|
||||
var resultData = (result.Data || []).map(item => ({
|
||||
...item,
|
||||
selected: false,
|
||||
showDetail: false
|
||||
}));
|
||||
this.detailTable.dataList = resultData;
|
||||
|
||||
if (this.detailTable.dataList.length === 0) {
|
||||
this.detailTable.message = '查詢完成,但未找到符合條件的資料';
|
||||
this.detailTable.messageType = 'info';
|
||||
} else {
|
||||
this.detailTable.message = `查詢完成,共找到 ${this.detailTable.dataList.length} 筆資料`;
|
||||
this.detailTable.messageType = 'success';
|
||||
}
|
||||
} else {
|
||||
this.detailTable.message = result.Message || '查詢失敗,請稍後重試';
|
||||
this.detailTable.messageType = 'danger';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('查詢失敗:', error);
|
||||
this.detailTable.message = '查詢失敗: ' + (error.message || '請檢查網路連線');
|
||||
this.detailTable.messageType = 'danger';
|
||||
} finally {
|
||||
this.detailTable.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async exportDetailTable(kind) {
|
||||
if (this.detailTable.dataList.length === 0) {
|
||||
this.detailTable.message = '沒有資料可以匯出,請先執行查詢';
|
||||
this.detailTable.messageType = 'warning';
|
||||
return;
|
||||
}
|
||||
|
||||
this.detailTable.loading = true;
|
||||
this.detailTable.message = '';
|
||||
|
||||
try {
|
||||
const params = {
|
||||
起日: this.detailTable.startDate ? this.parseDate(this.detailTable.startDate) : null,
|
||||
迄日: this.detailTable.endDate ? this.parseDate(this.detailTable.endDate) : null,
|
||||
身分別: this.detailTable.identity || null,
|
||||
憑證類別: this.detailTable.certificateType || null,
|
||||
輸出方式: `EXPORT_${kind}`,
|
||||
};
|
||||
|
||||
const result = await this.callApiWithErrorMsg(
|
||||
'領據處理',
|
||||
'SearchDetailTable',
|
||||
params,
|
||||
'匯出扣繳明細'
|
||||
);
|
||||
|
||||
if (result && result.Success && result.Data) {
|
||||
const excelData = result.Data;
|
||||
if (excelData.excelData && excelData.fileName) {
|
||||
this.downloadExcel(excelData.excelData, excelData.fileName);
|
||||
this.detailTable.message = '匯出成功';
|
||||
this.detailTable.messageType = 'success';
|
||||
} else {
|
||||
this.detailTable.message = '匯出失敗:無法取得檔案';
|
||||
this.detailTable.messageType = 'danger';
|
||||
}
|
||||
} else {
|
||||
this.detailTable.message = result.Message || '匯出失敗,請稍後重試';
|
||||
this.detailTable.messageType = 'danger';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('匯出失敗:', error);
|
||||
this.detailTable.message = '匯出失敗: ' + (error.message || '請檢查網路連線');
|
||||
this.detailTable.messageType = 'danger';
|
||||
} finally {
|
||||
this.detailTable.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
toggleSelectAll() {
|
||||
this.detailTable.dataList.forEach(item => {
|
||||
item.selected = this.detailTable.selectAll;
|
||||
});
|
||||
},
|
||||
|
||||
async previewEmail() {
|
||||
// 檢查是否有選中的項目
|
||||
const selectedItems = this.detailTable.dataList.filter(item => item.selected);
|
||||
|
||||
if (selectedItems.length === 0) {
|
||||
this.detailTable.emailMessage = '請先勾選要發送的項目';
|
||||
this.detailTable.emailMessageType = 'warning';
|
||||
return;
|
||||
}
|
||||
|
||||
// 檢查郵件設定
|
||||
if (!this.detailTable.senderEmail) {
|
||||
this.detailTable.emailMessage = '請輸入寄件人郵箱';
|
||||
this.detailTable.emailMessageType = 'warning';
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.detailTable.emailSubject) {
|
||||
this.detailTable.emailMessage = '請輸入郵件主旨';
|
||||
this.detailTable.emailMessageType = 'warning';
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.detailTable.emailBody) {
|
||||
this.detailTable.emailMessage = '請輸入郵件內文';
|
||||
this.detailTable.emailMessageType = 'warning';
|
||||
return;
|
||||
}
|
||||
|
||||
this.detailTable.loading = true;
|
||||
this.detailTable.emailMessage = '';
|
||||
|
||||
try {
|
||||
// 提取選中項目的編號作為講師非講師編號
|
||||
const 講師非講師編號 = selectedItems.map(item => item.講師_非講師編號);
|
||||
|
||||
const params = {
|
||||
起日: this.detailTable.startDate ? this.parseDate(this.detailTable.startDate) : null,
|
||||
迄日: this.detailTable.endDate ? this.parseDate(this.detailTable.endDate) : null,
|
||||
身分別: this.detailTable.identity || null,
|
||||
憑證類別: this.detailTable.certificateType || null,
|
||||
輸出方式: 'PREVIEW',
|
||||
講師非講師編號: 講師非講師編號,
|
||||
寄件人郵箱: this.detailTable.senderEmail,
|
||||
郵件主旨: this.detailTable.emailSubject,
|
||||
郵件內文: this.detailTable.emailBody
|
||||
};
|
||||
|
||||
const result = await this.callApiWithErrorMsg(
|
||||
'領據處理',
|
||||
'SendEmail',
|
||||
params,
|
||||
'預覽郵件'
|
||||
);
|
||||
|
||||
if (result && result.Success && result.Data) {
|
||||
this.detailTable.previewData = result.Data || [];
|
||||
this.detailTable.showEmailPreview = true;
|
||||
this.detailTable.emailMessage = '';
|
||||
} else {
|
||||
this.detailTable.emailMessage = result.Message || '預覽失敗,請稍後重試';
|
||||
this.detailTable.emailMessageType = 'danger';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('預覽失敗:', error);
|
||||
this.detailTable.emailMessage = '預覽失敗: ' + (error.message || '請檢查網路連線');
|
||||
this.detailTable.emailMessageType = 'danger';
|
||||
} finally {
|
||||
this.detailTable.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
closeEmailPreview() {
|
||||
this.detailTable.showEmailPreview = false;
|
||||
this.detailTable.previewData = [];
|
||||
},
|
||||
|
||||
async sendEmail() {
|
||||
// 檢查是否有選中的項目
|
||||
const selectedItems = this.detailTable.dataList.filter(item => item.selected);
|
||||
|
||||
if (selectedItems.length === 0) {
|
||||
this.detailTable.emailMessage = '請先勾選要發送的項目';
|
||||
this.detailTable.emailMessageType = 'warning';
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm(`確認要發送郵件給 ${selectedItems.length} 位收件者嗎?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.detailTable.loading = true;
|
||||
this.detailTable.emailMessage = '';
|
||||
|
||||
try {
|
||||
// 提取選中項目的編號作為講師非講師編號
|
||||
const 講師非講師編號 = selectedItems.map(item => item.講師_非講師編號);
|
||||
|
||||
const params = {
|
||||
起日: this.detailTable.startDate ? this.parseDate(this.detailTable.startDate) : null,
|
||||
迄日: this.detailTable.endDate ? this.parseDate(this.detailTable.endDate) : null,
|
||||
身分別: this.detailTable.identity || null,
|
||||
憑證類別: this.detailTable.certificateType || null,
|
||||
輸出方式: 'SENDMAIL',
|
||||
講師非講師編號: 講師非講師編號,
|
||||
寄件人郵箱: this.detailTable.senderEmail,
|
||||
郵件主旨: this.detailTable.emailSubject,
|
||||
郵件內文: this.detailTable.emailBody
|
||||
};
|
||||
|
||||
const result = await this.callApiWithErrorMsg(
|
||||
'領據處理',
|
||||
'SendEmail',
|
||||
params,
|
||||
'發送郵件'
|
||||
);
|
||||
|
||||
if (result && result.Success) {
|
||||
this.detailTable.emailMessage = `郵件已排定發送給 ${selectedItems.length} 位收件者`;
|
||||
this.detailTable.emailMessageType = 'success';
|
||||
|
||||
// 關閉預覽小窗
|
||||
this.detailTable.showEmailPreview = false;
|
||||
this.detailTable.previewData = [];
|
||||
|
||||
// 清空勾選
|
||||
this.detailTable.dataList.forEach(item => {
|
||||
item.selected = false;
|
||||
});
|
||||
this.detailTable.selectAll = false;
|
||||
} else {
|
||||
this.detailTable.emailMessage = result.Message || '發送失敗,請稍後重試';
|
||||
this.detailTable.emailMessageType = 'danger';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('發送失敗:', error);
|
||||
this.detailTable.emailMessage = '發送失敗: ' + (error.message || '請檢查網路連線');
|
||||
this.detailTable.emailMessageType = 'danger';
|
||||
} finally {
|
||||
this.detailTable.loading = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
canSendEmail() {
|
||||
const hasSelected = this.detailTable.dataList.some(item => item.selected);
|
||||
const hasEmail = this.detailTable.senderEmail && this.detailTable.senderEmail.trim().length > 0;
|
||||
const hasSubject = this.detailTable.emailSubject && this.detailTable.emailSubject.trim().length > 0;
|
||||
const hasBody = this.detailTable.emailBody && this.detailTable.emailBody.trim().length > 0;
|
||||
return hasSelected && hasEmail && hasSubject && hasBody && !this.detailTable.loading;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
function CreateReceiptImportApp() {
|
||||
const loadMarker = document.getElementById('receiptImportApp');
|
||||
if (loadMarker) {
|
||||
DoCreateReceiptImportApp();
|
||||
}
|
||||
}
|
||||
|
||||
function DoCreateReceiptImportApp() {
|
||||
new Vue({
|
||||
el: '#receiptImportApp',
|
||||
mixins: [DateMixin, ToastMixin, ApiMixin, Base64Mixin],
|
||||
data: {
|
||||
selectedFile: null,
|
||||
isProcessing: false,
|
||||
|
||||
validationMessages: [],
|
||||
validationSuccess: false,
|
||||
validatedRowCount: 0,
|
||||
|
||||
importMessages: [],
|
||||
importSuccess: false,
|
||||
importSuccessCount: 0,
|
||||
importFailCount: 0,
|
||||
importDone: false
|
||||
},
|
||||
methods: {
|
||||
async downloadTemplate() {
|
||||
try {
|
||||
this.isProcessing = true;
|
||||
const result = await this.callApiWithErrorMsg('領據處理', 'DownloadImportTemplate', {}, '下載範本');
|
||||
|
||||
const excelData = result.Data && result.Data.excelData;
|
||||
if (!excelData) {
|
||||
this.ShowError('下載失敗:未取得範本檔案資料');
|
||||
return;
|
||||
}
|
||||
|
||||
const blob = this.Base64ToBlob(excelData);
|
||||
const link = document.createElement('a');
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.download = (result.Data && result.Data.fileName) || '領據資料匯入格式.xlsx';
|
||||
link.click();
|
||||
|
||||
this.ShowSuccess('範本下載成功');
|
||||
} catch (error) {
|
||||
console.error('downloadTemplate error:', error);
|
||||
} finally {
|
||||
this.isProcessing = false;
|
||||
}
|
||||
},
|
||||
|
||||
onFileSelected(event) {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
this.selectedFile = file;
|
||||
this.validationMessages = [];
|
||||
this.validationSuccess = false;
|
||||
this.validatedRowCount = 0;
|
||||
this.importMessages = [];
|
||||
this.importSuccess = false;
|
||||
this.importSuccessCount = 0;
|
||||
this.importFailCount = 0;
|
||||
this.importDone = false;
|
||||
},
|
||||
|
||||
uploadFile() {
|
||||
if (!this.selectedFile) {
|
||||
this.ShowWarning('請先選擇檔案');
|
||||
return;
|
||||
}
|
||||
|
||||
this.isProcessing = true;
|
||||
this.validationMessages = [];
|
||||
this.validationSuccess = false;
|
||||
this.validatedRowCount = 0;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', this.selectedFile);
|
||||
|
||||
const uploadUrl = API_BASE + '/ReceiptUploadHandler.ashx';
|
||||
|
||||
fetch(uploadUrl, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('HTTP error! status: ' + response.status);
|
||||
}
|
||||
return response.text();
|
||||
})
|
||||
.then(text => {
|
||||
try {
|
||||
const result = JSON.parse(text);
|
||||
|
||||
if (result && result.Success) {
|
||||
const uploadedFilePath = result.Data;
|
||||
this.ShowSuccess('檔案上傳成功');
|
||||
this.ValidateUploadedFile(uploadedFilePath);
|
||||
} else {
|
||||
const errorMsg = result ? result.Message : '上傳失敗';
|
||||
this.ShowError('上傳失敗:' + errorMsg);
|
||||
this.isProcessing = false;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Parse response error:', e, 'Response text:', text);
|
||||
this.ShowError('檔案上傳失敗:回應格式錯誤');
|
||||
this.isProcessing = false;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Upload error:', error);
|
||||
this.ShowError('檔案上傳失敗:' + error.message);
|
||||
this.isProcessing = false;
|
||||
});
|
||||
},
|
||||
|
||||
async ValidateUploadedFile(filePath) {
|
||||
try {
|
||||
const result = await this.callApiWithErrorMsg('領據處理', 'ValidateImportFile', { filePath: filePath }, '驗證檔案');
|
||||
|
||||
const data = result.Data || {};
|
||||
|
||||
if (data.Success) {
|
||||
this.validationSuccess = true;
|
||||
this.validatedRowCount = data.RowCount || 0;
|
||||
const warnings = data.Warnings || [];
|
||||
if (warnings.length > 0) {
|
||||
this.validationMessages = ['驗證通過,但有以下重複資料警告:'].concat(warnings);
|
||||
this.ShowWarning('驗證通過,但有重複資料,請確認後再匯入');
|
||||
} else {
|
||||
this.validationMessages = ['驗證通過'];
|
||||
this.ShowSuccess('檔案驗證成功');
|
||||
}
|
||||
} else {
|
||||
this.validationSuccess = false;
|
||||
this.validationMessages = data.Errors || ['驗證失敗'];
|
||||
this.ShowError('檔案驗證失敗');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('ValidateUploadedFile error:', error);
|
||||
this.validationSuccess = false;
|
||||
} finally {
|
||||
this.isProcessing = false;
|
||||
}
|
||||
},
|
||||
|
||||
async importData() {
|
||||
if (!this.validationSuccess) {
|
||||
this.ShowWarning('請先上傳並驗證檔案');
|
||||
return;
|
||||
}
|
||||
if (this.importDone) {
|
||||
this.ShowWarning('匯入已完成,請點擊「重新匯入」開始新的匯入');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.isProcessing = true;
|
||||
const result = await this.callApiWithErrorMsg('領據處理', 'ImportReceiptData', {}, '匯入');
|
||||
|
||||
const data = result.Data || {};
|
||||
const successCount = data.successCount || 0;
|
||||
const failCount = data.failCount || 0;
|
||||
const failedRows = data.failedRows || [];
|
||||
|
||||
this.importSuccessCount = successCount;
|
||||
this.importFailCount = failCount;
|
||||
this.importMessages = failedRows;
|
||||
this.importSuccess = (failCount === 0);
|
||||
|
||||
if (this.importSuccess) {
|
||||
this.ShowSuccess('匯入成功,共匯入 ' + successCount + ' 筆');
|
||||
} else {
|
||||
this.ShowWarning('匯入完成,成功 ' + successCount + ' 筆,失敗 ' + failCount + ' 筆');
|
||||
}
|
||||
|
||||
this.importDone = true;
|
||||
} catch (error) {
|
||||
console.error('importData error:', error);
|
||||
this.importSuccess = false;
|
||||
} finally {
|
||||
this.isProcessing = false;
|
||||
}
|
||||
},
|
||||
|
||||
resetImport() {
|
||||
this.selectedFile = null;
|
||||
this.validationMessages = [];
|
||||
this.validationSuccess = false;
|
||||
this.validatedRowCount = 0;
|
||||
this.importMessages = [];
|
||||
this.importSuccess = false;
|
||||
this.importSuccessCount = 0;
|
||||
this.importFailCount = 0;
|
||||
this.importDone = false;
|
||||
|
||||
const fileInput = document.querySelector('#receiptImportApp input[type="file"]');
|
||||
if (fileInput) {
|
||||
fileInput.value = '';
|
||||
}
|
||||
},
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,351 @@
|
||||
function CreateReceiptSearchApp() {
|
||||
const loadMarker = document.getElementById('receiptSearchApp');
|
||||
if (loadMarker) {
|
||||
DoCreateReceiptSearchApp();
|
||||
}
|
||||
}
|
||||
|
||||
function DoCreateReceiptSearchApp() {
|
||||
window.__vms = window.__vms || {}
|
||||
|
||||
if (window.__vms.receiptSearchApp)
|
||||
return;
|
||||
|
||||
console.log('Create receiptSearchApp');
|
||||
|
||||
window.__vms.receiptSearchApp = new Vue({
|
||||
el: '#receiptSearchApp',
|
||||
mixins: [DateMixin, ToastMixin, ApiMixin],
|
||||
data: {
|
||||
searchParams: {
|
||||
計畫代號: '',
|
||||
計畫名稱: '',
|
||||
活動日期_1: '',
|
||||
活動日期_2: '',
|
||||
身分別: '',
|
||||
講師_非講師編號: '',
|
||||
憑證類別: '',
|
||||
活動類別: '',
|
||||
付款方式: ''
|
||||
},
|
||||
dropdown: {
|
||||
身分別: [],
|
||||
憑證類別: [],
|
||||
付款方式: []
|
||||
},
|
||||
dataList: [],
|
||||
selectedItems: [],
|
||||
selectAll: false,
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
totalCount: 0,
|
||||
totalPages: 0,
|
||||
loading: false
|
||||
},
|
||||
created() {
|
||||
// 從 AJAX 取得查詢用下拉選單資料
|
||||
this.loadSearchDropdownLists();
|
||||
|
||||
// 取回保存的查詢條件
|
||||
this.searchParams.計畫代號 = document.getElementById('ctl00_ContentPlaceHolder1_hid計畫代號').value;
|
||||
this.searchParams.計畫名稱 = document.getElementById('ctl00_ContentPlaceHolder1_hid計畫名稱').value;
|
||||
this.searchParams.活動日期_1 = document.getElementById('ctl00_ContentPlaceHolder1_hid活動日期_1').value;
|
||||
this.searchParams.活動日期_2 = document.getElementById('ctl00_ContentPlaceHolder1_hid活動日期_2').value;
|
||||
this.searchParams.身分別 = document.getElementById('ctl00_ContentPlaceHolder1_hid身分別').value;
|
||||
this.searchParams.憑證類別 = document.getElementById('ctl00_ContentPlaceHolder1_hid憑證類別').value;
|
||||
this.searchParams.活動類別 = document.getElementById('ctl00_ContentPlaceHolder1_hid活動類別').value;
|
||||
this.searchParams.付款方式 = document.getElementById('ctl00_ContentPlaceHolder1_hid付款方式').value;
|
||||
this.currentPage = 1 * document.getElementById('ctl00_ContentPlaceHolder1_hidCurrentPage').value || 1;
|
||||
this.pageSize = 1 * document.getElementById('ctl00_ContentPlaceHolder1_hidPageSize').value || 20;
|
||||
},
|
||||
computed: {
|
||||
displayPages() {
|
||||
const pages = [];
|
||||
const start = Math.max(1, this.currentPage - 2);
|
||||
const end = Math.min(this.totalPages, this.currentPage + 2);
|
||||
for (let i = start; i <= end; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
return pages;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.search();
|
||||
},
|
||||
methods: {
|
||||
async loadSearchDropdownLists() {
|
||||
this.dropdown.身分別 = await this.loadSearchDropdownList("身分別");
|
||||
this.dropdown.憑證類別 = await this.loadSearchDropdownList("憑證類別");
|
||||
this.dropdown.付款方式 = await this.loadSearchDropdownList("付款方式");
|
||||
},
|
||||
async loadSearchDropdownList(key) {
|
||||
try {
|
||||
const result = await this.callApiWithErrorMsg(
|
||||
'領據處理',
|
||||
'GetSearchDropdownLists',
|
||||
{ key: key },
|
||||
'載入下拉選單'
|
||||
);
|
||||
return result.Data || [];
|
||||
}
|
||||
catch(error) {
|
||||
console.error('載入下拉選單失敗:', error);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
clearSearch() {
|
||||
this.searchParams.計畫代號 = '';
|
||||
this.searchParams.計畫名稱 = '';
|
||||
this.searchParams.活動日期_1 = '';
|
||||
this.searchParams.活動日期_2 = '';
|
||||
this.searchParams.身分別 = '';
|
||||
this.searchParams.憑證類別 = '';
|
||||
this.searchParams.活動類別 = '';
|
||||
this.searchParams.付款方式 = '';
|
||||
this.search();
|
||||
},
|
||||
|
||||
async search() {
|
||||
this.loading = true;
|
||||
|
||||
const params = {
|
||||
pageIndex: this.currentPage,
|
||||
pageSize: parseInt(this.pageSize),
|
||||
活動日期_1: this.searchParams.活動日期_1 || null,
|
||||
活動日期_2: this.searchParams.活動日期_2 || null,
|
||||
身分別: this.searchParams.身分別 || null,
|
||||
講師_非講師編號: this.searchParams.講師_非講師編號 || null,
|
||||
憑證類別: this.searchParams.憑證類別 || null,
|
||||
計畫代號: this.searchParams.計畫代號 || null,
|
||||
計畫名稱: this.searchParams.計畫名稱 || null,
|
||||
活動類別: this.searchParams.活動類別 || null,
|
||||
付款方式: this.searchParams.付款方式 || null,
|
||||
orderBy: ''
|
||||
};
|
||||
|
||||
// 保存查詢條件
|
||||
document.getElementById('ctl00_ContentPlaceHolder1_hid計畫代號').value = params.計畫代號;
|
||||
document.getElementById('ctl00_ContentPlaceHolder1_hid計畫名稱').value = params.計畫名稱;
|
||||
document.getElementById('ctl00_ContentPlaceHolder1_hid活動日期_1').value = params.活動日期_1;
|
||||
document.getElementById('ctl00_ContentPlaceHolder1_hid活動日期_2').value = params.活動日期_2;
|
||||
document.getElementById('ctl00_ContentPlaceHolder1_hid身分別').value = params.身分別;
|
||||
document.getElementById('ctl00_ContentPlaceHolder1_hid憑證類別').value = params.憑證類別;
|
||||
document.getElementById('ctl00_ContentPlaceHolder1_hid活動類別').value = params.活動類別;
|
||||
document.getElementById('ctl00_ContentPlaceHolder1_hid付款方式').value = params.付款方式;
|
||||
document.getElementById('ctl00_ContentPlaceHolder1_hidCurrentPage').value = this.currentPage;
|
||||
document.getElementById('ctl00_ContentPlaceHolder1_hidPageSize').value = this.pageSize;
|
||||
|
||||
try {
|
||||
let value = null;
|
||||
const result = await this.callApiWithErrorMsg(
|
||||
'領據處理',
|
||||
'SelectPage',
|
||||
params,
|
||||
'查詢'
|
||||
);
|
||||
// 使用 ConvertDatesInObject 自動轉換日期
|
||||
this.dataList = this.ConvertDatesInObject(result.Data || [], '-');
|
||||
this.totalCount = result.TotalCount;
|
||||
this.totalPages = Math.ceil(this.totalCount / this.pageSize);
|
||||
this.selectedItems = [];
|
||||
this.selectAll = false;
|
||||
}
|
||||
catch (error) {
|
||||
console.error('查詢失敗:', error);
|
||||
}
|
||||
finally {
|
||||
this.loading = false;
|
||||
};
|
||||
},
|
||||
|
||||
searchButtonClick()
|
||||
{
|
||||
this.currentPage = 1;
|
||||
this.search();
|
||||
},
|
||||
|
||||
changePage(page) {
|
||||
if (page >= 1 && page <= this.totalPages) {
|
||||
this.currentPage = page;
|
||||
this.search();
|
||||
}
|
||||
},
|
||||
|
||||
toggleSelectAll() {
|
||||
if (this.selectAll) {
|
||||
this.selectedItems = [...this.dataList];
|
||||
} else {
|
||||
this.selectedItems = [];
|
||||
}
|
||||
},
|
||||
|
||||
addNew() {
|
||||
// 切換到領據登錄與修改分頁
|
||||
document.getElementById('ctl00_ContentPlaceHolder1_hid編號').value = '';
|
||||
window.__doPostBack('ctl00$ContentPlaceHolder1$btnTab2', '');
|
||||
},
|
||||
|
||||
edit(編號) {
|
||||
// 切換到領據登錄與修改分頁,並進入該行資料的修改
|
||||
document.getElementById('ctl00_ContentPlaceHolder1_hid編號').value = 編號;
|
||||
window.__doPostBack('ctl00$ContentPlaceHolder1$btnTab2', '');
|
||||
},
|
||||
|
||||
async printSelected() {
|
||||
if (this.selectedItems.length === 0) {
|
||||
this.ShowWarning('請選擇要列印的領據');
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
try {
|
||||
const idList = this.selectedItems.map(item => item.編號);
|
||||
|
||||
const result = await this.callApiWithErrorMsg(
|
||||
'領據處理',
|
||||
'Print',
|
||||
{ idList: idList },
|
||||
'列印領據'
|
||||
);
|
||||
|
||||
if (result.Success) {
|
||||
// 下載 Excel 檔案
|
||||
if (result.Data && result.Data.fileName) {
|
||||
const link = document.createElement('a');
|
||||
link.href = `data:application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;base64,${result.Data.excelData}`;
|
||||
link.download = result.Data.fileName;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
this.ShowSuccess(result.Message);
|
||||
} else {
|
||||
this.ShowWarning(result.Message);
|
||||
}
|
||||
} else
|
||||
{
|
||||
this.ShowError('列印失敗: ' + (error.message || '請稍後重試'));
|
||||
}
|
||||
|
||||
// 列印完成後,刷新列表
|
||||
this.search();
|
||||
} catch (error) {
|
||||
this.ShowError('列印失敗: ' + (error.message || '請檢查網路連線'));
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async doExport() {
|
||||
if (this.dataList.length === 0) {
|
||||
this.ShowWarning('請先查詢再匯出');
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
try {
|
||||
const idList = this.selectedItems.map(item => item.編號);
|
||||
|
||||
const params = {
|
||||
活動日期_1: this.searchParams.活動日期_1 || null,
|
||||
活動日期_2: this.searchParams.活動日期_2 || null,
|
||||
身分別: this.searchParams.身分別 || null,
|
||||
講師_非講師編號: this.searchParams.講師_非講師編號 || null,
|
||||
憑證類別: this.searchParams.憑證類別 || null,
|
||||
計畫代號: this.searchParams.計畫代號 || null,
|
||||
計畫名稱: this.searchParams.計畫名稱 || null,
|
||||
活動類別: this.searchParams.活動類別 || null,
|
||||
付款方式: this.searchParams.付款方式 || null,
|
||||
orderBy: ''
|
||||
};
|
||||
|
||||
const result = await this.callApiWithErrorMsg(
|
||||
'領據處理',
|
||||
'Export',
|
||||
params,
|
||||
'領據匯出'
|
||||
);
|
||||
|
||||
// 下載 Excel 檔案
|
||||
if (result.Data && result.Data.fileName) {
|
||||
const link = document.createElement('a');
|
||||
link.href = `data:application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;base64,${result.Data.excelData}`;
|
||||
link.download = result.Data.fileName;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
this.ShowSuccess('匯出完成');
|
||||
} else {
|
||||
this.ShowError('匯出失敗,沒有檔案內容');
|
||||
}
|
||||
} catch (error) {
|
||||
this.ShowError('匯出失敗: ' + (error.message || '請檢查網路連線'));
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
//async voidSelected() {
|
||||
// if (this.selectedItems.length === 0) {
|
||||
// this.ShowWarning('請選擇要作廢的領據');
|
||||
// return;
|
||||
// }
|
||||
// if (!confirm('確定要作廢/取消作廢選取的領據嗎?')) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// this.loading = true;
|
||||
// try {
|
||||
// // 並行調用多個 API
|
||||
// const promises = this.selectedItems.map(item => {
|
||||
// return this.callApi('領據處理', 'Void', {
|
||||
// no: item.編號,
|
||||
// dbAppNo: item.DB_APPNO
|
||||
// });
|
||||
// });
|
||||
|
||||
// await Promise.all(promises);
|
||||
// this.ShowSuccess('作廢操作完成');
|
||||
// this.search();
|
||||
// } catch (error) {
|
||||
// console.error('Void error:', error);
|
||||
// this.ShowError('作廢操作失敗: ' + (error.message || '請檢查網路連線'));
|
||||
// } finally {
|
||||
// this.loading = false;
|
||||
// }
|
||||
//},
|
||||
|
||||
//async deleteSelected() {
|
||||
// if (this.selectedItems.length === 0) {
|
||||
// this.ShowWarning('請選擇要刪除的領據');
|
||||
// return;
|
||||
// }
|
||||
// if (!confirm('確定要刪除選取的領據嗎?此操作無法復原!')) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// this.loading = true;
|
||||
// try {
|
||||
// // 並行調用多個 API
|
||||
// const promises = this.selectedItems.map(item => {
|
||||
// return this.callApi('領據處理', 'Delete', {
|
||||
// no: item.編號,
|
||||
// dbAppNo: item.DB_APPNO
|
||||
// });
|
||||
// });
|
||||
|
||||
// await Promise.all(promises);
|
||||
// this.ShowSuccess('刪除完成');
|
||||
// this.search();
|
||||
// } catch (error) {
|
||||
// console.error('Delete error:', error);
|
||||
// this.ShowError('刪除失敗: ' + (error.message || '請檢查網路連線'));
|
||||
// } finally {
|
||||
// this.loading = false;
|
||||
// }
|
||||
//}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,363 @@
|
||||
function CreateReceiptSearchApp() {
|
||||
const loadMarker = document.getElementById('receiptSearchApp');
|
||||
if (loadMarker) {
|
||||
DoCreateReceiptSearchApp();
|
||||
}
|
||||
}
|
||||
|
||||
function DoCreateReceiptSearchApp() {
|
||||
window.__vms = window.__vms || {}
|
||||
|
||||
if (window.__vms.receiptSearchApp)
|
||||
return;
|
||||
|
||||
console.log('Create receiptSearchApp');
|
||||
|
||||
window.__vms.receiptSearchApp = new Vue({
|
||||
el: '#receiptSearchApp',
|
||||
mixins: [DateMixin, ToastMixin, ApiMixin],
|
||||
data: {
|
||||
searchParams: {
|
||||
計畫代號: '',
|
||||
計畫名稱: '',
|
||||
活動日期_1: '',
|
||||
活動日期_2: '',
|
||||
身分別: '',
|
||||
講師_非講師編號: '',
|
||||
憑證類別: '',
|
||||
活動類別: '',
|
||||
付款方式: '',
|
||||
領款人姓名: ''
|
||||
},
|
||||
dropdown: {
|
||||
身分別: [],
|
||||
憑證類別: [],
|
||||
付款方式: []
|
||||
},
|
||||
dataList: [],
|
||||
selectedItems: [],
|
||||
selectAll: false,
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
totalCount: 0,
|
||||
totalPages: 0,
|
||||
loading: false
|
||||
},
|
||||
created() {
|
||||
// 從 AJAX 取得查詢用下拉選單資料
|
||||
this.loadSearchDropdownLists();
|
||||
|
||||
// 取回保存的查詢條件
|
||||
this.searchParams.計畫代號 = document.getElementById('ctl00_ContentPlaceHolder1_hid計畫代號').value;
|
||||
this.searchParams.計畫名稱 = document.getElementById('ctl00_ContentPlaceHolder1_hid計畫名稱').value;
|
||||
this.searchParams.活動日期_1 = document.getElementById('ctl00_ContentPlaceHolder1_hid活動日期_1').value;
|
||||
this.searchParams.活動日期_2 = document.getElementById('ctl00_ContentPlaceHolder1_hid活動日期_2').value;
|
||||
this.searchParams.身分別 = document.getElementById('ctl00_ContentPlaceHolder1_hid身分別').value;
|
||||
this.searchParams.憑證類別 = document.getElementById('ctl00_ContentPlaceHolder1_hid憑證類別').value;
|
||||
this.searchParams.活動類別 = document.getElementById('ctl00_ContentPlaceHolder1_hid活動類別').value;
|
||||
this.searchParams.付款方式 = document.getElementById('ctl00_ContentPlaceHolder1_hid付款方式').value;
|
||||
this.searchParams.領款人姓名 = document.getElementById('ctl00_ContentPlaceHolder1_hid領款人姓名').value;
|
||||
this.currentPage = 1 * document.getElementById('ctl00_ContentPlaceHolder1_hidCurrentPage').value || 1;
|
||||
this.pageSize = 1 * document.getElementById('ctl00_ContentPlaceHolder1_hidPageSize').value || 20;
|
||||
},
|
||||
computed: {
|
||||
displayPages() {
|
||||
const pages = [];
|
||||
const start = Math.max(1, this.currentPage - 2);
|
||||
const end = Math.min(this.totalPages, this.currentPage + 2);
|
||||
for (let i = start; i <= end; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
return pages;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.search();
|
||||
},
|
||||
methods: {
|
||||
async loadSearchDropdownLists() {
|
||||
this.dropdown.身分別 = await this.loadSearchDropdownList("身分別");
|
||||
this.dropdown.憑證類別 = await this.loadSearchDropdownList("憑證類別");
|
||||
this.dropdown.付款方式 = await this.loadSearchDropdownList("付款方式");
|
||||
},
|
||||
async loadSearchDropdownList(key) {
|
||||
try {
|
||||
const result = await this.callApiWithErrorMsg(
|
||||
'領據處理',
|
||||
'GetSearchDropdownLists',
|
||||
{ key: key },
|
||||
'載入下拉選單'
|
||||
);
|
||||
return result.Data || [];
|
||||
}
|
||||
catch(error) {
|
||||
console.error('載入下拉選單失敗:', error);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
clearSearch() {
|
||||
this.searchParams.計畫代號 = '';
|
||||
this.searchParams.計畫名稱 = '';
|
||||
this.searchParams.活動日期_1 = '';
|
||||
this.searchParams.活動日期_2 = '';
|
||||
this.searchParams.身分別 = '';
|
||||
this.searchParams.憑證類別 = '';
|
||||
this.searchParams.活動類別 = '';
|
||||
this.searchParams.付款方式 = '';
|
||||
this.searchParams.領款人姓名 = '';
|
||||
this.currentPage = 1;
|
||||
this.search();
|
||||
},
|
||||
|
||||
async search() {
|
||||
this.loading = true;
|
||||
|
||||
const params = {
|
||||
pageIndex: this.currentPage,
|
||||
pageSize: parseInt(this.pageSize),
|
||||
活動日期_1: this.searchParams.活動日期_1 || null,
|
||||
活動日期_2: this.searchParams.活動日期_2 || null,
|
||||
身分別: this.searchParams.身分別 || null,
|
||||
講師_非講師編號: this.searchParams.講師_非講師編號 || null,
|
||||
憑證類別: this.searchParams.憑證類別 || null,
|
||||
計畫代號: this.searchParams.計畫代號 || null,
|
||||
計畫名稱: this.searchParams.計畫名稱 || null,
|
||||
活動類別: this.searchParams.活動類別 || null,
|
||||
付款方式: this.searchParams.付款方式 || null,
|
||||
領款人姓名: this.searchParams.領款人姓名 || null,
|
||||
orderBy: ''
|
||||
};
|
||||
|
||||
// 保存查詢條件
|
||||
document.getElementById('ctl00_ContentPlaceHolder1_hid計畫代號').value = params.計畫代號;
|
||||
document.getElementById('ctl00_ContentPlaceHolder1_hid計畫名稱').value = params.計畫名稱;
|
||||
document.getElementById('ctl00_ContentPlaceHolder1_hid活動日期_1').value = params.活動日期_1;
|
||||
document.getElementById('ctl00_ContentPlaceHolder1_hid活動日期_2').value = params.活動日期_2;
|
||||
document.getElementById('ctl00_ContentPlaceHolder1_hid身分別').value = params.身分別;
|
||||
document.getElementById('ctl00_ContentPlaceHolder1_hid憑證類別').value = params.憑證類別;
|
||||
document.getElementById('ctl00_ContentPlaceHolder1_hid活動類別').value = params.活動類別;
|
||||
document.getElementById('ctl00_ContentPlaceHolder1_hid付款方式').value = params.付款方式;
|
||||
document.getElementById('ctl00_ContentPlaceHolder1_hid領款人姓名').value = params.領款人姓名;
|
||||
document.getElementById('ctl00_ContentPlaceHolder1_hidCurrentPage').value = this.currentPage;
|
||||
document.getElementById('ctl00_ContentPlaceHolder1_hidPageSize').value = this.pageSize;
|
||||
|
||||
try {
|
||||
let value = null;
|
||||
const result = await this.callApiWithErrorMsg(
|
||||
'領據處理',
|
||||
'SelectPage',
|
||||
params,
|
||||
'查詢'
|
||||
);
|
||||
// 使用 ConvertDatesInObject 自動轉換日期
|
||||
this.dataList = this.ConvertDatesInObject(result.Data || [], '-');
|
||||
this.totalCount = result.TotalCount;
|
||||
this.totalPages = Math.ceil(this.totalCount / this.pageSize);
|
||||
this.selectedItems = [];
|
||||
this.selectAll = false;
|
||||
|
||||
// 花花: 儲存當前頁編號清單及總頁數,供領據處理上下筆導航使用
|
||||
document.getElementById('ctl00_ContentPlaceHolder1_hidIdList').value =
|
||||
JSON.stringify(this.dataList.map(item => item.編號));
|
||||
document.getElementById('ctl00_ContentPlaceHolder1_hidNavTotalPages').value = this.totalPages;
|
||||
}
|
||||
catch (error) {
|
||||
console.error('查詢失敗:', error);
|
||||
}
|
||||
finally {
|
||||
this.loading = false;
|
||||
};
|
||||
},
|
||||
|
||||
searchButtonClick()
|
||||
{
|
||||
this.currentPage = 1;
|
||||
this.search();
|
||||
},
|
||||
|
||||
changePage(page) {
|
||||
if (page >= 1 && page <= this.totalPages) {
|
||||
this.currentPage = page;
|
||||
this.search();
|
||||
}
|
||||
},
|
||||
|
||||
toggleSelectAll() {
|
||||
if (this.selectAll) {
|
||||
this.selectedItems = [...this.dataList];
|
||||
} else {
|
||||
this.selectedItems = [];
|
||||
}
|
||||
},
|
||||
|
||||
addNew() {
|
||||
// 切換到領據登錄與修改分頁
|
||||
document.getElementById('ctl00_ContentPlaceHolder1_hid編號').value = '';
|
||||
window.__doPostBack('ctl00$ContentPlaceHolder1$btnTab2', '');
|
||||
},
|
||||
|
||||
edit(編號) {
|
||||
// 切換到領據登錄與修改分頁,並進入該行資料的修改
|
||||
document.getElementById('ctl00_ContentPlaceHolder1_hid編號').value = 編號;
|
||||
window.__doPostBack('ctl00$ContentPlaceHolder1$btnTab2', '');
|
||||
},
|
||||
|
||||
async printSelected() {
|
||||
if (this.selectedItems.length === 0) {
|
||||
this.ShowWarning('請選擇要列印的領據');
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
try {
|
||||
const idList = this.selectedItems.map(item => item.編號);
|
||||
|
||||
const result = await this.callApiWithErrorMsg(
|
||||
'領據處理',
|
||||
'Print',
|
||||
{ idList: idList },
|
||||
'列印領據'
|
||||
);
|
||||
|
||||
if (result.Success) {
|
||||
// 下載 Excel 檔案
|
||||
if (result.Data && result.Data.fileName) {
|
||||
const link = document.createElement('a');
|
||||
link.href = `data:application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;base64,${result.Data.excelData}`;
|
||||
link.download = result.Data.fileName;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
this.ShowSuccess(result.Message);
|
||||
} else {
|
||||
this.ShowWarning(result.Message);
|
||||
}
|
||||
} else
|
||||
{
|
||||
this.ShowError('列印失敗: ' + (error.message || '請稍後重試'));
|
||||
}
|
||||
|
||||
// 列印完成後,刷新列表
|
||||
this.search();
|
||||
} catch (error) {
|
||||
this.ShowError('列印失敗: ' + (error.message || '請檢查網路連線'));
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async doExport() {
|
||||
if (this.dataList.length === 0) {
|
||||
this.ShowWarning('請先查詢再匯出');
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
try {
|
||||
const idList = this.selectedItems.map(item => item.編號);
|
||||
|
||||
const params = {
|
||||
活動日期_1: this.searchParams.活動日期_1 || null,
|
||||
活動日期_2: this.searchParams.活動日期_2 || null,
|
||||
身分別: this.searchParams.身分別 || null,
|
||||
講師_非講師編號: this.searchParams.講師_非講師編號 || null,
|
||||
憑證類別: this.searchParams.憑證類別 || null,
|
||||
計畫代號: this.searchParams.計畫代號 || null,
|
||||
計畫名稱: this.searchParams.計畫名稱 || null,
|
||||
活動類別: this.searchParams.活動類別 || null,
|
||||
付款方式: this.searchParams.付款方式 || null,
|
||||
領款人姓名: this.searchParams.領款人姓名 || null,
|
||||
orderBy: ''
|
||||
};
|
||||
|
||||
const result = await this.callApiWithErrorMsg(
|
||||
'領據處理',
|
||||
'Export',
|
||||
params,
|
||||
'領據匯出'
|
||||
);
|
||||
|
||||
// 下載 Excel 檔案
|
||||
if (result.Data && result.Data.fileName) {
|
||||
const link = document.createElement('a');
|
||||
link.href = `data:application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;base64,${result.Data.excelData}`;
|
||||
link.download = result.Data.fileName;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
this.ShowSuccess('匯出完成');
|
||||
} else {
|
||||
this.ShowError('匯出失敗,沒有檔案內容');
|
||||
}
|
||||
} catch (error) {
|
||||
this.ShowError('匯出失敗: ' + (error.message || '請檢查網路連線'));
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
//async voidSelected() {
|
||||
// if (this.selectedItems.length === 0) {
|
||||
// this.ShowWarning('請選擇要作廢的領據');
|
||||
// return;
|
||||
// }
|
||||
// if (!confirm('確定要作廢/取消作廢選取的領據嗎?')) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// this.loading = true;
|
||||
// try {
|
||||
// // 並行調用多個 API
|
||||
// const promises = this.selectedItems.map(item => {
|
||||
// return this.callApi('領據處理', 'Void', {
|
||||
// no: item.編號,
|
||||
// dbAppNo: item.DB_APPNO
|
||||
// });
|
||||
// });
|
||||
|
||||
// await Promise.all(promises);
|
||||
// this.ShowSuccess('作廢操作完成');
|
||||
// this.search();
|
||||
// } catch (error) {
|
||||
// console.error('Void error:', error);
|
||||
// this.ShowError('作廢操作失敗: ' + (error.message || '請檢查網路連線'));
|
||||
// } finally {
|
||||
// this.loading = false;
|
||||
// }
|
||||
//},
|
||||
|
||||
//async deleteSelected() {
|
||||
// if (this.selectedItems.length === 0) {
|
||||
// this.ShowWarning('請選擇要刪除的領據');
|
||||
// return;
|
||||
// }
|
||||
// if (!confirm('確定要刪除選取的領據嗎?此操作無法復原!')) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// this.loading = true;
|
||||
// try {
|
||||
// // 並行調用多個 API
|
||||
// const promises = this.selectedItems.map(item => {
|
||||
// return this.callApi('領據處理', 'Delete', {
|
||||
// no: item.編號,
|
||||
// dbAppNo: item.DB_APPNO
|
||||
// });
|
||||
// });
|
||||
|
||||
// await Promise.all(promises);
|
||||
// this.ShowSuccess('刪除完成');
|
||||
// this.search();
|
||||
// } catch (error) {
|
||||
// console.error('Delete error:', error);
|
||||
// this.ShowError('刪除失敗: ' + (error.message || '請檢查網路連線'));
|
||||
// } finally {
|
||||
// this.loading = false;
|
||||
// }
|
||||
//}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,926 @@
|
||||
function CreateReceiptEditApp() {
|
||||
const loadMarker = document.getElementById('receiptEditApp');
|
||||
if (loadMarker) {
|
||||
DoCreateReceiptEditApp();
|
||||
}
|
||||
}
|
||||
|
||||
function DoCreateReceiptEditApp() {
|
||||
|
||||
window.__vms = window.__vms || {}
|
||||
|
||||
if (window.__vms.receiptEditApp)
|
||||
return;
|
||||
|
||||
window.__vms.receiptEditApp = new Vue({
|
||||
el: '#receiptEditApp',
|
||||
mixins: [DateMixin, ToastMixin, ApiMixin],
|
||||
data: {
|
||||
mode: 'view', // view, add, edit
|
||||
編號: 0,
|
||||
originalMainData: null,
|
||||
originalLaborData: null,
|
||||
originalTravelData: null,
|
||||
mainData: {
|
||||
編號: 0,
|
||||
表單編號: '',
|
||||
活動日期: '',
|
||||
身分別: '',
|
||||
講師_非講師編號: '',
|
||||
領款人姓名: '',
|
||||
身分證字號: '',
|
||||
憑證類別: '',
|
||||
計畫代號: '',
|
||||
計畫名稱: '',
|
||||
活動地點: '',
|
||||
活動類別: '',
|
||||
活動名稱事由: '',
|
||||
製單人員: '',
|
||||
付款方式: '',
|
||||
作廢: 0,
|
||||
列印次數: 0,
|
||||
已關帳: false,
|
||||
DB_APPNO: 1,
|
||||
DB_CRDAT: '',
|
||||
DB_CRUSR: '',
|
||||
DB_CRUSR_NAME: '',
|
||||
DB_TRDAT: '',
|
||||
DB_TRUSR: '',
|
||||
DB_TRUSR_NAME: ''
|
||||
},
|
||||
laborData: {
|
||||
編號: '',
|
||||
領據編號: 0,
|
||||
費用別: '',
|
||||
單價: 0,
|
||||
數量: 0,
|
||||
單位: '',
|
||||
扣繳類別: '',
|
||||
合計金額: 0,
|
||||
應扣繳稅額: 0,
|
||||
服務費: 0,
|
||||
作廢: 0,
|
||||
DB_APPNO: 1
|
||||
},
|
||||
travelData: {
|
||||
編號: '',
|
||||
領據編號: 0,
|
||||
票價資訊: '',
|
||||
費用別: '',
|
||||
起點: '',
|
||||
訖點: '',
|
||||
捷運公車: 0,
|
||||
火車: 0,
|
||||
計程車: 0,
|
||||
高鐵飛機: 0,
|
||||
自行開車: 0,
|
||||
過路費停車費: 0,
|
||||
住宿費: 0,
|
||||
合計: 0,
|
||||
作廢: 0,
|
||||
DB_APPNO: 1
|
||||
},
|
||||
dropdown: {
|
||||
身分別: [],
|
||||
憑證類別: [],
|
||||
付款方式: [],
|
||||
勞務費_費用別: [],
|
||||
差旅費_費用別: [],
|
||||
扣繳類別: [],
|
||||
單位別: []
|
||||
},
|
||||
recipientList: [],
|
||||
showRecipientDropdown: false,
|
||||
planList: [],
|
||||
showPlanDropdown: false,
|
||||
originStations: [],
|
||||
showOriginDropdown: false,
|
||||
destStations: [],
|
||||
showDestDropdown: false,
|
||||
stations: [],
|
||||
showActivityLocationPhrase: false,
|
||||
activityLocationPhrases: [],
|
||||
showActivityCategoryPhrase: false,
|
||||
activityCategoryPhrases: [],
|
||||
showTicketPricePhrase: false,
|
||||
ticketPricePhrases: [],
|
||||
loading: false,
|
||||
variables: {
|
||||
領據執行業務所得課稅門檻: 0,
|
||||
領據薪資課稅門檻: 0,
|
||||
領據基本工資: 0,
|
||||
領據執行業務所得稅率: 0,
|
||||
領據薪資稅率: 0
|
||||
}
|
||||
},
|
||||
created() {
|
||||
// 從 AJAX 取得下拉選單資料
|
||||
this.loading = true;
|
||||
this.loadVariables();
|
||||
this.loadDropdownLists();
|
||||
this.loadPhraseLists();
|
||||
this.loading = false;
|
||||
this.mainData.製單人員 = V製單人員;
|
||||
this.編號 = 0;
|
||||
if (document.getElementById('ctl00_ContentPlaceHolder1_hid編號').value !== '') {
|
||||
this.編號 = parseInt(document.getElementById('ctl00_ContentPlaceHolder1_hid編號').value) || 0;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.編號 !== 0) {
|
||||
this.mode = 'view';
|
||||
this.loadData(this.編號)
|
||||
} else {
|
||||
this.mode = 'add';
|
||||
this.reset();
|
||||
this.backupData();
|
||||
this.setDefault();
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'laborData.單價'() {
|
||||
this.calculateLaborTotal();
|
||||
},
|
||||
'laborData.數量'() {
|
||||
this.calculateLaborTotal();
|
||||
},
|
||||
'laborData.扣繳類別'() {
|
||||
this.calculateTax();
|
||||
},
|
||||
'mainData.身分別'() {
|
||||
if (this.mode === 'add' || this.mode === 'edit') {
|
||||
this.mainData.講師_非講師編號 = '';
|
||||
this.mainData.領款人姓名 = '';
|
||||
this.mainData.身分證字號 = '';
|
||||
this.recipientList = [];
|
||||
}
|
||||
},
|
||||
'travelData.捷運公車'() { this.calculateTravelTotal(); },
|
||||
'travelData.火車'() { this.calculateTravelTotal(); },
|
||||
'travelData.計程車'() { this.calculateTravelTotal(); },
|
||||
'travelData.高鐵飛機'() { this.calculateTravelTotal(); },
|
||||
'travelData.自行開車'() { this.calculateTravelTotal(); },
|
||||
'travelData.過路費停車費'() { this.calculateTravelTotal(); },
|
||||
'travelData.住宿費'() { this.calculateTravelTotal(); }
|
||||
},
|
||||
computed: {
|
||||
isViewMode() {
|
||||
return this.mode === 'view';
|
||||
},
|
||||
isEditMode() {
|
||||
return this.mode === 'edit' || this.mode === 'add';
|
||||
},
|
||||
canAddNew() {
|
||||
return this.mode === 'view';
|
||||
},
|
||||
canEdit() {
|
||||
return this.mode === 'view' && this.編號 !== 0;
|
||||
},
|
||||
canCancel() {
|
||||
return this.mode === 'add' || this.mode === 'edit';
|
||||
},
|
||||
canSave() {
|
||||
return this.mode === 'add' || this.mode === 'edit';
|
||||
},
|
||||
canReturnToList() {
|
||||
return this.mode === 'view';
|
||||
},
|
||||
closed() {
|
||||
return this.mainData.已關帳;
|
||||
},
|
||||
laborWarnings() {
|
||||
const warnings = [];
|
||||
const 扣繳類別 = this.laborData.扣繳類別 || '';
|
||||
|
||||
// 費用別警告
|
||||
if (['演講費', '撰稿費'].indexOf(this.laborData.費用別) >= 0 && 扣繳類別 === '50') {
|
||||
warnings.push('扣繳類別只能是執行業務所得');
|
||||
}
|
||||
|
||||
// 合計金額警告
|
||||
if (扣繳類別 === '50' && this.laborData.合計金額 >= this.variables.領據基本工資) {
|
||||
warnings.push(`薪資不得超過基本工資${this.variables.領據基本工資}(含),超過請拆成不同日期的領據`);
|
||||
}
|
||||
if ((扣繳類別.startsWith('9A') || 扣繳類別.startsWith('9B')) && this.laborData.合計金額 >= this.variables.領據執行業務所得課稅門檻) {
|
||||
warnings.push(`執行業務所得不得超過${this.variables.領據執行業務所得課稅門檻}(含),超過請拆成不同日期的領據`);
|
||||
}
|
||||
|
||||
return warnings;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async loadVariables() {
|
||||
try {
|
||||
const result = await this.callApi('領據處理', 'GetVariables', {});
|
||||
if (result.Data) {
|
||||
this.variables = result.Data;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('載入片語清單失敗:', error);
|
||||
}
|
||||
},
|
||||
|
||||
async loadDropdownLists() {
|
||||
this.dropdown.身分別 = await this.loadDropdownList("身分別");
|
||||
this.dropdown.憑證類別 = await this.loadDropdownList("憑證類別");
|
||||
this.dropdown.付款方式 = await this.loadDropdownList("付款方式");
|
||||
this.dropdown.勞務費_費用別 = await this.loadDropdownList("勞務費_費用別");
|
||||
this.dropdown.差旅費_費用別 = await this.loadDropdownList("差旅費_費用別");
|
||||
this.dropdown.扣繳類別 = await this.loadDropdownList("扣繳類別");
|
||||
this.dropdown.單位別 = await this.loadDropdownList("單位別");
|
||||
},
|
||||
|
||||
async loadDropdownList(key) {
|
||||
try {
|
||||
const result = await this.callApi('領據處理', 'GetDropdownLists', { key });
|
||||
return result.Data || [];
|
||||
} catch (error) {
|
||||
console.error('載入下拉選單失敗:', error);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
async loadPhraseLists() {
|
||||
try {
|
||||
const result = await this.callApi('領據處理', 'GetPhraseLists', {});
|
||||
if (result.Data) {
|
||||
this.activityLocationPhrases = result.Data.activityLocation || [];
|
||||
this.activityCategoryPhrases = result.Data.activityCategory || [];
|
||||
this.ticketPricePhrases = result.Data.ticketPrice || [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('載入公用變數失敗:', error);
|
||||
}
|
||||
},
|
||||
|
||||
toggleActivityLocationPhrase() {
|
||||
if (this.isViewMode) return;
|
||||
this.showActivityLocationPhrase = !this.showActivityLocationPhrase;
|
||||
this.showActivityCategoryPhrase = false;
|
||||
this.showTicketPricePhrase = false;
|
||||
},
|
||||
|
||||
toggleActivityCategoryPhrase() {
|
||||
if (this.isViewMode) return;
|
||||
this.showActivityCategoryPhrase = !this.showActivityCategoryPhrase;
|
||||
this.showActivityLocationPhrase = false;
|
||||
this.showTicketPricePhrase = false;
|
||||
},
|
||||
|
||||
toggleTicketPricePhrase() {
|
||||
if (this.isViewMode) return;
|
||||
this.showTicketPricePhrase = !this.showTicketPricePhrase;
|
||||
this.showActivityLocationPhrase = false;
|
||||
this.showActivityCategoryPhrase = false;
|
||||
},
|
||||
|
||||
insertPhrase(fieldName, phrase) {
|
||||
let input;
|
||||
if (fieldName === '活動地點') {
|
||||
input = this.$refs.活動地點Input;
|
||||
this.showActivityLocationPhrase = false;
|
||||
} else if (fieldName === '活動類別') {
|
||||
input = this.$refs.活動類別Input;
|
||||
this.showActivityCategoryPhrase = false;
|
||||
} else if (fieldName === '票價資訊') {
|
||||
input = this.$refs.票價資訊Input;
|
||||
this.showTicketPricePhrase = false;
|
||||
}
|
||||
|
||||
if (input) {
|
||||
const startPos = input.selectionStart || input.value.length;
|
||||
const endPos = input.selectionEnd || input.value.length;
|
||||
const currentValue = input.value;
|
||||
const newValue = currentValue.substring(0, startPos) + phrase + currentValue.substring(endPos);
|
||||
|
||||
if (fieldName === '活動地點') {
|
||||
this.mainData.活動地點 = newValue;
|
||||
} else if (fieldName === '活動類別') {
|
||||
this.mainData.活動類別 = newValue;
|
||||
} else if (fieldName === '票價資訊') {
|
||||
this.travelData.票價資訊 = newValue;
|
||||
}
|
||||
|
||||
// 更新游標位置
|
||||
this.$nextTick(() => {
|
||||
input.focus();
|
||||
input.setSelectionRange(startPos + phrase.length, startPos + phrase.length);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
calculateLaborTotal() {
|
||||
const price = parseFloat(this.laborData.單價) || 0;
|
||||
const qty = parseFloat(this.laborData.數量) || 0;
|
||||
this.laborData.合計金額 = Math.round(price * qty);
|
||||
this.calculateTax();
|
||||
},
|
||||
|
||||
async calculateTax() {
|
||||
const laborFee = parseInt(this.laborData.合計金額) || 0;
|
||||
const category = this.laborData.扣繳類別 || '';
|
||||
|
||||
if (laborFee > 0 && category) {
|
||||
this.loading = true;
|
||||
try {
|
||||
const result = await this.callApi('領據處理', 'CalcTax', {
|
||||
laborFee,
|
||||
category
|
||||
});
|
||||
this.laborData.應扣繳稅額 = result.Data;
|
||||
} catch (error) {
|
||||
console.error('計算稅額失敗:', error);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
} else {
|
||||
this.laborData.應扣繳稅額 = 0;
|
||||
}
|
||||
},
|
||||
|
||||
calculateTravelTotal() {
|
||||
this.travelData.合計 =
|
||||
(parseInt(this.travelData.捷運公車) || 0) +
|
||||
(parseInt(this.travelData.火車) || 0) +
|
||||
(parseInt(this.travelData.計程車) || 0) +
|
||||
(parseInt(this.travelData.高鐵飛機) || 0) +
|
||||
(parseInt(this.travelData.自行開車) || 0) +
|
||||
(parseInt(this.travelData.過路費停車費) || 0) +
|
||||
(parseInt(this.travelData.住宿費) || 0);
|
||||
},
|
||||
|
||||
searchStation(field) {
|
||||
const key = this.travelData[field];
|
||||
|
||||
if (key && key.length > 0) {
|
||||
axios({
|
||||
method: 'post',
|
||||
url: `${API_TRAIN_FARE}/SelectStation`,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
},
|
||||
data: JSON.stringify({ key })
|
||||
})
|
||||
.then(response => {
|
||||
const result = response.data.d || response.data;
|
||||
if (result.Success) {
|
||||
this.stations = result.Data || [];
|
||||
} else {
|
||||
this.stations = [];
|
||||
this.ShowWarning(result.Message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('查詢車站失敗:', error);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
async searchRecipient() {
|
||||
const key = this.mainData.領款人姓名;
|
||||
const identity = this.mainData.身分別;
|
||||
const activationDate = this.mainData.活動日期;
|
||||
|
||||
if (!key || key.length === 0) {
|
||||
this.recipientList = [];
|
||||
this.showRecipientDropdown = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!identity) {
|
||||
this.ShowWarning('請先選擇身分別');
|
||||
this.recipientList = [];
|
||||
this.showRecipientDropdown = false;
|
||||
return;
|
||||
}
|
||||
if (!activationDate) {
|
||||
this.ShowWarning('請輸入活動日期');
|
||||
this.recipientList = [];
|
||||
this.showRecipientDropdown = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
try {
|
||||
const result = await this.callApi('領據處理', 'SearchRecipient', { key, identity, activationDate });
|
||||
this.recipientList = result.Data || [];
|
||||
this.showRecipientDropdown = this.recipientList.length > 0;
|
||||
} catch (error) {
|
||||
console.error('查詢領款人失敗:', error);
|
||||
this.recipientList = [];
|
||||
this.showRecipientDropdown = false;
|
||||
this.ShowWarning(error.message);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
hideRecipientDropdown() {
|
||||
setTimeout(() => {
|
||||
this.showRecipientDropdown = false;
|
||||
this.recipientList = [];
|
||||
}, 200);
|
||||
},
|
||||
|
||||
selectRecipient(recipient) {
|
||||
this.mainData.講師_非講師編號 = recipient.Value;
|
||||
this.mainData.領款人姓名 = recipient.Name;
|
||||
this.mainData.身分證字號 = recipient.IdNumber;
|
||||
this.mainData.憑證類別 = recipient.VoucherType;
|
||||
this.laborData.扣繳類別 = recipient.ConductType;
|
||||
this.recipientList = [];
|
||||
this.showRecipientDropdown = false;
|
||||
},
|
||||
|
||||
async searchPlan() {
|
||||
const key = this.mainData.計畫代號;
|
||||
const year = this.mainData.活動日期 ? new Date(this.mainData.活動日期).getFullYear() - 1911 : null;
|
||||
|
||||
if (year === null) {
|
||||
this.ShowWarning("請輸入活動日期");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!key || key.length === 0) {
|
||||
this.planList = [];
|
||||
this.showPlanDropdown = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
try {
|
||||
const result = await this.callApi('領據處理', 'SearchPlan', { key, year });
|
||||
this.planList = result.Data || [];
|
||||
this.showPlanDropdown = this.planList.length > 0;
|
||||
} catch (error) {
|
||||
console.error('查詢計畫失敗:', error);
|
||||
this.planList = [];
|
||||
this.showPlanDropdown = false;
|
||||
this.ShowWarning(error.message);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
selectPlan(plan) {
|
||||
this.mainData.計畫代號 = plan.Code;
|
||||
this.mainData.計畫名稱 = plan.Name;
|
||||
this.planList = [];
|
||||
this.showPlanDropdown = false;
|
||||
},
|
||||
|
||||
hidePlanDropdown() {
|
||||
setTimeout(() => {
|
||||
this.showPlanDropdown = false;
|
||||
this.planList = [];
|
||||
}, 200);
|
||||
},
|
||||
|
||||
async searchOriginStation() {
|
||||
const key = this.travelData.起點;
|
||||
|
||||
if (!key || key.length === 0) {
|
||||
this.originStations = [];
|
||||
this.showOriginDropdown = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
try {
|
||||
const result = await this.callApi('火車票價', 'SelectOriginStation', { key });
|
||||
this.originStations = result.Data || [];
|
||||
this.showOriginDropdown = this.originStations.length > 0;
|
||||
} catch (error) {
|
||||
console.error('查詢起點失敗:', error);
|
||||
this.originStations = [];
|
||||
this.showOriginDropdown = false;
|
||||
this.ShowWarning(error.message);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
selectOriginStation(station) {
|
||||
this.travelData.起點 = station.Name;
|
||||
this.originStations = [];
|
||||
this.showOriginDropdown = false;
|
||||
},
|
||||
|
||||
hideOriginDropdown() {
|
||||
setTimeout(() => {
|
||||
this.showOriginDropdown = false;
|
||||
this.originStations = [];
|
||||
}, 200);
|
||||
this.calculateFare();
|
||||
},
|
||||
|
||||
async searchDestStation() {
|
||||
const key = this.travelData.訖點;
|
||||
|
||||
if (!key || key.length === 0) {
|
||||
this.destStations = [];
|
||||
this.showDestDropdown = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
try {
|
||||
const result = await this.callApi('火車票價', 'SelectDestStation', { key });
|
||||
this.destStations = result.Data || [];
|
||||
this.showDestDropdown = this.destStations.length > 0;
|
||||
} catch (error) {
|
||||
console.error('查詢訖點失敗:', error);
|
||||
this.destStations = [];
|
||||
this.showDestDropdown = false;
|
||||
this.ShowWarning(error.message);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
selectDestStation(station) {
|
||||
this.travelData.訖點 = station.Name;
|
||||
this.destStations = [];
|
||||
this.showDestDropdown = false;
|
||||
},
|
||||
|
||||
hideDestDropdown() {
|
||||
setTimeout(() => {
|
||||
this.showDestDropdown = false;
|
||||
this.destStations = [];
|
||||
}, 200);
|
||||
this.calculateFare();
|
||||
},
|
||||
|
||||
async calculateFare() {
|
||||
const 起站 = this.travelData.起點;
|
||||
const 迄站 = this.travelData.訖點;
|
||||
|
||||
if (!起站 || !迄站) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
try {
|
||||
const result = await this.callApi('火車票價', 'SelectFare', { 起站, 迄站 });
|
||||
this.travelData.火車 = result.Data;
|
||||
this.calculateTravelTotal();
|
||||
} catch (error) {
|
||||
console.error('查詢票價失敗:', error);
|
||||
this.ShowWarning(error.message);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async save() {
|
||||
if (!this.validate() || (this.laborWarnings || []).length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
|
||||
try {
|
||||
const data = {
|
||||
isNew: this.mode === 'add',
|
||||
mainItem: this.mainData,
|
||||
laborItem: this.laborData,
|
||||
travelItem: this.travelData
|
||||
};
|
||||
|
||||
const result = await this.callApiWithErrorMsg('領據處理', 'Save', data, '儲存');
|
||||
this.ShowSuccess(result.Message);
|
||||
// 新增存檔更新編號變數
|
||||
if (this.mode === 'add') {
|
||||
this.編號 = result.Data.編號;
|
||||
}
|
||||
// 返回查看模式
|
||||
this.mode = 'view';
|
||||
this.loadData(this.編號);
|
||||
this.backupData();
|
||||
} catch (error) {
|
||||
console.error('Save error:', error);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
validate() {
|
||||
if (!this.mainData.身分別) {
|
||||
this.ShowWarning('請選擇身分別');
|
||||
return false;
|
||||
}
|
||||
if (!this.mainData.領款人姓名) {
|
||||
this.ShowWarning('請輸入領款人姓名');
|
||||
return false;
|
||||
}
|
||||
if (!this.mainData.計畫代號) {
|
||||
this.ShowWarning('請輸入計畫代號');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
returnToList() {
|
||||
if ((this.mode === 'add' || this.mode === 'edit') && !confirm("是否放棄" + (this.mode === 'add' ? '新增' : '修改')))
|
||||
return;
|
||||
window.__doPostBack('ctl00$ContentPlaceHolder1$btnTab1', '');
|
||||
},
|
||||
|
||||
backupData() {
|
||||
this.originalMainData = JSON.parse(JSON.stringify(this.mainData));
|
||||
this.originalLaborData = JSON.parse(JSON.stringify(this.laborData));
|
||||
this.originalTravelData = JSON.parse(JSON.stringify(this.travelData));
|
||||
},
|
||||
|
||||
restoreData() {
|
||||
if (this.originalMainData) {
|
||||
this.mainData = JSON.parse(JSON.stringify(this.originalMainData));
|
||||
}
|
||||
if (this.originalLaborData) {
|
||||
this.laborData = JSON.parse(JSON.stringify(this.originalLaborData));
|
||||
}
|
||||
if (this.originalTravelData) {
|
||||
this.travelData = JSON.parse(JSON.stringify(this.originalTravelData));
|
||||
}
|
||||
},
|
||||
|
||||
addNew() {
|
||||
if (this.mode !== 'view') {
|
||||
return;
|
||||
}
|
||||
this.mode = 'add';
|
||||
this.reset();
|
||||
this.backupData();
|
||||
this.setDefault();
|
||||
},
|
||||
|
||||
edit() {
|
||||
if (this.mode !== 'view') {
|
||||
return;
|
||||
}
|
||||
this.mode = 'edit';
|
||||
this.backupData();
|
||||
},
|
||||
|
||||
cancel() {
|
||||
if (this.mode === 'view') {
|
||||
return;
|
||||
}
|
||||
if (!confirm("是否放棄" + (this.mode === 'add' ? '新增' : '修改') + ",資料將復原?")) {
|
||||
return;
|
||||
}
|
||||
this.restoreData();
|
||||
this.mode = 'view';
|
||||
},
|
||||
|
||||
reset() {
|
||||
this.mainData = {
|
||||
編號: 0,
|
||||
表單編號: '',
|
||||
活動日期: '',
|
||||
身分別: '',
|
||||
講師_非講師編號: '',
|
||||
領款人姓名: '',
|
||||
身分證字號: '',
|
||||
憑證類別: '',
|
||||
計畫代號: '',
|
||||
計畫名稱: '',
|
||||
活動地點: '',
|
||||
活動類別: '',
|
||||
活動名稱事由: '',
|
||||
付款方式: '',
|
||||
作廢: 0,
|
||||
列印次數: 0,
|
||||
DB_APPNO: 1,
|
||||
DB_CRDAT: '',
|
||||
DB_CRUSR: '',
|
||||
DB_CRUSR_NAME: '',
|
||||
DB_TRDAT: '',
|
||||
DB_TRUSR: '',
|
||||
DB_TRUSR_NAME: '',
|
||||
};
|
||||
this.laborData = {
|
||||
編號: '',
|
||||
領據編號: 0,
|
||||
費用別: '',
|
||||
單價: 0,
|
||||
數量: 0,
|
||||
單位: '',
|
||||
扣繳類別: '',
|
||||
合計金額: 0,
|
||||
應扣繳稅額: 0,
|
||||
服務費: 0,
|
||||
作廢: 0,
|
||||
DB_APPNO: 1
|
||||
};
|
||||
this.travelData = {
|
||||
編號: '',
|
||||
領據編號: 0,
|
||||
票價資訊: '',
|
||||
費用別: '',
|
||||
起點: '',
|
||||
訖點: '',
|
||||
捷運公車: 0,
|
||||
火車: 0,
|
||||
計程車: 0,
|
||||
高鐵飛機: 0,
|
||||
自行開車: 0,
|
||||
過路費停車費: 0,
|
||||
住宿費: 0,
|
||||
合計: 0,
|
||||
作廢: 0,
|
||||
DB_APPNO: 1
|
||||
};
|
||||
},
|
||||
|
||||
setDefault()
|
||||
{
|
||||
this.mainData.DB_CRDAT = this.FormatDate(new Date());
|
||||
this.mainData.DB_CRUSR = document.getElementById('ctl00_hidCurrentUser').value;
|
||||
this.mainData.DB_CRUSR_NAME = document.getElementById('ctl00_hidCurrentUserName').value;
|
||||
},
|
||||
|
||||
async loadData(no) {
|
||||
this.loading = true;
|
||||
|
||||
try {
|
||||
const result = await this.callApiWithErrorMsg('領據處理', 'GetOne', { no }, '載入');
|
||||
|
||||
const data = result.Data;
|
||||
|
||||
this.mainData = {
|
||||
編號: data.編號,
|
||||
表單編號: data.表單編號,
|
||||
活動日期: this.FormatDate(data.活動日期),
|
||||
身分別: data.身分別,
|
||||
講師_非講師編號: data.講師_非講師編號,
|
||||
領款人姓名: data.領款人姓名,
|
||||
身分證字號: data.身分證字號,
|
||||
憑證類別: data.憑證類別,
|
||||
計畫代號: data.計畫代號,
|
||||
計畫名稱: data.計畫名稱,
|
||||
活動地點: data.活動地點,
|
||||
活動類別: data.活動類別,
|
||||
活動名稱事由: data.活動名稱事由,
|
||||
付款方式: data.付款方式,
|
||||
作廢: data.作廢,
|
||||
列印次數: data.列印次數,
|
||||
已關帳: data.已關帳,
|
||||
DB_APPNO: data.DB_APPNO,
|
||||
DB_CRDAT: this.FormatDate(data.DB_CRDAT),
|
||||
DB_CRUSR: data.DB_CRUSR,
|
||||
DB_CRUSR_NAME: data.DB_CRUSR_NAME,
|
||||
DB_TRDAT: data.DB_TRUSR !== null ? this.FormatDate(data.DB_TRDAT) : '',
|
||||
DB_TRUSR: data.DB_TRUSR !== null ? data.DB_TRUSR : '',
|
||||
DB_TRUSR_NAME: data.DB_TRUSR != null ? data.DB_TRUSR_NAME: ''
|
||||
};
|
||||
this.laborData = {
|
||||
編號: data.勞務費_編號,
|
||||
領據編號: data.編號,
|
||||
費用別: data.勞務費_費用別,
|
||||
單價: data.勞務費_單價,
|
||||
數量: data.勞務費_數量,
|
||||
單位: data.勞務費_單位,
|
||||
扣繳類別: data.勞務費_扣繳類別,
|
||||
合計金額: data.勞務費_合計金額,
|
||||
應扣繳稅額: data.勞務費_應扣繳稅額,
|
||||
服務費: data.勞務費_服務費,
|
||||
作廢: data.勞務費_作廢
|
||||
};
|
||||
this.travelData = {
|
||||
編號: data.差旅費_編號,
|
||||
領據編號: data.編號,
|
||||
票價資訊: data.差旅費_票價資訊,
|
||||
費用別: data.差旅費_費用別,
|
||||
起點: data.差旅費_起點,
|
||||
訖點: data.差旅費_訖點,
|
||||
捷運公車: data.差旅費_捷運公車,
|
||||
火車: data.差旅費_火車,
|
||||
計程車: data.差旅費_計程車,
|
||||
高鐵飛機: data.差旅費_高鐵飛機,
|
||||
自行開車: data.差旅費_自行開車,
|
||||
過路費停車費: data.差旅費_過路費停車費,
|
||||
住宿費: data.差旅費_住宿費,
|
||||
合計: data.差旅費_合計,
|
||||
作廢: data.差旅費_作廢
|
||||
};
|
||||
|
||||
if (data.main) {
|
||||
this.mainData = data.main;
|
||||
}
|
||||
if (data.labor) {
|
||||
this.laborData = data.labor;
|
||||
}
|
||||
if (data.travel) {
|
||||
this.travelData = data.travel;
|
||||
}
|
||||
this.backupData();
|
||||
} catch (error) {
|
||||
console.error('LoadData error:', error);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async print() {
|
||||
if (this.mainData.作廢 !== 0) {
|
||||
this.ShowError('已作廢不可列印');
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
try {
|
||||
const result = await this.callApiWithErrorMsg(
|
||||
'領據處理',
|
||||
'Print',
|
||||
{ idList: [this.mainData.編號 ] },
|
||||
'列印領據'
|
||||
);
|
||||
|
||||
if (result.Success) {
|
||||
// 下載 Excel 檔案
|
||||
if (result.Data && result.Data.fileName) {
|
||||
const link = document.createElement('a');
|
||||
link.href = `data:application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;base64,${result.Data.excelData}`;
|
||||
link.download = result.Data.fileName;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
this.ShowSuccess(result.Message);
|
||||
} else {
|
||||
this.ShowWarning(result.Message);
|
||||
}
|
||||
} else {
|
||||
this.ShowError('列印失敗: ' + (error.message || '請稍後重試'));
|
||||
}
|
||||
} catch (error) {
|
||||
this.ShowError('列印失敗: ' + (error.message || '請檢查網路連線'));
|
||||
} finally {
|
||||
this.loadData(this.編號);
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async voidVoucher() {
|
||||
if (this.mainData.作廢 === 0 && !confirm("是否確定作廢?") || this.mainData.作廢 !== 0 && !confirm("是否確定取消作廢?")) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
try {
|
||||
const result = await this.callApi('領據處理', 'Void', {
|
||||
no: this.mainData.編號
|
||||
});
|
||||
this.loadData(this.mainData.編號);
|
||||
this.ShowSuccess('作廢操作完成');
|
||||
} catch (error) {
|
||||
console.error('Void error:', error);
|
||||
this.ShowError('作廢操作失敗: ' + (error.message || '請檢查網路連線'));
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async voidLaborFee() {
|
||||
if (this.laborData.作廢 === 0 && !confirm("是否確定作廢勞務費?") || this.laborData.作廢 !== 0 && !confirm("是否確定取消作廢勞務費?")) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
try {
|
||||
const result = await this.callApi('領據處理', 'VoidLaborFee', {
|
||||
receiptNo: this.mainData.編號
|
||||
});
|
||||
this.loadData(this.mainData.編號);
|
||||
this.ShowSuccess('勞務費作廢操作完成');
|
||||
} catch (error) {
|
||||
console.error('VoidLaborFee error:', error);
|
||||
this.ShowError('勞務費作廢操作失敗: ' + (error.message || '請檢查網路連線'));
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async voidTravelFee() {
|
||||
if (this.travelData.作廢 === 0 && !confirm("是否確定作廢差旅費?") || this.travelData.作廢 !== 0 && !confirm("是否確定取消作廢差旅費?")) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
try {
|
||||
const result = await this.callApi('領據處理', 'VoidTravelFee', {
|
||||
receiptNo: this.mainData.編號
|
||||
});
|
||||
this.loadData(this.mainData.編號);
|
||||
this.ShowSuccess('差旅費作廢操作完成');
|
||||
} catch (error) {
|
||||
console.error('VoidTravelFee error:', error);
|
||||
this.ShowError('差旅費作廢操作失敗: ' + (error.message || '請檢查網路連線'));
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,8 @@
|
||||
let V身分別 = [];
|
||||
let V憑證類別 = [];
|
||||
let V付款方式 = [];
|
||||
let V勞務費_費用別 = [];
|
||||
let V差旅費_費用別 = [];
|
||||
let V扣繳類別 = [];
|
||||
let V單位別 = [];
|
||||
let V製單人員 = "";
|
||||
Reference in New Issue
Block a user