Merge branch 'dev'

This commit is contained in:
Zyronon
2025-12-10 00:53:38 +08:00
20 changed files with 316 additions and 527 deletions

View File

@@ -1,71 +0,0 @@
## 目标
-`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` 中实现辅助函数与两处调用点的改动;
- 仅修改该文件,不影响其他页面。

View File

@@ -1,69 +0,0 @@
## 目标
- 在“人物名称管理”弹框中使用临时变量编辑名称列表,只在点击“确定”时写回 `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[]` 以与当前实现一致

View File

@@ -1,109 +0,0 @@
## 目标与范围
* 适配 `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`
* 修改完成后提供预览链接与截图确认

1
components.d.ts vendored
View File

@@ -99,6 +99,7 @@ declare module 'vue' {
IconFluentSlideTextTitleEdit20Regular: typeof import('~icons/fluent/slide-text-title-edit20-regular')['default']
IconFluentSparkle20Regular: typeof import('~icons/fluent/sparkle20-regular')['default']
IconFluentSpeakerEdit20Regular: typeof import('~icons/fluent/speaker-edit20-regular')['default']
IconFluentSpeakerSettings20Regular: typeof import('~icons/fluent/speaker-settings20-regular')['default']
IconFluentStar16Filled: typeof import('~icons/fluent/star16-filled')['default']
IconFluentStar16Regular: typeof import('~icons/fluent/star16-regular')['default']
IconFluentStar20Filled: typeof import('~icons/fluent/star20-filled')['default']

View File

@@ -2,7 +2,7 @@ version: "2"
services:
typeword:
image: "node:latest"
#environment: #按需配置,主要为了科学上网解决依赖安装网络问题
#environment: #按需配置, 主要为了科学上网解决依赖安装网络问题
# - HTTP_PROXY=http://127.0.0.1:80
# HTTPS_PROXY=http://127.0.0.1:80
working_dir: /home/node/app

View File

@@ -465,7 +465,7 @@
"id": "wr-2ZE",
"title": "The weekend",
"titleTranslate": "周末",
"text": "MRS. JOHNSON:Hello. Where you at the butcher's?\n\nMRS. WILLIAMS:Yes. I was.\n\nWere you at butcher's too?\n\nMRS. JOHNSON:No, I wasn't\n\nI was at the greengrocer's.\n\nHow's Jimmy today?\n\nMRS. WILLIAMS:He's very well, thank you.\n\nMRS. JOHNSON:Was he absent from school last week?\n\nMRS. WILLIAMS:Yes, he was.\n\nHe was absent on Monday,Tuesday,Wednesday and Tuesday.\n\nHow are you all keeping?\n\nMRS. JOHNSON:Very well, thank you.\n\nWe're going to spend three days in the country.\n\nWe're going to stay at my mother's for the weekend.\n\nMRS. WILLIAMS:Friday, Saturday and Sunday in the country!\n\nAren't you lucky!",
"text": "MRS. JOHNSON:Hello. Where you at the butcher's?\n\nMRS. WILLIAMS:Yes. I was.\n\nWere you at butcher's too?\n\nMRS. JOHNSON:No, I wasn't\n\nI was at the greengrocer's.\n\nHow's Jimmy today?\n\nMRS. WILLIAMS:He's very well, thank you.\n\nMRS. JOHNSON:Was he absent from school last week?\n\nMRS. WILLIAMS:Yes, he was.\n\nHe was absent on Monday,Tuesday,Wednesday and Thursday.\n\nHow are you all keeping?\n\nMRS. JOHNSON:Very well, thank you.\n\nWe're going to spend three days in the country.\n\nWe're going to stay at my mother's for the weekend.\n\nMRS. WILLIAMS:Friday, Saturday and Sunday in the country!\n\nAren't you lucky!",
"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你们真幸运啊",
"newWords": [],
"audioSrc": "/sound/article/nce1/067&068The Weekend.mp3",

View File

@@ -94,5 +94,7 @@ export const LIB_JS_URL = {
SHEPHERD: import.meta.env.MODE === 'development' ?
'https://cdn.jsdelivr.net/npm/shepherd.js@14.5.1/dist/esm/shepherd.mjs'
: Origin + '/libs/Shepherd.14.5.1.mjs',
SNAPDOM: `${Origin}/libs/snapdom.min.js`
SNAPDOM: `${Origin}/libs/snapdom.min.js`,
JSZIP: `${Origin}/libs/jszip.min.js`,
XLSX: `${Origin}/libs/xlsx.full.min.js`,
}

View File

@@ -1,8 +1,8 @@
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 { isMobile } from "@/utils";
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 {isMobile} from "@/utils";
export function useWindowClick(cb: (e: PointerEvent) => void) {
onMounted(() => {
@@ -92,9 +92,12 @@ export function useEventListener(type: string, listener: EventListenerOrEventLis
repeat: false,
isComposing: false,
type,
preventDefault() {},
stopPropagation() {},
stopImmediatePropagation() {},
preventDefault() {
},
stopPropagation() {
},
stopImmediatePropagation() {
},
}
return base as unknown as KeyboardEvent
}
@@ -131,7 +134,7 @@ export function useEventListener(type: string, listener: EventListenerOrEventLis
const value = target?.value ?? ''
if (event.inputType === 'deleteContentBackward') {
dispatchSyntheticKey({ key: 'Backspace', code: 'Backspace', keyCode: 8 })
dispatchSyntheticKey({key: 'Backspace', code: 'Backspace', keyCode: 8})
if (target) target.value = ''
return
}
@@ -252,6 +255,8 @@ export function useStartKeyboardEventListener() {
const settingStore = useSettingStore()
useEventListener('keydown', (e: KeyboardEvent) => {
//解决无法复制、全选的问题
if ((e.ctrlKey || e.metaKey) && ['KeyC', 'KeyA'].includes(e.code)) return
if (!runtimeStore.disableEventListener) {
// 检查当前单词是否包含空格,如果包含,则空格键应该被视为输入

View File

@@ -2,7 +2,7 @@ import {loadJsLib, shakeCommonDict} from "@/utils";
import {
APP_NAME,
APP_VERSION,
EXPORT_DATA_KEY,
EXPORT_DATA_KEY, LIB_JS_URL,
LOCAL_FILE_KEY,
Origin,
PracticeSaveArticleKey,
@@ -28,7 +28,7 @@ export function useExport() {
if (loading.value) return
loading.value = true
try {
const JSZip = await loadJsLib('JSZip', `${Origin}/libs/jszip.min.js`);
const JSZip = await loadJsLib('JSZip', LIB_JS_URL.JSZIP);
let data = {
version: EXPORT_DATA_KEY.version,
val: {

View File

@@ -280,7 +280,7 @@ let isNewHost = $ref(window.location.host === Host)
</div>
<BaseButton size="large" class="w-full md:w-auto"
@click="startStudy"
:disabled="!base.currentBook.name">
:disabled="!base.sbook.name">
<div class="flex items-center gap-2 justify-center w-full">
<span class="line-height-[2]">{{ isSaveData ? '继续学习' : '开始学习' }}</span>
<IconFluentArrowCircleRight16Regular class="text-xl"/>

View File

@@ -15,7 +15,7 @@ import {getDefaultArticle} from "@/types/func.ts";
import BackIcon from "@/components/BackIcon.vue";
import MiniDialog from "@/components/dialog/MiniDialog.vue";
import {onMounted} from "vue";
import {Origin} from "@/config/env.ts";
import { LIB_JS_URL, Origin } from "@/config/env.ts";
import {syncBookInMyStudyList} from "@/hooks/article.ts";
const base = useBaseStore()
@@ -132,7 +132,7 @@ function importData(e: any) {
let reader = new FileReader();
reader.onload = async function (s) {
importLoading = true
const XLSX = await loadJsLib('XLSX', `${Origin}/libs/xlsx.full.min.js`);
const XLSX = await loadJsLib('XLSX', LIB_JS_URL.XLSX);
let data = s.target.result;
let workbook = XLSX.read(data, {type: 'binary'});
let res: any[] = XLSX.utils.sheet_to_json(workbook.Sheets['Sheet1']);
@@ -198,7 +198,7 @@ function importData(e: any) {
async function exportData(val: { type: string, data?: Article }) {
exportLoading = true
const XLSX = await loadJsLib('XLSX', `${Origin}/libs/xlsx.full.min.js`);
const XLSX = await loadJsLib('XLSX', LIB_JS_URL.XLSX);
const {type, data} = val
let list = []
let filename = ''

View File

@@ -739,7 +739,7 @@ const currentPractice = inject('currentPractice', [])
@click="emit('replay')">重新练习
</BaseButton>
<BaseButton
v-if="store.currentBook.lastLearnIndex < store.currentBook.articles.length - 1"
v-if="store.sbook.lastLearnIndex < store.sbook.articles.length - 1"
@click="emit('next')">下一篇
</BaseButton>
</div>

237
src/pages/setting/Log.vue Normal file
View File

@@ -0,0 +1,237 @@
<script setup lang="ts">
</script>
<template>
<div>
<div class="log-item">
<div class="mb-2">
<div>
<div>日期2025/12/5</div>
<div>内容解决练习界面无法复制全选的问题</div>
</div>
</div>
</div>
<div class="log-item">
<div class="mb-2">
<div>
<div>日期2025/12/3</div>
<div>内容单词文章设置修改为弹框更方便</div>
</div>
</div>
</div>
<div class="log-item">
<div class="mb-2">
<div>
<div>日期2025/12/3</div>
<div>内容录入新概念部分音频优化文章相关功能</div>
</div>
</div>
</div>
<div class="log-item">
<div class="mb-2">
<div>
<div>日期2025/12/2</div>
<div>内容完成新概念音频优化文章管理页面</div>
</div>
</div>
</div>
<div class="log-item">
<div class="mb-2">
<div>
<div>日期2025/11/30</div>
<div>内容文章里的单词可点击播放</div>
</div>
</div>
</div>
<div class="log-item">
<div class="mb-2">
<div>
<div>日期2025/11/29</div>
<div>内容修改 Slider 组件显示bug新增 IE 浏览器检测提示</div>
</div>
</div>
</div>
<div class="log-item">
<div class="mb-2">
<div>
<div>日期2025/11/28</div>
<div>内容新增引导框 新增<a href="https://github.com/zyronon/TypeWords/pull/175" target="_blank">词典测试模式由大佬
hebeihang 开发</a></div>
</div>
</div>
</div>
<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>
<div>日期2025/11/22</div>
<div>内容适配移动端</div>
</div>
</div>
</div>
<div class="log-item">
<div class="mb-2">
<div>
<div>日期2025/11/16</div>
<div>内容自测单词时不认识单词可以直接输入自动标识为错误单词无需按2</div>
</div>
</div>
</div>
<div class="log-item">
<div class="mb-2">
<div>
<div>日期2025/11/15</div>
<div>内容练习单词时底部工具栏新增跳到下一阶段按钮</div>
</div>
</div>
</div>
<div class="log-item">
<div class="mb-2">
<div>
<div>日期2025/11/14</div>
<div>内容新增文章练习时可跳过空格如果在单词的最后一位上不按空格直接输入下一个字母的话自动跳下一个单词
按空格也自动跳下一个单词
</div>
</div>
</div>
</div>
<div class="log-item">
<div class="mb-2">
<div>
<div>日期2025/11/13</div>
<div>内容新增文章练习时输入时忽略符号/数字选项</div>
</div>
</div>
</div>
<div class="log-item">
<div class="mb-2">
<div>
<div>日期2025/11/6</div>
<div>内容新增随机复习功能</div>
</div>
</div>
</div>
<div class="log-item">
<div class="mb-2">
<div>
<div>日期2025/10/30</div>
<div>内容集成PWA基础配置支持用户以类App形式打开项目</div>
</div>
</div>
</div>
<div class="log-item">
<div class="mb-2">
<div>
<div>日期2025/10/26</div>
<div>内容进一步完善单词练习解决复习数量太多的问题</div>
</div>
<div class="text-base mt-1">
<ol>
<li>
<div class="title"><b>智能模式优化</b></div>
<div class="desc">练习时新增四种练习模式学习自测听写默写</div>
</li>
<li>
<div class="title"><b>学习模式</b></div>
<div class="desc">
<ul>
<li>仅在练习新词时出现</li>
<li>采用跟写 / 拼写方式进行学习</li>
<li> 7 个单词会 <b>强制进行听写</b>解决原来一次练太多听写时已忘记的问题</li>
</ul>
</div>
</li>
<li>
<div class="title"><b>自测模式新增</b></div>
<div class="desc">
<ul>
<li>仅在复习已学单词时出现</li>
<li>不再强制拼写提供我认识不认识选项</li>
<li>选择我认识该单词在后续听写或默写中将不再出现<b>显著减少复习数量</b></li>
</ul>
</div>
</li>
<li>
<div class="title"><b>听写模式</b></div>
<div class="desc">原有逻辑保持不变</div>
</li>
<li>
<div class="title"><b>默写模式新增</b></div>
<div class="desc">
<ul>
<li>仅显示释义不自动发音不显示单词长度</li>
<li>适合强化拼写记忆的场景</li>
</ul>
</div>
</li>
</ol>
<b>说明</b>
<div>本次更新重点解决了复习单词数量过多效率偏低的问题</div>
<div>通过引入复习默写两种模式使复习流程更加灵活高效</div>
</div>
</div>
</div>
<div class="log-item">
<div class="mb-2">
<div>
<div>日期2025/10/8</div>
<div>内容文章支持自动播放下一篇</div>
</div>
</div>
</div>
<div class="log-item">
<div class="mb-2">
<div>
<div>日期2025/9/14</div>
<div>内容完善文章编辑导入导出等功能</div>
</div>
<div class="text-base mt-1">
<div>1文章的音频管理功能目前已可添加音频设置句子与音频的对应位置</div>
<div>2文章可导入导出</div>
<div>3单词可导入导出</div>
</div>
</div>
</div>
<div class="log-item">
<div class="mb-2">
<div>
<div>日期2025/8/10</div>
<div>内容2.0版本发布全新UI全新逻辑新增短语例句近义词等功能</div>
</div>
</div>
</div>
<div class="log-item">
<div class="mb-2">
<div>
<div>日期2025/7/19</div>
<div>内容1.0版本发布</div>
</div>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.log-item {
border-bottom: 1px solid var(--color-input-border);
margin-bottom: 1rem;
}
</style>

View File

@@ -18,7 +18,7 @@ import {useBaseStore} from "@/stores/base.ts";
import {saveAs} from "file-saver";
import {
APP_NAME, APP_VERSION, EMAIL,
EXPORT_DATA_KEY, GITHUB, Host,
EXPORT_DATA_KEY, GITHUB, Host, LIB_JS_URL,
LOCAL_FILE_KEY,
Origin,
PracticeSaveArticleKey,
@@ -41,6 +41,7 @@ import {useRuntimeStore} from "@/stores/runtime.ts";
import {useUserStore} from "@/stores/user.ts";
import {useExport} from "@/hooks/export.ts";
import MigrateDialog from "@/components/MigrateDialog.vue";
import Log from "@/pages/setting/Log.vue";
const emit = defineEmits<{
toggleDisabledDialogEscKey: [val: boolean]
@@ -231,7 +232,7 @@ function importJson(str: string, notice: boolean = true) {
notice && Toast.success('导入成功!')
} catch (err) {
return Toast.error('导入失败!')
}finally {
} finally {
importLoading = false
}
}
@@ -253,7 +254,7 @@ async function importData(e) {
reader.readAsText(file);
} else if (file.name.endsWith(".zip")) {
try {
const JSZip = await loadJsLib('JSZip', `${Origin}/libs/jszip.min.js`);
const JSZip = await loadJsLib('JSZip', LIB_JS_URL.JSZIP);
const zip = await JSZip.loadAsync(file);
const dataFile = zip.file("data.json");
@@ -397,220 +398,7 @@ function transferOk() {
</div>
<!-- 日志-->
<div v-if="tabIndex === 5">
<div class="log-item">
<div class="mb-2">
<div>
<div>日期2025/12/3</div>
<div>内容单词文章设置修改为弹框更方便</div>
</div>
</div>
</div>
<div class="log-item">
<div class="mb-2">
<div>
<div>日期2025/12/3</div>
<div>内容录入新概念部分音频优化文章相关功能</div>
</div>
</div>
</div>
<div class="log-item">
<div class="mb-2">
<div>
<div>日期2025/12/2</div>
<div>内容完成新概念音频优化文章管理页面</div>
</div>
</div>
</div>
<div class="log-item">
<div class="mb-2">
<div>
<div>日期2025/11/30</div>
<div>内容文章里的单词可点击播放</div>
</div>
</div>
</div>
<div class="log-item">
<div class="mb-2">
<div>
<div>日期2025/11/29</div>
<div>内容修改 Slider 组件显示bug新增 IE 浏览器检测提示</div>
</div>
</div>
</div>
<div class="log-item">
<div class="mb-2">
<div>
<div>日期2025/11/28</div>
<div>内容新增引导框 新增<a href="https://github.com/zyronon/TypeWords/pull/175" target="_blank">词典测试模式由大佬
hebeihang 开发</a></div>
</div>
</div>
</div>
<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>
<div>日期2025/11/22</div>
<div>内容适配移动端</div>
</div>
</div>
</div>
<div class="log-item">
<div class="mb-2">
<div>
<div>日期2025/11/16</div>
<div>内容自测单词时不认识单词可以直接输入自动标识为错误单词无需按2</div>
</div>
</div>
</div>
<div class="log-item">
<div class="mb-2">
<div>
<div>日期2025/11/15</div>
<div>内容练习单词时底部工具栏新增跳到下一阶段按钮</div>
</div>
</div>
</div>
<div class="log-item">
<div class="mb-2">
<div>
<div>日期2025/11/14</div>
<div>内容新增文章练习时可跳过空格如果在单词的最后一位上不按空格直接输入下一个字母的话自动跳下一个单词
按空格也自动跳下一个单词
</div>
</div>
</div>
</div>
<div class="log-item">
<div class="mb-2">
<div>
<div>日期2025/11/13</div>
<div>内容新增文章练习时输入时忽略符号/数字选项</div>
</div>
</div>
</div>
<div class="log-item">
<div class="mb-2">
<div>
<div>日期2025/11/6</div>
<div>内容新增随机复习功能</div>
</div>
</div>
</div>
<div class="log-item">
<div class="mb-2">
<div>
<div>日期2025/10/30</div>
<div>内容集成PWA基础配置支持用户以类App形式打开项目</div>
</div>
</div>
</div>
<div class="log-item">
<div class="mb-2">
<div>
<div>日期2025/10/26</div>
<div>内容进一步完善单词练习解决复习数量太多的问题</div>
</div>
<div class="text-base mt-1">
<ol>
<li>
<div class="title"><b>智能模式优化</b></div>
<div class="desc">练习时新增四种练习模式学习自测听写默写</div>
</li>
<li>
<div class="title"><b>学习模式</b></div>
<div class="desc">
<ul>
<li>仅在练习新词时出现</li>
<li>采用跟写 / 拼写方式进行学习</li>
<li> 7 个单词会 <b>强制进行听写</b>解决原来一次练太多听写时已忘记的问题</li>
</ul>
</div>
</li>
<li>
<div class="title"><b>自测模式新增</b></div>
<div class="desc">
<ul>
<li>仅在复习已学单词时出现</li>
<li>不再强制拼写提供我认识不认识选项</li>
<li>选择我认识该单词在后续听写或默写中将不再出现<b>显著减少复习数量</b></li>
</ul>
</div>
</li>
<li>
<div class="title"><b>听写模式</b></div>
<div class="desc">原有逻辑保持不变</div>
</li>
<li>
<div class="title"><b>默写模式新增</b></div>
<div class="desc">
<ul>
<li>仅显示释义不自动发音不显示单词长度</li>
<li>适合强化拼写记忆的场景</li>
</ul>
</div>
</li>
</ol>
<b>说明</b>
<div>本次更新重点解决了复习单词数量过多效率偏低的问题</div>
<div>通过引入复习默写两种模式使复习流程更加灵活高效</div>
</div>
</div>
</div>
<div class="log-item">
<div class="mb-2">
<div>
<div>日期2025/10/8</div>
<div>内容文章支持自动播放下一篇</div>
</div>
</div>
</div>
<div class="log-item">
<div class="mb-2">
<div>
<div>日期2025/9/14</div>
<div>内容完善文章编辑导入导出等功能</div>
</div>
<div class="text-base mt-1">
<div>1文章的音频管理功能目前已可添加音频设置句子与音频的对应位置</div>
<div>2文章可导入导出</div>
<div>3单词可导入导出</div>
</div>
</div>
</div>
<div class="log-item">
<div class="mb-2">
<div>
<div>日期2025/8/10</div>
<div>内容2.0版本发布全新UI全新逻辑新增短语例句近义词等功能</div>
</div>
</div>
</div>
<div class="log-item">
<div class="mb-2">
<div>
<div>日期2025/7/19</div>
<div>内容1.0版本发布</div>
</div>
</div>
</div>
</div>
<Log v-if="tabIndex === 5"/>
<div v-if="tabIndex === 6" class="center flex-col">
<h1>Type Words</h1>
@@ -640,11 +428,6 @@ function transferOk() {
<style scoped lang="scss">
.log-item {
border-bottom: 1px solid var(--color-input-border);
margin-bottom: 1rem;
}
.col-line {
border-right: 2px solid gainsboro;
}

View File

@@ -278,7 +278,7 @@ function importData(e) {
reader.onload = async function (s) {
let data = s.target.result;
importLoading = true
const XLSX = await loadJsLib('XLSX', `${Origin}/libs/xlsx.full.min.js`);
const XLSX = await loadJsLib('XLSX', LIB_JS_URL.XLSX);
let workbook = XLSX.read(data, {type: 'binary'});
let res: any[] = XLSX.utils.sheet_to_json(workbook.Sheets['Sheet1']);
if (res.length) {
@@ -357,7 +357,7 @@ function importData(e) {
async function exportData() {
exportLoading = true
const XLSX = await loadJsLib('XLSX', `${Origin}/libs/xlsx.full.min.js`);
const XLSX = await loadJsLib('XLSX', LIB_JS_URL.XLSX);
let list = runtimeStore.editDict.words
let filename = runtimeStore.editDict.name
let wb = XLSX.utils.book_new()

View File

@@ -145,7 +145,7 @@ watch(dict_list, (val) => {
<div class="w-full" v-else>
<DictGroup
v-for="item in groupedByCategoryAndTag"
:select-id="store.currentStudyWordDict.id"
:select-id="store.sdict.id"
@selectDict="selectDict"
quantifier="个词"
:groupByTag="item[1]"

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { onMounted, provide, ref, watch } from "vue";
import {onMounted, onUnmounted, provide, ref, watch} from "vue";
import Statistics from "@/pages/word/Statistics.vue";
import { emitter, EventKey, useEvents } from "@/utils/eventBus.ts";
@@ -47,6 +47,8 @@ let showConflictNotice = $ref(false)
let allWrongWords = new Set()
let showStatDialog = $ref(false)
let loading = $ref(false)
let timer = $ref(0)
let isFocus = true
let taskWords = $ref<TaskWords>({
new: [],
review: [],
@@ -110,6 +112,13 @@ onMounted(() => {
} else {
showConflictNotice = true
}
document.addEventListener('visibilitychange', () => {
isFocus = !document.hidden
})
})
onUnmounted(() => {
timer && clearInterval(timer)
})
watchOnce(() => data.words.length, (newVal, oldVal) => {
@@ -220,8 +229,17 @@ function initData(initVal: TaskWords, init: boolean = false) {
statStore.startDate = Date.now()
statStore.inputWordNumber = 0
statStore.wrong = 0
statStore.spend = 0
isTypingWrongWord.value = false
}
clearInterval(timer)
timer = setInterval(() => {
if (isFocus) {
statStore.spend += 1000
savePracticeData()
}
}, 1000)
}
const word = $computed<Word>(() => {

View File

@@ -109,15 +109,16 @@ const progress = $computed(() => {
<div class="name">{{ status }}</div>
</div>
<div class="row">
<!-- <div class="num">{{ statStore.spend }}分钟</div>-->
<div class="num">{{ Math.floor(statStore.spend / 1000 / 60) }}分钟</div>
<div class="line"></div>
<div class="name">时间</div>
</div>
<div class="row">
<div class="num">{{ statStore.total }}</div>
<div class="line"></div>
<div class="name">单词总数</div>
</div>
<!-- <div class="row">-->
<!-- <div class="num">{{ format(statStore.inputWordNumber, '', 0) }}</div>-->
<!-- <div class="line"></div>-->
<!-- <div class="name">总输入数</div>-->
<!-- </div>-->
<div class="row">
<div class="num">{{ format(statStore.wrong, '', 0) }}</div>
<div class="line"></div>

View File

@@ -188,7 +188,7 @@ async function onTyping(e: KeyboardEvent) {
}
inputLock = true
let letter = e.key
console.log('letter',letter)
// console.log('letter',letter)
//默写特殊逻辑
if (settingStore.wordPracticeType === WordPracticeType.Dictation) {
if (e.code === 'Space') {
@@ -659,35 +659,35 @@ useEvents([
.typing-word {
padding: 0 0.5rem 12rem;
.word {
font-size: 2rem !important;
letter-spacing: 0.1rem;
margin: 0.5rem 0;
}
.phonetic, .translate {
font-size: 1rem;
}
.label {
width: 4rem;
font-size: 0.9rem;
}
.cn {
font-size: 0.9rem;
}
.en {
font-size: 1rem;
}
.pos {
font-size: 0.9rem;
width: 3rem;
}
// 移动端按钮组调整
.flex.gap-4 {
flex-direction: column;
@@ -695,7 +695,7 @@ useEvents([
gap: 0.5rem;
position: relative;
z-index: 10; // 确保按钮不被其他元素遮挡
.base-button {
width: 100%;
min-height: 48px;
@@ -705,14 +705,14 @@ useEvents([
cursor: pointer;
}
}
// 确保短语和例句区域保持默认层级
.phrase-section,
.sentence {
position: relative;
z-index: auto;
}
// 移动端例句和短语调整
.sentence,
.phrase {
@@ -721,7 +721,7 @@ useEvents([
margin-bottom: 0.5rem;
pointer-events: auto; // 允许点击但不调起输入法
}
// 移动端短语调整
.flex.items-center.gap-4 {
flex-direction: column;
@@ -735,35 +735,35 @@ useEvents([
@media (max-width: 480px) {
.typing-word {
padding: 0 0.3rem 12rem;
.word {
font-size: 1.5rem !important;
letter-spacing: 0.05rem;
margin: 0.3rem 0;
}
.phonetic, .translate {
font-size: 0.9rem;
}
.label {
width: 3rem;
font-size: 0.8rem;
}
.cn {
font-size: 0.8rem;
}
.en {
font-size: 0.9rem;
}
.pos {
font-size: 0.8rem;
width: 2.5rem;
}
.sentence {
font-size: 0.8rem;
line-height: 1.3;

View File

@@ -72,12 +72,6 @@ export const useBaseStore = defineStore('base', {
allIgnoreWords() {
return this.known.words.map((v: Word) => v.word.toLowerCase()).concat(this.simpleWords.map((v: string) => v.toLowerCase()))
},
currentStudyWordDict(): Dict {
if (this.word.studyIndex >= 0) {
return this.word.bookList[this.word.studyIndex] ?? getDefaultDict()
}
return getDefaultDict()
},
sdict(): Dict {
if (this.word.studyIndex >= 0) {
return this.word.bookList[this.word.studyIndex] ?? getDefaultDict()
@@ -93,9 +87,6 @@ export const useBaseStore = defineStore('base', {
if (!this.sdict.perDayStudyNumber) return 0
return Math.ceil((this.sdict.length - this.sdict.lastLearnIndex) / this.sdict.perDayStudyNumber)
},
currentBook(): Dict {
return this.article.bookList[this.article.studyIndex] ?? {}
},
sbook(): Dict {
return this.article.bookList[this.article.studyIndex] ?? {}
},