diff --git a/src/forge/Head/NoticeContent.jsx b/src/forge/Head/NoticeContent.jsx
index 634a7adc..39546707 100644
--- a/src/forge/Head/NoticeContent.jsx
+++ b/src/forge/Head/NoticeContent.jsx
@@ -224,7 +224,7 @@ function NoticeContent({ visible, showNotification, resetUserInfo, current_user:
diff --git a/src/forge/UsersList/fork_users.js b/src/forge/UsersList/fork_users.js
index 23cd5bea..a3d9c2a2 100644
--- a/src/forge/UsersList/fork_users.js
+++ b/src/forge/UsersList/fork_users.js
@@ -27,7 +27,7 @@ class ForkUsers extends Component {
});
const { projectsId , owner } = this.props.match.params;
- const url = `/${owner}/${projectsId}/members.json`;
+ const url = `/${owner}/${projectsId}/forks.json`;
axios
.get(url, {
params: {
diff --git a/src/forge/comments/comments.js b/src/forge/comments/comments.js
index dca7581f..fb19e1da 100644
--- a/src/forge/comments/comments.js
+++ b/src/forge/comments/comments.js
@@ -30,6 +30,7 @@ class comments extends Component {
reply_id: undefined,
reply_content: undefined,
new_journal_id: undefined,
+ atWhoLoginList:undefined
};
}
@@ -51,6 +52,7 @@ class comments extends Component {
});
return;
}
+
this.props.form.validateFieldsAndScroll((err, values) => {
if (!err) {
const {
@@ -60,10 +62,12 @@ class comments extends Component {
orderId,
reply_id,
is_reply,
+ atWhoLoginList,
} = this.state;
- const url = `/issues/${orderId}/journals.json`;
+
+ const url = `/issues/${orderId}/journals.json`;
axios
.post(url, {
...values,
@@ -71,6 +75,7 @@ class comments extends Component {
issue_id: orderId,
attachment_ids: fileList,
parent_id: reply_id,
+ receivers_login:atWhoLoginList,
})
.then((result) => {
if (result && result.data.status === 0) {
@@ -248,18 +253,29 @@ class comments extends Component {
onContentChange = (value) => {
if (value) {
this.setState({
- content: value,
quillFlag: false,
});
}
+ this.setState({
+ content: value,
+ });
};
replyContentChange = (value) => {
if (value) {
this.setState({
- reply_content: value,
quillFlag: false,
});
}
+ this.setState({
+ reply_content: value,
+ });
+ };
+
+ //评论中at谁列表(存储:login)
+ changeAtWhoLoginList = (loginList) =>{
+ this.setState({
+ atWhoLoginList:loginList,
+ });
};
onRef = (ref) => {
@@ -308,6 +324,7 @@ class comments extends Component {
new_journal_id,
} = this.state;
const { current_user, only_show_content } = this.props;
+ const { projectsId ,owner } = this.props.match.params;
const new_comment = (is_reply, item_id) => {
return (
@@ -339,6 +356,10 @@ class comments extends Component {
onChange={
is_reply ? this.replyContentChange : this.onContentChange
}
+ isCanAtme = {true}
+ changeAtWhoLoginList = {this.changeAtWhoLoginList}
+ owner = {owner}
+ projectsId = {projectsId}
>
{quillFlag && 请输入评论内容}
diff --git a/src/modules/tpm/challengesnew/css/newquestion.css b/src/modules/tpm/challengesnew/css/newquestion.css
index 444ca8d1..eb1a67eb 100644
--- a/src/modules/tpm/challengesnew/css/newquestion.css
+++ b/src/modules/tpm/challengesnew/css/newquestion.css
@@ -4,4 +4,39 @@
.Permanentban{
color:#5091FF !important;
border-color: #5091FF !important;
+}
+
+/*md编辑器中输入@弹出可选人列表样式*/
+.at_who_list{
+ position: absolute;
+ z-index: 100;
+ width: 180px;
+ max-height: 160px;
+ background: #FFFFFF;
+ box-shadow: 0px 4px 8px 2px rgba(212, 212, 212, 0.5);
+ border-radius: 4px;
+ overflow-y: scroll;
+ cursor: pointer;
+}
+.at_who{
+ height: 40px;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ border-bottom: 1px solid rgba(212, 212, 212, 0.5);
+ padding: 0 4px;
+}
+.at_who.active{
+ background: #F3F4F6;
+}
+.at_who img{
+ width:30px;
+ height:30px;
+ border-radius:50%;
+ margin-right: 10px;
+}
+.at_who span{
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
}
\ No newline at end of file
diff --git a/src/modules/tpm/challengesnew/tpm-md-editor.js b/src/modules/tpm/challengesnew/tpm-md-editor.js
index e1c06eda..46c01ec3 100644
--- a/src/modules/tpm/challengesnew/tpm-md-editor.js
+++ b/src/modules/tpm/challengesnew/tpm-md-editor.js
@@ -1,11 +1,12 @@
-
import React, { Fragment, useEffect, useRef, useState } from 'react';
import { getUploadActionUrl, getUrl } from 'educoder';
import ResizeObserver from 'resize-observer-polyfill';
-
+import { getImageUrl } from 'educoder';
+import axios from 'axios';
import '../../courses/css/Courses.css';
import './css/TPMchallengesnew.css';
import 'codemirror/lib/codemirror.css';
+import './css/newquestion.css';
const $ = window.$
const mdIcons = ["bold", "italic", "|", "list-ul", "list-ol", "|", "code", "code-block", "link", "|", "inline-latex", "latex", '|', "image", "table", '|', "line-break", "watch", "clear"];
@@ -39,7 +40,6 @@ function md_rec_data(k, mdu, id) {
}
window.md_rec_data = md_rec_data;
-
function md_elocalStorage(editor, mdu, id) {
let oc = window.sessionStorage.getItem('content' + mdu)
if (oc !== null && oc !== editor.getValue()) {
@@ -74,16 +74,38 @@ function md_elocalStorage(editor, mdu, id) {
return tid
}
-
-export default ({ mdID, onChange, onCMBeforeChange, onCMBlur, error = false, className = '', noStorage = false, imageExpand = true, placeholder = '', width = '100%', height = 400, initValue = '', emoji, watch, showNullButton = false, showResizeBar = false, startInit = true , forMember = true }) => {
-
+export default ({ mdID, onChange, onCMBeforeChange, onCMBlur, error = false, className = '', noStorage = false, imageExpand = true, placeholder = '', width = '100%', height = 400, initValue = '', emoji, watch, showNullButton = false, showResizeBar = false, startInit = true , forMember = true , isCanAtme = false , changeAtWhoLoginList, owner, projectsId }) => {
+
const editorEl = useRef();
const resizeBarEl = useRef();
const [editorInstance, setEditorInstance] = useState();
+ const [atWhoVisible, setAtWhoVisible] = useState(false);
+ const [atWhoLoginListState, setAtWhoLoginListState] = useState([]);
+ //调用member.json接口获取到的用户列表
+ const [users, setUsers] = useState([]);
+ //可以@的全部用户
+ const [allUsers, setAllUsers] = useState([]);
+ const atWhoLoginList = useRef([]);
+ const atWhoVisibleRef = useRef(false);
const containerId = `mdEditor_${mdID}`;
const editorBodyId = `mdEditors_${mdID}`;
const tipId = `e_tips_mdEditor_${mdID}`;
+ useEffect(()=>{
+ //请求members接口获取全部可@列表
+ isCanAtme && axios.get(`/${owner}/${projectsId}/members.json`).then(response=>{
+ if(response.data.total_count !== 0){
+ setAllUsers(response.data.users);
+ setUsers(response.data.users);
+ }
+ })
+ //点击其他地方关闭弹框
+ document.addEventListener('click',()=>{
+ atWhoVisibleRef.current = false;
+ setAtWhoVisible(false);
+ })
+ },[])
+
function onLayout() {
let ro;
if (editorEl.current) {
@@ -101,6 +123,96 @@ export default ({ mdID, onChange, onCMBeforeChange, onCMBlur, error = false, cla
return ro;
}
+ function selectAtWho(username){
+ atWhoVisibleRef.current = false;
+ setAtWhoVisible(false);
+ const cm = editorInstance.cm;
+ //获取鼠标所在行的行数和ch
+ const cursor = cm.doc.getCursor();
+ const line = cursor.line;//行
+ const ch = cursor.ch;//列
+ const startIndex = cm.getRange({line,ch:0},{line,ch}).lastIndexOf("@");
+ let selectUserLogin = undefined;
+ users.map((item)=>{
+ item.username === username && (selectUserLogin = item.login);
+ })
+ //替换内容
+ cm.replaceRange("[@"+username+"]"+`(/${selectUserLogin}) `,{line,ch:startIndex},{line,ch});
+ //鼠标聚焦
+ cm.focus();
+ //将此user的login存储到atWhoLoginList集合中
+ const list = new Set(atWhoLoginList.current);
+ list.add(selectUserLogin);
+ atWhoLoginList.current = Array.from(list);
+ setAtWhoLoginListState(Array.from(list));
+ }
+
+ function onMouseOver(key){
+ document.getElementsByClassName("at_who active")[0] && (document.getElementsByClassName("at_who active")[0].className="at_who");
+ document.getElementsByClassName("at_who")[key] && (document.getElementsByClassName("at_who")[key].className="at_who active");
+ }
+
+ //markdown编辑器中输入的键盘监听事件
+ function mdKeyDown(e){
+ if (e.shiftKey && e.code === "Digit2") {
+ // 输入@键后在对应的位置显示可选的项目成员
+ atWhoVisibleRef.current = true;
+ setAtWhoVisible(true);
+ //获取光标位置
+ const cssStyle = document.getElementsByClassName("CodeMirror cm-s-default CodeMirror-wrap")[0].firstChild.style;
+ //设置弹框位置
+ const newTop = placeholder === "添加评论..." ? 159: placeholder === "请输入合并请求的描述..." ? 172:62;
+ const newLeft = placeholder === "添加评论..." ? 80: 20;
+ document.getElementById("at_who_list").style.top = parseInt(cssStyle.getPropertyValue("top").replace("px","")) + newTop +"px";
+ document.getElementById("at_who_list").style.left = parseInt(cssStyle.getPropertyValue("left").replace("px",""))+newLeft+"px";
+ }
+ //处理本来@了某人 -> 删掉 -> 撤回 的情况
+ if(e.ctrlKey && e.code === "KeyZ" && allUsers.length != 0){
+ const codemirror = editorInstance.cm;
+ let value = codemirror.getValue();
+ //处理初始内容就自带@谁的情况
+ if(initValue){
+ const del = [];
+ allUsers.map(item=>{
+ if(initValue.indexOf(item.username)!=-1 && initValue.charAt(initValue.indexOf(item.username)-1) === "@" && initValue.indexOf(`@${item.username}`)===value.indexOf(`@${item.username}`)){
+ //初始内容中有符合@+名字的格式并且当前内容未删除初始内容
+ del[del.length] = `[@${item.username}](/${item.login})`;
+ }
+ })
+ del.length!=0 && del.map(str=>{
+ value = value.replace(str,"");
+ })
+ }
+ //判断value是否包含@符号
+ value.indexOf("@") != -1 && allUsers.map(item =>{
+ if(value.indexOf(item.username)!=-1 && value.charAt(value.indexOf(item.username)-1) ==="@"){
+ //将此user的login存储到atWhoLoginList集合中
+ const list = new Set(atWhoLoginList.current);
+ list.add(item.login);
+ atWhoLoginList.current = Array.from(list);
+ setAtWhoLoginListState(Array.from(list));
+ }
+ })
+ }
+ }
+
+ useEffect(()=>{
+ changeAtWhoLoginList && changeAtWhoLoginList(atWhoLoginListState);
+ },[atWhoLoginListState])
+
+ const atWhoList = (
+
+ {users && users.map((item,key)=>{
+ return(
+
{selectAtWho(item.username)}} onMouseOver={()=>{onMouseOver(key)}}>
+ {item.image_url &&
}
+
{item.username}
+
+ )
+ })}
+
+ )
+
useEffect(() => {
if (editorInstance) {
return
@@ -183,6 +295,69 @@ export default ({ mdID, onChange, onCMBeforeChange, onCMBlur, error = false, cla
const cmEl = editorInstance && editorInstance.cm
+ useEffect(()=>{
+ if(atWhoVisibleRef.current){
+ // 添加上下键、enter键监听事件
+ cmEl.addKeyMap({
+ 'Up':()=>{
+ const atWhoListDiv = document.getElementById("at_who_list");
+ const atWhoDivs = document.getElementsByClassName("at_who");
+ let index;
+ for(let i = 0; i
0){
+ index <=atWhoDivs.length-4 && (atWhoListDiv.scrollTop -=40)
+ atWhoDivs[index].className = "at_who";
+ atWhoDivs[index-1].className = "at_who active";
+ }
+ },
+ 'Down':()=>{
+ const atWhoListDiv = document.getElementById("at_who_list");
+ const atWhoDivs = document.getElementsByClassName("at_who");
+ let index;
+ for(let i = 0; i=3 && (atWhoListDiv.scrollTop +=40)
+ atWhoDivs[index].className = "at_who";
+ atWhoDivs[index+1].className = "at_who active";
+ }
+ },
+ 'Enter':()=>{
+ //找到classname为at_who active的div,执行click事件
+ if(document.getElementsByClassName("at_who active")[0]){
+ document.getElementsByClassName("at_who active")[0].click()
+ }else{
+ const cm = editorInstance.cm;
+ const cursor = cm.doc.getCursor();
+ const line = cursor.line;//行
+ const ch = cursor.ch;//列
+ //添加换行
+ cm.replaceRange("\n",{line,ch},{line,ch});
+ setAtWhoVisible(false);
+ atWhoVisibleRef.current = false;
+ }
+ }
+ })
+ } else {
+ //移除上下、enter键监听
+ cmEl && cmEl.removeKeyMap();
+ }
+ },[atWhoVisible])
+
+ useEffect(()=>{
+ //当users数组发生变化时改变框的位置
+ if(atWhoVisibleRef.current && users){
+ //获取光标位置
+ const cssStyle = document.getElementsByClassName("CodeMirror cm-s-default CodeMirror-wrap")[0].firstChild.style;
+ //设置弹框位置
+ const newLeft = placeholder === "添加评论..."? 80: 10;
+ document.getElementById("at_who_list").style.left = (parseInt(cssStyle.getPropertyValue("left").replace("px",""))+newLeft)+"px";
+ }
+ },[users])
+
useEffect(() => {
if (cmEl) {
let tid = null
@@ -198,19 +373,138 @@ export default ({ mdID, onChange, onCMBeforeChange, onCMBlur, error = false, cla
if (!noStorage) {
tid = md_elocalStorage(editorInstance, `MDEditor__${containerId}`, containerId)
}
- if (onChange) {
- editorInstance.cm.on('change', (cm) => {
- // if(forMember){
- // document.onkeydown = (e) => {
- // if (e.key === "@") {
- // // 输入@键后在对应的位置显示可选的项目成员
-
- // }
- // };
- // }
- onChange(cm.getValue())
- })
- }
+ //isCanAtme:只有issue和合并请求以及评论部分可以@他人操作
+ //绑定@事件
+ isCanAtme && editorInstance.cm.on("focus", () => {
+ document.addEventListener("keydown", mdKeyDown);
+ });
+ isCanAtme && editorInstance.cm.on("blur", () => {
+ document.removeEventListener("keydown",mdKeyDown);
+ });
+ editorInstance.cm.on("change", (cm) => {
+ //调用父组件的onchange方法,将输入内容传入父级组件
+ onChange && onChange(cm.getValue());
+ if(atWhoVisibleRef.current){
+ //搜索用户(弹框之后用户输入用户名信息)
+ const cur = cm.doc.getCursor();
+ const line = cur.line;
+ const ch = cur.ch;
+ let rangeCont = cmEl.getRange({line,ch:0},{line,ch});
+ //处理已经弹出列表框,但用户删除@符号
+ if(rangeCont.indexOf("@")===-1){
+ setAtWhoVisible(false);
+ atWhoVisibleRef.current = false;
+ }else{
+ rangeCont = rangeCont.substring(rangeCont.lastIndexOf("@")+1);
+ rangeCont ? axios.get(`/${owner}/${projectsId}/members.json`,{
+ params: {
+ search: rangeCont,
+ },
+ }).then(response=>{
+ if(response && response.data && response.data.total_count !== 0){
+ setUsers(response.data.users);
+ }else{
+ setUsers(undefined);
+ }
+ }):setUsers(allUsers)
+ }
+ }
+
+ //当内容发生改变并且有已@列表时
+ if(atWhoLoginList.current.length != 0){
+ const codemirror = editorInstance.cm;
+ //startValue:触发change方法时的内容,value:处理了初始内容带@用户的情况
+ let startValue = codemirror.getValue();
+ let value = codemirror.getValue();
+ //处理初始内容就自带@谁的情况
+ if(initValue){
+ const del = [];
+ allUsers.map(item=>{
+ if(initValue.indexOf(item.username)!=-1 && initValue.charAt(initValue.indexOf(item.username)-1) === "@" && initValue.indexOf(`@${item.username}`)===value.indexOf(`@${item.username}`)){
+ //初始内容中有符合@+名字的格式并且当前内容未删除初始内容
+ del[del.length] = `[@${item.username}](/${item.login})`;
+ }
+ })
+ del.length!=0 && del.map(str=>{
+ value = value.replace(str,"");
+ })
+ }
+ //以username为主键,login为value的map集合
+ let atWhoMap = new Map();
+ Array.from(atWhoLoginList.current).map(item=>{
+ allUsers.map(i=>{
+ if(i.login === item){
+ atWhoMap.set(i.username,i.login);
+ }
+ })
+ });
+ const cursor = codemirror.doc.getCursor();
+ const line = cursor.line;
+ const ch = cursor.ch;
+ //处理全部内容中不包含“@”的情况
+ if(value.indexOf("@") === -1){
+ //markdown嵌套的链接删掉
+ // Array.from(atWhoMap.keys()).map(username=>{
+ // startValue = startValue.replaceAll(`[${username}](/${atWhoMap.get(username)}) `,username);
+ // })
+ //替换全部内容
+ // codemirror.setValue(startValue);
+ //全部内容已经有要@的列表,但是没有@符号 -> 清空@集合
+ atWhoLoginList.current = [];
+ setAtWhoLoginListState([]);
+ }
+
+ //截取第一个字符到光标的内容
+ const curAfterCont = codemirror.getRange({line,ch:0},{line,ch});
+ const content = codemirror.getLine(line);
+ //处理光标所在行 有“@”的情况
+ if(content && content.indexOf("@") !== -1){
+ Array.from(atWhoMap.keys()).map(username=>{
+ //判断content是不是以列表中的某个username结尾
+ const userCont = `[@${username}](/${atWhoMap.get(username)})`;
+ //删除空格->选中@用户区域
+ if(curAfterCont.endsWith(userCont)){
+ codemirror.setSelection({line,ch:curAfterCont.lastIndexOf("@")-1},{line,ch});
+ }
+ //处理已经有@列表但是value中不包含完整[@用户名](/login)的情况
+ if(value.indexOf(userCont)===-1){
+ // //markdown嵌套的链接删掉,删[]、()的情况不用处理,markdown会自动认为不是链接
+ // //找到[和)的index,将区域内容替换成[]包裹的内容
+ // //光标之后的内容
+ // const curLeterCont = codemirror.getRange({line,ch},{line,ch:content.length});
+ // console.log('光标之后的内容curLeterCont',curLeterCont);
+ // //删除用户名 -> ]在curLeterCont中
+ // //删除login -> ]在curAfterCont中
+ // const a = curAfterCont.lastIndexOf('[');
+ // const b = curLeterCont.indexOf(')')
+ // const c = curLeterCont.indexOf(']') === -1 ? curAfterCont.lastIndexOf(']') : curLeterCont.indexOf(']')+curAfterCont.length;
+ // console.log('[',a,')',b,']',c);
+ // const newCont = codemirror.getRange({line,ch:a+1},{line,ch:c});
+ // console.log('newCont',newCont);
+ // codemirror.replaceRange(newCont,{line,ch:a-1},{line,ch:b+curAfterCont.length+1})
+
+ //符合情况->踢掉这个人 不给他发消息
+ const list = new Set(atWhoLoginList.current);
+ list.delete(atWhoMap.get(username));
+ atWhoLoginList.current = Array.from(list);
+ setAtWhoLoginListState(Array.from(list));
+ }
+ })
+ }else{
+ //处理所在行没有“@”的情况
+ Array.from(atWhoMap.keys()).map(username=>{
+ const userCont = `[@${username}](/${atWhoMap.get(username)})`;
+ if(value.indexOf(userCont)===-1){
+ //符合情况->踢掉这个人 不给他发消息
+ const list = new Set(atWhoLoginList.current);
+ list.delete(atWhoMap.get(username));
+ atWhoLoginList.current = Array.from(list);
+ setAtWhoLoginListState(Array.from(list));
+ }
+ })
+ }
+ }
+ });
ro = onLayout()
return () => {
if (!noStorage) {
@@ -271,7 +565,8 @@ export default ({ mdID, onChange, onCMBeforeChange, onCMBlur, error = false, cla
return (
-
+ {atWhoVisible && atWhoList}
+