wip
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
<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 BasePage from "@/components/BasePage.vue";
|
||||
import {_getDictDataByUrl, _nextTick, isMobile, msToHourMinute, resourceWrap, total, useNav} from "@/utils";
|
||||
import { DictResource, DictType } from "@/types/types.ts";
|
||||
import { useRuntimeStore } from "@/stores/runtime.ts";
|
||||
import {DictResource, DictType} from "@/types/types.ts";
|
||||
import {useRuntimeStore} from "@/stores/runtime.ts";
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
import Book from "@/components/Book.vue";
|
||||
import Progress from '@/components/base/Progress.vue';
|
||||
import Toast from '@/components/base/toast/Toast.ts'
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import PopConfirm from "@/components/PopConfirm.vue";
|
||||
import { watch } from "vue";
|
||||
import { getDefaultDict } from "@/types/func.ts";
|
||||
import {watch} from "vue";
|
||||
import {getDefaultDict} from "@/types/func.ts";
|
||||
import DeleteIcon from "@/components/icon/DeleteIcon.vue";
|
||||
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 { myDictList } from "@/apis";
|
||||
import {useFetch} from "@vueuse/core";
|
||||
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";
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
|
||||
dayjs.extend(isoWeek)
|
||||
dayjs.extend(isBetween);
|
||||
@@ -57,9 +57,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()
|
||||
}
|
||||
@@ -112,6 +112,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 +220,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 +235,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>
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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/>-->
|
||||
|
||||
@@ -269,7 +269,7 @@ async function importData(e) {
|
||||
|
||||
Toast.success("导入成功!");
|
||||
} catch (e) {
|
||||
Toast.error("导入失败!");
|
||||
Toast.error(e?.message || e || '导入失败')
|
||||
} finally {
|
||||
importLoading = false
|
||||
}
|
||||
@@ -278,29 +278,6 @@ async function importData(e) {
|
||||
}
|
||||
}
|
||||
|
||||
function importOldData() {
|
||||
exportData('已为您自动保存当前数据!稍后将进行老数据导入操作')
|
||||
setTimeout(() => {
|
||||
let oldDataStr = localStorage.getItem('type-word-dict-v3')
|
||||
if (oldDataStr) {
|
||||
try {
|
||||
let obj = JSON.parse(oldDataStr)
|
||||
let data = {
|
||||
version: 3,
|
||||
val: obj
|
||||
}
|
||||
let baseState = checkAndUpgradeSaveDict(data)
|
||||
store.setState(baseState)
|
||||
Toast.success('导入成功')
|
||||
} catch (err) {
|
||||
Toast.error('导入失败')
|
||||
}
|
||||
} else {
|
||||
Toast.error('导入失败!原因:本地无老数据备份')
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
let isNewHost = $ref(window.location.host === Host)
|
||||
|
||||
let showTransfer = $ref(false)
|
||||
@@ -630,11 +607,6 @@ function transferOk() {
|
||||
accept="application/json,.zip,application/zip"
|
||||
@change="importData">
|
||||
</div>
|
||||
<PopConfirm
|
||||
title="导入老版本数据前,请先备份当前数据,确定要导入老版本数据吗?"
|
||||
@confirm="importOldData">
|
||||
<BaseButton>老版本数据导入</BaseButton>
|
||||
</PopConfirm>
|
||||
</div>
|
||||
|
||||
<template v-if="isNewHost">
|
||||
@@ -652,7 +624,7 @@ function transferOk() {
|
||||
<div class="mb-2">
|
||||
<div>
|
||||
<div>日期:2025/11/28</div>
|
||||
<div>内容:新增引导框、新增词典测试模式(大佬 hebeihang 开发)</div>
|
||||
<div>内容:新增引导框、 新增<a href="https://github.com/zyronon/TypeWords/pull/175" target="_blank">词典测试模式(由大佬 hebeihang 开发)</a></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -856,74 +828,6 @@ function transferOk() {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
// 用户信息样式
|
||||
.user-info-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 2rem;
|
||||
border: 1px solid var(--color-input-border);
|
||||
border-radius: 8px;
|
||||
background: var(--color-bg);
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
|
||||
.user-avatar {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
border: 3px solid var(--color-select-bg);
|
||||
|
||||
.avatar-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.avatar-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
color: var(--color-font-1);
|
||||
}
|
||||
|
||||
.text-sm {
|
||||
font-size: 0.9rem;
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
|
||||
.color-gray {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.mb-1 {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.mb-2 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.mb-4 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.mt-4 {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.setting {
|
||||
@apply text-lg;
|
||||
|
||||
@@ -33,7 +33,7 @@ defineProps<{
|
||||
.setting-item__main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2.5rem;
|
||||
gap: 2rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ 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 +43,34 @@ let currentStudy = $ref({
|
||||
})
|
||||
|
||||
watch(() => store.load, n => {
|
||||
if (n) init()
|
||||
if (n) {
|
||||
init()
|
||||
_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)
|
||||
}
|
||||
}, {immediate: true})
|
||||
|
||||
async function init() {
|
||||
@@ -200,33 +228,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 +237,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">
|
||||
|
||||
332
src/pages/word/components/SettingDialog.vue
Normal file
332
src/pages/word/components/SettingDialog.vue
Normal 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>
|
||||
@@ -426,7 +426,7 @@ export async function loadJsLib(key: string, url: string) {
|
||||
const script = document.createElement("script");
|
||||
script.src = url;
|
||||
script.onload = () => resolve(window[key]);
|
||||
script.onerror = reject;
|
||||
script.onerror = () => reject(key + ' 加载失败')
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
}
|
||||
@@ -460,6 +460,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');
|
||||
}
|
||||
Reference in New Issue
Block a user