feat:autoplay
This commit is contained in:
2
components.d.ts
vendored
2
components.d.ts
vendored
@@ -73,7 +73,9 @@ declare module 'vue' {
|
||||
IconFluentSearch24Regular: typeof import('~icons/fluent/search24-regular')['default']
|
||||
IconFluentSettings20Regular: typeof import('~icons/fluent/settings20-regular')['default']
|
||||
IconFluentShieldQuestion20Regular: typeof import('~icons/fluent/shield-question20-regular')['default']
|
||||
IconFluentSpeaker220Regular: typeof import('~icons/fluent/speaker220-regular')['default']
|
||||
IconFluentSpeakerEdit20Regular: typeof import('~icons/fluent/speaker-edit20-regular')['default']
|
||||
IconFluentSpeakerSettings20Regular: typeof import('~icons/fluent/speaker-settings20-regular')['default']
|
||||
IconFluentStar12Regular: typeof import('~icons/fluent/star12-regular')['default']
|
||||
IconFluentStar16Filled: typeof import('~icons/fluent/star16-filled')['default']
|
||||
IconFluentStar16Regular: typeof import('~icons/fluent/star16-regular')['default']
|
||||
|
||||
@@ -348,6 +348,10 @@ a {
|
||||
gap: .5rem;
|
||||
color: var(--color-main-text);
|
||||
|
||||
span{
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.word {
|
||||
font-size: 1.2rem;
|
||||
display: flex;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import {ref, computed, watch, useAttrs} from 'vue';
|
||||
import { computed, ref, useAttrs, watch } from 'vue';
|
||||
|
||||
interface IProps {
|
||||
src?: string;
|
||||
@@ -20,6 +20,9 @@ const props = withDefaults(defineProps<IProps>(), {
|
||||
disabled: false
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
ended: []
|
||||
}>();
|
||||
|
||||
const attrs = useAttrs();
|
||||
|
||||
@@ -119,10 +122,6 @@ const handlePause = () => {
|
||||
isPlaying.value = false;
|
||||
};
|
||||
|
||||
const emit = defineEmits<{
|
||||
ended: []
|
||||
}>();
|
||||
|
||||
const handleEnded = () => {
|
||||
isPlaying.value = false;
|
||||
currentTime.value = 0;
|
||||
|
||||
@@ -47,8 +47,8 @@ defineExpose({scrollToBottom, scrollToItem})
|
||||
<slot name="prefix" :item="item" :index="index"></slot>
|
||||
</template>
|
||||
<template v-slot="{ item, index }">
|
||||
<div class="item-title word-title">
|
||||
<span class="index">{{ index + 1 }}.</span>
|
||||
<div class="item-title">
|
||||
<span class="text-sm">{{ index + 1 }}.</span>
|
||||
<span class="word" :class="!showWord && 'word-shadow'">{{ item.word }}</span>
|
||||
<span class="phonetic" :class="!showWord && 'word-shadow'">{{ item.phonetic0 }}</span>
|
||||
<VolumeIcon class="volume" @click="playWordAudio(item.word)"></VolumeIcon>
|
||||
@@ -70,13 +70,4 @@ defineExpose({scrollToBottom, scrollToItem})
|
||||
<slot name="suffix" :item="item" :index="index"></slot>
|
||||
</template>
|
||||
</BaseList>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.word-title{
|
||||
display: flex;
|
||||
span{
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</template>
|
||||
@@ -4,23 +4,25 @@ import BasePage from "@/components/BasePage.vue";
|
||||
import BackIcon from "@/components/BackIcon.vue";
|
||||
import Empty from "@/components/Empty.vue";
|
||||
import ArticleList from "@/components/list/ArticleList.vue";
|
||||
import {useBaseStore} from "@/stores/base.ts";
|
||||
import {Article, Dict, DictId, DictType} from "@/types/types.ts";
|
||||
import {useRuntimeStore} from "@/stores/runtime.ts";
|
||||
import { useBaseStore } from "@/stores/base.ts";
|
||||
import { Article, Dict, DictId, DictType } from "@/types/types.ts";
|
||||
import { useRuntimeStore } from "@/stores/runtime.ts";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import {useRoute, useRouter} from "vue-router";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import EditBook from "@/pages/article/components/EditBook.vue";
|
||||
import {computed, onMounted} from "vue";
|
||||
import {_dateFormat, _getDictDataByUrl, cloneDeep, msToHourMinute, total, useNav} from "@/utils";
|
||||
import { computed, onMounted } from "vue";
|
||||
import { _dateFormat, _getDictDataByUrl, cloneDeep, msToHourMinute, total, useNav } from "@/utils";
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
import {useArticleOptions} from "@/hooks/dict.ts";
|
||||
import {getDefaultArticle, getDefaultDict} from "@/types/func.ts";
|
||||
import { useArticleOptions } from "@/hooks/dict.ts";
|
||||
import { getDefaultArticle, getDefaultDict } from "@/types/func.ts";
|
||||
import Toast from "@/components/base/toast/Toast.ts";
|
||||
import ArticleAudio from "@/pages/article/components/ArticleAudio.vue";
|
||||
import {MessageBox} from "@/utils/MessageBox.tsx";
|
||||
import { MessageBox } from "@/utils/MessageBox.tsx";
|
||||
import book_list from "@/assets/book-list.json";
|
||||
import { useSettingStore } from "@/stores/setting.ts";
|
||||
|
||||
const runtimeStore = useRuntimeStore()
|
||||
const settingStore = useSettingStore()
|
||||
const base = useBaseStore()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
@@ -143,11 +145,20 @@ const currentPractice = $computed(() => {
|
||||
|
||||
const totalSpend = $computed(() => {
|
||||
if (runtimeStore.editDict.statistics?.length) {
|
||||
return msToHourMinute(total(runtimeStore.editDict.statistics,'spend'))
|
||||
return msToHourMinute(total(runtimeStore.editDict.statistics, 'spend'))
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
function next() {
|
||||
if (!settingStore.articleAutoPlayNext) return
|
||||
let index = runtimeStore.editDict.articles.findIndex(v => v.id === selectArticle.id)
|
||||
if (index > -1) {
|
||||
//如果是最后一个
|
||||
if (index === runtimeStore.editDict.articles.length - 1) index = -1
|
||||
selectArticle = runtimeStore.editDict.articles[index + 1]
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -194,18 +205,20 @@ const totalSpend = $computed(() => {
|
||||
<div v-if="selectArticle.id">
|
||||
<div class="font-family text-base mb-4 pr-2" v-if="currentPractice.length">
|
||||
<div class="text-2xl font-bold">学习记录</div>
|
||||
<div class="mt-1 mb-3">总学习时长:{{ msToHourMinute(total(currentPractice, 'spend'))}}</div>
|
||||
<div class="item border border-item border-solid mt-2 p-2 bg-[var(--bg-history)] rounded-md flex justify-between" v-for="i in currentPractice">
|
||||
<span class="color-gray ">{{_dateFormat(i.startDate,'YYYY/MM/DD HH:mm')}}</span> <span>{{ msToHourMinute(i.spend) }}</span>
|
||||
<div class="mt-1 mb-3">总学习时长:{{ msToHourMinute(total(currentPractice, 'spend')) }}</div>
|
||||
<div
|
||||
class="item border border-item border-solid mt-2 p-2 bg-[var(--bg-history)] rounded-md flex justify-between"
|
||||
v-for="i in currentPractice">
|
||||
<span class="color-gray ">{{ _dateFormat(i.startDate, 'YYYY/MM/DD HH:mm') }}</span>
|
||||
<span>{{ msToHourMinute(i.spend) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="en-article-family title text-xl">
|
||||
<div class="text-center text-2xl my-2">
|
||||
<ArticleAudio
|
||||
:article="selectArticle"
|
||||
:article-list="runtimeStore.editDict.articles"
|
||||
:current-index="currentArticleIndex"
|
||||
@play-next="handlePlayNext"></ArticleAudio>
|
||||
<ArticleAudio
|
||||
:article="selectArticle"
|
||||
:autoplay="settingStore.articleAutoPlayNext"
|
||||
@ended="next"/>
|
||||
</div>
|
||||
<div class="text-center text-2xl">{{ selectArticle.title }}</div>
|
||||
<div class="text-2xl" v-if="selectArticle.text">
|
||||
|
||||
@@ -33,9 +33,9 @@ import ConflictNotice from "@/components/ConflictNotice.vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import book_list from "@/assets/book-list.json";
|
||||
import PracticeLayout from "@/components/PracticeLayout.vue";
|
||||
import Switch from "@/components/base/Switch.vue";
|
||||
import ArticleAudio from "@/pages/article/components/ArticleAudio.vue";
|
||||
import { PracticeSaveArticleKey } from "@/utils/const.ts";
|
||||
import VolumeSetting from "@/pages/article/components/VolumeSetting.vue";
|
||||
|
||||
const store = useBaseStore()
|
||||
const settingStore = useSettingStore()
|
||||
@@ -497,17 +497,14 @@ provide('currentPractice', currentPractice)
|
||||
<div class="name">单词总数</div>
|
||||
</div>
|
||||
</div>
|
||||
<ArticleAudio
|
||||
ref="audioRef"
|
||||
:article="articleData.article"
|
||||
:article-list="articleData.list"
|
||||
:current-index="store.sbook.lastLearnIndex"
|
||||
@play-next="handlePlayNext"></ArticleAudio>
|
||||
<ArticleAudio
|
||||
ref="audioRef"
|
||||
:article="articleData.article"
|
||||
:autoplay="settingStore.articleAutoPlayNext"
|
||||
@ended="settingStore.articleAutoPlayNext && next()"></ArticleAudio>
|
||||
<div class="flex flex-col items-center justify-center gap-1">
|
||||
<div class="flex gap-2 center">
|
||||
<Tooltip title="自动发音">
|
||||
<Switch v-model="settingStore.articleSound"/>
|
||||
</Tooltip>
|
||||
<VolumeSetting/>
|
||||
<BaseIcon
|
||||
:title="`下一句(${settingStore.shortcutKeyMap[ShortcutKey.Next]})`"
|
||||
@click="skip">
|
||||
@@ -518,7 +515,6 @@ provide('currentPractice', currentPractice)
|
||||
@click="play">
|
||||
<IconFluentReplay20Regular/>
|
||||
</BaseIcon>
|
||||
|
||||
<BaseIcon
|
||||
@click="settingStore.dictation = !settingStore.dictation"
|
||||
:title="`开关默写模式(${settingStore.shortcutKeyMap[ShortcutKey.ToggleDictation]})`"
|
||||
|
||||
@@ -1,51 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import {Article} from "@/types/types.ts";
|
||||
import {watch} from "vue";
|
||||
import {LOCAL_FILE_KEY} from "@/utils/const.ts";
|
||||
import {get} from "idb-keyval";
|
||||
import { Article } from "@/types/types.ts";
|
||||
import { watch } from "vue";
|
||||
import { LOCAL_FILE_KEY } from "@/utils/const.ts";
|
||||
import { get } from "idb-keyval";
|
||||
import Audio from "@/components/base/Audio.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
article: Article
|
||||
articleList?: Article[]
|
||||
currentIndex?: number
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
ended: []
|
||||
}>();
|
||||
|
||||
|
||||
let file = $ref(null)
|
||||
let instance = $ref<{ audioRef: HTMLAudioElement }>({audioRef: null})
|
||||
let shouldAutoPlay = $ref(false) // 标记是否应该自动播放
|
||||
|
||||
const emit = defineEmits<{
|
||||
playNext: [nextArticle: Article]
|
||||
}>()
|
||||
|
||||
// 处理音频播放结束,自动播放下一个
|
||||
const handleAudioEnded = () => {
|
||||
if (props.articleList && props.currentIndex !== undefined) {
|
||||
const nextIndex = props.currentIndex + 1
|
||||
if (nextIndex < props.articleList.length) {
|
||||
const nextArticle = props.articleList[nextIndex]
|
||||
if (nextArticle.audioSrc || nextArticle.audioFileId) {
|
||||
shouldAutoPlay = true // 设置自动播放标记
|
||||
emit('playNext', nextArticle)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 当音频源改变时,如果需要自动播放则开始播放
|
||||
const startAutoPlay = async () => {
|
||||
if (shouldAutoPlay && instance?.audioRef) {
|
||||
shouldAutoPlay = false // 重置标记
|
||||
try {
|
||||
// 等待一小段时间确保音频元素已经准备好
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
await instance.audioRef.play()
|
||||
} catch (error) {
|
||||
console.error('自动播放失败:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.article.audioFileId, async () => {
|
||||
if (!props.article.audioSrc && props.article.audioFileId) {
|
||||
@@ -54,25 +24,13 @@ watch(() => props.article.audioFileId, async () => {
|
||||
let rItem = list.find((file) => file.id === props.article.audioFileId)
|
||||
if (rItem) {
|
||||
file = URL.createObjectURL(rItem.file)
|
||||
// 当文件加载完成后尝试自动播放
|
||||
await new Promise(resolve => setTimeout(resolve, 50))
|
||||
startAutoPlay()
|
||||
}
|
||||
}
|
||||
}else {
|
||||
} else {
|
||||
file = null
|
||||
}
|
||||
}, {immediate: true})
|
||||
|
||||
// 监听音频源变化,触发自动播放
|
||||
watch(() => props.article.audioSrc, async (newSrc) => {
|
||||
if (newSrc) {
|
||||
// 当音频源改变后尝试自动播放
|
||||
await new Promise(resolve => setTimeout(resolve, 50))
|
||||
startAutoPlay()
|
||||
}
|
||||
})
|
||||
|
||||
//转发一遍,这里Proxy的默认值不能为{},可能是vue做了什么
|
||||
defineExpose(new Proxy({
|
||||
currentTime: 0,
|
||||
@@ -105,9 +63,10 @@ defineExpose(new Proxy({
|
||||
<Audio v-bind="$attrs" ref="instance"
|
||||
v-if="props.article.audioSrc"
|
||||
:src="props.article.audioSrc"
|
||||
@ended="handleAudioEnded"/>
|
||||
<Audio ref="instance" v-else-if="file"
|
||||
@ended="emit('ended')"/>
|
||||
<Audio v-bind="$attrs" ref="instance"
|
||||
v-else-if="file"
|
||||
:src="file"
|
||||
@ended="handleAudioEnded"
|
||||
@ended="emit('ended')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -415,7 +415,7 @@ function setStartTime(val: Sentence, i: number, j: number) {
|
||||
<div class="flex gap-2">
|
||||
<div class="title">结果</div>
|
||||
<div class="flex gap-2 flex-1 justify-end">
|
||||
<ArticleAudio ref="audioRef" :article="editArticle"/>
|
||||
<ArticleAudio ref="audioRef" :article="editArticle" :autoplay="false"/>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="editArticle?.sections?.length">
|
||||
@@ -524,7 +524,10 @@ function setStartTime(val: Sentence, i: number, j: number) {
|
||||
教程:点击音频播放按钮,当播放到句子开始时,点击开始时间的 <span class="color-red">记录</span>
|
||||
按钮;当播放到句子结束时,点击结束时间的 <span class="color-red">记录</span> 按钮,最后再试听是否正确
|
||||
</div>
|
||||
<ArticleAudio ref="sentenceAudioRef" :article="editArticle" class="w-full"/>
|
||||
<ArticleAudio ref="sentenceAudioRef"
|
||||
:article="editArticle"
|
||||
:autoplay="false"
|
||||
class="w-full"/>
|
||||
<div class="flex items-center gap-2 space-between mb-2" v-if="editSentence.audioPosition?.length">
|
||||
<div>{{ editSentence.text }}</div>
|
||||
<div class="flex items-center gap-2 shrink-0">
|
||||
|
||||
44
src/pages/article/components/VolumeSetting.vue
Normal file
44
src/pages/article/components/VolumeSetting.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<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">
|
||||
@import "@/assets/css/style";
|
||||
</style>
|
||||
@@ -605,9 +605,12 @@ function importOldData() {
|
||||
<!-- 发音-->
|
||||
<div class="line"></div>
|
||||
<SettingItem mainTitle="音效"/>
|
||||
<SettingItem title="自动发音">
|
||||
<SettingItem title="自动播放句子">
|
||||
<Switch v-model="settingStore.articleSound"/>
|
||||
</SettingItem>
|
||||
<SettingItem title="自动播放下一篇">
|
||||
<Switch v-model="settingStore.articleAutoPlayNext"/>
|
||||
</SettingItem>
|
||||
<SettingItem title="音量">
|
||||
<Slider v-model="settingStore.articleSoundVolume"/>
|
||||
<span class="w-10 pl-5">{{ settingStore.articleSoundVolume }}%</span>
|
||||
|
||||
@@ -57,8 +57,6 @@ let data = $ref<PracticeData>({
|
||||
wrongWords: [],
|
||||
})
|
||||
|
||||
let isRandomWrite = false;
|
||||
|
||||
async function loadDict() {
|
||||
// console.log('load好了开始加载')
|
||||
let dict = getDefaultDict()
|
||||
@@ -231,8 +229,7 @@ function next(isTyping: boolean = true) {
|
||||
|
||||
//开始默写新词
|
||||
if (statStore.step === 0) {
|
||||
if (settingStore.wordPracticeMode === 1 || isRandomWrite) {
|
||||
isRandomWrite = false
|
||||
if (settingStore.wordPracticeMode === 1) {
|
||||
console.log('自由模式,全完学完了')
|
||||
showStatDialog = true
|
||||
localStorage.removeItem(PracticeSaveWordKey.key)
|
||||
@@ -388,7 +385,6 @@ function randomWrite() {
|
||||
data.words = shuffle(data.words);
|
||||
data.index = 0
|
||||
settingStore.dictation = true
|
||||
isRandomWrite = true
|
||||
}
|
||||
function nextRandomWrite() {
|
||||
console.log('继续随机默写')
|
||||
|
||||
@@ -12,6 +12,7 @@ export interface SettingState {
|
||||
wordSoundSpeed: number,
|
||||
|
||||
articleSound: boolean,
|
||||
articleAutoPlayNext: boolean,
|
||||
articleSoundVolume: number,
|
||||
articleSoundSpeed: number,
|
||||
|
||||
@@ -60,6 +61,7 @@ export const getDefaultSettingState = (): SettingState => ({
|
||||
wordSoundSpeed: 1,
|
||||
|
||||
articleSound: true,
|
||||
articleAutoPlayNext: false,
|
||||
articleSoundVolume: 100,
|
||||
articleSoundSpeed: 1,
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ export const SAVE_DICT_KEY = {
|
||||
}
|
||||
export const SAVE_SETTING_KEY = {
|
||||
key: 'typing-word-setting',
|
||||
version: 14
|
||||
version: 15
|
||||
}
|
||||
export const EXPORT_DATA_KEY = {
|
||||
key: 'typing-word-export',
|
||||
|
||||
Reference in New Issue
Block a user