save
This commit is contained in:
@@ -6,4 +6,6 @@ bug
|
||||
打完了没检测到
|
||||
|
||||
所有的图标hover时,有放大效果
|
||||
各种声音可以单独调节音量大小
|
||||
各种声音可以单独调节音量大小
|
||||
|
||||
列表加搜索
|
||||
1
components.d.ts
vendored
1
components.d.ts
vendored
@@ -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']
|
||||
|
||||
554
src/components/Add/AddArticle2.vue
Normal file
554
src/components/Add/AddArticle2.vue
Normal 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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user