├── .changeset └── config.json ├── .dockerignore ├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── improve.md └── workflows │ ├── express-server.yml │ ├── nest-server.yml │ ├── release.yml │ ├── vite-vue3.yml │ └── webpack-vue3.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .lintstagedrc ├── .npmrc ├── .prettierignore ├── .prettierrc.json ├── .volta └── hooks.json ├── .vscode ├── extensions.json └── settings.json ├── .yarnrc ├── Dockerfile ├── Dockerfile.dev ├── LICENSE ├── README.md ├── app ├── cra-react18 │ ├── .env │ ├── .env.production │ ├── .eslintignore │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── .prettierrc.cjs │ ├── .stylelintrc.cjs │ ├── CHANGELOG.md │ ├── README.md │ ├── babel.config.js │ ├── config │ │ ├── env.js │ │ ├── getHttpsConfig.js │ │ ├── jest │ │ │ ├── babelTransform.js │ │ │ ├── cssTransform.js │ │ │ └── fileTransform.js │ │ ├── modules.js │ │ ├── paths.js │ │ ├── webpack.config.js │ │ ├── webpack │ │ │ └── persistentCache │ │ │ │ └── createEnvironmentHash.js │ │ └── webpackDevServer.config.js │ ├── jest.config.js │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── manifest.json │ │ └── robots.txt │ ├── scripts │ │ ├── build.js │ │ ├── start.js │ │ └── test.js │ ├── src │ │ ├── App.test.tsx │ │ ├── App.tsx │ │ ├── assets │ │ │ └── img │ │ │ │ ├── avatar.jpg │ │ │ │ ├── chat-avatar.png │ │ │ │ ├── comment-avatar.svg │ │ │ │ ├── default.png │ │ │ │ ├── default_category.svg │ │ │ │ ├── logo.png │ │ │ │ ├── logo2.png │ │ │ │ ├── reply-avatar.svg │ │ │ │ └── wechat_payme.jpg │ │ ├── bean │ │ │ ├── base.ts │ │ │ ├── dto.ts │ │ │ └── xhr.ts │ │ ├── components │ │ │ ├── BaseLayout │ │ │ │ ├── BaseFooter.tsx │ │ │ │ ├── BaseMenu.tsx │ │ │ │ ├── HotColumn.tsx │ │ │ │ └── index.tsx │ │ │ ├── BottomTips │ │ │ │ └── index.tsx │ │ │ ├── CardArticle │ │ │ │ └── index.tsx │ │ │ ├── CardComment │ │ │ │ ├── comment-style-context.ts │ │ │ │ └── index.tsx │ │ │ ├── CommentUserInfoForm │ │ │ │ └── index.tsx │ │ │ ├── Comments │ │ │ │ └── index.tsx │ │ │ ├── IconSvg │ │ │ │ └── index.tsx │ │ │ └── LazyImage │ │ │ │ └── index.tsx │ │ ├── hooks │ │ │ ├── async.ts │ │ │ └── scroll.ts │ │ ├── index.tsx │ │ ├── jshashes.d.ts │ │ ├── logo.svg │ │ ├── react-app-env.d.ts │ │ ├── reportWebVitals.ts │ │ ├── router-extend.d.ts │ │ ├── router │ │ │ └── index.tsx │ │ ├── services │ │ │ ├── article.ts │ │ │ ├── category.ts │ │ │ ├── chatgpt.ts │ │ │ ├── comment.ts │ │ │ ├── index.ts │ │ │ ├── reply.ts │ │ │ ├── tag.ts │ │ │ ├── user.ts │ │ │ └── validator.ts │ │ ├── setupProxy.js │ │ ├── setupTests.ts │ │ ├── store │ │ │ ├── hooks │ │ │ │ ├── auth.ts │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ └── slices │ │ │ │ ├── auth.ts │ │ │ │ └── ui.ts │ │ ├── styled.d.ts │ │ ├── styles │ │ │ ├── animation.less │ │ │ ├── main.less │ │ │ ├── md.less │ │ │ ├── reset.less │ │ │ └── styled-mixins │ │ │ │ └── base.js │ │ ├── utils │ │ │ ├── bom.ts │ │ │ ├── date-utils.ts │ │ │ ├── dom.ts │ │ │ ├── eventbus.ts │ │ │ ├── formatter.ts │ │ │ ├── helper.ts │ │ │ ├── tree.ts │ │ │ ├── type.ts │ │ │ └── validator.ts │ │ └── views │ │ │ ├── Article │ │ │ └── index.tsx │ │ │ ├── Backend │ │ │ ├── Article │ │ │ │ └── index.tsx │ │ │ ├── Category │ │ │ │ ├── Edit.tsx │ │ │ │ └── index.tsx │ │ │ ├── Comment │ │ │ │ ├── All.tsx │ │ │ │ ├── Review.tsx │ │ │ │ └── ReviewReply.tsx │ │ │ ├── Message │ │ │ │ ├── All.tsx │ │ │ │ ├── Review.tsx │ │ │ │ └── ReviewReply.tsx │ │ │ ├── Tag │ │ │ │ └── index.tsx │ │ │ ├── Write │ │ │ │ └── index.tsx │ │ │ ├── index.tsx │ │ │ └── navs.tsx │ │ │ ├── Category │ │ │ └── index.tsx │ │ │ ├── Categoryies │ │ │ └── index.tsx │ │ │ ├── Chat │ │ │ └── index.jsx │ │ │ ├── Home │ │ │ └── index.tsx │ │ │ ├── Login │ │ │ └── index.tsx │ │ │ ├── Messages │ │ │ └── index.tsx │ │ │ ├── Tag │ │ │ └── index.tsx │ │ │ ├── Tags │ │ │ └── index.tsx │ │ │ └── Timeline │ │ │ └── index.tsx │ ├── tailwind.config.js │ └── tsconfig.json ├── express-server │ ├── .env │ ├── .eslintignore │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── CHANGELOG.md │ ├── deploy.config.example.js │ ├── ecosystem.config.js │ ├── package.json │ ├── process-dev.json │ ├── process-docker-dev.json │ ├── process-docker-prod.json │ └── src │ │ ├── app.js │ │ ├── config │ │ ├── README.md │ │ ├── env.example.js │ │ └── index.js │ │ ├── controllers │ │ ├── article.js │ │ ├── banner.js │ │ ├── base.js │ │ ├── category.js │ │ ├── chatgpt.js │ │ ├── comment.js │ │ ├── reply.js │ │ ├── tag.js │ │ ├── user.js │ │ └── validator.js │ │ ├── permissions │ │ └── auth.js │ │ ├── routes │ │ └── index.js │ │ ├── sql │ │ ├── article.js │ │ ├── banner.js │ │ ├── category.js │ │ ├── comment.js │ │ ├── index.js │ │ ├── reply.js │ │ ├── tag.js │ │ └── user.js │ │ ├── utils │ │ ├── auth.js │ │ ├── db.js │ │ ├── email.js │ │ ├── errcode.js │ │ ├── utils.js │ │ ├── validate.js │ │ └── ws.js │ │ └── views │ │ ├── error.ejs │ │ ├── index.ejs │ │ ├── login.ejs │ │ ├── nav.ejs │ │ └── success.ejs ├── nest-server │ ├── .eslintrc.js │ ├── .gitignore │ ├── CHANGELOG.md │ ├── ENV.md │ ├── README.md │ ├── db.md │ ├── nest-cli.json │ ├── package.json │ ├── process-docker-prod.json │ ├── src │ │ ├── app.module.ts │ │ ├── decorators │ │ │ ├── body-number.decorator.ts │ │ │ ├── public-access.decorator.ts │ │ │ └── query-number.decorator.ts │ │ ├── entities │ │ │ ├── Article.ts │ │ │ ├── ArticleCategory.ts │ │ │ ├── ArticleTag.ts │ │ │ ├── Authority.ts │ │ │ ├── Banner.ts │ │ │ ├── Category.ts │ │ │ ├── Comments.ts │ │ │ ├── Reply.ts │ │ │ ├── Role.ts │ │ │ ├── RoleAuth.ts │ │ │ ├── Tag.ts │ │ │ └── User.ts │ │ ├── exception-filters │ │ │ └── inner.filter.ts │ │ ├── exceptions │ │ │ └── inner.exception.ts │ │ ├── guards │ │ │ └── auth.guard.ts │ │ ├── interceptors │ │ │ └── response.interceptor.ts │ │ ├── main.ts │ │ ├── modules │ │ │ ├── article │ │ │ │ ├── article.controller.spec.ts │ │ │ │ ├── article.controller.ts │ │ │ │ ├── article.module.ts │ │ │ │ ├── article.service.spec.ts │ │ │ │ ├── article.service.ts │ │ │ │ └── dto │ │ │ │ │ └── article.dto.ts │ │ │ ├── banner │ │ │ │ ├── banner.controller.ts │ │ │ │ ├── banner.module.ts │ │ │ │ └── banner.service.ts │ │ │ ├── category │ │ │ │ ├── category.controller.spec.ts │ │ │ │ ├── category.controller.ts │ │ │ │ ├── category.module.ts │ │ │ │ ├── category.service.spec.ts │ │ │ │ ├── category.service.ts │ │ │ │ └── dto │ │ │ │ │ └── category.dto.ts │ │ │ ├── chat │ │ │ │ ├── chat.gateway.ts │ │ │ │ └── chat.module.ts │ │ │ ├── chatgpt │ │ │ │ ├── chatgpt.controller.ts │ │ │ │ └── chatgpt.module.ts │ │ │ ├── comment │ │ │ │ ├── comment.controller.spec.ts │ │ │ │ ├── comment.controller.ts │ │ │ │ ├── comment.module.ts │ │ │ │ ├── comment.service.spec.ts │ │ │ │ ├── comment.service.ts │ │ │ │ └── dto │ │ │ │ │ └── comment.dto.ts │ │ │ ├── common │ │ │ │ ├── auth.service.ts │ │ │ │ ├── common.module.ts │ │ │ │ └── email.service.ts │ │ │ ├── reply │ │ │ │ ├── dto │ │ │ │ │ └── reply.dto.ts │ │ │ │ ├── reply.controller.spec.ts │ │ │ │ ├── reply.controller.ts │ │ │ │ ├── reply.module.ts │ │ │ │ ├── reply.service.spec.ts │ │ │ │ └── reply.service.ts │ │ │ ├── tag │ │ │ │ ├── dto │ │ │ │ │ └── tag.dto.ts │ │ │ │ ├── tag.controller.spec.ts │ │ │ │ ├── tag.controller.ts │ │ │ │ ├── tag.module.ts │ │ │ │ ├── tag.service.spec.ts │ │ │ │ └── tag.service.ts │ │ │ ├── user │ │ │ │ ├── dto │ │ │ │ │ └── login.dto.ts │ │ │ │ ├── user.controller.spec.ts │ │ │ │ ├── user.controller.ts │ │ │ │ ├── user.module.ts │ │ │ │ ├── user.service.spec.ts │ │ │ │ └── user.service.ts │ │ │ └── validator │ │ │ │ ├── validator.controller.spec.ts │ │ │ │ ├── validator.controller.ts │ │ │ │ ├── validator.module.ts │ │ │ │ ├── validator.service.spec.ts │ │ │ │ └── validator.service.ts │ │ ├── types │ │ │ ├── base.ts │ │ │ ├── express.d.ts │ │ │ ├── session.d.ts │ │ │ └── svg-captcha.d.ts │ │ └── utils │ │ │ └── type.ts │ ├── test │ │ ├── app.e2e-spec.ts │ │ └── jest-e2e.json │ ├── tsconfig.build.json │ └── tsconfig.json ├── nuxt3-web │ ├── .gitignore │ ├── .prettierrc │ ├── .stylelintrc.mjs │ ├── README.md │ ├── app.vue │ ├── eslint.config.mjs │ ├── nuxt.config.ts │ ├── package.json │ ├── public │ │ └── favicon.ico │ ├── server │ │ └── tsconfig.json │ ├── test.less │ └── tsconfig.json ├── vite-vue3 │ ├── .env │ ├── .env.development │ ├── .env.production │ ├── .eslintignore │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── .stylelintrc.cjs │ ├── CHANGELOG.md │ ├── index.html │ ├── jsconfig.json │ ├── package.json │ ├── public │ │ ├── ads.txt │ │ ├── favicon.ico │ │ ├── js │ │ │ └── hm.js │ │ └── robots.txt │ ├── src │ │ ├── App.vue │ │ ├── assets │ │ │ ├── img │ │ │ │ ├── avatar.jpg │ │ │ │ ├── chat-avatar.png │ │ │ │ ├── comment-avatar.svg │ │ │ │ ├── default_category.svg │ │ │ │ ├── logo.png │ │ │ │ ├── logo2.png │ │ │ │ ├── reply-avatar.svg │ │ │ │ └── wechat_payme.jpg │ │ │ └── vue.svg │ │ ├── bean │ │ │ ├── base.ts │ │ │ ├── dto.ts │ │ │ └── xhr.ts │ │ ├── components │ │ │ ├── base-layout │ │ │ │ ├── base-footer.vue │ │ │ │ ├── base-menu.vue │ │ │ │ ├── hot-column.vue │ │ │ │ └── index.vue │ │ │ ├── bottom-tips │ │ │ │ └── index.vue │ │ │ ├── card │ │ │ │ ├── card-article.vue │ │ │ │ └── card-comment.vue │ │ │ ├── icon-svg │ │ │ │ └── index.vue │ │ │ └── my-button │ │ │ │ └── index.vue │ │ ├── directives │ │ │ ├── index.ts │ │ │ └── lazyload.ts │ │ ├── hooks │ │ │ └── async.ts │ │ ├── jshashes.d.ts │ │ ├── main.ts │ │ ├── router-type.d.ts │ │ ├── router │ │ │ ├── backend.ts │ │ │ ├── index.ts │ │ │ └── not-found.ts │ │ ├── services │ │ │ ├── article.ts │ │ │ ├── category.ts │ │ │ ├── chatgpt.ts │ │ │ ├── comment.ts │ │ │ ├── index.ts │ │ │ ├── reply.ts │ │ │ ├── tag.ts │ │ │ ├── user.ts │ │ │ └── validator.ts │ │ ├── stores │ │ │ ├── auth.ts │ │ │ └── global-ui-state.ts │ │ ├── styles │ │ │ ├── animation.scss │ │ │ ├── antd.scss │ │ │ ├── atom.scss │ │ │ ├── common.scss │ │ │ ├── element-vars.scss │ │ │ ├── index.scss │ │ │ ├── mixins.scss │ │ │ ├── preload.scss │ │ │ ├── reset.scss │ │ │ └── vars.scss │ │ ├── utils │ │ │ ├── bom.ts │ │ │ ├── date-utils.ts │ │ │ ├── dom.ts │ │ │ ├── eventbus.ts │ │ │ ├── formatter.ts │ │ │ ├── helper.ts │ │ │ ├── tree.ts │ │ │ ├── type.ts │ │ │ └── validator.ts │ │ ├── views │ │ │ ├── 404 │ │ │ │ └── index.vue │ │ │ ├── article │ │ │ │ ├── comment-user-info.vue │ │ │ │ ├── comments.vue │ │ │ │ ├── index.vue │ │ │ │ └── md-render.scss │ │ │ ├── backend │ │ │ │ ├── article │ │ │ │ │ ├── index.module.scss │ │ │ │ │ └── index.vue │ │ │ │ ├── category │ │ │ │ │ ├── edit.vue │ │ │ │ │ └── index.vue │ │ │ │ ├── comment │ │ │ │ │ ├── all │ │ │ │ │ │ └── index.vue │ │ │ │ │ ├── review-reply │ │ │ │ │ │ └── index.vue │ │ │ │ │ └── review │ │ │ │ │ │ └── index.vue │ │ │ │ ├── index.vue │ │ │ │ ├── msg │ │ │ │ │ ├── all │ │ │ │ │ │ └── index.vue │ │ │ │ │ ├── review-reply │ │ │ │ │ │ └── index.vue │ │ │ │ │ └── review │ │ │ │ │ │ └── index.vue │ │ │ │ ├── navs.tsx │ │ │ │ ├── styles │ │ │ │ │ └── avatar.scss │ │ │ │ ├── tag │ │ │ │ │ └── index.vue │ │ │ │ └── write │ │ │ │ │ └── index.vue │ │ │ ├── categories │ │ │ │ └── index.vue │ │ │ ├── category │ │ │ │ └── index.vue │ │ │ ├── chat │ │ │ │ └── index.vue │ │ │ ├── chatgpt │ │ │ │ └── index.vue │ │ │ ├── home │ │ │ │ └── index.vue │ │ │ ├── jumpout │ │ │ │ └── index.vue │ │ │ ├── login │ │ │ │ └── index.vue │ │ │ ├── messages │ │ │ │ └── index.vue │ │ │ ├── tag │ │ │ │ └── index.vue │ │ │ ├── tags │ │ │ │ └── index.vue │ │ │ └── timeline │ │ │ │ └── index.vue │ │ └── vite-env.d.ts │ ├── tsconfig.app.json │ ├── tsconfig.json │ ├── tsconfig.node.json │ ├── vite.config.docker.ts │ └── vite.config.ts └── webpack-vue3 │ ├── .browserslistrc │ ├── .env │ ├── .env.development │ ├── .env.production │ ├── .eslintignore │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── .stylelintrc.js │ ├── CHANGELOG.md │ ├── antd-theme.js │ ├── babel.config.js │ ├── jest.config.js │ ├── package.json │ ├── public │ ├── favicon.ico │ ├── index.html │ ├── js │ │ └── hm.js │ └── robots.txt │ ├── src │ ├── App.vue │ ├── assets │ │ └── img │ │ │ ├── avatar.jpg │ │ │ ├── chat-avatar.png │ │ │ ├── comment-avatar.svg │ │ │ ├── default_category.svg │ │ │ ├── logo.png │ │ │ ├── logo2.png │ │ │ ├── reply-avatar.svg │ │ │ └── wechat_payme.jpg │ ├── bean │ │ ├── base.ts │ │ ├── dto.ts │ │ └── xhr.ts │ ├── components │ │ ├── base-layout │ │ │ ├── base-footer.vue │ │ │ ├── base-menu.vue │ │ │ ├── hot-column.vue │ │ │ └── index.vue │ │ ├── bottom-tips │ │ │ └── index.vue │ │ ├── card │ │ │ ├── card-article.vue │ │ │ └── card-comment.vue │ │ ├── icon-svg │ │ │ └── index.vue │ │ └── my-button │ │ │ └── index.vue │ ├── directives │ │ ├── index.ts │ │ └── lazyload.ts │ ├── hooks │ │ └── async.ts │ ├── image.d.ts │ ├── main.ts │ ├── router │ │ ├── backend.ts │ │ ├── index.ts │ │ └── not-found.ts │ ├── services │ │ ├── article.ts │ │ ├── category.ts │ │ ├── chatgpt.ts │ │ ├── comment.ts │ │ ├── index.ts │ │ ├── reply.ts │ │ ├── tag.ts │ │ ├── user.ts │ │ └── validator.ts │ ├── shims-vue.d.ts │ ├── store │ │ ├── constants.ts │ │ └── index.ts │ ├── styles │ │ ├── animation.scss │ │ ├── antd.scss │ │ ├── atom.scss │ │ ├── common.scss │ │ ├── element-vars.scss │ │ ├── index.scss │ │ ├── mixins.scss │ │ ├── preload.scss │ │ ├── reset.scss │ │ └── vars.scss │ ├── types │ │ └── jshashes.d.ts │ ├── utils │ │ ├── bom.ts │ │ ├── date-utils.ts │ │ ├── dom.ts │ │ ├── eventbus.ts │ │ ├── formatter.ts │ │ ├── helper.ts │ │ ├── tree.ts │ │ ├── type.ts │ │ └── validator.ts │ └── views │ │ ├── 404 │ │ └── index.vue │ │ ├── article │ │ ├── comment-user-info.vue │ │ ├── comments.vue │ │ ├── index.vue │ │ └── md-render.scss │ │ ├── backend │ │ ├── article │ │ │ ├── index.module.scss │ │ │ └── index.vue │ │ ├── category │ │ │ ├── edit.vue │ │ │ └── index.vue │ │ ├── comment │ │ │ ├── all │ │ │ │ └── index.vue │ │ │ ├── review-reply │ │ │ │ └── index.vue │ │ │ └── review │ │ │ │ └── index.vue │ │ ├── index.vue │ │ ├── msg │ │ │ ├── all │ │ │ │ └── index.vue │ │ │ ├── review-reply │ │ │ │ └── index.vue │ │ │ └── review │ │ │ │ └── index.vue │ │ ├── navs.ts │ │ ├── styles │ │ │ └── avatar.scss │ │ ├── tag │ │ │ └── index.vue │ │ └── write │ │ │ └── index.vue │ │ ├── categories │ │ └── index.vue │ │ ├── category │ │ └── index.vue │ │ ├── chat │ │ └── index.vue │ │ ├── chatgpt │ │ └── index.vue │ │ ├── home │ │ └── index.vue │ │ ├── jumpout │ │ └── index.vue │ │ ├── login │ │ └── index.vue │ │ ├── messages │ │ └── index.vue │ │ ├── tag │ │ └── index.vue │ │ ├── tags │ │ └── index.vue │ │ └── timeline │ │ └── index.vue │ ├── tests │ └── unit │ │ └── example.spec.ts │ ├── tsconfig.json │ ├── vue.config.docker.js │ └── vue.config.js ├── build-dev-images.sh ├── commitlint.config.js ├── compose-dev.yml ├── compose-prod-local.yml ├── compose.yml ├── docker-ops.md ├── legacy-ops.md ├── mysql └── my.cnf ├── nginx └── default.conf.template ├── package.json ├── packages └── eslint-config │ ├── CHANGELOG.md │ ├── base.js │ └── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml └── tsconfig.base.json /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.0.1/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "restricted", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [], 11 | "privatePackages": { 12 | "version": true, 13 | "tag": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Include any files or directories that you don't want to be copied to your 2 | # container here (e.g., local build artifacts, temporary files, etc.). 3 | # 4 | # For more help, visit the .dockerignore file reference guide at 5 | # https://docs.docker.com/go/build-context-dockerignore/ 6 | 7 | **/.dockerignore 8 | **/.git 9 | **/.husky 10 | **/.gitignore 11 | **/docker-compose* 12 | **/compose.y*ml 13 | **/Dockerfile* 14 | **/node_modules 15 | **/npm-debug.log 16 | **/dist 17 | **/.vscode 18 | LICENSE 19 | README.md 20 | *.xmind 21 | 22 | # backend 23 | app/backend/src/config/dev.env.js 24 | app/backend/src/config/env.js 25 | app/backend/src/config/prod.env.js 26 | app/backend/deploy.config.js 27 | 28 | **/.project 29 | **/.settings 30 | **/.toolstarget 31 | **/.vs 32 | **/.next 33 | **/.cache 34 | **/*.*proj.user 35 | **/*.dbmdl 36 | **/*.jfm 37 | **/charts 38 | **/obj 39 | **/secrets.dev.yaml 40 | **/values.dev.yaml 41 | **/build 42 | **/.classpath 43 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | [*] 4 | end_of_line = lf 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 4 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[Bug Report] I encountered a bug" 5 | labels: bug 6 | assignees: cumt-robin 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[Feature request] I want to introduce a new feature" 5 | labels: feature 6 | assignees: cumt-robin 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/improve.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Improvement Suggestion 3 | about: Help us improve 4 | title: "[Improvement] I recommend an improvement..." 5 | labels: improvement 6 | assignees: cumt-robin 7 | 8 | --- 9 | 10 | **Describe the idea** 11 | Description of what your idea is. 12 | 13 | **Expected behavior** 14 | Description of what you expected to happen. 15 | 16 | **Additional context** 17 | Add any other context about the improvement here. -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - .changeset/** 9 | 10 | concurrency: ${{ github.workflow }}-${{ github.ref }} 11 | 12 | jobs: 13 | release: 14 | name: Release 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout Repo 18 | uses: actions/checkout@v4 19 | with: 20 | token: ${{ secrets.PERSONAL_TOKEN }} 21 | 22 | - name: Setup Node.js 18 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version: 18 26 | 27 | - name: Setup pnpm 28 | uses: pnpm/action-setup@v4.0.0 29 | with: 30 | version: 9.4.0 31 | 32 | - name: Install Workspace Root Dependencies 33 | run: | 34 | pnpm install --workspace-root --frozen-lockfile 35 | 36 | - name: Create PR or Publish Release 37 | uses: changesets/action@v1 38 | with: 39 | version: pnpm version-bump 40 | publish: pnpm release 41 | title: "bot: version packages" 42 | commit: "chore: version packages" 43 | createGithubReleases: true 44 | env: 45 | GITHUB_TOKEN: ${{ secrets.PERSONAL_TOKEN }} 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | dist 4 | .eslintcache 5 | .stylelintcache 6 | mysql/init-scripts 7 | 8 | # local env files 9 | .env.local 10 | .env.*.local 11 | 12 | # Log files 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | pnpm-debug.log* 17 | 18 | # webpack output file 19 | **/output.js 20 | 21 | # Editor directories and files 22 | .idea 23 | *.suo 24 | *.ntvs* 25 | *.njsproj 26 | *.sln 27 | *.sw? 28 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | pnpm dlx commitlint --edit $1 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | pnpm dlx lint-staged $1 5 | -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "app/webpack-vue3/src/**/*.{js,mjs,jsx,ts,tsx,vue}": "pnpm --filter webpack-vue3 lint-fix", 3 | "app/webpack-vue3/src/**/*.{css,less,scss,vue}": "pnpm --filter webpack-vue3 lint-style-fix", 4 | "app/vite-vue3/src/**/*.{js,mjs,jsx,ts,tsx,vue}": "pnpm --filter vite-vue3 lint-fix", 5 | "app/vite-vue3/src/**/*.{css,less,scss,vue}": "pnpm --filter vite-vue3 lint-style-fix", 6 | "app/backend/src/**/*.js": "pnpm --filter backend lint-fix" 7 | } 8 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmmirror.com 2 | ignore-workspace-root-check=true -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/node_modules -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabs": false, 3 | "tabWidth": 4, 4 | "endOfLine": "lf", 5 | "printWidth": 140 6 | } 7 | -------------------------------------------------------------------------------- /.volta/hooks.json: -------------------------------------------------------------------------------- 1 | { 2 | "node": { 3 | "index": { 4 | "prefix": "https://cdn.npmmirror.com/binaries/node/" 5 | }, 6 | "distro": { 7 | "template": "https://registry.npmmirror.com/-/binary/node/v{{version}}/node-v{{version}}-{{os}}-{{arch}}.{{ext}}" 8 | } 9 | }, 10 | "npm": { 11 | "index": { 12 | "prefix": "https://registry.npmmirror.com/npm/" 13 | }, 14 | "distro": { 15 | "template": "https://registry.npmmirror.com/npm/-/npm-{{version}}.tgz" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "stylelint.vscode-stylelint", 5 | "esbenp.prettier-vscode", 6 | "editorconfig.editorconfig", 7 | "github.vscode-github-actions", 8 | "mgmcdermott.vscode-language-babel", 9 | "bradlc.vscode-tailwindcss", 10 | "styled-components.vscode-styled-components", 11 | "tusi.fe-dev-snippets" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 4, 3 | "files.eol": "\n", 4 | "eslint.validate": ["javascript", "typescript", "javascriptreact", "typescriptreact", "vue"], 5 | // 防止内置css校验和stylelint重复报错 6 | "css.validate": false, 7 | "less.validate": false, 8 | "scss.validate": false, 9 | // 保存时不调用格式化,交给 lint fix 10 | "editor.formatOnSave": false, 11 | // 代码保存动作,调用 lint fix 12 | "editor.codeActionsOnSave": { 13 | "source.fixAll.eslint": "explicit", 14 | "source.fixAll.stylelint": "explicit" 15 | }, 16 | "editor.defaultFormatter": "esbenp.prettier-vscode", 17 | "eslint.workingDirectories": [ 18 | { 19 | "mode": "auto" 20 | } 21 | ], 22 | "stylelint.validate": [ 23 | "css", 24 | "less", 25 | "scss", 26 | "vue" 27 | ], 28 | "stylelint.enable": true, 29 | "[vue]": { 30 | "editor.defaultFormatter": "Vue.volar" 31 | }, 32 | "typescript.tsdk": "node_modules/typescript/lib", 33 | "files.associations": { 34 | "*.css": "tailwindcss" 35 | }, 36 | "editor.quickSuggestions": { 37 | "strings": "on" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | registry "https://registry.npmmirror.com" -------------------------------------------------------------------------------- /Dockerfile.dev: -------------------------------------------------------------------------------- 1 | FROM node:18-slim AS base 2 | 3 | # 安装 Python 和构建工具 4 | RUN apt-get update && apt-get install -y python3 make g++ \ 5 | && rm -rf /var/lib/apt/lists/* 6 | 7 | RUN npm i -g pnpm 8 | ENV PNPM_HOME="/pnpm" 9 | ENV PATH="$PNPM_HOME:$PATH" 10 | 11 | FROM base AS build 12 | COPY . /usr/src/app 13 | WORKDIR /usr/src/app 14 | # macos 15 运行下面这行会报错,所以上面加了 python3 15 | RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile 16 | RUN pnpm deploy --filter=vite-vue3 /app/vite-vue3 17 | # RUN pnpm deploy --filter=cra-react18 /app/cra-react18 18 | RUN pnpm deploy --filter=express-server /app/express-server 19 | # RUN pnpm deploy --filter=nest-server /app/nest-server 20 | 21 | FROM base AS vite-vue3-frontend 22 | COPY --from=build /app/vite-vue3 /usr/src/fullstack-blog/app/vite-vue3 23 | WORKDIR /usr/src/fullstack-blog/app/vite-vue3 24 | EXPOSE 3000 25 | CMD ["pnpm", "dev"] 26 | 27 | # FROM base AS cra-react18-frontend 28 | # COPY --from=build /app/cra-react18 /usr/src/fullstack-blog/app/cra-react18 29 | # WORKDIR /usr/src/fullstack-blog/app/cra-react18 30 | # EXPOSE 3000 31 | # CMD ["pnpm", "dev"] 32 | 33 | FROM base AS express-backend 34 | RUN npm i -g pm2-dev 35 | COPY --from=build /app/express-server /usr/src/fullstack-blog/app/express-server 36 | 37 | # FROM base AS nestjs-backend 38 | # COPY --from=build /app/nest-server /usr/src/fullstack-blog/app/nest-server 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Tusi 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 | -------------------------------------------------------------------------------- /app/cra-react18/.env: -------------------------------------------------------------------------------- 1 | REACT_APP_BASE_API=/api 2 | REACT_APP_SOCKET_SERVER=http://127.0.0.1:8012 3 | -------------------------------------------------------------------------------- /app/cra-react18/.env.production: -------------------------------------------------------------------------------- 1 | NODE_ENV=production 2 | REACT_APP_SOCKET_SERVER="" 3 | -------------------------------------------------------------------------------- /app/cra-react18/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | src/**/*.d.ts 3 | dist 4 | public 5 | scripts 6 | config 7 | -------------------------------------------------------------------------------- /app/cra-react18/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | }, 6 | parser: "@typescript-eslint/parser", 7 | extends: ["eslint:recommended", "react-app", "react-app/jest", "prettier"], 8 | plugins: ["prettier"], 9 | settings: { 10 | // eslint-import-resolver-webpack 11 | "import/resolver": { 12 | webpack: { 13 | config: "config/webpack.config.js", 14 | }, 15 | }, 16 | }, 17 | rules: { 18 | "prettier/prettier": "error", 19 | "no-case-declarations": "off", 20 | "import/order": "warn", 21 | "import/no-unresolved": "off", 22 | "@typescript-eslint/explicit-module-boundary-types": "off", 23 | "react-hooks/exhaustive-deps": "error", 24 | "react-hooks/rules-of-hooks": "error", 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /app/cra-react18/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /app/cra-react18/.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "tabs": false, 3 | "tabWidth": 4, 4 | "endOfLine": "lf", 5 | "printWidth": 140, 6 | "plugins": ["prettier-plugin-tailwindcss"] 7 | } 8 | -------------------------------------------------------------------------------- /app/cra-react18/.stylelintrc.cjs: -------------------------------------------------------------------------------- 1 | /** 2 | * @author: Tusi 3 | * @description: stylelint配置 4 | */ 5 | module.exports = { 6 | extends: ["stylelint-config-standard", "stylelint-prettier/recommended"], 7 | plugins: ["stylelint-less", "stylelint-prettier"], 8 | rules: { 9 | "no-descending-specificity": null, 10 | "at-rule-no-unknown": null, 11 | "color-no-invalid-hex": true, 12 | "less/color-no-invalid-hex": true, 13 | "keyframes-name-pattern": null, 14 | "selector-class-pattern": null, 15 | "import-notation": null, 16 | "no-invalid-position-at-import-rule": null 17 | }, 18 | customSyntax: "postcss-styled-syntax", 19 | overrides: [ 20 | { 21 | files: ["**/*.less"], 22 | customSyntax: "postcss-less", 23 | }, 24 | ], 25 | }; 26 | -------------------------------------------------------------------------------- /app/cra-react18/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["react-app"], 3 | plugins: ["babel-plugin-styled-components"], 4 | }; 5 | -------------------------------------------------------------------------------- /app/cra-react18/config/jest/babelTransform.js: -------------------------------------------------------------------------------- 1 | const babelJest = require("babel-jest").default; 2 | 3 | const hasJsxRuntime = (() => { 4 | if (process.env.DISABLE_NEW_JSX_TRANSFORM === "true") { 5 | return false; 6 | } 7 | 8 | try { 9 | require.resolve("react/jsx-runtime"); 10 | return true; 11 | } catch (e) { 12 | return false; 13 | } 14 | })(); 15 | 16 | module.exports = babelJest.createTransformer({ 17 | presets: [ 18 | [ 19 | require.resolve("babel-preset-react-app"), 20 | { 21 | runtime: hasJsxRuntime ? "automatic" : "classic", 22 | }, 23 | ], 24 | ], 25 | babelrc: false, 26 | configFile: false, 27 | }); 28 | -------------------------------------------------------------------------------- /app/cra-react18/config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | // This is a custom Jest transformer turning style imports into empty objects. 2 | // http://facebook.github.io/jest/docs/en/webpack.html 3 | 4 | module.exports = { 5 | process() { 6 | return "module.exports = {};"; 7 | }, 8 | getCacheKey() { 9 | // The output is always the same. 10 | return "cssTransform"; 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /app/cra-react18/config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const camelcase = require("camelcase"); 3 | 4 | // This is a custom Jest transformer turning file imports into filenames. 5 | // http://facebook.github.io/jest/docs/en/webpack.html 6 | 7 | module.exports = { 8 | process(src, filename) { 9 | const assetFilename = JSON.stringify(path.basename(filename)); 10 | 11 | if (filename.match(/\.svg$/)) { 12 | // Based on how SVGR generates a component name: 13 | // https://github.com/smooth-code/svgr/blob/01b194cf967347d43d4cbe6b434404731b87cf27/packages/core/src/state.js#L6 14 | const pascalCaseFilename = camelcase(path.parse(filename).name, { 15 | pascalCase: true, 16 | }); 17 | const componentName = `Svg${pascalCaseFilename}`; 18 | return `const React = require('react'); 19 | module.exports = { 20 | __esModule: true, 21 | default: ${assetFilename}, 22 | ReactComponent: React.forwardRef(function ${componentName}(props, ref) { 23 | return { 24 | $$typeof: Symbol.for('react.element'), 25 | type: 'svg', 26 | ref: ref, 27 | key: null, 28 | props: Object.assign({}, props, { 29 | children: ${assetFilename} 30 | }) 31 | }; 32 | }), 33 | };`; 34 | } 35 | 36 | return `module.exports = ${assetFilename};`; 37 | }, 38 | }; 39 | -------------------------------------------------------------------------------- /app/cra-react18/config/webpack/persistentCache/createEnvironmentHash.js: -------------------------------------------------------------------------------- 1 | const { createHash } = require("crypto"); 2 | 3 | module.exports = (env) => { 4 | const hash = createHash("md5"); 5 | hash.update(JSON.stringify(env)); 6 | 7 | return hash.digest("hex"); 8 | }; 9 | -------------------------------------------------------------------------------- /app/cra-react18/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ["/src"], 3 | collectCoverageFrom: ["src/**/*.{js,jsx,ts,tsx}", "!src/**/*.d.ts"], 4 | setupFiles: ["react-app-polyfill/jsdom"], 5 | setupFilesAfterEnv: ["/src/setupTests.ts"], 6 | testMatch: ["/src/**/__tests__/**/*.{js,jsx,ts,tsx}", "/src/**/*.{spec,test}.{js,jsx,ts,tsx}"], 7 | testEnvironment: "jsdom", 8 | transform: { 9 | "^.+\\.(js|jsx|mjs|cjs|ts|tsx)$": "/config/jest/babelTransform.js", 10 | "^.+\\.css$": "/config/jest/cssTransform.js", 11 | "^(?!.*\\.(js|jsx|mjs|cjs|ts|tsx|css|json)$)": "/config/jest/fileTransform.js", 12 | }, 13 | transformIgnorePatterns: ["[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs|cjs|ts|tsx)$", "^.+\\.module\\.(css|sass|scss)$"], 14 | modulePaths: [], 15 | moduleNameMapper: { 16 | "^react-native$": "react-native-web", 17 | "^.+\\.module\\.(css|sass|scss)$": "identity-obj-proxy", 18 | }, 19 | moduleFileExtensions: ["web.js", "js", "web.ts", "ts", "web.tsx", "tsx", "json", "web.jsx", "jsx", "node"], 20 | watchPlugins: ["jest-watch-typeahead/filename", "jest-watch-typeahead/testname"], 21 | resetMocks: true, 22 | }; 23 | -------------------------------------------------------------------------------- /app/cra-react18/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cumt-robin/fullstack-blog/02a1377d007f0b6906cac66a1d6829bc998b72ff/app/cra-react18/public/favicon.ico -------------------------------------------------------------------------------- /app/cra-react18/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /app/cra-react18/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: / 4 | -------------------------------------------------------------------------------- /app/cra-react18/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render, screen } from "@testing-library/react"; 3 | import App from "./App"; 4 | 5 | test("renders learn react link", () => { 6 | render(); 7 | const linkElement = screen.getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /app/cra-react18/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { RouterProvider } from "react-router-dom"; 2 | import { ConfigProvider, Skeleton } from "antd"; 3 | import { Provider } from "react-redux"; 4 | import { Suspense } from "react"; 5 | import zhCN from "antd/locale/zh_CN"; 6 | import { router } from "./router"; 7 | import { store } from "./store"; 8 | import "dayjs/locale/zh-cn"; 9 | 10 | function App() { 11 | return ( 12 | 13 | 23 | }> 24 | 25 | 26 | 27 | 28 | ); 29 | } 30 | 31 | export default App; 32 | -------------------------------------------------------------------------------- /app/cra-react18/src/assets/img/avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cumt-robin/fullstack-blog/02a1377d007f0b6906cac66a1d6829bc998b72ff/app/cra-react18/src/assets/img/avatar.jpg -------------------------------------------------------------------------------- /app/cra-react18/src/assets/img/chat-avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cumt-robin/fullstack-blog/02a1377d007f0b6906cac66a1d6829bc998b72ff/app/cra-react18/src/assets/img/chat-avatar.png -------------------------------------------------------------------------------- /app/cra-react18/src/assets/img/comment-avatar.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/cra-react18/src/assets/img/default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cumt-robin/fullstack-blog/02a1377d007f0b6906cac66a1d6829bc998b72ff/app/cra-react18/src/assets/img/default.png -------------------------------------------------------------------------------- /app/cra-react18/src/assets/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cumt-robin/fullstack-blog/02a1377d007f0b6906cac66a1d6829bc998b72ff/app/cra-react18/src/assets/img/logo.png -------------------------------------------------------------------------------- /app/cra-react18/src/assets/img/logo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cumt-robin/fullstack-blog/02a1377d007f0b6906cac66a1d6829bc998b72ff/app/cra-react18/src/assets/img/logo2.png -------------------------------------------------------------------------------- /app/cra-react18/src/assets/img/reply-avatar.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/cra-react18/src/assets/img/wechat_payme.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cumt-robin/fullstack-blog/02a1377d007f0b6906cac66a1d6829bc998b72ff/app/cra-react18/src/assets/img/wechat_payme.jpg -------------------------------------------------------------------------------- /app/cra-react18/src/bean/base.ts: -------------------------------------------------------------------------------- 1 | type IndexType = string | number | symbol; 2 | 3 | export type PlainObject = Record; 4 | 5 | export type PrimitiveType = number | string | boolean | undefined | null | symbol; 6 | 7 | export type GeneralFunction = (...args: any[]) => T; 8 | 9 | export interface PlainNode extends PlainObject { 10 | id: number; 11 | } 12 | 13 | export interface TreeNode extends PlainObject { 14 | key: string; 15 | children?: this[]; 16 | } 17 | 18 | export type Lazy = () => Promise; 19 | -------------------------------------------------------------------------------- /app/cra-react18/src/components/BottomTips/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { styled } from "styled-components"; 3 | 4 | const BottomTipsWrapper = styled.div` 5 | text-align: center; 6 | overflow: hidden; 7 | 8 | .tips { 9 | position: relative; 10 | font-size: 12px; 11 | color: #999; 12 | } 13 | 14 | .tips--line { 15 | &::before, 16 | &::after { 17 | content: ""; 18 | position: absolute; 19 | top: 8px; 20 | width: 60px; 21 | height: 1px; 22 | background-color: #ccc; 23 | } 24 | 25 | &::before { 26 | left: -70px; 27 | } 28 | 29 | &::after { 30 | right: -70px; 31 | } 32 | } 33 | `; 34 | 35 | const BottomTips: React.FC<{ 36 | content?: string; 37 | top?: string; 38 | bottom?: string; 39 | fontSize?: string; 40 | line?: boolean; 41 | children?: React.ReactNode; 42 | }> = (props) => { 43 | const { content, top = "10px", bottom = "10px", fontSize, line = true, children } = props; 44 | 45 | return ( 46 | 47 | 48 | {children || content} 49 | 50 | 51 | ); 52 | }; 53 | 54 | export default BottomTips; 55 | -------------------------------------------------------------------------------- /app/cra-react18/src/components/CardComment/comment-style-context.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | 3 | export const commentStyleContext = createContext<{ boxShadow?: string }>({}); 4 | -------------------------------------------------------------------------------- /app/cra-react18/src/components/IconSvg/index.tsx: -------------------------------------------------------------------------------- 1 | import { createFromIconfontCN } from "@ant-design/icons"; 2 | import { useMemo } from "react"; 3 | import styled, { css as styledCss } from "styled-components"; 4 | import { isNumber } from "@/utils/type"; 5 | 6 | const Icon = createFromIconfontCN({ 7 | scriptUrl: "//at.alicdn.com/t/font_1570300_zwupv8bc2lp.js", 8 | }); 9 | 10 | const iconPrefix = "icon-"; 11 | const fallbackIcon = "home"; 12 | 13 | interface IconSvgProps { 14 | icon: string; 15 | size?: number; 16 | color?: string; 17 | css?: ReturnType; 18 | } 19 | 20 | type ExtraProps = Omit, keyof IconSvgProps>; 21 | 22 | const IconSvg: React.FC = ({ icon, ...restAttrs }) => { 23 | const iconType = useMemo(() => `${iconPrefix}${icon || fallbackIcon}`, [icon]); 24 | return ; 25 | }; 26 | 27 | const StyledIconSvg = styled(IconSvg)` 28 | font-size: ${(props) => (isNumber(props.size) ? `${props.size}px` : "1em")}; 29 | color: ${(props) => props.color}; 30 | & + & { 31 | margin-left: 10px; 32 | } 33 | ${(props) => props.css} 34 | `; 35 | 36 | export default StyledIconSvg; 37 | -------------------------------------------------------------------------------- /app/cra-react18/src/hooks/async.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author: Tusi 3 | * @description: 异步 Hook,用于 loading, error 等状态反馈 4 | */ 5 | import { useCallback, useState } from "react"; 6 | import { GeneralFunction } from "@/bean/base"; 7 | 8 | interface AsyncLoadingResponse { 9 | trigger: GeneralFunction; 10 | loading: boolean; 11 | isError: boolean; 12 | error: unknown; 13 | } 14 | 15 | export const useAsyncLoading = ( 16 | fn: GeneralFunction>, 17 | deps: any[] = [], 18 | options?: { 19 | initialLoading?: boolean; 20 | }, 21 | ): AsyncLoadingResponse => { 22 | const { initialLoading = false } = options ?? {}; 23 | const [loading, setLoading] = useState(initialLoading); 24 | const [isError, setIsError] = useState(false); 25 | const [error, setError] = useState(); 26 | const trigger = useCallback(async (...args: any[]) => { 27 | try { 28 | setLoading(true); 29 | await fn(...args); 30 | } catch (err) { 31 | setIsError(true); 32 | setError(err); 33 | } finally { 34 | setLoading(false); 35 | } 36 | // eslint-disable-next-line react-hooks/exhaustive-deps 37 | }, deps); 38 | return { trigger, loading, isError, error }; 39 | }; 40 | -------------------------------------------------------------------------------- /app/cra-react18/src/hooks/scroll.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect } from "react"; 2 | 3 | interface UseScrollBottomProps { 4 | ref: React.RefObject; 5 | onBottomReached: () => void; 6 | offset?: number; 7 | disabled?: boolean; 8 | } 9 | 10 | export const useScrollBottom = (options: UseScrollBottomProps) => { 11 | const { ref, onBottomReached, offset = 50, disabled = false } = options; 12 | 13 | const handleScroll = useCallback(() => { 14 | if (ref.current && !disabled) { 15 | const { scrollTop, scrollHeight, clientHeight } = ref.current; 16 | if (scrollTop + clientHeight >= scrollHeight - offset) { 17 | onBottomReached(); 18 | } 19 | } 20 | }, [ref, onBottomReached, offset, disabled]); 21 | 22 | useEffect(() => { 23 | const currentElement = ref.current; 24 | if (currentElement) { 25 | currentElement.addEventListener("scroll", handleScroll); 26 | } 27 | 28 | return () => { 29 | if (currentElement) { 30 | currentElement.removeEventListener("scroll", handleScroll); 31 | } 32 | }; 33 | }, [ref, handleScroll]); 34 | }; 35 | -------------------------------------------------------------------------------- /app/cra-react18/src/index.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from "react-dom/client"; 2 | import "./styles/main.less"; 3 | import App from "./App"; 4 | // import reportWebVitals from './reportWebVitals'; 5 | import { init } from "@/utils/date-utils"; 6 | 7 | init(); 8 | 9 | const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement); 10 | root.render(); 11 | 12 | // If you want to start measuring performance in your app, pass a function 13 | // to log results (for example: reportWebVitals(console.log)) 14 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 15 | // reportWebVitals(); 16 | -------------------------------------------------------------------------------- /app/cra-react18/src/jshashes.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author: Tusi 3 | * @description: jshashes类型定义 4 | */ 5 | declare module "jshashes" { 6 | export class SHA256 { 7 | public hex: (string) => string; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /app/cra-react18/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from "web-vitals"; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /app/cra-react18/src/router-extend.d.ts: -------------------------------------------------------------------------------- 1 | // router.d.ts 2 | import 'react-router-dom'; 3 | 4 | declare module 'react-router-dom' { 5 | interface NonIndexRouteObject { 6 | meta?: { 7 | auth?: boolean; 8 | }; 9 | } 10 | 11 | interface IndexRouteObject { 12 | meta?: { 13 | auth?: boolean; 14 | }; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/cra-react18/src/services/category.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author: Tusi 3 | * @description: 分类服务 4 | */ 5 | import { ApiService } from "@/services/index"; 6 | import { ArrayResponse, PageResponse, QueryCategoryModel, QueryPageModel, UpdateCategoryModel } from "@/bean/xhr"; 7 | import { CategoryDTO } from "@/bean/dto"; 8 | 9 | class CategoryService extends ApiService { 10 | public all(params?: QueryCategoryModel) { 11 | return this.$get>("all", params); 12 | } 13 | 14 | public adminPage(params?: QueryPageModel) { 15 | return this.$get>("admin/page", params); 16 | } 17 | 18 | public adminUpdate(params?: UpdateCategoryModel) { 19 | return this.$putJson>("admin/update", params); 20 | } 21 | 22 | public fuzzy(params: { wd: string }) { 23 | return this.$get>>("fuzzy", params); 24 | } 25 | } 26 | 27 | export const categoryService = new CategoryService("category"); 28 | -------------------------------------------------------------------------------- /app/cra-react18/src/services/chatgpt.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author: Tusi 3 | * @description: ChatGPT服务 4 | */ 5 | import { PlainResponse } from "@/bean/xhr"; 6 | import { ApiService } from "@/services/index"; 7 | 8 | class ChatgptService extends ApiService { 9 | public feedback(result: string) { 10 | return this.$postJson("feedback", { result }); 11 | } 12 | 13 | public changeTopic() { 14 | return this.$post("changeTopic"); 15 | } 16 | 17 | public chatV1(wd: string) { 18 | return this.$get>("chat-v1", { wd }); 19 | } 20 | } 21 | 22 | export const chatgptService = new ChatgptService("chatgpt"); 23 | -------------------------------------------------------------------------------- /app/cra-react18/src/services/reply.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author: Tusi 3 | * @description: 回复服务 4 | */ 5 | import { ApiService } from "@/services/index"; 6 | import { PlainObject } from "@/bean/base"; 7 | import { PageResponse, QueryPageModel } from "@/bean/xhr"; 8 | import { ReplyDTO } from "@/bean/dto"; 9 | 10 | class ReplyService extends ApiService { 11 | public add(params: PlainObject) { 12 | return this.$post("add", params); 13 | } 14 | 15 | public unreviewdReplyPage(params: QueryPageModel) { 16 | return this.$get>("unreviewd_reply_page", params); 17 | } 18 | 19 | public review(params: PlainObject) { 20 | return this.$put("review", params); 21 | } 22 | } 23 | 24 | export const replyService = new ReplyService("reply"); 25 | -------------------------------------------------------------------------------- /app/cra-react18/src/services/tag.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author: Tusi 3 | * @description: 标签服务 4 | */ 5 | import { ApiService } from "@/services/index"; 6 | import { ArrayResponse, PageResponse, QueryPageModel, QueryTagModel } from "@/bean/xhr"; 7 | import { TagDTO } from "@/bean/dto"; 8 | 9 | class TagService extends ApiService { 10 | public all(params: QueryTagModel) { 11 | return this.$get>("all", params); 12 | } 13 | 14 | public adminPage(params?: QueryPageModel) { 15 | return this.$get>("admin/page", params); 16 | } 17 | 18 | public fuzzy(params: { wd: string }) { 19 | return this.$get>("fuzzy", params); 20 | } 21 | } 22 | 23 | export const tagService = new TagService("tag"); 24 | -------------------------------------------------------------------------------- /app/cra-react18/src/services/user.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author: Tusi 3 | * @description: 用户服务 4 | */ 5 | import { ApiService } from "@/services/index"; 6 | import { LoginModel, RecordResponse } from "@/bean/xhr"; 7 | import { UserDTO } from "@/bean/dto"; 8 | 9 | class UserService extends ApiService { 10 | public login(params: LoginModel) { 11 | return this.$put>("login", params); 12 | } 13 | 14 | public current() { 15 | return this.$get>("current"); 16 | } 17 | 18 | public logout() { 19 | return this.$put("logout"); 20 | } 21 | } 22 | 23 | export const userService = new UserService("user"); 24 | -------------------------------------------------------------------------------- /app/cra-react18/src/services/validator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author: Tusi 3 | * @description: 验证码服务 4 | */ 5 | import { ApiService } from "@/services/index"; 6 | import { PlainResponse } from "@/bean/xhr"; 7 | 8 | class ValidatorService extends ApiService { 9 | public imgCode() { 10 | return this.$get>("img_code"); 11 | } 12 | } 13 | 14 | export const validatorService = new ValidatorService("validator"); 15 | -------------------------------------------------------------------------------- /app/cra-react18/src/setupProxy.js: -------------------------------------------------------------------------------- 1 | const { createProxyMiddleware } = require("http-proxy-middleware"); 2 | 3 | module.exports = function (app) { 4 | app.use( 5 | "/api", 6 | createProxyMiddleware({ 7 | target: "http://127.0.0.1:8012", 8 | changeOrigin: true, 9 | pathRewrite: { 10 | "^/api": "", 11 | }, 12 | }), 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /app/cra-react18/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import "@testing-library/jest-dom"; 6 | -------------------------------------------------------------------------------- /app/cra-react18/src/store/hooks/auth.ts: -------------------------------------------------------------------------------- 1 | import { selectIsAuthed } from "../slices/auth"; 2 | import { useAppSelector } from "."; 3 | 4 | export const useIsAuthed = () => { 5 | return useAppSelector((state) => selectIsAuthed(state)); 6 | }; 7 | -------------------------------------------------------------------------------- /app/cra-react18/src/store/hooks/index.ts: -------------------------------------------------------------------------------- 1 | import { useDispatch, useSelector } from "react-redux"; 2 | import { AppDispatch, RootState } from ".."; 3 | 4 | export const useAppDispatch = useDispatch.withTypes(); 5 | export const useAppSelector = useSelector.withTypes(); 6 | -------------------------------------------------------------------------------- /app/cra-react18/src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { configureStore } from "@reduxjs/toolkit"; 2 | import authReducer from "./slices/auth"; 3 | import uiReducer from "./slices/ui"; 4 | 5 | export const store = configureStore({ 6 | reducer: { 7 | auth: authReducer, 8 | ui: uiReducer, 9 | }, 10 | }); 11 | 12 | export type RootState = ReturnType; 13 | export type AppDispatch = typeof store.dispatch; 14 | -------------------------------------------------------------------------------- /app/cra-react18/src/store/slices/ui.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from "@reduxjs/toolkit"; 2 | 3 | interface UIState { 4 | isMenuVisible: boolean; 5 | } 6 | 7 | const initialState: UIState = { 8 | isMenuVisible: false, 9 | }; 10 | 11 | export const uiSlice = createSlice({ 12 | name: "ui", 13 | initialState, 14 | reducers: { 15 | setIsMenuVisible: (state, action: PayloadAction) => { 16 | state.isMenuVisible = action.payload; 17 | }, 18 | }, 19 | }); 20 | 21 | export const { setIsMenuVisible } = uiSlice.actions; 22 | 23 | export default uiSlice.reducer; 24 | -------------------------------------------------------------------------------- /app/cra-react18/src/styled.d.ts: -------------------------------------------------------------------------------- 1 | import {} from 'react' 2 | import type { CSSProp } from 'styled-components' 3 | declare module 'react' { 4 | interface Attributes { 5 | css?: CSSProp | undefined 6 | } 7 | } 8 | 9 | // import original module declarations 10 | import 'styled-components'; 11 | 12 | // and extend them! 13 | declare module 'styled-components' { 14 | export interface DefaultTheme { 15 | borderRadius: string; 16 | colors: { 17 | main: string; 18 | secondary: string; 19 | }; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/cra-react18/src/styles/main.less: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer utilities { 6 | .clearfix { 7 | &::after, 8 | &::before { 9 | display: table; 10 | content: ""; 11 | } 12 | 13 | &::after { 14 | clear: both; 15 | } 16 | } 17 | } 18 | 19 | @import "./reset.less"; 20 | @import "./animation.less"; 21 | -------------------------------------------------------------------------------- /app/cra-react18/src/styles/reset.less: -------------------------------------------------------------------------------- 1 | *:fullscreen { 2 | // 必须加背景色,不然进全屏会被:not(:root):-webkit-full-screen::backdrop影响 3 | background-color: #fff; 4 | } 5 | 6 | *, 7 | *::before, 8 | *::after { 9 | box-sizing: border-box; 10 | } 11 | 12 | html, 13 | body, 14 | #root { 15 | height: 100%; 16 | } 17 | 18 | body { 19 | position: relative; 20 | margin: 0; 21 | padding: 0; 22 | font-size: 16px; 23 | line-height: 1.5715; 24 | -moz-osx-font-smoothing: grayscale; 25 | -webkit-font-smoothing: antialiased; 26 | text-rendering: optimizelegibility; 27 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue", 28 | Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 29 | } 30 | 31 | #app { 32 | height: 100%; 33 | } 34 | 35 | a { 36 | color: #87b4e2; 37 | } 38 | 39 | a, 40 | a:focus, 41 | a:hover { 42 | outline: none; 43 | text-decoration: none; 44 | } 45 | 46 | a:hover { 47 | opacity: 0.95; 48 | } 49 | 50 | ul, 51 | li { 52 | list-style: none; 53 | padding: 0; 54 | margin: 0; 55 | } 56 | 57 | h1, 58 | h2, 59 | h3, 60 | h4, 61 | h5, 62 | h6 { 63 | margin: 0 0 0.5em; 64 | font-weight: 500; 65 | } 66 | 67 | p { 68 | margin-top: 0; 69 | margin-bottom: 1em; 70 | } 71 | -------------------------------------------------------------------------------- /app/cra-react18/src/styles/styled-mixins/base.js: -------------------------------------------------------------------------------- 1 | export const ellipsis = ` 2 | overflow: hidden; 3 | white-space: nowrap; 4 | text-overflow: ellipsis; 5 | `; 6 | 7 | export const flexCenter = ` 8 | display: flex; 9 | align-items: center; 10 | justify-content: center; 11 | `; 12 | -------------------------------------------------------------------------------- /app/cra-react18/src/utils/bom.ts: -------------------------------------------------------------------------------- 1 | export const sleep = (seconds = 0) => { 2 | return new Promise((resolve) => { 3 | window.setTimeout(() => { 4 | resolve(true); 5 | }, seconds * 1000); 6 | }); 7 | }; 8 | 9 | export const getLocalData = (option: { key: string; parse?: boolean; defaultValue?: T }) => { 10 | const { key, parse = false, defaultValue } = option; 11 | const lData = localStorage.getItem(key); 12 | if (lData === null) { 13 | return defaultValue === undefined ? null : defaultValue; 14 | } 15 | if (parse) { 16 | return JSON.parse(lData) as T; 17 | } 18 | return lData as T; 19 | }; 20 | -------------------------------------------------------------------------------- /app/cra-react18/src/utils/eventbus.ts: -------------------------------------------------------------------------------- 1 | import mitt from "mitt"; 2 | 3 | type Events = { 4 | // key 是事件名,类型是事件传值的类型 5 | sessionInvalid: void; 6 | clearUserSession: void; 7 | }; 8 | 9 | export const eventBus = mitt(); 10 | -------------------------------------------------------------------------------- /app/cra-react18/src/utils/formatter.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author: Tusi 3 | * @description: 格式化,代替filter功能 4 | */ 5 | 6 | export function approvedFormatter(val: 0 | 1 | 2): string { 7 | switch (val) { 8 | case 1: 9 | return "通过"; 10 | case 2: 11 | return "不通过"; 12 | case 0: 13 | default: 14 | return "待审核"; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/cra-react18/src/utils/helper.ts: -------------------------------------------------------------------------------- 1 | import { getType, isArray, isDefined } from "./type"; 2 | import { PlainObject } from "@/bean/base"; 3 | 4 | /** 5 | * 处理参数对象 6 | * @param {Object} obj 参数对象 7 | * @param {options} isArrayToString 是否需要将数组处理成逗号分隔的string 8 | * @returns {Object} 处理后的参数对象 9 | */ 10 | export function requestParamsFilter(obj: PlainObject, isArrayToString = false): PlainObject { 11 | if (isArray(obj)) { 12 | return obj; 13 | } 14 | if (getType(obj) !== "object") { 15 | return {}; 16 | } 17 | const newObj: PlainObject = {}; 18 | Object.keys(obj).forEach((key) => { 19 | const element = obj[key]; 20 | if (Array.isArray(element)) { 21 | if (element.length > 0) { 22 | newObj[key] = isArrayToString ? element.join(",") : [...element]; 23 | } 24 | } else if (isDefined(element)) { 25 | newObj[key] = element; 26 | } 27 | }); 28 | return newObj; 29 | } 30 | -------------------------------------------------------------------------------- /app/cra-react18/src/utils/tree.ts: -------------------------------------------------------------------------------- 1 | import { TreeNode } from "@/bean/base"; 2 | 3 | // overload 4 | export function tree2Arr(tree: Array, replaceChildren?: string): Array; 5 | export function tree2Arr( 6 | tree: Array, 7 | replaceChildren?: string, 8 | mapper?: (item: T, index: number, arr: Array) => D, 9 | ): Array; 10 | export function tree2Arr( 11 | tree: Array, 12 | replaceChildren = "children", 13 | mapper?: (item: T, index: number, arr: Array) => D, 14 | ): Array | Array { 15 | const result = tree.reduce((prev, curr) => { 16 | const children = curr[replaceChildren] as T[]; 17 | const list = children && children.length > 0 ? [curr, ...tree2Arr(children, replaceChildren, mapper)] : [curr]; 18 | return prev.concat(list as ConcatArray); 19 | }, [] as Array); 20 | return typeof mapper === "function" ? result.map(mapper) : result; 21 | } 22 | -------------------------------------------------------------------------------- /app/cra-react18/src/utils/validator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author: Tusi 3 | * @description: 通用校验 4 | */ 5 | 6 | export const REQUIRED_VALIDATOR_BLUR = { 7 | required: true, 8 | message: "必填项", 9 | trigger: "blur", 10 | }; 11 | 12 | export const EMAIL_VALIDATOR = { 13 | pattern: 14 | /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/, 15 | message: "邮箱格式不正确", 16 | trigger: "blur", 17 | }; 18 | 19 | export const URL_VALIDATOR = { 20 | pattern: /(http|https):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-.,@?^=%&:/~+#]*[\w\-@?^=%&/~+#])?/, 21 | message: "链接格式不正确,注意以http或https开头", 22 | trigger: "blur", 23 | }; 24 | -------------------------------------------------------------------------------- /app/cra-react18/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ["./src/**/*.{js,jsx,ts,tsx}"], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [], 8 | }; 9 | -------------------------------------------------------------------------------- /app/cra-react18/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx", 18 | "baseUrl": ".", 19 | "paths": { 20 | "@/*": ["src/*"] 21 | } 22 | }, 23 | "include": ["src"] 24 | } 25 | -------------------------------------------------------------------------------- /app/express-server/.env: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cumt-robin/fullstack-blog/02a1377d007f0b6906cac66a1d6829bc998b72ff/app/express-server/.env -------------------------------------------------------------------------------- /app/express-server/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /app/express-server/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | }, 6 | extends: ["@fullstack-blog/eslint-config/base.js"], 7 | rules: { 8 | "no-console": "off", 9 | "no-shadow": "off", 10 | "no-unused-vars": "off", 11 | "no-multi-str": "off", 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /app/express-server/.gitignore: -------------------------------------------------------------------------------- 1 | # 敏感配置信息 2 | src/config/dev.env.js 3 | src/config/env.js 4 | src/config/prod.env.js 5 | deploy.config.js 6 | 7 | # Logs 8 | logs 9 | *.log 10 | 11 | # Optional eslint cache 12 | .eslintcache 13 | -------------------------------------------------------------------------------- /app/express-server/deploy.config.example.js: -------------------------------------------------------------------------------- 1 | // 请在同目录下新建一个 deploy.config.js 配置文件,配置说明如下。 2 | module.exports = { 3 | production: { 4 | user: "部署服务器的用户名", 5 | host: ["部署服务器的IP"], 6 | ref: "仓库分支,例如origin/main", 7 | repo: "https://github.com/cumt-robin/express-blog-backend.git", 8 | path: "部署服务器路径,比如/home/xxx/backend/express-blog-backend", 9 | "post-setup": "npm install -g pm2 && npm install && npm start-prod", 10 | "post-deploy": "git pull && npm install && pm2 restart blog", 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /app/express-server/ecosystem.config.js: -------------------------------------------------------------------------------- 1 | const deployConfig = require("./deploy.config"); 2 | 3 | module.exports = { 4 | /** 5 | * Application configuration section 6 | * http://pm2.keymetrics.io/docs/usage/application-declaration/ 7 | */ 8 | apps: [ 9 | // First application 10 | { 11 | // 应用名 12 | name: "blog", 13 | // 启动脚本 14 | script: "src/app.js", 15 | // –env参数指定运行的环境 16 | env_production: { 17 | NODE_ENV: "production", 18 | PORT: 8002, 19 | }, 20 | }, 21 | ], 22 | /** 23 | * Deployment section 24 | * http://pm2.keymetrics.io/docs/usage/deployment/ 25 | */ 26 | deploy: deployConfig, 27 | }; 28 | -------------------------------------------------------------------------------- /app/express-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-server", 3 | "version": "3.5.15", 4 | "private": true, 5 | "scripts": { 6 | "dev": "pm2 start process-dev.json", 7 | "start-prod": "pm2 start ecosystem.config.js --env production", 8 | "restart": "pm2 restart blog", 9 | "docker-dev": "pm2-dev start process-docker-dev.json", 10 | "start-docker-prod": "pm2-runtime start process-docker-prod.json", 11 | "deploy-setup:prod": "pm2 deploy production setup", 12 | "deploy:prod": "pm2 deploy production", 13 | "lint": "eslint --ext .js --cache src", 14 | "lint-fix": "eslint --ext .js --fix --cache src" 15 | }, 16 | "dependencies": { 17 | "compression": "^1.7.3", 18 | "cookie-parser": "~1.4.3", 19 | "debug": "~2.6.9", 20 | "ejs": "~2.5.7", 21 | "express": "~4.17.2", 22 | "express-session": "~1.17.2", 23 | "express-validator": "^7.1.0", 24 | "helmet": "^3.18.0", 25 | "jsonwebtoken": "^9.0.2", 26 | "lodash": "^4.17.21", 27 | "morgan": "~1.9.0", 28 | "mysql2": "~3.10.1", 29 | "nodemailer": "^4.6.8", 30 | "openai": "^3.1.0", 31 | "serve-favicon": "~2.4.5", 32 | "socket.io": "^4.8.1", 33 | "svg-captcha": "^1.3.12", 34 | "xss": "^1.0.9" 35 | }, 36 | "devDependencies": { 37 | "@fullstack-blog/eslint-config": "workspace:^", 38 | "eslint": "^7.32.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/express-server/process-dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "apps": [ 3 | { 4 | "name": "blog", 5 | "script": "src/app.js", 6 | "env": { 7 | "NODE_ENV": "development", 8 | "PORT": 8002 9 | }, 10 | "watch": true, 11 | "ignore_watch": ["node_modules", "*.md"] 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /app/express-server/process-docker-dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "apps": [ 3 | { 4 | "name": "blog", 5 | "script": "src/app.js", 6 | "watch": true, 7 | "ignore_watch": ["node_modules", "*.md"], 8 | "watch_options": { 9 | "usePolling": true 10 | } 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /app/express-server/process-docker-prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "apps": [ 3 | { 4 | "name": "blog", 5 | "script": "src/app.js" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /app/express-server/src/config/README.md: -------------------------------------------------------------------------------- 1 | # 配置说明 2 | 3 | `config`目录是一些必要的参数配置。 4 | 5 | - `env.js`是通用参数配置。配置示例参考`env.example.js`。 6 | -------------------------------------------------------------------------------- /app/express-server/src/config/env.example.js: -------------------------------------------------------------------------------- 1 | // 请在同目录下新建一个 env.js 配置文件,用于配置通用设置,配置说明如下。 2 | module.exports = { 3 | email: { 4 | service: "163", // 邮箱服务商 5 | port: 465, // SMTP 端口 6 | secureConnection: true, // 使用 SSL 7 | auth: { 8 | user: "your email used for sending notifications", 9 | // smtp授权码 10 | pass: "smtp auth code", 11 | }, 12 | }, 13 | authorEmail: "your private email which is used for receiving notifications", 14 | blogName: "your blog name, such as Tusi博客", 15 | siteURL: "visit url of your blog, such as https://blog.me", 16 | chatgpt: { 17 | key: "your OPENAI_API_KEY", 18 | }, 19 | jwt: { 20 | secret: "your jwt secret, you must change it to your own", 21 | expireDays: 3, 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /app/express-server/src/config/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 系统配置 3 | */ 4 | 5 | module.exports = { 6 | ...require("./env"), 7 | // 可以根据 process.env.NODE_ENV 再补充环境变量 8 | }; 9 | -------------------------------------------------------------------------------- /app/express-server/src/controllers/banner.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | 3 | const router = express.Router(); 4 | const indexSQL = require("../sql"); 5 | const dbUtils = require("../utils/db"); 6 | 7 | /** 8 | * @description 获得所有PC banner 9 | */ 10 | router.get("/pc", (req, res, next) => { 11 | dbUtils.query(indexSQL.GetPcBanners).then(({ results }) => { 12 | if (results) { 13 | res.send({ 14 | code: "0", 15 | data: results, 16 | }); 17 | } else { 18 | res.send({ 19 | code: "013001", 20 | data: [], 21 | }); 22 | } 23 | }); 24 | }); 25 | 26 | /** 27 | * @description 获得所有小程序 banner 28 | */ 29 | router.get("/weapp", (req, res, next) => { 30 | dbUtils.query(indexSQL.GetWeappBanners).then(({ results }) => { 31 | if (results) { 32 | res.send({ 33 | code: "0", 34 | data: results, 35 | }); 36 | } else { 37 | res.send({ 38 | code: "013002", 39 | data: [], 40 | }); 41 | } 42 | }); 43 | }); 44 | 45 | module.exports = router; 46 | -------------------------------------------------------------------------------- /app/express-server/src/controllers/base.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | 3 | const router = express.Router(); 4 | const errcode = require("../utils/errcode"); 5 | const authMap = require("../permissions/auth"); 6 | const { parseToken } = require("../utils/auth"); 7 | 8 | /** 9 | * base controller 10 | * 权限验证 11 | */ 12 | router.use(async (req, res, next) => { 13 | const authority = authMap.get(req.path); 14 | 15 | if (authority) { 16 | // 需要检验token的接口 17 | const payload = await parseToken(req); 18 | if (payload) { 19 | // token是否和权限符合 20 | if (payload.roleName !== authority.role) { 21 | return res.send({ 22 | ...errcode.AUTH.FORBIDDEN, 23 | }); 24 | } 25 | 26 | // 将user信息存在本次请求内存中 27 | req.currentUser = payload; 28 | 29 | // 执行权转交后续中间件 30 | next(); 31 | } else { 32 | return res.send({ 33 | ...errcode.AUTH.UNAUTHORIZED, 34 | }); 35 | } 36 | } else { 37 | // 执行权转交后续中间件 38 | next(); 39 | } 40 | }); 41 | 42 | module.exports = router; 43 | -------------------------------------------------------------------------------- /app/express-server/src/controllers/validator.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | 3 | const router = express.Router(); 4 | const svgCaptcha = require("svg-captcha"); 5 | 6 | /** 7 | * @description 获取验证码 8 | */ 9 | router.get("/img_code", (req, res, next) => { 10 | const captcha = svgCaptcha.create({ 11 | // 验证码长度 12 | size: 5, 13 | // 要排除的字符 14 | ignoreChars: "o0i1", 15 | // 干扰线条数量 16 | noise: 1, 17 | // 验证码的字符是否有颜色,默认没有,如果设定了背景,则默认有 18 | color: true, 19 | // 验证码图片背景颜色 20 | background: "#4dffc9", 21 | // 宽度 22 | width: 150, 23 | // 高度 24 | height: 50, 25 | // viewBox 26 | viewBox: "0,0,150,50", 27 | }); 28 | req.session.captcha = captcha.text; 29 | res.send({ 30 | code: "0", 31 | data: captcha.data, 32 | }); 33 | }); 34 | 35 | module.exports = router; 36 | -------------------------------------------------------------------------------- /app/express-server/src/permissions/auth.js: -------------------------------------------------------------------------------- 1 | const authMap = new Map(); 2 | 3 | authMap.set("/user/current", { role: "admin" }); 4 | authMap.set("/user/forgetpwd", { role: "admin" }); 5 | 6 | authMap.set("/article/page_admin", { role: "admin" }); 7 | authMap.set("/article/update_private", { role: "admin" }); 8 | authMap.set("/article/update_deleted", { role: "admin" }); 9 | authMap.set("/article/add", { role: "admin" }); 10 | authMap.set("/article/delete", { role: "admin" }); 11 | authMap.set("/article/publish", { role: "admin" }); 12 | authMap.set("/article/update", { role: "admin" }); 13 | 14 | authMap.set("/comment/get_not_approved", { role: "admin" }); 15 | authMap.set("/comment/page_not_approved", { role: "admin" }); 16 | authMap.set("/comment/review", { role: "admin" }); 17 | authMap.set("/comment/page_admin", { role: "admin" }); 18 | authMap.set("/comment/update", { role: "admin" }); 19 | authMap.set("/comment/delete", { role: "admin" }); 20 | 21 | authMap.set("/reply/getReplyOfCommentWaitReview", { role: "admin" }); 22 | authMap.set("/reply/getReplyOfMsgWaitReview", { role: "admin" }); 23 | authMap.set("/reply/unreviewd_reply_page", { role: "admin" }); 24 | authMap.set("/reply/review", { role: "admin" }); 25 | 26 | authMap.set("/category/admin/page", { role: "admin" }); 27 | authMap.set("/category/fuzzy", { role: "admin" }); 28 | 29 | authMap.set("/tag/admin/page", { role: "admin" }); 30 | authMap.set("/tag/fuzzy", { role: "admin" }); 31 | 32 | module.exports = authMap; 33 | -------------------------------------------------------------------------------- /app/express-server/src/routes/index.js: -------------------------------------------------------------------------------- 1 | const BaseController = require("../controllers/base"); 2 | const ValidatorController = require("../controllers/validator"); 3 | const UserController = require("../controllers/user"); 4 | const BannerController = require("../controllers/banner"); 5 | const ArticleController = require("../controllers/article"); 6 | const TagController = require("../controllers/tag"); 7 | const CategoryController = require("../controllers/category"); 8 | const CommentController = require("../controllers/comment"); 9 | const ReplyController = require("../controllers/reply"); 10 | const ChatgptController = require("../controllers/chatgpt"); 11 | 12 | module.exports = function (app) { 13 | app.use(BaseController); 14 | app.use("/validator", ValidatorController); 15 | app.use("/user", UserController); 16 | app.use("/banner", BannerController); 17 | app.use("/article", ArticleController); 18 | app.use("/tag", TagController); 19 | app.use("/category", CategoryController); 20 | app.use("/comment", CommentController); 21 | app.use("/reply", ReplyController); 22 | app.use("/chatgpt", ChatgptController); 23 | }; 24 | -------------------------------------------------------------------------------- /app/express-server/src/sql/banner.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // 查询所有 3 | GetAllBanners: "SELECT * FROM banner", 4 | GetPcBanners: "SELECT * FROM banner where type = 1", 5 | GetWeappBanners: "SELECT * FROM banner where type = 2", 6 | }; 7 | -------------------------------------------------------------------------------- /app/express-server/src/sql/category.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // 检查分类是否存在 3 | CheckCategory: "SELECT * FROM category WHERE category_name = ?", 4 | // 查询所有分类 5 | QueryAllCategories: "SELECT * FROM category", 6 | GetCategoryCount: "SELECT COUNT(*) AS count FROM category", 7 | // 查询所有被关联的分类及其数量 8 | QueryCategoryAndCount: 9 | "SELECT c.*, COUNT(*) AS category_count FROM category c\ 10 | LEFT JOIN article_category a_c ON c.id = a_c.category_id\ 11 | LEFT JOIN article a ON a.id = a_c.article_id\ 12 | WHERE a.private = 0 AND a.deleted = 0\ 13 | GROUP BY c.id", 14 | // 插入分类表 15 | AddCategories: "INSERT ignore into category (category_name) values (?)", 16 | GetCategoryAdminPage: 17 | "SELECT SQL_CALC_FOUND_ROWS c.*, GROUP_CONCAT(a_c.article_id) AS article_ids FROM category c\ 18 | LEFT JOIN article_category a_c ON a_c.category_id = c.id\ 19 | GROUP BY c.id\ 20 | LIMIT ?, ?;\ 21 | SELECT FOUND_ROWS() AS total;", 22 | AdminUpdateCategory: "UPDATE category SET ? WHERE id = ?", 23 | FuzzyQueryCategory: "SELECT id, category_name FROM category WHERE category_name LIKE ?", 24 | }; 25 | -------------------------------------------------------------------------------- /app/express-server/src/sql/index.js: -------------------------------------------------------------------------------- 1 | const article = require("./article.js"); 2 | const user = require("./user.js"); 3 | const comment = require("./comment.js"); 4 | const reply = require("./reply.js"); 5 | const tag = require("./tag.js"); 6 | const category = require("./category.js"); 7 | const banner = require("./banner.js"); 8 | 9 | module.exports = { 10 | ...article, 11 | ...user, 12 | ...comment, 13 | ...reply, 14 | ...tag, 15 | ...category, 16 | ...banner, 17 | }; 18 | -------------------------------------------------------------------------------- /app/express-server/src/sql/tag.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | QueryAllTags: "SELECT * FROM tag", 3 | QueryTagAndCount: 4 | "SELECT t.*, COUNT(*) AS tag_count FROM tag t\ 5 | LEFT JOIN article_tag a_t ON t.id = a_t.tag_id\ 6 | LEFT JOIN article a ON a.id = a_t.article_id\ 7 | WHERE a.private = 0 AND a.deleted = 0\ 8 | GROUP BY t.id", 9 | // 插入标签表 10 | AddTags: "INSERT ignore into tag (tag_name) values (?)", 11 | // 检查标签是否存在 12 | CheckTag: "SELECT * FROM tag WHERE tag_name = ?", 13 | GetTagAdminPage: 14 | "SELECT SQL_CALC_FOUND_ROWS t.*, GROUP_CONCAT(a_t.article_id) AS article_ids FROM tag t\ 15 | LEFT JOIN article_tag a_t ON a_t.tag_id = t.id\ 16 | GROUP BY t.id\ 17 | LIMIT ?, ?;\ 18 | SELECT FOUND_ROWS() AS total;", 19 | FuzzyQueryTag: "SELECT id, tag_name FROM tag WHERE tag_name LIKE ?", 20 | }; 21 | -------------------------------------------------------------------------------- /app/express-server/src/sql/user.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // 根据用户名和密码查询用户 3 | QueryByUserNameAndPwd: 4 | "SELECT u.id, u.role_id, u.user_name, u.avatar, u.last_login_time, r.role_name FROM user u\ 5 | LEFT JOIN role r ON r.id = u.role_id\ 6 | WHERE u.user_name = ? AND u.password = ?", 7 | // 查询所有用户的相关信息 8 | QueryAllUsers: "SELECT `id`, `role_id`, `user_name`, `avatar`, `last_login_time` FROM user", 9 | // 根据用户id更新登录时间 10 | UpdateUserById: "UPDATE user SET ? WHERE id = ?", 11 | // 查询当前用户 12 | GetCurrentUser: 13 | "SELECT u.id, u.role_id, u.user_name, u.avatar, u.last_login_time, r.role_name FROM user u\ 14 | LEFT JOIN role r ON r.id = u.role_id\ 15 | WHERE u.token = ?", 16 | }; 17 | -------------------------------------------------------------------------------- /app/express-server/src/utils/auth.js: -------------------------------------------------------------------------------- 1 | const jwt = require("jsonwebtoken"); 2 | const config = require("../config"); 3 | 4 | const parseToken = (req) => 5 | new Promise((resolve, reject) => { 6 | const token = req.headers.authorization ? req.headers.authorization.replace("Bearer ", "") : undefined; 7 | if (token) { 8 | jwt.verify(token, config.jwt.secret, (err, payload) => { 9 | if (err) { 10 | resolve(null); 11 | } else { 12 | resolve(payload); 13 | } 14 | }); 15 | } 16 | resolve(null); 17 | }); 18 | 19 | module.exports = { 20 | parseToken, 21 | }; 22 | -------------------------------------------------------------------------------- /app/express-server/src/utils/errcode.js: -------------------------------------------------------------------------------- 1 | // ${module}${feature}${errtype} 2 | module.exports = { 3 | DB: { 4 | CONNECT_EXCEPTION: { 5 | code: "-1", 6 | msg: "数据库连接异常", 7 | }, 8 | }, 9 | AUTH: { 10 | UNAUTHORIZED: { 11 | code: "000001", 12 | msg: "对不起,您还未获得授权", 13 | }, 14 | AUTHORIZE_EXPIRED: { 15 | code: "000002", 16 | msg: "授权已过期", 17 | }, 18 | FORBIDDEN: { 19 | code: "000003", 20 | msg: "抱歉,您没有权限访问该内容", 21 | }, 22 | }, 23 | ARTICLE: { 24 | TOP_READ_EMPTY: { 25 | code: "009001", 26 | msg: "阅读排行榜为空", 27 | }, 28 | NEIGHBORS_EMPTY: { 29 | code: "011001", 30 | msg: "上一篇或下一篇查询失败", 31 | }, 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /app/express-server/src/utils/validate.js: -------------------------------------------------------------------------------- 1 | const { validationResult } = require("express-validator"); 2 | 3 | class ValidationError extends Error { 4 | constructor(body) { 5 | super("Validation Error"); 6 | this.errorBody = body; 7 | this.statusCode = 400; 8 | } 9 | } 10 | 11 | module.exports.ValidationError = ValidationError; 12 | 13 | module.exports.validateInterceptor = (req, res, next) => { 14 | const errors = validationResult(req); 15 | console.error(errors); 16 | if (!errors.isEmpty()) { 17 | throw new ValidationError({ 18 | msg: "请求有误", 19 | }); 20 | } 21 | next(); 22 | }; 23 | -------------------------------------------------------------------------------- /app/express-server/src/views/error.ejs: -------------------------------------------------------------------------------- 1 |

<%= message %>

2 |

<%= error.status %>

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

<%= title %>

9 |

Welcome to <%= title %>

10 |

啦啦啦 <%= greet %>

11 | 12 | 13 | -------------------------------------------------------------------------------- /app/express-server/src/views/login.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 登录 5 | 6 | 7 | 8 | <%- include("./nav.ejs") %> 9 |
10 | 11 | 12 | 13 | 14 | 15 |
16 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/express-server/src/views/nav.ejs: -------------------------------------------------------------------------------- 1 |
    2 |
  • 首页
  • 3 |
  • 分类
  • 4 |
  • 关于
  • 5 |
-------------------------------------------------------------------------------- /app/express-server/src/views/success.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 登录成功 5 | 6 | 7 | 8 |

啦啦啦 登录成功

9 | 10 | 11 | -------------------------------------------------------------------------------- /app/nest-server/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | tsconfigRootDir: __dirname, 6 | sourceType: 'module', 7 | }, 8 | plugins: ['@typescript-eslint/eslint-plugin'], 9 | extends: [ 10 | 'plugin:@typescript-eslint/recommended', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | ignorePatterns: ['.eslintrc.js'], 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /app/nest-server/.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | /build 5 | 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | pnpm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | lerna-debug.log* 14 | 15 | # OS 16 | .DS_Store 17 | 18 | # Tests 19 | /coverage 20 | /.nyc_output 21 | 22 | # IDEs and editors 23 | /.idea 24 | .project 25 | .classpath 26 | .c9/ 27 | *.launch 28 | .settings/ 29 | *.sublime-workspace 30 | 31 | # IDE - VSCode 32 | .vscode/* 33 | !.vscode/settings.json 34 | !.vscode/tasks.json 35 | !.vscode/launch.json 36 | !.vscode/extensions.json 37 | 38 | # dotenv environment variable files 39 | .env 40 | .env.development.local 41 | .env.test.local 42 | .env.production.local 43 | .env.local 44 | 45 | # temp directory 46 | .temp 47 | .tmp 48 | 49 | # Runtime data 50 | pids 51 | *.pid 52 | *.seed 53 | *.pid.lock 54 | 55 | # Diagnostic reports (https://nodejs.org/api/report.html) 56 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 57 | -------------------------------------------------------------------------------- /app/nest-server/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # nest-server 2 | 3 | ## 1.0.4 4 | 5 | ### Patch Changes 6 | 7 | - 5a20f27: fix #215 nest服务留言板page查询报错 8 | 9 | ## 1.0.3 10 | 11 | ### Patch Changes 12 | 13 | - 9dcc2a2: 切换到 nest 服务后,部分文章的评论无法查询到 14 | 15 | ## 1.0.2 16 | 17 | ### Patch Changes 18 | 19 | - 1377f19: fix: 最后一篇文章详情页上一篇下一篇展示为同一篇 20 | 21 | ## 1.0.1 22 | 23 | ### Patch Changes 24 | 25 | - 5d42650: fix: socket.io v4 连接异常问题 26 | 27 | ## 1.0.0 28 | 29 | ### Major Changes 30 | 31 | - 3aab4d5: feat: nestjs socket.io 集成 32 | 33 | ## 0.8.0 34 | 35 | ### Minor Changes 36 | 37 | - 7178b87: feat: nestjs chatgpt 模块实现 38 | 39 | ## 0.7.0 40 | 41 | ### Minor Changes 42 | 43 | - 9ea1151: feat: nestjs banner模块实现 44 | 45 | ## 0.6.0 46 | 47 | ### Minor Changes 48 | 49 | - 65b232f: feat: nestjs reply 接口 50 | 51 | ## 0.5.0 52 | 53 | ### Minor Changes 54 | 55 | - 5245b96: feat: nestjs comment模块实现 56 | 57 | ## 0.4.0 58 | 59 | ### Minor Changes 60 | 61 | - 6a897d7: feat: NestJS article 模块接口实现 62 | 63 | ## 0.3.0 64 | 65 | ### Minor Changes 66 | 67 | - b6ee0a7: feat: NestJS category 模块接口实现 68 | 69 | ## 0.2.0 70 | 71 | ### Minor Changes 72 | 73 | - 9d4289e: feat: NestJS 认证鉴权逻辑梳理 74 | 75 | ## 0.1.0 76 | 77 | ### Minor Changes 78 | 79 | - 7cf286c: feat: NestJS tag 模块接口实现 80 | -------------------------------------------------------------------------------- /app/nest-server/ENV.md: -------------------------------------------------------------------------------- 1 | ## 准备环境变量 2 | 3 | 开发环境可以准备一个`.env.development.local`文件,内容如下: 4 | 5 | ``` 6 | PORT=8012 7 | MYSQL_HOST=127.0.0.1 8 | MYSQL_PORT=3306 9 | MYSQL_ROOT_PASSWORD=xxx 10 | MYSQL_DATABASE=blog_db 11 | JWT_SECRET=xxx 12 | EMAIL_USER=xxx@163.com 13 | EMAIL_PASS=xxx 14 | BLOG_NAME=Tusi博客 15 | AUTHOR_EMAIL=xxx 16 | SITE_URL=https://blog.wbjiang.cn 17 | OPENAI_API_KEY=xxx 18 | WEB_SOCKET_WHITE_LIST=http://localhost:3000,http://127.0.0.1:3000 19 | ``` 20 | 21 | ## 生产环境见 docker-ops.md 22 | -------------------------------------------------------------------------------- /app/nest-server/db.md: -------------------------------------------------------------------------------- 1 | ## 生成 Entify 2 | 3 | ``` 4 | https://www.npmjs.com/package/typeorm-model-generator 5 | ``` 6 | -------------------------------------------------------------------------------- /app/nest-server/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src", 5 | "compilerOptions": { 6 | "deleteOutDir": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /app/nest-server/process-docker-prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "apps": [ 3 | { 4 | "name": "blog-nest-server", 5 | "script": "dist/main.js" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /app/nest-server/src/decorators/body-number.decorator.ts: -------------------------------------------------------------------------------- 1 | import { Type } from "class-transformer"; 2 | import { IsInt, IsPositive } from "class-validator"; 3 | 4 | /** 5 | * body中的正整数类型 6 | */ 7 | export function BodyPositiveInt() { 8 | return function (target: any, propertyKey: string) { 9 | Type(() => Number)(target, propertyKey); 10 | IsInt({ message: "必须是整数" })(target, propertyKey); 11 | IsPositive({ message: "必须大于0" })(target, propertyKey); 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /app/nest-server/src/decorators/public-access.decorator.ts: -------------------------------------------------------------------------------- 1 | import { SetMetadata } from "@nestjs/common"; 2 | 3 | export const PUBLIC_ACCESS_KEY = "public-access"; 4 | export const PublicAccess = () => SetMetadata(PUBLIC_ACCESS_KEY, true); 5 | -------------------------------------------------------------------------------- /app/nest-server/src/decorators/query-number.decorator.ts: -------------------------------------------------------------------------------- 1 | import { Transform, Type } from "class-transformer"; 2 | import { IsInt, IsPositive } from "class-validator"; 3 | 4 | /** 5 | * 查询字符串中的正整数类型 6 | * @param defaultValue 默认值 7 | */ 8 | export function QueryPositiveInt(defaultValue: number = 0) { 9 | return function (target: any, propertyKey: string) { 10 | // 应用所有需要的装饰器 11 | Transform(({ value }) => { 12 | if (value === 0) { 13 | return defaultValue; 14 | } 15 | return value; 16 | })(target, propertyKey); 17 | Type(() => Number)(target, propertyKey); 18 | IsInt({ message: "必须是整数" })(target, propertyKey); 19 | IsPositive({ message: "必须大于0" })(target, propertyKey); 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /app/nest-server/src/entities/ArticleCategory.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from "typeorm"; 2 | // import { Article } from "./Article"; 3 | // import { Category } from "./Category"; 4 | 5 | @Index("article_id", ["article_id"], {}) 6 | @Index("category_id", ["category_id"], {}) 7 | @Entity("article_category", { schema: "blog_db" }) 8 | export class ArticleCategory { 9 | @PrimaryGeneratedColumn({ type: "int", name: "id" }) 10 | id: number; 11 | 12 | @Column("int", { name: "article_id" }) 13 | article_id: number; 14 | 15 | @Column("int", { name: "category_id" }) 16 | category_id: number; 17 | 18 | // @ManyToOne(() => Article, (article) => article.articleCategories, { 19 | // onDelete: "CASCADE", 20 | // onUpdate: "CASCADE", 21 | // }) 22 | // @JoinColumn([{ name: "article_id", referencedColumnName: "id" }]) 23 | // article: Article; 24 | 25 | // @ManyToOne(() => Category, (category) => category.articleCategories, { 26 | // onDelete: "CASCADE", 27 | // onUpdate: "CASCADE", 28 | // }) 29 | // @JoinColumn([{ name: "category_id", referencedColumnName: "id" }]) 30 | // category: Category; 31 | } 32 | -------------------------------------------------------------------------------- /app/nest-server/src/entities/ArticleTag.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, Index, PrimaryGeneratedColumn } from "typeorm"; 2 | 3 | @Index("article_id", ["article_id"], {}) 4 | @Index("tag_id", ["tag_id"], {}) 5 | @Entity("article_tag", { schema: "blog_db" }) 6 | export class ArticleTag { 7 | @PrimaryGeneratedColumn({ type: "int", name: "id" }) 8 | id: number; 9 | 10 | @Column("int", { name: "article_id" }) 11 | article_id: number; 12 | 13 | @Column("int", { name: "tag_id" }) 14 | tag_id: number; 15 | } 16 | -------------------------------------------------------------------------------- /app/nest-server/src/entities/Authority.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, Index, OneToMany, PrimaryGeneratedColumn } from "typeorm"; 2 | // import { RoleAuth } from "./RoleAuth"; 3 | 4 | @Index("AuthName", ["authName"], { unique: true }) 5 | @Entity("authority", { schema: "blog_db" }) 6 | export class Authority { 7 | @Column("varchar", { name: "auth_name", unique: true, length: 50 }) 8 | authName: string; 9 | 10 | @PrimaryGeneratedColumn({ type: "int", name: "id" }) 11 | id: number; 12 | 13 | @Column("varchar", { name: "auth_description", nullable: true, length: 300 }) 14 | authDescription: string | null; 15 | 16 | @Column("int", { name: "parent_auth_id", nullable: true }) 17 | parentAuthId: number | null; 18 | 19 | // @OneToMany(() => RoleAuth, (roleAuth) => roleAuth.auth) 20 | // roleAuths: RoleAuth[]; 21 | } 22 | -------------------------------------------------------------------------------- /app/nest-server/src/entities/Banner.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, PrimaryGeneratedColumn } from "typeorm"; 2 | 3 | @Entity("banner", { schema: "blog_db" }) 4 | export class Banner { 5 | @PrimaryGeneratedColumn({ type: "int", name: "id" }) 6 | id: number; 7 | 8 | @Column("varchar", { name: "poster", nullable: true, length: 300 }) 9 | poster: string | null; 10 | 11 | @Column("varchar", { name: "link", nullable: true, length: 300 }) 12 | link: string | null; 13 | 14 | @Column("varchar", { name: "name", nullable: true, length: 30 }) 15 | name: string | null; 16 | 17 | @Column("int", { name: "type", nullable: true }) 18 | type: number | null; 19 | 20 | @Column("varchar", { name: "prefer_position", nullable: true, length: 20 }) 21 | prefer_position: string | null; 22 | } 23 | -------------------------------------------------------------------------------- /app/nest-server/src/entities/Category.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, Index, JoinTable, ManyToMany, OneToMany, PrimaryGeneratedColumn } from "typeorm"; 2 | import { Article } from "./Article"; 3 | // import { ArticleCategory } from "./ArticleCategory"; 4 | 5 | @Index("category_name", ["category_name"], { unique: true }) 6 | @Entity("category", { schema: "blog_db" }) 7 | export class Category { 8 | @Column("varchar", { name: "category_name", unique: true, length: 50 }) 9 | category_name: string; 10 | 11 | @PrimaryGeneratedColumn({ type: "int", name: "id" }) 12 | id: number; 13 | 14 | @Column("datetime", { 15 | name: "create_time", 16 | default: () => "CURRENT_TIMESTAMP", 17 | }) 18 | create_time: Date; 19 | 20 | @Column("datetime", { name: "update_time", nullable: true }) 21 | update_time: Date | null; 22 | 23 | @Column("varchar", { name: "poster", nullable: true, length: 300 }) 24 | poster: string | null; 25 | 26 | @ManyToMany(() => Article, (article) => article.categories) 27 | @JoinTable({ 28 | name: "article_category", 29 | joinColumn: { 30 | name: "category_id", 31 | referencedColumnName: "id", 32 | }, 33 | inverseJoinColumn: { 34 | name: "article_id", 35 | referencedColumnName: "id", 36 | }, 37 | }) 38 | articles: Article[]; 39 | } 40 | -------------------------------------------------------------------------------- /app/nest-server/src/entities/Role.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, Index, OneToMany, PrimaryGeneratedColumn } from "typeorm"; 2 | // import { RoleAuth } from "./RoleAuth"; 3 | import { User } from "./User"; 4 | 5 | @Index("RoleName", ["roleName"], { unique: true }) 6 | @Entity("role", { schema: "blog_db" }) 7 | export class Role { 8 | @PrimaryGeneratedColumn({ type: "int", name: "id" }) 9 | id: number; 10 | 11 | @Column("varchar", { name: "role_name", unique: true, length: 50 }) 12 | roleName: string; 13 | 14 | // @OneToMany(() => RoleAuth, (roleAuth) => roleAuth.role) 15 | // roleAuths: RoleAuth[]; 16 | 17 | @OneToMany(() => User, (user) => user.role) 18 | users: User[]; 19 | } 20 | -------------------------------------------------------------------------------- /app/nest-server/src/entities/RoleAuth.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from "typeorm"; 2 | // import { Role } from "./Role"; 3 | // import { Authority } from "./Authority"; 4 | 5 | @Index("role_auth_ibfk_1", ["roleId"], {}) 6 | @Index("role_auth_ibfk_2", ["authId"], {}) 7 | @Entity("role_auth", { schema: "blog_db" }) 8 | export class RoleAuth { 9 | @PrimaryGeneratedColumn({ type: "int", name: "id" }) 10 | id: number; 11 | 12 | @Column("int", { name: "role_id" }) 13 | roleId: number; 14 | 15 | @Column("int", { name: "auth_id" }) 16 | authId: number; 17 | 18 | // @ManyToOne(() => Role, (role) => role.roleAuths, { 19 | // onDelete: "CASCADE", 20 | // onUpdate: "CASCADE", 21 | // }) 22 | // @JoinColumn([{ name: "role_id", referencedColumnName: "id" }]) 23 | // role: Role; 24 | 25 | // @ManyToOne(() => Authority, (authority) => authority.roleAuths, { 26 | // onDelete: "CASCADE", 27 | // onUpdate: "CASCADE", 28 | // }) 29 | // @JoinColumn([{ name: "auth_id", referencedColumnName: "id" }]) 30 | // auth: Authority; 31 | } 32 | -------------------------------------------------------------------------------- /app/nest-server/src/entities/Tag.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, Index, JoinTable, ManyToMany, PrimaryGeneratedColumn } from "typeorm"; 2 | import { Article } from "./Article"; 3 | 4 | @Index("tag_name", ["tag_name"], { unique: true }) 5 | @Entity("tag", { schema: "blog_db" }) 6 | export class Tag { 7 | @PrimaryGeneratedColumn({ type: "int", name: "id" }) 8 | id: number; 9 | 10 | @Column("varchar", { name: "tag_name", unique: true, length: 50 }) 11 | tag_name: string; 12 | 13 | @Column("datetime", { 14 | name: "create_time", 15 | default: () => "CURRENT_TIMESTAMP", 16 | }) 17 | create_time: Date; 18 | 19 | @Column("datetime", { name: "update_time", nullable: true }) 20 | update_time: Date | null; 21 | 22 | @ManyToMany(() => Article, (article) => article.tags) 23 | @JoinTable({ 24 | name: "article_tag", // Specify the junction table name 25 | joinColumn: { 26 | name: "tag_id", // Name of the column in the junction table that refers to Tag 27 | referencedColumnName: "id", // The primary key of Tag 28 | }, 29 | inverseJoinColumn: { 30 | name: "article_id", // Name of the column in the junction table that refers to Article 31 | referencedColumnName: "id", // The primary key of Article 32 | }, 33 | }) 34 | articles: Article[]; 35 | } 36 | -------------------------------------------------------------------------------- /app/nest-server/src/exception-filters/inner.filter.ts: -------------------------------------------------------------------------------- 1 | import { ArgumentsHost, Catch, ExceptionFilter, HttpStatus } from "@nestjs/common"; 2 | import { Response } from "express"; 3 | import { InnerException } from "../exceptions/inner.exception"; 4 | 5 | @Catch(InnerException) 6 | export class InnerExceptionFilter implements ExceptionFilter { 7 | catch(exception: InnerException, host: ArgumentsHost) { 8 | console.log("InnerExceptionFilter", exception); 9 | const ctx = host.switchToHttp(); 10 | const response = ctx.getResponse(); 11 | 12 | const data = exception.getResponse() as any; 13 | 14 | console.log("InnerExceptionFilter data", data); 15 | 16 | response.status(HttpStatus.OK).json(data); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/nest-server/src/exceptions/inner.exception.ts: -------------------------------------------------------------------------------- 1 | import { HttpException, HttpStatus } from "@nestjs/common"; 2 | 3 | export class InnerException extends HttpException { 4 | constructor(code: string, msg: string) { 5 | super({ code, msg }, HttpStatus.OK); // 使用 200 状态码 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /app/nest-server/src/modules/article/article.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { ArticleController } from './article.controller'; 3 | import { ArticleService } from './article.service'; 4 | 5 | describe('ArticleController', () => { 6 | let controller: ArticleController; 7 | 8 | beforeEach(async () => { 9 | const module: TestingModule = await Test.createTestingModule({ 10 | controllers: [ArticleController], 11 | providers: [ArticleService], 12 | }).compile(); 13 | 14 | controller = module.get(ArticleController); 15 | }); 16 | 17 | it('should be defined', () => { 18 | expect(controller).toBeDefined(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /app/nest-server/src/modules/article/article.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "@nestjs/common"; 2 | import { ArticleService } from "./article.service"; 3 | import { ArticleController } from "./article.controller"; 4 | import { TypeOrmModule } from "@nestjs/typeorm"; 5 | import { Article } from "@/entities/Article"; 6 | import { Category } from "@/entities/Category"; 7 | import { Tag } from "@/entities/Tag"; 8 | import { ArticleCategory } from "@/entities/ArticleCategory"; 9 | import { ArticleTag } from "@/entities/ArticleTag"; 10 | 11 | @Module({ 12 | imports: [TypeOrmModule.forFeature([Article, Category, Tag, ArticleCategory, ArticleTag])], 13 | controllers: [ArticleController], 14 | providers: [ArticleService], 15 | }) 16 | export class ArticleModule {} 17 | -------------------------------------------------------------------------------- /app/nest-server/src/modules/article/article.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { ArticleService } from './article.service'; 3 | 4 | describe('ArticleService', () => { 5 | let service: ArticleService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [ArticleService], 10 | }).compile(); 11 | 12 | service = module.get(ArticleService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /app/nest-server/src/modules/banner/banner.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from "@nestjs/common"; 2 | import { BannerService } from "./banner.service"; 3 | import { PublicAccess } from "@/decorators/public-access.decorator"; 4 | 5 | @Controller("banner") 6 | export class BannerController { 7 | constructor(private readonly bannerService: BannerService) {} 8 | 9 | @PublicAccess() 10 | @Get("/pc") 11 | getPcBanners() { 12 | return this.bannerService.getPcBanners(); 13 | } 14 | 15 | @PublicAccess() 16 | @Get("/weapp") 17 | getWeappBanners() { 18 | return this.bannerService.getWeappBanners(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/nest-server/src/modules/banner/banner.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "@nestjs/common"; 2 | import { BannerService } from "./banner.service"; 3 | import { BannerController } from "./banner.controller"; 4 | import { Banner } from "@/entities/Banner"; 5 | import { TypeOrmModule } from "@nestjs/typeorm"; 6 | 7 | @Module({ 8 | imports: [TypeOrmModule.forFeature([Banner])], 9 | controllers: [BannerController], 10 | providers: [BannerService], 11 | }) 12 | export class BannerModule {} 13 | -------------------------------------------------------------------------------- /app/nest-server/src/modules/banner/banner.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@nestjs/common"; 2 | import { InjectRepository } from "@nestjs/typeorm"; 3 | import { Repository } from "typeorm"; 4 | import { Banner } from "@/entities/Banner"; 5 | 6 | @Injectable() 7 | export class BannerService { 8 | constructor( 9 | @InjectRepository(Banner) 10 | private readonly bannerRepository: Repository, 11 | ) {} 12 | 13 | async getPcBanners() { 14 | const data = await this.bannerRepository.find({ where: { type: 1 } }); 15 | return { 16 | data, 17 | }; 18 | } 19 | 20 | async getWeappBanners() { 21 | const data = await this.bannerRepository.find({ where: { type: 2 } }); 22 | return { 23 | data, 24 | }; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/nest-server/src/modules/category/category.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { CategoryController } from './category.controller'; 3 | import { CategoryService } from './category.service'; 4 | 5 | describe('CategoryController', () => { 6 | let controller: CategoryController; 7 | 8 | beforeEach(async () => { 9 | const module: TestingModule = await Test.createTestingModule({ 10 | controllers: [CategoryController], 11 | providers: [CategoryService], 12 | }).compile(); 13 | 14 | controller = module.get(CategoryController); 15 | }); 16 | 17 | it('should be defined', () => { 18 | expect(controller).toBeDefined(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /app/nest-server/src/modules/category/category.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Get, Put, Query } from "@nestjs/common"; 2 | import { CategoryService } from "./category.service"; 3 | import { PublicAccess } from "@/decorators/public-access.decorator"; 4 | import { FuzzyQueryCategoriesDto, GetAllCategoriesDto, GetCategoryAdminPageDto, UpdateCategoryDto } from "./dto/category.dto"; 5 | 6 | @Controller("category") 7 | export class CategoryController { 8 | constructor(private readonly categoryService: CategoryService) {} 9 | 10 | @PublicAccess() 11 | @Get("/all") 12 | getAllCategories(@Query() query: GetAllCategoriesDto) { 13 | return query.getCount === "1" ? this.categoryService.getAllCategoriesWithArticleCount() : this.categoryService.getAllCategories(); 14 | } 15 | 16 | @Get("/fuzzy") 17 | fuzzyQueryCategories(@Query() query: FuzzyQueryCategoriesDto) { 18 | return this.categoryService.fuzzyQueryCategories(query.wd); 19 | } 20 | 21 | @Get("/admin/page") 22 | getCategoryAdminPage(@Query() query: GetCategoryAdminPageDto) { 23 | return this.categoryService.getCategoryAdminPageWithArticleCount(query); 24 | } 25 | 26 | @Put("/admin/update") 27 | updateCategory(@Body() body: UpdateCategoryDto) { 28 | return this.categoryService.updateCategory(body); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/nest-server/src/modules/category/category.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "@nestjs/common"; 2 | import { CategoryService } from "./category.service"; 3 | import { CategoryController } from "./category.controller"; 4 | import { Category } from "@/entities/Category"; 5 | import { TypeOrmModule } from "@nestjs/typeorm"; 6 | 7 | @Module({ 8 | imports: [TypeOrmModule.forFeature([Category])], 9 | controllers: [CategoryController], 10 | providers: [CategoryService], 11 | }) 12 | export class CategoryModule {} 13 | -------------------------------------------------------------------------------- /app/nest-server/src/modules/category/category.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { CategoryService } from './category.service'; 3 | 4 | describe('CategoryService', () => { 5 | let service: CategoryService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [CategoryService], 10 | }).compile(); 11 | 12 | service = module.get(CategoryService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /app/nest-server/src/modules/category/dto/category.dto.ts: -------------------------------------------------------------------------------- 1 | import { QueryPositiveInt } from "@/decorators/query-number.decorator"; 2 | import { IsIn, IsInt, IsNotEmpty, IsOptional, IsPositive, IsString } from "class-validator"; 3 | 4 | export class GetAllCategoriesDto { 5 | @IsOptional() 6 | @IsString() 7 | @IsIn(["0", "1"], { message: "必须是0或1" }) 8 | getCount?: "0" | "1"; 9 | } 10 | 11 | export class FuzzyQueryCategoriesDto { 12 | @IsString() 13 | @IsNotEmpty({ message: "不能为空" }) 14 | wd: string; 15 | } 16 | 17 | export class GetCategoryAdminPageDto { 18 | @IsOptional() 19 | @QueryPositiveInt(1) 20 | pageNo: number = 1; 21 | 22 | @IsOptional() 23 | @QueryPositiveInt(10) 24 | pageSize: number = 10; 25 | } 26 | 27 | export class UpdateCategoryDto { 28 | @IsInt() 29 | @IsPositive() 30 | id?: number; 31 | 32 | @IsString() 33 | @IsNotEmpty() 34 | category_name: string; 35 | 36 | @IsOptional() 37 | @IsString() 38 | poster?: string; 39 | } 40 | -------------------------------------------------------------------------------- /app/nest-server/src/modules/chat/chat.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "@nestjs/common"; 2 | import { ChatGateway } from "./chat.gateway"; 3 | 4 | @Module({ 5 | providers: [ChatGateway], 6 | }) 7 | export class ChatModule {} 8 | -------------------------------------------------------------------------------- /app/nest-server/src/modules/chatgpt/chatgpt.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "@nestjs/common"; 2 | import { ChatgptController } from "./chatgpt.controller"; 3 | 4 | @Module({ 5 | controllers: [ChatgptController], 6 | providers: [], 7 | }) 8 | export class ChatgptModule {} 9 | -------------------------------------------------------------------------------- /app/nest-server/src/modules/comment/comment.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { CommentController } from './comment.controller'; 3 | import { CommentService } from './comment.service'; 4 | 5 | describe('CommentController', () => { 6 | let controller: CommentController; 7 | 8 | beforeEach(async () => { 9 | const module: TestingModule = await Test.createTestingModule({ 10 | controllers: [CommentController], 11 | providers: [CommentService], 12 | }).compile(); 13 | 14 | controller = module.get(CommentController); 15 | }); 16 | 17 | it('should be defined', () => { 18 | expect(controller).toBeDefined(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /app/nest-server/src/modules/comment/comment.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "@nestjs/common"; 2 | import { CommentService } from "./comment.service"; 3 | import { CommentController } from "./comment.controller"; 4 | import { TypeOrmModule } from "@nestjs/typeorm"; 5 | import { Comments } from "@/entities/Comments"; 6 | import { Reply } from "@/entities/Reply"; 7 | 8 | @Module({ 9 | imports: [TypeOrmModule.forFeature([Comments, Reply])], 10 | controllers: [CommentController], 11 | providers: [CommentService], 12 | }) 13 | export class CommentModule {} 14 | -------------------------------------------------------------------------------- /app/nest-server/src/modules/comment/comment.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { CommentService } from './comment.service'; 3 | 4 | describe('CommentService', () => { 5 | let service: CommentService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [CommentService], 10 | }).compile(); 11 | 12 | service = module.get(CommentService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /app/nest-server/src/modules/common/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { ForbiddenException, Injectable } from "@nestjs/common"; 2 | import { ConfigService } from "@nestjs/config"; 3 | import { JwtService } from "@nestjs/jwt"; 4 | 5 | @Injectable() 6 | export class AuthService { 7 | constructor( 8 | private readonly configService: ConfigService, 9 | private readonly jwtService: JwtService, 10 | ) {} 11 | 12 | extractToken(authorization: string = ""): string | undefined { 13 | const [type, token] = authorization.split(" ") ?? []; 14 | return type === "Bearer" ? token : undefined; 15 | } 16 | 17 | async sign(payload: any) { 18 | const result = await this.jwtService.signAsync(payload); 19 | return result; 20 | } 21 | 22 | async verify(token: string) { 23 | const payload = await this.jwtService.verifyAsync(token, { 24 | secret: this.configService.get("JWT_SECRET"), 25 | }); 26 | return payload; 27 | } 28 | 29 | async getCurrentUser(authorization: string | undefined) { 30 | if (!authorization) { 31 | throw new ForbiddenException("请先登录"); 32 | } 33 | const token = this.extractToken(authorization); 34 | if (!token) { 35 | throw new ForbiddenException("请先登录"); 36 | } 37 | return this.verify(token); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/nest-server/src/modules/common/common.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, Module } from "@nestjs/common"; 2 | import { EmailService } from "./email.service"; 3 | import { AuthService } from "./auth.service"; 4 | 5 | @Global() 6 | @Module({ 7 | providers: [EmailService, AuthService], 8 | exports: [EmailService, AuthService], 9 | }) 10 | export class CommonModule {} 11 | -------------------------------------------------------------------------------- /app/nest-server/src/modules/reply/dto/reply.dto.ts: -------------------------------------------------------------------------------- 1 | import { BodyPositiveInt } from "@/decorators/body-number.decorator"; 2 | import { Type } from "class-transformer"; 3 | import { IsOptional, IsNotEmpty, IsString, IsIn } from "class-validator"; 4 | 5 | export class AddReplyDto { 6 | @BodyPositiveInt() 7 | comment_id: number; 8 | 9 | @IsString() 10 | @IsNotEmpty() 11 | content: string; 12 | 13 | @IsString() 14 | @IsNotEmpty() 15 | nick_name: string; 16 | 17 | @IsOptional() 18 | @BodyPositiveInt() 19 | parent_id: number; 20 | 21 | @IsOptional() 22 | @BodyPositiveInt() 23 | article_id: number; 24 | 25 | @IsOptional() 26 | @IsString() 27 | jump_url: string; 28 | 29 | @IsOptional() 30 | @IsString() 31 | email: string; 32 | 33 | @IsOptional() 34 | @IsString() 35 | site_url: string; 36 | 37 | @IsOptional() 38 | @IsString() 39 | avatar: string; 40 | 41 | @IsOptional() 42 | @IsString() 43 | device: string; 44 | } 45 | 46 | export class ReviewReplyDto { 47 | @BodyPositiveInt() 48 | id: number; 49 | 50 | @IsIn([1, 2, "1", "2"], { message: "必须是1或2,1是审核通过,2是审核不通过" }) 51 | @Type(() => Number) 52 | approved: 1 | 2; 53 | 54 | @IsString() 55 | @IsNotEmpty() 56 | content: string; 57 | 58 | @IsOptional() 59 | @IsString() 60 | email: string; 61 | 62 | @IsOptional() 63 | @IsString() 64 | jump_url: string; 65 | } 66 | -------------------------------------------------------------------------------- /app/nest-server/src/modules/reply/reply.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { ReplyController } from './reply.controller'; 3 | import { ReplyService } from './reply.service'; 4 | 5 | describe('ReplyController', () => { 6 | let controller: ReplyController; 7 | 8 | beforeEach(async () => { 9 | const module: TestingModule = await Test.createTestingModule({ 10 | controllers: [ReplyController], 11 | providers: [ReplyService], 12 | }).compile(); 13 | 14 | controller = module.get(ReplyController); 15 | }); 16 | 17 | it('should be defined', () => { 18 | expect(controller).toBeDefined(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /app/nest-server/src/modules/reply/reply.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Post, Body, Get, Query, Put } from "@nestjs/common"; 2 | import { ReplyService } from "./reply.service"; 3 | import { AddReplyDto, ReviewReplyDto } from "./dto/reply.dto"; 4 | import { PublicAccess } from "@/decorators/public-access.decorator"; 5 | import { GetTypedPageDto } from "../comment/dto/comment.dto"; 6 | 7 | @Controller("reply") 8 | export class ReplyController { 9 | constructor(private readonly replyService: ReplyService) {} 10 | 11 | @PublicAccess() 12 | @Post("/add") 13 | add(@Body() body: AddReplyDto) { 14 | return this.replyService.add(body); 15 | } 16 | 17 | @Get("/getReplyOfCommentWaitReview") 18 | getReplyOfCommentWaitReview() { 19 | return this.replyService.getReplyOfCommentWaitReview(); 20 | } 21 | 22 | @Get("/getReplyOfMsgWaitReview") 23 | getReplyOfMsgWaitReview() { 24 | return this.replyService.getReplyOfMsgWaitReview(); 25 | } 26 | 27 | @Get("/unreviewd_reply_page") 28 | unreviewdReplyPage(@Query() query: GetTypedPageDto) { 29 | return this.replyService.getUnreviewdReplyPage(query); 30 | } 31 | 32 | @Put("/review") 33 | review(@Body() body: ReviewReplyDto) { 34 | return this.replyService.review(body); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/nest-server/src/modules/reply/reply.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "@nestjs/common"; 2 | import { ReplyService } from "./reply.service"; 3 | import { ReplyController } from "./reply.controller"; 4 | import { Reply } from "@/entities/Reply"; 5 | import { TypeOrmModule } from "@nestjs/typeorm"; 6 | import { Comments } from "@/entities/Comments"; 7 | import { Article } from "@/entities/Article"; 8 | 9 | @Module({ 10 | imports: [TypeOrmModule.forFeature([Reply, Comments, Article])], 11 | controllers: [ReplyController], 12 | providers: [ReplyService], 13 | }) 14 | export class ReplyModule {} 15 | -------------------------------------------------------------------------------- /app/nest-server/src/modules/reply/reply.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { ReplyService } from './reply.service'; 3 | 4 | describe('ReplyService', () => { 5 | let service: ReplyService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [ReplyService], 10 | }).compile(); 11 | 12 | service = module.get(ReplyService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /app/nest-server/src/modules/tag/dto/tag.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsIn, IsNotEmpty, IsOptional, IsString } from "class-validator"; 2 | import { QueryPositiveInt } from "@/decorators/query-number.decorator"; 3 | 4 | export class GetAllTagsDto { 5 | @IsOptional() 6 | @IsString() 7 | @IsIn(["0", "1"], { message: "必须是0或1" }) 8 | getCount?: "0" | "1"; 9 | } 10 | 11 | export class FuzzyQueryTagsDto { 12 | @IsString() 13 | @IsNotEmpty({ message: "不能为空" }) 14 | wd: string; 15 | } 16 | export class GetTagAdminPageDto { 17 | @IsOptional() 18 | @QueryPositiveInt(1) 19 | pageNo: number = 1; 20 | 21 | @IsOptional() 22 | @QueryPositiveInt(10) 23 | pageSize: number = 10; 24 | } 25 | -------------------------------------------------------------------------------- /app/nest-server/src/modules/tag/tag.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { TagController } from './tag.controller'; 3 | import { TagService } from './tag.service'; 4 | 5 | describe('TagController', () => { 6 | let controller: TagController; 7 | 8 | beforeEach(async () => { 9 | const module: TestingModule = await Test.createTestingModule({ 10 | controllers: [TagController], 11 | providers: [TagService], 12 | }).compile(); 13 | 14 | controller = module.get(TagController); 15 | }); 16 | 17 | it('should be defined', () => { 18 | expect(controller).toBeDefined(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /app/nest-server/src/modules/tag/tag.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Query } from "@nestjs/common"; 2 | import { TagService } from "./tag.service"; 3 | import { FuzzyQueryTagsDto, GetAllTagsDto, GetTagAdminPageDto } from "./dto/tag.dto"; 4 | import { PublicAccess } from "@/decorators/public-access.decorator"; 5 | 6 | @Controller("tag") 7 | export class TagController { 8 | constructor(private readonly tagService: TagService) {} 9 | 10 | @PublicAccess() 11 | @Get("/all") 12 | getAllTags(@Query() query: GetAllTagsDto) { 13 | return query.getCount === "1" ? this.tagService.getAllTagsWithArticleCount() : this.tagService.getAllTags(); 14 | } 15 | 16 | @Get("/fuzzy") 17 | fuzzyQueryTags(@Query() query: FuzzyQueryTagsDto) { 18 | return this.tagService.fuzzyQueryTags(query.wd); 19 | } 20 | 21 | @Get("/admin/page") 22 | getTagAdminPage(@Query() query: GetTagAdminPageDto) { 23 | return this.tagService.getTagAdminPageWithArticleCount(query); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/nest-server/src/modules/tag/tag.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "@nestjs/common"; 2 | import { TagService } from "./tag.service"; 3 | import { TagController } from "./tag.controller"; 4 | import { Tag } from "src/entities/Tag"; 5 | import { TypeOrmModule } from "@nestjs/typeorm"; 6 | import { Article } from "@/entities/Article"; 7 | 8 | @Module({ 9 | imports: [TypeOrmModule.forFeature([Tag, Article])], 10 | controllers: [TagController], 11 | providers: [TagService], 12 | }) 13 | export class TagModule {} 14 | -------------------------------------------------------------------------------- /app/nest-server/src/modules/tag/tag.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { TagService } from './tag.service'; 3 | 4 | describe('TagService', () => { 5 | let service: TagService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [TagService], 10 | }).compile(); 11 | 12 | service = module.get(TagService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /app/nest-server/src/modules/user/dto/login.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, IsString } from "class-validator"; 2 | 3 | export class LoginDto { 4 | @IsNotEmpty() 5 | @IsString() 6 | userName: string; 7 | 8 | @IsNotEmpty() 9 | @IsString() 10 | password: string; 11 | 12 | @IsNotEmpty() 13 | @IsString() 14 | captcha: string; 15 | } 16 | -------------------------------------------------------------------------------- /app/nest-server/src/modules/user/user.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { UserController } from './user.controller'; 3 | import { UserService } from './user.service'; 4 | 5 | describe('UserController', () => { 6 | let controller: UserController; 7 | 8 | beforeEach(async () => { 9 | const module: TestingModule = await Test.createTestingModule({ 10 | controllers: [UserController], 11 | providers: [UserService], 12 | }).compile(); 13 | 14 | controller = module.get(UserController); 15 | }); 16 | 17 | it('should be defined', () => { 18 | expect(controller).toBeDefined(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /app/nest-server/src/modules/user/user.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Body, Put, Req, Get } from "@nestjs/common"; 2 | import { UserService } from "./user.service"; 3 | import { LoginDto } from "./dto/login.dto"; 4 | import { Request } from "express"; 5 | import { PublicAccess } from "@/decorators/public-access.decorator"; 6 | 7 | @Controller("user") 8 | export class UserController { 9 | constructor(private readonly userService: UserService) {} 10 | 11 | @PublicAccess() 12 | @Put("/login") 13 | login(@Body() loginDto: LoginDto, @Req() req: Request) { 14 | const sessionCaptcha = req.session.captcha; 15 | return this.userService.login(loginDto, sessionCaptcha); 16 | } 17 | 18 | @Put("/logout") 19 | logout() { 20 | // TODO: jwt 进入黑名单 21 | return {}; 22 | } 23 | 24 | @Get("/current") 25 | current(@Req() req: Request) { 26 | return { 27 | data: req.currentUser, 28 | }; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/nest-server/src/modules/user/user.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "@nestjs/common"; 2 | import { UserService } from "./user.service"; 3 | import { UserController } from "./user.controller"; 4 | import { TypeOrmModule } from "@nestjs/typeorm"; 5 | import { User } from "@/entities/User"; 6 | import { Role } from "@/entities/Role"; 7 | import { JwtModule } from "@nestjs/jwt"; 8 | import { ConfigService } from "@nestjs/config"; 9 | 10 | @Module({ 11 | imports: [ 12 | TypeOrmModule.forFeature([User, Role]), 13 | JwtModule.registerAsync({ 14 | global: true, 15 | inject: [ConfigService], 16 | useFactory: (configService: ConfigService) => ({ 17 | secret: configService.get("JWT_SECRET"), 18 | signOptions: { 19 | expiresIn: `3d`, 20 | }, 21 | }), 22 | }), 23 | ], 24 | controllers: [UserController], 25 | providers: [UserService], 26 | }) 27 | export class UserModule {} 28 | -------------------------------------------------------------------------------- /app/nest-server/src/modules/user/user.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { UserService } from './user.service'; 3 | 4 | describe('UserService', () => { 5 | let service: UserService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [UserService], 10 | }).compile(); 11 | 12 | service = module.get(UserService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /app/nest-server/src/modules/validator/validator.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { ValidatorController } from './validator.controller'; 3 | import { ValidatorService } from './validator.service'; 4 | 5 | describe('ValidatorController', () => { 6 | let controller: ValidatorController; 7 | 8 | beforeEach(async () => { 9 | const module: TestingModule = await Test.createTestingModule({ 10 | controllers: [ValidatorController], 11 | providers: [ValidatorService], 12 | }).compile(); 13 | 14 | controller = module.get(ValidatorController); 15 | }); 16 | 17 | it('should be defined', () => { 18 | expect(controller).toBeDefined(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /app/nest-server/src/modules/validator/validator.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Req } from "@nestjs/common"; 2 | import { ValidatorService } from "./validator.service"; 3 | import { Request } from "express"; 4 | import { PublicAccess } from "@/decorators/public-access.decorator"; 5 | 6 | @Controller("validator") 7 | export class ValidatorController { 8 | constructor(private readonly validatorService: ValidatorService) {} 9 | 10 | @PublicAccess() 11 | @Get("/img_code") 12 | getImgCode(@Req() req: Request) { 13 | const { data, text } = this.validatorService.getImgCode(); 14 | console.log(text); 15 | req.session.captcha = text; 16 | return { data }; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/nest-server/src/modules/validator/validator.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "@nestjs/common"; 2 | import { ValidatorService } from "./validator.service"; 3 | import { ValidatorController } from "./validator.controller"; 4 | 5 | @Module({ 6 | controllers: [ValidatorController], 7 | providers: [ValidatorService], 8 | }) 9 | export class ValidatorModule {} 10 | -------------------------------------------------------------------------------- /app/nest-server/src/modules/validator/validator.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { ValidatorService } from './validator.service'; 3 | 4 | describe('ValidatorService', () => { 5 | let service: ValidatorService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [ValidatorService], 10 | }).compile(); 11 | 12 | service = module.get(ValidatorService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /app/nest-server/src/modules/validator/validator.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@nestjs/common"; 2 | import * as svgCaptcha from "svg-captcha"; 3 | 4 | @Injectable() 5 | export class ValidatorService { 6 | getImgCode() { 7 | return svgCaptcha.create({ 8 | // 验证码长度 9 | size: 5, 10 | // 要排除的字符 11 | ignoreChars: "o0i1", 12 | // 干扰线条数量 13 | noise: 1, 14 | // 验证码的字符是否有颜色,默认没有,如果设定了背景,则默认有 15 | color: true, 16 | // 验证码图片背景颜色 17 | background: "#4dffc9", 18 | // 宽度 19 | width: 150, 20 | // 高度 21 | height: 50, 22 | // viewBox 23 | viewBox: "0,0,150,50", 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/nest-server/src/types/base.ts: -------------------------------------------------------------------------------- 1 | type IndexType = string | number | symbol; 2 | 3 | export type PlainObject = Record; 4 | 5 | export type PrimitiveType = number | string | boolean | undefined | null | symbol; 6 | 7 | export type GeneralFunction = (...args: any[]) => T; 8 | 9 | export interface PlainNode extends PlainObject { 10 | id: number; 11 | } 12 | 13 | export interface TreeNode extends PlainObject { 14 | key: string; 15 | children?: this[]; 16 | } 17 | -------------------------------------------------------------------------------- /app/nest-server/src/types/express.d.ts: -------------------------------------------------------------------------------- 1 | import "express"; 2 | 3 | declare module "express" { 4 | interface Request { 5 | currentUser: Record; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /app/nest-server/src/types/session.d.ts: -------------------------------------------------------------------------------- 1 | import "express-session"; 2 | 3 | declare module "express-session" { 4 | interface Session { 5 | captcha: string; 6 | // 在这里添加其他需要的 session 属性 7 | chatgptTimes?: number; 8 | chatgptTopicCount?: number; 9 | chatgptSessionPrompt?: string; 10 | chatgptRequestTime?: number; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/nest-server/src/types/svg-captcha.d.ts: -------------------------------------------------------------------------------- 1 | import "svg-captcha"; 2 | 3 | declare module "svg-captcha" { 4 | interface ConfigObject { 5 | viewBox?: string; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /app/nest-server/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /app/nest-server/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /app/nest-server/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /app/nest-server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "esModuleInterop": true, 10 | "target": "ES2021", 11 | "sourceMap": true, 12 | "outDir": "./dist", 13 | "baseUrl": "./", 14 | "paths": { 15 | "@/*": ["src/*"] 16 | }, 17 | "incremental": true, 18 | "skipLibCheck": true, 19 | "strictNullChecks": false, 20 | "noImplicitAny": false, 21 | "strictBindCallApply": false, 22 | "forceConsistentCasingInFileNames": false, 23 | "noFallthroughCasesInSwitch": false 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/nuxt3-web/.gitignore: -------------------------------------------------------------------------------- 1 | # Nuxt dev/build outputs 2 | .output 3 | .data 4 | .nuxt 5 | .nitro 6 | .cache 7 | dist 8 | 9 | # Node dependencies 10 | node_modules 11 | 12 | # Logs 13 | logs 14 | *.log 15 | 16 | # Misc 17 | .DS_Store 18 | .fleet 19 | .idea 20 | 21 | # Local env files 22 | .env 23 | .env.* 24 | !.env.example 25 | -------------------------------------------------------------------------------- /app/nuxt3-web/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabs": false, 3 | "tabWidth": 4, 4 | "endOfLine": "auto", 5 | "printWidth": 140 6 | } 7 | -------------------------------------------------------------------------------- /app/nuxt3-web/.stylelintrc.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('stylelint').Config} */ 2 | export default { 3 | extends: ["stylelint-config-standard", "stylelint-config-standard-scss", "stylelint-config-recommended-vue"], 4 | }; 5 | -------------------------------------------------------------------------------- /app/nuxt3-web/README.md: -------------------------------------------------------------------------------- 1 | # Nuxt Minimal Starter 2 | 3 | Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more. 4 | 5 | ## Setup 6 | 7 | Make sure to install dependencies: 8 | 9 | ```bash 10 | # npm 11 | npm install 12 | 13 | # pnpm 14 | pnpm install 15 | 16 | # yarn 17 | yarn install 18 | 19 | # bun 20 | bun install 21 | ``` 22 | 23 | ## Development Server 24 | 25 | Start the development server on `http://localhost:3000`: 26 | 27 | ```bash 28 | # npm 29 | npm run dev 30 | 31 | # pnpm 32 | pnpm dev 33 | 34 | # yarn 35 | yarn dev 36 | 37 | # bun 38 | bun run dev 39 | ``` 40 | 41 | ## Production 42 | 43 | Build the application for production: 44 | 45 | ```bash 46 | # npm 47 | npm run build 48 | 49 | # pnpm 50 | pnpm build 51 | 52 | # yarn 53 | yarn build 54 | 55 | # bun 56 | bun run build 57 | ``` 58 | 59 | Locally preview production build: 60 | 61 | ```bash 62 | # npm 63 | npm run preview 64 | 65 | # pnpm 66 | pnpm preview 67 | 68 | # yarn 69 | yarn preview 70 | 71 | # bun 72 | bun run preview 73 | ``` 74 | 75 | Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information. 76 | -------------------------------------------------------------------------------- /app/nuxt3-web/app.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 16 | -------------------------------------------------------------------------------- /app/nuxt3-web/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import globals from "globals"; 2 | import pluginJs from "@eslint/js"; 3 | import eslintConfigPrettier from "eslint-config-prettier"; 4 | import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended"; 5 | import tsEslint from "typescript-eslint"; 6 | import eslintPluginVue from "eslint-plugin-vue"; 7 | import vueParser from "vue-eslint-parser"; 8 | 9 | export default tsEslint.config( 10 | { ignores: ["node_modules", ".nuxt", ".output", "*.d.ts"] }, 11 | { 12 | extends: [pluginJs.configs.recommended, ...tsEslint.configs.recommended, ...eslintPluginVue.configs["flat/recommended"]], 13 | files: ["**/*.{js,ts,tsx,vue}"], 14 | languageOptions: { 15 | ecmaVersion: "latest", 16 | sourceType: "module", 17 | globals: globals.browser, 18 | parser: vueParser, 19 | parserOptions: { 20 | parser: tsEslint.parser, 21 | }, 22 | }, 23 | rules: { 24 | "prettier/prettier": "error", 25 | "no-unused-vars": "warn", 26 | }, 27 | }, 28 | eslintPluginPrettierRecommended, 29 | eslintConfigPrettier, 30 | ); 31 | -------------------------------------------------------------------------------- /app/nuxt3-web/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | // https://nuxt.com/docs/api/configuration/nuxt-config 2 | export default defineNuxtConfig({ 3 | compatibilityDate: "2024-11-01", 4 | devtools: { enabled: true }, 5 | }); 6 | -------------------------------------------------------------------------------- /app/nuxt3-web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxt3-web", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "build": "nuxt build", 7 | "dev": "nuxt dev", 8 | "generate": "nuxt generate", 9 | "preview": "nuxt preview", 10 | "postinstall": "nuxt prepare", 11 | "lint": "eslint --ext .js,.jsx,.ts,.tsx,.vue --cache", 12 | "lint-fix": "eslint --ext .js,.jsx,.ts,.tsx,.vue --fix --cache", 13 | "lint-style": "stylelint **/*.{vue,css,less,scss} --cache", 14 | "fix-style": "stylelint **/*.{vue,css,less,scss} --cache --fix" 15 | }, 16 | "dependencies": { 17 | "nuxt": "^3.15.4", 18 | "vue": "latest", 19 | "vue-router": "latest" 20 | }, 21 | "devDependencies": { 22 | "@eslint/js": "^9.22.0", 23 | "eslint": "^9.22.0", 24 | "eslint-config-prettier": "^10.1.1", 25 | "eslint-plugin-prettier": "^5.2.3", 26 | "eslint-plugin-vue": "^10.0.0", 27 | "globals": "^16.0.0", 28 | "less": "^4.2.0", 29 | "postcss-html": "^1.8.0", 30 | "prettier": "^3.5.3", 31 | "stylelint": "~16.8.1", 32 | "stylelint-config-recommended-vue": "^1.5.0", 33 | "stylelint-config-standard": "^36.0.1", 34 | "stylelint-config-standard-scss": "^14.0.0", 35 | "tslib": "^2.8.1", 36 | "typescript": "^5.8.2", 37 | "typescript-eslint": "^8.26.0", 38 | "vue-eslint-parser": "^10.1.1" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/nuxt3-web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cumt-robin/fullstack-blog/02a1377d007f0b6906cac66a1d6829bc998b72ff/app/nuxt3-web/public/favicon.ico -------------------------------------------------------------------------------- /app/nuxt3-web/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.nuxt/tsconfig.server.json" 3 | } 4 | -------------------------------------------------------------------------------- /app/nuxt3-web/test.less: -------------------------------------------------------------------------------- 1 | .a { 2 | position: relative; 3 | width: 100px; 4 | height: 100px; 5 | background-color: red; 6 | } 7 | -------------------------------------------------------------------------------- /app/nuxt3-web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://nuxt.com/docs/guide/concepts/typescript 3 | "extends": "./.nuxt/tsconfig.json" 4 | } 5 | -------------------------------------------------------------------------------- /app/vite-vue3/.env: -------------------------------------------------------------------------------- 1 | # 环境变量 2 | VITE_APP_TITLE=Tusi博客 3 | VITE_APP_BASE_API=/api 4 | -------------------------------------------------------------------------------- /app/vite-vue3/.env.development: -------------------------------------------------------------------------------- 1 | # 开发环境变量 2 | VITE_APP_SOCKET_SERVER=http://127.0.0.1:8012 3 | -------------------------------------------------------------------------------- /app/vite-vue3/.env.production: -------------------------------------------------------------------------------- 1 | # 生产环境变量 2 | VITE_APP_SOCKET_SERVER="" 3 | -------------------------------------------------------------------------------- /app/vite-vue3/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | src/**/*.d.ts 3 | dist 4 | .eslintrc.cjs 5 | public 6 | -------------------------------------------------------------------------------- /app/vite-vue3/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = { 4 | root: true, 5 | env: { 6 | node: true, 7 | }, 8 | parser: "vue-eslint-parser", 9 | parserOptions: { 10 | // for script 11 | parser: "@typescript-eslint/parser", 12 | ecmaVersion: 2020, 13 | }, 14 | extends: [ 15 | "@fullstack-blog/eslint-config/base.js", 16 | "plugin:vue/vue3-strongly-recommended", 17 | "@vue/typescript/recommended", 18 | "@vue/prettier", 19 | "@vue/prettier/@typescript-eslint", 20 | ], 21 | plugins: ["@typescript-eslint", "vue"], 22 | rules: { 23 | "no-debugger": 'error', 24 | "no-case-declarations": 'off', 25 | "import/order": 'warn', 26 | "import/no-unresolved": 'off', 27 | // https://eslint.vuejs.org/rules/ 28 | "vue/require-default-prop": 'off', 29 | '@typescript-eslint/explicit-module-boundary-types': 'off', 30 | }, 31 | overrides: [ 32 | { 33 | files: ["**/__tests__/*.{j,t}s?(x)", "**/tests/unit/**/*.spec.{j,t}s?(x)"], 34 | env: { 35 | jest: true, 36 | }, 37 | }, 38 | { 39 | files: ["*.ts", "*.tsx", "*.vue"], 40 | plugins: ["@typescript-eslint"], 41 | rules: { 42 | "no-shadow": "off", 43 | "@typescript-eslint/no-explicit-any": [2, { ignoreRestArgs: true }], 44 | }, 45 | }, 46 | ], 47 | }; 48 | -------------------------------------------------------------------------------- /app/vite-vue3/.gitignore: -------------------------------------------------------------------------------- 1 | components.d.ts 2 | -------------------------------------------------------------------------------- /app/vite-vue3/.stylelintrc.cjs: -------------------------------------------------------------------------------- 1 | /** 2 | * @author: Tusi 3 | * @description: stylelint配置 4 | */ 5 | module.exports = { 6 | extends: ["stylelint-config-standard", "stylelint-prettier/recommended", "stylelint-config-recommended-vue"], 7 | plugins: ["stylelint-scss", "stylelint-prettier"], 8 | rules: { 9 | 'prettier/prettier': true, 10 | "at-rule-no-unknown": null, 11 | "no-descending-specificity": null, 12 | "selector-pseudo-element-no-unknown": [ 13 | true, 14 | { 15 | ignorePseudoElements: ["deep"], 16 | }, 17 | ], 18 | "selector-pseudo-class-no-unknown": [ 19 | true, 20 | { 21 | ignorePseudoClasses: ["deep", "global"], 22 | }, 23 | ], 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /app/vite-vue3/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # vite-vue3 2 | 3 | ## 1.0.13 4 | 5 | ### Patch Changes 6 | 7 | - 606b53e: 添加 ads.txt 8 | 9 | ## 1.0.12 10 | 11 | ### Patch Changes 12 | 13 | - 7356046: 引入 google ads 14 | 15 | ## 1.0.11 16 | 17 | ### Patch Changes 18 | 19 | - 5d42650: fix: socket.io v4 连接异常问题 20 | 21 | ## 1.0.10 22 | 23 | ### Patch Changes 24 | 25 | - 373b310: fix: nest 采用 socket.io v4 版本,生产环境下前端连接 socket.io 报错 26 | 27 | ## 1.0.9 28 | 29 | ### Patch Changes 30 | 31 | - 3aab4d5: feat: nestjs socket.io 集成 32 | 33 | ## 1.0.8 34 | 35 | ### Patch Changes 36 | 37 | - 0e82de4: chore: nginx backend 引用错误 38 | 39 | ## 1.0.7 40 | 41 | ### Patch Changes 42 | 43 | - 4a2edf3: chore: backend 项目改为 express-server 44 | 45 | ## 1.0.6 46 | 47 | ### Patch Changes 48 | 49 | - 3bad363: ci: 重新部署 50 | 51 | ## 1.0.5 52 | 53 | ### Patch Changes 54 | 55 | - 7303cbe: ci: 支持 webpack-vue3 部署 56 | 57 | ## 1.0.4 58 | 59 | ### Patch Changes 60 | 61 | - e5a4cf8: chore: 增加 robots.txt 配置 62 | 63 | ## 1.0.3 64 | 65 | ### Patch Changes 66 | 67 | - b1cc483: fix: vite-vue3 项目百度统计代码 68 | 69 | ## 1.0.2 70 | 71 | ### Patch Changes 72 | 73 | - 0f24738: fix: 部分校验 bug 修复 74 | 75 | ## 1.0.1 76 | 77 | ### Patch Changes 78 | 79 | - c1eb96f: fix: Comment Controller 参数校验修改 80 | - c9f550e: fix: Reply Controller 参数校验修改 81 | -------------------------------------------------------------------------------- /app/vite-vue3/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Tusi博客 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/vite-vue3/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@/*": ["./src/*"] 6 | } 7 | }, 8 | "include": ["src/**/*"] 9 | } 10 | -------------------------------------------------------------------------------- /app/vite-vue3/public/ads.txt: -------------------------------------------------------------------------------- 1 | google.com, pub-4690279684125430, DIRECT, f08c47fec0942fa0 2 | -------------------------------------------------------------------------------- /app/vite-vue3/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cumt-robin/fullstack-blog/02a1377d007f0b6906cac66a1d6829bc998b72ff/app/vite-vue3/public/favicon.ico -------------------------------------------------------------------------------- /app/vite-vue3/public/js/hm.js: -------------------------------------------------------------------------------- 1 | var _hmt = _hmt || []; 2 | (function () { 3 | var hm = document.createElement("script"); 4 | hm.src = "https://hm.baidu.com/hm.js?d2feba2eac8bedae244304195f7b064f"; 5 | var s = document.getElementsByTagName("script")[0]; 6 | s.parentNode.insertBefore(hm, s); 7 | })(); 8 | -------------------------------------------------------------------------------- /app/vite-vue3/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: /backend 3 | Disallow: /login 4 | Disallow: /jumpout 5 | -------------------------------------------------------------------------------- /app/vite-vue3/src/App.vue: -------------------------------------------------------------------------------- 1 | 5 | 20 | 21 | 45 | 46 | 54 | -------------------------------------------------------------------------------- /app/vite-vue3/src/assets/img/avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cumt-robin/fullstack-blog/02a1377d007f0b6906cac66a1d6829bc998b72ff/app/vite-vue3/src/assets/img/avatar.jpg -------------------------------------------------------------------------------- /app/vite-vue3/src/assets/img/chat-avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cumt-robin/fullstack-blog/02a1377d007f0b6906cac66a1d6829bc998b72ff/app/vite-vue3/src/assets/img/chat-avatar.png -------------------------------------------------------------------------------- /app/vite-vue3/src/assets/img/comment-avatar.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/vite-vue3/src/assets/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cumt-robin/fullstack-blog/02a1377d007f0b6906cac66a1d6829bc998b72ff/app/vite-vue3/src/assets/img/logo.png -------------------------------------------------------------------------------- /app/vite-vue3/src/assets/img/logo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cumt-robin/fullstack-blog/02a1377d007f0b6906cac66a1d6829bc998b72ff/app/vite-vue3/src/assets/img/logo2.png -------------------------------------------------------------------------------- /app/vite-vue3/src/assets/img/reply-avatar.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/vite-vue3/src/assets/img/wechat_payme.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cumt-robin/fullstack-blog/02a1377d007f0b6906cac66a1d6829bc998b72ff/app/vite-vue3/src/assets/img/wechat_payme.jpg -------------------------------------------------------------------------------- /app/vite-vue3/src/assets/vue.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/vite-vue3/src/bean/base.ts: -------------------------------------------------------------------------------- 1 | import { defineComponent } from "vue"; 2 | 3 | type IndexType = string | number | symbol; 4 | 5 | export type PlainObject = Record; 6 | 7 | export type PrimitiveType = number | string | boolean | undefined | null | symbol; 8 | 9 | export type GeneralFunction = (...args: any[]) => T; 10 | 11 | export interface PlainNode extends PlainObject { 12 | id: number; 13 | } 14 | 15 | export interface TreeNode extends PlainObject { 16 | key: string; 17 | children?: this[]; 18 | } 19 | 20 | export type Lazy = () => Promise; 21 | 22 | export type DefineComponentOptions = Parameters[0]; 23 | -------------------------------------------------------------------------------- /app/vite-vue3/src/components/my-button/index.vue: -------------------------------------------------------------------------------- 1 | 5 | 13 | 14 | 33 | -------------------------------------------------------------------------------- /app/vite-vue3/src/directives/index.ts: -------------------------------------------------------------------------------- 1 | import { App } from "vue"; 2 | import Lazyload from "./lazyload"; 3 | 4 | export default { 5 | install(app: App): void { 6 | app.directive("lazyload", Lazyload); 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /app/vite-vue3/src/hooks/async.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author: Tusi 3 | * @description: 异步 Hook,用于 loading, error 等状态反馈 4 | */ 5 | import { Ref, ref } from "vue"; 6 | import { GeneralFunction } from "@/bean/base"; 7 | 8 | interface AsyncLoadingResponse { 9 | trigger: GeneralFunction; 10 | loading: Ref; 11 | isError: Ref; 12 | error: Ref; 13 | } 14 | 15 | export const useAsyncLoading = (fn: GeneralFunction>): AsyncLoadingResponse => { 16 | const loading = ref(false); 17 | const isError = ref(false); 18 | const error = ref(); 19 | const trigger = async (...args: any[]) => { 20 | try { 21 | loading.value = true; 22 | await fn(...args); 23 | } catch (err) { 24 | isError.value = true; 25 | error.value = err; 26 | } finally { 27 | loading.value = false; 28 | } 29 | }; 30 | return { trigger, loading, isError, error }; 31 | }; 32 | -------------------------------------------------------------------------------- /app/vite-vue3/src/jshashes.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author: Tusi 3 | * @description: jshashes类型定义 4 | */ 5 | declare module "jshashes" { 6 | export class SHA256 { 7 | public hex: (string) => string; 8 | } 9 | } -------------------------------------------------------------------------------- /app/vite-vue3/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | import "./styles/index.scss"; 3 | import { createPinia } from "pinia"; 4 | import App from "./App.vue"; 5 | import router from "./router"; 6 | import { init } from "./utils/date-utils"; 7 | 8 | init(); 9 | 10 | const pinia = createPinia(); 11 | 12 | const app = createApp(App); 13 | app.use(pinia).use(router).mount("#app"); 14 | -------------------------------------------------------------------------------- /app/vite-vue3/src/router-type.d.ts: -------------------------------------------------------------------------------- 1 | import "vue-router"; 2 | 3 | declare module "vue" { 4 | interface ComponentCustomProperties { 5 | $route: import("vue-router").RouteLocationNormalizedLoaded; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /app/vite-vue3/src/router/not-found.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author: Tusi 3 | * @description: 404路由 4 | */ 5 | import { RouteRecordRaw } from "vue-router"; 6 | 7 | export const NOT_FOUND_ROUTE: RouteRecordRaw = { 8 | name: "NotFound", 9 | path: "/404", 10 | component: () => import("@/views/404/index.vue"), 11 | meta: { 12 | auto: false, 13 | title: "页面找不到了", 14 | }, 15 | }; 16 | 17 | export const FALLBACK_ROUTE = { 18 | name: "Fallback", 19 | path: "/:pathMatch(.*)*", 20 | redirect: "/404", 21 | }; 22 | -------------------------------------------------------------------------------- /app/vite-vue3/src/services/category.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author: Tusi 3 | * @description: 分类服务 4 | */ 5 | import { ApiService } from "@/services/index"; 6 | import { ArrayResponse, PageResponse, QueryCategoryModel, QueryPageModel, UpdateCategoryModel } from "@/bean/xhr"; 7 | import { CategoryDTO } from "@/bean/dto"; 8 | 9 | class CategoryService extends ApiService { 10 | public all(params?: QueryCategoryModel) { 11 | return this.$get>("all", params); 12 | } 13 | 14 | public adminPage(params?: QueryPageModel) { 15 | return this.$get>("admin/page", params); 16 | } 17 | 18 | public adminUpdate(params?: UpdateCategoryModel) { 19 | return this.$putJson>("admin/update", params); 20 | } 21 | 22 | public fuzzy(params: { wd: string }) { 23 | return this.$get>>("fuzzy", params); 24 | } 25 | } 26 | 27 | export const categoryService = new CategoryService("category"); 28 | -------------------------------------------------------------------------------- /app/vite-vue3/src/services/chatgpt.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author: Tusi 3 | * @description: ChatGPT服务 4 | */ 5 | import { PlainResponse } from "@/bean/xhr"; 6 | import { ApiService } from "@/services/index"; 7 | 8 | class ChatgptService extends ApiService { 9 | public feedback(result: string) { 10 | return this.$postJson("feedback", { result }); 11 | } 12 | 13 | public changeTopic() { 14 | return this.$post("changeTopic"); 15 | } 16 | 17 | public chatV1(wd: string) { 18 | return this.$get>("chat-v1", { wd }); 19 | } 20 | } 21 | 22 | export const chatgptService = new ChatgptService("chatgpt"); 23 | -------------------------------------------------------------------------------- /app/vite-vue3/src/services/reply.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author: Tusi 3 | * @description: 回复服务 4 | */ 5 | import { ApiService } from "@/services/index"; 6 | import { PlainObject } from "@/bean/base"; 7 | import { PageResponse, QueryPageModel } from "@/bean/xhr"; 8 | import { ReplyDTO } from "@/bean/dto"; 9 | 10 | class ReplyService extends ApiService { 11 | public add(params: PlainObject) { 12 | return this.$post("add", params); 13 | } 14 | 15 | public unreviewdReplyPage(params: QueryPageModel) { 16 | return this.$get>("unreviewd_reply_page", params); 17 | } 18 | 19 | public review(params: PlainObject) { 20 | return this.$put("review", params); 21 | } 22 | } 23 | 24 | export const replyService = new ReplyService("reply"); 25 | -------------------------------------------------------------------------------- /app/vite-vue3/src/services/tag.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author: Tusi 3 | * @description: 标签服务 4 | */ 5 | import { ApiService } from "@/services/index"; 6 | import { ArrayResponse, PageResponse, QueryPageModel, QueryTagModel } from "@/bean/xhr"; 7 | import { TagDTO } from "@/bean/dto"; 8 | 9 | class TagService extends ApiService { 10 | public all(params: QueryTagModel) { 11 | return this.$get>("all", params); 12 | } 13 | 14 | public adminPage(params?: QueryPageModel) { 15 | return this.$get>("admin/page", params); 16 | } 17 | 18 | public fuzzy(params: { wd: string }) { 19 | return this.$get>("fuzzy", params); 20 | } 21 | } 22 | 23 | export const tagService = new TagService("tag"); 24 | -------------------------------------------------------------------------------- /app/vite-vue3/src/services/user.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author: Tusi 3 | * @description: 用户服务 4 | */ 5 | import { ApiService } from "@/services/index"; 6 | import { LoginModel, RecordResponse } from "@/bean/xhr"; 7 | import { UserDTO } from "@/bean/dto"; 8 | 9 | class UserService extends ApiService { 10 | public login(params: LoginModel) { 11 | return this.$put>("login", params); 12 | } 13 | 14 | public current() { 15 | return this.$get>("current"); 16 | } 17 | 18 | public logout() { 19 | return this.$put("logout"); 20 | } 21 | } 22 | 23 | export const userService = new UserService("user"); 24 | -------------------------------------------------------------------------------- /app/vite-vue3/src/services/validator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author: Tusi 3 | * @description: 验证码服务 4 | */ 5 | import { ApiService } from "@/services/index"; 6 | import { PlainResponse } from "@/bean/xhr"; 7 | 8 | class ValidatorService extends ApiService { 9 | public imgCode() { 10 | return this.$get>("img_code"); 11 | } 12 | } 13 | 14 | export const validatorService = new ValidatorService("validator"); 15 | -------------------------------------------------------------------------------- /app/vite-vue3/src/stores/global-ui-state.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia"; 2 | import { ref } from "vue"; 3 | 4 | export const useGlobalUIState = defineStore("globalUIState", () => { 5 | const isMenuVisible = ref(false); 6 | 7 | const setIsMenuVisible = (value: boolean) => { 8 | isMenuVisible.value = value; 9 | }; 10 | 11 | return { 12 | isMenuVisible, 13 | setIsMenuVisible, 14 | }; 15 | }); 16 | -------------------------------------------------------------------------------- /app/vite-vue3/src/styles/antd.scss: -------------------------------------------------------------------------------- 1 | .ant-pagination { 2 | &.pagination-common { 3 | margin-top: 20px; 4 | text-align: center; 5 | } 6 | } 7 | 8 | .ant-drawer { 9 | &.drawer-comment { 10 | .ant-drawer-content-wrapper { 11 | max-width: 400px; 12 | } 13 | .ant-drawer-wrapper-body { 14 | display: flex; 15 | flex-direction: column; 16 | } 17 | .ant-drawer-body { 18 | flex: 1; 19 | padding: 0; 20 | display: flex; 21 | overflow: auto; 22 | } 23 | } 24 | } 25 | 26 | .ant-table { 27 | .ant-table-scroll { 28 | .ant-table-body { 29 | overflow-x: auto !important; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/vite-vue3/src/styles/atom.scss: -------------------------------------------------------------------------------- 1 | // 原子类 2 | .align-left { 3 | text-align: left; 4 | } 5 | 6 | .align-center { 7 | text-align: center; 8 | } 9 | 10 | .align-right { 11 | text-align: right; 12 | } 13 | 14 | .float-l { 15 | float: left; 16 | } 17 | 18 | .float-r { 19 | float: right; 20 | } 21 | 22 | .hidden { 23 | display: none !important; 24 | } 25 | 26 | .overflow-auto { 27 | overflow: auto; 28 | } 29 | 30 | .overflow-hidden { 31 | overflow: hidden; 32 | } 33 | 34 | .align-middle { 35 | vertical-align: middle !important; 36 | } 37 | 38 | .pointer { 39 | cursor: pointer; 40 | } 41 | 42 | .disabled { 43 | cursor: not-allowed; 44 | } 45 | 46 | .margin-0 { 47 | margin: 0 !important; 48 | } 49 | 50 | .padding-0 { 51 | padding: 0 !important; 52 | } 53 | 54 | .mt-0 { 55 | margin-top: 0 !important; 56 | } 57 | 58 | .mb-0 { 59 | margin-bottom: 0 !important; 60 | } 61 | 62 | .mt-20 { 63 | margin-top: 20px !important; 64 | } 65 | 66 | .ml-20 { 67 | margin-left: 20px !important; 68 | } 69 | 70 | .block { 71 | display: block !important; 72 | width: 100%; 73 | } 74 | -------------------------------------------------------------------------------- /app/vite-vue3/src/styles/common.scss: -------------------------------------------------------------------------------- 1 | .flex-layout { 2 | @include flex-layout; 3 | } 4 | 5 | .flex-center { 6 | @include flex-center; 7 | } 8 | 9 | .flex-layout--column { 10 | @include flex-layout--column; 11 | } 12 | 13 | .flex-align-center { 14 | position: relative; 15 | display: flex; 16 | align-items: center; 17 | } 18 | 19 | .flex-1 { 20 | flex: 1; 21 | } 22 | 23 | .abs-center { 24 | position: absolute; 25 | top: 50%; 26 | left: 50%; 27 | transform: translate(-50%, -50%); 28 | } 29 | 30 | .clearfix { 31 | @include bfc-clearfix; 32 | } 33 | 34 | .ellipsis { 35 | @include one-line-ellipsis; 36 | } 37 | 38 | .icon-svg.icon--aside { 39 | @include flex-center; 40 | 41 | color: #fff; 42 | font-size: 24px; 43 | height: 100%; 44 | border-radius: 50%; 45 | background-color: rgba(102, 57, 57, 0.4); 46 | cursor: pointer; 47 | + .icon--aside { 48 | margin-top: 10px; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/vite-vue3/src/styles/element-vars.scss: -------------------------------------------------------------------------------- 1 | /* 改变主题色变量,见 node_modules/element-plus/packages/theme-chalk/src/common/var.scss */ 2 | -------------------------------------------------------------------------------- /app/vite-vue3/src/styles/index.scss: -------------------------------------------------------------------------------- 1 | // global styles 2 | 3 | @import "./reset.scss"; 4 | @import "./animation.scss"; 5 | @import "./atom.scss"; 6 | @import "./common.scss"; 7 | @import "./antd.scss"; 8 | -------------------------------------------------------------------------------- /app/vite-vue3/src/styles/mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin absolute-center { 2 | position: absolute; 3 | top: 50%; 4 | left: 50%; 5 | transform: translate3d(-50%, -50%, 0); 6 | } 7 | 8 | @mixin absolute-x-center { 9 | position: absolute; 10 | left: 50%; 11 | transform: translateX(-50%); 12 | } 13 | 14 | @mixin flex-center { 15 | display: flex; 16 | justify-content: center; 17 | align-items: center; 18 | } 19 | 20 | @mixin flex-layout { 21 | position: relative; 22 | display: flex; 23 | flex: 1; 24 | overflow: auto; 25 | } 26 | 27 | @mixin flex-layout--column { 28 | position: relative; 29 | display: flex; 30 | flex: 1; 31 | flex-direction: column; 32 | overflow: hidden; 33 | } 34 | 35 | @mixin flex-only--column { 36 | position: relative; 37 | display: flex; 38 | flex-direction: column; 39 | } 40 | 41 | @mixin one-line-ellipsis { 42 | overflow: hidden; 43 | text-overflow: ellipsis; 44 | white-space: nowrap; 45 | } 46 | 47 | @mixin bfc-clearfix { 48 | &::after, 49 | &::before { 50 | display: table; 51 | content: ""; 52 | } 53 | &::after { 54 | clear: both; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/vite-vue3/src/styles/preload.scss: -------------------------------------------------------------------------------- 1 | @import "./vars.scss"; 2 | @import "./mixins.scss"; 3 | // 在按需加载情况下,会按需加载特定 scss,所以定制主题只要让 element-vars.scss 预先加载就行。 4 | // @import "./element-vars.scss"; 5 | -------------------------------------------------------------------------------- /app/vite-vue3/src/styles/reset.scss: -------------------------------------------------------------------------------- 1 | *:fullscreen { 2 | // 必须加背景色,不然进全屏会被:not(:root):-webkit-full-screen::backdrop影响 3 | background-color: $color-white; 4 | } 5 | 6 | *, 7 | *::before, 8 | *::after { 9 | box-sizing: border-box; 10 | } 11 | 12 | html, 13 | body { 14 | height: 100%; 15 | } 16 | 17 | body { 18 | position: relative; 19 | margin: 0; 20 | padding: 0; 21 | font-size: 16px; 22 | line-height: 1.5715; 23 | -moz-osx-font-smoothing: grayscale; 24 | -webkit-font-smoothing: antialiased; 25 | text-rendering: optimizeLegibility; 26 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Helvetica Neue, Helvetica, 27 | Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol; 28 | } 29 | 30 | #app { 31 | height: 100%; 32 | } 33 | 34 | a { 35 | color: #87b4e2; 36 | } 37 | 38 | a, 39 | a:focus, 40 | a:hover { 41 | outline: none; 42 | text-decoration: none; 43 | } 44 | 45 | a:hover { 46 | opacity: 0.95; 47 | } 48 | 49 | ul, 50 | li { 51 | list-style: none; 52 | padding: 0; 53 | margin: 0; 54 | } 55 | 56 | h1, 57 | h2, 58 | h3, 59 | h4, 60 | h5, 61 | h6 { 62 | margin: 0 0 0.5em 0; 63 | font-weight: 500; 64 | } 65 | 66 | p { 67 | margin-top: 0; 68 | margin-bottom: 1em; 69 | } 70 | -------------------------------------------------------------------------------- /app/vite-vue3/src/utils/bom.ts: -------------------------------------------------------------------------------- 1 | export const sleep = (seconds = 0) => { 2 | return new Promise((resolve) => { 3 | window.setTimeout(() => { 4 | resolve(true); 5 | }, seconds * 1000); 6 | }); 7 | }; 8 | 9 | export const getLocalData = (option: { key: string; parse?: boolean; defaultValue?: T }) => { 10 | const { key, parse = false, defaultValue } = option; 11 | const lData = localStorage.getItem(key); 12 | if (lData === null) { 13 | return defaultValue === undefined ? null : defaultValue; 14 | } 15 | if (parse) { 16 | return JSON.parse(lData) as T; 17 | } 18 | return lData as T; 19 | }; 20 | -------------------------------------------------------------------------------- /app/vite-vue3/src/utils/eventbus.ts: -------------------------------------------------------------------------------- 1 | import mitt from "mitt"; 2 | 3 | type Events = { 4 | // key 是事件名,类型是事件传值的类型 5 | sessionInvalid: void; 6 | clearUserSession: void; 7 | }; 8 | 9 | export const eventBus = mitt(); 10 | -------------------------------------------------------------------------------- /app/vite-vue3/src/utils/formatter.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author: Tusi 3 | * @description: 格式化,代替filter功能 4 | */ 5 | 6 | export function approvedFormatter(val: 0 | 1 | 2): string { 7 | switch (val) { 8 | case 1: 9 | return "通过"; 10 | case 2: 11 | return "不通过"; 12 | case 0: 13 | default: 14 | return "待审核"; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/vite-vue3/src/utils/helper.ts: -------------------------------------------------------------------------------- 1 | import { PlainObject } from "@/bean/base"; 2 | import { getType, isArray, isDefined } from "./type"; 3 | 4 | /** 5 | * 处理参数对象 6 | * @param {Object} obj 参数对象 7 | * @param {options} isArrayToString 是否需要将数组处理成逗号分隔的string 8 | * @returns {Object} 处理后的参数对象 9 | */ 10 | export function requestParamsFilter(obj: PlainObject, isArrayToString = false): PlainObject { 11 | if (isArray(obj)) { 12 | return obj; 13 | } 14 | if (getType(obj) !== "object") { 15 | return {}; 16 | } 17 | const newObj: PlainObject = {}; 18 | Object.keys(obj).forEach((key) => { 19 | const element = obj[key]; 20 | if (Array.isArray(element)) { 21 | if (element.length > 0) { 22 | newObj[key] = isArrayToString ? element.join(",") : [...element]; 23 | } 24 | } else if (isDefined(element)) { 25 | newObj[key] = element; 26 | } 27 | }); 28 | return newObj; 29 | } 30 | -------------------------------------------------------------------------------- /app/vite-vue3/src/utils/tree.ts: -------------------------------------------------------------------------------- 1 | import { TreeNode } from "@/bean/base"; 2 | 3 | // overload 4 | export function tree2Arr(tree: Array, replaceChildren?: string): Array; 5 | export function tree2Arr( 6 | tree: Array, 7 | replaceChildren?: string, 8 | mapper?: (item: T, index: number, arr: Array) => D 9 | ): Array; 10 | export function tree2Arr( 11 | tree: Array, 12 | replaceChildren = "children", 13 | mapper?: (item: T, index: number, arr: Array) => D 14 | ): Array | Array { 15 | const result = tree.reduce((prev, curr) => { 16 | const children = curr[replaceChildren] as T[]; 17 | const list = children && children.length > 0 ? [curr, ...tree2Arr(children, replaceChildren, mapper)] : [curr]; 18 | return prev.concat(list as ConcatArray); 19 | }, [] as Array); 20 | return typeof mapper === "function" ? result.map(mapper) : result; 21 | } 22 | -------------------------------------------------------------------------------- /app/vite-vue3/src/utils/validator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author: Tusi 3 | * @description: 通用校验 4 | */ 5 | 6 | export const REQUIRED_VALIDATOR_BLUR = { 7 | required: true, 8 | message: "必填项", 9 | trigger: "blur", 10 | }; 11 | 12 | export const EMAIL_VALIDATOR = { 13 | pattern: 14 | /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/, 15 | message: "邮箱格式不正确", 16 | trigger: "blur", 17 | }; 18 | 19 | export const URL_VALIDATOR = { 20 | pattern: /(http|https):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-.,@?^=%&:/~+#]*[\w\-@?^=%&/~+#])?/, 21 | message: "链接格式不正确,注意以http或https开头", 22 | trigger: "blur", 23 | }; 24 | -------------------------------------------------------------------------------- /app/vite-vue3/src/views/404/index.vue: -------------------------------------------------------------------------------- 1 | 5 | 15 | 16 | 28 | -------------------------------------------------------------------------------- /app/vite-vue3/src/views/backend/article/index.module.scss: -------------------------------------------------------------------------------- 1 | .articlePoster { 2 | width: 100px; 3 | height: 72px; 4 | > img { 5 | width: 100%; 6 | height: 100%; 7 | object-fit: contain; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /app/vite-vue3/src/views/backend/styles/avatar.scss: -------------------------------------------------------------------------------- 1 | :deep(.comment-avatar) { 2 | width: 40px; 3 | height: 40px; 4 | > img { 5 | width: 100%; 6 | height: 100%; 7 | border-radius: 100%; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /app/vite-vue3/src/views/jumpout/index.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 29 | 30 | 48 | -------------------------------------------------------------------------------- /app/vite-vue3/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module "*.vue" { 4 | import type { DefineComponent } from "vue"; 5 | 6 | const component: DefineComponent<{}, {}, any>; 7 | export default component; 8 | } 9 | 10 | interface ImportMetaEnv { 11 | readonly VITE_APP_TITLE: string; 12 | readonly VITE_APP_BASE_API: string; 13 | readonly VITE_APP_SOCKET_SERVER: string; 14 | } 15 | 16 | interface ImportMeta { 17 | readonly env: ImportMetaEnv; 18 | } 19 | -------------------------------------------------------------------------------- /app/vite-vue3/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 5 | "target": "ES2020", 6 | "useDefineForClassFields": true, 7 | "module": "ESNext", 8 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 9 | "skipLibCheck": true, 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "moduleDetection": "force", 16 | "noEmit": true, 17 | "jsx": "preserve", 18 | "baseUrl": ".", 19 | "paths": { 20 | "@/*": [ 21 | "src/*" 22 | ] 23 | }, 24 | "allowJs": true, 25 | /* Linting */ 26 | "strict": true, 27 | "noUnusedLocals": true, 28 | "noUnusedParameters": true, 29 | "noFallthroughCasesInSwitch": true 30 | }, 31 | "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"] 32 | } 33 | -------------------------------------------------------------------------------- /app/vite-vue3/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "./tsconfig.app.json" 6 | }, 7 | { 8 | "path": "./tsconfig.node.json" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /app/vite-vue3/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 5 | "skipLibCheck": true, 6 | "module": "ESNext", 7 | "moduleResolution": "bundler", 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "noEmit": true 11 | }, 12 | "include": ["vite.config.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /app/vite-vue3/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import vue from "@vitejs/plugin-vue"; 3 | import vueJsx from "@vitejs/plugin-vue-jsx"; 4 | import Components from "unplugin-vue-components/vite"; 5 | import { AntDesignVueResolver, ElementPlusResolver } from "unplugin-vue-components/resolvers"; 6 | import path from "node:path"; 7 | 8 | // https://vitejs.dev/config/ 9 | export default defineConfig({ 10 | server: { 11 | host: "0.0.0.0", 12 | port: 3000, 13 | proxy: { 14 | "/api": { 15 | target: "http://127.0.0.1:8012", 16 | changeOrigin: true, 17 | rewrite: (path) => path.replace(/^\/api/, ""), 18 | }, 19 | }, 20 | }, 21 | plugins: [ 22 | vue(), 23 | vueJsx(), 24 | Components({ 25 | resolvers: [AntDesignVueResolver({ importStyle: false, resolveIcons: true }), ElementPlusResolver({ importStyle: true })], 26 | }), 27 | ], 28 | resolve: { 29 | alias: { 30 | "@": path.resolve(__dirname, "./src"), 31 | }, 32 | }, 33 | css: { 34 | preprocessorOptions: { 35 | less: { 36 | javascriptEnabled: true, 37 | // additionalData: '@import "@/styles/preload.less";', 38 | }, 39 | scss: { 40 | javascriptEnabled: true, 41 | additionalData: '@import "@/styles/preload.scss";', 42 | }, 43 | }, 44 | }, 45 | }); 46 | -------------------------------------------------------------------------------- /app/webpack-vue3/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /app/webpack-vue3/.env: -------------------------------------------------------------------------------- 1 | # 环境变量 2 | VUE_APP_TITLE=Tusi博客 3 | VUE_APP_BASE_API=/api 4 | -------------------------------------------------------------------------------- /app/webpack-vue3/.env.development: -------------------------------------------------------------------------------- 1 | # 开发环境变量 2 | VUE_APP_SOCKET_SERVER=http://127.0.0.1:8012 3 | -------------------------------------------------------------------------------- /app/webpack-vue3/.env.production: -------------------------------------------------------------------------------- 1 | # 生产环境变量 2 | VUE_APP_SOCKET_SERVER="" 3 | -------------------------------------------------------------------------------- /app/webpack-vue3/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | src/**/*.d.ts 3 | dist 4 | vue.config.js 5 | vue.config.docker.js 6 | .eslintrc.cjs -------------------------------------------------------------------------------- /app/webpack-vue3/.gitignore: -------------------------------------------------------------------------------- 1 | components.d.ts 2 | .stylelintcache 3 | -------------------------------------------------------------------------------- /app/webpack-vue3/.stylelintrc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author: Tusi 3 | * @description: stylelint配置 4 | */ 5 | module.exports = { 6 | extends: ["stylelint-config-standard", "stylelint-prettier/recommended", "stylelint-config-recommended-vue"], 7 | plugins: ["stylelint-scss", "stylelint-less", "stylelint-prettier"], 8 | rules: { 9 | 'prettier/prettier': true, 10 | "at-rule-no-unknown": null, 11 | "color-no-invalid-hex": true, 12 | "less/color-no-invalid-hex": true, 13 | "no-descending-specificity": null, 14 | "selector-pseudo-element-no-unknown": [ 15 | true, 16 | { 17 | ignorePseudoElements: ["deep"], 18 | }, 19 | ], 20 | "selector-pseudo-class-no-unknown": [ 21 | true, 22 | { 23 | ignorePseudoClasses: ["deep", "global"], 24 | }, 25 | ], 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /app/webpack-vue3/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # webpack-vue3 2 | 3 | ## 3.7.8 4 | 5 | ### Patch Changes 6 | 7 | - 5d42650: fix: socket.io v4 连接异常问题 8 | 9 | ## 3.7.7 10 | 11 | ### Patch Changes 12 | 13 | - 373b310: fix: nest 采用 socket.io v4 版本,生产环境下前端连接 socket.io 报错 14 | 15 | ## 3.7.6 16 | 17 | ### Patch Changes 18 | 19 | - 0e82de4: chore: nginx backend 引用错误 20 | 21 | ## 3.7.5 22 | 23 | ### Patch Changes 24 | 25 | - ff699c8: fix: 分类面包屑展示异常,Modal 无法关闭,其他样式问题修改 26 | 27 | ## 3.7.4 28 | 29 | ### Patch Changes 30 | 31 | - 4a2edf3: chore: backend 项目改为 express-server 32 | 33 | ## 3.7.3 34 | 35 | ### Patch Changes 36 | 37 | - 3bad363: ci: 重新部署 38 | 39 | ## 3.7.2 40 | 41 | ### Patch Changes 42 | 43 | - 7303cbe: ci: 支持 webpack-vue3 部署 44 | 45 | ## 3.7.1 46 | 47 | ### Patch Changes 48 | 49 | - 0f24738: fix: 部分校验 bug 修复 50 | 51 | ## 3.7.0 52 | 53 | ### Minor Changes 54 | 55 | - faf9406: feat: 支持 mermaid 图表 56 | 57 | ## 3.6.0 58 | 59 | ### Minor Changes 60 | 61 | - 77976ee: 文章内图片站内预览查看 62 | 63 | ## 3.5.0 64 | 65 | ### Minor Changes 66 | 67 | - 06b9971: feat: 文章中打开外链时跳到中间页 68 | 69 | ## 3.4.0 70 | 71 | ### Minor Changes 72 | 73 | - 36a8c5d: 创作时支持 tag 自动补全 74 | 75 | ## 3.3.0 76 | 77 | ### Minor Changes 78 | 79 | - 48ebfdb: 创作时支持分类模糊搜索 80 | 81 | ## 3.2.0 82 | 83 | ### Minor Changes 84 | 85 | - 48df5f3: 测试 minor 86 | - 8ba9fd9: test 87 | 88 | ## 3.1.0 89 | 90 | ### Minor Changes 91 | 92 | - 6de0cae: 测试流程 93 | 94 | ## 3.0.1 95 | 96 | ### Patch Changes 97 | 98 | - changeset init 99 | -------------------------------------------------------------------------------- /app/webpack-vue3/antd-theme.js: -------------------------------------------------------------------------------- 1 | // https://github.com/vueComponent/ant-design-vue/blob/master/components/style/themes/default.less 2 | module.exports = { 3 | "primary-color": "#008dff", 4 | "link-color": "#87b4e2", 5 | }; 6 | -------------------------------------------------------------------------------- /app/webpack-vue3/babel.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author: Tusi 3 | * @description: babel配置 4 | */ 5 | module.exports = { 6 | presets: ["@vue/cli-plugin-babel/preset"], 7 | plugins: [ 8 | "@babel/plugin-proposal-optional-chaining", 9 | "@babel/plugin-proposal-nullish-coalescing-operator", 10 | [ 11 | "import", 12 | { 13 | libraryName: "ant-design-vue", 14 | libraryDirectory: "es", 15 | // 配合按需加载以及主题定制 16 | style: true, 17 | }, 18 | ], 19 | [ 20 | "import", 21 | { 22 | libraryName: "element-plus", 23 | customStyleName: (name) => { 24 | name = name.slice(3); 25 | return `element-plus/packages/theme-chalk/src/${name}.scss`; 26 | }, 27 | }, 28 | "element", 29 | ], 30 | ], 31 | }; 32 | -------------------------------------------------------------------------------- /app/webpack-vue3/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "@vue/cli-plugin-unit-jest/presets/typescript-and-babel", 3 | transform: { 4 | "^.+\\.vue$": "vue-jest", 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /app/webpack-vue3/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cumt-robin/fullstack-blog/02a1377d007f0b6906cac66a1d6829bc998b72ff/app/webpack-vue3/public/favicon.ico -------------------------------------------------------------------------------- /app/webpack-vue3/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/webpack-vue3/public/js/hm.js: -------------------------------------------------------------------------------- 1 | var _hmt = _hmt || []; 2 | (function () { 3 | var hm = document.createElement("script"); 4 | hm.src = "https://hm.baidu.com/hm.js?d2feba2eac8bedae244304195f7b064f"; 5 | var s = document.getElementsByTagName("script")[0]; 6 | s.parentNode.insertBefore(hm, s); 7 | })(); 8 | -------------------------------------------------------------------------------- /app/webpack-vue3/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / 3 | -------------------------------------------------------------------------------- /app/webpack-vue3/src/App.vue: -------------------------------------------------------------------------------- 1 | 5 | 12 | 13 | 48 | 49 | 58 | -------------------------------------------------------------------------------- /app/webpack-vue3/src/assets/img/avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cumt-robin/fullstack-blog/02a1377d007f0b6906cac66a1d6829bc998b72ff/app/webpack-vue3/src/assets/img/avatar.jpg -------------------------------------------------------------------------------- /app/webpack-vue3/src/assets/img/chat-avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cumt-robin/fullstack-blog/02a1377d007f0b6906cac66a1d6829bc998b72ff/app/webpack-vue3/src/assets/img/chat-avatar.png -------------------------------------------------------------------------------- /app/webpack-vue3/src/assets/img/comment-avatar.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/webpack-vue3/src/assets/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cumt-robin/fullstack-blog/02a1377d007f0b6906cac66a1d6829bc998b72ff/app/webpack-vue3/src/assets/img/logo.png -------------------------------------------------------------------------------- /app/webpack-vue3/src/assets/img/logo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cumt-robin/fullstack-blog/02a1377d007f0b6906cac66a1d6829bc998b72ff/app/webpack-vue3/src/assets/img/logo2.png -------------------------------------------------------------------------------- /app/webpack-vue3/src/assets/img/reply-avatar.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/webpack-vue3/src/assets/img/wechat_payme.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cumt-robin/fullstack-blog/02a1377d007f0b6906cac66a1d6829bc998b72ff/app/webpack-vue3/src/assets/img/wechat_payme.jpg -------------------------------------------------------------------------------- /app/webpack-vue3/src/bean/base.ts: -------------------------------------------------------------------------------- 1 | import { defineComponent } from "vue"; 2 | 3 | type IndexType = string | number | symbol; 4 | 5 | export type PlainObject = Record; 6 | 7 | export type PrimitiveType = number | string | boolean | undefined | null | symbol; 8 | 9 | export type GeneralFunction = (...args: any[]) => T; 10 | 11 | export interface PlainNode extends PlainObject { 12 | id: number; 13 | } 14 | 15 | export interface TreeNode extends PlainObject { 16 | key: string; 17 | children?: this[]; 18 | } 19 | 20 | export type Lazy = () => Promise; 21 | 22 | export type DefineComponentOptions = Parameters[0]; 23 | -------------------------------------------------------------------------------- /app/webpack-vue3/src/components/my-button/index.vue: -------------------------------------------------------------------------------- 1 | 5 | 13 | 14 | 33 | -------------------------------------------------------------------------------- /app/webpack-vue3/src/directives/index.ts: -------------------------------------------------------------------------------- 1 | import { App } from "vue"; 2 | import Lazyload from "./lazyload"; 3 | 4 | export default { 5 | install(app: App): void { 6 | app.directive("lazyload", Lazyload); 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /app/webpack-vue3/src/hooks/async.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author: Tusi 3 | * @description: 异步 Hook,用于 loading, error 等状态反馈 4 | */ 5 | import { Ref, ref } from "vue"; 6 | import { GeneralFunction } from "@/bean/base"; 7 | 8 | interface AsyncLoadingResponse { 9 | trigger: GeneralFunction; 10 | loading: Ref; 11 | isError: Ref; 12 | error: Ref; 13 | } 14 | 15 | export const useAsyncLoading = (fn: GeneralFunction>): AsyncLoadingResponse => { 16 | const loading = ref(false); 17 | const isError = ref(false); 18 | const error = ref(); 19 | const trigger = async (...args: any[]) => { 20 | try { 21 | loading.value = true; 22 | await fn(...args); 23 | } catch (err) { 24 | isError.value = true; 25 | error.value = err; 26 | } finally { 27 | loading.value = false; 28 | } 29 | }; 30 | return { trigger, loading, isError, error }; 31 | }; 32 | -------------------------------------------------------------------------------- /app/webpack-vue3/src/image.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.svg"; 2 | declare module "*.png"; 3 | declare module "*.jpg"; 4 | declare module "*.jpeg"; 5 | -------------------------------------------------------------------------------- /app/webpack-vue3/src/main.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author: Tusi 3 | * @description: 入口文件 4 | */ 5 | 6 | import "ant-design-vue/es/message/style"; 7 | import "./styles/index.scss"; 8 | import { createApp } from "vue"; 9 | import App from "./App.vue"; 10 | import router from "./router"; 11 | import store, { key } from "./store"; 12 | import { init } from "./utils/date-utils"; 13 | 14 | init(); 15 | 16 | export const app = createApp(App); 17 | 18 | app.use(store, key); 19 | app.use(router); 20 | app.mount("#app"); 21 | -------------------------------------------------------------------------------- /app/webpack-vue3/src/router/not-found.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author: Tusi 3 | * @description: 404路由 4 | */ 5 | import { RouteRecordRaw } from "vue-router"; 6 | 7 | export const NOT_FOUND_ROUTE: RouteRecordRaw = { 8 | name: "NotFound", 9 | path: "/404", 10 | component: () => import(/* webpackChunkName: "not-found" */ "@/views/404/index.vue"), 11 | meta: { 12 | auto: false, 13 | title: "页面找不到了", 14 | }, 15 | }; 16 | 17 | export const FALLBACK_ROUTE = { 18 | name: "Fallback", 19 | path: "/:pathMatch(.*)*", 20 | redirect: "/404", 21 | }; 22 | -------------------------------------------------------------------------------- /app/webpack-vue3/src/services/category.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author: Tusi 3 | * @description: 分类服务 4 | */ 5 | import { ApiService } from "@/services/index"; 6 | import { ArrayResponse, PageResponse, QueryCategoryModel, QueryPageModel, UpdateCategoryModel } from "@/bean/xhr"; 7 | import { CategoryDTO } from "@/bean/dto"; 8 | 9 | class CategoryService extends ApiService { 10 | public all(params?: QueryCategoryModel) { 11 | return this.$get>("all", params); 12 | } 13 | 14 | public adminPage(params?: QueryPageModel) { 15 | return this.$get>("admin/page", params); 16 | } 17 | 18 | public adminUpdate(params?: UpdateCategoryModel) { 19 | return this.$putJson>("admin/update", params); 20 | } 21 | 22 | public fuzzy(params: { wd: string }) { 23 | return this.$get>>("fuzzy", params); 24 | } 25 | } 26 | 27 | export const categoryService = new CategoryService("category"); 28 | -------------------------------------------------------------------------------- /app/webpack-vue3/src/services/chatgpt.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author: Tusi 3 | * @description: ChatGPT服务 4 | */ 5 | import { PlainResponse } from "@/bean/xhr"; 6 | import { ApiService } from "@/services/index"; 7 | 8 | class ChatgptService extends ApiService { 9 | public feedback(result: string) { 10 | return this.$postJson("feedback", { result }); 11 | } 12 | 13 | public changeTopic() { 14 | return this.$post("changeTopic"); 15 | } 16 | 17 | public chatV1(wd: string) { 18 | return this.$get>("chat-v1", { wd }); 19 | } 20 | } 21 | 22 | export const chatgptService = new ChatgptService("chatgpt"); 23 | -------------------------------------------------------------------------------- /app/webpack-vue3/src/services/reply.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author: Tusi 3 | * @description: 回复服务 4 | */ 5 | import { ApiService } from "@/services/index"; 6 | import { PlainObject } from "@/bean/base"; 7 | import { PageResponse, QueryPageModel } from "@/bean/xhr"; 8 | import { ReplyDTO } from "@/bean/dto"; 9 | 10 | class ReplyService extends ApiService { 11 | public add(params: PlainObject) { 12 | return this.$post("add", params); 13 | } 14 | 15 | public unreviewdReplyPage(params: QueryPageModel) { 16 | return this.$get>("unreviewd_reply_page", params); 17 | } 18 | 19 | public review(params: PlainObject) { 20 | return this.$put("review", params); 21 | } 22 | } 23 | 24 | export const replyService = new ReplyService("reply"); 25 | -------------------------------------------------------------------------------- /app/webpack-vue3/src/services/tag.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author: Tusi 3 | * @description: 标签服务 4 | */ 5 | import { ApiService } from "@/services/index"; 6 | import { ArrayResponse, PageResponse, QueryPageModel, QueryTagModel } from "@/bean/xhr"; 7 | import { TagDTO } from "@/bean/dto"; 8 | 9 | class TagService extends ApiService { 10 | public all(params: QueryTagModel) { 11 | return this.$get>("all", params); 12 | } 13 | 14 | public adminPage(params?: QueryPageModel) { 15 | return this.$get>("admin/page", params); 16 | } 17 | 18 | public fuzzy(params: { wd: string }) { 19 | return this.$get>("fuzzy", params); 20 | } 21 | } 22 | 23 | export const tagService = new TagService("tag"); 24 | -------------------------------------------------------------------------------- /app/webpack-vue3/src/services/user.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author: Tusi 3 | * @description: 用户服务 4 | */ 5 | import { ApiService } from "@/services/index"; 6 | import { LoginModel, RecordResponse } from "@/bean/xhr"; 7 | import { UserDTO } from "@/bean/dto"; 8 | 9 | class UserService extends ApiService { 10 | public login(params: LoginModel) { 11 | return this.$put>("login", params); 12 | } 13 | 14 | public current() { 15 | return this.$get>("current"); 16 | } 17 | 18 | public logout() { 19 | return this.$put("logout"); 20 | } 21 | } 22 | 23 | export const userService = new UserService("user"); 24 | -------------------------------------------------------------------------------- /app/webpack-vue3/src/services/validator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author: Tusi 3 | * @description: 验证码服务 4 | */ 5 | import { ApiService } from "@/services/index"; 6 | import { PlainResponse } from "@/bean/xhr"; 7 | 8 | class ValidatorService extends ApiService { 9 | public imgCode() { 10 | return this.$get>("img_code"); 11 | } 12 | } 13 | 14 | export const validatorService = new ValidatorService("validator"); 15 | -------------------------------------------------------------------------------- /app/webpack-vue3/src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | declare module '*.vue' { 3 | import type { DefineComponent } from 'vue' 4 | const component: DefineComponent<{}, {}, any> 5 | export default component 6 | } 7 | 8 | declare module "*.scss"; -------------------------------------------------------------------------------- /app/webpack-vue3/src/store/constants.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author: Tusi 3 | * @description: vuex 常量 4 | */ 5 | // Root Mutations 6 | export const SET_IS_MENU_VISIBLE = "setIsMenuVisible"; 7 | 8 | export const SET_COMMENT_USER_INFO = "setCommentUserInfo"; 9 | 10 | export const SET_USER_INFO = "setUserInfo"; 11 | 12 | export const SET_USER_TOKEN = "setUserToken"; 13 | 14 | // Root Actions 15 | export const LOGIN_ACTION = "loginAction"; 16 | 17 | export const LOGOUT_ACTION = "logoutAction"; 18 | 19 | export const CLEAR_USER_SESSION = "clearUserSession"; 20 | -------------------------------------------------------------------------------- /app/webpack-vue3/src/styles/antd.scss: -------------------------------------------------------------------------------- 1 | .ant-pagination { 2 | &.pagination-common { 3 | margin-top: 20px; 4 | text-align: center; 5 | } 6 | } 7 | 8 | .ant-drawer { 9 | &.drawer-comment { 10 | .ant-drawer-content-wrapper { 11 | max-width: 400px; 12 | } 13 | .ant-drawer-wrapper-body { 14 | display: flex; 15 | flex-direction: column; 16 | } 17 | .ant-drawer-body { 18 | flex: 1; 19 | padding: 0; 20 | display: flex; 21 | overflow: auto; 22 | } 23 | } 24 | } 25 | 26 | .ant-table { 27 | .ant-table-scroll { 28 | .ant-table-body { 29 | overflow-x: auto !important; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/webpack-vue3/src/styles/atom.scss: -------------------------------------------------------------------------------- 1 | // 原子类 2 | .align-left { 3 | text-align: left; 4 | } 5 | 6 | .align-center { 7 | text-align: center; 8 | } 9 | 10 | .align-right { 11 | text-align: right; 12 | } 13 | 14 | .float-l { 15 | float: left; 16 | } 17 | 18 | .float-r { 19 | float: right; 20 | } 21 | 22 | .hidden { 23 | display: none !important; 24 | } 25 | 26 | .overflow-auto { 27 | overflow: auto; 28 | } 29 | 30 | .overflow-hidden { 31 | overflow: hidden; 32 | } 33 | 34 | .align-middle { 35 | vertical-align: middle !important; 36 | } 37 | 38 | .pointer { 39 | cursor: pointer; 40 | } 41 | 42 | .disabled { 43 | cursor: not-allowed; 44 | } 45 | 46 | .margin-0 { 47 | margin: 0 !important; 48 | } 49 | 50 | .padding-0 { 51 | padding: 0 !important; 52 | } 53 | 54 | .mt-0 { 55 | margin-top: 0 !important; 56 | } 57 | 58 | .mb-0 { 59 | margin-bottom: 0 !important; 60 | } 61 | 62 | .mt-20 { 63 | margin-top: 20px !important; 64 | } 65 | 66 | .ml-20 { 67 | margin-left: 20px !important; 68 | } 69 | 70 | .block { 71 | display: block !important; 72 | } 73 | -------------------------------------------------------------------------------- /app/webpack-vue3/src/styles/common.scss: -------------------------------------------------------------------------------- 1 | .flex-layout { 2 | @include flex-layout; 3 | } 4 | 5 | .flex-center { 6 | @include flex-center; 7 | } 8 | 9 | .flex-layout--column { 10 | @include flex-layout--column; 11 | } 12 | 13 | .flex-align-center { 14 | position: relative; 15 | display: flex; 16 | align-items: center; 17 | } 18 | 19 | .flex-1 { 20 | flex: 1; 21 | } 22 | 23 | .abs-center { 24 | position: absolute; 25 | top: 50%; 26 | left: 50%; 27 | transform: translate(-50%, -50%); 28 | } 29 | 30 | .clearfix { 31 | @include bfc-clearfix; 32 | } 33 | 34 | .ellipsis { 35 | @include one-line-ellipsis; 36 | } 37 | 38 | .icon-svg.icon--aside { 39 | @include flex-center; 40 | 41 | color: #fff; 42 | font-size: 24px; 43 | height: 100%; 44 | border-radius: 50%; 45 | background-color: rgba(102, 57, 57, 0.4); 46 | cursor: pointer; 47 | + .icon--aside { 48 | margin-top: 10px; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/webpack-vue3/src/styles/element-vars.scss: -------------------------------------------------------------------------------- 1 | /* 改变主题色变量,见 node_modules/element-plus/packages/theme-chalk/src/common/var.scss */ 2 | -------------------------------------------------------------------------------- /app/webpack-vue3/src/styles/index.scss: -------------------------------------------------------------------------------- 1 | // global styles 2 | 3 | @import "./reset.scss"; 4 | @import "./animation.scss"; 5 | @import "./atom.scss"; 6 | @import "./common.scss"; 7 | @import "./antd.scss"; 8 | -------------------------------------------------------------------------------- /app/webpack-vue3/src/styles/mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin absolute-center { 2 | position: absolute; 3 | top: 50%; 4 | left: 50%; 5 | transform: translate3d(-50%, -50%, 0); 6 | } 7 | 8 | @mixin absolute-x-center { 9 | position: absolute; 10 | left: 50%; 11 | transform: translateX(-50%); 12 | } 13 | 14 | @mixin flex-center { 15 | display: flex; 16 | justify-content: center; 17 | align-items: center; 18 | } 19 | 20 | @mixin flex-layout { 21 | position: relative; 22 | display: flex; 23 | flex: 1; 24 | overflow: auto; 25 | } 26 | 27 | @mixin flex-layout--column { 28 | position: relative; 29 | display: flex; 30 | flex: 1; 31 | flex-direction: column; 32 | overflow: hidden; 33 | } 34 | 35 | @mixin flex-only--column { 36 | position: relative; 37 | display: flex; 38 | flex-direction: column; 39 | } 40 | 41 | @mixin one-line-ellipsis { 42 | overflow: hidden; 43 | text-overflow: ellipsis; 44 | white-space: nowrap; 45 | } 46 | 47 | @mixin bfc-clearfix { 48 | &::after, 49 | &::before { 50 | display: table; 51 | content: ""; 52 | } 53 | &::after { 54 | clear: both; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/webpack-vue3/src/styles/preload.scss: -------------------------------------------------------------------------------- 1 | @import "./vars.scss"; 2 | @import "./mixins.scss"; 3 | // 在按需加载情况下,会按需加载特定 scss,所以定制主题只要让 element-vars.scss 预先加载就行。 4 | // @import "./element-vars.scss"; 5 | -------------------------------------------------------------------------------- /app/webpack-vue3/src/styles/reset.scss: -------------------------------------------------------------------------------- 1 | *:fullscreen { 2 | // 必须加背景色,不然进全屏会被:not(:root):-webkit-full-screen::backdrop影响 3 | background-color: $color-white; 4 | } 5 | 6 | body { 7 | position: relative; 8 | margin: 0; 9 | padding: 0; 10 | box-sizing: border-box; 11 | font-size: 16px; 12 | -moz-osx-font-smoothing: grayscale; 13 | -webkit-font-smoothing: antialiased; 14 | text-rendering: optimizeLegibility; 15 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Helvetica Neue, Helvetica, 16 | Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol; 17 | } 18 | 19 | #app { 20 | height: 100%; 21 | } 22 | 23 | a, 24 | a:focus, 25 | a:hover { 26 | outline: none; 27 | text-decoration: none; 28 | } 29 | 30 | ul, 31 | li { 32 | list-style: none; 33 | padding: 0; 34 | margin: 0; 35 | } 36 | -------------------------------------------------------------------------------- /app/webpack-vue3/src/types/jshashes.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author: Tusi 3 | * @description: jshashes类型定义 4 | */ 5 | declare module "jshashes" { 6 | export class SHA256 { 7 | public hex: (string) => string; 8 | } 9 | } -------------------------------------------------------------------------------- /app/webpack-vue3/src/utils/bom.ts: -------------------------------------------------------------------------------- 1 | export const sleep = (seconds = 0) => { 2 | return new Promise((resolve) => { 3 | window.setTimeout(() => { 4 | resolve(true); 5 | }, seconds * 1000); 6 | }); 7 | }; 8 | -------------------------------------------------------------------------------- /app/webpack-vue3/src/utils/eventbus.ts: -------------------------------------------------------------------------------- 1 | import mitt from "mitt"; 2 | 3 | type Events = { 4 | // key 是事件名,类型是事件传值的类型 5 | sessionInvalid: void; 6 | }; 7 | 8 | export const eventBus = mitt(); 9 | -------------------------------------------------------------------------------- /app/webpack-vue3/src/utils/formatter.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author: Tusi 3 | * @description: 格式化,代替filter功能 4 | */ 5 | 6 | export function approvedFormatter(val: 0 | 1 | 2): string { 7 | switch (val) { 8 | case 1: 9 | return "通过"; 10 | case 2: 11 | return "不通过"; 12 | case 0: 13 | default: 14 | return "待审核"; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/webpack-vue3/src/utils/helper.ts: -------------------------------------------------------------------------------- 1 | import { PlainObject } from "@/bean/base"; 2 | import { getType, isArray, isDefined } from "./type"; 3 | 4 | /** 5 | * 处理参数对象 6 | * @param {Object} obj 参数对象 7 | * @param {options} isArrayToString 是否需要将数组处理成逗号分隔的string 8 | * @returns {Object} 处理后的参数对象 9 | */ 10 | export function requestParamsFilter(obj: PlainObject, isArrayToString = false): PlainObject { 11 | if (isArray(obj)) { 12 | return obj; 13 | } 14 | if (getType(obj) !== "object") { 15 | return {}; 16 | } 17 | const newObj: PlainObject = {}; 18 | Object.keys(obj).forEach((key) => { 19 | const element = obj[key]; 20 | if (Array.isArray(element)) { 21 | if (element.length > 0) { 22 | newObj[key] = isArrayToString ? element.join(",") : [...element]; 23 | } 24 | } else if (isDefined(element)) { 25 | newObj[key] = element; 26 | } 27 | }); 28 | return newObj; 29 | } 30 | -------------------------------------------------------------------------------- /app/webpack-vue3/src/utils/tree.ts: -------------------------------------------------------------------------------- 1 | import { TreeNode } from "@/bean/base"; 2 | 3 | // overload 4 | export function tree2Arr(tree: Array, replaceChildren?: string): Array; 5 | export function tree2Arr( 6 | tree: Array, 7 | replaceChildren?: string, 8 | mapper?: (item: T, index: number, arr: Array) => D 9 | ): Array; 10 | export function tree2Arr( 11 | tree: Array, 12 | replaceChildren = "children", 13 | mapper?: (item: T, index: number, arr: Array) => D 14 | ): Array | Array { 15 | const result = tree.reduce((prev, curr) => { 16 | const children = curr[replaceChildren] as T[]; 17 | const list = children && children.length > 0 ? [curr, ...tree2Arr(children, replaceChildren, mapper)] : [curr]; 18 | return prev.concat(list as ConcatArray); 19 | }, [] as Array); 20 | return typeof mapper === "function" ? result.map(mapper) : result; 21 | } 22 | -------------------------------------------------------------------------------- /app/webpack-vue3/src/utils/validator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author: Tusi 3 | * @description: 通用校验 4 | */ 5 | 6 | export const REQUIRED_VALIDATOR_BLUR = { 7 | required: true, 8 | message: "必填项", 9 | trigger: "blur", 10 | }; 11 | 12 | export const EMAIL_VALIDATOR = { 13 | pattern: 14 | /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/, 15 | message: "邮箱格式不正确", 16 | trigger: "blur", 17 | }; 18 | 19 | export const URL_VALIDATOR = { 20 | pattern: /(http|https):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-.,@?^=%&:/~+#]*[\w\-@?^=%&/~+#])?/, 21 | message: "链接格式不正确,注意以http或https开头", 22 | trigger: "blur", 23 | }; 24 | -------------------------------------------------------------------------------- /app/webpack-vue3/src/views/404/index.vue: -------------------------------------------------------------------------------- 1 | 5 | 15 | 16 | 38 | -------------------------------------------------------------------------------- /app/webpack-vue3/src/views/backend/article/index.module.scss: -------------------------------------------------------------------------------- 1 | .articlePoster { 2 | width: 100px; 3 | height: 72px; 4 | > img { 5 | width: 100%; 6 | height: 100%; 7 | object-fit: contain; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /app/webpack-vue3/src/views/backend/styles/avatar.scss: -------------------------------------------------------------------------------- 1 | :deep(.comment-avatar) { 2 | width: 40px; 3 | height: 40px; 4 | > img { 5 | width: 100%; 6 | height: 100%; 7 | border-radius: 100%; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /app/webpack-vue3/tests/unit/example.spec.ts: -------------------------------------------------------------------------------- 1 | // import { shallowMount } from "@vue/test-utils"; 2 | // import HelloWorld from "@/components/HelloWorld.vue"; 3 | 4 | // describe("HelloWorld.vue", () => { 5 | // it("renders props.msg when passed", () => { 6 | // const msg = "new message"; 7 | // const wrapper = shallowMount(HelloWorld, { 8 | // props: { msg }, 9 | // }); 10 | // expect(wrapper.text()).toMatch(msg); 11 | // }); 12 | // }); 13 | -------------------------------------------------------------------------------- /app/webpack-vue3/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "strict": true, 5 | "moduleResolution": "node", 6 | "allowJs": true, 7 | "sourceMap": true, 8 | "baseUrl": ".", 9 | "types": [ 10 | "webpack-env", 11 | "jest" 12 | ], 13 | "paths": { 14 | "@/*": [ 15 | "src/*" 16 | ] 17 | }, 18 | "lib": [ 19 | "esnext", 20 | "dom", 21 | "dom.iterable", 22 | "scripthost" 23 | ] 24 | }, 25 | "include": [ 26 | "src/**/*.ts", 27 | "src/**/*.tsx", 28 | "src/**/*.vue", 29 | "tests/**/*.ts", 30 | "tests/**/*.tsx" 31 | ], 32 | "exclude": [ 33 | "node_modules" 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /build-dev-images.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker build . -f Dockerfile.dev --target vite-vue3-frontend --tag fullstack-blog-vite-vue3:dev 4 | # docker build . -f Dockerfile.dev --target cra-react18-frontend --tag fullstack-blog-cra-react18:dev 5 | docker build . -f Dockerfile.dev --target express-backend --tag fullstack-blog-express:dev 6 | # docker build . -f Dockerfile.dev --target nestjs-backend --tag fullstack-blog-nestjs:dev 7 | 8 | echo "build dev images successfully." 9 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | extends: ["@commitlint/config-conventional"], 3 | /* 4 | * Any rules defined here will override rules from @commitlint/config-conventional 5 | */ 6 | rules: { 7 | "type-enum": [2, "always", ["build", "chore", "ci", "docs", "feat", "fix", "perf", "refactor", "revert", "style", "test"]], 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /legacy-ops.md: -------------------------------------------------------------------------------- 1 | 这是通过常规方式运行项目的说明,如果你要使用 docker,请直接看 [docker-ops](./docker-ops.md) 文档。 2 | 3 | ## 开发环境 4 | 5 | 安装依赖: 6 | 7 | ```shell 8 | pnpm install 9 | ``` 10 | 11 | 由于不是使用 docker compose,需要修改`vite.config.ts`中的`server`配置: 12 | 13 | ``` 14 | target: "http://127.0.0.1:8012", 15 | ``` 16 | 17 | 启动项目: 18 | 19 | ```shell 20 | pnpm run fullstack:dev 21 | ``` 22 | 23 | ## 生产环境 24 | 25 | 前端部分,打包好资源 scp 到服务器。 26 | 27 | 后端部分,通过 pm2 deploy 部署到服务器,并重启相关服务。 28 | 29 | 以上都可以用脚本配合 CI/CD 工具实现,具体可以参考这篇文章《[前端上手全栈自动化部署,让你看起来像个“高手”](https://juejin.cn/post/7373488886461431860)》。 30 | -------------------------------------------------------------------------------- /mysql/my.cnf: -------------------------------------------------------------------------------- 1 | [mysqld] 2 | character-set-server=utf8mb4 3 | collation-server=utf8mb4_0900_ai_ci 4 | mysql-native-password=ON 5 | -------------------------------------------------------------------------------- /nginx/default.conf.template: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | root /usr/share/nginx/html; 4 | index index.html; 5 | 6 | location / { 7 | try_files $uri $uri/ /index.html; 8 | } 9 | 10 | location /api/ { 11 | rewrite ^/api/(.*) /$1 break; 12 | proxy_pass http://${SERVER_NAME}:${SERVER_PORT}/; 13 | proxy_set_header Host $host; 14 | proxy_set_header X-Real-IP $remote_addr; 15 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 16 | proxy_set_header X-Forwarded-Proto $scheme; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/eslint-config/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @fullstack-blog/eslint-config 2 | 3 | ## 1.0.1 4 | 5 | ### Patch Changes 6 | 7 | - changeset init 8 | -------------------------------------------------------------------------------- /packages/eslint-config/base.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'eslint:recommended', 4 | 'plugin:import/recommended', 5 | 'plugin:import/typescript', 6 | 'airbnb-base', 7 | 'prettier', 8 | ], 9 | plugins: ['import', 'prettier'], 10 | settings: { 11 | 'import/extensions': ['.js', '.jsx', '.ts', '.tsx', '.vue', '.json'], 12 | 'import/parsers': { 13 | '@typescript-eslint/parser': ['.ts', '.tsx'], 14 | }, 15 | }, 16 | rules: { 17 | 'no-console': ['warn', { allow: ['warn', 'error'] }], 18 | 'no-plusplus': 'off', 19 | 'no-use-before-define': 'off', 20 | 'global-require': 'off', 21 | 'func-names': ['off', 'as-needed'], 22 | 'consistent-return': 'off', 23 | 'camelcase': 'off', 24 | 'import/no-cycle': 'off', 25 | 'prefer-promise-reject-errors': 'off', 26 | 'default-param-last': 'off', 27 | 'no-unused-expressions': ['warn', { 'allowTernary': true }], 28 | 'no-param-reassign': 'off', 29 | 'prettier/prettier': 'error', 30 | 'import/prefer-default-export': 'off', 31 | 'import/extensions': 'off', 32 | 'import/no-named-as-default': 'off', 33 | 'import/no-extraneous-dependencies': ['error', { devDependencies: true, optionalDependencies: true, peerDependencies: true }], 34 | }, 35 | } -------------------------------------------------------------------------------- /packages/eslint-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fullstack-blog/eslint-config", 3 | "version": "1.0.1", 4 | "private": true, 5 | "description": "common eslint config", 6 | "files": [ 7 | "base.js" 8 | ], 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "author": "Tusi", 13 | "license": "MIT", 14 | "dependencies": { 15 | "eslint-config-airbnb-base": "^15.0.0", 16 | "eslint-config-prettier": "^9.1.0", 17 | "eslint-plugin-import": "^2.23.4", 18 | "eslint-plugin-prettier": "^3.3.1", 19 | "prettier": "^2.2.1" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "packages/*" 3 | - "app/*" 4 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "jsx": "preserve", 6 | "importHelpers": true, 7 | "skipLibCheck": true, 8 | "esModuleInterop": true, 9 | "allowSyntheticDefaultImports": true, 10 | "resolveJsonModule": true, 11 | "strictNullChecks": true 12 | } 13 | } 14 | --------------------------------------------------------------------------------