├── .env ├── .env.example ├── .github └── workflows │ └── deploy-to-bt.yml ├── .gitignore ├── DEPLOY.md ├── Dockerfile ├── IMPLEMENTATION_NOTES.md ├── NAMING_CONVENTIONS.md ├── README.md ├── STANDALONE.md ├── backup ├── ClientSidebar.tsx ├── delete-user.js └── reset-admin-account.js ├── blog.db ├── build-log.txt ├── copy-static-assets.sh ├── db-intro.md ├── deploy-standalone.sh ├── docker-compose.yml ├── ecosystem.config.js ├── eslint.config.mjs ├── html-page-example.html ├── links.db ├── logs └── .gitkeep ├── migrations └── add_page_type_to_posts.js ├── next.config.js ├── next.config.js.bak ├── next.config.ts ├── package-lock.json ├── package.json ├── postcss.config.js ├── postcss.config.mjs ├── public ├── file.svg ├── globe.svg ├── icon │ ├── android │ │ ├── play_store_512.png │ │ └── res │ │ │ ├── mipmap-anydpi-v26 │ │ │ └── ic_launcher.xml │ │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_background.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_monochrome.png │ │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_background.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_monochrome.png │ │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_background.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_monochrome.png │ │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_background.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_monochrome.png │ │ │ └── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_background.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_monochrome.png │ ├── ios │ │ ├── AppIcon-20@2x.png │ │ ├── AppIcon-20@2x~ipad.png │ │ ├── AppIcon-20@3x.png │ │ ├── AppIcon-20~ipad.png │ │ ├── AppIcon-29.png │ │ ├── AppIcon-29@2x.png │ │ ├── AppIcon-29@2x~ipad.png │ │ ├── AppIcon-29@3x.png │ │ ├── AppIcon-29~ipad.png │ │ ├── AppIcon-40@2x.png │ │ ├── AppIcon-40@2x~ipad.png │ │ ├── AppIcon-40@3x.png │ │ ├── AppIcon-40~ipad.png │ │ ├── AppIcon-60@2x~car.png │ │ ├── AppIcon-60@3x~car.png │ │ ├── AppIcon-83.5@2x~ipad.png │ │ ├── AppIcon@2x.png │ │ ├── AppIcon@2x~ipad.png │ │ ├── AppIcon@3x.png │ │ ├── AppIcon~ios-marketing.png │ │ ├── AppIcon~ipad.png │ │ └── Contents.json │ └── web │ │ ├── README.txt │ │ ├── apple-touch-icon.png │ │ ├── favicon.ico │ │ ├── icon-192-maskable.png │ │ ├── icon-192.png │ │ ├── icon-512-maskable.png │ │ └── icon-512.png ├── icons │ ├── github.svg │ ├── twitter.svg │ ├── wechat.svg │ └── weibo.svg ├── images │ ├── default-qrcode.png │ └── default-thumbnail.png ├── js │ ├── mobile-menu.js │ ├── slider.js │ └── theme.js ├── menu-test.html ├── next.svg ├── placeholder-qr.png ├── placeholder-qr.svg ├── test.html ├── uploads │ ├── 0bad1549-227b-462b-b500-5c40cf346f29.png │ ├── 1b65d6d3-6ef8-4fbe-a242-afe60c2136b0.png │ ├── 20ddc0c2-a981-404c-bd25-5acc8b5d6e54.png │ ├── 304bb973-5262-4074-99f2-ce2cce282dda.png │ ├── 33611009-b45e-4f5e-85cc-ab75da2541a9.png │ ├── 373a5d7f-4a9c-4f79-b4bf-144485bc60e1.png │ ├── 3971a0c0-2c9b-4cec-85fd-c613742a0a7d.png │ ├── 51a94123-3c24-431d-9a08-bc88a3be0d11.png │ ├── 51e92ef1-7c54-40a3-82a5-5e4d7ae41f47.png │ ├── 5a3c2bd2-e0fc-4890-a902-64039547cf2d.png │ ├── 5aa0c80f-b23d-4081-af5d-55c4b9f8e7c3.png │ ├── 5b13b0c7-8363-4d2d-a156-c37c46287df1.png │ ├── 5bcec724-4c3b-4545-9c95-f183698296d4.png │ ├── 6321e5b7-5866-44d8-98b6-8eb4e8531407.png │ ├── 6413a9bf-35ee-4fba-b6bd-f260d5543f95.png │ ├── 64ee3e69-f676-4ccb-b353-28f64095a362.png │ ├── 684f0a84-4d8b-4d57-bb71-b7451c7504d2.png │ ├── 686bee4c-67db-4f63-a623-8a291f06f30e.png │ ├── 6b1b3100-d856-4ffb-a033-280356beaf3d.png │ ├── 6bb144cc-a10a-44b0-acbb-a56f10dcd16a.png │ ├── 70d744cd-85df-4cbd-95d7-3efb3ba6b983.png │ ├── 7d0f6464-d3ad-480f-8963-25e9f442da42.png │ ├── 7e849ea9-76b5-4481-8af4-57dd0c1606f7.png │ ├── 8147285b-d577-43c0-a161-bfdcbb08e1e1.png │ ├── 875f2cbc-c88f-4b29-adf6-734530ccbc0e.png │ ├── 87b63bc7-96e2-4982-a47d-c6ea65b8efc3.png │ ├── 90733c3c-6d6d-4894-8c37-90dc8cf0a816.png │ ├── 9161c3b8-6daa-4e51-a3f1-f71d597e326d.png │ ├── 94deca3e-6e7d-4efd-8ebd-3755bb723ed0.png │ ├── 97d41ef4-cc7b-4d20-b1a5-47546288c2a7.png │ ├── 999bf369-8477-4add-8384-4c4abda1df6a.png │ ├── 9bb06850-815d-4834-8a3e-474c9e1d1e6b.png │ ├── a80ff25a-4a5b-45c2-97b6-2c2a8d31894d.png │ ├── aab9ba5b-3dd9-4f61-9130-00582184beca.png │ ├── abb91af5-0edd-4459-a37f-a30cc2e616b0.png │ ├── abf2ae63-7c74-465e-958c-7b39bd43478f.png │ ├── b6a65458-8256-402b-9a6b-67c203229de3.png │ ├── b7772814-5698-4902-9d31-6fd10453b673.png │ ├── bc982ea5-1909-486d-a7d2-ae591744126c.png │ ├── cc3a296e-5e6f-40e9-820d-5d49add696d0.png │ ├── cc978f5b-c4a6-4f3c-9242-a2e8b790b9a1.png │ ├── contact │ │ ├── 05547404-c0cc-4229-bb42-ed4eb28de871.jpeg │ │ ├── 3e5464e1-f6cd-4600-b9dc-6f9552547d29.png │ │ └── ca46cdc3-6463-4794-a27b-174aa373f15e.jpeg │ ├── dc32267f-895c-49f9-a03e-4bb861abeead.png │ ├── donation │ │ └── 63a278f4-8927-4123-b95c-4dffb5ea7430.png │ ├── e3f66a31-9000-4cf0-a1cb-44939aa69117.png │ ├── e8a76490-4fdb-411c-a996-01c3d896e7c0.png │ ├── f0a78892-ca47-4753-a5ff-369674b5e19c.png │ ├── f69badfa-94f4-4509-a70c-7338511697a4.png │ ├── f8eec96f-5d65-46b5-9648-b729eb4c54d9.png │ ├── f985396d-8d7a-4d65-8501-8a4bcad61929.png │ └── fa48845d-46e4-48e9-a53c-2a41d7488a34.png ├── vercel.svg └── window.svg ├── rebuild.sh ├── ref.html ├── reset-categories.js ├── rules.md ├── scripts ├── add-is-visible-to-links.js ├── add-missing-tables.js ├── create-admin.js ├── create-demo-db.js ├── init-db.js ├── init-links-db.js ├── insert-test-menus.sql ├── reset-admin.js ├── run-migration.js ├── start-dev.bat └── start-dev.sh ├── setup.bat ├── setup.sh ├── src ├── app │ ├── (frontend) │ │ ├── layout.tsx │ │ └── page.tsx │ ├── (main) │ │ └── layout.tsx │ ├── admin │ │ ├── categories │ │ │ └── page.tsx │ │ ├── dashboard │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── links │ │ │ ├── page.tsx │ │ │ └── webhooks │ │ │ │ └── page.tsx │ │ ├── menus │ │ │ └── page.tsx │ │ ├── page.tsx │ │ ├── posts │ │ │ ├── edit │ │ │ │ └── [id] │ │ │ │ │ └── page.tsx │ │ │ ├── new │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ ├── settings │ │ │ ├── general │ │ │ │ └── page.tsx │ │ │ ├── page.tsx │ │ │ └── scripts │ │ │ │ └── page.tsx │ │ ├── tags │ │ │ └── page.tsx │ │ └── users │ │ │ └── page.tsx │ ├── api │ │ ├── admin-bypass │ │ │ └── route.ts │ │ ├── admin │ │ │ ├── create-admin │ │ │ │ └── route.ts │ │ │ ├── init-db │ │ │ │ └── route.ts │ │ │ ├── posts │ │ │ │ └── route.ts │ │ │ └── stats │ │ │ │ └── route.ts │ │ ├── auth │ │ │ └── [...nextauth] │ │ │ │ └── route.ts │ │ ├── categories │ │ │ ├── [id] │ │ │ │ ├── posts │ │ │ │ │ └── route.ts │ │ │ │ └── route.ts │ │ │ ├── list │ │ │ │ └── route.ts │ │ │ ├── reorder │ │ │ │ └── route.ts │ │ │ ├── reset │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ ├── create-user │ │ │ └── route.ts │ │ ├── debug-auth │ │ │ └── route.ts │ │ ├── html-content │ │ │ └── route.ts │ │ ├── links │ │ │ ├── [id] │ │ │ │ └── route.ts │ │ │ ├── route.ts │ │ │ └── webhook │ │ │ │ └── route.ts │ │ ├── login │ │ │ └── route.ts │ │ ├── logout │ │ │ └── route.ts │ │ ├── menus │ │ │ ├── [id] │ │ │ │ └── route.ts │ │ │ ├── reorder │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ ├── post-by-slug │ │ │ └── [slug] │ │ │ │ └── route.ts │ │ ├── posts │ │ │ ├── [id] │ │ │ │ ├── categories │ │ │ │ │ └── route.ts │ │ │ │ ├── route.ts │ │ │ │ └── tags │ │ │ │ │ └── route.ts │ │ │ ├── all │ │ │ │ └── route.ts │ │ │ ├── navigation │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ ├── register │ │ │ └── route.ts │ │ ├── revalidate │ │ │ └── route.ts │ │ ├── search │ │ │ └── route.ts │ │ ├── settings │ │ │ ├── contact │ │ │ │ ├── [id] │ │ │ │ │ └── route.ts │ │ │ │ └── route.ts │ │ │ ├── donation │ │ │ │ ├── [id] │ │ │ │ │ └── route.ts │ │ │ │ └── route.ts │ │ │ ├── general │ │ │ │ └── route.ts │ │ │ ├── route.ts │ │ │ └── scripts │ │ │ │ ├── [id] │ │ │ │ └── route.ts │ │ │ │ ├── position │ │ │ │ └── route.ts │ │ │ │ └── route.ts │ │ ├── tags │ │ │ ├── [slug] │ │ │ │ ├── posts │ │ │ │ │ └── route.ts │ │ │ │ └── route.ts │ │ │ ├── all │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ ├── upload │ │ │ └── route.ts │ │ ├── users │ │ │ ├── [id] │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ └── webhooks │ │ │ ├── [id] │ │ │ └── route.ts │ │ │ └── route.ts │ ├── categories │ │ └── [slug] │ │ │ ├── page.client.tsx │ │ │ └── page.tsx │ ├── direct-login │ │ └── page.tsx │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ ├── links │ │ ├── [id] │ │ │ └── page.tsx │ │ ├── page.client.tsx │ │ └── page.tsx │ ├── login-debug │ │ └── page.tsx │ ├── login │ │ ├── layout.tsx │ │ └── page.tsx │ ├── menu-test │ │ └── page.tsx │ ├── new │ │ └── page.tsx │ ├── page.tsx │ ├── posts │ │ ├── [slug] │ │ │ └── page.tsx │ │ ├── page.client.tsx │ │ └── page.tsx │ ├── preview-login │ │ └── page.tsx │ ├── register │ │ └── page.tsx │ ├── search │ │ ├── page.client.tsx │ │ └── page.tsx │ ├── simple │ │ └── page.tsx │ ├── tags │ │ ├── [slug] │ │ │ ├── page.client.tsx │ │ │ └── page.tsx │ │ ├── page.client.tsx │ │ └── page.tsx │ └── uploads │ │ └── [...path] │ │ └── route.ts ├── auth │ └── options.ts ├── components │ ├── AdminCheck.tsx │ ├── AdminPublishLink.tsx │ ├── Avatar.tsx │ ├── EditPostLink.tsx │ ├── FeaturedSlider.tsx │ ├── FeaturedSliderClient.tsx │ ├── FloatingInfoBar.tsx │ ├── Footer.tsx │ ├── HeroSection.tsx │ ├── HtmlPageLayout.tsx │ ├── ImageUploader.tsx │ ├── IsolatedMarkdownEditor.tsx │ ├── LatestArticles.tsx │ ├── LatestArticlesClient.tsx │ ├── MainLayout.tsx │ ├── MarkdownEditor.tsx │ ├── MobileMenu.tsx │ ├── Navigation.tsx │ ├── NextScriptLoader.tsx │ ├── PaginationLinks.tsx │ ├── PostCard.tsx │ ├── PostsFilters.tsx │ ├── ScriptLoader.tsx │ ├── ScriptLoaderWrapper.tsx │ ├── SearchBox.tsx │ ├── SearchFilters.tsx │ ├── Sidebar.tsx │ ├── SimpleFooter.tsx │ ├── SimpleMobileMenu.tsx │ ├── SimpleNavigation.tsx │ ├── TagifyInput.tsx │ ├── ThemeToggle.tsx │ ├── admin │ │ ├── SortableItem.tsx │ │ ├── header.tsx │ │ └── layout.tsx │ ├── providers │ │ ├── SessionProvider.tsx │ │ └── ThemeProvider.tsx │ └── ui │ │ ├── alert-dialog.tsx │ │ ├── alert.tsx │ │ ├── badge.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── checkbox.tsx │ │ ├── dialog.tsx │ │ ├── icons.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── link.tsx │ │ ├── modal.tsx │ │ ├── navigation-menu.tsx │ │ ├── pagination.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── switch.tsx │ │ ├── table.tsx │ │ ├── tabs.tsx │ │ ├── textarea.tsx │ │ ├── toast.tsx │ │ ├── toaster.tsx │ │ ├── toggle-group.tsx │ │ ├── tooltip.tsx │ │ └── use-toast.ts ├── contexts │ └── CategoriesContext.tsx ├── hooks │ └── useSettings.ts ├── lib │ ├── actions │ │ └── links.ts │ ├── db.ts │ ├── db │ │ └── migrations │ │ │ └── 0007_add_head_scripts_table.ts │ ├── links-db.ts │ ├── migrate.ts │ ├── migrations │ │ ├── 0000_init_schema.sql │ │ ├── 0000_init_schema.ts │ │ └── meta │ │ │ └── _journal.json │ ├── mock-modules │ │ └── nodejieba.js │ ├── reset-password.ts │ ├── schema.ts │ ├── schema │ │ ├── links.ts │ │ └── settings.ts │ ├── services │ │ └── settings.ts │ ├── utils.ts │ └── utils │ │ └── menu-adapters.ts ├── middleware.ts ├── scripts │ ├── check-users.ts │ ├── migrate.ts │ ├── migrations │ │ ├── add-menus-table.ts │ │ └── update-tags-table.ts │ ├── update-categories.ts │ └── update-password.ts ├── styles │ ├── article.css │ ├── editor-fix.css │ ├── markdown-editor.css │ ├── markdown-preview.css │ └── navigation.css ├── test-html.html ├── types │ ├── index.ts │ ├── uuid.d.ts │ └── yaireo__tagify.d.ts └── utils │ ├── html-sanitizer.test.ts │ ├── html-sanitizer.ts │ └── test-sanitizer.js ├── start-dev.sh ├── start-standalone.js ├── tailwind.config.js ├── test-password.js ├── test.html ├── tsconfig.json ├── update-password.js ├── update-posts-add-cover.js ├── update-posts-schema.js └── vercel.json /.env: -------------------------------------------------------------------------------- 1 | # 认证相关 2 | NEXTAUTH_SECRET=joe-s 3 | NEXTAUTH_URL=http://localhost:3008 4 | 5 | # JWT密钥(用于管理员登录) 6 | JWT_SECRET=joe-k 7 | 8 | # 上传文件存储路径 9 | UPLOAD_DIR=public/uploads 10 | 11 | # 网站URL(用于生成绝对URL) 12 | NEXT_PUBLIC_SITE_URL=http://localhost:3008 -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # 认证相关 2 | NEXTAUTH_SECRET=your-nextauth-secret 3 | NEXTAUTH_URL=https://your-vercel-url.vercel.app 4 | 5 | # JWT密钥(用于管理员登录) 6 | JWT_SECRET=your-jwt-secret-key 7 | 8 | # 上传文件存储路径 9 | UPLOAD_DIR=public/uploads 10 | 11 | # 数据库连接 12 | DATABASE_URL="file:./demo.db" 13 | 14 | # 网站URL 15 | NEXT_PUBLIC_SITE_URL="https://blog.qiaomu.life" 16 | -------------------------------------------------------------------------------- /.github/workflows/deploy-to-bt.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to BT Panel 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | 13 | - name: Setup Node.js 14 | uses: actions/setup-node@v3 15 | with: 16 | node-version: '18' 17 | cache: 'npm' 18 | 19 | - name: Install dependencies 20 | run: npm ci 21 | 22 | - name: Build project 23 | run: npm run build 24 | 25 | - name: Deploy to BT Server 26 | uses: appleboy/ssh-action@master 27 | with: 28 | host: ${{ secrets.BT_HOST }} 29 | username: ${{ secrets.BT_USERNAME }} 30 | key: ${{ secrets.BT_SSH_KEY }} 31 | port: ${{ secrets.BT_PORT }} 32 | script: | 33 | # 进入网站目录 34 | cd /www/wwwroot/your-blog-directory 35 | 36 | # 备份当前版本 37 | if [ -d "current" ]; then 38 | mv current previous_$(date +%Y%m%d%H%M%S) 39 | fi 40 | 41 | # 创建新目录 42 | mkdir -p current 43 | 44 | # 拉取最新代码 45 | git clone --depth=1 https://github.com/joeseesun/qiaomu-blog3.git temp 46 | 47 | # 移动文件 48 | cp -R temp/. current/ 49 | rm -rf temp 50 | 51 | # 安装依赖并构建 52 | cd current 53 | npm ci 54 | npm run build 55 | 56 | # 使用PM2重启应用 57 | pm2 delete blog-app || true 58 | pm2 start npm --name "blog-app" -- start 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | /.pnp 4 | .pnp.js 5 | 6 | # testing 7 | /coverage 8 | 9 | # next.js 10 | /.next/ 11 | /out/ 12 | 13 | # production 14 | /build 15 | 16 | # misc 17 | .DS_Store 18 | *.pem 19 | 20 | # debug 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # local env files 26 | .env*.local 27 | 28 | # vercel 29 | .vercel 30 | 31 | # typescript 32 | *.tsbuildinfo 33 | next-env.d.ts 34 | 35 | # database 36 | !demo.db 37 | *.sqlite 38 | *.sqlite3 39 | 40 | # IDE 41 | .idea/ 42 | .vscode/ 43 | demo.db 44 | Send2qiaomu/ 45 | 46 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine 2 | 3 | WORKDIR /app 4 | 5 | # 复制package.json和package-lock.json 6 | COPY package*.json ./ 7 | 8 | # 安装依赖 9 | RUN npm ci 10 | 11 | # 复制源代码 12 | COPY . . 13 | 14 | # 设置环境变量 15 | ENV NODE_ENV=production 16 | ENV PORT=3000 17 | 18 | # 构建应用 19 | RUN npm run build 20 | 21 | # 暴露端口 22 | EXPOSE 3000 23 | 24 | # 启动命令 25 | CMD ["npm", "start"] 26 | -------------------------------------------------------------------------------- /IMPLEMENTATION_NOTES.md: -------------------------------------------------------------------------------- 1 | # HTML内容渲染实现记录 2 | 3 | ## 问题背景 4 | 5 | 在博客系统中,需要渲染HTML内容页面,同时添加一个可开关的底部浮动条,但不能影响原始HTML内容的显示和JavaScript的执行。 6 | 7 | ## 解决方案 8 | 9 | 通过API路由直接提供原始HTML内容,完全避免Next.js框架对HTML的处理,同时在HTML中注入JavaScript代码来创建可开关的浮动条。 10 | 11 | ### 核心实现 12 | 13 | 1. **iframe直接加载API提供的HTML内容** 14 | - 修改了页面渲染方式,使用iframe直接加载API路由提供的HTML内容 15 | - 完全绕过Next.js的渲染流程,避免添加任何框架代码 16 | 17 | 2. **API路由处理** 18 | - 创建了`/api/html-content`路由,直接提供原始HTML内容 19 | - 使用`sanitizeHtml`函数处理HTML内容,确保内容格式正确 20 | - 在HTML中注入JavaScript代码,创建浮动条 21 | - 设置正确的`Content-Type`头,确保浏览器正确解析HTML 22 | 23 | 3. **移动友好的浮动条** 24 | - 关闭按钮集成在浮动条内部,不再单独悬浮 25 | - 关闭后完全隐藏,不显示任何UI元素 26 | - 双击页面可以重新显示浮动条 27 | - 浮动条布局优化,适应移动设备屏幕 28 | 29 | ### 技术细节 30 | 31 | 1. **HTML内容处理** 32 | - 使用`sanitizeHtml`函数处理各种格式的HTML内容 33 | - 支持Markdown代码块、带前导说明文字的HTML片段、完整HTML文档等 34 | 35 | 2. **浮动条实现** 36 | - 使用JavaScript动态创建DOM元素 37 | - 添加事件监听器处理显示/隐藏 38 | - 使用localStorage保存用户偏好 39 | 40 | 3. **移动适配** 41 | - 针对不同屏幕尺寸优化布局 42 | - 增加触摸区域大小 43 | - 适配底部安全区域 44 | 45 | ## 文件修改 46 | 47 | 1. **src/app/posts/[slug]/page.tsx** 48 | - 修改HTML页面类型的渲染方式,使用iframe直接加载API提供的内容 49 | 50 | 2. **src/app/api/html-content/route.ts** 51 | - 新增API路由,处理HTML内容并注入浮动条代码 52 | 53 | 3. **src/utils/html-sanitizer.ts** 54 | - 实现HTML内容清理功能,处理各种格式的HTML输入 55 | 56 | 4. **src/components/HtmlPageLayout.tsx** 57 | - 移除原有的渲染逻辑,改为使用API路由 58 | 59 | ## 总结 60 | 61 | 这个实现方案完全保留了原始HTML内容的完整性,同时提供了一个移动友好的、可完全隐藏的浮动条。通过使用API路由和iframe,避免了Next.js框架对HTML内容的处理,确保JavaScript能够正常执行。 62 | -------------------------------------------------------------------------------- /NAMING_CONVENTIONS.md: -------------------------------------------------------------------------------- 1 | # 命名规范 2 | 3 | 为确保代码一致性和避免由命名不一致导致的问题,本项目采用以下命名规范。 4 | 5 | ## 1. 通用规则 6 | 7 | - **所有代码**必须使用**驼峰命名法**(camelCase) 8 | - 避免使用下划线命名法(snake_case) 9 | - 保持命名一致性,避免混用不同的命名风格 10 | 11 | ## 2. JavaScript/TypeScript 命名规范 12 | 13 | ### 变量和函数 14 | 15 | - 使用**小驼峰命名法**(camelCase) 16 | - 正确: `userName`, `fetchUserData`, `isLoading` 17 | - 错误: `user_name`, `fetch_user_data`, `is_loading` 18 | 19 | ### 类和组件 20 | 21 | - 使用**大驼峰命名法**(PascalCase) 22 | - 正确: `UserProfile`, `PostCard`, `AdminLayout` 23 | - 错误: `userProfile`, `post_card`, `admin_layout` 24 | 25 | ### 常量 26 | 27 | - 使用**大写字母和下划线**(仅此例外) 28 | - 正确: `MAX_RETRY_COUNT`, `API_BASE_URL` 29 | - 错误: `maxRetryCount`, `apiBaseUrl`(这些应该用于变量,而非常量) 30 | 31 | ## 3. 数据库相关命名 32 | 33 | ### 数据库字段 34 | 35 | - 在 schema 定义中使用**小驼峰命名法**(camelCase) 36 | - 正确: `createdAt`, `updatedAt`, `userId` 37 | - 错误: `created_at`, `updated_at`, `user_id` 38 | 39 | ### 数据库表 40 | 41 | - 使用**小驼峰命名法**的复数形式 42 | - 正确: `users`, `posts`, `postTags` 43 | - 错误: `user`, `post`, `post_tags` 44 | 45 | ## 4. API 相关命名 46 | 47 | ### API 路由 48 | 49 | - 使用**小写字母和连字符**(kebab-case) 50 | - 正确: `/api/user-profile`, `/api/blog-posts` 51 | - 错误: `/api/userProfile`, `/api/blogPosts` 52 | 53 | ### API 参数 54 | 55 | - 使用**小驼峰命名法**(camelCase) 56 | - 正确: `userId`, `postSlug`, `includeDeleted` 57 | - 错误: `user_id`, `post_slug`, `include_deleted` 58 | 59 | ## 5. 文件和目录命名 60 | 61 | ### 组件文件 62 | 63 | - 使用**大驼峰命名法**(PascalCase) 64 | - 正确: `UserProfile.tsx`, `PostCard.tsx` 65 | - 错误: `user-profile.tsx`, `post_card.tsx` 66 | 67 | ### 非组件文件 68 | 69 | - 使用**小驼峰命名法**(camelCase) 70 | - 正确: `utils.ts`, `apiClient.ts` 71 | - 错误: `Utils.ts`, `api-client.ts` 72 | 73 | ### 目录 74 | 75 | - 使用**小写字母和连字符**(kebab-case) 76 | - 正确: `user-profiles/`, `blog-posts/` 77 | - 错误: `UserProfiles/`, `blogPosts/` 78 | 79 | ## 6. CSS/SCSS 命名 80 | 81 | ### 类名 82 | 83 | - 使用**小写字母和连字符**(kebab-case) 84 | - 正确: `.user-profile`, `.post-card` 85 | - 错误: `.userProfile`, `.post_card` 86 | 87 | ## 7. 特别注意事项 88 | 89 | - **数据库字段与代码字段保持一致**:确保数据库中的字段名与代码中使用的字段名完全一致,避免因命名不一致导致的错误 90 | - **避免混用命名风格**:在同一个文件或组件中,保持命名风格的一致性 91 | - **遵循框架约定**:如果使用的框架有特定的命名约定,优先遵循框架的约定 92 | 93 | ## 8. 重构指南 94 | 95 | 当发现不符合命名规范的代码时: 96 | 97 | 1. 创建专门的重构分支 98 | 2. 系统性地更新命名 99 | 3. 确保所有相关引用都已更新 100 | 4. 全面测试功能 101 | 5. 提交代码审查 102 | 103 | 遵循这些命名规范将有助于提高代码可读性、减少错误,并使团队协作更加顺畅。 104 | -------------------------------------------------------------------------------- /backup/delete-user.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * 删除用户账号脚本 5 | * 6 | * 此脚本用于删除指定ID的用户账号 7 | */ 8 | 9 | const Database = require('better-sqlite3'); 10 | 11 | // 连接到数据库 12 | const db = new Database('./demo.db'); 13 | 14 | function deleteUser(userId) { 15 | try { 16 | console.log(`===== 删除用户账号 ID: ${userId} =====`); 17 | 18 | // 查找用户 19 | const user = db.prepare('SELECT id, email FROM users WHERE id = ?').get(userId); 20 | 21 | if (!user) { 22 | console.log(`❌ 未找到ID为 ${userId} 的用户账号`); 23 | return; 24 | } 25 | 26 | console.log(`找到用户: ID ${user.id}, 邮箱: ${user.email}`); 27 | 28 | // 删除用户 29 | const result = db.prepare('DELETE FROM users WHERE id = ?').run(userId); 30 | 31 | if (result.changes > 0) { 32 | console.log(`✅ 已成功删除用户账号: ${user.email} (ID: ${userId})`); 33 | } else { 34 | console.log(`❌ 删除用户账号失败`); 35 | } 36 | 37 | // 显示剩余用户账户 38 | const users = db.prepare('SELECT id, email FROM users').all(); 39 | console.log('\n当前系统中的用户账户:'); 40 | users.forEach(user => { 41 | console.log(`ID: ${user.id}, 邮箱: ${user.email}`); 42 | }); 43 | 44 | } catch (error) { 45 | console.error('错误:', error); 46 | } finally { 47 | // 关闭数据库连接 48 | db.close(); 49 | } 50 | } 51 | 52 | // 删除ID为3的用户 53 | deleteUser(3); 54 | -------------------------------------------------------------------------------- /backup/reset-admin-account.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * 重置管理员账户脚本 5 | * 6 | * 此脚本用于重置博客系统管理员账户,使其与README中的信息一致 7 | */ 8 | 9 | const Database = require('better-sqlite3'); 10 | const bcrypt = require('bcryptjs'); 11 | const path = require('path'); 12 | 13 | // 连接到数据库 14 | const db = new Database('./demo.db'); 15 | 16 | async function resetAdminAccount() { 17 | try { 18 | console.log('===== 重置管理员账户 ====='); 19 | 20 | // README中的管理员账户信息 21 | const email = 'admin@example.com'; 22 | const password = 'admin123'; 23 | 24 | // 生成密码哈希 25 | const salt = await bcrypt.genSalt(10); 26 | const hashedPassword = await bcrypt.hash(password, salt); 27 | 28 | // 检查是否已存在此邮箱的账户 29 | const existingUser = db.prepare('SELECT id FROM users WHERE email = ?').get(email); 30 | 31 | if (existingUser) { 32 | // 更新现有账户 33 | db.prepare('UPDATE users SET password = ? WHERE email = ?').run(hashedPassword, email); 34 | console.log(`✅ 已更新管理员账户 ${email} 的密码`); 35 | } else { 36 | // 查找demo@example.com账户 37 | const demoUser = db.prepare('SELECT id FROM users WHERE email = ?').get('demo@example.com'); 38 | 39 | if (demoUser) { 40 | // 将demo@example.com更新为admin@example.com 41 | db.prepare('UPDATE users SET email = ?, password = ? WHERE email = ?').run(email, hashedPassword, 'demo@example.com'); 42 | console.log(`✅ 已将demo@example.com账户更新为 ${email}`); 43 | } else { 44 | // 创建新账户 45 | db.prepare('INSERT INTO users (email, password, createdAt) VALUES (?, ?, datetime("now"))').run(email, hashedPassword); 46 | console.log(`✅ 已创建新管理员账户: ${email}`); 47 | } 48 | } 49 | 50 | // 显示所有用户账户 51 | const users = db.prepare('SELECT id, email FROM users').all(); 52 | console.log('\n当前系统中的用户账户:'); 53 | users.forEach(user => { 54 | console.log(`ID: ${user.id}, 邮箱: ${user.email}`); 55 | }); 56 | 57 | console.log('\n管理员账户已重置,现在可以使用以下信息登录:'); 58 | console.log(`用户名: ${email}`); 59 | console.log(`密码: ${password}`); 60 | 61 | } catch (error) { 62 | console.error('错误:', error); 63 | } finally { 64 | // 关闭数据库连接 65 | db.close(); 66 | } 67 | } 68 | 69 | // 运行函数 70 | resetAdminAccount(); 71 | -------------------------------------------------------------------------------- /blog.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/blog.db -------------------------------------------------------------------------------- /copy-static-assets.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 复制静态资源到 Standalone 目录的脚本 4 | # 使用方法: ./copy-static-assets.sh 5 | 6 | echo "开始复制静态资源到 Standalone 目录..." 7 | 8 | # 确保 Standalone 目录存在 9 | mkdir -p .next/standalone/.next/static 10 | cp -R .next/static .next/standalone/.next/ 11 | 12 | # 复制所有静态资源 13 | echo "复制 public 目录中的所有文件..." 14 | mkdir -p .next/standalone/public 15 | cp -R public/* .next/standalone/public/ 2>/dev/null || : 16 | 17 | # 特别确保 favicon 和其他重要图标文件被复制 18 | echo "确保图标文件存在..." 19 | if [ -f public/favicon.ico ]; then 20 | cp public/favicon.ico .next/standalone/public/ 21 | echo "已复制 favicon.ico" 22 | fi 23 | if [ -f public/icon.png ]; then 24 | cp public/icon.png .next/standalone/public/ 25 | echo "已复制 icon.png" 26 | fi 27 | if [ -f public/apple-touch-icon.png ]; then 28 | cp public/apple-touch-icon.png .next/standalone/public/ 29 | echo "已复制 apple-touch-icon.png" 30 | fi 31 | 32 | echo "静态资源复制完成!" 33 | -------------------------------------------------------------------------------- /deploy-standalone.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 部署脚本 - Standalone 模式 4 | # 使用方法: ./deploy-standalone.sh 5 | 6 | echo "开始部署博客 (Standalone 模式)..." 7 | 8 | # 拉取最新代码 9 | echo "拉取最新代码..." 10 | git pull origin main 11 | 12 | # 安装依赖 13 | echo "安装依赖..." 14 | npm install 15 | 16 | # 构建应用 17 | echo "构建应用..." 18 | npm run build 19 | 20 | # 准备 standalone 目录 21 | echo "准备 standalone 目录..." 22 | mkdir -p .next/standalone/.next/static 23 | cp -R .next/static .next/standalone/.next/ 24 | 25 | # 确保上传目录存在 26 | echo "确保上传目录存在..." 27 | mkdir -p .next/standalone/public/uploads 28 | cp -R public/uploads .next/standalone/public/ 2>/dev/null || : 29 | 30 | # 复制环境变量文件(如果存在) 31 | if [ -f .env.production ]; then 32 | echo "复制环境变量文件..." 33 | cp .env.production .next/standalone/ 34 | fi 35 | 36 | # 使用 PM2 启动或重启服务 37 | echo "启动服务..." 38 | cd .next/standalone 39 | if pm2 list | grep -q "qiaomu-blog"; then 40 | pm2 restart qiaomu-blog 41 | else 42 | pm2 start server.js --name qiaomu-blog 43 | fi 44 | 45 | echo "部署完成!" 46 | echo "您的博客现在应该可以通过配置的端口访问了" 47 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | qiaomu-blog: 5 | build: . 6 | ports: 7 | - "3000:3000" 8 | volumes: 9 | - ./demo.db:/app/demo.db 10 | - ./public/uploads:/app/public/uploads 11 | restart: unless-stopped 12 | environment: 13 | - NODE_ENV=production 14 | - PORT=3000 15 | -------------------------------------------------------------------------------- /ecosystem.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | apps: [ 3 | { 4 | name: 'qiaomu-blog', 5 | script: 'npm', 6 | args: 'start', 7 | cwd: './', 8 | instances: 1, 9 | autorestart: true, 10 | watch: false, 11 | max_memory_restart: '500M', 12 | env: { 13 | NODE_ENV: 'production', 14 | PORT: 3009 15 | }, 16 | merge_logs: true, 17 | log_date_format: "YYYY-MM-DD HH:mm:ss Z", 18 | error_file: "./logs/error.log", 19 | out_file: "./logs/output.log" 20 | } 21 | ] 22 | }; 23 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { dirname } from "path"; 2 | import { fileURLToPath } from "url"; 3 | import { FlatCompat } from "@eslint/eslintrc"; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname, 10 | }); 11 | 12 | const eslintConfig = [ 13 | ...compat.extends("next/core-web-vitals", "next/typescript"), 14 | ]; 15 | 16 | export default eslintConfig; 17 | -------------------------------------------------------------------------------- /links.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/links.db -------------------------------------------------------------------------------- /logs/.gitkeep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /migrations/add_page_type_to_posts.js: -------------------------------------------------------------------------------- 1 | // 添加 pageType 字段到 posts 表 2 | export async function up(db) { 3 | // 检查 pageType 列是否已存在 4 | const tableInfo = await db.all("PRAGMA table_info(posts)"); 5 | const pageTypeExists = tableInfo.some(column => column.name === 'pageType'); 6 | 7 | if (!pageTypeExists) { 8 | // 添加 pageType 列,默认值为 'markdown' 9 | await db.run("ALTER TABLE posts ADD COLUMN pageType TEXT NOT NULL DEFAULT 'markdown'"); 10 | console.log('Added pageType column to posts table'); 11 | } else { 12 | console.log('pageType column already exists in posts table'); 13 | } 14 | } 15 | 16 | export async function down(db) { 17 | // SQLite 不支持直接删除列,所以这里我们不实现回滚操作 18 | console.log('SQLite does not support dropping columns directly'); 19 | } 20 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | // 禁用ESLint检查,解决Vercel部署问题 5 | eslint: { 6 | ignoreDuringBuilds: true, 7 | }, 8 | // 添加对 @uiw/react-md-editor 的完整支持 9 | webpack: (config, { isServer }) => { 10 | // 处理 @uiw/react-md-editor 的兼容性问题 11 | if (!isServer) { 12 | // 客户端构建时的特殊处理 13 | config.resolve.fallback = { 14 | ...config.resolve.fallback, 15 | fs: false, 16 | path: false, 17 | os: false, 18 | }; 19 | } 20 | 21 | // 添加必要的解析器 22 | config.module.rules.push({ 23 | test: /\.m?js$/, 24 | resolve: { 25 | fullySpecified: false, 26 | }, 27 | }); 28 | 29 | return config; 30 | }, 31 | // 确保上传的图片在生产环境中能够正确显示 32 | images: { 33 | domains: ['localhost', 'blog.qiaomu.life'], 34 | remotePatterns: [ 35 | { 36 | protocol: 'http', 37 | hostname: 'localhost', 38 | port: '3000', 39 | pathname: '/uploads/**', 40 | }, 41 | { 42 | protocol: 'https', 43 | hostname: '**', 44 | pathname: '/uploads/**', 45 | }, 46 | ], 47 | // 禁用图片优化,直接使用原始图片 48 | unoptimized: true 49 | }, 50 | // 确保静态文件正确服务 51 | output: 'standalone', // 启用独立输出模式,便于跨平台部署 52 | // 外部包配置,用于静态资源处理 53 | serverExternalPackages: ['sharp'], 54 | // 实验性功能 55 | experimental: { 56 | // 禁用 App Router 参数类型检查,解决 Next.js 15 中的类型问题 57 | typedRoutes: false 58 | }, 59 | async headers() { 60 | return [ 61 | { 62 | source: '/:path*', 63 | headers: [ 64 | { 65 | key: 'Permissions-Policy', 66 | value: 'camera=(), microphone=(), geolocation=(), interest-cohort=()' 67 | }, 68 | { 69 | key: 'X-Content-Type-Options', 70 | value: 'nosniff' 71 | }, 72 | { 73 | key: 'X-Frame-Options', 74 | value: 'SAMEORIGIN' 75 | }, 76 | { 77 | key: 'X-XSS-Protection', 78 | value: '1; mode=block' 79 | }, 80 | { 81 | key: 'Cache-Control', 82 | value: 'no-store, must-revalidate' 83 | } 84 | ] 85 | } 86 | ]; 87 | } 88 | }; 89 | 90 | module.exports = nextConfig; 91 | -------------------------------------------------------------------------------- /next.config.js.bak: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | // 添加对 @uiw/react-md-editor 的完整支持 5 | webpack: (config, { isServer }) => { 6 | // 处理 @uiw/react-md-editor 的兼容性问题 7 | if (!isServer) { 8 | // 客户端构建时的特殊处理 9 | config.resolve.fallback = { 10 | ...config.resolve.fallback, 11 | fs: false, 12 | path: false, 13 | os: false, 14 | }; 15 | } 16 | 17 | // 添加必要的解析器 18 | config.module.rules.push({ 19 | test: /\.m?js$/, 20 | resolve: { 21 | fullySpecified: false, 22 | }, 23 | }); 24 | 25 | return config; 26 | }, 27 | async headers() { 28 | return [ 29 | { 30 | source: '/:path*', 31 | headers: [ 32 | { 33 | key: 'Permissions-Policy', 34 | value: 'camera=(), microphone=(), geolocation=(), interest-cohort=()' 35 | }, 36 | { 37 | key: 'X-Content-Type-Options', 38 | value: 'nosniff' 39 | }, 40 | { 41 | key: 'X-Frame-Options', 42 | value: 'SAMEORIGIN' 43 | }, 44 | { 45 | key: 'X-XSS-Protection', 46 | value: '1; mode=block' 47 | } 48 | ] 49 | } 50 | ]; 51 | } 52 | }; 53 | 54 | module.exports = nextConfig; 55 | -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from 'next'; 2 | 3 | const nextConfig: NextConfig = { 4 | webpack: (config, { isServer }) => { 5 | if (!isServer) { 6 | // 客户端打包配置 7 | config.resolve.fallback = { 8 | ...config.resolve.fallback, 9 | fs: false, 10 | path: false, 11 | os: false, 12 | stream: require.resolve('stream-browserify'), 13 | buffer: require.resolve('buffer'), 14 | crypto: require.resolve('crypto-browserify'), 15 | }; 16 | 17 | // 处理node:协议的polyfill 18 | config.plugins.push(new (require('webpack').NormalModuleReplacementPlugin)( 19 | /^node:/, 20 | (resource: { request: string }) => { 21 | resource.request = resource.request.replace(/^node:/, ''); 22 | } 23 | )); 24 | } 25 | return config; 26 | }, 27 | }; 28 | 29 | export default nextConfig; 30 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: ["@tailwindcss/postcss"], 3 | }; 4 | 5 | export default config; 6 | -------------------------------------------------------------------------------- /public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/icon/android/play_store_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/icon/android/play_store_512.png -------------------------------------------------------------------------------- /public/icon/android/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /public/icon/android/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/icon/android/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /public/icon/android/res/mipmap-hdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/icon/android/res/mipmap-hdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /public/icon/android/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/icon/android/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /public/icon/android/res/mipmap-hdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/icon/android/res/mipmap-hdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /public/icon/android/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/icon/android/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /public/icon/android/res/mipmap-mdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/icon/android/res/mipmap-mdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /public/icon/android/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/icon/android/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /public/icon/android/res/mipmap-mdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/icon/android/res/mipmap-mdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /public/icon/android/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/icon/android/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /public/icon/android/res/mipmap-xhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/icon/android/res/mipmap-xhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /public/icon/android/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/icon/android/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /public/icon/android/res/mipmap-xhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/icon/android/res/mipmap-xhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /public/icon/android/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/icon/android/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /public/icon/android/res/mipmap-xxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/icon/android/res/mipmap-xxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /public/icon/android/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/icon/android/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /public/icon/android/res/mipmap-xxhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/icon/android/res/mipmap-xxhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /public/icon/android/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/icon/android/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /public/icon/android/res/mipmap-xxxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/icon/android/res/mipmap-xxxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /public/icon/android/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/icon/android/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /public/icon/android/res/mipmap-xxxhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/icon/android/res/mipmap-xxxhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /public/icon/ios/AppIcon-20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/icon/ios/AppIcon-20@2x.png -------------------------------------------------------------------------------- /public/icon/ios/AppIcon-20@2x~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/icon/ios/AppIcon-20@2x~ipad.png -------------------------------------------------------------------------------- /public/icon/ios/AppIcon-20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/icon/ios/AppIcon-20@3x.png -------------------------------------------------------------------------------- /public/icon/ios/AppIcon-20~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/icon/ios/AppIcon-20~ipad.png -------------------------------------------------------------------------------- /public/icon/ios/AppIcon-29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/icon/ios/AppIcon-29.png -------------------------------------------------------------------------------- /public/icon/ios/AppIcon-29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/icon/ios/AppIcon-29@2x.png -------------------------------------------------------------------------------- /public/icon/ios/AppIcon-29@2x~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/icon/ios/AppIcon-29@2x~ipad.png -------------------------------------------------------------------------------- /public/icon/ios/AppIcon-29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/icon/ios/AppIcon-29@3x.png -------------------------------------------------------------------------------- /public/icon/ios/AppIcon-29~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/icon/ios/AppIcon-29~ipad.png -------------------------------------------------------------------------------- /public/icon/ios/AppIcon-40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/icon/ios/AppIcon-40@2x.png -------------------------------------------------------------------------------- /public/icon/ios/AppIcon-40@2x~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/icon/ios/AppIcon-40@2x~ipad.png -------------------------------------------------------------------------------- /public/icon/ios/AppIcon-40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/icon/ios/AppIcon-40@3x.png -------------------------------------------------------------------------------- /public/icon/ios/AppIcon-40~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/icon/ios/AppIcon-40~ipad.png -------------------------------------------------------------------------------- /public/icon/ios/AppIcon-60@2x~car.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/icon/ios/AppIcon-60@2x~car.png -------------------------------------------------------------------------------- /public/icon/ios/AppIcon-60@3x~car.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/icon/ios/AppIcon-60@3x~car.png -------------------------------------------------------------------------------- /public/icon/ios/AppIcon-83.5@2x~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/icon/ios/AppIcon-83.5@2x~ipad.png -------------------------------------------------------------------------------- /public/icon/ios/AppIcon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/icon/ios/AppIcon@2x.png -------------------------------------------------------------------------------- /public/icon/ios/AppIcon@2x~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/icon/ios/AppIcon@2x~ipad.png -------------------------------------------------------------------------------- /public/icon/ios/AppIcon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/icon/ios/AppIcon@3x.png -------------------------------------------------------------------------------- /public/icon/ios/AppIcon~ios-marketing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/icon/ios/AppIcon~ios-marketing.png -------------------------------------------------------------------------------- /public/icon/ios/AppIcon~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/icon/ios/AppIcon~ipad.png -------------------------------------------------------------------------------- /public/icon/web/README.txt: -------------------------------------------------------------------------------- 1 | Add this to your HTML : 2 | 3 | 4 | 5 | 6 | Add this to your app's manifest.json: 7 | 8 | ... 9 | { 10 | "icons": [ 11 | { "src": "/favicon.ico", "type": "image/x-icon", "sizes": "16x16 32x32" }, 12 | { "src": "/icon-192.png", "type": "image/png", "sizes": "192x192" }, 13 | { "src": "/icon-512.png", "type": "image/png", "sizes": "512x512" }, 14 | { "src": "/icon-192-maskable.png", "type": "image/png", "sizes": "192x192", "purpose": "maskable" }, 15 | { "src": "/icon-512-maskable.png", "type": "image/png", "sizes": "512x512", "purpose": "maskable" } 16 | ] 17 | } 18 | ... 19 | -------------------------------------------------------------------------------- /public/icon/web/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/icon/web/apple-touch-icon.png -------------------------------------------------------------------------------- /public/icon/web/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/icon/web/favicon.ico -------------------------------------------------------------------------------- /public/icon/web/icon-192-maskable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/icon/web/icon-192-maskable.png -------------------------------------------------------------------------------- /public/icon/web/icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/icon/web/icon-192.png -------------------------------------------------------------------------------- /public/icon/web/icon-512-maskable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/icon/web/icon-512-maskable.png -------------------------------------------------------------------------------- /public/icon/web/icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/icon/web/icon-512.png -------------------------------------------------------------------------------- /public/icons/github.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/icons/twitter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/icons/wechat.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/images/default-qrcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/images/default-qrcode.png -------------------------------------------------------------------------------- /public/images/default-thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/images/default-thumbnail.png -------------------------------------------------------------------------------- /public/js/mobile-menu.js: -------------------------------------------------------------------------------- 1 | // 移动端菜单脚本 2 | document.addEventListener('DOMContentLoaded', function() { 3 | const mobileMenuToggle = document.querySelector('.mobile-menu-toggle'); 4 | const mobileMenu = document.querySelector('.mobile-menu'); 5 | 6 | if (mobileMenuToggle && mobileMenu) { 7 | mobileMenuToggle.addEventListener('click', () => { 8 | mobileMenu.classList.toggle('active'); 9 | 10 | // 切换菜单图标 11 | const menuIcon = mobileMenuToggle.querySelector('svg'); 12 | if (menuIcon) { 13 | if (mobileMenu.classList.contains('active')) { 14 | menuIcon.innerHTML = ''; 15 | } else { 16 | menuIcon.innerHTML = ''; 17 | } 18 | } 19 | }); 20 | } 21 | 22 | // 移动端下拉菜单 23 | const dropdownToggles = document.querySelectorAll('.mobile-dropdown-toggle'); 24 | dropdownToggles.forEach(toggle => { 25 | toggle.addEventListener('click', () => { 26 | const dropdown = toggle.nextElementSibling; 27 | if (dropdown) { 28 | dropdown.classList.toggle('active'); 29 | 30 | // 切换箭头方向 31 | const icon = toggle.querySelector('.mobile-dropdown-icon'); 32 | if (icon) { 33 | if (dropdown.classList.contains('active')) { 34 | icon.classList.add('rotate-180'); 35 | } else { 36 | icon.classList.remove('rotate-180'); 37 | } 38 | } 39 | } 40 | }); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /public/js/theme.js: -------------------------------------------------------------------------------- 1 | // 主题切换脚本 2 | document.addEventListener('DOMContentLoaded', function() { 3 | const html = document.documentElement; 4 | const themeToggle = document.getElementById('themeToggle'); 5 | const mobileThemeToggle = document.getElementById('mobileThemeToggle'); 6 | 7 | // 使用 data-theme-icon 属性来选择图标,避免水合不匹配 8 | const lightIcon = document.querySelector('[data-theme-icon="light"].theme-light'); 9 | const darkIcon = document.querySelector('[data-theme-icon="dark"].theme-dark'); 10 | const mobileLightIcon = document.querySelector('[data-theme-icon="light"].mobile-theme-light'); 11 | const mobileDarkIcon = document.querySelector('[data-theme-icon="dark"].mobile-theme-dark'); 12 | 13 | // 检查系统偏好 14 | const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; 15 | 16 | // 检查本地存储 17 | const savedTheme = localStorage.getItem('theme'); 18 | 19 | // 初始化主题 20 | function initTheme() { 21 | const isDark = savedTheme === 'dark' || (!savedTheme && prefersDark); 22 | 23 | if (isDark) { 24 | html.classList.remove('light'); 25 | html.classList.add('dark'); 26 | toggleIcons(true); 27 | } else { 28 | html.classList.remove('dark'); 29 | html.classList.add('light'); 30 | toggleIcons(false); 31 | } 32 | } 33 | 34 | // 切换图标显示 35 | function toggleIcons(isDark) { 36 | if (lightIcon && darkIcon) { 37 | lightIcon.classList.toggle('hidden', isDark); 38 | darkIcon.classList.toggle('hidden', !isDark); 39 | } 40 | if (mobileLightIcon && mobileDarkIcon) { 41 | mobileLightIcon.classList.toggle('hidden', isDark); 42 | mobileDarkIcon.classList.toggle('hidden', !isDark); 43 | } 44 | } 45 | 46 | // 初始化主题 47 | initTheme(); 48 | 49 | function toggleTheme() { 50 | const isDark = html.classList.contains('dark'); 51 | 52 | if (isDark) { 53 | // 切换到浅色主题 54 | html.classList.remove('dark'); 55 | html.classList.add('light'); 56 | toggleIcons(false); 57 | localStorage.setItem('theme', 'light'); 58 | } else { 59 | // 切换到深色主题 60 | html.classList.remove('light'); 61 | html.classList.add('dark'); 62 | toggleIcons(true); 63 | localStorage.setItem('theme', 'dark'); 64 | } 65 | } 66 | 67 | if (themeToggle) { 68 | themeToggle.addEventListener('click', toggleTheme); 69 | } 70 | 71 | if (mobileThemeToggle) { 72 | mobileThemeToggle.addEventListener('click', toggleTheme); 73 | } 74 | }); 75 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/placeholder-qr.png: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /public/placeholder-qr.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 占位二维码 44 | 45 | -------------------------------------------------------------------------------- /public/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 测试页面 7 | 8 | 18 | 19 | 20 |
21 |

