feat: 修改文章编辑器

This commit is contained in:
zyronon
2025-03-23 02:57:59 +08:00
parent 47d4958a6a
commit 082532bca9
22 changed files with 1666 additions and 362 deletions

1
auto-imports.d.ts vendored
View File

@@ -6,4 +6,5 @@
export {}
declare global {
const ElMessage: typeof import('element-plus/es')['ElMessage']
const ElMessageBox: typeof import('element-plus/es')['ElMessageBox']
}

1
components.d.ts vendored
View File

@@ -28,6 +28,7 @@ declare module 'vue' {
ElSlider: typeof import('element-plus/es')['ElSlider']
ElSwitch: typeof import('element-plus/es')['ElSwitch']
ElTableV2: typeof import('element-plus/es')['ElTableV2']
ElUpload: typeof import('element-plus/es')['ElUpload']
Empty: typeof import('./src/components/Empty.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']

View File

@@ -34,6 +34,7 @@
"nanoid": "^5.0.3",
"pinia": "^2.1.6",
"sentence-splitter": "^4.2.1",
"string-comparison": "^1.3.0",
"tesseract.js": "^4.1.1",
"vant": "^4.8.1",
"vue": "^3.4.27",

9
pnpm-lock.yaml generated
View File

@@ -65,6 +65,9 @@ importers:
sentence-splitter:
specifier: ^4.2.1
version: 4.4.1
string-comparison:
specifier: ^1.3.0
version: 1.3.0
tesseract.js:
specifier: ^4.1.1
version: 4.1.4
@@ -2999,6 +3002,10 @@ packages:
stream-shift@1.0.3:
resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==}
string-comparison@1.3.0:
resolution: {integrity: sha512-46aD+slEwybxAMPRII83ATbgMgTiz5P8mVd7Z6VJsCzSHFjdt1hkAVLeFxPIyEb11tc6ihpJTlIqoO0MCF6NPw==}
engines: {node: ^16.0.0 || >=18.0.0}
string-width@1.0.2:
resolution: {integrity: sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==}
engines: {node: '>=0.10.0'}
@@ -6667,6 +6674,8 @@ snapshots:
stream-shift@1.0.3: {}
string-comparison@1.3.0: {}
string-width@1.0.2:
dependencies:
code-point-at: 1.1.0

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,18 @@
[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!'

Binary file not shown.

View File

@@ -15,6 +15,7 @@ import {isMobile, shakeCommonDict} from "@/utils";
import router, {routes} from "@/router.ts";
import {useRoute} from "vue-router";
import {splitEnArticle} from "@/hooks/article.ts";
const store = useBaseStore()
const runtimeStore = useRuntimeStore()

View File

@@ -84,6 +84,7 @@ html.dark {
--color-input-icon: #383737;
--color-textarea-bg: rgb(43, 45, 48);
--color-article: white;
}
@@ -114,7 +115,7 @@ html.dark {
@media (max-width: 1366px) {
:root {
--space: 10rem;
--space: 1rem;
--practice-wrapper-translateX: -22vw;
--article-width: 53vw;
--panel-width: 30vw;
@@ -219,6 +220,11 @@ a {
}
}
.base-input {
@extend .base-textarea;
flex: none;
}
@supports selector(::-webkit-scrollbar) {
::-webkit-scrollbar {
width: .5rem;

View File

@@ -64,6 +64,7 @@ defineEmits(['click'])
height: 2.5rem;
line-height: 1;
position: relative;
word-break: keep-all;
.loading {
position: absolute;

View File

@@ -1,6 +1,6 @@
import {Article, ArticleWord, DefaultArticleWord, DictType, Sentence, TranslateType} from "@/types.ts";
import {cloneDeep} from "lodash-es";
import nlp from "compromise";
import nlp from "compromise/one";
import {split} from "sentence-splitter";
interface KeyboardMap {
@@ -30,30 +30,53 @@ export const EnKeyboardMap: KeyboardMap = {
}
export function splitEnArticle(text: string): { sections: Sentence[][], newText: string } {
// console.log(text)
console.log('splitEnArticle')
//将中文符号替换
text = text.replaceAll('', "'")
text = text.replaceAll('—', "-")
text = text.replaceAll('”', '"')
text = text.replaceAll('“', '"')
// console.time()
let keyboardMap = EnKeyboardMap
// text = "On Jan. 20, former Sen. Barack Obama became the 44th President of the U.S. Millions attended the Inauguration. 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'LICENSE 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'LICENSE one o'clock'"
// text = "Mr.James Scott has a garage in Silbury and now he has just bought another garage in Pinhurst. Pinhurst is only five miles from Silbury, but Mr. Scott cannot get a telephone for his new garage, so he has just bought twelve pigeons. Yesterday, a pigeon carried the first message from Pinhurst to Silbury. The bird covered the distance in three minutes. Up to now, Mr.Scott has sent a great many requests for spare parts and other urgent messages from one garage to the other. In this way, he has begun his own private 'telephone' service."
// text = "How does the older investor differ in his approach to investment from the younger investor?\nThere is no shortage of tipsters around offering 'get-rich-quick' opportunities. But if you are a serious private investor, leave the Las Vegas mentality to those with money to fritter. The serious investor needs a proper 'portfolio' -- a well-planned selection of investments, with a definite structure and a clear aim. But exactly how does a newcomer to the stock market go about achieving that?\nWell, if you go to five reputable stock brokers and ask them what you should do with your money, you're likely to get five different answers, -- even if you give all the relevant information about your age age, family, finances and what you want from your investments. Moral? There is no one 'right' way to structure a portfolio. However, there are undoubtedly some wrong ways, and you can be sure that none of our five advisers would have suggested sinking all (or perhaps any) of your money into Periwigs*.\nSo what should you do? We'll assume that you have sorted out the basics -- like mortgages, pensions, insurance and access to sufficient cash reserves. You should then establish your own individual aims. These are partly a matter of personal circumstances, partly a matter of psychology.\nFor instance, if you are older you have less time to recover from any major losses, and you may well wish to boost your pension income. So preserving your capital and generating extra income are your main priorities. In this case, you'd probably construct a portfolio with some shares (but not high risk ones), along with gilts, cash deposits, and perhaps convertibles or the income shares of split capital investment trusts.\nIf you are younger, and in a solid financial position, you may decide to take an aggressive approach -- but only if you're blessed with a sanguine disposition and won't suffer sleepless nights over share prices. If portfolio, alongside your more pedestrian in vestments. Once you have decided on your investment aims, you can then decide where to put your money. The golden rule here is spread your risk -- if you put all of your money into Periwigs International, you're setting yourself up as a hostage to fortune.\n*'Periwigs' is the name of a fictitious company.\nINVESTOR'S CHRONICLE, March 23 1990"
let sections: Sentence[][] = []
text && text.trim().split('\n').map((rowSection, i) => {
text && text.trim().split('\n\n').filter(v => v).map((sectionText, i) => {
let section: Sentence[] = []
sections.push(section)
rowSection = rowSection.trim()
sectionText = sectionText.trim()
let doc = nlp.tokenize(rowSection)
let sentences = doc.json()
// console.log('--')
// console.log('ss', sentences)
sentences.map(sentenceRow => {
let doc = nlp(sectionText)
let sentenceNlpList = []
// console.log('ss', sentenceNlpList)
doc.json().map(item => {
//如果整句大于15个单词以上检测是否有 逗号子句
if (item.terms.length > 15) {
//正则匹配“逗号加and|but|so|because"
let list = item.text.split(/,\s(?=(and|but|so|because)\b)/).filter(_ => {
//匹配完之后会把and|but|so|because也提出来这里不需要重复的直接筛选掉
if (_ && !['and', 'but', 'so', 'because'].includes(_)) return _
})
if (list.length === 1) {
sentenceNlpList.push(item)
} else {
list.map((text, i) => {
//分割后每句都没有逗号了,所以除了最后一句外需要加回来
sentenceNlpList = sentenceNlpList.concat(nlp(text + (i !== list.length - 1 ? ',' : '')).json())
})
}
} else {
sentenceNlpList.push(item)
}
})
sentenceNlpList.map(item => {
let sentence: Sentence = {
//他没有空格,导致修改一行一行的数据时,汇总时全没有空格了,库无法正常断句
text: sentenceRow.text + ' ',
text: item.text + ' ',
// text: '',
translate: '',
words: []
words: [],
}
section.push(sentence)
@@ -79,7 +102,7 @@ export function splitEnArticle(text: string): { sections: Sentence[][], newText:
isSymbol: true,
symbolPosition: ''
};
// console.log('rrr', sentenceRow)
// console.log('rrr', item)
// console.log('nearSymbolPosition', nearSymbolPosition)
if (nearSymbolPosition === 'end' || nearSymbolPosition === null) {
word3.symbolPosition = 'start'
@@ -174,7 +197,7 @@ export function splitEnArticle(text: string): { sections: Sentence[][], newText:
}
}
sentenceRow.terms.map((v, index: number) => {
item.terms.map((v, index: number) => {
// console.log('v', v)
if (v.text) {
let pre: string = v.pre.trim()
@@ -196,6 +219,8 @@ export function splitEnArticle(text: string): { sections: Sentence[][], newText:
//去除空格占位符
sentence.words = sentence.words.filter(v => v.word !== 'placeholder')
})
// console.log(sentenceNlpList)
})
sections = sections.filter(sectionItem => sectionItem.length)
@@ -207,11 +232,13 @@ export function splitEnArticle(text: string): { sections: Sentence[][], newText:
}, '')
})
})
// console.log(sections)
// console.timeEnd()
// console.log('sections', sections)
// console.log(sections)
text= sections.map(v => v.map(s => s.text.trim()).join('\n')).join('\n\n');
// console.log('s',text)
return {
//s.text.trim()的trim()不能去掉,因为这个方法会重复执行,要保证句子后面只有一个\n不trim() \n就会累加
newText: text,
sections
}

View File

@@ -69,131 +69,127 @@ export async function getNetworkTranslate(
allShow: boolean = false,
progressCb?: (val: number) => void
) {
if (article.textNetworkTranslate) {
renewSectionTranslates(article, article.textNetworkTranslate)
} else {
let translator: Translator
if (translateEngine === TranslateEngine.Baidu) {
translator = new Baidu({
axios: axiosInstance as any,
config: {
appid: "20230910001811857",
key: "Xxe_yftQR3K3Ue43NQMC"
}
}) as any
let translator: Translator
if (translateEngine === TranslateEngine.Baidu) {
translator = new Baidu({
axios: axiosInstance as any,
config: {
appid: "20230910001811857",
key: "Xxe_yftQR3K3Ue43NQMC"
}
}) as any
}
if (translator) {
if (!article.titleTranslate) {
translator.translate(article.title, 'en', 'zh-CN').then(r => {
article.titleTranslate = r.trans.paragraphs[0]
})
}
let promiseList = []
let retryCount = 0
let retryCountMap = new Map()
if (translator) {
if (!article.titleTranslate) {
translator.translate(article.title, 'en', 'zh-CN').then(r => {
article.titleTranslate = r.trans.paragraphs[0]
})
}
let promiseList = []
let retryCount = 0
let retryCountMap = new Map()
const translate = async (sentence: Sentence) => {
try {
let r = await translator.translate(sentence.text, 'en', 'zh-CN')
if (r) {
const cb = () => {
sentence.translate = r.trans.paragraphs[0]
if (!allShow) {
//一次显示所有,顺序会乱
article.textNetworkTranslate += sentence.translate + '\n'
}
const translate = async (sentence: Sentence) => {
try {
let r = await translator.translate(sentence.text, 'en', 'zh-CN')
if (r) {
const cb = () => {
sentence.translate = r.trans.paragraphs[0]
if (!allShow) {
//一次显示所有,顺序会乱
article.textCustomTranslate += sentence.translate + '\n'
}
return Promise.resolve(cb)
} else {
return Promise.reject(() => translate(sentence))
}
} catch (e) {
return Promise.resolve(cb)
} else {
return Promise.reject(() => translate(sentence))
}
} catch (e) {
return Promise.reject(() => translate(sentence))
}
}
let total = 0
let index = 0
article.sections.map(v => total += v.length)
let total = 0
let index = 0
article.sections.map(v => total += v.length)
for (let i = 0; i < article.sections.length; i++) {
let v = article.sections[i]
for (let j = 0; j < v.length; j++) {
let sentence = v[j]
let promise = translate(sentence)
if (allShow) {
promiseList.push(promise)
} else {
retryCountMap.set(sentence.text, 0)
let errResult: any
let cb = await promise.catch(err => {
for (let i = 0; i < article.sections.length; i++) {
let v = article.sections[i]
for (let j = 0; j < v.length; j++) {
let sentence = v[j]
let promise = translate(sentence)
if (allShow) {
promiseList.push(promise)
} else {
retryCountMap.set(sentence.text, 0)
let errResult: any
let cb = await promise.catch(err => {
errResult = err
})
while (errResult) {
let count = retryCountMap.get(sentence.text)
if (count > 2) break
cb = await errResult().catch(err => {
errResult = err
})
while (errResult) {
let count = retryCountMap.get(sentence.text)
if (count > 2) break
cb = await errResult().catch(err => {
errResult = err
})
retryCountMap.set(sentence.text, count + 1)
}
if (cb) cb()
index++
if (progressCb) {
progressCb(Math.floor((index / total) * 100))
}
retryCountMap.set(sentence.text, count + 1)
}
}
}
if (promiseList.length) {
let timer = -1
let progress = 0
if (progressCb) {
timer = setInterval(() => {
progress++
if (progress > 90) {
return clearInterval(timer)
}
progressCb(progress)
}, 100)
}
return new Promise(async resolve => {
let cbs = []
do {
if (retryCount > 2) {
return resolve(true)
}
let results = await Promise.allSettled(promiseList)
promiseList = []
results.map(results => {
if (results.status === 'fulfilled') {
cbs.push(results.value)
} else {
promiseList.push(results.reason())
}
})
retryCount++
} while (promiseList.length)
cbs.map(v => v())
article.textNetworkTranslate = getSentenceAllTranslateText(article)
if (cb) cb()
index++
if (progressCb) {
clearInterval(timer)
progress = 100
progressCb(100)
progressCb(Math.floor((index / total) * 100))
}
resolve(true)
})
} else {
article.textNetworkTranslate = getSentenceAllTranslateText(article)
}
}
}
if (promiseList.length) {
let timer = -1
let progress = 0
if (progressCb) {
timer = setInterval(() => {
progress++
if (progress > 90) {
return clearInterval(timer)
}
progressCb(progress)
}, 100)
}
return new Promise(async resolve => {
let cbs = []
do {
if (retryCount > 2) {
return resolve(true)
}
let results = await Promise.allSettled(promiseList)
promiseList = []
results.map(results => {
if (results.status === 'fulfilled') {
cbs.push(results.value)
} else {
promiseList.push(results.reason())
}
})
retryCount++
} while (promiseList.length)
cbs.map(v => v())
article.textCustomTranslate = getSentenceAllTranslateText(article)
if (progressCb) {
clearInterval(timer)
progress = 100
progressCb(100)
}
resolve(true)
})
} else {
article.textCustomTranslate = getSentenceAllTranslateText(article)
}
}
}
@@ -202,30 +198,3 @@ export function renewSectionTexts(article: Article) {
article.text = newText
article.sections = sections
}
export function test(article: Article) {
if (article?.textCustomTranslateIsFormat) {
if (!article.sections?.length) {
}
} else {
if (article.useTranslateType !== undefined) {
if (article.useTranslateType) {
renewSectionTranslates(article, article.textCustomTranslate)
} else {
renewSectionTranslates(article, article.textNetworkTranslate)
}
} else {
// article.sections = splitEnArticle(article.article)
if (article.textCustomTranslate) {
article.textCustomTranslate = getSplitTranslateText(article.textCustomTranslate)
renewSectionTranslates(article, article.textCustomTranslate)
article.useTranslateType = TranslateType.custom
} else {
article.textNetworkTranslate = getSplitTranslateText(article.textNetworkTranslate)
renewSectionTranslates(article, article.textCustomTranslate)
article.useTranslateType = TranslateType.network
}
}
}
}

View File

@@ -11,6 +11,8 @@ 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()
@@ -55,86 +57,90 @@ onUnmounted(() => {
<template>
<div class="footer " :class="!settingStore.showToolbar && 'hide'">
<div class="bottom">
<div class="flex gap-2">
<el-progress
class="flex-1"
:percentage="progress"
:stroke-width="8"
:show-text="false"/>
</div>
<div class="flex justify-between">
<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 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>
<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/>
<TranslateSetting/>
<VolumeSetting/>
<VolumeSetting/>
<BaseIcon
:title="`编辑(${settingStore.shortcutKeyMap[ShortcutKey.EditArticle]})`"
icon="tabler:edit"
@click="emit('edit',)"
/>
<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
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"/>
<BaseIcon
@click="settingStore.showPanel = !settingStore.showPanel"
:title="`面板(${settingStore.shortcutKeyMap[ShortcutKey.TogglePanel]})`"
icon="tdesign:menu-unfold"/>
</div>
</div>
</div>
</div>

View File

@@ -1,14 +1,9 @@
<script setup lang="ts">
import {useBaseStore} from "@/stores/base.ts";
import DictListPanel from "@/pages/pc/components/DictListPanel.vue";
import {Icon} from '@iconify/vue'
import {ActivityCalendar} from "vue-activity-calendar";
import "vue-activity-calendar/style.css";
import {useRouter} from "vue-router";
import BaseIcon from "@/components/BaseIcon.vue";
import DictList from "@/pages/pc/components/list/DictList.vue";
import {enArticle} from "@/assets/dictionary.ts";
import {DictType} from "@/types.ts";
import BasePage from "@/pages/pc/components/BasePage.vue";
const base = useBaseStore()

View File

@@ -53,7 +53,6 @@ function toggle() {
<div
v-else
class="text"
:style="`font-size: 1rem;`"
@click="toggle">
{{ value }}
</div>

View File

@@ -5,15 +5,13 @@ import {useSettingStore} from "@/stores/setting.ts";
import {getAudioFileUrl, useChangeAllSound, usePlayAudio, useWatchAllSound} from "@/hooks/sound.ts";
import {getShortcutKey, useDisableEventListener, useEventListener} from "@/hooks/event.ts";
import {cloneDeep} from "lodash-es";
import {DefaultShortcutKeyMap, Dict, DictType, ShortcutKey} from "@/types.ts";
import {DefaultShortcutKeyMap, ShortcutKey} from "@/types.ts";
import BaseButton from "@/components/BaseButton.vue";
import {APP_NAME, EXPORT_DATA_KEY, SAVE_DICT_KEY, SAVE_SETTING_KEY, SoundFileOptions} from "@/utils/const.ts";
import VolumeIcon from "@/components/icon/VolumeIcon.vue";
import {BaseState, useBaseStore} from "@/stores/base.ts";
import * as copy from "copy-to-clipboard";
import {useBaseStore} from "@/stores/base.ts";
import {saveAs} from "file-saver";
import {checkAndUpgradeSaveDict, checkAndUpgradeSaveSetting, shakeCommonDict} from "@/utils";
import {dayjs} from "element-plus";
import {GITHUB} from "@/config/ENV.ts";

View File

@@ -11,13 +11,17 @@ import {
renewSectionTexts,
renewSectionTranslates
} from "@/hooks/translate.ts";
import * as copy from "copy-to-clipboard";
import {MessageBox} from "@/utils/MessageBox.tsx";
import {getSplitTranslateText} from "@/hooks/article.ts";
import {cloneDeep} from "lodash-es";
import {watch} from "vue";
import {watch, ref} from "vue";
import Empty from "@/components/Empty.vue";
import {UploadProps, UploadUserFile} from "element-plus";
import {_copy, _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";
interface IProps {
article?: Article,
@@ -70,6 +74,12 @@ watch(() => props.article, val => {
// console.log('ar', article)
}, {immediate: true})
watch(() => editArticle.text, (s) => {
if (!s.trim()) {
editArticle.sections = []
}
})
function renewSections() {
if (editArticle.text.trim()) {
renewSectionTexts(editArticle)
@@ -136,7 +146,6 @@ async function startNetworkTranslate() {
return ElMessage.error('请填写正文!')
}
renewSectionTexts(editArticle)
editArticle.textNetworkTranslate = ''
//注意!!!
//这里需要用异步因为watch了article.networkTranslate改变networkTranslate了之后会重新设置article.sections
//导致getNetworkTranslate里面拿到的article.sections是废弃的值
@@ -145,8 +154,6 @@ async function startNetworkTranslate() {
progress = v
})
failCount = 0
copy(JSON.stringify(editArticle.sections))
})
}
@@ -230,61 +237,147 @@ 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(editArticle.sections.flat())
// console.log(cosine.similarity('Thanos', 'Rival'))
}
const a = new Audio(audio)
function play(sentence: Sentence) {
if (sentence.audioPosition?.length) {
let start = sentence.audioPosition[0];
a.currentTime = start
a.play()
let end = sentence.audioPosition?.[1]
if (end && end !== -1) {
setTimeout(() => {
a.pause()
}, (end - start) * 1000)
}
}
}
function s() {
}
</script>
<template>
<div class="content">
<div class="row">
<div class="title">原文</div>
<div class="item">
<div class="label">标题</div>
<textarea
v-model="editArticle.title"
type="textarea"
class="base-textarea"
placeholder="请填写原文标题"
>
</textarea>
</div>
<div class="item basic">
<div class="label">正文</div>
<textarea
v-model="editArticle.text"
@input="renewSections"
:readonly="![100,0].includes(progress)"
type="textarea"
class="base-textarea"
placeholder="请填写原文正文"
>
<div class="row flex flex-col gap-2">
<div class="title">原文</div>
<div class="">标题</div>
<input
v-model="editArticle.title"
type="text"
class="base-input"
placeholder="请填写原文标题"
/>
<div class="">正文</div>
<textarea
v-model="editArticle.text"
:readonly="![100,0].includes(progress)"
type="textarea"
class="base-textarea"
placeholder="请复制原文"
>
</textarea>
<div class="justify-between items-center gap-2 flex">
<div class="text-base mb-1 color-white/60">请复制原文然后进行分句一行一句段落间空一行修改完成后点击 <span class="color-red font-bold">应用按钮</span> 同步到左侧结果
</div>
<el-button type="primary" @click="renewSections">应用</el-button>
</div>
</div>
<div class="row">
<div class="title">译文</div>
<div class="item">
<div class="label">
<span>标题</span>
<el-radio-group
v-model="editArticle.useTranslateType"
@change="renewSections"
>
<el-radio-button :value="TranslateType.custom">本地翻译</el-radio-button>
<el-radio-button :value="TranslateType.network">网络翻译</el-radio-button>
<el-radio-button :value="TranslateType.none">不需要翻译</el-radio-button>
</el-radio-group>
</div>
<textarea
v-model="editArticle.titleTranslate"
type="textarea"
class="base-textarea"
placeholder="请填写翻译标题"
>
</textarea>
<div class="row flex flex-col gap-2">
<div class="title">译文</div>
<div class="flex gap-2">
标题
</div>
<div class="item basic">
<div class="label">
<span>正文</span>
<div class="translate-item" v-if="editArticle.useTranslateType === TranslateType.network">
<input
v-model="editArticle.titleTranslate"
type="text"
class="base-input"
placeholder="请填写翻译标题"
/>
<div class="flex">
<span>正文</span>
</div>
<textarea
v-model="editArticle.textCustomTranslate"
:readonly="![100,0].includes(progress)"
@blur="onBlur"
@focus="onFocus"
type="textarea"
class="base-textarea"
placeholder="请填写翻译"
ref="textareaRef"
>
</textarea>
<div class="justify-between items-center gap-2 flex">
<div class="text-base mb-1 color-white/60">
请复制译文如果没有可点击网络翻译按钮进行翻译然后进行分句一行一句段落间空一行修改完成后点击 <span class="color-red font-bold">应用按钮</span> 同步到左侧结果
</div>
<div class="flex flex-col gap-2">
<div class="translate-item">
<el-progress :percentage="progress"
:duration="30"
:striped="progress !== 100"
@@ -292,7 +385,6 @@ defineExpose({save, getEditArticle: () => cloneDeep(editArticle)})
:stroke-width="8"
:show-text="true"/>
<el-select v-model="networkTranslateEngine"
style="width: 80rem;"
>
<el-option
v-for="item in TranslateEngineOptions"
@@ -305,55 +397,57 @@ defineExpose({save, getEditArticle: () => cloneDeep(editArticle)})
size="small"
@click="startNetworkTranslate"
:loading="progress!==0 && progress !== 100"
>开始翻译
>网络翻译
</BaseButton>
</div>
<div class="flex justify-end">
<el-button type="primary" @click="renewSections">应用</el-button>
</div>
</div>
<textarea
v-if="editArticle.useTranslateType === TranslateType.custom"
v-model="editArticle.textCustomTranslate"
@input="renewSections"
@blur="onBlur"
@focus="onFocus"
type="textarea"
class="base-textarea"
placeholder="请填写翻译正文"
ref="textareaRef"
>
</textarea>
<textarea
v-if="editArticle.useTranslateType === TranslateType.network"
v-model="editArticle.textNetworkTranslate"
:readonly="![100,0].includes(progress)"
@input="renewSections"
@blur="onBlur"
@focus="onFocus"
type="textarea"
class="base-textarea"
placeholder="等待网络翻译中..."
ref="textareaRef"
>
</textarea>
<Empty
text="不需要翻译~"
v-if="editArticle.useTranslateType === TranslateType.none"
/>
</div>
</div>
<div class="row">
<div class="title">③译文对照</div>
<div class="title">结果</div>
<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"
>
<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-upload>
<audio :src="audio" controls></audio>
</div>
<template v-if="editArticle.sections.length">
<div class="article-translate">
<div class="section" v-for="(item,indexI) in editArticle.sections">
<div class="sentence" v-for="(sentence,indexJ) in item">
<EditAbleText
:value="sentence.text"
@save="(e:string) => saveSentenceText(sentence,e)"
/>
<EditAbleText
:value="sentence.translate"
@save="(e:string) => saveSentenceTranslate(sentence,e)"
/>
<div class="sentence flex justify-between" v-for="(sentence,indexJ) in item">
<div>
<EditAbleText
:value="sentence.text"
@save="(e:string) => saveSentenceText(sentence,e)"
/>
<EditAbleText
v-if="sentence.translate"
:value="sentence.translate"
@save="(e:string) => saveSentenceTranslate(sentence,e)"
/>
</div>
<div class="flex items-center gap-2">
<div class="text-base"> {{ sentence.audioPosition?.[0] }} - {{ sentence.audioPosition?.[1] }}</div>
<div>
<BaseIcon icon="hugeicons:play" @click="play(sentence)"/>
</div>
</div>
</div>
</div>
</div>
@@ -384,7 +478,7 @@ defineExpose({save, getEditArticle: () => cloneDeep(editArticle)})
@import "@/assets/css/style";
.content {
color: var(--color-font-1);
color: var(--color-article);
flex: 1;
display: flex;
gap: var(--space);
@@ -393,7 +487,7 @@ defineExpose({save, getEditArticle: () => cloneDeep(editArticle)})
}
.row {
flex: 10;
flex: 7;
width: 33%;
//height: 100%;
display: flex;
@@ -404,28 +498,21 @@ defineExpose({save, getEditArticle: () => cloneDeep(editArticle)})
flex: 1;
display: flex;
flex-direction: column;
gap: 0.8rem;
}
&:nth-child(1) {
flex: 7;
&:nth-child(3) {
flex: 10;
}
.title {
font-weight: bold;
font-size: 1.4rem;
text-align: center;
}
.item {
width: 100%;
//margin-bottom: 10rem;
.label {
height: 3rem;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 1rem;
}
}
.translate-item {
@@ -458,7 +545,8 @@ defineExpose({save, getEditArticle: () => cloneDeep(editArticle)})
}
.sentence {
margin-bottom: 1.2rem;
margin-bottom: 0.5rem;
line-height: 1.2;
&:last-child {
margin-bottom: 0;

View File

@@ -21,6 +21,7 @@ import {useWindowClick} from "@/hooks/event.ts";
import ArticleList from "@/pages/pc/components/list/ArticleList.vue";
import * as copy from "copy-to-clipboard";
import {getTranslateText} from "@/hooks/article.ts";
import {_copy} from "@/utils";
const emit = defineEmits<{
back: []
@@ -210,7 +211,7 @@ function exportData(val: {
type: string,
data?: Article
}) {
copy(JSON.stringify(cloneDeep(runtimeStore.editDict.articles).map(v => {
_copy(JSON.stringify(cloneDeep(runtimeStore.editDict.articles).map(v => {
delete v.sections
return v
})))

View File

@@ -1,7 +1,24 @@
<script setup lang="ts">
import {splitEnArticle} from "@/hooks/article.ts";
function test() {
let {newText, sections} = splitEnArticle(
`While it is yet to be seen what direction the second Trump administration will take globally in its China policy, VOA traveled to the main island of Mahe in Seychelles to look at how China and the U.S. have impacted the country, and how each is fairing in that competition for influence there.
`)
}
function test2() {
let {newText, sections} = splitEnArticle(
`
Last week I went to the theatre. I had a very good seat. The play was very interesting. I did not enjoy it. A young man and a young woman were sitting behind me. They were talking loudly. I got very angry. I could not hear the actors. I turned round. I looked at the man and the woman angrily. They did not pay any attention. In the end, I could not bear it. I turned round again. I cant hear a word! I said angrily.
Its none of your business, the young man said rudely. This is a private conversation!
`)
}
</script>
<template>
<div class="word flex justify-center ">
<div class="word flex center h-screen ">
<El-Button @click="test">test</El-Button>
<El-Button @click="test2">test2</El-Button>
</div>
</template>

View File

@@ -95,7 +95,7 @@ watch(() => settingStore.translate, () => {
function checkCursorPosition(a = sectionIndex, b = sentenceIndex, c = wordIndex) {
console.log('checkCursorPosition')
// console.log('checkCursorPosition')
_nextTick(() => {
let currentWord = jq(`.section:nth(${a}) .sentence:nth(${b}) .word:nth(${c})`)
// console.log(a, b, c, currentWord)
@@ -115,7 +115,7 @@ function checkCursorPosition(a = sectionIndex, b = sentenceIndex, c = wordIndex)
}
function checkTranslateLocation() {
console.log('checkTranslateLocation')
// console.log('checkTranslateLocation')
return new Promise<void>(resolve => {
_nextTick(() => {
let articleRect = articleWrapperRef.getBoundingClientRect()
@@ -129,7 +129,7 @@ function checkTranslateLocation() {
let translate: HTMLDivElement = document.querySelector(translateClassName)
translate.style.opacity = '1'
translate.style.top = wordRect.top - articleRect.top + 28 + 'px'
translate.style.top = wordRect.top - articleRect.top + 24 + 'px'
// @ts-ignore
translate.firstChild.style.width = wordRect.left - articleRect.left + 'px'
// console.log(word, wordRect.left - articleRect.left)
@@ -469,8 +469,6 @@ defineExpose({showSentence, play, del, hideSentence, nextSentence})
//color: rgb(22, 163, 74);
}
$article-width: 1000px;
.typing-article {
height: 100%;
width: 100%;
@@ -538,18 +536,15 @@ $article-width: 1000px;
}
&.tall {
line-height: 2.5;
line-height: 2.2;
}
.section {
margin-bottom: 1.2rem;
margin-bottom: 1.5rem;
.sentence {
transition: all .3s;
&:first-child {
padding-left: 3rem;
}
}
.word {
@@ -580,8 +575,8 @@ $article-width: 1000px;
left: 0;
height: 100%;
width: 100%;
font-size: 1.1rem;
line-height: 3.5;
font-size: 1.2rem;
line-height: 3.0;
letter-spacing: .2rem;
font-family: var(--zh-article-family);
font-weight: bold;

View File

@@ -94,7 +94,8 @@ export interface ArticleWord extends Word {
export interface Sentence {
text: string,
translate: string,
words: ArticleWord[]
words: ArticleWord[],
audioPosition: number[]
}
export enum TranslateType {

View File

@@ -260,4 +260,30 @@ export function _nextTick(cb: Function, time?: number) {
} else {
nextTick(cb)
}
}
export function _copy(val: string) {
navigator.clipboard.writeText(val)
}
export function _parseLRC(lrc: string): { start: number, end: number, text: string }[] {
const lines = lrc.split("\n").filter(line => line.trim() !== "");
const regex = /\[(\d{2}):(\d{2}\.\d{2})\](.*)/;
let parsed: any = [];
for (let i = 0; i < lines.length; i++) {
let match = lines[i].match(regex);
if (match) {
let start = parseFloat(match[1]) * 60 + parseFloat(match[2]); // 转换成秒
let text = match[3].trim();
// 计算结束时间(下一个时间戳)
let nextMatch = lines[i + 1] ? lines[i + 1].match(regex) : null;
let end = nextMatch ? parseFloat(nextMatch[1]) * 60 + parseFloat(nextMatch[2]) : null;
parsed.push({start, end, text});
}
}
return parsed;
}