diff --git a/index.html b/index.html index a0fa74b9..44cc2b81 100644 --- a/index.html +++ b/index.html @@ -78,6 +78,47 @@
你需要启用 JavaScript 来运行 Type Words.
+ diff --git a/package.json b/package.json index a287bb29..d846fb8a 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,6 @@ "@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.12.0", "compromise": "^14.14.4", "copy-to-clipboard": "^3.3.3", @@ -32,7 +31,6 @@ "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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c5187ecd..209833c3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,9 +17,6 @@ importers: '@vueuse/core': specifier: 14.0.0-alpha.0 version: 14.0.0-alpha.0(vue@3.5.18(typescript@5.9.2)) - '@zumer/snapdom': - specifier: ^2.0.0 - version: 2.0.0 axios: specifier: ^1.12.0 version: 1.13.2 @@ -50,9 +47,6 @@ 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 @@ -866,9 +860,6 @@ 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==} @@ -1272,9 +1263,6 @@ packages: peerDependencies: vue: ^3.5.0 - '@zumer/snapdom@2.0.0': - resolution: {integrity: sha512-e/fkm5wCUd+9CssUIyH09xTeR4DvRTmZLGVOlnXLhr4HeI7sdc6ed8cLPiZKFtiQDRiwD3EKx4RIUrpQOJQY7A==} - acorn@8.15.0: resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} engines: {node: '>=0.4.0'} @@ -1807,10 +1795,6 @@ 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'} @@ -3224,10 +3208,6 @@ 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'} @@ -4492,8 +4472,6 @@ 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 @@ -5095,8 +5073,6 @@ snapshots: dependencies: vue: 3.5.18(typescript@5.9.2) - '@zumer/snapdom@2.0.0': {} - acorn@8.15.0: {} address@1.2.2: {} @@ -5683,8 +5659,6 @@ snapshots: dedent@0.7.0: {} - deepmerge-ts@7.1.5: {} - default-compare@1.0.0: dependencies: kind-of: 5.1.0 @@ -7224,12 +7198,6 @@ 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 diff --git a/src/assets/css/shepherd.css b/src/assets/css/shepherd.css new file mode 100644 index 00000000..20312b85 --- /dev/null +++ b/src/assets/css/shepherd.css @@ -0,0 +1,10 @@ +.shepherd-button{background:#3288e6;border:0;border-radius:3px;color:hsla(0,0%,100%,.75);cursor:pointer;margin-right:.5rem;padding:.5rem 1.5rem;transition:all .5s ease}.shepherd-button:not(:disabled):hover{background:#196fcc;color:hsla(0,0%,100%,.75)}.shepherd-button.shepherd-button-secondary{background:#f1f2f3;color:rgba(0,0,0,.75)}.shepherd-button.shepherd-button-secondary:not(:disabled):hover{background:#d6d9db;color:rgba(0,0,0,.75)}.shepherd-button:disabled{cursor:not-allowed} +.shepherd-footer{border-bottom-left-radius:5px;border-bottom-right-radius:5px;display:flex;justify-content:flex-end;padding:0 .75rem .75rem}.shepherd-footer .shepherd-button:last-child{margin-right:0} +.shepherd-cancel-icon{background:transparent;border:none;color:hsla(0,0%,50%,.75);cursor:pointer;font-size:2em;font-weight:400;margin:0;padding:0;transition:color .5s ease}.shepherd-cancel-icon:hover{color:rgba(0,0,0,.75)}.shepherd-has-title .shepherd-content .shepherd-cancel-icon{color:hsla(0,0%,50%,.75)}.shepherd-has-title .shepherd-content .shepherd-cancel-icon:hover{color:rgba(0,0,0,.75)} +.shepherd-title{color:rgba(0,0,0,.75);display:flex;flex:1 0 auto;font-size:1rem;font-weight:400;margin:0;padding:0} +.shepherd-header{align-items:center;border-top-left-radius:5px;border-top-right-radius:5px;display:flex;justify-content:flex-end;line-height:2em;padding:.75rem .75rem 0}.shepherd-has-title .shepherd-content .shepherd-header{background:#e6e6e6;padding:1em} +.shepherd-text{color:rgba(0,0,0,.75);font-size:1rem;line-height:1.3em;padding:.75em}.shepherd-text p{margin-top:0}.shepherd-text p:last-child{margin-bottom:0} +.shepherd-content{border-radius:5px;outline:none;padding:0} +.shepherd-element{background:#fff;border:none;border-radius:5px;box-shadow:0 1px 4px rgba(0,0,0,.2);margin:0;max-width:400px;opacity:0;outline:none;padding:0;transition:opacity .3s,visibility .3s;visibility:hidden;width:100%;z-index:9999}.shepherd-enabled.shepherd-element{opacity:1;visibility:visible}.shepherd-element[data-popper-reference-hidden]:not(.shepherd-centered){opacity:0;pointer-events:none;visibility:hidden}.shepherd-element,.shepherd-element *,.shepherd-element :after,.shepherd-element :before{box-sizing:border-box}.shepherd-arrow,.shepherd-arrow:before{height:16px;position:absolute;width:16px;z-index:-1}.shepherd-arrow:before{background:#fff;content:"";transform:rotate(45deg)}.shepherd-element[data-popper-placement^=top]>.shepherd-arrow{bottom:-8px}.shepherd-element[data-popper-placement^=bottom]>.shepherd-arrow{top:-8px}.shepherd-element[data-popper-placement^=left]>.shepherd-arrow{right:-8px}.shepherd-element[data-popper-placement^=right]>.shepherd-arrow{left:-8px}.shepherd-element.shepherd-centered>.shepherd-arrow{opacity:0}.shepherd-element.shepherd-has-title[data-popper-placement^=bottom]>.shepherd-arrow:before{background-color:#e6e6e6}.shepherd-target-click-disabled.shepherd-enabled.shepherd-target,.shepherd-target-click-disabled.shepherd-enabled.shepherd-target *{pointer-events:none} +.shepherd-modal-overlay-container{height:0;left:0;opacity:0;overflow:hidden;pointer-events:none;position:fixed;top:0;transition:all .3s ease-out,height 0s .3s,opacity .3s 0s;width:100vw;z-index:9997}.shepherd-modal-overlay-container.shepherd-modal-is-visible{height:100vh;opacity:.5;transform:translateZ(0);transition:all .3s ease-out,height 0s 0s,opacity .3s 0s}.shepherd-modal-overlay-container.shepherd-modal-is-visible path{pointer-events:all} + diff --git a/src/assets/css/style.scss b/src/assets/css/style.scss index 37df52c9..35be7305 100644 --- a/src/assets/css/style.scss +++ b/src/assets/css/style.scss @@ -1,5 +1,5 @@ @use "anim" as *; -@use 'shepherd.js/dist/css/shepherd.css'; +@use 'shepherd.css'; :root { --color-reverse-white: white; diff --git a/src/components/ChannelIcons.vue b/src/components/ChannelIcons.vue index beb44804..2b22124c 100644 --- a/src/components/ChannelIcons.vue +++ b/src/components/ChannelIcons.vue @@ -1,15 +1,14 @@ @@ -222,15 +225,5 @@ onMounted(() => { box-shadow: 0 0 5px #409eff; cursor: grabbing; } - - &__value { - position: absolute; - top: 100%; - left: 50%; - transform: translate(-50%, 4px); - font-size: 0.75rem; - color: #666; - user-select: none; - } } diff --git a/src/config/env.ts b/src/config/env.ts index 85c26557..4c74899a 100644 --- a/src/config/env.ts +++ b/src/config/env.ts @@ -84,8 +84,15 @@ export const TourConfig = { modalOverlayOpeningPadding: 10, modalOverlayOpeningRadius: 6, floatingUIOptions: { - middleware: [offset({mainAxis:30})] + middleware: [offset({mainAxis: 30})] }, }, total: 7 +} + +export const LIB_JS_URL = { + SHEPHERD: import.meta.env.MODE === 'development' ? + 'https://cdn.jsdelivr.net/npm/shepherd.js@14.5.1/dist/esm/shepherd.mjs' + : Origin + '/libs/Shepherd.14.5.1.mjs', + SNAPDOM: `${Origin}/libs/snapdom.min.js` } \ No newline at end of file diff --git a/src/hooks/export.ts b/src/hooks/export.ts index 3a2fc2fe..7ecc7d48 100644 --- a/src/hooks/export.ts +++ b/src/hooks/export.ts @@ -27,59 +27,64 @@ export function useExport() { async function exportData(notice = '导出成功!') { if (loading.value) return loading.value = true - const JSZip = await loadJsLib('JSZip', `${Origin}/libs/jszip.min.js`); - let data = { - version: EXPORT_DATA_KEY.version, - val: { - setting: { - version: SAVE_SETTING_KEY.version, - val: settingStore.$state - }, - dict: { - version: SAVE_DICT_KEY.version, - val: shakeCommonDict(store.$state) - }, - [PracticeSaveWordKey.key]: { - version: PracticeSaveWordKey.version, - val: {} - }, - [PracticeSaveArticleKey.key]: { - version: PracticeSaveArticleKey.version, - val: {} - }, - [APP_VERSION.key]: -1 + try { + const JSZip = await loadJsLib('JSZip', `${Origin}/libs/jszip.min.js`); + let data = { + version: EXPORT_DATA_KEY.version, + val: { + setting: { + version: SAVE_SETTING_KEY.version, + val: settingStore.$state + }, + dict: { + version: SAVE_DICT_KEY.version, + val: shakeCommonDict(store.$state) + }, + [PracticeSaveWordKey.key]: { + version: PracticeSaveWordKey.version, + val: {} + }, + [PracticeSaveArticleKey.key]: { + version: PracticeSaveArticleKey.version, + val: {} + }, + [APP_VERSION.key]: -1 + } } - } - let d = localStorage.getItem(PracticeSaveWordKey.key) - if (d) { - try { - data.val[PracticeSaveWordKey.key] = JSON.parse(d) - } catch (e) { + let d = localStorage.getItem(PracticeSaveWordKey.key) + if (d) { + try { + data.val[PracticeSaveWordKey.key] = JSON.parse(d) + } catch (e) { + } } - } - let d1 = localStorage.getItem(PracticeSaveArticleKey.key) - if (d1) { - try { - data.val[PracticeSaveArticleKey.key] = JSON.parse(d1) - } catch (e) { + let d1 = localStorage.getItem(PracticeSaveArticleKey.key) + if (d1) { + try { + data.val[PracticeSaveArticleKey.key] = JSON.parse(d1) + } catch (e) { + } } - } - let r = await get(APP_VERSION.key) - data.val[APP_VERSION.key] = r + let r = await get(APP_VERSION.key) + data.val[APP_VERSION.key] = r - const zip = new JSZip(); - zip.file("data.json", JSON.stringify(data)); + const zip = new JSZip(); + zip.file("data.json", JSON.stringify(data)); - const mp3 = zip.folder("mp3"); - const allRecords = await get(LOCAL_FILE_KEY); - for (const rec of allRecords ?? []) { - mp3.file(rec.id + ".mp3", rec.file); + const mp3 = zip.folder("mp3"); + const allRecords = await get(LOCAL_FILE_KEY); + for (const rec of allRecords ?? []) { + mp3.file(rec.id + ".mp3", rec.file); + } + let content = await zip.generateAsync({type: "blob"}) + saveAs(content, `${APP_NAME}-User-Data-${dayjs().format('YYYY-MM-DD HH-mm-ss')}.zip`); + notice && Toast.success(notice) + return content + } catch (e) { + Toast.error(e?.message || e || '导出失败') + } finally { + loading.value = false } - let content = await zip.generateAsync({type: "blob"}) - saveAs(content, `${APP_NAME}-User-Data-${dayjs().format('YYYY-MM-DD HH-mm-ss')}.zip`); - notice && Toast.success(notice) - loading.value = false - return content } return { diff --git a/src/pages/article/ArticlesPage.vue b/src/pages/article/ArticlesPage.vue index 7aa77d7d..da80f918 100644 --- a/src/pages/article/ArticlesPage.vue +++ b/src/pages/article/ArticlesPage.vue @@ -2,7 +2,16 @@ import { useBaseStore } from "@/stores/base.ts"; import { useRouter } from "vue-router"; import BasePage from "@/components/BasePage.vue"; -import {_getDictDataByUrl, _nextTick, isMobile, msToHourMinute, resourceWrap, total, useNav} from "@/utils"; +import { + _getDictDataByUrl, + _nextTick, + isMobile, + loadJsLib, + 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,9 +27,8 @@ 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, TourConfig } from "@/config/env.ts"; +import { AppEnv, DICT_LIST, Host, LIB_JS_URL, PracticeSaveArticleKey, TourConfig } from "@/config/env.ts"; import { myDictList } from "@/apis"; -import Shepherd from "shepherd.js"; import { useSettingStore } from "@/stores/setting.ts"; dayjs.extend(isoWeek) @@ -57,9 +65,9 @@ async function init() { let data = obj.val //如果全是0,说明未进行练习,直接重置 if ( - data.practiceData.sectionIndex === 0 && - data.practiceData.sentenceIndex === 0 && - data.practiceData.wordIndex === 0 + data.practiceData.sectionIndex === 0 && + data.practiceData.sentenceIndex === 0 && + data.practiceData.wordIndex === 0 ) { throw new Error() } @@ -73,7 +81,8 @@ async function init() { watch(() => store?.sbook?.id, (n) => { console.log('n', n) if (!n) { - _nextTick(() => { + _nextTick(async () => { + const Shepherd = await loadJsLib('Shepherd', LIB_JS_URL.SHEPHERD); const tour = new Shepherd.Tour(TourConfig); tour.on('cancel', () => { localStorage.setItem('tour-guide', '1'); @@ -112,6 +121,13 @@ function startStudy() { if (!base.sbook.articles.length) { return Toast.warning('没有文章可学习!') } + window.umami?.track('startStudyArticle', { + name: base.sbook.name, + index: base.sbook.lastLearnIndex, + custom: base.sbook.custom, + complete: base.sbook.complete, + title: base.sbook.articles[base.sbook.lastLearnIndex].title + }) nav('/practice-articles/' + store.sbook.id) } else { window.umami?.track('no-book') @@ -213,12 +229,12 @@ let isNewHost = $ref(window.location.host === Host)
+ v-if="base.sbook.id" + :is-add="false" + quantifier="篇" + :item="base.sbook" + :show-progress="false" + @click="goBookDetail(base.sbook)"/> @@ -228,27 +244,27 @@ let isNewHost = $ref(window.location.host === Host)
本周学习记录
{{ i + 1 }}
+ class="w-full sm:flex-1 rounded-xl p-4 box-border relative bg-[var(--bg-history)] border border-gray-200">
{{ todayTotalSpend }}
今日学习时长
+ class="w-full sm:flex-1 rounded-xl p-4 box-border relative bg-[var(--bg-history)] border border-gray-200">
{{ totalDay }}
总学习天数
+ class="w-full sm:flex-1 rounded-xl p-4 box-border relative bg-[var(--bg-history)] border border-gray-200">
{{ totalSpend }}
总学习时长
diff --git a/src/pages/article/PracticeArticles.vue b/src/pages/article/PracticeArticles.vue index 05d8b997..3a083e55 100644 --- a/src/pages/article/PracticeArticles.vue +++ b/src/pages/article/PracticeArticles.vue @@ -18,7 +18,7 @@ import { import { useDisableEventListener, useOnKeyboardEventListener, useStartKeyboardEventListener } from "@/hooks/event.ts"; import useTheme from "@/hooks/theme.ts"; import Toast from '@/components/base/toast/Toast.ts' -import {_getDictDataByUrl, _nextTick, cloneDeep, isMobile, msToMinute, resourceWrap, total} from "@/utils"; +import { _getDictDataByUrl, _nextTick, cloneDeep, isMobile, loadJsLib, msToMinute, resourceWrap, total } from "@/utils"; import { usePracticeStore } from "@/stores/practice.ts"; import { useArticleOptions } from "@/hooks/dict.ts"; import { genArticleSectionData, usePlaySentenceAudio } from "@/hooks/article.ts"; @@ -34,10 +34,9 @@ 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, TourConfig } from "@/config/env.ts"; +import { AppEnv, DICT_LIST, LIB_JS_URL, 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() @@ -160,7 +159,8 @@ watch([() => store.load, () => loading], ([a, b]) => { watch(() => articleData?.article?.id, id => { if (id) { - _nextTick(() => { + _nextTick(async () => { + const Shepherd = await loadJsLib('Shepherd', LIB_JS_URL.SHEPHERD); const tour = new Shepherd.Tour(TourConfig); tour.on('cancel', () => { localStorage.setItem('tour-guide', '1'); @@ -295,20 +295,8 @@ function setArticle(val: Article) { }, 1000) _nextTick(typingArticleRef?.init) - - window.umami?.track('startStudyArticle', { - name: store.sbook.name, - index: store.sbook.lastLearnIndex, - custom: store.sbook.custom, - complete: store.sbook.complete, - title: articleData.article.title, - }) } -watch(() => articleData.article.id, n => { - console.log('articleData.article.id', n) -}) - async function complete() { clearInterval(timer) setTimeout(() => { diff --git a/src/pages/layout.vue b/src/pages/layout.vue index 891ee676..c10d0d75 100644 --- a/src/pages/layout.vue +++ b/src/pages/layout.vue @@ -47,7 +47,7 @@ function goHome() {
- 建议反馈 + 反馈
diff --git a/src/pages/setting/Setting.vue b/src/pages/setting/Setting.vue index 81f93fff..3f2338f8 100644 --- a/src/pages/setting/Setting.vue +++ b/src/pages/setting/Setting.vue @@ -1,14 +1,14 @@