1129 lines
54 KiB
Plaintext
1129 lines
54 KiB
Plaintext
<%@ 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>
|