@@ -0,0 +1,71 @@
|
||||
## 目标
|
||||
- 在 `onTyping` 方法(291-382行)中判断当前/下一个单词是否为人名,并在练习时自动忽略(不要求输入、不提示错误、不停顿等待空格)。
|
||||
|
||||
## 数据来源
|
||||
- 使用 `props.article.nameList: string[]`(来自编辑页保存),作为要忽略的人名列表。
|
||||
|
||||
## 匹配策略
|
||||
- 构建一个人名集合 `nameSet`:
|
||||
- `trim()` 后的字符串;
|
||||
- 若开启 `ignoreCase` 则统一转小写匹配。
|
||||
- 判定函数 `isNameWord(word: ArticleWord)`:
|
||||
- 仅当 `word.type === PracticeArticleWordType.Word` 时参与匹配;
|
||||
- 对 `word.word` 进行同样的规范化后 `nameSet.has(...)`。
|
||||
|
||||
## 处理时机与行为
|
||||
- 在 `onTyping` 开始处、拿到 `currentWord` 后:
|
||||
- 若是“人名”,则直接跳过本词;若该词 `nextSpace` 为真,连带空格也跳过(避免进入 `isSpace` 状态)。
|
||||
- 跳过后继续处理当前按键:复用已有模式(如 `isSpace` 分支里)调用 `next()` 和递归 `onTyping(e)`。
|
||||
- 在 `next()` 内也追加“人名跳过”逻辑(与已有忽略符号/数字类似),保证连续多个需要忽略的词可以被连续跳过:
|
||||
- 当 `currentWord` 是人名:
|
||||
- 若 `currentWord.nextSpace` 为真:`isSpace = false`;
|
||||
- 递归调用 `next()` 继续到下一个词;
|
||||
- 否则正常 `emit('nextWord', currentWord)`。
|
||||
|
||||
## 代码改动点
|
||||
- 在组件顶部或方法内构建 `nameSet`(建议用 `$computed`):
|
||||
```ts
|
||||
const nameSet = $computed(() => {
|
||||
const list = props.article?.nameList ?? []
|
||||
return new Set(list.map(s => (settingStore.ignoreCase ? s.toLowerCase() : s).trim()).filter(Boolean))
|
||||
})
|
||||
function isNameWord(w: ArticleWord) {
|
||||
if (w.type !== PracticeArticleWordType.Word) return false
|
||||
const token = (settingStore.ignoreCase ? w.word.toLowerCase() : w.word).trim()
|
||||
return nameSet.has(token)
|
||||
}
|
||||
```
|
||||
- 在 `onTyping` 里,`let currentWord = currentSentence.words[wordIndex]` 之后:
|
||||
```ts
|
||||
if (isNameWord(currentWord)) {
|
||||
// 跳过当前人名,连带空格
|
||||
isSpace = false
|
||||
const savedTypingFlag = isTyping
|
||||
next()
|
||||
isTyping = false
|
||||
return onTyping(e)
|
||||
}
|
||||
```
|
||||
- 在 `next()` 内,设置 `currentWord` 后、`emit('nextWord', currentWord)` 之前:
|
||||
```ts
|
||||
if (isNameWord(currentWord)) {
|
||||
// 人名与后续空格都跳过
|
||||
isSpace = false
|
||||
return next()
|
||||
}
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
- 保持与现有忽略规则一致(符号/数字已通过 `ignoreSymbol` 处理),人名跳过逻辑与其同层级。
|
||||
- 忽略人名时不触发 `wrong` 或提示音,不进入 `isSpace` 等待。
|
||||
- 若连续出现多个需要忽略的词(如人名+标点+人名),递归 `next()` 将逐个跳过。
|
||||
|
||||
## 验证
|
||||
- 在包含人名的文本中练习:
|
||||
- 人名处不需要输入,光标自动跳到下一非忽略词;
|
||||
- 不会卡在空格等待;
|
||||
- 大小写忽略效果符合 `settingStore.ignoreCase` 设置。
|
||||
|
||||
## 交付
|
||||
- 按上述方案在 `TypingArticle.vue` 中实现辅助函数与两处调用点的改动;
|
||||
- 仅修改该文件,不影响其他页面。
|
||||
69
.trae/documents/完善人物名称管理弹框(读取与保存 editArticle.nameList).md
Normal file
69
.trae/documents/完善人物名称管理弹框(读取与保存 editArticle.nameList).md
Normal file
@@ -0,0 +1,69 @@
|
||||
## 目标
|
||||
- 在“人物名称管理”弹框中使用临时变量编辑名称列表,只在点击“确定”时写回 `editArticle.nameList: string[]`
|
||||
|
||||
## 数据结构
|
||||
- `editArticle.nameList: string[]`
|
||||
- 临时变量:`let nameListRef = $ref<string[]>([])`
|
||||
|
||||
## 生命周期
|
||||
- 弹框打开时初始化:`nameListRef = cloneDeep(editArticle.nameList || [])`
|
||||
- 弹框关闭时不写回:丢弃修改
|
||||
|
||||
## 交互设计
|
||||
- 弹框 `v-model="showNameDialog"`、`:footer="true"`、`@close="showNameDialog = false"`、`@ok="saveNameList"`
|
||||
- 按钮:
|
||||
- “添加名称” → `nameListRef.push('')`
|
||||
- 每行名称使用 `BaseInput v-model="nameListRef[i]"`
|
||||
- “删除”名称 → `nameListRef.splice(i,1)`
|
||||
|
||||
## 保存逻辑
|
||||
- `saveNameList()`:
|
||||
- 清理:`trim()` + `filter(Boolean)`
|
||||
- 写回:`editArticle.nameList = cleaned`
|
||||
- 关闭弹框:`showNameDialog = false`
|
||||
|
||||
## 实现细节(EditArticle.vue 增加)
|
||||
- 脚本:
|
||||
```ts
|
||||
let showNameDialog = $ref(false)
|
||||
let nameListRef = $ref<string[]>([])
|
||||
|
||||
watch(() => showNameDialog, (v) => {
|
||||
if (v) nameListRef = cloneDeep(Array.isArray(editArticle.nameList) ? editArticle.nameList : [])
|
||||
})
|
||||
|
||||
function addName() { nameListRef.push('') }
|
||||
function removeName(i: number) { nameListRef.splice(i,1) }
|
||||
function saveNameList() {
|
||||
const cleaned = nameListRef.map(s => (s ?? '').trim()).filter(Boolean)
|
||||
editArticle.nameList = cleaned
|
||||
}
|
||||
```
|
||||
- 模板(620-628 区域):
|
||||
```vue
|
||||
<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 size="small" 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" />
|
||||
<BaseButton size="small" type="info" @click="removeName(i)">删除</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
```
|
||||
|
||||
## 验证
|
||||
- 打开弹框 → 编辑临时列表 → 点击“确定”后检查 `editArticle.nameList` 是否更新;点击关闭则不更新
|
||||
|
||||
## 注意
|
||||
- 若类型仍为旧版 `string[][]`,请同步调整为 `string[]` 以与当前实现一致
|
||||
109
.trae/documents/静态首页移动端适配.md
Normal file
109
.trae/documents/静态首页移动端适配.md
Normal file
@@ -0,0 +1,109 @@
|
||||
## 目标与范围
|
||||
|
||||
* 适配 `public/static-home.html` 在 768px 以下与 480px 以下的移动端展示与触控体验
|
||||
|
||||
* 不改变页面文案与结构,只进行样式与布局响应式改造
|
||||
|
||||
## 结构与布局
|
||||
|
||||
* 将固定宽度 `.w { width: 60vw; }` 改为容器 `.container { width: min(1200px, 92%); }` 并在模板中替换为 `.container`
|
||||
|
||||
* `.card-wrap` 改为自适应栅格:`grid-template-columns: repeat(auto-fit, minmax(240px, 1fr))`
|
||||
|
||||
* 移动端(<=768px)卡片单列展示;间距适当增大,避免拥挤
|
||||
|
||||
## 样式改造
|
||||
|
||||
* 标题缩放:
|
||||
|
||||
* `@media (max-width: 768px) h1 { font-size: 3rem; }`
|
||||
|
||||
* `@media (max-width: 480px) h1 { font-size: 2.4rem; }`
|
||||
|
||||
* 主按钮组:
|
||||
|
||||
* 移动端按钮全宽:`.base-button { width: 100%; margin: .5rem 0; height: 2.8rem; font-size: 1rem; }`
|
||||
|
||||
* 悬浮降低透明度改为轻微位移:`transform: translateY(-1px);`
|
||||
|
||||
* 内容区与间距:
|
||||
|
||||
* `@media (max-width: 768px) .content { margin-top: 4rem; gap: 1.4rem; }`
|
||||
|
||||
* `@media (max-width: 480px) .content { margin-top: 3.2rem; gap: 1.2rem; }`
|
||||
|
||||
* 赞助区 `.sky`:
|
||||
|
||||
* 图片强制全宽:`.sky a { width: 100% !important; } .sky-img { width: 100%; }`
|
||||
|
||||
* 移动端上下内边距缩小,减少跳动
|
||||
|
||||
* 卡片 `.card`:
|
||||
|
||||
* 移动端去掉固定 `width: 25%`,改为自适应栅格控制
|
||||
|
||||
* 增加触控阴影:`box-shadow: 0 6px 20px rgba(0,0,0,.08);`
|
||||
|
||||
* 底部链接 `.bottom`:
|
||||
|
||||
* 在 <=768px 改为纵向堆叠:`flex-direction: column; align-items: flex-start; gap: .6rem;`
|
||||
|
||||
## 示例变更片段(将添加到 <style> 内)
|
||||
|
||||
```css
|
||||
/* 容器与栅格 */
|
||||
.container { width: min(1200px, 92%); }
|
||||
.card-wrap { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 1rem; margin-bottom: 1.2rem; }
|
||||
.card { width: auto; box-shadow: 0 6px 20px rgba(0,0,0,.08); }
|
||||
|
||||
/* 标题与内容间距 */
|
||||
@media (max-width: 768px) {
|
||||
h1 { font-size: 3rem !important; }
|
||||
.content { margin-top: 4rem; gap: 1.4rem; }
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
h1 { font-size: 2.4rem !important; }
|
||||
.content { margin-top: 3.2rem; gap: 1.2rem; }
|
||||
}
|
||||
|
||||
/* 按钮移动端全宽 */
|
||||
@media (max-width: 768px) {
|
||||
.base-button { width: 100%; margin: .5rem 0; height: 2.8rem; font-size: 1rem; }
|
||||
}
|
||||
|
||||
/* 赞助区图片全宽 */
|
||||
.sky a { width: 100% !important; }
|
||||
.sky-img { width: 100%; }
|
||||
|
||||
/* 底部链接在移动端纵向排布 */
|
||||
@media (max-width: 768px) {
|
||||
.bottom { flex-direction: column; align-items: flex-start; gap: .6rem; }
|
||||
}
|
||||
```
|
||||
|
||||
## 交互与触控优化
|
||||
|
||||
* 增加触控可点击区域:`.icon { padding: .2rem; }`
|
||||
|
||||
* 禁用点击高亮:`html, body { -webkit-tap-highlight-color: transparent; }`
|
||||
|
||||
## 可访问性与表现
|
||||
|
||||
* 保持当前 `meta viewport` 设置,确保缩放正确
|
||||
|
||||
* 文本对比度在暗色模式下保持可读
|
||||
|
||||
## 验证方式
|
||||
|
||||
* 本地预览在 Chrome DevTools 切换 iPhone 14 / Pixel 7 视窗
|
||||
|
||||
* 检查首屏按钮是否折行、卡片是否单列、赞助图是否铺满
|
||||
|
||||
* 交互:滚动、点击弹窗与社交按钮触控区域确认
|
||||
|
||||
## 执行与交付
|
||||
|
||||
* 我将按上述片段更新 `<style>` 并把模板里的 `.w` 替换为 `.container`
|
||||
|
||||
* 修改完成后提供预览链接与截图确认
|
||||
|
||||
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']
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
15
public/sound/article/nce1/001&002-Excuse Me.lrc
Normal file
15
public/sound/article/nce1/001&002-Excuse Me.lrc
Normal file
@@ -0,0 +1,15 @@
|
||||
[al:新概念英语(一)]
|
||||
[ar:MP3 同步字幕版(美音)]
|
||||
[ti:Excuse Me!]
|
||||
[by:更多学习内容,请到VeryCD.com搜索“露珠”]
|
||||
[00:00.61]Lesson 1
|
||||
[00:02.71]Excuse me!
|
||||
[00:05.61]Listen to the tape then answer this question.
|
||||
[00:10.80]Whose handbag is it?
|
||||
[00:15.11]Excuse me!
|
||||
[00:16.66]Yes?
|
||||
[00:18.26]Is this your handbag?
|
||||
[00:21.44]Pardon?
|
||||
[00:23.17]Is this your handbag?
|
||||
[00:26.73]Yes it is.
|
||||
[00:29.49]Thank you very much.
|
||||
BIN
public/sound/article/nce1/001&002-Excuse Me.mp3
Normal file
BIN
public/sound/article/nce1/001&002-Excuse Me.mp3
Normal file
Binary file not shown.
20
public/sound/article/nce1/003&004-Sorry, Sir..lrc
Normal file
20
public/sound/article/nce1/003&004-Sorry, Sir..lrc
Normal file
@@ -0,0 +1,20 @@
|
||||
[al:新概念英语(一)]
|
||||
[ar:MP3 同步字幕版(美音)]
|
||||
[ti:Sorry, Sir.]
|
||||
[by:更多学习内容,请到VeryCD.com搜索“露珠”]
|
||||
[00:00.79]Lesson 3
|
||||
[00:02.98]Sorry sir.
|
||||
[00:05.75]Listen to the tape then answer this question.
|
||||
[00:11.71]Does the man get his umbrella back?
|
||||
[00:17.56]My coat and my umbrella please.
|
||||
[00:22.00]Here is my ticket.
|
||||
[00:25.03]Thank you sir.
|
||||
[00:26.86]Number five.
|
||||
[00:29.09]Here's your umbrella and your coat.
|
||||
[00:33.72]This is not my umbrella.
|
||||
[00:37.39]Sorry sir.
|
||||
[00:39.67]Is this your umbrella?
|
||||
[00:42.56]No it isn't.
|
||||
[00:45.69]Is this it?
|
||||
[00:47.76]Yes it is.
|
||||
[00:50.53]Thank you very much.
|
||||
BIN
public/sound/article/nce1/003&004-Sorry, Sir..mp3
Normal file
BIN
public/sound/article/nce1/003&004-Sorry, Sir..mp3
Normal file
Binary file not shown.
28
public/sound/article/nce1/005&006-Nice to Meet You..lrc
Normal file
28
public/sound/article/nce1/005&006-Nice to Meet You..lrc
Normal file
@@ -0,0 +1,28 @@
|
||||
[al:新概念英语(一)]
|
||||
[ar:MP3 同步字幕版(美音)]
|
||||
[ti:Nice to Meet You.]
|
||||
[by:更多学习内容,请到VeryCD.com搜索“露珠”]
|
||||
[00:00.58]Lesson 5
|
||||
[00:02.40]Nice to meet you.
|
||||
[00:05.08]Listen to the tape then answer this question.
|
||||
[00:09.95]Is Chang-woo Chinese?
|
||||
[00:14.23]Good morning.
|
||||
[00:16.07]Good morning Mr. Blake.
|
||||
[00:19.72]This is Miss Sophie Dupont.
|
||||
[00:23.61]Sophie is a new student.
|
||||
[00:26.55]She is French.
|
||||
[00:28.99]Sophie this is Hans.
|
||||
[00:32.54]He is German.
|
||||
[00:34.56]Nice to meet you.
|
||||
[00:37.33]And this is Naoko.
|
||||
[00:39.99]She's Japanese.
|
||||
[00:42.25]Nice to meet you.
|
||||
[00:44.81]And this is Chang-woo.
|
||||
[00:47.49]He's Korean.
|
||||
[00:49.26]Nice to meet you.
|
||||
[00:51.65]And this is Luming.
|
||||
[00:54.14]He's Chinese.
|
||||
[00:56.20]Nice to meet you.
|
||||
[00:58.56]And this is Xiaohui.
|
||||
[01:01.73]She is Chinese too.
|
||||
[01:04.21]Nice to meet you.
|
||||
BIN
public/sound/article/nce1/005&006-Nice to Meet You..mp3
Normal file
BIN
public/sound/article/nce1/005&006-Nice to Meet You..mp3
Normal file
Binary file not shown.
24
public/sound/article/nce1/007&008-Are You a Teacher.lrc
Normal file
24
public/sound/article/nce1/007&008-Are You a Teacher.lrc
Normal file
@@ -0,0 +1,24 @@
|
||||
[al:新概念英语(一)]
|
||||
[ar:MP3 同步字幕版(美音)]
|
||||
[ti:Are You a Teacher?]
|
||||
[by:更多学习内容,请到VeryCD.com搜索“露珠”]
|
||||
[00:00.53]Lesson 7
|
||||
[00:02.58]Are you a teacher?
|
||||
[00:06.03]Listen to the tape then answer this question.
|
||||
[00:10.97]What is Rober's job?
|
||||
[00:15.24]I am a new student.
|
||||
[00:18.00]My name's Robert.
|
||||
[00:20.61]Nice to meet you.
|
||||
[00:22.45]My name's Sophie.
|
||||
[00:25.70]Are you French?
|
||||
[00:27.90]Yes I am.
|
||||
[00:30.38]Are you French too?
|
||||
[00:33.22]No I am not.
|
||||
[00:36.54]What nationality are you?
|
||||
[00:40.10]I'm Italian.
|
||||
[00:43.55]Are you a teacher?
|
||||
[00:46.12]No I'm not.
|
||||
[00:48.45]What's your job?
|
||||
[00:50.64]I'm a keyboard operator.
|
||||
[00:54.18]What's your job?
|
||||
[00:56.34]I'm an engineer.
|
||||
BIN
public/sound/article/nce1/007&008-Are You a Teacher.mp3
Normal file
BIN
public/sound/article/nce1/007&008-Are You a Teacher.mp3
Normal file
Binary file not shown.
22
public/sound/article/nce1/009&010-How Are You Today.lrc
Normal file
22
public/sound/article/nce1/009&010-How Are You Today.lrc
Normal file
@@ -0,0 +1,22 @@
|
||||
[al:新概念英语(一)]
|
||||
[ar:MP3 同步字幕版(美音)]
|
||||
[ti:How Are You Today?]
|
||||
[by:更多学习内容,请到VeryCD.com搜索“露珠”]
|
||||
[00:00.56]Lesson 9
|
||||
[00:02.48]How are you today?
|
||||
[00:05.79]Listen to the tape then answer this question.
|
||||
[00:11.54]How is Emma?
|
||||
[00:15.17]Hello Helen.
|
||||
[00:17.19]Hi Steven.
|
||||
[00:19.02]How are you today?
|
||||
[00:21.69]I'm very well thank you.
|
||||
[00:24.28]And you?
|
||||
[00:25.76]I'm fine thanks.
|
||||
[00:28.79]How is Tony?
|
||||
[00:30.80]He's fine thanks.
|
||||
[00:33.36]How's Emma?
|
||||
[00:34.95]She's very well too Helen.
|
||||
[00:39.20]Goodbye Helen.
|
||||
[00:40.70]Nice to see you.
|
||||
[00:42.76]Nice to see you too Steven.
|
||||
[00:46.17]Goodbye.
|
||||
BIN
public/sound/article/nce1/009&010-How Are You Today.mp3
Normal file
BIN
public/sound/article/nce1/009&010-How Are You Today.mp3
Normal file
Binary file not shown.
24
public/sound/article/nce1/011&012-Is This Your Shirt.lrc
Normal file
24
public/sound/article/nce1/011&012-Is This Your Shirt.lrc
Normal file
@@ -0,0 +1,24 @@
|
||||
[al:新概念英语(一)]
|
||||
[ar:MP3 同步字幕版(美音)]
|
||||
[ti:Is This Your Shirt?]
|
||||
[by:更多学习内容,请到VeryCD.com搜索“露珠”]
|
||||
[00:00.84]Lesson 11
|
||||
[00:03.39]Is this your shirt?
|
||||
[00:06.65]Listen to the tape then answer this question.
|
||||
[00:13.23]Whose shirt is white?
|
||||
[00:17.18]Whose shirt is that?
|
||||
[00:20.08]Is this your shirt Dave?
|
||||
[00:23.18]No. Sir.
|
||||
[00:24.58]It's not my shirt.
|
||||
[00:27.86]This is my shirt.
|
||||
[00:30.45]My shirt's blue.
|
||||
[00:33.31]Is this shirt Tim's?
|
||||
[00:36.52]Perhaps it is sir.
|
||||
[00:39.51]Tim's shirt's white.
|
||||
[00:42.22]Tim!
|
||||
[00:43.78]Yes sir?
|
||||
[00:45.35]Is this your shirt?
|
||||
[00:47.38]Yes sir.
|
||||
[00:48.80]Here you are.
|
||||
[00:50.45]Catch!
|
||||
[00:51.54]Thank you sir.
|
||||
BIN
public/sound/article/nce1/011&012-Is This Your Shirt.mp3
Normal file
BIN
public/sound/article/nce1/011&012-Is This Your Shirt.mp3
Normal file
Binary file not shown.
21
public/sound/article/nce1/013&014-A New Dress.lrc
Normal file
21
public/sound/article/nce1/013&014-A New Dress.lrc
Normal file
@@ -0,0 +1,21 @@
|
||||
[al:新概念英语(一)]
|
||||
[ar:MP3 同步字幕版(美音)]
|
||||
[ti:A New Dress]
|
||||
[by:更多学习内容,请到VeryCD.com搜索“露珠”]
|
||||
[00:00.41]Lesson 13
|
||||
[00:02.71]A new dress
|
||||
[00:05.53]Listen to the tape then answer this question.
|
||||
[00:11.72]What colour is Anna's hat?
|
||||
[00:16.67]What colour's your new dress?
|
||||
[00:20.46]It's green.
|
||||
[00:22.52]Come upstairs and see it.
|
||||
[00:25.78]Thank you.
|
||||
[00:27.53]Look!
|
||||
[00:28.66]Here it is!
|
||||
[00:30.79]That's a nice dress.
|
||||
[00:33.51]It's very smart.
|
||||
[00:36.16]My hat's new too.
|
||||
[00:39.22]What colour is it?
|
||||
[00:41.62]It's the same colour.
|
||||
[00:44.18]It's green too.
|
||||
[00:47.12]That is a lovely hat!
|
||||
BIN
public/sound/article/nce1/013&014-A New Dress.mp3
Normal file
BIN
public/sound/article/nce1/013&014-A New Dress.mp3
Normal file
Binary file not shown.
@@ -0,0 +1,26 @@
|
||||
[al:新概念英语(一)]
|
||||
[ar:MP3 同步字幕版(美音)]
|
||||
[ti:Your Passports, Please.]
|
||||
[by:更多学习内容,请到VeryCD.com搜索“露珠”]
|
||||
[00:00.50]Lesson 15
|
||||
[00:02.89]Your passports please.
|
||||
[00:06.37]Listen to the tape then answer this question.
|
||||
[00:12.30]Is there a problem with the Customs officer?
|
||||
[00:18.83]Are you Swedish?
|
||||
[00:21.40]No we are not.
|
||||
[00:23.94]We are Danish.
|
||||
[00:26.63]Are your friends Danish too?
|
||||
[00:30.21]No they aren't.
|
||||
[00:33.75]They are Norwegian.
|
||||
[00:37.46]Your passports please.
|
||||
[00:40.57]Here they are.
|
||||
[00:43.15]Are these your cases?
|
||||
[00:46.07]No they aren't.
|
||||
[00:48.94]Our cases are brown.
|
||||
[00:53.07]Here they are.
|
||||
[00:56.09]Are you tourists?
|
||||
[00:58.86]Yes we are.
|
||||
[01:01.87]Are your friends tourists too?
|
||||
[01:05.41]Yes they are.
|
||||
[01:08.66]That's fine.
|
||||
[01:10.78]Thank you very much.
|
||||
BIN
public/sound/article/nce1/015&016-Your Passports, Please..mp3
Normal file
BIN
public/sound/article/nce1/015&016-Your Passports, Please..mp3
Normal file
Binary file not shown.
26
public/sound/article/nce1/017&018-How do you do.lrc
Normal file
26
public/sound/article/nce1/017&018-How do you do.lrc
Normal file
@@ -0,0 +1,26 @@
|
||||
[al:新概念英语(一)]
|
||||
[ar:MP3 同步字幕版(美音)]
|
||||
[ti:How do you do?]
|
||||
[by:更多学习内容,请到VeryCD.com搜索“露珠”]
|
||||
[00:00.58]Lesson 17
|
||||
[00:02.89]How do you do?
|
||||
[00:06.40]Listen to the tape then answer this question.
|
||||
[00:12.62]What are Michael Baker and Jeremy Short's jobs?
|
||||
[00:19.59]Come and meet our employees Mr.Richards.
|
||||
[00:25.19]Thank you Mr. Jackson.
|
||||
[00:28.45]This is Nicola Grey
|
||||
[00:31.57]and this is Claire Taylor.
|
||||
[00:35.30]How do you do?
|
||||
[00:38.40]Those women are very hard-working.
|
||||
[00:43.18]What are their jobs?
|
||||
[00:46.60]They're keyboard operators.
|
||||
[00:50.90]This is Michael Baker
|
||||
[00:53.85]and this is Jeremy Short.
|
||||
[00:57.42]How do you do?
|
||||
[01:00.67]They aren't very busy!
|
||||
[01:03.80]What are their jobs?
|
||||
[01:07.95]They're sales reps.
|
||||
[01:10.21]They're very lazy.
|
||||
[01:13.28]Who is this young man?
|
||||
[01:17.03]This is Jim.
|
||||
[01:19.42]He's our office assistant.
|
||||
BIN
public/sound/article/nce1/017&018-How do you do.mp3
Normal file
BIN
public/sound/article/nce1/017&018-How do you do.mp3
Normal file
Binary file not shown.
22
public/sound/article/nce1/019&020-Tired and Thirsty.lrc
Normal file
22
public/sound/article/nce1/019&020-Tired and Thirsty.lrc
Normal file
@@ -0,0 +1,22 @@
|
||||
[al:新概念英语(一)]
|
||||
[ar:MP3 同步字幕版(美音)]
|
||||
[ti:Tired and Thirsty]
|
||||
[by:更多学习内容,请到VeryCD.com搜索“露珠”]
|
||||
[00:00.65]Lesson 19
|
||||
[00:02.86]Tired and thirsty
|
||||
[00:06.24]Listen to the tape then answer this question.
|
||||
[00:12.28]Why do the children thank their mother?
|
||||
[00:17.95]What's the matter children?
|
||||
[00:20.85]We're tired ...
|
||||
[00:22.71]... and thirsty Mum.
|
||||
[00:25.26]Sit down here.
|
||||
[00:27.82]Are you all right now?
|
||||
[00:30.11]No we aren't.
|
||||
[00:32.52]Look!
|
||||
[00:33.61]There's an ice cream man.
|
||||
[00:36.94]Two ice cream please.
|
||||
[00:40.68]Here you are children.
|
||||
[00:43.66]Thanks Mum.
|
||||
[00:46.09]These ice creams are nice.
|
||||
[00:49.82]Are you all right now?
|
||||
[00:52.89]Yes we are thank you!
|
||||
BIN
public/sound/article/nce1/019&020-Tired and Thirsty.mp3
Normal file
BIN
public/sound/article/nce1/019&020-Tired and Thirsty.mp3
Normal file
Binary file not shown.
16
public/sound/article/nce1/021&022-Which Book.lrc
Normal file
16
public/sound/article/nce1/021&022-Which Book.lrc
Normal file
@@ -0,0 +1,16 @@
|
||||
[al:新概念英语(一)]
|
||||
[ar:MP3 同步字幕版(美音)]
|
||||
[ti:Which Book?]
|
||||
[by:更多学习内容,请到VeryCD.com搜索“露珠”]
|
||||
[00:00.35]Lesson 21
|
||||
[00:02.87]Which book?
|
||||
[00:05.43]Listen to the tape then answer this question.
|
||||
[00:11.53]Which book does the man want?
|
||||
[00:16.70]Give me a book please, Jane.
|
||||
[00:20.22]Which book?
|
||||
[00:21.83]This one?
|
||||
[00:24.33]No, not that one. The red one.
|
||||
[00:29.44]This one?
|
||||
[00:30.97]Yes, please.
|
||||
[00:32.78]Here you are.
|
||||
[00:34.81]Thank you.
|
||||
BIN
public/sound/article/nce1/021&022-Which Book.mp3
Normal file
BIN
public/sound/article/nce1/021&022-Which Book.mp3
Normal file
Binary file not shown.
17
public/sound/article/nce1/023&024-Which Glasses.lrc
Normal file
17
public/sound/article/nce1/023&024-Which Glasses.lrc
Normal file
@@ -0,0 +1,17 @@
|
||||
[al:新概念英语(一)]
|
||||
[ar:MP3 同步字幕版(美音)]
|
||||
[ti:Which Glasses?]
|
||||
[by:更多学习内容,请到VeryCD.com搜索“露珠”]
|
||||
[00:00.42]Lesson 23
|
||||
[00:02.92]Which glasses?
|
||||
[00:05.64]Listen to the tape then answer this question.
|
||||
[00:11.83]Which glasses does the man want?
|
||||
[00:17.14]Give me some glasses please, Jane.
|
||||
[00:21.18]Which glasses?
|
||||
[00:23.28]These glasses?
|
||||
[00:25.83]No, not those.
|
||||
[00:28.58]The ones on the shelf.
|
||||
[00:31.40]These?
|
||||
[00:32.84]Yes, please.
|
||||
[00:34.98]Here you are.
|
||||
[00:37.13]Thanks.
|
||||
BIN
public/sound/article/nce1/023&024-Which Glasses.mp3
Normal file
BIN
public/sound/article/nce1/023&024-Which Glasses.mp3
Normal file
Binary file not shown.
20
public/sound/article/nce1/025&026-Mrs. Smith's Kitchen.lrc
Normal file
20
public/sound/article/nce1/025&026-Mrs. Smith's Kitchen.lrc
Normal file
@@ -0,0 +1,20 @@
|
||||
[al:新概念英语(一)]
|
||||
[ar:MP3 同步字幕版(美音)]
|
||||
[ti:Mrs. Smith's Kitchen]
|
||||
[by:更多学习内容,请到VeryCD.com搜索“露珠”]
|
||||
[00:00.41]Lesson 25
|
||||
[00:03.18]Mrs. Smith's Kitchen
|
||||
[00:06.61]Listen to the tape then answer this question.
|
||||
[00:12.58]What colour is the electric cooker?
|
||||
[00:18.31]Mrs. Smith's kitchen is small.
|
||||
[00:23.12]There is a refrigerator in the kitchen.
|
||||
[00:27.71]The refrigerator is white.
|
||||
[00:31.62]It is on the right.
|
||||
[00:35.03]There is an electric cooker in the kitchen.
|
||||
[00:39.62]The cooker is blue.
|
||||
[00:42.95]It is on the left.
|
||||
[00:46.22]There is a table in the middle of the room.
|
||||
[00:51.54]There is a bottle on the table.
|
||||
[00:55.27]The bottle is empty.
|
||||
[00:59.10]There is a cup on the table, too.
|
||||
[01:03.83]The cup is clean.
|
||||
BIN
public/sound/article/nce1/025&026-Mrs. Smith's Kitchen.mp3
Normal file
BIN
public/sound/article/nce1/025&026-Mrs. Smith's Kitchen.mp3
Normal file
Binary file not shown.
@@ -0,0 +1,21 @@
|
||||
[al:新概念英语(一)]
|
||||
[ar:MP3 同步字幕版(美音)]
|
||||
[ti:Mrs. Smith's Living Room]
|
||||
[by:更多学习内容,请到VeryCD.com搜索“露珠”]
|
||||
[00:00.55]Lesson 27
|
||||
[00:03.19]Mrs. Smith's living room
|
||||
[00:06.96]Listen to the tape then answer this question.
|
||||
[00:12.77]Where are the books?
|
||||
[00:16.76]Mrs. Smith's living room is large.
|
||||
[00:21.63]There is a television in the room.
|
||||
[00:26.40]The television is near the window.
|
||||
[00:31.28]There are some magazines on the television.
|
||||
[00:36.45]There is a table in the room.
|
||||
[00:40.44]There are some newspapers on the table.
|
||||
[00:45.11]There are some armchairs in the room.
|
||||
[00:49.27]The armchairs are near the table.
|
||||
[00:53.31]There is a stereo in the room.
|
||||
[00:57.53]The stereo is near the door.
|
||||
[01:01.55]There are some books on the stereo.
|
||||
[01:05.69]There are some pictures in the room.
|
||||
[01:09.62]The pictures are on the wall.
|
||||
BIN
public/sound/article/nce1/027&028-Mrs. Smith's Living Room.mp3
Normal file
BIN
public/sound/article/nce1/027&028-Mrs. Smith's Living Room.mp3
Normal file
Binary file not shown.
17
public/sound/article/nce1/029&030-Come In, Amy..lrc
Normal file
17
public/sound/article/nce1/029&030-Come In, Amy..lrc
Normal file
@@ -0,0 +1,17 @@
|
||||
[al:新概念英语(一)]
|
||||
[ar:MP3 同步字幕版(美音)]
|
||||
[ti:Come In, Amy.]
|
||||
[by:更多学习内容,请到VeryCD.com搜索“露珠”]
|
||||
[00:00.52]Lesson 29
|
||||
[00:03.33]Come in, Amy.
|
||||
[00:06.85]Listen to the tape then answer this question.
|
||||
[00:12.60]How must Amy clean the floor?
|
||||
[00:17.30]Come in, Amy.
|
||||
[00:19.85]Shut the door, please.
|
||||
[00:22.49]This bedroom's very untidy.
|
||||
[00:26.83]What must I do, Mrs. Jones?
|
||||
[00:30.53]Open the window and air the room.
|
||||
[00:34.92]Then put these clothes in the wardrobe.
|
||||
[00:39.69]Then make the bed.
|
||||
[00:42.81]Dust the dressing table.
|
||||
[00:45.85]Then sweep the floor.
|
||||
BIN
public/sound/article/nce1/029&030-Come In, Amy..mp3
Normal file
BIN
public/sound/article/nce1/029&030-Come In, Amy..mp3
Normal file
Binary file not shown.
22
public/sound/article/nce1/031&032-Where's Sally.lrc
Normal file
22
public/sound/article/nce1/031&032-Where's Sally.lrc
Normal file
@@ -0,0 +1,22 @@
|
||||
[al:新概念英语(一)]
|
||||
[ar:MP3 同步字幕版(美音)]
|
||||
[ti:Where's Sally?]
|
||||
[by:更多学习内容,请到VeryCD.com搜索“露珠”]
|
||||
[00:00.51]Lesson 31
|
||||
[00:03.04]Where's Sally?
|
||||
[00:05.65]Listen to the tape then answer this question.
|
||||
[00:11.45]Is the cat climbing the tree?
|
||||
[00:15.84]Where's Sally, Jack?
|
||||
[00:18.67]She's in the garden, Jean.
|
||||
[00:21.45]What's she doing?
|
||||
[00:23.47]She's sitting under the tree.
|
||||
[00:26.56]Is Tim in the garden, too?
|
||||
[00:29.88]Yes, he is.
|
||||
[00:31.82]He's climbing the tree.
|
||||
[00:34.68]I beg your pardon?
|
||||
[00:36.91]Who's climbing the tree?
|
||||
[00:39.47]Tim is.
|
||||
[00:41.41]What about the dog?
|
||||
[00:44.25]The dog's in the garden, too.
|
||||
[00:46.89]It's running across the grass.
|
||||
[00:50.02]It's running after a cat.
|
||||
BIN
public/sound/article/nce1/031&032-Where's Sally.mp3
Normal file
BIN
public/sound/article/nce1/031&032-Where's Sally.mp3
Normal file
Binary file not shown.
19
public/sound/article/nce1/033&034-A Fine Day.lrc
Normal file
19
public/sound/article/nce1/033&034-A Fine Day.lrc
Normal file
@@ -0,0 +1,19 @@
|
||||
[al:新概念英语(一)]
|
||||
[ar:MP3 同步字幕版(美音)]
|
||||
[ti:A Fine Day]
|
||||
[by:更多学习内容,请到VeryCD.com搜索“露珠”]
|
||||
[00:00.71]Lesson 33
|
||||
[00:03.26]A fine day
|
||||
[00:06.02]Listen to the tape then answer this question.
|
||||
[00:11.42]Where is the Jones family?
|
||||
[00:16.26]It is a fine day today.
|
||||
[00:19.97]There are some clouds in the sky,
|
||||
[00:24.18]but the sun is shining.
|
||||
[00:27.74]Mr. Jones is with his family.
|
||||
[00:31.78]They are walking over the bridge.
|
||||
[00:35.83]There are some boats on the river.
|
||||
[00:39.86]Mr. Jones and his wife are looking at them.
|
||||
[00:45.73]Sally is looking at a big ship.
|
||||
[00:50.18]The ship is going under the bridge.
|
||||
[00:54.75]Tim is looking at an aeroplane.
|
||||
[00:59.51]The aeroplane is flying over the river.
|
||||
BIN
public/sound/article/nce1/033&034-A Fine Day.mp3
Normal file
BIN
public/sound/article/nce1/033&034-A Fine Day.mp3
Normal file
Binary file not shown.
23
public/sound/article/nce1/035&036-Our Village.lrc
Normal file
23
public/sound/article/nce1/035&036-Our Village.lrc
Normal file
@@ -0,0 +1,23 @@
|
||||
[al:新概念英语(一)]
|
||||
[ar:MP3 同步字幕版(美音)]
|
||||
[ti:Our Village]
|
||||
[by:更多学习内容,请到VeryCD.com搜索“露珠”]
|
||||
[00:00.68]Lesson 35
|
||||
[00:03.21]Our village
|
||||
[00:06.06]Listen to the tape then answer this question.
|
||||
[00:11.74]Are the children coming out of the park or going into it?
|
||||
[00:18.97]This is a photograph of our village.
|
||||
[00:23.50]Our village is in a valley.
|
||||
[00:26.89]It is between two hills.
|
||||
[00:30.24]The village is on a river.
|
||||
[00:34.69]Here is another photograph of the village.
|
||||
[00:39.62]My wife and I are walking along the banks of the river.
|
||||
[00:46.39]We are on the left.
|
||||
[00:49.59]There is a boy in the water.
|
||||
[00:52.93]He is swimming across the river.
|
||||
[00:58.26]Here is another photograph.
|
||||
[01:01.76]This is the school building.
|
||||
[01:04.76]It is beside a park.
|
||||
[01:08.09]The park is on the right.
|
||||
[01:11.52]Some children are coming out of the building.
|
||||
[01:15.99]Some of them are going into the park.
|
||||
BIN
public/sound/article/nce1/035&036-Our Village.mp3
Normal file
BIN
public/sound/article/nce1/035&036-Our Village.mp3
Normal file
Binary file not shown.
26
public/sound/article/nce1/037&038-Making a Bookcase.lrc
Normal file
26
public/sound/article/nce1/037&038-Making a Bookcase.lrc
Normal file
@@ -0,0 +1,26 @@
|
||||
[al:新概念英语(一)]
|
||||
[ar:MP3 同步字幕版(美音)]
|
||||
[ti:Making a Bookcase]
|
||||
[by:更多学习内容,请到VeryCD.com搜索“露珠”]
|
||||
[00:00.54]Lesson 37
|
||||
[00:03.05]Making a bookcase
|
||||
[00:06.11]Listen to the tape then answer this question.
|
||||
[00:12.30]What is Susan's favourite colour?
|
||||
[00:17.45]You're working hard, George.
|
||||
[00:20.72]What are you doing?
|
||||
[00:23.22]I'm making a bookcase.
|
||||
[00:26.81]Give me that hammer please, Dan.
|
||||
[00:30.82]Which hammer?
|
||||
[00:32.64]This one?
|
||||
[00:34.12]No, not that one.
|
||||
[00:36.53]The big one.
|
||||
[00:38.94]Here you are.
|
||||
[00:40.56]Thanks, Dan.
|
||||
[00:43.51]What are you going to do now, George?
|
||||
[00:47.84]I'm going to paint it.
|
||||
[00:51.12]What colour are you going to paint it?
|
||||
[00:55.48]I'm going to paint it pink.
|
||||
[00:59.50]Pink!
|
||||
[01:00.99]This bookcase isn't for me.
|
||||
[01:03.96]It's for my daughter, Susan.
|
||||
[01:07.10]Pink's her favourite colour.
|
||||
20
public/sound/article/nce1/039&040-Don't Drop It.lrc
Normal file
20
public/sound/article/nce1/039&040-Don't Drop It.lrc
Normal file
@@ -0,0 +1,20 @@
|
||||
[al:新概念英语(一)]
|
||||
[ar:MP3 同步字幕版(美音)]
|
||||
[ti:Don't Drop It!]
|
||||
[by:更多学习内容,请到VeryCD.com搜索“露珠”]
|
||||
[00:00.78]Lesson 39
|
||||
[00:03.11]Don't drop it!
|
||||
[00:06.28]Listen to the tape then answer this question.
|
||||
[00:12.44]Where does Sam put the vase in the end?
|
||||
[00:18.55]What are you going to do with that vase, Penny?
|
||||
[00:24.02]I'm going to put it on this table, Sam.
|
||||
[00:29.15]Don't do that.
|
||||
[00:31.31]Give it to me.
|
||||
[00:33.63]What are you going to do with it?
|
||||
[00:37.39]I'm going to put it here, in front of the window.
|
||||
[00:43.03]Be careful! Don't drop it!
|
||||
[00:47.32]Don't put it there, Sam.
|
||||
[00:50.31]Put it here, on this shelf.
|
||||
[00:54.70]There we are!
|
||||
[00:56.74]It's a lovely vase.
|
||||
[00:59.47]Those flowers are lovely, too.
|
||||
@@ -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,8 +1,7 @@
|
||||
import { onMounted, onUnmounted, watch, onDeactivated } from "vue";
|
||||
import { onDeactivated, onMounted, onUnmounted, watch } from "vue";
|
||||
import { emitter, EventKey } from "@/utils/eventBus.ts";
|
||||
import { useRuntimeStore } from "@/stores/runtime.ts";
|
||||
import { useSettingStore } from "@/stores/setting.ts";
|
||||
import { ShortcutKey } from "@/types/types.ts";
|
||||
import { isMobile } from "@/utils";
|
||||
|
||||
export function useWindowClick(cb: (e: PointerEvent) => void) {
|
||||
|
||||
@@ -274,7 +274,7 @@ function next() {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.dict-detail-card {
|
||||
min-height: calc(100vh - 3rem);
|
||||
height: calc(100vh - 3rem);
|
||||
}
|
||||
|
||||
.dict-header {
|
||||
@@ -287,7 +287,7 @@ function next() {
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.dict-detail-card {
|
||||
min-height: calc(100vh - 2rem);
|
||||
height: calc(100vh - 2rem);
|
||||
}
|
||||
|
||||
.dict-header {
|
||||
|
||||
@@ -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
|
||||
@@ -307,7 +330,7 @@ function setStartTime(val: Sentence, i: number, j: number) {
|
||||
val.audioPosition[0] = Number(Number(audioRef.currentTime).toFixed(2))
|
||||
}
|
||||
if (val.audioPosition[0] > val.audioPosition[1] && val.audioPosition[1] !== 0) {
|
||||
val.audioPosition[1] = val.audioPosition[0]
|
||||
val.audioPosition[1] = 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -5,7 +5,7 @@ import {useBaseStore} from "@/stores/base.ts";
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
import {usePlayBeep, usePlayCorrect, usePlayKeyboardAudio} from "@/hooks/sound.ts";
|
||||
import {emitter, EventKey, useEvents} from "@/utils/eventBus.ts";
|
||||
import { _dateFormat, _nextTick, isMobile, msToHourMinute, total } from "@/utils";
|
||||
import {_dateFormat, _nextTick, isMobile, msToHourMinute, total} from "@/utils";
|
||||
import '@imengyu/vue3-context-menu/lib/vue3-context-menu.css'
|
||||
import ContextMenu from '@imengyu/vue3-context-menu'
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
@@ -19,6 +19,7 @@ import nlp from "compromise/three";
|
||||
import {nanoid} from "nanoid";
|
||||
import {usePracticeStore} from "@/stores/practice.ts";
|
||||
import {PracticeSaveArticleKey} from "@/config/env.ts";
|
||||
import {retry} from "ali-oss/lib/common/utils/retry";
|
||||
|
||||
interface IProps {
|
||||
article: Article,
|
||||
@@ -150,6 +151,10 @@ function init() {
|
||||
})
|
||||
typeArticleRef?.scrollTo({top: 0, behavior: "smooth"})
|
||||
}
|
||||
_nextTick(() => {
|
||||
emit('play', {sentence: props.article.sections[sectionIndex][sentenceIndex], handle: false})
|
||||
if (isNameWord()) next()
|
||||
})
|
||||
checkTranslateLocation().then(() => checkCursorPosition())
|
||||
focusMobileInput()
|
||||
}
|
||||
@@ -222,8 +227,10 @@ function processMobileCharacter(char: string) {
|
||||
const fakeEvent = {
|
||||
key: char,
|
||||
code,
|
||||
preventDefault() {},
|
||||
stopPropagation() {},
|
||||
preventDefault() {
|
||||
},
|
||||
stopPropagation() {
|
||||
},
|
||||
} as unknown as KeyboardEvent
|
||||
onTyping(fakeEvent)
|
||||
}
|
||||
@@ -247,6 +254,21 @@ function handleMobileBeforeInput(event: InputEvent) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const normalize = (s: string) => s.toLowerCase().trim()
|
||||
const namePatterns = $computed(() => {
|
||||
return Array.from(new Set((props.article?.nameList ?? []).map(normalize).filter(Boolean).map(s => s.split(/\s+/).filter(Boolean)).flat().concat([
|
||||
'Mr', 'Mrs', 'Ms', 'Dr', 'Miss',
|
||||
].map(normalize))))
|
||||
})
|
||||
|
||||
const isNameWord = () => {
|
||||
let currentSection = props.article.sections[sectionIndex]
|
||||
let currentSentence = currentSection[sentenceIndex]
|
||||
let w: ArticleWord = currentSentence.words[wordIndex]
|
||||
return w?.type === PracticeArticleWordType.Word && namePatterns.length > 0 && namePatterns.includes(normalize(w.word))
|
||||
}
|
||||
|
||||
let isTyping = false
|
||||
//专用锁,因为这个方法父级要调用
|
||||
let lock = false
|
||||
@@ -279,15 +301,47 @@ function nextSentence() {
|
||||
isEnd = true
|
||||
emit('complete')
|
||||
} else {
|
||||
if (isNameWord()) next()
|
||||
emit('play', {sentence: props.article.sections[sectionIndex][0], handle: false})
|
||||
}
|
||||
} else {
|
||||
if (isNameWord()) next()
|
||||
emit('play', {sentence: currentSection[sentenceIndex], handle: false})
|
||||
|
||||
}
|
||||
lock = false
|
||||
focusMobileInput()
|
||||
}
|
||||
|
||||
|
||||
const next = () => {
|
||||
isSpace = false;
|
||||
input = wrong = ''
|
||||
stringIndex = 0;
|
||||
|
||||
let currentSection = props.article.sections[sectionIndex]
|
||||
let currentSentence = currentSection[sentenceIndex]
|
||||
let currentWord: ArticleWord = currentSentence.words[wordIndex]
|
||||
|
||||
// 检查下一个单词是否存在
|
||||
if (wordIndex + 1 < currentSentence.words.length) {
|
||||
wordIndex++;
|
||||
currentWord = currentSentence.words[wordIndex]
|
||||
//这里把未输入的单词补全,因为删除时会用到input
|
||||
currentSentence.words.slice(0, wordIndex).forEach((word, i) => {
|
||||
word.input = word.input + word.word.slice(word.input?.length ?? 0)
|
||||
})
|
||||
if ([PracticeArticleWordType.Symbol, PracticeArticleWordType.Number].includes(currentWord.type) && settingStore.ignoreSymbol) {
|
||||
next()
|
||||
} else if (isNameWord()) {
|
||||
next()
|
||||
} else {
|
||||
emit('nextWord', currentWord);
|
||||
}
|
||||
} else {
|
||||
nextSentence()
|
||||
}
|
||||
}
|
||||
|
||||
function onTyping(e: KeyboardEvent) {
|
||||
debugger
|
||||
if (!props.article.sections.length) return
|
||||
@@ -300,24 +354,6 @@ function onTyping(e: KeyboardEvent) {
|
||||
let currentWord: ArticleWord = currentSentence.words[wordIndex]
|
||||
wrong = ''
|
||||
|
||||
const next = () => {
|
||||
isSpace = false;
|
||||
input = wrong = ''
|
||||
stringIndex = 0;
|
||||
// 检查下一个单词是否存在
|
||||
if (wordIndex + 1 < currentSentence.words.length) {
|
||||
wordIndex++;
|
||||
currentWord = currentSentence.words[wordIndex]
|
||||
if ([PracticeArticleWordType.Symbol,PracticeArticleWordType.Number].includes(currentWord.type) && settingStore.ignoreSymbol){
|
||||
next()
|
||||
}else {
|
||||
emit('nextWord', currentWord);
|
||||
}
|
||||
} else {
|
||||
nextSentence()
|
||||
}
|
||||
}
|
||||
|
||||
if (isSpace) {
|
||||
if (e.code === 'Space') {
|
||||
next()
|
||||
@@ -334,10 +370,13 @@ function onTyping(e: KeyboardEvent) {
|
||||
// }, 500)
|
||||
}
|
||||
} else {
|
||||
//如果是首句首词
|
||||
if (sectionIndex === 0 && sentenceIndex === 0 && wordIndex === 0 && stringIndex === 0) {
|
||||
emit('play', {sentence: currentSection[sentenceIndex], handle: false})
|
||||
}
|
||||
|
||||
// if (isNameWord(currentWord)) {
|
||||
// isSpace = false
|
||||
// next()
|
||||
// isTyping = false
|
||||
// return onTyping(e)
|
||||
// }
|
||||
let letter = e.key
|
||||
let key = currentWord.word[stringIndex]
|
||||
// console.log('key', key,)
|
||||
@@ -376,7 +415,7 @@ function onTyping(e: KeyboardEvent) {
|
||||
//todo 上报
|
||||
localStorage.removeItem(PracticeSaveArticleKey.key)
|
||||
init()
|
||||
}finally {
|
||||
} finally {
|
||||
isTyping = false
|
||||
}
|
||||
}
|
||||
@@ -599,19 +638,22 @@ const currentPractice = inject('currentPractice', [])
|
||||
<template>
|
||||
<div class="typing-article" ref="typeArticleRef" @click="focusMobileInput">
|
||||
<input
|
||||
v-if="isMob"
|
||||
ref="mobileInputRef"
|
||||
class="mobile-input"
|
||||
type="text"
|
||||
inputmode="text"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="none"
|
||||
@beforeinput="handleMobileBeforeInput"
|
||||
@input="handleMobileInput"
|
||||
v-if="isMob"
|
||||
ref="mobileInputRef"
|
||||
class="mobile-input"
|
||||
type="text"
|
||||
inputmode="text"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="none"
|
||||
@beforeinput="handleMobileBeforeInput"
|
||||
@input="handleMobileInput"
|
||||
/>
|
||||
<header class="mb-4">
|
||||
<div class="title word"><span class="font-family text-3xl">{{ store.sbook.lastLearnIndex + 1 }}.</span>{{ props.article.title }}</div>
|
||||
<div class="title word"><span class="font-family text-3xl">{{
|
||||
store.sbook.lastLearnIndex + 1
|
||||
}}.</span>{{ props.article.title }}
|
||||
</div>
|
||||
<div class="titleTranslate" v-if="settingStore.translate">{{ props.article.titleTranslate }}</div>
|
||||
</header>
|
||||
|
||||
@@ -624,10 +666,10 @@ const currentPractice = inject('currentPractice', [])
|
||||
<span class="sentence"
|
||||
v-for="(sentence,indexJ) in section">
|
||||
<span
|
||||
v-for="(word,indexW) in sentence.words"
|
||||
@contextmenu="e=>onContextMenu(e,sentence,indexI,indexJ,indexW)"
|
||||
class="word"
|
||||
:class="[(sectionIndex>indexI
|
||||
v-for="(word,indexW) in sentence.words"
|
||||
@contextmenu="e=>onContextMenu(e,sentence,indexI,indexJ,indexW)"
|
||||
class="word"
|
||||
:class="[(sectionIndex>indexI
|
||||
?'wrote':
|
||||
(sectionIndex>=indexI &&sentenceIndex>indexJ)
|
||||
?'wrote' :
|
||||
@@ -654,16 +696,16 @@ const currentPractice = inject('currentPractice', [])
|
||||
<span class="border-bottom" v-if="settingStore.dictation"></span>
|
||||
</span>
|
||||
<Space
|
||||
v-if="word.nextSpace"
|
||||
class="word-end"
|
||||
:is-wrong="false"
|
||||
:is-wait="isCurrent(indexI,indexJ,indexW) && isSpace"
|
||||
:is-shake="isCurrent(indexI,indexJ,indexW) && isSpace && wrong !== ''"
|
||||
v-if="word.nextSpace"
|
||||
class="word-end"
|
||||
:is-wrong="false"
|
||||
:is-wait="isCurrent(indexI,indexJ,indexW) && isSpace"
|
||||
:is-shake="isCurrent(indexI,indexJ,indexW) && isSpace && wrong !== ''"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
class="sentence-translate-mobile"
|
||||
v-if="isMob && settingStore.translate && sentence.translate">
|
||||
class="sentence-translate-mobile"
|
||||
v-if="isMob && settingStore.translate && sentence.translate">
|
||||
{{ sentence.translate }}
|
||||
</span>
|
||||
</span>
|
||||
@@ -693,11 +735,11 @@ const currentPractice = inject('currentPractice', [])
|
||||
|
||||
<div class="options flex justify-center" v-if="isEnd">
|
||||
<BaseButton
|
||||
@click="emit('replay')">重新练习
|
||||
@click="emit('replay')">重新练习
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
v-if="store.currentBook.lastLearnIndex < store.currentBook.articles.length - 1"
|
||||
@click="emit('next')">下一篇
|
||||
v-if="store.currentBook.lastLearnIndex < store.currentBook.articles.length - 1"
|
||||
@click="emit('next')">下一篇
|
||||
</BaseButton>
|
||||
</div>
|
||||
|
||||
@@ -881,7 +923,7 @@ $article-lh: 2.4;
|
||||
width: 100vw;
|
||||
max-width: 100%;
|
||||
padding: 1rem 0.5rem;
|
||||
|
||||
|
||||
// 标题优化
|
||||
header {
|
||||
.title {
|
||||
@@ -889,31 +931,31 @@ $article-lh: 2.4;
|
||||
line-height: 1.4;
|
||||
word-break: break-word;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
|
||||
.font-family {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.titleTranslate {
|
||||
font-size: 0.9rem;
|
||||
margin-top: 0.5rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 句子显示优化
|
||||
.article-content {
|
||||
article {
|
||||
.section {
|
||||
margin-bottom: 1rem;
|
||||
|
||||
|
||||
.sentence {
|
||||
font-size: 1rem;
|
||||
line-height: 1.6;
|
||||
word-break: break-word;
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
|
||||
.word {
|
||||
.word-wrap {
|
||||
padding: 0.1rem 0.05rem;
|
||||
@@ -936,16 +978,16 @@ $article-lh: 2.4;
|
||||
font-family: var(--zh-article-family);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
|
||||
// 翻译区域优化
|
||||
.translate {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
// 问答表单优化
|
||||
.question-form {
|
||||
padding: 0.5rem;
|
||||
|
||||
|
||||
.base-button {
|
||||
width: 100%;
|
||||
min-height: 48px;
|
||||
@@ -958,21 +1000,21 @@ $article-lh: 2.4;
|
||||
@media (max-width: 480px) {
|
||||
.typing-article {
|
||||
padding: 0.5rem 0.3rem;
|
||||
|
||||
|
||||
header {
|
||||
.title {
|
||||
font-size: 1rem;
|
||||
|
||||
|
||||
.font-family {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.titleTranslate {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.article-content {
|
||||
article {
|
||||
.section {
|
||||
@@ -983,7 +1025,7 @@ $article-lh: 2.4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.sentence-translate-mobile {
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.35;
|
||||
|
||||
@@ -568,7 +568,7 @@ function importOldData() {
|
||||
</SettingItem>
|
||||
|
||||
<div class="line"></div>
|
||||
<SettingItem title="输入时忽略符号/数字">
|
||||
<SettingItem title="输入时忽略符号/数字/人名">
|
||||
<Switch v-model="settingStore.ignoreSymbol"/>
|
||||
</SettingItem>
|
||||
</div>
|
||||
@@ -631,6 +631,22 @@ function importOldData() {
|
||||
</div>
|
||||
|
||||
<div v-if="tabIndex === 5">
|
||||
<div class="log-item">
|
||||
<div class="mb-2">
|
||||
<div>
|
||||
<div>日期:2025/11/25</div>
|
||||
<div>内容:文章练习新增人名忽略功能(新概念一已全部适配),上传了新概念(一)1-18 音频</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="log-item">
|
||||
<div class="mb-2">
|
||||
<div>
|
||||
<div>日期:2025/11/23</div>
|
||||
<div>内容:优化练习完成结算界面,新增分享功能</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="log-item">
|
||||
<div class="mb-2">
|
||||
<div>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script setup lang="tsx">
|
||||
import { DictId } from "@/types/types.ts";
|
||||
import {DictId} from "@/types/types.ts";
|
||||
|
||||
import BasePage from "@/components/BasePage.vue";
|
||||
import { computed, onMounted, reactive, ref, shallowReactive } from "vue";
|
||||
import { useRuntimeStore } from "@/stores/runtime.ts";
|
||||
import { _getDictDataByUrl, _nextTick, convertToWord, isMobile, loadJsLib, useNav } from "@/utils";
|
||||
import { nanoid } from "nanoid";
|
||||
import {computed, onMounted, reactive, ref, shallowReactive} from "vue";
|
||||
import {useRuntimeStore} from "@/stores/runtime.ts";
|
||||
import {_getDictDataByUrl, _nextTick, convertToWord, isMobile, loadJsLib, useNav} from "@/utils";
|
||||
import {nanoid} from "nanoid";
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
import BaseTable from "@/components/BaseTable.vue";
|
||||
import WordItem from "@/components/WordItem.vue";
|
||||
@@ -13,21 +13,21 @@ import Toast from '@/components/base/toast/Toast.ts'
|
||||
import PopConfirm from "@/components/PopConfirm.vue";
|
||||
import BackIcon from "@/components/BackIcon.vue";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { useBaseStore } from "@/stores/base.ts";
|
||||
import {useRoute, useRouter} from "vue-router";
|
||||
import {useBaseStore} from "@/stores/base.ts";
|
||||
import EditBook from "@/pages/article/components/EditBook.vue";
|
||||
import { getDefaultDict } from "@/types/func.ts";
|
||||
import {getDefaultDict} from "@/types/func.ts";
|
||||
import BaseInput from "@/components/base/BaseInput.vue";
|
||||
import Textarea from "@/components/base/Textarea.vue";
|
||||
import FormItem from "@/components/base/form/FormItem.vue";
|
||||
import Form from "@/components/base/form/Form.vue";
|
||||
import DeleteIcon from "@/components/icon/DeleteIcon.vue";
|
||||
import { getCurrentStudyWord } from "@/hooks/dict.ts";
|
||||
import {getCurrentStudyWord} from "@/hooks/dict.ts";
|
||||
import PracticeSettingDialog from "@/pages/word/components/PracticeSettingDialog.vue";
|
||||
import { useSettingStore } from "@/stores/setting.ts";
|
||||
import { MessageBox } from "@/utils/MessageBox.tsx";
|
||||
import { AppEnv, Origin, PracticeSaveWordKey } from "@/config/env.ts";
|
||||
import { detail } from "@/apis";
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
import {MessageBox} from "@/utils/MessageBox.tsx";
|
||||
import {AppEnv, Origin, PracticeSaveWordKey} from "@/config/env.ts";
|
||||
import {detail} from "@/apis";
|
||||
|
||||
const runtimeStore = useRuntimeStore()
|
||||
const base = useBaseStore()
|
||||
@@ -151,7 +151,7 @@ function word2Str(word) {
|
||||
res.phrases = word.phrases.map(v => (v.c + "\n" + v.cn).replaceAll('"', '')).join('\n\n')
|
||||
res.synos = word.synos.map(v => (v.pos + v.cn + "\n" + v.ws.join('/')).replaceAll('"', '')).join('\n\n')
|
||||
res.relWords = word.relWords.root ? ('词根:' + word.relWords.root + '\n\n' +
|
||||
word.relWords.rels.map(v => (v.pos + "\n" + v.words.map(v => (v.c + ':' + v.cn)).join('\n')).replaceAll('"', '')).join('\n\n')) : ''
|
||||
word.relWords.rels.map(v => (v.pos + "\n" + v.words.map(v => (v.c + ':' + v.cn)).join('\n')).replaceAll('"', '')).join('\n\n')) : ''
|
||||
res.etymology = word.etymology.map(v => (v.t + '\n' + v.d).replaceAll('"', '')).join('\n\n')
|
||||
return res
|
||||
}
|
||||
@@ -192,8 +192,8 @@ onMounted(async () => {
|
||||
router.push("/word")
|
||||
} else {
|
||||
if (!runtimeStore.editDict.words.length
|
||||
&& !runtimeStore.editDict.custom
|
||||
&& ![DictId.wordCollect, DictId.wordWrong, DictId.wordKnown].includes(runtimeStore.editDict.en_name || runtimeStore.editDict.id)
|
||||
&& !runtimeStore.editDict.custom
|
||||
&& ![DictId.wordCollect, DictId.wordWrong, DictId.wordKnown].includes(runtimeStore.editDict.en_name || runtimeStore.editDict.id)
|
||||
) {
|
||||
loading = true
|
||||
let r = await _getDictDataByUrl(runtimeStore.editDict)
|
||||
@@ -242,7 +242,7 @@ async function startPractice() {
|
||||
wordPracticeMode: settingStore.wordPracticeMode
|
||||
})
|
||||
let currentStudy = getCurrentStudyWord()
|
||||
nav('practice-words/' + store.sdict.id, {}, {taskWords:currentStudy})
|
||||
nav('practice-words/' + store.sdict.id, {}, {taskWords: currentStudy})
|
||||
}
|
||||
|
||||
async function addMyStudyList() {
|
||||
@@ -311,22 +311,22 @@ function importData(e) {
|
||||
|
||||
if (repeat.length) {
|
||||
MessageBox.confirm(
|
||||
'单词"' + repeat.map(v => v.word).join(', ') + '" 已存在,是否覆盖原单词?',
|
||||
'检测到重复单词',
|
||||
() => {
|
||||
repeat.map(v => {
|
||||
runtimeStore.editDict.words[v.index] = v
|
||||
delete runtimeStore.editDict.words[v.index]["index"]
|
||||
})
|
||||
},
|
||||
null,
|
||||
() => {
|
||||
tableRef.value.closeImportDialog()
|
||||
e.target.value = ''
|
||||
importLoading = false
|
||||
syncDictInMyStudyList()
|
||||
Toast.success('导入成功!')
|
||||
}
|
||||
'单词"' + repeat.map(v => v.word).join(', ') + '" 已存在,是否覆盖原单词?',
|
||||
'检测到重复单词',
|
||||
() => {
|
||||
repeat.map(v => {
|
||||
runtimeStore.editDict.words[v.index] = v
|
||||
delete runtimeStore.editDict.words[v.index]["index"]
|
||||
})
|
||||
},
|
||||
null,
|
||||
() => {
|
||||
tableRef.value.closeImportDialog()
|
||||
e.target.value = ''
|
||||
importLoading = false
|
||||
syncDictInMyStudyList()
|
||||
Toast.success('导入成功!')
|
||||
}
|
||||
)
|
||||
} else {
|
||||
tableRef.value.closeImportDialog()
|
||||
@@ -378,212 +378,213 @@ function searchWord() {
|
||||
|
||||
defineRender(() => {
|
||||
return (
|
||||
<BasePage>
|
||||
{
|
||||
showBookDetail.value ? <div className="card mb-0 dict-detail-card flex flex-col">
|
||||
<div class="dict-header flex justify-between items-center relative">
|
||||
<BackIcon class="dict-back z-2"/>
|
||||
<div class="dict-title absolute page-title text-align-center w-full">{runtimeStore.editDict.name}</div>
|
||||
<div class="dict-actions flex gap-2">
|
||||
<BaseButton loading={studyLoading || loading} type="info"
|
||||
onClick={() => isEdit = true}>编辑</BaseButton>
|
||||
<BaseButton loading={studyLoading || loading} onClick={addMyStudyList}>学习</BaseButton>
|
||||
</div>
|
||||
<BasePage>
|
||||
{
|
||||
showBookDetail.value ? <div className="card mb-0 dict-detail-card flex flex-col">
|
||||
<div class="dict-header flex justify-between items-center relative">
|
||||
<BackIcon class="dict-back z-2"/>
|
||||
<div class="dict-title absolute page-title text-align-center w-full">{runtimeStore.editDict.name}</div>
|
||||
<div class="dict-actions flex gap-2">
|
||||
<BaseButton loading={studyLoading || loading} type="info"
|
||||
onClick={() => isEdit = true}>编辑</BaseButton>
|
||||
<BaseButton loading={studyLoading || loading} onClick={addMyStudyList}>学习</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-lg ">介绍:{runtimeStore.editDict.description}</div>
|
||||
<div class="line my-3"></div>
|
||||
|
||||
{/* 移动端标签页导航 */}
|
||||
{isMob && isOperate && (
|
||||
<div class="tab-navigation mb-3">
|
||||
<div
|
||||
class={`tab-item ${activeTab === 'list' ? 'active' : ''}`}
|
||||
onClick={() => activeTab = 'list'}
|
||||
>
|
||||
单词列表
|
||||
</div>
|
||||
<div class="text-lg ">介绍:{runtimeStore.editDict.description}</div>
|
||||
<div class="line my-3"></div>
|
||||
|
||||
{/* 移动端标签页导航 */}
|
||||
{isMob && isOperate && (
|
||||
<div class="tab-navigation mb-3">
|
||||
<div
|
||||
class={`tab-item ${activeTab === 'list' ? 'active' : ''}`}
|
||||
onClick={() => activeTab = 'list'}
|
||||
>
|
||||
单词列表
|
||||
</div>
|
||||
<div
|
||||
class={`tab-item ${activeTab === 'edit' ? 'active' : ''}`}
|
||||
onClick={() => activeTab = 'edit'}
|
||||
>
|
||||
{wordForm.id ? '编辑' : '添加'}单词
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div class="flex flex-1 overflow-hidden content-area">
|
||||
<div class={`word-list-section ${isMob && isOperate && activeTab !== 'list' ? 'mobile-hidden' : ''}`}>
|
||||
<BaseTable
|
||||
ref={tableRef}
|
||||
class="h-full"
|
||||
list={list}
|
||||
loading={loading}
|
||||
onUpdate:list={e => list = e}
|
||||
del={delWord}
|
||||
batchDel={batchDel}
|
||||
add={addWord}
|
||||
onImportData={importData}
|
||||
onExportData={exportData}
|
||||
exportLoading={exportLoading}
|
||||
importLoading={importLoading}
|
||||
>
|
||||
{
|
||||
(val) =>
|
||||
<WordItem
|
||||
showTransPop={false}
|
||||
item={val.item}>
|
||||
{{
|
||||
prefix: () => val.checkbox(val.item),
|
||||
suffix: () => (
|
||||
<div class='flex flex-col'>
|
||||
<BaseIcon
|
||||
class="option-icon"
|
||||
onClick={() => editWord(val.item)}
|
||||
title="编辑">
|
||||
<IconFluentTextEditStyle20Regular/>
|
||||
</BaseIcon>
|
||||
<PopConfirm title="确认删除?"
|
||||
onConfirm={() => delWord(val.item.id)}
|
||||
>
|
||||
<BaseIcon
|
||||
class="option-icon"
|
||||
title="删除">
|
||||
<DeleteIcon/>
|
||||
</BaseIcon>
|
||||
</PopConfirm>
|
||||
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</WordItem>
|
||||
}
|
||||
</BaseTable>
|
||||
</div>
|
||||
{
|
||||
isOperate ? (
|
||||
<div class={`edit-section flex-1 flex flex-col ${isMob && activeTab !== 'edit' ? 'mobile-hidden' : ''}`}>
|
||||
<div class="common-title">
|
||||
{wordForm.id ? '修改' : '添加'}单词
|
||||
</div>
|
||||
<Form
|
||||
class="flex-1 overflow-auto pr-2"
|
||||
ref={e => wordFormRef = e}
|
||||
rules={wordRules}
|
||||
model={wordForm}
|
||||
label-width="7rem">
|
||||
<FormItem label="单词" prop="word">
|
||||
<BaseInput
|
||||
modelValue={wordForm.word}
|
||||
onUpdate:modelValue={e => wordForm.word = e}
|
||||
>
|
||||
|
||||
</BaseInput>
|
||||
</FormItem>
|
||||
<FormItem label="英音音标">
|
||||
<BaseInput
|
||||
modelValue={wordForm.phonetic0}
|
||||
onUpdate:modelValue={e => wordForm.phonetic0 = e}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="美音音标">
|
||||
<BaseInput
|
||||
modelValue={wordForm.phonetic1}
|
||||
onUpdate:modelValue={e => wordForm.phonetic1 = e}/>
|
||||
</FormItem>
|
||||
<FormItem label="翻译">
|
||||
<Textarea
|
||||
modelValue={wordForm.trans}
|
||||
onUpdate:modelValue={e => wordForm.trans = e}
|
||||
placeholder="一行一个翻译,前面词性,后面内容(如n.取消);多个翻译请换行"
|
||||
autosize={{minRows: 6, maxRows: 10}}/>
|
||||
</FormItem>
|
||||
<FormItem label="例句">
|
||||
<Textarea
|
||||
modelValue={wordForm.sentences}
|
||||
onUpdate:modelValue={e => wordForm.sentences = e}
|
||||
placeholder="一行原文,一行译文;多个请换两行"
|
||||
autosize={{minRows: 6, maxRows: 10}}/>
|
||||
</FormItem>
|
||||
<FormItem label="短语">
|
||||
<Textarea
|
||||
modelValue={wordForm.phrases}
|
||||
onUpdate:modelValue={e => wordForm.phrases = e}
|
||||
placeholder="一行原文,一行译文;多个请换两行"
|
||||
autosize={{minRows: 6, maxRows: 10}}/>
|
||||
</FormItem>
|
||||
<FormItem label="同义词">
|
||||
<Textarea
|
||||
modelValue={wordForm.synos}
|
||||
onUpdate:modelValue={e => wordForm.synos = e}
|
||||
placeholder="请参考已有单词格式"
|
||||
autosize={{minRows: 6, maxRows: 20}}/>
|
||||
</FormItem>
|
||||
<FormItem label="同根词">
|
||||
<Textarea
|
||||
modelValue={wordForm.relWords}
|
||||
onUpdate:modelValue={e => wordForm.relWords = e}
|
||||
placeholder="请参考已有单词格式"
|
||||
autosize={{minRows: 6, maxRows: 20}}/>
|
||||
</FormItem>
|
||||
<FormItem label="词源">
|
||||
<Textarea
|
||||
modelValue={wordForm.etymology}
|
||||
onUpdate:modelValue={e => wordForm.etymology = e}
|
||||
placeholder="请参考已有单词格式"
|
||||
autosize={{minRows: 6, maxRows: 10}}/>
|
||||
</FormItem>
|
||||
</Form>
|
||||
<div class="center">
|
||||
<BaseButton
|
||||
type="info"
|
||||
onClick={closeWordForm}>关闭
|
||||
</BaseButton>
|
||||
<BaseButton type="primary"
|
||||
onClick={onSubmitWord}>保存
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
</div>
|
||||
</div> :
|
||||
<div class="card mb-0 dict-detail-card">
|
||||
<div class="dict-header flex justify-between items-center relative">
|
||||
<BackIcon class="dict-back z-2" onClick={() => {
|
||||
if (isAdd) {
|
||||
router.back()
|
||||
} else {
|
||||
isEdit = false
|
||||
}
|
||||
}}/>
|
||||
<div class="dict-title absolute page-title text-align-center w-full">
|
||||
{runtimeStore.editDict.id ? '修改' : '创建'}词典
|
||||
</div>
|
||||
</div>
|
||||
<div class="center">
|
||||
<EditBook
|
||||
isAdd={isAdd}
|
||||
isBook={false}
|
||||
onClose={formClose}
|
||||
onSubmit={() => isEdit = isAdd = false}
|
||||
/>
|
||||
<div
|
||||
class={`tab-item ${activeTab === 'edit' ? 'active' : ''}`}
|
||||
onClick={() => activeTab = 'edit'}
|
||||
>
|
||||
{wordForm.id ? '编辑' : '添加'}单词
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
|
||||
<PracticeSettingDialog
|
||||
showLeftOption
|
||||
modelValue={showPracticeSettingDialog}
|
||||
onUpdate:modelValue={val => (showPracticeSettingDialog = val)}
|
||||
onOk={startPractice}/>
|
||||
</BasePage>
|
||||
<div class="flex flex-1 overflow-hidden content-area">
|
||||
<div class={`word-list-section ${isMob && isOperate && activeTab !== 'list' ? 'mobile-hidden' : ''}`}>
|
||||
<BaseTable
|
||||
ref={tableRef}
|
||||
class="h-full"
|
||||
list={list}
|
||||
loading={loading}
|
||||
onUpdate:list={e => list = e}
|
||||
del={delWord}
|
||||
batchDel={batchDel}
|
||||
add={addWord}
|
||||
onImportData={importData}
|
||||
onExportData={exportData}
|
||||
exportLoading={exportLoading}
|
||||
importLoading={importLoading}
|
||||
>
|
||||
{
|
||||
(val) =>
|
||||
<WordItem
|
||||
showTransPop={false}
|
||||
item={val.item}>
|
||||
{{
|
||||
prefix: () => val.checkbox(val.item),
|
||||
suffix: () => (
|
||||
<div class='flex flex-col'>
|
||||
<BaseIcon
|
||||
class="option-icon"
|
||||
onClick={() => editWord(val.item)}
|
||||
title="编辑">
|
||||
<IconFluentTextEditStyle20Regular/>
|
||||
</BaseIcon>
|
||||
<PopConfirm title="确认删除?"
|
||||
onConfirm={() => delWord(val.item.id)}
|
||||
>
|
||||
<BaseIcon
|
||||
class="option-icon"
|
||||
title="删除">
|
||||
<DeleteIcon/>
|
||||
</BaseIcon>
|
||||
</PopConfirm>
|
||||
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</WordItem>
|
||||
}
|
||||
</BaseTable>
|
||||
</div>
|
||||
{
|
||||
isOperate ? (
|
||||
<div
|
||||
class={`edit-section flex-1 flex flex-col ${isMob && activeTab !== 'edit' ? 'mobile-hidden' : ''}`}>
|
||||
<div class="common-title">
|
||||
{wordForm.id ? '修改' : '添加'}单词
|
||||
</div>
|
||||
<Form
|
||||
class="flex-1 overflow-auto pr-2"
|
||||
ref={e => wordFormRef = e}
|
||||
rules={wordRules}
|
||||
model={wordForm}
|
||||
label-width="7rem">
|
||||
<FormItem label="单词" prop="word">
|
||||
<BaseInput
|
||||
modelValue={wordForm.word}
|
||||
onUpdate:modelValue={e => wordForm.word = e}
|
||||
>
|
||||
|
||||
</BaseInput>
|
||||
</FormItem>
|
||||
<FormItem label="英音音标">
|
||||
<BaseInput
|
||||
modelValue={wordForm.phonetic0}
|
||||
onUpdate:modelValue={e => wordForm.phonetic0 = e}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="美音音标">
|
||||
<BaseInput
|
||||
modelValue={wordForm.phonetic1}
|
||||
onUpdate:modelValue={e => wordForm.phonetic1 = e}/>
|
||||
</FormItem>
|
||||
<FormItem label="翻译">
|
||||
<Textarea
|
||||
modelValue={wordForm.trans}
|
||||
onUpdate:modelValue={e => wordForm.trans = e}
|
||||
placeholder="一行一个翻译,前面词性,后面内容(如n.取消);多个翻译请换行"
|
||||
autosize={{minRows: 6, maxRows: 10}}/>
|
||||
</FormItem>
|
||||
<FormItem label="例句">
|
||||
<Textarea
|
||||
modelValue={wordForm.sentences}
|
||||
onUpdate:modelValue={e => wordForm.sentences = e}
|
||||
placeholder="一行原文,一行译文;多个请换两行"
|
||||
autosize={{minRows: 6, maxRows: 10}}/>
|
||||
</FormItem>
|
||||
<FormItem label="短语">
|
||||
<Textarea
|
||||
modelValue={wordForm.phrases}
|
||||
onUpdate:modelValue={e => wordForm.phrases = e}
|
||||
placeholder="一行原文,一行译文;多个请换两行"
|
||||
autosize={{minRows: 6, maxRows: 10}}/>
|
||||
</FormItem>
|
||||
<FormItem label="同义词">
|
||||
<Textarea
|
||||
modelValue={wordForm.synos}
|
||||
onUpdate:modelValue={e => wordForm.synos = e}
|
||||
placeholder="请参考已有单词格式"
|
||||
autosize={{minRows: 6, maxRows: 20}}/>
|
||||
</FormItem>
|
||||
<FormItem label="同根词">
|
||||
<Textarea
|
||||
modelValue={wordForm.relWords}
|
||||
onUpdate:modelValue={e => wordForm.relWords = e}
|
||||
placeholder="请参考已有单词格式"
|
||||
autosize={{minRows: 6, maxRows: 20}}/>
|
||||
</FormItem>
|
||||
<FormItem label="词源">
|
||||
<Textarea
|
||||
modelValue={wordForm.etymology}
|
||||
onUpdate:modelValue={e => wordForm.etymology = e}
|
||||
placeholder="请参考已有单词格式"
|
||||
autosize={{minRows: 6, maxRows: 10}}/>
|
||||
</FormItem>
|
||||
</Form>
|
||||
<div class="center">
|
||||
<BaseButton
|
||||
type="info"
|
||||
onClick={closeWordForm}>关闭
|
||||
</BaseButton>
|
||||
<BaseButton type="primary"
|
||||
onClick={onSubmitWord}>保存
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
</div>
|
||||
</div> :
|
||||
<div class="card mb-0 dict-detail-card">
|
||||
<div class="dict-header flex justify-between items-center relative">
|
||||
<BackIcon class="dict-back z-2" onClick={() => {
|
||||
if (isAdd) {
|
||||
router.back()
|
||||
} else {
|
||||
isEdit = false
|
||||
}
|
||||
}}/>
|
||||
<div class="dict-title absolute page-title text-align-center w-full">
|
||||
{runtimeStore.editDict.id ? '修改' : '创建'}词典
|
||||
</div>
|
||||
</div>
|
||||
<div class="center">
|
||||
<EditBook
|
||||
isAdd={isAdd}
|
||||
isBook={false}
|
||||
onClose={formClose}
|
||||
onSubmit={() => isEdit = isAdd = false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<PracticeSettingDialog
|
||||
showLeftOption
|
||||
modelValue={showPracticeSettingDialog}
|
||||
onUpdate:modelValue={val => (showPracticeSettingDialog = val)}
|
||||
onOk={startPractice}/>
|
||||
</BasePage>
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.dict-detail-card {
|
||||
min-height: calc(100vh - 3rem);
|
||||
height: calc(100vh - 3rem);
|
||||
}
|
||||
|
||||
.dict-header {
|
||||
@@ -613,6 +614,7 @@ defineRender(() => {
|
||||
// 移动端适配
|
||||
@media (max-width: 768px) {
|
||||
.dict-detail-card {
|
||||
height: unset;
|
||||
min-height: calc(100vh - 2rem);
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
@@ -690,6 +692,7 @@ defineRender(() => {
|
||||
// 超小屏幕适配
|
||||
@media (max-width: 480px) {
|
||||
.dict-detail-card {
|
||||
height: unset;
|
||||
min-height: calc(100vh - 1rem);
|
||||
}
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -652,14 +652,10 @@ useEvents([
|
||||
@apply text-lg w-12;
|
||||
}
|
||||
}
|
||||
|
||||
// 隐藏光标
|
||||
.cursor {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
// 移动端适配
|
||||
@media (max-width: 768px) {
|
||||
|
||||
.typing-word {
|
||||
padding: 0 0.5rem 12rem;
|
||||
|
||||
|
||||
@@ -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