0%

1, create a new component

1
2
mkdir src/components/publish-manage
touch src/components/publish-manage/NewsPublish.js

copy RightList.js to NewsPublish.js, modify content as below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import { Button, Table } from 'antd';

import React from 'react'

export default function NewsPublish(props) {
const columns = [
{
title: '新闻标题',
dataIndex: 'title',
render: (title, item) => {
return <a href={`#/news-manage/preview/${item.id}`}>{title}</a>
}
},
{
title: '作者',
dataIndex: 'author',
},
{
title: '新闻分类',
dataIndex: 'category',
render: category => {
return <div>{category.title}</div>
}
},
{
title: '操作',
render: item => {
return <div>
<Button>Button</Button>
</div>
}
},
];

return (
<div>
<Table dataSource={props.dataSource} columns={columns} pagination={{ pageSize: 5 }} rowKey={item => item.id} />;
</div>
)
}

so the Unpublish.js looks like

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import axios from 'axios';
import React, { useEffect, useState } from 'react'
import NewsPublish from '../../../components/publish-manage/NewsPublish';

export default function Unpublished() {
const [dataSource, setDataSource] = useState([])
const { username } = JSON.parse(localStorage.getItem('token'));
useEffect(() => {
axios.get(`/api/news?author=${username}&publishState=1&_expand=category`).then(res => {
console.log(res.data);
})
}, [username]);

return (
<div>
<NewsPublish dataSource={dataSource}></NewsPublish>
</div>
)
}

2. Define a custom hook

Because Published.js and Sunset.js are similar to Unpublish.js, only the publishState is difference. So we can define a custom hook.

1
touch src/components/publish-manage/usePublish.js

content is cut from Unpublish.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69


import { useEffect, useState } from 'react'
import axios from 'axios';
import { notification } from 'antd';

export default function usePublish(type) {
const [dataSource, setDataSource] = useState([])
const { username } = JSON.parse(localStorage.getItem('token'));

useEffect(() => {
axios.get(`/api/news?author=${username}&publishState=${type}&_expand=category`).then(res => {
// console.log(res.data);
setDataSource(res.data);
})
}, [username, type]);

const handlePublish = id => {
// console.log(id);
setDataSource(dataSource.filter(item => item.id !== id));

axios.patch(`/api/news/${id}`, {
publishStats: 2,
publishTime: Date.now(),
}).then(res => {
notification.info({
message: `Notification`,
description:
`Please go to published box for reviewing your news`,
placement: 'bottomRight',
});
})
}

const handleSunset = id => {
setDataSource(dataSource.filter(item => item.id !== id));
axios.patch(`/api/news/${id}`, {
publishStats: 3,
}).then(res => {
notification.info({
message: `Notification`,
description:
`Please go to Sunset box for reviewing your news`,
placement: 'bottomRight',
});
})
}

const handleDelete = id => {
setDataSource(dataSource.filter(item => item.id !== id));

axios.delete(`/api/news/${id}`).then(res => {
notification.info({
message: `Notification`,
description:
`Your news was deleted`,
placement: 'bottomRight',
});
})
}

return {
dataSource,
handlePublish,
handleSunset,
handleDelete,
}
}

So the Unpublished.js change to

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { Button } from 'antd';
import React from 'react';
import NewsPublish from '../../../components/publish-manage/NewsPublish';
import usePublish from '../../../components/publish-manage/usePublish';

export default function Unpublished() {
// 1 unpublished
// 2 published
// 3 sunset
const { dataSource, handlePublish } = usePublish(1);
return (
<div>
<NewsPublish dataSource={dataSource} button={id => <Button type='primary' onClick={() => handlePublish(id)}>发布</Button>}></NewsPublish>
</div>
)
}

and the NewsPublish.js change to

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import { Button, Table } from 'antd';

import React from 'react'

export default function NewsPublish(props) {
const columns = [
{
title: '新闻标题',
dataIndex: 'title',
render: (title, item) => {
return <a href={`#/news-manage/preview/${item.id}`}>{title}</a>
}
},
{
title: '作者',
dataIndex: 'author',
},
{
title: '新闻分类',
dataIndex: 'category',
render: category => {
return <div>{category.title}</div>
}
},
{
title: '操作',
render: item => {
return <div>
{props.button(item.id)}
</div>
}
},
];

return (
<div>
<Table dataSource={props.dataSource} columns={columns} pagination={{ pageSize: 5 }} rowKey={item => item.id} />;
</div>
)
}

according the type, easy to peform Published.js and Sunset.js

Custom hook is important!

The audit list should be showing date the user ownself, audiState should not equals to 0 and publishState should less than 1.

1. Get back data in AuditList.js

copy from RightList.js and mofify as below

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import { Button, Table, Tag } from 'antd';

import axios from 'axios';
import React, { useEffect, useState } from 'react'

export default function AuditList() {
const { username } = JSON.parse(localStorage.getItem('token'));

const [dataSource, setDataSource] = useState([]);

const columns = [
{
title: '新闻标题',
dataIndex: 'title',
render: (title, item) => {
return <a href={`#/news-manage/preview/${item.id}`}>{title}</a>
}
},
{
title: '新闻分类',
dataIndex: 'category',
render: category => {
return category.title
}
},
{
title: '作者',
dataIndex: 'author',
},
{
title: '审核状态',
dataIndex: 'auditState',
render: auditState => {
const colorList = ['', 'orange', 'green', 'red'];
const auditList = ['未审核', '审核中', '已通过', '未通过'];
return <Tag color={colorList[auditState]}>{auditList[auditState]}</Tag>
}
},
{
title: '操作',
render: item => {
return <div>
{
item.auditState === 1 && <Button danger>撤销</Button>
}
{
item.auditState === 2 && <Button>发布</Button>
}
{
item.auditState === 3 && <Button type='primary'>更新</Button>
}
</div>
}
},
];

useEffect(() => {
axios.get(`/api/news?_expand=category&author=${username}&auditState_ne=0&publishState_lte=1`).then(res => {
// console.log(res.data);
setDataSource(res.data);
})
}, [username]);

return (
<div>
<Table dataSource={dataSource} columns={columns} pagination={{ pageSize: 5 }} rowKey={item => item.id} />;
</div>
)
}

1
const auditList = ['未审核', '审核中', '已通过', '未通过'];

you wan to revert from audit. it is easy, change the auditState from 2 to 0.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const navigate = useNavigate();

const handleRevert = item => {
setDataSource(dataSource.filter(data=>data.id!==item.id));

axios.patch(`/api/news/${item.id}`, {
auditStats: 0
}).then(res=>{
navigate('/news-manage/draft');
notification.info({
message: `Notification`,
description:
`Please go to draft box for reviewing your news`,
placement: 'bottomRight',
});
})
}

update is easy, useNavigate to NewsUpdate.

1
2
3
const handleUpdate = item => {
navigate(`/news-manage/update/${item.id}`);
}

publish news, change the publishState from 1 to 2

1
2
3
4
5
6
7
8
9
10
11
12
13
const handlePublish = item => {
axios.patch(`/api/news/${item.id}`, {
publishStats: 2
}).then(res => {
navigate('/publish-manage/published');
notification.info({
message: `Notification`,
description:
`Please go to published box for reviewing your news`,
placement: 'bottomRight',
});
})
}

2. Audit news

copy from AuditList.js and modify as below

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import { Table, Button } from 'antd';
import axios from 'axios';
import React, { useEffect, useState } from 'react'
import { useNavigate } from 'react-router'

