├── .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 |
4 |
--------------------------------------------------------------------------------
/public/icons/twitter.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/icons/wechat.svg:
--------------------------------------------------------------------------------
1 |
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 |
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 |
14 |
15 | {children}
16 |
17 |
18 | >
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/src/app/(main)/layout.tsx:
--------------------------------------------------------------------------------
1 | import MainLayout from "@/components/MainLayout";
2 |
3 | export default function MainAppLayout({
4 | children,
5 | }: Readonly<{
6 | children: React.ReactNode;
7 | }>) {
8 | return (
9 |
10 | {children}
11 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/src/app/admin/layout.tsx:
--------------------------------------------------------------------------------
1 | import { AdminLayout } from "@/components/admin/layout";
2 | import type { Metadata } from "next";
3 | import { SessionProvider } from "@/components/providers/SessionProvider";
4 |
5 | export const metadata: Metadata = {
6 | title: "博客管理后台 - 向阳乔木的个人博客",
7 | description: "博客管理后台",
8 | };
9 |
10 | export default function AdminRootLayout({
11 | children,
12 | }: Readonly<{
13 | children: React.ReactNode;
14 | }>) {
15 | return (
16 |
17 | {children}
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/src/app/api/admin-bypass/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server';
2 | import { cookies } from 'next/headers';
3 | import { db } from '@/lib/db';
4 | import * as schema from '@/lib/schema';
5 | import { eq } from 'drizzle-orm';
6 |
7 | export async function GET(request: Request) {
8 | try {
9 | // This is a special bypass route for development/preview environments only
10 | console.log('Admin bypass route accessed');
11 |
12 | // Get user from database (we'll use the first admin user)
13 | const users = await db.select()
14 | .from(schema.users)
15 | .all();
16 |
17 | if (users.length === 0) {
18 | console.log('No users found in database');
19 | return NextResponse.json(
20 | { error: 'No users found' },
21 | { status: 500 }
22 | );
23 | }
24 |
25 | const user = users[0];
26 | console.log('Using user for bypass:', { id: user.id, email: user.email });
27 |
28 | // Create the auth cookie data
29 | const authData = {
30 | id: user.id,
31 | email: user.email,
32 | isLoggedIn: true,
33 | timestamp: new Date().toISOString(),
34 | bypass: true
35 | };
36 |
37 | // Create the response
38 | const response = NextResponse.json({
39 | success: true,
40 | message: 'Admin bypass successful',
41 | redirectTo: '/admin',
42 | user: {
43 | id: user.id,
44 | email: user.email
45 | }
46 | });
47 |
48 | // Set auth cookie in the response
49 | response.cookies.set({
50 | name: 'auth',
51 | value: JSON.stringify(authData),
52 | httpOnly: true,
53 | secure: process.env.NODE_ENV === 'production',
54 | maxAge: 60 * 60 * 24 * 7, // 1 week
55 | path: '/',
56 | sameSite: 'lax'
57 | });
58 |
59 | console.log('Admin bypass successful, auth cookie set');
60 |
61 | return response;
62 | } catch (error) {
63 | console.error('Admin bypass error:', error);
64 | return NextResponse.json(
65 | { error: 'Admin bypass failed' },
66 | { status: 500 }
67 | );
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/app/api/admin/create-admin/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server';
2 | import { db } from '@/lib/db';
3 | import { sql } from 'drizzle-orm';
4 | import bcrypt from 'bcryptjs';
5 | import { users } from '@/lib/schema';
6 |
7 | export async function GET(request: Request) {
8 | try {
9 | // 从URL参数中获取邮箱和密码
10 | const url = new URL(request.url);
11 | const email = url.searchParams.get('email');
12 | const password = url.searchParams.get('password');
13 |
14 | // 验证参数
15 | if (!email || !password) {
16 | return NextResponse.json({
17 | success: false,
18 | error: '邮箱和密码是必需的'
19 | }, { status: 400 });
20 | }
21 |
22 | if (!email.includes('@')) {
23 | return NextResponse.json({
24 | success: false,
25 | error: '请提供有效的邮箱地址'
26 | }, { status: 400 });
27 | }
28 |
29 | if (password.length < 6) {
30 | return NextResponse.json({
31 | success: false,
32 | error: '密码长度不能少于6位'
33 | }, { status: 400 });
34 | }
35 |
36 | // 检查邮箱是否已存在
37 | const existingUser = await db.select({ count: sql`count(*)` })
38 | .from(sql`users`)
39 | .where(sql`email = ${email}`);
40 |
41 | // 确保类型安全
42 | const userCount = existingUser[0] ? Number(existingUser[0].count) : 0;
43 |
44 | if (userCount > 0) {
45 | return NextResponse.json({
46 | success: false,
47 | error: '该邮箱已被注册'
48 | }, { status: 400 });
49 | }
50 |
51 | // 加密密码
52 | const salt = await bcrypt.genSalt(10);
53 | const hashedPassword = await bcrypt.hash(password, salt);
54 |
55 | // 创建管理员账户
56 | await db.insert(users).values({
57 | email,
58 | password: hashedPassword,
59 | createdAt: new Date().toISOString()
60 | });
61 |
62 | return NextResponse.json({
63 | success: true,
64 | message: '管理员账户创建成功',
65 | email
66 | });
67 | } catch (error) {
68 | console.error('创建管理员账户失败:', error);
69 | return NextResponse.json({
70 | success: false,
71 | error: '创建管理员账户失败',
72 | details: error instanceof Error ? error.message : String(error)
73 | }, { status: 500 });
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/app/api/admin/posts/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server';
2 | import { db } from '@/lib/db';
3 | import * as schema from '@/lib/schema';
4 |
5 | export async function POST(request: Request) {
6 | try {
7 | const { title, slug, content, published } = await request.json();
8 |
9 | await db.insert(schema.posts).values({
10 | title,
11 | slug,
12 | content,
13 | published
14 | });
15 |
16 | return NextResponse.json({ success: true });
17 | } catch (error) {
18 | console.error(error);
19 | return NextResponse.json(
20 | { success: false, error: 'Failed to create post' },
21 | { status: 500 }
22 | );
23 | }
24 | }
--------------------------------------------------------------------------------
/src/app/api/admin/stats/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server';
2 |
3 | export interface StatsResponse {
4 | posts: number;
5 | categories: number;
6 | tags: number;
7 | views: number;
8 | recentPosts: any[];
9 | }
10 |
11 | export async function GET(): Promise> {
12 | // 返回空的统计数据
13 | return NextResponse.json({
14 | posts: 0,
15 | categories: 0,
16 | tags: 0,
17 | views: 0,
18 | recentPosts: []
19 | });
20 | }
21 |
--------------------------------------------------------------------------------
/src/app/api/auth/[...nextauth]/route.ts:
--------------------------------------------------------------------------------
1 | import { authOptions } from '@/auth/options';
2 | import NextAuth from 'next-auth';
3 |
4 | // 使用 NextAuth 创建处理程序
5 | const handler = NextAuth(authOptions);
6 |
7 | // 确保导出正确的处理函数
8 | export { handler as GET, handler as POST };
9 |
--------------------------------------------------------------------------------
/src/app/api/categories/list/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server';
2 | import { db } from '@/lib/db';
3 | import * as schema from '@/lib/schema';
4 | import { eq, count } from 'drizzle-orm';
5 |
6 | // 获取所有分类列表
7 | export async function GET() {
8 | try {
9 | console.log('获取分类列表');
10 |
11 | // 先获取所有分类
12 | const allCategories = await db
13 | .select({
14 | id: schema.categories.id,
15 | name: schema.categories.name,
16 | slug: schema.categories.slug,
17 | description: schema.categories.description,
18 | parentId: schema.categories.parentId,
19 | order: schema.categories.order,
20 | createdAt: schema.categories.createdAt,
21 | updatedAt: schema.categories.updatedAt
22 | })
23 | .from(schema.categories)
24 | .orderBy(schema.categories.order)
25 | .all();
26 |
27 | // 获取每个分类的已发布文章数量
28 | const categoryCounts = await db
29 | .select({
30 | categoryId: schema.postCategories.categoryId,
31 | postCount: count(schema.postCategories.postId)
32 | })
33 | .from(schema.postCategories)
34 | .innerJoin(schema.posts, eq(schema.postCategories.postId, schema.posts.id))
35 | .where(eq(schema.posts.published, 1)) // 只计算已发布的文章
36 | .groupBy(schema.postCategories.categoryId)
37 | .all();
38 |
39 | // 创建分类ID到文章数量的映射
40 | const countMap = new Map();
41 | categoryCounts.forEach(item => {
42 | countMap.set(item.categoryId, item.postCount);
43 | });
44 |
45 | // 为每个分类添加文章数量
46 | const categoriesWithCounts = allCategories.map(category => ({
47 | ...category,
48 | postCount: countMap.get(category.id) || 0
49 | }));
50 |
51 | // 在管理界面中,我们需要返回所有分类,而不仅仅是有已发布文章的分类
52 | console.log(`成功获取 ${allCategories.length} 个分类`);
53 |
54 | return NextResponse.json(categoriesWithCounts);
55 | } catch (error) {
56 | console.error('获取分类列表失败:', error);
57 | return NextResponse.json(
58 | { error: '获取分类列表失败', details: error instanceof Error ? error.message : String(error) },
59 | { status: 500 }
60 | );
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/app/api/categories/reset/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server';
2 | import { db, sqlite } from '@/lib/db';
3 | import * as schema from '@/lib/schema';
4 | import { eq, ne, isNotNull } from 'drizzle-orm';
5 |
6 | /**
7 | * 重置分类数据
8 | * 此 API 将删除所有分类数据并创建新的默认分类
9 | */
10 | export async function POST() {
11 | try {
12 | console.log('开始重置分类数据...');
13 |
14 | // 检查是否有文章使用分类
15 | const postsWithCategories = await db.query.posts.findMany({
16 | where: (posts) =>
17 | isNotNull(posts.categoryId)
18 | });
19 |
20 | if (postsWithCategories.length > 0) {
21 | console.log('有文章使用分类,将文章的分类设置为 null');
22 |
23 | // 将所有文章的分类设置为 null
24 | for (const post of postsWithCategories) {
25 | await db
26 | .update(schema.posts)
27 | .set({ categoryId: null })
28 | .where(eq(schema.posts.id, post.id));
29 | }
30 | }
31 |
32 | // 使用原始 SQL 删除所有分类数据
33 | console.log('删除所有分类数据...');
34 | await sqlite.exec('DELETE FROM categories');
35 |
36 | // 重置自增 ID
37 | await sqlite.exec('DELETE FROM sqlite_sequence WHERE name = "categories"');
38 |
39 | // 创建新的默认分类
40 | console.log('创建新的默认分类...');
41 | const defaultCategories = [
42 | {
43 | name: '未分类',
44 | slug: 'uncategorized',
45 | description: '默认分类',
46 | parentId: null,
47 | order: 0
48 | },
49 | {
50 | name: '技术',
51 | slug: 'technology',
52 | description: '技术相关文章',
53 | parentId: null,
54 | order: 10
55 | },
56 | {
57 | name: '生活',
58 | slug: 'life',
59 | description: '生活相关文章',
60 | parentId: null,
61 | order: 20
62 | }
63 | ];
64 |
65 | // 插入默认分类
66 | for (const category of defaultCategories) {
67 | await db.insert(schema.categories).values({
68 | name: category.name,
69 | slug: category.slug,
70 | description: category.description,
71 | parentId: category.parentId,
72 | order: category.order
73 | });
74 | }
75 |
76 | console.log('分类数据重置完成');
77 |
78 | return NextResponse.json({
79 | success: true,
80 | message: '分类数据已成功重置',
81 | categories: await db.query.categories.findMany()
82 | });
83 | } catch (error) {
84 | console.error('重置分类数据失败:', error);
85 | return NextResponse.json(
86 | { error: '重置分类数据失败', details: error instanceof Error ? error.message : String(error) },
87 | { status: 500 }
88 | );
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/app/api/create-user/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server';
2 | import { db } from '@/lib/db';
3 | import * as schema from '@/lib/schema';
4 | import * as bcrypt from 'bcryptjs';
5 |
6 | export async function POST(request: Request) {
7 | try {
8 | const { email, password } = await request.json();
9 |
10 | if (!email || !password) {
11 | return NextResponse.json(
12 | { error: 'Missing email or password' },
13 | { status: 400 }
14 | );
15 | }
16 |
17 | // 对密码进行哈希处理
18 | const hashedPassword = await bcrypt.hash(password, 10);
19 |
20 | // 创建用户
21 | const result = await db.insert(schema.users).values({
22 | email,
23 | password: hashedPassword,
24 | }).returning();
25 |
26 | return NextResponse.json({
27 | message: '用户创建成功',
28 | user: {
29 | id: result[0].id,
30 | email: result[0].email
31 | }
32 | });
33 | } catch (error) {
34 | console.error('创建用户错误:', error);
35 | return NextResponse.json(
36 | {
37 | error: '创建用户失败',
38 | details: error instanceof Error ? error.message : '未知错误',
39 | },
40 | { status: 500 }
41 | );
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/app/api/links/[id]/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from 'next/server';
2 | import { getLink, updateLink, deleteLink } from '@/lib/actions/links';
3 |
4 | // GET - 获取单个链接
5 | export async function GET(
6 | request: NextRequest,
7 | { params }: { params: Promise<{ id: string }> }
8 | ) {
9 | try {
10 | const { id: idParam } = await params;
11 | const id = parseInt(idParam);
12 |
13 | const link = await getLink(id);
14 |
15 | return NextResponse.json(link);
16 | } catch (error) {
17 | console.error('获取链接失败:', error);
18 | return NextResponse.json(
19 | { error: error instanceof Error ? error.message : '获取链接失败' },
20 | { status: 500 }
21 | );
22 | }
23 | }
24 |
25 | // PUT - 更新链接
26 | export async function PUT(
27 | request: NextRequest,
28 | { params }: { params: Promise<{ id: string }> }
29 | ) {
30 | try {
31 | const { id: idParam } = await params;
32 | const id = parseInt(idParam);
33 |
34 | const data = await request.json();
35 |
36 | const link = await updateLink(id, data);
37 |
38 | return NextResponse.json({
39 | success: true,
40 | message: '链接更新成功',
41 | link
42 | });
43 | } catch (error) {
44 | console.error('更新链接失败:', error);
45 | return NextResponse.json(
46 | { error: error instanceof Error ? error.message : '更新链接失败' },
47 | { status: 500 }
48 | );
49 | }
50 | }
51 |
52 | // DELETE - 删除链接
53 | export async function DELETE(
54 | request: NextRequest,
55 | { params }: { params: Promise<{ id: string }> }
56 | ) {
57 | try {
58 | const { id: idParam } = await params;
59 | const id = parseInt(idParam);
60 |
61 | await deleteLink(id);
62 |
63 | return NextResponse.json({
64 | success: true,
65 | message: '链接删除成功'
66 | });
67 | } catch (error) {
68 | console.error('删除链接失败:', error);
69 | return NextResponse.json(
70 | { error: error instanceof Error ? error.message : '删除链接失败' },
71 | { status: 500 }
72 | );
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/app/api/links/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from 'next/server';
2 | import { getLinks, createLink } from '@/lib/actions/links';
3 |
4 | // GET - 获取所有链接
5 | export async function GET(request: NextRequest) {
6 | try {
7 | const searchParams = request.nextUrl.searchParams;
8 | const tag = searchParams.get('tag');
9 | const page = parseInt(searchParams.get('page') || '1');
10 | const pageSize = parseInt(searchParams.get('pageSize') || '20');
11 |
12 | const result = await getLinks(tag, page, pageSize);
13 |
14 | return NextResponse.json(result);
15 | } catch (error) {
16 | console.error('获取链接列表失败:', error);
17 | return NextResponse.json(
18 | { error: error instanceof Error ? error.message : '获取链接列表失败' },
19 | { status: 500 }
20 | );
21 | }
22 | }
23 |
24 | // POST - 创建新链接
25 | export async function POST(request: NextRequest) {
26 | try {
27 | const data = await request.json();
28 |
29 | const link = await createLink(data);
30 |
31 | return NextResponse.json({
32 | success: true,
33 | message: '链接创建成功',
34 | link
35 | });
36 | } catch (error) {
37 | console.error('创建链接失败:', error);
38 | return NextResponse.json(
39 | { error: error instanceof Error ? error.message : '创建链接失败' },
40 | { status: 500 }
41 | );
42 | }
43 | }
44 |
45 |
46 |
--------------------------------------------------------------------------------
/src/app/api/links/webhook/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from 'next/server';
2 | import { createLink } from '@/lib/actions/links';
3 |
4 | // 简单的验证码 - 在实际应用中可以使用更复杂的机制
5 | const VALID_CODES = ['qiaomu', '乔木', 'blog'];
6 |
7 | // POST - 接收Chrome插件发送的数据
8 | export async function POST(request: NextRequest) {
9 | try {
10 | const data = await request.json();
11 |
12 | // 验证必填字段
13 | if (!data.title || !data.url) {
14 | return NextResponse.json(
15 | { error: '标题和URL为必填项' },
16 | { status: 400 }
17 | );
18 | }
19 |
20 | // 验证验证码
21 | if (!data.verificationCode || !VALID_CODES.includes(data.verificationCode)) {
22 | return NextResponse.json(
23 | { error: '验证码无效' },
24 | { status: 403 }
25 | );
26 | }
27 |
28 | // 准备链接数据
29 | const linkData = {
30 | title: data.title,
31 | url: data.url,
32 | description: data.description || '',
33 | coverImage: data.coverImage || '',
34 | tags: data.tags || '[]'
35 | };
36 |
37 | // 创建或更新链接
38 | const link = await createLink(linkData);
39 |
40 | // 根据是否有创建时间和更新时间判断是新建还是更新
41 | const isNewLink = link.createdAt === link.updatedAt;
42 |
43 | return NextResponse.json({
44 | success: true,
45 | message: isNewLink ? '链接创建成功' : '链接更新成功',
46 | link,
47 | isNewLink
48 | });
49 | } catch (error) {
50 | console.error('通过Webhook创建链接失败:', error);
51 | return NextResponse.json(
52 | { error: error instanceof Error ? error.message : '创建链接失败' },
53 | { status: 500 }
54 | );
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/app/api/login/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server';
2 | import { cookies } from 'next/headers';
3 | import { db } from '@/lib/db';
4 | import * as schema from '@/lib/schema';
5 | import { eq } from 'drizzle-orm';
6 | import * as bcrypt from 'bcryptjs';
7 |
8 | export async function POST(request: Request) {
9 | try {
10 | const { email, password } = await request.json();
11 |
12 | console.log('Login attempt:', email);
13 |
14 | // Validate input
15 | if (!email || !password) {
16 | return NextResponse.json(
17 | { error: '邮箱和密码不能为空' },
18 | { status: 400 }
19 | );
20 | }
21 |
22 | // Get user from database
23 | const users = await db.select()
24 | .from(schema.users)
25 | .where(eq(schema.users.email, email))
26 | .all();
27 |
28 | const user = users[0];
29 |
30 | if (!user) {
31 | console.log('User not found');
32 | return NextResponse.json(
33 | { error: '用户不存在' },
34 | { status: 401 }
35 | );
36 | }
37 |
38 | // Verify password
39 | let isPasswordValid = false;
40 |
41 | try {
42 | // Try bcrypt first
43 | isPasswordValid = await bcrypt.compare(password, user.password);
44 | } catch (error) {
45 | // If bcrypt fails, try direct comparison as fallback
46 | isPasswordValid = user.password === password;
47 | }
48 |
49 | if (!isPasswordValid) {
50 | console.log('Invalid password');
51 | return NextResponse.json(
52 | { error: '密码不正确' },
53 | { status: 401 }
54 | );
55 | }
56 |
57 | // Create the response
58 | const response = NextResponse.json({ success: true });
59 |
60 | // Set auth cookie in the response
61 | response.cookies.set({
62 | name: 'auth',
63 | value: JSON.stringify({
64 | id: user.id,
65 | email: user.email,
66 | isLoggedIn: true
67 | }),
68 | httpOnly: true,
69 | secure: process.env.NODE_ENV === 'production',
70 | maxAge: 60 * 60 * 24 * 7, // 1 week
71 | path: '/'
72 | });
73 |
74 | console.log('Login successful');
75 |
76 | return response;
77 | } catch (error) {
78 | console.error('Login error:', error);
79 | return NextResponse.json(
80 | { error: '登录过程中发生错误' },
81 | { status: 500 }
82 | );
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/app/api/logout/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server';
2 | import { cookies } from 'next/headers';
3 |
4 | // 支持GET和POST请求
5 | export async function GET() {
6 | return handleLogout();
7 | }
8 |
9 | export async function POST() {
10 | return handleLogout();
11 | }
12 |
13 | async function handleLogout() {
14 | try {
15 | // 创建响应
16 | const response = NextResponse.json({
17 | success: true,
18 | redirectTo: '/login'
19 | });
20 |
21 | // 清除auth cookie
22 | response.cookies.set({
23 | name: 'auth',
24 | value: '',
25 | httpOnly: true,
26 | expires: new Date(0),
27 | path: '/'
28 | });
29 |
30 | // 清除next-auth.session-token cookie
31 | response.cookies.set({
32 | name: 'next-auth.session-token',
33 | value: '',
34 | httpOnly: true,
35 | expires: new Date(0),
36 | path: '/'
37 | });
38 |
39 | // 清除next-auth.csrf-token cookie
40 | response.cookies.set({
41 | name: 'next-auth.csrf-token',
42 | value: '',
43 | httpOnly: true,
44 | expires: new Date(0),
45 | path: '/'
46 | });
47 |
48 | // 清除next-auth.callback-url cookie
49 | response.cookies.set({
50 | name: 'next-auth.callback-url',
51 | value: '',
52 | httpOnly: true,
53 | expires: new Date(0),
54 | path: '/'
55 | });
56 |
57 | return response;
58 | } catch (error) {
59 | // 不使用console.error,避免Edge Runtime问题
60 | return NextResponse.json({
61 | success: false,
62 | error: 'Failed to logout'
63 | }, { status: 500 });
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/app/api/register/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server';
2 | import { db } from '@/lib/db';
3 | import * as schema from '@/lib/schema';
4 |
5 | export async function POST(request: Request) {
6 | try {
7 | const body = await request.json();
8 | const { email, password } = body;
9 |
10 | if (!email || !password) {
11 | return NextResponse.json(
12 | { error: 'Email and password are required' },
13 | { status: 400 }
14 | );
15 | }
16 |
17 | // Check if user already exists
18 | const existingUser = await db.query.users.findFirst({
19 | where: (users, { eq }) => eq(users.email, email)
20 | });
21 |
22 | if (existingUser) {
23 | return NextResponse.json(
24 | { error: 'User with this email already exists' },
25 | { status: 409 }
26 | );
27 | }
28 |
29 | // Insert new user
30 | const result = await db.insert(schema.users).values({
31 | email,
32 | password
33 | });
34 |
35 | return NextResponse.json(
36 | { success: true, message: 'User registered successfully' },
37 | { status: 201 }
38 | );
39 | } catch (error) {
40 | console.error('Registration error:', error);
41 | return NextResponse.json(
42 | { error: 'An error occurred during registration' },
43 | { status: 500 }
44 | );
45 | }
46 | }
--------------------------------------------------------------------------------
/src/app/api/settings/contact/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server';
2 | import { db } from '@/lib/db';
3 | import { contactInfo } from '@/lib/schema/settings';
4 | import { eq } from 'drizzle-orm';
5 |
6 | // 获取所有联系方式
7 | export async function GET() {
8 | try {
9 | const contacts = await db.select().from(contactInfo);
10 | return NextResponse.json(contacts);
11 | } catch (error) {
12 | console.error('获取联系方式失败:', error);
13 | return NextResponse.json({ error: '获取联系方式失败' }, { status: 500 });
14 | }
15 | }
16 |
17 | // 创建新的联系方式
18 | export async function POST(request: Request) {
19 | try {
20 | const data = await request.json();
21 |
22 | // 验证必填字段
23 | if (!data.type || !data.value) {
24 | return NextResponse.json({ error: '类型和值为必填项' }, { status: 400 });
25 | }
26 |
27 | const newContact = await db.insert(contactInfo).values({
28 | type: data.type,
29 | value: data.value,
30 | qrCodeUrl: data.qrCodeUrl || null,
31 | displayName: data.displayName || null,
32 | isActive: data.isActive !== undefined ? data.isActive : 1,
33 | }).returning();
34 |
35 | return NextResponse.json(newContact[0]);
36 | } catch (error) {
37 | console.error('创建联系方式失败:', error);
38 | return NextResponse.json({ error: '创建联系方式失败' }, { status: 500 });
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/app/api/settings/donation/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server';
2 | import { db } from '@/lib/db';
3 | import { donationInfo } from '@/lib/schema/settings';
4 | import { eq } from 'drizzle-orm';
5 |
6 | // 获取所有打赏信息
7 | export async function GET() {
8 | try {
9 | const donations = await db.select().from(donationInfo);
10 | return NextResponse.json(donations);
11 | } catch (error) {
12 | console.error('获取打赏信息失败:', error);
13 | return NextResponse.json({ error: '获取打赏信息失败' }, { status: 500 });
14 | }
15 | }
16 |
17 | // 创建新的打赏信息
18 | export async function POST(request: Request) {
19 | try {
20 | const data = await request.json();
21 |
22 | // 验证必填字段
23 | if (!data.type || !data.qrCodeUrl) {
24 | return NextResponse.json({ error: '类型和二维码URL为必填项' }, { status: 400 });
25 | }
26 |
27 | const newDonation = await db.insert(donationInfo).values({
28 | type: data.type,
29 | qrCodeUrl: data.qrCodeUrl,
30 | description: data.description || null,
31 | isActive: data.isActive !== undefined ? data.isActive : 1,
32 | }).returning();
33 |
34 | return NextResponse.json(newDonation[0]);
35 | } catch (error) {
36 | console.error('创建打赏信息失败:', error);
37 | return NextResponse.json({ error: '创建打赏信息失败' }, { status: 500 });
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/app/api/settings/general/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server';
2 | import { db } from '@/lib/db';
3 | import { siteSettings } from '@/lib/schema/settings';
4 | import { eq } from 'drizzle-orm';
5 |
6 | // 获取所有网站基本设置
7 | export async function GET() {
8 | try {
9 | const settings = await db.select().from(siteSettings);
10 |
11 | // 将设置转换为键值对对象
12 | const settingsMap: Record = {};
13 | if (settings && settings.length > 0) {
14 | for (const setting of settings) {
15 | settingsMap[setting.key] = setting.value;
16 | }
17 | }
18 |
19 | return NextResponse.json({
20 | success: true,
21 | data: {
22 | settings: settingsMap
23 | }
24 | });
25 | } catch (error) {
26 | console.error('获取网站设置失败:', error);
27 | return NextResponse.json({ error: '获取网站设置失败' }, { status: 500 });
28 | }
29 | }
30 |
31 | // 保存网站基本设置
32 | export async function POST(request: Request) {
33 | try {
34 | const data = await request.json();
35 |
36 | // 确保 data 不为 null 或 undefined
37 | if (!data) {
38 | return NextResponse.json({ error: '无效的设置数据' }, { status: 400 });
39 | }
40 |
41 | // 遍历所有设置项并保存
42 | for (const [key, value] of Object.entries(data)) {
43 | // 检查设置是否已存在
44 | const existingSetting = await db.select()
45 | .from(siteSettings)
46 | .where(eq(siteSettings.key, key))
47 | .limit(1);
48 |
49 | if (existingSetting.length > 0) {
50 | // 更新现有设置
51 | await db.update(siteSettings)
52 | .set({
53 | value: value as string,
54 | updatedAt: new Date().toISOString(),
55 | })
56 | .where(eq(siteSettings.key, key));
57 | } else {
58 | // 创建新设置
59 | await db.insert(siteSettings).values({
60 | key,
61 | value: value as string,
62 | group: 'general',
63 | });
64 | }
65 | }
66 |
67 | return NextResponse.json({
68 | success: true,
69 | message: '网站设置保存成功'
70 | });
71 | } catch (error) {
72 | console.error('保存网站设置失败:', error);
73 | return NextResponse.json({ error: '保存网站设置失败' }, { status: 500 });
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/app/api/settings/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server';
2 | import { db } from '@/lib/db';
3 | import * as schema from '@/lib/schema';
4 | import { eq } from 'drizzle-orm';
5 |
6 | // 获取所有网站设置
7 | export async function GET(request: Request) {
8 | try {
9 | // 获取网站基本设置
10 | const siteSettings = await db
11 | .select()
12 | .from(schema.siteSettings)
13 | .all();
14 |
15 | // 将设置转换为键值对对象
16 | const settingsMap: Record = {};
17 | if (siteSettings && siteSettings.length > 0) {
18 | for (const setting of siteSettings) {
19 | if (setting.key && setting.value) {
20 | settingsMap[setting.key] = setting.value;
21 | }
22 | }
23 | }
24 |
25 | // 获取社交媒体链接
26 | const socialLinks = await db
27 | .select()
28 | .from(schema.socialLinks)
29 | .where(eq(schema.socialLinks.isActive, 1))
30 | .orderBy(schema.socialLinks.order)
31 | .all();
32 |
33 | // 获取联系方式
34 | const contactInfo = await db
35 | .select()
36 | .from(schema.contactInfo)
37 | .where(eq(schema.contactInfo.isActive, 1))
38 | .all();
39 |
40 | // 获取打赏信息
41 | const donationInfo = await db
42 | .select()
43 | .from(schema.donationInfo)
44 | .where(eq(schema.donationInfo.isActive, 1))
45 | .all();
46 |
47 | // 获取Hero区域设置
48 | const heroSettings = await db
49 | .select()
50 | .from(schema.heroSettings)
51 | .where(eq(schema.heroSettings.isActive, 1))
52 | .limit(1)
53 | .all();
54 |
55 | // 返回所有设置
56 | return NextResponse.json({
57 | success: true,
58 | data: {
59 | settings: settingsMap,
60 | socialLinks,
61 | contactInfo,
62 | donationInfo,
63 | hero: heroSettings[0] || null,
64 | }
65 | });
66 | } catch (error) {
67 | console.error('获取网站设置失败:', error);
68 | return NextResponse.json(
69 | { success: false, error: '获取网站设置失败' },
70 | { status: 500 }
71 | );
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/app/api/settings/scripts/position/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from 'next/server';
2 | import { db } from '@/lib/db';
3 | import * as schema from '@/lib/schema';
4 | import { eq, and } from 'drizzle-orm';
5 | import { headScripts } from '@/lib/schema/settings';
6 |
7 | /**
8 | * 获取指定位置的脚本
9 | * @param request 请求对象
10 | * @returns 响应对象
11 | */
12 | export async function GET(request: NextRequest) {
13 | console.log('[API /api/settings/scripts/position] Received request');
14 | try {
15 | // 获取查询参数
16 | const { searchParams } = new URL(request.url);
17 | const position = searchParams.get('position');
18 | console.log(`[API /api/settings/scripts/position] Position requested: ${position}`);
19 |
20 | if (!position) {
21 | console.warn('[API /api/settings/scripts/position] Position parameter missing');
22 | return NextResponse.json({ success: false, message: '缺少位置参数' }, { status: 400 });
23 | }
24 |
25 | // 获取指定位置的脚本
26 | console.log(`[API /api/settings/scripts/position] Querying database for position: ${position}`);
27 | const scripts = await db
28 | .select({
29 | id: headScripts.id,
30 | content: headScripts.code,
31 | position: headScripts.position,
32 | })
33 | .from(headScripts)
34 | .where(
35 | and(
36 | eq(headScripts.position, position),
37 | eq(headScripts.isActive, 1)
38 | )
39 | );
40 |
41 | console.log(`[API /api/settings/scripts/position] Database query returned ${scripts.length} scripts for position: ${position}`);
42 | // console.log('[API /api/settings/scripts/position] Scripts data:', JSON.stringify(scripts)); // Optional: Uncomment for detailed data
43 |
44 | return NextResponse.json({
45 | success: true,
46 | scripts,
47 | });
48 | } catch (error) {
49 | console.error(`[API /api/settings/scripts/position] Error fetching scripts:`, error);
50 | return NextResponse.json(
51 | {
52 | success: false,
53 | message: '获取脚本失败',
54 | error: String(error)
55 | },
56 | { status: 500 }
57 | );
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/app/api/settings/scripts/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server';
2 | import { db } from '@/lib/db';
3 | import { headScripts } from '@/lib/schema/settings';
4 | import { eq } from 'drizzle-orm';
5 |
6 | // 获取所有脚本
7 | export async function GET() {
8 | try {
9 | console.log('API: 开始获取脚本');
10 | const scripts = await db.select().from(headScripts).orderBy(headScripts.order);
11 | console.log('API: 获取到脚本数量:', scripts.length);
12 | console.log('API: 脚本列表:', scripts);
13 |
14 | return NextResponse.json({
15 | success: true,
16 | data: {
17 | scripts
18 | }
19 | });
20 | } catch (error) {
21 | console.error('API: 获取脚本失败:', error);
22 | return NextResponse.json({ error: '获取脚本失败' }, { status: 500 });
23 | }
24 | }
25 |
26 | // 创建新脚本
27 | export async function POST(request: Request) {
28 | try {
29 | const data = await request.json();
30 |
31 | // 验证必填字段
32 | if (!data.name || !data.code) {
33 | return NextResponse.json({ error: '名称和代码是必填项' }, { status: 400 });
34 | }
35 |
36 | // 创建新脚本
37 | const result = await db.insert(headScripts).values({
38 | name: data.name,
39 | description: data.description || null,
40 | code: data.code,
41 | type: data.type || 'custom',
42 | isActive: data.isActive !== undefined ? data.isActive : 1,
43 | position: data.position || 'head',
44 | pages: data.pages || null,
45 | order: data.order || 0,
46 | updatedAt: new Date().toISOString(),
47 | });
48 |
49 | return NextResponse.json({
50 | success: true,
51 | message: '脚本创建成功',
52 | data: result
53 | });
54 | } catch (error) {
55 | console.error('创建脚本失败:', error);
56 | return NextResponse.json({ error: '创建脚本失败' }, { status: 500 });
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/app/api/tags/all/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from 'next/server';
2 | import { db } from '@/lib/db';
3 | import * as schema from '@/lib/schema';
4 | import { desc, count, sql, eq } from 'drizzle-orm';
5 |
6 | /**
7 | * 获取所有标签列表,支持分页
8 | * @param request 请求对象
9 | * @returns 响应对象
10 | */
11 | export async function GET(request: NextRequest) {
12 | try {
13 | // 获取查询参数
14 | const { searchParams } = new URL(request.url);
15 | const page = parseInt(searchParams.get('page') || '1', 10);
16 | const pageSize = parseInt(searchParams.get('pageSize') || '50', 10);
17 |
18 | // 计算分页偏移量
19 | const offset = (page - 1) * pageSize;
20 |
21 | // 获取标签总数
22 | const totalCountResult = await db
23 | .select({ count: count() })
24 | .from(schema.tags)
25 | .all();
26 |
27 | const totalTags = totalCountResult[0]?.count || 0;
28 | const totalPages = Math.ceil(totalTags / pageSize);
29 |
30 | // 获取分页标签数据
31 | const tags = await db
32 | .select({
33 | id: schema.tags.id,
34 | name: schema.tags.name,
35 | slug: schema.tags.slug,
36 | description: schema.tags.description,
37 | postCount: count(schema.postTags.postId)
38 | })
39 | .from(schema.tags)
40 | .leftJoin(schema.postTags, eq(schema.tags.id, schema.postTags.tagId))
41 | .leftJoin(schema.posts, eq(schema.postTags.postId, schema.posts.id))
42 | .where(eq(schema.posts.published, 1)) // 只计算已发布的文章
43 | .groupBy(schema.tags.id)
44 | .orderBy(desc(count(schema.postTags.postId)))
45 | .limit(pageSize)
46 | .offset(offset)
47 | .all();
48 |
49 | return NextResponse.json({
50 | tags,
51 | pagination: {
52 | page,
53 | pageSize,
54 | totalTags,
55 | totalPages
56 | }
57 | });
58 | } catch (error) {
59 | console.error('获取标签列表失败:', error);
60 | return NextResponse.json(
61 | {
62 | success: false,
63 | message: '获取标签列表失败',
64 | error: String(error)
65 | },
66 | { status: 500 }
67 | );
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/app/api/users/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from 'next/server';
2 | import { db } from '@/lib/db';
3 | import { sql } from 'drizzle-orm';
4 | import bcrypt from 'bcryptjs';
5 |
6 | // 获取所有用户
7 | export async function GET() {
8 | try {
9 | // 使用sqlite直接查询以确保返回数组
10 | const { sqlite } = await import('@/lib/db');
11 | const users = sqlite.prepare(`
12 | SELECT id, email, createdAt
13 | FROM users
14 | ORDER BY id ASC
15 | `).all();
16 |
17 | return NextResponse.json({ success: true, users });
18 | } catch (error) {
19 | console.error('获取用户列表失败:', error);
20 | return NextResponse.json(
21 | { success: false, error: '获取用户列表失败' },
22 | { status: 500 }
23 | );
24 | }
25 | }
26 |
27 | // 创建新用户
28 | export async function POST(request: NextRequest) {
29 | try {
30 | const { email, password } = await request.json();
31 |
32 | // 验证输入
33 | if (!email || !password) {
34 | return NextResponse.json(
35 | { success: false, error: '邮箱和密码不能为空' },
36 | { status: 400 }
37 | );
38 | }
39 |
40 | // 使用sqlite直接查询检查邮箱是否已存在
41 | const { sqlite } = await import('@/lib/db');
42 | const existingUser = sqlite.prepare(`
43 | SELECT id FROM users WHERE email = ?
44 | `).get(email);
45 |
46 | if (existingUser) {
47 | return NextResponse.json(
48 | { success: false, error: '该邮箱已被注册' },
49 | { status: 400 }
50 | );
51 | }
52 |
53 | // 加密密码
54 | const salt = await bcrypt.genSalt(10);
55 | const hashedPassword = await bcrypt.hash(password, salt);
56 |
57 | // 创建用户
58 | sqlite.prepare(`
59 | INSERT INTO users (email, password, createdAt)
60 | VALUES (?, ?, datetime('now'))
61 | `).run(email, hashedPassword);
62 |
63 | return NextResponse.json({ success: true, message: '用户创建成功' });
64 | } catch (error) {
65 | console.error('创建用户失败:', error);
66 | return NextResponse.json(
67 | { success: false, error: '创建用户失败' },
68 | { status: 500 }
69 | );
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/app/api/webhooks/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from 'next/server';
2 | import { webhooks } from '@/lib/schema/links';
3 | import { eq } from 'drizzle-orm';
4 | import { drizzle } from 'drizzle-orm/better-sqlite3';
5 | import Database from 'better-sqlite3';
6 | import path from 'path';
7 |
8 | // 初始化数据库连接
9 | function getDB() {
10 | try {
11 | const dbPath = path.join(process.cwd(), 'links.db');
12 | const sqlite = new Database(dbPath);
13 | return drizzle(sqlite);
14 | } catch (error) {
15 | console.error('Failed to connect to links database:', error);
16 | throw new Error('数据库连接失败');
17 | }
18 | }
19 |
20 | // GET - 获取所有Webhook
21 | export async function GET() {
22 | try {
23 | // 初始化数据库连接
24 | const db = getDB();
25 |
26 | const allWebhooks = await db.select().from(webhooks);
27 |
28 | return NextResponse.json({
29 | success: true,
30 | webhooks: allWebhooks
31 | });
32 | } catch (error) {
33 | console.error('获取Webhook列表失败:', error);
34 | return NextResponse.json(
35 | { error: '获取Webhook列表失败', details: error instanceof Error ? error.message : String(error) },
36 | { status: 500 }
37 | );
38 | }
39 | }
40 |
41 | // POST - 创建新Webhook
42 | export async function POST(request: NextRequest) {
43 | try {
44 | // 初始化数据库连接
45 | const db = getDB();
46 |
47 | const data = await request.json();
48 |
49 | // 验证必填字段
50 | if (!data.url) {
51 | return NextResponse.json(
52 | { error: 'Webhook URL为必填项' },
53 | { status: 400 }
54 | );
55 | }
56 |
57 | // 创建新Webhook
58 | const result = await db.insert(webhooks).values({
59 | url: data.url,
60 | secret: data.secret || null,
61 | isActive: data.isActive !== undefined ? data.isActive : 1,
62 | createdAt: new Date().toISOString(),
63 | updatedAt: new Date().toISOString()
64 | }).returning();
65 |
66 | return NextResponse.json({
67 | success: true,
68 | message: 'Webhook创建成功',
69 | webhook: result[0]
70 | });
71 | } catch (error) {
72 | console.error('创建Webhook失败:', error);
73 | return NextResponse.json(
74 | { error: '创建Webhook失败', details: error instanceof Error ? error.message : String(error) },
75 | { status: 500 }
76 | );
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/app/categories/[slug]/page.tsx:
--------------------------------------------------------------------------------
1 | import { getMenus, getSiteSettings, getCategories, getTags } from '@/lib/services/settings';
2 | import SimpleNavigation from '@/components/SimpleNavigation';
3 | import SimpleFooter from '@/components/SimpleFooter';
4 | import CategoryPostsClient from './page.client';
5 | import Sidebar from '@/components/Sidebar';
6 | import { adaptMenus } from '@/lib/utils/menu-adapters';
7 |
8 | // 定义MenuItem类型,与SimpleNavigation组件期望的类型一致
9 | type MenuItem = {
10 | id: number;
11 | name: string;
12 | url: string; // 非空字符串
13 | isExternal: number;
14 | parentId: number | null;
15 | order: number;
16 | isActive: number;
17 | };
18 |
19 | export default async function CategoryPage() {
20 | // 获取菜单、网站设置、分类和标签
21 | const [menus, settings, categories, tags] = await Promise.all([
22 | getMenus(),
23 | getSiteSettings(),
24 | getCategories(),
25 | getTags()
26 | ]);
27 |
28 | // 获取网站名称
29 | const siteTitle = settings['site_name'] || '向阳乔木的个人博客';
30 |
31 | return (
32 |
33 |
34 |
35 |
36 |
37 |
38 | {/* 左侧内容区 */}
39 |
40 |
41 |
42 |
43 | {/* 右侧边栏 */}
44 |
45 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 | );
60 | }
61 |
--------------------------------------------------------------------------------
/src/app/direct-login/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useEffect, useState } from 'react';
4 | import { signIn } from 'next-auth/react';
5 | import { useRouter } from 'next/navigation';
6 |
7 | export default function DirectLoginPage() {
8 | const [status, setStatus] = useState('准备登录...');
9 | const [error, setError] = useState(null);
10 | const router = useRouter();
11 |
12 | useEffect(() => {
13 | async function performLogin() {
14 | try {
15 | setStatus('正在登录...');
16 |
17 | // 使用固定的登录凭据
18 | const result = await signIn('credentials', {
19 | redirect: false,
20 | email: 'vista8@gmail.com',
21 | password: 'qq778899',
22 | });
23 |
24 | console.log('登录结果:', result);
25 |
26 | if (result?.error) {
27 | setError(result.error);
28 | setStatus('登录失败');
29 | } else {
30 | setStatus('登录成功,正在跳转...');
31 |
32 | // 使用window.location直接跳转,避免Next.js路由问题
33 | window.location.href = '/admin';
34 | }
35 | } catch (err) {
36 | console.error('登录错误:', err);
37 | setError(err instanceof Error ? err.message : '未知错误');
38 | setStatus('登录过程中发生错误');
39 | }
40 | }
41 |
42 | performLogin();
43 | }, [router]);
44 |
45 | return (
46 |
47 |
48 |
自动登录
49 |
50 |
51 | {status}
52 |
53 |
54 | {error && (
55 |
56 | {error}
57 |
58 | )}
59 |
60 |
61 | );
62 | }
63 |
--------------------------------------------------------------------------------
/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joeseesun/qiaomu-blog3/eb82b946aa1cf52d165b6d8cc4b99443352dae1b/src/app/favicon.ico
--------------------------------------------------------------------------------
/src/app/links/page.tsx:
--------------------------------------------------------------------------------
1 | import { getMenus, getSiteSettings, getCategories, getTags } from '@/lib/services/settings';
2 | import SimpleNavigation from '@/components/SimpleNavigation';
3 | import SimpleFooter from '@/components/SimpleFooter';
4 | import Sidebar from '@/components/Sidebar';
5 | import { adaptMenus } from '@/lib/utils/menu-adapters';
6 | import LinksClient from './page.client';
7 |
8 | export default async function LinksPage() {
9 | // 获取菜单、网站设置、分类和标签
10 | const [menus, settings, categories, tags] = await Promise.all([
11 | getMenus(null),
12 | getSiteSettings(),
13 | getCategories(),
14 | getTags()
15 | ]);
16 |
17 | // 获取网站名称
18 | const siteTitle = settings['site_name'] || '向阳乔木的个人博客';
19 |
20 | return (
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 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 | );
60 | }
61 |
--------------------------------------------------------------------------------
/src/app/login/layout.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { Geist } from "next/font/google";
4 | import "@/app/globals.css";
5 | import { SessionProvider } from "@/components/providers/SessionProvider";
6 |
7 | const geistSans = Geist({
8 | variable: "--font-geist-sans",
9 | subsets: ["latin"],
10 | });
11 |
12 | export default function LoginLayout({
13 | children,
14 | }: Readonly<{
15 | children: React.ReactNode;
16 | }>) {
17 | return (
18 |
19 |
20 | {children}
21 |
22 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | import { Metadata } from 'next';
2 | import SimpleNavigation from '@/components/SimpleNavigation';
3 | import SimpleFooter from '@/components/SimpleFooter';
4 | import Sidebar from '@/components/Sidebar';
5 | import { getCategories, getTags, getMenus, getSiteSettings } from '@/lib/services/settings';
6 | import { adaptMenus } from '@/lib/utils/menu-adapters';
7 | import FeaturedSliderClient from '@/components/FeaturedSliderClient';
8 | import LatestArticlesClient from '@/components/LatestArticlesClient';
9 |
10 | // 使用 segmentCache 配置,这是Next.js 15.2.4中推荐的缓存控制方法
11 | export const segmentCache = { revalidate: 0 };
12 |
13 | export const metadata: Metadata = {
14 | title: '向阳乔木的个人博客',
15 | description: '分享技术、生活和思考,记录成长的点滴。',
16 | };
17 |
18 | export default async function Home() {
19 | // 获取分类、标签和菜单
20 | const [categories, tags, menus, settings] = await Promise.all([
21 | getCategories(),
22 | getTags(),
23 | getMenus(),
24 | getSiteSettings(),
25 | ]);
26 |
27 | // 调试输出菜单数据
28 | console.log('首页获取到的菜单数据:', JSON.stringify(menus, null, 2));
29 |
30 | // 获取网站设置
31 | const siteSettings = Array.isArray(settings)
32 | ? settings.reduce((acc: Record, setting: { key: string, value: string | null }) => {
33 | acc[setting.key] = setting.value;
34 | return acc;
35 | }, {} as Record)
36 | : settings;
37 |
38 | return (
39 |
40 |
41 |
42 |
43 |
44 | {/* 特色文章轮播 - 使用客户端组件 */}
45 |
46 |
47 |
48 |
49 |
50 | {/* 左侧内容区 - 使用客户端组件 */}
51 |
52 |
53 |
54 |
55 | {/* 右侧边栏 */}
56 |
57 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 | );
73 | }
74 |
--------------------------------------------------------------------------------
/src/app/posts/page.tsx:
--------------------------------------------------------------------------------
1 | import { Metadata } from 'next';
2 | import { getMenus, getSiteSettings, getCategories, getTags } from '@/lib/services/settings';
3 | import { db } from '@/lib/db';
4 | import SimpleNavigation from '@/components/SimpleNavigation';
5 | import SimpleFooter from '@/components/SimpleFooter';
6 | import AllPostsClient from './page.client';
7 | import Sidebar from '@/components/Sidebar';
8 | import { adaptMenus } from '@/lib/utils/menu-adapters';
9 |
10 | export const metadata: Metadata = {
11 | title: '文章列表 - 向阳乔木的个人博客',
12 | description: '向阳乔木的个人博客文章列表,分享技术、生活和思考。',
13 | };
14 |
15 | // 强制动态渲染,确保每次访问都获取最新数据
16 | export const dynamic = 'force-dynamic';
17 |
18 | export default async function AllPostsPage({
19 | searchParams,
20 | }: {
21 | searchParams: Promise<{ q?: string; category?: string; tag?: string; page?: string }>;
22 | }) {
23 | // 获取查询参数
24 | const params = await searchParams;
25 | const query = params?.q || '';
26 | const categoryId = params?.category || '';
27 | const tagId = params?.tag || '';
28 | const page = parseInt(params?.page || '1', 10);
29 |
30 | // 获取菜单、网站设置、分类和标签
31 | const [menus, settings, categories, tags] = await Promise.all([
32 | getMenus(),
33 | getSiteSettings(),
34 | getCategories(),
35 | getTags()
36 | ]);
37 |
38 | // 获取网站名称
39 | const siteTitle = settings['site_name'] || '向阳乔木的个人博客';
40 |
41 | return (
42 |
43 |
44 |
45 |
46 |
47 |
48 | {/* 左侧内容区 */}
49 |
57 |
58 | {/* 右侧边栏 */}
59 |
60 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 | );
76 | }
77 |
--------------------------------------------------------------------------------
/src/app/search/page.tsx:
--------------------------------------------------------------------------------
1 | import { Metadata } from 'next';
2 | import { getMenus, getSiteSettings, getCategories, getTags } from '@/lib/services/settings';
3 | import { db } from '@/lib/db';
4 | import SimpleNavigation from '@/components/SimpleNavigation';
5 | import SimpleFooter from '@/components/SimpleFooter';
6 | import SearchClient from './page.client';
7 | import Sidebar from '@/components/Sidebar';
8 | import { adaptMenus } from '@/lib/utils/menu-adapters';
9 |
10 | export const metadata: Metadata = {
11 | title: '搜索结果 - 向阳乔木的个人博客',
12 | description: '搜索向阳乔木的个人博客上的文章',
13 | };
14 |
15 | export default async function SearchPage({
16 | searchParams,
17 | }: {
18 | searchParams: Promise<{ q?: string; category?: string; tag?: string; page?: string }>;
19 | }) {
20 | // 在 Next.js 中,searchParams 是一个只读对象,需要先解构出来
21 | const params = await searchParams;
22 | const query = params.q || '';
23 | const categoryId = params.category || '';
24 | const tagId = params.tag || '';
25 | const page = parseInt(params.page || '1', 10);
26 |
27 | // 获取菜单、网站设置、分类和标签
28 | const [menus, settings, categories, tags] = await Promise.all([
29 | getMenus(),
30 | getSiteSettings(),
31 | getCategories(),
32 | getTags()
33 | ]);
34 |
35 | // 获取网站名称
36 | const siteTitle = settings['site_name'] || '向阳乔木的个人博客';
37 |
38 | return (
39 |
40 |
41 |
42 |
43 |
44 |
45 | {/* 左侧内容区 */}
46 |
47 |
53 |
54 |
55 | {/* 右侧边栏 */}
56 |
57 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 | );
73 | }
74 |
--------------------------------------------------------------------------------
/src/app/simple/page.tsx:
--------------------------------------------------------------------------------
1 | export default function SimplePage() {
2 | return (
3 |
4 | {/* 头部导航 */}
5 |
16 |
17 | {/* Hero区域 */}
18 |
19 |
20 |
21 |
向阳乔木个人网站
22 |
分享AI探索、实践,精选各类工具,一起学习进步。
23 |
24 |
25 |
26 |
27 | {/* 主内容区 */}
28 |
29 |
30 |
简化版主页
31 |
这是一个简化版的主页,用于测试是否有遮罩层问题。
32 |
33 |
34 |
35 | {/* 页脚 */}
36 |
41 |
42 | );
43 | }
44 |
--------------------------------------------------------------------------------
/src/app/tags/[slug]/page.tsx:
--------------------------------------------------------------------------------
1 | import { db } from '@/lib/db';
2 | import * as schema from '@/lib/schema';
3 | import { eq } from 'drizzle-orm';
4 | import { notFound } from 'next/navigation';
5 | import SimpleNavigation from '@/components/SimpleNavigation';
6 | import SimpleFooter from '@/components/SimpleFooter';
7 | import { getMenus, getSiteSettings, getCategories, getTags } from '@/lib/services/settings';
8 | import { adaptMenus } from '@/lib/utils/menu-adapters';
9 | import TagPostsClient from './page.client';
10 | import Sidebar from '@/components/Sidebar';
11 |
12 | export default async function TagPage() {
13 |
14 | // 获取菜单、网站设置、分类和标签
15 | const [menus, settings, categories, tags] = await Promise.all([
16 | getMenus(),
17 | getSiteSettings(),
18 | getCategories(),
19 | getTags()
20 | ]);
21 |
22 | // 获取网站名称
23 | const siteTitle = settings['site_name'] || '向阳乔木的个人博客';
24 |
25 | // 不再在服务器组件中获取标签信息
26 |
27 | // 不需要获取标签下的文章,因为我们使用客户端组件通过 API 获取
28 |
29 | return (
30 |
31 |
32 |
33 |
34 |
35 |
36 | {/* 左侧内容区 */}
37 |
38 |
39 |
40 |
41 | {/* 右侧边栏 */}
42 |
43 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | );
58 | }
59 |
--------------------------------------------------------------------------------
/src/app/tags/page.tsx:
--------------------------------------------------------------------------------
1 | import { Metadata } from 'next';
2 | import { getMenus, getSiteSettings, getCategories, getTags } from '@/lib/services/settings';
3 | import SimpleNavigation from '@/components/SimpleNavigation';
4 | import SimpleFooter from '@/components/SimpleFooter';
5 | import TagsClient from './page.client';
6 | import Sidebar from '@/components/Sidebar';
7 | import { adaptMenus } from '@/lib/utils/menu-adapters';
8 |
9 | export const metadata: Metadata = {
10 | title: '标签列表 - 向阳乔木的个人博客',
11 | description: '向阳乔木的个人博客标签列表,按主题浏览文章内容。',
12 | };
13 |
14 | // 强制动态渲染,确保每次访问都获取最新数据
15 | export const dynamic = 'force-dynamic';
16 |
17 | export default async function AllTagsPage({
18 | searchParams,
19 | }: {
20 | searchParams: Promise<{ page?: string }>;
21 | }) {
22 | // 获取查询参数
23 | const params = await searchParams;
24 | const page = parseInt(params?.page || '1', 10);
25 |
26 | // 获取菜单、网站设置、分类和标签
27 | const [menus, settings, categories, tags] = await Promise.all([
28 | getMenus(),
29 | getSiteSettings(),
30 | getCategories(),
31 | getTags()
32 | ]);
33 |
34 | // 获取网站名称
35 | const siteTitle = settings['site_name'] || '向阳乔木的个人博客';
36 |
37 | return (
38 |
39 |
40 |
41 |
42 |
43 |
44 | {/* 左侧内容区 */}
45 |
46 |
47 |
48 |
49 | {/* 右侧边栏 */}
50 |
51 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 | );
67 | }
68 |
--------------------------------------------------------------------------------
/src/components/AdminCheck.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useState, useEffect } from 'react';
4 |
5 | // 客户端检查管理员状态
6 | export default function useAdminCheck() {
7 | const [isAdmin, setIsAdmin] = useState(false);
8 |
9 | useEffect(() => {
10 | // 检查cookie中是否有管理员标记
11 | const hasAdminCookie = document.cookie.split(';').some(item => item.trim().startsWith('admin_logged_in='));
12 | setIsAdmin(hasAdminCookie);
13 | }, []);
14 |
15 | return isAdmin;
16 | }
17 |
--------------------------------------------------------------------------------
/src/components/AdminPublishLink.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useSession } from 'next-auth/react';
4 | import Link from 'next/link';
5 | import { PenLine } from 'lucide-react';
6 |
7 | interface AdminPublishLinkProps {
8 | isMobile?: boolean;
9 | }
10 |
11 | export default function AdminPublishLink({ isMobile = false }: AdminPublishLinkProps) {
12 | const { data: session } = useSession();
13 |
14 | // 如果用户未登录,不显示发布链接
15 | if (!session?.user) {
16 | return null;
17 | }
18 |
19 | if (isMobile) {
20 | return (
21 |
25 |
26 | 发布文章
27 |
28 | );
29 | }
30 |
31 | return (
32 |
37 |
38 |
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/src/components/Avatar.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useState } from 'react';
4 |
5 | type AvatarProps = {
6 | src: string;
7 | alt: string;
8 | className?: string;
9 | fallbackText?: string;
10 | };
11 |
12 | export default function Avatar({
13 | src,
14 | alt,
15 | className = "w-full h-full object-cover",
16 | fallbackText
17 | }: AvatarProps) {
18 | const [error, setError] = useState(false);
19 |
20 | // 生成SVG占位符
21 | const generateFallbackSvg = () => {
22 | const text = fallbackText || alt;
23 | return `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'%3E%3Crect width='100' height='100' fill='%23f0f0f0'/%3E%3Ctext x='50' y='50' font-family='Arial' font-size='20' text-anchor='middle' dominant-baseline='middle' fill='%23999'%3E${encodeURIComponent(text)}%3C/text%3E%3C/svg%3E`;
24 | };
25 |
26 | return (
27 |
setError(true)}
32 | />
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/src/components/EditPostLink.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useSession } from 'next-auth/react';
4 | import Link from 'next/link';
5 |
6 | interface EditPostLinkProps {
7 | postId: number;
8 | }
9 |
10 | export default function EditPostLink({ postId }: EditPostLinkProps) {
11 | const { data: session } = useSession();
12 |
13 | // 如果用户未登录,不显示编辑链接
14 | if (!session?.user) {
15 | return null;
16 | }
17 |
18 | return (
19 |
23 | 编辑
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/src/components/MainLayout.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { ReactNode, useEffect, useState } from 'react';
4 | import Navigation from './Navigation';
5 | import Footer from './Footer';
6 | import { usePathname } from 'next/navigation';
7 | import { SocialLink, ContactInfo, DonationInfo } from '@/lib/schema';
8 |
9 | type MainLayoutProps = {
10 | children: ReactNode;
11 | };
12 |
13 | export default function MainLayout({ children }: MainLayoutProps) {
14 | const pathname = usePathname();
15 | const isAdminPage = pathname.startsWith('/admin');
16 |
17 | const [socialLinks, setSocialLinks] = useState([]);
18 | const [contactInfo, setContactInfo] = useState([]);
19 | const [donationInfo, setDonationInfo] = useState([]);
20 | const [siteSettings, setSiteSettings] = useState>({});
21 | const [loading, setLoading] = useState(true);
22 |
23 | // 获取网站设置
24 | useEffect(() => {
25 | if (!isAdminPage) {
26 | const fetchSettings = async () => {
27 | try {
28 | const response = await fetch('/api/settings');
29 | if (response.ok) {
30 | const data = await response.json();
31 | if (data.success) {
32 | setSocialLinks(data.data.socialLinks || []);
33 | setContactInfo(data.data.contactInfo || []);
34 | setDonationInfo(data.data.donationInfo || []);
35 | setSiteSettings(data.data.settings || {});
36 | }
37 | }
38 | } catch (error) {
39 | console.error('获取网站设置失败:', error);
40 | } finally {
41 | setLoading(false);
42 | }
43 | };
44 |
45 | fetchSettings();
46 | }
47 | }, [isAdminPage]);
48 |
49 | // 不在管理页面中渲染此布局
50 | if (isAdminPage) {
51 | return <>{children}>;
52 | }
53 |
54 | return (
55 |
56 |
59 |
60 | {children}
61 |
62 |
70 |
71 | );
72 | }
73 |
--------------------------------------------------------------------------------
/src/components/NextScriptLoader.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React, { useEffect } from 'react';
4 | import Script from 'next/script';
5 |
6 | type NextScriptLoaderProps = {
7 | position: 'head' | 'body_start' | 'body_end';
8 | };
9 |
10 | /**
11 | * 改进版脚本加载组件
12 | * 使用 Next.js 的 Script 组件加载脚本,与动态渲染兼容
13 | */
14 | export default function NextScriptLoader({ position }: NextScriptLoaderProps) {
15 | const [scripts, setScripts] = React.useState<{ id: number; content: string; position: string }[]>([]);
16 |
17 | useEffect(() => {
18 | // 在客户端获取脚本
19 | const fetchScripts = async () => {
20 | try {
21 | // 使用fetch API获取脚本,而不是直接使用db
22 | const response = await fetch(`/api/settings/scripts/position?position=${position}`);
23 | if (response.ok) {
24 | const data = await response.json();
25 | if (data.success && Array.isArray(data.scripts)) {
26 | setScripts(data.scripts);
27 | }
28 | }
29 | } catch (error) {
30 | console.error('获取脚本失败:', error);
31 | }
32 | };
33 |
34 | fetchScripts();
35 | }, [position]);
36 |
37 | return (
38 | <>
39 | {scripts.map((script) => {
40 | // Make sure script content is a string
41 | const scriptContent = typeof script.content === 'string' ? script.content : '';
42 |
43 | return (
44 |
51 | );
52 | })}
53 | >
54 | );
55 | }
56 |
--------------------------------------------------------------------------------
/src/components/ScriptLoader.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from 'react';
4 |
5 | type ScriptLoaderProps = {
6 | position: 'head' | 'body_start' | 'body_end';
7 | key?: string;
8 | };
9 |
10 | /**
11 | * 简化版ScriptLoader组件
12 | * 使用 fetchCache = 'force-no-store' 配置代替 dynamic = 'force-dynamic'
13 | * 这种方法在数据获取层面禁用缓存,而不是渲染层面,因此不会与客户端组件冲突
14 | */
15 | export default function ScriptLoader({ position }: ScriptLoaderProps) {
16 | // 在服务器端渲染时返回null
17 | if (typeof window === 'undefined') {
18 | return null;
19 | }
20 |
21 | // 暂时返回空元素,避免与动态渲染冲突
22 | return null;
23 | }
24 |
--------------------------------------------------------------------------------
/src/components/ScriptLoaderWrapper.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React from 'react';
4 | import NextScriptLoader from './NextScriptLoader';
5 |
6 | type ScriptLoaderWrapperProps = {
7 | position: 'head' | 'body_start' | 'body_end';
8 | };
9 |
10 | export default function ScriptLoaderWrapper({ position }: ScriptLoaderWrapperProps) {
11 | // Temporarily disabled to troubleshoot navigation menu issues
12 | console.log(`ScriptLoader for ${position} temporarily disabled`);
13 | return null;
14 | }
15 |
--------------------------------------------------------------------------------
/src/components/admin/header.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Link from 'next/link';
3 | import { usePathname, useRouter } from 'next/navigation';
4 | import { User, LogOut } from 'lucide-react';
5 | import { Button } from '@/components/ui/button';
6 |
7 | export function Header() {
8 | const pathname = usePathname();
9 | const router = useRouter();
10 |
11 | // Get the current page title based on the pathname
12 | const getPageTitle = () => {
13 | const path = pathname?.split('/').filter(Boolean);
14 |
15 | if (!path || path.length === 1) return '仪表盘';
16 |
17 | const pageTitles: Record = {
18 | 'posts': '文章管理',
19 | 'categories': '分类管理',
20 | 'tags': '标签管理',
21 | 'menus': '菜单管理',
22 | 'settings': '系统设置',
23 | };
24 |
25 | return pageTitles[path[1]] || '仪表盘';
26 | };
27 |
28 | // 处理登出
29 | const handleLogout = async () => {
30 | try {
31 | const response = await fetch('/api/logout');
32 | const data = await response.json();
33 | if (data.success) {
34 | router.push('/login');
35 | }
36 | } catch (error) {
37 | // 静默处理错误,避免在Edge Runtime中使用console
38 | router.push('/login');
39 | }
40 | };
41 |
42 | return (
43 |
65 | );
66 | }
67 |
--------------------------------------------------------------------------------
/src/components/providers/SessionProvider.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { SessionProvider as NextAuthSessionProvider } from 'next-auth/react';
4 | import { ReactNode } from 'react';
5 |
6 | type SessionProviderProps = {
7 | children: ReactNode;
8 | };
9 |
10 | export function SessionProvider({ children }: SessionProviderProps) {
11 | return {children};
12 | }
--------------------------------------------------------------------------------
/src/components/ui/alert.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { cva, type VariantProps } from "class-variance-authority"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const alertVariants = cva(
9 | "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
10 | {
11 | variants: {
12 | variant: {
13 | default: "bg-background text-foreground",
14 | destructive:
15 | "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
16 | },
17 | },
18 | defaultVariants: {
19 | variant: "default",
20 | },
21 | }
22 | )
23 |
24 | const Alert = React.forwardRef<
25 | HTMLDivElement,
26 | React.HTMLAttributes & VariantProps
27 | >(({ className, variant, ...props }, ref) => (
28 |
34 | ))
35 | Alert.displayName = "Alert"
36 |
37 | const AlertTitle = React.forwardRef<
38 | HTMLParagraphElement,
39 | React.HTMLAttributes
40 | >(({ className, ...props }, ref) => (
41 |
46 | ))
47 | AlertTitle.displayName = "AlertTitle"
48 |
49 | const AlertDescription = React.forwardRef<
50 | HTMLParagraphElement,
51 | React.HTMLAttributes
52 | >(({ className, ...props }, ref) => (
53 |
58 | ))
59 | AlertDescription.displayName = "AlertDescription"
60 |
61 | export { Alert, AlertTitle, AlertDescription }
62 |
--------------------------------------------------------------------------------
/src/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cva, type VariantProps } from "class-variance-authority"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const badgeVariants = cva(
7 | "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
8 | {
9 | variants: {
10 | variant: {
11 | default:
12 | "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
13 | secondary:
14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
15 | destructive:
16 | "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
17 | outline: "text-foreground",
18 | success:
19 | "border-transparent bg-green-100 text-green-800 hover:bg-green-200/80",
20 | warning:
21 | "border-transparent bg-amber-100 text-amber-800 hover:bg-amber-200/80",
22 | info:
23 | "border-transparent bg-blue-100 text-blue-800 hover:bg-blue-200/80",
24 | },
25 | },
26 | defaultVariants: {
27 | variant: "default",
28 | },
29 | }
30 | )
31 |
32 | export interface BadgeProps
33 | extends React.HTMLAttributes,
34 | VariantProps {}
35 |
36 | function Badge({ className, variant, ...props }: BadgeProps) {
37 | return (
38 |
39 | )
40 | }
41 |
42 | export { Badge, badgeVariants }
43 |
--------------------------------------------------------------------------------
/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-primary text-primary-foreground hover:bg-primary/90",
13 | destructive:
14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90",
15 | outline:
16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
17 | secondary:
18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19 | ghost: "hover:bg-accent hover:text-accent-foreground",
20 | link: "text-primary underline-offset-4 hover:underline",
21 | },
22 | size: {
23 | default: "h-10 px-4 py-2",
24 | sm: "h-9 rounded-md px-3",
25 | lg: "h-11 rounded-md px-8",
26 | icon: "h-10 w-10",
27 | },
28 | },
29 | defaultVariants: {
30 | variant: "default",
31 | size: "default",
32 | },
33 | }
34 | )
35 |
36 | export interface ButtonProps
37 | extends React.ButtonHTMLAttributes,
38 | VariantProps {
39 | asChild?: boolean
40 | }
41 |
42 | const Button = React.forwardRef(
43 | ({ className, variant, size, asChild = false, ...props }, ref) => {
44 | const Comp = asChild ? Slot : "button"
45 | return (
46 |
51 | )
52 | }
53 | )
54 | Button.displayName = "Button"
55 |
56 | export { Button, buttonVariants }
57 |
--------------------------------------------------------------------------------
/src/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
17 | ))
18 | Card.displayName = "Card"
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ))
30 | CardHeader.displayName = "CardHeader"
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLParagraphElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
44 | ))
45 | CardTitle.displayName = "CardTitle"
46 |
47 | const CardDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ))
57 | CardDescription.displayName = "CardDescription"
58 |
59 | const CardContent = React.forwardRef<
60 | HTMLDivElement,
61 | React.HTMLAttributes
62 | >(({ className, ...props }, ref) => (
63 |
64 | ))
65 | CardContent.displayName = "CardContent"
66 |
67 | const CardFooter = React.forwardRef<
68 | HTMLDivElement,
69 | React.HTMLAttributes
70 | >(({ className, ...props }, ref) => (
71 |
76 | ))
77 | CardFooter.displayName = "CardFooter"
78 |
79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
80 |
--------------------------------------------------------------------------------
/src/components/ui/checkbox.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
5 | import { Check } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Checkbox = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => (
13 |
21 |
24 |
25 |
26 |
27 | ))
28 | Checkbox.displayName = CheckboxPrimitive.Root.displayName
29 |
30 | export { Checkbox }
31 |
--------------------------------------------------------------------------------
/src/components/ui/icons.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import {
4 | HomeIcon as HeroHomeIcon,
5 | DocumentTextIcon as HeroDocumentTextIcon,
6 | TagIcon as HeroTagIcon,
7 | Bars3Icon as HeroBars3Icon,
8 | Cog6ToothIcon as HeroCog6ToothIcon,
9 | ArrowTopRightOnSquareIcon as HeroArrowTopRightOnSquareIcon,
10 | ArrowRightOnRectangleIcon as HeroArrowRightOnRectangleIcon,
11 | FolderIcon as HeroFolderIcon,
12 | UserIcon as HeroUserIcon,
13 | LinkIcon as HeroLinkIcon
14 | } from '@heroicons/react/24/outline';
15 |
16 | export interface IconProps extends React.SVGProps {
17 | className?: string;
18 | }
19 |
20 | export function HomeIcon(props: IconProps) {
21 | return ;
22 | }
23 |
24 | export function DocumentTextIcon(props: IconProps) {
25 | return ;
26 | }
27 |
28 | export function TagIcon(props: IconProps) {
29 | return ;
30 | }
31 |
32 | export function Bars3Icon(props: IconProps) {
33 | return ;
34 | }
35 |
36 | export function Cog6ToothIcon(props: IconProps) {
37 | return ;
38 | }
39 |
40 | export function ArrowTopRightOnSquareIcon(props: IconProps) {
41 | return ;
42 | }
43 |
44 | export function ArrowRightOnRectangleIcon(props: IconProps) {
45 | return ;
46 | }
47 |
48 | export function FolderIcon(props: IconProps) {
49 | return ;
50 | }
51 |
52 | export function UserIcon(props: IconProps) {
53 | return ;
54 | }
55 |
56 | export function LinkIcon(props: IconProps) {
57 | return ;
58 | }
59 |
--------------------------------------------------------------------------------
/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | )
21 | }
22 | )
23 | Input.displayName = "Input"
24 |
25 | export { Input }
26 |
--------------------------------------------------------------------------------
/src/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const labelVariants = cva(
10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
11 | )
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ))
24 | Label.displayName = LabelPrimitive.Root.displayName
25 |
26 | export { Label }
27 |
--------------------------------------------------------------------------------
/src/components/ui/link.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import NextLink from 'next/link';
4 | import { forwardRef } from 'react';
5 |
6 | export interface LinkProps extends React.ComponentPropsWithoutRef {
7 | className?: string;
8 | children: React.ReactNode;
9 | }
10 |
11 | export const Link = forwardRef(
12 | ({ className, children, ...props }, ref) => {
13 | return (
14 |
15 | {children}
16 |
17 | );
18 | }
19 | );
20 |
21 | Link.displayName = 'Link';
22 |
--------------------------------------------------------------------------------
/src/components/ui/modal.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useEffect, useState } from 'react';
4 | import { X } from 'lucide-react';
5 |
6 | interface ModalProps {
7 | isOpen: boolean;
8 | onClose: () => void;
9 | title?: string;
10 | children: React.ReactNode;
11 | }
12 |
13 | export function Modal({ isOpen, onClose, title, children }: ModalProps) {
14 |
15 | useEffect(() => {
16 | console.log('Modal isOpen 状态变化:', isOpen);
17 |
18 | if (isOpen) {
19 | document.body.style.overflow = 'hidden';
20 | } else {
21 | document.body.style.overflow = '';
22 | }
23 |
24 | return () => {
25 | document.body.style.overflow = '';
26 | };
27 | }, [isOpen]);
28 |
29 | if (!isOpen) {
30 | console.log('Modal 未显示');
31 | return null;
32 | }
33 |
34 | console.log('Modal 正在渲染,标题:', title);
35 |
36 | return (
37 |
38 |
42 |
43 | {title && (
44 |
45 |
{title}
46 |
53 |
54 | )}
55 | {!title && (
56 |
57 |
64 |
65 | )}
66 |
67 | {children}
68 |
69 |
70 |
71 | );
72 | }
73 |
--------------------------------------------------------------------------------
/src/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as SeparatorPrimitive from "@radix-ui/react-separator"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const Separator = React.forwardRef<
7 | React.ElementRef,
8 | React.ComponentPropsWithoutRef
9 | >(
10 | (
11 | { className, orientation = "horizontal", decorative = true, ...props },
12 | ref
13 | ) => (
14 |
25 | )
26 | )
27 | Separator.displayName = SeparatorPrimitive.Root.displayName
28 |
29 | export { Separator }
30 |
--------------------------------------------------------------------------------
/src/components/ui/switch.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SwitchPrimitives from "@radix-ui/react-switch"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Switch = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 |
25 |
26 | ))
27 | Switch.displayName = SwitchPrimitives.Root.displayName
28 |
29 | export { Switch }
30 |
--------------------------------------------------------------------------------
/src/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TabsPrimitive from "@radix-ui/react-tabs"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Tabs = TabsPrimitive.Root
9 |
10 | const TabsList = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
22 | ))
23 | TabsList.displayName = TabsPrimitive.List.displayName
24 |
25 | const TabsTrigger = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef
28 | >(({ className, ...props }, ref) => (
29 |
37 | ))
38 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
39 |
40 | const TabsContent = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...props }, ref) => (
44 |
52 | ))
53 | TabsContent.displayName = TabsPrimitive.Content.displayName
54 |
55 | export { Tabs, TabsList, TabsTrigger, TabsContent }
56 |
--------------------------------------------------------------------------------
/src/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface TextareaProps
6 | extends React.TextareaHTMLAttributes {}
7 |
8 | const Textarea = React.forwardRef(
9 | ({ className, ...props }, ref) => {
10 | return (
11 |
19 | )
20 | }
21 | )
22 | Textarea.displayName = "Textarea"
23 |
24 | export { Textarea }
25 |
--------------------------------------------------------------------------------
/src/components/ui/toaster.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import {
4 | Toast,
5 | ToastClose,
6 | ToastDescription,
7 | ToastProvider,
8 | ToastTitle,
9 | ToastViewport,
10 | } from "@/components/ui/toast"
11 | import { useToast } from "@/components/ui/use-toast"
12 | import { type ToasterToast } from "@/components/ui/use-toast"
13 |
14 | export function Toaster() {
15 | const { toasts } = useToast()
16 |
17 | return (
18 |
19 | {toasts.map(function ({ id, title, description, action, ...props }: ToasterToast) {
20 | return (
21 |
22 |
23 | {title && {title}}
24 | {description && (
25 |
26 | {description}
27 |
28 | )}
29 |
30 | {action}
31 |
32 |
33 | )
34 | })}
35 |
36 |
37 | )
38 | }
39 |
--------------------------------------------------------------------------------
/src/components/ui/toggle-group.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
5 | import { cn } from "@/lib/utils"
6 | import { VariantProps, cva } from "class-variance-authority"
7 |
8 | const toggleGroupVariants = cva(
9 | "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
10 | {
11 | variants: {
12 | variant: {
13 | default: "bg-transparent",
14 | outline: "border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
15 | },
16 | size: {
17 | default: "h-10",
18 | sm: "h-9 rounded-md",
19 | lg: "h-11 rounded-md",
20 | },
21 | },
22 | defaultVariants: {
23 | variant: "default",
24 | size: "default",
25 | },
26 | }
27 | )
28 |
29 | const ToggleGroup = React.forwardRef<
30 | React.ElementRef,
31 | React.ComponentPropsWithoutRef &
32 | VariantProps
33 | >(({ className, variant, size, ...props }, ref) => (
34 |
39 | ))
40 |
41 | ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName
42 |
43 | const ToggleGroupItem = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef &
46 | VariantProps
47 | >(({ className, variant, size, ...props }, ref) => (
48 |
57 | ))
58 |
59 | ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName
60 |
61 | export { ToggleGroup, ToggleGroupItem }
62 |
--------------------------------------------------------------------------------
/src/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const TooltipProvider = TooltipPrimitive.Provider
7 |
8 | const Tooltip = TooltipPrimitive.Root
9 |
10 | const TooltipTrigger = TooltipPrimitive.Trigger
11 |
12 | const TooltipContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, sideOffset = 4, ...props }, ref) => (
16 |
25 | ))
26 | TooltipContent.displayName = TooltipPrimitive.Content.displayName
27 |
28 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
29 |
--------------------------------------------------------------------------------
/src/hooks/useSettings.ts:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useState, useEffect } from 'react';
4 |
5 | interface Settings {
6 | siteName?: string;
7 | siteDescription?: string;
8 | siteKeywords?: string;
9 | siteUrl?: string;
10 | heroTitle?: string;
11 | heroSubtitle?: string;
12 | contactQrCode?: string;
13 | rewardQrCode?: string;
14 | footerText?: string;
15 | footerLinks?: Array<{ name: string; url: string }>;
16 | socialLinks?: Array<{ platform: string; url: string; icon?: string }>;
17 | }
18 |
19 | export function useSettings() {
20 | const [settings, setSettings] = useState(null);
21 | const [loading, setLoading] = useState(true);
22 | const [error, setError] = useState(null);
23 |
24 | useEffect(() => {
25 | async function fetchSettings() {
26 | try {
27 | setLoading(true);
28 | const response = await fetch('/api/settings/general');
29 |
30 | if (!response.ok) {
31 | throw new Error('Failed to fetch settings');
32 | }
33 |
34 | const data = await response.json();
35 | setSettings(data);
36 | } catch (err) {
37 | console.error('Error fetching settings:', err);
38 | setError(err instanceof Error ? err : new Error(String(err)));
39 | } finally {
40 | setLoading(false);
41 | }
42 | }
43 |
44 | fetchSettings();
45 | }, []);
46 |
47 | return { settings, loading, error };
48 | }
49 |
--------------------------------------------------------------------------------
/src/lib/db/migrations/0007_add_head_scripts_table.ts:
--------------------------------------------------------------------------------
1 | import { sql } from 'drizzle-orm';
2 | import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
3 |
4 | export async function up(db: any): Promise {
5 | // 创建head_scripts表
6 | await db.run(sql`
7 | CREATE TABLE IF NOT EXISTS head_scripts (
8 | id INTEGER PRIMARY KEY AUTOINCREMENT,
9 | name TEXT NOT NULL,
10 | description TEXT,
11 | code TEXT NOT NULL,
12 | type TEXT NOT NULL DEFAULT 'analytics',
13 | is_active INTEGER NOT NULL DEFAULT 1,
14 | position TEXT NOT NULL DEFAULT 'head',
15 | pages TEXT,
16 | "order" INTEGER NOT NULL DEFAULT 0,
17 | created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
18 | updated_at TEXT
19 | );
20 | `);
21 | }
22 |
23 | export async function down(db: any): Promise {
24 | // 删除head_scripts表
25 | await db.run(sql`DROP TABLE IF EXISTS head_scripts;`);
26 | }
27 |
--------------------------------------------------------------------------------
/src/lib/links-db.ts:
--------------------------------------------------------------------------------
1 | // This file is kept for compatibility with existing imports
2 | // The actual database connection is now handled in src/lib/actions/links.ts
3 |
4 | // Placeholder exports to maintain compatibility
5 | export const db = null;
6 | export const sqlite = null;
7 |
--------------------------------------------------------------------------------
/src/lib/migrate.ts:
--------------------------------------------------------------------------------
1 | import { migrate } from 'drizzle-orm/better-sqlite3/migrator';
2 | import { drizzle } from 'drizzle-orm/better-sqlite3';
3 | import Database from 'better-sqlite3';
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 | // Initialize SQLite database
12 | const sqlite = new Database('./demo.db');
13 | const db = drizzle(sqlite);
14 |
15 | // Define migrations folder path
16 | const migrationsFolder = path.join(__dirname, 'migrations');
17 |
18 | // Run migrations
19 | console.log('Starting database migrations...');
20 | try {
21 | migrate(db, { migrationsFolder });
22 | console.log('Migrations completed successfully');
23 | } catch (err) {
24 | console.error('Migration failed:', err);
25 | process.exit(1);
26 | }
27 |
28 | // Close database connection
29 | sqlite.close();
--------------------------------------------------------------------------------
/src/lib/migrations/0000_init_schema.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS users (
2 | id INTEGER PRIMARY KEY AUTOINCREMENT,
3 | email TEXT UNIQUE NOT NULL,
4 | password TEXT NOT NULL,
5 | createdAt TEXT DEFAULT CURRENT_TIMESTAMP
6 | );
7 |
8 | CREATE TABLE IF NOT EXISTS posts (
9 | id INTEGER PRIMARY KEY AUTOINCREMENT,
10 | title TEXT NOT NULL,
11 | slug TEXT UNIQUE NOT NULL,
12 | content TEXT NOT NULL,
13 | excerpt TEXT,
14 | published INTEGER DEFAULT 0,
15 | createdAt TEXT DEFAULT CURRENT_TIMESTAMP,
16 | updatedAt TEXT DEFAULT CURRENT_TIMESTAMP,
17 | authorId INTEGER REFERENCES users(id)
18 | );
19 |
20 | CREATE TABLE IF NOT EXISTS categories (
21 | id INTEGER PRIMARY KEY AUTOINCREMENT,
22 | name TEXT UNIQUE NOT NULL,
23 | slug TEXT UNIQUE NOT NULL,
24 | description TEXT,
25 | parent_id INTEGER,
26 | "order" INTEGER NOT NULL DEFAULT 0,
27 | created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
28 | updated_at TEXT
29 | );
30 |
31 | CREATE TABLE IF NOT EXISTS tags (
32 | id INTEGER PRIMARY KEY AUTOINCREMENT,
33 | name TEXT UNIQUE NOT NULL
34 | );
35 |
36 | CREATE TABLE IF NOT EXISTS post_tags (
37 | postId INTEGER REFERENCES posts(id),
38 | tagId INTEGER REFERENCES tags(id),
39 | PRIMARY KEY (postId, tagId)
40 | );
41 |
42 | CREATE TABLE IF NOT EXISTS post_categories (
43 | postId INTEGER REFERENCES posts(id),
44 | categoryId INTEGER REFERENCES categories(id),
45 | PRIMARY KEY (postId, categoryId)
46 | );
47 |
48 | CREATE TABLE IF NOT EXISTS media (
49 | id INTEGER PRIMARY KEY AUTOINCREMENT,
50 | url TEXT NOT NULL,
51 | altText TEXT,
52 | width INTEGER,
53 | height INTEGER,
54 | createdAt TEXT DEFAULT CURRENT_TIMESTAMP
55 | );
--------------------------------------------------------------------------------
/src/lib/migrations/0000_init_schema.ts:
--------------------------------------------------------------------------------
1 | import { sql } from 'drizzle-orm';
2 | import { db } from '../db';
3 |
4 | export async function up() {
5 | await db.run(sql`
6 | CREATE TABLE IF NOT EXISTS users (
7 | id INTEGER PRIMARY KEY AUTOINCREMENT,
8 | email TEXT UNIQUE NOT NULL,
9 | password TEXT NOT NULL,
10 | createdAt TEXT DEFAULT CURRENT_TIMESTAMP
11 | )`);
12 |
13 | await db.run(sql`
14 | CREATE TABLE IF NOT EXISTS posts (
15 | id INTEGER PRIMARY KEY AUTOINCREMENT,
16 | title TEXT NOT NULL,
17 | slug TEXT UNIQUE NOT NULL,
18 | content TEXT NOT NULL,
19 | excerpt TEXT,
20 | published INTEGER DEFAULT 0,
21 | createdAt TEXT DEFAULT CURRENT_TIMESTAMP,
22 | updatedAt TEXT DEFAULT CURRENT_TIMESTAMP,
23 | authorId INTEGER REFERENCES users(id)
24 | )`);
25 |
26 | await db.run(sql`
27 | CREATE TABLE IF NOT EXISTS categories (
28 | id INTEGER PRIMARY KEY AUTOINCREMENT,
29 | name TEXT UNIQUE NOT NULL
30 | )`);
31 |
32 | await db.run(sql`
33 | CREATE TABLE IF NOT EXISTS tags (
34 | id INTEGER PRIMARY KEY AUTOINCREMENT,
35 | name TEXT UNIQUE NOT NULL
36 | )`);
37 |
38 | await db.run(sql`
39 | CREATE TABLE IF NOT EXISTS post_tags (
40 | postId INTEGER REFERENCES posts(id),
41 | tagId INTEGER REFERENCES tags(id),
42 | PRIMARY KEY (postId, tagId)
43 | )`);
44 |
45 | await db.run(sql`
46 | CREATE TABLE IF NOT EXISTS post_categories (
47 | postId INTEGER REFERENCES posts(id),
48 | categoryId INTEGER REFERENCES categories(id),
49 | PRIMARY KEY (postId, categoryId)
50 | )`);
51 |
52 | await db.run(sql`
53 | CREATE TABLE IF NOT EXISTS media (
54 | id INTEGER PRIMARY KEY AUTOINCREMENT,
55 | url TEXT NOT NULL,
56 | altText TEXT,
57 | width INTEGER,
58 | height INTEGER,
59 | createdAt TEXT DEFAULT CURRENT_TIMESTAMP
60 | )`);
61 | }
62 |
63 | export async function down() {
64 | await db.run(sql`DROP TABLE IF EXISTS media`);
65 | await db.run(sql`DROP TABLE IF EXISTS post_categories`);
66 | await db.run(sql`DROP TABLE IF EXISTS post_tags`);
67 | await db.run(sql`DROP TABLE IF EXISTS tags`);
68 | await db.run(sql`DROP TABLE IF EXISTS categories`);
69 | await db.run(sql`DROP TABLE IF EXISTS posts`);
70 | await db.run(sql`DROP TABLE IF EXISTS users`);
71 | }
72 |
--------------------------------------------------------------------------------
/src/lib/migrations/meta/_journal.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "5",
3 | "dialect": "sqlite",
4 | "entries": [
5 | {
6 | "idx": 0,
7 | "when": "1714727482882",
8 | "tag": "0000_init_schema",
9 | "breakpoints": true
10 | }
11 | ]
12 | }
--------------------------------------------------------------------------------
/src/lib/mock-modules/nodejieba.js:
--------------------------------------------------------------------------------
1 | // 模拟 nodejieba 模块
2 | module.exports = {
3 | cut: function(text) {
4 | // 简单的分词实现,按空格分割
5 | return text.split(/\s+/);
6 | }
7 | };
8 |
--------------------------------------------------------------------------------
/src/lib/reset-password.ts:
--------------------------------------------------------------------------------
1 | import { db } from './db';
2 | import * as schema from './schema';
3 | import { eq } from 'drizzle-orm';
4 |
5 | /**
6 | * This script resets the password for a specific user
7 | * Run with: npx ts-node src/lib/reset-password.ts
8 | */
9 | async function resetPassword() {
10 | const email = 'vista8@gmail.com'; // The email of the user to reset
11 | const newPassword = 'admin123'; // The new password to set
12 |
13 | try {
14 | // Update the user's password
15 | await db
16 | .update(schema.users)
17 | .set({ password: newPassword })
18 | .where(eq(schema.users.email, email));
19 |
20 | console.log(`Password reset successful for ${email}`);
21 | console.log(`New password: ${newPassword}`);
22 | } catch (error) {
23 | console.error('Error resetting password:', error);
24 | }
25 | }
26 |
27 | // Run the function
28 | resetPassword();
29 |
--------------------------------------------------------------------------------
/src/lib/schema/links.ts:
--------------------------------------------------------------------------------
1 | import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
2 | import { sql } from 'drizzle-orm';
3 |
4 | // Links table definition
5 | export const links = sqliteTable('links', {
6 | id: integer('id').primaryKey({ autoIncrement: true }),
7 | title: text('title').notNull(),
8 | url: text('url').notNull(),
9 | description: text('description'),
10 | coverImage: text('cover_image'),
11 | tags: text('tags'),
12 | isVisible: integer('is_visible').notNull().default(1), // 1 = visible, 0 = hidden
13 | createdAt: text('created_at').notNull().default(sql`CURRENT_TIMESTAMP`),
14 | updatedAt: text('updated_at'),
15 | });
16 |
17 | // Webhooks table definition
18 | export const webhooks = sqliteTable('webhooks', {
19 | id: integer('id').primaryKey({ autoIncrement: true }),
20 | url: text('url').notNull(),
21 | secret: text('secret'),
22 | isActive: integer('is_active').notNull().default(1),
23 | createdAt: text('created_at').notNull().default(sql`CURRENT_TIMESTAMP`),
24 | updatedAt: text('updated_at'),
25 | });
26 |
27 | // Type declarations
28 | export type Link = typeof links.$inferSelect;
29 | export type NewLink = typeof links.$inferInsert;
30 | export type Webhook = typeof webhooks.$inferSelect;
31 | export type NewWebhook = typeof webhooks.$inferInsert;
32 |
--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Utility functions for the blog application
3 | */
4 | import { type ClassValue, clsx } from "clsx";
5 | import { twMerge } from "tailwind-merge";
6 | import pinyin from 'pinyin';
7 |
8 | /**
9 | * Combines class names with tailwind-merge for optimal class generation
10 | */
11 | export function cn(...inputs: ClassValue[]) {
12 | return twMerge(clsx(inputs));
13 | }
14 |
15 | /**
16 | * Generates a URL-friendly slug from a string
17 | * Converts Chinese characters to pinyin and limits to 20 characters
18 | *
19 | * @param text The text to convert to a slug
20 | * @returns A URL-friendly slug
21 | */
22 | export function generateSlug(text: string): string {
23 | if (!text) return '';
24 |
25 | // 检测是否包含中文字符
26 | const hasChinese = /[\u4e00-\u9fa5]/.test(text);
27 |
28 | // 如果包含中文,转换为拼音
29 | let processedText = text;
30 | if (hasChinese) {
31 | processedText = pinyin(text, {
32 | style: pinyin.STYLE_NORMAL, // 普通风格,不带声调
33 | heteronym: false, // 禁用多音字
34 | }).join('');
35 | }
36 |
37 | return processedText
38 | .toLowerCase()
39 | .replace(/[^\w]+/g, '-') // 将非单词字符替换为连字符
40 | .replace(/^-+|-+$/g, '') // 删除开头和结尾的连字符
41 | .substring(0, 20); // 限制长度为20个字符
42 | }
43 |
44 | /**
45 | * 格式化日期为中文友好格式
46 | *
47 | * @param dateString ISO格式的日期字符串
48 | * @returns 格式化后的日期字符串,例如:2025年4月5日
49 | */
50 | export function formatDate(dateString: string): string {
51 | if (!dateString) return '';
52 |
53 | const date = new Date(dateString);
54 |
55 | // 检查日期是否有效
56 | if (isNaN(date.getTime())) {
57 | return '';
58 | }
59 |
60 | // 中文日期格式
61 | return `${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日`;
62 | }
63 |
--------------------------------------------------------------------------------
/src/lib/utils/menu-adapters.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 菜单适配器工具函数
3 | * 用于将数据库菜单转换为组件期望的格式
4 | */
5 |
6 | // SimpleNavigation组件期望的MenuItem类型
7 | export type MenuItem = {
8 | id: number;
9 | name: string;
10 | url: string; // 非空字符串
11 | isExternal: number;
12 | parentId: number | null;
13 | order: number;
14 | isActive: number;
15 | };
16 |
17 | /**
18 | * 适配器函数:将数据库菜单转换为SimpleNavigation期望的格式
19 | * @param menus 数据库菜单数组
20 | * @returns 转换后的菜单数组
21 | */
22 | export function adaptMenus(menus: any[]): MenuItem[] {
23 | return menus.map(menu => ({
24 | ...menu,
25 | url: menu.url || '#' // 确保url永远不为null
26 | }));
27 | }
28 |
--------------------------------------------------------------------------------
/src/middleware.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server';
2 | import type { NextRequest } from 'next/server';
3 | import { getToken } from 'next-auth/jwt';
4 |
5 | export async function middleware(request: NextRequest) {
6 | // Get the pathname of the request
7 | const path = request.nextUrl.pathname;
8 |
9 | // Check if the path is for admin routes
10 | const isAdminPath = path.startsWith('/admin');
11 |
12 | if (isAdminPath) {
13 | // Get the session token using NextAuth
14 | const session = await getToken({
15 | req: request,
16 | secret: process.env.NEXTAUTH_SECRET
17 | });
18 |
19 | // If no session exists, redirect to login
20 | if (!session) {
21 | return NextResponse.redirect(new URL('/login', request.url));
22 | }
23 | }
24 |
25 | return NextResponse.next();
26 | }
27 |
28 | export const config = {
29 | matcher: ['/((?!api|_next/static|_next/image|favicon.ico|uploads/.*|.*\\.png$).*)'],
30 | };
31 |
--------------------------------------------------------------------------------
/src/scripts/check-users.ts:
--------------------------------------------------------------------------------
1 | import { db } from '@/lib/db';
2 | import * as schema from '@/lib/schema';
3 |
4 | async function checkUsers() {
5 | try {
6 | console.log('Checking users in database...');
7 |
8 | // Query all users
9 | const users = await db.select().from(schema.users).all();
10 |
11 | console.log(`Found ${users.length} users:`);
12 |
13 | // Display user info (excluding full password hash)
14 | users.forEach((user, index) => {
15 | console.log(`User ${index + 1}:`);
16 | console.log(` ID: ${user.id}`);
17 | console.log(` Email: ${user.email}`);
18 | console.log(` Password: ${user.password ? '********' + user.password.substring(user.password.length - 5) : 'No password'}`);
19 | console.log(` Created: ${user.createdAt}`);
20 | console.log('---');
21 | });
22 |
23 | if (users.length === 0) {
24 | console.log('No users found in the database. You may need to create a user first.');
25 | }
26 |
27 | } catch (error) {
28 | console.error('Error checking users:', error);
29 | } finally {
30 | process.exit(0);
31 | }
32 | }
33 |
34 | // Run the function
35 | checkUsers();
36 |
--------------------------------------------------------------------------------
/src/scripts/migrations/add-menus-table.ts:
--------------------------------------------------------------------------------
1 | import { db } from '@/lib/db';
2 | import { sql } from 'drizzle-orm';
3 |
4 | async function main() {
5 | console.log('开始创建菜单表...');
6 |
7 | try {
8 | // 创建菜单表
9 | await db.run(sql`
10 | CREATE TABLE IF NOT EXISTS menus (
11 | id INTEGER PRIMARY KEY AUTOINCREMENT,
12 | name TEXT NOT NULL,
13 | description TEXT,
14 | url TEXT,
15 | is_external INTEGER DEFAULT 0 NOT NULL,
16 | parent_id INTEGER,
17 | "order" INTEGER DEFAULT 0 NOT NULL,
18 | is_active INTEGER DEFAULT 1 NOT NULL,
19 | created_at TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
20 | updated_at TEXT,
21 | FOREIGN KEY (parent_id) REFERENCES menus(id)
22 | );
23 | `);
24 |
25 | console.log('菜单表创建成功!');
26 | } catch (error) {
27 | console.error('创建菜单表失败:', error);
28 | process.exit(1);
29 | }
30 | }
31 |
32 | main().then(() => process.exit(0));
33 |
--------------------------------------------------------------------------------
/src/scripts/update-password.ts:
--------------------------------------------------------------------------------
1 | import { db } from '../lib/db';
2 | import * as schema from '../lib/schema';
3 | import { eq } from 'drizzle-orm';
4 | import * as bcrypt from 'bcryptjs';
5 |
6 | /**
7 | * This script updates a user's password with proper bcrypt hashing
8 | * Run with: npx ts-node src/scripts/update-password.ts
9 | */
10 | async function updatePassword() {
11 | const email = 'vista8@gmail.com'; // The email of the user
12 | const newPassword = 'qq778899'; // The new password to set
13 |
14 | try {
15 | // Hash the password with bcrypt
16 | const salt = await bcrypt.genSalt(10);
17 | const hashedPassword = await bcrypt.hash(newPassword, salt);
18 |
19 | // Update the user's password in the database
20 | await db
21 | .update(schema.users)
22 | .set({ password: hashedPassword })
23 | .where(eq(schema.users.email, email));
24 |
25 | console.log(`Password updated successfully for ${email}`);
26 | console.log('You can now log in with your new password');
27 | } catch (error) {
28 | console.error('Error updating password:', error);
29 | }
30 | }
31 |
32 | // Run the function
33 | updatePassword();
34 |
--------------------------------------------------------------------------------
/src/types/index.ts:
--------------------------------------------------------------------------------
1 | // 通用的Post类型定义
2 | export type Post = {
3 | id: number;
4 | title: string;
5 | slug: string;
6 | excerpt: string | null;
7 | coverImage: string | null;
8 | createdAt: string;
9 | pinned?: number | boolean;
10 | author?: {
11 | id: number;
12 | email: string | null;
13 | };
14 | category?: {
15 | id: number;
16 | name: string | null;
17 | slug: string | null;
18 | };
19 | tags?: {
20 | id: number;
21 | name: string;
22 | slug: string;
23 | }[];
24 | };
25 |
26 | // 其他类型定义可以在此添加
27 |
--------------------------------------------------------------------------------
/src/types/uuid.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'uuid';
2 |
--------------------------------------------------------------------------------
/src/types/yaireo__tagify.d.ts:
--------------------------------------------------------------------------------
1 | declare module '@yaireo/tagify' {
2 | export default class Tagify {
3 | constructor(input: HTMLInputElement | HTMLTextAreaElement, settings?: any);
4 |
5 | on(event: string, callback: (e: CustomEvent) => void): this;
6 | off(event: string, callback?: (e: CustomEvent) => void): this;
7 |
8 | destroy(): void;
9 | removeAllTags(): this;
10 | addTags(tags: string | string[] | object | object[], clearInput?: boolean, skipInvalid?: boolean): this;
11 |
12 | loadOriginalValues(value: string): this;
13 |
14 | readonly value: any[];
15 | readonly DOM: any;
16 | readonly settings: any;
17 | readonly dropdown: {
18 | show(): void;
19 | hide(): void;
20 | position(): void;
21 | readonly isVisible: boolean;
22 | };
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/utils/html-sanitizer.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 清理HTML内容
3 | *
4 | * 这个函数处理各种格式的HTML内容,包括:
5 | * 1. Markdown代码块
6 | * 2. 带前导说明文字的HTML片段
7 | * 3. 完整的HTML文档
8 | * 4. 非HTML内容
9 | *
10 | * @param content 需要清理的HTML内容
11 | * @returns 清理后的HTML内容
12 | */
13 | export function sanitizeHtml(content: string): string {
14 | if (!content) return '';
15 |
16 | // 移除可能的Markdown代码块标记
17 | let cleanedContent = content;
18 |
19 | // 处理Markdown代码块格式 ```html ... ```
20 | const markdownHtmlPattern = /```html\s*([\s\S]*?)\s*```/;
21 | const markdownMatch = content.match(markdownHtmlPattern);
22 | if (markdownMatch && markdownMatch[1]) {
23 | cleanedContent = markdownMatch[1];
24 | }
25 |
26 | // 处理可能的前导说明文字,寻找第一个HTML标签
27 | const htmlStartPattern = /<(!DOCTYPE|html|head|body|div|p|span|a|img|script|link|meta)/i;
28 | const htmlStartMatch = cleanedContent.match(htmlStartPattern);
29 | if (htmlStartMatch) {
30 | const startIndex = htmlStartMatch.index;
31 | if (startIndex && startIndex > 0) {
32 | cleanedContent = cleanedContent.substring(startIndex);
33 | }
34 | }
35 |
36 | // 确保内容是有效的HTML
37 | if (!cleanedContent.trim().startsWith('<')) {
38 | // 如果不是以HTML标签开头,包装为HTML
39 | cleanedContent = `${cleanedContent}
`;
40 | }
41 |
42 | return cleanedContent;
43 | }
44 |
--------------------------------------------------------------------------------
/start-dev.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # 设置固定端口
4 | PORT=3099
5 |
6 | # 检查端口是否被占用
7 | pid=$(lsof -ti :$PORT)
8 | if [ ! -z "$pid" ]; then
9 | echo "Port $PORT is in use by process $pid. Killing it..."
10 | kill -9 $pid
11 | fi
12 |
13 | # 清理旧的构建文件
14 | echo "Cleaning..."
15 | rm -rf .next
16 |
17 | # 重新构建项目
18 | echo "Building..."
19 | npm run build
20 |
21 | # 启动服务器
22 | echo "Starting server on port $PORT..."
23 | PORT=$PORT npm run start
--------------------------------------------------------------------------------
/start-standalone.js:
--------------------------------------------------------------------------------
1 | // 更健壮的 Standalone 启动脚本
2 | const path = require('path');
3 | const fs = require('fs');
4 | const { spawn } = require('child_process');
5 |
6 | // 确保 .next/standalone 目录存在
7 | const standaloneDir = path.join(__dirname, '.next', 'standalone');
8 | if (!fs.existsSync(standaloneDir)) {
9 | console.error('Standalone 目录不存在。请先运行 npm run build');
10 | process.exit(1);
11 | }
12 |
13 | // 确保静态资源目录存在
14 | const staticDir = path.join(standaloneDir, '.next', 'static');
15 | if (!fs.existsSync(staticDir)) {
16 | console.log('复制静态资源...');
17 | fs.mkdirSync(path.join(standaloneDir, '.next'), { recursive: true });
18 | fs.cpSync(path.join(__dirname, '.next', 'static'), staticDir, { recursive: true });
19 | }
20 |
21 | // 确保 public 目录存在
22 | const publicDir = path.join(standaloneDir, 'public');
23 | if (!fs.existsSync(publicDir)) {
24 | console.log('复制 public 目录...');
25 | fs.cpSync(path.join(__dirname, 'public'), publicDir, { recursive: true });
26 | }
27 |
28 | // 启动服务器
29 | console.log('启动 Standalone 服务器...');
30 | const port = process.env.PORT || 3099;
31 | console.log(`端口: ${port}`);
32 |
33 | // 使用 arch -x86_64 启动 Node.js(在 Apple Silicon Mac 上)
34 | const isAppleSilicon = process.arch === 'arm64';
35 | let serverProcess;
36 |
37 | if (isAppleSilicon) {
38 | console.log('检测到 Apple Silicon,使用 Rosetta 2 运行...');
39 | serverProcess = spawn('arch', ['-x86_64', 'node', path.join(standaloneDir, 'server.js')], {
40 | env: { ...process.env, PORT: port },
41 | stdio: 'inherit'
42 | });
43 | } else {
44 | serverProcess = spawn('node', [path.join(standaloneDir, 'server.js')], {
45 | env: { ...process.env, PORT: port },
46 | stdio: 'inherit'
47 | });
48 | }
49 |
50 | // 处理进程事件
51 | serverProcess.on('error', (err) => {
52 | console.error('启动服务器时出错:', err);
53 | process.exit(1);
54 | });
55 |
56 | serverProcess.on('close', (code) => {
57 | console.log(`服务器进程退出,退出码: ${code}`);
58 | process.exit(code);
59 | });
60 |
61 | // 处理终止信号
62 | process.on('SIGINT', () => {
63 | console.log('接收到 SIGINT 信号,正在关闭服务器...');
64 | serverProcess.kill('SIGINT');
65 | });
66 |
67 | process.on('SIGTERM', () => {
68 | console.log('接收到 SIGTERM 信号,正在关闭服务器...');
69 | serverProcess.kill('SIGTERM');
70 | });
71 |
--------------------------------------------------------------------------------
/test-password.js:
--------------------------------------------------------------------------------
1 | const Database = require('better-sqlite3');
2 | const bcrypt = require('bcryptjs');
3 |
4 | // Initialize SQLite database
5 | const db = new Database('./blog.db');
6 |
7 | async function testPassword() {
8 | const email = 'vista8@gmail.com';
9 | const password = 'qq778899';
10 |
11 | try {
12 | // Get the user from the database
13 | const stmt = db.prepare('SELECT * FROM users WHERE email = ?');
14 | const user = stmt.get(email);
15 |
16 | if (!user) {
17 | console.log(`No user found with email ${email}`);
18 | return;
19 | }
20 |
21 | console.log('User found:', {
22 | id: user.id,
23 | email: user.email,
24 | passwordHash: user.password.substring(0, 20) + '...'
25 | });
26 |
27 | // Test password verification
28 | const isPasswordValid = await bcrypt.compare(password, user.password);
29 | console.log('Password valid?', isPasswordValid);
30 |
31 | } catch (error) {
32 | console.error('Error testing password:', error);
33 | } finally {
34 | // Close the database connection
35 | db.close();
36 | }
37 | }
38 |
39 | // Run the function
40 | testPassword();
41 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2017",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "plugins": [
17 | {
18 | "name": "next"
19 | }
20 | ],
21 | "paths": {
22 | "@/*": ["./src/*"]
23 | }
24 | },
25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26 | "exclude": ["node_modules"]
27 | }
28 |
--------------------------------------------------------------------------------
/update-posts-add-cover.js:
--------------------------------------------------------------------------------
1 | // 添加文章封面字段的迁移脚本
2 | const sqlite3 = require('sqlite3').verbose();
3 | const path = require('path');
4 |
5 | // 数据库文件路径
6 | const dbPath = path.join(process.cwd(), 'blog.db');
7 |
8 | // 连接到数据库
9 | const db = new sqlite3.Database(dbPath, (err) => {
10 | if (err) {
11 | console.error('无法连接到数据库:', err.message);
12 | process.exit(1);
13 | }
14 | console.log('已连接到SQLite数据库');
15 | });
16 |
17 | // 检查posts表是否存在coverImage列
18 | db.all("PRAGMA table_info(posts)", (err, rows) => {
19 | if (err) {
20 | console.error('查询表结构时出错:', err.message);
21 | closeDb();
22 | process.exit(1);
23 | }
24 |
25 | // 检查coverImage列是否存在
26 | const hasColumn = rows && rows.some(row => row.name === 'coverImage');
27 |
28 | if (!hasColumn) {
29 | // 添加coverImage列
30 | db.run("ALTER TABLE posts ADD COLUMN coverImage TEXT", (err) => {
31 | if (err) {
32 | console.error('添加coverImage列时出错:', err.message);
33 | } else {
34 | console.log('成功添加coverImage列到posts表');
35 | }
36 | closeDb();
37 | });
38 | } else {
39 | console.log('coverImage列已存在,无需修改');
40 | closeDb();
41 | }
42 | });
43 |
44 | // 关闭数据库连接
45 | function closeDb() {
46 | db.close((err) => {
47 | if (err) {
48 | console.error('关闭数据库连接时出错:', err.message);
49 | } else {
50 | console.log('数据库连接已关闭');
51 | }
52 | });
53 | }
54 |
--------------------------------------------------------------------------------
/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 2,
3 | "buildCommand": "npm run build",
4 | "devCommand": "npm run dev",
5 | "installCommand": "npm install",
6 | "outputDirectory": ".next",
7 | "framework": "nextjs",
8 | "regions": ["hnd1"],
9 | "env": {
10 | "NEXTAUTH_SECRET": "your-nextauth-secret-change-me-in-production",
11 | "JWT_SECRET": "your-jwt-secret-change-me-in-production"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------