测试页面

22 |

这是一个简单的测试页面,用于检查是否有遮罩层问题。

23 |
24 |

如果您能看到这段文字,说明页面正常显示,没有遮罩层问题。

25 |
26 |
27 | 28 | 29 | -------------------------------------------------------------------------------- /public/uploads/0bad1549-227b-462b-b500-5c40cf346f29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/uploads/0bad1549-227b-462b-b500-5c40cf346f29.png -------------------------------------------------------------------------------- /public/uploads/1b65d6d3-6ef8-4fbe-a242-afe60c2136b0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/uploads/1b65d6d3-6ef8-4fbe-a242-afe60c2136b0.png -------------------------------------------------------------------------------- /public/uploads/20ddc0c2-a981-404c-bd25-5acc8b5d6e54.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/uploads/20ddc0c2-a981-404c-bd25-5acc8b5d6e54.png -------------------------------------------------------------------------------- /public/uploads/304bb973-5262-4074-99f2-ce2cce282dda.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/uploads/304bb973-5262-4074-99f2-ce2cce282dda.png -------------------------------------------------------------------------------- /public/uploads/33611009-b45e-4f5e-85cc-ab75da2541a9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/uploads/33611009-b45e-4f5e-85cc-ab75da2541a9.png -------------------------------------------------------------------------------- /public/uploads/373a5d7f-4a9c-4f79-b4bf-144485bc60e1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/uploads/373a5d7f-4a9c-4f79-b4bf-144485bc60e1.png -------------------------------------------------------------------------------- /public/uploads/3971a0c0-2c9b-4cec-85fd-c613742a0a7d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/uploads/3971a0c0-2c9b-4cec-85fd-c613742a0a7d.png -------------------------------------------------------------------------------- /public/uploads/51a94123-3c24-431d-9a08-bc88a3be0d11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/uploads/51a94123-3c24-431d-9a08-bc88a3be0d11.png -------------------------------------------------------------------------------- /public/uploads/51e92ef1-7c54-40a3-82a5-5e4d7ae41f47.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/uploads/51e92ef1-7c54-40a3-82a5-5e4d7ae41f47.png -------------------------------------------------------------------------------- /public/uploads/5a3c2bd2-e0fc-4890-a902-64039547cf2d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/uploads/5a3c2bd2-e0fc-4890-a902-64039547cf2d.png -------------------------------------------------------------------------------- /public/uploads/5aa0c80f-b23d-4081-af5d-55c4b9f8e7c3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/uploads/5aa0c80f-b23d-4081-af5d-55c4b9f8e7c3.png -------------------------------------------------------------------------------- /public/uploads/5b13b0c7-8363-4d2d-a156-c37c46287df1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/uploads/5b13b0c7-8363-4d2d-a156-c37c46287df1.png -------------------------------------------------------------------------------- /public/uploads/5bcec724-4c3b-4545-9c95-f183698296d4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/uploads/5bcec724-4c3b-4545-9c95-f183698296d4.png -------------------------------------------------------------------------------- /public/uploads/6321e5b7-5866-44d8-98b6-8eb4e8531407.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/uploads/6321e5b7-5866-44d8-98b6-8eb4e8531407.png -------------------------------------------------------------------------------- /public/uploads/6413a9bf-35ee-4fba-b6bd-f260d5543f95.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/uploads/6413a9bf-35ee-4fba-b6bd-f260d5543f95.png -------------------------------------------------------------------------------- /public/uploads/64ee3e69-f676-4ccb-b353-28f64095a362.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/uploads/64ee3e69-f676-4ccb-b353-28f64095a362.png -------------------------------------------------------------------------------- /public/uploads/684f0a84-4d8b-4d57-bb71-b7451c7504d2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/uploads/684f0a84-4d8b-4d57-bb71-b7451c7504d2.png -------------------------------------------------------------------------------- /public/uploads/686bee4c-67db-4f63-a623-8a291f06f30e.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/uploads/686bee4c-67db-4f63-a623-8a291f06f30e.png -------------------------------------------------------------------------------- /public/uploads/6b1b3100-d856-4ffb-a033-280356beaf3d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/uploads/6b1b3100-d856-4ffb-a033-280356beaf3d.png -------------------------------------------------------------------------------- /public/uploads/6bb144cc-a10a-44b0-acbb-a56f10dcd16a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/uploads/6bb144cc-a10a-44b0-acbb-a56f10dcd16a.png -------------------------------------------------------------------------------- /public/uploads/70d744cd-85df-4cbd-95d7-3efb3ba6b983.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/uploads/70d744cd-85df-4cbd-95d7-3efb3ba6b983.png -------------------------------------------------------------------------------- /public/uploads/7d0f6464-d3ad-480f-8963-25e9f442da42.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/uploads/7d0f6464-d3ad-480f-8963-25e9f442da42.png -------------------------------------------------------------------------------- /public/uploads/7e849ea9-76b5-4481-8af4-57dd0c1606f7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/uploads/7e849ea9-76b5-4481-8af4-57dd0c1606f7.png -------------------------------------------------------------------------------- /public/uploads/8147285b-d577-43c0-a161-bfdcbb08e1e1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/uploads/8147285b-d577-43c0-a161-bfdcbb08e1e1.png -------------------------------------------------------------------------------- /public/uploads/875f2cbc-c88f-4b29-adf6-734530ccbc0e.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/uploads/875f2cbc-c88f-4b29-adf6-734530ccbc0e.png -------------------------------------------------------------------------------- /public/uploads/87b63bc7-96e2-4982-a47d-c6ea65b8efc3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/uploads/87b63bc7-96e2-4982-a47d-c6ea65b8efc3.png -------------------------------------------------------------------------------- /public/uploads/90733c3c-6d6d-4894-8c37-90dc8cf0a816.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/uploads/90733c3c-6d6d-4894-8c37-90dc8cf0a816.png -------------------------------------------------------------------------------- /public/uploads/9161c3b8-6daa-4e51-a3f1-f71d597e326d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/uploads/9161c3b8-6daa-4e51-a3f1-f71d597e326d.png -------------------------------------------------------------------------------- /public/uploads/94deca3e-6e7d-4efd-8ebd-3755bb723ed0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/uploads/94deca3e-6e7d-4efd-8ebd-3755bb723ed0.png -------------------------------------------------------------------------------- /public/uploads/97d41ef4-cc7b-4d20-b1a5-47546288c2a7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/uploads/97d41ef4-cc7b-4d20-b1a5-47546288c2a7.png -------------------------------------------------------------------------------- /public/uploads/999bf369-8477-4add-8384-4c4abda1df6a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/uploads/999bf369-8477-4add-8384-4c4abda1df6a.png -------------------------------------------------------------------------------- /public/uploads/9bb06850-815d-4834-8a3e-474c9e1d1e6b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/uploads/9bb06850-815d-4834-8a3e-474c9e1d1e6b.png -------------------------------------------------------------------------------- /public/uploads/a80ff25a-4a5b-45c2-97b6-2c2a8d31894d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/uploads/a80ff25a-4a5b-45c2-97b6-2c2a8d31894d.png -------------------------------------------------------------------------------- /public/uploads/aab9ba5b-3dd9-4f61-9130-00582184beca.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/uploads/aab9ba5b-3dd9-4f61-9130-00582184beca.png -------------------------------------------------------------------------------- /public/uploads/abb91af5-0edd-4459-a37f-a30cc2e616b0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/uploads/abb91af5-0edd-4459-a37f-a30cc2e616b0.png -------------------------------------------------------------------------------- /public/uploads/abf2ae63-7c74-465e-958c-7b39bd43478f.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/uploads/abf2ae63-7c74-465e-958c-7b39bd43478f.png -------------------------------------------------------------------------------- /public/uploads/b6a65458-8256-402b-9a6b-67c203229de3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/uploads/b6a65458-8256-402b-9a6b-67c203229de3.png -------------------------------------------------------------------------------- /public/uploads/b7772814-5698-4902-9d31-6fd10453b673.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/uploads/b7772814-5698-4902-9d31-6fd10453b673.png -------------------------------------------------------------------------------- /public/uploads/bc982ea5-1909-486d-a7d2-ae591744126c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/uploads/bc982ea5-1909-486d-a7d2-ae591744126c.png -------------------------------------------------------------------------------- /public/uploads/cc3a296e-5e6f-40e9-820d-5d49add696d0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/uploads/cc3a296e-5e6f-40e9-820d-5d49add696d0.png -------------------------------------------------------------------------------- /public/uploads/cc978f5b-c4a6-4f3c-9242-a2e8b790b9a1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/uploads/cc978f5b-c4a6-4f3c-9242-a2e8b790b9a1.png -------------------------------------------------------------------------------- /public/uploads/contact/05547404-c0cc-4229-bb42-ed4eb28de871.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/uploads/contact/05547404-c0cc-4229-bb42-ed4eb28de871.jpeg -------------------------------------------------------------------------------- /public/uploads/contact/3e5464e1-f6cd-4600-b9dc-6f9552547d29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/uploads/contact/3e5464e1-f6cd-4600-b9dc-6f9552547d29.png -------------------------------------------------------------------------------- /public/uploads/contact/ca46cdc3-6463-4794-a27b-174aa373f15e.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/uploads/contact/ca46cdc3-6463-4794-a27b-174aa373f15e.jpeg -------------------------------------------------------------------------------- /public/uploads/dc32267f-895c-49f9-a03e-4bb861abeead.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/uploads/dc32267f-895c-49f9-a03e-4bb861abeead.png -------------------------------------------------------------------------------- /public/uploads/donation/63a278f4-8927-4123-b95c-4dffb5ea7430.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/uploads/donation/63a278f4-8927-4123-b95c-4dffb5ea7430.png -------------------------------------------------------------------------------- /public/uploads/e3f66a31-9000-4cf0-a1cb-44939aa69117.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/uploads/e3f66a31-9000-4cf0-a1cb-44939aa69117.png -------------------------------------------------------------------------------- /public/uploads/e8a76490-4fdb-411c-a996-01c3d896e7c0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/uploads/e8a76490-4fdb-411c-a996-01c3d896e7c0.png -------------------------------------------------------------------------------- /public/uploads/f0a78892-ca47-4753-a5ff-369674b5e19c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/uploads/f0a78892-ca47-4753-a5ff-369674b5e19c.png -------------------------------------------------------------------------------- /public/uploads/f69badfa-94f4-4509-a70c-7338511697a4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/uploads/f69badfa-94f4-4509-a70c-7338511697a4.png -------------------------------------------------------------------------------- /public/uploads/f8eec96f-5d65-46b5-9648-b729eb4c54d9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/uploads/f8eec96f-5d65-46b5-9648-b729eb4c54d9.png -------------------------------------------------------------------------------- /public/uploads/f985396d-8d7a-4d65-8501-8a4bcad61929.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/uploads/f985396d-8d7a-4d65-8501-8a4bcad61929.png -------------------------------------------------------------------------------- /public/uploads/fa48845d-46e4-48e9-a53c-2a41d7488a34.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/public/uploads/fa48845d-46e4-48e9-a53c-2a41d7488a34.png -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /rebuild.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 重建监控脚本 4 | # 监控.rebuild目录中的触发文件,并在检测到新的触发文件时执行重建 5 | 6 | TRIGGER_DIR=".rebuild" 7 | TRIGGER_FILE="$TRIGGER_DIR/trigger.txt" 8 | LOG_FILE="$TRIGGER_DIR/rebuild.log" 9 | 10 | # 确保目录存在 11 | mkdir -p "$TRIGGER_DIR" 12 | 13 | # 日志函数 14 | log() { 15 | echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >> "$LOG_FILE" 16 | echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" 17 | } 18 | 19 | log "启动重建监控脚本" 20 | 21 | # 检查触发文件是否存在 22 | if [ -f "$TRIGGER_FILE" ]; then 23 | # 读取触发文件内容 24 | TRIGGER_CONTENT=$(cat "$TRIGGER_FILE") 25 | log "检测到触发文件: $TRIGGER_CONTENT" 26 | 27 | # 执行重建 28 | log "开始执行重建..." 29 | npm run build >> "$LOG_FILE" 2>&1 30 | BUILD_RESULT=$? 31 | 32 | if [ $BUILD_RESULT -eq 0 ]; then 33 | log "重建成功完成" 34 | # 更新触发文件状态 35 | echo "{\"timestamp\":\"$(date -u +"%Y-%m-%dT%H:%M:%SZ")\",\"status\":\"completed\",\"result\":\"success\"}" > "$TRIGGER_FILE" 36 | else 37 | log "重建失败,错误代码: $BUILD_RESULT" 38 | # 更新触发文件状态 39 | echo "{\"timestamp\":\"$(date -u +"%Y-%m-%dT%H:%M:%SZ")\",\"status\":\"failed\",\"result\":\"error\",\"code\":$BUILD_RESULT}" > "$TRIGGER_FILE" 40 | fi 41 | else 42 | log "未检测到触发文件,无需重建" 43 | fi 44 | 45 | log "监控脚本执行完毕" 46 | -------------------------------------------------------------------------------- /reset-categories.js: -------------------------------------------------------------------------------- 1 | // 重置分类数据的脚本 2 | const path = require('path'); 3 | 4 | // 直接使用 SQLite 进行操作 5 | const Database = require('better-sqlite3'); 6 | const dbPath = path.resolve(process.cwd(), './blog.db'); 7 | const sqlite = new Database(dbPath); 8 | 9 | function resetCategories() { 10 | try { 11 | console.log('开始重置分类数据...'); 12 | 13 | // 检查是否有文章使用分类 14 | const stmt = sqlite.prepare('SELECT COUNT(*) as count FROM posts WHERE categoryId IS NOT NULL'); 15 | const result = stmt.get(); 16 | 17 | if (result.count > 0) { 18 | console.log(`有 ${result.count} 篇文章使用了分类,将它们的分类设为 null`); 19 | sqlite.exec('UPDATE posts SET categoryId = NULL WHERE categoryId IS NOT NULL'); 20 | } 21 | 22 | // 删除所有分类 23 | console.log('删除所有分类数据...'); 24 | sqlite.exec('DELETE FROM categories'); 25 | 26 | // 重置自增 ID 27 | sqlite.exec("DELETE FROM sqlite_sequence WHERE name = 'categories'"); 28 | 29 | // 添加示例分类 30 | console.log('添加示例分类...'); 31 | sqlite.exec(` 32 | INSERT INTO categories (name, slug, description, parent_id, "order", created_at) VALUES 33 | ('未分类', 'uncategorized', '默认分类', NULL, 0, CURRENT_TIMESTAMP), 34 | ('技术', 'technology', '技术相关文章', NULL, 10, CURRENT_TIMESTAMP), 35 | ('生活', 'life', '生活相关文章', NULL, 20, CURRENT_TIMESTAMP), 36 | ('前端开发', 'frontend', '前端开发相关文章', 2, 0, CURRENT_TIMESTAMP), 37 | ('后端开发', 'backend', '后端开发相关文章', 2, 10, CURRENT_TIMESTAMP), 38 | ('旅行', 'travel', '旅行相关文章', 3, 0, CURRENT_TIMESTAMP) 39 | `); 40 | 41 | // 查询新的分类列表 42 | const categories = sqlite.prepare('SELECT * FROM categories ORDER BY "order"').all(); 43 | console.log('分类重置完成,当前分类列表:'); 44 | console.table(categories); 45 | 46 | } catch (error) { 47 | console.error('重置分类失败:', error); 48 | } finally { 49 | // 关闭数据库连接 50 | sqlite.close(); 51 | } 52 | } 53 | 54 | resetCategories(); 55 | -------------------------------------------------------------------------------- /scripts/add-is-visible-to-links.js: -------------------------------------------------------------------------------- 1 | const sqlite3 = require('sqlite3').verbose(); 2 | const path = require('path'); 3 | 4 | // 数据库文件路径 5 | const dbPath = path.join(process.cwd(), 'links.db'); 6 | 7 | // 连接数据库 8 | const db = new sqlite3.Database(dbPath, (err) => { 9 | if (err) { 10 | console.error('无法连接到数据库:', err.message); 11 | process.exit(1); 12 | } 13 | console.log('已连接到链接数据库'); 14 | }); 15 | 16 | // 检查 is_visible 字段是否已存在 17 | db.all("PRAGMA table_info(links)", (err, rows) => { 18 | if (err) { 19 | console.error('查询表结构失败:', err.message); 20 | db.close(); 21 | process.exit(1); 22 | } 23 | 24 | // 检查是否已存在 is_visible 字段 25 | const hasIsVisible = rows.some(row => row.name === 'is_visible'); 26 | 27 | if (hasIsVisible) { 28 | console.log('is_visible 字段已存在,无需添加'); 29 | db.close(); 30 | return; 31 | } 32 | 33 | // 添加 is_visible 字段 34 | db.run("ALTER TABLE links ADD COLUMN is_visible INTEGER NOT NULL DEFAULT 1", (err) => { 35 | if (err) { 36 | console.error('添加 is_visible 字段失败:', err.message); 37 | db.close(); 38 | process.exit(1); 39 | } 40 | 41 | console.log('成功添加 is_visible 字段到 links 表'); 42 | 43 | // 设置所有现有链接为可见 44 | db.run("UPDATE links SET is_visible = 1", (err) => { 45 | if (err) { 46 | console.error('更新现有链接失败:', err.message); 47 | } else { 48 | console.log('已将所有现有链接设置为可见'); 49 | } 50 | 51 | db.close(); 52 | }); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /scripts/create-admin.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * 创建管理员账户脚本 5 | * 6 | * 此脚本用于创建博客系统的管理员账户 7 | */ 8 | 9 | const { db } = require('../dist/lib/db'); 10 | const { sql } = require('drizzle-orm'); 11 | const readline = require('readline'); 12 | const bcrypt = require('bcryptjs'); 13 | 14 | // 创建命令行交互界面 15 | const rl = readline.createInterface({ 16 | input: process.stdin, 17 | output: process.stdout 18 | }); 19 | 20 | // 提示用户输入 21 | function prompt(question) { 22 | return new Promise((resolve) => { 23 | rl.question(question, (answer) => { 24 | resolve(answer); 25 | }); 26 | }); 27 | } 28 | 29 | async function main() { 30 | console.log('创建管理员账户'); 31 | console.log('================'); 32 | 33 | try { 34 | // 获取用户输入 35 | const email = await prompt('请输入管理员邮箱: '); 36 | if (!email || !email.includes('@')) { 37 | console.error('错误: 请输入有效的邮箱地址'); 38 | rl.close(); 39 | return; 40 | } 41 | 42 | // 检查邮箱是否已存在 43 | const existingUser = await db.select({ count: sql`count(*)` }) 44 | .from(sql`users`) 45 | .where(sql`email = ${email}`); 46 | 47 | if (existingUser[0].count > 0) { 48 | console.error('错误: 该邮箱已被注册'); 49 | rl.close(); 50 | return; 51 | } 52 | 53 | // 获取密码 54 | const password = await prompt('请输入管理员密码 (至少6位): '); 55 | if (!password || password.length < 6) { 56 | console.error('错误: 密码长度不能少于6位'); 57 | rl.close(); 58 | return; 59 | } 60 | 61 | const confirmPassword = await prompt('请再次输入密码: '); 62 | if (password !== confirmPassword) { 63 | console.error('错误: 两次输入的密码不一致'); 64 | rl.close(); 65 | return; 66 | } 67 | 68 | // 加密密码 69 | const salt = await bcrypt.genSalt(10); 70 | const hashedPassword = await bcrypt.hash(password, salt); 71 | 72 | // 创建管理员账户 73 | await db.execute(sql` 74 | INSERT INTO users (email, password) 75 | VALUES (${email}, ${hashedPassword}); 76 | `); 77 | 78 | console.log('✓ 管理员账户创建成功!'); 79 | console.log(`邮箱: ${email}`); 80 | console.log('您现在可以使用这些凭据登录管理后台'); 81 | } catch (error) { 82 | console.error('创建管理员账户失败:', error); 83 | } finally { 84 | rl.close(); 85 | } 86 | } 87 | 88 | main(); 89 | -------------------------------------------------------------------------------- /scripts/create-demo-db.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const { Database } = require('sqlite3').verbose(); 4 | const bcrypt = require('bcryptjs'); 5 | 6 | // 路径配置 7 | const sourceDbPath = path.join(__dirname, '..', 'blog.db'); 8 | const demoDbPath = path.join(__dirname, '..', 'demo.db'); 9 | 10 | // 确保源数据库存在 11 | if (!fs.existsSync(sourceDbPath)) { 12 | console.error('源数据库不存在:', sourceDbPath); 13 | process.exit(1); 14 | } 15 | 16 | // 复制数据库文件 17 | try { 18 | fs.copyFileSync(sourceDbPath, demoDbPath); 19 | console.log(`✓ 已复制数据库: ${sourceDbPath} -> ${demoDbPath}`); 20 | } catch (error) { 21 | console.error('复制数据库失败:', error); 22 | process.exit(1); 23 | } 24 | 25 | // 打开演示数据库 26 | const db = new Database(demoDbPath, (err) => { 27 | if (err) { 28 | console.error('打开数据库失败:', err); 29 | process.exit(1); 30 | } 31 | }); 32 | 33 | // 生成演示管理员密码的哈希 34 | async function createDemoAdmin() { 35 | try { 36 | // 生成密码哈希 37 | const salt = await bcrypt.genSalt(10); 38 | const demoPassword = 'demo123456'; // 演示密码 39 | const hashedPassword = await bcrypt.hash(demoPassword, salt); 40 | 41 | // 删除现有用户 42 | db.run('DELETE FROM users', function(err) { 43 | if (err) { 44 | console.error('删除现有用户失败:', err); 45 | return; 46 | } 47 | 48 | console.log('✓ 已删除现有用户'); 49 | 50 | // 创建演示管理员账户 51 | db.run( 52 | 'INSERT INTO users (email, password, createdAt) VALUES (?, ?, ?)', 53 | ['demo@example.com', hashedPassword, new Date().toISOString()], 54 | function(err) { 55 | if (err) { 56 | console.error('创建演示管理员失败:', err); 57 | return; 58 | } 59 | 60 | console.log('✓ 已创建演示管理员账户:'); 61 | console.log(' 邮箱: demo@example.com'); 62 | console.log(' 密码: demo123456'); 63 | 64 | // 关闭数据库连接 65 | db.close((err) => { 66 | if (err) { 67 | console.error('关闭数据库失败:', err); 68 | return; 69 | } 70 | 71 | console.log('✓ 演示数据库创建完成!'); 72 | console.log(`数据库路径: ${demoDbPath}`); 73 | }); 74 | } 75 | ); 76 | }); 77 | } catch (error) { 78 | console.error('处理密码哈希失败:', error); 79 | db.close(); 80 | } 81 | } 82 | 83 | // 执行创建演示管理员 84 | createDemoAdmin(); 85 | -------------------------------------------------------------------------------- /scripts/init-links-db.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * 链接数据库初始化脚本 5 | * 6 | * 此脚本用于初始化链接系统的数据库,创建必要的表结构 7 | */ 8 | 9 | const sqlite3 = require('sqlite3').verbose(); 10 | const path = require('path'); 11 | const fs = require('fs'); 12 | 13 | // 数据库文件路径 14 | const dbPath = path.join(process.cwd(), 'links.db'); 15 | 16 | async function main() { 17 | console.log('开始初始化链接数据库...'); 18 | 19 | // 检查数据库文件是否存在,如果存在则备份 20 | if (fs.existsSync(dbPath)) { 21 | const backupPath = `${dbPath}.backup-${Date.now()}`; 22 | console.log(`数据库文件已存在,创建备份: ${backupPath}`); 23 | fs.copyFileSync(dbPath, backupPath); 24 | } 25 | 26 | // 创建或打开数据库连接 27 | const db = new sqlite3.Database(dbPath); 28 | 29 | try { 30 | // 创建链接表 31 | await new Promise((resolve, reject) => { 32 | db.run(` 33 | CREATE TABLE IF NOT EXISTS links ( 34 | id INTEGER PRIMARY KEY AUTOINCREMENT, 35 | title TEXT NOT NULL, 36 | url TEXT NOT NULL, 37 | description TEXT, 38 | cover_image TEXT, 39 | tags TEXT, 40 | created_at TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL, 41 | updated_at TEXT 42 | ); 43 | `, function(err) { 44 | if (err) reject(err); 45 | else resolve(); 46 | }); 47 | }); 48 | console.log('✓ 链接表创建成功'); 49 | 50 | // 创建Webhook表 51 | await new Promise((resolve, reject) => { 52 | db.run(` 53 | CREATE TABLE IF NOT EXISTS webhooks ( 54 | id INTEGER PRIMARY KEY AUTOINCREMENT, 55 | url TEXT NOT NULL, 56 | secret TEXT, 57 | is_active INTEGER DEFAULT 1 NOT NULL, 58 | created_at TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL, 59 | updated_at TEXT 60 | ); 61 | `, function(err) { 62 | if (err) reject(err); 63 | else resolve(); 64 | }); 65 | }); 66 | console.log('✓ Webhook表创建成功'); 67 | 68 | console.log('链接数据库初始化完成!'); 69 | } catch (error) { 70 | console.error('数据库初始化失败:', error); 71 | process.exit(1); 72 | } finally { 73 | // 关闭数据库连接 74 | db.close(); 75 | } 76 | } 77 | 78 | main().then(() => process.exit(0)); 79 | -------------------------------------------------------------------------------- /scripts/insert-test-menus.sql: -------------------------------------------------------------------------------- 1 | -- 清空现有菜单数据 2 | DELETE FROM menus; 3 | 4 | -- 插入顶级菜单 5 | INSERT INTO menus (id, name, description, url, is_external, parent_id, sort_order, is_active, created_at) 6 | VALUES 7 | (1, 'AI工具', 'AI工具分类', '/categories/ai-tools', 0, NULL, 1, 1, CURRENT_TIMESTAMP), 8 | (2, '未分类', '未分类文章', '/categories/uncategorized', 0, NULL, 2, 1, CURRENT_TIMESTAMP), 9 | (3, 'AI教程', 'AI教程分类', '/categories/ai-tutorials', 0, NULL, 3, 1, CURRENT_TIMESTAMP), 10 | (4, '工具推荐', '工具推荐分类', '/categories/tool-recommendations', 0, NULL, 4, 1, CURRENT_TIMESTAMP), 11 | (5, 'Prompt分享', 'Prompt分享分类', '/categories/prompt-sharing', 0, NULL, 5, 1, CURRENT_TIMESTAMP), 12 | (6, '阅读思考', '阅读思考分类', '/categories/reading-thoughts', 0, NULL, 6, 1, CURRENT_TIMESTAMP), 13 | (7, 'AI总结', 'AI总结分类', '/categories/ai-summaries', 0, NULL, 7, 1, CURRENT_TIMESTAMP); 14 | 15 | -- 插入子菜单 16 | INSERT INTO menus (id, name, description, url, is_external, parent_id, sort_order, is_active, created_at) 17 | VALUES 18 | -- AI工具子菜单 19 | (8, 'ChatGPT', 'ChatGPT相关', '/categories/ai-tools/chatgpt', 0, 1, 1, 1, CURRENT_TIMESTAMP), 20 | (9, 'Claude', 'Claude相关', '/categories/ai-tools/claude', 0, 1, 2, 1, CURRENT_TIMESTAMP), 21 | (10, 'Midjourney', 'Midjourney相关', '/categories/ai-tools/midjourney', 0, 1, 3, 1, CURRENT_TIMESTAMP), 22 | (11, 'Stable Diffusion', 'Stable Diffusion相关', '/categories/ai-tools/stable-diffusion', 0, 1, 4, 1, CURRENT_TIMESTAMP), 23 | 24 | -- AI教程子菜单 25 | (12, '入门教程', '入门级AI教程', '/categories/ai-tutorials/beginner', 0, 3, 1, 1, CURRENT_TIMESTAMP), 26 | (13, '进阶教程', '进阶级AI教程', '/categories/ai-tutorials/advanced', 0, 3, 2, 1, CURRENT_TIMESTAMP), 27 | 28 | -- Prompt分享子菜单 29 | (14, '文本提示词', '文本类提示词', '/categories/prompt-sharing/text', 0, 5, 1, 1, CURRENT_TIMESTAMP), 30 | (15, '图像提示词', '图像类提示词', '/categories/prompt-sharing/image', 0, 5, 2, 1, CURRENT_TIMESTAMP); 31 | -------------------------------------------------------------------------------- /scripts/reset-admin.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * 重置管理员密码脚本 5 | * 6 | * 此脚本用于重置博客系统管理员的密码 7 | */ 8 | 9 | const Database = require('better-sqlite3'); 10 | const readline = require('readline'); 11 | const bcrypt = require('bcryptjs'); 12 | const path = require('path'); 13 | 14 | // 连接到数据库 15 | const db = new Database('./demo.db'); 16 | 17 | // 创建命令行交互界面 18 | const rl = readline.createInterface({ 19 | input: process.stdin, 20 | output: process.stdout 21 | }); 22 | 23 | // 提示用户输入 24 | function prompt(question) { 25 | return new Promise((resolve) => { 26 | rl.question(question, (answer) => { 27 | resolve(answer); 28 | }); 29 | }); 30 | } 31 | 32 | async function main() { 33 | console.log('重置管理员密码'); 34 | console.log('================'); 35 | 36 | try { 37 | // 获取所有用户 38 | const users = db.prepare('SELECT id, email FROM users').all(); 39 | 40 | if (users.length === 0) { 41 | console.log('系统中没有用户,请先创建管理员账户'); 42 | rl.close(); 43 | return; 44 | } 45 | 46 | // 显示用户列表 47 | console.log('系统中的用户:'); 48 | users.forEach((user, index) => { 49 | console.log(`${index + 1}. ${user.email}`); 50 | }); 51 | 52 | // 选择要重置密码的用户 53 | const userIndex = await prompt('请选择要重置密码的用户 (输入序号): '); 54 | const selectedIndex = parseInt(userIndex) - 1; 55 | 56 | if (isNaN(selectedIndex) || selectedIndex < 0 || selectedIndex >= users.length) { 57 | console.error('错误: 无效的选择'); 58 | rl.close(); 59 | return; 60 | } 61 | 62 | const selectedUser = users[selectedIndex]; 63 | 64 | // 获取新密码 65 | const password = await prompt('请输入新密码 (至少6位): '); 66 | if (!password || password.length < 6) { 67 | console.error('错误: 密码长度不能少于6位'); 68 | rl.close(); 69 | return; 70 | } 71 | 72 | const confirmPassword = await prompt('请再次输入新密码: '); 73 | if (password !== confirmPassword) { 74 | console.error('错误: 两次输入的密码不一致'); 75 | rl.close(); 76 | return; 77 | } 78 | 79 | // 加密密码 80 | const salt = await bcrypt.genSalt(10); 81 | const hashedPassword = await bcrypt.hash(password, salt); 82 | 83 | // 更新密码 84 | db.prepare('UPDATE users SET password = ? WHERE id = ?').run(hashedPassword, selectedUser.id); 85 | 86 | console.log('✓ 密码重置成功!'); 87 | console.log(`用户: ${selectedUser.email}`); 88 | console.log('您现在可以使用新密码登录管理后台'); 89 | } catch (error) { 90 | console.error('重置密码失败:', error); 91 | } finally { 92 | rl.close(); 93 | } 94 | } 95 | 96 | main(); 97 | -------------------------------------------------------------------------------- /scripts/run-migration.js: -------------------------------------------------------------------------------- 1 | // 运行单个迁移脚本 2 | import sqlite3 from 'sqlite3'; 3 | import { open } from 'sqlite'; 4 | import path from 'path'; 5 | import { fileURLToPath } from 'url'; 6 | 7 | // 获取当前文件的目录 8 | const __filename = fileURLToPath(import.meta.url); 9 | const __dirname = path.dirname(__filename); 10 | 11 | // 迁移脚本名称作为命令行参数 12 | const migrationName = process.argv[2]; 13 | 14 | if (!migrationName) { 15 | console.error('请提供迁移脚本名称'); 16 | process.exit(1); 17 | } 18 | 19 | async function runMigration() { 20 | // 打开数据库连接 21 | const db = await open({ 22 | filename: path.join(__dirname, '..', 'blog.db'), 23 | driver: sqlite3.Database 24 | }); 25 | 26 | try { 27 | // 导入迁移脚本 28 | const migrationPath = `../migrations/${migrationName}.js`; 29 | const migration = await import(migrationPath); 30 | 31 | // 执行迁移 32 | console.log(`执行迁移: ${migrationName}`); 33 | await migration.up(db); 34 | console.log(`迁移完成: ${migrationName}`); 35 | 36 | } catch (error) { 37 | console.error('迁移失败:', error); 38 | } finally { 39 | // 关闭数据库连接 40 | await db.close(); 41 | } 42 | } 43 | 44 | runMigration(); 45 | -------------------------------------------------------------------------------- /scripts/start-dev.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | setlocal enabledelayedexpansion 3 | 4 | :: 定义端口号 5 | set PORT=3000 6 | 7 | echo ===== 乔木博客开发环境启动脚本 ===== 8 | 9 | :: 检查端口是否被占用 10 | echo 检查端口 %PORT% 是否被占用... 11 | netstat -ano | findstr :%PORT% > nul 12 | if %errorlevel% equ 0 ( 13 | echo 端口 %PORT% 已被占用。 14 | 15 | :: 获取占用端口的进程PID 16 | for /f "tokens=5" %%a in ('netstat -ano ^| findstr :%PORT%') do ( 17 | set PID=%%a 18 | goto :found 19 | ) 20 | 21 | :found 22 | echo 找到占用端口的进程 PID: !PID!,正在终止... 23 | taskkill /F /PID !PID! 24 | echo 进程已终止。 25 | 26 | :: 等待一会儿,确保进程被完全终止 27 | timeout /t 2 /nobreak > nul 28 | ) else ( 29 | echo 端口 %PORT% 未被占用。 30 | ) 31 | 32 | :: 启动应用 33 | echo 正在启动应用... 34 | npm run dev 35 | 36 | endlocal 37 | -------------------------------------------------------------------------------- /scripts/start-dev.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 定义端口号 4 | PORT=3000 5 | 6 | # 检查端口是否被占用 7 | check_port() { 8 | echo "检查端口 $PORT 是否被占用..." 9 | if lsof -i :$PORT > /dev/null; then 10 | echo "端口 $PORT 已被占用。" 11 | return 0 12 | else 13 | echo "端口 $PORT 未被占用。" 14 | return 1 15 | fi 16 | } 17 | 18 | # 杀死占用端口的进程 19 | kill_process() { 20 | echo "正在杀死占用端口 $PORT 的进程..." 21 | # 获取占用端口的进程PID 22 | PID=$(lsof -t -i :$PORT) 23 | if [ -n "$PID" ]; then 24 | echo "找到占用端口的进程 PID: $PID,正在终止..." 25 | kill -9 $PID 26 | echo "进程已终止。" 27 | else 28 | echo "未找到占用端口的进程。" 29 | fi 30 | } 31 | 32 | # 启动应用 33 | start_app() { 34 | echo "正在启动应用..." 35 | npm run dev 36 | } 37 | 38 | # 主流程 39 | main() { 40 | echo "===== 乔木博客开发环境启动脚本 =====" 41 | 42 | # 检查端口是否被占用,如果被占用则杀死进程 43 | if check_port; then 44 | kill_process 45 | # 等待一会儿,确保进程被完全终止 46 | sleep 2 47 | fi 48 | 49 | # 启动应用 50 | start_app 51 | } 52 | 53 | # 执行主流程 54 | main 55 | -------------------------------------------------------------------------------- /setup.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | echo === 向阳乔木博客系统一键部署脚本 === 3 | echo 此脚本将帮助您快速部署博客系统 4 | echo. 5 | 6 | REM 检查Node.js是否安装 7 | where node >nul 2>nul 8 | if %ERRORLEVEL% neq 0 ( 9 | echo 错误: 未检测到Node.js,请先安装Node.js 18或更高版本 10 | exit /b 1 11 | ) 12 | 13 | REM 检查npm是否安装 14 | where npm >nul 2>nul 15 | if %ERRORLEVEL% neq 0 ( 16 | echo 错误: 未检测到npm,请先安装npm 17 | exit /b 1 18 | ) 19 | 20 | echo ✓ 环境检查通过 21 | echo. 22 | 23 | REM 安装依赖 24 | echo 正在安装依赖... 25 | call npm install 26 | if %ERRORLEVEL% neq 0 ( 27 | echo 错误: 安装依赖失败 28 | exit /b 1 29 | ) 30 | echo ✓ 依赖安装完成 31 | echo. 32 | 33 | REM 初始化数据库 34 | echo 正在初始化数据库... 35 | call npm run init-db 36 | if %ERRORLEVEL% neq 0 ( 37 | echo 错误: 数据库初始化失败 38 | exit /b 1 39 | ) 40 | echo ✓ 数据库初始化完成 41 | echo. 42 | 43 | REM 创建管理员账户 44 | echo 正在创建管理员账户... 45 | call npm run create-admin 46 | if %ERRORLEVEL% neq 0 ( 47 | echo 错误: 创建管理员账户失败 48 | exit /b 1 49 | ) 50 | echo ✓ 管理员账户创建完成 51 | echo. 52 | 53 | REM 构建生产版本 54 | echo 正在构建生产版本... 55 | call npm run build 56 | if %ERRORLEVEL% neq 0 ( 57 | echo 错误: 构建失败 58 | exit /b 1 59 | ) 60 | echo ✓ 构建完成 61 | echo. 62 | 63 | REM 启动服务器 64 | echo 正在启动服务器... 65 | echo 您可以使用Ctrl+C停止服务器 66 | echo 或者使用'npm start'命令再次启动服务器 67 | echo. 68 | echo 博客前台: http://localhost:3000 69 | echo 管理后台: http://localhost:3000/admin 70 | echo. 71 | call npm start 72 | -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 向阳乔木博客系统一键部署脚本 4 | echo "=== 向阳乔木博客系统一键部署脚本 ===" 5 | echo "此脚本将帮助您快速部署博客系统" 6 | echo 7 | 8 | # 检查Node.js是否安装 9 | if ! command -v node &> /dev/null; then 10 | echo "错误: 未检测到Node.js,请先安装Node.js 18或更高版本" 11 | exit 1 12 | fi 13 | 14 | # 检查npm是否安装 15 | if ! command -v npm &> /dev/null; then 16 | echo "错误: 未检测到npm,请先安装npm" 17 | exit 1 18 | fi 19 | 20 | # 检查Node.js版本 21 | NODE_VERSION=$(node -v | cut -d 'v' -f 2 | cut -d '.' -f 1) 22 | if [ "$NODE_VERSION" -lt 18 ]; then 23 | echo "错误: Node.js版本过低,需要18或更高版本" 24 | echo "当前版本: $(node -v)" 25 | exit 1 26 | fi 27 | 28 | echo "✓ 环境检查通过" 29 | echo 30 | 31 | # 安装依赖 32 | echo "正在安装依赖..." 33 | npm install 34 | if [ $? -ne 0 ]; then 35 | echo "错误: 安装依赖失败" 36 | exit 1 37 | fi 38 | echo "✓ 依赖安装完成" 39 | echo 40 | 41 | # 初始化数据库 42 | echo "正在初始化数据库..." 43 | npm run init-db 44 | if [ $? -ne 0 ]; then 45 | echo "错误: 数据库初始化失败" 46 | exit 1 47 | fi 48 | echo "✓ 数据库初始化完成" 49 | echo 50 | 51 | # 创建管理员账户 52 | echo "正在创建管理员账户..." 53 | npm run create-admin 54 | if [ $? -ne 0 ]; then 55 | echo "错误: 创建管理员账户失败" 56 | exit 1 57 | fi 58 | echo "✓ 管理员账户创建完成" 59 | echo 60 | 61 | # 构建生产版本 62 | echo "正在构建生产版本..." 63 | npm run build 64 | if [ $? -ne 0 ]; then 65 | echo "错误: 构建失败" 66 | exit 1 67 | fi 68 | echo "✓ 构建完成" 69 | echo 70 | 71 | # 启动服务器 72 | echo "正在启动服务器..." 73 | echo "您可以使用Ctrl+C停止服务器" 74 | echo "或者使用'npm start'命令再次启动服务器" 75 | echo 76 | echo "博客前台: http://localhost:3000" 77 | echo "管理后台: http://localhost:3000/admin" 78 | echo 79 | npm start 80 | -------------------------------------------------------------------------------- /src/app/(frontend)/layout.tsx: -------------------------------------------------------------------------------- 1 | import Navigation from "@/components/Navigation"; 2 | import Footer from "@/components/Footer"; 3 | 4 | export default function FrontendLayout({ 5 | children, 6 | }: { 7 | children: React.ReactNode; 8 | }) { 9 | return ( 10 | <> 11 |
12 | 13 |
14 |
15 | {children} 16 |
17 |