export default function Audit() {
const [dataSource, setDataSource] = useState([])

const navigate = useNavigate();

const { roleId, region, username } = JSON.parse(localStorage.getItem('token'));

const columns = [
{
title: '新闻标题',
dataIndex: 'title',
render: (title, item) => {
return <a href={`#/news-manage/preview/${item.id}`}>{title}</a>
}
},
{
title: '新闻分类',
dataIndex: 'category',
render: category => {
return category.title
}
},
{
title: '作者',
dataIndex: 'author',
},
{
title: '操作',
render: item => {
return <div>
<Button type='primary'>通过</Button>
<Button >驳回</Button>
</div>
}
},
];

useEffect(() => {
const roleObj = {
"1": "superadmin",
"2": "admin",
"3": "editor",
}

axios.get(`/api/news?auditState=1&_expand=category`).then(res => {
console.log(res.data);
const list = res.data;
setDataSource(roleObj[roleId] === 'superadmin' ? list : [
...list.filter(item => item.username === username),
...list.filter(item => item.region === region && roleObj[item.roleId] === 'editor')
]);
});
}, [roleId, region, username]);

return (
<div>
<Table dataSource={dataSource} columns={columns} pagination={{ pageSize: 5 }} rowKey={item => item.id} />;
</div>
)
}

Audit & Reject
audit change the auditState from 1 to 2, publishState change to 1,
reject change the auditState from 1 to 3, publishState change to 0

1
2
3
4
5
6
7
8
9
10
const handleAudit = (item, auditState, publishState) => {
setDataSource(dataSource.filter(data => data.id !== item.id));

axios.patch(`/api/news/${item.id}`, {
auditState,
publishState
}).then(res=>{

})
}

3. Add NewsCategory

copy RightList.js and modify it as below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import { Button, Table, Modal } from 'antd';
import {
DeleteOutlined,
ExclamationCircleOutlined,
} from '@ant-design/icons';

import axios from 'axios';
import React, { useEffect, useState } from 'react'

const { confirm } = Modal;

export default function NewsCategory() {
const [dataSource, setDataSource] = useState([]);
const columns = [
{
title: 'ID',
dataIndex: 'id',
render: (id) => <b>{id}</b>
},
{
title: '分类名称',
dataIndex: 'title',
},
{
title: '操作',
render: item => {
return <div>
<Button danger shape='circle' icon={<DeleteOutlined />} onClick={() => confirmDelete(item)} />
</div>
}
},
];

const confirmDelete = item => {
confirm({
title: 'Do you Want to delete this item?',
icon: <ExclamationCircleOutlined />,
onOk() {
// console.log('OK');
realDelete(item)
},
onCancel() {
console.log('Cancel');
},
});
}

const realDelete = (item) => {
// console.log(item);
setDataSource(dataSource.filter(data => data.id !== item.id));
axios.delete(`/api/categories/${item.id}`);
}

useEffect(() => {
axios.get(`/api/categories`).then(res => {
// console.log(res.data);
setDataSource(res.data);
})
}, []);

return (
<div>
<Table dataSource={dataSource} columns={columns} pagination={{ pageSize: 5 }} rowKey={item => item.id} />;
</div>
)
}

use table Table EditCell for editing title

add components proeprty in Table, the components nees EditableRow and EditableCell, it is complex to read this document but need careful, at the end the NewsCategory should be looks like

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
import { Button, Table, Modal, Form, Input } from 'antd';
import {
DeleteOutlined,
ExclamationCircleOutlined,
} from '@ant-design/icons';

import axios from 'axios';
import React, { useContext, useState, useEffect, useRef } from 'react'

const { confirm } = Modal;

export default function NewsCategory() {
const [dataSource, setDataSource] = useState([]);

const EditableContext = React.createContext(null);

const columns = [
{
title: 'ID',
dataIndex: 'id',
render: (id) => <b>{id}</b>
},
{
title: '分类名称',
dataIndex: 'title',
onCell: (record) => ({
record,
editable: true,
dataIndex: 'title',
title: '分类名称',
handleSave: handleCellSave,
}),
},
{
title: '操作',
render: item => {
return <div>
<Button danger shape='circle' icon={<DeleteOutlined />} onClick={() => confirmDelete(item)} />
</div>
}
},
];

const handleCellSave = record => {
// console.log(record);
setDataSource(dataSource.map(item => {
if (item.id === record.id) {
return {
id: item.id,
title: record.title,
value: record.title,
}
}
return item;
}))

axios.patch(`/api/categories/${record.id}`, {
title: record.title,
value: record.title,
})
}

const confirmDelete = item => {
confirm({
title: 'Do you Want to delete this item?',
icon: <ExclamationCircleOutlined />,
onOk() {
// console.log('OK');
realDelete(item)
},
onCancel() {
console.log('Cancel');
},
});
}

const realDelete = (item) => {
// console.log(item);
setDataSource(dataSource.filter(data => data.id !== item.id));
axios.delete(`/api/categories/${item.id}`);
}

useEffect(() => {
axios.get(`/api/categories`).then(res => {
// console.log(res.data);
setDataSource(res.data);
})
}, []);

const EditableRow = ({ index, ...props }) => {
const [form] = Form.useForm();
return (
<Form form={form} component={false}>
<EditableContext.Provider value={form}>
<tr {...props} />
</EditableContext.Provider>
</Form>
);
};

const EditableCell = ({
title,
editable,
children,
dataIndex,
record,
handleSave,
...restProps
}) => {
const [editing, setEditing] = useState(false);
const inputRef = useRef(null);
const form = useContext(EditableContext);
useEffect(() => {
if (editing) {
inputRef.current.focus();
}
}, [editing]);

const toggleEdit = () => {
setEditing(!editing);
form.setFieldsValue({
[dataIndex]: record[dataIndex],
});
};

const save = async () => {
try {
const values = await form.validateFields();
toggleEdit();
handleSave({ ...record, ...values });
} catch (errInfo) {
console.log('Save failed:', errInfo);
}
};

let childNode = children;

if (editable) {
childNode = editing ? (
<Form.Item
style={{
margin: 0,
}}
name={dataIndex}
rules={[
{
required: true,
message: `${title} is required.`,
},
]}
>
<Input ref={inputRef} onPressEnter={save} onBlur={save} />
</Form.Item>
) : (
<div
className="editable-cell-value-wrap"
style={{
paddingRight: 24,
}}
onClick={toggleEdit}
>
{children}
</div>
);
}

return <td {...restProps}>{childNode}</td>;
};

return (
<div>
<Table dataSource={dataSource} columns={columns} pagination={{ pageSize: 5 }} rowKey={item => item.id} components={{
body: {
row: EditableRow,
cell: EditableCell,
},
}} />;
</div>
)
}

We have finished the permission, let’s back to focus the busines of publish news.

1. performing the src/views/sandbox/news-manage/NewsAdd.js

use Header Basic to perform the title.

1
2
3
4
5
6
7
8
import { PageHeader } from 'antd'
...
<PageHeader
className="site-page-header"
onBack={() => null}
title="Add news"
subTitle="This is for adding news"
/>

and we use Step Basic for the progress.

1
2
3
4
5
<Steps current={current}>
<Step title="基本信息" description="新闻标题,新闻分类" />
<Step title="新闻内容" description="新闻主体内容" />
<Step title="新闻提交" description="保存草稿或者提交审核" />
</Steps>

chose Form Basic for the input and select button

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<div style={{ marginTop: '50px' }}>
<Form {...layout}>
<Form.Item name="title" label="新闻标题" rules={[{ required: true, message: "Please input news title." }]}>
<Input />
</Form.Item>
<Form.Item name="categoryId" label="新闻分类" rules={[{ required: true }]}>
<Select>
{
categoryList.map(item => {
return <Option value={item.id} key={item.id}>{item.title}</Option>
})
}
</Select>
</Form.Item>
</Form>
</div>

above we called step 1, next to step 2

2. Add news content.

use react draft editor for edit news content.

1
2
3
yarn add react-draft-wysiwyg draft-js draftjs-to-html html-to-draftjs
mkdir src/components/news-manage
touch src/components/news-manage/NewsEditor.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { convertToRaw } from 'draft-js'
import draftToHtml from 'draftjs-to-html';
import React, { useState } from 'react'
import { Editor } from 'react-draft-wysiwyg'
import 'react-draft-wysiwyg/dist/react-draft-wysiwyg.css'

