feat:add guide & dict test mode

This commit is contained in:
Zyronon
2025-11-28 00:42:31 +08:00
parent fb3cca70c8
commit 0677031ebb
18 changed files with 632 additions and 451 deletions

View File

@@ -18,16 +18,16 @@
"deploy-oss": "node scripts/deploy-oss.js"
},
"dependencies": {
"@floating-ui/dom": "^1.7.4",
"@imengyu/vue3-context-menu": "^1.5.1",
"@vueuse/core": "14.0.0-alpha.0",
"@zumer/snapdom": "^2.0.0",
"axios": "^1.10.0",
"axios": "^1.12.0",
"compromise": "^14.14.4",
"copy-to-clipboard": "^3.3.3",
"dayjs": "^1.11.13",
"file-saver": "^2.0.5",
"idb-keyval": "^6.2.2",
"intro.js": "^8.3.2",
"md5": "^2.2.1",
"mitt": "^3.0.1",
"nanoid": "^5.1.5",

65
pnpm-lock.yaml generated
View File

@@ -8,6 +8,9 @@ importers:
.:
dependencies:
'@floating-ui/dom':
specifier: ^1.7.4
version: 1.7.4
'@imengyu/vue3-context-menu':
specifier: ^1.5.1
version: 1.5.2
@@ -18,8 +21,8 @@ importers:
specifier: ^2.0.0
version: 2.0.0
axios:
specifier: ^1.10.0
version: 1.11.0
specifier: ^1.12.0
version: 1.13.2
compromise:
specifier: ^14.14.4
version: 14.14.4
@@ -35,9 +38,6 @@ importers:
idb-keyval:
specifier: ^6.2.2
version: 6.2.2
intro.js:
specifier: ^8.3.2
version: 8.3.2
md5:
specifier: ^2.2.1
version: 2.3.0
@@ -50,6 +50,9 @@ importers:
pinia:
specifier: ^3.0.3
version: 3.0.3(typescript@5.9.2)(vue@3.5.18(typescript@5.9.2))
shepherd.js:
specifier: ^14.5.1
version: 14.5.1
string-comparison:
specifier: ^1.3.0
version: 1.3.0
@@ -520,6 +523,15 @@ packages:
cpu: [x64]
os: [win32]
'@floating-ui/core@1.7.3':
resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==}
'@floating-ui/dom@1.7.4':
resolution: {integrity: sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==}
'@floating-ui/utils@0.2.10':
resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
'@iconify-json/bx@1.2.2':
resolution: {integrity: sha512-hZVx6LMEkYckScdRdUuQWcmv8Lm2au6Cnf799TLoR6YgiAfFvaJ4M5ElwcnExvCu8ntsS7jW89r0W5LwBAfZXQ==}
@@ -854,6 +866,9 @@ packages:
cpu: [x64]
os: [win32]
'@scarf/scarf@1.4.0':
resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==}
'@tybys/wasm-util@0.10.0':
resolution: {integrity: sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==}
@@ -1424,8 +1439,8 @@ packages:
engines: {node: '>= 4.5.0'}
hasBin: true
axios@1.11.0:
resolution: {integrity: sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==}
axios@1.13.2:
resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==}
bach@1.2.0:
resolution: {integrity: sha512-bZOOfCb3gXBXbTFXq3OZtGR88LwGeJvzu6szttaIzymOTS4ZttBNOWSv7aLZja2EMycKtRYV0Oa8SNKH/zkxvg==}
@@ -1792,6 +1807,10 @@ packages:
dedent@0.7.0:
resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==}
deepmerge-ts@7.1.5:
resolution: {integrity: sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==}
engines: {node: '>=16.0.0'}
default-compare@1.0.0:
resolution: {integrity: sha512-QWfXlM0EkAbqOCbD/6HjdwT19j7WCkMyiRhWilc4H9/5h/RzTF9gv5LYh1+CmDV5d1rki6KAWLtQale0xt20eQ==}
engines: {node: '>=0.10.0'}
@@ -2343,9 +2362,6 @@ packages:
resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==}
engines: {node: '>= 0.10'}
intro.js@8.3.2:
resolution: {integrity: sha512-+QsuU8P7Z/O7stAwZ8Uj6CPyKsY6xVx/7hKTVLjlIEPYHaT43XnEUUYHCln6Ehr7GlDuwczh6I8PD/HGf4EG0A==}
invert-kv@1.0.0:
resolution: {integrity: sha512-xgs2NH9AE66ucSq4cNG1nhSFghr5l6tdL15Pk+jl46bmmBapgoaY/AacXyaDznAqmGL99TiLSQgO/XazFSKYeQ==}
engines: {node: '>=0.10.0'}
@@ -3208,6 +3224,10 @@ packages:
resolution: {integrity: sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==}
engines: {node: '>=0.10.0'}
shepherd.js@14.5.1:
resolution: {integrity: sha512-VuvPvLG1QjNOLP7AIm2HGyfmxEIz8QdskvWOHwUcxLDibYWjLRBmCWd8LSL5FlwhBW7D/GU+3gNVC/ASxAWdxg==}
engines: {node: 18.* || >= 20}
side-channel-list@1.0.0:
resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==}
engines: {node: '>= 0.4'}
@@ -4167,6 +4187,17 @@ snapshots:
'@esbuild/win32-x64@0.25.9':
optional: true
'@floating-ui/core@1.7.3':
dependencies:
'@floating-ui/utils': 0.2.10
'@floating-ui/dom@1.7.4':
dependencies:
'@floating-ui/core': 1.7.3
'@floating-ui/utils': 0.2.10
'@floating-ui/utils@0.2.10': {}
'@iconify-json/bx@1.2.2':
dependencies:
'@iconify/types': 2.0.0
@@ -4461,6 +4492,8 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.46.2':
optional: true
'@scarf/scarf@1.4.0': {}
'@tybys/wasm-util@0.10.0':
dependencies:
tslib: 2.8.1
@@ -5234,7 +5267,7 @@ snapshots:
atob@2.1.2: {}
axios@1.11.0:
axios@1.13.2:
dependencies:
follow-redirects: 1.15.11
form-data: 4.0.4
@@ -5650,6 +5683,8 @@ snapshots:
dedent@0.7.0: {}
deepmerge-ts@7.1.5: {}
default-compare@1.0.0:
dependencies:
kind-of: 5.1.0
@@ -6306,8 +6341,6 @@ snapshots:
interpret@1.4.0: {}
intro.js@8.3.2: {}
invert-kv@1.0.0: {}
is-absolute@1.0.0:
@@ -7191,6 +7224,12 @@ snapshots:
is-plain-object: 2.0.4
split-string: 3.1.0
shepherd.js@14.5.1:
dependencies:
'@floating-ui/dom': 1.7.4
'@scarf/scarf': 1.4.0
deepmerge-ts: 7.1.5
side-channel-list@1.0.0:
dependencies:
es-errors: 1.3.0

View File

