feat: 调整文章编辑器

This commit is contained in:
zyronon
2025-03-22 03:03:02 +08:00
parent c760264048
commit 47d4958a6a
5 changed files with 279 additions and 215 deletions

View File

@@ -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
View File

@@ -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

View File

@@ -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;
}
}
}

View File

@@ -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;

View File

@@ -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 24100
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;