Skip to content

Commit 1ea1701

Browse files
committed
Merge branch 'master' into dev
2 parents aeda3bc + 2f13b44 commit 1ea1701

17 files changed

Lines changed: 1106 additions & 109 deletions

File tree

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
<script setup lang="ts">
2+
import { BaseButton, BaseInput, Checkbox, Toast } from '@typewords/base'
3+
import { MessageBox } from '@typewords/core/utils/MessageBox.tsx'
4+
import { computed } from 'vue'
5+
import { useI18n } from 'vue-i18n'
6+
7+
export type FailedWordRow = {
8+
id: string
9+
word: string
10+
checked: boolean
11+
editing: boolean
12+
}
13+
14+
const props = defineProps<{
15+
rows: FailedWordRow[]
16+
}>()
17+
18+
const emit = defineEmits<{
19+
'update:rows': [FailedWordRow[]]
20+
}>()
21+
22+
const { t } = useI18n()
23+
const checkedCount = computed(() => props.rows.filter(r => r.checked).length)
24+
25+
const allChecked = computed({
26+
get: () => props.rows.length > 0 && props.rows.every(r => r.checked),
27+
set: (val: boolean) => {
28+
emit(
29+
'update:rows',
30+
props.rows.map(r => ({ ...r, checked: val }))
31+
)
32+
},
33+
})
34+
35+
function updateRows(rows: FailedWordRow[]) {
36+
emit('update:rows', rows)
37+
}
38+
39+
function updateRow(id: string, patch: Partial<FailedWordRow>) {
40+
updateRows(props.rows.map(r => (r.id === id ? { ...r, ...patch } : r)))
41+
}
42+
43+
function toggleRow(id: string, checked: boolean) {
44+
updateRow(id, { checked })
45+
}
46+
47+
function deleteRow(id: string) {
48+
updateRows(props.rows.filter(r => r.id !== id))
49+
}
50+
51+
function doBatchDelete() {
52+
const toDelete = props.rows.filter(r => r.checked)
53+
if (!toDelete.length) return Toast.warning('请先勾选要删除的单词')
54+
55+
const remove = () => updateRows(props.rows.filter(r => !r.checked))
56+
57+
if (toDelete.length > 10) {
58+
MessageBox.confirm(
59+
`确定删除已勾选的 ${toDelete.length} 个单词吗?`,
60+
'批量删除',
61+
() => void 0,
62+
() => void 0,
63+
null,
64+
{ t, onConfirm: remove }
65+
)
66+
return
67+
}
68+
remove()
69+
}
70+
71+
function startEdit(row: FailedWordRow) {
72+
updateRow(row.id, { editing: true })
73+
}
74+
75+
function finishEdit(row: FailedWordRow, value: string) {
76+
const word = value.trim() || row.word
77+
updateRow(row.id, { word, editing: false })
78+
}
79+
80+
function onEditKeydown(e: KeyboardEvent, row: FailedWordRow, value: string) {
81+
if (e.key === 'Enter') {
82+
finishEdit(row, value)
83+
}
84+
}
85+
</script>
86+
87+
<template>
88+
<div class="failed-table">
89+
<p class="failed-table-tip">
90+
以下单词未收录,可点击单词修改拼写后重试;勾选后可通过底部按钮处理
91+
</p>
92+
<div class="failed-table-toolbar">
93+
<Checkbox v-model="allChecked">全选</Checkbox>
94+
<BaseButton type="info" size="small" :disabled="!checkedCount" @click="doBatchDelete">
95+
批量删除
96+
</BaseButton>
97+
<span class="failed-table-count">已选 {{ checkedCount }} / {{ rows.length }}</span>
98+
</div>
99+
<div class="failed-table-head">
100+
<span class="col-check" />
101+
<span class="col-word">单词</span>
102+
<span class="col-action">操作</span>
103+
</div>
104+
<ul class="failed-table-body">
105+
<li v-for="row in rows" :key="row.id" class="failed-table-row">
106+
<div class="col-check">
107+
<Checkbox :model-value="row.checked" @update:model-value="toggleRow(row.id, $event)" />
108+
</div>
109+
<div class="col-word">
110+
<BaseInput
111+
v-if="row.editing"
112+
autofocus
113+
:model-value="row.word"
114+
@update:model-value="val => updateRow(row.id, { word: val })"
115+
@blur="finishEdit(row, row.word)"
116+
@keydown="onEditKeydown($event, row, row.word)"
117+
/>
118+
<button v-else type="button" class="word-text" @click="startEdit(row)">
119+
{{ row.word }}
120+
</button>
121+
</div>
122+
<div class="col-action">
123+
<button type="button" class="delete-btn" title="删除" @click="deleteRow(row.id)">
124+
<IconFluentDelete20Regular />
125+
</button>
126+
</div>
127+
</li>
128+
</ul>
129+
</div>
130+
</template>
131+
132+
<style scoped lang="scss">
133+
.failed-table-tip {
134+
color: var(--color-font-2);
135+
font-size: 0.88rem;
136+
line-height: 1.6;
137+
margin: 0 0 0.75rem;
138+
}
139+
140+
.failed-table-toolbar {
141+
align-items: center;
142+
display: flex;
143+
flex-wrap: wrap;
144+
gap: 0.75rem;
145+
margin-bottom: 0.75rem;
146+
}
147+
148+
.failed-table-count {
149+
color: var(--color-font-3);
150+
font-size: 0.85rem;
151+
margin-left: auto;
152+
}
153+
154+
.failed-table-head,
155+
.failed-table-row {
156+
align-items: center;
157+
display: grid;
158+
gap: 0.5rem;
159+
grid-template-columns: 2rem 1fr 2.5rem;
160+
}
161+
162+
.failed-table-head {
163+
border-bottom: 1px solid var(--color-input-border);
164+
color: var(--color-font-3);
165+
font-size: 0.82rem;
166+
font-weight: 700;
167+
padding-bottom: 0.5rem;
168+
}
169+
170+
.failed-table-body {
171+
list-style: none;
172+
margin: 0;
173+
max-height: 16rem;
174+
overflow: auto;
175+
padding: 0;
176+
}
177+
178+
.failed-table-row {
179+
border-bottom: 1px solid var(--color-input-border);
180+
padding: 0.55rem 0;
181+
182+
&:last-child {
183+
border-bottom: none;
184+
}
185+
}
186+
187+
.word-text {
188+
background: none;
189+
border: none;
190+
color: var(--color-font-1);
191+
cursor: pointer;
192+
font-size: 0.95rem;
193+
padding: 0.35rem 0;
194+
text-align: left;
195+
width: 100%;
196+
197+
&:hover {
198+
color: var(--color-select-bg);
199+
text-decoration: underline;
200+
}
201+
}
202+
203+
.delete-btn {
204+
align-items: center;
205+
background: none;
206+
border: none;
207+
color: var(--color-font-3);
208+
cursor: pointer;
209+
display: inline-flex;
210+
font-size: 1.1rem;
211+
justify-content: center;
212+
padding: 0.25rem;
213+
214+
&:hover {
215+
color: #dc2626;
216+
}
217+
}
218+
</style>

