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:
@@ -30,8 +30,8 @@
|
||||
<br/>
|
||||
</p>
|
||||
|
||||

|
||||

|
||||
<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
1
components.d.ts
vendored
@@ -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']
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||

|
||||

|
||||
<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
|
||||
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
) : (
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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++
|
||||
}
|
||||
|
||||
|
||||
@@ -38,3 +38,6 @@ useWindowClick(() => show = false)
|
||||
</MiniDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
</style>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user