SCRUM-36 前台數據頁實作分析
JIRA: SCRUM-36 Parent: SCRUM-11 (前台-資訊分析頁) 難度: ⭐⭐⭐ (中等) 預估工作量: 3-5 天
功能需求摘要
篩選條件
| 篩選項 | 類型 | 選項 | 預設值 |
|---|---|---|---|
| 時間篩選 | 下拉選單 | 7日 / 30日 / 全部 | 全部 |
| 指定題型 | 下拉選單 | 對應後台設定 | 全部 |
| 指定議題 | 下拉選單 | 對應後台設定 | 全部 |
| 得分範圍 | 下拉選單 | 0-10分 / 11-15分 / 16-20分 / 全部 | 全部 |
統計數據卡片
| 指標 | 說明 |
|---|---|
| 已批改題數 | 篩選條件內的批改總數 |
| 平均做題時間 | 單位:秒 |
| 平均字數 | 使用 CHAR_LENGTH 計算 |
| 平均得分 | 總分平均 |
雷達圖 (四維評分)
| 位置 | 維度 | JSON 欄位 |
|---|---|---|
| 上方 | 文法平均得分 | ai_score.grammar.score |
| 右方 | 字彙平均得分 | ai_score.vocabulary.score |
| 下方 | 內容平均得分 | ai_score.content.score |
| 左方 | 組織平均得分 | ai_score.organization.score |
數值最大 5 分,顯示至小數點第一位 (如 3.2)
特殊狀態處理
- 無批閱結果提示: 數據以已批閱為主,需增加作題後未批閱的提示文字
- 無標籤狀態: 題型、議題可能有無指定標籤,需額外增加「無」或「其他」選項
現有後端 API 分析
✅ 已實作: POST /web/statistics/summary
檔案: /home/matt/Github/writeahead-main-api/app/Controllers/Web/StatisticsController.php
請求參數:
{
"date_range": "7" | "30" | "all",
"type_id": 1,
"issue_id": 2,
"score_range": "0-10" | "11-15" | "16-20" | "all"
}回傳欄位:
{
"status": "success",
"data": {
"total_reviewed_count": 100,
"reviewed_count": 50,
"avg_time": 1800.5,
"avg_word_count": 350.25,
"total_avg_score": 14.8
}
}❌ 待實作: 雷達圖數據 (category_avg_scores)
目前 StatisticsModel.php:33 有 TODO 註解:
// TODO: 個別項目得分平均,待確認後實作
// 'category_avg_scores' => $this->getCategoryAvgScores($filters, $userEmail),需新增欄位:
{
"category_avg_scores": {
"grammar": 3.2,
"vocabulary": 4.1,
"content": 3.8,
"organization": 3.5
}
}❌ 待確認: 篩選選項 API
目前前台沒有取得題型/議題列表的 API,需確認:
- 是否複用後台 API (
/admin/questionType/list,/admin/questionIssue/list) - 或新增前台專用端點 (
/web/question/types,/web/question/issues)
ai_score JSON 結構
interface ScoringData {
content: ScoreItem
organization: ScoreItem
grammar: ScoreItem
vocabulary: ScoreItem
}
interface ScoreItem {
score: number // 0-5 分
comments: {
en: string
zh: string
}
comment: string
}MySQL JSON 提取範例:
SELECT
AVG(JSON_EXTRACT(ai_score, '$.grammar.score')) as grammar_avg,
AVG(JSON_EXTRACT(ai_score, '$.vocabulary.score')) as vocabulary_avg,
AVG(JSON_EXTRACT(ai_score, '$.content.score')) as content_avg,
AVG(JSON_EXTRACT(ai_score, '$.organization.score')) as organization_avg
FROM answer
WHERE ai_score IS NOT NULL AND is_review = 1實作 Checklist
後端 (writeahead-main-api)
-
擴展 StatisticsModel 加入雷達圖數據
- 檔案:
app/Models/Web/StatisticsModel.php - 新增
getCategoryAvgScores()方法 - 使用
JSON_EXTRACT解析ai_score欄位 - 處理
ai_score為 NULL 的情況
- 檔案:
-
擴展 StatisticsController 回傳雷達圖數據
- 檔案:
app/Controllers/Web/StatisticsController.php - 在
summary()方法加入category_avg_scores
- 檔案:
-
新增篩選選項 API (可選)
- 選項 A: 新增
GET /web/statistics/filters回傳題型/議題列表 - 選項 B: 複用後台 API (需調整權限)
- 選項 A: 新增
-
更新 API 文件
- 檔案:
docs/API_Web_Statistics.md - 記錄
category_avg_scores欄位 - 記錄篩選選項 API
- 檔案:
前端 (writeahead-web)
安裝依賴
-
npm install echarts vue-echarts
建立 Types
- 新增統計類型定義
- 檔案:
types/statistics.ts
interface StatisticsFilters { dateRange: '7' | '30' | 'all' typeId: number | null issueId: number | null scoreRange: '0-10' | '11-15' | '16-20' | 'all' } interface CategoryScores { grammar: number vocabulary: number content: number organization: number } interface StatisticsSummary { totalReviewedCount: number reviewedCount: number avgTime: number avgWordCount: number totalAvgScore: number categoryAvgScores: CategoryScores | null } - 檔案:
建立 Server API
-
統計摘要 API
- 檔案:
server/api/statistics/summary.post.ts - 轉發至後端
/web/statistics/summary - 轉換命名慣例 (snake_case → camelCase)
- 檔案:
-
篩選選項 API (可選)
- 檔案:
server/api/statistics/filters.get.ts - 取得題型/議題選項列表
- 檔案:
建立 Composables
-
API 呼叫層
- 檔案:
app/composables/useStatisticsApi.ts fetchSummary(filters: StatisticsFilters)fetchFilters()(可選)
- 檔案:
-
業務邏輯層
- 檔案:
app/composables/useStatistics.ts - 管理篩選狀態
- 管理統計數據
- 處理載入/錯誤狀態
- 檔案:
建立組件
-
篩選面板
- 檔案:
app/components/statistics/FilterPanel.vue - 4 個下拉選單 (時間/題型/議題/得分)
- 篩選變更時觸發 API 呼叫
- 檔案:
-
統計卡片
- 檔案:
app/components/statistics/SummaryCards.vue - 4 張統計卡片 (批改題數/平均時間/平均字數/平均得分)
- 時間格式化 (秒 → 分:秒)
- 檔案:
-
雷達圖
- 檔案:
app/components/statistics/RadarChart.vue - 使用 ECharts radar 圖表
- 四維:文法/字彙/內容/組織
- 最大值 5,顯示一位小數
- 使用
<ClientOnly>包裹 (SSR 兼容)
- 檔案:
-
空狀態提示
- 檔案:
app/components/statistics/EmptyState.vue - 無資料時顯示引導文字
- 未批閱提示
- 檔案:
實作頁面
- 數據分析頁
- 檔案:
app/pages/analysis.vue - 整合所有組件
- 設定 dashboard layout
- 處理初始載入
- 檔案:
測試
- 篩選條件變更正確觸發 API
- 雷達圖正確顯示四維數據
- 空資料狀態正確顯示
- 時間格式化正確 (秒 → 可讀格式)
- SSR 無報錯
技術決策
圖表庫選擇
| 選項 | 優點 | 缺點 | 推薦度 |
|---|---|---|---|
| ECharts | 雷達圖支援完整、中文文檔 | 體積較大 (~800KB) | ⭐⭐⭐⭐⭐ |
| Chart.js | 輕量 (~200KB) | 雷達圖功能有限 | ⭐⭐⭐ |
結論: 採用 ECharts + vue-echarts
SSR 處理
<ClientOnly>
<RadarChart :data="categoryScores" />
</ClientOnly>空資料處理
// 避免 NaN / undefined
const safeScore = (score: number | undefined) =>
typeof score === 'number' && !isNaN(score) ? score.toFixed(1) : '--'關鍵檔案路徑
後端
/home/matt/Github/writeahead-main-api/
├── app/Controllers/Web/StatisticsController.php # 控制器
├── app/Models/Web/StatisticsModel.php # 模型 (需擴展)
└── docs/API_Web_Statistics.md # API 文件
前端
/home/matt/Github/writeahead-web/
├── app/pages/analysis.vue # 頁面 (待實作)
├── app/composables/
│ ├── useStatistics.ts # 業務邏輯 (待建立)
│ └── useStatisticsApi.ts # API 呼叫 (待建立)
├── app/components/statistics/
│ ├── FilterPanel.vue # 篩選面板 (待建立)
│ ├── SummaryCards.vue # 統計卡片 (待建立)
│ ├── RadarChart.vue # 雷達圖 (待建立)
│ └── EmptyState.vue # 空狀態 (待建立)
├── server/api/statistics/
│ └── summary.post.ts # Server API (待建立)
└── types/statistics.ts # 類型定義 (待建立)
風險與待確認事項
- 雷達圖數據格式: 後端需確認
getCategoryAvgScores()的實作方式 - 篩選選項 API: 需決定是新增前台端點或複用後台 API
- 「無」選項處理: 題型/議題可能有未分類的情況,需與 PM 確認如何顯示
- 效能考量: 大量資料時 JSON_EXTRACT 的查詢效能
相關筆記
- WriteAhead Sprint 4 工程分析報告
- SCRUM-43 後台數據監控與管理實作分析
- SCRUM-67 核心數據抽取實作分析
更新紀錄
| 日期 | 更新內容 |
|---|---|
| 2026-02-06 | 初始分析,建立 checklist |