export default function NewsEditor() {
const [editorState, setEditorState] = useState("")
return (
<div>
<Editor editorState={editorState}
toolbarClassName='toolbarClassName'
wrapperClassName='wrapperClassName'
editorClassName='editorClassName'
onEditorStateChange={editorState => setEditorState(editorState)}
onBlur={() => {
console.log(draftToHtml(convertToRaw(editorState.getCurrentContent())))
}} />
</div>
)
}

3. Add save in progress 3

use Notification Basic in step 3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const handleSave = auditState => {
axios.post('/api/news', {
...formInfo,
"author": User.username,
"region": User.region ? User.region : 'Global',
"roleId": User.roleId,
"auditState": auditState,
"publishState": 0,
"content": content,
"createTime": Date.now(),
"star": 0,
"view": 0
}).then(res => {
navigate(auditState === 0 ? '/news-manage/draft' : '/audit-manage/list');
notification.info({
message: `Notification`,
description:
`Please go to ${auditState === 0 ? "draft box" : "audit box"} for reviewing your news`,
placement: 'bottomRight',
});
});
}

4. perform the draft box

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
import { Button, Table, Modal } from 'antd';
import {
DeleteOutlined,
EditOutlined,
ExclamationCircleOutlined,
UploadOutlined,
} from '@ant-design/icons';

import axios from 'axios';
import React, { useEffect, useState } from 'react'

const { confirm } = Modal;

export default function NewsDraft() {
const [dataSource, setDataSource] = useState([]);

const { username } = JSON.parse(localStorage.getItem('token'));

const columns = [
{
title: 'ID',
dataIndex: 'id',
render: (id) => <b>{id}</b>
},
{
title: '新闻标题',
dataIndex: 'title',
},
{
title: '新闻分类',
dataIndex: 'category',
render: category => {
return category.title
}
},
{
title: '作者',
dataIndex: 'author',
},
{
title: '操作',
render: item => {
return <div>
<Button shape='circle' icon={<DeleteOutlined />} onClick={() => confirmDelete(item)} />
<Button type='primary' shape='circle' icon={<EditOutlined />} />
<Button danger shape='circle' icon={<UploadOutlined />} />

</div>
}
},
];

const confirmDelete = item => {
confirm({
title: 'Do you Want to delete this item?',
icon: <ExclamationCircleOutlined />,
onOk() {
// console.log('OK');
realDelete(item)
},
onCancel() {
console.log('Cancel');
},
});
}

const realDelete = (item) => {
// console.log(item);
setDataSource(dataSource.filter(data => data.id !== item.id));
axios.delete(`/api/news/${item.id}`);
}

useEffect(() => {
axios.get(`/api/news?_expand=category&author=${username}&auditState=0`).then(res => {
// console.log(res.data);
setDataSource(res.data);
})
}, [username]);

return (
<div>
<Table dataSource={dataSource} columns={columns} pagination={{ pageSize: 5 }} rowKey={item => item.id} />;
</div>
)
}

support news preview function.

1
2
touch src/views/sandbox/news-manage/NewsPreview.js
yarn add moment

moment is used for format DateTime.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import { PageHeader, Descriptions } from 'antd'
import axios from 'axios';
import moment from 'moment';
import React, { useEffect, useState } from 'react'
import { useParams } from 'react-router';

export default function NewsPreview() {
const [newsInfo, setNewsInfo] = useState(null);

const params = useParams();

useEffect(() => {
// console.log(props.match.params.id)
axios.get(`/api/news/${params.id}?_expand=category&_expand=role`).then(res => {
console.log(res.data);
setNewsInfo(res.data);
})
}, [params.id]);

const auditList = ['未审核', '审核中', '已通过', '未通过'];
const publishList = ['未发布', '待发布', '已发布', '已下线'];

return (
newsInfo && <div>
<PageHeader
onBack={() => window.history.back()}
title={newsInfo.title}
subTitle={newsInfo.category?.title}>
<Descriptions size="small" column={3}>
<Descriptions.Item label="创建者">{newsInfo.author}</Descriptions.Item>
<Descriptions.Item label="创建时间">{moment(newsInfo.createTime).format("YYYY/MM/DD HH:mm:ss")}</Descriptions.Item>
<Descriptions.Item label="发布时间">{newsInfo.publishTime ? moment(newsInfo.publishTime).format("YYYY/MM/DD HH:mm:ss") : "-"}</Descriptions.Item>
<Descriptions.Item label="区域">{newsInfo.region}</Descriptions.Item>
<Descriptions.Item label="审核状态"><span style={{ color: "red" }}>{auditList[newsInfo.auditState]}</span></Descriptions.Item>
<Descriptions.Item label="发布状态"><span style={{ color: "red" }}>{publishList[newsInfo.publishState]}</span></Descriptions.Item>
<Descriptions.Item label="访问数量">{newsInfo.view}</Descriptions.Item>
<Descriptions.Item label="点赞数量">{newsInfo.star}</Descriptions.Item>
<Descriptions.Item label="评论数量">0</Descriptions.Item>
</Descriptions>
</PageHeader>
<div dangerouslySetInnerHTML={{
__html: newsInfo.content
}} style={{
margin: "0 24px",
border: "1px solid gray"
}}>
</div>
</div>
)
}

support edit news

1
touch src/views/sandbox/news-manage/NewsUpdate.js

most of code can copy from NewsAdd.js and modify to as below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
import { PageHeader, Steps, Button, Form, Input, Select, message, notification } from 'antd'
import axios from 'axios';
import React, { useEffect, useState, useRef } from 'react'
import { useNavigate, useParams } from 'react-router';
import NewsEditor from '../../../components/news-manage/NewsEditor';
import style from './News.module.css'
const { Step } = Steps;
const { Option } = Select;

export default function NewsUpdate() {
const layout = {
labelCol: { span: 4 },
wrapperCol: { span: 20 },
};

const User = JSON.parse(localStorage.getItem('token'));

const [current, setCurrent] = useState(0);
const [categoryList, setCategoryList] = useState([]);
const [formInfo, setFormInfo] = useState({});
const [content, setContent] = useState('')

const newsForm = useRef(null);
const navigate = useNavigate();
const params = useParams();

const handlePrevious = () => {
setCurrent(current - 1);
}

const handleNext = () => {
if (current === 0) {
newsForm.current.validateFields().then(res => {
// console.log(res);
setFormInfo(res);
setCurrent(current + 1);
}).catch(err => {
console.log(err);
})
} else {
// console.log(formInfo,content);
if (content === '' || content.trim() === '<p></p>') {
message.error(`News content can not be empty!`);
} else {
setCurrent(current + 1);
}
}
}

const handleSave = auditState => {
axios.patch(`/api/news/${params.id}`, {
...formInfo,
"auditState": auditState,
"content": content,
}).then(res => {
navigate(auditState === 0 ? '/news-manage/draft' : '/audit-manage/list');
notification.info({
message: `Notification`,
description:
`Please go to ${auditState === 0 ? "draft box" : "audit box"} for reviewing your news`,
placement: 'bottomRight',
});
});
}

useEffect(() => {
axios.get(`/api/categories`).then(res => {
// console.log(res.data);
setCategoryList(res.data);
})
}, []);

useEffect(() => {
// console.log(props.match.params.id)
axios.get(`/api/news/${params.id}?_expand=category&_expand=role`).then(res => {
// console.log(res.data);
// setNewsInfo(res.data);
// content
// formInfo
const { title, categoryId, content } = res.data;

newsForm.current.setFieldsValue({
title,
categoryId
});
setContent(content);
})
}, [params.id]);

return (
<div>
<PageHeader
className="site-page-header"
onBack={() => navigate('/news-manage/draft')}
title="Update news"
subTitle="This is for update news"
/>
<Steps current={current}>
<Step title="基本信息" description="新闻标题,新闻分类" />
<Step title="新闻内容" description="新闻主体内容" />
<Step title="新闻提交" description="保存草稿或者提交审核" />
</Steps>
<div style={{ marginTop: '50px' }}>
<div className={current === 0 ? '' : style.active}>
<Form {...layout} name='basic' ref={newsForm}>
<Form.Item name="title" label="新闻标题" rules={[{ required: true, message: "Please input news title." }]}>
<Input />
</Form.Item>
<Form.Item name="categoryId" label="新闻分类" rules={[{ required: true }]}>
<Select>
{
categoryList.map(item => {
return <Option value={item.id} key={item.id}>{item.title}</Option>
})
}
</Select>
</Form.Item>
</Form>
</div>
</div>

<div className={current === 1 ? '' : style.active}>
<NewsEditor getContent={value => {
// console.log(value);
setContent(value);
}} content={content}></NewsEditor>
</div>

<div className={current === 2 ? '' : style.active}>
</div>

<div style={{ marginTop: '50px' }}>
{current > 0 && <Button onClick={handlePrevious}> 上一步</Button>}
{current < 2 && <Button type='primary' onClick={handleNext}> 下一步</Button>}
{
current === 2 && <sapn>
<Button type='primary' onClick={() => handleSave(0)}>保存草稿箱</Button>
<Button danger onClick={() => handleSave(1)}>提交审核</Button>
</sapn>
}
</div>
</div>
)
}

