node 6 年 前
コミット
0d003e8da3
共有71 個のファイルを変更した4414 個の追加2 個の削除を含む
  1. 7
    0
      .babelrc
  2. 3
    0
      .eslintrc
  3. 47
    0
      .gitignore
  4. 79
    2
      README.md
  5. 6
    0
      config-overrides.js
  6. 9
    0
      lib/App.css
  7. 394
    0
      lib/App.js
  8. 1
    0
      lib/App.js.map
  9. 38
    0
      lib/Comment.js
  10. 1
    0
      lib/Comment.js.map
  11. 16
    0
      lib/axios.js
  12. 1
    0
      lib/axios.js.map
  13. 48
    0
      lib/components/CommentBox/index.css
  14. 180
    0
      lib/components/CommentBox/index.js
  15. 1
    0
      lib/components/CommentBox/index.js.map
  16. 202
    0
      lib/components/CommentInput/index.js
  17. 1
    0
      lib/components/CommentInput/index.js.map
  18. 14
    0
      lib/components/CommentList/index.css
  19. 95
    0
      lib/components/CommentList/index.js
  20. 1
    0
      lib/components/CommentList/index.js.map
  21. 52
    0
      lib/components/ContentItem/index.css
  22. 201
    0
      lib/components/ContentItem/index.js
  23. 1
    0
      lib/components/ContentItem/index.js.map
  24. 23
    0
      lib/components/Editor/Emoji.css
  25. 106
    0
      lib/components/Editor/Emoji.js
  26. 1
    0
      lib/components/Editor/Emoji.js.map
  27. 8
    0
      lib/components/Editor/Upload.css
  28. 205
    0
      lib/components/Editor/Upload.js
  29. 1
    0
      lib/components/Editor/Upload.js.map
  30. 82
    0
      lib/components/Editor/index.css
  31. 213
    0
      lib/components/Editor/index.js
  32. 1
    0
      lib/components/Editor/index.js.map
  33. 22
    0
      lib/constant.js
  34. 1
    0
      lib/constant.js.map
  35. 175
    0
      lib/emoji.js
  36. 1
    0
      lib/emoji.js.map
  37. 57
    0
      lib/helper.js
  38. 1
    0
      lib/helper.js.map
  39. 23
    0
      lib/index.js
  40. 1
    0
      lib/index.js.map
  41. 72
    0
      lib/mock.js
  42. 1
    0
      lib/mock.js.map
  43. 108
    0
      lib/registerServiceWorker.js
  44. 1
    0
      lib/registerServiceWorker.js.map
  45. 52
    0
      package.json
  46. バイナリ
      public/favicon.ico
  47. 42
    0
      public/index.html
  48. 15
    0
      public/manifest.json
  49. 9
    0
      src/App.css
  50. 307
    0
      src/App.js
  51. 21
    0
      src/Comment.js
  52. 5
    0
      src/axios.js
  53. 48
    0
      src/components/CommentBox/index.css
  54. 120
    0
      src/components/CommentBox/index.js
  55. 135
    0
      src/components/CommentInput/index.js
  56. 14
    0
      src/components/CommentList/index.css
  57. 46
    0
      src/components/CommentList/index.js
  58. 52
    0
      src/components/ContentItem/index.css
  59. 130
    0
      src/components/ContentItem/index.js
  60. 23
    0
      src/components/Editor/Emoji.css
  61. 65
    0
      src/components/Editor/Emoji.js
  62. 8
    0
      src/components/Editor/Upload.css
  63. 132
    0
      src/components/Editor/Upload.js
  64. 82
    0
      src/components/Editor/index.css
  65. 125
    0
      src/components/Editor/index.js
  66. 16
    0
      src/constant.js
  67. 226
    0
      src/emoji.js
  68. 44
    0
      src/helper.js
  69. 7
    0
      src/index.js
  70. 72
    0
      src/mock.js
  71. 117
    0
      src/registerServiceWorker.js

+ 7
- 0
.babelrc ファイルの表示

@@ -0,0 +1,7 @@
1
+{
2
+  "presets": ["env", "react"],
3
+  "plugins": [
4
+    "transform-object-rest-spread",
5
+    ["import", { "libraryName": "antd", "libraryDirectory": "es", "style": "css" }]
6
+  ]
7
+}

+ 3
- 0
.eslintrc ファイルの表示

@@ -0,0 +1,3 @@
1
+{
2
+  "extends": "react-app"
3
+}

+ 47
- 0
.gitignore ファイルの表示

@@ -0,0 +1,47 @@
1
+# misc
2
+.DS_Store
3
+npm-debug.log*
4
+.DS_STORE
5
+node_modules
6
+scripts/flow/*/.flowconfig
7
+*~
8
+*.pyc
9
+.grunt
10
+_SpecRunner.html
11
+__benchmarks__
12
+build/
13
+dist/
14
+remote-repo/
15
+coverage/
16
+flow-coverage/
17
+.module-cache
18
+fixtures/dom/public/react-dom.js
19
+fixtures/dom/public/react.js
20
+test/the-files-to-test.generated.js
21
+*.log*
22
+chrome-user-data
23
+*.sublime-project
24
+*.sublime-workspace
25
+.idea
26
+*.iml
27
+.vscode
28
+*.swp
29
+*.swo
30
+package-lock.json
31
+yarn.lock
32
+
33
+# testing
34
+/coverage
35
+
36
+# production
37
+/build
38
+
39
+# misc
40
+.DS_Store
41
+.env.local
42
+.env.development.local
43
+.env.test.local
44
+.env.production.local
45
+npm-debug.log*
46
+yarn-debug.log*
47
+yarn-error.log*

+ 79
- 2
README.md ファイルの表示

@@ -1,3 +1,80 @@
1
-# comment
1
+# Comment
2 2
 
3
-通用评论
3
+通用评论系统
4
+
5
+
6
+## 使用
7
+
8
+
9
+### 作为组件使用
10
+
11
+```
12
+# npm 安装
13
+$ npm install git+https://git.links123.net/node/test_comment.git
14
+# yarn 安装
15
+$ yarn add https://git.links123.net/node/test_comment.git
16
+```
17
+
18
+然后在代码里面引入 `comment` 组件:
19
+
20
+```jsx
21
+import Comment from 'comment'
22
+
23
+// ...
24
+
25
+render() {
26
+  return (
27
+    <Comment />
28
+  )
29
+}
30
+```
31
+
32
+**注意:最好,还需要在 HTML 文件里面引入阿里云 OSS SDK `<script src="http://gosspublic.alicdn.com/aliyun-oss-sdk.min.js"></script>`**
33
+
34
+
35
+### 作为静态文件引入
36
+
37
+首先引入 OSS SDK,用于直传文件到 OSS。
38
+
39
+然后分别通过 link 和 script 标签引入打包后的文件。
40
+
41
+再创建一个 id 为 `root-comment` 的标签,作为渲染通用评论的跟元素。
42
+
43
+如下所示:
44
+
45
+```html
46
+<!DOCTYPE html>
47
+<html>
48
+<head>
49
+  <meta charset="utf-8" />
50
+  <meta http-equiv="X-UA-Compatible" content="IE=edge">
51
+  <title>Page Title</title>
52
+  <meta name="viewport" content="width=device-width, initial-scale=1">
53
+  <link rel="stylesheet" type="text/css" media="screen" href="./static/css/main.f205c84d.css" />
54
+</head>
55
+<body>
56
+  <div id="root-comment" style="width: 700px"></div>
57
+  <script src="http://gosspublic.alicdn.com/aliyun-oss-sdk.min.js"></script>
58
+  <script src="./static/js/main.eac872df.js"></script>
59
+</body>
60
+</html>
61
+```
62
+
63
+
64
+## 开发
65
+
66
+```
67
+$ git clone https://git.links123.net/npm/comment
68
+$ cd comment
69
+$ yarn
70
+$ yarn start
71
+```
72
+
73
+- `yarn build` 将项目打包成一个单页应用
74
+- `yarn lib` 将项目打包成一个组件
75
+- `yarn prettier` 格式化代码
76
+
77
+## TODO
78
+
79
+- [ ] 前后端统一错误码
80
+- [ ] type 和 businessID 的定义

+ 6
- 0
config-overrides.js ファイルの表示

@@ -0,0 +1,6 @@
1
+const { injectBabelPlugin } = require('react-app-rewired');
2
+
3
+module.exports = function override(config, env) {
4
+  config = injectBabelPlugin(['import', { libraryName: 'antd', libraryDirectory: 'es', style: 'css' }], config);
5
+  return config;
6
+};

+ 9
- 0
lib/App.css ファイルの表示

@@ -0,0 +1,9 @@
1
+.comment {
2
+  width: 100%;
3
+  padding: 10px;
4
+  margin-bottom: 100px;
5
+}
6
+.comment .ant-spin-nested-loading > div > .ant-spin .ant-spin-dot {
7
+  position: fixed;
8
+  top: 45%;
9
+}

+ 394
- 0
lib/App.js ファイルの表示

@@ -0,0 +1,394 @@
1
+"use strict";
2
+
3
+Object.defineProperty(exports, "__esModule", {
4
+  value: true
5
+});
6
+
7
+var _message2 = require("antd/es/message");
8
+
9
+var _message3 = _interopRequireDefault(_message2);
10
+
11
+var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
12
+
13
+var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
14
+
15
+require("antd/es/message/style/css");
16
+
17
+var _react = require("react");
18
+
19
+var _react2 = _interopRequireDefault(_react);
20
+
21
+var _axios = require("./axios");
22
+
23
+var _axios2 = _interopRequireDefault(_axios);
24
+
25
+var _constant = require("./constant");
26
+
27
+var _Comment = require("./Comment");
28
+
29
+var _helper = require("./helper");
30
+
31
+var _CommentInput = require("./components/CommentInput");
32
+
33
+var _CommentInput2 = _interopRequireDefault(_CommentInput);
34
+
35
+var _CommentList = require("./components/CommentList");
36
+
37
+var _CommentList2 = _interopRequireDefault(_CommentList);
38
+
39
+require("./App.css");
40
+
41
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
42
+
43
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
44
+
45
+function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
46
+
47
+function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
48
+// import * as mock from "./mock";
49
+
50
+
51
+var App = function (_Component) {
52
+  _inherits(App, _Component);
53
+
54
+  function App(props) {
55
+    _classCallCheck(this, App);
56
+
57
+    var _this = _possibleConstructorReturn(this, (App.__proto__ || Object.getPrototypeOf(App)).call(this, props));
58
+
59
+    _this.state = {
60
+      loading: {},
61
+
62
+      // oss 配置
63
+      oss: {},
64
+
65
+      // 评论数据
66
+      list: [],
67
+      page: 1,
68
+      total: 0,
69
+      // 是否没有更多评论了
70
+      isNoMoreComment: false
71
+    };
72
+    _this.handleChangeLoading = _this.handleChangeLoading.bind(_this);
73
+    _this.sGetComment = _this.sGetComment.bind(_this);
74
+    _this.sGetReply = _this.sGetReply.bind(_this);
75
+    _this.sCreateComment = _this.sCreateComment.bind(_this);
76
+    _this.sCreateReply = _this.sCreateReply.bind(_this);
77
+    _this.sCommentFavor = _this.sCommentFavor.bind(_this);
78
+    _this.sOssSts = _this.sOssSts.bind(_this);
79
+    return _this;
80
+  }
81
+
82
+  _createClass(App, [{
83
+    key: "componentDidMount",
84
+    value: function componentDidMount() {
85
+      this.sGetComment();
86
+    }
87
+
88
+    /**
89
+     * 改变 loading 状态
90
+     * @param {string} key key
91
+     * @param {string} value value
92
+     */
93
+
94
+  }, {
95
+    key: "handleChangeLoading",
96
+    value: function handleChangeLoading(key, value) {
97
+      var loading = this.state.loading;
98
+
99
+      loading[key] = value;
100
+      this.setState({ loading: loading });
101
+    }
102
+
103
+    /**
104
+     * 获取评论列表
105
+     */
106
+
107
+  }, {
108
+    key: "sGetComment",
109
+    value: function sGetComment() {
110
+      var _this2 = this;
111
+
112
+      var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
113
+          _ref$type = _ref.type,
114
+          type = _ref$type === undefined ? 1 : _ref$type,
115
+          _ref$businessId = _ref.businessId,
116
+          businessId = _ref$businessId === undefined ? 1 : _ref$businessId,
117
+          _ref$page = _ref.page,
118
+          page = _ref$page === undefined ? 1 : _ref$page;
119
+
120
+      this.handleChangeLoading("sGetComment", true);
121
+      // 测试数据列表
122
+      // const { comments } = mock;
123
+      // this.setState({
124
+      //   list: comments.list,
125
+      //   page: 1,
126
+      //   total: 100
127
+      // });
128
+      // this.handleChangeLoading("sGetComment", false);
129
+      // return;
130
+      _axios2.default.get(_constant.URL + "/comments?type=" + type + "&business_id=" + businessId + "&page=" + page + "&limit=" + _constant.LIMIT).then(function (response) {
131
+        var _response$data = response.data,
132
+            list = _response$data.list,
133
+            page = _response$data.page,
134
+            total = _response$data.total;
135
+
136
+        if (list) {
137
+          var newList = list;
138
+          if (page > 1) {
139
+            var oldList = _this2.state.list;
140
+            // 删除临时数据
141
+
142
+            oldList = oldList.filter(function (o) {
143
+              return !o.isTemporary;
144
+            });
145
+            newList = oldList.concat(newList);
146
+          }
147
+          _this2.setState({
148
+            list: newList,
149
+            page: page,
150
+            total: total
151
+          });
152
+        } else {
153
+          _message3.default.info("没有更多评论了");
154
+          _this2.setState({
155
+            isNoMoreComment: true
156
+          });
157
+        }
158
+      }).catch(function (error) {
159
+        if (error.response && error.response.data && error.response.data.msg) {
160
+          _message3.default.error(error.response.data.msg || _constant.ERROR_DEFAULT);
161
+          return;
162
+        }
163
+        _message3.default.error(error.message || _constant.ERROR_DEFAULT);
164
+      }).finally(function () {
165
+        _this2.handleChangeLoading("sGetComment", false);
166
+      });
167
+    }
168
+
169
+    /**
170
+     * 获取更多回复
171
+     */
172
+
173
+  }, {
174
+    key: "sGetReply",
175
+    value: function sGetReply() {
176
+      var _this3 = this;
177
+
178
+      var _ref2 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
179
+          commentId = _ref2.commentId,
180
+          _ref2$page = _ref2.page,
181
+          page = _ref2$page === undefined ? 1 : _ref2$page;
182
+
183
+      this.handleChangeLoading("sGetReply", true);
184
+      _axios2.default.get(_constant.URL + "/replies?comment_id=" + commentId + "&page=" + page + "&limit=" + _constant.LIMIT).then(function (response) {
185
+        var replies = response.data.list;
186
+
187
+        if (!replies) {
188
+          _message3.default.info("没有更多数据了!");
189
+        }
190
+        var list = _this3.state.list.map(function (item) {
191
+          if (item.id === commentId) {
192
+            if (!item.replies) item.replies = [];
193
+            if (replies) {
194
+              if (page === 1) {
195
+                // 如果当前页数为第一页,则清空当前所有的 replies
196
+                // 并将获取到的 replies 存放在 state
197
+                item.replies = replies;
198
+              } else {
199
+                item.replies = item.replies.concat(replies);
200
+                // 如果当前页数非第一页,则合并 replies
201
+              }
202
+            } else {
203
+              item.isNoMoreReply = true;
204
+            }
205
+          }
206
+          return item;
207
+        });
208
+        _this3.setState({ list: list });
209
+      }).catch(function (error) {
210
+        if (error.response && error.response.data && error.response.data.msg) {
211
+          _message3.default.error(error.response.data.msg || _constant.ERROR_DEFAULT);
212
+          return;
213
+        }
214
+        _message3.default.error(error.message || _constant.ERROR_DEFAULT);
215
+      }).finally(function () {
216
+        _this3.handleChangeLoading("sGetReply", false);
217
+      });
218
+    }
219
+
220
+    /**
221
+     * 添加评论
222
+     * @param {object} data { type, business_id, content }
223
+     */
224
+
225
+  }, {
226
+    key: "sCreateComment",
227
+    value: function sCreateComment(data) {
228
+      var _this4 = this;
229
+
230
+      if (!data.content) return _message3.default.error("评论内容不能为空 ");
231
+      this.handleChangeLoading("sCreateComment", true);
232
+      (0, _axios2.default)(_constant.URL + "/comments", {
233
+        method: "post",
234
+        data: data,
235
+        withCredentials: true
236
+      }).then(function (response) {
237
+        _message3.default.success("评论成功!");
238
+        // 将数据写入到 list 中
239
+        // 临时插入
240
+        // 等到获取数据之后,删除临时数据
241
+        var list = _this4.state.list;
242
+
243
+        list.unshift(_extends({}, response.data, {
244
+          isTemporary: true // 临时的数据
245
+        }));
246
+        _this4.setState({ list: list });
247
+      }).catch(function (error) {
248
+        if (error.response && error.response.data && error.response.data.msg) {
249
+          _message3.default.error(error.response.data.msg || _constant.ERROR_DEFAULT);
250
+          return;
251
+        }
252
+        _message3.default.error(error.message || _constant.ERROR_DEFAULT);
253
+      }).finally(function () {
254
+        _this4.handleChangeLoading("sCreateComment", false);
255
+      });
256
+    }
257
+
258
+    /**
259
+     * 添加回复
260
+     * 回复评论/回复回复
261
+     * @param {object} data { comment_id, content, [reply_id] }
262
+     */
263
+
264
+  }, {
265
+    key: "sCreateReply",
266
+    value: function sCreateReply(data, cb) {
267
+      var _this5 = this;
268
+
269
+      if (!data.content) return _message3.default.error("回复内容不能为空 ");
270
+      this.handleChangeLoading("sCreateReply", true);
271
+      (0, _axios2.default)(_constant.URL + "/replies", {
272
+        method: "post",
273
+        data: data,
274
+        withCredentials: true
275
+      }).then(function (response) {
276
+        // console.log("response: ", response.data);
277
+        // // 将该条数据插入到 list 中
278
+        // const list = this.state.list.map(item => {
279
+        //   if (item.id === data.comment_id) {
280
+        //     if (!item.replies) item.replies = [];
281
+        //     item.reply_count += 1
282
+        //     item.replies.unshift(response.data);
283
+        //   }
284
+        //   return item;
285
+        // });
286
+        // this.setState({ list });
287
+        _this5.sGetReply({ commentId: data.comment_id });
288
+        _message3.default.success("回复成功!");
289
+        if ((0, _helper.isFunction)(cb)) cb();
290
+      }).catch(function (error) {
291
+        if (error.response && error.response.data && error.response.data.msg) {
292
+          _message3.default.error(error.response.data.msg || _constant.ERROR_DEFAULT);
293
+          return;
294
+        }
295
+        _message3.default.error(error.message || _constant.ERROR_DEFAULT);
296
+      }).finally(function () {
297
+        _this5.handleChangeLoading("sCreateReply", false);
298
+      });
299
+    }
300
+
301
+    /**
302
+     * 点赞/取消点赞
303
+     * @param {string} commentId { commentId }
304
+     * @param {boolean} favored   是否已经点过赞
305
+     */
306
+
307
+  }, {
308
+    key: "sCommentFavor",
309
+    value: function sCommentFavor(commentId, favored) {
310
+      var _this6 = this;
311
+
312
+      this.handleChangeLoading("sCommentFavor", true);
313
+      (0, _axios2.default)(_constant.URL + "/comments/" + commentId + "/favor", {
314
+        method: favored ? "delete" : "put",
315
+        withCredentials: true
316
+      }).then(function (response) {
317
+        _message3.default.success(favored ? "取消点赞成功!" : "点赞成功!");
318
+        // 更新 list 中的该项数据的 favored
319
+        var list = _this6.state.list.map(function (item) {
320
+          if (item.id === commentId) {
321
+            item.favored = !favored;
322
+            item.favor_count += favored ? -1 : 1;
323
+          }
324
+          return item;
325
+        });
326
+        _this6.setState({ list: list });
327
+      }).catch(function (error) {
328
+        if (error.response && error.response.data && error.response.data.msg) {
329
+          _message3.default.error(error.response.data.msg || _constant.ERROR_DEFAULT);
330
+          return;
331
+        }
332
+        _message3.default.error(error.message || _constant.ERROR_DEFAULT);
333
+      }).finally(function () {
334
+        _this6.handleChangeLoading("sCommentFavor", false);
335
+      });
336
+    }
337
+
338
+    /**
339
+     * 获取 OSS 上传的参数
340
+     */
341
+
342
+  }, {
343
+    key: "sOssSts",
344
+    value: function sOssSts() {
345
+      var _this7 = this;
346
+
347
+      this.handleChangeLoading("sOssSts", true);
348
+      _axios2.default.get(_constant.URL + "/oss/sts").then(function (response) {
349
+        _this7.setState({ oss: _extends({}, response.data) });
350
+      }).catch(function (error) {
351
+        if (error.response && error.response.data && error.response.data.msg) {
352
+          _message3.default.error(error.response.data.msg || _constant.ERROR_DEFAULT);
353
+          return;
354
+        }
355
+        _message3.default.error(error.message || _constant.ERROR_DEFAULT);
356
+      }).finally(function () {
357
+        _this7.handleChangeLoading("sOssSts", false);
358
+      });
359
+    }
360
+  }, {
361
+    key: "render",
362
+    value: function render() {
363
+      // 添加到 Context 的数据
364
+      var value = _extends({}, this.state, {
365
+        sCreateComment: this.sCreateComment,
366
+        sGetComment: this.sGetComment,
367
+        sCommentFavor: this.sCommentFavor,
368
+        sCreateReply: this.sCreateReply,
369
+        sGetReply: this.sGetReply,
370
+        sOssSts: this.sOssSts
371
+      });
372
+
373
+      return _react2.default.createElement(
374
+        _Comment.CommentContext.Provider,
375
+        { value: value },
376
+        _react2.default.createElement(
377
+          "div",
378
+          { className: "comment" },
379
+          _react2.default.createElement(_CommentInput2.default, null),
380
+          _react2.default.createElement(
381
+            "div",
382
+            { style: { marginTop: 20 } },
383
+            _react2.default.createElement(_CommentList2.default, null)
384
+          )
385
+        )
386
+      );
387
+    }
388
+  }]);
389
+
390
+  return App;
391
+}(_react.Component);
392
+
393
+exports.default = App;
394
+//# sourceMappingURL=App.js.map

