Merge branch 'master' into fix_theme_color2
This commit is contained in:
@@ -71,6 +71,7 @@
|
||||
--color-progress-bar: #d1d5df !important;
|
||||
|
||||
--color-label-bg: whitesmoke;
|
||||
--color-link: rgb(64, 158, 255)
|
||||
}
|
||||
|
||||
.footer {
|
||||
@@ -213,8 +214,7 @@ html, body {
|
||||
}
|
||||
|
||||
a {
|
||||
$main: rgb(64, 158, 255);
|
||||
color: $main;
|
||||
color: var(--color-link);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ interface IProps {
|
||||
disabled?: boolean
|
||||
loading?: boolean
|
||||
size?: 'small' | 'normal' | 'large',
|
||||
type?: 'primary' | 'link' | 'info'
|
||||
type?: 'primary' | 'link' | 'info' | 'orange'
|
||||
}
|
||||
|
||||
withDefaults(defineProps<IProps>(), {
|
||||
@@ -97,8 +97,8 @@ defineEmits(['click'])
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: .8;
|
||||
&:hover:not(.disabled) {
|
||||
opacity: .6;
|
||||
}
|
||||
|
||||
&.primary {
|
||||
@@ -120,6 +120,11 @@ defineEmits(['click'])
|
||||
color: var(--color-main-text);
|
||||
}
|
||||
|
||||
&.orange {
|
||||
background: #FACC15;
|
||||
color: black;
|
||||
}
|
||||
|
||||
&.active {
|
||||
opacity: .4;
|
||||
}
|
||||
|
||||
@@ -1,189 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import Close from "@/components/icon/Close.vue";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import {watch} from "vue";
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
|
||||
import {isMobile} from "@/utils";
|
||||
import {ProjectName, Host} from "@/config/env.ts";
|
||||
|
||||
let settingStore = useSettingStore()
|
||||
let showNotice = $ref(false)
|
||||
let show = $ref(false)
|
||||
let num = $ref(5)
|
||||
let timer = -1
|
||||
let mobile = $ref(isMobile())
|
||||
const isMac = /macintosh|mac os x/i.test(navigator.userAgent);
|
||||
|
||||
function toggleNotice() {
|
||||
showNotice = true
|
||||
settingStore.first = false
|
||||
timer = setInterval(() => {
|
||||
num--
|
||||
if (num <= 0) close()
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
function close() {
|
||||
clearInterval(timer)
|
||||
show = settingStore.first = false
|
||||
}
|
||||
|
||||
watch(() => settingStore.load, (n) => {
|
||||
if (n && settingStore.first) {
|
||||
setTimeout(() => {
|
||||
show = true
|
||||
}, 1000)
|
||||
}
|
||||
}, {immediate: true})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<transition name="right">
|
||||
<div class="CollectNotice"
|
||||
:class="{mobile}"
|
||||
v-if="show">
|
||||
<div class="notice">
|
||||
坚持练习,提高外语能力。将
|
||||
<span class="active">「{{ ProjectName }}」</span>
|
||||
保存为书签,永不迷失!
|
||||
</div>
|
||||
<div class="wrapper">
|
||||
<transition name="fade">
|
||||
<div class="collect" v-if="showNotice">
|
||||
<div class="href-wrapper">
|
||||
<div class="round">
|
||||
<div class="href">{{ Host }}</div>
|
||||
<IconFluentStar12Regular width="22"/>
|
||||
</div>
|
||||
<div class="right">
|
||||
👈
|
||||
<IconFluentStar20Filled class="star" width="22"/>
|
||||
点亮它!
|
||||
</div>
|
||||
</div>
|
||||
<div class="collect-keyboard" v-if="!mobile">或使用收藏快捷键<span
|
||||
class="active">{{ isMac ? 'Command' : 'Ctrl' }} + D</span></div>
|
||||
</div>
|
||||
<BaseButton v-else size="large" @click="toggleNotice">我想收藏</BaseButton>
|
||||
</transition>
|
||||
</div>
|
||||
<div class="close-wrapper">
|
||||
<span v-show="showNotice"><span class="active">{{ num }}s</span> 后自动关闭</span>
|
||||
<Close @click="close" title="关闭"/>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
.right-enter-active,
|
||||
.right-leave-active {
|
||||
transition: all .5s ease;
|
||||
}
|
||||
|
||||
.right-enter-from,
|
||||
.right-leave-to {
|
||||
transform: translateX(110%);
|
||||
}
|
||||
|
||||
.CollectNotice {
|
||||
position: fixed;
|
||||
right: var(--space);
|
||||
top: var(--space);
|
||||
z-index: 2;
|
||||
font-size: 1.2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background: var(--color-notice-bg);
|
||||
padding: 1.8rem;
|
||||
border-radius: 0.7rem;
|
||||
width: 30rem;
|
||||
gap: 2.4rem;
|
||||
color: var(--color-font-1);
|
||||
line-height: 1.5;
|
||||
border: 1px solid var(--color-item-border);
|
||||
box-shadow: var(--shadow);
|
||||
box-sizing: border-box;
|
||||
|
||||
&.mobile {
|
||||
width: 95%;
|
||||
padding: 0.6rem;
|
||||
}
|
||||
|
||||
.notice {
|
||||
margin-top: 2.4rem;
|
||||
}
|
||||
|
||||
.active {
|
||||
color: var(--color-select-bg);
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
.collect {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.href-wrapper {
|
||||
display: flex;
|
||||
font-size: 1rem;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
|
||||
.round {
|
||||
color: var(--color-font-1);
|
||||
border-radius: 3rem;
|
||||
padding: 0.6rem 0.6rem;
|
||||
padding-left: 1.2rem;
|
||||
gap: 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: var(--color-primary);
|
||||
|
||||
.href {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
.star {
|
||||
color: var(--color-select-bg);
|
||||
}
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.collect-keyboard {
|
||||
margin-top: 1.2rem;
|
||||
font-size: 1rem;
|
||||
|
||||
span {
|
||||
margin-left: 0.6rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.close-wrapper {
|
||||
right: var(--space);
|
||||
top: var(--space);
|
||||
position: absolute;
|
||||
font-size: 0.9rem;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
color: var(--color-font-1);
|
||||
gap: 0.6rem;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -8,14 +8,16 @@ interface IProps {
|
||||
strokeWidth?: number;
|
||||
color?: string;
|
||||
format?: (percentage: number) => string;
|
||||
size?: 'normal' | 'large';
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<IProps>(), {
|
||||
showText: true,
|
||||
textInside: false,
|
||||
strokeWidth: 6,
|
||||
color: '#93ADE3',
|
||||
color: '#409eff',
|
||||
format: (percentage) => `${percentage}%`,
|
||||
size: 'normal',
|
||||
});
|
||||
|
||||
const barStyle = computed(() => {
|
||||
@@ -26,13 +28,15 @@ const barStyle = computed(() => {
|
||||
});
|
||||
|
||||
const trackStyle = computed(() => {
|
||||
const height = props.size === 'large' ? props.strokeWidth * 2.5 : props.strokeWidth;
|
||||
return {
|
||||
height: `${props.strokeWidth}px`,
|
||||
height: `${height}px`,
|
||||
};
|
||||
});
|
||||
|
||||
const progressTextSize = computed(() => {
|
||||
return props.strokeWidth * 0.83 + 6;
|
||||
const baseSize = props.strokeWidth * 0.83 + 6;
|
||||
return props.size === 'large' ? baseSize * 1.2 : baseSize;
|
||||
});
|
||||
|
||||
const content = computed(() => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {Article, TaskWords, Word, WordPracticeMode} from "@/types/types.ts";
|
||||
import { Article, TaskWords, Word, WordPracticeMode } from "@/types/types.ts";
|
||||
import { useBaseStore } from "@/stores/base.ts";
|
||||
import { useSettingStore } from "@/stores/setting.ts";
|
||||
import { getDefaultWord } from "@/types/func.ts";
|
||||
@@ -87,7 +87,7 @@ export function useArticleOptions() {
|
||||
|
||||
export function getCurrentStudyWord(): TaskWords {
|
||||
const store = useBaseStore()
|
||||
let data = {new: [], review: [], write: []}
|
||||
let data = {new: [], review: [], write: [], shuffle: []}
|
||||
let dict = store.sdict;
|
||||
let isTest = false
|
||||
let words = dict.words.slice()
|
||||
|
||||
@@ -215,7 +215,7 @@ const {data: recommendBookList, isFetching} = useFetch(resourceWrap(DICT_LIST.AR
|
||||
</div>
|
||||
<div class="flex flex-col justify-between items-end">
|
||||
<div class="flex gap-4 items-center" v-opacity="base.sbook.id">
|
||||
<div class="color-blue cursor-pointer" @click="router.push('/book-list')">更换</div>
|
||||
<div class="color-link cursor-pointer" @click="router.push('/book-list')">更换</div>
|
||||
</div>
|
||||
<BaseButton size="large"
|
||||
@click="startStudy"
|
||||
@@ -238,10 +238,10 @@ const {data: recommendBookList, isFetching} = useFetch(resourceWrap(DICT_LIST.AR
|
||||
</BaseIcon>
|
||||
</PopConfirm>
|
||||
|
||||
<div class="color-blue cursor-pointer" v-if="base.article.bookList.length > 1"
|
||||
<div class="color-link cursor-pointer" v-if="base.article.bookList.length > 1"
|
||||
@click="isMultiple = !isMultiple; selectIds = []">{{ isMultiple ? '取消' : '管理书籍' }}
|
||||
</div>
|
||||
<div class="color-blue cursor-pointer" @click="nav('book-detail', { isAdd: true })">创建个人书籍</div>
|
||||
<div class="color-link cursor-pointer" @click="nav('book-detail', { isAdd: true })">创建个人书籍</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-4 flex-wrap mt-4">
|
||||
@@ -262,7 +262,7 @@ const {data: recommendBookList, isFetching} = useFetch(resourceWrap(DICT_LIST.AR
|
||||
<div class="flex justify-between">
|
||||
<div class="title">推荐</div>
|
||||
<div class="flex gap-4 items-center">
|
||||
<div class="color-blue cursor-pointer" @click="router.push('/book-list')">更多</div>
|
||||
<div class="color-link cursor-pointer" @click="router.push('/book-list')">更多</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -278,8 +278,7 @@ const {data: recommendBookList, isFetching} = useFetch(resourceWrap(DICT_LIST.AR
|
||||
|
||||
<style scoped lang="scss">
|
||||
.stat {
|
||||
@apply rounded-xl p-4 box-border relative flex-1;
|
||||
background: white;
|
||||
@apply rounded-xl p-4 box-border relative flex-1 bg-[var(--bg-history)];
|
||||
border: 1px solid gainsboro;
|
||||
|
||||
.num {
|
||||
|
||||
@@ -427,8 +427,8 @@ function onContextMenu(e: MouseEvent, sentence: Sentence, i, j, w) {
|
||||
label: "收藏单词",
|
||||
onClick: () => {
|
||||
let word = props.article.sections[i][j].words[w]
|
||||
let doc = nlp(word.word)
|
||||
let text = word.word
|
||||
let doc = nlp(text)
|
||||
// 优先判断是不是动词
|
||||
if (doc.verbs().found) {
|
||||
text = doc.verbs().toInfinitive().text()
|
||||
|
||||
@@ -1,239 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import {GITHUB, ProjectName} from "@/config/env.ts";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
import {defineAsyncComponent} from "vue";
|
||||
|
||||
const Dialog = defineAsyncComponent(() => import('@/components/dialog/Dialog.vue'))
|
||||
let showWechatDialog = $ref(false)
|
||||
let showXhsDialog = $ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col justify-between min-h-screen">
|
||||
<div class="center flex-col gap-8">
|
||||
<h1>{{ ProjectName }}</h1>
|
||||
<div class="text-center -mt-10">
|
||||
<h2>学习英语,一次敲击,一点进步</h2>
|
||||
<h2>记忆不再盲目,学习更高效,开源单词与文章练习工具</h2>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<BaseButton size="large" @click="$router.push('/words')">单词练习</BaseButton>
|
||||
<BaseButton size="large" @click="$router.push('/articles')">文章练习</BaseButton>
|
||||
</div>
|
||||
|
||||
<div class="center justify-center flex-col gap-2 w-full mb-4">
|
||||
<a href="https://skywork.ai/p/GrXQb4" class="w-60vw" target="_blank"><img src="/skywork-ai.png" alt="Skywork.AI" class="w-full rounded-lg"></a>
|
||||
<span>Skywork.AI:<a href="https://skywork.ai/p/GrXQb4" class="color-blue!" target="_blank">10 tasks in 1 hour, not 10 hours →Limited free spots: 127 left</a></span>
|
||||
</div>
|
||||
<div class="w-60vw">
|
||||
<div class="flex mb-5 gap-space">
|
||||
<div class="card">
|
||||
<div class="emoji">📚</div>
|
||||
<div class="title">单词练习</div>
|
||||
<div class="desc">
|
||||
<ul>
|
||||
<li>三种输入模式:跟打 / 复习 / 默写</li>
|
||||
<li>智能模式:智能规划复习与默写</li>
|
||||
<li>自由模式:不受限制,自行规划</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="emoji">✍️</div>
|
||||
<div class="title">文章练习</div>
|
||||
<div class="desc">
|
||||
<ul>
|
||||
<li>内置常见书籍,也可自行添加文章</li>
|
||||
<li>跟打 + 默写双模式,让背诵更高效</li>
|
||||
<li>支持边听边默写,强化记忆</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="emoji">📕</div>
|
||||
<div class="title">收藏、错词本、已掌握</div>
|
||||
<div class="desc">
|
||||
<ul>
|
||||
<li>输入错误自动添加到错词本</li>
|
||||
<li>主动添加到已掌握,后续自动跳过</li>
|
||||
<li>主动添加到收藏中,以便巩固复习</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="emoji">🌐</div>
|
||||
<div class="title">海量词库</div>
|
||||
<div class="desc">
|
||||
内置小学、初中、高中、四六级、考研、雅思、托福、GRE、GMAT、SAT、BEC、专四、专八等词库
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="flex gap-space">
|
||||
<div class="card">
|
||||
<div class="emoji">🆓</div>
|
||||
<div class="title">免费开源</div>
|
||||
<div class="desc">
|
||||
<ul>
|
||||
<li>完全开源,可审查、可修改</li>
|
||||
<li>免费使用</li>
|
||||
<li>私有部署</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="emoji">⚙️</div>
|
||||
<div class="title">高度自由</div>
|
||||
<div class="desc">
|
||||
<ul>
|
||||
<li>丰富的键盘音效</li>
|
||||
<li>可自定义快捷键</li>
|
||||
<li>高度定制化的设置选项</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="emoji">🎨</div>
|
||||
<div class="title">简洁高效</div>
|
||||
<div class="desc">
|
||||
<ul>
|
||||
<li>简洁设计,现代化UI,无广告</li>
|
||||
<li>界面清爽,操作简单</li>
|
||||
<li>不强制关注任何平台</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="emoji">🎯</div>
|
||||
<div class="title">个性学习</div>
|
||||
<div class="desc">
|
||||
<ul>
|
||||
<li>自由添加词典与文章</li>
|
||||
<li>定制个性学习计划</li>
|
||||
<li>多种学习复习策略</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-60vw text-center" v-if="false">
|
||||
<h3 class="text-4xl">单词练习</h3>
|
||||
<img src="/word.png" alt="word.png" class="w-full rounded-xl">
|
||||
<h3 class="text-4xl">文章练习</h3>
|
||||
<img src="/article.png" alt="article.png" class="w-full rounded-xl">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="center gap-space my-10 bottom">
|
||||
<div class="center gap-1">
|
||||
<a
|
||||
:href="GITHUB"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
aria-label="GITHUB 项目地址">
|
||||
<BaseIcon>
|
||||
<IconSimpleIconsGithub/>
|
||||
</BaseIcon>
|
||||
</a>
|
||||
|
||||
<BaseIcon @click="showWechatDialog = true">
|
||||
<IconSimpleIconsWechat/>
|
||||
</BaseIcon>
|
||||
<BaseIcon @click="showXhsDialog = true" >
|
||||
<IconSimpleIconsXiaohongshu/>
|
||||
</BaseIcon>
|
||||
<a
|
||||
href="https://x.com/typewords2"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
aria-label="关注我的 X 账户 typewords2">
|
||||
<BaseIcon>
|
||||
<IconRiTwitterFill/>
|
||||
</BaseIcon>
|
||||
</a>
|
||||
<a
|
||||
href="mailto:zyronon@163.com"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
aria-label="发送邮件到 zyronon@163.com">
|
||||
<BaseIcon>
|
||||
<IconMaterialSymbolsMail/>
|
||||
</BaseIcon>
|
||||
</a>
|
||||
</div>
|
||||
<div>蜀ICP备2025157466号</div>
|
||||
</div>
|
||||
|
||||
<Dialog v-model="showWechatDialog" title="Type Words 交流群">
|
||||
<div class="w-120 p-6 pt-0">
|
||||
<div class="mb-4">
|
||||
加入我们的用户社群后,您可以与我们的开发团队进行沟通,分享您的使用体验和建议,帮助我们改进产品,同时也能够及时了解我们的最新动态和更新内容。
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<img src="/wechat.png" alt="微信群二维码" class="w-60 rounded-lg">
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
<Dialog v-model="showXhsDialog" title="小红书">
|
||||
<div class="w-120 p-6 pt-0">
|
||||
<div class="mb-4">
|
||||
关注小红书后,您可以获得开发团队的最新动态和更新内容,反馈您的使用体验和建议,帮助我们改进产品,同时也能够及时了解我们的最新动态和更新内容。
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<img src="/xhs.png" alt="小红书二维码" class="w-60 rounded-lg">
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped lang="scss">
|
||||
h1 {
|
||||
font-size: 5rem;
|
||||
background: linear-gradient(120deg, #bd34fe 30%, #41d1ff);
|
||||
-webkit-text-fill-color: transparent;
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
margin: 2rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h3:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.card {
|
||||
@apply flex flex-col items-start gap-2 mb-0 w-25%;
|
||||
.emoji {
|
||||
display: inline-block;
|
||||
background: var(--color-third);
|
||||
padding: .6rem;
|
||||
border-radius: 0.4rem;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
padding-left: 1.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.bottom {
|
||||
width: 100%;
|
||||
padding-top: 2rem;
|
||||
border-top: 1px solid #c4c4c4;
|
||||
}
|
||||
|
||||
a {
|
||||
color: unset;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -8,13 +8,15 @@ import useTheme from "@/hooks/theme.ts";
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
import {useRuntimeStore} from "@/stores/runtime.ts";
|
||||
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const runtimeStore = useRuntimeStore()
|
||||
const router = useRouter()
|
||||
const {toggleTheme,getTheme} = useTheme()
|
||||
|
||||
const {toggleTheme, getTheme} = useTheme()
|
||||
|
||||
//首页为了seo被剥离出去了,现在是一个静态页面,用nginx 重定向控制对应的跳转
|
||||
function goHome() {
|
||||
window.location.href = '/';
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -24,7 +26,7 @@ const {toggleTheme,getTheme} = useTheme()
|
||||
<div class="aside anim fixed" :class="{'expand':settingStore.sideExpand}">
|
||||
<div class="top">
|
||||
<Logo v-if="settingStore.sideExpand"/>
|
||||
<div class="row" @click="router.push('/')">
|
||||
<div class="row" @click="goHome">
|
||||
<IconFluentHome20Regular/>
|
||||
<span v-if="settingStore.sideExpand">主页</span>
|
||||
</div>
|
||||
@@ -42,21 +44,21 @@ const {toggleTheme,getTheme} = useTheme()
|
||||
<span v-if="settingStore.sideExpand">设置</span>
|
||||
<div class="red-point" :class="!settingStore.sideExpand && 'top-1 right-0'" v-if="runtimeStore.isNew"></div>
|
||||
</div>
|
||||
<!-- <div class="row" @click="router.push('/user')">-->
|
||||
<!-- <IconFluentPerson20Regular/>-->
|
||||
<!-- <span v-if="settingStore.sideExpand">用户</span>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="row" @click="router.push('/user')">-->
|
||||
<!-- <IconFluentPerson20Regular/>-->
|
||||
<!-- <span v-if="settingStore.sideExpand">用户</span>-->
|
||||
<!-- </div>-->
|
||||
</div>
|
||||
<div class="bottom flex justify-evenly ">
|
||||
<BaseIcon
|
||||
@click="settingStore.sideExpand = !settingStore.sideExpand">
|
||||
@click="settingStore.sideExpand = !settingStore.sideExpand">
|
||||
<IconFluentChevronLeft20Filled v-if="settingStore.sideExpand"/>
|
||||
<IconFluentChevronLeft20Filled class="transform-rotate-180" v-else/>
|
||||
</BaseIcon>
|
||||
<BaseIcon
|
||||
v-if="settingStore.sideExpand"
|
||||
:title="`切换主题(${settingStore.shortcutKeyMap[ShortcutKey.ToggleTheme]})`"
|
||||
@click="toggleTheme"
|
||||
v-if="settingStore.sideExpand"
|
||||
:title="`切换主题(${settingStore.shortcutKeyMap[ShortcutKey.ToggleTheme]})`"
|
||||
@click="toggleTheme"
|
||||
>
|
||||
<IconFluentWeatherMoon16Regular v-if="getTheme() === 'light'"/>
|
||||
<IconFluentWeatherSunny16Regular v-else/>
|
||||
@@ -4,7 +4,7 @@ import { useSettingStore } from "@/stores/setting.ts";
|
||||
import { getAudioFileUrl, usePlayAudio } from "@/hooks/sound.ts";
|
||||
import { getShortcutKey, useEventListener } from "@/hooks/event.ts";
|
||||
import { checkAndUpgradeSaveDict, checkAndUpgradeSaveSetting, cloneDeep, loadJsLib, shakeCommonDict } from "@/utils";
|
||||
import {DefaultShortcutKeyMap, ShortcutKey, WordPracticeMode} from "@/types/types.ts";
|
||||
import { DefaultShortcutKeyMap, ShortcutKey, WordPracticeMode } from "@/types/types.ts";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import VolumeIcon from "@/components/icon/VolumeIcon.vue";
|
||||
import { useBaseStore } from "@/stores/base.ts";
|
||||
@@ -40,6 +40,7 @@ const tabIndex = $ref(0)
|
||||
const settingStore = useSettingStore()
|
||||
const runtimeStore = useRuntimeStore()
|
||||
const store = useBaseStore()
|
||||
|
||||
//@ts-ignore
|
||||
const gitLastCommitHash = ref(LATEST_COMMIT_HASH);
|
||||
const simpleWords = $computed({
|
||||
@@ -679,7 +680,23 @@ function importOldData() {
|
||||
</div>
|
||||
|
||||
<div v-if="tabIndex === 5">
|
||||
<div class="item p-2">
|
||||
<div class="log-item">
|
||||
<div class="mb-2">
|
||||
<div>
|
||||
<div>更新日期:2025/11/6</div>
|
||||
<div>更新内容:新增随机复习功能</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="log-item">
|
||||
<div class="mb-2">
|
||||
<div>
|
||||
<div>更新日期:2025/10/30</div>
|
||||
<div>更新内容:集成PWA基础配置,支持用户以类App形式打开项目</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="log-item">
|
||||
<div class="mb-2">
|
||||
<div>
|
||||
<div>更新日期:2025/10/26</div>
|
||||
@@ -730,9 +747,16 @@ function importOldData() {
|
||||
<div>通过引入「复习」与「默写」两种模式,使复习流程更加灵活、高效。</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="line"></div>
|
||||
</div>
|
||||
<div class="item p-2">
|
||||
<div class="log-item">
|
||||
<div class="mb-2">
|
||||
<div>
|
||||
<div>更新日期:2025/10/8</div>
|
||||
<div>更新内容:文章支持自动播放下一篇</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="log-item">
|
||||
<div class="mb-2">
|
||||
<div>
|
||||
<div>更新日期:2025/9/14</div>
|
||||
@@ -744,7 +768,6 @@ function importOldData() {
|
||||
<div>3、单词可导入、导出</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="line"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -775,6 +798,11 @@ function importOldData() {
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
.log-item{
|
||||
border-bottom: 1px solid var(--color-input-border);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.setting {
|
||||
@apply text-lg;
|
||||
display: flex;
|
||||
|
||||
@@ -237,7 +237,7 @@ async function startPractice() {
|
||||
wordPracticeMode: settingStore.wordPracticeMode
|
||||
})
|
||||
let currentStudy = getCurrentStudyWord()
|
||||
nav('practice-words/' + store.sdict.id, {}, currentStudy)
|
||||
nav('practice-words/' + store.sdict.id, {}, {taskWords:currentStudy})
|
||||
}
|
||||
|
||||
async function addMyStudyList() {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {onMounted, provide, ref, watch} from "vue";
|
||||
import {onMounted, provide, ref, toRef, watch} from "vue";
|
||||
|
||||
import Statistics from "@/pages/word/Statistics.vue";
|
||||
import {emitter, EventKey, useEvents} from "@/utils/eventBus.ts";
|
||||
@@ -50,6 +49,7 @@ let taskWords = $ref<TaskWords>({
|
||||
new: [],
|
||||
review: [],
|
||||
write: [],
|
||||
shuffle: [],
|
||||
})
|
||||
|
||||
let data = $ref<PracticeData>({
|
||||
@@ -60,10 +60,9 @@ let data = $ref<PracticeData>({
|
||||
})
|
||||
let isTypingWrongWord = ref(false)
|
||||
|
||||
let practiceMode = ref(WordPracticeType.FollowWrite)
|
||||
provide('isTypingWrongWord', isTypingWrongWord)
|
||||
provide('practiceData', data)
|
||||
provide('practiceMode', practiceMode)
|
||||
provide('practiceTaskWords', taskWords)
|
||||
|
||||
async function loadDict() {
|
||||
// console.log('load好了开始加载')
|
||||
@@ -100,7 +99,7 @@ watch(() => store.load, (n) => {
|
||||
onMounted(() => {
|
||||
//如果是从单词学习主页过来的,就直接使用;否则等待加载
|
||||
if (runtimeStore.routeData) {
|
||||
initData(runtimeStore.routeData, true)
|
||||
initData(runtimeStore.routeData.taskWords, true)
|
||||
} else {
|
||||
loading = true
|
||||
}
|
||||
@@ -124,27 +123,45 @@ function initData(initVal: TaskWords, init: boolean = false) {
|
||||
initData(initVal, true)
|
||||
}
|
||||
} else {
|
||||
taskWords = initVal
|
||||
if (taskWords.new.length === 0) {
|
||||
if (taskWords.review.length) {
|
||||
settingStore.wordPracticeType = WordPracticeType.Identify
|
||||
statStore.step = 3
|
||||
data.words = taskWords.review
|
||||
} else {
|
||||
if (taskWords.write.length) {
|
||||
// taskWords = initVal
|
||||
//不能直接赋值,会导致 inject 的数据为默认值
|
||||
taskWords = Object.assign(taskWords, initVal)
|
||||
//如果 shuffle 数组不为空,就说明是复习
|
||||
if (taskWords.shuffle.length === 0) {
|
||||
if (taskWords.new.length === 0) {
|
||||
if (taskWords.review.length) {
|
||||
settingStore.wordPracticeType = WordPracticeType.Identify
|
||||
data.words = taskWords.write
|
||||
statStore.step = 6
|
||||
statStore.step = 3
|
||||
data.words = taskWords.review
|
||||
} else {
|
||||
Toast.warning('没有可学习的单词!')
|
||||
router.push('/word')
|
||||
if (taskWords.write.length) {
|
||||
settingStore.wordPracticeType = WordPracticeType.Identify
|
||||
data.words = taskWords.write
|
||||
statStore.step = 6
|
||||
} else {
|
||||
Toast.warning('没有可学习的单词!')
|
||||
router.push('/word')
|
||||
}
|
||||
}
|
||||
} else {
|
||||
settingStore.wordPracticeType = WordPracticeType.FollowWrite
|
||||
data.words = taskWords.new
|
||||
statStore.step = 0
|
||||
}
|
||||
statStore.total = taskWords.review.length + taskWords.new.length + taskWords.write.length
|
||||
statStore.newWordNumber = taskWords.new.length
|
||||
statStore.reviewWordNumber = taskWords.review.length
|
||||
statStore.writeWordNumber = taskWords.write.length
|
||||
} else {
|
||||
settingStore.wordPracticeType = WordPracticeType.FollowWrite
|
||||
data.words = taskWords.new
|
||||
statStore.step = 0
|
||||
settingStore.wordPracticeType = WordPracticeType.Dictation
|
||||
data.words = taskWords.shuffle
|
||||
statStore.step = 10
|
||||
statStore.total = taskWords.shuffle.length
|
||||
statStore.newWordNumber = 0
|
||||
statStore.reviewWordNumber = 0
|
||||
statStore.writeWordNumber = statStore.total
|
||||
}
|
||||
|
||||
data.index = 0
|
||||
data.wrongWords = []
|
||||
data.excludeWords = []
|
||||
@@ -152,11 +169,6 @@ function initData(initVal: TaskWords, init: boolean = false) {
|
||||
statStore.startDate = Date.now()
|
||||
statStore.inputWordNumber = 0
|
||||
statStore.wrong = 0
|
||||
statStore.total = taskWords.review.length + taskWords.new.length + taskWords.write.length
|
||||
statStore.newWordNumber = taskWords.new.length
|
||||
statStore.reviewWordNumber = taskWords.review.length
|
||||
statStore.writeWordNumber = taskWords.write.length
|
||||
statStore.index = 0
|
||||
isTypingWrongWord.value = false
|
||||
}
|
||||
}
|
||||
@@ -210,6 +222,8 @@ function wordLoop() {
|
||||
}
|
||||
}
|
||||
|
||||
let toastInstance: ToastInstance = null
|
||||
|
||||
function goNextStep(originList, mode, msg) {
|
||||
//每次都判断,因为每次都可能新增已掌握的单词
|
||||
let list = originList.filter(v => (!data.excludeWords.includes(v.word)))
|
||||
@@ -228,8 +242,6 @@ function goNextStep(originList, mode, msg) {
|
||||
}
|
||||
}
|
||||
|
||||
let toastInstance: ToastInstance = null
|
||||
|
||||
async function next(isTyping: boolean = true) {
|
||||
if (isTyping) {
|
||||
statStore.inputWordNumber++
|
||||
@@ -244,7 +256,7 @@ async function next(isTyping: boolean = true) {
|
||||
data.words = shuffle(cloneDeep(data.wrongWords))
|
||||
data.index = 0
|
||||
data.wrongWords = []
|
||||
}else {
|
||||
} else {
|
||||
console.log('自由模式,全完学完了')
|
||||
showStatDialog = true
|
||||
localStorage.removeItem(PracticeSaveWordKey.key)
|
||||
@@ -388,7 +400,7 @@ function onKeyUp(e: KeyboardEvent) {
|
||||
typingRef.hideWord()
|
||||
}
|
||||
|
||||
async function onKeyDown(e: KeyboardEvent) {
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
// console.log('onKeyDown', e)
|
||||
switch (e.key) {
|
||||
case 'Backspace':
|
||||
@@ -401,21 +413,27 @@ useOnKeyboardEventListener(onKeyDown, onKeyUp)
|
||||
|
||||
function repeat() {
|
||||
console.log('重学一遍')
|
||||
if (settingStore.wordPracticeMode === WordPracticeMode.System) settingStore.dictation = false
|
||||
if (store.sdict.lastLearnIndex === 0 && store.sdict.complete) {
|
||||
//如果是刚刚完成,那么学习进度要从length减回去,因为lastLearnIndex为0了,同时改complete为false
|
||||
store.sdict.lastLearnIndex = store.sdict.length - statStore.newWordNumber
|
||||
store.sdict.complete = false
|
||||
let temp = cloneDeep(taskWords)
|
||||
let ignoreList = [store.allIgnoreWords, store.knownWords][settingStore.ignoreSimpleWord ? 0 : 1]
|
||||
//随机练习单独处理
|
||||
if (taskWords.shuffle.length) {
|
||||
temp.shuffle = shuffle(temp.shuffle.filter(v => !ignoreList.includes(v.word)))
|
||||
} else {
|
||||
//将学习进度减回去
|
||||
store.sdict.lastLearnIndex = store.sdict.lastLearnIndex - statStore.newWordNumber
|
||||
if (settingStore.wordPracticeMode === WordPracticeMode.System) settingStore.dictation = false
|
||||
if (store.sdict.lastLearnIndex === 0 && store.sdict.complete) {
|
||||
//如果是刚刚完成,那么学习进度要从length减回去,因为lastLearnIndex为0了,同时改complete为false
|
||||
store.sdict.lastLearnIndex = store.sdict.length - statStore.newWordNumber
|
||||
store.sdict.complete = false
|
||||
} else {
|
||||
//将学习进度减回去
|
||||
store.sdict.lastLearnIndex = store.sdict.lastLearnIndex - statStore.newWordNumber
|
||||
}
|
||||
//排除已掌握单词
|
||||
temp.new = temp.new.filter(v => !ignoreList.includes(v.word))
|
||||
temp.review = temp.review.filter(v => !ignoreList.includes(v.word))
|
||||
temp.write = temp.write.filter(v => !ignoreList.includes(v.word))
|
||||
}
|
||||
emitter.emit(EventKey.resetWord)
|
||||
let temp = cloneDeep(taskWords)
|
||||
//排除已掌握单词
|
||||
temp.new = temp.new.filter(v => !store.knownWords.includes(v.word))
|
||||
temp.review = temp.review.filter(v => !store.knownWords.includes(v.word))
|
||||
temp.write = temp.write.filter(v => !store.knownWords.includes(v.word))
|
||||
initData(temp)
|
||||
}
|
||||
|
||||
@@ -477,16 +495,26 @@ function togglePanel() {
|
||||
}
|
||||
|
||||
function continueStudy() {
|
||||
if (settingStore.wordPracticeMode === WordPracticeMode.System) settingStore.dictation = false
|
||||
//这里判断是否显示结算弹框,如果显示了结算弹框的话,就不用加进度了
|
||||
if (!showStatDialog) {
|
||||
console.log('没学完,强行跳过')
|
||||
store.sdict.lastLearnIndex = store.sdict.lastLearnIndex + statStore.newWordNumber
|
||||
let temp = cloneDeep(taskWords)
|
||||
//随机练习单独处理
|
||||
if (taskWords.shuffle.length) {
|
||||
let ignoreList = [store.allIgnoreWords, store.knownWords][settingStore.ignoreSimpleWord ? 0 : 1]
|
||||
temp.shuffle = shuffle(store.sdict.words.filter(v => !ignoreList.includes(v.word))).slice(0, runtimeStore.routeData.total)
|
||||
if (showStatDialog) showStatDialog = false
|
||||
} else {
|
||||
console.log('学完了,正常下一组')
|
||||
showStatDialog = false
|
||||
if (settingStore.wordPracticeMode === WordPracticeMode.System) settingStore.dictation = false
|
||||
//这里判断是否显示结算弹框,如果显示了结算弹框的话,就不用加进度了
|
||||
if (!showStatDialog) {
|
||||
console.log('没学完,强行跳过')
|
||||
store.sdict.lastLearnIndex = store.sdict.lastLearnIndex + statStore.newWordNumber
|
||||
} else {
|
||||
console.log('学完了,正常下一组')
|
||||
showStatDialog = false
|
||||
}
|
||||
temp = getCurrentStudyWord()
|
||||
}
|
||||
initData(getCurrentStudyWord())
|
||||
emitter.emit(EventKey.resetWord)
|
||||
initData(temp)
|
||||
}
|
||||
|
||||
function randomWrite() {
|
||||
@@ -533,8 +561,8 @@ useEvents([
|
||||
|
||||
<template>
|
||||
<PracticeLayout
|
||||
v-loading="loading"
|
||||
panelLeft="var(--word-panel-margin-left)">
|
||||
v-loading="loading"
|
||||
panelLeft="var(--word-panel-margin-left)">
|
||||
<template v-slot:practice>
|
||||
<div class="practice-word">
|
||||
<div class="absolute z-1 top-4 w-full" v-if="settingStore.showNearWord">
|
||||
@@ -543,16 +571,16 @@ useEvents([
|
||||
v-if="prevWord">
|
||||
<IconFluentArrowLeft16Regular class="arrow" width="22"/>
|
||||
<Tooltip
|
||||
:title="`上一个(${settingStore.shortcutKeyMap[ShortcutKey.Previous]})`"
|
||||
:title="`上一个(${settingStore.shortcutKeyMap[ShortcutKey.Previous]})`"
|
||||
>
|
||||
<div class="word">{{ prevWord.word }}</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div class="center gap-2 cursor-pointer float-right "
|
||||
<div class="center gap-2 cursor-pointer float-right mr-3"
|
||||
@click="next(false)"
|
||||
v-if="nextWord">
|
||||
<Tooltip
|
||||
:title="`下一个(${settingStore.shortcutKeyMap[ShortcutKey.Next]})`"
|
||||
:title="`下一个(${settingStore.shortcutKeyMap[ShortcutKey.Next]})`"
|
||||
>
|
||||
<div class="word" :class="settingStore.dictation && 'word-shadow'">{{ nextWord.word }}</div>
|
||||
</Tooltip>
|
||||
@@ -560,11 +588,11 @@ useEvents([
|
||||
</div>
|
||||
</div>
|
||||
<TypeWord
|
||||
ref="typingRef"
|
||||
:word="word"
|
||||
@wrong="onTypeWrong"
|
||||
@complete="next"
|
||||
@know="onWordKnow"
|
||||
ref="typingRef"
|
||||
:word="word"
|
||||
@wrong="onTypeWrong"
|
||||
@complete="next"
|
||||
@know="onWordKnow"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -576,41 +604,41 @@ useEvents([
|
||||
<span>{{ store.sdict.name }} ({{ store.sdict.lastLearnIndex }} / {{ store.sdict.length }})</span>
|
||||
|
||||
<BaseIcon
|
||||
@click="continueStudy"
|
||||
:title="`下一组(${settingStore.shortcutKeyMap[ShortcutKey.NextChapter]})`">
|
||||
@click="continueStudy"
|
||||
:title="`下一组(${settingStore.shortcutKeyMap[ShortcutKey.NextChapter]})`">
|
||||
<IconFluentArrowRight16Regular class="arrow" width="22"/>
|
||||
</BaseIcon>
|
||||
<BaseIcon
|
||||
@click="randomWrite"
|
||||
:title="`随机默写(${settingStore.shortcutKeyMap[ShortcutKey.RandomWrite]})`">
|
||||
@click="randomWrite"
|
||||
:title="`随机默写(${settingStore.shortcutKeyMap[ShortcutKey.RandomWrite]})`">
|
||||
<IconFluentArrowShuffle16Regular class="arrow" width="22"/>
|
||||
</BaseIcon>
|
||||
</div>
|
||||
</template>
|
||||
<div class="panel-page-item pl-4">
|
||||
<WordList
|
||||
v-if="data.words.length"
|
||||
:is-active="settingStore.showPanel"
|
||||
:static="false"
|
||||
:show-word="!settingStore.dictation"
|
||||
:show-translate="settingStore.translate"
|
||||
:list="data.words"
|
||||
:activeIndex="data.index"
|
||||
@click="(val:any) => data.index = val.index"
|
||||
v-if="data.words.length"
|
||||
:is-active="settingStore.showPanel"
|
||||
:static="false"
|
||||
:show-word="!settingStore.dictation"
|
||||
:show-translate="settingStore.translate"
|
||||
:list="data.words"
|
||||
:activeIndex="data.index"
|
||||
@click="(val:any) => data.index = val.index"
|
||||
>
|
||||
<template v-slot:suffix="{item,index}">
|
||||
<BaseIcon
|
||||
:class="!isWordCollect(item)?'collect':'fill'"
|
||||
@click.stop="toggleWordCollect(item)"
|
||||
:title="!isWordCollect(item) ? '收藏' : '取消收藏'">
|
||||
:class="!isWordCollect(item)?'collect':'fill'"
|
||||
@click.stop="toggleWordCollect(item)"
|
||||
:title="!isWordCollect(item) ? '收藏' : '取消收藏'">
|
||||
<IconFluentStar16Regular v-if="!isWordCollect(item)"/>
|
||||
<IconFluentStar16Filled v-else/>
|
||||
</BaseIcon>
|
||||
|
||||
<BaseIcon
|
||||
:class="!isWordSimple(item)?'collect':'fill'"
|
||||
@click.stop="toggleWordSimple(item)"
|
||||
:title="!isWordSimple(item) ? '标记为已掌握' : '取消标记已掌握'">
|
||||
:class="!isWordSimple(item)?'collect':'fill'"
|
||||
@click.stop="toggleWordSimple(item)"
|
||||
:title="!isWordSimple(item) ? '标记为已掌握' : '取消标记已掌握'">
|
||||
<IconFluentCheckmarkCircle16Regular v-if="!isWordSimple(item)"/>
|
||||
<IconFluentCheckmarkCircle16Filled v-else/>
|
||||
</BaseIcon>
|
||||
@@ -622,11 +650,11 @@ useEvents([
|
||||
</template>
|
||||
<template v-slot:footer>
|
||||
<Footer
|
||||
:is-simple="isWordSimple(word)"
|
||||
@toggle-simple="toggleWordSimpleWrapper"
|
||||
:is-collect="isWordCollect(word)"
|
||||
@toggle-collect="toggleWordCollect(word)"
|
||||
@skip="next(false)"
|
||||
:is-simple="isWordSimple(word)"
|
||||
@toggle-simple="toggleWordSimpleWrapper"
|
||||
:is-collect="isWordCollect(word)"
|
||||
@toggle-collect="toggleWordCollect(word)"
|
||||
@skip="next(false)"
|
||||
/>
|
||||
</template>
|
||||
</PracticeLayout>
|
||||
|
||||
@@ -1,26 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import {useBaseStore} from "@/stores/base.ts";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import {ShortcutKey, Statistics} from "@/types/types.ts";
|
||||
import {PracticeData, ShortcutKey, Statistics, TaskWords, WordPracticeMode} from "@/types/types.ts";
|
||||
import {emitter, EventKey, useEvents} from "@/utils/eventBus.ts";
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
import {usePracticeStore} from "@/stores/practice.ts";
|
||||
import dayjs from "dayjs";
|
||||
import isBetween from "dayjs/plugin/isBetween";
|
||||
import {defineAsyncComponent, watch} from "vue";
|
||||
import {defineAsyncComponent, inject, watch} from "vue";
|
||||
import isoWeek from 'dayjs/plugin/isoWeek'
|
||||
import {msToHourMinute, msToMinute} from "@/utils";
|
||||
|
||||
dayjs.extend(isoWeek)
|
||||
dayjs.extend(isBetween);
|
||||
const Dialog = defineAsyncComponent(() => import('@/components/dialog/Dialog.vue'))
|
||||
|
||||
|
||||
const store = useBaseStore()
|
||||
const settingStore = useSettingStore()
|
||||
const statStore = usePracticeStore()
|
||||
const model = defineModel({default: false})
|
||||
let list = $ref([])
|
||||
let dictIsEnd = $ref(false)
|
||||
let practiceTaskWords = inject<TaskWords>('practiceTaskWords')
|
||||
|
||||
function calcWeekList() {
|
||||
// 获取本周的起止时间
|
||||
@@ -68,12 +69,16 @@ watch(model, (newVal) => {
|
||||
complete: store.sdict.complete,
|
||||
str: `name:${store.sdict.name},per:${store.sdict.perDayStudyNumber},spend:${Number(statStore.spend / 1000 / 60).toFixed(1)},index:${store.sdict.lastLearnIndex}`
|
||||
})
|
||||
store.sdict.lastLearnIndex = store.sdict.lastLearnIndex + statStore.newWordNumber
|
||||
if (store.sdict.lastLearnIndex >= store.sdict.length) {
|
||||
dictIsEnd = true;
|
||||
store.sdict.complete = true
|
||||
store.sdict.lastLearnIndex = 0
|
||||
//如果 shuffle 数组不为空,就说明是复习,不用修改 lastLearnIndex
|
||||
if (!practiceTaskWords.shuffle.length) {
|
||||
store.sdict.lastLearnIndex = store.sdict.lastLearnIndex + statStore.newWordNumber
|
||||
if (store.sdict.lastLearnIndex >= store.sdict.length) {
|
||||
dictIsEnd = true;
|
||||
store.sdict.complete = true
|
||||
store.sdict.lastLearnIndex = 0
|
||||
}
|
||||
}
|
||||
|
||||
store.sdict.statistics.push(data as any)
|
||||
calcWeekList(); // 新增:计算本周学习记录
|
||||
}
|
||||
@@ -97,33 +102,41 @@ function options(emitType: string) {
|
||||
|
||||
<template>
|
||||
<Dialog
|
||||
:close-on-click-bg="false"
|
||||
:header="false"
|
||||
:keyboard="false"
|
||||
:show-close="false"
|
||||
v-model="model">
|
||||
:close-on-click-bg="false"
|
||||
:header="false"
|
||||
:keyboard="false"
|
||||
:show-close="false"
|
||||
v-model="model">
|
||||
<div class="w-140 bg-white color-black p-6 relative flex flex-col gap-6">
|
||||
<div class="w-full flex flex-col justify-evenly">
|
||||
<div class="center text-2xl mb-2">已完成今日任务</div>
|
||||
<div class="center text-2xl mb-2">已完成{{ practiceTaskWords.shuffle.length ? '随机复习' : '今日任务' }}</div>
|
||||
<div class="flex">
|
||||
<div class="flex-1 flex flex-col items-center">
|
||||
<div class="text-sm color-gray">新词数</div>
|
||||
<div class="text-4xl font-bold">{{ statStore.newWordNumber }}</div>
|
||||
</div>
|
||||
<div class="flex-1 flex flex-col items-center">
|
||||
<div class="text-sm color-gray">复习上次</div>
|
||||
<div class="text-4xl font-bold">{{ statStore.reviewWordNumber }}</div>
|
||||
</div>
|
||||
<div class="flex-1 flex flex-col items-center">
|
||||
<div class="text-sm color-gray">复习之前</div>
|
||||
<div class="text-4xl font-bold">{{ statStore.writeWordNumber }}</div>
|
||||
<div v-if="practiceTaskWords.shuffle.length"
|
||||
class="flex-1 flex flex-col items-center">
|
||||
<div class="text-sm color-gray">随机复习</div>
|
||||
<div class="text-4xl font-bold">{{ practiceTaskWords.shuffle.length }}</div>
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="flex-1 flex flex-col items-center">
|
||||
<div class="text-sm color-gray">新词数</div>
|
||||
<div class="text-4xl font-bold">{{ statStore.newWordNumber }}</div>
|
||||
</div>
|
||||
<template v-if="settingStore.wordPracticeMode !== WordPracticeMode.Free">
|
||||
<div class="flex-1 flex flex-col items-center">
|
||||
<div class="text-sm color-gray">复习上次</div>
|
||||
<div class="text-4xl font-bold">{{ statStore.reviewWordNumber }}</div>
|
||||
</div>
|
||||
<div class="flex-1 flex flex-col items-center">
|
||||
<div class="text-sm color-gray">复习之前</div>
|
||||
<div class="text-4xl font-bold">{{ statStore.writeWordNumber }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-xl text-center flex flex-col justify-around">
|
||||
<div>非常棒! 坚持了 <span class="color-emerald-500 font-bold text-2xl">
|
||||
{{ dayjs().diff(statStore.startDate, 'm') }}</span>分钟
|
||||
<div>非常棒! 坚持了 <span class="color-emerald-500 font-bold text-2xl">{{msToHourMinute(statStore.spend) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-center gap-10">
|
||||
@@ -149,29 +162,29 @@ function options(emitType: string) {
|
||||
<div class="title text-align-center mb-2">本周学习记录</div>
|
||||
<div class="flex gap-4 color-gray">
|
||||
<div
|
||||
class="w-8 h-8 rounded-md center"
|
||||
:class="item ? 'bg-emerald-500 color-white' : 'bg-gray-200'"
|
||||
v-for="(item, i) in list"
|
||||
:key="i"
|
||||
class="w-8 h-8 rounded-md center"
|
||||
:class="item ? 'bg-emerald-500 color-white' : 'bg-gray-200'"
|
||||
v-for="(item, i) in list"
|
||||
:key="i"
|
||||
>{{ i + 1 }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-center gap-4 ">
|
||||
<BaseButton
|
||||
:keyboard="settingStore.shortcutKeyMap[ShortcutKey.RepeatChapter]"
|
||||
@click="options(EventKey.repeatStudy)">
|
||||
:keyboard="settingStore.shortcutKeyMap[ShortcutKey.RepeatChapter]"
|
||||
@click="options(EventKey.repeatStudy)">
|
||||
重学一遍
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
:keyboard="settingStore.shortcutKeyMap[ShortcutKey.NextChapter]"
|
||||
@click="options(EventKey.continueStudy)">
|
||||
{{ dictIsEnd ? '重新练习' : '再来一组' }}
|
||||
:keyboard="settingStore.shortcutKeyMap[ShortcutKey.NextChapter]"
|
||||
@click="options(EventKey.continueStudy)">
|
||||
{{ dictIsEnd ? '从头开始练习' : '再来一组' }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
:keyboard="settingStore.shortcutKeyMap[ShortcutKey.NextRandomWrite]"
|
||||
@click="options(EventKey.randomWrite)">
|
||||
继续默写
|
||||
:keyboard="settingStore.shortcutKeyMap[ShortcutKey.NextRandomWrite]"
|
||||
@click="options(EventKey.randomWrite)">
|
||||
继续默写
|
||||
</BaseButton>
|
||||
<BaseButton @click="$router.back">
|
||||
返回主页
|
||||
@@ -182,7 +195,4 @@ function options(emitType: string) {
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</template>
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
</template>
|
||||
@@ -1,28 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import { useBaseStore } from "@/stores/base.ts";
|
||||
import { useRouter } from "vue-router";
|
||||
import {useBaseStore} from "@/stores/base.ts";
|
||||
import {useRouter} from "vue-router";
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
import { _getAccomplishDate, _getDictDataByUrl, resourceWrap, useNav } from "@/utils";
|
||||
import {_getAccomplishDate, _getDictDataByUrl, resourceWrap, shuffle, useNav} from "@/utils";
|
||||
import BasePage from "@/components/BasePage.vue";
|
||||
import {DictResource, WordPracticeMode} from "@/types/types.ts";
|
||||
import { watch } from "vue";
|
||||
import { getCurrentStudyWord } from "@/hooks/dict.ts";
|
||||
import { useRuntimeStore } from "@/stores/runtime.ts";
|
||||
import {watch} from "vue";
|
||||
import {getCurrentStudyWord} from "@/hooks/dict.ts";
|
||||
import {useRuntimeStore} from "@/stores/runtime.ts";
|
||||
import Book from "@/components/Book.vue";
|
||||
import PopConfirm from "@/components/PopConfirm.vue";
|
||||
import Progress from '@/components/base/Progress.vue';
|
||||
import Toast from '@/components/base/toast/Toast.ts';
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import { getDefaultDict } from "@/types/func.ts";
|
||||
import {getDefaultDict} from "@/types/func.ts";
|
||||
import DeleteIcon from "@/components/icon/DeleteIcon.vue";
|
||||
import PracticeSettingDialog from "@/pages/word/components/PracticeSettingDialog.vue";
|
||||
import ChangeLastPracticeIndexDialog from "@/pages/word/components/ChangeLastPracticeIndexDialog.vue";
|
||||
import { useSettingStore } from "@/stores/setting.ts";
|
||||
import CollectNotice from "@/components/CollectNotice.vue";
|
||||
import { useFetch } from "@vueuse/core";
|
||||
import { CAN_REQUEST, DICT_LIST, PracticeSaveWordKey } from "@/config/env.ts";
|
||||
import { myDictList } from "@/apis";
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
import {useFetch} from "@vueuse/core";
|
||||
import {CAN_REQUEST, DICT_LIST, PracticeSaveWordKey} from "@/config/env.ts";
|
||||
import {myDictList} from "@/apis";
|
||||
import PracticeWordListDialog from "@/pages/word/components/PracticeWordListDialog.vue";
|
||||
import ShufflePracticeSettingDialog from "@/pages/word/components/ShufflePracticeSettingDialog.vue";
|
||||
|
||||
|
||||
const store = useBaseStore()
|
||||
@@ -35,7 +35,8 @@ let isSaveData = $ref(false)
|
||||
let currentStudy = $ref({
|
||||
new: [],
|
||||
review: [],
|
||||
write: []
|
||||
write: [],
|
||||
shuffle: [],
|
||||
})
|
||||
|
||||
watch(() => store.load, n => {
|
||||
@@ -85,7 +86,7 @@ function startPractice() {
|
||||
complete: store.sdict.complete,
|
||||
wordPracticeMode: settingStore.wordPracticeMode
|
||||
})
|
||||
nav('practice-words/' + store.sdict.id, {}, currentStudy)
|
||||
nav('practice-words/' + store.sdict.id, {}, {taskWords: currentStudy})
|
||||
} else {
|
||||
window.umami?.track('no-dict')
|
||||
Toast.warning('请先选择一本词典')
|
||||
@@ -93,15 +94,17 @@ function startPractice() {
|
||||
}
|
||||
|
||||
let showPracticeSettingDialog = $ref(false)
|
||||
let showShufflePracticeSettingDialog = $ref(false)
|
||||
let showChangeLastPracticeIndexDialog = $ref(false)
|
||||
let showPracticeWordListDialog = $ref(false)
|
||||
|
||||
async function goDictDetail(val: DictResource) {
|
||||
if (!val.id) return nav('dict-list')
|
||||
runtimeStore.editDict = getDefaultDict(val)
|
||||
nav('dict-detail', {})
|
||||
}
|
||||
|
||||
let isMultiple = $ref(false)
|
||||
let isManageDict = $ref(false)
|
||||
let selectIds = $ref([])
|
||||
|
||||
function handleBatchDel() {
|
||||
@@ -156,6 +159,26 @@ async function savePracticeSetting() {
|
||||
currentStudy = getCurrentStudyWord()
|
||||
}
|
||||
|
||||
async function onShufflePracticeSettingOk(total) {
|
||||
window.umami?.track('startShuffleStudyWord', {
|
||||
name: store.sdict.name,
|
||||
index: store.sdict.lastLearnIndex,
|
||||
perDayStudyNumber: store.sdict.perDayStudyNumber,
|
||||
total,
|
||||
custom: store.sdict.custom,
|
||||
complete: store.sdict.complete,
|
||||
})
|
||||
isSaveData = false
|
||||
localStorage.removeItem(PracticeSaveWordKey.key)
|
||||
|
||||
let ignoreList = [store.allIgnoreWords, store.knownWords][settingStore.ignoreSimpleWord ? 0 : 1]
|
||||
currentStudy.shuffle = shuffle(store.sdict.words.slice(0, store.sdict.lastLearnIndex).filter(v => !ignoreList.includes(v.word))).slice(0, total)
|
||||
nav('practice-words/' + store.sdict.id, {}, {
|
||||
taskWords: currentStudy,
|
||||
total //用于再来一组时,随机出正确的长度,因为练习中可能会点击已掌握,导致重学一遍之后长度变少,如果再来一组,此时长度就不正确
|
||||
})
|
||||
}
|
||||
|
||||
async function saveLastPracticeIndex(e) {
|
||||
Toast.success('修改成功')
|
||||
runtimeStore.editDict.lastLearnIndex = e
|
||||
@@ -171,93 +194,127 @@ const {
|
||||
isFetching
|
||||
} = useFetch(resourceWrap(DICT_LIST.WORD.RECOMMENDED)).json()
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasePage>
|
||||
<div class="card flex gap-10">
|
||||
<div class="flex-1 flex flex-col gap-2">
|
||||
<div class="flex">
|
||||
<div class="bg-third px-3 h-14 rounded-md flex items-center ">
|
||||
<span @click="goDictDetail(store.sdict)"
|
||||
class="text-lg font-bold cursor-pointer">{{ store.sdict.name || '请选择词典开始学习' }}</span>
|
||||
<BaseIcon title="切换词典"
|
||||
class="ml-4"
|
||||
@click="router.push('/dict-list')"
|
||||
<div class="card flex gap-8">
|
||||
<div class="flex-1 flex flex-col justify-between">
|
||||
<div class="flex gap-3">
|
||||
<div class="p-1 center rounded-full bg-white">
|
||||
<IconFluentBookNumber20Filled class="text-xl color-link"/>
|
||||
</div>
|
||||
<div
|
||||
@click="goDictDetail(store.sdict)"
|
||||
class="text-2xl font-bold cursor-pointer">
|
||||
{{ store.sdict.name || '请选择词典开始学习' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 flex flex-col gap-2">
|
||||
<div class="">当前进度:{{ progressTextLeft }}</div>
|
||||
<Progress size="large" :percentage="store.currentStudyProgress" :show-text="false"></Progress>
|
||||
<div class="text-sm flex justify-between">
|
||||
<span>已完成 {{ progressTextRight }} 词 / 共 {{ store.sdict.words.length }} 词</span>
|
||||
<span v-if="store.sdict.id">
|
||||
预计完成日期:{{ _getAccomplishDate(store.sdict.words.length, store.sdict.perDayStudyNumber) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex mt-4 gap-4">
|
||||
<BaseButton type="info" @click="router.push('/dict-list')">
|
||||
<div class="center gap-1">
|
||||
<IconFluentArrowSwap20Regular/>
|
||||
<span>{{ store.sdict.name ? '切换' : '选择' }}词典</span>
|
||||
</div>
|
||||
</BaseButton>
|
||||
<PopConfirm
|
||||
:disabled="!isSaveData"
|
||||
title="当前存在未完成的学习任务,修改会重新生成学习任务,是否继续?"
|
||||
@confirm="check(()=>showChangeLastPracticeIndexDialog = true)">
|
||||
<BaseButton type="info"
|
||||
v-if="store.sdict.id"
|
||||
>
|
||||
<IconFluentArrowSort20Regular v-if="store.sdict.name"/>
|
||||
<IconFluentAdd20Filled v-else/>
|
||||
</BaseIcon>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-end gap-space">
|
||||
<div class="flex-1">
|
||||
<div class="text-sm flex justify-between">
|
||||
<span>{{ progressTextLeft }}</span>
|
||||
<span>{{ progressTextRight }} / {{ store.sdict.words.length }}</span>
|
||||
</div>
|
||||
<Progress class="mt-1" :percentage="store.currentStudyProgress" :show-text="false"></Progress>
|
||||
</div>
|
||||
<PopConfirm
|
||||
:disabled="!isSaveData"
|
||||
title="当前存在未完成的学习任务,修改会重新生成学习任务,是否继续?"
|
||||
@confirm="check(()=>showChangeLastPracticeIndexDialog = true)">
|
||||
<div class="color-blue cursor-pointer">更改</div>
|
||||
</PopConfirm>
|
||||
|
||||
</div>
|
||||
<div class="text-sm text-align-end">
|
||||
预计完成日期:{{ _getAccomplishDate(store.sdict.words.length, store.sdict.perDayStudyNumber) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-3/10 flex flex-col justify-evenly">
|
||||
<div class="center gap-2">
|
||||
<span class="text-xl">{{ isSaveData ? '上次学习任务' : '今日任务' }}</span>
|
||||
<span class="color-blue cursor-pointer" @click="showPracticeWordListDialog = true">词表</span>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<div class="flex-1 flex flex-col items-center">
|
||||
<div class="text-4xl font-bold">{{ currentStudy.new.length }}</div>
|
||||
<div class="text">新词</div>
|
||||
</div>
|
||||
<template v-if="settingStore.wordPracticeMode === WordPracticeMode.System">
|
||||
<div class="flex-1 flex flex-col items-center">
|
||||
<div class="text-4xl font-bold">{{ currentStudy.review.length }}</div>
|
||||
<div class="text">复习上次</div>
|
||||
</div>
|
||||
<div class="flex-1 flex flex-col items-center">
|
||||
<div class="text-4xl font-bold">{{ currentStudy.write.length }}
|
||||
<div class="center gap-1">
|
||||
<IconFluentSlideTextTitleEdit20Regular/>
|
||||
<span>更改进度</span>
|
||||
</div>
|
||||
<div class="text">复习之前</div>
|
||||
</div>
|
||||
</template>
|
||||
</BaseButton>
|
||||
</PopConfirm>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col items-end justify-around ">
|
||||
<div class="flex gap-1 items-center">
|
||||
每日目标
|
||||
<div style="color:#ac6ed1;"
|
||||
class="bg-third px-2 h-10 flex center text-2xl rounded">
|
||||
{{ store.sdict.id ? store.sdict.perDayStudyNumber : 0 }}
|
||||
<div class="flex-1">
|
||||
<div class="flex justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="p-2 center rounded-full bg-white ">
|
||||
<IconFluentStar20Filled class="text-lg color-amber"/>
|
||||
</div>
|
||||
<div class="text-xl font-bold">
|
||||
{{ isSaveData ? '上次学习任务' : '今日任务' }}
|
||||
</div>
|
||||
<span class="color-link cursor-pointer"
|
||||
v-if="store.sdict.id"
|
||||
@click="showPracticeWordListDialog = true">词表</span>
|
||||
|
||||
</div>
|
||||
个单词
|
||||
<PopConfirm
|
||||
<div class="flex gap-1 items-center"
|
||||
v-if="store.sdict.id"
|
||||
>
|
||||
每日目标
|
||||
<div style="color:#ac6ed1;"
|
||||
class="bg-third px-2 h-10 flex center text-2xl rounded">
|
||||
{{ store.sdict.id ? store.sdict.perDayStudyNumber : 0 }}
|
||||
</div>
|
||||
个单词
|
||||
<PopConfirm
|
||||
:disabled="!isSaveData"
|
||||
title="当前存在未完成的学习任务,修改会重新生成学习任务,是否继续?"
|
||||
@confirm="check(()=>showPracticeSettingDialog = true)">
|
||||
<span class="color-blue cursor-pointer">更改</span>
|
||||
</PopConfirm>
|
||||
</div>
|
||||
<BaseButton size="large" :disabled="!store.sdict.name"
|
||||
:loading="loading"
|
||||
@click="startPractice">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="line-height-[2]">{{ isSaveData ? '继续学习' : '开始学习' }}</span>
|
||||
<IconFluentArrowCircleRight16Regular class="text-xl"/>
|
||||
<BaseButton
|
||||
type="info" size="small">更改
|
||||
</BaseButton>
|
||||
</PopConfirm>
|
||||
</div>
|
||||
</BaseButton>
|
||||
</div>
|
||||
<div class="flex mt-4 justify-between">
|
||||
<div class="stat">
|
||||
<div class="num">{{ currentStudy.new.length }}</div>
|
||||
<div class="txt">新词数</div>
|
||||
</div>
|
||||
<template v-if="settingStore.wordPracticeMode === WordPracticeMode.System">
|
||||
<div class="stat">
|
||||
<div class="num">{{ currentStudy.review.length }}</div>
|
||||
<div class="txt">复习上次</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="num">{{ currentStudy.write.length }}</div>
|
||||
<div class="txt">复习之前</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="flex items-end mt-4">
|
||||
<BaseButton size="large"
|
||||
class="flex-1"
|
||||
:disabled="!store.sdict.id"
|
||||
:loading="loading"
|
||||
@click="startPractice">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="line-height-[2]">{{ isSaveData ? '继续学习' : '开始学习' }}</span>
|
||||
<IconFluentArrowCircleRight16Regular class="text-xl"/>
|
||||
</div>
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
v-if="store.sdict.id && store.sdict.lastLearnIndex"
|
||||
size="large" type="orange"
|
||||
:loading="loading"
|
||||
@click="check(()=>showShufflePracticeSettingDialog = true)">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="line-height-[2]">随机复习</span>
|
||||
<IconFluentArrowShuffle20Filled class="text-xl"/>
|
||||
</div>
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -271,15 +328,15 @@ const {
|
||||
</BaseIcon>
|
||||
</PopConfirm>
|
||||
|
||||
<div class="color-blue cursor-pointer" v-if="store.word.bookList.length > 3"
|
||||
@click="isMultiple = !isMultiple; selectIds = []">{{ isMultiple ? '取消' : '管理词典' }}
|
||||
<div class="color-link cursor-pointer" v-if="store.word.bookList.length > 3"
|
||||
@click="isManageDict = !isManageDict; selectIds = []">{{ isManageDict ? '取消' : '管理词典' }}
|
||||
</div>
|
||||
<div class="color-blue cursor-pointer" @click="nav('dict-detail', { isAdd: true })">创建个人词典</div>
|
||||
<div class="color-link cursor-pointer" @click="nav('dict-detail', { isAdd: true })">创建个人词典</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-4 flex-wrap mt-4">
|
||||
<Book :is-add="false" quantifier="个词" :item="item" :checked="selectIds.includes(item.id)"
|
||||
@check="() => toggleSelect(item)" :show-checkbox="isMultiple && j >= 3"
|
||||
@check="() => toggleSelect(item)" :show-checkbox="isManageDict && j >= 3"
|
||||
v-for="(item, j) in store.word.bookList" @click="goDictDetail(item)"/>
|
||||
<Book :is-add="true" @click="router.push('/dict-list')"/>
|
||||
</div>
|
||||
@@ -289,7 +346,7 @@ const {
|
||||
<div class="flex justify-between">
|
||||
<div class="title">推荐</div>
|
||||
<div class="flex gap-4 items-center">
|
||||
<div class="color-blue cursor-pointer" @click="router.push('/dict-list')">更多</div>
|
||||
<div class="color-link cursor-pointer" @click="router.push('/dict-list')">更多</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -303,22 +360,37 @@ const {
|
||||
</BasePage>
|
||||
|
||||
<PracticeSettingDialog
|
||||
:show-left-option="false"
|
||||
v-model="showPracticeSettingDialog"
|
||||
@ok="savePracticeSetting"/>
|
||||
:show-left-option="false"
|
||||
v-model="showPracticeSettingDialog"
|
||||
@ok="savePracticeSetting"/>
|
||||
|
||||
<ChangeLastPracticeIndexDialog
|
||||
v-model="showChangeLastPracticeIndexDialog"
|
||||
@ok="saveLastPracticeIndex"
|
||||
v-model="showChangeLastPracticeIndexDialog"
|
||||
@ok="saveLastPracticeIndex"
|
||||
/>
|
||||
|
||||
<PracticeWordListDialog
|
||||
:data="currentStudy"
|
||||
v-model="showPracticeWordListDialog"
|
||||
:data="currentStudy"
|
||||
v-model="showPracticeWordListDialog"
|
||||
/>
|
||||
|
||||
<CollectNotice/>
|
||||
<ShufflePracticeSettingDialog
|
||||
v-model="showShufflePracticeSettingDialog"
|
||||
@ok="onShufflePracticeSettingOk"/>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.stat {
|
||||
@apply w-31% box-border flex flex-col items-center justify-center rounded-xl p-2 bg-[var(--bg-history)];
|
||||
border: 1px solid gainsboro;
|
||||
|
||||
.num {
|
||||
@apply color-[#409eff] text-4xl font-bold;
|
||||
}
|
||||
|
||||
.txt {
|
||||
@apply color-gray-500;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
import { inject, Ref, watch } from "vue"
|
||||
import { usePracticeStore } from "@/stores/practice.ts";
|
||||
import { useSettingStore } from "@/stores/setting.ts";
|
||||
import { PracticeData, WordPracticeType, ShortcutKey } from "@/types/types.ts";
|
||||
import {PracticeData, WordPracticeType, ShortcutKey, TaskWords} from "@/types/types.ts";
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
import Tooltip from "@/components/base/Tooltip.vue";
|
||||
import Progress from '@/components/base/Progress.vue'
|
||||
|
||||
const statisticsStore = usePracticeStore()
|
||||
const statStore = usePracticeStore()
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
defineProps<{
|
||||
@@ -34,7 +34,7 @@ function format(val: number, suffix: string = '', check: number = -1) {
|
||||
const status = $computed(() => {
|
||||
if (isTypingWrongWord.value) return '复习错词'
|
||||
let str = ''
|
||||
switch (statisticsStore.step) {
|
||||
switch (statStore.step) {
|
||||
case 0:
|
||||
str += `学习新词`
|
||||
break
|
||||
@@ -62,6 +62,9 @@ const status = $computed(() => {
|
||||
case 8:
|
||||
str += '默写之前学习'
|
||||
break
|
||||
case 10:
|
||||
str += '随机复习'
|
||||
break
|
||||
}
|
||||
return str
|
||||
})
|
||||
@@ -96,22 +99,22 @@ const progress = $computed(() => {
|
||||
<div class="name">{{ status }}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="num">{{ statisticsStore.total }}</div>
|
||||
<div class="num">{{ statStore.total }}</div>
|
||||
<div class="line"></div>
|
||||
<div class="name">单词总数</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="num">{{ format(statisticsStore.inputWordNumber, '', 0) }}</div>
|
||||
<div class="num">{{ format(statStore.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="num">{{ format(statStore.wrong, '', 0) }}</div>
|
||||
<div class="line"></div>
|
||||
<div class="name">总错误数</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2 justify-center items-center">
|
||||
<div class="flex gap-2 justify-center items-center">
|
||||
<BaseIcon
|
||||
:class="!isSimple?'collect':'fill'"
|
||||
@click="$emit('toggleSimple')"
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {_getAccomplishDays} from "@/utils";
|
||||
import Radio from "@/components/base/radio/Radio.vue";
|
||||
import RadioGroup from "@/components/base/radio/RadioGroup.vue";
|
||||
import { _getAccomplishDays } from "@/utils";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import Checkbox from "@/components/base/checkbox/Checkbox.vue";
|
||||
import Slider from "@/components/base/Slider.vue";
|
||||
import {useBaseStore} from "@/stores/base.ts";
|
||||
import {defineAsyncComponent, watch} from "vue";
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
import { defineAsyncComponent, watch } from "vue";
|
||||
import { useSettingStore } from "@/stores/setting.ts";
|
||||
import Toast from "@/components/base/toast/Toast.ts";
|
||||
import ChangeLastPracticeIndexDialog from "@/pages/word/components/ChangeLastPracticeIndexDialog.vue";
|
||||
import Tooltip from "@/components/base/Tooltip.vue";
|
||||
import {useRuntimeStore} from "@/stores/runtime.ts";
|
||||
import { useRuntimeStore } from "@/stores/runtime.ts";
|
||||
|
||||
const Dialog = defineAsyncComponent(() => import('@/components/dialog/Dialog.vue'))
|
||||
|
||||
|
||||
69
src/pages/word/components/ShufflePracticeSettingDialog.vue
Normal file
69
src/pages/word/components/ShufflePracticeSettingDialog.vue
Normal file
@@ -0,0 +1,69 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import Slider from "@/components/base/Slider.vue";
|
||||
import {defineAsyncComponent, watch} from "vue";
|
||||
import {useBaseStore} from "@/stores/base.ts";
|
||||
|
||||
const Dialog = defineAsyncComponent(() => import('@/components/dialog/Dialog.vue'))
|
||||
|
||||
const store = useBaseStore()
|
||||
|
||||
const model = defineModel()
|
||||
|
||||
const emit = defineEmits<{
|
||||
ok: [val: number];
|
||||
}>()
|
||||
|
||||
let num = $ref(0)
|
||||
let min = $ref(0)
|
||||
|
||||
watch(() => model.value, (n) => {
|
||||
if (n) {
|
||||
num = Math.floor(store.sdict.lastLearnIndex / 3)
|
||||
num = num > 50 ? 50 : num
|
||||
min = num < 10 ? num : 10
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog v-model="model" title="随机复习设置"
|
||||
:footer="true"
|
||||
@ok="emit('ok',num)">
|
||||
<div class="target-modal color-main">
|
||||
<div class="flex gap-4 items-end mb-2">
|
||||
<span>随机复习:<span class="font-bold">{{ store.sdict.name }}</span></span>
|
||||
<span class="text-3xl mx-2 lh">{{ num }}</span>个单词
|
||||
</div>
|
||||
<div class="flex gap-space">
|
||||
<span class="shrink-0">随机数量</span>
|
||||
<Slider :min="min"
|
||||
:step="10"
|
||||
show-text
|
||||
class="mt-1"
|
||||
:max="store.sdict.lastLearnIndex"
|
||||
v-model="num"/>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
.target-modal {
|
||||
width: 30rem;
|
||||
padding: 0 var(--space);
|
||||
|
||||
.lh {
|
||||
color: rgb(176, 116, 211)
|
||||
}
|
||||
|
||||
.mode-item {
|
||||
@apply w-50% border border-blue border-solid p-2 rounded-lg cursor-pointer;
|
||||
}
|
||||
|
||||
.active {
|
||||
@apply bg-blue color-white;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,18 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import {WordPracticeType, ShortcutKey, Word, WordPracticeMode} from "@/types/types.ts";
|
||||
import {ShortcutKey, Word, WordPracticeType} from "@/types/types.ts";
|
||||
import VolumeIcon from "@/components/icon/VolumeIcon.vue";
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
import {usePlayBeep, usePlayCorrect, usePlayKeyboardAudio, usePlayWordAudio} from "@/hooks/sound.ts";
|
||||
import {emitter, EventKey, useEvents} from "@/utils/eventBus.ts";
|
||||
import {inject, onMounted, onUnmounted, Ref, watch} from "vue";
|
||||
import {onMounted, onUnmounted, watch} from "vue";
|
||||
import SentenceHightLightWord from "@/pages/word/components/SentenceHightLightWord.vue";
|
||||
import {usePracticeStore} from "@/stores/practice.ts";
|
||||
import {getDefaultWord} from "@/types/func.ts";
|
||||
import {_nextTick, last, sleep} from "@/utils";
|
||||
import {_nextTick, last} from "@/utils";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import Space from "@/pages/article/components/Space.vue";
|
||||
import Toast from "@/components/base/toast/Toast.ts";
|
||||
import Tooltip from "@/components/base/Tooltip.vue";
|
||||
|
||||
interface IProps {
|
||||
word: Word,
|
||||
@@ -104,9 +103,7 @@ function repeat() {
|
||||
wordRepeatCount++
|
||||
inputLock = false
|
||||
|
||||
if (settingStore.wordSound) {
|
||||
volumeIconRef?.play()
|
||||
}
|
||||
if (settingStore.wordSound) volumeIconRef?.play()
|
||||
}, settingStore.waitTimeForChangeWord)
|
||||
}
|
||||
|
||||
@@ -144,6 +141,7 @@ function unknown(e) {
|
||||
if (!showWordResult) {
|
||||
showWordResult = true
|
||||
emit('wrong')
|
||||
if (settingStore.wordSound) volumeIconRef?.play()
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -153,21 +151,40 @@ function unknown(e) {
|
||||
async function onTyping(e: KeyboardEvent) {
|
||||
debugger
|
||||
let word = props.word.word
|
||||
// 输入完成会锁死不能再输入
|
||||
if (inputLock) {
|
||||
// 因为输入完成会锁死不能再输入,所以在这里判断空格键切换到下一个单词
|
||||
if (e.code === 'Space' && input.toLowerCase() === word.toLowerCase()) {
|
||||
showWordResult = inputLock = false
|
||||
emit('complete')
|
||||
} else {
|
||||
//当显示单词时,提示用户正确按键
|
||||
if (showWordResult) {
|
||||
pressNumber++
|
||||
if (pressNumber >= 3) {
|
||||
Toast.info(right ? '请按空格键切换' : '请按删除键重新输入', {duration: 2000})
|
||||
pressNumber = 0
|
||||
//判断是否是空格键以便切换到下一个单词
|
||||
if (e.code === 'Space') {
|
||||
//正确时就切换到下一个单词
|
||||
if (right) {
|
||||
showWordResult = inputLock = false
|
||||
emit('complete')
|
||||
} else {
|
||||
if (showWordResult) {
|
||||
// 错误时,提示用户按删除键,仅默写需要提示
|
||||
pressNumber++
|
||||
if (pressNumber >= 3) {
|
||||
Toast.info('请按删除键重新输入', {duration: 2000})
|
||||
pressNumber = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
//当正确时,提醒用户按空格键切下一个
|
||||
if (right) {
|
||||
pressNumber++
|
||||
if (pressNumber >= 3) {
|
||||
Toast.info('请按空格键继续', {duration: 2000})
|
||||
pressNumber = 0
|
||||
}
|
||||
} else {
|
||||
//当错误时,按任意键重新输入
|
||||
showWordResult = inputLock = false
|
||||
input = wrong = ''
|
||||
onTyping(e)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
inputLock = true
|
||||
@@ -186,12 +203,12 @@ async function onTyping(e: KeyboardEvent) {
|
||||
} else {
|
||||
//未显示单词,则播放正确音乐,并在后面设置为 showWordResult 为 true 来显示单词
|
||||
playCorrect()
|
||||
volumeIconRef?.play()
|
||||
if (settingStore.wordSound) volumeIconRef?.play()
|
||||
}
|
||||
} else {
|
||||
//错误处理
|
||||
playBeep()
|
||||
volumeIconRef?.play()
|
||||
if (settingStore.wordSound) volumeIconRef?.play()
|
||||
emit('wrong')
|
||||
}
|
||||
showWordResult = true
|
||||
@@ -209,7 +226,7 @@ async function onTyping(e: KeyboardEvent) {
|
||||
if (settingStore.ignoreCase) {
|
||||
right = letter.toLowerCase() === word[input.length].toLowerCase()
|
||||
} else {
|
||||
right = letter === props.word.word[input.length]
|
||||
right = letter === word[input.length]
|
||||
}
|
||||
if (right) {
|
||||
input += letter
|
||||
@@ -219,10 +236,11 @@ async function onTyping(e: KeyboardEvent) {
|
||||
emit('wrong')
|
||||
wrong = letter
|
||||
playBeep()
|
||||
volumeIconRef?.play()
|
||||
await sleep(500)
|
||||
if (settingStore.inputWrongClear) input = ''
|
||||
wrong = ''
|
||||
if (settingStore.wordSound) volumeIconRef?.play()
|
||||
setTimeout(() => {
|
||||
if (settingStore.inputWrongClear) input = ''
|
||||
wrong = ''
|
||||
}, 500)
|
||||
}
|
||||
// 更新当前单词信息
|
||||
updateCurrentWordInfo();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as VueRouter from 'vue-router'
|
||||
import {RouteRecordRaw} from 'vue-router'
|
||||
import { RouteRecordRaw } from 'vue-router'
|
||||
import WordsPage from "@/pages/word/WordsPage.vue";
|
||||
import PC from "@/pages/index.vue";
|
||||
import Layout from "@/pages/layout.vue";
|
||||
import ArticlesPage from "@/pages/article/ArticlesPage.vue";
|
||||
import PracticeArticles from "@/pages/article/PracticeArticles.vue";
|
||||
import DictDetail from "@/pages/word/DictDetail.vue";
|
||||
@@ -10,17 +10,15 @@ import BookDetail from "@/pages/article/BookDetail.vue";
|
||||
import DictList from "@/pages/word/DictList.vue";
|
||||
import BookList from "@/pages/article/BookList.vue";
|
||||
import Setting from "@/pages/setting/Setting.vue";
|
||||
import Home from "@/pages/home/index.vue";
|
||||
import Login from "@/pages/user/login.vue";
|
||||
import User from "@/pages/user/index.vue";
|
||||
|
||||
export const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/',
|
||||
component: PC,
|
||||
redirect: '/',
|
||||
component: Layout,
|
||||
children: [
|
||||
{path: '/', component: Home},
|
||||
{path: '/', redirect: '/words'},
|
||||
{path: 'words', component: WordsPage},
|
||||
{path: 'word', redirect: '/words'},
|
||||
{path: 'practice-words/:id', component: PracticeWords},
|
||||
|
||||
@@ -5,14 +5,11 @@ export interface PracticeState {
|
||||
startDate: number,
|
||||
spend: number,
|
||||
total: number,
|
||||
index: number,//当前输入的第几个,用于和total计算进度
|
||||
newWordNumber: number,
|
||||
reviewWordNumber: number,
|
||||
writeWordNumber: number,
|
||||
inputWordNumber: number,//当前总输入了多少个单词(不包含跳过)
|
||||
wrong: number,
|
||||
startIndex: number,
|
||||
endIndex: number,
|
||||
}
|
||||
|
||||
export const usePracticeStore = defineStore('practice', {
|
||||
@@ -22,9 +19,6 @@ export const usePracticeStore = defineStore('practice', {
|
||||
spend: 0,
|
||||
startDate: Date.now(),
|
||||
total: 0,
|
||||
index: 0,
|
||||
startIndex: 0,
|
||||
endIndex: 0,
|
||||
newWordNumber: 0,
|
||||
reviewWordNumber: 0,
|
||||
writeWordNumber: 0,
|
||||
|
||||
@@ -69,7 +69,7 @@ export const getDefaultSettingState = (): SettingState => ({
|
||||
|
||||
keyboardSound: true,
|
||||
keyboardSoundVolume: 100,
|
||||
keyboardSoundFile: '机械键盘2',
|
||||
keyboardSoundFile: '笔记本键盘',
|
||||
|
||||
effectSound: true,
|
||||
effectSoundVolume: 100,
|
||||
|
||||
@@ -200,6 +200,7 @@ export interface TaskWords {
|
||||
new: Word[],
|
||||
review: Word[],
|
||||
write: Word[],
|
||||
shuffle: Word[],
|
||||
}
|
||||
|
||||
export class DictId {
|
||||
|
||||
Reference in New Issue
Block a user