通用评论

App.js 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651
  1. import React, { Component } from "react";
  2. import PropTypes from "prop-types";
  3. import { message, Modal, Icon } from "antd";
  4. import axios from "axios";
  5. import Cookies from "js-cookie";
  6. import intl from "react-intl-universal";
  7. import { ERROR_DEFAULT, LIMIT, COMMENT_TYPE, LANGUAGE_LINK } from "./constant";
  8. import { CommentContext } from "./Comment";
  9. import { isFunction } from "./helper";
  10. import CommentInput from "./components/CommentInput";
  11. import CommentList from "./components/CommentList";
  12. import Editor from "./components/Editor";
  13. import RenderText from "./components/RenderText";
  14. import EditComment from "./components/EditComment/EditComment";
  15. import { SUPPORT_LOCALES, LOCALES_RESPONSE } from "./lang";
  16. import "./App.css";
  17. class App extends Component {
  18. constructor(props) {
  19. super(props);
  20. this.state = {
  21. loading: {},
  22. // oss 配置
  23. oss: {},
  24. // 评论数据
  25. list: [],
  26. page: 1,
  27. total: 0,
  28. // 是否没有更多评论了
  29. isNoMoreComment: false,
  30. initDone: false,
  31. locale: "zh-CN",
  32. editModalVisible: false,
  33. action: "",
  34. replyId: "",
  35. commentId: "",
  36. userId: "",
  37. content: "",
  38. replyPage: 1
  39. };
  40. this.handleChangeLoading = this.handleChangeLoading.bind(this);
  41. this.sCreateComment = this.sCreateComment.bind(this);
  42. this.sUpdateComment = this.sUpdateComment.bind(this);
  43. this.sDeleteComment = this.sDeleteComment.bind(this);
  44. this.sCommentFavor = this.sCommentFavor.bind(this);
  45. this.sCreateReply = this.sCreateReply.bind(this);
  46. this.sDeleteReply = this.sDeleteReply.bind(this);
  47. this.errorHandler = this.errorHandler.bind(this);
  48. this.sGetComment = this.sGetComment.bind(this);
  49. this.sReplyFavor = this.sReplyFavor.bind(this);
  50. this.sGetReply = this.sGetReply.bind(this);
  51. this.sUpdateReply = this.sUpdateReply.bind(this);
  52. this.sOssSts = this.sOssSts.bind(this);
  53. this.handleEdit = this.handleEdit.bind(this);
  54. this.handleClose = this.handleClose.bind(this);
  55. }
  56. componentWillMount() {
  57. this.axios = axios;
  58. this.axios.defaults.withCredentials = true;
  59. if (this.props.token) {
  60. this.axios.defaults.headers.common["Authorization"] = `Bearer ${
  61. this.props.token
  62. }`;
  63. }
  64. }
  65. componentDidMount() {
  66. this.loadLocales();
  67. }
  68. /**
  69. * 加载语言包
  70. * 只能根据url或者传入的props来确定加载哪个语言包
  71. * 优先级:传入的props > url
  72. */
  73. loadLocales() {
  74. let { locales: currentLocale } = this.props;
  75. const cookieLang = Cookies.get("lnk_lang");
  76. if (!currentLocale) {
  77. currentLocale =
  78. cookieLang ||
  79. intl.determineLocale({
  80. urlLocaleKey: "lang"
  81. });
  82. }
  83. currentLocale = SUPPORT_LOCALES.find(item => item.value === currentLocale)
  84. ? currentLocale
  85. : "zh-CN";
  86. const version = require("./version.json").hash;
  87. // 使用绝对路径
  88. const languageURL = `${LANGUAGE_LINK}/${currentLocale}.json?v=${version}`;
  89. return fetch(languageURL)
  90. .then(response => {
  91. if (response.status >= 400) {
  92. throw new Error("Bad response from server");
  93. }
  94. return response.json();
  95. })
  96. .then(data => {
  97. // console.log('data: ', data);
  98. intl
  99. .init({
  100. currentLocale,
  101. locales: {
  102. [currentLocale]: data
  103. }
  104. })
  105. .then(() => {
  106. this.setState({ initDone: true, locale: currentLocale });
  107. });
  108. });
  109. }
  110. handleEdit({ replyId, commentId, userId, content, replyPage }) {
  111. this.setState({
  112. editModalVisible: true,
  113. action: content.replies
  114. ? "comment"
  115. : content.reply
  116. ? "replyToReply"
  117. : "reply",
  118. replyId,
  119. commentId,
  120. userId,
  121. content,
  122. replyPage
  123. });
  124. }
  125. handleClose() {
  126. this.setState({
  127. editModalVisible: false
  128. });
  129. }
  130. error(msg, info = {}) {
  131. if (this.props.showError) {
  132. message.error(msg);
  133. }
  134. if (this.props.onError) {
  135. this.props.onError(msg, info);
  136. }
  137. }
  138. errorHandler(error) {
  139. const { locale } = this.state;
  140. const localResponse = LOCALES_RESPONSE[locale];
  141. if (error.response && error.response.data && error.response.data.msg) {
  142. this.error(localResponse[error.response.data.msg] || ERROR_DEFAULT, {
  143. response: error.response
  144. });
  145. return;
  146. }
  147. this.error(localResponse[error.response.data.msg] || ERROR_DEFAULT, {
  148. response: error.response
  149. });
  150. }
  151. /**
  152. * 改变 loading 状态
  153. * @param {string} key key
  154. * @param {string} value value
  155. */
  156. handleChangeLoading(key, value) {
  157. const { loading } = this.state;
  158. loading[key] = value;
  159. this.setState({ loading });
  160. }
  161. /**
  162. * 获取评论列表
  163. */
  164. sGetComment({ page = 1 } = {}) {
  165. const { pageType } = this.props;
  166. this.handleChangeLoading("sGetComment", true);
  167. const { API, type, businessId, limit } = this.props;
  168. this.axios
  169. .get(
  170. `${API}/comments?type=${type}&business_id=${businessId}&page=${page}&limit=${limit}`
  171. )
  172. .then(response => {
  173. const { list, page, total } = response.data;
  174. if (list) {
  175. let newList = list;
  176. let { list: oldList } = this.state;
  177. if (pageType === "more") {
  178. if (page > 1) {
  179. // 删除临时数据
  180. oldList = oldList.filter(o => !o.isTemporary);
  181. newList = oldList.concat(newList);
  182. }
  183. } else if (pageType === "pagination") {
  184. // TODO 滚动到顶部
  185. window.scrollTo(0, 0);
  186. }
  187. this.setState({
  188. list: newList,
  189. page,
  190. total
  191. });
  192. this.props.onCountChange(total);
  193. } else {
  194. message.info(intl.get("message.noMoreComment"));
  195. this.setState({
  196. isNoMoreComment: true
  197. });
  198. }
  199. })
  200. .catch(this.errorHandler)
  201. .finally(() => {
  202. this.handleChangeLoading("sGetComment", false);
  203. });
  204. }
  205. /**
  206. * 获取更多回复
  207. */
  208. sGetReply({ commentId, page = 1 } = {}) {
  209. this.handleChangeLoading("sGetReply", true);
  210. const { API, limit } = this.props;
  211. this.axios
  212. .get(`${API}/replies?comment_id=${commentId}&page=${page}&limit=${limit}`)
  213. .then(response => {
  214. if (!response.data.list) {
  215. message.info(intl.get("message.noMoreData"));
  216. }
  217. const list = this.state.list.map(item => {
  218. if (item.id === commentId) {
  219. if (!item.replies) item.replies = [];
  220. if (response.data.list) {
  221. if (page === 1) {
  222. // 如果当前页数为第一页,则清空当前所有的 replies
  223. // 并将获取到的 replies 存放在 state
  224. item.replies = response.data.list;
  225. } else {
  226. item.replies = item.replies
  227. .filter(o => !o.isTemporary)
  228. .concat(response.data.list);
  229. // 如果当前页数非第一页,则合并 replies
  230. }
  231. item.reply_count = response.data.total;
  232. item.reply_page = response.data.page;
  233. } else {
  234. item.isNoMoreReply = true;
  235. }
  236. }
  237. return item;
  238. });
  239. this.setState({ list });
  240. })
  241. .catch(this.errorHandler)
  242. .finally(() => {
  243. this.handleChangeLoading("sGetReply", false);
  244. });
  245. }
  246. /**
  247. * 添加评论
  248. * @param {object} {content} comment content
  249. */
  250. sCreateComment({ content } = {}, cb) {
  251. if (!content) return this.error(intl.get("message.notNull"));
  252. this.handleChangeLoading("sCreateComment", true);
  253. const { API, type, businessId, businessUserId } = this.props;
  254. this.axios(`${API}/comments`, {
  255. method: "post",
  256. data: {
  257. type,
  258. business_id: businessId,
  259. business_user_id: businessUserId,
  260. content
  261. },
  262. withCredentials: true
  263. })
  264. .then(response => {
  265. if (this.props.showAlertComment) {
  266. message.success(intl.get("message.success"));
  267. }
  268. if (isFunction(cb)) cb(response.data);
  269. // 将数据写入到 list 中
  270. // 临时插入
  271. // 等到获取数据之后,删除临时数据
  272. const { list, total } = this.state;
  273. list.unshift({
  274. ...response.data,
  275. isTemporary: true // 临时的数据
  276. });
  277. this.setState({ list, total: total + 1 });
  278. this.props.onCountChange(total + 1);
  279. })
  280. .catch(this.errorHandler)
  281. .finally(() => {
  282. this.handleChangeLoading("sCreateComment", false);
  283. });
  284. }
  285. /**
  286. * 删除评论
  287. */
  288. sDeleteComment(commentId) {
  289. this.handleChangeLoading("sDeleteComment", true);
  290. const { API } = this.props;
  291. this.axios(`${API}/comments/${commentId}`, {
  292. method: "delete",
  293. withCredentials: true
  294. })
  295. .then(() => {
  296. const { list, total } = this.state;
  297. const res = list.filter(item => item.id !== commentId);
  298. const deletedItem = list.find(item => item.id === commentId);
  299. this.setState({ list: res, total: total - 1 });
  300. this.props.onDelete(COMMENT_TYPE.COMMENT, deletedItem);
  301. this.props.onCountChange(total - 1);
  302. })
  303. .catch(this.errorHandler)
  304. .finally(() => {
  305. this.handleChangeLoading("sDeleteComment", false);
  306. });
  307. }
  308. /**
  309. * 更新评论
  310. * @param {object} {content} comment content
  311. */
  312. sUpdateComment({ commentId, content }) {
  313. this.handleChangeLoading("sUpdateComment", true);
  314. const { API } = this.props;
  315. this.axios(`${API}/comments/${commentId}`, {
  316. method: "post",
  317. data: {
  318. content
  319. },
  320. withCredentials: true
  321. })
  322. .then(() => {
  323. let { list } = this.state;
  324. list = list.map(it => {
  325. if (it.id === commentId) {
  326. return {
  327. ...it,
  328. content
  329. };
  330. }
  331. return it;
  332. });
  333. this.props.onUpdateComment("comment");
  334. this.setState({ list });
  335. })
  336. .catch(this.errorHandler)
  337. .finally(() => {
  338. this.handleChangeLoading("sUpdateComment", false);
  339. });
  340. }
  341. /**
  342. * 添加回复
  343. * 回复评论/回复回复
  344. * @param {object} data { comment_id, content, [reply_id] }
  345. */
  346. sCreateReply(data, cb) {
  347. if (!data.content) return this.error(intl.get("message.replyNoNull"));
  348. this.handleChangeLoading("sCreateReply", true);
  349. const { API } = this.props;
  350. this.axios(`${API}/replies`, {
  351. method: "post",
  352. data,
  353. withCredentials: true
  354. })
  355. .then(response => {
  356. if (this.props.showAlertReply) {
  357. message.success(intl.get("message.replySuccess"));
  358. }
  359. if (isFunction(cb)) cb(response.data);
  360. // 将数据写入到 list 中
  361. // 临时插入
  362. // 等到获取数据之后,删除临时数据
  363. const list = this.state.list.map(item => {
  364. if (item.id === data.comment_id) {
  365. if (!item.replies) item.replies = [];
  366. item.replies.push({
  367. ...response.data,
  368. isTemporary: true // 临时的数据
  369. });
  370. item.reply_count += 1;
  371. }
  372. return item;
  373. });
  374. this.setState({ list });
  375. })
  376. .catch(this.errorHandler)
  377. .finally(() => {
  378. this.handleChangeLoading("sCreateReply", false);
  379. });
  380. }
  381. /**
  382. * 删除回复
  383. * @param {*} replyId
  384. * @param {*} commentId
  385. */
  386. sDeleteReply(replyId, commentId) {
  387. this.handleChangeLoading("sDeleteReply", true);
  388. const { API } = this.props;
  389. this.axios(`${API}/replies/${replyId}?CommentID=${commentId}`, {
  390. method: "delete",
  391. withCredentials: true
  392. })
  393. .then(() => {
  394. let deletedItem = null;
  395. const list = this.state.list.map(item => {
  396. if (item.id === commentId) {
  397. const replies = item.replies.filter(item => item.id !== replyId);
  398. deletedItem = item.replies.find(item => item.id === replyId);
  399. item.replies = replies;
  400. item.reply_count -= 1;
  401. }
  402. return item;
  403. });
  404. this.setState({ list });
  405. this.props.onDelete(COMMENT_TYPE.REPLY, deletedItem);
  406. })
  407. .catch(this.errorHandler)
  408. .finally(() => {
  409. this.handleChangeLoading("sDeleteReply", false);
  410. });
  411. }
  412. /**
  413. * 更新回复
  414. * 回复评论/回复回复
  415. * @param {object} data { comment_id, content, reply_id }
  416. */
  417. sUpdateReply({ commentId, content, replyId, replyPage }) {
  418. this.handleChangeLoading("sUpdateReply", true);
  419. const { API } = this.props;
  420. this.axios(`${API}/replies/${replyId}`, {
  421. method: "post",
  422. data: {
  423. comment_id: commentId,
  424. content
  425. },
  426. withCredentials: true
  427. })
  428. .then(() => {
  429. for (let i = 1; i <= replyPage; i++) {
  430. this.sGetReply({ commentId, page: i });
  431. }
  432. this.props.onUpdateComment("reply");
  433. })
  434. .catch(this.errorHandler)
  435. .finally(() => {
  436. this.handleChangeLoading("sUpdateReply", false);
  437. });
  438. }
  439. /**
  440. * 评论 点赞/取消点赞
  441. * @param {string} commentId { commentId }
  442. * @param {boolean} favored 是否已经点过赞
  443. */
  444. sCommentFavor(commentId, favored) {
  445. this.handleChangeLoading("sCommentFavor", true);
  446. const { API } = this.props;
  447. this.axios(`${API}/comments/${commentId}/favor`, {
  448. method: favored ? "delete" : "put",
  449. withCredentials: true
  450. })
  451. .then(response => {
  452. if (this.props.showAlertFavor) {
  453. message.success(
  454. favored
  455. ? intl.get("message.cancelLickSuccess")
  456. : intl.get("message.likeSuccess")
  457. );
  458. }
  459. // 更新 list 中的该项数据的 favored
  460. const list = this.state.list.map(item => {
  461. if (item.id === commentId) {
  462. item.favored = !favored;
  463. item.favor_count += favored ? -1 : 1;
  464. }
  465. return item;
  466. });
  467. this.setState({ list });
  468. })
  469. .catch(this.errorHandler)
  470. .finally(() => {
  471. this.handleChangeLoading("sCommentFavor", false);
  472. });
  473. }
  474. /**
  475. * 回复 点赞/取消点赞
  476. * @param {string} replyId replyId
  477. * @param {string} commentId commentId
  478. * @param {boolean} favored 是否已经点过赞
  479. */
  480. sReplyFavor(replyId, commentId, favored) {
  481. this.handleChangeLoading("sReplyFavor", true);
  482. const { API } = this.props;
  483. this.axios(`${API}/replies/${replyId}/favor`, {
  484. method: favored ? "delete" : "put",
  485. data: {
  486. comment_id: commentId
  487. },
  488. withCredentials: true
  489. })
  490. .then(response => {
  491. message.success(
  492. favored
  493. ? intl.get("message.cancelLickSuccess")
  494. : intl.get("message.likeSuccess")
  495. );
  496. // 更新 list 中的该项数据的 favored
  497. const list = this.state.list.map(item => {
  498. if (item.id === commentId) {
  499. item.replies = item.replies.map(r => {
  500. if (r.id === replyId) {
  501. r.favored = !favored;
  502. // r.favor_count = response.data.favor_count;
  503. // 点赞数 +1,而不是使用后端返回的点赞数
  504. // 不然如果返回的不是增加 1,用户可能以为程序错误
  505. r.favor_count += favored ? -1 : 1;
  506. }
  507. return r;
  508. });
  509. }
  510. return item;
  511. });
  512. this.setState({ list });
  513. })
  514. .catch(this.errorHandler)
  515. .finally(() => {
  516. this.handleChangeLoading("sReplyFavor", false);
  517. });
  518. }
  519. /**
  520. * 获取 OSS 上传的参数
  521. */
  522. sOssSts() {
  523. this.handleChangeLoading("sOssSts", true);
  524. const { API } = this.props;
  525. this.axios
  526. .get(`${API}/oss/sts`)
  527. .then(response => {
  528. this.setState({ oss: { ...response.data } });
  529. })
  530. .catch(this.errorHandler)
  531. .finally(() => {
  532. this.handleChangeLoading("sOssSts", false);
  533. });
  534. }
  535. render() {
  536. // 添加到 Context 的数据
  537. const value = {
  538. ...this.state,
  539. ...this.props,
  540. sCreateComment: this.sCreateComment,
  541. sGetComment: this.sGetComment,
  542. sCommentFavor: this.sCommentFavor,
  543. sReplyFavor: this.sReplyFavor,
  544. sCreateReply: this.sCreateReply,
  545. sGetReply: this.sGetReply,
  546. sOssSts: this.sOssSts,
  547. sDeleteComment: this.sDeleteComment,
  548. sDeleteReply: this.sDeleteReply,
  549. sUpdateReply: this.sUpdateReply,
  550. sUpdateComment: this.sUpdateComment,
  551. handleEdit: this.handleEdit
  552. };
  553. return (
  554. this.state.initDone && (
  555. <CommentContext.Provider value={value}>
  556. <div className="comment">
  557. {this.props.showEditor && (
  558. <CommentInput content={this.props.children} />
  559. )}
  560. {this.props.showList && (
  561. <div style={{ marginTop: 20 }}>
  562. <CommentList />
  563. </div>
  564. )}
  565. </div>
  566. {this.state.editModalVisible && (
  567. <EditComment
  568. visible={this.state.editModalVisible}
  569. action={this.state.action}
  570. replyId={this.state.replyId}
  571. replyPage={this.state.replyPage}
  572. commentId={this.state.commentId}
  573. userId={this.state.content.user_id}
  574. content={this.state.content}
  575. handleClose={this.handleClose}
  576. />
  577. )}
  578. </CommentContext.Provider>
  579. )
  580. );
  581. }
  582. }
  583. App.propTypes = {
  584. type: PropTypes.number.isRequired, // 评论的 type
  585. businessId: PropTypes.string.isRequired, // 评论的 business_id
  586. businessUserId: PropTypes.number,
  587. API: PropTypes.string, // 评论的 API 前缀
  588. showList: PropTypes.bool, // 是否显示评论列表
  589. showEditor: PropTypes.bool, // 是否显示评论输入框
  590. showAlertComment: PropTypes.bool, // 评论成功之后,是否通过 Antd 的 Message 组件进行提示
  591. showAlertReply: PropTypes.bool, // 回复成功之后,是否通过 Antd 的 Message 组件进行提示
  592. showAlertFavor: PropTypes.bool, // 点赞/取消点赞成功之后,是否通过 Antd 的 Message 组件进行提示
  593. showError: PropTypes.bool, // 是否使用Antd的Message组件提示错误信息
  594. onError: PropTypes.func, // 错误回调, 出错了会被调用
  595. userId: PropTypes.number, // 用户id, comment内部不维护用户id, 调用组件时传递过来, 目前用于判断是否显示删除按钮
  596. pageType: PropTypes.string, // 分页类型
  597. page: PropTypes.number, // 页码
  598. limit: PropTypes.number, // 一次加载评论数量
  599. onPageChange: PropTypes.func, // 页码变化回调
  600. onGetMoreBtnClick: PropTypes.func, // 点击查看更多按钮回调
  601. onDelete: PropTypes.func,
  602. onUpdateComment: PropTypes.func,
  603. locales: PropTypes.string, // 传入的语言环境, en-US/zh-CN
  604. onCountChange: PropTypes.func // 评论数量变更时的回调
  605. };
  606. App.defaultProps = {
  607. businessUserId: 0,
  608. API: "//api.links123.net/comment/v1",
  609. showList: true,
  610. showEditor: true,
  611. showAlertComment: false,
  612. showAlertReply: false,
  613. showAlertFavor: false,
  614. showError: true,
  615. showEdit: false,
  616. pageType: "more",
  617. limit: LIMIT,
  618. onGetMoreBtnClick: () => {},
  619. onPageChange: page => {},
  620. onDelete: () => {},
  621. onUpdateComment: () => {},
  622. onReforeUpdateComment: () => {},
  623. onCountChange: () => {}
  624. };
  625. export { Editor, RenderText };
  626. export default App;