+ 1
- 0
lib/App.js.map
ファイル差分が大きすぎるため省略します
ファイルの表示


+ 38
- 0
lib/Comment.js ファイルの表示

@@ -0,0 +1,38 @@
1
+"use strict";
2
+
3
+Object.defineProperty(exports, "__esModule", {
4
+  value: true
5
+});
6
+exports.CommentContext = undefined;
7
+
8
+var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
9
+
10
+exports.Comment = Comment;
11
+
12
+var _react = require("react");
13
+
14
+var _react2 = _interopRequireDefault(_react);
15
+
16
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
17
+
18
+var CommentContext = _react2.default.createContext();
19
+
20
+// This function takes a component...
21
+function Comment(Component) {
22
+  // ...and returns another component...
23
+  return function (props) {
24
+    // ... and renders the wrapped component with the context theme!
25
+    // Notice that we pass through any additional props as well
26
+    return _react2.default.createElement(
27
+      CommentContext.Consumer,
28
+      null,
29
+      function (app) {
30
+        return _react2.default.createElement(Component, _extends({}, props, { app: app }));
31
+      }
32
+    );
33
+  };
34
+}
35
+
36
+exports.CommentContext = CommentContext;
37
+exports.default = Comment;
38
+//# sourceMappingURL=Comment.js.map

+ 1
- 0
lib/Comment.js.map ファイルの表示

@@ -0,0 +1 @@
1
+{"version":3,"sources":["../src/Comment.js"],"names":["Comment","CommentContext","React","createContext","Component","props","app"],"mappings":";;;;;;;;;QAKgBA,O,GAAAA,O;;AALhB;;;;;;AAEA,IAAMC,iBAAiBC,gBAAMC,aAAN,EAAvB;;AAEA;AACO,SAASH,OAAT,CAAiBI,SAAjB,EAA4B;AACjC;AACA,SAAO,UAASC,KAAT,EAAgB;AACrB;AACA;AACA,WACE;AAAC,oBAAD,CAAgB,QAAhB;AAAA;AACG;AAAA,eAAO,8BAAC,SAAD,eAAeA,KAAf,IAAsB,KAAKC,GAA3B,IAAP;AAAA;AADH,KADF;AAKD,GARD;AASD;;QAEQL,c,GAAAA,c;kBAEMD,O","file":"Comment.js","sourcesContent":["import React from \"react\";\n\nconst CommentContext = React.createContext();\n\n// This function takes a component...\nexport function Comment(Component) {\n  // ...and returns another component...\n  return function(props) {\n    // ... and renders the wrapped component with the context theme!\n    // Notice that we pass through any additional props as well\n    return (\n      <CommentContext.Consumer>\n        {app => <Component {...props} app={app} />}\n      </CommentContext.Consumer>\n    );\n  };\n}\n\nexport { CommentContext };\n\nexport default Comment;\n"]}

+ 16
- 0
lib/axios.js ファイルの表示

@@ -0,0 +1,16 @@
1
+"use strict";
2
+
3
+Object.defineProperty(exports, "__esModule", {
4
+  value: true
5
+});
6
+
7
+var _axios = require("axios");
8
+
9
+var _axios2 = _interopRequireDefault(_axios);
10
+
11
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
12
+
13
+_axios2.default.defaults.withCredentials = true;
14
+
15
+exports.default = _axios2.default;
16
+//# sourceMappingURL=axios.js.map

+ 1
- 0
lib/axios.js.map ファイルの表示

@@ -0,0 +1 @@
1
+{"version":3,"sources":["../src/axios.js"],"names":["axios","defaults","withCredentials"],"mappings":";;;;;;AAAA;;;;;;AAEAA,gBAAMC,QAAN,CAAeC,eAAf,GAAiC,IAAjC;;kBAEeF,e","file":"axios.js","sourcesContent":["import axios from \"axios\";\n\naxios.defaults.withCredentials = true;\n\nexport default axios;\n"]}

+ 48
- 0
lib/components/CommentBox/index.css ファイルの表示

@@ -0,0 +1,48 @@
1
+.showMore {
2
+  color: #4a90e2;
3
+  text-align: center;
4
+  width: 100px;
5
+  background-color: #f8f8f8;
6
+  cursor: pointer;
7
+  padding: 10px;
8
+  margin: 0 auto;
9
+  transition: all 0.3s;
10
+}
11
+.showMore:hover {
12
+  background-color: #f5f5f5;
13
+  color: #1890ff;
14
+}
15
+.moreBox {
16
+  text-align: center;
17
+  width: 90%;
18
+  margin-left: 40px;
19
+  height: 40px;
20
+  display: inline-block;
21
+}
22
+@media screen and (max-width: 616px) and (min-width: 449px) {
23
+  .moreBox {
24
+    text-align: center;
25
+    width: 85%;
26
+    margin-left: 40px;
27
+    height: 40px;
28
+    display: inline-block;
29
+  }
30
+}
31
+@media screen and (max-width: 449px) and (min-width: 365px) {
32
+  .moreBox {
33
+    text-align: center;
34
+    width: 80%;
35
+    margin-left: 40px;
36
+    height: 40px;
37
+    display: inline-block;
38
+  }
39
+}
40
+@media screen and (max-width: 365px) {
41
+  .moreBox {
42
+    text-align: center;
43
+    width: 75%;
44
+    margin-left: 40px;
45
+    height: 40px;
46
+    display: inline-block;
47
+  }
48
+}

+ 180
- 0
lib/components/CommentBox/index.js ファイルの表示

@@ -0,0 +1,180 @@
1
+"use strict";
2
+
3
+Object.defineProperty(exports, "__esModule", {
4
+  value: true
5
+});
6
+
7
+var _icon = require("antd/es/icon");
8
+
9
+var _icon2 = _interopRequireDefault(_icon);
10
+
11
+var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
12
+
13
+require("antd/es/icon/style/css");
14
+
15
+var _react = require("react");
16
+
17
+var _react2 = _interopRequireDefault(_react);
18
+
19
+var _propTypes = require("prop-types");
20
+
21
+var _propTypes2 = _interopRequireDefault(_propTypes);
22
+
23
+var _Comment = require("../../Comment");
24
+
25
+var _Comment2 = _interopRequireDefault(_Comment);
26
+
27
+var _ContentItem = require("./../ContentItem");
28
+
29
+var _ContentItem2 = _interopRequireDefault(_ContentItem);
30
+
31
+require("./index.css");
32
+
33
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
34
+
35
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
36
+
37
+function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
38
+
39
+function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
40
+
41
+var CommentBox = function (_Component) {
42
+  _inherits(CommentBox, _Component);
43
+
44
+  function CommentBox(props) {
45
+    _classCallCheck(this, CommentBox);
46
+
47
+    var _this = _possibleConstructorReturn(this, (CommentBox.__proto__ || Object.getPrototypeOf(CommentBox)).call(this, props));
48
+
49
+    _this.state = {
50
+      showReply: true,
51
+      page: 1
52
+    };
53
+
54
+    _this.handleToggleReply = _this.handleToggleReply.bind(_this);
55
+    _this.handleGetMoreReply = _this.handleGetMoreReply.bind(_this);
56
+    _this.renderReplies = _this.renderReplies.bind(_this);
57
+    return _this;
58
+  }
59
+
60
+  /**
61
+   * 切换是否显示回复列表
62
+   */
63
+
64
+
65
+  _createClass(CommentBox, [{
66
+    key: "handleToggleReply",
67
+    value: function handleToggleReply() {
68
+      this.setState({ showReply: !this.state.showReply });
69
+    }
70
+
71
+    /**
72
+     * 获取更多评论
73
+     * @param {string} commentId comment id
74
+     */
75
+
76
+  }, {
77
+    key: "handleGetMoreReply",
78
+    value: function handleGetMoreReply(commentId) {
79
+      // 从第一页开始获取评论
80
+      var page = this.state.page;
81
+
82
+      this.props.app.sGetReply({ commentId: commentId, page: page });
83
+      this.setState({ page: page + 1 });
84
+    }
85
+
86
+    /**
87
+     * 渲染回复 DOM
88
+     * @param {array} replies 回复列表
89
+     * @param {boolean} isNoMoreReply 是否没有更多回复
90
+     */
91
+
92
+  }, {
93
+    key: "renderReplies",
94
+    value: function renderReplies(replies, isNoMoreReply) {
95
+      var _this2 = this;
96
+
97
+      var commentId = this.props.commentId;
98
+      var showReply = this.state.showReply;
99
+
100
+      if (showReply && replies && replies.length) {
101
+        var len = replies.length;
102
+        return _react2.default.createElement(
103
+          "div",
104
+          { style: { marginLeft: 50 } },
105
+          replies.map(function (item, index) {
106
+            if (index === len - 1) {
107
+              return [_react2.default.createElement(_ContentItem2.default, {
108
+                commentId: commentId,
109
+                replyId: item.id,
110
+                key: item.id,
111
+                content: item,
112
+                type: "reply"
113
+              }), _react2.default.createElement(
114
+                "div",
115
+                { className: "moreBox", key: "show_more_button" },
116
+                !isNoMoreReply && _react2.default.createElement(
117
+                  "span",
118
+                  {
119
+                    className: "showMore",
120
+                    onClick: function onClick() {
121
+                      return _this2.handleGetMoreReply(commentId);
122
+                    }
123
+                  },
124
+                  "\u67E5\u770B\u66F4\u591A\u56DE\u590D"
125
+                ),
126
+                _react2.default.createElement(
127
+                  "a",
128
+                  {
129
+                    style: { float: "right" },
130
+                    onClick: _this2.handleToggleReply
131
+                  },
132
+                  _react2.default.createElement(_icon2.default, { type: "up" }),
133
+                  " \u6536\u8D77\u56DE\u590D"
134
+                )
135
+              )];
136
+            }
137
+            return _react2.default.createElement(_ContentItem2.default, {
138
+              commentId: commentId,
139
+              replyId: item.id,
140
+              key: item.id,
141
+              content: item,
142
+              type: "reply"
143
+            });
144
+          })
145
+        );
146
+      }
147
+      return null;
148
+    }
149
+  }, {
150
+    key: "render",
151
+    value: function render() {
152
+      var content = this.props.content;
153
+      var showReply = this.state.showReply;
154
+
155
+      return _react2.default.createElement(
156
+        "div",
157
+        null,
158
+        _react2.default.createElement(_ContentItem2.default, {
159
+          content: content,
160
+          onShowReply: this.handleToggleReply,
161
+          showReply: showReply,
162
+          commentId: content.id,
163
+          type: "comment"
164
+        }),
165
+        this.renderReplies(content.replies, content.isNoMoreReply)
166
+      );
167
+    }
168
+  }]);
169
+
170
+  return CommentBox;
171
+}(_react.Component);
172
+
173
+CommentBox.propTypes = {
174
+  commentId: _propTypes2.default.string.isRequired
175
+};
176
+
177
+CommentBox.defaultProps = {};
178
+
179
+exports.default = (0, _Comment2.default)(CommentBox);
180
+//# sourceMappingURL=index.js.map

+ 1
- 0
lib/components/CommentBox/index.js.map
ファイル差分が大きすぎるため省略します
ファイルの表示


+ 202
- 0
lib/components/CommentInput/index.js ファイルの表示

@@ -0,0 +1,202 @@
1
+"use strict";
2
+
3
+Object.defineProperty(exports, "__esModule", {
4
+  value: true
5
+});
6
+
7
+var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
8
+
9
+var _react = require("react");
10
+
11
+var _react2 = _interopRequireDefault(_react);
12
+
13
+var _propTypes = require("prop-types");
14
+
15
+var _propTypes2 = _interopRequireDefault(_propTypes);
16
+
17
+var _constant = require("../../constant");
18
+
19
+var _Comment = require("../../Comment");
20
+
21
+var _Comment2 = _interopRequireDefault(_Comment);
22
+
23
+var _Editor = require("../Editor");
24
+
25
+var _Editor2 = _interopRequireDefault(_Editor);
26
+
27
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
28
+
29
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
30
+
31
+function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
32
+
33
+function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
34
+
35
+var PLACEHOLDER = {
36
+  normal: "说点什么吧...",
37
+  default: "说点什么吧..."
38
+};
39
+
40
+var CommentInput = function (_Component) {
41
+  _inherits(CommentInput, _Component);
42
+
43
+  function CommentInput(props) {
44
+    _classCallCheck(this, CommentInput);
45
+
46
+    var _this = _possibleConstructorReturn(this, (CommentInput.__proto__ || Object.getPrototypeOf(CommentInput)).call(this, props));
47
+
48
+    _this.state = {
49
+      value: "",
50
+
51
+      fileList: [], // 图片列表
52
+      fileMap: {} // 已经上传的图片路径和 uid 的映射 { uid: path }
53
+    };
54
+    _this.handleChange = _this.handleChange.bind(_this);
55
+    _this.handleSubmit = _this.handleSubmit.bind(_this);
56
+    _this.handleChangeFileList = _this.handleChangeFileList.bind(_this);
57
+    _this.handleChangeEmoji = _this.handleChangeEmoji.bind(_this);
58
+    _this.handleUpload = _this.handleUpload.bind(_this);
59
+    return _this;
60
+  }
61
+
62
+  _createClass(CommentInput, [{
63
+    key: "handleChange",
64
+    value: function handleChange(e) {
65
+      this.setState({ value: e.target.value });
66
+    }
67
+  }, {
68
+    key: "handleChangeFileList",
69
+    value: function handleChangeFileList(fileList) {
70
+      this.setState({ fileList: fileList });
71
+    }
72
+  }, {
73
+    key: "handleChangeEmoji",
74
+    value: function handleChangeEmoji(emojiId) {
75
+      var value = this.state.value;
76
+
77
+      value += "[" + emojiId + "]";
78
+      this.setState({ value: value });
79
+    }
80
+  }, {
81
+    key: "handleUpload",
82
+    value: function handleUpload(_ref) {
83
+      var uid = _ref.uid,
84
+          path = _ref.path;
85
+      var fileMap = this.state.fileMap;
86
+
87
+      fileMap[uid] = path;
88
+      this.setState({ fileMap: fileMap });
89
+    }
90
+  }, {
91
+    key: "handleSubmit",
92
+    value: function handleSubmit() {
93
+      var _state = this.state,
94
+          value = _state.value,
95
+          fileMap = _state.fileMap,
96
+          fileList = _state.fileList;
97
+
98
+      if (fileList.length) {
99
+        fileList.forEach(function (item) {
100
+          value += "[" + _constant.OSS_LINK + fileMap[item.uid] + "]";
101
+        });
102
+      }
103
+
104
+      var _props = this.props,
105
+          type = _props.type,
106
+          commentId = _props.commentId,
107
+          replyId = _props.replyId,
108
+          handleToggleInput = _props.handleToggleInput;
109
+
110
+      if (type === "normal") {
111
+        this.props.app.sCreateComment({
112
+          type: 1,
113
+          business_id: "1",
114
+          content: value
115
+        });
116
+      } else if (type === "comment") {
117
+        this.props.app.sCreateReply({
118
+          comment_id: commentId,
119
+          content: value
120
+        }, function () {
121
+          return handleToggleInput();
122
+        });
123
+      } else if (type === "reply") {
124
+        this.props.app.sCreateReply({
125
+          comment_id: commentId,
126
+          content: value,
127
+          reply_id: replyId
128
+        }, function () {
129
+          return handleToggleInput();
130
+        });
131
+      }
132
+    }
133
+  }, {
134
+    key: "render",
135
+    value: function render() {
136
+      var type = this.props.type;
137
+      var _state2 = this.state,
138
+          value = _state2.value,
139
+          fileList = _state2.fileList;
140
+
141
+
142
+      return _react2.default.createElement(
143
+        "div",
144
+        null,
145
+        type === "normal" ? _react2.default.createElement(
146
+          "div",
147
+          null,
148
+          _react2.default.createElement(
149
+            "span",
150
+            {
151
+              style: {
152
+                border: "1px solid #CECECE",
153
+                color: "#666",
154
+                padding: "2px 3px"
155
+              }
156
+            },
157
+            "\u56DE\u590D"
158
+          ),
159
+          _react2.default.createElement(
160
+            "span",
161
+            { style: { marginLeft: "20px", color: "#5198EB" } },
162
+            "\u53E3\u7891",
163
+            _react2.default.createElement(
164
+              "span",
165
+              { style: { marginLeft: "20px", color: "#666666" } },
166
+              "(\u5168\u7AD9\u6311\u51FA\u6BDB\u75C5\u6216\u63D0\u51FA\u5408\u7406\u5EFA\u8BAE\uFF0C\u5956\u52B110\u5230100\u5143\u7EA2\u5305)"
167
+            )
168
+          )
169
+        ) : null,
170
+        _react2.default.createElement(
171
+          "div",
172
+          { style: { marginTop: 40 } },
173
+          _react2.default.createElement(_Editor2.default, {
174
+            value: value,
175
+            placeholder: PLACEHOLDER[type] || PLACEHOLDER.default,
176
+            fileList: fileList,
177
+            onChange: this.handleChange,
178
+            onSubmit: this.handleSubmit,
179
+            onChangeFileList: this.handleChangeFileList,
180
+            onChangeEmoji: this.handleChangeEmoji,
181
+            onUpload: this.handleUpload,
182
+            loading: this.props.app.loading.sCreateComment
183
+          })
184
+        )
185
+      );
186
+    }
187
+  }]);
188
+
189
+  return CommentInput;
190
+}(_react.Component);
191
+
192
+CommentInput.propTypes = {
193
+  // normal 有切换回复/口碑的 header ; comment 评论输入框 / reply 回复输入框
194
+  type: _propTypes2.default.oneOf(["normal", "comment", "reply"])
195
+};
196
+
197
+CommentInput.defaultProps = {
198
+  type: "normal"
199
+};
200
+
201
+exports.default = (0, _Comment2.default)(CommentInput);
202
+//# sourceMappingURL=index.js.map

