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,需確認:

  1. 是否複用後台 API (/admin/questionType/list, /admin/questionIssue/list)
  2. 或新增前台專用端點 (/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 (需調整權限)
  • 更新 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                            # 類型定義 (待建立)

風險與待確認事項

  1. 雷達圖數據格式: 後端需確認 getCategoryAvgScores() 的實作方式
  2. 篩選選項 API: 需決定是新增前台端點或複用後台 API
  3. 「無」選項處理: 題型/議題可能有未分類的情況,需與 PM 確認如何顯示
  4. 效能考量: 大量資料時 JSON_EXTRACT 的查詢效能

相關筆記


更新紀錄

日期更新內容
2026-02-06初始分析,建立 checklist