feat:save

This commit is contained in:
zyronon
2025-07-09 04:11:48 +08:00
parent b7fa79d148
commit 50bea6d759
46 changed files with 2519 additions and 1108 deletions

3
components.d.ts vendored
View File

@@ -14,14 +14,13 @@ declare module 'vue' {
DeleteIcon: typeof import('./src/components/icon/DeleteIcon.vue')['default']
ElButton: typeof import('element-plus/es')['ElButton']
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
ElCollapse: typeof import('element-plus/es')['ElCollapse']
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
ElForm: typeof import('element-plus/es')['ElForm']
ElFormItem: typeof import('element-plus/es')['ElFormItem']
ElInput: typeof import('element-plus/es')['ElInput']
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
ElOption: typeof import('element-plus/es')['ElOption']
ElPagination: typeof import('element-plus/es')['ElPagination']
ElPopover: typeof import('element-plus/es')['ElPopover']
ElProgress: typeof import('element-plus/es')['ElProgress']
ElRadio: typeof import('element-plus/es')['ElRadio']
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']

View File

@@ -15,14 +15,14 @@
"i18n:write": "gulp i18nwrite"
},
"dependencies": {
"@imengyu/vue3-context-menu": "^1.5.0",
"@imengyu/vue3-context-menu": "^1.5.1",
"@opentranslate/baidu": "^1.4.2",
"@opentranslate/translator": "^1.4.2",
"axios": "^1.9.0",
"axios": "^1.10.0",
"compromise": "^14.14.4",
"copy-to-clipboard": "^3.3.3",
"dayjs": "^1.11.13",
"element-plus": "^2.9.10",
"element-plus": "^2.10.3",
"file-saver": "^2.0.5",
"git-last-commit": "^1.0.1",
"hover.css": "^2.3.2",
@@ -32,16 +32,15 @@
"lodash-es": "^4.17.21",
"mitt": "^3.0.1",
"nanoid": "^5.1.5",
"pinia": "^2.3.1",
"pinia": "^3.0.3",
"sentence-splitter": "^4.4.1",
"string-comparison": "^1.3.0",
"tesseract.js": "^4.1.4",
"vant": "^4.9.19",
"vue": "^3.5.14",
"vant": "^4.9.20",
"vue": "^3.5.17",
"vue-activity-calendar": "^1.2.2",
"vue-i18n": "^9.14.4",
"vue-router": "^4.5.1",
"vue-toast-notification": "^3.1.3",
"vue-virtual-scroller": "2.0.0-beta.8"
},
"devDependencies": {
@@ -49,24 +48,25 @@
"@types/file-saver": "^2.0.7",
"@types/lodash-es": "^4.17.12",
"@unocss/postcss": "^0.60.4",
"@vitejs/plugin-vue": "^4.6.2",
"@vitejs/plugin-vue-jsx": "^3.1.0",
"@vue/compiler-sfc": "^3.5.14",
"@vitejs/plugin-vue": "^6.0.0",
"@vitejs/plugin-vue-jsx": "^5.0.1",
"@vue/compiler-sfc": "^3.5.17",
"commitizen": "^4.3.1",
"cz-conventional-changelog": "^3.3.0",
"esm": "^3.2.25",
"gulp": "^4.0.2",
"husky": "^8.0.3",
"rollup-plugin-visualizer": "^5.14.0",
"sass": "^1.89.0",
"sass": "^1.89.2",
"tslib": "^2.8.1",
"typescript": "^5.8.3",
"unocss": "^0.60.4",
"unocss": "^66.3.3",
"unplugin-auto-import": "^0.16.7",
"unplugin-vue-components": "^0.25.2",
"unplugin-vue-macros": "^2.14.5",
"vite": "^5.4.19",
"vue-tsc": "^2.2.10",
"vite": "^7.0.3",
"vite-plugin-cdn-import": "^1.0.1",
"vue-tsc": "^3.0.1",
"xlsx": "^0.18.5"
},
"config": {

2250
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -12,8 +12,8 @@
{
"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\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!'",
"textTranslate": "那是个星期天,\n而在星期天我是从来不早起的\n有时我要一直躺到吃午饭的时候。\n上个星期天我起得很晚。\n我望望窗外\n外面一片昏暗。\n“鬼天气”我想\n“又下雨了。”正在这时,电话铃响了。\n是我姑母露西打来的。\n“我刚下火车”她说\n“我这就来看你。”\n\n“但我还在吃早饭”我说。\n\n“你在干什么”她问道。\n\n“我正在吃早饭”我又说了一遍。\n\n“天啊”她说\n“你总是起得这么晚吗现在已经1点钟了",
"text": "It was Sunday. \nI never get up early on Sundays. \nI sometimes stay in bed until lunchtime. \nLast Sunday I got up very late. \nI looked out of the window. \nIt was dark outside. \n'What a day!' I thought. 'It's raining again.'\n Just then, the telephone rang. \nIt was my aunt Lucy. \n'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!'",
"textTranslate": "那是个星期天,\n而在星期天我是从来不早起的\n有时我要一直躺到吃午饭的时候。\n上个星期天我起得很晚。\n我望望窗外\n外面一片昏暗。\n“鬼天气”我想“又下雨了。”\n正在这时,电话铃响了。\n是我姑母露西打来的。\n“我刚下火车”她说“我这就来看你。”\n\n“但我还在吃早饭”我说。\n\n“你在干什么”她问道。\n\n“我正在吃早饭”我又说了一遍。\n\n“天啊”她说“你总是起得这么晚吗现在已经1点钟了",
"newWords": [],
"audioSrc": "/public/sound/article/nce2-1/02Breakfast or Lunch.mp3",
"lrcPosition": [[15.9,17.48],[17.75,21.51],[21.54,26.13],[26.58,30.47],[30.86,33.34],[33.34,35.68],[35.68,39.41],[39.41,45.64],[45.64,48.45],[48.45,53.01],[53.01,55.3],[55.3,60.11],[60.11,63.68],[63.4,67.15],[67.3,70.19],[69.98,75.54]],
@@ -1418,4 +1418,4 @@
"audioSrc": "/public/sound/article/nce2-1/12Goodbye and Good Luck.mp3",
"lrcPosition": []
}
]
]

View File

