├── .DS_Store ├── LICENSE ├── README.md ├── alipay.png ├── blog-server ├── .DS_Store ├── .env ├── .gitignore ├── .npmrc ├── Dockerfile ├── README.md ├── app.js ├── db │ └── online_blog.sql ├── directoryList.md ├── docker-compose.yml ├── nginx.conf ├── package-lock.json ├── package.json ├── public │ └── stylesheets │ │ └── style.css ├── sql │ └── online_blog.sql ├── src │ ├── app │ │ └── errorHandler.js │ ├── config │ │ └── config.default.js │ ├── controller │ │ ├── article │ │ │ ├── common.js │ │ │ └── index.js │ │ ├── category │ │ │ └── index.js │ │ ├── chat │ │ │ └── index.js │ │ ├── comment │ │ │ └── index.js │ │ ├── header │ │ │ └── index.js │ │ ├── like │ │ │ └── index.js │ │ ├── links │ │ │ └── index.js │ │ ├── message │ │ │ └── index.js │ │ ├── notify │ │ │ └── index.js │ │ ├── photo │ │ │ └── index.js │ │ ├── photoAlbum │ │ │ └── index.js │ │ ├── statistic │ │ │ └── index.js │ │ ├── tag │ │ │ └── index.js │ │ ├── talk │ │ │ └── index.js │ │ ├── user │ │ │ └── index.js │ │ └── utils │ │ │ └── index.js │ ├── db │ │ └── seq.js │ ├── main.js │ ├── middleware │ │ ├── article │ │ │ └── index.js │ │ ├── auth │ │ │ └── index.js │ │ ├── category │ │ │ └── category.js │ │ ├── limit-request │ │ │ └── index.js │ │ ├── tag │ │ │ └── tag.js │ │ └── user │ │ │ └── index.js │ ├── model │ │ ├── article │ │ │ ├── article.js │ │ │ └── articleTag.js │ │ ├── category │ │ │ └── category.js │ │ ├── chat │ │ │ └── chat.js │ │ ├── comment │ │ │ └── comment.js │ │ ├── config │ │ │ └── config.js │ │ ├── header │ │ │ └── header.js │ │ ├── like │ │ │ └── like.js │ │ ├── links │ │ │ └── links.js │ │ ├── message │ │ │ └── message.js │ │ ├── notify │ │ │ └── notify.js │ │ ├── photo │ │ │ ├── photo.js │ │ │ └── photoAlbum.js │ │ ├── recommend │ │ │ └── recommend.js │ │ ├── tag │ │ │ └── tag.js │ │ ├── talk │ │ │ ├── talk.js │ │ │ └── talkPhoto.js │ │ └── user │ │ │ └── user.js │ ├── result │ │ └── index.js │ ├── router │ │ ├── article.js │ │ ├── category.js │ │ ├── chat.js │ │ ├── comment.js │ │ ├── config.js │ │ ├── index.js │ │ ├── like.js │ │ ├── links.js │ │ ├── message.js │ │ ├── notify.js │ │ ├── pageHeader.js │ │ ├── photo.js │ │ ├── photoAlbum.js │ │ ├── statistic.js │ │ ├── tag.js │ │ ├── talk.js │ │ ├── user.js │ │ └── utils.js │ ├── service │ │ ├── article │ │ │ ├── articleTag.js │ │ │ └── index.js │ │ ├── category │ │ │ └── index.js │ │ ├── chat │ │ │ └── index.js │ │ ├── comment │ │ │ └── index.js │ │ ├── config │ │ │ └── index.js │ │ ├── header │ │ │ └── index.js │ │ ├── like │ │ │ └── index.js │ │ ├── links │ │ │ └── index.js │ │ ├── message │ │ │ └── index.js │ │ ├── notify │ │ │ └── index.js │ │ ├── photo │ │ │ └── index.js │ │ ├── photoAlbum │ │ │ └── index.js │ │ ├── tag │ │ │ └── index.js │ │ ├── talk │ │ │ ├── index.js │ │ │ └── talkPhoto.js │ │ └── user │ │ │ └── index.js │ ├── upload │ │ └── online │ │ │ ├── 35a148158b323e72b22f7ab01.png │ │ │ ├── 9bb507f4bd065759a3d093d04.webp │ │ │ ├── alipay.png │ │ │ └── zhifupay.png │ ├── utils │ │ ├── minioUpload.js │ │ ├── qiniuUpload.js │ │ ├── sensitive.js │ │ ├── swagger.js │ │ ├── tool.js │ │ └── websocket.js │ └── views │ │ ├── error.ejs │ │ └── index.ejs └── test │ └── test.js ├── blog-v3-admin ├── .editorconfig ├── .env ├── .env.development ├── .env.production ├── .env.staging ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .husky │ ├── commit-msg │ ├── common.sh │ ├── lintstagedrc.js │ └── pre-commit ├── .markdownlint.json ├── .npmrc ├── .prettierrc.js ├── .stylelintignore ├── .vscode │ ├── extensions.json │ ├── settings.json │ ├── vue3.0.code-snippets │ └── vue3.2.code-snippets ├── LICENSE ├── README.md ├── build │ ├── cdn.ts │ ├── compress.ts │ ├── index.ts │ ├── info.ts │ ├── optimize.ts │ └── plugins.ts ├── commitlint.config.js ├── index.html ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── public │ ├── favicon.ico │ └── serverConfig.json ├── server.js ├── src │ ├── App.vue │ ├── api │ │ ├── article.ts │ │ ├── category.ts │ │ ├── home.ts │ │ ├── links.ts │ │ ├── message.ts │ │ ├── pageheader.ts │ │ ├── photo.ts │ │ ├── site.ts │ │ ├── tag.ts │ │ ├── talk.ts │ │ └── user.ts │ ├── assets │ │ ├── iconfont │ │ │ ├── iconfont.css │ │ │ ├── iconfont.js │ │ │ ├── iconfont.json │ │ │ ├── iconfont.ttf │ │ │ ├── iconfont.woff │ │ │ └── iconfont.woff2 │ │ ├── img │ │ │ ├── bathymetry_bw_composite_4k.jpg │ │ │ ├── clouds.png │ │ │ ├── earth.jpg │ │ │ ├── night.jpg │ │ │ └── starfield.jpg │ │ ├── login │ │ │ ├── avatar.svg │ │ │ ├── bg.png │ │ │ └── illustration.svg │ │ ├── status │ │ │ ├── 403.svg │ │ │ ├── 404.svg │ │ │ └── 500.svg │ │ └── svg │ │ │ ├── back_top.svg │ │ │ ├── biaoqian.svg │ │ │ ├── dark.svg │ │ │ ├── day.svg │ │ │ ├── enter_outlined.svg │ │ │ ├── exit_screen.svg │ │ │ ├── fenlei.svg │ │ │ ├── full_screen.svg │ │ │ ├── keyboard_esc.svg │ │ │ ├── wenzhang.svg │ │ │ ├── yonghu.svg │ │ │ └── zhiding.svg │ ├── components │ │ ├── HomeCard │ │ │ └── home-card.vue │ │ ├── ReAuth │ │ │ ├── index.ts │ │ │ └── src │ │ │ │ └── auth.tsx │ │ ├── ReIcon │ │ │ ├── index.ts │ │ │ └── src │ │ │ │ ├── hooks.ts │ │ │ │ ├── iconfont.ts │ │ │ │ ├── iconifyIconOffline.ts │ │ │ │ ├── iconifyIconOnline.ts │ │ │ │ ├── offlineIcon.ts │ │ │ │ └── types.ts │ │ ├── ReTypeit │ │ │ └── index.vue │ │ ├── TextOverflow │ │ │ └── index.vue │ │ └── Upload │ │ │ └── upload.vue │ ├── config │ │ └── index.ts │ ├── directives │ │ ├── auth │ │ │ └── index.ts │ │ ├── elResizeDetector │ │ │ └── index.ts │ │ └── index.ts │ ├── layout │ │ ├── components │ │ │ ├── appMain.vue │ │ │ ├── navbar.vue │ │ │ ├── notice │ │ │ │ ├── data.ts │ │ │ │ ├── index.vue │ │ │ │ ├── noticeItem.vue │ │ │ │ └── noticeList.vue │ │ │ ├── panel │ │ │ │ └── index.vue │ │ │ ├── screenfull │ │ │ │ └── index.vue │ │ │ ├── search │ │ │ │ ├── components │ │ │ │ │ ├── SearchFooter.vue │ │ │ │ │ ├── SearchModal.vue │ │ │ │ │ ├── SearchResult.vue │ │ │ │ │ └── index.ts │ │ │ │ └── index.vue │ │ │ ├── setting │ │ │ │ └── index.vue │ │ │ ├── sidebar │ │ │ │ ├── breadCrumb.vue │ │ │ │ ├── horizontal.vue │ │ │ │ ├── leftCollapse.vue │ │ │ │ ├── logo.vue │ │ │ │ ├── mixNav.vue │ │ │ │ ├── sidebarItem.vue │ │ │ │ ├── topCollapse.vue │ │ │ │ └── vertical.vue │ │ │ └── tag │ │ │ │ ├── index.scss │ │ │ │ └── index.vue │ │ ├── frameView.vue │ │ ├── hooks │ │ │ ├── useBoolean.ts │ │ │ ├── useDataThemeChange.ts │ │ │ ├── useLayout.ts │ │ │ ├── useNav.ts │ │ │ └── useTag.ts │ │ ├── index.vue │ │ ├── redirect.vue │ │ ├── theme │ │ │ └── index.ts │ │ └── types.ts │ ├── main.ts │ ├── plugins │ │ ├── echarts │ │ │ └── index.ts │ │ └── element-plus │ │ │ └── index.ts │ ├── router │ │ ├── index.ts │ │ ├── modules │ │ │ ├── article.ts │ │ │ ├── error.ts │ │ │ ├── home.ts │ │ │ ├── personal.ts │ │ │ ├── photo.ts │ │ │ ├── remaining.ts │ │ │ ├── site.ts │ │ │ ├── talk.ts │ │ │ └── user.ts │ │ └── utils.ts │ ├── store │ │ ├── index.ts │ │ └── modules │ │ │ ├── app.ts │ │ │ ├── epTheme.ts │ │ │ ├── multiTags.ts │ │ │ ├── permission.ts │ │ │ ├── settings.ts │ │ │ ├── static.ts │ │ │ ├── types.ts │ │ │ └── user.ts │ ├── style │ │ ├── dark.scss │ │ ├── element-plus.scss │ │ ├── index.scss │ │ ├── login.css │ │ ├── mixin.scss │ │ ├── reset.scss │ │ ├── sidebar.scss │ │ ├── tailwind.css │ │ └── transition.scss │ ├── utils │ │ ├── auth.ts │ │ ├── globalPolyfills.ts │ │ ├── http │ │ │ ├── index.ts │ │ │ └── types.d.ts │ │ ├── message.ts │ │ ├── mitt.ts │ │ ├── print.ts │ │ ├── progress │ │ │ └── index.ts │ │ ├── propTypes.ts │ │ ├── responsive.ts │ │ ├── sso.ts │ │ ├── tree.ts │ │ └── utils.ts │ └── views │ │ ├── article │ │ ├── add-edit-article │ │ │ ├── index.ts │ │ │ ├── index.vue │ │ │ └── validator.ts │ │ └── article-manage │ │ │ ├── index.tsx │ │ │ └── index.vue │ │ ├── category │ │ ├── index.tsx │ │ └── index.vue │ │ ├── error │ │ └── 404.vue │ │ ├── home │ │ ├── components │ │ │ ├── Bar.vue │ │ │ ├── CodeMap.vue │ │ │ └── WordCloud.vue │ │ └── index.vue │ │ ├── links │ │ ├── index.ts │ │ └── index.vue │ │ ├── login │ │ ├── index.vue │ │ └── utils │ │ │ ├── motion.ts │ │ │ ├── rule.ts │ │ │ └── static.ts │ │ ├── message │ │ ├── index.ts │ │ └── index.vue │ │ ├── pageheader │ │ ├── index.vue │ │ └── routerList.js │ │ ├── personal │ │ ├── index.ts │ │ └── index.vue │ │ ├── photo │ │ ├── photoAlbum │ │ │ └── index.vue │ │ └── photoList │ │ │ ├── add-photos.vue │ │ │ └── index.vue │ │ ├── register │ │ └── index.vue │ │ ├── site │ │ ├── index.tsx │ │ └── site.vue │ │ ├── tag │ │ ├── index.tsx │ │ └── index.vue │ │ ├── talk │ │ ├── add-edit-talk │ │ │ └── index.vue │ │ └── talk-list │ │ │ └── index.vue │ │ └── user │ │ ├── index.tsx │ │ └── index.vue ├── stylelint.config.js ├── tailwind.config.js ├── tsconfig.json ├── types │ ├── global-components.d.ts │ ├── global.d.ts │ ├── index.d.ts │ ├── shims-tsx.d.ts │ └── shims-vue.d.ts └── vite.config.ts ├── blog-v3 ├── .eslintignore ├── .eslintrc.cjs ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc.cjs ├── .vscode │ └── extensions.json ├── LICENSE ├── README.md ├── auto-imports.d.ts ├── components.d.ts ├── index.html ├── jsconfig.json ├── package.json ├── pnpm-lock.yaml ├── postcss.config.cjs ├── public │ └── favicon.ico ├── server.js ├── src │ ├── App.vue │ ├── api │ │ ├── article.js │ │ ├── category.js │ │ ├── chat.js │ │ ├── comment.js │ │ ├── config.js │ │ ├── home.js │ │ ├── like.js │ │ ├── links.js │ │ ├── message.js │ │ ├── music.js │ │ ├── notify.js │ │ ├── photo.js │ │ ├── tag.js │ │ ├── talk.js │ │ └── user.js │ ├── assets │ │ ├── context-menu │ │ │ ├── bofang.png │ │ │ ├── bofangliebiao.png │ │ │ ├── danquxunhuan.png │ │ │ ├── fenlei.png │ │ │ ├── fuzhi1.png │ │ │ ├── link.png │ │ │ ├── lrc.png │ │ │ ├── qiansemoshi.png │ │ │ ├── quanbuxunhuan.png │ │ │ ├── shangyishou.png │ │ │ ├── shensemoshi.png │ │ │ ├── shuaxin.png │ │ │ ├── shunxubofang.png │ │ │ ├── suiji.png │ │ │ ├── suijibofang.png │ │ │ ├── tupian.jpeg │ │ │ ├── xiangyou.png │ │ │ ├── xiangzuo.png │ │ │ ├── xiayishou.png │ │ │ └── zanting.png │ │ ├── css │ │ │ └── iconFont │ │ │ │ ├── demo.css │ │ │ │ ├── demo_index.html │ │ │ │ ├── iconfont.css │ │ │ │ ├── iconfont.js │ │ │ │ ├── iconfont.json │ │ │ │ ├── iconfont.ttf │ │ │ │ ├── iconfont.woff │ │ │ │ └── iconfont.woff2 │ │ └── img │ │ │ ├── apple-logo-white.svg │ │ │ └── blogAvatar.png │ ├── components │ │ ├── BackTop │ │ │ └── index.vue │ │ ├── ChatRoom │ │ │ └── index.vue │ │ ├── Comment │ │ │ ├── Comment.vue │ │ │ ├── item │ │ │ │ ├── ChildrenItem.vue │ │ │ │ ├── CommentInput.vue │ │ │ │ ├── IconList.vue │ │ │ │ └── ParentItem.vue │ │ │ └── tool.js │ │ ├── ContextMenu │ │ │ └── index.vue │ │ ├── GsapCount │ │ │ └── index.vue │ │ ├── HomeArticle │ │ │ ├── components │ │ │ │ └── article-skeleton.vue │ │ │ └── home-article-list.vue │ │ ├── Layout │ │ │ ├── footer │ │ │ │ └── blog-footer.vue │ │ │ ├── header │ │ │ │ ├── blog-header.vue │ │ │ │ └── login │ │ │ │ │ └── login.vue │ │ │ ├── index.vue │ │ │ └── main │ │ │ │ └── blog-main.vue │ │ ├── Loading │ │ │ └── Loading.vue │ │ ├── MessageBox │ │ │ └── message-box.vue │ │ ├── Music │ │ │ ├── controls │ │ │ │ ├── components │ │ │ │ │ ├── audio-controls.vue │ │ │ │ │ ├── blogAvatar.png │ │ │ │ │ ├── information.vue │ │ │ │ │ ├── progress-line.vue │ │ │ │ │ └── time-volume.vue │ │ │ │ └── index.vue │ │ │ ├── index.vue │ │ │ ├── list │ │ │ │ ├── components │ │ │ │ │ ├── custom-music-list.vue │ │ │ │ │ ├── lyric-board.vue │ │ │ │ │ ├── search-list.vue │ │ │ │ │ ├── special-title.vue │ │ │ │ │ └── useSpecial.js │ │ │ │ └── index.vue │ │ │ ├── musicTool.js │ │ │ └── useMusic.js │ │ ├── PageHeader │ │ │ ├── home-header.vue │ │ │ └── index.vue │ │ ├── Pagination │ │ │ └── pagination.vue │ │ ├── RightSide │ │ │ ├── components │ │ │ │ ├── item │ │ │ │ │ ├── right-side-item.vue │ │ │ │ │ └── right-side-top.vue │ │ │ │ └── skeleton │ │ │ │ │ ├── mobile-top-skeleton.vue │ │ │ │ │ ├── right-side-skeleton-item.vue │ │ │ │ │ └── right-side-top-skeleton.vue │ │ │ └── right-side.vue │ │ ├── Search │ │ │ └── blog-search.vue │ │ ├── SkeletonItem │ │ │ └── skeleton-item.vue │ │ ├── SvgIcon │ │ │ └── index.vue │ │ ├── SwitchTheme │ │ │ └── index.vue │ │ ├── TextOverflow │ │ │ └── index.vue │ │ ├── TimeLine │ │ │ └── time-line.vue │ │ ├── ToolTip │ │ │ └── tooltip.vue │ │ ├── TypeWriter │ │ │ └── type-writer.vue │ │ ├── Upload │ │ │ └── upload.vue │ │ └── WelcomeComps │ │ │ ├── first.vue │ │ │ ├── second.vue │ │ │ └── waves.vue │ ├── config │ │ ├── config.js │ │ └── request.js │ ├── directives │ │ ├── copy.js │ │ └── imageLoading.js │ ├── icons │ │ └── svg │ │ │ ├── 404.svg │ │ │ ├── chat.svg │ │ │ ├── comment.svg │ │ │ ├── darkDisc.svg │ │ │ ├── darkRocket.svg │ │ │ ├── darkTop.svg │ │ │ ├── delete.svg │ │ │ ├── disc.svg │ │ │ ├── greyHeart.svg │ │ │ ├── image404.svg │ │ │ ├── lightDisc.svg │ │ │ ├── lightRocket.svg │ │ │ ├── lightTop.svg │ │ │ ├── publish.svg │ │ │ ├── redHeart.svg │ │ │ ├── rocket.svg │ │ │ ├── search.svg │ │ │ ├── smileFace.svg │ │ │ └── user.svg │ ├── main.js │ ├── router │ │ └── index.js │ ├── store │ │ └── index.js │ ├── styles │ │ ├── base.scss │ │ ├── custom.scss │ │ ├── element │ │ │ ├── dark.scss │ │ │ ├── element.scss │ │ │ └── loading.scss │ │ ├── markdown.scss │ │ ├── tailwind.css │ │ ├── transition.scss │ │ └── variable.scss │ ├── utils │ │ ├── encipher.js │ │ ├── enum.js │ │ ├── tool.js │ │ └── transform.js │ └── views │ │ ├── 404 │ │ └── index.vue │ │ ├── archives │ │ └── archives.vue │ │ ├── article │ │ ├── article-list.vue │ │ └── article.vue │ │ ├── category │ │ └── category.vue │ │ ├── home │ │ └── home.vue │ │ ├── links │ │ ├── link-apply.vue │ │ └── link-list.vue │ │ ├── message │ │ ├── components │ │ │ ├── card-message.vue │ │ │ └── danmu-message.vue │ │ ├── detail.vue │ │ ├── index.vue │ │ ├── publish.vue │ │ └── useMessage.js │ │ ├── photo │ │ ├── photo-album.vue │ │ └── photos.vue │ │ ├── resources │ │ ├── category-list.vue │ │ ├── data.js │ │ └── site-list.vue │ │ ├── tag │ │ └── tag.vue │ │ ├── talk │ │ └── talk.vue │ │ └── user │ │ └── user-center.vue ├── tailwind.config.cjs └── vite.config.js └── zhifupay.png /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrzym99/vue3-blog/42c6177d254d2760180d08b1d560d083642291ea/.DS_Store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Mr Zhang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 小张的个人博客 2 | 3 | [博客前台在线预览地址](http://mrzym.top) 4 | 5 | [博客后台在线预览地址](http://mrzym.top/admin) 6 | 7 | 第一个稳定版本的代码,陆陆续续开发了也快一年了,从我在B站发视频分享开始,我也根据大家的建议开发了一些新的功能,解决了能发现的一些bug,也踩了很多博客部署的坑,跟着我的部署教程来,你一定能学到前后端分离他是如何将前端与后端的交互联系起来、前端项目到底是以什么样的姿态部署在服务器上的。 8 | 博客也使用了vue3、tailwind、element-plus、部分ts、vueUse等相对较新的技术框架,本人是一名前端开发,对于一些工作上会用到的开发思路、开发习惯在项目里也会体现到,大家可以试着去阅读一下一些页面的代码(大部分难点的注释也是比较多的),对以后的学习会有帮助的。 9 | 10 | #### 介绍 11 | 12 | 前端主要技术:Vue3、ELement-Plus、Vue-router、Pinia、Tailwind.css、Sass、Animate.css 13 | 14 | 后端主要技术:Node.js、Koa、Koa-router、Seqlize、Mysql 15 | 16 | 此项目包括博客后台、前台的稳定版本代码,后续的开发不会在这上面改动 17 | 如果有兴趣的小伙伴,可以自行开发 18 | 评论管理的后台接口我是写好了的,有兴趣的小伙伴可以尝试写一下评论后台管理的增删改查页面,对一下接口。 19 | 20 | #### 安装教程 21 | 22 | 每一个项目都有一个md文档 可以点进去看一看 23 | [博客部署教程](http://mrzym.top/#/article?id=6) 24 | 25 | #### 使用说明 26 | 27 | 免费使用 28 | 大家随意发挥 29 | 30 | 如果觉得这个项目还不错的话,可以打赏或捐赠作者,谢谢各位大哥赏饭吃,你的鼓励,是我最大的动力 31 | 32 | 作者还有在学习react next项目,感兴趣的话可以加群分享、帮忙答疑 33 | 34 |
35 | 36 | 37 |
38 | 39 |
40 |
博客群1(已满,可以试试能不能加进去,可能有退群的): 763111710
41 |
博客群2: 939161701
42 |
43 | 44 | -------------------------------------------------------------------------------- /alipay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrzym99/vue3-blog/42c6177d254d2760180d08b1d560d083642291ea/alipay.png -------------------------------------------------------------------------------- /blog-server/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrzym99/vue3-blog/42c6177d254d2760180d08b1d560d083642291ea/blog-server/.DS_Store -------------------------------------------------------------------------------- /blog-server/.env: -------------------------------------------------------------------------------- 1 | # node项目启动端口号 2 | APP_PORT = 8888 3 | # 数据库地址 4 | MYSQL_HOST = 127.0.0.1 # docker 部署 应改成 host.docker.internal 5 | # 数据库端口号 6 | MYSQL_PORT = 3306 7 | # 数据库连接名 8 | MYSQL_USER = root 9 | # 数据库密码 10 | MYSQL_PASSWORD = 12345678 11 | # 数据库名称 12 | MYSQL_DB = online_blog 13 | # 超级管理员密码 超级管理员账户默认是admin 密码在这里自定义 14 | # 通过管理员给你自己注册的用户管理员角色来获得操作权限 15 | ADMIN_PASSWORD = 16 | 17 | # local本地 、qiniu 七牛云、minio minio服务器 18 | UPLOADTYPE = 'local' 19 | 20 | # AK 21 | ACCESSKEY = '' 22 | # SK 23 | SECRETKEY = '' 24 | # 存储空间名称 25 | BUCKET = '' 26 | # regin 地区 根据桶的地区填写 一般是 ali、qcloud 才填写 这里没有实际作用 先预留 27 | REGIN = '' 28 | # 访问域名 29 | # 如果使用 七牛云 需配置成 测试域名或你自己绑定的二级域名 30 | # 具体可以去查看官方文档 https://developer.qiniu.com/kodo/8527/kodo-domain-name-management 31 | # 使用其他的比如 ali、qcloud 可配置也可不配置 先预留配置 后续可能会加 阿里云、腾讯云上传 32 | # 使用 minio 不配置此选项 33 | DOMAIN = '' 34 | 35 | 36 | # 上传方式使用 minio才配置 MINIO_PATH 37 | MINIO_PATH = host.docker.internal 38 | 39 | # JWT密钥 40 | JWT_SECRET = blog -------------------------------------------------------------------------------- /blog-server/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | secret 3 | 4 | .env 5 | 6 | .DS_Store 7 | 8 | **/dist/** -------------------------------------------------------------------------------- /blog-server/.npmrc: -------------------------------------------------------------------------------- 1 | # https://registry.npmmirror.com/ 淘宝 2 | # https://registry.npmjs.org/ 官方 3 | registry=https://registry.npmmirror.com/ 4 | -------------------------------------------------------------------------------- /blog-server/Dockerfile: -------------------------------------------------------------------------------- 1 | # 定义工作目录 2 | ARG PROJECT_DIR=/blog-server 3 | # 使用官方的 Node.js 运行时镜像作为基础镜像 4 | FROM node:18.17.0-alpine AS builder 5 | ARG PROJECT_DIR 6 | 7 | # 设置工作目录 8 | WORKDIR $PROJECT_DIR 9 | 10 | COPY . . 11 | 12 | # 安装项目依赖 13 | RUN npm config set registry https://registry.npmmirror.com 14 | RUN npm install 15 | 16 | # 暴露应用的端口 17 | EXPOSE 8888 18 | 19 | # 启动 20 | CMD ["npm", "run", "serve"] 21 | -------------------------------------------------------------------------------- /blog-server/directoryList.md: -------------------------------------------------------------------------------- 1 | -- bin 2 | |-- www 项目的入口 npm start 3 | -- public 存放项目静态文件 4 | |-- images 5 | |-- javascripts 6 | |-- stylesheets 7 | |-- style.css 8 | -- routes 路由 分发请求 9 | |-- index.js 10 | |-- users.js 11 | -- views 视图文件 12 | |-- error.ejs 13 | |-- index.ejs 14 | -- app.js 配置文件 15 | -- package-lock.json 控制包版本 16 | -- package.json 包管理 17 | -------------------------------------------------------------------------------- /blog-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blog-server", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "nodemon app.js", 7 | "dev": "./node_modules/.bin/nodemon app.js", 8 | "prd": "pm2 start app.js", 9 | "test": "echo \"Error: no test specified\" && exit 1", 10 | "docker:build": "docker-compose build", 11 | "docker:up": "docker-compose up -d", 12 | "docker:down": "docker-compose down", 13 | "docker:down:v": "docker-compose down -v" 14 | }, 15 | "dependencies": { 16 | "bcryptjs": "^2.4.3", 17 | "debug": "^4.1.1", 18 | "dotenv": "^16.0.3", 19 | "ejs": "~2.3.3", 20 | "jsonwebtoken": "^9.0.0", 21 | "koa": "^2.7.0", 22 | "koa-body": "^6.0.1", 23 | "koa-convert": "^1.2.0", 24 | "koa-json": "^2.0.2", 25 | "koa-logger": "^3.2.0", 26 | "koa-onerror": "^4.1.0", 27 | "koa-parameter": "^3.0.1", 28 | "koa-router": "^7.4.0", 29 | "koa-static": "^5.0.0", 30 | "koa-views": "^6.2.0", 31 | "koa2-ratelimit": "^1.1.3", 32 | "koa2-swagger-ui": "^5.3.0", 33 | "minio": "^7.1.3", 34 | "mint-filter": "^4.0.3", 35 | "moment": "^2.29.4", 36 | "mysql2": "^3.1.2", 37 | "qiniu": "^7.8.0", 38 | "request": "^2.88.2", 39 | "sequelize": "^6.29.0", 40 | "swagger-jsdoc": "^6.1.0", 41 | "ws": "^8.17.0", 42 | "xz-ipnet": "^1.0.2" 43 | }, 44 | "devDependencies": { 45 | "nodemon": "^1.19.4" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /blog-server/public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 50px; 3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; 4 | } 5 | 6 | a { 7 | color: #00B7FF; 8 | } 9 | -------------------------------------------------------------------------------- /blog-server/src/app/errorHandler.js: -------------------------------------------------------------------------------- 1 | module.exports = (err, ctx) => { 2 | let status = 500; 3 | switch (err.code) { 4 | case "100002": 5 | // 没有权限 6 | status = 403; 7 | break; 8 | case "100016": 9 | // token 过期 需要重新登录 10 | status = 401; 11 | break; 12 | default: 13 | status = 500; 14 | } 15 | ctx.status = status; 16 | ctx.body = err; 17 | console.error(err); 18 | }; 19 | -------------------------------------------------------------------------------- /blog-server/src/config/config.default.js: -------------------------------------------------------------------------------- 1 | // 可以百度一下dotenv 2 | const dotenv = require("dotenv"); 3 | // 这里读取的是.env文件下的配置 4 | dotenv.config(); 5 | 6 | // console.log(process.env) 7 | 8 | module.exports = process.env; 9 | -------------------------------------------------------------------------------- /blog-server/src/controller/statistic/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Description: 获取一些统计信息 3 | * @Author: M 4 | * @Date: 2023-03-20 11:02:29 5 | * @LastEditTime: 2023-03-20 11:18:52 6 | * @LastEditors: M 7 | */ 8 | const { getArticleCount } = require("../../service/article/index") 9 | const { getTagCount } = require("../../service/tag/index") 10 | const { getCategoryCount } = require("../../service/category/index") 11 | const { getUserCount } = require("../../service/user/index") 12 | 13 | const { result, ERRORCODE, throwError } = require("../../result/index") 14 | const errorCode = ERRORCODE.STATISTIC 15 | 16 | class Statistic { 17 | async homeGetStatistic(ctx) { 18 | try { 19 | let articleCount = await getArticleCount() 20 | let tagCount = await getTagCount() 21 | let categoryCount = await getCategoryCount() 22 | let userCount = await getUserCount() 23 | ctx.body = result("获取数据统计成功", { 24 | articleCount, 25 | tagCount, 26 | categoryCount, 27 | userCount, 28 | }) 29 | } catch (err) { 30 | console.error(err) 31 | return ctx.app.emit("error", throwError(errorCode, "获取数据统计失败"), ctx) 32 | } 33 | } 34 | } 35 | 36 | module.exports = new Statistic() 37 | -------------------------------------------------------------------------------- /blog-server/src/db/seq.js: -------------------------------------------------------------------------------- 1 | const { Sequelize } = require("sequelize"); 2 | 3 | const { 4 | MYSQL_HOST, 5 | MYSQL_PORT, 6 | MYSQL_USER, 7 | MYSQL_PASSWORD, 8 | MYSQL_DB, 9 | } = require("../config/config.default"); 10 | 11 | if (!MYSQL_PASSWORD) { 12 | console.error("数据库密码不能为空"); 13 | } 14 | 15 | const seq = new Sequelize(MYSQL_DB, MYSQL_USER, MYSQL_PASSWORD, { 16 | host: MYSQL_HOST, 17 | port: MYSQL_PORT, 18 | dialect: "mysql", 19 | timezone: "+08:00", 20 | }); 21 | 22 | seq 23 | .authenticate() 24 | .then(() => { 25 | console.log("数据库连接成功"); 26 | }) 27 | .catch((err) => { 28 | console.log(err); 29 | console.log("数据库连接失败"); 30 | }); 31 | 32 | module.exports = seq; 33 | -------------------------------------------------------------------------------- /blog-server/src/middleware/category/category.js: -------------------------------------------------------------------------------- 1 | const { getOneCategory } = require("../../service/category/index") 2 | 3 | const { ERRORCODE, throwError } = require("../../result/index") 4 | const errorCode = ERRORCODE.CATEGORY 5 | 6 | const verifyCategory = async (ctx, next) => { 7 | const { id, category_name } = ctx.request.body 8 | if (!category_name) { 9 | console.error("分类名称不能为空") 10 | return ctx.app.emit("error", throwError(errorCode, "分类名称不能为空"), ctx) 11 | } 12 | let res = await getOneCategory({ category_name }) 13 | if (res && res.id == id) { 14 | console.error("分类已存在") 15 | return ctx.app.emit("error", throwError(errorCode, "分类已存在"), ctx) 16 | } 17 | 18 | await next() 19 | } 20 | 21 | const verifyDeleteCategories = async (ctx, next) => { 22 | const { categoryIdList } = ctx.request.body 23 | if (!categoryIdList.length) { 24 | console.error("分类id列表不能为空") 25 | return ctx.app.emit("error", throwError(errorCode, "分类id列表不能为空"), ctx) 26 | } 27 | 28 | await next() 29 | } 30 | 31 | module.exports = { 32 | verifyCategory, 33 | verifyDeleteCategories, 34 | } 35 | -------------------------------------------------------------------------------- /blog-server/src/middleware/limit-request/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @author: Zhang Yuming 3 | * @date: 2024-07-20 13:55:38 4 | * @params: ctx 5 | * @description: 限制自动化脚本测试网站 6 | */ 7 | 8 | const RateLimit = require("koa2-ratelimit").RateLimit; 9 | 10 | /* 11 | * @author: Zhang Yuming 12 | * @params: time 限制时间内 13 | * @params: max 最多访问多少次 14 | * @params: prefix 路径 15 | * @params: message 消息提示 16 | * @description: 限制同一个 ip 多少时间内只能发送多少次请求 17 | */ 18 | const createTimesLimiter = (options) => { 19 | if (!Object.getOwnPropertyNames(options).includes("prefixKey")) { 20 | console.error("TimesLimiterError, prefixKey is required"); 21 | } 22 | 23 | const defaultOptions = { 24 | interval: 1 * 60 * 1000, // 1 minutes 25 | max: 10, 26 | prefixKey: "", // to allow the bdd to Differentiate the endpoint 27 | message: "小黑子 压测我是吧", 28 | messageKey: "message", 29 | }; 30 | Object.assign(defaultOptions, options); 31 | return RateLimit.middleware(defaultOptions); 32 | }; 33 | 34 | module.exports = { 35 | createTimesLimiter, 36 | }; 37 | -------------------------------------------------------------------------------- /blog-server/src/middleware/tag/tag.js: -------------------------------------------------------------------------------- 1 | const { getOneTag } = require("../../service/tag/index") 2 | const { ERRORCODE, throwError } = require("../../result/index") 3 | const errorCode = ERRORCODE.TAG 4 | 5 | const verifyTag = async (ctx, next) => { 6 | const { id, tag_name } = ctx.request.body 7 | if (!tag_name) { 8 | console.error("标签名称不能为空") 9 | return ctx.app.emit("error", throwError(errorCode, "标签名称不能为空"), ctx) 10 | } 11 | let res = await getOneTag({ tag_name }) 12 | if (res && res.id !== id) { 13 | console.error("标签已存在") 14 | return ctx.app.emit("error", throwError(errorCode, "标签已存在"), ctx) 15 | } 16 | await next() 17 | } 18 | 19 | const verifyDeleteTags = async (ctx, next) => { 20 | const { tagIdList } = ctx.request.body 21 | if (!tagIdList.length) { 22 | console.error("标签id列表不能为空") 23 | return ctx.app.emit("error", throwError(errorCode, "标签id列表不能为空"), ctx) 24 | } 25 | 26 | await next() 27 | } 28 | 29 | module.exports = { 30 | verifyTag, 31 | verifyDeleteTags, 32 | } 33 | -------------------------------------------------------------------------------- /blog-server/src/model/article/articleTag.js: -------------------------------------------------------------------------------- 1 | const { DataTypes } = require("sequelize") 2 | const { Sequelize } = require("sequelize") 3 | var moment = require("moment") 4 | 5 | const seq = require("../../db/seq") 6 | 7 | const ArticleTag = seq.define( 8 | "blog_article_tag", 9 | { 10 | article_id: { 11 | type: DataTypes.INTEGER, 12 | require: true, 13 | comment: "文章id", 14 | }, 15 | tag_id: { 16 | type: DataTypes.INTEGER, 17 | require: true, 18 | comment: "标签id", 19 | }, 20 | createdAt: { 21 | type: Sequelize.DATE, 22 | get() { 23 | return moment(this.getDataValue("createdAt")).format("YYYY-MM-DD HH:mm:ss") 24 | }, 25 | }, 26 | updatedAt: { 27 | type: Sequelize.DATE, 28 | get() { 29 | return moment(this.getDataValue("updatedAt")).format("YYYY-MM-DD HH:mm:ss") 30 | }, 31 | }, 32 | }, 33 | { 34 | freezeTableName: true, // 强制表名不转复数 35 | } 36 | ) 37 | 38 | // ArticleTag.sync({ alter: true }) //同步数据表 39 | 40 | module.exports = ArticleTag 41 | -------------------------------------------------------------------------------- /blog-server/src/model/category/category.js: -------------------------------------------------------------------------------- 1 | const { DataTypes } = require("sequelize") 2 | const { Sequelize } = require("sequelize") 3 | var moment = require("moment") 4 | 5 | const seq = require("../../db/seq") 6 | 7 | const Category = seq.define( 8 | "blog_category", 9 | { 10 | category_name: { 11 | type: DataTypes.STRING(55), 12 | require: true, 13 | unique: true, 14 | comment: "分类名称 唯一", 15 | }, 16 | createdAt: { 17 | type: Sequelize.DATE, 18 | get() { 19 | return moment(this.getDataValue("createdAt")).format("YYYY-MM-DD HH:mm:ss") 20 | }, 21 | }, 22 | updatedAt: { 23 | type: Sequelize.DATE, 24 | get() { 25 | return moment(this.getDataValue("updatedAt")).format("YYYY-MM-DD HH:mm:ss") 26 | }, 27 | }, 28 | }, 29 | { 30 | freezeTableName: true, // 强制表名不转复数 31 | } 32 | ) 33 | 34 | // Category.sync({ alter: true }) //同步数据表 35 | 36 | module.exports = Category 37 | -------------------------------------------------------------------------------- /blog-server/src/model/chat/chat.js: -------------------------------------------------------------------------------- 1 | const { DataTypes } = require("sequelize"); 2 | const { Sequelize } = require("sequelize"); 3 | var moment = require("moment"); 4 | 5 | const seq = require("../../db/seq"); 6 | 7 | const Chat = seq.define( 8 | "blog_chat", 9 | { 10 | user_id: { 11 | type: DataTypes.INTEGER, 12 | require: true, 13 | comment: "用户id 用于判断是谁发送的", 14 | }, 15 | content: { 16 | type: DataTypes.STRING(555), 17 | require: true, 18 | comment: "聊天内容", 19 | }, 20 | content_type: { 21 | type: DataTypes.STRING(55), 22 | require: true, 23 | comment: "聊天的内容格式 如果是文本就是text 图片就是 img", 24 | }, 25 | createdAt: { 26 | type: Sequelize.DATE, 27 | get() { 28 | return moment(this.getDataValue("createdAt")).format("YYYY-MM-DD HH:mm:ss"); 29 | }, 30 | }, 31 | updatedAt: { 32 | type: Sequelize.DATE, 33 | get() { 34 | return moment(this.getDataValue("updatedAt")).format("YYYY-MM-DD HH:mm:ss"); 35 | }, 36 | }, 37 | }, 38 | { 39 | freezeTableName: true, // 强制表名不转复数 40 | } 41 | ); 42 | 43 | // Chat.sync({ alter: true }); //同步数据表 44 | 45 | module.exports = Chat; 46 | -------------------------------------------------------------------------------- /blog-server/src/model/header/header.js: -------------------------------------------------------------------------------- 1 | const { DataTypes } = require("sequelize"); 2 | const { Sequelize } = require("sequelize"); 3 | var moment = require("moment"); 4 | 5 | const seq = require("../../db/seq"); 6 | 7 | const Header = seq.define( 8 | "blog_header", 9 | { 10 | route_name: { 11 | type: DataTypes.STRING(555), 12 | require: true, 13 | comment: "路由名称", 14 | }, 15 | bg_url: { 16 | type: DataTypes.STRING, // STRING 默认255 17 | require: true, 18 | comment: "背景图", 19 | }, 20 | createdAt: { 21 | type: Sequelize.DATE, 22 | get() { 23 | return moment(this.getDataValue("createdAt")).format("YYYY-MM-DD HH:mm:ss"); 24 | }, 25 | }, 26 | updatedAt: { 27 | type: Sequelize.DATE, 28 | get() { 29 | return moment(this.getDataValue("updatedAt")).format("YYYY-MM-DD HH:mm:ss"); 30 | }, 31 | }, 32 | }, 33 | { 34 | freezeTableName: true, // 强制表名不转复数 35 | } 36 | ); 37 | 38 | // Header.sync({ alter: true }); //同步数据表 39 | 40 | module.exports = Header; 41 | -------------------------------------------------------------------------------- /blog-server/src/model/like/like.js: -------------------------------------------------------------------------------- 1 | const { DataTypes } = require("sequelize"); 2 | const { Sequelize } = require("sequelize"); 3 | var moment = require("moment"); 4 | 5 | const seq = require("../../db/seq"); 6 | 7 | const Like = seq.define( 8 | "blog_like", 9 | { 10 | type: { 11 | type: DataTypes.INTEGER, 12 | require: true, 13 | comment: "点赞类型 1 文章 2 说说 3 留言 4 评论", 14 | }, 15 | for_id: { 16 | type: DataTypes.INTEGER, 17 | require: true, 18 | comment: "点赞的id 文章id 说说id 留言id", 19 | }, 20 | user_id: { 21 | type: DataTypes.INTEGER, 22 | require: false, 23 | comment: "点赞用户id", 24 | }, 25 | ip: { 26 | type: DataTypes.STRING, 27 | require: true, 28 | comment: "点赞ip", 29 | }, 30 | createdAt: { 31 | type: Sequelize.DATE, 32 | get() { 33 | return moment(this.getDataValue("createdAt")).format("YYYY-MM-DD HH:mm:ss"); 34 | }, 35 | }, 36 | updatedAt: { 37 | type: Sequelize.DATE, 38 | get() { 39 | return moment(this.getDataValue("updatedAt")).format("YYYY-MM-DD HH:mm:ss"); 40 | }, 41 | }, 42 | }, 43 | { 44 | freezeTableName: true, // 强制表名不转复数 45 | } 46 | ); 47 | 48 | // Like.sync({ alter: true }); //同步数据表 49 | 50 | module.exports = Like; 51 | -------------------------------------------------------------------------------- /blog-server/src/model/links/links.js: -------------------------------------------------------------------------------- 1 | const { DataTypes } = require("sequelize"); 2 | const { Sequelize } = require("sequelize"); 3 | var moment = require("moment"); 4 | 5 | const seq = require("../../db/seq"); 6 | 7 | const Links = seq.define( 8 | "blog_links", 9 | { 10 | site_name: { 11 | type: DataTypes.STRING(55), 12 | require: true, 13 | comment: "网站名称", 14 | }, 15 | site_desc: { 16 | type: DataTypes.STRING, // STRING 默认255 17 | comment: "网站描述", 18 | }, 19 | site_avatar: { 20 | type: DataTypes.STRING(555), // STRING 默认255 21 | comment: "网站头像", 22 | }, 23 | url: { 24 | type: DataTypes.STRING, // STRING 默认255 25 | require: true, 26 | comment: "网站地址", 27 | }, 28 | status: { 29 | type: DataTypes.INTEGER, // STRING 默认255 30 | require: true, 31 | comment: "友链状态 1 待审核 2 审核通过", 32 | }, 33 | user_id: { 34 | type: DataTypes.STRING, // STRING 默认255 35 | require: true, 36 | comment: "申请者id", 37 | }, 38 | createdAt: { 39 | type: Sequelize.DATE, 40 | get() { 41 | return moment(this.getDataValue("createdAt")).format("YYYY-MM-DD HH:mm:ss"); 42 | }, 43 | }, 44 | updatedAt: { 45 | type: Sequelize.DATE, 46 | get() { 47 | return moment(this.getDataValue("updatedAt")).format("YYYY-MM-DD HH:mm:ss"); 48 | }, 49 | }, 50 | }, 51 | { 52 | freezeTableName: true, // 强制表名不转复数 53 | } 54 | ); 55 | 56 | // Links.sync({ alter: true }); //同步数据表 57 | 58 | module.exports = Links; 59 | -------------------------------------------------------------------------------- /blog-server/src/model/notify/notify.js: -------------------------------------------------------------------------------- 1 | const { DataTypes } = require("sequelize"); 2 | const { Sequelize } = require("sequelize"); 3 | var moment = require("moment"); 4 | 5 | const seq = require("../../db/seq"); 6 | 7 | const Message = seq.define( 8 | "blog_notify", 9 | { 10 | message: { 11 | type: DataTypes.STRING(555), 12 | require: true, 13 | comment: "通知内容", 14 | }, 15 | user_id: { 16 | type: DataTypes.INTEGER, // STRING 默认255 17 | require: true, 18 | comment: "通知给谁", 19 | }, 20 | type: { 21 | type: DataTypes.INTEGER, 22 | require: true, 23 | comment: "通知类型 1 文章 2 说说 3 留言 4 友链", 24 | }, 25 | to_id: { 26 | type: DataTypes.INTEGER, // STRING 默认255 27 | comment: "说说或者是文章的id 用于跳转", 28 | }, 29 | isView: { 30 | type: DataTypes.INTEGER, 31 | require: true, 32 | defaultValue: 1, 33 | comment: "是否被查看 1 没有 2 已经查看", 34 | }, 35 | createdAt: { 36 | type: Sequelize.DATE, 37 | get() { 38 | return moment(this.getDataValue("createdAt")).format("YYYY-MM-DD HH:mm:ss"); 39 | }, 40 | }, 41 | updatedAt: { 42 | type: Sequelize.DATE, 43 | get() { 44 | return moment(this.getDataValue("updatedAt")).format("YYYY-MM-DD HH:mm:ss"); 45 | }, 46 | }, 47 | }, 48 | { 49 | freezeTableName: true, // 强制表名不转复数 50 | } 51 | ); 52 | // Message.sync({ alter: true }); //同步数据表 53 | 54 | module.exports = Message; 55 | -------------------------------------------------------------------------------- /blog-server/src/model/photo/photo.js: -------------------------------------------------------------------------------- 1 | const { DataTypes } = require("sequelize") 2 | const { Sequelize } = require("sequelize") 3 | var moment = require("moment") 4 | 5 | const seq = require("../../db/seq") 6 | 7 | const Photo = seq.define( 8 | "blog_photo", 9 | { 10 | album_id: { 11 | type: DataTypes.INTEGER, 12 | require: true, 13 | comment: "相册 id 属于哪个相册", 14 | }, 15 | url: { 16 | type: DataTypes.STRING(555), 17 | require: true, 18 | comment: "图片地址", 19 | }, 20 | status: { 21 | type: DataTypes.INTEGER, 22 | require: true, 23 | defaultValue: 1, 24 | comment: "状态 1 正常 2 回收站", 25 | }, 26 | createdAt: { 27 | type: Sequelize.DATE, 28 | get() { 29 | return moment(this.getDataValue("createdAt")).format("YYYY-MM-DD HH:mm:ss") 30 | }, 31 | }, 32 | updatedAt: { 33 | type: Sequelize.DATE, 34 | get() { 35 | return moment(this.getDataValue("updatedAt")).format("YYYY-MM-DD HH:mm:ss") 36 | }, 37 | }, 38 | }, 39 | { 40 | freezeTableName: true, // 强制表名不转复数 41 | } 42 | ) 43 | 44 | //Photo.sync({ alter: true }) // 同步数据库表 45 | 46 | module.exports = Photo 47 | -------------------------------------------------------------------------------- /blog-server/src/model/photo/photoAlbum.js: -------------------------------------------------------------------------------- 1 | const { DataTypes } = require("sequelize") 2 | const { Sequelize } = require("sequelize") 3 | var moment = require("moment") 4 | 5 | const seq = require("../../db/seq") 6 | 7 | const PhotoAlbum = seq.define( 8 | "blog_photo_album", 9 | { 10 | album_name: { 11 | type: DataTypes.STRING(26), 12 | require: true, 13 | comment: "相册名称", 14 | }, 15 | album_cover: { 16 | type: DataTypes.STRING(555), 17 | require: true, 18 | comment: "相册封面", 19 | }, 20 | description: { 21 | type: DataTypes.STRING(55), 22 | require: true, 23 | comment: "相册描述信息", 24 | }, 25 | createdAt: { 26 | type: Sequelize.DATE, 27 | get() { 28 | return moment(this.getDataValue("createdAt")).format("YYYY-MM-DD HH:mm:ss") 29 | }, 30 | }, 31 | updatedAt: { 32 | type: Sequelize.DATE, 33 | get() { 34 | return moment(this.getDataValue("updatedAt")).format("YYYY-MM-DD HH:mm:ss") 35 | }, 36 | }, 37 | }, 38 | { 39 | freezeTableName: true, // 强制表名不转复数 40 | } 41 | ) 42 | 43 | // PhotoAlbum.sync({ alter: true }) // 同步数据库表 44 | 45 | module.exports = PhotoAlbum 46 | -------------------------------------------------------------------------------- /blog-server/src/model/recommend/recommend.js: -------------------------------------------------------------------------------- 1 | const { DataTypes } = require("sequelize") 2 | const { Sequelize } = require("sequelize") 3 | var moment = require("moment") 4 | 5 | const seq = require("../../db/seq") 6 | 7 | const Recommend = seq.define( 8 | "blog_recommend", 9 | { 10 | title: { 11 | type: DataTypes.STRING(55), 12 | require: true, 13 | comment: "推荐网站标题", 14 | }, 15 | link: { 16 | type: DataTypes.STRING, // STRING 默认255 17 | require: true, 18 | comment: "网站地址", 19 | }, 20 | createdAt: { 21 | type: Sequelize.DATE, 22 | get() { 23 | return moment(this.getDataValue("createdAt")).format("YYYY-MM-DD HH:mm:ss") 24 | }, 25 | }, 26 | updatedAt: { 27 | type: Sequelize.DATE, 28 | get() { 29 | return moment(this.getDataValue("updatedAt")).format("YYYY-MM-DD HH:mm:ss") 30 | }, 31 | }, 32 | }, 33 | { 34 | freezeTableName: true, // 强制表名不转复数 35 | } 36 | ) 37 | 38 | Recommend.sync({ alter: true }) //同步数据表 39 | 40 | module.exports = Recommend 41 | -------------------------------------------------------------------------------- /blog-server/src/model/tag/tag.js: -------------------------------------------------------------------------------- 1 | const { DataTypes } = require("sequelize") 2 | const { Sequelize } = require("sequelize") 3 | var moment = require("moment") 4 | 5 | const seq = require("../../db/seq") 6 | 7 | const Tag = seq.define( 8 | "blog_tag", 9 | { 10 | tag_name: { 11 | type: DataTypes.STRING(55), 12 | require: true, 13 | unique: true, 14 | comment: "标签名称 唯一", 15 | }, 16 | createdAt: { 17 | type: Sequelize.DATE, 18 | get() { 19 | return moment(this.getDataValue("createdAt")).format("YYYY-MM-DD HH:mm:ss") 20 | }, 21 | }, 22 | updatedAt: { 23 | type: Sequelize.DATE, 24 | get() { 25 | return moment(this.getDataValue("updatedAt")).format("YYYY-MM-DD HH:mm:ss") 26 | }, 27 | }, 28 | }, 29 | { 30 | freezeTableName: true, // 强制表名不转复数 31 | } 32 | ) 33 | 34 | // Tag.sync({ alter: true }) // 同步数据库表 35 | 36 | module.exports = Tag 37 | -------------------------------------------------------------------------------- /blog-server/src/model/talk/talk.js: -------------------------------------------------------------------------------- 1 | const { DataTypes } = require("sequelize"); 2 | const { Sequelize } = require("sequelize"); 3 | var moment = require("moment"); 4 | 5 | const seq = require("../../db/seq"); 6 | 7 | const Talk = seq.define( 8 | "blog_talk", 9 | { 10 | content: { 11 | type: DataTypes.STRING(255), 12 | require: true, 13 | comment: "说说内容", 14 | }, 15 | user_id: { 16 | type: DataTypes.INTEGER, 17 | require: true, 18 | comment: "发布说说的用户id", 19 | }, 20 | status: { 21 | type: DataTypes.INTEGER, 22 | require: true, 23 | defaultValue: 1, 24 | comment: "说说状态 1 公开 2 私密 3 回收站", 25 | }, 26 | is_top: { 27 | type: DataTypes.INTEGER, 28 | require: true, 29 | defaultValue: 2, 30 | comment: "是否置顶 1 置顶 2 不置顶", 31 | }, 32 | like_times: { 33 | type: DataTypes.INTEGER, 34 | defaultValue: 0, 35 | comment: "点赞次数", 36 | }, 37 | createdAt: { 38 | type: Sequelize.DATE, 39 | get() { 40 | return moment(this.getDataValue("createdAt")).format("YYYY-MM-DD HH:mm:ss"); 41 | }, 42 | }, 43 | updatedAt: { 44 | type: Sequelize.DATE, 45 | get() { 46 | return moment(this.getDataValue("updatedAt")).format("YYYY-MM-DD HH:mm:ss"); 47 | }, 48 | }, 49 | }, 50 | { 51 | freezeTableName: true, // 强制表名不转复数 52 | } 53 | ); 54 | 55 | // Talk.sync({ alter: true }); // 同步数据库表 56 | 57 | module.exports = Talk; 58 | -------------------------------------------------------------------------------- /blog-server/src/model/talk/talkPhoto.js: -------------------------------------------------------------------------------- 1 | const { DataTypes } = require("sequelize") 2 | const { Sequelize } = require("sequelize") 3 | var moment = require("moment") 4 | 5 | const seq = require("../../db/seq") 6 | 7 | const TalkPhoto = seq.define( 8 | "blog_talk_photo", 9 | { 10 | talk_id: { 11 | type: DataTypes.INTEGER, 12 | require: true, 13 | comment: "说说的id", 14 | }, 15 | url: { 16 | type: DataTypes.STRING(255), 17 | require: true, 18 | comment: "图片地址", 19 | }, 20 | createdAt: { 21 | type: Sequelize.DATE, 22 | get() { 23 | return moment(this.getDataValue("createdAt")).format("YYYY-MM-DD HH:mm:ss") 24 | }, 25 | }, 26 | updatedAt: { 27 | type: Sequelize.DATE, 28 | get() { 29 | return moment(this.getDataValue("updatedAt")).format("YYYY-MM-DD HH:mm:ss") 30 | }, 31 | }, 32 | }, 33 | { 34 | freezeTableName: true, // 强制表名不转复数 35 | } 36 | ) 37 | 38 | // TalkPhoto.sync({ alter: true }) // 同步数据库表 39 | 40 | module.exports = TalkPhoto 41 | -------------------------------------------------------------------------------- /blog-server/src/result/index.js: -------------------------------------------------------------------------------- 1 | const ERRORCODE = { 2 | USER: "100001", // 用户错误码 3 | AUTH: "100002", // 主要是用户权限不足 4 | TAG: "100003", 5 | CATEGORY: "100004", 6 | ARTICLE: "100005", 7 | UPLOAD: "100006", 8 | CONFIG: "100007", 9 | STATISTIC: "100008", 10 | PHOTOALBUM: "100009", 11 | PHOTO: "100010", 12 | TALK: "100011", // 说说 13 | MESSAGE: "100012", // 留言 14 | RECOMMEND: "100012", // 推荐 15 | HEADER: "100013", // 背景图 16 | LINKS: "100014", // 友链 17 | COMMENT: "100015", // 评论 18 | AUTHTOKEN: "100016", // 用户登录过期 19 | NOTIFY: "100017", // 消息推送 20 | LIKE: "100018", // 点赞 21 | CHAT: "100019", // 聊天 22 | TIPS: "111111", // 提示 23 | }; 24 | 25 | /** 26 | * 公共返回结果方法 27 | * @param {*} message 提示信息 28 | * @param {*} result 结果 29 | * @returns 30 | */ 31 | function result(message, result) { 32 | return { 33 | code: 0, 34 | message, 35 | result, 36 | }; 37 | } 38 | 39 | /** 40 | * 公共返回提示方法 41 | * @param {*} message 提示信息 42 | * @param {*} result 结果 43 | * @returns 44 | */ 45 | function tipsResult(message) { 46 | return { 47 | code: 100, 48 | message, 49 | }; 50 | } 51 | 52 | /** 53 | * 公共抛出错误方法 54 | * @param {*} code 错误码 55 | * @param {*} message 错误信息 56 | * @returns 57 | */ 58 | function throwError(code, message) { 59 | return { 60 | code, 61 | message, 62 | }; 63 | } 64 | 65 | module.exports = { 66 | ERRORCODE, 67 | result, 68 | throwError, 69 | tipsResult, 70 | }; 71 | -------------------------------------------------------------------------------- /blog-server/src/router/category.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 分类路由 3 | * @author: M 4 | */ 5 | const Router = require("koa-router"); 6 | const router = new Router({ prefix: "/category" }); 7 | 8 | const { auth, needAdminAuthNotNeedSuper } = require("../middleware/auth/index"); 9 | 10 | const { addCategory, updateCategory, deleteCategories, getCategoryList, getCategoryDictionary } = require("../controller/category/index"); 11 | 12 | const { verifyCategory, verifyDeleteCategories } = require("../middleware/category/category"); 13 | 14 | // 新增分类 15 | router.post("/add", auth, needAdminAuthNotNeedSuper, verifyCategory, addCategory); 16 | 17 | // 修改分类 18 | router.put("/update", auth, needAdminAuthNotNeedSuper, verifyCategory, updateCategory); 19 | 20 | // 删除分类 21 | router.post("/delete", auth, needAdminAuthNotNeedSuper, verifyDeleteCategories, deleteCategories); 22 | 23 | // 条件分页获取分类 24 | router.post("/getCategoryList", getCategoryList); 25 | 26 | // 获取分类简略信息,用于字典反显 27 | router.get("/getCategoryDictionary", getCategoryDictionary); 28 | 29 | module.exports = router; 30 | -------------------------------------------------------------------------------- /blog-server/src/router/chat.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 聊天路由 3 | * @author: M 4 | */ 5 | const Router = require("koa-router"); 6 | const router = new Router({ prefix: "/chat" }); 7 | 8 | const { auth, needAdminAuth } = require("../middleware/auth/index"); 9 | 10 | const { createChat, deleteChats, deleteOneChat, getChatList } = require("../controller/chat/index"); 11 | 12 | // 新增聊天 13 | router.post("/add", auth, createChat); 14 | 15 | // 删除单条聊天 16 | router.delete("/deleteOne/:id", deleteOneChat); 17 | 18 | // 删除聊天 19 | router.post("/delete", auth, needAdminAuth, deleteChats); 20 | 21 | // 条件分页获取聊天 22 | router.post("/getChatList", getChatList); 23 | 24 | module.exports = router; 25 | -------------------------------------------------------------------------------- /blog-server/src/router/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 网站设置路由 3 | * @author: M 4 | */ 5 | const Router = require("koa-router"); 6 | const router = new Router({ prefix: "/config" }); 7 | 8 | const { auth, needAdminAuth } = require("../middleware/auth/index"); 9 | 10 | const { updateConfig, getConfig, addView } = require("../controller/utils/index"); 11 | const { createTimesLimiter } = require("../middleware/limit-request/index"); 12 | 13 | // 修改网站设置 14 | router.post("/update", auth, needAdminAuth, updateConfig); 15 | 16 | // 获取网站设置 17 | router.get("/", getConfig); 18 | 19 | // 修改网站设置的访问次数 20 | router.put( 21 | "/addView", 22 | createTimesLimiter({ 23 | prefixKey: "put/addView", 24 | message: "访问网站过于频繁 请稍后再试", 25 | interval: { 26 | min: 60, 27 | }, 28 | max: 100, 29 | }), 30 | addView 31 | ); 32 | 33 | module.exports = router; 34 | -------------------------------------------------------------------------------- /blog-server/src/router/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: M 3 | * @Date: 2023-03-01 16:23:19 4 | * @Description: 路由公共注册 5 | * @LastEditTime: 2023-03-02 17:05:41 6 | * @LastEditors: M 7 | */ 8 | 9 | const fs = require("fs"); // 文件模块 10 | const Router = require("koa-router"); 11 | 12 | const router = new Router(); 13 | 14 | fs.readdirSync(__dirname).forEach((file) => { 15 | if (file !== "index.js") { 16 | let r = require("./" + file); 17 | router.use(r.routes()); 18 | } 19 | }); 20 | 21 | // 随便写的一个欢迎 22 | router.get("/", (ctx, next) => { 23 | ctx.body = "欢迎 这是后台server首页"; 24 | }); 25 | 26 | module.exports = router; 27 | -------------------------------------------------------------------------------- /blog-server/src/router/like.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 记录用户点赞信息路由 点赞次数是直接记录到对应的文章、说说、留言内的 3 | * @author: M 4 | */ 5 | const Router = require("koa-router"); 6 | const router = new Router({ prefix: "/like" }); 7 | 8 | const { addLike, cancelLike, getIsLikeByIdOrIpAndType } = require("../controller/like/index"); 9 | const { createTimesLimiter } = require("../middleware/limit-request/index"); 10 | 11 | // 点赞 12 | router.post( 13 | "/addLike", 14 | createTimesLimiter({ 15 | prefixKey: "post/like/addLike", 16 | message: "点赞过于频繁 请稍后再试", 17 | max: 10, 18 | }), 19 | addLike 20 | ); 21 | 22 | // 取消点赞 23 | router.post( 24 | "/cancelLike", 25 | createTimesLimiter({ 26 | prefixKey: "post/like/cancelLike", 27 | message: "取消点赞过于频繁 请稍后再试", 28 | max: 10, 29 | }), 30 | cancelLike 31 | ); 32 | 33 | // 获取当前用户对当前文章/说说/留言 是否点赞 34 | router.post("/getIsLikeByIdOrIpAndType", getIsLikeByIdOrIpAndType); 35 | 36 | module.exports = router; 37 | -------------------------------------------------------------------------------- /blog-server/src/router/links.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 友链的路由 3 | * @author: M 4 | */ 5 | const Router = require("koa-router"); 6 | const router = new Router({ prefix: "/links" }); 7 | 8 | const { auth, needAdminAuthNotNeedSuper, needAdminAuth } = require("../middleware/auth/index"); 9 | const { addOrUpdateLinks, deleteLinks, approveLinks, getLinksList, frontUpdateLinks } = require("../controller/links/index"); 10 | const { createTimesLimiter } = require("../middleware/limit-request/index"); 11 | 12 | // 新增友链 13 | router.post( 14 | "/add", 15 | createTimesLimiter({ 16 | prefixKey: "post/like/cancelLike", 17 | message: "新增过于频繁 请稍后再试", 18 | max: 5, 19 | }), 20 | addOrUpdateLinks 21 | ); 22 | 23 | // 博客前台修改友链 24 | router.post("/frontUpdate", frontUpdateLinks); 25 | 26 | // 博客后台修改友链 27 | router.post("/backUpdate", auth, needAdminAuthNotNeedSuper, addOrUpdateLinks); 28 | 29 | // 删除友链 30 | router.put("/delete", auth, needAdminAuthNotNeedSuper, deleteLinks); 31 | 32 | // 批量审核友链 33 | router.put("/approve", auth, needAdminAuthNotNeedSuper, approveLinks); 34 | 35 | // 分页获取友链 36 | router.post("/getLinksList", getLinksList); 37 | 38 | module.exports = router; 39 | -------------------------------------------------------------------------------- /blog-server/src/router/notify.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 消息推送路由 3 | * @author: M 4 | */ 5 | const Router = require("koa-router"); 6 | const router = new Router({ prefix: "/notify" }); 7 | 8 | 9 | const { updateNotify, deleteNotifys, getNotifyList } = require("../controller/notify/index"); 10 | 11 | // 修改消息推送 12 | router.put("/update/:id", updateNotify); 13 | 14 | // 删除消息推送 15 | router.put("/delete/:id", deleteNotifys); 16 | 17 | // 条件分页获取消息推送 18 | router.post("/getNotifyList", getNotifyList); 19 | 20 | module.exports = router; 21 | -------------------------------------------------------------------------------- /blog-server/src/router/pageHeader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 页面头部背景图片的路由 3 | * @author: M 4 | */ 5 | const Router = require("koa-router"); 6 | const router = new Router({ prefix: "/pageHeader" }); 7 | 8 | const { auth, needAdminAuth } = require("../middleware/auth/index"); 9 | const { addOrUpdateHeader, deleteHeader, getAllHeader } = require("../controller/header/index"); 10 | 11 | // 根据是否有id来判断新增/编辑背景 12 | router.post("/addOrUpdate", auth, needAdminAuth, addOrUpdateHeader); 13 | 14 | // 删除背景 15 | router.post("/delete", auth, needAdminAuth, deleteHeader); 16 | 17 | // 根据相册id 获取相册的所有图片 18 | router.get("/getAll", getAllHeader); 19 | 20 | module.exports = router; 21 | -------------------------------------------------------------------------------- /blog-server/src/router/photo.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 关于相册内图片的路由 3 | * @author: M 4 | */ 5 | const Router = require("koa-router"); 6 | const router = new Router({ prefix: "/photo" }); 7 | 8 | const { auth, needAdminAuthNotNeedSuper } = require("../middleware/auth/index"); 9 | const { addPhotos, deletePhotos, getPhotosByAlbumId, revertPhotos, getAllPhotosByAlbumId } = require("../controller/photo/index"); 10 | 11 | // 批量新增图片 判断是否有id来新增 并且需要传入相册id记录相册 12 | router.post("/add", auth, needAdminAuthNotNeedSuper, addPhotos); 13 | 14 | // 批量删除图片 15 | router.put("/delete", auth, needAdminAuthNotNeedSuper, deletePhotos); 16 | 17 | // 批量恢复文章 18 | router.put("/revert", auth, needAdminAuthNotNeedSuper, revertPhotos); 19 | 20 | // 根据相册id 分页获取图片列表 21 | router.post("/getPhotoListByAlbumId", getPhotosByAlbumId); 22 | 23 | // 根据相册id 获取相册的所有图片 24 | router.get("/getAllPhotosByAlbumId/:id", getAllPhotosByAlbumId); 25 | 26 | module.exports = router; 27 | -------------------------------------------------------------------------------- /blog-server/src/router/photoAlbum.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 关于相册的路由 3 | * @author: M 4 | */ 5 | const Router = require("koa-router") 6 | const router = new Router({ prefix: "/photoAlbum" }) 7 | 8 | const { auth, needAdminAuthNotNeedSuper } = require("../middleware/auth/index") 9 | const { addAlbum, deleteAlbum, updateAlbum, getAlbumList, getAllAlbumList } = require("../controller/photoAlbum/index") 10 | 11 | // 新增相册 12 | router.post("/add", auth, needAdminAuthNotNeedSuper, addAlbum) 13 | 14 | // 删除相册 15 | router.delete("/delete/:id", auth, needAdminAuthNotNeedSuper, deleteAlbum) 16 | 17 | // 修改相册 18 | router.put("/update", auth, needAdminAuthNotNeedSuper, updateAlbum) 19 | 20 | // 分页获取相册列表 21 | router.post("/", getAlbumList) 22 | 23 | // 一次性获取所有的相册列表 24 | router.get("/getAllAlbumList", getAllAlbumList) 25 | 26 | module.exports = router 27 | -------------------------------------------------------------------------------- /blog-server/src/router/statistic.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 数据统计路由 3 | * @author: M 4 | */ 5 | const Router = require("koa-router") 6 | const router = new Router({ prefix: "/statistic" }) 7 | 8 | const { homeGetStatistic } = require("../controller/statistic/index") 9 | 10 | // 获取数据统计 11 | router.get("/", homeGetStatistic) 12 | 13 | module.exports = router 14 | -------------------------------------------------------------------------------- /blog-server/src/router/tag.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 标签路由 3 | * @author: M 4 | */ 5 | const Router = require("koa-router") 6 | const router = new Router({ prefix: "/tag" }) 7 | 8 | const { auth, needAdminAuth } = require("../middleware/auth/index") 9 | 10 | const { addTag, updateTag, deleteTags, getTagList, getTagDictionary } = require("../controller/tag/index") 11 | 12 | const { verifyTag, verifyDeleteTags } = require("../middleware/tag/tag") 13 | 14 | // 新增标签 15 | router.post("/add", auth, needAdminAuth, verifyTag, addTag) 16 | 17 | // 修改标签 18 | router.put("/update", auth, needAdminAuth, verifyTag, updateTag) 19 | 20 | // 删除标签 21 | router.post("/delete", auth, needAdminAuth, verifyDeleteTags, deleteTags) 22 | 23 | // 条件分页获取标签 24 | router.post("/getTagList", getTagList) 25 | 26 | // 获取标签简略信息,用于字典反显 27 | router.get("/getTagDictionary", getTagDictionary) 28 | 29 | module.exports = router 30 | -------------------------------------------------------------------------------- /blog-server/src/router/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 上传路由 3 | * @author: M 4 | */ 5 | 6 | const Router = require("koa-router"); 7 | const router = new Router({ prefix: "/upload" }); 8 | 9 | const { upload } = require("../controller/utils/index"); 10 | const { createTimesLimiter } = require("../middleware/limit-request/index"); 11 | 12 | // 图片上传 13 | router.post( 14 | "/img", 15 | createTimesLimiter({ 16 | prefixKey: "post/img", 17 | message: "上传图片过于频繁 请稍后再试", 18 | max: 100, 19 | }), 20 | upload 21 | ); 22 | 23 | module.exports = router; 24 | -------------------------------------------------------------------------------- /blog-server/src/service/config/index.js: -------------------------------------------------------------------------------- 1 | const Config = require("../../model/config/config"); 2 | /** 3 | * 网站设置服务层 4 | */ 5 | class ConfigService { 6 | async updateConfig(config) { 7 | const { id } = config; 8 | let one = await Config.findByPk(id); 9 | 10 | let res; 11 | if (one) { 12 | res = await Config.update(config, { 13 | where: { 14 | id, 15 | }, 16 | }); 17 | } else { 18 | res = await Config.create(config); 19 | } 20 | 21 | return res ? true : false; 22 | } 23 | 24 | async getConfig() { 25 | let res = await Config.findAll(); 26 | // 这里不能反悔 res[0].dataValues 因为dataValues不能格式化时间 27 | return res.length ? res[0] : false; 28 | } 29 | 30 | async addView() { 31 | let res = await Config.findAll(); 32 | let flag = false, 33 | config; 34 | if (res.length) { 35 | config = await Config.findByPk(res[0].dataValues.id); 36 | if (config) { 37 | config.increment(["view_time"], { by: 1 }); 38 | flag = "添加成功"; 39 | } 40 | } else { 41 | flag = "需要初始化"; 42 | } 43 | 44 | return flag; 45 | } 46 | } 47 | 48 | module.exports = new ConfigService(); 49 | -------------------------------------------------------------------------------- /blog-server/src/service/header/index.js: -------------------------------------------------------------------------------- 1 | const Header = require("../../model/header/header"); 2 | /** 3 | * 头部背景图服务层 4 | */ 5 | class HeaderService { 6 | /** 7 | * 新增 / 修改 背景 8 | */ 9 | async addOrUpdateHeader({ id, route_name, bg_url }) { 10 | let res; 11 | if (id) { 12 | res = await Header.update( 13 | { route_name, bg_url }, 14 | { 15 | where: { 16 | id, 17 | }, 18 | } 19 | ); 20 | } else { 21 | res = await Header.create({ route_name, bg_url }); 22 | } 23 | 24 | return res ? true : false; 25 | } 26 | 27 | /** 28 | * 根据id删除背景 29 | * @param {*} id 30 | * @returns 31 | */ 32 | async deleteHeader(id) { 33 | let res = await Header.destroy({ 34 | where: { 35 | id, 36 | }, 37 | }); 38 | 39 | return res ? res : null; 40 | } 41 | 42 | /** 43 | * 获取所有背景 44 | */ 45 | async getAllHeader() { 46 | let header = await Header.findAll({ 47 | attributes: ["id", "route_name", "bg_url"], 48 | }); 49 | 50 | return header; 51 | } 52 | 53 | /** 54 | * 根据 55 | */ 56 | async getOneByPath(route_name) { 57 | let header = await Header.findOne({ 58 | where: { 59 | route_name, 60 | }, 61 | }); 62 | 63 | return header ? header.dataValues : null; 64 | } 65 | } 66 | 67 | module.exports = new HeaderService(); 68 | -------------------------------------------------------------------------------- /blog-server/src/service/like/index.js: -------------------------------------------------------------------------------- 1 | const Like = require("../../model/like/like"); 2 | /** 3 | * 头部背景图服务层 4 | */ 5 | class LikeService { 6 | /** 7 | * 点赞 8 | */ 9 | async addLike({ for_id, type, user_id, ip }) { 10 | let res; 11 | 12 | // 只有没点赞的用户才可以点赞 13 | res = await Like.create({ for_id, type, user_id, ip }); 14 | 15 | return res ? true : false; 16 | } 17 | 18 | /** 19 | * 根据for_id、type、user_id取消点赞 20 | */ 21 | async cancelLike({ for_id, type, user_id, ip }) { 22 | let whereOpt = { 23 | for_id, 24 | type, 25 | }; 26 | ip && (whereOpt.ip = ip); 27 | user_id && (whereOpt.user_id = user_id); 28 | let res = await Like.destroy({ 29 | where: whereOpt, 30 | }); 31 | 32 | return res ? res : null; 33 | } 34 | 35 | /** 36 | * 获取当前用户对当前文章/说说/留言 是否点赞 37 | */ 38 | async getIsLikeByIdAndType({ for_id, type, user_id }) { 39 | let like = await Like.findAll({ 40 | where: { 41 | for_id, 42 | type, 43 | user_id, 44 | }, 45 | }); 46 | 47 | return like.length ? true : false; 48 | } 49 | 50 | /** 51 | * 获取当前ip对当前文章/说说/留言 是否点赞 52 | */ 53 | async getIsLikeByIpAndType({ for_id, type, ip }) { 54 | let like = await Like.findAll({ 55 | where: { 56 | for_id, 57 | type, 58 | ip, 59 | }, 60 | }); 61 | 62 | return like.length ? true : false; 63 | } 64 | } 65 | 66 | module.exports = new LikeService(); 67 | -------------------------------------------------------------------------------- /blog-server/src/service/notify/index.js: -------------------------------------------------------------------------------- 1 | const Notify = require("../../model/notify/notify"); 2 | 3 | /** 4 | * 消息通知服务层 5 | */ 6 | class NotifyService { 7 | /** 8 | * 新增消息通知 9 | * @returns Boolean 10 | */ 11 | async createNotify(notify) { 12 | const { user_id, type, to_id, message } = notify; 13 | const res = await Notify.create({ user_id, type, to_id, message }); 14 | 15 | return res.dataValues; 16 | } 17 | 18 | /** 19 | * 已阅消息通知 20 | * @param {*} id 21 | * @returns Boolean 22 | */ 23 | async updateNotify(id) { 24 | const res = await Notify.update({ isView: 2 }, { where: { id } }); 25 | 26 | return res[0] > 0 ? true : false; 27 | } 28 | 29 | /** 30 | * 删除消息通知 31 | * @param {*} id 32 | */ 33 | async deleteNotifys(id) { 34 | const res = await Notify.destroy({ 35 | where: { 36 | id, 37 | }, 38 | }); 39 | 40 | return res; 41 | } 42 | 43 | /** 44 | * 获取当前用户的消息推送 45 | */ 46 | async getNotifyList({ current, size, userId }) { 47 | const whereOpt = {}; 48 | const offset = (current - 1) * size; 49 | const limit = size * 1; 50 | 51 | userId && 52 | Object.assign(whereOpt, { 53 | user_id: userId, 54 | }); 55 | 56 | const { count, rows } = await Notify.findAndCountAll({ 57 | offset, 58 | limit, 59 | where: whereOpt, 60 | order: [["isView", "ASC"], ["createdAt", "DESC"]] 61 | }); 62 | 63 | return { 64 | current: current, 65 | size: size, 66 | total: count, 67 | list: rows, 68 | }; 69 | } 70 | } 71 | 72 | module.exports = new NotifyService(); 73 | -------------------------------------------------------------------------------- /blog-server/src/upload/online/35a148158b323e72b22f7ab01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrzym99/vue3-blog/42c6177d254d2760180d08b1d560d083642291ea/blog-server/src/upload/online/35a148158b323e72b22f7ab01.png -------------------------------------------------------------------------------- /blog-server/src/upload/online/9bb507f4bd065759a3d093d04.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrzym99/vue3-blog/42c6177d254d2760180d08b1d560d083642291ea/blog-server/src/upload/online/9bb507f4bd065759a3d093d04.webp -------------------------------------------------------------------------------- /blog-server/src/upload/online/alipay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrzym99/vue3-blog/42c6177d254d2760180d08b1d560d083642291ea/blog-server/src/upload/online/alipay.png -------------------------------------------------------------------------------- /blog-server/src/upload/online/zhifupay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrzym99/vue3-blog/42c6177d254d2760180d08b1d560d083642291ea/blog-server/src/upload/online/zhifupay.png -------------------------------------------------------------------------------- /blog-server/src/utils/sensitive.js: -------------------------------------------------------------------------------- 1 | const { Mint } = require("mint-filter"); 2 | const request = require("request"); 3 | 4 | const sArr = ["我是你爸爸", "我是你爸", "我是你爹", "爸爸", "我是你爷爷", "操你奶奶", "我是你妈", "我日你爸", "草泥马", "草你妈", "操你妈", "傻逼"]; 5 | const badJs = /script|alert|window|prompt|location|href|iframe|onload|onerror/g; 6 | 7 | async function filterSensitive(text) { 8 | // 过滤敏感词 9 | const mint = new Mint(sArr); 10 | 11 | let res = mint.filter(text).text; 12 | if (res.indexOf("*") != -1 || badJs.test(text)) { 13 | res = await getSaying(); 14 | return res; 15 | } else { 16 | return res; 17 | } 18 | } 19 | 20 | function getSaying() { 21 | return new Promise((resolve, reject) => { 22 | request("https://open.iciba.com/dsapi/", function (error, response, body) { 23 | if (!error && response.statusCode == 200) { 24 | res = JSON.parse(body).note; 25 | resolve(res); 26 | } 27 | }); 28 | }); 29 | } 30 | 31 | module.exports = filterSensitive; 32 | -------------------------------------------------------------------------------- /blog-server/src/utils/swagger.js: -------------------------------------------------------------------------------- 1 | const { APP_PORT } = require("../config/config.default"); 2 | const router = require("../router"); 3 | 4 | const swaggerJSDoc = require("swagger-jsdoc"); 5 | const path = require("path"); 6 | const swaggerDefinition = { 7 | //swagger-ui显示的基本信息,如标题、版本、描述 8 | info: { 9 | title: "小张的个人博客接口文档", 10 | version: "1.0.0", 11 | description: "API文档", 12 | }, 13 | // 安全设置,支持apikey的方式直接定义http请求头 14 | securityDefinitions: { 15 | bearerAuth: { 16 | type: "apiKey", 17 | name: "Authorization", 18 | in: "header", 19 | }, 20 | }, 21 | // 声明后,每个api右上角会出现一个小锁,支持输入API Key 22 | security: [ 23 | { 24 | bearerAuth: [], 25 | }, 26 | ], 27 | host: "localhost:" + APP_PORT, // 想着改这里,如果不修改,那么接口文档访问地址为:localhost:3000/swagger 28 | // 根路由 29 | basePath: "/", // Base path (optional) 30 | schemes: ["http", "https"], 31 | }; 32 | const options = { 33 | swaggerDefinition, 34 | apis: [path.join(__dirname, "../router/*.js")], // 写有注解的router的存放地址, 最好path.join() 35 | }; 36 | const swaggerSpec = swaggerJSDoc(options); 37 | // 通过路由获取生成的注解文件 38 | router.get("/swagger.json", async function (ctx) { 39 | ctx.set("Content-Type", "application/json"); 40 | ctx.body = swaggerSpec; 41 | }); 42 | module.exports = router; 43 | -------------------------------------------------------------------------------- /blog-server/src/utils/tool.js: -------------------------------------------------------------------------------- 1 | const ipnet = require("xz-ipnet")(); 2 | 3 | /** 4 | * 随机生成昵称 5 | * @param {*} prefix 前缀 6 | * @param {*} randomLength 长度 7 | * @returns nickname 8 | */ 9 | function randomNickname(prefix, randomLength) { 10 | // 兼容更低版本的默认值写法 11 | prefix === undefined ? (prefix = "") : prefix; 12 | randomLength === undefined ? (randomLength = 8) : randomLength; 13 | 14 | // 设置随机用户名 15 | // 用户名随机词典数组 16 | let nameArr = [ 17 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 0], 18 | ["a", "b", "c", "d", "e", "f", "g", "h", "i", "g", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"], 19 | ]; 20 | // 随机名字字符串 21 | let name = prefix; 22 | // 循环遍历从用户词典中随机抽出一个 23 | for (var i = 0; i < randomLength; i++) { 24 | // 随机生成index 25 | let index = Math.floor(Math.random() * 2); 26 | let zm = nameArr[index][Math.floor(Math.random() * nameArr[index].length)]; 27 | // 拼接进名字变量中 28 | name += zm; 29 | } 30 | // 将随机生成的名字返回 31 | return name; 32 | } 33 | 34 | function getIpAddress(ip) { 35 | const arr = ipnet.find(ip); 36 | 37 | if (!arr) { 38 | return "本机地址"; 39 | } 40 | return arr[1] || arr[0]; 41 | } 42 | 43 | // 获取当前type类型数字的公共方法 44 | function getCurrentTypeName(type) { 45 | let res = 0; 46 | switch (type + '') { 47 | case "1": 48 | res = "文章"; 49 | break; 50 | case "2": 51 | res = "说说"; 52 | break; 53 | case "3": 54 | res = "留言"; 55 | break; 56 | } 57 | 58 | return res; 59 | } 60 | // 判断网址是否带http / https 61 | function isValidUrl(url) { 62 | 63 | return url.indexOf('http') != -1 || url.indexOf('https') != -1 64 | } 65 | 66 | module.exports = { 67 | isValidUrl, 68 | getIpAddress, 69 | randomNickname, 70 | getCurrentTypeName 71 | }; 72 | -------------------------------------------------------------------------------- /blog-server/src/views/error.ejs: -------------------------------------------------------------------------------- 1 |

<%= message %>

2 |

<%= error.status %>

3 |
<%= error.stack %>
4 | -------------------------------------------------------------------------------- /blog-server/src/views/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= title %> 5 | 6 | 7 | 8 |

<%= title %>

9 |

EJS Welcome to <%= title %>

10 | 11 | 12 | -------------------------------------------------------------------------------- /blog-server/test/test.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrzym99/vue3-blog/42c6177d254d2760180d08b1d560d083642291ea/blog-server/test/test.js -------------------------------------------------------------------------------- /blog-v3-admin/.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | insert_final_newline = false 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /blog-v3-admin/.env: -------------------------------------------------------------------------------- 1 | # 平台本地运行端口号 2 | VITE_PORT = 8848 3 | -------------------------------------------------------------------------------- /blog-v3-admin/.env.development: -------------------------------------------------------------------------------- 1 | # 平台本地运行端口号 2 | VITE_PORT = 8848 3 | 4 | # 开发环境读取配置文件路径 5 | VITE_PUBLIC_PATH = ./ 6 | 7 | # 开发环境路由历史模式(Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数") 8 | VITE_ROUTER_HISTORY = "hash" 9 | -------------------------------------------------------------------------------- /blog-v3-admin/.env.production: -------------------------------------------------------------------------------- 1 | # 线上环境平台打包路径 2 | VITE_PUBLIC_PATH = ./ 3 | 4 | # 线上环境路由历史模式(Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数") 5 | VITE_ROUTER_HISTORY = "hash" 6 | 7 | # 是否在打包时使用cdn替换本地库 替换 true 不替换 false 8 | VITE_CDN = false 9 | 10 | # 是否启用gzip压缩或brotli压缩(分两种情况,删除原始文件和不删除原始文件) 11 | # 压缩时不删除原始文件的配置:gzip、brotli、both(同时开启 gzip 与 brotli 压缩)、none(不开启压缩,默认) 12 | # 压缩时删除原始文件的配置:gzip-clear、brotli-clear、both-clear(同时开启 gzip 与 brotli 压缩)、none(不开启压缩,默认) 13 | VITE_COMPRESSION = "none" -------------------------------------------------------------------------------- /blog-v3-admin/.env.staging: -------------------------------------------------------------------------------- 1 | # 预发布也需要生产环境的行为 2 | # https://cn.vitejs.dev/guide/env-and-mode.html#modes 3 | NODE_ENV=production 4 | 5 | VITE_PUBLIC_PATH = ./ 6 | 7 | # 预发布环境路由历史模式(Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数") 8 | VITE_ROUTER_HISTORY = "hash" 9 | 10 | # 是否在打包时使用cdn替换本地库 替换 true 不替换 false 11 | VITE_CDN = true 12 | 13 | # 是否启用gzip压缩或brotli压缩(分两种情况,删除原始文件和不删除原始文件) 14 | # 压缩时不删除原始文件的配置:gzip、brotli、both(同时开启 gzip 与 brotli 压缩)、none(不开启压缩,默认) 15 | # 压缩时删除原始文件的配置:gzip-clear、brotli-clear、both-clear(同时开启 gzip 与 brotli 压缩)、none(不开启压缩,默认) 16 | VITE_COMPRESSION = "both-clear" 17 | -------------------------------------------------------------------------------- /blog-v3-admin/.eslintignore: -------------------------------------------------------------------------------- 1 | public 2 | dist 3 | *.d.ts 4 | /src/assets 5 | package.json 6 | .eslintrc.js 7 | .prettierrc.js 8 | commitlint.config.js 9 | postcss.config.js 10 | tailwind.config.js 11 | stylelint.config.js 12 | /src/utils/auth.ts 13 | /types/global.d.ts -------------------------------------------------------------------------------- /blog-v3-admin/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | .eslintcache 7 | report.html 8 | 9 | yarn.lock 10 | npm-debug.log* 11 | .pnpm-error.log* 12 | .pnpm-debug.log 13 | tests/**/coverage/ 14 | 15 | # Editor directories and files 16 | .idea 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | tsconfig.tsbuildinfo -------------------------------------------------------------------------------- /blog-v3-admin/.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # shellcheck source=./_/husky.sh 4 | . "$(dirname "$0")/_/husky.sh" 5 | 6 | npx --no-install commitlint --edit "$1" 7 | -------------------------------------------------------------------------------- /blog-v3-admin/.husky/common.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | command_exists () { 3 | command -v "$1" >/dev/null 2>&1 4 | } 5 | 6 | # Workaround for Windows 10, Git Bash and Pnpm 7 | if command_exists winpty && test -t 1; then 8 | exec < /dev/tty 9 | fi 10 | -------------------------------------------------------------------------------- /blog-v3-admin/.husky/lintstagedrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "*.{js,jsx,ts,tsx}": ["eslint --fix", "prettier --write"], 3 | "{!(package)*.json}": ["prettier --write--parser json"], 4 | "package.json": ["prettier --write"], 5 | "*.vue": ["eslint --fix", "prettier --write", "stylelint --fix"], 6 | "*.{vue,css,scss,postcss,less}": ["stylelint --fix", "prettier --write"], 7 | "*.md": ["prettier --write"] 8 | }; 9 | -------------------------------------------------------------------------------- /blog-v3-admin/.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | . "$(dirname "$0")/common.sh" 4 | 5 | [ -n "$CI" ] && exit 0 6 | 7 | # Format and submit code according to lintstagedrc.js configuration 8 | npm run lint:lint-staged 9 | 10 | npm run lint:pretty 11 | -------------------------------------------------------------------------------- /blog-v3-admin/.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": true, 3 | "MD003": false, 4 | "MD033": false, 5 | "MD013": false, 6 | "MD001": false, 7 | "MD025": false, 8 | "MD024": false, 9 | "MD007": { "indent": 4 }, 10 | "no-hard-tabs": false 11 | } 12 | -------------------------------------------------------------------------------- /blog-v3-admin/.npmrc: -------------------------------------------------------------------------------- 1 | 2 | shamefully-hoist=true 3 | strict-peer-dependencies=false 4 | shell-emulator=true 5 | # https://registry.npmmirror.com/ 淘宝 6 | # https://registry.npmjs.org/ 官方 7 | registry=https://registry.npmmirror.com/ 8 | 9 | 10 | -------------------------------------------------------------------------------- /blog-v3-admin/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | bracketSpacing: true, 3 | singleQuote: false, 4 | arrowParens: "avoid", 5 | trailingComma: "none" 6 | }; 7 | -------------------------------------------------------------------------------- /blog-v3-admin/.stylelintignore: -------------------------------------------------------------------------------- 1 | /dist/* 2 | /public/* 3 | public/* -------------------------------------------------------------------------------- /blog-v3-admin/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "christian-kohler.path-intellisense", 4 | "vscode-icons-team.vscode-icons", 5 | "davidanson.vscode-markdownlint", 6 | "stylelint.vscode-stylelint", 7 | "bradlc.vscode-tailwindcss", 8 | "dbaeumer.vscode-eslint", 9 | "esbenp.prettier-vscode", 10 | "redhat.vscode-yaml", 11 | "csstools.postcss", 12 | "mikestead.dotenv", 13 | "eamodio.gitlens", 14 | "antfu.iconify", 15 | "Vue.volar" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /blog-v3-admin/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnType": true, 3 | "editor.formatOnSave": true, 4 | "[vue]": { 5 | "editor.defaultFormatter": "esbenp.prettier-vscode" 6 | }, 7 | "editor.tabSize": 2, 8 | "editor.formatOnPaste": true, 9 | "editor.guides.bracketPairs": "active", 10 | "files.autoSave": "afterDelay", 11 | "git.confirmSync": false, 12 | "workbench.startupEditor": "newUntitledFile", 13 | "editor.suggestSelection": "first", 14 | "editor.acceptSuggestionOnCommitCharacter": false, 15 | "css.lint.propertyIgnoredDueToDisplay": "ignore", 16 | "editor.quickSuggestions": { 17 | "other": true, 18 | "comments": true, 19 | "strings": true 20 | }, 21 | "files.associations": { 22 | "editor.snippetSuggestions": "top" 23 | }, 24 | "[css]": { 25 | "editor.defaultFormatter": "esbenp.prettier-vscode" 26 | }, 27 | "editor.codeActionsOnSave": { 28 | "source.fixAll.eslint": "explicit" 29 | }, 30 | "iconify.excludes": ["el"] 31 | } 32 | -------------------------------------------------------------------------------- /blog-v3-admin/.vscode/vue3.0.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | "Vue3.0快速生成模板": { 3 | "prefix": "Vue3.0", 4 | "body": [ 5 | "\n", 9 | "\n", 16 | "", 18 | "$2" 19 | ], 20 | "description": "Vue3.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /blog-v3-admin/.vscode/vue3.2.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | "Vue3.2+快速生成模板": { 3 | "prefix": "Vue3.2+", 4 | "body": [ 5 | "\n", 7 | "\n", 11 | "", 13 | "$2" 14 | ], 15 | "description": "Vue3.2+" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /blog-v3-admin/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 | -------------------------------------------------------------------------------- /blog-v3-admin/build/cdn.ts: -------------------------------------------------------------------------------- 1 | import { Plugin as importToCDN } from "vite-plugin-cdn-import"; 2 | 3 | /** 4 | * @description 打包时采用`cdn`模式,仅限外网使用(默认不采用,如果需要采用cdn模式,请在 .env.production 文件,将 VITE_CDN 设置成true) 5 | * 平台采用国内cdn:https://www.bootcdn.cn,当然你也可以选择 https://unpkg.com 或者 https://www.jsdelivr.com 6 | * 提醒:mockjs不能用cdn模式引入,会报错。正确的方式是,生产环境删除mockjs,使用真实的后端请求 7 | * 注意:上面提到的仅限外网使用也不是完全肯定的,如果你们公司内网部署的有相关js、css文件,也可以将下面配置对应改一下,整一套内网版cdn 8 | */ 9 | export const cdn = importToCDN({ 10 | //(prodUrl解释: name: 对应下面modules的name,version: 自动读取本地package.json中dependencies依赖中对应包的版本号,path: 对应下面modules的path,当然也可写完整路径,会替换prodUrl) 11 | prodUrl: "https://cdn.bootcdn.net/ajax/libs/{name}/{version}/{path}", 12 | modules: [ 13 | { 14 | name: "vue", 15 | var: "Vue", 16 | path: "vue.global.prod.min.js" 17 | }, 18 | { 19 | name: "vue-router", 20 | var: "VueRouter", 21 | path: "vue-router.global.min.js" 22 | }, 23 | // 项目中没有直接安装vue-demi,但是pinia用到了,所以需要在引入pinia前引入vue-demi(https://github.com/vuejs/pinia/blob/v2/packages/pinia/package.json#L77) 24 | { 25 | name: "vue-demi", 26 | var: "VueDemi", 27 | path: "index.iife.min.js" 28 | }, 29 | { 30 | name: "pinia", 31 | var: "Pinia", 32 | path: "pinia.iife.min.js" 33 | }, 34 | { 35 | name: "element-plus", 36 | var: "ElementPlus", 37 | path: "index.full.min.js", 38 | css: "index.min.css" 39 | }, 40 | { 41 | name: "axios", 42 | var: "axios", 43 | path: "axios.min.js" 44 | }, 45 | { 46 | name: "dayjs", 47 | var: "dayjs", 48 | path: "dayjs.min.js" 49 | }, 50 | { 51 | name: "echarts", 52 | var: "echarts", 53 | path: "echarts.min.js" 54 | } 55 | ] 56 | }); 57 | -------------------------------------------------------------------------------- /blog-v3-admin/build/index.ts: -------------------------------------------------------------------------------- 1 | /** 处理环境变量 */ 2 | const warpperEnv = (envConf: Recordable): ViteEnv => { 3 | /** 此处为默认值 */ 4 | const ret: ViteEnv = { 5 | VITE_PORT: 8848, 6 | VITE_PUBLIC_PATH: "", 7 | VITE_ROUTER_HISTORY: "", 8 | VITE_CDN: false, 9 | VITE_COMPRESSION: "none" 10 | }; 11 | 12 | for (const envName of Object.keys(envConf)) { 13 | let realName = envConf[envName].replace(/\\n/g, "\n"); 14 | realName = 15 | realName === "true" ? true : realName === "false" ? false : realName; 16 | 17 | if (envName === "VITE_PORT") { 18 | realName = Number(realName); 19 | } 20 | ret[envName] = realName; 21 | if (typeof realName === "string") { 22 | process.env[envName] = realName; 23 | } else if (typeof realName === "object") { 24 | process.env[envName] = JSON.stringify(realName); 25 | } 26 | } 27 | return ret; 28 | }; 29 | 30 | export { warpperEnv }; 31 | -------------------------------------------------------------------------------- /blog-v3-admin/build/info.ts: -------------------------------------------------------------------------------- 1 | import type { Plugin } from "vite"; 2 | import dayjs, { Dayjs } from "dayjs"; 3 | import duration from "dayjs/plugin/duration"; 4 | import { green, blue, bold } from "picocolors"; 5 | import { getPackageSize } from "@pureadmin/utils"; 6 | dayjs.extend(duration); 7 | 8 | export function viteBuildInfo(): Plugin { 9 | let config: { command: string }; 10 | let startTime: Dayjs; 11 | let endTime: Dayjs; 12 | let outDir: string; 13 | return { 14 | name: "vite:buildInfo", 15 | configResolved(resolvedConfig) { 16 | config = resolvedConfig; 17 | outDir = resolvedConfig.build?.outDir ?? "dist"; 18 | }, 19 | buildStart() { 20 | console.log( 21 | bold( 22 | green( 23 | `👏欢迎使用${blue( 24 | "[vue-pure-admin]" 25 | )},如果您感觉不错,记得点击后面链接给个star哦💖 https://github.com/pure-admin/vue-pure-admin` 26 | ) 27 | ) 28 | ); 29 | if (config.command === "build") { 30 | startTime = dayjs(new Date()); 31 | } 32 | }, 33 | closeBundle() { 34 | if (config.command === "build") { 35 | endTime = dayjs(new Date()); 36 | getPackageSize({ 37 | folder: outDir, 38 | callback: (size: string) => { 39 | console.log( 40 | bold( 41 | green( 42 | `🎉恭喜打包完成(总用时${dayjs 43 | .duration(endTime.diff(startTime)) 44 | .format("mm分ss秒")},打包后的大小为${size})` 45 | ) 46 | ) 47 | ); 48 | } 49 | }); 50 | } 51 | } 52 | }; 53 | } 54 | -------------------------------------------------------------------------------- /blog-v3-admin/build/optimize.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 此文件作用于 `vite.config.ts` 的 `optimizeDeps.include` 依赖预构建配置项 3 | * 依赖预构建,`vite` 启动时会将下面 include 里的模块,编译成 esm 格式并缓存到 node_modules/.vite 文件夹,页面加载到对应模块时如果浏览器有缓存就读取浏览器缓存,如果没有会读取本地缓存并按需加载 4 | * 尤其当您禁用浏览器缓存时(这种情况只应该发生在调试阶段)必须将对应模块加入到 include里,否则会遇到开发环境切换页面卡顿的问题(vite 会认为它是一个新的依赖包会重新加载并强制刷新页面),因为它既无法使用浏览器缓存,又没有在本地 node_modules/.vite 里缓存 5 | * 温馨提示:如果您使用的第三方库是全局引入,也就是引入到 src/main.ts 文件里,就不需要再添加到 include 里了,因为 vite 会自动将它们缓存到 node_modules/.vite 6 | */ 7 | const include = [ 8 | "qs", 9 | "mitt", 10 | "dayjs", 11 | "axios", 12 | "pinia", 13 | "echarts", 14 | "js-cookie", 15 | "@vueuse/core", 16 | "@pureadmin/utils", 17 | "responsive-storage", 18 | "element-resize-detector" 19 | ]; 20 | 21 | /** 22 | * 在预构建中强制排除的依赖项 23 | * 温馨提示:所有以 `@iconify-icons/` 开头引入的的本地图标模块,都应该加入到下面的 `exclude` 里,因为平台推荐的使用方式是哪里需要哪里引入而且都是单个的引入,不需要预构建,直接让浏览器加载就好 24 | */ 25 | const exclude = [ 26 | "@iconify-icons/ep", 27 | "@iconify-icons/ri", 28 | "@pureadmin/theme/dist/browser-utils" 29 | ]; 30 | 31 | export { include, exclude }; 32 | -------------------------------------------------------------------------------- /blog-v3-admin/commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ignores: [commit => commit.includes("init")], 3 | extends: ["@commitlint/config-conventional"], 4 | rules: { 5 | "body-leading-blank": [2, "always"], 6 | "footer-leading-blank": [1, "always"], 7 | "header-max-length": [2, "always", 108], 8 | "subject-empty": [2, "never"], 9 | "type-empty": [2, "never"], 10 | "type-enum": [ 11 | 2, 12 | "always", 13 | [ 14 | "feat", 15 | "fix", 16 | "perf", 17 | "style", 18 | "docs", 19 | "test", 20 | "refactor", 21 | "build", 22 | "ci", 23 | "chore", 24 | "revert", 25 | "wip", 26 | "workflow", 27 | "types", 28 | "release" 29 | ] 30 | ] 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /blog-v3-admin/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | "postcss-import": {}, 4 | tailwindcss: {}, 5 | autoprefixer: {}, 6 | ...(process.env.NODE_ENV === "production" ? { cssnano: {} } : {}) 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /blog-v3-admin/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrzym99/vue3-blog/42c6177d254d2760180d08b1d560d083642291ea/blog-v3-admin/public/favicon.ico -------------------------------------------------------------------------------- /blog-v3-admin/public/serverConfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "3.9.7", 3 | "Title": "小张的博客后台", 4 | "FixedHeader": true, 5 | "HiddenSideBar": false, 6 | "MultiTagsCache": false, 7 | "KeepAlive": true, 8 | "Layout": "vertical", 9 | "Theme": "default", 10 | "DarkMode": false, 11 | "Grey": false, 12 | "Weak": false, 13 | "HideTabs": false, 14 | "SidebarStatus": true, 15 | "EpThemeColor": "#409EFF", 16 | "ShowLogo": true, 17 | "ShowModel": "smart", 18 | "MenuArrowIconNoTransition": true, 19 | "CachingAsyncRoutes": false, 20 | "TooltipEffect": "light" 21 | } 22 | -------------------------------------------------------------------------------- /blog-v3-admin/server.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | 3 | const app = express(); 4 | 5 | const port = 8080; // 自定义端口号(不要与已存在端口冲突) 6 | 7 | app.use(express.static("dist")); // dist 是项目的打包资源路径 8 | 9 | app.listen(port, () => console.log(`服务器 ${port} 开启成功!`)); 10 | -------------------------------------------------------------------------------- /blog-v3-admin/src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 34 | -------------------------------------------------------------------------------- /blog-v3-admin/src/api/category.ts: -------------------------------------------------------------------------------- 1 | import { http } from "@/utils/http"; 2 | 3 | export type CategoryResult = { 4 | code: number; 5 | message: string; 6 | result: any; 7 | }; 8 | 9 | /** 获取分类列表 */ 10 | export const getCategoryList = (data?: object) => { 11 | return http.request("post", "/api/category/getCategoryList", { 12 | data 13 | }); 14 | }; 15 | 16 | /** 新增分类 */ 17 | export const addCategory = (data?: object) => { 18 | return http.request("post", "/api/category/add", { data }); 19 | }; 20 | 21 | /** 修改分类 */ 22 | export const editCategory = (data?: object) => { 23 | return http.request("put", "/api/category/update", { data }); 24 | }; 25 | 26 | /** 删除分类 */ 27 | export const deleteCategoryList = (data?: object) => { 28 | return http.request("post", "/api/category/delete", { data }); 29 | }; 30 | 31 | /** 获取分类字典 */ 32 | export const getCategoryDictionary = () => { 33 | return http.request( 34 | "get", 35 | "/api/Category/getCategoryDictionary", 36 | {} 37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /blog-v3-admin/src/api/home.ts: -------------------------------------------------------------------------------- 1 | import { http } from "@/utils/http"; 2 | 3 | export type homeResult = { 4 | code: number; 5 | message: string; 6 | result: any; 7 | }; 8 | 9 | /** 获取数据统计 用户、标签、分类、文章数 */ 10 | export const getStatistic = () => { 11 | return http.request("get", "/api/statistic", {}); 12 | }; 13 | -------------------------------------------------------------------------------- /blog-v3-admin/src/api/links.ts: -------------------------------------------------------------------------------- 1 | import { http } from "@/utils/http"; 2 | 3 | export type LinksResult = { 4 | code: number; 5 | message: string; 6 | result: any; 7 | }; 8 | 9 | /** 获取友链列表 */ 10 | export const getLinksList = (data?: object) => { 11 | return http.request("post", "/api/links/getLinksList", { data }); 12 | }; 13 | 14 | /** 后台修改友链 */ 15 | export const updateLinks = (data?: object) => { 16 | return http.request("post", "/api/links/backUpdate", { data }); 17 | }; 18 | 19 | /** 审核友链 */ 20 | export const approveLinks = (data?: object) => { 21 | return http.request("put", "/api/links/approve", { data }); 22 | }; 23 | 24 | /** 删除友链 */ 25 | export const deleteLinks = (data?: object) => { 26 | return http.request("put", "/api/links/delete", { data }); 27 | }; 28 | -------------------------------------------------------------------------------- /blog-v3-admin/src/api/message.ts: -------------------------------------------------------------------------------- 1 | import { http } from "@/utils/http"; 2 | 3 | export type MessageResult = { 4 | code: number; 5 | message: string; 6 | result: any; 7 | }; 8 | 9 | /** 获取留言列表 */ 10 | export const getMessageList = (data?: object) => { 11 | return http.request("post", "/api/message/getMessageList", { 12 | data 13 | }); 14 | }; 15 | 16 | /** 删除留言 */ 17 | export const deleteMessage = (data?: object) => { 18 | return http.request("put", "/api/message/backDelete", { 19 | data 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /blog-v3-admin/src/api/pageheader.ts: -------------------------------------------------------------------------------- 1 | import { http } from "@/utils/http"; 2 | 3 | export type pageResult = { 4 | code: number; 5 | message: string; 6 | result: any; 7 | }; 8 | 9 | /** 获取所有背景 */ 10 | export const getPageHeaderList = (data?: object) => { 11 | return http.request("get", "/api/pageHeader/getAll", { data }); 12 | }; 13 | 14 | /** 新增/修改背景 */ 15 | export const addOrUpdatePageHeader = (data?: object) => { 16 | return http.request("post", "/api/pageHeader/addOrUpdate", { 17 | data 18 | }); 19 | }; 20 | 21 | /** 删除背景 */ 22 | export const deletePageHeader = data => { 23 | return http.request("post", "/api/pageHeader/delete", { data }); 24 | }; 25 | -------------------------------------------------------------------------------- /blog-v3-admin/src/api/photo.ts: -------------------------------------------------------------------------------- 1 | import { http } from "@/utils/http"; 2 | 3 | export type photoResult = { 4 | code: number; 5 | message: string; 6 | result: any; 7 | }; 8 | 9 | /** 新增相册 */ 10 | export const addAlbum = data => { 11 | return http.request("post", "/api/photoAlbum/add", { data }); 12 | }; 13 | 14 | /** 修改相册 */ 15 | export const updateAlbum = data => { 16 | return http.request("put", "/api/photoAlbum/update", { data }); 17 | }; 18 | 19 | /** 分页获取相册 */ 20 | export const getAlbumList = data => { 21 | return http.request("post", "/api/photoAlbum", { data }); 22 | }; 23 | 24 | /** 删除相册 */ 25 | export const deleteAlbum = id => { 26 | return http.request( 27 | "delete", 28 | "/api/photoAlbum/delete/" + id, 29 | {} 30 | ); 31 | }; 32 | 33 | /** 批量新增图片 */ 34 | export const addPhotos = data => { 35 | return http.request("post", "/api/photo/add", { data }); 36 | }; 37 | 38 | /** 分页获取相册的所有图片 */ 39 | export const getPhotoListByAlbumId = data => { 40 | return http.request("post", "/api/photo/getPhotoListByAlbumId", { 41 | data 42 | }); 43 | }; 44 | 45 | /** 批量恢复图片 */ 46 | export const revertPhotos = data => { 47 | return http.request("put", "/api/photo/revert", { data }); 48 | }; 49 | 50 | /** 批量删除图片 */ 51 | export const deletePhotos = data => { 52 | return http.request("put", "/api/photo/delete", { data }); 53 | }; 54 | -------------------------------------------------------------------------------- /blog-v3-admin/src/api/tag.ts: -------------------------------------------------------------------------------- 1 | import { http } from "@/utils/http"; 2 | 3 | export type TagResult = { 4 | code: number; 5 | message: string; 6 | result: any; 7 | }; 8 | 9 | /** 获取标签列表 */ 10 | export const getTagList = (data?: object) => { 11 | return http.request("post", "/api/tag/getTagList", { data }); 12 | }; 13 | 14 | /** 新增标签 */ 15 | export const addTag = (data?: object) => { 16 | return http.request("post", "/api/tag/add", { data }); 17 | }; 18 | 19 | /** 修改标签 */ 20 | export const editTag = (data?: object) => { 21 | return http.request("put", "/api/tag/update", { data }); 22 | }; 23 | 24 | /** 删除标签 */ 25 | export const deleteTagList = (data?: object) => { 26 | return http.request("post", "/api/tag/delete", { data }); 27 | }; 28 | 29 | /** 获取标签字典 */ 30 | export const getTagDictionary = () => { 31 | return http.request("get", "/api/tag/getTagDictionary", {}); 32 | }; 33 | -------------------------------------------------------------------------------- /blog-v3-admin/src/api/talk.ts: -------------------------------------------------------------------------------- 1 | import { http } from "@/utils/http"; 2 | 3 | export type TalkResult = { 4 | code: number; 5 | message: string; 6 | result: any; 7 | }; 8 | 9 | /** 获取说说列表 */ 10 | export const getTalkList = (data?: object) => { 11 | return http.request("post", "/api/talk/getTalkList", { data }); 12 | }; 13 | 14 | /** 新增说说 */ 15 | export const addTalk = (data?: object) => { 16 | return http.request("post", "/api/talk/publishTalk", { data }); 17 | }; 18 | 19 | /** 修改说说 */ 20 | export const editTalk = (data?: object) => { 21 | return http.request("put", "/api/talk/updateTalk", { data }); 22 | }; 23 | 24 | /** 删除说说 */ 25 | export const deleteTalkById = (id, status) => { 26 | return http.request( 27 | "delete", 28 | `/api/talk/deleteTalkById/${id}/${status}`, 29 | {} 30 | ); 31 | }; 32 | 33 | /** 公开 / 私密说说 */ 34 | export const togglePublic = (id, status) => { 35 | return http.request( 36 | "put", 37 | `/api/talk/togglePublic/${id}/${status}`, 38 | {} 39 | ); 40 | }; 41 | 42 | /** 置顶 / 取消置顶说说 */ 43 | export const toggleTop = (id, is_top) => { 44 | return http.request( 45 | "put", 46 | `/api/talk/toggleTop/${id}/${is_top}`, 47 | {} 48 | ); 49 | }; 50 | 51 | /** 恢复说说 */ 52 | export const revertTalk = id => { 53 | return http.request("put", `/api/talk/revertTalk/${id}`, {}); 54 | }; 55 | /** 获取说说详情 */ 56 | export const getTalkById = id => { 57 | return http.request("get", `/api/talk/getTalkById/${id}`, {}); 58 | }; 59 | -------------------------------------------------------------------------------- /blog-v3-admin/src/api/user.ts: -------------------------------------------------------------------------------- 1 | import { http } from "@/utils/http"; 2 | 3 | export type UserResult = { 4 | code: number; 5 | message: String; 6 | result: { 7 | token: string; // token 8 | username: string; // 用户名 9 | role: number; // 用户角色 10 | id: number; // 用户id 11 | }; 12 | }; 13 | 14 | export type Result = { 15 | code: number; 16 | message: string; 17 | result: any; 18 | }; 19 | 20 | /** 登录 */ 21 | export const getLogin = (data?: object) => { 22 | return http.request("post", "/api/user/login", { data }); 23 | }; 24 | 25 | /** 注册 */ 26 | export const registerUser = (data?: object) => { 27 | return http.request("post", "/api/user/register", { data }); 28 | }; 29 | 30 | /** 用户修改个人信息 */ 31 | export const updateUserInfo = (data?: object) => { 32 | return http.request("put", "/api/user/updateOwnUserInfo", { data }); 33 | }; 34 | 35 | /** 用户修改密码 */ 36 | export const updateUserPassword = (data?: object) => { 37 | return http.request("put", "/api/user/updatePassword", { data }); 38 | }; 39 | 40 | /** 管理员修改用户角色 */ 41 | export const updateUserRole = (id, role) => { 42 | return http.request("put", `/api/user/updateRole/${id}/${role}`, {}); 43 | }; 44 | 45 | /** 管理员修改用户信息 */ 46 | export const adminUpdateUserInfo = data => { 47 | return http.request("put", "/api/user/adminUpdateUserInfo", { data }); 48 | }; 49 | 50 | /** 条件分页获取用户信息 */ 51 | export const getUserList = (data?: object) => { 52 | return http.request("post", "/api/user/getUserList", { data }); 53 | }; 54 | 55 | /** 获取当前登录人的信息 */ 56 | export const getUserInfoById = id => { 57 | return http.request("get", `/api/user/getUserInfoById/` + id, {}); 58 | }; 59 | -------------------------------------------------------------------------------- /blog-v3-admin/src/assets/iconfont/iconfont.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "iconfont"; /* Project id 2208059 */ 3 | src: url("iconfont.woff2?t=1638023560828") format("woff2"), 4 | url("iconfont.woff?t=1638023560828") format("woff"), 5 | url("iconfont.ttf?t=1638023560828") format("truetype"); 6 | } 7 | 8 | .iconfont { 9 | font-family: "iconfont" !important; 10 | font-size: 16px; 11 | font-style: normal; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | } 15 | 16 | .team-icontabs::before { 17 | content: "\e63e"; 18 | } 19 | 20 | .team-iconlogo::before { 21 | content: "\e620"; 22 | } 23 | 24 | .team-iconxinpin::before { 25 | content: "\e614"; 26 | } 27 | 28 | .team-iconxinpinrenqiwang::before { 29 | content: "\e615"; 30 | } 31 | 32 | .team-iconexit-fullscreen::before { 33 | content: "\e62a"; 34 | } 35 | 36 | .team-iconfullscreen::before { 37 | content: "\e62b"; 38 | } 39 | -------------------------------------------------------------------------------- /blog-v3-admin/src/assets/iconfont/iconfont.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "2208059", 3 | "name": "pure-admin", 4 | "font_family": "iconfont", 5 | "css_prefix_text": "team-icon", 6 | "description": "pure-admin", 7 | "glyphs": [ 8 | { 9 | "icon_id": "20594647", 10 | "name": "标签页", 11 | "font_class": "tabs", 12 | "unicode": "e63e", 13 | "unicode_decimal": 58942 14 | }, 15 | { 16 | "icon_id": "22129506", 17 | "name": "水能", 18 | "font_class": "logo", 19 | "unicode": "e620", 20 | "unicode_decimal": 58912 21 | }, 22 | { 23 | "icon_id": "7795613", 24 | "name": "新品", 25 | "font_class": "xinpin", 26 | "unicode": "e614", 27 | "unicode_decimal": 58900 28 | }, 29 | { 30 | "icon_id": "7795615", 31 | "name": "新品人气王", 32 | "font_class": "xinpinrenqiwang", 33 | "unicode": "e615", 34 | "unicode_decimal": 58901 35 | }, 36 | { 37 | "icon_id": "5698509", 38 | "name": "全屏缩小", 39 | "font_class": "exit-fullscreen", 40 | "unicode": "e62a", 41 | "unicode_decimal": 58922 42 | }, 43 | { 44 | "icon_id": "5698510", 45 | "name": "全屏显示", 46 | "font_class": "fullscreen", 47 | "unicode": "e62b", 48 | "unicode_decimal": 58923 49 | } 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /blog-v3-admin/src/assets/iconfont/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrzym99/vue3-blog/42c6177d254d2760180d08b1d560d083642291ea/blog-v3-admin/src/assets/iconfont/iconfont.ttf -------------------------------------------------------------------------------- /blog-v3-admin/src/assets/iconfont/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrzym99/vue3-blog/42c6177d254d2760180d08b1d560d083642291ea/blog-v3-admin/src/assets/iconfont/iconfont.woff -------------------------------------------------------------------------------- /blog-v3-admin/src/assets/iconfont/iconfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrzym99/vue3-blog/42c6177d254d2760180d08b1d560d083642291ea/blog-v3-admin/src/assets/iconfont/iconfont.woff2 -------------------------------------------------------------------------------- /blog-v3-admin/src/assets/img/bathymetry_bw_composite_4k.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrzym99/vue3-blog/42c6177d254d2760180d08b1d560d083642291ea/blog-v3-admin/src/assets/img/bathymetry_bw_composite_4k.jpg -------------------------------------------------------------------------------- /blog-v3-admin/src/assets/img/clouds.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrzym99/vue3-blog/42c6177d254d2760180d08b1d560d083642291ea/blog-v3-admin/src/assets/img/clouds.png -------------------------------------------------------------------------------- /blog-v3-admin/src/assets/img/earth.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrzym99/vue3-blog/42c6177d254d2760180d08b1d560d083642291ea/blog-v3-admin/src/assets/img/earth.jpg -------------------------------------------------------------------------------- /blog-v3-admin/src/assets/img/night.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrzym99/vue3-blog/42c6177d254d2760180d08b1d560d083642291ea/blog-v3-admin/src/assets/img/night.jpg -------------------------------------------------------------------------------- /blog-v3-admin/src/assets/img/starfield.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrzym99/vue3-blog/42c6177d254d2760180d08b1d560d083642291ea/blog-v3-admin/src/assets/img/starfield.jpg -------------------------------------------------------------------------------- /blog-v3-admin/src/assets/login/avatar.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blog-v3-admin/src/assets/login/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrzym99/vue3-blog/42c6177d254d2760180d08b1d560d083642291ea/blog-v3-admin/src/assets/login/bg.png -------------------------------------------------------------------------------- /blog-v3-admin/src/assets/svg/back_top.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blog-v3-admin/src/assets/svg/biaoqian.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blog-v3-admin/src/assets/svg/dark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blog-v3-admin/src/assets/svg/day.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blog-v3-admin/src/assets/svg/enter_outlined.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blog-v3-admin/src/assets/svg/exit_screen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blog-v3-admin/src/assets/svg/fenlei.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blog-v3-admin/src/assets/svg/full_screen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blog-v3-admin/src/assets/svg/keyboard_esc.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blog-v3-admin/src/assets/svg/wenzhang.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blog-v3-admin/src/assets/svg/yonghu.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blog-v3-admin/src/assets/svg/zhiding.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blog-v3-admin/src/components/HomeCard/home-card.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 27 | 28 | 47 | -------------------------------------------------------------------------------- /blog-v3-admin/src/components/ReAuth/index.ts: -------------------------------------------------------------------------------- 1 | import auth from "./src/auth"; 2 | 3 | const Auth = auth; 4 | 5 | export { Auth }; 6 | -------------------------------------------------------------------------------- /blog-v3-admin/src/components/ReAuth/src/auth.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent, Fragment } from "vue"; 2 | import { hasAuth } from "@/router/utils"; 3 | 4 | export default defineComponent({ 5 | name: "Auth", 6 | props: { 7 | value: { 8 | type: undefined, 9 | default: [] 10 | } 11 | }, 12 | setup(props, { slots }) { 13 | return () => { 14 | if (!slots) return null; 15 | return hasAuth(props.value) ? ( 16 | {slots.default?.()} 17 | ) : null; 18 | }; 19 | } 20 | }); 21 | -------------------------------------------------------------------------------- /blog-v3-admin/src/components/ReIcon/index.ts: -------------------------------------------------------------------------------- 1 | import iconifyIconOffline from "./src/iconifyIconOffline"; 2 | import iconifyIconOnline from "./src/iconifyIconOnline"; 3 | import fontIcon from "./src/iconfont"; 4 | 5 | /** 本地图标组件 */ 6 | const IconifyIconOffline = iconifyIconOffline; 7 | /** 在线图标组件 */ 8 | const IconifyIconOnline = iconifyIconOnline; 9 | /** iconfont组件 */ 10 | const FontIcon = fontIcon; 11 | 12 | export { IconifyIconOffline, IconifyIconOnline, FontIcon }; 13 | -------------------------------------------------------------------------------- /blog-v3-admin/src/components/ReIcon/src/iconfont.ts: -------------------------------------------------------------------------------- 1 | import { h, defineComponent } from "vue"; 2 | 3 | // 封装iconfont组件,默认`font-class`引用模式,支持`unicode`引用、`font-class`引用、`symbol`引用 (https://www.iconfont.cn/help/detail?spm=a313x.7781069.1998910419.20&helptype=code) 4 | export default defineComponent({ 5 | name: "FontIcon", 6 | props: { 7 | icon: { 8 | type: String, 9 | default: "" 10 | } 11 | }, 12 | render() { 13 | const attrs = this.$attrs; 14 | if (Object.keys(attrs).includes("uni") || attrs?.iconType === "uni") { 15 | return h( 16 | "i", 17 | { 18 | class: "iconfont", 19 | ...attrs 20 | }, 21 | this.icon 22 | ); 23 | } else if ( 24 | Object.keys(attrs).includes("svg") || 25 | attrs?.iconType === "svg" 26 | ) { 27 | return h( 28 | "svg", 29 | { 30 | class: "icon-svg", 31 | "aria-hidden": true 32 | }, 33 | { 34 | default: () => [ 35 | h("use", { 36 | "xlink:href": `#${this.icon}` 37 | }) 38 | ] 39 | } 40 | ); 41 | } else { 42 | return h("i", { 43 | class: `iconfont ${this.icon}`, 44 | ...attrs 45 | }); 46 | } 47 | } 48 | }); 49 | -------------------------------------------------------------------------------- /blog-v3-admin/src/components/ReIcon/src/iconifyIconOffline.ts: -------------------------------------------------------------------------------- 1 | import { h, defineComponent } from "vue"; 2 | import { Icon as IconifyIcon, addIcon } from "@iconify/vue/dist/offline"; 3 | 4 | // Iconify Icon在Vue里本地使用(用于内网环境)https://docs.iconify.design/icon-components/vue/offline.html 5 | export default defineComponent({ 6 | name: "IconifyIconOffline", 7 | components: { IconifyIcon }, 8 | props: { 9 | icon: { 10 | default: null 11 | } 12 | }, 13 | render() { 14 | if (typeof this.icon === "object") addIcon(this.icon, this.icon); 15 | const attrs = this.$attrs; 16 | return h( 17 | IconifyIcon, 18 | { 19 | icon: this.icon, 20 | style: attrs?.style 21 | ? Object.assign(attrs.style, { outline: "none" }) 22 | : { outline: "none" }, 23 | ...attrs 24 | }, 25 | { 26 | default: () => [] 27 | } 28 | ); 29 | } 30 | }); 31 | -------------------------------------------------------------------------------- /blog-v3-admin/src/components/ReIcon/src/iconifyIconOnline.ts: -------------------------------------------------------------------------------- 1 | import { h, defineComponent } from "vue"; 2 | import { Icon as IconifyIcon } from "@iconify/vue"; 3 | 4 | // Iconify Icon在Vue里在线使用(用于外网环境) 5 | export default defineComponent({ 6 | name: "IconifyIconOnline", 7 | components: { IconifyIcon }, 8 | props: { 9 | icon: { 10 | type: String, 11 | default: "" 12 | } 13 | }, 14 | render() { 15 | const attrs = this.$attrs; 16 | return h( 17 | IconifyIcon, 18 | { 19 | icon: `${this.icon}`, 20 | style: attrs?.style 21 | ? Object.assign(attrs.style, { outline: "none" }) 22 | : { outline: "none" }, 23 | ...attrs 24 | }, 25 | { 26 | default: () => [] 27 | } 28 | ); 29 | } 30 | }); 31 | -------------------------------------------------------------------------------- /blog-v3-admin/src/components/ReIcon/src/offlineIcon.ts: -------------------------------------------------------------------------------- 1 | import { addIcon } from "@iconify/vue/dist/offline"; 2 | 3 | /** 4 | * 这里存放本地图标,在 src/layout/index.vue 文件中加载,避免在首启动加载 5 | */ 6 | 7 | // 本地菜单图标,后端在路由的icon中返回对应的图标字符串并且前端在此处使用addIcon添加即可渲染菜单图标 8 | import HomeFilled from "@iconify-icons/ep/home-filled"; 9 | import InformationLine from "@iconify-icons/ri/information-line"; 10 | import Lollipop from "@iconify-icons/ep/lollipop"; 11 | import List from "@iconify-icons/ep/list"; 12 | import Setting from "@iconify-icons/ep/setting"; 13 | import User from "@iconify-icons/ep/avatar"; 14 | import userFilled from "@iconify-icons/ep/user-filled"; 15 | 16 | addIcon("homeFilled", HomeFilled); 17 | addIcon("informationLine", InformationLine); 18 | addIcon("lollipop", Lollipop); 19 | addIcon("list", List); 20 | addIcon("setting", Setting); 21 | addIcon("user", User); 22 | addIcon("userFilled", userFilled); 23 | -------------------------------------------------------------------------------- /blog-v3-admin/src/components/ReIcon/src/types.ts: -------------------------------------------------------------------------------- 1 | export interface iconType { 2 | // iconify (https://docs.iconify.design/icon-components/vue/#properties) 3 | inline?: boolean; 4 | width?: string | number; 5 | height?: string | number; 6 | horizontalFlip?: boolean; 7 | verticalFlip?: boolean; 8 | flip?: string; 9 | rotate?: number | string; 10 | color?: string; 11 | horizontalAlign?: boolean; 12 | verticalAlign?: boolean; 13 | align?: string; 14 | onLoad?: Function; 15 | includes?: Function; 16 | 17 | // all icon 18 | style?: object; 19 | } 20 | -------------------------------------------------------------------------------- /blog-v3-admin/src/components/ReTypeit/index.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 19 | -------------------------------------------------------------------------------- /blog-v3-admin/src/config/index.ts: -------------------------------------------------------------------------------- 1 | import { App } from "vue"; 2 | import axios from "axios"; 3 | 4 | let config: object = {}; 5 | const { VITE_PUBLIC_PATH } = import.meta.env; 6 | 7 | const setConfig = (cfg?: unknown) => { 8 | config = Object.assign(config, cfg); 9 | }; 10 | 11 | const getConfig = (key?: string): ServerConfigs => { 12 | if (typeof key === "string") { 13 | const arr = key.split("."); 14 | if (arr && arr.length) { 15 | let data = config; 16 | arr.forEach(v => { 17 | if (data && typeof data[v] !== "undefined") { 18 | data = data[v]; 19 | } else { 20 | data = null; 21 | } 22 | }); 23 | return data; 24 | } 25 | } 26 | return config; 27 | }; 28 | 29 | /** 获取项目动态全局配置 */ 30 | export const getServerConfig = async (app: App): Promise => { 31 | app.config.globalProperties.$config = getConfig(); 32 | return axios({ 33 | method: "get", 34 | url: `${VITE_PUBLIC_PATH}serverConfig.json` 35 | }) 36 | .then(({ data: config }) => { 37 | let $config = app.config.globalProperties.$config; 38 | // 自动注入项目配置 39 | if (app && $config && typeof config === "object") { 40 | $config = Object.assign($config, config); 41 | app.config.globalProperties.$config = $config; 42 | // 设置全局配置 43 | setConfig($config); 44 | } 45 | return $config; 46 | }) 47 | .catch(() => { 48 | throw "请在public文件夹下添加serverConfig.json配置文件"; 49 | }); 50 | }; 51 | 52 | export { getConfig, setConfig }; 53 | -------------------------------------------------------------------------------- /blog-v3-admin/src/directives/auth/index.ts: -------------------------------------------------------------------------------- 1 | import { hasAuth } from "@/router/utils"; 2 | import { Directive, type DirectiveBinding } from "vue"; 3 | 4 | export const auth: Directive = { 5 | mounted(el: HTMLElement, binding: DirectiveBinding) { 6 | const { value } = binding; 7 | if (value) { 8 | !hasAuth(value) && el.parentNode?.removeChild(el); 9 | } else { 10 | throw new Error("need auths! Like v-auth=\"['btn.add','btn.edit']\""); 11 | } 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /blog-v3-admin/src/directives/elResizeDetector/index.ts: -------------------------------------------------------------------------------- 1 | import { Directive, type DirectiveBinding, type VNode } from "vue"; 2 | import elementResizeDetectorMaker from "element-resize-detector"; 3 | import type { Erd } from "element-resize-detector"; 4 | import { emitter } from "@/utils/mitt"; 5 | 6 | const erd: Erd = elementResizeDetectorMaker({ 7 | strategy: "scroll" 8 | }); 9 | 10 | export const resize: Directive = { 11 | mounted(el: HTMLElement, binding?: DirectiveBinding, vnode?: VNode) { 12 | erd.listenTo(el, elem => { 13 | const width = elem.offsetWidth; 14 | const height = elem.offsetHeight; 15 | if (binding?.instance) { 16 | emitter.emit("resize", { detail: { width, height } }); 17 | } else { 18 | vnode.el.dispatchEvent( 19 | new CustomEvent("resize", { detail: { width, height } }) 20 | ); 21 | } 22 | }); 23 | }, 24 | unmounted(el: HTMLElement) { 25 | erd.uninstall(el); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /blog-v3-admin/src/directives/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./auth"; 2 | export * from "./elResizeDetector"; 3 | -------------------------------------------------------------------------------- /blog-v3-admin/src/layout/components/notice/noticeList.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 24 | -------------------------------------------------------------------------------- /blog-v3-admin/src/layout/components/screenfull/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | -------------------------------------------------------------------------------- /blog-v3-admin/src/layout/components/search/components/SearchFooter.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 25 | 26 | 45 | -------------------------------------------------------------------------------- /blog-v3-admin/src/layout/components/search/components/index.ts: -------------------------------------------------------------------------------- 1 | import SearchModal from "./SearchModal.vue"; 2 | 3 | export { SearchModal }; 4 | -------------------------------------------------------------------------------- /blog-v3-admin/src/layout/components/search/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 21 | -------------------------------------------------------------------------------- /blog-v3-admin/src/layout/components/sidebar/topCollapse.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 34 | -------------------------------------------------------------------------------- /blog-v3-admin/src/layout/frameView.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 |