and the NewsEditor looks like below

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import { ContentState, convertToRaw, EditorState } from 'draft-js'
import draftToHtml from 'draftjs-to-html';
import htmlToDraft from 'html-to-draftjs';
import React, { useEffect, useState } from 'react'
import { Editor } from 'react-draft-wysiwyg'
import 'react-draft-wysiwyg/dist/react-draft-wysiwyg.css'

export default function NewsEditor(props) {
const [editorState, setEditorState] = useState("");

useEffect(() => {
// console.log(props.content);
const html = props.content;

if (html === undefined) {
return;
}

const contentBlock = htmlToDraft(html);
if (contentBlock) {
const contentState = ContentState.createFromBlockArray(contentBlock.contentBlocks);
const editorState = EditorState.createWithContent(contentState);
setEditorState(editorState);
}
}, [props.content]);

return (
<div>
<Editor editorState={editorState}
toolbarClassName='toolbarClassName'
wrapperClassName='wrapperClassName'
editorClassName='editorClassName'
onEditorStateChange={editorState => setEditorState(editorState)}
onBlur={() => {
props.getContent(draftToHtml(convertToRaw(editorState.getCurrentContent())));
}} />
</div>
)
}

support Audit feature. bind a handleAudit event then fine for supporting this feature.

1
2
3
4
5
6
7
8
9
10
11
12
13
const handleAudit = id => {
axios.patch(`/api/news/${id}`, {
auditState: 1
}).then(res => {
navigate('/audit-manage/list');
notification.info({
message: `Notification`,
description:
`Please go to audit box for checking your news`,
placement: 'bottomRight',
});
});
}

It’s time to implement a Login componnet.

1. Login out in TopHeader.js

1
2
3
4
5
6
7
8
9
import { useNavigate } from 'react-router';

const navigate = useNavigate();

<Menu.Item danger key='loginOutMenu' onClick={()=>{
localStorage.removeItem('token');
navigate('/login');
}}>Login out</Menu.Item>

2. create Login component in src/views/login/Login.js

we use particles for performing paticles.

1
2
3
yarn add react-tsparticles

touch src/views/login/Login.css

The css looks like below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.formContainer {
position: fixed;
background-color: rgba(0,0,0,0.7);
width: 500px;
height: 300px;
top: 50%;
left: 50%;
transform: translate(-50%,-50%);
padding: 20px;
z-index: 100;
}
.loginTitle {
text-align: center;
height: 80px;
line-height: 80px;
font-size: 30px;
color:white;
}

We use localStorage to store token for verifing and retrieving.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const navigate = useNavigate();

const onFormFinish = (values) => {
// console.log(values);

//Because the limition of Json-server, here we chose verb get. we should use post in real db.
axios.get(`/api/users?_expand=role&username=${values.username}&password=${values.password}&roleState=true`).then(res => {
// console.log(res.data);
if(res.data.length === 0) {
message.error(`UserName or Password is wrong!`)
} else {
localStorage.setItem('token',JSON.stringify(res.data[0]));
navigate('/');
}
})
};

3. Back to TopHeader.js to perform and add real information in there.

1
const { role: { roleName }, username } = JSON.parse(localStorage.getItem('token'));

4. Checking the SideMenu, we should guarantee difference user has difference menu.

1
2
3
4
5
const { role: { rights } } = JSON.parse(localStorage.getItem('token'));

const checkPagePermission = (item) => {
return item.pagepermission === 1 && rights.includes(item.key);
}

5. locale admin only see his region user and himself, perform in UserList.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const { roleId, region, username } = JSON.parse(localStorage.getItem('token'));
const roleObj = {
"1": "superadmin",
"2": "admin",
"3": "editor",
}
...
useEffect(() => {
axios.get(`/api/users?_expand=role`).then(res => {
// console.log(res.data);
// setDataSource(res.data);
const list = res.data;
setDataSource(roleObj[roleId] === 'superadmin' ? list : [
...list.filter(item => item.username === username),
...list.filter(item => item.region === region && roleObj[item.roleId] === 'editor')
]);
});
}, []);

6. Difference use has difference permission in add and edit user form.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const { roleId, region } = JSON.parse(localStorage.getItem('token'));
const roleObj = {
"1": "superadmin",
"2": "admin",
"3": "editor",
}

const checkRegionDisabled = item => {
if (roleObj[roleId] === 'superadmin') {
return false;
}

if (props.isUpdate) {
return true;
}

return item.value !== region;
}

const checkRoleDisabled = item => {
if (roleObj[roleId] === 'superadmin') {
return false;
}

if (props.isUpdate) {
return true;
}

return roleObj[item.id] !== 'editor';
}
1
touch src/components/sandbox/NewsRouter.js

This is a second level router, the content we can copy from NewsSandBox.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React from 'react'
import { Routes, Route, Navigate } from 'react-router-dom'
import Home from '../../views/sandbox/home/Home'
import NoPermission from '../../views/sandbox/nopermission/NoPermission'
import RightList from '../../views/sandbox/right-manage/RightList'
import RoleList from '../../views/sandbox/right-manage/RoleList'
import UserList from '../../views/sandbox/user-manage/UserList'

export default function NewsRouter() {
return (
<Routes>
<Route path="home" element={<Home />} />
<Route path="user-manage/list" element={<UserList />} />
<Route path="right-manage/role/list" element={<RoleList />} />
<Route path="right-manage/right/list" element={<RightList />} />
<Route path="/" element={<Navigate replace from="/" to="home" />} />
<Route path="/*" element={<NoPermission />} />
</Routes>

)
}

8 Create route dynamiclly

1
2
3
4
5
6
7
8
9
10
11
mkdir src/views/sandbox/news-manage
touch src/views/sandbox/news-manage/NewsAdd.js
touch src/views/sandbox/news-manage/NewsDraft.js
touch src/views/sandbox/news-manage/NewsCategory.js
mkdir src/views/sandbox/audit-manage
touch src/views/sandbox/audit-manage/Audit.js
touch src/views/sandbox/audit-manage/AuditList.js
mkdir src/views/sandbox/publish-manage
touch src/views/sandbox/publish-manage/Unpublished.js
touch src/views/sandbox/publish-manage/Published.js
touch src/views/sandbox/publish-manage/Sunset.js

defined a localRouteMap and use map for crating route.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import axios from 'axios'
import React, { useEffect, useState } from 'react'
import { Routes, Route, Navigate } from 'react-router-dom'
import Audit from '../../views/sandbox/audit-manage/Audit'
import AuditList from '../../views/sandbox/audit-manage/AuditList'
import Home from '../../views/sandbox/home/Home'
import NewsAdd from '../../views/sandbox/news-manage/NewsAdd'
import NewsCategory from '../../views/sandbox/news-manage/NewsCategory'
import NewsDraft from '../../views/sandbox/news-manage/NewsDraft'
import NoPermission from '../../views/sandbox/nopermission/NoPermission'
import Published from '../../views/sandbox/publish-manage/Published'
import Sunset from '../../views/sandbox/publish-manage/Sunset'
import Unpublished from '../../views/sandbox/publish-manage/Unpublished'
import RightList from '../../views/sandbox/right-manage/RightList'
import RoleList from '../../views/sandbox/right-manage/RoleList'
import UserList from '../../views/sandbox/user-manage/UserList'

