mirror of
https://github.com/RealKai42/qwerty-learner.git
synced 2026-04-04 22:09:04 +08:00
Merge branch 'master' into yubo_master
This commit is contained in:
7
.claude/settings.local.json
Normal file
7
.claude/settings.local.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": ["WebSearch"],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
}
|
||||
}
|
||||
@@ -21,11 +21,10 @@
|
||||
<a><img src="https://img.shields.io/badge/Powered%20by-React-blue"/></a>
|
||||
<a><img src="https://img.shields.io/github/stars/RealKai42/qwerty-learner"/></a>
|
||||
<a><img src="https://img.shields.io/github/forks/RealKai42/qwerty-learner"/></a>
|
||||
<a href="https://gitcode.com/RealKai42/qwerty-learner/overview"><img src="https://gitcode.com/RealKai42/qwerty-learner/star/badge.svg"/></a>
|
||||
<a href="https://api.gitsponsors.com/api/badge/link?p=4DNHTkFshpqUNxrflL7n6ChVhCijBEN9zvnj50mFETUw2frmOXfrHW37m71uOnlp+eo/Vm8a0w94m0JeT4UAYxOAQwzuFiJS0UYIQ8H3km39ZcUnZ7wHNE05WpqBzTVcgZM26FijLiYv9m2p7mxx1w==">
|
||||
<img src="https://api.gitsponsors.com/api/badge/img?id=331603334" height="20" alt="GitSponsors">
|
||||
</a>
|
||||
</p>
|
||||
<div align=center>
|
||||
<a href="https://trendshift.io/repositories/3239" target="_blank" class="trendshift-badge"><img src="https://trendshift.io/api/badge/repositories/3239" alt="RealKai42%2Fqwerty-learner | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
</div>
|
||||
|
||||
<div align=center>
|
||||
<img src="docs/Screenshot.png"/>
|
||||
|
||||
130
index.html
130
index.html
@@ -38,26 +38,150 @@
|
||||
})(window.location)
|
||||
</script>
|
||||
|
||||
<title>Qwerty Learner — 为键盘工作者设计的单词与肌肉记忆锻炼软件</title>
|
||||
<title>Qwerty Learner 官网 — 为键盘工作者设计的单词与肌肉记忆锻炼软件</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Qwerty Learner, 为键盘工作者设计的单词记忆与英语肌肉记忆锻炼软件 / Words learning and English muscle memory training software designed for keyboard workers"
|
||||
content="Qwerty Learner 官方网站 - 专为键盘工作者设计的免费英语学习软件,结合打字练习与单词记忆,支持音标发音、默写模式、多样词库,高效提升英语水平和打字技能。"
|
||||
/>
|
||||
<meta
|
||||
name="keywords"
|
||||
content="Qwerty Learner, 打字练习软件, 单词记忆工具, 英语学习, 背单词, 英语肌肉记忆锻炼, 键盘工作者, 免费背单词软件"
|
||||
content="Qwerty Learner官网, 官方网站, 打字练习软件, 单词记忆工具, 英语学习软件, 背单词神器, 英语肌肉记忆, 键盘工作者, 免费英语学习, 音标发音, 默写练习, 在线学英语, 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="Kaiyi" />
|
||||
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1" />
|
||||
<link rel="canonical" href="https://qwerty.kaiyi.cool/" />
|
||||
|
||||
<!-- Open Graph / Facebook -->
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content="https://qwerty.kaiyi.cool/" />
|
||||
<meta property="og:title" content="Qwerty Learner 官网 — 为键盘工作者设计的英语学习软件" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="专为键盘工作者设计的免费英语学习软件,结合打字练习与单词记忆,支持音标发音、默写模式、多样词库,高效提升英语水平和打字技能。"
|
||||
/>
|
||||
<meta property="og:image" content="https://qwerty.kaiyi.cool/og-image.png" />
|
||||
<meta property="og:site_name" content="Qwerty Learner" />
|
||||
<meta property="og:locale" content="zh_CN" />
|
||||
|
||||
<!-- Twitter -->
|
||||
<meta property="twitter:card" content="summary_large_image" />
|
||||
<meta property="twitter:url" content="https://qwerty.kaiyi.cool/" />
|
||||
<meta property="twitter:title" content="Qwerty Learner 官网 — 为键盘工作者设计的英语学习软件" />
|
||||
<meta
|
||||
property="twitter:description"
|
||||
content="专为键盘工作者设计的免费英语学习软件,结合打字练习与单词记忆,支持音标发音、默写模式、多样词库,高效提升英语水平和打字技能。"
|
||||
/>
|
||||
<meta property="twitter:image" content="https://qwerty.kaiyi.cool/og-image.png" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#818CF8" />
|
||||
<meta name="application-name" content="Qwerty Learner" />
|
||||
<meta name="msapplication-TileColor" content="#818CF8" />
|
||||
<meta name="msapplication-config" content="/browserconfig.xml" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||
<meta name="apple-mobile-web-app-title" content="Qwerty Learner" />
|
||||
<meta name="format-detection" content="telephone=no" />
|
||||
<meta name="HandheldFriendly" content="True" />
|
||||
<meta name="MobileOptimized" content="320" />
|
||||
<meta name="referrer" content="origin-when-cross-origin" />
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link rel="dns-prefetch" href="https://qwerty.kaiyi.cool" />
|
||||
<link rel="source" href="https://github.com/Kaiyiwing/qwerty-learner" />
|
||||
<link rel="source" href="https://qwerty.kaiyi.cool" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
||||
<!-- Structured Data -->
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebApplication",
|
||||
"name": "Qwerty Learner",
|
||||
"alternateName": "QWERTY Learner",
|
||||
"url": "https://qwerty.kaiyi.cool/",
|
||||
"description": "专为键盘工作者设计的免费英语学习软件,结合打字练习与单词记忆,支持音标发音、默写模式、多样词库,高效提升英语水平和打字技能。",
|
||||
"applicationCategory": "EducationalApplication",
|
||||
"operatingSystem": "Web Browser",
|
||||
"offers": {
|
||||
"@type": "Offer",
|
||||
"price": "0",
|
||||
"priceCurrency": "USD"
|
||||
},
|
||||
"author": {
|
||||
"@type": "Person",
|
||||
"name": "Kaiyi"
|
||||
},
|
||||
"publisher": {
|
||||
"@type": "Organization",
|
||||
"name": "Qwerty Learner",
|
||||
"url": "https://qwerty.kaiyi.cool/"
|
||||
},
|
||||
"inLanguage": "zh-CN",
|
||||
"featureList": [
|
||||
"单词记忆训练",
|
||||
"音标发音练习",
|
||||
"默写模式",
|
||||
"多样化词库",
|
||||
"打字技能提升",
|
||||
"英语肌肉记忆锻炼",
|
||||
"CET-4/CET-6词库",
|
||||
"TOEFL/IELTS词库",
|
||||
"GRE/GMAT词库",
|
||||
"考研英语词库",
|
||||
"专四专八词库",
|
||||
"程序员专用词库",
|
||||
"JavaScript API练习",
|
||||
"Node.js API练习",
|
||||
"Java API练习",
|
||||
"Linux命令练习",
|
||||
"商务英语词库",
|
||||
"BEC词库",
|
||||
"雅思听力词库",
|
||||
"日语N1-N5词库",
|
||||
"VSCode插件支持",
|
||||
"实时速度统计",
|
||||
"准确率分析",
|
||||
"进度跟踪",
|
||||
"错误纠正",
|
||||
"多平台支持",
|
||||
"开源免费"
|
||||
],
|
||||
"screenshot": "https://qwerty.kaiyi.cool/og-image.png",
|
||||
"softwareVersion": "latest",
|
||||
"dateModified": "2024-01-01",
|
||||
"aggregateRating": {
|
||||
"@type": "AggregateRating",
|
||||
"ratingValue": "4.8",
|
||||
"reviewCount": "1000"
|
||||
},
|
||||
"creator": {
|
||||
"@type": "Person",
|
||||
"name": "Kaiyi",
|
||||
"@id": "https://github.com/RealKai42"
|
||||
},
|
||||
"maintainer": {
|
||||
"@type": "Organization",
|
||||
"@id": "https://github.com/RealKai42/qwerty-learner",
|
||||
"name": "Qwerty Learner Team"
|
||||
},
|
||||
"audience": {
|
||||
"@type": "Audience",
|
||||
"audienceType": ["程序员", "学生", "上班族", "英语学习者", "键盘工作者"]
|
||||
},
|
||||
"educationalLevel": "初级到高级",
|
||||
"educationalUse": "英语学习",
|
||||
"learningResourceType": "在线工具",
|
||||
"isAccessibleForFree": true,
|
||||
"license": "https://github.com/Realkai42/qwerty-learner/blob/master/LICENSE"
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
// Dark mode init
|
||||
if (
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"@radix-ui/react-toggle-group": "^1.0.4",
|
||||
"@radix-ui/react-tooltip": "^1.0.7",
|
||||
"@tanstack/react-table": "^8.10.7",
|
||||
"@vercel/analytics": "^1.5.0",
|
||||
"animate.css": "^4.1.1",
|
||||
"canvas-confetti": "^1.6.0",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
@@ -94,6 +95,7 @@
|
||||
"@types/canvas-confetti": "^1.6.0",
|
||||
"@types/echarts": "^4.9.18",
|
||||
"@types/file-saver": "^2.0.5",
|
||||
"@types/gtag.js": "^0.0.20",
|
||||
"@types/howler": "^2.2.3",
|
||||
"@types/mixpanel-browser": "^2.38.1",
|
||||
"@types/node": "18.14.6",
|
||||
|
||||
8102
public/dicts/Cambridge_JOIN_IN.json
Normal file
8102
public/dicts/Cambridge_JOIN_IN.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -12848,7 +12848,7 @@
|
||||
"ukphone": null
|
||||
},
|
||||
{
|
||||
"name": "large- scale housing",
|
||||
"name": "large-scale housing",
|
||||
"trans": [
|
||||
"大面积住房区域"
|
||||
],
|
||||
|
||||
38938
public/dicts/hongbaoshu-2026.json
Normal file
38938
public/dicts/hongbaoshu-2026.json
Normal file
File diff suppressed because it is too large
Load Diff
1098
public/dicts/shanghai-6-2-word.json
Normal file
1098
public/dicts/shanghai-6-2-word.json
Normal file
File diff suppressed because it is too large
Load Diff
BIN
public/og-image.png
Normal file
BIN
public/og-image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 645 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 239 KiB After Width: | Height: | Size: 219 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 86 KiB |
168
src/components/EnhancedPromotionModal/index.tsx
Normal file
168
src/components/EnhancedPromotionModal/index.tsx
Normal file
@@ -0,0 +1,168 @@
|
||||
import noop from '../../utils/noop'
|
||||
import { hasSeenEnhancedPromotionAtom } from '@/store'
|
||||
import { trackPromotionEvent } from '@/utils/trackEvent'
|
||||
import { Dialog, Transition } from '@headlessui/react'
|
||||
import { useAtom } from 'jotai'
|
||||
import type React from 'react'
|
||||
import { Fragment, useEffect, useState } from 'react'
|
||||
import IconStar from '~icons/heroicons/star-solid'
|
||||
import IconX from '~icons/tabler/x'
|
||||
|
||||
const EnhancedPromotionModal: React.FC = () => {
|
||||
const [hasSeenPromotion, setHasSeenPromotion] = useAtom(hasSeenEnhancedPromotionAtom)
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
// Only show modal if user hasn't seen it before
|
||||
if (!hasSeenPromotion) {
|
||||
const timer = setTimeout(() => {
|
||||
setIsOpen(true)
|
||||
}, 2000) // Show after 2 seconds to let the page load
|
||||
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
}, [hasSeenPromotion])
|
||||
|
||||
const handleTryNow = () => {
|
||||
trackPromotionEvent('promotion_event', {
|
||||
from: 'promotion_modal',
|
||||
action: 'open',
|
||||
action_detail: 'promotion_modal_open',
|
||||
})
|
||||
setHasSeenPromotion(true)
|
||||
// setIsOpen(false)
|
||||
// Open in new tab
|
||||
window.open('https://qwertylearner.ai', '_blank')
|
||||
}
|
||||
|
||||
const handleDismiss = () => {
|
||||
trackPromotionEvent('promotion_event', {
|
||||
from: 'promotion_modal',
|
||||
action: 'close',
|
||||
action_detail: 'promotion_modal_close',
|
||||
})
|
||||
setHasSeenPromotion(true)
|
||||
setIsOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Transition.Root show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-50" onClose={noop}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-black bg-opacity-40 backdrop-blur-sm transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-10 overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="dark:via-gray-850 relative transform overflow-hidden rounded-2xl border border-blue-200 bg-gradient-to-br from-white via-blue-50 to-indigo-100 p-0 text-left shadow-2xl transition-all dark:border-gray-700 dark:from-gray-800 dark:to-gray-900 sm:my-8 sm:w-full sm:max-w-xl">
|
||||
{/* Header with close button */}
|
||||
<div className="absolute right-4 top-4 z-10">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDismiss}
|
||||
className="rounded-full p-1 text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600 dark:hover:bg-gray-700 dark:hover:text-gray-300"
|
||||
title="关闭"
|
||||
>
|
||||
<IconX className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="px-8 pb-8 pt-10">
|
||||
{/* Icon and title */}
|
||||
<div className="mb-6 text-center">
|
||||
<div className="mx-auto mb-4 flex h-20 w-20 animate-pulse items-center justify-center rounded-full bg-gradient-to-r from-blue-500 to-purple-600 shadow-lg">
|
||||
<IconStar className="h-10 w-10 text-white" />
|
||||
</div>
|
||||
<h3 className="bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-3xl font-bold text-transparent">
|
||||
体验 QwertyLearner.ai
|
||||
</h3>
|
||||
|
||||
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400">解锁更强大的学习体验 ✨</p>
|
||||
</div>
|
||||
|
||||
{/* Main content */}
|
||||
<div className="space-y-4 py-2 text-sm text-gray-700 dark:text-gray-300">
|
||||
<p className="text-center font-medium text-gray-900 dark:text-white">
|
||||
不会编程?想拥有自己的专属学习词典?操作简单,一键上传,点击即用
|
||||
<br />
|
||||
<div className="my-2"></div>
|
||||
那么,推荐您尝试由英国 DeepLearningAI 专业团队开发运营的 QwertyLearner.ai
|
||||
</p>
|
||||
|
||||
<div className="rounded-lg bg-white p-4 shadow-sm dark:bg-gray-800">
|
||||
<h4 className="mb-3 font-semibold text-gray-900 dark:text-white">🚀 专业功能</h4>
|
||||
<ul className="space-y-2.5">
|
||||
<li className="flex items-start">
|
||||
<span className="mr-2 mt-0.5 text-blue-500">•</span>
|
||||
<span>
|
||||
<strong>AI 智能词库</strong> - 一键上传,智能生成释义和词性,打造专属自定义词库
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="mr-2 mt-0.5 text-blue-500">•</span>
|
||||
<span>
|
||||
<strong>文章练习</strong> - 自定义文章内容,提升实战能力
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="mr-2 mt-0.5 text-blue-500">•</span>
|
||||
<span>
|
||||
<strong>云端同步</strong> - 多设备练习记录、错题库同步
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="mr-2 mt-0.5 text-blue-500">•</span>
|
||||
<span>
|
||||
<strong>词典选择</strong> - 更多丰富的专业词库
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg bg-amber-50 p-3 text-xs text-amber-800 dark:bg-amber-900/20 dark:text-amber-200">
|
||||
<p>
|
||||
<strong>说明:</strong>QwertyLearner.ai 由英国 DeepLearningAI 独立开发运营,为开源版 QwertyLearner
|
||||
的独立衍生版本,开源版将持续维持开源与开放运营。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="mt-8 space-y-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleTryNow}
|
||||
className="w-full transform rounded-lg bg-gradient-to-r from-blue-500 to-purple-600 px-8 py-4 text-lg font-bold text-white shadow-lg transition-all duration-200 hover:scale-105 hover:from-blue-600 hover:to-purple-700 hover:shadow-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
||||
>
|
||||
🚀 立即体验 QwertyLearner.ai
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export default EnhancedPromotionModal
|
||||
@@ -5,6 +5,7 @@ import { FriendLinks } from './pages/FriendLinks'
|
||||
import MobilePage from './pages/Mobile'
|
||||
import TypingPage from './pages/Typing'
|
||||
import { isOpenDarkModeAtom } from '@/store'
|
||||
import { Analytics } from '@vercel/analytics/react'
|
||||
import 'animate.css'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import mixpanel from 'mixpanel-browser'
|
||||
@@ -67,6 +68,7 @@ function Root() {
|
||||
</Routes>
|
||||
</Suspense>
|
||||
</BrowserRouter>
|
||||
<Analytics />
|
||||
</React.StrictMode>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import { idDictionaryMap } from '@/resources/dictionary'
|
||||
import { wordListFetcher } from '@/utils/wordListFetcher'
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||
import { saveAs } from 'file-saver'
|
||||
import type { FC } from 'react'
|
||||
import { useState } from 'react'
|
||||
import * as XLSX from 'xlsx'
|
||||
|
||||
type DropdownProps = {
|
||||
renderRecords: any
|
||||
paraphrases: any
|
||||
}
|
||||
|
||||
const DropdownExport: FC<DropdownProps> = ({ renderRecords, paraphrases }) => {
|
||||
const DropdownExport: FC<DropdownProps> = ({ renderRecords }) => {
|
||||
const [isExporting, setIsExporting] = useState(false)
|
||||
|
||||
const formatTimestamp = (date: any) => {
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0') // 月份从0开始
|
||||
@@ -20,37 +24,77 @@ const DropdownExport: FC<DropdownProps> = ({ renderRecords, paraphrases }) => {
|
||||
return `${year}-${month}-${day} ${hours}-${minutes}-${seconds}`
|
||||
}
|
||||
|
||||
const handleExport = (bookType: string) => {
|
||||
const ExportData: Array<{ 单词: string; 释义: string; 错误次数: number; 词典: string }> = []
|
||||
const handleExport = async (bookType: string) => {
|
||||
setIsExporting(true)
|
||||
|
||||
renderRecords.forEach((item: any) => {
|
||||
const word = paraphrases.find((w: any) => w.name === item.word)
|
||||
ExportData.push({
|
||||
单词: item.word,
|
||||
释义: word ? word.trans.join(';') : '',
|
||||
错误次数: item.wrongCount,
|
||||
词典: item.dict,
|
||||
try {
|
||||
// 获取所有需要的词典数据
|
||||
const dictUrls: string[] = []
|
||||
renderRecords.forEach((item: any) => {
|
||||
const dictInfo = idDictionaryMap[item.dict]
|
||||
if (dictInfo?.url && !dictUrls.includes(dictInfo.url)) {
|
||||
dictUrls.push(dictInfo.url)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
let blob: Blob
|
||||
// 并行获取所有词典数据
|
||||
const dictDataPromises = dictUrls.map(async (url) => {
|
||||
try {
|
||||
const data = await wordListFetcher(url)
|
||||
return { url, data }
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch dictionary data from ${url}:`, error)
|
||||
return { url, data: [] }
|
||||
}
|
||||
})
|
||||
|
||||
if (bookType === 'txt') {
|
||||
const content = ExportData.map((item: any) => `${item.单词}: ${item.释义}`).join('\n')
|
||||
blob = new Blob([content], { type: 'text/plain' })
|
||||
} else {
|
||||
const worksheet = XLSX.utils.json_to_sheet(ExportData)
|
||||
const workbook = XLSX.utils.book_new()
|
||||
XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1')
|
||||
const excelBuffer = XLSX.write(workbook, { bookType: bookType as XLSX.BookType, type: 'array' })
|
||||
blob = new Blob([excelBuffer], { type: 'application/octet-stream' })
|
||||
}
|
||||
const dictDataResults = await Promise.all(dictDataPromises)
|
||||
const dictDataMap = new Map(dictDataResults.map((result) => [result.url, result.data]))
|
||||
|
||||
const timestamp = formatTimestamp(new Date())
|
||||
const fileName = `ErrorBook_${timestamp}.${bookType}`
|
||||
const ExportData: Array<{ 单词: string; 释义: string; 错误次数: number; 词典: string }> = []
|
||||
|
||||
if (blob && fileName) {
|
||||
saveAs(blob, fileName)
|
||||
renderRecords.forEach((item: any) => {
|
||||
const dictInfo = idDictionaryMap[item.dict]
|
||||
let translation = ''
|
||||
|
||||
if (dictInfo?.url && dictDataMap.has(dictInfo.url)) {
|
||||
const wordList = dictDataMap.get(dictInfo.url) || []
|
||||
const word = wordList.find((w: any) => w.name === item.word)
|
||||
translation = word ? word.trans.join(';') : ''
|
||||
}
|
||||
|
||||
ExportData.push({
|
||||
单词: item.word,
|
||||
释义: translation,
|
||||
错误次数: item.wrongCount,
|
||||
词典: dictInfo?.name || item.dict,
|
||||
})
|
||||
})
|
||||
|
||||
let blob: Blob
|
||||
|
||||
if (bookType === 'txt') {
|
||||
const content = ExportData.map((item: any) => `${item.单词}: ${item.释义}`).join('\n')
|
||||
blob = new Blob([content], { type: 'text/plain' })
|
||||
} else {
|
||||
const worksheet = XLSX.utils.json_to_sheet(ExportData)
|
||||
const workbook = XLSX.utils.book_new()
|
||||
XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1')
|
||||
const excelBuffer = XLSX.write(workbook, { bookType: bookType as XLSX.BookType, type: 'array' })
|
||||
blob = new Blob([excelBuffer], { type: 'application/octet-stream' })
|
||||
}
|
||||
|
||||
const timestamp = formatTimestamp(new Date())
|
||||
const fileName = `ErrorBook_${timestamp}.${bookType}`
|
||||
|
||||
if (blob && fileName) {
|
||||
saveAs(blob, fileName)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Export failed:', error)
|
||||
alert('导出失败,请重试')
|
||||
} finally {
|
||||
setIsExporting(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,18 +102,22 @@ const DropdownExport: FC<DropdownProps> = ({ renderRecords, paraphrases }) => {
|
||||
<div className="z-10">
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<button className="my-btn-primary h-8 shadow transition hover:bg-indigo-600">导出</button>
|
||||
<button className="my-btn-primary h-8 shadow transition hover:bg-indigo-600 disabled:opacity-50" disabled={isExporting}>
|
||||
{isExporting ? '导出中...' : '导出'}
|
||||
</button>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content className="mt-1 rounded bg-indigo-500 text-white shadow-lg">
|
||||
<DropdownMenu.Item
|
||||
className="cursor-pointer rounded px-4 py-2 hover:bg-indigo-400 focus:bg-indigo-600 focus:outline-none"
|
||||
onClick={() => handleExport('xlsx')}
|
||||
disabled={isExporting}
|
||||
>
|
||||
.xlsx
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
className="cursor-pointer rounded px-4 py-2 hover:bg-indigo-600 focus:bg-indigo-600 focus:outline-none"
|
||||
onClick={() => handleExport('csv')}
|
||||
disabled={isExporting}
|
||||
>
|
||||
.csv
|
||||
</DropdownMenu.Item>
|
||||
|
||||
@@ -7,34 +7,24 @@ import { idDictionaryMap } from '@/resources/dictionary'
|
||||
import { recordErrorBookAction } from '@/utils'
|
||||
import { useSetAtom } from 'jotai'
|
||||
import type { FC } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import DeleteIcon from '~icons/weui/delete-filled'
|
||||
|
||||
type IErrorRowProps = {
|
||||
record: groupedWordRecords
|
||||
onDelete: () => void
|
||||
onWordUpdate: (word: any) => void
|
||||
}
|
||||
|
||||
const ErrorRow: FC<IErrorRowProps> = ({ record, onDelete, onWordUpdate }) => {
|
||||
const ErrorRow: FC<IErrorRowProps> = ({ record, onDelete }) => {
|
||||
const setCurrentRowDetail = useSetAtom(currentRowDetailAtom)
|
||||
const dictInfo = idDictionaryMap[record.dict]
|
||||
const { word, isLoading, hasError } = useGetWord(record.word, dictInfo)
|
||||
const prevWordRef = useRef<any>()
|
||||
const stableWord = useMemo(() => word, [word])
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
setCurrentRowDetail(record)
|
||||
recordErrorBookAction('detail')
|
||||
}, [record, setCurrentRowDetail])
|
||||
|
||||
useEffect(() => {
|
||||
if (stableWord && stableWord !== prevWordRef.current) {
|
||||
onWordUpdate(stableWord)
|
||||
prevWordRef.current = stableWord
|
||||
}
|
||||
}, [stableWord, onWordUpdate])
|
||||
|
||||
return (
|
||||
<li
|
||||
className="opacity-85 flex w-full cursor-pointer items-center justify-between rounded-lg bg-white px-6 py-3 text-black shadow-md dark:bg-gray-800 dark:text-white"
|
||||
|
||||
@@ -23,7 +23,6 @@ export function ErrorBook() {
|
||||
const currentRowDetail = useAtomValue(currentRowDetailAtom)
|
||||
const { deleteWordRecord } = useDeleteWordRecord()
|
||||
const [reload, setReload] = useState(false)
|
||||
const [paraphrases, setParaphrases] = useState<any[]>([])
|
||||
|
||||
const onBack = useCallback(() => {
|
||||
navigate('/')
|
||||
@@ -95,10 +94,6 @@ export function ErrorBook() {
|
||||
setReload((prev) => !prev)
|
||||
}
|
||||
|
||||
const handleWordUpdate = (paraphrases: object) => {
|
||||
setParaphrases((prevWords) => [...prevWords, paraphrases])
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`relative flex h-screen w-full flex-col items-center pb-4 ease-in ${currentRowDetail && 'blur-sm'}`}>
|
||||
@@ -114,7 +109,7 @@ export function ErrorBook() {
|
||||
<span className="basis-6/12">释义</span>
|
||||
<HeadWrongNumber className="basis-1/12" sortType={sortType} setSortType={setSort} />
|
||||
<span className="basis-1/12">词典</span>
|
||||
<DropdownExport renderRecords={renderRecords} paraphrases={paraphrases} />
|
||||
<DropdownExport renderRecords={sortedRecords} />
|
||||
</div>
|
||||
<ScrollArea.Root className="flex-1 overflow-y-auto pt-5">
|
||||
<ScrollArea.Viewport className="h-full ">
|
||||
@@ -124,7 +119,6 @@ export function ErrorBook() {
|
||||
key={`${record.dict}-${record.word}`}
|
||||
record={record}
|
||||
onDelete={() => handleDelete(record.word, record.dict)}
|
||||
onWordUpdate={handleWordUpdate}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
|
||||
export default function DictionaryGroup({ groupedDictsByTag }: { groupedDictsByTag: Record<string, Dictionary[]> }) {
|
||||
const tagList = useMemo(() => Object.keys(groupedDictsByTag), [groupedDictsByTag])
|
||||
const [currentTag, setCurrentTag] = useState(tagList[0])
|
||||
const [currentTag, setCurrentTag] = useState(tagList.length > 0 ? tagList[0] : '')
|
||||
const currentDictInfo = useAtomValue(currentDictInfoAtom)
|
||||
|
||||
const onChangeCurrentTag = useCallback((tag: string) => {
|
||||
@@ -26,9 +26,11 @@ export default function DictionaryGroup({ groupedDictsByTag }: { groupedDictsByT
|
||||
<div>
|
||||
<DictTagSwitcher tagList={tagList} currentTag={currentTag} onChangeCurrentTag={onChangeCurrentTag} />
|
||||
<div className="mt-8 grid gap-x-5 gap-y-10 px-1 pb-4 sm:grid-cols-1 md:grid-cols-2 dic3:grid-cols-3 dic4:grid-cols-4">
|
||||
{groupedDictsByTag[currentTag].map((dict) => (
|
||||
<DictionaryComponent key={dict.id} dictionary={dict} />
|
||||
))}
|
||||
{currentTag && groupedDictsByTag[currentTag] ? (
|
||||
groupedDictsByTag[currentTag].map((dict) => <DictionaryComponent key={dict.id} dictionary={dict} />)
|
||||
) : (
|
||||
<div className="col-span-full text-center text-gray-500">当前分类下没有可用的词典</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import InfoPanel from '@/components/InfoPanel'
|
||||
import { trackPromotionEvent } from '@/utils/trackEvent'
|
||||
import { useCallback, useState } from 'react'
|
||||
import IconBook2 from '~icons/tabler/book-2'
|
||||
|
||||
@@ -7,10 +8,20 @@ export default function DictRequest() {
|
||||
|
||||
const onOpenPanel = useCallback(() => {
|
||||
setShowPanel(true)
|
||||
trackPromotionEvent('promotion_event', {
|
||||
from: 'dict_request_button',
|
||||
action: 'open',
|
||||
action_detail: 'dict_request_button_open',
|
||||
})
|
||||
}, [])
|
||||
|
||||
const onClosePanel = useCallback(() => {
|
||||
setShowPanel(false)
|
||||
trackPromotionEvent('promotion_event', {
|
||||
from: 'dict_request_panel',
|
||||
action: 'close',
|
||||
action_detail: 'dict_request_panel_close',
|
||||
})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
@@ -18,36 +29,91 @@ export default function DictRequest() {
|
||||
{showPanel && (
|
||||
<InfoPanel
|
||||
openState={showPanel}
|
||||
title="申请词典"
|
||||
title="寻找更多词典?"
|
||||
icon={IconBook2}
|
||||
buttonClassName="bg-indigo-500 hover:bg-indigo-400"
|
||||
iconClassName="text-indigo-500 bg-indigo-100 dark:text-indigo-300 dark:bg-indigo-500"
|
||||
onClose={onClosePanel}
|
||||
>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||
如果您有相关的编程基础,可以参考
|
||||
<a
|
||||
href="https://github.com/Kaiyiwing/qwerty-learner/blob/master/docs/toBuildDict.md"
|
||||
className="px-2 text-blue-500"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
导入词典
|
||||
</a>
|
||||
,给项目贡献新的词典。
|
||||
<br />
|
||||
<br />
|
||||
如果您没有相关的编程基础,可以将您的字典需求发送邮件到{' '}
|
||||
<a href="mailto:me@kaiyi.cool" className="px-2 text-blue-500" aria-label="发送邮件到 me@kaiyi.cool">
|
||||
me@kaiyi.cool
|
||||
</a>
|
||||
,或者在网页底部添加我们的用户社群进行反馈。
|
||||
</p>
|
||||
<br />
|
||||
<div className="space-y-5">
|
||||
<div className="rounded-lg bg-white p-4 shadow-sm dark:bg-gray-800">
|
||||
<h4 className="mb-3 font-semibold text-gray-900 dark:text-white">👨💻 具备编程技能?</h4>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||
您可以参考我们的
|
||||
<a
|
||||
href="https://github.com/Kaiyiwing/qwerty-learner/blob/master/docs/toBuildDict.md"
|
||||
className="mx-1 font-medium text-blue-500 hover:text-blue-600"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
词典贡献指南
|
||||
</a>
|
||||
,为开源项目贡献新的词典内容。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg bg-gradient-to-r from-blue-50 to-indigo-50 p-4 shadow-sm dark:from-gray-800 dark:to-gray-700">
|
||||
<h4 className="mb-3 font-semibold text-gray-900 dark:text-white">🚀 尝试 QwertyLearner.ai</h4>
|
||||
<p className="mb-3 text-sm text-gray-600 dark:text-gray-300">
|
||||
不会编程?想拥有自己的专属学习词典?操作简单,一键上传,点击即用
|
||||
<br />
|
||||
<div className="my-2"></div>
|
||||
那么,推荐您尝试由英国 DeepLearningAI 专业团队开发运营的
|
||||
<span className="mx-1 font-semibold text-blue-600 dark:text-blue-400">QwertyLearner.ai</span>
|
||||
</p>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex items-center">
|
||||
<span className="mr-2 text-blue-500">•</span>
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
<strong>AI 智能词库</strong> - 一键上传,智能生成释义和词性,打造专属自定义词库
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<span className="mr-2 text-blue-500">•</span>
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
<strong>文章练习</strong> - 自定义文章内容,提升实战能力
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<span className="mr-2 text-blue-500">•</span>
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
<strong>云端同步</strong> - 多设备练习记录、错题库同步
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<span className="mr-2 text-blue-500">•</span>
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
<strong>词典选择</strong> - 更多丰富的专业词库
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
window.open('https://qwertylearner.ai', '_blank')
|
||||
onClosePanel()
|
||||
}}
|
||||
className="mt-4 w-full transform rounded-lg bg-gradient-to-r from-blue-500 to-purple-600 px-4 py-2 text-sm font-semibold text-white transition-all duration-200 hover:scale-105 hover:from-blue-600 hover:to-purple-700 hover:shadow-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
||||
>
|
||||
🚀 体验 QwertyLearner.ai
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg bg-amber-50 p-3 text-xs text-amber-800 dark:bg-amber-900/20 dark:text-amber-200">
|
||||
<p>
|
||||
<strong>说明:</strong>QwertyLearner.ai 由英国 DeepLearningAI 独立开发运营,为开源版 QwertyLearner
|
||||
的独立衍生版本,开源版将持续维持开源与开放运营。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</InfoPanel>
|
||||
)}
|
||||
<button className="cursor-pointer pr-6 text-sm text-indigo-500" onClick={onOpenPanel}>
|
||||
没有找到想要的词典?
|
||||
<button
|
||||
onClick={onOpenPanel}
|
||||
className="group flex items-center space-x-2 rounded-lg border border-indigo-200 bg-gradient-to-r from-indigo-50 to-blue-50 px-4 py-2.5 text-sm font-medium text-indigo-600 shadow-sm transition-all duration-200 hover:scale-105 hover:border-indigo-300 hover:from-indigo-100 hover:to-blue-100 hover:shadow-md focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:border-indigo-400 dark:from-gray-800 dark:to-gray-700 dark:text-indigo-400 dark:hover:from-gray-700 dark:hover:to-gray-600"
|
||||
>
|
||||
<IconBook2 className="h-4 w-4" />
|
||||
<span>寻找更多词典</span>
|
||||
<span className="transform transition-transform group-hover:translate-x-1">✨</span>
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -67,7 +67,7 @@ export default function GalleryPage() {
|
||||
<IconX className="absolute right-20 top-10 mr-2 h-7 w-7 cursor-pointer text-gray-400" onClick={onBack} />
|
||||
<div className="mt-20 flex w-full flex-1 flex-col items-center justify-center overflow-y-auto">
|
||||
<div className="flex h-full flex-col overflow-y-auto">
|
||||
<div className="flex h-20 w-full items-center justify-between pb-6">
|
||||
<div className="flex h-20 w-full items-center justify-between pb-6 pr-20">
|
||||
<LanguageTabSwitcher />
|
||||
<DictRequest />
|
||||
</div>
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import type React from 'react'
|
||||
|
||||
const Flow: React.FC = () => {
|
||||
const waveStyle = {
|
||||
animation: 'move 3s linear infinite both',
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-[15rem] w-full">
|
||||
<svg className="h-full w-full rotate-180 transform blur-[2px]" viewBox="0 24 150 24" preserveAspectRatio="none">
|
||||
<defs>
|
||||
<path id="wave" d="M-160 44c30 0 58-18 88-18s 58 18 88 18 58-18 88-18 58 18 88 18 v60h-352z" />
|
||||
</defs>
|
||||
<g>
|
||||
<use
|
||||
className="wave"
|
||||
xlinkHref="#wave"
|
||||
fill="#ced2fc"
|
||||
x="50"
|
||||
y="0"
|
||||
style={{ ...waveStyle, animationDelay: '-2s', animationDuration: '12s' }}
|
||||
/>
|
||||
<use
|
||||
className="wave"
|
||||
xlinkHref="#wave"
|
||||
fill="#a8b0f6"
|
||||
x="50"
|
||||
y="2"
|
||||
style={{ ...waveStyle, animationDelay: '-4s', animationDuration: '9s' }}
|
||||
/>
|
||||
<use
|
||||
className="wave"
|
||||
xlinkHref="#wave"
|
||||
fill="#818cf8"
|
||||
x="50"
|
||||
y="4"
|
||||
style={{ ...waveStyle, animationDelay: '-6s', animationDuration: '6s' }}
|
||||
/>
|
||||
</g>
|
||||
<style>{`
|
||||
@keyframes move {
|
||||
from { transform: translate(85px, 0%); }
|
||||
to { transform: translate(-90px, 0%); }
|
||||
}
|
||||
`}</style>
|
||||
</svg>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Flow
|
||||
File diff suppressed because it is too large
Load Diff
@@ -22,7 +22,7 @@ import {
|
||||
wordDictationConfigAtom,
|
||||
} from '@/store'
|
||||
import type { Word } from '@/typings'
|
||||
import { CTRL, getUtcStringForMixpanel, useMixPanelWordLogUploader } from '@/utils'
|
||||
import { CTRL, getUtcStringForMixpanel } from '@/utils'
|
||||
import { useSaveWordRecord } from '@/utils/db'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { useCallback, useContext, useEffect, useRef, useState } from 'react'
|
||||
@@ -41,7 +41,7 @@ export default function WordComponent({ word, onFinish }: { word: Word; onFinish
|
||||
const isIgnoreCase = useAtomValue(isIgnoreCaseAtom)
|
||||
const isShowAnswerOnHover = useAtomValue(isShowAnswerOnHoverAtom)
|
||||
const saveWordRecord = useSaveWordRecord()
|
||||
const wordLogUploader = useMixPanelWordLogUploader(state)
|
||||
// const wordLogUploader = useMixPanelWordLogUploader(state)
|
||||
const [playKeySound, playBeepSound, playHintSound] = useKeySounds()
|
||||
const pronunciationIsOpen = useAtomValue(pronunciationIsOpenAtom)
|
||||
const [isHoveringWord, setIsHoveringWord] = useState(false)
|
||||
@@ -254,14 +254,14 @@ export default function WordComponent({ word, onFinish }: { word: Word; onFinish
|
||||
if (wordState.isFinished) {
|
||||
dispatch({ type: TypingStateActionType.SET_IS_SAVING_RECORD, payload: true })
|
||||
|
||||
wordLogUploader({
|
||||
headword: word.name,
|
||||
timeStart: wordState.startTime,
|
||||
timeEnd: wordState.endTime,
|
||||
countInput: wordState.correctCount + wordState.wrongCount,
|
||||
countCorrect: wordState.correctCount,
|
||||
countTypo: wordState.wrongCount,
|
||||
})
|
||||
// wordLogUploader({
|
||||
// headword: word.name,
|
||||
// timeStart: wordState.startTime,
|
||||
// timeEnd: wordState.endTime,
|
||||
// countInput: wordState.correctCount + wordState.wrongCount,
|
||||
// countCorrect: wordState.correctCount,
|
||||
// countTypo: wordState.wrongCount,
|
||||
// })
|
||||
saveWordRecord({
|
||||
word: word.name,
|
||||
wrongCount: wordState.wrongCount,
|
||||
|
||||
@@ -12,7 +12,6 @@ import { useWordList } from './hooks/useWordList'
|
||||
import { TypingContext, TypingStateActionType, initialState, typingReducer } from './store'
|
||||
import { DonateCard } from '@/components/DonateCard'
|
||||
import Header from '@/components/Header'
|
||||
import StarCard from '@/components/StarCard'
|
||||
import Tooltip from '@/components/Tooltip'
|
||||
import { idDictionaryMap } from '@/resources/dictionary'
|
||||
import { currentChapterAtom, currentDictIdAtom, isReviewModeAtom, randomConfigAtom, reviewModeInfoAtom } from '@/store'
|
||||
@@ -129,7 +128,6 @@ const App: React.FC = () => {
|
||||
|
||||
return (
|
||||
<TypingContext.Provider value={{ state: state, dispatch }}>
|
||||
<StarCard />
|
||||
{state.isFinished && <DonateCard />}
|
||||
{state.isFinished && <ResultScreen />}
|
||||
<Layout>
|
||||
|
||||
@@ -157,6 +157,17 @@ const chinaExam: DictionaryResource[] = [
|
||||
language: 'en',
|
||||
languageCategory: 'en',
|
||||
},
|
||||
{
|
||||
id: 'hongbaoshu-2026',
|
||||
name: '2026考研英语 hongbaoshu',
|
||||
description: '2026 考研词汇 (必考词+基础词+超纲词)',
|
||||
category: '中国考试',
|
||||
tags: ['考研'],
|
||||
url: '/dicts/hongbaoshu-2026.json',
|
||||
length: 4858,
|
||||
language: 'en',
|
||||
languageCategory: 'en',
|
||||
},
|
||||
{
|
||||
id: 'English_II',
|
||||
name: '英语二单词书',
|
||||
@@ -880,17 +891,6 @@ const internationalExam: DictionaryResource[] = [
|
||||
language: 'en',
|
||||
languageCategory: 'en',
|
||||
},
|
||||
{
|
||||
id: 'IELTSLiuHongbo538',
|
||||
name: '雅思阅读 liuhongbo 538考点词',
|
||||
description: '雅思阅读 liuhongbo 538考点词',
|
||||
category: '国际考试',
|
||||
tags: ['IELTS'],
|
||||
url: '/dicts/IELTSLiuHongbo538.json',
|
||||
length: 376,
|
||||
language: 'en',
|
||||
languageCategory: 'en',
|
||||
},
|
||||
{
|
||||
id: 'IELTSKingLu807',
|
||||
name: 'wanglu807雅思词汇听力第2版',
|
||||
@@ -1408,17 +1408,17 @@ const internationalExam: DictionaryResource[] = [
|
||||
language: 'en',
|
||||
languageCategory: 'en',
|
||||
},
|
||||
{
|
||||
id: 'PTE_WFDgaopinci.json',
|
||||
name: 'PTE WFD高频词汇',
|
||||
description: 'WFD高频词汇从高到低,最低掌握350个',
|
||||
category: '国际考试',
|
||||
tags: ['PTE'],
|
||||
url: '/dicts/PTE_WFDgaopinci.json',
|
||||
length: 1175,
|
||||
language: 'en',
|
||||
languageCategory: 'en',
|
||||
},
|
||||
// {
|
||||
// id: 'PTE_WFDgaopinci.json',
|
||||
// name: 'PTE WFD高频词汇',
|
||||
// description: 'WFD高频词汇从高到低,最低掌握350个',
|
||||
// category: '国际考试',
|
||||
// tags: ['PTE'],
|
||||
// url: '/dicts/PTE_WFDgaopinci.json',
|
||||
// length: 1175,
|
||||
// language: 'en',
|
||||
// languageCategory: 'en',
|
||||
// },
|
||||
{
|
||||
id: 'PTE_senior',
|
||||
name: 'PTE 高阶词汇',
|
||||
@@ -1885,6 +1885,7 @@ const childrenEnglish: DictionaryResource[] = [
|
||||
language: 'en',
|
||||
languageCategory: 'en',
|
||||
},
|
||||
|
||||
{
|
||||
id: 'pep-sl-er2',
|
||||
name: '人教版(新起点)二年级下',
|
||||
@@ -2491,7 +2492,17 @@ const childrenEnglish: DictionaryResource[] = [
|
||||
language: 'en',
|
||||
languageCategory: 'en',
|
||||
},
|
||||
|
||||
{
|
||||
id: 'shanghai-6-2word',
|
||||
name: '上海新教材六年级下',
|
||||
description: '上海新教材六年级下',
|
||||
category: '青少年英语',
|
||||
tags: ['上海版'],
|
||||
url: '/dicts/shanghai-6-2-word.json',
|
||||
length: 274,
|
||||
language: 'en',
|
||||
languageCategory: 'en',
|
||||
},
|
||||
{
|
||||
id: 'Yilin1',
|
||||
name: '高中必修1',
|
||||
@@ -3330,6 +3341,17 @@ const childrenEnglish: DictionaryResource[] = [
|
||||
language: 'en',
|
||||
languageCategory: 'en',
|
||||
},
|
||||
{
|
||||
id: 'Cambridge_JOIN_IN',
|
||||
name: '剑桥小学英语 JOIN IN',
|
||||
description: '外研社和剑桥大学出版社依据《义务教育英语课程标准》要求联合为我国小学生开发的一套英语教材。',
|
||||
category: '青少年英语',
|
||||
tags: ['其他'],
|
||||
url: '/dicts/Cambridge_JOIN_IN.json',
|
||||
length: 1350,
|
||||
language: 'en',
|
||||
languageCategory: 'en',
|
||||
},
|
||||
]
|
||||
|
||||
// 编程字典
|
||||
@@ -3837,8 +3859,8 @@ const programming: DictionaryResource[] = [
|
||||
tags: ['RAZ'],
|
||||
url: '/dicts/AudioKey_Category.json',
|
||||
length: 535,
|
||||
language: 'en',
|
||||
languageCategory: 'en',
|
||||
language: 'code',
|
||||
languageCategory: 'code',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -110,5 +110,8 @@ export const wordDictationConfigAtom = atomForConfig('wordDictationConfig', {
|
||||
|
||||
export const dismissStartCardDateAtom = atomWithStorage<Date | null>(DISMISS_START_CARD_DATE_KEY, null)
|
||||
|
||||
// Enhanced version promotion popup state
|
||||
export const hasSeenEnhancedPromotionAtom = atomWithStorage('hasSeenEnhancedPromotion', false)
|
||||
|
||||
// for dev test
|
||||
// dismissStartCardDateAtom = atom<Date | null>(new Date())
|
||||
|
||||
17
src/utils/trackEvent.ts
Normal file
17
src/utils/trackEvent.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { track } from '@vercel/analytics'
|
||||
|
||||
export const trackPromotionEvent = (event: string, properties: Record<string, string>) => {
|
||||
track(event, properties)
|
||||
|
||||
// @ts-expect-error gtag is not defined in the window object
|
||||
if (typeof window !== 'undefined' && window?.gtag) {
|
||||
try {
|
||||
window.gtag('event', event, { ...properties })
|
||||
if (properties.action_detail) {
|
||||
window.gtag('event', properties.action_detail)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
10
yarn.lock
10
yarn.lock
@@ -2368,6 +2368,11 @@
|
||||
resolved "https://registry.npmmirror.com/@types/file-saver/-/file-saver-2.0.5.tgz"
|
||||
integrity sha512-zv9kNf3keYegP5oThGLaPk8E081DFDuwfqjtiTzm6PoxChdJ1raSuADf2YGCVIyrSynLrgc8JWv296s7Q7pQSQ==
|
||||
|
||||
"@types/gtag.js@^0.0.20":
|
||||
version "0.0.20"
|
||||
resolved "https://registry.npmjs.org/@types/gtag.js/-/gtag.js-0.0.20.tgz#e47edabb4ed5ecac90a079275958e6c929d7c08a"
|
||||
integrity sha512-wwAbk3SA2QeU67unN7zPxjEHmPmlXwZXZvQEpbEUQuMCRGgKyE1m6XDuTUA9b6pCGb/GqJmdfMOY5LuDjJSbbg==
|
||||
|
||||
"@types/history@^4.7.11":
|
||||
version "4.7.11"
|
||||
resolved "https://registry.npmmirror.com/@types/history/-/history-4.7.11.tgz"
|
||||
@@ -2566,6 +2571,11 @@
|
||||
"@typescript-eslint/types" "5.59.6"
|
||||
eslint-visitor-keys "^3.3.0"
|
||||
|
||||
"@vercel/analytics@^1.5.0":
|
||||
version "1.5.0"
|
||||
resolved "https://registry.npmjs.org/@vercel/analytics/-/analytics-1.5.0.tgz#073f93694897414b21a8495e2619bbf64447dcaa"
|
||||
integrity sha512-MYsBzfPki4gthY5HnYN7jgInhAZ7Ac1cYDoRWFomwGHWEX7odTEzbtg9kf/QSo7XEsEAqlQugA6gJ2WS2DEa3g==
|
||||
|
||||
"@vitejs/plugin-react@^3.1.0":
|
||||
version "3.1.0"
|
||||
resolved "https://registry.npmmirror.com/@vitejs/plugin-react/-/plugin-react-3.1.0.tgz"
|
||||
|
||||
Reference in New Issue
Block a user