This commit is contained in:
zyronon
2025-09-13 19:56:02 +08:00
parent 02e056a0c2
commit fdd872090c
20 changed files with 5518 additions and 4426 deletions

2
components.d.ts vendored
View File

@@ -71,6 +71,8 @@ declare module 'vue' {
IconFluentTranslateOff16Regular: typeof import('~icons/fluent/translate-off16-regular')['default']
IconFluentWeatherMoon16Regular: typeof import('~icons/fluent/weather-moon16-regular')['default']
IconFluentWeatherSunny16Regular: typeof import('~icons/fluent/weather-sunny16-regular')['default']
IconIconParkOutlineAddMusic: typeof import('~icons/icon-park-outline/add-music')['default']
IconIconParkSolidAddMusic: typeof import('~icons/icon-park-solid/add-music')['default']
IconMaterialSymbolsMail: typeof import('~icons/material-symbols/mail')['default']
IconRiTwitterFill: typeof import('~icons/ri/twitter-fill')['default']
IconSimpleIconsGithub: typeof import('~icons/simple-icons/github')['default']

View File

@@ -39,6 +39,8 @@
"@iconify-json/bx": "^1.2.2",
"@iconify-json/eos-icons": "^1.2.4",
"@iconify-json/fluent": "^1.2.28",
"@iconify-json/icon-park-outline": "^1.2.4",
"@iconify-json/icon-park-solid": "^1.2.4",
"@iconify-json/material-symbols": "^1.2.33",
"@iconify-json/ri": "^1.2.5",
"@iconify-json/simple-icons": "^1.2.48",

9478
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -937,8 +937,69 @@
"textTranslate": "我喜欢在乡间旅行,但却不愿意迷路。 \n最近我作了一次短途旅行 \n但这次旅行所花费的时间比我预计的要长。 \n“我要去伍德福德草地”我一上车就对售票员说 \n“但我不知道它在那儿。” \n“我来告诉您在哪儿下车” 售票员回答说。 \n我坐在汽车的前部以便饱览农村风光。 \n过了一些时候车停了。 \n我环视了一下身旁惊奇地发现车里就只剩我一个乘客了。 \n“您得在这里下车”售票员说“我们的车就到此为止了。” \n“这里是伍德福德草地吗” 我问道。 \n“哎呀”售票员突然说 “我忘了让您下车了。” \n“没关系”我说“我就在这儿下吧。” \n“我们现在要返回去”售票员说。 \n“好吧既然如此我还是留在车上吧。”我回答说。",
"newWords": [],
"textAllWords": [],
"audioSrc": "",
"lrcPosition": []
"audioSrc": "blob:http://localhost:3000/e561fd93-4d0c-4d0d-9718-784d78af70ff",
"lrcPosition": [
[
15.56,
21.92
],
[
21.92,
25.52
],
[
25.52,
29.21
],
[
29.21,
36.53
],
[
36.53,
39.71
],
[
39.71,
45.24
],
[
45.24,
51.31
],
[
51.31,
55.56
],
[
55.56,
63.37
],
[
63.37,
71.71
],
[
71.71,
76.18
],
[
76.18,
82.75
],
[
82.75,
87.94
],
[
87.94,
92.73
],
[
92.73,
98.9
]
]
},
{
"id": "HnoyBu",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 31 KiB

View File

@@ -4,20 +4,47 @@ import {BaseState, useBaseStore} from "@/stores/base.ts";
import {useRuntimeStore} from "@/stores/runtime.ts";
import {useSettingStore} from "@/stores/setting.ts";
import useTheme from "@/hooks/theme.ts";
import {SAVE_DICT_KEY, SAVE_SETTING_KEY} from "@/utils/const.ts";
import {LOCAL_FILE_KEY, SAVE_DICT_KEY, SAVE_SETTING_KEY} from "@/utils/const.ts";
import {shakeCommonDict} from "@/utils";
import {routes} from "@/router.ts";
import {set} from 'idb-keyval'
import {get, set} from 'idb-keyval'
import {useRoute} from "vue-router";
import {DictId} from "@/types/types.ts";
import {curry} from "lodash-es";
const store = useBaseStore()
const runtimeStore = useRuntimeStore()
const settingStore = useSettingStore()
const {setTheme} = useTheme()
let lastAudioFileIdList = []
watch(store.$state, (n: BaseState) => {
set(SAVE_DICT_KEY.key, JSON.stringify({val: shakeCommonDict(n), version: SAVE_DICT_KEY.version}))
let data = shakeCommonDict(n)
set(SAVE_DICT_KEY.key, JSON.stringify({val: data, version: SAVE_DICT_KEY.version}))
//筛选自定义和收藏
let bookList = data.article.bookList.filter(v => v.custom || [DictId.articleCollect].includes(v.id))
let audioFileIdList = []
bookList.forEach(v => {
//筛选 audioFileId 字体有值的
v.articles.filter(s => !s.audioSrc && s.audioFileId).forEach(a => {
//所有 id 存起来下次直接判断字符串是否相等因为这个watch会频繁调用
audioFileIdList.push(a.audioFileId)
})
})
if (audioFileIdList.toString() !== lastAudioFileIdList.toString()) {
let result = []
//删除未使用到的文件
get(LOCAL_FILE_KEY).then((fileList: Array<{ id: string, file: Blob }>) => {
audioFileIdList.forEach(a => {
let item = fileList.find(b => b.id === a)
item && result.push(item)
})
set(LOCAL_FILE_KEY, result)
lastAudioFileIdList = audioFileIdList
})
}
})
watch(settingStore.$state, (n) => {
@@ -36,6 +63,7 @@ onMounted(init)
let transitionName = $ref('go')
const route = useRoute()
watch(() => route.path, (to, from) => {
return transitionName = ''
// console.log('watch', to, from)
// //footer下面的5个按钮对跳不要用动画
let noAnimation = [
@@ -56,13 +84,14 @@ watch(() => route.path, (to, from) => {
</script>
<template>
<router-view v-slot="{ Component }">
<transition :name="transitionName">
<keep-alive :exclude="runtimeStore.excludeRoutes">
<component :is="Component"/>
</keep-alive>
</transition>
</router-view>
<!-- <router-view v-slot="{ Component }">-->
<!-- <transition :name="transitionName">-->
<!-- <keep-alive :exclude="runtimeStore.excludeRoutes">-->
<!-- <component :is="Component"/>-->
<!-- </keep-alive>-->
<!-- </transition>-->
<!-- </router-view>-->
<router-view></router-view>
</template>
<style scoped lang="scss">

View File

@@ -538,8 +538,8 @@ export function usePlaySentenceAudio() {
const settingStore = useSettingStore()
let timer = $ref(0)
function playSentenceAudio(sentence: Sentence, ref?: HTMLAudioElement, article?: Article) {
if (sentence.audioPosition?.length && article.audioSrc && ref) {
function playSentenceAudio(sentence: Sentence, ref?: HTMLAudioElement,) {
if (sentence.audioPosition?.length && ref && ref.src) {
clearTimeout(timer)
if (ref.played) {
ref.pause()

View File

@@ -158,7 +158,7 @@ useWindowClick(() => showExport = false)
<template>
<div class="add-article">
<div class="aslide">
<header class="flex justify-between items-center">
<header class="flex gap-2 items-center">
<BackIcon/>
<div class="text-xl">{{ runtimeStore.editDict.name }}</div>
</header>
@@ -239,7 +239,7 @@ useWindowClick(() => showExport = false)
display: flex;
flex-direction: column;
$height: 4rem;
$height: 3rem;
header {
height: $height;
@@ -254,7 +254,7 @@ useWindowClick(() => showExport = false)
}
.add {
width: 16rem;
width: 100%;
box-sizing: border-box;
border-radius: .5rem;
margin-bottom: .6rem;
@@ -262,8 +262,8 @@ useWindowClick(() => showExport = false)
display: flex;
justify-content: space-between;
transition: all .3s;
color: var(--color-font-1);
background: var(--color-item-active);
color: var(--color-font-active-1);
background: var(--color-select-bg);
}
.footer {

View File

@@ -302,7 +302,7 @@ const {playSentenceAudio} = usePlaySentenceAudio()
function play2(e) {
if (settingStore.articleSound || e.handle) {
playSentenceAudio(e.sentence, audioRef, articleData.article)
playSentenceAudio(e.sentence, audioRef)
}
}

View File

@@ -0,0 +1,34 @@
<script setup lang="ts">
import {Article} from "@/types/types.ts";
import {watch, ref} from "vue";
import {LOCAL_FILE_KEY} from "@/utils/const.ts";
import {get} from "idb-keyval";
const props = defineProps<{
article: Article
}>()
let file = $ref(null)
let el = ref()
watch(() => props.article.audioFileId, async () => {
if (!props.article.audioSrc && props.article.audioFileId) {
let list = await get(LOCAL_FILE_KEY)
let rItem = list.find((file) => file.id === props.article.audioFileId)
if (rItem) {
file = URL.createObjectURL(rItem.file)
}
}
}, {immediate: true})
defineExpose({el})
</script>
<template>
<audio v-bind="$attrs" ref="el" v-if="props.article.audioSrc" :src="props.article.audioSrc" controls></audio>
<audio ref="el" v-else-if="file" :src="file" controls></audio>
</template>
<style scoped lang="scss">
</style>

View File

@@ -17,6 +17,10 @@ import {Option, Select} from "@/pages/pc/components/base/select";
import Tooltip from "@/pages/pc/components/base/Tooltip.vue";
import InputNumber from "@/pages/pc/components/base/InputNumber.vue";
import {nanoid} from "nanoid";
import {update} from "idb-keyval";
import {LOCAL_FILE_KEY} from "@/utils/const.ts";
import Audio from "@/pages/pc/article/components/Audio.vue";
import BaseInput from "@/pages/pc/components/base/BaseInput.vue";
const Dialog = defineAsyncComponent(() => import('@/pages/pc/components/dialog/Dialog.vue'))
@@ -50,7 +54,7 @@ watch(() => props.article, val => {
editArticle = cloneDeep(val)
progress = 0
failCount = 0
// apply(false)
apply(false)
}, {immediate: true})
watch(() => editArticle.text, (s) => {
@@ -144,9 +148,12 @@ function save(option: 'save' | 'saveAndNext') {
return resolve(false)
}
console.log(editArticle)
let d = cloneDeep(editArticle)
if (!d.id) d.id = nanoid(6)
delete d.sections
// copy(console.json(d, 2))
copy(JSON.stringify(d, null, 2))
const saveTemp = () => {
emit(option as any, editArticle)
@@ -161,20 +168,24 @@ function save(option: 'save' | 'saveAndNext') {
defineExpose({save, getEditArticle: () => cloneDeep(editArticle)})
// 处理音频文件上传
function handleAudioChange(e: any) {
// 获取上传的文件
async function handleAudioChange(e: any) {
let uploadFile = e.target?.files?.[0]
if (!uploadFile) return
// 创建一个临时的URL以访问文件
const audioURL = URL.createObjectURL(uploadFile)
// 设置音频源
editArticle.audioSrc = audioURL
let data = {
id: nanoid(6),
file: uploadFile,
}
//把文件存到indexDB
await update(LOCAL_FILE_KEY, (val) => {
if (val) val.push(data)
else val = [data]
return val
})
//保存id后续从indexDb里读文件来使用
editArticle.audioFileId = data.id
editArticle.audioSrc = ''
// 重置input确保即使选择同一个文件也能触发change事件
e.target.value = ''
Toast.success('音频添加成功')
}
@@ -183,7 +194,7 @@ function handleChange(e: any) {
// 获取上传的文件
let uploadFile = e.target?.files?.[0]
if (!uploadFile) return
// 读取文件内容
let reader = new FileReader();
reader.readAsText(uploadFile, 'UTF-8');
@@ -211,12 +222,12 @@ function handleChange(e: any) {
return w.audioPosition ?? []
})
}).flat()
Toast.success('LRC文件解析成功')
}
}
}
// 重置input确保即使选择同一个文件也能触发change事件
e.target.value = ''
}
@@ -225,15 +236,15 @@ let currentSentence = $ref<Sentence>({} as any)
let editSentence = $ref<Sentence>({} as any)
let preSentence = $ref<Sentence>({} as any)
let showEditAudioDialog = $ref(false)
let sentenceAudioRef = $ref<HTMLAudioElement>()
let audioRef = $ref<HTMLAudioElement>()
let sentenceAudioRef = $ref<{ el: HTMLAudioElement }>({el: null})
let audioRef = $ref<{ el: HTMLAudioElement }>({el: null})
function handleShowEditAudioDialog(val: Sentence, i: number, j: number) {
showEditAudioDialog = true
currentSentence = val
editSentence = cloneDeep(val)
preSentence = null
audioRef.pause()
audioRef.el.pause()
if (j == 0) {
if (i != 0) {
preSentence = last(editArticle.sections[i - 1])
@@ -248,22 +259,25 @@ function handleShowEditAudioDialog(val: Sentence, i: number, j: number) {
}
}
_nextTick(() => {
sentenceAudioRef.currentTime = editSentence.audioPosition[0]
sentenceAudioRef.el.currentTime = editSentence.audioPosition[0]
})
}
function recordStart() {
if (sentenceAudioRef.paused) {
sentenceAudioRef.play()
if (sentenceAudioRef.el.paused) {
sentenceAudioRef.el.play()
}
editSentence.audioPosition[0] = Number(sentenceAudioRef.el.currentTime.toFixed(2))
if (editSentence.audioPosition[0] > editSentence.audioPosition[1]) {
editSentence.audioPosition[1] = editSentence.audioPosition[0]
}
editSentence.audioPosition[0] = Number(sentenceAudioRef.currentTime.toFixed(2))
}
function recordEnd() {
if (!sentenceAudioRef.paused) {
sentenceAudioRef.pause()
if (!sentenceAudioRef.el.paused) {
sentenceAudioRef.el.pause()
}
editSentence.audioPosition[1] = Number(sentenceAudioRef.currentTime.toFixed(2))
editSentence.audioPosition[1] = Number(sentenceAudioRef.el.currentTime.toFixed(2))
}
const {playSentenceAudio} = usePlaySentenceAudio()
@@ -275,7 +289,7 @@ function saveLrcPosition() {
}
function jumpAudio(time: number) {
sentenceAudioRef.currentTime = time
sentenceAudioRef.el.currentTime = time
}
function setPreEndTimeToCurrentStartTime() {
@@ -296,8 +310,15 @@ function setStartTime(val: Sentence, i: number, j: number) {
if (preSentence) {
val.audioPosition[0] = preSentence.audioPosition[1]
} else {
val.audioPosition[0] = Number(Number(audioRef.currentTime).toFixed(2))
val.audioPosition[0] = Number(Number(audioRef.el.currentTime).toFixed(2))
}
if (val.audioPosition[0] > val.audioPosition[1]) {
val.audioPosition[1] = val.audioPosition[0]
}
}
function uploadFileTrigger(id: string) {
document.querySelector('#' + id).click()
}
</script>
@@ -306,14 +327,15 @@ function setStartTime(val: Sentence, i: number, j: number) {
<div class="content">
<div class="row flex flex-col gap-2">
<div class="title">原文</div>
<div class="">标题</div>
<input
v-model="editArticle.title"
type="text"
class="base-input"
placeholder="请填写原文标题"
/>
<div class="">正文</div>
<div class="flex gap-2 items-center">
<div class="shrink-0">标题</div>
<BaseInput
v-model="editArticle.title"
type="text"
placeholder="请填写原文标题"
/>
</div>
<div class="">正文<span class="text-sm color-gray">一行一句段落间空一行</span></div>
<textarea
v-model="editArticle.text"
:readonly="![100,0].includes(progress)"
@@ -346,19 +368,15 @@ function setStartTime(val: Sentence, i: number, j: number) {
</div>
<div class="row flex flex-col gap-2">
<div class="title">译文</div>
<div class="flex gap-2">
标题
</div>
<input
v-model="editArticle.titleTranslate"
type="text"
class="base-input"
placeholder="请填写翻译标题"
/>
<div class="flex">
<span>正文</span>
<div class="flex gap-2 items-center">
<div class="shrink-0">标题</div>
<BaseInput
v-model="editArticle.titleTranslate"
type="text"
placeholder="请填写翻译标题"
/>
</div>
<div class="">正文<span class="text-sm color-gray">一行一句段落间空一行</span></div>
<textarea
v-model="editArticle.textTranslate"
:readonly="![100,0].includes(progress)"
@@ -409,35 +427,49 @@ function setStartTime(val: Sentence, i: number, j: number) {
</div>
</div>
<div class="row flex flex-col gap-2">
<div class="title">结果</div>
<div class="center">正文译文与结果均可编辑编辑后点击应用按钮会自动同步</div>
<div class="flex gap-2">
<div class="upload relative">
<BaseButton>添加音频</BaseButton>
<input type="file"
accept="audio/*"
@change="handleAudioChange"
class="w-full h-full absolute left-0 top-0 opacity-0"/>
<div class="title">结果</div>
<div class="flex gap-2">
<div class="upload relative">
<BaseButton>添加音频</BaseButton>
<input type="file"
accept="audio/*"
@change="handleAudioChange"
class="w-full h-full absolute left-0 top-0 opacity-0"/>
</div>
<div class="upload relative">
<BaseButton>添加音频LRC文件</BaseButton>
<input type="file"
accept=".lrc"
@change="handleChange"
class="w-full h-full absolute left-0 top-0 opacity-0"/>
</div>
<Audio ref="audioRef" :article="editArticle"/>
</div>
<div class="upload relative">
<BaseButton>添加音频LRC文件</BaseButton>
<input type="file"
accept=".lrc"
@change="handleChange"
class="w-full h-full absolute left-0 top-0 opacity-0"/>
</div>
<audio ref="audioRef" :src="editArticle.audioSrc" controls></audio>
</div>
<template v-if="editArticle?.sections?.length">
<div class="flex-1 overflow-auto flex flex-col">
<div class="flex justify-between bg-black/10 py-2">
<div class="center flex-[7]">内容</div>
<div class="flex justify-between bg-black/10 py-2 rounded-lt-md rounded-rt-md">
<div class="center flex-[7]">内容
<span class="text-sm color-black/70">均可编辑编辑后点击应用按钮会自动同步</span></div>
<div>|</div>
<div class="center flex-[3]">音频</div>
<div class="center flex-[3] gap-2">
<span>音频</span>
<BaseIcon title="添加音频"
@click="uploadFileTrigger('updateFile1')"
>
<IconIconParkOutlineAddMusic/>
</BaseIcon>
<input type="file"
id="updateFile1"
accept="audio/*"
@change="handleAudioChange"
class="w-0 h-0 absolute left-0 top-0 opacity-0"/>
</div>
</div>
<div class="article-translate">
<div class="section " v-for="(item,indexI) in editArticle.sections">
<div class="section-title">{{ indexI + 1 }}</div>
<div class="section rounded-md " v-for="(item,indexI) in editArticle.sections">
<div class="section-title text-lg font-bold">{{ indexI + 1 }}</div>
<div class="sentence" v-for="(sentence,indexJ) in item">
<div class="flex-[7]">
<EditAbleText
@@ -468,7 +500,7 @@ function setStartTime(val: Sentence, i: number, j: number) {
<div v-if="sentence.audioPosition?.[1] !== -1">{{ sentence.audioPosition?.[1] ?? 0 }}s</div>
<div v-else> 结束</div>
<BaseIcon
@click="sentence.audioPosition[1] = Number(Number(audioRef.currentTime).toFixed(2))"
@click="sentence.audioPosition[1] = Number(Number(audioRef.el.currentTime).toFixed(2))"
title="设置结束时间"
>
<IconFluentMyLocation20Regular/>
@@ -479,13 +511,14 @@ function setStartTime(val: Sentence, i: number, j: number) {
<BaseIcon :icon="sentence.audioPosition?.length ? 'basil:edit-outline' : 'basil:add-outline'"
title="编辑"
@click="handleShowEditAudioDialog(sentence,indexI,indexJ)">
<IconFluentSpeakerEdit20Regular v-if="sentence.audioPosition?.length && sentence.audioPosition[1]"/>
<IconFluentSpeakerEdit20Regular
v-if="sentence.audioPosition?.length && sentence.audioPosition[1]"/>
<IconFluentAddSquare20Regular v-else/>
</BaseIcon>
<BaseIcon
title="播放"
v-if="sentence.audioPosition?.length"
@click="playSentenceAudio(sentence,audioRef,editArticle)">
@click="playSentenceAudio(sentence,audioRef.el)">
<IconFluentPlay20Regular/>
</BaseIcon>
</div>
@@ -506,7 +539,7 @@ function setStartTime(val: Sentence, i: number, j: number) {
翻译完成
</div>
</div>
<div class="left">
<div>
<BaseButton @click="save('save')">保存</BaseButton>
<BaseButton v-if="type === 'batch'" @click="save('saveAndNext')">保存并添加下一篇</BaseButton>
</div>
@@ -525,7 +558,7 @@ function setStartTime(val: Sentence, i: number, j: number) {
教程点击音频播放按钮当播放到句子开始时点击开始时间的 <span class="color-red">记录</span>
按钮当播放到句子结束时点击结束时间的 <span class="color-red">记录</span> 按钮最后再试听是否正确
</div>
<audio ref="sentenceAudioRef" :src="editArticle.audioSrc" controls class="w-full"></audio>
<Audio ref="sentenceAudioRef" :article="editArticle" 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">
@@ -536,7 +569,7 @@ function setStartTime(val: Sentence, i: number, j: number) {
</div>
<BaseIcon
title="播放"
@click="playSentenceAudio(editSentence,sentenceAudioRef,editArticle)">
@click="playSentenceAudio(editSentence,sentenceAudioRef.el)">
<IconFluentPlay20Regular/>
</BaseIcon>
</div>
@@ -549,13 +582,14 @@ function setStartTime(val: Sentence, i: number, j: number) {
<InputNumber v-model="editSentence.audioPosition[0]" :precision="2" :step="0.1"/>
<BaseIcon
@click="jumpAudio(editSentence.audioPosition[0])"
title="跳转"
:title='`跳转至${editSentence.audioPosition[0]}秒`'
>
<IconFluentMyLocation20Regular/>
</BaseIcon>
<BaseIcon
v-if="preSentence"
@click="setPreEndTimeToCurrentStartTime"
title="使用前一句的结束时间"
:title="`使用前一句的结束时间${preSentence?.audioPosition?.[1]||0}秒`"
>
<IconFluentPaddingLeft20Regular/>
</BaseIcon>
@@ -588,8 +622,8 @@ function setStartTime(val: Sentence, i: number, j: number) {
box-sizing: border-box;
display: flex;
gap: var(--space);
padding: var(--space);
padding-top: .6rem;
padding: 0.6rem;
padding-left: 0;
}
.row {
@@ -607,7 +641,6 @@ function setStartTime(val: Sentence, i: number, j: number) {
.title {
font-weight: bold;
font-size: 1.4rem;
text-align: center;
}
.article-translate {
@@ -629,7 +662,7 @@ function setStartTime(val: Sentence, i: number, j: number) {
.sentence {
display: flex;
padding: 0.5rem 1.5rem;
padding: 0.5rem;
line-height: 1.2;
border-bottom: 1px solid var(--color-item-border);
@@ -663,11 +696,6 @@ function setStartTime(val: Sentence, i: number, j: number) {
font-size: 1.2rem;
color: #67C23A;
}
.left {
gap: var(--space);
display: flex;
}
}
}
</style>

View File

@@ -196,7 +196,7 @@ function nextSentence() {
if (!currentSection[sentenceIndex]) {
sentenceIndex = 0
sectionIndex++
if (!props.article.sections[sectionIndex]) {
console.log('打完了')
isEnd = true
@@ -207,14 +207,14 @@ function nextSentence() {
} else {
emit('play', {sentence: currentSection[sentenceIndex], handle: false})
}
// 如果有新的单词,更新当前单词信息
if (!isEnd && props.article.sections[sectionIndex] &&
props.article.sections[sectionIndex][sentenceIndex] &&
if (!isEnd && props.article.sections[sectionIndex] &&
props.article.sections[sectionIndex][sentenceIndex] &&
props.article.sections[sectionIndex][sentenceIndex].words[wordIndex]) {
updateCurrentWordInfo(props.article.sections[sectionIndex][sentenceIndex].words[wordIndex]);
}
lockNextSentence = false
}
@@ -235,7 +235,7 @@ function onTyping(e: KeyboardEvent) {
let currentSection = props.article.sections[sectionIndex]
let currentSentence = currentSection[sentenceIndex]
let currentWord: ArticleWord = currentSentence.words[wordIndex]
// 更新当前单词信息
updateCurrentWordInfo(currentWord);
@@ -258,7 +258,7 @@ function onTyping(e: KeyboardEvent) {
if (e.code === 'Space') {
// 检查下一个单词是否存在
const hasNextWord = wordIndex + 1 < currentSentence.words.length;
// 当按下空格键时,移动到下一个单词,而不是下跳过句子,末尾跳转到下一个
if (hasNextWord) {
// 重置isSpace状态
@@ -266,19 +266,19 @@ function onTyping(e: KeyboardEvent) {
stringIndex = 0;
wordIndex++;
input = '';
emit('nextWord', currentWord);
// 获取下一个单词
currentWord = currentSentence.words[wordIndex];
if (currentWord && currentWord.word && currentWord.word[0] === ' ') {
input = ' ';
if (!currentWord.input) currentWord.input = '';
currentWord.input = input;
stringIndex = 1;
}
// 更新当前单词信息
updateCurrentWordInfo(currentWord);
} else {
@@ -300,7 +300,7 @@ function onTyping(e: KeyboardEvent) {
emit('play', {sentence: currentSection[sentenceIndex], handle: false})
}
let letter = e.key
// 如果是空格键,需要判断是作为输入还是切换单词
if (letter === ' ' || e.code === 'Space') {
// 如果当前单词包含空格,且当前输入位置应该是空格,则视为正常输入
@@ -341,12 +341,12 @@ function onTyping(e: KeyboardEvent) {
if (!currentWord.isSymbol) {
playCorrect()
}
// 检查是否是句子的最后一个单词
const isLastWordInSentence = wordIndex + 1 >= currentSentence.words.length;
if (isLastWordInSentence) {
// 如果是句子的最后一个单词,自动跳转到下一句
// 如果是句子的最后一个单词,自动跳转到下一句,不用再输入空格
nextSentence();
} else if (currentWord.nextSpace) {
// 如果不是最后一个单词,且需要空格,设置等待空格输入
@@ -356,10 +356,10 @@ function onTyping(e: KeyboardEvent) {
nextWord();
}
}
// 更新当前单词信息
updateCurrentWordInfo(currentWord);
playKeyboardAudio()
}
e.preventDefault()
@@ -528,19 +528,19 @@ function onContextMenu(e: MouseEvent, sentence: Sentence, i, j, w) {
onMounted(() => {
// 初始化当前单词信息
if (props.article.sections &&
props.article.sections[sectionIndex] &&
props.article.sections[sectionIndex][sentenceIndex] &&
if (props.article.sections &&
props.article.sections[sectionIndex] &&
props.article.sections[sectionIndex][sentenceIndex] &&
props.article.sections[sectionIndex][sentenceIndex].words[wordIndex]) {
updateCurrentWordInfo(props.article.sections[sectionIndex][sentenceIndex].words[wordIndex]);
}
emitter.on(EventKey.resetWord, () => {
wrong = input = ''
// 重置时更新当前单词信息
if (props.article.sections &&
props.article.sections[sectionIndex] &&
props.article.sections[sectionIndex][sentenceIndex] &&
if (props.article.sections &&
props.article.sections[sectionIndex] &&
props.article.sections[sectionIndex][sentenceIndex] &&
props.article.sections[sectionIndex][sentenceIndex].words[wordIndex]) {
updateCurrentWordInfo(props.article.sections[sectionIndex][sentenceIndex].words[wordIndex]);
}

View File

@@ -31,7 +31,10 @@ const list = $computed(() => {
}
}
props.word.input.split('').forEach((k, i) => {
if (k === ' ') t.push({type: 'space'})
if (k === ' ') {
right = wrong = ''
t.push({type: 'space'})
}
else {
if (compare(k, props.word.word[i])) {
right += k

View File

@@ -40,7 +40,7 @@
</template>
<script setup lang="ts">
import {ref, computed, onBeforeUnmount} from 'vue'
import {ref, computed, onBeforeUnmount, watch} from 'vue'
const props = defineProps({
modelValue: {type: [Number, String], default: null},
@@ -59,6 +59,9 @@ const inner = ref<number | null>(normalizeToNumber(props.modelValue))
let holdTimer: number | null = null
let holdInterval: number | null = null
watch(() => props.modelValue, (value: number) => {
inner.value = value
})
const displayValue = computed({
get: () => inner.value === null ? '' : format(inner.value),
set: v => {

View File

@@ -34,15 +34,15 @@ export const routes: RouteRecordRaw[] = [
{path: 'book-detail', component: BookDetail},
{path: 'book-list', component: BookList},
{path: 'edit-article', component: () => import("@/pages/pc/article/EditArticlePage.vue")},
{path: 'batch-edit-article', component: () => import("@/pages/pc/article/BatchEditArticlePage.vue")},
{path: 'setting', component: Setting},
]
},
{path: '/batch-edit-article', component: () => import("@/pages/pc/article/BatchEditArticlePage.vue")},
{path: '/test', component: () => import("@/pages/test/test.vue")},
{path: '/:pathMatch(.*)*', redirect: '/word'},
]
console.log(import.meta.env.VITE_ROUTE_BASE)
const router = VueRouter.createRouter({
history: VueRouter.createWebHistory(import.meta.env.VITE_ROUTE_BASE),
// history: VueRouter.createWebHashHistory(),
@@ -58,6 +58,8 @@ const router = VueRouter.createRouter({
})
router.beforeEach((to: any, from: any) => {
return true
// console.log('beforeEach-to',to.path)
// console.log('beforeEach-from',from.path)
const runtimeStore = useRuntimeStore()
@@ -69,6 +71,7 @@ router.beforeEach((to: any, from: any) => {
'/mobile',
'/'
]
if (noAnimation.indexOf(from.path) !== -1 && noAnimation.indexOf(to.path) !== -1) {
return true
}

View File

@@ -44,6 +44,7 @@ export function getDefaultArticle(val: Partial<Article> = {}): Article {
textAllWords: [],
sections: [],
audioSrc: '',
audioFileId: '',
lrcPosition: [],
questions: [],
...cloneDeep(val)

View File

@@ -2,7 +2,7 @@ declare global {
interface Console {
parse(v: any): void
json(v: any, space: number): void
json(v: any, space: number): string
}
interface Window {

View File

@@ -55,7 +55,7 @@ export interface ArticleWord extends Word {
nextSpace: boolean,
isSymbol: boolean,
symbolPosition: 'start' | 'end' | '',
input:string
input: string
}
export interface Sentence {
@@ -75,6 +75,7 @@ export interface Article {
textAllWords: string[],
sections: Sentence[][],
audioSrc: string,
audioFileId: string,
lrcPosition: number[][],
questions: {
stem: string,

View File

@@ -1,22 +1,25 @@
export const SoundFileOptions = [
{value: '机械键盘', label: '机械键盘'},
{value: '机械键盘1', label: '机械键盘1'},
{value: '机械键盘2', label: '机械键盘2'},
{value: '老式机械键盘', label: '老式机械键盘'},
{value: '笔记本键盘', label: '笔记本键盘'},
{value: '机械键盘', label: '机械键盘'},
{value: '机械键盘1', label: '机械键盘1'},
{value: '机械键盘2', label: '机械键盘2'},
{value: '老式机械键盘', label: '老式机械键盘'},
{value: '笔记本键盘', label: '笔记本键盘'},
]
export const APP_NAME = 'Type Words'
export const SAVE_DICT_KEY = {
key: 'typing-word-dict',
version: 4
key: 'typing-word-dict',
version: 4
}
export const SAVE_SETTING_KEY = {
key: 'typing-word-setting',
version: 14
key: 'typing-word-setting',
version: 14
}
export const EXPORT_DATA_KEY = {
key: 'typing-word-export',
version: 1
key: 'typing-word-export',
version: 1
}
export const LOCAL_FILE_KEY = 'typing-word-files'

View File

@@ -240,6 +240,12 @@ export function shakeCommonDict(n: BaseState): BaseState {
})
data.article.bookList.map((v: Dict) => {
if (!v.custom && ![DictId.articleCollect].includes(v.id)) v.articles = []
else {
v.articles.map(a => {
//运行时再生成
a.sections = []
})
}
})
return data
}