Merge branch 'master' into fix_theme_color2

This commit is contained in:
Zyronon
2025-11-08 23:45:00 +08:00
committed by GitHub
44 changed files with 6436 additions and 5186 deletions

1
.env
View File

@@ -1,3 +1,2 @@
VITE_ROUTE_BASE=/

View File

@@ -34,7 +34,7 @@ jobs:
run: pnpm install
- name: Build
run: pnpm run build
run: pnpm run build-oss
- name: Deploy to OSS + Refresh CDN
run: pnpm run deploy-oss

View File

@@ -34,7 +34,7 @@ jobs:
run: pnpm install
- name: Build
run: pnpm run build-nocdn
run: pnpm run build
- name: Upload artifact
uses: actions/upload-pages-artifact@v3

View File

@@ -65,4 +65,7 @@ I found this note on my car: 'Sir, we welcome you to our city. This is a 'No Par
Food and talk
A new play is coming to "The Globe"soon, I said. Will you be seeing it?
26的 of curse
26的 of curse
1、例句可以选中单词并添加到收藏
2、ABC页面太墨迹不简洁进度复杂本周学习记录改成日历有个标记+激励分享功能,满足炫耀欲望

View File

@@ -18,14 +18,14 @@
</p>
<div align=center>
<a href="https://trendshift.io/repositories/14139" target="_blank" class="trendshift-badge"><img src="https://trendshift.io/api/badge/repositories/14139" alt="TypeWords | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
<a href="https://trendshift.io/repositories/15226" target="_blank"><img src="https://trendshift.io/api/badge/repositories/15226" alt="zyronon%2FTypeWords | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
</div>
<p align="center">
<br/>
<a href="https://skywork.ai/p/GrXQb4"><img src="/public/skywork-ai.png" alt="License"></a>
Skywork.AI:<a href="https://skywork.ai/p/GrXQb4" target="_blank">10 tasks in 1 hour, not 10 hours →Limited free spots: 127 left</a>
<br/>
<br>
<a href="https://skywork.ai/p/GrXQb4"><img src="/public/skywork-ai.png" alt="License" style="width: 650px;"></a>
<br>
赞助: <a href="https://skywork.ai/p/GrXQb4" target="_blank">Skywork.AI: 10 tasks in 1 hour, not 10 hours →Limited free spots: 127 left</a>
<br/>
<br/>
</p>

13
components.d.ts vendored
View File

@@ -20,7 +20,6 @@ declare module 'vue' {
Book: typeof import('./src/components/Book.vue')['default']
Checkbox: typeof import('./src/components/base/checkbox/Checkbox.vue')['default']
Close: typeof import('./src/components/icon/Close.vue')['default']
CollectNotice: typeof import('./src/components/CollectNotice.vue')['default']
ConflictNotice: typeof import('./src/components/ConflictNotice.vue')['default']
DeleteIcon: typeof import('./src/components/icon/DeleteIcon.vue')['default']
Dialog: typeof import('./src/components/dialog/Dialog.vue')['default']
@@ -36,16 +35,19 @@ declare module 'vue' {
IconBxVolumeMute: typeof import('~icons/bx/volume-mute')['default']
IconEosIconsLoading: typeof import('~icons/eos-icons/loading')['default']
IconFluentAdd16Regular: typeof import('~icons/fluent/add16-regular')['default']
IconFluentAdd20Filled: typeof import('~icons/fluent/add20-filled')['default']
IconFluentAdd20Regular: typeof import('~icons/fluent/add20-regular')['default']
IconFluentAddSquare20Regular: typeof import('~icons/fluent/add-square20-regular')['default']
IconFluentArrowBounce20Regular: typeof import('~icons/fluent/arrow-bounce20-regular')['default']
IconFluentArrowCircleRight16Regular: typeof import('~icons/fluent/arrow-circle-right16-regular')['default']
IconFluentArrowLeft16Regular: typeof import('~icons/fluent/arrow-left16-regular')['default']
IconFluentArrowMove20Regular: typeof import('~icons/fluent/arrow-move20-regular')['default']
IconFluentArrowRight16Regular: typeof import('~icons/fluent/arrow-right16-regular')['default']
IconFluentArrowShuffle16Regular: typeof import('~icons/fluent/arrow-shuffle16-regular')['default']
IconFluentArrowShuffle20Filled: typeof import('~icons/fluent/arrow-shuffle20-filled')['default']
IconFluentArrowSort20Regular: typeof import('~icons/fluent/arrow-sort20-regular')['default']
IconFluentArrowSwap20Regular: typeof import('~icons/fluent/arrow-swap20-regular')['default']
IconFluentBookLetter20Regular: typeof import('~icons/fluent/book-letter20-regular')['default']
IconFluentBookNumber20Filled: typeof import('~icons/fluent/book-number20-filled')['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']
@@ -72,9 +74,9 @@ declare module 'vue' {
IconFluentSearch24Regular: typeof import('~icons/fluent/search24-regular')['default']
IconFluentSettings20Regular: typeof import('~icons/fluent/settings20-regular')['default']
IconFluentShieldQuestion20Regular: typeof import('~icons/fluent/shield-question20-regular')['default']
IconFluentSlideTextTitleEdit20Regular: typeof import('~icons/fluent/slide-text-title-edit20-regular')['default']
IconFluentSpeakerEdit20Regular: typeof import('~icons/fluent/speaker-edit20-regular')['default']
IconFluentSpeakerSettings20Regular: typeof import('~icons/fluent/speaker-settings20-regular')['default']
IconFluentStar12Regular: typeof import('~icons/fluent/star12-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']
@@ -88,12 +90,7 @@ declare module 'vue' {
IconFluentWeatherMoon16Regular: typeof import('~icons/fluent/weather-moon16-regular')['default']
IconFluentWeatherSunny16Regular: typeof import('~icons/fluent/weather-sunny16-regular')['default']
IconIconParkOutlineAddMusic: typeof import('~icons/icon-park-outline/add-music')['default']
IconMaterialSymbolsMail: typeof import('~icons/material-symbols/mail')['default']
IconPhExportLight: typeof import('~icons/ph/export-light')['default']
IconRiTwitterFill: typeof import('~icons/ri/twitter-fill')['default']
IconSimpleIconsGithub: typeof import('~icons/simple-icons/github')['default']
IconSimpleIconsWechat: typeof import('~icons/simple-icons/wechat')['default']
IconSimpleIconsXiaohongshu: typeof import('~icons/simple-icons/xiaohongshu')['default']
IconSystemUiconsImport: typeof import('~icons/system-uicons/import')['default']
InputNumber: typeof import('./src/components/base/InputNumber.vue')['default']
List: typeof import('./src/components/list/List.vue')['default']

View File

@@ -26,13 +26,13 @@ Practice English, one strike, one step forward
<p align="center">
<br/>
<a href="https://skywork.ai/p/GrXQb4"><img src="/public/skywork-ai.png" alt="License"></a>
Skywork.AI:<a href="https://skywork.ai/p/GrXQb4" target="_blank">10 tasks in 1 hour, not 10 hours →Limited free spots: 127 left</a>
<br>
<a href="https://skywork.ai/p/GrXQb4"><img src="/public/skywork-ai.png" alt="License" style="width: 650px;"></a>
<br>
Sponsor: <a href="https://skywork.ai/p/GrXQb4" target="_blank">Skywork.AI: 10 tasks in 1 hour, not 10 hours →Limited free spots: 127 left</a>
<br/>
<br/>
<br/>
</p>
</p>
<img width="1920" height="1440" alt="295shots_so" src="https://github.com/user-attachments/assets/383ed437-856e-48fe-92b0-9619babb49be" />
<img width="1920" height="1440" alt="922shots_so" src="https://github.com/user-attachments/assets/5b5fa13f-747c-4368-ae21-3c9d7d30fbc7" />

View File

@@ -2,43 +2,51 @@
<html lang="zh-CN">
<head>
<meta charset="UTF-8"/>
<link rel="icon" type="image/svg+xml" href="/favicon.png"/>
<link rel="manifest" href="/manifest.json">
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Type Words - 词文记 | 单词跟打 · 文章跟打</title>
<title>Type Words 官网 - 词文记 | 单词跟打 · 文章跟打</title>
<!-- 搜索引擎描述 -->
<meta name="description"
content="Type Words在线英语练习平台,支持单词、文章跟打练习,提升打字与语言能力。Practice English, one keystroke at a time.">
content="Type Words 官方网站 - 在线英语练习平台,支持单词、文章跟打练习,提升英语学习效率。Practice English, one strike, one step forward">
<!-- 关键词(可选,搜索引擎基本不用,但能补充信息) -->
<meta name="keywords"
content="Type Words, Typing Word, 英语打字练习, 单词跟打, 文章跟打, 键盘练习, 英语学习, 文章学习">
content="Type Words, Typing Word, Type Words 官网, 官方网站, 英语打字练习, 单词跟打, 文章跟打, 键盘练习, 英语学习, 文章学习, 打字练习软件, 单词记忆工具, 英语学习软件, 背单词神器, 英语肌肉记忆, 键盘工作者, 免费英语学习, 音标发音, 默写练习, 在线学英语, CET-4, CET-6, TOEFL, IELTS, GRE, GMAT, SAT, 考研英语, 专四专八, 程序员英语, JavaScript API, Node.js API, Java API, Linux命令, 编程词汇, 技术英语, VSCode插件, 开源项目, GitHub趋势榜, V2EX热搜, Gitee GVP, 少数派推荐, 英语打字训练, WPM统计, 准确率分析, 商务英语, BEC, 雅思听力, 日语学习, 多语言学习, 英语口语练习, 单词拼写训练">
<meta name="author" content="zyronon"/>
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1"/>
<link rel="canonical" href="https://2study.top/"/>
<!-- Open Graph用于社交媒体分享微信/QQ/知乎/Facebook 等) -->
<meta property="og:title" content="Type Words - 英语打字练习平台">
<meta property="og:title" content="Type Words 官网 - 英语打字练习平台">
<meta property="og:description"
content="在线英语打字练习平台,支持单词跟打与文章跟打,帮助提升打字速度与英语学习效率。">
content="Type Words 官方网站 - 在线英语练习平台,支持单词文章跟打练习提升英语学习效率。Practice English, one strike, one step forward">
<meta property="og:type" content="website">
<meta property="og:url" content="https://2study.top/">
<meta property="og:image" content="https://2study.top/favicon.png">
<!-- Twitter Card用于 Twitter 分享) -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Type Words - 英语打字练习平台">
<meta name="twitter:title" content="Type Words 官网 - 英语打字练习平台">
<meta name="twitter:description"
content="Type Words在线英语练习平台,支持单词跟打、文章练习,提升打字速度与英语水平。">
content="Type Words 官方网站 - 在线英语练习平台,支持单词、文章跟打练习,提升英语学习效率。Practice English, one strike, one step forward">
<meta name="twitter:image" content="https://2study.top/favicon.png">
<!--用于百度站长验证 -->
<meta name="baidu-site-verification" content="codeva-Kw33xFT3p2"/>
<link rel="icon" type="image/svg+xml" href="/favicon.png"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<!-- 苹果设备iOS Safari在用户添加到主屏时显示的图标-->
<link rel="apple-touch-icon" sizes="180x180" href="/favicon.png"/>
<!-- 设置浏览器地址栏颜色(在 Android Chrome 特别明显)。-->
<meta name="theme-color" content="#818CF8"/>
<!-- 阻止 iOS 自动把数字识别为电话号码。-->
<!-- HandheldFriendly 和 MobileOptimized 是旧手机浏览器的优化提示(现在作用不大)。-->
<meta name="format-detection" content="telephone=no"/>
<meta name="HandheldFriendly" content="True"/>
<meta name="MobileOptimized" content="320"/>
<!-- referrer 控制请求来源信息-->
<meta name="referrer" content="origin-when-cross-origin"/>
<!-- color-scheme 告诉浏览器支持亮/暗模式-->
<meta name="color-scheme" content="light dark"/>
<script>
;(function () {
var src = '//cdn.jsdelivr.net/npm/eruda';
if (!/eruda=true/.test(window.location) && localStorage.getItem('active-eruda') != 'true') return;
document.write('<scr' + 'ipt src="' + src + '"></scr' + 'ipt>');
document.write('<scr' + 'ipt>eruda.init();</scr' + 'ipt>');
})();
</script>
<script>
if (!location.href.includes('localhost')
&& !location.href.includes('192.168')
@@ -76,18 +84,11 @@
</script>
</head>
<body>
<noscript>
<div>You need to enable JavaScript to run Type Words.</div>
<div>你需要启用 JavaScript 来运行 Type Words.</div>
</noscript>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js').then(registration => {
console.log('ServiceWorker registration successful with scope: ', registration.scope);
}).catch(error => {
console.log('ServiceWorker registration failed: ', error);
});
});
}
</script>
</body>
</html>

View File

@@ -1,5 +0,0 @@
[[redirects]]
from = "/baidu"
to = "https://api.fanyi.baidu.com/api/trans/vip/translate"
status = 200
force = true

View File

@@ -6,10 +6,11 @@
"start": "vite",
"dev": "vite",
"test": "",
"build": "vite build && node scripts/generate-sitemap.js",
"build-nocdn": "vite build",
"build": "vite build",
"build-oss": "vite build && node scripts/do.js",
"build-tsc": "vue-tsc && vite build",
"report": "vite build",
"report-oss": "vite build",
"preview": "vite preview",
"commit": "git-cz",
"prepare": "husky install",

9558
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 200 KiB

BIN
public/qq.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

554
public/static-home.html Normal file
View File

