261 lines
13 KiB
JavaScript
261 lines
13 KiB
JavaScript
import React, { useEffect, useState } from 'react';
|
||
import { Button, Popconfirm, Input, message, Spin, Pagination } from 'antd';
|
||
import { getImageUrl, timeAgo, isPhone } from 'educoder';
|
||
import RenderHtml from '../render-html';
|
||
import Attachment from '../../forge/Upload/attachment';
|
||
import EditComment from './EditComment';
|
||
import CheckProfile from '../../forge/Component/ProfileModal/Profile';
|
||
import './comments.scss';
|
||
import { Link } from 'react-router-dom';
|
||
import { normalizeUrl } from '../../forge/common/util';
|
||
import { COMMENT_PAGE_SIZE, COMMENT_CATEGORY } from './constants';
|
||
import * as commentApi from './api';
|
||
|
||
/**
|
||
* 通用评论列表组件
|
||
* @param {Object} props - 组件属性
|
||
* @property {string} type - 评论类型:'news' | 'project' | 'resource' | 'skill'
|
||
* @property {string|number} id - 资源ID(文章ID、项目ID等)
|
||
* @property {Object} [extraParams] - 额外的业务参数
|
||
* @property {string} [owner] - 所有者/组织名(用于 Markdown 渲染)
|
||
* @property {string} [projectsId] - 项目 ID(用于 Markdown 渲染)
|
||
* @property {Object} current_user - 当前用户信息
|
||
* @property {string} current_user.login - 用户登录名
|
||
* @property {boolean} current_user.admin - 是否平台管理员
|
||
* @property {string} current_user.image_url - 用户头像URL
|
||
* @property {string} current_user.username - 用户姓名
|
||
* @property {boolean} [isManager] - 是否为仓库/项目管理员
|
||
* @property {Function} [showNotification] - 显示通知
|
||
* @property {Function} [showLoginDialog] - 显示登录弹窗
|
||
* @property {string} [author] - 评论作者(用于权限判断)
|
||
*/
|
||
function CommentsList(props){
|
||
const {type, id, extraParams, owner, projectsId, current_user, isManager, showLoginDialog, author, showNotification} = props;
|
||
const {login, admin, image_url, username} = current_user;
|
||
|
||
const [category, setCategory] = useState('comment');
|
||
const [journals, setJournals] = useState(undefined);
|
||
// 是否展示新建/编辑评论markdown部分新建评论1 编辑父级评论2 回复评论3 编辑回复内容4 回复子机评论5
|
||
const [showEdit, setShowEdit] = useState(false);
|
||
const [parentId, setParentId] = useState(undefined);
|
||
const [replyId, setReplyId] = useState(undefined);
|
||
// 修改评论id
|
||
const [updateId, setUpdateId] = useState(undefined);
|
||
// 操作日志展开效果
|
||
const [open, setOpen] = useState(undefined);
|
||
// 加载中效果(缓冲css 线条效果)
|
||
const [spin, setSpin] = useState(false);
|
||
// 评论总数
|
||
const [journalsCount, setJournalsCount] = useState(undefined);
|
||
// 分页
|
||
const limit = COMMENT_PAGE_SIZE;
|
||
const [page, setPage] = useState(1);
|
||
const [totalCount, setTotalCount] = useState(undefined);
|
||
const [reload, setReload] = useState(undefined);
|
||
|
||
// 根据 type 加载评论列表
|
||
useEffect(()=>{
|
||
setSpin(true);
|
||
let params = {
|
||
limit: limit,
|
||
page: page
|
||
};
|
||
commentApi.getCommentList(type, id, page, limit).then(res=>{
|
||
if(res && res.data && res.data.data){
|
||
const {journals: journalList, total_comment_journals_count, total_count} = res.data.data;
|
||
if(category === COMMENT_CATEGORY.ALL){
|
||
const array = [];
|
||
let start = undefined;
|
||
let isJournalCount = 0;
|
||
journalList.map((item, index)=>{
|
||
!isJournalCount && (start = index);
|
||
item.is_journal_detail && (isJournalCount++);
|
||
if(!item.is_journal_detail && isJournalCount < 10){
|
||
isJournalCount = 0;
|
||
return;
|
||
}
|
||
if(isJournalCount >= 10 && (!item.is_journal_detail || journalList.length-1 === index)){
|
||
array.push({start, count: isJournalCount});
|
||
isJournalCount = 0;
|
||
}
|
||
});
|
||
array.map((item, i)=>{
|
||
journalList[item.start].numCount = item.count-1;
|
||
for (let index = 0; index < item.count; index++) {
|
||
journalList[item.start+index].start = journalList[item.start].id;
|
||
journalList[item.start+index].closeAndSpan = true;
|
||
}
|
||
});
|
||
}
|
||
setTotalCount(total_count);
|
||
setJournalsCount(total_comment_journals_count);
|
||
setJournals(journalList);
|
||
setSpin(false);
|
||
}
|
||
}).catch(err => {
|
||
console.error('获取评论列表失败:', err);
|
||
setSpin(false);
|
||
message.error('获取评论列表失败');
|
||
});
|
||
}, [type, id, reload, category, page]);
|
||
|
||
// 刷新评论列表的回调
|
||
const reloadComment = () => {
|
||
setReload(Math.random());
|
||
};
|
||
|
||
function commentCtx(v){
|
||
return <RenderHtml owner={owner} projectsId={projectsId} className="break_word_comments imageLayerParent commentRenderHtml" value={v} url={{pathname: ''}}/>;
|
||
};
|
||
|
||
// 删除评论内容
|
||
function deleteCommentFunc(commentId){
|
||
commentApi.deleteComment(type, id, commentId).then(res=>{
|
||
if(res && res.data && res.data.code === 200){
|
||
reloadComment();
|
||
message.success('删除成功');
|
||
}
|
||
}).catch(err => {
|
||
console.error('删除评论失败:', err);
|
||
message.error('删除评论失败');
|
||
});
|
||
};
|
||
|
||
// markdown 取消操作
|
||
function cancelMd(){
|
||
setShowEdit(false);
|
||
};
|
||
|
||
// 创建不包含已解构属性的 props 对象传递给子组件
|
||
const childProps = {
|
||
...props,
|
||
type,
|
||
id,
|
||
extraParams,
|
||
owner,
|
||
projectsId,
|
||
current_user,
|
||
isManager,
|
||
showLoginDialog,
|
||
author,
|
||
showNotification,
|
||
reloadComment,
|
||
cancelMd
|
||
};
|
||
|
||
return(
|
||
(isPhone() && journalsCount === 0) ? '' : <div className="commentListBox info_text">
|
||
{/* 评论快速入口-仅有评论且登录时展示 */}
|
||
{login && category !== COMMENT_CATEGORY.OPERATE && journalsCount > 0 && !isPhone() && <div className={`pb30 ${journalsCount > 0 && 'pt15'}`}><div className='gotoComment'>点击<a className='ml5' href='#addComments'>添加评论</a></div></div>}
|
||
{/* 评论/操作日志 展示列表 */}
|
||
<Spin spinning={spin}>
|
||
<div className={`issueCommentsBox ${category === COMMENT_CATEGORY.COMMENT && !spin && 'justComment'}`}>
|
||
{journals && (journals.length > 0 && journals.map(item => {return <div key={item.id} className='commentContentBox pb30'>
|
||
{/* 评论 */}
|
||
<div className='commentOperationBor'></div>
|
||
{
|
||
item.user.userName ?
|
||
<Link to={`/${item.user.userName}`}><img src={normalizeUrl(item.user.avatar)} alt="" className='commentUserImg mr15'/></Link>
|
||
:
|
||
<span><img src={normalizeUrl(item.user.avatar)} alt="" className='commentUserImg mr15'/></span>
|
||
}
|
||
<div className='commentContentRight'>
|
||
{/* 判断是否是编辑状态 */}
|
||
{(showEdit === 2 && updateId === item.id) ? <div className='mt15 mr20'><EditComment extraParams={extraParams} {...childProps} updateId={updateId} content={item.notes} defaultFileList={item.attachments} showUserImg={false}/></div> : <div>
|
||
<div className='commentContent'>
|
||
<div className='flexCenter font-14'>
|
||
<div>
|
||
{item.user.userName ? <Link to={`/${item.user.userName}`}>{item.user.nickName}</Link>:<span>{item.user.nickName}</span>}
|
||
<span className='ml15 timeAgo font-14'>{timeAgo(item.created_at)}</span>
|
||
</div>
|
||
{login && !isPhone() && <div>
|
||
{/* 平台管理员/仓库管理员/发布评论者/issue创建者 */}
|
||
{(admin || isManager || login === item.user.userName || username === author) && <Popconfirm
|
||
placement="bottom"
|
||
title={`确定要删除此条评论吗?${item.children_journals.length > 0 ? '子评论也将被一起删除。' : ''}`}
|
||
okText="是"
|
||
cancelText="否"
|
||
onConfirm={() => deleteCommentFunc(item.id)}
|
||
>
|
||
<Button type='link' className='color-grey-89'><i className='iconfont icon-fuzhi-shanchu font-14 mr8'></i>删除</Button>
|
||
</Popconfirm>}
|
||
{/* 仅评论者可修改 */}
|
||
{login === item.user.userName && <Button type='link' className='color-grey-89' onClick={()=>{setUpdateId(item.id); setShowEdit(2)}}><i className='iconfont icon-a-bianji12 font-14 mr8'></i>修改</Button>}
|
||
<CheckProfile {...props} sureFunc={()=>{setParentId(item.id); setReplyId(item.id); setShowEdit(3)}}><Button type='link' className='color-grey-89'><i className='iconfont icon-a-xiaoxi1 font-14 mr8'></i>回复</Button></CheckProfile>
|
||
</div>}
|
||
</div>
|
||
<div className='contentHtml mb5'>{commentCtx(item.notes)}</div>
|
||
{item && item.attachments && item.attachments.length > 0 && <div className='attachmentBox mb5'><Attachment
|
||
attachments={item.attachments}
|
||
showNotification={showNotification}
|
||
canDelete={false}
|
||
/></div>}
|
||
</div>
|
||
</div>}
|
||
{showEdit === 3 && replyId === item.id && <div className='contentHtml mr20'><EditComment extraParams={extraParams} {...childProps} parentId={parentId} replyId={replyId}/></div>}
|
||
{/* 评论回复部分 */}
|
||
{item.children_journals.map(i =>{
|
||
return <div className='commentReply' key={i.id}>
|
||
{(showEdit === 4 && updateId === i.id) ? <div className='mr20'><EditComment extraParams={extraParams} {...childProps} updateId={updateId} content={i.notes} defaultFileList={i.attachments}/></div> : <div>
|
||
<div className='flexCenter'>
|
||
<div>
|
||
<Link to={`/${i.user.userName}`}><img src={normalizeUrl(i.user.avatar)} alt="" className='commentUserImg mr8'/></Link>
|
||
<Link to={`/${i.user.userName}`}>{i.user.nickName}</Link>
|
||
{i.reply_user && <span className='ml5 timeAgo mr3'>回复</span>}
|
||
<span>{i.reply_user && i.reply_user.name}</span>
|
||
<span className='ml15 timeAgo font-14'>{timeAgo(i.created_at)}</span>
|
||
</div>
|
||
{login && !isPhone() && <div>
|
||
{/* 平台管理员/仓库管理员/发布评论者/回复评论者/issue创建者 */}
|
||
{(admin || isManager || login === i.user.userName || (i.reply_user && login === i.reply_user.login) || username === author) && <Popconfirm
|
||
placement="bottom"
|
||
title={"确定要删除当前回复吗?"}
|
||
okText="是"
|
||
cancelText="否"
|
||
onConfirm={() => deleteCommentFunc(i.id)}
|
||
>
|
||
<Button type='link' className='color-grey-89'><i className='iconfont icon-fuzhi-shanchu font-14 mr8'></i>删除</Button>
|
||
</Popconfirm>}
|
||
{/* 仅回复评论者可修改 */}
|
||
{login === i.user.userName && <Button type='link' className='color-grey-89' onClick={()=>{setUpdateId(i.id); setShowEdit(4)}}><i className='iconfont icon-a-bianji12 font-14 mr8'></i>修改</Button>}
|
||
<CheckProfile {...props} sureFunc={()=>{setParentId(item.id);setReplyId(i.id); setShowEdit(5)}}>
|
||
<Button type='link' className='color-grey-89'><i className='iconfont icon-a-xiaoxi1 font-14 mr8'></i>回复</Button>
|
||
</CheckProfile>
|
||
</div>}
|
||
</div>
|
||
<div className='contentHtml mt5 mb10'>{commentCtx(i.notes)}</div>
|
||
{i && i.attachments && i.attachments.length > 0 && <div className='attachmentBox'><Attachment
|
||
attachments={i.attachments}
|
||
showNotification={showNotification}
|
||
canDelete={false}
|
||
/></div>}
|
||
</div>}
|
||
{showEdit === 5 && replyId === i.id && <div className='contentHtml mr20'><EditComment extraParams={extraParams} {...childProps} parentId={parentId} replyId={replyId}/></div>}
|
||
</div>})}
|
||
</div>
|
||
</div>}))}
|
||
</div>
|
||
</Spin>
|
||
{totalCount > limit && <div className='mt20 paginationIssueComment mb20'>
|
||
<Pagination simple current={page} pageSize={limit} total={totalCount} onChange={(page)=>setPage(page)}/>
|
||
</div>}
|
||
{/* 添加评论 */}
|
||
{category !== COMMENT_CATEGORY.OPERATE && !isPhone() && <div className="pb30">
|
||
{login ? showEdit === 1 ? <EditComment extraParams={extraParams} {...childProps}/> : <div className="addComments" id='addComments'>
|
||
<img src={getImageUrl(image_url)} alt="" />
|
||
<div style={{flex:1}}>
|
||
<CheckProfile {...props} sureFunc={()=>{setShowEdit(1);}}>
|
||
<Input className='addCommentBox' placeholder="添加评论"/>
|
||
</CheckProfile>
|
||
</div>
|
||
</div> : <div className='unLoginComment font-15 pl20'>
|
||
{/* 未登录用户 */}
|
||
<a className='mr5 loginBtn' onClick={()=>{showLoginDialog()}}>登录</a>并参与评论与回复
|
||
</div>}
|
||
</div>}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default CommentsList;
|