This commit is contained in:
Zyronon
2025-11-27 20:09:50 +08:00
committed by GitHub
parent 69cccbf904
commit fb3cca70c8
9 changed files with 247 additions and 106 deletions

View File

@@ -32,6 +32,7 @@
"mitt": "^3.0.1",
"nanoid": "^5.1.5",
"pinia": "^3.0.3",
"shepherd.js": "^14.5.1",
"string-comparison": "^1.3.0",
"vue": "^3.5.17",
"vue-router": "^4.5.1",

View File

@@ -1,6 +1,10 @@
@use "anim" as *;
@use 'intro.js/minified/introjs.min.css';
//@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">
<div class="book relative overflow-hidden" :id="item?.id">
<template v-if="!isAdd">
<div>
<div class="text-base">{{ item?.name }}</div>

View File

@@ -175,6 +175,7 @@ async function cancel() {
<div class="right">
<BaseButton type="info" @click="cancel">{{ cancelButtonText }}</BaseButton>
<BaseButton
id="dialog-ok"
:loading="confirmButtonLoading"
@click="ok">{{ confirmButtonText }}
</BaseButton>

View File

@@ -73,4 +73,16 @@ export const PracticeSaveWordKey = {
export const PracticeSaveArticleKey = {
key: 'PracticeSaveArticle',
version: 1
}
export const TourConfig = {
useModalOverlay: true,
defaultStepOptions: {
canClickTarget: false,
classes: 'shadow-md bg-purple-dark',
cancelIcon: {enabled: true},
modalOverlayOpeningPadding: 10,
modalOverlayOpeningRadius: 6,
},
total: 10
}

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 } 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,22 @@ 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 } 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()
const base = useBaseStore()
@@ -384,6 +385,75 @@ function searchWord() {
console.log('wordForm.word', wordForm.word)
}
watch(() => loading, (val) => {
if (!val) return
_nextTick(() => {
const tour = new Shepherd.Tour(TourConfig);
tour.addStep({
id: 'step3',
text: '点击这里开始学习',
attachTo: {element: '#study', on: 'bottom'},
buttons: [
{
text: `下一步3/${TourConfig.total}`,
action() {
// localStorage.setItem('shepherd_step', 'step3');
tour.next()
addMyStudyList()
}
}
]
});
tour.addStep({
id: 'step4',
text: '点击这里选择学习模式',
attachTo: {element: '#mode', on: 'bottom'},
beforeShowPromise() {
return new Promise((resolve) => {
const timer = setInterval(() => {
if (document.querySelector('#mode')) {
clearInterval(timer);
resolve(true);
}
}, 100);
});
},
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()
}
}
]
});
const stepId = localStorage.getItem('shepherd_step');
if (stepId) {
// localStorage.removeItem('shepherd_step');
tour.start();
tour.show(stepId); // 直接跳到对应步骤
}
},)
})
defineRender(() => {
return (
<BasePage>
@@ -395,7 +465,7 @@ defineRender(() => {
<div class="dict-actions flex">
<BaseButton loading={studyLoading || loading} type="info"
onClick={() => isEdit = true}>编辑</BaseButton>
<BaseButton loading={studyLoading || loading} onClick={addMyStudyList}>学习</BaseButton>
<BaseButton id="study" loading={studyLoading || loading} onClick={addMyStudyList}>学习</BaseButton>
<BaseButton loading={studyLoading || loading} onClick={startTest}>测试</BaseButton>
</div>
</div>

View File

@@ -1,21 +1,22 @@
<script setup lang="ts">
import { 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 } from "vue";
import { getDefaultDict } from "@/types/func.ts";
import { useFetch } from "@vueuse/core";
import { DICT_LIST } 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";
const {nav} = useNav()
const runtimeStore = useRuntimeStore()
@@ -56,7 +57,7 @@ const groupedByCategoryAndTag = $computed(() => {
data.push([key, groupByDictTags(value)])
}
[data[2], data[3]] = [data[3], data[2]];
console.log('data',data)
console.log('data', data)
return data
})
@@ -68,15 +69,46 @@ 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 []
})
watch(dict_list, (val) => {
if (!val.length) return
let cet4 = val.find(v => v.id === 'cet4')
if (!cet4) return
_nextTick(() => {
const tour = new Shepherd.Tour(TourConfig);
tour.addStep({
id: 'step2',
text: '选一本自己准备学习的词典',
attachTo: {element: '#cet4', on: 'bottom'},
buttons: [
{
text: `下一步2/${TourConfig.total}`,
action() {
localStorage.setItem('shepherd_step', 'step3');
tour.next()
selectDict({dict: cet4})
}
}
]
});
const stepId = localStorage.getItem('shepherd_step');
if (stepId) {
// localStorage.removeItem('shepherd_step');
tour.start();
tour.show(stepId); // 直接跳到对应步骤
}
},)
})
</script>
<template>
@@ -91,31 +123,31 @@ const searchList = computed<any[]>(() => {
<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>
@@ -128,37 +160,37 @@ const searchList = computed<any[]>(() => {
.dict-list-page {
padding: 0.8rem;
margin-bottom: 1rem;
.header-section {
flex-direction: column;
gap: 0.5rem;
.flex.flex-1.gap-4 {
width: 100%;
.base-input {
font-size: 0.9rem;
}
.base-button {
padding: 0.5rem 0.8rem;
font-size: 0.9rem;
}
}
.py-1.flex.flex-1.justify-end {
width: 100%;
.page-title {
font-size: 1.2rem;
}
.base-icon {
font-size: 1.2rem;
}
}
}
.mt-4 {
margin-top: 0.8rem;
}
@@ -169,24 +201,24 @@ const searchList = computed<any[]>(() => {
@media (max-width: 480px) {
.dict-list-page {
padding: 0.5rem;
.header-section {
.flex.flex-1.gap-4 {
.base-input {
font-size: 0.8rem;
}
.base-button {
padding: 0.4rem 0.6rem;
font-size: 0.8rem;
}
}
.py-1.flex.flex-1.justify-end {
.page-title {
font-size: 1rem;
}
.base-icon {
font-size: 1rem;
}

View File

@@ -1,29 +1,30 @@
<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 } 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";
const store = useBaseStore()
@@ -202,7 +203,27 @@ let isNewHost = $ref(window.location.host === Host)
onMounted(() => {
_nextTick(() => {
introJs.tour().start();
const tour = new Shepherd.Tour(TourConfig);
tour.addStep({
id: 'step1',
text: '点击这里选择一本词典开始学习',
attachTo: {
element: '#step1',
on: 'bottom'
},
buttons: [
{
text: `下一步1/${TourConfig.total}`,
action() {
// 保存当前步骤(下一页继续执行)
localStorage.setItem('shepherd_step', 'step2');
tour.next()
router.push('/dict-list')
}
}
]
});
tour.start();
}, 500)
})
</script>
@@ -221,8 +242,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>
@@ -248,9 +269,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"
@@ -266,7 +287,7 @@ onMounted(() => {
<div class="flex items-center gap-4 mt-2 flex-1" v-else>
<div class="title">请选择一本词典开始学习</div>
<BaseButton data-intro='点击这里添加一本词典' type="primary" size="large" @click="router.push('/dict-list')">
<BaseButton id="step1" type="primary" size="large" @click="router.push('/dict-list')">
<div class="center gap-1">
<IconFluentAdd16Regular/>
<span>选择词典</span>
@@ -299,11 +320,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>
@@ -337,31 +358,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"/>
@@ -370,9 +391,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"/>
@@ -384,10 +405,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"/>
@@ -439,23 +460,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

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