Files
17168ERP/web/admin/pivot/query.aspx
2025-10-20 11:54:40 +08:00

1129 lines
54 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<%@ Page Title="數據透視查詢" Language="C#" MasterPageFile="~/admin/Templates/TBS5ADM001/MasterPage.master" AutoEventWireup="true" EnableEventValidation="false" CodeFile="query.aspx.cs" Inherits="admin_pivot_query" %>
<asp:Content ID="Content1" ContentPlaceHolderID="page_header" runat="Server">
<link rel="stylesheet" href="../../js/_bootstrap-icons-1.8.1/bootstrap-icons.css">
<style>
/* 與 transfer 保持一致的樣式 */
.v-data-table th {
background-color: #f5f5f5 !important;
font-weight: bold;
}
.v-data-table tbody tr:hover {
background-color: #f0f8ff !important;
}
/* 欄位色彩標記 */
.field-activity { border-left: 3px solid #ffc107; } /* 橙 */
.field-follower { border-left: 3px solid #17a2b8; } /* 藍 */
.field-merit { border-left: 3px solid #28a745; } /* 綠 */
.field-calculated { border-left: 3px solid #6c757d; } /* 紫 */
/* Badge 樣式 */
.badge-field {
font-size: 0.75em;
padding: 2px 6px;
margin-right: 4px;
}
/* 頁籤樣式 */
.v-tabs {
margin-bottom: 20px;
}
/* 過濾區域 */
.filter-section {
background-color: #f8f9fa;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
}
/* 統計卡片 */
.stat-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
border-radius: 10px;
margin-bottom: 20px;
}
/* 載入動畫 */
.loading-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
}
</style>
</asp:Content>
<asp:Content ID="Content3" ContentPlaceHolderID="page_nav" runat="Server">
<h5 class="mb-0">
<i class="bi bi-graph-up"></i> 數據透視查詢
</h5>
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="ContentPlaceHolder1" runat="Server">
<div id="pivot-query-app">
<v-app>
<!-- 載入遮罩 -->
<div v-if="loading" class="loading-overlay">
<v-progress-circular
:size="70"
:width="7"
color="primary"
indeterminate
></v-progress-circular>
</div>
<v-container fluid>
<!-- 頁籤導航 -->
<v-tabs v-model="activeTab" background-color="white" color="primary" grow>
<v-tab>
<v-icon left>mdi-magnify</v-icon>
查詢條件
</v-tab>
<v-tab :disabled="!hasData">
<v-icon left>mdi-table</v-icon>
報名明細
<v-chip v-if="rawData.length > 0" small class="ml-2">{{ rawData.length }}</v-chip>
</v-tab>
<v-tab :disabled="!hasData">
<v-icon left>mdi-account-group</v-icon>
信眾分析
</v-tab>
<v-tab :disabled="!hasData">
<v-icon left>mdi-currency-usd</v-icon>
收入統計
</v-tab>
<v-tab :disabled="!hasData">
<v-icon left>mdi-chart-line</v-icon>
趨勢分析
</v-tab>
<v-tab :disabled="!hasData">
<v-icon left>mdi-compare</v-icon>
對比分析
</v-tab>
</v-tabs>
<!-- 頁籤內容 -->
<v-tabs-items v-model="activeTab">
<!-- Tab 1: 查詢條件 -->
<v-tab-item>
<v-card class="mt-4">
<v-card-title class="bg-primary white--text">
<v-icon left color="white">mdi-filter</v-icon>
查詢條件設定
</v-card-title>
<v-card-text>
<v-row class="mt-4">
<v-col cols="12" md="3">
<v-select
v-model="searchCriteria.year"
:items="yearOptions"
label="年份"
dense
outlined
prepend-icon="mdi-calendar"
></v-select>
</v-col>
<v-col cols="12" md="3">
<v-select
v-model="searchCriteria.month"
:items="monthOptions"
label="月份(可選)"
dense
outlined
clearable
prepend-icon="mdi-calendar-month"
></v-select>
</v-col>
<v-col cols="12" md="3">
<v-btn
color="primary"
large
block
@click="loadActivities"
:loading="loadingActivities"
>
<v-icon left>mdi-magnify</v-icon>
查詢法會
</v-btn>
</v-col>
<v-col cols="12" md="3">
<v-btn
color="secondary"
large
block
outlined
@click="resetSearch"
>
<v-icon left>mdi-refresh</v-icon>
重設
</v-btn>
</v-col>
</v-row>
<!-- 法會清單 -->
<v-divider class="my-4"></v-divider>
<h6 class="mb-3">
<v-icon left color="primary">mdi-format-list-bulleted</v-icon>
法會清單
<v-chip v-if="activities.length > 0" small class="ml-2">
共 {{ activities.length }} 場
</v-chip>
</h6>
<v-data-table
:headers="activityHeaders"
:items="activities"
:loading="loadingActivities"
loading-text="載入中..."
class="elevation-1"
item-key="activity_num"
:items-per-page="10"
>
<template v-slot:item.start_date="{ item }">
{{ formatDate(item.start_date) }}
</template>
<template v-slot:item.end_date="{ item }">
{{ formatDate(item.end_date) }}
</template>
<template v-slot:item.total_amount="{ item }">
{{ formatCurrency(item.total_amount) }}
</template>
<template v-slot:item.actions="{ item }">
<v-btn
small
color="success"
@click="selectActivity(item)"
:loading="item.loading"
>
<v-icon left small>mdi-check</v-icon>
選擇
</v-btn>
</template>
<template v-slot:no-data>
<v-alert type="info" class="ma-4">
請選擇查詢條件後點擊「查詢法會」
</v-alert>
</template>
</v-data-table>
</v-card-text>
</v-card>
</v-tab-item>
<!-- Tab 2: 報名明細 -->
<v-tab-item>
<v-card class="mt-4">
<v-card-title class="bg-success white--text">
<v-icon left color="white">mdi-table</v-icon>
報名明細
<v-spacer></v-spacer>
<v-chip color="white" text-color="success">
{{ selectedActivity ? selectedActivity.activity_name : '未選擇' }}
</v-chip>
</v-card-title>
<v-card-text>
<!-- 過濾條件 -->
<div class="filter-section">
<v-row>
<v-col cols="12" md="3">
<v-text-field
v-model="tab2Filter.followerName"
label="信眾姓名"
dense
outlined
clearable
prepend-icon="mdi-account-search"
@input="applyTab2Filter"
></v-text-field>
</v-col>
<v-col cols="12" md="3">
<v-select
v-model="tab2Filter.itemKind"
:items="itemKindOptions"
label="功德類型"
dense
outlined
clearable
prepend-icon="mdi-tag"
@change="applyTab2Filter"
></v-select>
</v-col>
<v-col cols="12" md="3">
<v-select
v-model="tab2Filter.isParent"
:items="[{text: '是', value: true}, {text: '否', value: false}]"
label="功德主"
dense
outlined
clearable
prepend-icon="mdi-account-star"
@change="applyTab2Filter"
></v-select>
</v-col>
<v-col cols="12" md="3">
<v-btn
color="primary"
block
@click="exportTab2Data"
>
<v-icon left>mdi-download</v-icon>
匯出 Excel
</v-btn>
</v-col>
</v-row>
</div>
<!-- 統計資訊 -->
<v-row class="mb-4">
<v-col cols="12" md="3">
<v-card color="primary" dark>
<v-card-text>
<div class="text-h6">{{ filteredRegistrations.length }}</div>
<div>報名筆數</div>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" md="3">
<v-card color="success" dark>
<v-card-text>
<div class="text-h6">{{ uniqueFollowersCount }}</div>
<div>報名人數</div>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" md="3">
<v-card color="warning" dark>
<v-card-text>
<div class="text-h6">{{ formatCurrency(totalAmount) }}</div>
<div>總金額</div>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" md="3">
<v-card color="info" dark>
<v-card-text>
<div class="text-h6">{{ formatCurrency(averageAmount) }}</div>
<div>平均金額</div>
</v-card-text>
</v-card>
</v-col>
</v-row>
<!-- 報名明細表格 -->
<v-data-table
:headers="tab2Headers"
:items="paginatedRegistrations"
:items-per-page="tab2PageSize"
:page.sync="tab2CurrentPage"
:server-items-length="filteredRegistrations.length"
class="elevation-1"
item-key="報名編號"
:footer-props="{
'items-per-page-options': [10, 20, 50, 100]
}"
>
<template v-slot:item.法會名稱="{ item }">
<span class="badge badge-field bg-warning text-dark">法</span>
{{ item.法會名稱 }}
</template>
<template v-slot:item.信眾姓名="{ item }">
<span class="badge badge-field bg-info">信</span>
{{ item.信眾姓名 }}
</template>
<template v-slot:item.功德主="{ item }">
<span class="badge badge-field bg-success">功德</span>
<v-chip :color="item.功德主 === '是' ? 'success' : 'default'" x-small>
{{ item.功德主 }}
</v-chip>
</template>
<template v-slot:item.功德類型="{ item }">
<span class="badge badge-field bg-success">功德</span>
{{ item.功德類型 }}
</template>
<template v-slot:item.金額="{ item }">
<span class="badge badge-field bg-secondary">計</span>
{{ formatCurrency(item.金額) }}
</template>
<template v-slot:item.未收="{ item }">
<span class="badge badge-field bg-secondary">計</span>
{{ formatCurrency(item.未收) }}
</template>
<template v-slot:item.報名日期="{ item }">
{{ formatDateTime(item.報名日期) }}
</template>
</v-data-table>
</v-card-text>
</v-card>
</v-tab-item>
<!-- Tab 3: 信眾分析 -->
<v-tab-item>
<v-card class="mt-4">
<v-card-title class="bg-info white--text">
<v-icon left color="white">mdi-account-group</v-icon>
信眾參與分析
<v-spacer></v-spacer>
<v-chip color="white" text-color="info">
共 {{ followerAnalysis.length }} 位信眾
</v-chip>
</v-card-title>
<v-card-text>
<!-- 過濾條件 -->
<div class="filter-section">
<v-row>
<v-col cols="12" md="3">
<v-text-field
v-model="tab3Filter.followerCode"
label="信眾編號"
dense
outlined
clearable
prepend-icon="mdi-account-search"
></v-text-field>
</v-col>
<v-col cols="12" md="3">
<v-text-field
v-model.number="tab3Filter.minCount"
label="最少參與次數"
type="number"
dense
outlined
clearable
prepend-icon="mdi-counter"
></v-text-field>
</v-col>
<v-col cols="12" md="3">
<v-text-field
v-model.number="tab3Filter.minAmount"
label="最少總金額"
type="number"
dense
outlined
clearable
prepend-icon="mdi-currency-usd"
></v-text-field>
</v-col>
<v-col cols="12" md="3">
<v-select
v-model="tab3SortBy"
:items="[
{text: '參與次數(降序)', value: 'count_desc'},
{text: '參與次數(升序)', value: 'count_asc'},
{text: '總金額(降序)', value: 'amount_desc'},
{text: '總金額(升序)', value: 'amount_asc'}
]"
label="排序方式"
dense
outlined
prepend-icon="mdi-sort"
></v-select>
</v-col>
</v-row>
</div>
<!-- 信眾分析表格 -->
<v-data-table
:headers="tab3Headers"
:items="filteredFollowerAnalysis"
:items-per-page="20"
class="elevation-1"
item-key="信眾編號"
>
<template v-slot:item.信眾編號="{ item }">
<v-chip small color="info">{{ item.信眾編號 }}</v-chip>
</template>
<template v-slot:item.參與次數="{ item }">
<v-chip small :color="item.參與次數 >= 5 ? 'success' : 'default'">
{{ item.參與次數 }}
</v-chip>
</template>
<template v-slot:item.總金額="{ item }">
{{ formatCurrency(item.總金額) }}
</template>
<template v-slot:item.平均金額="{ item }">
{{ formatCurrency(item.平均金額) }}
</template>
<template v-slot:item.最近參與日期="{ item }">
{{ formatDate(item.最近參與日期) }}
</template>
</v-data-table>
</v-card-text>
</v-card>
</v-tab-item>
<!-- Tab 4: 收入統計 -->
<v-tab-item>
<v-card class="mt-4">
<v-card-title class="bg-warning white--text">
<v-icon left color="white">mdi-currency-usd</v-icon>
收入統計分析
</v-card-title>
<v-card-text>
<!-- 分組方式選擇 -->
<v-row class="mb-4">
<v-col cols="12">
<v-chip-group v-model="tab4GroupBy" mandatory>
<v-chip value="monthly" color="primary">
<v-icon left>mdi-calendar-month</v-icon>
月份
</v-chip>
<v-chip value="yearly" color="success">
<v-icon left>mdi-calendar</v-icon>
年度
</v-chip>
<v-chip value="activity" color="info">
<v-icon left>mdi-star</v-icon>
法會
</v-chip>
<v-chip value="itemKind" color="warning">
<v-icon left>mdi-tag</v-icon>
功德類型
</v-chip>
</v-chip-group>
</v-col>
</v-row>
<!-- 收入統計表格 -->
<v-data-table
:headers="tab4Headers"
:items="incomeStats"
:items-per-page="20"
class="elevation-1"
item-key="分組名稱"
>
<template v-slot:item.分組名稱="{ item }">
<strong>{{ item.分組名稱 }}</strong>
</template>
<template v-slot:item.總金額="{ item }">
<strong class="text-success">{{ formatCurrency(item.總金額) }}</strong>
</template>
<template v-slot:item.已收="{ item }">
{{ formatCurrency(item.已收) }}
</template>
<template v-slot:item.未收="{ item }">
<span :class="item.未收 > 0 ? 'text-danger' : ''">
{{ formatCurrency(item.未收) }}
</span>
</template>
<template v-slot:item.收款率="{ item }">
<v-chip :color="getPaymentRateColor(item.收款率)" small>
{{ item.收款率 }}
</v-chip>
</template>
</v-data-table>
</v-card-text>
</v-card>
</v-tab-item>
<!-- Tab 5: 趨勢分析 -->
<v-tab-item>
<v-card class="mt-4">
<v-card-title class="bg-secondary white--text">
<v-icon left color="white">mdi-chart-line</v-icon>
趨勢分析
</v-card-title>
<v-card-text>
<!-- Tab 5 內容待實作 -->
<v-alert type="info" class="mt-4">
趨勢分析功能開發中...
</v-alert>
</v-card-text>
</v-card>
</v-tab-item>
<!-- Tab 6: 對比分析 -->
<v-tab-item>
<v-card class="mt-4">
<v-card-title class="bg-dark white--text">
<v-icon left color="white">mdi-compare</v-icon>
對比分析
</v-card-title>
<v-card-text>
<!-- Tab 6 內容待實作 -->
<v-alert type="info" class="mt-4">
對比分析功能開發中...
</v-alert>
</v-card-text>
</v-card>
</v-tab-item>
</v-tabs-items>
</v-container>
</v-app>
</div>
</asp:Content>
<asp:Content ID="Content5" ContentPlaceHolderID="footer_script" runat="Server">
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vuetify@2.6.14/dist/vuetify.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<link href="https://cdn.jsdelivr.net/npm/vuetify@2.6.14/dist/vuetify.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/@mdi/font@6.x/css/materialdesignicons.min.css" rel="stylesheet">
<script>
new Vue({
el: '#pivot-query-app',
vuetify: new Vuetify(),
data() {
return {
// 當前頁籤
activeTab: 0,
// 載入狀態
loading: false,
loadingActivities: false,
// 查詢條件
searchCriteria: {
year: new Date().getFullYear(),
month: null
},
// 年份選項2020~2030
yearOptions: Array.from({ length: 11 }, (_, i) => 2020 + i),
// 月份選項
monthOptions: [
{ text: '1月', value: 1 },
{ text: '2月', value: 2 },
{ text: '3月', value: 3 },
{ text: '4月', value: 4 },
{ text: '5月', value: 5 },
{ text: '6月', value: 6 },
{ text: '7月', value: 7 },
{ text: '8月', value: 8 },
{ text: '9月', value: 9 },
{ text: '10月', value: 10 },
{ text: '11月', value: 11 },
{ text: '12月', value: 12 }
],
// 法會清單
activities: [],
// 法會清單表頭
activityHeaders: [
{ text: '序號', value: 'activity_num', width: 100 },
{ text: '法會名稱', value: 'activity_name', width: 250 },
{ text: '開始日期', value: 'start_date', width: 130 },
{ text: '結束日期', value: 'end_date', width: 130 },
{ text: '報名人數', value: 'total_followers', width: 120, align: 'end' },
{ text: '報名筆數', value: 'total_orders', width: 120, align: 'end' },
{ text: '總金額', value: 'total_amount', width: 150, align: 'end' },
{ text: '操作', value: 'actions', width: 120, sortable: false, align: 'center' }
],
// 選中的法會
selectedActivity: null,
// 原始完整資料(供所有頁籤使用)
rawData: [],
// Tab 2: 報名明細
tab2Filter: {
followerName: '',
itemKind: null,
isParent: null
},
tab2CurrentPage: 1,
tab2PageSize: 50,
tab2Headers: [
{ text: '報名日期', value: '報名日期', width: 150 },
{ text: '信眾姓名', value: '信眾姓名', width: 120 },
{ text: '功德類型', value: '功德類型', width: 120 },
{ text: '功德名稱', value: '功德名稱', width: 200 },
{ text: '功德主', value: '功德主', width: 100, align: 'center' },
{ text: '數量', value: '數量', width: 80, align: 'end' },
{ text: '金額', value: '金額', width: 120, align: 'end' },
{ text: '未收', value: '未收', width: 120, align: 'end' }
],
// Tab 3: 信眾參與分析
tab3Filter: {
followerCode: '',
minCount: null,
minAmount: null
},
tab3SortBy: 'count_desc',
tab3Headers: [
{ text: '信眾編號', value: '信眾編號', width: 120 },
{ text: '姓名', value: '姓名', width: 120 },
{ text: '參與次數', value: '參與次數', width: 100, align: 'center' },
{ text: '總金額', value: '總金額', width: 150, align: 'end' },
{ text: '平均金額', value: '平均金額', width: 150, align: 'end' },
{ text: '最近參與日期', value: '最近參與日期', width: 130 }
],
// Tab 4: 收入統計分析
tab4GroupBy: 'monthly',
tab4Headers: [
{ text: '分組名稱', value: '分組名稱', width: 200 },
{ text: '報名人數', value: '報名人數', width: 120, align: 'center' },
{ text: '總金額', value: '總金額', width: 150, align: 'end' },
{ text: '已收', value: '已收', width: 120, align: 'end' },
{ text: '未收', value: '未收', width: 120, align: 'end' },
{ text: '收款率', value: '收款率', width: 100, align: 'center' }
]
};
},
computed: {
// 是否有資料
hasData() {
return this.rawData.length > 0;
},
// Tab 2: 功德類型選項(從原始資料提取)
itemKindOptions() {
if (!this.rawData.length) return [];
const kinds = [...new Set(this.rawData.map(x => x.功德類型))];
return kinds.filter(k => k).sort();
},
// Tab 2: 過濾後的報名明細
filteredRegistrations() {
let data = this.rawData;
// 過濾:信眾姓名
if (this.tab2Filter.followerName) {
data = data.filter(x => x.信眾姓名 && x.信眾姓名.includes(this.tab2Filter.followerName));
}
// 過濾:功德類型
if (this.tab2Filter.itemKind) {
data = data.filter(x => x.功德類型 === this.tab2Filter.itemKind);
}
// 過濾:功德主
if (this.tab2Filter.isParent !== null) {
data = data.filter(x => x.功德主 === (this.tab2Filter.isParent ? '是' : '否'));
}
return data;
},
// Tab 2: 前端分頁
paginatedRegistrations() {
const start = (this.tab2CurrentPage - 1) * this.tab2PageSize;
const end = start + this.tab2PageSize;
return this.filteredRegistrations.slice(start, end);
},
// Tab 2: 不重複信眾數
uniqueFollowersCount() {
if (!this.filteredRegistrations.length) return 0;
const followers = new Set(this.filteredRegistrations.map(x => x.信眾編號));
return followers.size;
},
// Tab 2: 總金額
totalAmount() {
return this.filteredRegistrations.reduce((sum, item) => {
return sum + (item.金額 * item.數量);
}, 0);
},
// Tab 2: 平均金額
averageAmount() {
if (!this.filteredRegistrations.length) return 0;
return Math.round(this.totalAmount / this.filteredRegistrations.length);
},
// Tab 3: 信眾參與分析(從 rawData 計算)
followerAnalysis() {
const followerMap = {};
this.rawData.forEach(item => {
const fNum = item.信眾編號;
if (!followerMap[fNum]) {
followerMap[fNum] = {
信眾編號: fNum,
姓名: item.信眾姓名,
參與次數: 0,
總金額: 0,
最近參與日期: item.報名日期
};
}
followerMap[fNum].參與次數++;
followerMap[fNum].總金額 += (item.金額 * item.數量);
// 更新最近參與日期
if (new Date(item.報名日期) > new Date(followerMap[fNum].最近參與日期)) {
followerMap[fNum].最近參與日期 = item.報名日期;
}
});
// 轉換為陣列,並計算平均金額
return Object.values(followerMap).map(f => ({
...f,
平均金額: Math.round(f.總金額 / f.參與次數)
}));
},
// Tab 3: 過濾並排序信眾分析
filteredFollowerAnalysis() {
let data = this.followerAnalysis;
// 過濾:信眾編號
if (this.tab3Filter.followerCode) {
data = data.filter(x => x.信眾編號 && x.信眾編號.includes(this.tab3Filter.followerCode));
}
// 過濾:最少參與次數
if (this.tab3Filter.minCount) {
data = data.filter(x => x.參與次數 >= this.tab3Filter.minCount);
}
// 過濾:最少總金額
if (this.tab3Filter.minAmount) {
data = data.filter(x => x.總金額 >= this.tab3Filter.minAmount);
}
// 排序
switch (this.tab3SortBy) {
case 'count_desc':
data.sort((a, b) => b.參與次數 - a.參與次數);
break;
case 'count_asc':
data.sort((a, b) => a.參與次數 - b.參與次數);
break;
case 'amount_desc':
data.sort((a, b) => b.總金額 - a.總金額);
break;
case 'amount_asc':
data.sort((a, b) => a.總金額 - b.總金額);
break;
}
return data;
},
// Tab 4: 收入統計分析(從 rawData 計算)
incomeStats() {
const statsMap = {};
const groupBy = this.tab4GroupBy;
this.rawData.forEach(item => {
let key;
switch(groupBy) {
case 'monthly':
key = item.報名日期 ? item.報名日期.substring(0, 7) : '未知'; // YYYY-MM
break;
case 'yearly':
key = item.報名日期 ? item.報名日期.substring(0, 4) : '未知'; // YYYY
break;
case 'activity':
key = item.法會名稱 || '未知';
break;
case 'itemKind':
key = item.功德類型 || '未知';
break;
}
if (!statsMap[key]) {
statsMap[key] = {
分組名稱: key,
報名人數: new Set(),
總金額: 0,
已收: 0,
未收: 0
};
}
statsMap[key].報名人數.add(item.信眾編號);
const amount = item.金額 * item.數量;
statsMap[key].總金額 += amount;
statsMap[key].已收 += item.已收 || 0;
statsMap[key].未收 += item.未收 || amount;
});
// 計算收款率並轉換 Set 為數字
return Object.values(statsMap).map(s => ({
分組名稱: s.分組名稱,
報名人數: s.報名人數.size,
總金額: s.總金額,
已收: s.已收,
未收: s.未收,
收款率: s.總金額 > 0 ? ((s.已收 / s.總金額) * 100).toFixed(1) + '%' : '0%'
})).sort((a, b) => {
// 依分組名稱排序
if (groupBy === 'monthly' || groupBy === 'yearly') {
return b.分組名稱.localeCompare(a.分組名稱);
}
return b.總金額 - a.總金額;
});
}
},
methods: {
/**
* 載入法會清單
*/
async loadActivities() {
if (!this.searchCriteria.year) {
this.$swal('提示', '請選擇年份', 'warning');
return;
}
this.loadingActivities = true;
this.activities = [];
try {
// 計算日期範圍
let startDate, endDate;
if (this.searchCriteria.month) {
// 指定月份
startDate = `${this.searchCriteria.year}-${String(this.searchCriteria.month).padStart(2, '0')}-01`;
const lastDay = new Date(this.searchCriteria.year, this.searchCriteria.month, 0).getDate();
endDate = `${this.searchCriteria.year}-${String(this.searchCriteria.month).padStart(2, '0')}-${lastDay}`;
} else {
// 整年
startDate = `${this.searchCriteria.year}-01-01`;
endDate = `${this.searchCriteria.year}-12-31`;
}
// 呼叫 API
const response = await axios.get(HTTP_HOST + 'api/pivot/activity_stats', {
params: {
startDate: startDate,
endDate: endDate
}
});
if (response.data.success) {
this.activities = response.data.data;
if (this.activities.length === 0) {
this.$swal('提示', '查無法會資料', 'info');
}
} else {
throw new Error(response.data.message || '查詢失敗');
}
} catch (error) {
console.error('載入法會清單失敗:', error);
this.$swal('錯誤', error.response?.data?.Message || error.message || '查詢失敗', 'error');
} finally {
this.loadingActivities = false;
}
},
/**
* 選擇法會並載入完整資料
*/
async selectActivity(activity) {
this.loading = true;
this.selectedActivity = activity;
try {
// 一次性查詢完整報名明細(不分頁)
const response = await axios.get(HTTP_HOST + 'api/pivot/registration_details', {
params: {
activityNum: activity.activity_num,
pageSize: 9999 // 取得全部資料
}
});
if (response.data.success) {
// 存入原始資料(供所有頁籤使用)
this.rawData = response.data.data.list;
// 自動切換到 Tab 2
this.activeTab = 1;
this.$swal({
icon: 'success',
title: '載入成功',
text: `已載入 ${this.rawData.length} 筆報名資料`,
timer: 2000,
showConfirmButton: false
});
} else {
throw new Error(response.data.message || '載入失敗');
}
} catch (error) {
console.error('載入報名資料失敗:', error);
this.$swal('錯誤', error.response?.data?.Message || error.message || '載入失敗', 'error');
} finally {
this.loading = false;
}
},
/**
* 重設查詢條件
*/
resetSearch() {
this.searchCriteria = {
year: new Date().getFullYear(),
month: null
};
this.activities = [];
this.rawData = [];
this.selectedActivity = null;
this.activeTab = 0;
},
/**
* 格式化日期
*/
formatDate(date) {
if (!date) return '-';
const d = new Date(date);
return d.toLocaleDateString('zh-TW', { year: 'numeric', month: '2-digit', day: '2-digit' });
},
/**
* 格式化金額
*/
formatCurrency(amount) {
if (amount === null || amount === undefined) return '-';
return new Intl.NumberFormat('zh-TW', {
style: 'currency',
currency: 'TWD',
minimumFractionDigits: 0
}).format(amount);
},
/**
* 格式化日期時間
*/
formatDateTime(datetime) {
if (!datetime) return '-';
const d = new Date(datetime);
return d.toLocaleString('zh-TW', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
},
/**
* Tab 2: 應用過濾條件
*/
applyTab2Filter() {
// 重置頁碼
this.tab2CurrentPage = 1;
},
/**
* Tab 2: 匯出 Excel
*/
async exportTab2Data() {
if (!this.filteredRegistrations.length) {
Swal.fire('提示', '沒有資料可匯出', 'warning');
return;
}
try {
// 顯示載入訊息
Swal.fire({
title: '匯出中...',
text: '請稍候',
icon: 'info',
showConfirmButton: false,
allowOutsideClick: false,
didOpen: () => {
Swal.showLoading();
}
});
// 延遲一點時間讓 UI 更新
await new Promise(resolve => setTimeout(resolve, 100));
// 準備匯出資料
const exportData = this.filteredRegistrations.map(item => ({
'報名日期': this.formatDateTime(item.報名日期),
'信眾編號': item.信眾編號,
'信眾姓名': item.信眾姓名,
'功德類型': item.功德類型,
'功德名稱': item.功德名稱,
'功德主': item.功德主,
'數量': item.數量,
'金額': item.金額,
'已收': item.已收,
'未收': item.未收
}));
// 轉換為 CSV處理特殊欄位格式
const headers = Object.keys(exportData[0]);
const csvRows = [
headers.join(','),
...exportData.map(row => {
return headers.map(h => {
let value = row[h];
// 信眾編號強制為文字(使用 ="值" 格式)
if (h === '信眾編號') {
return `"=""${value}"""`;
}
// 一般文字欄位:如果包含逗號、引號、換行,需要用雙引號包起來
if (typeof value === 'string') {
if (value.includes(',') || value.includes('"') || value.includes('\n')) {
return `"${value.replace(/"/g, '""')}"`;
}
return value;
}
// 數字欄位(處理 null/undefined
return value !== null && value !== undefined ? value : '';
}).join(',');
})
];
const csv = csvRows.join('\n');
// 下載檔案
const blob = new Blob(['\uFEFF' + csv], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', `報名明細_${this.selectedActivity.activity_name}_${new Date().getTime()}.csv`);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
// 關閉載入訊息並顯示成功
Swal.fire('成功', '匯出完成', 'success');
} catch (error) {
console.error('匯出失敗:', error);
Swal.fire('錯誤', `匯出失敗: ${error.message}`, 'error');
}
},
/**
* 取得收款率顏色
*/
getPaymentRateColor(rate) {
const numRate = parseFloat(rate);
if (numRate >= 80) return 'success';
if (numRate >= 50) return 'warning';
return 'error';
}
},
mounted() {
console.log('Pivot Query App 已載入');
// 檢查是否有 SweetAlert2
if (typeof Swal === 'undefined') {
console.warn('SweetAlert2 未載入,將使用原生 alert');
this.$swal = function(title, text, icon) {
alert(`${title}\n${text}`);
return Promise.resolve();
};
} else {
this.$swal = Swal.fire.bind(Swal);
}
}
});
</script>
</asp:Content>