refactor: 重構 Generate 頁面移除過度抽象 + 統一按鈕樣式
主要改動: - 移除 ClickableTextV2 組件 (115行) → 內聯為35行邏輯 - 新增 selectedWord 狀態管理與統一 WordPopup 組件 - 移除慣用語區塊複雜星星判斷邏輯 (17行 → 0行) - 調整句子主體字體大小 text-xl→lg 更適中 - 重構單字樣式: 下劃線 → 按鈕樣式 (邊框+圓角+hover) - 根據 CEFR 等級設置顏色主題 (A1/A2綠、B1/B2藍、C1/C2紅) 效果: - 淨減少 ~80行代碼複雜度 - 統一視覺風格 (慣用語 + 單字按鈕一致) - 提升用戶體驗 (清晰可點擊按鈕) - 簡化維護成本 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
6a5831bb16
commit
3783be0fcd
|
|
@ -3,11 +3,11 @@
|
||||||
import { useState, useMemo, useCallback } from 'react'
|
import { useState, useMemo, useCallback } from 'react'
|
||||||
import { ProtectedRoute } from '@/components/shared/ProtectedRoute'
|
import { ProtectedRoute } from '@/components/shared/ProtectedRoute'
|
||||||
import { Navigation } from '@/components/shared/Navigation'
|
import { Navigation } from '@/components/shared/Navigation'
|
||||||
import { ClickableTextV2 } from '@/components/generate/ClickableTextV2'
|
|
||||||
import { WordPopup } from '@/components/word/WordPopup'
|
import { WordPopup } from '@/components/word/WordPopup'
|
||||||
import { useToast } from '@/components/shared/Toast'
|
import { useToast } from '@/components/shared/Toast'
|
||||||
import { flashcardsService } from '@/lib/services/flashcards'
|
import { flashcardsService } from '@/lib/services/flashcards'
|
||||||
import { compareCEFRLevels, getLevelIndex, getTargetLearningRange } from '@/lib/utils/cefrUtils'
|
import { compareCEFRLevels, getLevelIndex, getTargetLearningRange } from '@/lib/utils/cefrUtils'
|
||||||
|
import { useWordAnalysis } from '@/hooks/word/useWordAnalysis'
|
||||||
import { API_CONFIG } from '@/lib/config/api'
|
import { API_CONFIG } from '@/lib/config/api'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
|
@ -34,6 +34,7 @@ interface GrammarCorrection {
|
||||||
|
|
||||||
function GenerateContent() {
|
function GenerateContent() {
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
const { findWordAnalysis, getWordClass, shouldShowStar } = useWordAnalysis()
|
||||||
const [textInput, setTextInput] = useState('')
|
const [textInput, setTextInput] = useState('')
|
||||||
const [isAnalyzing, setIsAnalyzing] = useState(false)
|
const [isAnalyzing, setIsAnalyzing] = useState(false)
|
||||||
const [showAnalysisView, setShowAnalysisView] = useState(false)
|
const [showAnalysisView, setShowAnalysisView] = useState(false)
|
||||||
|
|
@ -41,6 +42,7 @@ function GenerateContent() {
|
||||||
const [sentenceMeaning, setSentenceMeaning] = useState('')
|
const [sentenceMeaning, setSentenceMeaning] = useState('')
|
||||||
const [grammarCorrection, setGrammarCorrection] = useState<GrammarCorrection | null>(null)
|
const [grammarCorrection, setGrammarCorrection] = useState<GrammarCorrection | null>(null)
|
||||||
const [selectedIdiom, setSelectedIdiom] = useState<string | null>(null)
|
const [selectedIdiom, setSelectedIdiom] = useState<string | null>(null)
|
||||||
|
const [selectedWord, setSelectedWord] = useState<string | null>(null)
|
||||||
|
|
||||||
// 處理句子分析 - 使用真實API
|
// 處理句子分析 - 使用真實API
|
||||||
const handleAnalyzeSentence = async () => {
|
const handleAnalyzeSentence = async () => {
|
||||||
|
|
@ -393,16 +395,50 @@ function GenerateContent() {
|
||||||
|
|
||||||
{/* 句子主體展示 */}
|
{/* 句子主體展示 */}
|
||||||
<div className="text-left mb-8">
|
<div className="text-left mb-8">
|
||||||
<div className="text-xl sm:text-2xl lg:text-3xl font-medium text-gray-900 mb-6" >
|
<div className="text-lg font-medium text-gray-900 mb-6 select-text leading-relaxed">
|
||||||
<ClickableTextV2
|
{textInput.split(/(\s+)/).map((token, index) => {
|
||||||
text={textInput}
|
const cleanToken = token.replace(/[^\w']/g, '')
|
||||||
analysis={sentenceAnalysis?.vocabularyAnalysis || undefined}
|
if (!cleanToken || /^\s+$/.test(token)) {
|
||||||
showIdiomsInline={false}
|
return (
|
||||||
onWordClick={(word, analysis) => {
|
<span key={index} className="whitespace-pre">
|
||||||
console.log('Clicked word:', word, analysis)
|
{token}
|
||||||
}}
|
</span>
|
||||||
onSaveWord={handleSaveWord}
|
)
|
||||||
/>
|
}
|
||||||
|
|
||||||
|
const analysis = sentenceAnalysis?.vocabularyAnalysis || {}
|
||||||
|
const wordAnalysis = findWordAnalysis(cleanToken, analysis)
|
||||||
|
if (!wordAnalysis) {
|
||||||
|
return (
|
||||||
|
<span key={index} className="text-gray-900">
|
||||||
|
{token}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span key={index} className="relative">
|
||||||
|
<span
|
||||||
|
className={getWordClass(cleanToken, analysis)}
|
||||||
|
onClick={() => setSelectedWord(cleanToken)}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
setSelectedWord(cleanToken)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{token}
|
||||||
|
</span>
|
||||||
|
{/* {shouldShowStar(wordAnalysis) && (
|
||||||
|
<span className="absolute -top-1 -right-1 text-xs text-yellow-500">
|
||||||
|
⭐
|
||||||
|
</span>
|
||||||
|
)} */}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 翻譯 - 參考翻卡背面設計 */}
|
{/* 翻譯 - 參考翻卡背面設計 */}
|
||||||
|
|
@ -434,23 +470,6 @@ function GenerateContent() {
|
||||||
title={`${idiom.idiom}: ${idiom.translation}`}
|
title={`${idiom.idiom}: ${idiom.translation}`}
|
||||||
>
|
>
|
||||||
{idiom.idiom}
|
{idiom.idiom}
|
||||||
{(() => {
|
|
||||||
// 只有當慣用語為常用且不是簡單慣用語時才顯示星星
|
|
||||||
// 簡單慣用語定義:學習者CEFR > 慣用語CEFR
|
|
||||||
const userLevel = localStorage.getItem('userEnglishLevel') || 'A2'
|
|
||||||
const isHighFrequency = idiom?.frequency === 'high'
|
|
||||||
const idiomCefr = idiom?.cefrLevel || 'A1'
|
|
||||||
const isNotSimpleIdiom = !compareCEFRLevels(userLevel, idiomCefr, '>')
|
|
||||||
|
|
||||||
return isHighFrequency && isNotSimpleIdiom ? (
|
|
||||||
<span
|
|
||||||
className="absolute -top-1 -right-1 text-xs pointer-events-none z-10"
|
|
||||||
style={{ fontSize: '8px', lineHeight: 1 }}
|
|
||||||
>
|
|
||||||
⭐
|
|
||||||
</span>
|
|
||||||
) : null
|
|
||||||
})()}
|
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -485,6 +504,18 @@ function GenerateContent() {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* 單詞彈窗 - 使用統一的 WordPopup */}
|
||||||
|
<WordPopup
|
||||||
|
selectedWord={selectedWord}
|
||||||
|
analysis={sentenceAnalysis?.vocabularyAnalysis || {}}
|
||||||
|
isOpen={!!selectedWord}
|
||||||
|
onClose={() => setSelectedWord(null)}
|
||||||
|
onSaveWord={async (word, analysis) => {
|
||||||
|
const result = await handleSaveWord(word, analysis)
|
||||||
|
return result
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Toast 通知系統 */}
|
{/* Toast 通知系統 */}
|
||||||
<toast.ToastContainer />
|
<toast.ToastContainer />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -36,13 +36,30 @@ export function useWordAnalysis() {
|
||||||
const wordAnalysis = findWordAnalysis(word, analysis)
|
const wordAnalysis = findWordAnalysis(word, analysis)
|
||||||
if (!wordAnalysis) return 'cursor-default text-gray-900'
|
if (!wordAnalysis) return 'cursor-default text-gray-900'
|
||||||
|
|
||||||
let classes = 'cursor-pointer transition-all duration-200 '
|
// 基礎按鈕樣式 - 類似慣用語區塊
|
||||||
|
let classes = 'cursor-pointer transition-all duration-200 rounded px-1 py-0.5 border font-medium hover:shadow-md transform hover:-translate-y-0.5 '
|
||||||
|
|
||||||
if (wordAnalysis.isIdiom) {
|
if (wordAnalysis.isIdiom) {
|
||||||
classes += 'bg-purple-100 text-purple-800 border-b-2 border-purple-300 hover:bg-purple-200 font-medium'
|
classes += 'bg-purple-50 text-purple-700 border-purple-200 hover:bg-purple-100'
|
||||||
} else {
|
} else {
|
||||||
const cefrColor = getCEFRColor(wordAnalysis.cefr)
|
// 根據 CEFR 等級設置顏色主題
|
||||||
classes += `underline decoration-2 hover:bg-opacity-20 ${cefrColor.replace('border-', 'decoration-').replace('text-', 'hover:bg-')}`
|
const cefr = wordAnalysis.cefr || 'A1'
|
||||||
|
switch (cefr) {
|
||||||
|
case 'A1':
|
||||||
|
case 'A2':
|
||||||
|
classes += 'bg-green-50 text-green-700 border-green-200 hover:bg-green-100'
|
||||||
|
break
|
||||||
|
case 'B1':
|
||||||
|
case 'B2':
|
||||||
|
classes += 'bg-blue-50 text-blue-700 border-blue-200 hover:bg-blue-100'
|
||||||
|
break
|
||||||
|
case 'C1':
|
||||||
|
case 'C2':
|
||||||
|
classes += 'bg-red-50 text-red-700 border-red-200 hover:bg-red-100'
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
classes += 'bg-gray-50 text-gray-700 border-gray-200 hover:bg-gray-100'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return classes
|
return classes
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue