impr:change the word and article Settings to pop-up boxes

This commit is contained in:
Zyronon
2025-12-03 20:03:17 +08:00
committed by GitHub
parent 9779c7402e
commit 6a5156ae36
15 changed files with 177 additions and 911 deletions

View File

@@ -53,8 +53,13 @@ export function levelBenefits(params) {
}
export function orderCreate(params) {
return http<{ orderNo: string,result:string, }>('/member/orderCreate', params, null, 'post')
return http<{ orderNo: string, result: string, }>('/member/orderCreate', params, null, 'post')
}
export function alipayQuery(params) {
return http('/member/alipayQuery', null, params, 'get')
}
export function testPay() {
return http('/member/testPay', null, null, 'get')
}

View File

@@ -2,7 +2,8 @@
import {defineAsyncComponent, onMounted, watch} from "vue";
import {useSettingStore} from "@/stores/setting.ts";
import { jump2Feedback } from "@/utils";
import {jump2Feedback} from "@/utils";
import {useDisableEventListener} from "@/hooks/event.ts";
const Dialog = defineAsyncComponent(() => import('@/components/dialog/Dialog.vue'))
@@ -17,41 +18,33 @@ watch(() => settingStore.load, (n) => {
}
}, {immediate: true})
useDisableEventListener(() => show)
</script>
<template>
<Dialog v-model="show"
title="提示"
footer
:closeOnClickBg="false"
cancel-button-text="不再提醒"
confirm-button-text="关闭"
@cancel="settingStore.conflictNotice = false"
<Dialog
v-model="show"
title="重要提示"
footer
:closeOnClickBg="false"
cancel-button-text="不再提醒"
confirm-button-text="关闭"
@cancel="settingStore.conflictNotice = false"
>
<div class="card w-120 center flex-col color-main py-0 mb-0">
<div>
<div class="text">
<div>
1 如果您安装了 <span class="font-bold text-red">调速 Vim</span> 等插件/脚本将导致本网站无法正常使用
</div>
<div>
因为它们会强行接管键盘按下事件<span class="font-bold text-red">导致使用本网站时按 'A' 'S' 等等按钮无反应</span>
</div>
</div>
<div class="pl-4">
<div>在对应插件/脚本的设置里面排除本网站</div>
<div>临时禁用对应插件/脚本</div>
<div>请打开浏览器无痕模式尝试</div>
</div>
<div class="text mt-2">
2如果您未安装以上插件/脚本还是无法使用
</div>
<div class="pl-4">
<div>请打开浏览器无痕模式尝试</div>
<div>无痕模式下无法正常使用请给<span class="color-link mx-1 cp" @click="jump2Feedback">点此</span>给作者反馈
</div>
</div>
<div class="card w-150 center flex-col color-main py-0 mb-0">
<div class="text">
如果您安装了 <span class="font-bold text-red">调速 Vim</span> 等插件/脚本它们会拦截键盘按下事件<span
class="font-bold text-red">导致在本网站练习时按 'A' 'S' 'D' 等键无反应</span>您可以根据以下步骤解决冲突
</div>
<ul class="m-0">
<li>用浏览器无痕模式打开本网站确认能否正常输入</li>
<li>无痕模式下无法输入请给<span class="color-link mx-1 cp" @click="jump2Feedback">点此</span>反馈</li>
<li>无痕模式下可以输入则是插件/脚本导致的冲突</li>
<li>临时禁用对应插件/脚本或在对应插件/脚本的设置里面排除本网站</li>
<li>可安装 <a
href="https://chromewebstore.google.com/detail/one-click-extensions-mana/pbgjpgbpljobkekbhnnmlikbbfhbhmem" target="_blank">此插件</a> 来快速激活禁用其他插件</li>
</ul>
</div>
</Dialog>
</template>

View File

