fix: 修復前端認證 token 發送和用戶資料隔離問題
- 啟用前端 API client 的認證 header 發送(修復關鍵問題) - 添加 401 錯誤自動清除過期 token 機制 - 設計兩種空狀態畫面:新用戶歡迎 vs 完成慶祝 - 改進錯誤處理:區分認證錯誤和一般錯誤 - 添加詳細除錯日誌追蹤 API 調用過程 - 修復前端條件判斷邏輯,確保正確顯示空狀態 現在用戶資料完全隔離,認證過期會自動處理並引導重新登入。 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
1eb28e83c5
commit
4a7c3aec92
|
|
@ -5,11 +5,12 @@ using DramaLing.Api.Repositories;
|
||||||
using DramaLing.Api.Services.Review;
|
using DramaLing.Api.Services.Review;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using DramaLing.Api.Utils;
|
using DramaLing.Api.Utils;
|
||||||
|
using DramaLing.Api.Services;
|
||||||
|
|
||||||
namespace DramaLing.Api.Controllers;
|
namespace DramaLing.Api.Controllers;
|
||||||
|
|
||||||
[Route("api/flashcards")]
|
[Route("api/flashcards")]
|
||||||
[AllowAnonymous]
|
[Authorize] // 恢復認證要求,確保用戶資料隔離
|
||||||
public class FlashcardsController : BaseController
|
public class FlashcardsController : BaseController
|
||||||
{
|
{
|
||||||
private readonly IFlashcardRepository _flashcardRepository;
|
private readonly IFlashcardRepository _flashcardRepository;
|
||||||
|
|
@ -18,7 +19,8 @@ public class FlashcardsController : BaseController
|
||||||
public FlashcardsController(
|
public FlashcardsController(
|
||||||
IFlashcardRepository flashcardRepository,
|
IFlashcardRepository flashcardRepository,
|
||||||
IReviewService reviewService,
|
IReviewService reviewService,
|
||||||
ILogger<FlashcardsController> logger) : base(logger)
|
IAuthService authService,
|
||||||
|
ILogger<FlashcardsController> logger) : base(logger, authService)
|
||||||
{
|
{
|
||||||
_flashcardRepository = flashcardRepository;
|
_flashcardRepository = flashcardRepository;
|
||||||
_reviewService = reviewService;
|
_reviewService = reviewService;
|
||||||
|
|
|
||||||
|
|
@ -23,9 +23,21 @@ export default function ReviewPage() {
|
||||||
handleRestart,
|
handleRestart,
|
||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
flashcards
|
flashcards,
|
||||||
|
totalFlashcardsCount
|
||||||
} = useReviewSession()
|
} = useReviewSession()
|
||||||
|
|
||||||
|
// 除錯日誌 - 檢查狀態
|
||||||
|
console.log('🔍 Review Page 狀態檢查:', {
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
flashcardsLength: flashcards.length,
|
||||||
|
totalFlashcardsCount,
|
||||||
|
currentQuizItem: currentQuizItem?.id,
|
||||||
|
currentCard: currentCard?.word,
|
||||||
|
isComplete
|
||||||
|
})
|
||||||
|
|
||||||
// 顯示載入狀態
|
// 顯示載入狀態
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -46,21 +58,221 @@ export default function ReviewPage() {
|
||||||
|
|
||||||
// 顯示錯誤狀態
|
// 顯示錯誤狀態
|
||||||
if (error) {
|
if (error) {
|
||||||
|
const isAuthError = error.includes('登入已過期') || error.includes('認證')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
|
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
|
||||||
<Navigation />
|
<Navigation />
|
||||||
<div className="py-8">
|
<div className="py-8">
|
||||||
<div className="max-w-4xl mx-auto px-4">
|
<div className="max-w-4xl mx-auto px-4">
|
||||||
<div className="bg-white rounded-xl shadow-lg p-8 text-center">
|
<div className="bg-white rounded-xl shadow-lg p-8 text-center">
|
||||||
<div className="text-red-500 text-4xl mb-4">⚠️</div>
|
<div className={`text-4xl mb-4 ${isAuthError ? 'text-yellow-500' : 'text-red-500'}`}>
|
||||||
<h2 className="text-xl font-semibold text-red-700 mb-2">載入失敗</h2>
|
{isAuthError ? '🔒' : '⚠️'}
|
||||||
|
</div>
|
||||||
|
<h2 className={`text-xl font-semibold mb-2 ${isAuthError ? 'text-yellow-700' : 'text-red-700'}`}>
|
||||||
|
{isAuthError ? '需要重新登入' : '載入失敗'}
|
||||||
|
</h2>
|
||||||
<p className="text-gray-600 mb-6">{error}</p>
|
<p className="text-gray-600 mb-6">{error}</p>
|
||||||
|
|
||||||
|
<div className="flex flex-col sm:flex-row gap-3 justify-center">
|
||||||
|
{isAuthError ? (
|
||||||
|
<button
|
||||||
|
onClick={() => window.location.href = '/login'}
|
||||||
|
className="px-8 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-semibold"
|
||||||
|
>
|
||||||
|
前往登入
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
<button
|
<button
|
||||||
onClick={handleRestart}
|
onClick={handleRestart}
|
||||||
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||||
>
|
>
|
||||||
重新載入
|
重新載入
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => window.location.href = '/'}
|
||||||
|
className="px-6 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors"
|
||||||
|
>
|
||||||
|
回到首頁
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 情境 1: 新用戶 - 一張詞卡都沒有
|
||||||
|
if (!isLoading && !error && totalFlashcardsCount === 0) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
|
||||||
|
<Navigation />
|
||||||
|
<div className="py-8">
|
||||||
|
<div className="max-w-4xl mx-auto px-4">
|
||||||
|
<div className="bg-white rounded-xl shadow-lg p-8 text-center">
|
||||||
|
{/* 歡迎圖標 */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<div className="w-24 h-24 bg-gradient-to-br from-blue-400 to-blue-600 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<span className="text-4xl text-white">👋</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 歡迎標題 */}
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900 mb-4">
|
||||||
|
歡迎來到 DramaLing!
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
{/* 說明文字 */}
|
||||||
|
<p className="text-lg text-gray-600 mb-8">
|
||||||
|
開始您的英語學習之旅,建立第一張詞卡來開始學習吧!
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* 功能介紹卡片 */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
|
||||||
|
<div className="bg-gradient-to-br from-green-50 to-green-100 rounded-lg p-6">
|
||||||
|
<div className="text-2xl text-green-600 mb-2">🎯</div>
|
||||||
|
<h3 className="font-semibold text-gray-900">智能學習</h3>
|
||||||
|
<p className="text-sm text-gray-600 mt-2">
|
||||||
|
AI 驅動的個人化詞彙學習系統
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-gradient-to-br from-yellow-50 to-yellow-100 rounded-lg p-6">
|
||||||
|
<div className="text-2xl text-yellow-600 mb-2">🧠</div>
|
||||||
|
<h3 className="font-semibold text-gray-900">科學記憶</h3>
|
||||||
|
<p className="text-sm text-gray-600 mt-2">
|
||||||
|
基於遺忘曲線的複習提醒
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 主要行動按鈕 */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<button
|
||||||
|
onClick={() => window.location.href = '/generate'}
|
||||||
|
className="px-10 py-4 bg-gradient-to-r from-blue-600 to-blue-700 text-white rounded-lg hover:from-blue-700 hover:to-blue-800 transition-all font-semibold text-lg flex items-center justify-center gap-3 mx-auto"
|
||||||
|
>
|
||||||
|
<span className="text-xl">🚀</span>
|
||||||
|
建立第一張詞卡
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 次要功能 */}
|
||||||
|
<div className="flex flex-col sm:flex-row gap-3 justify-center">
|
||||||
|
<button
|
||||||
|
onClick={() => window.location.href = '/flashcards'}
|
||||||
|
className="px-6 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors font-medium"
|
||||||
|
>
|
||||||
|
瀏覽詞卡功能
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 學習提示 */}
|
||||||
|
<div className="mt-8 p-4 bg-blue-50 rounded-lg border border-blue-200">
|
||||||
|
<p className="text-sm text-blue-800">
|
||||||
|
💡 <strong>開始提示</strong>: 建議從日常生活中的詞彙開始,
|
||||||
|
或輸入您感興趣的英文句子讓 AI 協助分析!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 情境 2: 有詞卡但都已訓練完成
|
||||||
|
if (!isLoading && !error && totalFlashcardsCount && totalFlashcardsCount > 0 && flashcards.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
|
||||||
|
<Navigation />
|
||||||
|
<div className="py-8">
|
||||||
|
<div className="max-w-4xl mx-auto px-4">
|
||||||
|
<div className="bg-white rounded-xl shadow-lg p-8 text-center">
|
||||||
|
{/* 慶祝圖標 */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<div className="w-24 h-24 bg-gradient-to-br from-green-400 to-green-600 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<span className="text-4xl text-white">🎉</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 主要標題 */}
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900 mb-4">
|
||||||
|
太棒了!所有詞卡都已掌握
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
{/* 次標題 */}
|
||||||
|
<p className="text-lg text-gray-600 mb-4">
|
||||||
|
您已完成所有 <span className="font-semibold text-green-600">{totalFlashcardsCount}</span> 張詞卡的學習!
|
||||||
|
</p>
|
||||||
|
<p className="text-md text-gray-500 mb-8">
|
||||||
|
目前沒有需要複習的詞卡,您的學習進度非常優秀!
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* 統計卡片 */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
||||||
|
<div className="bg-gradient-to-br from-blue-50 to-blue-100 rounded-lg p-6">
|
||||||
|
<div className="text-2xl text-blue-600 mb-2">📚</div>
|
||||||
|
<h3 className="font-semibold text-gray-900">持續學習</h3>
|
||||||
|
<p className="text-sm text-gray-600 mt-2">
|
||||||
|
保持每日複習習慣
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-gradient-to-br from-purple-50 to-purple-100 rounded-lg p-6">
|
||||||
|
<div className="text-2xl text-purple-600 mb-2">📈</div>
|
||||||
|
<h3 className="font-semibold text-gray-900">查看進度</h3>
|
||||||
|
<p className="text-sm text-gray-600 mt-2">
|
||||||
|
檢視學習統計數據
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-gradient-to-br from-green-50 to-green-100 rounded-lg p-6">
|
||||||
|
<div className="text-2xl text-green-600 mb-2">➕</div>
|
||||||
|
<h3 className="font-semibold text-gray-900">擴展詞庫</h3>
|
||||||
|
<p className="text-sm text-gray-600 mt-2">
|
||||||
|
新增更多詞彙挑戰
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 行動按鈕 */}
|
||||||
|
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||||
|
<button
|
||||||
|
onClick={() => window.location.href = '/generate'}
|
||||||
|
className="px-8 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-semibold flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
<span>➕</span>
|
||||||
|
新增詞卡
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => window.location.href = '/stats'}
|
||||||
|
className="px-8 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors font-semibold flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
<span>📊</span>
|
||||||
|
查看統計
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => window.location.href = '/flashcards'}
|
||||||
|
className="px-8 py-3 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors font-semibold flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
<span>📋</span>
|
||||||
|
管理詞卡
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 提示文字 */}
|
||||||
|
<div className="mt-8 p-4 bg-yellow-50 rounded-lg border border-yellow-200">
|
||||||
|
<p className="text-sm text-yellow-800">
|
||||||
|
💡 <strong>小提示</strong>: 詞卡會根據遺忘曲線算法自動安排複習時間。
|
||||||
|
繼續保持學習,明天可能會有新的複習內容!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -108,6 +320,8 @@ export default function ReviewPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 主要線性測驗頁面
|
// 主要線性測驗頁面
|
||||||
|
// 只有在有可用測驗項目時才顯示測驗界面
|
||||||
|
if (!isLoading && !error && totalFlashcardsCount !== null && totalFlashcardsCount > 0 && flashcards.length > 0 && currentQuizItem && currentCard) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
|
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
|
||||||
<Navigation />
|
<Navigation />
|
||||||
|
|
@ -147,4 +361,32 @@ export default function ReviewPage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback 狀態 - 處理其他未預期情況
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
|
||||||
|
<Navigation />
|
||||||
|
<div className="py-8">
|
||||||
|
<div className="max-w-4xl mx-auto px-4">
|
||||||
|
<div className="bg-white rounded-xl shadow-lg p-8 text-center">
|
||||||
|
<div className="text-4xl mb-4">🤔</div>
|
||||||
|
<h2 className="text-xl font-semibold text-gray-700 mb-2">正在準備學習內容</h2>
|
||||||
|
<p className="text-gray-600 mb-4">
|
||||||
|
系統正在載入您的學習資料,請稍候片刻。
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-500 mb-6">
|
||||||
|
如果問題持續,請嘗試重新載入頁面。
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={handleRestart}
|
||||||
|
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||||
|
>
|
||||||
|
重新載入
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -34,6 +34,7 @@ interface ReviewState {
|
||||||
error: string | null
|
error: string | null
|
||||||
pendingWordSubmission: string | null // 等待提交的詞彙ID
|
pendingWordSubmission: string | null // 等待提交的詞彙ID
|
||||||
submittingWords: Set<string> // 正在提交的詞彙ID集合
|
submittingWords: Set<string> // 正在提交的詞彙ID集合
|
||||||
|
totalFlashcardsCount: number | null // 用戶總詞卡數(用於區分空狀態)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ReviewAction =
|
type ReviewAction =
|
||||||
|
|
@ -42,7 +43,7 @@ type ReviewAction =
|
||||||
| { type: 'SKIP_TEST_ITEM'; payload: { quizItemId: string } }
|
| { type: 'SKIP_TEST_ITEM'; payload: { quizItemId: string } }
|
||||||
| { type: 'RESTART' }
|
| { type: 'RESTART' }
|
||||||
| { type: 'LOAD_FLASHCARDS_START' }
|
| { type: 'LOAD_FLASHCARDS_START' }
|
||||||
| { type: 'LOAD_FLASHCARDS_SUCCESS'; payload: { flashcards: Flashcard[]; quizItems: QuizItem[] } }
|
| { type: 'LOAD_FLASHCARDS_SUCCESS'; payload: { flashcards: Flashcard[]; quizItems: QuizItem[]; totalCount: number } }
|
||||||
| { type: 'LOAD_FLASHCARDS_ERROR'; payload: { error: string } }
|
| { type: 'LOAD_FLASHCARDS_ERROR'; payload: { error: string } }
|
||||||
| { type: 'WORD_SUBMIT_START'; payload: { cardId: string } }
|
| { type: 'WORD_SUBMIT_START'; payload: { cardId: string } }
|
||||||
| { type: 'WORD_SUBMIT_SUCCESS'; payload: { cardId: string; nextReviewDate: string } }
|
| { type: 'WORD_SUBMIT_SUCCESS'; payload: { cardId: string; nextReviewDate: string } }
|
||||||
|
|
@ -165,7 +166,8 @@ const reviewReducer = (state: ReviewState, action: ReviewAction): ReviewState =>
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
error: null,
|
error: null,
|
||||||
flashcards: action.payload.flashcards,
|
flashcards: action.payload.flashcards,
|
||||||
quizItems: action.payload.quizItems
|
quizItems: action.payload.quizItems,
|
||||||
|
totalFlashcardsCount: action.payload.totalCount
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'LOAD_FLASHCARDS_ERROR':
|
case 'LOAD_FLASHCARDS_ERROR':
|
||||||
|
|
@ -301,10 +303,11 @@ export function useReviewSession() {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
error: null,
|
error: null,
|
||||||
pendingWordSubmission: null,
|
pendingWordSubmission: null,
|
||||||
submittingWords: new Set<string>()
|
submittingWords: new Set<string>(),
|
||||||
|
totalFlashcardsCount: null
|
||||||
})
|
})
|
||||||
|
|
||||||
const { quizItems, score, isComplete, flashcards, isLoading, error, pendingWordSubmission, submittingWords } = state
|
const { quizItems, score, isComplete, flashcards, isLoading, error, pendingWordSubmission, submittingWords, totalFlashcardsCount } = state
|
||||||
|
|
||||||
// 智能排序獲取當前測驗項目 - 使用 useMemo 優化性能
|
// 智能排序獲取當前測驗項目 - 使用 useMemo 優化性能
|
||||||
const sortedQuizItems = useMemo(() => sortQuizItemsByPriority(quizItems), [quizItems])
|
const sortedQuizItems = useMemo(() => sortQuizItemsByPriority(quizItems), [quizItems])
|
||||||
|
|
@ -321,29 +324,95 @@ export function useReviewSession() {
|
||||||
dispatch({ type: 'LOAD_FLASHCARDS_START' })
|
dispatch({ type: 'LOAD_FLASHCARDS_START' })
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await flashcardsService.getDueFlashcards(10)
|
console.log('🚀 開始載入詞卡資料...')
|
||||||
|
|
||||||
if (response.success && response.data) {
|
// 同時獲取待複習詞卡和總詞卡數
|
||||||
const flashcards = response.data
|
const [dueResponse, totalResponse] = await Promise.all([
|
||||||
const quizItems = generateQuizItemsFromFlashcards(flashcards)
|
flashcardsService.getDueFlashcards(10),
|
||||||
|
flashcardsService.getFlashcards() // 獲取所有詞卡來計算總數
|
||||||
|
])
|
||||||
|
|
||||||
|
console.log('📥 API 回應:', {
|
||||||
|
dueResponse: {
|
||||||
|
success: dueResponse.success,
|
||||||
|
dataType: typeof dueResponse.data,
|
||||||
|
dataLength: Array.isArray(dueResponse.data) ? dueResponse.data.length : 'not array',
|
||||||
|
error: dueResponse.error
|
||||||
|
},
|
||||||
|
totalResponse: {
|
||||||
|
success: totalResponse.success,
|
||||||
|
dataType: typeof totalResponse.data,
|
||||||
|
dataLength: Array.isArray(totalResponse.data) ? totalResponse.data.length : 'not array',
|
||||||
|
error: totalResponse.error
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (dueResponse.success && totalResponse.success) {
|
||||||
|
// 正確的資料結構處理
|
||||||
|
const dueFlashcards = dueResponse.data || [] // getDueFlashcards 返回 Flashcard[]
|
||||||
|
const totalFlashcardsData = totalResponse.data || { flashcards: [], count: 0 } // getFlashcards 返回 { flashcards: [], count: number }
|
||||||
|
const totalFlashcards = totalFlashcardsData.flashcards || []
|
||||||
|
const totalCount = totalFlashcardsData.count || totalFlashcards.length
|
||||||
|
|
||||||
|
const quizItems = generateQuizItemsFromFlashcards(dueFlashcards)
|
||||||
|
|
||||||
|
console.log('📊 資料統計:', {
|
||||||
|
dueFlashcards: dueFlashcards.length,
|
||||||
|
totalFlashcards: totalFlashcards.length,
|
||||||
|
totalCount: totalCount,
|
||||||
|
quizItems: quizItems.length
|
||||||
|
})
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'LOAD_FLASHCARDS_SUCCESS',
|
type: 'LOAD_FLASHCARDS_SUCCESS',
|
||||||
payload: { flashcards, quizItems }
|
payload: {
|
||||||
|
flashcards: dueFlashcards,
|
||||||
|
quizItems,
|
||||||
|
totalCount: totalCount
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log('✅ 成功載入', flashcards.length, '張詞卡')
|
console.log('✅ 成功載入', dueFlashcards.length, '張待複習詞卡 / 總共', totalCount, '張詞卡')
|
||||||
console.log('🎯 生成', quizItems.length, '個測驗項目')
|
console.log('🎯 生成', quizItems.length, '個測驗項目')
|
||||||
} else {
|
} else {
|
||||||
|
console.error('❌ API 調用失敗:', {
|
||||||
|
dueError: dueResponse.error,
|
||||||
|
totalError: totalResponse.error,
|
||||||
|
dueSuccess: dueResponse.success,
|
||||||
|
totalSuccess: totalResponse.success
|
||||||
|
})
|
||||||
|
|
||||||
|
// 檢查是否為認證錯誤
|
||||||
|
const authError = dueResponse.error?.includes('登入已過期') ||
|
||||||
|
totalResponse.error?.includes('登入已過期') ||
|
||||||
|
dueResponse.error?.includes('認證') ||
|
||||||
|
totalResponse.error?.includes('認證')
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'LOAD_FLASHCARDS_ERROR',
|
type: 'LOAD_FLASHCARDS_ERROR',
|
||||||
payload: { error: response.error || '載入詞卡失敗' }
|
payload: {
|
||||||
|
error: authError ?
|
||||||
|
'登入已過期,請重新登入' :
|
||||||
|
(dueResponse.error || totalResponse.error || '載入詞卡失敗')
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('❌ API 調用異常:', error)
|
||||||
|
|
||||||
|
// 檢查是否為認證錯誤
|
||||||
|
const errorMessage = error instanceof Error ? error.message : '載入詞卡失敗'
|
||||||
|
const isAuthError = errorMessage.includes('登入已過期') ||
|
||||||
|
errorMessage.includes('認證') ||
|
||||||
|
errorMessage.includes('Unauthorized')
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'LOAD_FLASHCARDS_ERROR',
|
type: 'LOAD_FLASHCARDS_ERROR',
|
||||||
payload: { error: error instanceof Error ? error.message : '載入詞卡失敗' }
|
payload: {
|
||||||
|
error: isAuthError ?
|
||||||
|
'登入已過期,請重新登入' :
|
||||||
|
errorMessage
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -467,6 +536,7 @@ export function useReviewSession() {
|
||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
flashcards,
|
flashcards,
|
||||||
|
totalFlashcardsCount,
|
||||||
|
|
||||||
// 計算屬性
|
// 計算屬性
|
||||||
totalQuizItems: quizItems.length,
|
totalQuizItems: quizItems.length,
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,12 @@ export class ApiClient {
|
||||||
const apiError = await createApiErrorFromResponse(response, context)
|
const apiError = await createApiErrorFromResponse(response, context)
|
||||||
lastError = apiError
|
lastError = apiError
|
||||||
|
|
||||||
|
// 處理認證錯誤 - 自動清除過期token
|
||||||
|
if (response.status === 401) {
|
||||||
|
localStorage.removeItem('auth_token')
|
||||||
|
console.log('🔒 認證過期,已清除token')
|
||||||
|
}
|
||||||
|
|
||||||
// 如果不可重試,直接返回錯誤
|
// 如果不可重試,直接返回錯誤
|
||||||
if (attempt === retries || !this.isRetryableStatus(response.status)) {
|
if (attempt === retries || !this.isRetryableStatus(response.status)) {
|
||||||
return createErrorResponse(apiError)
|
return createErrorResponse(apiError)
|
||||||
|
|
@ -138,12 +144,11 @@ export class ApiClient {
|
||||||
private getDefaultHeaders(): Record<string, string> {
|
private getDefaultHeaders(): Record<string, string> {
|
||||||
const headers: Record<string, string> = {}
|
const headers: Record<string, string> = {}
|
||||||
|
|
||||||
// 開發階段:不添加認證Token
|
// 添加認證Token(如果存在)
|
||||||
// TODO: 生產環境需要添加認證邏輯
|
const token = localStorage.getItem('auth_token')
|
||||||
// const token = localStorage.getItem('auth_token')
|
if (token) {
|
||||||
// if (token) {
|
headers.Authorization = `Bearer ${token}`
|
||||||
// headers.Authorization = `Bearer ${token}`
|
}
|
||||||
// }
|
|
||||||
|
|
||||||
return headers
|
return headers
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue