├── .editorconfig ├── .env ├── .eslintrc.js ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .umirc.ts ├── README.md ├── mock └── .gitkeep ├── package.json ├── public ├── favicon.png └── images │ ├── 管理页003.gif │ ├── 编辑器页面002.gif │ └── 首页详情页001.gif ├── src ├── components │ ├── Account │ │ └── Me │ │ │ ├── index.jsx │ │ │ └── index.less │ ├── Admin │ │ ├── Article │ │ │ └── index.jsx │ │ ├── Category │ │ │ └── index.jsx │ │ ├── Comment │ │ │ └── index.jsx │ │ └── Tag │ │ │ └── index.jsx │ ├── AliOssUpload │ │ └── index.jsx │ ├── Anchor │ │ └── index.jsx │ ├── CheckTag │ │ └── index.jsx │ ├── Comment │ │ └── index.jsx │ ├── Header │ │ ├── index.jsx │ │ └── index.less │ ├── HomeArticleList │ │ └── index.jsx │ ├── Markdown │ │ ├── CodeTag │ │ │ └── index.jsx │ │ ├── HeadTag │ │ │ └── index.jsx │ │ ├── ImageTag │ │ │ └── index.jsx │ │ ├── MathInline │ │ │ └── index.jsx │ │ ├── MathTag │ │ │ └── index.jsx │ │ └── index.jsx │ ├── SiderList │ │ └── index.jsx │ ├── Tags │ │ └── index.jsx │ ├── UserAvatar │ │ └── index.jsx │ └── forms │ │ ├── LoginCommentForm.jsx │ │ └── NoLoginCommentForm.jsx ├── global.less ├── models │ ├── admin.js │ ├── article.js │ ├── user.js │ └── write.js ├── pages │ ├── 404 │ │ └── index.jsx │ ├── Account │ │ ├── index.jsx │ │ └── index.less │ ├── Admin │ │ └── index.jsx │ ├── Article │ │ ├── index.jsx │ │ ├── index.less │ │ └── markdown.css │ ├── Course │ │ ├── index.jsx │ │ └── index.less │ ├── Draft │ │ ├── index.jsx │ │ └── index.less │ ├── Home │ │ ├── index.jsx │ │ └── index.less │ ├── Login │ │ └── index.jsx │ ├── Register │ │ └── index.jsx │ ├── Write │ │ ├── index.jsx │ │ ├── index.less │ │ ├── markdown-github.css │ │ └── markdown.css │ └── WriteCourse │ │ ├── index.jsx │ │ └── index.less ├── services │ ├── admin.js │ ├── article.js │ ├── user.js │ └── write.js └── utils │ ├── request.js │ └── storage.js ├── tsconfig.json └── typings.d.ts /.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 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | PORT=8888 umi dev 2 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true, 5 | node: true, 6 | }, 7 | extends: ['plugin:react/recommended', 'standard'], 8 | globals: { 9 | Atomics: 'readonly', 10 | SharedArrayBuffer: 'readonly', 11 | }, 12 | parserOptions: { 13 | ecmaFeatures: { 14 | jsx: true, 15 | }, 16 | ecmaVersion: 2018, 17 | sourceType: 'module', 18 | }, 19 | plugins: ['react'], 20 | rules: { 21 | 'react/prop-types': 0, 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /npm-debug.log* 6 | /yarn-error.log 7 | /yarn.lock 8 | /package-lock.json 9 | 10 | # production 11 | /dist 12 | 13 | # misc 14 | .DS_Store 15 | 16 | # umi 17 | /src/.umi 18 | /src/.umi-production 19 | /src/.umi-test 20 | /.env.local 21 | 22 | secret.js 23 | .env 24 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/*.md 2 | **/*.svg 3 | **/*.ejs 4 | **/*.html 5 | package.json 6 | .umi 7 | .umi-production 8 | .umi-test 9 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi":false, 4 | "trailingComma": "all", 5 | "printWidth": 80, 6 | "overrides": [ 7 | { 8 | "files": ".prettierrc", 9 | "options": { "parser": "json" } 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.umirc.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'umi' 2 | 3 | export default defineConfig({ 4 | title: '这是个人网站首页', 5 | favicon: 6 | 'https://immisso-upload.oss-cn-hangzhou.aliyuncs.com/20200517/rc-upload-1589714215963-2.png', 7 | proxy: { 8 | '/api': { 9 | target: 'http://localhost:7001/api', 10 | pathRewrite: { '^/api': '' }, 11 | changeOrigin: true, 12 | }, 13 | }, 14 | routes: [ 15 | { 16 | path: '/', 17 | redirect: '/home', 18 | }, 19 | { 20 | path: '/home', 21 | component: '@/pages/Home', 22 | // exact: true, 23 | routes: [ 24 | { 25 | path: '/home', 26 | component: '@/components/HomeArticleList', 27 | // exact: true, 28 | }, 29 | { 30 | path: '/home/:category', 31 | exact: true, 32 | component: '@/components/HomeArticleList', 33 | }, 34 | { 35 | path: '/home/:category/:tag', 36 | exact: true, 37 | component: '@/components/HomeArticleList', 38 | }, 39 | ], 40 | }, 41 | { 42 | path: '/article/:id', 43 | component: '@/pages/Article', 44 | }, 45 | { 46 | path: '/write/course', 47 | component: '@/pages/WriteCourse', 48 | }, 49 | { 50 | path: '/write/drafts', 51 | component: '@/pages/Draft', 52 | }, 53 | { 54 | path: '/write/draft/:key', 55 | component: '@/pages/Write', 56 | }, 57 | { 58 | path: '/admin', 59 | component: '@/pages/Admin', 60 | routes: [ 61 | { 62 | path: '/admin', 63 | redirect: '/admin/categories', 64 | }, 65 | { 66 | path: '/admin/categories', 67 | component: '@/components/Admin/Category', 68 | }, 69 | { 70 | path: '/admin/tags', 71 | component: '@/components/Admin/Tag', 72 | }, 73 | { 74 | path: '/admin/articles', 75 | component: '@/components/Admin/Article', 76 | }, 77 | { 78 | path: '/admin/comments', 79 | component: '@/components/Admin/Comment', 80 | }, 81 | ], 82 | }, 83 | { 84 | path: '/login', 85 | component: '@/pages/Login', 86 | }, 87 | { 88 | path: '/register', 89 | component: '@/pages/Register', 90 | }, 91 | { 92 | path: '/account', 93 | component: '@/pages/Account', 94 | routes: [ 95 | { 96 | path: '/account', 97 | redirect: '/account/me', 98 | }, 99 | { 100 | path: '/account/me', 101 | component: '@/components/Account/Me', 102 | }, 103 | ], 104 | }, 105 | { 106 | component: '@/pages/404', 107 | }, 108 | ], 109 | }) 110 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 个人博客网站 2 | ![](https://img.shields.io/badge/React-%5E16.12.0-brightgreen) 3 | ![](https://img.shields.io/badge/dva-%5E2.4.1-brightgreen) 4 | ![](https://img.shields.io/badge/umi-%5E3.0.18-brightgreen) 5 | 6 | 接下来我将对该项目进行大致的使用说明,后续也会写一份更为详细的免费的《Node全栈开发——带你从零开发前后端分离的个人网站》教程,带你从零开发到部署上线的全过程,敬请期待。您现在看的这个项目只是网站的前端项目,服务端项目地址请点击[服务端](https://github.com/immisso/blog-server) 7 | 8 | ## 关于Demo 9 | 该项目采用`React+antd+umi+dva`技术栈进行实现。查看[demo](https://blog-demo.immisso.com),测试账号和密码都是`qiye@admin.com` 10 | 11 | 12 | >**注意:** 接下来的说明都只针对该前端项目的说明。因为是前后端分离。所以启动是需要前后端一起启动的。服务端项目请移步[博客网站服务端](https://github.com/immisso/blog-server) 13 | 14 | ## 运行效果 15 | 下面是一些主要功能的效果图。请君参考。 16 | 17 | 首页详情页 18 | ![](https://github.com/immisso/blog-web/blob/feature/public/images/%E9%A6%96%E9%A1%B5%E8%AF%A6%E6%83%85%E9%A1%B5001.gif) 19 | 20 | 写文章 21 | ![](https://github.com/immisso/blog-web/blob/feature/public/images/%E7%BC%96%E8%BE%91%E5%99%A8%E9%A1%B5%E9%9D%A2002.gif) 22 | 23 | 管理页 24 | ![](https://github.com/immisso/blog-web/blob/feature/public/images/%E7%AE%A1%E7%90%86%E9%A1%B5003.gif) 25 | 26 | ## 如何开始 27 | 28 | #### clone项目到本地 29 | 30 | ```git 31 | $ git clone https://github.com/immisso/blog-web 32 | ``` 33 | 34 | #### 安装依赖 35 | 36 | ```bash 37 | $ npm install 38 | ``` 39 | 或者 40 | ```bash 41 | $ yarn 42 | ``` 43 | #### 启动项目 44 | 45 | ```bash 46 | $ npm run start 47 | ``` 48 | 或 49 | ```bash 50 | $ yarn start 51 | ``` 52 | 启动成功后,然后再浏览器上打开[http://localhost:8888](http://localhost:8888)即可! 53 | 虽然此时可以我们可以成功启动,但是还不能上传文件到阿里云,因为我们还需要一些配置。在`src/`目录下创建`config`文件夹,然后创建一个`secret.js`文件。改文件内容如下: 54 | 55 | ```javascript 56 | module.exports = { 57 | accessKeyId: '', // 阿里云Keyid 58 | accessKeySecret: '', // 阿里云Key secret 59 | bucket: '', // Oss bucket 名字 60 | ENCRYPT_KEY: '' // localStorage加密Key 61 | } 62 | ``` 63 | 这样配置好,就可以成功上传文件了! 64 | 65 | > 当然你要和服务端同时启用。 66 | 67 | ## 功能介绍 68 | 这个项目虽然不大,但是功能还算齐全。大体来说分为主网站和管理系统两部分。目前已经实现主要功能如下: 69 | 70 | ### 主网站 71 | + [x] 登录 72 | + [x] 注册 73 | + [x] 文章列表 74 | + [x] 点赞 75 | + [x] 评论 76 | + [x] markdown写文章 77 | + [x] 阿里云上传图片 78 | + [x] 保存草稿 79 | + [x] 发表文章 80 | + [x] 个人信息更新 81 | 82 | ### 管理系统 83 | + [x] 分类管理(分类列表、添加、删除) 84 | + [x] 标签管理(标签列表、添加、删除) 85 | + [x] 文章管理(文章列表、删除) 86 | + [x] 评论管理(评论列表、删除) 87 | 88 | ## 技术栈 89 | 90 | 该网站采用前后端分离技术,前端采用`React+antd+umi+dva`开发,服务端采用`Node`开发。主要功能模块包括 91 | 92 | + `react` 93 | + `antd` 94 | + `umi` 95 | + `dva` 96 | + `react-markdown` 97 | + `highlight.js` 98 | 99 | ## 特别说明 100 | 该项目会长期更新。会逐步完善其他许多功能。如果写教程功能、邮件提醒、用户管理、主题风格、代码风格等。欢迎长期关注。 101 | 102 | 103 | -------------------------------------------------------------------------------- /mock/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/immisso/blog-web/d30b8628547573f7a5db751f15dd059f729f3591/mock/.gitkeep -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "start": "umi dev", 5 | "build": "umi build", 6 | "prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'", 7 | "test": "umi-test", 8 | "test:coverage": "umi-test --coverage" 9 | }, 10 | "gitHooks": { 11 | "pre-commit": "lint-staged" 12 | }, 13 | "lint-staged": { 14 | "*.{js,jsx,less,md,json}": [ 15 | "prettier --write" 16 | ], 17 | "*.ts?(x)": [ 18 | "prettier --parser=typescript --write" 19 | ] 20 | }, 21 | "dependencies": { 22 | "@umijs/preset-react": "1.x", 23 | "@umijs/test": "^3.0.18", 24 | "ali-oss": "^6.7.0", 25 | "crypto-js": "^4.0.0", 26 | "dva": "^2.4.1", 27 | "lint-staged": "^10.0.7", 28 | "prettier": "^1.19.1", 29 | "react": "^16.12.0", 30 | "react-dom": "^16.12.0", 31 | "react-keyboard-event-handler": "^1.5.4", 32 | "react-markdown": "^4.3.1", 33 | "react-mathjax": "^1.0.1", 34 | "react-syntax-highlighter": "^12.2.1", 35 | "react-zmage": "^0.8.5-beta.31", 36 | "remark-math": "^2.0.1", 37 | "umi": "^3.0.18", 38 | "umi-request": "^1.2.19", 39 | "yorkie": "^2.0.0" 40 | }, 41 | "devDependencies": { 42 | "eslint": "^6.8.0", 43 | "eslint-config-standard": "^14.1.1", 44 | "eslint-plugin-import": "^2.20.2", 45 | "eslint-plugin-node": "^11.1.0", 46 | "eslint-plugin-promise": "^4.2.1", 47 | "eslint-plugin-react": "^7.19.0", 48 | "eslint-plugin-standard": "^4.0.1" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/immisso/blog-web/d30b8628547573f7a5db751f15dd059f729f3591/public/favicon.png -------------------------------------------------------------------------------- /public/images/管理页003.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/immisso/blog-web/d30b8628547573f7a5db751f15dd059f729f3591/public/images/管理页003.gif -------------------------------------------------------------------------------- /public/images/编辑器页面002.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/immisso/blog-web/d30b8628547573f7a5db751f15dd059f729f3591/public/images/编辑器页面002.gif -------------------------------------------------------------------------------- /public/images/首页详情页001.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/immisso/blog-web/d30b8628547573f7a5db751f15dd059f729f3591/public/images/首页详情页001.gif -------------------------------------------------------------------------------- /src/components/Account/Me/index.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 柒叶 3 | * @Date: 2020-05-06 20:59:56 4 | * @Last Modified by: 柒叶 5 | * @Last Modified time: 2020-05-13 20:41:23 6 | */ 7 | 8 | import React, { useEffect } from 'react' 9 | import { Form, Input, Row, Col, Avatar, Button, Tag } from 'antd' 10 | import { connect } from 'dva' 11 | 12 | const Me = props => { 13 | const { dispatch, account, history, avatar } = props 14 | const [form] = Form.useForm() 15 | useEffect(() => { 16 | if (!account || !account.id) { 17 | history.push('/login') 18 | } 19 | dispatch({ 20 | type: 'user/account', 21 | callback(res) { 22 | if (res.status === 200) { 23 | const account = res.data 24 | Object.keys(form.getFieldsValue()).forEach(key => { 25 | const obj = {} 26 | obj[key] = account[key] || null 27 | form.setFieldsValue(obj) 28 | }) 29 | } 30 | }, 31 | }) 32 | }, []) 33 | 34 | const changeAvatar = () => { 35 | dispatch({ type: 'user/changeAvatar' }) 36 | } 37 | 38 | const onFinish = values => { 39 | if (dispatch) { 40 | dispatch({ 41 | type: 'user/setAccount', 42 | payload: { ...values, avatar }, 43 | }) 44 | } 45 | } 46 | 47 | return ( 48 | <> 49 |

个人信息

50 | 51 | 52 |
53 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 79 | 80 | 81 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 98 | 99 |
100 | 101 | 102 |
103 | 104 |
105 |
106 | 107 |
108 |
109 | 110 | 账户类型:管理员 111 | 112 |
113 | 114 |
115 | 116 | ) 117 | } 118 | 119 | export default connect(({ user: { avatar, account }, loading }) => ({ 120 | avatar, 121 | account, 122 | loading, 123 | }))(Me) 124 | -------------------------------------------------------------------------------- /src/components/Account/Me/index.less: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/immisso/blog-web/d30b8628547573f7a5db751f15dd059f729f3591/src/components/Account/Me/index.less -------------------------------------------------------------------------------- /src/components/Admin/Article/index.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 柒叶 3 | * @Date: 2020-04-28 20:58:37 4 | * @Last Modified by: 柒叶 5 | * @Last Modified time: 2020-05-12 12:46:30 6 | */ 7 | import React, { useEffect, useState } from 'react' 8 | import { Card, Table, Button, Tag, Popconfirm } from 'antd' 9 | import { connect } from 'dva' 10 | import { Link } from 'umi' 11 | import moment from 'moment' 12 | import { v4 as uuidv4 } from 'uuid' 13 | 14 | const Article = props => { 15 | const { dispatch, articles, articleCount, loading } = props 16 | const [page, setPage] = useState(1) 17 | useEffect(() => { 18 | if (dispatch) { 19 | dispatch({ type: 'admin/articles', payload: { page, pageSize: 10 } }) 20 | } 21 | }, []) 22 | 23 | const pageChange = pageNum => { 24 | setPage(pageNum) 25 | if (dispatch) { 26 | dispatch({ 27 | type: 'admin/articles', 28 | payload: { page: pageNum, pageSize: 10 }, 29 | }) 30 | } 31 | } 32 | const deleteArticle = id => { 33 | if (dispatch) { 34 | dispatch({ type: 'admin/deleteArticle', payload: { id } }) 35 | } 36 | } 37 | const columns = [ 38 | { 39 | title: 'ID', 40 | dataIndex: 'id', 41 | width: 50, 42 | }, 43 | { 44 | title: '标题', 45 | width: 150, 46 | ellipsis: true, 47 | render(article) { 48 | return ( 49 | 50 | {article.title} 51 | 52 | ) 53 | }, 54 | }, 55 | { 56 | title: '分类', 57 | dataIndex: 'category', 58 | render(category) { 59 | return {category.name} 60 | }, 61 | }, 62 | { 63 | title: '标签', 64 | dataIndex: 'tag', 65 | render(tag) { 66 | return {tag.name} 67 | }, 68 | }, 69 | { 70 | title: '作者', 71 | dataIndex: 'user', 72 | render(user) { 73 | return {user.nickname} 74 | }, 75 | }, 76 | { 77 | title: '点赞数', 78 | dataIndex: 'favorite', 79 | }, 80 | { 81 | title: '浏览量', 82 | dataIndex: 'view', 83 | }, 84 | { 85 | title: '评论数', 86 | dataIndex: 'comment', 87 | }, 88 | { 89 | title: '创建时间', 90 | dataIndex: 'createdAt', 91 | render(date) { 92 | return {moment(date).format('YYYY-MM-DD')} 93 | }, 94 | }, 95 | { 96 | title: '操作', 97 | dataIndex: 'id', 98 | render(id) { 99 | return ( 100 | deleteArticle(id)} 105 | > 106 | 109 | 110 | ) 111 | }, 112 | }, 113 | ] 114 | return ( 115 | <> 116 | 117 | uuidv4()} 121 | loading={loading} 122 | pagination={{ 123 | pageSize: 10, 124 | total: articleCount, 125 | current: page, 126 | onChange: pageChange, 127 | }} 128 | /> 129 | 130 | 131 | ) 132 | } 133 | 134 | export default connect(({ admin: { articles, articleCount }, loading }) => ({ 135 | articles, 136 | articleCount, 137 | loading: loading.effects['admin/articles'], 138 | }))(Article) 139 | -------------------------------------------------------------------------------- /src/components/Admin/Category/index.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 柒叶 3 | * @Date: 2020-04-28 20:56:49 4 | * @Last Modified by: 柒叶 5 | * @Last Modified time: 2020-05-12 11:12:46 6 | */ 7 | import React, { useEffect, useState } from 'react' 8 | import { 9 | Card, 10 | Table, 11 | Button, 12 | Space, 13 | Modal, 14 | Input, 15 | Form, 16 | Popconfirm, 17 | } from 'antd' 18 | import { connect } from 'dva' 19 | import moment from 'moment' 20 | import { PlusOutlined } from '@ant-design/icons' 21 | import { v4 as uuidv4 } from 'uuid' 22 | 23 | const Category = props => { 24 | const { dispatch, categories, loading } = props 25 | const [visible, setVisible] = useState(false) 26 | const [form] = Form.useForm() 27 | useEffect(() => { 28 | if (dispatch) { 29 | dispatch({ type: 'admin/categories' }) 30 | } 31 | }, []) 32 | const addCategory = () => {} 33 | const handleCancel = () => { 34 | setVisible(false) 35 | } 36 | const showModal = () => { 37 | setVisible(true) 38 | } 39 | const onSubmit = values => { 40 | if (dispatch) { 41 | dispatch({ type: 'admin/createCategory', payload: values }) 42 | } 43 | form.resetFields() 44 | } 45 | const deleteCategory = id => { 46 | if (dispatch) { 47 | dispatch({ type: 'admin/deleteCategory', payload: { id } }) 48 | } 49 | } 50 | const columns = [ 51 | { 52 | title: 'ID', 53 | dataIndex: 'id', 54 | key: 'id', 55 | }, 56 | { 57 | title: '分类名称', 58 | dataIndex: 'name', 59 | key: 'name', 60 | }, 61 | { 62 | title: '英文名称', 63 | dataIndex: 'en_name', 64 | key: 'en_name', 65 | }, 66 | { 67 | title: '创建时间', 68 | dataIndex: 'createdAt', 69 | render(date) { 70 | return {moment(date).format('YYYY-MM-DD')} 71 | }, 72 | }, 73 | { 74 | title: '操作', 75 | render(category) { 76 | return ( 77 | deleteCategory(category.id)} 82 | > 83 | 86 | 87 | ) 88 | }, 89 | }, 90 | ] 91 | 92 | return ( 93 | <> 94 | 95 | 98 | 99 | 100 |
uuidv4()} 104 | loading={loading} 105 | pagination={false} 106 | /> 107 | 108 | 119 |
120 | 130 | 131 | 132 | 142 | 143 | 144 | 145 | 148 | 149 | 150 |
151 | 152 | ) 153 | } 154 | 155 | export default connect(({ admin: { categories }, loading }) => ({ 156 | categories, 157 | loading: loading.effects['admin/categories'], 158 | }))(Category) 159 | -------------------------------------------------------------------------------- /src/components/Admin/Comment/index.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 柒叶 3 | * @Date: 2020-04-28 20:59:36 4 | * @Last Modified by: 柒叶 5 | * @Last Modified time: 2020-05-02 19:14:04 6 | */ 7 | 8 | import React, { useEffect } from 'react' 9 | import { Card, Table, Button, Tag, Popconfirm } from 'antd' 10 | import { connect } from 'dva' 11 | import { Link } from 'umi' 12 | import moment from 'moment' 13 | import { v4 as uuidv4 } from 'uuid' 14 | 15 | const Comment = props => { 16 | const { dispatch, comments, loading } = props 17 | useEffect(() => { 18 | if (dispatch) { 19 | dispatch({ type: 'admin/comments' }) 20 | } 21 | }, []) 22 | 23 | const deleteComment = id => { 24 | if (dispatch) { 25 | dispatch({ type: 'admin/deleteComment', payload: { id } }) 26 | } 27 | } 28 | const columns = [ 29 | { 30 | title: 'ID', 31 | dataIndex: 'id', 32 | width: 50, 33 | }, 34 | { 35 | title: '评论用户', 36 | dataIndex: 'user', 37 | ellipsis: true, 38 | render(user) { 39 | return {user.nickname} 40 | }, 41 | }, 42 | { 43 | title: '评论文章', 44 | dataIndex: 'article', 45 | ellipsis: true, 46 | width: 150, 47 | render(article) { 48 | return ( 49 | 50 | {article.title} 51 | 52 | ) 53 | }, 54 | }, 55 | { 56 | title: '评论内容', 57 | dataIndex: 'content', 58 | ellipsis: true, 59 | }, 60 | { 61 | title: '状态', 62 | dataIndex: 'status', 63 | render(status) { 64 | return status === 1 ? ( 65 | 可见 66 | ) : ( 67 | 不可见 68 | ) 69 | }, 70 | }, 71 | { 72 | title: '创建时间', 73 | dataIndex: 'createdAt', 74 | render(date) { 75 | return {moment(date).format('YYYY-MM-DD')} 76 | }, 77 | }, 78 | { 79 | title: '操作', 80 | dataIndex: 'id', 81 | render(id) { 82 | return ( 83 | deleteComment(id)} 88 | > 89 | 92 | 93 | ) 94 | }, 95 | }, 96 | ] 97 | 98 | return ( 99 | <> 100 | 101 |
uuidv4()} 105 | loading={loading} 106 | pagination={false} 107 | // pagination={{ 108 | // pageSize: 10, 109 | // total: articleCount, 110 | // current: page, 111 | // onChange: pageChange 112 | // }} 113 | /> 114 | 115 | 116 | ) 117 | } 118 | 119 | export default connect(({ admin: { comments }, loading }) => ({ 120 | comments, 121 | loading: loading.effects['admin/comments'], 122 | }))(Comment) 123 | -------------------------------------------------------------------------------- /src/components/Admin/Tag/index.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 柒叶 3 | * @Date: 2020-04-28 21:00:14 4 | * @Last Modified by: 柒叶 5 | * @Last Modified time: 2020-05-01 17:57:18 6 | */ 7 | import React, { useEffect, useState } from 'react' 8 | import { 9 | Card, 10 | Table, 11 | Button, 12 | Space, 13 | Modal, 14 | Form, 15 | Input, 16 | Popconfirm, 17 | Select, 18 | } from 'antd' 19 | import { connect } from 'dva' 20 | import moment from 'moment' 21 | import { v4 as uuidv4 } from 'uuid' 22 | import { PlusOutlined } from '@ant-design/icons' 23 | 24 | const { Option } = Select 25 | 26 | const Tag = props => { 27 | const { dispatch, categories, tags, loading } = props 28 | const [visible, setVisible] = useState(false) 29 | const [form] = Form.useForm() 30 | useEffect(() => { 31 | if (dispatch) { 32 | dispatch({ type: 'admin/categories' }) 33 | dispatch({ type: 'admin/tags' }) 34 | } 35 | }, []) 36 | const handleCancel = () => { 37 | setVisible(false) 38 | } 39 | const showModal = () => { 40 | setVisible(true) 41 | } 42 | const onSubmit = values => { 43 | if (dispatch) { 44 | dispatch({ type: 'admin/createTag', payload: values }) 45 | } 46 | form.resetFields() 47 | } 48 | const deleteTag = id => { 49 | if (dispatch) { 50 | dispatch({ type: 'admin/deleteTag', payload: { id } }) 51 | } 52 | } 53 | 54 | const columns = [ 55 | { 56 | title: 'ID', 57 | dataIndex: 'id', 58 | key: 'id', 59 | }, 60 | { 61 | title: '标签名称', 62 | dataIndex: 'name', 63 | key: 'name', 64 | }, 65 | { 66 | title: '英文名称', 67 | dataIndex: 'en_name', 68 | key: 'en_name', 69 | }, 70 | { 71 | title: '创建时间', 72 | dataIndex: 'createdAt', 73 | render(date) { 74 | return {moment(date).format('YYYY-MM-DD')} 75 | }, 76 | }, 77 | { 78 | title: '操作', 79 | render(tag) { 80 | return ( 81 | deleteTag(tag.id)} 86 | > 87 | 90 | 91 | ) 92 | }, 93 | }, 94 | ] 95 | return ( 96 | <> 97 | 98 | 101 | 102 | 103 |
uuidv4()} 107 | loading={loading} 108 | pagination={false} 109 | /> 110 | 111 | 119 |
120 | 125 | 133 | 134 | 144 | 145 | 146 | 156 | 157 | 158 | 159 | 162 | 163 | 164 |
165 | 166 | ) 167 | } 168 | 169 | export default connect(({ admin: { categories, tags }, loading }) => ({ 170 | tags, 171 | categories, 172 | loading: loading.effects['admin/tags'], 173 | }))(Tag) 174 | -------------------------------------------------------------------------------- /src/components/AliOssUpload/index.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 柒叶 3 | * @Date: 2020-04-21 20:46:32 4 | * @Last Modified by: 柒叶 5 | * @Last Modified time: 2020-05-17 19:02:55 6 | */ 7 | 8 | import React, { useState } from 'react' 9 | import { Upload, message } from 'antd' 10 | import moment from 'moment' 11 | import OSS from 'ali-oss' 12 | import { PlusOutlined, LoadingOutlined, InboxOutlined } from '@ant-design/icons' 13 | import { accessKeySecret, accessKeyId, bucket } from '@/config/secret' 14 | 15 | const { Dragger } = Upload 16 | 17 | const client = new OSS({ 18 | region: 'oss-cn-hangzhou', 19 | accessKeyId, 20 | accessKeySecret, 21 | bucket, 22 | secure: true, 23 | }) 24 | 25 | const UploadToOss = (path, file) => { 26 | return new Promise((resolve, reject) => { 27 | client 28 | .put(path, file) 29 | .then(data => { 30 | resolve(data) 31 | }) 32 | .catch(error => { 33 | reject(error) 34 | }) 35 | }) 36 | } 37 | 38 | const filePath = file => { 39 | // 上传文件路径和名称 40 | return `${moment().format('YYYYMMDD')}/${file.uid}.${file.type.split('/')[1]}` 41 | } 42 | 43 | const AliOssUpload = props => { 44 | const { type, returnImageUrl } = props 45 | const [loading, setLoadding] = useState(false) 46 | const [imageUrl, setImageUrl] = useState(null) 47 | 48 | const beforeUpload = file => { 49 | const isJpgOrPng = 50 | file.type === 'image/png' || 51 | file.type === 'image/jpeg' || 52 | file.type === 'image/gif' 53 | if (!isJpgOrPng) { 54 | message.error('你只能上传JPG/PNG格式的图片') 55 | } 56 | const isLt4M = file.size / 1024 / 1024 < 4 57 | if (!isLt4M) { 58 | message.error('图片必须小于4M') 59 | } 60 | UploadToOss(filePath(file), file) 61 | .then(data => { 62 | setImageUrl(data.url) 63 | returnImageUrl(data.url) 64 | }) 65 | .catch(error => { 66 | console.log(error) 67 | }) 68 | return isJpgOrPng && isLt4M 69 | } 70 | 71 | const onChange = info => { 72 | if (info.file.status === 'uploading') { 73 | setLoadding(true) 74 | } 75 | if (info.file.status === 'done') { 76 | console.log(info) 77 | } 78 | } 79 | 80 | const uploadButton = ( 81 |
82 | {loading ? : } 83 |
Upload
84 |
85 | ) 86 | 87 | if (type === 'click') { 88 | return ( 89 | 98 | {imageUrl ? ( 99 | 100 | ) : ( 101 | uploadButton 102 | )} 103 | 104 | ) 105 | } 106 | 107 | if (type === 'drag') { 108 | return ( 109 | 115 |

116 | 117 |

118 |

点击或者拖拽图片到这个区域

119 |
120 | ) 121 | } 122 | } 123 | 124 | export default AliOssUpload 125 | -------------------------------------------------------------------------------- /src/components/Anchor/index.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 柒叶 3 | * @Date: 2020-04-11 17:25:22 4 | * @Last Modified by: 柒叶 5 | * @Last Modified time: 2020-04-11 18:10:37 6 | */ 7 | 8 | import React from 'react'; 9 | import { Anchor } from 'antd'; 10 | 11 | const ArticleAnchor = props => { 12 | const { anchors } = props; 13 | const anchorRender = data => { 14 | return data.map(item => { 15 | if (item.children) { 16 | return ( 17 | 23 | {anchorRender(item.children)} 24 | 25 | ); 26 | } 27 | return ( 28 | 34 | ); 35 | }); 36 | }; 37 | return ( 38 | <> 39 | 40 | {anchorRender(anchors)} 41 | 42 | 43 | ); 44 | }; 45 | 46 | export default ArticleAnchor; 47 | -------------------------------------------------------------------------------- /src/components/CheckTag/index.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 柒叶 3 | * @Date: 2020-04-19 17:15:56 4 | * @Last Modified by: 柒叶 5 | * @Last Modified time: 2020-04-19 20:01:42 6 | */ 7 | 8 | import React from 'react' 9 | import { Tag } from 'antd' 10 | 11 | const { CheckableTag } = Tag 12 | 13 | const CheckTag = props => { 14 | const { data, checkTagHandle } = props 15 | return ( 16 | <> 17 | {data && 18 | data.length > 0 && 19 | data.map(item => ( 20 | checkTagHandle(item.id, checked)} 23 | > 24 | {item.name} 25 | 26 | ))} 27 | 28 | ) 29 | } 30 | 31 | export default CheckTag 32 | -------------------------------------------------------------------------------- /src/components/Comment/index.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 柒叶 3 | * @Date: 2020-04-11 20:19:37 4 | * @Last Modified by: 柒叶 5 | * @Last Modified time: 2020-05-13 16:38:45 6 | */ 7 | import React, { useEffect } from 'react' 8 | import { connect } from 'dva' 9 | import { Comment, Divider, Tooltip, List, Card } from 'antd' 10 | import moment from 'moment' 11 | import UserAvatar from '@/components/UserAvatar' 12 | import LoginCommentForm from '@/components/Forms/LoginCommentForm' 13 | import NoLoginCommentForm from '@/components/Forms/NoLoginCommentForm' 14 | 15 | moment.locale('zh-cn') 16 | const Content = ({ content }) =>