+ 1
- 0
lib/components/CommentInput/index.js.map
ファイル差分が大きすぎるため省略します
ファイルの表示


+ 14
- 0
lib/components/CommentList/index.css ファイルの表示

@@ -0,0 +1,14 @@
1
+.showMore {
2
+  color: #4a90e2;
3
+  text-align: center;
4
+  width: 100%;
5
+  background-color: #f8f8f8;
6
+  cursor: pointer;
7
+  padding: 10px;
8
+  margin: 40px auto;
9
+  transition: all 0.3s;
10
+}
11
+.showMore:hover {
12
+  background-color: #f5f5f5;
13
+  color: #1890ff;
14
+}

+ 95
- 0
lib/components/CommentList/index.js ファイルの表示

@@ -0,0 +1,95 @@
1
+"use strict";
2
+
3
+Object.defineProperty(exports, "__esModule", {
4
+  value: true
5
+});
6
+
7
+var _spin = require("antd/es/spin");
8
+
9
+var _spin2 = _interopRequireDefault(_spin);
10
+
11
+var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
12
+
13
+require("antd/es/spin/style/css");
14
+
15
+var _react = require("react");
16
+
17
+var _react2 = _interopRequireDefault(_react);
18
+
19
+var _Comment = require("../../Comment");
20
+
21
+var _Comment2 = _interopRequireDefault(_Comment);
22
+
23
+var _CommentBox = require("../CommentBox");
24
+
25
+var _CommentBox2 = _interopRequireDefault(_CommentBox);
26
+
27
+require("./index.css");
28
+
29
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
30
+
31
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
32
+
33
+function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
34
+
35
+function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
36
+
37
+var CommentList = function (_Component) {
38
+  _inherits(CommentList, _Component);
39
+
40
+  function CommentList(props) {
41
+    _classCallCheck(this, CommentList);
42
+
43
+    var _this = _possibleConstructorReturn(this, (CommentList.__proto__ || Object.getPrototypeOf(CommentList)).call(this, props));
44
+
45
+    _this.state = {};
46
+    return _this;
47
+  }
48
+
49
+  _createClass(CommentList, [{
50
+    key: "render",
51
+    value: function render() {
52
+      var _props$app = this.props.app,
53
+          list = _props$app.list,
54
+          page = _props$app.page,
55
+          loading = _props$app.loading,
56
+          isNoMoreComment = _props$app.isNoMoreComment,
57
+          sGetComment = _props$app.sGetComment;
58
+
59
+
60
+      var spinning = Boolean(loading.sGetComment || loading.sCommentFavor);
61
+      return _react2.default.createElement(
62
+        "div",
63
+        null,
64
+        _react2.default.createElement(
65
+          _spin2.default,
66
+          { spinning: spinning },
67
+          list.map(function (item) {
68
+            return _react2.default.createElement(_CommentBox2.default, { content: item, key: item.id, commentId: item.id });
69
+          }),
70
+          !isNoMoreComment && _react2.default.createElement(
71
+            "div",
72
+            {
73
+              className: "showMore",
74
+              onClick: function onClick() {
75
+                return sGetComment({ page: page + 1 });
76
+              }
77
+            },
78
+            _react2.default.createElement(
79
+              "span",
80
+              null,
81
+              "\u67E5\u770B\u66F4\u591A\u8BC4\u8BBA"
82
+            )
83
+          )
84
+        )
85
+      );
86
+    }
87
+  }]);
88
+
89
+  return CommentList;
90
+}(_react.Component);
91
+
92
+CommentList.propTypes = {};
93
+
94
+exports.default = (0, _Comment2.default)(CommentList);
95
+//# sourceMappingURL=index.js.map

+ 1
- 0
lib/components/CommentList/index.js.map ファイルの表示

@@ -0,0 +1 @@
1
+{"version":3,"sources":["../../../src/components/CommentList/index.js"],"names":["CommentList","props","state","app","list","page","loading","isNoMoreComment","sGetComment","spinning","Boolean","sCommentFavor","map","item","id","Component","propTypes"],"mappings":";;;;;;;;;;;;;;AAAA;;;;AAEA;;;;AACA;;;;AACA;;;;;;;;;;IAEMA,W;;;AACJ,uBAAYC,KAAZ,EAAmB;AAAA;;AAAA,0HACXA,KADW;;AAEjB,UAAKC,KAAL,GAAa,EAAb;AAFiB;AAGlB;;;;6BAEQ;AAAA,uBAOH,KAAKD,KAAL,CAAWE,GAPR;AAAA,UAELC,IAFK,cAELA,IAFK;AAAA,UAGLC,IAHK,cAGLA,IAHK;AAAA,UAILC,OAJK,cAILA,OAJK;AAAA,UAKLC,eALK,cAKLA,eALK;AAAA,UAMLC,WANK,cAMLA,WANK;;;AASP,UAAMC,WAAWC,QAAQJ,QAAQE,WAAR,IAAuBF,QAAQK,aAAvC,CAAjB;AACA,aACE;AAAA;AAAA;AACE;AAAA;AAAA,YAAM,UAAUF,QAAhB;AACGL,eAAKQ,GAAL,CAAS;AAAA,mBACR,8BAAC,oBAAD,IAAY,SAASC,IAArB,EAA2B,KAAKA,KAAKC,EAArC,EAAyC,WAAWD,KAAKC,EAAzD,GADQ;AAAA,WAAT,CADH;AAKG,WAACP,eAAD,IACC;AAAA;AAAA;AACE,yBAAU,UADZ;AAEE,uBAAS;AAAA,uBAAMC,YAAY,EAAEH,MAAMA,OAAO,CAAf,EAAZ,CAAN;AAAA;AAFX;AAIE;AAAA;AAAA;AAAA;AAAA;AAJF;AANJ;AADF,OADF;AAkBD;;;;EAlCuBU,gB;;AAqC1Bf,YAAYgB,SAAZ,GAAwB,EAAxB;;kBAEe,uBAAQhB,WAAR,C","file":"index.js","sourcesContent":["import React, { Component } from \"react\";\nimport { Spin } from \"antd\";\nimport Comment from \"../../Comment\";\nimport CommentBox from \"../CommentBox\";\nimport \"./index.css\";\n\nclass CommentList extends Component {\n  constructor(props) {\n    super(props);\n    this.state = {};\n  }\n\n  render() {\n    const {\n      list,\n      page,\n      loading,\n      isNoMoreComment,\n      sGetComment\n    } = this.props.app;\n\n    const spinning = Boolean(loading.sGetComment || loading.sCommentFavor);\n    return (\n      <div>\n        <Spin spinning={spinning}>\n          {list.map(item => (\n            <CommentBox content={item} key={item.id} commentId={item.id} />\n          ))}\n\n          {!isNoMoreComment && (\n            <div\n              className=\"showMore\"\n              onClick={() => sGetComment({ page: page + 1 })}\n            >\n              <span>查看更多评论</span>\n            </div>\n          )}\n        </Spin>\n      </div>\n    );\n  }\n}\n\nCommentList.propTypes = {};\n\nexport default Comment(CommentList);\n"]}

+ 52
- 0
lib/components/ContentItem/index.css ファイルの表示

@@ -0,0 +1,52 @@
1
+.left {
2
+  display: inline-block;
3
+  vertical-align: top;
4
+  width: 40px;
5
+}
6
+.right {
7
+  display: inline-block;
8
+  width: 90%;
9
+  margin-left: 10px;
10
+}
11
+.box {
12
+  margin: 10px;
13
+  padding: 15px 5px;
14
+  border-top: 1px solid #eee;
15
+}
16
+.content {
17
+  margin: 10px 0;
18
+}
19
+.itemLeft {
20
+  float: left;
21
+}
22
+.itemRight {
23
+  float: right;
24
+  margin-left: 5px;
25
+}
26
+.bottom {
27
+  margin: 20px auto;
28
+}
29
+.favored {
30
+  color: #4a90e2;
31
+}
32
+@media screen and (max-width: 616px) and (min-width: 449px) {
33
+  .right {
34
+    display: inline-block;
35
+    width: 85%;
36
+    margin-left: 10px;
37
+  }
38
+}
39
+@media screen and (max-width: 449px) and (min-width: 365px) {
40
+  .right {
41
+    display: inline-block;
42
+    width: 80%;
43
+    margin-left: 10px;
44
+  }
45
+}
46
+@media screen and (max-width: 365px) {
47
+  .right {
48
+    display: inline-block;
49
+    width: 75%;
50
+    margin-left: 10px;
51
+  }
52
+}

+ 201
- 0
lib/components/ContentItem/index.js ファイルの表示

@@ -0,0 +1,201 @@
1
+"use strict";
2
+
3
+Object.defineProperty(exports, "__esModule", {
4
+  value: true
5
+});
6
+
7
+var _icon = require("antd/es/icon");
8
+
9
+var _icon2 = _interopRequireDefault(_icon);
10
+
11
+var _avatar = require("antd/es/avatar");
12
+
13
+var _avatar2 = _interopRequireDefault(_avatar);
14
+
15
+var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
16
+
17
+require("antd/es/icon/style/css");
18
+
19
+require("antd/es/avatar/style/css");
20
+
21
+var _react = require("react");
22
+
23
+var _react2 = _interopRequireDefault(_react);
24
+
25
+var _propTypes = require("prop-types");
26
+
27
+var _propTypes2 = _interopRequireDefault(_propTypes);
28
+
29
+var _dayjs = require("dayjs");
30
+
31
+var _dayjs2 = _interopRequireDefault(_dayjs);
32
+
33
+var _Comment = require("../../Comment");
34
+
35
+var _Comment2 = _interopRequireDefault(_Comment);
36
+
37
+var _CommentInput = require("../CommentInput");
38
+
39
+var _CommentInput2 = _interopRequireDefault(_CommentInput);
40
+
41
+var _helper = require("../../helper");
42
+
43
+require("./index.css");
44
+
45
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
46
+
47
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
48
+
49
+function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
50
+
51
+function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
52
+
53
+var CommentItem = function (_Component) {
54
+  _inherits(CommentItem, _Component);
55
+
56
+  function CommentItem(props) {
57
+    _classCallCheck(this, CommentItem);
58
+
59
+    var _this = _possibleConstructorReturn(this, (CommentItem.__proto__ || Object.getPrototypeOf(CommentItem)).call(this, props));
60
+
61
+    _this.state = {
62
+      isShowInput: false
63
+    };
64
+    _this.handleToggleInput = _this.handleToggleInput.bind(_this);
65
+    _this.renderTextWithReply = _this.renderTextWithReply.bind(_this);
66
+    return _this;
67
+  }
68
+
69
+  _createClass(CommentItem, [{
70
+    key: "handleToggleInput",
71
+    value: function handleToggleInput() {
72
+      this.setState({ isShowInput: !this.state.isShowInput });
73
+    }
74
+  }, {
75
+    key: "renderTextWithReply",
76
+    value: function renderTextWithReply(text, content) {
77
+      var newText = text;
78
+      var reply = content.reply;
79
+
80
+      if (reply) {
81
+        newText = newText + " //@<a href=\"/" + reply.user_id + "\">" + reply.user_name + "</a> " + reply.content;
82
+        if (reply.reply) {
83
+          return this.renderTextWithReply(newText, reply);
84
+        }
85
+      }
86
+      return newText;
87
+    }
88
+  }, {
89
+    key: "render",
90
+    value: function render() {
91
+      var _props = this.props,
92
+          commentId = _props.commentId,
93
+          replyId = _props.replyId,
94
+          content = _props.content,
95
+          type = _props.type,
96
+          showReply = _props.showReply,
97
+          onShowReply = _props.onShowReply,
98
+          app = _props.app;
99
+      var isShowInput = this.state.isShowInput;
100
+
101
+      var isComment = type === "comment";
102
+      return _react2.default.createElement(
103
+        "div",
104
+        { className: "box" },
105
+        _react2.default.createElement(
106
+          "div",
107
+          { className: "left" },
108
+          _react2.default.createElement(_avatar2.default, { src: content.user_avatar, size: "large" })
109
+        ),
110
+        _react2.default.createElement(
111
+          "div",
112
+          { className: "right" },
113
+          _react2.default.createElement(
114
+            "div",
115
+            { className: "name" },
116
+            _react2.default.createElement(
117
+              "a",
118
+              { href: "/" + content.user_id },
119
+              content.user_name || "暂无昵称"
120
+            ),
121
+            _react2.default.createElement(
122
+              "span",
123
+              { style: { marginLeft: 10 } },
124
+              (0, _dayjs2.default)(content.created * 1000).format("YYYY-MM-DD HH:mm:ss")
125
+            )
126
+          ),
127
+          _react2.default.createElement("div", {
128
+            className: "content",
129
+            dangerouslySetInnerHTML: {
130
+              __html: (0, _helper.renderContent)(this.renderTextWithReply(content.content, content))
131
+            }
132
+          }),
133
+          _react2.default.createElement(
134
+            "div",
135
+            { className: "bottom" },
136
+            isComment && content.reply_count ? _react2.default.createElement(
137
+              "div",
138
+              null,
139
+              _react2.default.createElement(
140
+                "a",
141
+                {
142
+                  className: "itemLeft",
143
+                  onClick: onShowReply,
144
+                  style: { userSelect: "none" }
145
+                },
146
+                content.reply_count,
147
+                " \u6761\u56DE\u590D",
148
+                showReply ? _react2.default.createElement(_icon2.default, { type: "up" }) : _react2.default.createElement(_icon2.default, { type: "down" })
149
+              )
150
+            ) : null,
151
+            _react2.default.createElement(
152
+              "a",
153
+              { onClick: this.handleToggleInput, className: "itemRight" },
154
+              "\xA0 \u56DE\u590D"
155
+            ),
156
+            _react2.default.createElement(
157
+              "div",
158
+              {
159
+                className: "itemRight",
160
+                style: { cursor: "pointer" },
161
+                onClick: function onClick() {
162
+                  return app.sCommentFavor(content.id, content.favored);
163
+                }
164
+              },
165
+              _react2.default.createElement(_icon2.default, {
166
+                type: "like-o",
167
+                className: content.favored ? "favored" : ""
168
+              }),
169
+              "\xA0",
170
+              content.favor_count
171
+            )
172
+          ),
173
+          isShowInput ? _react2.default.createElement(_CommentInput2.default, {
174
+            type: type,
175
+            replyId: replyId,
176
+            commentId: commentId,
177
+            handleToggleInput: this.handleToggleInput
178
+          }) : null
179
+        )
180
+      );
181
+    }
182
+  }]);
183
+
184
+  return CommentItem;
185
+}(_react.Component);
186
+
187
+CommentItem.propTypes = {
188
+  content: _propTypes2.default.object.isRequired,
189
+  // comment 评论
190
+  // reply 回复
191
+  type: _propTypes2.default.oneOf(["comment", "reply"]),
192
+  onShowReply: _propTypes2.default.func
193
+};
194
+
195
+CommentItem.defaultProps = {
196
+  type: "comment",
197
+  onShowReply: function onShowReply() {}
198
+};
199
+
200
+exports.default = (0, _Comment2.default)(CommentItem);
201
+//# sourceMappingURL=index.js.map

+ 1
- 0
lib/components/ContentItem/index.js.map
ファイル差分が大きすぎるため省略します
ファイルの表示


+ 23
- 0
lib/components/Editor/Emoji.css ファイルの表示

@@ -0,0 +1,23 @@
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
+}

+ 106
- 0
lib/components/Editor/Emoji.js ファイルの表示

@@ -0,0 +1,106 @@
1
+"use strict";
2
+
3
+Object.defineProperty(exports, "__esModule", {
4
+  value: true
5
+});
6
+
7
+var _carousel = require("antd/es/carousel");
8
+
9
+var _carousel2 = _interopRequireDefault(_carousel);
10
+
11
+require("antd/es/carousel/style/css");
12
+
13
+var _react = require("react");
14
+
15
+var _react2 = _interopRequireDefault(_react);
16
+
17
+var _emoji = require("../../emoji");
18
+
19
+var _emoji2 = _interopRequireDefault(_emoji);
20
+
21
+require("./Emoji.css");
22
+
23
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
24
+
25
+// 每页 20  5*4
26
+// 共 20 * 3 = 60 (实际是 54)
27
+
28
+var Emoji = function Emoji(_ref) {
29
+  var _onClick = _ref.onClick;
30
+
31
+  var content = [[], [], []];
32
+
33
+  for (var i = 0; i < _emoji2.default.length; i++) {
34
+    if (i < 20) {
35
+      content[0].push(_emoji2.default[i]);
36
+    } else if (i < 40) {
37
+      content[1].push(_emoji2.default[i]);
38
+    } else if (i < _emoji2.default.length) {
39
+      content[2].push(_emoji2.default[i]);
40
+    }
41
+  }
42
+  return _react2.default.createElement(
43
+    _carousel2.default,
44
+    null,
45
+    _react2.default.createElement(
46
+      "div",
47
+      null,
48
+      content[0].map(function (item, index) {
49
+        return _react2.default.createElement(
50
+          "div",
51
+          { className: "item", key: item.value },
52
+          _react2.default.createElement("span", { className: "helper" }),
53
+          _react2.default.createElement("img", {
54
+            src: "" + _emoji.prefixUrl + item.value + "." + _emoji.ext,
55
+            alt: item.title,
56
+            style: { display: "inline-block" },
57
+            onClick: function onClick() {
58
+              return _onClick(item.title);
59
+            }
60
+          })
61
+        );
62
+      })
63
+    ),
64
+    _react2.default.createElement(
65
+      "div",
66
+      null,
67
+      content[1].map(function (item, index) {
68
+        return _react2.default.createElement(
69
+          "div",
70
+          { className: "item", key: item.value },
71
+          _react2.default.createElement("span", { className: "helper" }),
72
+          _react2.default.createElement("img", {
73
+            src: "" + _emoji.prefixUrl + item.value + "." + _emoji.ext,
74
+            alt: item.title,
75
+            style: { display: "inline-block" },
76
+            onClick: function onClick() {
77
+              return _onClick(item.title);
78
+            }
79
+          })
80
+        );
81
+      })
82
+    ),
83
+    _react2.default.createElement(
84
+      "div",
85
+      null,
86
+      content[2].map(function (item) {
87
+        return _react2.default.createElement(
88
+          "div",
89
+          { className: "item", key: item.value },
90
+          _react2.default.createElement("span", { className: "helper" }),
91
+          _react2.default.createElement("img", {
92
+            src: "" + _emoji.prefixUrl + item.value + "." + _emoji.ext,
93
+            alt: item.title,
94
+            style: { display: "inline-block" },
95
+            onClick: function onClick() {
96
+              return _onClick(item.title);
97
+            }
98
+          })
99
+        );
100
+      })
101
+    )
102
+  );
103
+};
104
+
105
+exports.default = Emoji;
106
+//# sourceMappingURL=Emoji.js.map

