Skip to content

Commit 99b09e8

Browse files
committed
feat: complete topic comment & reply
1 parent 1676b5c commit 99b09e8

File tree

14 files changed

+384
-130
lines changed

14 files changed

+384
-130
lines changed

config/config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { defineConfig } from 'umi';
55
export default defineConfig({
66
singular: true,
77
fastRefresh: {},
8-
mfsu: {},
8+
// mfsu: {},
99

1010
nodeModulesTransform: {
1111
type: 'none',

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,4 @@
4848
"umi": "^3.5.20",
4949
"yorkie": "^2.0.0"
5050
}
51-
}
51+
}

src/access.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export default function (initialState: InitialState) {
2+
const { token } = initialState;
3+
4+
return {
5+
canPostComment: !!token,
6+
};
7+
}

src/component/Markdown/index.less

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
.editor {
2+
border: none;
3+
4+
:global {
5+
.editor-container {
6+
> .sec-html {
7+
> .html-wrap {
8+
padding: 0;
9+
}
10+
}
11+
}
12+
}
13+
}

src/component/Markdown/index.tsx

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import React from 'react';
2+
import MarkdownIt from 'markdown-it';
3+
import MdEditor from 'react-markdown-editor-lite';
4+
import 'react-markdown-editor-lite/lib/index.css';
5+
6+
import * as styles from './index.less';
7+
8+
const mdParser = new MarkdownIt();
9+
10+
const Markdown: React.FC<Props> = (props) => {
11+
const { value, type, onChange } = props;
12+
13+
let view;
14+
15+
if (type === 'render') {
16+
view = {
17+
menu: false,
18+
md: false,
19+
html: true,
20+
};
21+
}
22+
23+
if (type === 'editor') {
24+
view = {
25+
menu: true,
26+
md: true,
27+
html: false,
28+
};
29+
}
30+
31+
return (
32+
<MdEditor
33+
className={styles.editor}
34+
readOnly={type === 'render'}
35+
view={view}
36+
value={value}
37+
renderHTML={(text) => mdParser.render(text)}
38+
onChange={(data) => {
39+
onChange && onChange(data.text);
40+
}}
41+
/>
42+
);
43+
};
44+
45+
export default Markdown;
46+
47+
interface Props {
48+
type: 'editor' | 'render';
49+
value: string;
50+
onChange?: (text: string) => void;
51+
}

src/layout.tsx

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React from 'react';
22
import { Link, history } from 'umi';
3-
import { Avatar, Tooltip } from 'antd';
3+
import { Avatar, Tooltip, Button } from 'antd';
44

