通用评论

App.js 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448
  1. import React, { Component } from "react";
  2. import PropTypes from "prop-types";
  3. import { message } from "antd";
  4. import axios from "axios";
  5. import { ERROR_DEFAULT, LIMIT } from "./constant";
  6. import { CommentContext } from "./Comment";
  7. import { isFunction } from "./helper";
  8. import CommentInput from "./components/CommentInput";
  9. import CommentList from "./components/CommentList";
  10. import Editor from "./components/Editor";
  11. import RenderText from "./components/RenderText";
  12. import lang from "./lang";
  13. import "./App.css";
  14. class App extends Component {
  15. constructor(props) {
  16. super(props);
  17. this.state = {
  18. loading: {},
  19. // oss 配置
  20. oss: {},
  21. // 评论数据
  22. list: [],
  23. page: 1,
  24. total: 0,
  25. // 是否没有更多评论了
  26. isNoMoreComment: false
  27. };
  28. this.handleChangeLoading = this.handleChangeLoading.bind(this);
  29. this.sCreateComment = this.sCreateComment.bind(this);
  30. this.sDeleteComment = this.sDeleteComment.bind(this);
  31. this.sCommentFavor = this.sCommentFavor.bind(this);
  32. this.sCreateReply = this.sCreateReply.bind(this);
  33. this.sDeleteReply = this.sDeleteReply.bind(this);
  34. this.errorHandler = this.errorHandler.bind(this);
  35. this.sGetComment = this.sGetComment.bind(this);
  36. this.sReplyFavor = this.sReplyFavor.bind(this);
  37. this.sGetReply = this.sGetReply.bind(this);
  38. this.sOssSts = this.sOssSts.bind(this);
  39. }
  40. componentWillMount() {
  41. this.axios = axios;
  42. this.axios.defaults.withCredentials = true;
  43. if (this.props.token) {
  44. this.axios.defaults.headers.common["Authorization"] = `Bearer ${
  45. this.props.token
  46. }`;
  47. }
  48. }
  49. componentDidMount() {}
  50. error(msg, info = {}) {
  51. if (this.props.showError) {
  52. message.error(msg);
  53. }
  54. if (this.props.onError) {
  55. this.props.onError(msg, info);
  56. }
  57. }
  58. errorHandler(error) {
  59. if (error.response && error.response.data && error.response.data.msg) {
  60. this.error(lang[error.response.data.msg] || ERROR_DEFAULT, {
  61. response: error.response
  62. });
  63. return;
  64. }
  65. this.error(lang[error.message] || ERROR_DEFAULT, {
  66. response: error.response
  67. });
  68. }
  69. /**
  70. * 改变 loading 状态
  71. * @param {string} key key
  72. * @param {string} value value
  73. */
  74. handleChangeLoading(key, value) {
  75. const { loading } = this.state;
  76. loading[key] = value;
  77. this.setState({ loading });
  78. }
  79. /**
  80. * 获取评论列表
  81. */
  82. sGetComment({ page = 1 } = {}) {
  83. this.handleChangeLoading("sGetComment", true);
  84. const { API, type, businessId } = this.props;
  85. this.axios
  86. .get(
  87. `${API}/comments?type=${type}&business_id=${businessId}&page=${page}&limit=${LIMIT}`
  88. )
  89. .then(response => {
  90. const { list, page, total } = response.data;
  91. if (list) {
  92. let newList = list;
  93. if (page > 1) {
  94. let { list: oldList } = this.state;
  95. // 删除临时数据
  96. oldList = oldList.filter(o => !o.isTemporary);
  97. newList = oldList.concat(newList);
  98. }
  99. this.setState({
  100. list: newList,
  101. page,
  102. total
  103. });
  104. } else {
  105. message.info("没有更多评论了");
  106. this.setState({
  107. isNoMoreComment: true
  108. });
  109. }
  110. })
  111. .catch(this.errorHandler)
  112. .finally(() => {
  113. this.handleChangeLoading("sGetComment", false);
  114. });
  115. }
  116. /**
  117. * 获取更多回复
  118. */
  119. sGetReply({ commentId, page = 1 } = {}) {
  120. this.handleChangeLoading("sGetReply", true);
  121. const { API } = this.props;
  122. this.axios
  123. .get(`${API}/replies?comment_id=${commentId}&page=${page}&limit=${LIMIT}`)
  124. .then(response => {
  125. if (!response.data.list) {
  126. message.info("没有更多数据了!");
  127. }
  128. const list = this.state.list.map(item => {
  129. if (item.id === commentId) {
  130. if (!item.replies) item.replies = [];
  131. if (response.data.list) {
  132. if (page === 1) {
  133. // 如果当前页数为第一页,则清空当前所有的 replies
  134. // 并将获取到的 replies 存放在 state
  135. item.replies = response.data.list;
  136. } else {
  137. item.replies = item.replies
  138. .filter(o => !o.isTemporary)
  139. .concat(response.data.list);
  140. // 如果当前页数非第一页,则合并 replies
  141. }
  142. item.reply_count = response.data.total;
  143. item.reply_page = response.data.page;
  144. } else {
  145. item.isNoMoreReply = true;
  146. }
  147. }
  148. return item;
  149. });
  150. this.setState({ list });
  151. })
  152. .catch(this.errorHandler)
  153. .finally(() => {
  154. this.handleChangeLoading("sGetReply", false);
  155. });
  156. }
  157. /**
  158. * 添加评论
  159. * @param {object} {content} comment content
  160. */
  161. sCreateComment({ content } = {}, cb) {
  162. if (!content) return this.error("评论内容不能为空 ");
  163. this.handleChangeLoading("sCreateComment", true);
  164. const { API, type, businessId } = this.props;
  165. this.axios(`${API}/comments`, {
  166. method: "post",
  167. data: {
  168. type,
  169. business_id: businessId,
  170. content
  171. },
  172. withCredentials: true
  173. })
  174. .then(response => {
  175. if (this.props.showAlertComment) {
  176. message.success("评论成功!");
  177. }
  178. if (isFunction(cb)) cb();
  179. // 将数据写入到 list 中
  180. // 临时插入
  181. // 等到获取数据之后,删除临时数据
  182. const { list, total } = this.state;
  183. list.unshift({
  184. ...response.data,
  185. isTemporary: true // 临时的数据
  186. });
  187. this.setState({ list, total: total + 1 });
  188. })
  189. .catch(this.errorHandler)
  190. .finally(() => {
  191. this.handleChangeLoading("sCreateComment", false);
  192. });
  193. }
  194. /**
  195. * 删除评论
  196. */
  197. sDeleteComment(commentId) {
  198. this.handleChangeLoading("sDeleteComment", true);
  199. const { API } = this.props;
  200. this.axios(`${API}/comments/${commentId}`, {
  201. method: "delete",
  202. withCredentials: true
  203. })
  204. .then(() => {
  205. const { list, total } = this.state;
  206. const res = list.filter(item => item.id !== commentId);
  207. this.setState({ list: res, total: total - 1 });
  208. })
  209. .catch(this.errorHandler)
  210. .finally(() => {
  211. this.handleChangeLoading("sDeleteComment", false);
  212. });
  213. }
  214. /**
  215. * 添加回复
  216. * 回复评论/回复回复
  217. * @param {object} data { comment_id, content, [reply_id] }
  218. */
  219. sCreateReply(data, cb) {
  220. if (!data.content) return this.error("回复内容不能为空 ");
  221. this.handleChangeLoading("sCreateReply", true);
  222. const { API } = this.props;
  223. this.axios(`${API}/replies`, {
  224. method: "post",
  225. data,
  226. withCredentials: true
  227. })
  228. .then(response => {
  229. if (this.props.showAlertReply) {
  230. message.success("回复成功!");
  231. }
  232. if (isFunction(cb)) cb();
  233. // 将数据写入到 list 中
  234. // 临时插入
  235. // 等到获取数据之后,删除临时数据
  236. const list = this.state.list.map(item => {
  237. if (item.id === data.comment_id) {
  238. if (!item.replies) item.replies = [];
  239. item.replies.push({
  240. ...response.data,
  241. isTemporary: true // 临时的数据
  242. });
  243. item.reply_count += 1;
  244. }
  245. return item;
  246. });
  247. this.setState({ list });
  248. })
  249. .catch(this.errorHandler)
  250. .finally(() => {
  251. this.handleChangeLoading("sCreateReply", false);
  252. });
  253. }
  254. /**
  255. * 删除回复
  256. * @param {*} replyId
  257. * @param {*} commentId
  258. */
  259. sDeleteReply(replyId, commentId) {
  260. this.handleChangeLoading("sDeleteReply", true);
  261. const { API } = this.props;
  262. this.axios(`${API}/replies/${replyId}?CommentID=${commentId}`, {
  263. method: "delete",
  264. withCredentials: true
  265. })
  266. .then(() => {
  267. const list = this.state.list.map(item => {
  268. if (item.id === commentId) {
  269. const replies = item.replies.filter(item => item.id !== replyId);
  270. item.replies = replies;
  271. item.reply_count -= 1;
  272. }
  273. return item;
  274. });
  275. this.setState({ list });
  276. })
  277. .catch(this.errorHandler)
  278. .finally(() => {
  279. this.handleChangeLoading("sDeleteReply", false);
  280. });
  281. }
  282. /**
  283. * 评论 点赞/取消点赞
  284. * @param {string} commentId { commentId }
  285. * @param {boolean} favored 是否已经点过赞
  286. */
  287. sCommentFavor(commentId, favored) {
  288. this.handleChangeLoading("sCommentFavor", true);
  289. const { API } = this.props;
  290. this.axios(`${API}/comments/${commentId}/favor`, {
  291. method: favored ? "delete" : "put",
  292. withCredentials: true
  293. })
  294. .then(response => {
  295. if (this.props.showAlertFavor) {
  296. message.success(favored ? "取消点赞成功!" : "点赞成功!");
  297. }
  298. // 更新 list 中的该项数据的 favored
  299. const list = this.state.list.map(item => {
  300. if (item.id === commentId) {
  301. item.favored = !favored;
  302. item.favor_count += favored ? -1 : 1;
  303. }
  304. return item;
  305. });
  306. this.setState({ list });
  307. })
  308. .catch(this.errorHandler)
  309. .finally(() => {
  310. this.handleChangeLoading("sCommentFavor", false);
  311. });
  312. }
  313. /**
  314. * 回复 点赞/取消点赞
  315. * @param {string} replyId replyId
  316. * @param {string} commentId commentId
  317. * @param {boolean} favored 是否已经点过赞
  318. */
  319. sReplyFavor(replyId, commentId, favored) {
  320. this.handleChangeLoading("sReplyFavor", true);
  321. const { API } = this.props;
  322. this.axios(`${API}/replies/${replyId}/favor`, {
  323. method: favored ? "delete" : "put",
  324. data: {
  325. comment_id: commentId
  326. },
  327. withCredentials: true
  328. })
  329. .then(response => {
  330. message.success(favored ? "取消点赞成功!" : "点赞成功!");
  331. // 更新 list 中的该项数据的 favored
  332. const list = this.state.list.map(item => {
  333. if (item.id === commentId) {
  334. item.replies = item.replies.map(r => {
  335. if (r.id === replyId) {
  336. r.favored = !favored;
  337. // r.favor_count = response.data.favor_count;
  338. // 点赞数 +1,而不是使用后端返回的点赞数
  339. // 不然如果返回的不是增加 1,用户可能以为程序错误
  340. r.favor_count += favored ? -1 : 1;
  341. }
  342. return r;
  343. });
  344. }
  345. return item;
  346. });
  347. this.setState({ list });
  348. })
  349. .catch(this.errorHandler)
  350. .finally(() => {
  351. this.handleChangeLoading("sReplyFavor", false);
  352. });
  353. }
  354. /**
  355. * 获取 OSS 上传的参数
  356. */
  357. sOssSts() {
  358. this.handleChangeLoading("sOssSts", true);
  359. const { API } = this.props;
  360. this.axios
  361. .get(`${API}/oss/sts`)
  362. .then(response => {
  363. this.setState({ oss: { ...response.data } });
  364. })
  365. .catch(this.errorHandler)
  366. .finally(() => {
  367. this.handleChangeLoading("sOssSts", false);
  368. });
  369. }
  370. render() {
  371. // 添加到 Context 的数据
  372. const value = {
  373. ...this.state,
  374. ...this.props,
  375. sCreateComment: this.sCreateComment,
  376. sGetComment: this.sGetComment,
  377. sCommentFavor: this.sCommentFavor,
  378. sReplyFavor: this.sReplyFavor,
  379. sCreateReply: this.sCreateReply,
  380. sGetReply: this.sGetReply,
  381. sOssSts: this.sOssSts,
  382. sDeleteComment: this.sDeleteComment,
  383. sDeleteReply: this.sDeleteReply
  384. };
  385. return (
  386. <CommentContext.Provider value={value}>
  387. <div className="comment">
  388. {this.props.showEditor && (
  389. <CommentInput content={this.props.children} />
  390. )}
  391. {this.props.showList && (
  392. <div style={{ marginTop: 20 }}>
  393. <CommentList />
  394. </div>
  395. )}
  396. </div>
  397. </CommentContext.Provider>
  398. );
  399. }
  400. }
  401. App.propTypes = {
  402. type: PropTypes.number.isRequired, // 评论的 type
  403. businessId: PropTypes.string.isRequired, // 评论的 business_id
  404. API: PropTypes.string, // 评论的 API 前缀
  405. showList: PropTypes.bool, // 是否显示评论列表
  406. showEditor: PropTypes.bool, // 是否显示评论输入框
  407. showAlertComment: PropTypes.bool, // 评论成功之后,是否通过 Antd 的 Message 组件进行提示
  408. showAlertReply: PropTypes.bool, // 回复成功之后,是否通过 Antd 的 Message 组件进行提示
  409. showAlertFavor: PropTypes.bool, // 点赞/取消点赞成功之后,是否通过 Antd 的 Message 组件进行提示
  410. showError: PropTypes.bool, // 是否使用Antd的Message组件提示错误信息
  411. onError: PropTypes.func, // 错误回调, 出错了会被调用
  412. userId: PropTypes.number // 用户id, comment内部不维护用户id, 调用组件时传递过来, 目前用于判断是否显示删除按钮
  413. };
  414. App.defaultProps = {
  415. API: "http://api.links123.net/comment/v1",
  416. showList: true,
  417. showEditor: true,
  418. showAlertComment: false,
  419. showAlertReply: false,
  420. showAlertFavor: false,
  421. showError: true
  422. };
  423. export { Editor, RenderText };
  424. export default App;