├── .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 | 
3 | 
4 | 
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 | 
19 |
20 | 写文章
21 | 
22 |
23 | 管理页
24 | 
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 |
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 |
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 | } onClick={showModal}>
96 | 添加分类
97 |
98 |
99 |
100 | uuidv4()}
104 | loading={loading}
105 | pagination={false}
106 | />
107 |
108 |
119 |
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 | } onClick={showModal}>
99 | 添加标签
100 |
101 |
102 |
103 | uuidv4()}
107 | loading={loading}
108 | pagination={false}
109 | />
110 |
111 |
119 |
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 |
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 | {/*
*/}
89 |
105 |
106 |
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 |
238 |
239 | 导航栏
240 | >
241 | }
242 | placement="left"
243 | closable
244 | onClose={onClose}
245 | visible={visible}
246 | bodyStyle={{ padding: 0 }}
247 | >
248 |
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 |
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 |
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 |
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 |
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 |
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 | {/*
*/}
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 |
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 |
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 |
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 => ``).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 |
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 |
580 |
590 |
604 |
625 |
626 |
627 |
628 |
629 |
630 |
631 |
632 |
633 |
634 |
635 |
636 |
637 |
638 |
646 | >
647 | )
648 | }
649 |
650 | export default connect(
651 | ({
652 | write: {
653 | title,
654 | markdown,
655 | drafts,
656 | categories,
657 | tags,
658 | selectedCategory,
659 | selectedTag,
660 | },
661 | user: { account },
662 | loading,
663 | }) => ({
664 | categories,
665 | tags,
666 | title,
667 | markdown,
668 | drafts,
669 | selectedCategory,
670 | selectedTag,
671 | account,
672 | loading: loading.effects['write/updateDraft'],
673 | }),
674 | )(Write)
675 |
--------------------------------------------------------------------------------
/src/pages/Write/index.less:
--------------------------------------------------------------------------------
1 | .textareScroll {
2 | &::-webkit-scrollbar {
3 | width: 10px;
4 | height: 4px;
5 | display: none;
6 | }
7 | scrollbar-width: none;
8 | &::-webkit-scrollbar-thumb {
9 | border-radius: 4px;
10 | background: #cccccc;
11 | }
12 | &::-webkit-scrollbar-track {
13 | border-radius: 0;
14 | background: #ffffff;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/pages/Write/markdown-github.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: octicons-link;
3 | src: url(data:font/woff;charset=utf-8;base64,d09GRgABAAAAAAZwABAAAAAACFQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABEU0lHAAAGaAAAAAgAAAAIAAAAAUdTVUIAAAZcAAAACgAAAAoAAQAAT1MvMgAAAyQAAABJAAAAYFYEU3RjbWFwAAADcAAAAEUAAACAAJThvmN2dCAAAATkAAAABAAAAAQAAAAAZnBnbQAAA7gAAACyAAABCUM+8IhnYXNwAAAGTAAAABAAAAAQABoAI2dseWYAAAFsAAABPAAAAZwcEq9taGVhZAAAAsgAAAA0AAAANgh4a91oaGVhAAADCAAAABoAAAAkCA8DRGhtdHgAAAL8AAAADAAAAAwGAACfbG9jYQAAAsAAAAAIAAAACABiATBtYXhwAAACqAAAABgAAAAgAA8ASm5hbWUAAAToAAABQgAAAlXu73sOcG9zdAAABiwAAAAeAAAAME3QpOBwcmVwAAAEbAAAAHYAAAB/aFGpk3jaTY6xa8JAGMW/O62BDi0tJLYQincXEypYIiGJjSgHniQ6umTsUEyLm5BV6NDBP8Tpts6F0v+k/0an2i+itHDw3v2+9+DBKTzsJNnWJNTgHEy4BgG3EMI9DCEDOGEXzDADU5hBKMIgNPZqoD3SilVaXZCER3/I7AtxEJLtzzuZfI+VVkprxTlXShWKb3TBecG11rwoNlmmn1P2WYcJczl32etSpKnziC7lQyWe1smVPy/Lt7Kc+0vWY/gAgIIEqAN9we0pwKXreiMasxvabDQMM4riO+qxM2ogwDGOZTXxwxDiycQIcoYFBLj5K3EIaSctAq2kTYiw+ymhce7vwM9jSqO8JyVd5RH9gyTt2+J/yUmYlIR0s04n6+7Vm1ozezUeLEaUjhaDSuXHwVRgvLJn1tQ7xiuVv/ocTRF42mNgZGBgYGbwZOBiAAFGJBIMAAizAFoAAABiAGIAznjaY2BkYGAA4in8zwXi+W2+MjCzMIDApSwvXzC97Z4Ig8N/BxYGZgcgl52BCSQKAA3jCV8CAABfAAAAAAQAAEB42mNgZGBg4f3vACQZQABIMjKgAmYAKEgBXgAAeNpjYGY6wTiBgZWBg2kmUxoDA4MPhGZMYzBi1AHygVLYQUCaawqDA4PChxhmh/8ODDEsvAwHgMKMIDnGL0x7gJQCAwMAJd4MFwAAAHjaY2BgYGaA4DAGRgYQkAHyGMF8NgYrIM3JIAGVYYDT+AEjAwuDFpBmA9KMDEwMCh9i/v8H8sH0/4dQc1iAmAkALaUKLgAAAHjaTY9LDsIgEIbtgqHUPpDi3gPoBVyRTmTddOmqTXThEXqrob2gQ1FjwpDvfwCBdmdXC5AVKFu3e5MfNFJ29KTQT48Ob9/lqYwOGZxeUelN2U2R6+cArgtCJpauW7UQBqnFkUsjAY/kOU1cP+DAgvxwn1chZDwUbd6CFimGXwzwF6tPbFIcjEl+vvmM/byA48e6tWrKArm4ZJlCbdsrxksL1AwWn/yBSJKpYbq8AXaaTb8AAHja28jAwOC00ZrBeQNDQOWO//sdBBgYGRiYWYAEELEwMTE4uzo5Zzo5b2BxdnFOcALxNjA6b2ByTswC8jYwg0VlNuoCTWAMqNzMzsoK1rEhNqByEyerg5PMJlYuVueETKcd/89uBpnpvIEVomeHLoMsAAe1Id4AAAAAAAB42oWQT07CQBTGv0JBhagk7HQzKxca2sJCE1hDt4QF+9JOS0nbaaYDCQfwCJ7Au3AHj+LO13FMmm6cl7785vven0kBjHCBhfpYuNa5Ph1c0e2Xu3jEvWG7UdPDLZ4N92nOm+EBXuAbHmIMSRMs+4aUEd4Nd3CHD8NdvOLTsA2GL8M9PODbcL+hD7C1xoaHeLJSEao0FEW14ckxC+TU8TxvsY6X0eLPmRhry2WVioLpkrbp84LLQPGI7c6sOiUzpWIWS5GzlSgUzzLBSikOPFTOXqly7rqx0Z1Q5BAIoZBSFihQYQOOBEdkCOgXTOHA07HAGjGWiIjaPZNW13/+lm6S9FT7rLHFJ6fQbkATOG1j2OFMucKJJsxIVfQORl+9Jyda6Sl1dUYhSCm1dyClfoeDve4qMYdLEbfqHf3O/AdDumsjAAB42mNgYoAAZQYjBmyAGYQZmdhL8zLdDEydARfoAqIAAAABAAMABwAKABMAB///AA8AAQAAAAAAAAAAAAAAAAABAAAAAA==) format('woff');
4 | }
5 |
6 | .markdown-body {
7 | -ms-text-size-adjust: 100%;
8 | -webkit-text-size-adjust: 100%;
9 | line-height: 1.5;
10 | color: #24292e;
11 | font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
12 | font-size: 16px;
13 | line-height: 1.5;
14 | word-wrap: break-word;
15 | }
16 |
17 | .markdown-body .pl-c {
18 | color: #969896;
19 | }
20 |
21 | .markdown-body .pl-c1,
22 | .markdown-body .pl-s .pl-v {
23 | color: #0086b3;
24 | }
25 |
26 | .markdown-body .pl-e,
27 | .markdown-body .pl-en {
28 | color: #795da3;
29 | }
30 |
31 | .markdown-body .pl-smi,
32 | .markdown-body .pl-s .pl-s1 {
33 | color: #333;
34 | }
35 |
36 | .markdown-body .pl-ent {
37 | color: #63a35c;
38 | }
39 |
40 | .markdown-body .pl-k {
41 | color: #a71d5d;
42 | }
43 |
44 | .markdown-body .pl-s,
45 | .markdown-body .pl-pds,
46 | .markdown-body .pl-s .pl-pse .pl-s1,
47 | .markdown-body .pl-sr,
48 | .markdown-body .pl-sr .pl-cce,
49 | .markdown-body .pl-sr .pl-sre,
50 | .markdown-body .pl-sr .pl-sra {
51 | color: #183691;
52 | }
53 |
54 | .markdown-body .pl-v,
55 | .markdown-body .pl-smw {
56 | color: #ed6a43;
57 | }
58 |
59 | .markdown-body .pl-bu {
60 | color: #b52a1d;
61 | }
62 |
63 | .markdown-body .pl-ii {
64 | color: #f8f8f8;
65 | background-color: #b52a1d;
66 | }
67 |
68 | .markdown-body .pl-c2 {
69 | color: #f8f8f8;
70 | background-color: #b52a1d;
71 | }
72 |
73 | .markdown-body .pl-c2::before {
74 | content: "^M";
75 | }
76 |
77 | .markdown-body .pl-sr .pl-cce {
78 | font-weight: bold;
79 | color: #63a35c;
80 | }
81 |
82 | .markdown-body .pl-ml {
83 | color: #693a17;
84 | }
85 |
86 | .markdown-body .pl-mh,
87 | .markdown-body .pl-mh .pl-en,
88 | .markdown-body .pl-ms {
89 | font-weight: bold;
90 | color: #1d3e81;
91 | }
92 |
93 | .markdown-body .pl-mq {
94 | color: #008080;
95 | }
96 |
97 | .markdown-body .pl-mi {
98 | font-style: italic;
99 | color: #333;
100 | }
101 |
102 | .markdown-body .pl-mb {
103 | font-weight: bold;
104 | color: #333;
105 | }
106 |
107 | .markdown-body .pl-md {
108 | color: #bd2c00;
109 | background-color: #ffecec;
110 | }
111 |
112 | .markdown-body .pl-mi1 {
113 | color: #55a532;
114 | background-color: #eaffea;
115 | }
116 |
117 | .markdown-body .pl-mc {
118 | color: #ef9700;
119 | background-color: #ffe3b4;
120 | }
121 |
122 | .markdown-body .pl-mi2 {
123 | color: #d8d8d8;
124 | background-color: #808080;
125 | }
126 |
127 | .markdown-body .pl-mdr {
128 | font-weight: bold;
129 | color: #795da3;
130 | }
131 |
132 | .markdown-body .pl-mo {
133 | color: #1d3e81;
134 | }
135 |
136 | .markdown-body .pl-ba {
137 | color: #595e62;
138 | }
139 |
140 | .markdown-body .pl-sg {
141 | color: #c0c0c0;
142 | }
143 |
144 | .markdown-body .pl-corl {
145 | text-decoration: underline;
146 | color: #183691;
147 | }
148 |
149 | .markdown-body .octicon {
150 | display: inline-block;
151 | vertical-align: text-top;
152 | fill: currentColor;
153 | }
154 |
155 | .markdown-body a {
156 | background-color: transparent;
157 | -webkit-text-decoration-skip: objects;
158 | }
159 |
160 | .markdown-body a:active,
161 | .markdown-body a:hover {
162 | outline-width: 0;
163 | }
164 |
165 | .markdown-body strong {
166 | font-weight: inherit;
167 | }
168 |
169 | .markdown-body strong {
170 | font-weight: bolder;
171 | }
172 |
173 | .markdown-body h1 {
174 | font-size: 2em;
175 | margin: 0.67em 0;
176 | }
177 |
178 | .markdown-body img {
179 | border-style: none;
180 | }
181 |
182 | .markdown-body svg:not(:root) {
183 | overflow: hidden;
184 | }
185 |
186 | .markdown-body code,
187 | .markdown-body kbd,
188 | .markdown-body pre {
189 | font-family: monospace, monospace;
190 | font-size: 1em;
191 | }
192 | .markdown-body p > code:not([class]) {
193 | padding: 2px 4px;
194 | font-size: 90%;
195 | color: #c7254e;
196 | background-color: #f9f2f4;
197 | border-radius: 4px;
198 | }
199 | .markdown-body hr {
200 | box-sizing: content-box;
201 | height: 0;
202 | overflow: visible;
203 | }
204 |
205 | .markdown-body input {
206 | font: inherit;
207 | margin: 0;
208 | }
209 |
210 | .markdown-body input {
211 | overflow: visible;
212 | }
213 |
214 | .markdown-body [type="checkbox"] {
215 | box-sizing: border-box;
216 | padding: 0;
217 | }
218 |
219 | .markdown-body * {
220 | box-sizing: border-box;
221 | }
222 |
223 | .markdown-body input {
224 | font-family: inherit;
225 | font-size: inherit;
226 | line-height: inherit;
227 | }
228 |
229 | .markdown-body a {
230 | color: #0366d6;
231 | text-decoration: none;
232 | }
233 |
234 | .markdown-body a:hover {
235 | text-decoration: underline;
236 | }
237 |
238 | .markdown-body strong {
239 | font-weight: 600;
240 | }
241 |
242 | .markdown-body hr {
243 | height: 0;
244 | margin: 15px 0;
245 | overflow: hidden;
246 | background: transparent;
247 | border: 0;
248 | border-bottom: 1px solid #dfe2e5;
249 | }
250 |
251 | .markdown-body hr::before {
252 | display: table;
253 | content: "";
254 | }
255 |
256 | .markdown-body hr::after {
257 | display: table;
258 | clear: both;
259 | content: "";
260 | }
261 |
262 | .markdown-body table {
263 | border-spacing: 0;
264 | border-collapse: collapse;
265 | }
266 |
267 | .markdown-body td,
268 | .markdown-body th {
269 | padding: 0;
270 | }
271 |
272 | .markdown-body h1,
273 | .markdown-body h2,
274 | .markdown-body h3,
275 | .markdown-body h4,
276 | .markdown-body h5,
277 | .markdown-body h6 {
278 | margin-top: 0;
279 | margin-bottom: 0;
280 | }
281 |
282 | .markdown-body h1 {
283 | font-size: 32px;
284 | font-weight: 600;
285 | }
286 |
287 | .markdown-body h2 {
288 | font-size: 24px;
289 | font-weight: 600;
290 | }
291 |
292 | .markdown-body h3 {
293 | font-size: 20px;
294 | font-weight: 600;
295 | }
296 |
297 | .markdown-body h4 {
298 | font-size: 16px;
299 | font-weight: 600;
300 | }
301 |
302 | .markdown-body h5 {
303 | font-size: 14px;
304 | font-weight: 600;
305 | }
306 |
307 | .markdown-body h6 {
308 | font-size: 12px;
309 | font-weight: 600;
310 | }
311 |
312 | .markdown-body p {
313 | margin-top: 0;
314 | margin-bottom: 10px;
315 | }
316 |
317 | .markdown-body blockquote {
318 | margin: 0;
319 | }
320 |
321 | .markdown-body ul,
322 | .markdown-body ol {
323 | padding-left: 0;
324 | margin-top: 0;
325 | margin-bottom: 0;
326 | }
327 |
328 | .markdown-body ol ol,
329 | .markdown-body ul ol {
330 | list-style-type: lower-roman;
331 | }
332 |
333 | .markdown-body ul ul ol,
334 | .markdown-body ul ol ol,
335 | .markdown-body ol ul ol,
336 | .markdown-body ol ol ol {
337 | list-style-type: lower-alpha;
338 | }
339 |
340 | .markdown-body dd {
341 | margin-left: 0;
342 | }
343 |
344 | .markdown-body code {
345 | font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
346 | font-size: 12px;
347 | }
348 |
349 | .markdown-body pre {
350 | margin-top: 0;
351 | margin-bottom: 0;
352 | font: 12px "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
353 | }
354 |
355 | .markdown-body .octicon {
356 | vertical-align: text-bottom;
357 | }
358 |
359 | .markdown-body .pl-0 {
360 | padding-left: 0 !important;
361 | }
362 |
363 | .markdown-body .pl-1 {
364 | padding-left: 4px !important;
365 | }
366 |
367 | .markdown-body .pl-2 {
368 | padding-left: 8px !important;
369 | }
370 |
371 | .markdown-body .pl-3 {
372 | padding-left: 16px !important;
373 | }
374 |
375 | .markdown-body .pl-4 {
376 | padding-left: 24px !important;
377 | }
378 |
379 | .markdown-body .pl-5 {
380 | padding-left: 32px !important;
381 | }
382 |
383 | .markdown-body .pl-6 {
384 | padding-left: 40px !important;
385 | }
386 |
387 | .markdown-body::before {
388 | display: table;
389 | content: "";
390 | }
391 |
392 | .markdown-body::after {
393 | display: table;
394 | clear: both;
395 | content: "";
396 | }
397 |
398 | .markdown-body>*:first-child {
399 | margin-top: 0 !important;
400 | }
401 |
402 | .markdown-body>*:last-child {
403 | margin-bottom: 0 !important;
404 | }
405 |
406 | .markdown-body a:not([href]) {
407 | color: inherit;
408 | text-decoration: none;
409 | }
410 |
411 | .markdown-body .anchor {
412 | float: left;
413 | padding-right: 4px;
414 | margin-left: -20px;
415 | line-height: 1;
416 | }
417 |
418 | .markdown-body .anchor:focus {
419 | outline: none;
420 | }
421 |
422 | .markdown-body p,
423 | .markdown-body blockquote,
424 | .markdown-body ul,
425 | .markdown-body ol,
426 | .markdown-body dl,
427 | .markdown-body table,
428 | .markdown-body pre {
429 | margin-top: 0;
430 | margin-bottom: 16px;
431 | }
432 |
433 | .markdown-body hr {
434 | height: 0.25em;
435 | padding: 0;
436 | margin: 24px 0;
437 | background-color: #e1e4e8;
438 | border: 0;
439 | }
440 |
441 | .markdown-body blockquote {
442 | padding: 0 1em;
443 | color: #6a737d;
444 | border-left: 0.25em solid #4680be;
445 | }
446 |
447 | .markdown-body blockquote>:first-child {
448 | margin-top: 0;
449 | }
450 |
451 | .markdown-body blockquote>:last-child {
452 | margin-bottom: 0;
453 | }
454 |
455 | .markdown-body kbd {
456 | display: inline-block;
457 | padding: 3px 5px;
458 | font-size: 11px;
459 | line-height: 10px;
460 | color: #444d56;
461 | vertical-align: middle;
462 | background-color: #fafbfc;
463 | border: solid 1px #c6cbd1;
464 | border-bottom-color: #959da5;
465 | border-radius: 3px;
466 | box-shadow: inset 0 -1px 0 #959da5;
467 | }
468 |
469 | .markdown-body h1,
470 | .markdown-body h2,
471 | .markdown-body h3,
472 | .markdown-body h4,
473 | .markdown-body h5,
474 | .markdown-body h6 {
475 | margin-top: 24px;
476 | margin-bottom: 16px;
477 | font-weight: 600;
478 | line-height: 1.25;
479 | }
480 |
481 | .markdown-body h1 .octicon-link,
482 | .markdown-body h2 .octicon-link,
483 | .markdown-body h3 .octicon-link,
484 | .markdown-body h4 .octicon-link,
485 | .markdown-body h5 .octicon-link,
486 | .markdown-body h6 .octicon-link {
487 | color: #1b1f23;
488 | vertical-align: middle;
489 | visibility: hidden;
490 | }
491 |
492 | .markdown-body h1:hover .anchor,
493 | .markdown-body h2:hover .anchor,
494 | .markdown-body h3:hover .anchor,
495 | .markdown-body h4:hover .anchor,
496 | .markdown-body h5:hover .anchor,
497 | .markdown-body h6:hover .anchor {
498 | text-decoration: none;
499 | }
500 |
501 | .markdown-body h1:hover .anchor .octicon-link,
502 | .markdown-body h2:hover .anchor .octicon-link,
503 | .markdown-body h3:hover .anchor .octicon-link,
504 | .markdown-body h4:hover .anchor .octicon-link,
505 | .markdown-body h5:hover .anchor .octicon-link,
506 | .markdown-body h6:hover .anchor .octicon-link {
507 | visibility: visible;
508 | }
509 |
510 | .markdown-body h1 {
511 | padding-bottom: 0.3em;
512 | font-size: 2em;
513 | border-bottom: 1px solid #eaecef;
514 | }
515 |
516 | .markdown-body h2 {
517 | padding-bottom: 0.3em;
518 | font-size: 1.5em;
519 | border-bottom: 1px solid #eaecef;
520 | }
521 |
522 | .markdown-body h3 {
523 | font-size: 1.25em;
524 | }
525 |
526 | .markdown-body h4 {
527 | font-size: 1em;
528 | }
529 |
530 | .markdown-body h5 {
531 | font-size: 0.875em;
532 | }
533 |
534 | .markdown-body h6 {
535 | font-size: 0.85em;
536 | color: #6a737d;
537 | }
538 |
539 | .markdown-body ul,
540 | .markdown-body ol {
541 | padding-left: 2em;
542 | }
543 |
544 | .markdown-body ul ul,
545 | .markdown-body ul ol,
546 | .markdown-body ol ol,
547 | .markdown-body ol ul {
548 | margin-top: 0;
549 | margin-bottom: 0;
550 | }
551 |
552 | .markdown-body li>p {
553 | margin-top: 16px;
554 | }
555 |
556 | .markdown-body li+li {
557 | margin-top: 0.25em;
558 | }
559 |
560 | .markdown-body dl {
561 | padding: 0;
562 | }
563 |
564 | .markdown-body dl dt {
565 | padding: 0;
566 | margin-top: 16px;
567 | font-size: 1em;
568 | font-style: italic;
569 | font-weight: 600;
570 | }
571 |
572 | .markdown-body dl dd {
573 | padding: 0 16px;
574 | margin-bottom: 16px;
575 | }
576 |
577 | .markdown-body table {
578 | display: block;
579 | width: 100%;
580 | overflow: auto;
581 | }
582 |
583 | .markdown-body table th {
584 | font-weight: 600;
585 | }
586 |
587 | .markdown-body table th,
588 | .markdown-body table td {
589 | padding: 6px 13px;
590 | border: 1px solid #dfe2e5;
591 | }
592 |
593 | .markdown-body table tr {
594 | background-color: #fff;
595 | border-top: 1px solid #c6cbd1;
596 | }
597 |
598 | .markdown-body table tr:nth-child(2n) {
599 | background-color: #f6f8fa;
600 | }
601 |
602 | .markdown-body img {
603 | max-width: 100%;
604 | box-sizing: content-box;
605 | background-color: #fff;
606 | }
607 |
608 | .markdown-body code {
609 | padding: 0;
610 | padding-top: 0.2em;
611 | padding-bottom: 0.2em;
612 | margin: 0;
613 | font-size: 85%;
614 | background-color: rgba(27,31,35,0.05);
615 | border-radius: 3px;
616 | }
617 |
618 | .markdown-body code::before,
619 | .markdown-body code::after {
620 | letter-spacing: -0.2em;
621 | content: "\00a0";
622 | }
623 |
624 | .markdown-body pre {
625 | word-wrap: normal;
626 | }
627 |
628 | .markdown-body pre>code {
629 | padding: 0;
630 | margin: 0;
631 | font-size: 100%;
632 | word-break: normal;
633 | white-space: pre;
634 | background: transparent;
635 | border: 0;
636 | }
637 |
638 | .markdown-body .highlight {
639 | margin-bottom: 16px;
640 | }
641 |
642 | .markdown-body .highlight pre {
643 | margin-bottom: 0;
644 | word-break: normal;
645 | }
646 |
647 | .markdown-body .highlight pre,
648 | .markdown-body pre {
649 | padding: 16px;
650 | overflow: auto;
651 | font-size: 85%;
652 | line-height: 1.45;
653 | background-color: #f6f8fa;
654 | border-radius: 3px;
655 | }
656 |
657 | .markdown-body pre code {
658 | display: inline;
659 | max-width: auto;
660 | padding: 0;
661 | margin: 0;
662 | overflow: visible;
663 | line-height: inherit;
664 | word-wrap: normal;
665 | background-color: transparent;
666 | border: 0;
667 | }
668 |
669 | .markdown-body pre code::before,
670 | .markdown-body pre code::after {
671 | content: normal;
672 | }
673 |
674 | .markdown-body .full-commit .btn-outline:not(:disabled):hover {
675 | color: #005cc5;
676 | border-color: #005cc5;
677 | }
678 |
679 | .markdown-body kbd {
680 | display: inline-block;
681 | padding: 3px 5px;
682 | font: 11px "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
683 | line-height: 10px;
684 | color: #444d56;
685 | vertical-align: middle;
686 | background-color: #fcfcfc;
687 | border: solid 1px #c6cbd1;
688 | border-bottom-color: #959da5;
689 | border-radius: 3px;
690 | box-shadow: inset 0 -1px 0 #959da5;
691 | }
692 |
693 | .markdown-body :checked+.radio-label {
694 | position: relative;
695 | z-index: 1;
696 | border-color: #0366d6;
697 | }
698 |
699 | .markdown-body .task-list-item {
700 | list-style-type: none;
701 | }
702 |
703 | .markdown-body .task-list-item+.task-list-item {
704 | margin-top: 3px;
705 | }
706 |
707 | .markdown-body .task-list-item input {
708 | margin: 0 0.2em 0.25em -1.6em;
709 | vertical-align: middle;
710 | }
711 |
712 | .markdown-body hr {
713 | border-bottom-color: #eee;
714 | }
715 |
--------------------------------------------------------------------------------
/src/pages/Write/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/WriteCourse/index.jsx:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: 柒叶
3 | * @Date: 2020-05-21 09:51:22
4 | * @Last Modified by: 柒叶
5 | * @Last Modified time: 2020-05-21 13:05:03
6 | */
7 | import React, { useState, useEffect } from 'react'
8 | import {
9 | Row,
10 | Col,
11 | Drawer,
12 | Layout,
13 | Tree,
14 | Space,
15 | Button,
16 | Divider,
17 | Input,
18 | } from 'antd'
19 | import { MenuOutlined } from '@ant-design/icons'
20 | import styles from './index.less'
21 | const { Header, Footer, Sider, Content } = Layout
22 | const { TreeNode } = Tree
23 | const Course = props => {
24 | return (
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | 预览
34 | 保存
35 |
36 |
37 |
38 |
39 |
48 |
49 |
50 |
51 |
52 | //
53 | //
60 | // }
63 | // defaultExpandedKeys={['0-0-0']}
64 | // // onSelect={this.onSelect}
65 | // className={`${styles.menuTree} ft-13`}
66 | // >
67 | //
68 | //
69 | //
70 | //
71 | //
72 | //
73 | //
74 | //
75 | //
76 | //
77 | //
78 | //
79 | //
80 | //
81 | //
82 | // Content
83 | //
84 | //
85 | )
86 | }
87 |
88 | export default Course
89 |
--------------------------------------------------------------------------------
/src/pages/WriteCourse/index.less:
--------------------------------------------------------------------------------
1 | .courseEditor {
2 | width: 100%;
3 | height: 40px;
4 | .courseWrapper {
5 | width: 700px;
6 | margin: 0 auto;
7 | .courseHeaderContainer {
8 | display: flex;
9 | justify-content: space-between;
10 |
11 | height: inherit;
12 |
13 | line-height: 40px;
14 | // background-color: #cccccc;
15 | padding: 0 10px;
16 | }
17 | }
18 | }
19 |
20 | .courseSider {
21 | height: calc(100vh);
22 | background-color: #eeeeee;
23 | // padding-left: 20px;
24 | .menuTree {
25 | background-color: #eeeeee;
26 | margin-top: 20px;
27 | margin-left: 20px;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/services/admin.js:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: 柒叶
3 | * @Date: 2020-04-29 18:04:12
4 | * @Last Modified by: 柒叶
5 | * @Last Modified time: 2020-05-02 19:25:53
6 | */
7 | import { stringify } from 'qs'
8 | import request from '@/utils/request'
9 |
10 | // 获取评论
11 |
12 | export async function getComments() {
13 | return request('/api/admin/comments')
14 | }
15 |
16 | // 获取分类列表
17 | export async function getCategories() {
18 | return request('/api/admin/categories')
19 | }
20 |
21 | // 删除分类
22 | export async function deleteCategory(data) {
23 | return request('/api/admin/delete/category', {
24 | method: 'POST',
25 | data,
26 | })
27 | }
28 |
29 | // 添加分类
30 | export async function createCategory(data) {
31 | return request('/api/admin/create/category', {
32 | method: 'POST',
33 | data,
34 | })
35 | }
36 |
37 | // 获取标签列表
38 | export async function getTags() {
39 | return request('/api/admin/tags')
40 | }
41 |
42 | // 删除标签
43 | export async function deleteTag(data) {
44 | return request('/api/admin/delete/tag', {
45 | method: 'POST',
46 | data,
47 | })
48 | }
49 |
50 | // 添加标签
51 | export async function createTag(data) {
52 | return request('/api/admin/create/tag', {
53 | method: 'POST',
54 | data,
55 | })
56 | }
57 |
58 | // 删除文章
59 | export async function deleteArticle(data) {
60 | return request('/api/admin/delete/article', {
61 | method: 'POST',
62 | data,
63 | })
64 | }
65 |
66 | // 删除评论
67 | export async function deleteComment(data) {
68 | return request('/api/admin/delete/comment', {
69 | method: 'POST',
70 | data,
71 | })
72 | }
73 |
74 | // 获取文章列表
75 | export async function getArticles(params) {
76 | return request(`/api/admin/articles?${stringify(params)}`)
77 | }
78 |
--------------------------------------------------------------------------------
/src/services/article.js:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: 柒叶
3 | * @Date: 2020-04-07 12:58:34
4 | * @Last Modified by: 柒叶
5 | * @Last Modified time: 2020-05-11 20:49:39
6 | */
7 |
8 | import { stringify } from 'qs'
9 | import request from '@/utils/request'
10 |
11 | // 获取分类
12 |
13 | export async function getCategories() {
14 | return request('/api/categories')
15 | }
16 |
17 | // 获取文章列表
18 | export async function getArticles(params) {
19 | return request(`/api/articles?${stringify(params)}`)
20 | }
21 |
22 | // 获取热门文章列表
23 | export async function getHotArticles() {
24 | return request('/api/hot')
25 | }
26 |
27 | // 获取文章详情
28 | export async function getArticleDetail(params) {
29 | return request(`/api/detail?${stringify(params)}`)
30 | }
31 |
32 | // 获取用户评论
33 | export async function getComments(params) {
34 | return request(`/api/comments?${stringify(params)}`)
35 | }
36 |
37 | // 未登录添加评论
38 | export async function createNoLoginComment(data) {
39 | return request('/api/toursit/comment', {
40 | method: 'POST',
41 | data,
42 | })
43 | }
44 |
45 | // 添加评论
46 | export async function createComment(data) {
47 | return request('/api/create/comment', { method: 'POST', data })
48 | }
49 |
50 | // 获取tags
51 | export async function getTags() {
52 | return request('/api/tags')
53 | }
54 |
55 | // 文章点赞
56 | export async function updateFavorite(data) {
57 | return request('/api/update/favorite', { method: 'POST', data })
58 | }
59 |
60 | // 是否已点赞
61 | export async function getIsFavorite(params) {
62 | return request(`/api/isFavorite?${stringify(params)}`)
63 | }
64 |
--------------------------------------------------------------------------------
/src/services/user.js:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: 柒叶
3 | * @Date: 2020-05-06 09:22:30
4 | * @Last Modified by: 柒叶
5 | * @Last Modified time: 2020-05-09 12:37:54
6 | */
7 | import { stringify } from 'qs'
8 | import request from '@/utils/request'
9 |
10 | // 注册
11 |
12 | export async function registerAccount(data) {
13 | return request('/api/register', {
14 | method: 'POST',
15 | data,
16 | })
17 | }
18 |
19 | // 登录
20 | export async function loginAccount(data) {
21 | return request('/api/login', {
22 | method: 'POST',
23 | data,
24 | })
25 | }
26 |
27 | // 得到用户信息
28 | export async function getAccount() {
29 | return request('/api/account')
30 | }
31 |
32 | // 退出登录
33 | export async function logoutAccount() {
34 | return request('/api/logout', { method: 'POST' })
35 | }
36 |
37 | // 修改用户信息
38 | export async function modifyAccount(data) {
39 | return request('/api/update/account', { method: 'POST', data })
40 | }
41 |
--------------------------------------------------------------------------------
/src/services/write.js:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: 柒叶
3 | * @Date: 2020-04-07 12:58:34
4 | * @Last Modified by: 柒叶
5 | * @Last Modified time: 2020-05-21 07:25:11
6 | */
7 |
8 | import { stringify } from 'qs'
9 | import request from '@/utils/request'
10 |
11 | // 保存draft
12 | export async function createDraft(data) {
13 | return request('/api/create/draft', {
14 | method: 'POST',
15 | data,
16 | })
17 | }
18 |
19 | // 获取 draft
20 | export async function getDraft(params) {
21 | return request(`/api/draft?${stringify(params)}`)
22 | }
23 |
24 | // 获取drafts
25 | export async function getDrafts() {
26 | return request('/api/drafts')
27 | }
28 |
29 | // 更新draft
30 | export async function updateDraft(data) {
31 | return request('/api/update/draft', {
32 | method: 'POST',
33 | data,
34 | })
35 | }
36 |
37 | // 删除draft
38 | export async function deleteDraft(data) {
39 | return request('/api/delete/draft', {
40 | method: 'POST',
41 | data,
42 | })
43 | }
44 |
45 | // 获到标签和分类
46 | export async function getCategories() {
47 | return request('/api/categories')
48 | }
49 |
50 | // 发布文章
51 | export async function createPublish(data) {
52 | return request('/api/create/publish', {
53 | method: 'POST',
54 | data,
55 | })
56 | }
57 |
--------------------------------------------------------------------------------
/src/utils/request.js:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: 柒叶
3 | * @Date: 2020-04-07 12:53:08
4 | * @Last Modified by: 柒叶
5 | * @Last Modified time: 2020-04-07 12:53:50
6 | */
7 |
8 | import { extend } from 'umi-request';
9 |
10 | const request = extend({
11 | // errorHandler, // 默认错误处理
12 | credentials: 'include', // 默认请求是否带上cookie
13 | });
14 |
15 | export default request;
16 |
--------------------------------------------------------------------------------
/src/utils/storage.js:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: 柒叶
3 | * @Date: 2020-05-08 13:06:19
4 | * @Last Modified by: 柒叶
5 | * @Last Modified time: 2020-05-08 13:09:52
6 | */
7 |
8 | import CryptoJS from 'crypto-js'
9 | import { ENCRYPT_KEY } from '../config/secret'
10 |
11 | const encrypt = dataToStorage => {
12 | return CryptoJS.AES.encrypt(JSON.stringify(dataToStorage), ENCRYPT_KEY)
13 | }
14 |
15 | const decrypt = dataFromStorage => {
16 | const bytes = CryptoJS.AES.decrypt(dataFromStorage, ENCRYPT_KEY)
17 | const decryptedData = JSON.parse(bytes.toString(CryptoJS.enc.Utf8))
18 | return decryptedData
19 | }
20 |
21 | const storageHelper = {
22 | get: key => {
23 | try {
24 | const formatted = decrypt(localStorage.getItem(key))
25 | return formatted
26 | } catch (e) {
27 | return undefined
28 | }
29 | },
30 | set: (key, value) => {
31 | localStorage.setItem(key, encrypt(value))
32 | },
33 | clear: key => {
34 | localStorage.removeItem(key)
35 | },
36 | }
37 |
38 | export default storageHelper
39 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "esnext",
4 | "module": "esnext",
5 | "moduleResolution": "node",
6 | "importHelpers": true,
7 | "jsx": "react",
8 | "esModuleInterop": true,
9 | "sourceMap": true,
10 | "baseUrl": "./",
11 | "strict": true,
12 | "paths": {
13 | "@/*": ["src/*"],
14 | "@@/*": ["src/.umi/*"]
15 | },
16 | "allowSyntheticDefaultImports": true
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/typings.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.css';
2 | declare module '*.less';
3 | declare module "*.png";
4 |
--------------------------------------------------------------------------------