├── .dockerignore ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .gitpod.yml ├── .prettierignore ├── .prettierrc.js ├── .stylelintrc.js ├── CODE_OF_CONDUCT.md ├── Dockerfile ├── Dockerfile.dev ├── Dockerfile.hub ├── LICENSE ├── README.md ├── azure-pipelines.yml ├── config ├── config.ts ├── defaultSettings.ts ├── plugin.config.ts ├── proxy.ts ├── router.config.ts └── theme.config.json ├── docker ├── docker-compose.dev.yml ├── docker-compose.yml └── nginx.conf ├── jest.config.js ├── jsconfig.json ├── lambda ├── api.js └── mock │ ├── index.js │ └── matchMock.js ├── package.json ├── public ├── favicon.png ├── home_bg.png └── icons │ ├── icon-128x128.png │ ├── icon-192x192.png │ └── icon-512x512.png ├── src ├── app.ts ├── assets │ └── logo.svg ├── components │ ├── AssignPermissionModal │ │ └── index.tsx │ ├── AssignRoleModal │ │ └── index.tsx │ ├── Authorized │ │ ├── Authorized.tsx │ │ ├── AuthorizedRoute.tsx │ │ ├── CheckPermissions.tsx │ │ ├── PromiseRender.tsx │ │ ├── Secured.tsx │ │ ├── index.tsx │ │ └── renderAuthorize.ts │ ├── Buttons │ │ ├── DownvoteBtn.tsx │ │ ├── FavoriteBtn.tsx │ │ ├── LikeBtn.tsx │ │ ├── RelationBtn.tsx │ │ └── UpvoteBtn.tsx │ ├── Ellipsis │ │ ├── demo │ │ │ ├── line.md │ │ │ └── number.md │ │ ├── index.d.ts │ │ ├── index.en-US.md │ │ ├── index.js │ │ ├── index.less │ │ ├── index.test.js │ │ └── index.zh-CN.md │ ├── GlobalHeader │ │ ├── AvatarDropdown.tsx │ │ ├── NoticeIcon.tsx │ │ ├── RightContent.tsx │ │ └── index.less │ ├── HeaderDropdown │ │ ├── index.less │ │ └── index.tsx │ ├── HeaderSearch │ │ ├── index.less │ │ └── index.tsx │ ├── MarkdownBody │ │ ├── index.tsx │ │ └── tocify.tsx │ ├── ModalForm │ │ └── index.tsx │ └── PageLoading │ │ └── index.tsx ├── e2e │ ├── __mocks__ │ │ └── antd-pro-merge-less.js │ ├── baseLayout.e2e.js │ └── topMenu.e2e.js ├── global.less ├── global.tsx ├── layouts │ ├── AuthLayout.less │ ├── AuthLayout.tsx │ ├── BasicLayout.tsx │ ├── BlankLayout.tsx │ └── SecurityLayout.tsx ├── locales │ ├── en-US.ts │ ├── en-US │ │ ├── component.ts │ │ ├── globalHeader.ts │ │ ├── menu.ts │ │ ├── pwa.ts │ │ ├── settingDrawer.ts │ │ └── settings.ts │ ├── pt-BR.ts │ ├── pt-BR │ │ ├── component.ts │ │ ├── globalHeader.ts │ │ ├── menu.ts │ │ ├── pwa.ts │ │ ├── settingDrawer.ts │ │ └── settings.ts │ ├── zh-CN.ts │ ├── zh-CN │ │ ├── component.ts │ │ ├── globalHeader.ts │ │ ├── menu.ts │ │ ├── pwa.ts │ │ ├── settingDrawer.ts │ │ └── settings.ts │ ├── zh-TW.ts │ └── zh-TW │ │ ├── component.ts │ │ ├── globalHeader.ts │ │ ├── menu.ts │ │ ├── pwa.ts │ │ ├── settingDrawer.ts │ │ └── settings.ts ├── manifest.json ├── models │ ├── I.d.ts │ ├── auth.ts │ ├── connect.d.ts │ ├── global.ts │ └── setting.ts ├── pages │ ├── 404.tsx │ ├── account │ │ ├── center │ │ │ ├── components │ │ │ │ ├── Comments │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── style.less │ │ │ │ ├── Favorites │ │ │ │ │ └── index.tsx │ │ │ │ └── Likers │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── style.less │ │ │ ├── index.tsx │ │ │ ├── services.ts │ │ │ └── style.less │ │ ├── notifications │ │ │ ├── components │ │ │ │ ├── Messages │ │ │ │ │ └── index.tsx │ │ │ │ ├── Notifications │ │ │ │ │ ├── components │ │ │ │ │ │ ├── CommentMyArticle.tsx │ │ │ │ │ │ ├── LikedMyArticle.tsx │ │ │ │ │ │ ├── MentionedMe.tsx │ │ │ │ │ │ ├── ReplyMyComment.tsx │ │ │ │ │ │ ├── UpVotedMyComment.tsx │ │ │ │ │ │ └── style.less │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── style.less │ │ │ │ └── Systems │ │ │ │ │ └── index.tsx │ │ │ ├── index.tsx │ │ │ ├── services.ts │ │ │ └── style.less │ │ └── settings │ │ │ ├── components │ │ │ ├── AvatarView.less │ │ │ ├── AvatarView.tsx │ │ │ ├── BaseView.less │ │ │ ├── GeographicView.less │ │ │ ├── GeographicView.tsx │ │ │ ├── base.tsx │ │ │ ├── notification.tsx │ │ │ └── security.tsx │ │ │ ├── data.d.ts │ │ │ ├── geographic │ │ │ ├── city.json │ │ │ └── province.json │ │ │ ├── index.tsx │ │ │ ├── services.ts │ │ │ └── style.less │ ├── articles │ │ ├── create │ │ │ ├── index.tsx │ │ │ ├── services.ts │ │ │ └── style.less │ │ ├── edit │ │ │ ├── index.tsx │ │ │ ├── services.ts │ │ │ └── style.less │ │ ├── list │ │ │ ├── components │ │ │ │ ├── ArticleListContent │ │ │ │ │ ├── index.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── StandardFormRow │ │ │ │ │ ├── index.less │ │ │ │ │ └── index.tsx │ │ │ │ └── TagSelect │ │ │ │ │ ├── index.less │ │ │ │ │ └── index.tsx │ │ │ ├── index.tsx │ │ │ ├── services.ts │ │ │ └── style.less │ │ └── show │ │ │ ├── components │ │ │ ├── ArticleComments │ │ │ │ ├── Editor.less │ │ │ │ ├── Editor.tsx │ │ │ │ ├── InlineUpload.ts │ │ │ │ ├── calculateNodeHeight.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── style.less │ │ │ └── ArticleContent │ │ │ │ ├── index.tsx │ │ │ │ └── style.less │ │ │ ├── index.tsx │ │ │ ├── services.ts │ │ │ └── style.less │ ├── auth │ │ └── login │ │ │ ├── index.tsx │ │ │ ├── services.ts │ │ │ └── style.less │ ├── comments │ │ └── list │ │ │ ├── index.tsx │ │ │ ├── services.ts │ │ │ └── style.less │ ├── demos │ │ ├── emojipicker │ │ │ ├── index.tsx │ │ │ └── style.less │ │ └── simplemdeeditor │ │ │ ├── index.tsx │ │ │ └── style.less │ ├── document.ejs │ ├── permissions │ │ └── list │ │ │ ├── components │ │ │ ├── CreateModal.tsx │ │ │ └── UpdateModal.tsx │ │ │ ├── index.tsx │ │ │ ├── services.ts │ │ │ └── style.less │ ├── roles │ │ └── list │ │ │ ├── components │ │ │ ├── CreateModal.tsx │ │ │ └── UpdateModal.tsx │ │ │ ├── index.tsx │ │ │ ├── services.ts │ │ │ └── style.less │ ├── sensitivewords │ │ ├── index.tsx │ │ └── services.ts │ ├── tags │ │ └── list │ │ │ ├── components │ │ │ ├── CreateModal.tsx │ │ │ └── UpdateModal.tsx │ │ │ ├── index.tsx │ │ │ └── services.ts │ ├── tools │ │ └── emojicheatsheet │ │ │ ├── index.tsx │ │ │ ├── style.less │ │ │ └── utils.ts │ └── users │ │ └── list │ │ ├── index.tsx │ │ └── services.ts ├── service-worker.js ├── services.ts ├── typings.d.ts ├── utils │ ├── Authorized.ts │ ├── authority.ts │ ├── scrollToTop.ts │ ├── utils.less │ └── utils.ts └── websocket.ts └── tsconfig.json /.dockerignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | **/node_modules 5 | /src/utils/request-temp.js 6 | 7 | # production 8 | /.vscode 9 | 10 | # misc 11 | .DS_Store 12 | npm-debug.log* 13 | yarn-error.log 14 | 15 | /coverage 16 | .idea 17 | yarn.lock 18 | package-lock.json 19 | *bak 20 | .vscode 21 | 22 | # visual studio code 23 | .history 24 | *.log 25 | 26 | functions/mock 27 | .temp/** 28 | 29 | # umi 30 | .umi 31 | .umi-production 32 | 33 | # screenshot 34 | screenshot 35 | .firebase -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [Makefile] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /lambda/ 2 | /scripts 3 | /config 4 | .history -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [require.resolve('@umijs/fabric/dist/eslint')], 3 | rules: { 4 | 'react/no-array-index-key': [0], 5 | 'react/no-danger': [0], 6 | 'react/sort-comp': [0], 7 | 'no-plusplus': [0], 8 | 'no-unused-expressions': [0], 9 | 'react/no-children-prop': [0], 10 | 'consistent-return': [0], 11 | }, 12 | globals: { 13 | page: true, 14 | REACT_APP_ENV: true, 15 | SOCKET_HOST: true, 16 | UPLOAD_URL: true, 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | **/node_modules 5 | # roadhog-api-doc ignore 6 | /src/utils/request-temp.js 7 | _roadhog-api-doc 8 | 9 | # production 10 | /dist 11 | /.vscode 12 | 13 | # misc 14 | .DS_Store 15 | npm-debug.log* 16 | yarn-error.log 17 | 18 | /coverage 19 | .idea 20 | yarn.lock 21 | package-lock.json 22 | *bak 23 | .vscode 24 | 25 | # visual studio code 26 | .history 27 | *.log 28 | functions/* 29 | .temp/** 30 | 31 | # umi 32 | .umi 33 | .umi-production 34 | 35 | # screenshot 36 | screenshot 37 | .firebase 38 | .eslintcache 39 | 40 | build 41 | 42 | /.webpack.config.js 43 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | ports: 2 | - port: 8000 3 | onOpen: open-preview 4 | tasks: 5 | - init: npm install 6 | command: npm start 7 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/*.svg 2 | package.json 3 | .umi 4 | .umi-production 5 | /dist 6 | .dockerignore 7 | .DS_Store 8 | .eslintignore 9 | *.png 10 | *.toml 11 | docker 12 | .editorconfig 13 | Dockerfile* 14 | .gitignore 15 | .prettierignore 16 | LICENSE 17 | .eslintcache 18 | *.lock 19 | yarn-error.log 20 | .history 21 | CNAME 22 | /build 23 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | const fabric = require('@umijs/fabric'); 2 | 3 | module.exports = { 4 | ...fabric.prettier, 5 | }; 6 | -------------------------------------------------------------------------------- /.stylelintrc.js: -------------------------------------------------------------------------------- 1 | const fabric = require('@umijs/fabric'); 2 | 3 | module.exports = { 4 | ...fabric.stylelint, 5 | }; 6 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | - Using welcoming and inclusive language 12 | - Being respectful of differing viewpoints and experiences 13 | - Gracefully accepting constructive criticism 14 | - Focusing on what is best for the community 15 | - Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | - The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | - Trolling, insulting/derogatory comments, and personal or political attacks 21 | - Public or private harassment 22 | - Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | - Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at afc163@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM circleci/node:latest-browsers 2 | 3 | WORKDIR /usr/src/app/ 4 | USER root 5 | COPY package.json ./ 6 | RUN yarn 7 | 8 | COPY ./ ./ 9 | 10 | RUN npm run test:all 11 | 12 | RUN npm run fetch:blocks 13 | 14 | CMD ["npm", "run", "build"] 15 | -------------------------------------------------------------------------------- /Dockerfile.dev: -------------------------------------------------------------------------------- 1 | FROM node:latest 2 | 3 | WORKDIR /usr/src/app/ 4 | 5 | COPY package.json ./ 6 | RUN npm install --silent --no-cache --registry=https://registry.npm.taobao.org 7 | 8 | COPY ./ ./ 9 | 10 | RUN npm run fetch:blocks 11 | 12 | CMD ["npm", "run", "start"] 13 | -------------------------------------------------------------------------------- /Dockerfile.hub: -------------------------------------------------------------------------------- 1 | FROM circleci/node:latest-browsers as builder 2 | 3 | WORKDIR /usr/src/app/ 4 | USER root 5 | COPY package.json ./ 6 | RUN yarn 7 | 8 | COPY ./ ./ 9 | 10 | RUN npm run test:all 11 | 12 | RUN npm run fetch:blocks 13 | 14 | RUN npm run build 15 | 16 | 17 | FROM nginx 18 | 19 | WORKDIR /usr/share/nginx/html/ 20 | 21 | COPY ./docker/nginx.conf /etc/nginx/conf.d/default.conf 22 | 23 | COPY --from=builder /usr/src/app/dist /usr/share/nginx/html/ 24 | 25 | EXPOSE 80 26 | 27 | CMD ["nginx", "-g", "daemon off;"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Alipay.inc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > 该项目以迁移至 https://github.com/yanthink/pingfan.blog ,今后不再维护 2 | 3 | ## 项目概述 4 | 5 | - 产品名称:个人博客系统前端 6 | - 项目代号:blog-v2 7 | - 演示地址:https://www.einsition.com 8 | - api 项目地址:https://github.com/yanthink/blog-api 9 | 10 | 该系统使用 ANT DESIGN PROv4.0 编写而成。 11 | 12 | ## 功能如下 13 | 14 | - 文章列表 -- 支持搜索; 15 | - 文章详情 -- 支持代码高亮、emoji 表情,支持评论、回复、收藏、点赞; 16 | - 用户认证 -- 登录、退出,支持小程序扫码登录绑定; 17 | - 权限控制 -- 权限控制菜单和内容; 18 | - 文章管理 -- 列表、详情、发布、修改。集成 SimpleMDE 编辑器,支持粘贴、拖拽上传附件和 emoji 表情; 19 | - 用户管理 -- 列表、添加、修改、分配角色权限; 20 | - 在线用户 -- 实时查看在线用户数据; 21 | - 通知管理 -- 在线时 websocket 接收,离线时邮件通知; 22 | - 个人设置 -- 用户名、密码、头像修改,邮箱验证; 23 | - debugbar; 24 | 25 | ## 开发环境部署/安装 26 | 27 | 本项目代码使用 React 框架 [ANT DESIGN PRO](https://pro.ant.design/index-cn) 开发。 28 | 29 | # 基础安装 30 | 31 | #### 1. 克隆源代码 32 | 33 | 克隆 `blog` 源代码到本地: 34 | 35 | > git clone https://github.com/yanthink/blog-v2.git 36 | 37 | #### 2. 安装扩展包依赖 38 | 39 | ```shell 40 | $ npm install 41 | ``` 42 | 43 | 你可以根据情况修改 `config/config.ts` 文件里的内容,如代理设置等: 44 | 45 | ``` 46 | proxy: { 47 | '/api': { 48 | target: 'http://api.blog.test/', 49 | changeOrigin: true, 50 | }, 51 | '/_debugbar': { 52 | target: 'http://api.blog.test/', 53 | changeOrigin: true, 54 | }, 55 | }, 56 | ``` 57 | 58 | #### Usage 59 | 60 | ```shell 61 | $ npm start # visit http://localhost:8000 62 | ``` 63 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | # Node.js 2 | # Build a general Node.js project with npm. 3 | # Add steps that analyze code, save build artifacts, deploy, and more: 4 | # https://docs.microsoft.com/azure/devops/pipelines/languages/javascript 5 | name: ant design pro 6 | 7 | trigger: 8 | - master 9 | 10 | jobs: 11 | - job: lintAndBuild 12 | 13 | pool: 14 | vmImage: 'Ubuntu-16.04' 15 | 16 | steps: 17 | - checkout: self 18 | clean: false 19 | - script: yarn 20 | displayName: install 21 | - script: npm run lint 22 | displayName: lint 23 | - script: npm run tsc 24 | displayName: tsc 25 | - script: npm run build 26 | env: 27 | PROGRESS: none 28 | displayName: build 29 | 30 | - job: test 31 | pool: 32 | vmImage: 'Ubuntu-16.04' 33 | 34 | container: 35 | image: circleci/node:latest-browsers 36 | options: '-u root' 37 | 38 | steps: 39 | - script: yarn 40 | displayName: install 41 | - script: npm run test:all 42 | env: 43 | PROGRESS: none 44 | UMI_UI: none 45 | displayName: test 46 | 47 | - job: Windows 48 | pool: 49 | vmImage: 'windows-latest' 50 | steps: 51 | - task: NodeTool@0 52 | inputs: 53 | versionSpec: '12.x' 54 | - script: yarn 55 | displayName: install 56 | - script: npm run lint 57 | displayName: lint 58 | - script: npm run tsc 59 | displayName: tsc 60 | - script: npm run test:all 61 | env: 62 | PROGRESS: none 63 | UMI_UI: none 64 | displayName: test 65 | - script: npm run build 66 | env: 67 | PROGRESS: none 68 | displayName: build 69 | 70 | - job: MacOS 71 | pool: 72 | vmImage: 'macOS-latest' 73 | steps: 74 | - task: NodeTool@0 75 | inputs: 76 | versionSpec: '12.x' 77 | - script: yarn 78 | displayName: install 79 | - script: npm run lint 80 | displayName: lint 81 | - script: npm run tsc 82 | displayName: tsc 83 | - script: npm run 84 | env: 85 | PROGRESS: none 86 | UMI_UI: none 87 | displayName: build 88 | -------------------------------------------------------------------------------- /config/config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'umi'; 2 | import defaultSettings from './defaultSettings'; // https://umijs.org/config/ 3 | import proxy from './proxy'; 4 | import routerData from './router.config'; 5 | import webpackPlugin from './plugin.config'; 6 | 7 | const { REACT_APP_ENV } = process.env; 8 | const prod = process.env.NODE_ENV === 'production'; 9 | 10 | export default defineConfig({ 11 | hash: true, 12 | // @umijs/preset-react 13 | antd: { 14 | // https://umijs.org/plugins/plugin-antd 15 | dark: false, 16 | }, 17 | dva: { 18 | // https://umijs.org/plugins/plugin-dva 19 | hmr: true, 20 | }, 21 | locale: { 22 | default: 'zh-CN', 23 | antd: true, 24 | title: false, 25 | baseNavigator: false, 26 | baseSeparator: '-', 27 | }, 28 | dynamicImport: { 29 | loading: '@/components/PageLoading/index', 30 | }, 31 | request: { 32 | // https://umijs.org/plugins/plugin-request 33 | dataField: 'data', 34 | }, 35 | targets: { 36 | ie: 11, 37 | }, 38 | // umi routes: https://umijs.org/zh/guide/router.html 39 | routes: routerData, 40 | // Theme for antd: https://ant.design/docs/react/customize-theme-cn 41 | theme: { 42 | // ...darkTheme, 43 | 'primary-color': defaultSettings.primaryColor, 44 | }, 45 | // @ts-ignore 46 | title: false, 47 | define: { 48 | SOCKET_HOST: !prod ? 'http://api.blog.test' : 'https://www.einsition.com', 49 | UPLOAD_URL: '/api/attachments/upload', 50 | }, 51 | ignoreMomentLocale: true, 52 | manifest: { 53 | basePath: '/', 54 | }, 55 | proxy: proxy[REACT_APP_ENV || 'dev'], 56 | extraBabelPlugins: [ 57 | [ 58 | 'prismjs', 59 | { 60 | languages: [ 61 | 'markup', 62 | 'markup-templating', 63 | 'cpp', 64 | 'css', 65 | 'less', 66 | 'scss', 67 | 'clike', 68 | 'javascript', 69 | 'typescript', 70 | 'jsx', 71 | 'tsx', 72 | 'php', 73 | 'java', 74 | 'bash', 75 | 'ini', 76 | 'json', 77 | 'sql', 78 | 'yaml', 79 | ], 80 | plugins: ['line-numbers', 'show-language', 'copy-to-clipboard'], 81 | theme: 'okaidia', 82 | css: true, 83 | }, 84 | ], 85 | ], 86 | // https://umijs.org/zh-CN/guide/boost-compile-speed 87 | nodeModulesTransform: { 88 | type: prod ? 'all' : 'none', 89 | exclude: [], 90 | }, 91 | chainWebpack: webpackPlugin, 92 | devtool: !prod ? 'source-map' : false, 93 | }); 94 | -------------------------------------------------------------------------------- /config/defaultSettings.ts: -------------------------------------------------------------------------------- 1 | import { Settings as ProSettings } from '@ant-design/pro-layout'; 2 | 3 | type DefaultSettings = ProSettings & { 4 | pwa: boolean; 5 | }; 6 | 7 | const proSettings: DefaultSettings = { 8 | navTheme: 'light', 9 | // 拂晓蓝 10 | primaryColor: '#13C2C2', 11 | layout: 'topmenu', 12 | contentWidth: 'Fixed', 13 | fixedHeader: false, 14 | fixSiderbar: false, 15 | colorWeak: false, 16 | menu: { 17 | locale: false, 18 | }, 19 | title: '平凡的博客', 20 | pwa: false, 21 | iconfontUrl: '', 22 | }; 23 | 24 | export { DefaultSettings }; 25 | 26 | export default proSettings; 27 | -------------------------------------------------------------------------------- /config/plugin.config.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import Config from 'webpack-chain'; 3 | import fs from 'fs'; 4 | 5 | export default (config: Config) => { 6 | config.plugin('copy').tap((args) => [ 7 | [ 8 | ...args[0], 9 | ...[ 10 | { 11 | from: join(__dirname, '../node_modules/emoji-assets'), 12 | to: join(__dirname, '../dist/emoji-assets'), 13 | toType: 'dir', 14 | }, 15 | { 16 | from: join(__dirname, '../node_modules/font-awesome'), 17 | to: join(__dirname, '../dist/font-awesome'), 18 | toType: 'dir', 19 | }, 20 | ], 21 | ], 22 | ]); 23 | 24 | const configStr = '/* eslint-disable */\nexport default ' + config.toString(); 25 | fs.writeFileSync('./.webpack.config.js', configStr); 26 | }; 27 | -------------------------------------------------------------------------------- /config/proxy.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | dev: { 3 | '/api/': { 4 | target: 'http://api.blog.test', 5 | changeOrigin: true, 6 | }, 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /config/theme.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "theme": [ 3 | { 4 | "key": "dark", 5 | "theme": "dark", 6 | "fileName": "dark.css" 7 | }, 8 | { 9 | "key": "volcano", 10 | "fileName": "volcano.css", 11 | "modifyVars": { 12 | "@primary-color": "#FA541C" 13 | } 14 | }, 15 | { 16 | "key": "cyan", 17 | "fileName": "cyan.css", 18 | "modifyVars": { 19 | "@primary-color": "#13C2C2" 20 | } 21 | }, 22 | { 23 | "key": "volcano", 24 | "theme": "dark", 25 | "fileName": "dark-volcano.css", 26 | "modifyVars": { 27 | "@primary-color": "#FA541C" 28 | } 29 | }, 30 | { 31 | "key": "cyan", 32 | "theme": "dark", 33 | "fileName": "dark-cyan.css", 34 | "modifyVars": { 35 | "@primary-color": "#13C2C2" 36 | } 37 | } 38 | ], 39 | "min": false, 40 | "isModule": true, 41 | "ignoreAntd": false, 42 | "ignoreProLayout": false, 43 | "cache": true 44 | } 45 | -------------------------------------------------------------------------------- /docker/docker-compose.dev.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | 3 | services: 4 | ant-design-pro_dev: 5 | ports: 6 | - 8000:8000 7 | build: 8 | context: ../ 9 | dockerfile: Dockerfile.dev 10 | container_name: 'ant-design-pro_dev' 11 | volumes: 12 | - ../src:/usr/src/app/src 13 | - ../config:/usr/src/app/config 14 | - ../mock:/usr/src/app/mock 15 | -------------------------------------------------------------------------------- /docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | 3 | services: 4 | ant-design-pro_build: 5 | build: ../ 6 | container_name: 'ant-design-pro_build' 7 | volumes: 8 | - dist:/usr/src/app/dist 9 | 10 | ant-design-pro_web: 11 | image: nginx 12 | ports: 13 | - 80:80 14 | container_name: 'ant-design-pro_web' 15 | restart: unless-stopped 16 | volumes: 17 | - dist:/usr/share/nginx/html:ro 18 | - ./nginx.conf:/etc/nginx/conf.d/default.conf 19 | 20 | volumes: 21 | dist: 22 | -------------------------------------------------------------------------------- /docker/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | # gzip config 4 | gzip on; 5 | gzip_min_length 1k; 6 | gzip_comp_level 9; 7 | gzip_types text/plain text/css text/javascript application/json application/javascript application/x-javascript application/xml; 8 | gzip_vary on; 9 | gzip_disable "MSIE [1-6]\."; 10 | 11 | root /usr/share/nginx/html; 12 | 13 | location / { 14 | try_files $uri $uri/ /index.html; 15 | } 16 | location /api { 17 | proxy_pass https://ant-design-pro.netlify.com; 18 | proxy_set_header X-Forwarded-Proto $scheme; 19 | proxy_set_header X-Real-IP $remote_addr; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testURL: 'http://localhost:8000', 3 | testEnvironment: './tests/PuppeteerEnvironment', 4 | verbose: false, 5 | globals: { 6 | ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: false, 7 | localStorage: null, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "emitDecoratorMetadata": true, 4 | "experimentalDecorators": true, 5 | "baseUrl": ".", 6 | "paths": { 7 | "@/*": ["./src/*"] 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lambda/api.js: -------------------------------------------------------------------------------- 1 | // [START functions import] 2 | const express = require('express'); 3 | const serverLess = require('serverless-http'); 4 | 5 | const matchMock = require('./mock/matchMock'); 6 | 7 | const app = express(); 8 | 9 | app.all('*', (req, res, next) => { 10 | res.header('Access-Control-Allow-Origin', '*'); 11 | res.header( 12 | 'Access-Control-Allow-Headers', 13 | 'Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild', 14 | ); 15 | res.header('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS'); 16 | 17 | if (req.method == 'OPTIONS') { 18 | res.send(200); 19 | } else { 20 | next(); 21 | } 22 | }); 23 | 24 | app.use(matchMock); 25 | 26 | exports.handler = serverLess(app); 27 | -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanthink/blog-v2/3e61226613adf361b63a406773d0a9908b03c1d6/public/favicon.png -------------------------------------------------------------------------------- /public/home_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanthink/blog-v2/3e61226613adf361b63a406773d0a9908b03c1d6/public/home_bg.png -------------------------------------------------------------------------------- /public/icons/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanthink/blog-v2/3e61226613adf361b63a406773d0a9908b03c1d6/public/icons/icon-128x128.png -------------------------------------------------------------------------------- /public/icons/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanthink/blog-v2/3e61226613adf361b63a406773d0a9908b03c1d6/public/icons/icon-192x192.png -------------------------------------------------------------------------------- /public/icons/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanthink/blog-v2/3e61226613adf361b63a406773d0a9908b03c1d6/public/icons/icon-512x512.png -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | // https://umijs.org/zh/guide/with-dva.html#%E9%85%8D%E7%BD%AE%E5%8F%8A%E6%8F%92%E4%BB%B6 2 | // https://dvajs.com/api/ 3 | import { RequestConfig } from 'umi'; 4 | import { getToken, getSocketId } from '@/utils/authority'; 5 | 6 | export const request: RequestConfig = { 7 | timeout: 30000, 8 | prefix: '/api/', 9 | credentials: 'omit', 10 | headers: { 11 | Accept: 'application/json', 12 | 'Content-Type': 'application/json; charset=utf-8', 13 | 'X-Socket-ID': getSocketId(), 14 | }, 15 | errorConfig: { 16 | adaptor: (resData) => ({ 17 | ...resData, 18 | errorMessage: resData.message, 19 | }), 20 | }, 21 | middlewares: [ 22 | async function withToken(ctx, next) { 23 | ctx.req.options.headers = { 24 | ...ctx.req.options.headers, 25 | Authorization: `Bearer ${getToken()}`, 26 | }; 27 | await next(); 28 | }, 29 | ], 30 | }; 31 | 32 | // https://umijs.org/zh/guide/with-dva.html#%E9%85%8D%E7%BD%AE%E5%8F%8A%E6%8F%92%E4%BB%B6 33 | // https://dvajs.com/api/ 34 | export const dva = { 35 | config: { 36 | onError(e: any) { 37 | // effect 执行错误或 subscription 通过 done 主动抛错时触发 38 | e.preventDefault(); 39 | }, 40 | }, 41 | }; 42 | 43 | let lastPathname = ''; 44 | 45 | export function onRouteChange({ location }: any) { 46 | if (lastPathname !== location.pathname) { 47 | window.scrollTo(0, 0); 48 | lastPathname = location.pathname; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/components/AssignPermissionModal/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { groupBy } from 'lodash'; 3 | import { Checkbox, Form, Modal, Tooltip } from 'antd'; 4 | import { IPermission } from '@/models/I'; 5 | 6 | export interface FormValueType { 7 | permissions: number[]; 8 | } 9 | 10 | export interface AssignPermissionModalProps { 11 | visible: boolean; 12 | onSubmit: (values: FormValueType) => void; 13 | onCancel: () => void; 14 | submitting: boolean; 15 | title?: string; 16 | allPermissions?: IPermission[]; 17 | currentPermissions?: IPermission[]; 18 | } 19 | 20 | const formItemLayout = { labelCol: { span: 5 }, wrapperCol: { span: 18 } }; 21 | 22 | const AssignPermissionModal: React.FC = (props) => { 23 | const [form] = Form.useForm(); 24 | 25 | function handleOK() { 26 | const values = form.getFieldsValue(); 27 | props.onSubmit(values as FormValueType); 28 | } 29 | 30 | const groupPermissions = groupBy( 31 | props.allPermissions, 32 | (item) => (item.name as string).split('.')[0], 33 | ); 34 | 35 | return ( 36 | 45 |
item.id) }} 48 | > 49 | 50 | 51 | {Object.entries(groupPermissions).map(([key, permissions]) => ( 52 | 53 | {permissions.map((permission) => ( 54 | 55 | {permission.display_name} 56 | 57 | ))} 58 | 59 | ))} 60 | 61 | 62 |
63 |
64 | ); 65 | }; 66 | 67 | export default AssignPermissionModal; 68 | -------------------------------------------------------------------------------- /src/components/AssignRoleModal/index.tsx: -------------------------------------------------------------------------------- 1 | import { Form, Modal, Checkbox, Tooltip } from 'antd'; 2 | import React from 'react'; 3 | import { IRole } from '@/models/I'; 4 | 5 | export interface FormValueType { 6 | roles: number[]; 7 | } 8 | 9 | export interface AssignRoleModalProps { 10 | visible: boolean; 11 | onSubmit: (values: FormValueType) => void; 12 | onCancel: () => void; 13 | submitting: boolean; 14 | title?: string; 15 | allRoles?: IRole[]; 16 | currentRoles?: IRole[]; 17 | } 18 | 19 | const Index: React.FC = (props) => { 20 | const [form] = Form.useForm(); 21 | 22 | function handleOK() { 23 | const values = form.getFieldsValue(); 24 | props.onSubmit(values as FormValueType); 25 | } 26 | 27 | return ( 28 | 37 |
item.id) }}> 38 | 39 | 40 | {props.allRoles?.map((role) => ( 41 | 42 | {role.display_name} 43 | 44 | ))} 45 | 46 | 47 |
48 |
49 | ); 50 | }; 51 | 52 | export default Index; 53 | -------------------------------------------------------------------------------- /src/components/Authorized/Authorized.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Result } from 'antd'; 3 | import check, { IAuthorityType } from './CheckPermissions'; 4 | 5 | import AuthorizedRoute from './AuthorizedRoute'; 6 | import Secured from './Secured'; 7 | 8 | interface AuthorizedProps { 9 | authority: IAuthorityType; 10 | noMatch?: React.ReactNode; 11 | } 12 | 13 | type IAuthorizedType = React.FunctionComponent & { 14 | Secured: typeof Secured; 15 | check: typeof check; 16 | AuthorizedRoute: typeof AuthorizedRoute; 17 | }; 18 | 19 | const Authorized: React.FunctionComponent = ({ 20 | children, 21 | authority, 22 | noMatch = ( 23 | 28 | ), 29 | }) => { 30 | const childrenRender: React.ReactNode = typeof children === 'undefined' ? null : children; 31 | const dom = check(authority, childrenRender, noMatch); 32 | return <>{dom}; 33 | }; 34 | 35 | export default Authorized as IAuthorizedType; 36 | -------------------------------------------------------------------------------- /src/components/Authorized/AuthorizedRoute.tsx: -------------------------------------------------------------------------------- 1 | import { Redirect, Route } from 'umi'; 2 | 3 | import React from 'react'; 4 | import Authorized from './Authorized'; 5 | import { IAuthorityType } from './CheckPermissions'; 6 | 7 | interface AuthorizedRouteProps { 8 | currentAuthority: string; 9 | component: React.ComponentClass; 10 | render: (props: any) => React.ReactNode; 11 | redirectPath: string; 12 | authority: IAuthorityType; 13 | } 14 | 15 | const AuthorizedRoute: React.SFC = ({ 16 | component: Component, 17 | render, 18 | authority, 19 | redirectPath, 20 | ...rest 21 | }) => ( 22 | } />} 25 | > 26 | (Component ? : render(props))} 29 | /> 30 | 31 | ); 32 | 33 | export default AuthorizedRoute; 34 | -------------------------------------------------------------------------------- /src/components/Authorized/CheckPermissions.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { CURRENT } from './renderAuthorize'; 3 | // eslint-disable-next-line import/no-cycle 4 | import PromiseRender from './PromiseRender'; 5 | 6 | export type IAuthorityType = 7 | | undefined 8 | | string 9 | | string[] 10 | | Promise 11 | | ((currentAuthority: string | string[]) => IAuthorityType); 12 | 13 | /** 14 | * 通用权限检查方法 15 | * Common check permissions method 16 | * @param { 权限判定 | Permission judgment } authority 17 | * @param { 你的权限 | Your permission description } currentAuthority 18 | * @param { 通过的组件 | Passing components } target 19 | * @param { 未通过的组件 | no pass components } Exception 20 | */ 21 | const checkPermissions = ( 22 | authority: IAuthorityType, 23 | currentAuthority: string | string[], 24 | target: T, 25 | Exception: K, 26 | ): T | K | React.ReactNode => { 27 | // 没有判定权限.默认查看所有 28 | // Retirement authority, return target; 29 | if (!authority) { 30 | return target; 31 | } 32 | // 数组处理 33 | if (Array.isArray(authority)) { 34 | if (Array.isArray(currentAuthority)) { 35 | if (currentAuthority.some((item) => authority.includes(item))) { 36 | return target; 37 | } 38 | } else if (authority.includes(currentAuthority)) { 39 | return target; 40 | } 41 | return Exception; 42 | } 43 | // string 处理 44 | if (typeof authority === 'string') { 45 | if (Array.isArray(currentAuthority)) { 46 | if (currentAuthority.some((item) => authority === item)) { 47 | return target; 48 | } 49 | } else if (authority === currentAuthority) { 50 | return target; 51 | } 52 | return Exception; 53 | } 54 | // Promise 处理 55 | if (authority instanceof Promise) { 56 | return ok={target} error={Exception} promise={authority} />; 57 | } 58 | // Function 处理 59 | if (typeof authority === 'function') { 60 | try { 61 | const bool = authority(currentAuthority); 62 | // 函数执行后返回值是 Promise 63 | if (bool instanceof Promise) { 64 | return ok={target} error={Exception} promise={bool} />; 65 | } 66 | if (bool) { 67 | return target; 68 | } 69 | return Exception; 70 | } catch (error) { 71 | throw error; 72 | } 73 | } 74 | throw new Error('unsupported parameters'); 75 | }; 76 | 77 | export { checkPermissions }; 78 | 79 | function check(authority: IAuthorityType, target: T, Exception: K): T | K | React.ReactNode { 80 | return checkPermissions(authority, CURRENT, target, Exception); 81 | } 82 | 83 | export default check; 84 | -------------------------------------------------------------------------------- /src/components/Authorized/PromiseRender.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Spin } from 'antd'; 3 | import isEqual from 'lodash/isEqual'; 4 | import { isComponentClass } from './Secured'; 5 | // eslint-disable-next-line import/no-cycle 6 | 7 | interface PromiseRenderProps { 8 | ok: T; 9 | error: K; 10 | promise: Promise; 11 | } 12 | 13 | interface PromiseRenderState { 14 | component: React.ComponentClass | React.FunctionComponent; 15 | } 16 | 17 | export default class PromiseRender extends React.Component< 18 | PromiseRenderProps, 19 | PromiseRenderState 20 | > { 21 | state: PromiseRenderState = { 22 | component: () => null, 23 | }; 24 | 25 | componentDidMount() { 26 | this.setRenderComponent(this.props); 27 | } 28 | 29 | shouldComponentUpdate = (nextProps: PromiseRenderProps, nextState: PromiseRenderState) => { 30 | const { component } = this.state; 31 | if (!isEqual(nextProps, this.props)) { 32 | this.setRenderComponent(nextProps); 33 | } 34 | if (nextState.component !== component) return true; 35 | return false; 36 | }; 37 | 38 | // set render Component : ok or error 39 | setRenderComponent(props: PromiseRenderProps) { 40 | const ok = this.checkIsInstantiation(props.ok); 41 | const error = this.checkIsInstantiation(props.error); 42 | props.promise 43 | .then(() => { 44 | this.setState({ 45 | component: ok, 46 | }); 47 | return true; 48 | }) 49 | .catch(() => { 50 | this.setState({ 51 | component: error, 52 | }); 53 | }); 54 | } 55 | 56 | // Determine whether the incoming component has been instantiated 57 | // AuthorizedRoute is already instantiated 58 | // Authorized render is already instantiated, children is no instantiated 59 | // Secured is not instantiated 60 | checkIsInstantiation = ( 61 | target: React.ReactNode | React.ComponentClass, 62 | ): React.FunctionComponent => { 63 | if (isComponentClass(target)) { 64 | const Target = target as React.ComponentClass; 65 | return (props: any) => ; 66 | } 67 | if (React.isValidElement(target)) { 68 | return (props: any) => React.cloneElement(target, props); 69 | } 70 | return () => target as React.ReactNode & null; 71 | }; 72 | 73 | render() { 74 | const { component: Component } = this.state; 75 | const { ok, error, promise, ...rest } = this.props; 76 | 77 | return Component ? ( 78 | 79 | ) : ( 80 |
89 | 90 |
91 | ); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/components/Authorized/Secured.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import CheckPermissions from './CheckPermissions'; 3 | 4 | /** 5 | * 默认不能访问任何页面 6 | * default is "NULL" 7 | */ 8 | const Exception403 = () => 403; 9 | 10 | export const isComponentClass = (component: React.ComponentClass | React.ReactNode): boolean => { 11 | if (!component) return false; 12 | const proto = Object.getPrototypeOf(component); 13 | if (proto === React.Component || proto === Function.prototype) return true; 14 | return isComponentClass(proto); 15 | }; 16 | 17 | // Determine whether the incoming component has been instantiated 18 | // AuthorizedRoute is already instantiated 19 | // Authorized render is already instantiated, children is no instantiated 20 | // Secured is not instantiated 21 | const checkIsInstantiation = (target: React.ComponentClass | React.ReactNode) => { 22 | if (isComponentClass(target)) { 23 | const Target = target as React.ComponentClass; 24 | return (props: any) => ; 25 | } 26 | if (React.isValidElement(target)) { 27 | return (props: any) => React.cloneElement(target, props); 28 | } 29 | return () => target; 30 | }; 31 | 32 | /** 33 | * 用于判断是否拥有权限访问此 view 权限 34 | * authority 支持传入 string, () => boolean | Promise 35 | * e.g. 'user' 只有 user 用户能访问 36 | * e.g. 'user,admin' user 和 admin 都能访问 37 | * e.g. ()=>boolean 返回true能访问,返回false不能访问 38 | * e.g. Promise then 能访问 catch不能访问 39 | * e.g. authority support incoming string, () => boolean | Promise 40 | * e.g. 'user' only user user can access 41 | * e.g. 'user, admin' user and admin can access 42 | * e.g. () => boolean true to be able to visit, return false can not be accessed 43 | * e.g. Promise then can not access the visit to catch 44 | * @param {string | function | Promise} authority 45 | * @param {ReactNode} error 非必需参数 46 | */ 47 | const authorize = (authority: string, error?: React.ReactNode) => { 48 | /** 49 | * conversion into a class 50 | * 防止传入字符串时找不到staticContext造成报错 51 | * String parameters can cause staticContext not found error 52 | */ 53 | let classError: boolean | React.FunctionComponent = false; 54 | if (error) { 55 | classError = (() => error) as React.FunctionComponent; 56 | } 57 | if (!authority) { 58 | throw new Error('authority is required'); 59 | } 60 | return function decideAuthority(target: React.ComponentClass | React.ReactNode) { 61 | const component = CheckPermissions(authority, target, classError || Exception403); 62 | return checkIsInstantiation(component); 63 | }; 64 | }; 65 | 66 | export default authorize; 67 | -------------------------------------------------------------------------------- /src/components/Authorized/index.tsx: -------------------------------------------------------------------------------- 1 | import Authorized from './Authorized'; 2 | import Secured from './Secured'; 3 | import check from './CheckPermissions'; 4 | import renderAuthorize from './renderAuthorize'; 5 | 6 | Authorized.Secured = Secured; 7 | Authorized.check = check; 8 | 9 | const RenderAuthorize = renderAuthorize(Authorized); 10 | 11 | // @ts-ignore 12 | export default RenderAuthorize; 13 | -------------------------------------------------------------------------------- /src/components/Authorized/renderAuthorize.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable eslint-comments/disable-enable-pair */ 2 | /* eslint-disable import/no-mutable-exports */ 3 | let CURRENT: string | string[] = 'NULL'; 4 | 5 | export type CurrentAuthorityType = string | string[] | (() => typeof CURRENT); 6 | /** 7 | * use authority or getAuthority 8 | * @param {string|()=>String} currentAuthority 9 | */ 10 | const renderAuthorize = (Authorized: T): ((currentAuthority: CurrentAuthorityType) => T) => ( 11 | currentAuthority: CurrentAuthorityType, 12 | ): T => { 13 | if (currentAuthority) { 14 | if (typeof currentAuthority === 'function') { 15 | CURRENT = currentAuthority(); 16 | } 17 | if ( 18 | Object.prototype.toString.call(currentAuthority) === '[object String]' || 19 | Array.isArray(currentAuthority) 20 | ) { 21 | CURRENT = currentAuthority as string[]; 22 | } 23 | } else { 24 | CURRENT = 'NULL'; 25 | } 26 | return Authorized; 27 | }; 28 | 29 | export { CURRENT }; 30 | export default (Authorized: T) => renderAuthorize(Authorized); 31 | -------------------------------------------------------------------------------- /src/components/Buttons/DownvoteBtn.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { DislikeOutlined, DislikeTwoTone } from '@ant-design/icons'; 3 | import RelationBtn from './RelationBtn'; 4 | 5 | export interface DownvoteBtnProps { 6 | relation: string; 7 | item: any; 8 | hideText?: boolean; 9 | onAfterToggle?: () => void; 10 | } 11 | 12 | export interface DownvoteBtnState { 13 | downVotesCount: number; 14 | } 15 | 16 | class DownvoteBtn extends React.Component { 17 | constructor(props: DownvoteBtnProps) { 18 | super(props); 19 | 20 | const { item } = props; 21 | 22 | this.state = { 23 | downVotesCount: item.cache?.down_voters_count || 0, 24 | }; 25 | } 26 | 27 | componentWillReceiveProps(nextProps: Readonly) { 28 | this.setState({ downVotesCount: nextProps.item.cache?.down_voters_count || 0 }); 29 | } 30 | 31 | onAfterToggle = (status: boolean) => { 32 | let { downVotesCount } = this.state; 33 | status ? downVotesCount++ : downVotesCount--; 34 | this.setState({ downVotesCount }); 35 | 36 | if (this.props.onAfterToggle) { 37 | this.props.onAfterToggle(); 38 | } 39 | }; 40 | 41 | getChildren = (slot: 'on' | 'off') => ( 42 |
43 | {slot === 'on' ? : } 44 | {!this.props.hideText && {this.state.downVotesCount}} 45 |
46 | ); 47 | 48 | render() { 49 | const { relation, item } = this.props; 50 | 51 | return ( 52 | 60 | ); 61 | } 62 | } 63 | 64 | export default DownvoteBtn; 65 | -------------------------------------------------------------------------------- /src/components/Buttons/FavoriteBtn.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Tooltip } from 'antd'; 3 | import { HeartOutlined, HeartTwoTone } from '@ant-design/icons'; 4 | import RelationBtn from './RelationBtn'; 5 | 6 | export interface FavoriteBtnProps { 7 | relation: string; 8 | item: any; 9 | hideText?: boolean; 10 | onAfterToggle?: () => void; 11 | } 12 | 13 | export interface FavoriteBtnState { 14 | favoritesCount: number; 15 | } 16 | 17 | class FavoriteBtn extends React.Component { 18 | constructor(props: FavoriteBtnProps) { 19 | super(props); 20 | 21 | const { item } = props; 22 | 23 | this.state = { 24 | favoritesCount: item.cache?.favorites_count || 0, 25 | }; 26 | } 27 | 28 | componentWillReceiveProps(nextProps: Readonly) { 29 | this.setState({ favoritesCount: nextProps.item.cache?.favorites_count || 0 }); 30 | } 31 | 32 | onAfterToggle = (status: boolean) => { 33 | let { favoritesCount } = this.state; 34 | status ? favoritesCount++ : favoritesCount--; 35 | this.setState({ favoritesCount }); 36 | 37 | if (this.props.onAfterToggle) { 38 | this.props.onAfterToggle(); 39 | } 40 | }; 41 | 42 | getChildren = (slot: 'on' | 'off') => ( 43 | 44 |
45 | {slot === 'on' ? : } 46 | {!this.props.hideText && {this.state.favoritesCount}} 47 |
48 |
49 | ); 50 | 51 | render() { 52 | const { relation, item } = this.props; 53 | 54 | return ( 55 | 63 | ); 64 | } 65 | } 66 | 67 | export default FavoriteBtn; 68 | -------------------------------------------------------------------------------- /src/components/Buttons/LikeBtn.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Tooltip } from 'antd'; 3 | import { LikeOutlined, LikeTwoTone } from '@ant-design/icons'; 4 | import RelationBtn from './RelationBtn'; 5 | 6 | export interface LikeBtnProps { 7 | relation: string; 8 | item: any; 9 | hideText?: boolean; 10 | onAfterToggle?: () => void; 11 | } 12 | 13 | export interface LikeBtnState { 14 | likesCount: number; 15 | } 16 | 17 | class LikeBtn extends React.Component { 18 | constructor(props: LikeBtnProps) { 19 | super(props); 20 | 21 | const { item } = props; 22 | 23 | this.state = { 24 | likesCount: item.cache?.likes_count || 0, 25 | }; 26 | } 27 | 28 | componentWillReceiveProps(nextProps: Readonly) { 29 | this.setState({ likesCount: nextProps.item.cache.likes_count || 0 }); 30 | } 31 | 32 | onAfterToggle = (status: boolean) => { 33 | let { likesCount } = this.state; 34 | status ? likesCount++ : likesCount--; 35 | this.setState({ likesCount }); 36 | 37 | if (this.props.onAfterToggle) { 38 | this.props.onAfterToggle(); 39 | } 40 | }; 41 | 42 | getChildren = (slot: 'on' | 'off') => ( 43 | 44 |
45 | {slot === 'on' ? : } 46 | {!this.props.hideText && {this.state.likesCount}} 47 |
48 |
49 | ); 50 | 51 | render() { 52 | const { relation, item } = this.props; 53 | 54 | return ( 55 | 63 | ); 64 | } 65 | } 66 | 67 | export default LikeBtn; 68 | -------------------------------------------------------------------------------- /src/components/Buttons/RelationBtn.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react'; 2 | import { connect, history, request } from 'umi'; 3 | import { Modal } from 'antd'; 4 | import { debounce } from 'lodash'; 5 | import { stringify } from 'qs'; 6 | import { getPageQuery } from '@/utils/utils'; 7 | import { AuthModelState, ConnectState } from '@/models/connect'; 8 | 9 | export interface RelationBtnProps { 10 | auth: AuthModelState; 11 | relation: string; 12 | action: string; 13 | item: any; 14 | on: ReactNode; 15 | off: ReactNode; 16 | onAfterToggle?: (status: boolean) => void; 17 | } 18 | 19 | export interface RelationBtnState { 20 | status: boolean; 21 | } 22 | 23 | class RelationBtn extends React.Component { 24 | static types = { 25 | article: 'App\\Models\\Article', 26 | comment: 'App\\Models\\Comment', 27 | }; 28 | 29 | static actions = { 30 | like: 'has_liked', 31 | favorite: 'has_favorited', 32 | upvote: 'has_up_voted', 33 | downvote: 'has_down_voted', 34 | }; 35 | 36 | constructor(props: RelationBtnProps) { 37 | super(props); 38 | 39 | const { item, action } = props; 40 | 41 | this.state = { 42 | status: item[RelationBtn.actions[action]], 43 | }; 44 | } 45 | 46 | componentWillReceiveProps(nextProps: Readonly): void { 47 | const { item, action } = nextProps; 48 | this.setState({ status: item[RelationBtn.actions[action]] }); 49 | } 50 | 51 | toggle = debounce(async () => { 52 | const { relation, action, item, onAfterToggle, auth = { logged: false } } = this.props; 53 | 54 | if (!auth.logged) { 55 | Modal.confirm({ 56 | title: '登录确认?', 57 | content: '您还没有登录,点击【确定】前去登录。', 58 | okText: '确定', 59 | cancelText: '取消', 60 | onOk() { 61 | const { redirect } = getPageQuery(); 62 | // redirect 63 | if (window.location.pathname !== '/auth/login' && !redirect) { 64 | history.replace({ 65 | pathname: '/auth/login', 66 | search: stringify({ 67 | redirect: window.location.href, 68 | }), 69 | }); 70 | } 71 | }, 72 | onCancel() {}, 73 | }); 74 | 75 | return; 76 | } 77 | 78 | await request(`relations/${action}`, { 79 | method: 'POST', 80 | data: { 81 | followable_type: RelationBtn.types[relation], 82 | followable_id: item.id, 83 | }, 84 | }); 85 | 86 | // eslint-disable-next-line react/no-access-state-in-setstate 87 | const status = !this.state.status; 88 | 89 | if (onAfterToggle) { 90 | onAfterToggle(status); 91 | } 92 | 93 | this.setState({ status }); 94 | }, 600); 95 | 96 | render() { 97 | return ( 98 |
99 | {this.state.status ? this.props.on : this.props.off} 100 |
101 | ); 102 | } 103 | } 104 | 105 | export default connect(({ auth }: ConnectState) => ({ 106 | auth, 107 | }))(RelationBtn); 108 | -------------------------------------------------------------------------------- /src/components/Buttons/UpvoteBtn.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { LikeOutlined, LikeTwoTone } from '@ant-design/icons'; 3 | import RelationBtn from './RelationBtn'; 4 | 5 | export interface UpvoteBtnProps { 6 | relation: string; 7 | item: any; 8 | hideText?: boolean; 9 | onAfterToggle?: () => void; 10 | } 11 | 12 | export interface UpvoteBtnState { 13 | upVotersCount: number; 14 | } 15 | 16 | class UpvoteBtn extends React.Component { 17 | constructor(props: UpvoteBtnProps) { 18 | super(props); 19 | 20 | const { item } = props; 21 | 22 | this.state = { 23 | upVotersCount: item.cache?.up_voters_count || 0, 24 | }; 25 | } 26 | 27 | componentWillReceiveProps(nextProps: Readonly) { 28 | this.setState({ upVotersCount: nextProps.item.cache?.up_voters_count || 0 }); 29 | } 30 | 31 | onAfterToggle = (status: boolean) => { 32 | let { upVotersCount } = this.state; 33 | status ? upVotersCount++ : upVotersCount--; 34 | this.setState({ upVotersCount }); 35 | 36 | if (this.props.onAfterToggle) { 37 | this.props.onAfterToggle(); 38 | } 39 | }; 40 | 41 | getChildren = (slot: 'on' | 'off') => ( 42 |
43 | {slot === 'on' ? : } 44 | {!this.props.hideText && {this.state.upVotersCount}} 45 |
46 | ); 47 | 48 | render() { 49 | const { relation, item } = this.props; 50 | 51 | return ( 52 | 60 | ); 61 | } 62 | } 63 | 64 | export default UpvoteBtn; 65 | -------------------------------------------------------------------------------- /src/components/Ellipsis/demo/line.md: -------------------------------------------------------------------------------- 1 | --- 2 | order: 1 3 | title: 4 | zh-CN: 按照行数省略 5 | en-US: Truncate according to the number of rows 6 | --- 7 | 8 | ## zh-CN 9 | 10 | 通过设置 `lines` 属性指定最大行数,如果超过这个行数的文本会自动截取。但是在这种模式下所有 `children` 将会被转换成纯文本。 11 | 12 | 并且注意在这种模式下,外容器需要有指定的宽度(或设置自身宽度)。 13 | 14 | ## en-US 15 | 16 | `lines` attribute specifies the maximum number of rows where the text will automatically be truncated when exceeded. In this mode, all children will be converted to plain text. 17 | 18 | Also note that, in this mode, the outer container needs to have a specified width (or set its own width). 19 | 20 | ```jsx 21 | import Ellipsis from 'ant-design-pro/lib/Ellipsis'; 22 | 23 | const article = ( 24 |

