From 4c7696f80b8c81532096b5fc61a4d7236a753f1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=84=AD=E6=B2=9B=E8=BB=92?= Date: Tue, 7 Oct 2025 07:18:35 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=87=8D=E6=96=B0=E8=A8=AD=E8=A8=88?= =?UTF-8?q?=E5=80=8B=E4=BA=BA=E6=AA=94=E6=A1=88=E9=A0=81=E9=9D=A2=E4=B8=A6?= =?UTF-8?q?=E6=95=B4=E5=90=88=E8=A8=AD=E5=AE=9A=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 建立全新分頁式個人檔案頁面(👤個人資料 ⚙️學習設定 🎯英語程度) - 整合原有 settings 功能到 profile 頁面的分頁中 - 重新設計導航列:移除設定連結,個人檔案放在右上角用戶區域 - 改進響應式設計:桌面和手機版都有清晰的個人檔案入口 - 簡化 settings 頁面為重新導向頁面,統一用戶體驗 - 修復前端條件判斷邏輯,改善空狀態畫面顯示 新設計更簡潔易用,符合標準 UI 模式。 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- frontend/app/profile/page.tsx | 700 ++++++++++++++++++++++ frontend/app/review/page.tsx | 2 +- frontend/app/settings/page.tsx | 209 +------ frontend/components/shared/Navigation.tsx | 30 +- 4 files changed, 729 insertions(+), 212 deletions(-) create mode 100644 frontend/app/profile/page.tsx diff --git a/frontend/app/profile/page.tsx b/frontend/app/profile/page.tsx new file mode 100644 index 0000000..af34c71 --- /dev/null +++ b/frontend/app/profile/page.tsx @@ -0,0 +1,700 @@ +'use client' + +import { useState, useEffect } from 'react' +import { Navigation } from '@/components/shared/Navigation' + +interface UserProfile { + id: string + email: string + displayName: string | null + avatarUrl: string | null + subscriptionType: string + createdAt: string +} + +interface UserSettings { + dailyGoal: number + reminderTime: string + reminderEnabled: boolean + difficultyPreference: string + autoPlayAudio: boolean + showPronunciation: boolean +} + +interface LanguageLevel { + value: string + label: string + description: string + examples: string[] +} + +type TabType = 'profile' | 'settings' | 'level' + +export default function ProfilePage() { + const [activeTab, setActiveTab] = useState('profile') + const [profile, setProfile] = useState(null) + const [settings, setSettings] = useState(null) + const [userLevel, setUserLevel] = useState('A2') + const [isLoading, setIsLoading] = useState(true) + const [error, setError] = useState(null) + const [isSaving, setIsSaving] = useState(false) + + // 編輯表單狀態 + const [editForm, setEditForm] = useState({ + displayName: '', + dailyGoal: 20, + reminderEnabled: true, + difficultyPreference: 'balanced', + autoPlayAudio: true, + showPronunciation: true + }) + + // 英語程度定義 + const levels: LanguageLevel[] = [ + { + value: 'A1', + label: 'A1 - 初學者', + description: '能理解基本詞彙和簡單句子', + examples: ['hello', 'good', 'house', 'eat', 'happy'] + }, + { + value: 'A2', + label: 'A2 - 基礎', + description: '能處理日常對話和常見主題', + examples: ['important', 'difficult', 'interesting', 'beautiful', 'understand'] + }, + { + value: 'B1', + label: 'B1 - 中級', + description: '能理解清楚標準語言的要點', + examples: ['analyze', 'opportunity', 'environment', 'responsibility', 'development'] + }, + { + value: 'B2', + label: 'B2 - 中高級', + description: '能理解複雜文本的主要內容', + examples: ['sophisticated', 'implications', 'comprehensive', 'substantial', 'methodology'] + }, + { + value: 'C1', + label: 'C1 - 高級', + description: '能流利表達,理解含蓄意思', + examples: ['meticulous', 'predominantly', 'intricate', 'corroborate', 'paradigm'] + }, + { + value: 'C2', + label: 'C2 - 精通', + description: '接近母語水平', + examples: ['ubiquitous', 'ephemeral', 'perspicacious', 'multifarious', 'idiosyncratic'] + } + ] + + const tabs = [ + { id: 'profile' as TabType, label: '個人資料', icon: '👤' }, + { id: 'settings' as TabType, label: '學習設定', icon: '⚙️' }, + { id: 'level' as TabType, label: '英語程度', icon: '🎯' } + ] + + // 載入資料 + useEffect(() => { + loadData() + }, []) + + const loadData = async () => { + setIsLoading(true) + setError(null) + + try { + const token = localStorage.getItem('auth_token') + if (!token) { + setError('請先登入') + return + } + + console.log('🚀 載入個人檔案資料...') + + // 並行載入所有資料 + const [profileResponse, settingsResponse] = await Promise.all([ + fetch('http://localhost:5000/api/auth/profile', { + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + } + }), + fetch('http://localhost:5000/api/auth/settings', { + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + } + }) + ]) + + if (!profileResponse.ok || !settingsResponse.ok) { + if (profileResponse.status === 401 || settingsResponse.status === 401) { + localStorage.removeItem('auth_token') + setError('登入已過期,請重新登入') + } else { + setError('載入個人資料失敗') + } + return + } + + const [profileData, settingsData] = await Promise.all([ + profileResponse.json(), + settingsResponse.json() + ]) + + if (profileData.success && settingsData.success) { + setProfile(profileData.data) + setSettings(settingsData.data) + + // 初始化編輯表單 + setEditForm({ + displayName: profileData.data.displayName || '', + dailyGoal: settingsData.data.dailyGoal || 20, + reminderEnabled: settingsData.data.reminderEnabled ?? true, + difficultyPreference: settingsData.data.difficultyPreference || 'balanced', + autoPlayAudio: settingsData.data.autoPlayAudio ?? true, + showPronunciation: settingsData.data.showPronunciation ?? true + }) + + console.log('✅ 個人檔案資料載入成功') + } else { + setError('載入資料失敗') + } + + // 載入英語程度 + const savedLevel = localStorage.getItem('userEnglishLevel') + if (savedLevel) { + setUserLevel(savedLevel) + } + } catch (error) { + console.error('載入個人資料錯誤:', error) + setError(error instanceof Error ? error.message : '載入失敗') + } finally { + setIsLoading(false) + } + } + + const handleSaveProfile = async () => { + if (!profile) return + + setIsSaving(true) + try { + const token = localStorage.getItem('auth_token') + if (!token) { + setError('請先登入') + return + } + + const response = await fetch('http://localhost:5000/api/auth/profile', { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify({ displayName: editForm.displayName }) + }) + + if (response.ok) { + await loadData() // 重新載入資料 + console.log('✅ 個人資料更新成功') + } else { + setError('儲存失敗') + } + } catch (error) { + setError('儲存失敗') + } finally { + setIsSaving(false) + } + } + + const handleSaveSettings = async () => { + setIsSaving(true) + try { + const token = localStorage.getItem('auth_token') + if (!token) { + setError('請先登入') + return + } + + const response = await fetch('http://localhost:5000/api/auth/settings', { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify({ + dailyGoal: editForm.dailyGoal, + reminderEnabled: editForm.reminderEnabled, + difficultyPreference: editForm.difficultyPreference, + autoPlayAudio: editForm.autoPlayAudio, + showPronunciation: editForm.showPronunciation + }) + }) + + if (response.ok) { + await loadData() // 重新載入資料 + console.log('✅ 學習設定更新成功') + } else { + setError('儲存失敗') + } + } catch (error) { + setError('儲存失敗') + } finally { + setIsSaving(false) + } + } + + const handleSaveLevel = () => { + localStorage.setItem('userEnglishLevel', userLevel) + console.log('✅ 英語程度已保存:', userLevel) + } + + const handleLogout = () => { + localStorage.removeItem('auth_token') + localStorage.removeItem('userEnglishLevel') + window.location.href = '/login' + } + + // 載入狀態 + if (isLoading) { + return ( +
+ +
+
+
+
+

載入個人資料中...

+
+
+
+
+ ) + } + + // 錯誤狀態 + if (error) { + const isAuthError = error.includes('登入') || error.includes('認證') + + return ( +
+ +
+
+
+
+ {isAuthError ? '🔒' : '⚠️'} +
+

