feat: 添加音频
This commit is contained in:
@@ -79,7 +79,7 @@ export function splitEnArticle(text: string): { sections: Sentence[][], newText:
|
||||
// text: '',
|
||||
translate: '',
|
||||
words: [],
|
||||
audioPosition: [],
|
||||
audioPosition: [0, 0],
|
||||
})
|
||||
section.push(sentence)
|
||||
|
||||
@@ -215,7 +215,7 @@ export function splitEnArticle(text: string): { sections: Sentence[][], newText:
|
||||
let post: string = v.post
|
||||
//判断是不是等于空,因为正常的词后面都会有个空格。这种不需要处理。
|
||||
if (post && post !== ' ') {
|
||||
checkSymbol(post)
|
||||
checkSymbol(post.trim())
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -258,33 +258,34 @@ export function splitCNArticle(text: string): Sentence[][] {
|
||||
//去除头和尾部的空格
|
||||
text = text.trim()
|
||||
let sections: Sentence[][] = []
|
||||
text && text.split('\n').map((rowSection, i) => {
|
||||
text && text.split('\n\n').map((rowSection, i) => {
|
||||
let section: Sentence[] = []
|
||||
sections.push(section)
|
||||
rowSection = rowSection.trim()
|
||||
let sentences = split(rowSection)
|
||||
sentences.map(sentenceRow => {
|
||||
let row = sentenceRow.raw
|
||||
let sentence: Sentence = {
|
||||
text: row,
|
||||
// text: '',
|
||||
translate: '',
|
||||
words: []
|
||||
}
|
||||
section.push(sentence)
|
||||
// console.log('LICENSE', )
|
||||
if (row) {
|
||||
//这个库总是会把反引号给断句到下一行
|
||||
if (row[0] === "”") {
|
||||
sentence.text = row.substr(1)
|
||||
let lastSentence = section[section.length - 2]
|
||||
lastSentence.text += "”"
|
||||
if (!sentence.text) {
|
||||
section.pop()
|
||||
let list = rowSection.trim().split('\n')
|
||||
list.map(sentenceText => {
|
||||
let sentences = split(sentenceText)
|
||||
sentences.map(sentenceRow => {
|
||||
let row = sentenceRow.raw
|
||||
let sentence: Sentence = {
|
||||
text: row,
|
||||
translate: '',
|
||||
words: [],
|
||||
audioPosition: [0, 0],
|
||||
}
|
||||
section.push(sentence)
|
||||
if (row) {
|
||||
//这个库总是会把反引号给断句到下一行
|
||||
if (row[0] === "”") {
|
||||
sentence.text = row.substr(1)
|
||||
let lastSentence = section[section.length - 2]
|
||||
lastSentence.text += "”"
|
||||
if (!sentence.text) {
|
||||
section.pop()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// console.log('sentence', sentenceRow)
|
||||
// console.log('sentence', sentenceRow)
|
||||
})
|
||||
})
|
||||
})
|
||||
// console.log('sections', sections)
|
||||
@@ -297,12 +298,12 @@ export function getSplitTranslateText(article: string) {
|
||||
if (sections.length) {
|
||||
sections.map((sectionItem) => {
|
||||
sectionItem.map((sentenceItem) => {
|
||||
str += sentenceItem.text + '\n'
|
||||
str += sentenceItem.text + (sentenceItem.text.endsWith("\n") ? '' : '\n')
|
||||
})
|
||||
str += '\n'
|
||||
str += str.endsWith("\n\n") ? '' : '\n'
|
||||
})
|
||||
}
|
||||
return str
|
||||
return str.trim()
|
||||
}
|
||||
|
||||
export function isArticle(type: DictType): boolean {
|
||||
@@ -312,16 +313,8 @@ export function isArticle(type: DictType): boolean {
|
||||
}
|
||||
|
||||
export function getTranslateText(article: Article) {
|
||||
if (article.useTranslateType === TranslateType.custom) {
|
||||
|
||||
return article.textCustomTranslate
|
||||
.split('\n\n').filter(v => v)
|
||||
} else if (article.useTranslateType === TranslateType.network) {
|
||||
return article.textNetworkTranslate
|
||||
.split('\n\n').filter(v => v)
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
return article.textTranslate
|
||||
.split('\n\n').filter(v => v)
|
||||
}
|
||||
|
||||
export function usePlaySentenceAudio() {
|
||||
|
||||
@@ -42,7 +42,7 @@ export function getSentenceAllTranslateText(article: Article) {
|
||||
})
|
||||
str += '\n'
|
||||
})
|
||||
return str
|
||||
return str.trim()
|
||||
}
|
||||
|
||||
export function getSentenceAllText(article: Article) {
|
||||
@@ -100,7 +100,7 @@ export async function getNetworkTranslate(
|
||||
sentence.translate = r.trans.paragraphs[0]
|
||||
if (!allShow) {
|
||||
//一次显示所有,顺序会乱
|
||||
article.textCustomTranslate += sentence.translate + '\n'
|
||||
article.textTranslate += sentence.translate + '\n'
|
||||
}
|
||||
}
|
||||
return Promise.resolve(cb)
|
||||
@@ -178,7 +178,7 @@ export async function getNetworkTranslate(
|
||||
retryCount++
|
||||
} while (promiseList.length)
|
||||
cbs.map(v => v())
|
||||
article.textCustomTranslate = getSentenceAllTranslateText(article)
|
||||
article.textTranslate = getSentenceAllTranslateText(article)
|
||||
|
||||
if (progressCb) {
|
||||
clearInterval(timer)
|
||||
@@ -188,7 +188,7 @@ export async function getNetworkTranslate(
|
||||
resolve(true)
|
||||
})
|
||||
} else {
|
||||
article.textCustomTranslate = getSentenceAllTranslateText(article)
|
||||
article.textTranslate = getSentenceAllTranslateText(article)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -197,10 +197,12 @@ export function renewSectionTexts(article: Article) {
|
||||
let {newText, sections} = splitEnArticle(article.text)
|
||||
article.text = newText
|
||||
article.sections = sections
|
||||
let count = 0
|
||||
if (article.lrcPosition.length) {
|
||||
article.sections.map((v, i) => {
|
||||
v.map((w, j) => {
|
||||
w.audioPosition = article.lrcPosition[(i * (article.sections[i - 1]||[]).length) + j]
|
||||
w.audioPosition = article.lrcPosition[count]
|
||||
count++
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import {ShortcutKey} from "@/types.ts";
|
||||
import DictModal from "@/pages/pc/components/dialog/DictDiglog.vue";
|
||||
import {useStartKeyboardEventListener} from "@/hooks/event.ts";
|
||||
import useTheme from "@/hooks/theme.ts";
|
||||
import ArticleFooter from "@/pages/pc/article/ArticleFooter.vue";
|
||||
|
||||
const statisticsStore = usePracticeStore()
|
||||
const store = useBaseStore()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {Article, DefaultArticle, Sentence, TranslateEngine, TranslateType} from "@/types.ts";
|
||||
import {Article, DefaultArticle, Sentence, TranslateEngine} from "@/types.ts";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import EditAbleText from "@/pages/pc/components/EditAbleText.vue";
|
||||
import {Icon} from "@iconify/vue";
|
||||
@@ -13,12 +13,12 @@ import {
|
||||
} from "@/hooks/translate.ts";
|
||||
|
||||
import {MessageBox} from "@/utils/MessageBox.tsx";
|
||||
import {getSplitTranslateText} from "@/hooks/article.ts";
|
||||
import {getSplitTranslateText, usePlaySentenceAudio} from "@/hooks/article.ts";
|
||||
import {cloneDeep, last} from "lodash-es";
|
||||
import {watch, ref, nextTick} from "vue";
|
||||
import {watch} from "vue";
|
||||
import Empty from "@/components/Empty.vue";
|
||||
import {UploadProps, UploadUserFile} from "element-plus";
|
||||
import {_copy, _nextTick, _parseLRC} from "@/utils";
|
||||
import {UploadProps} from "element-plus";
|
||||
import {_nextTick, _parseLRC} from "@/utils";
|
||||
import * as Comparison from "string-comparison"
|
||||
import audio from '/public/sound/article/nce2-1/1.mp3'
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
@@ -54,23 +54,15 @@ watch(() => props.article, val => {
|
||||
editArticle = cloneDeep(val)
|
||||
progress = 0
|
||||
failCount = 0
|
||||
if (editArticle.text.trim()) {
|
||||
if (editArticle.useTranslateType === TranslateType.custom) {
|
||||
if (editArticle.textCustomTranslate.trim()) {
|
||||
if (!editArticle.textCustomTranslateIsFormat) {
|
||||
let r = getSplitTranslateText(editArticle.textCustomTranslate)
|
||||
if (r) {
|
||||
editArticle.textCustomTranslate = r
|
||||
ElMessage({
|
||||
message: '检测到本地翻译未格式化,已自动格式化',
|
||||
type: 'success',
|
||||
duration: 3000
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// let r = getSplitTranslateText(editArticle.textTranslate)
|
||||
// if (r) {
|
||||
// editArticle.textTranslate = r
|
||||
// ElMessage({
|
||||
// message: '检测到本地翻译未格式化,已自动格式化',
|
||||
// type: 'success',
|
||||
// duration: 3000
|
||||
// })
|
||||
// }
|
||||
renewSections()
|
||||
console.log('ar', editArticle)
|
||||
}, {immediate: true})
|
||||
@@ -84,12 +76,7 @@ watch(() => editArticle.text, (s) => {
|
||||
function renewSections() {
|
||||
if (editArticle.text.trim()) {
|
||||
renewSectionTexts(editArticle)
|
||||
if (editArticle.useTranslateType === TranslateType.custom) {
|
||||
failCount = renewSectionTranslates(editArticle, editArticle.textCustomTranslate)
|
||||
}
|
||||
if (editArticle.useTranslateType === TranslateType.network) {
|
||||
failCount = renewSectionTranslates(editArticle, editArticle.textNetworkTranslate)
|
||||
}
|
||||
failCount = renewSectionTranslates(editArticle, editArticle.textTranslate)
|
||||
} else {
|
||||
editArticle.sections = []
|
||||
}
|
||||
@@ -98,11 +85,13 @@ function renewSections() {
|
||||
function appendTranslate(str: string) {
|
||||
let selectionStart = textareaRef.selectionStart;
|
||||
let selectionEnd = textareaRef.selectionEnd;
|
||||
if (editArticle.useTranslateType === TranslateType.custom) {
|
||||
editArticle.textCustomTranslate = editArticle.textCustomTranslate.slice(0, selectionStart) + str + editArticle.textCustomTranslate.slice(selectionEnd)
|
||||
}
|
||||
if (editArticle.useTranslateType === TranslateType.network) {
|
||||
editArticle.textNetworkTranslate = editArticle.textNetworkTranslate.slice(0, selectionStart) + str + editArticle.textNetworkTranslate.slice(selectionEnd)
|
||||
editArticle.textTranslate = editArticle.textTranslate.slice(0, selectionStart) + str + editArticle.textTranslate.slice(selectionEnd)
|
||||
}
|
||||
|
||||
function splitTranslateText() {
|
||||
if (editArticle.textTranslate.trim()){
|
||||
editArticle.textTranslate = getSplitTranslateText(editArticle.textTranslate.trim())
|
||||
renewSections()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,6 +128,7 @@ function onFocus() {
|
||||
document.addEventListener('paste', onPaste);
|
||||
}
|
||||
|
||||
//TODO
|
||||
async function startNetworkTranslate() {
|
||||
if (!editArticle.title.trim()) {
|
||||
return ElMessage.error('请填写标题!')
|
||||
@@ -160,12 +150,7 @@ async function startNetworkTranslate() {
|
||||
|
||||
function saveSentenceTranslate(sentence: Sentence, val: string) {
|
||||
sentence.translate = val
|
||||
if (editArticle.useTranslateType === TranslateType.custom) {
|
||||
editArticle.textCustomTranslate = getSentenceAllTranslateText(editArticle)
|
||||
}
|
||||
if (editArticle.useTranslateType === TranslateType.network) {
|
||||
editArticle.textNetworkTranslate = getSentenceAllTranslateText(editArticle)
|
||||
}
|
||||
editArticle.textTranslate = getSentenceAllTranslateText(editArticle)
|
||||
renewSections()
|
||||
}
|
||||
|
||||
@@ -184,8 +169,7 @@ function save(option: 'save' | 'saveAndNext') {
|
||||
editArticle.title = editArticle.title.trim()
|
||||
editArticle.titleTranslate = editArticle.titleTranslate.trim()
|
||||
editArticle.text = editArticle.text.trim()
|
||||
editArticle.textCustomTranslate = editArticle.textCustomTranslate.trim()
|
||||
editArticle.textNetworkTranslate = editArticle.textNetworkTranslate.trim()
|
||||
editArticle.textTranslate = editArticle.textTranslate.trim()
|
||||
|
||||
if (!editArticle.title) {
|
||||
ElMessage.error('请填写标题!')
|
||||
@@ -197,40 +181,10 @@ function save(option: 'save' | 'saveAndNext') {
|
||||
}
|
||||
|
||||
const saveTemp = () => {
|
||||
editArticle.textCustomTranslateIsFormat = true
|
||||
emit(option as any, editArticle)
|
||||
|
||||
return resolve(true)
|
||||
}
|
||||
|
||||
if (editArticle.useTranslateType === TranslateType.network) {
|
||||
if (!editArticle.textNetworkTranslate) {
|
||||
return MessageBox.confirm(
|
||||
'您选择了“网络翻译”,但译文内容却为空白,是否修改为“不需要翻译”并保存?',
|
||||
'提示',
|
||||
() => {
|
||||
editArticle.useTranslateType = TranslateType.none
|
||||
saveTemp()
|
||||
},
|
||||
() => void 0,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (editArticle.useTranslateType === TranslateType.custom) {
|
||||
if (!editArticle.textCustomTranslate) {
|
||||
return MessageBox.confirm(
|
||||
'您选择了“本地翻译”,但译文内容却为空白,是否修改为“不需要翻译”并保存?',
|
||||
'提示',
|
||||
() => {
|
||||
editArticle.useTranslateType = TranslateType.none
|
||||
saveTemp()
|
||||
},
|
||||
() => void 0,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
saveTemp()
|
||||
})
|
||||
}
|
||||
@@ -238,7 +192,6 @@ function save(option: 'save' | 'saveAndNext') {
|
||||
//不知道为什么直接用editArticle,取到是空的默认值
|
||||
defineExpose({save, getEditArticle: () => cloneDeep(editArticle)})
|
||||
|
||||
|
||||
const handleChange: UploadProps['onChange'] = (uploadFile, uploadFiles) => {
|
||||
console.log(uploadFile)
|
||||
let reader = new FileReader();
|
||||
@@ -284,6 +237,7 @@ function handleShowEditAudioDialog(val: Sentence, i: number, j: number) {
|
||||
currentSentence = val
|
||||
editSentence = cloneDeep(val)
|
||||
preSentence = null
|
||||
audioRef.pause()
|
||||
if (j == 0) {
|
||||
if (i != 0) {
|
||||
preSentence = last(editArticle.sections[i - 1])
|
||||
@@ -316,25 +270,7 @@ function recordEnd() {
|
||||
editSentence.audioPosition[1] = Number(sentenceAudioRef.currentTime.toFixed(2))
|
||||
}
|
||||
|
||||
let timer = -1
|
||||
|
||||
function playSentenceAudio(sentence: Sentence, ref?: HTMLAudioElement) {
|
||||
clearTimeout(timer)
|
||||
if (sentence.audioPosition?.length) {
|
||||
if (ref.played) {
|
||||
ref.pause()
|
||||
}
|
||||
let start = sentence.audioPosition[0];
|
||||
ref.currentTime = start
|
||||
ref.play()
|
||||
let end = sentence.audioPosition?.[1]
|
||||
if (end && end !== -1) {
|
||||
timer = setTimeout(() => {
|
||||
ref.pause()
|
||||
}, (end - start) * 1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
const {playSentenceAudio} = usePlaySentenceAudio()
|
||||
|
||||
function saveLrcPosition() {
|
||||
// showEditAudioDialog = false
|
||||
@@ -351,6 +287,23 @@ function setPreEndTimeToCurrentStartTime() {
|
||||
editSentence.audioPosition[0] = preSentence.audioPosition[1]
|
||||
}
|
||||
}
|
||||
|
||||
function setStartTime(val: Sentence, i: number, j: number) {
|
||||
let preSentence = null
|
||||
if (j == 0) {
|
||||
if (i != 0) {
|
||||
preSentence = last(editArticle.sections[i - 1])
|
||||
}
|
||||
} else {
|
||||
preSentence = editArticle.sections[i][j - 1]
|
||||
}
|
||||
if (preSentence) {
|
||||
val.audioPosition[0] = preSentence.audioPosition[1]
|
||||
} else {
|
||||
val.audioPosition[0] = Number(Number(audioRef.currentTime).toFixed(2))
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -376,10 +329,9 @@ function setPreEndTimeToCurrentStartTime() {
|
||||
<div class="justify-between items-center gap-2 flex">
|
||||
<ol class="py-0 pl-5 my-0 text-base color-black/60">
|
||||
<li>复制原文</li>
|
||||
<li>点击 <span class="color-red font-bold">应用</span> 按钮进行分句,并同步到左侧结果</li>
|
||||
<li>手动调整分句,一行一句,段落之间空一行<span class="color-red font-bold">(可选)</span></li>
|
||||
<li>修改完成后点击 <span class="color-red font-bold">应用</span> 按钮同步到左侧结果<span
|
||||
class="color-red font-bold">(可选)</span>
|
||||
<li>点击 <span class="color-red font-bold">分句</span> 按钮进行自动分句</li>
|
||||
<li><span class="color-red font-bold">或</span> 手动调整分句,一行一句,段落之间空一行</li>
|
||||
<li>修改完成后点击 <span class="color-red font-bold">应用</span> 按钮同步到左侧结果栏
|
||||
</li>
|
||||
</ol>
|
||||
<el-button type="primary" @click="renewSections">应用</el-button>
|
||||
@@ -401,7 +353,7 @@ function setPreEndTimeToCurrentStartTime() {
|
||||
<span>正文:</span>
|
||||
</div>
|
||||
<textarea
|
||||
v-model="editArticle.textCustomTranslate"
|
||||
v-model="editArticle.textTranslate"
|
||||
:readonly="![100,0].includes(progress)"
|
||||
@blur="onBlur"
|
||||
@focus="onFocus"
|
||||
@@ -413,13 +365,10 @@ function setPreEndTimeToCurrentStartTime() {
|
||||
</textarea>
|
||||
<div class="justify-between items-center gap-2 flex">
|
||||
<ol class="py-0 pl-5 my-0 text-base color-black/60">
|
||||
<li>复制译文</li>
|
||||
<li>如果没有译文,点击 <span class="color-red font-bold">翻译</span> 按钮<span
|
||||
class="color-red font-bold">(可选)</span></li>
|
||||
<li>点击 <span class="color-red font-bold">应用</span> 按钮进行分句,并同步到左侧结果</li>
|
||||
<li>手动调整分句,一行一句,段落之间空一行<span class="color-red font-bold">(可选)</span></li>
|
||||
<li>修改完成后点击 <span class="color-red font-bold">应用</span> 按钮同步到左侧结果<span
|
||||
class="color-red font-bold">(可选)</span>
|
||||
<li>复制译文,如果没有请点击 <span class="color-red font-bold">翻译</span> 按钮</li>
|
||||
<li>点击 <span class="color-red font-bold">分句</span> 按钮进行自动分句</li>
|
||||
<li><span class="color-red font-bold">或</span> 手动调整分句,一行一句,段落之间空一行</li>
|
||||
<li>修改完成后点击 <span class="color-red font-bold">应用</span> 按钮同步到左侧结果栏
|
||||
</li>
|
||||
</ol>
|
||||
<div class="flex flex-col gap-2 items-end">
|
||||
@@ -443,7 +392,11 @@ function setPreEndTimeToCurrentStartTime() {
|
||||
:loading="progress!==0 && progress !== 100"
|
||||
>翻译
|
||||
</el-button>
|
||||
<el-button type="primary" @click="renewSections">应用</el-button>
|
||||
<div>
|
||||
<el-button type="primary" @click="splitTranslateText">分句</el-button>
|
||||
<el-button type="primary" @click="renewSections">应用</el-button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -485,20 +438,33 @@ function setPreEndTimeToCurrentStartTime() {
|
||||
@save="(e:string) => saveSentenceTranslate(sentence,e)"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex-[3]">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<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 class="flex-[2] flex justify-end gap-1 items-center">
|
||||
<div class="flex justify-end gap-2">
|
||||
<div class="flex flex-col items-center justify-center">
|
||||
<div>{{ sentence.audioPosition?.[0] ?? 0 }}s</div>
|
||||
<BaseIcon
|
||||
@click="setStartTime(sentence,indexI,indexJ)"
|
||||
:icon="indexI === 0 && indexJ === 0 ?'ic:sharp-my-location':'twemoji:end-arrow'"
|
||||
:title="indexI === 0 && indexJ === 0 ?'设置开始时间':'使用前一句的结束时间'"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<BaseIcon :icon="sentence.audioPosition?.length ? 'basil:edit-outline' : 'basil:add-outline'"
|
||||
@click="handleShowEditAudioDialog(sentence,indexI,indexJ)"/>
|
||||
<BaseIcon v-if="sentence.audioPosition?.length" icon="hugeicons:play"
|
||||
@click="playSentenceAudio(sentence,audioRef)"/>
|
||||
<div>-</div>
|
||||
<div class="flex flex-col items-center justify-center">
|
||||
<div v-if="sentence.audioPosition?.[1] !== -1">{{ sentence.audioPosition?.[1] ?? 0 }}s</div>
|
||||
<div v-else> 结束</div>
|
||||
<BaseIcon
|
||||
@click="sentence.audioPosition[1] = Number(Number(audioRef.currentTime).toFixed(2))"
|
||||
title="设置结束时间"
|
||||
icon="ic:sharp-my-location"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<BaseIcon :icon="sentence.audioPosition?.length ? 'basil:edit-outline' : 'basil:add-outline'"
|
||||
@click="handleShowEditAudioDialog(sentence,indexI,indexJ)"/>
|
||||
<BaseIcon v-if="sentence.audioPosition?.length" icon="hugeicons:play"
|
||||
@click="playSentenceAudio(sentence,audioRef,editArticle)"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -507,7 +473,7 @@ function setPreEndTimeToCurrentStartTime() {
|
||||
<div class="options" v-if="editArticle.text.trim()">
|
||||
<div class="status">
|
||||
<span>状态:</span>
|
||||
<div class="warning" v-if="failCount && editArticle.useTranslateType !== TranslateType.none">
|
||||
<div class="warning" v-if="failCount">
|
||||
<Icon icon="typcn:warning-outline"/>
|
||||
共有{{ failCount }}句没有翻译!
|
||||
</div>
|
||||
@@ -546,7 +512,7 @@ function setPreEndTimeToCurrentStartTime() {
|
||||
</div>
|
||||
<BaseIcon icon="hugeicons:play"
|
||||
title="试听"
|
||||
@click="playSentenceAudio(editSentence,sentenceAudioRef)"/>
|
||||
@click="playSentenceAudio(editSentence,sentenceAudioRef,editArticle)"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
@@ -591,7 +557,6 @@ function setPreEndTimeToCurrentStartTime() {
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -59,16 +59,13 @@ function checkDataChange() {
|
||||
editArticle.title = editArticle.title.trim()
|
||||
editArticle.titleTranslate = editArticle.titleTranslate.trim()
|
||||
editArticle.text = editArticle.text.trim()
|
||||
editArticle.textCustomTranslate = editArticle.textCustomTranslate.trim()
|
||||
editArticle.textNetworkTranslate = editArticle.textNetworkTranslate.trim()
|
||||
editArticle.textTranslate = editArticle.textTranslate.trim()
|
||||
|
||||
if (
|
||||
editArticle.title !== article.title ||
|
||||
editArticle.titleTranslate !== article.titleTranslate ||
|
||||
editArticle.text !== article.text ||
|
||||
editArticle.textCustomTranslate !== article.textCustomTranslate ||
|
||||
editArticle.textNetworkTranslate !== article.textNetworkTranslate ||
|
||||
editArticle.useTranslateType !== article.useTranslateType
|
||||
editArticle.textTranslate !== article.textTranslate
|
||||
) {
|
||||
return MessageBox.confirm(
|
||||
'检测到数据有变动,是否保存?',
|
||||
|
||||
@@ -1,25 +1,40 @@
|
||||
<script setup lang="ts">
|
||||
import {nextTick, onMounted, watch} from "vue";
|
||||
|
||||
interface IProps {
|
||||
modelValue?: boolean,
|
||||
width?: string
|
||||
}
|
||||
|
||||
withDefaults(defineProps<IProps>(), {
|
||||
let props = withDefaults(defineProps<IProps>(), {
|
||||
modelValue: true,
|
||||
width: '180rem'
|
||||
})
|
||||
let modalRef = $ref(null)
|
||||
let style = $ref({top: '2.4rem', bottom: 'unset'})
|
||||
|
||||
watch(() => props.modelValue, (n) => {
|
||||
if (n)
|
||||
nextTick(() => {
|
||||
if (modalRef) {
|
||||
const modal = modalRef as HTMLElement
|
||||
if (modal.getBoundingClientRect().bottom > window.innerHeight) {
|
||||
style = {top: 'unset', 'bottom': '2.5rem'}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Transition name="fade">
|
||||
<div v-if="modelValue" class="mini-modal" :style="{width}">
|
||||
<div v-if="modelValue" ref="modalRef" class="mini-modal" :style="{width, ...style}">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
<style lang="scss">
|
||||
@import "@/assets/css/style";
|
||||
|
||||
.mini-row-title {
|
||||
@@ -48,7 +63,7 @@ withDefaults(defineProps<IProps>(), {
|
||||
border-radius: .5rem;
|
||||
box-shadow: 0 0 8px 2px var(--color-item-border);
|
||||
padding: .6rem var(--space);
|
||||
top: 2.4rem;
|
||||
//top: 2.4rem;
|
||||
left: 50%;
|
||||
transform: translate3d(-50%, 0, 0);
|
||||
//margin-top: 10rem;
|
||||
|
||||
@@ -152,14 +152,12 @@ function importData(e: any) {
|
||||
if (v['原文标题'] && v['原文正文']) {
|
||||
let article: Article = {
|
||||
...DefaultArticle,
|
||||
textCustomTranslateIsFormat: false,
|
||||
useTranslateType: TranslateType.custom,
|
||||
id: nanoid(6),
|
||||
checked: false,
|
||||
title: String(v['原文标题']),
|
||||
text: String(v['原文正文']),
|
||||
titleTranslate: String(v['译文标题']),
|
||||
textCustomTranslate: String(v['译文正文']),
|
||||
textTranslate: String(v['译文正文']),
|
||||
}
|
||||
return article
|
||||
}
|
||||
@@ -235,7 +233,7 @@ function exportData(val: {
|
||||
原文标题: v.title,
|
||||
原文正文: v.text,
|
||||
译文标题: v.titleTranslate,
|
||||
译文正文: v.textCustomTranslate,
|
||||
译文正文: v.textTranslate,
|
||||
}
|
||||
})
|
||||
wb.Sheets['Sheet1'] = XLSX.utils.json_to_sheet(sheetData)
|
||||
|
||||
@@ -14,6 +14,7 @@ import ContextMenu from '@imengyu/vue3-context-menu'
|
||||
import {useToast} from 'vue-toast-notification';
|
||||
import 'vue-toast-notification/dist/theme-sugar.css';
|
||||
import {getTranslateText} from "@/hooks/article.ts";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
|
||||
interface IProps {
|
||||
article: Article,
|
||||
@@ -448,12 +449,17 @@ defineExpose({showSentence, play, del, hideSentence, nextSentence})
|
||||
</div>
|
||||
<div class="cursor" v-if="!isEnd" :style="{top:cursor.top+'px',left:cursor.left+'px'}"></div>
|
||||
</div>
|
||||
<div class="translate-bottom">
|
||||
<div class="options flex justify-center" v-if="isEnd">
|
||||
<BaseButton
|
||||
v-if="store.currentArticleDict.lastLearnIndex < store.currentArticleDict.articles.length - 1"
|
||||
@click="emitter.emit(EventKey.next)">下一章</BaseButton>
|
||||
</div>
|
||||
<div class="translate-bottom" v-if="settingStore.translate">
|
||||
<header class="mb-4">
|
||||
<div class="text-2xl center">{{ props.article.titleTranslate }}</div>
|
||||
</header>
|
||||
<template v-if="getTranslateText(article).length">
|
||||
<div class="text-xl mb-4" v-for="t in getTranslateText(article)">{{ t }}</div>
|
||||
<div class="text-xl mb-4 indent-8" v-for="t in getTranslateText(article)">{{ t }}</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -105,61 +105,10 @@ function getCurrentPractice() {
|
||||
if (tempArticle.sections.length) {
|
||||
setArticle(tempArticle)
|
||||
} else {
|
||||
if (tempArticle.useTranslateType === TranslateType.none) {
|
||||
renewSectionTexts(tempArticle)
|
||||
renewSectionTexts(tempArticle)
|
||||
if (tempArticle.textTranslate.trim()) {
|
||||
renewSectionTranslates(tempArticle, tempArticle.textTranslate)
|
||||
setArticle(tempArticle)
|
||||
} else {
|
||||
if (tempArticle.useTranslateType === TranslateType.custom) {
|
||||
if (tempArticle.textCustomTranslate.trim()) {
|
||||
if (tempArticle.textCustomTranslateIsFormat) {
|
||||
renewSectionTexts(tempArticle)
|
||||
renewSectionTranslates(tempArticle, tempArticle.textCustomTranslate)
|
||||
setArticle(tempArticle)
|
||||
} else {
|
||||
//说明有本地翻译,但是没格式化成一行一行的
|
||||
MessageBox.confirm('检测到存在本地翻译,但未格式化,是否进行编辑?',
|
||||
'提示',
|
||||
() => {
|
||||
editArticle = tempArticle
|
||||
showEditArticle = true
|
||||
},
|
||||
() => {
|
||||
renewSectionTexts(tempArticle)
|
||||
tempArticle.useTranslateType = TranslateType.none
|
||||
setArticle(tempArticle)
|
||||
}, null,
|
||||
{
|
||||
confirmButtonText: '去编辑',
|
||||
cancelButtonText: '不需要翻译',
|
||||
})
|
||||
}
|
||||
} else {
|
||||
//没有本地翻译
|
||||
MessageBox.confirm(
|
||||
'没有本地翻译,是否进行编辑?',
|
||||
'提示',
|
||||
() => {
|
||||
editArticle = tempArticle
|
||||
showEditArticle = true
|
||||
},
|
||||
|
||||
() => {
|
||||
renewSectionTexts(tempArticle)
|
||||
tempArticle.useTranslateType = TranslateType.none
|
||||
setArticle(tempArticle)
|
||||
}, null,
|
||||
{
|
||||
confirmButtonText: '去编辑',
|
||||
cancelButtonText: '不需要翻译',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (tempArticle.useTranslateType === TranslateType.network) {
|
||||
renewSectionTexts(tempArticle)
|
||||
renewSectionTranslates(tempArticle, tempArticle.textNetworkTranslate)
|
||||
setArticle(tempArticle)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -467,8 +416,18 @@ const {playSentenceAudio} = usePlaySentenceAudio()
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col items-center justify-center gap-1">
|
||||
<audio ref="audioRef" v-if="articleData.article.audioSrc" :src="articleData.article.audioSrc" controls></audio>
|
||||
<audio ref="audioRef" v-if="articleData.article.audioSrc" :src="articleData.article.audioSrc"
|
||||
controls></audio>
|
||||
<div class="flex gap-3 center">
|
||||
<BaseIcon
|
||||
:title="`下一句(${settingStore.shortcutKeyMap[ShortcutKey.Next]})`"
|
||||
icon="icon-park-outline:go-ahead"
|
||||
@click="emit('over')"/>
|
||||
<BaseIcon
|
||||
:title="`重听(${settingStore.shortcutKeyMap[ShortcutKey.PlayWordPronunciation]})`"
|
||||
icon="fluent:replay-16-filled"
|
||||
@click="play"/>
|
||||
|
||||
<Tooltip
|
||||
:title="`开关默写模式(${settingStore.shortcutKeyMap[ShortcutKey.ToggleDictation]})`"
|
||||
>
|
||||
@@ -492,22 +451,7 @@ const {playSentenceAudio} = usePlaySentenceAudio()
|
||||
@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"
|
||||
|
||||
@@ -9,9 +9,9 @@ let data = {
|
||||
"title": "A cold welcome",
|
||||
"titleTranslate": "冷遇",
|
||||
"text": "On Wednesday evening, we went to the Town Hall. It was the last day of the year and a large crowd of people had gathered under the Town Hall clock. It would strike twelve in twenty minutes' time. Fifteen minutes passed and then, at five to twelve, the clock stopped. The big minute hand did not move. We waited and waited, but nothing happened. Suddenly someone shouted. 'It's two minutes past twelve! The clock has stopped!' I looked at my watch. It was true. The big clock refused to welcome the New Year. At that moment, everybody began to laugh and sing.\n",
|
||||
"textCustomTranslate": "星期三的晚上,我们去了市政厅。 那是一年的最后一天,一大群人聚集在市政厅的大钟下面。再过20分钟,大钟将敲响12下。15分钟过去了,而就在11点55分时,大钟停了。那根巨大的分针不动了。 我们等啊等啊,可情况没有变化。突然有人喊道:“已经12点零2分了!那钟已经停了!”我看了一下我的手表,果真如此。那座大钟不愿意迎接新年。此时,大家已经笑了起来,同时唱起了歌。",
|
||||
"textTranslate": "星期三的晚上,我们去了市政厅。 那是一年的最后一天,一大群人聚集在市政厅的大钟下面。再过20分钟,大钟将敲响12下。15分钟过去了,而就在11点55分时,大钟停了。那根巨大的分针不动了。 我们等啊等啊,可情况没有变化。突然有人喊道:“已经12点零2分了!那钟已经停了!”我看了一下我的手表,果真如此。那座大钟不愿意迎接新年。此时,大家已经笑了起来,同时唱起了歌。",
|
||||
"textNetworkTranslate": "",
|
||||
"textCustomTranslateIsFormat": false,
|
||||
"textTranslateIsFormat": false,
|
||||
"useTranslateType": "custom",
|
||||
"newWords": [],
|
||||
"id": "UydP2M"
|
||||
@@ -20,9 +20,9 @@ let data = {
|
||||
// "title": "The best and the worst",
|
||||
// "titleTranslate": "最好的和最差的",
|
||||
// "text": "Joe Sanders has the most beautiful garden in our town. Nearly everybody enters for 'The Nicest Garden Competition' each year, but Joe wins every time. Bill Frith's garden is larger than Joe's. Bill works harder than Joe and grows more flowers and vegetables, but Joe's garden is more interesting. He has made neat paths and has built a wooden bridge over a pool. I like gardens too, but I do not like hard work. Every year I enter for the garden competition too, and I always win a little prize for the worst garden in the town!",
|
||||
// "textCustomTranslate": "乔.桑德斯拥有我们镇上最漂亮的花园。\n几乎每个人都参加每年举办的“最佳花园竞赛”,而每次都是乔获胜。\n比尔.弗里斯的花园比乔的花园大,\n他比乔也更为勤奋,种植的花卉和蔬菜也更多,但乔的花园更富有情趣。\n他修筑了一条条整洁的小路,并在一个池塘上架了一座小木桥。\n我也喜欢花园,但我却不愿意辛勤劳动。\n每年的花园竞赛我也参加,但总因是镇上最劣的花园而获得一个小奖!",
|
||||
// "textTranslate": "乔.桑德斯拥有我们镇上最漂亮的花园。\n几乎每个人都参加每年举办的“最佳花园竞赛”,而每次都是乔获胜。\n比尔.弗里斯的花园比乔的花园大,\n他比乔也更为勤奋,种植的花卉和蔬菜也更多,但乔的花园更富有情趣。\n他修筑了一条条整洁的小路,并在一个池塘上架了一座小木桥。\n我也喜欢花园,但我却不愿意辛勤劳动。\n每年的花园竞赛我也参加,但总因是镇上最劣的花园而获得一个小奖!",
|
||||
// "textNetworkTranslate": "",
|
||||
// "textCustomTranslateIsFormat": true,
|
||||
// "textTranslateIsFormat": true,
|
||||
// "useTranslateType": "custom",
|
||||
// "newWords": [],
|
||||
// "id": "TdAAqD"
|
||||
|
||||
@@ -131,7 +131,8 @@ export const DefaultBaseState = (): BaseState => ({
|
||||
language: 'en',
|
||||
type: DictType.article,
|
||||
resourceId: 'article_nce2',
|
||||
length: 96
|
||||
length: 96,
|
||||
lastLearnIndex:10
|
||||
},
|
||||
],
|
||||
wordDictList: [
|
||||
|
||||
12
src/types.ts
12
src/types.ts
@@ -109,13 +109,10 @@ export interface Article {
|
||||
title: string,
|
||||
titleTranslate: string,
|
||||
text: string,
|
||||
textCustomTranslate: string,
|
||||
textCustomTranslateIsFormat: boolean,//翻译是否格式化
|
||||
textNetworkTranslate: string,
|
||||
textTranslate: string,
|
||||
newWords: Word[],
|
||||
textAllWords: string[],
|
||||
sections: Sentence[][],
|
||||
useTranslateType: TranslateType,
|
||||
audioSrc: string,
|
||||
lrcPosition: number[][],
|
||||
}
|
||||
@@ -126,13 +123,10 @@ export const DefaultArticle: Article = {
|
||||
title: '',
|
||||
titleTranslate: '',
|
||||
text: '',
|
||||
textCustomTranslate: '',
|
||||
textNetworkTranslate: '',
|
||||
textCustomTranslateIsFormat: false,
|
||||
textTranslate: '',
|
||||
newWords: [],
|
||||
textAllWords: [],
|
||||
sections: [],
|
||||
useTranslateType: TranslateType.custom,
|
||||
audioSrc: '',
|
||||
lrcPosition: [],
|
||||
}
|
||||
@@ -175,6 +169,7 @@ export enum ShortcutKey {
|
||||
ShowWord = 'ShowWord',
|
||||
EditArticle = 'EditArticle',
|
||||
Next = 'Next',
|
||||
Replay = 'Replay',
|
||||
Previous = 'Previous',
|
||||
ToggleSimple = 'ToggleSimple',
|
||||
ToggleCollect = 'ToggleCollect',
|
||||
@@ -199,6 +194,7 @@ export const DefaultShortcutKeyMap = {
|
||||
[ShortcutKey.ShowWord]: 'Escape',
|
||||
[ShortcutKey.Previous]: 'Alt+⬅',
|
||||
[ShortcutKey.Next]: 'Tab',
|
||||
[ShortcutKey.Replay]: 'Tab',
|
||||
[ShortcutKey.ToggleSimple]: '`',
|
||||
[ShortcutKey.ToggleCollect]: 'Enter',
|
||||
[ShortcutKey.PreviousChapter]: 'Ctrl+⬅',
|
||||
|
||||
Reference in New Issue
Block a user