wip
This commit is contained in:
2
components.d.ts
vendored
2
components.d.ts
vendored
@@ -44,6 +44,7 @@ declare module 'vue' {
|
||||
IconFluentArrowClockwise20Regular: typeof import('~icons/fluent/arrow-clockwise20-regular')['default']
|
||||
IconFluentArrowDownload20Regular: typeof import('~icons/fluent/arrow-download20-regular')['default']
|
||||
IconFluentArrowLeft16Regular: typeof import('~icons/fluent/arrow-left16-regular')['default']
|
||||
IconFluentArrowMove20Regular: typeof import('~icons/fluent/arrow-move20-regular')['default']
|
||||
IconFluentArrowRepeatAll20Regular: typeof import('~icons/fluent/arrow-repeat-all20-regular')['default']
|
||||
IconFluentArrowRight16Regular: typeof import('~icons/fluent/arrow-right16-regular')['default']
|
||||
IconFluentArrowShuffle16Regular: typeof import('~icons/fluent/arrow-shuffle16-regular')['default']
|
||||
@@ -54,7 +55,6 @@ declare module 'vue' {
|
||||
IconFluentBookLetter20Regular: typeof import('~icons/fluent/book-letter20-regular')['default']
|
||||
IconFluentBookNumber20Filled: typeof import('~icons/fluent/book-number20-filled')['default']
|
||||
IconFluentCalendarDate20Regular: typeof import('~icons/fluent/calendar-date20-regular')['default']
|
||||
IconFluentCheckmark20Regular: typeof import('~icons/fluent/checkmark20-regular')['default']
|
||||
IconFluentCheckmarkCircle16Filled: typeof import('~icons/fluent/checkmark-circle16-filled')['default']
|
||||
IconFluentCheckmarkCircle16Regular: typeof import('~icons/fluent/checkmark-circle16-regular')['default']
|
||||
IconFluentCheckmarkCircle20Filled: typeof import('~icons/fluent/checkmark-circle20-filled')['default']
|
||||
|
||||
@@ -31,15 +31,15 @@
|
||||
"id": "3uh9Iy",
|
||||
"title": "Nice to meet you",
|
||||
"titleTranslate": "很高兴见到你。",
|
||||
"text": "MR. BLAKE:Good morning. \n\nSTUDENTS:Good morning, Mr. Blake. \n\nMR. BLAKE:This is Miss Sophie Dupont. \n\nSophie is a new student. \n\nShe is French. \n\nMR. BLAKE:Sophie, this is Hans. \n\nHe is German. \n\nHANS:Nice to meet you. \n\nMR. BLAKE:And this is Naoko. \n\nShe's Japanese. \n\nNAOKO:Nice to meet you. \n\nMR. BLAKE:And this is Chang-woo. \n\nHe's Korean. \n\nCHANG-WOO:Nice to meet you. \n\nMR. BLAKE:And this is Luming. \n\nHe is Chinese. \n\nLUMNG:Nice to meet you. \n\nMR. BLAKE:And this is Xiaohui. \n\nShe's Chinese, too. \n\nXIAOHUI:Nice to meet you.",
|
||||
"text": "MR. BLAKE:Good morning.\n\nSTUDENTS:Good morning, Mr. Blake.\n\nMR. BLAKE:This is Miss Sophie Dupont.\n\nSophie is a new student.\n\nShe is French.\n\nMR. BLAKE:Sophie, this is Hans.\n\nHe is German.\n\nHANS:Nice to meet you.\n\nMR. BLAKE:And this is Naoko.\n\nShe's Japanese.\n\nNAOKO:Nice to meet you.\n\nMR. BLAKE:And this is Chang-woo.\n\nHe's Korean.\n\nCHANG-WOO:Nice to meet you.\n\nMR. BLAKE:And this is Luming.\n\nHe is Chinese.\n\nLUMNG:Nice to meet you.\n\nMR. BLAKE:And this is Xiaohui.\n\nShe's Chinese, too.\n\nXIAOHUI:Nice to meet you.",
|
||||
"textTranslate": "布莱克先生:早上好。 \n\n学 生:早上好,布莱克先生。 \n\n布莱克先生:这位是索菲娅.杜邦小姐。 \n\n索菲娅是个新学生。 \n\n她是法国人。 \n\n布莱克先生:索菲娅,这位是汉斯。 \n\n他是德国人。 \n\n汉 斯:很高兴见到你。 \n\n布莱克先生:这位是直子。 \n\n她是日本人。 \n\n直 子:很高兴见到你。 \n\n布莱克先生:这位是昌宇。 \n\n他是韩国人。 \n\n昌 宇:很高兴见到你。 \n\n布莱克先生:这位是鲁明。 \n\n他是中国人。 \n\n鲁 明:很高兴见到你。 \n\n布莱克先生:这位是晓惠。 \n\n她也是中国人。 \n\n晓 惠:很高兴见到你。",
|
||||
"newWords": [],
|
||||
"textAllWords": [],
|
||||
"sections": [],
|
||||
"audioSrc": "",
|
||||
"audioFileId": "",
|
||||
"lrcPosition": [],
|
||||
"questions": []
|
||||
"questions": [],
|
||||
"nameList": ["MR. BLAKE", "STUDENTS", "Sophie Dupont", "Sophie", "Hans", "Naoko", "Chang-woo", "Luming", "Xiaohui"],
|
||||
"textAllWords": []
|
||||
},
|
||||
{
|
||||
"id": "13nyyY",
|
||||
|
||||
@@ -157,9 +157,7 @@ const sentence = $computed(() => {
|
||||
</div>
|
||||
|
||||
<!-- 学习总结分享图片生成对话框 -->
|
||||
<Dialog
|
||||
v-model="showShareDialog"
|
||||
title="分享" :close-on-click-bg="true" custom-class="!max-w-4xl !w-auto">
|
||||
<Dialog v-model="showShareDialog" title="分享">
|
||||
<div class="flex min-w-160 max-w-200 p-6 pt-0 gap-space">
|
||||
<!-- 左侧:海报预览区域 -->
|
||||
<div ref="posterEl" class="flex-1 border-r border-gray-200 bg-gray-100 rounded-xl overflow-hidden relative">
|
||||
|
||||
@@ -1,27 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {Article, Sentence, TranslateEngine} from "@/types/types.ts";
|
||||
import { Article, Sentence, TranslateEngine } from "@/types/types.ts";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import EditAbleText from "@/components/EditAbleText.vue";
|
||||
import {getNetworkTranslate, getSentenceAllText, getSentenceAllTranslateText} from "@/hooks/translate.ts";
|
||||
import {genArticleSectionData, splitCNArticle2, splitEnArticle2, usePlaySentenceAudio} from "@/hooks/article.ts";
|
||||
import {_nextTick, _parseLRC, cloneDeep, last} from "@/utils";
|
||||
import {defineAsyncComponent, watch} from "vue";
|
||||
import { getNetworkTranslate, getSentenceAllText, getSentenceAllTranslateText } from "@/hooks/translate.ts";
|
||||
import { genArticleSectionData, splitCNArticle2, splitEnArticle2, usePlaySentenceAudio } from "@/hooks/article.ts";
|
||||
import { _nextTick, _parseLRC, cloneDeep, last } from "@/utils";
|
||||
import { defineAsyncComponent, watch } from "vue";
|
||||
import Empty from "@/components/Empty.vue";
|
||||
import Toast from '@/components/base/toast/Toast.ts'
|
||||
import * as Comparison from "string-comparison"
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
import {getDefaultArticle} from "@/types/func.ts";
|
||||
import { getDefaultArticle } from "@/types/func.ts";
|
||||
import copy from "copy-to-clipboard";
|
||||
import {Option, Select} from "@/components/base/select";
|
||||
import { Option, Select } from "@/components/base/select";
|
||||
import Tooltip from "@/components/base/Tooltip.vue";
|
||||
import InputNumber from "@/components/base/InputNumber.vue";
|
||||
import {nanoid} from "nanoid";
|
||||
import {update} from "idb-keyval";
|
||||
import { nanoid } from "nanoid";
|
||||
import { update } from "idb-keyval";
|
||||
import ArticleAudio from "@/pages/article/components/ArticleAudio.vue";
|
||||
import BaseInput from "@/components/base/BaseInput.vue";
|
||||
import Textarea from "@/components/base/Textarea.vue";
|
||||
import { LOCAL_FILE_KEY } from "@/config/env.ts";
|
||||
import PopConfirm from "@/components/PopConfirm.vue";
|
||||
|
||||
const Dialog = defineAsyncComponent(() => import('@/components/dialog/Dialog.vue'))
|
||||
|
||||
@@ -52,7 +53,7 @@ const TranslateEngineOptions = [
|
||||
let editArticle = $ref<Article>(getDefaultArticle())
|
||||
|
||||
watch(() => props.article, val => {
|
||||
editArticle = cloneDeep(val)
|
||||
editArticle = getDefaultArticle(val)
|
||||
progress = 0
|
||||
failCount = 0
|
||||
apply(false)
|
||||
@@ -230,9 +231,31 @@ let editSentence = $ref<Sentence>({} as any)
|
||||
let preSentence = $ref<Sentence>({} as any)
|
||||
let showEditAudioDialog = $ref(false)
|
||||
let showAudioDialog = $ref(false)
|
||||
let showNameDialog = $ref(false)
|
||||
let sentenceAudioRef = $ref<HTMLAudioElement>()
|
||||
let audioRef = $ref<HTMLAudioElement>()
|
||||
|
||||
let nameListRef = $ref<string[]>([])
|
||||
watch(() => showNameDialog, (v) => {
|
||||
if (v) {
|
||||
nameListRef = cloneDeep(Array.isArray(editArticle.nameList) ? editArticle.nameList : [])
|
||||
nameListRef.push('')
|
||||
}
|
||||
})
|
||||
|
||||
function addName() {
|
||||
nameListRef.push('')
|
||||
}
|
||||
|
||||
function removeName(i: number) {
|
||||
nameListRef.splice(i, 1)
|
||||
}
|
||||
|
||||
function saveNameList() {
|
||||
const cleaned = Array.from(new Set(nameListRef.map(s => (s ?? '').trim()).filter(Boolean)))
|
||||
editArticle.nameList = cleaned as any
|
||||
}
|
||||
|
||||
function handleShowEditAudioDialog(val: Sentence, i: number, j: number) {
|
||||
showEditAudioDialog = true
|
||||
currentSentence = val
|
||||
@@ -326,7 +349,15 @@ function setStartTime(val: Sentence, i: number, j: number) {
|
||||
placeholder="请填写原文标题"
|
||||
/>
|
||||
</div>
|
||||
<div class="">正文:<span class="text-sm color-gray">一行一句,段落间空一行</span></div>
|
||||
<div class="flex justify-between">
|
||||
<span>正文:<span class="text-sm color-gray">一行一句,段落间空一行</span></span>
|
||||
<Tooltip title="配置人名之后,在练习时自动忽略(可选,默认开启)">
|
||||
<div @click="showNameDialog = true" class="center gap-1 cp">
|
||||
<span>人名配置</span>
|
||||
<IconFluentSettings20Regular/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Textarea v-model="editArticle.text"
|
||||
class="h-full"
|
||||
:disabled="![100,0].includes(progress)"
|
||||
@@ -609,6 +640,31 @@ function setStartTime(val: Sentence, i: number, j: number) {
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
<Dialog title="人名管理"
|
||||
v-model="showNameDialog"
|
||||
:footer="true"
|
||||
@close="showNameDialog = false"
|
||||
@ok="saveNameList"
|
||||
>
|
||||
<div class="p-4 pt-0 color-main w-150 flex flex-col gap-3">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="text-base">配置需要忽略的人名,练习时自动忽略这些名称(可选,默认开启)</div>
|
||||
<BaseButton type="info" @click="addName">添加名称</BaseButton>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center gap-2" v-for="(name,i) in nameListRef" :key="i">
|
||||
<BaseInput v-model="nameListRef[i]"
|
||||
placeholder="输入名称"
|
||||
size="large"
|
||||
:autofocus="i===nameListRef.length-1"/>
|
||||
<BaseButton type="info" @click="removeName(i)">删除</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -703,65 +759,65 @@ function setStartTime(val: Sentence, i: number, j: number) {
|
||||
flex-direction: column;
|
||||
padding: 0.5rem;
|
||||
gap: 1rem;
|
||||
|
||||
|
||||
.row {
|
||||
width: 100%;
|
||||
flex: none;
|
||||
|
||||
|
||||
&:nth-child(3) {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
|
||||
.title {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
|
||||
// 表单元素优化
|
||||
.base-input, .base-textarea {
|
||||
width: 100%;
|
||||
font-size: 16px; // 防止iOS自动缩放
|
||||
}
|
||||
|
||||
|
||||
.base-textarea {
|
||||
min-height: 150px;
|
||||
max-height: 30vh;
|
||||
}
|
||||
|
||||
|
||||
// 按钮组优化
|
||||
.flex.gap-2 {
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
|
||||
|
||||
.base-button {
|
||||
min-height: 44px;
|
||||
flex: 1;
|
||||
min-width: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 文章翻译区域优化
|
||||
.article-translate {
|
||||
.section {
|
||||
margin-bottom: 1rem;
|
||||
|
||||
|
||||
.section-title {
|
||||
font-size: 1rem;
|
||||
padding: 0.4rem;
|
||||
}
|
||||
|
||||
|
||||
.sentence {
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
padding: 0.4rem;
|
||||
|
||||
|
||||
.flex-\[7\] {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
.flex-\[2\] {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
|
||||
|
||||
.flex.justify-end.gap-2 {
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
@@ -771,17 +827,17 @@ function setStartTime(val: Sentence, i: number, j: number) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 选项区域优化
|
||||
.options {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
|
||||
|
||||
.status {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
|
||||
.warning, .success {
|
||||
font-size: 1rem;
|
||||
}
|
||||
@@ -793,12 +849,12 @@ function setStartTime(val: Sentence, i: number, j: number) {
|
||||
@media (max-width: 480px) {
|
||||
.content {
|
||||
padding: 0.3rem;
|
||||
|
||||
|
||||
.row {
|
||||
.base-textarea {
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
|
||||
.flex.gap-2 {
|
||||
.base-button {
|
||||
min-width: 100px;
|
||||
|
||||
@@ -300,6 +300,12 @@ function onTyping(e: KeyboardEvent) {
|
||||
let currentWord: ArticleWord = currentSentence.words[wordIndex]
|
||||
wrong = ''
|
||||
|
||||
const normalize = (s: string) => (settingStore.ignoreCase ? s.toLowerCase() : s).trim()
|
||||
const nameList = (props.article?.nameList ?? []).map(normalize).filter(Boolean)
|
||||
const isNameWord = (w: ArticleWord) => {
|
||||
return w?.type === PracticeArticleWordType.Word && nameList.length > 0 && nameList.includes(normalize(w.word))
|
||||
}
|
||||
|
||||
const next = () => {
|
||||
isSpace = false;
|
||||
input = wrong = ''
|
||||
@@ -310,7 +316,10 @@ function onTyping(e: KeyboardEvent) {
|
||||
currentWord = currentSentence.words[wordIndex]
|
||||
if ([PracticeArticleWordType.Symbol,PracticeArticleWordType.Number].includes(currentWord.type) && settingStore.ignoreSymbol){
|
||||
next()
|
||||
}else {
|
||||
} else if (isNameWord(currentWord)) {
|
||||
isSpace = false
|
||||
next()
|
||||
} else {
|
||||
emit('nextWord', currentWord);
|
||||
}
|
||||
} else {
|
||||
@@ -338,6 +347,13 @@ function onTyping(e: KeyboardEvent) {
|
||||
if (sectionIndex === 0 && sentenceIndex === 0 && wordIndex === 0 && stringIndex === 0) {
|
||||
emit('play', {sentence: currentSection[sentenceIndex], handle: false})
|
||||
}
|
||||
if (isNameWord(currentWord)) {
|
||||
isSpace = false
|
||||
const savedTyping = isTyping
|
||||
next()
|
||||
isTyping = false
|
||||
return onTyping(e)
|
||||
}
|
||||
let letter = e.key
|
||||
let key = currentWord.word[stringIndex]
|
||||
// console.log('key', key,)
|
||||
|
||||
@@ -138,8 +138,7 @@ calcWeekList(); // 新增:计算本周学习记录
|
||||
:close-on-click-bg="false"
|
||||
:header="false"
|
||||
:keyboard="false"
|
||||
:show-close="false"
|
||||
class="statistics-modal">
|
||||
:show-close="false">
|
||||
<div class="p-8 pr-3 bg-[var(--bg-card-primary)] rounded-2xl space-y-6">
|
||||
<!-- Header Section -->
|
||||
<div class="text-center relative">
|
||||
|
||||
@@ -46,6 +46,7 @@ export function getDefaultArticle(val: Partial<Article> = {}): Article {
|
||||
audioFileId: '',
|
||||
lrcPosition: [],
|
||||
questions: [],
|
||||
nameList:[],
|
||||
...cloneDeep(val)
|
||||
}
|
||||
}
|
||||
|
||||
13
src/types/global.d.ts
vendored
13
src/types/global.d.ts
vendored
@@ -19,18 +19,25 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
console.json = function (v: any, space = 0) {
|
||||
const json = JSON.stringify(
|
||||
v,
|
||||
(key, value) => {
|
||||
if (Array.isArray(value)) {
|
||||
if (Array.isArray(value) && key !== 'nameList') {
|
||||
return `__ARRAY__${JSON.stringify(value)}`;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
space
|
||||
).replace(/"__ARRAY__(\[.*?\])"/g, (_, arr) => arr);
|
||||
)
|
||||
.replace(/"__ARRAY__(\[.*?\])"/g, (_, arr) => arr)
|
||||
// 专门处理 nameList,将其压缩成一行
|
||||
.replace(/"nameList": \[\s*([^\]]+)\s*\]/g, (match, content) => {
|
||||
// 移除数组内部的换行和多余空格,但保留字符串间的空格
|
||||
const compressed = content.replace(/\s*\n\s*/g, ' ').trim();
|
||||
return `"nameList": [${compressed}]`;
|
||||
});
|
||||
|
||||
console.log(json);
|
||||
return json;
|
||||
}
|
||||
|
||||
@@ -76,6 +76,7 @@ export interface Article {
|
||||
audioSrc: string,
|
||||
audioFileId: string,
|
||||
lrcPosition: number[][],
|
||||
nameList: string[],
|
||||
questions: {
|
||||
stem: string,
|
||||
options: string[],
|
||||
|
||||
Reference in New Issue
Block a user