├── .eslintrc.json ├── .gitignore ├── .nvmrc ├── .vscode ├── extensions.json └── settings.json ├── LICENSE ├── README.md ├── app ├── about │ ├── about.module.css │ └── page.tsx ├── api │ ├── dead-chain │ │ └── route.ts │ ├── rss │ │ └── route.ts │ └── sitemap │ │ └── route.ts ├── archive │ ├── archive.module.css │ └── page.tsx ├── blog-classify │ └── [...params] │ │ └── page.tsx ├── blog-details │ ├── [blogId] │ │ ├── page.tsx │ │ └── post-client.tsx │ └── blogDetail.module.css ├── blog │ ├── [slug] │ │ ├── page.tsx │ │ └── post-client.tsx │ └── blog.module.css ├── components │ ├── ClassifyPrevOrNext │ │ ├── classifyPrevOrNext.module.css │ │ └── index.tsx │ ├── CodeBlock │ │ └── index.tsx │ ├── Comment │ │ ├── comment.module.css │ │ └── index.tsx │ ├── Footer │ │ ├── footer.module.css │ │ └── index.tsx │ ├── LazyCom │ │ ├── index.module.css │ │ └── index.tsx │ ├── MdxComponent │ │ └── index.tsx │ ├── NavBar │ │ ├── index.tsx │ │ ├── navBar.module.css │ │ └── routes.ts │ ├── PagerComponent │ │ ├── index.tsx │ │ └── pager.module.css │ ├── Permit │ │ ├── index.tsx │ │ └── permit.module.css │ ├── ScrollComponent │ │ ├── index.module.css │ │ └── index.tsx │ ├── SysIcon │ │ ├── index.md │ │ └── index.tsx │ ├── VideoPlay │ │ ├── index.module.css │ │ └── index.tsx │ ├── VirtuallyItem │ │ ├── index.tsx │ │ └── virtuallyItem.module.css │ └── WithLoading │ │ ├── index.tsx │ │ └── useChangeLoading.ts ├── copyright-notice │ ├── copyrightNotice.module.css │ └── page.tsx ├── disclaimers │ ├── disclaimers.module.css │ └── page.tsx ├── error.tsx ├── friendly-links │ ├── friendlyLinks.module.css │ ├── page.tsx │ └── post-client.tsx ├── layout.tsx ├── loading.tsx ├── more │ ├── more.module.css │ └── page.tsx ├── news │ ├── news.module.css │ ├── newsItem.tsx │ ├── page.tsx │ └── post-client.tsx ├── not-found.tsx ├── page.tsx ├── photography │ ├── Photography.module.css │ ├── page.tsx │ └── post-client.tsx ├── resume │ ├── page.tsx │ └── resume.module.css ├── store │ └── layoutStore.tsx ├── styles │ ├── comment.css │ ├── error404.module.css │ ├── font │ │ ├── VjlkfcVsDrtK.woff │ │ └── VjlkfcVsDrtK.woff2 │ ├── globals.css │ ├── home.module.css │ ├── id.css │ ├── loading.css │ ├── markdown.css │ ├── reset.css │ └── themeColor.css ├── tree-hole │ ├── page.tsx │ └── treehole.module.css ├── utils │ ├── CustomHooks │ │ ├── usePageSize.ts │ │ └── useWindow.ts │ ├── classicUtils.ts │ ├── cloneUtils │ │ └── throttleByAnimationFrame.ts │ ├── dataImage.ts │ ├── dataUtils.ts │ ├── dict.ts │ ├── element.ts │ ├── elementUtils.ts │ ├── httpClient │ │ ├── apis │ │ │ ├── blog.ts │ │ │ ├── index.ts │ │ │ ├── news.ts │ │ │ ├── photography.ts │ │ │ ├── secret.ts │ │ │ ├── user.ts │ │ │ ├── video.ts │ │ │ └── wallpaper.ts │ │ └── request.ts │ └── local.ts ├── visitor │ ├── page.tsx │ └── visitor.module.css └── wallpaper │ ├── page.tsx │ ├── post-client.tsx │ └── wallpaper.module.css ├── next.config.js ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── public ├── dead-chain.xml ├── favicon.ico ├── images │ ├── about_3.jpg │ ├── bg00001.jpeg │ ├── bg00002.jpeg │ ├── bg00003.jpeg │ ├── bg00004.jpeg │ ├── bg00005.jpeg │ ├── bg00007.jpeg │ ├── bg00008.jpeg │ ├── bg00009.jpeg │ ├── logo.png │ ├── logo_black.png │ ├── logo_white.png │ ├── travelling-dark.png │ └── travelling-light.png ├── robots.txt ├── rss.xml └── sitemap.xml ├── tailwind.config.ts └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 18.19.0 -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint" 4 | ] 5 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll.eslint": "explicit" 4 | }, 5 | "editor.formatOnSave": true, 6 | "editor.wordWrap": "wordWrapColumn", 7 | "editor.tabSize": 4, 8 | "editor.formatOnSaveMode": "modifications", 9 | "files.autoSave": "afterDelay", 10 | "typescript.validate.enable": true, 11 | "javascript.validate.enable": true, 12 | "eslint.enable": true, 13 | "eslint.options": { 14 | "configFile": ".eslintrc.js" 15 | } 16 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Shimmer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 项目名称 2 | 3 | 这是一个基于nextjs写的个人博客项目,博客内容丰富,全项目代码公开,欢迎参考 4 | 5 | ## 功能特点 6 | 7 | [🟢] [个人主页](https://wp-boke.work) 8 | 9 | [🟢] [文章列表](https://wp-boke.work/blog/1) 10 | 11 | [🟢] [文归档](https://wp-boke.work/archive) 12 | 13 | [🟢] [树洞](https://wp-boke.work/tree-hole) 14 | 15 | [🟢] [摄影](https://wp-boke.work/photography) 16 | 17 | [🟢] [关于](https://wp-boke.work/about) 18 | 19 | [🟢] [我的简历](https://wp-boke.work/resume) 20 | 21 | [🟢] [访客列表](https://wp-boke.work/visitor) 22 | 23 | [🟢] [友情链接](https://wp-boke.work/friendly-links) 24 | 25 | [🟢] [更多](https://wp-boke.work/more) 26 | 27 | ## 技术栈 28 | 29 | - Next.js:[Next.js](https://nextjs.org/) 是一个使用 React 进行服务端渲染的框架。 30 | - Vercel:[Vercel](https://vercel.com/) 是一个用于快速部署静态网站和服务器渲染应用程序的平台。 31 | - Egg.js:[Egg.js](https://eggjs.org/) 是一个基于 Node.js 和 Koa 的企业级后端开发框架。 32 | - MySQL:[MySQL](https://www.mysql.com/) 是一个流行的关系型数据库管理系统。 33 | 34 | ## 开始使用 35 | 36 | ### 环境要求 37 | 38 | 列出运行项目所需的软件和环境要求。 39 | 40 | ### 安装依赖 41 | 42 | ```bash 43 | node 18^ 44 | 45 | pnpm i 46 | ``` 47 | 48 | ### 配置 49 | 50 | ```bash 51 | SERVER_NAME='vercel' 52 | ``` 53 | 54 | ### 启动项目 55 | 56 | ```bash 57 | pnpm dev 58 | ``` 59 | 60 | ### 构建项目 61 | 62 | ```bash 63 | pnpm build 64 | ``` 65 | 66 | ## 部署 67 | ### Vercel 部署(https://vercel.com/dashboard) 68 | 69 | 1. 在 Vercel 上创建一个新项目。 70 | 2. 配置环境变量并设置相关的值。 71 | 3. 部署项目到 Vercel。 72 | 73 | ## 贡献 74 | 75 | 欢迎贡献你的代码,以及提交问题和建议。请遵循贡献指南。 76 | 77 | ## 许可证 78 | 79 | 采用 [MIT licensed](LICENSE) 许可协议. -------------------------------------------------------------------------------- /app/about/about.module.css: -------------------------------------------------------------------------------- 1 | .about { 2 | width: 100%; 3 | padding-top: 110px; 4 | display: flex; 5 | flex-direction: column; 6 | align-items: center; 7 | padding-bottom: 24px; 8 | background-color: var(--bg-w-pure); 9 | flex: 1; 10 | } 11 | 12 | .about_content { 13 | max-width: 850px; 14 | margin: 0 auto; 15 | } 16 | 17 | .title { 18 | height: 36px; 19 | font-size: 20px; 20 | color: var(--b-alpha-80); 21 | display: flex; 22 | align-items: center; 23 | margin-bottom: 12px; 24 | letter-spacing: 0.1em; 25 | } 26 | 27 | .about_img { 28 | width: 100% !important; 29 | height: auto !important; 30 | } 31 | 32 | .info { 33 | } 34 | 35 | .desc { 36 | } 37 | 38 | .contact { 39 | display: flex; 40 | flex-direction: column; 41 | } 42 | 43 | .contact_item { 44 | display: flex; 45 | align-items: center; 46 | } 47 | 48 | .contact_item_key { 49 | padding-right: 12px; 50 | } 51 | 52 | .about_img, 53 | .info, 54 | .desc, 55 | .contact { 56 | margin-bottom: 24px; 57 | color: var(--b-alpha-70); 58 | line-height: 1.8em; 59 | } 60 | 61 | .toResume{ 62 | color: var(--purple-text); 63 | font-size: 14px; 64 | } 65 | 66 | .toResume:hover{ 67 | color: var(--purple-text-active); 68 | } 69 | 70 | @media screen and (max-width: 1000px) { 71 | .about_content { 72 | width: 85%; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /app/about/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import Link from "next/link"; 3 | import Image from "next/image"; 4 | import { useEffect } from "react"; 5 | import { 6 | addNavItemStyle, 7 | bindHandleScroll, 8 | removeNavItemStyle, 9 | removeScroll, 10 | } from "@utils/elementUtils"; 11 | import withLoading from "@components/WithLoading"; 12 | import useChangeLoading from "@components/WithLoading/useChangeLoading"; 13 | import aboutImg from "@public/images/about_3.jpg"; 14 | import style from "./about.module.css"; 15 | 16 | const About = (props) => { 17 | useEffect(() => { 18 | addNavItemStyle(); 19 | bindHandleScroll(); 20 | 21 | return () => { 22 | removeNavItemStyle(); 23 | removeScroll(); 24 | }; 25 | }, []); 26 | 27 | useChangeLoading({ ...props, name: "about" }); 28 | 29 | return ( 30 |
31 |
32 |
About
33 | about 40 |
Me
41 |
42 | Hi!我是Shimmer🌈,在北京工作,目前从事Web前端工程师。 43 | 44 | (了解更多请点击这里) 45 | 46 |
47 |
关于Blog
48 |
49 | 我的个人博客已经经历了三个不同版本的演进,从最初的纯HTML,到React,再到现在的NextJS。写博客的初衷是为了拓展自己的视野、记录自己的成长和生活,同时也希望通过自己的文章为读者们提供有价值的信息和知识。目前,我已经建立了对应的后台管理系统,使得写作变得更加方便和高效。未来,我会持续探索和尝试新的技术和方法,让我的博客能够走得更远,为读者们带来更多有趣的内容和更好的阅读体验。 50 |
51 |
Contact
52 |
53 |
54 |
Email:
55 |
webwp0403@163.com
56 |
57 |
58 |
微信:
59 |
wp0403-
60 |
61 |
62 |
63 |
64 | ); 65 | }; 66 | 67 | export default withLoading(About); 68 | -------------------------------------------------------------------------------- /app/api/dead-chain/route.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import { SitemapStream, streamToPromise } from "sitemap"; 3 | 4 | export async function GET(req: Request, res: Response) { 5 | // Create a Sitemap stream 6 | const sitemapStream = new SitemapStream({ 7 | hostname: "https://wp-boke.work", 8 | }); 9 | 10 | // Add URLs to the Sitemap stream 11 | const pages = ["/blog-details/38", "/secret", "/blog-details/45"]; 12 | pages?.map((v) => sitemapStream.write({ url: `${v}` })); 13 | // ... 14 | 15 | // End the stream 16 | sitemapStream.end(); 17 | 18 | // Generate the XML 19 | const sitemap = await streamToPromise(sitemapStream); 20 | 21 | const serverName = process.env.SERVER_NAME; 22 | if (serverName !== "vercel") { 23 | const sitemapPath = "./public/dead-chain.xml"; 24 | fs.writeFileSync(sitemapPath, sitemap); 25 | } 26 | 27 | // Write the XML to the response 28 | let myResponse = new Response(sitemap, { 29 | status: 200, 30 | headers: { 31 | "Content-Type": "application/xml", 32 | "Cache-Control": "public, max-age=0.1, must-revalidate", 33 | }, 34 | }); 35 | 36 | return myResponse 37 | } 38 | -------------------------------------------------------------------------------- /app/api/rss/route.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import RSS from "rss"; 3 | import getDataApi from "@/utils/httpClient/request"; 4 | 5 | export async function GET(req: Request, res: Response) { 6 | const feed = new RSS({ 7 | title: "Shimmer RSS", 8 | description: "shimmer博客的rss", 9 | feed_url: "https://wp-boke.work/rss.xml", 10 | site_url: "https://wp-boke.work", 11 | language: "zh-CN", 12 | pubDate: new Date(), 13 | }); 14 | 15 | // 调用外部 API 获取内容 16 | const classifyList = (await getDataApi({ type: "all_blog_List" })).data; 17 | 18 | classifyList?.map( 19 | (v: { 20 | title: string; 21 | desc: string; 22 | id: string | number; 23 | userInfo: { name: string }; 24 | time_str: string; 25 | }) => { 26 | feed.item({ 27 | title: v.title, 28 | description: v.desc, 29 | url: `https://wp-boke.work/blog-details/${v.id}`, 30 | author: v.userInfo?.name, 31 | date: v.time_str, 32 | }); 33 | } 34 | ); 35 | 36 | const rssContent = feed.xml(); 37 | 38 | const serverName = process.env.SERVER_NAME; 39 | if (serverName !== "vercel") { 40 | const sitemapPath = "./public/rss.xml"; 41 | fs.writeFileSync(sitemapPath, rssContent); 42 | } 43 | 44 | // Write the XML to the response 45 | let myResponse = new Response(rssContent, { 46 | status: 200, 47 | headers: { 48 | "Content-Type": "application/xml", 49 | "Cache-Control": "public, max-age=0.1, must-revalidate", 50 | }, 51 | }); 52 | 53 | return myResponse; 54 | } 55 | -------------------------------------------------------------------------------- /app/api/sitemap/route.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Descripttion: 3 | * @version: 4 | * @Author: WangPeng 5 | * @Date: 2023-04-24 15:00:43 6 | * @LastEditors: WangPeng 7 | * @LastEditTime: 2023-12-26 17:19:35 8 | */ 9 | import fs from "fs"; 10 | import { SitemapStream, streamToPromise } from "sitemap"; 11 | import getDataApi from "@/utils/httpClient/request"; 12 | 13 | export async function GET(req: Request, res: Response) { 14 | // Create a Sitemap stream 15 | const sitemapStream = new SitemapStream({ 16 | hostname: "https://wp-boke.work", 17 | }); 18 | 19 | // Add URLs to the Sitemap stream 20 | const pages = [ 21 | "/", 22 | "/archive", 23 | "/tree-hole", 24 | "/photography", 25 | "/about", 26 | "/resume", 27 | "/friendly-links", 28 | "/disclaimers", 29 | "/copyright-notice", 30 | ]; 31 | pages?.map((v) => sitemapStream.write({ url: `${v}` })); 32 | 33 | // 调用外部 API 获取内容 34 | const classifyList = (await getDataApi({ type: "all_blog_List" })).data; 35 | 36 | classifyList?.map((v) => 37 | sitemapStream.write({ 38 | url: `/blog-details/${v.id}`, 39 | }) 40 | ); 41 | // ... 42 | 43 | // End the stream 44 | sitemapStream.end(); 45 | 46 | // Generate the XML 47 | const sitemap = await streamToPromise(sitemapStream); 48 | 49 | const serverName = process.env.SERVER_NAME; 50 | if (serverName !== "vercel") { 51 | const sitemapPath = "./public/sitemap.xml"; 52 | fs.writeFileSync(sitemapPath, sitemap); 53 | } 54 | 55 | // Write the XML to the response 56 | let myResponse = new Response(sitemap, { 57 | status: 200, 58 | headers: { 59 | "Content-Type": "application/xml", 60 | "Cache-Control": "public, max-age=0.1, must-revalidate", 61 | }, 62 | }); 63 | 64 | return myResponse; 65 | } 66 | -------------------------------------------------------------------------------- /app/archive/archive.module.css: -------------------------------------------------------------------------------- 1 | .archive { 2 | width: 100%; 3 | padding: 92px 0 22px 0; 4 | flex: 1; 5 | display: flex; 6 | flex-direction: column; 7 | align-items: center; 8 | background-color: var(--bg-w-pure); 9 | position: relative; 10 | -webkit-user-select: none; /* Safari */ 11 | -moz-user-select: none; /* Firefox */ 12 | -ms-user-select: none; /* Internet Explorer/Edge */ 13 | user-select: none; /* Non-prefixed version */ 14 | } 15 | 16 | .archive_content { 17 | width: 60%; 18 | max-width: 800px; 19 | flex:1; 20 | min-height: 100px; 21 | margin: 0 auto; 22 | display: flex; 23 | flex-direction: column; 24 | align-items: center; 25 | } 26 | 27 | .archive_item { 28 | width: 100%; 29 | display: flex; 30 | flex-direction: column; 31 | } 32 | 33 | .year { 34 | width: 100%; 35 | line-height: 34px; 36 | margin: 18px 0; 37 | position: relative; 38 | padding-left: 30px; 39 | display: flex; 40 | align-items: center; 41 | font-size: 28px; 42 | font-weight: 400; 43 | color: var(--b-alpha-80); 44 | } 45 | 46 | .class_item { 47 | width: 100%; 48 | display: flex; 49 | align-items: center; 50 | height: 24px; 51 | padding: 0 30px; 52 | margin-bottom: 15px; 53 | transition: 0.4s; 54 | cursor: pointer; 55 | } 56 | 57 | .class_item_time { 58 | width: 86px; 59 | color: var(--b-alpha-40); 60 | letter-spacing: 0; 61 | } 62 | 63 | .class_item_name { 64 | flex: 1; 65 | color: var(--b-alpha-80); 66 | font-size: 16px; 67 | white-space: nowrap; 68 | overflow: hidden; 69 | text-overflow: ellipsis; 70 | } 71 | 72 | .class_item:hover { 73 | transform: translateX(8px); 74 | } 75 | 76 | .class_item:hover .class_item_time, 77 | .class_item:hover .class_item_name { 78 | color: var(--purple-primary); 79 | } 80 | 81 | @media screen and (max-width: 1000px) { 82 | .archive_content{ 83 | width: 85%; 84 | } 85 | } 86 | 87 | @media screen and (max-width: 800px) { 88 | .archive_content{ 89 | width: 100%; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /app/archive/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import Link from "next/link"; 3 | import { useEffect, useState } from "react"; 4 | import { 5 | addNavItemStyle, 6 | bindHandleScroll, 7 | removeNavItemStyle, 8 | removeScroll, 9 | } from "@utils/elementUtils"; 10 | import getData from "@/utils/httpClient/request"; 11 | import withLoading from "@components/WithLoading"; 12 | import useChangeLoading from "@components/WithLoading/useChangeLoading"; 13 | import style from "./archive.module.css"; 14 | 15 | const Archive = (props) => { 16 | const [data, setDate] = useState([]); 17 | 18 | const init = async () => { 19 | const { data } = await getData({ type: "all_blog_archive_list" }); 20 | 21 | setDate(data); 22 | }; 23 | 24 | useEffect(() => { 25 | addNavItemStyle(); 26 | bindHandleScroll(); 27 | init(); 28 | 29 | return () => { 30 | removeNavItemStyle(); 31 | removeScroll(); 32 | }; 33 | }, []); 34 | 35 | useChangeLoading({ ...props, name: "archive" }); 36 | 37 | return ( 38 |
39 |
40 | {data?.map((v) => { 41 | return ( 42 |
43 |
{v?.year}
44 | {v?.children?.map((item) => ( 45 | 50 |
{item?.date_str}
51 |
{item?.title}
52 | 53 | ))} 54 |
55 | ); 56 | })} 57 |
58 |
59 | ); 60 | }; 61 | 62 | export default withLoading(Archive); 63 | -------------------------------------------------------------------------------- /app/blog-classify/[...params]/page.tsx: -------------------------------------------------------------------------------- 1 | import getData from "@/utils/httpClient/request"; 2 | import PostClient from "../../blog/[slug]/post-client"; 3 | 4 | // 动态路由 5 | export async function generateStaticParams() { 6 | // 调用外部 API 获取内容 7 | const classifyNum = await getData({ type: "all_blog_ClassifyNum" }); 8 | 9 | const arr = [] as any[]; 10 | classifyNum.data?.classifyNum?.forEach((v) => { 11 | const totalPage = Math.ceil(v.count / 10); 12 | const pageList = [] as string[]; 13 | for (let i = 1; i <= totalPage; i++) { 14 | pageList.push(i.toString()); 15 | } 16 | pageList.forEach((v1) => { 17 | arr.push({ type: v.type, page: v1 }); 18 | }); 19 | }); 20 | 21 | return arr.map((v) => [v.type, v.page]); 22 | } 23 | 24 | // 获取数据 25 | async function getPost({ params }) { 26 | const [type, page] = params; 27 | // 获取页码 28 | const post1 = await getData({ 29 | type: "all_blog_PageList", 30 | params: { id: type }, 31 | }); 32 | 33 | const pageList = [] as string[]; 34 | for (let i = 1; i <= post1.data; i++) { 35 | pageList.push(i.toString()); 36 | } 37 | // 调用外部 API 获取内容 38 | const posts2 = await getData({ 39 | type: "all_blog_List", 40 | params: { id: type, page: page }, 41 | }); 42 | const classifyNum = await getData({ type: "all_blog_ClassifyNum" }); 43 | 44 | return { 45 | page: page, 46 | totalPage: post1.data, 47 | pageList, 48 | type: type, 49 | classifyNum: classifyNum.data, 50 | isType: true, 51 | ...posts2, 52 | }; 53 | } 54 | 55 | export default async function BlogList({ params }) { 56 | const post = await getPost(params); 57 | 58 | return ; 59 | } 60 | -------------------------------------------------------------------------------- /app/blog-details/[blogId]/page.tsx: -------------------------------------------------------------------------------- 1 | import { serialize } from "next-mdx-remote/serialize"; 2 | import remarkGfm from "remark-gfm"; 3 | import remarkMath from "remark-math"; 4 | import getData from "@/utils/httpClient/request"; 5 | import PostClient from "./post-client"; 6 | 7 | export const dynamicParams = false; 8 | 9 | export async function generateStaticParams() { 10 | // 调用外部 API 获取内容 11 | const res = await getData({ type: "all_blog_List" }); 12 | 13 | return res.data.map(({ id }) => ({ blogId: id.toString() })); 14 | } 15 | 16 | async function getPost(params: { blogId: string | number }) { 17 | return await getData({ 18 | type: "all_blog_ClassifyDetail", 19 | params: { id: params.blogId }, 20 | }); 21 | } 22 | 23 | export default async function BlogDetails({ params }) { 24 | const { data = {} } = await getPost(params); 25 | 26 | const source = await serialize(data.content, { 27 | scope: {}, 28 | mdxOptions: { 29 | development: process.env.NODE_ENV === "development", 30 | remarkPlugins: [remarkMath, remarkGfm], 31 | rehypePlugins: [], 32 | }, 33 | }); 34 | return source ? : ""; 35 | } 36 | -------------------------------------------------------------------------------- /app/blog-details/[blogId]/post-client.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useEffect } from "react"; 3 | import SysIcon from "@components/SysIcon"; 4 | import { 5 | formatDate, 6 | hasUnicode, 7 | unicodeToEmoji, 8 | shareWebPage, 9 | } from "@utils/dataUtils"; 10 | import { 11 | addNavItemStyle, 12 | bindHandleScroll, 13 | removeNavItemStyle, 14 | removeScroll, 15 | } from "@utils/elementUtils"; 16 | import Permit from "@components/Permit"; 17 | import ClassifyPrevOrNext from "@components/ClassifyPrevOrNext"; 18 | import Comment from "@components/Comment"; 19 | import useMdxComponent from "@components/MdxComponent"; 20 | import withLoading from "@components/WithLoading"; 21 | import useChangeLoading from "@components/WithLoading/useChangeLoading"; 22 | import style from "../blogDetail.module.css"; 23 | 24 | const BlogDetails = (props) => { 25 | const { data, source } = props; 26 | const { markdownHtml, tocDom } = useMdxComponent(source); 27 | 28 | useEffect(() => { 29 | addNavItemStyle(); 30 | bindHandleScroll(); 31 | 32 | return () => { 33 | removeNavItemStyle(); 34 | removeScroll(); 35 | }; 36 | }, []); 37 | 38 | useChangeLoading({ ...props, name: "blog_detail" }); 39 | 40 | const clickOperate = (type) => { 41 | switch (type) { 42 | case "forward": 43 | shareWebPage({ 44 | url: `https://wp-boke.work/blog-details/${data.id}`, 45 | title: data.title, 46 | text: data.desc, 47 | }); 48 | return; 49 | } 50 | }; 51 | 52 | return ( 53 |
56 |
57 |
58 |
59 | 60 | 61 | clickOperate("forward")} 65 | /> 66 | 67 |
68 |
69 |
70 |
71 |
{data.title}
72 |
73 |
74 | 75 | {data.classify} 76 | | 77 | {data.classify_sub} 78 |
79 |
80 | 81 | 发布于{formatDate(data.time_str, "yyyy-MM-dd")} 最近修改 82 | {formatDate(data.last_edit_time, "yyyy-MM-dd")} 83 |
84 |
85 | 86 | {data.views} 87 |
88 |
89 | 90 | {data.likes} 91 |
92 |
93 | 94 | {hasUnicode(data?.userInfo?.name) 95 | ? unicodeToEmoji(data?.userInfo?.name) 96 | : data?.userInfo?.name} 97 |
98 |
99 |
100 |
101 | {data.storage_type === "1" && markdownHtml} 102 | {data.storage_type === "2" && ( 103 |
108 | )} 109 | {data.storage_type === "3" && data.content} 110 |
111 |
112 |
113 |
114 |
目录
115 |
{tocDom}
116 |
117 |
118 |
119 |
120 | 121 | 122 |
123 |
124 | 125 |
126 |
127 | ); 128 | }; 129 | 130 | export default withLoading(BlogDetails); 131 | -------------------------------------------------------------------------------- /app/blog-details/blogDetail.module.css: -------------------------------------------------------------------------------- 1 | .blog_detail { 2 | width: 100%; 3 | flex: 1; 4 | padding-top: 70px; 5 | background-color: var(--bg-w-pure); 6 | display: flex; 7 | flex-direction: column; 8 | align-items: center; 9 | } 10 | 11 | .blog_detail_box { 12 | display: flex; 13 | width: 100%; 14 | border-bottom: 1px solid var(--b-alpha-10); 15 | } 16 | 17 | .blog_detail_box .left_content { 18 | flex-shrink: 0; 19 | position: relative; 20 | height: 100%; 21 | padding-right: 24px; 22 | } 23 | 24 | .blog_detail_box .right_content { 25 | flex-shrink: 0; 26 | position: relative; 27 | } 28 | 29 | .blog_detail_box .left_content .operate_box { 30 | position: sticky; 31 | top: 50%; 32 | transform: translateY(-50%); 33 | width: 100%; 34 | display: flex; 35 | flex-direction: column; 36 | align-items: center; 37 | } 38 | 39 | .blog_detail_box .left_content .operate_box .operate_item { 40 | font-size: 24px; 41 | margin: 8px 0; 42 | width: 48px; 43 | height: 48px; 44 | border-radius: 50%; 45 | background-color: var(--b-alpha-10); 46 | color: var(--b-alpha-90); 47 | display: flex; 48 | align-items: center; 49 | justify-content: center; 50 | cursor: pointer; 51 | transition: 0.4s; 52 | outline: none !important; 53 | } 54 | 55 | .blog_detail_box .left_content .operate_box .operate_item:hover { 56 | background-color: var(--b-alpha-30); 57 | box-shadow: 0 0 10px 5px var(--b-alpha-20); 58 | } 59 | 60 | .blog_detail_box .right_content .blog_toc_box { 61 | position: sticky; 62 | top: 78px; 63 | } 64 | 65 | .blog_toc_title { 66 | color: var(--b-alpha-90); 67 | font-weight: 500; 68 | margin-left: 1.5em; 69 | font-size: 18px; 70 | line-height: 2rem; 71 | display: flex; 72 | justify-content: space-between; 73 | align-items: center; 74 | height: 56px; 75 | border-bottom: 1px solid var(--b-alpha-20); 76 | } 77 | 78 | .blog_toc { 79 | margin-left: 1.5em; 80 | display: flex; 81 | flex-direction: column; 82 | padding: 1em 0; 83 | max-height: calc(100vh - 140px); 84 | overflow-y: auto; 85 | } 86 | 87 | .blog_detail_box .content { 88 | flex: 1; 89 | min-width: 100px; 90 | display: flex; 91 | flex-direction: column; 92 | } 93 | 94 | .header { 95 | flex: 1; 96 | border-bottom: 3px var(--b-alpha-80) solid; 97 | padding: 40px 0; 98 | } 99 | 100 | .header .list_item_title { 101 | width: 100%; 102 | font-size: 24px; 103 | line-height: 1.4em; 104 | color: var(--b-alpha); 105 | cursor: pointer; 106 | transition: 0.1s; 107 | padding-bottom: 8px; 108 | } 109 | 110 | .header .list_item_title:hover { 111 | text-decoration: underline; 112 | font-weight: 500; 113 | } 114 | 115 | .header .list_item_info { 116 | width: 100%; 117 | display: flex; 118 | align-items: center; 119 | flex-wrap: wrap; 120 | min-height: 24px; 121 | color: var(--b-alpha-80); 122 | } 123 | 124 | .header .icon { 125 | font-size: 12px; 126 | margin-right: 4px; 127 | } 128 | 129 | .header .list_item_type { 130 | height: 24px; 131 | display: flex; 132 | align-items: center; 133 | white-space: nowrap; 134 | margin-right: 20px; 135 | } 136 | 137 | .header .blog_item_class_border { 138 | padding: 0 4px; 139 | } 140 | 141 | .header .list_item_type_item { 142 | cursor: pointer; 143 | color: var(--b-alpha); 144 | } 145 | 146 | .header .list_item_type_item:hover { 147 | text-decoration: underline; 148 | } 149 | 150 | .header .list_item_time { 151 | height: 24px; 152 | line-height: 24px; 153 | display: block; 154 | white-space: nowrap; 155 | overflow: hidden; 156 | text-overflow: ellipsis; 157 | margin-right: 20px; 158 | } 159 | 160 | .blog_content { 161 | flex: 1; 162 | display: flex; 163 | flex-direction: column; 164 | align-items: center; 165 | padding-bottom: 15px; 166 | } 167 | 168 | .footer { 169 | padding: 30px 0 30px; 170 | width: calc(100% - 48px); 171 | } 172 | 173 | .comment { 174 | width: calc(100% - 48px); 175 | } 176 | 177 | @media screen and (max-width: 800px) { 178 | .blog_detail_box { 179 | padding: 0 24px; 180 | width: 100%; 181 | } 182 | 183 | .left_content, 184 | .right_content { 185 | display: none; 186 | } 187 | 188 | .footer, 189 | .comment { 190 | width: calc(100% - 48px); 191 | } 192 | } 193 | 194 | @media screen and (min-width: 800px) { 195 | .blog_detail_box { 196 | padding: 0 24px; 197 | width: 100%; 198 | } 199 | 200 | .left_content { 201 | width: 104px; 202 | } 203 | 204 | .right_content { 205 | width: 260px; 206 | } 207 | 208 | .footer, 209 | .comment { 210 | width: calc(80% - 48px); 211 | } 212 | } 213 | 214 | @media screen and (min-width: 1200px) { 215 | .blog_detail_box { 216 | padding: 0 24px; 217 | width: 1200px; 218 | } 219 | 220 | .left_content { 221 | width: 104px; 222 | } 223 | 224 | .right_content { 225 | width: 300px; 226 | } 227 | 228 | .footer, 229 | .comment { 230 | width: calc(1200px - 48px); 231 | } 232 | } 233 | 234 | @media print { 235 | .left_content, 236 | .right_content, 237 | .footer, 238 | .comment { 239 | display: none; 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /app/blog/[slug]/page.tsx: -------------------------------------------------------------------------------- 1 | import getData from "@/utils/httpClient/request"; 2 | import PostClient from "./post-client"; 3 | 4 | export const dynamicParams = false; 5 | 6 | // 动态路由 7 | export async function generateStaticParams() { 8 | const posts = await getData({ 9 | type: "all_blog_PageList", 10 | config: { next: { revalidate: 6000 } }, 11 | }); 12 | 13 | const arr = [] as string[]; 14 | for (let i = 1; i <= posts.data; i++) { 15 | arr.push(i.toString()); 16 | } 17 | 18 | return arr.map((v) => ({ slug: v })); 19 | } 20 | 21 | // 获取数据 22 | async function getPost(params: { slug: any }) { 23 | const posts1 = await getData({ 24 | type: "all_blog_PageList", 25 | config: { next: { revalidate: 6000 } }, 26 | }); 27 | 28 | const pageList = [] as string[]; 29 | for (let i = 1; i <= posts1.data; i++) { 30 | pageList.push(i.toString()); 31 | } 32 | 33 | const posts2 = await getData({ 34 | type: "all_blog_List", 35 | params: { page: params.slug }, 36 | }); 37 | 38 | const classifyNum = await getData({ 39 | type: "all_blog_ClassifyNum", 40 | }); 41 | 42 | return { 43 | page: params.slug, 44 | totalPage: posts1.data, 45 | pageList, 46 | classifyNum: classifyNum.data, 47 | ...posts2, 48 | }; 49 | } 50 | 51 | export default async function BlogList({ params }) { 52 | const post = await getPost(params); 53 | 54 | return ; 55 | } 56 | -------------------------------------------------------------------------------- /app/blog/blog.module.css: -------------------------------------------------------------------------------- 1 | .blog { 2 | width: 100%; 3 | flex: 1; 4 | padding-top: 70px; 5 | background-color: var(--bg-w-pure); 6 | } 7 | 8 | .blog_con{ 9 | width: 75%; 10 | max-width: 1100px; 11 | margin: 0 auto; 12 | display: flex; 13 | justify-content: center; 14 | } 15 | 16 | .blog_list { 17 | width: 800px; 18 | position: relative; 19 | } 20 | 21 | .blog_content { 22 | width: 100%; 23 | padding-top: 24px; 24 | display: flex; 25 | flex-direction: column; 26 | } 27 | 28 | .blog_Pagination { 29 | width: 100%; 30 | } 31 | 32 | .blog_right { 33 | flex: 1; 34 | padding: 16px; 35 | position: relative; 36 | } 37 | 38 | .blog_right_content{ 39 | position: sticky; 40 | top: 86px; 41 | display: flex; 42 | flex-direction: column; 43 | } 44 | 45 | .blog_search { 46 | height: 50px; 47 | padding: 12px; 48 | } 49 | 50 | .blog_search_input { 51 | border-radius: 8px; 52 | background-color: transparent; 53 | color: var(--b-alpha-90); 54 | } 55 | 56 | .blog_search_input:hover{ 57 | border-color: var(--purple-text-hover); 58 | } 59 | 60 | .blog_class { 61 | width: 100%; 62 | padding: 12px; 63 | } 64 | 65 | .blog_class_title { 66 | height: 40px; 67 | line-height: 40px; 68 | color: var(--b-alpha-90); 69 | margin-bottom: 18px; 70 | font-size: 16px; 71 | padding-bottom: 5px; 72 | border-bottom: 1px solid var(--b-alpha-30); 73 | } 74 | 75 | .blog_class_content{ 76 | display: flex; 77 | flex-wrap: wrap; 78 | } 79 | 80 | .blog_class_item { 81 | display: flex; 82 | align-items: center; 83 | justify-content: center; 84 | margin: 0 12px 8px 0; 85 | color: var(--b-alpha-60); 86 | transition: 0.4s; 87 | cursor: pointer; 88 | text-transform: uppercase; 89 | } 90 | 91 | .blog_class_item_active,.blog_class_item_active:hover{ 92 | color: var(--purple-primary); 93 | } 94 | 95 | .blog_class_item_num{ 96 | font-size: 12px; 97 | color: var(--b-alpha-50); 98 | transform: translateY(-30%) scale(0.8); 99 | } 100 | 101 | .blog_class_item:hover { 102 | color: var(--b-alpha-90); 103 | } 104 | 105 | .blog_item { 106 | width: 100%; 107 | height: 230px; 108 | padding: 34px 16px; 109 | display: flex; 110 | align-items: center; 111 | border-bottom: 1px solid var(--b-alpha-10); 112 | transition: 0.4s; 113 | } 114 | 115 | /* .blog_item:hover{ 116 | transform: scale(1.06); 117 | } */ 118 | 119 | .blog_card_img_box { 120 | flex-shrink: 0; 121 | width: 300px; 122 | height: 100%; 123 | padding-right: 16px; 124 | } 125 | 126 | .blog_card_img { 127 | width: 100%; 128 | height: 100%; 129 | object-fit: cover; 130 | border-radius: 12px; 131 | } 132 | 133 | .blog_item_content { 134 | width: 100%; 135 | height: 100%; 136 | display: flex; 137 | flex-direction: column; 138 | } 139 | 140 | .blog_item_title { 141 | width: 100%; 142 | height: 35px; 143 | font-size: 20px; 144 | margin-bottom: 18px; 145 | overflow: hidden; 146 | text-overflow: ellipsis; 147 | white-space: nowrap; 148 | color: var(--b-alpha-75); 149 | } 150 | 151 | .blog_item_title:hover { 152 | color: var(--purple-text-hover); 153 | } 154 | 155 | .blog_item_desc { 156 | width: 100%; 157 | flex: 1; 158 | font-size: 14px; 159 | line-height: 1.6em; 160 | color: var(--b-alpha-70); 161 | display: -webkit-box; 162 | -webkit-box-orient: vertical; 163 | -webkit-line-clamp: 3; 164 | text-overflow: ellipsis; 165 | overflow: hidden; 166 | } 167 | 168 | .blog_item_footer { 169 | width: 100%; 170 | padding-top: 24px; 171 | display: flex; 172 | align-items: center; 173 | flex-wrap: wrap; 174 | color: var(--b-alpha-70); 175 | } 176 | 177 | .blog_item_class { 178 | color: var(--b-alpha-60); 179 | display: flex; 180 | align-items: center; 181 | padding-right: 16px; 182 | } 183 | 184 | .blog_item_class_border { 185 | padding: 0 4px; 186 | } 187 | 188 | .blog_item_time { 189 | padding-right: 16px; 190 | letter-spacing: 0; 191 | } 192 | 193 | .blog_item_data { 194 | flex: 1; 195 | display: flex; 196 | align-items: center; 197 | } 198 | 199 | .blog_item_browse, 200 | .blog_item_follow { 201 | display: flex; 202 | align-items: center; 203 | padding-right: 8px; 204 | } 205 | 206 | .blog_item_icon { 207 | padding-right: 4px; 208 | } 209 | 210 | .blog_Pagination { 211 | width: 100%; 212 | height: 80px; 213 | display: flex; 214 | align-items: center; 215 | justify-content: center; 216 | } 217 | 218 | @media screen and (max-width: 1200px) { 219 | .blog_con { 220 | width: 100%; 221 | } 222 | 223 | .blog_left { 224 | display: none; 225 | } 226 | 227 | .blog_list { 228 | width: 700px; 229 | } 230 | 231 | .blog_right { 232 | flex: none; 233 | width: 260px; 234 | } 235 | } 236 | 237 | @media screen and (max-width: 1000px) { 238 | .blog_con { 239 | width: 100%; 240 | } 241 | 242 | .blog_left, 243 | .blog_right { 244 | display: none; 245 | } 246 | 247 | .blog_list { 248 | width: calc(100% - 40px); 249 | } 250 | } 251 | 252 | @media screen and (max-width: 700px) { 253 | .blog_con { 254 | width: 100%; 255 | } 256 | 257 | .blog_item { 258 | flex-direction: column; 259 | height: auto; 260 | } 261 | 262 | .blog_card_img_box { 263 | width: 100%; 264 | height: 200px; 265 | padding-right: 0; 266 | } 267 | 268 | .blog_item_content { 269 | padding-top: 12px; 270 | } 271 | 272 | .blog_item_desc { 273 | height: 68px; 274 | } 275 | } 276 | 277 | @media print{ 278 | .blog_right{ 279 | display: none; 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /app/components/ClassifyPrevOrNext/classifyPrevOrNext.module.css: -------------------------------------------------------------------------------- 1 | .prev_next { 2 | width: 100%; 3 | max-width: 800px; 4 | margin: 0 auto; 5 | padding: 20px; 6 | border-radius: 8px; 7 | background-color: var(--purple-bg); 8 | margin-top: 15px; 9 | display: flex; 10 | align-items: flex-start; 11 | justify-content: space-between; 12 | } 13 | 14 | .prev, 15 | .next { 16 | display: flex; 17 | flex-direction: column; 18 | } 19 | 20 | .prev { 21 | align-items: flex-start; 22 | } 23 | 24 | .next { 25 | align-items: flex-end; 26 | } 27 | 28 | .prev_title, 29 | .next_title { 30 | font-size: 18px; 31 | font-weight: 700; 32 | padding-bottom: 8px; 33 | color: var(--b-alpha-80); 34 | white-space: nowrap; 35 | } 36 | 37 | .prev_content, 38 | .next_content { 39 | font-size: 14px; 40 | color: var(--b-alpha-80); 41 | } 42 | 43 | .prev_content:hover, 44 | .next_content:hover { 45 | color: var(--color-blue); 46 | text-decoration: underline; 47 | } 48 | 49 | .prev_content { 50 | text-align: start; 51 | padding-right: 12px; 52 | } 53 | 54 | .next_content { 55 | text-align: end; 56 | padding-left: 12px; 57 | } 58 | 59 | .disabled:hover { 60 | color: var(--b-alpha-50); 61 | text-decoration: none; 62 | } 63 | -------------------------------------------------------------------------------- /app/components/ClassifyPrevOrNext/index.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import Link from "next/link"; 3 | import React, { useCallback, useEffect, useState } from "react"; 4 | import getDataApi from "@/utils/httpClient/request"; 5 | import style from "./classifyPrevOrNext.module.css"; 6 | 7 | type Props = { 8 | id: number | string; 9 | }; 10 | 11 | const ClassifyPrevOrNext = (props: Props) => { 12 | const { id } = props; 13 | 14 | const [data, setData] = useState([]); 15 | 16 | const getData = useCallback(async () => { 17 | const posts = await getDataApi({ 18 | type: "all_blog_NextOrPrev", 19 | params: { id: id }, 20 | }); 21 | setData(posts.data); 22 | }, [id]); 23 | 24 | useEffect(() => { 25 | getData(); 26 | }, [getData]); 27 | return ( 28 |
29 | (data[0]?.obj.id ? "" : e.preventDefault())} 33 | > 34 |
上一篇
35 |
40 | {data[0]?.obj.id ? data[0]?.obj.title : "没有了"} 41 |
42 | 43 | (data[1]?.obj.id ? "" : e.preventDefault())} 47 | > 48 |
下一篇
49 |
54 | {data[1]?.obj.id ? data[1]?.obj.title : "没有了"} 55 |
56 | 57 |
58 | ); 59 | }; 60 | 61 | export default ClassifyPrevOrNext; 62 | -------------------------------------------------------------------------------- /app/components/CodeBlock/index.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { useEffect } from "react"; 3 | import hljs from "highlight.js"; 4 | 5 | const CodeBlock = ({ language, children }) => { 6 | useEffect(() => { 7 | // 重新渲染前取消所有dataset.highlighted 8 | const highlightedElements = document.querySelectorAll(".hljs"); 9 | highlightedElements.forEach((element: any) => { 10 | if(element.dataset){ 11 | delete element.dataset.highlighted 12 | } 13 | }); 14 | // 页面加载完成 15 | hljs.configure({ 16 | ignoreUnescapedHTML: true, 17 | throwUnescapedHTML: false, 18 | }); 19 | // 高亮所有代码块 20 | hljs.highlightAll(); 21 | }, []); 22 | 23 | return {children}; 24 | }; 25 | 26 | export default CodeBlock; 27 | -------------------------------------------------------------------------------- /app/components/Comment/comment.module.css: -------------------------------------------------------------------------------- 1 | .tcomment{ 2 | width: 100%; 3 | /* padding: 16px; */ 4 | /* background-color: var(--purple-border); */ 5 | /* border-radius: 8px; */ 6 | margin: 0 auto; 7 | margin-bottom: 24px; 8 | max-width: 800px; 9 | } 10 | 11 | .border{ 12 | width: 100%; 13 | height: 2px; 14 | background-color: var(--b-alpha-10); 15 | margin-bottom: 24px; 16 | } 17 | -------------------------------------------------------------------------------- /app/components/Comment/index.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import React, { useEffect } from "react"; 3 | import style from "./comment.module.css"; 4 | 5 | type Props = {}; 6 | 7 | const Comment = (props: Props) => { 8 | const {} = props; 9 | 10 | useEffect(() => { 11 | async function initTwikoo() { 12 | try { 13 | const twikoo = await import("twikoo"); 14 | const _window: any = window; 15 | _window.twikoo = twikoo; 16 | _window.twikoo.init({ 17 | // envId: "https://comment.wp-boke.work", 18 | // 腾讯云环境填 envId;Vercel 环境填地址(https://xxx.vercel.app) 19 | envId: "https://comment.shimmer.work", 20 | el: "#tcomment", // 容器元素 21 | // region: 'ap-guangzhou', // 环境地域,默认为 ap-shanghai,腾讯云环境填 ap-shanghai 或 ap-guangzhou;Vercel 环境不填 22 | // path: location.pathname, // 用于区分不同文章的自定义 js 路径,如果您的文章路径不是 location.pathname,需传此参数 23 | // lang: 'zh-CN', // 用于手动设定评论区语言,支持的语言列表 https://github.com/imaegoo/twikoo/blob/main/src/client/utils/i18n/index.js 24 | }); 25 | } catch (error) { 26 | console.error(error); 27 | } 28 | } 29 | 30 | initTwikoo(); 31 | }, []); 32 | 33 | return ( 34 |
35 |
36 |
37 |
38 | ); 39 | }; 40 | 41 | export default Comment; 42 | -------------------------------------------------------------------------------- /app/components/Footer/footer.module.css: -------------------------------------------------------------------------------- 1 | .footer { 2 | width: 100%; 3 | padding: 30px 0 20px; 4 | background-color: var(--w-alpha-70); 5 | font-size: 12px; 6 | display: flex; 7 | flex-wrap: wrap; 8 | justify-content: center; 9 | border-top: 1px solid var(--b-alpha-10); 10 | backdrop-filter: blur(8px); 11 | align-items: flex-start; 12 | } 13 | 14 | .copyright { 15 | min-width: 220px; 16 | display: flex; 17 | flex-direction: column; 18 | padding: 12px; 19 | } 20 | 21 | .other_websites, 22 | .contact { 23 | min-width: 150px; 24 | display: flex; 25 | flex-wrap: wrap; 26 | justify-content: center; 27 | flex-direction: column; 28 | padding: 12px; 29 | } 30 | 31 | .user { 32 | text-align: start; 33 | padding: 3px 30px; 34 | color: var(--b-alpha); 35 | display: flex; 36 | align-items: center; 37 | } 38 | 39 | .legalText { 40 | display: flex; 41 | flex-wrap: wrap; 42 | justify-content: center; 43 | flex-direction: column; 44 | } 45 | 46 | .link { 47 | padding: 3px 30px; 48 | color: var(--b-alpha-80); 49 | white-space: nowrap; 50 | height: 23px; 51 | display: flex; 52 | align-items: center; 53 | } 54 | 55 | .title { 56 | text-align: start; 57 | padding: 3px 30px; 58 | color: var(--b-alpha-90); 59 | font-size: 14px; 60 | font-weight: 500; 61 | } 62 | 63 | .click { 64 | cursor: pointer; 65 | transition: 0.4s; 66 | } 67 | 68 | .click:hover { 69 | color: var(--b-alpha); 70 | } 71 | 72 | .link_click { 73 | cursor: pointer; 74 | transition: 0.4s; 75 | } 76 | 77 | .link_click:hover { 78 | color: var(--color-blue-hover); 79 | } 80 | 81 | .github_link{ 82 | display: flex; 83 | align-items: center; 84 | margin-left: 4px; 85 | } 86 | 87 | @media screen and (max-width: 700px) { 88 | .user, 89 | .title { 90 | padding: 3px 30px; 91 | } 92 | 93 | .legalText { 94 | flex-direction: column; 95 | align-items: flex-start; 96 | } 97 | 98 | .other_websites, 99 | .contact { 100 | flex-direction: column; 101 | align-items: flex-start; 102 | } 103 | 104 | .copyright, 105 | .other_websites, 106 | .contact { 107 | width: 260px; 108 | } 109 | } 110 | 111 | .link_img{ 112 | width: auto; 113 | height: 16px; 114 | } 115 | 116 | @media screen and (max-width: 760px) and (min-width: 700px) { 117 | .user { 118 | padding: 3px 30px; 119 | } 120 | 121 | .legalText { 122 | flex-direction: column; 123 | align-items: flex-start; 124 | } 125 | } 126 | 127 | @media print { 128 | .footer{ 129 | display: none; 130 | } 131 | } 132 | 133 | -------------------------------------------------------------------------------- /app/components/Footer/index.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import Link from "next/link"; 3 | import Image from "next/image"; 4 | import { useContext } from "react"; 5 | import { LayoutContext } from "@store/layoutStore"; 6 | import TravellingDark from "@public/images/travelling-dark.png"; 7 | import TravellingLight from "@public/images/travelling-light.png"; 8 | import style from "./footer.module.css"; 9 | 10 | const Footer = () => { 11 | const { theme } = useContext(LayoutContext); 12 | return ( 13 |
14 |
15 |
站内索引
16 | 21 | 站点地图 22 | 23 | 28 | RSS订阅 29 | 30 | 34 | 友情链接 35 | 36 |
37 |
38 |
外部链接
39 | 46 | 十年之约-虫洞 57 | 58 | 65 | 开往 72 | 73 | 80 | 十年之约 87 | 88 |
89 |
90 |
联系我
91 |
qq: 1818784856
92 |
微信: wp0403-
93 | 99 | 邮箱: webwp0403@163.com 100 | 101 |
102 |
103 |
104 | © {new Date().getFullYear()} Shimmer🌈 | 105 | 110 | github 120 | 121 |
122 |
123 | 127 | 版权声明 128 | 129 | 133 | 免责声明 134 | 135 | 141 | 京ICP备2022004838号-1 142 | 143 |
144 |
145 |
146 | ); 147 | }; 148 | 149 | export default Footer; 150 | -------------------------------------------------------------------------------- /app/components/LazyCom/index.module.css: -------------------------------------------------------------------------------- 1 | .lazyImg{ 2 | position: relative; 3 | display: inline-block; 4 | } 5 | 6 | .lazyImg :global .ant-image{ 7 | position: absolute; 8 | left: 0; 9 | top: 0; 10 | } 11 | 12 | .photography_image { 13 | object-fit: cover; 14 | padding: 4px; 15 | box-sizing: border-box; 16 | width: 100%; 17 | height: 100%; 18 | } 19 | 20 | .photography_image_div{ 21 | border: 4px solid var(--bg-w-pure); 22 | box-sizing: border-box; 23 | } 24 | 25 | .photography_image_pa{ 26 | position: absolute; 27 | left: 0; 28 | top: 0; 29 | } 30 | 31 | .photography_image_antd{ 32 | object-fit: cover; 33 | padding: 4px; 34 | box-sizing: border-box; 35 | display: none; 36 | width: 100%; 37 | height: 100%; 38 | } 39 | 40 | .photography_image_auto{ 41 | height: auto !important; 42 | } 43 | 44 | @media screen and (max-width: 600px) { 45 | .photography_image { 46 | padding: 2px; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/components/LazyCom/index.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import Image from "next/image"; 3 | import React, { useEffect, useRef, useState } from "react"; 4 | import { Image as AntImage } from "antd"; 5 | import { useInViewport } from "ahooks"; 6 | import { getRandomColor } from "@utils/dataUtils"; 7 | import style from "./index.module.css"; 8 | 9 | type Props = { 10 | imgSrc: string; 11 | domKey?: number | string; 12 | width: number | string; 13 | className?: string; 14 | reset?: object; 15 | }; 16 | 17 | const LazyCom = (props: Props) => { 18 | const { className, imgSrc, domKey, width, reset = {} } = props; 19 | const ref = useRef(null); 20 | const backgroundColor = useRef(getRandomColor()); 21 | const [src, setSrc] = useState(); 22 | const [inViewport] = useInViewport(ref); 23 | const [isLoad, setIsLoad] = useState(false); 24 | const [isAntdLoad, setIsAntdLoad] = useState(false); 25 | const [isBrowser, setIsBrowser] = useState(false); 26 | 27 | useEffect(() => { 28 | setIsBrowser(true); 29 | }, []); 30 | 31 | useEffect(() => { 32 | inViewport && !src && !isLoad && setSrc(imgSrc); 33 | }, [imgSrc, inViewport, src, isLoad]); 34 | return ( 35 | 44 | {src && ( 45 | <> 46 | { 52 | !isLoad && setIsLoad(true); 53 | }} 54 | src={imgSrc} 55 | quality={100} 56 | {...reset} 57 | /> 58 | {isLoad && ( 59 | { 64 | setIsAntdLoad(true); 65 | }} 66 | preview={isAntdLoad} 67 | alt="" 68 | src={imgSrc} 69 | rootClassName={`${className}`} 70 | /> 71 | )} 72 | 73 | )} 74 | {!isLoad && ( 75 |
89 | )} 90 | 91 | ); 92 | }; 93 | 94 | export default LazyCom; 95 | -------------------------------------------------------------------------------- /app/components/MdxComponent/index.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import Link from "next/link"; 3 | import { MDXRemote, MDXRemoteProps } from "next-mdx-remote"; 4 | import { CopyToClipboard } from "react-copy-to-clipboard"; 5 | import { Image, message } from "antd"; 6 | import React, { useMemo, useEffect } from "react"; 7 | import { useGetState } from "ahooks"; 8 | import CodeBlock from "@components/CodeBlock"; 9 | 10 | export default function useMdx(props: MDXRemoteProps) { 11 | const [tocList, setTocList, getTocList] = useGetState([]); 12 | const handleCopy = () => { 13 | message.success("复制成功"); 14 | }; 15 | 16 | // 将标题内容转换成id时可以用的字符串 17 | const generateUniqueId = (text = "") => { 18 | if (typeof text !== "string") { 19 | return ""; 20 | } 21 | // 将文本转换为小写,去除空格,并使用一种哈希函数生成数字 22 | const hash = text 23 | .trim() 24 | .toLowerCase() 25 | .replace(/\s/g, "") 26 | .split("") 27 | .reduce(function (acc, char) { 28 | return acc + char.charCodeAt(0); 29 | }, 0); 30 | 31 | // 使用前缀加上哈希值,以确保唯一性 32 | const uniqueId = "anchor_" + hash; 33 | 34 | return uniqueId; 35 | }; 36 | 37 | // 生成唯一id 38 | const generateHeadingId = (string, level) => { 39 | return generateUniqueId(string) + "-" + level; 40 | }; 41 | 42 | const flag = 43 | typeof window !== "undefined" 44 | ? document.querySelector(".markdown_body")?.innerHTML 45 | : ""; 46 | 47 | useEffect(() => { 48 | const list = document.querySelectorAll(".markdown-toc-item"); 49 | 50 | const newList = Array.from(list).map((v: any) => { 51 | return { 52 | id: v.id, 53 | title: v.innerText, 54 | i: v.nodeName.split("")[1], 55 | }; 56 | }); 57 | 58 | setTocList(newList); 59 | }, [flag]); 60 | 61 | const renderTitle = (node, type) => { 62 | const newChlidren = Array.isArray(node.children) 63 | ? node.children[0] 64 | : node.children; 65 | const id = generateHeadingId(newChlidren, type); 66 | switch (type) { 67 | case 1: 68 | return ( 69 |

70 | {newChlidren} 71 |

72 | ); 73 | case 2: 74 | return ( 75 |

76 | {newChlidren} 77 |

78 | ); 79 | case 3: 80 | return ( 81 |

82 | {newChlidren} 83 |

84 | ); 85 | case 4: 86 | return ( 87 |

88 | {newChlidren} 89 |

90 | ); 91 | case 5: 92 | return ( 93 |
94 | {newChlidren} 95 |
96 | ); 97 | case 6: 98 | return ( 99 |
100 | {newChlidren} 101 |
102 | ); 103 | } 104 | }; 105 | 106 | const markdownHtml = useMemo( 107 | () => ( 108 |
109 | 110 | renderTitle(node, 1), 114 | h2: (node: any) => renderTitle(node, 2), 115 | h3: (node: any) => renderTitle(node, 3), 116 | h4: (node: any) => renderTitle(node, 4), 117 | h5: (node: any) => renderTitle(node, 5), 118 | h6: (node: any) => renderTitle(node, 6), 119 | code({ className, children, ...props }) { 120 | const match = /language-(\w+)/.exec(className || ""); 121 | return ( 122 | <> 123 | {match && match[1] && ( 124 |
125 |
{match[1]}
126 | 130 |
复制代码
131 |
132 |
133 | )} 134 | 135 | {String(children).replace(/\n$/, "")} 136 | 137 | 138 | ); 139 | }, 140 | img({ src, alt }) { 141 | return ( 142 | {alt} 148 | ); 149 | }, 150 | a({ href, children }) { 151 | if (RegExp("#").test(href || "")) { 152 | return {children}; 153 | } 154 | return ( 155 | 160 | {children} 161 | 162 | ); 163 | }, 164 | p({ children }) { 165 | return
{children}
; 166 | }, 167 | }} 168 | /> 169 |
170 |
171 | ), 172 | [props] 173 | ); 174 | 175 | const tocDom = useMemo(() => { 176 | let list: any[] = getTocList().map((v) => v.i); 177 | list = [...new Set(list)].sort((a, b) => a - b); 178 | 179 | return ( 180 | <> 181 | {getTocList().map((v) => { 182 | return ( 183 |
val == v.i 186 | )}`} 187 | key={v.id} 188 | onClick={() => { 189 | const top = document.getElementById(v.id)?.offsetTop || 0; 190 | document.body.scrollTo({ 191 | left: 0, 192 | top: top - 80, 193 | behavior: "smooth", 194 | }); 195 | }} 196 | > 197 | {v.title} 198 |
199 | ); 200 | })} 201 | 202 | ); 203 | }, [tocList]); 204 | 205 | return { 206 | tocDom, 207 | markdownHtml, 208 | }; 209 | } 210 | -------------------------------------------------------------------------------- /app/components/NavBar/index.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import Link from "next/link"; 3 | import Image from "next/image"; 4 | import { usePathname } from "next/navigation"; 5 | import { useCallback, useContext, useEffect, useState } from "react"; 6 | import SysIcon from "../SysIcon"; 7 | import { navList } from "./routes"; 8 | import logo_black from "@public/images/logo_black.png"; 9 | import logo_white from "@public/images/logo_white.png"; 10 | import { handleThemeChange } from "@utils/dataUtils"; 11 | import { LayoutContext } from "@store/layoutStore"; 12 | import styles from "./navBar.module.css"; 13 | 14 | export default function Navbar() { 15 | const pathname = usePathname(); 16 | const { changeTheme } = useContext(LayoutContext); 17 | // 主题 18 | const [theme, setTheme] = useState(1); 19 | // 是否弹出遮罩 20 | const [avtive, setActive] = useState(false); 21 | 22 | // 导航item 23 | const navItem = (obj) => { 24 | return ( 25 | setActive(false)} 34 | > 35 | 36 | {obj?.title} 37 | 38 | ); 39 | }; 40 | 41 | // 切换主题 42 | const themeSwitch = useCallback( 43 | (event) => { 44 | if (event === "click") { 45 | document.documentElement.classList.toggle("dark"); 46 | setTheme(theme === 1 ? 2 : 1); 47 | changeTheme(theme === 1 ? 2 : 1); 48 | } else { 49 | setTheme(handleThemeChange(event)); 50 | changeTheme(handleThemeChange(event)); 51 | } 52 | }, 53 | [setTheme, changeTheme, theme] 54 | ); 55 | 56 | useEffect(() => { 57 | const darkModeMediaQuery = window.matchMedia( 58 | "(prefers-color-scheme: light)" 59 | ); 60 | 61 | // 添加一个监听器来监听主题切换 62 | darkModeMediaQuery && 63 | darkModeMediaQuery.addEventListener("change", themeSwitch); 64 | themeSwitch(darkModeMediaQuery); 65 | 66 | return () => { 67 | darkModeMediaQuery && 68 | darkModeMediaQuery.removeEventListener("change", themeSwitch); 69 | }; 70 | }, []); 71 | 72 | return ( 73 | <> 74 | 121 |
setActive(false)} 126 | /> 127 |
132 |
setActive(!avtive)} 135 | > 136 | 137 |
138 | 157 |
158 | 159 | ); 160 | } 161 | -------------------------------------------------------------------------------- /app/components/NavBar/navBar.module.css: -------------------------------------------------------------------------------- 1 | .nav { 2 | width: 100%; 3 | height: 70px; 4 | display: flex; 5 | align-items: center; 6 | justify-content: space-between; 7 | padding: 0 30px; 8 | position: fixed; 9 | top: 0; 10 | left: 0; 11 | z-index: 10; 12 | -webkit-user-select: none; /* Safari */ 13 | -moz-user-select: none; /* Firefox */ 14 | -ms-user-select: none; /* Internet Explorer/Edge */ 15 | user-select: none; /* Non-prefixed version */ 16 | } 17 | 18 | .nav_left { 19 | display: flex; 20 | align-items: center; 21 | padding-left: 24px; 22 | } 23 | 24 | .title { 25 | font-family: "Times New Roman", Times, serif; 26 | font-size: 20px; 27 | white-space: nowrap; 28 | color: var(--txt-w-pure); 29 | } 30 | 31 | .title:hover { 32 | color: var(--txt-w-pure); 33 | } 34 | 35 | .nav_right { 36 | flex: 1; 37 | display: flex; 38 | align-items: center; 39 | justify-content: flex-end; 40 | } 41 | 42 | .nav_list { 43 | display: flex; 44 | align-items: center; 45 | margin-right: 20px; 46 | justify-content: flex-end; 47 | } 48 | 49 | .nav_item { 50 | margin: 0 5px; 51 | padding: 5px 10px; 52 | display: flex; 53 | align-items: center; 54 | color: var(--txt-w-pure); 55 | } 56 | 57 | /* 当前选中导航的样式 */ 58 | .nav_item_active{ 59 | background-color: var(--b-alpha-5); 60 | border-radius: 5px; 61 | } 62 | 63 | .nav_item:hover { 64 | background-color: var(--b-alpha-5); 65 | border-radius: 5px; 66 | color: var(--txt-w-pure); 67 | } 68 | 69 | .nav_item_icon { 70 | margin-right: 4px; 71 | font-size: 18px; 72 | } 73 | 74 | .nav_item_title { 75 | font-size: 16px; 76 | white-space: nowrap; 77 | } 78 | 79 | .nav_type { 80 | padding: 0 10px; 81 | display: flex; 82 | align-items: center; 83 | background: var(--ct-theme); 84 | border: var(--ct-theme-border); 85 | border-radius: 20px; 86 | overflow: hidden; 87 | } 88 | 89 | .nav_type_item { 90 | padding: 5px; 91 | font-size: 24px; 92 | transform: translateY(-40px); 93 | transition: 1s; 94 | } 95 | 96 | .nav_type_item_active { 97 | transform: translateY(0); 98 | } 99 | 100 | /* mobile */ 101 | .nav_mobile { 102 | flex: 1; 103 | height: 100%; 104 | display: none; 105 | align-items: center; 106 | justify-content: flex-end; 107 | } 108 | 109 | .nav_mobile_btn { 110 | padding: 0 20px; 111 | height: 100%; 112 | display: flex; 113 | align-items: center; 114 | font-size: 24px; 115 | color: var(--b-alpha); 116 | } 117 | 118 | .nav_mobile_mask { 119 | position: fixed; 120 | right: 0; 121 | top: 0; 122 | width: 100vw; 123 | height: 100vh; 124 | z-index: 10; 125 | background-color: rgba(0, 0, 0, 0.45); 126 | backdrop-filter: blur(10px); 127 | justify-content: flex-end; 128 | visibility: hidden; 129 | opacity: 0; 130 | transition: 0.25s; 131 | } 132 | 133 | .nav_mobile_content { 134 | position: fixed; 135 | right: 0; 136 | top: 0; 137 | z-index: 20; 138 | width: 66vw; 139 | max-width: 300px; 140 | height: 100vh; 141 | background-color: var(--w-alpha-80); 142 | -webkit-backdrop-filter: blur(30px) saturate(180%); 143 | backdrop-filter: blur(30px) saturate(180%); 144 | flex-direction: column; 145 | visibility: hidden; 146 | transform: translateX(100%); 147 | transition: 0.25s; 148 | } 149 | 150 | .nav_mobile_content .nav_mobile_btn { 151 | display: flex; 152 | align-items: center; 153 | justify-content: flex-end; 154 | height: 70px; 155 | } 156 | 157 | .nav_mobile_list { 158 | flex: 1; 159 | display: flex; 160 | align-items: center; 161 | justify-content: flex-end; 162 | padding: 10px; 163 | flex-direction: column; 164 | } 165 | 166 | .nav_mobile_list .nav_list { 167 | width: 100%; 168 | align-items: center; 169 | flex-direction: column; 170 | margin-right: 0; 171 | border-bottom: 1px solid var(--b-alpha-5); 172 | } 173 | 174 | .nav_mobile_list .nav_item { 175 | width: 100%; 176 | padding: 15px 30px; 177 | margin: 0; 178 | color: var(--b-alpha) !important; 179 | } 180 | 181 | .nav_mobile_list .nav_type { 182 | margin-top: 20px; 183 | } 184 | 185 | @media screen and (max-width: 1040px) { 186 | .nav { 187 | padding: 0; 188 | } 189 | } 190 | 191 | @media screen and (max-width: 980px) { 192 | .nav { 193 | padding: 0; 194 | } 195 | .nav_right { 196 | display: none; 197 | } 198 | 199 | .nav_mobile { 200 | display: flex; 201 | } 202 | 203 | .nav_mobile_mask_active { 204 | visibility: visible; 205 | opacity: 1; 206 | } 207 | 208 | .nav_mobile_content_active { 209 | visibility: visible; 210 | transform: translateX(0); 211 | } 212 | } 213 | 214 | @media print { 215 | .nav { 216 | padding: 0; 217 | } 218 | 219 | .nav_right { 220 | display: none; 221 | } 222 | 223 | .nav_mobile { 224 | display: flex; 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /app/components/NavBar/routes.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Descripttion: 3 | * @version: 4 | * @Author: WangPeng 5 | * @Date: 2023-01-12 14:27:58 6 | * @LastEditors: WangPeng 7 | * @LastEditTime: 2023-12-26 17:03:03 8 | */ 9 | export const navList = [ 10 | { 11 | key: "home", 12 | href: "/", 13 | icon: "icon-zhuye", 14 | title: "首页", 15 | }, 16 | { 17 | key: "blog", 18 | href: "/blog/1", 19 | icon: "icon-16", 20 | title: "文章", 21 | }, 22 | { 23 | key: "archive", 24 | href: "/archive", 25 | icon: "icon-guidang", 26 | title: "文归档", 27 | }, 28 | { 29 | key: "tree-hole", 30 | href: "/tree-hole", 31 | icon: "icon--_liaotian", 32 | title: "树洞", 33 | }, 34 | { 35 | key: "photography", 36 | href: "/photography", 37 | icon: "icon-sheying", 38 | title: "摄影", 39 | }, 40 | { 41 | key: "friendly-links", 42 | href: "/friendly-links", 43 | icon: "icon-icon_xinyong_xianxing_jijin-", 44 | title: "友情链接", 45 | }, 46 | { 47 | key: "about", 48 | href: "/about", 49 | icon: "icon-geren", 50 | title: "关于", 51 | }, 52 | { 53 | key: "more", 54 | href: "/more", 55 | icon: "icon-fenlei", 56 | title: "更多", 57 | }, 58 | ]; 59 | -------------------------------------------------------------------------------- /app/components/PagerComponent/index.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import SysIcon from "@components/SysIcon"; 5 | import styles from "./pager.module.css"; 6 | 7 | interface Props { 8 | total: number; 9 | pageSize: number; 10 | current: number; 11 | onChange: (page: number) => void; 12 | } 13 | 14 | const PagerComponent = (props: Props) => { 15 | const { total, pageSize, current, onChange } = props; 16 | const totalPage = Math.ceil(total / pageSize); 17 | 18 | const clickPage = (type) => { 19 | if (type === "prev" && current > 1) { 20 | onChange(current - 1); 21 | } 22 | if (type === "next" && current < totalPage) { 23 | onChange(current + 1); 24 | } 25 | }; 26 | 27 | return ( 28 |
29 |
onChange(1)} 34 | > 35 | 首页 36 |
37 |
clickPage("prev")} 42 | > 43 | 44 |
45 |
46 | {current} 47 | / 48 | {totalPage} 49 |
50 |
= totalPage ? styles.btn_disabled : "" 53 | }`} 54 | onClick={() => clickPage("next")} 55 | > 56 | 57 |
58 |
= totalPage ? styles.btn_disabled : "" 61 | }`} 62 | onClick={() => onChange(totalPage)} 63 | > 64 | 尾页 65 |
66 |
67 | ); 68 | }; 69 | 70 | export default PagerComponent; 71 | -------------------------------------------------------------------------------- /app/components/PagerComponent/pager.module.css: -------------------------------------------------------------------------------- 1 | .gaper{ 2 | display: flex; 3 | align-items: center; 4 | } 5 | 6 | .prev_btn,.next_btn{ 7 | width: 32px; 8 | height: 32px; 9 | display: flex; 10 | align-items: center; 11 | justify-content: center; 12 | font-size: 24px; 13 | margin: 0 8px; 14 | border-radius: 8px; 15 | transition: 0.25s; 16 | cursor: pointer; 17 | color: var(--b-alpha); 18 | } 19 | 20 | .prev_btn:hover,.next_btn:hover{ 21 | background-color: var(--purple-text-hover); 22 | } 23 | 24 | 25 | .prev_btn:hover .icon,.next_btn:hover .icon{ 26 | color: var(--w-alpha); 27 | } 28 | 29 | .show{ 30 | display: flex; 31 | align-items: center; 32 | } 33 | 34 | .text{ 35 | font-size: 14px; 36 | color: var(--b-alpha-80); 37 | padding: 0 2px; 38 | } 39 | 40 | .first_page,.last_page{ 41 | font-size: 14px; 42 | cursor: pointer; 43 | color: var(--b-alpha-80); 44 | padding: 0 8px; 45 | border-radius: 8px; 46 | height: 32px; 47 | display: flex; 48 | align-items: center; 49 | justify-content: center; 50 | transition: 0.25s; 51 | } 52 | 53 | .first_page:hover,.last_page:hover{ 54 | background-color: var(--purple-text-hover); 55 | color: var(--w-alpha); 56 | } 57 | 58 | .btn_disabled{ 59 | cursor:not-allowed; 60 | color: var(--b-alpha-40); 61 | } 62 | -------------------------------------------------------------------------------- /app/components/Permit/index.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | /* 4 | * @Descripttion:知识共享许可协议 5 | * @version: 6 | * @Author: WangPeng 7 | * @Date: 2023-05-24 13:14:12 8 | * @LastEditors: WangPeng 9 | * @LastEditTime: 2024-01-03 15:15:20 10 | */ 11 | 12 | import Link from "next/link"; 13 | import React from "react"; 14 | import style from "./permit.module.css"; 15 | import SysIcon from "@components/SysIcon"; 16 | 17 | type Props = { 18 | id: number | string; 19 | user: string; 20 | }; 21 | 22 | const Permit = (props: Props) => { 23 | const { id, user } = props; 24 | return ( 25 |
26 |
27 | 28 |
版权所属:{user}
29 |
30 |
31 | 32 |
33 | 本文链接:https://wp-boke.work/blog-details/{id} 34 |
35 |
36 |
37 | 38 |
39 | 作品许可:本作品采用 40 | 46 | 知识共享署名-相同方式共享 4.0 国际许可协议 47 | 48 | 进行许可。 49 |
50 |
51 |
52 | ); 53 | }; 54 | 55 | export default Permit; 56 | -------------------------------------------------------------------------------- /app/components/Permit/permit.module.css: -------------------------------------------------------------------------------- 1 | .permit{ 2 | width: 100%; 3 | margin: 0 auto; 4 | padding: 20px; 5 | border-radius: 8px; 6 | background-color: var(--color-blue-bg); 7 | max-width: 800px; 8 | } 9 | 10 | .copyright_owner,.article_link,.license_agreement{ 11 | padding: 6px 0px; 12 | display: flex; 13 | } 14 | 15 | .icon{ 16 | flex-shrink: 0; 17 | width: 28px; 18 | height: 28px; 19 | font-size: 12px; 20 | background-color: var(--purple-border-hover); 21 | display: flex; 22 | align-items: center; 23 | justify-content: center; 24 | border-radius: 50%; 25 | color: var(--txt-w-pure); 26 | margin-right: 16px; 27 | } 28 | 29 | .text{ 30 | font-size: 14px; 31 | color: var(--b-alpha-60); 32 | line-height: 28px; 33 | } 34 | 35 | .link{ 36 | color: var(--color-blue); 37 | } 38 | 39 | .link:hover{ 40 | text-decoration: underline; 41 | } -------------------------------------------------------------------------------- /app/components/ScrollComponent/index.module.css: -------------------------------------------------------------------------------- 1 | .scroll-container { 2 | width: 100%; 3 | max-height: 100vh; 4 | height: 100%; 5 | overflow-y: auto; 6 | scroll-behavior: smooth; 7 | } 8 | 9 | .scroll-item { 10 | width: 100%; 11 | height: auto; 12 | box-sizing: border-box; 13 | } 14 | -------------------------------------------------------------------------------- /app/components/ScrollComponent/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect, useState } from "react"; 2 | import styles from "./index.module.css"; 3 | 4 | type ListItem = string | number | boolean | object | null | undefined; 5 | 6 | interface ScrollListProps { 7 | className?: string; 8 | items: T[]; 9 | renderItem: (v: ListItem, index: number, current: number) => React.ReactNode; 10 | } 11 | 12 | type Props = { 13 | className?: string; 14 | data: ListItem[]; 15 | renderItem: (v: ListItem, index: number, current: number) => React.ReactNode; 16 | }; 17 | 18 | const ScrollList = ({ 19 | className, 20 | items, 21 | renderItem, 22 | }: ScrollListProps) => { 23 | const containerRef = useRef(null); 24 | const scrollTimeoutRef = useRef(null); 25 | const [currentIndex, setCurrentIndex] = useState(0); 26 | const startYRef = useRef(0); 27 | const endYRef = useRef(0); 28 | const deltaThreshold = 80; // 滚动距离阈值 29 | 30 | const scrollToIndex = (index) => { 31 | if (containerRef.current) { 32 | const itemHeight = containerRef.current.children[0].clientHeight; 33 | containerRef.current.scrollTo({ 34 | top: index * itemHeight, 35 | behavior: "smooth", 36 | }); 37 | setCurrentIndex(index); 38 | } 39 | }; 40 | 41 | const handleWheel = (event) => { 42 | event.preventDefault(); 43 | clearTimeout(scrollTimeoutRef.current); 44 | scrollTimeoutRef.current = setTimeout(() => { 45 | if (event.deltaY > 0) { 46 | scrollToIndex(Math.min(currentIndex + 1, items.length - 1)); 47 | } else { 48 | scrollToIndex(Math.max(currentIndex - 1, 0)); 49 | } 50 | }, 100); 51 | }; 52 | 53 | const handleTouchStart = (event) => { 54 | startYRef.current = event.touches[0].clientY; 55 | }; 56 | 57 | const handleTouchMove = (event) => { 58 | endYRef.current = event.touches[0].clientY; 59 | }; 60 | 61 | const handleTouchEnd = () => { 62 | const deltaY = startYRef.current - endYRef.current; 63 | if (deltaY > deltaThreshold) { 64 | scrollToIndex(Math.min(currentIndex + 1, items.length - 1)); 65 | } else if (deltaY < -deltaThreshold) { 66 | scrollToIndex(Math.max(currentIndex - 1, 0)); 67 | } 68 | }; 69 | 70 | useEffect(() => { 71 | const container = containerRef.current; 72 | if (container) { 73 | container.addEventListener("wheel", handleWheel); 74 | container.addEventListener("touchstart", handleTouchStart); 75 | container.addEventListener("touchmove", handleTouchMove); 76 | container.addEventListener("touchend", handleTouchEnd); 77 | 78 | return () => { 79 | container.removeEventListener("wheel", handleWheel); 80 | container.removeEventListener("touchstart", handleTouchStart); 81 | container.removeEventListener("touchmove", handleTouchMove); 82 | container.removeEventListener("touchend", handleTouchEnd); 83 | }; 84 | } 85 | }, [currentIndex, items.length]); 86 | 87 | return ( 88 |
92 | {items.map((item, index) => ( 93 |
94 | {renderItem(item, index, currentIndex)} 95 |
96 | ))} 97 |
98 | ); 99 | }; 100 | 101 | const ScrollComponent = (props: Props) => { 102 | const { data, renderItem, className } = props; 103 | return ( 104 | 105 | ); 106 | }; 107 | 108 | export default ScrollComponent; 109 | -------------------------------------------------------------------------------- /app/components/SysIcon/index.md: -------------------------------------------------------------------------------- 1 | # SysIcon 图标组件 2 | 3 | ## 代码演示 4 | 5 | ### 图标访问 https://www.iconfont.cn/manage/index?spm=a313x.7781069.1998910419.20&manage_type=myprojects&projectId=3968880&keyword=&project_type=&page= 6 | 7 | ```javascript 8 | import SysIcon from '@/components/SysIcon'; 9 | 10 | export default class TestPage extends Component { 11 | render() { 12 | return ( 13 |
14 | 15 |
16 | ); 17 | } 18 | } 19 | ``` 20 | -------------------------------------------------------------------------------- /app/components/SysIcon/index.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { createFromIconfontCN } from "@ant-design/icons"; 3 | 4 | const SysIcon = createFromIconfontCN({ 5 | scriptUrl: [ 6 | "//at.alicdn.com/t/c/font_3968880_yu7d3orxs38.js", 7 | "//wp-1302605407.cos.ap-beijing.myqcloud.com/font_3968880_qi74h90o7s/iconfont.js", 8 | ], 9 | }); 10 | 11 | export default SysIcon; 12 | -------------------------------------------------------------------------------- /app/components/VideoPlay/index.module.css: -------------------------------------------------------------------------------- 1 | .video_item{ 2 | width: 100%; 3 | height: 100%; 4 | } 5 | 6 | .player { 7 | padding-top: 0 !important; 8 | width: 100% !important; 9 | height: 100% !important; 10 | } 11 | 12 | .player video { 13 | border: none; 14 | } -------------------------------------------------------------------------------- /app/components/VideoPlay/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from "react"; 2 | import Player from "xgplayer"; 3 | import styles from "./index.module.css"; 4 | 5 | type Props = { 6 | className?: string; 7 | url: string; 8 | keyId?: string | number; 9 | current?: string | number; 10 | isShow?: Function; 11 | }; 12 | 13 | const VideoPlay = (props: Props) => { 14 | const { className, url, keyId, current, isShow } = props; 15 | 16 | const playerRef = useRef(null); 17 | 18 | const init = () => { 19 | if (current !== keyId) return; 20 | if (playerRef.current) { 21 | playerRef.current.destroy(); 22 | } 23 | playerRef.current = new Player({ 24 | id: `video-play-${keyId}`, 25 | url: url, 26 | autoplay: false, 27 | volume: 1, 28 | muted: false, 29 | fluid: true, 30 | ignores: ["fullscreen"], 31 | cssFullscreen: true, 32 | rotateFullscreen: true, 33 | // 倍速播放 34 | playbackRate: [0.5, 0.75, 1, 1.5, 2], 35 | defaultPlaybackRate: 1, 36 | }); 37 | }; 38 | 39 | useEffect(() => { 40 | init(); 41 | }, [url, keyId, current]); 42 | 43 | return ( 44 |
45 | {(!isShow || (isShow && isShow(current, keyId))) && ( 46 |
47 | )} 48 |
49 | ); 50 | }; 51 | 52 | export default VideoPlay; 53 | -------------------------------------------------------------------------------- /app/components/VirtuallyItem/index.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { useEffect, useRef, useState } from "react"; 3 | import { useSize, useGetState } from "ahooks"; 4 | import { bindHandleScroll, removeScroll } from "@/utils/elementUtils"; 5 | import style from "./virtuallyItem.module.css"; 6 | 7 | const VirtuallyItem = (props) => { 8 | const size = useSize(() => document.querySelector("body")); 9 | // 用于记录当前元素的高度 10 | const [itemHeight, setItemHeight, getItemHeight] = useGetState( 11 | null 12 | ); 13 | // 用户保存当前的元素 14 | const itemRef = useRef(null); 15 | // 判断当前元素是否在可视窗口 16 | const [isVisual, setIsVisual] = useState(true); 17 | 18 | const scrollCallback = () => { 19 | // get position relative to viewport 20 | const rect = itemRef.current?.getBoundingClientRect(); 21 | 22 | if (!rect) return; 23 | const distanceFromTop = rect.top; 24 | const distanceFromBottom = rect.bottom; 25 | // 可视区域高度 26 | const viewportHeight = 27 | window.innerHeight || document.documentElement.clientHeight; 28 | if ( 29 | (distanceFromTop > -200 && distanceFromTop < viewportHeight + 200) || 30 | (distanceFromBottom > -200 && distanceFromBottom < viewportHeight + 200) 31 | ) { 32 | setIsVisual(true); 33 | } else { 34 | setIsVisual(false); 35 | } 36 | }; 37 | 38 | useEffect(() => { 39 | bindHandleScroll(scrollCallback); 40 | 41 | return () => { 42 | removeScroll(scrollCallback); 43 | }; 44 | }, []); 45 | 46 | useEffect(() => { 47 | if (itemRef.current && getItemHeight() !== itemRef.current?.offsetHeight) { 48 | setItemHeight(itemRef.current?.offsetHeight); 49 | } 50 | }, [isVisual]); 51 | 52 | useEffect(() => { 53 | setItemHeight(null); 54 | }, [size?.width]); 55 | 56 | return ( 57 |
64 | {isVisual && props.children} 65 |
66 | ); 67 | }; 68 | 69 | export default VirtuallyItem; 70 | -------------------------------------------------------------------------------- /app/components/VirtuallyItem/virtuallyItem.module.css: -------------------------------------------------------------------------------- 1 | .virtually_item{ 2 | display: inline-block; 3 | width: 100%; 4 | } -------------------------------------------------------------------------------- /app/components/WithLoading/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import Loading from "../../loading"; 3 | 4 | const withLoading = (WrappedComponent) => { 5 | const WithLoadingComponent = (props) => { 6 | const [isLoading, setIsLoading] = useState(true); 7 | 8 | const handleLoaded = () => { 9 | setIsLoading(false); 10 | }; 11 | 12 | return ( 13 | <> 14 | {isLoading && } 15 | 20 | 21 | ); 22 | }; 23 | 24 | // 设置 displayName 帮助调试 25 | const wrappedComponentName = 26 | WrappedComponent.displayName || WrappedComponent.name || "Component"; 27 | WithLoadingComponent.displayName = `WithLoading(${wrappedComponentName})`; 28 | 29 | return WithLoadingComponent; 30 | }; 31 | 32 | export default withLoading; 33 | -------------------------------------------------------------------------------- /app/components/WithLoading/useChangeLoading.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { sessionSet, sessionGet } from "@utils/local"; 3 | 4 | const useChangeLoading = (props) => { 5 | useEffect(() => { 6 | const isCache = sessionGet(`${props.name}_cache`); 7 | 8 | // 模拟一个异步加载过程 9 | const timer = setTimeout( 10 | () => { 11 | props.onLoaded(); 12 | sessionSet(`${props.name}_cache`, true); 13 | }, 14 | isCache ? 0 : 1500 15 | ); // 最少加载时间 16 | 17 | return () => clearTimeout(timer); 18 | }, []); 19 | 20 | return null; 21 | }; 22 | 23 | export default useChangeLoading; 24 | -------------------------------------------------------------------------------- /app/copyright-notice/copyrightNotice.module.css: -------------------------------------------------------------------------------- 1 | .copyright_notice { 2 | width: 100%; 3 | flex: 1; 4 | padding-top: 70px; 5 | background-color: var(--bg-w-pure); 6 | display: flex; 7 | flex-direction: column; 8 | align-items: center; 9 | } 10 | 11 | .title { 12 | padding: 36px; 13 | font-size: 24px; 14 | font-weight: 700; 15 | color: var(--b-alpha-80); 16 | text-align: center; 17 | } 18 | 19 | .content { 20 | width: 60%; 21 | max-width: 800px; 22 | padding-bottom: 24px; 23 | } 24 | 25 | .content .p { 26 | font-size: 15px; 27 | font-weight: 500; 28 | line-height: 1.6em; 29 | padding: 4px 0; 30 | color: var(--b-alpha-90); 31 | margin-bottom: 12px; 32 | } 33 | 34 | .footer { 35 | font-size: 16px; 36 | font-weight: 500; 37 | line-height: 1.6em; 38 | margin-top: 24px; 39 | color: var(--b-alpha-80); 40 | border: 1px solid var(--b-alpha-30); 41 | border-radius: 12px; 42 | padding: 24px; 43 | } 44 | 45 | .link { 46 | color: var(--color-blue); 47 | height: 23px; 48 | } 49 | 50 | .link_click { 51 | cursor: pointer; 52 | transition: 0.4s; 53 | } 54 | 55 | .link_click:hover { 56 | color: var(--color-blue-hover); 57 | } 58 | 59 | @media screen and (max-width: 800px) { 60 | .content{ 61 | width: 90%; 62 | } 63 | } -------------------------------------------------------------------------------- /app/copyright-notice/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import Link from "next/link"; 3 | import React, { useEffect } from "react"; 4 | import { 5 | addNavItemStyle, 6 | bindHandleScroll, 7 | removeNavItemStyle, 8 | removeScroll, 9 | } from "@/utils/elementUtils"; 10 | import withLoading from "@components/WithLoading"; 11 | import useChangeLoading from "@components/WithLoading/useChangeLoading"; 12 | import style from "./copyrightNotice.module.css"; 13 | 14 | const CopyrightNotice = (props) => { 15 | useEffect(() => { 16 | addNavItemStyle(); 17 | bindHandleScroll(); 18 | 19 | return () => { 20 | removeNavItemStyle(); 21 | removeScroll(); 22 | }; 23 | }, []); 24 | 25 | useChangeLoading({ ...props, name: "copyright_notice" }); 26 | 27 | return ( 28 |
33 |
版权声明
34 |
35 |
36 | 1、本博客(域名为wp-boke.work)的所有内容(包括但不限于文字、图片、音频、视频等),除特别注明外,其余均由shimmer创作或原创,版权归shimmer个人所有。 37 |
38 |
39 | 2、未经本人授权,任何人或机构不得复制、转载、摘编或以任何其他形式使用本站内容。如需转载,请在摘要或正文部分注明出处。 40 |
41 |
42 | 3、本博客允许授权使用部分文案,需注明原作者及网址,如用于商业用途,需与原作者确认。任何未授权使用内容的行为都将被视为侵权行为,shimmer保留追究法律责任的权利。 43 |
44 |
45 | 4、本站不承担用户因使用本站所提供的服务而产生的任何直接、间接或者连带的责任和赔偿。 46 |
47 |
48 | 5、本声明的解释权及修改权归shimmer所有,并保留随时更新网站内容和服务的权利,在不做事先通知的情况下,修改本声明产生效力。 49 |
50 |
51 | 如对本博客版权声明有任何疑问或建议,请联系shimmer的 52 | 58 | 电子邮箱 59 | 60 | 或通过本博客页面上的相关联系方式进行联系。 61 |
62 |
63 |
64 | ); 65 | }; 66 | 67 | export default withLoading(CopyrightNotice); 68 | -------------------------------------------------------------------------------- /app/disclaimers/disclaimers.module.css: -------------------------------------------------------------------------------- 1 | .disclaimers { 2 | width: 100%; 3 | flex: 1; 4 | padding-top: 70px; 5 | background-color: var(--bg-w-pure); 6 | display: flex; 7 | flex-direction: column; 8 | align-items: center; 9 | } 10 | 11 | .title { 12 | padding: 36px; 13 | font-size: 24px; 14 | font-weight: 700; 15 | color: var(--b-alpha-80); 16 | text-align: center; 17 | } 18 | 19 | .content { 20 | width: 60%; 21 | max-width: 800px; 22 | padding-bottom: 24px; 23 | } 24 | 25 | .content .p { 26 | font-size: 14px; 27 | font-weight: 400; 28 | line-height: 1.6em; 29 | padding: 4px 0; 30 | color: var(--b-alpha-70); 31 | margin-bottom: 12px; 32 | } 33 | 34 | .content_title { 35 | font-size: 14px; 36 | font-weight: 500; 37 | color: var(--b-alpha-60); 38 | border-left: 3px solid var(--b-alpha-50); 39 | padding-left: 12px; 40 | margin-bottom: 24px; 41 | } 42 | 43 | .desc_title { 44 | font-size: 16px; 45 | font-weight: 500; 46 | color: var(--b-alpha-90); 47 | padding-bottom: 8px; 48 | } 49 | 50 | .footer { 51 | font-size: 16px; 52 | font-weight: 500; 53 | line-height: 1.6em; 54 | margin-top: 24px; 55 | color: var(--b-alpha-80); 56 | border: 1px solid var(--b-alpha-30); 57 | border-radius: 12px; 58 | padding: 24px; 59 | } 60 | 61 | .link { 62 | color: var(--color-blue); 63 | height: 23px; 64 | } 65 | 66 | .link_click { 67 | cursor: pointer; 68 | transition: 0.4s; 69 | } 70 | 71 | .link_click:hover { 72 | color: var(--color-blue-hover); 73 | } 74 | 75 | @media screen and (max-width: 800px) { 76 | .content{ 77 | width: 90%; 78 | } 79 | } -------------------------------------------------------------------------------- /app/disclaimers/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import Link from "next/link"; 3 | import React, { useEffect } from "react"; 4 | import { 5 | addNavItemStyle, 6 | bindHandleScroll, 7 | removeNavItemStyle, 8 | removeScroll, 9 | } from "@/utils/elementUtils"; 10 | import withLoading from "@components/WithLoading"; 11 | import useChangeLoading from "@components/WithLoading/useChangeLoading"; 12 | import style from "./disclaimers.module.css"; 13 | 14 | const Disclaimers = (props) => { 15 | useEffect(() => { 16 | addNavItemStyle(); 17 | bindHandleScroll(); 18 | 19 | return () => { 20 | removeNavItemStyle(); 21 | removeScroll(); 22 | }; 23 | }, []); 24 | 25 | useChangeLoading({ ...props, name: "disclaimers" }); 26 | 27 | return ( 28 |
31 |
免责声明
32 |
33 |
34 | 欢迎访问我的个人博客,本站提供的所有信息均在善意和合理的基础上发布和使用,但本人无法保证信息的准确性、完整性和实时性。 35 |
36 |
37 |
信息准确性和完整性
38 | 本人尽力确保本站提供的所有信息、内容和资料的准确性、完整性和实时性,但本人无法保证其完全正确、完整和即时更新。本人不能保证所有信息和内容的精确性、可靠性和适用性,因此任何使用本站提供的信息造成的后果和损失由用户自行承担。 39 |
40 |
41 |
知识产权
42 | 本人发布的所有信息、内容和资料,包括文字、图片、图形、标识、标志、图表和编程代码等,均受到相关的知识产权法律的保护。未经本人的明确授权,任何人不得使用、复制、传播、更改或销售本人的信息和内容。 43 |
44 |
45 |
免责条款
46 | 本人不对因本站内容和服务的误导性或不准确性而导致的任何直接、间接、偶然、特殊或结果性损失负责,并且对于本站上其他网站的链接或外部资源的内容、广告、产品或其他资料不负任何责任。 47 |
48 |
49 |
合法性
50 | 本人承诺所有内容都是合法的,并未侵犯他人的知识产权或其他合法权利。对于任何侵权行为或侵犯隐私和安全的行为,本人不负责并保留追究法律责任的权利。 51 |
52 |
53 |
变更和更新
54 | 本人保留对本免责声明进行随时变更、更新和修改的权利。本人建议您在访问和使用本人的博客时定期查看本免责声明并了解最新更新。 55 |
56 |
57 | 如对本博客免责声明有任何疑问或建议,请联系shimmer的 58 | 64 | 电子邮箱 65 | 66 | 或通过本博客页面上的相关联系方式进行联系。 67 |
68 |
69 |
70 | ); 71 | }; 72 | 73 | export default withLoading(Disclaimers); 74 | -------------------------------------------------------------------------------- /app/error.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Link from "next/link"; 4 | import { useEffect } from "react"; 5 | import { addLayoutNavStyle, removeLayoutNavStyle } from "@utils/elementUtils"; 6 | import styles from "@styles/error404.module.css"; 7 | 8 | export default function Custom500({ 9 | error, 10 | reset, 11 | }: { 12 | error: Error & { digest?: string }; 13 | reset: () => void; 14 | }) { 15 | useEffect(() => { 16 | addLayoutNavStyle(); 17 | 18 | return () => { 19 | removeLayoutNavStyle(); 20 | }; 21 | }, []); 22 | 23 | return ( 24 |
25 |
26 |
500
27 |
28 | 很抱歉,服务器发生了预期之外的错误,请联系管理员修复。 29 |
30 |
31 | Sorry, the server encountered an unexpected error. Please contact the 32 | administrator to fix it. 33 |
34 |
35 |
36 |
37 | 刷新 38 |
39 | 40 | 回到首页 41 | 42 | 47 | 联系我 48 | 49 |
50 |
51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /app/friendly-links/friendlyLinks.module.css: -------------------------------------------------------------------------------- 1 | .friendly_links { 2 | width: 100%; 3 | flex: 1; 4 | padding-top: 70px; 5 | background-color: var(--bg-w-pure); 6 | display: flex; 7 | flex-direction: column; 8 | align-items: center; 9 | } 10 | 11 | .title { 12 | padding: 36px; 13 | font-size: 24px; 14 | font-weight: 700; 15 | color: var(--b-alpha-80); 16 | text-align: center; 17 | } 18 | 19 | .demo { 20 | width: 60%; 21 | background-color: var(--color-blue-bg); 22 | padding: 16px; 23 | border-radius: 8px; 24 | } 25 | 26 | .demo_item { 27 | font-size: 14px; 28 | padding: 4px 0; 29 | color: var(--b-alpha-80); 30 | } 31 | 32 | .desc { 33 | width: 60%; 34 | font-size: 16px; 35 | font-weight: 700; 36 | padding: 16px; 37 | margin: 24px 0; 38 | background-color: var(--purple-border); 39 | color: var(--b-alpha-70); 40 | border-radius: 8px; 41 | } 42 | 43 | .content { 44 | width: 60%; 45 | border-radius: 8px; 46 | display: grid; 47 | grid-gap: 8px; 48 | padding-bottom: 16px; 49 | gap: 12px; 50 | grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); 51 | } 52 | 53 | .blog_item { 54 | padding: 24px; 55 | border-radius: 8px; 56 | display: flex; 57 | align-items: flex-start; 58 | transition: 0.3s; 59 | } 60 | 61 | .blog_item:hover { 62 | /* transform: translateY(-5px) scale(1.05, 1.05); */ 63 | box-shadow: 0 1px 10px 5px var(--b-alpha-30); 64 | z-index: 1; 65 | } 66 | 67 | .blog_item:hover .blog_item_logo { 68 | transform: rotate(360deg); 69 | } 70 | 71 | .blog_item_logo { 72 | width: 50px; 73 | height: 50px; 74 | border-radius: 50%; 75 | margin-right: 16px; 76 | flex-shrink: 0; 77 | transition: 0.3s; 78 | } 79 | 80 | .blog_item_title { 81 | color: var(--b-alpha-80); 82 | font-size: 16px; 83 | font-weight: 700; 84 | padding-bottom: 8px; 85 | } 86 | 87 | .blog_item_desc { 88 | color: var(--b-alpha-70); 89 | font-size: 14px; 90 | } 91 | 92 | .comment { 93 | width: 60%; 94 | } 95 | 96 | @media screen and (min-width: 1200px) { 97 | .demo, 98 | .desc, 99 | .content, 100 | .comment { 101 | width: 80%; 102 | max-width: 800px; 103 | } 104 | } 105 | 106 | @media screen and (max-width: 1300px) { 107 | .demo, 108 | .desc, 109 | .content, 110 | .comment { 111 | width: 75%; 112 | max-width: 800px; 113 | } 114 | } 115 | 116 | @media screen and (max-width: 1100px) { 117 | .demo, 118 | .desc, 119 | .content, 120 | .comment { 121 | width: 80%; 122 | } 123 | } 124 | 125 | @media screen and (max-width: 900px) { 126 | .demo, 127 | .desc, 128 | .content, 129 | .comment { 130 | width: 90%; 131 | } 132 | } 133 | 134 | @media screen and (max-width: 600px) { 135 | .demo, 136 | .desc, 137 | .content { 138 | width: 90%; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /app/friendly-links/page.tsx: -------------------------------------------------------------------------------- 1 | import PostClient from './post-client' 2 | import type { Metadata } from 'next' 3 | import getData from "@/utils/httpClient/request"; 4 | 5 | export const metadata: Metadata = { 6 | title: '友情链接', 7 | description: '存放友链地址', 8 | } 9 | 10 | const FriendlyLinks = async () => { 11 | const { data } = await getData({type: 'all_user_friendly_Links'}); 12 | 13 | return ; 14 | }; 15 | 16 | export default FriendlyLinks; 17 | -------------------------------------------------------------------------------- /app/friendly-links/post-client.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import Link from "next/link"; 3 | import React, { useContext, useEffect } from "react"; 4 | import { 5 | addNavItemStyle, 6 | bindHandleScroll, 7 | removeNavItemStyle, 8 | removeScroll, 9 | } from "@/utils/elementUtils"; 10 | import { getRandomColor } from "@utils/dataUtils"; 11 | import { LayoutContext } from "@/store/layoutStore"; 12 | import Comment from "@components/Comment"; 13 | import withLoading from "@components/WithLoading"; 14 | import useChangeLoading from "@components/WithLoading/useChangeLoading"; 15 | import style from "./friendlyLinks.module.css"; 16 | 17 | const FriendlyLinks = (props) => { 18 | const { theme } = useContext(LayoutContext); 19 | 20 | useEffect(() => { 21 | addNavItemStyle(); 22 | bindHandleScroll(); 23 | 24 | return () => { 25 | removeNavItemStyle(); 26 | removeScroll(); 27 | }; 28 | }, []); 29 | 30 | useChangeLoading({ ...props, name: "friendly_links" }); 31 | 32 | return ( 33 |
38 |

友情链接

39 |
40 |
网站名:shimmer
41 |
42 | 站点头像:https://wp-boke.work/images/logo.png 43 |
44 |
网站链接:https://wp-boke.work
45 |
46 | 网站描述:欲买桂花同载酒,终不似,少年游。 47 |
48 |
联系邮箱:webwp0403@163.com
49 |
50 |
不定期清理失效网站,拒绝无效互链。
51 |
52 | {props?.data?.map((v) => ( 53 | 64 | {v.title} 65 |
66 |
{v.title}
67 |
{v.desc}
68 |
69 | 70 | ))} 71 |
72 |
73 | 74 |
75 |
76 | ); 77 | }; 78 | 79 | export default withLoading(FriendlyLinks); 80 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Viewport } from "next"; 3 | import { Analytics } from "@vercel/analytics/react"; 4 | import { SpeedInsights } from "@vercel/speed-insights/next"; 5 | import { LayoutContextProvider } from "@store/layoutStore"; 6 | import NavBar from "@components/NavBar"; 7 | import Footer from "@components/Footer"; 8 | import "./styles/globals.css"; 9 | 10 | export const metadata: Metadata = { 11 | title: "shimmer的博客", 12 | description: "shimmer的个人博客站,旨在记录生活,分享知识", 13 | keywords: ["shimmer博客", "shimmer的博客", "shimmer", "博客", "个人博客"], 14 | authors: [{ name: "shimmer", url: "https://wp-boke.work/about" }], 15 | }; 16 | 17 | export const viewport: Viewport = { 18 | width: "device-width", 19 | initialScale: 1, 20 | maximumScale: 1, 21 | userScalable: false, 22 | }; 23 | 24 | export default function RootLayout({ 25 | children, 26 | }: { 27 | children: React.ReactNode; 28 | }) { 29 | return ( 30 | 31 | 32 | 33 | 34 | {children} 35 |