+ 1
- 0
lib/components/Editor/Emoji.js.map ファイルの表示

@@ -0,0 +1 @@
1
+{"version":3,"sources":["../../../src/components/Editor/Emoji.js"],"names":["Emoji","onClick","content","i","emoji","length","push","map","item","index","value","prefixUrl","ext","title","display"],"mappings":";;;;;;;;;;;;AAAA;;;;AAEA;;;;AACA;;;;AACA;AACA;;AAEA,IAAMA,QAAQ,SAARA,KAAQ,OAAiB;AAAA,MAAdC,QAAc,QAAdA,OAAc;;AAC7B,MAAMC,UAAU,CAAC,EAAD,EAAK,EAAL,EAAS,EAAT,CAAhB;;AAEA,OAAK,IAAIC,IAAI,CAAb,EAAgBA,IAAIC,gBAAMC,MAA1B,EAAkCF,GAAlC,EAAuC;AACrC,QAAIA,IAAI,EAAR,EAAY;AACVD,cAAQ,CAAR,EAAWI,IAAX,CAAgBF,gBAAMD,CAAN,CAAhB;AACD,KAFD,MAEO,IAAIA,IAAI,EAAR,EAAY;AACjBD,cAAQ,CAAR,EAAWI,IAAX,CAAgBF,gBAAMD,CAAN,CAAhB;AACD,KAFM,MAEA,IAAIA,IAAIC,gBAAMC,MAAd,EAAsB;AAC3BH,cAAQ,CAAR,EAAWI,IAAX,CAAgBF,gBAAMD,CAAN,CAAhB;AACD;AACF;AACD,SACE;AAAA;AAAA;AACE;AAAA;AAAA;AACGD,cAAQ,CAAR,EAAWK,GAAX,CAAe,UAACC,IAAD,EAAOC,KAAP;AAAA,eACd;AAAA;AAAA,YAAK,WAAU,MAAf,EAAsB,KAAKD,KAAKE,KAAhC;AACE,kDAAM,WAAU,QAAhB,GADF;AAEE;AACE,sBAAQC,gBAAR,GAAoBH,KAAKE,KAAzB,SAAkCE,UADpC;AAEE,iBAAKJ,KAAKK,KAFZ;AAGE,mBAAO,EAAEC,SAAS,cAAX,EAHT;AAIE,qBAAS;AAAA,qBAAMb,SAAQO,KAAKK,KAAb,CAAN;AAAA;AAJX;AAFF,SADc;AAAA,OAAf;AADH,KADF;AAcE;AAAA;AAAA;AACGX,cAAQ,CAAR,EAAWK,GAAX,CAAe,UAACC,IAAD,EAAOC,KAAP;AAAA,eACd;AAAA;AAAA,YAAK,WAAU,MAAf,EAAsB,KAAKD,KAAKE,KAAhC;AACE,kDAAM,WAAU,QAAhB,GADF;AAEE;AACE,sBAAQC,gBAAR,GAAoBH,KAAKE,KAAzB,SAAkCE,UADpC;AAEE,iBAAKJ,KAAKK,KAFZ;AAGE,mBAAO,EAAEC,SAAS,cAAX,EAHT;AAIE,qBAAS;AAAA,qBAAMb,SAAQO,KAAKK,KAAb,CAAN;AAAA;AAJX;AAFF,SADc;AAAA,OAAf;AADH,KAdF;AA2BE;AAAA;AAAA;AACGX,cAAQ,CAAR,EAAWK,GAAX,CAAe;AAAA,eACd;AAAA;AAAA,YAAK,WAAU,MAAf,EAAsB,KAAKC,KAAKE,KAAhC;AACE,kDAAM,WAAU,QAAhB,GADF;AAEE;AACE,sBAAQC,gBAAR,GAAoBH,KAAKE,KAAzB,SAAkCE,UADpC;AAEE,iBAAKJ,KAAKK,KAFZ;AAGE,mBAAO,EAAEC,SAAS,cAAX,EAHT;AAIE,qBAAS;AAAA,qBAAMb,SAAQO,KAAKK,KAAb,CAAN;AAAA;AAJX;AAFF,SADc;AAAA,OAAf;AADH;AA3BF,GADF;AA2CD,CAvDD;;kBAyDeb,K","file":"Emoji.js","sourcesContent":["import React from \"react\";\nimport { Carousel } from \"antd\";\nimport emoji, { prefixUrl, ext } from \"../../emoji\";\nimport \"./Emoji.css\";\n// 每页 20  5*4\n// 共 20 * 3 = 60 (实际是 54)\n\nconst Emoji = ({ onClick }) => {\n  const content = [[], [], []];\n\n  for (let i = 0; i < emoji.length; i++) {\n    if (i < 20) {\n      content[0].push(emoji[i]);\n    } else if (i < 40) {\n      content[1].push(emoji[i]);\n    } else if (i < emoji.length) {\n      content[2].push(emoji[i]);\n    }\n  }\n  return (\n    <Carousel>\n      <div>\n        {content[0].map((item, index) => (\n          <div className=\"item\" key={item.value}>\n            <span className=\"helper\" />\n            <img\n              src={`${prefixUrl}${item.value}.${ext}`}\n              alt={item.title}\n              style={{ display: \"inline-block\" }}\n              onClick={() => onClick(item.title)}\n            />\n          </div>\n        ))}\n      </div>\n      <div>\n        {content[1].map((item, index) => (\n          <div className=\"item\" key={item.value}>\n            <span className=\"helper\" />\n            <img\n              src={`${prefixUrl}${item.value}.${ext}`}\n              alt={item.title}\n              style={{ display: \"inline-block\" }}\n              onClick={() => onClick(item.title)}\n            />\n          </div>\n        ))}\n      </div>\n      <div>\n        {content[2].map(item => (\n          <div className=\"item\" key={item.value}>\n            <span className=\"helper\" />\n            <img\n              src={`${prefixUrl}${item.value}.${ext}`}\n              alt={item.title}\n              style={{ display: \"inline-block\" }}\n              onClick={() => onClick(item.title)}\n            />\n          </div>\n        ))}\n      </div>\n    </Carousel>\n  );\n};\n\nexport default Emoji;\n"]}

+ 8
- 0
lib/components/Editor/Upload.css ファイルの表示

@@ -0,0 +1,8 @@
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
+}

+ 205
- 0
lib/components/Editor/Upload.js ファイルの表示

@@ -0,0 +1,205 @@
1
+"use strict";
2
+
3
+Object.defineProperty(exports, "__esModule", {
4
+  value: true
5
+});
6
+
7
+var _modal = require("antd/es/modal");
8
+
9
+var _modal2 = _interopRequireDefault(_modal);
10
+
11
+var _upload = require("antd/es/upload");
12
+
13
+var _upload2 = _interopRequireDefault(_upload);
14
+
15
+var _icon = require("antd/es/icon");
16
+
17
+var _icon2 = _interopRequireDefault(_icon);
18
+
19
+var _message2 = require("antd/es/message");
20
+
21
+var _message3 = _interopRequireDefault(_message2);
22
+
23
+var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
24
+
25
+require("antd/es/modal/style/css");
26
+
27
+require("antd/es/upload/style/css");
28
+
29
+require("antd/es/icon/style/css");
30
+
31
+require("antd/es/message/style/css");
32
+
33
+var _react = require("react");
34
+
35
+var _react2 = _interopRequireDefault(_react);
36
+
37
+var _dayjs = require("dayjs");
38
+
39
+var _dayjs2 = _interopRequireDefault(_dayjs);
40
+
41
+var _shortid = require("shortid");
42
+
43
+var _shortid2 = _interopRequireDefault(_shortid);
44
+
45
+var _constant = require("../../constant");
46
+
47
+var _Comment = require("../../Comment");
48
+
49
+var _Comment2 = _interopRequireDefault(_Comment);
50
+
51
+require("./Upload.css");
52
+
53
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
54
+
55
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
56
+
57
+function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
58
+
59
+function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
60
+// import styles from "./Upload.less";
61
+
62
+
63
+var client = function client(oss) {
64
+  return new window.OSS.Wrapper({
65
+    accessKeyId: oss.access_key_id,
66
+    accessKeySecret: oss.access_key_secret,
67
+    stsToken: oss.security_token,
68
+    endpoint: _constant.OSS_ENDPOINT, //常量,你可以自己定义
69
+    bucket: _constant.OSS_BUCKET
70
+  });
71
+};
72
+
73
+var uploadPath = function uploadPath(path, file) {
74
+  return path + "/" + (0, _dayjs2.default)().format("YYYYMMDD") + "/" + _shortid2.default.generate() + "." + file.type.split("/")[1];
75
+};
76
+
77
+var UploadToOss = function UploadToOss(oss, path, file) {
78
+  var url = uploadPath(path, file);
79
+  return new Promise(function (resolve, reject) {
80
+    client(oss).multipartUpload(url, file).then(function (data) {
81
+      resolve(data);
82
+    }).catch(function (error) {
83
+      reject(error);
84
+    });
85
+  });
86
+};
87
+
88
+var App = function (_React$Component) {
89
+  _inherits(App, _React$Component);
90
+
91
+  function App(props) {
92
+    _classCallCheck(this, App);
93
+
94
+    var _this = _possibleConstructorReturn(this, (App.__proto__ || Object.getPrototypeOf(App)).call(this, props));
95
+
96
+    _this.state = {
97
+      previewVisible: false,
98
+      previewImage: ""
99
+    };
100
+    _this.handleCancel = _this.handleCancel.bind(_this);
101
+    _this.handlePreview = _this.handlePreview.bind(_this);
102
+    _this.handleChange = _this.handleChange.bind(_this);
103
+    _this.customRequest = _this.customRequest.bind(_this);
104
+    return _this;
105
+  }
106
+
107
+  _createClass(App, [{
108
+    key: "componentDidMount",
109
+    value: function componentDidMount() {
110
+      this.props.app.sOssSts();
111
+    }
112
+  }, {
113
+    key: "handleCancel",
114
+    value: function handleCancel() {
115
+      this.setState({ previewVisible: false });
116
+    }
117
+  }, {
118
+    key: "handlePreview",
119
+    value: function handlePreview(file) {
120
+      this.setState({
121
+        previewImage: file.url || file.thumbUrl,
122
+        previewVisible: true
123
+      });
124
+    }
125
+  }, {
126
+    key: "handleChange",
127
+    value: function handleChange(_ref) {
128
+      var fileList = _ref.fileList;
129
+
130
+      this.props.onChangeFileList(fileList);
131
+    }
132
+  }, {
133
+    key: "customRequest",
134
+    value: function customRequest(info) {
135
+      var _this2 = this;
136
+
137
+      var file = info.file;
138
+
139
+      info.onProgress({ percent: 10 });
140
+      var reader = new FileReader();
141
+      reader.readAsDataURL(info.file);
142
+      reader.onloadend = function () {
143
+        info.onProgress({ percent: 20 });
144
+        // DRIVER_LICENSE_PATH oss 的存储路径位置
145
+        UploadToOss(_this2.props.app.oss, _constant.DRIVER_LICENSE_PATH, file).then(function (data) {
146
+          info.onProgress({ percent: 100 });
147
+          info.onSuccess();
148
+          _this2.props.onUpload({ path: data.name, uid: file.uid });
149
+        }).catch(function (e) {
150
+          _message3.default.error(e.message || _constant.ERROR_DEFAULT);
151
+          info.onError(e);
152
+        });
153
+      };
154
+    }
155
+  }, {
156
+    key: "render",
157
+    value: function render() {
158
+      var _state = this.state,
159
+          previewVisible = _state.previewVisible,
160
+          previewImage = _state.previewImage;
161
+      var fileList = this.props.fileList;
162
+
163
+      var uploadButton = _react2.default.createElement(
164
+        "div",
165
+        null,
166
+        _react2.default.createElement(_icon2.default, { type: "plus" }),
167
+        _react2.default.createElement(
168
+          "div",
169
+          { className: "ant-upload-text" },
170
+          "\u4E0A\u4F20"
171
+        )
172
+      );
173
+      return _react2.default.createElement(
174
+        "div",
175
+        null,
176
+        _react2.default.createElement(
177
+          _upload2.default,
178
+          {
179
+            accept: "image/jpg,image/jpeg,image/png,image/bmp",
180
+            listType: "picture-card",
181
+            fileList: fileList,
182
+            customRequest: this.customRequest,
183
+            onPreview: this.handlePreview,
184
+            onChange: this.handleChange
185
+          },
186
+          fileList.length >= _constant.MAX_UPLOAD_NUMBER ? null : uploadButton
187
+        ),
188
+        _react2.default.createElement(
189
+          _modal2.default,
190
+          {
191
+            visible: previewVisible,
192
+            footer: null,
193
+            onCancel: this.handleCancel
194
+          },
195
+          _react2.default.createElement("img", { alt: "upload", style: { width: "100%" }, src: previewImage })
196
+        )
197
+      );
198
+    }
199
+  }]);
200
+
201
+  return App;
202
+}(_react2.default.Component);
203
+
204
+exports.default = (0, _Comment2.default)(App);
205
+//# sourceMappingURL=Upload.js.map

+ 1
- 0
lib/components/Editor/Upload.js.map
ファイル差分が大きすぎるため省略します
ファイルの表示


+ 82
- 0
lib/components/Editor/index.css ファイルの表示

@@ -0,0 +1,82 @@
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
+}

+ 213
- 0
lib/components/Editor/index.js ファイルの表示

@@ -0,0 +1,213 @@
1
+"use strict";
2
+
3
+Object.defineProperty(exports, "__esModule", {
4
+  value: true
5
+});
6
+
7
+var _button = require("antd/es/button");
8
+
9
+var _button2 = _interopRequireDefault(_button);
10
+
11
+var _popover = require("antd/es/popover");
12
+
13
+var _popover2 = _interopRequireDefault(_popover);
14
+
15
+var _icon = require("antd/es/icon");
16
+
17
+var _icon2 = _interopRequireDefault(_icon);
18
+
19
+var _input = require("antd/es/input");
20
+
21
+var _input2 = _interopRequireDefault(_input);
22
+
23
+var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
24
+
25
+require("antd/es/button/style/css");
26
+
27
+require("antd/es/popover/style/css");
28
+
29
+require("antd/es/icon/style/css");
30
+
31
+require("antd/es/input/style/css");
32
+
33
+var _react = require("react");
34
+
35
+var _react2 = _interopRequireDefault(_react);
36
+
37
+var _constant = require("../../constant");
38
+
39
+var _Upload = require("./Upload");
40
+
41
+var _Upload2 = _interopRequireDefault(_Upload);
42
+
43
+var _Emoji = require("./Emoji");
44
+
45
+var _Emoji2 = _interopRequireDefault(_Emoji);
46
+
47
+require("./index.css");
48
+
49
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
50
+
51
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
52
+
53
+function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
54
+
55
+function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } // https://github.com/lovasoa/react-contenteditable/blob/master/src/react-contenteditable.js
56
+
57
+// import styles from "./index.less";
58
+
59
+
60
+var TextArea = _input2.default.TextArea;
61
+
62
+var Editor = function (_React$Component) {
63
+  _inherits(Editor, _React$Component);
64
+
65
+  function Editor(props) {
66
+    _classCallCheck(this, Editor);
67
+
68
+    var _this = _possibleConstructorReturn(this, (Editor.__proto__ || Object.getPrototypeOf(Editor)).call(this, props));
69
+
70
+    _this.state = {
71
+      showUpload: false
72
+    };
73
+    _this.handleClickEmoji = _this.handleClickEmoji.bind(_this);
74
+    _this.handleShowUpload = _this.handleShowUpload.bind(_this);
75
+    return _this;
76
+  }
77
+
78
+  _createClass(Editor, [{
79
+    key: "componentDidMount",
80
+    value: function componentDidMount() {}
81
+  }, {
82
+    key: "handleClickEmoji",
83
+    value: function handleClickEmoji(emojiId) {
84
+      this.props.onChangeEmoji(emojiId);
85
+    }
86
+  }, {
87
+    key: "handleShowUpload",
88
+    value: function handleShowUpload(showUpload) {
89
+      if (typeof showUpload === "boolean") {
90
+        this.setState({ showUpload: showUpload });
91
+      } else {
92
+        this.setState({ showUpload: !this.state.showUpload });
93
+      }
94
+    }
95
+  }, {
96
+    key: "render",
97
+    value: function render() {
98
+      var _this2 = this;
99
+
100
+      var _props = this.props,
101
+          value = _props.value,
102
+          onChange = _props.onChange,
103
+          placeholder = _props.placeholder,
104
+          fileList = _props.fileList,
105
+          onChangeFileList = _props.onChangeFileList,
106
+          onUpload = _props.onUpload;
107
+
108
+      return _react2.default.createElement(
109
+        "div",
110
+        { className: "editor" },
111
+        _react2.default.createElement(TextArea, {
112
+          value: value,
113
+          onChange: onChange,
114
+          rows: 5,
115
+          placeholder: placeholder
116
+        }),
117
+        _react2.default.createElement(
118
+          "div",
119
+          { className: "toolbar" },
120
+          _react2.default.createElement(
121
+            "div",
122
+            { style: { float: "left", margin: "8px 11px" } },
123
+            _react2.default.createElement(
124
+              _popover2.default,
125
+              {
126
+                trigger: "click",
127
+                placement: "bottomLeft",
128
+                autoAdjustOverflow: false,
129
+                content: _react2.default.createElement(
130
+                  "div",
131
+                  { style: { width: 200 } },
132
+                  _react2.default.createElement(_Emoji2.default, { onClick: this.handleClickEmoji })
133
+                ),
134
+                overlayClassName: "feed"
135
+              },
136
+              _react2.default.createElement(_icon2.default, { type: "smile-o", className: "icon" })
137
+            ),
138
+            _react2.default.createElement(
139
+              _popover2.default,
140
+              {
141
+                visible: this.state.showUpload,
142
+                overlayStyle: { zIndex: 999 },
143
+                content: _react2.default.createElement(
144
+                  "div",
145
+                  { style: { width: 112 * _constant.MAX_UPLOAD_NUMBER, minHeight: 100 } },
146
+                  _react2.default.createElement(_Upload2.default, {
147
+                    onChangeFileList: onChangeFileList,
148
+                    onUpload: onUpload,
149
+                    fileList: fileList
150
+                  })
151
+                ),
152
+                placement: "bottomLeft",
153
+                title: _react2.default.createElement(
154
+                  "div",
155
+                  { style: { margin: "5px auto" } },
156
+                  _react2.default.createElement(
157
+                    "span",
158
+                    null,
159
+                    "\u4E0A\u4F20\u56FE\u7247",
160
+                    _react2.default.createElement(
161
+                      "span",
162
+                      { style: { color: "#666", fontWeight: 400 } },
163
+                      "(\u60A8\u8FD8\u80FD\u4E0A\u4F20",
164
+                      _constant.MAX_UPLOAD_NUMBER - fileList.length,
165
+                      "\u5F20\u56FE\u7247)"
166
+                    )
167
+                  ),
168
+                  _react2.default.createElement(_icon2.default, {
169
+                    type: "close",
170
+                    onClick: function onClick() {
171
+                      return _this2.handleShowUpload(false);
172
+                    },
173
+                    style: {
174
+                      float: "right",
175
+                      cursor: "pointer",
176
+                      marginTop: 4
177
+                    }
178
+                  })
179
+                )
180
+              },
181
+              _react2.default.createElement(_icon2.default, {
182
+                type: "picture",
183
+                className: "icon",
184
+                style: { marginLeft: 10 },
185
+                onClick: function onClick() {
186
+                  return _this2.handleShowUpload(true);
187
+                }
188
+              })
189
+            )
190
+          ),
191
+          _react2.default.createElement(
192
+            "div",
193
+            { style: { float: "right", margin: "5px 11px" } },
194
+            _react2.default.createElement(
195
+              _button2.default,
196
+              {
197
+                onClick: this.props.onSubmit,
198
+                type: "primary",
199
+                loading: this.props.loading
200
+              },
201
+              "\u53D1\u8868"
202
+            )
203
+          )
204
+        )
205
+      );
206
+    }
207
+  }]);
208
+
209
+  return Editor;
210
+}(_react2.default.Component);
211
+
212
+exports.default = Editor;
213
+//# sourceMappingURL=index.js.map

