pull 组织首页代码

This commit is contained in:
谢思 2023-04-26 15:01:45 +08:00
commit 243d782a0a
31 changed files with 824 additions and 188 deletions

14
package-lock.json generated
View File

@ -4561,6 +4561,11 @@
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
"dev": true
},
"cssfilter": {
"version": "0.0.10",
"resolved": "https://registry.npmmirror.com/cssfilter/-/cssfilter-0.0.10.tgz",
"integrity": "sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw=="
},
"cssnano": {
"version": "4.1.11",
"resolved": "http://173.15.15.82:8081/repository/npm-all/cssnano/-/cssnano-4.1.11.tgz",
@ -21771,6 +21776,15 @@
"resolved": "http://173.15.15.82:8081/repository/npm-all/xmlchars/-/xmlchars-2.2.0.tgz",
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="
},
"xss": {
"version": "1.0.14",
"resolved": "https://registry.npmmirror.com/xss/-/xss-1.0.14.tgz",
"integrity": "sha512-og7TEJhXvn1a7kzZGQ7ETjdQVS2UfZyTlsEdDOqvQF7GoxNfY+0YLCzBy1kPdsDDx4QuNAonQPddpsn6Xl/7sw==",
"requires": {
"commander": "^2.20.3",
"cssfilter": "0.0.10"
}
},
"xtend": {
"version": "4.0.2",
"resolved": "http://173.15.15.82:8081/repository/npm-all/xtend/-/xtend-4.0.2.tgz",

View File

@ -100,6 +100,7 @@
"styled-components": "^4.4.1",
"whatwg-fetch": "2.0.3",
"wrap-md-editor": "^0.2.20",
"xss": "^1.0.14",
"xterm": "4.8.1",
"xterm-addon-fit": "0.4.0"
},

View File

