46 KiB
法會報名統計
相關檢視VIEW
法會統計
CREATE VIEW [dbo].[法會統計]
AS
SELECT TOP (100) PERCENT dbo.activity.num, dbo.activity.category_kind, dbo.activity_category_kind.kind AS 活動主類型, dbo.activity.kind,
dbo.activity_kind.kind AS 活動詳細分類, dbo.activity.subject AS 活動名稱, dbo.activity.startDate_solar AS 開始日期,
dbo.activity.endDate_solar AS 結束日期, dbo.activity.dueDate AS 報名截止日, SUM(dbo.pro_order_detail.qty) AS 功德數量,
SUM(dbo.pro_order_detail.price) AS 合計
FROM dbo.pro_order_detail LEFT OUTER JOIN
dbo.pro_order ON dbo.pro_order_detail.order_no = dbo.pro_order.order_no RIGHT OUTER JOIN
dbo.activity ON dbo.pro_order.activity_num = dbo.activity.num LEFT OUTER JOIN
dbo.activity_kind ON dbo.activity.kind = dbo.activity_kind.num LEFT OUTER JOIN
dbo.activity_category_kind ON dbo.activity.category_kind = dbo.activity_category_kind.num
GROUP BY dbo.activity.num, dbo.activity.category_kind, dbo.activity_category_kind.kind, dbo.activity.kind, dbo.activity_kind.kind, dbo.activity.subject,
dbo.activity.startDate_solar, dbo.activity.endDate_solar, dbo.activity.dueDate
GO
報名明細查詢
CREATE VIEW [dbo].[報名明細查詢]
AS
SELECT dbo.activity.num AS 法會ID, dbo.activity.subject AS 法會名稱, dbo.activity.startDate_solar AS 開始日期, dbo.activity.endDate_solar AS 結束日期,
dbo.followers.f_number AS 信眾編號, dbo.followers.u_name AS 信眾姓名, dbo.pro_order.order_no AS 報名編號, dbo.pro_order.up_time AS 報名日期,
CASE WHEN parent_num IS NOT NULL THEN '是' ELSE '否' END AS 功德主, dbo.actItem_kind.kind AS 功德類型, dbo.actItem.subject AS 功德名稱,
dbo.pro_order_detail.qty AS 數量, dbo.pro_order_detail.price AS 金額, 0 AS 已收, dbo.pro_order_detail.price * dbo.pro_order_detail.qty - 0 AS 未收
FROM dbo.pro_order_detail INNER JOIN
dbo.pro_order ON dbo.pro_order_detail.order_no = dbo.pro_order.order_no INNER JOIN
dbo.actItem ON dbo.pro_order_detail.actItem_num = dbo.actItem.num INNER JOIN
dbo.activity ON dbo.pro_order.activity_num = dbo.activity.num INNER JOIN
dbo.followers ON dbo.pro_order.f_num = dbo.followers.num INNER JOIN
dbo.actItem_kind ON dbo.actItem.kind = dbo.actItem_kind.num
GO
欄位分類
// 🟠 橘色欄位 - 法會基礎資料 (法會相關的直接來源)
法會ID = act.num,
法會名稱 = act.subject,
開始日期 = act.startDate_solar,
結束日期 = act.endDate_solar,
// 🔵 藍色欄位 - 信眾基礎資料 (信眾相關的直接來源)
信眾編號 = f.f_number,
信眾姓名 = f.u_name,
報名編號 = po.order_no,
報名日期 = po.up_time,
// 🟢綠色欄位 - 功德資訊欄位 (功德相關資訊)
功德主 = pod.parent_num != null ? "是" : "否",
功德類型 = aik.kind,
功德名稱 = ai.subject,
// 🟣 紫色欄位 - 計算欄位 (需要計算)
數量 = pod.qty,
金額 = pod.price,
已收 = 0, // 業務邏輯:固定為 0
未收 = pod.price * pod.qty - 0, // 計算未收金額
API設計
- pivot01Controller
- 查詢: 法會統計, 條件參數: 年份
- 查詢: 報名明細查詢, 條件參數: 法會ID
- 因為: 希望能隨時加上新的檢視, 不需要動到EF-MODEL
- 所以: 直接用SQL SELECT指令查詢, VIEW名稱也以字串命名
- 各檢視的查詢, 都用很固定的模式.
- 例: SELECT * FROM {VIEW名稱} WHERE {條件欄位} = {條件值(參數傳入)} { AND/OR 其他固定條件} ORDER BY {欄位, 欄位, ...}
- 傳回欄位: 都以中文名稱為主, 故不用定義對照表
- 其它統計, 都以VUE/JS, 在瀏覽器端操作
頁面設計需求
- 檔名: pivot/pivot01.aspx
- .cs只是個空架子, 參考: D:\dev\ez\17168erp\git_17888\web\admin\pivot\query.aspx.cs
- 頁籤式操作介面
- Tab1 : 法會選擇頁面
- 相關檢視: 法會統計
- 一開始只有Tab1, 其它隱藏
- 年份選擇, 預設今年, 切換年份時重新查詢
- 表格顯示資料條件:
- 當年度(日期範圍涵蓋), 及無日期範圍(NULL)
- 排序: 日期
- 表格按鈕: "選擇", 點選後, 以該場法會活動, 為查詢條件
- 查詢: 報名明細查詢, where: 指定法會的所有資料
- 按"選擇"後, 才出現Tab2~5
- Tab2 : 牌位分析
- PIVOT TABLE, 詳見說明
- Tab3 : 信眾報名分析
- PIVOT TABLE, 詳見說明
- Tab4 : 功德主查詢分析
- PIVOT TABLE, 詳見說明
- Tab5 : 報名明細資料
- 相關檢視: 報名明細查詢
- 以表格呈現資料
- Tab1所選擇法會的所有明細資料
Tab1 : 法會選擇頁面
- 查詢列:
- 下拉:年份選擇,
- 今年起前後5年, 預設今年
- 可自行輸入, 也可輸入前後5年以外的年份
- "查看"按鈕,
- 按下即查詢所選年份的所有活動
- 更新下方表格
- 下拉:年份選擇,
- 法會表格:
- 頁面載入時, 依下拉預設查詢當年度所有法會(包含)
- 直接列出所有欄位
Tab2 : 牌位分析
- 參考的EXCEL樞紐分析表
- 篩選: 法會名稱(不用處理, 因為我們的查詢, 已經只限一場法會了)
- 列: 功德類型, 功德名稱
- 欄: 無
- 值: 數量:加總, 金額加總
- UI呈現:
- 以vuetify Table呈現樞紐分析表
- 用JS把該法會明細資料變成PIVOT TABLE的型式
- 可以2層收合表格列(功德類型, 功德名稱)
- 分別顯示: 該類型, 或該功德項目名稱的: 加總數量, 加總金額
- 若無法2層, 就直接呈現4個欄位: 功德類型, 功德名稱, 數量, 金額
- 上下層, 樣式要有明顯區隔
- 最後一列, 要有全部總計, 若vuetify table有footer也可以
Tab3 : 信眾報名分析
- 搜尋欄位: 關鍵字: 姓名過濾
- 也是先做PIVOT運算, 欄位如下:
- 信眾編號
- 信眾姓名
- 功德主(功德主=是)數量
- 功德主(功德主=是)金額
- 功德數量(功德主=否)
- 功德金額(功德主=否)
Tab4 : 功德主查詢分析
- 搜尋欄位:
- 關鍵字: 姓名過濾
- 功德類型下拉: 全部或單一分類
- RADIO : 切換值: 金額, 或:數量
- CHECK : 功德主: 是/否
- PIVOT TABLE
- 第一欄: 信眾姓名, FIXED
- 第二欄: 總計(金額或數量)
- 第三欄以後:
- 標題: 所選功德類型(或全部)的功德項目
- 資料列: (依選取) 金額或數量 (該信眾/該功德)
Tab5 : 報名明細資料
(細節待補充)
- 表格: 欄位: 報名明細查詢的所有欄位
- 各欄位表頭顏色標示為前述分組
實作計劃
Phase 1 : implement controller
目標
創建 pivot01Controller.cs,提供簡單直觀的 API 端點
設計原則
- VIEW 導向: 直接使用 SQL 查詢 VIEW,不依賴 EF Model
- 動態查詢: VIEW 名稱以字串參數傳遞,便於新增 VIEW 而不需改程式
- 固定查詢模式:
SELECT * FROM {VIEW名稱} WHERE {條件欄位} = {參數值} ORDER BY {排序欄位} - 中文欄位名: 查詢結果保留 VIEW 的中文欄位名,前端直接使用
API 端點設計
1. GET /api/pivot01/activity_stats
功能: 查詢法會統計(對應「法會統計」VIEW)
參數:
year(int, required): 查詢年份includeNoDate(bool, optional, default=true): 是否包含無日期的法會
查詢邏輯:
SELECT * FROM [法會統計]
WHERE (YEAR(開始日期) = @year OR YEAR(結束日期) = @year)
OR (@includeNoDate = 1 AND (開始日期 IS NULL OR 結束日期 IS NULL))
ORDER BY 開始日期 DESC, 結束日期 DESC
回應格式:
{
"success": true,
"data": [
{
"num": 123,
"category_kind": 1,
"活動主類型": "法會",
"kind": 5,
"活動詳細分類": "法會-梁皇寶懺",
"活動名稱": "2025年梁皇寶懺",
"開始日期": "2025-03-01",
"結束日期": "2025-03-10",
"報名截止日": "2025-02-25",
"功德數量": 150,
"合計": 500000
}
],
"message": "查詢成功",
"rowCount": 10
}
2. GET /api/pivot01/registration_details
功能: 查詢報名明細(對應「報名明細查詢」VIEW)
參數:
activityNum(int, required): 法會編號(法會ID)page(int, optional, default=1): 頁碼pageSize(int, optional, default=50): 每頁筆數(最大 10000)
查詢邏輯:
SELECT * FROM [報名明細查詢]
WHERE 法會ID = @activityNum
ORDER BY 報名日期 DESC, 報名編號 DESC
OFFSET (@page - 1) * @pageSize ROWS
FETCH NEXT @pageSize ROWS ONLY
回應格式:
{
"success": true,
"data": {
"list": [
{
"法會ID": 123,
"法會名稱": "2025年梁皇寶懺",
"開始日期": "2025-03-01",
"結束日期": "2025-03-10",
"信眾編號": "F12345",
"信眾姓名": "王大明",
"報名編號": "ORD20250101001",
"報名日期": "2025-01-15T10:30:00",
"功德主": "是",
"功德類型": "牌位",
"功德名稱": "消災大牌位",
"數量": 1,
"金額": 3000,
"已收": 0,
"未收": 3000
}
],
"pagination": {
"currentPage": 1,
"pageSize": 50,
"totalRows": 523,
"totalPages": 11
}
},
"message": "查詢成功"
}
實作檔案
D:\dev\ez\17168erp\git_17888\web\App_Code\api\pivot01Controller.cs
程式架構
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Linq;
using System.Web.Http;
using System.Configuration;
[ezAuthorize]
public class pivot01Controller : ApiController
{
private string _connectionString = ConfigurationManager.ConnectionStrings["ezEntities"].ConnectionString;
// 通用 VIEW 查詢方法
private DataTable ExecuteSqlQuery(string sql, SqlParameter[] parameters = null);
private List<Dictionary<string, object>> DataTableToDictionary(DataTable dt);
// API 端點
[HttpGet]
[Route("api/pivot01/activity_stats")]
public IHttpActionResult GetActivityStats(int year, bool includeNoDate = true);
[HttpGet]
[Route("api/pivot01/registration_details")]
public IHttpActionResult GetRegistrationDetails(int activityNum, int page = 1, int pageSize = 50);
}
開發步驟
- 確認資料庫中 VIEW 已建立
- 建立
pivot01Controller.cs - 實作通用 SQL 查詢方法
- 實作
GetActivityStats端點 - 實作
GetRegistrationDetails端點 - 使用 Postman/瀏覽器測試 API
- 調整錯誤處理和回應格式
Phase 2 : implement aspx/cs, basic
目標
建立前端頁面 pivot-01.aspx,實作 Tab1(法會選擇)和 Tab5(報名明細資料)
實作檔案
D:\dev\ez\17168erp\git_17888\web\admin\pivot\pivot-01.aspx
D:\dev\ez\17168erp\git_17888\web\admin\pivot\pivot-01.aspx.cs
設計原則
- 頁籤式介面: 使用 Vuetify Tabs 元件
- 動態顯示: 只在選擇法會後才顯示 Tab2-56
- 響應式設計: 適配桌面和平板
- 資料快取: 選擇法會後一次載入全部明細,供所有 Tab 使用
Tab1: 法會選擇頁面
UI 結構
┌─────────────────────────────────────────────────┐
│ 📊 法會報名統計分析 │
├─────────────────────────────────────────────────┤
│ Tab1: 法會選擇 │ Tab2 │ Tab3 │ Tab4 │ Tab5 │ │
├─────────────────────────────────────────────────┤
│ 查詢條件 │
│ ┌──────────┐ ┌─────────┐ │
│ │ 年份: 2025 ▼│ │ [查看] │ │
│ └──────────┘ └─────────┘ │
│ │
│ 法會清單 (共 15 場) │
│ ┌─────────────────────────────────────────────┐│
│ │編號│法會名稱│日期│報名數│金額│動作│ ││
│ ├─────────────────────────────────────────────┤│
│ │123│2025梁皇...│3/1-3/10│150│50萬│[選擇]│ ││
│ │124│2025地藏...│4/5-4/12│200│60萬│[選擇]│ ││
│ └─────────────────────────────────────────────┘│
└─────────────────────────────────────────────────┘
功能規格
-
年份選擇器
- 下拉選單,範圍:今年 ± 5 年
- 預設值:當前年份(2025)
- 可手動輸入其他年份
-
查看按鈕
- 點擊後呼叫 API:
GET /api/pivot01/activity_stats?year={year} - 顯示 Loading 動畫
- 更新法會清單表格
- 點擊後呼叫 API:
-
法會清單表格
- 欄位:編號、法會名稱、日期範圍、報名數、總金額、操作按鈕
- 排序:日期降序
- 可搜尋、可排序
-
選擇按鈕
- 點擊後呼叫 API:
GET /api/pivot01/registration_details?activityNum={num}&pageSize=10000 - 儲存完整資料到 Vue data (
rawData) - 顯示 Tab2-6
- 自動切換到 Tab5
- 點擊後呼叫 API:
Tab5: 報名明細資料
UI 結構
┌─────────────────────────────────────────────────┐
│ Tab1 │ Tab2 │ Tab3 │ Tab4 │ Tab5: 報名明細資料 │ │
├─────────────────────────────────────────────────┤
│ 📌 當前法會: 2025年梁皇寶懺 (3/1-3/10) │
│ 📊 總筆數: 523 筆 │ 總金額: NT$ 1,569,000 │
│ │
│ 🔍 搜尋: [____________________] [匯出Excel] │
│ │
│ 報名明細表格 │
│ ┌─────────────────────────────────────────────┐│
│ │法會│信眾│姓名│日期│類型│名稱│主│量│金額│收│ ││
│ ├─────────────────────────────────────────────┤│
│ │梁皇│F001│王大│1/15│牌位│消災│是│1│3000│3K│ ││
│ │梁皇│F002│李小│1/16│供養│供僧│否│2│1000│1K│ ││
│ └─────────────────────────────────────────────┘│
│ 顯示 1-50 / 共 523 筆 [1][2][3]...[11] │
└─────────────────────────────────────────────────┘
功能規格
-
資訊列
- 顯示當前法會名稱和日期
- 顯示總筆數和總金額(從
rawData計算)
-
搜尋框
- 可搜尋:信眾編號、信眾姓名、功德類型、功德名稱
- 即時過濾(前端)
-
匯出 Excel 按鈕
- 匯出當前過濾後的資料
- 使用 CSV 格式(UTF-8 BOM)
- 檔名:
報名明細_{法會名稱}_{時間戳記}.csv
-
報名明細表格
- 欄位顏色標示:
- 🟠 橘色:法會相關(法會名稱、日期)
- 🔵 藍色:信眾相關(編號、姓名、報名日期)
- 🟢 綠色:功德相關(類型、名稱、功德主)
- 🟣 紫色:計算欄位(數量、金額、已收、未收)
- 可排序所有欄位
- 分頁顯示(前端分頁)
- 欄位顏色標示:
CSS 樣式
/* 欄位色彩標記 */
.field-activity { border-left: 4px solid #ff9800; } /* 橘色 */
.field-follower { border-left: 4px solid #2196f3; } /* 藍色 */
.field-merit { border-left: 4px solid #4caf50; } /* 綠色 */
.field-calculated { border-left: 4px solid #9c27b0; } /* 紫色 */
Vue 資料結構
data() {
return {
activeTab: 0, // 當前頁籤
// Tab1: 法會選擇
tab1: {
year: 2025, // 預設今年
yearOptions: [2020, 2021, ..., 2030],
loading: false,
activities: [], // 法會清單
selectedActivity: null
},
// 全域資料(所有 Tab 共用)
rawData: [], // 完整報名明細資料
// Tab5: 報名明細
tab5: {
headers: [
{ text: '信眾姓名', value: '信眾姓名', class: 'field-follower' },
{ text: '報名日期', value: '報名日期', class: 'field-follower' },
{ text: '功德類型', value: '功德類型', class: 'field-merit' },
{ text: '功德名稱', value: '功德名稱', class: 'field-merit' },
{ text: '功德主', value: '功德主', class: 'field-merit' },
{ text: '數量', value: '數量', class: 'field-calculated' },
{ text: '金額', value: '金額', class: 'field-calculated' },
{ text: '已收', value: '已收', class: 'field-calculated' },
{ text: '未收', value: '未收', class: 'field-calculated' }
],
search: ''
}
}
},
computed: {
hasData() { return this.rawData && this.rawData.length > 0; },
filteredData() { /* 搜尋過濾邏輯 */ },
totalAmount() { /* 總金額計算 */ }
},
methods: {
async loadActivities() { /* 載入法會清單 */ },
async selectActivity(activity) { /* 選擇法會並載入明細 */ },
exportToExcel() { /* 匯出 CSV */ }
}
開發步驟
- 建立
pivot-01.aspx.cs(空架子,參考 query.aspx.cs) - 建立
pivot-01.aspx基本框架(MasterPage、Content) - 實作 Tab1 UI(年份選擇、法會表格)
- 實作 Tab1 邏輯(API 呼叫、資料綁定)
- 實作 Tab5 UI(資訊列、搜尋框、明細表格)
- 實作 Tab5 邏輯(過濾、排序、分頁)
- 實作匯出 Excel 功能
- 調整 CSS 樣式(欄位顏色標示)
- 測試所有功能
資料流程
使用者選擇年份 → API查詢法會列表 → 顯示法會表格
↓
使用者點選法會 → API載入完整明細 → 儲存到rawData
↓
切換到Tab5 → 顯示表格 → 可搜尋/排序/匯出
Phase 3 : Tab2 牌位分析實作計劃
目標
實作 Tab2 牌位分析功能,將 rawData 轉換為樞紐分析表格式,提供功德類型和功德名稱的分層統計
設計規格
資料結構分析
- 來源資料:
rawData(從 Tab1 選擇法會後載入的完整報名明細) - 分組維度:
- 第一層:功德類型 (
功德類型欄位) - 第二層:功德名稱 (
功德名稱欄位)
- 第一層:功德類型 (
- 統計值:
- 數量總計:sum(
數量) - 金額總計:sum(
金額)
- 數量總計:sum(
Vue 資料結構設計
// Tab2: 牌位分析
tab2: {
expanded: [], // 展開的功德類型 ID 陣列
pivotData: [], // 處理後的樞紐資料
loading: false,
headers: [
{ text: '功德項目', value: 'name', width: 300 },
{ text: '數量總計', value: 'totalQty', width: 120, align: 'end' },
{ text: '金額總計', value: 'totalAmount', width: 150, align: 'end' },
{ text: '', value: 'data-table-expand', width: 50 } // 展開按鈕欄位
]
}
樞紐資料轉換演算法
computed: {
tab2PivotData() {
if (!this.rawData || this.rawData.length === 0) return [];
// Step 1: 按功德類型分組
const typeGroups = {};
this.rawData.forEach(row => {
const type = row.功德類型 || '未分類';
const name = row.功德名稱 || '未命名';
const qty = parseInt(row.數量) || 0;
const amount = parseFloat(row.金額) || 0;
if (!typeGroups[type]) {
typeGroups[type] = {
name: type,
isGroup: true,
totalQty: 0,
totalAmount: 0,
children: {}
};
}
// Step 2: 按功德名稱分組
if (!typeGroups[type].children[name]) {
typeGroups[type].children[name] = {
name: name,
isGroup: false,
totalQty: 0,
totalAmount: 0,
parentType: type
};
}
// Step 3: 累計統計
typeGroups[type].children[name].totalQty += qty;
typeGroups[type].children[name].totalAmount += amount;
typeGroups[type].totalQty += qty;
typeGroups[type].totalAmount += amount;
});
// Step 4: 轉換為表格資料
const result = [];
Object.values(typeGroups).forEach(group => {
result.push(group);
Object.values(group.children).forEach(child => {
result.push(child);
});
});
return result;
}
}
UI 實作規格
表格結構:
<v-data-table
:headers="tab2.headers"
:items="tab2PivotData"
:expanded.sync="tab2.expanded"
show-expand
item-key="name"
class="elevation-1"
hide-default-footer
>
<!-- 功德項目欄位 -->
<template v-slot:item.name="{ item }">
<div :class="{'font-weight-bold primary--text': item.isGroup, 'ml-6': !item.isGroup}">
<v-icon v-if="item.isGroup" left color="orange">mdi-tag</v-icon>
<v-icon v-else left color="green" small>mdi-circle-small</v-icon>
{{ item.name }}
</div>
</template>
<!-- 數量總計欄位 -->
<template v-slot:item.totalQty="{ item }">
<span :class="{'font-weight-bold': item.isGroup}">
{{ item.totalQty.toLocaleString() }}
</span>
</template>
<!-- 金額總計欄位 -->
<template v-slot:item.totalAmount="{ item }">
<span :class="{'font-weight-bold': item.isGroup}">
{{ formatCurrency(item.totalAmount) }}
</span>
</template>
<!-- 總計 Footer -->
<template v-slot:foot>
<tfoot>
<tr class="grey lighten-3">
<td class="font-weight-bold">總計</td>
<td class="text-right font-weight-bold">{{ grandTotalQty.toLocaleString() }}</td>
<td class="text-right font-weight-bold">{{ formatCurrency(grandTotalAmount) }}</td>
<td></td>
</tr>
</tfoot>
</template>
</v-data-table>
樣式設計:
/* Tab2 樞紐分析表樣式 */
.pivot-group-row {
background-color: #f5f5f5 !important;
border-left: 4px solid #ff9800;
}
.pivot-detail-row {
border-left: 4px solid #4caf50;
}
.pivot-total-row {
background-color: #e3f2fd !important;
border-left: 4px solid #2196f3;
font-weight: bold !important;
}
實作步驟
-
需求分析與設計 (1 小時)
- 確認樞紐分析表的確切需求
- 設計資料轉換演算法
- 規劃 UI 元件結構
-
資料處理邏輯 (2 小時)
- 實作
tab2PivotDatacomputed 屬性 - 實作資料分組和統計演算法
- 添加總計計算邏輯
- 實作
-
UI 元件實作 (2 小時)
- 在
pivot-01.aspx中添加 Tab2 內容 - 實作樞紐分析表格元件
- 添加展開/收合功能
- 在
-
樣式與視覺化 (1 小時)
- 設計分層樣式(群組 vs 明細)
- 添加圖示區分功德類型和名稱
- 實作總計列樣式
-
測試與優化 (1 小時)
- 測試不同資料量的效能
- 驗證統計數字正確性
- 調整使用者體驗
預期成果
功能完成標準:
- ✅ 可正確按功德類型和功德名稱分組顯示
- ✅ 統計數字正確(數量總計、金額總計)
- ✅ 支援展開/收合功德類型
- ✅ 有明顯的視覺層級區分
- ✅ 表格底部顯示全部總計
- ✅ 響應式設計適配不同螢幕
資料範例展示:
┌─────────────────────────────────────────────────┐
│ 📊 牌位分析 │
├─────────────────────────────────────────────────┤
│ ▼ 🏷️ 牌位 │ 150 │ NT$ 450,000 │ │
│ ● 消災大牌位 │ 80 │ NT$ 240,000 │ │
│ ● 祈福小牌位 │ 70 │ NT$ 210,000 │ │
│ ▼ 🏷️ 供養 │ 95 │ NT$ 285,000 │ │
│ ● 供僧 │ 45 │ NT$ 135,000 │ │
│ ● 供燈 │ 50 │ NT$ 150,000 │ │
├─────────────────────────────────────────────────┤
│ 總計 │ 245 │ NT$ 735,000 │ │
└─────────────────────────────────────────────────┘
Phase 4 : Tab3 信眾報名分析實作計劃
目標
實作 Tab3 信眾報名分析功能,統計每位信眾的報名情況,區分功德主和一般信眾的數量與金額
設計規格
需求分析
根據文件說明:
- 搜尋欄位: 關鍵字(姓名過濾)
- PIVOT 運算欄位:
- 信眾編號
- 信眾姓名
- 功德主數量(功德主=是)
- 功德主金額(功德主=是)
- 功德數量(功德主=否)
- 功德金額(功德主=否)
資料結構分析
- 來源資料:
rawData - 分組維度: 信眾(
信眾編號+信眾姓名) - 統計維度:
- 按
功德主欄位分類("是" / "否") - 計算數量和金額
- 按
Vue 資料結構設計
// Tab3: 信眾報名分析
tab3: {
search: '', // 搜尋關鍵字
headers: [
{ text: '信眾編號', value: '信眾編號', width: 120, class: 'field-follower' },
{ text: '信眾姓名', value: '信眾姓名', width: 120, class: 'field-follower' },
{ text: '功德主數量', value: '功德主數量', width: 120, align: 'end', class: 'field-calculated' },
{ text: '功德主金額', value: '功德主金額', width: 150, align: 'end', class: 'field-calculated' },
{ text: '功德數量', value: '功德數量', width: 120, align: 'end', class: 'field-calculated' },
{ text: '功德金額', value: '功德金額', width: 150, align: 'end', class: 'field-calculated' }
]
}
樞紐資料轉換演算法
computed: {
tab3PivotData() {
if (!this.rawData || this.rawData.length === 0) return [];
// Step 1: 按信眾分組
const followerGroups = {};
this.rawData.forEach(row => {
const fNumber = row.信眾編號 || '';
const fName = row.信眾姓名 || '';
const key = fNumber + '_' + fName;
const qty = parseInt(row.數量) || 0;
const amount = parseFloat(row.金額) || 0;
const isMaster = row.功德主 === '是';
if (!followerGroups[key]) {
followerGroups[key] = {
信眾編號: fNumber,
信眾姓名: fName,
功德主數量: 0,
功德主金額: 0,
功德數量: 0,
功德金額: 0
};
}
// Step 2: 累計統計
if (isMaster) {
followerGroups[key].功德主數量 += qty;
followerGroups[key].功德主金額 += amount;
} else {
followerGroups[key].功德數量 += qty;
followerGroups[key].功德金額 += amount;
}
});
// Step 3: 轉換為陣列並排序
return Object.values(followerGroups)
.sort((a, b) => {
// 依功德主金額降序,再依姓名升序
const amountDiff = (b.功德主金額 + b.功德金額) - (a.功德主金額 + a.功德金額);
return amountDiff !== 0 ? amountDiff : a.信眾姓名.localeCompare(b.信眾姓名);
});
},
// 搜尋過濾
tab3FilteredData() {
if (!this.tab3.search) return this.tab3PivotData;
const keyword = this.tab3.search.toLowerCase();
return this.tab3PivotData.filter(row => {
return row.信眾編號.toLowerCase().includes(keyword) ||
row.信眾姓名.toLowerCase().includes(keyword);
});
},
// 總計統計
tab3Summary() {
const data = this.tab3FilteredData;
return {
總人數: data.length,
功德主數量合計: data.reduce((sum, row) => sum + row.功德主數量, 0),
功德主金額合計: data.reduce((sum, row) => sum + row.功德主金額, 0),
功德數量合計: data.reduce((sum, row) => sum + row.功德數量, 0),
功德金額合計: data.reduce((sum, row) => sum + row.功德金額, 0)
};
}
}
UI 實作規格
表格結構:
<v-card>
<v-card-title class="success white--text">
<v-icon left color="white">mdi-account-multiple</v-icon>
信眾報名分析
<v-spacer></v-spacer>
<v-chip v-if="tab1.selectedActivity" color="white" text-color="success">
{{ tab1.selectedActivity.法會名稱 }}
</v-chip>
</v-card-title>
<v-card-text>
<!-- 搜尋列 -->
<v-row class="mb-4">
<v-col cols="12" md="6">
<v-text-field
v-model="tab3.search"
label="搜尋信眾(編號或姓名)"
prepend-icon="mdi-magnify"
clearable
outlined
dense
></v-text-field>
</v-col>
<v-col cols="12" md="6" class="text-right">
<v-btn color="primary" @click="exportTab3ToCSV">
<v-icon left>mdi-download</v-icon>
匯出 Excel
</v-btn>
</v-col>
</v-row>
<!-- 統計資訊列 -->
<div class="info-bar mb-4">
<v-row>
<v-col cols="6" md="3">
<div><strong>總人數</strong></div>
<div class="text-h6">{{ tab3Summary.總人數 }} 人</div>
</v-col>
<v-col cols="6" md="3">
<div><strong>功德主數量</strong></div>
<div class="text-h6">{{ tab3Summary.功德主數量合計.toLocaleString() }}</div>
</v-col>
<v-col cols="6" md="3">
<div><strong>功德主金額</strong></div>
<div class="text-h6">{{ formatCurrency(tab3Summary.功德主金額合計) }}</div>
</v-col>
<v-col cols="6" md="3">
<div><strong>總金額</strong></div>
<div class="text-h6 primary--text">
{{ formatCurrency(tab3Summary.功德主金額合計 + tab3Summary.功德金額合計) }}
</div>
</v-col>
</v-row>
</div>
<!-- 信眾統計表格 -->
<v-data-table
:headers="tab3.headers"
:items="tab3FilteredData"
:items-per-page="20"
class="elevation-1"
>
<!-- 金額欄位格式化 -->
<template v-slot:item.功德主金額="{ item }">
<span :class="{'font-weight-bold success--text': item.功德主金額 > 0}">
{{ formatCurrency(item.功德主金額) }}
</span>
</template>
<template v-slot:item.功德金額="{ item }">
{{ formatCurrency(item.功德金額) }}
</template>
<!-- 無資料提示 -->
<template v-slot:no-data>
<v-alert type="info" class="ma-4">
{{ tab3.search ? '查無符合條件的信眾' : '請先選擇法會以載入分析資料' }}
</v-alert>
</template>
</v-data-table>
</v-card-text>
</v-card>
樣式設計:
/* Tab3 信眾分析樣式 */
.info-bar {
background-color: #f5f5f5;
border-radius: 8px;
padding: 16px;
}
.info-bar .text-h6 {
color: #1976d2;
margin-top: 4px;
}
/* 功德主高亮 */
.follower-master-highlight {
background-color: #fff3e0 !important;
}
實作步驟
-
資料處理邏輯 (2 小時)
- 實作
tab3PivotDatacomputed 屬性 - 實作信眾分組演算法
- 區分功德主和一般功德統計
- 實作搜尋過濾邏輯
- 實作
-
UI 元件實作 (2 小時)
- 在
pivot-01.aspx中添加 Tab3 內容 - 實作搜尋框和過濾功能
- 實作統計資訊列
- 實作信眾統計表格
- 在
-
匯出功能 (1 小時)
- 實作
exportTab3ToCSV()方法 - 處理 CSV 格式和編碼
- 實作
-
測試與優化 (1 小時)
- 測試不同搜尋條件
- 驗證統計數字正確性
- 測試匯出功能
預期成果
功能完成標準:
- ✅ 可正確按信眾分組並統計
- ✅ 區分功德主和一般功德的數量/金額
- ✅ 搜尋功能正常運作
- ✅ 統計資訊列顯示正確
- ✅ 支援匯出 CSV
- ✅ 資料排序合理(按金額降序)
資料範例展示:
┌────────────────────────────────────────────────────────────────┐
│ 👥 信眾報名分析 114年法會報名統計分析 │
├────────────────────────────────────────────────────────────────┤
│ 🔍 [搜尋信眾... ] [匯出Excel] │
│ │
│ 📊 總人數: 85 人 │ 功德主數量: 150 │ 總金額: NT$ 1,569,000 │
├────────────────────────────────────────────────────────────────┤
│ 編號 │ 姓名 │ 功德主數量 │ 功德主金額 │ 功德數量 │ 功德金額 │
│ F001 │ 王大明 │ 5 │ NT$ 15,000 │ 3 │ NT$ 9,000│
│ F002 │ 李小華 │ 3 │ NT$ 9,000 │ 5 │ NT$ 5,000│
│ F003 │ 張美麗 │ 0 │ NT$ 0 │ 2 │ NT$ 6,000│
└────────────────────────────────────────────────────────────────┘
Phase 5 : Tab4 功德主查詢分析實作計劃
目標
實作 Tab4 功德主查詢分析功能,提供動態樞紐分析表,可按功德類型和功德項目查看每位信眾的數量或金額
設計規格
需求分析
根據文件說明:
- 搜尋欄位:
- 關鍵字:姓名過濾
- 功德類型下拉:全部或單一分類
- RADIO:切換值:金額 或 數量
- CHECK:功德主:是/否
- PIVOT TABLE:
- 第一欄:信眾姓名(FIXED)
- 第二欄:總計(金額或數量)
- 第三欄以後:所選功德類型(或全部)的功德項目(橫向展開)
- 資料列:該信眾在該功德項目的金額或數量
資料結構分析
- 來源資料:
rawData - 動態維度:
- 列:信眾姓名
- 欄:功德項目(依選擇的功德類型過濾)
- 值:數量 或 金額(可切換)
- 過濾條件:
- 姓名關鍵字
- 功德類型
- 功德主狀態
Vue 資料結構設計
// Tab4: 功德主查詢分析
tab4: {
search: '', // 姓名關鍵字
meritType: '全部', // 功德類型選擇
meritTypeOptions: [], // 功德類型下拉選項
valueType: 'amount', // 'amount' 或 'quantity'
showMasterOnly: false, // 只顯示功德主
loading: false
}
樞紐資料轉換演算法
computed: {
// 功德類型選項(從 rawData 動態產生)
tab4MeritTypeOptions() {
if (!this.rawData || this.rawData.length === 0) return ['全部'];
const types = new Set(this.rawData.map(row => row.功德類型).filter(Boolean));
return ['全部', ...Array.from(types).sort()];
},
// 過濾後的原始資料
tab4FilteredRawData() {
let data = this.rawData;
// 功德主過濾
if (this.tab4.showMasterOnly) {
data = data.filter(row => row.功德主 === '是');
}
// 功德類型過濾
if (this.tab4.meritType !== '全部') {
data = data.filter(row => row.功德類型 === this.tab4.meritType);
}
// 姓名過濾
if (this.tab4.search) {
const keyword = this.tab4.search.toLowerCase();
data = data.filter(row =>
row.信眾姓名.toLowerCase().includes(keyword)
);
}
return data;
},
// 取得功德項目清單(橫向欄位)
tab4MeritItems() {
const items = new Set(
this.tab4FilteredRawData.map(row => row.功德名稱).filter(Boolean)
);
return Array.from(items).sort();
},
// 動態表頭
tab4Headers() {
const headers = [
{ text: '信眾姓名', value: '信眾姓名', width: 150, fixed: true, class: 'field-follower' }
];
// 總計欄
const valueLabel = this.tab4.valueType === 'amount' ? '總金額' : '總數量';
headers.push({
text: valueLabel,
value: '總計',
width: 120,
align: 'end',
class: 'field-calculated font-weight-bold'
});
// 功德項目欄(動態)
this.tab4MeritItems.forEach(item => {
headers.push({
text: item,
value: item,
width: 100,
align: 'end',
class: 'field-merit'
});
});
return headers;
},
// 樞紐分析資料
tab4PivotData() {
if (!this.tab4FilteredRawData || this.tab4FilteredRawData.length === 0) return [];
// Step 1: 按信眾和功德項目分組
const followerMeritMap = {};
this.tab4FilteredRawData.forEach(row => {
const fName = row.信眾姓名;
const mName = row.功德名稱;
const qty = parseInt(row.數量) || 0;
const amount = parseFloat(row.金額) || 0;
if (!followerMeritMap[fName]) {
followerMeritMap[fName] = { 信眾姓名: fName };
}
if (!followerMeritMap[fName][mName]) {
followerMeritMap[fName][mName] = { qty: 0, amount: 0 };
}
followerMeritMap[fName][mName].qty += qty;
followerMeritMap[fName][mName].amount += amount;
});
// Step 2: 轉換為表格格式
const result = Object.values(followerMeritMap).map(follower => {
const row = { 信眾姓名: follower.信眾姓名 };
let total = 0;
// 計算每個功德項目的值
this.tab4MeritItems.forEach(item => {
if (follower[item]) {
const value = this.tab4.valueType === 'amount'
? follower[item].amount
: follower[item].qty;
row[item] = value;
total += value;
} else {
row[item] = 0;
}
});
row.總計 = total;
return row;
});
// Step 3: 依總計降序排列
return result.sort((a, b) => b.總計 - a.總計);
}
}
UI 實作規格
表格結構:
<v-card>
<v-card-title class="warning white--text">
<v-icon left color="white">mdi-table-pivot</v-icon>
功德主查詢分析
<v-spacer></v-spacer>
<v-chip v-if="tab1.selectedActivity" color="white" text-color="warning">
{{ tab1.selectedActivity.法會名稱 }}
</v-chip>
</v-card-title>
<v-card-text>
<!-- 搜尋與過濾列 -->
<v-row class="mb-4">
<!-- 姓名搜尋 -->
<v-col cols="12" md="3">
<v-text-field
v-model="tab4.search"
label="搜尋姓名"
prepend-icon="mdi-magnify"
clearable
outlined
dense
></v-text-field>
</v-col>
<!-- 功德類型下拉 -->
<v-col cols="12" md="3">
<v-select
v-model="tab4.meritType"
:items="tab4MeritTypeOptions"
label="功德類型"
prepend-icon="mdi-tag"
outlined
dense
></v-select>
</v-col>
<!-- 值類型切換 -->
<v-col cols="12" md="3">
<v-radio-group v-model="tab4.valueType" row dense>
<v-radio label="金額" value="amount"></v-radio>
<v-radio label="數量" value="quantity"></v-radio>
</v-radio-group>
</v-col>
<!-- 功德主過濾 -->
<v-col cols="12" md="3">
<v-checkbox
v-model="tab4.showMasterOnly"
label="只顯示功德主"
dense
></v-checkbox>
</v-col>
</v-row>
<!-- 統計資訊 -->
<div class="info-bar mb-4">
<v-row>
<v-col cols="4">
<strong>信眾數:</strong> {{ tab4PivotData.length }} 人
</v-col>
<v-col cols="4">
<strong>功德項目:</strong> {{ tab4MeritItems.length }} 項
</v-col>
<v-col cols="4">
<v-btn color="primary" small @click="exportTab4ToCSV">
<v-icon left small>mdi-download</v-icon>
匯出 Excel
</v-btn>
</v-col>
</v-row>
</div>
<!-- 樞紐分析表格 -->
<v-data-table
:headers="tab4Headers"
:items="tab4PivotData"
:items-per-page="20"
class="elevation-1 pivot-table-horizontal"
fixed-header
dense
>
<!-- 總計欄位強調 -->
<template v-slot:item.總計="{ item }">
<span class="font-weight-bold primary--text">
{{ tab4.valueType === 'amount' ? formatCurrency(item.總計) : item.總計.toLocaleString() }}
</span>
</template>
<!-- 功德項目欄位格式化 -->
<template v-for="item in tab4MeritItems" v-slot:[`item.${item}`]="{ item: row }">
<span :class="{'grey--text': row[item] === 0}">
{{ tab4.valueType === 'amount' ? formatCurrency(row[item]) : (row[item] || '-') }}
</span>
</template>
<!-- 無資料提示 -->
<template v-slot:no-data>
<v-alert type="info" class="ma-4">
{{ tab4.search ? '查無符合條件的信眾' : '請先選擇法會以載入分析資料' }}
</v-alert>
</template>
</v-data-table>
</v-card-text>
</v-card>
樣式設計:
/* Tab4 橫向樞紐表樣式 */
.pivot-table-horizontal {
overflow-x: auto;
}
.pivot-table-horizontal .v-data-table__wrapper {
overflow-x: auto;
max-height: 600px;
}
/* 固定第一欄 */
.pivot-table-horizontal th:first-child,
.pivot-table-horizontal td:first-child {
position: sticky;
left: 0;
z-index: 2;
background-color: #fff;
box-shadow: 2px 0 4px rgba(0,0,0,0.1);
}
/* 總計欄樣式 */
.pivot-table-horizontal th:nth-child(2),
.pivot-table-horizontal td:nth-child(2) {
background-color: #f5f5f5;
font-weight: bold;
}
/* 零值淡化 */
.grey--text {
color: #9e9e9e !important;
}
實作步驟
-
資料處理邏輯 (3 小時)
- 實作過濾條件邏輯
- 實作動態表頭生成
- 實作橫向樞紐分析演算法
- 處理金額/數量切換
-
UI 元件實作 (3 小時)
- 實作搜尋與過濾區塊
- 實作動態表頭的 v-data-table
- 實作固定第一欄(信眾姓名)
- 處理橫向滾動
-
互動功能 (1 小時)
- 實作即時過濾更新
- 實作值類型切換
- 實作功德主過濾
-
匯出功能 (1 小時)
- 實作
exportTab4ToCSV()方法 - 處理動態欄位的 CSV 匯出
- 實作
-
測試與優化 (1 小時)
- 測試各種過濾組合
- 測試橫向滾動和固定欄
- 驗證統計數字正確性
- 優化大量欄位時的效能
預期成果
功能完成標準:
- ✅ 動態產生功德項目欄位
- ✅ 支援姓名、功德類型、功德主過濾
- ✅ 可切換金額/數量顯示
- ✅ 第一欄(姓名)固定,橫向滾動
- ✅ 總計欄醒目顯示
- ✅ 支援匯出 CSV(包含動態欄位)
- ✅ 零值適當淡化顯示
資料範例展示:
┌───────────────────────────────────────────────────────────────┐
│ 📊 功德主查詢分析 114年法會報名統計分析 │
├───────────────────────────────────────────────────────────────┤
│ 🔍[姓名] [功德類型:全部▼] ⚪金額 ⚪數量 ☑只顯示功德主 │
│ │
│ 信眾數: 35 人 │ 功德項目: 8 項 │ [匯出Excel] │
├───────────────────────────────────────────────────────────────┤
│ 姓名 │ 總金額 │ 消災大牌位│ 祈福小牌位│ 供僧 │ 供燈 │...│
│ 王大明 │ 50,000 │ 15,000 │ 10,000 │ 15K │ 10K │... │
│ 李小華 │ 35,000 │ 20,000 │ 5,000 │ 5K │ 5K │... │
│ 張美麗 │ 28,000 │ 0 │ 18,000 │ 5K │ 5K │... │
└───────────────────────────────────────────────────────────────┘
↑固定欄 ↑ 橫向滾動區域 →
測試計劃
Phase 1 測試
- API 端點測試(使用 Postman)
- 測試
/api/pivot01/activity_stats?year=2025 - 測試不同年份參數
- 測試
includeNoDate參數 - 測試
/api/pivot01/registration_details?activityNum=123 - 測試分頁參數
- 驗證回應格式和資料正確性
- 測試
Phase 2 測試
-
UI 功能測試
- 測試年份選擇器
- 測試法會清單載入
- 測試選擇法會功能
- 測試 Tab 切換(Tab2-6 在選擇前應隱藏)
-
資料顯示測試
- 驗證 Tab5 資料正確顯示
- 測試搜尋功能(信眾、功德類型等)
- 測試排序功能
- 測試分頁功能
-
匯出測試
- 測試匯出 CSV 檔案
- 驗證檔案格式正確(UTF-8 BOM)
- 測試特殊字元處理
- 測試中文編碼
-
瀏覽器相容性
- Chrome
- Edge
- Firefox
預期成果
Phase 1 完成後
✅ pivot01Controller.cs 正常運作
✅ API 端點可正確查詢資料
✅ 回應格式符合前端需求
✅ 錯誤處理完善
Phase 2 完成後
✅ pivot-01.aspx 頁面正常運作
✅ Tab1 可正常查詢和選擇法會
✅ Tab5 可正常顯示報名明細
✅ 搜尋、排序、分頁功能正常
✅ 匯出 Excel 功能正常
✅ 使用者體驗良好