import React, { Component } from "react"; import PropTypes from "prop-types"; import { message } from "antd"; import axios from "axios"; import Cookies from "js-cookie"; import intl from "react-intl-universal"; import { ERROR_DEFAULT, LIMIT, COMMENT_TYPE, LANGUAGE_LINK } from "./constant"; import { CommentContext } from "./Comment"; import { isFunction } from "./helper"; import CommentInput from "./components/CommentInput"; import CommentList from "./components/CommentList"; import Editor from "./components/Editor"; import RenderText from "./components/RenderText"; import EditComment from "./components/EditComment/EditComment"; import { SUPPORT_LOCALES, LOCALES_RESPONSE } from "./lang"; import "./App.css"; class App extends Component { constructor(props) { super(props); this.state = { loading: {}, // oss 配置 oss: {}, // 评论数据 list: [], page: 1, total: 0, // 是否没有更多评论了 isNoMoreComment: false, initDone: false, locale: "zh-CN", editModalVisible: false, action: "", replyId: "", commentId: "", userId: "", content: "", replyPage: 1, emojiList: [] }; this.handleChangeLoading = this.handleChangeLoading.bind(this); this.sCreateComment = this.sCreateComment.bind(this); this.sUpdateComment = this.sUpdateComment.bind(this); this.sDeleteComment = this.sDeleteComment.bind(this); this.sCommentFavor = this.sCommentFavor.bind(this); this.sCreateReply = this.sCreateReply.bind(this); this.sDeleteReply = this.sDeleteReply.bind(this); this.errorHandler = this.errorHandler.bind(this); this.sGetComment = this.sGetComment.bind(this); this.sReplyFavor = this.sReplyFavor.bind(this); this.sGetReply = this.sGetReply.bind(this); this.sUpdateReply = this.sUpdateReply.bind(this); this.sOssSts = this.sOssSts.bind(this); this.handleEdit = this.handleEdit.bind(this); this.handleClose = this.handleClose.bind(this); } componentWillMount() { this.axios = axios; this.axios.defaults.withCredentials = true; if (this.props.token) { this.axios.defaults.headers.common["Authorization"] = `Bearer ${ this.props.token }`; } } componentDidMount() { this.loadLocales(); this.loadEmoji(); } /** * 加载语言包 * 只能根据url或者传入的props来确定加载哪个语言包 * 优先级:传入的props > url */ loadLocales() { let { locales: currentLocale } = this.props; const cookieLang = Cookies.get("lnk_lang"); if (!currentLocale) { currentLocale = cookieLang || intl.determineLocale({ urlLocaleKey: "lang" }); } currentLocale = SUPPORT_LOCALES.find(item => item.value === currentLocale) ? currentLocale : "zh-CN"; const version = require("./version.json").hash; // 使用绝对路径 const languageURL = `${LANGUAGE_LINK}/${currentLocale}.json?v=${version}`; return fetch(languageURL) .then(response => { if (response.status >= 400) { throw new Error("Bad response from server"); } return response.json(); }) .then(data => { // console.log('data: ', data); intl .init({ currentLocale, locales: { [currentLocale]: data } }) .then(() => { this.setState({ initDone: true, locale: currentLocale }); }); }); } /** * 加载表情包 */ loadEmoji() { this.axios .get(`${this.props.emojiAPI}/emoticons`) .then(response => { const emojiMap = {}; response.data.list.forEach(item => { emojiMap[item.name] = item.url; }); window.sessionStorage.setItem("emojiMap", JSON.stringify(emojiMap)); this.setState({ emojiList: response.data.list }); }) .catch(this.errorHandler); } handleEdit({ replyId, commentId, userId, content, replyPage }) { this.setState({ editModalVisible: true, action: content.replies ? "comment" : content.reply ? "replyToReply" : "reply", replyId, commentId, userId, content, replyPage }); } handleClose() { this.setState({ editModalVisible: false }); } error(msg, info = {}) { if (this.props.showError) { message.error(msg); } if (this.props.onError) { this.props.onError(msg, info); } } errorHandler(error) { const { locale } = this.state; const localResponse = LOCALES_RESPONSE[locale]; if (error.response && error.response.data && error.response.data.msg) { this.error(localResponse[error.response.data.msg] || ERROR_DEFAULT, { response: error.response }); return; } this.error(localResponse[error.response.data.msg] || ERROR_DEFAULT, { response: error.response }); } /** * 改变 loading 状态 * @param {string} key key * @param {string} value value */ handleChangeLoading(key, value) { const { loading } = this.state; loading[key] = value; this.setState({ loading }); } /** * 获取评论列表 */ sGetComment({ page = 1, filterSpeak = 0 } = {}) { const { pageType } = this.props; this.handleChangeLoading("sGetComment", true); const { API, type, businessId, limit } = this.props; this.axios .get( `${API}/comments?type=${type}&business_id=${businessId}&is_speak=${filterSpeak}&page=${page}&limit=${limit}` ) .then(response => { const { list, page, total } = response.data; if (list) { let newList = list; let { list: oldList } = this.state; if (pageType === "more") { if (page > 1) { // 删除临时数据 oldList = oldList.filter(o => !o.isTemporary); newList = oldList.concat(newList); } } else if (pageType === "pagination") { // TODO 滚动到顶部 window.scrollTo(0, 0); } this.setState({ list: newList, page, total }); this.props.onCountChange(total); } else { message.info(intl.get("message.noMoreComment")); this.setState({ isNoMoreComment: true }); } }) .catch(this.errorHandler) .finally(() => { this.handleChangeLoading("sGetComment", false); }); } /** * 获取更多回复 */ sGetReply({ commentId, page = 1 } = {}) { this.handleChangeLoading("sGetReply", true); const { API, limit } = this.props; this.axios .get(`${API}/replies?comment_id=${commentId}&page=${page}&limit=${limit}`) .then(response => { if (!response.data.list) { message.info(intl.get("message.noMoreData")); } const list = this.state.list.map(item => { if (item.id === commentId) { if (!item.replies) item.replies = []; if (response.data.list) { if (page === 1) { // 如果当前页数为第一页,则清空当前所有的 replies // 并将获取到的 replies 存放在 state item.replies = response.data.list; } else { item.replies = item.replies .filter(o => !o.isTemporary) .concat(response.data.list); // 如果当前页数非第一页,则合并 replies } item.reply_count = response.data.total; item.reply_page = response.data.page; } else { item.isNoMoreReply = true; } } return item; }); this.setState({ list }); }) .catch(this.errorHandler) .finally(() => { this.handleChangeLoading("sGetReply", false); }); } /** * 添加评论 * @param {object} {content} comment content */ sCreateComment({ content } = {}, cb) { if (!content) return this.error(intl.get("message.notNull")); this.handleChangeLoading("sCreateComment", true); const { API, type, businessId, businessUserId } = this.props; this.axios(`${API}/comments`, { method: "post", data: { type, business_id: businessId, business_user_id: businessUserId, content }, withCredentials: true }) .then(response => { if (this.props.showAlertComment) { message.success(intl.get("message.success")); } if (isFunction(cb)) cb(response.data); // 将数据写入到 list 中 // 临时插入 // 等到获取数据之后,删除临时数据 const { list, total } = this.state; list.unshift({ ...response.data, replies: [], isTemporary: true // 临时的数据 }); this.setState({ list, total: total + 1 }); this.props.onCountChange(total + 1); }) .catch(error => { this.props.onCommentFail(error.response.status, error.response.data); this.errorHandler(error); }) .finally(() => { this.handleChangeLoading("sCreateComment", false); }); } /** * 删除评论 */ sDeleteComment(commentId) { this.handleChangeLoading("sDeleteComment", true); const { API } = this.props; this.axios(`${API}/comments/${commentId}`, { method: "delete", withCredentials: true }) .then(() => { const { list, total } = this.state; const res = list.filter(item => item.id !== commentId); const deletedItem = list.find(item => item.id === commentId); this.setState({ list: res, total: total - 1 }); this.props.onDelete(COMMENT_TYPE.COMMENT, deletedItem); this.props.onCountChange(total - 1); }) .catch(this.errorHandler) .finally(() => { this.handleChangeLoading("sDeleteComment", false); }); } /** * 更新评论 * @param {object} {content} comment content */ sUpdateComment({ commentId, content, successCallback }) { this.handleChangeLoading("sUpdateComment", true); const { API } = this.props; this.axios(`${API}/comments/${commentId}`, { method: "post", data: { content }, withCredentials: true }) .then(() => { let { list } = this.state; list = list.map(it => { if (it.id === commentId) { return { ...it, content }; } return it; }); this.props.onUpdateComment("comment"); this.setState({ list }); if (successCallback) { successCallback(); } }) .catch(error => { this.props.onCommentFail(error.response.status, error.response.data); this.errorHandler(error); }) .finally(() => { this.handleChangeLoading("sUpdateComment", false); }); } /** * 添加回复 * 回复评论/回复回复 * @param {object} data { comment_id, content, [reply_id] } */ sCreateReply(data, cb) { if (!data.content) return this.error(intl.get("message.replyNoNull")); this.handleChangeLoading("sCreateReply", true); const { API } = this.props; this.axios(`${API}/replies`, { method: "post", data, withCredentials: true }) .then(response => { if (this.props.showAlertReply) { message.success(intl.get("message.replySuccess")); } if (isFunction(cb)) cb(response.data); // 将数据写入到 list 中 // 临时插入 // 等到获取数据之后,删除临时数据 const list = this.state.list.map(item => { if (item.id === data.comment_id) { if (!item.replies) item.replies = []; item.replies.push({ ...response.data, isTemporary: true // 临时的数据 }); item.reply_count += 1; } return item; }); this.setState({ list }); }) .catch(error => { this.props.onCommentFail(error.response.status, error.response.data); this.errorHandler(error); }) .finally(() => { this.handleChangeLoading("sCreateReply", false); }); } /** * 删除回复 * @param {*} replyId * @param {*} commentId */ sDeleteReply(replyId, commentId) { this.handleChangeLoading("sDeleteReply", true); const { API } = this.props; this.axios(`${API}/replies/${replyId}?CommentID=${commentId}`, { method: "delete", withCredentials: true }) .then(() => { let deletedItem = null; const list = this.state.list.map(item => { if (item.id === commentId) { const replies = item.replies.filter(item => item.id !== replyId); deletedItem = item.replies.find(item => item.id === replyId); item.replies = replies; item.reply_count -= 1; } return item; }); this.setState({ list }); this.props.onDelete(COMMENT_TYPE.REPLY, deletedItem); }) .catch(this.errorHandler) .finally(() => { this.handleChangeLoading("sDeleteReply", false); }); } /** * 更新回复 * 回复评论/回复回复 * @param {object} data { comment_id, content, reply_id } */ sUpdateReply({ commentId, content, replyId, replyPage, successCallback }) { this.handleChangeLoading("sUpdateReply", true); const { API } = this.props; this.axios(`${API}/replies/${replyId}`, { method: "post", data: { comment_id: commentId, content }, withCredentials: true }) .then(() => { for (let i = 1; i <= replyPage; i++) { this.sGetReply({ commentId, page: i }); } if (successCallback) { successCallback(); } this.props.onUpdateComment("reply"); }) .catch(error => { this.props.onCommentFail(error.response.status, error.response.data); this.errorHandler(error); }) .finally(() => { this.handleChangeLoading("sUpdateReply", false); }); } /** * 评论 点赞/取消点赞 * @param {string} commentId { commentId } * @param {boolean} favored 是否已经点过赞 */ sCommentFavor(commentId, favored) { this.handleChangeLoading("sCommentFavor", true); const { API } = this.props; this.axios(`${API}/comments/${commentId}/favor`, { method: favored ? "delete" : "put", withCredentials: true }) .then(response => { if (this.props.showAlertFavor) { message.success( favored ? intl.get("message.cancelLickSuccess") : intl.get("message.likeSuccess") ); } // 更新 list 中的该项数据的 favored const list = this.state.list.map(item => { if (item.id === commentId) { item.favored = !favored; item.favor_count += favored ? -1 : 1; } return item; }); this.setState({ list }); }) .catch(this.errorHandler) .finally(() => { this.handleChangeLoading("sCommentFavor", false); }); } /** * 回复 点赞/取消点赞 * @param {string} replyId replyId * @param {string} commentId commentId * @param {boolean} favored 是否已经点过赞 */ sReplyFavor(replyId, commentId, favored) { this.handleChangeLoading("sReplyFavor", true); const { API } = this.props; this.axios(`${API}/replies/${replyId}/favor`, { method: favored ? "delete" : "put", data: { comment_id: commentId }, withCredentials: true }) .then(response => { message.success( favored ? intl.get("message.cancelLickSuccess") : intl.get("message.likeSuccess") ); // 更新 list 中的该项数据的 favored const list = this.state.list.map(item => { if (item.id === commentId) { item.replies = item.replies.map(r => { if (r.id === replyId) { r.favored = !favored; // r.favor_count = response.data.favor_count; // 点赞数 +1,而不是使用后端返回的点赞数 // 不然如果返回的不是增加 1,用户可能以为程序错误 r.favor_count += favored ? -1 : 1; } return r; }); } return item; }); this.setState({ list }); }) .catch(this.errorHandler) .finally(() => { this.handleChangeLoading("sReplyFavor", false); }); } /** * 获取 OSS 上传的参数 */ sOssSts() { this.handleChangeLoading("sOssSts", true); const { API } = this.props; this.axios .get(`${API}/oss/sts`) .then(response => { this.setState({ oss: { ...response.data } }); }) .catch(this.errorHandler) .finally(() => { this.handleChangeLoading("sOssSts", false); }); } render() { // 添加到 Context 的数据 const value = { ...this.state, ...this.props, sCreateComment: this.sCreateComment, sGetComment: this.sGetComment, sCommentFavor: this.sCommentFavor, sReplyFavor: this.sReplyFavor, sCreateReply: this.sCreateReply, sGetReply: this.sGetReply, sOssSts: this.sOssSts, sDeleteComment: this.sDeleteComment, sDeleteReply: this.sDeleteReply, sUpdateReply: this.sUpdateReply, sUpdateComment: this.sUpdateComment, handleEdit: this.handleEdit }; return ( this.state.initDone && (
{this.props.showEditor && ( )} {this.props.showList && (
)}
{this.state.editModalVisible && ( )}
) ); } } App.propTypes = { type: PropTypes.number.isRequired, // 评论的 type businessId: PropTypes.string.isRequired, // 评论的 business_id businessUserId: PropTypes.number, API: PropTypes.string, // 评论的 API 前缀 showList: PropTypes.bool, // 是否显示评论列表 showEditor: PropTypes.bool, // 是否显示评论输入框 showAlertComment: PropTypes.bool, // 评论成功之后,是否通过 Antd 的 Message 组件进行提示 showAlertReply: PropTypes.bool, // 回复成功之后,是否通过 Antd 的 Message 组件进行提示 showAlertFavor: PropTypes.bool, // 点赞/取消点赞成功之后,是否通过 Antd 的 Message 组件进行提示 showError: PropTypes.bool, // 是否使用Antd的Message组件提示错误信息 onError: PropTypes.func, // 错误回调, 出错了会被调用 userId: PropTypes.number, // 用户id, comment内部不维护用户id, 调用组件时传递过来, 目前用于判断是否显示删除按钮 pageType: PropTypes.string, // 分页类型 page: PropTypes.number, // 页码 limit: PropTypes.number, // 一次加载评论数量 onPageChange: PropTypes.func, // 页码变化回调 onGetMoreBtnClick: PropTypes.func, // 点击查看更多按钮回调 onDelete: PropTypes.func, onUpdateComment: PropTypes.func, locales: PropTypes.string, // 传入的语言环境, en-US/zh-CN onCountChange: PropTypes.func, // 评论数量变更时的回调 onCommentFail: PropTypes.func, // 评论失败时的回调 preRenderValue: PropTypes.func // 编辑器渲染前对值需要做的工作 }; App.defaultProps = { businessUserId: 0, API: "//api.links123.net/comment/v1", emojiAPI: "//api.links123.net/link/v1", LOGINLINK: process.env.RUN_MOD === "production" ? "https://passport.links123.com/login" : "http://test.links123.net:5050/login", showList: true, showEditor: true, showAlertComment: false, showAlertReply: false, showAlertFavor: false, showError: true, showEdit: false, pageType: "more", limit: LIMIT, onGetMoreBtnClick: () => {}, onPageChange: page => {}, onDelete: () => {}, onUpdateComment: () => {}, onBeforeUpdateComment: () => {}, onCountChange: () => {}, onCommentFail: () => {}, preRenderValue: v => v }; export { Editor, RenderText }; export default App;