+ {isAuthError ? '需要重新登入' : '載入失敗'} +

+

{error}

+ +
+ {isAuthError ? ( + + ) : ( + + )} + + +
+
+
+
+
+ ) + } + + if (!profile || !settings) { + return null + } + + return ( +
+ + +
+
+ {/* 頁面標題 */} +
+

個人檔案

+

管理您的帳戶資訊和學習偏好設定

+
+ + {/* 分頁導航 */} +
+
+ +
+
+ + {/* 分頁內容 */} +
+ {/* 個人資料分頁 */} + {activeTab === 'profile' && ( +
+
+ {/* 用戶資訊卡片 */} +
+
+ {profile.avatarUrl ? ( + 用戶頭像 + ) : ( + + {(profile.displayName || profile.email)[0].toUpperCase()} + + )} +
+ +

+ {profile.displayName || '用戶'} +

+

{profile.email}

+ + + {profile.subscriptionType === 'premium' ? '🌟 Premium' : '🆓 免費版'} + +
+ + {/* 編輯資料 */} +
+
+ +
+ setEditForm(prev => ({ ...prev, displayName: e.target.value }))} + className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" + placeholder="請輸入顯示名稱" + /> + +
+
+ + {/* 帳戶資訊 */} +
+

帳戶資訊

+
+
+ 用戶 ID +

{profile.id}

+
+
+ 加入時間 +

+ {new Date(profile.createdAt).toLocaleDateString('zh-TW')} +

+
+
+
+ + {/* 登出 */} +
+ +
+
+
+
+ )} + + {/* 學習設定分頁 */} + {activeTab === 'settings' && ( +
+
+
+ {/* 每日目標 */} +
+ +
+
+ {editForm.dailyGoal} + 張詞卡/天 +
+ setEditForm(prev => ({ ...prev, dailyGoal: Number(e.target.value) }))} + className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer slider" + /> +
+ 1 + 50 + 100 +
+
+
+ + {/* 學習偏好 */} +
+ +
+ {[ + { value: 'conservative', label: '保守', desc: '較簡單', color: 'green' }, + { value: 'balanced', label: '均衡', desc: '適中', color: 'blue' }, + { value: 'aggressive', label: '積極', desc: '較難', color: 'purple' } + ].map(option => ( + + ))} +
+
+ + {/* 音頻和顯示設定 */} +
+

音頻和顯示設定

+ + {[ + { + key: 'reminderEnabled' as keyof typeof editForm, + label: '複習提醒', + desc: '接收學習提醒通知' + }, + { + key: 'autoPlayAudio' as keyof typeof editForm, + label: '自動播放音頻', + desc: '顯示詞卡時自動播放發音' + }, + { + key: 'showPronunciation' as keyof typeof editForm, + label: '顯示發音標示', + desc: '在詞卡上顯示音標' + } + ].map(setting => ( +
+
+ {setting.label} +

{setting.desc}

+
+ +
+ ))} +
+ + {/* 儲存按鈕 */} +
+ +
+
+
+
+ )} + + {/* 英語程度分頁 */} + {activeTab === 'level' && ( +
+
+
+

🎯 英語程度設定

+

+ 選擇您的英語程度,系統將為您提供個人化的詞彙學習建議 +

+
+ + {/* 程度選擇 */} +
+ {levels.map(level => ( + + ))} +
+ + {/* 學習效果預覽 */} +
+

+ 💡 您的個人化學習效果 +

+
+
+

重點學習範圍

+

+ 系統將為您標記比目前程度({userLevel})高1-2級的重點詞彙 +

+
+
+

學習建議

+

+ 專注學習具有挑戰性但不會太困難的詞彙,提升學習效率 +

+
+
+
+ + {/* 儲存按鈕 */} + +
+
+ )} +
+ + {/* 快速操作區 */} +
+