const LocalRouterMap = {
"/home": <Home />,
"/user-manage/list": <UserList />,
"/right-manage/role/list": <RoleList />,
"/right-manage/right/list": <RightList />,
"/news-manage/add": <NewsAdd />,
"/news-manage/draft": <NewsDraft />,
"/news-manage/category": <NewsCategory />,
"/audit-manage/audit": <Audit />,
"/audit-manage/list": <AuditList />,
"/publish-manage/unpublished": <Unpublished />,
"/publish-manage/published": <Published />,
"/publish-manage/sunset": <Sunset />,
};

export default function NewsRouter() {
const [BackRouteList, setBackRouteList] = useState([]);

useEffect(() => {
Promise.all([
axios.get(`/api/rights`),
axios.get(`/api/children`),
]).then((res) => {
// console.log(res.data);
setBackRouteList([...res[0].data, ...res[1].data]);
});
}, []);

return (
<Routes>
{BackRouteList.map(item => (
<Route
path={item.key}
key={item.key}
element={LocalRouterMap[item.key]}
/>
))}
<Route path="/" element={<Navigate replace from="/" to="home" />} />
<Route path="/*" element={<NoPermission />} />
</Routes>

)
}

add permission for route checking and user checking

1
2
3
4
5
6
7
8
9
const checkRoute = item => {
return LocalRouterMap[item.key] && item.pagepermission;
}

const checkUserPermission = item => {
return rights.includes(item.key);
}

const { role: { rights } } = JSON.parse(localStorage.getItem('token'));

9. Add progress in NewsSandBox

1
yarn add nprogress

we use NProcess to peform this progress feature.

1
2
3
4
5
6
7
8
9
import nProgress from 'nprogress'

import 'nprogress/nprogress.css'

nProgress.start()
useEffect(() => {
nProgress.done();
})

Today let perform the right and role in news-publish-management

1. Use the Table Basic component in src/views/sandbox/right-manage/RightList.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import { Table } from 'antd';
import axios from 'axios';
import React, { useEffect, useState } from 'react'

export default function RightList() {
const [dataSource, setDataSource] = useState([]);
const columns = [
{
title: 'ID',
dataIndex: 'id',
},
{
title: '权限名称',
dataIndex: 'title',
},
{
title: '权限名称',
dataIndex: 'key',
},
];

useEffect(() => {
axios.get("/api/rights").then(res => {
// console.log(res.data);
setDataSource(res.data);
})
}, []);

return (
<div>
<Table dataSource={dataSource} columns={columns} />;
</div>
)
}

2.Custom some column, we can use render property, render proeprty must a function. function first in Ant

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const columns = [
{
title: 'ID',
dataIndex: 'id',
render: (id)=> <b>{id}</b>
},
{
title: '权限名称',
dataIndex: 'title',
},
{
title: '权限路径',
dataIndex: 'key',
render: (key)=>{
return <Tag color='orange'>{key}</Tag>
}
},
];

3. Add operation column in the table component

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import {
DeleteOutlined,
EditOutlined,
} from '@ant-design/icons';
...
{
title: '操作',
render: () => {
return <div>
<Button danger shape='circle' icon={<DeleteOutlined />} />
<Button type='primary' shape='circle' icon={<EditOutlined />} />
</div>
}
}

4. Add pagenition for Table

1
<Table dataSource={dataSource} columns={columns} pagination={{ pageSize: 5 }} />;

5. use Tree to display children, it is pretty easy, change the useEffect to

1
2
3
4
5
6
7
useEffect(() => {
axios.get("/api/rights?_embed=children").then(res => {
// console.log(res.data);
res.data.forEach((item) => item.children?.length === 0 ? item.children = "" : item.children);
setDataSource(res.data);
})
}, []);

Here the Tree is not the Tree antd component, it is one of the column in RightList.

6. Get and Edit in RightList.js

Because delete is danger,so we popup a Model Basic information for confirming.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//show confirm modal
const confirmDelete = (item) => {
confirm({
title: 'Do you Want to delete these items?',
icon: <ExclamationCircleOutlined />,
onOk() {
// console.log('OK');
realDelete(item)
},
onCancel() {
console.log('Cancel');
},
});
}

//delete item in both frontEnd and backEnd
const realDelete = (item) => {
// console.log(item);
setDataSource(dataSource.filter(data => data.id !== item.id));
axios.delete(`/api/rights/${item.id}`);
}

above command can delete level 1 menu, but when we try to delete level 2 menu, then we wil encountered an issue says the data not found. How to delete level 2 menu? Let’s look back the response data. We can get the first level data according the rightId, and then filter the children, and then send delete in children table.

1
2
3
4
5
6
7
8
9
10
11
12
13
const realDelete = (item) => {
// console.log(item);
if (item.grade === 1) {
setDataSource(dataSource.filter(data => data.id !== item.id));
axios.delete(`/api/rights/${item.id}`);
} else {
const list = dataSource.filter(data => data.id === item.rightId);
list[0].children = list[0].children.filter(data => data.id !== item.id);
setDataSource([...dataSource]);
axios.delete(`/api/children/${item.id}`);
}

}

Another button is Edit button, edit button control the page configuration. We will chose Popover to perform this feature.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<Popover
content={<div style={{ textAlign: 'center' }}>
<Switch checked={item.pagepermission} onChange={() => swithPageConfiguration(item)}></Switch>
</div>}
title="Page configuration" trigger={item.pagepermission === undefined ? '' : 'click'}>
<Button type='primary' shape='circle' icon={<EditOutlined />} disabled={item.pagepermission === undefined} />
</Popover>
...
const swithPageConfiguration = item => {
// console.log(item);
item.pagepermission = item.pagepermission === 1 ? 0 : 1;
setDataSource([...dataSource])
if (item.grade === 1) {
axios.patch(`/api/rights/${item.id}`, { pagepermission: item.pagepermission });
} else {
axios.patch(`/api/children/${item.id}`, { pagepermission: item.pagepermission });
}
}

7. Use Table to perform role in RoleList.js

We already implemented the RightList, so we can refer that implement.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import { Button, Table, Modal } from 'antd';
import {
DeleteOutlined,
EditOutlined,
ExclamationCircleOutlined,
} from '@ant-design/icons';

import axios from 'axios';
import React, { useEffect, useState } from 'react'

const { confirm } = Modal;

export default function RoleList() {
const [dataSource, setDataSource] = useState([]);
const columns = [
{
title: 'ID',
dataIndex: 'id',
render: (id) => <b>{id}</b>
},
{
title: '角色名称',
dataIndex: 'roleName',
},
{
title: '操作',
render: item => {
return <div>
<Button danger shape='circle' icon={<DeleteOutlined />} onClick={() => confirmDelete(item)} />
<Button type='primary' shape='circle' icon={<EditOutlined />} />
</div>
}
},
];

const confirmDelete = item => {
confirm({
title: 'Do you Want to delete these items?',
icon: <ExclamationCircleOutlined />,
onOk() {
// console.log('OK');
realDelete(item)
},
onCancel() {
console.log('Cancel');
},
});
}

const realDelete = (item) => {
// console.log(item);
setDataSource(dataSource.filter(data => data.id !== item.id));
axios.delete(`/api/roles/${item.id}`);
}

useEffect(() => {
axios.get("/api/roles").then(res => {
// console.log(res.data);
setDataSource(res.data);
})
}, []);

return (
<div>
<Table dataSource={dataSource} columns={columns} pagination={{ pageSize: 5 }} rowKey={item => item.id} />;
</div>
)
}

use Tree Basic component to perform edit feature in RoleList.js

