+
+
+
+ {/* 頁面標題 */}
+
+
詞彙明細版型展示
+
6種不同風格的詞彙彈窗設計
+
+ {/* 測試詞彙 */}
+
+ 測試詞彙:
+
+
+
+
+ {/* 版型選擇器 */}
+
+
+ {designs.map((design) => (
+
+ ))}
+
+
+
+ {/* 版型預覽 */}
+
+ {/* 左側:版型說明 */}
+
+
+ {designs.find(d => d.id === selectedDesign)?.name}
+
+
+ {designs.find(d => d.id === selectedDesign)?.description}
+
+
+
+
+
設計特色
+
+ {getDesignFeatures(selectedDesign).map((feature, idx) => (
+ -
+ •
+ {feature}
+
+ ))}
+
+
+
+
+
適用場景
+
+ {getDesignScenario(selectedDesign)}
+
+
+
+
+
+ {/* 右側:版型預覽 */}
+
+
+
點擊下方按鈕預覽版型效果
+
+
+
+ {/* 模擬背景文字 */}
+
+ This is a sample sentence where you can click on any word to see the elaborate definition and detailed explanation.
+
+
+
+
+
+ {/* 詞彙彈窗 - 根據選擇的設計風格渲染 */}
+ {showPopup && (
+ <>
+ {/* 背景遮罩 */}
+
setShowPopup(false)}
+ />
+
+ {/* 彈窗內容 */}
+
+ {renderVocabPopup(selectedDesign, mockWord, () => setShowPopup(false))}
+
+ >
+ )}
+
+ )
+}
+
+// 渲染不同風格的詞彙彈窗
+function renderVocabPopup(design: string, word: any, onClose: () => void) {
+ const handleSave = () => {
+ alert(`✅ 已將「${word.word}」保存到詞卡!`)
+ onClose()
+ }
+
+ switch (design) {
+ case 'modern':
+ return
+ case 'classic':
+ return
+ case 'minimal':
+ return
+ case 'magazine':
+ return
+ case 'mobile':
+ return
+ case 'learning':
+ return
+ default:
+ return
+ }
+}
+
+// 1. 現代玻璃風格
+function ModernGlassDesign({ word, onClose, onSave }: any) {
+ return (
+
+
+
+
+
+
+
{word.word}
+ {word.isHighValue && ⭐}
+
+
+ {word.pronunciation}
+
+ {word.difficultyLevel}
+
+
+
+
+
+
+
+
翻譯
+
{word.translation}
+
+
+
+
定義
+
{word.definition}
+
+
+
+
同義詞
+
+ {word.synonyms.map((synonym: string, idx: number) => (
+
+ {synonym}
+
+ ))}
+
+
+
+
+
+
+ )
+}
+
+// 2. 經典卡片風格
+function ClassicCardDesign({ word, onClose, onSave }: any) {
+ return (
+
+
+
+
+
{word.word}
+
{word.pronunciation}
+
+
+
+
+
+
+
+
+ {word.partOfSpeech}
+
+
+ {word.difficultyLevel}
+
+ {word.isHighValue && (
+
+ ⭐ 高價值
+
+ )}
+
+
+
+
中文翻譯
+
{word.translation}
+
+
+
+
英文定義
+
{word.definition}
+
+
+
+
同義詞
+
+ {word.synonyms.map((synonym: string, idx: number) => (
+
+ {synonym}
+
+ ))}
+
+
+
+
+
+
+ )
+}
+
+// 3. 極簡風格
+function MinimalDesign({ word, onClose, onSave }: any) {
+ return (
+
+
+
+
{word.word}
+
+
+
{word.pronunciation}
+
+
+
+
+ {word.translation}
+ ({word.partOfSpeech})
+
+
+
{word.definition}
+
+
+ {word.synonyms.slice(0, 3).map((synonym: string, idx: number) => (
+
+ {synonym}
+
+ ))}
+
+
+
+
+
+ )
+}
+
+// 4. 雜誌排版風格
+function MagazineDesign({ word, onClose, onSave }: any) {
+ return (
+
+
+
+
+
+ {word.word}
+
+
+ {word.pronunciation}
+ {word.partOfSpeech}
+ {word.difficultyLevel}
+
+
+
+
+
+
+
+
+
+ Translation
+
+
+ {word.translation}
+
+
+
+
+
Definition
+
+ {word.definition}
+
+
+
+
+
Synonyms
+
+ {word.synonyms.join(', ')}
+
+
+
+
+
+
+ )
+}
+
+// 5. 移動應用風格
+function MobileAppDesign({ word, onClose, onSave }: any) {
+ return (
+
+
+
+
+
+ {word.word[0].toUpperCase()}
+
+
詞彙詳情
+
+
+
+
+
+
+
+
{word.word}
+
{word.pronunciation}
+
+
+ {word.partOfSpeech}
+
+
+ {word.difficultyLevel}
+
+
+
+
+
+
+
🌏
+
+
{word.translation}
+
中文翻譯
+
+
+
+
+
📝
+
+
{word.definition}
+
英文定義
+
+
+
+
+
🔗
+
+
+ {word.synonyms.slice(0, 3).map((synonym: string, idx: number) => (
+
+ {synonym}
+
+ ))}
+
+
同義詞
+
+
+
+
+
+
+
+ )
+}
+
+// 6. 學習卡片風格
+function LearningCardDesign({ word, onClose, onSave }: any) {
+ return (
+
+
+
+
+
+ {word.word[0].toUpperCase()}
+
+
+
{word.word}
+
{word.partOfSpeech}
+
+
+
+
+
+
+
{word.pronunciation}
+
+
+
+
+
+
+
翻譯
+
{word.translation}
+
+
+
+
定義
+
{word.definition}
+
+
+
+
同義詞
+
+ {word.synonyms.map((synonym: string, idx: number) => (
+
+ {synonym}
+
+ ))}
+
+
+
+
+
+
+ )
+}
+
+// 輔助函數
+function getDesignFeatures(design: string): string[] {
+ const features = {
+ modern: [
+ '毛玻璃背景效果',
+ '大尺寸陰影和圓角',
+ '漸層按鈕設計',
+ '微互動動畫',
+ '現代配色方案'
+ ],
+ classic: [
+ '藍色漸層標題欄',
+ '清晰的區塊分隔',
+ '傳統卡片佈局',
+ '顏色編碼標籤',
+ '穩重的設計風格'
+ ],
+ minimal: [
+ '極簡配色方案',
+ '去除多餘裝飾',
+ '重點信息突出',
+ '輕量化設計',
+ '快速瀏覽體驗'
+ ],
+ magazine: [
+ '雜誌式字體',
+ '專業排版設計',
+ '大標題小說明',
+ '黑白主色調',
+ '閱讀導向佈局'
+ ],
+ mobile: [
+ 'iOS/Android風格',
+ '圓角和圖標設計',
+ '列表式信息展示',
+ '觸控友好設計',
+ '移動端優化'
+ ],
+ learning: [
+ '學習功能一致性',
+ '彩色區塊設計',
+ '教育導向佈局',
+ '清晰的信息分類',
+ '學習體驗優化'
+ ]
+ }
+ return features[design as keyof typeof features] || []
+}
+
+function getDesignScenario(design: string): string {
+ const scenarios = {
+ modern: '適合追求現代感和科技感的用戶,特別是年輕用戶群體。設計前衛,視覺效果佳。',
+ classic: '適合喜歡傳統界面的用戶,信息展示清晰,功能分區明確,適合學術或商務場景。',
+ minimal: '適合追求效率的用戶,減少視覺干擾,快速獲取核心信息,適合頻繁使用的場景。',
+ magazine: '適合喜歡閱讀體驗的用戶,類似字典或雜誌的專業排版,適合深度學習。',
+ mobile: '適合手機用戶,觸控友好,符合移動端應用的使用習慣,適合隨時隨地學習。',
+ learning: '與現有學習功能保持一致,用戶體驗連貫,適合在學習流程中使用。'
+ }
+ return scenarios[design as keyof typeof scenarios] || ''
+}
\ No newline at end of file
diff --git a/frontend/components/CardSelectionDialog.tsx b/frontend/components/CardSelectionDialog.tsx
new file mode 100644
index 0000000..cf202a4
--- /dev/null
+++ b/frontend/components/CardSelectionDialog.tsx
@@ -0,0 +1,267 @@
+'use client'
+
+import React, { useState, useMemo } from 'react'
+import { Modal } from './ui/Modal'
+import { Check, Loader2 } from 'lucide-react'
+
+interface GeneratedCard {
+ word: string
+ translation: string
+ definition: string
+ partOfSpeech?: string
+ pronunciation?: string
+ example?: string
+ exampleTranslation?: string
+ synonyms?: string[]
+ difficultyLevel?: string
+}
+
+interface CardSet {
+ id: string
+ name: string
+ color: string
+}
+
+interface CardSelectionDialogProps {
+ isOpen: boolean
+ generatedCards: GeneratedCard[]
+ cardSets: CardSet[]
+ onClose: () => void
+ onSave: (selectedCards: GeneratedCard[], cardSetId?: string) => Promise
+}
+
+export const CardSelectionDialog: React.FC = ({
+ isOpen,
+ generatedCards,
+ cardSets,
+ onClose,
+ onSave
+}) => {
+ const [selectedCardIndices, setSelectedCardIndices] = useState>(
+ new Set(generatedCards.map((_, index) => index)) // 預設全選
+ )
+ const [selectedCardSetId, setSelectedCardSetId] = useState('')
+ const [isSaving, setIsSaving] = useState(false)
+
+ const selectedCount = selectedCardIndices.size
+
+ const handleSelectAll = (checked: boolean) => {
+ if (checked) {
+ setSelectedCardIndices(new Set(generatedCards.map((_, index) => index)))
+ } else {
+ setSelectedCardIndices(new Set())
+ }
+ }
+
+ const handleCardToggle = (index: number, checked: boolean) => {
+ const newSelected = new Set(selectedCardIndices)
+ if (checked) {
+ newSelected.add(index)
+ } else {
+ newSelected.delete(index)
+ }
+ setSelectedCardIndices(newSelected)
+ }
+
+ const handleSave = async () => {
+ if (selectedCount === 0) {
+ alert('請至少選擇一張詞卡')
+ return
+ }
+
+ setIsSaving(true)
+ try {
+ const selectedCards = Array.from(selectedCardIndices).map(index => generatedCards[index])
+ await onSave(selectedCards, selectedCardSetId || undefined)
+ } catch (error) {
+ console.error('Save error:', error)
+ alert(`保存失敗: ${error instanceof Error ? error.message : '未知錯誤'}`)
+ } finally {
+ setIsSaving(false)
+ }
+ }
+
+ const isAllSelected = selectedCount === generatedCards.length
+
+ return (
+
+
+ {/* 操作工具列 */}
+
+
+
+
+
+
+ 保存到:
+
+
+
+
+ {/* 詞卡列表 */}
+
+ {generatedCards.map((card, index) => (
+ handleCardToggle(index, checked)}
+ />
+ ))}
+
+
+ {/* 底部操作按鈕 */}
+
+
+
+
+
+
+ )
+}
+
+interface CardPreviewItemProps {
+ card: GeneratedCard
+ index: number
+ isSelected: boolean
+ onToggle: (checked: boolean) => void
+}
+
+const CardPreviewItem: React.FC = ({
+ card,
+ index,
+ isSelected,
+ onToggle
+}) => {
+ return (
+
+
+
+
+
+
+
+ {card.word}
+
+ {card.difficultyLevel && (
+
+ {card.difficultyLevel}
+
+ )}
+
+
+
+
+ 翻譯:
+ {card.translation}
+
+
+ {card.partOfSpeech && (
+
+ 詞性:
+ {card.partOfSpeech}
+
+ )}
+
+ {card.pronunciation && (
+
+ 發音:
+ {card.pronunciation}
+
+ )}
+
+
+
+
定義:
+
{card.definition}
+
+
+ {card.example && (
+
+
例句:
+
"{card.example}"
+ {card.exampleTranslation && (
+
{card.exampleTranslation}
+ )}
+
+ )}
+
+ {card.synonyms && card.synonyms.length > 0 && (
+
+
同義詞:
+
+ {card.synonyms.map((synonym, idx) => (
+
+ {synonym}
+
+ ))}
+
+
+ )}
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/frontend/components/ClickableTextV2.tsx b/frontend/components/ClickableTextV2.tsx
index 39f68b1..3f81b62 100644
--- a/frontend/components/ClickableTextV2.tsx
+++ b/frontend/components/ClickableTextV2.tsx
@@ -36,6 +36,7 @@ interface ClickableTextProps {
onWordClick?: (word: string, analysis: WordAnalysis) => void
onWordCostConfirm?: (word: string, cost: number) => Promise // 收費確認
onNewWordAnalysis?: (word: string, analysis: WordAnalysis) => void // 新詞彙分析資料回調
+ onSaveWord?: (word: string, analysis: WordAnalysis) => Promise // 保存詞彙回調
remainingUsage?: number // 剩餘使用次數
}
@@ -47,15 +48,17 @@ export function ClickableTextV2({
onWordClick,
onWordCostConfirm,
onNewWordAnalysis,
+ onSaveWord,
remainingUsage = 5
}: ClickableTextProps) {
const [selectedWord, setSelectedWord] = useState(null)
- const [popupPosition, setPopupPosition] = useState({ x: 0, y: 0 })
+ const [popupPosition, setPopupPosition] = useState({ x: 0, y: 0, showBelow: false })
const [showCostConfirm, setShowCostConfirm] = useState<{
word: string
cost: number
position: { x: number, y: number }
} | null>(null)
+ const [isSavingWord, setIsSavingWord] = useState(false)
// 輔助函數:兼容大小寫屬性名稱
const getWordProperty = (wordData: any, propName: string) => {
@@ -72,9 +75,17 @@ export function ClickableTextV2({
const wordAnalysis = analysis?.[cleanWord]
const rect = event.currentTarget.getBoundingClientRect()
+ const viewportHeight = window.innerHeight
+ const popupHeight = 400 // 估計popup高度
+
+ // 智能定位:如果上方空間不足,就顯示在下方
+ const spaceAbove = rect.top
+ const spaceBelow = viewportHeight - rect.bottom
+
const position = {
x: rect.left + rect.width / 2,
- y: rect.top - 10
+ y: spaceAbove >= popupHeight ? rect.top - 10 : rect.bottom + 10,
+ showBelow: spaceAbove < popupHeight
}
if (wordAnalysis) {
@@ -150,6 +161,21 @@ export function ClickableTextV2({
setSelectedWord(null)
}
+ const handleSaveWord = async () => {
+ if (!selectedWord || !analysis?.[selectedWord] || !onSaveWord) return
+
+ setIsSavingWord(true)
+ try {
+ await onSaveWord(selectedWord, analysis[selectedWord])
+ setSelectedWord(null) // 保存成功後關閉popup
+ } catch (error) {
+ console.error('Save word error:', error)
+ alert(`保存詞彙失敗: ${error instanceof Error ? error.message : '未知錯誤'}`)
+ } finally {
+ setIsSavingWord(false)
+ }
+ }
+
const queryWordWithAI = async (word: string, position: { x: number, y: number }) => {
try {
console.log(`🤖 查詢單字: ${word}`)
@@ -258,135 +284,53 @@ export function ClickableTextV2({
})}
- {/* 單字資訊彈窗 */}
+ {/* 現代風格詞彙彈窗 */}
{selectedWord && analysis?.[selectedWord] && (