feat: 调整文章编辑器
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
"i18n:write": "gulp i18nwrite"
|
||||
},
|
||||
"dependencies": {
|
||||
"@imengyu/vue3-context-menu": "^1.4.5",
|
||||
"@opentranslate/baidu": "^1.4.2",
|
||||
"@opentranslate/translator": "^1.4.2",
|
||||
"axios": "^1.7.2",
|
||||
@@ -39,6 +40,7 @@
|
||||
"vue-activity-calendar": "^1.2.2",
|
||||
"vue-i18n": "9",
|
||||
"vue-router": "4",
|
||||
"vue-toast-notification": "3",
|
||||
"vue-virtual-scroller": "2.0.0-beta.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
21
pnpm-lock.yaml
generated
21
pnpm-lock.yaml
generated
@@ -8,6 +8,9 @@ importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@imengyu/vue3-context-menu':
|
||||
specifier: ^1.4.5
|
||||
version: 1.4.5
|
||||
'@opentranslate/baidu':
|
||||
specifier: ^1.4.2
|
||||
version: 1.4.2
|
||||
@@ -80,6 +83,9 @@ importers:
|
||||
vue-router:
|
||||
specifier: '4'
|
||||
version: 4.3.2(vue@3.4.27(typescript@5.4.5))
|
||||
vue-toast-notification:
|
||||
specifier: '3'
|
||||
version: 3.1.3(vue@3.4.27(typescript@5.4.5))
|
||||
vue-virtual-scroller:
|
||||
specifier: 2.0.0-beta.8
|
||||
version: 2.0.0-beta.8(vue@3.4.27(typescript@5.4.5))
|
||||
@@ -506,6 +512,9 @@ packages:
|
||||
peerDependencies:
|
||||
vue: '>=3'
|
||||
|
||||
'@imengyu/vue3-context-menu@1.4.5':
|
||||
resolution: {integrity: sha512-VVsfuCYWWchVAIRQHXOUttSemEunUqZIZ4y/Y8aIWSFb/Z9w1E+RjO0dw3H8Jm+yaCbLW4/Mn9pRy3rnry71Hw==}
|
||||
|
||||
'@intlify/core-base@9.13.1':
|
||||
resolution: {integrity: sha512-+bcQRkJO9pcX8d0gel9ZNfrzU22sZFSA0WVhfXrf5jdJOS24a+Bp8pozuS9sBI9Hk/tGz83pgKfmqcn/Ci7/8w==}
|
||||
engines: {node: '>= 16'}
|
||||
@@ -3367,6 +3376,12 @@ packages:
|
||||
vue-template-compiler@2.7.16:
|
||||
resolution: {integrity: sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==}
|
||||
|
||||
vue-toast-notification@3.1.3:
|
||||
resolution: {integrity: sha512-XNyWqwLIGBFfX5G9sK+clq3N3IPlhDjzNdbZaXkEElcotPlWs0wWZailk1vqhdtLYT/93Y4FHAVuzyatLmPZRA==}
|
||||
engines: {node: '>=12.15.0'}
|
||||
peerDependencies:
|
||||
vue: ^3.0
|
||||
|
||||
vue-tsc@2.0.19:
|
||||
resolution: {integrity: sha512-JWay5Zt2/871iodGF72cELIbcAoPyhJxq56mPPh+M2K7IwI688FMrFKc/+DvB05wDWEuCPexQJ6L10zSwzzapg==}
|
||||
hasBin: true
|
||||
@@ -3840,6 +3855,8 @@ snapshots:
|
||||
'@iconify/types': 2.0.0
|
||||
vue: 3.4.27(typescript@5.4.5)
|
||||
|
||||
'@imengyu/vue3-context-menu@1.4.5': {}
|
||||
|
||||
'@intlify/core-base@9.13.1':
|
||||
dependencies:
|
||||
'@intlify/message-compiler': 9.13.1
|
||||
@@ -7114,6 +7131,10 @@ snapshots:
|
||||
de-indent: 1.0.2
|
||||
he: 1.2.0
|
||||
|
||||
vue-toast-notification@3.1.3(vue@3.4.27(typescript@5.4.5)):
|
||||
dependencies:
|
||||
vue: 3.4.27(typescript@5.4.5)
|
||||
|
||||
vue-tsc@2.0.19(typescript@5.4.5):
|
||||
dependencies:
|
||||
'@volar/typescript': 2.2.5
|
||||
|
||||
@@ -47,11 +47,14 @@
|
||||
--color-input-icon: #d3d4d7;
|
||||
|
||||
--color-textarea-bg: white;
|
||||
--color-article: black;
|
||||
|
||||
--aside-width: 12rem;
|
||||
|
||||
--font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
||||
--word-font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace;
|
||||
--font-family: -apple-system, sans-serif;
|
||||
--word-font-family: ui-monospace, sans-serif;
|
||||
--en-article-family: Georgia, sans-serif;
|
||||
--zh-article-family: "Songti SC", "SimSun", "Noto Serif CJK SC", serif;
|
||||
}
|
||||
|
||||
html.dark {
|
||||
@@ -89,21 +92,21 @@ html.dark {
|
||||
--practice-wrapper-translateX: -12vw;
|
||||
--toolbar-width: 40vw;
|
||||
--article-width: 60vw;
|
||||
--panel-width: 380rem;
|
||||
--toolbar-height: 48rem;
|
||||
--panel-width: 38rem;
|
||||
--toolbar-height: 4.8rem;
|
||||
--panel-margin-left: calc(50vw + var(--practice-wrapper-translateX) + var(--toolbar-width) / 2 + 5vw);
|
||||
--article-panel-margin-left: calc(50% + var(--practice-wrapper-translateX) + var(--article-width) / 2 + 48rem);
|
||||
}
|
||||
.footer {
|
||||
.bottom {
|
||||
padding: 1.5rem 5rem 5rem 5rem !important;
|
||||
padding: 1.5rem 1rem 1rem 1rem !important;
|
||||
}
|
||||
|
||||
.stat {
|
||||
margin-top: 4rem !important;
|
||||
margin-top: 0.4rem !important;
|
||||
|
||||
.row {
|
||||
gap: 5rem !important;
|
||||
gap: 0.5rem !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -123,14 +126,14 @@ html.dark {
|
||||
|
||||
.footer {
|
||||
.bottom {
|
||||
padding: 1.5rem 5rem 5rem 5rem !important;
|
||||
padding: 1.5rem 0.5rem 0.5rem 0.5rem !important;
|
||||
}
|
||||
|
||||
.stat {
|
||||
margin-top: 4rem !important;
|
||||
margin-top: 0.4rem !important;
|
||||
|
||||
.row {
|
||||
gap: 5rem !important;
|
||||
gap: 0.5rem !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,95 +54,87 @@ onUnmounted(() => {
|
||||
|
||||
<template>
|
||||
<div class="footer " :class="!settingStore.showToolbar && 'hide'">
|
||||
<div class="bottom ">
|
||||
<div class="bottom">
|
||||
<div class="flex gap-2">
|
||||
<el-progress
|
||||
class="flex-1"
|
||||
:percentage="progress"
|
||||
:stroke-width="8"
|
||||
:show-text="false"/>
|
||||
<el-progress
|
||||
class="flex-1"
|
||||
:percentage="progress"
|
||||
:stroke-width="8"
|
||||
:show-text="false"/>
|
||||
</div>
|
||||
<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 class="center flex-col">
|
||||
<div class="text-xl">A private conversation!</div>
|
||||
<div class="options-wrapper">
|
||||
<div class="flex gap-1">
|
||||
<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 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>
|
||||
|
||||
<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="emit('edit',)"
|
||||
/>
|
||||
|
||||
<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"/>
|
||||
</div>
|
||||
</div>
|
||||
<BaseIcon
|
||||
@click="settingStore.showPanel = !settingStore.showPanel"
|
||||
:title="`面板(${settingStore.shortcutKeyMap[ShortcutKey.TogglePanel]})`"
|
||||
icon="tdesign:menu-unfold"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -188,6 +180,7 @@ onUnmounted(() => {
|
||||
margin-top: .5rem;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
gap: 2rem;
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
|
||||
@@ -9,7 +9,10 @@ import {cloneDeep} from "lodash-es";
|
||||
import {emitter, EventKey} from "@/utils/eventBus.ts";
|
||||
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';
|
||||
|
||||
interface IProps {
|
||||
article: Article,
|
||||
@@ -52,13 +55,17 @@ let hoverIndex = $ref({
|
||||
sectionIndex: -1,
|
||||
sentenceIndex: -1,
|
||||
})
|
||||
let cursor = $ref({})
|
||||
let cursor = $ref({
|
||||
top: 0,
|
||||
left: 0,
|
||||
})
|
||||
let isEnd = $ref(false)
|
||||
|
||||
const currentIndex = computed(() => {
|
||||
return `${sectionIndex}${sentenceIndex}${wordIndex}`
|
||||
})
|
||||
|
||||
const $toast = useToast();
|
||||
const playBeep = usePlayBeep()
|
||||
const playCorrect = usePlayCorrect()
|
||||
const playKeyboardAudio = usePlayKeyboardAudio()
|
||||
@@ -68,9 +75,6 @@ const store = useBaseStore()
|
||||
const statisticsStore = usePracticeStore()
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
//当跳过句子时,会同时触发光标检测和布局检测,但布局检测
|
||||
let isDelayCheckCursorPosition = false
|
||||
|
||||
window.$ = jq
|
||||
watch([() => sectionIndex, () => sentenceIndex, () => wordIndex, () => stringIndex], ([a, b, c,]) => {
|
||||
checkCursorPosition(a, b, c)
|
||||
@@ -82,26 +86,16 @@ watch(() => props.article, () => {
|
||||
wordIndex = props.wordIndex
|
||||
stringIndex = props.stringIndex
|
||||
typeArticleRef?.scrollTo({top: 0, behavior: "smooth"})
|
||||
checkTranslateLocation()
|
||||
checkCursorPosition()
|
||||
checkTranslateLocation().then(() => checkCursorPosition())
|
||||
}, {immediate: true})
|
||||
|
||||
watch(() => settingStore.dictation, () => {
|
||||
if (settingStore.translate) {
|
||||
checkTranslateLocation()
|
||||
}
|
||||
isDelayCheckCursorPosition = true
|
||||
checkCursorPosition()
|
||||
})
|
||||
watch(() => settingStore.translate, () => {
|
||||
isDelayCheckCursorPosition = true
|
||||
checkCursorPosition()
|
||||
checkTranslateLocation()
|
||||
checkTranslateLocation().then(() => checkCursorPosition())
|
||||
})
|
||||
|
||||
|
||||
function checkCursorPosition(a = sectionIndex, b = sentenceIndex, c = wordIndex) {
|
||||
console.log('checkCursorPosition', isDelayCheckCursorPosition)
|
||||
console.log('checkCursorPosition')
|
||||
_nextTick(() => {
|
||||
let currentWord = jq(`.section:nth(${a}) .sentence:nth(${b}) .word:nth(${c})`)
|
||||
// console.log(a, b, c, currentWord)
|
||||
@@ -117,33 +111,34 @@ function checkCursorPosition(a = sectionIndex, b = sentenceIndex, c = wordIndex)
|
||||
// console.log(cursor)
|
||||
}
|
||||
}
|
||||
isDelayCheckCursorPosition = false
|
||||
}, isDelayCheckCursorPosition ? 300 : 0)
|
||||
},)
|
||||
}
|
||||
|
||||
function checkTranslateLocation() {
|
||||
console.log('checkTranslateLocation')
|
||||
_nextTick(() => {
|
||||
let articleRect = articleWrapperRef.getBoundingClientRect()
|
||||
props.article.sections.map((v, i) => {
|
||||
v.map((w, j) => {
|
||||
let location = i + '-' + j
|
||||
let wordClassName = `.word${location}`
|
||||
let word = document.querySelector(wordClassName)
|
||||
let wordRect = word.getBoundingClientRect()
|
||||
let translateClassName = `.translate${location}`
|
||||
let translate: HTMLDivElement = document.querySelector(translateClassName)
|
||||
return new Promise<void>(resolve => {
|
||||
_nextTick(() => {
|
||||
let articleRect = articleWrapperRef.getBoundingClientRect()
|
||||
props.article.sections.map((v, i) => {
|
||||
v.map((w, j) => {
|
||||
let location = i + '-' + j
|
||||
let wordClassName = `.word${location}`
|
||||
let word = document.querySelector(wordClassName)
|
||||
let wordRect = word.getBoundingClientRect()
|
||||
let translateClassName = `.translate${location}`
|
||||
let translate: HTMLDivElement = document.querySelector(translateClassName)
|
||||
|
||||
translate.style.opacity = '1'
|
||||
translate.style.top = wordRect.top - articleRect.top - 22 + 'px'
|
||||
// @ts-ignore
|
||||
translate.firstChild.style.width = wordRect.left - articleRect.left + 'px'
|
||||
// console.log(word, wordRect.left - articleRect.left)
|
||||
// console.log('word-wordRect', wordRect)
|
||||
translate.style.opacity = '1'
|
||||
translate.style.top = wordRect.top - articleRect.top + 28 + 'px'
|
||||
// @ts-ignore
|
||||
translate.firstChild.style.width = wordRect.left - articleRect.left + 'px'
|
||||
// console.log(word, wordRect.left - articleRect.left)
|
||||
// console.log('word-wordRect', wordRect)
|
||||
})
|
||||
})
|
||||
})
|
||||
// checkCursorPosition(sectionIndex, sentenceIndex, wordIndex)
|
||||
}, 300)
|
||||
resolve()
|
||||
}, 300)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -180,12 +175,6 @@ function nextSentence() {
|
||||
emit('over')
|
||||
}
|
||||
} else {
|
||||
if (settingStore.dictation){
|
||||
isDelayCheckCursorPosition = true
|
||||
}
|
||||
if (settingStore.dictation && settingStore.translate) {
|
||||
checkTranslateLocation()
|
||||
}
|
||||
playWordAudio(currentSection[sentenceIndex].text)
|
||||
}
|
||||
lockNextSentence = false
|
||||
@@ -269,7 +258,6 @@ function onTyping(e: KeyboardEvent) {
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
|
||||
function play() {
|
||||
let currentSection = props.article.sections[sectionIndex]
|
||||
|
||||
@@ -300,48 +288,12 @@ function playWord(word: ArticleWord) {
|
||||
playWordAudio(word.word)
|
||||
}
|
||||
|
||||
function currentWordString(word: ArticleWord, i: number, i2: number,) {
|
||||
let str = word.word.slice(input.length + wrong.length, input.length + wrong.length + 1)
|
||||
if (word.isSymbol) {
|
||||
return str
|
||||
}
|
||||
if (hoverIndex.sectionIndex === i && hoverIndex.sentenceIndex === i2) {
|
||||
return str
|
||||
}
|
||||
|
||||
if (settingStore.dictation) {
|
||||
return '_'
|
||||
}
|
||||
return str
|
||||
function indexWord(word: ArticleWord) {
|
||||
return word.word.slice(input.length, input.length + 1)
|
||||
}
|
||||
|
||||
function currentWordEnd(word: ArticleWord, i: number, i2: number,) {
|
||||
let str = word.word.slice(input.length + wrong.length + (wrong ? 0 : 1))
|
||||
if (hoverIndex.sectionIndex === i && hoverIndex.sentenceIndex === i2) {
|
||||
return str
|
||||
}
|
||||
|
||||
if (settingStore.dictation) {
|
||||
return str.split('').map(v => '_').join('')
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
function getAllWord(word: ArticleWord, i: number, i2: number, i3: number) {
|
||||
let str = word.word
|
||||
if (word.isSymbol) {
|
||||
return str
|
||||
}
|
||||
|
||||
if (hoverIndex.sectionIndex === i && hoverIndex.sentenceIndex === i2) {
|
||||
return str
|
||||
}
|
||||
|
||||
//剩100是因为,可能存在特殊情况,比如003,010这种,0 12 24,100
|
||||
if (sectionIndex * 10000 + sentenceIndex * 100 + wordIndex < i * 10000 + i2 * 100 + i3 && settingStore.dictation) {
|
||||
return str.split('').map(v => '_').join('')
|
||||
}
|
||||
return str
|
||||
function remainderWord(word: ArticleWord,) {
|
||||
return word.word.slice(input.length + 1)
|
||||
}
|
||||
|
||||
function showSentence(i1: number = sectionIndex, i2: number = sentenceIndex) {
|
||||
@@ -352,6 +304,54 @@ function hideSentence() {
|
||||
hoverIndex = {sectionIndex: -1, sentenceIndex: -1}
|
||||
}
|
||||
|
||||
function onContextMenu(e: MouseEvent, text: string, i, j) {
|
||||
//prevent the browser's default menu
|
||||
e.preventDefault();
|
||||
//show your menu
|
||||
ContextMenu.showContextMenu({
|
||||
x: e.x,
|
||||
y: e.y,
|
||||
items: [
|
||||
{
|
||||
label: "从这开始",
|
||||
onClick: () => {
|
||||
sectionIndex = i
|
||||
sentenceIndex = j
|
||||
playWordAudio(text)
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "播放",
|
||||
onClick: () => {
|
||||
playWordAudio(text)
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "复制",
|
||||
onClick: () => {
|
||||
navigator.clipboard.writeText(text).then(r => {
|
||||
$toast.success('已复制!', {position: 'top'});
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "语法分析",
|
||||
onClick: () => {
|
||||
navigator.clipboard.writeText(text).then(r => {
|
||||
$toast.success('已复制!随后将打开语法分析网站!', {
|
||||
position: 'top',
|
||||
duration: 3000,
|
||||
});
|
||||
setTimeout(() => {
|
||||
window.open('https://enpuz.com/')
|
||||
}, 1000)
|
||||
})
|
||||
}
|
||||
},
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
emitter.on(EventKey.resetWord, () => {
|
||||
wrong = input = ''
|
||||
@@ -377,16 +377,19 @@ defineExpose({showSentence, play, del, hideSentence, nextSentence})
|
||||
<div class="titleTranslate" v-if="settingStore.translate">{{ props.article.titleTranslate }}</div>
|
||||
</header>
|
||||
<div class="article-content" ref="articleWrapperRef">
|
||||
<article :class="settingStore.translate && 'tall'">
|
||||
<article :class="[
|
||||
settingStore.translate && 'tall',
|
||||
settingStore.dictation && 'dictation',
|
||||
]">
|
||||
<div class="section" v-for="(section,indexI) in props.article.sections">
|
||||
<span class="sentence"
|
||||
:class="[
|
||||
sectionIndex === indexI && sentenceIndex === indexJ && settingStore.dictation
|
||||
?'dictation':''
|
||||
hoverIndex.sectionIndex === indexI && hoverIndex.sentenceIndex === indexJ
|
||||
&&'hover-show'
|
||||
]"
|
||||
@contextmenu="e=>onContextMenu(e,sentence.text,indexI,indexJ)"
|
||||
@mouseenter="settingStore.allowWordTip && showSentence(indexI,indexJ)"
|
||||
@mouseleave="hideSentence"
|
||||
@click="playWordAudio(sentence.text)"
|
||||
v-for="(sentence,indexJ) in section">
|
||||
<span
|
||||
v-for="(word,indexW) in sentence.words"
|
||||
@@ -402,17 +405,20 @@ defineExpose({showSentence, play, del, hideSentence, nextSentence})
|
||||
''),
|
||||
(`${indexI}${indexJ}${indexW}` === currentIndex && !isSpace && wrong )?'word-wrong':'',
|
||||
indexW === 0 && `word${indexI}-${indexJ}`
|
||||
]"
|
||||
@click="playWord(word)">
|
||||
<span class="all-word" v-if="`${indexI}${indexJ}${indexW}` === currentIndex && !isSpace">
|
||||
]">
|
||||
<span class="word-wrap" v-if="`${indexI}${indexJ}${indexW}` === currentIndex && !isSpace">
|
||||
<span class="word-start" v-if="input">{{ input }}</span>
|
||||
<span class="word-end">
|
||||
<span class="wrong" :class="wrong === ' ' && 'bg-wrong'" v-if="wrong">{{ wrong }}</span>
|
||||
<span class="input" v-else>{{ currentWordString(word, indexI, indexJ) }}</span>
|
||||
<span>{{ currentWordEnd(word, indexI, indexJ,) }}</span>
|
||||
<span :class="!word.isSymbol && 'dictation-hide'" v-else>{{ indexWord(word) }}</span>
|
||||
<span class="dictation-hide">{{ remainderWord(word) }}</span>
|
||||
</span>
|
||||
<span class="border-bottom" v-if="settingStore.dictation"></span>
|
||||
</span>
|
||||
<span v-else class="word-wrap">
|
||||
<span :class="!word.isSymbol && 'dictation-hide'">{{ word.word }}</span>
|
||||
<span class="border-bottom" v-if="settingStore.dictation"></span>
|
||||
</span>
|
||||
<span v-else class="all-word">{{ getAllWord(word, indexI, indexJ, indexW) }}</span>
|
||||
<span
|
||||
v-if="word.nextSpace"
|
||||
class="word-end"
|
||||
@@ -422,7 +428,6 @@ defineExpose({showSentence, play, del, hideSentence, nextSentence})
|
||||
>
|
||||
<span class="word-space"
|
||||
:class="[
|
||||
`${indexI}${indexJ}${indexW}`,
|
||||
settingStore.dictation && 'to-bottom',
|
||||
(`${indexI}${indexJ}${indexW}` === currentIndex && isSpace && !wrong ) && 'wait',
|
||||
]"
|
||||
@@ -433,10 +438,17 @@ defineExpose({showSentence, play, del, hideSentence, nextSentence})
|
||||
</div>
|
||||
</article>
|
||||
<div class="translate" v-show="settingStore.translate">
|
||||
<template v-for="(v,i) in props.article.sections">
|
||||
<template v-for="(v,indexI) in props.article.sections">
|
||||
<div class="row"
|
||||
:class="`translate${i+'-'+j}`"
|
||||
v-for="(item,j) in v">
|
||||
:class="[
|
||||
`translate${indexI+'-'+indexJ}`,
|
||||
(sectionIndex>indexI
|
||||
?'wrote':
|
||||
(sectionIndex>=indexI &&sentenceIndex>indexJ)
|
||||
?'wrote' :
|
||||
''),
|
||||
]"
|
||||
v-for="(item,indexJ) in v">
|
||||
<span class="space"></span>
|
||||
<Transition name="fade">
|
||||
<span class="text" v-if="item.translate">{{ item.translate }}</span>
|
||||
@@ -453,19 +465,17 @@ defineExpose({showSentence, play, del, hideSentence, nextSentence})
|
||||
@import "@/assets/css/style";
|
||||
|
||||
.wrote {
|
||||
//color: green;
|
||||
color: rgb(22, 163, 74);
|
||||
color: grey;
|
||||
//color: rgb(22, 163, 74);
|
||||
}
|
||||
|
||||
.word {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
$article-width: 1000px;
|
||||
.typing-article {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
color: var(--color-article);
|
||||
|
||||
header {
|
||||
word-wrap: break-word;
|
||||
@@ -474,16 +484,15 @@ $article-width: 1000px;
|
||||
|
||||
.title {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
color: rgba(gray, .8);
|
||||
font-size: 2.2rem;
|
||||
font-family: var(--word-font-family);
|
||||
font-family: var(--en-article-family);
|
||||
}
|
||||
|
||||
.titleTranslate {
|
||||
@extend .title;
|
||||
font-size: 1.2rem;
|
||||
font-family: unset;
|
||||
font-family: var(--zh-article-family);
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -495,33 +504,71 @@ $article-width: 1000px;
|
||||
article {
|
||||
font-size: 1.6rem;
|
||||
line-height: 1.3;
|
||||
color: gray;
|
||||
word-break: keep-all;
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
font-family: var(--en-article-family);
|
||||
|
||||
&.dictation {
|
||||
.dictation-hide {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.border-bottom {
|
||||
display: inline-block !important;
|
||||
}
|
||||
}
|
||||
|
||||
.wrote, .hover-show {
|
||||
.dictation-hide {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.border-bottom {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.hover-show {
|
||||
background: var(--color-main-active);
|
||||
color: white !important;
|
||||
.wrote{
|
||||
color: white !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.tall {
|
||||
line-height: 2.5;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 3rem;
|
||||
margin-bottom: 1.2rem;
|
||||
|
||||
.sentence {
|
||||
transition: all .3s;
|
||||
|
||||
&:first-child {
|
||||
padding-left: .2rem;
|
||||
}
|
||||
|
||||
&.dictation {
|
||||
font-family: var(--word-font-family);
|
||||
letter-spacing: .2rem;
|
||||
padding-left: 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
.word {
|
||||
display: inline-block;
|
||||
|
||||
.word-wrap {
|
||||
position: relative;
|
||||
|
||||
.border-bottom {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
border-bottom: 2px solid var(--color-article);
|
||||
display: none;
|
||||
transform: translateY(-0.2rem);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -534,16 +581,17 @@ $article-width: 1000px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
font-size: 1.1rem;
|
||||
color: gray;
|
||||
line-height: 3.5;
|
||||
letter-spacing: .2rem;
|
||||
//display: none;
|
||||
font-family: var(--zh-article-family);
|
||||
font-weight: bold;
|
||||
|
||||
.row {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
opacity: 0;
|
||||
transition: all .3s;
|
||||
|
||||
.space {
|
||||
transition: all .3s;
|
||||
@@ -554,7 +602,6 @@ $article-width: 1000px;
|
||||
|
||||
.word-space {
|
||||
position: relative;
|
||||
color: gray;
|
||||
display: inline-block;
|
||||
width: 0.8rem;
|
||||
height: 1.5rem;
|
||||
@@ -566,14 +613,14 @@ $article-width: 1000px;
|
||||
}
|
||||
|
||||
&.wait {
|
||||
border-bottom: 2px solid gray;
|
||||
border-bottom: 2px solid var(--color-article);
|
||||
|
||||
&::after {
|
||||
content: ' ';
|
||||
position: absolute;
|
||||
width: .1rem;
|
||||
width: 2px;
|
||||
height: .25rem;
|
||||
background: gray;
|
||||
background: var(--color-article);
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
@@ -581,16 +628,15 @@ $article-width: 1000px;
|
||||
&::before {
|
||||
content: ' ';
|
||||
position: absolute;
|
||||
width: .1rem;
|
||||
width: 2px;
|
||||
height: .26rem;
|
||||
background: gray;
|
||||
background: var(--color-article);
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.word-start {
|
||||
color: var(--color-main-active);
|
||||
}
|
||||
@@ -607,7 +653,6 @@ $article-width: 1000px;
|
||||
.bg-wrong {
|
||||
display: inline-block;
|
||||
line-height: 1;
|
||||
color: gray;
|
||||
background: rgba(red, 0.6);
|
||||
animation: shake 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
|
||||
}
|
||||
@@ -623,7 +668,7 @@ $article-width: 1000px;
|
||||
|
||||
@keyframes underline {
|
||||
0%, 100% {
|
||||
border-left: .1rem solid black;
|
||||
border-left: .1rem solid var(--color-article);
|
||||
}
|
||||
50% {
|
||||
border-left: .1rem solid transparent;
|
||||
|
||||
Reference in New Issue
Block a user