feat:add guide & dict test mode
This commit is contained in:
@@ -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
65
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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; // 箭头图标不拦截点击
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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')"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user