@@ -1,10 +1,6 @@
@use "anim" as *;
//@use 'intro.js/minified/introjs.min.css';
@use 'shepherd.js/dist/css/shepherd.css';
.shepherd-enabled.shepherd-element{
transform: translateY(30px);
}
:root {
--color-reverse-white: white;
--color-reverse-black: black;

View File

@@ -33,7 +33,7 @@ const studyProgress = $computed(() => {
</script>
<template>
<div class="book relative overflow-hidden" :id="item?.id">
<div class="book relative overflow-hidden" :id="item?.id ?? 'no-book'">
<template v-if="!isAdd">
<div>
<div class="text-base">{{ item?.name }}</div>

View File

@@ -2,6 +2,7 @@
import {defineAsyncComponent, onMounted, watch} from "vue";
import {useSettingStore} from "@/stores/setting.ts";
import { jump2Feedback } from "@/utils";
const Dialog = defineAsyncComponent(() => import('@/components/dialog/Dialog.vue'))
@@ -22,6 +23,7 @@ watch(() => settingStore.load, (n) => {
<Dialog v-model="show"
title="提示"
footer
:closeOnClickBg="false"
cancel-button-text="不再提醒"
confirm-button-text="关闭"
@cancel="settingStore.conflictNotice = false"
@@ -29,18 +31,24 @@ watch(() => settingStore.load, (n) => {
<div class="card w-120 center flex-col color-main py-0 mb-0">
<div>
<div class="text">
1 如果您安装了 <span class="font-bold text-red">调速 Vim</span> 等会接管键盘点击的插件/脚本将导致本网站无法正常使用
<div>
1 如果您安装了 <span class="font-bold text-red">调速 Vim</span> 等插件/脚本将导致本网站无法正常使用
</div>
<div>
因为它们会强行接管键盘按下事件<span class="font-bold text-red">导致使用本网站时按 'A' 'S' 等等按钮无反应</span>
</div>
</div>
<div class="pl-4">
<div>在对应插件/脚本的设置里面排除本网站</div>
<div>临时禁用对应插件/脚本</div>
<div>请打开浏览器无痕模式尝试</div>
</div>
<div class="text mt-2">
2如果您未安装以上插件/脚本还是无法使用
</div>
<div class="pl-4">
<div>请打开浏览器无痕模式尝试</div>
<div>无痕模式下无法正常使用请给<a href="https://github.com/zyronon/TypeWords/issues">作者提 BUG</a>
<div>无痕模式下无法正常使用请给<span class="color-link mx-1 cp" @click="jump2Feedback">点此</span>给作者反馈
</div>
</div>
</div>

View File

@@ -1,3 +1,5 @@
import { offset } from "@floating-ui/dom";
export const GITHUB = 'https://github.com/zyronon/TypeWords'
export const Host = 'typewords.cc'
export const EMAIL = 'zyronon@163.com'
@@ -83,6 +85,9 @@ export const TourConfig = {
cancelIcon: {enabled: true},
modalOverlayOpeningPadding: 10,
modalOverlayOpeningRadius: 6,
floatingUIOptions: {
middleware: [offset({mainAxis:30})]
},
},
total: 10
total: 7
}

View File

@@ -2,7 +2,7 @@
import { useBaseStore } from "@/stores/base.ts";
import { useRouter } from "vue-router";
import BasePage from "@/components/BasePage.vue";
import { _getDictDataByUrl, msToHourMinute, resourceWrap, total, useNav } from "@/utils";
import { _getDictDataByUrl, _nextTick, msToHourMinute, resourceWrap, total, useNav } from "@/utils";
import { DictResource, DictType } from "@/types/types.ts";
import { useRuntimeStore } from "@/stores/runtime.ts";
import BaseIcon from "@/components/BaseIcon.vue";
@@ -18,8 +18,10 @@ import dayjs from "dayjs";
import isBetween from "dayjs/plugin/isBetween";
import isoWeek from 'dayjs/plugin/isoWeek'
import { useFetch } from "@vueuse/core";
import {AppEnv, DICT_LIST, Host, PracticeSaveArticleKey} from "@/config/env.ts";
import { AppEnv, DICT_LIST, Host, PracticeSaveArticleKey, TourConfig } from "@/config/env.ts";
import { myDictList } from "@/apis";
import Shepherd from "shepherd.js";
import { useSettingStore } from "@/stores/setting.ts";
dayjs.extend(isoWeek)
dayjs.extend(isBetween);
@@ -27,6 +29,7 @@ dayjs.extend(isBetween);
const {nav} = useNav()
const base = useBaseStore()
const store = useBaseStore()
const settingStore = useSettingStore()
const router = useRouter()
const runtimeStore = useRuntimeStore()
let isSaveData = $ref(false)
@@ -67,6 +70,40 @@ async function init() {
}
}
watch(() => store?.sbook?.id, (n) => {
console.log('n', n)
if (!n) {
_nextTick(() => {
const tour = new Shepherd.Tour(TourConfig);
tour.on('cancel', () => {
localStorage.setItem('tour-guide', '1');
});
tour.addStep({
id: 'step7',
text: '点击这里选择一本书籍开始学习,步骤前面选词典相同,让我们跳过中间步骤,直接开始练习吧',
attachTo: {
element: '#no-book',
on: 'bottom'
},
buttons: [
{
text: `下一步7/${TourConfig.total}`,
action() {
tour.next()
nav('/practice-articles/article_nce2', {guide: 1})
}
}
]
});
const r = localStorage.getItem('tour-guide');
if (settingStore.first && !r) {
tour.start();
}
}, 500)
}
}, {immediate: true})
function startStudy() {
// console.log(store.sbook.articles[1])
// genArticleSectionData(cloneDeep(store.sbook.articles[1]))
@@ -169,7 +206,8 @@ let isNewHost = $ref(window.location.host === Host)
<template>
<BasePage>
<div class="mb-4" v-if="!isNewHost">
新域名已启用后续请访问 <a href="https://typewords.cc/words?from_old_site=1">https://typewords.cc</a>。当前 2study.top 域名将在不久后停止使用
新域名已启用后续请访问 <a href="https://typewords.cc/words?from_old_site=1">https://typewords.cc</a>。当前
2study.top 域名将在不久后停止使用
</div>
<div class="card flex flex-col md:flex-row justify-between gap-space p-4 md:p-6">
@@ -199,15 +237,18 @@ let isNewHost = $ref(window.location.host === Host)
</div>
</div>
<div class="flex flex-col sm:flex-row gap-4 items-center mt-3 gap-space w-full">
<div class="w-full sm:flex-1 rounded-xl p-4 box-border relative bg-[var(--bg-history)] border border-gray-200">
<div
class="w-full sm:flex-1 rounded-xl p-4 box-border relative bg-[var(--bg-history)] border border-gray-200">
<div class="text-[#409eff] text-xl font-bold">{{ todayTotalSpend }}</div>
<div class="text-gray-500">今日学习时长</div>
</div>
<div class="w-full sm:flex-1 rounded-xl p-4 box-border relative bg-[var(--bg-history)] border border-gray-200">
<div
class="w-full sm:flex-1 rounded-xl p-4 box-border relative bg-[var(--bg-history)] border border-gray-200">
<div class="text-[#409eff] text-xl font-bold">{{ totalDay }}</div>
<div class="text-gray-500">总学习天数</div>
</div>
<div class="w-full sm:flex-1 rounded-xl p-4 box-border relative bg-[var(--bg-history)] border border-gray-200">
<div
class="w-full sm:flex-1 rounded-xl p-4 box-border relative bg-[var(--bg-history)] border border-gray-200">
<div class="text-[#409eff] text-xl font-bold">{{ totalSpend }}</div>
<div class="text-gray-500">总学习时长</div>
</div>

View File

@@ -34,9 +34,10 @@ import { useRoute, useRouter } from "vue-router";
import PracticeLayout from "@/components/PracticeLayout.vue";
import ArticleAudio from "@/pages/article/components/ArticleAudio.vue";
import VolumeSetting from "@/pages/article/components/VolumeSetting.vue";
import { AppEnv, DICT_LIST, PracticeSaveArticleKey } from "@/config/env.ts";
import { AppEnv, DICT_LIST, PracticeSaveArticleKey, TourConfig } from "@/config/env.ts";
import { addStat, setDictProp } from "@/apis";
import { useRuntimeStore } from "@/stores/runtime.ts";
import Shepherd from "shepherd.js";
const store = useBaseStore()
const runtimeStore = useRuntimeStore()
@@ -50,6 +51,7 @@ let articleData = $ref({
})
let showEditArticle = $ref(false)
let typingArticleRef = $ref<any>()
let showConflictNotice = $ref(false)
let loading = $ref<boolean>(false)
let allWrongWords = new Set()
let editArticle = $ref<Article>(getDefaultArticle())
@@ -152,11 +154,45 @@ const handleSpeedUpdate = (speed: number) => {
})
}
watch(() => store.load, (n) => {
if (n && loading) init()
watch([() => store.load, () => loading], ([a, b]) => {
if (a && b) init()
}, {immediate: true})
watch(() => articleData?.article?.id, id => {
if (id) {
_nextTick(() => {
const tour = new Shepherd.Tour(TourConfig);
tour.on('cancel', () => {
localStorage.setItem('tour-guide', '1');
});
tour.addStep({
id: 'step8',
text: '这里可以练习文章,只需要按下键盘上对应的按键即可,没有输入框!',
attachTo: {
element: '#article-content',
on: 'auto'
},
buttons: [
{
text: `关闭`,
action() {
settingStore.first = false
tour.next()
setTimeout(() => {
showConflictNotice = true
}, 1500)
}
}
]
});
const r = localStorage.getItem('tour-guide');
if (settingStore.first && !r) {
tour.start();
}
}, 500)
}
})
watch(() => settingStore.$state, (n) => {
initAudio()
}, {immediate: true, deep: true})
@@ -168,6 +204,12 @@ onMounted(() => {
} else {
loading = true
}
if (route.query.guide) {
showConflictNotice = false
} else {
showConflictNotice = true
}
})
onUnmounted(() => {
@@ -605,7 +647,7 @@ provide('currentPractice', currentPractice)
@save="saveArticle"
/>
<ConflictNotice/>
<ConflictNotice v-if="showConflictNotice"/>
</template>
<style scoped lang="scss">
@@ -670,7 +712,7 @@ provide('currentPractice', currentPractice)
.practice-article {
padding-top: 3rem; // 为固定标题留出空间
}
// 优化标题区域
.typing-article {
header {
@@ -683,63 +725,63 @@ provide('currentPractice', currentPractice)
padding: 0.5rem 1rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
margin-bottom: 0;
.title {
font-size: 1rem;
line-height: 1.4;
word-break: break-word;
.font-family {
font-size: 0.9rem;
}
}
.titleTranslate {
font-size: 0.8rem;
margin-top: 0.2rem;
opacity: 0.8;
}
}
.article-content {
margin-top: 2rem; // 为固定标题留出空间
}
}
.footer {
width: 100%;
.bottom {
padding: 0.3rem 0.5rem 0.5rem 0.5rem;
border-radius: 0.4rem;
.stat {
margin-top: 0.3rem;
gap: 0.2rem;
flex-direction: row;
overflow-x: auto;
.row {
min-width: 3.5rem;
gap: 0.2rem;
.num {
font-size: 0.8rem;
font-weight: bold;
}
.name {
font-size: 0.7rem;
}
}
}
.flex.flex-col.items-center.justify-center.gap-1 {
.flex.gap-2.center {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.4rem;
.base-icon {
padding: 0.3rem;
font-size: 1rem;
@@ -752,7 +794,7 @@ provide('currentPractice', currentPractice)
}
}
}
.arrow {
font-size: 1rem;
padding: 0.3rem;

View File

@@ -657,7 +657,7 @@ const currentPractice = inject('currentPractice', [])
<div class="titleTranslate" v-if="settingStore.translate">{{ props.article.titleTranslate }}</div>
</header>
<div class="article-content" ref="articleWrapperRef">
<div id="article-content" class="article-content" ref="articleWrapperRef">
<article :class="[
settingStore.translate && 'tall',
settingStore.dictation && 'dictation',

View File

@@ -35,7 +35,7 @@ function goHome() {
<IconFluentTextUnderlineDouble20Regular/>
<span v-if="settingStore.sideExpand">单词</span>
</div>
<div class="row" @click="router.push('/articles')">
<div id="article" class="row" @click="router.push('/articles')">
<!-- <IconPhArticleNyTimes/>-->
<IconFluentBookLetter20Regular/>
<span v-if="settingStore.sideExpand">文章</span>
@@ -49,10 +49,10 @@ function goHome() {
<IconFluentCommentEdit20Regular/>
<span v-if="settingStore.sideExpand">建议反馈</span>
</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

View File

@@ -648,6 +648,14 @@ function transferOk() {
</div>
<div v-if="tabIndex === 5">
<div class="log-item">
<div class="mb-2">
<div>
<div>日期2025/11/28</div>
<div>内容新增引导框新增词典测试模式大佬 hebeihang 开发</div>
</div>
</div>
</div>
<div class="log-item">
<div class="mb-2">
<div>

View File

@@ -1,11 +1,11 @@
<script setup lang="tsx">
import {DictId} from "@/types/types.ts";
import { DictId } from "@/types/types.ts";
import BasePage from "@/components/BasePage.vue";
import {computed, onMounted, reactive, ref, shallowReactive, watch} from "vue";
import {useRuntimeStore} from "@/stores/runtime.ts";
import {_getDictDataByUrl, _nextTick, convertToWord, isMobile, loadJsLib, sleep, useNav} from "@/utils";
import {nanoid} from "nanoid";
import { computed, onMounted, reactive, ref, shallowReactive, watch } from "vue";
import { useRuntimeStore } from "@/stores/runtime.ts";
import { _getDictDataByUrl, _nextTick, convertToWord, isMobile, loadJsLib, sleep, useNav } from "@/utils";
import { nanoid } from "nanoid";
import BaseIcon from "@/components/BaseIcon.vue";
import BaseTable from "@/components/BaseTable.vue";
import WordItem from "@/components/WordItem.vue";
@@ -13,21 +13,21 @@ import Toast from '@/components/base/toast/Toast.ts'
import PopConfirm from "@/components/PopConfirm.vue";
import BackIcon from "@/components/BackIcon.vue";
import BaseButton from "@/components/BaseButton.vue";
import {useRoute, useRouter} from "vue-router";
import {useBaseStore} from "@/stores/base.ts";
import { useRoute, useRouter } from "vue-router";
import { useBaseStore } from "@/stores/base.ts";
import EditBook from "@/pages/article/components/EditBook.vue";
import {getDefaultDict} from "@/types/func.ts";
import { getDefaultDict } from "@/types/func.ts";
import BaseInput from "@/components/base/BaseInput.vue";
import Textarea from "@/components/base/Textarea.vue";
import FormItem from "@/components/base/form/FormItem.vue";
import Form from "@/components/base/form/Form.vue";
import DeleteIcon from "@/components/icon/DeleteIcon.vue";
import {getCurrentStudyWord} from "@/hooks/dict.ts";
import { getCurrentStudyWord } from "@/hooks/dict.ts";
import PracticeSettingDialog from "@/pages/word/components/PracticeSettingDialog.vue";
import {useSettingStore} from "@/stores/setting.ts";
import {MessageBox} from "@/utils/MessageBox.tsx";
import {AppEnv, Origin, PracticeSaveWordKey, TourConfig} from "@/config/env.ts";
import {detail} from "@/apis";
import { useSettingStore } from "@/stores/setting.ts";
import { MessageBox } from "@/utils/MessageBox.tsx";
import { AppEnv, Origin, PracticeSaveWordKey, TourConfig } from "@/config/env.ts";
import { detail } from "@/apis";
import Shepherd from "shepherd.js";
const runtimeStore = useRuntimeStore()
@@ -152,7 +152,7 @@ function word2Str(word) {
res.phrases = word.phrases.map(v => (v.c + "\n" + v.cn).replaceAll('"', '')).join('\n\n')
res.synos = word.synos.map(v => (v.pos + v.cn + "\n" + v.ws.join('/')).replaceAll('"', '')).join('\n\n')
res.relWords = word.relWords.root ? ('词根:' + word.relWords.root + '\n\n' +
word.relWords.rels.map(v => (v.pos + "\n" + v.words.map(v => (v.c + ':' + v.cn)).join('\n')).replaceAll('"', '')).join('\n\n')) : ''
word.relWords.rels.map(v => (v.pos + "\n" + v.words.map(v => (v.c + ':' + v.cn)).join('\n')).replaceAll('"', '')).join('\n\n')) : ''
res.etymology = word.etymology.map(v => (v.t + '\n' + v.d).replaceAll('"', '')).join('\n\n')
return res
}
@@ -193,8 +193,8 @@ onMounted(async () => {
router.push("/word")
} else {
if (!runtimeStore.editDict.words.length
&& !runtimeStore.editDict.custom
&& ![DictId.wordCollect, DictId.wordWrong, DictId.wordKnown].includes(runtimeStore.editDict.en_name || runtimeStore.editDict.id)
&& !runtimeStore.editDict.custom
&& ![DictId.wordCollect, DictId.wordWrong, DictId.wordKnown].includes(runtimeStore.editDict.en_name || runtimeStore.editDict.id)
) {
loading = true
let r = await _getDictDataByUrl(runtimeStore.editDict)
@@ -229,7 +229,7 @@ const settingStore = useSettingStore()
const {nav} = useNav()
//todo 可以和首页合并
async function startPractice() {
async function startPractice(query = {}) {
localStorage.removeItem(PracticeSaveWordKey.key)
studyLoading = true
await base.changeDict(runtimeStore.editDict)
@@ -243,7 +243,7 @@ async function startPractice() {
wordPracticeMode: settingStore.wordPracticeMode
})
let currentStudy = getCurrentStudyWord()
nav('practice-words/' + store.sdict.id, {}, {taskWords: currentStudy})
nav('practice-words/' + store.sdict.id, query, {taskWords: currentStudy})
}
async function addMyStudyList() {
@@ -320,22 +320,22 @@ function importData(e) {
if (repeat.length) {
MessageBox.confirm(
'单词"' + repeat.map(v => v.word).join(', ') + '" 已存在,是否覆盖原单词?',
'检测到重复单词',
() => {
repeat.map(v => {
runtimeStore.editDict.words[v.index] = v
delete runtimeStore.editDict.words[v.index]["index"]
})
},
null,
() => {
tableRef.value.closeImportDialog()
e.target.value = ''
importLoading = false
syncDictInMyStudyList()
Toast.success('导入成功!')
}
'单词"' + repeat.map(v => v.word).join(', ') + '" 已存在,是否覆盖原单词?',
'检测到重复单词',
() => {
repeat.map(v => {
runtimeStore.editDict.words[v.index] = v
delete runtimeStore.editDict.words[v.index]["index"]
})
},
null,
() => {
tableRef.value.closeImportDialog()
e.target.value = ''
importLoading = false
syncDictInMyStudyList()
Toast.success('导入成功!')
}
)
} else {
tableRef.value.closeImportDialog()
@@ -390,6 +390,9 @@ watch(() => loading, (val) => {
if (!val) return
_nextTick(() => {
const tour = new Shepherd.Tour(TourConfig);
tour.on('cancel', () => {
localStorage.setItem('tour-guide', '1');
});
tour.addStep({
id: 'step3',
text: '点击这里开始学习',
@@ -398,7 +401,6 @@ watch(() => loading, (val) => {
{
text: `下一步3/${TourConfig.total}`,
action() {
// localStorage.setItem('shepherd_step', 'step3');
tour.next()
addMyStudyList()
}
@@ -408,14 +410,14 @@ watch(() => loading, (val) => {
tour.addStep({
id: 'step4',
text: '点击这里选择学习模式',
text: '这里可以选择学习模式、设置学习数量、修改学习进度',
attachTo: {element: '#mode', on: 'bottom'},
beforeShowPromise() {
return new Promise((resolve) => {
const timer = setInterval(() => {
if (document.querySelector('#mode')) {
clearInterval(timer);
resolve(true);
setTimeout(resolve, 500)
}
}, 100);
});
@@ -423,240 +425,225 @@ watch(() => loading, (val) => {
buttons: [
{
text: `下一步4/${TourConfig.total}`,
action: tour.next
}
]
});
tour.addStep({
id: 'step5',
text: '点击这里开始学习',
attachTo: {element: '#dialog-ok', on: 'bottom'},
buttons: [
{
text: `下一步5/${TourConfig.total}`,
action() {
localStorage.setItem('shepherd_step', 'step6');
tour.next()
startPractice()
startPractice({guide: 1})
}
}
]
});
const stepId = localStorage.getItem('shepherd_step');
if (stepId) {
// localStorage.removeItem('shepherd_step');
const r = localStorage.getItem('tour-guide');
if (settingStore.first && !r) {
tour.start();
tour.show(stepId); // 直接跳到对应步骤
}
},)
}, 500)
})
defineRender(() => {
return (
<BasePage>
{
showBookDetail.value ? <div className="card mb-0 dict-detail-card flex flex-col">
<div class="dict-header flex justify-between items-center relative">
<BackIcon class="dict-back z-2"/>
<div class="dict-title absolute page-title text-align-center w-full">{runtimeStore.editDict.name}</div>
<div class="dict-actions flex">
<BaseButton loading={studyLoading || loading} type="info"
onClick={() => isEdit = true}>编辑</BaseButton>
<BaseButton id="study" loading={studyLoading || loading} onClick={addMyStudyList}>学习</BaseButton>
<BaseButton loading={studyLoading || loading} onClick={startTest}>测试</BaseButton>
</div>
</div>
<div class="text-lg ">介绍{runtimeStore.editDict.description}</div>
<div class="line my-3"></div>
{/* 移动端标签页导航 */}
{isMob && isOperate && (
<div class="tab-navigation mb-3">
<div
class={`tab-item ${activeTab === 'list' ? 'active' : ''}`}
onClick={() => activeTab = 'list'}
>
单词列表
</div>
<div
class={`tab-item ${activeTab === 'edit' ? 'active' : ''}`}
onClick={() => activeTab = 'edit'}
>
{wordForm.id ? '编辑' : '添加'}单词
</div>
</div>
)}
<div class="flex flex-1 overflow-hidden content-area">
<div class={`word-list-section ${isMob && isOperate && activeTab !== 'list' ? 'mobile-hidden' : ''}`}>
<BaseTable
ref={tableRef}
class="h-full"
list={list}
loading={loading}
onUpdate:list={e => list = e}
del={delWord}
batchDel={batchDel}
add={addWord}
onImportData={importData}
onExportData={exportData}
exportLoading={exportLoading}
importLoading={importLoading}
>
{
(val) =>
<WordItem
showTransPop={false}
item={val.item}>
{{
prefix: () => val.checkbox(val.item),
suffix: () => (
<div class='flex flex-col'>
<BaseIcon
class="option-icon"
onClick={() => editWord(val.item)}
title="编辑">
<IconFluentTextEditStyle20Regular/>
</BaseIcon>
<PopConfirm title="确认删除?"
onConfirm={() => delWord(val.item.id)}
>
<BaseIcon
class="option-icon"
title="删除">
<DeleteIcon/>
</BaseIcon>
</PopConfirm>
</div>
)
}}
</WordItem>
}
</BaseTable>
</div>
{
isOperate ? (
<div
class={`edit-section flex-1 flex flex-col ${isMob && activeTab !== 'edit' ? 'mobile-hidden' : ''}`}>
<div class="common-title">
{wordForm.id ? '修改' : '添加'}单词
</div>
<Form
class="flex-1 overflow-auto pr-2"
ref={e => wordFormRef = e}
rules={wordRules}
model={wordForm}
label-width="7rem">
<FormItem label="单词" prop="word">
<BaseInput
modelValue={wordForm.word}
onUpdate:modelValue={e => wordForm.word = e}
>
</BaseInput>
</FormItem>
<FormItem label="英音音标">
<BaseInput
modelValue={wordForm.phonetic0}
onUpdate:modelValue={e => wordForm.phonetic0 = e}
/>
</FormItem>
<FormItem label="美音音标">
<BaseInput
modelValue={wordForm.phonetic1}
onUpdate:modelValue={e => wordForm.phonetic1 = e}/>
</FormItem>
<FormItem label="翻译">
<Textarea
modelValue={wordForm.trans}
onUpdate:modelValue={e => wordForm.trans = e}
placeholder="一行一个翻译前面词性后面内容如n.取消);多个翻译请换行"
autosize={{minRows: 6, maxRows: 10}}/>
</FormItem>
<FormItem label="例句">
<Textarea
modelValue={wordForm.sentences}
onUpdate:modelValue={e => wordForm.sentences = e}
placeholder="一行原文,一行译文;多个请换两行"
autosize={{minRows: 6, maxRows: 10}}/>
</FormItem>
<FormItem label="短语">
<Textarea
modelValue={wordForm.phrases}
onUpdate:modelValue={e => wordForm.phrases = e}
placeholder="一行原文,一行译文;多个请换两行"
autosize={{minRows: 6, maxRows: 10}}/>
</FormItem>
<FormItem label="同义词">
<Textarea
modelValue={wordForm.synos}
onUpdate:modelValue={e => wordForm.synos = e}
placeholder="请参考已有单词格式"
autosize={{minRows: 6, maxRows: 20}}/>
</FormItem>
<FormItem label="同根词">
<Textarea
modelValue={wordForm.relWords}
onUpdate:modelValue={e => wordForm.relWords = e}
placeholder="请参考已有单词格式"
autosize={{minRows: 6, maxRows: 20}}/>
</FormItem>
<FormItem label="词源">
<Textarea
modelValue={wordForm.etymology}
onUpdate:modelValue={e => wordForm.etymology = e}
placeholder="请参考已有单词格式"
autosize={{minRows: 6, maxRows: 10}}/>
</FormItem>
</Form>
<div class="center">
<BaseButton
type="info"
onClick={closeWordForm}>关闭
</BaseButton>
<BaseButton type="primary"
onClick={onSubmitWord}>保存
</BaseButton>
</div>
<BasePage>
{
showBookDetail.value ? <div className="card mb-0 dict-detail-card flex flex-col">
<div class="dict-header flex justify-between items-center relative">
<BackIcon class="dict-back z-2"/>
<div class="dict-title absolute page-title text-align-center w-full">{runtimeStore.editDict.name}</div>
<div class="dict-actions flex">
<BaseButton loading={studyLoading || loading} type="info"
onClick={() => isEdit = true}>编辑</BaseButton>
<BaseButton id="study" loading={studyLoading || loading} onClick={addMyStudyList}>学习</BaseButton>
<BaseButton loading={studyLoading || loading} onClick={startTest}>测试</BaseButton>
</div>
) : null
}
</div>
</div> :
<div class="card mb-0 dict-detail-card">
<div class="dict-header flex justify-between items-center relative">
<BackIcon class="dict-back z-2" onClick={() => {
if (isAdd) {
router.back()
} else {
isEdit = false
}
}}/>
<div class="dict-title absolute page-title text-align-center w-full">
{runtimeStore.editDict.id ? '修改' : '创建'}词典
</div>
</div>
<div class="center">
<EditBook
isAdd={isAdd}
isBook={false}
onClose={formClose}
onSubmit={() => isEdit = isAdd = false}
/>
</div>
</div>
}
</div>
<div class="text-lg ">介绍{runtimeStore.editDict.description}</div>
<div class="line my-3"></div>
<PracticeSettingDialog
showLeftOption
modelValue={showPracticeSettingDialog}
onUpdate:modelValue={val => (showPracticeSettingDialog = val)}
onOk={startPractice}/>
</BasePage>
{/* 移动端标签页导航 */}
{isMob && isOperate && (
<div class="tab-navigation mb-3">
<div
class={`tab-item ${activeTab === 'list' ? 'active' : ''}`}
onClick={() => activeTab = 'list'}
>
单词列表
</div>
<div
class={`tab-item ${activeTab === 'edit' ? 'active' : ''}`}
onClick={() => activeTab = 'edit'}
>
{wordForm.id ? '编辑' : '添加'}单词
</div>
</div>
)}
<div class="flex flex-1 overflow-hidden content-area">
<div class={`word-list-section ${isMob && isOperate && activeTab !== 'list' ? 'mobile-hidden' : ''}`}>
<BaseTable
ref={tableRef}
class="h-full"
list={list}
loading={loading}
onUpdate:list={e => list = e}
del={delWord}
batchDel={batchDel}
add={addWord}
onImportData={importData}
onExportData={exportData}
exportLoading={exportLoading}
importLoading={importLoading}
>
{
(val) =>
<WordItem
showTransPop={false}
item={val.item}>
{{
prefix: () => val.checkbox(val.item),
suffix: () => (
<div class='flex flex-col'>
<BaseIcon
class="option-icon"
onClick={() => editWord(val.item)}
title="编辑">
<IconFluentTextEditStyle20Regular/>
</BaseIcon>
<PopConfirm title="确认删除?"
onConfirm={() => delWord(val.item.id)}
>
<BaseIcon
class="option-icon"
title="删除">
<DeleteIcon/>
</BaseIcon>
</PopConfirm>
</div>
)
}}
</WordItem>
}
</BaseTable>
</div>
{
isOperate ? (
<div
class={`edit-section flex-1 flex flex-col ${isMob && activeTab !== 'edit' ? 'mobile-hidden' : ''}`}>
<div class="common-title">
{wordForm.id ? '修改' : '添加'}单词
</div>
<Form
class="flex-1 overflow-auto pr-2"
ref={e => wordFormRef = e}
rules={wordRules}
model={wordForm}
label-width="7rem">
<FormItem label="单词" prop="word">
<BaseInput
modelValue={wordForm.word}
onUpdate:modelValue={e => wordForm.word = e}
>
</BaseInput>
</FormItem>
<FormItem label="英音音标">
<BaseInput
modelValue={wordForm.phonetic0}
onUpdate:modelValue={e => wordForm.phonetic0 = e}
/>
</FormItem>
<FormItem label="美音音标">
<BaseInput
modelValue={wordForm.phonetic1}
onUpdate:modelValue={e => wordForm.phonetic1 = e}/>
</FormItem>
<FormItem label="翻译">
<Textarea
modelValue={wordForm.trans}
onUpdate:modelValue={e => wordForm.trans = e}
placeholder="一行一个翻译前面词性后面内容如n.取消);多个翻译请换行"
autosize={{minRows: 6, maxRows: 10}}/>
</FormItem>
<FormItem label="例句">
<Textarea
modelValue={wordForm.sentences}
onUpdate:modelValue={e => wordForm.sentences = e}
placeholder="一行原文,一行译文;多个请换两行"
autosize={{minRows: 6, maxRows: 10}}/>
</FormItem>
<FormItem label="短语">
<Textarea
modelValue={wordForm.phrases}
onUpdate:modelValue={e => wordForm.phrases = e}
placeholder="一行原文,一行译文;多个请换两行"
autosize={{minRows: 6, maxRows: 10}}/>
</FormItem>
<FormItem label="同义词">
<Textarea
modelValue={wordForm.synos}
onUpdate:modelValue={e => wordForm.synos = e}
placeholder="请参考已有单词格式"
autosize={{minRows: 6, maxRows: 20}}/>
</FormItem>
<FormItem label="同根词">
<Textarea
modelValue={wordForm.relWords}
onUpdate:modelValue={e => wordForm.relWords = e}
placeholder="请参考已有单词格式"
autosize={{minRows: 6, maxRows: 20}}/>
</FormItem>
<FormItem label="词源">
<Textarea
modelValue={wordForm.etymology}
onUpdate:modelValue={e => wordForm.etymology = e}
placeholder="请参考已有单词格式"
autosize={{minRows: 6, maxRows: 10}}/>
</FormItem>
</Form>
<div class="center">
<BaseButton
type="info"
onClick={closeWordForm}>关闭
</BaseButton>
<BaseButton type="primary"
onClick={onSubmitWord}>保存
</BaseButton>
</div>
</div>
) : null
}
</div>
</div> :
<div class="card mb-0 dict-detail-card">
<div class="dict-header flex justify-between items-center relative">
<BackIcon class="dict-back z-2" onClick={() => {
if (isAdd) {
router.back()
} else {
isEdit = false
}
}}/>
<div class="dict-title absolute page-title text-align-center w-full">
{runtimeStore.editDict.id ? '修改' : '创建'}词典
</div>
</div>
<div class="center">
<EditBook
isAdd={isAdd}
isBook={false}
onClose={formClose}
onSubmit={() => isEdit = isAdd = false}
/>
</div>
</div>
}
<PracticeSettingDialog
showLeftOption
modelValue={showPracticeSettingDialog}
onUpdate:modelValue={val => (showPracticeSettingDialog = val)}
onOk={startPractice}/>
</BasePage>
)
})
</script>

View File

@@ -1,25 +1,27 @@
<script setup lang="ts">
import {_nextTick, groupBy, resourceWrap, useNav} from "@/utils";
import { _nextTick, groupBy, resourceWrap, useNav } from "@/utils";
import BasePage from "@/components/BasePage.vue";
import {DictResource} from "@/types/types.ts";
import {useRuntimeStore} from "@/stores/runtime.ts";
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 BaseButton from "@/components/BaseButton.vue";
import DictList from "@/components/list/DictList.vue";
import BackIcon from "@/components/BackIcon.vue";
import DictGroup from "@/components/list/DictGroup.vue";
import {useBaseStore} from "@/stores/base.ts";
import {useRouter} from "vue-router";
import {computed, onMounted, watch} from "vue";
import {getDefaultDict} from "@/types/func.ts";
import {useFetch} from "@vueuse/core";
import {DICT_LIST, TourConfig} from "@/config/env.ts";
import { useBaseStore } from "@/stores/base.ts";
import { useRouter } from "vue-router";
import { computed, onMounted, watch } from "vue";
import { getDefaultDict } from "@/types/func.ts";
import { useFetch } from "@vueuse/core";
import { DICT_LIST, TourConfig } from "@/config/env.ts";
import BaseInput from "@/components/base/BaseInput.vue";
import Shepherd from "shepherd.js";
import { useSettingStore } from "@/stores/setting.ts";
const {nav} = useNav()
const runtimeStore = useRuntimeStore()
const settingStore = useSettingStore()
const store = useBaseStore()
const router = useRouter()
@@ -69,10 +71,10 @@ const searchList = computed<any[]>(() => {
let s = searchKey.toLowerCase()
return dict_list.value.filter((item) => {
return item.id.toLowerCase().includes(s)
|| item.name.toLowerCase().includes(s)
|| item.category.toLowerCase().includes(s)
|| item.tags.join('').replace('所有', '').toLowerCase().includes(s)
|| item?.url?.toLowerCase?.().includes?.(s)
|| item.name.toLowerCase().includes(s)
|| item.category.toLowerCase().includes(s)
|| item.tags.join('').replace('所有', '').toLowerCase().includes(s)
|| item?.url?.toLowerCase?.().includes?.(s)
})
}
return []
@@ -84,6 +86,9 @@ watch(dict_list, (val) => {
if (!cet4) return
_nextTick(() => {
const tour = new Shepherd.Tour(TourConfig);
tour.on('cancel', () => {
localStorage.setItem('tour-guide', '1');
});
tour.addStep({
id: 'step2',
text: '选一本自己准备学习的词典',
@@ -92,7 +97,6 @@ watch(dict_list, (val) => {
{
text: `下一步2/${TourConfig.total}`,
action() {
localStorage.setItem('shepherd_step', 'step3');
tour.next()
selectDict({dict: cet4})
}
@@ -100,13 +104,11 @@ watch(dict_list, (val) => {
]
});
const stepId = localStorage.getItem('shepherd_step');
if (stepId) {
// localStorage.removeItem('shepherd_step');
const r = localStorage.getItem('tour-guide');
if (settingStore.first && !r) {
tour.start();
tour.show(stepId); // 直接跳到对应步骤
}
},)
}, 500)
})
</script>
@@ -123,31 +125,31 @@ watch(dict_list, (val) => {
<div class="py-1 flex flex-1 justify-end" v-else>
<span class="page-title absolute w-full center">词典列表</span>
<BaseIcon
title="搜索"
@click="showSearchInput = true"
class="z-1"
icon="fluent:search-24-regular">
title="搜索"
@click="showSearchInput = true"
class="z-1"
icon="fluent:search-24-regular">
<IconFluentSearch24Regular/>
</BaseIcon>
</div>
</div>
<div class="mt-4" v-if="searchKey">
<DictList
v-if="searchList.length "
@selectDict="selectDict"
:list="searchList"
quantifier="个词"
:select-id="'-1'"/>
v-if="searchList.length "
@selectDict="selectDict"
:list="searchList"
quantifier="个词"
:select-id="'-1'"/>
<Empty v-else text="没有相关词典"/>
</div>
<div class="w-full" v-else>
<DictGroup
v-for="item in groupedByCategoryAndTag"
:select-id="store.currentStudyWordDict.id"
@selectDict="selectDict"
quantifier="个词"
:groupByTag="item[1]"
:category="item[0]"
v-for="item in groupedByCategoryAndTag"
:select-id="store.currentStudyWordDict.id"
@selectDict="selectDict"
quantifier="个词"
:groupByTag="item[1]"
:category="item[0]"
/>
</div>
</div>

View File

@@ -1,16 +1,16 @@
<script setup lang="ts">
import {onMounted, provide, ref, toRef, 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";
import {useSettingStore} from "@/stores/setting.ts";
import {useRuntimeStore} from "@/stores/runtime.ts";
import {Dict, PracticeData, WordPracticeType, ShortcutKey, TaskWords, Word, WordPracticeMode} from "@/types/types.ts";
import {useDisableEventListener, useOnKeyboardEventListener, useStartKeyboardEventListener} from "@/hooks/event.ts";
import { emitter, EventKey, useEvents } from "@/utils/eventBus.ts";
import { useSettingStore } from "@/stores/setting.ts";
import { useRuntimeStore } from "@/stores/runtime.ts";
import { Dict, PracticeData, WordPracticeType, ShortcutKey, TaskWords, Word, WordPracticeMode } from "@/types/types.ts";
import { useDisableEventListener, useOnKeyboardEventListener, useStartKeyboardEventListener } from "@/hooks/event.ts";
import useTheme from "@/hooks/theme.ts";
import {getCurrentStudyWord, useWordOptions} from "@/hooks/dict.ts";
import {_getDictDataByUrl, cloneDeep, resourceWrap, shuffle} from "@/utils";
import {useRoute, useRouter} from "vue-router";
import { getCurrentStudyWord, useWordOptions } from "@/hooks/dict.ts";
import { _getDictDataByUrl, _nextTick, cloneDeep, resourceWrap, shuffle } from "@/utils";
import { useRoute, useRouter } from "vue-router";
import Footer from "@/pages/word/components/Footer.vue";
import Panel from "@/components/Panel.vue";
import BaseIcon from "@/components/BaseIcon.vue";
@@ -18,15 +18,18 @@ import Tooltip from "@/components/base/Tooltip.vue";
import WordList from "@/components/list/WordList.vue";
import TypeWord from "@/pages/word/components/TypeWord.vue";
import Empty from "@/components/Empty.vue";
import {useBaseStore} from "@/stores/base.ts";
import {usePracticeStore} from "@/stores/practice.ts";
import { useBaseStore } from "@/stores/base.ts";
import { usePracticeStore } from "@/stores/practice.ts";
import Toast from '@/components/base/toast/Toast.ts'
import {getDefaultDict, getDefaultWord} from "@/types/func.ts";
import { getDefaultDict, getDefaultWord } from "@/types/func.ts";
import ConflictNotice from "@/components/ConflictNotice.vue";
import PracticeLayout from "@/components/PracticeLayout.vue";
import {DICT_LIST, PracticeSaveWordKey} from "@/config/env.ts";
import {ToastInstance} from "@/components/base/toast/type.ts";
import { DICT_LIST, PracticeSaveWordKey, TourConfig } from "@/config/env.ts";
import { ToastInstance } from "@/components/base/toast/type.ts";
import { watchOnce } from "@vueuse/core";
import Shepherd from "shepherd.js";
import { offset } from '@floating-ui/dom';
const {
isWordCollect,
@@ -42,6 +45,7 @@ const route = useRoute()
const store = useBaseStore()
const statStore = usePracticeStore()
const typingRef: any = $ref()
let showConflictNotice = $ref(false)
let allWrongWords = new Set()
let showStatDialog = $ref(false)
let loading = $ref(false)
@@ -103,6 +107,54 @@ onMounted(() => {
} else {
loading = true
}
if (route.query.guide) {
showConflictNotice = false
} else {
showConflictNotice = true
}
})
watchOnce(() => data.words.length, (newVal, oldVal) => {
//如果是从无值变有值,代表是开始
if (!oldVal && newVal) {
_nextTick(() => {
const tour = new Shepherd.Tour(TourConfig);
tour.on('cancel', () => {
localStorage.setItem('tour-guide', '1');
});
tour.addStep({
id: 'step5',
text: '这里可以练习拼写单词,只需要按下键盘上对应的按键即可,没有输入框!',
attachTo: {element: '#word', on: 'bottom'},
buttons: [
{
text: `下一步5/${TourConfig.total}`,
action:tour.next
}
]
});
tour.addStep({
id: 'step6',
text: '这里是文章练习',
attachTo: {element: '#article', on: 'top'},
buttons: [
{
text: `下一步6/${TourConfig.total}`,
action(){
tour.next()
router.push('/articles')
}
}
]
});
const r = localStorage.getItem('tour-guide');
if (settingStore.first && !r) {
tour.start();
}
}, 500)
}
})
useStartKeyboardEventListener()
@@ -248,7 +300,7 @@ function goNextStep(originList, mode, msg) {
}
async function next(isTyping: boolean = true) {
if (isTyping) statStore.inputWordNumber++
if (isTyping) statStore.inputWordNumber++
if (settingStore.wordPracticeMode === WordPracticeMode.Free) {
if (data.index === data.words.length - 1) {
data.wrongWords = data.wrongWords.filter(v => (!data.excludeWords.includes(v.word)))
@@ -350,7 +402,7 @@ async function next(isTyping: boolean = true) {
savePracticeData()
}
function skipStep(){
function skipStep() {
data.index = data.words.length - 1
settingStore.wordPracticeType = WordPracticeType.Spell
data.wrongWords = []
@@ -560,8 +612,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">
@@ -570,7 +622,7 @@ 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>
@@ -579,7 +631,7 @@ useEvents([
@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>
@@ -587,11 +639,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>
@@ -603,41 +655,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>
@@ -649,17 +701,17 @@ 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)"
@skipStep="skipStep"
:is-simple="isWordSimple(word)"
@toggle-simple="toggleWordSimpleWrapper"
:is-collect="isWordCollect(word)"
@toggle-collect="toggleWordCollect(word)"
@skip="next(false)"
@skipStep="skipStep"
/>
</template>
</PracticeLayout>
<Statistics v-model="showStatDialog"/>
<ConflictNotice/>
<ConflictNotice v-if="showConflictNotice"/>
</template>
<style scoped lang="scss">
@@ -686,10 +738,10 @@ useEvents([
@media (max-width: 768px) {
.practice-word {
width: 100%;
.absolute.z-1.top-4 {
z-index: 100; // 提高层级,确保不被遮挡
.center.gap-2.cursor-pointer {
min-height: 44px;
min-width: 44px;
@@ -697,11 +749,11 @@ useEvents([
display: flex;
align-items: center;
justify-content: center;
.word {
pointer-events: none; // 文字不拦截点击
}
.arrow {
pointer-events: none; // 箭头图标不拦截点击
}

View File

@@ -1,29 +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, _nextTick, resourceWrap, shuffle, useNav} from "@/utils";
import { _getAccomplishDate, _getDictDataByUrl, _nextTick, resourceWrap, shuffle, useNav } from "@/utils";
import BasePage from "@/components/BasePage.vue";
import {DictResource, WordPracticeMode} from "@/types/types.ts";
import {onMounted, watch} from "vue";
import {getCurrentStudyWord} from "@/hooks/dict.ts";
import {useRuntimeStore} from "@/stores/runtime.ts";
import { DictResource, WordPracticeMode } from "@/types/types.ts";
import { onMounted, 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 {useFetch} from "@vueuse/core";
import {AppEnv, DICT_LIST, Host, PracticeSaveWordKey, TourConfig} from "@/config/env.ts";
import {myDictList} from "@/apis";
import { useSettingStore } from "@/stores/setting.ts";
import { useFetch } from "@vueuse/core";
import { AppEnv, DICT_LIST, Host, PracticeSaveWordKey, TourConfig } from "@/config/env.ts";
import { myDictList } from "@/apis";
import PracticeWordListDialog from "@/pages/word/components/PracticeWordListDialog.vue";
import ShufflePracticeSettingDialog from "@/pages/word/components/ShufflePracticeSettingDialog.vue";
import introJs from "intro.js";
import Shepherd from "shepherd.js";
@@ -204,6 +203,9 @@ let isNewHost = $ref(window.location.host === Host)
onMounted(() => {
_nextTick(() => {
const tour = new Shepherd.Tour(TourConfig);
tour.on('cancel', () => {
localStorage.setItem('tour-guide', '1');
});
tour.addStep({
id: 'step1',
text: '点击这里选择一本词典开始学习',
@@ -215,15 +217,14 @@ onMounted(() => {
{
text: `下一步1/${TourConfig.total}`,
action() {
// 保存当前步骤(下一页继续执行)
localStorage.setItem('shepherd_step', 'step2');
tour.next()
router.push('/dict-list')
}
}
]
});
tour.start();
const r = localStorage.getItem('tour-guide');
if (settingStore.first && !r) tour.start();
}, 500)
})
</script>
@@ -242,8 +243,8 @@ onMounted(() => {
<IconFluentBookNumber20Filled class="text-xl color-link"/>
</div>
<div
@click="goDictDetail(store.sdict)"
class="text-2xl font-bold cursor-pointer">
@click="goDictDetail(store.sdict)"
class="text-2xl font-bold cursor-pointer">
{{ store.sdict.name || '当前无正在学习的词典' }}
</div>
</div>
@@ -269,9 +270,9 @@ onMounted(() => {
</div>
</BaseButton>
<PopConfirm
:disabled="!isSaveData"
title="当前存在未完成的学习任务,修改会重新生成学习任务,是否继续?"
@confirm="check(()=>showChangeLastPracticeIndexDialog = true)">
:disabled="!isSaveData"
title="当前存在未完成的学习任务,修改会重新生成学习任务,是否继续?"
@confirm="check(()=>showChangeLastPracticeIndexDialog = true)">
<BaseButton type="info"
size="small"
v-if="store.sdict.id"
@@ -320,11 +321,11 @@ onMounted(() => {
</div>
个单词
<PopConfirm
:disabled="!isSaveData"
title="当前存在未完成的学习任务,修改会重新生成学习任务,是否继续?"
@confirm="check(()=>showPracticeSettingDialog = true)">
:disabled="!isSaveData"
title="当前存在未完成的学习任务,修改会重新生成学习任务,是否继续?"
@confirm="check(()=>showPracticeSettingDialog = true)">
<BaseButton
type="info" size="small">更改
type="info" size="small">更改
</BaseButton>
</PopConfirm>
</div>
@@ -358,31 +359,31 @@ onMounted(() => {
</BaseButton>
<div
v-if="false"
class="w-full flex box-border cp color-white">
v-if="false"
class="w-full flex box-border cp color-white">
<div
@click="startPractice"
class="flex-1 rounded-l-lg center gap-2 py-1 bg-[var(--btn-primary)] hover:opacity-50">
@click="startPractice"
class="flex-1 rounded-l-lg center gap-2 py-1 bg-[var(--btn-primary)] hover:opacity-50">
<span class="line-height-[2]">{{ isSaveData ? '继续学习' : '开始学习' }}</span>
<IconFluentArrowCircleRight16Regular class="text-xl"/>
</div>
<div class="relative group">
<div
class="w-10 rounded-r-lg h-full center bg-[var(--btn-primary)] hover:bg-gray border-solid border-2 border-l-gray border-transparent box-border">
class="w-10 rounded-r-lg h-full center bg-[var(--btn-primary)] hover:bg-gray border-solid border-2 border-l-gray border-transparent box-border">
<IconFluentChevronDown20Regular/>
</div>
<div
class="space-y-2 pt-2 absolute z-2 right-0 border rounded opacity-0 scale-95
class="space-y-2 pt-2 absolute z-2 right-0 border rounded opacity-0 scale-95
group-hover:opacity-100 group-hover:scale-100
transition-all duration-150 pointer-events-none group-hover:pointer-events-auto"
>
<div>
<BaseButton
size="large" type="orange"
:loading="loading"
@click="check(()=>showShufflePracticeSettingDialog = true)">
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"/>
@@ -391,9 +392,9 @@ onMounted(() => {
</div>
<div>
<BaseButton
size="large" type="orange"
:loading="loading"
@click="check(()=>showShufflePracticeSettingDialog = true)">
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"/>
@@ -405,10 +406,10 @@ onMounted(() => {
</div>
<BaseButton
v-if="store.sdict.id && store.sdict.lastLearnIndex"
size="large" type="orange"
:loading="loading"
@click="check(()=>showShufflePracticeSettingDialog = true)">
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"/>
@@ -460,23 +461,23 @@ onMounted(() => {
</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"
/>
<ShufflePracticeSettingDialog
v-model="showShufflePracticeSettingDialog"
@ok="onShufflePracticeSettingOk"/>
v-model="showShufflePracticeSettingDialog"
@ok="onShufflePracticeSettingOk"/>
</template>

View File

@@ -122,7 +122,7 @@ const progress = $computed(() => {
<div class="name">总错误数</div>
</div>
</div>
<div class="flex gap-2 justify-center items-center">
<div class="flex gap-2 justify-center items-center" id="toolbar-icons">
<BaseIcon
v-if="statStore.step < 9"
@click="emit('skipStep')"

View File

@@ -58,9 +58,9 @@ watch(() => model.value, (n) => {
<template>
<Dialog v-model="model" title="学习设置" :footer="true"
@ok="changePerDayStudyNumber">
<div class="target-modal color-main">
<div class="target-modal color-main" id="mode">
<div class="center">
<div class="flex gap-4 text-center h-30 w-85" id="mode">
<div class="flex gap-4 text-center h-30 w-85">
<div class="mode-item" :class="temPracticeMode == 0 && 'active'" @click=" temPracticeMode = 0">
<div class="title text-align-center">智能模式</div>
<div class="desc mt-2">自动规划学习复习听写默写</div>

View File

@@ -423,7 +423,7 @@ useEvents([
ref="volumeIconRef" :simple="true" :cb="() => playWordAudio(word.word)"/>
</div>
<div class="word my-1"
<div id="word" class="word my-1"
:class="wrong && 'is-wrong'"
:style="{fontSize: settingStore.fontSize.wordForeignFontSize +'px'}"
@mouseenter="showWord"