{content}

17 | 18 | const Datetime = ({ time }) => { 19 | return ( 20 | 21 | {moment(time).fromNow()} 22 | 23 | ) 24 | } 25 | 26 | const AddComment = props => { 27 | const { account, dispatch, id, author, comments, loading } = props 28 | useEffect(() => { 29 | if (dispatch) { 30 | dispatch({ type: 'article/comments', payload: { id } }) 31 | } 32 | }, []) 33 | return ( 34 | 41 | ( 47 | 48 | } 51 | content={} 52 | datetime={} 53 | /> 54 | 55 | )} 56 | /> 57 | 58 | {account && account.id ? ( 59 | } 61 | content={} 62 | /> 63 | ) : ( 64 | 65 | )} 66 | 67 | ) 68 | } 69 | 70 | export default connect( 71 | ({ article: { comments }, user: { account }, loading }) => ({ 72 | comments, 73 | account, 74 | loading: loading.effects['article/comments'], 75 | }), 76 | )(AddComment) 77 | -------------------------------------------------------------------------------- /src/components/Header/index.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 柒叶 3 | * @Date: 2020-04-05 12:05:06 4 | * @Last Modified by: 柒叶 5 | * @Last Modified time: 2020-05-21 10:07:51 6 | */ 7 | 8 | import React, { useState, useEffect } from 'react' 9 | import { Layout, Menu, Drawer, Button, Dropdown } from 'antd' 10 | import { connect } from 'dva' 11 | import Icon, { MenuOutlined } from '@ant-design/icons' 12 | import { Link } from 'umi' 13 | import UserAvatar from '@/components/UserAvatar' 14 | import storageHelper from '@/utils/storage' 15 | 16 | import styles from './index.less' 17 | 18 | const { Header } = Layout 19 | const { SubMenu } = Menu 20 | 21 | const tabs = [ 22 | { 23 | title: '首页', 24 | key: 'home', 25 | icon: 'home', 26 | path: '/', 27 | }, 28 | // { 29 | // title: '文章', 30 | // key: 'articles', 31 | // key2: 'all', 32 | // icon: 'file-done', 33 | // path: '/home/articles/all' 34 | // }, 35 | // { 36 | // title: '教程', 37 | // key: 'course', 38 | // key2: 'all', 39 | // icon: 'project', 40 | // path: '/home/course/all' 41 | // } 42 | ] 43 | 44 | // const categories = null 45 | 46 | const MainHeader = props => { 47 | const { dispatch, categories, account, pathname } = props 48 | 49 | const [visible, setVisible] = useState(false) 50 | 51 | useEffect(() => { 52 | if (dispatch) { 53 | dispatch({ type: 'article/categories' }) 54 | } 55 | }, []) 56 | 57 | const showDrawer = () => { 58 | setVisible(true) 59 | } 60 | const onClose = () => { 61 | setVisible(false) 62 | } 63 | const logout = () => { 64 | storageHelper.clear('user') 65 | if (dispatch) { 66 | dispatch({ type: 'user/logout' }) 67 | } 68 | } 69 | const handleClick = () => {} 70 | return ( 71 |
72 |
73 |
74 |
75 | 76 | {/* 84 | 88 | */} 89 | 99 | 104 | 105 | 106 | 112 | {tabs && 113 | tabs.map(item => ( 114 | 115 | 121 | {item.title} 122 | 123 | 124 | ))} 125 | {categories.length > 0 && 126 | categories.map(item => { 127 | return item.tags.length > 0 ? ( 128 | 131 | {item.name} 132 | 133 | } 134 | key={`/home/${item.en_name}`} 135 | > 136 | {item.tags.map(tag => ( 137 | 138 | 144 | {tag.name} 145 | 146 | 147 | ))} 148 | 149 | ) : ( 150 | 151 | 157 | {item.name} 158 | 159 | 160 | ) 161 | })} 162 | 163 |
164 |
165 | 168 | 柒叶 169 |
170 |
171 |
172 | {account && account.email && account.id ? ( 173 | 176 | 177 | 写文章 178 | 179 | 180 | 草稿箱 181 | 182 | 183 | 184 | 写教程 185 | 186 | 187 | {account.account_type === 'ADMIN' ? ( 188 | 189 | 管理中心 190 | 191 | ) : ( 192 | '' 193 | )} 194 | 195 | 个人中心 196 | 197 | 198 | 199 | 退出 200 | 201 | 202 | } 203 | trigger={['click']} 204 | > 205 | e.preventDefault()} 208 | > 209 | 210 | 211 | 212 | ) : ( 213 | 214 | 登录 215 | · 216 | 注册 217 | 218 | )} 219 |
220 |
221 | 224 | 225 | 233 | 237 | 238 | 239 | 导航栏 240 | 241 | } 242 | placement="left" 243 | closable 244 | onClose={onClose} 245 | visible={visible} 246 | bodyStyle={{ padding: 0 }} 247 | > 248 | 255 | {tabs && 256 | tabs.map(item => ( 257 | 258 | 264 | 265 | {item.title} 266 | 267 | 268 | ))} 269 | {categories && 270 | categories.map(item => { 271 | return item.tags.length > 0 ? ( 272 | {item.name} 275 | } 276 | key={item.id} 277 | > 278 | {item.tags.map(tag => ( 279 | 280 | 286 | {tag.name} 287 | 288 | 289 | ))} 290 | 291 | ) : ( 292 | 293 | 299 | {item.name} 300 | 301 | 302 | ) 303 | })} 304 | 305 | 306 |
307 | ) 308 | } 309 | 310 | export default connect( 311 | ({ article: { categories }, user: { account }, loading }) => ({ 312 | categories, 313 | account, 314 | loading: loading, 315 | }), 316 | )(MainHeader) 317 | -------------------------------------------------------------------------------- /src/components/Header/index.less: -------------------------------------------------------------------------------- 1 | .homeHeader { 2 | margin: 0 auto; 3 | display: flex; 4 | align-items: center; 5 | justify-content: space-between; 6 | .homeHeaderPc { 7 | display: flex; 8 | } 9 | .homeHeaderLeft { 10 | display: flex; 11 | } 12 | .homeHeaderRight { 13 | display: flex; 14 | align-items: center; 15 | } 16 | .brand { 17 | // display: flex; 18 | align-items: center; 19 | margin-right: 1rem; 20 | font-size: 1.25rem; 21 | white-space: nowrap; 22 | svg { 23 | fill: rgb(0, 123, 255); 24 | } 25 | } 26 | } 27 | 28 | @media (max-width: 991px) { 29 | .homeHeader { 30 | .homeHeaderPc { 31 | display: none; 32 | } 33 | } 34 | } 35 | 36 | // @media screen and (min-width:576px) and (max-width:767px) { 37 | // .homeHeader{ 38 | // .homeHeaderPc{ 39 | // display: none; 40 | // } 41 | // } 42 | 43 | // } 44 | 45 | // @media screen and (min-width: 768px) and (max-width: 991px) { 46 | // .ant-layout-header { 47 | // padding: 0 5px; 48 | // .main-header{ 49 | // .main-header-pc{ 50 | // display: none; 51 | // } 52 | // } 53 | // } 54 | // } 55 | 56 | @media screen and (min-width: 992px) and (max-width: 1199px) { 57 | .homeHeader { 58 | width: 100%; 59 | .homeHeaderMobile { 60 | display: none; 61 | } 62 | } 63 | } 64 | 65 | @media screen and (min-width: 1200px) { 66 | .homeHeader { 67 | width: 1100px; 68 | .homeHeaderMobile { 69 | display: none; 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/components/HomeArticleList/index.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 柒叶 3 | * @Date: 2020-04-09 07:58:49 4 | * @Last Modified by: 柒叶 5 | * @Last Modified time: 2020-05-13 16:51:28 6 | */ 7 | 8 | import React, { useState, useEffect } from 'react' 9 | import { Tooltip, List, Skeleton, Tag, Card } from 'antd' 10 | import { EyeOutlined, LikeOutlined, MessageOutlined } from '@ant-design/icons' 11 | import { Link } from 'umi' 12 | import moment from 'moment' 13 | import { connect } from 'dva' 14 | 15 | const IconText = ({ icon, text }) => ( 16 | 17 | {React.createElement(icon, { style: { marginRight: 8 } })} 18 | {text} 19 | 20 | ) 21 | 22 | const HomeArticleList = props => { 23 | const { 24 | dispatch, 25 | articles, 26 | articleCount, 27 | loading, 28 | location: { state = {} }, 29 | } = props 30 | const { category, tag } = state 31 | const [page, setPage] = useState(1) 32 | useEffect( 33 | () => { 34 | if (dispatch) { 35 | dispatch({ 36 | type: 'article/articles', 37 | payload: { page, pageSize: 10, category, tag }, 38 | }) 39 | } 40 | }, 41 | tag ? [tag] : category ? [category] : [], 42 | ) 43 | const pageChange = pageNum => { 44 | setPage(pageNum) 45 | if (dispatch) { 46 | dispatch({ 47 | type: 'article/articles', 48 | payload: { page: pageNum, pageSize: 10, category, tag }, 49 | }) 50 | } 51 | } 52 | return ( 53 |
54 | 55 | ( 67 | 68 | , 75 | , 80 | , 85 | ]} 86 | extra={ 87 | item.cover ? ( 88 | logo 89 | ) : null 90 | } 91 | > 92 | 95 |

{item.title}

96 | 97 | } 98 | description={ 99 | 100 | {item.tag && item.tag.name} 101 | {item.user && item.user.nickname} 102 | · 103 | 104 | 105 | {moment(item.createdAt).fromNow()} 106 | 107 | 108 | 109 | } 110 | /> 111 |
112 |
113 | )} 114 | /> 115 |
116 |
117 | ) 118 | } 119 | 120 | export default connect(({ article: { articles, articleCount }, loading }) => ({ 121 | articles, 122 | articleCount, 123 | loading: loading.effects['article/articles'], 124 | }))(HomeArticleList) 125 | -------------------------------------------------------------------------------- /src/components/Markdown/CodeTag/index.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 柒叶 3 | * @Date: 2020-04-16 06:35:02 4 | * @Last Modified by: 柒叶 5 | * @Last Modified time: 2020-05-13 16:32:50 6 | */ 7 | 8 | import React from 'react' 9 | // import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter' 10 | import SyntaxHighlighter from 'react-syntax-highlighter' 11 | 12 | import { 13 | coy, 14 | dark, 15 | funky, 16 | okaidia, 17 | twilight, 18 | solarizedlight, 19 | tomorrow, 20 | prism, 21 | atomDark, 22 | base16AteliersulphurpoolLight, 23 | cb, 24 | darcula, 25 | duotoneDark, 26 | duotoneEarth, 27 | duotoneForest, 28 | duotoneLight, 29 | duotoneSea, 30 | duotoneSpace, 31 | ghcolors, 32 | hopscotch, 33 | pojoaque, 34 | vs, 35 | xonokai, 36 | } from 'react-syntax-highlighter/dist/esm/styles/prism' 37 | 38 | import { 39 | docco, 40 | a11yDark, 41 | a11yLight, 42 | agate, 43 | anOldHope, 44 | arduinoLight, 45 | ascetic, 46 | github, 47 | } from 'react-syntax-highlighter/dist/esm/styles/hljs' 48 | // import { 49 | // json, 50 | // jsx, 51 | // javascript, 52 | // python, 53 | // c, 54 | // sass, 55 | // scss, 56 | // go, 57 | // java, 58 | // css, 59 | // sql, 60 | // cpp, 61 | // nginx, 62 | // rust, 63 | // ruby, 64 | // php 65 | // } from 'react-syntax-highlighter/dist/esm/languages/prism' 66 | 67 | const CodeTag = props => { 68 | const { value, language } = props 69 | if (!value) return null 70 | return ( 71 | 76 | {value} 77 | 78 | ) 79 | } 80 | 81 | export default CodeTag 82 | -------------------------------------------------------------------------------- /src/components/Markdown/HeadTag/index.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 柒叶 3 | * @Date: 2020-04-16 20:44:38 4 | * @Last Modified by: 柒叶 5 | * @Last Modified time: 2020-04-16 21:35:25 6 | */ 7 | 8 | import React from 'react' 9 | 10 | const HeadTag = ({ level, children }) => { 11 | if (children.length === 0) return null 12 | const { 13 | props: { nodeKey, value }, 14 | } = children[0] 15 | return React.createElement( 16 | `h${level}`, 17 | { className: 'fw-700', key: nodeKey }, 18 | value, 19 | ) 20 | } 21 | 22 | export default HeadTag 23 | -------------------------------------------------------------------------------- /src/components/Markdown/ImageTag/index.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 柒叶 3 | * @Date: 2020-04-16 20:08:54 4 | * @Last Modified by: 柒叶 5 | * @Last Modified time: 2020-04-16 20:35:46 6 | */ 7 | 8 | import React from 'react'; 9 | import Zmage from 'react-zmage'; 10 | 11 | const ImageTag = props => { 12 | const { src } = props; 13 | return ( 14 | 22 | ); 23 | }; 24 | 25 | export default ImageTag; 26 | -------------------------------------------------------------------------------- /src/components/Markdown/MathInline/index.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 柒叶 3 | * @Date: 2020-04-17 14:03:03 4 | * @Last Modified by: 柒叶 5 | * @Last Modified time: 2020-04-18 06:20:44 6 | */ 7 | 8 | import React from 'react'; 9 | import MathJax from 'react-mathjax'; 10 | 11 | const MathInline = props => { 12 | return ; 13 | }; 14 | 15 | export default MathInline; 16 | -------------------------------------------------------------------------------- /src/components/Markdown/MathTag/index.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 柒叶 3 | * @Date: 2020-04-18 06:10:45 4 | * @Last Modified by: 柒叶 5 | * @Last Modified time: 2020-04-18 06:18:48 6 | */ 7 | 8 | import React from 'react'; 9 | import MathJax from 'react-mathjax'; 10 | 11 | const MathTag = props => { 12 | return ; 13 | }; 14 | 15 | export default MathTag; 16 | -------------------------------------------------------------------------------- /src/components/Markdown/index.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 柒叶 3 | * @Date: 2020-04-16 06:32:48 4 | * @Last Modified by: 柒叶 5 | * @Last Modified time: 2020-05-17 19:03:59 6 | */ 7 | 8 | import React from 'react' 9 | import ReactMarkdown from 'react-markdown' 10 | import CodeTag from './CodeTag' 11 | import ImageTag from './ImageTag' 12 | import HeadTag from './HeadTag' 13 | import MathTag from './MathTag' 14 | import MathInline from './MathInline' 15 | 16 | const Markdown = props => { 17 | const { markdown } = props 18 | return ( 19 | '_blank'} 22 | plugins={[[require('remark-math')]]} 23 | renderers={{ 24 | code: CodeTag, 25 | image: ImageTag, 26 | heading: HeadTag, 27 | math: MathTag, 28 | inlineMath: MathInline, 29 | }} 30 | /> 31 | ) 32 | } 33 | 34 | export default Markdown 35 | -------------------------------------------------------------------------------- /src/components/SiderList/index.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 柒叶 3 | * @Date: 2020-04-08 07:37:50 4 | * @Last Modified by: 柒叶 5 | * @Last Modified time: 2020-05-12 10:57:44 6 | */ 7 | 8 | import React from 'react' 9 | import { List } from 'antd' 10 | import { Link } from 'umi' 11 | import { EyeOutlined, LikeOutlined } from '@ant-design/icons' 12 | 13 | const SiderList = props => { 14 | const { dataSource, size, split } = props 15 | return ( 16 | ( 22 | 26 | 27 | {item.view} 28 | , 29 | 30 | 31 | {item.favorite} 32 | , 33 | ]} 34 | > 35 | 40 | {item.title} 41 | 42 | 43 | )} 44 | /> 45 | ) 46 | } 47 | 48 | export default SiderList 49 | -------------------------------------------------------------------------------- /src/components/Tags/index.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 柒叶 3 | * @Date: 2020-04-13 07:33:17 4 | * @Last Modified by: 柒叶 5 | * @Last Modified time: 2020-05-13 16:35:55 6 | */ 7 | 8 | import React, { useEffect } from 'react' 9 | import { Tag, Card } from 'antd' 10 | import { Link } from 'umi' 11 | import { connect } from 'dva' 12 | 13 | const Tags = props => { 14 | const { dispatch, tags, loading } = props 15 | useEffect(() => { 16 | if (dispatch) { 17 | dispatch({ type: 'article/tags' }) 18 | } 19 | }, []) 20 | 21 | return ( 22 | 29 | {tags && 30 | tags.map(tag => ( 31 | 32 | 38 | {tag.name} 39 | 40 | 41 | ))} 42 | 43 | ) 44 | } 45 | 46 | export default connect(({ article: { tags }, loading }) => ({ 47 | tags, 48 | loading: loading.effects['article/tags'], 49 | }))(Tags) 50 | -------------------------------------------------------------------------------- /src/components/UserAvatar/index.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 柒叶 3 | * @Date: 2020-04-10 09:04:59 4 | * @Last Modified by: 柒叶 5 | * @Last Modified time: 2020-04-10 09:10:38 6 | */ 7 | 8 | import React from 'react'; 9 | import { Avatar } from 'antd'; 10 | import { UserOutlined } from '@ant-design/icons'; 11 | 12 | const UserAvatar = props => 13 | props.src ? ( 14 | 15 | ) : ( 16 | } /> 17 | ); 18 | 19 | export default UserAvatar; 20 | -------------------------------------------------------------------------------- /src/components/forms/LoginCommentForm.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 柒叶 3 | * @Date: 2020-04-12 14:10:08 4 | * @Last Modified by: 柒叶 5 | * @Last Modified time: 2020-05-11 20:48:12 6 | */ 7 | 8 | import React, { useState } from 'react' 9 | import { Button, Input, Form } from 'antd' 10 | import { connect } from 'dva' 11 | 12 | const LoginCommentForm = props => { 13 | const { id, author, dispatch } = props 14 | const [form] = Form.useForm() 15 | const onFinish = values => { 16 | if (dispatch) { 17 | dispatch({ 18 | type: 'article/addComment', 19 | payload: { ...values, article_id: id, author }, 20 | }) 21 | } 22 | form.resetFields() 23 | } 24 | return ( 25 | <> 26 |
27 | 36 | 37 | 38 | 39 | 42 | 43 | 44 | 45 | ) 46 | } 47 | 48 | export default connect(({ loading }) => ({ 49 | loading, 50 | }))(LoginCommentForm) 51 | -------------------------------------------------------------------------------- /src/components/forms/NoLoginCommentForm.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 柒叶 3 | * @Date: 2020-04-12 14:07:31 4 | * @Last Modified by: 柒叶 5 | * @Last Modified time: 2020-05-13 16:37:23 6 | */ 7 | 8 | import React from 'react' 9 | import { Button, Input, Form } from 'antd' 10 | import { connect } from 'dva' 11 | 12 | const NoLoginCommentForm = props => { 13 | const { dispatch, id, author } = props 14 | const [form] = Form.useForm() 15 | const formItemLayout = { 16 | labelCol: { 17 | xs: { span: 24 }, 18 | sm: { span: 8 }, 19 | }, 20 | wrapperCol: { 21 | xs: { span: 24 }, 22 | sm: { span: 14 }, 23 | }, 24 | } 25 | const onFinish = values => { 26 | if (dispatch) { 27 | dispatch({ 28 | type: 'article/addNoLoginComment', 29 | payload: { ...values, article_id: id, author }, 30 | }) 31 | } 32 | form.resetFields() 33 | } 34 | return ( 35 |
36 | 51 | 52 | 53 | 64 | 65 | 66 | 77 | 78 | 79 | 89 | 90 | 91 | 92 | 95 | 96 | 97 | ) 98 | } 99 | 100 | export default connect(({ loading }) => ({ 101 | loading: loading.effects['article/addNoLoginComment'], 102 | }))(NoLoginCommentForm) 103 | -------------------------------------------------------------------------------- /src/global.less: -------------------------------------------------------------------------------- 1 | .ant-layout-header { 2 | position: relative; 3 | height: 64px; 4 | // flex-direction: row; 5 | // flex-wrap: nowrap; 6 | background: #ffffff !important; 7 | box-shadow: 0 2px 2px rgba(0, 0, 0, 0.02), 0 1px 0 rgba(0, 0, 0, 0.02); 8 | } 9 | 10 | .ant-list-vertical .ant-list-item-action { 11 | margin-top: 5px !important; 12 | } 13 | 14 | // .ant-pro-basicLayout-content { 15 | // margin: 0 !important; 16 | // } 17 | 18 | .ant-input:focus { 19 | // border: none !important; 20 | // border-bottom: 1px solid white !important; 21 | box-shadow: none !important; 22 | } 23 | 24 | .ant-layout { 25 | background: none !important; 26 | } 27 | 28 | // anchor 29 | 30 | .bold > .ant-anchor-link-title { 31 | font-weight: 700 !important; 32 | } 33 | .ant-anchor-ink::before { 34 | background: #e8e8e8 !important; 35 | } 36 | 37 | body { 38 | background: #fafafa !important; 39 | } 40 | ///////////////////////////////height/////// 41 | .h-55 { 42 | height: 55px !important; 43 | } 44 | /////////////////////////////fontSize/////// 45 | 46 | .ft-16 { 47 | font-size: 16px !important; 48 | } 49 | .ft-13 { 50 | font-size: 13px !important; 51 | } 52 | .ft-18 { 53 | font-size: 18px !important; 54 | } 55 | .ft-25 { 56 | font-size: 25px !important; 57 | } 58 | .ft-20 { 59 | font-size: 20px !important; 60 | } 61 | 62 | //////////////////////////////padding/////// 63 | 64 | .p-1m { 65 | padding: 1rem !important; 66 | } 67 | // .prl-10m{ 68 | // padding: 0 1rem !important; 69 | // } 70 | 71 | .pd-5 { 72 | padding: 5px !important; 73 | } 74 | .pl-10 { 75 | padding-left: 10px !important; 76 | } 77 | .pl-1m { 78 | padding-left: 1rem !important; 79 | } 80 | .pl-0 { 81 | padding-left: 0 !important; 82 | } 83 | .pl-2 { 84 | padding-left: 2px !important; 85 | } 86 | .pl-3 { 87 | padding-left: 3px !important; 88 | } 89 | 90 | .pt-3 { 91 | padding-top: 3px !important; 92 | } 93 | 94 | /////////////////////////////margin///////// 95 | 96 | .mrl-5 { 97 | margin: 0 5px !important; 98 | } 99 | .mtb-20 { 100 | margin: 20px 0 !important; 101 | } 102 | 103 | .ml-10 { 104 | margin-left: 10px !important; 105 | } 106 | .ml-0 { 107 | margin-left: 0 !important; 108 | } 109 | 110 | .mt-0 { 111 | margin-top: 0px !important; 112 | } 113 | 114 | .mt-10 { 115 | margin-top: 10px !important; 116 | } 117 | .mt-20 { 118 | margin-top: 20px !important; 119 | } 120 | .mt-10m { 121 | margin-top: 1rem !important; 122 | } 123 | .mt-15m { 124 | margin-top: 1.5rem !important; 125 | } 126 | .mb-0 { 127 | margin-bottom: 0px !important; 128 | } 129 | .mb-10 { 130 | margin-bottom: 10px !important; 131 | } 132 | .mb-1m { 133 | margin-bottom: 1rem !important; 134 | } 135 | .mb-15m { 136 | margin-bottom: 1.5rem !important; 137 | } 138 | 139 | .mr-5 { 140 | margin-right: 5px !important; 141 | } 142 | .mr-10 { 143 | margin-right: 10px !important; 144 | } 145 | .mr-20 { 146 | margin-right: 20px !important; 147 | } 148 | .m-0 { 149 | margin: 0px !important; 150 | } 151 | ///////////////////////////other/////////// 152 | 153 | .tc { 154 | text-align: center; 155 | } 156 | .bn { 157 | background: none !important; 158 | } 159 | .dx { 160 | display: flex !important; 161 | } 162 | .fw-700 { 163 | font-weight: 700 !important; 164 | } 165 | .fw-400 { 166 | font-weight: 400 !important; 167 | } 168 | .bdn { 169 | border: none !important; 170 | } 171 | .tln { 172 | outline: none !important; 173 | } 174 | .ellipsis { 175 | white-space: nowrap; 176 | text-overflow: ellipsis; 177 | overflow: hidden; 178 | // word-break: break-all; 179 | } 180 | .fr { 181 | float: right; 182 | } 183 | -------------------------------------------------------------------------------- /src/models/admin.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 柒叶 3 | * @Date: 2020-04-29 18:05:19 4 | * @Last Modified by: 柒叶 5 | * @Last Modified time: 2020-05-12 14:53:44 6 | */ 7 | 8 | import { 9 | getComments, 10 | getCategories, 11 | deleteCategory, 12 | createCategory, 13 | getTags, 14 | deleteTag, 15 | createTag, 16 | deleteArticle, 17 | deleteComment, 18 | getArticles, 19 | } from '@/services/admin' 20 | 21 | export default { 22 | namespace: 'admin', 23 | state: { 24 | comments: [], 25 | categories: [], 26 | tags: [], 27 | articles: [], 28 | articleCount: 0, 29 | }, 30 | effects: { 31 | *comments({ payload }, { call, put }) { 32 | const { status, data } = yield call(getComments, payload) 33 | if (status === 200) { 34 | yield put({ 35 | type: 'handle', 36 | payload: { 37 | comments: data, 38 | }, 39 | }) 40 | } 41 | }, 42 | 43 | *categories({ payload }, { call, put }) { 44 | const { status, data } = yield call(getCategories, payload) 45 | if (status === 200) { 46 | yield put({ 47 | type: 'handle', 48 | payload: { 49 | categories: data, 50 | }, 51 | }) 52 | } 53 | }, 54 | 55 | *deleteCategory({ payload }, { call, put }) { 56 | const { status, data } = yield call(deleteCategory, payload) 57 | if (status === 200) { 58 | yield put({ 59 | type: 'deleteCategoryHandle', 60 | payload: data, 61 | }) 62 | } 63 | }, 64 | 65 | *createCategory({ payload }, { call, put }) { 66 | const { status, data } = yield call(createCategory, payload) 67 | if (status === 200) { 68 | yield put({ 69 | type: 'createCategoryHandle', 70 | payload: data, 71 | }) 72 | } 73 | }, 74 | 75 | *tags({ payload }, { call, put }) { 76 | const { status, data } = yield call(getTags, payload) 77 | if (status === 200) { 78 | yield put({ 79 | type: 'handle', 80 | payload: { 81 | tags: data, 82 | }, 83 | }) 84 | } 85 | }, 86 | 87 | *deleteTag({ payload }, { call, put }) { 88 | const { status, data } = yield call(deleteTag, payload) 89 | if (status === 200) { 90 | yield put({ 91 | type: 'deleteTagHandle', 92 | payload: data, 93 | }) 94 | } 95 | }, 96 | *createTag({ payload }, { call, put }) { 97 | const { status, data } = yield call(createTag, payload) 98 | if (status === 200) { 99 | yield put({ 100 | type: 'createTagHandle', 101 | payload: data, 102 | }) 103 | } 104 | }, 105 | *deleteArticle({ payload }, { call, put }) { 106 | const { status, data } = yield call(deleteArticle, payload) 107 | if (status === 200) { 108 | yield put({ 109 | type: 'deleteArticleHandle', 110 | payload: data, 111 | }) 112 | } 113 | }, 114 | *deleteComment({ payload }, { call, put }) { 115 | const { status, data } = yield call(deleteComment, payload) 116 | if (status === 200) { 117 | yield put({ 118 | type: 'deleteCommentHandle', 119 | payload: data, 120 | }) 121 | } 122 | }, 123 | *articles({ payload }, { call, put }) { 124 | const { status, data } = yield call(getArticles, payload) 125 | if (status === 200) { 126 | yield put({ 127 | type: 'handle', 128 | payload: { 129 | articles: data.articles, 130 | articleCount: data.count, 131 | }, 132 | }) 133 | } 134 | }, 135 | }, 136 | reducers: { 137 | handle(state, { payload }) { 138 | return { ...state, ...payload } 139 | }, 140 | createCategoryHandle(state, { payload }) { 141 | return { 142 | ...state, 143 | categories: [...state.categories, payload], 144 | } 145 | }, 146 | createTagHandle(state, { payload }) { 147 | return { 148 | ...state, 149 | tags: [...state.tags, payload], 150 | } 151 | }, 152 | deleteCategoryHandle(state, { payload }) { 153 | return { 154 | ...state, 155 | categories: [...state.categories].filter( 156 | item => item.id !== payload.id, 157 | ), 158 | } 159 | }, 160 | deleteTagHandle(state, { payload }) { 161 | return { 162 | ...state, 163 | tags: [...state.tags].filter(item => item.id !== payload.id), 164 | } 165 | }, 166 | deleteArticleHandle(state, { payload }) { 167 | return { 168 | ...state, 169 | articles: [...state.articles].filter(item => item.id !== payload.id), 170 | } 171 | }, 172 | deleteCommentHandle(state, { payload }) { 173 | return { 174 | ...state, 175 | comments: [...state.comments].filter(item => item.id !== payload.id), 176 | } 177 | }, 178 | }, 179 | } 180 | -------------------------------------------------------------------------------- /src/models/article.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 柒叶 3 | * @Date: 2020-04-07 12:55:33 4 | * @Last Modified by: 柒叶 5 | * @Last Modified time: 2020-05-12 14:58:18 6 | */ 7 | import { history } from 'umi' 8 | import { 9 | getCategories, 10 | getArticles, 11 | getHotArticles, 12 | getArticleDetail, 13 | getComments, 14 | getTags, 15 | createNoLoginComment, 16 | createComment, 17 | updateFavorite, 18 | getIsFavorite, 19 | } from '@/services/article' 20 | 21 | export default { 22 | namespace: 'article', 23 | state: { 24 | categories: [], 25 | articles: [], 26 | hots: [], 27 | comments: [], 28 | tags: [], 29 | detail: {}, 30 | articleCount: 0, 31 | isFavorite: false, 32 | favoriteCount: 0, 33 | }, 34 | effects: { 35 | *categories({ payload }, { call, put }) { 36 | const { status, data } = yield call(getCategories, payload) 37 | if (status === 200) { 38 | yield put({ 39 | type: 'handle', 40 | payload: { 41 | categories: data, 42 | }, 43 | }) 44 | } 45 | }, 46 | 47 | *articles({ payload }, { call, put }) { 48 | const { status, data } = yield call(getArticles, payload) 49 | if (status === 200) { 50 | yield put({ 51 | type: 'handle', 52 | payload: { 53 | articles: data.articles, 54 | articleCount: data.count, 55 | }, 56 | }) 57 | } 58 | }, 59 | 60 | *hot({ payload }, { call, put }) { 61 | const { status, data } = yield call(getHotArticles, payload) 62 | if (status === 200) { 63 | yield put({ 64 | type: 'handle', 65 | payload: { 66 | hots: data, 67 | }, 68 | }) 69 | } 70 | }, 71 | 72 | *detail({ payload }, { call, put }) { 73 | const { status, data } = yield call(getArticleDetail, payload) 74 | if (status === 200) { 75 | yield put({ 76 | type: 'handle', 77 | payload: { 78 | detail: data, 79 | favoriteCount: data.favorite, 80 | }, 81 | }) 82 | } 83 | }, 84 | 85 | *comments({ payload }, { call, put }) { 86 | const { status, data } = yield call(getComments, payload) 87 | if (status === 200) { 88 | yield put({ 89 | type: 'handle', 90 | payload: { 91 | comments: data, 92 | }, 93 | }) 94 | } 95 | }, 96 | 97 | *tags({ payload }, { call, put }) { 98 | const { status, data } = yield call(getTags, payload) 99 | if (status === 200) { 100 | yield put({ 101 | type: 'handle', 102 | payload: { 103 | tags: data, 104 | }, 105 | }) 106 | } 107 | }, 108 | 109 | *addNoLoginComment({ payload }, { call, put }) { 110 | const { status, data } = yield call(createNoLoginComment, payload) 111 | if (status === 200) { 112 | yield put({ 113 | type: 'createCommentHandle', 114 | payload: data, 115 | }) 116 | } 117 | }, 118 | 119 | *addComment({ payload }, { call, put }) { 120 | const { status, data } = yield call(createComment, payload) 121 | if (status === 200) { 122 | yield put({ 123 | type: 'createCommentHandle', 124 | payload: data, 125 | }) 126 | } 127 | }, 128 | 129 | *favorite({ payload }, { call, put }) { 130 | const { status } = yield call(updateFavorite, payload) 131 | if (status === 200) { 132 | yield put({ type: 'changeFavorite' }) 133 | } else { 134 | history.push('/login') 135 | } 136 | }, 137 | 138 | *isFavorite({ payload, callback }, { call, put }) { 139 | const { status, data } = yield call(getIsFavorite, payload) 140 | if (status === 200) { 141 | yield put({ 142 | type: 'handle', 143 | payload: { 144 | isFavorite: data, 145 | }, 146 | }) 147 | } 148 | }, 149 | }, 150 | reducers: { 151 | changeFavorite(state) { 152 | const type = state.isFavorite ? 'reduce' : 'plus' 153 | let favoriteCount = state.favoriteCount 154 | if (type === 'plus') { 155 | favoriteCount += 1 156 | } 157 | if (type === 'reduce') { 158 | favoriteCount -= 1 159 | } 160 | return { 161 | ...state, 162 | isFavorite: !state.isFavorite, 163 | favoriteCount, 164 | } 165 | }, 166 | handle(state, { payload }) { 167 | return { ...state, ...payload } 168 | }, 169 | createCommentHandle(state, { payload }) { 170 | return { 171 | ...state, 172 | comments: [payload, ...state.comments], 173 | } 174 | }, 175 | }, 176 | } 177 | -------------------------------------------------------------------------------- /src/models/user.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 柒叶 3 | * @Date: 2020-05-06 09:25:04 4 | * @Last Modified by: 柒叶 5 | * @Last Modified time: 2020-05-17 19:31:36 6 | */ 7 | import { message } from 'antd' 8 | import { history } from 'umi' 9 | import { 10 | registerAccount, 11 | loginAccount, 12 | getAccount, 13 | logoutAccount, 14 | modifyAccount, 15 | } from '@/services/user' 16 | import storageHelper from '@/utils/storage' 17 | 18 | const avatars = [ 19 | 'https://immisso.oss-cn-hangzhou.aliyuncs.com/avatar/001.png', 20 | 'https://immisso.oss-cn-hangzhou.aliyuncs.com/avatar/002.png', 21 | 'https://immisso.oss-cn-hangzhou.aliyuncs.com/avatar/003.png', 22 | 'https://immisso.oss-cn-hangzhou.aliyuncs.com/avatar/004.png', 23 | ] 24 | 25 | const initAccount = () => { 26 | const user = storageHelper.get('user') 27 | if (!user || user.exp * 1000 < new Date().getTime()) { 28 | return {} 29 | } 30 | return user 31 | } 32 | 33 | export default { 34 | namespace: 'user', 35 | state: { 36 | account: initAccount(), 37 | avatar: null, 38 | }, 39 | effects: { 40 | *register({ payload }, { call, put }) { 41 | const { status } = yield call(registerAccount, payload) 42 | if (status === 200) { 43 | message.success('注册成功') 44 | history.push({ pathname: '/login', isRegister: true }) 45 | } else { 46 | message.warn('注册失败,请重新注册') 47 | } 48 | }, 49 | 50 | *login({ payload, callback }, { call, put }) { 51 | const response = yield call(loginAccount, payload) 52 | if (response.status !== 200) { 53 | message.error(response.message) 54 | } else { 55 | message.success('登录成功') 56 | if (callback) callback(response) 57 | } 58 | }, 59 | 60 | *account({ payload, callback }, { call, put }) { 61 | const response = yield call(getAccount, payload) 62 | if (response.status === 200) { 63 | storageHelper.set('user', response.data) 64 | if (callback) callback(response) 65 | yield put({ 66 | type: 'handle', 67 | payload: { 68 | account: response.data, 69 | avatar: response.data.avatar, 70 | }, 71 | }) 72 | } 73 | }, 74 | 75 | *logout({ payload }, { call, put }) { 76 | yield call(logoutAccount, payload) 77 | yield put({ 78 | type: 'handle', 79 | payload: { 80 | account: {}, 81 | }, 82 | }) 83 | }, 84 | 85 | *setAccount({ payload, callback }, { call, put }) { 86 | const { status, data } = yield call(modifyAccount, payload) 87 | if (status === 200) { 88 | yield put({ 89 | type: 'handle', 90 | payload: { 91 | account: data, 92 | }, 93 | }) 94 | message.success('更新成功') 95 | } 96 | }, 97 | }, 98 | reducers: { 99 | handle(state, { payload }) { 100 | return { ...state, ...payload } 101 | }, 102 | changeAvatar(state) { 103 | return { ...state, avatar: avatars[Math.floor(Math.random() * 4)] } 104 | }, 105 | }, 106 | } 107 | -------------------------------------------------------------------------------- /src/models/write.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 柒叶 3 | * @Date: 2020-04-07 12:55:33 4 | * @Last Modified by: 柒叶 5 | * @Last Modified time: 2020-05-21 07:28:44 6 | */ 7 | import { message } from 'antd' 8 | import { history } from 'umi' 9 | import { 10 | getDraft, 11 | createDraft, 12 | updateDraft, 13 | getDrafts, 14 | getCategories, 15 | createPublish, 16 | deleteDraft, 17 | } from '@/services/write' 18 | 19 | export default { 20 | namespace: 'write', 21 | state: { 22 | drafts: [], 23 | categories: [], 24 | tags: [], 25 | markdown: '', 26 | title: null, 27 | selectedCategory: null, 28 | selectedTag: null, 29 | }, 30 | effects: { 31 | *saveDraft({ payload, callback }, { call, put }) { 32 | const { status, data } = yield call(createDraft, payload) 33 | if (status === 200) { 34 | history.push(`/write/draft/${data.id}`) 35 | message.success('保存草稿成功') 36 | } 37 | }, 38 | 39 | *draft({ payload }, { call, put }) { 40 | const { status, data } = yield call(getDraft, payload) 41 | if (status === 200) { 42 | yield put({ 43 | type: 'handle', 44 | payload: { 45 | markdown: data.markdown, 46 | title: data.title, 47 | }, 48 | }) 49 | } 50 | }, 51 | 52 | *drafts({ payload }, { call, put }) { 53 | const { status, data } = yield call(getDrafts, payload) 54 | if (status === 200) { 55 | yield put({ 56 | type: 'handle', 57 | payload: { 58 | drafts: data, 59 | }, 60 | }) 61 | } 62 | }, 63 | 64 | *categories({ payload }, { call, put }) { 65 | const { status, data } = yield call(getCategories, payload) 66 | if (status === 200) { 67 | yield put({ 68 | type: 'categoriesHandle', 69 | payload: { 70 | categories: data, 71 | selectedCategory: data.length > 0 && data[0].id, 72 | tags: data.length > 0 && data[0].tags, 73 | selectedTag: 74 | data.length > 0 && data[0].tags.length > 0 && data[0].tags[0].id, 75 | }, 76 | }) 77 | } 78 | }, 79 | 80 | *updateDraft({ payload }, { call, put }) { 81 | const { status } = yield call(updateDraft, payload) 82 | if (status === 200) { 83 | message.success('保存草稿成功') 84 | } 85 | }, 86 | 87 | *deleteDraft({ payload }, { call, put }) { 88 | const { status, data } = yield call(deleteDraft, payload) 89 | if (status === 200) { 90 | yield put({ 91 | type: 'deleteDraftHandle', 92 | payload: data, 93 | }) 94 | } 95 | }, 96 | 97 | *publish({ payload, callback }, { call, put }) { 98 | const { status } = yield call(createPublish, payload) 99 | if (status === 200) { 100 | message.success('发布文章成功') 101 | yield put({ 102 | type: 'setMarkdown', 103 | payload: { markdown: '' }, 104 | }) 105 | history.push('/') 106 | } 107 | }, 108 | }, 109 | reducers: { 110 | handle(state, { payload }) { 111 | return { ...state, ...payload } 112 | }, 113 | 114 | categoriesHandle(state, { payload }) { 115 | return { 116 | ...state, 117 | ...payload, 118 | selectedCategory: state.selectedCategory || payload.selectedCategory, 119 | selectedTag: state.selectedTag || payload.selectedTag, 120 | } 121 | }, 122 | deleteDraftHandle(state, { payload }) { 123 | return { 124 | ...state, 125 | drafts: [...state.drafts].filter(item => item.id !== payload.id), 126 | } 127 | }, 128 | 129 | setSelecteCategory(state, { payload }) { 130 | return { ...state, selectedCategory: payload.selectedCategory } 131 | }, 132 | 133 | setSelecteTag(state, { payload }) { 134 | return { ...state, selectedTag: payload.selectedTag } 135 | }, 136 | 137 | setTags(state, { payload }) { 138 | return { 139 | ...state, 140 | tags: payload.tags, 141 | selectedTag: payload.tags.length > 0 ? payload.tags[0].id : null, 142 | } 143 | }, 144 | 145 | setMarkdown(state, { payload }) { 146 | return { ...state, markdown: payload.markdown } 147 | }, 148 | 149 | setTitle(state, { payload }) { 150 | return { ...state, title: payload.title } 151 | }, 152 | }, 153 | } 154 | -------------------------------------------------------------------------------- /src/pages/404/index.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 柒叶 3 | * @Date: 2020-05-12 16:05:52 4 | * @Last Modified by: 柒叶 5 | * @Last Modified time: 2020-05-12 16:33:39 6 | */ 7 | 8 | import React from 'react' 9 | import { Button, Space } from 'antd' 10 | import { Link } from 'umi' 11 | 12 | const NoPage = props => { 13 | return ( 14 |
15 |

404

16 |
很抱歉,该页面不存在!(* ̄︶ ̄)
17 |
18 | 19 | 点击 20 | 23 | 送你回去 24 | 25 |
26 |
27 | ) 28 | } 29 | 30 | export default NoPage 31 | -------------------------------------------------------------------------------- /src/pages/Account/index.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 柒叶 3 | * @Date: 2020-05-06 20:18:13 4 | * @Last Modified by: 柒叶 5 | * @Last Modified time: 2020-05-17 19:29:37 6 | */ 7 | import React from 'react' 8 | import { Menu, Row, Col } from 'antd' 9 | import { Link } from 'umi' 10 | import Header from '@/components/Header' 11 | import styles from './index.less' 12 | 13 | const Account = props => { 14 | const { 15 | children, 16 | location: { pathname }, 17 | } = props 18 | return ( 19 | <> 20 |
21 | 22 |
23 |
24 |
25 | 30 | 31 | 我的信息 32 | 33 | 主题设置 34 | 代码风格 35 | 36 |
37 |
{children}
38 |
39 | 40 | 41 | 42 | ) 43 | } 44 | 45 | export default Account 46 | -------------------------------------------------------------------------------- /src/pages/Account/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd/lib/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: @body-background; 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-top: 8px; 26 | padding-right: 40px; 27 | padding-bottom: 8px; 28 | padding-left: 40px; 29 | .title { 30 | margin-bottom: 12px; 31 | color: @heading-color; 32 | font-weight: 500; 33 | font-size: 20px; 34 | line-height: 28px; 35 | } 36 | } 37 | :global { 38 | .ant-list-split .ant-list-item:last-child { 39 | border-bottom: 1px solid #e8e8e8; 40 | } 41 | .ant-list-item { 42 | padding-top: 14px; 43 | padding-bottom: 14px; 44 | } 45 | } 46 | } 47 | :global { 48 | .ant-list-item-meta { 49 | // 账号绑定图标 50 | .taobao { 51 | display: block; 52 | color: #ff4000; 53 | font-size: 48px; 54 | line-height: 48px; 55 | border-radius: @border-radius-base; 56 | } 57 | .dingding { 58 | margin: 2px; 59 | padding: 6px; 60 | color: #fff; 61 | font-size: 32px; 62 | line-height: 32px; 63 | background-color: #2eabff; 64 | border-radius: @border-radius-base; 65 | } 66 | .alipay { 67 | color: #2eabff; 68 | font-size: 48px; 69 | line-height: 48px; 70 | border-radius: @border-radius-base; 71 | } 72 | } 73 | 74 | // 密码强度 75 | font.strong { 76 | color: @success-color; 77 | } 78 | font.medium { 79 | color: @warning-color; 80 | } 81 | font.weak { 82 | color: @error-color; 83 | } 84 | } 85 | 86 | @media screen and (max-width: @screen-md) { 87 | .main { 88 | flex-direction: column; 89 | .leftmenu { 90 | width: 100%; 91 | border: none; 92 | } 93 | .right { 94 | padding: 40px; 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/pages/Admin/index.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 柒叶 3 | * @Date: 2020-04-27 17:56:34 4 | * @Last Modified by: 柒叶 5 | * @Last Modified time: 2020-05-17 19:37:23 6 | */ 7 | 8 | import React, { useEffect } from 'react' 9 | import { Link } from 'umi' 10 | import { connect } from 'dva' 11 | import ProLayout from '@ant-design/pro-layout' 12 | import { 13 | FileTextOutlined, 14 | TagsOutlined, 15 | CommentOutlined, 16 | ClusterOutlined, 17 | } from '@ant-design/icons' 18 | 19 | const routes = { 20 | routes: [ 21 | { 22 | exact: true, 23 | name: '分类管理', 24 | icon: , 25 | path: '/admin/categories', 26 | }, 27 | { 28 | exact: true, 29 | name: '标签管理', 30 | icon: , 31 | path: '/admin/tags', 32 | }, 33 | { 34 | exact: true, 35 | name: '文章管理', 36 | icon: , 37 | path: '/admin/articles', 38 | }, 39 | { 40 | exact: true, 41 | name: '评论管理', 42 | icon: , 43 | path: '/admin/comments', 44 | }, 45 | ], 46 | } 47 | 48 | const Admin = props => { 49 | const { children, account, history } = props 50 | useEffect(() => { 51 | if (!account || !account.id) { 52 | history.push('/login') 53 | } 54 | if (account.account_type !== 'ADMIN') { 55 | history.push('/404') 56 | } 57 | }, []) 58 | return ( 59 |
60 | { 70 | if ( 71 | menuItemProps.isUrl || 72 | menuItemProps.children || 73 | !menuItemProps.path 74 | ) { 75 | return defaultDom 76 | } 77 | 78 | return {defaultDom} 79 | }} 80 | > 81 | {children} 82 | 83 |
84 | ) 85 | } 86 | 87 | export default connect(({ user: { account }, loading }) => ({ 88 | account, 89 | loading, 90 | }))(Admin) 91 | -------------------------------------------------------------------------------- /src/pages/Article/index.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 柒叶 3 | * @Date: 2020-04-09 21:43:20 4 | * @Last Modified by: 柒叶 5 | * @Last Modified time: 2020-05-15 10:54:50 6 | */ 7 | 8 | import React, { useEffect, useState } from 'react' 9 | import { Layout, Card, List, Row, Col, Divider, Tooltip } from 'antd' 10 | import moment from 'moment' 11 | import { Link } from 'umi' 12 | import { connect } from 'dva' 13 | import { 14 | createFromIconfontCN, 15 | WeiboCircleOutlined, 16 | GlobalOutlined, 17 | GithubOutlined, 18 | EyeOutlined, 19 | LikeOutlined, 20 | MessageOutlined, 21 | } from '@ant-design/icons' 22 | import MathJax from 'react-mathjax' 23 | import Header from '@/components/Header' 24 | import UserAvatar from '@/components/UserAvatar' 25 | import ArticleAnchor from '@/components/Anchor' 26 | import AddComment from '@/components/Comment' 27 | import Markdown from '@/components/Markdown' 28 | 29 | import styles from './index.less' 30 | import './markdown.css' 31 | 32 | const { Content } = Layout 33 | const IconFont = createFromIconfontCN({ 34 | scriptUrl: '//at.alicdn.com/t/font_1439645_kzb7blmpkvc.js', 35 | }) 36 | 37 | const Article = props => { 38 | const { 39 | dispatch, 40 | loading, 41 | loading2, 42 | detail, 43 | hots, 44 | isFavorite, 45 | favoriteCount, 46 | match: { 47 | params: { id }, 48 | }, 49 | } = props 50 | 51 | useEffect(() => { 52 | if (dispatch) { 53 | dispatch({ type: 'article/detail', payload: { id } }) 54 | dispatch({ type: 'article/isFavorite', payload: { id } }) 55 | dispatch({ type: 'article/hot' }) 56 | } 57 | }, []) 58 | 59 | const handleFavorite = () => { 60 | if (dispatch) { 61 | dispatch({ 62 | type: 'article/favorite', 63 | payload: { id, author: detail.uid }, 64 | }) 65 | } 66 | } 67 | 68 | return ( 69 | <> 70 |
71 | 72 |
73 |
74 | 80 |
81 |
89 |
90 | {detail && detail.user && detail.user.avatar && ( 91 | 92 | )} 93 |
94 |

95 | {detail.user && detail.user.nickname} 96 |

97 | 98 | {moment(detail.createdAt).format( 99 | 'YYYY[年]MM[月]DD[日]', 100 | )} 101 | {detail.view}阅读 102 | 103 |
104 |
105 |
106 | {detail && detail.cover && ( 107 |
108 | 109 |
110 | )} 111 | 112 |

{detail.title}

113 |
114 | 115 | 116 | 117 |
118 |
119 |
120 | 121 |
122 |
123 | 129 |
130 | {detail && detail.user && detail.user.avatar && ( 131 | 132 | )} 133 |
134 |
{detail.user && detail.user.nickname}
135 | {detail.user && detail.user.profession} 136 |
137 |
138 | 144 |
145 |

146 | {detail.user && detail.user.total_view} 147 |

148 | 浏览 149 | 150 | 151 |

152 | {detail.user && detail.user.total_like} 153 |

154 | 点赞 155 | 156 | 157 |

158 | {detail.user && detail.user.total_comment} 159 |

160 | 评论 161 | 162 | 163 | 164 |
165 | {detail.user && detail.user.website && ( 166 | 167 | 168 | 169 | 170 | 171 | )} 172 | {detail.user && detail.user.github && ( 173 | 174 | 175 | 176 | 177 | 178 | )} 179 | {detail.user && detail.user.weibo && ( 180 | 181 | 182 | 183 | 184 | 185 | )} 186 | {detail.user && detail.user.gitee && ( 187 | 188 | 189 | 190 | 191 | 192 | )} 193 |
194 | 195 | 202 | ( 209 | 213 | 214 | {item.view} 215 | , 216 | 217 | 218 | {item.favorite} 219 | , 220 | ]} 221 | > 222 | 227 | {item.title} 228 | 229 | 230 | )} 231 | /> 232 | 233 | {detail && detail.anchor && ( 234 | 235 | )} 236 | 237 |
238 |
239 |
240 | 244 |
245 |
246 | {favoriteCount} 247 |
248 |
249 |
250 |
251 | 252 |
253 |
254 | {detail.comment} 255 |
256 |
257 |
258 | 259 | 260 | 261 | ) 262 | } 263 | 264 | export default connect( 265 | ({ article: { detail, hots, isFavorite, favoriteCount }, loading }) => ({ 266 | detail, 267 | hots, 268 | isFavorite, 269 | favoriteCount, 270 | loading: loading.effects['article/detail'], 271 | loading2: loading.effects['article/hot'], 272 | }), 273 | )(Article) 274 | -------------------------------------------------------------------------------- /src/pages/Article/index.less: -------------------------------------------------------------------------------- 1 | .articlePanel { 2 | position: fixed; 3 | width: 40px; 4 | height: 200px; 5 | top: 16rem; 6 | margin-left: -4rem; 7 | .articlePanelItem { 8 | display: flex; 9 | flex-flow: column; 10 | width: 40px; 11 | margin-bottom: 20px; 12 | align-items: center; 13 | .articlePanelIcon { 14 | margin-bottom: 5px; 15 | width: 38px; 16 | height: 38px; 17 | background: #ffffff; 18 | border-radius: 50%; 19 | font-size: 20px; 20 | display: flex; 21 | align-items: center; 22 | justify-content: center; 23 | cursor: pointer; 24 | &:hover { 25 | outline-color: #cccccc; 26 | } 27 | } 28 | .articlePanelCount { 29 | color: #cccccc; 30 | } 31 | } 32 | } 33 | 34 | .articleContainer { 35 | width: 1152px; 36 | padding: 1.2rem 0; 37 | margin: 0 auto; 38 | .articleContainerWrapper { 39 | display: flex; 40 | justify-content: space-between; 41 | .articleContainerDetail { 42 | width: 700px; 43 | } 44 | .articleContainerSider { 45 | width: 240px; 46 | } 47 | } 48 | } 49 | 50 | @media (max-width: 576px) { 51 | .articleContainer { 52 | width: 100%; 53 | .articleContainerWrapper { 54 | display: flex; 55 | justify-content: space-between; 56 | .articleContainerDetail { 57 | width: 100%; 58 | } 59 | .articleContainerSider { 60 | width: 100%; 61 | display: none; 62 | } 63 | .articlePanel { 64 | display: none; 65 | } 66 | } 67 | } 68 | } 69 | 70 | @media screen and (min-width: 576px) and (max-width: 767px) { 71 | .articleContainer { 72 | width: 90%; 73 | .articleContainerWrapper { 74 | display: flex; 75 | justify-content: space-between; 76 | .articleContainerDetail { 77 | width: 100%; 78 | } 79 | .articleContainerSider { 80 | width: 100%; 81 | display: none; 82 | } 83 | .articlePanel { 84 | display: none; 85 | } 86 | } 87 | } 88 | } 89 | 90 | @media screen and (min-width: 768px) and (max-width: 991px) { 91 | .articleContainer { 92 | width: 750px; 93 | .articleContainerWrapper { 94 | display: flex; 95 | justify-content: space-between; 96 | .articleContainerDetail { 97 | width: 100%; 98 | } 99 | .articleContainerSider { 100 | width: 100%; 101 | display: none; 102 | } 103 | .articlePanel { 104 | display: none; 105 | } 106 | } 107 | } 108 | } 109 | 110 | @media screen and (min-width: 992px) and (max-width: 1199px) { 111 | .articleContainer { 112 | width: 960px; 113 | .articleContainerWrapper { 114 | display: flex; 115 | justify-content: space-between; 116 | .articleContainerDetail { 117 | width: 700px; 118 | } 119 | .articleContainerSider { 120 | width: 240px; 121 | } 122 | } 123 | } 124 | } 125 | 126 | @media screen and (min-width: 1200px) { 127 | .articleContainer { 128 | width: 960px; 129 | .articleContainerWrapper { 130 | display: flex; 131 | justify-content: space-between; 132 | .articleContainerDetail { 133 | width: 700px; 134 | } 135 | .articleContainerSider { 136 | width: 240px; 137 | } 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/pages/Article/markdown.css: -------------------------------------------------------------------------------- 1 | 2 | .markdown-body { 3 | word-wrap: break-word; 4 | line-height: 1.8; 5 | font-weight: 400; 6 | font-size: 16px; 7 | } 8 | 9 | .markdown-body h1, .markdown-body h2, .markdown-body h3, .markdown-body h4, .markdown-body h5, .markdown-body h6 { 10 | /* margin-top: 24px; */ 11 | margin-bottom: 16px; 12 | font-weight: 600; 13 | line-height: 1.25; 14 | } 15 | 16 | .markdown-body h2 { 17 | padding-bottom: 0.3em; 18 | font-size: 1.5em; 19 | border-bottom: 1px solid #eaecef; 20 | } 21 | 22 | .markdown-body p, .markdown-body blockquote, .markdown-body ul, .markdown-body ol, .markdown-body dl, .markdown-body table, .markdown-body pre { 23 | margin-top: 0; 24 | margin-bottom: 16px; 25 | } 26 | 27 | .markdown-body blockquote { 28 | padding: 0 1em; 29 | color: #6a737d; 30 | border-left: 0.25em solid #4680be; 31 | } 32 | 33 | .markdown-body code { 34 | background: #fff5f5; 35 | color: #ff502c; 36 | font-size: 0.87em; 37 | padding: 0.065em 0.4em; 38 | } 39 | 40 | .markdown-body pre>code{ 41 | background: inherit; 42 | padding: 0; 43 | } 44 | 45 | .markdown-body a { 46 | color: #0269c8; 47 | border-bottom: 1px dashed #0269c8; 48 | } 49 | 50 | .markdown-body blockquote { 51 | padding: 0 1em; 52 | color: #728498; 53 | border-left: 0.25em solid #6596ca; 54 | background: #fafafa; 55 | } 56 | 57 | /* .markdown-body table { 58 | border-spacing: 0; 59 | border-collapse: collapse; 60 | } */ 61 | 62 | .markdown-body table { 63 | margin: 0.8em 0; 64 | border-spacing: 0; 65 | border-collapse: collapse; 66 | } 67 | 68 | .markdown-body table tr { 69 | border-top: 1px solid #dfe2e5; 70 | margin: 0; 71 | padding: 0; 72 | } 73 | 74 | .markdown-body table tr:nth-child(2n), 75 | .markdown-body thead { 76 | background-color: #fafafa; 77 | } 78 | 79 | .markdown-body table tr th { 80 | font-weight: bold; 81 | border: 1px solid #dfe2e5; 82 | border-bottom: 0; 83 | text-align: left; 84 | margin: 0; 85 | padding: 6px 13px; 86 | } 87 | 88 | .markdown-body table tr td { 89 | border: 1px solid #dfe2e5; 90 | text-align: left; 91 | margin: 0; 92 | padding: 6px 13px; 93 | } 94 | 95 | .markdown-body table tr th:first-child, 96 | .markdown-body table tr td:first-child { 97 | margin-top: 0; 98 | } 99 | 100 | .markdown-body table tr th:last-child, 101 | .markdown-body table tr td:last-child { 102 | margin-bottom: 0; 103 | } 104 | 105 | 106 | 107 | /* .markdown-here-wrapper { 108 | font-size: 16px; 109 | line-height: 1.8em; 110 | letter-spacing: 0.1em; 111 | } 112 | 113 | pre, 114 | code { 115 | font-size: 14px; 116 | font-family: Roboto, 'Courier New', Consolas, Inconsolata, Courier, monospace; 117 | margin: auto 5px; 118 | } 119 | 120 | code { 121 | white-space: pre-wrap; 122 | border-radius: 2px; 123 | display: inline; 124 | } 125 | 126 | pre { 127 | font-size: 15px; 128 | line-height: 1.4em; 129 | display: block !important; 130 | } 131 | 132 | pre code { 133 | white-space: pre; 134 | overflow: auto; 135 | border-radius: 3px; 136 | padding: 1px 1px; 137 | display: block !important; 138 | } 139 | 140 | strong, 141 | b { 142 | color: #BF360C; 143 | } 144 | 145 | em, 146 | i { 147 | color: #009688; 148 | } 149 | 150 | hr { 151 | border: 1px solid #BF360C; 152 | margin: 1.5em auto; 153 | } 154 | 155 | p { 156 | margin: 1.5em 5px !important; 157 | } 158 | 159 | table, 160 | pre, 161 | dl, 162 | blockquote, 163 | q, 164 | ul, 165 | ol { 166 | margin: 10px 5px; 167 | } 168 | 169 | ul, 170 | ol { 171 | padding-left: 15px; 172 | } 173 | 174 | li { 175 | margin: 10px; 176 | } 177 | 178 | li p { 179 | margin: 10px 0 !important; 180 | } 181 | 182 | ul ul, 183 | ul ol, 184 | ol ul, 185 | ol ol { 186 | margin: 0; 187 | padding-left: 10px; 188 | } 189 | 190 | ul { 191 | list-style-type: circle; 192 | } 193 | 194 | dl { 195 | padding: 0; 196 | } 197 | 198 | dl dt { 199 | font-size: 1em; 200 | font-weight: bold; 201 | font-style: italic; 202 | } 203 | 204 | dl dd { 205 | margin: 0 0 10px; 206 | padding: 0 10px; 207 | } 208 | 209 | blockquote, 210 | q { 211 | border-left: 2px solid #009688; 212 | padding: 0 10px; 213 | color: #777; 214 | quotes: none; 215 | margin-left: 1em; 216 | } 217 | 218 | blockquote::before, 219 | blockquote::after, 220 | q::before, 221 | q::after { 222 | content: none; 223 | } 224 | 225 | h1, 226 | h2, 227 | h3, 228 | h4, 229 | h5, 230 | h6 { 231 | margin: 20px 0 10px; 232 | padding: 0; 233 | font-style: bold !important; 234 | color: #009688 !important; 235 | text-align: center !important; 236 | margin: 1.5em 5px !important; 237 | padding: 0.5em 1em !important; 238 | } 239 | 240 | h1 { 241 | font-size: 24px !important; 242 | border-bottom: 1px solid #ddd !important; 243 | } 244 | 245 | h2 { 246 | font-size: 20px !important; 247 | border-bottom: 1px solid #eee !important; 248 | } 249 | 250 | h3 { 251 | font-size: 18px; 252 | } 253 | 254 | h4 { 255 | font-size: 16px; 256 | } 257 | 258 | 259 | table { 260 | padding: 0; 261 | border-collapse: collapse; 262 | border-spacing: 0; 263 | font-size: 1em; 264 | font: inherit; 265 | border: 0; 266 | margin: 0 auto; 267 | } 268 | 269 | tbody { 270 | margin: 0; 271 | padding: 0; 272 | border: 0; 273 | } 274 | 275 | table tr { 276 | border: 0; 277 | border-top: 1px solid #CCC; 278 | background-color: white; 279 | margin: 0; 280 | padding: 0; 281 | } 282 | 283 | table tr:nth-child(2n) { 284 | background-color: #F8F8F8; 285 | } 286 | 287 | table tr th, 288 | table tr td { 289 | font-size: 16px; 290 | border: 1px solid #CCC; 291 | margin: 0; 292 | padding: 5px 10px; 293 | } 294 | 295 | table tr th { 296 | font-weight: bold; 297 | color: #eee; 298 | border: 1px solid #009688; 299 | background-color: #009688; 300 | } */ 301 | -------------------------------------------------------------------------------- /src/pages/Course/index.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 柒叶 3 | * @Date: 2020-05-21 09:51:22 4 | * @Last Modified by: 柒叶 5 | * @Last Modified time: 2020-05-21 10:50:31 6 | */ 7 | import React, { useState, useEffect } from 'react' 8 | import { Cow, Col, Drawer, Layout, Tree } from 'antd' 9 | import styles from './index.less' 10 | const { Header, Footer, Sider, Content } = Layout 11 | const { TreeNode } = Tree 12 | const Course = props => { 13 | return ( 14 | 15 | 22 | } 25 | defaultExpandedKeys={['0-0-0']} 26 | // onSelect={this.onSelect} 27 | className={`${styles.menuTree} ft-13`} 28 | > 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |
Header
44 | Content 45 |
46 |
47 | ) 48 | } 49 | 50 | export default Course 51 | -------------------------------------------------------------------------------- /src/pages/Course/index.less: -------------------------------------------------------------------------------- 1 | .courseSider { 2 | height: calc(100vh); 3 | background-color: #eeeeee; 4 | // padding-left: 20px; 5 | .menuTree { 6 | background-color: #eeeeee; 7 | margin-top: 20px; 8 | margin-left: 20px; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/pages/Draft/index.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 柒叶 3 | * @Date: 2020-05-20 17:30:58 4 | * @Last Modified by: 柒叶 5 | * @Last Modified time: 2020-05-21 07:47:15 6 | */ 7 | import React, { useEffect } from 'react' 8 | import { Card, List, Skeleton, Tag, Popconfirm } from 'antd' 9 | import { connect } from 'dva' 10 | import { Link } from 'umi' 11 | import moment from 'moment' 12 | import Header from '@/components/Header' 13 | import styles from './index.less' 14 | 15 | const Draft = props => { 16 | const { dispatch, drafts, loading, account, history } = props 17 | useEffect(() => { 18 | if (!account || !account.id) { 19 | history.push('/login') 20 | } 21 | if (dispatch) { 22 | dispatch({ type: 'write/drafts' }) 23 | } 24 | }, []) 25 | 26 | const deleteDraft = id => { 27 | if (dispatch) { 28 | dispatch({ type: 'write/deleteDraft', payload: { id } }) 29 | } 30 | } 31 | return ( 32 | <> 33 |
34 |
35 | 36 | ( 41 | 44 | 编辑 45 | , 46 | deleteDraft(item.id)} 50 | okText="确定" 51 | cancelText="取消" 52 | > 53 | 删除 54 | , 55 | ]} 56 | > 57 | 58 | 61 | {item.title} 62 | {item.is_publish ? ( 63 | 64 | 已发表 65 | 66 | ) : null} 67 | 68 | } 69 | description={`上次修改于${moment(item.updatedAt).format( 70 | 'YYYY[年]MM[月]DD[日] HH:mm', 71 | )}`} 72 | /> 73 | 74 | 75 | )} 76 | > 77 | 78 |
79 | 80 | ) 81 | } 82 | 83 | export default connect(({ write: { drafts }, user: { account }, loading }) => ({ 84 | drafts, 85 | account, 86 | loading: loading.effects['write/drafts'], 87 | }))(Draft) 88 | -------------------------------------------------------------------------------- /src/pages/Draft/index.less: -------------------------------------------------------------------------------- 1 | .homeContainer { 2 | width: 750px; 3 | margin: 0 auto; 4 | padding: 1.2rem 0; 5 | } 6 | 7 | @media (max-width: 576px) { 8 | .homeContainer { 9 | width: 100%; 10 | } 11 | } 12 | 13 | @media screen and (min-width: 576px) and (max-width: 767px) { 14 | .homeContainer { 15 | width: 90%; 16 | } 17 | } 18 | 19 | @media screen and (min-width: 768px) { 20 | .homeContainer { 21 | width: 750px; 22 | } 23 | } 24 | 25 | .ant-list-vertical .ant-list-item-action { 26 | margin-top: 5px !important; 27 | } 28 | 29 | .aboutColor { 30 | color: #909090; 31 | } 32 | -------------------------------------------------------------------------------- /src/pages/Home/index.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 柒叶 3 | * @Date: 2020-04-05 11:41:31 4 | * @Last Modified by: 柒叶 5 | * @Last Modified time: 2020-05-13 19:51:34 6 | */ 7 | 8 | import React, { useEffect } from 'react' 9 | import { Layout, Card } from 'antd' 10 | import { connect } from 'dva' 11 | import Header from '@/components/Header' 12 | import SiderList from '@/components/SiderList' 13 | import Tags from '@/components/Tags' 14 | import styles from './index.less' 15 | 16 | const { Content } = Layout 17 | 18 | const Home = props => { 19 | const { 20 | dispatch, 21 | hots, 22 | loading, 23 | children, 24 | location: { pathname }, 25 | } = props 26 | useEffect(() => { 27 | if (dispatch) { 28 | dispatch({ type: 'article/hot' }) 29 | } 30 | }, []) 31 | return ( 32 | <> 33 |
34 | 35 |
36 |
{children}
37 |
38 | 44 | 50 | 51 | 52 |
53 | {/*
54 | 友情链接 55 | www.scxingm.cn 56 |
*/} 57 | {/*
蜀ICP备16032900号-2
*/} 58 |
©2019 柒叶 Create by QiYe
59 |
60 |
61 |
62 |
63 | 64 | ) 65 | } 66 | 67 | export default connect(({ article: { hots }, loading }) => ({ 68 | hots, 69 | loading: loading.effects['article/hot'], 70 | }))(Home) 71 | -------------------------------------------------------------------------------- /src/pages/Home/index.less: -------------------------------------------------------------------------------- 1 | .homeContainer { 2 | width: 1152px; 3 | margin: 0 auto; 4 | padding: 1.2rem 0; 5 | .homeContainerWrapper { 6 | display: flex; 7 | justify-content: space-between; 8 | .homeContainerList { 9 | width: 700px; 10 | } 11 | .homeContainerSiderlist { 12 | width: 240px; 13 | } 14 | } 15 | } 16 | 17 | @media (max-width: 576px) { 18 | .homeContainer { 19 | width: 100%; 20 | .homeContainerWrapper { 21 | display: flex; 22 | justify-content: space-between; 23 | .homeContainerList { 24 | width: 100%; 25 | :global { 26 | .ant-list-item-extra { 27 | display: none; 28 | } 29 | } 30 | } 31 | .homeContainerSiderlist { 32 | display: none; 33 | width: 100%; 34 | } 35 | } 36 | } 37 | } 38 | 39 | @media screen and (min-width: 576px) and (max-width: 767px) { 40 | .homeContainer { 41 | width: 90%; 42 | .homeContainerWrapper { 43 | display: flex; 44 | justify-content: space-between; 45 | .homeContainerList { 46 | width: 100%; 47 | :global { 48 | .ant-list-item-extra { 49 | display: none; 50 | } 51 | } 52 | } 53 | .homeContainerSiderlist { 54 | display: none; 55 | width: 100%; 56 | } 57 | } 58 | } 59 | } 60 | 61 | @media screen and (min-width: 768px) and (max-width: 991px) { 62 | .homeContainer { 63 | width: 750px; 64 | .homeContainerWrapper { 65 | display: flex; 66 | justify-content: space-between; 67 | .homeContainerList { 68 | width: 100%; 69 | :global { 70 | .ant-list-item-extra { 71 | display: none; 72 | } 73 | } 74 | } 75 | .homeContainerSiderlist { 76 | display: none; 77 | width: 100%; 78 | } 79 | } 80 | } 81 | } 82 | 83 | @media screen and (min-width: 992px) and (max-width: 1199px) { 84 | .homeContainer { 85 | width: 960px; 86 | .homeContainerWrapper { 87 | display: flex; 88 | justify-content: space-between; 89 | .homeContainerList { 90 | width: 700px; 91 | } 92 | .homeContainerSiderlist { 93 | width: 240px; 94 | } 95 | } 96 | } 97 | } 98 | 99 | @media screen and (min-width: 1200px) { 100 | .homeContainer { 101 | width: 960px; 102 | .homeContainerWrapper { 103 | display: flex; 104 | justify-content: space-between; 105 | .homeContainerList { 106 | width: 700px; 107 | } 108 | .homeContainerSiderlist { 109 | width: 240px; 110 | } 111 | } 112 | } 113 | } 114 | 115 | .ant-list-vertical .ant-list-item-action { 116 | margin-top: 5px !important; 117 | } 118 | 119 | .aboutColor { 120 | color: #909090; 121 | } 122 | -------------------------------------------------------------------------------- /src/pages/Login/index.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 柒叶 3 | * @Date: 2020-05-05 14:52:52 4 | * @Last Modified by: 柒叶 5 | * @Last Modified time: 2020-05-15 12:59:05 6 | */ 7 | 8 | import React, { useEffect } from 'react' 9 | import { Button, Row, Form, Input, Checkbox } from 'antd' 10 | import { MailOutlined, LockOutlined } from '@ant-design/icons' 11 | import { Link } from 'umi' 12 | import { connect } from 'dva' 13 | 14 | const Login = props => { 15 | const [form] = Form.useForm() 16 | const { dispatch, history, location, account } = props 17 | useEffect(() => { 18 | if (account && account.id) { 19 | history.push('/') 20 | } 21 | }, []) 22 | const onFinish = values => { 23 | if (dispatch) { 24 | dispatch({ 25 | type: 'user/login', 26 | payload: values, 27 | callback(res) { 28 | dispatch({ 29 | type: 'user/account', 30 | callback(user) { 31 | if (location.isRegister) { 32 | history.push('/') 33 | } else { 34 | history.goBack() 35 | } 36 | }, 37 | }) 38 | }, 39 | }) 40 | } 41 | } 42 | return ( 43 | <> 44 | 50 |
51 |

登录

52 |
53 | 66 | } placeholder="输入您的电子邮箱" /> 67 | 68 | 72 | } 74 | type="password" 75 | placeholder="请输入你的密码" 76 | /> 77 | 78 | 79 | 80 | 自动登录 81 | 82 | 83 | 忘记密码 84 | 85 | 86 | 87 | 90 | 91 | 注册账户 92 | 93 |
94 |
95 | 96 | ) 97 | } 98 | 99 | export default connect(({ user: { account }, loading }) => ({ 100 | account, 101 | loading, 102 | }))(Login) 103 | -------------------------------------------------------------------------------- /src/pages/Register/index.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 柒叶 3 | * @Date: 2020-05-06 07:16:03 4 | * @Last Modified by: 柒叶 5 | * @Last Modified time: 2020-05-12 11:25:09 6 | */ 7 | 8 | import React from 'react' 9 | import { Button, Row, Form, Input } from 'antd' 10 | import { MailOutlined, LockOutlined } from '@ant-design/icons' 11 | import { Link } from 'umi' 12 | import { connect } from 'dva' 13 | 14 | const Register = props => { 15 | const { dispatch } = props 16 | const [form] = Form.useForm() 17 | const onFinish = values => { 18 | if (dispatch) { 19 | dispatch({ 20 | type: 'user/register', 21 | payload: values, 22 | }) 23 | } 24 | } 25 | return ( 26 | <> 27 | 33 |
34 |

注册

35 |
36 | 49 | } placeholder="输入您的电子邮箱" /> 50 | 51 | 55 | } 57 | type="password" 58 | placeholder="请输入你的密码" 59 | /> 60 | 61 | ({ 66 | validator(rule, value) { 67 | if (!value || getFieldValue('password') === value) { 68 | return Promise.resolve() 69 | } 70 | return Promise.reject('两次密码不一致') 71 | }, 72 | }), 73 | ]} 74 | > 75 | } 77 | type="password" 78 | placeholder="请输入你的密码" 79 | /> 80 | 81 | 82 | 85 | 86 | 登录账户 87 | 88 |
89 |
90 | 91 | ) 92 | } 93 | 94 | export default connect(({ user, loading }) => ({ 95 | user, 96 | loading, 97 | }))(Register) 98 | -------------------------------------------------------------------------------- /src/pages/Write/index.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 柒叶 3 | * @Date: 2020-04-13 21:20:12 4 | * @Last Modified by: 柒叶 5 | * @Last Modified time: 2020-05-17 19:25:16 6 | */ 7 | 8 | import React, { useState, useEffect, useRef } from 'react' 9 | import ReactDOMServer from 'react-dom/server' 10 | import { connect } from 'dva' 11 | import moment from 'moment' 12 | import { 13 | Input, 14 | Row, 15 | Col, 16 | Button, 17 | Popover, 18 | Tag, 19 | Dropdown, 20 | Menu, 21 | Drawer, 22 | List, 23 | Modal, 24 | Table, 25 | } from 'antd' 26 | import { 27 | CaretDownOutlined, 28 | PictureOutlined, 29 | QuestionCircleOutlined, 30 | } from '@ant-design/icons' 31 | import { history, Link } from 'umi' 32 | import MathJax from 'react-mathjax' 33 | import KeyboardEventHandler from 'react-keyboard-event-handler' 34 | 35 | import UserAvatar from '@/components/UserAvatar' 36 | import Markdown from '@/components/Markdown' 37 | import AliOssUpload from '@/components/AliOssUpload' 38 | 39 | import styles from './index.less' 40 | 41 | const { CheckableTag } = Tag 42 | const { TextArea } = Input 43 | 44 | const Content = props => { 45 | const { 46 | categories, 47 | tags, 48 | selectedTag, 49 | selectedCategory, 50 | checkTagHandle, 51 | checkCategorysHandle, 52 | onPublish, 53 | returnCoverImageUrl, 54 | } = props 55 | return ( 56 |
57 |

分类

58 |
59 | {/* {categories && } */} 60 | {categories && 61 | categories.map(category => ( 62 | checkCategorysHandle(category)} 66 | > 67 | {category.name} 68 | 69 | ))} 70 |
71 |

标签

72 |
73 | {tags && 74 | tags.map(tag => ( 75 | checkTagHandle(tag)} 79 | > 80 | {tag.name} 81 | 82 | ))} 83 |
84 |