+ 1
- 0
lib/components/Editor/index.js.map
ファイル差分が大きすぎるため省略します
ファイルの表示


+ 22
- 0
lib/constant.js ファイルの表示

@@ -0,0 +1,22 @@
1
+"use strict";
2
+
3
+Object.defineProperty(exports, "__esModule", {
4
+  value: true
5
+});
6
+var URL = exports.URL = "http://121.41.20.11:8082/v1";
7
+// export const URL = "http://api.links123.net/comment/v1";
8
+
9
+var ERROR_DEFAULT = exports.ERROR_DEFAULT = "出错了!";
10
+
11
+var LIMIT = exports.LIMIT = 10; // 默认 limit
12
+
13
+var OSS_ENDPOINT = exports.OSS_ENDPOINT = "oss-cn-beijing.aliyuncs.com";
14
+var OSS_BUCKET = exports.OSS_BUCKET = "links-comment";
15
+var DRIVER_LICENSE_PATH = exports.DRIVER_LICENSE_PATH = "/comment";
16
+
17
+var OSS_LINK = exports.OSS_LINK = "http://links-comment.oss-cn-beijing.aliyuncs.com";
18
+
19
+var MAX_UPLOAD_NUMBER = exports.MAX_UPLOAD_NUMBER = 4;
20
+
21
+var REGEXP = exports.REGEXP = /\[.+?\]/g;
22
+//# sourceMappingURL=constant.js.map

+ 1
- 0
lib/constant.js.map ファイルの表示

@@ -0,0 +1 @@
1
+{"version":3,"sources":["../src/constant.js"],"names":["URL","ERROR_DEFAULT","LIMIT","OSS_ENDPOINT","OSS_BUCKET","DRIVER_LICENSE_PATH","OSS_LINK","MAX_UPLOAD_NUMBER","REGEXP"],"mappings":";;;;;AAAO,IAAMA,oBAAM,6BAAZ;AACP;;AAEO,IAAMC,wCAAgB,MAAtB;;AAEA,IAAMC,wBAAQ,EAAd,C,CAAkB;;AAElB,IAAMC,sCAAe,6BAArB;AACA,IAAMC,kCAAa,eAAnB;AACA,IAAMC,oDAAsB,UAA5B;;AAEA,IAAMC,8BAAW,kDAAjB;;AAEA,IAAMC,gDAAoB,CAA1B;;AAEA,IAAMC,0BAAS,UAAf","file":"constant.js","sourcesContent":["export const URL = \"http://121.41.20.11:8082/v1\";\n// export const URL = \"http://api.links123.net/comment/v1\";\n\nexport const ERROR_DEFAULT = \"出错了!\";\n\nexport const LIMIT = 10; // 默认 limit\n\nexport const OSS_ENDPOINT = \"oss-cn-beijing.aliyuncs.com\";\nexport const OSS_BUCKET = \"links-comment\";\nexport const DRIVER_LICENSE_PATH = \"/comment\";\n\nexport const OSS_LINK = \"http://links-comment.oss-cn-beijing.aliyuncs.com\";\n\nexport const MAX_UPLOAD_NUMBER = 4;\n\nexport const REGEXP = /\\[.+?\\]/g;\n"]}

+ 175
- 0
lib/emoji.js ファイルの表示

@@ -0,0 +1,175 @@
1
+"use strict";
2
+
3
+Object.defineProperty(exports, "__esModule", {
4
+  value: true
5
+});
6
+var emoji = [{
7
+  value: "0",
8
+  title: "捶地"
9
+}, {
10
+  value: "1",
11
+  title: "怀疑"
12
+}, {
13
+  value: "2",
14
+  title: "撇嘴"
15
+}, {
16
+  value: "3",
17
+  title: "色"
18
+}, {
19
+  value: "4",
20
+  ttile: "发呆"
21
+}, {
22
+  value: "5",
23
+  title: "酷"
24
+}, {
25
+  value: "6",
26
+  title: "害羞"
27
+}, {
28
+  value: "7",
29
+  title: "闭嘴"
30
+}, {
31
+  value: "8",
32
+  title: "睡觉"
33
+}, {
34
+  value: "9",
35
+  title: "大哭"
36
+}, {
37
+  value: "10",
38
+  title: "尴尬"
39
+}, {
40
+  value: "11",
41
+  title: "发怒"
42
+}, {
43
+  value: "12",
44
+  title: "调皮"
45
+}, {
46
+  value: "13",
47
+  title: "呲牙"
48
+}, {
49
+  value: "14",
50
+  title: "微笑"
51
+}, {
52
+  value: "15",
53
+  title: "难过"
54
+}, {
55
+  value: "16",
56
+  title: "帅呆"
57
+}, {
58
+  value: "17",
59
+  title: "折磨"
60
+}, {
61
+  value: "18",
62
+  title: "吐"
63
+}, {
64
+  value: "19",
65
+  title: "偷笑"
66
+}, {
67
+  value: "20",
68
+  title: "可爱"
69
+}, {
70
+  value: "21",
71
+  title: "白眼"
72
+}, {
73
+  value: "22",
74
+  title: "傲慢"
75
+}, {
76
+  value: "23",
77
+  title: "嘴馋"
78
+}, {
79
+  value: "24",
80
+  title: "困"
81
+}, {
82
+  value: "25",
83
+  title: "恐慌"
84
+}, {
85
+  value: "26",
86
+  title: "流汗"
87
+}, {
88
+  value: "27",
89
+  title: "憨笑"
90
+}, {
91
+  value: "28",
92
+  title: "大兵"
93
+}, {
94
+  value: "29",
95
+  title: "奋斗"
96
+}, {
97
+  value: "30",
98
+  title: "疑问"
99
+}, {
100
+  value: "31",
101
+  title: "嘘"
102
+}, {
103
+  value: "32",
104
+  title: "晕"
105
+}, {
106
+  value: "33",
107
+  title: "被炸了"
108
+}, {
109
+  value: "34",
110
+  title: "骷髅"
111
+}, {
112
+  value: "35",
113
+  title: "敲打"
114
+}, {
115
+  value: "36",
116
+  title: "拜拜"
117
+}, {
118
+  value: "37",
119
+  title: "发抖"
120
+}, {
121
+  value: "38",
122
+  title: "亲密"
123
+}, {
124
+  value: "39",
125
+  title: "跳"
126
+}, {
127
+  value: "40",
128
+  title: "猪头"
129
+}, {
130
+  value: "41",
131
+  title: "拥抱"
132
+}, {
133
+  value: "42",
134
+  title: "生日蛋糕"
135
+}, {
136
+  value: "43",
137
+  title: "闪电"
138
+}, {
139
+  value: "44",
140
+  title: "地雷"
141
+}, {
142
+  value: "45",
143
+  title: "刀"
144
+}, {
145
+  value: "46",
146
+  title: "足球"
147
+}, {
148
+  value: "47",
149
+  title: "便便"
150
+}, {
151
+  value: "48",
152
+  title: "咖啡"
153
+}, {
154
+  value: "49",
155
+  title: "米饭"
156
+}, {
157
+  value: "50",
158
+  title: "玫瑰"
159
+}, {
160
+  value: "51",
161
+  title: "枯萎玫瑰"
162
+}, {
163
+  value: "52",
164
+  title: "爱心"
165
+}, {
166
+  value: "53",
167
+  title: "心碎"
168
+}];
169
+
170
+var prefixUrl = exports.prefixUrl = "https://a.links123.cn/site/src/feed/imgs/qq/";
171
+
172
+var ext = exports.ext = "gif";
173
+
174
+exports.default = emoji;
175
+//# sourceMappingURL=emoji.js.map

+ 1
- 0
lib/emoji.js.map
ファイル差分が大きすぎるため省略します
ファイルの表示


+ 57
- 0
lib/helper.js ファイルの表示

@@ -0,0 +1,57 @@
1
+"use strict";
2
+
3
+Object.defineProperty(exports, "__esModule", {
4
+  value: true
5
+});
6
+exports.isFunction = isFunction;
7
+exports.isUrl = isUrl;
8
+exports.arrayToObject = arrayToObject;
9
+exports.renderContent = renderContent;
10
+
11
+var _constant = require("./constant");
12
+
13
+var _emoji = require("./emoji");
14
+
15
+var _emoji2 = _interopRequireDefault(_emoji);
16
+
17
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
18
+
19
+function isFunction(functionToCheck) {
20
+  return functionToCheck && {}.toString.call(functionToCheck) === "[object Function]";
21
+}
22
+
23
+function isUrl(userInput) {
24
+  var regexp = /(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&//=]*)/g;
25
+  var res = userInput.match(regexp);
26
+  if (res === null) return false;else return true;
27
+}
28
+
29
+/**
30
+ * 将对象数组转换为对象
31
+ * @param {array} array Array of Objects
32
+ * @param {string} keyField string
33
+ */
34
+function arrayToObject(array, keyField) {
35
+  return array.reduce(function (obj, item) {
36
+    obj[item[keyField]] = item;
37
+    return obj;
38
+  }, {});
39
+}
40
+
41
+/**
42
+ * 渲染编辑器
43
+ * [x] => <img src="x" />
44
+ * @param {strig} content
45
+ */
46
+function renderContent(content, onClick) {
47
+  return content.replace(_constant.REGEXP, function (a, b) {
48
+    var src = a.slice(1, -1);
49
+    if (isUrl(src)) {
50
+      return "<img src=\"" + src + "\" alt=\"" + src + "\" style=\"max-width: 300px\" />";
51
+    }
52
+    var emojiObejct = arrayToObject(_emoji2.default, "title");
53
+    var value = emojiObejct[src].value;
54
+    return "<img src=\"" + _emoji.prefixUrl + value + "." + _emoji.ext + "\" alt=\"" + value + "\" />";
55
+  });
56
+}
57
+//# sourceMappingURL=helper.js.map

+ 1
- 0
lib/helper.js.map ファイルの表示

@@ -0,0 +1 @@
1
+{"version":3,"sources":["../src/helper.js"],"names":["isFunction","isUrl","arrayToObject","renderContent","functionToCheck","toString","call","userInput","regexp","res","match","array","keyField","reduce","obj","item","content","onClick","replace","REGEXP","a","b","src","slice","emojiObejct","emoji","value","prefixUrl","ext"],"mappings":";;;;;QAGgBA,U,GAAAA,U;QAMAC,K,GAAAA,K;QAYAC,a,GAAAA,a;QAYAC,a,GAAAA,a;;AAjChB;;AACA;;;;;;AAEO,SAASH,UAAT,CAAoBI,eAApB,EAAqC;AAC1C,SACEA,mBAAmB,GAAGC,QAAH,CAAYC,IAAZ,CAAiBF,eAAjB,MAAsC,mBAD3D;AAGD;;AAEM,SAASH,KAAT,CAAeM,SAAf,EAA0B;AAC/B,MAAMC,SAAS,kGAAf;AACA,MAAIC,MAAMF,UAAUG,KAAV,CAAgBF,MAAhB,CAAV;AACA,MAAIC,QAAQ,IAAZ,EAAkB,OAAO,KAAP,CAAlB,KACK,OAAO,IAAP;AACN;;AAED;;;;;AAKO,SAASP,aAAT,CAAuBS,KAAvB,EAA8BC,QAA9B,EAAwC;AAC7C,SAAOD,MAAME,MAAN,CAAa,UAACC,GAAD,EAAMC,IAAN,EAAe;AACjCD,QAAIC,KAAKH,QAAL,CAAJ,IAAsBG,IAAtB;AACA,WAAOD,GAAP;AACD,GAHM,EAGJ,EAHI,CAAP;AAID;;AAED;;;;;AAKO,SAASX,aAAT,CAAuBa,OAAvB,EAAgCC,OAAhC,EAAyC;AAC9C,SAAOD,QAAQE,OAAR,CAAgBC,gBAAhB,EAAwB,UAASC,CAAT,EAAYC,CAAZ,EAAe;AAC5C,QAAMC,MAAMF,EAAEG,KAAF,CAAQ,CAAR,EAAW,CAAC,CAAZ,CAAZ;AACA,QAAItB,MAAMqB,GAAN,CAAJ,EAAgB;AACd,6BAAoBA,GAApB,iBAAiCA,GAAjC;AACD;AACD,QAAME,cAActB,cAAcuB,eAAd,EAAqB,OAArB,CAApB;AACA,QAAMC,QAAQF,YAAYF,GAAZ,EAAiBI,KAA/B;AACA,2BAAoBC,gBAApB,GAAgCD,KAAhC,SAAyCE,UAAzC,iBAAsDF,KAAtD;AACD,GARM,CAAP;AASD","file":"helper.js","sourcesContent":["import { REGEXP } from \"./constant\";\nimport emoji, { prefixUrl, ext } from \"./emoji\";\n\nexport function isFunction(functionToCheck) {\n  return (\n    functionToCheck && {}.toString.call(functionToCheck) === \"[object Function]\"\n  );\n}\n\nexport function isUrl(userInput) {\n  const regexp = /(http(s)?:\\/\\/.)?(www\\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%_+.~#?&//=]*)/g;\n  var res = userInput.match(regexp);\n  if (res === null) return false;\n  else return true;\n}\n\n/**\n * 将对象数组转换为对象\n * @param {array} array Array of Objects\n * @param {string} keyField string\n */\nexport function arrayToObject(array, keyField) {\n  return array.reduce((obj, item) => {\n    obj[item[keyField]] = item;\n    return obj;\n  }, {});\n}\n\n/**\n * 渲染编辑器\n * [x] => <img src=\"x\" />\n * @param {strig} content\n */\nexport function renderContent(content, onClick) {\n  return content.replace(REGEXP, function(a, b) {\n    const src = a.slice(1, -1);\n    if (isUrl(src)) {\n      return `<img src=\"${src}\" alt=\"${src}\" style=\"max-width: 300px\" />`;\n    }\n    const emojiObejct = arrayToObject(emoji, \"title\");\n    const value = emojiObejct[src].value;\n    return `<img src=\"${prefixUrl}${value}.${ext}\" alt=\"${value}\" />`;\n  });\n}\n"]}

+ 23
- 0
lib/index.js ファイルの表示

@@ -0,0 +1,23 @@
1
+"use strict";
2
+
3
+var _react = require("react");
4
+
5
+var _react2 = _interopRequireDefault(_react);
6
+
7
+var _reactDom = require("react-dom");
8
+
9
+var _reactDom2 = _interopRequireDefault(_reactDom);
10
+
11
+var _App = require("./App");
12
+
13
+var _App2 = _interopRequireDefault(_App);
14
+
15
+var _registerServiceWorker = require("./registerServiceWorker");
16
+
17
+var _registerServiceWorker2 = _interopRequireDefault(_registerServiceWorker);
18
+
19
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
20
+
21
+_reactDom2.default.render(_react2.default.createElement(_App2.default, null), document.getElementById("root-comment"));
22
+(0, _registerServiceWorker2.default)();
23
+//# sourceMappingURL=index.js.map

+ 1
- 0
lib/index.js.map ファイルの表示

@@ -0,0 +1 @@
1
+{"version":3,"sources":["../src/index.js"],"names":["ReactDOM","render","document","getElementById"],"mappings":";;AAAA;;;;AACA;;;;AACA;;;;AACA;;;;;;AAEAA,mBAASC,MAAT,CAAgB,8BAAC,aAAD,OAAhB,EAAyBC,SAASC,cAAT,CAAwB,cAAxB,CAAzB;AACA","file":"index.js","sourcesContent":["import React from \"react\";\nimport ReactDOM from \"react-dom\";\nimport App from \"./App\";\nimport registerServiceWorker from \"./registerServiceWorker\";\n\nReactDOM.render(<App />, document.getElementById(\"root-comment\"));\nregisterServiceWorker();\n"]}

+ 72
- 0
lib/mock.js ファイルの表示

@@ -0,0 +1,72 @@
1
+"use strict";
2
+
3
+Object.defineProperty(exports, "__esModule", {
4
+  value: true
5
+});
6
+var comments = exports.comments = {
7
+  list: [{
8
+    id: "5b3a4f0973df3e6a32b0a9d2",
9
+    user_id: 0,
10
+    user_name: "",
11
+    user_avatar: "",
12
+    content: "test[微笑]",
13
+    replies: [{
14
+      id: "5b3c58c473df3e64d8d53afb",
15
+      user_id: 0,
16
+      user_name: "",
17
+      user_avatar: "",
18
+      reply: null,
19
+      content: "test",
20
+      favor_count: 1,
21
+      favored: false,
22
+      created: 1530681540
23
+    }, {
24
+      id: "5b3c58c473df3e64d8d53afa",
25
+      user_id: 0,
26
+      user_name: "",
27
+      user_avatar: "",
28
+      reply: {
29
+        id: "5b3c58c473df3e64d8d53afb",
30
+        user_id: 0,
31
+        user_name: "",
32
+        user_avatar: "",
33
+        reply: null,
34
+        content: "test",
35
+        favor_count: 1,
36
+        favored: false,
37
+        created: 1530681540
38
+      },
39
+      content: "test",
40
+      favor_count: 2,
41
+      favored: false,
42
+      created: 1530681540
43
+    }, {
44
+      id: "5b3c6e4873df3e53f870d912",
45
+      user_id: 0,
46
+      user_name: "",
47
+      user_avatar: "",
48
+      reply: {
49
+        id: "5b3c58c473df3e64d8d53afb",
50
+        user_id: 0,
51
+        user_name: "",
52
+        user_avatar: "",
53
+        reply: null,
54
+        content: "test",
55
+        favor_count: 1,
56
+        favored: false,
57
+        created: 1530681540
58
+      },
59
+      content: "test",
60
+      favor_count: 0,
61
+      favored: false,
62
+      created: 1530687048
63
+    }],
64
+    reply_count: 4,
65
+    favor_count: 0,
66
+    favored: false,
67
+    created: 1530547977
68
+  }],
69
+  total: 1,
70
+  page: 1
71
+};
72
+//# sourceMappingURL=mock.js.map

+ 1
- 0
lib/mock.js.map ファイルの表示

@@ -0,0 +1 @@
1
+{"version":3,"sources":["../src/mock.js"],"names":["comments","list","id","user_id","user_name","user_avatar","content","replies","reply","favor_count","favored","created","reply_count","total","page"],"mappings":";;;;;AAAO,IAAMA,8BAAW;AACtBC,QAAM,CACJ;AACEC,QAAI,0BADN;AAEEC,aAAS,CAFX;AAGEC,eAAW,EAHb;AAIEC,iBAAa,EAJf;AAKEC,aAAS,UALX;AAMEC,aAAS,CACP;AACEL,UAAI,0BADN;AAEEC,eAAS,CAFX;AAGEC,iBAAW,EAHb;AAIEC,mBAAa,EAJf;AAKEG,aAAO,IALT;AAMEF,eAAS,MANX;AAOEG,mBAAa,CAPf;AAQEC,eAAS,KARX;AASEC,eAAS;AATX,KADO,EAYP;AACET,UAAI,0BADN;AAEEC,eAAS,CAFX;AAGEC,iBAAW,EAHb;AAIEC,mBAAa,EAJf;AAKEG,aAAO;AACLN,YAAI,0BADC;AAELC,iBAAS,CAFJ;AAGLC,mBAAW,EAHN;AAILC,qBAAa,EAJR;AAKLG,eAAO,IALF;AAMLF,iBAAS,MANJ;AAOLG,qBAAa,CAPR;AAQLC,iBAAS,KARJ;AASLC,iBAAS;AATJ,OALT;AAgBEL,eAAS,MAhBX;AAiBEG,mBAAa,CAjBf;AAkBEC,eAAS,KAlBX;AAmBEC,eAAS;AAnBX,KAZO,EAiCP;AACET,UAAI,0BADN;AAEEC,eAAS,CAFX;AAGEC,iBAAW,EAHb;AAIEC,mBAAa,EAJf;AAKEG,aAAO;AACLN,YAAI,0BADC;AAELC,iBAAS,CAFJ;AAGLC,mBAAW,EAHN;AAILC,qBAAa,EAJR;AAKLG,eAAO,IALF;AAMLF,iBAAS,MANJ;AAOLG,qBAAa,CAPR;AAQLC,iBAAS,KARJ;AASLC,iBAAS;AATJ,OALT;AAgBEL,eAAS,MAhBX;AAiBEG,mBAAa,CAjBf;AAkBEC,eAAS,KAlBX;AAmBEC,eAAS;AAnBX,KAjCO,CANX;AA6DEC,iBAAa,CA7Df;AA8DEH,iBAAa,CA9Df;AA+DEC,aAAS,KA/DX;AAgEEC,aAAS;AAhEX,GADI,CADgB;AAqEtBE,SAAO,CArEe;AAsEtBC,QAAM;AAtEgB,CAAjB","file":"mock.js","sourcesContent":["export const comments = {\n  list: [\n    {\n      id: \"5b3a4f0973df3e6a32b0a9d2\",\n      user_id: 0,\n      user_name: \"\",\n      user_avatar: \"\",\n      content: \"test[微笑]\",\n      replies: [\n        {\n          id: \"5b3c58c473df3e64d8d53afb\",\n          user_id: 0,\n          user_name: \"\",\n          user_avatar: \"\",\n          reply: null,\n          content: \"test\",\n          favor_count: 1,\n          favored: false,\n          created: 1530681540\n        },\n        {\n          id: \"5b3c58c473df3e64d8d53afa\",\n          user_id: 0,\n          user_name: \"\",\n          user_avatar: \"\",\n          reply: {\n            id: \"5b3c58c473df3e64d8d53afb\",\n            user_id: 0,\n            user_name: \"\",\n            user_avatar: \"\",\n            reply: null,\n            content: \"test\",\n            favor_count: 1,\n            favored: false,\n            created: 1530681540\n          },\n          content: \"test\",\n          favor_count: 2,\n          favored: false,\n          created: 1530681540\n        },\n        {\n          id: \"5b3c6e4873df3e53f870d912\",\n          user_id: 0,\n          user_name: \"\",\n          user_avatar: \"\",\n          reply: {\n            id: \"5b3c58c473df3e64d8d53afb\",\n            user_id: 0,\n            user_name: \"\",\n            user_avatar: \"\",\n            reply: null,\n            content: \"test\",\n            favor_count: 1,\n            favored: false,\n            created: 1530681540\n          },\n          content: \"test\",\n          favor_count: 0,\n          favored: false,\n          created: 1530687048\n        }\n      ],\n      reply_count: 4,\n      favor_count: 0,\n      favored: false,\n      created: 1530547977\n    }\n  ],\n  total: 1,\n  page: 1\n};\n"]}

+ 108
- 0
lib/registerServiceWorker.js ファイルの表示

@@ -0,0 +1,108 @@
1
+"use strict";
2
+
3
+Object.defineProperty(exports, "__esModule", {
4
+  value: true
5
+});
6
+exports.default = register;
7
+exports.unregister = unregister;
8
+// In production, we register a service worker to serve assets from local cache.
9
+
10
+// This lets the app load faster on subsequent visits in production, and gives
11
+// it offline capabilities. However, it also means that developers (and users)
12
+// will only see deployed updates on the "N+1" visit to a page, since previously
13
+// cached resources are updated in the background.
14
+
15
+// To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
16
+// This link also includes instructions on opting out of this behavior.
17
+
18
+var isLocalhost = Boolean(window.location.hostname === "localhost" ||
19
+// [::1] is the IPv6 localhost address.
20
+window.location.hostname === "[::1]" ||
21
+// 127.0.0.1/8 is considered localhost for IPv4.
22
+window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/));
23
+
24
+function register() {
25
+  if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) {
26
+    // The URL constructor is available in all browsers that support SW.
27
+    var publicUrl = new URL(process.env.PUBLIC_URL, window.location);
28
+    if (publicUrl.origin !== window.location.origin) {
29
+      // Our service worker won't work if PUBLIC_URL is on a different origin
30
+      // from what our page is served on. This might happen if a CDN is used to
31
+      // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
32
+      return;
33
+    }
34
+
35
+    window.addEventListener("load", function () {
36
+      var swUrl = process.env.PUBLIC_URL + "/service-worker.js";
37
+
38
+      if (isLocalhost) {
39
+        // This is running on localhost. Lets check if a service worker still exists or not.
40
+        checkValidServiceWorker(swUrl);
41
+
42
+        // Add some additional logging to localhost, pointing developers to the
43
+        // service worker/PWA documentation.
44
+        navigator.serviceWorker.ready.then(function () {
45
+          console.log("This web app is being served cache-first by a service " + "worker. To learn more, visit https://goo.gl/SC7cgQ");
46
+        });
47
+      } else {
48
+        // Is not local host. Just register service worker
49
+        registerValidSW(swUrl);
50
+      }
51
+    });
52
+  }
53
+}
54
+
55
+function registerValidSW(swUrl) {
56
+  navigator.serviceWorker.register(swUrl).then(function (registration) {
57
+    registration.onupdatefound = function () {
58
+      var installingWorker = registration.installing;
59
+      installingWorker.onstatechange = function () {
60
+        if (installingWorker.state === "installed") {
61
+          if (navigator.serviceWorker.controller) {
62
+            // At this point, the old content will have been purged and
63
+            // the fresh content will have been added to the cache.
64
+            // It's the perfect time to display a "New content is
65
+            // available; please refresh." message in your web app.
66
+            console.log("New content is available; please refresh.");
67
+          } else {
68
+            // At this point, everything has been precached.
69
+            // It's the perfect time to display a
70
+            // "Content is cached for offline use." message.
71
+            console.log("Content is cached for offline use.");
72
+          }
73
+        }
74
+      };
75
+    };
76
+  }).catch(function (error) {
77
+    console.error("Error during service worker registration:", error);
78
+  });
79
+}
80
+
81
+function checkValidServiceWorker(swUrl) {
82
+  // Check if the service worker can be found. If it can't reload the page.
83
+  fetch(swUrl).then(function (response) {
84
+    // Ensure service worker exists, and that we really are getting a JS file.
85
+    if (response.status === 404 || response.headers.get("content-type").indexOf("javascript") === -1) {
86
+      // No service worker found. Probably a different app. Reload the page.
87
+      navigator.serviceWorker.ready.then(function (registration) {
88
+        registration.unregister().then(function () {
89
+          window.location.reload();
90
+        });
91
+      });
92
+    } else {
93
+      // Service worker found. Proceed as normal.
94
+      registerValidSW(swUrl);
95
+    }
96
+  }).catch(function () {
97
+    console.log("No internet connection found. App is running in offline mode.");
98
+  });
99
+}
100
+
101
+function unregister() {
102
+  if ("serviceWorker" in navigator) {
103
+    navigator.serviceWorker.ready.then(function (registration) {
104
+      registration.unregister();
105
+    });
106
+  }
107
+}
108
+//# sourceMappingURL=registerServiceWorker.js.map