Also we use Modal Basic to popup the tree.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<Modal title="Permisson Configuration" visible={isModalVisible} onOk={handleOk} onCancel={handleCancel}>
<Tree
checkable
checkedKeys={currentRights}
onCheck={handleCheck}
checkStrictly={true}
treeData={treeData}
/>
</Modal>
...
const [treeData, setTreeData] = useState([]);
const [currentRights, setCurrentRights] = useState([]);
const [currentId, setCurrentId] = useState(0);
const [isModalVisible, setIsModalVisible] = useState(false);
...
const editPermission = item => {
setIsModalVisible(true);
setCurrentRights(item.rights);
setCurrentId(item.id);
}

const handleOk = () => {
setIsModalVisible(false);
setDataSource(dataSource.map(item=>{
if(item.id == currentId) {
return {
...item,
rights: currentRights
}
}
return item;
}));

axios.patch(`/api/roles/${currentId}`,{rights:currentRights});
}
const handleCancel = () => {
setIsModalVisible(false);
}

const handleCheck = (checkedKeys) => {
// console.log(checkedKeys);
setCurrentRights(checkedKeys);
}
...
useEffect(() => {
axios.get(`/api/rights?_embed=children`).then(res => {
// console.log(res.data);
setTreeData(res.data)
})
}, []);

8. Use Table to perform user in UserList.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
import { Button, Table, Modal, Switch } from 'antd';
import {
DeleteOutlined,
EditOutlined,
ExclamationCircleOutlined,
} from '@ant-design/icons';

import axios from 'axios';
import React, { useEffect, useState } from 'react'

const { confirm } = Modal;

export default function UserList() {
const [dataSource, setDataSource] = useState([]);
const columns = [
{
title: '区域',
dataIndex: 'region',
render: (region) => <b>{region === '' ? 'Global' : region}</b>
},
{
title: '角色名称',
dataIndex: 'role',
render: role => {
return role?.roleName
}
},
{
title: '用户名',
dataIndex: 'username',
},
{
title: '用户状态',
dataIndex: 'roleState',
render: (roleState, item) => {
return <Switch checked={roleState} disabled={item.default} ></Switch>
}
},
{
title: '操作',
render: item => {
return <div>
<Button danger shape='circle' icon={<DeleteOutlined />} disabled={item.default} onClick={() => confirmDelete(item)} />

<Button type='primary' shape='circle' icon={<EditOutlined />} disabled={item.default} />
</div>
}
},
];

const confirmDelete = item => {
confirm({
title: 'Do you Want to delete these items?',
icon: <ExclamationCircleOutlined />,
onOk() {
// console.log('OK');
realDelete(item)
},
onCancel() {
console.log('Cancel');
},
});
}

const realDelete = (item) => {
// console.log(item);
}

useEffect(() => {
axios.get(`/api/users?_expand=role`).then(res => {
// console.log(res.data);
res.data.forEach((item) => item.children?.length === 0 ? item.children = "" : item.children);
setDataSource(res.data);
})
}, []);

return (
<div>
<Table dataSource={dataSource} columns={columns} pagination={{ pageSize: 5 }} rowKey={item => item.id} />;
</div>
)
}

9. CRUD user in UserList.js

use Form in Modal component to perform this feature.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
import { Button, Table, Modal, Switch, Form, Input, Select } from 'antd';
import {
DeleteOutlined,
EditOutlined,
ExclamationCircleOutlined,
} from '@ant-design/icons';

import axios from 'axios';
import React, { useEffect, useState } from 'react'

const { confirm } = Modal;
const { Option } = Select;

export default function UserList() {
const [dataSource, setDataSource] = useState([]);
const [regionList, setRegionList] = useState([]);
const [roleList, setRoleList] = useState([]);
const [addFormVisible, setAddFormVisible] = useState(false)
const columns = [
{
title: '区域',
dataIndex: 'region',
render: (region) => <b>{region === '' ? 'Global' : region}</b>
},
{
title: '角色名称',
dataIndex: 'role',
render: role => {
return role?.roleName
}
},
{
title: '用户名',
dataIndex: 'username',
},
{
title: '用户状态',
dataIndex: 'roleState',
render: (roleState, item) => {
return <Switch checked={roleState} disabled={item.default} ></Switch>
}
},
{
title: '操作',
render: item => {
return <div>
<Button danger shape='circle' icon={<DeleteOutlined />} disabled={item.default} onClick={() => confirmDelete(item)} />

<Button type='primary' shape='circle' icon={<EditOutlined />} disabled={item.default} />
</div>
}
},
];

const confirmDelete = item => {
confirm({
title: 'Do you Want to delete these items?',
icon: <ExclamationCircleOutlined />,
onOk() {
// console.log('OK');
realDelete(item)
},
onCancel() {
console.log('Cancel');
},
});
}

const realDelete = (item) => {
// console.log(item);
}

useEffect(() => {
axios.get(`/api/users?_expand=role`).then(res => {
// console.log(res.data);
setDataSource(res.data);
})
}, []);

useEffect(() => {
axios.get(`/api/regions`).then(res => {
// console.log(res.data);
setRegionList(res.data);
})
}, []);

useEffect(() => {
axios.get(`/api/roles`).then(res => {
// console.log(res.data);
setRoleList(res.data);
})
}, []);

return (
<div>
<Button type='primary' onClick={() => setAddFormVisible(true)}>Add new user</Button>
<Table dataSource={dataSource} columns={columns} pagination={{ pageSize: 5 }} rowKey={item => item.id} />;
<Modal
visible={addFormVisible}
title="Create a new user"
okText="Create"
cancelText="Cancel"
onCancel={() => setAddFormVisible(false)}
onOk={() => {
setAddFormVisible(false);
}}>
<Form layout="vertical">
<Form.Item
name="username"
label="User Name"
rules={[{ required: true, message: 'Please input the user name!' }]}>
<Input />
</Form.Item>
<Form.Item
name="password"
label="Password"
rules={[{ required: true, message: 'Please input the password!' }]}>
<Input />
</Form.Item>
<Form.Item
name="region"
label="Region"
rules={[{ required: true, message: 'Please input the region!' }]}>
<Select>
{
regionList.map(item =>
<Option value={item.value} key={item.id}>{item.title}</Option>
)
}
</Select>
</Form.Item>
<Form.Item
name="roleId"
label="Role"
rules={[{ required: true, message: 'Please input the role!' }]}>
<Select>
{
roleList.map(item =>
<Option value={item.id} key={item.id}>{item.roleName}</Option>
)
}
</Select>
</Form.Item>
</Form>
</Modal>
</div>
)
}

10. Wrapper addForm as component

1
2
mkdir src/components/user-manage
touch src/components/user-manage/UserForm.js

Copy the Form section to this file.

use forwardRef for exporting UserForm.js for communicating with UserList.js

create a ref in UserList.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React, { useRef } from 'react'
...
const addForm = useRef(null);
...

<Modal
visible={addFormVisible}
title="Create a new user"
okText="Create"
cancelText="Cancel"
onCancel={() => setAddFormVisible(false)}
onOk={() => {
// console.log(addForm);
addForm.current.validateFields().then(value => {
console.log(value)
}).catch(err => {
console.log(err);
})
}}>
<UserForm regionList={regionList} roleList={roleList} ref={addForm} />
</Modal>

The UserForm.js we change the definition formation

1
2
3
4
5
6
7
8
const UserForm = forwardRef((props, ref) => {
return (
<Form layout="vertical" ref={ref} >
...
</Form>
)
});
export default UserForm;

we can set the modalOk as below

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const addFormOk = () => {
// console.log(addForm);
addForm.current.validateFields().then(value => {
// console.log(value)
setAddFormVisible(false);
addForm.current.resetFields();
axios.post(`/api/users`,{
...value,
'roleState': true,
'default': false,
}).then(res=>{
// console.log(res.data);
setDataSource([...dataSource, {...res.data,
role: roleList.filter(item=>item.id=value.roleId)[0]}])
})
}).catch(err => {
console.log(err);
})
}

Delete user in UserList.js

1
2
3
4
5
const realDelete = (item) => {
// console.log(item);
setDataSource(dataSource.filter(data=>data.id!==item.id));
axios.delete(`/api/users/${item.id}`);
}

