Merge branch 'dev'

This commit is contained in:
Zyronon
2025-11-30 02:25:35 +08:00
20 changed files with 1113 additions and 797 deletions

View File

@@ -78,6 +78,47 @@
<div>你需要启用 JavaScript 来运行 Type Words.</div>
</noscript>
<div id="app"></div>
<script>
(function(){
var ua = navigator.userAgent || ''
var isIE = !!document.documentMode || /MSIE|Trident/i.test(ua)
if (!isIE) return
var style = document.createElement('style')
style.type = 'text/css'
style.appendChild(document.createTextNode(
'.ie-mask{position:fixed;left:0;top:0;right:0;bottom:0;background:rgba(0,0,0,.35);z-index:9998}'+
'.ie-dialog{position:fixed;left:50%;top:50%;transform:translate(-50%,-50%);width:28rem;max-width:90vw;background:#fff;color:#111;border-radius:.6rem;box-shadow:0 10px 30px rgba(0,0,0,.15);z-index:9999;padding:1.2rem}'+
'.ie-dialog .title{font-size:1.2rem;font-weight:700;margin-bottom:.6rem}'+
'.ie-dialog .desc{font-size:.95rem;line-height:1.6;color:#555}'+
'.ie-dialog .actions{display:flex;justify-content:flex-end;margin-top:1rem}'+
'.ie-dialog .actions > * + *{margin-left:.6rem}'+
'.ie-dialog .btn{display:inline-flex;align-items:center;justify-content:center;height:2.2rem;padding:0 1rem;border-radius:.4rem;background:#0C8CE9;color:#fff;text-decoration:none}'+
'.ie-dialog .btn-secondary{display:inline-flex;align-items:center;justify-content:center;height:2.2rem;padding:0 .9rem;border-radius:.4rem;background:#eee;color:#333;border:1px solid #ddd}'+
'@media (prefers-color-scheme: dark){.ie-dialog{background:#1e1f22;color:#e6e6e6}.ie-dialog .desc{color:#c6c6c6}.ie-dialog .btn-secondary{background:#2a2b2f;color:#e6e6e6;border-color:#3a3b3f}}'
))
document.head.appendChild(style)
var mask = document.createElement('div')
mask.className = 'ie-mask'
var dialog = document.createElement('div')
dialog.className = 'ie-dialog'
dialog.innerHTML = '<div class="title">不支持 IE 浏览器</div>'+
'<div class="desc">Type Words 使用现代技术构建,请使用 Chrome、Edge、Firefox 或 Safari 等现代浏览器访问。</div>'+
'<div class="actions">'+
'<a class="btn" href="https://www.google.cn/chrome/" target="_blank" rel="noreferrer">下载 Chrome</a>'+
'<button class="btn-secondary" type="button">我知道了</button>'+
'</div>'
function close(){
try{document.body.removeChild(mask)}catch(e){}
try{document.body.removeChild(dialog)}catch(e){}
}
mask.addEventListener('click', close)
var btn = null
try{btn = dialog.querySelector('.btn-secondary')}catch(e){}
if (btn) btn.addEventListener('click', close)
document.body.appendChild(mask)
document.body.appendChild(dialog)
})()
</script>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@@ -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",

32
pnpm-lock.yaml generated
View File

@@ -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

View File

@@ -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}

View File