@@ -123,10 +123,9 @@ function startStudy() {
}
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
s:`name:${base.sbook.name},index:${base.sbook.lastLearnIndex},title:${base.sbook.articles[base.sbook.lastLearnIndex].title}`,
})
nav('/practice-articles/' + store.sbook.id)
} else {

View File

@@ -63,9 +63,9 @@ async function addMyStudyList() {
window.umami?.track('startStudyArticle', {
name: sbook.name,
index: sbook.lastLearnIndex,
custom: sbook.custom,
complete: sbook.complete,
s:`name:${sbook.name},index:${sbook.lastLearnIndex},title:${sbook.articles[sbook.lastLearnIndex].title}`,
})
nav('/practice-articles/' + sbook.id)
}

View File

@@ -1,9 +1,9 @@
<script setup lang="ts">
import { computed, onMounted, onUnmounted, provide, watch } from "vue";
import { useBaseStore } from "@/stores/base.ts";
import { emitter, EventKey, useEvents } from "@/utils/eventBus.ts";
import { useSettingStore } from "@/stores/setting.ts";
import {computed, onMounted, onUnmounted, provide, watch} from "vue";
import {useBaseStore} from "@/stores/base.ts";
import {emitter, EventKey, useEvents} from "@/utils/eventBus.ts";
import {useSettingStore} from "@/stores/setting.ts";
import {
Article,
ArticleItem,
@@ -15,14 +15,14 @@ import {
Statistics,
Word
} from "@/types/types.ts";
import { useDisableEventListener, useOnKeyboardEventListener, useStartKeyboardEventListener } from "@/hooks/event.ts";
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, 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";
import { getDefaultArticle, getDefaultDict, getDefaultWord } from "@/types/func.ts";
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";
import {getDefaultArticle, getDefaultDict, getDefaultWord} from "@/types/func.ts";
import TypingArticle from "@/pages/article/components/TypingArticle.vue";
import BaseIcon from "@/components/BaseIcon.vue";
import Panel from "@/components/Panel.vue";
@@ -30,13 +30,13 @@ import ArticleList from "@/components/list/ArticleList.vue";
import EditSingleArticleModal from "@/pages/article/components/EditSingleArticleModal.vue";
import Tooltip from "@/components/base/Tooltip.vue";
import ConflictNotice from "@/components/ConflictNotice.vue";
import { useRoute, useRouter } from "vue-router";
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, LIB_JS_URL, PracticeSaveArticleKey, TourConfig } from "@/config/env.ts";
import { addStat, setDictProp } from "@/apis";
import { useRuntimeStore } from "@/stores/runtime.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 SettingDialog from "@/pages/word/components/SettingDialog.vue";
const store = useBaseStore()
const runtimeStore = useRuntimeStore()
@@ -591,14 +591,15 @@ provide('currentPractice', currentPractice)
></ArticleAudio>
<div class="flex flex-col items-center justify-center gap-1">
<div class="flex gap-2 center">
<VolumeSetting/>
<SettingDialog type="article"/>
<BaseIcon
:title="`下一句(${settingStore.shortcutKeyMap[ShortcutKey.Next]})`"
@click="skip">
<IconFluentArrowBounce20Regular class="transform-rotate-180"/>
</BaseIcon>
<BaseIcon
:title="`重听(${settingStore.shortcutKeyMap[ShortcutKey.PlayWordPronunciation]})`"
:title="`播放当前句子(${settingStore.shortcutKeyMap[ShortcutKey.PlayWordPronunciation]})`"
@click="play">
<IconFluentReplay20Regular/>
</BaseIcon>

View File

@@ -1,43 +0,0 @@
<script setup lang="ts">
import MiniDialog from "@/components/dialog/MiniDialog.vue";
import { useWindowClick } from "@/hooks/event.ts";
import { useSettingStore } from "@/stores/setting.ts";
import BaseIcon from "@/components/BaseIcon.vue";
import Switch from "@/components/base/Switch.vue";
const settingStore = useSettingStore()
let show = $ref(false)
useWindowClick(() => show = false)
</script>
<template>
<div class="relative"
@click.stop="null"
>
<BaseIcon
title="播放设置"
@click="show = !show">
<IconFluentSpeakerSettings20Regular/>
</BaseIcon>
<MiniDialog
width="12rem"
v-model="show">
<div class="mini-row-title">
播放设置
</div>
<div class="flex justify-between mb-3">
<label class="">自动播放句子</label>
<Switch v-model="settingStore.articleSound"/>
</div>
<div class="flex justify-between">
<label class="">自动播放下一篇</label>
<Switch v-model="settingStore.articleAutoPlayNext"/>
</div>
</MiniDialog>
</div>
</template>
<style scoped lang="scss">
</style>

View File

@@ -39,7 +39,7 @@ const emit = defineEmits<{
toggleDisabledDialogEscKey: [val: boolean]
}>()
const tabIndex = $ref(0)
const tabIndex = $ref(3)
const settingStore = useSettingStore()
const runtimeStore = useRuntimeStore()
const store = useBaseStore()
@@ -296,18 +296,6 @@ function transferOk() {
<div class="flex flex-1 overflow-hidden gap-4">
<div class="left">
<div class="tabs">
<div class="tab" :class="tabIndex === 0 && 'active'" @click="tabIndex = 0">
<IconFluentSettings20Regular width="20"/>
<span>通用练习设置</span>
</div>
<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 === 3 && 'active'" @click="tabIndex = 3">
<IconFluentKeyboardLayoutFloat20Regular width="20"/>
<span>快捷键设置</span>
@@ -333,224 +321,6 @@ function transferOk() {
</div>
<div class="col-line"></div>
<div class="flex-1 overflow-y-auto overflow-x-hidden pr-4 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 class="body" v-if="tabIndex === 3">
<div class="row">
@@ -615,6 +385,14 @@ function transferOk() {
<!-- 日志-->
<div v-if="tabIndex === 5">
<div class="log-item">
<div class="mb-2">
<div>
<div>日期2025/12/3</div>
<div>内容单词文章设置修改为弹框更方便</div>
</div>
</div>
</div>
<div class="log-item">
<div class="mb-2">
<div>
@@ -829,9 +607,6 @@ function transferOk() {
<p>
GitHub地址<a :href="GITHUB" target="_blank">{{ GITHUB }}</a>
</p>
<p>
反馈<a :href="`${GITHUB}/issues`" target="_blank">{{ GITHUB }}/issues</a>
</p>
<p>
作者邮箱<a :href="`mailto:${EMAIL}`">{{ EMAIL }}</a>
</p>
@@ -879,17 +654,18 @@ function transferOk() {
@apply cursor-pointer flex items-center relative;
padding: .6rem .9rem;
border-radius: .5rem;
width: 10rem;
gap: .6rem;
transition: all .5s;
&:hover {
background: var(--color-select-bg);
color: var(--color-select-text);
background: var(--btn-primary);
color: white;
}
&.active {
background: var(--color-select-bg);
color: var(--color-select-text);
background: var(--btn-primary);
color: white;
}
}
}

View File

@@ -14,7 +14,7 @@ import FormItem from "@/components/base/form/FormItem.vue";
import Form from "@/components/base/form/Form.vue";
import {FormInstance} from "@/components/base/form/types.ts";
import {codeRules, emailRules, passwordRules, phoneRules} from "@/utils/validation.ts";
import {_dateFormat, cloneDeep} from "@/utils";
import {_dateFormat, cloneDeep, jump2Feedback} from "@/utils";
import Toast from "@/components/base/toast/Toast.ts";
import Code from "@/pages/user/Code.vue";
import {MessageBox} from "@/utils/MessageBox.tsx";
@@ -37,10 +37,6 @@ const contactSupport = () => {
console.log('Contact support')
}
const goIssues = () => {
window.open(GITHUB + '/issues', '_blank')
}
onMounted(() => {
userStore.fetchUserInfo()
})
@@ -541,7 +537,7 @@ function onFileChange(e) {
<!-- 去github issue-->
<div class="item cp"
@click="goIssues">
@click="jump2Feedback()">
<div class="flex-1">
给 {{ APP_NAME }} 提交意见
</div>
@@ -554,7 +550,7 @@ function onFileChange(e) {
<BaseButton
@click="handleLogout"
size="large"
class="w-[80%]"
class="w-[40%]"
>
登出
</BaseButton>

View File

@@ -7,6 +7,7 @@ import {User} from "@/apis/user.ts";
import {computed, onMounted, onUnmounted, ref, watch} from "vue";
import Header from "@/components/Header.vue";
import {
alipayQuery,
CouponInfo,
couponInfo,
LevelBenefits,
@@ -202,6 +203,8 @@ let orderNo = $ref('')
let timer: number = $ref()
let showCouponInput = $ref(false)
let coupon = $ref<CouponInfo>({code: ''} as CouponInfo)
let checkLoading = $ref(false)
let showCheckBtn = $ref(false)
watch(() => startLoop, (n) => {
if (n) {
@@ -221,8 +224,12 @@ watch(() => startLoop, (n) => {
}
})
}, 1000)
setTimeout(() => {
showCheckBtn = true
}, 3000)
} else {
clearInterval(timer)
showCheckBtn = false
}
})
@@ -269,6 +276,16 @@ async function handlePayment() {
loading = false
}
async function checkOrderStatus() {
if (checkLoading) return
checkLoading = true
let res = await alipayQuery({orderNo})
if (!res.success) {
Toast.info(res.msg || '未付款')
}
checkLoading = false
}
let couponLoading = $ref(false)
async function getCouponInfo() {
@@ -302,7 +319,7 @@ async function getCouponInfo() {
<div class="card-white">
<Header title="会员介绍"></Header>
<div class="grid grid-cols-3 grid-rows-3 gap-3">
<div class="text-lg items-center" v-for="f in data.benefits" :key="f.name">
<div class="text-lg flex items-center" v-for="f in data.benefits" :key="f.name">
<IconFluentCheckmarkCircle20Regular class="mr-2 text-green-600"/>
<span>
<span>{{ f.name }}</span>
@@ -371,7 +388,7 @@ async function getCouponInfo() {
</div>
</div>
<div id="pay" class="mb-50">
<div id="pay" class="mb-50" v-if="selectedPlanId">
<!-- Page Header -->
<div class="text-center mb-6">
<h1 class="text-xl font-semibold mb-2">安全支付</h1>
@@ -509,9 +526,15 @@ async function getCouponInfo() {
</div>
<iframe id="payFrame" class="w-[205px] h-[205px] center border-none"></iframe>
<div class="text-center mt-4">
<div class="text-center my-4">
请使用支付宝扫码支付
</div>
<BaseButton size="large"
v-if="showCheckBtn"
:loading="checkLoading"
@click="checkOrderStatus">
我已付款
</BaseButton>
</div>
</div>
</div>

View File

@@ -295,7 +295,7 @@ enum ImportStep {
}
const {exportData} = useExport()
let importStep = $ref<ImportStep>(ImportStep.SUCCESS)
let importStep = $ref<ImportStep>(ImportStep.CONFIRMATION)
let isImporting = $ref(false)
let reason = $ref('')
let timer = $ref(-1)

View File

@@ -1,509 +1 @@
<script setup lang="ts">
import { useBaseStore } from "@/stores/base.ts";
import { useRouter } from "vue-router";
import BaseIcon from "@/components/BaseIcon.vue";
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 { watch } from "vue";
import { getCurrentStudyWord } from "@/hooks/dict.ts";
import { useRuntimeStore } from "@/stores/runtime.ts";
import Book from "@/components/Book.vue";
import PopConfirm from "@/components/PopConfirm.vue";
import Progress from '@/components/base/Progress.vue';
import Toast from '@/components/base/toast/Toast.ts';
import BaseButton from "@/components/BaseButton.vue";
import { getDefaultDict } from "@/types/func.ts";
import DeleteIcon from "@/components/icon/DeleteIcon.vue";
import PracticeSettingDialog from "@/pages/word/components/PracticeSettingDialog.vue";
import ChangeLastPracticeIndexDialog from "@/pages/word/components/ChangeLastPracticeIndexDialog.vue";
import { useSettingStore } from "@/stores/setting.ts";
import { useFetch } from "@vueuse/core";
import { AppEnv, DICT_LIST, Host, 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 SettingDialog from "@/pages/word/components/SettingDialog.vue";
const store = useBaseStore()
const settingStore = useSettingStore()
const router = useRouter()
const {nav} = useNav()
const runtimeStore = useRuntimeStore()
let loading = $ref(true)
let isSaveData = $ref(false)
let currentStudy = $ref({
new: [],
review: [],
write: [],
shuffle: [],
})
watch(() => store.load, n => {
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() {
if (AppEnv.CAN_REQUEST) {
let res = await myDictList({type: "word"})
if (res.success) {
store.setState(Object.assign(store.$state, res.data))
}
}
if (store.word.studyIndex >= 3) {
if (!store.sdict.custom && !store.sdict.words.length) {
store.word.bookList[store.word.studyIndex] = await _getDictDataByUrl(store.sdict)
}
}
if (!currentStudy.new.length && store.sdict.words.length) {
let d = localStorage.getItem(PracticeSaveWordKey.key)
if (d) {
try {
let obj = JSON.parse(d)
currentStudy = obj.val.taskWords
isSaveData = true
} catch (e) {
localStorage.removeItem(PracticeSaveWordKey.key)
currentStudy = getCurrentStudyWord()
}
} else {
currentStudy = getCurrentStudyWord()
}
}
loading = false
}
function startPractice() {
if (store.sdict.id) {
if (!store.sdict.words.length) {
return Toast.warning('没有单词可学习!')
}
window.umami?.track('startStudyWord', {
name: store.sdict.name,
index: store.sdict.lastLearnIndex,
perDayStudyNumber: store.sdict.perDayStudyNumber,
custom: store.sdict.custom,
complete: store.sdict.complete,
wordPracticeMode: settingStore.wordPracticeMode
})
//把是否是第一次设置为false
settingStore.first = false
nav('practice-words/' + store.sdict.id, {}, {taskWords: currentStudy})
} else {
window.umami?.track('no-dict')
Toast.warning('请先选择一本词典')
}
}
let showPracticeSettingDialog = $ref(false)
let showShufflePracticeSettingDialog = $ref(false)
let showChangeLastPracticeIndexDialog = $ref(false)
let showPracticeWordListDialog = $ref(false)
async function goDictDetail(val: DictResource) {
if (!val.id) return nav('dict-list')
runtimeStore.editDict = getDefaultDict(val)
nav('dict-detail', {})
}
let isManageDict = $ref(false)
let selectIds = $ref([])
function handleBatchDel() {
selectIds.forEach(id => {
let r = store.word.bookList.findIndex(v => v.id === id)
if (r !== -1) {
if (store.word.studyIndex === r) {
store.word.studyIndex = -1
}
if (store.word.studyIndex > r) {
store.word.studyIndex--
}
store.word.bookList.splice(r, 1)
}
})
selectIds = []
Toast.success("删除成功!")
}
function toggleSelect(item) {
let rIndex = selectIds.findIndex(v => v === item.id)
if (rIndex > -1) {
selectIds.splice(rIndex, 1)
} else {
selectIds.push(item.id)
}
}
const progressTextLeft = $computed(() => {
if (store.sdict.complete) return '已学完,进入总复习阶段'
return '已学习' + store.currentStudyProgress + '%'
})
const progressTextRight = $computed(() => {
// if (store.sdict.complete) return store.sdict?.length
return store.sdict?.lastLearnIndex
})
function check(cb: Function) {
if (!store.sdict.id) {
Toast.warning('请先选择一本词典')
} else {
runtimeStore.editDict = getDefaultDict(store.sdict)
cb()
}
}
async function savePracticeSetting() {
Toast.success('修改成功')
isSaveData = false
localStorage.removeItem(PracticeSaveWordKey.key)
await store.changeDict(runtimeStore.editDict)
currentStudy = getCurrentStudyWord()
}
async function onShufflePracticeSettingOk(total) {
window.umami?.track('startShuffleStudyWord', {
name: store.sdict.name,
index: store.sdict.lastLearnIndex,
perDayStudyNumber: store.sdict.perDayStudyNumber,
total,
custom: store.sdict.custom,
complete: store.sdict.complete,
})
isSaveData = false
localStorage.removeItem(PracticeSaveWordKey.key)
let ignoreList = [store.allIgnoreWords, store.knownWords][settingStore.ignoreSimpleWord ? 0 : 1]
currentStudy.shuffle = shuffle(store.sdict.words.slice(0, store.sdict.lastLearnIndex).filter(v => !ignoreList.includes(v.word))).slice(0, total)
nav('practice-words/' + store.sdict.id, {}, {
taskWords: currentStudy,
total //用于再来一组时,随机出正确的长度,因为练习中可能会点击已掌握,导致重学一遍之后长度变少,如果再来一组,此时长度就不正确
})
}
async function saveLastPracticeIndex(e) {
Toast.success('修改成功')
runtimeStore.editDict.lastLearnIndex = e
showChangeLastPracticeIndexDialog = false
isSaveData = false
localStorage.removeItem(PracticeSaveWordKey.key)
await store.changeDict(runtimeStore.editDict)
currentStudy = getCurrentStudyWord()
}
const {
data: recommendDictList,
isFetching
} = useFetch(resourceWrap(DICT_LIST.WORD.RECOMMENDED)).json()
let isNewHost = $ref(window.location.host === Host)
</script>
<template>
<BasePage>
<div class="mb-4" v-if="!isNewHost">
新域名已启用后续请访问 <a href="https://typewords.cc/words?from_old_site=1">https://typewords.cc</a>。当前
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">
<div class="p-1 center rounded-full bg-white">
<IconFluentBookNumber20Filled class="text-xl color-link"/>
</div>
<div
@click="goDictDetail(store.sdict)"
class="text-2xl font-bold cursor-pointer">
{{ store.sdict.name || '当前无正在学习的词典' }}
</div>
</div>
<template v-if="store.sdict.id">
<div class="mt-4 flex flex-col gap-2">
<div class="">当前进度{{ progressTextLeft }}</div>
<Progress size="large" :percentage="store.currentStudyProgress" :show-text="false"></Progress>
<div class="text-sm flex justify-between">
<span>已完成 {{ progressTextRight }} / {{ store.sdict.words.length }} </span>
<span v-if="store.sdict.id">
预计完成日期{{ _getAccomplishDate(store.sdict.words.length, store.sdict.perDayStudyNumber) }}
</span>
</div>
</div>
<div class="flex items-center mt-4 gap-4">
<BaseButton type="info"
size="small"
@click="router.push('/dict-list')">
<div class="center gap-1">
<IconFluentArrowSwap20Regular/>
<span>选择词典</span>
</div>
</BaseButton>
<PopConfirm
:disabled="!isSaveData"
title="当前存在未完成的学习任务,修改会重新生成学习任务,是否继续?"
@confirm="check(()=>showChangeLastPracticeIndexDialog = true)">
<BaseButton type="info"
size="small"
v-if="store.sdict.id"
>
<div class="center gap-1">
<IconFluentSlideTextTitleEdit20Regular/>
<span>更改进度</span>
</div>
</BaseButton>
</PopConfirm>
</div>
</template>
<div class="flex items-center gap-4 mt-2 flex-1" v-else>
<div class="title">请选择一本词典开始学习</div>
<BaseButton id="step1" type="primary" size="large" @click="router.push('/dict-list')">
<div class="center gap-1">
<IconFluentAdd16Regular/>
<span>选择词典</span>
</div>
</BaseButton>
</div>
</div>
<div class="flex-1 w-full mt-4 md:mt-0" :class="!store.sdict.id && 'opacity-30 cursor-not-allowed'">
<div class="flex justify-between">
<div class="flex items-center gap-2">
<div class="p-2 center rounded-full bg-white ">
<IconFluentStar20Filled class="text-lg color-amber"/>
</div>
<div class="text-xl font-bold">
{{ isSaveData ? '上次任务' : '今日任务' }}
</div>
<span class="color-link cursor-pointer"
v-if="store.sdict.id"
@click="showPracticeWordListDialog = true">词表</span>
</div>
<div class="flex gap-1 items-center"
v-if="store.sdict.id"
>
每日目标
<div style="color:#ac6ed1;"
class="bg-third px-2 h-10 flex center text-2xl rounded">
{{ store.sdict.id ? store.sdict.perDayStudyNumber : 0 }}
</div>
个单词
<PopConfirm
:disabled="!isSaveData"
title="当前存在未完成的学习任务,修改会重新生成学习任务,是否继续?"
@confirm="check(()=>showPracticeSettingDialog = true)">
<BaseButton
type="info" size="small">更改
</BaseButton>
</PopConfirm>
</div>
</div>
<div class="flex mt-4 justify-between">
<div class="stat">
<div class="num">{{ currentStudy.new.length }}</div>
<div class="txt">新词数</div>
</div>
<template v-if="settingStore.wordPracticeMode === WordPracticeMode.System">
<div class="stat">
<div class="num">{{ currentStudy.review.length }}</div>
<div class="txt">复习上次</div>
</div>
<div class="stat">
<div class="num">{{ currentStudy.write.length }}</div>
<div class="txt">复习之前</div>
</div>
</template>
</div>
<div class="flex items-end mt-4">
<BaseButton size="large"
class="flex-1"
:disabled="!store.sdict.id"
:loading="loading"
@click="startPractice">
<div class="flex items-center gap-2">
<span class="line-height-[2]">{{ isSaveData ? '继续学习' : '开始学习' }}</span>
<IconFluentArrowCircleRight16Regular class="text-xl"/>
</div>
</BaseButton>
<div
v-if="false"
class="w-full flex box-border cp color-white">
<div
@click="startPractice"
class="flex-1 rounded-l-lg center gap-2 py-1 bg-[var(--btn-primary)] hover:opacity-50">
<span class="line-height-[2]">{{ isSaveData ? '继续学习' : '开始学习' }}</span>
<IconFluentArrowCircleRight16Regular class="text-xl"/>
</div>
<div class="relative group">
<div
class="w-10 rounded-r-lg h-full center bg-[var(--btn-primary)] hover:bg-gray border-solid border-2 border-l-gray border-transparent box-border">
<IconFluentChevronDown20Regular/>
</div>
<div
class="space-y-2 pt-2 absolute z-2 right-0 border rounded opacity-0 scale-95
group-hover:opacity-100 group-hover:scale-100
transition-all duration-150 pointer-events-none group-hover:pointer-events-auto"
>
<div>
<BaseButton
size="large" type="orange"
:loading="loading"
@click="check(()=>showShufflePracticeSettingDialog = true)">
<div class="flex items-center gap-2">
<span class="line-height-[2]">随机复习</span>
<IconFluentArrowShuffle20Filled class="text-xl"/>
</div>
</BaseButton>
</div>
<div>
<BaseButton
size="large" type="orange"
:loading="loading"
@click="check(()=>showShufflePracticeSettingDialog = true)">
<div class="flex items-center gap-2">
<span class="line-height-[2]">重新学习</span>
<IconFluentArrowShuffle20Filled class="text-xl"/>
</div>
</BaseButton>
</div>
</div>
</div>
</div>
<BaseButton
v-if="store.sdict.id && store.sdict.lastLearnIndex"
size="large" type="orange"
:loading="loading"
@click="check(()=>showShufflePracticeSettingDialog = true)">
<div class="flex items-center gap-2">
<span class="line-height-[2]">随机复习</span>
<IconFluentArrowShuffle20Filled class="text-xl"/>
</div>
</BaseButton>
</div>
</div>
</div>
<div class="card flex flex-col">
<div class="flex justify-between">
<div class="title">我的词典</div>
<div class="flex gap-4 items-center">
<PopConfirm title="确认删除所有选中词典?" @confirm="handleBatchDel" v-if="selectIds.length">
<BaseIcon class="del" title="删除">
<DeleteIcon/>
</BaseIcon>
</PopConfirm>
<div class="color-link cursor-pointer" v-if="store.word.bookList.length > 3"
@click="isManageDict = !isManageDict; selectIds = []">{{ isManageDict ? '取消' : '管理词典' }}
</div>
<div class="color-link cursor-pointer" @click="nav('dict-detail', { isAdd: true })">创建个人词典</div>
</div>
</div>
<div class="flex gap-4 flex-wrap mt-4">
<Book :is-add="false" quantifier="个词" :item="item" :checked="selectIds.includes(item.id)"
@check="() => toggleSelect(item)" :show-checkbox="isManageDict && j >= 3"
v-for="(item, j) in store.word.bookList" @click="goDictDetail(item)"/>
<Book :is-add="true" @click="router.push('/dict-list')"/>
</div>
</div>
<div class="card flex flex-col overflow-hidden" v-loading="isFetching">
<div class="flex justify-between">
<div class="title">推荐</div>
<div class="flex gap-4 items-center">
<div class="color-link cursor-pointer" @click="router.push('/dict-list')">更多</div>
</div>
</div>
<div class="flex gap-4 flex-wrap mt-4 min-h-50">
<Book :is-add="false"
quantifier="个词"
:item="item as any"
v-for="(item, j) in recommendDictList" @click="goDictDetail(item as any)"/>
</div>
</div>
</BasePage>
<PracticeSettingDialog
:show-left-option="false"
v-model="showPracticeSettingDialog"
@ok="savePracticeSetting"/>
<ChangeLastPracticeIndexDialog
v-model="showChangeLastPracticeIndexDialog"
@ok="saveLastPracticeIndex"
/>
<PracticeWordListDialog
:data="currentStudy"
v-model="showPracticeWordListDialog"
/>
<ShufflePracticeSettingDialog
v-model="showShufflePracticeSettingDialog"
@ok="onShufflePracticeSettingOk"/>
</template>
<style scoped lang="scss">
.stat {
@apply w-31% box-border flex flex-col items-center justify-center rounded-xl p-2 bg-[var(--bg-history)];
border: 1px solid gainsboro;
.num {
@apply color-[#409eff] text-4xl font-bold;
}
.txt {
@apply color-gray-500;
}
}
</style>
WordsPage.vue

View File

@@ -7,6 +7,7 @@ import { PracticeData, WordPracticeType, ShortcutKey, TaskWords } from "@/types/
import BaseIcon from "@/components/BaseIcon.vue";
import Tooltip from "@/components/base/Tooltip.vue";
import Progress from '@/components/base/Progress.vue'
import SettingDialog from "@/pages/word/components/SettingDialog.vue";
const statStore = usePracticeStore()
const settingStore = useSettingStore()
@@ -123,6 +124,8 @@ const progress = $computed(() => {
</div>
</div>
<div class="flex gap-2 justify-center items-center" id="toolbar-icons">
<SettingDialog type="word"/>
<BaseIcon
v-if="statStore.step < 9"
@click="emit('skipStep')"

View File

@@ -1,11 +1,10 @@
<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 {ShortcutKey} 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";
@@ -14,12 +13,19 @@ 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";
import {defineAsyncComponent} from "vue";
import BaseIcon from "@/components/BaseIcon.vue";
const tabIndex = $ref(1)
const Dialog = defineAsyncComponent(() => import('@/components/dialog/Dialog.vue'))
const props = defineProps<{
type: 'article' | 'word'
}>()
const tabIndex = $ref(props.type === 'word' ? 1 : 2)
const settingStore = useSettingStore()
const runtimeStore = useRuntimeStore()
const store = useBaseStore()
let show = $ref(false)
const simpleWords = $computed({
get: () => store.simpleWords.join(','),
@@ -35,23 +41,22 @@ const simpleWords = $computed({
</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>
<Dialog v-model="show" title="设置">
<div class="setting text-lg w-200 h-[50vh] text-md flex flex-col">
<div class="flex flex-1 overflow-hidden">
<div class="left">
<div class="tabs">
<div class="tab" :class="tabIndex === 1 && 'active'" @click="tabIndex = 1">
<div class="tab" :class="tabIndex === 1 && 'active'" @click="tabIndex = 1" v-if="type === 'word'">
<IconFluentTextUnderlineDouble20Regular width="20"/>
<span>单词练习设置</span>
<span>单词</span>
</div>
<div class="tab" :class="tabIndex === 2 && 'active'" @click="tabIndex = 2">
<div class="tab" :class="tabIndex === 2 && 'active'" @click="tabIndex = 2" v-if="type === 'article'">
<IconFluentBookLetter20Regular width="20"/>
<span>文章练习设置</span>
<span>文章</span>
</div>
<div class="tab" :class="tabIndex === 0 && 'active'" @click="tabIndex = 0">
<IconFluentSettings20Regular width="20"/>
<span>通用练习设置</span>
<span>通用</span>
</div>
</div>
</div>
@@ -93,7 +98,9 @@ const simpleWords = $computed({
<!-- 音效-->
<div class="line"></div>
<SettingItem main-title="音效"/>
<SettingItem title="单词/句子发音口音">
<SettingItem title="单词/句子发音口音"
desc="仅单词生效,文章固定美音"
>
<Select v-model="settingStore.soundType"
placeholder="请选择"
class="w-50!"
@@ -138,12 +145,12 @@ const simpleWords = $computed({
<!-- 单词练习设置-->
<!-- 单词练习设置-->
<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="练习模式">-->
<!-- <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="开启后,练习中会在上方显示上一个/下一个单词"
@@ -277,7 +284,10 @@ const simpleWords = $computed({
</div>
</div>
</div>
</BasePage>
</Dialog>
<BaseIcon title="设置" @click="show = true;tabIndex = props.type === 'word' ? 1 : 2">
<IconFluentSettings20Regular/>
</BaseIcon>
</template>
<style scoped lang="scss">
@@ -289,10 +299,10 @@ const simpleWords = $computed({
flex-direction: column;
justify-content: space-between;
align-items: center;
border-right: 2px solid gainsboro;
border-right: 1px solid gainsboro;
.tabs {
padding: .6rem 1.6rem;
padding: 1rem;
display: flex;
flex-direction: column;
gap: .6rem;
@@ -302,17 +312,18 @@ const simpleWords = $computed({
@apply cursor-pointer flex items-center relative;
padding: .6rem .9rem;
border-radius: .5rem;
width: 10rem;
gap: .6rem;
transition: all .5s;
&:hover {
background: var(--color-select-bg);
color: var(--color-select-text);
background: var(--btn-primary);
color: white;
}
&.active {
background: var(--color-select-bg);
color: var(--color-select-text);
background: var(--btn-primary);
color: white;
}
}
}