@ -1,8 +1,8 @@
@font-face {
font-family: "iconfont"; /* Project id 2340181 */
src: url('iconfont.woff2?t=1682485448664') format('woff2'),
url('iconfont.woff?t=1682485448664') format('woff'),
url('iconfont.ttf?t=1682485448664') format('truetype');
src: url('iconfont.woff2?t=1682492410798') format('woff2'),
url('iconfont.woff?t=1682492410798') format('woff'),
url('iconfont.ttf?t=1682492410798') format('truetype');
}
.iconfont {

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -4,7 +4,7 @@ import { Spin } from 'antd';
class Loading extends Component {
componentDidUpdate(prevProps, prevState) {
if (!prevProps.error && this.props.error) {
console.log(this.props.error)
// console.log(this.props.error)
}
}

View File

@ -1,14 +1,16 @@
import React, { useEffect, useRef, useMemo } from 'react'
import 'katex/dist/katex.min.css'
import React, { useEffect, useRef, useMemo , useState } from 'react'
import 'katex/dist/katex.min.css';
import marked, { getTocContent, cleanToc, getMathExpressions, resetMathExpressions } from '../common/marked';
import 'code-prettify';
import dompurify from 'dompurify';
import { getEmoji } from '../forge/Main/emoji';
import axios from 'axios';
import { renderToString } from 'katex'
const preRegex = /<pre[^>]*>/g;
const strRegexSub = /:([a-zA-Z_]+):/g;
const quoteRegex = /\[[#][0-9]{0,}\]\(\/(.*?)\/(.*?)\/issues\/[0-9]{0,}\)/g;
function _unescape(str) {
let div = document.createElement('div')
div.innerHTML = str
@ -21,8 +23,29 @@ export default ({
value = '',
className,
style = {},
url
url,
owner=undefined,
projectsId=undefined
}) => {
const [ issues , setIssues ] = useState([]);
useEffect(()=>{
if(owner&&projectsId){
getIssueList();
}
},[owner,projectsId])
function getIssueList(){
axios.get(`/v1/${owner}/${projectsId}/issues`,{params:{
only_name:true,sort_direction:"desc",sort_by:"issues.created_on"
}}).then(result=>{
if(result){
let data = result.data.issues;
setIssues(data);
}
})
}
let str = String(value);
const html = useMemo(() => {
let rs = marked(str);
@ -38,6 +61,26 @@ export default ({
rs = rs.replace(matchStr[i],getEmoji(matchStr[i]));
}
}
if(owner && projectsId && issues && issues.length>0){
let matchQuote = str.match(quoteRegex);
if(matchQuote && matchQuote.length>0){
let getIndexReg = /(?<=#)(.+?)(?=\])/g;
for(var x=0;x<matchQuote.length;x++){
let getIndex = matchQuote[x].match(getIndexReg);
if(getIndex && getIndex.length>0 && getIndex[0]){
let index = getIndex[0];
let filter = issues.filter(f=>f.project_issues_index.toString() === index);
if(filter && filter.length === 1){
let content = `#${index}:${filter[0].subject}`;
rs = rs.replace(`#${index}`,content);
}else{
let content = `<span>#${index}(已删除)</span>`;
rs = rs.replace(`<a href="`+`/${owner}/${projectsId}/issues/${index}`+`">#${index}</a>`,content);
}
}
}
}
}
rs = rs.replace(/(__special_katext_id_\d+__)/g, (_match, capture) => {
const { type, expression } = math_expressions[capture];
@ -46,7 +89,7 @@ export default ({
rs = rs.replace(/▁/g, "▁▁▁")
resetMathExpressions()
return dompurify.sanitize(rs)
}, [str]);
}, [str,issues]);
// #id
useEffect(()=>{

View File

@ -263,15 +263,15 @@ class Activity extends Component{
<div class="normalBox-title">项目演化分析</div>
<div className="echartBox">
<span className="echartTitle" style={{marginTop:0}}>开源项目社群激发</span>
<span className="echartTitle" style={{marginTop:0}}>开源项目社群激发演化拓扑</span>
<BranchLine url={ai_shang_v1_url}/>
<p>基于信息熵围绕疑修任务的社群群智激发演化度量</p>
<span className="echartTitle">开源项目代码变更</span>
<span className="echartTitle">开源项目代码变更演化拓扑</span>
<IssueLine url={ai_shang_v3_url}/>
<p>基于信息熵围绕代码提交的软件代码变更演化度量</p>
<span className="echartTitle">开源项目社区演化</span>
<span className="echartTitle">开源项目社区演化拓扑</span>
<SmoothLineTwo url={ai_shang_v4_url}/>
<p>基于信息熵围绕项目社群的社区分裂缩减合并和扩大演化行为度量</p>

View File

@ -41,7 +41,7 @@ function Line({url}) {
yAxis: {
type: 'value',
scale: true,
name: '社\n群\n激\n发\n熵',
name: '社\n群\n激\n发\n演\n化\n拓\n扑\n熵',
min:0,
splitLine : {
show : true,
@ -49,7 +49,7 @@ function Line({url}) {
nameLocation:'end',
nameTextStyle:{
fontSize:14,
padding:[0,70,-80,0]
padding:[0,70,-120,0]
}
},
dataZoom: [

View File

@ -43,7 +43,7 @@ function Line({url}) {
yAxis: {
type: 'value',
scale: true,
name: '代\n码\n变\n更\n熵',
name: '代\n码\n变\n更\n演\n化\n拓\n扑\n熵',
min:0,
splitLine : {
show : true,
@ -51,7 +51,7 @@ function Line({url}) {
nameLocation:'end',
nameTextStyle:{
fontSize:14,
padding:[0,70,-80,0]
padding:[0,70,-120,0]
}
},
dataZoom: [

View File

@ -53,7 +53,7 @@ function SmoothLineTwo({url}) {
yAxis: {
type: 'value',
scale: true,
name: '社\n区\n演\n化\n熵',
name: '社\n区\n演\n化\n拓\n扑\n熵',
min:0,
splitLine : {
show : true,
@ -61,7 +61,7 @@ function SmoothLineTwo({url}) {
nameLocation:'end',
nameTextStyle:{
fontSize:14,
padding:[0,70,-80,0]
padding:[0,70,-120,0]
}
},
dataZoom: [

View File

@ -115,6 +115,7 @@ function EditComment(props){
initValue={content}
onChange={(value)=>{setQuillFlag(false);setContent(value);}}
isCanAtme = {true}
isQuoteIssue={true}
changeAtWhoLoginList = {(loginList)=>{setAtWhoLoginList(loginList); setAttachmentClean(true)}}
owner = {owner}
projectsId = {projectsId}

View File

@ -89,7 +89,7 @@ function IssueCommentList(props){
}, [reload, category, page])
function commentCtx(v){
return <RenderHtml className="break_word_comments imageLayerParent commentRenderHtml" value={v} url={location}/>;
return <RenderHtml owner={owner} projectsId={projectsId} className="break_word_comments imageLayerParent commentRenderHtml" value={v} url={location}/>;
};
//

View File

@ -6,7 +6,7 @@ import Attachments from "../../Upload/attachment";
import UploadImg from '../Img/UploadImg.png';
function NewPanel(props,ref){
const [ description , setDescription ] = useState("");
const [ description , setDescription ] = useState(undefined);
const [ fileList , setFileList] = useState(undefined);
const [ attachments , setAttachments ] = useState([]);
const [ save , setSave ] = useState([]);
@ -15,12 +15,11 @@ function NewPanel(props,ref){
const { createFunc , title , desc , files , onCancel , owner , projectsId } = props;
const { form: { getFieldDecorator, validateFields , setFieldsValue } } = props;
useState(()=>{
title && setTimeout(()=>{
setFieldsValue({subject:title});
desc && onContentChange(desc);
},100)
desc && setDescription(desc);
files && setAttachments(files);
files && setSave(files);
},[title , desc , files])
@ -84,11 +83,12 @@ function NewPanel(props,ref){
<MDEditor
placeholder={"请输入描述信息"}
height={392}
mdID={"order-new-description"}
mdID={"issue-edit-description"}
initValue={description}
onChange={onContentChange}
className="mt20"
isCanAtme = {true}
isQuoteIssue={true}
owner = {owner}
projectsId = {projectsId}
changeAtWhoLoginList = {changeAtWhoLoginList}

View File

@ -32,6 +32,7 @@ function Details(props){
const [ tag , setTag ] = useState(undefined);
const [ branchList , setBranchList ] = useState(undefined);
const [ branch , setBranch ] = useState(undefined);
const [ desc , setDesc ] = useState(undefined);
const [ orderId , setOrderId ] = useState(undefined);
@ -91,6 +92,8 @@ function Details(props){
let per = data && !data.user_permission;
setEditFlag(per);
setDesc(data && data.description);
const aut = current_user && data.author && (data.author.login === current_user.login);
const con = data.pull_fixed === false && aut;
setAmountEditFlag(con);
@ -277,10 +280,10 @@ function Details(props){
if(result){
window.scrollTo(0,0);
props.showNotification("疑修更新成功!");
//
setCommentReload(Math.random());
Init();
setEdit(false);
//
setCommentReload(Math.random());
}
}).catch(error=>{})
}else{
@ -318,6 +321,7 @@ function Details(props){
props.history.push(`/${owner}/${projectsId}/issues/${index}`);
}else{
setEdit(false);
window.scrollTo(0,0);
}
}
@ -367,7 +371,7 @@ function Details(props){
{...props}
onCancel={onCancel}
title={details.subject}
desc={details.description}
desc={edit ? desc : undefined}
files={copy ? undefined : details.attachments}
createFunc={createFunc}
owner = {owner} projectsId = {projectsId}
@ -405,8 +409,8 @@ function Details(props){
}
</div>
<div class="descPanel">
{details.description ?
<RenderHtml className="break_word_comments imageLayerParent" value={details.description} url={props.history.location} />
{desc ?
<RenderHtml owner={owner} projectsId={projectsId} className="break_word_comments imageLayerParent" value={desc} url={props.history.location} />
:
<span className="color-grey-9 ml3 mr3">暂无描述</span>
}

View File

@ -519,7 +519,6 @@ class Detail extends Component {
}
textFunc = (forked_from_project_id, fork_info) => {
let type = fork_info && fork_info.fork_project_user_type;
return forked_from_project_id && fork_info ?
<div className="color-grey-9 df">
<span>复刻自</span>

View File

@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
import { Button, Collapse , Tooltip , Spin } from 'antd';
import { Button, Collapse , Tooltip , Spin , Pagination } from 'antd';
import axios from 'axios';
import Content from './historyContent';
import Fault from '../images/fault.png';
@ -8,21 +8,30 @@ const { Panel } = Collapse;
function PushHistory({id,owner,projectsId,showNotification}) {
const [ list , setList ] = useState(undefined);
const [ isSpin , setIsSpin ] = useState(false);
const [ page , setPage ] = useState(1);
const [ total , setTotal ] = useState(0);
const pageSize = 10;
useEffect(()=>{
if(id && owner && projectsId){
Init();
Init(1);
}
},[id,owner,projectsId])
function Init() {
useEffect(()=>{
setList(undefined)
Init(page);
},[page])
function Init(page) {
const url = `/${owner}/${projectsId}/webhooks/${id}/tasks.json`;
axios.get(url,{
params:{page:1,limit:10}
params:{page:page,limit:pageSize}
}).then(result=>{
if(result && result.data){
setList(result.data.tasks);
setIsSpin(false);
setTotal(result.data.total_count);
}
}).catch(error=>{})
}
@ -49,7 +58,7 @@ function PushHistory({id,owner,projectsId,showNotification}) {
</span>
</div>
{
list && list.length>0 &&
list && list.length>0 ?
<Collapse accordion bordered={false} className="historyColl">
{
list.map((i,k)=>{
@ -75,6 +84,15 @@ function PushHistory({id,owner,projectsId,showNotification}) {
})
}
</Collapse>
:
<div style={{height:"470px",display:"flex",alignItems:"center",justifyContent:"center"}}>
<Spin />
</div>
}
{
total > pageSize ?
<div style={{padding:"15px 0px",textAlign:"right"}}><Pagination size="small" showQuickJumper pageSize={pageSize} current={page} total={total} onChange={(p)=>setPage(p)}/></div>
:""
}
</div>
)

View File

@ -1,12 +1,12 @@
import React from 'react';
import { Link } from 'react-router-dom';
export default (({ name , count , bottom , children , url })=>{
export default (({ name , icon , count , bottom , children , url })=>{
return(
<div className="box">
<div className="head">
<span className="font-16">{name}</span>
<Link to={url}>{count}<i className="iconfont icon-youjiantou font-12 ml3"></i></Link>
<span className="font-18">{icon}{name}{count && `(${count})`}</span>
{ url && <Link to={url} style={{color:"rgba(31, 35, 41, 1)"}}>更多<i className="iconfont icon-youjiantou font-12 ml3"></i></Link>}
</div>
<div className="content">
{children}

View File

@ -19,6 +19,10 @@ const New = Loadable({
loading: Loading,
});
const DetailIndex = Loadable({
loader: () => import("./List"),
loading: Loading,
});
const TeamDetailIndex = Loadable({
loader: () => import("./Sub/Detail"),
loading: Loading,
});
@ -61,14 +65,14 @@ export default withRouter(CNotificationHOC()(SnackbarHOC()(TPMIndexHOC(
<Route
path="/:OIdentifier/teams"
render={(p) => {
return <DetailIndex {...props} {...p}/>
return <TeamDetailIndex {...props} {...p}/>
}}
></Route>
{/* 组织成员 */}
<Route
path="/:OIdentifier/members"
render={(p) => {
return <DetailIndex {...props} {...p}/>
return <TeamDetailIndex {...props} {...p}/>
}}
></Route>
@ -77,17 +81,17 @@ export default withRouter(CNotificationHOC()(SnackbarHOC()(TPMIndexHOC(
<Route
path="/:OIdentifier/setting"
render={(p) => (
<DetailIndex {...props} {...p}/>
<TeamDetailIndex {...props} {...p}/>
)}
></Route>
{/* 组织下的项目详情 */}
<Route
path="/:owner/:projectsId"
render={(p) => (
<ProjectDetail {...props} {...p} />
)}
></Route>
path="/:owner/:projectsId"
render={(p) => (
<ProjectDetail {...props} {...p} />
)}
></Route>
{/* 组织详情(包含组织设置) */}
<Route

View File

@ -1,3 +1,46 @@
.top-orz{
background-color:#f9fbfe;
box-shadow:0px 8px 20px rgba(17, 35, 146, 0.06);
.box{
width: 1200px;
display: flex;
margin:0px auto;
align-items: center;
padding:25px 0px 35px;
&>img{
margin-right: 27px;
// max-width: 140px;
height: 112px;
border-radius:4px;
}
.df{
display: flex;
align-items: center;
}
.info-orz{
flex: 1;
.orz-main{
display: flex;
justify-content: space-between;
width: 100%;
align-items: center;
margin-bottom: 14px!important;
height:30px;
}
.orz-name{
font-weight:700;
color:#333333;
font-size:22px;
}
.orz-desc{
color:#4c5b76;
font-size:15px;
line-height:26px;
height: 52px;
}
}
}
}
.teamBox{
margin:10px 0px;
border:1px solid #eee;
@ -40,17 +83,79 @@
margin:0px auto;
padding-top:18px;
}
.list{
.content-orz{
background-color:#f3f5f8;
}
.content-orz .list{
display: flex;
align-items: flex-start;
width: 1200px;
margin:0px auto;
padding-top: 40px;
.list-l{
background-color: #fff;
background-image: url('./images/left-bc.png');
background-size: 100%;
max-width: 860px;
width: 72%;
margin-bottom: 30px;
border:1px solid #eee;
padding:30px;
.newstatus{
padding-bottom: 20px;
border-bottom: 1px dashed rgba(170, 175, 190, 1);
.desc{
color:#4c5876;
font-size:14px;
line-height:26px;
}
.title{
height:22px;
line-height:22px;
color:#1f2329;
font-size:16px;
margin:20px auto 14px!important;
}
.name{
height:24px;
span{
font-weight:700;
color:#1f2329;
font-size:17px;
}
i{
margin-right: 4px;
color:rgba(31, 35, 41, 1);
}
}
}
.sepment{
text-align: center;
margin:30px auto 20px;
.ant-radio-group{
box-shadow:0px 0px 10px rgba(30, 47, 162, 0.09);
.ant-radio-button-wrapper{
border:none;
background-color: #fff;
line-height: 34px;
&:first-child{
padding:3px 0px 3px 3px;
}
&:last-child{
padding:3px 3px 3px 0px;
}
span{
background-color:rgba(30, 47, 162, 0.09) ;
display: block;
}
}
.ant-radio-button-wrapper-checked{
background-color:#466aff;
color: #fff;
}
}
}
.head{
padding:16px 32px;
padding:16px 0px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
@ -70,7 +175,6 @@
}
}
.team{
padding:0px 32px;
min-height: 450px;
.team_project{
padding:22px 0px;
@ -106,31 +210,53 @@
}
.list-r{
width: 28%;
max-width: 340px;
padding-left: 20px;
box-sizing: border-box;
max-width: 338px;
padding-left:30px;
margin-bottom: 30px;
& > div{
border:1px solid #eee;
}
box-sizing: border-box;
}
.box{
background:rgba(255,255,255,1);
border-radius:5px;
margin-bottom: 20px;
.head{
padding:25px 20px;
padding:25px 0px 15px;
display: flex;
border-bottom: 1px solid #eee;
border-bottom: 1px dashed rgba(170, 175, 190, 1);
justify-content: space-between;
color: #333;
align-items: center;
margin:0px 20px;
a:hover{
color: #466aff!important;
}
}
.content{
padding:13px 0px;
padding:5px 0px;
.progress{
display: flex;
padding:10px 35px 10px 20px;
align-items: center;
&:hover{
background-color: rgba(175, 183, 194, 0.13);
}
}
.teammembers{
display: flex;
align-items: center;
padding:13px 20px;
align-items: flex-start;
padding:10px 20px 7px 20px;
&:hover{
background-color: rgba(175, 183, 194, 0.13);
}
.memberIndex{
display: block;
width:22px;
height:22px;
line-height:22px;
background-color:#466aff;
border-radius:2px;
margin-right: 10px;
text-align: center;
color: #fff;
}
.m-img{
border-radius: 50%;
width:45px;
@ -141,7 +267,14 @@
}
.foot{
padding:15px 20px;
border-top: 1px solid #eee;
text-align: center;
.newBtn{
border-color:#466aff;
border-radius:8px;
color: #466aff;
display: inline-flex;
align-items: center;
}
}
}
}
@ -413,4 +546,13 @@
}
}
}
}
.overviewcontent{
.ConcentrateTip{
margin:20px 0px 0px 0px!important;
}
.concentrateUl li{
border-color:#cddbeb!important;
border-radius:4px;
}
}

View File

@ -7,7 +7,9 @@ import Item from './ListItem';
import Right from './RightBox';
import NoData from '../Nodata';
import CheckProfile from '../Component/ProfileModal/Profile';
import { Menu , Pagination , Dropdown , Spin } from 'antd';
import { Menu , Pagination , Dropdown , Spin , Tabs , Radio } from 'antd';
import ConcentrateProject from '../users/GeneralView/ConcentrateProject';
import { getImageUrl } from 'educoder';
import axios from 'axios';
const limit = 15;
@ -18,9 +20,27 @@ function List(props){
const [ search , setSearch ] = useState(undefined);
const [ page , setPage ] = useState(1);
const [ sortBy , setSortBy ] = useState("updated_on");
const [ organizeDetail , setOrganizeDetail ] = useState(undefined);
const [ mode , setMode ] = useState("overview");
const OIdentifier = props.match.params.OIdentifier;
const organizeDetail = props.organizeDetail;
const { current_user } = props;
console.log(props);
useEffect(()=>{
if(OIdentifier){
getDetail(OIdentifier);
}
},[OIdentifier]);
function getDetail(id) {
const url = `/organizations/${id}.json`;
axios.get(url).then(result=>{
if(result && result.data){
setOrganizeDetail(result.data);
}
}).catch(error=>{})
}
useEffect(()=>{
if(organizeDetail){
@ -31,11 +51,11 @@ function List(props){
},[organizeDetail])
useEffect(()=>{
if(OIdentifier){
if(OIdentifier && mode==="subject"){
setIsSpin(true);
getProject();
}
},[OIdentifier,sortBy,page,search])
},[OIdentifier,sortBy,page,search,mode])
function getProject(){
const url = `/organizations/${OIdentifier}/projects.json`;
@ -76,55 +96,129 @@ function List(props){
</li>
</ul>
)
function changeRadio(e){
setMode(e.target.value);
}
return(
<div className="list">
<div className="list-l">
<div>
<div className="head">
<div style={{width:"370px"}}>
<Search placeholder="输入仓库名称进行搜索" onSearch={onSearch}/>
</div>
<p>
{ organizeDetail && organizeDetail.can_create_project ?
<Sort menu={menu_new} overlayClassName={"newPopUl"}>
<a className="addBtn mr30">+&nbsp;新建项目</a>
</Sort>
:""}
<Dropdown overlay={menu}>
<a className="color-blue">排序<i className="iconfont icon-sanjiaoxing-down ml3 font-14"></i></a>
</Dropdown>
</p>
</div>
<Spin spinning={isSpin}>
<div className="team">
{
list && list.length>0 ? list.map((item,key)=>{
return(
<Item item={item} keu={key} OIdentifier={OIdentifier}/>
)
})
:
<NoData _html="暂无数据"/>
}
</div>
</Spin>
organizeDetail ?
<div className="content-orz">
<div className="top-orz">
<div className="box">
<img src={getImageUrl(organizeDetail && organizeDetail.avatar_url)} alt=""/>
<div className="info-orz">
<p className="orz-main">
<span className="orz-name">{organizeDetail && organizeDetail.nickname}</span>
{organizeDetail && organizeDetail.is_admin ?
<Link to={`/${OIdentifier}/setting`} className="color-blue ml10 font-14 df"><i className="iconfont icon-shezhi2"></i>设置</Link>
:""}
</p>
<div className="orz-desc task-hide-2">{organizeDetail && organizeDetail.description}</div>
</div>
</div>
</div>
<div className="list">
<div className="list-l">
{
totalCount > limit &&
<div className="mb20 mt20" style={{textAlign:"center"}}>
<Pagination simple current={page} total={totalCount} pageSize={limit} onChange={(page)=>setPage(page)}/>
organizeDetail.news_title &&
<div className="newstatus">
<p className="name"><i className="iconfont icon-dongtaiicon1" /><span>新闻动态</span></p>
<p className="title">
{organizeDetail.news_title}
</p>
<div className="desc">
{organizeDetail.news_content}
</div>
</div>
}
<div className="sepment" onChange={changeRadio} >
<Radio.Group size="large" value={mode}>
<Radio.Button value="overview" style={{width:"130px"}}>概览</Radio.Button>
<Radio.Button value="subject" style={{width:"130px"}}>仓库</Radio.Button>
</Radio.Group>
</div>
{
mode === "overview" &&
<div className="overviewcontent">
<ConcentrateProject
{...props}
title={
<p className="names">
<i className="iconfont icon-jingxuanxiangmu font-17 mr8"></i>
<span className="font-17">精选项目</span>
</p>
}
btn={
<span><i className="iconfont icon-zidingyi font-14 mr5"></i>自定义</span>
}
userLogin={OIdentifier}
type="organizations"
current={organizeDetail.is_admin}
/>
<div>
<p className="mt30 mb10">
<i className="iconfont icon-zuzhijieshao mr4"></i>
<span className="font-17">组织介绍</span>
</p>
<div style={{color:"#4c5876",lineHeight:"30px",wordBreak:"break-all",textAlign:"justify"}}>{organizeDetail.description}</div>
</div>
</div>
}
{
mode === "subject" &&
<div>
<div>
<div className="head">
<div style={{width:"370px"}}>
<Search placeholder="输入仓库名称进行搜索" onSearch={onSearch}/>
</div>
<p>
{ organizeDetail && organizeDetail.can_create_project ?
<Sort menu={menu_new} overlayClassName={"newPopUl"}>
<a className="addBtn mr30">+&nbsp;新建项目</a>
</Sort>
:""}
<Dropdown overlay={menu}>
<a className="color-blue">排序<i className="iconfont icon-sanjiaoxing-down ml3 font-14"></i></a>
</Dropdown>
</p>
</div>
<Spin spinning={isSpin}>
<div className="team">
{
list && list.length>0 ? list.map((item,key)=>{
return(
<Item item={item} keu={key} OIdentifier={OIdentifier}/>
)
})
:
<NoData _html="暂无数据"/>
}
</div>
</Spin>
</div>
{
totalCount > limit &&
<div className="mb20 mt20" style={{textAlign:"center"}}>
<Pagination simple current={page} total={totalCount} pageSize={limit} onChange={(page)=>setPage(page)}/>
</div>
}
</div>
}
</div>
<Right
admin={organizeDetail && organizeDetail.is_admin}
OIdentifier={OIdentifier}
showCompeleteDialog={props.showCompeleteDialog}
completeProfile={props.completeProfile}
history={props.history}
/>
<Right
admin={organizeDetail && organizeDetail.is_admin}
OIdentifier={OIdentifier}
showCompeleteDialog={props.showCompeleteDialog}
completeProfile={props.completeProfile}
history={props.history}
/>
</div>
</div>
:""
)
}
export default List;

View File

@ -5,6 +5,8 @@ import axios from 'axios';
import { getImageUrl } from 'educoder';
import { Link } from 'react-router-dom';
import Nodata from '../Nodata';
import { Progress } from 'antd';
import Right from './images/right.png';
import CheckProfile from '../Component/ProfileModal/Profile';
@ -25,7 +27,7 @@ const ListName = styled.div`{
line-height:18px;
}`;
const ColorListName = styled.div`{
color:#5091FF;
color:rgba(31, 35, 41, 1);
font-size:14px;
margin-bottom:8px;
height:18px;
@ -38,6 +40,7 @@ const Img = styled.img`{
margin-right:12px;
}`
function RightBox({ OIdentifier , history , admin , showCompeleteDialog ,completeProfile }) {
const [ languageData, setLanguageData ] = useState(undefined);
const [ memberData, setMemberData ] = useState(undefined);
const [ groupData, setGroupData ] = useState(undefined);
@ -45,8 +48,32 @@ function RightBox({ OIdentifier , history , admin , showCompeleteDialog ,complet
if(OIdentifier){
getMember(OIdentifier);
getGroup(OIdentifier);
getLanguage(OIdentifier);
}
},[OIdentifier])
function getLanguage(iden){
const url = `/organizations/${iden}/languages.json`;
axios.get(url,{params:{limit:10}}).then(result=>{
if(result && result.data){
let languages = result.data;
let arr = [];
Object.keys(languages).map((item,key)=>{
arr.push({name:item,percent:languages[item].replace("%",""),color:getColor()});
})
setLanguageData(arr);
console.log(result.data);
}
}).catch(error=>{})
}
  function getColor(){
let str = "#";
let arr = ["1","2","3","4","4","5","6","7","8","9","a","b","c","d","e","f"];
for(var i=0;i<6;i++){
let num = parseInt(Math.random() * 16);
str+=arr[num];
}
return str;
}
function getMember(iden){
const url = `/organizations/${iden}/organization_users.json`;
@ -66,17 +93,43 @@ function RightBox({ OIdentifier , history , admin , showCompeleteDialog ,complet
}
return(
<div className="list-r">
{
languageData ?
<Box
name="仓库语言"
// count={languageData && languageData.length}
icon={<i className="iconfont icon-cangkuyuyanicon font-17 mr8"></i>}
// url={`/${OIdentifier}/members`}
>
{
languageData.map((item,key)=>{
return(
<div className="progress">
<span className="mr10">{item.name}</span>
<Progress percent={item.percent} strokeColor={item.color}/>
</div>
)
})
}
</Box>
:""
}
{
memberData && memberData.organization_users && memberData.organization_users.length>0 ?
<Box name="组织成员" count={memberData && memberData.total_count} url={`/${OIdentifier}/members`}>
<Box
name="组织成员"
count={memberData && memberData.total_count}
icon={<i className="iconfont icon-zuzhichengyuan1 font-16 mr8"></i>}
url={`/${OIdentifier}/members`}>
{
memberData.organization_users.map((item,key)=>{
return(
<div className="teammembers" key={key}>
key<5 && <div className="teammembers" key={key}>
<Link to={`/${item.user && item.user.login}`}><Img src={getImageUrl(`/${item.user && item.user.image_url}`)} alt="" className="m-img"/></Link>
<div>
<Link to={`/${item.user && item.user.login}`}><ListName>{item.user && item.user.name}</ListName></Link>
<Align><i className="iconfont icon-shijian color-green mr3 font-13"></i><Span>加入时间{item.created_at}</Span></Align>
<Align><Span>加入时间{item.created_at}</Span></Align>
{/* <i className="iconfont icon-shijian color-green mr3 font-13"></i> */}
</div>
</div>
)
@ -88,14 +141,15 @@ function RightBox({ OIdentifier , history , admin , showCompeleteDialog ,complet
<Box
name="组织团队"
count={groupData && groupData.total_count}
icon={<i className="iconfont icon-zuzhituandui font-17 mr8"></i>}
bottom={
admin &&
<CheckProfile
showCompeleteDialog={showCompeleteDialog}
completeProfile={completeProfile}
sureFunc={()=>history.push(`/${OIdentifier}/teams/new`)}
className={"ant-btn ant-btn-primary"}
>新建团队</CheckProfile>
className={"ant-btn newBtn"}
>新建团队<img src={Right} alt="" width="9" className="ml5" /></CheckProfile>
}
url={`/${OIdentifier}/teams`}
>
@ -104,7 +158,8 @@ function RightBox({ OIdentifier , history , admin , showCompeleteDialog ,complet
<React.Fragment>
{groupData.teams.map((item,key)=>{
return(
<div className="teammembers" key={key}>
key < 5 &&<div className="teammembers" key={key}>
<span className="memberIndex">{key+1}</span>
<div>
{
(item.is_admin || item.is_member) ?
@ -113,8 +168,8 @@ function RightBox({ OIdentifier , history , admin , showCompeleteDialog ,complet
<ColorListName>{item.name}</ColorListName>
}
<Align>
<Span>{item.num_users}名成员</Span>
<Span>{item.num_projects}个仓库</Span>
<Span><span style={{color:"rgba(31, 35, 41, 1)"}}>{item.num_users}</span>名成员</Span>
<Span><span style={{color:"rgba(31, 35, 41, 1)"}}>{item.num_projects}</span>个仓库</Span>
</Align>
</div>
</div>

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 736 B

View File

@ -5,7 +5,7 @@ import CheckProfile from '../../Component/ProfileModal/Profile';
const { Search } = Input;
const limit = 20;
function ConcentrateBox({ visible , onCancel , onSure , username , choosed , history , showCompeleteDialog , completeProfile }) {
function ConcentrateBox({ visible , onCancel , onSure , type , username , choosed , history , showCompeleteDialog , completeProfile }) {
const [ page , setPage ]= useState(1);
const [ total , setTotal ]= useState(0);
const [ pageSize , setPageSize ] = useState(false);
@ -54,7 +54,7 @@ function ConcentrateBox({ visible , onCancel , onSure , username , choosed , his
},[value])
function getProjectList(p,s) {
const url = `/users/${username}/projects.json`;
const url = `/${type || `users`}/${username}/projects.json`;
Axios.get(url,{
params:{
page:p,limit,is_public: "public",search:s,choosed
@ -152,15 +152,16 @@ function ConcentrateBox({ visible , onCancel , onSure , username , choosed , his
{
copyList && copyList.length >0 && copyList.map((i,k)=>{
return(
<Checkbox value={i.id} disabled={disable && (value.filter(j=>j === i.id).length===0)}>{i.author && i.author.name}/{i.name}</Checkbox>
<Checkbox value={i.id} disabled={disable && (value.filter(j=>j === i.id).length===0)}>{i.author && `${i.author.name}/`}{i.name}</Checkbox>
)
})
}
{
list && list.length > 0 && list.map((i,k)=>{
console.log(i);
let c = copyList && copyList.length >0 && copyList.filter(j=>j.id === i.id).length !== 0;
return(
!c && <Checkbox value={i.id} disabled={disable && (value.filter(j=>j === i.id).length===0)}>{i.author && i.author.name}/{i.name}</Checkbox>
!c && <Checkbox value={i.id} disabled={disable && (value.filter(j=>j === i.id).length===0)}>{i.author && `${i.author.name}/`}{i.name}</Checkbox>
)
})
}

View File

@ -3,8 +3,18 @@ import { FlexAJ , AlignCenter } from '../../Component/layout';
import { Link } from 'react-router-dom';
import axios from 'axios';
import Box from './ConcentrateBox';
import './Index.scss';
function ConcentrateProject({userLogin,current,showCompeleteDialog,completeProfile,history}) {
function ConcentrateProject({
userLogin,
current,
showCompeleteDialog,
completeProfile,
history,
type,
title,
btn
}) {
const [ list , setList ] = useState(undefined);
const [ visible , setVisible ] = useState(false);
const [ value , setValue ] = useState([]);
@ -14,7 +24,7 @@ function ConcentrateProject({userLogin,current,showCompeleteDialog,completeProfi
},[])
function getList() {
const url = `/users/${userLogin}/is_pinned_projects.json`;
const url = `/${type || `users`}/${userLogin}/is_pinned_projects.json`;
axios.get(url).then(result=>{
if(result && result.data){
let p = result.data.projects;
@ -33,7 +43,7 @@ function ConcentrateProject({userLogin,current,showCompeleteDialog,completeProfi
if(is_pinned_project_ids && is_pinned_project_ids.length===0){
setValue([]);
}
const url = `/users/${userLogin}/is_pinned_projects/pin.json`;
const url = `/${type || `users`}/${userLogin}/is_pinned_projects/pin.json`;
axios.post(url,{
is_pinned_project_ids
}).then(result=>{
@ -54,14 +64,21 @@ function ConcentrateProject({userLogin,current,showCompeleteDialog,completeProfi
completeProfile={completeProfile}
showCompeleteDialog={showCompeleteDialog}
history={history}
type={type}
/>
{
list && list.length>0 &&
<div className="concentrate">
<FlexAJ>
<span className="font-18">精选项目</span>
{ current && <a className="color-blue" onClick={()=>setVisible(true)}>自定义精选项目</a> }
{
title || <span className="font-18">精选项目</span>
}
{ current &&
<a className="color-blue" onClick={()=>setVisible(true)}>
{btn || "自定义精选项目"}
</a>
}
</FlexAJ>
{
list && list.length>0 &&
<div>
<ul className="concentrateUl">
{
@ -71,12 +88,12 @@ function ConcentrateProject({userLogin,current,showCompeleteDialog,completeProfi
<Link to={`/${i.author && i.author.login}/${i.identifier}`} className="name">{i.name}</Link>
<p className="task-hide desc">{i.description}</p>
{/* 项目标签 */}
{i.topics && <div className='viewProListTopics'>
{i.topics && <div className='viewProListTopics mt10'>
{i.topics.map(item=><Link to={`/explore/topic/${item.id}/${encodeURIComponent(item.name)}`} className='viewProListTopic mr15 font-13 task-hide'>{item.name}</Link>)}
</div>}
<AlignCenter>
{ i.category && <span className="tagName">{i.category.name}</span> }
<span className="pariseCount"><i className="iconfont icon-xingzhuang"></i>{i.watchers_count}</span>
<span className="pariseCount ml2"><i className="iconfont icon-xingzhuang"></i>{i.watchers_count}</span>
<span className="forkCount"><i className="iconfont icon-morenfuke_icon1"></i>{i.forked_count}</span>
</AlignCenter>
</li>
@ -85,8 +102,8 @@ function ConcentrateProject({userLogin,current,showCompeleteDialog,completeProfi
}
</ul>
</div>
</div>
}
</div>
{
list && list.length === 0 && current && <div className="ConcentrateTip"><i className="iconfont icon-tishi2"></i>你还没有设置精选项目<a onClick={()=>setVisible(true)}>点击设置</a></div>
}

View File

@ -18,14 +18,16 @@
}
.name{
font-size: 16px;
// color: #4CACFF;
color: $primary-color;
height: 20px;
line-height: 20px;
&:hover{
color: $primary-color-hover;
}
}
.desc{
color: #999;
line-height: 21px;
}
.tagName{
display: block;
@ -36,6 +38,7 @@
height: 22px;
line-height: 22px;
font-size: 13px;
margin-right: 20px;
}
.pariseCount,.forkCount{
i{
@ -43,7 +46,7 @@
margin-right: 4px;
}
color: #999;
margin-left: 20px;
margin-right: 20px;
}
}
}
@ -152,13 +155,13 @@
display: flex;
flex-wrap: wrap;
.viewProListTopic{
background-color:#f3f8ff;
background-color:rgba(70, 106, 255, 0.12);
border-radius: 4px;
max-width: 158px;
padding: 0 10px;
height: 24px;
line-height: 24px;
color: #4c5b76;
color:#466aff;
margin-bottom: 5px !important;
}
}

View File

@ -14,14 +14,21 @@ function LoginRegisterPage(props){
return(
<div className="loginRegister">
<div className="login_register_left">
<img src={logo} className="logo" onClick={()=>{window.location.href='/'}}></img>
<img src={ball} className="ball"></img>
<img src={banner} className="banner"></img>
<img src={logo} className="logo" alt="" onClick={()=>{window.location.href='/'}} />
<img src={ball} className="ball" alt="" />
<img src={banner} className="banner" alt="" />
</div>
<div className="login_register_right">
{props.location.pathname === "/login" ? <Login {...props}/> : props.location.pathname === "/register" ? <Register mygetHelmetapi={mygetHelmetapi}/> : <ResetPassword mygetHelmetapi={mygetHelmetapi}/>}
<img src={img1} className="img1"></img>
<img src={img2} className="img2"></img>
{props.location.pathname === "/login" ?
<Login {...props}/>
:
props.location.pathname === "/register" ?
<Register mygetHelmetapi={mygetHelmetapi}/>
:
<ResetPassword mygetHelmetapi={mygetHelmetapi}/>
}
<img src={img1} className="img1" alt="" />
<img src={img2} className="img2" alt="" />
</div>
<div className="clear"></div>
</div>

View File

@ -5,9 +5,41 @@
color:#5091FF !important;
border-color: #5091FF !important;
}
.quoteDiv{
width: 360px!important;
max-height: 280px!important;
}
.quoteDiv > li{
display: flex;
align-items: center;
height: 32px;
padding:0px 10px;
border-bottom: 1px solid rgba(212, 212, 212, 0.5);
}
.quoteDiv > li.active{
background-color: #F3F4F6;
}
.quoteDiv > li:last-child{
border-bottom: none;
}
.issueIndex{
background-color: rgba(213, 220, 246, 0.36);
border-radius: 4px;
height: 19px;
line-height: 19px;
color: #666666;
text-align: center;
float: left;
padding: 0px 2px;
cursor: pointer;
font-size: 12px;
margin-right: 7px;
}
.issueName{
flex:1
}
/*md编辑器中输入@弹出可选人列表样式*/
.at_who_list{
.at_who_list,.quoteDiv{
position: absolute;
z-index: 100;
width: 180px;

View File

@ -1,6 +1,8 @@
import React, { Fragment, useEffect, useRef, useState } from 'react';
import { getUploadActionUrl, getUrl } from 'educoder';
import ResizeObserver from 'resize-observer-polyfill';
// import myxss from '../../../common/filterXss';
import xss from 'xss';
import { getImageUrl } from 'educoder';
import axios from 'axios';
import '../../courses/css/Courses.css';
@ -74,8 +76,8 @@ function md_elocalStorage(editor, mdu, id) {
return tid
}
// isFocus是否聚焦到md编译器
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, isFocus = true }) => {
export default ({ mdID, onChange, onCMBeforeChange, onCMBlur, error = false, className = '', noStorage = false, imageExpand = true, placeholder = '', width = '100%', height = 400, initValue = '', emoji, watch=true, showNullButton = false, showResizeBar = false, startInit = true , forMember = true , isCanAtme = false , isQuoteIssue = false , changeAtWhoLoginList, owner, projectsId , isFocus = true}) => {
const editorEl = useRef();
const resizeBarEl = useRef();
const [editorInstance, setEditorInstance] = useState();
@ -91,6 +93,13 @@ export default ({ mdID, onChange, onCMBeforeChange, onCMBlur, error = false, cla
const editorBodyId = `mdEditors_${mdID}`;
const tipId = `e_tips_mdEditor_${mdID}`;
// quote相关
const [issues, setIssues ] = useState([]);
const [quoteVisible, setQuoteVisible] = useState(false);
const [screenIssues, setScreenIssues ] = useState([]);
const quoteVisibleRef = useRef(false);
const chooseIssuesList = useRef([]);
useEffect(()=>{
//请求members接口获取全部可@列表
isCanAtme && axios.get(`/${owner}/${projectsId}/members.json`).then(response=>{
@ -103,9 +112,108 @@ export default ({ mdID, onChange, onCMBeforeChange, onCMBlur, error = false, cla
document.addEventListener('click',()=>{
atWhoVisibleRef.current = false;
setAtWhoVisible(false);
quoteVisibleRef.current = false;
setQuoteVisible(false);
})
isQuoteIssue && getIssueList();
},[])
// 请求issues接口获取全部可quote的issue列表
function getIssueList(keyword){
axios.get(`/v1/${owner}/${projectsId}/issues`,{params:{
only_name:true,keyword,sort_direction:"desc",sort_by:"issues.created_on"
}}).then(result=>{
if(result){
!keyword && setIssues(result.data.issues);
let data = result.data.issues;
setScreenIssues(data && data.length > 0 ? data:undefined);
}
})
}
// # 引用的弹框div
const QuoteDiv = (
<div id="quoteDiv" className="quoteDiv">
{
screenIssues && screenIssues.map((i,k)=>{
return(
<li className={`quote ${k === 0 && "active"}`} onClick={()=>selectQuoteIssue(i)} onMouseOver={()=>onMouseOverQuote(k)}>
<span style={{minWidth:"40px"}}><span className="issueIndex" title={i.project_issues_index}>#{i.project_issues_index}</span></span>
<span className="issueName task-hide">{i.subject}</span>
</li>
)
})
}
</div>
)
// 点击选择要引用的issue
function selectQuoteIssue(i){
quoteVisibleRef.current = false;
setQuoteVisible(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("#");
//替换内容
cm.replaceRange(`[#${i.project_issues_index}](/${owner}/${projectsId}/issues/${i.project_issues_index}) `,{line,ch:startIndex},{line,ch});
//鼠标聚焦
cm.focus();
// 保存引用的issue
const list = new Set(chooseIssuesList.current);
list.add(i.project_issues_index);
chooseIssuesList.current = Array.from(list);
}
function onMouseOverQuote(key){
document.getElementsByClassName("quote active")[0] && (document.getElementsByClassName("quote active")[0].className="quote");
document.getElementsByClassName("quote")[key] && (document.getElementsByClassName("quote")[key].className="quote active");
}
useEffect(()=>{
if(cmEl){
if(quoteVisibleRef.current){
const atWhoListDiv = document.getElementById("quoteDiv");
const atWhoDivs = document.getElementsByClassName("quote");
cmEl.addKeyMap({
'Up':()=>{
let index;
for(let i = 0; i<atWhoDivs.length;i++){
atWhoDivs[i].className === "quote active" && (index = i);
}
if(index>0){
index <=atWhoDivs.length-4 && (atWhoListDiv.scrollTop -=40)
atWhoDivs[index].className = "quote";
atWhoDivs[index-1].className = "quote active";
}
},
'Down':()=>{
let index;
for(let i = 0; i<atWhoDivs.length;i++){
atWhoDivs[i].className === "quote active" && (index = i);
}
if(index<atWhoDivs.length-1){
index >=3 && (atWhoListDiv.scrollTop +=40)
atWhoDivs[index].className = "quote";
atWhoDivs[index+1].className = "quote active";
}
},
'Enter':()=>{
//找到classname为quote active的div执行click事件
if(document.getElementsByClassName("quote active")[0]){
document.getElementsByClassName("quote active")[0].click();
}
}
})
} else {
//移除上下、enter键监听
cmEl.removeKeyMap();
}
}
},[quoteVisible])
function onLayout() {
let ro;
if (editorEl.current) {
@ -153,23 +261,22 @@ export default ({ mdID, onChange, onCMBeforeChange, onCMBlur, error = false, cla
}
//markdown编辑器中输入的键盘监听事件
function mdKeyDown(e){
function mdKeyDown(e){ //获取光标位置
const cssStyle = document.getElementsByClassName("CodeMirror cm-s-default CodeMirror-wrap")[0].firstChild.style;
//设置弹框位置
const newTop = 62
const newLeft = 20;
const codemirror = editorInstance.cm;
let value = codemirror.getValue();
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 = 62
const newLeft = 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(e.ctrlKey && e.code === "KeyZ" && allUsers.length !== 0){
//处理初始内容就自带@谁的情况
if(initValue){
const del = [];
@ -194,6 +301,13 @@ export default ({ mdID, onChange, onCMBeforeChange, onCMBlur, error = false, cla
}
})
}
if(e.shiftKey && e.code === "Digit3" && isQuoteIssue){
// 输入#键后在对应的位置显示可选的项目issue
quoteVisibleRef.current = true;
setQuoteVisible(true);
document.getElementById("quoteDiv").style.top = parseInt(cssStyle.getPropertyValue("top").replace("px","")) + newTop +"px";
document.getElementById("quoteDiv").style.left = parseInt(cssStyle.getPropertyValue("left").replace("px",""))+newLeft+"px";
}
}
useEffect(()=>{
@ -233,8 +347,7 @@ export default ({ mdID, onChange, onCMBeforeChange, onCMBlur, error = false, cla
htmlDecode: "style,script,iframe",
sequenceDiagram: true,
autoFocus: false,
watch: watch === undefined ? true : watch,
watch: watch,
saveHTMLToTextarea: true,
dialogMaskOpacity: 0.6,
placeholder: placeholder,
@ -265,7 +378,6 @@ export default ({ mdID, onChange, onCMBeforeChange, onCMBlur, error = false, cla
},
"fullScreen":function(cm,icon,cursor,selection){
icon.addClass("none");
console.log(cm,icon)
},
"inline-latex": function (cm, icon, cursor, selection) {
cm.replaceSelection("$$" + selection + "$$");
@ -298,7 +410,7 @@ export default ({ mdID, onChange, onCMBeforeChange, onCMBlur, error = false, cla
if(atWhoVisibleRef.current){
// 添加上下键、enter键监听事件
cmEl.addKeyMap({
'Up':()=>{
'Up':()=>{
const atWhoListDiv = document.getElementById("at_who_list");
const atWhoDivs = document.getElementsByClassName("at_who");
let index;
@ -357,10 +469,11 @@ export default ({ mdID, onChange, onCMBeforeChange, onCMBlur, error = false, cla
}
},[users])
useEffect(() => {
if (cmEl) {
let tid = null
let ro
let ro;
if (onCMBlur) {
editorInstance.cm.on('blur', () => { onCMBlur(editorInstance.getValue()) })
}
@ -374,15 +487,45 @@ export default ({ mdID, onChange, onCMBeforeChange, onCMBlur, error = false, cla
}
//isCanAtme:只有issue和合并请求以及评论部分可以@他人操作
//绑定@事件
isCanAtme && editorInstance.cm.on("focus", () => {
(isCanAtme || isQuoteIssue) && editorInstance.cm.on("focus", () => {
document.addEventListener("keydown", mdKeyDown);
});
isCanAtme && editorInstance.cm.on("blur", () => {
(isCanAtme || isQuoteIssue) && editorInstance.cm.on("blur", () => {
document.removeEventListener("keydown",mdKeyDown);
});
editorInstance.cm.on("change", (cm) => {
//调用父组件的onchange方法将输入内容传入父级组件
onChange && onChange(cm.getValue());
// let reg = /alert\((.*?)\)/g;
let v = cm.getValue();
// if(v){
// let matchvalue = v.match(reg);
// if(matchvalue && matchvalue.length>0){
// for(var x=0;x<matchvalue.length;x++){
// v = v.replace(matchvalue[x],"");
// }
// }
// }
// xss()防止恶意代码如alert等
onChange && onChange(xss(v));
if(quoteVisibleRef.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){
setQuoteVisible(false);
quoteVisibleRef.current = false;
}else{
rangeCont = rangeCont.substring(rangeCont.lastIndexOf("#")+1);
if(rangeCont === " "){
setQuoteVisible(false);
quoteVisibleRef.current = false;
}else{
getIssueList(rangeCont);
}
}
}
if(atWhoVisibleRef.current){
//搜索用户(弹框之后用户输入用户名信息)
const cur = cm.doc.getCursor();
@ -409,11 +552,12 @@ export default ({ mdID, onChange, onCMBeforeChange, onCMBlur, error = false, cla
}
}
changeValueTime("#",issues,chooseIssuesList.current);
//当内容发生改变并且有已@列表时
if(atWhoLoginList.current.length != 0){
const codemirror = editorInstance.cm;
//startValue触发change方法时的内容value处理了初始内容带@用户的情况
let startValue = codemirror.getValue();
let value = codemirror.getValue();
//处理初始内容就自带@谁的情况
if(initValue){
@ -442,13 +586,6 @@ export default ({ mdID, onChange, onCMBeforeChange, onCMBlur, error = false, cla
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([]);
}
@ -467,21 +604,6 @@ export default ({ mdID, onChange, onCMBeforeChange, onCMBlur, error = false, cla
}
//处理已经有@列表但是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));
@ -516,6 +638,86 @@ export default ({ mdID, onChange, onCMBeforeChange, onCMBlur, error = false, cla
}
}, [cmEl])
function changeValueTime(str,alllist,chooselsit){
if(chooselsit.length != 0){
const codemirror = editorInstance.cm;
//startValue触发change方法时的内容value处理了初始内容带@用户的情况
let value = codemirror.getValue();
//处理初始内容就自带@谁的情况
if(initValue){
const del = [];
alllist.map(item=>{
let strings = item.username;
if(str==="#"){
strings = item.project_issues_index;
}
if(initValue.indexOf(strings)!=-1 && initValue.charAt(initValue.indexOf(strings)-1) === str && initValue.indexOf(`${str}${strings}`)===value.indexOf(`${str}${strings}`)){
//初始内容中有符合@+名字的格式并且当前内容未删除初始内容
del[del.length] = `[${str}${strings}](/${strings})`;
if(str==="#"){
del[del.length] = `[${str}${strings}](${owner}/${projectsId}/issues/${strings})`;
}
}
})
del.length!=0 && del.map(str=>{
value = value.replace(str,"");
})
}
//以project_issues_index为主键和value的map集合
let atWhoMap = new Map();
Array.from(chooselsit).map(item=>{
alllist.map(i=>{
if(i.project_issues_index === item){
atWhoMap.set(i.project_issues_index,i.project_issues_index);
}
})
});
const cursor = codemirror.doc.getCursor();
const line = cursor.line;
const ch = cursor.ch;
//处理全部内容中不包含“@”的情况
if(value.indexOf(str) === -1){
if(str==="#"){
chooseIssuesList.current = [];
}
}
//截取第一个字符到光标的内容
const curAfterCont = codemirror.getRange({line,ch:0},{line,ch});
const content = codemirror.getLine(line);
//处理光标所在行 有“@”的情况
if(content && content.indexOf(str) !== -1){
Array.from(atWhoMap.keys()).map(index=>{
//判断content是不是以列表中的某个username结尾
const userCont = `[${str}${index}](/${owner}/${projectsId}/issues/${atWhoMap.get(index)})`;
//删除空格->选中@用户区域
if(curAfterCont.endsWith(userCont)){
codemirror.setSelection({line,ch:curAfterCont.lastIndexOf(str)-1},{line,ch});
}
if(value.indexOf(userCont)===-1){
//符合情况->从数组中删除
if(str==="#"){
const list = new Set(chooseIssuesList.current);
list.delete(atWhoMap.get(index));
chooseIssuesList.current = Array.from(list);
}
}
})
}else{
//处理所在行没有“@”的情况
Array.from(atWhoMap.keys()).map(index=>{
const userCont = `[${str}${index}](/${owner}/${projectsId}/issues/${atWhoMap.get(index)})`;
if(value.indexOf(userCont)===-1){
if(str==="#"){
const list = new Set(chooseIssuesList.current);
list.delete(atWhoMap.get(index));
chooseIssuesList.current = Array.from(list);
}
}
})
}
}
}
useEffect(() => {
if (editorInstance && initValue !== undefined) {
if (initValue !== null && initValue !== editorInstance.getValue()) {
@ -558,14 +760,13 @@ export default ({ mdID, onChange, onCMBeforeChange, onCMBlur, error = false, cla
document.removeEventListener('mouseup', onMouseUp)
}
}
}, [
editorInstance, resizeBarEl
])
}, [ editorInstance, resizeBarEl ])
return (
<Fragment>
{atWhoVisible && atWhoList}
<div ref={editorEl} className={`df editormd-editing ${className} ${imageExpand && 'editormd-image-click-expand'} `}>
{quoteVisible && QuoteDiv }
<div className={`edu-back-greyf5 radius4 editormd ${error ? 'error' : ''}`} id={containerId} >
<textarea style={{ display: 'none' }} id={editorBodyId} name="content"></textarea>
<div className="CodeMirror cm-s-defualt"></div>