通用评论

App.js 15KB

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