文章封面图

85 |
86 | 87 |
88 |
89 | 92 |
93 |
94 | ) 95 | } 96 | 97 | const ShortCutKey = () => { 98 | const columns = [ 99 | { 100 | title: 'Markdown', 101 | dataIndex: 'markdown', 102 | key: 'markdown', 103 | }, 104 | { 105 | title: '说明', 106 | dataIndex: 'explain', 107 | key: 'explain', 108 | }, 109 | { 110 | title: '快捷键', 111 | dataIndex: 'keybord', 112 | key: 'keybord', 113 | }, 114 | ] 115 | const dataSource = [ 116 | { 117 | markdown: '## 标题', 118 | explain: 'H2', 119 | keybord: 'Ctrl / ⌘ + H', 120 | }, 121 | { 122 | markdown: '**文本**', 123 | explain: '加粗', 124 | keybord: 'Ctrl / ⌘ + B', 125 | }, 126 | { 127 | markdown: '*文本*', 128 | explain: '斜体', 129 | keybord: 'Ctrl / ⌘ + Alt + I', 130 | }, 131 | { 132 | markdown: '[描述](链接)', 133 | explain: '链接', 134 | keybord: 'Ctrl / ⌘ + L', 135 | }, 136 | { 137 | markdown: '![描述](链接)', 138 | explain: '插入图片', 139 | keybord: 'Ctrl / ⌘ + I', 140 | }, 141 | { 142 | markdown: '> 引用', 143 | explain: '引用', 144 | keybord: 'Ctrl / ⌘ + Q', 145 | }, 146 | { 147 | markdown: '```code```', 148 | explain: '代码块', 149 | keybord: 'Ctrl / ⌘ + Alt + C', 150 | }, 151 | { 152 | markdown: '`code`', 153 | explain: '行代码块', 154 | keybord: 'Ctrl / ⌘ + Alt + K', 155 | }, 156 | { 157 | markdown: '省略', 158 | explain: '表格', 159 | keybord: 'Ctrl / ⌘ + Alt + T', 160 | }, 161 | ] 162 | return ( 163 |
169 | ) 170 | } 171 | 172 | const ImageModal = props => { 173 | const { 174 | imageModalVisible, 175 | closeImageModal, 176 | insertImageOk, 177 | returnImage, 178 | insertImageValue, 179 | insertImageValueChange, 180 | } = props 181 | 182 | return ( 183 | 194 | 195 |

196 | } 201 | style={{ border: '1px solid #ccc' }} 202 | onChange={insertImageValueChange} 203 | /> 204 |
205 | ) 206 | } 207 | 208 | const Write = props => { 209 | const { 210 | dispatch, 211 | categories, 212 | tags, 213 | title, 214 | markdown, 215 | drafts, 216 | selectedCategory, 217 | selectedTag, 218 | account, 219 | loading, 220 | match: { 221 | params: { key }, 222 | }, 223 | } = props 224 | 225 | const [visible, setVisible] = useState(false) 226 | const [imageModalVisible, setImageModalVisible] = useState(false) 227 | const [coverImageUrl, setCoverImageUrl] = useState(null) 228 | const [insertImages, setInsertImages] = useState([]) 229 | const [insertImageValue, setInsertImageValue] = useState(null) 230 | const inputRef = useRef() 231 | const textAreaRef = useRef() 232 | 233 | useEffect(() => { 234 | if (!account || !account.id) { 235 | history.push('/login') 236 | } 237 | if (dispatch) { 238 | dispatch({ type: 'write/categories' }) 239 | if (key !== 'new' && /^\d+$/.test(key)) { 240 | dispatch({ type: 'write/draft', payload: { id: key } }) 241 | } else { 242 | dispatch({ type: 'write/setMarkdown', payload: { markdown: null } }) 243 | dispatch({ type: 'write/setTitle', payload: { title: null } }) 244 | } 245 | } 246 | if (inputRef) { 247 | inputRef.current.focus() 248 | } 249 | }, [key]) 250 | 251 | const onChangeMarkdown = e => { 252 | if (dispatch) { 253 | dispatch({ 254 | type: 'write/setMarkdown', 255 | payload: { markdown: e.target.value }, 256 | }) 257 | } 258 | } 259 | 260 | const onChangeTitle = e => { 261 | if (dispatch) { 262 | dispatch({ type: 'write/setTitle', payload: { title: e.target.value } }) 263 | } 264 | } 265 | 266 | const saveDraft = () => { 267 | if (dispatch) { 268 | if (key !== 'new' && /^\d+$/.test(key)) { 269 | dispatch({ 270 | type: 'write/updateDraft', 271 | payload: { markdown, title, id: key }, 272 | }) 273 | } else { 274 | dispatch({ 275 | type: 'write/saveDraft', 276 | payload: { markdown, title }, 277 | }) 278 | } 279 | } 280 | } 281 | 282 | const showDrawer = () => { 283 | if (dispatch) { 284 | dispatch({ type: 'write/drafts' }) 285 | } 286 | setVisible(true) 287 | } 288 | 289 | const onClose = () => { 290 | setVisible(false) 291 | } 292 | 293 | const showImageModal = () => { 294 | setImageModalVisible(true) 295 | } 296 | 297 | const closeImageModal = () => { 298 | setInsertImages([]) 299 | setImageModalVisible(false) 300 | } 301 | 302 | const writeNew = () => { 303 | dispatch({ type: 'write/setMarkdown', payload: { markdown: null } }) 304 | dispatch({ type: 'write/setTitle', payload: { title: null } }) 305 | history.push('/write/draft/new') 306 | } 307 | 308 | const checkTagHandle = tag => { 309 | if (dispatch) { 310 | dispatch({ 311 | type: 'write/setSelecteTag', 312 | payload: { selectedTag: tag.id }, 313 | }) 314 | } 315 | } 316 | 317 | const checkCategorysHandle = category => { 318 | if (dispatch) { 319 | dispatch({ 320 | type: 'write/setSelecteCategory', 321 | payload: { selectedCategory: category.id }, 322 | }) 323 | dispatch({ type: 'write/setTags', payload: { tags: category.tags } }) 324 | } 325 | } 326 | 327 | const onPublish = () => { 328 | if (dispatch) { 329 | dispatch({ 330 | type: 'write/publish', 331 | payload: { 332 | markdown, 333 | title, 334 | selectedTag, 335 | selectedCategory, 336 | coverImageUrl, 337 | html: ReactDOMServer.renderToString( 338 | 339 | 340 | , 341 | ), 342 | }, 343 | }) 344 | } 345 | } 346 | 347 | const insertImageValueChange = e => { 348 | setInsertImageValue(e.target.value) 349 | } 350 | 351 | const insertImageOk = () => { 352 | let images = [...insertImages] 353 | if (insertImageValue) { 354 | images = [...images, insertImageValue] 355 | } 356 | 357 | if (images.length > 0) { 358 | const str = images.map(image => `![](${image})`).join('\n') 359 | setMarkdown(textAreaRef.current.resizableTextArea.textArea, str) 360 | } 361 | setImageModalVisible(false) 362 | } 363 | 364 | const returnImage = imageUrl => { 365 | setInsertImages([...insertImages, imageUrl]) 366 | } 367 | 368 | const returnCoverImageUrl = imageUrl => { 369 | setCoverImageUrl(imageUrl) 370 | } 371 | 372 | const writeMenu = ( 373 | 374 | 375 | 新文章 376 | 377 | 378 | 草稿箱 379 | 380 | 381 | 382 | 回到首页 383 | 384 | 385 | ) 386 | 387 | const setMarkdown = (el, data, start, num) => { 388 | if (dispatch) { 389 | const { selectionStart, selectionEnd } = el 390 | dispatch({ 391 | type: 'write/setMarkdown', 392 | payload: { 393 | markdown: [ 394 | markdown.substring(0, selectionStart), 395 | data, 396 | markdown.substring(selectionEnd), 397 | ].join(''), 398 | }, 399 | }) 400 | el.focus() 401 | el.setSelectionRange(selectionStart + start, selectionStart + start + num) 402 | } 403 | } 404 | 405 | const addBold = el => { 406 | setMarkdown(el, '**加粗**', 2, 2) 407 | } 408 | const addItalic = el => { 409 | setMarkdown(el, '*斜体*', 1, 2) 410 | } 411 | const addImage = el => { 412 | setMarkdown(el, '![描述](链接)', 6, 2) 413 | } 414 | const addLink = el => { 415 | setMarkdown(el, '[描述](链接)', 5, 2) 416 | } 417 | const addCode = el => { 418 | setMarkdown(el, '\n```\n```', 4, 0) 419 | } 420 | const addLineCode = el => { 421 | setMarkdown(el, '``', 1, 0) 422 | } 423 | const addQuote = el => { 424 | setMarkdown(el, '\n> 引用', 3, 2) 425 | } 426 | const addTable = el => { 427 | setMarkdown( 428 | el, 429 | '\n\n| Col1 | Col2 | Col3 |\n| :----: | :----: | :----: |\n| field1 | field2 | field3 |\n', 430 | 4, 431 | 4, 432 | ) 433 | } 434 | const addHeading = el => { 435 | let title = '## 标题' 436 | let start = 3 437 | if (markdown) { 438 | title = '\n## 标题' 439 | start = 4 440 | } 441 | setMarkdown(el, title, start, 2) 442 | } 443 | 444 | const onKeyEvent = (key, e) => { 445 | e.preventDefault() 446 | switch (key) { 447 | case 'ctrl+b': 448 | addBold(e.target) 449 | break 450 | case 'ctrl+h': 451 | addHeading(e.target) 452 | break 453 | case 'ctrl+l': 454 | addLink(e.target) 455 | break 456 | case 'ctrl+alt+t': 457 | addTable(e.target) 458 | break 459 | case 'ctrl+i': 460 | addImage(e.target) 461 | break 462 | case 'ctrl+q': 463 | addQuote(e.target) 464 | break 465 | case 'ctrl+alt+i': 466 | addItalic(e.target) 467 | break 468 | case 'ctrl+alt+c': 469 | addCode(e.target) 470 | break 471 | case 'ctrl+alt+k': 472 | addLineCode(e.target) 473 | break 474 | default: 475 | break 476 | } 477 | } 478 | 479 | return ( 480 | <> 481 | 482 | 483 |
484 | 492 |
493 | 494 | 495 | 快捷键} 498 | overlayStyle={{ width: 350 }} 499 | content={} 500 | > 501 | 502 | 503 | 发布文章} 506 | content={ 507 | 517 | } 518 | overlayStyle={{ width: 300 }} 519 | trigger="click" 520 | > 521 | 525 | 526 | 534 | 535 | e.preventDefault()}> 536 | 537 | 538 | 539 | {visible && ( 540 | 541 | ( 545 | 546 | { 550 | history.push(`/write/draft/${item.id}`) 551 | onClose() 552 | }} 553 | > 554 | {item.title} 555 | {item.is_publish ? ( 556 | 557 | 已发表 558 | 559 | ) : null} 560 | 561 | } 562 | description={`${moment(item.updatedAt).format( 563 | 'YYYY[年]MM[月]DD[日] HH:mm', 564 | )}`} 565 | /> 566 | 567 | )} 568 | /> 569 | 570 | )} 571 | 572 | 573 | 574 | 575 |
576 | 579 |
580 |
590 | 604 |