+ 1
- 0
lib/registerServiceWorker.js.map
ファイル差分が大きすぎるため省略します
ファイルの表示


+ 52
- 0
package.json ファイルの表示

@@ -0,0 +1,52 @@
1
+{
2
+  "name": "comment",
3
+  "version": "0.0.1",
4
+  "main": "lib/App.js",
5
+  "description": "通用评论",
6
+  "keywords": [
7
+    "评论"
8
+  ],
9
+  "author": "node",
10
+  "email": "nodejh@qq.com",
11
+  "homepage": "https://git.links123.net/node/test_comment",
12
+  "bugs": {
13
+    "url": "https://git.links123.net/node/test_comment/issues"
14
+  },
15
+  "private": true,
16
+  "dependencies": {
17
+    "antd": "^3.6.6",
18
+    "axios": "^0.18.0",
19
+    "dayjs": "^1.7.2",
20
+    "react": "^16.4.1",
21
+    "react-dom": "^16.4.1",
22
+    "react-scripts": "1.1.4",
23
+    "shortid": "^2.2.11"
24
+  },
25
+  "lint-staged": {
26
+    "src/**/*.{js,jsx,json,css,less}": [
27
+      "prettier --write",
28
+      "git add"
29
+    ]
30
+  },
31
+  "scripts": {
32
+    "precommit": "lint-staged",
33
+    "prettier": "prettier --write src/**/*.{js,jsx,json,css,less}",
34
+    "start": "react-app-rewired start",
35
+    "build": "react-app-rewired build",
36
+    "test": "react-app-rewired test --env=jsdom",
37
+    "eject": "react-scripts eject",
38
+    "lib": "babel ./src --out-dir ./lib --source-maps --copy-files"
39
+  },
40
+  "devDependencies": {
41
+    "babel-cli": "^6.26.0",
42
+    "babel-plugin-import": "^1.8.0",
43
+    "babel-plugin-transform-object-rest-spread": "^6.26.0",
44
+    "babel-preset-env": "^1.7.0",
45
+    "babel-preset-react": "^6.24.1",
46
+    "husky": "^0.14.3",
47
+    "lint-staged": "^7.2.0",
48
+    "prettier": "^1.13.7",
49
+    "react-app-rewired": "^1.5.2"
50
+  },
51
+  "license": "MIT"
52
+}

バイナリ
public/favicon.ico ファイルの表示


+ 42
- 0
public/index.html ファイルの表示

@@ -0,0 +1,42 @@
1
+<!DOCTYPE html>
2
+<html lang="en">
3
+  <head>
4
+    <meta charset="utf-8">
5
+    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
6
+    <meta name="theme-color" content="#000000">
7
+    <!--
8
+      manifest.json provides metadata used when your web app is added to the
9
+      homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
10
+    -->
11
+    <link rel="manifest" href="%PUBLIC_URL%/manifest.json">
12
+    <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
13
+    <!--
14
+      Notice the use of %PUBLIC_URL% in the tags above.
15
+      It will be replaced with the URL of the `public` folder during the build.
16
+      Only files inside the `public` folder can be referenced from the HTML.
17
+
18
+      Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
19
+      work correctly both with client-side routing and a non-root public URL.
20
+      Learn how to configure a non-root public URL by running `npm run build`.
21
+    -->
22
+
23
+     <script src="http://gosspublic.alicdn.com/aliyun-oss-sdk.min.js"></script>
24
+    <title>评论</title>
25
+  </head>
26
+  <body>
27
+    <noscript>
28
+      You need to enable JavaScript to run this app.
29
+    </noscript>
30
+    <div id="root-comment"></div>
31
+    <!--
32
+      This HTML file is a template.
33
+      If you open it directly in the browser, you will see an empty page.
34
+
35
+      You can add webfonts, meta tags, or analytics to this file.
36
+      The build step will place the bundled scripts into the <body> tag.
37
+
38
+      To begin the development, run `npm start` or `yarn start`.
39
+      To create a production bundle, use `npm run build` or `yarn build`.
40
+    -->
41
+  </body>
42
+</html>

+ 15
- 0
public/manifest.json ファイルの表示

@@ -0,0 +1,15 @@
1
+{
2
+  "short_name": "React App",
3
+  "name": "Create React App Sample",
4
+  "icons": [
5
+    {
6
+      "src": "favicon.ico",
7
+      "sizes": "64x64 32x32 24x24 16x16",
8
+      "type": "image/x-icon"
9
+    }
10
+  ],
11
+  "start_url": "./index.html",
12
+  "display": "standalone",
13
+  "theme_color": "#000000",
14
+  "background_color": "#ffffff"
15
+}

+ 9
- 0
src/App.css ファイルの表示

@@ -0,0 +1,9 @@
1
+.comment {
2
+  width: 100%;
3
+  padding: 10px;
4
+  margin-bottom: 100px;
5
+}
6
+.comment .ant-spin-nested-loading > div > .ant-spin .ant-spin-dot {
7
+  position: fixed;
8
+  top: 45%;
9
+}

+ 307
- 0
src/App.js ファイルの表示