@@ -82,7 +82,6 @@ watch(() => route.path, (to, from) => {
</template>
<style scoped lang="scss">
@import "@/assets/css/style";
.main-page {
position: relative;

View File

@@ -444,7 +444,6 @@ html, body {
}
</style>
<style scoped lang="scss">
@import "assets/css/variable";
.mobile {
width: 100vw;
@@ -459,4 +458,4 @@ html, body {
}
}
</style>
</style>

View File

@@ -1,8 +1,7 @@
//@import '/node_modules/element-plus/dist/index.css';
@import "/node_modules/hover.css";
@import "variable.scss";
@import "anim";
@import 'element-plus/theme-chalk/dark/css-vars';
//@use "/node_modules/hover.css" as *;
@use "anim" as *;
@use 'element-plus/theme-chalk/dark/css-vars' as *;
:root {
--color-background: #E6E8EB;
@@ -485,4 +484,4 @@ footer {
.center {
@apply flex justify-center items-center;
}
}

View File

@@ -48,7 +48,6 @@ defineEmits(['click'])
</template>
<style scoped lang="scss">
@import "@/assets/css/style";
.base-button {
cursor: pointer;
@@ -139,4 +138,4 @@ defineEmits(['click'])
transform: scale(0.8);
}
}
</style>
</style>

View File

@@ -30,6 +30,7 @@ export const EnKeyboardMap: KeyboardMap = {
QuoteRight: `'`,
}
//TODO 废弃
export function splitEnArticle(text: string): { sections: Sentence[][], newText: string } {
console.log('splitEnArticle')
//将中文符号替换
@@ -252,6 +253,7 @@ export function splitEnArticle(text: string): { sections: Sentence[][], newText:
}
}
//TODO 废弃
export function splitCNArticle(text: string): Sentence[][] {
// text = "飞机误点了,侦探们在机场等了整整一上午。他们正期待从南非来的一个装着钻石的贵重包裹。数小时以前,有人向警方报告,说有人企图偷走这些钻石。当飞机到达时,一些侦探等候在主楼内,另一些侦探则守候在停机坪上。有两个人把包裹拿下飞机,进了海关。这时两个侦探把住门口,另外两个侦探打开了包裹。令他们吃惊的是,那珍贵的包裹里面装的全是石头和沙子!"
// text = "那是 4.4 个星期天?而在星期天我是从来不早起的,有时我要一直躺到吃午饭的时候。上个星期天,我起得很晚。我望望窗外,外面一片昏暗。“鬼天气!”我想,“又下雨了。”正在这时,电话铃响了。是我姑母露西打来的。“我刚下火车,”她说,“我这就来看你。”\n “但我还在吃早饭,”我说。\n “你在干什么?”她问道。\n “我正在吃早饭,”我又说了一遍。\n “天啊”她说“你总是起得这么晚吗现在已经1点钟了”"
@@ -274,7 +276,7 @@ export function splitCNArticle(text: string): Sentence[][] {
}
section.push(sentence)
if (row) {
//这个库总是会把反引号给断句到下一行
//sentence-splitter 这个库总是会把反引号给断句到下一行
if (row[0] === "”") {
sentence.text = row.substr(1)
let lastSentence = section[section.length - 2]
@@ -292,6 +294,468 @@ export function splitCNArticle(text: string): Sentence[][] {
return sections
}
//生成文章段落数据
export function genArticleSectionData(text: string): Sentence[][] {
if (!text) {
// text = "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 can't hear a word!' I said angrily.\n\n 'It's none of your business,' the young man said rudely. 'This is a private conversation!'"
// text = `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.`
// 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!'"
}
console.log(text)
// console.time()
let keyboardMap = EnKeyboardMap
let sections: Sentence[][] = []
let sectionTextList = text.split('\n\n')
// console.log(sectionTextList);
sectionTextList.filter(v => v).map((sectionText, i) => {
let section: Sentence[] = []
sections.push(section)
sectionText = sectionText.trim()
let sentenceNlpList = []
sectionText.split('\n').map((rowSection, i) => {
let doc = nlp(rowSection)
let temp = {text: '', terms: []}
doc.json().map(item => {
temp.text += item.text
temp.terms = temp.terms.concat(item.terms)
})
sentenceNlpList.push(temp)
})
sentenceNlpList.map(item => {
let sentence: Sentence = cloneDeep({
//他没有空格,导致修改一行一行的数据时,汇总时全没有空格了,库无法正常断句
text: item.text + ' ',
// text: '',
translate: '',
words: [],
audioPosition: [0, 0],
})
section.push(sentence)
const checkQuote = (pre: string, index?: number) => {
let nearSymbolPosition = null
if (index === 0) {
nearSymbolPosition = 'end'
} else {
//TODO 可以优化成for+break
section.toReversed().map((sentenceItem, b) => {
sentenceItem.words.toReversed().map((wordItem, c) => {
if (wordItem.symbolPosition !== '' && nearSymbolPosition === null) {
nearSymbolPosition = wordItem.symbolPosition
}
})
})
}
let word3: ArticleWord = {
...DefaultArticleWord,
word: pre,
nextSpace: false,
isSymbol: true,
symbolPosition: ''
};
// console.log('rrr', item)
// console.log('nearSymbolPosition', nearSymbolPosition)
if (nearSymbolPosition === 'end' || nearSymbolPosition === null) {
word3.symbolPosition = 'start'
sentence.words.push(word3)
} else {
sentence.words[sentence.words.length - 1].nextSpace = false
word3.symbolPosition = 'end'
word3.nextSpace = true
let addCurrent = false
sentence.words.toReversed().map((wordItem, c) => {
if (wordItem.symbolPosition === 'start' && !addCurrent) {
addCurrent = true
}
})
if (addCurrent) {
sentence.words.push(word3)
} else {
// 'Do you always get up so late? It'LICENSE one o'clock!' 会被断成两句
let lastSentence = section[section.length - 2]
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()
}
}
}
const checkSymbol = (post: string, nextSpace: boolean = true) => {
switch (post) {
case keyboardMap.Period:
case keyboardMap.Comma:
case keyboardMap.Slash:
case keyboardMap.Exclamation:
sentence.words[sentence.words.length - 1].nextSpace = false
let word2 = cloneDeep({
...DefaultArticleWord,
word: post,
isSymbol: true,
nextSpace
});
sentence.words.push(word2)
break
case keyboardMap.QuoteLeft:
case ')':
checkQuote(post)
break
case `.'`:
case `!'`:
case `?'`:
case `,'`:
case `*'`:
post.split('').map(v => {
checkSymbol(v, false)
})
break
//类似于这种的“' -- ”的。需要保留空格用了一个占位符才处理因为每个符号都会把前面的那个字符的nextSpace改为false
case ' ':
// console.log('sentence', sentence)
//遇到“The clock has stopped!' I looked at my watch.”
//检测到stopped!' 的'时如果前引号不在当前句会把当前句的word合并到前一句。那么当前句的word就为空了会报错
//所以需要检测一下
if (sentence.words.length) {
sentence.words[sentence.words.length - 1].nextSpace = true
let word3 = cloneDeep({
...DefaultArticleWord,
word: 'placeholder',
isSymbol: true,
nextSpace: false,
});
sentence.words.push(word3)
}
break
default:
// console.log('post', post)
//这里多半是一些奇怪的连接符之类的
if (post.length > 1) {
post.split('').map(v => {
checkSymbol(v, false)
})
} else {
sentence.words[sentence.words.length - 1].nextSpace = false
let word3 = cloneDeep({
...DefaultArticleWord,
word: post,
isSymbol: true,
nextSpace: false,
});
sentence.words.push(word3)
}
break
}
}
item.terms.map((v, index: number) => {
// console.log('v', v)
if (v.text) {
let pre: string = v.pre.trim()
if (pre) {
checkQuote(pre, index)
}
let word = cloneDeep({...DefaultArticleWord, word: v.text, nextSpace: true});
sentence.words.push(word)
let post: string = v.post
//判断是不是等于空,因为正常的词后面都会有个空格。这种不需要处理。
if (post && post !== ' ') {
checkSymbol(post.trim())
}
}
})
//去除空格占位符
sentence.words = sentence.words.filter(v => v.word !== 'placeholder')
//如果是空的,直接去掉
if (!sentence.words.length) {
section.pop()
}
})
// console.log('section', section)
})
sections = sections.filter(sectionItem => sectionItem.length)
sections.map((sectionItem, a) => {
sectionItem.map((sentenceItem, b) => {
sentenceItem.text = sentenceItem.words.reduce((previousValue: string, currentValue) => {
previousValue += currentValue.word + (currentValue.nextSpace ? ' ' : '')
return previousValue
}, '')
})
})
// console.log(sections)
return sections
}
export function splitEnArticle2(text: string): string {
if (!text) {
// text = "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 can't hear a word!' I said angrily.\n\n 'It's none of your business,' the young man said rudely. 'This is a private conversation!'"
// text = `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.`
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!'"
}
//将中文符号替换
text = text.replaceAll('', "'")
text = text.replaceAll('—', "-")
text = text.replaceAll('”', '"')
text = text.replaceAll('“', '"')
// console.time()
let keyboardMap = EnKeyboardMap
let sections: Sentence[][] = []
let sectionTextList = text.replaceAll('\n\n', '`^`').replaceAll('\n', '').split('`^`')
// console.log(sectionTextList);
sectionTextList.filter(v => v).map((sectionText, i) => {
let section: Sentence[] = []
sections.push(section)
sectionText = sectionText.trim()
let doc = nlp(sectionText)
let 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 = cloneDeep({
//他没有空格,导致修改一行一行的数据时,汇总时全没有空格了,库无法正常断句
text: item.text + ' ',
// text: '',
translate: '',
words: [],
audioPosition: [0, 0],
})
section.push(sentence)
const checkQuote = (pre: string, index?: number) => {
let nearSymbolPosition = null
if (index === 0) {
nearSymbolPosition = 'end'
} else {
//TODO 可以优化成for+break
section.toReversed().map((sentenceItem, b) => {
sentenceItem.words.toReversed().map((wordItem, c) => {
if (wordItem.symbolPosition !== '' && nearSymbolPosition === null) {
nearSymbolPosition = wordItem.symbolPosition
}
})
})
}
let word3: ArticleWord = {
...DefaultArticleWord,
word: pre,
nextSpace: false,
isSymbol: true,
symbolPosition: ''
};
// console.log('rrr', item)
// console.log('nearSymbolPosition', nearSymbolPosition)
if (nearSymbolPosition === 'end' || nearSymbolPosition === null) {
word3.symbolPosition = 'start'
sentence.words.push(word3)
} else {
sentence.words[sentence.words.length - 1].nextSpace = false
word3.symbolPosition = 'end'
word3.nextSpace = true
let addCurrent = false
sentence.words.toReversed().map((wordItem, c) => {
if (wordItem.symbolPosition === 'start' && !addCurrent) {
addCurrent = true
}
})
if (addCurrent) {
sentence.words.push(word3)
} else {
// 'Do you always get up so late? It'LICENSE one o'clock!' 会被断成两句
let lastSentence = section[section.length - 2]
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()
}
}
}
const checkSymbol = (post: string, nextSpace: boolean = true) => {
switch (post) {
case keyboardMap.Period:
case keyboardMap.Comma:
case keyboardMap.Slash:
case keyboardMap.Exclamation:
sentence.words[sentence.words.length - 1].nextSpace = false
let word2 = cloneDeep({
...DefaultArticleWord,
word: post,
isSymbol: true,
nextSpace
});
sentence.words.push(word2)
break
case keyboardMap.QuoteLeft:
case ')':
checkQuote(post)
break
case `.'`:
case `!'`:
case `?'`:
case `,'`:
case `*'`:
post.split('').map(v => {
checkSymbol(v, false)
})
break
//类似于这种的“' -- ”的。需要保留空格用了一个占位符才处理因为每个符号都会把前面的那个字符的nextSpace改为false
case ' ':
// console.log('sentence', sentence)
//遇到“The clock has stopped!' I looked at my watch.”
//检测到stopped!' 的'时如果前引号不在当前句会把当前句的word合并到前一句。那么当前句的word就为空了会报错
//所以需要检测一下
if (sentence.words.length) {
sentence.words[sentence.words.length - 1].nextSpace = true
let word3 = cloneDeep({
...DefaultArticleWord,
word: 'placeholder',
isSymbol: true,
nextSpace: false,
});
sentence.words.push(word3)
}
break
default:
// console.log('post', post)
//这里多半是一些奇怪的连接符之类的
if (post.length > 1) {
post.split('').map(v => {
checkSymbol(v, false)
})
} else {
sentence.words[sentence.words.length - 1].nextSpace = false
let word3 = cloneDeep({
...DefaultArticleWord,
word: post,
isSymbol: true,
nextSpace: false,
});
sentence.words.push(word3)
}
break
}
}
item.terms.map((v, index: number) => {
// console.log('v', v)
if (v.text) {
let pre: string = v.pre.trim()
if (pre) {
checkQuote(pre, index)
}
let word = cloneDeep({...DefaultArticleWord, word: v.text, nextSpace: true});
sentence.words.push(word)
let post: string = v.post
//判断是不是等于空,因为正常的词后面都会有个空格。这种不需要处理。
if (post && post !== ' ') {
checkSymbol(post.trim())
}
}
})
//去除空格占位符
sentence.words = sentence.words.filter(v => v.word !== 'placeholder')
//如果是空的,直接去掉
if (!sentence.words.length) {
section.pop()
}
})
// console.log(sentenceNlpList)
})
sections = sections.filter(sectionItem => sectionItem.length)
sections.map((sectionItem, a) => {
sectionItem.map((sentenceItem, b) => {
sentenceItem.text = sentenceItem.words.reduce((previousValue: string, currentValue) => {
previousValue += currentValue.word + (currentValue.nextSpace ? ' ' : '')
return previousValue
}, '')
})
})
// console.log(sections)
//这里在每一行结尾处,加一个空格,因为. 号后面必要要有空格才能被库正常短句
text = sections.map(v => v.map(s => s.text.trim()).join(' \n')).join(' \n\n');
// console.log('s',text)
// return text
return text
}
export function splitCNArticle2(text: string): string {
if (!text) {
// text = "飞机误点了,侦探们在机场等了整整一上午。他们正期待从南非来的一个装着钻石的贵重包裹。数小时以前,有人向警方报告,说有人企图偷走这些钻石。当飞机到达时,一些侦探等候在主楼内,另一些侦探则守候在停机坪上。有两个人把包裹拿下飞机,进了海关。这时两个侦探把住门口,另外两个侦探打开了包裹。令他们吃惊的是,那珍贵的包裹里面装的全是石头和沙子!"
text = `那是个星期天,而在星期天我是从来不早起的,有时我要一直躺到吃午饭的时候。上个星期天,我起得很晚。我望望窗外,外面一片昏暗。“鬼天气!”我想,“又下雨了。”正在这时,电话铃响了。是我姑母露西打来的。“我刚下火车,”她说,“我这就来看你。”
“但我还在吃早饭,”我说。
“你在干什么?”她问道。
“我正在吃早饭,”我又说了一遍。
“天啊”她说“你总是起得这么晚吗现在已经1点钟了`
}
const segmenterJa = new Intl.Segmenter("zh-CN", {granularity: "sentence"});
let sectionTextList = text.replaceAll('\n\n', '`^`').replaceAll('\n', '').split('`^`')
let s = sectionTextList.filter(v => v).map((rowSection, i) => {
const segments = segmenterJa.segment(rowSection);
let ss = ''
Array.from(segments).map(sentenceRow => {
let row = sentenceRow.segment
if (row) {
//这个库总是会把反引号给断句到上一行末尾
//而 sentence-splitter 这个库总是会把反引号给断句到下一行开头
if (row[row.length - 1] === "“") {
row = row.substring(0, row.length - 1)
ss += (row + '\n') + '“'
} else {
ss += (row + '\n')
}
}
})
return ss
}).join('\n').trim()
return s
}
//todo 废弃
export function getSplitTranslateText(article: string) {
let sections = splitCNArticle(article)
let str = ''
@@ -344,4 +808,4 @@ export function usePlaySentenceAudio() {
return {
playSentenceAudio
}
}
}

View File

@@ -1,6 +1,6 @@
import {Article, Dict, Word} from "@/types.ts";
import {useBaseStore} from "@/stores/base.ts";
import {cloneDeep, shuffle} from "lodash-es";
import {cloneDeep,} from "lodash-es";
import {isArticle} from "@/hooks/article.ts";
@@ -159,4 +159,4 @@ export function getCurrentStudyWord() {
// console.timeEnd()
// console.log('data', data)
return data
}
}

View File

@@ -54,7 +54,6 @@ useEvent(EventKey.changeDict, () => {
</div>
</template>
<style scoped lang="scss">
@import "@/assets/css/variable";
$header-height: 50rem;
.panel {

View File

@@ -113,8 +113,7 @@ function $no() {
</template>
<style scoped lang="scss">
@import "../common";
@use '../common' as *;
.my {
font-size: 18rem;
@@ -210,4 +209,4 @@ function $no() {
}
}
}
</style>
</style>

View File

@@ -42,7 +42,7 @@ const gitLastCommitHash = ref(LATEST_COMMIT_HASH);
</template>
<style scoped lang="scss">
@import "../../common";
@use '../../common' as *;
.setting {
display: flex;
@@ -61,4 +61,4 @@ const gitLastCommitHash = ref(LATEST_COMMIT_HASH);
}
}
</style>
</style>

View File

@@ -235,7 +235,6 @@ watch(() => props.word, () => {
</template>
<style scoped lang="scss">
@import "@/assets/css/variable";
.typing-word {
width: 95%;
@@ -350,4 +349,4 @@ watch(() => props.word, () => {
}
}
}
</style>
</style>

View File

@@ -27,7 +27,7 @@ function clickEvent(e) {
<div class="absolute bottom-4 right-4">3</div>
</div>
</div>
<div class="grid flex-1 flex gap-5 mt-4">
<div class="grid flex-1 flex gap-5 mt-4" @click="router.push('edit-article')">
<div class="p-4 flex-1 rounded-md bg-slate-200 relative">
<span>添加</span>
<div class="absolute bottom-4 right-4">3</div>
@@ -82,4 +82,4 @@ function clickEvent(e) {
.title {
@apply text-lg font-medium;
}
</style>
</style>

View File

@@ -0,0 +1,15 @@
<script setup lang="ts">
import EditArticle2 from "@/pages/pc/components/article/EditArticle2.vue";
</script>
<template>
<div class="h-screen">
<EditArticle2 class="vue"></EditArticle2>
</div>
</template>
<style scoped lang="scss">
</style>

View File

@@ -155,7 +155,6 @@ onUnmounted(() => {
</template>
<style scoped lang="scss">
@import "@/assets/css/variable";
.footer {
width: var(--article-width);
@@ -220,4 +219,4 @@ onUnmounted(() => {
}
}
</style>
</style>

View File

@@ -116,7 +116,6 @@ function del(e) {
</template>
<style scoped lang="scss">
@import "@/assets/css/style";
.dict-list-panel {
width: 50%;
@@ -183,4 +182,4 @@ function del(e) {
}
}
</style>
</style>

View File

@@ -40,7 +40,7 @@ useDisableEventListener(() => focus)
</template>
<style scoped lang="scss">
@import "@/assets/css/style";
.base-input {
border: 1px solid var(--color-second-bg);
@@ -83,4 +83,4 @@ useDisableEventListener(() => focus)
}
}
}
</style>
</style>

View File

@@ -210,7 +210,7 @@ function changeCollect() {
</Transition>
</template>
<style scoped lang="scss">
@import "@/assets/css/variable";
$header-height: 3rem;
.slide-item {

View File

@@ -44,7 +44,7 @@ onMounted(() => {
})
</script>
<style scoped lang="scss">
@import "@/assets/css/variable";
$w: 6rem;
$w2: calc($w / 2);
@@ -75,4 +75,4 @@ $w2: calc($w / 2);
}
}
</style>
</style>

View File

@@ -463,7 +463,7 @@ function importData(e) {
</template>
<style scoped lang="scss">
@import "@/assets/css/style";
.setting {
width: 40vw;
@@ -650,4 +650,4 @@ function importData(e) {
text-align: center;
}
</style>
</style>

View File

@@ -200,7 +200,7 @@ defineExpose({del, showWord, hideWord, play})
</template>
<style scoped lang="scss">
@import "@/assets/css/variable";
.typing-word {
width: 100%;
@@ -273,4 +273,4 @@ defineExpose({del, showWord, hideWord, play})
}
}
}
</style>
</style>

View File

@@ -325,7 +325,7 @@ const status = $computed(() => {
</template>
<style scoped lang="scss">
@import "@/assets/css/variable";
.practice-word {
height: 100%;
@@ -393,4 +393,4 @@ const status = $computed(() => {
height: calc(100% - 1.5rem);
}
</style>
</style>

View File

@@ -561,7 +561,7 @@ function setStartTime(val: Sentence, i: number, j: number) {
</template>
<style scoped lang="scss">
@import "@/assets/css/style";
.content {
color: var(--color-article);
@@ -658,4 +658,4 @@ function setStartTime(val: Sentence, i: number, j: number) {
}
}
}
</style>
</style>

View File

@@ -0,0 +1,664 @@
<script setup lang="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";
import {
getNetworkTranslate,
getSentenceAllText,
getSentenceAllTranslateText,
renewSectionTexts,
renewSectionTranslates
} from "@/hooks/translate.ts";
import {genArticleSectionData, splitCNArticle2, splitEnArticle2, usePlaySentenceAudio} from "@/hooks/article.ts";
import {cloneDeep, last} from "lodash-es";
import {watch} from "vue";
import Empty from "@/components/Empty.vue";
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";
import Dialog from "@/pages/pc/components/dialog/Dialog.vue";
interface IProps {
article?: Article,
type?: 'single' | 'batch'
}
const props = withDefaults(defineProps<IProps>(), {
article: () => cloneDeep(DefaultArticle),
type: 'single'
})
const emit = defineEmits<{
save: [val: Article],
saveAndNext: [val: Article]
}>()
let networkTranslateEngine = $ref('baidu')
let progress = $ref(0)
let failCount = $ref(0)
let textareaRef = $ref<HTMLTextAreaElement>()
const TranslateEngineOptions = [
{value: 'baidu', label: '百度'},
{value: 'youdao', label: '有道'},
]
let editArticle = $ref<Article>(cloneDeep(DefaultArticle))
watch(() => props.article, val => {
editArticle = cloneDeep(val)
progress = 0
failCount = 0
// let r = getSplitTranslateText(editArticle.textTranslate)
// if (r) {
// editArticle.textTranslate = r
// ElMessage({
// message: '检测到本地翻译未格式化,已自动格式化',
// type: 'success',
// duration: 3000
// })
// }
renewSections()
console.log('ar', editArticle)
}, {immediate: true})
watch(() => editArticle.text, (s) => {
if (!s.trim()) {
editArticle.sections = []
}
})
function renewSections() {
if (editArticle.text.trim()) {
renewSectionTexts(editArticle)
failCount = renewSectionTranslates(editArticle, editArticle.textTranslate)
} else {
editArticle.sections = []
}
}
function apply() {
if (editArticle.text.trim()) {
editArticle.sections = genArticleSectionData(editArticle.text)
let count = 0
if (editArticle.lrcPosition.length) {
editArticle.sections.map((v, i) => {
v.map((w, j) => {
w.audioPosition = editArticle.lrcPosition[count]
count++
})
})
}
failCount = renewSectionTranslates(editArticle, editArticle.textTranslate)
} else {
editArticle.sections = []
}
}
//分句原文
function splitText() {
editArticle.text = splitEnArticle2(editArticle.text.trim())
return
let text = editArticle.text.trim();
if (text) {
editArticle.text = splitEnArticle2(text)
}
}
//分句翻译
function splitTranslateText() {
editArticle.textTranslate = splitCNArticle2(editArticle.textTranslate.trim())
return
let text = editArticle.textTranslate.trim();
if (text) {
editArticle.textTranslate = splitCNArticle2(text)
}
}
//TODO
async function startNetworkTranslate() {
if (!editArticle.title.trim()) {
return ElMessage.error('请填写标题!')
}
if (!editArticle.text.trim()) {
return ElMessage.error('请填写正文!')
}
renewSectionTexts(editArticle)
//注意!!!
//这里需要用异步因为watch了article.networkTranslate改变networkTranslate了之后会重新设置article.sections
//导致getNetworkTranslate里面拿到的article.sections是废弃的值
setTimeout(async () => {
await getNetworkTranslate(editArticle, TranslateEngine.Baidu, true, (v: number) => {
progress = v
})
failCount = 0
})
}
function saveSentenceTranslate(sentence: Sentence, val: string) {
sentence.translate = val
editArticle.textTranslate = getSentenceAllTranslateText(editArticle)
renewSections()
}
function saveSentenceText(sentence: Sentence, val: string) {
sentence.text = val
editArticle.text = getSentenceAllText(editArticle)
renewSections()
}
function save(option: 'save' | 'saveAndNext') {
// return console.log(cloneDeep(editArticle))
return new Promise((resolve: Function) => {
// console.log('article', article)
// copy(JSON.stringify(article))
editArticle.title = editArticle.title.trim()
editArticle.titleTranslate = editArticle.titleTranslate.trim()
editArticle.text = editArticle.text.trim()
editArticle.textTranslate = editArticle.textTranslate.trim()
if (!editArticle.title) {
ElMessage.error('请填写标题!')
return resolve(false)
}
if (!editArticle.text) {
ElMessage.error('请填写正文!')
return resolve(false)
}
const saveTemp = () => {
emit(option as any, editArticle)
return resolve(true)
}
saveTemp()
})
}
//不知道为什么直接用editArticle取到是空的默认值
defineExpose({save, getEditArticle: () => cloneDeep(editArticle)})
const handleChange: UploadProps['onChange'] = (uploadFile, uploadFiles) => {
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()
}
}
}
}
let currentSentence = $ref<Sentence>({} as any)
let editSentence = $ref<Sentence>({} as any)
let preSentence = $ref<Sentence>({} as any)
let showEditAudioDialog = $ref(false)
let sentenceAudioRef = $ref<HTMLAudioElement>()
let audioRef = $ref<HTMLAudioElement>()
function handleShowEditAudioDialog(val: Sentence, i: number, j: number) {
showEditAudioDialog = true
currentSentence = val
editSentence = cloneDeep(val)
preSentence = null
audioRef.pause()
if (j == 0) {
if (i != 0) {
preSentence = last(editArticle.sections[i - 1])
}
} else {
preSentence = editArticle.sections[i][j - 1]
}
if (!editSentence.audioPosition?.length) {
editSentence.audioPosition = [0, 0]
if (preSentence) {
editSentence.audioPosition = [preSentence.audioPosition[1] ?? 0, 0]
}
}
_nextTick(() => {
sentenceAudioRef.currentTime = editSentence.audioPosition[0]
})
}
function recordStart() {
if (sentenceAudioRef.paused) {
sentenceAudioRef.play()
}
editSentence.audioPosition[0] = Number(sentenceAudioRef.currentTime.toFixed(2))
}
function recordEnd() {
if (!sentenceAudioRef.paused) {
sentenceAudioRef.pause()
}
editSentence.audioPosition[1] = Number(sentenceAudioRef.currentTime.toFixed(2))
}
const {playSentenceAudio} = usePlaySentenceAudio()
function saveLrcPosition() {
// showEditAudioDialog = false
currentSentence.audioPosition = cloneDeep(editSentence.audioPosition)
editArticle.lrcPosition = editArticle.sections.map((v, i) => v.map((w, j) => (w.audioPosition ?? []))).flat()
}
function jumpAudio(time: number) {
sentenceAudioRef.currentTime = time
}
function setPreEndTimeToCurrentStartTime() {
if (preSentence) {
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>
<div class="content">
<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-end items-center flex">
<el-popover
class="box-item"
title="使用方法"
placement="top"
:width="400"
>
<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>分句规则一行一句段落间空一行</li>
<li>修改完成后点击 <span class="color-red font-bold">应用</span> 按钮同步到左侧结果栏
</li>
</ol>
<template #reference>
<Icon icon="ri:question-line" class="mr-3" width="20"/>
</template>
</el-popover>
<el-button type="primary" @click="splitText">分句</el-button>
<el-button type="primary" @click="apply">应用</el-button>
</div>
</div>
<div class="row flex flex-col gap-2">
<div class="title">译文</div>
<div class="flex gap-2">
标题
</div>
<input
v-model="editArticle.titleTranslate"
type="text"
class="base-input"
placeholder="请填写翻译标题"
/>
<div class="flex">
<span>正文</span>
</div>
<textarea
v-model="editArticle.textTranslate"
:readonly="![100,0].includes(progress)"
type="textarea"
class="base-textarea"
placeholder="请填写翻译"
ref="textareaRef"
>
</textarea>
<div class="justify-between items-center flex">
<div class="flex gap-2 items-center ">
<el-button
type="primary"
@click="startNetworkTranslate"
:loading="progress!==0 && progress !== 100"
>翻译
</el-button>
<el-select v-model="networkTranslateEngine"
class="w-20"
>
<el-option
v-for="item in TranslateEngineOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
{{ progress }}%
</div>
<div class="flex items-center">
<el-popover
class="box-item"
title="使用方法"
placement="top"
:width="400"
>
<ol class="py-0 pl-5 my-0 text-base color-black/60">
<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>
<li>分句规则一行一句段落间空一行</li>
<li>修改完成后点击 <span class="color-red font-bold">应用</span> 按钮同步到左侧结果栏
</li>
</ol>
<template #reference>
<Icon icon="ri:question-line" class="mr-3" width="20"/>
</template>
</el-popover>
<el-button type="primary" @click="splitTranslateText">分句</el-button>
<el-button type="primary" @click="apply">应用</el-button>
</div>
</div>
</div>
<div class="row flex flex-col gap-2">
<div class="title">结果</div>
<div class="center">正文译文与结果均可编辑修改一处另外两处会自动同步变动</div>
<div class="flex gap-2">
<BaseButton>添加音频</BaseButton>
<el-upload
class="upload-demo"
:limit="1"
:on-change="handleChange"
:auto-upload="false"
>
<el-button type="primary">添加音频LRC文件</el-button>
</el-upload>
<audio ref="audioRef" :src="editArticle.audioSrc" controls></audio>
</div>
<template v-if="editArticle.sections.length">
<div class="flex-1 overflow-auto flex flex-col">
<div class="flex justify-between bg-black/10 py-2">
<div class="center flex-[7]">内容</div>
<div>|</div>
<div class="center flex-[3]">音频</div>
</div>
<div class="article-translate">
<div class="section " v-for="(item,indexI) in editArticle.sections">
<div class="section-title">{{ indexI + 1 }}</div>
<div class="sentence" v-for="(sentence,indexJ) in item">
<div class="flex-[7]">
<EditAbleText
:value="sentence.text"
@save="(e:string) => saveSentenceText(sentence,e)"
/>
<EditAbleText
class="text-lg!"
v-if="sentence.translate"
:value="sentence.translate"
@save="(e:string) => saveSentenceTranslate(sentence,e)"
/>
</div>
<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>-</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>
</div>
</div>
<div class="options" v-if="editArticle.text.trim()">
<div class="status">
<span>状态</span>
<div class="warning" v-if="failCount">
<Icon icon="typcn:warning-outline"/>
共有{{ failCount }}句没有翻译
</div>
<div class="success" v-else>
<Icon icon="mdi:success-circle-outline"/>
翻译完成
</div>
</div>
<div class="left">
<BaseButton @click="save('save')">保存</BaseButton>
<BaseButton v-if="type === 'batch'" @click="save('saveAndNext')">保存并添加下一篇</BaseButton>
</div>
</div>
</template>
<Empty v-else text="没有译文对照~"/>
</div>
<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 class="">
教程点击音频播放按钮当播放到句子开始时点击开始时间的 <span class="color-red">记录</span>
按钮当播放到句子结束时点击结束时间的 <span class="color-red">记录</span> 按钮最后再试听是否正确
</div>
<audio ref="sentenceAudioRef" :src="editArticle.audioSrc" controls class="w-full"></audio>
<div class="flex items-center gap-2 space-between mb-2" v-if="editSentence.audioPosition?.length">
<div>{{ editSentence.text }}</div>
<div class="flex items-center gap-2 shrink-0">
<div>
<span>{{ editSentence.audioPosition?.[0] }}s</span>
<span v-if="editSentence.audioPosition?.[1] !== -1"> - {{ editSentence.audioPosition?.[1] }}s</span>
<span v-else> - 结束</span>
</div>
<BaseIcon icon="hugeicons:play"
title="试听"
@click="playSentenceAudio(editSentence,sentenceAudioRef,editArticle)"/>
</div>
</div>
<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-2">
<el-input-number v-model="editSentence.audioPosition[0]" :precision="2" :step="0.1">
<template #suffix>
<span>s</span>
</template>
</el-input-number>
<BaseIcon
@click="jumpAudio(editSentence.audioPosition[0])"
title="跳转"
icon="ic:sharp-my-location"
/>
<BaseIcon
@click="setPreEndTimeToCurrentStartTime"
title="使用前一句的结束时间"
icon="twemoji:end-arrow"
/>
</div>
<BaseButton @click="recordStart">记录</BaseButton>
</div>
</div>
<div class="flex gap-2 items-center">
<div>结束时间</div>
<div class="flex space-between flex-1">
<div class="flex items-center gap-2">
<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>
</Dialog>
</div>
</template>
<style scoped lang="scss">
.content {
color: var(--color-article);
height: 100%;
box-sizing: border-box;
display: flex;
gap: var(--space);
padding: var(--space);
padding-top: .6rem;
}
.row {
flex: 7;
width: 33%;
//height: 100%;
display: flex;
flex-direction: column;
//opacity: 0;
&:nth-child(3) {
flex: 10;
}
.title {
font-weight: bold;
font-size: 1.4rem;
text-align: center;
}
.article-translate {
flex: 1;
overflow-y: overlay;
.section {
background: var(--color-textarea-bg);
margin-bottom: 1.2rem;
.section-title {
padding: 0.5rem;
border-bottom: 1px solid var(--color-item-border);
}
&:last-child {
margin-bottom: 0;
}
.sentence {
display: flex;
padding: 0.5rem 1.5rem;
line-height: 1.2;
border-bottom: 1px solid var(--color-item-border);
&:last-child {
border-bottom: none;
}
}
}
}
.options {
display: flex;
align-items: center;
justify-content: space-between;
.status {
display: flex;
align-items: center;
}
.warning {
display: flex;
align-items: center;
font-size: 1.2rem;
color: red;
}
.success {
display: flex;
align-items: center;
font-size: 1.2rem;
color: #67C23A;
}
.left {
gap: var(--space);
display: flex;
}
}
}
</style>

View File

@@ -204,7 +204,7 @@ useWindowClick(() => showExport = false)
</template>
<style scoped lang="scss">
@import "@/assets/css/style";
.add-article {
//position: fixed;
@@ -289,4 +289,4 @@ useWindowClick(() => showExport = false)
}
}
}
</style>
</style>

View File

@@ -41,7 +41,6 @@ useDisableEventListener(() => props.modelValue)
</template>
<style scoped lang="scss">
@import "@/assets/css/style";
.wrapper {
width: 100%;
@@ -49,4 +48,4 @@ useDisableEventListener(() => props.modelValue)
display: flex;
background: var(--color-main-bg);
}
</style>
</style>

View File

@@ -186,7 +186,7 @@ async function cancel() {
</template>
<style scoped lang="scss">
@import "@/assets/css/variable";
$modal-mask-bg: rgba(#000, .45);
$radius: .5rem;
@@ -365,4 +365,4 @@ $header-height: 4rem;
}
}
}
</style>
</style>

View File

@@ -35,7 +35,7 @@ watch(() => props.modelValue, (n) => {
</template>
<style lang="scss">
@import "@/assets/css/style";
.mini-row-title {
min-height: 2rem;
@@ -68,4 +68,4 @@ watch(() => props.modelValue, (n) => {
transform: translate3d(-50%, 0, 0);
//margin-top: 10rem;
}
</style>
</style>

View File

@@ -17,7 +17,7 @@ let disabledDialogEscKey = $ref(true)
</template>
<style scoped lang="scss">
@import "@/assets/css/style";
.setting-modal {
width: 40vw;
@@ -151,4 +151,4 @@ let disabledDialogEscKey = $ref(true)
}
}
</style>
</style>

View File

@@ -48,7 +48,7 @@ onUnmounted(() => {
</template>
<style lang="scss" scoped>
@import "@/assets/css/style";
.all-word {
padding-bottom: var(--space);

View File

@@ -169,7 +169,7 @@ defineExpose({scrollToBottom, scrollToItem})
</template>
<style lang="scss" scoped>
@import "@/assets/css/style";
.scroller {
flex: 1;

View File

@@ -26,7 +26,7 @@ function toggle() {
</template>
<style scoped lang="scss">
@import "@/assets/css/style";
.wrapper {
position: relative;
@@ -36,4 +36,4 @@ function toggle() {
position: relative;
}
</style>
</style>

View File

@@ -71,7 +71,7 @@ onMounted(() => {
</template>
<style scoped lang="scss">
@import "@/assets/css/style";
.setting {
position: relative;
@@ -86,4 +86,4 @@ onMounted(() => {
align-items: flex-start;
}
}
</style>
</style>

View File

@@ -107,7 +107,7 @@ function save() {
</template>
<style scoped lang="scss">
@import "@/assets/css/style";
.setting {
position: relative;
@@ -119,4 +119,4 @@ function save() {
justify-content: flex-end;
gap: .6rem;
}
</style>
</style>

View File

@@ -152,7 +152,7 @@ function toggle2() {
</template>
<style scoped lang="scss">
@import "@/assets/css/style";
.wrapper {
width: 100rem;
@@ -174,4 +174,4 @@ function toggle2() {
transform: translateX(10rem);
}
}
</style>
</style>

View File

@@ -112,7 +112,7 @@ const {nav} = useNav()
}
</style>
<style scoped lang="scss">
@import "@/assets/css/style";
header {
width: var(--toolbar-width);
@@ -165,4 +165,4 @@ header {
}
}
}
</style>
</style>

View File

@@ -117,7 +117,7 @@ function formatLangType(val) {
</template>
<style scoped lang="scss">
@import "@/assets/css/style";
.dict-list-panel {
width: 100%;
@@ -186,4 +186,4 @@ function formatLangType(val) {
}
}
</style>
</style>

View File

@@ -11,8 +11,6 @@ import jq from 'jquery'
import {_nextTick} from "@/utils";
import '@imengyu/vue3-context-menu/lib/vue3-context-menu.css'
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";
import QuestionForm from "@/pages/pc/components/QuestionForm.vue";
@@ -69,7 +67,6 @@ const currentIndex = computed(() => {
return `${sectionIndex}${sentenceIndex}${wordIndex}`
})
const $toast = useToast();
const playBeep = usePlayBeep()
const playCorrect = usePlayCorrect()
const playKeyboardAudio = usePlayKeyboardAudio()
@@ -322,7 +319,10 @@ function onContextMenu(e: MouseEvent, sentence: Sentence, i, j) {
label: "复制",
onClick: () => {
navigator.clipboard.writeText(sentence.text).then(r => {
$toast.success('已复制!', {position: 'top'});
ElMessage({
message: '已复制',
type: 'success',
})
})
}
},
@@ -330,10 +330,11 @@ function onContextMenu(e: MouseEvent, sentence: Sentence, i, j) {
label: "语法分析",
onClick: () => {
navigator.clipboard.writeText(sentence.text).then(r => {
$toast.success('已复制!随后将打开语法分析网站!', {
position: 'top',
duration: 3000,
});
ElMessage({
message: '已复制!随后将打开语法分析网站!',
type: 'success',
duration: 3000
})
setTimeout(() => {
window.open('https://enpuz.com/')
}, 1000)
@@ -483,7 +484,6 @@ let showQuestions = $ref(false)
</template>
<style scoped lang="scss">
@import "@/assets/css/style";
.wrote {
color: grey;
@@ -692,4 +692,4 @@ let showQuestions = $ref(false)
}
}
</style>
</style>

View File

@@ -471,7 +471,7 @@ const {playSentenceAudio} = usePlaySentenceAudio()
</template>
<style scoped lang="scss">
@import "@/assets/css/style";
.practice-wrapper {
font-size: 0.9rem;
@@ -591,4 +591,4 @@ const {playSentenceAudio} = usePlaySentenceAudio()
}
</style>
</style>

View File

@@ -75,7 +75,7 @@ onUnmounted(() => {
</template>
<style scoped lang="scss">
@import "@/assets/css/variable";
.footer {
width: var(--toolbar-width);
@@ -140,4 +140,4 @@ onUnmounted(() => {
}
}
</style>
</style>

View File

@@ -125,7 +125,7 @@ const isEnd = $computed(() => {
<Fireworks v-if="open"/>
</template>
<style scoped lang="scss">
@import "@/assets/css/variable";
$card-radius: .5rem;
$dark-second-bg: rgb(60, 63, 65);
@@ -169,4 +169,4 @@ $item-hover: rgb(75, 75, 75);
}
}
</style>
</style>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import origin from './data.json'
// import origin from './data.json'
import BaseButton from "@/components/BaseButton.vue";
import {checkAndUpgradeSaveDict, shakeCommonDict} from "@/utils";
import localforage from "localforage";
@@ -36,7 +36,7 @@ async function look() {
}
function set() {
localforage.setItem(SAVE_DICT_KEY.key, JSON.stringify({val: shakeCommonDict(origin.val as any), version: 3}))
// localforage.setItem(SAVE_DICT_KEY.key, JSON.stringify({val: shakeCommonDict(origin.val as any), version: 3}))
}
async function check() {
@@ -113,4 +113,4 @@ const data1 = generateData(columns, 1000)
width: 30rem;
}
}
</style>
</style>

View File

@@ -24,11 +24,12 @@ import HomeIndex from "@/pages/pc/home/HomeIndex.vue";
import LearnArticle from "@/pages/pc/article/LearnArticle.vue";
import EditWordDict from "@/pages/pc/word/EditWordDict.vue";
import StudyWord from "@/pages/pc/word/StudyWord.vue";
import EditArticlePage from "@/pages/pc/article/EditArticlePage.vue";
export const routes: RouteRecordRaw[] = [
{
path: '/', component: PC,
redirect: '/word',
redirect: '/edit-article',
children: [
{path: 'home', component: HomeIndex},
{path: 'word', component: WordHome},
@@ -37,6 +38,7 @@ export const routes: RouteRecordRaw[] = [
{path: 'dict', component: Dict2},
{path: 'article', component: ArticleIndex},
{path: 'article2', component: Article2Index},
{path: 'edit-article', component: EditArticlePage},
{path: 'learn-article', component: LearnArticle},
]
},
@@ -115,4 +117,4 @@ router.beforeEach((to: any, from: any) => {
})
export default router
export default router

View File

@@ -9,6 +9,7 @@ import Components from 'unplugin-vue-components/vite'
import {getLastCommit} from "git-last-commit";
import UnoCSS from 'unocss/vite'
import VueMacros from 'unplugin-vue-macros/vite'
import { Plugin as importToCDN } from 'vite-plugin-cdn-import'
function pathResolve(dir: string) {
return resolve(__dirname, ".", dir)
@@ -45,6 +46,30 @@ export default defineConfig(async () => {
filename: "report.html", //分析图生成的文件名
open: true //如果存在本地服务端口,将在打包后自动展示
}) : null,
importToCDN({
modules: [
{
name: 'vue',
var: 'Vue',
path: `https://cdn.jsdelivr.net/npm/vue@3.5.14/dist/vue.global.prod.min.js`
},
{
name: 'vue-router',
var: 'VueRouter',
path: `https://cdn.jsdelivr.net/npm/vue-router@4.5.1/dist/vue-router.global.prod.min.js`
},
{
name: 'jquery',
var: 'jQuery',
path: 'https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js'
},
// {
// name: 'axios',
// var: 'axios',
// path: 'https://cdn.jsdelivr.net/npm/axios@1.9.0/dist/axios.min.js'
// },
]
})
],
define: {
LATEST_COMMIT_HASH: JSON.stringify(latestCommitHash + (process.env.NODE_ENV === 'production' ? '' : ' (dev)')),
@@ -57,6 +82,14 @@ export default defineConfig(async () => {
},
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue']
},
css: {
preprocessorOptions: {
scss: {
//解决 sass 控制台出现 Deprecation Warning: The legacy JS API is deprecated and will be removed in Dart Sass 2.0.0 的问题
api: "modern-compiler" // or 'modern'
}
}
},
server: {
port: 3000,
open: false,