@@ -1,5 +1,5 @@
@use "anim" as *;
@use 'shepherd.js/dist/css/shepherd.css';
@use 'shepherd.css';
:root {
--color-reverse-white: white;

View File

@@ -1,15 +1,14 @@
<script setup lang="ts">
import { APP_NAME, GITHUB, Origin } from "@/config/env.ts";
import { APP_NAME, GITHUB, LIB_JS_URL, Origin } from "@/config/env.ts";
import BaseIcon from "@/components/BaseIcon.vue";
import { defineAsyncComponent, watch } from "vue";
import { usePracticeStore } from "@/stores/practice.ts";
import { useBaseStore } from "@/stores/base.ts";
import { msToHourMinute } from "@/utils";
import { loadJsLib, msToHourMinute } from "@/utils";
import dayjs from "dayjs";
import Toast from "@/components/base/toast/Toast.ts";
import { useUserStore } from "@/stores/user.ts";
import Progress from "@/components/base/Progress.vue";
import { snapdom } from "@zumer/snapdom";
const Dialog = defineAsyncComponent(() => import('@/components/dialog/Dialog.vue'))
@@ -21,6 +20,8 @@ let showWechatDialog = $ref(false)
let showXhsDialog = $ref(false)
let showQQDialog = $ref(false)
let showShareDialog = $ref(false)
let loading1 = $ref(false)
let loading2 = $ref(false)
let posterEl = $ref<HTMLDivElement | null>(null)
// 计算学习统计数据
@@ -47,6 +48,8 @@ watch(() => showShareDialog, (newVal) => {
// 复制图片到剪贴板
async function copyImageToClipboard() {
try {
loading1 = true
const snapdom = await loadJsLib('snapdom',LIB_JS_URL.SNAPDOM);
const blob = await snapdom.toBlob(posterEl, {scale: 2, type: 'png'})
if (!blob) throw new Error('capture failed')
@@ -59,12 +62,17 @@ async function copyImageToClipboard() {
} catch (error) {
Toast.error('复制失败!')
await downloadImage()
} finally {
loading1 = false
}
}
// 下载图片
async function downloadImage() {
loading2 = true
const snapdom = await loadJsLib('snapdom',LIB_JS_URL.SNAPDOM);
snapdom.download(posterEl, {scale: 2})
loading2 = false
}
let imgIndex = $ref(Math.floor(Math.random() * 10))
@@ -127,7 +135,8 @@ const sentence = $computed(() => {
<IconFluentShare20Regular class="text-blue-500 hover:text-blue-600"/>
</BaseIcon>
<a :href="GITHUB" target="_blank" rel="noreferrer" aria-label="GITHUB 项目地址" class="color-[--color-reverse-black]">
<a :href="GITHUB" target="_blank" rel="noreferrer" aria-label="GITHUB 项目地址"
class="color-[--color-reverse-black]">
<BaseIcon>
<IconSimpleIconsGithub/>
</BaseIcon>
@@ -264,13 +273,15 @@ const sentence = $computed(() => {
<!-- 分享战绩 -->
<div @click="copyImageToClipboard"
class="flex items-center justify-start gap-space px-6 py-3 bg-gradient-to-r from-green-500 to-green-600 text-white cp rounded-lg hover:from-green-600 hover:to-green-700 transition-all duration-200">
<IconFluentCopy20Regular class="w-5 h-5"/>
<IconEosIconsLoading class="text-xl" v-if="loading1"/>
<IconFluentCopy20Regular class="w-5 h-5" v-else/>
<span class="font-medium">复制到剪贴板</span>
</div>
<div @click="downloadImage"
class="flex items-center justify-start gap-space px-6 py-3 bg-gradient-to-r from-purple-500 to-purple-600 text-white cp rounded-lg hover:from-purple-600 hover:to-purple-700 transition-all duration-200">
<IconFluentArrowDownload20Regular class="w-5 h-5"/>
<IconEosIconsLoading class="text-xl" v-if="loading2"/>
<IconFluentArrowDownload20Regular class="w-5 h-5" v-else/>
<span class="font-medium">保存高清海报</span>
</div>
</div>

View File

@@ -9,6 +9,7 @@ const props = defineProps<{
disabled?: boolean;
showText?: boolean;
showValue?: boolean; // 是否显示当前值
unit?: string
}>();
const emit = defineEmits(['update:modelValue']);
@@ -134,20 +135,21 @@ onMounted(() => {
</script>
<template>
<div class="w-full">
<div
<div class="w-full flex">
<div class="flex-1">
<div
ref="sliderRef"
class="custom-slider"
:class="{ 'is-disabled': disabled }"
@mousedown="onClickTrack"
@touchstart.prevent="onClickTrack"
>
<div class="custom-slider__track"></div>
<div
>
<div class="custom-slider__track"></div>
<div
class="custom-slider__fill"
:style="{ width: valueToPercent(currentValue) + '%' }"
></div>
<div
></div>
<div
class="custom-slider__thumb"
:style="{ left: valueToPercent(currentValue) + '%' }"
@mousedown.stop.prevent="onMouseDown"
@@ -158,13 +160,14 @@ onMounted(() => {
:aria-valuemax="max"
:aria-valuenow="currentValue"
:aria-disabled="disabled"
></div>
<div v-if="showValue" class="custom-slider__value">{{ currentValue }}</div>
</div>
<div class="text flex justify-between text-sm color-gray" v-if="showText">
<span>{{ min }}</span>
<span>{{ max }}</span>
></div>
</div>
<div class="text flex justify-between text-sm color-gray" v-if="showText">
<span>{{ min }}</span>
<span>{{ max }}</span>
</div>
</div>
<div v-if="showValue" class="w-10 pl-5 ">{{ currentValue }}{{ unit}}</div>
</div>
</template>
@@ -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;
}
}
</style>

View File

@@ -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`
}

View File

@@ -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 {

View File

@@ -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)
<div class="card flex flex-col md:flex-row justify-between gap-space p-4 md:p-6">
<div class="">
<Book
v-if="base.sbook.id"
:is-add="false"
quantifier="篇"
:item="base.sbook"
:show-progress="false"
@click="goBookDetail(base.sbook)"/>
v-if="base.sbook.id"
:is-add="false"
quantifier="篇"
:item="base.sbook"
:show-progress="false"
@click="goBookDetail(base.sbook)"/>
<Book v-else
:is-add="true"
@click="router.push('/book-list')"/>
@@ -228,27 +244,27 @@ let isNewHost = $ref(window.location.host === Host)
<div class="title mr-4 truncate">本周学习记录</div>
<div class="flex gap-4 color-gray">
<div
class="w-6 h-6 md:w-8 md:h-8 rounded-md center text-sm md:text-base"
:class="item ? 'bg-[#409eff] color-white' : 'bg-gray-200'"
v-for="(item, i) in weekList"
:key="i"
class="w-6 h-6 md:w-8 md:h-8 rounded-md center text-sm md:text-base"
:class="item ? 'bg-[#409eff] color-white' : 'bg-gray-200'"
v-for="(item, i) in weekList"
:key="i"
>{{ i + 1 }}
</div>
</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">
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">
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">
class="w-full sm:flex-1 rounded-xl p-4 box-border relative bg-[var(--bg-history)] border border-gray-200">
<div class="text-[#409eff] text-xl font-bold">{{ totalSpend }}</div>
<div class="text-gray-500">总学习时长</div>
</div>

View File

@@ -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(() => {

View File

@@ -47,7 +47,7 @@ function goHome() {
</div>
<div class="row" @click="jump2Feedback">
<IconFluentCommentEdit20Regular/>
<span v-if="settingStore.sideExpand">建议反馈</span>
<span v-if="settingStore.sideExpand">反馈</span>
</div>
<!-- <div class="row" @click="router.push('/user')">-->
<!-- <IconFluentPerson20Regular/>-->

File diff suppressed because it is too large Load Diff

View File

@@ -33,7 +33,7 @@ defineProps<{
.setting-item__main {
display: flex;
align-items: center;
gap: 2.5rem;
gap: 2rem;
width: 100%;
}

View File

@@ -4,7 +4,7 @@ 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 { _getDictDataByUrl, _nextTick, convertToWord, isMobile, loadJsLib, useNav } from "@/utils";
import { nanoid } from "nanoid";
import BaseIcon from "@/components/BaseIcon.vue";
import BaseTable from "@/components/BaseTable.vue";
@@ -26,9 +26,8 @@ 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 { AppEnv, LIB_JS_URL, Origin, PracticeSaveWordKey, TourConfig } from "@/config/env.ts";
import { detail } from "@/apis";
import Shepherd from "shepherd.js";
const runtimeStore = useRuntimeStore()
const base = useBaseStore()
@@ -388,7 +387,8 @@ function searchWord() {
watch(() => loading, (val) => {
if (!val) return
_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');

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import {_nextTick, groupBy, isMobile, resourceWrap, useNav} from "@/utils";
import { _nextTick, groupBy, isMobile, loadJsLib, resourceWrap, useNav } from "@/utils";
import BasePage from "@/components/BasePage.vue";
import { DictResource } from "@/types/types.ts";
import { useRuntimeStore } from "@/stores/runtime.ts";
@@ -11,12 +11,11 @@ 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 { computed, watch } from "vue";
import { getDefaultDict } from "@/types/func.ts";
import { useFetch } from "@vueuse/core";
import { DICT_LIST, TourConfig } from "@/config/env.ts";
import { DICT_LIST, LIB_JS_URL, 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()
@@ -84,7 +83,8 @@ watch(dict_list, (val) => {
if (!val.length) return
let cet4 = val.find(v => v.id === 'cet4')
if (!cet4) return
_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');

View File

@@ -1,15 +1,15 @@
<script setup lang="ts">
import { onMounted, provide, ref, toRef, watch } from "vue";
import { onMounted, provide, ref, 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 { Dict, PracticeData, ShortcutKey, TaskWords, Word, WordPracticeMode, WordPracticeType } 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, _nextTick, cloneDeep, isMobile, resourceWrap, shuffle} from "@/utils";
import { _getDictDataByUrl, _nextTick, cloneDeep, isMobile, loadJsLib, resourceWrap, shuffle } from "@/utils";
import { useRoute, useRouter } from "vue-router";
import Footer from "@/pages/word/components/Footer.vue";
import Panel from "@/components/Panel.vue";
@@ -25,11 +25,9 @@ import { getDefaultDict, getDefaultWord } from "@/types/func.ts";
import ConflictNotice from "@/components/ConflictNotice.vue";
import PracticeLayout from "@/components/PracticeLayout.vue";
import { DICT_LIST, PracticeSaveWordKey, TourConfig } from "@/config/env.ts";
import { DICT_LIST, LIB_JS_URL, 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,
@@ -117,7 +115,8 @@ onMounted(() => {
watchOnce(() => data.words.length, (newVal, oldVal) => {
//如果是从无值变有值,代表是开始
if (!oldVal && newVal) {
_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');

View File

@@ -2,10 +2,19 @@
import { useBaseStore } from "@/stores/base.ts";
import { useRouter } from "vue-router";
import BaseIcon from "@/components/BaseIcon.vue";
import {_getAccomplishDate, _getDictDataByUrl, _nextTick, isMobile, resourceWrap, shuffle, useNav} from "@/utils";
import {
_getAccomplishDate,
_getDictDataByUrl,
_nextTick,
isMobile,
loadJsLib,
resourceWrap,
shuffle,
useNav
} from "@/utils";
import BasePage from "@/components/BasePage.vue";
import { DictResource, WordPracticeMode } from "@/types/types.ts";
import { onMounted, watch } from "vue";
import { watch } from "vue";
import { getCurrentStudyWord } from "@/hooks/dict.ts";
import { useRuntimeStore } from "@/stores/runtime.ts";
import Book from "@/components/Book.vue";
@@ -19,11 +28,11 @@ import PracticeSettingDialog from "@/pages/word/components/PracticeSettingDialog
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 { AppEnv, DICT_LIST, Host, LIB_JS_URL, 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 Shepherd from "shepherd.js";
import SettingDialog from "@/pages/word/components/SettingDialog.vue";
const store = useBaseStore()
@@ -42,7 +51,35 @@ let currentStudy = $ref({
})
watch(() => store.load, n => {
if (n) init()
if (n) {
init()
_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');
});
tour.addStep({
id: 'step1',
text: '点击这里选择一本词典开始学习',
attachTo: {
element: '#step1',
on: 'bottom'
},
buttons: [
{
text: `下一步1/${TourConfig.total}`,
action() {
tour.next()
router.push('/dict-list')
}
}
]
});
const r = localStorage.getItem('tour-guide');
if (settingStore.first && !r && !isMobile()) tour.start();
}, 500)
}
}, {immediate: true})
async function init() {
@@ -200,33 +237,6 @@ const {
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: '点击这里选择一本词典开始学习',
attachTo: {
element: '#step1',
on: 'bottom'
},
buttons: [
{
text: `下一步1/${TourConfig.total}`,
action() {
tour.next()
router.push('/dict-list')
}
}
]
});
const r = localStorage.getItem('tour-guide');
if (settingStore.first && !r && !isMobile()) tour.start();
}, 500)
})
</script>
<template>
@@ -236,6 +246,8 @@ onMounted(() => {
2study.top 域名将在不久后停止使用
</div>
<!-- <SettingDialog/>-->
<div class="card flex flex-col md:flex-row gap-8">
<div class="flex-1 w-full flex flex-col justify-between">
<div class="flex gap-3">

View File

@@ -0,0 +1,332 @@
<script setup lang="ts">
import {useSettingStore} from "@/stores/setting.ts";
import {getAudioFileUrl, usePlayAudio} from "@/hooks/sound.ts";
import {ShortcutKey, WordPracticeMode} from "@/types/types.ts";
import VolumeIcon from "@/components/icon/VolumeIcon.vue";
import {useBaseStore} from "@/stores/base.ts";
import {SoundFileOptions} from "@/config/env.ts";
import BasePage from "@/components/BasePage.vue";
import {Option, Select} from "@/components/base/select";
import Switch from "@/components/base/Switch.vue";
import Slider from "@/components/base/Slider.vue";
import RadioGroup from "@/components/base/radio/RadioGroup.vue";
import Radio from "@/components/base/radio/Radio.vue";
import InputNumber from "@/components/base/InputNumber.vue";
import Textarea from "@/components/base/Textarea.vue";
import SettingItem from "@/pages/setting/SettingItem.vue";
import {useRuntimeStore} from "@/stores/runtime.ts";
const tabIndex = $ref(1)
const settingStore = useSettingStore()
const runtimeStore = useRuntimeStore()
const store = useBaseStore()
const simpleWords = $computed({
get: () => store.simpleWords.join(','),
set: v => {
try {
store.simpleWords = v.split(',');
} catch (e) {
}
}
})
</script>
<template>
<BasePage>
<div class="setting text-lg w-200 h-200 bg-white text-md flex flex-col">
<div class="page-title text-align-center">设置</div>
<div class="flex flex-1 overflow-hidden">
<div class="left">
<div class="tabs">
<div class="tab" :class="tabIndex === 1 && 'active'" @click="tabIndex = 1">
<IconFluentTextUnderlineDouble20Regular width="20"/>
<span>单词练习设置</span>
</div>
<div class="tab" :class="tabIndex === 2 && 'active'" @click="tabIndex = 2">
<IconFluentBookLetter20Regular width="20"/>
<span>文章练习设置</span>
</div>
<div class="tab" :class="tabIndex === 0 && 'active'" @click="tabIndex = 0">
<IconFluentSettings20Regular width="20"/>
<span>通用练习设置</span>
</div>
</div>
</div>
<div class="content">
<!-- 通用练习设置-->
<!-- 通用练习设置-->
<!-- 通用练习设置-->
<div v-if="tabIndex === 0">
<SettingItem title="忽略大小写"
desc="开启后输入时不区分大小写如输入“hello”和“Hello”都会被认为是正确的"
>
<Switch v-model="settingStore.ignoreCase"/>
</SettingItem>
<SettingItem title="允许默写模式下显示提示"
:desc="`开启后,可以通过将鼠标移动到单词上或者按快捷键 ${settingStore.shortcutKeyMap[ShortcutKey.ShowWord]} 显示正确答案`"
>
<Switch v-model="settingStore.allowWordTip"/>
</SettingItem>
<div class="line"></div>
<SettingItem title="简单词过滤"
desc="开启后,练习的单词中不会包含简单词;文章统计的总词数中不会包含简单词"
>
<Switch v-model="settingStore.ignoreSimpleWord"/>
</SettingItem>
<SettingItem title="简单词列表"
class="items-start!"
v-if="settingStore.ignoreSimpleWord"
>
<Textarea
placeholder="多个单词用英文逗号隔号"
v-model="simpleWords" :autosize="{minRows: 6, maxRows: 10}"/>
</SettingItem>
<!-- 音效-->
<!-- 音效-->
<!-- 音效-->
<div class="line"></div>
<SettingItem main-title="音效"/>
<SettingItem title="单词/句子发音口音">
<Select v-model="settingStore.soundType"
placeholder="请选择"
class="w-50!"
>
<Option label="美音" value="us"/>
<Option label="英音" value="uk"/>
</Select>
</SettingItem>
<div class="line"></div>
<SettingItem title="按键音">
<Switch v-model="settingStore.keyboardSound"/>
</SettingItem>
<SettingItem title="按键音效">
<Select v-model="settingStore.keyboardSoundFile"
placeholder="请选择"
class="w-50!"
>
<Option
v-for="item in SoundFileOptions"
:key="item.value"
:label="item.label"
:value="item.value"
>
<div class="flex justify-between items-center w-full">
<span>{{ item.label }}</span>
<VolumeIcon
:time="100"
@click="usePlayAudio(getAudioFileUrl(item.value)[0])"/>
</div>
</Option>
</Select>
</SettingItem>
<SettingItem title="音量">
<Slider v-model="settingStore.keyboardSoundVolume" showText showValue unit="%"/>
</SettingItem>
</div>
<!-- 单词练习设置-->
<!-- 单词练习设置-->
<!-- 单词练习设置-->
<div v-if="tabIndex === 1">
<SettingItem title="练习模式">
<RadioGroup v-model="settingStore.wordPracticeMode" class="flex-col gap-0!">
<Radio :value="WordPracticeMode.System" label="智能模式:自动规划学习、复习、听写、默写"/>
<Radio :value="WordPracticeMode.Free" label="自由模式:系统不强制复习与默写"/>
</RadioGroup>
</SettingItem>
<SettingItem title="显示上一个/下一个单词"
desc="开启后,练习中会在上方显示上一个/下一个单词"
>
<Switch v-model="settingStore.showNearWord"/>
</SettingItem>
<SettingItem title="不默认显示练习设置弹框"
desc="在词典详情页面,点击学习按钮后,是否显示练习设置弹框"
>
<Switch v-model="settingStore.disableShowPracticeSettingDialog"/>
</SettingItem>
<SettingItem title="输入错误时,清空已输入内容"
>
<Switch v-model="settingStore.inputWrongClear"/>
</SettingItem>
<SettingItem title="单词循环设置" class="gap-0!">
<RadioGroup v-model="settingStore.repeatCount">
<Radio :value="1" size="default">1</Radio>
<Radio :value="2" size="default">2</Radio>
<Radio :value="3" size="default">3</Radio>
<Radio :value="5" size="default">5</Radio>
<Radio :value="100" size="default">自定义</Radio>
</RadioGroup>
<div class="ml-2 center gap-space" v-if="settingStore.repeatCount === 100">
<span>循环次数</span>
<InputNumber v-model="settingStore.repeatCustomCount"
:min="6"
:max="15"
type="number"
/>
</div>
</SettingItem>
<!-- 发音-->
<!-- 发音-->
<!-- 发音-->
<div class="line"></div>
<SettingItem mainTitle="音效"/>
<SettingItem title="单词自动发音">
<Switch v-model="settingStore.wordSound"/>
</SettingItem>
<SettingItem title="音量">
<Slider v-model="settingStore.wordSoundVolume" showText showValue unit="%"/>
</SettingItem>
<SettingItem title="倍速">
<Slider v-model="settingStore.wordSoundSpeed" :step="0.1" :min="0.5" :max="3" showText showValue/>
</SettingItem>
<div class="line"></div>
<SettingItem title="效果音(输入错误、完成时的音效)">
<Switch v-model="settingStore.effectSound"/>
</SettingItem>
<SettingItem title="音量">
<Slider v-model="settingStore.effectSoundVolume" showText showValue unit="%"/>
</SettingItem>
<!-- 自动切换-->
<!-- 自动切换-->
<!-- 自动切换-->
<div class="line"></div>
<SettingItem mainTitle="自动切换"/>
<SettingItem title="自动切换下一个单词"
desc="仅在 **跟写** 时生效,听写、辨认、默写均不会自动切换,需要手动按 **空格键** 切换"
>
<Switch v-model="settingStore.autoNextWord"/>
</SettingItem>
<SettingItem title="自动切换下一个单词时间"
desc="正确输入单词后,自动跳转下一个单词的时间"
>
<InputNumber v-model="settingStore.waitTimeForChangeWord"
:disabled="!settingStore.autoNextWord"
:min="0"
:max="10000"
:step="100"
type="number"
/>
<span class="ml-4">毫秒</span>
</SettingItem>
<!-- 字体设置-->
<!-- 字体设置-->
<!-- 字体设置-->
<div class="line"></div>
<SettingItem mainTitle="字体设置"/>
<SettingItem title="外语字体">
<Slider
:min="10"
:max="100"
v-model="settingStore.fontSize.wordForeignFontSize" showText showValue unit="px"/>
</SettingItem>
<SettingItem title="中文字体">
<Slider
:min="10"
:max="100"
v-model="settingStore.fontSize.wordTranslateFontSize" showText showValue unit="px"/>
</SettingItem>
</div>
<!-- 文章练习设置-->
<!-- 文章练习设置-->
<!-- 文章练习设置-->
<div v-if="tabIndex === 2">
<!-- 发音-->
<!-- 发音-->
<!-- 发音-->
<SettingItem mainTitle="音效"/>
<SettingItem title="自动播放句子">
<Switch v-model="settingStore.articleSound"/>
</SettingItem>
<SettingItem title="自动播放下一篇">
<Switch v-model="settingStore.articleAutoPlayNext"/>
</SettingItem>
<SettingItem title="音量">
<Slider v-model="settingStore.articleSoundVolume" showText showValue unit="%"/>
</SettingItem>
<SettingItem title="倍速">
<Slider v-model="settingStore.articleSoundSpeed" :step="0.1" :min="0.5" :max="3" showText showValue/>
</SettingItem>
<div class="line"></div>
<SettingItem title="输入时忽略符号/数字/人名">
<Switch v-model="settingStore.ignoreSymbol"/>
</SettingItem>
</div>
</div>
</div>
</div>
</BasePage>
</template>
<style scoped lang="scss">
.setting {
.left {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
border-right: 2px solid gainsboro;
.tabs {
padding: .6rem 1.6rem;
display: flex;
flex-direction: column;
gap: .6rem;
//color: #0C8CE9;
.tab {
@apply cursor-pointer flex items-center relative;
padding: .6rem .9rem;
border-radius: .5rem;
gap: .6rem;
transition: all .5s;
&:hover {
background: var(--color-select-bg);
color: var(--color-select-text);
}
&.active {
background: var(--color-select-bg);
color: var(--color-select-text);
}
}
}
}
.content {
flex: 1;
height: 100%;
overflow: auto;
padding: 0 1.6rem;
.line {
border-bottom: 1px solid #c4c3c3;
}
}
}
</style>

View File

@@ -1,13 +1,13 @@
import {BaseState, getDefaultBaseState, useBaseStore} from "@/stores/base.ts";
import {getDefaultSettingState, SettingState} from "@/stores/setting.ts";
import {Dict, DictId, DictResource, DictType} from "@/types/types.ts";
import {useRouter} from "vue-router";
import {useRuntimeStore} from "@/stores/runtime.ts";
import { BaseState, getDefaultBaseState, useBaseStore } from "@/stores/base.ts";
import { getDefaultSettingState, SettingState } from "@/stores/setting.ts";
import { Dict, DictId, DictResource, DictType } from "@/types/types.ts";
import { useRouter } from "vue-router";
import { useRuntimeStore } from "@/stores/runtime.ts";
import dayjs from 'dayjs'
import {AppEnv, RESOURCE_PATH, SAVE_DICT_KEY, SAVE_SETTING_KEY} from "@/config/env.ts";
import {nextTick} from "vue";
import { AppEnv, RESOURCE_PATH, SAVE_DICT_KEY, SAVE_SETTING_KEY } from "@/config/env.ts";
import { nextTick } from "vue";
import Toast from '@/components/base/toast/Toast.ts'
import {getDefaultDict, getDefaultWord} from "@/types/func.ts";
import { getDefaultDict, getDefaultWord } from "@/types/func.ts";
import duration from "dayjs/plugin/duration";
dayjs.extend(duration);
@@ -424,9 +424,43 @@ export async function loadJsLib(key: string, url: string) {
if (window[key]) return window[key];
return new Promise((resolve, reject) => {
const script = document.createElement("script");
// 判断是否是 .mjs 文件,如果是,则使用 type="module"
if (url.endsWith(".mjs")) {
script.type = "module"; // 需要加上 type="module"
script.src = url;
script.onload = async () => {
try {
// 使用动态 import 加载模块
const module = await import(url); // 动态导入 .mjs 模块
window[key] = module.default || module; // 将模块挂到 window 对象
resolve(window[key]);
} catch (err) {
reject(`${key} 加载失败: ${err.message}`);
}
};
} else {
// 如果是非 .mjs 文件,直接按原方式加载
script.src = url;
script.onload = () => resolve(window[key]);
}
script.onerror = () => reject(key + " 加载失败");
document.head.appendChild(script);
});
}
export async function loadJsLib2(key: string, url: string, module: boolean = false) {
if (window[key]) return window[key];
return new Promise((resolve, reject) => {
const script = document.createElement("script");
if (module) {
script.type = 'module'
}
script.src = url;
script.onload = () => resolve(window[key]);
script.onerror = reject;
script.onload = () => {
console.log('key', key)
resolve(window[key])
};
script.onerror = () => reject(key + ' 加载失败')
document.head.appendChild(script);
});
}
@@ -460,6 +494,6 @@ export async function isNewUser() {
return JSON.stringify(base.$state) === JSON.stringify({...getDefaultBaseState(), ...{load: true}})
}
export function jump2Feedback(){
export function jump2Feedback() {
window.open('https://v.wjx.cn/vm/ev0W7fv.aspx#', '_blank');
}