diff --git a/package.json b/package.json index 6e798bf4..236ce827 100644 --- a/package.json +++ b/package.json @@ -25,17 +25,16 @@ "file-saver": "^2.0.5", "idb-keyval": "^6.2.2", "libarchive-wasm": "^1.2.0", + "md5": "^2.2.1", "mitt": "^3.0.1", "nanoid": "^5.1.5", "pinia": "^3.0.3", "string-comparison": "^1.3.0", "vue": "^3.5.17", "vue-router": "^4.5.1", - "vue-virtual-scroller": "2.0.0-beta.8", - "md5": "^2.2.1" + "vue-virtual-scroller": "2.0.0-beta.8" }, "devDependencies": { - "@types/md5": "^2.1.33", "@alicloud/pop-core": "^1.8.0", "@iconify-json/basil": "^1.2.4", "@iconify-json/bi": "^1.2.6", @@ -67,6 +66,7 @@ "@iconify-json/uil": "^1.2.3", "@types/file-saver": "^2.0.7", "@types/lodash-es": "^4.17.12", + "@types/md5": "^2.1.33", "@unocss/postcss": "^66.4.0", "@vitejs/plugin-vue": "^6.0.0", "@vitejs/plugin-vue-jsx": "^5.0.1", @@ -86,10 +86,11 @@ "unplugin-icons": "^22.2.0", "unplugin-vue-components": "^29.0.0", "unplugin-vue-macros": "^2.14.5", - "vite-plugin-externals": "^0.6.2", "vite": "^7.0.3", + "vite-plugin-externals": "^0.6.2", "vue-tsc": "^3.0.1", - "xlsx": "^0.18.5" + "xlsx": "^0.18.5", + "sitemap": "^8.0.0" }, "config": { "commitizen": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index db316201..0a5597b6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -44,6 +44,9 @@ importers: pinia: specifier: ^3.0.3 version: 3.0.3(typescript@5.9.2)(vue@3.5.18(typescript@5.9.2)) + sitemap: + specifier: ^8.0.0 + version: 8.0.0 string-comparison: specifier: ^1.3.0 version: 1.3.0 @@ -953,12 +956,18 @@ packages: '@types/md5@2.3.5': resolution: {integrity: sha512-/i42wjYNgE6wf0j2bcTX6kuowmdL/6PE4IVitMpm2eYKBUuYCprdcWVK+xEF0gcV6ufMCRhtxmReGfc6hIK7Jw==} + '@types/node@17.0.45': + resolution: {integrity: sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==} + '@types/node@20.19.11': resolution: {integrity: sha512-uug3FEEGv0r+jrecvUUpbY8lLisvIjg6AAic6a2bSP5OEOLeJsDSnvhCDov7ipFFMXS3orMpzlmi0ZcuGkBbow==} '@types/node@24.3.0': resolution: {integrity: sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==} + '@types/sax@1.2.7': + resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==} + '@types/web-bluetooth@0.0.16': resolution: {integrity: sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==} @@ -1404,6 +1413,9 @@ packages: archy@1.0.0: resolution: {integrity: sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==} + arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -3293,6 +3305,11 @@ packages: resolution: {integrity: sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==} engines: {node: '>=18'} + sitemap@8.0.0: + resolution: {integrity: sha512-+AbdxhM9kJsHtruUF39bwS/B0Fytw6Fr1o4ZAIAEqA6cke2xcoO2GleBw9Zw7nRzILVEgz7zBM5GiTJjie1G9A==} + engines: {node: '>=14.0.0', npm: '>=6.0.0'} + hasBin: true + snapdragon-node@2.1.1: resolution: {integrity: sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==} engines: {node: '>=0.10.0'} @@ -4595,6 +4612,8 @@ snapshots: '@types/md5@2.3.5': {} + '@types/node@17.0.45': {} + '@types/node@20.19.11': dependencies: undici-types: 6.21.0 @@ -4602,7 +4621,10 @@ snapshots: '@types/node@24.3.0': dependencies: undici-types: 7.10.0 - optional: true + + '@types/sax@1.2.7': + dependencies: + '@types/node': 24.3.0 '@types/web-bluetooth@0.0.16': optional: true @@ -5278,6 +5300,8 @@ snapshots: archy@1.0.0: {} + arg@5.0.2: {} + argparse@2.0.1: optional: true @@ -7340,6 +7364,13 @@ snapshots: mrmime: 2.0.1 totalist: 3.0.1 + sitemap@8.0.0: + dependencies: + '@types/node': 17.0.45 + '@types/sax': 1.2.7 + arg: 5.0.2 + sax: 1.4.1 + snapdragon-node@2.1.1: dependencies: define-property: 1.0.0 @@ -7599,8 +7630,7 @@ snapshots: undici-types@6.21.0: {} - undici-types@7.10.0: - optional: true + undici-types@7.10.0: {} unescape@1.0.1: dependencies: diff --git a/scripts/generate-sitemap.js b/scripts/generate-sitemap.js new file mode 100644 index 00000000..e1fe58e7 --- /dev/null +++ b/scripts/generate-sitemap.js @@ -0,0 +1,41 @@ +const {SitemapStream, streamToPromise} = require('sitemap') +const {createWriteStream} = require('fs') +const {resolve} = require('path') + +// 你的网站域名 +const SITE_URL = 'https://yourdomain.com' + +// 静态路由(首页、练习页等) +const staticPages = [ + {url: '/', changefreq: 'daily', priority: 1.0}, + {url: '/word', changefreq: 'daily', priority: 0.9}, + {url: '/article', changefreq: 'daily', priority: 0.9}, + {url: '/setting', changefreq: 'monthly', priority: 0.3}, +] + +// 动态页面示例(假设你有文章或单词数据) +const dynamicPages = [ + {url: '/article/vue-seo', changefreq: 'weekly', priority: 0.8}, + {url: '/article/js-tips', changefreq: 'weekly', priority: 0.8}, + // 如果文章很多,可以用 JSON / API 自动生成数组 +] + +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() diff --git a/src/pages/pc/article/ArticleHomePage.vue b/src/pages/pc/article/ArticleHomePage.vue index be76e642..059017a0 100644 --- a/src/pages/pc/article/ArticleHomePage.vue +++ b/src/pages/pc/article/ArticleHomePage.vue @@ -43,7 +43,7 @@ function startStudy() { custom: base.sbook.custom, complete: base.sbook.complete, }) - nav('/study-article', {name: store.sbook.name, id: store.sbook.id}) + nav('/study-article/' + store.sbook.id) } else { window.umami?.track('no-book') Toast.warning('请先选择一本书籍') @@ -128,7 +128,7 @@ async function goBookDetail(val: DictResource) {
{{ isMultiple ? '取消' : '管理书籍' }}
-
创建个人书籍
+
创建个人书籍
diff --git a/src/pages/pc/article/BookDetail.vue b/src/pages/pc/article/BookDetail.vue index 07876965..7d0f7943 100644 --- a/src/pages/pc/article/BookDetail.vue +++ b/src/pages/pc/article/BookDetail.vue @@ -146,7 +146,7 @@ const {
- +
{{ runtimeStore.editDict.id ? '修改' : '创建' }}书籍
diff --git a/src/pages/pc/article/StudyArticle.vue b/src/pages/pc/article/StudyArticle.vue index f1510933..b6284c57 100644 --- a/src/pages/pc/article/StudyArticle.vue +++ b/src/pages/pc/article/StudyArticle.vue @@ -10,7 +10,7 @@ import useTheme from "@/hooks/theme.ts"; import Toast from '@/pages/pc/components/base/toast/Toast.ts' import {_getDictDataByUrl, cloneDeep} from "@/utils"; import {usePracticeStore} from "@/stores/practice.ts"; -import {useArticleOptions} from "@/hooks/dict.ts"; +import {getCurrentStudyWord, useArticleOptions} from "@/hooks/dict.ts"; import {genArticleSectionData, usePlaySentenceAudio} from "@/hooks/article.ts"; import {getDefaultArticle, getDefaultDict} from "@/types/func.ts"; import TypingArticle from "@/pages/pc/article/components/TypingArticle.vue"; @@ -20,7 +20,7 @@ import ArticleList from "@/pages/pc/components/list/ArticleList.vue"; import EditSingleArticleModal from "@/pages/pc/article/components/EditSingleArticleModal.vue"; import Tooltip from "@/pages/pc/components/base/Tooltip.vue"; import ConflictNotice from "@/pages/pc/components/ConflictNotice.vue"; -import {enArticle} from "@/assets/dictionary.ts"; +import {dictionaryResources, enArticle} from "@/assets/dictionary.ts"; import {useRoute, useRouter} from "vue-router"; import {useRuntimeStore} from "@/stores/runtime.ts"; @@ -39,7 +39,7 @@ let articleData = $ref({ }) let showEditArticle = $ref(false) let typingArticleRef = $ref() -let loading = $ref(true) +let loading = $ref(false) let editArticle = $ref
(getDefaultArticle()) function write() { @@ -87,61 +87,51 @@ function next() { const router = useRouter() const route = useRoute() -const runtimeStore = useRuntimeStore() - -watch(() => store.load, (n) => { - if (n && loading && runtimeStore.editDict.id) { - console.log('load好了开始加载') - store.changeBook(runtimeStore.editDict) - articleData.list = cloneDeep(store.sbook.articles) - getCurrentPractice() - loading = false - } -},{immediate: true}) - -useStartKeyboardEventListener() -useDisableEventListener(() => loading) - -function init() { - if (store.sbook?.articles?.length) { - articleData.list = cloneDeep(store.sbook.articles) - getCurrentPractice() - loading = false - } else { - let dictName = route.query.name - let dictId = route.query.id - //如果url里有词典id或name,那么直接请求词典数据,并加到bookList里面进行学习 - //todo 这里要处理自定义词典的问题 - if (dictName || dictId) { - let dictResource = getDefaultDict() - if (dictId) dictResource = enArticle.find(v => v.id === dictId) as Dict - else if (dictName) dictResource = enArticle.find(v => v.name === dictName) as Dict - if (dictResource.id) { - loading = true - _getDictDataByUrl(dictResource, DictType.article).then(r => { - if (!r.articles.length) { - router.push('/article') - return Toast.warning('没有文章可学习!') - } - runtimeStore.editDict = r - if (store.load) { - console.log('直接加载') - store.changeBook(r) - articleData.list = cloneDeep(store.sbook.articles) - getCurrentPractice() - loading = false - } - }) - } else { +async function init() { + console.log('load好了开始加载') + let dict = getDefaultDict() + let dictId = route.params.id + if (dictId) { + //先在自己的词典列表里面找,如果没有再在资源列表里面找 + dict = store.article.bookList.find(v => v.id === dictId) + if (!dict) dict = enArticle.find(v => v.id === dictId) as Dict + if (dict && dict.id) { + //如果是不是自定义词典,就请求数据 + if (!dict.custom) dict = await _getDictDataByUrl(dict, DictType.article) + if (!dict.articles.length) { router.push('/article') + return Toast.warning('没有文章可学习!') } + store.changeBook(dict) + articleData.list = cloneDeep(store.sbook.articles) + getCurrentPractice() + loading = false } else { router.push('/article') } + } else { + router.push('/article') } } +watch(() => store.load, (n) => { + if (n && loading) init() +}, {immediate: true}) + +onMounted(() => { + if (store.sbook?.articles?.length) { + articleData.list = cloneDeep(store.sbook.articles) + getCurrentPractice() + } else { + loading = true + } +}) + + +useStartKeyboardEventListener() +useDisableEventListener(() => loading) + function setArticle(val: Article) { statisticsStore.inputWordNumber = 0 statisticsStore.wrong = 0 @@ -246,8 +236,6 @@ async function onKeyDown(e: KeyboardEvent) { useOnKeyboardEventListener(onKeyDown, onKeyUp) -onMounted(init) - useEvents([ [EventKey.write, write], [EventKey.repeatStudy, repeat], diff --git a/src/pages/pc/word/StudyWord.vue b/src/pages/pc/word/StudyWord.vue index 16fa0a8f..7f6384e9 100644 --- a/src/pages/pc/word/StudyWord.vue +++ b/src/pages/pc/word/StudyWord.vue @@ -61,54 +61,47 @@ let data = $ref({ wrongWords: [], }) -watch(() => store.load, (n) => { - if (n && loading && runtimeStore.editDict.id) { - console.log('load好了开始加载') - store.changeDict(runtimeStore.editDict) - studyData = getCurrentStudyWord() - loading = false +async function init() { + console.log('load好了开始加载') + let dict = getDefaultDict() + let dictId = route.params.id + if (dictId) { + //先在自己的词典列表里面找,如果没有再在资源列表里面找 + dict = store.word.bookList.find(v => v.id === dictId) + if (!dict) dict = dictionaryResources.find(v => v.id === dictId) as Dict + if (dict && dict.id) { + //如果是不是自定义词典,就请求数据 + if (!dict.custom) dict = await _getDictDataByUrl(dict) + if (!dict.words.length) { + router.push('/word') + return Toast.warning('没有单词可学习!') + } + store.changeDict(dict) + studyData = getCurrentStudyWord() + loading = false + } else { + router.push('/word') + } + } else { + router.push('/word') } -}, {immediate: true}) +} -useStartKeyboardEventListener() -useDisableEventListener(() => loading) +watch(() => store.load, (n) => { + if (n && loading) init() +}, {immediate: true}) onMounted(() => { if (runtimeStore.routeData) { studyData = runtimeStore.routeData } else { - let dictName = route.query.name - let dictId = route.query.id - //如果url里有词典id或name,那么直接请求词典数据,并加到bookList里面进行学习 - //todo 这里要处理自定义词典的问题 - if (dictName || dictId) { - let dictResource = getDefaultDict() - if (dictId) dictResource = dictionaryResources.find(v => v.id === dictId) as Dict - else if (dictName) dictResource = dictionaryResources.find(v => v.name === dictName) as Dict - if (dictResource.id) { - loading = true - _getDictDataByUrl(dictResource).then(r => { - if (!r.words.length) { - router.push('/word') - return Toast.warning('没有单词可学习!') - } - runtimeStore.editDict = r - if (store.load) { - console.log('直接加载') - store.changeDict(r) - studyData = getCurrentStudyWord() - loading = false - } - }) - } else { - router.push('/word') - } - } else { - router.push('/word') - } + loading = true } }) +useStartKeyboardEventListener() +useDisableEventListener(() => loading) + watch(() => studyData, () => { if (studyData.new.length === 0) { if (studyData.review.length) { diff --git a/src/pages/pc/word/WordHomePage.vue b/src/pages/pc/word/WordHomePage.vue index 17de57c2..aee63aa2 100644 --- a/src/pages/pc/word/WordHomePage.vue +++ b/src/pages/pc/word/WordHomePage.vue @@ -60,7 +60,7 @@ function startStudy() { custom: store.sdict.custom, complete: store.sdict.complete, }) - nav('study-word', {name: store.sdict.name, id: store.sdict.id}, currentStudy) + nav('study-word/' + store.sdict.id, {}, currentStudy) } else { window.umami?.track('no-dict') Toast.warning('请先选择一本词典') diff --git a/src/router.ts b/src/router.ts index d1c90c1a..f3bd3286 100644 --- a/src/router.ts +++ b/src/router.ts @@ -21,11 +21,11 @@ export const routes: RouteRecordRaw[] = [ // {path: 'home', component: HomeIndex}, {path: 'word', component: WordHomePage}, {path: 'dict-list', component: DictList}, - {path: 'study-word', component: StudyWord}, + {path: 'study-word/:id', component: StudyWord}, {path: 'dict-detail', component: DictDetail}, {path: 'article', component: ArticleHomePage}, - {path: 'study-article', component: StudyArticle}, + {path: 'study-article/:id', component: StudyArticle}, {path: 'book-detail', component: BookDetail}, {path: 'book-list', component: BookList}, {path: 'edit-article', component: () => import("@/pages/pc/article/EditArticlePage.vue")}, @@ -39,8 +39,8 @@ export const routes: RouteRecordRaw[] = [ ] const router = VueRouter.createRouter({ - // history: VueRouter.createWebHistory(), - history: VueRouter.createWebHashHistory(), + history: VueRouter.createWebHistory(), + // history: VueRouter.createWebHashHistory(), routes, scrollBehavior(to, from, savedPosition) { // console.log('savedPosition', savedPosition) diff --git a/src/utils/index.ts b/src/utils/index.ts index d29c5c76..517670e0 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -414,9 +414,9 @@ export async function sleep(time: number) { export async function _getDictDataByUrl(val: DictResource, type: DictType = DictType.word): Promise { // await sleep(2000); - let dictResourceUrl = `./dicts/${val.language}/word/${val.url}` + let dictResourceUrl = `/dicts/${val.language}/word/${val.url}` if (type === DictType.article) { - dictResourceUrl = `./dicts/${val.language}/${val.type}/${val.url}`; + dictResourceUrl = `/dicts/${val.language}/${val.type}/${val.url}`; } let s = await getDictFile(dictResourceUrl) if (s) {