This commit is contained in:
zyronon
2023-09-25 00:28:25 +08:00
parent 9bcba81b43
commit 6562d5abda
4 changed files with 570 additions and 85 deletions

View File

@@ -6,4 +6,6 @@ bug
打完了没检测到
所有的图标hover时有放大效果
各种声音可以单独调节音量大小
各种声音可以单独调节音量大小
列表加搜索

1
components.d.ts vendored
View File

@@ -9,6 +9,7 @@ declare module 'vue' {
export interface GlobalComponents {
Add: typeof import('./src/components/Toolbar/Add.vue')['default']
AddArticle: typeof import('./src/components/Practice/AddArticle.vue')['default']
AddArticle2: typeof import('./src/components/Add/AddArticle2.vue')['default']
Backgorund: typeof import('./src/components/Backgorund.vue')['default']
BaseButton: typeof import('./src/components/BaseButton.vue')['default']
BaseIcon: typeof import('./src/components/BaseIcon.vue')['default']

View File

@@ -0,0 +1,554 @@
<script setup lang="ts">
import {onMounted, watch} from "vue";
import {Article, DefaultArticle, Sentence, TranslateEngine, TranslateType} from "@/types.ts";
import BaseButton from "@/components/BaseButton.vue";
import {
getNetworkTranslate,
getSentenceAllText,
getSentenceAllTranslateText,
updateLocalSentenceTranslate,
updateSections
} from "@/hooks/translate.ts";
import * as copy from "copy-to-clipboard";
import {getSplitTranslateText} from "@/hooks/article.ts";
import EditAbleText from "@/components/EditAbleText.vue";
import {Icon} from "@iconify/vue";
import {cloneDeep} from "lodash-es";
import {useDisableEventListener} from "@/hooks/event.ts";
import {MessageBox} from "@/utils/MessageBox.tsx";
import BaseIcon from "@/components/BaseIcon.vue";
import {useBaseStore} from "@/stores/base.ts";
interface IProps {
article?: Article
}
const props = withDefaults(defineProps<IProps>(), {
article: () => cloneDeep(DefaultArticle),
})
const base = useBaseStore()
let article = $ref<Article>(cloneDeep(props.article))
let networkTranslateEngine = $ref('baidu')
let progress = $ref(0)
const TranslateEngineOptions = [
{value: 'baidu', label: '百度'},
{value: 'youdao', label: '有道'},
]
const emit = defineEmits([
'update:modelValue',
'close',
'save'
])
// useDisableEventListener(() => props.modelValue)
watch(() => props.article, n => {
if (n) {
article = cloneDeep(props.article)
if (article.text.trim()) {
if (article.useTranslateType === TranslateType.custom) {
if (article.textCustomTranslate.trim()) {
if (!article.textCustomTranslateIsFormat) {
let r = getSplitTranslateText(article.textCustomTranslate)
if (r) article.textCustomTranslate = r
ElMessage({
message: '检测到本地翻译未格式化,已自动格式',
type: 'success',
duration: 5000
})
}
}
}
}
updateSentenceTranslate()
}
})
onMounted(() => {
})
async function startNetworkTranslate() {
if (!article.title.trim()) {
return ElMessage.error('请填写标题!')
}
if (!article.text.trim()) {
return ElMessage.error('请填写正文!')
}
updateSections(article)
article.textNetworkTranslate = ''
//注意!!!
//这里需要用异步因为watch了article.networkTranslate改变networkTranslate了之后会重新设置article.sections
//导致getNetworkTranslate里面拿到的article.sections是废弃的值
setTimeout(async () => {
await getNetworkTranslate(article, TranslateEngine.Baidu, true, (v: number) => {
progress = v
})
copy(JSON.stringify(article.sections))
})
}
function saveSentenceTranslate(sentence: Sentence, val: string) {
sentence.translate = val
if (article.useTranslateType === TranslateType.custom) {
article.textCustomTranslate = getSentenceAllTranslateText(article)
}
if (article.useTranslateType === TranslateType.network) {
article.textNetworkTranslate = getSentenceAllTranslateText(article)
}
}
function saveSentenceText(sentence: Sentence, val: string) {
sentence.text = val
article.text = getSentenceAllText(article)
updateSentenceTranslate()
}
function updateSentenceTranslate() {
if (article.text.trim()) {
updateSections(article)
if (article.useTranslateType === TranslateType.custom) {
updateLocalSentenceTranslate(article, article.textCustomTranslate)
}
if (article.useTranslateType === TranslateType.network) {
updateLocalSentenceTranslate(article, article.textNetworkTranslate)
}
} else {
article.sections = []
}
}
function appendTranslate(str: string) {
if (article.useTranslateType === TranslateType.custom) {
article.textCustomTranslate += str
}
if (article.useTranslateType === TranslateType.network) {
article.textNetworkTranslate += str
}
}
function onPaste(event: ClipboardEvent) {
event.preventDefault()
// @ts-ignore
let paste = (event.clipboardData || window.clipboardData).getData("text");
return MessageBox.confirm(
'是否需要进行自动分句',
'提示',
() => {
let r = getSplitTranslateText(paste)
if (r) appendTranslate(r)
},
() => {
appendTranslate(paste)
},
{
confirmButtonText: '需要',
cancelButtonText: '关闭',
}
)
}
function onBlur() {
document.removeEventListener('paste', onPaste);
}
function onFocus() {
document.addEventListener('paste', onPaste);
}
function save() {
console.log('article', article)
copy(JSON.stringify(article))
if (!article.title.trim()) {
return ElMessage.error('请填写标题!')
}
if (!article.text.trim()) {
return ElMessage.error('请填写正文!')
}
const saveTemp = () => {
article.textCustomTranslateIsFormat = true
emit('close')
emit('save', cloneDeep(article))
}
if (article.useTranslateType === TranslateType.network) {
if (!article.textNetworkTranslate.trim()) {
return MessageBox.confirm(
'您选择了“网络翻译”,但译文内容却为空白,是否修改为“不需要翻译”并保存?',
'提示',
() => {
article.useTranslateType = TranslateType.none
saveTemp()
},
() => void 0,
)
}
}
if (article.useTranslateType === TranslateType.custom) {
if (!article.textCustomTranslate.trim()) {
return MessageBox.confirm(
'您选择了“本地翻译”,但译文内容却为空白,是否修改为“不需要翻译”并保存?',
'提示',
() => {
article.useTranslateType = TranslateType.none
saveTemp()
},
() => void 0,
)
}
}
saveTemp()
}
watch(() => article.textCustomTranslate, (str: string) => {
updateSentenceTranslate()
})
watch(() => article.textNetworkTranslate, (str: string) => {
updateSentenceTranslate()
})
watch(() => article.useTranslateType, () => {
if (article.text.trim()) {
if (article.useTranslateType === TranslateType.custom) {
if (article.textCustomTranslate.trim()) {
updateLocalSentenceTranslate(article, article.textCustomTranslate)
} else {
updateSections(article)
}
}
if (article.useTranslateType === TranslateType.network) {
if (article.textNetworkTranslate.trim()) {
updateLocalSentenceTranslate(article, article.textNetworkTranslate)
} else {
updateSections(article)
}
}
if (article.useTranslateType === TranslateType.none) {
updateSections(article)
}
}
})
</script>
<template>
<div class="add-article">
<div class="slide">
<header>
<div class="dict-name">{{ base.dictTitle }}</div>
<BaseIcon title="选择其他词典/文章" icon="carbon:change-catalog"/>
</header>
<div class="article-list">
<div class="item" v-for="(item,index) in base.currentDict.articles">
<div class="left">
<div class="name"> {{ `${index + 1}. ${item.title}` }}</div>
<div class="translate-name"> {{ ` ${item.titleTranslate}` }}</div>
</div>
<div class="right">
<BaseIcon title="删除" icon="fluent:delete-24-regular"/>
</div>
</div>
</div>
<div class="footer">
<BaseButton>导入</BaseButton>
<BaseButton>导出</BaseButton>
</div>
</div>
<div class="content">
<div class="row">
<div class="title">原文</div>
<div class="item">
<div class="label">标题</div>
<textarea
v-model="article.title"
type="textarea"
class="base-textarea"
placeholder="请填写原文标题"
>
</textarea>
</div>
<div class="item basic">
<div class="label">正文</div>
<textarea
v-model="article.text"
@input="updateSentenceTranslate"
:readonly="![100,0].includes(progress)"
type="textarea"
class="base-textarea"
placeholder="请填写原文正文"
>
</textarea>
</div>
</div>
<div class="row">
<div class="title">译文</div>
<div class="item">
<div class="label">
<span>标题</span>
<el-radio-group v-model="article.useTranslateType">
<el-radio-button :label="TranslateType.custom">本地翻译</el-radio-button>
<el-radio-button :label="TranslateType.network">网络翻译</el-radio-button>
<el-radio-button :label="TranslateType.none">不需要翻译</el-radio-button>
</el-radio-group>
</div>
<textarea
v-model="article.titleTranslate"
type="textarea"
class="base-textarea"
placeholder="请填写翻译标题"
>
</textarea>
</div>
<div class="item basic">
<div class="label">
<span>正文</span>
<div class="translate-item" v-if="article.useTranslateType === TranslateType.network">
<el-progress :percentage="progress"
:duration="30"
:striped="progress !== 100"
:striped-flow="progress !== 100"
:stroke-width="8"
:show-text="true"/>
<el-select v-model="networkTranslateEngine"
style="width: 80rem;"
>
<el-option
v-for="item in TranslateEngineOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<BaseButton
size="small"
@click="startNetworkTranslate"
:loading="progress!==0 && progress !== 100"
>开始翻译
</BaseButton>
</div>
</div>
<textarea
v-if="article.useTranslateType === TranslateType.custom"
v-model="article.textCustomTranslate"
:readonly="![100,0].includes(progress)"
@blur="onBlur"
@focus="onFocus"
type="textarea"
class="base-textarea"
placeholder="请填写翻译正文"
>
</textarea>
<textarea
v-if="article.useTranslateType === TranslateType.network"
v-model="article.textNetworkTranslate"
@blur="onBlur"
@focus="onFocus"
type="textarea"
class="base-textarea"
placeholder="等待网络翻译中..."
>
</textarea>
</div>
</div>
<div class="row">
<div class="title">译文对照</div>
<div class="article-translate">
<div class="section" v-for="(item,indexI) in article.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>
</div>
</div>
<div class="options">
<BaseButton @click="save">保存</BaseButton>
</div>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
@import "@/assets/css/style.scss";
.add-article {
//position: fixed;
left: 0;
top: 0;
z-index: 9;
width: 100%;
height: 100%;
box-sizing: border-box;
color: black;
background: var(--color-main-bg);
display: flex;
gap: $space;
.slide {
height: 100%;
background: white;
padding: 0 10rem;
display: flex;
flex-direction: column;
$height: 60rem;
header {
height: $height;
display: flex;
justify-content: space-between;
align-items: center;
.dict-name {
font-size: 30rem;
color: black;
}
}
.article-list {
flex: 1;
width: 300rem;
overflow: auto;
.item {
background: #e1e1e1;
border-radius: 8rem;
margin-bottom: 10rem;
padding: 10rem;
display: flex;
justify-content: space-between;
.right {
transition: all .3s;
opacity: 0;
}
&:hover {
.right {
opacity: 1;
}
}
.name {
font-size: 18rem;
}
.translate-name {
font-size: 16rem;
}
}
}
.footer {
height: $height;
display: flex;
gap: $space;
align-items: center;
justify-content: flex-end;
}
}
.content {
flex: 1;
display: flex;
gap: $space;
}
.row {
flex: 1;
width: 33%;
//height: 100%;
display: flex;
flex-direction: column;
//opacity: 0;
.basic {
flex: 1;
display: flex;
flex-direction: column;
}
&:nth-child(2) {
opacity: 1;
}
.title {
font-size: 22rem;
text-align: center;
}
.item {
width: 100%;
//margin-bottom: 10rem;
.label {
height: 45rem;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 16rem;
}
}
.translate-item {
flex: 1;
display: flex;
align-items: center;
justify-content: flex-end;
gap: calc($space / 2);
}
.el-progress {
flex: 1;
}
.article-translate {
margin-top: 10rem;
margin-bottom: 20rem;
flex: 1;
overflow: auto;
border-radius: 8rem;
.section {
background: white;
margin-bottom: 20rem;
padding: $space;
border-radius: 8rem;
&:last-child {
margin-bottom: 0;
}
.sentence {
margin-bottom: 20rem;
.text {
font-size: 18rem;
}
}
}
}
.options {
display: flex;
justify-content: flex-end;
}
}
}
</style>

View File

@@ -1,40 +1,24 @@
<script setup lang="ts">
import {useBaseStore} from "@/stores/base.ts";
import IconWrapper from "@/components/IconWrapper.vue";
import {Icon} from "@iconify/vue";
import Tooltip from "@/components/Tooltip.vue";
import BaseButton from "@/components/BaseButton.vue";
import BaseIcon from "@/components/BaseIcon.vue";
import AddArticle2 from "@/components/Add/AddArticle2.vue";
import {$ref} from "vue/macros";
import {cloneDeep} from "lodash-es";
import {DefaultArticle} from "@/types.ts";
const base = useBaseStore()
let articleData = $ref({
article: cloneDeep(DefaultArticle),
sectionIndex: 0,
sentenceIndex: 0,
wordIndex: 0,
stringIndex: 0,
})
</script>
<template>
<div id="BatchAddArticle">
<div class="slide">
<header>
<div class="dict-name">{{ base.dictTitle }}</div>
<BaseIcon title="选择其他词典/文章" icon="carbon:change-catalog"/>
</header>
<div class="article-list">
<div class="item" v-for="(item,index) in base.currentDict.articles">
<div class="left">
<div class="name"> {{ `${index + 1}. ${item.title}` }}</div>
<div class="translate-name"> {{ ` ${item.titleTranslate}` }}</div>
</div>
<div class="right">
<BaseIcon title="删除" icon="fluent:delete-24-regular"/>
</div>
</div>
</div>
<div class="footer">
<BaseButton>导入</BaseButton>
<BaseButton>导出</BaseButton>
</div>
</div>
<div class="content">
</div>
<AddArticle2/>
</div>
</template>
@@ -47,61 +31,5 @@ const base = useBaseStore()
display: flex;
color: black;
.slide {
height: 100%;
background: white;
padding: 0 10rem;
display: flex;
flex-direction: column;
$height: 60rem;
header {
height: $height;
display: flex;
justify-content: space-between;
align-items: center;
.dict-name {
font-size: 30rem;
color: black;
}
}
.article-list {
flex: 1;
width: 300rem;
overflow: auto;
.item {
background: #e1e1e1;
border-radius: 8rem;
margin-bottom: 10rem;
padding: 10rem;
display: flex;
justify-content: space-between;
.name {
font-size: 18rem;
}
.translate-name {
font-size: 16rem;
}
}
}
.footer {
height: $height;
display: flex;
gap: $space;
align-items: center;
justify-content: flex-end;
}
}
.content {
flex: 1;
}
}
</style>