Sfoglia il codice sorgente

feat: 将通用评论中的编辑器等拆分为组件,并预留 props 供使用者自定义功能

node 5 anni fa
parent
commit
adb03f746a

+ 2
- 1
package.json Vedi File

17
     "antd": "^3.6.6",
17
     "antd": "^3.6.6",
18
     "axios": "^0.18.0",
18
     "axios": "^0.18.0",
19
     "dayjs": "^1.7.2",
19
     "dayjs": "^1.7.2",
20
+    "prop-types": "^15.6.2",
20
     "react": "^16.4.1",
21
     "react": "^16.4.1",
21
     "react-dom": "^16.4.1",
22
     "react-dom": "^16.4.1",
22
     "react-scripts": "1.1.4",
23
     "react-scripts": "1.1.4",
30
   },
31
   },
31
   "scripts": {
32
   "scripts": {
32
     "precommit": "lint-staged",
33
     "precommit": "lint-staged",
33
-    "prettier": "prettier --write src/**/*.{js,jsx,json,css,less}",
34
+    "prettier": "prettier --trailing-comma --write 'src/**/*.{js,jsx,json,css,less}'",
34
     "start": "react-app-rewired start",
35
     "start": "react-app-rewired start",
35
     "build": "react-app-rewired build",
36
     "build": "react-app-rewired build",
36
     "test": "react-app-rewired test --env=jsdom",
37
     "test": "react-app-rewired test --env=jsdom",

+ 52
- 30
src/App.js Vedi File

1
 import React, { Component } from "react";
1
 import React, { Component } from "react";
2
+import PropTypes from "prop-types";
2
 import { message } from "antd";
3
 import { message } from "antd";
3
 import axios from "./axios";
4
 import axios from "./axios";
4
-import { URL, ERROR_DEFAULT, LIMIT } from "./constant";
5
+import { ERROR_DEFAULT, LIMIT } from "./constant";
5
 import { CommentContext } from "./Comment";
6
 import { CommentContext } from "./Comment";
6
 import { isFunction } from "./helper";
7
 import { isFunction } from "./helper";
7
 import CommentInput from "./components/CommentInput";
8
 import CommentInput from "./components/CommentInput";
8
 import CommentList from "./components/CommentList";
9
 import CommentList from "./components/CommentList";
9
-// import * as mock from "./mock";
10
+import Editor from "./components/Editor";
10
 import "./App.css";
11
 import "./App.css";
11
 
12
 
12
 class App extends Component {
13
 class App extends Component {
34
     this.sOssSts = this.sOssSts.bind(this);
35
     this.sOssSts = this.sOssSts.bind(this);
35
   }
36
   }
36
 
37
 
37
-  componentDidMount() {
38
-    this.sGetComment();
39
-  }
38
+  componentDidMount() {}
40
 
39
 
41
   /**
40
   /**
42
    * 改变 loading 状态
41
    * 改变 loading 状态
52
   /**
51
   /**
53
    * 获取评论列表
52
    * 获取评论列表
54
    */
53
    */
55
-  sGetComment({ type = 1, businessId = 1, page = 1 } = {}) {
54
+  sGetComment({ page = 1 } = {}) {
56
     this.handleChangeLoading("sGetComment", true);
55
     this.handleChangeLoading("sGetComment", true);
57
-    // 测试数据列表
58
-    // const { comments } = mock;
59
-    // this.setState({
60
-    //   list: comments.list,
61
-    //   page: 1,
62
-    //   total: 100
63
-    // });
64
-    // this.handleChangeLoading("sGetComment", false);
65
-    // return;
56
+    const { API, type, businessId } = this.props;
66
     axios
57
     axios
67
       .get(
58
       .get(
68
-        `${URL}/comments?type=${type}&business_id=${businessId}&page=${page}&limit=${LIMIT}`
59
+        `${API}/comments?type=${type}&business_id=${businessId}&page=${page}&limit=${LIMIT}`
69
       )
60
       )
70
       .then(response => {
61
       .then(response => {
71
         const { list, page, total } = response.data;
62
         const { list, page, total } = response.data;
106
    */
97
    */
107
   sGetReply({ commentId, page = 1 } = {}) {
98
   sGetReply({ commentId, page = 1 } = {}) {
108
     this.handleChangeLoading("sGetReply", true);
99
     this.handleChangeLoading("sGetReply", true);
100
+    const { API } = this.props;
109
     axios
101
     axios
110
-      .get(`${URL}/replies?comment_id=${commentId}&page=${page}&limit=${LIMIT}`)
102
+      .get(`${API}/replies?comment_id=${commentId}&page=${page}&limit=${LIMIT}`)
111
       .then(response => {
103
       .then(response => {
112
         const { list: replies } = response.data;
104
         const { list: replies } = response.data;
113
         if (!replies) {
105
         if (!replies) {
147
 
139
 
148
   /**
140
   /**
149
    * 添加评论
141
    * 添加评论
150
-   * @param {object} data { type, business_id, content }
142
+   * @param {string} content comment content
151
    */
143
    */
152
-  sCreateComment(data) {
153
-    if (!data.content) return message.error("评论内容不能为空 ");
144
+  sCreateComment(content) {
145
+    if (!content) return message.error("评论内容不能为空 ");
154
     this.handleChangeLoading("sCreateComment", true);
146
     this.handleChangeLoading("sCreateComment", true);
155
-    axios(`${URL}/comments`, {
147
+    const { API, type, businessId } = this.props;
148
+    axios(`${API}/comments`, {
156
       method: "post",
149
       method: "post",
157
-      data,
150
+      data: {
151
+        type,
152
+        business_id: businessId,
153
+        content
154
+      },
158
       withCredentials: true
155
       withCredentials: true
159
     })
156
     })
160
       .then(response => {
157
       .then(response => {
189
   sCreateReply(data, cb) {
186
   sCreateReply(data, cb) {
190
     if (!data.content) return message.error("回复内容不能为空 ");
187
     if (!data.content) return message.error("回复内容不能为空 ");
191
     this.handleChangeLoading("sCreateReply", true);
188
     this.handleChangeLoading("sCreateReply", true);
192
-    axios(`${URL}/replies`, {
189
+    const { API } = this.props;
190
+    axios(`${API}/replies`, {
193
       method: "post",
191
       method: "post",
194
       data,
192
       data,
195
       withCredentials: true
193
       withCredentials: true
196
     })
194
     })
197
       .then(response => {
195
       .then(response => {
198
-        // console.log("response: ", response.data);
199
         // // 将该条数据插入到 list 中
196
         // // 将该条数据插入到 list 中
200
         // const list = this.state.list.map(item => {
197
         // const list = this.state.list.map(item => {
201
         //   if (item.id === data.comment_id) {
198
         //   if (item.id === data.comment_id) {
229
    */
226
    */
230
   sCommentFavor(commentId, favored) {
227
   sCommentFavor(commentId, favored) {
231
     this.handleChangeLoading("sCommentFavor", true);
228
     this.handleChangeLoading("sCommentFavor", true);
232
-    axios(`${URL}/comments/${commentId}/favor`, {
229
+    const { API } = this.props;
230
+    axios(`${API}/comments/${commentId}/favor`, {
233
       method: favored ? "delete" : "put",
231
       method: favored ? "delete" : "put",
234
       withCredentials: true
232
       withCredentials: true
235
     })
233
     })
262
    */
260
    */
263
   sOssSts() {
261
   sOssSts() {
264
     this.handleChangeLoading("sOssSts", true);
262
     this.handleChangeLoading("sOssSts", true);
263
+    const { API } = this.props;
265
     axios
264
     axios
266
-      .get(`${URL}/oss/sts`)
265
+      .get(`${API}/oss/sts`)
267
       .then(response => {
266
       .then(response => {
268
         this.setState({ oss: { ...response.data } });
267
         this.setState({ oss: { ...response.data } });
269
       })
268
       })
283
     // 添加到 Context 的数据
282
     // 添加到 Context 的数据
284
     const value = {
283
     const value = {
285
       ...this.state,
284
       ...this.state,
285
+      ...this.props,
286
       sCreateComment: this.sCreateComment,
286
       sCreateComment: this.sCreateComment,
287
       sGetComment: this.sGetComment,
287
       sGetComment: this.sGetComment,
288
       sCommentFavor: this.sCommentFavor,
288
       sCommentFavor: this.sCommentFavor,
294
     return (
294
     return (
295
       <CommentContext.Provider value={value}>
295
       <CommentContext.Provider value={value}>
296
         <div className="comment">
296
         <div className="comment">
297
-          <CommentInput />
298
-          <div style={{ marginTop: 20 }}>
299
-            <CommentList />
300
-          </div>
297
+          {this.props.showEditor && (
298
+            <CommentInput content={this.props.children} />
299
+          )}
300
+          {this.props.showList && (
301
+            <div style={{ marginTop: 20 }}>
302
+              <CommentList />
303
+            </div>
304
+          )}
301
         </div>
305
         </div>
302
       </CommentContext.Provider>
306
       </CommentContext.Provider>
303
     );
307
     );
304
   }
308
   }
305
 }
309
 }
306
 
310
 
311
+App.propTypes = {
312
+  type: PropTypes.number.isRequired, // 评论的 type
313
+  businessId: PropTypes.string.isRequired, // 评论的 business_id
314
+  API: PropTypes.string, // 评论的 API 前缀
315
+  showList: PropTypes.bool, // 是否显示评论列表
316
+  showEditor: PropTypes.bool // 是否显示评论输入框
317
+  // editor: PropTypes.node, // 评论编辑器
318
+};
319
+
320
+App.defaultProps = {
321
+  API: "http://api.links123.net/comment/v1",
322
+  showList: true,
323
+  showEditor: true
324
+  // editor: <span>a</span>,
325
+};
326
+
327
+export { Editor };
328
+
307
 export default App;
329
 export default App;

+ 7
- 0
src/CHANGELOG.md Vedi File

1
+# CHANGELOG
2
+
3
+
4
+## 0.1.0
5
+
6
+
7
+

+ 31
- 23
src/components/CommentInput/index.js Vedi File

2
 import PropTypes from "prop-types";
2
 import PropTypes from "prop-types";
3
 import { OSS_LINK } from "../../constant";
3
 import { OSS_LINK } from "../../constant";
4
 import Comment from "../../Comment";
4
 import Comment from "../../Comment";
5
-import Editor from "../Editor";
6
-
7
-const PLACEHOLDER = {
8
-  normal: "说点什么吧...",
9
-  default: "说点什么吧..."
10
-};
11
 
5
 
12
 class CommentInput extends Component {
6
 class CommentInput extends Component {
13
   constructor(props) {
7
   constructor(props) {
25
     this.handleUpload = this.handleUpload.bind(this);
19
     this.handleUpload = this.handleUpload.bind(this);
26
   }
20
   }
27
 
21
 
28
-  handleChange(e) {
29
-    this.setState({ value: e.target.value });
22
+  handleChange(value) {
23
+    this.setState({ value });
30
   }
24
   }
31
 
25
 
32
   handleChangeFileList(fileList) {
26
   handleChangeFileList(fileList) {
37
     let { value } = this.state;
31
     let { value } = this.state;
38
     value += `[${emojiId}]`;
32
     value += `[${emojiId}]`;
39
     this.setState({ value });
33
     this.setState({ value });
34
+    React.Children.forEach(this.props.content, child => {
35
+      // 如果 Editor 的父组件传入了 onChange 事件,则需要将改变之后的值传递给父组件
36
+      if (child.props.onChange) {
37
+        child.props.onChange(value);
38
+      }
39
+    });
40
   }
40
   }
41
 
41
 
42
   handleUpload({ uid, path }) {
42
   handleUpload({ uid, path }) {
48
   handleSubmit() {
48
   handleSubmit() {
49
     let { value, fileMap, fileList } = this.state;
49
     let { value, fileMap, fileList } = this.state;
50
     if (fileList.length) {
50
     if (fileList.length) {
51
+      value += "<br/>";
51
       fileList.forEach(item => {
52
       fileList.forEach(item => {
52
         value += `[${OSS_LINK}${fileMap[item.uid]}]`;
53
         value += `[${OSS_LINK}${fileMap[item.uid]}]`;
53
       });
54
       });
56
     const { type, commentId, replyId, handleToggleInput } = this.props;
57
     const { type, commentId, replyId, handleToggleInput } = this.props;
57
     if (type === "normal") {
58
     if (type === "normal") {
58
       this.props.app.sCreateComment({
59
       this.props.app.sCreateComment({
59
-        type: 1,
60
-        business_id: "1",
61
         content: value
60
         content: value
62
       });
61
       });
63
     } else if (type === "comment") {
62
     } else if (type === "comment") {
78
         () => handleToggleInput()
77
         () => handleToggleInput()
79
       );
78
       );
80
     }
79
     }
80
+
81
+    React.Children.forEach(this.props.content, child => {
82
+      // 如果 Editor 的父组件传入了 onSubmit 事件,则需要将改变之后的值传递给父组件
83
+      if (child.props.onSubmit) {
84
+        child.props.onSubmit(value);
85
+      }
86
+    });
81
   }
87
   }
82
 
88
 
83
   render() {
89
   render() {
84
     const { type } = this.props;
90
     const { type } = this.props;
85
     const { value, fileList } = this.state;
91
     const { value, fileList } = this.state;
86
 
92
 
93
+    const childrenWithProps = React.Children.map(this.props.content, child => {
94
+      return React.cloneElement(child, {
95
+        value: value,
96
+        fileList: fileList,
97
+        onChange: this.handleChange,
98
+        onSubmit: this.handleSubmit,
99
+        onChangeFileList: this.handleChangeFileList,
100
+        onChangeEmoji: this.handleChangeEmoji,
101
+        onUpload: this.handleUpload,
102
+        loading: this.props.app.loading.sCreateComment,
103
+        ...child.props
104
+      });
105
+    });
106
+
87
     return (
107
     return (
88
       <div>
108
       <div>
89
         {type === "normal" ? (
109
         {type === "normal" ? (
105
             </span>
125
             </span>
106
           </div>
126
           </div>
107
         ) : null}
127
         ) : null}
108
-        <div style={{ marginTop: 40 }}>
109
-          <Editor
110
-            value={value}
111
-            placeholder={PLACEHOLDER[type] || PLACEHOLDER.default}
112
-            fileList={fileList}
113
-            onChange={this.handleChange}
114
-            onSubmit={this.handleSubmit}
115
-            onChangeFileList={this.handleChangeFileList}
116
-            onChangeEmoji={this.handleChangeEmoji}
117
-            onUpload={this.handleUpload}
118
-            loading={this.props.app.loading.sCreateComment}
119
-          />
120
-        </div>
128
+        <div style={{ marginTop: 40 }}>{childrenWithProps}</div>
121
       </div>
129
       </div>
122
     );
130
     );
123
   }
131
   }

+ 4
- 0
src/components/CommentList/index.js Vedi File

10
     this.state = {};
10
     this.state = {};
11
   }
11
   }
12
 
12
 
13
+  componentWillMount() {
14
+    this.props.app.sGetComment();
15
+  }
16
+
13
   render() {
17
   render() {
14
     const {
18
     const {
15
       list,
19
       list,

+ 23
- 0
src/components/Editor.1/Emoji.css Vedi File

1
+.item {
2
+  float: left;
3
+  width: 40px;
4
+  height: 40px;
5
+  cursor: pointer;
6
+  white-space: nowrap;
7
+  /* this is required unless you put the helper span closely near the img */
8
+  text-align: center;
9
+  margin: 0;
10
+}
11
+.item .helper {
12
+  display: inline-block;
13
+  height: 100%;
14
+  vertical-align: middle;
15
+}
16
+.item img {
17
+  margin: 0 auto;
18
+  vertical-align: middle;
19
+  padding: 3px;
20
+}
21
+.item img:hover {
22
+  border: 1px solid #40a9ff;
23
+}

+ 65
- 0
src/components/Editor.1/Emoji.js Vedi File

1
+import React from "react";
2
+import { Carousel } from "antd";
3
+import emoji, { prefixUrl, ext } from "../../emoji";
4
+import "./Emoji.css";
5
+// 每页 20  5*4
6
+// 共 20 * 3 = 60 (实际是 54)
7
+
8
+const Emoji = ({ onClick }) => {
9
+  const content = [[], [], []];
10
+
11
+  for (let i = 0; i < emoji.length; i++) {
12
+    if (i < 20) {
13
+      content[0].push(emoji[i]);
14
+    } else if (i < 40) {
15
+      content[1].push(emoji[i]);
16
+    } else if (i < emoji.length) {
17
+      content[2].push(emoji[i]);
18
+    }
19
+  }
20
+  return (
21
+    <Carousel>
22
+      <div>
23
+        {content[0].map((item, index) => (
24
+          <div className="item" key={item.value}>
25
+            <span className="helper" />
26
+            <img
27
+              src={`${prefixUrl}${item.value}.${ext}`}
28
+              alt={item.title}
29
+              style={{ display: "inline-block" }}
30
+              onClick={() => onClick(item.title)}
31
+            />
32
+          </div>
33
+        ))}
34
+      </div>
35
+      <div>
36
+        {content[1].map((item, index) => (
37
+          <div className="item" key={item.value}>
38
+            <span className="helper" />
39
+            <img
40
+              src={`${prefixUrl}${item.value}.${ext}`}
41
+              alt={item.title}
42
+              style={{ display: "inline-block" }}
43
+              onClick={() => onClick(item.title)}
44
+            />
45
+          </div>
46
+        ))}
47
+      </div>
48
+      <div>
49
+        {content[2].map(item => (
50
+          <div className="item" key={item.value}>
51
+            <span className="helper" />
52
+            <img
53
+              src={`${prefixUrl}${item.value}.${ext}`}
54
+              alt={item.title}
55
+              style={{ display: "inline-block" }}
56
+              onClick={() => onClick(item.title)}
57
+            />
58
+          </div>
59
+        ))}
60
+      </div>
61
+    </Carousel>
62
+  );
63
+};
64
+
65
+export default Emoji;

+ 8
- 0
src/components/Editor.1/Upload.css Vedi File

1
+.ant-upload-select-picture-card i {
2
+  font-size: 32px;
3
+  color: #999;
4
+}
5
+.ant-upload-select-picture-card .ant-upload-text {
6
+  margin-top: 8px;
7
+  color: #666;
8
+}

+ 132
- 0
src/components/Editor.1/Upload.js Vedi File

1
+import React from "react";
2
+import { Upload, Icon, Modal, message } from "antd";
3
+import dayjs from "dayjs";
4
+import shortid from "shortid";
5
+import {
6
+  OSS_ENDPOINT,
7
+  OSS_BUCKET,
8
+  DRIVER_LICENSE_PATH,
9
+  ERROR_DEFAULT,
10
+  MAX_UPLOAD_NUMBER
11
+} from "../../constant";
12
+import Comment from "../../Comment";
13
+// import styles from "./Upload.less";
14
+import "./Upload.css";
15
+
16
+const client = oss => {
17
+  return new window.OSS.Wrapper({
18
+    accessKeyId: oss.access_key_id,
19
+    accessKeySecret: oss.access_key_secret,
20
+    stsToken: oss.security_token,
21
+    endpoint: OSS_ENDPOINT, //常量,你可以自己定义
22
+    bucket: OSS_BUCKET
23
+  });
24
+};
25
+
26
+const uploadPath = (path, file) => {
27
+  return `${path}/${dayjs().format("YYYYMMDD")}/${shortid.generate()}.${
28
+    file.type.split("/")[1]
29
+  }`;
30
+};
31
+
32
+const UploadToOss = (oss, path, file) => {
33
+  const url = uploadPath(path, file);
34
+  return new Promise((resolve, reject) => {
35
+    client(oss)
36
+      .multipartUpload(url, file)
37
+      .then(data => {
38
+        resolve(data);
39
+      })
40
+      .catch(error => {
41
+        reject(error);
42
+      });
43
+  });
44
+};
45
+
46
+class App extends React.Component {
47
+  constructor(props) {
48
+    super(props);
49
+    this.state = {
50
+      previewVisible: false,
51
+      previewImage: ""
52
+    };
53
+    this.handleCancel = this.handleCancel.bind(this);
54
+    this.handlePreview = this.handlePreview.bind(this);
55
+    this.handleChange = this.handleChange.bind(this);
56
+    this.customRequest = this.customRequest.bind(this);
57
+  }
58
+
59
+  componentDidMount() {
60
+    this.props.app.sOssSts();
61
+  }
62
+
63
+  handleCancel() {
64
+    this.setState({ previewVisible: false });
65
+  }
66
+
67
+  handlePreview(file) {
68
+    this.setState({
69
+      previewImage: file.url || file.thumbUrl,
70
+      previewVisible: true
71
+    });
72
+  }
73
+
74
+  handleChange({ fileList }) {
75
+    this.props.onChangeFileList(fileList);
76
+  }
77
+
78
+  customRequest(info) {
79
+    const { file } = info;
80
+    info.onProgress({ percent: 10 });
81
+    let reader = new FileReader();
82
+    reader.readAsDataURL(info.file);
83
+    reader.onloadend = () => {
84
+      info.onProgress({ percent: 20 });
85
+      // DRIVER_LICENSE_PATH oss 的存储路径位置
86
+      UploadToOss(this.props.app.oss, DRIVER_LICENSE_PATH, file)
87
+        .then(data => {
88
+          info.onProgress({ percent: 100 });
89
+          info.onSuccess();
90
+          this.props.onUpload({ path: data.name, uid: file.uid });
91
+        })
92
+        .catch(e => {
93
+          message.error(e.message || ERROR_DEFAULT);
94
+          info.onError(e);
95
+        });
96
+    };
97
+  }
98
+
99
+  render() {
100
+    const { previewVisible, previewImage } = this.state;
101
+    const { fileList } = this.props;
102
+    const uploadButton = (
103
+      <div>
104
+        <Icon type="plus" />
105
+        <div className="ant-upload-text">上传</div>
106
+      </div>
107
+    );
108
+    return (
109
+      <div>
110
+        <Upload
111
+          accept="image/jpg,image/jpeg,image/png,image/bmp"
112
+          listType="picture-card"
113
+          fileList={fileList}
114
+          customRequest={this.customRequest}
115
+          onPreview={this.handlePreview}
116
+          onChange={this.handleChange}
117
+        >
118
+          {fileList.length >= MAX_UPLOAD_NUMBER ? null : uploadButton}
119
+        </Upload>
120
+        <Modal
121
+          visible={previewVisible}
122
+          footer={null}
123
+          onCancel={this.handleCancel}
124
+        >
125
+          <img alt="upload" style={{ width: "100%" }} src={previewImage} />
126
+        </Modal>
127
+      </div>
128
+    );
129
+  }
130
+}
131
+
132
+export default Comment(App);

+ 82
- 0
src/components/Editor.1/index.css Vedi File

1
+.editor {
2
+  box-sizing: border-box;
3
+  margin: 0;
4
+  padding: 0;
5
+  width: 100%;
6
+  max-width: 100%;
7
+  list-style: none;
8
+  position: relative;
9
+  display: block;
10
+  font-size: 14px;
11
+  line-height: 1.5;
12
+  color: rgba(0, 0, 0, 0.65);
13
+  background-color: #fff;
14
+  background-image: none;
15
+  border: 1px solid #d9d9d9;
16
+  border-radius: 4px;
17
+  transition: all 0.3s, height 0s;
18
+}
19
+.editor textarea.ant-input {
20
+  border: none;
21
+  border-bottom: 1px solid #eee;
22
+  border-bottom-right-radius: 0;
23
+  border-bottom-left-radius: 0;
24
+}
25
+.editor textarea.ant-input:hover {
26
+  border: none;
27
+  border-bottom: 1px solid #eee;
28
+}
29
+.editor textarea.ant-input:focus {
30
+  box-shadow: none;
31
+  border-bottom: 1px solid #eee;
32
+}
33
+.editor [contentEditable="true"]:empty:not(:focus):before {
34
+  content: attr(data-text);
35
+  color: #bfbfbf;
36
+}
37
+.editor:focus,
38
+.editor:hover {
39
+  border-color: #40a9ff;
40
+  outline: 0;
41
+  box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
42
+}
43
+.content {
44
+  display: block;
45
+  box-sizing: border-box;
46
+  padding: 4px 10px;
47
+  width: 100%;
48
+  max-width: 100%;
49
+  height: 100%;
50
+  max-height: 110px;
51
+  overflow-y: auto;
52
+  overflow-x: auto;
53
+}
54
+.content:focus,
55
+.content:hover {
56
+  outline: 0;
57
+}
58
+.toolbar {
59
+  display: inline-block;
60
+  width: 100%;
61
+  margin: 5px 0 0 0;
62
+}
63
+.toolbar .icon {
64
+  font-size: 23px;
65
+  cursor: pointer;
66
+  transition: color 0.3s;
67
+}
68
+.toolbar .icon:hover {
69
+  color: #40a9ff;
70
+}
71
+.feed .ant-popover-inner-content {
72
+  padding: 12px 16px 20px 16px;
73
+}
74
+.feed .ant-carousel .slick-dots {
75
+  bottom: -10px;
76
+}
77
+.feed .ant-carousel .slick-dots li.slick-active button {
78
+  background-color: #7b868a;
79
+}
80
+.feed .ant-carousel .slick-dots li button {
81
+  background-color: #a2aeb5;
82
+}

+ 142
- 0
src/components/Editor.1/index.js Vedi File

1
+import React from "react";
2
+import PropTypes from "prop-types";
3
+import { Icon, Button, Popover, Input } from "antd";
4
+import { MAX_UPLOAD_NUMBER } from "../../constant";
5
+import Upload from "./Upload";
6
+import Emoji from "./Emoji";
7
+import "./index.css";
8
+
9
+const { TextArea } = Input;
10
+
11
+class Editor extends React.Component {
12
+  constructor(props) {
13
+    super(props);
14
+    this.state = {
15
+      showUpload: false
16
+    };
17
+    this.handleClickEmoji = this.handleClickEmoji.bind(this);
18
+    this.handleShowUpload = this.handleShowUpload.bind(this);
19
+  }
20
+
21
+  componentDidMount() {}
22
+
23
+  handleClickEmoji(emojiId) {
24
+    this.props.onChangeEmoji(emojiId);
25
+  }
26
+
27
+  handleShowUpload(showUpload) {
28
+    if (typeof showUpload === "boolean") {
29
+      this.setState({ showUpload: showUpload });
30
+    } else {
31
+      this.setState({ showUpload: !this.state.showUpload });
32
+    }
33
+  }
34
+
35
+  render() {
36
+    const {
37
+      value,
38
+      onChange,
39
+      placeholder,
40
+      fileList,
41
+      onChangeFileList,
42
+      rows,
43
+      onUpload
44
+    } = this.props;
45
+
46
+    return (
47
+      <div className="editor">
48
+        <TextArea
49
+          value={value}
50
+          onChange={onChange}
51
+          rows={rows}
52
+          placeholder={placeholder}
53
+        />
54
+
55
+        <div className="toolbar">
56
+          {false && (
57
+            <div style={{ float: "left", margin: "8px 11px" }}>
58
+              <Popover
59
+                trigger="click"
60
+                placement="bottomLeft"
61
+                autoAdjustOverflow={false}
62
+                content={
63
+                  <div style={{ width: 200 }}>
64
+                    <Emoji onClick={this.handleClickEmoji} />
65
+                  </div>
66
+                }
67
+                overlayClassName="feed"
68
+              >
69
+                <Icon type="smile-o" className="icon" />
70
+              </Popover>
71
+
72
+              <Popover
73
+                visible={this.state.showUpload}
74
+                overlayStyle={{ zIndex: 999 }}
75
+                content={
76
+                  <div
77
+                    style={{ width: 112 * MAX_UPLOAD_NUMBER, minHeight: 100 }}
78
+                  >
79
+                    <Upload
80
+                      onChangeFileList={onChangeFileList}
81
+                      onUpload={onUpload}
82
+                      fileList={fileList}
83
+                    />
84
+                  </div>
85
+                }
86
+                placement="bottomLeft"
87
+                title={
88
+                  <div style={{ margin: "5px auto" }}>
89
+                    <span>
90
+                      上传图片
91
+                      <span style={{ color: "#666", fontWeight: 400 }}>
92
+                        (您还能上传{MAX_UPLOAD_NUMBER - fileList.length}张图片)
93
+                      </span>
94
+                    </span>
95
+                    <Icon
96
+                      type="close"
97
+                      onClick={() => this.handleShowUpload(false)}
98
+                      style={{
99
+                        float: "right",
100
+                        cursor: "pointer",
101
+                        marginTop: 4
102
+                      }}
103
+                    />
104
+                  </div>
105
+                }
106
+              >
107
+                <Icon
108
+                  type="picture"
109
+                  className="icon"
110
+                  style={{ marginLeft: 10 }}
111
+                  onClick={() => this.handleShowUpload(true)}
112
+                />
113
+              </Popover>
114
+            </div>
115
+          )}
116
+
117
+          <div style={{ float: "right", margin: "5px 11px" }}>
118
+            <Button
119
+              onClick={this.props.onSubmit}
120
+              type="primary"
121
+              loading={this.props.loading}
122
+            >
123
+              发表
124
+            </Button>
125
+          </div>
126
+        </div>
127
+      </div>
128
+    );
129
+  }
130
+}
131
+
132
+Editor.propTypes = {
133
+  rows: PropTypes.number,
134
+  placeholder: PropTypes.string
135
+};
136
+
137
+Editor.defaultProps = {
138
+  rows: 5,
139
+  placeholder: "说点什么吧..."
140
+};
141
+
142
+export default Editor;

+ 2
- 0
src/components/Editor/Emoji.js Vedi File

5
 // 每页 20  5*4
5
 // 每页 20  5*4
6
 // 共 20 * 3 = 60 (实际是 54)
6
 // 共 20 * 3 = 60 (实际是 54)
7
 
7
 
8
+// class Emoji
9
+
8
 const Emoji = ({ onClick }) => {
10
 const Emoji = ({ onClick }) => {
9
   const content = [[], [], []];
11
   const content = [[], [], []];
10
 
12
 

+ 97
- 65
src/components/Editor/index.js Vedi File

1
-// https://github.com/lovasoa/react-contenteditable/blob/master/src/react-contenteditable.js
2
 import React from "react";
1
 import React from "react";
2
+import PropTypes from "prop-types";
3
 import { Icon, Button, Popover, Input } from "antd";
3
 import { Icon, Button, Popover, Input } from "antd";
4
 import { MAX_UPLOAD_NUMBER } from "../../constant";
4
 import { MAX_UPLOAD_NUMBER } from "../../constant";
5
 import Upload from "./Upload";
5
 import Upload from "./Upload";
6
 import Emoji from "./Emoji";
6
 import Emoji from "./Emoji";
7
-// import styles from "./index.less";
8
 import "./index.css";
7
 import "./index.css";
9
 
8
 
10
 const { TextArea } = Input;
9
 const { TextArea } = Input;
10
+// 设置 Editor 组件的默认值
11
+// 不能在 Editor.defaultProps 中设置
12
+// 因为 Editor 在 ComponentInput 中调用
13
+// 在 ComponentInput 中,需要使用 Editor 的 props 覆盖 ComponentInput 传入的 props
14
+const EditorDefaultProps = {
15
+  rows: 5,
16
+  placeholder: "说点什么吧...",
17
+  showEmoji: true,
18
+  showUpload: true,
19
+  submitText: "发表",
20
+  onChange: () => {}
21
+};
11
 
22
 
12
-export default class Editor extends React.Component {
23
+class Editor extends React.Component {
13
   constructor(props) {
24
   constructor(props) {
14
     super(props);
25
     super(props);
15
     this.state = {
26
     this.state = {
34
   }
45
   }
35
 
46
 
36
   render() {
47
   render() {
48
+    const props = { ...EditorDefaultProps, ...this.props };
37
     const {
49
     const {
38
       value,
50
       value,
39
       onChange,
51
       onChange,
52
+      onSubmit,
53
+      loading,
40
       placeholder,
54
       placeholder,
41
       fileList,
55
       fileList,
42
       onChangeFileList,
56
       onChangeFileList,
43
-      onUpload
44
-    } = this.props;
57
+      rows,
58
+      onUpload,
59
+      showEmoji,
60
+      showUpload,
61
+      submitText
62
+    } = props;
63
+
45
     return (
64
     return (
46
       <div className="editor">
65
       <div className="editor">
47
         <TextArea
66
         <TextArea
48
           value={value}
67
           value={value}
49
-          onChange={onChange}
50
-          rows={5}
68
+          onChange={e => onChange(e.target.value)}
69
+          rows={rows}
51
           placeholder={placeholder}
70
           placeholder={placeholder}
52
         />
71
         />
53
 
72
 
54
         <div className="toolbar">
73
         <div className="toolbar">
55
           <div style={{ float: "left", margin: "8px 11px" }}>
74
           <div style={{ float: "left", margin: "8px 11px" }}>
56
-            <Popover
57
-              trigger="click"
58
-              placement="bottomLeft"
59
-              autoAdjustOverflow={false}
60
-              content={
61
-                <div style={{ width: 200 }}>
62
-                  <Emoji onClick={this.handleClickEmoji} />
63
-                </div>
64
-              }
65
-              overlayClassName="feed"
66
-            >
67
-              <Icon type="smile-o" className="icon" />
68
-            </Popover>
75
+            {showEmoji && (
76
+              <Popover
77
+                trigger="click"
78
+                placement="bottomLeft"
79
+                autoAdjustOverflow={false}
80
+                content={
81
+                  <div style={{ width: 200 }}>
82
+                    <Emoji onClick={this.handleClickEmoji} />
83
+                  </div>
84
+                }
85
+                overlayClassName="feed"
86
+              >
87
+                <Icon type="smile-o" className="icon" />
88
+              </Popover>
89
+            )}
69
 
90
 
70
-            <Popover
71
-              visible={this.state.showUpload}
72
-              overlayStyle={{ zIndex: 999 }}
73
-              content={
74
-                <div style={{ width: 112 * MAX_UPLOAD_NUMBER, minHeight: 100 }}>
75
-                  <Upload
76
-                    onChangeFileList={onChangeFileList}
77
-                    onUpload={onUpload}
78
-                    fileList={fileList}
79
-                  />
80
-                </div>
81
-              }
82
-              placement="bottomLeft"
83
-              title={
84
-                <div style={{ margin: "5px auto" }}>
85
-                  <span>
86
-                    上传图片
87
-                    <span style={{ color: "#666", fontWeight: 400 }}>
88
-                      (您还能上传{MAX_UPLOAD_NUMBER - fileList.length}张图片)
91
+            {showUpload && (
92
+              <Popover
93
+                visible={this.state.showUpload}
94
+                overlayStyle={{ zIndex: 999 }}
95
+                content={
96
+                  <div
97
+                    style={{ width: 112 * MAX_UPLOAD_NUMBER, minHeight: 100 }}
98
+                  >
99
+                    <Upload
100
+                      onChangeFileList={onChangeFileList}
101
+                      onUpload={onUpload}
102
+                      fileList={fileList}
103
+                    />
104
+                  </div>
105
+                }
106
+                placement="bottomLeft"
107
+                title={
108
+                  <div style={{ margin: "5px auto" }}>
109
+                    <span>
110
+                      上传图片
111
+                      <span style={{ color: "#666", fontWeight: 400 }}>
112
+                        (您还能上传{MAX_UPLOAD_NUMBER - fileList.length}张图片)
113
+                      </span>
89
                     </span>
114
                     </span>
90
-                  </span>
91
-                  <Icon
92
-                    type="close"
93
-                    onClick={() => this.handleShowUpload(false)}
94
-                    style={{
95
-                      float: "right",
96
-                      cursor: "pointer",
97
-                      marginTop: 4
98
-                    }}
99
-                  />
100
-                </div>
101
-              }
102
-            >
103
-              <Icon
104
-                type="picture"
105
-                className="icon"
106
-                style={{ marginLeft: 10 }}
107
-                onClick={() => this.handleShowUpload(true)}
108
-              />
109
-            </Popover>
115
+                    <Icon
116
+                      type="close"
117
+                      onClick={() => this.handleShowUpload(false)}
118
+                      style={{
119
+                        float: "right",
120
+                        cursor: "pointer",
121
+                        marginTop: 4
122
+                      }}
123
+                    />
124
+                  </div>
125
+                }
126
+              >
127
+                <Icon
128
+                  type="picture"
129
+                  className="icon"
130
+                  style={{ marginLeft: 10 }}
131
+                  onClick={() => this.handleShowUpload(true)}
132
+                />
133
+              </Popover>
134
+            )}
110
           </div>
135
           </div>
111
 
136
 
112
           <div style={{ float: "right", margin: "5px 11px" }}>
137
           <div style={{ float: "right", margin: "5px 11px" }}>
113
-            <Button
114
-              onClick={this.props.onSubmit}
115
-              type="primary"
116
-              loading={this.props.loading}
117
-            >
118
-              发表
138
+            <Button onClick={onSubmit} type="primary" loading={loading}>
139
+              {submitText}
119
             </Button>
140
             </Button>
120
           </div>
141
           </div>
121
         </div>
142
         </div>
123
     );
144
     );
124
   }
145
   }
125
 }
146
 }
147
+
148
+Editor.propTypes = {
149
+  rows: PropTypes.number,
150
+  placeholder: PropTypes.string,
151
+  showEmoji: PropTypes.bool,
152
+  showUpload: PropTypes.bool,
153
+  submitText: PropTypes.string,
154
+  onChange: PropTypes.func
155
+};
156
+
157
+export default Editor;

+ 0
- 3
src/constant.js Vedi File

1
-// export const URL = "http://121.41.20.11:8082/v1";
2
-export const URL = "http://api.links123.net/comment/v1";
3
-
4
 export const ERROR_DEFAULT = "出错了!";
1
 export const ERROR_DEFAULT = "出错了!";
5
 
2
 
6
 export const LIMIT = 10; // 默认 limit
3
 export const LIMIT = 10; // 默认 limit

+ 20
- 2
src/index.js Vedi File

1
 import React from "react";
1
 import React from "react";
2
 import ReactDOM from "react-dom";
2
 import ReactDOM from "react-dom";
3
-import App from "./App";
3
+import App, { Editor } from "./App";
4
 import registerServiceWorker from "./registerServiceWorker";
4
 import registerServiceWorker from "./registerServiceWorker";
5
 
5
 
6
-ReactDOM.render(<App />, document.getElementById("root-comment"));
6
+const props = {
7
+  type: 1,
8
+  businessId: "1",
9
+  API: "http://api.links123.net/comment/v1",
10
+  showList: false
11
+};
12
+
13
+const editorProps = {
14
+  showEmoji: true,
15
+  placeholder: "说点什么吧",
16
+  rows: 5
17
+};
18
+
19
+ReactDOM.render(
20
+  <App {...props}>
21
+    <Editor {...editorProps} onSubmit={v => console.log()} />
22
+  </App>,
23
+  document.getElementById("root-comment")
24
+);
7
 registerServiceWorker();
25
 registerServiceWorker();

+ 15
- 0
src/lang/index.js Vedi File

1
+// 语言包
2
+// 英文短语和中文提示的对应
3
+const data = {
4
+  "auth failed": "请先登录",
5
+  "create comment failed": "创建评论失败",
6
+  "comment favor failed": "评论点赞失败",
7
+  "delete comment favor failed": "评论取消点赞失败",
8
+  "get comments failed": "获取评论列表失败",
9
+  "create reply failed": "创建回复失败",
10
+  "reply favor failed": "回复点赞失败",
11
+  "delete reply favor failed": "删除回复点赞失败",
12
+  "get replies failed": "获取回复列表失败"
13
+};
14
+
15
+export default data;