fix:fix getCurrentStudyWord function,fix Setting page

This commit is contained in:
zyronon
2025-08-19 02:02:37 +08:00
parent a603bcdb0d
commit b5639dd6c1
17 changed files with 363 additions and 423 deletions

View File

@@ -164,6 +164,7 @@ html, body {
overflow-x: hidden;
color: var(--color-main-text);
font-family: var(--font-family);
background: var(--color-primary);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

View File

@@ -1,5 +1,8 @@
import {Article, Word} from "@/types/types.ts";
import {useBaseStore} from "@/stores/base.ts";
import {useSettingStore} from "@/stores/setting.ts";
import {getDefaultWord} from "@/types/func.ts";
import {getRandomN, splitIntoN} from "@/utils";
export function useWordOptions() {
const store = useBaseStore()
@@ -83,105 +86,98 @@ export function useArticleOptions() {
}
export function getCurrentStudyWord() {
// console.time()
console.log('getCurrentStudyWord')
const store = useBaseStore()
let data = {new: [], review: [], write: []}
let dict = store.sdict;
if (dict.words?.length) {
const getList = (startIndex: number, endIndex: number) => dict.words.slice(startIndex, endIndex)
const perDay = store.sdict.perDayStudyNumber;
const totalNeed = perDay * 3;
let isTest = false
let words = dict.words.slice()
if (isTest) {
words = Array.from({length: 10}).map((v, i) => {
return getDefaultWord({word: String(i)})
})
}
if (words?.length) {
const settingStore = useSettingStore()
//忽略时是否加上自定义的简单词
let ignoreList = [store.allIgnoreWords, store.knownWords][settingStore.ignoreSimpleWord ? 0 : 1]
const perDay = dict.perDayStudyNumber;
let start = dict.lastLearnIndex;
let end = start + dict.perDayStudyNumber
if (dict.complete) {
let complete = dict.complete;
if (isTest) {
start = 1
complete = true
}
let end = start
let list = dict.words.slice(start)
if (complete) {
//如果是已完成,那么把应该学的新词放到复习词组里面
dict.words.slice(start, end).map(item => {
if (!store.knownWords.includes(item.word)) {
data.review.push(item)
for (let item of list) {
if (!ignoreList.includes(item.word.toLowerCase())) {
if (data.review.length < perDay) {
data.review.push(item)
} else break
}
})
//如果起点index 减去总默写不足的话,那就直接从最后面取
//todo 这里有空了,优化成往前滚动取值
if (start - totalNeed < 0) {
end = dict.length
} else {
end = start
end++
}
} else {
dict.words.slice(start, end).map(item => {
if (!store.knownWords.includes(item.word)) {
data.new.push(item)
//从start往后取perDay个单词作为当前练习单词
for (let item of list) {
if (!ignoreList.includes(item.word.toLowerCase())) {
if (data.new.length < perDay) {
data.new.push(item)
} else break
}
})
end = start
start = start - dict.perDayStudyNumber
if (start < 0) start = 0
//取上一次学习的单词用于复习
let list = getList(start, end)
list.map(item => {
if (!store.knownWords.includes(item.word)) {
data.review.push(item)
end++
}
//从start往前取perDay个单词作为当前复习单词取到0为止
list = dict.words.slice(0, start).toReversed()
for (let item of list) {
if (!ignoreList.includes(item.word.toLowerCase())) {
if (data.review.length < perDay) {
data.review.push(item)
} else break
}
})
end = start
start--
}
}
// console.log(start,end)
// end = start
// start = start - dict.perDayStudyNumber * 3
// //在上次学习再往前取前3次学习的单词用于默写
// list = getList(start, end)
// list.map(item => {
// if (!store.knownWords.includes(item.word)) {
// data.write.push(item)
// }
// })
//write数组放的是上上次之前的单词总的数量为perDayStudyNumber * 3取单词的规则为从后往前取6个perDayStudyNumber的越靠前的取的单词越多。
// 上上次更早的单词
if (end > 0) {
const allWords = dict.words;
const candidateWords = allWords.slice(0, end).filter(w => !store.knownWords.includes(w.word));
//默认只取start之前的单词
let candidateWords = dict.words.slice(0, start).toReversed()
//但如果已完成,则滚动取值
if (complete) candidateWords = candidateWords.concat(dict.words.slice(end).toReversed())
candidateWords = candidateWords.filter(w => !ignoreList.includes(w.word.toLowerCase()));
// console.log(candidateWords.map(v => v.word))
//最终要获取的单词数量
const totalNeed = perDay * 3;
if (candidateWords.length <= totalNeed) {
data.write = candidateWords
} else {
//write数组放的是上上次之前的单词总的数量为perDayStudyNumber * 3取单词的规则为从后往前取6个perDayStudyNumber的越靠前的取的单词越多。
let days = 6
// 分6组每组最多 perDay 个
const groups: Word[][] = splitIntoN(candidateWords.slice(0, days * perDay), 6)
// console.log('groups', groups)
// 分6组每组 perDay 个
const groupCount = 6;
const groupSize = perDay;
const groups: Word[][] = [];
for (let i = 0; i < groupCount; i++) {
const start = candidateWords.length - (i + 1) * groupSize;
const end = candidateWords.length - i * groupSize;
if (start < 0 && end <= 0) break;
groups.unshift(candidateWords.slice(Math.max(0, start), Math.max(0, end)));
}
// 分配数量,靠前组多,靠后组少
// 例如分配比例 [1,2,3,4,5,6]
const ratio = [1, 2, 3, 4, 5, 6];
// 分配数量,靠前组多,靠后组少,例如分配比例 [6,5,4,3,2,1]
const ratio = Array.from({length: days}, (_, i) => i + 1).reverse();
const ratioSum = ratio.reduce((a, b) => a + b, 0);
const realRatio = ratio.map(r => Math.round(r * totalNeed / ratioSum));
const realRatio = ratio.map(r => Math.round(r / ratioSum * totalNeed));
// console.log(ratio, ratioSum, realRatio, realRatio.reduce((a, b) => a + b, 0))
// 按比例从每组取单词
// 按比例从每组随机取单词
let writeWords: Word[] = [];
for (let i = 0; i < groups.length; i++) {
writeWords = writeWords.concat(groups[i].slice(-realRatio[i]));
}
// 如果数量不够,补足
if (writeWords.length < totalNeed) {
const extra = candidateWords.filter(w => !writeWords.includes(w)).slice(-(totalNeed - writeWords.length));
writeWords = writeWords.concat(extra);
}
// 最终数量截断
writeWords = writeWords.slice(-totalNeed);
//这里需要反转一下,越靠近今天的单词,越先练习
data.write = writeWords.reverse();
groups.map((v, i) => {
writeWords = writeWords.concat(getRandomN(v, realRatio[i]))
})
// console.log('writeWords', writeWords)
data.write = writeWords;
}
}
// console.timeEnd()
// console.log('data', data)
// console.log('data-new', data.new.map(v => v.word))
// console.log('data-review', data.review.map(v => v.word))
// console.log('data-write', data.write.map(v => v.word))
return data
}

View File

@@ -157,7 +157,7 @@ function setArticle(val: Article) {
articleData.article.sections.map((v, i) => {
v.map((w, j) => {
w.words.map(s => {
if (!store.knownWordsWithSimpleWords.includes(s.word.toLowerCase()) && !s.isSymbol) {
if (!store.allIgnoreWords.includes(s.word.toLowerCase()) && !s.isSymbol) {
statisticsStore.total++
}
})
@@ -199,13 +199,13 @@ function wrong(word: Word) {
if (!store.wrong.words.find((v: Word) => v.word.toLowerCase() === lowerName)) {
store.wrong.words.push(word)
}
if (!store.knownWordsWithSimpleWords.includes(lowerName)) {
if (!store.allIgnoreWords.includes(lowerName)) {
//todo
}
}
function nextWord(word: ArticleWord) {
if (!store.knownWordsWithSimpleWords.includes(word.word.toLowerCase()) && !word.isSymbol) {
if (!store.allIgnoreWords.includes(word.word.toLowerCase()) && !word.isSymbol) {
statisticsStore.inputWordNumber++
}
}

View File

@@ -160,7 +160,7 @@ function nextSentence() {
input = wrong = ''
//todo 计得把略过的单词加上统计里面去
// if (!store.knownWordsWithSimpleWords.includes(currentWord.word.toLowerCase()) && !currentWord.isSymbol) {
// if (!store.allIgnoreWords.includes(currentWord.word.toLowerCase()) && !currentWord.isSymbol) {
// statisticsStore.inputNumber++
// }

View File

@@ -1,15 +1,20 @@
<script setup lang="ts">
import {ref, computed, watch} from 'vue';
const props = defineProps<{
interface IProps {
modelValue: boolean;
disabled?: boolean;
width?: number; // 开关宽度,默认 40px
activeText?: string; // 开启状态显示文字
inactiveText?: string;// 关闭状态显示文字
}>();
}
const emit = defineEmits(['update:modelValue']);
const props = withDefaults(defineProps<IProps>(), {
activeText: '开',
inactiveText: '关',
})
const emit = defineEmits(['update:modelValue', 'change']);
const isChecked = ref(props.modelValue);
@@ -21,6 +26,7 @@ const toggle = () => {
if (props.disabled) return;
isChecked.value = !isChecked.value;
emit('update:modelValue', isChecked.value);
emit('change', isChecked.value);
};
const onKeydown = (e: KeyboardEvent) => {
@@ -96,7 +102,6 @@ const ballSize = computed(() => switchHeight.value - 4);
font-size: 0.75rem;
color: #fff;
user-select: none;
top: 0;
&.left {
margin-left: 6px;

View File

@@ -23,17 +23,15 @@
<script setup lang="ts">
import {ref, watch, computed, nextTick} from "vue"
const props = defineProps({
modelValue: String,
placeholder: String,
maxlength: Number,
rows: {type: Number, default: 1},
autosize: {
type: [Boolean, Object] as () => boolean | { minRows?: number; maxRows?: number },
default: false
},
showWordLimit: Boolean
})
const props = defineProps<{
modelValue: string,
placeholder?: string,
maxlength?: number,
rows?: number,
autosize: boolean | { minRows?: number; maxRows?: number }
showWordLimit?: boolean
}>()
const emit = defineEmits(["update:modelValue"])
@@ -44,7 +42,7 @@ const textareaRef = ref<HTMLTextAreaElement>()
// 样式(用于控制高度)
const textareaStyle = computed(() => {
return props.autosize ? { height: "auto" } : {}
return props.autosize ? {height: "auto"} : {}
})
// 输入处理
@@ -64,7 +62,7 @@ const resizeTextarea = () => {
let overflow = "hidden"
if (typeof props.autosize === "object") {
const { minRows, maxRows } = props.autosize
const {minRows, maxRows} = props.autosize
const lineHeight = 24 // 行高约等于 24px
if (minRows) height = Math.max(height, minRows * lineHeight)
if (maxRows) {
@@ -90,6 +88,7 @@ textarea {
font-family: var(--font-family);
color: var(--color-input-color);
background: var(--color-input-bg);
@apply text-base;
&:focus {
outline: none;

View File

@@ -52,6 +52,7 @@ function onClick() {
align-items: center;
cursor: pointer;
user-select: none;
flex-shrink: 0;
&.is-disabled {
cursor: not-allowed;

View File

@@ -30,7 +30,7 @@ watch(() => props.value, () => {}, { immediate: true });
<template>
<li
class="el-option"
class="option"
:class="{
'is-selected': isSelected,
'is-disabled': disabled
@@ -38,13 +38,13 @@ watch(() => props.value, () => {}, { immediate: true });
@click="handleClick"
>
<slot>
<span class="el-option__label">{{ label }}</span>
<span class="option__label">{{ label }}</span>
</slot>
</li>
</template>
<style scoped lang="scss">
.el-option {
.option {
display: flex;
align-items: center;
padding: 0.2rem 1rem;

View File

@@ -150,16 +150,16 @@ onBeforeUnmount(() => {
<template>
<div
class="custom-select"
class="select"
v-bind="attrs"
:class="{ 'is-disabled': disabled, 'is-active': isOpen, 'is-reverse': isReverse }"
ref="selectRef"
>
<div class="custom-select__wrapper" @click="toggleDropdown">
<div class="custom-select__label" :class="{ 'is-placeholder': !selectedOption }">
<div class="select__wrapper" @click="toggleDropdown">
<div class="select__label" :class="{ 'is-placeholder': !selectedOption }">
{{ displayValue }}
</div>
<div class="custom-select__suffix">
<div class="select__suffix">
<IconMdiChevronDown
:class="{ 'is-reverse': isOpen }"
width="16"
@@ -170,17 +170,17 @@ onBeforeUnmount(() => {
<teleport to="body">
<transition :name="isReverse ? 'zoom-in-bottom' : 'zoom-in-top'" :key="isReverse ? 'bottom' : 'top'">
<div
class="custom-select__dropdown"
class="select__dropdown"
v-if="isOpen"
ref="dropdownRef"
:style="dropdownStyle"
>
<ul class="custom-select__options">
<ul class="select__options">
<li
v-if="options"
v-for="(option, index) in options"
:key="index"
class="custom-select__option"
class="select__option"
:class="{
'is-selected': option.value === modelValue,
'is-disabled': option.disabled
@@ -198,7 +198,7 @@ onBeforeUnmount(() => {
</template>
<style scoped lang="scss">
.custom-select {
.select {
position: relative;
width: 100%;
font-size: 1rem;
@@ -243,7 +243,7 @@ onBeforeUnmount(() => {
}
}
.custom-select__dropdown {
.select__dropdown {
max-height: 200px;
overflow-y: auto;
background-color: #fff;
@@ -252,13 +252,13 @@ onBeforeUnmount(() => {
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.custom-select__options {
.select__options {
margin: 0;
padding: 0;
list-style: none;
}
.custom-select__option {
.select__option {
padding: 0.5rem;
cursor: pointer;
transition: background-color 0.3s;

View File

@@ -1,27 +1,30 @@
<script setup lang="ts">
import { ref, watch } from "vue";
import { useSettingStore } from "@/stores/setting.ts";
import { getAudioFileUrl, useChangeAllSound, usePlayAudio, useWatchAllSound } from "@/hooks/sound.ts";
import { getShortcutKey, useEventListener } from "@/hooks/event.ts";
import { checkAndUpgradeSaveDict, checkAndUpgradeSaveSetting, cloneDeep, shakeCommonDict } from "@/utils";
import { DefaultShortcutKeyMap, ShortcutKey } from "@/types/types.ts";
import {computed, ref, watch} from "vue";
import {useSettingStore} from "@/stores/setting.ts";
import {getAudioFileUrl, useChangeAllSound, usePlayAudio, useWatchAllSound} from "@/hooks/sound.ts";
import {getShortcutKey, useEventListener} from "@/hooks/event.ts";
import {checkAndUpgradeSaveDict, checkAndUpgradeSaveSetting, cloneDeep, shakeCommonDict} from "@/utils";
import {DefaultShortcutKeyMap, ShortcutKey} from "@/types/types.ts";
import BaseButton from "@/components/BaseButton.vue";
import { APP_NAME, EXPORT_DATA_KEY, SAVE_DICT_KEY, SAVE_SETTING_KEY, SoundFileOptions } from "@/utils/const.ts";
import {APP_NAME, EXPORT_DATA_KEY, SAVE_DICT_KEY, SAVE_SETTING_KEY, SoundFileOptions} from "@/utils/const.ts";
import VolumeIcon from "@/components/icon/VolumeIcon.vue";
import { useBaseStore } from "@/stores/base.ts";
import { saveAs } from "file-saver";
import { GITHUB } from "@/config/ENV.ts";
import {useBaseStore} from "@/stores/base.ts";
import {saveAs} from "file-saver";
import {GITHUB} from "@/config/ENV.ts";
import dayjs from "dayjs";
import BasePage from "@/pages/pc/components/BasePage.vue";
import Toast from '@/pages/pc/components/base/toast/Toast.ts'
import { Option, Select } from "@/pages/pc/components/base/select";
import {Option, Select} from "@/pages/pc/components/base/select";
import Switch from "@/pages/pc/components/base/Switch.vue";
import Slider from "@/pages/pc/components/base/Slider.vue";
import RadioGroup from "@/pages/pc/components/base/radio/RadioGroup.vue";
import Radio from "@/pages/pc/components/base/radio/Radio.vue";
import InputNumber from "@/pages/pc/components/base/InputNumber.vue";
import PopConfirm from "@/pages/pc/components/PopConfirm.vue";
import { get, set } from "idb-keyval";
import {get, set} from "idb-keyval";
import BaseInput from "@/pages/pc/components/base/BaseInput.vue";
import Textarea from "@/pages/pc/components/base/Textarea.vue";
import SettingItem from "@/pages/pc/setting/SettingItem.vue";
const emit = defineEmits<{
toggleDisabledDialogEscKey: [val: boolean]
@@ -32,7 +35,16 @@ const settingStore = useSettingStore()
const store = useBaseStore()
//@ts-ignore
const gitLastCommitHash = ref(LATEST_COMMIT_HASH);
const simpleWords = $computed({
get: () => store.simpleWords.join(','),
set: v => {
try {
store.simpleWords = v.split(',');
} catch (e) {
}
}
})
useWatchAllSound()
let editShortcutKey = $ref('')
@@ -93,7 +105,7 @@ function exportData(notice = '导出成功!') {
}
}
}
let blob = new Blob([JSON.stringify(data)], { type: "text/plain;charset=utf-8" });
let blob = new Blob([JSON.stringify(data)], {type: "text/plain;charset=utf-8"});
saveAs(blob, `${APP_NAME}-User-Data-${dayjs().format('YYYY-MM-DD HH-mm-ss')}.json`);
Toast.success(notice)
}
@@ -117,8 +129,10 @@ function importData(e) {
obj = JSON.parse(str)
let data = obj.val
let settingState = checkAndUpgradeSaveSetting(data.setting)
settingState.load = true
settingStore.setState(settingState)
let baseState = checkAndUpgradeSaveDict(data.dict)
baseState.load = true
store.setState(baseState)
Toast.success('导入成功!')
} catch (err) {
@@ -136,7 +150,7 @@ function importOldData() {
if (oldDataStr) {
try {
let obj = JSON.parse(oldDataStr)
let data = {
let data = {
version: 3,
val: obj
}
@@ -187,215 +201,151 @@ function importOldData() {
<div class="content">
<div class="page-title text-align-center">设置</div>
<div v-if="tabIndex === 0">
<div class="row">
<label class="main-title">所有音效</label>
<div class="wrapper">
<Switch v-model="settingStore.allSound"
@change="useChangeAllSound"
active-text="开"
inactive-text="关"
/>
</div>
</div>
<div class="line"></div>
<div class="row">
<label class="item-title">单词/句子自动发音</label>
<div class="wrapper">
<Switch v-model="settingStore.wordSound"
inline-prompt
active-text="开"
inactive-text="关"
/>
</div>
</div>
<div class="row">
<label class="sub-title">单词/句子发音口音</label>
<div class="wrapper">
<Select v-model="settingStore.wordSoundType"
placeholder="请选择"
class="w-50!"
>
<Option label="美音" value="us"/>
<Option label="英音" value="uk"/>
</Select>
</div>
</div>
<div class="row">
<label class="sub-title">音量</label>
<div class="wrapper">
<Slider v-model="settingStore.wordSoundVolume"/>
<span>{{ settingStore.wordSoundVolume }}%</span>
</div>
</div>
<div class="row">
<label class="sub-title">倍速</label>
<div class="wrapper">
<Slider v-model="settingStore.wordSoundSpeed" :step="0.1" :min="0.5" :max="3"/>
<span>{{ settingStore.wordSoundSpeed }}</span>
</div>
</div>
<div class="line"></div>
<div class="row">
<label class="item-title">按键音</label>
<div class="wrapper">
<Switch v-model="settingStore.keyboardSound"
inline-prompt
active-text="开"
inactive-text="关"
/>
</div>
</div>
<div class="row">
<label class="item-title">按键音效</label>
<div class="wrapper">
<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="el-option-row">
<span>{{ item.label }}</span>
<VolumeIcon
:time="100"
@click="usePlayAudio(getAudioFileUrl(item.value)[0])"/>
</div>
</Option>
</Select>
</div>
</div>
<div class="row">
<label class="sub-title">音量</label>
<div class="wrapper">
<Slider v-model="settingStore.keyboardSoundVolume"/>
<span>{{ settingStore.keyboardSoundVolume }}%</span>
</div>
</div>
<div class="line"></div>
<div class="row">
<label class="item-title">效果音输入错误完成时的音效</label>
<div class="wrapper">
<Switch v-model="settingStore.effectSound"
inline-prompt
active-text="开"
inactive-text="关"
/>
</div>
</div>
<div class="row">
<label class="sub-title">音量</label>
<div class="wrapper">
<Slider v-model="settingStore.effectSoundVolume"/>
<span>{{ settingStore.effectSoundVolume }}%</span>
</div>
</div>
</div>
<div v-if="tabIndex === 1">
<div class="row">
<label class="item-title">单词循环设置</label>
<div class="wrapper">
<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="mini-row" v-if="settingStore.repeatCount === 100">
<label class="item-title">循环次数</label>
<InputNumber v-model="settingStore.repeatCustomCount"
:min="6"
:max="15"
type="number"
/>
</div>
</div>
</div>
<div class="row">
<label class="item-title">显示上一个/下一个单词</label>
<div class="wrapper">
<Switch v-model="settingStore.showNearWord"
inline-prompt
active-text="开"
inactive-text="关"
/>
</div>
</div>
<div class="desc">
开启后练习中会在上方显示上一个/下一个单词
</div>
<div class="line"></div>
<div class="row">
<label class="item-title">忽略大小写</label>
<div class="wrapper">
<Switch v-model="settingStore.ignoreCase"
inline-prompt
active-text="开"
inactive-text="关"
/>
</div>
</div>
<div class="desc">
开启后输入时不区分大小写如输入helloHello都会被认为是正确的
</div>
<div class="line"></div>
<div class="row">
<label class="item-title">允许默写模式下显示提示</label>
<div class="wrapper">
<Switch v-model="settingStore.allowWordTip"
inline-prompt
active-text="开"
inactive-text="关"
/>
</div>
</div>
<div class="desc">
开启后可以通过鼠标 hover 单词或者按 {{ settingStore.shortcutKeyMap[ShortcutKey.ShowWord] }} 显示正确答案
</div>
<div class="line"></div>
<div class="row">
<label class="item-title">字体设置(仅可调整单词练习)</label>
</div>
<div class="row">
<label class="sub-title">外语字体</label>
<div class="wrapper">
<Slider
:min="10"
:max="100"
v-model="settingStore.fontSize.wordForeignFontSize"/>
<span>{{ settingStore.fontSize.wordForeignFontSize }}</span>
</div>
</div>
<div class="row">
<label class="sub-title">中文字体</label>
<div class="wrapper">
<Slider
:min="10"
:max="100"
v-model="settingStore.fontSize.wordTranslateFontSize"/>
<span>{{ settingStore.fontSize.wordTranslateFontSize }}</span>
</div>
</div>
<SettingItem mainTitle="所有音效">
<Switch v-model="settingStore.allSound" @change="useChangeAllSound"/>
</SettingItem>
<div class="line"></div>
<div class="row">
<label class="item-title">其他设置</label>
</div>
<div class="row">
<label class="sub-title">切换下一个单词时间</label>
<div class="wrapper">
<InputNumber v-model="settingStore.waitTimeForChangeWord"
:min="10"
:max="100"
<SettingItem title="单词/句子自动发音">
<Switch v-model="settingStore.wordSound"/>
</SettingItem>
<SettingItem title="单词/句子发音口音">
<Select v-model="settingStore.wordSoundType"
placeholder="请选择"
class="w-50!"
>
<Option label="美音" value="us"/>
<Option label="英音" value="uk"/>
</Select>
</SettingItem>
<SettingItem title="音量">
<Slider v-model="settingStore.wordSoundVolume"/>
<span class="w-10 pl-5">{{ settingStore.wordSoundVolume }}%</span>
</SettingItem>
<SettingItem title="倍速">
<Slider v-model="settingStore.wordSoundSpeed" :step="0.1" :min="0.5" :max="3"/>
<span class="w-10 pl-5">{{ settingStore.wordSoundSpeed }}</span>
</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"/>
<span class="w-10 pl-5">{{ settingStore.keyboardSoundVolume }}%</span>
</SettingItem>
<div class="line"></div>
<SettingItem title="效果音(输入错误、完成时的音效)">
<Switch v-model="settingStore.effectSound"/>
</SettingItem>
<SettingItem title="音量">
<Slider v-model="settingStore.effectSoundVolume"/>
<span class="w-10 pl-5">{{ settingStore.effectSoundVolume }}%</span>
</SettingItem>
</div>
<div v-if="tabIndex === 1">
<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"
/>
<span>毫秒</span>
</div>
</div>
</SettingItem>
<SettingItem title="显示上一个/下一个单词"
desc="开启后,练习中会在上方显示上一个/下一个单词"
>
<Switch v-model="settingStore.showNearWord"/>
</SettingItem>
<SettingItem title="忽略大小写"
desc="开启后输入时不区分大小写如输入“hello”和“Hello”都会被认为是正确的"
>
<Switch v-model="settingStore.ignoreCase"/>
</SettingItem>
<SettingItem title="允许默写模式下显示提示"
:desc="`开启后,可以通过鼠标 hover 单词或者按快捷键 ${settingStore.shortcutKeyMap[ShortcutKey.ShowWord]} 显示正确答案`"
>
<Switch v-model="settingStore.allowWordTip"/>
</SettingItem>
<div class="line"></div>
<SettingItem title="字体设置(仅可调整单词练习)"/>
<SettingItem title="外语字体">
<Slider
:min="10"
:max="100"
v-model="settingStore.fontSize.wordForeignFontSize"/>
<span class="w-10 pl-5">{{ settingStore.fontSize.wordForeignFontSize }}px</span>
</SettingItem>
<SettingItem title="中文字体">
<Slider
:min="10"
:max="100"
v-model="settingStore.fontSize.wordTranslateFontSize"/>
<span class="w-10 pl-5">{{ settingStore.fontSize.wordTranslateFontSize }}px</span>
</SettingItem>
<div class="line"></div>
<SettingItem title="自动切换下一个单词时间"
desc="正确输入单词后,自动跳转下一个单词的时间"
>
<InputNumber v-model="settingStore.waitTimeForChangeWord"
:min="10"
:max="100"
type="number"
/>
<span class="ml-4">毫秒</span>
</SettingItem>
<div class="line"></div>
<SettingItem title="简单词过滤"
desc="开启后,练习的单词中不会再出现简单词"
>
<Switch v-model="settingStore.ignoreSimpleWord"/>
</SettingItem>
<SettingItem title="简单词列表"
class="items-start!"
>
<Textarea
placeholder="多个单词用英文逗号隔号"
v-model="simpleWords" :autosize="{minRows: 6, maxRows: 10}"/>
</SettingItem>
</div>
<div class="body" v-if="tabIndex === 2">
<div class="row">
@@ -425,55 +375,35 @@ function importOldData() {
</div>
</div>
<div v-if="tabIndex === 3">
<div class="row">
<div class="main-title">数据导出</div>
<div>
目前用户的所有数据(自定义设置自定义词典自定义文章学习进度等)
<b class="text-red">仅保存在本地</b>如果您需要在不同的设备浏览器或者其他非官方部署上使用 {{ APP_NAME }}
您需要手动进行数据同步和保存
</div>
<div class="row">
<label class="sub-title">
目前用户的所有数据(自定义设置自定义词典练习进度等)
<b>仅保存在本地</b>
如果您需要在不同的设备浏览器或者其他非官方部署上使用 {{ APP_NAME }} 您需要手动进行数据同步和保存
</label>
<BaseButton class="mt-3" @click="exportData()">导出数据</BaseButton>
<div class="line my-3"></div>
<div>请注意导入数据后将<b class="text-red"> 完全覆盖 </b>当前所有数据(自定义设置自定义词典自定义文章学习进度等)请谨慎操作
</div>
<div class="row mt-2">
<BaseButton @click="exportData">数据导出</BaseButton>
</div>
<div class="row">
<div class="main-title">数据导入</div>
</div>
<div class="row">
<label class="sub-title">
请注意导入数据将
<b style="color: red"> 完全覆盖 </b>
当前数据请谨慎操作
</label>
</div>
<div class="row">
<div class="flex gap-space mt-3">
<div class="import hvr-grow">
<BaseButton>数据导入</BaseButton>
<BaseButton>导入数据</BaseButton>
<input type="file"
accept="application/json"
@change="importData">
</div>
</div>
<div class="row">
<div class="main-title">老版本数据导入</div>
</div>
<div class="row">
<div class="import hvr-grow">
<PopConfirm
title="导入老版本数据前,请先备份当前数据。确定要导入老版本数据吗?"
@confirm="importOldData">
<BaseButton>老版本数据导入</BaseButton>
</PopConfirm>
</div>
<PopConfirm
title="导入老版本数据前,请先备份当前数据,确定要导入老版本数据吗?"
@confirm="importOldData">
<BaseButton>老版本数据导入</BaseButton>
</PopConfirm>
</div>
</div>
<div v-if="tabIndex === 4" class="feedback-modal">
<div v-if="tabIndex === 4">
<div>
给我发Email<a href="mailto:zyronon@163.com">zyronon@163.com</a>
</div>
<p>or</p>
<span><a :href="GITHUB" target="_blank"> Github </a>上给作者提一个
<a :href="`${GITHUB}/issues`" target="_blank"> Issue </a>
</span>
@@ -493,7 +423,6 @@ function importOldData() {
<div class="text-md color-gray">
Build {{ gitLastCommitHash }}
</div>
</div>
</div>
</div>
@@ -501,7 +430,9 @@ function importOldData() {
</template>
<style scoped lang="scss">
.setting {
@apply text-lg;
display: flex;
color: var(--color-font-1);
@@ -533,7 +464,6 @@ function importOldData() {
}
}
}
}
.content {
@@ -609,28 +539,12 @@ function importOldData() {
overflow: auto;
}
.desc {
margin-bottom: .6rem;
font-size: .8rem;
}
.line {
border-bottom: 1px solid #c4c3c3;
}
}
}
.el-option-row {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
.icon-wrapper {
transform: translateX(10rem);
}
}
.import {
display: inline-flex;
position: relative;
@@ -642,23 +556,4 @@ function importOldData() {
opacity: 0;
}
}
.feedback-modal {
//height: 80vh;
display: flex;
flex-direction: column;
align-items: center;
padding: var(--space);
//justify-content: center;
color: var(--color-font-1);
p {
font-size: 2.4rem;
}
}
.about {
text-align: center;
}
</style>

View File

@@ -0,0 +1,22 @@
<script setup lang="ts">
defineProps<{
mainTitle?: string,
title?: string,
desc?: string,
}>()
</script>
<template>
<div class="flex items-center gap-40" :class="desc ? 'mt-3' : 'my-3'" v-bind="$attrs">
<span v-if="title">{{ title }}</span>
<span class="text-xl font-bold" v-if="mainTitle">{{ mainTitle }}</span>
<div class="flex flex-1 justify-end">
<slot></slot>
</div>
</div>
<div class="text-sm mb-3" v-if="desc">{{ desc }}</div>
</template>
<style scoped lang="scss">
</style>

View File

@@ -31,9 +31,9 @@ let currentStudy = $ref({
write: []
})
//todo 当选完词返回时,计算今日任务时,还是老的词典
onMounted(init)
watch(() => store.load, init)
watch(() => store.load, n => {
if (n) init()
}, {immediate: true})
async function init() {
if (store.word.studyIndex >= 3) {

View File

@@ -10,7 +10,7 @@ import StudyWord from "@/pages/pc/word/StudyWord.vue";
import BookDetail from "@/pages/pc/article/BookDetail.vue";
import DictList from "@/pages/pc/word/DictList.vue";
import BookList from "@/pages/pc/article/BookList.vue";
import Setting from "@/pages/pc/Setting.vue";
import Setting from "@/pages/pc/setting/Setting.vue";
export const routes: RouteRecordRaw[] = [
{

View File

@@ -77,8 +77,8 @@ export const useBaseStore = defineStore('base', {
knownWords(): string[] {
return this.known.words.map((v: Word) => v.word.toLowerCase())
},
knownWordsWithSimpleWords() {
return this.known.words.map((v: Word) => v.word.toLowerCase()).concat(this.simpleWords)
allIgnoreWords() {
return this.known.words.map((v: Word) => v.word.toLowerCase()).concat(this.simpleWords.map((v: string) => v.toLowerCase()))
},
currentStudyWordDict(): Dict {
if (this.word.studyIndex >= 0) {

View File

@@ -22,7 +22,6 @@ export interface SettingState {
repeatCustomCount?: number,
dictation: boolean,
translate: boolean,
detail: boolean,
showNearWord: boolean
ignoreCase: boolean
allowWordTip: boolean
@@ -36,17 +35,14 @@ export interface SettingState {
showPanel: boolean,
sideExpand: boolean,
theme: string,
collapse: boolean,
chapterWordNumber: number,
shortcutKeyMap: Record<string, string>,
first: boolean
firstTime: number
load: boolean
conflictNotice: boolean // 其他脚本/插件冲突提示
ignoreSimpleWord: boolean // 忽略简单词
}
export const DefaultChapterWordNumber = 30
export const getDefaultSettingState = (): SettingState => ({
showToolbar: true,
show: false,
@@ -67,7 +63,6 @@ export const getDefaultSettingState = (): SettingState => ({
repeatCustomCount: null,
dictation: false,
translate: true,
detail: false,
showNearWord: true,
ignoreCase: true,
@@ -80,13 +75,12 @@ export const getDefaultSettingState = (): SettingState => ({
},
waitTimeForChangeWord: 300,
theme: 'auto',
collapse: false,
chapterWordNumber: DefaultChapterWordNumber,
shortcutKeyMap: cloneDeep(DefaultShortcutKeyMap),
first: true,
firstTime: Date.now(),
load: false,
conflictNotice: true
conflictNotice: true,
ignoreSimpleWord: false
})
export const useSettingStore = defineStore('setting', {

View File

@@ -14,7 +14,7 @@ export const SAVE_DICT_KEY = {
}
export const SAVE_SETTING_KEY = {
key: 'typing-word-setting',
version: 9
version: 10
}
export const EXPORT_DATA_KEY = {
key: 'typing-word-export',

View File

@@ -587,3 +587,30 @@ export function groupBy<T extends Record<string, any>>(array: T[], key: string)
return result;
}, {});
}
//随机取N个
export function getRandomN(arr: any[], n: number) {
const copy = [...arr]
for (let i = copy.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[copy[i], copy[j]] = [copy[j], copy[i]] // 交换
}
return copy.slice(0, n)
}
//数组分成N份
export function splitIntoN(arr: any[], n: number) {
const result = []
const len = arr.length
const base = Math.floor(len / n) // 每份至少这么多
let extra = len % n // 前几份多 1 个
let index = 0
for (let i = 0; i < n; i++) {
const size = base + (extra > 0 ? 1 : 0)
result.push(arr.slice(index, index + size))
index += size
if (extra > 0) extra--
}
return result
}