apps/nuxt/app/pages/(articles)/book/[id].vue

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ import { DictType } from '@typewords/core/types/enum.ts'
3030
import { usePracticeWordPersistence } from '@typewords/core/composables/usePracticePersistence.ts'
3131
import { getPracticeArticleCacheLocal } from '@typewords/core/utils/cache.ts'
3232
import { usePlayArticleTextAudio } from '@typewords/core/hooks/article.ts'
33+
import ClickableEnglishText from '@typewords/core/components/word/ClickableEnglishText.vue'
34+
import ClickableWord from '@typewords/core/components/word/ClickableWord.vue'
35+
import WordLookupPopover from '@typewords/core/components/word/WordLookupPopover.vue'
3336
3437
const { t } = useI18n()
3538
@@ -252,6 +255,18 @@ function playArticleQuestionAudio() {
252255
)
253256
}
254257
258+
function playArticleQuoteAudio() {
259+
if (!selectArticle?.quote?.text) return
260+
playArticleTextAudio(
261+
{
262+
text: selectArticle.quote.text,
263+
start: selectArticle.quote.start,
264+
end: selectArticle.quote.end,
265+
},
266+
audioRef
267+
)
268+
}
269+
255270
// 计算段落数量
256271
const paragraphCount = $computed(() => {
257272
if (!selectArticle.text) return 0
@@ -364,7 +379,13 @@ watch(
364379
<div class="flex justify-between items-center relative">
365380
<span>
366381
<span class="inline-flex items-center gap-1">
367-
<span class="text-3xl">{{ selectArticle.title }}</span>
382+
<ClickableEnglishText
383+
class="text-3xl"
384+
:text="selectArticle.title"
385+
word=""
386+
:dictation="false"
387+
:high-light="false"
388+
/>
368389
<VolumeIcon :simple="true" :title="$t('play')" :cb="playArticleTitleAudio" />
369390
</span>
370391
<span class="ml-6 text-2xl" v-if="showTranslate">{{ selectArticle.titleTranslate }}</span>
@@ -397,8 +418,14 @@ watch(
397418
</div>
398419

399420
<div class="mt-2 text-2xl" v-if="selectArticle?.question?.text">
400-
<div class="inline-flex items-center gap-1">
401-
<span>Question: {{ selectArticle?.question?.text }}</span>
421+
<div class="inline-flex items-center gap-1 flex-wrap">
422+
<span>Question:</span>
423+
<ClickableEnglishText
424+
:text="selectArticle?.question?.text"
425+
word=""
426+
:dictation="false"
427+
:high-light="false"
428+
/>
402429
<VolumeIcon :simple="true" :title="$t('play')" :cb="playArticleQuestionAudio" />
403430
</div>
404431
<div
@@ -427,7 +454,8 @@ watch(
427454
v-for="(s, n) in w.split(' ').filter(Boolean)"
428455
:class="`inline-block word-${i}-${j}-${n}`"
429456
:key="`${i}-${j}-${n}`"
430-
><span>{{ s }}</span>
457+
>
458+
<ClickableWord :word="s" />
431459
<span class="space"></span>
432460
</span>
433461
</span>
@@ -444,7 +472,16 @@ watch(
444472
</div>
445473
</template>
446474
<div class="text-right italic">
447-
<div class="text-2xl" v-if="selectArticle?.quote?.text">{{ selectArticle?.quote?.text }}</div>
475+
<div class="inline-flex items-center gap-1 justify-end flex-wrap" v-if="selectArticle?.quote?.text">
476+
<ClickableEnglishText
477+
class="text-2xl"
478+
:text="selectArticle.quote.text"
479+
word=""
480+
:dictation="false"
481+
:high-light="false"
482+
/>
483+
<VolumeIcon :simple="true" :title="$t('play')" :cb="playArticleQuoteAudio" />
484+
</div>
448485
<div
449486
class="trans-row text-xl color-translate-second"
450487
v-if="
@@ -538,6 +575,7 @@ watch(
538575
</div>
539576
</div>
540577
</div>
578+
<WordLookupPopover />
541579
</template>
542580

543581
<style scoped lang="scss">

0 commit comments

Comments
 (0)