Update user status in UserList.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    {
title: '用户状态',
dataIndex: 'roleState',
render: (roleState, item) => {
return <Switch checked={roleState} disabled={item.default} onChange={() => handleUserState(item)} ></Switch>
}
},
....
const handleUserState = (item)=> {
// console.log(item);
item.roleState = !item.roleState;
setDataSource([...dataSource])
axios.patch(`/api/users/${item.id}`,{roleState:item.roleState});
}

Update user in UserList.js
It is very similar with addForm, so copy an updateForm modal.

1
omit

Filter data in region field in UserList.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const columns = [
{
title: '区域',
dataIndex: 'region',
filters: [
...regionList.map(item => ({
text: item.title,
value: item.value
})),
{
text: 'Global',
value: ''
}
],
onFilter: (value, item) => item.region === value,
render: (region) => <b>{region === '' ? 'Global' : region}</b>
},
...
}

Here I am going to perform the layout first for news-publish-management

1. Setup setupProxy.js

1
2
yarn add http-proxy-middleware
touch src/setupProxy.js

copy below code-snippets into setupProxy.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
const { createProxyMiddleware } = require('http-proxy-middleware');

module.exports = function(app) {
app.use(
'/api',
createProxyMiddleware({
target: 'https://news-publish-management.herokuapp.com',
changeOrigin: true,
})
);
};
````
> Why we setup like this in our project? The main reason for doing this is avoids [CORS](https://stackoverflow.com/questions/21854516/understanding-ajax-cors-and-security-considerations) issues
### 2. Setup router
```bash
yarn add react-router-dom

mkdir src/router
touch src/router/IndexRouter.js

mkdir src/views
mkdir src/views/login
touch src/views/login/Login.js
mkdir src/views/sandbox
touch src/views/sandbox/NewsSandBox.js
mkdir src/views/sandbox/home
touch src/views/sandbox/home/Home.js
mkdir src/views/sandbox/user-manage
touch src/views/sandbox/user-manage/UserList.js
mkdir src/views/sandbox/right-manage
touch src/views/sandbox/right-manage/RightList.js
touch src/views/sandbox/right-manage/RoleList.js
mkdir src/views/sandbox/nopermission
touch src/views/sandbox/nopermission/NoPermission.js

mkdir src/components
mkdir src/components/sandbox
touch src/components/sandbox/SideMenu.js
touch src/components/sandbox/TopHeader.js

Every each JS file we can add below code-snippets for creating react function component quickly.

1
2
3
4
5
6
7
8
9
import React from 'react'

export default function ComponentName() {
return (
<div>
ComponentName
</div>
)
}

We can install a plugin ES7 React/Redux/GraphQL/React-Native snippets for VS Code. Once install successfully, use key rfc for lightning generating above code-snippets.

We assume src/router/IndexRouter.js is the first level router, when user already logined the system, they will be redirected to src/views/sandbox/NewsSandBox.js, otherwise they will be redireted to src/views/login/Login.js. So the IndexRouter.js looks like below

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React from 'react'
import { HashRouter, Routes, Route, Navigate } from 'react-router-dom'
import Login from '../views/login/Login'
import NewsSandBox from '../views/sandbox/NewsSandBox'

export default function IndexRouter() {
return (
<HashRouter>
<Routes>
<Route path="/login" element={<Login/>} />
<Route path="*" element={localStorage.getItem("token") ? <NewsSandBox/> : <Navigate to="/login" />} />
</Routes>
</HashRouter>
)
}

Go to src/App.js, and import our src/router/IndexRouter.js component.

1
2
3
4
5
6
7
8
9
10
import React from 'react'
import IndexRouter from './router/IndexRouter'

export default function App() {
return (
<div>
<IndexRouter/>
</div>
)
}

Go to browser, url https//localhost:3000 will show our Login component. If you manually setup the token with command of

1
localStorage.setItem("token","token")

then the browser will show NewsSandBox component.

3. Assume NewsSandBox.js as a second level router, and change its content to

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import React from 'react'
import { Routes, Route, Navigate } from 'react-router-dom'
import SideMenu from '../../components/sandbox/SideMenu'
import TopHeader from '../../components/sandbox/TopHeader'
import Home from './home/Home'
import NoPermission from './nopermission/NoPermission'
import RightList from './right-manage/RightList'
import RoleList from './right-manage/RoleList'
import UserList from './user-manage/UserList'

export default function NewsSandBox() {
return (
<div>
<SideMenu />
<TopHeader />
<Routes>
<Route path="home" element={<Home />} />
<Route path="user-manage/list" element={<UserList />} />
<Route path="right-manage/role/list" element={<RoleList />} />
<Route path="right-manage/right/list" element={<RightList />} />
<Route path="/" element={<Navigate replace from="/" to="home" />} />
<Route path="/*" element={<NoPermission />} />
</Routes>
</div>
)
}

4. Import Ant Design and setup layout, our layout was designed in src/views/sandbox/NewsSandBox.js. NewsSandBox contains src/components/sandbox/TopHeader.js and src/componets/sandbox/SideMenu.js`

1
yarn add antd

choose the custom trigger as our layout. First thing is show code and copy code into NewsSandBox and then sperate them as below

  • Layout.Sider was pasted to SideMenu
  • Layout.Header was pasted to TopHeader

The import thing is don’t forget to copy the CSS into our src/views/sandbox/NewsSandBox.css file.

5. Add click event on the collapsed/expanded icon in src/components/sandbox/TopHeader.js.

1
2
3
4
5
const [collapsed, setCollapsed] = useState(false)

const changeCollapsed = () => {
setCollapsed(!collapsed)
}

React hooks is important. Here we use the useState.

Then give the icon an onClick event property. for example

1
<MenuFoldOutlined onClick={changeCollapsed} />

6. Add Wellcome user in the TopHeader.

We copy the Dropdown Basic and Avator Basic components and modify them as we expected.

7. Modify the SideMenu

use axios for getting data.

1
2
3
yarn add axios
mkdir src/util
touch src/util/http.js

http.js with content

1
2
3
4
5
6
7
import axios from "axios";

axios.defaults.baseURL="https://news-publish-management.herokuapp.com"

// axios.defaults.headers
// axios.interceptors.request.use
// axios.interceptors.response.use

In this page, will use hook useEffect for getting data back and use hook useState to control returned data

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import React, { useEffect, useState } from 'react'
import { Layout, Menu } from 'antd';
import {
UserOutlined,
HomeOutlined,
CrownOutlined,
} from '@ant-design/icons';

import './index.css'
import { useNavigate } from 'react-router';
import axios from 'axios';

const { Sider } = Layout;
const { SubMenu } = Menu;

export default function SideMenu() {
const navigate = useNavigate();
const [menuList, setMenuList] = useState([]);

const renderMenu = (menuList) => {
return menuList.map(item => {
if (item.children) {
return <SubMenu key={item.key} icon={item.icon} title={item.title}>
{renderMenu(item.children)}
</SubMenu>
}
return <Menu.Item key={item.key} icon={item.icon} onClick={() => navigate(item.key)}>
{item.title}</Menu.Item>
})
}

useEffect(() => {
axios.get("/api/rights?_embed=children").then(res=>{
// console.log(res.data)
setMenuList(res.data);
})
}, [])

return (
<Sider trigger={null} collapsible collapsed={false}>
<div className="logo">News Publish Management</div>
<Menu theme="dark" mode="inline" defaultSelectedKeys={['1']}>
{renderMenu(menuList)}
</Menu>
</Sider>
)
}

8. Fix some issue for SideMenu

Finished above 7 steps, the page should be correctly rendering, but it is weird, some menu should be in second level route, but it is in SideMenu, another thing is each of the menu without icon. Let’s fix those issues we identified so far:

  • For issue 1, we can identifiy the field pagepermission, check its 1 or not.
    1
    2
    3
    const checkPagePermission = (item) => {
    return item.pagepermission === 1
    }

    pagepermission is very important!!

  • For issue 2, we can define an iconList to store icon, and use array[index] to get the icon.
    1
    2
    3
    4
    5
    6
    7
    8
        const iconList = {
    '/home':<HomeOutlined />,
    '/user-manage':<UserOutlined />,
    '/user-manage/list':<UserOutlined />,
    '/right-manage':<CrownOutlined />,
    '/right-manage/role/list':<CrownOutlined />,
    '/right-manage/right/list':<CrownOutlined />,
    }
  • Issue 3, The Home is no children, we do not need the expansed icon in the right, so we have to add children.length to check
    1
    item.children?.length > 0 && checkPagePermission(item)
  • Issue 4, the style is egly, we import antd.css into App.css
    1
    2
    3
    4
    5
    @import '~antd/dist/antd.css';

    ::-webkit-scrollbar {width: 5px;height: 5px;position:absolute}
    ::-webkit-scrollbar-thumb {background-color: #1890ff;}
    ::-webkit-scrollbar-track {background-color: #ddd;}

-Issue 5, focus the previous select menu if user refresh page.
Menu component have SelectedKeys and defaultOpenKeys, we can set those two field to fix our issue.
In Ant library, most of time, if field called defaultXXXX, that meant it is uncontrolled field, otherwise it use useState for controlling.

1
2
3
4
5
6
7
8
9
10
const location = useLocation();

const selectKeys = [location.pathname];
const openKeys = ["/" + location.pathname.split("/")[1]];

....

<Menu theme="dark" mode="inline" selectedKeys={selectKeys} defaultOpenKeys={openKeys}>
{renderMenu(menuList)}
</Menu>

I am learning react at home duraing the annual leave and company holiday. I have created a repository named news-publish-management at GitHub.

The node version I used is v16.13.1

The react version I used is ^17.0.2

The first commit is doing following 9 things:

  1. Use yarn for creating a new react app
    1
    yarn create react-app news-publish-manaments

    Yarn is a package manager for your code. It allows you to use and share code with other developers from around the world. Yarn does this quickly, securely, and reliably so you don’t ever have to worry.

Yarn allows you to use other developers’ solutions to different problems, making it easier for you to develop your software. If you have problems, you can report issues or contribute back on GitHub, and when the problem is fixed, you can use Yarn to keep it all up to date.

I will swith to yarn instead of npm after this learning section.

  1. Install json-server for deploying the db.json to hereku

    1
    yarn add json-server

    JSONServer was stored in GitHub.

  2. Modify the package.json, and append below line in scripts selction

    1
    "json-server": "node mock-api/server.js"
  3. Add netlify.toml

    1
    touch netlify.toml

    paste content into the toml file

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    [build]
    command = "CI= npm run build"

    [[redirects]]
    from = "/api/*"
    to = "https://news-publish-management.herokuapp.com/api/:splat"
    status = 200

    [[redirects]]
    from = "/*"
    to = "/index.html"
    status = 200
  4. Add Procfile, the detail information of ProcFile in Heroku can be foud in here. Base on this page, we can add below line in the Procfile as well.

    1
    web: npm run json-server-prod
  5. Create a server.js under mock-api folder.

    1
    2
    mkdir mock-api
    touch mock-api/server.js

    paste below content into server.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    const jsonServer = require("json-server");
    const server = jsonServer.create();
    const router = jsonServer.router("mock-api/db.json");
    const middlewares = jsonServer.defaults();
    const port = process.env.PORT || 4000;

    server.use(middlewares);
    server.use("/api", router);

    server.listen(port);
  1. Push above changs into to GitHub

  2. Go to Heroku for deploying news-publish-managements.
    Once it was successfully deployed. It can be verified via
    https://news-publish-management.herokuapp.com/api/news

  1. Go to netlify for deploying news-publish-managements.
    Once it was successfully deployed. It is can be verified via
    https://news-publish-management.netlify.app/

Welcome to Nate Liu’s Blog! This is step by step I created this blog with Hexo.

Step 1 install Git and Node

Official Website:

Git

Node

1
2
brew install git
brew install node

Step 2 install hexo

1
npm install hexo-cli -g

Step 3 initilize hexo-blog

1
2
3
4
hexo init hexo-blog
cd hexo-blog
npm install
hexo server

Step 4 generate html pages

1
2
3
hexo generate 
or
hexo g

Step 5 testing in local

1
2
3
hexo server 
or
hexo s

Step 6 deploy into github.io with One-command-deployment

1
npm install hexo-deployer-git --save

modify _config.yml, go to the end and add following lines:

1
2
3
4
deploy:
type: git
repo: https://github.com/nateliu/nateliu.github.io.git
branch: master

Step 7 launch with following lines

1
2
3
hexo clean
hexo generate
hexo deploy

Step 8 go to chrome to see the result.

My Hexo Blog: My-Hexo-Blog

Step 9 my source for hexo-blog

hexo-blog

Step 10 using GitHub actions automatically deploy hexo

  1. Generate public and private keys
    1
    2
    3
    cd hexo-blog
    git checkout main
    ssh-keygen -t rsa -b 4096 -C "jinliangliu@163.com" -f github-deploy-key -N ""
    Two files are generated in the directory:
  • github-deploy-key.pub —Public key file
  • github-deploy-key —Private key file

    Remember to add public and private keys to .gitignore Medium!!!

  1. GitHub adds public key
    In GitHub, in hexo-blog project, follow the Settings->Deploye keys->Add deploy key Find the corresponding page and add the public key. In this page, Title You can customize it,Key Add github-deploy-key.pub The contents of the document.

    Don’t copy more spaces!!!
    Remember to check it Allow write access. Otherwise, deployment will not be possible. Use below command to copy the contents into pasteboard

    1
    pbcopy < github-deploy-key.pub
  2. GitHub adds private key
    In GitHub, in nateliu.github.io project, follow the Settings->Secrets->Add a new secrets Find the corresponding page and add the private key. In this pageNameYou can customize it,ValueAdd github-deploy-key The contents of the document.

    Don’t copy more spaces!!!
    Use below command to copy the contents into pasteboard

    1
    pbcopy < github-deploy-key
  3. Create compilation script
    Create it in the hexo-blog branch (I’m here main branch).github/workflows/ci.yml The contents of the document are as follows:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    name: Build and Deploy
    on:
    push:
    branches:
    - main
    jobs:
    hexo-blog:
    runs-on: ubuntu-latest
    steps:
    - name: Checkout source
    uses: actions/checkout@v2
    with:
    ref: main
    - name: Use Node.js ${{ matrix.node-version }}
    uses: actions/setup-node@v2
    with:
    node-version: '${{ matrix.node-version }}'
    - name: Setup hexo
    env:
    ACTION_DEPLOY_KEY: ${{ secrets.DEPLOY }}
    run: |
    mkdir -p ~/.ssh/
    echo "$ACTION_DEPLOY_KEY" > ~/.ssh/id_rsa
    chmod 600 ~/.ssh/id_rsa
    ssh-keyscan github.com >> ~/.ssh/known_hosts
    git config --global user.email "jinliangliu@163.com"
    git config --global user.name "nateliu"
    npm install hexo-cli -g
    npm install
    - name: Hexo deploy
    run: |
    hexo clean
    hexo generate
    hexo deploy
  4. Hexo configuration
    Modify in project root _config.yml, add deployment related content:

1
2
3
4
deploy:
type: git
repo: git@github.com:nateliu/nateliu.github.io.git
branch: master

It looks like Step 6 before mentioned, but update the repo:

  1. Verification
    Now the hexo-blog has been integrated with GitHub actions, push the code into main branch to automatically compile and deploy. specific
    The execution process can be performed in ActionsView

Hexo GitHub pages
GitHub actions automatically deploy hexo
Hexo Action

Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub.

Quick Start

Create a new post

1
$ hexo new "My New Post"

More info: Writing

Run server

1
$ hexo server

More info: Server

Generate static files

1
$ hexo generate

More info: Generating

Deploy to remote sites

1
$ hexo deploy

More info: Deployment