├── .babelrc ├── .commitlintrc.js ├── .eslintrc.js ├── .gitignore ├── LICENSE ├── README.md ├── package.json ├── public ├── assets │ ├── config.js │ └── favicon.ico └── index.html ├── scripts ├── antd-theme.js ├── config │ ├── webpack.common.js │ ├── webpack.dev.js │ └── webpack.prod.js ├── constant.js └── env.js ├── src ├── App.scss ├── App.tsx ├── components │ ├── BackToTop │ │ ├── index.custom.scss │ │ ├── index.scss │ │ └── index.tsx │ ├── Card │ │ ├── index.scss │ │ └── index.tsx │ ├── Comment │ │ ├── Divider │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── EditBox │ │ │ ├── AdminBox │ │ │ │ ├── index.scss │ │ │ │ └── index.tsx │ │ │ ├── Emoji │ │ │ │ ├── EmojiItem │ │ │ │ │ ├── index.scss │ │ │ │ │ └── index.tsx │ │ │ │ ├── index.custom.scss │ │ │ │ ├── index.scss │ │ │ │ ├── index.tsx │ │ │ │ └── useEmoji.ts │ │ │ ├── PreShow │ │ │ │ ├── index.scss │ │ │ │ └── index.tsx │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── MsgList │ │ │ ├── MsgItem │ │ │ │ ├── index.scss │ │ │ │ └── index.tsx │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── Placehold │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── fetchData.ts │ │ └── index.tsx │ ├── DisplayBar │ │ ├── DisplayBarLoading │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── index.scss │ │ └── index.tsx │ ├── ErrorBoundary │ │ └── index.tsx │ ├── Footer │ │ ├── index.scss │ │ └── index.tsx │ ├── ImgView │ │ ├── index.scss │ │ └── index.tsx │ ├── Layout │ │ ├── index.scss │ │ └── index.tsx │ ├── LayoutLoading │ │ └── index.tsx │ ├── Main │ │ ├── index.scss │ │ └── index.tsx │ ├── MarkDown │ │ ├── hljs.custom.scss │ │ ├── index.scss │ │ └── index.tsx │ ├── MyPagination │ │ ├── index.scss │ │ ├── index.tsx │ │ └── pagination.custom.scss │ ├── Nav │ │ ├── config.ts │ │ ├── index.custom.scss │ │ ├── index.scss │ │ └── index.tsx │ └── PageTitle │ │ ├── index.scss │ │ └── index.tsx ├── fonts │ └── FiraCode-Regular.ttf ├── global.custom.scss ├── imgs │ ├── bg0.webp │ ├── bg1.webp │ └── bg2.webp ├── index.tsx ├── pages │ ├── About │ │ ├── AboutMe │ │ │ └── index.tsx │ │ ├── AboutSite │ │ │ ├── AboutText │ │ │ │ └── index.tsx │ │ │ ├── Chart │ │ │ │ ├── index.scss │ │ │ │ ├── index.tsx │ │ │ │ └── useOption.ts │ │ │ └── index.tsx │ │ ├── Switch │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── fetchData.ts │ │ ├── index.scss │ │ └── index.tsx │ ├── ArtDetail │ │ └── index.tsx │ ├── Articles │ │ ├── ArtList │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── Search │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ └── index.tsx │ ├── Classes │ │ ├── ClassBar │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── index.scss │ │ └── index.tsx │ ├── Home │ │ ├── Aside │ │ │ ├── AccountCard │ │ │ │ ├── Csdn │ │ │ │ │ ├── index.scss │ │ │ │ │ └── index.tsx │ │ │ │ ├── IcoBtn │ │ │ │ │ ├── index.scss │ │ │ │ │ └── index.tsx │ │ │ │ ├── index.scss │ │ │ │ ├── index.tsx │ │ │ │ └── useAccount.tsx │ │ │ ├── BlogCard │ │ │ │ ├── index.scss │ │ │ │ └── index.tsx │ │ │ ├── ClockCard │ │ │ │ ├── index.scss │ │ │ │ ├── index.tsx │ │ │ │ └── useClock.ts │ │ │ ├── DataCard │ │ │ │ ├── fetchData.ts │ │ │ │ ├── index.scss │ │ │ │ └── index.tsx │ │ │ ├── NoticeCard │ │ │ │ ├── index.scss │ │ │ │ └── index.tsx │ │ │ ├── SiteCard │ │ │ │ ├── index.scss │ │ │ │ ├── index.tsx │ │ │ │ └── useRunTime.ts │ │ │ ├── TagCard │ │ │ │ ├── index.scss │ │ │ │ └── index.tsx │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── Section │ │ │ ├── PostCard │ │ │ │ ├── PostCardLoading │ │ │ │ │ ├── index.scss │ │ │ │ │ └── index.tsx │ │ │ │ ├── index.scss │ │ │ │ └── index.tsx │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── index.scss │ │ └── index.tsx │ ├── Link │ │ ├── LinkItem │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── index.scss │ │ └── index.tsx │ ├── Log │ │ ├── TimeItem │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ └── index.tsx │ ├── Msg │ │ ├── MsgInfo │ │ │ ├── index.scss │ │ │ ├── index.tsx │ │ │ └── useSite.ts │ │ └── index.tsx │ ├── Post │ │ ├── CopyRight │ │ │ ├── CopyIcon │ │ │ │ └── index.tsx │ │ │ ├── CopyrightIcon │ │ │ │ └── index.tsx │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── Navbar │ │ │ ├── index.custom.scss │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── PostTags │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── index.scss │ │ └── index.tsx │ ├── Say │ │ ├── SayPop │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ └── index.tsx │ ├── Show │ │ ├── ShowItem │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── index.scss │ │ └── index.tsx │ ├── Tags │ │ ├── index.scss │ │ └── index.tsx │ ├── constant.ts │ └── titleConfig.ts ├── redux │ ├── actions.ts │ ├── constant.ts │ ├── interface.ts │ ├── reducers │ │ ├── artSum.ts │ │ ├── avatar.ts │ │ ├── email.ts │ │ ├── index.ts │ │ ├── link.ts │ │ ├── mode.ts │ │ ├── name.ts │ │ └── navShow.ts │ └── store.ts ├── styles │ ├── base.scss │ └── style.scss ├── types │ ├── asset.d.ts │ └── style.d.ts └── utils │ ├── apis │ ├── authLogin.ts │ ├── axios.ts │ ├── dbConfig.ts │ ├── getData.ts │ ├── getOrderData.ts │ ├── getPageData.ts │ ├── getSiteCount.ts │ ├── getSum.ts │ ├── getWhereData.ts │ ├── getWhereOrderData.ts │ ├── getWhereOrderPageData.ts │ ├── getWhereOrderPageSum.ts │ ├── getWhereSum.ts │ └── setData.ts │ ├── cloudBase.ts │ ├── constant.ts │ ├── function.ts │ ├── hooks │ ├── useLazyImg.ts │ ├── useTime.ts │ └── useTop.ts │ └── modeMap.ts ├── tsconfig.json └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "modules": false 7 | } 8 | ], 9 | "@babel/preset-react", // 需要放在@babel/preset-env后面,先处理 10 | "@babel/preset-typescript" 11 | ], 12 | "plugins": [ 13 | [ 14 | "@babel/plugin-transform-runtime", 15 | { 16 | "corejs": { 17 | "version": 3, 18 | "proposals": true 19 | } 20 | } 21 | ], 22 | [ 23 | "import", 24 | { 25 | "libraryName": "antd", 26 | "libraryDirectory": "es", 27 | "style": true // `style: true` 会加载 less 文件 28 | } 29 | ], 30 | ["@babel/plugin-syntax-dynamic-import"] 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /.commitlintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'] 3 | }; 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ['alloy', 'alloy/react', 'alloy/typescript'], 4 | parserOptions: { 5 | warnOnUnsupportedTypeScriptVersion: false 6 | }, 7 | env: { 8 | es2021: true, 9 | browser: true, 10 | node: true 11 | }, 12 | settings: { 13 | react: { 14 | version: 'detect' 15 | } 16 | }, 17 | plugins: ['simple-import-sort'], 18 | rules: { 19 | 'simple-import-sort/imports': 'error', 20 | 'simple-import-sort/exports': 'error', 21 | 'default-case': 2, 22 | 'guard-for-in': 2, 23 | 'no-eval': 2, 24 | 'no-implied-eval': 2, 25 | 'no-lone-blocks': 2, 26 | 'require-await': 2, 27 | 'comma-dangle': 2, 28 | // 变量定义但未使用 29 | 'no-unused-vars': 2, 30 | '@typescript-eslint/no-require-imports': 0, 31 | '@typescript-eslint/explicit-member-accessibility': 0, 32 | '@typescript-eslint/member-ordering': 0, 33 | 'react/jsx-no-useless-fragment': 0, 34 | 'no-param-reassign': 0 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | /.pnp 4 | .pnp.js 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 飞鸟 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > 这是个人博客系统的**博客展示页面**,**后台管理页面**仓库见「react-blog-admin」。 2 | 3 | Hi,这是我自己用 React 写的个人博客系统,欢迎大家`star`、`fork`,互相交流学习!💪💪 4 | 5 | ## 博客已于2022/3/29完成重构 6 | 7 | > 终于把博客重构完上线了!😴 8 | 9 | ## 重构上线 10 | 11 | 看到这篇文章时,新版博客已经上线啦!最近三周时间,把个人博客系统的展示页面几乎重写了一遍。为什么要重构?因为之前写的代码自己看不下去了:重复逻辑多、组件没有有效复用、项目整体架构问题、数据存储问题等等。用了三周的时间,整体样式基本保持不变,把逻辑代码完完全全进行重写。 12 | 13 | 现在博客的体验(性能+操作逻辑)应该会好很多😌。 14 | 15 | ## 优化点 16 | 17 | 由于重构的博客同样也是基于`React`开发,功能、样式基本一致,这里就说说新版博客相比于旧版,有所改进的地方。 18 | 19 | ### 开发 20 | 21 | 在开发上,旧版博客使用的是`create-react-app`进行开发。 22 | 23 | 为了学习 webpack 项目的搭建、配置,并进行可行性验证,重构的博客使用自建脚手架my-react进行开发,集成配置了常用功能。 24 | 25 | 新版博客的改进之处: 26 | 27 | - 使用`typescript`编写代码 28 | - 使用`commitlint`保证`git commit`提交规范 29 | - 使用`eslint`规范代码风格 30 | - 使用`husky`在每次提交之前,触发`commitlint`与`eslint` 31 | - 区分开发环境、生产环境,并抽离公共配置 32 | - CSS 预处理器使用`scss` 33 | 34 | ### 代码逻辑 35 | 36 | 几乎重构了全部的逻辑代码,改进之处: 37 | 38 | - 抽离了 10 余个公共组件,有效复用重复逻辑 39 | - 使用`ahooks`提供的常见功能逻辑`hook`,避免闭包问题 40 | - `useRequest`异步数据管理 41 | - `useMount` 42 | - `useTitle` 43 | - `useToggle` 44 | - `useLocalStorageState` 45 | - `useSafeState` 46 | - `useUpdateEffect` 47 | - `useEventListener` 48 | - `useKeyPress` 49 | - ... 50 | - 根据页面自动切换网页`title` 51 | - 使用`classnames`拼接多个类名 52 | - 使用`dayjs`代替`Moment.js`格式化时间 53 | - 改用`echarts`绘制文章分布饼图 54 | - 使用`markdown-navbar`生成文章目录锚点 55 | - 评论模块中,对用户的输入内容进行过滤 56 | - `react-router-dom`升级 6 版本 57 | - 基于路由进行代码分割,打包生成多个`js`文件,按需加载 58 | - 进入博客不再一次性获取全部数据,而是每个组件单独请求 59 | - 首页文章卡片分页、文章页分页改为**后端分页**,只请求当前页的数据 60 | - 搜索文章功能,改为发送请求后端搜索 61 | - 评论组件 62 | - `emoji`表情功能支持点击复制,**有待改进** 63 | - 更改预览框、回复框出现位置,减少定位图层 64 | - 增加评论分页器 65 | - 优化部分组件的样式 66 | - 小卡片触发`hover`的样式 67 | - 覆盖`antd`组件样式 68 | - 分类页改为双列展示 69 | - 移除`animate.css`库,取消动画 70 | - 正文移除字体「仓耳渔阳体」,改为浏览器内置字体 71 | - 移动端适配采用动态`rem`方案 72 | - 用到的图片资源,改用`webp`格式 73 | 74 | ### 新功能 75 | 76 | 新增的其实就一个: 77 | 78 | - 新增主题切换功能,一键切换**黑**/**蓝**/**灰**三种主题,保存至`localStorage`,下次打开时自动切换至已选的主题 79 | 80 | ## 待优化 81 | 82 | - 继续优化 webpack 打包后的体积,提升首屏加载速度 83 | - 图片懒加载 84 | - 尝试预渲染 85 | - 改进添加`emoji`表情功能,点击即可插入表情 86 | 87 | 88 | 89 | > 欢迎大家给出改进意见! 90 | 91 | 92 | 93 | *** 94 | 95 | 96 | 97 | ## 旧博客 98 | 99 | > 以下是旧博客完成时的介绍。 100 | 101 | 之前我使用`hexo`搭建过个人博客。`hexo`很强大,渲染页面速度快,支持`markdown`语法,可以一键部署,还可以扩展各种插件。 102 | 103 | 但`hexo`搭建的是静态页面,每次更新文章,都要**重新生成**静态页面,再部署页面。`hexo`也没有后台管理,想要修改发布的文章,只能修改源代码,再重新生成页面。所以很早之前就想写一个自己的博客系统,由**博客展示页面**和**后台管理页面**构成,通过后台管理页面,可以实时更新、发布文章,非常方便。但在当时还没有能力写出这样一个系统,就一直没有去做。 104 | 105 | 后来学习了`React`之后,想尝试下写自己的博客,就每天课余时间写一点,最后写出来了 😅😅😅。 106 | 107 | 由于之前有搭建过`hexo`博客,所以就按照之前自己`hexo`博客的功能来写,基本的功能有文章管理、文章搜索、分类/标签、图库、说说、留言板/评论、友链、小作品页面、建站日志时间轴、关于页面等。但是很多功能还不完善,不具有通用性,只适用于本博客,以后会慢慢改进 🧐🧐🧐! 108 | 109 | ### 用到的技术/工具 110 | 111 | 🔖 博客主要使用到的技术如下: 112 | 113 | **前端**(博客页面+后台管理): 114 | 115 | - `React`脚手架`Create-React-App` 116 | - 状态集中管理工具`Redux` 117 | - 前端路由`React-Router` 118 | - `AntD`组件库 119 | - 今日诗词提供首页的诗句 120 | - 时间格式化工具moment 121 | - `markdown`格式渲染工具marked 122 | - 代码高亮渲染工具highlight.js 123 | - 其他第三方包 124 | 125 | **后端**: 126 | 127 | 后端使用腾讯云`CloudBase`云端一体化后端云服务,包括: 128 | 129 | - 用户管理:管理员登录、访客匿名用户登录 130 | - 数据库:存放管理员的博客数据 131 | - 网站托管:托管后台管理页面 132 | 133 | **其他**: 134 | 135 | - 评论回复的邮箱提醒`API`,使用`Node.js`编写,运行在自己的**阿里云服务器**上 136 | - 已配置**SSL 证书**,开启**HTTPS**访问 137 | - 博客展示页面托管于**腾讯云开发静态文件托管** 138 | - 图床使用**阿里云OSS** 139 | - `Webify`:应用托管,自动部署**后台管理页面** 140 | 141 | ### 主要功能 142 | 143 | #### 博客展示页面 144 | 145 | - 首页预览所有文章 146 | - 查看文章评论、发布评论、评论回复 147 | - 搜索文章:根据关键字搜索、分类搜索、标签搜索 148 | - 查看相册 149 | - 查看说说 150 | - 查看留言板留言、发布留言、留言回复 151 | - 查看友链、访问友链 152 | - 查看小作品 153 | - 查看建站日志时间轴 154 | - 查看关于本站/关于我 155 | - 进入后台管理页面 156 | 157 | #### 后台管理页面 158 | 159 | **管理**是指:对数据的**增**、**删**、**改**、**查**。 160 | 161 | - 首页预览博客基本数据(文章数、草稿数、友链数等),管理分类、标签 162 | - 文章管理、草稿管理 163 | - 相册管理 164 | - 说说管理 165 | - 查看留言、评论,删除留言、评论 166 | - 友链管理 167 | - 小作品管理 168 | - 关于页面文字管理 169 | - 建站日志管理 170 | 171 | ### 不断改进 172 | 173 | 由于时间有限、本人能力有限,博客系统还有很多不足之处,会在学习新知识的同时不断改进博客。 174 | 175 | 也请各路大佬多多指点 😆😆😆! 176 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blog-jack", 3 | "version": "2.0.0", 4 | "sideEffects": [ 5 | "*.css", 6 | "*.less", 7 | "*.scss" 8 | ], 9 | "license": "MIT", 10 | "scripts": { 11 | "start": "cross-env NODE_ENV=development webpack-dev-server --config ./scripts/config/webpack.dev.js", 12 | "build": "cross-env NODE_ENV=production webpack --config ./scripts/config/webpack.prod.js", 13 | "eslint": "eslint ./src/** --ignore-pattern *.jpg --ignore-pattern *.webp --ignore-pattern *.scss --ignore-pattern *.ttf --no-error-on-unmatched-pattern --quiet", 14 | "commitlint": "commitlint --config .commitlintrc.js -e" 15 | }, 16 | "browserslist": [ 17 | ">0.2%", 18 | "not dead", 19 | "ie >= 9", 20 | "not op_mini all" 21 | ], 22 | "husky": { 23 | "hooks": { 24 | "pre-commit": "yarn eslint", 25 | "commit-msg": "yarn commitlint" 26 | } 27 | }, 28 | "dependencies": { 29 | "@ahooksjs/use-url-state": "^3.1.13", 30 | "@ant-design/icons": "4.7.0", 31 | "@babel/core": "7.17.5", 32 | "@babel/plugin-syntax-dynamic-import": "7.8.3", 33 | "@babel/plugin-transform-runtime": "7.17.0", 34 | "@babel/preset-env": "7.16.11", 35 | "@babel/preset-react": "7.16.7", 36 | "@babel/preset-typescript": "7.16.7", 37 | "@babel/runtime-corejs3": "7.17.2", 38 | "@cloudbase/js-sdk": "^1.7.2", 39 | "@commitlint/cli": "16.2.1", 40 | "@commitlint/config-conventional": "16.2.1", 41 | "@redux-devtools/extension": "3.2.2", 42 | "@types/markdown-navbar": "^1.4.0", 43 | "@types/marked": "3.0.0", 44 | "@types/pubsub-js": "^1.8.3", 45 | "@types/react": "^17.0.40", 46 | "@types/react-dom": "^17.0.13", 47 | "@types/sanitize-html": "^2.6.2", 48 | "@types/webpack-env": "1.16.3", 49 | "@typescript-eslint/eslint-plugin": "5.13.0", 50 | "@typescript-eslint/parser": "5.13.0", 51 | "ahooks": "3.1.13", 52 | "antd": "4.19.0", 53 | "antd-dayjs-webpack-plugin": "1.0.6", 54 | "axios": "^0.26.1", 55 | "babel-loader": "8.2.3", 56 | "babel-plugin-import": "1.13.3", 57 | "classnames": "2.3.1", 58 | "clean-webpack-plugin": "4.0.0", 59 | "copy-to-clipboard": "3.3.1", 60 | "copy-webpack-plugin": "10.2.4", 61 | "cross-env": "7.0.3", 62 | "css-loader": "6.6.0", 63 | "css-minimizer-webpack-plugin": "3.4.1", 64 | "dayjs": "1.10.8", 65 | "echarts": "^5.3.1", 66 | "echarts-for-react": "^3.0.2", 67 | "eslint": "8.10.0", 68 | "eslint-config-alloy": "4.5.1", 69 | "eslint-plugin-react": "7.29.3", 70 | "eslint-plugin-simple-import-sort": "^7.0.0", 71 | "fork-ts-checker-webpack-plugin": "7.2.1", 72 | "highlight.js": "11.1.0", 73 | "html-webpack-plugin": "5.5.0", 74 | "husky": "4.3.8", 75 | "jinrishici": "^1.0.6", 76 | "less": "4.1.2", 77 | "less-loader": "10.2.0", 78 | "markdown-navbar": "^1.4.3", 79 | "marked": "2.1.3", 80 | "mini-css-extract-plugin": "2.5.3", 81 | "postcss-loader": "6.2.1", 82 | "postcss-preset-env": "7.4.1", 83 | "react": "17.0.2", 84 | "react-dom": "17.0.2", 85 | "react-icons": "4.3.1", 86 | "react-redux": "7.2.6", 87 | "react-router-dom": "6.2.2", 88 | "redux": "4.1.2", 89 | "sanitize-html": "^2.7.0", 90 | "sass": "^1.63.6", 91 | "sass-loader": "12.6.0", 92 | "style-loader": "3.3.1", 93 | "typescript": "4.6.2", 94 | "webpack": "5.69.1", 95 | "webpack-bundle-analyzer": "4.5.0", 96 | "webpack-cli": "4.9.2", 97 | "webpack-dev-server": "4.7.4", 98 | "webpack-merge": "5.8.0", 99 | "webpackbar": "5.0.2" 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /public/assets/config.js: -------------------------------------------------------------------------------- 1 | function inMobile() { 2 | document.getElementsByTagName('html')[0].style.fontSize = 3 | document.documentElement.clientWidth / 450 + 'px'; 4 | } 5 | document.addEventListener('DOMContentLoaded', inMobile); 6 | window.onresize = inMobile; 7 | -------------------------------------------------------------------------------- /public/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lzxjack/react-blog/e4d1fb3ccb570a8d86362a3858677acd629cc589/public/assets/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 10 | 11 |") 32 | }} 33 | /> 34 | ); 35 | }; 36 | 37 | export default MarkDown; 38 | -------------------------------------------------------------------------------- /src/components/MyPagination/index.scss: -------------------------------------------------------------------------------- 1 | .pageBox { 2 | height: 50px; 3 | user-select: none; 4 | text-align: center; 5 | } 6 | 7 | @media screen and (max-width: 1240px) { 8 | .pageBox { 9 | height: 30rem; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/components/MyPagination/index.tsx: -------------------------------------------------------------------------------- 1 | import './pagination.custom.scss'; 2 | 3 | import { Pagination } from 'antd'; 4 | import React from 'react'; 5 | import { connect } from 'react-redux'; 6 | 7 | import { setNavShow } from '@/redux/actions'; 8 | 9 | import s from './index.scss'; 10 | 11 | interface Props { 12 | current?: number; 13 | defaultPageSize?: number; 14 | total?: number; 15 | setPage?: Function; 16 | scrollToTop?: number; 17 | autoScroll?: boolean; 18 | setNavShow?: Function; 19 | } 20 | 21 | const MyPagination: React.FC= ({ 22 | current, 23 | defaultPageSize = 8, 24 | total = 0, 25 | setPage, 26 | scrollToTop = 0, 27 | autoScroll = false, 28 | setNavShow 29 | }) => { 30 | return ( 31 | <> 32 | {total > defaultPageSize ? ( 33 | 34 |47 | ) : null} 48 | > 49 | ); 50 | }; 51 | 52 | export default connect(() => ({}), { 53 | setNavShow 54 | })(MyPagination); 55 | -------------------------------------------------------------------------------- /src/components/MyPagination/pagination.custom.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/base.scss'; 2 | @import '../../styles/style.scss'; 3 | 4 | #myPagination { 5 | .ant-pagination { 6 | color: $textColor; 7 | height: 100%; 8 | } 9 | 10 | .ant-pagination .ant-pagination-prev, 11 | .ant-pagination .ant-pagination-next { 12 | width: 50px; 13 | height: 50px; 14 | } 15 | 16 | .ant-pagination .ant-pagination-item-link, 17 | .ant-pagination .ant-pagination-item, 18 | .ant-pagination .ant-pagination-jump-prev, 19 | .ant-pagination .ant-pagination-jump-next { 20 | background-color: $themeColor1; 21 | border: none; 22 | border-radius: $midRadius; 23 | cursor: default; 24 | color: $textColor; 25 | width: 50px; 26 | height: 50px; 27 | font-size: 18px; 28 | line-height: 50px; 29 | @extend .trans; 30 | } 31 | 32 | .ant-pagination .ant-pagination-item a { 33 | @extend .trans; 34 | cursor: default; 35 | color: $textColor; 36 | } 37 | .ant-pagination .ant-pagination-item a:hover, 38 | .ant-pagination-item-active a, 39 | .ant-pagination-item-ellipsis { 40 | color: $textColor !important; 41 | } 42 | 43 | .ant-pagination .ant-pagination-item-link:hover, 44 | .ant-pagination .ant-pagination-item:hover, 45 | .ant-pagination-item-active:hover, 46 | .ant-pagination .ant-pagination-jump-prev:hover, 47 | .ant-pagination .ant-pagination-jump-next:hover, 48 | .ant-pagination-item-active { 49 | background-color: $hoverColor !important; 50 | color: $textColor !important; 51 | } 52 | 53 | .ant-pagination-item-link-icon svg { 54 | width: 18px; 55 | height: 18px; 56 | fill: $textColor; 57 | } 58 | } 59 | 60 | @media screen and (max-width: 1240px) { 61 | #myPagination { 62 | .ant-pagination .ant-pagination-prev, 63 | .ant-pagination .ant-pagination-next { 64 | width: 30rem; 65 | height: 30rem; 66 | } 67 | 68 | .ant-pagination .ant-pagination-item-link, 69 | .ant-pagination .ant-pagination-item, 70 | .ant-pagination .ant-pagination-jump-prev, 71 | .ant-pagination .ant-pagination-jump-next { 72 | width: 30rem; 73 | height: 30rem; 74 | font-size: 14rem; 75 | line-height: 30rem; 76 | border-radius: 8rem; 77 | } 78 | 79 | .ant-pagination-item-link-icon svg { 80 | width: 14rem; 81 | height: 14rem; 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/components/Nav/config.ts: -------------------------------------------------------------------------------- 1 | export const useLinkList = () => { 2 | const navArr = [ 3 | { name: '说说', to: '/say' }, 4 | { name: '留言', to: '/msg' }, 5 | { name: '友链', to: '/link' }, 6 | { name: '作品', to: '/show' }, 7 | { name: '建站', to: '/log' }, 8 | { name: '关于', to: '/about' } 9 | ]; 10 | const secondNavArr = [ 11 | { name: '找文章', to: '/articles' }, 12 | { name: '分类', to: '/classes' }, 13 | { name: '标签', to: '/tags' } 14 | ]; 15 | 16 | const mobileNavArr = [ 17 | { name: '主页', to: '/' }, 18 | { name: '文章', to: '/articles' }, 19 | { name: '分类', to: '/classes' }, 20 | { name: '标签', to: '/tags' }, 21 | { name: '说说', to: '/say' }, 22 | { name: '留言', to: '/msg' }, 23 | { name: '友链', to: '/link' }, 24 | { name: '作品', to: '/show' }, 25 | { name: '建站', to: '/log' }, 26 | { name: '关于', to: '/about' } 27 | ]; 28 | 29 | return { 30 | navArr, 31 | secondNavArr, 32 | mobileNavArr 33 | }; 34 | }; 35 | -------------------------------------------------------------------------------- /src/components/Nav/index.custom.scss: -------------------------------------------------------------------------------- 1 | @import './../../styles/base.scss'; 2 | @import './../../styles/style.scss'; 3 | 4 | .mobile-nav-box { 5 | display: none; 6 | 7 | .ant-drawer-content-wrapper { 8 | width: 90rem !important; 9 | 10 | .ant-drawer-header-close-only { 11 | display: none; 12 | } 13 | 14 | .ant-drawer-content { 15 | background-color: $themeColor; 16 | color: $textColor; 17 | 18 | .ant-drawer-body { 19 | padding: 0; 20 | display: flex; 21 | justify-content: center; 22 | align-items: center; 23 | } 24 | } 25 | } 26 | } 27 | 28 | @media screen and (max-width: 1240px) { 29 | .mobile-nav-box { 30 | display: block; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/components/Nav/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/base.scss'; 2 | @import '../../styles/style.scss'; 3 | 4 | .baseBtn { 5 | height: 44px; 6 | width: 70px; 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | font-size: 22px; 11 | font-weight: 700; 12 | margin-right: 20px; 13 | border-radius: 14px; 14 | color: $textColor; 15 | user-select: none; 16 | @extend .hover; 17 | } 18 | 19 | .hiddenNav { 20 | box-shadow: none !important; 21 | transform: translate(0, -$navHeight); 22 | } 23 | 24 | .nav { 25 | width: 100%; 26 | height: $navHeight; 27 | background-color: $themeColor1; 28 | position: fixed; 29 | top: 0; 30 | z-index: 10; 31 | box-shadow: 0 0 18px $footerBg; 32 | @extend .trans; 33 | 34 | .navContent { 35 | position: relative; 36 | width: $centerWidth; 37 | height: 100%; 38 | margin: 0 auto; 39 | display: flex; 40 | justify-content: center; 41 | align-items: center; 42 | 43 | .homeAndAdmin { 44 | @extend .baseBtn; 45 | position: absolute; 46 | top: 50%; 47 | right: 0; 48 | transform: translate(0, -50%); 49 | font-size: 26px; 50 | width: 60px; 51 | } 52 | 53 | .homeBtn { 54 | @extend .homeAndAdmin; 55 | cursor: pointer; 56 | left: 0; 57 | } 58 | 59 | .adminBtn { 60 | @extend .homeAndAdmin; 61 | margin-right: 0; 62 | } 63 | 64 | .navBtn { 65 | @extend .baseBtn; 66 | } 67 | .navBtn:last-child { 68 | margin-right: 0; 69 | } 70 | .navActive { 71 | @extend .navBtn; 72 | background-color: $hoverColor; 73 | } 74 | 75 | .articlesBtn { 76 | position: relative; 77 | @extend .baseBtn; 78 | 79 | .articelsSecond { 80 | position: absolute; 81 | top: -160px; 82 | width: 90px; 83 | background-color: $themeColor1; 84 | border-radius: 14px; 85 | padding: 10px; 86 | z-index: 0; 87 | @extend .trans; 88 | 89 | .articelsSecondItem { 90 | @extend .hover; 91 | display: flex; 92 | justify-content: center; 93 | align-items: center; 94 | font-size: 18px; 95 | height: 34px; 96 | margin-bottom: 10px; 97 | border-radius: 10px; 98 | user-select: none; 99 | background-color: $themeColor2; 100 | color: $textColor; 101 | } 102 | .articelsSecondItem:last-child { 103 | margin-bottom: 0; 104 | } 105 | .sedActive { 106 | @extend .articelsSecondItem; 107 | background-color: $hoverColor; 108 | } 109 | } 110 | } 111 | .articlesBtn:hover .articelsSecond { 112 | top: 60px; 113 | } 114 | 115 | .modeBtn { 116 | @extend .homeAndAdmin; 117 | right: 80px; 118 | margin-right: 0; 119 | 120 | .modeOpions { 121 | position: absolute; 122 | left: 50%; 123 | top: -180px; 124 | transform: translate(-50%, 0); 125 | width: 80px; 126 | background-color: $themeColor1; 127 | border-radius: 14px; 128 | padding: 10px; 129 | z-index: 0; 130 | @extend .trans; 131 | 132 | .modeItem { 133 | height: 40px; 134 | background-color: $themeColor; 135 | margin-bottom: 10px; 136 | border-radius: 10px; 137 | display: flex; 138 | justify-content: center; 139 | align-items: center; 140 | font-size: 20px; 141 | @extend .trans; 142 | color: #fff; 143 | } 144 | .modeItem1, 145 | .modeItem2 { 146 | color: #000; 147 | } 148 | 149 | .modeItem:last-child { 150 | margin-bottom: 0; 151 | } 152 | .modeItem:hover { 153 | transform: scale(1.07); 154 | box-shadow: 4px 4px 8px rgba(0, 0, 0, 0.4); 155 | } 156 | } 157 | } 158 | .modeBtn:hover .modeOpions { 159 | top: 60px; 160 | } 161 | } 162 | } 163 | 164 | // 手机端呼出导航的按钮 165 | .mobileNavBtn { 166 | width: 50rem; 167 | height: 50rem; 168 | display: none; 169 | justify-content: center; 170 | align-items: center; 171 | font-size: 22rem; 172 | color: $textColor; 173 | position: fixed; 174 | top: 0; 175 | right: 0; 176 | z-index: 99; 177 | } 178 | 179 | // 手机端导航 180 | .mobileNavBox { 181 | .mobileNavItem { 182 | display: flex; 183 | justify-content: center; 184 | align-items: center; 185 | width: 100%; 186 | color: $textColor; 187 | font-size: 18rem; 188 | font-family: 'dengxian'; 189 | width: 60rem; 190 | height: 34rem; 191 | border-radius: 10rem; 192 | margin-bottom: 16rem; 193 | } 194 | .mobileNavItem:last-child { 195 | margin-bottom: 0; 196 | } 197 | .mobileNavActive { 198 | @extend .mobileNavItem; 199 | background-color: $hoverColor; 200 | } 201 | .modeItem { 202 | @extend .mobileNavItem; 203 | border: 2rem solid #ccc; 204 | } 205 | } 206 | 207 | @media screen and (max-width: 1240px) { 208 | .nav { 209 | display: none; 210 | } 211 | .mobileNavBtn { 212 | display: flex; 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /src/components/PageTitle/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/base.scss'; 2 | 3 | .box { 4 | color: $textColor; 5 | height: $topTitleHeight; 6 | display: flex; 7 | justify-content: center; 8 | align-items: center; 9 | flex-direction: column; 10 | user-select: none; 11 | width: $layoutWidth; 12 | margin: 0 auto; 13 | 14 | .title { 15 | font-size: 56px; 16 | font-weight: 700; 17 | text-shadow: 4px 4px 10px $footerBg; 18 | } 19 | 20 | .desc { 21 | font-size: 28px; 22 | } 23 | } 24 | 25 | @media screen and (max-width: 1240px) { 26 | .box { 27 | width: 100vw; 28 | 29 | .title { 30 | font-size: 40rem; 31 | text-shadow: 4rem 4rem 10rem $footerBg; 32 | } 33 | 34 | .desc { 35 | font-size: 22rem; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/components/PageTitle/index.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import React from 'react'; 3 | 4 | import s from './index.scss'; 5 | 6 | interface Props { 7 | title?: string; 8 | desc?: string; 9 | className?: string; 10 | } 11 | 12 | const PageTitle: React.FC{ 41 | setPage?.(page); 42 | setNavShow?.(false); 43 | autoScroll && window.scrollTo(0, scrollToTop); 44 | }} 45 | /> 46 | = ({ title, desc, className, children }) => { 13 | return ( 14 | 15 |19 | ); 20 | }; 21 | 22 | export default PageTitle; 23 | -------------------------------------------------------------------------------- /src/fonts/FiraCode-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lzxjack/react-blog/e4d1fb3ccb570a8d86362a3858677acd629cc589/src/fonts/FiraCode-Regular.ttf -------------------------------------------------------------------------------- /src/global.custom.scss: -------------------------------------------------------------------------------- 1 | @import './styles/base.scss'; 2 | 3 | html, 4 | body, 5 | #root { 6 | height: 100%; 7 | } 8 | 9 | p { 10 | padding: 0; 11 | margin: 0; 12 | } 13 | 14 | a, 15 | a:link, 16 | a:visited, 17 | a:hover, 18 | a:active, 19 | a:focus { 20 | text-decoration: none !important; 21 | } 22 | 23 | body { 24 | /* 隐藏x轴导航条 */ 25 | overflow-x: hidden; 26 | overflow-y: overlay; 27 | background-color: $bodyColor; 28 | } 29 | body::-webkit-scrollbar { 30 | /*滚动条整体样式*/ 31 | width: 14px; 32 | } 33 | body::-webkit-scrollbar-thumb { 34 | /*滚动条里面小方块*/ 35 | border-radius: 7px; 36 | background-color: $themeColor2; 37 | } 38 | body::-webkit-scrollbar-thumb:hover { 39 | background-color: $hoverColor; 40 | } 41 | body::-webkit-scrollbar-track { 42 | /*滚动条里面轨道*/ 43 | border-radius: 7px; 44 | background: rgba(0, 0, 0, 0); 45 | } 46 | 47 | div, 48 | input { 49 | box-sizing: border-box; 50 | } 51 | 52 | input, 53 | textarea { 54 | background: none; 55 | outline: none; 56 | border: none; 57 | border-radius: 0; 58 | } 59 | 60 | img { 61 | -webkit-user-drag: none; 62 | } 63 | 64 | ::selection { 65 | background: $themeColor2 !important; 66 | } 67 | 68 | // antd的message 69 | .ant-message-notice-content { 70 | background-color: $themeColor1 !important; 71 | border-radius: 14px !important; 72 | font-family: 'dengxian' !important; 73 | color: $textColor !important; 74 | font-size: 18px !important; 75 | } 76 | 77 | @media screen and (max-width: 1240px) { 78 | body::-webkit-scrollbar, 79 | div::-webkit-scrollbar { 80 | display: none; 81 | } 82 | 83 | .ant-message-notice-content { 84 | border-radius: 8rem !important; 85 | font-size: 16rem !important; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/imgs/bg0.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lzxjack/react-blog/e4d1fb3ccb570a8d86362a3858677acd629cc589/src/imgs/bg0.webp -------------------------------------------------------------------------------- /src/imgs/bg1.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lzxjack/react-blog/e4d1fb3ccb570a8d86362a3858677acd629cc589/src/imgs/bg1.webp -------------------------------------------------------------------------------- /src/imgs/bg2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lzxjack/react-blog/e4d1fb3ccb570a8d86362a3858677acd629cc589/src/imgs/bg2.webp -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import { BrowserRouter } from 'react-router-dom'; 5 | 6 | import store from '@/redux/store'; 7 | 8 | import App from './App'; 9 | 10 | if (module?.hot) { 11 | module.hot.accept(); 12 | } 13 | 14 | ReactDOM.render( 15 |{title}16 | {desc &&{desc}} 17 | {children} 18 |16 | , 20 | document.getElementById('root') 21 | ); 22 | -------------------------------------------------------------------------------- /src/pages/About/AboutMe/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import MarkDown from '@/components/MarkDown'; 4 | 5 | interface Props { 6 | content?: string; 7 | className?: string; 8 | } 9 | 10 | const AboutMe: React.FC17 | 19 |18 | = ({ content, className }) => { 11 | return ( 12 | 13 |15 | ); 16 | }; 17 | 18 | export default AboutMe; 19 | -------------------------------------------------------------------------------- /src/pages/About/AboutSite/AboutText/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import MarkDown from '@/components/MarkDown'; 4 | 5 | interface Props { 6 | content?: string; 7 | } 8 | 9 | const AboutText: React.FC14 | = ({ content }) => { 10 | return ; 11 | }; 12 | 13 | export default AboutText; 14 | -------------------------------------------------------------------------------- /src/pages/About/AboutSite/Chart/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../styles/base.scss'; 2 | 3 | .box { 4 | margin-bottom: $space; 5 | 6 | h3 { 7 | margin-top: 0; 8 | margin-bottom: 0.5em; 9 | color: $textColor; 10 | font-weight: 700; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/pages/About/AboutSite/Chart/index.tsx: -------------------------------------------------------------------------------- 1 | import { PieChart } from 'echarts/charts'; 2 | import { LegendComponent, TitleComponent, TooltipComponent } from 'echarts/components'; 3 | import * as echarts from 'echarts/core'; 4 | import { LabelLayout } from 'echarts/features'; 5 | import { CanvasRenderer } from 'echarts/renderers'; 6 | import ReactEChartsCore from 'echarts-for-react/lib/core'; 7 | import React from 'react'; 8 | import { connect } from 'react-redux'; 9 | 10 | import { storeState } from '@/redux/interface'; 11 | 12 | import { ClassType } from '../index'; 13 | import s from './index.scss'; 14 | import { useOption } from './useOption'; 15 | 16 | interface Props { 17 | classes?: ClassType[]; 18 | artSum?: number; 19 | mode?: number; 20 | } 21 | 22 | echarts.use([ 23 | TitleComponent, 24 | TooltipComponent, 25 | LegendComponent, 26 | PieChart, 27 | CanvasRenderer, 28 | LabelLayout 29 | ]); 30 | 31 | const Chart: React.FC = ({ classes, artSum, mode }) => { 32 | const option = useOption(classes!, artSum!, mode!); 33 | 34 | return ( 35 | 36 |48 | ); 49 | }; 50 | 51 | export default connect((state: storeState) => ({ 52 | mode: state.mode 53 | }))(Chart); 54 | -------------------------------------------------------------------------------- /src/pages/About/AboutSite/Chart/useOption.ts: -------------------------------------------------------------------------------- 1 | import { ClassType } from '..'; 2 | 3 | const getChartData = (classes: ClassType[], artSum: number) => { 4 | let sum = 0; 5 | const res = classes.map(obj => { 6 | sum += obj.count; 7 | return { name: obj.class, value: obj.count }; 8 | }); 9 | const leave = artSum - sum; 10 | leave && 11 | res.push({ 12 | name: '未分类', 13 | value: leave 14 | }); 15 | return res; 16 | }; 17 | 18 | export const useOption = (classes: ClassType[], artSum: number, mode: number) => { 19 | const data = getChartData(classes!, artSum!); 20 | 21 | const labelColor = ['rgb(255, 255, 255)', 'rgb(53, 53, 53)', 'rgb(53, 53, 53)']; 22 | const backgroundColor = ['rgb(22, 54, 51)', 'rgb(157, 222, 255)', 'rgb(194, 209, 223)']; 23 | 24 | return { 25 | tooltip: { 26 | trigger: 'item', 27 | backgroundColor: backgroundColor[mode], 28 | borderColor: backgroundColor[mode], 29 | textStyle: { 30 | color: labelColor[mode], 31 | fontSize: 16, 32 | fontFamily: 'dengxian' 33 | } 34 | }, 35 | series: [ 36 | { 37 | type: 'pie', 38 | radius: '88%', 39 | height: '400px', 40 | data, 41 | emphasis: { 42 | itemStyle: { 43 | shadowBlur: 10, 44 | shadowOffsetX: 0, 45 | shadowColor: 'rgba(0, 0, 0, 0.5)' 46 | } 47 | }, 48 | label: { 49 | color: labelColor[mode], 50 | fontSize: 18, 51 | fontFamily: 'dengxian' 52 | } 53 | } 54 | ] 55 | }; 56 | }; 57 | -------------------------------------------------------------------------------- /src/pages/About/AboutSite/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import AboutText from './AboutText'; 4 | import Chart from './Chart'; 5 | 6 | export interface ClassType { 7 | class: string; 8 | count: number; 9 | _id: string; 10 | _openid: string; 11 | } 12 | 13 | interface Props { 14 | content?: string; 15 | classes?: ClassType[]; 16 | artSum?: number; 17 | className?: string; 18 | } 19 | 20 | const AboutSite: React.FC📊文章分布
37 |47 | = ({ content, classes, artSum, className }) => { 21 | return ( 22 | 23 |26 | ); 27 | }; 28 | 29 | export default AboutSite; 30 | -------------------------------------------------------------------------------- /src/pages/About/Switch/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/base.scss'; 2 | @import '../../../styles/style.scss'; 3 | 4 | $switchWidth: 36px; 5 | 6 | .switch { 7 | height: 50px; 8 | display: flex; 9 | justify-content: flex-start; 10 | align-items: center; 11 | user-select: none; 12 | margin-bottom: $space; 13 | 14 | .box { 15 | height: $switchWidth; 16 | width: $switchWidth * 2; 17 | border-radius: calc($switchWidth / 2); 18 | position: relative; 19 | background-color: $themeColor1; 20 | 21 | .btn { 22 | height: $switchWidth; 23 | width: $switchWidth; 24 | border-radius: 50%; 25 | position: absolute; 26 | top: 50%; 27 | left: 0; 28 | transform: translate(0, -50%); 29 | background-color: $themeColor2; 30 | @extend .trans; 31 | } 32 | 33 | .isMe { 34 | left: $switchWidth; 35 | } 36 | } 37 | .box:hover .btn { 38 | background-color: $hoverColor; 39 | } 40 | 41 | .text { 42 | font-size: 34px; 43 | font-weight: 700; 44 | color: $textColor; 45 | } 46 | 47 | .site { 48 | @extend .text; 49 | margin-right: 20px; 50 | } 51 | .me { 52 | @extend .text; 53 | margin-left: 20px; 54 | } 55 | 56 | .titleOff { 57 | color: $switchOff; 58 | } 59 | } 60 | 61 | @media screen and (max-width: 1240px) { 62 | .switch { 63 | height: 24rem; 64 | margin-bottom: 20rem; 65 | 66 | .box { 67 | height: 24rem; 68 | width: 24rem * 2; 69 | border-radius: 24rem / 2; 70 | 71 | .btn { 72 | height: 24rem; 73 | width: 24rem; 74 | } 75 | 76 | .isMe { 77 | left: 24rem; 78 | } 79 | } 80 | 81 | .text { 82 | font-size: 24rem; 83 | } 84 | 85 | .site { 86 | margin-right: 10rem; 87 | } 88 | .me { 89 | margin-left: 10rem; 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/pages/About/Switch/index.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import React from 'react'; 3 | 4 | import s from './index.scss'; 5 | 6 | interface Props { 7 | state: boolean; 8 | toggle: Function; 9 | setLeft: Function; 10 | setRight: Function; 11 | } 12 | 13 | const Switch: React.FC24 | 25 | = ({ state, toggle, setLeft, setRight }) => { 14 | return ( 15 | 16 |32 | ); 33 | }; 34 | 35 | export default Switch; 36 | -------------------------------------------------------------------------------- /src/pages/About/fetchData.ts: -------------------------------------------------------------------------------- 1 | import { DB } from '@/utils/apis/dbConfig'; 2 | import { getOrderData } from '@/utils/apis/getOrderData'; 3 | import { getSum } from '@/utils/apis/getSum'; 4 | import { _ } from '@/utils/cloudBase'; 5 | 6 | export const fetchData = async () => { 7 | const [about, classes, artSum] = await Promise.all([ 8 | getOrderData({ dbName: DB.About }), 9 | getOrderData({ dbName: DB.Class }), 10 | getSum(DB.Article, { post: _.eq(true) }) 11 | ]); 12 | 13 | return { 14 | about, 15 | classes, 16 | artSum 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /src/pages/About/index.scss: -------------------------------------------------------------------------------- 1 | .hidden { 2 | display: none; 3 | } 4 | -------------------------------------------------------------------------------- /src/pages/About/index.tsx: -------------------------------------------------------------------------------- 1 | import { useRequest, useToggle } from 'ahooks'; 2 | import React from 'react'; 3 | 4 | import Layout from '@/components/Layout'; 5 | import { DB } from '@/utils/apis/dbConfig'; 6 | import { staleTime } from '@/utils/constant'; 7 | 8 | import { Title } from '../titleConfig'; 9 | import AboutMe from './AboutMe'; 10 | import AboutSite from './AboutSite'; 11 | import { fetchData } from './fetchData'; 12 | import s from './index.scss'; 13 | import Switch from './Switch'; 14 | 15 | const About: React.FC = () => { 16 | const [state, { toggle, setLeft, setRight }] = useToggle(); 17 | 18 | const { data, loading } = useRequest(fetchData, { 19 | retryCount: 3, 20 | cacheKey: `About-${DB.About}`, 21 | staleTime 22 | }); 23 | 24 | return ( 25 |setLeft()} 19 | > 20 | 关于本站 21 |22 |toggle()}> 23 | 24 |25 |setRight()} 28 | > 29 | 关于我 30 |31 |26 | 35 | ); 36 | }; 37 | 38 | export default About; 39 | -------------------------------------------------------------------------------- /src/pages/ArtDetail/index.tsx: -------------------------------------------------------------------------------- 1 | import useUrlState from '@ahooksjs/use-url-state'; 2 | import { useRequest, useSafeState } from 'ahooks'; 3 | import dayjs from 'dayjs'; 4 | import React from 'react'; 5 | import { useNavigate } from 'react-router-dom'; 6 | 7 | import DisplayBar from '@/components/DisplayBar'; 8 | import Layout from '@/components/Layout'; 9 | import MyPagination from '@/components/MyPagination'; 10 | import { DB } from '@/utils/apis/dbConfig'; 11 | import { getWhereOrderPageSum } from '@/utils/apis/getWhereOrderPageSum'; 12 | import { _, db } from '@/utils/cloudBase'; 13 | import { detailPostSize, staleTime } from '@/utils/constant'; 14 | 15 | import { ArticleType } from '../constant'; 16 | 17 | const ArtDetail: React.FC = () => { 18 | const [query] = useUrlState(); 19 | const navigate = useNavigate(); 20 | 21 | const [page, setPage] = useSafeState(1); 22 | 23 | const where = query.tag 24 | ? { 25 | tags: db.RegExp({ 26 | regexp: `${query.tag}`, 27 | options: 'i' 28 | }) 29 | } 30 | : { 31 | classes: query.class 32 | }; 33 | 34 | const { data, loading } = useRequest( 35 | () => 36 | getWhereOrderPageSum({ 37 | dbName: DB.Article, 38 | where: { ...where, post: _.eq(true) }, 39 | page, 40 | size: detailPostSize, 41 | sortKey: 'date' 42 | }), 43 | { 44 | retryCount: 3, 45 | refreshDeps: [page], 46 | cacheKey: `ArtDetail-${DB.Article}-${JSON.stringify(where)}-${page}`, 47 | staleTime 48 | } 49 | ); 50 | 51 | return ( 52 |27 | 28 | 34 | 53 | {data?.articles.data.map((item: ArticleType) => ( 54 | 71 | ); 72 | }; 73 | 74 | export default ArtDetail; 75 | -------------------------------------------------------------------------------- /src/pages/Articles/ArtList/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/base.scss'; 2 | 3 | .none { 4 | height: 200px; 5 | display: flex; 6 | justify-content: center; 7 | align-items: center; 8 | color: $tip; 9 | user-select: none; 10 | } 11 | 12 | @media screen and (max-width: 1240px) { 13 | .none { 14 | height: 220rem; 15 | font-size: 16rem; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/pages/Articles/ArtList/index.tsx: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs'; 2 | import React from 'react'; 3 | import { useNavigate } from 'react-router-dom'; 4 | 5 | import DisplayBar from '@/components/DisplayBar'; 6 | import { ArticleType } from '@/pages/constant'; 7 | 8 | import s from './index.scss'; 9 | 10 | interface Props { 11 | articles?: ArticleType[]; 12 | loading?: boolean; 13 | } 14 | 15 | const ArtList: React.FCnavigate(`/post?title=${encodeURIComponent(item.titleEng)}`)} 60 | /> 61 | ))} 62 | 70 | = ({ articles, loading }) => { 16 | const navigate = useNavigate(); 17 | 18 | return ( 19 | <> 20 | {articles?.length ? ( 21 | articles?.map((item: ArticleType) => ( 22 | navigate(`/post?title=${encodeURIComponent(item.titleEng)}`)} 27 | loading={loading} 28 | /> 29 | )) 30 | ) : ( 31 | 暂时无相应文章 ~32 | )} 33 | > 34 | ); 35 | }; 36 | 37 | export default ArtList; 38 | -------------------------------------------------------------------------------- /src/pages/Articles/Search/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/base.scss'; 2 | @import '../../../styles/style.scss'; 3 | 4 | .searchBox { 5 | display: flex; 6 | margin-bottom: $space; 7 | 8 | .search { 9 | height: 50px; 10 | border-radius: 14px; 11 | background-color: $themeColor1; 12 | flex: 1; 13 | text-align: center; 14 | padding: 0 $space; 15 | } 16 | .searchBtn { 17 | height: 50px; 18 | width: 50px; 19 | border-radius: 14px; 20 | background-color: $themeColor1; 21 | display: flex; 22 | justify-content: center; 23 | align-items: center; 24 | user-select: none; 25 | margin-left: $space; 26 | @extend .hover; 27 | } 28 | } 29 | 30 | @media screen and (max-width: 1240px) { 31 | .searchBox { 32 | margin-bottom: 10rem; 33 | 34 | .search { 35 | height: 40rem; 36 | border-radius: 10rem; 37 | font-size: 18rem; 38 | padding: 0 10rem; 39 | } 40 | .searchBtn { 41 | height: 40rem; 42 | width: 40rem; 43 | border-radius: 10rem; 44 | margin-left: 6rem; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/pages/Articles/Search/index.tsx: -------------------------------------------------------------------------------- 1 | import { ArrowRightOutlined, RedoOutlined } from '@ant-design/icons'; 2 | import { useKeyPress, useSafeState } from 'ahooks'; 3 | import { message } from 'antd'; 4 | import React, { useRef } from 'react'; 5 | 6 | import { db } from '@/utils/cloudBase'; 7 | 8 | import s from './index.scss'; 9 | 10 | interface Props { 11 | page: number; 12 | setPage: Function; 13 | where: object; 14 | setWhere: Function; 15 | run: Function; 16 | } 17 | 18 | const Search: React.FC= ({ page, setPage, where, setWhere, run }) => { 19 | const [input, setInput] = useSafeState(''); 20 | const inputRef = useRef(null); 21 | 22 | const search = () => { 23 | if (!input) { 24 | message.info('请输入关键词再搜索!'); 25 | return; 26 | } 27 | setTimeout(() => { 28 | setWhere({ 29 | title: db.RegExp({ 30 | regexp: `${input}`, 31 | options: 'i' 32 | }) 33 | }); 34 | setPage(1); 35 | run?.(); 36 | }, 0); 37 | }; 38 | 39 | const reset = () => { 40 | if (JSON.stringify(where) === '{}' && page === 1 && !input) { 41 | message.info('无需重置!'); 42 | return; 43 | } 44 | if (JSON.stringify(where) === '{}' && page === 1) { 45 | setInput(''); 46 | return; 47 | } 48 | setTimeout(() => { 49 | setInput?.(''); 50 | setWhere({}); 51 | setPage(1); 52 | run?.(); 53 | }, 0); 54 | }; 55 | 56 | useKeyPress(13, search, { 57 | target: inputRef 58 | }); 59 | 60 | useKeyPress(27, reset, { 61 | target: inputRef 62 | }); 63 | 64 | return ( 65 | 66 | setInput?.(e.target.value)} 74 | /> 75 | {/* 搜索按钮 */} 76 |84 | ); 85 | }; 86 | 87 | export default Search; 88 | -------------------------------------------------------------------------------- /src/pages/Articles/index.tsx: -------------------------------------------------------------------------------- 1 | import { useRequest, useSafeState } from 'ahooks'; 2 | import React from 'react'; 3 | 4 | import Layout from '@/components/Layout'; 5 | import MyPagination from '@/components/MyPagination'; 6 | import { DB } from '@/utils/apis/dbConfig'; 7 | import { getWhereOrderPageSum } from '@/utils/apis/getWhereOrderPageSum'; 8 | import { _ } from '@/utils/cloudBase'; 9 | import { detailPostSize, staleTime } from '@/utils/constant'; 10 | 11 | import { Title } from '../titleConfig'; 12 | import ArtList from './ArtList'; 13 | import Search from './Search'; 14 | 15 | const Articles: React.FC = () => { 16 | const [page, setPage] = useSafeState(1); 17 | 18 | const [where, setWhere] = useSafeState(() => ({})); 19 | 20 | const { data, loading, run } = useRequest( 21 | () => 22 | getWhereOrderPageSum({ 23 | dbName: DB.Article, 24 | where: { ...where, post: _.eq(true) }, 25 | page, 26 | size: detailPostSize, 27 | sortKey: 'date' 28 | }), 29 | { 30 | retryCount: 3, 31 | refreshDeps: [page], 32 | cacheKey: `Articles-${DB.Article}-${JSON.stringify(where)}-${page}`, 33 | staleTime 34 | } 35 | ); 36 | 37 | return ( 38 |77 |79 | {/* 重置按钮 */} 80 |78 | 81 |83 |82 | 39 | 50 | ); 51 | }; 52 | 53 | export default Articles; 54 | -------------------------------------------------------------------------------- /src/pages/Classes/ClassBar/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/base.scss'; 2 | @import '../../../styles/style.scss'; 3 | 4 | .classBar { 5 | position: relative; 6 | background-color: $themeColor1; 7 | height: 50px; 8 | border-radius: 14px; 9 | display: flex; 10 | justify-content: center; 11 | align-items: center; 12 | user-select: none; 13 | @extend .hover; 14 | cursor: pointer; 15 | 16 | .classNum { 17 | position: absolute; 18 | right: 10px; 19 | top: 50%; 20 | transform: translate(0, -50%); 21 | width: 36px; 22 | display: flex; 23 | justify-content: center; 24 | align-items: center; 25 | } 26 | } 27 | 28 | @media screen and (max-width: 1240px) { 29 | .classBar { 30 | height: 40rem; 31 | border-radius: 10rem; 32 | font-size: 18rem; 33 | 34 | .classNum { 35 | right: 6rem; 36 | width: 28rem; 37 | font-size: 16rem; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/pages/Classes/ClassBar/index.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import React, { MouseEventHandler } from 'react'; 3 | 4 | import s from './index.scss'; 5 | 6 | interface Props { 7 | content: string; 8 | num: number; 9 | className?: string; 10 | onClick?: MouseEventHandler40 | 41 | 49 | ; 11 | } 12 | 13 | const ClassBar: React.FC = ({ content, num, onClick, className }) => { 14 | return ( 15 | 16 | {content} 17 |19 | ); 20 | }; 21 | 22 | export default ClassBar; 23 | -------------------------------------------------------------------------------- /src/pages/Classes/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/base.scss'; 2 | 3 | .classBox { 4 | display: flex; 5 | flex-wrap: wrap; 6 | justify-content: space-between; 7 | 8 | .classItem { 9 | width: 49%; 10 | margin-bottom: $space / 2; 11 | } 12 | 13 | .classItem:nth-last-child(1), 14 | .classItem:nth-last-child(2) { 15 | margin-bottom: 0; 16 | } 17 | } 18 | 19 | @media screen and (max-width: 1240px) { 20 | .classBox { 21 | .classItem { 22 | width: 100%; 23 | margin-bottom: 10rem; 24 | } 25 | 26 | .classItem:nth-last-child(2) { 27 | margin-bottom: 10rem; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/pages/Classes/index.tsx: -------------------------------------------------------------------------------- 1 | import { useRequest } from 'ahooks'; 2 | import React from 'react'; 3 | import { useNavigate } from 'react-router-dom'; 4 | 5 | import Layout from '@/components/Layout'; 6 | import { DB } from '@/utils/apis/dbConfig'; 7 | import { getData } from '@/utils/apis/getData'; 8 | import { staleTime } from '@/utils/constant'; 9 | 10 | import { Title } from '../titleConfig'; 11 | import ClassBar from './ClassBar'; 12 | import s from './index.scss'; 13 | 14 | interface ClassType { 15 | _id: string; 16 | class: string; 17 | count: number; 18 | } 19 | 20 | const Classes: React.FC = () => { 21 | const navigate = useNavigate(); 22 | 23 | const { data, loading } = useRequest(getData, { 24 | defaultParams: [DB.Class], 25 | retryCount: 3, 26 | cacheKey: `Classes-${DB.Class}`, 27 | staleTime 28 | }); 29 | 30 | return ( 31 |{num}18 |32 | {data?.data.map((item: ClassType) => ( 33 | 42 | ); 43 | }; 44 | 45 | export default Classes; 46 | -------------------------------------------------------------------------------- /src/pages/Home/Aside/AccountCard/Csdn/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../../styles/base.scss'; 2 | @import '../../../../../styles/style.scss'; 3 | 4 | .csdn { 5 | width: 30px; 6 | height: 30px; 7 | 8 | path { 9 | fill: $textColor; 10 | @extend .trans; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/pages/Home/Aside/AccountCard/Csdn/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import s from './index.scss'; 4 | 5 | const Csdn: React.FC = () => ( 6 | 9 | ); 10 | 11 | export default Csdn; 12 | -------------------------------------------------------------------------------- /src/pages/Home/Aside/AccountCard/IcoBtn/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../../styles/base.scss'; 2 | @import '../../../../../styles/style.scss'; 3 | 4 | .socialBtn { 5 | color: $textColor; 6 | width: 44px; 7 | height: 44px; 8 | display: flex; 9 | justify-content: center; 10 | align-items: center; 11 | border-radius: $midRadius; 12 | @extend .hover; 13 | } 14 | .socialBtn:hover { 15 | color: $textColor; 16 | } 17 | 18 | .card { 19 | border-radius: $bigRadius; 20 | overflow: hidden; 21 | padding: 0; 22 | box-shadow: 0 0 14px $footerBg; 23 | 24 | span { 25 | display: none; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/pages/Home/Aside/AccountCard/IcoBtn/index.tsx: -------------------------------------------------------------------------------- 1 | import { Popover } from 'antd'; 2 | import React, { ReactNode } from 'react'; 3 | 4 | import s from './index.scss'; 5 | 6 | interface Props { 7 | isLink: boolean; 8 | link?: string; 9 | content?: ReactNode; 10 | } 11 | 12 | const IcoBtn: React.FCnavigate(`/artDetail?class=${encodeURIComponent(item.class)}`)} 39 | /> 40 | ))} 41 | = ({ isLink, link, content, children }) => { 13 | return isLink ? ( 14 | 15 | {children} 16 | 17 | ) : ( 18 | 24 | {children} 25 | 26 | ); 27 | }; 28 | 29 | export default IcoBtn; 30 | -------------------------------------------------------------------------------- /src/pages/Home/Aside/AccountCard/index.scss: -------------------------------------------------------------------------------- 1 | .card { 2 | display: flex; 3 | justify-content: space-between; 4 | align-items: center; 5 | font-size: 30px; 6 | } 7 | -------------------------------------------------------------------------------- /src/pages/Home/Aside/AccountCard/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Card from '@/components/Card'; 4 | 5 | import IcoBtn from './IcoBtn'; 6 | import s from './index.scss'; 7 | import { useAccount } from './useAccount'; 8 | 9 | const AccountCard: React.FC = () => { 10 | const accounts = useAccount(); 11 | 12 | return ( 13 |14 | {accounts.map(({ isLink, link, ico, content }, index) => ( 15 | 20 | ); 21 | }; 22 | 23 | export default AccountCard; 24 | -------------------------------------------------------------------------------- /src/pages/Home/Aside/AccountCard/useAccount.tsx: -------------------------------------------------------------------------------- 1 | import { GithubOutlined, QqOutlined, WechatOutlined } from '@ant-design/icons'; 2 | import React from 'react'; 3 | 4 | import { csdnUrl, githubUrl, QQ_QRCode, weChatQRCode } from '@/utils/constant'; 5 | 6 | import Csdn from './Csdn'; 7 | 8 | export const useAccount = () => { 9 | const imgStyle = { width: '120px', height: '120px' }; 10 | 11 | return [ 12 | { 13 | isLink: true, 14 | link: githubUrl, 15 | ico:16 | {ico} 17 | 18 | ))} 19 |, 16 | content: null 17 | }, 18 | { 19 | isLink: true, 20 | link: csdnUrl, 21 | ico: , 22 | content: null 23 | }, 24 | { 25 | isLink: false, 26 | link: '', 27 | ico: , 28 | content: 29 | }, 30 | { 31 | isLink: false, 32 | link: '', 33 | ico:
, 34 | content: 35 | } 36 | ]; 37 | }; 38 | -------------------------------------------------------------------------------- /src/pages/Home/Aside/BlogCard/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../styles/base.scss'; 2 | 3 | .card { 4 | height: 200px; 5 | position: relative; 6 | 7 | .avatar { 8 | position: absolute; 9 | bottom: 0; 10 | right: -30px; 11 | height: 200px; 12 | } 13 | 14 | .text { 15 | margin: 0; 16 | position: absolute; 17 | top: 36px; 18 | left: 20px; 19 | font-size: 20px; 20 | line-height: 34px; 21 | } 22 | 23 | .color { 24 | font-weight: 700; 25 | color: $hoverColor; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/pages/Home/Aside/BlogCard/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Card from '@/components/Card'; 4 | import { cardUrl } from '@/utils/constant'; 5 | import { useTime } from '@/utils/hooks/useTime'; 6 | 7 | import s from './index.scss'; 8 | 9 | const BlogCard: React.FC = () => { 10 | const { timeText } = useTime(); 11 | 12 | return ( 13 |
14 | 23 | ); 24 | }; 25 | 26 | export default BlogCard; 27 | -------------------------------------------------------------------------------- /src/pages/Home/Aside/ClockCard/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../styles/base.scss'; 2 | 3 | .pointX { 4 | position: absolute; 5 | left: 50%; 6 | width: 2px; 7 | height: 18px; 8 | background-color: $textColor; 9 | transform: translate(-50%, 0); 10 | } 11 | 12 | .pointY { 13 | position: absolute; 14 | top: 50%; 15 | height: 2px; 16 | width: 18px; 17 | background-color: $textColor; 18 | transform: translate(0, -50%); 19 | } 20 | 21 | .clockLineBase { 22 | position: absolute; 23 | transform-origin: bottom; 24 | background-color: $textColor; 25 | bottom: 0; 26 | } 27 | 28 | .card { 29 | height: $asideWidth; 30 | display: flex; 31 | align-items: center; 32 | justify-content: center; 33 | position: relative; 34 | 35 | .dial { 36 | border: 2px solid $textColor; 37 | width: $asideWidth - 2 * $space; 38 | height: $asideWidth - 2 * $space; 39 | border-radius: 50%; 40 | // box-shadow: 0 0 24px #000 inset; 41 | background-color: $themeColor1; 42 | position: absolute; 43 | 44 | .zero { 45 | @extend .pointX; 46 | top: 0; 47 | } 48 | .six { 49 | @extend .pointX; 50 | bottom: 0; 51 | } 52 | .three { 53 | @extend .pointY; 54 | right: 0; 55 | } 56 | .nine { 57 | @extend .pointY; 58 | left: 0; 59 | } 60 | } 61 | 62 | .container { 63 | position: relative; 64 | 65 | .dot { 66 | position: absolute; 67 | width: 12px; 68 | height: 12px; 69 | top: -6px; 70 | left: -6px; 71 | border-radius: 50%; 72 | z-index: 4; 73 | background-color: $hoverColor; 74 | } 75 | 76 | .clockMinuteLine { 77 | @extend .clockLineBase; 78 | width: 4px; 79 | height: 90px; 80 | border-radius: 2px; 81 | left: -2px; 82 | } 83 | 84 | .clockHourLine { 85 | @extend .clockLineBase; 86 | width: 6px; 87 | height: 60px; 88 | border-radius: 3px; 89 | left: -3px; 90 | } 91 | 92 | .clockSecondLine { 93 | @extend .clockLineBase; 94 | background-color: $hoverColor; 95 | width: 2px; 96 | height: 90px; 97 | border-radius: 1px; 98 | left: -1px; 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/pages/Home/Aside/ClockCard/index.tsx: -------------------------------------------------------------------------------- 1 | import { useInterval } from 'ahooks'; 2 | import React from 'react'; 3 | 4 | import Card from '@/components/Card'; 5 | 6 | import s from './index.scss'; 7 | import { useClock } from './useClock'; 8 | 9 | const ClockCard: React.FC = () => { 10 | const { hour, minute, second, runPerSecond } = useClock(); 11 | useInterval(runPerSecond, 1000); 12 | 13 | return ( 14 |15 | {timeText},
21 |
16 | 我叫飞鸟,
17 | 欢迎来到 18 |
19 | 我的个人博客。 20 |22 |
15 | 34 | ); 35 | }; 36 | 37 | export default ClockCard; 38 | -------------------------------------------------------------------------------- /src/pages/Home/Aside/ClockCard/useClock.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | export const useClock = () => { 4 | const [hour, setHour] = useState(0); 5 | const [minute, setMinute] = useState(0); 6 | const [second, setSecond] = useState(0); 7 | 8 | const runPerSecond = () => { 9 | const date = new Date(); 10 | const hours = date.getHours(); 11 | const minutes = date.getMinutes(); 12 | const seconds = date.getSeconds(); 13 | 14 | const hour = (hours % 12) * (360 / 12) + (360 / 12) * (minutes / 60); 15 | const minute = minutes * (360 / 60) + (360 / 60) * (seconds / 60); 16 | const second = seconds * (360 / 60); 17 | 18 | setHour(hour); 19 | setMinute(minute); 20 | setSecond(second); 21 | }; 22 | 23 | return { 24 | hour, 25 | minute, 26 | second, 27 | runPerSecond 28 | }; 29 | }; 30 | -------------------------------------------------------------------------------- /src/pages/Home/Aside/DataCard/fetchData.ts: -------------------------------------------------------------------------------- 1 | import { DB } from '@/utils/apis/dbConfig'; 2 | import { getSum } from '@/utils/apis/getSum'; 3 | import { _ } from '@/utils/cloudBase'; 4 | 5 | export const fetchData = async () => { 6 | const [articles, classes, tags] = await Promise.all([ 7 | getSum(DB.Article, { post: _.eq(true) }), 8 | getSum(DB.Class), 9 | getSum(DB.Tag) 10 | ]); 11 | 12 | return { 13 | articles, 14 | classes, 15 | tags 16 | }; 17 | }; 18 | -------------------------------------------------------------------------------- /src/pages/Home/Aside/DataCard/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../styles/base.scss'; 2 | @import '../../../../styles/style.scss'; 3 | 4 | .card { 5 | height: 80px; 6 | position: relative; 7 | display: flex; 8 | justify-content: space-between; 9 | align-items: center; 10 | 11 | .blogData { 12 | width: 64px; 13 | height: 58px; 14 | font-size: 18px; 15 | display: flex; 16 | flex-direction: column; 17 | justify-content: center; 18 | align-items: center; 19 | border-radius: 14px; 20 | cursor: pointer; 21 | @extend .hover; 22 | } 23 | 24 | .name, 25 | .num { 26 | flex: 1; 27 | display: flex; 28 | justify-content: center; 29 | align-items: center; 30 | } 31 | .num { 32 | @extend .trans; 33 | color: $hoverColor; 34 | font-weight: 700; 35 | } 36 | } 37 | 38 | .blogData:hover .num { 39 | color: #fff !important; 40 | } 41 | -------------------------------------------------------------------------------- /src/pages/Home/Aside/DataCard/index.tsx: -------------------------------------------------------------------------------- 1 | import { useRequest } from 'ahooks'; 2 | import React from 'react'; 3 | import { connect } from 'react-redux'; 4 | import { useNavigate } from 'react-router-dom'; 5 | 6 | import Card from '@/components/Card'; 7 | import { setArtSum } from '@/redux/actions'; 8 | import { DB } from '@/utils/apis/dbConfig'; 9 | import { staleTime } from '@/utils/constant'; 10 | 11 | import { fetchData } from './fetchData'; 12 | import s from './index.scss'; 13 | 14 | interface Props { 15 | setArtSum?: Function; 16 | } 17 | 18 | const DataCard: React.FC16 | 17 | 18 | 19 | 20 |21 |22 | 23 | 27 | 28 | 32 |33 |= ({ setArtSum }) => { 19 | const navigate = useNavigate(); 20 | const { data, loading } = useRequest(fetchData, { 21 | retryCount: 3, 22 | cacheKey: `DataCard-count-${DB.Article}-${DB.Class}-${DB.Tag}`, 23 | staleTime, 24 | onSuccess: data => setArtSum!(data?.articles.total) 25 | }); 26 | 27 | return ( 28 | 29 | 42 | ); 43 | }; 44 | 45 | export default connect(() => ({}), { 46 | setArtSum 47 | })(DataCard); 48 | -------------------------------------------------------------------------------- /src/pages/Home/Aside/NoticeCard/index.scss: -------------------------------------------------------------------------------- 1 | @import '../.././../../styles/base.scss'; 2 | 3 | .notice { 4 | border-radius: 14px; 5 | background-color: $themeColor1; 6 | padding: 14px; 7 | font-size: 16px; 8 | } 9 | -------------------------------------------------------------------------------- /src/pages/Home/Aside/NoticeCard/index.tsx: -------------------------------------------------------------------------------- 1 | import { useRequest } from 'ahooks'; 2 | import React from 'react'; 3 | 4 | import Card from '@/components/Card'; 5 | import { DB } from '@/utils/apis/dbConfig'; 6 | import { getOrderData } from '@/utils/apis/getOrderData'; 7 | import { staleTime } from '@/utils/constant'; 8 | 9 | import s from './index.scss'; 10 | 11 | const NoticeCard: React.FC = () => { 12 | const { data, loading } = useRequest(getOrderData, { 13 | defaultParams: [{ dbName: DB.Notice }], 14 | retryCount: 3, 15 | cacheKey: `NoticeCard-${DB.Notice}`, 16 | staleTime 17 | }); 18 | 19 | return ( 20 |navigate('/articles')}> 30 |33 |文章31 |{data?.articles.total}32 |navigate('/classes')}> 34 |37 |分类35 |{data?.classes.total}36 |navigate('/tags')}> 38 |41 |标签39 |{data?.tags.total}40 |21 | 23 | ); 24 | }; 25 | 26 | export default NoticeCard; 27 | -------------------------------------------------------------------------------- /src/pages/Home/Aside/SiteCard/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../styles/base.scss'; 2 | @import '../../../../styles/style.scss'; 3 | 4 | .card { 5 | height: 120px; 6 | display: flex; 7 | flex-direction: column; 8 | justify-content: space-between; 9 | align-items: center; 10 | 11 | .item { 12 | height: 36px; 13 | width: 100%; 14 | font-size: 18px; 15 | display: flex; 16 | justify-content: space-between; 17 | align-items: center; 18 | border-radius: $midRadius; 19 | @extend .hover; 20 | 21 | .key { 22 | margin-left: 10px; 23 | } 24 | 25 | .value { 26 | margin-right: 10px; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/pages/Home/Aside/SiteCard/index.tsx: -------------------------------------------------------------------------------- 1 | import { useRequest } from 'ahooks'; 2 | import React from 'react'; 3 | 4 | import Card from '@/components/Card'; 5 | import { DB } from '@/utils/apis/dbConfig'; 6 | import { getSiteCount } from '@/utils/apis/getSiteCount'; 7 | import { siteCountStale } from '@/utils/constant'; 8 | 9 | import s from './index.scss'; 10 | import { useRunTime } from './useRunTime'; 11 | 12 | const SiteCard: React.FC = () => { 13 | const { runTime } = useRunTime(); 14 | 15 | const { data, loading } = useRequest(getSiteCount, { 16 | retryCount: 3, 17 | cacheKey: `SiteCard-${DB.Count}`, 18 | staleTime: siteCountStale 19 | }); 20 | 21 | return ( 22 |{data?.data[0].notice}22 |23 | 32 | ); 33 | }; 34 | 35 | export default SiteCard; 36 | -------------------------------------------------------------------------------- /src/pages/Home/Aside/SiteCard/useRunTime.ts: -------------------------------------------------------------------------------- 1 | import { useMount, useSafeState } from 'ahooks'; 2 | import dayjs from 'dayjs'; 3 | 4 | import { time } from '@/utils/constant'; 5 | 6 | export const useRunTime = () => { 7 | const [runTime, setRunTime] = useSafeState(0); 8 | 9 | useMount(() => { 10 | const nowTime = new Date().getTime(); 11 | const startTime = new Date(time).getTime(); 12 | const runTime = dayjs(nowTime).diff(dayjs(startTime), 'days'); 13 | setRunTime(runTime); 14 | }); 15 | 16 | return { runTime }; 17 | }; 18 | -------------------------------------------------------------------------------- /src/pages/Home/Aside/TagCard/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../styles/base.scss'; 2 | @import '../../../../styles/style.scss'; 3 | 4 | .card { 5 | padding: 10px; 6 | } 7 | 8 | .tag { 9 | @extend .tagBase; 10 | } 11 | -------------------------------------------------------------------------------- /src/pages/Home/Aside/TagCard/index.tsx: -------------------------------------------------------------------------------- 1 | import { useRequest } from 'ahooks'; 2 | import React from 'react'; 3 | 4 | import Card from '@/components/Card'; 5 | import { DB } from '@/utils/apis/dbConfig'; 6 | import { getData } from '@/utils/apis/getData'; 7 | import { staleTime } from '@/utils/constant'; 8 | 9 | import s from './index.scss'; 10 | 11 | const TagCard: React.FC = () => { 12 | const { data, loading } = useRequest(getData, { 13 | defaultParams: [DB.Tag], 14 | retryCount: 3, 15 | cacheKey: `TagCard-${DB.Tag}`, 16 | staleTime 17 | }); 18 | 19 | return ( 20 |24 | 总浏览量 25 | {data?.data[0].count}次 26 |27 |28 | 运行时间 29 | {runTime}天 30 |31 |21 | {data?.data?.map( 22 | (item: { _id: string; _openid: string; tag: string }, index: number) => ( 23 | 24 | {item.tag} 25 | 26 | ) 27 | )} 28 | 29 | ); 30 | }; 31 | 32 | export default TagCard; 33 | -------------------------------------------------------------------------------- /src/pages/Home/Aside/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/base.scss'; 2 | 3 | .aside { 4 | width: $asideWidth; 5 | 6 | .cardSticky { 7 | position: sticky; 8 | top: $navHeight + $space; 9 | } 10 | } 11 | 12 | @media screen and (max-width: 1240px) { 13 | .aside { 14 | display: none; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/pages/Home/Aside/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import AccountCard from './AccountCard'; 4 | import BlogCard from './BlogCard'; 5 | import ClockCard from './ClockCard'; 6 | import DataCard from './DataCard'; 7 | import s from './index.scss'; 8 | import NoticeCard from './NoticeCard'; 9 | import SiteCard from './SiteCard'; 10 | import TagCard from './TagCard'; 11 | 12 | const Aside: React.FC = () => { 13 | return ( 14 | 25 | ); 26 | }; 27 | 28 | export default Aside; 29 | -------------------------------------------------------------------------------- /src/pages/Home/Section/PostCard/PostCardLoading/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../../styles/base.scss'; 2 | 3 | .postCardLoading { 4 | height: 50px + 80px + 80px; 5 | display: flex; 6 | flex-direction: column; 7 | justify-content: space-evenly; 8 | align-items: center; 9 | 10 | .bar { 11 | width: 85%; 12 | height: 20px; 13 | background-color: $themeColor1; 14 | border-radius: 6px; 15 | } 16 | } 17 | 18 | @media screen and (max-width: 1240px) { 19 | .postCardLoading { 20 | height: 50rem+ 72rem; 21 | 22 | .bar { 23 | width: 85%; 24 | height: 14rem; 25 | border-radius: 4rem; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/pages/Home/Section/PostCard/PostCardLoading/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import s from './index.scss'; 4 | 5 | const PostCardLoading: React.FC = () => { 6 | return ( 7 |8 | 9 | 10 | 11 | 12 |13 | ); 14 | }; 15 | 16 | export default PostCardLoading; 17 | -------------------------------------------------------------------------------- /src/pages/Home/Section/PostCard/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../styles/base.scss'; 2 | @import '../../../../styles/style.scss'; 3 | 4 | .tagBase { 5 | padding: 0 10px; 6 | border-radius: 10px; 7 | color: $textColor; 8 | height: 30px; 9 | font-size: 18px; 10 | background-color: $themeColor1; 11 | @extend .hover; 12 | } 13 | 14 | .card { 15 | cursor: pointer; 16 | 17 | .title { 18 | margin: 0; 19 | height: 80px; 20 | display: flex; 21 | justify-content: center; 22 | align-items: center; 23 | font-size: 28px; 24 | font-weight: 700; 25 | color: $textColor; 26 | } 27 | 28 | .content { 29 | margin: 0; 30 | height: 80px; 31 | text-indent: 2em; 32 | color: $textColor; 33 | font-size: 18px; 34 | overflow: hidden; 35 | text-overflow: ellipsis; 36 | display: -webkit-box; 37 | -webkit-line-clamp: 3; 38 | -webkit-box-orient: vertical; 39 | } 40 | 41 | .info { 42 | position: relative; 43 | height: 50px; 44 | 45 | .date { 46 | @extend .tagBase; 47 | position: absolute; 48 | bottom: 0; 49 | display: flex; 50 | justify-content: center; 51 | align-items: center; 52 | } 53 | 54 | .tags { 55 | position: absolute; 56 | bottom: 0; 57 | right: 0; 58 | max-width: 800px; 59 | height: 30px; 60 | 61 | .tag { 62 | @extend .tagBase; 63 | display: inline-block; 64 | margin-right: 20px; 65 | text-align: center; 66 | line-height: 30px; 67 | } 68 | 69 | .tag:last-child { 70 | margin-right: 0; 71 | } 72 | } 73 | } 74 | } 75 | 76 | .card:hover { 77 | transform: scale(1.02); 78 | cursor: pointer; 79 | } 80 | 81 | @media screen and (max-width: 1240px) { 82 | .card { 83 | .title { 84 | height: 50rem; 85 | line-height: 24rem; 86 | font-size: 20rem; 87 | } 88 | 89 | .content { 90 | height: 72rem; 91 | text-indent: 2em; 92 | font-size: 16rem; 93 | } 94 | 95 | .info { 96 | display: none; 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/pages/Home/Section/PostCard/index.tsx: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs'; 2 | import React, { MouseEventHandler } from 'react'; 3 | 4 | import Card from '@/components/Card'; 5 | 6 | import s from './index.scss'; 7 | import PostCardLoading from './PostCardLoading'; 8 | 9 | interface Props { 10 | title?: string; 11 | content?: string; 12 | date?: number; 13 | tags?: string[]; 14 | loading?: boolean; 15 | onClick?: MouseEventHandler; 16 | } 17 | 18 | const PostCard: React.FC = ({ title, content, date, tags, loading, onClick }) => { 19 | return ( 20 | 21 | {loading ? ( 22 | 42 | ); 43 | }; 44 | 45 | export default PostCard; 46 | -------------------------------------------------------------------------------- /src/pages/Home/Section/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/base.scss'; 2 | 3 | .section { 4 | width: calc(100% - #{$asideWidth}); 5 | padding-right: $space; 6 | box-sizing: border-box; 7 | } 8 | 9 | @media screen and (max-width: 1240px) { 10 | .section { 11 | width: 100vw; 12 | padding: 0 10rem; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/pages/Home/Section/index.tsx: -------------------------------------------------------------------------------- 1 | import { useRequest, useSafeState } from 'ahooks'; 2 | import React from 'react'; 3 | import { connect } from 'react-redux'; 4 | import { useNavigate } from 'react-router-dom'; 5 | 6 | import MyPagination from '@/components/MyPagination'; 7 | import { storeState } from '@/redux/interface'; 8 | import { DB } from '@/utils/apis/dbConfig'; 9 | import { getPageData } from '@/utils/apis/getPageData'; 10 | import { _ } from '@/utils/cloudBase'; 11 | import { homeSize, staleTime } from '@/utils/constant'; 12 | 13 | import s from './index.scss'; 14 | import PostCard from './PostCard'; 15 | 16 | interface theAtc { 17 | classes: string; 18 | content: string; 19 | date: number; 20 | tags: string[]; 21 | title: string; 22 | titleEng: string; 23 | url: string; 24 | _id: string; 25 | _openid: string; 26 | } 27 | 28 | interface Props { 29 | artSum?: number; 30 | } 31 | 32 | const Section: React.FC23 | ) : ( 24 | <> 25 | {title}26 |27 | {content!.replace(/(.*?)<\/a>/g, '$2').replace(/[# |**|`|>]/g, '')} 28 |
29 |30 | {dayjs(date!).format('YYYY-MM-DD')} 31 |39 | > 40 | )} 41 |32 | {tags!.map(tag => ( 33 | 34 | {tag} 35 | 36 | ))} 37 |38 |= ({ artSum }) => { 33 | const navigate = useNavigate(); 34 | const [page, setPage] = useSafeState(1); 35 | 36 | const { data, loading } = useRequest( 37 | () => 38 | getPageData({ 39 | dbName: DB.Article, 40 | where: { post: _.eq(true) }, 41 | sortKey: 'date', 42 | isAsc: false, 43 | page, 44 | size: homeSize 45 | }), 46 | { 47 | retryCount: 3, 48 | refreshDeps: [page], 49 | cacheKey: `Section-${DB.Article}-${page}`, 50 | staleTime 51 | } 52 | ); 53 | 54 | return ( 55 | 56 | {data?.data.map(({ _id, title, content, date, tags, titleEng }: theAtc) => ( 57 | 76 | ); 77 | }; 78 | 79 | export default connect((state: storeState) => ({ artSum: state.artSum }))(Section); 80 | -------------------------------------------------------------------------------- /src/pages/Home/index.scss: -------------------------------------------------------------------------------- 1 | .homeTitle { 2 | height: 100vh; 3 | } 4 | 5 | .body { 6 | display: flex; 7 | } 8 | 9 | @media screen and (max-width: 1240px) { 10 | .homeTitle { 11 | height: 50vh; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/pages/Home/index.tsx: -------------------------------------------------------------------------------- 1 | import { useMount, useSafeState, useTitle } from 'ahooks'; 2 | import React from 'react'; 3 | import { connect } from 'react-redux'; 4 | 5 | import PageTitle from '@/components/PageTitle'; 6 | import { setNavShow } from '@/redux/actions'; 7 | import { siteTitle } from '@/utils/constant'; 8 | import useTop from '@/utils/hooks/useTop'; 9 | 10 | import Aside from './Aside'; 11 | import s from './index.scss'; 12 | import Section from './Section'; 13 | 14 | interface Props { 15 | setNavShow?: Function; 16 | } 17 | 18 | const getPoem = require('jinrishici'); 19 | 20 | const Home: React.FCnavigate(`/post?title=${encodeURIComponent(titleEng)}`)} 65 | /> 66 | ))} 67 | 75 | = ({ setNavShow }) => { 21 | useTitle(siteTitle); 22 | useTop(setNavShow); 23 | 24 | const [poem, setPoem] = useSafeState(''); 25 | useMount(() => { 26 | getPoem.load( 27 | (res: { 28 | data: { 29 | content: string; 30 | }; 31 | }) => setPoem(res.data.content) 32 | ); 33 | }); 34 | 35 | return ( 36 | <> 37 | 38 | 39 | 40 | 41 |42 | > 43 | ); 44 | }; 45 | 46 | export default connect(() => ({}), { setNavShow })(Home); 47 | -------------------------------------------------------------------------------- /src/pages/Link/LinkItem/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/base.scss'; 2 | @import '../../../styles/style.scss'; 3 | 4 | .item { 5 | width: 31.94%; 6 | height: 90px; 7 | border-radius: 20px; 8 | padding: 10px; 9 | margin-bottom: $space; 10 | display: flex; 11 | justify-content: center; 12 | align-items: center; 13 | user-select: none; 14 | background-color: $themeColor1; 15 | @extend .hover; 16 | 17 | .link { 18 | width: 100%; 19 | height: 100%; 20 | display: flex; 21 | 22 | .left { 23 | width: 70px; 24 | height: 70px; 25 | border-radius: 14px; 26 | overflow: hidden; 27 | display: flex; 28 | justify-content: center; 29 | align-items: center; 30 | background-color: $themeColor; 31 | 32 | .avatar { 33 | width: 100%; 34 | height: 100%; 35 | } 36 | 37 | .loading { 38 | width: 60%; 39 | height: 60%; 40 | } 41 | } 42 | 43 | .right { 44 | width: calc(100% - 70px); 45 | 46 | .title { 47 | padding: 0 10px; 48 | font-weight: 700; 49 | height: 28px; 50 | color: $hoverColor; 51 | text-align: center; 52 | overflow: hidden; 53 | @extend .trans; 54 | } 55 | 56 | .descr { 57 | padding-left: 10px; 58 | display: flex; 59 | justify-content: center; 60 | align-items: center; 61 | height: 42px; 62 | font-size: 16px; 63 | display: flex; 64 | align-items: center; 65 | line-height: 20px; 66 | color: $textColor; 67 | overflow: hidden; 68 | } 69 | } 70 | } 71 | } 72 | 73 | .item:hover .title { 74 | color: #fff !important; 75 | } 76 | 77 | .item:nth-last-child(1), 78 | .item:nth-last-child(2), 79 | .item:nth-last-child(3) { 80 | margin-bottom: 0; 81 | } 82 | 83 | @media screen and (max-width: 1240px) { 84 | .item { 85 | width: 48.81%; 86 | height: 60rem; 87 | border-radius: 10rem; 88 | padding: 5rem; 89 | margin-bottom: 10rem; 90 | 91 | .link { 92 | .left { 93 | width: 50rem; 94 | height: 50rem; 95 | border-radius: 8rem; 96 | } 97 | .right { 98 | width: calc(100% - 50rem); 99 | 100 | .title { 101 | padding: 0 10rem; 102 | height: 100%; 103 | display: flex; 104 | justify-content: center; 105 | align-items: center; 106 | font-size: 16rem; 107 | line-height: 16rem; 108 | } 109 | 110 | .descr { 111 | display: none; 112 | } 113 | } 114 | } 115 | } 116 | .item:nth-last-child(3) { 117 | margin-bottom: 10rem; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/pages/Link/LinkItem/index.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import React from 'react'; 3 | 4 | import { smallLoadingUrl } from '@/utils/constant'; 5 | import { useLazyImg } from '@/utils/hooks/useLazyImg'; 6 | 7 | import s from './index.scss'; 8 | 9 | interface Props { 10 | link?: string; 11 | avatar?: string; 12 | name?: string; 13 | descr?: string; 14 | } 15 | 16 | const LinkItem: React.FC= ({ link, avatar, name, descr }) => { 17 | const { imgRef, imgUrl } = useLazyImg(avatar!, smallLoadingUrl); 18 | 19 | return ( 20 | 37 | ); 38 | }; 39 | 40 | export default LinkItem; 41 | -------------------------------------------------------------------------------- /src/pages/Link/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/base.scss'; 2 | @import '../../styles/style.scss'; 3 | 4 | .box { 5 | display: flex; 6 | flex-wrap: wrap; 7 | justify-content: space-between; 8 | } 9 | -------------------------------------------------------------------------------- /src/pages/Link/index.tsx: -------------------------------------------------------------------------------- 1 | import { useRequest } from 'ahooks'; 2 | import React from 'react'; 3 | 4 | import Layout from '@/components/Layout'; 5 | import { DB } from '@/utils/apis/dbConfig'; 6 | import { getData } from '@/utils/apis/getData'; 7 | import { staleTime } from '@/utils/constant'; 8 | import { shuffleArray } from '@/utils/function'; 9 | 10 | import { Title } from '../titleConfig'; 11 | import s from './index.scss'; 12 | import LinkItem from './LinkItem'; 13 | 14 | interface linkType { 15 | _id: string; 16 | link: string; 17 | avatar: string; 18 | name: string; 19 | descr: string; 20 | } 21 | 22 | const Link: React.FC = () => { 23 | const { data, loading } = useRequest(getData, { 24 | defaultParams: [DB.Link], 25 | retryCount: 3, 26 | cacheKey: `Link-${DB.Link}`, 27 | staleTime 28 | }); 29 | 30 | return ( 31 | 32 | {shuffleArray(data?.data).map((item: linkType) => ( 33 | 42 | ); 43 | }; 44 | 45 | export default Link; 46 | -------------------------------------------------------------------------------- /src/pages/Log/TimeItem/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/base.scss'; 2 | @import '../../../styles/style.scss'; 3 | 4 | .item { 5 | position: relative; 6 | border-left: 6px solid $themeColor1; 7 | 8 | .time { 9 | position: relative; 10 | height: 58px; 11 | width: 190px; 12 | display: flex; 13 | justify-content: flex-start; 14 | align-items: center; 15 | padding-left: $space; 16 | font-size: 28px; 17 | user-select: none; 18 | color: $textColor; 19 | @extend .trans; 20 | 21 | .dot { 22 | width: 30px; 23 | height: 30px; 24 | border-radius: 50%; 25 | position: absolute; 26 | left: -18px; 27 | top: 50%; 28 | transform: translate(0, -50%); 29 | background-color: $themeColor1; 30 | display: flex; 31 | justify-content: center; 32 | align-items: center; 33 | 34 | .dotIn { 35 | width: 14px; 36 | height: 14px; 37 | background-color: $hoverColor; 38 | border-radius: 50%; 39 | @extend .trans; 40 | } 41 | } 42 | } 43 | .time:hover .dotIn { 44 | width: 22px; 45 | height: 22px; 46 | } 47 | .content { 48 | list-style: none; 49 | margin: 0; 50 | padding: 0; 51 | margin: 0 16px; 52 | border-radius: 14px; 53 | padding: 10px $space; 54 | background-color: $themeColor1; 55 | @extend .trans; 56 | 57 | .timeLi { 58 | margin-bottom: 6px; 59 | } 60 | .timeLi:last-child { 61 | margin-bottom: 0; 62 | } 63 | } 64 | .content:hover { 65 | transform: scale(1.02); 66 | } 67 | } 68 | 69 | @media screen and (max-width: 1240px) { 70 | .item { 71 | border-left: 5rem solid $themeColor1; 72 | 73 | .time { 74 | height: 40rem; 75 | width: 140rem; 76 | padding-left: 16rem; 77 | font-size: 20rem; 78 | 79 | .dot { 80 | width: 24rem; 81 | height: 24rem; 82 | left: -14.5rem; 83 | 84 | .dotIn { 85 | width: 10rem; 86 | height: 10rem; 87 | } 88 | } 89 | } 90 | .time:hover .dotIn { 91 | width: 16rem; 92 | height: 16rem; 93 | } 94 | .content { 95 | margin: 0 10rem; 96 | border-radius: 12rem; 97 | padding: 6rem 12rem; 98 | font-size: 16rem; 99 | 100 | .timeLi { 101 | margin-bottom: 2rem; 102 | } 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/pages/Log/TimeItem/index.tsx: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs'; 2 | import React from 'react'; 3 | 4 | import s from './index.scss'; 5 | 6 | interface Props { 7 | date: number; 8 | logContent: string[]; 9 | } 10 | 11 | const TimeItem: React.FC40 | ))} 41 | = ({ date, logContent }) => { 12 | return ( 13 | 14 |29 | ); 30 | }; 31 | 32 | export default TimeItem; 33 | -------------------------------------------------------------------------------- /src/pages/Log/index.tsx: -------------------------------------------------------------------------------- 1 | import { useRequest } from 'ahooks'; 2 | import React from 'react'; 3 | 4 | import Layout from '@/components/Layout'; 5 | import { DB } from '@/utils/apis/dbConfig'; 6 | import { getOrderData } from '@/utils/apis/getOrderData'; 7 | import { staleTime } from '@/utils/constant'; 8 | 9 | import { Title } from '../titleConfig'; 10 | import TimeItem from './TimeItem'; 11 | 12 | interface Log { 13 | _id: string; 14 | date: number; 15 | logContent: string[]; 16 | } 17 | 18 | const Log: React.FC = () => { 19 | const { data, loading } = useRequest(getOrderData, { 20 | defaultParams: [{ dbName: DB.Log, sortKey: 'date' }], 21 | retryCount: 3, 22 | cacheKey: `Log-${DB.Log}`, 23 | staleTime 24 | }); 25 | 26 | return ( 27 |15 |20 | 21 |16 | 17 |18 | {dayjs(date).format('YYYY-MM-DD')} 19 |22 | {logContent.map((log, index) => ( 23 |
28 |- 24 | {log} 25 |
26 | ))} 27 |28 | {data?.data.map(({ _id, date, logContent }: Log) => ( 29 | 32 | ); 33 | }; 34 | 35 | export default Log; 36 | -------------------------------------------------------------------------------- /src/pages/Msg/MsgInfo/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/base.scss'; 2 | 3 | .info { 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: center; 7 | align-items: center; 8 | font-size: 30px; 9 | font-weight: 700; 10 | 11 | div { 12 | margin-bottom: 10px; 13 | } 14 | } 15 | 16 | .siteLink { 17 | .link { 18 | font-size: 26px; 19 | font-weight: 700; 20 | color: $textColor2; 21 | } 22 | 23 | .value { 24 | padding: 0 6px; 25 | margin-left: 6px; 26 | } 27 | } 28 | 29 | .hoverName { 30 | color: $hoverColor; 31 | } 32 | 33 | @media screen and (max-width: 1240px) { 34 | .info { 35 | font-size: 22rem; 36 | 37 | div { 38 | margin-bottom: 3rem; 39 | } 40 | } 41 | 42 | .siteLink { 43 | display: none; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/pages/Msg/MsgInfo/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { useTime } from '@/utils/hooks/useTime'; 4 | 5 | import s from './index.scss'; 6 | import { useSite } from './useSite'; 7 | 8 | const MsgInfo: React.FC = () => { 9 | const { timeText } = useTime(); 10 | const { mySite } = useSite(); 11 | 12 | return ( 13 | <> 14 |30 | ))} 31 | 15 |22 |16 | {timeText},我叫飞鸟, 17 |18 |欢迎来到我的博客!19 |可以在这里留言、吐槽,20 |交换友链。21 |23 |39 | > 40 | ); 41 | }; 42 | 43 | export default MsgInfo; 44 | -------------------------------------------------------------------------------- /src/pages/Msg/MsgInfo/useSite.ts: -------------------------------------------------------------------------------- 1 | import { myAvatar, myDescr, myLink, myName } from '@/utils/constant'; 2 | 3 | export const useSite = () => { 4 | const mySite = [ 5 | { 6 | key: 'name', 7 | value: myName 8 | }, 9 | { 10 | key: 'link', 11 | value: myLink 12 | }, 13 | { 14 | key: 'avatar', 15 | value: myAvatar 16 | }, 17 | { 18 | key: 'descr', 19 | value: myDescr 20 | } 21 | ]; 22 | 23 | return { mySite }; 24 | }; 25 | -------------------------------------------------------------------------------- /src/pages/Msg/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Comment from '@/components/Comment'; 4 | import Layout from '@/components/Layout'; 5 | 6 | import { Title } from '../titleConfig'; 7 | import MsgInfo from './MsgInfo'; 8 | 9 | const Msg: React.FC = () => { 10 | return ( 11 |本站链接:24 | {mySite.map( 25 | ( 26 | item: { 27 | key: string; 28 | value: string; 29 | }, 30 | index 31 | ) => ( 32 |33 | {item.key}: 34 | {item.value} 35 |36 | ) 37 | )} 38 |12 | 15 | ); 16 | }; 17 | 18 | export default Msg; 19 | -------------------------------------------------------------------------------- /src/pages/Post/CopyRight/CopyIcon/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface Props { 4 | className?: string; 5 | } 6 | 7 | const CopyIcon: React.FC13 | 14 | = ({ className }) => ( 8 | 18 | ); 19 | 20 | export default CopyIcon; 21 | -------------------------------------------------------------------------------- /src/pages/Post/CopyRight/CopyrightIcon/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface Props { 4 | className?: string; 5 | } 6 | 7 | const CopyrightIcon: React.FC = ({ className }) => ( 8 | 14 | ); 15 | 16 | export default CopyrightIcon; 17 | -------------------------------------------------------------------------------- /src/pages/Post/CopyRight/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/base.scss'; 2 | @import '../../../styles/style.scss'; 3 | 4 | .copyrightBox { 5 | background-color: $themeColor1; 6 | position: relative; 7 | border-radius: 20px; 8 | padding: 14px; 9 | overflow: hidden; 10 | color: $textColor2; 11 | 12 | .copyrightIcon { 13 | position: absolute; 14 | right: 40px; 15 | width: 128px; 16 | height: 128px; 17 | top: 50%; 18 | transform: translate(0, -50%); 19 | 20 | path { 21 | fill: $textColor2; 22 | } 23 | } 24 | 25 | .title { 26 | font-size: 20px; 27 | font-weight: 700; 28 | } 29 | 30 | .urlBox { 31 | font-size: 16px; 32 | display: flex; 33 | 34 | .url { 35 | height: 24px; 36 | line-height: 24px; 37 | display: flex; 38 | justify-content: flex-start; 39 | align-items: center; 40 | margin-right: 6px; 41 | } 42 | 43 | .copyBtn { 44 | width: 24px; 45 | height: 24px; 46 | border-radius: 8px; 47 | display: flex; 48 | justify-content: center; 49 | align-items: center; 50 | 51 | cursor: pointer; 52 | @extend .hover; 53 | 54 | .copyIcon { 55 | width: 16px; 56 | height: 16px; 57 | 58 | path { 59 | fill: $textColor2; 60 | @extend .trans; 61 | } 62 | } 63 | } 64 | } 65 | 66 | .text { 67 | font-size: 16px; 68 | 69 | .copyrightName { 70 | color: $textColor2; 71 | display: inline-block; 72 | padding: 0 4px; 73 | height: 24px; 74 | font-weight: 700; 75 | line-height: 24px; 76 | border-radius: 8px; 77 | @extend .hover; 78 | } 79 | } 80 | } 81 | 82 | @media screen and (max-width: 1240px) { 83 | .copyrightBox { 84 | border-radius: 12rem; 85 | padding: 4rem 8rem; 86 | 87 | .copyrightIcon { 88 | display: none; 89 | } 90 | 91 | .title { 92 | font-size: 16rem; 93 | } 94 | 95 | .urlBox { 96 | font-size: 16rem; 97 | 98 | .url { 99 | height: 24rem; 100 | line-height: 24rem; 101 | margin-right: 3rem; 102 | } 103 | 104 | .copyBtn { 105 | width: 24rem; 106 | height: 24rem; 107 | border-radius: 6rem; 108 | 109 | .copyIcon { 110 | width: 16rem; 111 | height: 16rem; 112 | } 113 | } 114 | } 115 | 116 | .text { 117 | font-size: 14rem; 118 | 119 | .copyrightName { 120 | padding: 0 4rem; 121 | height: 16rem; 122 | line-height: 16rem; 123 | border-radius: 4rem; 124 | font-weight: 400; 125 | } 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/pages/Post/CopyRight/index.tsx: -------------------------------------------------------------------------------- 1 | import { message } from 'antd'; 2 | import copy from 'copy-to-clipboard'; 3 | import React from 'react'; 4 | 5 | import { myLink, siteTitle } from '@/utils/constant'; 6 | 7 | import CopyIcon from './CopyIcon'; 8 | import CopyrightIcon from './CopyrightIcon'; 9 | import s from './index.scss'; 10 | 11 | interface Props { 12 | titleEng?: string; 13 | title?: string; 14 | } 15 | 16 | const CopyRight: React.FC = ({ titleEng, title }) => { 17 | const url = `${myLink}/post?title=${titleEng}`; 18 | 19 | const copyUrl = () => { 20 | if (copy(url)) { 21 | message.success('复制成功!'); 22 | } 23 | }; 24 | 25 | return ( 26 | 27 |52 | ); 53 | }; 54 | 55 | export default CopyRight; 56 | -------------------------------------------------------------------------------- /src/pages/Post/Navbar/index.custom.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/base.scss'; 2 | @import '../../../styles/style.scss'; 3 | 4 | $title-level-margin: 18px; 5 | 6 | .markdown-navigation.postNavBar { 7 | position: fixed; 8 | top: 50%; 9 | transform: translate(0, -50%); 10 | right: 10px; 11 | width: 300px; 12 | user-select: none; 13 | padding: 10px; 14 | max-height: 700px; 15 | overflow-y: auto; 16 | margin: 10px; 17 | 18 | .title-anchor { 19 | padding: 4px 20px; 20 | display: flex; 21 | justify-content: flex-start; 22 | align-items: center; 23 | margin-bottom: 10px; 24 | font-size: 18px; 25 | word-break: break-all; 26 | background-color: $themeColor1; 27 | border-radius: 8px; 28 | @extend .hover; 29 | } 30 | .title-anchor:last-child { 31 | margin-bottom: 0; 32 | } 33 | 34 | .title-level3 { 35 | margin-left: $title-level-margin * 1; 36 | } 37 | 38 | .title-level4 { 39 | margin-left: $title-level-margin * 2; 40 | } 41 | 42 | .title-level5 { 43 | margin-left: $title-level-margin * 3; 44 | } 45 | 46 | .active { 47 | background-color: $hoverColor; 48 | } 49 | } 50 | 51 | /* 设置滚动条的样式 */ 52 | .markdown-navigation.postNavBar::-webkit-scrollbar { 53 | width: 12px; 54 | } 55 | /* 滚动槽 */ 56 | .markdown-navigation.postNavBar::-webkit-scrollbar-track { 57 | background-color: rgba(0, 0, 0, 0); 58 | } 59 | /* 滚动条滑块 */ 60 | .markdown-navigation.postNavBar::-webkit-scrollbar-thumb { 61 | /* 副色2 */ 62 | background-color: $themeColor2; 63 | border-radius: 6px; 64 | } 65 | .markdown-navigation.postNavBar::-webkit-scrollbar-thumb:hover { 66 | background-color: $hoverColor; 67 | } 68 | 69 | .ant-drawer-header-close-only { 70 | display: none; 71 | } 72 | 73 | .ant-drawer-content { 74 | background-color: $themeColor; 75 | color: $textColor; 76 | font-family: 'dengxian'; 77 | font-size: 18px; 78 | } 79 | 80 | @media screen and (max-width: 1240px) { 81 | // 抽屉 82 | .mobile-navBar-box { 83 | .ant-drawer-content-wrapper { 84 | width: 200rem !important; 85 | 86 | .ant-drawer-body { 87 | padding: 0; 88 | } 89 | } 90 | } 91 | 92 | // 目录 93 | .markdown-navigation.postNavBar { 94 | right: 10rem; 95 | width: 180rem; 96 | padding: 0; 97 | max-height: 80vh; 98 | margin: 0; 99 | 100 | .title-anchor { 101 | padding: 2rem 8rem; 102 | margin-bottom: 10rem; 103 | font-size: 16rem; 104 | border-radius: 8rem; 105 | } 106 | 107 | .title-level3 { 108 | margin-left: 6rem * 1; 109 | } 110 | 111 | .title-level4 { 112 | margin-left: 6rem * 2; 113 | } 114 | 115 | .title-level5 { 116 | margin-left: 6rem * 3; 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/pages/Post/Navbar/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/base.scss'; 2 | @import '../../../styles/style.scss'; 3 | 4 | $hoverBarSize: 50px; 5 | 6 | .hoverBar { 7 | position: fixed; 8 | top: 50%; 9 | right: 20px; 10 | z-index: 99; 11 | transform: translate(0, -50%); 12 | background-color: $themeColor1; 13 | font-size: 24px; 14 | width: $hoverBarSize; 15 | height: $hoverBarSize; 16 | border-radius: 14px; 17 | display: none; 18 | justify-content: center; 19 | align-items: center; 20 | user-select: none; 21 | @extend .trans; 22 | } 23 | 24 | .hoverBar:hover { 25 | background-color: $hoverColor !important; 26 | } 27 | 28 | .drawer { 29 | display: none; 30 | } 31 | 32 | @media screen and (max-width: 1540px) { 33 | .hoverBar { 34 | display: flex; 35 | } 36 | 37 | .navBar { 38 | display: none; 39 | } 40 | 41 | .drawer { 42 | display: block; 43 | } 44 | } 45 | 46 | @media screen and (max-width: 1240px) { 47 | .hoverBar { 48 | width: 50rem; 49 | height: 50rem; 50 | display: flex; 51 | justify-content: center; 52 | align-items: center; 53 | font-size: 22rem; 54 | color: $textColor; 55 | position: fixed; 56 | top: 100rem; 57 | right: 0; 58 | background-color: transparent; 59 | } 60 | 61 | .hoverBar:hover { 62 | background-color: transparent !important; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/pages/Post/Navbar/index.tsx: -------------------------------------------------------------------------------- 1 | import './index.custom.scss'; 2 | 3 | import { MenuFoldOutlined } from '@ant-design/icons'; 4 | import { useBoolean } from 'ahooks'; 5 | import { Drawer } from 'antd'; 6 | import classNames from 'classnames'; 7 | import MarkNav from 'markdown-navbar'; 8 | import React from 'react'; 9 | import { connect } from 'react-redux'; 10 | 11 | import { setNavShow } from '@/redux/actions'; 12 | 13 | import s from './index.scss'; 14 | 15 | interface Props { 16 | content?: string; 17 | setNavShow?: Function; 18 | } 19 | 20 | const Navbar: React.FC28 | {title}29 |30 |35 |{url}31 |32 |34 |33 | 36 | 本站所有文章除特别声明外,均采用 37 | 43 | CC BY-NC-SA 4.0 44 | 45 | 许可协议,转载请注明来自 46 | 47 | {siteTitle} 48 | 49 | 。 50 |51 |= ({ content, setNavShow }) => { 21 | const [visible, { setTrue: openDrawer, setFalse: closeDrawer }] = useBoolean(false); 22 | 23 | return ( 24 | <> 25 | {/* 正常的目录 */} 26 | setNavShow?.(false)} 33 | /> 34 | {/* 中屏显示的按钮 */} 35 | 36 |38 | {/* 中屏抽屉 */} 39 |37 | 46 | 55 | > 56 | ); 57 | }; 58 | 59 | export default connect(() => ({}), { 60 | setNavShow 61 | })(Navbar); 62 | -------------------------------------------------------------------------------- /src/pages/Post/PostTags/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/base.scss'; 2 | @import '../../../styles/style.scss'; 3 | 4 | .articleTags { 5 | margin-bottom: calc($space / 2); 6 | 7 | .articleTag { 8 | display: inline-block; 9 | margin-right: 10px; 10 | height: 36px; 11 | line-height: 36px; 12 | padding: 0 10px; 13 | border-radius: $midRadius; 14 | user-select: none; 15 | font-size: 16px; 16 | background-color: $themeColor1; 17 | @extend .hover; 18 | } 19 | } 20 | 21 | @media screen and (max-width: 1240px) { 22 | .articleTags { 23 | margin-bottom: 7rem; 24 | 25 | .articleTag { 26 | margin-right: 6rem; 27 | height: 28rem; 28 | line-height: 28rem; 29 | padding: 0 8rem; 30 | border-radius: 8rem; 31 | font-size: 14rem; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/pages/Post/PostTags/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import s from './index.scss'; 4 | 5 | interface Props { 6 | tags: string[]; 7 | } 8 | 9 | const PostTags: React.FCsetNavShow?.(true)} 53 | /> 54 | = ({ tags }) => { 10 | return ( 11 | 12 | {tags.map((item, index) => ( 13 | 14 | {item} 15 | 16 | ))} 17 |18 | ); 19 | }; 20 | 21 | export default PostTags; 22 | -------------------------------------------------------------------------------- /src/pages/Post/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/base.scss'; 2 | 3 | .mb { 4 | margin-bottom: 3 * $space; 5 | } 6 | 7 | @media screen and (max-width: 1540px) { 8 | .navBar { 9 | display: none; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/pages/Post/index.tsx: -------------------------------------------------------------------------------- 1 | import useUrlState from '@ahooksjs/use-url-state'; 2 | import { useRequest } from 'ahooks'; 3 | import React from 'react'; 4 | 5 | import Comment from '@/components/Comment'; 6 | import Layout from '@/components/Layout'; 7 | import MarkDown from '@/components/MarkDown'; 8 | import { DB } from '@/utils/apis/dbConfig'; 9 | import { getWhereData } from '@/utils/apis/getWhereData'; 10 | import { _ } from '@/utils/cloudBase'; 11 | import { staleTime } from '@/utils/constant'; 12 | 13 | import CopyRight from './CopyRight'; 14 | import s from './index.scss'; 15 | import Navbar from './Navbar'; 16 | import PostTags from './PostTags'; 17 | 18 | const Post: React.FC = () => { 19 | const [search] = useUrlState(); 20 | 21 | const { data, loading } = useRequest(getWhereData, { 22 | defaultParams: [DB.Article, { titleEng: _.eq(search.title), post: _.eq(true) }], 23 | retryCount: 3, 24 | cacheKey: `Post-${DB.Article}-${search.title}`, 25 | staleTime 26 | }); 27 | 28 | return ( 29 |37 | 43 | ); 44 | }; 45 | 46 | export default Post; 47 | -------------------------------------------------------------------------------- /src/pages/Say/SayPop/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/base.scss'; 2 | @import '../../../styles/style.scss'; 3 | 4 | .sayItem { 5 | display: flex; 6 | margin-bottom: $space; 7 | 8 | .avatarBox { 9 | width: 70px; 10 | height: 70px; 11 | 12 | .avatar { 13 | width: 70px; 14 | height: 70px; 15 | border-radius: 14px; 16 | -webkit-user-drag: none; 17 | } 18 | } 19 | 20 | .contentBox { 21 | margin-left: 20px; 22 | flex: 1; 23 | 24 | .content { 25 | border-radius: 14px; 26 | word-break: break-all; 27 | padding: 20px 20px 44px; 28 | position: relative; 29 | background-color: $themeColor1; 30 | user-select: text; 31 | @extend .trans; 32 | 33 | .sayImgsBox { 34 | margin: 5px 0; 35 | height: 120px; 36 | 37 | .sayImg { 38 | display: inline-block; 39 | height: 100%; 40 | width: calc((100% - 3 * 6px) / 4); 41 | margin-right: 6px; 42 | border-radius: 6px; 43 | overflow: hidden; 44 | position: relative; 45 | cursor: pointer; 46 | @extend .trans; 47 | 48 | img { 49 | height: 100%; 50 | position: absolute; 51 | left: 50%; 52 | transform: translate(-50%, 0); 53 | } 54 | } 55 | 56 | .sayImg:last-child { 57 | margin-right: 0; 58 | } 59 | 60 | .sayImg:hover { 61 | transform: scale(1.03); 62 | } 63 | } 64 | } 65 | .content:hover { 66 | transform: scale(1.02); 67 | } 68 | 69 | .date { 70 | padding: 0 10px; 71 | border-radius: 10px; 72 | color: $textColor; 73 | height: 30px; 74 | position: absolute; 75 | right: 10px; 76 | bottom: 10px; 77 | display: flex; 78 | justify-content: center; 79 | align-items: center; 80 | font-size: 18px; 81 | user-select: none; 82 | background-color: $themeColor2; 83 | @extend .hover; 84 | } 85 | } 86 | } 87 | 88 | .sayItem:last-child { 89 | margin-bottom: 0; 90 | } 91 | 92 | @media screen and (max-width: 1240px) { 93 | .sayItem { 94 | margin-bottom: 12rem; 95 | 96 | .avatarBox { 97 | width: 45rem; 98 | height: 45rem; 99 | 100 | .avatar { 101 | width: 45rem; 102 | height: 45rem; 103 | border-radius: 8rem; 104 | } 105 | } 106 | 107 | .contentBox { 108 | margin-left: 5rem; 109 | 110 | .content { 111 | border-radius: 10rem; 112 | padding: 10rem 10rem 32rem; 113 | font-size: 16rem; 114 | 115 | .sayImgsBox { 116 | margin: 2rem 0; 117 | height: 70rem; 118 | 119 | .sayImg { 120 | width: calc((100% - 3 * 6rem) / 4); 121 | margin-right: 6rem; 122 | border-radius: 6rem; 123 | } 124 | } 125 | } 126 | 127 | .date { 128 | padding: 0 6rem; 129 | border-radius: 6rem; 130 | height: 24rem; 131 | right: 5rem; 132 | bottom: 5rem; 133 | font-size: 14rem; 134 | } 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/pages/Say/SayPop/index.tsx: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs'; 2 | import React from 'react'; 3 | 4 | import { myAvatar70 } from '@/utils/constant'; 5 | 6 | import s from './index.scss'; 7 | 8 | interface Props { 9 | content: string; 10 | date: number; 11 | imgs: string[]; 12 | handlePreView: (url: string) => void; 13 | } 14 | 15 | const SayPop: React.FC38 | 39 | 40 | 41 | 42 | = ({ content, date, imgs, handlePreView }) => ( 16 | 17 |37 | ); 38 | 39 | export default SayPop; 40 | -------------------------------------------------------------------------------- /src/pages/Say/index.tsx: -------------------------------------------------------------------------------- 1 | import { useRequest } from 'ahooks'; 2 | import React, { useState } from 'react'; 3 | 4 | import ImgView from '@/components/ImgView'; 5 | import Layout from '@/components/Layout'; 6 | import { DB } from '@/utils/apis/dbConfig'; 7 | import { getOrderData } from '@/utils/apis/getOrderData'; 8 | import { staleTime } from '@/utils/constant'; 9 | 10 | import { Title } from '../titleConfig'; 11 | import SayPop from './SayPop'; 12 | 13 | interface SayType { 14 | _id: string; 15 | content: string; 16 | date: number; 17 | imgs: string[]; 18 | } 19 | 20 | const Say: React.FC = () => { 21 | const { data, loading } = useRequest(getOrderData, { 22 | defaultParams: [{ dbName: DB.Say, sortKey: 'date' }], 23 | retryCount: 3, 24 | cacheKey: `Say-${DB.Say}`, 25 | staleTime 26 | }); 27 | 28 | const [url, setUrl] = useState(''); 29 | const [showPreView, setShowPreView] = useState(false); 30 | 31 | const handlePreView = (url: string) => { 32 | setShowPreView(true); 33 | setUrl(url); 34 | }; 35 | 36 | return ( 37 |18 |20 | 21 |19 |
22 |36 |23 | {content} 24 | {dayjs(date).format('YYYY-MM-DD HH:mm:ss')} 25 | {!!imgs?.length && ( 26 |35 |27 | {imgs.map((img, index) => ( 28 |33 | )} 34 |handlePreView(img)}> 29 |31 | ))} 32 |30 |
38 | {data?.data.map(({ _id, content, date, imgs }: SayType) => ( 39 | 54 | ); 55 | }; 56 | 57 | export default Say; 58 | -------------------------------------------------------------------------------- /src/pages/Show/ShowItem/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/base.scss'; 2 | @import '../../../styles/style.scss'; 3 | 4 | .showItem { 5 | overflow: hidden; 6 | position: relative; 7 | margin-bottom: $space + 10px; 8 | width: 47.83%; 9 | height: 330px; 10 | color: #fff; 11 | background-size: cover; 12 | border-radius: 20px; 13 | user-select: none; 14 | background-position: center; 15 | @extend .trans; 16 | cursor: pointer; 17 | 18 | .link { 19 | color: #fff; 20 | } 21 | 22 | .link:hover { 23 | color: #fff; 24 | } 25 | 26 | .title { 27 | position: relative; 28 | padding-top: 46px; 29 | font-size: 36px; 30 | font-weight: 700; 31 | margin: 0 auto; 32 | height: 90px; 33 | width: 75%; 34 | @extend .trans; 35 | z-index: 2; 36 | border-bottom: 4px solid transparent; 37 | } 38 | 39 | .descr { 40 | position: relative; 41 | z-index: 2; 42 | padding-top: 10px; 43 | width: 75%; 44 | margin: 0 auto; 45 | font-size: 24px; 46 | @extend .trans; 47 | word-wrap: break-word; 48 | opacity: 0; 49 | line-height: 1.5; 50 | } 51 | 52 | .mask { 53 | position: absolute; 54 | width: 100%; 55 | height: 100%; 56 | top: 0; 57 | @extend .trans; 58 | background-color: rgba(0, 0, 0, 0.2); 59 | } 60 | } 61 | .showItem:hover { 62 | transform: scale(1.04); 63 | } 64 | .showItem:hover .title { 65 | border-bottom: 4px solid #fff; 66 | } 67 | .showItem:hover .descr { 68 | opacity: 1; 69 | } 70 | .showItem:hover .mask { 71 | background-color: rgba(0, 0, 0, 0.6); 72 | } 73 | 74 | .showItem:nth-last-child(1), 75 | .showItem:nth-last-child(2) { 76 | margin-bottom: 0; 77 | } 78 | 79 | @media screen and (max-width: 1240px) { 80 | .showItem { 81 | margin-bottom: 10rem; 82 | width: 48.81%; 83 | height: 160rem; 84 | border-radius: 10rem; 85 | 86 | .title { 87 | padding-top: 20rem; 88 | font-size: 18rem; 89 | height: 48rem; 90 | border-bottom: 4px solid transparent; 91 | } 92 | 93 | .descr { 94 | padding-top: 4rem; 95 | width: 75%; 96 | font-size: 13rem; 97 | } 98 | } 99 | 100 | .showItem:hover .title { 101 | border-bottom: 2rem solid #fff; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/pages/Show/ShowItem/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import s from './index.scss'; 4 | 5 | interface Props { 6 | cover?: string; 7 | link?: string; 8 | name?: string; 9 | descr?: string; 10 | } 11 | 12 | const ShowItem: React.FC46 | ))} 47 | 48 | setShowPreView(false)} 52 | /> 53 | = ({ cover, link, name, descr }) => { 13 | return ( 14 | 23 | ); 24 | }; 25 | 26 | export default ShowItem; 27 | -------------------------------------------------------------------------------- /src/pages/Show/index.scss: -------------------------------------------------------------------------------- 1 | // @import '../../styles/style.scss'; 2 | // @import '../../styles/base.scss'; 3 | 4 | // .imgBox { 5 | 6 | // } 7 | 8 | .showBox { 9 | display: flex; 10 | flex-wrap: wrap; 11 | justify-content: space-between; 12 | } 13 | -------------------------------------------------------------------------------- /src/pages/Show/index.tsx: -------------------------------------------------------------------------------- 1 | import { useRequest } from 'ahooks'; 2 | import React from 'react'; 3 | 4 | import Layout from '@/components/Layout'; 5 | import { DB } from '@/utils/apis/dbConfig'; 6 | import { getOrderData } from '@/utils/apis/getOrderData'; 7 | import { staleTime } from '@/utils/constant'; 8 | 9 | import { Title } from '../titleConfig'; 10 | import s from './index.scss'; 11 | import ShowItem from './ShowItem'; 12 | 13 | interface ShowType { 14 | _id: string; 15 | cover: string; 16 | link: string; 17 | name: string; 18 | descr: string; 19 | } 20 | 21 | const Show: React.FC = () => { 22 | const { data, loading } = useRequest(getOrderData, { 23 | defaultParams: [ 24 | { 25 | dbName: DB.Show, 26 | sortKey: 'order', 27 | isAsc: true 28 | } 29 | ], 30 | retryCount: 3, 31 | cacheKey: `Show-${DB.Show}`, 32 | staleTime 33 | }); 34 | 35 | return ( 36 | 37 | {data?.data.map((item: ShowType) => ( 38 | 47 | ); 48 | }; 49 | 50 | export default Show; 51 | -------------------------------------------------------------------------------- /src/pages/Tags/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/style.scss'; 2 | 3 | .tagsBox { 4 | display: flex; 5 | justify-content: center; 6 | align-items: center; 7 | flex-wrap: wrap; 8 | 9 | .tagItem { 10 | @extend .tagBase; 11 | font-size: 22px; 12 | margin: 6px; 13 | } 14 | } 15 | 16 | @media screen and (max-width: 1240px) { 17 | .tagsBox { 18 | .tagItem { 19 | font-size: 18rem; 20 | margin: 6rem; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/pages/Tags/index.tsx: -------------------------------------------------------------------------------- 1 | import { useRequest } from 'ahooks'; 2 | import React from 'react'; 3 | import { useNavigate } from 'react-router-dom'; 4 | 5 | import Layout from '@/components/Layout'; 6 | import { DB } from '@/utils/apis/dbConfig'; 7 | import { getData } from '@/utils/apis/getData'; 8 | import { staleTime } from '@/utils/constant'; 9 | 10 | import { Title } from '../titleConfig'; 11 | import s from './index.scss'; 12 | 13 | interface TagType { 14 | _id: string; 15 | _openid: string; 16 | tag: string; 17 | } 18 | 19 | const Tags: React.FC = () => { 20 | const navigate = useNavigate(); 21 | 22 | const { data, loading } = useRequest(getData, { 23 | defaultParams: [DB.Tag], 24 | retryCount: 3, 25 | cacheKey: `Tags-${DB.Tag}`, 26 | staleTime 27 | }); 28 | 29 | return ( 30 |45 | ))} 46 | 31 | {data?.data.map((item: TagType) => ( 32 | navigate(`/artDetail?tag=${encodeURIComponent(item.tag)}`)} 36 | > 37 | {item.tag} 38 | 39 | ))} 40 | 41 | ); 42 | }; 43 | 44 | export default Tags; 45 | -------------------------------------------------------------------------------- /src/pages/constant.ts: -------------------------------------------------------------------------------- 1 | export interface ArticleType { 2 | _id: string; 3 | title: string; 4 | date: number; 5 | titleEng: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/pages/titleConfig.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | export enum Title { 3 | Articles = '所有文章', 4 | Classes = '分类', 5 | Tags = '标签', 6 | Say = '自言自语', 7 | Msg = '留言板', 8 | Link = '友情链接', 9 | Show = '小作品', 10 | Log = '建站日志', 11 | About = '关于' 12 | } 13 | -------------------------------------------------------------------------------- /src/redux/actions.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SET_ART_SUM, 3 | SET_AVATAR, 4 | SET_EMAIL, 5 | SET_LINK, 6 | SET_MODE, 7 | SET_NAME, 8 | SET_NAV_SHOW 9 | } from './constant'; 10 | 11 | export const setNavShow = (data: boolean) => ({ 12 | type: SET_NAV_SHOW, 13 | data 14 | }); 15 | 16 | export const setArtSum = (data: number) => ({ 17 | type: SET_ART_SUM, 18 | data 19 | }); 20 | 21 | export const setName = (data: string) => ({ 22 | type: SET_NAME, 23 | data 24 | }); 25 | export const setEmail = (data: string) => ({ 26 | type: SET_EMAIL, 27 | data 28 | }); 29 | export const setLink = (data: string) => ({ 30 | type: SET_LINK, 31 | data 32 | }); 33 | export const setAvatar = (data: string) => ({ 34 | type: SET_AVATAR, 35 | data 36 | }); 37 | 38 | export const setMode = (data: number) => ({ 39 | type: SET_MODE, 40 | data 41 | }); 42 | -------------------------------------------------------------------------------- /src/redux/constant.ts: -------------------------------------------------------------------------------- 1 | export const SET_NAV_SHOW = 'setNavShow'; 2 | 3 | export const SET_ART_SUM = 'setArtSum'; 4 | 5 | export const SET_NAME = 'setName'; 6 | export const SET_EMAIL = 'setEmail'; 7 | export const SET_LINK = 'setLink'; 8 | export const SET_AVATAR = 'setAvatar'; 9 | 10 | export const SET_MODE = 'setMode'; 11 | -------------------------------------------------------------------------------- /src/redux/interface.ts: -------------------------------------------------------------------------------- 1 | export interface storeState { 2 | navShow: boolean; 3 | artSum: number; 4 | name: string; 5 | link: string; 6 | email: string; 7 | avatar: string; 8 | mode: number; 9 | } 10 | -------------------------------------------------------------------------------- /src/redux/reducers/artSum.ts: -------------------------------------------------------------------------------- 1 | import { SET_ART_SUM } from '../constant'; 2 | 3 | interface Action { 4 | type: string; 5 | data: number; 6 | } 7 | 8 | const initState = 0; 9 | 10 | export default function addReducer(preState = initState, action: Action) { 11 | const { type, data } = action; 12 | switch (type) { 13 | case SET_ART_SUM: 14 | return data; 15 | default: 16 | return preState; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/redux/reducers/avatar.ts: -------------------------------------------------------------------------------- 1 | import { SET_AVATAR } from '../constant'; 2 | 3 | interface Action { 4 | type: string; 5 | data: string; 6 | } 7 | 8 | const initState = ''; 9 | 10 | export default function addReducer(preState = initState, action: Action) { 11 | const { type, data } = action; 12 | switch (type) { 13 | case SET_AVATAR: 14 | return data; 15 | default: 16 | return preState; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/redux/reducers/email.ts: -------------------------------------------------------------------------------- 1 | import { SET_EMAIL } from '../constant'; 2 | 3 | interface Action { 4 | type: string; 5 | data: string; 6 | } 7 | 8 | const initState = ''; 9 | 10 | export default function addReducer(preState = initState, action: Action) { 11 | const { type, data } = action; 12 | switch (type) { 13 | case SET_EMAIL: 14 | return data; 15 | default: 16 | return preState; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/redux/reducers/index.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | 3 | import artSum from './artSum'; 4 | import avatar from './avatar'; 5 | import email from './email'; 6 | import link from './link'; 7 | import mode from './mode'; 8 | import name from './name'; 9 | import navShow from './navShow'; 10 | 11 | export default combineReducers({ 12 | navShow, 13 | artSum, 14 | avatar, 15 | email, 16 | link, 17 | name, 18 | mode 19 | }); 20 | -------------------------------------------------------------------------------- /src/redux/reducers/link.ts: -------------------------------------------------------------------------------- 1 | import { SET_LINK } from '../constant'; 2 | 3 | interface Action { 4 | type: string; 5 | data: string; 6 | } 7 | 8 | const initState = ''; 9 | 10 | export default function addReducer(preState = initState, action: Action) { 11 | const { type, data } = action; 12 | switch (type) { 13 | case SET_LINK: 14 | return data; 15 | default: 16 | return preState; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/redux/reducers/mode.ts: -------------------------------------------------------------------------------- 1 | import { SET_MODE } from '../constant'; 2 | 3 | interface Action { 4 | type: string; 5 | data: number; 6 | } 7 | 8 | const initState = 0; 9 | 10 | export default function addReducer(preState = initState, action: Action) { 11 | const { type, data } = action; 12 | switch (type) { 13 | case SET_MODE: 14 | return data; 15 | default: 16 | return preState; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/redux/reducers/name.ts: -------------------------------------------------------------------------------- 1 | import { SET_NAME } from '../constant'; 2 | 3 | interface Action { 4 | type: string; 5 | data: string; 6 | } 7 | 8 | const initState = ''; 9 | 10 | export default function addReducer(preState = initState, action: Action) { 11 | const { type, data } = action; 12 | switch (type) { 13 | case SET_NAME: 14 | return data; 15 | default: 16 | return preState; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/redux/reducers/navShow.ts: -------------------------------------------------------------------------------- 1 | import { SET_NAV_SHOW } from '../constant'; 2 | 3 | interface Action { 4 | type: string; 5 | data: boolean; 6 | } 7 | 8 | const initState = true; 9 | 10 | export default function addReducer(preState = initState, action: Action) { 11 | const { type, data } = action; 12 | switch (type) { 13 | case SET_NAV_SHOW: 14 | return data; 15 | default: 16 | return preState; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/redux/store.ts: -------------------------------------------------------------------------------- 1 | import { composeWithDevTools } from '@redux-devtools/extension'; 2 | import { createStore } from 'redux'; 3 | 4 | import allReducers from './reducers'; 5 | 6 | const store = 7 | process.env.NODE_ENV === 'development' 8 | ? createStore(allReducers, composeWithDevTools()) 9 | : createStore(allReducers); 10 | 11 | export default store; 12 | -------------------------------------------------------------------------------- /src/styles/base.scss: -------------------------------------------------------------------------------- 1 | // size 2 | $navHeight: 60px; 3 | $footerHeight: 120px; 4 | $centerWidth: 1200px; 5 | $asideWidth: 260px; 6 | $layoutWidth: 1000px; 7 | $topTitleHeight: 440px; 8 | $btnWidth: 70px; 9 | 10 | // 圆角 11 | $bigRadius: 20px; 12 | $midRadius: 12px; 13 | 14 | // 间距 15 | $space: 20px; 16 | 17 | // 不变的颜色 18 | $codeOther: #c9d1d9; 19 | // 按钮/字体高亮/hover 20 | $hoverColor: var(--hoverColor, rgb(98, 164, 218)); 21 | 22 | // ---------黑色----------- 23 | // 主色 24 | $themeColor: var(--themeColor, rgb(12, 29, 27)); 25 | // 副色1 26 | $themeColor1: var(--themeColor1, rgb(22, 54, 51)); 27 | // 副色2 28 | $themeColor2: var(--themeColor2, rgb(39, 95, 89)); 29 | // 字体色 30 | $textColor: var(--textColor, rgb(255, 255, 255)); 31 | // 切换开关灰色 32 | $switchOff: var(--switchOff, rgb(77, 77, 77)); 33 | // placeholder 34 | $tip: var(--tip, rgb(116, 116, 116)); 35 | // App背景色 36 | $bodyColor: var(--bodyColor, rgb(0, 0, 0)); 37 | // footer透明背景色 38 | $footerBg: var(--footerBg, rgba(0, 0, 0, 0.3)); 39 | // 代码块背景 40 | $codeBg: var(--codeBg, rgb(37, 43, 48)); 41 | // 稍微不明显的字体颜色(版权信息页) 42 | $textColor2: var(--textColor2, rgb(167, 167, 167)); 43 | // 行内代码 44 | $inlineCode: var(--inlineCode, rgb(29, 71, 67)); 45 | -------------------------------------------------------------------------------- /src/styles/style.scss: -------------------------------------------------------------------------------- 1 | @import './base.scss'; 2 | 3 | .trans { 4 | transition: all 0.3s !important; 5 | } 6 | 7 | .hover { 8 | @extend .trans; 9 | } 10 | .hover:hover { 11 | background-color: $hoverColor !important; 12 | } 13 | 14 | .btnBase { 15 | position: absolute; 16 | width: $btnWidth; 17 | height: 36px; 18 | display: flex; 19 | justify-content: center; 20 | align-items: center; 21 | user-select: none; 22 | bottom: 0; 23 | border-radius: 14px; 24 | font-size: 18px; 25 | cursor: pointer; 26 | background-color: $themeColor1; 27 | @extend .hover; 28 | } 29 | 30 | .tagBase { 31 | display: inline-block; 32 | padding: 0 8px; 33 | margin: 2px; 34 | height: 30px; 35 | text-align: center; 36 | line-height: 30px; 37 | font-size: 18px; 38 | user-select: none; 39 | color: $textColor; 40 | border-radius: 10px; 41 | cursor: pointer; 42 | @extend .hover; 43 | } 44 | -------------------------------------------------------------------------------- /src/types/asset.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' { 2 | const path: string; 3 | export default path; 4 | } 5 | 6 | declare module '*.bmp' { 7 | const path: string; 8 | export default path; 9 | } 10 | 11 | declare module '*.gif' { 12 | const path: string; 13 | export default path; 14 | } 15 | 16 | declare module '*.jpg' { 17 | const path: string; 18 | export default path; 19 | } 20 | 21 | declare module '*.jpeg' { 22 | const path: string; 23 | export default path; 24 | } 25 | 26 | declare module '*.png' { 27 | const path: string; 28 | export default path; 29 | } 30 | -------------------------------------------------------------------------------- /src/types/style.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.css' { 2 | const style: { [className: string]: string }; 3 | export default style; 4 | } 5 | 6 | declare module '*.scss' { 7 | const style: { [className: string]: string }; 8 | export default style; 9 | } 10 | 11 | declare module '*.less' { 12 | const style: { [className: string]: string }; 13 | export default style; 14 | } 15 | -------------------------------------------------------------------------------- /src/utils/apis/authLogin.ts: -------------------------------------------------------------------------------- 1 | import { auth } from '../cloudBase'; 2 | 3 | export const authLogin = (adminEmail: string, adminPwd: string) => 4 | auth 5 | .signInWithEmailAndPassword(adminEmail, adminPwd) 6 | .then(() => true) 7 | .catch(() => false); 8 | -------------------------------------------------------------------------------- /src/utils/apis/axios.ts: -------------------------------------------------------------------------------- 1 | import axios, { Method } from 'axios'; 2 | 3 | export const axiosAPI = (url: string, method: Method, params: object) => { 4 | return axios({ 5 | url, 6 | method, 7 | params, 8 | withCredentials: true 9 | }) 10 | .then(res => res.status === 200) 11 | .catch(() => false); 12 | }; 13 | -------------------------------------------------------------------------------- /src/utils/apis/dbConfig.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | export enum DB { 3 | About = 'about', 4 | Msg = 'allComments', 5 | Article = 'articles', 6 | Class = 'classes', 7 | Drafe = 'drafts', 8 | Link = 'links', 9 | Log = 'logs', 10 | Notice = 'notice', 11 | Say = 'says', 12 | Show = 'shows', 13 | Count = 'siteCount', 14 | Tag = 'tags' 15 | } 16 | -------------------------------------------------------------------------------- /src/utils/apis/getData.ts: -------------------------------------------------------------------------------- 1 | import { db } from '../cloudBase'; 2 | 3 | export const getData = (dbName: string) => 4 | db 5 | .collection(dbName) 6 | .get() 7 | .then(res => res) 8 | .catch(err => err); 9 | -------------------------------------------------------------------------------- /src/utils/apis/getOrderData.ts: -------------------------------------------------------------------------------- 1 | import { db } from '../cloudBase'; 2 | 3 | export const getOrderData = (config: { 4 | dbName: string; 5 | sortKey?: string; 6 | isAsc?: boolean; 7 | }) => { 8 | const { dbName, sortKey = '_id', isAsc = false } = config; 9 | 10 | return db 11 | .collection(dbName) 12 | .orderBy(sortKey, isAsc ? 'asc' : 'desc') 13 | .get() 14 | .then(res => res) 15 | .catch(err => err); 16 | }; 17 | -------------------------------------------------------------------------------- /src/utils/apis/getPageData.ts: -------------------------------------------------------------------------------- 1 | import { db } from '../cloudBase'; 2 | 3 | export const getPageData = (config: { 4 | dbName: string; 5 | sortKey: string; 6 | isAsc: boolean; 7 | page: number; 8 | size: number; 9 | where?: object; 10 | }) => { 11 | const { dbName, sortKey, isAsc, page, size, where = {} } = config; 12 | 13 | return db 14 | .collection(dbName) 15 | .where(where) 16 | .orderBy(sortKey, isAsc ? 'asc' : 'desc') 17 | .skip((page - 1) * size) 18 | .limit(size) 19 | .get() 20 | .then(res => res) 21 | .catch(err => err); 22 | }; 23 | -------------------------------------------------------------------------------- /src/utils/apis/getSiteCount.ts: -------------------------------------------------------------------------------- 1 | import { _, db } from '../cloudBase'; 2 | import { count_id } from '../constant'; 3 | import { DB } from './dbConfig'; 4 | 5 | export const getSiteCount = () => 6 | db 7 | .collection(DB.Count) 8 | .doc(count_id) 9 | .update({ 10 | count: _.inc(1) 11 | }) 12 | .then(() => 13 | db 14 | .collection(DB.Count) 15 | .doc(count_id) 16 | .get() 17 | .then(res => res) 18 | .catch(err => err) 19 | ) 20 | .catch(err => err); 21 | -------------------------------------------------------------------------------- /src/utils/apis/getSum.ts: -------------------------------------------------------------------------------- 1 | import { db } from '../cloudBase'; 2 | 3 | export const getSum = (dbName: string, where?: object) => 4 | db 5 | .collection(dbName) 6 | .where(where || {}) 7 | .count() 8 | .then(res => res) 9 | .catch(err => err); 10 | -------------------------------------------------------------------------------- /src/utils/apis/getWhereData.ts: -------------------------------------------------------------------------------- 1 | import { db } from '../cloudBase'; 2 | 3 | export const getWhereData = (dbName: string, where: object) => 4 | db 5 | .collection(dbName) 6 | .where(where) 7 | .get() 8 | .then(res => res) 9 | .catch(err => err); 10 | -------------------------------------------------------------------------------- /src/utils/apis/getWhereOrderData.ts: -------------------------------------------------------------------------------- 1 | import { db } from '../cloudBase'; 2 | 3 | export const getWhereOrderData = (config: { 4 | dbName: string; 5 | where: object; 6 | sortKey?: string; 7 | isAsc?: boolean; 8 | }) => { 9 | const { dbName, where, sortKey = '_id', isAsc = false } = config; 10 | 11 | return db 12 | .collection(dbName) 13 | .where(where) 14 | .orderBy(sortKey, isAsc ? 'asc' : 'desc') 15 | .get() 16 | .then(res => res) 17 | .catch(err => err); 18 | }; 19 | -------------------------------------------------------------------------------- /src/utils/apis/getWhereOrderPageData.ts: -------------------------------------------------------------------------------- 1 | import { db } from '../cloudBase'; 2 | 3 | export const getWhereOrderPageData = (config: { 4 | dbName: string; 5 | where: object; 6 | page: number; 7 | size: number; 8 | sortKey?: string; 9 | isAsc?: boolean; 10 | }) => { 11 | const { dbName, where, sortKey = '_id', isAsc = false, page, size } = config; 12 | 13 | return db 14 | .collection(dbName) 15 | .where(where) 16 | .orderBy(sortKey, isAsc ? 'asc' : 'desc') 17 | .skip((page - 1) * size) 18 | .limit(size) 19 | .get() 20 | .then(res => res) 21 | .catch(err => err); 22 | }; 23 | -------------------------------------------------------------------------------- /src/utils/apis/getWhereOrderPageSum.ts: -------------------------------------------------------------------------------- 1 | import { getWhereOrderPageData } from '@/utils/apis/getWhereOrderPageData'; 2 | import { getWhereSum } from '@/utils/apis/getWhereSum'; 3 | 4 | export const getWhereOrderPageSum = async (config: { 5 | dbName: string; 6 | where: object; 7 | page: number; 8 | size: number; 9 | sortKey?: string; 10 | isAsc?: boolean; 11 | }) => { 12 | const { dbName, where } = config; 13 | 14 | const [articles, sum] = await Promise.all([ 15 | getWhereOrderPageData(config), 16 | getWhereSum(dbName, where) 17 | ]); 18 | 19 | return { 20 | articles, 21 | sum 22 | }; 23 | }; 24 | -------------------------------------------------------------------------------- /src/utils/apis/getWhereSum.ts: -------------------------------------------------------------------------------- 1 | import { db } from '../cloudBase'; 2 | 3 | export const getWhereSum = (dbName: string, where: object) => { 4 | return db 5 | .collection(dbName) 6 | .where(where) 7 | .count() 8 | .then(res => res) 9 | .catch(err => err); 10 | }; 11 | -------------------------------------------------------------------------------- /src/utils/apis/setData.ts: -------------------------------------------------------------------------------- 1 | import { db } from '../cloudBase'; 2 | 3 | export const setData = (config: { 4 | DBName: string; 5 | name: string; 6 | email: string; 7 | link: string; 8 | content: string; 9 | date: number; 10 | avatar: string; 11 | postTitle: string; 12 | replyId: string; 13 | }) => { 14 | const { DBName, name, email, link, content, date, avatar, postTitle, replyId } = config; 15 | return db 16 | .collection(DBName) 17 | .add({ 18 | name, 19 | email, 20 | link, 21 | content, 22 | date, 23 | avatar, 24 | postTitle, 25 | replyId 26 | }) 27 | .then(() => true) 28 | .catch(() => false); 29 | }; 30 | -------------------------------------------------------------------------------- /src/utils/cloudBase.ts: -------------------------------------------------------------------------------- 1 | // 腾讯云开发的一些API 2 | import cloudbase from '@cloudbase/js-sdk'; 3 | 4 | import { env } from './constant'; 5 | 6 | export const app = cloudbase.init({ env }); 7 | 8 | export const auth = app.auth({ persistence: 'local' }); 9 | 10 | export const db = app.database(); 11 | 12 | export const _ = db.command; 13 | -------------------------------------------------------------------------------- /src/utils/constant.ts: -------------------------------------------------------------------------------- 1 | // 博客的云环境ID 2 | export const env = 'react-blog-admin-test-1a3424a4e2'; 3 | 4 | export const source_github = 'https://github.com/lzxjack/react-blog'; 5 | 6 | export const icp_site = 'https://beian.miit.gov.cn/#/Integrated/index'; 7 | export const icp_no = '浙ICP备2020043821号-1'; 8 | 9 | export const blogAdminUrl = 'https://admin.lzxjack.top:81'; 10 | 11 | export const siteTitle = '飞鸟小站'; 12 | 13 | // GitHub地址 14 | export const githubUrl = 'https://github.com/lzxjack'; 15 | // CSDN地址 16 | export const csdnUrl = 'https://blog.csdn.net/Jack_lzx'; 17 | 18 | // siteCount ID 19 | export const count_id = 'cd045e756100126d005169f014931c65'; 20 | 21 | // 微信二维码地址 22 | export const weChatQRCode = 'https://img.lzxjack.top:99/202203302344287.webp'; 23 | // QQ二维码地址 24 | export const QQ_QRCode = 'https://img.lzxjack.top:99/202203302344487.webp'; 25 | 26 | // 透明头像 27 | export const cardUrl = 'https://img.lzxjack.top:99/202203302348298.webp'; 28 | 29 | // loading 30 | export const smallLoadingUrl = 'https://img.lzxjack.top:99/202203302022741.webp'; 31 | 32 | // 博客运行起始日 33 | export const time = '2020-12-16 14:00:00'; 34 | 35 | // // 博客背景图片 36 | // export const blogBackGroundImgs = [ 37 | // 'https://img.lzxjack.top:99/20210818111500.jpg', 38 | // 'https://img.lzxjack.top:99/20210818111501.png', 39 | // 'https://img.lzxjack.top:99/20210818111502.jpg', 40 | // 'https://img.lzxjack.top:99/20211126190312.jpg', 41 | // 'https://img.lzxjack.top:99/202203241558769.jpg', 42 | // 'https://img.lzxjack.top:99/202203241604408.jpg', 43 | // 'https://img.lzxjack.top:99/202203241627101.jpg', 44 | // 'https://img.lzxjack.top:99/202203241627102.jpg', 45 | // 'https://img.lzxjack.top:99/202203241627103.jpg' 46 | // ]; 47 | // // 背景图选择 48 | // export const imgNum = 7; 49 | 50 | // // 与模式相符合的背景图 51 | // export const modeBg = [ 52 | // 'https://img.lzxjack.top:99/202203241627101.jpg', 53 | // 'https://img.lzxjack.top:99/202203242228220.jpg', 54 | // 'https://img.lzxjack.top:99/202203241627103.jpg' 55 | // ]; 56 | 57 | // 数据缓存时间 58 | export const staleTime = 180000; 59 | export const siteCountStale = 300000; 60 | 61 | // 首页文章分页,每页数据 62 | export const homeSize = 8; 63 | export const msgSize = 10; 64 | export const detailPostSize = 10; 65 | 66 | // 个人信息 67 | export const myName = '飞鸟'; 68 | export const myLink = 'https://lzxjack.top'; 69 | export const myAvatar = 'https://img.lzxjack.top:99/202203302154224.webp'; 70 | export const myDescr = '一只平凡的鸟罢了。'; 71 | export const myEmail = '965555169@qq.com'; 72 | export const adminUid = '41fcc65978324a8db4048993dfc0a9df'; 73 | export const QQ = '965555169'; 74 | 75 | export const myAvatar70 = 'https://img.lzxjack.top:99/202203302156259.webp'; 76 | 77 | // 默认头像集合(若用户没获取QQ头像,则随机显示此数组中的头像) 78 | export const defaultCommentAvatarArr = [ 79 | 'https://img.lzxjack.top:99/202203302148474.webp', 80 | 'https://img.lzxjack.top:99/202203302148475.webp', 81 | 'https://img.lzxjack.top:99/202203302148476.webp', 82 | 'https://img.lzxjack.top:99/202203302148477.webp', 83 | 'https://img.lzxjack.top:99/202203302148478.webp', 84 | 'https://img.lzxjack.top:99/202203302148479.webp', 85 | 'https://img.lzxjack.top:99/202203302148480.webp', 86 | 'https://img.lzxjack.top:99/202203302148481.webp', 87 | 'https://img.lzxjack.top:99/202203302148482.webp', 88 | 'https://img.lzxjack.top:99/202203302148483.webp' 89 | ]; 90 | 91 | export const avatarArrLen = defaultCommentAvatarArr.length; 92 | 93 | // 评论回复时,发送邮件提醒的API地址 94 | export const emailApi = 95 | 'https://react-blog-admin-test-1a3424a4e2-1304393382.ap-shanghai.app.tcloudbase.com/email'; 96 | -------------------------------------------------------------------------------- /src/utils/function.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 生成指定范围内的随机整数,左闭右闭 3 | * @param {Number} Min 4 | * @param {Number} Max 5 | * @return {Number} 6 | */ 7 | export const getRandomNum = (Min: number, Max: number) => { 8 | const Range = Max - Min + 1; 9 | const Rand = Math.random(); 10 | return Min + Math.floor(Rand * Range); 11 | }; 12 | 13 | /** 14 | * 打乱数组 15 | * @param {any[]} array 16 | * @return {any[]} 17 | */ 18 | export const shuffleArray = (array: any[]) => { 19 | if (!array) return []; 20 | const res = [...array]; 21 | const len = res.length; 22 | for (let i = len - 1; i > 0; i--) { 23 | const randomPos = Math.floor(Math.random() * (i + 1)); 24 | [res[i], res[randomPos]] = [res[randomPos], res[i]]; 25 | } 26 | return res; 27 | }; 28 | -------------------------------------------------------------------------------- /src/utils/hooks/useLazyImg.ts: -------------------------------------------------------------------------------- 1 | import { useInViewport, useSafeState } from 'ahooks'; 2 | import { useEffect, useRef } from 'react'; 3 | 4 | export const useLazyImg = (avatar: string, loading: string) => { 5 | const imgRef = useRef(null); 6 | // eslint-disable-next-line no-unused-vars 7 | const [inViewport] = useInViewport(imgRef); 8 | 9 | const [imgUrl, setImgUrl] = useSafeState(loading); 10 | 11 | useEffect(() => { 12 | if (!inViewport) return; 13 | setImgUrl(avatar); 14 | }, [inViewport]); 15 | 16 | return { imgRef, imgUrl }; 17 | }; 18 | -------------------------------------------------------------------------------- /src/utils/hooks/useTime.ts: -------------------------------------------------------------------------------- 1 | // import { useMount, useSafeState } from 'ahooks'; 2 | 3 | export const useTime = () => { 4 | const hour = new Date().getHours(); 5 | const timeText = 6 | hour < 6 7 | ? '凌晨好' 8 | : hour < 9 9 | ? '早上好' 10 | : hour < 11 11 | ? '上午好' 12 | : hour < 13 13 | ? '中午好' 14 | : hour < 17 15 | ? '下午好' 16 | : hour < 19 17 | ? '傍晚好' 18 | : '晚上好'; 19 | 20 | return { timeText }; 21 | }; 22 | -------------------------------------------------------------------------------- /src/utils/hooks/useTop.ts: -------------------------------------------------------------------------------- 1 | import { useMount } from 'ahooks'; 2 | 3 | const useTop = (setNavShow?: Function) => { 4 | useMount(() => { 5 | window.scrollTo(0, 0); 6 | setNavShow?.(true); 7 | }); 8 | }; 9 | 10 | export default useTop; 11 | -------------------------------------------------------------------------------- /src/utils/modeMap.ts: -------------------------------------------------------------------------------- 1 | export const modeMap = { 2 | '--themeColor': ['rgb(12, 29, 27)', 'rgb(185, 232, 255)', 'rgb(221, 239, 255)'], 3 | '--themeColor1': ['rgb(22, 54, 51)', 'rgb(157, 222, 255)', 'rgb(194, 209, 223)'], 4 | '--themeColor2': ['rgb(39, 95, 89)', 'rgb(110, 207, 255)', 'rgb(171, 185, 199)'], 5 | '--textColor': ['rgb(255, 255, 255)', 'rgb(53, 53, 53)', 'rgb(53, 53, 53)'], 6 | '--switchOff': ['rgb(77, 77, 77)', 'rgb(179, 198, 207)', 'rgb(184, 198, 211)'], 7 | '--tip': ['rgb(116, 116, 116)', 'rgb(180, 180, 180)', 'rgb(139, 139, 139)'], 8 | '--bodyColor': ['rgb(0, 0, 0)', 'rgb(255, 255, 255)', 'rgb(255, 255, 255)'], 9 | '--footerBg': [ 10 | 'rgba(0, 0, 0, 0.3)', 11 | 'rgba(255, 255, 255, 0.4)', 12 | 'rgba(255, 255, 255, 0.3)' 13 | ], 14 | '--codeBg': ['rgb(37, 43, 48)', 'rgb(50, 57, 63)', 'rgb(50, 57, 63)'], 15 | '--textColor2': ['rgb(167, 167, 167)', 'rgb(122, 122, 122)', 'rgb(104, 104, 104)'], 16 | '--inlineCode': ['rgb(29, 71, 67)', 'rgb(136, 215, 255)', 'rgb(205, 219, 233)'] 17 | }; 18 | 19 | export const modeMapArr = Object.keys(modeMap); 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // 基本配置 4 | "target": "ES5", // 编译成哪个版本的 es 5 | "module": "ESNext", // 指定生成哪个模块系统代码 6 | "lib": ["dom", "dom.iterable", "esnext"], // 编译过程中需要引入的库文件的列表 7 | "allowJs": false, // 允许编译 js 文件 8 | "jsx": "react", // 在 .tsx 文件里支持 JSX 9 | "isolatedModules": true, // 提供额外的一些语法检查,如文件没有模块导出会报错 10 | "strict": true, // 启用所有严格类型检查选项 11 | 12 | // 模块解析选项 13 | "moduleResolution": "node", // 指定模块解析策略 14 | "esModuleInterop": true, // 支持 CommonJS 和 ES 模块之间的互操作性 15 | "resolveJsonModule": true, // 支持导入 json 模块 16 | "baseUrl": "./", // 根路径 17 | "paths": { 18 | // 路径映射,与 baseUrl 关联 19 | "@/*": ["./src/*"] 20 | }, 21 | 22 | // 实验性选项 23 | "experimentalDecorators": true, // 启用实验性的ES装饰器 24 | "emitDecoratorMetadata": true, // 给源码里的装饰器声明加上设计类型元数据 25 | 26 | // 其他选项 27 | "forceConsistentCasingInFileNames": true, // 禁止对同一个文件的不一致的引用 28 | "skipLibCheck": true, // 忽略所有的声明文件( *.d.ts)的类型检查 29 | "allowSyntheticDefaultImports": true, // 允许从没有设置默认导出的模块中默认导入 30 | "noEmit": true // 只想使用tsc的类型检查作为函数时(当其他工具(例如Babel实际编译)时)使用它 31 | // "strictNullChecks": false 32 | }, 33 | "exclude": ["node_modules"] 34 | } 35 | --------------------------------------------------------------------------------