From 262312b02a9d92e37d1eb745cf525fc27b79a33c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=84=AD=E6=B2=9B=E8=BB=92?= Date: Mon, 6 Oct 2025 00:42:05 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AF=A6=E7=8F=BE=E6=85=A3=E7=94=A8?= =?UTF-8?q?=E8=AA=9E=E5=BD=88=E7=AA=97=E6=99=BA=E8=83=BD=E5=AE=9A=E4=BD=8D?= =?UTF-8?q?=20+=20=E7=B0=A1=E5=8C=96=20WordPopup=20=E7=B5=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 慣用語彈窗智能定位系統 - ✅ 創建智能定位工具 (popupPositioning.ts) - ✅ 自動檢測可用空間,防止彈窗被底部遮蔽 - ✅ 智能選擇彈出方向 (上方/下方/居中) - ✅ 響應式適配:桌面智能定位 + 手機底部modal - ✅ 修正底部慣用語點擊體驗問題 ## WordPopup 組件簡化 - 🔧 移除未使用的 useState import - 🔧 簡化過度的響應式設計 (移除多處 sm: 斷點) - 🔧 替換 ContentBlock 為簡單 div 結構 - 🔧 簡化條件渲染邏輯 (IIFE → 簡單 &&) - ✅ 統一字體大小,與慣用語彈窗保持一致 ## 技術改進 - 📱 設備檢測:自動適配移動/桌面體驗 - 🎯 智能定位:邊界檢測 + 動態位置計算 - 🧹 代碼簡化:減少複雜度,提升維護性 - 🎨 設計統一:兩種彈窗風格對齊 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- frontend/app/generate/page.tsx | 72 +++++++++---- frontend/components/word/WordPopup.tsx | 55 +++++----- frontend/lib/utils/popupPositioning.ts | 139 +++++++++++++++++++++++++ 3 files changed, 218 insertions(+), 48 deletions(-) create mode 100644 frontend/lib/utils/popupPositioning.ts diff --git a/frontend/app/generate/page.tsx b/frontend/app/generate/page.tsx index 5450004..dce6c6f 100644 --- a/frontend/app/generate/page.tsx +++ b/frontend/app/generate/page.tsx @@ -9,6 +9,7 @@ import { flashcardsService } from '@/lib/services/flashcards' import { compareCEFRLevels, getLevelIndex, getTargetLearningRange } from '@/lib/utils/cefrUtils' import { BluePlayButton } from '@/components/shared/BluePlayButton' import { API_CONFIG } from '@/lib/config/api' +import { calculateSmartPopupPosition, isMobileDevice, getElementPosition } from '@/lib/utils/popupPositioning' import Link from 'next/link' // 常數定義 @@ -34,6 +35,7 @@ interface IdiomPopup { idiom: string; analysis: any; position: { x: number; y: number }; + placement?: 'top' | 'bottom' | 'center'; } function GenerateContent() { @@ -442,15 +444,36 @@ function GenerateContent() { key={index} className="cursor-pointer transition-all duration-200 rounded-lg relative mx-0.5 px-1 py-0.5 inline-flex items-center gap-1 bg-blue-50 border border-blue-200 hover:bg-blue-100 hover:shadow-lg transform hover:-translate-y-0.5 text-blue-700 font-medium" onClick={(e) => { - // 使用新的API格式,直接使用idiom物件 - setIdiomPopup({ - idiom: idiom.idiom, - analysis: idiom, - position: { - x: e.currentTarget.getBoundingClientRect().left + e.currentTarget.getBoundingClientRect().width / 2, - y: e.currentTarget.getBoundingClientRect().bottom + 10 - } - }) + // 檢測移動設備 + const isMobile = isMobileDevice() + + if (isMobile) { + // 移動設備:使用底部居中 modal + setIdiomPopup({ + idiom: idiom.idiom, + analysis: idiom, + position: { x: 0, y: 0 }, // 移動版不使用計算位置 + placement: 'center' + }) + } else { + // 桌面設備:使用智能定位系統 + const elementPosition = getElementPosition(e.currentTarget) + const smartPosition = calculateSmartPopupPosition( + elementPosition, + 384, // w-96 = 384px + 400 // 預估高度 + ) + + setIdiomPopup({ + idiom: idiom.idiom, + analysis: idiom, + position: { + x: smartPosition.x, + y: smartPosition.y + }, + placement: smartPosition.placement + }) + } }} title={`${idiom.idiom}: ${idiom.translation}`} > @@ -494,7 +517,7 @@ function GenerateContent() { )} - {/* 片語彈窗 */} + {/* 片語彈窗 - 智能定位系統 */} {idiomPopup && ( <>
setIdiomPopup(null)} />
diff --git a/frontend/components/word/WordPopup.tsx b/frontend/components/word/WordPopup.tsx index 3c994c2..0f9284a 100644 --- a/frontend/components/word/WordPopup.tsx +++ b/frontend/components/word/WordPopup.tsx @@ -1,6 +1,5 @@ -import React, { useState } from 'react' +import React from 'react' import { Modal } from '@/components/ui/Modal' -import { ContentBlock } from '@/components/shared/ContentBlock' import { getCEFRColor } from '@/lib/utils/flashcardUtils' import { BluePlayButton } from '@/components/shared/BluePlayButton' import { useWordAnalysis } from '@/hooks/word/useWordAnalysis' @@ -39,20 +38,18 @@ export const WordPopup: React.FC = ({ return ( {/* Header */} -
+
-

- {getWordProperty(wordAnalysis, 'word')} -

+

{getWordProperty(wordAnalysis, 'word')}

-
- +
+ {getWordProperty(wordAnalysis, 'partOfSpeech')}
- + {getWordProperty(wordAnalysis, 'pronunciation')} = ({
{/* Content */} -
+
{/* Translation */} - +
+

中文翻譯

{getWordProperty(wordAnalysis, 'translation')}

- +
{/* Definition */} - -

+

+

英文定義

+

{getWordProperty(wordAnalysis, 'definition')}

- +
{/* Example */} - {(() => { - const example = getWordProperty(wordAnalysis, 'example') - return example && example !== 'null' && example !== 'undefined' - })() && ( - + {getWordProperty(wordAnalysis, 'example') && ( +
+

例句

-

+

"{getWordProperty(wordAnalysis, 'example')}"

-

+

{getWordProperty(wordAnalysis, 'exampleTranslation')}

- +
)} {/* Synonyms */} - {(() => { - const synonyms = getWordProperty(wordAnalysis, 'synonyms') - return synonyms && Array.isArray(synonyms) && synonyms.length > 0 - })() && ( - + {getWordProperty(wordAnalysis, 'synonyms')?.length > 0 && ( +
+

同義詞

{getWordProperty(wordAnalysis, 'synonyms')?.map((synonym: string, index: number) => ( = ({ ))}
- +
)}
{/* Save Button */} {onSaveWord && ( -
+