@@ -0,0 +1,554 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>Type Words 官网 - 词文记 | 单词跟打 · 文章跟打</title>
<!-- 搜索引擎描述 -->
<meta name="description"
content="Type Words 官方网站 - 在线英语练习平台支持单词、文章跟打练习提升英语学习效率。Practice English, one strike, one step forward">
<!-- 关键词(可选,搜索引擎基本不用,但能补充信息) -->
<meta name="keywords"
content="Type Words, Typing Word, Type Words 官网, 官方网站, 英语打字练习, 单词跟打, 文章跟打, 键盘练习, 英语学习, 文章学习, 打字练习软件, 单词记忆工具, 英语学习软件, 背单词神器, 英语肌肉记忆, 键盘工作者, 免费英语学习, 音标发音, 默写练习, 在线学英语, CET-4, CET-6, TOEFL, IELTS, GRE, GMAT, SAT, 考研英语, 专四专八, 程序员英语, JavaScript API, Node.js API, Java API, Linux命令, 编程词汇, 技术英语, VSCode插件, 开源项目, GitHub趋势榜, V2EX热搜, Gitee GVP, 少数派推荐, 英语打字训练, WPM统计, 准确率分析, 商务英语, BEC, 雅思听力, 日语学习, 多语言学习, 英语口语练习, 单词拼写训练">
<meta name="author" content="zyronon" />
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1" />
<link rel="canonical" href="https://2study.top/" />
<!-- Open Graph用于社交媒体分享微信/QQ/知乎/Facebook 等) -->
<meta property="og:title" content="Type Words 官网 - 英语打字练习平台">
<meta property="og:description"
content="Type Words 官方网站 - 在线英语练习平台支持单词、文章跟打练习提升英语学习效率。Practice English, one strike, one step forward">
<meta property="og:type" content="website">
<meta property="og:url" content="https://2study.top/">
<meta property="og:image" content="https://2study.top/favicon.png">
<!-- Twitter Card用于 Twitter 分享) -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Type Words 官网 - 英语打字练习平台">
<meta name="twitter:description"
content="Type Words 官方网站 - 在线英语练习平台支持单词、文章跟打练习提升英语学习效率。Practice English, one strike, one step forward">
<meta name="twitter:image" content="https://2study.top/favicon.png">
<link rel="icon" type="image/svg+xml" href="/favicon.png"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<!-- 苹果设备iOS Safari在用户添加到主屏时显示的图标-->
<link rel="apple-touch-icon" sizes="180x180" href="/favicon.png" />
<!-- 设置浏览器地址栏颜色(在 Android Chrome 特别明显)。-->
<meta name="theme-color" content="#818CF8" />
<!-- 阻止 iOS 自动把数字识别为电话号码。-->
<!-- HandheldFriendly 和 MobileOptimized 是旧手机浏览器的优化提示(现在作用不大)。-->
<meta name="format-detection" content="telephone=no" />
<meta name="HandheldFriendly" content="True" />
<meta name="MobileOptimized" content="320" />
<!-- referrer 控制请求来源信息-->
<meta name="referrer" content="origin-when-cross-origin" />
<!-- color-scheme 告诉浏览器支持亮/暗模式-->
<meta name="color-scheme" content="light dark" />
<style>
body {
background: rgb(231, 232, 235);
}
h1 {
font-size: 4.8rem !important;
background: linear-gradient(120deg, #bd34fe 30%, #41d1ff);
-webkit-text-fill-color: transparent;
-webkit-background-clip: text;
background-clip: text;
color: transparent;
margin: 0;
font-weight: bold !important;
}
h2 {
font-size: 1.4rem !important;
font-weight: normal !important;
color: rgb(91, 91, 91);
margin: 0;
}
.card {
position: relative;
border-radius: 1rem;
padding: 1rem;
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.6rem;
margin-bottom: 0;
width: 25%;
background: rgb(247, 247, 247);
.emoji {
display: inline-block;
background: rgb(226 232 240 / 1);
padding: 0.3rem .6rem;
border-radius: 0.4rem;
font-size: 1.5rem;
}
.title {
font-weight: bold;
}
ul {
margin: 0;
padding-left: 1.2rem;
}
}
a {
color: dodgerblue !important;
}
.base-button {
cursor: pointer;
box-sizing: border-box;
display: inline-flex;
align-items: center;
justify-content: center;
outline: none;
text-align: center;
transition: 0.1s;
user-select: none;
vertical-align: middle;
white-space: nowrap;
border-radius: 0.3rem;
color: white;
background: rgb(12, 140, 233);
padding: 0 1.3rem;
height: 2.5rem;
font-size: 0.9rem;
}
.base-button + .base-button {
margin-left: 2rem;
}
.base-button:hover {
opacity: 0.8;
}
.icon {
cursor: pointer;
width: 2rem;
height: 2rem;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: .3rem;
background: transparent;
transition: all .3s;
color: dimgray;
}
.icon:hover {
background: rgb(12, 140, 233);
color: white;
}
.mask {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.3);
display: none;
opacity: 0;
transition: all .3s;
}
.dialog {
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
background: rgb(247, 247, 247);
width: 30rem;
border-radius: 1rem;
display: none;
opacity: 0;
transition: all .3s;
}
.dialog header {
padding: 1rem;
display: flex;
justify-content: space-between;
}
.dialog header .title {
font-size: 1.4rem;
}
.dialog-body {
padding: 0 1.2rem 1.2rem 1.2rem;
}
.wrapper {
display: flex;
flex-direction: column;
justify-content: space-between;
min-height: 100vh;
}
.content {
margin-top: 6rem;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
gap: 2rem;
}
.text-center {
text-align: center;
}
.sky {
margin-top: 3rem;
border-top: 1px solid #cecece;
border-bottom: 1px solid #cecece;
padding: 1.2rem 0;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
gap: 0.4rem;
width: 100%;
margin-bottom: 1rem;
}
.w {
width: 60vw;
}
.sky-img {
width: 100%;
border-radius: 0.5rem;
}
.card-wrap {
display: flex;
margin-bottom: 1.2rem;
gap: 1rem;
}
.bottom {
display: flex;
gap: 1rem;
margin:1rem 0 2rem 0;
width: 100%;
padding-top: 1.5rem;
border-top: 1px solid #c4c4c4;
justify-content: center;
align-items: center;
}
.gap-1 {
gap: 1rem;
}
.center {
display: flex;
justify-content: center;
align-items: center;
}
.img {
width: 16rem;
border-radius: 1rem;
margin-top: 1.2rem;
}
</style>
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js').then(registration => {
console.log('ServiceWorker registration successful with scope: ', registration.scope);
}).catch(error => {
console.log('ServiceWorker registration failed: ', error);
});
});
}
</script>
<script>
function nav(url) {
window.location.href = url;
// history.pushState(null, "", url);
}
function toggleEl(val, close = false) {
let le = document.querySelector(val)
if (le) {
if (['none', ''].includes(le.style.display) && !close) {
le.style.display = 'block';
setTimeout(function () {
le.style.opacity = 1;
}, 10)
} else {
le.style.opacity = 0;
setTimeout(function () {
le.style.display = 'none';
}, 300)
}
}
}
function toggleWechatDialog() {
toggleEl('.mask')
toggleEl('#wechatDialog')
}
function toggleQQDialog() {
toggleEl('.mask')
toggleEl('#qqDialog')
}
function toggleXhsDialog() {
toggleEl('.mask')
toggleEl('#xhsDialog')
}
function closeDialog() {
toggleEl('.mask')
toggleEl('#wechatDialog', true)
toggleEl('#xhsDialog', true)
toggleEl('#qqDialog', true)
}
</script>
</head>
<body>
<div class="wrapper">
<div class="content">
<h1>Type Words</h1>
<div class="text-center">
<h2>学习英语,一次敲击,一点进步,开源单词与文章练习工具</h2>
</div>
<div class="">
<div class="base-button" onclick="nav('/words')">单词练习</div>
<div class="base-button" onclick="nav('/articles')">文章练习</div>
</div>
<div class="sky">
<a href="https://skywork.ai/p/GrXQb4" style="width: 40%;" target="_blank">
<img src="https://2study.top/skywork-ai.png"
alt="Skywork.AI"
class="sky-img"></a>
<span>赞助:<a href="https://skywork.ai/p/GrXQb4" class="color-blue!" target="_blank">Skywork.AI: 10 tasks in 1 hour, not 10 hours →Limited free spots: 127 left</a></span>
</div>
<div class="w">
<div class="card-wrap">
<div class="card">
<div class="emoji">📚</div>
<div class="title">单词练习</div>
<div class="desc">
<ul>
<li>三种输入模式:跟打 / 复习 / 默写</li>
<li>智能模式:智能规划复习与默写</li>
<li>自由模式:不受限制,自行规划</li>
</ul>
</div>
</div>
<div class="card">
<div class="emoji">✍️</div>
<div class="title">文章练习</div>
<div class="desc">
<ul>
<li>内置常见书籍,也可自行添加文章</li>
<li>跟打 + 默写双模式,让背诵更高效</li>
<li>支持边听边默写,强化记忆</li>
</ul>
</div>
</div>
<div class="card">
<div class="emoji">📕</div>
<div class="title">收藏、错词本、已掌握</div>
<div class="desc">
<ul>
<li>输入错误自动添加到错词本</li>
<li>主动添加到已掌握,后续自动跳过</li>
<li>主动添加到收藏中,以便巩固复习</li>
</ul>
</div>
</div>
<div class="card">
<div class="emoji">🌐</div>
<div class="title">海量词库</div>
<div class="desc">
内置小学、初中、高中、四六级、考研、雅思、托福、GRE、GMAT、SAT、BEC、专四、专八等词库
</div>
</div>
</div>
<div class="card-wrap">
<div class="card">
<div class="emoji">🆓</div>
<div class="title">免费开源</div>
<div class="desc">
<ul>
<li>完全开源,可审查、可修改</li>
<li>免费使用</li>
<li>私有部署</li>
</ul>
</div>
</div>
<div class="card">
<div class="emoji">⚙️</div>
<div class="title">高度自由</div>
<div class="desc">
<ul>
<li>丰富的键盘音效</li>
<li>可自定义快捷键</li>
<li>高度定制化的设置选项</li>
</ul>
</div>
</div>
<div class="card">
<div class="emoji">🎨</div>
<div class="title">简洁高效</div>
<div class="desc">
<ul>
<li>简洁设计现代化UI</li>
<li>界面清爽,操作简单</li>
<li>不强制关注任何平台</li>
</ul>
</div>
</div>
<div class="card">
<div class="emoji">🎯</div>
<div class="title">个性学习</div>
<div class="desc">
<ul>
<li>自由添加词典与文章</li>
<li>定制个性学习计划</li>
<li>多种学习复习策略</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<div class="bottom">
<div class="center gap-1">
<a
href="https://github.com/zyronon/TypeWords"
target="_blank"
rel="noreferrer"
aria-label="Github Address">
<div class="icon">
<svg viewBox="0 0 24 24" width="1.4em" height="1.4em">
<path fill="currentColor"
d="M12 .297c-6.63 0-12 5.373-12 12c0 5.303 3.438 9.8 8.205 11.385c.6.113.82-.258.82-.577c0-.285-.01-1.04-.015-2.04c-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729c1.205.084 1.838 1.236 1.838 1.236c1.07 1.835 2.809 1.305 3.495.998c.108-.776.417-1.305.76-1.605c-2.665-.3-5.466-1.332-5.466-5.93c0-1.31.465-2.38 1.235-3.22c-.135-.303-.54-1.523.105-3.176c0 0 1.005-.322 3.3 1.23c.96-.267 1.98-.399 3-.405c1.02.006 2.04.138 3 .405c2.28-1.552 3.285-1.23 3.285-1.23c.645 1.653.24 2.873.12 3.176c.765.84 1.23 1.91 1.23 3.22c0 4.61-2.805 5.625-5.475 5.92c.42.36.81 1.096.81 2.22c0 1.606-.015 2.896-.015 3.286c0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"></path>
</svg>
</div>
</a>
<div class="icon" onclick="toggleWechatDialog()">
<svg viewBox="0 0 24 24" width="1.4em" height="1.4em">
<path fill="currentColor"
d="M8.691 2.188C3.891 2.188 0 5.476 0 9.53c0 2.212 1.17 4.203 3.002 5.55a.59.59 0 0 1 .213.665l-.39 1.48c-.019.07-.048.141-.048.213c0 .163.13.295.29.295a.33.33 0 0 0 .167-.054l1.903-1.114a.86.86 0 0 1 .717-.098a10.2 10.2 0 0 0 2.837.403c.276 0 .543-.027.811-.05c-.857-2.578.157-4.972 1.932-6.446c1.703-1.415 3.882-1.98 5.853-1.838c-.576-3.583-4.196-6.348-8.596-6.348M5.785 5.991c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 0 1-1.162 1.178A1.17 1.17 0 0 1 4.623 7.17c0-.651.52-1.18 1.162-1.18zm5.813 0c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 0 1-1.162 1.178a1.17 1.17 0 0 1-1.162-1.178c0-.651.52-1.18 1.162-1.18m5.34 2.867c-1.797-.052-3.746.512-5.28 1.786c-1.72 1.428-2.687 3.72-1.78 6.22c.942 2.453 3.666 4.229 6.884 4.229c.826 0 1.622-.12 2.361-.336a.72.72 0 0 1 .598.082l1.584.926a.3.3 0 0 0 .14.047c.134 0 .24-.111.24-.247c0-.06-.023-.12-.038-.177l-.327-1.233a.6.6 0 0 1-.023-.156a.49.49 0 0 1 .201-.398C23.024 18.48 24 16.82 24 14.98c0-3.21-2.931-5.837-6.656-6.088V8.89c-.135-.01-.27-.027-.407-.03zm-2.53 3.274c.535 0 .969.44.969.982a.976.976 0 0 1-.969.983a.976.976 0 0 1-.969-.983c0-.542.434-.982.97-.982zm4.844 0c.535 0 .969.44.969.982a.976.976 0 0 1-.969.983a.976.976 0 0 1-.969-.983c0-.542.434-.982.969-.982"></path>
</svg>
</div>
<div class="icon" onclick="toggleQQDialog()">
<svg viewBox="0 0 24 24" width="1.4em" height="1.4em"><g fill="none"><path d="m12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.018-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z"/><path fill="currentColor" d="M12 2a6.285 6.285 0 0 0-6.276 5.937l-.146 2.63a28 28 0 0 0-.615 1.41c-1.24 3.073-1.728 5.773-1.088 6.032c.335.135.913-.426 1.566-1.432a6.67 6.67 0 0 0 1.968 3.593c-1.027.35-1.91.828-1.91 1.33c0 .509 2.48.503 4.239.5h.001c.549-.002 1.01-.008 1.38-.057a6.7 6.7 0 0 0 1.76 0c.37.05.833.055 1.382.056c1.76.004 4.239.01 4.239-.499c0-.502-.883-.979-1.909-1.33a6.67 6.67 0 0 0 1.967-3.586c.65 1.002 1.227 1.56 1.56 1.425c.64-.259.154-2.96-1.088-6.032a28 28 0 0 0-.607-1.395l-.147-2.645A6.285 6.285 0 0 0 12 2"/></g></svg>
</div>
<div class="icon" onclick="toggleXhsDialog()">
<svg viewBox="0 0 24 24" width="1.4em" height="1.4em">
<path fill="currentColor"
d="M22.405 9.879c.002.016.01.02.07.019h.725a.797.797 0 0 0 .78-.972a.794.794 0 0 0-.884-.618a.795.795 0 0 0-.692.794c0 .101-.002.666.001.777m-11.509 4.808c-.203.001-1.353.004-1.685.003a2.5 2.5 0 0 1-.766-.126a.025.025 0 0 0-.03.014L7.7 16.127a.025.025 0 0 0 .01.032c.111.06.336.124.495.124c.66.01 1.32.002 1.981 0q.017 0 .023-.015l.712-1.545a.025.025 0 0 0-.024-.036zM.477 9.91c-.071 0-.076.002-.076.01l-.01.08c-.027.397-.038.495-.234 3.06c-.012.24-.034.389-.135.607c-.026.057-.033.042.003.112c.046.092.681 1.523.787 1.74c.008.015.011.02.017.02c.008 0 .033-.026.047-.044q.219-.282.371-.606c.306-.635.44-1.325.486-1.706c.014-.11.021-.22.03-.33l.204-2.616l.022-.293c.003-.029 0-.033-.03-.034zm7.203 3.757a1.4 1.4 0 0 1-.135-.607c-.004-.084-.031-.39-.235-3.06a.4.4 0 0 0-.01-.082c-.004-.011-.052-.008-.076-.008h-1.48c-.03.001-.034.005-.03.034l.021.293q.114 1.473.233 2.946c.05.4.186 1.085.487 1.706c.103.215.223.419.37.606c.015.018.037.051.048.049c.02-.003.742-1.642.804-1.765c.036-.07.03-.055.003-.112m3.861-.913h-.872a.126.126 0 0 1-.116-.178l1.178-2.625a.025.025 0 0 0-.023-.035l-1.318-.003a.148.148 0 0 1-.135-.21l.876-1.954a.025.025 0 0 0-.023-.035h-1.56q-.017 0-.024.015l-.926 2.068c-.085.169-.314.634-.399.938a.5.5 0 0 0-.02.191a.46.46 0 0 0 .23.378a1 1 0 0 0 .46.119h.59c.041 0-.688 1.482-.834 1.972a.5.5 0 0 0-.023.172a.47.47 0 0 0 .23.398c.15.092.342.12.475.12l1.66-.001q.017 0 .023-.015l.575-1.28a.025.025 0 0 0-.024-.035m-6.93-4.937H3.1a.032.032 0 0 0-.034.033c0 1.048-.01 2.795-.01 6.829c0 .288-.269.262-.28.262h-.74c-.04.001-.044.004-.04.047c.001.037.465 1.064.555 1.263c.01.02.03.033.051.033c.157.003.767.009.938-.014c.153-.02.3-.06.438-.132c.3-.156.49-.419.595-.765c.052-.172.075-.353.075-.533q.003-3.495-.007-6.991a.03.03 0 0 0-.032-.032zm11.784 6.896q-.002-.02-.024-.022h-1.465c-.048-.001-.049-.002-.05-.049v-4.66c0-.072-.005-.07.07-.07h.863c.08 0 .075.004.075-.074V8.393c0-.082.006-.076-.08-.076h-3.5c-.064 0-.075-.006-.075.073v1.445c0 .083-.006.077.08.077h.854c.075 0 .07-.004.07.07v4.624c0 .095.008.084-.085.084c-.37 0-1.11-.002-1.304 0c-.048.001-.06.03-.06.03l-.697 1.519s-.014.025-.008.036s.013.008.058.008q2.622.003 5.243.002c.03-.001.034-.006.035-.033zm4.177-3.43q0 .021-.02.024c-.346.006-.692.004-1.037.004q-.021-.003-.022-.024q-.006-.651-.01-1.303c0-.072-.006-.071.07-.07l.733-.003c.041 0 .081.002.12.015c.093.025.16.107.165.204c.006.431.002 1.153.001 1.153m2.67.244a1.95 1.95 0 0 0-.883-.222h-.18c-.04-.001-.04-.003-.042-.04V10.21q.001-.198-.025-.394a1.8 1.8 0 0 0-.153-.53a1.53 1.53 0 0 0-.677-.71a2.2 2.2 0 0 0-1-.258c-.153-.003-.567 0-.72 0c-.07 0-.068.004-.068-.065V7.76c0-.031-.01-.041-.046-.039H17.93s-.016 0-.023.007q-.008.008-.008.023v.546c-.008.036-.057.015-.082.022h-.95c-.022.002-.028.008-.03.032v1.481c0 .09-.004.082.082.082h.913c.082 0 .072.128.072.128v1.148s.003.117-.06.117h-1.482c-.068 0-.06.082-.06.082v1.445s-.01.068.064.068h1.457c.082 0 .076-.006.076.079v3.225c0 .088-.007.081.082.081h1.43c.09 0 .082.007.082-.08v-3.27c0-.029.006-.035.033-.035l2.323-.003a.7.7 0 0 1 .28.061a.46.46 0 0 1 .274.407c.008.395.003.79.003 1.185c0 .259-.107.367-.33.367h-1.218c-.023.002-.029.008-.028.033q.276.655.57 1.303a.05.05 0 0 0 .04.026c.17.005.34.002.51.003c.15-.002.517.004.666-.01a2 2 0 0 0 .408-.075c.59-.18.975-.698.976-1.313v-1.981q.001-.191-.034-.38c0 .078-.029-.641-.724-.998"></path>
</svg>
</div>
<a
href="https://x.com/typewords2"
target="_blank"
rel="noreferrer"
aria-label="关注我的 X 账户 typewords2">
<div class="icon">
<svg viewBox="0 0 24 24" width="1.4em" height="1.4em">
<path fill="currentColor"
d="M22.213 5.656a8.4 8.4 0 0 1-2.402.658A4.2 4.2 0 0 0 21.649 4c-.82.488-1.719.83-2.655 1.015a4.182 4.182 0 0 0-7.126 3.814a11.87 11.87 0 0 1-8.621-4.37a4.17 4.17 0 0 0-.566 2.103c0 1.45.739 2.731 1.86 3.481a4.2 4.2 0 0 1-1.894-.523v.051a4.185 4.185 0 0 0 3.355 4.102a4.2 4.2 0 0 1-1.89.072A4.185 4.185 0 0 0 8.02 16.65a8.4 8.4 0 0 1-6.192 1.732a11.83 11.83 0 0 0 6.41 1.88c7.694 0 11.9-6.373 11.9-11.9q0-.271-.012-.541a8.5 8.5 0 0 0 2.086-2.164"></path>
</svg>
</div>
</a>
<a
href="mailto:zyronon@163.com"
target="_blank"
rel="noreferrer"
aria-label="发送邮件到 zyronon@163.com">
<div class="icon">
<svg viewBox="0 0 24 24" width="1.4em" height="1.4em">
<path fill="currentColor"
d="M4 20q-.825 0-1.412-.587T2 18V6q0-.825.588-1.412T4 4h16q.825 0 1.413.588T22 6v12q0 .825-.587 1.413T20 20zm8-7l8-5V6l-8 5l-8-5v2z"></path>
</svg>
</div>
</a>
</div>
<div>蜀ICP备2025157466号</div>
</div>
<div class="mask" onclick="closeDialog()"></div>
<div class="dialog" id="wechatDialog">
<header>
<div class="title">微信群</div>
<svg
onclick="toggleWechatDialog()"
viewBox="0 0 20 20" width="24" height="1.2em" class="cursor-pointer">
<path fill="currentColor"
d="m4.089 4.216l.057-.07a.5.5 0 0 1 .638-.057l.07.057L10 9.293l5.146-5.147a.5.5 0 0 1 .638-.057l.07.057a.5.5 0 0 1 .057.638l-.057.07L10.707 10l5.147 5.146a.5.5 0 0 1 .057.638l-.057.07a.5.5 0 0 1-.638.057l-.07-.057L10 10.707l-5.146 5.147a.5.5 0 0 1-.638.057l-.07-.057a.5.5 0 0 1-.057-.638l.057-.07L9.293 10L4.146 4.854a.5.5 0 0 1-.057-.638l.057-.07z"></path>
</svg>
</header>
<div class="dialog-body">
<span>加入我们的用户社群后,您可以与我们的开发团队进行沟通,分享您的使用体验和建议,帮助我们改进产品,同时也能够及时了解我们的最新动态和更新内容。</span>
<div class="center">
<img src="/wechat.png" alt="微信群二维码" class="img">
</div>
</div>
</div>
<div class="dialog" id="xhsDialog">
<header>
<div class="title">小红书</div>
<svg
onclick="toggleXhsDialog()"
viewBox="0 0 20 20" width="24" height="1.2em" class="cursor-pointer">
<path fill="currentColor"
d="m4.089 4.216l.057-.07a.5.5 0 0 1 .638-.057l.07.057L10 9.293l5.146-5.147a.5.5 0 0 1 .638-.057l.07.057a.5.5 0 0 1 .057.638l-.057.07L10.707 10l5.147 5.146a.5.5 0 0 1 .057.638l-.057.07a.5.5 0 0 1-.638.057l-.07-.057L10 10.707l-5.146 5.147a.5.5 0 0 1-.638.057l-.07-.057a.5.5 0 0 1-.057-.638l.057-.07L9.293 10L4.146 4.854a.5.5 0 0 1-.057-.638l.057-.07z"></path>
</svg>
</header>
<div class="dialog-body">
<span>关注小红书后,您可以获得开发团队的最新动态和更新内容,反馈您的使用体验和建议,帮助我们改进产品,同时也能够及时了解我们的最新动态和更新内容。</span>
<div class="center">
<img src="/xhs.png" alt="小红书二维码" class="img">
</div>
</div>
</div>
<div class="dialog" id="qqDialog">
<header>
<div class="title">QQ群</div>
<svg
onclick="toggleXhsDialog()"
viewBox="0 0 20 20" width="24" height="1.2em" class="cursor-pointer">
<path fill="currentColor"
d="m4.089 4.216l.057-.07a.5.5 0 0 1 .638-.057l.07.057L10 9.293l5.146-5.147a.5.5 0 0 1 .638-.057l.07.057a.5.5 0 0 1 .057.638l-.057.07L10.707 10l5.147 5.146a.5.5 0 0 1 .057.638l-.057.07a.5.5 0 0 1-.638.057l-.07-.057L10 10.707l-5.146 5.147a.5.5 0 0 1-.638.057l-.07-.057a.5.5 0 0 1-.057-.638l.057-.07L9.293 10L4.146 4.854a.5.5 0 0 1-.057-.638l.057-.07z"></path>
</svg>
</header>
<div class="dialog-body">
<span>加入我们的用户社群后,您可以与我们的开发团队进行沟通,分享您的使用体验和建议,帮助我们改进产品,同时也能够及时了解我们的最新动态和更新内容。</span>
<div class="center">
<img src="/qq.jpg" alt="QQ群二维码" class="img">
</div>
</div>
</div>
</div>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 KiB