25 | There were injuries alleged in three cases in 2015, and a fourth incident 26 | in September, according to the safety recall report. After meeting with US regulators in 27 | October, the firm decided to issue a voluntary recall. 28 |

29 | ); 30 | 31 | ReactDOM.render( 32 |
33 | 34 | {article} 35 | 36 |
, 37 | mountNode, 38 | ); 39 | ``` 40 | -------------------------------------------------------------------------------- /src/components/Ellipsis/demo/number.md: -------------------------------------------------------------------------------- 1 | --- 2 | order: 0 3 | title: 4 | zh-CN: 按照字符数省略 5 | en-US: Truncate according to the number of character 6 | --- 7 | 8 | ## zh-CN 9 | 10 | 通过设置 `length` 属性指定文本最长长度,如果超过这个长度会自动截取。 11 | 12 | ## en-US 13 | 14 | `length` attribute specifies the maximum length where the text will automatically be truncated when exceeded. 15 | 16 | ```jsx 17 | import Ellipsis from 'ant-design-pro/lib/Ellipsis'; 18 | 19 | const article = 20 | 'There were injuries alleged in three cases in 2015, and a fourth incident in September, according to the safety recall report. After meeting with US regulators in October, the firm decided to issue a voluntary recall.'; 21 | 22 | ReactDOM.render( 23 |
24 | {article} 25 |

Show Tooltip

26 | 27 | {article} 28 | 29 |
, 30 | mountNode, 31 | ); 32 | ``` 33 | -------------------------------------------------------------------------------- /src/components/Ellipsis/index.d.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { TooltipProps } from 'antd/lib/tooltip'; 3 | 4 | export interface IEllipsisTooltipProps extends TooltipProps { 5 | title?: undefined; 6 | overlayStyle?: undefined; 7 | } 8 | 9 | export interface IEllipsisProps { 10 | tooltip?: boolean | IEllipsisTooltipProps; 11 | length?: number; 12 | lines?: number; 13 | style?: React.CSSProperties; 14 | className?: string; 15 | fullWidthRecognition?: boolean; 16 | } 17 | 18 | export function getStrFullLength(str: string): number; 19 | export function cutStrByFullLength(str: string, maxLength: number): string; 20 | 21 | /* eslint react/prefer-stateless-function:0 */ 22 | export default class Ellipsis extends React.Component {} 23 | -------------------------------------------------------------------------------- /src/components/Ellipsis/index.en-US.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Ellipsis 3 | cols: 1 4 | order: 10 5 | --- 6 | 7 | When the text is too long, the Ellipsis automatically shortens it according to its length or the maximum number of lines. 8 | 9 | ## API 10 | 11 | | Property | Description | Type | Default | 12 | | --- | --- | --- | --- | 13 | | tooltip | tooltip for showing the full text content when hovering over | boolean | - | 14 | | length | maximum number of characters in the text before being truncated | number | - | 15 | | lines | maximum number of rows in the text before being truncated | number | `1` | 16 | | fullWidthRecognition | whether consider full-width character length as 2 when calculate string length | boolean | - | 17 | -------------------------------------------------------------------------------- /src/components/Ellipsis/index.less: -------------------------------------------------------------------------------- 1 | .ellipsis { 2 | display: inline-block; 3 | width: 100%; 4 | overflow: hidden; 5 | word-break: break-all; 6 | } 7 | 8 | .lines { 9 | position: relative; 10 | .shadow { 11 | position: absolute; 12 | z-index: -999; 13 | display: block; 14 | color: transparent; 15 | opacity: 0; 16 | } 17 | } 18 | 19 | .lineClamp { 20 | position: relative; 21 | display: -webkit-box; 22 | overflow: hidden; 23 | text-overflow: ellipsis; 24 | } 25 | -------------------------------------------------------------------------------- /src/components/Ellipsis/index.test.js: -------------------------------------------------------------------------------- 1 | import { getStrFullLength, cutStrByFullLength } from './index'; 2 | 3 | describe('test calculateShowLength', () => { 4 | it('get full length', () => { 5 | expect(getStrFullLength('一二,a,')).toEqual(8); 6 | }); 7 | it('cut str by full length', () => { 8 | expect(cutStrByFullLength('一二,a,', 7)).toEqual('一二,a'); 9 | }); 10 | it('cut str when length small', () => { 11 | expect(cutStrByFullLength('一22三', 5)).toEqual('一22'); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/components/Ellipsis/index.zh-CN.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Ellipsis 3 | subtitle: 文本自动省略号 4 | cols: 1 5 | order: 10 6 | --- 7 | 8 | 文本过长自动处理省略号,支持按照文本长度和最大行数两种方式截取。 9 | 10 | ## API 11 | 12 | | 参数 | 说明 | 类型 | 默认值 | 13 | | -------------------- | ------------------------------------------------ | ------- | ------ | 14 | | tooltip | 移动到文本展示完整内容的提示 | boolean | - | 15 | | length | 在按照长度截取下的文本最大字符数,超过则截取省略 | number | - | 16 | | lines | 在按照行数截取下最大的行数,超过则截取省略 | number | `1` | 17 | | fullWidthRecognition | 是否将全角字符的长度视为 2 来计算字符串长度 | boolean | - | 18 | -------------------------------------------------------------------------------- /src/components/GlobalHeader/AvatarDropdown.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { history, connect } from 'umi'; 3 | import { Avatar, Menu } from 'antd'; 4 | import { BugOutlined, UserOutlined, SettingOutlined, LogoutOutlined } from '@ant-design/icons'; 5 | import { stringify } from 'qs'; 6 | import { ClickParam } from 'antd/es/menu'; 7 | import { getToken } from '@/utils/authority'; 8 | import { getPageQuery } from '@/utils/utils'; 9 | import { ConnectState, ConnectProps, AuthModelState } from '@/models/connect'; 10 | import HeaderDropdown from '../HeaderDropdown'; 11 | import styles from './index.less'; 12 | 13 | export interface GlobalHeaderRightProps extends Partial { 14 | auth: AuthModelState; 15 | } 16 | 17 | class AvatarDropdown extends React.Component { 18 | onMenuClick = (event: ClickParam) => { 19 | const { key } = event; 20 | const { dispatch } = this.props; 21 | if (!dispatch) return; 22 | 23 | switch (key) { 24 | case 'logout': 25 | return dispatch({ 26 | type: 'auth/logout', 27 | }); 28 | case 'telescope': 29 | window.open(`/api/web/login?_token=${getToken()}`); 30 | return; 31 | default: 32 | break; 33 | } 34 | 35 | history.push(`/account/${key}`); 36 | }; 37 | 38 | onLoginClick = () => { 39 | const { redirect } = getPageQuery(); 40 | // redirect 41 | if (window.location.pathname !== '/auth/login' && !redirect) { 42 | history.push({ 43 | pathname: '/auth/login', 44 | search: stringify({ 45 | redirect: window.location.href, 46 | }), 47 | }); 48 | } 49 | }; 50 | 51 | render() { 52 | const { auth } = this.props; 53 | 54 | const menuHeaderDropdown = ( 55 | 56 | {auth && auth.user.id === 1 && ( 57 | 58 | 59 | 调试工具 60 | 61 | )} 62 | 63 | 64 | 个人中心 65 | 66 | 67 | 68 | 个人设置 69 | 70 | 71 | 72 | 73 | 退出登录 74 | 75 | 76 | ); 77 | 78 | return auth.logged ? ( 79 | 80 | 81 | 82 | {auth.user.username} 83 | 84 | 85 | ) : ( 86 | 87 | } alt="登录" size="small" /> 88 | 89 | 账户中心 90 | 91 | 92 | ); 93 | } 94 | } 95 | 96 | export default connect(({ auth }: ConnectState) => ({ 97 | auth, 98 | }))(AvatarDropdown); 99 | -------------------------------------------------------------------------------- /src/components/GlobalHeader/NoticeIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect, Link } from 'umi'; 3 | import { Badge } from 'antd'; 4 | import { BellOutlined } from '@ant-design/icons'; 5 | import { ConnectState, AuthModelState } from '@/models/connect'; 6 | import styles from './index.less'; 7 | 8 | export interface NoticeIconProps { 9 | auth: AuthModelState; 10 | } 11 | 12 | const NoticeIcon: React.FC = (props) => { 13 | const { auth } = props; 14 | if (auth.logged) { 15 | return ( 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | } 23 | 24 | return null; 25 | }; 26 | 27 | export default connect(({ auth }: ConnectState) => ({ 28 | auth, 29 | }))(NoticeIcon); 30 | -------------------------------------------------------------------------------- /src/components/GlobalHeader/RightContent.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { connect, history, useRequest } from 'umi'; 3 | import { GithubOutlined } from '@ant-design/icons'; 4 | import { ConnectState, ConnectProps } from '@/models/connect'; 5 | import { ResponseResultType } from '@/models/I'; 6 | import * as services from '@/services'; 7 | import NoticeIcon from './NoticeIcon'; 8 | import Avatar from './AvatarDropdown'; 9 | import HeaderSearch from '../HeaderSearch'; 10 | import styles from './index.less'; 11 | 12 | export type SiderTheme = 'light' | 'dark' | 'realDark'; 13 | 14 | export interface GlobalHeaderRightProps extends Partial { 15 | theme?: SiderTheme; 16 | layout: 'sidemenu' | 'topmenu'; 17 | } 18 | 19 | const GlobalHeaderRight: React.FC = (props) => { 20 | const { data: keywords = [], run: getHotKeywords } = useRequest>( 21 | services.getHotKeywords, 22 | { 23 | manual: true, 24 | throttleInterval: 3600000, 25 | }, 26 | ); 27 | 28 | useEffect(() => { 29 | getHotKeywords(); 30 | }, []); 31 | 32 | const { theme, layout } = props; 33 | let className = styles.right; 34 | 35 | if (theme === 'dark' && layout === 'topmenu') { 36 | className = `${styles.right} ${styles.dark}`; 37 | } 38 | 39 | return ( 40 |
41 | b && getHotKeywords()} 45 | onSearch={(value) => { 46 | history.push({ 47 | pathname: '/articles/list', 48 | query: { 49 | q: value, 50 | }, 51 | }); 52 | }} 53 | options={keywords.map((keyword) => ({ label: keyword, value: keyword }))} 54 | /> 55 | 56 | 57 | 63 | 64 | 65 |
66 | ); 67 | }; 68 | 69 | export default connect(({ settings }: ConnectState) => ({ 70 | theme: settings.navTheme, 71 | layout: settings.layout, 72 | }))(GlobalHeaderRight); 73 | -------------------------------------------------------------------------------- /src/components/GlobalHeader/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | @pro-header-hover-bg: rgba(0, 0, 0, 0.025); 4 | 5 | .menu { 6 | :global(.anticon) { 7 | margin-right: 8px; 8 | } 9 | :global(.ant-dropdown-menu-item) { 10 | min-width: 160px; 11 | } 12 | } 13 | 14 | .right { 15 | display: flex; 16 | float: right; 17 | height: @layout-header-height; 18 | margin-left: auto; 19 | overflow: hidden; 20 | .action { 21 | display: flex; 22 | align-items: center; 23 | height: 100%; 24 | padding: 0 12px; 25 | cursor: pointer; 26 | transition: all 0.3s; 27 | > span { 28 | color: @text-color; 29 | vertical-align: middle; 30 | } 31 | &:hover { 32 | background: @pro-header-hover-bg; 33 | } 34 | &:global(.opened) { 35 | background: @pro-header-hover-bg; 36 | } 37 | } 38 | .search { 39 | padding: 0 12px; 40 | &:hover { 41 | background: transparent; 42 | } 43 | } 44 | .account { 45 | .avatar { 46 | margin: ~'calc((@{layout-header-height} - 24px) / 2)' 0; 47 | margin-right: 8px; 48 | color: @primary-color; 49 | vertical-align: top; 50 | background: rgba(255, 255, 255, 0.85); 51 | } 52 | } 53 | } 54 | 55 | .dark { 56 | .action { 57 | color: rgba(255, 255, 255, 0.85); 58 | > span { 59 | color: rgba(255, 255, 255, 0.85); 60 | } 61 | &:hover, 62 | &:global(.opened) { 63 | background: @primary-color; 64 | } 65 | } 66 | } 67 | 68 | :global(.ant-pro-global-header) { 69 | .dark { 70 | .action { 71 | color: @text-color; 72 | > span { 73 | color: @text-color; 74 | } 75 | &:hover { 76 | color: rgba(255, 255, 255, 0.85); 77 | > span { 78 | color: rgba(255, 255, 255, 0.85); 79 | } 80 | } 81 | } 82 | } 83 | } 84 | 85 | @media only screen and (max-width: @screen-md) { 86 | :global(.ant-divider-vertical) { 87 | vertical-align: unset; 88 | } 89 | .name { 90 | display: none; 91 | } 92 | .right { 93 | position: absolute; 94 | top: 0; 95 | right: 12px; 96 | .account { 97 | .avatar { 98 | margin-right: 0; 99 | } 100 | } 101 | .search { 102 | display: none; 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/components/HeaderDropdown/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .container > * { 4 | background-color: @popover-bg; 5 | border-radius: 4px; 6 | box-shadow: @shadow-1-down; 7 | } 8 | 9 | @media screen and (max-width: @screen-xs) { 10 | .container { 11 | width: 100% !important; 12 | } 13 | .container > * { 14 | border-radius: 0 !important; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/components/HeaderDropdown/index.tsx: -------------------------------------------------------------------------------- 1 | import { DropDownProps } from 'antd/es/dropdown'; 2 | import { Dropdown } from 'antd'; 3 | import React from 'react'; 4 | import classNames from 'classnames'; 5 | import styles from './index.less'; 6 | 7 | declare type OverlayFunc = () => React.ReactNode; 8 | 9 | export interface HeaderDropdownProps extends Omit { 10 | overlayClassName?: string; 11 | overlay: React.ReactNode | OverlayFunc | any; 12 | placement?: 'bottomLeft' | 'bottomRight' | 'topLeft' | 'topCenter' | 'topRight' | 'bottomCenter'; 13 | } 14 | 15 | const HeaderDropdown: React.FC = ({ overlayClassName: cls, ...restProps }) => ( 16 | 17 | ); 18 | 19 | export default HeaderDropdown; 20 | -------------------------------------------------------------------------------- /src/components/HeaderSearch/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .headerSearch { 4 | :global(.anticon-search) { 5 | font-size: 16px; 6 | cursor: pointer; 7 | } 8 | .input { 9 | width: 0; 10 | background: transparent; 11 | border-radius: 0; 12 | transition: width 0.3s, margin-left 0.3s; 13 | :global(.ant-select-selection) { 14 | background: transparent; 15 | } 16 | input { 17 | padding-right: 0; 18 | padding-left: 0; 19 | border: 0; 20 | box-shadow: none !important; 21 | } 22 | &, 23 | &:hover, 24 | &:focus { 25 | border-bottom: 1px solid @border-color-base; 26 | } 27 | &.show { 28 | width: 210px; 29 | margin-left: 8px; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/components/HeaderSearch/index.tsx: -------------------------------------------------------------------------------- 1 | import { SearchOutlined } from '@ant-design/icons'; 2 | import { AutoComplete, Input } from 'antd'; 3 | import useMergeValue from 'use-merge-value'; 4 | import { AutoCompleteProps } from 'antd/es/auto-complete'; 5 | import React, { useRef } from 'react'; 6 | 7 | import classNames from 'classnames'; 8 | import styles from './index.less'; 9 | 10 | export interface HeaderSearchProps { 11 | onSearch?: (value?: string) => void; 12 | onChange?: (value?: string) => void; 13 | onVisibleChange?: (b: boolean) => void; 14 | className?: string; 15 | placeholder?: string; 16 | options: AutoCompleteProps['options']; 17 | defaultOpen?: boolean; 18 | open?: boolean; 19 | defaultValue?: string; 20 | value?: string; 21 | } 22 | 23 | const HeaderSearch: React.FC = (props) => { 24 | const { 25 | className, 26 | defaultValue, 27 | onVisibleChange, 28 | placeholder, 29 | open, 30 | defaultOpen, 31 | ...restProps 32 | } = props; 33 | 34 | const inputRef = useRef(null); 35 | 36 | const [value, setValue] = useMergeValue(defaultValue, { 37 | value: props.value, 38 | onChange: props.onChange, 39 | }); 40 | 41 | const [searchMode, setSearchMode] = useMergeValue(defaultOpen || false, { 42 | value: props.open, 43 | onChange: onVisibleChange, 44 | }); 45 | 46 | const inputClass = classNames(styles.input, { 47 | [styles.show]: searchMode, 48 | }); 49 | 50 | return ( 51 |
{ 54 | setSearchMode(true); 55 | if (inputRef.current) { 56 | inputRef.current.focus(); 57 | } 58 | }} 59 | onTransitionEnd={({ propertyName }) => { 60 | if (propertyName === 'width' && !searchMode) { 61 | if (onVisibleChange) { 62 | onVisibleChange(searchMode); 63 | } 64 | } 65 | }} 66 | > 67 | 73 | 84 | { 90 | if (e.key === 'Enter') { 91 | if (restProps.onSearch) { 92 | restProps.onSearch(value); 93 | } 94 | } 95 | }} 96 | onBlur={() => { 97 | setSearchMode(false); 98 | }} 99 | /> 100 | 101 |
102 | ); 103 | }; 104 | 105 | export default HeaderSearch; 106 | -------------------------------------------------------------------------------- /src/components/MarkdownBody/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from 'react'; 2 | import marked from 'marked'; 3 | import DOMPurify from 'dompurify'; 4 | // @ts-ignore 5 | import emojiToolkit from 'emoji-toolkit'; 6 | import Prism from 'prismjs'; 7 | import { getDefaultMarkedOptions, resetMarkedOptions } from '@/utils/utils'; 8 | import Tocify from './tocify'; 9 | 10 | export interface MarkdownBodyProps { 11 | markdown: string; 12 | prismPlugin?: boolean; 13 | toc?: boolean; 14 | getTocify?: (tocify: Tocify) => void; 15 | } 16 | 17 | const tocify = new Tocify(); 18 | 19 | const MarkdownBody: React.FC = (props) => { 20 | const markdownRef = useRef(null); 21 | 22 | const runPlugin = async () => { 23 | // https://webpack.docschina.org/guides/code-splitting/#%E5%8A%A8%E6%80%81%E5%AF%BC%E5%85%A5-dynamic-imports- 24 | const [{ default: jQuery }, { debounce, throttle }]: any = await Promise.all([ 25 | import(/* webpackChunkName: 'jquery' */ 'jquery'), 26 | // @ts-ignore 27 | import(/* webpackChunkName: 'throttle-debounce' */ 'throttle-debounce'), 28 | ]); 29 | 30 | jQuery.debounce = debounce; 31 | jQuery.throttle = throttle; 32 | window.jQuery = jQuery; 33 | 34 | await Promise.all([ 35 | // @ts-ignore 36 | import(/* webpackChunkName: 'fluidbox' */ 'fluidbox'), 37 | import(/* webpackChunkName: 'fluidbox' */ 'fluidbox/dist/css/fluidbox.min.css'), 38 | ]); 39 | 40 | jQuery(markdownRef.current) 41 | .find('img:not(.joypixels)') 42 | .each(function () { 43 | // @ts-ignore 44 | jQuery(this).wrap(``); 45 | }) 46 | .promise() 47 | .done(() => jQuery(markdownRef.current).find('a.fluidbox').fluidbox()); 48 | 49 | if (props.prismPlugin) { 50 | jQuery(markdownRef.current).find('pre').addClass('line-numbers'); 51 | Prism.highlightAllUnder(markdownRef.current as any); 52 | } 53 | }; 54 | 55 | useEffect(() => { 56 | resetMarkedOptions(); 57 | 58 | if (props.toc && props.getTocify) { 59 | props.getTocify(tocify); 60 | } 61 | 62 | runPlugin(); 63 | }, []); 64 | 65 | const createMarkup = () => { 66 | if (props.toc) { 67 | tocify.reset(); 68 | 69 | const { renderer, ...otherOptions } = getDefaultMarkedOptions(); 70 | renderer.heading = (text, level) => { 71 | const anchor = tocify.add(text, level); 72 | return `${text}\n`; 73 | }; 74 | 75 | marked.setOptions({ renderer, ...otherOptions }); 76 | } 77 | 78 | const markup = DOMPurify.sanitize(emojiToolkit.toImage(marked(props.markdown))); 79 | 80 | resetMarkedOptions(); 81 | 82 | return { __html: markup }; 83 | }; 84 | 85 | return ( 86 |
87 | ); 88 | }; 89 | 90 | export default MarkdownBody; 91 | -------------------------------------------------------------------------------- /src/components/MarkdownBody/tocify.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Anchor } from 'antd'; 3 | import { last } from 'lodash'; 4 | import escape from 'escape-string-regexp'; 5 | 6 | const { Link } = Anchor; 7 | 8 | export interface TocItem { 9 | anchor: string; 10 | level: number; 11 | text: string; 12 | children?: TocItem[]; 13 | } 14 | 15 | export type TocItems = TocItem[]; 16 | 17 | export default class Tocify { 18 | anchors: string[]; 19 | 20 | tocItems: TocItems = []; 21 | 22 | constructor() { 23 | this.anchors = []; 24 | this.tocItems = []; 25 | } 26 | 27 | add(text: string, level: number, id: string = '') { 28 | let anchor = encodeURIComponent(id || text); 29 | const count = this.anchors.filter((value) => 30 | new RegExp(`^${escape(anchor)}[0-9]*$`).test(value), 31 | ).length; 32 | if (count > 0) anchor += count; 33 | this.anchors.push(anchor); 34 | const item = { anchor, level, text }; 35 | const items = this.tocItems; 36 | 37 | if (items.length === 0) { 38 | items.push(item); 39 | } else { 40 | let lastItem = last(items) as TocItem; 41 | 42 | if (item.level > lastItem.level) { 43 | for (let i = lastItem.level + 1; i <= 6; i++) { 44 | const { children } = lastItem; 45 | if (!children) { 46 | lastItem.children = [item]; 47 | break; 48 | } 49 | 50 | lastItem = last(children) as TocItem; 51 | 52 | if (item.level <= lastItem.level) { 53 | children.push(item); 54 | break; 55 | } 56 | } 57 | } else { 58 | items.push(item); 59 | } 60 | } 61 | 62 | return anchor; 63 | } 64 | 65 | reset = () => { 66 | this.tocItems = []; 67 | this.anchors = []; 68 | }; 69 | 70 | renderToc(items: TocItem[]) { 71 | return items.map((item) => ( 72 | 73 | {item.children && this.renderToc(item.children)} 74 | 75 | )); 76 | } 77 | 78 | render() { 79 | return ( 80 | 81 | {this.renderToc(this.tocItems)} 82 | 83 | ); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/components/ModalForm/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Form, Modal } from 'antd'; 3 | import { IRole } from '@/models/I'; 4 | import { ModalProps } from 'antd/es/modal'; 5 | import { FormInstance } from 'antd/es/form'; 6 | 7 | export interface ModalFormProps extends ModalProps { 8 | onSubmit: (values: object) => void; 9 | submitting: boolean; 10 | initialValues?: { [key: string]: any }; 11 | form?: FormInstance; 12 | } 13 | 14 | const ModalForm: React.FC = (props) => { 15 | const [form] = Form.useForm(props.form); 16 | 17 | const { title, visible, onSubmit, onCancel, submitting, ...modalProps } = props; 18 | 19 | async function handleOK() { 20 | const values = await form.validateFields(); 21 | await props.onSubmit(values as IRole); 22 | form.resetFields(); 23 | } 24 | 25 | return ( 26 | 35 |
36 | {props.children} 37 |
38 |
39 | ); 40 | }; 41 | 42 | export default ModalForm; 43 | -------------------------------------------------------------------------------- /src/components/PageLoading/index.tsx: -------------------------------------------------------------------------------- 1 | import { PageLoading } from '@ant-design/pro-layout'; 2 | 3 | // loading components from code split 4 | // https://umijs.org/plugin/umi-plugin-react.html#dynamicimport 5 | export default PageLoading; 6 | -------------------------------------------------------------------------------- /src/e2e/__mocks__/antd-pro-merge-less.js: -------------------------------------------------------------------------------- 1 | export default undefined; 2 | -------------------------------------------------------------------------------- /src/e2e/baseLayout.e2e.js: -------------------------------------------------------------------------------- 1 | const { uniq } = require('lodash'); 2 | const RouterConfig = require('../../config/config').default.routes; 3 | 4 | const BASE_URL = `http://localhost:${process.env.PORT || 8000}`; 5 | 6 | function formatter(routes, parentPath = '') { 7 | const fixedParentPath = parentPath.replace(/\/{1,}/g, '/'); 8 | let result = []; 9 | routes.forEach(item => { 10 | if (item.path) { 11 | result.push(`${fixedParentPath}/${item.path}`.replace(/\/{1,}/g, '/')); 12 | } 13 | if (item.routes) { 14 | result = result.concat( 15 | formatter(item.routes, item.path ? `${fixedParentPath}/${item.path}` : parentPath), 16 | ); 17 | } 18 | }); 19 | return uniq(result.filter(item => !!item)); 20 | } 21 | 22 | describe('Ant Design Pro E2E test', () => { 23 | const testPage = path => async () => { 24 | await page.goto(`${BASE_URL}${path}`); 25 | await page.waitForSelector('footer', { 26 | timeout: 2000, 27 | }); 28 | const haveFooter = await page.evaluate( 29 | () => document.getElementsByTagName('footer').length > 0, 30 | ); 31 | expect(haveFooter).toBeTruthy(); 32 | }; 33 | 34 | const routers = formatter(RouterConfig); 35 | routers.forEach(route => { 36 | it(`test pages ${route}`, testPage(route)); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /src/e2e/topMenu.e2e.js: -------------------------------------------------------------------------------- 1 | const BASE_URL = `http://localhost:${process.env.PORT || 8000}`; 2 | 3 | describe('Homepage', () => { 4 | it('topmenu should have footer', async () => { 5 | const params = '/form/basic-form?navTheme=light&layout=topmenu'; 6 | await page.goto(`${BASE_URL}${params}`); 7 | await page.waitForSelector('footer', { 8 | timeout: 2000, 9 | }); 10 | const haveFooter = await page.evaluate( 11 | () => document.getElementsByTagName('footer').length > 0, 12 | ); 13 | expect(haveFooter).toBeTruthy(); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/global.tsx: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | import 'moment/locale/zh-cn'; 3 | // @ts-ignore 4 | import emojiToolkit from 'emoji-toolkit'; 5 | import { resetMarkedOptions } from '@/utils/utils'; 6 | 7 | moment.locale('zh-en'); 8 | 9 | emojiToolkit.sprites = true; 10 | emojiToolkit.spriteSize = 32; 11 | resetMarkedOptions(); 12 | 13 | if (window.location.hostname === 'www.einsition.com') { 14 | // 百度统计 15 | (() => { 16 | const hm = document.createElement('script'); 17 | hm.src = 'https://hm.baidu.com/hm.js?ac1bc08008f195f8b3c753b4b718104b'; 18 | document.head.appendChild(hm); 19 | })(); 20 | } 21 | -------------------------------------------------------------------------------- /src/layouts/AuthLayout.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .container { 4 | display: flex; 5 | flex-direction: column; 6 | height: 100vh; 7 | overflow: auto; 8 | background: @layout-body-background; 9 | } 10 | 11 | .lang { 12 | width: 100%; 13 | height: 40px; 14 | line-height: 44px; 15 | text-align: right; 16 | :global(.ant-dropdown-trigger) { 17 | margin-right: 24px; 18 | } 19 | } 20 | 21 | .content { 22 | flex: 1; 23 | padding: 32px 0; 24 | } 25 | 26 | @media (min-width: @screen-md-min) { 27 | .container { 28 | background-image: url('https://gw.alipayobjects.com/zos/rmsportal/TVYTbAXWheQpRcWDaDMu.svg'); 29 | background-repeat: no-repeat; 30 | background-position: center 110px; 31 | background-size: 100%; 32 | } 33 | 34 | .content { 35 | padding: 32px 0 24px; 36 | } 37 | } 38 | 39 | .top { 40 | text-align: center; 41 | } 42 | 43 | .header { 44 | height: 44px; 45 | line-height: 44px; 46 | a { 47 | text-decoration: none; 48 | } 49 | } 50 | 51 | .logo { 52 | height: 44px; 53 | margin-right: 16px; 54 | vertical-align: top; 55 | } 56 | 57 | .title { 58 | position: relative; 59 | top: 2px; 60 | color: @heading-color; 61 | font-weight: 600; 62 | font-size: 33px; 63 | font-family: Avenir, 'Helvetica Neue', Arial, Helvetica, sans-serif; 64 | } 65 | 66 | .desc { 67 | margin-top: 12px; 68 | margin-bottom: 40px; 69 | color: @text-color-secondary; 70 | font-size: @font-size-base; 71 | } 72 | -------------------------------------------------------------------------------- /src/layouts/AuthLayout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { getMenuData, getPageTitle, DefaultFooter } from '@ant-design/pro-layout'; 3 | import { Helmet, HelmetProvider } from 'react-helmet-async'; 4 | import { connect } from 'umi'; 5 | import { GithubOutlined } from '@ant-design/icons'; 6 | import { ConnectProps, ConnectState } from '@/models/connect'; 7 | import styles from './AuthLayout.less'; 8 | 9 | interface AuthLayoutProps extends ConnectProps {} 10 | 11 | const AuthLayout: React.FC = (props) => { 12 | const { route = { routes: [] } } = props; 13 | const { routes = [] } = route; 14 | const { children, location = { pathname: '' } } = props; 15 | const { breadcrumb } = getMenuData(routes); 16 | 17 | const title = getPageTitle({ 18 | pathname: location.pathname, 19 | breadcrumb, 20 | ...props, 21 | }); 22 | 23 | const links = [ 24 | { 25 | key: 'Ant Design Pro', 26 | title: 'Ant Design Pro', 27 | href: 'https://pro.ant.design', 28 | blankTarget: true, 29 | }, 30 | { 31 | key: 'github', 32 | title: , 33 | href: 'https://github.com/yanthink/blog-v2', 34 | blankTarget: true, 35 | }, 36 | { 37 | key: 'Ant Design', 38 | title: 'Ant Design', 39 | href: 'https://ant.design', 40 | blankTarget: true, 41 | }, 42 | ]; 43 | 44 | const copyright = '2019 平凡的博客 粤ICP备18080782号-1'; 45 | 46 | return ( 47 | 48 | 49 | {title} 50 | 51 | 52 | 53 |
54 |
{children}
55 | 56 |
57 |
58 | ); 59 | }; 60 | 61 | export default connect(({ settings }: ConnectState) => ({ 62 | ...settings, 63 | }))(AuthLayout); 64 | -------------------------------------------------------------------------------- /src/layouts/BlankLayout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Layout: React.FC = ({ children }) =>
{children}
; 4 | 5 | export default Layout; 6 | -------------------------------------------------------------------------------- /src/layouts/SecurityLayout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { PageLoading } from '@ant-design/pro-layout'; 3 | import { connect } from 'umi'; 4 | import { ConnectState, ConnectProps, AuthModelState } from '@/models/connect'; 5 | 6 | interface SecurityLayoutProps extends ConnectProps { 7 | auth: AuthModelState; 8 | loading: boolean; 9 | } 10 | 11 | interface SecurityLayoutState { 12 | isReady: boolean; 13 | } 14 | 15 | class SecurityLayout extends React.Component { 16 | state: SecurityLayoutState = { 17 | isReady: false, 18 | }; 19 | 20 | componentDidMount() { 21 | this.setState({ 22 | isReady: true, 23 | }); 24 | 25 | const { dispatch } = this.props; 26 | 27 | if (dispatch) { 28 | dispatch({ type: 'auth/loadUser' }); 29 | } 30 | } 31 | 32 | render() { 33 | const { isReady } = this.state; 34 | const { children, loading, auth } = this.props; 35 | 36 | if ((!auth.logged && loading) || !isReady) { 37 | return ; 38 | } 39 | 40 | return children; 41 | } 42 | } 43 | 44 | export default connect(({ auth, loading }: ConnectState) => ({ 45 | auth, 46 | loading: loading.effects['auth/loadUser'], 47 | }))(SecurityLayout); 48 | -------------------------------------------------------------------------------- /src/locales/en-US.ts: -------------------------------------------------------------------------------- 1 | import component from './en-US/component'; 2 | import globalHeader from './en-US/globalHeader'; 3 | import menu from './en-US/menu'; 4 | import pwa from './en-US/pwa'; 5 | import settingDrawer from './en-US/settingDrawer'; 6 | import settings from './en-US/settings'; 7 | 8 | export default { 9 | 'navBar.lang': 'Languages', 10 | 'layout.user.link.help': 'Help', 11 | 'layout.user.link.privacy': 'Privacy', 12 | 'layout.user.link.terms': 'Terms', 13 | 'app.preview.down.block': 'Download this page to your local project', 14 | 'app.welcome.link.fetch-blocks': 'Get all block', 15 | 'app.welcome.link.block-list': 'Quickly build standard, pages based on `block` development', 16 | ...globalHeader, 17 | ...menu, 18 | ...settingDrawer, 19 | ...settings, 20 | ...pwa, 21 | ...component, 22 | }; 23 | -------------------------------------------------------------------------------- /src/locales/en-US/component.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.tagSelect.expand': 'Expand', 3 | 'component.tagSelect.collapse': 'Collapse', 4 | 'component.tagSelect.all': 'All', 5 | }; 6 | -------------------------------------------------------------------------------- /src/locales/en-US/globalHeader.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.globalHeader.search': 'Search', 3 | 'component.globalHeader.search.example1': 'Search example 1', 4 | 'component.globalHeader.search.example2': 'Search example 2', 5 | 'component.globalHeader.search.example3': 'Search example 3', 6 | 'component.globalHeader.help': 'Help', 7 | 'component.globalHeader.notification': 'Notification', 8 | 'component.globalHeader.notification.empty': 'You have viewed all notifications.', 9 | 'component.globalHeader.message': 'Message', 10 | 'component.globalHeader.message.empty': 'You have viewed all messsages.', 11 | 'component.globalHeader.event': 'Event', 12 | 'component.globalHeader.event.empty': 'You have viewed all events.', 13 | 'component.noticeIcon.clear': 'Clear', 14 | 'component.noticeIcon.cleared': 'Cleared', 15 | 'component.noticeIcon.empty': 'No notifications', 16 | 'component.noticeIcon.view-more': 'View more', 17 | }; 18 | -------------------------------------------------------------------------------- /src/locales/en-US/menu.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.welcome': 'Welcome', 3 | 'menu.more-blocks': 'More Blocks', 4 | 'menu.home': 'Home', 5 | 'menu.admin': 'admin', 6 | 'menu.login': 'Login', 7 | 'menu.register': 'Register', 8 | 'menu.register.result': 'Register Result', 9 | 'menu.dashboard': 'Dashboard', 10 | 'menu.dashboard.analysis': 'Analysis', 11 | 'menu.dashboard.monitor': 'Monitor', 12 | 'menu.dashboard.workplace': 'Workplace', 13 | 'menu.exception.403': '403', 14 | 'menu.exception.404': '404', 15 | 'menu.exception.500': '500', 16 | 'menu.form': 'Form', 17 | 'menu.form.basic-form': 'Basic Form', 18 | 'menu.form.step-form': 'Step Form', 19 | 'menu.form.step-form.info': 'Step Form(write transfer information)', 20 | 'menu.form.step-form.confirm': 'Step Form(confirm transfer information)', 21 | 'menu.form.step-form.result': 'Step Form(finished)', 22 | 'menu.form.advanced-form': 'Advanced Form', 23 | 'menu.list': 'List', 24 | 'menu.list.table-list': 'Search Table', 25 | 'menu.list.basic-list': 'Basic List', 26 | 'menu.list.card-list': 'Card List', 27 | 'menu.list.search-list': 'Search List', 28 | 'menu.list.search-list.articles': 'Search List(articles)', 29 | 'menu.list.search-list.projects': 'Search List(projects)', 30 | 'menu.list.search-list.applications': 'Search List(applications)', 31 | 'menu.profile': 'Profile', 32 | 'menu.profile.basic': 'Basic Profile', 33 | 'menu.profile.advanced': 'Advanced Profile', 34 | 'menu.result': 'Result', 35 | 'menu.result.success': 'Success', 36 | 'menu.result.fail': 'Fail', 37 | 'menu.exception': 'Exception', 38 | 'menu.exception.not-permission': '403', 39 | 'menu.exception.not-find': '404', 40 | 'menu.exception.server-error': '500', 41 | 'menu.exception.trigger': 'Trigger', 42 | 'menu.account': 'Account', 43 | 'menu.account.center': 'Account Center', 44 | 'menu.account.settings': 'Account Settings', 45 | 'menu.account.trigger': 'Trigger Error', 46 | 'menu.account.logout': 'Logout', 47 | 'menu.editor': 'Graphic Editor', 48 | 'menu.editor.flow': 'Flow Editor', 49 | 'menu.editor.mind': 'Mind Editor', 50 | 'menu.editor.koni': 'Koni Editor', 51 | }; 52 | -------------------------------------------------------------------------------- /src/locales/en-US/pwa.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.pwa.offline': 'You are offline now', 3 | 'app.pwa.serviceworker.updated': 'New content is available', 4 | 'app.pwa.serviceworker.updated.hint': 'Please press the "Refresh" button to reload current page', 5 | 'app.pwa.serviceworker.updated.ok': 'Refresh', 6 | }; 7 | -------------------------------------------------------------------------------- /src/locales/en-US/settingDrawer.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.setting.pagestyle': 'Page style setting', 3 | 'app.setting.pagestyle.dark': 'Dark style', 4 | 'app.setting.pagestyle.light': 'Light style', 5 | 'app.setting.content-width': 'Content Width', 6 | 'app.setting.content-width.fixed': 'Fixed', 7 | 'app.setting.content-width.fluid': 'Fluid', 8 | 'app.setting.themecolor': 'Theme Color', 9 | 'app.setting.themecolor.dust': 'Dust Red', 10 | 'app.setting.themecolor.volcano': 'Volcano', 11 | 'app.setting.themecolor.sunset': 'Sunset Orange', 12 | 'app.setting.themecolor.cyan': 'Cyan', 13 | 'app.setting.themecolor.green': 'Polar Green', 14 | 'app.setting.themecolor.daybreak': 'Daybreak Blue (default)', 15 | 'app.setting.themecolor.geekblue': 'Geek Glue', 16 | 'app.setting.themecolor.purple': 'Golden Purple', 17 | 'app.setting.navigationmode': 'Navigation Mode', 18 | 'app.setting.sidemenu': 'Side Menu Layout', 19 | 'app.setting.topmenu': 'Top Menu Layout', 20 | 'app.setting.fixedheader': 'Fixed Header', 21 | 'app.setting.fixedsidebar': 'Fixed Sidebar', 22 | 'app.setting.fixedsidebar.hint': 'Works on Side Menu Layout', 23 | 'app.setting.hideheader': 'Hidden Header when scrolling', 24 | 'app.setting.hideheader.hint': 'Works when Hidden Header is enabled', 25 | 'app.setting.othersettings': 'Other Settings', 26 | 'app.setting.weakmode': 'Weak Mode', 27 | 'app.setting.copy': 'Copy Setting', 28 | 'app.setting.copyinfo': 'copy success,please replace defaultSettings in src/models/setting.js', 29 | 'app.setting.production.hint': 30 | 'Setting panel shows in development environment only, please manually modify', 31 | }; 32 | -------------------------------------------------------------------------------- /src/locales/pt-BR.ts: -------------------------------------------------------------------------------- 1 | import component from './pt-BR/component'; 2 | import globalHeader from './pt-BR/globalHeader'; 3 | import menu from './pt-BR/menu'; 4 | import pwa from './pt-BR/pwa'; 5 | import settingDrawer from './pt-BR/settingDrawer'; 6 | import settings from './pt-BR/settings'; 7 | 8 | export default { 9 | 'navBar.lang': 'Idiomas', 10 | 'layout.user.link.help': 'ajuda', 11 | 'layout.user.link.privacy': 'política de privacidade', 12 | 'layout.user.link.terms': 'termos de serviços', 13 | 'app.preview.down.block': 'Download this page to your local project', 14 | ...globalHeader, 15 | ...menu, 16 | ...settingDrawer, 17 | ...settings, 18 | ...pwa, 19 | ...component, 20 | }; 21 | -------------------------------------------------------------------------------- /src/locales/pt-BR/component.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.tagSelect.expand': 'Expandir', 3 | 'component.tagSelect.collapse': 'Diminuir', 4 | 'component.tagSelect.all': 'Todas', 5 | }; 6 | -------------------------------------------------------------------------------- /src/locales/pt-BR/globalHeader.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.globalHeader.search': 'Busca', 3 | 'component.globalHeader.search.example1': 'Exemplo de busca 1', 4 | 'component.globalHeader.search.example2': 'Exemplo de busca 2', 5 | 'component.globalHeader.search.example3': 'Exemplo de busca 3', 6 | 'component.globalHeader.help': 'Ajuda', 7 | 'component.globalHeader.notification': 'Notificação', 8 | 'component.globalHeader.notification.empty': 'Você visualizou todas as notificações.', 9 | 'component.globalHeader.message': 'Mensagem', 10 | 'component.globalHeader.message.empty': 'Você visualizou todas as mensagens.', 11 | 'component.globalHeader.event': 'Evento', 12 | 'component.globalHeader.event.empty': 'Você visualizou todos os eventos.', 13 | 'component.noticeIcon.clear': 'Limpar', 14 | 'component.noticeIcon.cleared': 'Limpo', 15 | 'component.noticeIcon.empty': 'Sem notificações', 16 | 'component.noticeIcon.loaded': 'Carregado', 17 | 'component.noticeIcon.view-more': 'Veja mais', 18 | }; 19 | -------------------------------------------------------------------------------- /src/locales/pt-BR/menu.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.welcome': 'Welcome', 3 | 'menu.more-blocks': 'More Blocks', 4 | 'menu.home': 'Início', 5 | 'menu.login': 'Login', 6 | 'menu.admin': 'admin', 7 | 'menu.register': 'Registro', 8 | 'menu.register.result': 'Resultado de registro', 9 | 'menu.dashboard': 'Dashboard', 10 | 'menu.dashboard.analysis': 'Análise', 11 | 'menu.dashboard.monitor': 'Monitor', 12 | 'menu.dashboard.workplace': 'Ambiente de Trabalho', 13 | 'menu.exception.403': '403', 14 | 'menu.exception.404': '404', 15 | 'menu.exception.500': '500', 16 | 'menu.form': 'Formulário', 17 | 'menu.form.basic-form': 'Formulário Básico', 18 | 'menu.form.step-form': 'Formulário Assistido', 19 | 'menu.form.step-form.info': 'Formulário Assistido(gravar informações de transferência)', 20 | 'menu.form.step-form.confirm': 'Formulário Assistido(confirmar informações de transferência)', 21 | 'menu.form.step-form.result': 'Formulário Assistido(finalizado)', 22 | 'menu.form.advanced-form': 'Formulário Avançado', 23 | 'menu.list': 'Lista', 24 | 'menu.list.table-list': 'Tabela de Busca', 25 | 'menu.list.basic-list': 'Lista Básica', 26 | 'menu.list.card-list': 'Lista de Card', 27 | 'menu.list.search-list': 'Lista de Busca', 28 | 'menu.list.search-list.articles': 'Lista de Busca(artigos)', 29 | 'menu.list.search-list.projects': 'Lista de Busca(projetos)', 30 | 'menu.list.search-list.applications': 'Lista de Busca(aplicações)', 31 | 'menu.profile': 'Perfil', 32 | 'menu.profile.basic': 'Perfil Básico', 33 | 'menu.profile.advanced': 'Perfil Avançado', 34 | 'menu.result': 'Resultado', 35 | 'menu.result.success': 'Sucesso', 36 | 'menu.result.fail': 'Falha', 37 | 'menu.exception': 'Exceção', 38 | 'menu.exception.not-permission': '403', 39 | 'menu.exception.not-find': '404', 40 | 'menu.exception.server-error': '500', 41 | 'menu.exception.trigger': 'Disparar', 42 | 'menu.account': 'Conta', 43 | 'menu.account.center': 'Central da Conta', 44 | 'menu.account.settings': 'Configurar Conta', 45 | 'menu.account.trigger': 'Disparar Erro', 46 | 'menu.account.logout': 'Sair', 47 | 'menu.editor': 'Graphic Editor', 48 | 'menu.editor.flow': 'Flow Editor', 49 | 'menu.editor.mind': 'Mind Editor', 50 | 'menu.editor.koni': 'Koni Editor', 51 | }; 52 | -------------------------------------------------------------------------------- /src/locales/pt-BR/pwa.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.pwa.offline': 'Você está offline agora', 3 | 'app.pwa.serviceworker.updated': 'Novo conteúdo está disponível', 4 | 'app.pwa.serviceworker.updated.hint': 5 | 'Por favor, pressione o botão "Atualizar" para recarregar a página atual', 6 | 'app.pwa.serviceworker.updated.ok': 'Atualizar', 7 | }; 8 | -------------------------------------------------------------------------------- /src/locales/pt-BR/settingDrawer.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.setting.pagestyle': 'Configuração de estilo da página', 3 | 'app.setting.pagestyle.dark': 'Dark style', 4 | 'app.setting.pagestyle.light': 'Light style', 5 | 'app.setting.content-width': 'Largura do conteúdo', 6 | 'app.setting.content-width.fixed': 'Fixo', 7 | 'app.setting.content-width.fluid': 'Fluido', 8 | 'app.setting.themecolor': 'Cor do Tema', 9 | 'app.setting.themecolor.dust': 'Dust Red', 10 | 'app.setting.themecolor.volcano': 'Volcano', 11 | 'app.setting.themecolor.sunset': 'Sunset Orange', 12 | 'app.setting.themecolor.cyan': 'Cyan', 13 | 'app.setting.themecolor.green': 'Polar Green', 14 | 'app.setting.themecolor.daybreak': 'Daybreak Blue (default)', 15 | 'app.setting.themecolor.geekblue': 'Geek Glue', 16 | 'app.setting.themecolor.purple': 'Golden Purple', 17 | 'app.setting.navigationmode': 'Modo de Navegação', 18 | 'app.setting.sidemenu': 'Layout do Menu Lateral', 19 | 'app.setting.topmenu': 'Layout do Menu Superior', 20 | 'app.setting.fixedheader': 'Cabeçalho fixo', 21 | 'app.setting.fixedsidebar': 'Barra lateral fixa', 22 | 'app.setting.fixedsidebar.hint': 'Funciona no layout do menu lateral', 23 | 'app.setting.hideheader': 'Esconder o cabeçalho quando rolar', 24 | 'app.setting.hideheader.hint': 'Funciona quando o esconder cabeçalho está abilitado', 25 | 'app.setting.othersettings': 'Outras configurações', 26 | 'app.setting.weakmode': 'Weak Mode', 27 | 'app.setting.copy': 'Copiar Configuração', 28 | 'app.setting.copyinfo': 29 | 'copiado com sucesso,por favor trocar o defaultSettings em src/models/setting.js', 30 | 'app.setting.production.hint': 31 | 'O painel de configuração apenas é exibido no ambiente de desenvolvimento, por favor modifique manualmente o', 32 | }; 33 | -------------------------------------------------------------------------------- /src/locales/zh-CN.ts: -------------------------------------------------------------------------------- 1 | import component from './zh-CN/component'; 2 | import globalHeader from './zh-CN/globalHeader'; 3 | import menu from './zh-CN/menu'; 4 | import pwa from './zh-CN/pwa'; 5 | import settingDrawer from './zh-CN/settingDrawer'; 6 | import settings from './zh-CN/settings'; 7 | 8 | export default { 9 | 'navBar.lang': '语言', 10 | 'layout.user.link.help': '帮助', 11 | 'layout.user.link.privacy': '隐私', 12 | 'layout.user.link.terms': '条款', 13 | 'app.preview.down.block': '下载此页面到本地项目', 14 | 'app.welcome.link.fetch-blocks': '获取全部区块', 15 | 'app.welcome.link.block-list': '基于 block 开发,快速构建标准页面', 16 | ...globalHeader, 17 | ...menu, 18 | ...settingDrawer, 19 | ...settings, 20 | ...pwa, 21 | ...component, 22 | }; 23 | -------------------------------------------------------------------------------- /src/locales/zh-CN/component.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.tagSelect.expand': '展开', 3 | 'component.tagSelect.collapse': '收起', 4 | 'component.tagSelect.all': '全部', 5 | }; 6 | -------------------------------------------------------------------------------- /src/locales/zh-CN/globalHeader.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.globalHeader.search': '站内搜索', 3 | 'component.globalHeader.search.example1': '搜索提示一', 4 | 'component.globalHeader.search.example2': '搜索提示二', 5 | 'component.globalHeader.search.example3': '搜索提示三', 6 | 'component.globalHeader.help': '使用文档', 7 | 'component.globalHeader.notification': '通知', 8 | 'component.globalHeader.notification.empty': '你已查看所有通知', 9 | 'component.globalHeader.message': '消息', 10 | 'component.globalHeader.message.empty': '您已读完所有消息', 11 | 'component.globalHeader.event': '待办', 12 | 'component.globalHeader.event.empty': '你已完成所有待办', 13 | 'component.noticeIcon.clear': '清空', 14 | 'component.noticeIcon.cleared': '清空了', 15 | 'component.noticeIcon.empty': '暂无数据', 16 | 'component.noticeIcon.view-more': '查看更多', 17 | }; 18 | -------------------------------------------------------------------------------- /src/locales/zh-CN/menu.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.welcome': '欢迎', 3 | 'menu.more-blocks': '更多区块', 4 | 'menu.home': '首页', 5 | 'menu.admin': '管理页', 6 | 'menu.login': '登录', 7 | 'menu.register': '注册', 8 | 'menu.register.result': '注册结果', 9 | 'menu.dashboard': 'Dashboard', 10 | 'menu.dashboard.analysis': '分析页', 11 | 'menu.dashboard.monitor': '监控页', 12 | 'menu.dashboard.workplace': '工作台', 13 | 'menu.exception.403': '403', 14 | 'menu.exception.404': '404', 15 | 'menu.exception.500': '500', 16 | 'menu.form': '表单页', 17 | 'menu.form.basic-form': '基础表单', 18 | 'menu.form.step-form': '分步表单', 19 | 'menu.form.step-form.info': '分步表单(填写转账信息)', 20 | 'menu.form.step-form.confirm': '分步表单(确认转账信息)', 21 | 'menu.form.step-form.result': '分步表单(完成)', 22 | 'menu.form.advanced-form': '高级表单', 23 | 'menu.list': '列表页', 24 | 'menu.list.table-list': '查询表格', 25 | 'menu.list.basic-list': '标准列表', 26 | 'menu.list.card-list': '卡片列表', 27 | 'menu.list.search-list': '搜索列表', 28 | 'menu.list.search-list.articles': '搜索列表(文章)', 29 | 'menu.list.search-list.projects': '搜索列表(项目)', 30 | 'menu.list.search-list.applications': '搜索列表(应用)', 31 | 'menu.profile': '详情页', 32 | 'menu.profile.basic': '基础详情页', 33 | 'menu.profile.advanced': '高级详情页', 34 | 'menu.result': '结果页', 35 | 'menu.result.success': '成功页', 36 | 'menu.result.fail': '失败页', 37 | 'menu.exception': '异常页', 38 | 'menu.exception.not-permission': '403', 39 | 'menu.exception.not-find': '404', 40 | 'menu.exception.server-error': '500', 41 | 'menu.exception.trigger': '触发错误', 42 | 'menu.account': '个人页', 43 | 'menu.account.center': '个人中心', 44 | 'menu.account.settings': '个人设置', 45 | 'menu.account.trigger': '触发报错', 46 | 'menu.account.logout': '退出登录', 47 | 'menu.editor': '图形编辑器', 48 | 'menu.editor.flow': '流程编辑器', 49 | 'menu.editor.mind': '脑图编辑器', 50 | 'menu.editor.koni': '拓扑编辑器', 51 | }; 52 | -------------------------------------------------------------------------------- /src/locales/zh-CN/pwa.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.pwa.offline': '当前处于离线状态', 3 | 'app.pwa.serviceworker.updated': '有新内容', 4 | 'app.pwa.serviceworker.updated.hint': '请点击“刷新”按钮或者手动刷新页面', 5 | 'app.pwa.serviceworker.updated.ok': '刷新', 6 | }; 7 | -------------------------------------------------------------------------------- /src/locales/zh-CN/settingDrawer.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.setting.pagestyle': '整体风格设置', 3 | 'app.setting.pagestyle.dark': '暗色菜单风格', 4 | 'app.setting.pagestyle.light': '亮色菜单风格', 5 | 'app.setting.content-width': '内容区域宽度', 6 | 'app.setting.content-width.fixed': '定宽', 7 | 'app.setting.content-width.fluid': '流式', 8 | 'app.setting.themecolor': '主题色', 9 | 'app.setting.themecolor.dust': '薄暮', 10 | 'app.setting.themecolor.volcano': '火山', 11 | 'app.setting.themecolor.sunset': '日暮', 12 | 'app.setting.themecolor.cyan': '明青', 13 | 'app.setting.themecolor.green': '极光绿', 14 | 'app.setting.themecolor.daybreak': '拂晓蓝(默认)', 15 | 'app.setting.themecolor.geekblue': '极客蓝', 16 | 'app.setting.themecolor.purple': '酱紫', 17 | 'app.setting.navigationmode': '导航模式', 18 | 'app.setting.sidemenu': '侧边菜单布局', 19 | 'app.setting.topmenu': '顶部菜单布局', 20 | 'app.setting.fixedheader': '固定 Header', 21 | 'app.setting.fixedsidebar': '固定侧边菜单', 22 | 'app.setting.fixedsidebar.hint': '侧边菜单布局时可配置', 23 | 'app.setting.hideheader': '下滑时隐藏 Header', 24 | 'app.setting.hideheader.hint': '固定 Header 时可配置', 25 | 'app.setting.othersettings': '其他设置', 26 | 'app.setting.weakmode': '色弱模式', 27 | 'app.setting.copy': '拷贝设置', 28 | 'app.setting.copyinfo': '拷贝成功,请到 src/defaultSettings.js 中替换默认配置', 29 | 'app.setting.production.hint': 30 | '配置栏只在开发环境用于预览,生产环境不会展现,请拷贝后手动修改配置文件', 31 | }; 32 | -------------------------------------------------------------------------------- /src/locales/zh-CN/settings.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.settings.menuMap.basic': '基本设置', 3 | 'app.settings.menuMap.security': '安全设置', 4 | 'app.settings.menuMap.binding': '账号绑定', 5 | 'app.settings.menuMap.notification': '新消息通知', 6 | 'app.settings.basic.avatar': '头像', 7 | 'app.settings.basic.change-avatar': '更换头像', 8 | 'app.settings.basic.email': '邮箱', 9 | 'app.settings.basic.email-message': '请输入您的邮箱!', 10 | 'app.settings.basic.nickname': '昵称', 11 | 'app.settings.basic.nickname-message': '请输入您的昵称!', 12 | 'app.settings.basic.profile': '个人简介', 13 | 'app.settings.basic.profile-message': '请输入个人简介!', 14 | 'app.settings.basic.profile-placeholder': '个人简介', 15 | 'app.settings.basic.country': '国家/地区', 16 | 'app.settings.basic.country-message': '请输入您的国家或地区!', 17 | 'app.settings.basic.geographic': '所在省市', 18 | 'app.settings.basic.geographic-message': '请输入您的所在省市!', 19 | 'app.settings.basic.address': '街道地址', 20 | 'app.settings.basic.address-message': '请输入您的街道地址!', 21 | 'app.settings.basic.phone': '联系电话', 22 | 'app.settings.basic.phone-message': '请输入您的联系电话!', 23 | 'app.settings.basic.update': '更新基本信息', 24 | 'app.settings.security.strong': '强', 25 | 'app.settings.security.medium': '中', 26 | 'app.settings.security.weak': '弱', 27 | 'app.settings.security.password': '账户密码', 28 | 'app.settings.security.password-description': '当前密码强度', 29 | 'app.settings.security.phone': '密保手机', 30 | 'app.settings.security.phone-description': '已绑定手机', 31 | 'app.settings.security.question': '密保问题', 32 | 'app.settings.security.question-description': '未设置密保问题,密保问题可有效保护账户安全', 33 | 'app.settings.security.email': '备用邮箱', 34 | 'app.settings.security.email-description': '已绑定邮箱', 35 | 'app.settings.security.mfa': 'MFA 设备', 36 | 'app.settings.security.mfa-description': '未绑定 MFA 设备,绑定后,可以进行二次确认', 37 | 'app.settings.security.modify': '修改', 38 | 'app.settings.security.set': '设置', 39 | 'app.settings.security.bind': '绑定', 40 | 'app.settings.binding.taobao': '绑定淘宝', 41 | 'app.settings.binding.taobao-description': '当前未绑定淘宝账号', 42 | 'app.settings.binding.alipay': '绑定支付宝', 43 | 'app.settings.binding.alipay-description': '当前未绑定支付宝账号', 44 | 'app.settings.binding.dingding': '绑定钉钉', 45 | 'app.settings.binding.dingding-description': '当前未绑定钉钉账号', 46 | 'app.settings.binding.bind': '绑定', 47 | 'app.settings.notification.password': '账户密码', 48 | 'app.settings.notification.password-description': '其他用户的消息将以站内信的形式通知', 49 | 'app.settings.notification.messages': '系统消息', 50 | 'app.settings.notification.messages-description': '系统消息将以站内信的形式通知', 51 | 'app.settings.notification.todo': '待办任务', 52 | 'app.settings.notification.todo-description': '待办任务将以站内信的形式通知', 53 | 'app.settings.open': '开', 54 | 'app.settings.close': '关', 55 | }; 56 | -------------------------------------------------------------------------------- /src/locales/zh-TW.ts: -------------------------------------------------------------------------------- 1 | import component from './zh-TW/component'; 2 | import globalHeader from './zh-TW/globalHeader'; 3 | import menu from './zh-TW/menu'; 4 | import pwa from './zh-TW/pwa'; 5 | import settingDrawer from './zh-TW/settingDrawer'; 6 | import settings from './zh-TW/settings'; 7 | 8 | export default { 9 | 'navBar.lang': '語言', 10 | 'layout.user.link.help': '幫助', 11 | 'layout.user.link.privacy': '隱私', 12 | 'layout.user.link.terms': '條款', 13 | 'app.preview.down.block': '下載此頁面到本地項目', 14 | ...globalHeader, 15 | ...menu, 16 | ...settingDrawer, 17 | ...settings, 18 | ...pwa, 19 | ...component, 20 | }; 21 | -------------------------------------------------------------------------------- /src/locales/zh-TW/component.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.tagSelect.expand': '展開', 3 | 'component.tagSelect.collapse': '收起', 4 | 'component.tagSelect.all': '全部', 5 | }; 6 | -------------------------------------------------------------------------------- /src/locales/zh-TW/globalHeader.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.globalHeader.search': '站內搜索', 3 | 'component.globalHeader.search.example1': '搜索提示壹', 4 | 'component.globalHeader.search.example2': '搜索提示二', 5 | 'component.globalHeader.search.example3': '搜索提示三', 6 | 'component.globalHeader.help': '使用手冊', 7 | 'component.globalHeader.notification': '通知', 8 | 'component.globalHeader.notification.empty': '妳已查看所有通知', 9 | 'component.globalHeader.message': '消息', 10 | 'component.globalHeader.message.empty': '您已讀完所有消息', 11 | 'component.globalHeader.event': '待辦', 12 | 'component.globalHeader.event.empty': '妳已完成所有待辦', 13 | 'component.noticeIcon.clear': '清空', 14 | 'component.noticeIcon.cleared': '清空了', 15 | 'component.noticeIcon.empty': '暫無資料', 16 | 'component.noticeIcon.view-more': '查看更多', 17 | }; 18 | -------------------------------------------------------------------------------- /src/locales/zh-TW/menu.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.welcome': '歡迎', 3 | 'menu.more-blocks': '更多區塊', 4 | 5 | 'menu.home': '首頁', 6 | 'menu.login': '登錄', 7 | 'menu.admin': '权限', 8 | 'menu.exception.403': '403', 9 | 'menu.exception.404': '404', 10 | 'menu.exception.500': '500', 11 | 'menu.register': '註冊', 12 | 'menu.register.result': '註冊結果', 13 | 'menu.dashboard': 'Dashboard', 14 | 'menu.dashboard.analysis': '分析頁', 15 | 'menu.dashboard.monitor': '監控頁', 16 | 'menu.dashboard.workplace': '工作臺', 17 | 'menu.form': '表單頁', 18 | 'menu.form.basic-form': '基礎表單', 19 | 'menu.form.step-form': '分步表單', 20 | 'menu.form.step-form.info': '分步表單(填寫轉賬信息)', 21 | 'menu.form.step-form.confirm': '分步表單(確認轉賬信息)', 22 | 'menu.form.step-form.result': '分步表單(完成)', 23 | 'menu.form.advanced-form': '高級表單', 24 | 'menu.list': '列表頁', 25 | 'menu.list.table-list': '查詢表格', 26 | 'menu.list.basic-list': '標淮列表', 27 | 'menu.list.card-list': '卡片列表', 28 | 'menu.list.search-list': '搜索列表', 29 | 'menu.list.search-list.articles': '搜索列表(文章)', 30 | 'menu.list.search-list.projects': '搜索列表(項目)', 31 | 'menu.list.search-list.applications': '搜索列表(應用)', 32 | 'menu.profile': '詳情頁', 33 | 'menu.profile.basic': '基礎詳情頁', 34 | 'menu.profile.advanced': '高級詳情頁', 35 | 'menu.result': '結果頁', 36 | 'menu.result.success': '成功頁', 37 | 'menu.result.fail': '失敗頁', 38 | 'menu.account': '個人頁', 39 | 'menu.account.center': '個人中心', 40 | 'menu.account.settings': '個人設置', 41 | 'menu.account.trigger': '觸發報錯', 42 | 'menu.account.logout': '退出登錄', 43 | 'menu.exception': '异常页', 44 | 'menu.exception.not-permission': '403', 45 | 'menu.exception.not-find': '404', 46 | 'menu.exception.server-error': '500', 47 | 'menu.exception.trigger': '触发错误', 48 | 'menu.editor': '圖形編輯器', 49 | 'menu.editor.flow': '流程編輯器', 50 | 'menu.editor.mind': '腦圖編輯器', 51 | 'menu.editor.koni': '拓撲編輯器', 52 | }; 53 | -------------------------------------------------------------------------------- /src/locales/zh-TW/pwa.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.pwa.offline': '當前處於離線狀態', 3 | 'app.pwa.serviceworker.updated': '有新內容', 4 | 'app.pwa.serviceworker.updated.hint': '請點擊“刷新”按鈕或者手動刷新頁面', 5 | 'app.pwa.serviceworker.updated.ok': '刷新', 6 | }; 7 | -------------------------------------------------------------------------------- /src/locales/zh-TW/settingDrawer.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.setting.pagestyle': '整體風格設置', 3 | 'app.setting.pagestyle.dark': '暗色菜單風格', 4 | 'app.setting.pagestyle.light': '亮色菜單風格', 5 | 'app.setting.content-width': '內容區域寬度', 6 | 'app.setting.content-width.fixed': '定寬', 7 | 'app.setting.content-width.fluid': '流式', 8 | 'app.setting.themecolor': '主題色', 9 | 'app.setting.themecolor.dust': '薄暮', 10 | 'app.setting.themecolor.volcano': '火山', 11 | 'app.setting.themecolor.sunset': '日暮', 12 | 'app.setting.themecolor.cyan': '明青', 13 | 'app.setting.themecolor.green': '極光綠', 14 | 'app.setting.themecolor.daybreak': '拂曉藍(默認)', 15 | 'app.setting.themecolor.geekblue': '極客藍', 16 | 'app.setting.themecolor.purple': '醬紫', 17 | 'app.setting.navigationmode': '導航模式', 18 | 'app.setting.sidemenu': '側邊菜單布局', 19 | 'app.setting.topmenu': '頂部菜單布局', 20 | 'app.setting.fixedheader': '固定 Header', 21 | 'app.setting.fixedsidebar': '固定側邊菜單', 22 | 'app.setting.fixedsidebar.hint': '側邊菜單布局時可配置', 23 | 'app.setting.hideheader': '下滑時隱藏 Header', 24 | 'app.setting.hideheader.hint': '固定 Header 時可配置', 25 | 'app.setting.othersettings': '其他設置', 26 | 'app.setting.weakmode': '色弱模式', 27 | 'app.setting.copy': '拷貝設置', 28 | 'app.setting.copyinfo': '拷貝成功,請到 src/defaultSettings.js 中替換默認配置', 29 | 'app.setting.production.hint': 30 | '配置欄只在開發環境用於預覽,生產環境不會展現,請拷貝後手動修改配置文件', 31 | }; 32 | -------------------------------------------------------------------------------- /src/locales/zh-TW/settings.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.settings.menuMap.basic': '基本設置', 3 | 'app.settings.menuMap.security': '安全設置', 4 | 'app.settings.menuMap.binding': '賬號綁定', 5 | 'app.settings.menuMap.notification': '新消息通知', 6 | 'app.settings.basic.avatar': '頭像', 7 | 'app.settings.basic.change-avatar': '更換頭像', 8 | 'app.settings.basic.email': '郵箱', 9 | 'app.settings.basic.email-message': '請輸入您的郵箱!', 10 | 'app.settings.basic.nickname': '昵稱', 11 | 'app.settings.basic.nickname-message': '請輸入您的昵稱!', 12 | 'app.settings.basic.profile': '個人簡介', 13 | 'app.settings.basic.profile-message': '請輸入個人簡介!', 14 | 'app.settings.basic.profile-placeholder': '個人簡介', 15 | 'app.settings.basic.country': '國家/地區', 16 | 'app.settings.basic.country-message': '請輸入您的國家或地區!', 17 | 'app.settings.basic.geographic': '所在省市', 18 | 'app.settings.basic.geographic-message': '請輸入您的所在省市!', 19 | 'app.settings.basic.address': '街道地址', 20 | 'app.settings.basic.address-message': '請輸入您的街道地址!', 21 | 'app.settings.basic.phone': '聯系電話', 22 | 'app.settings.basic.phone-message': '請輸入您的聯系電話!', 23 | 'app.settings.basic.update': '更新基本信息', 24 | 'app.settings.security.strong': '強', 25 | 'app.settings.security.medium': '中', 26 | 'app.settings.security.weak': '弱', 27 | 'app.settings.security.password': '賬戶密碼', 28 | 'app.settings.security.password-description': '當前密碼強度', 29 | 'app.settings.security.phone': '密保手機', 30 | 'app.settings.security.phone-description': '已綁定手機', 31 | 'app.settings.security.question': '密保問題', 32 | 'app.settings.security.question-description': '未設置密保問題,密保問題可有效保護賬戶安全', 33 | 'app.settings.security.email': '備用郵箱', 34 | 'app.settings.security.email-description': '已綁定郵箱', 35 | 'app.settings.security.mfa': 'MFA 設備', 36 | 'app.settings.security.mfa-description': '未綁定 MFA 設備,綁定後,可以進行二次確認', 37 | 'app.settings.security.modify': '修改', 38 | 'app.settings.security.set': '設置', 39 | 'app.settings.security.bind': '綁定', 40 | 'app.settings.binding.taobao': '綁定淘寶', 41 | 'app.settings.binding.taobao-description': '當前未綁定淘寶賬號', 42 | 'app.settings.binding.alipay': '綁定支付寶', 43 | 'app.settings.binding.alipay-description': '當前未綁定支付寶賬號', 44 | 'app.settings.binding.dingding': '綁定釘釘', 45 | 'app.settings.binding.dingding-description': '當前未綁定釘釘賬號', 46 | 'app.settings.binding.bind': '綁定', 47 | 'app.settings.notification.password': '賬戶密碼', 48 | 'app.settings.notification.password-description': '其他用戶的消息將以站內信的形式通知', 49 | 'app.settings.notification.messages': '系統消息', 50 | 'app.settings.notification.messages-description': '系統消息將以站內信的形式通知', 51 | 'app.settings.notification.todo': '待辦任務', 52 | 'app.settings.notification.todo-description': '待辦任務將以站內信的形式通知', 53 | 'app.settings.open': '開', 54 | 'app.settings.close': '關', 55 | }; 56 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Ant Design Pro", 3 | "short_name": "Ant Design Pro", 4 | "display": "standalone", 5 | "start_url": "./?utm_source=homescreen", 6 | "theme_color": "#002140", 7 | "background_color": "#001529", 8 | "icons": [ 9 | { 10 | "src": "icons/icon-192x192.png", 11 | "sizes": "192x192" 12 | }, 13 | { 14 | "src": "icons/icon-128x128.png", 15 | "sizes": "128x128" 16 | }, 17 | { 18 | "src": "icons/icon-512x512.png", 19 | "sizes": "512x512" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /src/models/connect.d.ts: -------------------------------------------------------------------------------- 1 | import { ConnectProps as DefaultConnectProps } from 'umi'; 2 | import { MenuDataItem, Settings as ProSettings } from '@ant-design/pro-layout'; 3 | // eslint-disable-next-line import/no-extraneous-dependencies 4 | import { match } from 'react-router-dom'; 5 | // eslint-disable-next-line import/no-extraneous-dependencies 6 | import { Location, LocationState } from 'history'; 7 | import { GlobalModelState } from './global'; 8 | import { StateType as AuthModelState } from './auth'; 9 | import { DefaultSettings as SettingModelState } from '../../config/defaultSettings'; 10 | 11 | export { GlobalModelState, SettingModelState, AuthModelState }; 12 | 13 | export interface Loading { 14 | global: boolean; 15 | effects: { [key: string]: boolean }; 16 | models: { 17 | global: boolean; 18 | setting: boolean; 19 | auth: boolean; 20 | }; 21 | } 22 | 23 | export interface ConnectState { 24 | global: GlobalModelState; 25 | loading: Loading; 26 | settings: ProSettings; 27 | auth: AuthModelState; 28 | } 29 | 30 | export interface Route extends MenuDataItem { 31 | routes?: Route[]; 32 | } 33 | 34 | export interface ConnectProps