@@ -0,0 +1,307 @@
1
+import React, { Component } from "react";
2
+import { message } from "antd";
3
+import axios from "./axios";
4
+import { URL, ERROR_DEFAULT, LIMIT } from "./constant";
5
+import { CommentContext } from "./Comment";
6
+import { isFunction } from "./helper";
7
+import CommentInput from "./components/CommentInput";
8
+import CommentList from "./components/CommentList";
9
+// import * as mock from "./mock";
10
+import "./App.css";
11
+
12
+class App extends Component {
13
+  constructor(props) {
14
+    super(props);
15
+    this.state = {
16
+      loading: {},
17
+
18
+      // oss 配置
19
+      oss: {},
20
+
21
+      // 评论数据
22
+      list: [],
23
+      page: 1,
24
+      total: 0,
25
+      // 是否没有更多评论了
26
+      isNoMoreComment: false
27
+    };
28
+    this.handleChangeLoading = this.handleChangeLoading.bind(this);
29
+    this.sGetComment = this.sGetComment.bind(this);
30
+    this.sGetReply = this.sGetReply.bind(this);
31
+    this.sCreateComment = this.sCreateComment.bind(this);
32
+    this.sCreateReply = this.sCreateReply.bind(this);
33
+    this.sCommentFavor = this.sCommentFavor.bind(this);
34
+    this.sOssSts = this.sOssSts.bind(this);
35
+  }
36
+
37
+  componentDidMount() {
38
+    this.sGetComment();
39
+  }
40
+
41
+  /**
42
+   * 改变 loading 状态
43
+   * @param {string} key key
44
+   * @param {string} value value
45
+   */
46
+  handleChangeLoading(key, value) {
47
+    const { loading } = this.state;
48
+    loading[key] = value;
49
+    this.setState({ loading });
50
+  }
51
+
52
+  /**
53
+   * 获取评论列表
54
+   */
55
+  sGetComment({ type = 1, businessId = 1, page = 1 } = {}) {
56
+    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;
66
+    axios
67
+      .get(
68
+        `${URL}/comments?type=${type}&business_id=${businessId}&page=${page}&limit=${LIMIT}`
69
+      )
70
+      .then(response => {
71
+        const { list, page, total } = response.data;
72
+        if (list) {
73
+          let newList = list;
74
+          if (page > 1) {
75
+            let { list: oldList } = this.state;
76
+            // 删除临时数据
77
+            oldList = oldList.filter(o => !o.isTemporary);
78
+            newList = oldList.concat(newList);
79
+          }
80
+          this.setState({
81
+            list: newList,
82
+            page,
83
+            total
84
+          });
85
+        } else {
86
+          message.info("没有更多评论了");
87
+          this.setState({
88
+            isNoMoreComment: true
89
+          });
90
+        }
91
+      })
92
+      .catch(error => {
93
+        if (error.response && error.response.data && error.response.data.msg) {
94
+          message.error(error.response.data.msg || ERROR_DEFAULT);
95
+          return;
96
+        }
97
+        message.error(error.message || ERROR_DEFAULT);
98
+      })
99
+      .finally(() => {
100
+        this.handleChangeLoading("sGetComment", false);
101
+      });
102
+  }
103
+
104
+  /**
105
+   * 获取更多回复
106
+   */
107
+  sGetReply({ commentId, page = 1 } = {}) {
108
+    this.handleChangeLoading("sGetReply", true);
109
+    axios
110
+      .get(`${URL}/replies?comment_id=${commentId}&page=${page}&limit=${LIMIT}`)
111
+      .then(response => {
112
+        const { list: replies } = response.data;
113
+        if (!replies) {
114
+          message.info("没有更多数据了!");
115
+        }
116
+        const list = this.state.list.map(item => {
117
+          if (item.id === commentId) {
118
+            if (!item.replies) item.replies = [];
119
+            if (replies) {
120
+              if (page === 1) {
121
+                // 如果当前页数为第一页,则清空当前所有的 replies
122
+                // 并将获取到的 replies 存放在 state
123
+                item.replies = replies;
124
+              } else {
125
+                item.replies = item.replies.concat(replies);
126
+                // 如果当前页数非第一页,则合并 replies
127
+              }
128
+            } else {
129
+              item.isNoMoreReply = true;
130
+            }
131
+          }
132
+          return item;
133
+        });
134
+        this.setState({ list });
135
+      })
136
+      .catch(error => {
137
+        if (error.response && error.response.data && error.response.data.msg) {
138
+          message.error(error.response.data.msg || ERROR_DEFAULT);
139
+          return;
140
+        }
141
+        message.error(error.message || ERROR_DEFAULT);
142
+      })
143
+      .finally(() => {
144
+        this.handleChangeLoading("sGetReply", false);
145
+      });
146
+  }
147
+
148
+  /**
149
+   * 添加评论
150
+   * @param {object} data { type, business_id, content }
151
+   */
152
+  sCreateComment(data) {
153
+    if (!data.content) return message.error("评论内容不能为空 ");
154
+    this.handleChangeLoading("sCreateComment", true);
155
+    axios(`${URL}/comments`, {
156
+      method: "post",
157
+      data,
158
+      withCredentials: true
159
+    })
160
+      .then(response => {
161
+        message.success("评论成功!");
162
+        // 将数据写入到 list 中
163
+        // 临时插入
164
+        // 等到获取数据之后,删除临时数据
165
+        const { list } = this.state;
166
+        list.unshift({
167
+          ...response.data,
168
+          isTemporary: true // 临时的数据
169
+        });
170
+        this.setState({ list });
171
+      })
172
+      .catch(error => {
173
+        if (error.response && error.response.data && error.response.data.msg) {
174
+          message.error(error.response.data.msg || ERROR_DEFAULT);
175
+          return;
176
+        }
177
+        message.error(error.message || ERROR_DEFAULT);
178
+      })
179
+      .finally(() => {
180
+        this.handleChangeLoading("sCreateComment", false);
181
+      });
182
+  }
183
+
184
+  /**
185
+   * 添加回复
186
+   * 回复评论/回复回复
187
+   * @param {object} data { comment_id, content, [reply_id] }
188
+   */
189
+  sCreateReply(data, cb) {
190
+    if (!data.content) return message.error("回复内容不能为空 ");
191
+    this.handleChangeLoading("sCreateReply", true);
192
+    axios(`${URL}/replies`, {
193
+      method: "post",
194
+      data,
195
+      withCredentials: true
196
+    })
197
+      .then(response => {
198
+        // console.log("response: ", response.data);
199
+        // // 将该条数据插入到 list 中
200
+        // const list = this.state.list.map(item => {
201
+        //   if (item.id === data.comment_id) {
202
+        //     if (!item.replies) item.replies = [];
203
+        //     item.reply_count += 1
204
+        //     item.replies.unshift(response.data);
205
+        //   }
206
+        //   return item;
207
+        // });
208
+        // this.setState({ list });
209
+        this.sGetReply({ commentId: data.comment_id });
210
+        message.success("回复成功!");
211
+        if (isFunction(cb)) cb();
212
+      })
213
+      .catch(error => {
214
+        if (error.response && error.response.data && error.response.data.msg) {
215
+          message.error(error.response.data.msg || ERROR_DEFAULT);
216
+          return;
217
+        }
218
+        message.error(error.message || ERROR_DEFAULT);
219
+      })
220
+      .finally(() => {
221
+        this.handleChangeLoading("sCreateReply", false);
222
+      });
223
+  }
224
+
225
+  /**
226
+   * 点赞/取消点赞
227
+   * @param {string} commentId { commentId }
228
+   * @param {boolean} favored   是否已经点过赞
229
+   */
230
+  sCommentFavor(commentId, favored) {
231
+    this.handleChangeLoading("sCommentFavor", true);
232
+    axios(`${URL}/comments/${commentId}/favor`, {
233
+      method: favored ? "delete" : "put",
234
+      withCredentials: true
235
+    })
236
+      .then(response => {
237
+        message.success(favored ? "取消点赞成功!" : "点赞成功!");
238
+        // 更新 list 中的该项数据的 favored
239
+        const list = this.state.list.map(item => {
240
+          if (item.id === commentId) {
241
+            item.favored = !favored;
242
+            item.favor_count += favored ? -1 : 1;
243
+          }
244
+          return item;
245
+        });
246
+        this.setState({ list });
247
+      })
248
+      .catch(error => {
249
+        if (error.response && error.response.data && error.response.data.msg) {
250
+          message.error(error.response.data.msg || ERROR_DEFAULT);
251
+          return;
252
+        }
253
+        message.error(error.message || ERROR_DEFAULT);
254
+      })
255
+      .finally(() => {
256
+        this.handleChangeLoading("sCommentFavor", false);
257
+      });
258
+  }
259
+
260
+  /**
261
+   * 获取 OSS 上传的参数
262
+   */
263
+  sOssSts() {
264
+    this.handleChangeLoading("sOssSts", true);
265
+    axios
266
+      .get(`${URL}/oss/sts`)
267
+      .then(response => {
268
+        this.setState({ oss: { ...response.data } });
269
+      })
270
+      .catch(error => {
271
+        if (error.response && error.response.data && error.response.data.msg) {
272
+          message.error(error.response.data.msg || ERROR_DEFAULT);
273
+          return;
274
+        }
275
+        message.error(error.message || ERROR_DEFAULT);
276
+      })
277
+      .finally(() => {
278
+        this.handleChangeLoading("sOssSts", false);
279
+      });
280
+  }
281
+
282
+  render() {
283
+    // 添加到 Context 的数据
284
+    const value = {
285
+      ...this.state,
286
+      sCreateComment: this.sCreateComment,
287
+      sGetComment: this.sGetComment,
288
+      sCommentFavor: this.sCommentFavor,
289
+      sCreateReply: this.sCreateReply,
290
+      sGetReply: this.sGetReply,
291
+      sOssSts: this.sOssSts
292
+    };
293
+
294
+    return (
295
+      <CommentContext.Provider value={value}>
296
+        <div className="comment">
297
+          <CommentInput />
298
+          <div style={{ marginTop: 20 }}>
299
+            <CommentList />
300
+          </div>
301
+        </div>
302
+      </CommentContext.Provider>
303
+    );
304
+  }
305
+}
306
+
307
+export default App;

+ 21
- 0
src/Comment.js ファイルの表示

@@ -0,0 +1,21 @@
1
+import React from "react";
2
+
3
+const CommentContext = React.createContext();
4
+
5
+// This function takes a component...
6
+export function Comment(Component) {
7
+  // ...and returns another component...
8
+  return function(props) {
9
+    // ... and renders the wrapped component with the context theme!
10
+    // Notice that we pass through any additional props as well
11
+    return (
12
+      <CommentContext.Consumer>
13
+        {app => <Component {...props} app={app} />}
14
+      </CommentContext.Consumer>
15
+    );
16
+  };
17
+}
18
+
19
+export { CommentContext };
20
+
21
+export default Comment;

+ 5
- 0
src/axios.js ファイルの表示

@@ -0,0 +1,5 @@
1
+import axios from "axios";
2
+
3
+axios.defaults.withCredentials = true;
4
+
5
+export default axios;

+ 48
- 0
src/components/CommentBox/index.css ファイルの表示

@@ -0,0 +1,48 @@
1
+.showMore {
2
+  color: #4a90e2;
3
+  text-align: center;
4
+  width: 100px;
5
+  background-color: #f8f8f8;
6
+  cursor: pointer;
7
+  padding: 10px;
8
+  margin: 0 auto;
9
+  transition: all 0.3s;
10
+}
11
+.showMore:hover {
12
+  background-color: #f5f5f5;
13
+  color: #1890ff;
14
+}
15
+.moreBox {
16
+  text-align: center;
17
+  width: 90%;
18
+  margin-left: 40px;
19
+  height: 40px;
20
+  display: inline-block;
21
+}
22
+@media screen and (max-width: 616px) and (min-width: 449px) {
23
+  .moreBox {
24
+    text-align: center;
25
+    width: 85%;
26
+    margin-left: 40px;
27
+    height: 40px;
28
+    display: inline-block;
29
+  }
30
+}
31
+@media screen and (max-width: 449px) and (min-width: 365px) {
32
+  .moreBox {
33
+    text-align: center;
34
+    width: 80%;
35
+    margin-left: 40px;
36
+    height: 40px;
37
+    display: inline-block;
38
+  }
39
+}
40
+@media screen and (max-width: 365px) {
41
+  .moreBox {
42
+    text-align: center;
43
+    width: 75%;
44
+    margin-left: 40px;
45
+    height: 40px;
46
+    display: inline-block;
47
+  }
48
+}

+ 120
- 0
src/components/CommentBox/index.js ファイルの表示

@@ -0,0 +1,120 @@
1
+import React, { Component } from "react";
2
+import PropTypes from "prop-types";
3
+import { Icon } from "antd";
4
+import Comment from "../../Comment";
5
+import ContentItem from "./../ContentItem";
6
+import "./index.css";
7
+
8
+class CommentBox extends Component {
9
+  constructor(props) {
10
+    super(props);
11
+    this.state = {
12
+      showReply: true,
13
+      page: 1
14
+    };
15
+
16
+    this.handleToggleReply = this.handleToggleReply.bind(this);
17
+    this.handleGetMoreReply = this.handleGetMoreReply.bind(this);
18
+    this.renderReplies = this.renderReplies.bind(this);
19
+  }
20
+
21
+  /**
22
+   * 切换是否显示回复列表
23
+   */
24
+  handleToggleReply() {
25
+    this.setState({ showReply: !this.state.showReply });
26
+  }
27
+
28
+  /**
29
+   * 获取更多评论
30
+   * @param {string} commentId comment id
31
+   */
32
+  handleGetMoreReply(commentId) {
33
+    // 从第一页开始获取评论
34
+    const { page } = this.state;
35
+    this.props.app.sGetReply({ commentId, page });
36
+    this.setState({ page: page + 1 });
37
+  }
38
+
39
+  /**
40
+   * 渲染回复 DOM
41
+   * @param {array} replies 回复列表
42
+   * @param {boolean} isNoMoreReply 是否没有更多回复
43
+   */
44
+  renderReplies(replies, isNoMoreReply) {
45
+    const { commentId } = this.props;
46
+    const { showReply } = this.state;
47
+    if (showReply && replies && replies.length) {
48
+      const len = replies.length;
49
+      return (
50
+        <div style={{ marginLeft: 50 }}>
51
+          {replies.map((item, index) => {
52
+            if (index === len - 1) {
53
+              return [
54
+                <ContentItem
55
+                  commentId={commentId}
56
+                  replyId={item.id}
57
+                  key={item.id}
58
+                  content={item}
59
+                  type="reply"
60
+                />,
61
+                <div className="moreBox" key="show_more_button">
62
+                  {!isNoMoreReply && (
63
+                    <span
64
+                      className="showMore"
65
+                      onClick={() => this.handleGetMoreReply(commentId)}
66
+                    >
67
+                      查看更多回复
68
+                    </span>
69
+                  )}
70
+
71
+                  <a
72
+                    style={{ float: "right" }}
73
+                    onClick={this.handleToggleReply}
74
+                  >
75
+                    <Icon type="up" /> 收起回复
76
+                  </a>
77
+                </div>
78
+              ];
79
+            }
80
+            return (
81
+              <ContentItem
82
+                commentId={commentId}
83
+                replyId={item.id}
84
+                key={item.id}
85
+                content={item}
86
+                type="reply"
87
+              />
88
+            );
89
+          })}
90
+        </div>
91
+      );
92
+    }
93
+    return null;
94
+  }
95
+
96
+  render() {
97
+    const { content } = this.props;
98
+    const { showReply } = this.state;
99
+    return (
100
+      <div>
101
+        <ContentItem
102
+          content={content}
103
+          onShowReply={this.handleToggleReply}
104
+          showReply={showReply}
105
+          commentId={content.id}
106
+          type="comment"
107
+        />
108
+        {this.renderReplies(content.replies, content.isNoMoreReply)}
109
+      </div>
110
+    );
111
+  }
112
+}
113
+
114
+CommentBox.propTypes = {
115
+  commentId: PropTypes.string.isRequired
116
+};
117
+
118
+CommentBox.defaultProps = {};
119
+
120
+export default Comment(CommentBox);

+ 135
- 0
src/components/CommentInput/index.js ファイルの表示

@@ -0,0 +1,135 @@
1
+import React, { Component } from "react";
2
+import PropTypes from "prop-types";
3
+import { OSS_LINK } from "../../constant";
4
+import Comment from "../../Comment";
5
+import Editor from "../Editor";
6
+
7
+const PLACEHOLDER = {
8
+  normal: "说点什么吧...",
9
+  default: "说点什么吧..."
10
+};
11
+
12
+class CommentInput extends Component {
13
+  constructor(props) {
14
+    super(props);
15
+    this.state = {
16
+      value: "",
17
+
18
+      fileList: [], // 图片列表
19
+      fileMap: {} // 已经上传的图片路径和 uid 的映射 { uid: path }
20
+    };
21
+    this.handleChange = this.handleChange.bind(this);
22
+    this.handleSubmit = this.handleSubmit.bind(this);
23
+    this.handleChangeFileList = this.handleChangeFileList.bind(this);
24
+    this.handleChangeEmoji = this.handleChangeEmoji.bind(this);
25
+    this.handleUpload = this.handleUpload.bind(this);
26
+  }
27
+
28
+  handleChange(e) {
29
+    this.setState({ value: e.target.value });
30
+  }
31
+
32
+  handleChangeFileList(fileList) {
33
+    this.setState({ fileList });
34
+  }
35
+
36
+  handleChangeEmoji(emojiId) {
37
+    let { value } = this.state;
38
+    value += `[${emojiId}]`;
39
+    this.setState({ value });
40
+  }
41
+
42
+  handleUpload({ uid, path }) {
43
+    const { fileMap } = this.state;
44
+    fileMap[uid] = path;
45
+    this.setState({ fileMap });
46
+  }
47
+
48
+  handleSubmit() {
49
+    let { value, fileMap, fileList } = this.state;
50
+    if (fileList.length) {
51
+      fileList.forEach(item => {
52
+        value += `[${OSS_LINK}${fileMap[item.uid]}]`;
53
+      });
54
+    }
55
+
56
+    const { type, commentId, replyId, handleToggleInput } = this.props;
57
+    if (type === "normal") {
58
+      this.props.app.sCreateComment({
59
+        type: 1,
60
+        business_id: "1",
61
+        content: value
62
+      });
63
+    } else if (type === "comment") {
64
+      this.props.app.sCreateReply(
65
+        {
66
+          comment_id: commentId,
67
+          content: value
68
+        },
69
+        () => handleToggleInput()
70
+      );
71
+    } else if (type === "reply") {
72
+      this.props.app.sCreateReply(
73
+        {
74
+          comment_id: commentId,
75
+          content: value,
76
+          reply_id: replyId
77
+        },
78
+        () => handleToggleInput()
79
+      );
80
+    }
81
+  }
82
+
83
+  render() {
84
+    const { type } = this.props;
85
+    const { value, fileList } = this.state;
86
+
87
+    return (
88
+      <div>
89
+        {type === "normal" ? (
90
+          <div>
91
+            <span
92
+              style={{
93
+                border: "1px solid #CECECE",
94
+                color: "#666",
95
+                padding: "2px 3px"
96
+              }}
97
+            >
98
+              回复
99
+            </span>
100
+            <span style={{ marginLeft: "20px", color: "#5198EB" }}>
101
+              口碑
102
+              <span style={{ marginLeft: "20px", color: "#666666" }}>
103
+                (全站挑出毛病或提出合理建议,奖励10到100元红包)
104
+              </span>
105
+            </span>
106
+          </div>
107
+        ) : 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>
121
+      </div>
122
+    );
123
+  }
124
+}
125
+
126
+CommentInput.propTypes = {
127
+  // normal 有切换回复/口碑的 header ; comment 评论输入框 / reply 回复输入框
128
+  type: PropTypes.oneOf(["normal", "comment", "reply"])
129
+};
130
+
131
+CommentInput.defaultProps = {
132
+  type: "normal"
133
+};
134
+
135
+export default Comment(CommentInput);

+ 14
- 0
src/components/CommentList/index.css ファイルの表示

@@ -0,0 +1,14 @@
1
+.showMore {
2
+  color: #4a90e2;
3
+  text-align: center;
4
+  width: 100%;
5
+  background-color: #f8f8f8;
6
+  cursor: pointer;
7
+  padding: 10px;
8
+  margin: 40px auto;
9
+  transition: all 0.3s;
10
+}
11
+.showMore:hover {
12
+  background-color: #f5f5f5;
13
+  color: #1890ff;
14
+}

+ 46
- 0
src/components/CommentList/index.js ファイルの表示

@@ -0,0 +1,46 @@
1
+import React, { Component } from "react";
2
+import { Spin } from "antd";
3
+import Comment from "../../Comment";
4
+import CommentBox from "../CommentBox";
5
+import "./index.css";
6
+
7
+class CommentList extends Component {
8
+  constructor(props) {
9
+    super(props);
10
+    this.state = {};
11
+  }
12
+
13
+  render() {
14
+    const {
15
+      list,
16
+      page,
17
+      loading,
18
+      isNoMoreComment,
19
+      sGetComment
20
+    } = this.props.app;
21
+
22
+    const spinning = Boolean(loading.sGetComment || loading.sCommentFavor);
23
+    return (
24
+      <div>
25
+        <Spin spinning={spinning}>
26
+          {list.map(item => (
27
+            <CommentBox content={item} key={item.id} commentId={item.id} />
28
+          ))}
29
+
30
+          {!isNoMoreComment && (
31
+            <div
32
+              className="showMore"
33
+              onClick={() => sGetComment({ page: page + 1 })}
34
+            >
35
+              <span>查看更多评论</span>
36
+            </div>
37
+          )}
38
+        </Spin>
39
+      </div>
40
+    );
41
+  }
42
+}
43
+
44
+CommentList.propTypes = {};
45
+
46
+export default Comment(CommentList);

+ 52
- 0
src/components/ContentItem/index.css ファイルの表示

@@ -0,0 +1,52 @@
1
+.left {
2
+  display: inline-block;
3
+  vertical-align: top;
4
+  width: 40px;
5
+}
6
+.right {
7
+  display: inline-block;
8
+  width: 90%;
9
+  margin-left: 10px;
10
+}
11
+.box {
12
+  margin: 10px;
13
+  padding: 15px 5px;
14
+  border-top: 1px solid #eee;
15
+}
16
+.content {
17
+  margin: 10px 0;
18
+}
19
+.itemLeft {
20
+  float: left;
21
+}
22
+.itemRight {
23
+  float: right;
24
+  margin-left: 5px;
25
+}
26
+.bottom {
27
+  margin: 20px auto;
28
+}
29
+.favored {
30
+  color: #4a90e2;
31
+}
32
+@media screen and (max-width: 616px) and (min-width: 449px) {
33
+  .right {
34
+    display: inline-block;
35
+    width: 85%;
36
+    margin-left: 10px;
37
+  }
38
+}
39
+@media screen and (max-width: 449px) and (min-width: 365px) {
40
+  .right {
41
+    display: inline-block;
42
+    width: 80%;
43
+    margin-left: 10px;
44
+  }
45
+}
46
+@media screen and (max-width: 365px) {
47
+  .right {
48
+    display: inline-block;
49
+    width: 75%;
50
+    margin-left: 10px;
51
+  }
52
+}

+ 130
- 0
src/components/ContentItem/index.js ファイルの表示

@@ -0,0 +1,130 @@
1
+import React, { Component } from "react";
2
+import PropTypes from "prop-types";
3
+import { Avatar, Icon } from "antd";
4
+import dayjs from "dayjs";
5
+import Comment from "../../Comment";
6
+import CommentInput from "../CommentInput";
7
+import { renderContent } from "../../helper";
8
+import "./index.css";
9
+
10
+class CommentItem extends Component {
11
+  constructor(props) {
12
+    super(props);
13
+    this.state = {
14
+      isShowInput: false
15
+    };
16
+    this.handleToggleInput = this.handleToggleInput.bind(this);
17
+    this.renderTextWithReply = this.renderTextWithReply.bind(this);
18
+  }
19
+
20
+  handleToggleInput() {
21
+    this.setState({ isShowInput: !this.state.isShowInput });
22
+  }
23
+
24
+  renderTextWithReply(text, content) {
25
+    let newText = text;
26
+    const { reply } = content;
27
+    if (reply) {
28
+      newText = `${newText} //@<a href="/${reply.user_id}">${
29
+        reply.user_name
30
+      }</a> ${reply.content}`;
31
+      if (reply.reply) {
32
+        return this.renderTextWithReply(newText, reply);
33
+      }
34
+    }
35
+    return newText;
36
+  }
37
+
38
+  render() {
39
+    const {
40
+      commentId,
41
+      replyId,
42
+      content,
43
+      type,
44
+      showReply,
45
+      onShowReply,
46
+      app
47
+    } = this.props;
48
+    const { isShowInput } = this.state;
49
+    const isComment = type === "comment";
50
+    return (
51
+      <div className="box">
52
+        <div className="left">
53
+          <Avatar src={content.user_avatar} size="large" />
54
+        </div>
55
+        <div className="right">
56
+          <div className="name">
57
+            <a href={`/${content.user_id}`}>
58
+              {content.user_name || "暂无昵称"}
59
+            </a>
60
+
61
+            <span style={{ marginLeft: 10 }}>
62
+              {dayjs(content.created * 1000).format("YYYY-MM-DD HH:mm:ss")}
63
+            </span>
64
+          </div>
65
+          <div
66
+            className="content"
67
+            dangerouslySetInnerHTML={{
68
+              __html: renderContent(
69
+                this.renderTextWithReply(content.content, content)
70
+              )
71
+            }}
72
+          />
73
+          <div className="bottom">
74
+            {isComment && content.reply_count ? (
75
+              <div>
76
+                <a
77
+                  className="itemLeft"
78
+                  onClick={onShowReply}
79
+                  style={{ userSelect: "none" }}
80
+                >
81
+                  {content.reply_count} 条回复
82
+                  {showReply ? <Icon type="up" /> : <Icon type="down" />}
83
+                </a>
84
+              </div>
85
+            ) : null}
86
+
87
+            <a onClick={this.handleToggleInput} className="itemRight">
88
+              &nbsp; 回复
89
+            </a>
90
+            <div
91
+              className="itemRight"
92
+              style={{ cursor: "pointer" }}
93
+              onClick={() => app.sCommentFavor(content.id, content.favored)}
94
+            >
95
+              <Icon
96
+                type="like-o"
97
+                className={content.favored ? "favored" : ""}
98
+              />
99
+              &nbsp;{content.favor_count}
100
+            </div>
101
+          </div>
102
+
103
+          {isShowInput ? (
104
+            <CommentInput
105
+              type={type}
106
+              replyId={replyId}
107
+              commentId={commentId}
108
+              handleToggleInput={this.handleToggleInput}
109
+            />
110
+          ) : null}
111
+        </div>
112
+      </div>
113
+    );
114
+  }
115
+}
116
+
117
+CommentItem.propTypes = {
118
+  content: PropTypes.object.isRequired,
119
+  // comment 评论
120
+  // reply 回复
121
+  type: PropTypes.oneOf(["comment", "reply"]),
122
+  onShowReply: PropTypes.func
123
+};
124
+
125
+CommentItem.defaultProps = {
126
+  type: "comment",
127
+  onShowReply: () => {}
128
+};
129
+
130
+export default Comment(CommentItem);

+ 23
- 0
src/components/Editor/Emoji.css ファイルの表示

@@ -0,0 +1,23 @@
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/Emoji.js ファイルの表示

@@ -0,0 +1,65 @@
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/Upload.css ファイルの表示

@@ -0,0 +1,8 @@
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/Upload.js ファイルの表示

@@ -0,0 +1,132 @@
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/index.css ファイルの表示

@@ -0,0 +1,82 @@
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
+}