49
scripts/do.js Normal file
View File

@@ -0,0 +1,49 @@
const {SitemapStream, streamToPromise} = require('sitemap')
const {createWriteStream} = require('fs')
const {resolve} = require('path')
const fs = require('fs')
async function generateSitemap() {
const bookList = require('../public/list/article.json')
const dictList = require('../public/list/word.json')
const SITE_URL = 'https://2study.top'
// 静态路由(首页、练习页等)
const staticPages = [
{url: '/', changefreq: 'daily', priority: 1.0},
{url: '/words', changefreq: 'daily', priority: 0.9},
{url: '/articles', changefreq: 'daily', priority: 0.9},
{url: '/setting', changefreq: 'monthly', priority: 0.3},
]
// 动态页面示例(假设你有文章或单词数据)
const dynamicPages = bookList.flat().map(book => {
return {url: '/practice-articles/' + book.id, changefreq: 'weekly', priority: 0.8}
}).concat(dictList.flat().map(book => {
return {url: '/practice-words/' + book.id, changefreq: 'weekly', priority: 0.8}
}))
const sitemap = new SitemapStream({hostname: SITE_URL})
const writeStream = createWriteStream(resolve(__dirname, '../dist/sitemap.xml'))
sitemap.pipe(writeStream)
// 添加静态页
staticPages.forEach(page => sitemap.write(page))
// 添加动态页
dynamicPages.forEach(page => sitemap.write(page))
sitemap.end()
await streamToPromise(sitemap)
console.log('✅ sitemap.xml 已生成在 dist 目录')
}
function renameHtml() {
//首页为了seo被剥离出去了现在是一个静态页面用nginx 重定向控制对应的跳转
fs.renameSync('dist/index.html', 'dist/app.html')
fs.renameSync('dist/static-home.html', 'dist/index.html')
}
generateSitemap()
renameHtml()

View File

@@ -1,42 +0,0 @@
const {SitemapStream, streamToPromise} = require('sitemap')
const {createWriteStream} = require('fs')
const {resolve} = require('path')
const bookList = require('../public/list/article.json')
const dictList = require('../public/list/word.json')
// 你的网站域名
const SITE_URL = 'https://2study.top'
// 静态路由(首页、练习页等)
const staticPages = [
{url: '/', changefreq: 'daily', priority: 1.0},
{url: '/words', changefreq: 'daily', priority: 0.9},
{url: '/articles', changefreq: 'daily', priority: 0.9},
{url: '/setting', changefreq: 'monthly', priority: 0.3},
]
// 动态页面示例(假设你有文章或单词数据)
const dynamicPages = bookList.flat().map(book => {
return {url: '/practice-articles/' + book.id, changefreq: 'weekly', priority: 0.8}
}).concat(dictList.flat().map(book => {
return {url: '/practice-words/' + book.id, changefreq: 'weekly', priority: 0.8}
}))
async function generateSitemap() {
const sitemap = new SitemapStream({hostname: SITE_URL})
const writeStream = createWriteStream(resolve(__dirname, '../dist/sitemap.xml'))
sitemap.pipe(writeStream)
// 添加静态页
staticPages.forEach(page => sitemap.write(page))
// 添加动态页
dynamicPages.forEach(page => sitemap.write(page))
sitemap.end()
await streamToPromise(sitemap)
console.log('✅ sitemap.xml 已生成在 dist 目录')
}
generateSitemap()

