Merge branch 'refs/heads/master' into dev

# Conflicts:
#	src/pages/article/BookList.vue
#	src/pages/article/components/VolumeSetting.vue
#	src/pages/word/DictDetail.vue
#	src/pages/word/DictList.vue
This commit is contained in:
Zyronon
2025-10-19 03:05:56 +08:00
19 changed files with 241 additions and 298 deletions

View File

@@ -30,8 +30,8 @@
<br/>
</p>
![image](/public/word.png)
![image](/public/article.png)
<img width="1920" height="1440" alt="295shots_so" src="https://github.com/user-attachments/assets/383ed437-856e-48fe-92b0-9619babb49be" />
<img width="1920" height="1440" alt="922shots_so" src="https://github.com/user-attachments/assets/5b5fa13f-747c-4368-ae21-3c9d7d30fbc7" />
## 在线访问
@@ -78,7 +78,7 @@
## 运行
#### 注:本项目可单独运行,数据保存在本地,换设备需手动备份数据,不影响正常使用;官方部署版本包含后端接口用于同步数据,后端项目暂未开源
#### 注:本项目可单独运行,数据保存在本地,换设备需手动备份数据,不影响正常使用;
本项目是基于`Vue`开发的,需要 node 环境来运行。
1. 安装 NodeJS参考[官方文档](https://nodejs.org/en/download)

1
components.d.ts vendored
View File

@@ -96,7 +96,6 @@ declare module 'vue' {
IconSimpleIconsWechat: typeof import('~icons/simple-icons/wechat')['default']
IconSimpleIconsXiaohongshu: typeof import('~icons/simple-icons/xiaohongshu')['default']
IconSystemUiconsImport: typeof import('~icons/system-uicons/import')['default']
Input: typeof import('./src/components/Input.vue')['default']
InputNumber: typeof import('./src/components/base/InputNumber.vue')['default']
List: typeof import('./src/components/list/List.vue')['default']
Logo: typeof import('./src/components/Logo.vue')['default']

View File

@@ -1,5 +1,5 @@
<h1 align="center">
Type Words
<h1 align=center>
<img src="https://github.com/user-attachments/assets/9d626e0f-0601-4640-8981-ad66d8ac4853" alt="TypeWords" style="width: 500px;"/>
</h1>
<p align="center">
@@ -34,9 +34,8 @@ Practice English, one strike, one step forward
<br/>
</p>
![image](/public/word.png)
![image](/public/article.png)
<img width="1920" height="1440" alt="295shots_so" src="https://github.com/user-attachments/assets/383ed437-856e-48fe-92b0-9619babb49be" />
<img width="1920" height="1440" alt="922shots_so" src="https://github.com/user-attachments/assets/5b5fa13f-747c-4368-ae21-3c9d7d30fbc7" />
## Online visit

View File

@@ -3277,12 +3277,6 @@ async function start() {
// console.log(JSON.stringify(v, null, 2));
let res = await fetch('http://localhost/v1/words/addDict', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
...v,

File diff suppressed because one or more lines are too long

View File

@@ -140,8 +140,8 @@
"id": "0Zgns4",
"title": "Do you speak English?",
"titleTranslate": "你会讲英语吗?",
"text": "I had an amusing experience last year. \nAfter I had left a small village in the south of France, I drove on to the next town. \nOn the way, a young man waved to me. \nI stopped and he asked me for a lift. \nAs soon as he had got into the car, I said good morning to him in French and he replied in the same language. \nApart from a few words, I do not know any French at all. \nNeither of us spoke during the journey. \nI had nearly reached the town, when the young man suddenly said, very slowly, 'Do you speak English?' As I soon learnt, he was English himself!",
"textTranslate": "去年我有过一次有趣的经历。 \n在离开法国南部的一个小村庄后我继续驶往下一个城镇。 \n途中一个青年人向我招手。 \n我把车停下他向我提出要求搭车。 \n他一上车我就用法语向他问早上好他也同样用法语回答我。 \n除了个别几个单词外我根本不会法语。 \n旅途中我们谁也没讲话。 \n就要到达那个镇时那青年突然开了口慢慢地说道“你会讲英语吗”",
"text": "I had an amusing experience last year. \nAfter I had left a small village in the south of France, I drove on to the next town. \nOn the way, a young man waved to me. \nI stopped and he asked me for a lift. \nAs soon as he had got into the car, I said good morning to him in French and he replied in the same language. \nApart from a few words, I do not know any French at all. \nNeither of us spoke during the journey. \nI had nearly reached the town, when the young man suddenly said, very slowly, 'Do you speak English?' \nAs I soon learnt, he was English himself!",
"textTranslate": "去年我有过一次有趣的经历。 \n在离开法国南部的一个小村庄后我继续驶往下一个城镇。 \n途中一个青年人向我招手。 \n我把车停下他向我提出要求搭车。 \n他一上车我就用法语向他问早上好他也同样用法语回答我。 \n除了个别几个单词外我根本不会法语。 \n旅途中我们谁也没讲话。 \n就要到达那个镇时那青年突然开了口慢慢地说道“你会讲英语吗 \n我很快就知道他自己就是个英国人。",
"newWords": [],
"textAllWords": [],
"audioSrc": "/sound/article/nce2-1/Do you speak English.mp3",

View File

@@ -214,34 +214,6 @@ a {
text-decoration: none;
}
.base-textarea {
flex: 1;
font-family: var(--font-family);
font-size: 1.1rem;
outline: none;
border: 1px solid transparent;
border-radius: .4rem;
padding: .5rem .6rem;
transition: all .3s;
min-height: 1.2rem;
width: 100%;
box-sizing: border-box;
background: var(--color-textarea-bg);
&:focus {
border: 1px solid var(--color-select-bg);
}
&[readonly] {
cursor: not-allowed;
opacity: .7;
}
}
.base-input {
@extend .base-textarea;
flex: none;
}
@supports selector(::-webkit-scrollbar) {
::-webkit-scrollbar {
@@ -348,7 +320,7 @@ a {
gap: .5rem;
color: var(--color-main-text);
span{
span {
flex-shrink: 0;
}
@@ -469,7 +441,7 @@ a {
}
}
#typing-listener{
#typing-listener {
position: fixed;
right: 0;
bottom: 0;

View File

@@ -6,7 +6,6 @@ import MiniDialog from "@/components/dialog/MiniDialog.vue";
import BaseIcon from "@/components/BaseIcon.vue";
import BaseButton from "@/components/BaseButton.vue";
import {cloneDeep, debounce, reverse, shuffle} from "@/utils";
import Input from "@/components/Input.vue";
import PopConfirm from "@/components/PopConfirm.vue";
import Empty from "@/components/Empty.vue";
import Pagination from '@/components/base/Pagination.vue'
@@ -14,6 +13,7 @@ import Toast from '@/components/base/toast/Toast.ts'
import Checkbox from "@/components/base/checkbox/Checkbox.vue";
import DeleteIcon from "@/components/icon/DeleteIcon.vue";
import Dialog from "@/components/dialog/Dialog.vue";
import BaseInput from "@/components/base/BaseInput.vue";
let list = defineModel('list')
@@ -146,11 +146,17 @@ defineRender(
{
showSearchInput ? (
<div class="flex gap-4">
<Input
prefixIcon
<BaseInput
clearable
modelValue={searchKey}
onUpdate:modelValue={debounce(e => searchKey = e)}
class="flex-1"/>
class="flex-1">
{{
subfix: () => <IconFluentSearch24Regular
class="text-lg text-gray"
/>
}}
</BaseInput>
<BaseButton onClick={() => (showSearchInput = false, searchKey = '')}>取消</BaseButton>
</div>
) : (

View File

@@ -1,97 +0,0 @@
<script setup lang="ts">
import Close from "@/components/icon/Close.vue";
import {useDisableEventListener, useWindowClick} from "@/hooks/event.ts";
defineProps<{
modelValue: string
placeholder?: string
autofocus?: boolean
prefixIcon?: boolean
}>()
defineEmits(['update:modelValue'])
let focus = $ref(false)
let inputEl = $ref<HTMLDivElement>()
useWindowClick((e: PointerEvent) => {
if (!e) return
focus = inputEl.contains(e.target as any);
})
useDisableEventListener(() => focus)
const vFocus = {
mounted: (el, bind) => {
if (bind.value) {
el.focus()
setTimeout(() => focus = true)
}
}
}
</script>
<template>
<div class="base-input"
:class="{focus}"
ref="inputEl"
>
<IconFluentSearch24Regular
v-if="prefixIcon"
width="20"/>
<input type="text"
:value="modelValue"
v-focus="autofocus"
:placeholder="placeholder"
@input="e=>$emit('update:modelValue',e.target.value)"
>
<transition name="fade">
<Close v-if="modelValue" @click="$emit('update:modelValue','')"/>
</transition>
</div>
</template>
<style scoped lang="scss">
.base-input {
border: 1px solid var(--color-input-border);
border-radius: .4rem;
overflow: hidden;
padding: .2rem .3rem;
transition: all .3s;
display: flex;
align-items: center;
background: var(--color-input-bg);
:deep(svg) {
transition: all .3s;
color: var(--color-input-icon);
}
&.focus {
border: 1px solid var(--color-select-bg);
:deep(svg) {
color: gray;
}
}
input {
font-family: var(--font-family);
font-size: 1.1rem;
outline: none;
min-height: 1.2rem;
flex: 1;
box-sizing: border-box;
outline: none;
border: none;
background: transparent;
&[readonly] {
cursor: not-allowed;
opacity: .7;
}
}
}
</style>

View File

@@ -1,10 +1,13 @@
<script setup lang="ts">
import {ref, useAttrs, watch} from 'vue';
import { ref, useAttrs, watch } from 'vue';
import Close from "@/components/icon/Close.vue";
import { useDisableEventListener } from "@/hooks/event.ts";
const props = defineProps({
modelValue: [String, Number],
placeholder: String,
disabled: Boolean,
autofocus: Boolean,
type: {
type: String,
default: 'text',
@@ -25,6 +28,8 @@ const attrs = useAttrs();
const inputValue = ref(props.modelValue);
const errorMsg = ref('');
let focus = $ref(false)
let inputEl = $ref<HTMLDivElement>()
watch(() => props.modelValue, (val) => {
inputValue.value = val;
@@ -57,24 +62,42 @@ const onChange = (e: Event) => {
};
const onFocus = (e: FocusEvent) => {
focus = true
emit('focus', e);
};
const onBlur = (e: FocusEvent) => {
focus = false
validate(inputValue.value);
emit('blur', e);
};
const clearInput = () => {
inputValue.value = '';
validate('');
emit('update:modelValue', '');
};
//当聚焦时,禁用输入监听
useDisableEventListener(() => focus)
const vFocus = {
mounted: (el, bind) => {
if (bind.value) {
el.focus()
setTimeout(() => focus = true)
}
}
}
</script>
<template>
<div class="custom-input" :class="{ 'is-disabled': disabled, 'has-error': errorMsg }">
<div class="base-input2"
ref="inputEl"
:class="{ 'is-disabled': disabled, 'has-error': errorMsg,focus }">
<slot name="subfix"></slot>
<input
v-bind="attrs"
:type="type"
@@ -85,88 +108,71 @@ const clearInput = () => {
@change="onChange"
@focus="onFocus"
@blur="onBlur"
class="custom-input__inner"
class="inner"
v-focus="autofocus"
:maxlength="maxLength"
/>
<button
<slot name="prefix"></slot>
<Close
v-if="clearable && inputValue && !disabled"
type="button"
class="custom-input__clear"
@click="clearInput"
aria-label="Clear input"
>×
</button>
<div v-if="errorMsg" class="custom-input__error">{{ errorMsg }}</div>
@click="clearInput"/>
<div v-if="errorMsg" class="base-input2__error">{{ errorMsg }}</div>
</div>
</template>
<style scoped lang="scss">
.custom-input {
.base-input2 {
position: relative;
display: inline-block;
display: inline-flex;
box-sizing: border-box;
width: 100%;
background: var(--color-input-bg);
border: 1px solid var(--color-input-border);
border-radius: 4px;
overflow: hidden;
padding: .2rem .3rem;
transition: all .3s;
align-items: center;
background: var(--color-input-bg);
&.is-disabled {
opacity: 0.6;
}
&.has-error {
.custom-input__inner {
.base-input2__inner {
border-color: #f56c6c;
}
.custom-input__error {
.base-input2__error {
color: #f56c6c;
font-size: 0.85rem;
margin-top: 0.25rem;
}
}
&__inner {
width: 100%;
padding: 0.4rem 1.5rem 0.4rem 0.5rem;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 1rem;
box-sizing: border-box;
transition: all .3s;
color: var(--color-input-color);
background: var(--color-input-bg);
&:focus {
outline: none;
border-color: #409eff;
box-shadow: 0 0 3px #409eff;
}
&:disabled {
background-color: #f5f5f5;
cursor: not-allowed;
}
&.focus {
border: 1px solid var(--color-select-bg);
}
&__clear {
position: absolute;
right: 0.4rem;
top: 50%;
transform: translateY(-50%);
border: none;
background: transparent;
font-size: 1.2rem;
line-height: 1;
cursor: pointer;
color: #999;
padding: 0;
user-select: none;
&:hover {
color: #666;
}
&:disabled {
background-color: #f5f5f5;
cursor: not-allowed;
}
&__error {
padding-left: 0.5rem;
}
.inner {
flex: 1;
font-size: 1rem;
outline: none;
border: none;
box-sizing: border-box;
transition: all .3s;
height: 1.5rem;
color: var(--color-input-color);
}
}
</style>

View File

@@ -104,8 +104,7 @@ textarea {
&:focus {
outline: none;
border-color: #409eff;
box-shadow: 0 0 3px #409eff;
border: 1px solid var(--color-select-bg);
}
}
</style>

View File

@@ -1,10 +1,8 @@
<script setup lang="ts">
import Input from "@/components/Input.vue";
import {Article} from "@/types/types.ts";
import { Article } from "@/types/types.ts";
import BaseList from "@/components/list/BaseList.vue";
import * as sea from "node:sea";
import {watch, watchEffect} from "vue";
import BaseInput from "@/components/base/BaseInput.vue";
const props = withDefaults(defineProps<{
list: Article[],
@@ -34,7 +32,9 @@ let localList = $computed(() => {
let d = Number(t)
//如果是纯数字,把那一条加进去
if (!isNaN(d)) {
res.push(props.list[d])
if (d - 1 < props.list.length) {
res.push(props.list[d - 1])
}
}
} catch (err) {
}
@@ -69,7 +69,14 @@ defineExpose({scrollToBottom, scrollToItem})
<template>
<div class="list">
<div class="search">
<Input prefix-icon v-model="searchKey"/>
<BaseInput
clearable
v-model="searchKey"
>
<template #subfix>
<IconFluentSearch24Regular class="text-lg text-gray"/>
</template>
</BaseInput>
</div>
<BaseList
ref="listRef"

View File

@@ -1,10 +1,10 @@
<script setup lang="ts" generic="T extends {id:string}">
import BaseIcon from "@/components/BaseIcon.vue";
import Input from "@/components/Input.vue";
import {cloneDeep, throttle} from "@/utils";
import {Article} from "@/types/types.ts";
import { cloneDeep, throttle } from "@/utils";
import { Article } from "@/types/types.ts";
import DeleteIcon from "@/components/icon/DeleteIcon.vue";
import BaseInput from "@/components/base/BaseInput.vue";
interface IProps {
list: T[]
@@ -102,7 +102,14 @@ defineExpose({scrollBottom})
ref="el"
>
<div class="search">
<Input prefix-icon v-model="searchKey"/>
<BaseInput
clearable
v-model="searchKey"
>
<template #subfix>
<IconFluentSearch24Regular class="text-lg text-gray"/>
</template>
</BaseInput>
</div>
<transition-group name="drag" class="list" tag="div">
<div class="item"

View File

@@ -5,7 +5,6 @@ import { DictResource } from "@/types/types.ts";
import { useRuntimeStore } from "@/stores/runtime.ts";
import BaseIcon from "@/components/BaseIcon.vue";
import Empty from "@/components/Empty.vue";
import Input from "@/components/Input.vue";
import BaseButton from "@/components/BaseButton.vue";
import DictList from "@/components/list/DictList.vue";
import BackIcon from "@/components/BackIcon.vue";
@@ -14,6 +13,7 @@ import { computed } from "vue";
import { getDefaultDict } from "@/types/func.ts";
import { useFetch } from "@vueuse/core";
import { DICT_LIST } from "@/config/env.ts";
import BaseInput from "@/components/base/BaseInput.vue";
const {nav} = useNav()
const runtimeStore = useRuntimeStore()
@@ -55,7 +55,7 @@ const searchList = computed<any[]>(() => {
<div class="flex items-center relative gap-2">
<BackIcon class="z-2" @Click='router.back'/>
<div class="flex flex-1 gap-4" v-if="showSearchInput">
<Input prefix-icon placeholder="请输入书籍名称/缩写/类别" v-model="searchKey" class="flex-1" autofocus/>
<BaseInput prefix-icon placeholder="请输入书籍名称/缩写/类别" v-model="searchKey" class="flex-1" autofocus clearable/>
<BaseButton @click="showSearchInput = false, searchKey = ''">取消</BaseButton>
</div>
<div class="py-1 flex flex-1 justify-end" v-else>

View File

@@ -207,16 +207,6 @@ function setArticle(val: Article) {
allWrongWords = new Set()
articleData.list[store.sbook.lastLearnIndex] = val
articleData.article = val
savePracticeData()
clearInterval(timer)
timer = setInterval(() => {
if (isFocus) {
statStore.spend += 1000
savePracticeData(false)
}
}, 1000)
let ignoreList = [store.allIgnoreWords, store.knownWords][settingStore.ignoreSimpleWord ? 0 : 1]
articleData.article.sections.map((v, i) => {
v.map((w) => {
@@ -227,6 +217,16 @@ function setArticle(val: Article) {
})
})
})
savePracticeData()
clearInterval(timer)
timer = setInterval(() => {
if (isFocus) {
statStore.spend += 1000
savePracticeData(false)
}
}, 1000)
_nextTick(typingArticleRef?.init)
window.umami?.track('startStudyArticle', {
@@ -314,8 +314,8 @@ function wrong(word: Word) {
if (settingStore.ignoreSimpleWord) {
if (store.simpleWords.includes(temp)) return
}
if (!allWrongWords.has(word.word.toLowerCase())) {
allWrongWords.add(word.word.toLowerCase())
if (!allWrongWords.has(temp)) {
allWrongWords.add(temp)
statStore.wrong++
}

View File

@@ -38,3 +38,6 @@ useWindowClick(() => show = false)
</MiniDialog>
</div>
</template>
<style scoped lang="scss">
</style>

View File

@@ -28,6 +28,7 @@ import { useSettingStore } from "@/stores/setting.ts";
import { MessageBox } from "@/utils/MessageBox.tsx";
import { CAN_REQUEST, Origin } from "@/config/env.ts";
import { detail } from "@/apis";
import { PracticeSaveWordKey } from "@/utils/const.ts";
const runtimeStore = useRuntimeStore()
const base = useBaseStore()
@@ -224,6 +225,7 @@ const {nav} = useNav()
//todo 可以和首页合并
async function startPractice() {
localStorage.removeItem(PracticeSaveWordKey.key)
studyLoading = true
await base.changeDict(runtimeStore.editDict)
studyLoading = false
@@ -366,6 +368,10 @@ async function exportData() {
exportLoading = false
}
function searchWord() {
console.log('wordForm.word',wordForm.word)
}
defineRender(() => {
return (
<BasePage>
@@ -447,7 +453,9 @@ defineRender(() => {
<BaseInput
modelValue={wordForm.word}
onUpdate:modelValue={e => wordForm.word = e}
/>
>
</BaseInput>
</FormItem>
<FormItem label="英音音标">
<BaseInput

View File

@@ -5,7 +5,6 @@ import { DictResource } from "@/types/types.ts";
import { useRuntimeStore } from "@/stores/runtime.ts";
import BaseIcon from "@/components/BaseIcon.vue";
import Empty from "@/components/Empty.vue";
import Input from "@/components/Input.vue";
import BaseButton from "@/components/BaseButton.vue";
import DictList from "@/components/list/DictList.vue";
import BackIcon from "@/components/BackIcon.vue";
@@ -16,6 +15,7 @@ import { computed } from "vue";
import { getDefaultDict } from "@/types/func.ts";
import { useFetch } from "@vueuse/core";
import { DICT_LIST } from "@/config/env.ts";
import BaseInput from "@/components/base/BaseInput.vue";
const {nav} = useNav()
const runtimeStore = useRuntimeStore()
@@ -85,7 +85,7 @@ const searchList = computed<any[]>(() => {
<div class="flex items-center relative gap-2">
<BackIcon class="z-2" @click='router.back'/>
<div class="flex flex-1 gap-4" v-if="showSearchInput">
<Input prefix-icon placeholder="请输入词典名称/缩写/类别" v-model="searchKey" class="flex-1" autofocus/>
<BaseInput clearable placeholder="请输入词典名称/缩写/类别" v-model="searchKey" class="flex-1" autofocus/>
<BaseButton @click="showSearchInput = false, searchKey = ''">取消</BaseButton>
</div>
<div class="py-1 flex flex-1 justify-end" v-else>

View File

@@ -1,15 +1,15 @@
<script setup lang="ts">
import { ShortcutKey, Word } from "@/types/types.ts";
import {ShortcutKey, Word} from "@/types/types.ts";
import VolumeIcon from "@/components/icon/VolumeIcon.vue";
import { useSettingStore } from "@/stores/setting.ts";
import { usePlayBeep, usePlayCorrect, usePlayKeyboardAudio, usePlayWordAudio, useTTsPlayAudio } from "@/hooks/sound.ts";
import { emitter, EventKey } from "@/utils/eventBus.ts";
import { nextTick, onMounted, onUnmounted, watch } from "vue";
import {useSettingStore} from "@/stores/setting.ts";
import {usePlayBeep, usePlayCorrect, usePlayKeyboardAudio, usePlayWordAudio, useTTsPlayAudio} from "@/hooks/sound.ts";
import {emitter, EventKey} from "@/utils/eventBus.ts";
import {nextTick, onMounted, onUnmounted, watch} from "vue";
import Tooltip from "@/components/base/Tooltip.vue";
import SentenceHightLightWord from "@/pages/word/components/SentenceHightLightWord.vue";
import { usePracticeStore } from "@/stores/practice.ts";
import { getDefaultWord } from "@/types/func.ts";
import { _nextTick, sleep } from "@/utils";
import {usePracticeStore} from "@/stores/practice.ts";
import {getDefaultWord} from "@/types/func.ts";
import {_nextTick, sleep} from "@/utils";
interface IProps {
word: Word,
@@ -219,8 +219,6 @@ function play() {
defineExpose({del, showWord, hideWord, play})
let tab = $ref(0)
function mouseleave() {
setTimeout(() => {
showFullWord = false
@@ -299,8 +297,8 @@ function checkCursorPosition() {
}}]
</div>
<VolumeIcon
:title="`发音(${settingStore.shortcutKeyMap[ShortcutKey.PlayWordPronunciation]})`"
ref="volumeIconRef" :simple="true" :cb="() => playWordAudio(word.word)"/>
:title="`发音(${settingStore.shortcutKeyMap[ShortcutKey.PlayWordPronunciation]})`"
ref="volumeIconRef" :simple="true" :cb="() => playWordAudio(word.word)"/>
</div>
<div class="word my-1"
@@ -318,78 +316,102 @@ function checkCursorPosition() {
<span class="letter" v-else>{{ displayWord }}</span>
</div>
<div class="translate anim"
v-opacity="settingStore.translate"
<div class="translate anim flex flex-col gap-2 my-3"
v-opacity="settingStore.translate || showFullWord"
:style="{
fontSize: settingStore.fontSize.wordTranslateFontSize +'px',
}"
>
<div class="my-2 flex" v-for="(v,i) in word.trans">
<div class="shrink-0" :class="v.pos && 'w-12'">{{ v.pos }}</div>
<div class="flex" v-for="(v,i) in word.trans">
<div class="shrink-0" :class="v.pos ? 'w-12 en-article-family' : '-ml-3'">{{ v.pos }}</div>
<span v-if="settingStore.dictation && !showFullWord" v-html="hideWordInTranslation(v.cn, word.word)"></span>
<span v-else>{{ v.cn }}</span>
</div>
</div>
</div>
<div class="other">
<template v-if="word.sentences && word.sentences.length">
<div class="line-white my-4"></div>
<div class="sentences">
<div class="sentence my-2" v-for="item in word.sentences">
<SentenceHightLightWord class="text-lg" :text="item.c" :word="word.word"
<div class="line-white my-2"></div>
<template v-if="word?.sentences?.length">
<div class="flex flex-col gap-3">
<div class="sentence" v-for="item in word.sentences">
<SentenceHightLightWord class="text-xl" :text="item.c" :word="word.word"
:dictation="(settingStore.dictation && !showFullWord)"/>
<div class="text-md anim" v-opacity="settingStore.translate">{{ item.cn }}</div>
<div class="text-base anim" v-opacity="settingStore.translate || showFullWord">{{ item.cn }}</div>
</div>
</div>
<div class="line-white my-2 mb-5 anim" v-opacity="settingStore.translate || showFullWord"></div>
</template>
<template v-if="word.phrases.length || word.synos.length || word.relWords.root || word.etymology.length">
<div class="line-white my-4"></div>
<div class="tabs">
<div @click="tab = 0" class="tab" :class="tab === 0 && 'active'">短语</div>
<div @click="tab = 1" class="tab" :class="tab === 1 && 'active'">同近义词</div>
<!-- <div @click="tab = 2" class="tab" :class="tab === 2 && 'active'">同根词</div>-->
<div @click="tab = 3" class="tab" :class="tab === 3 && 'active'">词源</div>
</div>
</template>
<template v-if="tab === 0">
<div class="my-2" v-for="item in word.phrases">
<SentenceHightLightWord class="text-lg" :text="item.c" :word="word.word"
:dictation="(settingStore.dictation && !showFullWord)"/>
<div class="text-md anim" v-opacity="settingStore.translate">{{ item.cn }}</div>
</div>
</template>
<template v-if="tab === 1">
<div class="flex my-2" v-for="item in word.synos">
<div class="text-lg w-12">{{ item.pos }}</div>
<div>
<div class="text-md">{{ item.cn }}</div>
<span class="text-md" v-for="(i,j) in item.ws">{{ i }} {{ j !== item.ws.length - 1 ? ' / ' : '' }} </span>
</div>
</div>
</template>
<template v-if="tab === 2">
<div class="mt-2">
<div v-if="word.relWords.root">
词根{{ word.relWords.root }}
</div>
<div class="flex my-2" v-for="item in word.relWords.rels">
<div class="text-lg w-12">{{ item.pos }}</div>
<div>
<div class="flex gap-4" v-for="itemj in item.words">
<div class="text-md">{{ itemj.c }}</div>
<div class="text-md">{{ itemj.cn }}</div>
<div class="anim" v-opacity="settingStore.translate || showFullWord">
<template v-if="word?.phrases?.length">
<div class="flex">
<div class="label">短语</div>
<div class="flex flex-col">
<div class="flex items-center gap-4" v-for="item in word.phrases">
<SentenceHightLightWord class="en" :text="item.c" :word="word.word"
:dictation="(settingStore.dictation && !showFullWord)"/>
<div class="cn anim" v-opacity="settingStore.translate">{{ item.cn }}</div>
</div>
</div>
</div>
</div>
</template>
<template v-if="tab === 3">
<div class="my-2" v-for="item in word.etymology">
<div class="text-lg">{{ item.t }}</div>
<div class="text-md">{{ item.d }}</div>
</div>
</template>
<div class="line-white mt-3 mb-2"></div>
</template>
<template v-if="word?.synos?.length">
<div class="flex">
<div class='label'>同近义词</div>
<div class="flex flex-col gap-3">
<div class="flex" v-for="item in word.synos">
<div class="pos line-height-1.4rem!">{{ item.pos }}</div>
<div>
<div class="cn">{{ item.cn }}</div>
<div>
<span class="en" v-for="(i,j) in item.ws">{{ i }} {{
j !== item.ws.length - 1 ? ' / ' : ''
}} </span>
</div>
</div>
</div>
</div>
</div>
<div class="line-white my-2"></div>
</template>
<template v-if="word?.etymology?.length">
<div class="flex">
<div class="label">词源</div>
<div class="text-base">
<div class="mb-2" v-for="item in word.etymology">
<div class="">{{ item.t }}</div>
<div class="">{{ item.d }}</div>
</div>
</div>
</div>
<!-- <div class="line-white my-2"></div>-->
</template>
<template v-if="word?.relWords?.root && false">
<div class="flex">
<div class="label">同根词</div>
<div class="flex flex-col gap-3">
<div v-if="word.relWords.root" class=" ">
词根<span class="en">{{ word.relWords.root }}</span>
</div>
<div class="flex" v-for="item in word.relWords.rels">
<div class="pos">{{ item.pos }}</div>
<div>
<div class="flex items-center gap-4" v-for="itemj in item.words">
<div class="en">{{ itemj.c }}</div>
<div class="cn">{{ itemj.cn }}</div>
</div>
</div>
</div>
</div>
</div>
</template>
</div>
</div>
<div class="cursor"
:style="{top:cursor.top+'px',left:cursor.left+'px',height: settingStore.fontSize.wordForeignFontSize +'px'}"></div>
@@ -447,5 +469,24 @@ function checkCursorPosition() {
}
}
.label {
width: 6rem;
padding-top: 0.2rem;
flex-shrink: 0;
}
.cn {
@apply text-base;
}
.en {
@apply text-lg;
}
.pos {
font-family: var(--en-article-family);
@apply text-lg w-12;
}
}
</style>