通用评论 vedio

App.js 15KB

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