View File

@@ -71,6 +71,7 @@
--color-progress-bar: #d1d5df !important;
--color-label-bg: whitesmoke;
--color-link: rgb(64, 158, 255)
}
.footer {
@@ -213,8 +214,7 @@ html, body {
}
a {
$main: rgb(64, 158, 255);
color: $main;
color: var(--color-link);
text-decoration: none;
}

View File

@@ -7,7 +7,7 @@ interface IProps {
disabled?: boolean
loading?: boolean
size?: 'small' | 'normal' | 'large',
type?: 'primary' | 'link' | 'info'
type?: 'primary' | 'link' | 'info' | 'orange'
}
withDefaults(defineProps<IProps>(), {
@@ -97,8 +97,8 @@ defineEmits(['click'])
}
}
&:hover {
opacity: .8;
&:hover:not(.disabled) {
opacity: .6;
}
&.primary {
@@ -120,6 +120,11 @@ defineEmits(['click'])
color: var(--color-main-text);
}
&.orange {
background: #FACC15;
color: black;
}
&.active {
opacity: .4;
}

View File

@@ -1,189 +0,0 @@
<script setup lang="ts">
import Close from "@/components/icon/Close.vue";
import BaseButton from "@/components/BaseButton.vue";
import {watch} from "vue";
import {useSettingStore} from "@/stores/setting.ts";
import {isMobile} from "@/utils";
import {ProjectName, Host} from "@/config/env.ts";
let settingStore = useSettingStore()
let showNotice = $ref(false)
let show = $ref(false)
let num = $ref(5)
let timer = -1
let mobile = $ref(isMobile())
const isMac = /macintosh|mac os x/i.test(navigator.userAgent);
function toggleNotice() {
showNotice = true
settingStore.first = false
timer = setInterval(() => {
num--
if (num <= 0) close()
}, 1000)
}
function close() {
clearInterval(timer)
show = settingStore.first = false
}
watch(() => settingStore.load, (n) => {
if (n && settingStore.first) {
setTimeout(() => {
show = true
}, 1000)
}
}, {immediate: true})
</script>
<template>
<transition name="right">
<div class="CollectNotice"
:class="{mobile}"
v-if="show">
<div class="notice">
坚持练习提高外语能力
<span class="active">{{ ProjectName }}</span>
保存为书签永不迷失
</div>
<div class="wrapper">
<transition name="fade">
<div class="collect" v-if="showNotice">
<div class="href-wrapper">
<div class="round">
<div class="href">{{ Host }}</div>
<IconFluentStar12Regular width="22"/>
</div>
<div class="right">
👈
<IconFluentStar20Filled class="star" width="22"/>
点亮它!
</div>
</div>
<div class="collect-keyboard" v-if="!mobile">或使用收藏快捷键<span
class="active">{{ isMac ? 'Command' : 'Ctrl' }} + D</span></div>
</div>
<BaseButton v-else size="large" @click="toggleNotice">我想收藏</BaseButton>
</transition>
</div>
<div class="close-wrapper">
<span v-show="showNotice"><span class="active">{{ num }}s</span> 后自动关闭</span>
<Close @click="close" title="关闭"/>
</div>
</div>
</transition>
</template>
<style scoped lang="scss">
.right-enter-active,
.right-leave-active {
transition: all .5s ease;
}
.right-enter-from,
.right-leave-to {
transform: translateX(110%);
}
.CollectNotice {
position: fixed;
right: var(--space);
top: var(--space);
z-index: 2;
font-size: 1.2rem;
display: flex;
flex-direction: column;
align-items: center;
background: var(--color-notice-bg);
padding: 1.8rem;
border-radius: 0.7rem;
width: 30rem;
gap: 2.4rem;
color: var(--color-font-1);
line-height: 1.5;
border: 1px solid var(--color-item-border);
box-shadow: var(--shadow);
box-sizing: border-box;
&.mobile {
width: 95%;
padding: 0.6rem;
}
.notice {
margin-top: 2.4rem;
}
.active {
color: var(--color-select-bg);
}
.wrapper {
.collect {
display: flex;
flex-direction: column;
align-items: center;
.href-wrapper {
display: flex;
font-size: 1rem;
align-items: center;
gap: 0.6rem;
.round {
color: var(--color-font-1);
border-radius: 3rem;
padding: 0.6rem 0.6rem;
padding-left: 1.2rem;
gap: 2rem;
display: flex;
align-items: center;
justify-content: space-between;
background: var(--color-primary);
.href {
font-size: 0.9rem;
}
}
.star {
color: var(--color-select-bg);
}
.right {
display: flex;
align-items: center;
}
}
.collect-keyboard {
margin-top: 1.2rem;
font-size: 1rem;
span {
margin-left: 0.6rem;
}
}
}
}
.close-wrapper {
right: var(--space);
top: var(--space);
position: absolute;
font-size: 0.9rem;
display: flex;
justify-content: flex-end;
align-items: center;
color: var(--color-font-1);
gap: 0.6rem;
}
}
</style>

View File

@@ -8,14 +8,16 @@ interface IProps {
strokeWidth?: number;
color?: string;
format?: (percentage: number) => string;
size?: 'normal' | 'large';
}
const props = withDefaults(defineProps<IProps>(), {
showText: true,
textInside: false,
strokeWidth: 6,
color: '#93ADE3',
color: '#409eff',
format: (percentage) => `${percentage}%`,
size: 'normal',
});
const barStyle = computed(() => {
@@ -26,13 +28,15 @@ const barStyle = computed(() => {
});
const trackStyle = computed(() => {
const height = props.size === 'large' ? props.strokeWidth * 2.5 : props.strokeWidth;
return {
height: `${props.strokeWidth}px`,
height: `${height}px`,
};
});
const progressTextSize = computed(() => {
return props.strokeWidth * 0.83 + 6;
const baseSize = props.strokeWidth * 0.83 + 6;
return props.size === 'large' ? baseSize * 1.2 : baseSize;
});
const content = computed(() => {

View File

@@ -1,4 +1,4 @@
import {Article, TaskWords, Word, WordPracticeMode} from "@/types/types.ts";
import { Article, TaskWords, Word, WordPracticeMode } from "@/types/types.ts";
import { useBaseStore } from "@/stores/base.ts";
import { useSettingStore } from "@/stores/setting.ts";
import { getDefaultWord } from "@/types/func.ts";
@@ -87,7 +87,7 @@ export function useArticleOptions() {
export function getCurrentStudyWord(): TaskWords {
const store = useBaseStore()
let data = {new: [], review: [], write: []}
let data = {new: [], review: [], write: [], shuffle: []}
let dict = store.sdict;
let isTest = false
let words = dict.words.slice()

View File

@@ -215,7 +215,7 @@ const {data: recommendBookList, isFetching} = useFetch(resourceWrap(DICT_LIST.AR
</div>
<div class="flex flex-col justify-between items-end">
<div class="flex gap-4 items-center" v-opacity="base.sbook.id">
<div class="color-blue cursor-pointer" @click="router.push('/book-list')">更换</div>
<div class="color-link cursor-pointer" @click="router.push('/book-list')">更换</div>
</div>
<BaseButton size="large"
@click="startStudy"
@@ -238,10 +238,10 @@ const {data: recommendBookList, isFetching} = useFetch(resourceWrap(DICT_LIST.AR
</BaseIcon>
</PopConfirm>
<div class="color-blue cursor-pointer" v-if="base.article.bookList.length > 1"
<div class="color-link cursor-pointer" v-if="base.article.bookList.length > 1"
@click="isMultiple = !isMultiple; selectIds = []">{{ isMultiple ? '取消' : '管理书籍' }}
</div>
<div class="color-blue cursor-pointer" @click="nav('book-detail', { isAdd: true })">创建个人书籍</div>
<div class="color-link cursor-pointer" @click="nav('book-detail', { isAdd: true })">创建个人书籍</div>
</div>
</div>
<div class="flex gap-4 flex-wrap mt-4">
@@ -262,7 +262,7 @@ const {data: recommendBookList, isFetching} = useFetch(resourceWrap(DICT_LIST.AR
<div class="flex justify-between">
<div class="title">推荐</div>
<div class="flex gap-4 items-center">
<div class="color-blue cursor-pointer" @click="router.push('/book-list')">更多</div>
<div class="color-link cursor-pointer" @click="router.push('/book-list')">更多</div>
</div>
</div>
@@ -278,8 +278,7 @@ const {data: recommendBookList, isFetching} = useFetch(resourceWrap(DICT_LIST.AR
<style scoped lang="scss">
.stat {
@apply rounded-xl p-4 box-border relative flex-1;
background: white;
@apply rounded-xl p-4 box-border relative flex-1 bg-[var(--bg-history)];
border: 1px solid gainsboro;
.num {

View File

@@ -427,8 +427,8 @@ function onContextMenu(e: MouseEvent, sentence: Sentence, i, j, w) {
label: "收藏单词",
onClick: () => {
let word = props.article.sections[i][j].words[w]
let doc = nlp(word.word)
let text = word.word
let doc = nlp(text)
// 优先判断是不是动词
if (doc.verbs().found) {
text = doc.verbs().toInfinitive().text()

View File

@@ -1,239 +0,0 @@
<script setup lang="ts">
import {GITHUB, ProjectName} from "@/config/env.ts";
import BaseButton from "@/components/BaseButton.vue";
import BaseIcon from "@/components/BaseIcon.vue";
import {defineAsyncComponent} from "vue";
const Dialog = defineAsyncComponent(() => import('@/components/dialog/Dialog.vue'))
let showWechatDialog = $ref(false)
let showXhsDialog = $ref(false)
</script>
<template>
<div class="flex flex-col justify-between min-h-screen">
<div class="center flex-col gap-8">
<h1>{{ ProjectName }}</h1>
<div class="text-center -mt-10">
<h2>学习英语一次敲击一点进步</h2>
<h2>记忆不再盲目学习更高效开源单词与文章练习工具</h2>
</div>
<div class="flex">
<BaseButton size="large" @click="$router.push('/words')">单词练习</BaseButton>
<BaseButton size="large" @click="$router.push('/articles')">文章练习</BaseButton>
</div>
<div class="center justify-center flex-col gap-2 w-full mb-4">
<a href="https://skywork.ai/p/GrXQb4" class="w-60vw" target="_blank"><img src="/skywork-ai.png" alt="Skywork.AI" class="w-full rounded-lg"></a>
<span>Skywork.AI:<a href="https://skywork.ai/p/GrXQb4" class="color-blue!" target="_blank">10 tasks in 1 hour, not 10 hours Limited free spots: 127 left</a></span>
</div>
<div class="w-60vw">
<div class="flex mb-5 gap-space">
<div class="card">
<div class="emoji">📚</div>
<div class="title">单词练习</div>
<div class="desc">
<ul>
<li>三种输入模式跟打 / 复习 / 默写</li>
<li>智能模式智能规划复习与默写</li>
<li>自由模式不受限制自行规划</li>
</ul>
</div>
</div>
<div class="card">
<div class="emoji"></div>
<div class="title">文章练习</div>
<div class="desc">
<ul>
<li>内置常见书籍也可自行添加文章</li>
<li>跟打 + 默写双模式让背诵更高效</li>
<li>支持边听边默写强化记忆</li>
</ul>
</div>
</div>
<div class="card">
<div class="emoji">📕</div>
<div class="title">收藏错词本已掌握</div>
<div class="desc">
<ul>
<li>输入错误自动添加到错词本</li>
<li>主动添加到已掌握后续自动跳过</li>
<li>主动添加到收藏中以便巩固复习</li>
</ul>
</div>
</div>
<div class="card">
<div class="emoji">🌐</div>
<div class="title">海量词库</div>
<div class="desc">
内置小学初中高中四六级考研雅思托福GREGMATSATBEC专四专八等词库
</div>
</div>
</div>
<div class="flex gap-space">
<div class="card">
<div class="emoji">🆓</div>
<div class="title">免费开源</div>
<div class="desc">
<ul>
<li>完全开源可审查可修改</li>
<li>免费使用</li>
<li>私有部署</li>
</ul>
</div>
</div>
<div class="card">
<div class="emoji"></div>
<div class="title">高度自由</div>
<div class="desc">
<ul>
<li>丰富的键盘音效</li>
<li>可自定义快捷键</li>
<li>高度定制化的设置选项</li>
</ul>
</div>
</div>
<div class="card">
<div class="emoji">🎨</div>
<div class="title">简洁高效</div>
<div class="desc">
<ul>
<li>简洁设计现代化UI无广告</li>
<li>界面清爽操作简单</li>
<li>不强制关注任何平台</li>
</ul>
</div>
</div>
<div class="card">
<div class="emoji">🎯</div>
<div class="title">个性学习</div>
<div class="desc">
<ul>
<li>自由添加词典与文章</li>
<li>定制个性学习计划</li>
<li>多种学习复习策略</li>
</ul>
</div>
</div>
</div>
<div class="w-60vw text-center" v-if="false">
<h3 class="text-4xl">单词练习</h3>
<img src="/word.png" alt="word.png" class="w-full rounded-xl">
<h3 class="text-4xl">文章练习</h3>
<img src="/article.png" alt="article.png" class="w-full rounded-xl">
</div>
</div>
</div>
<div class="center gap-space my-10 bottom">
<div class="center gap-1">
<a
:href="GITHUB"
target="_blank"
rel="noreferrer"
aria-label="GITHUB 项目地址">
<BaseIcon>
<IconSimpleIconsGithub/>
</BaseIcon>
</a>
<BaseIcon @click="showWechatDialog = true">
<IconSimpleIconsWechat/>
</BaseIcon>
<BaseIcon @click="showXhsDialog = true" >
<IconSimpleIconsXiaohongshu/>
</BaseIcon>
<a
href="https://x.com/typewords2"
target="_blank"
rel="noreferrer"
aria-label="关注我的 X 账户 typewords2">
<BaseIcon>
<IconRiTwitterFill/>
</BaseIcon>
</a>
<a
href="mailto:zyronon@163.com"
target="_blank"
rel="noreferrer"
aria-label="发送邮件到 zyronon@163.com">
<BaseIcon>
<IconMaterialSymbolsMail/>
</BaseIcon>
</a>
</div>
<div>蜀ICP备2025157466号</div>
</div>
<Dialog v-model="showWechatDialog" title="Type Words 交流群">
<div class="w-120 p-6 pt-0">
<div class="mb-4">
加入我们的用户社群后您可以与我们的开发团队进行沟通分享您的使用体验和建议帮助我们改进产品同时也能够及时了解我们的最新动态和更新内容
</div>
<div class="text-center">
<img src="/wechat.png" alt="微信群二维码" class="w-60 rounded-lg">
</div>
</div>
</Dialog>
<Dialog v-model="showXhsDialog" title="小红书">
<div class="w-120 p-6 pt-0">
<div class="mb-4">
关注小红书后您可以获得开发团队的最新动态和更新内容反馈您的使用体验和建议帮助我们改进产品同时也能够及时了解我们的最新动态和更新内容
</div>
<div class="text-center">
<img src="/xhs.png" alt="小红书二维码" class="w-60 rounded-lg">
</div>
</div>
</Dialog>
</div>
</template>
<style scoped lang="scss">
h1 {
font-size: 5rem;
background: linear-gradient(120deg, #bd34fe 30%, #41d1ff);
-webkit-text-fill-color: transparent;
-webkit-background-clip: text;
background-clip: text;
color: transparent;
margin: 2rem;
}
h2 {
margin: 0;
}
h3:first-child {
margin-top: 0;
}
.card {
@apply flex flex-col items-start gap-2 mb-0 w-25%;
.emoji {
display: inline-block;
background: var(--color-third);
padding: .6rem;
border-radius: 0.4rem;
font-size: 1.5rem;
}
.title {
font-weight: bold;
}
ul {
margin: 0;
padding-left: 1.2rem;
}
}
.bottom {
width: 100%;
padding-top: 2rem;
border-top: 1px solid #c4c4c4;
}
a {
color: unset;
}
</style>

View File

@@ -8,13 +8,15 @@ import useTheme from "@/hooks/theme.ts";
import BaseIcon from "@/components/BaseIcon.vue";
import {useRuntimeStore} from "@/stores/runtime.ts";
const settingStore = useSettingStore()
const runtimeStore = useRuntimeStore()
const router = useRouter()
const {toggleTheme,getTheme} = useTheme()
const {toggleTheme, getTheme} = useTheme()
//seonginx
function goHome() {
window.location.href = '/';
}
</script>
<template>
@@ -24,7 +26,7 @@ const {toggleTheme,getTheme} = useTheme()
<div class="aside anim fixed" :class="{'expand':settingStore.sideExpand}">
<div class="top">
<Logo v-if="settingStore.sideExpand"/>
<div class="row" @click="router.push('/')">
<div class="row" @click="goHome">
<IconFluentHome20Regular/>
<span v-if="settingStore.sideExpand">主页</span>
</div>
@@ -42,21 +44,21 @@ const {toggleTheme,getTheme} = useTheme()
<span v-if="settingStore.sideExpand">设置</span>
<div class="red-point" :class="!settingStore.sideExpand && 'top-1 right-0'" v-if="runtimeStore.isNew"></div>
</div>
<!-- <div class="row" @click="router.push('/user')">-->
<!-- <IconFluentPerson20Regular/>-->
<!-- <span v-if="settingStore.sideExpand">用户</span>-->
<!-- </div>-->
<!-- <div class="row" @click="router.push('/user')">-->
<!-- <IconFluentPerson20Regular/>-->
<!-- <span v-if="settingStore.sideExpand">用户</span>-->
<!-- </div>-->
</div>
<div class="bottom flex justify-evenly ">
<BaseIcon
@click="settingStore.sideExpand = !settingStore.sideExpand">
@click="settingStore.sideExpand = !settingStore.sideExpand">
<IconFluentChevronLeft20Filled v-if="settingStore.sideExpand"/>
<IconFluentChevronLeft20Filled class="transform-rotate-180" v-else/>
</BaseIcon>
<BaseIcon
v-if="settingStore.sideExpand"
:title="`切换主题(${settingStore.shortcutKeyMap[ShortcutKey.ToggleTheme]})`"
@click="toggleTheme"
v-if="settingStore.sideExpand"
:title="`切换主题(${settingStore.shortcutKeyMap[ShortcutKey.ToggleTheme]})`"
@click="toggleTheme"
>
<IconFluentWeatherMoon16Regular v-if="getTheme() === 'light'"/>
<IconFluentWeatherSunny16Regular v-else/>

View File

@@ -4,7 +4,7 @@ import { useSettingStore } from "@/stores/setting.ts";
import { getAudioFileUrl, usePlayAudio } from "@/hooks/sound.ts";
import { getShortcutKey, useEventListener } from "@/hooks/event.ts";
import { checkAndUpgradeSaveDict, checkAndUpgradeSaveSetting, cloneDeep, loadJsLib, shakeCommonDict } from "@/utils";
import {DefaultShortcutKeyMap, ShortcutKey, WordPracticeMode} from "@/types/types.ts";
import { DefaultShortcutKeyMap, ShortcutKey, WordPracticeMode } from "@/types/types.ts";
import BaseButton from "@/components/BaseButton.vue";
import VolumeIcon from "@/components/icon/VolumeIcon.vue";
import { useBaseStore } from "@/stores/base.ts";
@@ -40,6 +40,7 @@ const tabIndex = $ref(0)
const settingStore = useSettingStore()
const runtimeStore = useRuntimeStore()
const store = useBaseStore()
//@ts-ignore
const gitLastCommitHash = ref(LATEST_COMMIT_HASH);
const simpleWords = $computed({
@@ -679,7 +680,23 @@ function importOldData() {
</div>
<div v-if="tabIndex === 5">
<div class="item p-2">
<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>
@@ -730,9 +747,16 @@ function importOldData() {
<div>通过引入复习默写两种模式使复习流程更加灵活高效</div>
</div>
</div>
<div class="line"></div>
</div>
<div class="item p-2">
<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>
@@ -744,7 +768,6 @@ function importOldData() {
<div>3单词可导入导出</div>
</div>
</div>
<div class="line"></div>
</div>
</div>
@@ -775,6 +798,11 @@ function importOldData() {
<style scoped lang="scss">
.log-item{
border-bottom: 1px solid var(--color-input-border);
margin-bottom: 1rem;
}
.setting {
@apply text-lg;
display: flex;

View File

@@ -237,7 +237,7 @@ async function startPractice() {
wordPracticeMode: settingStore.wordPracticeMode
})
let currentStudy = getCurrentStudyWord()
nav('practice-words/' + store.sdict.id, {}, currentStudy)
nav('practice-words/' + store.sdict.id, {}, {taskWords:currentStudy})
}
async function addMyStudyList() {

View File

@@ -1,6 +1,5 @@
<script setup lang="ts">
import {onMounted, provide, ref, watch} from "vue";
import {onMounted, provide, ref, toRef, watch} from "vue";
import Statistics from "@/pages/word/Statistics.vue";
import {emitter, EventKey, useEvents} from "@/utils/eventBus.ts";
@@ -50,6 +49,7 @@ let taskWords = $ref<TaskWords>({
new: [],
review: [],
write: [],
shuffle: [],
})
let data = $ref<PracticeData>({
@@ -60,10 +60,9 @@ let data = $ref<PracticeData>({
})
let isTypingWrongWord = ref(false)
let practiceMode = ref(WordPracticeType.FollowWrite)
provide('isTypingWrongWord', isTypingWrongWord)
provide('practiceData', data)
provide('practiceMode', practiceMode)
provide('practiceTaskWords', taskWords)
async function loadDict() {
// console.log('load好了开始加载')
@@ -100,7 +99,7 @@ watch(() => store.load, (n) => {
onMounted(() => {
//如果是从单词学习主页过来的,就直接使用;否则等待加载
if (runtimeStore.routeData) {
initData(runtimeStore.routeData, true)
initData(runtimeStore.routeData.taskWords, true)
} else {
loading = true
}
@@ -124,27 +123,45 @@ function initData(initVal: TaskWords, init: boolean = false) {
initData(initVal, true)
}
} else {
taskWords = initVal
if (taskWords.new.length === 0) {
if (taskWords.review.length) {
settingStore.wordPracticeType = WordPracticeType.Identify
statStore.step = 3
data.words = taskWords.review
} else {
if (taskWords.write.length) {
// taskWords = initVal
//不能直接赋值,会导致 inject 的数据为默认值
taskWords = Object.assign(taskWords, initVal)
//如果 shuffle 数组不为空,就说明是复习
if (taskWords.shuffle.length === 0) {
if (taskWords.new.length === 0) {
if (taskWords.review.length) {
settingStore.wordPracticeType = WordPracticeType.Identify
data.words = taskWords.write
statStore.step = 6
statStore.step = 3
data.words = taskWords.review
} else {
Toast.warning('没有可学习的单词!')
router.push('/word')
if (taskWords.write.length) {
settingStore.wordPracticeType = WordPracticeType.Identify
data.words = taskWords.write
statStore.step = 6
} else {
Toast.warning('没有可学习的单词!')
router.push('/word')
}
}
} else {
settingStore.wordPracticeType = WordPracticeType.FollowWrite
data.words = taskWords.new
statStore.step = 0
}
statStore.total = taskWords.review.length + taskWords.new.length + taskWords.write.length
statStore.newWordNumber = taskWords.new.length
statStore.reviewWordNumber = taskWords.review.length
statStore.writeWordNumber = taskWords.write.length
} else {
settingStore.wordPracticeType = WordPracticeType.FollowWrite
data.words = taskWords.new
statStore.step = 0
settingStore.wordPracticeType = WordPracticeType.Dictation
data.words = taskWords.shuffle
statStore.step = 10
statStore.total = taskWords.shuffle.length
statStore.newWordNumber = 0
statStore.reviewWordNumber = 0
statStore.writeWordNumber = statStore.total
}
data.index = 0
data.wrongWords = []
data.excludeWords = []
@@ -152,11 +169,6 @@ function initData(initVal: TaskWords, init: boolean = false) {
statStore.startDate = Date.now()
statStore.inputWordNumber = 0
statStore.wrong = 0
statStore.total = taskWords.review.length + taskWords.new.length + taskWords.write.length
statStore.newWordNumber = taskWords.new.length
statStore.reviewWordNumber = taskWords.review.length
statStore.writeWordNumber = taskWords.write.length
statStore.index = 0
isTypingWrongWord.value = false
}
}
@@ -210,6 +222,8 @@ function wordLoop() {
}
}
let toastInstance: ToastInstance = null
function goNextStep(originList, mode, msg) {
//每次都判断,因为每次都可能新增已掌握的单词
let list = originList.filter(v => (!data.excludeWords.includes(v.word)))
@@ -228,8 +242,6 @@ function goNextStep(originList, mode, msg) {
}
}
let toastInstance: ToastInstance = null
async function next(isTyping: boolean = true) {
if (isTyping) {
statStore.inputWordNumber++
@@ -244,7 +256,7 @@ async function next(isTyping: boolean = true) {
data.words = shuffle(cloneDeep(data.wrongWords))
data.index = 0
data.wrongWords = []
}else {
} else {
console.log('自由模式,全完学完了')
showStatDialog = true
localStorage.removeItem(PracticeSaveWordKey.key)
@@ -388,7 +400,7 @@ function onKeyUp(e: KeyboardEvent) {
typingRef.hideWord()
}
async function onKeyDown(e: KeyboardEvent) {
function onKeyDown(e: KeyboardEvent) {
// console.log('onKeyDown', e)
switch (e.key) {
case 'Backspace':
@@ -401,21 +413,27 @@ useOnKeyboardEventListener(onKeyDown, onKeyUp)
function repeat() {
console.log('重学一遍')
if (settingStore.wordPracticeMode === WordPracticeMode.System) settingStore.dictation = false
if (store.sdict.lastLearnIndex === 0 && store.sdict.complete) {
//如果是刚刚完成那么学习进度要从length减回去因为lastLearnIndex为0了同时改complete为false
store.sdict.lastLearnIndex = store.sdict.length - statStore.newWordNumber
store.sdict.complete = false
let temp = cloneDeep(taskWords)
let ignoreList = [store.allIgnoreWords, store.knownWords][settingStore.ignoreSimpleWord ? 0 : 1]
//随机练习单独处理
if (taskWords.shuffle.length) {
temp.shuffle = shuffle(temp.shuffle.filter(v => !ignoreList.includes(v.word)))
} else {
//将学习进度减回去
store.sdict.lastLearnIndex = store.sdict.lastLearnIndex - statStore.newWordNumber
if (settingStore.wordPracticeMode === WordPracticeMode.System) settingStore.dictation = false
if (store.sdict.lastLearnIndex === 0 && store.sdict.complete) {
//如果是刚刚完成那么学习进度要从length减回去因为lastLearnIndex为0了同时改complete为false
store.sdict.lastLearnIndex = store.sdict.length - statStore.newWordNumber
store.sdict.complete = false
} else {
//将学习进度减回去
store.sdict.lastLearnIndex = store.sdict.lastLearnIndex - statStore.newWordNumber
}
//排除已掌握单词
temp.new = temp.new.filter(v => !ignoreList.includes(v.word))
temp.review = temp.review.filter(v => !ignoreList.includes(v.word))
temp.write = temp.write.filter(v => !ignoreList.includes(v.word))
}
emitter.emit(EventKey.resetWord)
let temp = cloneDeep(taskWords)
//排除已掌握单词
temp.new = temp.new.filter(v => !store.knownWords.includes(v.word))
temp.review = temp.review.filter(v => !store.knownWords.includes(v.word))
temp.write = temp.write.filter(v => !store.knownWords.includes(v.word))
initData(temp)
}
@@ -477,16 +495,26 @@ function togglePanel() {
}
function continueStudy() {
if (settingStore.wordPracticeMode === WordPracticeMode.System) settingStore.dictation = false
//这里判断是否显示结算弹框,如果显示了结算弹框的话,就不用加进度了
if (!showStatDialog) {
console.log('没学完,强行跳过')
store.sdict.lastLearnIndex = store.sdict.lastLearnIndex + statStore.newWordNumber
let temp = cloneDeep(taskWords)
//随机练习单独处理
if (taskWords.shuffle.length) {
let ignoreList = [store.allIgnoreWords, store.knownWords][settingStore.ignoreSimpleWord ? 0 : 1]
temp.shuffle = shuffle(store.sdict.words.filter(v => !ignoreList.includes(v.word))).slice(0, runtimeStore.routeData.total)
if (showStatDialog) showStatDialog = false
} else {
console.log('学完了,正常下一组')
showStatDialog = false
if (settingStore.wordPracticeMode === WordPracticeMode.System) settingStore.dictation = false
//这里判断是否显示结算弹框,如果显示了结算弹框的话,就不用加进度了
if (!showStatDialog) {
console.log('没学完,强行跳过')
store.sdict.lastLearnIndex = store.sdict.lastLearnIndex + statStore.newWordNumber
} else {
console.log('学完了,正常下一组')
showStatDialog = false
}
temp = getCurrentStudyWord()
}
initData(getCurrentStudyWord())
emitter.emit(EventKey.resetWord)
initData(temp)
}
function randomWrite() {
@@ -533,8 +561,8 @@ useEvents([
<template>
<PracticeLayout
v-loading="loading"
panelLeft="var(--word-panel-margin-left)">
v-loading="loading"
panelLeft="var(--word-panel-margin-left)">
<template v-slot:practice>
<div class="practice-word">
<div class="absolute z-1 top-4 w-full" v-if="settingStore.showNearWord">
@@ -543,16 +571,16 @@ useEvents([
v-if="prevWord">
<IconFluentArrowLeft16Regular class="arrow" width="22"/>
<Tooltip
:title="`上一个(${settingStore.shortcutKeyMap[ShortcutKey.Previous]})`"
:title="`上一个(${settingStore.shortcutKeyMap[ShortcutKey.Previous]})`"
>
<div class="word">{{ prevWord.word }}</div>
</Tooltip>
</div>
<div class="center gap-2 cursor-pointer float-right "
<div class="center gap-2 cursor-pointer float-right mr-3"
@click="next(false)"
v-if="nextWord">
<Tooltip
:title="`下一个(${settingStore.shortcutKeyMap[ShortcutKey.Next]})`"
:title="`下一个(${settingStore.shortcutKeyMap[ShortcutKey.Next]})`"
>
<div class="word" :class="settingStore.dictation && 'word-shadow'">{{ nextWord.word }}</div>
</Tooltip>
@@ -560,11 +588,11 @@ useEvents([
</div>
</div>
<TypeWord
ref="typingRef"
:word="word"
@wrong="onTypeWrong"
@complete="next"
@know="onWordKnow"
ref="typingRef"
:word="word"
@wrong="onTypeWrong"
@complete="next"
@know="onWordKnow"
/>
</div>
</template>
@@ -576,41 +604,41 @@ useEvents([
<span>{{ store.sdict.name }} ({{ store.sdict.lastLearnIndex }} / {{ store.sdict.length }})</span>
<BaseIcon
@click="continueStudy"
:title="`下一组(${settingStore.shortcutKeyMap[ShortcutKey.NextChapter]})`">
@click="continueStudy"
:title="`下一组(${settingStore.shortcutKeyMap[ShortcutKey.NextChapter]})`">
<IconFluentArrowRight16Regular class="arrow" width="22"/>
</BaseIcon>
<BaseIcon
@click="randomWrite"
:title="`随机默写(${settingStore.shortcutKeyMap[ShortcutKey.RandomWrite]})`">
@click="randomWrite"
:title="`随机默写(${settingStore.shortcutKeyMap[ShortcutKey.RandomWrite]})`">
<IconFluentArrowShuffle16Regular class="arrow" width="22"/>
</BaseIcon>
</div>
</template>
<div class="panel-page-item pl-4">
<WordList
v-if="data.words.length"
:is-active="settingStore.showPanel"
:static="false"
:show-word="!settingStore.dictation"
:show-translate="settingStore.translate"
:list="data.words"
:activeIndex="data.index"
@click="(val:any) => data.index = val.index"
v-if="data.words.length"
:is-active="settingStore.showPanel"
:static="false"
:show-word="!settingStore.dictation"
:show-translate="settingStore.translate"
:list="data.words"
:activeIndex="data.index"
@click="(val:any) => data.index = val.index"
>
<template v-slot:suffix="{item,index}">
<BaseIcon
:class="!isWordCollect(item)?'collect':'fill'"
@click.stop="toggleWordCollect(item)"
:title="!isWordCollect(item) ? '收藏' : '取消收藏'">
:class="!isWordCollect(item)?'collect':'fill'"
@click.stop="toggleWordCollect(item)"
:title="!isWordCollect(item) ? '收藏' : '取消收藏'">
<IconFluentStar16Regular v-if="!isWordCollect(item)"/>
<IconFluentStar16Filled v-else/>
</BaseIcon>
<BaseIcon
:class="!isWordSimple(item)?'collect':'fill'"
@click.stop="toggleWordSimple(item)"
:title="!isWordSimple(item) ? '标记为已掌握' : '取消标记已掌握'">
:class="!isWordSimple(item)?'collect':'fill'"
@click.stop="toggleWordSimple(item)"
:title="!isWordSimple(item) ? '标记为已掌握' : '取消标记已掌握'">
<IconFluentCheckmarkCircle16Regular v-if="!isWordSimple(item)"/>
<IconFluentCheckmarkCircle16Filled v-else/>
</BaseIcon>
@@ -622,11 +650,11 @@ useEvents([
</template>
<template v-slot:footer>
<Footer
:is-simple="isWordSimple(word)"
@toggle-simple="toggleWordSimpleWrapper"
:is-collect="isWordCollect(word)"
@toggle-collect="toggleWordCollect(word)"
@skip="next(false)"
:is-simple="isWordSimple(word)"
@toggle-simple="toggleWordSimpleWrapper"
:is-collect="isWordCollect(word)"
@toggle-collect="toggleWordCollect(word)"
@skip="next(false)"
/>
</template>
</PracticeLayout>

View File

@@ -1,26 +1,27 @@
<script setup lang="ts">
import {useBaseStore} from "@/stores/base.ts";
import BaseButton from "@/components/BaseButton.vue";
import {ShortcutKey, Statistics} from "@/types/types.ts";
import {PracticeData, ShortcutKey, Statistics, TaskWords, WordPracticeMode} from "@/types/types.ts";
import {emitter, EventKey, useEvents} from "@/utils/eventBus.ts";
import {useSettingStore} from "@/stores/setting.ts";
import {usePracticeStore} from "@/stores/practice.ts";
import dayjs from "dayjs";
import isBetween from "dayjs/plugin/isBetween";
import {defineAsyncComponent, watch} from "vue";
import {defineAsyncComponent, inject, watch} from "vue";
import isoWeek from 'dayjs/plugin/isoWeek'
import {msToHourMinute, msToMinute} from "@/utils";
dayjs.extend(isoWeek)
dayjs.extend(isBetween);
const Dialog = defineAsyncComponent(() => import('@/components/dialog/Dialog.vue'))
const store = useBaseStore()
const settingStore = useSettingStore()
const statStore = usePracticeStore()
const model = defineModel({default: false})
let list = $ref([])
let dictIsEnd = $ref(false)
let practiceTaskWords = inject<TaskWords>('practiceTaskWords')
function calcWeekList() {
// 获取本周的起止时间
@@ -68,12 +69,16 @@ watch(model, (newVal) => {
complete: store.sdict.complete,
str: `name:${store.sdict.name},per:${store.sdict.perDayStudyNumber},spend:${Number(statStore.spend / 1000 / 60).toFixed(1)},index:${store.sdict.lastLearnIndex}`
})
store.sdict.lastLearnIndex = store.sdict.lastLearnIndex + statStore.newWordNumber
if (store.sdict.lastLearnIndex >= store.sdict.length) {
dictIsEnd = true;
store.sdict.complete = true
store.sdict.lastLearnIndex = 0
//如果 shuffle 数组不为空,就说明是复习,不用修改 lastLearnIndex
if (!practiceTaskWords.shuffle.length) {
store.sdict.lastLearnIndex = store.sdict.lastLearnIndex + statStore.newWordNumber
if (store.sdict.lastLearnIndex >= store.sdict.length) {
dictIsEnd = true;
store.sdict.complete = true
store.sdict.lastLearnIndex = 0
}
}
store.sdict.statistics.push(data as any)
calcWeekList(); // 新增:计算本周学习记录
}
@@ -97,33 +102,41 @@ function options(emitType: string) {
<template>
<Dialog
:close-on-click-bg="false"
:header="false"
:keyboard="false"
:show-close="false"
v-model="model">
:close-on-click-bg="false"
:header="false"
:keyboard="false"
:show-close="false"
v-model="model">
<div class="w-140 bg-white color-black p-6 relative flex flex-col gap-6">
<div class="w-full flex flex-col justify-evenly">
<div class="center text-2xl mb-2">已完成今日任务</div>
<div class="center text-2xl mb-2">已完成{{ practiceTaskWords.shuffle.length ? '随机复习' : '今日任务' }}</div>
<div class="flex">
<div class="flex-1 flex flex-col items-center">
<div class="text-sm color-gray">新词数</div>
<div class="text-4xl font-bold">{{ statStore.newWordNumber }}</div>
</div>
<div class="flex-1 flex flex-col items-center">
<div class="text-sm color-gray">复习上次</div>
<div class="text-4xl font-bold">{{ statStore.reviewWordNumber }}</div>
</div>
<div class="flex-1 flex flex-col items-center">
<div class="text-sm color-gray">复习之前</div>
<div class="text-4xl font-bold">{{ statStore.writeWordNumber }}</div>
<div v-if="practiceTaskWords.shuffle.length"
class="flex-1 flex flex-col items-center">
<div class="text-sm color-gray">随机复习</div>
<div class="text-4xl font-bold">{{ practiceTaskWords.shuffle.length }}</div>
</div>
<template v-else>
<div class="flex-1 flex flex-col items-center">
<div class="text-sm color-gray">新词数</div>
<div class="text-4xl font-bold">{{ statStore.newWordNumber }}</div>
</div>
<template v-if="settingStore.wordPracticeMode !== WordPracticeMode.Free">
<div class="flex-1 flex flex-col items-center">
<div class="text-sm color-gray">复习上次</div>
<div class="text-4xl font-bold">{{ statStore.reviewWordNumber }}</div>
</div>
<div class="flex-1 flex flex-col items-center">
<div class="text-sm color-gray">复习之前</div>
<div class="text-4xl font-bold">{{ statStore.writeWordNumber }}</div>
</div>
</template>
</template>
</div>
</div>
<div class="text-xl text-center flex flex-col justify-around">
<div>非常棒! 坚持了 <span class="color-emerald-500 font-bold text-2xl">
{{ dayjs().diff(statStore.startDate, 'm') }}</span>分钟
<div>非常棒! 坚持了 <span class="color-emerald-500 font-bold text-2xl">{{msToHourMinute(statStore.spend) }}</span>
</div>
</div>
<div class="flex justify-center gap-10">
@@ -149,29 +162,29 @@ function options(emitType: string) {
<div class="title text-align-center mb-2">本周学习记录</div>
<div class="flex gap-4 color-gray">
<div
class="w-8 h-8 rounded-md center"
:class="item ? 'bg-emerald-500 color-white' : 'bg-gray-200'"
v-for="(item, i) in list"
:key="i"
class="w-8 h-8 rounded-md center"
:class="item ? 'bg-emerald-500 color-white' : 'bg-gray-200'"
v-for="(item, i) in list"
:key="i"
>{{ i + 1 }}
</div>
</div>
</div>
<div class="flex justify-center gap-4 ">
<BaseButton
:keyboard="settingStore.shortcutKeyMap[ShortcutKey.RepeatChapter]"
@click="options(EventKey.repeatStudy)">
:keyboard="settingStore.shortcutKeyMap[ShortcutKey.RepeatChapter]"
@click="options(EventKey.repeatStudy)">
重学一遍
</BaseButton>
<BaseButton
:keyboard="settingStore.shortcutKeyMap[ShortcutKey.NextChapter]"
@click="options(EventKey.continueStudy)">
{{ dictIsEnd ? '重新练习' : '再来一组' }}
:keyboard="settingStore.shortcutKeyMap[ShortcutKey.NextChapter]"
@click="options(EventKey.continueStudy)">
{{ dictIsEnd ? '从头开始练习' : '再来一组' }}
</BaseButton>
<BaseButton
:keyboard="settingStore.shortcutKeyMap[ShortcutKey.NextRandomWrite]"
@click="options(EventKey.randomWrite)">
继续默写
:keyboard="settingStore.shortcutKeyMap[ShortcutKey.NextRandomWrite]"
@click="options(EventKey.randomWrite)">
继续默写
</BaseButton>
<BaseButton @click="$router.back">
返回主页
@@ -182,7 +195,4 @@ function options(emitType: string) {
</div>
</div>
</Dialog>
</template>
<style scoped lang="scss">
</style>
</template>

View File

@@ -1,28 +1,28 @@
<script setup lang="ts">
import { useBaseStore } from "@/stores/base.ts";
import { useRouter } from "vue-router";
import {useBaseStore} from "@/stores/base.ts";
import {useRouter} from "vue-router";
import BaseIcon from "@/components/BaseIcon.vue";
import { _getAccomplishDate, _getDictDataByUrl, resourceWrap, useNav } from "@/utils";
import {_getAccomplishDate, _getDictDataByUrl, resourceWrap, shuffle, useNav} from "@/utils";
import BasePage from "@/components/BasePage.vue";
import {DictResource, WordPracticeMode} from "@/types/types.ts";
import { watch } from "vue";
import { getCurrentStudyWord } from "@/hooks/dict.ts";
import { useRuntimeStore } from "@/stores/runtime.ts";
import {watch} from "vue";
import {getCurrentStudyWord} from "@/hooks/dict.ts";
import {useRuntimeStore} from "@/stores/runtime.ts";
import Book from "@/components/Book.vue";
import PopConfirm from "@/components/PopConfirm.vue";
import Progress from '@/components/base/Progress.vue';
import Toast from '@/components/base/toast/Toast.ts';
import BaseButton from "@/components/BaseButton.vue";
import { getDefaultDict } from "@/types/func.ts";
import {getDefaultDict} from "@/types/func.ts";
import DeleteIcon from "@/components/icon/DeleteIcon.vue";
import PracticeSettingDialog from "@/pages/word/components/PracticeSettingDialog.vue";
import ChangeLastPracticeIndexDialog from "@/pages/word/components/ChangeLastPracticeIndexDialog.vue";
import { useSettingStore } from "@/stores/setting.ts";
import CollectNotice from "@/components/CollectNotice.vue";
import { useFetch } from "@vueuse/core";
import { CAN_REQUEST, DICT_LIST, PracticeSaveWordKey } from "@/config/env.ts";
import { myDictList } from "@/apis";
import {useSettingStore} from "@/stores/setting.ts";
import {useFetch} from "@vueuse/core";
import {CAN_REQUEST, DICT_LIST, PracticeSaveWordKey} from "@/config/env.ts";
import {myDictList} from "@/apis";
import PracticeWordListDialog from "@/pages/word/components/PracticeWordListDialog.vue";
import ShufflePracticeSettingDialog from "@/pages/word/components/ShufflePracticeSettingDialog.vue";
const store = useBaseStore()
@@ -35,7 +35,8 @@ let isSaveData = $ref(false)
let currentStudy = $ref({
new: [],
review: [],
write: []
write: [],
shuffle: [],
})
watch(() => store.load, n => {
@@ -85,7 +86,7 @@ function startPractice() {
complete: store.sdict.complete,
wordPracticeMode: settingStore.wordPracticeMode
})
nav('practice-words/' + store.sdict.id, {}, currentStudy)
nav('practice-words/' + store.sdict.id, {}, {taskWords: currentStudy})
} else {
window.umami?.track('no-dict')
Toast.warning('请先选择一本词典')
@@ -93,15 +94,17 @@ function startPractice() {
}
let showPracticeSettingDialog = $ref(false)
let showShufflePracticeSettingDialog = $ref(false)
let showChangeLastPracticeIndexDialog = $ref(false)
let showPracticeWordListDialog = $ref(false)
async function goDictDetail(val: DictResource) {
if (!val.id) return nav('dict-list')
runtimeStore.editDict = getDefaultDict(val)
nav('dict-detail', {})
}
let isMultiple = $ref(false)
let isManageDict = $ref(false)
let selectIds = $ref([])
function handleBatchDel() {
@@ -156,6 +159,26 @@ async function savePracticeSetting() {
currentStudy = getCurrentStudyWord()
}
async function onShufflePracticeSettingOk(total) {
window.umami?.track('startShuffleStudyWord', {
name: store.sdict.name,
index: store.sdict.lastLearnIndex,
perDayStudyNumber: store.sdict.perDayStudyNumber,
total,
custom: store.sdict.custom,
complete: store.sdict.complete,
})
isSaveData = false
localStorage.removeItem(PracticeSaveWordKey.key)
let ignoreList = [store.allIgnoreWords, store.knownWords][settingStore.ignoreSimpleWord ? 0 : 1]
currentStudy.shuffle = shuffle(store.sdict.words.slice(0, store.sdict.lastLearnIndex).filter(v => !ignoreList.includes(v.word))).slice(0, total)
nav('practice-words/' + store.sdict.id, {}, {
taskWords: currentStudy,
total //用于再来一组时,随机出正确的长度,因为练习中可能会点击已掌握,导致重学一遍之后长度变少,如果再来一组,此时长度就不正确
})
}
async function saveLastPracticeIndex(e) {
Toast.success('修改成功')
runtimeStore.editDict.lastLearnIndex = e
@@ -171,93 +194,127 @@ const {
isFetching
} = useFetch(resourceWrap(DICT_LIST.WORD.RECOMMENDED)).json()
</script>
<template>
<BasePage>
<div class="card flex gap-10">
<div class="flex-1 flex flex-col gap-2">
<div class="flex">
<div class="bg-third px-3 h-14 rounded-md flex items-center ">
<span @click="goDictDetail(store.sdict)"
class="text-lg font-bold cursor-pointer">{{ store.sdict.name || '请选择词典开始学习' }}</span>
<BaseIcon title="切换词典"
class="ml-4"
@click="router.push('/dict-list')"
<div class="card flex gap-8">
<div class="flex-1 flex flex-col justify-between">
<div class="flex gap-3">
<div class="p-1 center rounded-full bg-white">
<IconFluentBookNumber20Filled class="text-xl color-link"/>
</div>
<div
@click="goDictDetail(store.sdict)"
class="text-2xl font-bold cursor-pointer">
{{ store.sdict.name || '请选择词典开始学习' }}
</div>
</div>
<div class="mt-4 flex flex-col gap-2">
<div class="">当前进度{{ progressTextLeft }}</div>
<Progress size="large" :percentage="store.currentStudyProgress" :show-text="false"></Progress>
<div class="text-sm flex justify-between">
<span>已完成 {{ progressTextRight }} / {{ store.sdict.words.length }} </span>
<span v-if="store.sdict.id">
预计完成日期{{ _getAccomplishDate(store.sdict.words.length, store.sdict.perDayStudyNumber) }}
</span>
</div>
</div>
<div class="flex mt-4 gap-4">
<BaseButton type="info" @click="router.push('/dict-list')">
<div class="center gap-1">
<IconFluentArrowSwap20Regular/>
<span>{{ store.sdict.name ? '切换' : '选择' }}词典</span>
</div>
</BaseButton>
<PopConfirm
:disabled="!isSaveData"
title="当前存在未完成的学习任务,修改会重新生成学习任务,是否继续?"
@confirm="check(()=>showChangeLastPracticeIndexDialog = true)">
<BaseButton type="info"
v-if="store.sdict.id"
>
<IconFluentArrowSort20Regular v-if="store.sdict.name"/>
<IconFluentAdd20Filled v-else/>
</BaseIcon>
</div>
</div>
<div class="flex items-end gap-space">
<div class="flex-1">
<div class="text-sm flex justify-between">
<span>{{ progressTextLeft }}</span>
<span>{{ progressTextRight }} / {{ store.sdict.words.length }}</span>
</div>
<Progress class="mt-1" :percentage="store.currentStudyProgress" :show-text="false"></Progress>
</div>
<PopConfirm
:disabled="!isSaveData"
title="当前存在未完成的学习任务,修改会重新生成学习任务,是否继续?"
@confirm="check(()=>showChangeLastPracticeIndexDialog = true)">
<div class="color-blue cursor-pointer">更改</div>
</PopConfirm>
</div>
<div class="text-sm text-align-end">
预计完成日期{{ _getAccomplishDate(store.sdict.words.length, store.sdict.perDayStudyNumber) }}
</div>
</div>
<div class="w-3/10 flex flex-col justify-evenly">
<div class="center gap-2">
<span class="text-xl">{{ isSaveData ? '上次学习任务' : '今日任务' }}</span>
<span class="color-blue cursor-pointer" @click="showPracticeWordListDialog = true">词表</span>
</div>
<div class="flex">
<div class="flex-1 flex flex-col items-center">
<div class="text-4xl font-bold">{{ currentStudy.new.length }}</div>
<div class="text">新词</div>
</div>
<template v-if="settingStore.wordPracticeMode === WordPracticeMode.System">
<div class="flex-1 flex flex-col items-center">
<div class="text-4xl font-bold">{{ currentStudy.review.length }}</div>
<div class="text">复习上次</div>
</div>
<div class="flex-1 flex flex-col items-center">
<div class="text-4xl font-bold">{{ currentStudy.write.length }}
<div class="center gap-1">
<IconFluentSlideTextTitleEdit20Regular/>
<span>更改进度</span>
</div>
<div class="text">复习之前</div>
</div>
</template>
</BaseButton>
</PopConfirm>
</div>
</div>
<div class="flex flex-col items-end justify-around ">
<div class="flex gap-1 items-center">
每日目标
<div style="color:#ac6ed1;"
class="bg-third px-2 h-10 flex center text-2xl rounded">
{{ store.sdict.id ? store.sdict.perDayStudyNumber : 0 }}
<div class="flex-1">
<div class="flex justify-between">
<div class="flex items-center gap-3">
<div class="p-2 center rounded-full bg-white ">
<IconFluentStar20Filled class="text-lg color-amber"/>
</div>
<div class="text-xl font-bold">
{{ isSaveData ? '上次学习任务' : '今日任务' }}
</div>
<span class="color-link cursor-pointer"
v-if="store.sdict.id"
@click="showPracticeWordListDialog = true">词表</span>
</div>
个单词
<PopConfirm
<div class="flex gap-1 items-center"
v-if="store.sdict.id"
>
每日目标
<div style="color:#ac6ed1;"
class="bg-third px-2 h-10 flex center text-2xl rounded">
{{ store.sdict.id ? store.sdict.perDayStudyNumber : 0 }}
</div>
个单词
<PopConfirm
:disabled="!isSaveData"
title="当前存在未完成的学习任务,修改会重新生成学习任务,是否继续?"
@confirm="check(()=>showPracticeSettingDialog = true)">
<span class="color-blue cursor-pointer">更改</span>
</PopConfirm>
</div>
<BaseButton size="large" :disabled="!store.sdict.name"
:loading="loading"
@click="startPractice">
<div class="flex items-center gap-2">
<span class="line-height-[2]">{{ isSaveData ? '继续学习' : '开始学习' }}</span>
<IconFluentArrowCircleRight16Regular class="text-xl"/>
<BaseButton
type="info" size="small">更改
</BaseButton>
</PopConfirm>
</div>
</BaseButton>
</div>
<div class="flex mt-4 justify-between">
<div class="stat">
<div class="num">{{ currentStudy.new.length }}</div>
<div class="txt">新词数</div>
</div>
<template v-if="settingStore.wordPracticeMode === WordPracticeMode.System">
<div class="stat">
<div class="num">{{ currentStudy.review.length }}</div>
<div class="txt">复习上次</div>
</div>
<div class="stat">
<div class="num">{{ currentStudy.write.length }}</div>
<div class="txt">复习之前</div>
</div>
</template>
</div>
<div class="flex items-end mt-4">
<BaseButton size="large"
class="flex-1"
:disabled="!store.sdict.id"
:loading="loading"
@click="startPractice">
<div class="flex items-center gap-2">
<span class="line-height-[2]">{{ isSaveData ? '继续学习' : '开始学习' }}</span>
<IconFluentArrowCircleRight16Regular class="text-xl"/>
</div>
</BaseButton>
<BaseButton
v-if="store.sdict.id && store.sdict.lastLearnIndex"
size="large" type="orange"
:loading="loading"
@click="check(()=>showShufflePracticeSettingDialog = true)">
<div class="flex items-center gap-2">
<span class="line-height-[2]">随机复习</span>
<IconFluentArrowShuffle20Filled class="text-xl"/>
</div>
</BaseButton>
</div>
</div>
</div>
@@ -271,15 +328,15 @@ const {
</BaseIcon>
</PopConfirm>
<div class="color-blue cursor-pointer" v-if="store.word.bookList.length > 3"
@click="isMultiple = !isMultiple; selectIds = []">{{ isMultiple ? '取消' : '管理词典' }}
<div class="color-link cursor-pointer" v-if="store.word.bookList.length > 3"
@click="isManageDict = !isManageDict; selectIds = []">{{ isManageDict ? '取消' : '管理词典' }}
</div>
<div class="color-blue cursor-pointer" @click="nav('dict-detail', { isAdd: true })">创建个人词典</div>
<div class="color-link cursor-pointer" @click="nav('dict-detail', { isAdd: true })">创建个人词典</div>
</div>
</div>
<div class="flex gap-4 flex-wrap mt-4">
<Book :is-add="false" quantifier="个词" :item="item" :checked="selectIds.includes(item.id)"
@check="() => toggleSelect(item)" :show-checkbox="isMultiple && j >= 3"
@check="() => toggleSelect(item)" :show-checkbox="isManageDict && j >= 3"
v-for="(item, j) in store.word.bookList" @click="goDictDetail(item)"/>
<Book :is-add="true" @click="router.push('/dict-list')"/>
</div>
@@ -289,7 +346,7 @@ const {
<div class="flex justify-between">
<div class="title">推荐</div>
<div class="flex gap-4 items-center">
<div class="color-blue cursor-pointer" @click="router.push('/dict-list')">更多</div>
<div class="color-link cursor-pointer" @click="router.push('/dict-list')">更多</div>
</div>
</div>
@@ -303,22 +360,37 @@ const {
</BasePage>
<PracticeSettingDialog
:show-left-option="false"
v-model="showPracticeSettingDialog"
@ok="savePracticeSetting"/>
:show-left-option="false"
v-model="showPracticeSettingDialog"
@ok="savePracticeSetting"/>
<ChangeLastPracticeIndexDialog
v-model="showChangeLastPracticeIndexDialog"
@ok="saveLastPracticeIndex"
v-model="showChangeLastPracticeIndexDialog"
@ok="saveLastPracticeIndex"
/>
<PracticeWordListDialog
:data="currentStudy"
v-model="showPracticeWordListDialog"
:data="currentStudy"
v-model="showPracticeWordListDialog"
/>
<CollectNotice/>
<ShufflePracticeSettingDialog
v-model="showShufflePracticeSettingDialog"
@ok="onShufflePracticeSettingOk"/>
</template>
<style scoped lang="scss">
.stat {
@apply w-31% box-border flex flex-col items-center justify-center rounded-xl p-2 bg-[var(--bg-history)];
border: 1px solid gainsboro;
.num {
@apply color-[#409eff] text-4xl font-bold;
}
.txt {
@apply color-gray-500;
}
}
</style>

View File

@@ -3,12 +3,12 @@
import { inject, Ref, watch } from "vue"
import { usePracticeStore } from "@/stores/practice.ts";
import { useSettingStore } from "@/stores/setting.ts";
import { PracticeData, WordPracticeType, ShortcutKey } from "@/types/types.ts";
import {PracticeData, WordPracticeType, ShortcutKey, TaskWords} from "@/types/types.ts";
import BaseIcon from "@/components/BaseIcon.vue";
import Tooltip from "@/components/base/Tooltip.vue";
import Progress from '@/components/base/Progress.vue'
const statisticsStore = usePracticeStore()
const statStore = usePracticeStore()
const settingStore = useSettingStore()
defineProps<{
@@ -34,7 +34,7 @@ function format(val: number, suffix: string = '', check: number = -1) {
const status = $computed(() => {
if (isTypingWrongWord.value) return '复习错词'
let str = ''
switch (statisticsStore.step) {
switch (statStore.step) {
case 0:
str += `学习新词`
break
@@ -62,6 +62,9 @@ const status = $computed(() => {
case 8:
str += '默写之前学习'
break
case 10:
str += '随机复习'
break
}
return str
})
@@ -96,22 +99,22 @@ const progress = $computed(() => {
<div class="name">{{ status }}</div>
</div>
<div class="row">
<div class="num">{{ statisticsStore.total }}</div>
<div class="num">{{ statStore.total }}</div>
<div class="line"></div>
<div class="name">单词总数</div>
</div>
<div class="row">
<div class="num">{{ format(statisticsStore.inputWordNumber, '', 0) }}</div>
<div class="num">{{ format(statStore.inputWordNumber, '', 0) }}</div>
<div class="line"></div>
<div class="name">总输入数</div>
</div>
<div class="row">
<div class="num">{{ format(statisticsStore.wrong, '', 0) }}</div>
<div class="num">{{ format(statStore.wrong, '', 0) }}</div>
<div class="line"></div>
<div class="name">总错误数</div>
</div>
</div>
<div class="flex gap-2 justify-center items-center">
<div class="flex gap-2 justify-center items-center">
<BaseIcon
:class="!isSimple?'collect':'fill'"
@click="$emit('toggleSimple')"

View File

@@ -1,18 +1,15 @@
<script setup lang="ts">
import {_getAccomplishDays} from "@/utils";
import Radio from "@/components/base/radio/Radio.vue";
import RadioGroup from "@/components/base/radio/RadioGroup.vue";
import { _getAccomplishDays } from "@/utils";
import BaseButton from "@/components/BaseButton.vue";
import Checkbox from "@/components/base/checkbox/Checkbox.vue";
import Slider from "@/components/base/Slider.vue";
import {useBaseStore} from "@/stores/base.ts";
import {defineAsyncComponent, watch} from "vue";
import {useSettingStore} from "@/stores/setting.ts";
import { defineAsyncComponent, watch } from "vue";
import { useSettingStore } from "@/stores/setting.ts";
import Toast from "@/components/base/toast/Toast.ts";
import ChangeLastPracticeIndexDialog from "@/pages/word/components/ChangeLastPracticeIndexDialog.vue";
import Tooltip from "@/components/base/Tooltip.vue";
import {useRuntimeStore} from "@/stores/runtime.ts";
import { useRuntimeStore } from "@/stores/runtime.ts";
const Dialog = defineAsyncComponent(() => import('@/components/dialog/Dialog.vue'))

View File

@@ -0,0 +1,69 @@
<script setup lang="ts">
import Slider from "@/components/base/Slider.vue";
import {defineAsyncComponent, watch} from "vue";
import {useBaseStore} from "@/stores/base.ts";
const Dialog = defineAsyncComponent(() => import('@/components/dialog/Dialog.vue'))
const store = useBaseStore()
const model = defineModel()
const emit = defineEmits<{
ok: [val: number];
}>()
let num = $ref(0)
let min = $ref(0)
watch(() => model.value, (n) => {
if (n) {
num = Math.floor(store.sdict.lastLearnIndex / 3)
num = num > 50 ? 50 : num
min = num < 10 ? num : 10
}
})
</script>
<template>
<Dialog v-model="model" title="随机复习设置"
:footer="true"
@ok="emit('ok',num)">
<div class="target-modal color-main">
<div class="flex gap-4 items-end mb-2">
<span>随机复习<span class="font-bold">{{ store.sdict.name }}</span></span>
<span class="text-3xl mx-2 lh">{{ num }}</span>个单词
</div>
<div class="flex gap-space">
<span class="shrink-0">随机数量</span>
<Slider :min="min"
:step="10"
show-text
class="mt-1"
:max="store.sdict.lastLearnIndex"
v-model="num"/>
</div>
</div>
</Dialog>
</template>
<style scoped lang="scss">
.target-modal {
width: 30rem;
padding: 0 var(--space);
.lh {
color: rgb(176, 116, 211)
}
.mode-item {
@apply w-50% border border-blue border-solid p-2 rounded-lg cursor-pointer;
}
.active {
@apply bg-blue color-white;
}
}
</style>

View File

@@ -1,18 +1,17 @@
<script setup lang="ts">
import {WordPracticeType, ShortcutKey, Word, WordPracticeMode} from "@/types/types.ts";
import {ShortcutKey, Word, WordPracticeType} from "@/types/types.ts";
import VolumeIcon from "@/components/icon/VolumeIcon.vue";
import {useSettingStore} from "@/stores/setting.ts";
import {usePlayBeep, usePlayCorrect, usePlayKeyboardAudio, usePlayWordAudio} from "@/hooks/sound.ts";
import {emitter, EventKey, useEvents} from "@/utils/eventBus.ts";
import {inject, onMounted, onUnmounted, Ref, watch} from "vue";
import {onMounted, onUnmounted, watch} from "vue";
import SentenceHightLightWord from "@/pages/word/components/SentenceHightLightWord.vue";
import {usePracticeStore} from "@/stores/practice.ts";
import {getDefaultWord} from "@/types/func.ts";
import {_nextTick, last, sleep} from "@/utils";
import {_nextTick, last} from "@/utils";
import BaseButton from "@/components/BaseButton.vue";
import Space from "@/pages/article/components/Space.vue";
import Toast from "@/components/base/toast/Toast.ts";
import Tooltip from "@/components/base/Tooltip.vue";
interface IProps {
word: Word,
@@ -104,9 +103,7 @@ function repeat() {
wordRepeatCount++
inputLock = false
if (settingStore.wordSound) {
volumeIconRef?.play()
}
if (settingStore.wordSound) volumeIconRef?.play()
}, settingStore.waitTimeForChangeWord)
}
@@ -144,6 +141,7 @@ function unknown(e) {
if (!showWordResult) {
showWordResult = true
emit('wrong')
if (settingStore.wordSound) volumeIconRef?.play()
return
}
}
@@ -153,21 +151,40 @@ function unknown(e) {
async function onTyping(e: KeyboardEvent) {
debugger
let word = props.word.word
// 输入完成会锁死不能再输入
if (inputLock) {
// 因为输入完成会锁死不能再输入,所以在这里判断空格键切换到下一个单词
if (e.code === 'Space' && input.toLowerCase() === word.toLowerCase()) {
showWordResult = inputLock = false
emit('complete')
} else {
//当显示单词时,提示用户正确按键
if (showWordResult) {
pressNumber++
if (pressNumber >= 3) {
Toast.info(right ? '请按空格键切换' : '请按删除键重新输入', {duration: 2000})
pressNumber = 0
//判断是否是空格键以便切换到下一个单词
if (e.code === 'Space') {
//正确时就切换到下一个单词
if (right) {
showWordResult = inputLock = false
emit('complete')
} else {
if (showWordResult) {
// 错误时,提示用户按删除键,仅默写需要提示
pressNumber++
if (pressNumber >= 3) {
Toast.info('请按删除键重新输入', {duration: 2000})
pressNumber = 0
}
}
}
} else {
//当正确时,提醒用户按空格键切下一个
if (right) {
pressNumber++
if (pressNumber >= 3) {
Toast.info('请按空格键继续', {duration: 2000})
pressNumber = 0
}
} else {
//当错误时,按任意键重新输入
showWordResult = inputLock = false
input = wrong = ''
onTyping(e)
}
}
return
}
inputLock = true
@@ -186,12 +203,12 @@ async function onTyping(e: KeyboardEvent) {
} else {
//未显示单词,则播放正确音乐,并在后面设置为 showWordResult 为 true 来显示单词
playCorrect()
volumeIconRef?.play()
if (settingStore.wordSound) volumeIconRef?.play()
}
} else {
//错误处理
playBeep()
volumeIconRef?.play()
if (settingStore.wordSound) volumeIconRef?.play()
emit('wrong')
}
showWordResult = true
@@ -209,7 +226,7 @@ async function onTyping(e: KeyboardEvent) {
if (settingStore.ignoreCase) {
right = letter.toLowerCase() === word[input.length].toLowerCase()
} else {
right = letter === props.word.word[input.length]
right = letter === word[input.length]
}
if (right) {
input += letter
@@ -219,10 +236,11 @@ async function onTyping(e: KeyboardEvent) {
emit('wrong')
wrong = letter
playBeep()
volumeIconRef?.play()
await sleep(500)
if (settingStore.inputWrongClear) input = ''
wrong = ''
if (settingStore.wordSound) volumeIconRef?.play()
setTimeout(() => {
if (settingStore.inputWrongClear) input = ''
wrong = ''
}, 500)
}
// 更新当前单词信息
updateCurrentWordInfo();

View File

@@ -1,7 +1,7 @@
import * as VueRouter from 'vue-router'
import {RouteRecordRaw} from 'vue-router'
import { RouteRecordRaw } from 'vue-router'
import WordsPage from "@/pages/word/WordsPage.vue";
import PC from "@/pages/index.vue";
import Layout from "@/pages/layout.vue";
import ArticlesPage from "@/pages/article/ArticlesPage.vue";
import PracticeArticles from "@/pages/article/PracticeArticles.vue";
import DictDetail from "@/pages/word/DictDetail.vue";
@@ -10,17 +10,15 @@ import BookDetail from "@/pages/article/BookDetail.vue";
import DictList from "@/pages/word/DictList.vue";
import BookList from "@/pages/article/BookList.vue";
import Setting from "@/pages/setting/Setting.vue";
import Home from "@/pages/home/index.vue";
import Login from "@/pages/user/login.vue";
import User from "@/pages/user/index.vue";
export const routes: RouteRecordRaw[] = [
{
path: '/',
component: PC,
redirect: '/',
component: Layout,
children: [
{path: '/', component: Home},
{path: '/', redirect: '/words'},
{path: 'words', component: WordsPage},
{path: 'word', redirect: '/words'},
{path: 'practice-words/:id', component: PracticeWords},

View File

@@ -5,14 +5,11 @@ export interface PracticeState {
startDate: number,
spend: number,
total: number,
index: number,//当前输入的第几个用于和total计算进度
newWordNumber: number,
reviewWordNumber: number,
writeWordNumber: number,
inputWordNumber: number,//当前总输入了多少个单词(不包含跳过)
wrong: number,
startIndex: number,
endIndex: number,
}
export const usePracticeStore = defineStore('practice', {
@@ -22,9 +19,6 @@ export const usePracticeStore = defineStore('practice', {
spend: 0,
startDate: Date.now(),
total: 0,
index: 0,
startIndex: 0,
endIndex: 0,
newWordNumber: 0,
reviewWordNumber: 0,
writeWordNumber: 0,

View File

@@ -69,7 +69,7 @@ export const getDefaultSettingState = (): SettingState => ({
keyboardSound: true,
keyboardSoundVolume: 100,
keyboardSoundFile: '机械键盘2',
keyboardSoundFile: '笔记本键盘',
effectSound: true,
effectSoundVolume: 100,

View File

@@ -200,6 +200,7 @@ export interface TaskWords {
new: Word[],
review: Word[],
write: Word[],
shuffle: Word[],
}
export class DictId {

View File

@@ -8,7 +8,10 @@ export default defineConfig({
'bg-third': 'bg-[var(--color-third)]',
'bg-card-active': 'bg-[var(--color-card-active)]',
'bg-item': 'bg-[var(--color-item-bg)]',
'bg-reverse-white': 'bg-[var(--color-reverse-white)]',
'bg-reverse-black': 'bg-[var(--color-reverse-black)]',
'color-main': 'color-[var(--color-main-text)]',
'color-link': 'color-[var(--color-link)]',
'gap-space': 'gap-[var(--space)]',
'p-space': 'p-[var(--space)]',
'px-space': 'px-[var(--space)]',

View File

@@ -1,23 +1,24 @@
import { defineConfig } from 'vite'
import {defineConfig} from 'vite'
import Vue from '@vitejs/plugin-vue'
import VueJsx from "@vitejs/plugin-vue-jsx";
import { resolve } from 'path'
import { visualizer } from "rollup-plugin-visualizer";
import {resolve} from 'path'
import {visualizer} from "rollup-plugin-visualizer";
import SlidePlugin from './src/components/slide/data.js';
import { getLastCommit } from "git-last-commit";
import {getLastCommit} from "git-last-commit";
import UnoCSS from 'unocss/vite'
import VueMacros from 'unplugin-vue-macros/vite'
import Icons from 'unplugin-icons/vite'
import Components from 'unplugin-vue-components/vite'
import IconsResolver from 'unplugin-icons/resolver'
import { viteExternalsPlugin } from 'vite-plugin-externals'
import {viteExternalsPlugin} from 'vite-plugin-externals'
function pathResolve(dir: string) {
return resolve(__dirname, ".", dir)
}
const lifecycle = process.env.npm_lifecycle_event;
let isCdnBuild = ['build', 'report'].includes(lifecycle)
let isCdnBuild = ['build-oss', 'report-oss'].includes(lifecycle)
let isAnalyseBuild = ['report-oss', 'report'].includes(lifecycle)
// https://vitejs.dev/config/
export default defineConfig(() => {
@@ -47,7 +48,7 @@ export default defineConfig(() => {
},
}),
UnoCSS(),
lifecycle === 'report' ?
isAnalyseBuild ?
visualizer({
gzipSize: true,
brotliSize: true,
@@ -125,9 +126,6 @@ export default defineConfig(() => {
port: 3000,
open: false,
host: '0.0.0.0',
fs: {
strict: false,
},
proxy: {
'/baidu': 'https://api.fanyi.baidu.com/api/trans/vip/translate'
}