通用评论

App.js 14KB

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