feat: 修改 splitEnArticle 方法,修复'Do you always get up so late? It's one o'clock!' 断成两句,然后合并之后,后面的那句为空,但未被删除掉

This commit is contained in:
zyronon
2025-05-19 02:31:51 +08:00
parent 1bac1721ac
commit 8235edb84d
12 changed files with 430 additions and 432 deletions

View File

@@ -8,17 +8,21 @@
"newWords": [],
"textCustomTranslateIsFormat": true,
"useTranslateType": "custom",
"id": "HmlGhw"
"id": "HmlGhw",
"audioSrc": "/public/sound/article/nce2-1/1.mp3",
"lrcPosition": [[14.6,19.15],[19.15,22.03],[22.03,24.59],[24.59,27.26],[27.26,31.65],[31.65,34.43],[34.43,36.98],[36.98,40.36],[40.7,42.47],[40.36,46.59],[46.59,50.65],[50.65,54.57],[55.03,56.84],[57.17,63],[62.98,68.85],[68.85,-1]]
},
{
"title": "Breakfast or lunch?",
"titleTranslate": "早餐还是午餐?",
"text": "It was Sunday. I never get up early on Sundays. I sometimes stay in bed until lunchtime. Last Sunday I got up very late. I looked out of the window. It was dark outside. 'What a day!' I thought. 'It's raining again.' Just then, the telephone rang. It was my aunt Lucy. 'I've just arrived by train,' she said. 'I'm coming to see you.'\n 'But I'm still having breakfast,' I said.\n 'What are you doing?' she asked.\n 'I'm having breakfast,' I repeated.\n 'Dear me,' she said. 'Do you always get up so late? It's one o'clock!'",
"textCustomTranslate": "那是个星期天,\n而在星期天我是从来不早起的\n有时我要一直躺到吃午饭的时候。\n上个星期天我起得很晚。\n我望望窗外外面一片昏暗。\n“鬼天气\n我想,“又下雨了。”\n正在这时,电话铃响了。\n是我姑母露西打来的。\n“我刚下火车”她说\n“我这就来看你。”\n\n“但我还在吃早饭”我说。\n\n“你在干什么”她问道。\n\n“我正在吃早饭”我又说了一遍。\n\n“天啊”她说\n“你总是起得这么晚吗",
"text": "It was Sunday. I never get up early on Sundays. I sometimes stay in bed until lunchtime. Last Sunday I got up very late. I looked out of the window. It was dark outside. 'What a day!' I thought. 'It's raining again.' Just then, the telephone rang. It was my aunt Lucy. 'I've just arrived by train,' she said. 'I'm coming to see you.'\n\n 'But I'm still having breakfast,' I said.\n\n 'What are you doing?' she asked.\n\n 'I'm having breakfast,' I repeated.\n\n 'Dear me,' she said. 'Do you always get up so late? It's one o'clock!'",
"textCustomTranslate": "那是个星期天,\n而在星期天我是从来不早起的\n有时我要一直躺到吃午饭的时候。\n上个星期天我起得很晚。\n我望望窗外\n外面一片昏暗。\n“鬼天气”我想\n“又下雨了。”正在这时,电话铃响了。\n是我姑母露西打来的。\n“我刚下火车”她说\n“我这就来看你。”\n\n“但我还在吃早饭”我说。\n\n“你在干什么”她问道。\n\n“我正在吃早饭”我又说了一遍。\n\n“天啊”她说\n“你总是起得这么晚吗现在已经1点钟了",
"textNetworkTranslate": "",
"textCustomTranslateIsFormat": true,
"useTranslateType": "custom",
"newWords": [],
"audioSrc": "/public/sound/article/nce2-1/2.mp3",
"lrcPosition": [],
"id": "1ao0Qx"
},
{

View File

@@ -0,0 +1,18 @@
[ti:<3A>¸<EFBFBD><C2B8><EFBFBD>Ӣ<EFBFBD><D3A2><EFBFBD>ڶ<EFBFBD><DAB6><EFBFBD>]
[by:http://www.TingClass.com]
[00:02.77]Lesson 2
[00:04.08] Breakfast or lunch?
[00:06.06]First listen and then answer the question:
[00:10.85]Why was the writer&#39;s aunt surprised?
[00:16.35]It was Sunday.I never get up early on Sundays.
[00:21.54]I sometimes stay in bed until lunchtime.
[00:27.48]Last Sunday I got up very late.
[00:31.76]I looked out of the window. It was dark outside.
[00:37.01]&#39;What a day!&#39;I thought. &#39;It&#39;s raining again.&#39;
[00:43.20]Just then,the telephone rang.It was my aunt Lucy.
[00:49.33]&#39;I&#39;ve just arrived by train,&#39; she said.&#39;I&#39;m coming to see you.&#39;
[00:56.43]&#39;But I&#39;m still having breakfast,&#39;I said. &#39;What are you doing? she asked.
[01:04.20]&#39;I&#39;m having breakfast,&#39;I repeated.
[01:08.00]&#39;Dear me,&#39;she said.
[01:10.88] &#39;Do you always get up so late? It&#39;s one o&#39;clock!&#39;

Binary file not shown.

View File

@@ -50,6 +50,7 @@ export function splitEnArticle(text: string): { sections: Sentence[][], newText:
let sentenceNlpList = []
// console.log('ss', sentenceNlpList)
doc.json().map(item => {
//如果整句大于15个单词以上检测是否有 逗号子句
if (item.terms.length > 15) {
//正则匹配“逗号加and|but|so|because"
@@ -71,13 +72,14 @@ export function splitEnArticle(text: string): { sections: Sentence[][], newText:
})
sentenceNlpList.map(item => {
let sentence: Sentence = {
let sentence: Sentence = cloneDeep({
//他没有空格,导致修改一行一行的数据时,汇总时全没有空格了,库无法正常断句
text: item.text + ' ',
// text: '',
translate: '',
words: [],
}
audioPosition: [],
})
section.push(sentence)
const checkQuote = (pre: string, index?: number) => {
@@ -126,6 +128,7 @@ export function splitEnArticle(text: string): { sections: Sentence[][], newText:
lastSentence.words = lastSentence.words.concat(sentence.words)
lastSentence.words.push(word3)
sentence.words = []
//这里还不能直接删除sentence因为后面还有一个 sentence.words = sentence.words.filter(v => v.word !== 'placeholder') 的判断
// section.pop()
}
}
@@ -218,6 +221,10 @@ export function splitEnArticle(text: string): { sections: Sentence[][], newText:
//去除空格占位符
sentence.words = sentence.words.filter(v => v.word !== 'placeholder')
//如果是空的,直接去掉
if (!sentence.words.length) {
section.pop()
}
})
// console.log(sentenceNlpList)

View File

@@ -197,4 +197,11 @@ export function renewSectionTexts(article: Article) {
let {newText, sections} = splitEnArticle(article.text)
article.text = newText
article.sections = sections
if (article.lrcPosition.length) {
article.sections.map((v, i) => {
v.map((w, j) => {
w.audioPosition = article.lrcPosition[(i * (article.sections[i - 1]||[]).length) + j]
})
})
}
}

View File

@@ -1,222 +0,0 @@
<script setup lang="ts">
import {onMounted, onUnmounted} from "vue"
import {usePracticeStore} from "@/stores/practice.ts";
import {useSettingStore} from "@/stores/setting.ts";
import {Article, ArticleWord, ShortcutKey, Word} from "@/types.ts";
import {Icon} from "@iconify/vue";
import VolumeSetting from "@/pages/pc/components/toolbar/VolumeSetting.vue";
import TranslateSetting from "@/pages/pc/components/toolbar/TranslateSetting.vue";
import Tooltip from "@/pages/pc/components/Tooltip.vue";
import BaseIcon from "@/components/BaseIcon.vue";
import IconWrapper from "@/pages/pc/components/IconWrapper.vue";
import {useArticleOptions} from "@/hooks/dict.ts";
import audio from '/public/sound/article/nce2-1/1.mp3'
import {emitter} from "@/utils/eventBus.ts";
const statisticsStore = usePracticeStore()
const settingStore = useSettingStore()
const {
isArticleCollect,
toggleArticleCollect
} = useArticleOptions()
const emit = defineEmits<{
ignore: [],
wrong: [val: Word],
nextWord: [val: ArticleWord],
over: [],
edit: [val: Article]
}>()
function format(val: number, suffix: string = '', check: number = -1) {
return val === check ? '-' : (val + suffix)
}
const progress = $computed(() => {
if (!statisticsStore.total) return 0
if (statisticsStore.index > statisticsStore.total) return 100
return ((statisticsStore.index / statisticsStore.total) * 100)
})
let speedMinute = $ref(0)
let timer = $ref(0)
onMounted(() => {
timer = setInterval(() => {
speedMinute = Math.floor((Date.now() - statisticsStore.startDate) / 1000 / 60)
}, 1000)
})
onUnmounted(() => {
timer && clearInterval(timer)
})
</script>
<template>
<div class="footer " :class="!settingStore.showToolbar && 'hide'">
<div class="bottom">
<div class="flex justify-between">
<div>
<el-progress
class="flex-1"
:percentage="progress"
:stroke-width="8"
:show-text="false"/>
<div class="stat">
<div class="row">
<div class="num">{{ speedMinute }}分钟</div>
<div class="line"></div>
<div class="name">时间</div>
</div>
<div class="row">
<div class="num">{{ statisticsStore.total }}</div>
<div class="line"></div>
<div class="name">单词总数</div>
</div>
<div class="row">
<div class="num">{{ format(statisticsStore.inputWordNumber, '', 0) }}</div>
<div class="line"></div>
<div class="name">输入数</div>
</div>
<div class="row">
<div class="num">{{ format(statisticsStore.wrong, '', 0) }}</div>
<div class="line"></div>
<div class="name">错误数</div>
</div>
<div class="row">
<div class="num">{{ format(statisticsStore.correctRate, '%') }}</div>
<div class="line"></div>
<div class="name">正确率</div>
</div>
</div>
</div>
<div>
<audio :src="audio" controls></audio>
<div class="flex gap-3 center">
<Tooltip
:title="`开关默写模式(${settingStore.shortcutKeyMap[ShortcutKey.ToggleDictation]})`"
>
<IconWrapper>
<Icon icon="majesticons:eye-off-line"
v-if="settingStore.dictation"
@click="settingStore.dictation = false"/>
<Icon icon="mdi:eye-outline"
v-else
@click="settingStore.dictation = true"/>
</IconWrapper>
</Tooltip>
<TranslateSetting/>
<VolumeSetting/>
<BaseIcon
:title="`编辑(${settingStore.shortcutKeyMap[ShortcutKey.EditArticle]})`"
icon="tabler:edit"
@click="emitter.emit(ShortcutKey.EditArticle)"
/>
<BaseIcon
v-if="!isArticleCollect()"
class="collect"
@click="toggleArticleCollect()"
:title="`收藏(${settingStore.shortcutKeyMap[ShortcutKey.ToggleCollect]})`"
icon="ph:star"/>
<BaseIcon
v-else
class="fill"
@click="toggleArticleCollect()"
:title="`取消收藏(${settingStore.shortcutKeyMap[ShortcutKey.ToggleCollect]})`"
icon="ph:star-fill"/>
<BaseIcon
:title="`下一句(${settingStore.shortcutKeyMap[ShortcutKey.Next]})`"
icon="icon-park-outline:go-ahead"
@click="emit('over')"/>
<BaseIcon
@click="settingStore.showPanel = !settingStore.showPanel"
:title="`面板(${settingStore.shortcutKeyMap[ShortcutKey.TogglePanel]})`"
icon="tdesign:menu-unfold"/>
</div>
</div>
</div>
</div>
<div class="progress">
<el-progress :percentage="progress"
:stroke-width="8"
:show-text="false"/>
</div>
</div>
</template>
<style scoped lang="scss">
@import "@/assets/css/variable";
.footer {
width: var(--article-width);
margin-bottom: .8rem;
transition: all var(--anim-time);
position: relative;
margin-top: 1rem;
&.hide {
margin-bottom: -6rem;
margin-top: 3rem;
.progress {
bottom: calc(100% + 1.8rem);
}
}
.bottom {
position: relative;
width: 100%;
box-sizing: border-box;
border-radius: .6rem;
background: var(--color-second-bg);
padding: .2rem var(--space) .4rem var(--space);
z-index: 2;
border: 1px solid var(--color-item-border);
box-shadow: var(--shadow);
.stat {
margin-top: .5rem;
display: flex;
justify-content: space-around;
gap: 2rem;
.row {
display: flex;
flex-direction: column;
align-items: center;
gap: .3rem;
width: 5rem;
color: gray;
.line {
height: 1px;
width: 100%;
background: var(--color-sub-gray);
}
}
}
}
.progress {
width: 100%;
transition: all .3s;
padding: 0 .6rem;
box-sizing: border-box;
position: absolute;
bottom: 0;
}
:deep(.el-progress-bar__inner) {
background: var(--color-scrollbar);
}
}
</style>

View File

@@ -137,27 +137,10 @@ useStartKeyboardEventListener()
</script>
<template>
<div class="practice-wrapper">
<PracticeArticle ref="practiceRef"/>
<ArticleFooter/>
</div>
<PracticeArticle ref="practiceRef"/>
<DictModal/>
<Statistics/>
</template>
<style scoped lang="scss">
.practice-wrapper {
font-size: 0.9rem;
width: 100%;
height: 100vh;
display: flex;
overflow: hidden;
flex-direction: column;
justify-content: space-between;
align-items: center;
//padding-right: var(--practice-wrapper-padding-right);
transform: translateX(var(--practice-wrapper-translateX));
}
</style>

View File

@@ -14,7 +14,7 @@ import {
import {MessageBox} from "@/utils/MessageBox.tsx";
import {getSplitTranslateText} from "@/hooks/article.ts";
import {cloneDeep} from "lodash-es";
import {cloneDeep, last} from "lodash-es";
import {watch, ref} from "vue";
import Empty from "@/components/Empty.vue";
import {UploadProps, UploadUserFile} from "element-plus";
@@ -72,7 +72,7 @@ watch(() => props.article, val => {
}
}
renewSections()
// console.log('ar', article)
console.log('ar', editArticle)
}, {immediate: true})
watch(() => editArticle.text, (s) => {
@@ -238,86 +238,63 @@ function save(option: 'save' | 'saveAndNext') {
//不知道为什么直接用editArticle取到是空的默认值
defineExpose({save, getEditArticle: () => cloneDeep(editArticle)})
const fileList = ref<UploadUserFile[]>([])
const handleExceed: UploadProps['onExceed'] = (files, uploadFiles) => {
ElMessage.warning(
`The limit is 3, you selected ${files.length} files this time, add up to ${
files.length + uploadFiles.length
} totally`
)
}
const handleChange: UploadProps['onChange'] = (uploadFile, uploadFiles) => {
console.log(1)
fileList.value.push({
name: uploadFile.name,
url: uploadFile.url,
})
}
function test() {
let lrc = `[00:00.00]Lesson 1 A Private Conversation
[00:04.35]First listen and then answer the question.
[00:09.26]Why did the writer complain to the people behind him?
[00:14.60]Last week I went to the theatre.
[00:19.15]I had a very good seat.
[00:22.03]The play was very interesting.
[00:24.59]I did not enjoy it.
[00:27.26]A young man and a young woman were sitting behind me.
[00:31.65]They were talking loudly.
[00:34.43]I got very angry.
[00:36.98]I could not hear the actors.
[00:40.36]I turned round.I looked at the man and the woman angrily.
[00:46.59]They did not pay any attention.
[00:50.65]In the end,I could not bear it.
[00:54.57]I turned round again 'I can't hear a word!'I said angrily
[01:02.98]'It's none of your business,'the young man said rudely.
[01:08.85]'This is a private conversation!'
`
let lrcList = _parseLRC(lrc)
console.log(lrcList)
editArticle.sections.map((v, i) => {
v.map((w, j) => {
console.log(w)
for (let k = 0; k < lrcList.length; k++) {
let s = lrcList[k]
let d = Comparison.default.cosine.similarity(w.text, s.text)
d = Comparison.default.levenshtein.similarity(w.text, s.text)
d = Comparison.default.longestCommonSubsequence.similarity(w.text, s.text)
// d = Comparison.default.metricLcs.similarity(w.text, s.text)
console.log(w.text, s.text, d)
if (d >= 0.8) {
w.audioPosition = [s.start, s.end ?? -1]
w.test = s.text
break
}
console.log(uploadFile)
let reader = new FileReader();
reader.readAsText(uploadFile.raw, 'UTF-8');
reader.onload = function (e) {
let lrc: string = e.target.result as string;
console.log(lrc)
if (lrc.trim()) {
let lrcList = _parseLRC(lrc)
console.log('lrcList', lrcList)
if (lrcList.length) {
editArticle.lrcPosition = editArticle.sections.map((v, i) => {
return v.map((w, j) => {
for (let k = 0; k < lrcList.length; k++) {
let s = lrcList[k]
let d = Comparison.default.cosine.similarity(w.text, s.text)
d = Comparison.default.levenshtein.similarity(w.text, s.text)
d = Comparison.default.longestCommonSubsequence.similarity(w.text, s.text)
// d = Comparison.default.metricLcs.similarity(w.text, s.text)
// console.log(w.text, s.text, d)
if (d >= 0.8) {
w.audioPosition = [s.start, s.end ?? -1]
break
}
}
return w.audioPosition ?? []
})
}).flat()
}
})
})
console.log(editArticle.sections.flat())
// console.log(cosine.similarity('Thanos', 'Rival'))
}
function s() {
}
function confirm() {
}
}
}
let currentSentence = $ref<Sentence>({} as any)
let editSentence = $ref<Sentence>({} as any)
let showEditAudioDialog = $ref(false)
let sentenceAudioRef = $ref<HTMLAudioElement>()
let audioRef = $ref<HTMLAudioElement>()
function handleShowEditAudioDialog(val: Sentence) {
function handleShowEditAudioDialog(val: Sentence, i: number, j: number) {
showEditAudioDialog = true
currentSentence = val
if (!currentSentence.audioPosition?.length) {
currentSentence.audioPosition = [0, 0]
editSentence = cloneDeep(val)
let pre = null
if (j == 0) {
if (i != 0) {
pre = last(editArticle.sections[i - 1])
}
} else {
pre = editArticle.sections[i][j - 1]
}
if (!editSentence.audioPosition?.length) {
editSentence.audioPosition = [0, 0]
if (pre) {
editSentence.audioPosition = [pre.audioPosition[1] ?? 0, 0]
}
}
}
@@ -325,14 +302,14 @@ function recordStart() {
if (sentenceAudioRef.paused) {
sentenceAudioRef.play()
}
currentSentence.audioPosition[0] = Number(sentenceAudioRef.currentTime.toFixed(2))
editSentence.audioPosition[0] = Number(sentenceAudioRef.currentTime.toFixed(2))
}
function recordEnd() {
if (!sentenceAudioRef.paused) {
sentenceAudioRef.pause()
}
currentSentence.audioPosition[1] = Number(sentenceAudioRef.currentTime.toFixed(2))
editSentence.audioPosition[1] = Number(sentenceAudioRef.currentTime.toFixed(2))
}
let timer = -1
@@ -354,6 +331,12 @@ function playSentenceAudio(sentence: Sentence, ref?: HTMLAudioElement) {
}
}
}
function saveLrcPosition() {
// showEditAudioDialog = false
currentSentence.audioPosition = cloneDeep(editSentence.audioPosition)
editArticle.lrcPosition = editArticle.sections.map((v, i) => v.map((w, j) => (w.audioPosition ?? []))).flat()
}
</script>
<template>
@@ -455,21 +438,15 @@ function playSentenceAudio(sentence: Sentence, ref?: HTMLAudioElement) {
<div class="center">正文译文与结果均可编辑修改一处另外两处会自动同步变动</div>
<div class="flex gap-2">
<BaseButton>添加音频</BaseButton>
<BaseButton @click="test">添加音频LRC</BaseButton>
<el-upload
v-model:file-list="fileList"
class="upload-demo"
:limit="1"
:on-change="handleChange"
:auto-upload="false"
>
<el-button type="primary">Click to upload</el-button>
<template #tip>
<div class="el-upload__tip">
jpg/png files with a size less than 500KB.
</div>
</template>
<el-button type="primary">添加音频LRC文件</el-button>
</el-upload>
<audio ref="audioRef" :src="audio" controls></audio>
<audio ref="audioRef" :src="editArticle.audioSrc" controls></audio>
</div>
<template v-if="editArticle.sections.length">
<div class="flex-1 overflow-auto flex flex-col">
@@ -499,10 +476,11 @@ function playSentenceAudio(sentence: Sentence, ref?: HTMLAudioElement) {
<div class="text-base" v-if="sentence.audioPosition?.length">
<span>{{ sentence.audioPosition?.[0] }}s</span>
<span v-if="sentence.audioPosition?.[1] !== -1"> - {{ sentence.audioPosition?.[1] }}s</span>
<span v-else> - 结束</span>
</div>
<div>
<BaseIcon :icon="sentence.audioPosition?.length ? 'basil:edit-outline' : 'basil:add-outline'"
@click="handleShowEditAudioDialog(sentence)"/>
@click="handleShowEditAudioDialog(sentence,indexI,indexJ)"/>
<BaseIcon v-if="sentence.audioPosition?.length" icon="hugeicons:play"
@click="playSentenceAudio(sentence,audioRef)"/>
</div>
@@ -532,43 +510,58 @@ function playSentenceAudio(sentence: Sentence, ref?: HTMLAudioElement) {
</template>
<Empty v-else text="没有译文对照~"/>
</div>
<Dialog title="音频"
<Dialog title="设置音频与句子的对应位置(LRC)"
v-model="showEditAudioDialog"
:footer="true"
@close="showEditAudioDialog = false"
@ok="saveLrcPosition"
>
<div class="p-4 pt-0 color-black w-150 flex flex-col gap-2">
<div>
句子{{ currentSentence.text }}
句子{{ editSentence.text }}
</div>
<audio ref="sentenceAudioRef" :src="audio" controls class="w-full"></audio>
<audio ref="sentenceAudioRef" :src="editArticle.audioSrc" controls class="w-full"></audio>
<div class="">
使用方法点击上方播放按钮音频播放到句子开始时点击 记录开始时间 按钮播放到句子结束时点击 记录开始时间 按钮
使用方法点击上方播放按钮音频播放到当前句子开始时点击开始时间 <span class="color-red">记录</span>
按钮当播放到句子结束时点击结束时间的 <span class="color-red">记录</span> 按钮
</div>
<div class="flex items-center gap-4">
<div class="flex flex-col gap-2">
<div class="flex gap-2 items-center">
<BaseButton @click="recordStart">记录开始时间</BaseButton>
<div class="flex flex-col gap-2">
<div class="flex gap-2 items-center">
<div>开始时间</div>
<div class="flex space-between flex-1">
<div class="flex items-center gap-1">
<el-input-number v-model="currentSentence.audioPosition[0]" :precision="2" :step="0.1">
<template #suffix>
<span>s</span>
</template>
</el-input-number>
</div>
</div>
<div class="flex gap-2 items-center">
<BaseButton @click="recordEnd">记录结束时间</BaseButton>
<div class="flex items-center gap-1">
<el-input-number v-model="currentSentence.audioPosition[1]" :precision="2" :step="0.1">
<el-input-number v-model="editSentence.audioPosition[0]" :precision="2" :step="0.1">
<template #suffix>
<span>s</span>
</template>
</el-input-number>
</div>
<BaseButton @click="recordStart">记录</BaseButton>
</div>
</div>
<BaseButton @click="playSentenceAudio(currentSentence,audioRef)">播放记录时间</BaseButton>
<div class="flex gap-2 items-center">
<div>结束时间</div>
<div class="flex space-between flex-1">
<div class="flex items-center gap-1">
<el-input-number v-model="editSentence.audioPosition[1]" :precision="2" :step="0.1">
<template #suffix>
<span>s</span>
</template>
</el-input-number>
<span></span>
<BaseButton size="small" @click="editSentence.audioPosition[1] = -1">结束</BaseButton>
</div>
<BaseButton @click="recordEnd">记录</BaseButton>
</div>
</div>
</div>
<div class="flex items-center gap-2" v-if="editSentence.audioPosition?.length">
<div>
lrc<span>{{ editSentence.audioPosition?.[0] }}s</span>
<span v-if="editSentence.audioPosition?.[1] !== -1"> - {{ editSentence.audioPosition?.[1] }}s</span>
<span v-else> - 结束</span>
</div>
<BaseButton @click="playSentenceAudio(editSentence,audioRef)">试听</BaseButton>
</div>
</div>
</Dialog>

View File

@@ -464,7 +464,7 @@ defineExpose({showSentence, play, del, hideSentence, nextSentence})
<div class="text-2xl center">{{ props.article.titleTranslate }}</div>
</header>
<template v-if="getTranslateText(article).length">
<div class="text-xl" v-for="t in getTranslateText(article)">{{ t }}</div>
<div class="text-xl mb-4" v-for="t in getTranslateText(article)">{{ t }}</div>
</template>
</div>
</div>
@@ -510,7 +510,7 @@ defineExpose({showSentence, play, del, hideSentence, nextSentence})
article {
font-size: 1.6rem;
line-height: 1.5;
line-height: 1.3;
word-break: keep-all;
word-wrap: break-word;
white-space: pre-wrap;

View File

@@ -13,7 +13,7 @@ import {
import {cloneDeep} from "lodash-es";
import TypingWord from "@/pages/pc/components/TypingWord.vue";
import Panel from "../../components/Panel.vue";
import {onMounted, onUnmounted, watch} from "vue";
import {onMounted, onUnmounted} from "vue";
import {renewSectionTexts, renewSectionTranslates} from "@/hooks/translate.ts";
import {MessageBox} from "@/utils/MessageBox.tsx";
import {useBaseStore} from "@/stores/base.ts";
@@ -26,9 +26,11 @@ import Tooltip from "@/pages/pc/components/Tooltip.vue";
import {useRuntimeStore} from "@/stores/runtime.ts";
import {useSettingStore} from "@/stores/setting.ts";
import BaseIcon from "@/components/BaseIcon.vue";
import {syncMyDictList, useArticleOptions} from "@/hooks/dict.ts";
import {useArticleOptions} from "@/hooks/dict.ts";
import ArticleList from "@/pages/pc/components/list/ArticleList.vue";
import {useOnKeyboardEventListener} from "@/hooks/event.ts";
import VolumeSetting from "@/pages/pc/components/toolbar/VolumeSetting.vue";
import TranslateSetting from "@/pages/pc/components/toolbar/TranslateSetting.vue";
const store = useBaseStore()
const statisticsStore = usePracticeStore()
@@ -161,7 +163,7 @@ function getCurrentPractice() {
}
function saveArticle(val: Article) {
console.log('saveArticle', val)
console.log('saveArticle', val, JSON.stringify(val.lrcPosition))
showEditArticle = false
let rIndex = store.currentArticleDict.articles.findIndex(v => v.id === val.id)
if (rIndex > -1) {
@@ -300,101 +302,241 @@ useEvents([
defineExpose({getCurrentPractice})
const emit = defineEmits<{
ignore: [],
wrong: [val: Word],
nextWord: [val: ArticleWord],
over: [],
edit: [val: Article]
}>()
function format(val: number, suffix: string = '', check: number = -1) {
return val === check ? '-' : (val + suffix)
}
const progress = $computed(() => {
if (!statisticsStore.total) return 0
if (statisticsStore.index > statisticsStore.total) return 100
return ((statisticsStore.index / statisticsStore.total) * 100)
})
let speedMinute = $ref(0)
let timer = $ref(0)
onMounted(() => {
timer = setInterval(() => {
speedMinute = Math.floor((Date.now() - statisticsStore.startDate) / 1000 / 60)
}, 1000)
})
onUnmounted(() => {
timer && clearInterval(timer)
})
</script>
<template>
<div class="practice-article">
<div class="swiper-wrapper">
<div class="swiper-list" :class="`step${tabIndex}`">
<div class="swiper-item">
<TypingArticle
ref="typingArticleRef"
:active="tabIndex === 0"
@edit="edit"
@wrong="wrong"
@over="skip"
@nextWord="nextWord"
:article="articleData.article"
/>
</div>
<div class="swiper-item">
<div class="typing-word-wrapper">
<TypingWord
@sort="sort"
:words="wordData.words"
:index="wordData.index"
v-if="tabIndex === 1"
<div class="practice-wrapper">
<div class="practice-article">
<div class="swiper-wrapper">
<div class="swiper-list" :class="`step${tabIndex}`">
<div class="swiper-item">
<TypingArticle
ref="typingArticleRef"
:active="tabIndex === 0"
@edit="edit"
@wrong="wrong"
@over="skip"
@nextWord="nextWord"
:article="articleData.article"
/>
</div>
<div class="swiper-item">
<div class="typing-word-wrapper">
<TypingWord
@sort="sort"
:words="wordData.words"
:index="wordData.index"
v-if="tabIndex === 1"
/>
</div>
</div>
</div>
</div>
</div>
<Teleport to="body">
<div class="panel-wrapper">
<Panel v-if="tabIndex === 0">
<template v-slot="{active}">
<div class="panel-page-item">
<div class="list-header">
<div class="left">
<BaseIcon title="切换词典"
@click="emitter.emit(EventKey.openDictModal,'list')"
icon="carbon:change-catalog"/>
<div class="title">
{{ store.currentArticleDict.name }}
<Teleport to="body">
<div class="panel-wrapper">
<Panel v-if="tabIndex === 0">
<template v-slot="{active}">
<div class="panel-page-item">
<div class="list-header">
<div class="left">
<BaseIcon title="切换词典"
@click="emitter.emit(EventKey.openDictModal,'list')"
icon="carbon:change-catalog"/>
<div class="title">
{{ store.currentArticleDict.name }}
</div>
<Tooltip
:title="`下一章(${settingStore.shortcutKeyMap[ShortcutKey.NextChapter]})`"
v-if="store.currentArticleDict.lastLearnIndex < articleData.articles.length - 1">
<IconWrapper>
<Icon @click="emitter.emit(EventKey.next)" icon="octicon:arrow-right-24"/>
</IconWrapper>
</Tooltip>
</div>
<div class="right">
{{ articleData.articles.length }}篇文章
</div>
<Tooltip
:title="`下一章(${settingStore.shortcutKeyMap[ShortcutKey.NextChapter]})`"
v-if="store.currentArticleDict.lastLearnIndex < articleData.articles.length - 1">
<IconWrapper>
<Icon @click="emitter.emit(EventKey.next)" icon="octicon:arrow-right-24"/>
</IconWrapper>
</Tooltip>
</div>
<div class="right">
{{ articleData.articles.length }}篇文章
</div>
<ArticleList
:isActive="active"
:static="false"
:show-border="true"
:show-translate="settingStore.translate"
@title="e => emitter.emit(EventKey.openArticleContentModal,e.item)"
@click="handleChangeChapterIndex"
:active-id="articleData.article.id"
:list="articleData.articles ">
<template v-slot:suffix="{item,index}">
<BaseIcon
v-if="!isArticleCollect(item)"
class="collect"
@click="toggleArticleCollect(item)"
title="收藏" icon="ph:star"/>
<BaseIcon
v-else
class="fill"
@click="toggleArticleCollect(item)"
title="取消收藏" icon="ph:star-fill"/>
</template>
</ArticleList>
</div>
</template>
</Panel>
</div>
</Teleport>
<ArticleList
:isActive="active"
:static="false"
:show-border="true"
:show-translate="settingStore.translate"
@title="e => emitter.emit(EventKey.openArticleContentModal,e.item)"
@click="handleChangeChapterIndex"
:active-id="articleData.article.id"
:list="articleData.articles ">
<template v-slot:suffix="{item,index}">
<BaseIcon
v-if="!isArticleCollect(item)"
class="collect"
@click="toggleArticleCollect(item)"
title="收藏" icon="ph:star"/>
<BaseIcon
v-else
class="fill"
@click="toggleArticleCollect(item)"
title="取消收藏" icon="ph:star-fill"/>
</template>
</ArticleList>
<EditSingleArticleModal
v-model="showEditArticle"
:article="editArticle"
@save="saveArticle"
/>
</div>
<div class="footer " :class="!settingStore.showToolbar && 'hide'">
<div class="bottom">
<div class="flex justify-between">
<div>
<el-progress
class="flex-1"
:percentage="progress"
:stroke-width="8"
:show-text="false"/>
<div class="stat">
<div class="row">
<div class="num">{{ speedMinute }}分钟</div>
<div class="line"></div>
<div class="name">时间</div>
</div>
<div class="row">
<div class="num">{{ statisticsStore.total }}</div>
<div class="line"></div>
<div class="name">单词总数</div>
</div>
<div class="row">
<div class="num">{{ format(statisticsStore.inputWordNumber, '', 0) }}</div>
<div class="line"></div>
<div class="name">输入数</div>
</div>
<div class="row">
<div class="num">{{ format(statisticsStore.wrong, '', 0) }}</div>
<div class="line"></div>
<div class="name">错误数</div>
</div>
<div class="row">
<div class="num">{{ format(statisticsStore.correctRate, '%') }}</div>
<div class="line"></div>
<div class="name">正确率</div>
</div>
</div>
</template>
</Panel>
</div>
</Teleport>
</div>
<div>
<audio :src="articleData.article.audioSrc" controls></audio>
<div class="flex gap-3 center">
<Tooltip
:title="`开关默写模式(${settingStore.shortcutKeyMap[ShortcutKey.ToggleDictation]})`"
>
<IconWrapper>
<Icon icon="majesticons:eye-off-line"
v-if="settingStore.dictation"
@click="settingStore.dictation = false"/>
<Icon icon="mdi:eye-outline"
v-else
@click="settingStore.dictation = true"/>
</IconWrapper>
</Tooltip>
<EditSingleArticleModal
v-model="showEditArticle"
:article="editArticle"
@save="saveArticle"
/>
<TranslateSetting/>
<VolumeSetting/>
<BaseIcon
:title="`编辑(${settingStore.shortcutKeyMap[ShortcutKey.EditArticle]})`"
icon="tabler:edit"
@click="emitter.emit(ShortcutKey.EditArticle)"
/>
<BaseIcon
v-if="!isArticleCollect()"
class="collect"
@click="toggleArticleCollect()"
:title="`收藏(${settingStore.shortcutKeyMap[ShortcutKey.ToggleCollect]})`"
icon="ph:star"/>
<BaseIcon
v-else
class="fill"
@click="toggleArticleCollect()"
:title="`取消收藏(${settingStore.shortcutKeyMap[ShortcutKey.ToggleCollect]})`"
icon="ph:star-fill"/>
<BaseIcon
:title="`下一句(${settingStore.shortcutKeyMap[ShortcutKey.Next]})`"
icon="icon-park-outline:go-ahead"
@click="emit('over')"/>
<BaseIcon
@click="settingStore.showPanel = !settingStore.showPanel"
:title="`面板(${settingStore.shortcutKeyMap[ShortcutKey.TogglePanel]})`"
icon="tdesign:menu-unfold"/>
</div>
</div>
</div>
</div>
<div class="progress">
<el-progress :percentage="progress"
:stroke-width="8"
:show-text="false"/>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
@import "@/assets/css/style";
.practice-wrapper {
font-size: 0.9rem;
width: 100%;
height: 100vh;
display: flex;
overflow: hidden;
flex-direction: column;
justify-content: space-between;
align-items: center;
//padding-right: var(--practice-wrapper-padding-right);
transform: translateX(var(--practice-wrapper-translateX));
}
.swiper-wrapper {
height: 100%;
overflow: hidden;
@@ -435,4 +577,69 @@ defineExpose({getCurrentPractice})
height: calc(100% - 1.2rem);
}
.footer {
width: var(--article-width);
margin-bottom: .8rem;
transition: all var(--anim-time);
position: relative;
margin-top: 1rem;
&.hide {
margin-bottom: -6rem;
margin-top: 3rem;
.progress {
bottom: calc(100% + 1.8rem);
}
}
.bottom {
position: relative;
width: 100%;
box-sizing: border-box;
border-radius: .6rem;
background: var(--color-second-bg);
padding: .2rem var(--space) .4rem var(--space);
z-index: 2;
border: 1px solid var(--color-item-border);
box-shadow: var(--shadow);
.stat {
margin-top: .5rem;
display: flex;
justify-content: space-around;
gap: 2rem;
.row {
display: flex;
flex-direction: column;
align-items: center;
gap: .3rem;
width: 5rem;
color: gray;
.line {
height: 1px;
width: 100%;
background: var(--color-sub-gray);
}
}
}
}
.progress {
width: 100%;
transition: all .3s;
padding: 0 .6rem;
box-sizing: border-box;
position: absolute;
bottom: 0;
}
:deep(.el-progress-bar__inner) {
background: var(--color-scrollbar);
}
}
</style>

View File

@@ -118,7 +118,6 @@ export const DefaultBaseState = (): BaseState => ({
index: 5, name: '已掌握', type: DictType.master, words: [], statistics: []
},
],
articleDictList: [
{
...getDefaultDict(),
@@ -165,7 +164,6 @@ export const DefaultBaseState = (): BaseState => ({
dictIndex: 0,
},
},
myDictList: [
{
...getDefaultDict(),
@@ -337,7 +335,6 @@ export const useBaseStore = defineStore('base', {
}))
}
console.log('this.currentArticleDict', this.currentArticleDict.articles[0])
}
emitter.emit(EventKey.changeDict)
resolve(true)

View File

@@ -115,7 +115,9 @@ export interface Article {
newWords: Word[],
textAllWords: string[],
sections: Sentence[][],
useTranslateType: TranslateType
useTranslateType: TranslateType,
audioSrc: string,
lrcPosition: number[][],
}
export const DefaultArticle: Article = {
@@ -130,7 +132,9 @@ export const DefaultArticle: Article = {
newWords: [],
textAllWords: [],
sections: [],
useTranslateType: TranslateType.custom
useTranslateType: TranslateType.custom,
audioSrc: '',
lrcPosition: [],
}
export interface Statistics {