+ 125
- 0
src/components/Editor/index.js ファイルの表示

@@ -0,0 +1,125 @@
1
+// https://github.com/lovasoa/react-contenteditable/blob/master/src/react-contenteditable.js
2
+import React from "react";
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 styles from "./index.less";
8
+import "./index.css";
9
+
10
+const { TextArea } = Input;
11
+
12
+export default class Editor extends React.Component {
13
+  constructor(props) {
14
+    super(props);
15
+    this.state = {
16
+      showUpload: false
17
+    };
18
+    this.handleClickEmoji = this.handleClickEmoji.bind(this);
19
+    this.handleShowUpload = this.handleShowUpload.bind(this);
20
+  }
21
+
22
+  componentDidMount() {}
23
+
24
+  handleClickEmoji(emojiId) {
25
+    this.props.onChangeEmoji(emojiId);
26
+  }
27
+
28
+  handleShowUpload(showUpload) {
29
+    if (typeof showUpload === "boolean") {
30
+      this.setState({ showUpload: showUpload });
31
+    } else {
32
+      this.setState({ showUpload: !this.state.showUpload });
33
+    }
34
+  }
35
+
36
+  render() {
37
+    const {
38
+      value,
39
+      onChange,
40
+      placeholder,
41
+      fileList,
42
+      onChangeFileList,
43
+      onUpload
44
+    } = this.props;
45
+    return (
46
+      <div className="editor">
47
+        <TextArea
48
+          value={value}
49
+          onChange={onChange}
50
+          rows={5}
51
+          placeholder={placeholder}
52
+        />
53
+
54
+        <div className="toolbar">
55
+          <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>
69
+
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}张图片)
89
+                    </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>
110
+          </div>
111
+
112
+          <div style={{ float: "right", margin: "5px 11px" }}>
113
+            <Button
114
+              onClick={this.props.onSubmit}
115
+              type="primary"
116
+              loading={this.props.loading}
117
+            >
118
+              发表
119
+            </Button>
120
+          </div>
121
+        </div>
122
+      </div>
123
+    );
124
+  }
125
+}

+ 16
- 0
src/constant.js ファイルの表示

@@ -0,0 +1,16 @@
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 = "出错了!";
5
+
6
+export const LIMIT = 10; // 默认 limit
7
+
8
+export const OSS_ENDPOINT = "oss-cn-beijing.aliyuncs.com";
9
+export const OSS_BUCKET = "links-comment";
10
+export const DRIVER_LICENSE_PATH = "/comment";
11
+
12
+export const OSS_LINK = "http://links-comment.oss-cn-beijing.aliyuncs.com";
13
+
14
+export const MAX_UPLOAD_NUMBER = 4;
15
+
16
+export const REGEXP = /\[.+?\]/g;

+ 226
- 0
src/emoji.js ファイルの表示

@@ -0,0 +1,226 @@
1
+const emoji = [
2
+  {
3
+    value: "0",
4
+    title: "捶地"
5
+  },
6
+  {
7
+    value: "1",
8
+    title: "怀疑"
9
+  },
10
+
11
+  {
12
+    value: "2",
13
+    title: "撇嘴"
14
+  },
15
+  {
16
+    value: "3",
17
+    title: "色"
18
+  },
19
+  {
20
+    value: "4",
21
+    ttile: "发呆"
22
+  },
23
+  {
24
+    value: "5",
25
+    title: "酷"
26
+  },
27
+  {
28
+    value: "6",
29
+    title: "害羞"
30
+  },
31
+
32
+  {
33
+    value: "7",
34
+    title: "闭嘴"
35
+  },
36
+  {
37
+    value: "8",
38
+    title: "睡觉"
39
+  },
40
+  {
41
+    value: "9",
42
+    title: "大哭"
43
+  },
44
+  {
45
+    value: "10",
46
+    title: "尴尬"
47
+  },
48
+  {
49
+    value: "11",
50
+    title: "发怒"
51
+  },
52
+  {
53
+    value: "12",
54
+    title: "调皮"
55
+  },
56
+  {
57
+    value: "13",
58
+    title: "呲牙"
59
+  },
60
+  {
61
+    value: "14",
62
+    title: "微笑"
63
+  },
64
+  {
65
+    value: "15",
66
+    title: "难过"
67
+  },
68
+  {
69
+    value: "16",
70
+    title: "帅呆"
71
+  },
72
+  {
73
+    value: "17",
74
+    title: "折磨"
75
+  },
76
+  {
77
+    value: "18",
78
+    title: "吐"
79
+  },
80
+  {
81
+    value: "19",
82
+    title: "偷笑"
83
+  },
84
+  {
85
+    value: "20",
86
+    title: "可爱"
87
+  },
88
+  {
89
+    value: "21",
90
+    title: "白眼"
91
+  },
92
+  {
93
+    value: "22",
94
+    title: "傲慢"
95
+  },
96
+  {
97
+    value: "23",
98
+    title: "嘴馋"
99
+  },
100
+  {
101
+    value: "24",
102
+    title: "困"
103
+  },
104
+  {
105
+    value: "25",
106
+    title: "恐慌"
107
+  },
108
+  {
109
+    value: "26",
110
+    title: "流汗"
111
+  },
112
+  {
113
+    value: "27",
114
+    title: "憨笑"
115
+  },
116
+  {
117
+    value: "28",
118
+    title: "大兵"
119
+  },
120
+  {
121
+    value: "29",
122
+    title: "奋斗"
123
+  },
124
+  {
125
+    value: "30",
126
+    title: "疑问"
127
+  },
128
+  {
129
+    value: "31",
130
+    title: "嘘"
131
+  },
132
+  {
133
+    value: "32",
134
+    title: "晕"
135
+  },
136
+  {
137
+    value: "33",
138
+    title: "被炸了"
139
+  },
140
+  {
141
+    value: "34",
142
+    title: "骷髅"
143
+  },
144
+  {
145
+    value: "35",
146
+    title: "敲打"
147
+  },
148
+  {
149
+    value: "36",
150
+    title: "拜拜"
151
+  },
152
+  {
153
+    value: "37",
154
+    title: "发抖"
155
+  },
156
+  {
157
+    value: "38",
158
+    title: "亲密"
159
+  },
160
+  {
161
+    value: "39",
162
+    title: "跳"
163
+  },
164
+  {
165
+    value: "40",
166
+    title: "猪头"
167
+  },
168
+  {
169
+    value: "41",
170
+    title: "拥抱"
171
+  },
172
+  {
173
+    value: "42",
174
+    title: "生日蛋糕"
175
+  },
176
+  {
177
+    value: "43",
178
+    title: "闪电"
179
+  },
180
+  {
181
+    value: "44",
182
+    title: "地雷"
183
+  },
184
+  {
185
+    value: "45",
186
+    title: "刀"
187
+  },
188
+  {
189
+    value: "46",
190
+    title: "足球"
191
+  },
192
+  {
193
+    value: "47",
194
+    title: "便便"
195
+  },
196
+  {
197
+    value: "48",
198
+    title: "咖啡"
199
+  },
200
+  {
201
+    value: "49",
202
+    title: "米饭"
203
+  },
204
+  {
205
+    value: "50",
206
+    title: "玫瑰"
207
+  },
208
+  {
209
+    value: "51",
210
+    title: "枯萎玫瑰"
211
+  },
212
+  {
213
+    value: "52",
214
+    title: "爱心"
215
+  },
216
+  {
217
+    value: "53",
218
+    title: "心碎"
219
+  }
220
+];
221
+
222
+export const prefixUrl = "https://a.links123.cn/site/src/feed/imgs/qq/";
223
+
224
+export const ext = "gif";
225
+
226
+export default emoji;

+ 44
- 0
src/helper.js ファイルの表示

@@ -0,0 +1,44 @@
1
+import { REGEXP } from "./constant";
2
+import emoji, { prefixUrl, ext } from "./emoji";
3
+
4
+export function isFunction(functionToCheck) {
5
+  return (
6
+    functionToCheck && {}.toString.call(functionToCheck) === "[object Function]"
7
+  );
8
+}
9
+
10
+export function isUrl(userInput) {
11
+  const regexp = /(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&//=]*)/g;
12
+  var res = userInput.match(regexp);
13
+  if (res === null) return false;
14
+  else return true;
15
+}
16
+
17
+/**
18
+ * 将对象数组转换为对象
19
+ * @param {array} array Array of Objects
20
+ * @param {string} keyField string
21
+ */
22
+export function arrayToObject(array, keyField) {
23
+  return array.reduce((obj, item) => {
24
+    obj[item[keyField]] = item;
25
+    return obj;
26
+  }, {});
27
+}
28
+
29
+/**
30
+ * 渲染编辑器
31
+ * [x] => <img src="x" />
32
+ * @param {strig} content
33
+ */
34
+export function renderContent(content, onClick) {
35
+  return content.replace(REGEXP, function(a, b) {
36
+    const src = a.slice(1, -1);
37
+    if (isUrl(src)) {
38
+      return `<img src="${src}" alt="${src}" style="max-width: 300px" />`;
39
+    }
40
+    const emojiObejct = arrayToObject(emoji, "title");
41
+    const value = emojiObejct[src].value;
42
+    return `<img src="${prefixUrl}${value}.${ext}" alt="${value}" />`;
43
+  });
44
+}

+ 7
- 0
src/index.js ファイルの表示

@@ -0,0 +1,7 @@
1
+import React from "react";
2
+import ReactDOM from "react-dom";
3
+import App from "./App";
4
+import registerServiceWorker from "./registerServiceWorker";
5
+
6
+ReactDOM.render(<App />, document.getElementById("root-comment"));
7
+registerServiceWorker();

+ 72
- 0
src/mock.js ファイルの表示

@@ -0,0 +1,72 @@
1
+export const comments = {
2
+  list: [
3
+    {
4
+      id: "5b3a4f0973df3e6a32b0a9d2",
5
+      user_id: 0,
6
+      user_name: "",
7
+      user_avatar: "",
8
+      content: "test[微笑]",
9
+      replies: [
10
+        {
11
+          id: "5b3c58c473df3e64d8d53afb",
12
+          user_id: 0,
13
+          user_name: "",
14
+          user_avatar: "",
15
+          reply: null,
16
+          content: "test",
17
+          favor_count: 1,
18
+          favored: false,
19
+          created: 1530681540
20
+        },
21
+        {
22
+          id: "5b3c58c473df3e64d8d53afa",
23
+          user_id: 0,
24
+          user_name: "",
25
+          user_avatar: "",
26
+          reply: {
27
+            id: "5b3c58c473df3e64d8d53afb",
28
+            user_id: 0,
29
+            user_name: "",
30
+            user_avatar: "",
31
+            reply: null,
32
+            content: "test",
33
+            favor_count: 1,
34
+            favored: false,
35
+            created: 1530681540
36
+          },
37
+          content: "test",
38
+          favor_count: 2,
39
+          favored: false,
40
+          created: 1530681540
41
+        },
42
+        {
43
+          id: "5b3c6e4873df3e53f870d912",
44
+          user_id: 0,
45
+          user_name: "",
46
+          user_avatar: "",
47
+          reply: {
48
+            id: "5b3c58c473df3e64d8d53afb",
49
+            user_id: 0,
50
+            user_name: "",
51
+            user_avatar: "",
52
+            reply: null,
53
+            content: "test",
54
+            favor_count: 1,
55
+            favored: false,
56
+            created: 1530681540
57
+          },
58
+          content: "test",
59
+          favor_count: 0,
60
+          favored: false,
61
+          created: 1530687048
62
+        }
63
+      ],
64
+      reply_count: 4,
65
+      favor_count: 0,
66
+      favored: false,
67
+      created: 1530547977
68
+    }
69
+  ],
70
+  total: 1,
71
+  page: 1
72
+};

+ 117
- 0
src/registerServiceWorker.js ファイルの表示

@@ -0,0 +1,117 @@
1
+// In production, we register a service worker to serve assets from local cache.
2
+
3
+// This lets the app load faster on subsequent visits in production, and gives
4
+// it offline capabilities. However, it also means that developers (and users)
5
+// will only see deployed updates on the "N+1" visit to a page, since previously
6
+// cached resources are updated in the background.
7
+
8
+// To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
9
+// This link also includes instructions on opting out of this behavior.
10
+
11
+const isLocalhost = Boolean(
12
+  window.location.hostname === "localhost" ||
13
+    // [::1] is the IPv6 localhost address.
14
+    window.location.hostname === "[::1]" ||
15
+    // 127.0.0.1/8 is considered localhost for IPv4.
16
+    window.location.hostname.match(
17
+      /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
18
+    )
19
+);
20
+
21
+export default function register() {
22
+  if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) {
23
+    // The URL constructor is available in all browsers that support SW.
24
+    const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
25
+    if (publicUrl.origin !== window.location.origin) {
26
+      // Our service worker won't work if PUBLIC_URL is on a different origin
27
+      // from what our page is served on. This might happen if a CDN is used to
28
+      // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
29
+      return;
30
+    }
31
+
32
+    window.addEventListener("load", () => {
33
+      const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
34
+
35
+      if (isLocalhost) {
36
+        // This is running on localhost. Lets check if a service worker still exists or not.
37
+        checkValidServiceWorker(swUrl);
38
+
39
+        // Add some additional logging to localhost, pointing developers to the
40
+        // service worker/PWA documentation.
41
+        navigator.serviceWorker.ready.then(() => {
42
+          console.log(
43
+            "This web app is being served cache-first by a service " +
44
+              "worker. To learn more, visit https://goo.gl/SC7cgQ"
45
+          );
46
+        });
47
+      } else {
48
+        // Is not local host. Just register service worker
49
+        registerValidSW(swUrl);
50
+      }
51
+    });
52
+  }
53
+}
54
+
55
+function registerValidSW(swUrl) {
56
+  navigator.serviceWorker
57
+    .register(swUrl)
58
+    .then(registration => {
59
+      registration.onupdatefound = () => {
60
+        const installingWorker = registration.installing;
61
+        installingWorker.onstatechange = () => {
62
+          if (installingWorker.state === "installed") {
63
+            if (navigator.serviceWorker.controller) {
64
+              // At this point, the old content will have been purged and
65
+              // the fresh content will have been added to the cache.
66
+              // It's the perfect time to display a "New content is
67
+              // available; please refresh." message in your web app.
68
+              console.log("New content is available; please refresh.");
69
+            } else {
70
+              // At this point, everything has been precached.
71
+              // It's the perfect time to display a
72
+              // "Content is cached for offline use." message.
73
+              console.log("Content is cached for offline use.");
74
+            }
75
+          }
76
+        };
77
+      };
78
+    })
79
+    .catch(error => {
80
+      console.error("Error during service worker registration:", error);
81
+    });
82
+}
83
+
84
+function checkValidServiceWorker(swUrl) {
85
+  // Check if the service worker can be found. If it can't reload the page.
86
+  fetch(swUrl)
87
+    .then(response => {
88
+      // Ensure service worker exists, and that we really are getting a JS file.
89
+      if (
90
+        response.status === 404 ||
91
+        response.headers.get("content-type").indexOf("javascript") === -1
92
+      ) {
93
+        // No service worker found. Probably a different app. Reload the page.
94
+        navigator.serviceWorker.ready.then(registration => {
95
+          registration.unregister().then(() => {
96
+            window.location.reload();
97
+          });
98
+        });
99
+      } else {
100
+        // Service worker found. Proceed as normal.
101
+        registerValidSW(swUrl);
102
+      }
103
+    })
104
+    .catch(() => {
105
+      console.log(
106
+        "No internet connection found. App is running in offline mode."
107
+      );
108
+    });
109
+}
110
+
111
+export function unregister() {
112
+  if ("serviceWorker" in navigator) {
113
+    navigator.serviceWorker.ready.then(registration => {
114
+      registration.unregister();
115
+    });
116
+  }
117
+}