dramaling-app/apps/web/src/views/learning/VocabularyIntroductionView.vue

1429 lines
38 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="vocabulary-layout">
<!-- 主內容區 - 多列布局 (依據 function-specs) -->
<div class="main-content">
<!-- 頁面標題區域 -->
<div class="page-header">
<div class="header-section">
<div class="header-text">
<h1>詞彙學習 - {{ currentWord?.text || '選擇詞彙' }}</h1>
<p>探索新詞彙掌握語言精髓</p>
</div>
<div class="header-stats">
<div class="stat-item">
<span class="stat-value">{{ userProgress.learnedCount }}</span>
<span class="stat-label">已學詞彙</span>
</div>
<div class="stat-item">
<span class="stat-value">{{ Math.round(currentWordProgress) }}%</span>
<span class="stat-label">學習進度</span>
</div>
</div>
</div>
</div>
<!-- 多列布局容器 (依據 function-specs) -->
<div class="vocabulary-content">
<!-- 左側詞彙資訊主區域 -->
<div class="vocabulary-main">
<!-- 詞彙介紹卡片 -->
<div class="vocabulary-section">
<div class="vocabulary-card" v-if="currentWord">
<!-- 目標詞彙文字 (大字體標題) -->
<div class="vocabulary-word">{{ currentWord.text }}</div>
<!-- 音標顯示 (支援點擊複製) -->
<div
class="vocabulary-phonetic"
@click="copyPhonetic"
:title="'點擊複製音標: ' + currentWord.phonetic"
>
{{ currentWord.phonetic }}
<QIcon name="content_copy" class="copy-icon" />
</div>
<!-- 詞性標記 (色彩編碼) -->
<div class="part-of-speech">
<span
v-for="pos in currentWord.parts_of_speech"
:key="pos"
class="pos-tag"
:class="`pos-${pos}`"
>
{{ pos }}
</span>
</div>
<!-- 詞頻統計 (1-5星評級) -->
<div class="frequency-rating">
<span class="frequency-label">使用頻率:</span>
<div class="stars">
<QIcon
v-for="i in 5"
:key="i"
name="star"
:class="{ active: i <= (currentWord.frequency_rating || 0) }"
/>
</div>
</div>
<!-- 中文定義 (主要定義區域) -->
<div class="vocabulary-definition">
{{ currentWord.definition }}
</div>
<!-- 英文定義 (進階模式,可切換) -->
<div v-if="showEnglishDefinition && currentWord.definition_english" class="vocabulary-definition-en">
<strong>English:</strong> {{ currentWord.definition_english }}
</div>
<!-- 使用情境說明 (獨立區塊) -->
<div v-if="currentWord.usage_context" class="usage-context">
<h4>使用情境</h4>
<p>{{ currentWord.usage_context }}</p>
</div>
<!-- 多例句並列顯示 -->
<div class="examples-section">
<h4>例句</h4>
<div
v-for="(example, index) in currentWord.examples"
:key="index"
class="vocabulary-example"
>
<div class="example-text">{{ example.text }}</div>
<div class="example-translation">{{ example.translation }}</div>
<button
class="example-audio-btn"
@click="playExampleAudio(example, index + 1)"
:title="`播放例句 ${index + 1} (快捷鍵: ${index + 1})`"
>
<QIcon name="volume_up" />
</button>
</div>
</div>
<!-- 控制按鈕區域 -->
<div class="vocabulary-controls">
<!-- 發音播放按鈕 (Space) -->
<button
class="control-btn"
@click="playAudio"
:disabled="audioLoading"
:title="'播放發音 (Space)'"
>
<QIcon :name="audioLoading ? 'hourglass_empty' : 'volume_up'" />
播放發音
</button>
<!-- 慢速發音按鈕 (Shift+Space) -->
<button
class="control-btn"
@click="playSlowAudio"
:disabled="audioLoading"
:title="'慢速播放 (Shift+Space)'"
>
<QIcon name="slow_motion_video" />
慢速播放
</button>
<!-- 收藏按鈕 (Ctrl+D) -->
<button
class="control-btn"
:class="{ bookmarked: isBookmarked }"
@click="toggleBookmark"
:title="'收藏詞彙 (Ctrl+D)'"
>
<QIcon :name="isBookmarked ? 'bookmark' : 'bookmark_border'" />
{{ isBookmarked ? '已收藏' : '收藏' }}
</button>
<!-- 詞典查詢按鈕 (F1) -->
<button
class="control-btn"
@click="openDictionary"
@contextmenu.prevent="showDictionaryMenu"
:title="'查詢外部詞典 (F1 或右鍵)'"
>
<QIcon name="menu_book" />
詞典查詢
</button>
<!-- 開始練習按鈕 (Enter) -->
<button
class="control-btn primary"
@click="startPractice"
:title="'開始練習 (Enter)'"
>
<QIcon name="play_arrow" />
開始練習
</button>
<!-- 跳過介紹按鈕 (Shift+Enter) -->
<button
class="control-btn secondary"
@click="skipIntroduction"
:title="'跳過介紹 (Shift+Enter)'"
>
<QIcon name="skip_next" />
跳過介紹
</button>
</div>
<!-- 學習進度條 -->
<div class="progress-section">
<div class="progress-label">學習進度: {{ Math.round(currentWordProgress) }}%</div>
<QLinearProgress
:value="currentWordProgress / 100"
color="teal"
track-color="grey-3"
size="md"
/>
</div>
</div>
</div>
<!-- 筆記編輯器區域 (支援Markdown可摺疊) -->
<div class="notes-section" :class="{ expanded: notesExpanded }">
<div class="notes-header" @click="toggleNotes">
<h4>
<QIcon name="edit_note" />
學習筆記 (Ctrl+N)
</h4>
<QIcon :name="notesExpanded ? 'expand_less' : 'expand_more'" />
</div>
<Transition name="slide-down">
<div v-show="notesExpanded" class="notes-content">
<QInput
v-model="userNotes"
type="textarea"
placeholder="在此記錄學習重點,支援 Markdown 格式..."
rows="6"
outlined
class="notes-editor"
/>
<div class="notes-actions">
<QBtn
flat
dense
size="sm"
icon="preview"
label="預覽"
@click="toggleNotesPreview"
/>
<QBtn
flat
dense
size="sm"
icon="save"
label="保存"
@click="saveNotes"
/>
</div>
<!-- Markdown 預覽 -->
<div v-if="showNotesPreview" class="notes-preview" v-html="renderedNotes"></div>
</div>
</Transition>
</div>
</div>
<!-- 右側:相關詞彙和例句面板 (依據 function-specs) -->
<div class="vocabulary-sidebar">
<!-- 相關詞彙推薦 -->
<div class="related-words-section">
<h4>相關詞彙</h4>
<div class="related-words-list">
<button
v-for="relatedWord in currentWord?.related_words || []"
:key="relatedWord.id"
class="related-word-item"
@click="selectRelatedWord(relatedWord)"
@click.middle="openInNewTab(relatedWord)"
@contextmenu.prevent="openInNewTab(relatedWord)"
:title="`點擊學習Ctrl+Click 或右鍵新標籤開啟`"
>
<div class="related-word-text">{{ relatedWord.text }}</div>
<div class="related-word-def">{{ relatedWord.definition }}</div>
<QIcon name="open_in_new" class="new-tab-icon" />
</button>
</div>
</div>
<!-- 詞彙清單 -->
<div class="vocabulary-list">
<div class="list-header">
<div class="list-title">詞彙清單</div>
<div class="filter-tabs">
<button
v-for="filter in filterOptions"
:key="filter.value"
class="filter-tab"
:class="{ active: currentFilter === filter.value }"
@click="setFilter(filter.value)"
>
{{ filter.label }}
</button>
</div>
</div>
<div class="vocabulary-items-container">
<div
v-for="word in filteredWords"
:key="word.id"
class="vocabulary-item"
:class="{ active: currentWord?.id === word.id }"
@click="selectWord(word)"
>
<div class="word-info">
<div class="word-text">
<div class="word-main">{{ word.text }}</div>
<div class="word-definition">{{ word.definition }}</div>
</div>
</div>
<div class="word-status">
<div
class="mastery-indicator"
:class="getMasteryClass(word.mastery_level)"
></div>
<button
class="play-btn"
@click.stop="playWordAudio(word)"
:title="`播放 ${word.text} 發音`"
>
<QIcon name="volume_up" />
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import { useQuasar } from 'quasar'
import { useAudio } from '@/composables/useAudio'
// 類型定義 (依據 function-specs)
interface VocabularyWord {
id: string
text: string
phonetic: string
definition: string
definition_english?: string
parts_of_speech: string[]
frequency_rating?: number
usage_context?: string
examples: ExampleSentence[]
audio_url?: string
difficulty_level?: number
mastery_level?: number
related_words?: RelatedWord[]
user_notes?: string
}
interface ExampleSentence {
text: string
translation: string
audio_url?: string
}
interface RelatedWord {
id: string
text: string
definition: string
}
interface FilterOption {
value: string
label: string
}
// 狀態管理
const router = useRouter()
const $q = useQuasar()
const { quickPlay, isLoading: audioLoading } = useAudio()
const currentFilter = ref('all')
const currentWordIndex = ref(0)
const notesExpanded = ref(false)
const showNotesPreview = ref(false)
const userNotes = ref('')
const isBookmarked = ref(false)
const showEnglishDefinition = ref(false)
// 過濾選項
const filterOptions: FilterOption[] = [
{ value: 'all', label: '全部' },
{ value: 'learning', label: '學習中' },
{ value: 'learned', label: '已掌握' },
{ value: 'difficult', label: '困難' }
]
// 模擬詞彙數據 (依據 function-specs 欄位細節)
const vocabularyWords = ref<VocabularyWord[]>([
{
id: '1',
text: 'sophisticated',
phonetic: '/səˈfɪstɪkeɪtɪd/',
definition: '複雜的;精密的;老練的;世故的',
definition_english: 'Having great knowledge or experience; complex and refined',
parts_of_speech: ['adj.'],
frequency_rating: 4,
usage_context: '常用於描述技術、系統、人的品味或行為的複雜性和精細程度',
examples: [
{
text: 'She has sophisticated tastes in art.',
translation: '她在藝術方面品味很高雅。',
audio_url: '/audio/sophisticated_example1.mp3'
},
{
text: 'The sophisticated security system protects the building.',
translation: '精密的安全系統保護著這棟建築。',
audio_url: '/audio/sophisticated_example2.mp3'
}
],
audio_url: '/audio/sophisticated.mp3',
difficulty_level: 3,
mastery_level: 60,
related_words: [
{ id: '4', text: 'complex', definition: '複雜的' },
{ id: '5', text: 'refined', definition: '精緻的' },
{ id: '6', text: 'advanced', definition: '先進的' }
],
user_notes: '## 學習重點\n- 可用於描述人或事物\n- 褒義詞,表示正面的複雜性'
},
{
id: '2',
text: 'benevolent',
phonetic: '/bəˈnevələnt/',
definition: '仁慈的;善意的;慈善的',
definition_english: 'Well meaning and kindly; charitable',
parts_of_speech: ['adj.'],
frequency_rating: 3,
usage_context: '正式用語,常用於描述統治者、組織或個人的善意行為',
examples: [
{
text: 'The benevolent king helped the poor.',
translation: '仁慈的國王幫助貧民。',
audio_url: '/audio/benevolent_example1.mp3'
}
],
audio_url: '/audio/benevolent.mp3',
difficulty_level: 2,
mastery_level: 80,
related_words: [
{ id: '7', text: 'kind', definition: '善良的' },
{ id: '8', text: 'charitable', definition: '慈善的' }
]
},
{
id: '3',
text: 'meticulous',
phonetic: '/məˈtɪkjələs/',
definition: '一絲不苟的;細心的;謹慎的',
definition_english: 'Showing great attention to detail; very careful and precise',
parts_of_speech: ['adj.'],
frequency_rating: 3,
usage_context: '用於描述工作態度、行為方式,強調注重細節和精確性',
examples: [
{
text: 'She is meticulous about her work.',
translation: '她對工作一絲不苟。',
audio_url: '/audio/meticulous_example1.mp3'
}
],
audio_url: '/audio/meticulous.mp3',
difficulty_level: 3,
mastery_level: 30,
related_words: [
{ id: '9', text: 'careful', definition: '小心的' },
{ id: '10', text: 'precise', definition: '精確的' }
]
}
])
// 計算屬性
const currentWord = computed(() => {
return vocabularyWords.value[currentWordIndex.value] || null
})
const currentWordProgress = computed(() => {
return currentWord.value?.mastery_level || 0
})
const filteredWords = computed(() => {
const words = vocabularyWords.value
switch (currentFilter.value) {
case 'learning':
return words.filter(w => (w.mastery_level || 0) < 80 && (w.mastery_level || 0) > 0)
case 'learned':
return words.filter(w => (w.mastery_level || 0) >= 80)
case 'difficult':
return words.filter(w => (w.difficulty_level || 1) >= 3)
default:
return words
}
})
const userProgress = computed(() => ({
learnedCount: vocabularyWords.value.filter(w => (w.mastery_level || 0) >= 80).length,
todayCount: 5 // 模擬今日學習數量
}))
// 筆記相關 (Markdown 渲染)
const renderedNotes = computed(() => {
if (!userNotes.value) return ''
// 簡單的 Markdown 渲染 (實際會使用專門的 Markdown 庫)
return userNotes.value
.replace(/^### (.+$)/gim, '<h3>$1</h3>')
.replace(/^## (.+$)/gim, '<h2>$1</h2>')
.replace(/^# (.+$)/gim, '<h1>$1</h1>')
.replace(/\*\*(.+)\*\*/gim, '<strong>$1</strong>')
.replace(/\*(.+)\*/gim, '<em>$1</em>')
.replace(/\n/gim, '<br>')
})
// 方法
const selectWord = (word: VocabularyWord) => {
const index = vocabularyWords.value.findIndex(w => w.id === word.id)
if (index !== -1) {
currentWordIndex.value = index
// 載入用戶筆記
userNotes.value = word.user_notes || ''
// 載入收藏狀態 (模擬)
isBookmarked.value = Math.random() > 0.5
}
}
const selectRelatedWord = (word: RelatedWord) => {
// 模擬導航到相關詞彙
const fullWord = vocabularyWords.value.find(w => w.id === word.id)
if (fullWord) {
selectWord(fullWord)
}
}
const openInNewTab = (word: RelatedWord) => {
// 實現新標籤開啟功能 (依據 function-specs)
const url = `/learning/vocabulary/introduction/${word.id}`
window.open(url, '_blank')
}
const setFilter = (filterValue: string) => {
currentFilter.value = filterValue
}
const getMasteryClass = (masteryLevel?: number): string => {
const level = masteryLevel || 0
if (level >= 80) return 'learned'
if (level > 0) return 'learning'
return ''
}
// 音頻播放相關
const playAudio = async () => {
if (currentWord.value?.audio_url) {
await quickPlay(currentWord.value.audio_url)
}
}
const playSlowAudio = async () => {
if (currentWord.value?.audio_url) {
await quickPlay(currentWord.value.audio_url, { playbackRate: 0.75 })
}
}
const playExampleAudio = async (example: ExampleSentence, index: number) => {
if (example.audio_url) {
await quickPlay(example.audio_url)
}
}
const playWordAudio = async (word: VocabularyWord) => {
if (word.audio_url) {
await quickPlay(word.audio_url)
}
}
// 筆記功能 (依據 function-specs)
const toggleNotes = () => {
notesExpanded.value = !notesExpanded.value
}
const toggleNotesPreview = () => {
showNotesPreview.value = !showNotesPreview.value
}
const saveNotes = () => {
if (currentWord.value) {
currentWord.value.user_notes = userNotes.value
$q.notify({
type: 'positive',
message: '筆記已保存',
position: 'top-right'
})
}
}
// 書籤功能 (依據 function-specs)
const toggleBookmark = () => {
isBookmarked.value = !isBookmarked.value
if (isBookmarked.value) {
// 實際會整合瀏覽器書籤 API
$q.notify({
type: 'positive',
message: '已添加到書籤',
position: 'top-right'
})
} else {
$q.notify({
type: 'info',
message: '已從書籤移除',
position: 'top-right'
})
}
}
// 詞典整合功能 (依據 function-specs)
const openDictionary = () => {
if (currentWord.value) {
const word = encodeURIComponent(currentWord.value.text)
// 默認使用 Cambridge Dictionary
window.open(`https://dictionary.cambridge.org/search/english/direct/?q=${word}`, '_blank')
}
}
const showDictionaryMenu = () => {
// 顯示詞典選擇菜單
const dictionaries = [
{ name: 'Cambridge', url: 'https://dictionary.cambridge.org/search/english/direct/?q=' },
{ name: 'Oxford', url: 'https://www.oxfordlearnersdictionaries.com/definition/english/' },
{ name: 'Merriam-Webster', url: 'https://www.merriam-webster.com/dictionary/' }
]
// 這裡會顯示上下文菜單 (實際實現)
console.log('Dictionary menu for:', currentWord.value?.text)
}
// 複製音標功能
const copyPhonetic = async () => {
if (currentWord.value?.phonetic) {
try {
await navigator.clipboard.writeText(currentWord.value.phonetic)
$q.notify({
type: 'positive',
message: '音標已複製到剪貼板',
position: 'top-right'
})
} catch (error) {
console.error('複製失敗:', error)
}
}
}
// 練習和導航
const startPractice = () => {
if (currentWord.value) {
router.push(`/learning/vocabulary/practice?word=${currentWord.value.id}`)
}
}
const skipIntroduction = () => {
// 跳過當前詞彙,進入下一個
if (currentWordIndex.value < vocabularyWords.value.length - 1) {
currentWordIndex.value++
} else {
router.push('/learning/vocabulary/practice')
}
}
// 快捷鍵系統 (依據 function-specs)
const handleKeyboard = (event: KeyboardEvent) => {
// 避免在輸入框中觸發快捷鍵
if (event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement) {
return
}
switch (event.code) {
case 'Space':
event.preventDefault()
if (event.shiftKey) {
playSlowAudio()
} else {
playAudio()
}
break
case 'Enter':
event.preventDefault()
if (event.shiftKey) {
skipIntroduction()
} else {
startPractice()
}
break
case 'KeyD':
if (event.ctrlKey || event.metaKey) {
event.preventDefault()
toggleBookmark()
}
break
case 'KeyN':
if (event.ctrlKey || event.metaKey) {
event.preventDefault()
toggleNotes()
}
break
case 'F1':
event.preventDefault()
openDictionary()
break
case 'Digit1':
case 'Digit2':
case 'Digit3':
case 'Digit4':
case 'Digit5':
const index = parseInt(event.code.slice(-1)) - 1
if (currentWord.value?.examples[index]) {
playExampleAudio(currentWord.value.examples[index], index + 1)
}
break
}
}
// 生命週期
onMounted(() => {
document.addEventListener('keydown', handleKeyboard)
// 自動播放第一個詞彙的發音 (依據 function-specs 操作流程)
if (currentWord.value?.audio_url) {
setTimeout(() => {
playAudio()
}, 1000)
}
})
onUnmounted(() => {
document.removeEventListener('keydown', handleKeyboard)
})
</script>
<style lang="scss" scoped>
// 嚴格對照 vocabulary.html 原型的樣式 + function-specs 多列布局
.vocabulary-layout {
min-height: 100vh;
background: linear-gradient(135deg, var(--bg-primary, #{$background-primary}) 0%, var(--bg-secondary, #{$background-secondary}) 100%);
display: flex;
}
.main-content {
flex: 1;
padding: var(--space-6, #{$space-6});
overflow-y: auto;
}
// 多列布局容器 (依據 function-specs)
.vocabulary-content {
display: grid;
grid-template-columns: 2fr 1fr;
gap: var(--space-6, #{$space-6});
@include respond-to(md) {
grid-template-columns: 1fr;
gap: var(--space-4, #{$space-4});
}
}
// 左側:詞彙資訊主區域
.vocabulary-main {
display: flex;
flex-direction: column;
gap: var(--space-6, #{$space-6});
}
// 右側:相關詞彙和例句面板
.vocabulary-sidebar {
display: flex;
flex-direction: column;
gap: var(--space-6, #{$space-6});
}
.page-header {
margin-bottom: var(--space-8, #{$space-8});
}
// 詞性標記 (色彩編碼)
.part-of-speech {
display: flex;
gap: var(--space-2, #{$space-2});
margin-bottom: var(--space-4, #{$space-4});
justify-content: center;
}
.pos-tag {
padding: var(--space-1, #{$space-1}) var(--space-3, #{$space-3});
border-radius: var(--radius-md, #{$radius-md});
font-size: var(--text-sm, #{$text-sm});
font-weight: 600;
&.pos-adj {
background: rgba(156, 39, 176, 0.1);
color: var(--accent-violet, #{$accent-violet});
}
&.pos-n {
background: rgba(52, 152, 219, 0.1);
color: var(--info-cyan, #{$info-cyan});
}
&.pos-v {
background: rgba(231, 76, 60, 0.1);
color: var(--error-red, #{$error-red});
}
&.pos-adv {
background: rgba(243, 156, 18, 0.1);
color: var(--warning-yellow, #{$warning-yellow});
}
}
// 詞頻統計 (1-5星評級)
.frequency-rating {
display: flex;
align-items: center;
justify-content: center;
gap: var(--space-2, #{$space-2});
margin-bottom: var(--space-4, #{$space-4});
}
.frequency-label {
font-size: var(--text-sm, #{$text-sm});
color: var(--text-secondary, #{$text-secondary});
}
.stars {
display: flex;
gap: 2px;
.q-icon {
font-size: 16px;
color: var(--text-tertiary, #{$text-tertiary});
transition: color 0.2s ease;
&.active {
color: var(--star-active, #{$star-active});
}
}
}
// 音標顯示 (支援點擊複製)
.vocabulary-phonetic {
font-size: var(--text-xl, #{$text-xl});
color: var(--text-secondary, #{$text-secondary});
margin-bottom: var(--space-6, #{$space-6});
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: var(--space-2, #{$space-2});
transition: all 0.2s ease;
&:hover {
color: var(--primary-teal, #{$primary-teal});
.copy-icon {
opacity: 1;
}
}
.copy-icon {
font-size: 16px;
opacity: 0;
transition: opacity 0.2s ease;
}
}
// 使用情境說明
.usage-context {
background: var(--bg-secondary, #{$background-secondary});
padding: var(--space-4, #{$space-4});
border-radius: var(--radius-lg, #{$radius-lg});
margin-bottom: var(--space-6, #{$space-6});
border-left: 4px solid var(--primary-teal, #{$primary-teal});
h4 {
margin: 0 0 var(--space-2, #{$space-2}) 0;
font-size: var(--text-base, #{$text-base});
font-weight: 600;
color: var(--text-primary, #{$text-primary});
}
p {
margin: 0;
font-size: var(--text-sm, #{$text-sm});
color: var(--text-secondary, #{$text-secondary});
line-height: 1.5;
}
}
// 例句區域
.examples-section {
margin-bottom: var(--space-6, #{$space-6});
h4 {
margin: 0 0 var(--space-4, #{$space-4}) 0;
font-size: var(--text-lg, #{$text-lg});
font-weight: 600;
color: var(--text-primary, #{$text-primary});
text-align: center;
}
}
.vocabulary-example {
background: var(--bg-secondary, #{$background-secondary});
padding: var(--space-4, #{$space-4});
border-radius: var(--radius-lg, #{$radius-lg});
margin-bottom: var(--space-3, #{$space-3});
position: relative;
.example-text {
font-style: italic;
color: var(--text-primary, #{$text-primary});
margin-bottom: var(--space-2, #{$space-2});
}
.example-translation {
font-size: var(--text-sm, #{$text-sm});
color: var(--text-secondary, #{$text-secondary});
}
.example-audio-btn {
position: absolute;
top: var(--space-2, #{$space-2});
right: var(--space-2, #{$space-2});
width: 24px;
height: 24px;
border: none;
border-radius: 50%;
background: var(--primary-teal, #{$primary-teal});
color: white;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
&:hover {
background: #00b8a0;
transform: scale(1.1);
}
.q-icon {
font-size: 14px;
}
}
}
// 控制按鈕增強樣式
.vocabulary-controls {
display: flex;
justify-content: center;
gap: var(--space-3, #{$space-3});
margin-top: var(--space-8, #{$space-8});
flex-wrap: wrap;
@include respond-to(sm) {
flex-direction: column;
align-items: center;
}
}
.control-btn {
padding: var(--space-3, #{$space-3}) var(--space-4, #{$space-4});
border: 2px solid var(--divider, #{$divider});
border-radius: var(--radius-lg, #{$radius-lg});
background: var(--bg-card, #{$card-background});
color: var(--text-primary, #{$text-primary});
font-size: var(--text-sm, #{$text-sm});
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: var(--space-2, #{$space-2});
white-space: nowrap;
&:hover {
border-color: var(--primary-teal, #{$primary-teal});
background: rgba(0, 229, 204, 0.1);
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
&.primary {
background: var(--primary-teal, #{$primary-teal});
border-color: var(--primary-teal, #{$primary-teal});
color: white;
&:hover {
background: #00b8a0;
}
}
&.secondary {
background: var(--background-secondary, #{$background-secondary});
border-color: var(--text-tertiary, #{$text-tertiary});
color: var(--text-secondary, #{$text-secondary});
&:hover {
border-color: var(--text-secondary, #{$text-secondary});
color: var(--text-primary, #{$text-primary});
}
}
&.bookmarked {
background: var(--warning-yellow, #{$warning-yellow});
border-color: var(--warning-yellow, #{$warning-yellow});
color: white;
&:hover {
background: #e67e22;
}
}
}
// 學習進度條
.progress-section {
margin-top: var(--space-6, #{$space-6});
.progress-label {
font-size: var(--text-sm, #{$text-sm});
color: var(--text-secondary, #{$text-secondary});
margin-bottom: var(--space-2, #{$space-2});
text-align: center;
}
}
// 筆記編輯器區域 (支援Markdown可摺疊)
.notes-section {
background: var(--bg-card, #{$card-background});
border: 1px solid var(--divider, #{$divider});
border-radius: var(--radius-xl, #{$radius-xl});
overflow: hidden;
transition: all 0.3s ease;
&.expanded {
box-shadow: var(--shadow-lg, #{$shadow-lg});
}
}
.notes-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--space-4, #{$space-4}) var(--space-6, #{$space-6});
background: var(--bg-secondary, #{$background-secondary});
cursor: pointer;
transition: background 0.2s ease;
&:hover {
background: rgba(0, 229, 204, 0.05);
}
h4 {
margin: 0;
font-size: var(--text-base, #{$text-base});
font-weight: 600;
color: var(--text-primary, #{$text-primary});
display: flex;
align-items: center;
gap: var(--space-2, #{$space-2});
}
.q-icon {
font-size: 20px;
color: var(--text-secondary, #{$text-secondary});
}
}
.notes-content {
padding: var(--space-6, #{$space-6});
}
.notes-editor {
margin-bottom: var(--space-4, #{$space-4});
}
.notes-actions {
display: flex;
gap: var(--space-2, #{$space-2});
margin-bottom: var(--space-4, #{$space-4});
}
.notes-preview {
background: var(--bg-secondary, #{$background-secondary});
padding: var(--space-4, #{$space-4});
border-radius: var(--radius-lg, #{$radius-lg});
border: 1px solid var(--divider, #{$divider});
h1, h2, h3 {
margin-top: 0;
margin-bottom: var(--space-2, #{$space-2});
color: var(--text-primary, #{$text-primary});
}
strong {
color: var(--text-primary, #{$text-primary});
}
em {
color: var(--text-secondary, #{$text-secondary});
}
}
// 動畫效果
.slide-down-enter-active,
.slide-down-leave-active {
transition: all 0.3s ease;
}
.slide-down-enter-from,
.slide-down-leave-to {
opacity: 0;
transform: translateY(-10px);
}
// 相關詞彙推薦
.related-words-section {
background: var(--bg-card, #{$card-background});
border: 1px solid var(--divider, #{$divider});
border-radius: var(--radius-xl, #{$radius-xl});
padding: var(--space-6, #{$space-6});
h4 {
margin: 0 0 var(--space-4, #{$space-4}) 0;
font-size: var(--text-lg, #{$text-lg});
font-weight: 600;
color: var(--text-primary, #{$text-primary});
}
}
.related-words-list {
display: flex;
flex-direction: column;
gap: var(--space-3, #{$space-3});
}
.related-word-item {
display: flex;
flex-direction: column;
padding: var(--space-3, #{$space-3});
border: 1px solid var(--divider, #{$divider});
border-radius: var(--radius-lg, #{$radius-lg});
background: var(--bg-secondary, #{$background-secondary});
cursor: pointer;
transition: all 0.2s ease;
position: relative;
&:hover {
border-color: var(--primary-teal, #{$primary-teal});
background: rgba(0, 229, 204, 0.05);
transform: translateY(-1px);
.new-tab-icon {
opacity: 1;
}
}
.related-word-text {
font-weight: 600;
color: var(--text-primary, #{$text-primary});
margin-bottom: var(--space-1, #{$space-1});
}
.related-word-def {
font-size: var(--text-sm, #{$text-sm});
color: var(--text-secondary, #{$text-secondary});
}
.new-tab-icon {
position: absolute;
top: var(--space-2, #{$space-2});
right: var(--space-2, #{$space-2});
font-size: 14px;
color: var(--text-tertiary, #{$text-tertiary});
opacity: 0;
transition: opacity 0.2s ease;
}
}
// 詞彙清單增強
.vocabulary-list {
background: var(--bg-card, #{$card-background});
border: 1px solid var(--divider, #{$divider});
border-radius: var(--radius-xl, #{$radius-xl});
padding: var(--space-6, #{$space-6});
}
.vocabulary-items-container {
max-height: 400px;
overflow-y: auto;
}
.vocabulary-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-4, #{$space-4});
border: 1px solid var(--divider, #{$divider});
border-radius: var(--radius-lg, #{$radius-lg});
margin-bottom: var(--space-3, #{$space-3});
transition: all 0.2s ease;
cursor: pointer;
&:hover {
border-color: var(--primary-teal, #{$primary-teal});
background: rgba(0, 229, 204, 0.05);
}
&.active {
border-color: var(--primary-teal, #{$primary-teal});
background: rgba(0, 229, 204, 0.1);
box-shadow: 0 0 0 1px rgba(0, 229, 204, 0.2);
}
}
.header-section {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--space-6, #{$space-6});
}
.header-text h1 {
font-size: var(--text-3xl, #{$text-3xl});
font-weight: 700;
color: var(--text-primary, #{$text-primary});
margin-bottom: var(--space-2, #{$space-2});
}
.header-text p {
font-size: var(--text-lg, #{$text-lg});
color: var(--text-secondary, #{$text-secondary});
}
.header-stats {
display: flex;
gap: var(--space-6, #{$space-6});
align-items: center;
}
.stat-item {
text-align: center;
}
.stat-value {
font-size: var(--text-2xl, #{$text-2xl});
font-weight: 700;
color: var(--primary-teal, #{$primary-teal});
display: block;
}
.stat-label {
font-size: var(--text-sm, #{$text-sm});
color: var(--text-secondary, #{$text-secondary});
}
/* 學習模式選擇 */
.mode-selector {
display: flex;
gap: var(--space-4, #{$space-4});
margin-bottom: var(--space-8, #{$space-8});
@include respond-to(sm) {
flex-direction: column;
}
}
.mode-card {
flex: 1;
background: var(--bg-card, #{$card-background});
border: 2px solid var(--divider, #{$divider});
border-radius: var(--radius-xl, #{$radius-xl});
padding: var(--space-6, #{$space-6});
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
border-color: var(--primary-teal, #{$primary-teal});
transform: translateY(-2px);
box-shadow: var(--shadow-lg, #{$shadow-lg});
}
&.active {
border-color: var(--primary-teal, #{$primary-teal});
background: rgba(0, 229, 204, 0.1);
}
}
.mode-icon {
font-size: var(--text-4xl, #{$text-4xl});
margin-bottom: var(--space-3, #{$space-3});
}
.mode-title {
font-size: var(--text-lg, #{$text-lg});
font-weight: 600;
color: var(--text-primary, #{$text-primary});
margin-bottom: var(--space-2, #{$space-2});
}
.mode-description {
font-size: var(--text-sm, #{$text-sm});
color: var(--text-secondary, #{$text-secondary});
margin-bottom: var(--space-4, #{$space-4});
}
.mode-progress {
display: flex;
justify-content: space-between;
font-size: var(--text-sm, #{$text-sm});
color: var(--text-tertiary, #{$text-tertiary});
}
/* 詞彙清單 */
.vocabulary-list {
background: var(--bg-card, #{$card-background});
border: 1px solid var(--divider, #{$divider});
border-radius: var(--radius-xl, #{$radius-xl});
padding: var(--space-6, #{$space-6});
}
.list-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--space-6, #{$space-6});
}
.list-title {
font-size: var(--text-xl, #{$text-xl});
font-weight: 600;
color: var(--text-primary, #{$text-primary});
}
.filter-tabs {
display: flex;
gap: var(--space-2, #{$space-2});
}
.filter-tab {
padding: var(--space-2, #{$space-2}) var(--space-4, #{$space-4});
border: 1px solid var(--divider, #{$divider});
border-radius: var(--radius-md, #{$radius-md});
background: var(--bg-secondary, #{$background-secondary});
color: var(--text-secondary, #{$text-secondary});
font-size: var(--text-sm, #{$text-sm});
cursor: pointer;
transition: all 0.2s ease;
&.active {
background: var(--primary-teal, #{$primary-teal});
border-color: var(--primary-teal, #{$primary-teal});
color: white;
}
}
.vocabulary-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-4, #{$space-4});
border: 1px solid var(--divider, #{$divider});
border-radius: var(--radius-lg, #{$radius-lg});
margin-bottom: var(--space-3, #{$space-3});
transition: all 0.2s ease;
cursor: pointer;
&:hover {
border-color: var(--primary-teal, #{$primary-teal});
background: rgba(0, 229, 204, 0.05);
}
}
.word-info {
display: flex;
align-items: center;
gap: var(--space-4, #{$space-4});
}
.word-text {
display: flex;
flex-direction: column;
gap: var(--space-1, #{$space-1});
}
.word-main {
font-size: var(--text-lg, #{$text-lg});
font-weight: 600;
color: var(--text-primary, #{$text-primary});
}
.word-definition {
font-size: var(--text-sm, #{$text-sm});
color: var(--text-secondary, #{$text-secondary});
}
.word-status {
display: flex;
align-items: center;
gap: var(--space-3, #{$space-3});
}
.mastery-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--text-tertiary, #{$text-tertiary});
&.learned {
background: var(--success-green, #{$success-green});
}
&.learning {
background: var(--warning-yellow, #{$warning-yellow});
}
}
.play-btn {
width: 32px;
height: 32px;
border: none;
border-radius: 50%;
background: var(--primary-teal, #{$primary-teal});
color: white;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
&:hover {
background: #00b8a0;
transform: scale(1.1);
}
}
</style>