├── .nvmrc ├── .npmrc ├── .husky └── pre-commit ├── public ├── e2e │ └── rebuild │ │ ├── articles │ │ ├── 1111.md │ │ ├── 3333.md │ │ └── 2222.md │ │ └── json │ │ └── articles.json ├── robots.txt ├── rebuild │ ├── knowledges │ │ └── 27345828.md │ ├── records │ │ └── 77070256.md │ └── json │ │ ├── articles.json │ │ ├── records.json │ │ └── knowledges.json ├── icon.png ├── favicon.png ├── google646c319d599abc5c.html ├── how-to-upgrade.png ├── sticker │ ├── aru │ │ ├── 1.png │ │ ├── 10.png │ │ ├── 100.png │ │ ├── 101.png │ │ ├── 102.png │ │ ├── 103.png │ │ ├── 104.png │ │ ├── 105.png │ │ ├── 106.png │ │ ├── 107.png │ │ ├── 108.png │ │ ├── 109.png │ │ ├── 11.png │ │ ├── 110.png │ │ ├── 111.png │ │ ├── 112.png │ │ ├── 113.png │ │ ├── 114.png │ │ ├── 115.png │ │ ├── 116.png │ │ ├── 117.png │ │ ├── 118.png │ │ ├── 119.png │ │ ├── 12.png │ │ ├── 120.png │ │ ├── 121.png │ │ ├── 122.png │ │ ├── 123.png │ │ ├── 124.png │ │ ├── 125.png │ │ ├── 126.png │ │ ├── 127.png │ │ ├── 128.png │ │ ├── 129.png │ │ ├── 13.png │ │ ├── 130.png │ │ ├── 131.png │ │ ├── 132.png │ │ ├── 133.png │ │ ├── 134.png │ │ ├── 135.png │ │ ├── 136.png │ │ ├── 137.png │ │ ├── 138.png │ │ ├── 139.png │ │ ├── 14.png │ │ ├── 140.png │ │ ├── 141.png │ │ ├── 142.png │ │ ├── 143.png │ │ ├── 15.png │ │ ├── 16.png │ │ ├── 17.png │ │ ├── 18.png │ │ ├── 19.png │ │ ├── 2.png │ │ ├── 20.png │ │ ├── 21.png │ │ ├── 22.png │ │ ├── 23.png │ │ ├── 24.png │ │ ├── 25.png │ │ ├── 26.png │ │ ├── 27.png │ │ ├── 28.png │ │ ├── 29.png │ │ ├── 3.png │ │ ├── 30.png │ │ ├── 31.png │ │ ├── 32.png │ │ ├── 33.png │ │ ├── 34.png │ │ ├── 35.png │ │ ├── 36.png │ │ ├── 37.png │ │ ├── 38.png │ │ ├── 39.png │ │ ├── 4.png │ │ ├── 40.png │ │ ├── 41.png │ │ ├── 42.png │ │ ├── 43.png │ │ ├── 44.png │ │ ├── 45.png │ │ ├── 46.png │ │ ├── 47.png │ │ ├── 48.png │ │ ├── 49.png │ │ ├── 5.png │ │ ├── 50.png │ │ ├── 51.png │ │ ├── 52.png │ │ ├── 53.png │ │ ├── 54.png │ │ ├── 55.png │ │ ├── 56.png │ │ ├── 57.png │ │ ├── 58.png │ │ ├── 59.png │ │ ├── 6.png │ │ ├── 60.png │ │ ├── 61.png │ │ ├── 62.png │ │ ├── 63.png │ │ ├── 64.png │ │ ├── 65.png │ │ ├── 66.png │ │ ├── 67.png │ │ ├── 68.png │ │ ├── 69.png │ │ ├── 7.png │ │ ├── 70.png │ │ ├── 71.png │ │ ├── 72.png │ │ ├── 73.png │ │ ├── 74.png │ │ ├── 75.png │ │ ├── 76.png │ │ ├── 77.png │ │ ├── 78.png │ │ ├── 79.png │ │ ├── 8.png │ │ ├── 80.png │ │ ├── 81.png │ │ ├── 82.png │ │ ├── 83.png │ │ ├── 84.png │ │ ├── 85.png │ │ ├── 86.png │ │ ├── 87.png │ │ ├── 88.png │ │ ├── 89.png │ │ ├── 9.png │ │ ├── 90.png │ │ ├── 91.png │ │ ├── 92.png │ │ ├── 93.png │ │ ├── 94.png │ │ ├── 95.png │ │ ├── 96.png │ │ ├── 97.png │ │ ├── 98.png │ │ └── 99.png │ ├── cube │ │ ├── 1.png │ │ ├── 10.png │ │ ├── 11.png │ │ ├── 12.png │ │ ├── 13.png │ │ ├── 14.png │ │ ├── 15.png │ │ ├── 16.png │ │ ├── 17.png │ │ ├── 18.png │ │ ├── 19.png │ │ ├── 2.png │ │ ├── 20.png │ │ ├── 21.png │ │ ├── 22.png │ │ ├── 23.png │ │ ├── 24.png │ │ ├── 25.png │ │ ├── 26.png │ │ ├── 27.png │ │ ├── 28.png │ │ ├── 29.png │ │ ├── 3.png │ │ ├── 30.png │ │ ├── 31.png │ │ ├── 32.png │ │ ├── 33.png │ │ ├── 34.png │ │ ├── 35.png │ │ ├── 36.png │ │ ├── 37.png │ │ ├── 38.png │ │ ├── 39.png │ │ ├── 4.png │ │ ├── 40.png │ │ ├── 41.png │ │ ├── 42.png │ │ ├── 43.png │ │ ├── 44.png │ │ ├── 45.png │ │ ├── 46.png │ │ ├── 47.png │ │ ├── 48.png │ │ ├── 49.png │ │ ├── 5.png │ │ ├── 50.png │ │ ├── 51.png │ │ ├── 52.png │ │ ├── 53.png │ │ ├── 54.png │ │ ├── 55.png │ │ ├── 56.png │ │ ├── 57.png │ │ ├── 58.png │ │ ├── 59.png │ │ ├── 6.png │ │ ├── 60.png │ │ ├── 61.png │ │ ├── 62.png │ │ ├── 63.png │ │ ├── 64.png │ │ ├── 65.png │ │ ├── 66.png │ │ ├── 67.png │ │ ├── 68.png │ │ ├── 69.png │ │ ├── 7.png │ │ ├── 70.png │ │ ├── 71.png │ │ ├── 72.png │ │ ├── 8.png │ │ └── 9.png │ └── yellow-face │ │ ├── 1.png │ │ ├── 10.png │ │ ├── 11.png │ │ ├── 12.png │ │ ├── 13.png │ │ ├── 14.png │ │ ├── 15.png │ │ ├── 16.png │ │ ├── 17.png │ │ ├── 18.png │ │ ├── 19.png │ │ ├── 2.png │ │ ├── 20.png │ │ ├── 21.png │ │ ├── 22.png │ │ ├── 23.png │ │ ├── 24.png │ │ ├── 25.png │ │ ├── 26.png │ │ ├── 27.png │ │ ├── 28.png │ │ ├── 29.png │ │ ├── 3.png │ │ ├── 30.png │ │ ├── 31.png │ │ ├── 32.png │ │ ├── 33.png │ │ ├── 34.png │ │ ├── 35.png │ │ ├── 36.png │ │ ├── 37.png │ │ ├── 38.png │ │ ├── 39.png │ │ ├── 4.png │ │ ├── 40.png │ │ ├── 41.png │ │ ├── 42.png │ │ ├── 43.png │ │ ├── 44.png │ │ ├── 45.png │ │ ├── 46.png │ │ ├── 47.png │ │ ├── 48.png │ │ ├── 49.png │ │ ├── 5.png │ │ ├── 50.png │ │ ├── 51.png │ │ ├── 52.png │ │ ├── 53.png │ │ ├── 54.png │ │ ├── 55.png │ │ ├── 56.png │ │ ├── 57.png │ │ ├── 58.png │ │ ├── 59.png │ │ ├── 6.png │ │ ├── 60.png │ │ ├── 61.png │ │ ├── 62.png │ │ ├── 63.png │ │ ├── 64.png │ │ ├── 65.png │ │ ├── 66.png │ │ ├── 67.png │ │ ├── 68.png │ │ ├── 69.png │ │ ├── 7.png │ │ ├── 70.png │ │ ├── 71.png │ │ ├── 8.png │ │ └── 9.png └── BingSiteAuth.xml ├── app ├── utils │ ├── api │ │ ├── db │ │ │ ├── index.ts │ │ │ └── visitors.ts │ │ └── smms │ │ │ ├── index.ts │ │ │ └── upload.ts │ ├── nuxt │ │ ├── constants.ts │ │ ├── localStorage.ts │ │ ├── public │ │ │ ├── index.ts │ │ │ ├── list.ts │ │ │ └── detail.ts │ │ ├── manage │ │ │ ├── list.ts │ │ │ └── __github.ts │ │ ├── notify │ │ │ └── index.ts │ │ ├── i18n.ts │ │ ├── viewer.ts │ │ ├── format-time.ts │ │ └── fetch.ts │ ├── hooks │ │ ├── useUnmount.ts │ │ ├── useMarkdownParser.test.ts │ │ ├── useRouteQuery.ts │ │ ├── useBlogList.ts │ │ ├── useMarkdownParser.ts │ │ ├── useBlogList.test.ts │ │ └── useBlogItem.ts │ └── common │ │ ├── constants.ts │ │ ├── dayjs.ts │ │ ├── scroll-event.ts │ │ ├── locales.ts │ │ ├── utils.ts │ │ ├── hljs.ts │ │ ├── process-encrypt-decrypt.ts │ │ └── types.ts ├── assets │ ├── image │ │ └── outerwilds.jpg │ └── style │ │ ├── tailwind.css │ │ ├── lxgwwenkai.css │ │ ├── main.css │ │ ├── source-han-sans.css │ │ ├── fira-code.css │ │ └── code-style.css ├── layouts │ └── blank.vue ├── pages │ ├── manage │ │ ├── index.vue │ │ ├── md-ref │ │ │ └── index.vue │ │ ├── articles │ │ │ └── index.vue │ │ ├── records │ │ │ └── index.vue │ │ ├── knowledges │ │ │ └── index.vue │ │ ├── comps │ │ │ ├── sticker-pick.vue │ │ │ └── version-update-modal.vue │ │ └── config │ │ │ └── index.vue │ ├── about │ │ └── index.vue │ ├── records │ │ └── [id].vue │ └── knowledges │ │ └── [id].vue ├── middleware │ └── layout.global.ts ├── plugins │ ├── seo.ts │ ├── hot-event.client.ts │ ├── loading.client.ts │ ├── beforeunload.client.ts │ ├── i18n.ts │ ├── all.client.ts │ ├── monaco.client.ts │ └── github-token.client.ts ├── composables │ ├── i18n.ts │ ├── themeMode.ts │ ├── states.ts │ └── loading.ts └── components │ ├── common-checkbox.vue │ ├── the-tag.vue │ ├── common-button.vue │ └── common-dropdown.vue ├── server ├── tsconfig.json └── api │ ├── smms │ ├── list.ts │ ├── delete.ts │ └── upload.ts │ └── db │ ├── get-visitors.ts │ └── inc-visitors.ts ├── scripts ├── tsconfig.json ├── gulpfile.ts ├── change-pwd.ts ├── utils │ ├── html.ts │ ├── rss.ts │ ├── index.ts │ └── encrypt.ts ├── substitute-img.ts ├── generate-img-map.ts └── download-img.ts ├── vite-plugins ├── types.ts ├── index.ts ├── dev-import.ts └── rebuild.ts ├── Dockerfile ├── .vscode ├── launch.json └── settings.json ├── vercel.json ├── gulpfile.js ├── tsconfig.json ├── env.sample ├── e2e ├── page-objects │ └── manage │ │ ├── ConfigPage.ts │ │ ├── ListPage.ts │ │ └── BasePage.ts └── manage │ ├── config.test.ts │ ├── test-helpers.ts │ ├── item-delete.test.ts │ ├── item-create.test.ts │ ├── list.test.ts │ └── item-edit.test.ts ├── vitest.config.mts ├── LICENSE ├── shim.d.ts ├── eslint.config.mjs ├── config.ts ├── tailwind.config.mjs └── .gitignore /.nvmrc: -------------------------------------------------------------------------------- 1 | 20 -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx lint-staged -------------------------------------------------------------------------------- /public/e2e/rebuild/articles/1111.md: -------------------------------------------------------------------------------- 1 | test -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: /manage/ -------------------------------------------------------------------------------- /app/utils/api/db/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./visitors"; 2 | -------------------------------------------------------------------------------- /app/utils/api/smms/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./upload"; 2 | -------------------------------------------------------------------------------- /public/rebuild/knowledges/27345828.md: -------------------------------------------------------------------------------- 1 | Just for testing! -------------------------------------------------------------------------------- /public/rebuild/records/77070256.md: -------------------------------------------------------------------------------- 1 | Add your records here! -------------------------------------------------------------------------------- /public/e2e/rebuild/articles/3333.md: -------------------------------------------------------------------------------- 1 | U2FsdGVkX18wyEu7vCLMOGilOsG2cQdWY+kvi3b+AZE= -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.nuxt/tsconfig.server.json" 3 | } 4 | -------------------------------------------------------------------------------- /public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/icon.png -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/favicon.png -------------------------------------------------------------------------------- /public/google646c319d599abc5c.html: -------------------------------------------------------------------------------- 1 | google-site-verification: google646c319d599abc5c.html; 2 | -------------------------------------------------------------------------------- /scripts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true 4 | } 5 | } -------------------------------------------------------------------------------- /public/how-to-upgrade.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/how-to-upgrade.png -------------------------------------------------------------------------------- /public/sticker/aru/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/1.png -------------------------------------------------------------------------------- /public/sticker/aru/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/10.png -------------------------------------------------------------------------------- /public/sticker/aru/100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/100.png -------------------------------------------------------------------------------- /public/sticker/aru/101.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/101.png -------------------------------------------------------------------------------- /public/sticker/aru/102.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/102.png -------------------------------------------------------------------------------- /public/sticker/aru/103.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/103.png -------------------------------------------------------------------------------- /public/sticker/aru/104.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/104.png -------------------------------------------------------------------------------- /public/sticker/aru/105.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/105.png -------------------------------------------------------------------------------- /public/sticker/aru/106.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/106.png -------------------------------------------------------------------------------- /public/sticker/aru/107.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/107.png -------------------------------------------------------------------------------- /public/sticker/aru/108.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/108.png -------------------------------------------------------------------------------- /public/sticker/aru/109.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/109.png -------------------------------------------------------------------------------- /public/sticker/aru/11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/11.png -------------------------------------------------------------------------------- /public/sticker/aru/110.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/110.png -------------------------------------------------------------------------------- /public/sticker/aru/111.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/111.png -------------------------------------------------------------------------------- /public/sticker/aru/112.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/112.png -------------------------------------------------------------------------------- /public/sticker/aru/113.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/113.png -------------------------------------------------------------------------------- /public/sticker/aru/114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/114.png -------------------------------------------------------------------------------- /public/sticker/aru/115.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/115.png -------------------------------------------------------------------------------- /public/sticker/aru/116.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/116.png -------------------------------------------------------------------------------- /public/sticker/aru/117.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/117.png -------------------------------------------------------------------------------- /public/sticker/aru/118.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/118.png -------------------------------------------------------------------------------- /public/sticker/aru/119.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/119.png -------------------------------------------------------------------------------- /public/sticker/aru/12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/12.png -------------------------------------------------------------------------------- /public/sticker/aru/120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/120.png -------------------------------------------------------------------------------- /public/sticker/aru/121.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/121.png -------------------------------------------------------------------------------- /public/sticker/aru/122.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/122.png -------------------------------------------------------------------------------- /public/sticker/aru/123.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/123.png -------------------------------------------------------------------------------- /public/sticker/aru/124.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/124.png -------------------------------------------------------------------------------- /public/sticker/aru/125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/125.png -------------------------------------------------------------------------------- /public/sticker/aru/126.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/126.png -------------------------------------------------------------------------------- /public/sticker/aru/127.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/127.png -------------------------------------------------------------------------------- /public/sticker/aru/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/128.png -------------------------------------------------------------------------------- /public/sticker/aru/129.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/129.png -------------------------------------------------------------------------------- /public/sticker/aru/13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/13.png -------------------------------------------------------------------------------- /public/sticker/aru/130.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/130.png -------------------------------------------------------------------------------- /public/sticker/aru/131.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/131.png -------------------------------------------------------------------------------- /public/sticker/aru/132.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/132.png -------------------------------------------------------------------------------- /public/sticker/aru/133.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/133.png -------------------------------------------------------------------------------- /public/sticker/aru/134.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/134.png -------------------------------------------------------------------------------- /public/sticker/aru/135.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/135.png -------------------------------------------------------------------------------- /public/sticker/aru/136.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/136.png -------------------------------------------------------------------------------- /public/sticker/aru/137.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/137.png -------------------------------------------------------------------------------- /public/sticker/aru/138.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/138.png -------------------------------------------------------------------------------- /public/sticker/aru/139.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/139.png -------------------------------------------------------------------------------- /public/sticker/aru/14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/14.png -------------------------------------------------------------------------------- /public/sticker/aru/140.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/140.png -------------------------------------------------------------------------------- /public/sticker/aru/141.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/141.png -------------------------------------------------------------------------------- /public/sticker/aru/142.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/142.png -------------------------------------------------------------------------------- /public/sticker/aru/143.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/143.png -------------------------------------------------------------------------------- /public/sticker/aru/15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/15.png -------------------------------------------------------------------------------- /public/sticker/aru/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/16.png -------------------------------------------------------------------------------- /public/sticker/aru/17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/17.png -------------------------------------------------------------------------------- /public/sticker/aru/18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/18.png -------------------------------------------------------------------------------- /public/sticker/aru/19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/19.png -------------------------------------------------------------------------------- /public/sticker/aru/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/2.png -------------------------------------------------------------------------------- /public/sticker/aru/20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/20.png -------------------------------------------------------------------------------- /public/sticker/aru/21.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/21.png -------------------------------------------------------------------------------- /public/sticker/aru/22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/22.png -------------------------------------------------------------------------------- /public/sticker/aru/23.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/23.png -------------------------------------------------------------------------------- /public/sticker/aru/24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/24.png -------------------------------------------------------------------------------- /public/sticker/aru/25.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/25.png -------------------------------------------------------------------------------- /public/sticker/aru/26.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/26.png -------------------------------------------------------------------------------- /public/sticker/aru/27.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/27.png -------------------------------------------------------------------------------- /public/sticker/aru/28.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/28.png -------------------------------------------------------------------------------- /public/sticker/aru/29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/29.png -------------------------------------------------------------------------------- /public/sticker/aru/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/3.png -------------------------------------------------------------------------------- /public/sticker/aru/30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/30.png -------------------------------------------------------------------------------- /public/sticker/aru/31.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/31.png -------------------------------------------------------------------------------- /public/sticker/aru/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/32.png -------------------------------------------------------------------------------- /public/sticker/aru/33.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/33.png -------------------------------------------------------------------------------- /public/sticker/aru/34.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/34.png -------------------------------------------------------------------------------- /public/sticker/aru/35.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/35.png -------------------------------------------------------------------------------- /public/sticker/aru/36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/36.png -------------------------------------------------------------------------------- /public/sticker/aru/37.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/37.png -------------------------------------------------------------------------------- /public/sticker/aru/38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/38.png -------------------------------------------------------------------------------- /public/sticker/aru/39.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/39.png -------------------------------------------------------------------------------- /public/sticker/aru/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/4.png -------------------------------------------------------------------------------- /public/sticker/aru/40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/40.png -------------------------------------------------------------------------------- /public/sticker/aru/41.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/41.png -------------------------------------------------------------------------------- /public/sticker/aru/42.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/42.png -------------------------------------------------------------------------------- /public/sticker/aru/43.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/43.png -------------------------------------------------------------------------------- /public/sticker/aru/44.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/44.png -------------------------------------------------------------------------------- /public/sticker/aru/45.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/45.png -------------------------------------------------------------------------------- /public/sticker/aru/46.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/46.png -------------------------------------------------------------------------------- /public/sticker/aru/47.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/47.png -------------------------------------------------------------------------------- /public/sticker/aru/48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/48.png -------------------------------------------------------------------------------- /public/sticker/aru/49.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/49.png -------------------------------------------------------------------------------- /public/sticker/aru/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/5.png -------------------------------------------------------------------------------- /public/sticker/aru/50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/50.png -------------------------------------------------------------------------------- /public/sticker/aru/51.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/51.png -------------------------------------------------------------------------------- /public/sticker/aru/52.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/52.png -------------------------------------------------------------------------------- /public/sticker/aru/53.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/53.png -------------------------------------------------------------------------------- /public/sticker/aru/54.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/54.png -------------------------------------------------------------------------------- /public/sticker/aru/55.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/55.png -------------------------------------------------------------------------------- /public/sticker/aru/56.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/56.png -------------------------------------------------------------------------------- /public/sticker/aru/57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/57.png -------------------------------------------------------------------------------- /public/sticker/aru/58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/58.png -------------------------------------------------------------------------------- /public/sticker/aru/59.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/59.png -------------------------------------------------------------------------------- /public/sticker/aru/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/6.png -------------------------------------------------------------------------------- /public/sticker/aru/60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/60.png -------------------------------------------------------------------------------- /public/sticker/aru/61.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/61.png -------------------------------------------------------------------------------- /public/sticker/aru/62.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/62.png -------------------------------------------------------------------------------- /public/sticker/aru/63.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/63.png -------------------------------------------------------------------------------- /public/sticker/aru/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/64.png -------------------------------------------------------------------------------- /public/sticker/aru/65.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/65.png -------------------------------------------------------------------------------- /public/sticker/aru/66.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/66.png -------------------------------------------------------------------------------- /public/sticker/aru/67.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/67.png -------------------------------------------------------------------------------- /public/sticker/aru/68.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/68.png -------------------------------------------------------------------------------- /public/sticker/aru/69.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/69.png -------------------------------------------------------------------------------- /public/sticker/aru/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/7.png -------------------------------------------------------------------------------- /public/sticker/aru/70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/70.png -------------------------------------------------------------------------------- /public/sticker/aru/71.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/71.png -------------------------------------------------------------------------------- /public/sticker/aru/72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/72.png -------------------------------------------------------------------------------- /public/sticker/aru/73.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/73.png -------------------------------------------------------------------------------- /public/sticker/aru/74.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/74.png -------------------------------------------------------------------------------- /public/sticker/aru/75.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/75.png -------------------------------------------------------------------------------- /public/sticker/aru/76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/76.png -------------------------------------------------------------------------------- /public/sticker/aru/77.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/77.png -------------------------------------------------------------------------------- /public/sticker/aru/78.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/78.png -------------------------------------------------------------------------------- /public/sticker/aru/79.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/79.png -------------------------------------------------------------------------------- /public/sticker/aru/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/8.png -------------------------------------------------------------------------------- /public/sticker/aru/80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/80.png -------------------------------------------------------------------------------- /public/sticker/aru/81.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/81.png -------------------------------------------------------------------------------- /public/sticker/aru/82.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/82.png -------------------------------------------------------------------------------- /public/sticker/aru/83.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/83.png -------------------------------------------------------------------------------- /public/sticker/aru/84.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/84.png -------------------------------------------------------------------------------- /public/sticker/aru/85.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/85.png -------------------------------------------------------------------------------- /public/sticker/aru/86.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/86.png -------------------------------------------------------------------------------- /public/sticker/aru/87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/87.png -------------------------------------------------------------------------------- /public/sticker/aru/88.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/88.png -------------------------------------------------------------------------------- /public/sticker/aru/89.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/89.png -------------------------------------------------------------------------------- /public/sticker/aru/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/9.png -------------------------------------------------------------------------------- /public/sticker/aru/90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/90.png -------------------------------------------------------------------------------- /public/sticker/aru/91.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/91.png -------------------------------------------------------------------------------- /public/sticker/aru/92.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/92.png -------------------------------------------------------------------------------- /public/sticker/aru/93.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/93.png -------------------------------------------------------------------------------- /public/sticker/aru/94.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/94.png -------------------------------------------------------------------------------- /public/sticker/aru/95.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/95.png -------------------------------------------------------------------------------- /public/sticker/aru/96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/96.png -------------------------------------------------------------------------------- /public/sticker/aru/97.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/97.png -------------------------------------------------------------------------------- /public/sticker/aru/98.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/98.png -------------------------------------------------------------------------------- /public/sticker/aru/99.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/aru/99.png -------------------------------------------------------------------------------- /public/sticker/cube/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/1.png -------------------------------------------------------------------------------- /public/sticker/cube/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/10.png -------------------------------------------------------------------------------- /public/sticker/cube/11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/11.png -------------------------------------------------------------------------------- /public/sticker/cube/12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/12.png -------------------------------------------------------------------------------- /public/sticker/cube/13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/13.png -------------------------------------------------------------------------------- /public/sticker/cube/14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/14.png -------------------------------------------------------------------------------- /public/sticker/cube/15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/15.png -------------------------------------------------------------------------------- /public/sticker/cube/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/16.png -------------------------------------------------------------------------------- /public/sticker/cube/17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/17.png -------------------------------------------------------------------------------- /public/sticker/cube/18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/18.png -------------------------------------------------------------------------------- /public/sticker/cube/19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/19.png -------------------------------------------------------------------------------- /public/sticker/cube/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/2.png -------------------------------------------------------------------------------- /public/sticker/cube/20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/20.png -------------------------------------------------------------------------------- /public/sticker/cube/21.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/21.png -------------------------------------------------------------------------------- /public/sticker/cube/22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/22.png -------------------------------------------------------------------------------- /public/sticker/cube/23.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/23.png -------------------------------------------------------------------------------- /public/sticker/cube/24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/24.png -------------------------------------------------------------------------------- /public/sticker/cube/25.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/25.png -------------------------------------------------------------------------------- /public/sticker/cube/26.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/26.png -------------------------------------------------------------------------------- /public/sticker/cube/27.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/27.png -------------------------------------------------------------------------------- /public/sticker/cube/28.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/28.png -------------------------------------------------------------------------------- /public/sticker/cube/29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/29.png -------------------------------------------------------------------------------- /public/sticker/cube/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/3.png -------------------------------------------------------------------------------- /public/sticker/cube/30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/30.png -------------------------------------------------------------------------------- /public/sticker/cube/31.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/31.png -------------------------------------------------------------------------------- /public/sticker/cube/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/32.png -------------------------------------------------------------------------------- /public/sticker/cube/33.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/33.png -------------------------------------------------------------------------------- /public/sticker/cube/34.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/34.png -------------------------------------------------------------------------------- /public/sticker/cube/35.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/35.png -------------------------------------------------------------------------------- /public/sticker/cube/36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/36.png -------------------------------------------------------------------------------- /public/sticker/cube/37.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/37.png -------------------------------------------------------------------------------- /public/sticker/cube/38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/38.png -------------------------------------------------------------------------------- /public/sticker/cube/39.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/39.png -------------------------------------------------------------------------------- /public/sticker/cube/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/4.png -------------------------------------------------------------------------------- /public/sticker/cube/40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/40.png -------------------------------------------------------------------------------- /public/sticker/cube/41.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/41.png -------------------------------------------------------------------------------- /public/sticker/cube/42.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/42.png -------------------------------------------------------------------------------- /public/sticker/cube/43.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/43.png -------------------------------------------------------------------------------- /public/sticker/cube/44.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/44.png -------------------------------------------------------------------------------- /public/sticker/cube/45.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/45.png -------------------------------------------------------------------------------- /public/sticker/cube/46.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/46.png -------------------------------------------------------------------------------- /public/sticker/cube/47.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/47.png -------------------------------------------------------------------------------- /public/sticker/cube/48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/48.png -------------------------------------------------------------------------------- /public/sticker/cube/49.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/49.png -------------------------------------------------------------------------------- /public/sticker/cube/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/5.png -------------------------------------------------------------------------------- /public/sticker/cube/50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/50.png -------------------------------------------------------------------------------- /public/sticker/cube/51.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/51.png -------------------------------------------------------------------------------- /public/sticker/cube/52.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/52.png -------------------------------------------------------------------------------- /public/sticker/cube/53.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/53.png -------------------------------------------------------------------------------- /public/sticker/cube/54.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/54.png -------------------------------------------------------------------------------- /public/sticker/cube/55.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/55.png -------------------------------------------------------------------------------- /public/sticker/cube/56.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/56.png -------------------------------------------------------------------------------- /public/sticker/cube/57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/57.png -------------------------------------------------------------------------------- /public/sticker/cube/58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/58.png -------------------------------------------------------------------------------- /public/sticker/cube/59.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/59.png -------------------------------------------------------------------------------- /public/sticker/cube/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/6.png -------------------------------------------------------------------------------- /public/sticker/cube/60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/60.png -------------------------------------------------------------------------------- /public/sticker/cube/61.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/61.png -------------------------------------------------------------------------------- /public/sticker/cube/62.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/62.png -------------------------------------------------------------------------------- /public/sticker/cube/63.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/63.png -------------------------------------------------------------------------------- /public/sticker/cube/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/64.png -------------------------------------------------------------------------------- /public/sticker/cube/65.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/65.png -------------------------------------------------------------------------------- /public/sticker/cube/66.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/66.png -------------------------------------------------------------------------------- /public/sticker/cube/67.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/67.png -------------------------------------------------------------------------------- /public/sticker/cube/68.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/68.png -------------------------------------------------------------------------------- /public/sticker/cube/69.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/69.png -------------------------------------------------------------------------------- /public/sticker/cube/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/7.png -------------------------------------------------------------------------------- /public/sticker/cube/70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/70.png -------------------------------------------------------------------------------- /public/sticker/cube/71.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/71.png -------------------------------------------------------------------------------- /public/sticker/cube/72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/72.png -------------------------------------------------------------------------------- /public/sticker/cube/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/8.png -------------------------------------------------------------------------------- /public/sticker/cube/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/cube/9.png -------------------------------------------------------------------------------- /app/assets/image/outerwilds.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/app/assets/image/outerwilds.jpg -------------------------------------------------------------------------------- /public/BingSiteAuth.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | E7F7A24CB0CB9AFB668F36CF85C9D026 4 | -------------------------------------------------------------------------------- /public/sticker/yellow-face/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/1.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/10.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/11.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/12.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/13.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/14.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/15.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/16.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/17.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/18.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/19.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/2.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/20.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/21.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/21.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/22.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/23.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/23.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/24.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/25.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/25.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/26.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/26.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/27.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/27.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/28.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/28.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/29.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/3.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/30.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/31.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/31.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/32.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/33.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/33.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/34.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/34.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/35.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/35.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/36.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/37.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/37.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/38.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/39.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/39.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/4.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/40.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/41.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/41.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/42.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/42.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/43.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/43.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/44.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/44.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/45.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/45.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/46.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/46.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/47.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/47.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/48.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/49.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/49.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/5.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/50.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/51.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/51.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/52.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/52.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/53.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/53.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/54.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/54.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/55.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/55.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/56.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/56.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/57.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/58.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/59.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/59.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/6.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/60.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/61.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/61.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/62.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/62.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/63.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/63.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/64.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/65.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/65.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/66.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/66.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/67.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/67.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/68.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/68.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/69.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/69.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/7.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/70.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/71.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/71.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/8.png -------------------------------------------------------------------------------- /public/sticker/yellow-face/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunyuyuan/nuxt3-blog/HEAD/public/sticker/yellow-face/9.png -------------------------------------------------------------------------------- /vite-plugins/types.ts: -------------------------------------------------------------------------------- 1 | export const rebuildEvent = "nb:rebuild"; 2 | 3 | export const allHotEvent = [rebuildEvent]; 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-alpine 2 | 3 | WORKDIR /app 4 | 5 | COPY ./.output ./ 6 | 7 | ENTRYPOINT [ "node", "server/index.mjs" ] -------------------------------------------------------------------------------- /app/layouts/blank.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | -------------------------------------------------------------------------------- /app/utils/nuxt/constants.ts: -------------------------------------------------------------------------------- 1 | export const isPrerender = process.env.NODE_ENV === "prerender"; 2 | export const isDev = process.env.NODE_ENV === "development"; 3 | -------------------------------------------------------------------------------- /app/pages/manage/index.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /app/middleware/layout.global.ts: -------------------------------------------------------------------------------- 1 | export default defineNuxtRouteMiddleware((to) => { 2 | if (to.fullPath.startsWith("/manage")) { 3 | to.meta.layout = "manage"; 4 | } 5 | }); 6 | -------------------------------------------------------------------------------- /vite-plugins/index.ts: -------------------------------------------------------------------------------- 1 | import devImport from "./dev-import"; 2 | import rebuild from "./rebuild"; 3 | 4 | export const allPlugins = [devImport, rebuild]; 5 | export const buildPlugins = [devImport]; 6 | -------------------------------------------------------------------------------- /public/e2e/rebuild/articles/2222.md: -------------------------------------------------------------------------------- 1 | test 2 | [encrypt] 3 | U2FsdGVkX19x3vz71QYkQDP6Mo0nbZGCRDdIWK0DoBs= 4 | [/encrypt] 5 | test 6 | [encrypt] 7 | U2FsdGVkX191jslHj/zf0feLh6mxQ2l871FVGjPE5Kg= 8 | [/encrypt] -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "command": "pnpm dev", 5 | "name": "Dev", 6 | "request": "launch", 7 | "type": "node-terminal" 8 | } 9 | ] 10 | } -------------------------------------------------------------------------------- /app/plugins/seo.ts: -------------------------------------------------------------------------------- 1 | export default defineNuxtPlugin((app) => { 2 | app.hook("vue:setup", () => { 3 | useSeoMeta({ 4 | ogImage: "/icon.png", 5 | twitterCard: "summary" 6 | }); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /public/rebuild/json/articles.json: -------------------------------------------------------------------------------- 1 | [{"title":"Welcome to your new blog!","len":3820,"tags":["test"],"id":93332711,"customSlug":"test-article","time":1759475464661,"modifyTime":1759475464661,"showComments":true,"encrypt":false}] -------------------------------------------------------------------------------- /app/utils/hooks/useUnmount.ts: -------------------------------------------------------------------------------- 1 | export const useUnmount = () => { 2 | const destroyFns: CallableFunction[] = []; 3 | 4 | onBeforeUnmount(() => { 5 | destroyFns.forEach(fn => fn()); 6 | }); 7 | return destroyFns; 8 | }; 9 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "framework": null, 3 | "buildCommand": "pnpm build", 4 | "installCommand": "pnpm install", 5 | "github": { 6 | "silent": true, 7 | "autoJobCancelation": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /server/api/smms/list.ts: -------------------------------------------------------------------------------- 1 | export default defineEventHandler((event) => { 2 | if (event.node.req.method?.toUpperCase() !== "POST") { 3 | throw createError({ 4 | statusCode: 405, 5 | data: "Post only!" 6 | }); 7 | } 8 | }); 9 | -------------------------------------------------------------------------------- /server/api/smms/delete.ts: -------------------------------------------------------------------------------- 1 | export default defineEventHandler((event) => { 2 | if (event.node.req.method?.toUpperCase() !== "POST") { 3 | throw createError({ 4 | statusCode: 405, 5 | data: "Post only!" 6 | }); 7 | } 8 | }); 9 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-require-imports */ 2 | const path = require("path"); 3 | 4 | require("ts-node").register({ 5 | project: path.join(__dirname, "./scripts/tsconfig.json") 6 | }); 7 | 8 | require("./scripts/gulpfile"); 9 | -------------------------------------------------------------------------------- /public/rebuild/json/records.json: -------------------------------------------------------------------------------- 1 | [{"images":[{"src":"https://github.com/yunyuyuan/nuxt3-blog/blob/master/public/favicon.png?raw=true","alt":"nuxt3-blog"}],"id":77070256,"customSlug":"2025-10-03","time":1759475507353,"modifyTime":1759475632531,"showComments":false,"encrypt":false}] 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://v3.nuxtjs.org/concepts/typescript 3 | "exclude": [ 4 | "**/node_modules/*", 5 | "./dist", 6 | "./.output" 7 | ], 8 | "vueCompilerOptions": { 9 | "experimentalRfc436": true 10 | }, 11 | "extends": "./.nuxt/tsconfig.json" 12 | } -------------------------------------------------------------------------------- /env.sample: -------------------------------------------------------------------------------- 1 | # for visitors statistics 2 | CLOUDFLARE_D1_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 3 | CLOUDFLARE_D1_DATABASE_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx 4 | CLOUDFLARE_D1_ACCOUNT_ID=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 5 | 6 | ALGOLIA_ADMIN_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 7 | -------------------------------------------------------------------------------- /e2e/page-objects/manage/ConfigPage.ts: -------------------------------------------------------------------------------- 1 | import { ManageBasePage } from "./BasePage"; 2 | 3 | export class ManageConfigPage extends ManageBasePage { 4 | async updateConfig(content: string) { 5 | await this.clearAndTypeInMonacoEditor(content); 6 | await this.clickElement("update-config-btn"); 7 | await this.waitForTimeout(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /public/rebuild/json/knowledges.json: -------------------------------------------------------------------------------- 1 | [{"title":"Test Item","summary":"I'm a test item","link":"https://github.com/yunyuyuan/nuxt3-blog","cover":"https://github.com/yunyuyuan/nuxt3-blog/blob/master/public/favicon.png?raw=true","type":"film","id":27345828,"customSlug":"test-item","time":1759475113757,"modifyTime":1759475113757,"showComments":true,"encrypt":false}] 2 | -------------------------------------------------------------------------------- /vitest.config.mts: -------------------------------------------------------------------------------- 1 | import { defineVitestConfig } from "@nuxt/test-utils/config"; 2 | 3 | export default defineVitestConfig({ 4 | test: { 5 | environment: "nuxt", 6 | testTimeout: 25000, 7 | alias: [ 8 | { 9 | find: /^monaco-editor$/, 10 | replacement: 11 | __dirname + "/node_modules/monaco-editor/esm/vs/editor/editor.api" 12 | } 13 | ] 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /app/utils/nuxt/localStorage.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * localStorage 操作 3 | */ 4 | export function getLocalStorage(key: string): T | null { 5 | return window?.localStorage?.getItem(key) as T; 6 | } 7 | 8 | export function setLocalStorage(key: string, value: string) { 9 | window?.localStorage?.setItem(key, value); 10 | } 11 | 12 | export function rmLocalStorage(key: string) { 13 | window?.localStorage?.removeItem(key); 14 | } 15 | -------------------------------------------------------------------------------- /app/utils/common/constants.ts: -------------------------------------------------------------------------------- 1 | export const NotificationContainerId = "_NOTIFICATION_CONTAINER_"; 2 | export const ModalContainerId = "_MODAL_CONTAINER_"; 3 | export const ViewerAttr = "data-viewer"; 4 | export const I18nStoreKey = "nb-lang"; 5 | export const GithubTokenKey = "nb-github-token"; 6 | export const ThemeModeKey = "nb-theme-mode"; 7 | export const IgnoredVersionKey = "nb-ignored-version"; 8 | export const OfficialRepo = "yunyuyuan/nuxt3-blog"; 9 | -------------------------------------------------------------------------------- /app/plugins/hot-event.client.ts: -------------------------------------------------------------------------------- 1 | import { isDev } from "~/utils/nuxt/constants"; 2 | import { allHotEvent } from "~~/vite-plugins/types"; 3 | 4 | // HACK need `import.meta.hot.off()` 5 | export default defineNuxtPlugin(() => { 6 | if (isDev) { 7 | for (const e of allHotEvent) { 8 | import.meta.hot!.on(e, (data) => { 9 | window.dispatchEvent(new CustomEvent(e, { 10 | detail: data 11 | })); 12 | }); 13 | } 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /app/utils/nuxt/public/index.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { isPrerender } from "~/utils/nuxt/constants"; 3 | 4 | export function DBOperate ( 5 | { apiPath, query, callback }: 6 | { apiPath: string, query: any, callback: (_: T) => any} 7 | ) { 8 | if (import.meta.client && !isPrerender && __NB_DATABASE_ENABLED__) { 9 | const cb = (data: T) => { 10 | try { 11 | callback(data); 12 | } catch { } 13 | }; 14 | 15 | axios.post(`/api${apiPath}`, query).then(res => cb(res.data)); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/plugins/loading.client.ts: -------------------------------------------------------------------------------- 1 | export default defineNuxtPlugin((app) => { 2 | const router = useRouter(); 3 | const loading = useLoading(); 4 | const firstLoad = useFirstLoad(); 5 | let index = 0; 6 | router.beforeEach((to, from) => { 7 | if (to.path === from.path) { 8 | return; 9 | } 10 | if (index < 2) { 11 | index += 1; 12 | } 13 | if (index === 2 && firstLoad.value) { 14 | firstLoad.value = false; 15 | } 16 | loading.start(); 17 | }); 18 | 19 | app.hook("page:finish", () => { 20 | loading.finish(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /app/plugins/beforeunload.client.ts: -------------------------------------------------------------------------------- 1 | import { translate } from "~/utils/nuxt/i18n"; 2 | 3 | export default defineNuxtPlugin(() => { 4 | const unSavedContent = useUnsavedContent(); 5 | window.onbeforeunload = () => { 6 | if (unSavedContent.value) { 7 | return translate("confirm-leave"); 8 | } 9 | }; 10 | addRouteMiddleware("unload", () => { 11 | if (unSavedContent.value) { 12 | if (!confirm(translate("confirm-leave"))) { 13 | return abortNavigation(); 14 | } 15 | unSavedContent.value = false; 16 | } 17 | }, { global: true }); 18 | }); 19 | -------------------------------------------------------------------------------- /server/api/db/get-visitors.ts: -------------------------------------------------------------------------------- 1 | import { readBody, createError } from "h3"; 2 | import { getVisitors } from "../../../app/utils/api/db"; 3 | 4 | export default defineEventHandler(async (event) => { 5 | if (event.node.req.method?.toUpperCase() !== "POST") { 6 | throw createError({ 7 | statusCode: 405, 8 | data: "Post only!" 9 | }); 10 | } 11 | 12 | try { 13 | const args = await readBody(event); 14 | return await getVisitors(args.type); 15 | } catch (e: any) { 16 | return createError({ 17 | statusCode: 503, 18 | data: e.toString() 19 | }); 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /vite-plugins/dev-import.ts: -------------------------------------------------------------------------------- 1 | import type { Plugin } from "vite"; 2 | 3 | const LOCAL_SERVER = "ls:"; 4 | 5 | export default { 6 | name: "nb-dev-import-plugin", 7 | resolveId(source, importer, options) { 8 | if (source.startsWith(LOCAL_SERVER)) { 9 | const realPath = source.slice(LOCAL_SERVER.length); 10 | const id = process.env.NODE_ENV === "development" && process.env.VITESTING !== "true" ? realPath.replace(/([^/]*)$/, "__$1") : realPath; 11 | return this.resolve(id, importer, options).then(resolved => resolved || { id }); 12 | } 13 | return null; 14 | } 15 | } as Plugin; 16 | -------------------------------------------------------------------------------- /app/utils/hooks/useMarkdownParser.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { useMarkdownParser } from "./useMarkdownParser"; 3 | 4 | const timeout = () => new Promise(resolve => setTimeout(resolve, 16)); 5 | 6 | describe("useMarkdownParser", () => { 7 | it("should works", async () => { 8 | const mdValueRef = ref("# Hello World\n# test ![sticker](aru/1)"); 9 | const { htmlContent, menuItems } = await useMarkdownParser({ mdValueRef }); 10 | await timeout(); 11 | expect(htmlContent.value).toContain(">Hello World 2 | import MdEditor from "~/pages/manage/comps/md-editor.vue"; 3 | import mdSample from "~/assets/style/markdown.md?raw"; 4 | import config from "~~/config"; 5 | import { useCommonSEOTitle } from "~/utils/nuxt/utils"; 6 | import { translate } from "~/utils/nuxt/i18n"; 7 | 8 | const inputMarkdown = ref(mdSample); 9 | 10 | useCommonSEOTitle(computed(() => translate("markdown-ref") + config.SEO_title)); 11 | 12 | 13 | 21 | -------------------------------------------------------------------------------- /e2e/manage/config.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { createConfigPage, setupTestEnvironment } from "./test-helpers"; 3 | 4 | describe("Config Editing", async () => { 5 | await setupTestEnvironment(); 6 | 7 | it("Editing Works", async () => { 8 | const { configPage } = await createConfigPage("/manage/config"); 9 | 10 | const btn = await configPage.getByTestId("update-config-btn"); 11 | expect(await btn.isDisabled()).toBe(true); 12 | 13 | await configPage.updateConfig("{test-config}"); 14 | 15 | expect(configPage.requestAdditions[0].content).toContain("{test-config}"); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /app/composables/i18n.ts: -------------------------------------------------------------------------------- 1 | import config from "~~/config"; 2 | import { I18nStoreKey } from "~/utils/common/constants"; 3 | import type { I18nCode } from "~/utils/common/locales"; 4 | import { loadI18nJson } from "~/utils/nuxt/i18n"; 5 | import { setLocalStorage } from "~/utils/nuxt/localStorage"; 6 | 7 | export const useI18nCode = () => { 8 | const i18nCode = useState(I18nStoreKey, () => config.defaultLang as I18nCode); 9 | 10 | return { 11 | i18nCode, 12 | changeI18n: async (code: I18nCode) => { 13 | await loadI18nJson(code); 14 | i18nCode.value = code; 15 | setLocalStorage(I18nStoreKey, code); 16 | } 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /app/composables/themeMode.ts: -------------------------------------------------------------------------------- 1 | import { ThemeModeKey } from "~/utils/common/constants"; 2 | import { setLocalStorage } from "~/utils/nuxt/localStorage"; 3 | 4 | export const useThemeMode = () => { 5 | const themeMode = useState<"light" | "dark" | "">(ThemeModeKey, () => ""); 6 | 7 | const toggleThemeMode = () => { 8 | document.documentElement.classList.remove(themeMode.value); 9 | themeMode.value = themeMode.value === "light" ? "dark" : "light"; 10 | document.documentElement.classList.add(themeMode.value); 11 | setLocalStorage(ThemeModeKey, themeMode.value); 12 | }; 13 | return { 14 | themeMode, 15 | toggleThemeMode 16 | }; 17 | }; 18 | -------------------------------------------------------------------------------- /app/utils/hooks/useRouteQuery.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 因为hydration无法正确处理`useRoute().query`的初始化,所以不能使用`computed(() => useRoute().query.xxx)`这种形式来获取动态的query值 3 | * @param query query名 4 | * @param parse parse函数 5 | * @returns 默认值是`parse(undefined)` 6 | */ 7 | export const useRouteQuery = (query: string, parse: ((_?: string) => T) = q => (q as T)) => { 8 | const result = ref(parse()); 9 | const route = useRoute(); 10 | 11 | onMounted(() => { 12 | result.value = parse(route.query[query] as string); 13 | }); 14 | 15 | watch(() => route.query[query], (newQuery) => { 16 | result.value = parse(newQuery as string); 17 | }); 18 | 19 | return result; 20 | }; 21 | -------------------------------------------------------------------------------- /app/utils/common/dayjs.ts: -------------------------------------------------------------------------------- 1 | import dayjs from "dayjs"; 2 | import weekOfYear from "dayjs/plugin/weekOfYear.js"; 3 | import utc from "dayjs/plugin/utc.js"; 4 | import timezone from "dayjs/plugin/timezone"; 5 | import isToday from "dayjs/plugin/isToday.js"; 6 | 7 | dayjs.extend(weekOfYear); 8 | dayjs.extend(utc); 9 | dayjs.extend(timezone); 10 | dayjs.extend(isToday); 11 | 12 | export function getNowDayjs(time?: number) { 13 | return dayjs(time).tz(); 14 | } 15 | 16 | export function getNowDayjsString(time?: number) { 17 | return getNowDayjs(time).toDate().toUTCString(); 18 | } 19 | 20 | export function getNowStamp(time?: number) { 21 | return getNowDayjs(time).toDate().getTime(); 22 | } 23 | -------------------------------------------------------------------------------- /server/api/db/inc-visitors.ts: -------------------------------------------------------------------------------- 1 | import { readBody, createError } from "h3"; 2 | import { increaseVisitors } from "../../../app/utils/api/db"; 3 | 4 | export default defineEventHandler(async (event) => { 5 | if (event.node.req.method?.toUpperCase() !== "POST") { 6 | throw createError({ 7 | statusCode: 405, 8 | data: "Post only!" 9 | }); 10 | } 11 | try { 12 | const args = await readBody(event); 13 | return await increaseVisitors({ 14 | id: args.id, 15 | type: args.type, 16 | inc: args.inc 17 | }); 18 | } catch (e: any) { 19 | return createError({ 20 | statusCode: 503, 21 | data: e.toString() 22 | }); 23 | } 24 | }); 25 | -------------------------------------------------------------------------------- /app/utils/common/scroll-event.ts: -------------------------------------------------------------------------------- 1 | import throttle from "lodash/throttle.js"; 2 | 3 | let scrollEvent: Event; 4 | const eventName = "scroll-event"; 5 | 6 | export function initScrollTrigger() { 7 | scrollEvent = new CustomEvent(eventName); 8 | window.addEventListener( 9 | "scroll", 10 | throttle(() => { 11 | window.dispatchEvent(scrollEvent); 12 | }, 200) 13 | ); 14 | } 15 | 16 | export function addScrollListener(callback: () => void) { 17 | if (scrollEvent) { 18 | window.addEventListener(eventName, callback); 19 | } 20 | } 21 | 22 | export function rmScrollListener(callback: () => void) { 23 | if (scrollEvent) { 24 | window.removeEventListener(eventName, callback); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/utils/nuxt/manage/list.ts: -------------------------------------------------------------------------------- 1 | import { translate } from "../i18n"; 2 | import { getCurrentTab, useCommonSEOTitle } from "../utils"; 3 | import type { CommonItem } from "~/utils/common/types"; 4 | import { useBlogList } from "~/utils/hooks/useBlogList"; 5 | 6 | /** 7 | * 管理页面列表通用功能 8 | */ 9 | export async function useManageList() { 10 | const targetTab = getCurrentTab(); 11 | 12 | useCommonSEOTitle(computed(() => translate("list-manage", [translate(targetTab)]))); 13 | 14 | const { decryptedList, originList } = await useBlogList(targetTab); 15 | 16 | return { 17 | originList: originList, 18 | list: decryptedList.value, 19 | targetTab, 20 | decryptedList 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /e2e/page-objects/manage/ListPage.ts: -------------------------------------------------------------------------------- 1 | import { ManageBasePage } from "./BasePage"; 2 | 3 | export class ManageListPage extends ManageBasePage { 4 | async selectItemByIndex(index: number) { 5 | const checkbox = await this.getByTestId(`list-item-check-${index}`); 6 | await checkbox.click(); 7 | await this.waitForTimeout(); 8 | return checkbox; 9 | } 10 | 11 | async deleteSelectedItems() { 12 | const deleteBtn = await this.getByTestId("list-delete-btn"); 13 | await deleteBtn.click(); 14 | await this.clickElement("confirm-list-delete"); 15 | await this.waitForTimeout(); 16 | } 17 | 18 | async getListItemsText() { 19 | const listItems = await this.getByTestId("list-items"); 20 | return listItems.innerText(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/utils/common/locales.ts: -------------------------------------------------------------------------------- 1 | export const i18nLocales = [ 2 | { 3 | code: "en", 4 | iso: "en", 5 | file: "en.json", 6 | name: "English", 7 | formatFull: "MM/DD/YYYY HH:mm:ss", 8 | formatDate: "MM/DD/YYYY", 9 | formatMonth: "MM/DD" 10 | }, 11 | { 12 | code: "zh", 13 | iso: "cn", 14 | file: "zh.json", 15 | name: "中文", 16 | formatFull: "YYYY-MM-DD HH:mm:ss", 17 | formatDate: "YYYY-MM-DD", 18 | formatMonth: "MM-DD" 19 | } 20 | ] as const; 21 | 22 | export const allLocales = i18nLocales.map(item => item.code); 23 | export type I18nCode = typeof allLocales[number]; 24 | 25 | export const getLocaleByCode = (code: typeof allLocales[number]) => { 26 | return i18nLocales.find(locale => locale.code === code); 27 | }; 28 | -------------------------------------------------------------------------------- /public/e2e/rebuild/json/articles.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 1111, 4 | "time": 0, 5 | "modifyTime": 0, 6 | "encrypt": false, 7 | "showComments": true, 8 | "title": "test", 9 | "len": 4, 10 | "tags": ["tag1", "tag2"] 11 | },{ 12 | "id": 2222, 13 | "time": 0, 14 | "modifyTime": 0, 15 | "encrypt": false, 16 | "encryptBlocks": [{"start":86,"end":130},{"start":15,"end":59}], 17 | "showComments": false, 18 | "title": "test", 19 | "len": 61, 20 | "tags": ["tag1", "tag2"] 21 | },{ 22 | "id": 3333, 23 | "time": 0, 24 | "modifyTime": 0, 25 | "encrypt": true, 26 | "showComments": false, 27 | "title": "U2FsdGVkX18wyEu7vCLMOGilOsG2cQdWY+kvi3b+AZE=", 28 | "len": 4, 29 | "tags": [] 30 | } 31 | ] -------------------------------------------------------------------------------- /app/utils/nuxt/notify/index.ts: -------------------------------------------------------------------------------- 1 | import { createVNode, render } from "vue"; 2 | import notification from "./common-notification.vue"; 3 | import { NotificationContainerId } from "~/utils/common/constants"; 4 | 5 | export type NotifyType = "success" | "warn" | "error"; 6 | 7 | export type NotifyOption = { 8 | type?: NotifyType; 9 | title: string; 10 | description?: string; 11 | }; 12 | 13 | export function notify(options: NotifyOption) { 14 | const container = document.createElement("div"); 15 | const vm = createVNode(notification, options); 16 | vm.props!.onDestroy = () => { 17 | render(null, container); 18 | }; 19 | render(vm, container); 20 | document 21 | .getElementById(NotificationContainerId)! 22 | .appendChild(container.firstElementChild!); 23 | } 24 | -------------------------------------------------------------------------------- /app/plugins/i18n.ts: -------------------------------------------------------------------------------- 1 | import config from "~~/config"; 2 | import { type I18nCode, i18nLocales } from "~/utils/common/locales"; 3 | import { getI18nJson, translate } from "~/utils/nuxt/i18n"; 4 | 5 | export default defineNuxtPlugin(async (app) => { 6 | app.hook("vue:setup", () => { 7 | const { i18nCode } = useI18nCode(); 8 | useHead({ 9 | // @ts-expect-error only language attribute 10 | htmlAttrs: computed(() => { 11 | return { 12 | lang: i18nLocales.find(i => i.code === i18nCode.value)!.iso 13 | }; 14 | }) 15 | }); 16 | }); 17 | return { 18 | provide: { 19 | i18nMessages: ref({ 20 | [config.defaultLang as I18nCode]: await getI18nJson(config.defaultLang as I18nCode) 21 | }), 22 | t: (...args: Parameters) => translate(...args) 23 | } 24 | }; 25 | }); 26 | -------------------------------------------------------------------------------- /server/api/smms/upload.ts: -------------------------------------------------------------------------------- 1 | import { readMultipartFormData, createError } from "h3"; 2 | import { smmsUpload } from "../../../app/utils/api/smms"; 3 | 4 | export default defineEventHandler(async (event) => { 5 | if (event.node.req.method?.toUpperCase() !== "POST") { 6 | throw createError({ 7 | statusCode: 405, 8 | data: "Post only!" 9 | }); 10 | } 11 | try { 12 | const form = await readMultipartFormData(event); 13 | const token = form!.find(i => i.name === "token")?.data.toString(); 14 | const tinyPngToken = form!.find(i => i.name === "tinyPngToken")?.data.toString(); 15 | const file = form!.find(i => i.name === "file"); 16 | 17 | return await smmsUpload({ token, tinyPngToken, file }); 18 | } catch (e: any) { 19 | return createError({ 20 | statusCode: 503, 21 | data: e.toString() 22 | }); 23 | } 24 | }); 25 | -------------------------------------------------------------------------------- /scripts/gulpfile.ts: -------------------------------------------------------------------------------- 1 | import gulp from "gulp"; 2 | import chpwd from "./change-pwd"; 3 | import genImgMap from "./generate-img-map"; 4 | import subImg from "./substitute-img"; 5 | import downloadImg from "./download-img"; 6 | import { uploadAlgoliaIndex } from "./nuxt-hooks"; 7 | 8 | gulp.task("generate-image-map", async (cb) => { 9 | await genImgMap(process.env.NB_PASSWD, process.env.NB_IMG_REGEX); 10 | cb(); 11 | }); 12 | 13 | gulp.task("download-image", async (cb) => { 14 | await downloadImg(parseInt(process.env.FILE_USER), parseInt(process.env.FILE_GROUP)); 15 | cb(); 16 | }); 17 | 18 | gulp.task("substitute-image", async (cb) => { 19 | await subImg(); 20 | cb(); 21 | }); 22 | 23 | gulp.task("change-passwd", async (cb) => { 24 | await chpwd(); 25 | cb(); 26 | }); 27 | 28 | gulp.task("upload-algolia", async (cb) => { 29 | await uploadAlgoliaIndex(); 30 | cb(); 31 | }); 32 | -------------------------------------------------------------------------------- /app/utils/nuxt/i18n.ts: -------------------------------------------------------------------------------- 1 | import type { I18nCode } from "~/utils/common/locales"; 2 | 3 | export async function getI18nJson(code: I18nCode) { 4 | const json = await import(`../../../i18n/${code}.json?raw`); 5 | return JSON.parse(json.default) as Record; 6 | } 7 | 8 | export async function loadI18nJson(code: I18nCode) { 9 | const messages = useNuxtApp().$i18nMessages; 10 | if (!messages.value[code]) { 11 | messages.value[code] = await getI18nJson(code); 12 | } 13 | } 14 | 15 | export function translate(name: string, params?: any[], code?: I18nCode): string { 16 | code = code || useI18nCode().i18nCode.value!; 17 | const messages = useNuxtApp().$i18nMessages.value[code]; 18 | if (!messages || !messages[name]) { 19 | return name; 20 | } 21 | const regex = /\{(\d+)\}/g; 22 | return messages[name]?.replace(regex, (_, idx) => (params || [])[+idx]) || name; 23 | } 24 | -------------------------------------------------------------------------------- /app/composables/states.ts: -------------------------------------------------------------------------------- 1 | import { useState } from "#app"; 2 | 3 | import { GithubTokenKey } from "~/utils/common/constants"; 4 | 5 | // avoid loading during SSG 6 | export const useFirstLoad = () => useState("first-loaded", () => true); 7 | 8 | export const useGithubToken = () => useState(GithubTokenKey, () => ""); 9 | export const useIsAuthor = () => useState("is-author", () => null); 10 | export const useRemoteLatestSha = () => useState("remote-latest-sha", () => ""); 11 | export const useUnsavedContent = () => useState("unsaved-content", () => false); 12 | export const useShowPwdModal = () => useState("show-pwd-modal", () => false); 13 | export const useModalZIndex = () => { 14 | const zIndex = useState("modal-z-index", () => 999); 15 | const getZIndex = () => { 16 | return ++zIndex.value; 17 | }; 18 | return { 19 | _zIndex: zIndex, 20 | getZIndex 21 | }; 22 | }; 23 | -------------------------------------------------------------------------------- /app/composables/loading.ts: -------------------------------------------------------------------------------- 1 | let handle: number; 2 | 3 | export const useLoadingState = () => useState("loading", () => 0); 4 | 5 | export const useLoading = () => { 6 | const loadingState = useLoadingState(); 7 | const finish = (addClass = true) => { 8 | if (handle) { 9 | clearInterval(handle); 10 | } 11 | loadingState.value = 0; 12 | if (addClass) { 13 | setTimeout(() => { 14 | document.documentElement.classList.add("smooth-scroll"); 15 | }, 500); 16 | } 17 | }; 18 | 19 | const start = () => { 20 | document.documentElement.classList.remove("smooth-scroll"); 21 | finish(false); 22 | handle = setInterval(() => { 23 | loadingState.value += 1; 24 | if (loadingState.value === 100) { 25 | clearInterval(handle); 26 | } 27 | }, 30) as unknown as number; 28 | }; 29 | 30 | return { 31 | start, 32 | finish 33 | }; 34 | }; 35 | -------------------------------------------------------------------------------- /app/utils/hooks/useBlogList.ts: -------------------------------------------------------------------------------- 1 | import { encryptDecryptItem } from "../common/process-encrypt-decrypt"; 2 | import type { CommonItem, HeaderTabUrl } from "../common/types"; 3 | import { fetchList } from "../nuxt/fetch"; 4 | import { deepClone } from "../nuxt/utils"; 5 | 6 | export const useBlogList = async (url: HeaderTabUrl, decryptId?: string) => { 7 | const encryptor = useEncryptor(); 8 | 9 | const originList = await fetchList(url); 10 | 11 | const decryptedList = ref(deepClone(originList)) as Ref; 12 | 13 | await encryptor.decryptOrWatchToDecrypt(async (decrypt) => { 14 | for (const item of decryptedList.value) { 15 | if (item.encrypt && (!decryptId || item.customSlug === decryptId || item.id === Number(decryptId))) { 16 | await encryptDecryptItem(item, decrypt, url); 17 | } 18 | } 19 | }); 20 | 21 | return { 22 | originList, 23 | decryptedList 24 | }; 25 | }; 26 | -------------------------------------------------------------------------------- /vite-plugins/rebuild.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | import type { Plugin } from "vite"; 4 | import type { CommitParams, CommitParamsAddition, CommitParamsDeletion } from "../utils/common/types"; 5 | import { rebuildEvent } from "./types"; 6 | 7 | function writeFile(file: CommitParamsAddition): void { 8 | fs.writeFileSync(path.resolve(__dirname, "../", file.path), file.content!); 9 | } 10 | 11 | function removeFile(file: CommitParamsDeletion): void { 12 | fs.unlinkSync(path.resolve(__dirname, "../", file.path)); 13 | } 14 | 15 | export default { 16 | name: "nb-rebuild-plugin", 17 | configureServer(server) { 18 | server.ws.on(rebuildEvent, (data: CommitParams, client) => { 19 | try { 20 | data.additions?.forEach(writeFile); 21 | data.deletions?.forEach(removeFile); 22 | client.send(rebuildEvent, true); 23 | } catch (e: any) { 24 | client.send(rebuildEvent, e.toString()); 25 | } 26 | }); 27 | } 28 | } as Plugin; 29 | -------------------------------------------------------------------------------- /app/components/common-checkbox.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 34 | -------------------------------------------------------------------------------- /scripts/change-pwd.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import { processBlogItem, encrypt, encryptAndWriteMd, promptTask } from "./utils"; 3 | 4 | export default async function () { 5 | await promptTask([ 6 | { 7 | name: "oldPwd", 8 | type: "text", 9 | message: "Old password", 10 | validate: v => !!v 11 | }, { 12 | name: "newPwd", 13 | type: "text", 14 | message: "New password", 15 | validate: v => !!v 16 | } 17 | ], async function (result) { 18 | const _encrypt = (s: string) => encrypt(s, result.newPwd); 19 | 20 | await processBlogItem(result.oldPwd, async ({ decryptedItem, decryptedMd, type, mdPath }) => { 21 | await encryptAndWriteMd({ 22 | item: decryptedItem, 23 | md: decryptedMd, 24 | type, 25 | path: mdPath, 26 | encrypt: _encrypt 27 | }); 28 | }, ({ decryptedItemList, jsonPath }) => { 29 | // 写入json,json本身是不加密的 30 | fs.writeFileSync(jsonPath, JSON.stringify(decryptedItemList)); 31 | }); 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /scripts/utils/html.ts: -------------------------------------------------------------------------------- 1 | import cheerio from "cheerio"; 2 | 3 | export default function extractTextFromHtml(html: string) { 4 | const opts = { 5 | normalizeWhitespace: true, 6 | preserveNewlines: false, 7 | excludeSelectors: ["script", "style", "noscript", "iframe", "svg", ".encrypt-block"] 8 | }; 9 | 10 | // 加载HTML 11 | const $ = cheerio.load(html); 12 | 13 | // 移除不需要的元素 14 | if (opts.excludeSelectors && opts.excludeSelectors.length) { 15 | $(opts.excludeSelectors.join(", ")).remove(); 16 | } 17 | 18 | // 获取纯文本 19 | let text = $("body").text(); 20 | 21 | // 处理空白字符 22 | if (opts.normalizeWhitespace) { 23 | if (opts.preserveNewlines) { 24 | // 保留换行符,但规范化其他空白 25 | const lines = text.split("\n"); 26 | text = lines 27 | .map(line => line.replace(/\s+/g, " ").trim()) 28 | .filter(line => line) 29 | .join("\n"); 30 | } else { 31 | // 将所有空白字符(包括换行符)规范化为单个空格 32 | text = text.replace(/\s+/g, " ").trim(); 33 | } 34 | } 35 | 36 | return text; 37 | } 38 | -------------------------------------------------------------------------------- /app/assets/style/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer utilities { 6 | .form-item-invalid { 7 | @apply before:content-['*'] !text-red-400 dark:!text-red-500; 8 | } 9 | .icon-button { 10 | @apply size-10 transition flex items-center justify-center overflow-hidden cursor-pointer; 11 | @apply rounded-full text-dark-500 dark:text-dark-400 hover:bg-dark-100 dark:hover:bg-dark-700; 12 | > svg { 13 | @apply size-5; 14 | } 15 | } 16 | .title-text { 17 | @apply text-lg font-semibold text-dark-600 dark:text-dark-200; 18 | } 19 | .anim-shake { 20 | transform-origin: center center; 21 | transform-box: border-box; 22 | backface-visibility: hidden; 23 | perspective: 1000px; 24 | 25 | &:hover svg { 26 | @apply animate-shake; 27 | 28 | path { 29 | @apply text-primary-600 dark:text-primary-400; 30 | &:first-of-type{ 31 | @apply text-red-500 dark:text-red-400; 32 | } 33 | } 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 云与原 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /shim.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*?raw" { 2 | const content: string; 3 | export default content; 4 | } 5 | 6 | // Need a better way 7 | declare module "ls:*github" { 8 | export const deleteList: typeof import("./utils/nuxt/manage/github")["deleteList"]; 9 | export const createCommit: typeof import("./utils/nuxt/manage/github")["createCommit"]; 10 | export const isAuthor: typeof import("./utils/nuxt/manage/github")["isAuthor"]; 11 | export const commitStagedItems: typeof import("./utils/nuxt/manage/github")["commitStagedItems"]; 12 | } 13 | 14 | declare const __NB_DATABASE_ENABLED__: string; 15 | declare const __NB_CMTREPOID__: string; 16 | declare const __NB_CMTREPOCATEID__: string; 17 | declare const __NB_ALGOLIA_APP_ID__: string; 18 | declare const __NB_ALGOLIA_SEARCH_KEY__: string; 19 | declare const __NB_ALGOLIA_INDEX_NAME__: string; 20 | declare const __NB_ALGOLIA_ENABLED__: boolean; 21 | declare const __NB_BUILD_TIME__: string; 22 | declare const __NB_GITHUB_REPO__: string; 23 | declare const __NB_CURRENT_GIT_SHA__: string; 24 | declare const __NB_BUILDTIME_VITESTING__: boolean; 25 | declare const __NB_CURRENT_VERSION__: string; 26 | -------------------------------------------------------------------------------- /app/assets/style/lxgwwenkai.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: lxgwwenkai; 3 | font-weight: 300; 4 | font-style: normal; 5 | font-stretch: normal; 6 | src: 7 | url("https://cdn.jsdelivr.net/npm/lxgwwenkai@1.0.1/woff2/LXGWWenKai-Light.woff2") format("woff2"), 8 | url("https://cdn.jsdelivr.net/npm/lxgwwenkai@1.0.1/ttf/LXGWWenKai-Light.ttf") format("truetype"); 9 | font-display: swap; 10 | } 11 | 12 | @font-face { 13 | font-family: lxgwwenkai; 14 | font-weight: 400; 15 | font-style: normal; 16 | font-stretch: normal; 17 | src: 18 | url("https://cdn.jsdelivr.net/npm/lxgwwenkai@1.0.1/woff2/LXGWWenKai-Regular.woff2") format("woff2"), 19 | url("https://cdn.jsdelivr.net/npm/lxgwwenkai@1.0.1/ttf/LXGWWenKai-Regular.ttf") format("truetype"); 20 | font-display: swap; 21 | } 22 | 23 | @font-face { 24 | font-family: lxgwwenkai; 25 | font-weight: 700; 26 | font-style: normal; 27 | font-stretch: normal; 28 | src: 29 | url("https://cdn.jsdelivr.net/npm/lxgwwenkai@1.0.1/woff2/LXGWWenKai-Bold.woff2") format("woff2"), 30 | url("https://cdn.jsdelivr.net/npm/lxgwwenkai@1.0.1/ttf/LXGWWenKai-Bold.ttf") format("truetype"); 31 | font-display: swap; 32 | } 33 | -------------------------------------------------------------------------------- /app/plugins/all.client.ts: -------------------------------------------------------------------------------- 1 | import { NotificationContainerId, ModalContainerId, ThemeModeKey, I18nStoreKey } from "~/utils/common/constants"; 2 | import { initScrollTrigger } from "~/utils/common/scroll-event"; 3 | import { getLocalStorage } from "~/utils/nuxt/localStorage"; 4 | 5 | export default defineNuxtPlugin((app) => { 6 | initScrollTrigger(); 7 | const { _zIndex } = useModalZIndex(); 8 | 9 | app.hook("app:suspense:resolve", () => { 10 | useThemeMode().themeMode.value = getLocalStorage(ThemeModeKey) || "light"; 11 | useI18nCode().changeI18n(getLocalStorage(I18nStoreKey) || useI18nCode().i18nCode.value); 12 | }); 13 | 14 | const fragment = new DocumentFragment(); 15 | 16 | const notifyContainer = document.createElement("div"); 17 | notifyContainer.id = NotificationContainerId; 18 | fragment.appendChild(notifyContainer); 19 | 20 | const modalContainer = document.createElement("div"); 21 | modalContainer.id = ModalContainerId; 22 | fragment.appendChild(modalContainer); 23 | 24 | document.body.appendChild(fragment); 25 | 26 | watch(_zIndex, (zIndex) => { 27 | notifyContainer.style.setProperty("z-index", (zIndex + 1).toString()); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import pluginVue from "eslint-plugin-vue"; 2 | import tailwind from "eslint-plugin-tailwindcss"; 3 | import eslint from "@eslint/js"; 4 | import tseslint from "typescript-eslint"; 5 | import withNuxt from "./.nuxt/eslint.config.mjs"; 6 | 7 | export default withNuxt( 8 | eslint.configs.recommended, 9 | tseslint.configs.recommended, 10 | ...pluginVue.configs["flat/recommended"], 11 | ...tailwind.configs["flat/recommended"], 12 | { 13 | files: ["**/*.ts", "**/*.tsx", "**/*.vue"], 14 | rules: { 15 | "quotes": ["error", "double"], 16 | "semi": ["error", "always"], 17 | "prefer-promise-reject-errors": "off", 18 | "no-multi-spaces": ["error"], 19 | "no-undef": "off", 20 | "no-unused-vars": "off", 21 | "@typescript-eslint/no-unused-vars": ["error"], 22 | "@typescript-eslint/no-var-requires": "off", 23 | "@typescript-eslint/no-explicit-any": "off", 24 | "@typescript-eslint/no-non-null-assertion": "off", 25 | "typescript-config/strict": "off", 26 | "vue/multi-word-component-names": "off", 27 | "vue/no-multiple-template-root": "off", 28 | "vue/no-v-model-argument": "off", 29 | "vue/no-v-html": "off" 30 | } 31 | } 32 | ); 33 | -------------------------------------------------------------------------------- /app/assets/style/main.css: -------------------------------------------------------------------------------- 1 | /* @import "./fira-code.css"; */ 2 | @import "./markdown.css"; 3 | @import "./generated-theme-colors.css"; 4 | 5 | html { 6 | @apply font-sans; 7 | &.smooth-scroll { 8 | @apply scroll-smooth; 9 | } 10 | } 11 | 12 | html, 13 | body { 14 | @apply min-w-full; 15 | } 16 | 17 | body { 18 | @apply bg-nb-light dark:bg-nb-dark text-dark-900 dark:text-dark-300; 19 | transition: background 0s linear 0.5s; 20 | 21 | &.resizing { 22 | @apply select-none cursor-ew-resize; 23 | } 24 | } 25 | 26 | input, 27 | textarea, 28 | button { 29 | @apply outline-none; 30 | } 31 | 32 | input, textarea { 33 | @apply rounded-lg border border-dark-300 bg-dark-50 py-2 px-3 placeholder:text-dark-400 dark:border-dark-600 dark:bg-dark-700 dark:text-white max-md:text-sm min-w-5 transition-[border]; 34 | @apply focus:border-primary-400 dark:focus:border-primary-600; 35 | @apply hover:border-primary-500 dark:hover:border-primary-500; 36 | } 37 | 38 | img[data-viewer] { 39 | @apply cursor-zoom-in; 40 | } 41 | 42 | ::-webkit-scrollbar { 43 | @apply size-2.5 bg-transparent; 44 | } 45 | 46 | ::-webkit-scrollbar-thumb { 47 | @apply rounded bg-dark-300 hover:bg-dark-400 dark:bg-dark-500 dark:hover:bg-dark-400; 48 | } 49 | -------------------------------------------------------------------------------- /app/pages/manage/articles/index.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 38 | -------------------------------------------------------------------------------- /app/utils/nuxt/public/list.ts: -------------------------------------------------------------------------------- 1 | import { type CommonItem } from "~/utils/common/types"; 2 | import { useBlogList } from "~/utils/hooks/useBlogList"; 3 | import { DBOperate } from "."; 4 | import { translate } from "../i18n"; 5 | import { getCurrentTab, useCommonSEOTitle } from "../utils"; 6 | 7 | /** 8 | * 列表页面通用功能 9 | */ 10 | export async function useListPage () { 11 | const githubToken = useGithubToken(); 12 | const encryptor = useEncryptor(); 13 | const targetTab = getCurrentTab(); 14 | 15 | useCommonSEOTitle(computed(() => translate(targetTab))); 16 | 17 | const { decryptedList } = await useBlogList(targetTab); 18 | 19 | DBOperate({ 20 | apiPath: "/db/get-visitors", 21 | query: { type: targetTab }, 22 | callback: (data) => { 23 | decryptedList.value.forEach((item) => { 24 | item._visitors = data.find(i => i[0] === item.id)?.[2] || 0; 25 | }); 26 | } 27 | }); 28 | 29 | // 有token或者密码正确,显示加密的item 30 | watch([githubToken, encryptor.passwdCorrect], ([hasToken, hasPwd]) => { 31 | decryptedList.value.forEach((item) => { 32 | item._show = !item.encrypt || (!!hasToken || hasPwd); 33 | }); 34 | }, { immediate: true }); 35 | 36 | return decryptedList.value; 37 | } 38 | -------------------------------------------------------------------------------- /e2e/manage/test-helpers.ts: -------------------------------------------------------------------------------- 1 | import { createPage, setup } from "@nuxt/test-utils/e2e"; 2 | import { ManageItemPage } from "../page-objects/manage/ItemPage"; 3 | import { ManageListPage } from "../page-objects/manage/ListPage"; 4 | import { ManageConfigPage } from "../page-objects/manage/ConfigPage"; 5 | 6 | const baseURL = "http://localhost:13000"; 7 | 8 | export const setupTestEnvironment = async () => { 9 | await setup({ 10 | host: baseURL 11 | }); 12 | }; 13 | 14 | const createPageWithBaseURL = async (path: string) => { 15 | const page = await createPage(path, { 16 | baseURL 17 | }); 18 | page.evaluate(() => { 19 | localStorage.clear(); 20 | }); 21 | return page; 22 | }; 23 | 24 | export const createItemPage = async (path: string) => { 25 | const page = await createPageWithBaseURL(path); 26 | const itemPage = new ManageItemPage(page); 27 | return { page, itemPage }; 28 | }; 29 | 30 | export const createListPage = async (path: string) => { 31 | const page = await createPageWithBaseURL(path); 32 | const listPage = new ManageListPage(page); 33 | return { page, listPage }; 34 | }; 35 | 36 | export const createConfigPage = async (path: string) => { 37 | const page = await createPageWithBaseURL(path); 38 | const configPage = new ManageConfigPage(page); 39 | return { page, configPage }; 40 | }; 41 | -------------------------------------------------------------------------------- /app/utils/hooks/useMarkdownParser.ts: -------------------------------------------------------------------------------- 1 | import { parseMarkdown } from "../common/markdown"; 2 | import { afterInsertHtml } from "../nuxt/markdown"; 3 | 4 | type useMarkdownParserProps = { 5 | mdValueRef: Ref; 6 | fromEdit?: boolean; 7 | onAfterInsertHtml?: CallableFunction; 8 | destroyFns?: CallableFunction[]; 9 | }; 10 | 11 | export const useMarkdownParser = async ({ mdValueRef, fromEdit, onAfterInsertHtml, destroyFns }: useMarkdownParserProps) => { 12 | const markdownRef = ref(); 13 | 14 | const htmlContent = ref(""); 15 | const menuItems = ref>["menu"]>([]); 16 | 17 | const parse = async (md: string) => { 18 | const result = await parseMarkdown(md); 19 | htmlContent.value = result.md; 20 | menuItems.value = result.menu; 21 | }; 22 | 23 | watch(mdValueRef, async (md) => { 24 | parse(md); 25 | }); 26 | 27 | await parse(mdValueRef.value); 28 | 29 | watch(htmlContent, () => { 30 | setTimeout(async () => { 31 | if (markdownRef.value) { 32 | const fns = await afterInsertHtml(markdownRef.value, fromEdit); 33 | onAfterInsertHtml?.(); 34 | destroyFns?.splice(0, destroyFns.length, ...fns); 35 | } 36 | }); 37 | }, { immediate: true }); 38 | 39 | return { 40 | markdownRef, 41 | htmlContent, 42 | menuItems 43 | }; 44 | }; 45 | -------------------------------------------------------------------------------- /app/pages/manage/records/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 33 | 34 | 51 | -------------------------------------------------------------------------------- /app/pages/manage/knowledges/index.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 41 | -------------------------------------------------------------------------------- /app/components/the-tag.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 38 | 39 | 53 | -------------------------------------------------------------------------------- /app/utils/common/utils.ts: -------------------------------------------------------------------------------- 1 | import type { CommonItem, HeaderTabUrl } from "./types"; 2 | 3 | /** 4 | * 生成唯一id 5 | */ 6 | let uniqueId = 0; 7 | export function getUniqueId(): typeof uniqueId { 8 | return uniqueId++; 9 | } 10 | 11 | /** 12 | * 创建一个新item 13 | */ 14 | export function createNewItem(url: HeaderTabUrl): CommonItem { 15 | const baseInfo = { 16 | id: 0, 17 | customSlug: "", 18 | time: 0, 19 | modifyTime: 0, 20 | showComments: false, 21 | encrypt: false, 22 | _show: true, 23 | _visitors: 0 24 | }; 25 | 26 | if (url === "/articles") { 27 | return { 28 | title: "", 29 | len: 0, 30 | tags: [], 31 | ...baseInfo 32 | }; 33 | } else if (url === "/records") { 34 | return { 35 | images: [], 36 | ...baseInfo 37 | }; 38 | } else { 39 | return { 40 | title: "", 41 | summary: "", 42 | link: "", 43 | cover: "", 44 | type: "book", 45 | ...baseInfo 46 | }; 47 | } 48 | } 49 | 50 | export function escapeHtml(s: string, inUrl = false) { 51 | return s.toString() 52 | .replace(/&/g, inUrl ? "-" : "&") 53 | .replace(//g, inUrl ? "-" : ">") 55 | .replace(/"/g, inUrl ? "-" : """) 56 | .replace(/'/g, inUrl ? "-" : "'"); 57 | } 58 | 59 | // 用在两个地方:提交时,获取时 60 | export function escapeNewLine(s: string) { 61 | return s.replace(/\r\n/g, "\n"); 62 | } 63 | -------------------------------------------------------------------------------- /app/utils/nuxt/viewer.ts: -------------------------------------------------------------------------------- 1 | import Viewer from "viewerjs"; 2 | import type { Ref } from "vue"; 3 | import { ViewerAttr } from "~/utils/common/constants"; 4 | 5 | function useMutationObserver( 6 | target: HTMLElement, 7 | callback: MutationCallback 8 | ) { 9 | let observer: MutationObserver | undefined; 10 | 11 | const cleanup = () => { 12 | if (observer) { 13 | observer.disconnect(); 14 | observer = undefined; 15 | } 16 | }; 17 | if (window && "MutationObserver" in window && target) { 18 | observer = new MutationObserver(callback); 19 | observer.observe(target, { attributes: true, childList: true, characterData: true, subtree: true }); 20 | } 21 | 22 | return cleanup; 23 | } 24 | 25 | /** 26 | * viewerjs 27 | */ 28 | export function initViewer(el: Ref): Ref | undefined { 29 | if (!import.meta.client) { 30 | return; 31 | } 32 | let viewerContainer; 33 | let viewer: Viewer; 34 | let stop: () => void = () => undefined; 35 | 36 | onMounted(() => { 37 | viewer = new Viewer(el.value!, { 38 | filter(image: HTMLImageElement) { 39 | return image.hasAttribute(ViewerAttr); 40 | } 41 | }); 42 | stop = useMutationObserver(el.value!, () => { 43 | nextTick(() => { 44 | viewer?.update(); 45 | }); 46 | }); 47 | }); 48 | 49 | onBeforeUnmount(() => { 50 | stop(); 51 | viewer?.destroy(); 52 | }); 53 | return viewerContainer; 54 | } 55 | -------------------------------------------------------------------------------- /app/utils/nuxt/format-time.ts: -------------------------------------------------------------------------------- 1 | import { getLocaleByCode } from "../common/locales"; 2 | import { translate } from "~/utils/nuxt/i18n"; 3 | import { getNowDayjs } from "~/utils/common/dayjs"; 4 | 5 | export function formatTime(stamp?: number, type: "full" | "date" | "month" = "full") { 6 | return computed(() => { 7 | const { formatFull, formatDate, formatMonth } = getLocaleByCode(useI18nCode().i18nCode.value)!; 8 | let format = ""; 9 | switch (type) { 10 | case "full": 11 | format = formatFull; 12 | break; 13 | case "date": 14 | format = formatDate; 15 | break; 16 | case "month": 17 | format = formatMonth; 18 | break; 19 | } 20 | return getNowDayjs(stamp).format(format); 21 | }).value; 22 | } 23 | 24 | export function literalTime(stamp: number) { 25 | const dayOld = getNowDayjs(stamp); 26 | const dayNew = getNowDayjs(); 27 | const subDay = dayNew.diff(dayOld, "day"); 28 | const subWeek = dayNew.diff(dayOld, "week"); 29 | const subMonth = dayNew.diff(dayOld, "month"); 30 | const subYear = dayNew.diff(dayOld, "year"); 31 | if (dayOld.isToday()) { 32 | return translate("today"); 33 | } 34 | if (subWeek < 1) { 35 | return translate("days-ago", [Math.max(1, subDay)]); 36 | } 37 | if (subMonth < 1) { 38 | return translate("weeks-ago", [Math.max(1, subWeek)]); 39 | } 40 | if (subYear < 1) { 41 | return translate("months-ago", [Math.max(1, subMonth)]); 42 | } 43 | return translate("years-ago", [Math.max(1, subYear)]); 44 | } 45 | -------------------------------------------------------------------------------- /app/components/common-button.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 46 | -------------------------------------------------------------------------------- /app/utils/common/hljs.ts: -------------------------------------------------------------------------------- 1 | import type highlight from "highlight.js"; 2 | 3 | export function initHljs(hljs: typeof highlight) { 4 | if (!hljs.listLanguages().includes("vue")) { 5 | hljs.registerLanguage("vue", hljs => ({ 6 | subLanguage: "xml", 7 | contains: [ 8 | hljs.COMMENT("", { 9 | relevance: 10 10 | }), 11 | { 12 | begin: /^(\s*)( 39 | 40 | 59 | 60 | 83 | -------------------------------------------------------------------------------- /tailwind.config.mjs: -------------------------------------------------------------------------------- 1 | import colors from "tailwindcss/colors"; 2 | import config from "./config"; 3 | 4 | /** @type {import('tailwindcss').Config} */ 5 | export default { 6 | content: [ 7 | "./app/utils/**/*.{tsx,vue}", 8 | "./app/pages/**/*.vue", 9 | "./app/components/**/*.vue", 10 | "./app/layouts/**/*.vue", 11 | "./app/app.vue" 12 | ], 13 | darkMode: "class", 14 | theme: { 15 | extend: { 16 | colors: { 17 | primary: { 18 | 50: "var(--color-primary-50)", 19 | 100: "var(--color-primary-100)", 20 | 200: "var(--color-primary-200)", 21 | 300: "var(--color-primary-300)", 22 | 400: "var(--color-primary-400)", 23 | 500: "var(--color-primary-500)", 24 | 600: "var(--color-primary-600)", 25 | 700: "var(--color-primary-700)", 26 | 800: "var(--color-primary-800)", 27 | 900: "var(--color-primary-900)", 28 | 950: "var(--color-primary-950)" 29 | }, 30 | dark: colors[config.themeColorDark] 31 | }, 32 | backgroundColor: { 33 | nb: { 34 | light: "#f9fafb", 35 | dark: "#151515" 36 | } 37 | }, 38 | spacing: { 39 | header: "60px" 40 | }, 41 | zIndex: { 42 | modeBg: "10", 43 | body: "20", 44 | footer: "30", 45 | header: "500", 46 | headerLoading: "501", 47 | modal: "999", 48 | dropdown: "999", 49 | mermaidFullscreen: "999" 50 | }, 51 | animation: { 52 | shake: "shake 0.82s cubic-bezier(.36,.07,.19,.97) infinite" 53 | }, 54 | boxShadow: { 55 | card: "0 12px 30px -27px rgba(15,23,42,0.45)" 56 | }, 57 | keyframes: { 58 | shake: { 59 | "10%, 90%": { 60 | transform: "rotate(3deg)" 61 | }, 62 | "20%, 80%": { 63 | transform: "rotate(0deg)" 64 | }, 65 | "30%, 50%, 70%": { 66 | transform: "rotate(3deg)" 67 | }, 68 | "40%, 60%": { 69 | transform: "rotate(0deg)" 70 | } 71 | } 72 | } 73 | } 74 | }, 75 | plugins: [] 76 | }; 77 | -------------------------------------------------------------------------------- /app/assets/style/fira-code.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "Fira Code"; 3 | src: 4 | url("https://cdn.jsdelivr.net/npm/firacode@6.2.0/distr/woff2/FiraCode-Light.woff2") format("woff2"), 5 | url("https://cdn.jsdelivr.net/npm/firacode@6.2.0/distr/woff/FiraCode-Light.woff") format("woff"); 6 | font-weight: 300; 7 | font-style: normal; 8 | font-display: swap; 9 | } 10 | 11 | @font-face { 12 | font-family: "Fira Code"; 13 | src: 14 | url("https://cdn.jsdelivr.net/npm/firacode@6.2.0/distr/woff2/FiraCode-Regular.woff2") format("woff2"), 15 | url("https://cdn.jsdelivr.net/npm/firacode@6.2.0/distr/woff/FiraCode-Regular.woff") format("woff"); 16 | font-weight: 400; 17 | font-style: normal; 18 | font-display: swap; 19 | } 20 | 21 | @font-face { 22 | font-family: "Fira Code"; 23 | src: 24 | url("https://cdn.jsdelivr.net/npm/firacode@6.2.0/distr/woff2/FiraCode-Medium.woff2") format("woff2"), 25 | url("https://cdn.jsdelivr.net/npm/firacode@6.2.0/distr/woff/FiraCode-Medium.woff") format("woff"); 26 | font-weight: 500; 27 | font-style: normal; 28 | font-display: swap; 29 | } 30 | 31 | @font-face { 32 | font-family: "Fira Code"; 33 | src: 34 | url("https://cdn.jsdelivr.net/npm/firacode@6.2.0/distr/woff2/FiraCode-SemiBold.woff2") format("woff2"), 35 | url("https://cdn.jsdelivr.net/npm/firacode@6.2.0/distr/woff/FiraCode-SemiBold.woff") format("woff"); 36 | font-weight: 600; 37 | font-style: normal; 38 | font-display: swap; 39 | } 40 | 41 | @font-face { 42 | font-family: "Fira Code"; 43 | src: 44 | url("https://cdn.jsdelivr.net/npm/firacode@6.2.0/distr/woff2/FiraCode-Bold.woff2") format("woff2"), 45 | url("https://cdn.jsdelivr.net/npm/firacode@6.2.0/distr/woff/FiraCode-Bold.woff") format("woff"); 46 | font-weight: 700; 47 | font-style: normal; 48 | font-display: swap; 49 | } 50 | 51 | @font-face { 52 | font-family: "Fira Code VF"; 53 | src: 54 | url("https://cdn.jsdelivr.net/npm/firacode@6.2.0/distr/woff2/FiraCode-VF.woff2") format("woff2-variations"), 55 | url("https://cdn.jsdelivr.net/npm/firacode@6.2.0/distr/woff/FiraCode-VF.woff") format("woff-variations"); 56 | font-weight: 300 700; 57 | font-style: normal; 58 | font-display: swap; 59 | } 60 | -------------------------------------------------------------------------------- /app/pages/about/index.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 67 | -------------------------------------------------------------------------------- /app/utils/nuxt/manage/__github.ts: -------------------------------------------------------------------------------- 1 | import { translate } from "../i18n"; 2 | import { getCurrentTab, devHotListen } from "../utils"; 3 | import { createDiffModal } from "."; 4 | import type { CommitParams, CommitParamsAddition, CommonItem } from "~/utils/common/types"; 5 | import { notify } from "~/utils/nuxt/notify"; 6 | import { rebuildEvent } from "~~/vite-plugins/types"; 7 | 8 | export function isAuthor(): never { 9 | throw new Error("Can't do that"); 10 | } 11 | 12 | export async function createCommit( 13 | _commit = "", 14 | { additions, deletions }: CommitParams 15 | ): Promise { 16 | // 显示 diff 弹窗让用户确认更改 17 | if (!(await createDiffModal({ additions, deletions }))) { 18 | return false; 19 | } 20 | import.meta.hot!.send(rebuildEvent, { 21 | additions, 22 | deletions 23 | }); 24 | return listenServer(); 25 | } 26 | 27 | export async function deleteList( 28 | newList: CommonItem[], 29 | dels: { item: CommonItem; md: string }[] 30 | ): Promise { 31 | const folder = getCurrentTab(); 32 | return createCommit( 33 | `Delete ${dels.length} items from ${folder}`, 34 | { 35 | additions: [{ 36 | path: `public/rebuild/json${folder}.json`, 37 | content: JSON.stringify(newList) 38 | }], 39 | deletions: dels.map(item => ({ 40 | path: `public/rebuild${folder}/${item.item.id}.md`, 41 | content: item.md 42 | })) 43 | } 44 | ); 45 | } 46 | 47 | export async function commitStagedItems( 48 | additions: CommitParamsAddition[] 49 | ): Promise { 50 | if (additions.length === 0) { 51 | return true; 52 | } 53 | 54 | const commitMessage = `Batch update ${additions.length} items`; 55 | return createCommit(commitMessage, { additions }); 56 | } 57 | 58 | function listenServer(): Promise { 59 | return new Promise((resolve, reject) => { 60 | devHotListen(rebuildEvent, (data) => { 61 | if (typeof data === "boolean") { 62 | resolve(data); 63 | if (data) { 64 | notify({ 65 | title: translate("update-success"), 66 | description: translate("refresh-after-sec", [1]) 67 | }); 68 | setTimeout(() => { 69 | location.reload(); 70 | }, 1000); 71 | } 72 | } else { 73 | reject(data); 74 | } 75 | }); 76 | }); 77 | } 78 | -------------------------------------------------------------------------------- /app/pages/records/[id].vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 59 | 60 | 69 | -------------------------------------------------------------------------------- /e2e/manage/item-delete.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { createItemPage, setupTestEnvironment } from "./test-helpers"; 3 | 4 | describe("Item Deletion", async () => { 5 | await setupTestEnvironment(); 6 | 7 | it("Deleting works", async () => { 8 | const { itemPage } = await createItemPage("/manage/articles/1111"); 9 | 10 | const deleteBtn = await itemPage.getByTestId("item-delete-btn"); 11 | expect(await deleteBtn.isDisabled()).toBe(false); 12 | 13 | await itemPage.deleteItem(); 14 | 15 | await itemPage.verifyItemListInResponse({ 16 | shouldNotFindItem: i => i.id === 1111, 17 | encryptBlocksItemsCount: 1, 18 | shouldContainEncryptedTitle: true 19 | }); 20 | 21 | await itemPage.verifyDeletionPathInResponse("1111.md"); 22 | }); 23 | 24 | it("Deleting works after entering password", async () => { 25 | const { itemPage } = await createItemPage("/manage/articles/1111"); 26 | 27 | const deleteBtn = await itemPage.getByTestId("item-delete-btn"); 28 | expect(await deleteBtn.isDisabled()).toBe(false); 29 | 30 | await itemPage.enterPassword(); 31 | 32 | await itemPage.deleteItem(); 33 | 34 | await itemPage.verifyItemListInResponse({ 35 | shouldNotFindItem: i => i.id === 1111, 36 | encryptBlocksItemsCount: 1, 37 | shouldContainEncryptedTitle: true 38 | }); 39 | 40 | await itemPage.verifyDeletionPathInResponse("1111.md"); 41 | }); 42 | 43 | it("Deleting item with encryptedBlock works after entering password", async () => { 44 | const { itemPage } = await createItemPage("/manage/articles/2222"); 45 | 46 | const deleteBtn = await itemPage.getByTestId("item-delete-btn"); 47 | expect(await deleteBtn.isDisabled()).toBe(false); 48 | 49 | await itemPage.waitForTimeout(1500); 50 | const renderedMarkdown = await itemPage.getByTestId("rendered-markdown"); 51 | expect((await renderedMarkdown.innerText()).match(/test/g)).lengthOf(2); 52 | 53 | await itemPage.enterPassword(); 54 | expect((await renderedMarkdown.innerText()).match(/test/g)).lengthOf(4); 55 | 56 | await itemPage.deleteItem(); 57 | 58 | await itemPage.verifyItemListInResponse({ 59 | shouldNotFindItem: i => i.id === 2222, 60 | shouldContainEncryptedTitle: true 61 | }); 62 | 63 | await itemPage.verifyDeletionPathInResponse("2222.md"); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /scripts/utils/index.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import cmd from "child_process"; 3 | import path from "path"; 4 | import colors from "colors"; 5 | import type { PromptObject } from "prompts"; 6 | import prompts from "prompts"; 7 | import type { CommonItemByHeaderType, HeaderTabUrl } from "../../app/utils/common/types"; 8 | import { escapeNewLine } from "../../app/utils/common/utils"; 9 | 10 | export async function promptTask(params: T[], cb: (_: Record) => any | Promise) { 11 | let canceled = false; 12 | const response = await prompts(params, { 13 | onCancel: () => { 14 | console.log(colors.red("Program canceled")); 15 | canceled = true; 16 | } 17 | }) as any; 18 | 19 | if (!canceled) { 20 | await cb(response); 21 | } 22 | } 23 | 24 | export function getAbsolutePath(...s: string[]) { 25 | return path.join(__dirname, "..", "..", ...s); 26 | } 27 | 28 | export function getRebuildPath(...s: string[]) { 29 | return getAbsolutePath("public", "rebuild", ...s); 30 | } 31 | 32 | export function walkAllBlogData() { 33 | const walk = (type: T) => { 34 | const jsonPath = getRebuildPath("json", type + ".json"); 35 | const itemList: (CommonItemByHeaderType & { _md: string; _mdPath: string })[] = JSON.parse(fs.readFileSync(jsonPath).toString()); 36 | for (const item of itemList) { 37 | const mdPath = getRebuildPath(type, String(item.id) + ".md"); 38 | item._mdPath = mdPath; 39 | item._md = escapeNewLine(fs.readFileSync(mdPath).toString()); 40 | } 41 | return { type, jsonPath, list: itemList }; 42 | }; 43 | 44 | return [ 45 | walk("/articles"), 46 | walk("/records"), 47 | walk("/knowledges") 48 | ]; 49 | } 50 | 51 | export async function runCmd(command: string) { 52 | return await new Promise((resolve, reject) => { 53 | cmd.exec(command, { 54 | maxBuffer: 1024 * 1024 * 5 55 | }, (err) => { 56 | if (err) { 57 | reject(err); 58 | } else { 59 | resolve(); 60 | } 61 | }).stdout?.pipe(process.stdout); 62 | }); 63 | } 64 | 65 | export function nbLog(s: string, head = "nuxt hook") { 66 | console.log(`[${colors.blue.bold(head)}] ${colors.green(s)}`); 67 | } 68 | 69 | export type ImgMap = Record; 73 | 74 | export * from "./encrypt"; 75 | export * from "./rss"; 76 | -------------------------------------------------------------------------------- /app/plugins/github-token.client.ts: -------------------------------------------------------------------------------- 1 | import { GithubTokenKey, IgnoredVersionKey, OfficialRepo } from "~/utils/common/constants"; 2 | import { isDev } from "~/utils/nuxt/constants"; 3 | import { translate } from "~/utils/nuxt/i18n"; 4 | import { getLocalStorage, setLocalStorage } from "~/utils/nuxt/localStorage"; 5 | import { isAuthor } from "~/utils/nuxt/manage/github"; 6 | import { createVersionUpdateModal } from "~/utils/nuxt/manage"; 7 | import { notify } from "~/utils/nuxt/notify"; 8 | 9 | async function checkVersion() { 10 | try { 11 | const response = await fetch(`https://raw.githubusercontent.com/${OfficialRepo}/refs/heads/master/CHANGELOG.md`); 12 | const content = await response.text(); 13 | const versionMatch = content.match(/##\s*\[v(\d+)\]/); 14 | const latestVersion = versionMatch ? versionMatch[1] : null; 15 | 16 | if (latestVersion && latestVersion !== __NB_CURRENT_VERSION__) { 17 | const ignoredVersion = getLocalStorage(IgnoredVersionKey); 18 | if (ignoredVersion !== latestVersion) { 19 | await createVersionUpdateModal(latestVersion); 20 | // 不会弹出第二次 21 | setLocalStorage(IgnoredVersionKey, latestVersion); 22 | } 23 | } 24 | } catch (e) { 25 | // 检查版本失败,静默处理 26 | console.error("Failed to check version:", e); 27 | } 28 | } 29 | 30 | export default defineNuxtPlugin((app) => { 31 | if (isDev || __NB_BUILDTIME_VITESTING__) { 32 | app.hook("app:suspense:resolve", () => { 33 | useGithubToken().value = "LocalServer"; 34 | useRemoteLatestSha().value = __NB_CURRENT_GIT_SHA__; 35 | useIsAuthor().value = true; 36 | }); 37 | } else { 38 | const localToken = getLocalStorage(GithubTokenKey); 39 | if (localToken) { 40 | // 进入界面时,检查token 41 | isAuthor(localToken) 42 | .then((res) => { 43 | notify({ 44 | title: res ? translate("token-verified") : translate("token-unverified"), 45 | type: res ? "success" : "error" 46 | }); 47 | useIsAuthor().value = res; 48 | // 验证成功后检查版本 49 | if (res) { 50 | checkVersion(); 51 | } 52 | }) 53 | .catch((e) => { 54 | notify({ 55 | title: translate("token-unverified"), 56 | type: "error", 57 | description: e 58 | }); 59 | useIsAuthor().value = false; 60 | }); 61 | } else { 62 | useIsAuthor().value = false; 63 | } 64 | } 65 | }); 66 | -------------------------------------------------------------------------------- /e2e/manage/item-create.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { createItemPage, setupTestEnvironment } from "./test-helpers"; 3 | 4 | describe("Item Creation", async () => { 5 | await setupTestEnvironment(); 6 | 7 | it("Creating works", async () => { 8 | const { itemPage } = await createItemPage("/manage/articles/0"); 9 | 10 | const uploadBtn = await itemPage.getByTestId("item-upload-btn"); 11 | expect(await uploadBtn.isDisabled()).toBe(true); 12 | 13 | await itemPage.fillItemDetails("new title", "newtag", "new content"); 14 | 15 | await itemPage.uploadItem(); 16 | 17 | await itemPage.verifyItemListInResponse({ 18 | expectedLength: 4, 19 | shouldFindItem: { encrypt: false, title: "new title", tags: ["newtag"] }, 20 | encryptBlocksItemsCount: 1, 21 | shouldContainEncryptedTitle: true 22 | }); 23 | 24 | await itemPage.verifyItemContentInResponse("new content"); 25 | }); 26 | 27 | it("Creating item with encryptedBlock works", async () => { 28 | const { itemPage } = await createItemPage("/manage/articles/0"); 29 | 30 | const uploadBtn = await itemPage.getByTestId("item-upload-btn"); 31 | expect(await uploadBtn.isDisabled()).toBe(true); 32 | 33 | await itemPage.fillItemDetails("new title", "newtag", "[encrypt]\nnew content\n[/encrypt]"); 34 | 35 | await itemPage.enterPassword(); 36 | 37 | await itemPage.uploadItem(); 38 | 39 | await itemPage.verifyItemListInResponse({ 40 | expectedLength: 4, 41 | shouldFindItem: { encrypt: false, title: "new title", tags: ["newtag"] }, 42 | encryptBlocksItemsCount: 2, 43 | shouldContainEncryptedTitle: true 44 | }); 45 | 46 | await itemPage.verifyItemContentInResponse(undefined, "new content"); 47 | }); 48 | 49 | it("Creating encrypted item works", async () => { 50 | const { itemPage } = await createItemPage("/manage/articles/0"); 51 | 52 | const uploadBtn = await itemPage.getByTestId("item-upload-btn"); 53 | expect(await uploadBtn.isDisabled()).toBe(true); 54 | 55 | await itemPage.toggleEncrypted(); 56 | await itemPage.fillItemDetails("new title", undefined, "new content"); 57 | 58 | await itemPage.enterPassword(); 59 | 60 | await itemPage.uploadItem(); 61 | 62 | await itemPage.verifyItemListInResponse({ 63 | expectedLength: 4, 64 | shouldFindItem: { encrypt: true, tags: [] }, 65 | encryptBlocksItemsCount: 1, 66 | shouldContainEncryptedTitle: true 67 | }); 68 | 69 | await itemPage.verifyItemContentInResponse(undefined, "new content"); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /app/utils/common/types.ts: -------------------------------------------------------------------------------- 1 | import { BookOpen, Film, Gamepad2 } from "lucide-vue-next"; 2 | import type { FunctionalComponent } from "vue"; 3 | 4 | export type EncryptBlock = { 5 | start: number; 6 | end: number; 7 | }; 8 | 9 | export type ItemBase = T & { 10 | id: number; 11 | customSlug: string; 12 | time: number; 13 | modifyTime: number; 14 | encrypt: boolean; 15 | encryptBlocks?: EncryptBlock[]; 16 | showComments: boolean; 17 | _visitors?: number; 18 | _show?: boolean; 19 | }; 20 | 21 | export type ArticleItem = ItemBase<{ 22 | title: string; 23 | len: number; 24 | tags: string[]; 25 | }>; 26 | 27 | export type RecordItem = ItemBase<{ 28 | images: { src: string; alt: string; id?: number }[]; 29 | }>; 30 | 31 | export const KnowledgeTabsList = [ 32 | { key: "book", name: "book" }, 33 | { key: "film", name: "film" }, 34 | { key: "game", name: "game" } 35 | ] as const; 36 | export const KnowledgeTabs = KnowledgeTabsList.map(item => item.key); 37 | export type KnowledgeTab = typeof KnowledgeTabs[number]; 38 | 39 | export type KnowledgeItem = ItemBase<{ 40 | title: string; 41 | type: KnowledgeTab; 42 | link: string; 43 | cover: string; 44 | summary: string; 45 | }>; 46 | 47 | export const KnowledgeIconMap = { 48 | game: Gamepad2, 49 | film: Film, 50 | book: BookOpen 51 | } as Record; 52 | 53 | export const KnowledgeColorMap = { 54 | game: "bg-blue-100 dark:bg-blue-900/40 text-blue-800 dark:text-blue-300", 55 | film: "bg-rose-100 text-rose-500 dark:bg-rose-900/30 dark:text-rose-400", 56 | book: "bg-purple-100 dark:bg-purple-900/40 text-purple-800 dark:text-purple-300" 57 | } as Record; 58 | 59 | export type CommonItem = ArticleItem | RecordItem | KnowledgeItem; 60 | 61 | export const HeaderTabs = [ 62 | "/articles", 63 | "/records", 64 | "/knowledges" 65 | ] as const; 66 | 67 | export type HeaderTabUrl = typeof HeaderTabs[number]; 68 | export type CommonItemByHeaderType = T extends "/articles" 69 | ? ArticleItem 70 | : T extends "/records" 71 | ? RecordItem : KnowledgeItem; 72 | 73 | export type DecryptFunction = (_s: string) => Promise; 74 | 75 | export type CommitParamsAddition = { path: string; content: string }; 76 | export type CommitParamsDeletion = { path: string; content: string }; 77 | 78 | export type CommitParams = { 79 | additions?: CommitParamsAddition[]; 80 | deletions?: CommitParamsDeletion[]; 81 | }; 82 | 83 | export type AlgoliaBody = { 84 | title: string; 85 | content: string; 86 | cover: string; 87 | metaData: any; 88 | }; 89 | -------------------------------------------------------------------------------- /e2e/manage/list.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import type { ArticleItem } from "../../app/utils/common/types"; 3 | import { createListPage, setupTestEnvironment } from "./test-helpers"; 4 | 5 | describe("List Editing", async () => { 6 | await setupTestEnvironment(); 7 | 8 | it("Deleting works", async () => { 9 | const { listPage } = await createListPage("/manage/articles"); 10 | 11 | const deleteBtn = await listPage.getByTestId("list-delete-btn"); 12 | expect(deleteBtn).not.toBeNull(); 13 | expect(await deleteBtn.isDisabled()).toBe(true); 14 | 15 | await listPage.selectItemByIndex(0); 16 | expect(await deleteBtn.isDisabled()).toBe(false); 17 | 18 | await listPage.deleteSelectedItems(); 19 | 20 | const additionList = JSON.parse(listPage.requestAdditions[0].content || "") as ArticleItem[]; 21 | expect(additionList.find(i => i.id === 1111)).toBeUndefined(); 22 | expect(additionList.find(i => i.encryptBlocks?.length)).toBeDefined(); 23 | expect(additionList.find(i => i.encrypt && i.title === "U2FsdGVkX18wyEu7vCLMOGilOsG2cQdWY+kvi3b+AZE=")).toBeDefined(); 24 | 25 | const deletionItem = listPage.requestDeletions[0].path || ""; 26 | expect(deletionItem).toContain("1111.md"); 27 | }); 28 | 29 | it("Deleting works after entering password", async () => { 30 | const { listPage } = await createListPage("/manage/articles"); 31 | 32 | const deleteBtn = await listPage.getByTestId("list-delete-btn"); 33 | expect(deleteBtn).not.toBeNull(); 34 | expect(await deleteBtn.isDisabled()).toBe(true); 35 | 36 | const listItemsText = await listPage.getListItemsText(); 37 | expect(listItemsText).toContain("U2FsdGVkX18wyEu7vCLMOGilOsG2cQdWY+kvi3b+AZE="); 38 | 39 | await listPage.enterPassword(); 40 | 41 | const updatedListItemsText = await listPage.getListItemsText(); 42 | expect(updatedListItemsText).not.toContain("U2FsdGVkX18wyEu7vCLMOGilOsG2cQdWY+kvi3b+AZE="); 43 | 44 | await listPage.selectItemByIndex(0); 45 | expect(await deleteBtn.isDisabled()).toBe(false); 46 | 47 | await listPage.deleteSelectedItems(); 48 | 49 | const additionList = JSON.parse(listPage.requestAdditions[0].content || "") as ArticleItem[]; 50 | expect(additionList.find(i => i.id === 1111)).toBeUndefined(); 51 | expect(additionList.find(i => i.encryptBlocks?.length)).toBeDefined(); 52 | expect(additionList.find(i => i.encrypt && i.title === "U2FsdGVkX18wyEu7vCLMOGilOsG2cQdWY+kvi3b+AZE=")).toBeDefined(); 53 | 54 | const deletionItem = listPage.requestDeletions[0].path || ""; 55 | expect(deletionItem).toContain("1111.md"); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /app/pages/manage/comps/sticker-pick.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 77 | 78 | 83 | -------------------------------------------------------------------------------- /scripts/download-img.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | import https from "https"; 4 | import colors from "colors"; 5 | import type { ImgMap } from "./utils"; 6 | import { getAbsolutePath } from "./utils"; 7 | 8 | export default async function (user?: number, group?: number) { 9 | await downloadImages(JSON.parse(fs.readFileSync(getAbsolutePath("img.json")).toString()), user, group); 10 | } 11 | 12 | const imgsPath = getAbsolutePath("imgs"); 13 | 14 | function sleep() { 15 | return new Promise((resolve) => { 16 | setTimeout(resolve, Math.floor(Math.random() * (3000 + 1)) + 2000); 17 | }); 18 | } 19 | 20 | async function downloadImages(json: ImgMap, user?: number, group?: number) { 21 | const urls = Object.keys(json); 22 | 23 | if (!fs.existsSync(imgsPath)) { 24 | fs.mkdirSync(imgsPath); 25 | } 26 | 27 | let succeedCount = 0; 28 | for (const url of urls) { 29 | let count = 0; 30 | while (true) { 31 | try { 32 | const res = await downloadImage(url, user, group); 33 | if (res !== null) { 34 | await sleep(); 35 | } 36 | succeedCount += 1; 37 | break; 38 | } catch { 39 | count += 1; 40 | if (count > 2) { 41 | console.log(`3 times failed, ignore ${url}`); 42 | break; 43 | } 44 | console.log(`sleep and try again(${count})`); 45 | await sleep(); 46 | } 47 | } 48 | } 49 | console.log(colors.bold("Downloaded " + colors.green(succeedCount + "/" + urls.length + " items"))); 50 | } 51 | 52 | function downloadImage(url: string, user?: number, group?: number) { 53 | return new Promise((resolve, reject) => { 54 | const fileName = path.join(imgsPath, url.replace(/^.*?([^/]*)$/, "$1")); 55 | if (fs.existsSync(fileName)) { 56 | console.log(`${fileName} existed, ignore...`); 57 | resolve(null); 58 | return; 59 | } 60 | https.get(url, (response) => { 61 | if (response.statusCode !== 200) { 62 | console.log(`Failed to Download ${fileName}: ${response.statusCode}`); 63 | reject(); 64 | return; 65 | } 66 | 67 | const fileStream = fs.createWriteStream(fileName); 68 | 69 | response.pipe(fileStream); 70 | fileStream.on("finish", () => { 71 | if (user && group) { 72 | fs.chown(fileName, user, group, () => undefined); 73 | } 74 | console.log(`Downloaded ${url} as ${fileName}`); 75 | resolve(true); 76 | }); 77 | }).on("error", (error) => { 78 | console.log(`Error downloading ${url}: ${error}`); 79 | reject(); 80 | }); 81 | }); 82 | } 83 | -------------------------------------------------------------------------------- /e2e/manage/item-edit.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { createItemPage, setupTestEnvironment } from "./test-helpers"; 3 | 4 | describe("Item Editing", async () => { 5 | await setupTestEnvironment(); 6 | 7 | it("Editing works", async () => { 8 | const { itemPage } = await createItemPage("/manage/articles/1111"); 9 | 10 | const uploadBtn = await itemPage.getByTestId("item-upload-btn"); 11 | expect(await uploadBtn.isDisabled()).toBe(true); 12 | 13 | await itemPage.fillItemDetails("new title", "newtag", "new content"); 14 | 15 | await itemPage.uploadItem(); 16 | 17 | await itemPage.verifyItemListInResponse({ 18 | expectedLength: 3, 19 | shouldFindItem: { encrypt: false, title: "new title", tags: ["newtag"] }, 20 | encryptBlocksItemsCount: 1, 21 | shouldContainEncryptedTitle: true 22 | }); 23 | 24 | await itemPage.verifyItemContentInResponse("new content"); 25 | }); 26 | 27 | it("Editing item with encryptedBlock works", async () => { 28 | const { itemPage } = await createItemPage("/manage/articles/2222"); 29 | 30 | const uploadBtn = await itemPage.getByTestId("item-upload-btn"); 31 | expect(await uploadBtn.isDisabled()).toBe(true); 32 | 33 | expect(await itemPage.isElementDisabled("item-title-input")).toBe(true); 34 | 35 | await itemPage.enterPassword(); 36 | 37 | await itemPage.fillItemDetails("new title", "newtag", "[encrypt]\nnew content\n[/encrypt]"); 38 | 39 | await itemPage.uploadItem(); 40 | 41 | await itemPage.verifyItemListInResponse({ 42 | expectedLength: 3, 43 | shouldFindItem: { encrypt: false, title: "new title", tags: ["newtag"] }, 44 | encryptBlocksItemsCount: 1, 45 | shouldContainEncryptedTitle: true 46 | }); 47 | 48 | await itemPage.verifyItemContentInResponse(undefined, "new content"); 49 | }); 50 | 51 | it("Editing encrypted item works", async () => { 52 | const { itemPage } = await createItemPage("/manage/articles/3333"); 53 | 54 | const uploadBtn = await itemPage.getByTestId("item-upload-btn"); 55 | expect(await uploadBtn.isDisabled()).toBe(true); 56 | 57 | expect(await itemPage.isElementDisabled("item-title-input")).toBe(true); 58 | 59 | await itemPage.enterPassword(); 60 | 61 | await itemPage.fillItemDetails("new title", undefined, "new content"); 62 | 63 | await itemPage.uploadItem(); 64 | 65 | await itemPage.verifyItemListInResponse({ 66 | expectedLength: 3, 67 | shouldFindItem: { encrypt: true, tags: [] }, 68 | encryptBlocksItemsCount: 1 69 | }); 70 | 71 | await itemPage.verifyItemContentInResponse(undefined, "new content"); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /app/utils/hooks/useBlogList.test.ts: -------------------------------------------------------------------------------- 1 | import { registerEndpoint } from "@nuxt/test-utils/runtime"; 2 | import { beforeEach, describe, expect, it } from "vitest"; 3 | import type { ArticleItem } from "../common/types"; 4 | import { useBlogList } from "./useBlogList"; 5 | 6 | const timeout = () => new Promise((resolve) => { 7 | setTimeout(resolve, 16); 8 | }); 9 | 10 | registerEndpoint("/rebuild/json/articles.json", () => [ 11 | { 12 | id: 1111, 13 | time: 0, 14 | modifyTime: 0, 15 | encrypt: false, 16 | showComments: true, 17 | title: "test", 18 | len: 4, 19 | tags: ["tag1", "tag2"] 20 | }, { 21 | id: 2222, 22 | time: 0, 23 | modifyTime: 0, 24 | encrypt: false, 25 | encryptBlocks: [{ start: 86, end: 130 }, { start: 15, end: 59 }], 26 | showComments: false, 27 | title: "test", 28 | len: 61, 29 | tags: ["tag1", "tag2"] 30 | }, { 31 | id: 3333, 32 | time: 0, 33 | modifyTime: 0, 34 | encrypt: true, 35 | showComments: false, 36 | title: "U2FsdGVkX18wyEu7vCLMOGilOsG2cQdWY+kvi3b+AZE=", 37 | len: 4, 38 | tags: [] 39 | } 40 | ] as ArticleItem[]); 41 | 42 | describe("useBlogList", () => { 43 | const encryptor = useEncryptor(); 44 | 45 | beforeEach(() => { 46 | encryptor.usePasswd.value = ""; 47 | }); 48 | 49 | it("should return originList and decryptedList", async () => { 50 | const { originList, decryptedList } = await useBlogList("/articles"); 51 | expect(originList).lengthOf(3); 52 | expect(decryptedList.value).lengthOf(3); 53 | }); 54 | 55 | it("should success decrypt already correct", async () => { 56 | encryptor.usePasswd.value = "123"; 57 | await timeout(); 58 | const { decryptedList } = await useBlogList("/articles"); 59 | expect(decryptedList.value?.[2]?.title).toBe("test"); 60 | }); 61 | 62 | it("should success decrypt correct -> incorrect", async () => { 63 | const { decryptedList } = await useBlogList("/articles"); 64 | encryptor.usePasswd.value = "123"; 65 | await timeout(); 66 | expect(decryptedList.value?.[2]?.title).toBe("test"); 67 | encryptor.usePasswd.value = "456"; 68 | await timeout(); 69 | expect(decryptedList.value?.[2]?.title).toBe("test"); 70 | }); 71 | 72 | it("should success decrypt incorrect -> correct", async () => { 73 | const { decryptedList } = await useBlogList("/articles", "3333"); 74 | encryptor.usePasswd.value = "456"; 75 | await timeout(); 76 | expect(decryptedList.value?.[2]?.title).not.toBe("test"); 77 | encryptor.usePasswd.value = "123"; 78 | await timeout(); 79 | expect(decryptedList.value?.[2]?.title).toBe("test"); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /app/utils/api/smms/upload.ts: -------------------------------------------------------------------------------- 1 | import https from "https"; 2 | import FormData from "form-data"; 3 | import tinify from "tinify"; 4 | 5 | export async function smmsUpload( 6 | { token, tinyPngToken, file }: 7 | { token?: string; tinyPngToken?: string; file: any } 8 | ) { 9 | if (!token) { 10 | throw new Error("Need token"); 11 | } 12 | 13 | let buffer = file.data; 14 | let size = Buffer.byteLength(buffer); 15 | // tinypng 16 | if (tinyPngToken) { 17 | tinify.key = tinyPngToken; 18 | try { 19 | const source = tinify.fromBuffer(buffer); 20 | buffer = await source.toBuffer(); 21 | size = Buffer.byteLength(buffer); 22 | } catch (e: any) { 23 | throw new Error(`Error from tinypng: ${e.toString()}`); 24 | } 25 | } 26 | 27 | const formData = new FormData(); 28 | const filename = Date.now().toString(); 29 | formData.append("smfile", buffer, { 30 | knownLength: size, 31 | filepath: filename, 32 | filename 33 | }); 34 | const len = await new Promise((resolve, reject) => { 35 | formData.getLength((err, len) => { 36 | if (err) { 37 | reject(err); 38 | return; 39 | } 40 | resolve(len); 41 | }); 42 | }); 43 | const response = await uploadFile({ 44 | headers: { 45 | "Authorization": token, 46 | "Content-Length": len.toString(), 47 | "Content-Type": `multipart/form-data; boundary=${formData.getBoundary()}` 48 | }, 49 | formData 50 | }); 51 | // XXX why $fetch cannot update FormData? 52 | // const response = await $fetch("https://sm.ms/api/v2/upload", { 53 | // method: "POST", 54 | // headers: { 55 | // Authorization: token, 56 | // "Content-Length": len.toString(), 57 | // "Content-Type": `multipart/form-data; boundary=${formData.getBoundary()}` 58 | // }, 59 | // body: formData 60 | // }); 61 | return response; 62 | } 63 | 64 | function uploadFile({ headers, formData }: { 65 | headers: any; 66 | formData: any; 67 | }) { 68 | const url = "https://sm.ms/api/v2/upload"; 69 | 70 | const options = { 71 | method: "POST", 72 | headers 73 | }; 74 | 75 | return new Promise((resolve, reject) => { 76 | const req = https.request(url, options, (res) => { 77 | let responseData = ""; 78 | 79 | res.on("data", (chunk) => { 80 | responseData += chunk; 81 | }); 82 | 83 | res.on("end", () => { 84 | if (res.statusCode === 200) { 85 | resolve(JSON.parse(responseData)); 86 | } else { 87 | reject(responseData); 88 | } 89 | }); 90 | }); 91 | 92 | req.on("error", (error) => { 93 | reject(error.message); 94 | }); 95 | 96 | formData.pipe(req); 97 | }); 98 | } 99 | -------------------------------------------------------------------------------- /app/pages/knowledges/[id].vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 68 | 69 | 78 | -------------------------------------------------------------------------------- /app/assets/style/code-style.css: -------------------------------------------------------------------------------- 1 | html.light .hljs { 2 | .hljs-doctag, 3 | .hljs-keyword, 4 | .hljs-meta .hljs-keyword, 5 | .hljs-template-tag, 6 | .hljs-template-variable, 7 | .hljs-type, 8 | .hljs-variable.language_ { color: #d73a49; } 9 | 10 | .hljs-title, 11 | .hljs-title.class_, 12 | .hljs-title.class_.inherited__, 13 | .hljs-title.function_ { color: #6f42c1; } 14 | 15 | .hljs-attr, 16 | .hljs-attribute, 17 | .hljs-literal, 18 | .hljs-meta, 19 | .hljs-number, 20 | .hljs-operator, 21 | .hljs-selector-attr, 22 | .hljs-selector-class, 23 | .hljs-selector-id, 24 | .hljs-variable { color: #005cc5; } 25 | 26 | .hljs-meta .hljs-string, 27 | .hljs-regexp, 28 | .hljs-string { color: #032f62; } 29 | 30 | .hljs-built_in, 31 | .hljs-symbol { color: #e36209; } 32 | 33 | .hljs-code, 34 | .hljs-comment, 35 | .hljs-formula { color: #6a737d; } 36 | 37 | .hljs-name, 38 | .hljs-quote, 39 | .hljs-selector-pseudo, 40 | .hljs-selector-tag { color: #22863a; } 41 | .hljs-subst { color: #24292e; } 42 | 43 | .hljs-section { 44 | color: #005cc5; 45 | font-weight: 700; 46 | } 47 | .hljs-bullet { color: #735c0f; } 48 | 49 | .hljs-emphasis { 50 | color: #24292e; 51 | font-style: italic; 52 | } 53 | 54 | .hljs-strong { 55 | color: #24292e; 56 | font-weight: 700; 57 | } 58 | 59 | .hljs-addition { 60 | color: #22863a; 61 | background-color: #f0fff4; 62 | } 63 | 64 | .hljs-deletion { 65 | color: #b31d28; 66 | background-color: #ffeef0; 67 | } 68 | } 69 | 70 | html.dark .hljs { 71 | .hljs-comment, 72 | .hljs-quote { 73 | color: #5c6370; 74 | font-style: italic; 75 | } 76 | 77 | .hljs-doctag, 78 | .hljs-formula, 79 | .hljs-keyword { color: #c678dd; } 80 | 81 | .hljs-deletion, 82 | .hljs-name, 83 | .hljs-section, 84 | .hljs-selector-tag, 85 | .hljs-subst { color: #e06c75; } 86 | .hljs-literal { color: #56b6c2; } 87 | 88 | .hljs-addition, 89 | .hljs-attribute, 90 | .hljs-meta .hljs-string, 91 | .hljs-regexp, 92 | .hljs-string { color: #98c379; } 93 | 94 | .hljs-attr, 95 | .hljs-number, 96 | .hljs-selector-attr, 97 | .hljs-selector-class, 98 | .hljs-selector-pseudo, 99 | .hljs-template-variable, 100 | .hljs-type, 101 | .hljs-variable { color: #d19a66; } 102 | 103 | .hljs-bullet, 104 | .hljs-link, 105 | .hljs-meta, 106 | .hljs-selector-id, 107 | .hljs-symbol, 108 | .hljs-title { color: #61aeee; } 109 | 110 | .hljs-built_in, 111 | .hljs-class .hljs-title, 112 | .hljs-title.class_ { color: #e6c07b; } 113 | .hljs-emphasis { font-style: italic; } 114 | .hljs-strong { font-weight: 700; } 115 | .hljs-link { text-decoration: underline; } 116 | } 117 | -------------------------------------------------------------------------------- /app/pages/manage/config/index.vue: -------------------------------------------------------------------------------- 1 | 63 | 64 | 90 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .nuxt 2 | nuxt.d.ts 3 | .output 4 | img.json 5 | imgs/ 6 | 7 | /app/assets/style/generated-theme-colors.css 8 | 9 | # Logs 10 | logs 11 | *.log 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | lerna-debug.log* 16 | .pnpm-debug.log* 17 | 18 | # Diagnostic reports (https://nodejs.org/api/report.html) 19 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 20 | 21 | # Runtime data 22 | pids 23 | *.pid 24 | *.seed 25 | *.pid.lock 26 | 27 | # Directory for instrumented libs generated by jscoverage/JSCover 28 | lib-cov 29 | 30 | # Coverage directory used by tools like istanbul 31 | coverage 32 | *.lcov 33 | 34 | # nyc test coverage 35 | .nyc_output 36 | 37 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 38 | .grunt 39 | 40 | # Bower dependency directory (https://bower.io/) 41 | bower_components 42 | 43 | # node-waf configuration 44 | .lock-wscript 45 | 46 | # Compiled binary addons (https://nodejs.org/api/addons.html) 47 | build/Release 48 | 49 | # Dependency directories 50 | node_modules/ 51 | jspm_packages/ 52 | 53 | # Snowpack dependency directory (https://snowpack.dev/) 54 | web_modules/ 55 | 56 | # TypeScript cache 57 | *.tsbuildinfo 58 | 59 | # Optional npm cache directory 60 | .npm 61 | 62 | # Optional eslint cache 63 | .eslintcache 64 | 65 | # Optional stylelint cache 66 | .stylelintcache 67 | 68 | # Microbundle cache 69 | .rpt2_cache/ 70 | .rts2_cache_cjs/ 71 | .rts2_cache_es/ 72 | .rts2_cache_umd/ 73 | 74 | # Optional REPL history 75 | .node_repl_history 76 | 77 | # Output of 'npm pack' 78 | *.tgz 79 | 80 | # Yarn Integrity file 81 | .yarn-integrity 82 | 83 | # dotenv environment variable files 84 | .env 85 | .env.development.local 86 | .env.test.local 87 | .env.production.local 88 | .env.local 89 | 90 | # parcel-bundler cache (https://parceljs.org/) 91 | .cache 92 | .parcel-cache 93 | 94 | # Next.js build output 95 | .next 96 | out 97 | 98 | # Nuxt.js build / generate output 99 | .nuxt 100 | dist 101 | 102 | # Gatsby files 103 | .cache/ 104 | # Comment in the public line in if your project uses Gatsby and not Next.js 105 | # https://nextjs.org/blog/next-9-1#public-directory-support 106 | # public 107 | 108 | # vuepress build output 109 | .vuepress/dist 110 | 111 | # vuepress v2.x temp and cache directory 112 | .temp 113 | .cache 114 | 115 | # Docusaurus cache and generated files 116 | .docusaurus 117 | 118 | # Serverless directories 119 | .serverless/ 120 | 121 | # FuseBox cache 122 | .fusebox/ 123 | 124 | # DynamoDB Local files 125 | .dynamodb/ 126 | 127 | # TernJS port file 128 | .tern-port 129 | 130 | # Stores VSCode versions used for testing VSCode extensions 131 | .vscode-test 132 | 133 | # yarn v2 134 | .yarn/cache 135 | .yarn/unplugged 136 | .yarn/build-state.yml 137 | .yarn/install-state.gz 138 | .pnp.* 139 | -------------------------------------------------------------------------------- /e2e/page-objects/manage/BasePage.ts: -------------------------------------------------------------------------------- 1 | import type { NuxtPage } from "@nuxt/test-utils"; 2 | import { ref, unref } from "vue"; 3 | import type { CommitParams } from "../../../app/utils/common/types"; 4 | 5 | export class ManageBasePage { 6 | protected requestDataRef = ref(); 7 | 8 | constructor(protected page: NuxtPage) { 9 | page.route("https://api.github.com/graphql", async (route) => { 10 | this.requestDataRef.value = JSON.parse(route.request().postDataJSON().query); 11 | await route.fulfill({ json: { data: {} } }); 12 | }); 13 | } 14 | 15 | get requestAdditions() { 16 | return unref(this.requestDataRef)?.additions || []; 17 | } 18 | 19 | get requestDeletions() { 20 | return unref(this.requestDataRef)?.deletions || []; 21 | } 22 | 23 | async screenShot() { 24 | await this.page.screenshot({ path: "screenshot.png" }); 25 | } 26 | 27 | async waitForTimeout(ms = 200) { 28 | return new Promise((resolve) => { 29 | setTimeout(resolve, ms); 30 | }); 31 | } 32 | 33 | async getByTestId(testId: string) { 34 | return this.page.getByTestId(testId); 35 | } 36 | 37 | async fillInput(testId: string, value: string) { 38 | const input = await this.getByTestId(testId); 39 | await input.fill(value); 40 | await this.waitForTimeout(); 41 | return input; 42 | } 43 | 44 | async clickElement(testId: string) { 45 | const element = await this.getByTestId(testId); 46 | await element.click(); 47 | await this.waitForTimeout(); 48 | return element; 49 | } 50 | 51 | async getMonacoEditor() { 52 | return this.page.locator(".monaco-editor").nth(0); 53 | } 54 | 55 | async clearAndTypeInMonacoEditor(content: string) { 56 | const monacoEditor = await this.getMonacoEditor(); 57 | await monacoEditor.click(); 58 | await this.page.keyboard.press("ControlOrMeta+KeyA"); 59 | await this.page.keyboard.type(content); 60 | await this.waitForTimeout(); 61 | return monacoEditor; 62 | } 63 | 64 | async getMonacoEditorText() { 65 | const monacoEditor = await this.getMonacoEditor(); 66 | return (await monacoEditor.textContent())?.replace(/\s+/g, ""); 67 | } 68 | 69 | async isElementDisabled(testId: string) { 70 | const element = await this.getByTestId(testId); 71 | return element.isDisabled(); 72 | } 73 | 74 | async getInputText(testId: string) { 75 | const element = await this.getByTestId(testId); 76 | return element.inputValue(); 77 | } 78 | 79 | async getElementText(testId: string) { 80 | const element = await this.getByTestId(testId); 81 | return element.innerText(); 82 | } 83 | 84 | async enterPassword() { 85 | this.clickElement("show-token-password-btn"); 86 | await this.waitForTimeout(); 87 | this.fillInput("password-input", "123"); 88 | this.clickElement("token-password-confirm"); 89 | await this.waitForTimeout(1000); 90 | }; 91 | } 92 | -------------------------------------------------------------------------------- /app/utils/hooks/useBlogItem.ts: -------------------------------------------------------------------------------- 1 | import type { CommonItem, HeaderTabUrl } from "../common/types"; 2 | import { fetchMd } from "../nuxt/fetch"; 3 | import { translate } from "../nuxt/i18n"; 4 | import { useBlogList } from "./useBlogList"; 5 | 6 | export const useBlogItem = async (id: string, url: HeaderTabUrl, showNotFound = true) => { 7 | const githubToken = useGithubToken(); 8 | const encryptor = useEncryptor(); 9 | const { originList, decryptedList } = await useBlogList(url, id); 10 | 11 | const originItem = originList.find(i => i.customSlug === id || i.id === Number(id)); 12 | const decryptedItem = computed(() => decryptedList.value?.find(i => i.customSlug === id || i.id === Number(id))) as Readonly>; 13 | let originMd = ""; 14 | const decryptedMd = ref(""); 15 | const successDecrypt = ref(false); 16 | 17 | if (originItem) { 18 | const item = originItem; 19 | originMd = await fetchMd(url, String(originItem.id)); 20 | if (item.encrypt) { 21 | decryptedMd.value = originMd; 22 | await encryptor.decryptOrWatchToDecrypt( 23 | async (decrypt) => { 24 | decryptedMd.value = await decrypt(originMd); 25 | successDecrypt.value = true; 26 | } 27 | ); 28 | } else if (item.encryptBlocks) { 29 | watch(githubToken, async (logined) => { 30 | let newMarkdownContent = originMd; 31 | for (const block of item.encryptBlocks!) { 32 | const { start, end } = block; 33 | newMarkdownContent = logined 34 | // 如果已登录:给block显示为sticker表情 35 | ? newMarkdownContent.slice(0, start) + translate("encrypted-content") + "![sticker](aru/59)" + newMarkdownContent.slice(end) 36 | // 如果未登录:直接隐藏block 37 | : newMarkdownContent.slice(0, start - 10) + newMarkdownContent.slice(end + 11); 38 | } 39 | decryptedMd.value = newMarkdownContent; 40 | 41 | await encryptor.decryptOrWatchToDecrypt(async (decrypt) => { 42 | let newMarkdownContent = originMd; 43 | for (const block of item.encryptBlocks!) { 44 | const { start, end } = block; 45 | newMarkdownContent = newMarkdownContent.slice(0, start) + await decrypt(newMarkdownContent.slice(start, end)) + newMarkdownContent.slice(end); 46 | } 47 | decryptedMd.value = newMarkdownContent; 48 | successDecrypt.value = true; 49 | }); 50 | }, { immediate: true }); 51 | } else { 52 | decryptedMd.value = originMd; 53 | successDecrypt.value = true; 54 | } 55 | } else if (showNotFound) { 56 | showError({ 57 | status: 404, 58 | statusText: `${url}/${id} not found`, 59 | message: "wtf bro" 60 | }); 61 | } 62 | 63 | return { 64 | originList, 65 | decryptedList, 66 | 67 | successDecrypt, 68 | originItem, 69 | decryptedItem, 70 | originMd, 71 | decryptedMd 72 | }; 73 | }; 74 | -------------------------------------------------------------------------------- /scripts/utils/encrypt.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import colors from "colors"; 3 | import CryptoJS from "crypto-js"; 4 | import type { CommonItem, DecryptFunction, HeaderTabUrl } from "../../app/utils/common/types"; 5 | import { encryptDecryptItem, getEncryptedBlocks } from "../../app/utils/common/process-encrypt-decrypt"; 6 | import { walkAllBlogData } from "."; 7 | 8 | export async function encrypt(s: string, pwd: string): Promise { 9 | return CryptoJS.AES.encrypt(s, pwd).toString(); 10 | } 11 | 12 | export async function decrypt(s: string, pwd: string): Promise { 13 | const result = CryptoJS.AES.decrypt(s, pwd).toString(CryptoJS.enc.Utf8); 14 | if (!result) { 15 | console.error(colors.red("Password incorrect")); 16 | process.exit(); 17 | } 18 | return result; 19 | } 20 | 21 | export async function processBlogItem( 22 | pwd: string, 23 | /** 单个item回调 */ 24 | processItem: (_: { decryptedItem: CommonItem; decryptedMd: string; type: HeaderTabUrl; mdPath: string }) => any | Promise, 25 | /** 整个json回调 */ 26 | processJson: (_: { decryptedItemList: CommonItem[]; type: HeaderTabUrl; jsonPath: string }) => any | Promise = () => null 27 | ) { 28 | const _decrypt = (s: string) => decrypt(s, pwd); 29 | 30 | const blogData = walkAllBlogData(); 31 | for (const { type, jsonPath, list: itemList } of blogData) { 32 | let count = 0; 33 | for (const item of itemList) { 34 | let decryptedMd = item._md; 35 | if (item.encrypt) { 36 | // 解密item 37 | await encryptDecryptItem(item, _decrypt, type); 38 | // 解密markdown 39 | decryptedMd = await _decrypt(decryptedMd); 40 | } else if (item.encryptBlocks?.length) { 41 | // 解密 encrypted blocks 42 | for (const block of item.encryptBlocks) { 43 | const { start, end } = block; 44 | decryptedMd = decryptedMd.slice(0, start) + await _decrypt(decryptedMd.slice(start, end)) + decryptedMd.slice(end); 45 | } 46 | } 47 | await processItem({ decryptedItem: item, decryptedMd, type, mdPath: item._mdPath }); 48 | count += 1; 49 | } 50 | await processJson({ decryptedItemList: itemList, type, jsonPath }); 51 | console.log(colors.bold.bgCyan(type.substring(1) + ` (${count} in total) processing completed!`)); 52 | } 53 | } 54 | 55 | /** 56 | * 重新加密md并写入文件,同时处理item里的encrypted blocks 57 | */ 58 | export async function encryptAndWriteMd({ item, md, path, encrypt, type }: { item: CommonItem; md: string; path: string; encrypt: DecryptFunction; type: HeaderTabUrl }) { 59 | if (item.encrypt) { 60 | // 重新加密item 61 | await encryptDecryptItem(item, encrypt, type); 62 | // 重新加密markdown 63 | const encryptedMd = await encrypt(md); 64 | fs.writeFileSync(path, encryptedMd); 65 | } else if (item.encryptBlocks) { 66 | // 重新加密blocks 67 | const { md: encryptedMd, blocks } = await getEncryptedBlocks(md, encrypt); 68 | fs.writeFileSync(path, encryptedMd); 69 | if (blocks.length) { 70 | item.encryptBlocks = blocks.reverse(); 71 | } else { 72 | delete item.encryptBlocks; 73 | } 74 | } else { 75 | fs.writeFileSync(path, md); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /app/pages/manage/comps/version-update-modal.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 85 | --------------------------------------------------------------------------------