55
import {
66
DefaultFooter,
@@ -17,7 +17,18 @@ const RightContent: React.FC<{
1717
const user = props?.user;
1818

1919
if (!user) {
20-
return null;
20+
return (
21+
<div className="cnode-header-right">
22+
<Button
23+
type="link"
24+
onClick={() => {
25+
history.push('/auth');
26+
}}
27+
>
28+
登录
29+
</Button>
30+
</div>
31+
);
2132
}
2233

2334
const { loginname, avatar_url } = user;
@@ -36,8 +47,8 @@ const layoutConfig = ({
3647
initialState: InitialState;
3748
}): BasicLayoutProps => {
3849
const { title, logo, description } = config;
50+
3951
return {
40-
// common
4152
navTheme: 'light',
4253
layout: 'top',
4354
headerHeight: 64,
@@ -47,15 +58,10 @@ const layoutConfig = ({
4758
logo,
4859
title,
4960

50-
// waterMarkProps: {
51-
// content: config.title,
52-
// },
53-
5461
menuHeaderRender: () => {
5562
return <Brand title={title} description={description} logo={logo} />;
5663
},
5764

58-
// heander
5965
menuDataRender: (menuData: MenuDataItem[]) => {
6066
let menus: MenuDataItem[] = [];
6167
const apps: MenuDataItem[] = [];
@@ -83,29 +89,16 @@ const layoutConfig = ({
8389
menuItemRender: (item) =>
8490
item.path && <Link to={item.path}>{item.name}</Link>,
8591

86-
// right
8792
rightContentRender: () => {
8893
return <RightContent user={initialState.user} />;
8994
},
9095

91-
// footer
9296
footerRender: () => (
9397
<DefaultFooter
9498
links={false}
9599
copyright={`${new Date().getFullYear()} - CNodejs.org`}
96100
/>
97101
),
98-
99-
onPageChange: () => {
100-
const { user, token } = initialState || {};
101-
102-
// 非登录页面
103-
if (history.location.pathname !== '/auth') {
104-
if (!user || !token) {
105-
history.push('/auth');
106-
}
107-
}
108-
},
109102
};
110103
};
111104

src/layout/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,9 @@ const Layout: React.FC<React.PropsWithChildren<Props>> = (props) => {
6262
<ProCard gutter={16} bordered={false} ghost>
6363
<ProCard bordered={false}>{props.children}</ProCard>
6464
<ProCard
65-
title=""
6665
layout="center"
6766
bordered={false}
67+
ghost
6868
colSpan={{
6969
xs: '50px',
7070
sm: '100px',
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import React, { useState, useEffect } from 'react';
2+
import { Comment, Avatar, Button } from 'antd';
3+
import Markdown from '@/component/Markdown';
4+
5+
const CommentForm: React.FC<Props> = (props) => {
6+
const { data, user, onSubmit, onSubmitText = '提交评论' } = props;
7+
const { loginname, avatar_url } = user;
8+
const [value, setValue] = useState('');
9+
10+
useEffect(() => {
11+
if (!data) {
12+
return;
13+
}
14+
setValue(data);
15+
}, [data]);
16+
17+
return (
18+
<Comment
19+
author={<span>{loginname}</span>}
20+
avatar={<Avatar src={avatar_url} alt={loginname} />}
21+
actions={[
22+
<Button
23+
type="primary"
24+
size="small"
25+
onClick={async () => {
26+
if (!value) {
27+
return;
28+
}
29+
30+
await onSubmit(value);
31+
}}
32+
>
33+
{onSubmitText}
34+
</Button>,
35+
]}
36+
content={<Markdown type="editor" value={value} onChange={setValue} />}
37+
/>
38+
);
39+
};
40+
41+
export default CommentForm;
42+
43+
interface Props {
44+
data?: string;
45+
user: {
46+
loginname: string;
47+
avatar_url: string;
48+
};
49+
onSubmit: (value: string) => Promise<void>;
50+
onSubmitText?: string;
51+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
.list {
2+
img {
3+
max-width: 100%;
4+
overflow: hidden;
5+
}
6+
}
7+
8+
.detail {
9+
* {
10+
overflow: hidden;
11+
}
12+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import React, { Fragment } from 'react';
2+
import dayjs from 'dayjs';
3+
import Markdown from '@/component/Markdown';
4+
5+
import { Comment, Avatar, Divider } from 'antd';
6+
import {
7+
LikeFilled,
8+
EditFilled,
9+
DeleteFilled,
10+
CommentOutlined,
11+
} from '@ant-design/icons';
12+
13+
import * as styles from './index.less';
14+
15+
const unflatten = (array: Node[], parent?: Node, tree?: Node[]) => {
16+
let _parent = parent || { id: null, children: [] };
17+
let _tree = tree || [];
18+
19+
const children = array.filter((child) => child.reply_id === _parent.id);
20+
21+
if (children.length > 0) {
22+
if (!_parent.id) {
23+
_tree = children;
24+
} else {
25+
_parent.children = children;
26+
}
27+
children.forEach((child) => unflatten(array, child));
28+
}
29+
30+
return _tree;
31+
};
32+
33+
const CommentList: React.FC<Props> = (props) => {
34+
const { list, onReply, replyRender } = props;
35+
const tree = unflatten(list);
36+
37+
const ComentDetail: React.FC<{
38+
data: Node;
39+
}> = ({ data }) => {
40+
const { id, author, content, create_at, children } = data;
41+
42+
return (
43+
<Fragment key={`fragment-${id}`}>
44+
<Divider type="horizontal" key={`divider-${id}`} />
45+
46+
<Comment
47+
key={id}
48+
actions={[
49+
<LikeFilled />,
50+
<EditFilled />,
51+
<DeleteFilled />,
52+
<CommentOutlined
53+
onClick={() => {
54+
onReply(data);
55+
}}
56+
/>,
57+
]}
58+
author={<span>{author.loginname}</span>}
59+
datetime={
60+
<span>{dayjs(create_at).format('YYYY-MM-DD hh:mm:ss')}</span>
61+
}
62+
avatar={<Avatar src={author.avatar_url} alt={author.loginname} />}
63+
content={
64+
<div className={styles.detail}>
65+
<Markdown type="render" value={content} />
66+
</div>
67+
}
68+
>
69+
{replyRender(id)}
70+
71+
{children?.map((item) => (
72+
<ComentDetail key={`detail-${id}-${item.id}`} data={item} />
73+
))}
74+
</Comment>
75+
</Fragment>
76+
);
77+
};
78+
79+
return (
80+
<div className={styles.list}>
81+
{tree.map((item) => (
82+
<ComentDetail key={`list-${item.id}`} data={item} />
83+
))}
84+
</div>
85+
);
86+
};
87+
88+
export default CommentList;
89+
90+
interface Props {
91+
list: ReplyModel[];
92+
93+
onReply: (record: Node) => void;
94+
replyRender: (id: string) => React.ReactNode;
95+
}
96+
97+
interface Node extends ReplyModel {
98+
children?: Node[];
99+
}

0 commit comments

Comments
 (0)