快速操作

+
+ {[ + { href: '/review', icon: '📚', label: '開始複習', desc: '複習詞卡' }, + { href: '/generate', icon: '➕', label: '新增詞卡', desc: '建立內容' }, + { href: '/flashcards', icon: '📋', label: '管理詞卡', desc: '編輯詞卡' }, + { href: '/stats', icon: '📊', label: '學習統計', desc: '查看進度' } + ].map(action => ( + + ))} +
+
+
+
+
+ ) +} \ No newline at end of file diff --git a/frontend/app/review/page.tsx b/frontend/app/review/page.tsx index a1fd53a..2fea1e8 100644 --- a/frontend/app/review/page.tsx +++ b/frontend/app/review/page.tsx @@ -322,7 +322,7 @@ export default function ReviewPage() { // 主要線性測驗頁面 // 只有在有可用測驗項目時才顯示測驗界面 if (!isLoading && !error && totalFlashcardsCount !== null && totalFlashcardsCount > 0 && flashcards.length > 0 && currentQuizItem && currentCard) { - return ( + return (
diff --git a/frontend/app/settings/page.tsx b/frontend/app/settings/page.tsx index 4b06d6b..675fad1 100644 --- a/frontend/app/settings/page.tsx +++ b/frontend/app/settings/page.tsx @@ -1,209 +1,18 @@ 'use client' -import { useState, useEffect } from 'react' - -interface LanguageLevel { - value: string; - label: string; - description: string; - examples: string[]; -} +import { useEffect } from 'react' export default function SettingsPage() { - const [userLevel, setUserLevel] = useState('A2'); - const [isLoading, setIsLoading] = useState(false); - - const levels: LanguageLevel[] = [ - { - value: 'A1', - label: 'A1 - 初學者', - description: '能理解基本詞彙和簡單句子', - examples: ['hello', 'good', 'house', 'eat', 'happy'] - }, - { - value: 'A2', - label: 'A2 - 基礎', - description: '能處理日常對話和常見主題', - examples: ['important', 'difficult', 'interesting', 'beautiful', 'understand'] - }, - { - value: 'B1', - label: 'B1 - 中級', - description: '能理解清楚標準語言的要點', - examples: ['analyze', 'opportunity', 'environment', 'responsibility', 'development'] - }, - { - value: 'B2', - label: 'B2 - 中高級', - description: '能理解複雜文本的主要內容', - examples: ['sophisticated', 'implications', 'comprehensive', 'substantial', 'methodology'] - }, - { - value: 'C1', - label: 'C1 - 高級', - description: '能流利表達,理解含蓄意思', - examples: ['meticulous', 'predominantly', 'intricate', 'corroborate', 'paradigm'] - }, - { - value: 'C2', - label: 'C2 - 精通', - description: '接近母語水平', - examples: ['ubiquitous', 'ephemeral', 'perspicacious', 'multifarious', 'idiosyncratic'] - } - ]; - - // 載入用戶已設定的程度 + // 自動重新導向到個人檔案頁面 useEffect(() => { - const savedLevel = localStorage.getItem('userEnglishLevel'); - if (savedLevel) { - setUserLevel(savedLevel); - } - }, []); - - const saveUserLevel = async () => { - setIsLoading(true); - try { - // 保存到本地存儲 - localStorage.setItem('userEnglishLevel', userLevel); - - // TODO: 如果用戶已登入,也保存到伺服器 - // const token = localStorage.getItem('authToken'); - // if (token) { - // await fetch('/api/user/update-level', { - // method: 'POST', - // headers: { - // 'Content-Type': 'application/json', - // 'Authorization': `Bearer ${token}` - // }, - // body: JSON.stringify({ englishLevel: userLevel }) - // }); - // } - - alert('✅ 程度設定已保存!系統將為您提供個人化的詞彙標記。'); - } catch (error) { - console.error('Error saving user level:', error); - alert('❌ 保存失敗,請稍後再試'); - } finally { - setIsLoading(false); - } - }; - - const getHighValueRange = (level: string) => { - const ranges = { - 'A1': 'A2-B1', - 'A2': 'B1-B2', - 'B1': 'B2-C1', - 'B2': 'C1-C2', - 'C1': 'C2', - 'C2': 'C2' - }; - return ranges[level as keyof typeof ranges] || 'B1-B2'; - }; + window.location.href = '/profile' + }, []) return ( -
-
-

🎯 英語程度設定

-

- 設定您的英語程度,系統將為您提供個人化的詞彙學習建議。 - 設定後,我們會重點標記比您目前程度高1-2級的詞彙。 -

-
- -
- {levels.map(level => ( - - ))} -
- - {/* 個人化效果預覽 */} -
-

- 💡 您的個人化學習效果預覽 -

-
-
-

重點學習範圍

-

- 系統將為您標記 {getHighValueRange(userLevel)} 等級的重點學習詞彙 -

-
-
-

學習建議

-

- 專注於比您目前程度({userLevel})高1-2級的詞彙,既有挑戰性又不會太困難 -

-
-
-
- - - -
-

- 💡 提示: 您隨時可以回到這裡調整程度設定 -

+
+
+
+

正在跳轉到個人檔案頁面...

- ); + ) } \ No newline at end of file diff --git a/frontend/components/shared/Navigation.tsx b/frontend/components/shared/Navigation.tsx index 101fa03..da93262 100644 --- a/frontend/components/shared/Navigation.tsx +++ b/frontend/components/shared/Navigation.tsx @@ -19,8 +19,7 @@ export function Navigation({ showExitLearning = false, onExitLearning }: Navigat { href: '/dashboard', label: '儀表板' }, { href: '/flashcards', label: '詞卡' }, { href: '/review', label: '複習' }, - { href: '/generate', label: 'AI 生成' }, - { href: '/settings', label: '⚙️ 設定' } + { href: '/generate', label: 'AI 生成' } ] return ( @@ -82,14 +81,19 @@ export function Navigation({ showExitLearning = false, onExitLearning }: Navigat {/* 用戶資訊 - 只在桌面版顯示 */} -
-
- {user?.username?.[0]?.toUpperCase() || 'U'} -
- {user?.displayName || user?.username} +
+ +
+ {user?.username?.[0]?.toUpperCase() || 'U'} +
+ {user?.displayName || user?.username} + @@ -120,12 +124,16 @@ export function Navigation({ showExitLearning = false, onExitLearning }: Navigat {/* 手機版用戶區域 */}
-
+ setIsMobileMenuOpen(false)} + className="flex items-center space-x-3 px-3 py-2 hover:bg-gray-100 rounded-lg transition-colors" + >
{user?.username?.[0]?.toUpperCase() || 'U'}
- {user?.displayName || user?.username} -
+ 👤 個人檔案 +