35 | extends DefaultConnectProps { 36 | match?: match

; 37 | location: Location & { query: { [key: string]: any } }; 38 | } 39 | -------------------------------------------------------------------------------- /src/models/global.ts: -------------------------------------------------------------------------------- 1 | import { Reducer } from 'umi'; 2 | 3 | export interface GlobalModelState { 4 | collapsed: boolean; 5 | } 6 | 7 | export interface GlobalModelType { 8 | namespace: 'global'; 9 | state: GlobalModelState; 10 | reducers: { 11 | changeLayoutCollapsed: Reducer; 12 | }; 13 | } 14 | 15 | const GlobalModel: GlobalModelType = { 16 | namespace: 'global', 17 | 18 | state: { 19 | collapsed: false, 20 | }, 21 | 22 | reducers: { 23 | changeLayoutCollapsed(state = { collapsed: true }, { payload }): GlobalModelState { 24 | return { 25 | ...state, 26 | collapsed: payload, 27 | }; 28 | }, 29 | }, 30 | }; 31 | 32 | export default GlobalModel; 33 | -------------------------------------------------------------------------------- /src/models/setting.ts: -------------------------------------------------------------------------------- 1 | import { Reducer } from 'umi'; 2 | import defaultSettings, { DefaultSettings } from '../../config/defaultSettings'; 3 | 4 | export interface SettingModelType { 5 | namespace: 'settings'; 6 | state: DefaultSettings; 7 | reducers: { 8 | changeSetting: Reducer; 9 | }; 10 | } 11 | 12 | const updateColorWeak: (colorWeak: boolean) => void = (colorWeak) => { 13 | const root = document.getElementById('root'); 14 | if (root) { 15 | root.className = colorWeak ? 'colorWeak' : ''; 16 | } 17 | }; 18 | 19 | const SettingModel: SettingModelType = { 20 | namespace: 'settings', 21 | state: defaultSettings, 22 | reducers: { 23 | changeSetting(state = defaultSettings, { payload }) { 24 | const { colorWeak, contentWidth } = payload; 25 | 26 | if (state.contentWidth !== contentWidth && window.dispatchEvent) { 27 | window.dispatchEvent(new Event('resize')); 28 | } 29 | updateColorWeak(!!colorWeak); 30 | 31 | return { 32 | ...state, 33 | ...payload, 34 | }; 35 | }, 36 | }, 37 | }; 38 | export default SettingModel; 39 | -------------------------------------------------------------------------------- /src/pages/404.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Result } from 'antd'; 2 | import React from 'react'; 3 | import { history } from 'umi'; 4 | 5 | const NoFoundPage: React.FC<{}> = () => ( 6 | history.push('/')}> 12 | Back Home 13 | 14 | } 15 | /> 16 | ); 17 | 18 | export default NoFoundPage; 19 | -------------------------------------------------------------------------------- /src/pages/account/center/components/Comments/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link, useRequest } from 'umi'; 3 | import { List } from 'antd'; 4 | import { ClockCircleOutlined, LikeOutlined, MessageOutlined } from '@ant-design/icons'; 5 | import { IArticle, IComment, ResponseResultType } from '@/models/I'; 6 | import { umiformatPaginationResult } from '@/utils/utils'; 7 | import MarkdownBody from '@/components/MarkdownBody'; 8 | import * as services from '../../services'; 9 | import styles from './style.less'; 10 | 11 | interface CommentsProps {} 12 | 13 | const Comments: React.FC = () => { 14 | const { loading, data, pagination } = useRequest, IComment>( 15 | ({ current, pageSize }) => 16 | services.queryComments({ 17 | page: current, 18 | per_page: pageSize, 19 | include: 'commentable,parent.user', 20 | }), 21 | { 22 | paginated: true, 23 | formatResult: umiformatPaginationResult, 24 | }, 25 | ); 26 | 27 | function renderItemTitle(comment: IComment) { 28 | switch (comment.commentable_type) { 29 | case 'App\\Models\\Article': 30 | return ( 31 | 32 | {(comment.commentable as IArticle)?.title} 33 | 34 | ); 35 | default: 36 | return null; 37 | } 38 | } 39 | 40 | return ( 41 |

42 | ( 53 | 54 | 55 |
56 |
57 | 58 |
59 |
60 | 61 | 62 | {item.created_at_timeago} 63 | 64 | 65 | 66 | {item.friendly_up_voters_count} 67 | 68 | 69 | 70 | {item.friendly_comments_count} 71 | 72 |
73 |
74 |
75 | )} 76 | /> 77 |
78 | ); 79 | }; 80 | 81 | export default Comments; 82 | -------------------------------------------------------------------------------- /src/pages/account/center/components/Comments/style.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .content { 4 | .extra { 5 | margin-top: 16px; 6 | color: @text-color-secondary; 7 | line-height: 22px; 8 | 9 | & > em { 10 | margin-left: 16px; 11 | color: @disabled-color; 12 | font-style: normal; 13 | } 14 | } 15 | } 16 | 17 | .description { 18 | :global(.markdown-body) { 19 | font-size: 14px; 20 | } 21 | } 22 | 23 | @media screen and (max-width: @screen-xs) { 24 | .content { 25 | .extra { 26 | & > em { 27 | display: block; 28 | margin-top: 8px; 29 | margin-left: 0; 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/pages/account/center/components/Favorites/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link, useRequest } from 'umi'; 3 | import { List, Tag } from 'antd'; 4 | import { umiformatPaginationResult } from '@/utils/utils'; 5 | import { IArticle, IFollowable, ResponseResultType } from '@/models/I'; 6 | import ArticleListContent from '@/pages/articles/list/components/ArticleListContent'; 7 | import * as services from '../../services'; 8 | 9 | interface FavoritesProps {} 10 | 11 | const Favorites: React.FC = () => { 12 | const { loading, data, pagination } = useRequest, IFollowable>( 13 | ({ current, pageSize }) => 14 | services.queryFollowRelations({ 15 | relation: 'favorite', 16 | page: current, 17 | per_page: pageSize, 18 | include: 'followable.user,followable.tags', 19 | }), 20 | { 21 | paginated: true, 22 | formatResult: umiformatPaginationResult, 23 | }, 24 | ); 25 | 26 | function renderItemTitle(item: IFollowable) { 27 | switch (item.followable_type) { 28 | case 'App\\Models\\Article': 29 | return ( 30 | {(item.followable as IArticle)?.title} 31 | ); 32 | default: 33 | return null; 34 | } 35 | } 36 | 37 | function renderItemContent(item: IFollowable) { 38 | switch (item.followable_type) { 39 | case 'App\\Models\\Article': 40 | return item.followable && ; 41 | default: 42 | return null; 43 | } 44 | } 45 | 46 | return ( 47 | ( 58 | 59 | ( 62 | {tag.name} 63 | ))} 64 | /> 65 | {renderItemContent(item)} 66 | 67 | )} 68 | /> 69 | ); 70 | }; 71 | 72 | export default Favorites; 73 | -------------------------------------------------------------------------------- /src/pages/account/center/components/Likers/style.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .content { 4 | .extra { 5 | margin-top: 16px; 6 | color: @text-color-secondary; 7 | line-height: 22px; 8 | 9 | & > em { 10 | margin-left: 16px; 11 | color: @disabled-color; 12 | font-style: normal; 13 | } 14 | } 15 | } 16 | 17 | @media screen and (max-width: @screen-xs) { 18 | .content { 19 | .extra { 20 | & > em { 21 | display: block; 22 | margin-top: 8px; 23 | margin-left: 0; 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/pages/account/center/services.ts: -------------------------------------------------------------------------------- 1 | import { request } from 'umi'; 2 | 3 | export async function queryFollowRelations(params: object) { 4 | return request('user/follow_relations', { 5 | params, 6 | }); 7 | } 8 | 9 | export async function queryComments(params: object) { 10 | return request('user/comments', { 11 | params, 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /src/pages/account/center/style.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .avatarHolder { 4 | margin-bottom: 24px; 5 | text-align: center; 6 | 7 | & > img { 8 | width: 104px; 9 | height: 104px; 10 | margin-bottom: 20px; 11 | border-radius: 50%; 12 | box-shadow: @shadow-1-down; 13 | } 14 | 15 | .name { 16 | margin-bottom: 4px; 17 | color: @heading-color; 18 | font-weight: 500; 19 | font-size: 20px; 20 | line-height: 28px; 21 | } 22 | } 23 | 24 | .detail { 25 | p { 26 | position: relative; 27 | margin-bottom: 8px; 28 | padding-left: 26px; 29 | 30 | &:last-child { 31 | margin-bottom: 0; 32 | } 33 | 34 | :global { 35 | .anticon { 36 | margin-right: 12px; 37 | } 38 | } 39 | } 40 | } 41 | 42 | .tabsCard { 43 | :global { 44 | .ant-card-body { 45 | padding-top: 8px; 46 | } 47 | 48 | .ant-list-item-meta-title { 49 | margin-top: 16px; 50 | margin-bottom: 0; 51 | color: black; 52 | font-weight: bold; 53 | } 54 | 55 | .ant-list-item-meta-description { 56 | margin-top: 8px; 57 | } 58 | 59 | .ant-list-item { 60 | padding-top: 0; 61 | padding-right: 0; 62 | padding-left: 0; 63 | &:hover { 64 | background-color: @item-hover-bg; 65 | } 66 | } 67 | } 68 | } 69 | 70 | @media (max-width: @screen-md) { 71 | .leftCard { 72 | width: 100%; 73 | margin-bottom: 12px; 74 | } 75 | 76 | .tabsCard { 77 | width: 100%; 78 | } 79 | 80 | .affix { 81 | > div:first-child[aria-hidden='true'] { 82 | display: none; 83 | } 84 | 85 | :global(.ant-affix) { 86 | position: static !important; 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/pages/account/notifications/components/Messages/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Empty } from 'antd'; 3 | 4 | interface MessagesProps {} 5 | 6 | const Messages: React.FC = () => { 7 | return ; 8 | }; 9 | 10 | export default Messages; 11 | -------------------------------------------------------------------------------- /src/pages/account/notifications/components/Notifications/components/CommentMyArticle.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'umi'; 3 | import { Avatar, Tooltip } from 'antd'; 4 | import { ClockCircleOutlined } from '@ant-design/icons'; 5 | import MarkdownBody from '@/components/MarkdownBody'; 6 | import { INotification } from '@/models/I'; 7 | import styles from './style.less'; 8 | 9 | interface CommentMyArticleProps { 10 | notification: INotification; 11 | } 12 | 13 | const CommentMyArticle: React.FC = ({ notification }) => { 14 | function getLink() { 15 | const pathname = `/articles/${notification.data.article_id}`; 16 | const hash = `#comment-${notification.data.comment_id}`; 17 | 18 | return `${pathname}${hash}`; 19 | } 20 | 21 | return ( 22 |
23 |
24 | 25 |
26 |
27 |
28 | {notification.data.username} 29 | 评论了您的文章 30 | {notification.data.article_title} 31 |
32 |
33 | 34 |
35 |
36 |
37 | 38 | 39 | 40 | {notification.created_at_timeago} 41 | 42 | 43 |
44 |
45 | ); 46 | }; 47 | 48 | export default CommentMyArticle; 49 | -------------------------------------------------------------------------------- /src/pages/account/notifications/components/Notifications/components/LikedMyArticle.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'umi'; 3 | import { Avatar, Tooltip } from 'antd'; 4 | import { ClockCircleOutlined } from '@ant-design/icons'; 5 | import { INotification } from '@/models/I'; 6 | import styles from './style.less'; 7 | 8 | interface LikedMyArticleProps { 9 | notification: INotification; 10 | } 11 | 12 | const LikedMyArticle: React.FC = ({ notification }) => { 13 | return ( 14 |
15 |
16 | 17 |
18 |
19 |
20 | {notification.data.username} 21 | 赞了您的文章 22 | 23 | {notification.data.article_title} 24 | 25 |
26 |
27 |
28 |
29 | 30 | 31 | 32 | {notification.created_at_timeago} 33 | 34 | 35 |
36 |
37 | ); 38 | }; 39 | 40 | export default LikedMyArticle; 41 | -------------------------------------------------------------------------------- /src/pages/account/notifications/components/Notifications/components/MentionedMe.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'umi'; 3 | import { Avatar, Tooltip } from 'antd'; 4 | import { ClockCircleOutlined } from '@ant-design/icons'; 5 | import MarkdownBody from '@/components/MarkdownBody'; 6 | import { INotification } from '@/models/I'; 7 | import styles from './style.less'; 8 | 9 | interface MentionedMeProps { 10 | notification: INotification; 11 | } 12 | 13 | const MentionedMe: React.FC = ({ notification }) => { 14 | function renderContentHead() { 15 | switch (notification.data.contentable_type) { 16 | case 'App\\Models\\Article': 17 | return ( 18 | <> 19 | {notification.data.username} 20 | 21 | 22 | {notification.data.contentable_title} 23 | 24 | 文章中提及了您 25 | 26 | ); 27 | case 'App\\Models\\Comment': 28 | // eslint-disable-next-line no-case-declarations 29 | const pathname = `/articles/${notification.data.commentable_id}`; 30 | // eslint-disable-next-line no-case-declarations 31 | const hash = `#comment-${notification.data.comment_id}`; 32 | 33 | return ( 34 | <> 35 | {notification.data.username} 36 | 37 | {notification.data.commentable_title} 38 | 的评论中提及了您 39 | 40 | ); 41 | default: 42 | return null; 43 | } 44 | } 45 | 46 | return ( 47 |
48 |
49 | 50 |
51 |
52 |
{renderContentHead()}
53 |
54 | 55 |
56 |
57 |
58 | 59 | 60 | 61 | {notification.created_at_timeago} 62 | 63 | 64 |
65 |
66 | ); 67 | }; 68 | 69 | export default MentionedMe; 70 | -------------------------------------------------------------------------------- /src/pages/account/notifications/components/Notifications/components/ReplyMyComment.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect, Link } from 'umi'; 3 | import { Avatar, Tooltip } from 'antd'; 4 | import { ClockCircleOutlined } from '@ant-design/icons'; 5 | import MarkdownBody from '@/components/MarkdownBody'; 6 | import { ConnectProps, ConnectState, AuthModelState } from '@/models/connect'; 7 | import { INotification } from '@/models/I'; 8 | import styles from './style.less'; 9 | 10 | interface ReplyMyCommentProps extends Partial { 11 | notification: INotification; 12 | auth?: AuthModelState; 13 | } 14 | 15 | const ReplyMyComment: React.FC = ({ notification, auth }) => { 16 | function getLink() { 17 | const pathname = `/articles/${notification.data.commentable_id}`; 18 | const hash = `#comment-${notification.data.comment_id}`; 19 | 20 | return `${pathname}${hash}`; 21 | } 22 | 23 | function getCombineMarkdown() { 24 | return `${notification.data.content} //@${auth?.user.username}:${notification.data.parent_content}`; 25 | } 26 | 27 | return ( 28 |
29 |
30 | 31 |
32 |
33 |
34 | {notification.data.username} 35 | 回复了您的评论 36 | {notification.data.commentable_title} 37 |
38 |
39 | 40 |
41 |
42 |
43 | 44 | 45 | 46 | {notification.created_at_timeago} 47 | 48 | 49 |
50 |
51 | ); 52 | }; 53 | 54 | export default connect(({ auth }: ConnectState) => ({ auth }))(ReplyMyComment); 55 | -------------------------------------------------------------------------------- /src/pages/account/notifications/components/Notifications/components/UpVotedMyComment.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'umi'; 3 | import { Avatar, Tooltip } from 'antd'; 4 | import { ClockCircleOutlined } from '@ant-design/icons'; 5 | import MarkdownBody from '@/components/MarkdownBody'; 6 | import { INotification } from '@/models/I'; 7 | import styles from './style.less'; 8 | 9 | interface UpVotedMyCommentProps { 10 | notification: INotification; 11 | } 12 | 13 | const UpVotedMyComment: React.FC = ({ notification }) => { 14 | function getLink() { 15 | const pathname = `/articles/${notification.data.commentable_id}`; 16 | const hash = `#comment-${notification.data.comment_id}`; 17 | 18 | return `${pathname}${hash}`; 19 | } 20 | 21 | return ( 22 |
23 |
24 | 25 |
26 |
27 |
28 | {notification.data.username} 29 | 赞了您的评论 30 | {notification.data.commentable_title} 31 |
32 |
33 | 34 |
35 |
36 |
37 | 38 | 39 | 40 | {notification.created_at_timeago} 41 | 42 | 43 |
44 |
45 | ); 46 | }; 47 | 48 | export default UpVotedMyComment; 49 | -------------------------------------------------------------------------------- /src/pages/account/notifications/components/Notifications/components/style.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .notification { 4 | display: flex; 5 | flex-direction: row; 6 | justify-content: space-between; 7 | } 8 | 9 | .avatar { 10 | width: 32px; 11 | height: 32px; 12 | border: 1px solid white; 13 | border-radius: 50%; 14 | } 15 | 16 | .content { 17 | flex: 1; 18 | margin: 0 20px; 19 | } 20 | 21 | .contentHead { 22 | min-height: 32px; 23 | padding-top: 4px; 24 | font-weight: bold; 25 | 26 | a { 27 | color: rgba(0, 0, 0, 0.65); 28 | font-weight: 700; 29 | text-decoration: underline; 30 | } 31 | } 32 | 33 | .contentBody { 34 | margin-top: 16px; 35 | 36 | p:last-child { 37 | margin-bottom: 0; 38 | } 39 | 40 | :global { 41 | .markdown-body { 42 | font-weight: normal; 43 | font-size: 15px; 44 | } 45 | } 46 | } 47 | 48 | .timeago { 49 | padding-top: 4px; 50 | color: #bbb; 51 | text-align: right; 52 | } 53 | 54 | @media (max-width: @screen-md) { 55 | .content { 56 | margin: 0 12px; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/pages/account/notifications/components/Notifications/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useRequest } from 'umi'; 3 | import { List } from 'antd'; 4 | import { umiformatPaginationResult } from '@/utils/utils'; 5 | import { INotification, ResponseResultType } from '@/models/I'; 6 | import CommentMyArticle from './components/CommentMyArticle'; 7 | import LikedMyArticle from './components/LikedMyArticle'; 8 | import MentionedMe from './components/MentionedMe'; 9 | import ReplyMyComment from './components/ReplyMyComment'; 10 | import UpVotedMyComment from './components/UpVotedMyComment'; 11 | import * as service from '../../services'; 12 | import styles from './style.less'; 13 | 14 | interface NotificationsProps {} 15 | 16 | const Notifications: React.FC = () => { 17 | const { loading, data, pagination } = useRequest< 18 | ResponseResultType, 19 | INotification 20 | >(({ current, pageSize }) => service.queryNotifications({ page: current, per_page: pageSize }), { 21 | paginated: true, 22 | formatResult: umiformatPaginationResult, 23 | }); 24 | 25 | function renderNotification(notification: INotification) { 26 | switch (notification.type) { 27 | case 'App\\Notifications\\CommentMyArticle': 28 | return ; 29 | case 'App\\Notifications\\LikedMyArticle': 30 | return ; 31 | case 'App\\Notifications\\MentionedMe': 32 | return ; 33 | case 'App\\Notifications\\ReplyMyComment': 34 | return ; 35 | case 'App\\Notifications\\UpVotedMyComment': 36 | return ; 37 | default: 38 | return null; 39 | } 40 | } 41 | 42 | return ( 43 | ( 54 | 55 | {renderNotification(item)} 56 | 57 | )} 58 | /> 59 | ); 60 | }; 61 | 62 | export default Notifications; 63 | -------------------------------------------------------------------------------- /src/pages/account/notifications/components/Notifications/style.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .unread { 4 | margin: 0 -16px; 5 | padding: 0 16px; 6 | background-color: #fffbe6; 7 | } 8 | -------------------------------------------------------------------------------- /src/pages/account/notifications/components/Systems/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Empty } from 'antd'; 3 | 4 | interface SystemsProps {} 5 | 6 | const Systems: React.FC = () => { 7 | return ; 8 | }; 9 | 10 | export default Systems; 11 | -------------------------------------------------------------------------------- /src/pages/account/notifications/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { GridContent } from '@ant-design/pro-layout'; 3 | import { Menu } from 'antd'; 4 | import { BellOutlined, MailOutlined, NotificationOutlined } from '@ant-design/icons'; 5 | import { ConnectProps } from '@/models/connect'; 6 | import Notifications from './components/Notifications'; 7 | import Messages from './components/Messages'; 8 | import Systems from './components/Systems'; 9 | import styles from './style.less'; 10 | 11 | interface NoticesProps extends ConnectProps {} 12 | 13 | type StateKeysType = 'notifications' | 'messages' | 'systems'; 14 | 15 | const menuMap: { [key: string]: React.ReactNode } = { 16 | notifications: ( 17 | 18 | 19 | 通知 20 | 21 | ), 22 | messages: ( 23 | 24 | 25 | 私信 26 | 27 | ), 28 | systems: ( 29 | 30 | 31 | 系统 32 | 33 | ), 34 | }; 35 | 36 | const Notices: React.FC = () => { 37 | const [mode, setMode] = useState<'inline' | 'horizontal'>(); 38 | const [selectKey, setSelectKey] = useState('notifications'); 39 | 40 | function resize() { 41 | requestAnimationFrame(() => setMode(window.innerWidth < 768 ? 'horizontal' : 'inline')); 42 | } 43 | 44 | useEffect(() => { 45 | window.addEventListener('resize', resize); 46 | resize(); 47 | return () => window.removeEventListener('resize', resize); 48 | }, []); 49 | 50 | function renderChildren() { 51 | switch (selectKey) { 52 | case 'notifications': 53 | return ; 54 | case 'messages': 55 | return ; 56 | case 'systems': 57 | return ; 58 | default: 59 | return null; 60 | } 61 | } 62 | 63 | return ( 64 | 65 |
66 |
67 | setSelectKey(key as StateKeysType)} 71 | > 72 | {Object.entries(menuMap).map(([key, value]) => ( 73 | {value} 74 | ))} 75 | 76 |
77 |
78 |
{menuMap[selectKey]}
79 | {renderChildren()} 80 |
81 |
82 |
83 | ); 84 | }; 85 | 86 | export default Notices; 87 | -------------------------------------------------------------------------------- /src/pages/account/notifications/services.ts: -------------------------------------------------------------------------------- 1 | import { request } from 'umi'; 2 | 3 | export async function queryNotifications(params: object) { 4 | return request('user/notifications', { 5 | params, 6 | }); 7 | } 8 | -------------------------------------------------------------------------------- /src/pages/account/notifications/style.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .main { 4 | display: flex; 5 | width: 100%; 6 | height: 100%; 7 | padding-top: 16px; 8 | padding-bottom: 16px; 9 | overflow: auto; 10 | background-color: @menu-bg; 11 | 12 | .leftMenu { 13 | width: 224px; 14 | border-right: @border-width-base @border-style-base @border-color-split; 15 | 16 | :global { 17 | .ant-menu-inline { 18 | border: none; 19 | } 20 | 21 | .ant-menu:not(.ant-menu-horizontal) .ant-menu-item-selected { 22 | font-weight: bold; 23 | } 24 | } 25 | } 26 | 27 | .right { 28 | flex: 1; 29 | padding: 8px 40px; 30 | overflow: hidden; 31 | 32 | .title { 33 | margin-bottom: 12px; 34 | color: @heading-color; 35 | font-weight: 500; 36 | font-size: 20px; 37 | line-height: 28px; 38 | 39 | :global { 40 | .anticon { 41 | margin-right: 8px; 42 | } 43 | } 44 | } 45 | 46 | :global { 47 | .ant-list-item { 48 | &:hover { 49 | background-color: @item-hover-bg; 50 | } 51 | } 52 | } 53 | } 54 | 55 | :global { 56 | .ant-list-split .ant-list-item:last-child { 57 | border-bottom: 1px solid @border-color-split; 58 | } 59 | 60 | .ant-list-item { 61 | padding-top: 14px; 62 | padding-bottom: 14px; 63 | } 64 | } 65 | } 66 | 67 | @media screen and (max-width: @screen-md) { 68 | .main { 69 | flex-direction: column; 70 | padding-top: 8px; 71 | 72 | .leftMenu { 73 | width: 100%; 74 | border: none; 75 | 76 | :global { 77 | .ant-menu-item { 78 | padding-left: 12px !important; 79 | } 80 | } 81 | } 82 | 83 | .right { 84 | padding: 24px 12px 0 12px; 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/pages/account/settings/components/AvatarView.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .avatar_title { 4 | height: 22px; 5 | margin-bottom: 8px; 6 | color: @heading-color; 7 | font-size: @font-size-base; 8 | line-height: 22px; 9 | } 10 | .avatar { 11 | width: 144px; 12 | height: 144px; 13 | margin-bottom: 12px; 14 | overflow: hidden; 15 | border: 1px solid white; 16 | border-radius: 50%; 17 | box-shadow: @shadow-1-down; 18 | img { 19 | width: 100%; 20 | } 21 | } 22 | .button_view { 23 | width: 144px; 24 | text-align: center; 25 | } 26 | -------------------------------------------------------------------------------- /src/pages/account/settings/components/AvatarView.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import { Upload, message, Button } from 'antd'; 3 | import { UploadChangeParam } from 'antd/es/upload'; 4 | import { getToken } from '@/utils/authority'; 5 | import styles from './AvatarView.less'; 6 | 7 | interface AvatarViewState { 8 | value: string; 9 | } 10 | 11 | interface AvatarViewProps { 12 | value?: string; 13 | onChange?: (value: string) => void; 14 | } 15 | 16 | // 头像组件 方便以后独立,增加裁剪之类的功能 17 | class AvatarView extends React.Component { 18 | static getDerivedStateFromProps(nextProps: AvatarViewProps) { 19 | return { 20 | value: nextProps.value, 21 | }; 22 | } 23 | 24 | state: AvatarViewState = { 25 | value: '', 26 | }; 27 | 28 | render() { 29 | const uploadProps = { 30 | name: 'file', 31 | action: UPLOAD_URL, 32 | accept: 'image/*', 33 | showUploadList: false, 34 | headers: { 35 | authorization: getToken(), 36 | }, 37 | onChange: (info: UploadChangeParam) => { 38 | if (info.file.status === 'done') { 39 | this.setState({ value: info.file.response.data.fileUrl }); 40 | message.success(`${info.file.name} file uploaded successfully`); 41 | const { onChange } = this.props; 42 | if (onChange) { 43 | onChange(info.file.response.data.url); 44 | } 45 | } else if (info.file.status === 'error') { 46 | message.error(`${info.file.name} file upload failed.`); 47 | } 48 | }, 49 | }; 50 | 51 | const { value } = this.state; 52 | 53 | return ( 54 | 55 |
头像
56 |
57 | avatar 58 |
59 | 60 |
61 | 62 |
63 |
64 |
65 | ); 66 | } 67 | } 68 | 69 | export default AvatarView; 70 | -------------------------------------------------------------------------------- /src/pages/account/settings/components/BaseView.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .baseView { 4 | display: flex; 5 | padding-top: 12px; 6 | 7 | .left { 8 | max-width: 448px; 9 | } 10 | .right { 11 | flex: 1; 12 | padding-left: 104px; 13 | } 14 | } 15 | 16 | @media screen and (max-width: @screen-xl) { 17 | .baseView { 18 | flex-direction: column-reverse; 19 | 20 | .right { 21 | display: flex; 22 | flex-direction: column; 23 | align-items: center; 24 | max-width: 448px; 25 | padding: 20px; 26 | .avatar_title { 27 | display: none; 28 | } 29 | } 30 | } 31 | } 32 | 33 | @media screen and (min-width: @screen-xl) { 34 | .baseView { 35 | .left { 36 | width: 300px; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/pages/account/settings/components/GeographicView.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .row { 4 | .item { 5 | width: 50%; 6 | max-width: 220px; 7 | } 8 | .item:first-child { 9 | width: ~'calc(50% - 8px)'; 10 | margin-right: 8px; 11 | } 12 | } 13 | 14 | @media screen and (max-width: @screen-sm) { 15 | .item:first-child { 16 | margin: 0; 17 | margin-bottom: 8px; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/pages/account/settings/components/GeographicView.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useRequest } from 'umi'; 3 | import { Select, Spin } from 'antd'; 4 | import * as services from '../services'; 5 | import { GeographicItemType } from '../data'; 6 | import styles from './GeographicView.less'; 7 | 8 | interface GeographicViewProps { 9 | value?: { 10 | province: SelectItem; 11 | city: SelectItem; 12 | }; 13 | onChange?: (value: any) => void; 14 | } 15 | 16 | interface SelectItem { 17 | label: string; 18 | key: string; 19 | } 20 | 21 | const nullSelectItem: SelectItem = { 22 | label: '', 23 | key: '', 24 | }; 25 | 26 | const GeographicView: React.FC = (props) => { 27 | const { loading: provinceLoading, data: province = [nullSelectItem] } = useRequest( 28 | services.queryProvince, 29 | ); 30 | const { 31 | loading: cityLoading, 32 | data: city = [nullSelectItem], 33 | run: fetchCity, 34 | } = useRequest(services.queryCity, { manual: true }); 35 | 36 | function getOption(list: GeographicItemType[]) { 37 | if (!list || list.length < 1) { 38 | return ( 39 | 40 | 没有找到选项 41 | 42 | ); 43 | } 44 | return list.map((item) => ( 45 | 46 | {item.name} 47 | 48 | )); 49 | } 50 | 51 | function getProvinceOption() { 52 | if (province) { 53 | return getOption(province); 54 | } 55 | return []; 56 | } 57 | 58 | async function selectProvinceItem(item: SelectItem) { 59 | await fetchCity(item.key); 60 | console.info(city); 61 | props.onChange!({ 62 | province: item, 63 | city: nullSelectItem, 64 | }); 65 | } 66 | 67 | function selectCityItem(item: SelectItem) { 68 | props.onChange!({ 69 | province: props.value?.province, 70 | city: item, 71 | }); 72 | } 73 | 74 | function getCityOption() { 75 | if (city) { 76 | return getOption(city); 77 | } 78 | return []; 79 | } 80 | 81 | return ( 82 | 83 | 92 | 101 | 102 | ); 103 | }; 104 | 105 | export default GeographicView; 106 | -------------------------------------------------------------------------------- /src/pages/account/settings/components/notification.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button, Form, List, message, Switch } from 'antd'; 3 | import { connect, useRequest } from 'umi'; 4 | import { AuthModelState, ConnectProps, ConnectState } from '@/models/connect'; 5 | import { IUser, ResponseResultType } from '@/models/I'; 6 | import * as services from '../services'; 7 | 8 | interface NotificationViewState extends Partial { 9 | auth?: AuthModelState; 10 | } 11 | 12 | const NotificationView: React.FC = (props) => { 13 | const { loading, run: updateSettings } = useRequest>( 14 | services.updateSettings, 15 | { 16 | manual: true, 17 | onSuccess(data) { 18 | props.dispatch!({ 19 | type: 'auth/setUser', 20 | user: data, 21 | }); 22 | message.success('修改成功!'); 23 | }, 24 | }, 25 | ); 26 | 27 | async function handleSubmit(values: object) { 28 | await updateSettings(values); 29 | } 30 | 31 | return ( 32 |
42 | 43 | 51 | 52 | , 53 | ], 54 | }, 55 | { 56 | title: '点赞通知', 57 | description: '系统在你离线时将以邮件的形式通知', 58 | actions: [ 59 | 60 | 61 | , 62 | ], 63 | }, 64 | ]} 65 | renderItem={(item) => ( 66 | 67 | 68 | 69 | )} 70 | /> 71 | 72 | 73 | 76 | 77 |
78 | ); 79 | }; 80 | 81 | export default connect(({ auth }: ConnectState) => ({ auth }))(NotificationView); 82 | -------------------------------------------------------------------------------- /src/pages/account/settings/data.d.ts: -------------------------------------------------------------------------------- 1 | export interface TagType { 2 | key: string; 3 | label: string; 4 | } 5 | 6 | export interface GeographicItemType { 7 | name: string; 8 | id: string; 9 | } 10 | 11 | export interface GeographicType { 12 | province: GeographicItemType; 13 | city: GeographicItemType; 14 | } 15 | 16 | export interface NoticeType { 17 | id: string; 18 | title: string; 19 | logo: string; 20 | description: string; 21 | updatedAt: string; 22 | member: string; 23 | href: string; 24 | memberLink: string; 25 | } 26 | -------------------------------------------------------------------------------- /src/pages/account/settings/geographic/province.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "北京市", 4 | "id": "110000" 5 | }, 6 | { 7 | "name": "天津市", 8 | "id": "120000" 9 | }, 10 | { 11 | "name": "河北省", 12 | "id": "130000" 13 | }, 14 | { 15 | "name": "山西省", 16 | "id": "140000" 17 | }, 18 | { 19 | "name": "内蒙古自治区", 20 | "id": "150000" 21 | }, 22 | { 23 | "name": "辽宁省", 24 | "id": "210000" 25 | }, 26 | { 27 | "name": "吉林省", 28 | "id": "220000" 29 | }, 30 | { 31 | "name": "黑龙江省", 32 | "id": "230000" 33 | }, 34 | { 35 | "name": "上海市", 36 | "id": "310000" 37 | }, 38 | { 39 | "name": "江苏省", 40 | "id": "320000" 41 | }, 42 | { 43 | "name": "浙江省", 44 | "id": "330000" 45 | }, 46 | { 47 | "name": "安徽省", 48 | "id": "340000" 49 | }, 50 | { 51 | "name": "福建省", 52 | "id": "350000" 53 | }, 54 | { 55 | "name": "江西省", 56 | "id": "360000" 57 | }, 58 | { 59 | "name": "山东省", 60 | "id": "370000" 61 | }, 62 | { 63 | "name": "河南省", 64 | "id": "410000" 65 | }, 66 | { 67 | "name": "湖北省", 68 | "id": "420000" 69 | }, 70 | { 71 | "name": "湖南省", 72 | "id": "430000" 73 | }, 74 | { 75 | "name": "广东省", 76 | "id": "440000" 77 | }, 78 | { 79 | "name": "广西壮族自治区", 80 | "id": "450000" 81 | }, 82 | { 83 | "name": "海南省", 84 | "id": "460000" 85 | }, 86 | { 87 | "name": "重庆市", 88 | "id": "500000" 89 | }, 90 | { 91 | "name": "四川省", 92 | "id": "510000" 93 | }, 94 | { 95 | "name": "贵州省", 96 | "id": "520000" 97 | }, 98 | { 99 | "name": "云南省", 100 | "id": "530000" 101 | }, 102 | { 103 | "name": "西藏自治区", 104 | "id": "540000" 105 | }, 106 | { 107 | "name": "陕西省", 108 | "id": "610000" 109 | }, 110 | { 111 | "name": "甘肃省", 112 | "id": "620000" 113 | }, 114 | { 115 | "name": "青海省", 116 | "id": "630000" 117 | }, 118 | { 119 | "name": "宁夏回族自治区", 120 | "id": "640000" 121 | }, 122 | { 123 | "name": "新疆维吾尔自治区", 124 | "id": "650000" 125 | }, 126 | { 127 | "name": "台湾省", 128 | "id": "710000" 129 | }, 130 | { 131 | "name": "香港特别行政区", 132 | "id": "810000" 133 | }, 134 | { 135 | "name": "澳门特别行政区", 136 | "id": "820000" 137 | } 138 | ] 139 | -------------------------------------------------------------------------------- /src/pages/account/settings/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { GridContent } from '@ant-design/pro-layout'; 3 | import { Menu } from 'antd'; 4 | import { ProfileOutlined, MailOutlined, SafetyOutlined } from '@ant-design/icons'; 5 | import { ConnectProps } from '@/models/connect'; 6 | import BaseView from './components/base'; 7 | import NotificationView from './components/notification'; 8 | import SecurityView from './components/security'; 9 | import styles from './style.less'; 10 | 11 | interface SettingsProps extends ConnectProps {} 12 | 13 | type StateKeysType = 'base' | 'notification' | 'security'; 14 | 15 | const menuMap: { [key: string]: React.ReactNode } = { 16 | base: ( 17 | 18 | 19 | 基本设置 20 | 21 | ), 22 | notification: ( 23 | 24 | 25 | 通知设置 26 | 27 | ), 28 | security: ( 29 | 30 | 31 | 修改密码 32 | 33 | ), 34 | }; 35 | 36 | const Settings: React.FC = () => { 37 | const [mode, setMode] = useState<'inline' | 'horizontal'>(); 38 | const [selectKey, setSelectKey] = useState('base'); 39 | 40 | function resize() { 41 | requestAnimationFrame(() => setMode(window.innerWidth < 768 ? 'horizontal' : 'inline')); 42 | } 43 | 44 | useEffect(() => { 45 | window.addEventListener('resize', resize); 46 | resize(); 47 | return () => window.removeEventListener('resize', resize); 48 | }, []); 49 | 50 | function renderChildren() { 51 | switch (selectKey) { 52 | case 'base': 53 | return ; 54 | case 'notification': 55 | return ; 56 | case 'security': 57 | return ; 58 | default: 59 | return null; 60 | } 61 | } 62 | 63 | return ( 64 | 65 |
66 |
67 | setSelectKey(key as StateKeysType)} 71 | > 72 | {Object.entries(menuMap).map(([key, value]) => ( 73 | {value} 74 | ))} 75 | 76 |
77 |
78 |
{menuMap[selectKey]}
79 | {renderChildren()} 80 |
81 |
82 |
83 | ); 84 | }; 85 | 86 | export default Settings; 87 | -------------------------------------------------------------------------------- /src/pages/account/settings/services.ts: -------------------------------------------------------------------------------- 1 | import { request } from 'umi'; 2 | // @ts-ignore 3 | import city from './geographic/city.json'; 4 | // @ts-ignore 5 | import province from './geographic/province.json'; 6 | 7 | export async function queryProvince() { 8 | return { data: province }; 9 | } 10 | 11 | export async function queryCity(p: string) { 12 | return { data: city[p] }; 13 | } 14 | 15 | export async function updateBaseInfo(params: object) { 16 | return request('user/base_info', { 17 | method: 'POST', 18 | data: params, 19 | }); 20 | } 21 | 22 | export async function updateSettings(params: object) { 23 | return request('user/settings', { 24 | method: 'POST', 25 | data: params, 26 | }); 27 | } 28 | 29 | export async function updatePassword(params: object) { 30 | return request('user/password', { 31 | method: 'POST', 32 | data: params, 33 | }); 34 | } 35 | 36 | export async function sendEmailCode(email: string) { 37 | return request('user/send_email_code', { 38 | method: 'POST', 39 | data: { email }, 40 | }); 41 | } 42 | -------------------------------------------------------------------------------- /src/pages/account/settings/style.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .main { 4 | display: flex; 5 | width: 100%; 6 | height: 100%; 7 | padding-top: 16px; 8 | padding-bottom: 16px; 9 | overflow: auto; 10 | background-color: @menu-bg; 11 | .leftMenu { 12 | width: 224px; 13 | border-right: @border-width-base @border-style-base @border-color-split; 14 | :global { 15 | .ant-menu-inline { 16 | border: none; 17 | } 18 | .ant-menu:not(.ant-menu-horizontal) .ant-menu-item-selected { 19 | font-weight: bold; 20 | } 21 | } 22 | } 23 | .right { 24 | flex: 1; 25 | padding: 8px 40px; 26 | .title { 27 | margin-bottom: 12px; 28 | color: @heading-color; 29 | font-weight: 500; 30 | font-size: 20px; 31 | line-height: 28px; 32 | } 33 | :global { 34 | .anticon { 35 | margin-right: 8px; 36 | } 37 | } 38 | } 39 | :global { 40 | .ant-list-split .ant-list-item:last-child { 41 | border-bottom: 1px solid @border-color-split; 42 | } 43 | .ant-list-item { 44 | padding-top: 14px; 45 | padding-bottom: 14px; 46 | } 47 | } 48 | } 49 | 50 | @media screen and (max-width: @screen-md) { 51 | .main { 52 | flex-direction: column; 53 | padding-top: 8px; 54 | .leftMenu { 55 | width: 100%; 56 | border: none; 57 | :global { 58 | .ant-menu-item { 59 | padding-left: 12px !important; 60 | } 61 | } 62 | } 63 | .right { 64 | padding: 24px 12px 0 12px; 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/pages/articles/create/services.ts: -------------------------------------------------------------------------------- 1 | import { request } from 'umi'; 2 | 3 | export async function storeArticle(params: object) { 4 | return request('articles', { 5 | method: 'POST', 6 | data: params, 7 | }); 8 | } 9 | -------------------------------------------------------------------------------- /src/pages/articles/create/style.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .previewUploader { 4 | :global { 5 | .ant-upload.ant-upload-select-picture-card { 6 | width: auto; 7 | min-width: 100px; 8 | height: auto; 9 | min-height: 100px; 10 | margin: 0; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/pages/articles/edit/services.ts: -------------------------------------------------------------------------------- 1 | import { request } from 'umi'; 2 | 3 | export async function updateArticle(id: number | string, data: object) { 4 | return request(`articles/${id}`, { 5 | method: 'PUT', 6 | data, 7 | }); 8 | } 9 | 10 | export async function queryArticle(id: number | string, params?: object) { 11 | return request(`articles/${id}`, { 12 | params, 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /src/pages/articles/edit/style.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .previewUploader { 4 | :global { 5 | .ant-upload.ant-upload-select-picture-card { 6 | width: auto; 7 | min-width: 100px; 8 | height: auto; 9 | min-height: 100px; 10 | margin: 0; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/pages/articles/list/components/ArticleListContent/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .listContent { 4 | .description { 5 | max-width: 720px; 6 | line-height: 22px; 7 | } 8 | .extra { 9 | margin-top: 16px; 10 | color: @text-color-secondary; 11 | line-height: 22px; 12 | & > :global(.ant-avatar) { 13 | position: relative; 14 | top: 1px; 15 | width: 20px; 16 | height: 20px; 17 | margin-right: 8px; 18 | vertical-align: top; 19 | } 20 | & > em { 21 | margin-left: 16px; 22 | color: @disabled-color; 23 | font-style: normal; 24 | } 25 | } 26 | } 27 | 28 | @media screen and (max-width: @screen-xs) { 29 | .listContent { 30 | .extra { 31 | & > em { 32 | display: block; 33 | margin-top: 8px; 34 | margin-left: 0; 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/pages/articles/list/components/ArticleListContent/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Tooltip } from 'antd'; 3 | import { 4 | UserOutlined, 5 | ClockCircleOutlined, 6 | EyeOutlined, 7 | LikeOutlined, 8 | MessageOutlined, 9 | } from '@ant-design/icons'; 10 | import Ellipsis from '@/components/Ellipsis'; 11 | import { IArticle } from '@/models/I'; 12 | import styles from './index.less'; 13 | 14 | interface ArticleListContentProps { 15 | data: IArticle; 16 | } 17 | 18 | const ArticleListContent: React.FC = ({ data: article }) => ( 19 |
20 |
21 | 22 | {article.highlights?.content 23 | ? article.highlights.content.map((html, key) => ( 24 | 30 | )) 31 | : article.content?.combine_markdown?.substr(0, 300)} 32 | 33 |
34 |
35 | 36 | 37 | {article.user?.username} 38 | 39 | 40 | 41 | 42 | {article.created_at_timeago} 43 | 44 | 45 | 46 | 47 | {article.friendly_views_count} 48 | 49 | 50 | 51 | {article.friendly_likes_count} 52 | 53 | 54 | 55 | {article.friendly_comments_count} 56 | 57 |
58 |
59 | ); 60 | 61 | export default ArticleListContent; 62 | -------------------------------------------------------------------------------- /src/pages/articles/list/components/StandardFormRow/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .standardFormRow { 4 | display: flex; 5 | width: 100%; 6 | margin-bottom: 16px; 7 | padding-bottom: 16px; 8 | border-bottom: 1px dashed @border-color-split; 9 | :global { 10 | .ant-form-item, 11 | .ant-legacy-form-item { 12 | margin-right: 24px; 13 | } 14 | .ant-form-item-label, 15 | .ant-legacy-form-item-label { 16 | label { 17 | margin-right: 0; 18 | color: @text-color; 19 | } 20 | } 21 | .ant-form-item-label, 22 | .ant-legacy-form-item-label, 23 | .ant-form-item-control, 24 | .ant-legacy-form-item-control { 25 | padding: 0; 26 | line-height: 32px; 27 | } 28 | } 29 | .label { 30 | flex: 0 0 auto; 31 | margin-right: 24px; 32 | color: @heading-color; 33 | font-size: @font-size-base; 34 | text-align: right; 35 | & > span { 36 | display: inline-block; 37 | height: 32px; 38 | line-height: 32px; 39 | &::after { 40 | content: ':'; 41 | } 42 | } 43 | } 44 | .content { 45 | flex: 1 1 0; 46 | :global { 47 | .ant-form-item, 48 | .ant-legacy-form-item { 49 | &:last-child { 50 | display: block; 51 | margin-right: 0; 52 | } 53 | } 54 | } 55 | } 56 | } 57 | 58 | .standardFormRowLast { 59 | margin-bottom: 0; 60 | padding-bottom: 0; 61 | border: none; 62 | } 63 | 64 | .standardFormRowBlock { 65 | :global { 66 | .ant-form-item, 67 | .ant-legacy-form-item, 68 | div.ant-form-item-control-wrapper, 69 | div.ant-legacy-form-item-control-wrapper { 70 | display: block; 71 | } 72 | } 73 | } 74 | 75 | .standardFormRowGrid { 76 | :global { 77 | .ant-form-item, 78 | .ant-legacy-form-item, 79 | div.ant-form-item-control-wrapper, 80 | div.ant-legacy-form-item-control-wrapper { 81 | display: block; 82 | } 83 | .ant-form-item-label, 84 | .ant-legacy-form-item-label { 85 | float: left; 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/pages/articles/list/components/StandardFormRow/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | import styles from './index.less'; 4 | 5 | interface StandardFormRowProps { 6 | title?: string; 7 | last?: boolean; 8 | block?: boolean; 9 | grid?: boolean; 10 | style?: React.CSSProperties; 11 | } 12 | 13 | const StandardFormRow: React.FC = ({ 14 | title, 15 | children, 16 | last, 17 | block, 18 | grid, 19 | ...rest 20 | }) => { 21 | const cls = classNames(styles.standardFormRow, { 22 | [styles.standardFormRowBlock]: block, 23 | [styles.standardFormRowLast]: last, 24 | [styles.standardFormRowGrid]: grid, 25 | }); 26 | 27 | return ( 28 |
29 | {title && ( 30 |
31 | {title} 32 |
33 | )} 34 |
{children}
35 |
36 | ); 37 | }; 38 | 39 | export default StandardFormRow; 40 | -------------------------------------------------------------------------------- /src/pages/articles/list/components/TagSelect/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .tagSelect { 4 | position: relative; 5 | max-height: 32px; 6 | margin-left: -8px; 7 | overflow: hidden; 8 | line-height: 32px; 9 | transition: all 0.3s; 10 | user-select: none; 11 | :global { 12 | .ant-tag { 13 | margin-right: 24px; 14 | padding: 0 8px; 15 | font-size: @font-size-base; 16 | } 17 | } 18 | &.expanded { 19 | max-height: 200px; 20 | transition: all 0.3s; 21 | } 22 | .trigger { 23 | position: absolute; 24 | top: 0; 25 | right: 0; 26 | span.anticon { 27 | font-size: 12px; 28 | } 29 | } 30 | &.hasExpandTag { 31 | padding-right: 50px; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/pages/articles/list/services.ts: -------------------------------------------------------------------------------- 1 | import { request } from 'umi'; 2 | import { stringify } from 'qs'; 3 | 4 | export async function queryArticles(params: object) { 5 | return request(`articles?${stringify(params)}`); 6 | } 7 | -------------------------------------------------------------------------------- /src/pages/articles/list/style.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .listCard { 4 | margin-top: 24px; 5 | } 6 | 7 | .list { 8 | :global { 9 | .ant-list-item { 10 | padding-right: 0; 11 | padding-left: 0; 12 | } 13 | 14 | .ant-list-item-main { 15 | display: flex; 16 | flex-direction: column; 17 | } 18 | 19 | .ant-list-item-action { 20 | margin-left: 0; 21 | } 22 | 23 | .ant-list-item-extra { 24 | margin-left: 60px; 25 | } 26 | } 27 | } 28 | 29 | a.listItemMetaTitle { 30 | color: @heading-color; 31 | 32 | strong { 33 | color: @volcano-6; 34 | } 35 | 36 | em { 37 | color: @volcano-6; 38 | font-style: normal; 39 | } 40 | } 41 | 42 | .listItemExtra { 43 | width: 320px; 44 | overflow: hidden; 45 | 46 | .preview { 47 | position: absolute; 48 | width: 320px; 49 | height: 180px; 50 | object-fit: cover; 51 | background-repeat: no-repeat; 52 | background-position: center; 53 | } 54 | 55 | img { 56 | max-width: 100%; 57 | max-height: 100%; 58 | } 59 | } 60 | 61 | .selfTrigger { 62 | margin-left: 12px; 63 | } 64 | 65 | @media screen and (max-width: @screen-xs) { 66 | .selfTrigger { 67 | display: block; 68 | margin-left: 0; 69 | } 70 | } 71 | 72 | @media screen and (max-width: @screen-md) { 73 | .selfTrigger { 74 | display: block; 75 | margin-left: 0; 76 | } 77 | 78 | .listCard { 79 | margin-top: 12px; 80 | } 81 | 82 | .list :global(.ant-list-item-extra) { 83 | margin-left: 0; 84 | } 85 | } 86 | 87 | @media screen and (max-width: @screen-lg) { 88 | .listItemExtra { 89 | width: 0; 90 | height: 1px; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/pages/articles/show/components/ArticleComments/Editor.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .editor { 4 | background-color: #fff; 5 | border: 1px solid @border-color-base; 6 | border-radius: @border-radius-base; 7 | } 8 | 9 | .textarea textarea, .textarea :global(.ant-mentions) { 10 | border: none; 11 | } 12 | 13 | .mentionsAvatar { 14 | width: 24px; 15 | height: 24px; 16 | margin-right: 8px; 17 | border-radius: 50%; 18 | } 19 | 20 | .toolbar { 21 | display: flex; 22 | flex-direction: row; 23 | align-items: center; 24 | justify-content: space-between; 25 | padding: 12px; 26 | background-color: #fafafa; 27 | 28 | .actions { 29 | display: flex; 30 | flex-direction: row; 31 | align-items: center; 32 | } 33 | 34 | .action { 35 | padding: 0 8px; 36 | } 37 | } 38 | 39 | .avatar { 40 | margin-right: 12px; 41 | } 42 | 43 | .upload, .emojiPickerBtn { 44 | width: 20px; 45 | cursor: pointer; 46 | 47 | :global(.anticon) { 48 | font-size: 20px; 49 | } 50 | } 51 | 52 | .submitBtn { 53 | width: 100px; 54 | } 55 | 56 | .emojiPickerPopup { 57 | :global { 58 | .emoji { 59 | width: 40px; 60 | height: 40px; 61 | } 62 | } 63 | } 64 | 65 | .preview { 66 | margin-top: 24px; 67 | padding: 12px; 68 | overflow: hidden; 69 | line-height: 21px; 70 | word-break: break-all; 71 | border: 1px dashed @border-color-base; 72 | border-radius: @border-radius-base; 73 | 74 | :global(.markdown-body) { 75 | > p:last-child { 76 | margin-bottom: 0; 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/pages/articles/show/components/ArticleComments/style.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .list { 4 | margin-top: 20px; 5 | 6 | :global { 7 | .ant-comment { 8 | width: 100%; 9 | 10 | .markdown-body { 11 | background-color: transparent; 12 | } 13 | } 14 | 15 | .ant-list-item { 16 | border-bottom-style: dashed; 17 | } 18 | 19 | .ant-comment-content-detail { 20 | //overflow: hidden; 21 | 22 | p { 23 | margin-bottom: 8px; 24 | } 25 | } 26 | 27 | .ant-comment-actions { 28 | margin-top: 0; 29 | } 30 | 31 | .ant-comment-actions > li > span { 32 | display: inline-block; 33 | } 34 | 35 | .ant-comment-nested { 36 | background-color: #f7f7f7; 37 | } 38 | 39 | .ant-comment-nested .ant-comment { 40 | padding: 0 12px; 41 | 42 | .ant-comment-content-author-name, .ant-comment-content-author-time { 43 | height: 22px; 44 | line-height: 22px; 45 | } 46 | } 47 | 48 | .ant-tag { 49 | margin: 0 0 0 4px; 50 | padding: 0 5px; 51 | color: @primary-color; 52 | line-height: 1.4; 53 | border-color: @primary-color; 54 | } 55 | } 56 | } 57 | 58 | .loadMoreReplysBtn { 59 | padding-bottom: @padding-md; 60 | background-color: #f7f7f7; 61 | 62 | a { 63 | margin-left: 12px; 64 | } 65 | } 66 | 67 | .topComment { 68 | background-color: #fffbe6; 69 | 70 | .loadMoreReplysBtn { 71 | background-color: #fffbe6; 72 | } 73 | 74 | :global(.ant-comment-nested) { 75 | background-color: #fffbe6; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/pages/articles/show/components/ArticleContent/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Tag, Tooltip } from 'antd'; 3 | import { ClockCircleOutlined, EyeOutlined, TagsFilled } from '@ant-design/icons'; 4 | import { Link } from 'umi'; 5 | // @ts-ignore 6 | import MarkdownBody from '@/components/MarkdownBody'; 7 | import LikeBtn from '@/components/Buttons/LikeBtn'; 8 | import FavoriteBtn from '@/components/Buttons/FavoriteBtn'; 9 | import Tocify from '@/components/MarkdownBody/tocify'; 10 | import { IArticle } from '@/models/I'; 11 | import styles from './style.less'; 12 | 13 | interface ArticleContentProps { 14 | article?: IArticle; 15 | getTocify?: (tocify: Tocify) => void; 16 | shouldUpdate?: boolean; 17 | } 18 | 19 | export default class ArticleContent extends React.Component { 20 | shouldComponentUpdate(nextProps: Readonly) { 21 | return !!nextProps.shouldUpdate; 22 | } 23 | 24 | render() { 25 | const { article } = this.props; 26 | 27 | if (!article?.content || !article.user) { 28 | return null; 29 | } 30 | 31 | return ( 32 |
33 |
34 |

{article.title}

35 |
36 | {article.user.username} 37 | 38 | 39 | 40 | 41 | {article.created_at_timeago} 42 | 43 | 44 | 45 | 46 | 47 | {article.friendly_views_count} 阅读 48 | 49 |
50 |
51 | 52 |
53 | 59 |
60 | 61 |
62 | 63 | {article.tags?.map((tag) => ( 64 | 65 | {tag.name} 66 | 67 | ))} 68 |
69 | 70 |
71 | 72 | 73 |
74 |
75 | ); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/pages/articles/show/components/ArticleContent/style.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .header { 4 | margin-right: -24px; 5 | margin-left: -24px; 6 | padding: 0 24px 12px 24px; 7 | background: white; 8 | border-bottom: 1px solid @border-color-base; 9 | 10 | > h1 { 11 | font-size: @heading-3-size; 12 | } 13 | 14 | .meta { 15 | color: @text-color-secondary; 16 | font-size: 14px; 17 | } 18 | } 19 | 20 | .content { 21 | padding-top: 24px; 22 | } 23 | 24 | .tags { 25 | display: flex; 26 | align-items: center; 27 | justify-content: flex-start; 28 | margin-top: 40px; 29 | 30 | a div { 31 | cursor: pointer; 32 | } 33 | } 34 | 35 | .actions { 36 | display: flex; 37 | align-items: center; 38 | justify-content: center; 39 | margin-top: 40px; 40 | 41 | :global(.btn) { 42 | margin: 0 10px; 43 | } 44 | 45 | :global(.anticon) { 46 | font-size: 40px; 47 | } 48 | } 49 | 50 | @media (max-width: @screen-md) { 51 | .header { 52 | margin-right: -12px; 53 | margin-left: -12px; 54 | padding: 0 12px 12px 12px; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/pages/articles/show/services.ts: -------------------------------------------------------------------------------- 1 | import { request } from 'umi'; 2 | 3 | export async function queryArticle(id: number | string, params?: object) { 4 | return request(`articles/${id}`, { 5 | params, 6 | }); 7 | } 8 | 9 | export async function queryArticleComments(article_id: number | string, params?: object) { 10 | return request(`articles/${article_id}/comments`, { 11 | params, 12 | }); 13 | } 14 | 15 | export async function storeArticleComment(article_id: number | string, params: object) { 16 | return request(`articles/${article_id}/comments`, { 17 | method: 'post', 18 | data: params, 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /src/pages/articles/show/style.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .tocifyBox { 4 | background-color: white; 5 | 6 | :global { 7 | .ant-skeleton-content { 8 | padding: @padding-lg; 9 | } 10 | 11 | .ant-anchor-wrapper { 12 | margin-left: 0; 13 | } 14 | } 15 | } 16 | 17 | .commentCard { 18 | margin-top: 24px; 19 | } 20 | 21 | @media (max-width: @screen-md) { 22 | .commentCard { 23 | margin-top: 12px; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/pages/auth/login/services.ts: -------------------------------------------------------------------------------- 1 | import { request } from 'umi'; 2 | 3 | export async function getLoginCode() { 4 | return request('auth/login_code'); 5 | } 6 | -------------------------------------------------------------------------------- /src/pages/auth/login/style.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .main { 4 | width: 368px; 5 | margin: 60px auto 0; 6 | padding: @padding-md @padding-lg; 7 | background-color: white; 8 | box-shadow: @shadow-1-down; 9 | 10 | :global { 11 | .ant-tabs .ant-tabs-bar { 12 | margin-bottom: 24px; 13 | text-align: center; 14 | } 15 | 16 | .antd-pro-login-submit { 17 | width: 100%; 18 | margin-top: 24px; 19 | } 20 | } 21 | 22 | @media screen and (max-width: @screen-md) { 23 | width: 94%; 24 | margin-top: 0; 25 | } 26 | 27 | .qrcodeBox { 28 | text-align: center; 29 | 30 | .noticeTitle { 31 | display: inline-block; 32 | margin-top: 60px; 33 | margin-left: 12px; 34 | font-size: 28px; 35 | vertical-align: middle; 36 | } 37 | 38 | .noticeBtn { 39 | width: 90%; 40 | margin-top: 20px; 41 | margin-bottom: 80px; 42 | } 43 | 44 | p { 45 | margin-bottom: 0.2em; 46 | color: rgba(0, 0, 0, 0.45); 47 | font-size: 14px; 48 | } 49 | 50 | img { 51 | margin: 24px auto 36px auto; 52 | } 53 | 54 | :global { 55 | .anticon.anticon-close-circle { 56 | margin-top: 60px; 57 | color: @error-color; 58 | font-size: 40px; 59 | vertical-align: middle; 60 | } 61 | 62 | .ant-spin-spinning { 63 | margin-top: 24px; 64 | margin-bottom: 60px; 65 | } 66 | .anticon-loading { 67 | margin-bottom: 24px; 68 | color: @primary-color; 69 | font-size: 70px; 70 | } 71 | .ant-spin-text { 72 | font-weight: bold; 73 | } 74 | } 75 | } 76 | 77 | .title { 78 | padding-bottom: @padding-md; 79 | font-weight: 600; 80 | font-size: @heading-3-size; 81 | text-align: center; 82 | } 83 | 84 | .icon { 85 | margin-left: 16px; 86 | color: rgba(0, 0, 0, 0.2); 87 | font-size: 24px; 88 | vertical-align: middle; 89 | cursor: pointer; 90 | transition: color 0.3s; 91 | 92 | &:hover { 93 | color: @primary-color; 94 | } 95 | } 96 | 97 | .other { 98 | margin-top: 24px; 99 | line-height: 22px; 100 | text-align: left; 101 | 102 | .register { 103 | float: right; 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/pages/comments/list/services.ts: -------------------------------------------------------------------------------- 1 | import { request } from 'umi'; 2 | 3 | export async function queryComments(params?: object) { 4 | return request('comments', { 5 | params, 6 | }); 7 | } 8 | 9 | export async function updateComment(id: number | string, data: object) { 10 | return request(`comments/${id}`, { 11 | method: 'PUT', 12 | data, 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /src/pages/comments/list/style.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .table { 4 | :global { 5 | .editable-cell { 6 | position: relative; 7 | } 8 | 9 | .editable-cell-value-wrap { 10 | padding: 5px 12px; 11 | cursor: pointer; 12 | } 13 | 14 | .editable-row:hover .editable-cell-value-wrap { 15 | padding: 4px 11px; 16 | border: 1px solid #d9d9d9; 17 | border-radius: 4px; 18 | } 19 | 20 | [data-theme='dark'] .editable-row:hover .editable-cell-value-wrap { 21 | border: 1px solid #434343; 22 | } 23 | } 24 | } 25 | 26 | .username, 27 | .articleTitle, 28 | .content { 29 | overflow: hidden; 30 | white-space: nowrap; 31 | text-overflow: ellipsis; 32 | word-break: break-all; 33 | } 34 | 35 | .username { 36 | max-width: 80px; 37 | } 38 | 39 | .articleTitle { 40 | max-width: 200px; 41 | } 42 | 43 | .content { 44 | max-width: 280px; 45 | } 46 | -------------------------------------------------------------------------------- /src/pages/demos/emojipicker/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { GridContent } from '@ant-design/pro-layout'; 3 | import { Card, Form } from 'antd'; 4 | import Editor from '@/pages/articles/show/components/ArticleComments/Editor'; 5 | import styles from './style.less'; 6 | 7 | interface EmojiPickerProps {} 8 | 9 | const formLayout = { 10 | labelCol: { 11 | xs: { span: 24 }, 12 | sm: { span: 4 }, 13 | }, 14 | wrapperCol: { 15 | xs: { span: 24 }, 16 | sm: { span: 16 }, 17 | }, 18 | }; 19 | 20 | const EmojiPicker: React.FC = () => { 21 | const [submitting, setSubmitting] = useState(false); 22 | 23 | const editorProps = { 24 | className: styles.commentEditorBox, 25 | submitting, 26 | onSubmit: async (values: any) => { 27 | setSubmitting(true); 28 | // eslint-disable-next-line no-console 29 | console.info(values); 30 | setTimeout(() => setSubmitting(false), 3000); 31 | }, 32 | minRows: 8, 33 | preview: true, 34 | }; 35 | 36 | return ( 37 | 38 | 39 |
40 | 41 | 42 | 43 |
44 |
45 |
46 | ); 47 | }; 48 | 49 | export default EmojiPicker; 50 | -------------------------------------------------------------------------------- /src/pages/demos/emojipicker/style.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .emojiPickerCard { 4 | min-height: 560px; 5 | } 6 | -------------------------------------------------------------------------------- /src/pages/demos/simplemdeeditor/style.less: -------------------------------------------------------------------------------- 1 | .editorCard { 2 | :global { 3 | .CodeMirror, 4 | .CodeMirror-scroll { 5 | min-height: 400px; 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/pages/permissions/list/components/CreateModal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Form, Input } from 'antd'; 3 | import ModalForm, { ModalFormProps } from '@/components/ModalForm'; 4 | 5 | export interface CreateModalProps extends ModalFormProps {} 6 | 7 | const formItemLayout = { 8 | labelCol: { span: 6 }, 9 | wrapperCol: { span: 15 }, 10 | }; 11 | 12 | const CreateModal: React.FC = (props) => { 13 | return ( 14 | 15 | 22 | 23 | 24 | 31 | 32 | 33 | 34 | ); 35 | }; 36 | 37 | export default CreateModal; 38 | -------------------------------------------------------------------------------- /src/pages/permissions/list/components/UpdateModal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Form, Input } from 'antd'; 3 | import ModalForm, { ModalFormProps } from '@/components/ModalForm'; 4 | 5 | export interface UpdateModalProps extends ModalFormProps {} 6 | 7 | const formItemLayout = { 8 | labelCol: { span: 6 }, 9 | wrapperCol: { span: 15 }, 10 | }; 11 | 12 | const UpdateModal: React.FC = (props) => { 13 | return ( 14 | 15 | 22 | 23 | 24 | 31 | 32 | 33 | 34 | ); 35 | }; 36 | 37 | export default UpdateModal; 38 | -------------------------------------------------------------------------------- /src/pages/permissions/list/services.ts: -------------------------------------------------------------------------------- 1 | import { request } from 'umi'; 2 | 3 | export async function queryPermissions(params?: object) { 4 | return request('permissions', { 5 | params, 6 | }); 7 | } 8 | 9 | export async function storePermission(data: object) { 10 | return request('permissions', { 11 | method: 'POST', 12 | data, 13 | }); 14 | } 15 | 16 | export async function updatePermission(id: number, data: object) { 17 | return request(`permissions/${id}`, { 18 | method: 'PUT', 19 | data, 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /src/pages/permissions/list/style.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .searchForm { 4 | margin-bottom: 24px; 5 | 6 | :global { 7 | .ant-form-item { 8 | display: flex; 9 | margin-right: 0; 10 | 11 | > .ant-form-item-label { 12 | width: auto; 13 | padding-right: 8px; 14 | line-height: 32px; 15 | } 16 | 17 | .ant-form-item-control { 18 | line-height: 32px; 19 | } 20 | } 21 | 22 | .ant-form-item-control-wrapper { 23 | flex: 1; 24 | } 25 | } 26 | 27 | .action { 28 | display: flex; 29 | justify-content: space-between; 30 | } 31 | 32 | .submitButtons { 33 | display: block; 34 | white-space: nowrap; 35 | } 36 | } 37 | 38 | @media screen and (max-width: @screen-md) { 39 | .searchForm { 40 | margin-bottom: 12px; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/pages/roles/list/components/CreateModal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Form, Input } from 'antd'; 3 | import ModalForm, { ModalFormProps } from '@/components/ModalForm'; 4 | 5 | const formItemLayout = { 6 | labelCol: { span: 6 }, 7 | wrapperCol: { span: 15 }, 8 | }; 9 | 10 | export interface CreateModalProps extends ModalFormProps {} 11 | 12 | const CreateModal: React.FC = (props) => { 13 | return ( 14 | 15 | 22 | 23 | 24 | 31 | 32 | 33 | 34 | ); 35 | }; 36 | 37 | export default CreateModal; 38 | -------------------------------------------------------------------------------- /src/pages/roles/list/components/UpdateModal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Form, Input } from 'antd'; 3 | import ModalForm, { ModalFormProps } from '@/components/ModalForm'; 4 | 5 | const formItemLayout = { 6 | labelCol: { span: 6 }, 7 | wrapperCol: { span: 15 }, 8 | }; 9 | 10 | export interface UpdateFormProps extends ModalFormProps {} 11 | 12 | const UpdateModal: React.FC = (props) => { 13 | return ( 14 | 15 | 22 | 23 | 24 | 31 | 32 | 33 | 34 | ); 35 | }; 36 | 37 | export default UpdateModal; 38 | -------------------------------------------------------------------------------- /src/pages/roles/list/services.ts: -------------------------------------------------------------------------------- 1 | import { request } from 'umi'; 2 | 3 | export async function queryRoles(params?: object) { 4 | return request('roles', { 5 | params, 6 | }); 7 | } 8 | 9 | export async function storeRole(data: object) { 10 | return request('roles', { 11 | method: 'POST', 12 | data, 13 | }); 14 | } 15 | 16 | export async function updateRole(id: number, data: object) { 17 | return request(`roles/${id}`, { 18 | method: 'PUT', 19 | data, 20 | }); 21 | } 22 | 23 | export async function getRolePermissions(roleId: number) { 24 | return request(`roles/${roleId}/permissions`); 25 | } 26 | 27 | export async function assignPermissions(roleId: number, data: object) { 28 | return request(`roles/${roleId}/assign_permissions`, { 29 | method: 'POST', 30 | data, 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /src/pages/roles/list/style.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .searchForm { 4 | margin-bottom: 24px; 5 | 6 | :global { 7 | .ant-form-item { 8 | display: flex; 9 | margin-right: 0; 10 | 11 | > .ant-form-item-label { 12 | width: auto; 13 | padding-right: 8px; 14 | line-height: 32px; 15 | } 16 | 17 | .ant-form-item-control { 18 | line-height: 32px; 19 | } 20 | } 21 | 22 | .ant-form-item-control-wrapper { 23 | flex: 1; 24 | } 25 | } 26 | 27 | .action { 28 | display: flex; 29 | justify-content: space-between; 30 | } 31 | 32 | .submitButtons { 33 | display: block; 34 | white-space: nowrap; 35 | } 36 | } 37 | 38 | @media screen and (max-width: @screen-md) { 39 | .searchForm { 40 | margin-bottom: 12px; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/pages/sensitivewords/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { GridContent } from '@ant-design/pro-layout'; 3 | import { useRequest } from 'umi'; 4 | import { Button, Card, Form, Input, message } from 'antd'; 5 | import { ResponseResultType } from '@/models/I'; 6 | import * as services from './services'; 7 | 8 | interface SensitiveWordsProps {} 9 | 10 | const { TextArea } = Input; 11 | 12 | const SensitiveWords: React.FC = () => { 13 | const [form] = Form.useForm(); 14 | const { loading } = useRequest>( 15 | services.fetchSensitiveWords, 16 | { 17 | onSuccess({ content }) { 18 | form.setFieldsValue({ content }); 19 | }, 20 | }, 21 | ); 22 | 23 | const { loading: submitting, run: updateSensitiveWords } = useRequest( 24 | services.updateSensitiveWords, 25 | { 26 | manual: true, 27 | onSuccess() { 28 | message.success('修改成功!'); 29 | }, 30 | }, 31 | ); 32 | 33 | async function handleSubmit(values: object) { 34 | await updateSensitiveWords(values); 35 | } 36 | 37 | return ( 38 | 39 | 40 |
41 | 42 |