save
This commit is contained in:
2
components.d.ts
vendored
2
components.d.ts
vendored
@@ -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']
|
||||
|
||||
@@ -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
9478
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -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 |
49
src/App.vue
49
src/App.vue
@@ -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">
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
34
src/pages/pc/article/components/Audio.vue
Normal file
34
src/pages/pc/article/components/Audio.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ export function getDefaultArticle(val: Partial<Article> = {}): Article {
|
||||
textAllWords: [],
|
||||
sections: [],
|
||||
audioSrc: '',
|
||||
audioFileId: '',
|
||||
lrcPosition: [],
|
||||
questions: [],
|
||||
...cloneDeep(val)
|
||||
|
||||
2
src/types/global.d.ts
vendored
2
src/types/global.d.ts
vendored
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user