This commit is contained in:
2024-03-01 20:28:14 +08:00
commit 076c21dc36
491 changed files with 84482 additions and 0 deletions

View File

@@ -0,0 +1,123 @@
import React, { PureComponent as Component } from 'react';
import GroupList from './GroupList/GroupList.js';
import ProjectList from './ProjectList/ProjectList.js';
import MemberList from './MemberList/MemberList.js';
import GroupLog from './GroupLog/GroupLog.js';
import GroupSetting from './GroupSetting/GroupSetting.js';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Route, Switch, Redirect } from 'react-router-dom';
import { Tabs, Layout, Spin } from 'antd';
const { Content, Sider } = Layout;
const TabPane = Tabs.TabPane;
import { fetchNewsData } from '../../reducer/modules/news.js';
import {
setCurrGroup
} from '../../reducer/modules/group';
import './Group.scss';
import axios from 'axios'
@connect(
state => {
return {
curGroupId: state.group.currGroup._id,
curUserRole: state.user.role,
curUserRoleInGroup: state.group.currGroup.role || state.group.role,
currGroup: state.group.currGroup
};
},
{
fetchNewsData: fetchNewsData,
setCurrGroup
}
)
export default class Group extends Component {
constructor(props) {
super(props);
this.state = {
groupId: -1
}
}
async componentDidMount(){
let r = await axios.get('/api/group/get_mygroup')
try{
let group = r.data.data;
this.setState({
groupId: group._id
})
this.props.setCurrGroup(group)
}catch(e){
console.error(e)
}
}
static propTypes = {
fetchNewsData: PropTypes.func,
curGroupId: PropTypes.number,
curUserRole: PropTypes.string,
currGroup: PropTypes.object,
curUserRoleInGroup: PropTypes.string,
setCurrGroup: PropTypes.func
};
// onTabClick=(key)=> {
// // if (key == 3) {
// // this.props.fetchNewsData(this.props.curGroupId, "group", 1, 10)
// // }
// }
render() {
if(this.state.groupId === -1)return <Spin />
const GroupContent = (
<Layout style={{ minHeight: 'calc(100vh - 100px)', marginLeft: '24px', marginTop: '24px' }}>
<Sider style={{ height: '100%' }} width={300}>
<div className="logo" />
<GroupList />
</Sider>
<Layout>
<Content
style={{
height: '100%',
margin: '0 24px 0 16px',
overflow: 'initial',
backgroundColor: '#fff'
}}
>
<Tabs type="card" className="m-tab tabs-large" style={{ height: '100%' }}>
<TabPane tab="项目列表" key="1">
<ProjectList />
</TabPane>
{this.props.currGroup.type === 'public' ? (
<TabPane tab="成员列表" key="2">
<MemberList />
</TabPane>
) : null}
{['admin', 'owner', 'guest', 'dev'].indexOf(this.props.curUserRoleInGroup) > -1 ||
this.props.curUserRole === 'admin' ? (
<TabPane tab="分组动态" key="3">
<GroupLog />
</TabPane>
) : (
''
)}
{(this.props.curUserRole === 'admin' || this.props.curUserRoleInGroup === 'owner') &&
this.props.currGroup.type !== 'private' ? (
<TabPane tab="分组设置" key="4">
<GroupSetting />
</TabPane>
) : null}
</Tabs>
</Content>
</Layout>
</Layout>
);
return (
<div className="projectGround">
<Switch>
<Redirect exact from="/group" to={"/group/" + this.state.groupId} />
<Route path="/group/:groupId" render={() => GroupContent} />
</Switch>
</div>
);
}
}

View File

@@ -0,0 +1,10 @@
@import '../../styles/mixin.scss';
.g-doc {
@include row-width-limit;
margin: 0 auto .24rem;
}
.news-box .news-timeline .ant-timeline-item .ant-timeline-item-content{
min-width: 300px !important;
width: 75% !important;
}

View File

@@ -0,0 +1,319 @@
import React, { PureComponent as Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Icon, Modal, Input, message,Spin, Row, Menu, Col, Popover, Tooltip } from 'antd';
import { autobind } from 'core-decorators';
import axios from 'axios';
import { withRouter } from 'react-router-dom';
const { TextArea } = Input;
const Search = Input.Search;
import UsernameAutoComplete from '../../../components/UsernameAutoComplete/UsernameAutoComplete.js';
import GuideBtns from '../../../components/GuideBtns/GuideBtns.js';
import { fetchNewsData } from '../../../reducer/modules/news.js';
import {
fetchGroupList,
setCurrGroup,
setGroupList,
fetchGroupMsg
} from '../../../reducer/modules/group.js';
import _ from 'underscore';
import './GroupList.scss';
const tip = (
<div className="title-container">
<h3 className="title">欢迎使用 YApi ~</h3>
<p>
这里的 <b>个人空间</b>{' '}
是你自己才能看到的分组你拥有这个分组的全部权限可以在这个分组里探索 YApi 的功能
</p>
</div>
);
@connect(
state => ({
groupList: state.group.groupList,
currGroup: state.group.currGroup,
curUserRole: state.user.role,
curUserRoleInGroup: state.group.currGroup.role || state.group.role,
studyTip: state.user.studyTip,
study: state.user.study
}),
{
fetchGroupList,
setCurrGroup,
setGroupList,
fetchNewsData,
fetchGroupMsg
}
)
@withRouter
export default class GroupList extends Component {
static propTypes = {
groupList: PropTypes.array,
currGroup: PropTypes.object,
fetchGroupList: PropTypes.func,
setCurrGroup: PropTypes.func,
setGroupList: PropTypes.func,
match: PropTypes.object,
history: PropTypes.object,
curUserRole: PropTypes.string,
curUserRoleInGroup: PropTypes.string,
studyTip: PropTypes.number,
study: PropTypes.bool,
fetchNewsData: PropTypes.func,
fetchGroupMsg: PropTypes.func
};
state = {
addGroupModalVisible: false,
newGroupName: '',
newGroupDesc: '',
currGroupName: '',
currGroupDesc: '',
groupList: [],
owner_uids: []
};
constructor(props) {
super(props);
}
async UNSAFE_componentWillMount() {
const groupId = !isNaN(this.props.match.params.groupId)
? parseInt(this.props.match.params.groupId)
: 0;
await this.props.fetchGroupList();
let currGroup = false;
if (this.props.groupList.length && groupId) {
for (let i = 0; i < this.props.groupList.length; i++) {
if (this.props.groupList[i]._id === groupId) {
currGroup = this.props.groupList[i];
}
}
} else if (!groupId && this.props.groupList.length) {
this.props.history.push(`/group/${this.props.groupList[0]._id}`);
}
if (!currGroup) {
currGroup = this.props.groupList[0] || { group_name: '', group_desc: '' };
this.props.history.replace(`${currGroup._id}`);
}
this.setState({ groupList: this.props.groupList });
this.props.setCurrGroup(currGroup);
}
@autobind
showModal() {
this.setState({
addGroupModalVisible: true
});
}
@autobind
hideModal() {
this.setState({
newGroupName: '',
group_name: '',
owner_uids: [],
addGroupModalVisible: false
});
}
@autobind
async addGroup() {
const { newGroupName: group_name, newGroupDesc: group_desc, owner_uids } = this.state;
const res = await axios.post('/api/group/add', { group_name, group_desc, owner_uids });
if (!res.data.errcode) {
this.setState({
newGroupName: '',
group_name: '',
owner_uids: [],
addGroupModalVisible: false
});
await this.props.fetchGroupList();
this.setState({ groupList: this.props.groupList });
this.props.fetchGroupMsg(this.props.currGroup._id);
this.props.fetchNewsData(this.props.currGroup._id, 'group', 1, 10);
} else {
message.error(res.data.errmsg);
}
}
@autobind
async editGroup() {
const { currGroupName: group_name, currGroupDesc: group_desc } = this.state;
const id = this.props.currGroup._id;
const res = await axios.post('/api/group/up', { group_name, group_desc, id });
if (res.data.errcode) {
message.error(res.data.errmsg);
} else {
await this.props.fetchGroupList();
this.setState({ groupList: this.props.groupList });
const currGroup = _.find(this.props.groupList, group => {
return +group._id === +id;
});
this.props.setCurrGroup(currGroup);
// this.props.setCurrGroup({ group_name, group_desc, _id: id });
this.props.fetchGroupMsg(this.props.currGroup._id);
this.props.fetchNewsData(this.props.currGroup._id, 'group', 1, 10);
}
}
@autobind
inputNewGroupName(e) {
this.setState({ newGroupName: e.target.value });
}
@autobind
inputNewGroupDesc(e) {
this.setState({ newGroupDesc: e.target.value });
}
@autobind
selectGroup(e) {
const groupId = e.key;
//const currGroup = this.props.groupList.find((group) => { return +group._id === +groupId });
const currGroup = _.find(this.props.groupList, group => {
return +group._id === +groupId;
});
this.props.setCurrGroup(currGroup);
this.props.history.replace(`${currGroup._id}`);
this.props.fetchNewsData(groupId, 'group', 1, 10);
}
@autobind
onUserSelect(uids) {
this.setState({
owner_uids: uids
});
}
@autobind
searchGroup(e, value) {
const v = value || e.target.value;
const { groupList } = this.props;
if (v === '') {
this.setState({ groupList });
} else {
this.setState({
groupList: groupList.filter(group => new RegExp(v, 'i').test(group.group_name))
});
}
}
UNSAFE_componentWillReceiveProps(nextProps) {
// GroupSetting 组件设置的分组信息通过redux同步到左侧分组菜单中
if (this.props.groupList !== nextProps.groupList) {
this.setState({
groupList: nextProps.groupList
});
}
}
render() {
const { currGroup } = this.props;
return (
<div className="m-group">
{!this.props.study ? <div className="study-mask" /> : null}
<div className="group-bar">
<div className="curr-group">
<div className="curr-group-name">
<span className="name">{currGroup.group_name}</span>
<Tooltip title="添加分组">
<a className="editSet">
<Icon className="btn" type="folder-add" onClick={this.showModal} />
</a>
</Tooltip>
</div>
<div className="curr-group-desc">简介: {currGroup.group_desc}</div>
</div>
<div className="group-operate">
<div className="search">
<Search
placeholder="搜索分类"
onChange={this.searchGroup}
onSearch={v => this.searchGroup(null, v)}
/>
</div>
</div>
{this.state.groupList.length === 0 && <Spin style={{
marginTop: 20,
display: 'flex',
justifyContent: 'center'
}} />}
<Menu
className="group-list"
mode="inline"
onClick={this.selectGroup}
selectedKeys={[`${currGroup._id}`]}
>
{this.state.groupList.map(group => {
if (group.type === 'private') {
return (
<Menu.Item
key={`${group._id}`}
className="group-item"
style={{ zIndex: this.props.studyTip === 0 ? 3 : 1 }}
>
<Icon type="user" />
<Popover
overlayClassName="popover-index"
content={<GuideBtns />}
title={tip}
placement="right"
visible={this.props.studyTip === 0 && !this.props.study}
>
{group.group_name}
</Popover>
</Menu.Item>
);
} else {
return (
<Menu.Item key={`${group._id}`} className="group-item">
<Icon type="folder-open" />
{group.group_name}
</Menu.Item>
);
}
})}
</Menu>
</div>
{this.state.addGroupModalVisible ? (
<Modal
title="添加分组"
visible={this.state.addGroupModalVisible}
onOk={this.addGroup}
onCancel={this.hideModal}
className="add-group-modal"
>
<Row gutter={6} className="modal-input">
<Col span="5">
<div className="label">分组名</div>
</Col>
<Col span="15">
<Input placeholder="请输入分组名称" onChange={this.inputNewGroupName} />
</Col>
</Row>
<Row gutter={6} className="modal-input">
<Col span="5">
<div className="label">简介</div>
</Col>
<Col span="15">
<TextArea rows={3} placeholder="请输入分组描述" onChange={this.inputNewGroupDesc} />
</Col>
</Row>
<Row gutter={6} className="modal-input">
<Col span="5">
<div className="label">组长</div>
</Col>
<Col span="15">
<UsernameAutoComplete callbackState={this.onUserSelect} />
</Col>
</Row>
</Modal>
) : (
''
)}
</div>
);
}
}

View File

@@ -0,0 +1,147 @@
@import '../../../styles/mixin.scss';
.group-bar {
min-height: 5rem;
.curr-group {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei",
SimSun, sans-serif;
background-color: $color-bg-dark;
color: $color-white;
padding: .24rem .24rem 0;
.curr-group-name {
color: $color-white;
font-size: .22rem;
display: flex;
justify-content: space-between;
align-items: center;
.text {
display: inline-block;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
max-width: 140px;
font-weight: 200;
vertical-align: bottom;
}
.name{
display: inline-block;
// width: 117px;
margin-right: 20px;
overflow: hidden;
text-overflow:ellipsis;
white-space: nowrap;
}
.editSet{
color: rgba(255, 255, 255, 0.85);
&:hover{
color: #2395f1;
}
}
.ant-dropdown-link{
float: right;
display: block;
color: rgba(255, 255, 255, 0.85);
&:hover{
color: #2395f1;
}
}
.operate {
font-size: 0;
width: 150px;
display: inline-block;
i{
margin-left: 4px;
}
::-webkit-scrollbar {
width: 0px;
}
}
}
.curr-group-desc {
color: $color-white-secondary;
font-size: 13px;
max-height: 54px;
margin-top: .16rem;
text-overflow:ellipsis;
overflow:hidden;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
display: -webkit-box;
}
.delete-group, .edit-group {
font-size: 14px;
margin-left: .08rem;
cursor: pointer;
border: 1px solid $color-white;
padding: 6px 12px;
border-radius: 4px;
transition: all .2s;
}
.delete-group:hover, .edit-group:hover {
background-color: $color-blue;
border: 1px solid $color-blue;
}
}
.group-operate {
padding: 16px 24px;
display: flex;
justify-content: space-around;
background-color: $color-bg-dark;
.search {
flex-grow: 1;
}
.ant-input {
color: $color-white;
background-color: $color-bg-dark;
}
.ant-input::-webkit-input-placeholder { /* Chrome/Opera/Safari */
color: $color-white-secondary;
}
.ant-input-suffix {
color: $color-white;
}
}
.group-list {
overflow-x: hidden;
border-bottom: 1px solid #e9e9e9;
padding-bottom: 24px;
border: none;
.group-item {
// height: 48px;
// line-height: 48px;
// padding: 0 24px;
font-size: 14px;
}
.group-item:hover {
// background: #34495E;
// color: $color-white;
}
.group-item.selected {
// background: #34495E;
}
.group-name {
float: left;
}
.group-edit {
float: right;
font-size: 18px;
}
}
}
.add-group-modal {
.modal-input {
margin: 24px;
}
.label {
text-align: right;
line-height: 28px;
}
}
.user-menu{
a{
&:hover{
color: #ccc;
}
}
}

View File

@@ -0,0 +1,32 @@
import React, { PureComponent as Component } from 'react';
import TimeTree from '../../../components/TimeLine/TimeLine';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
// import { Button } from 'antd'
@connect(state => {
return {
uid: state.user.uid + '',
curGroupId: state.group.currGroup._id
};
})
class GroupLog extends Component {
constructor(props) {
super(props);
}
static propTypes = {
uid: PropTypes.string,
match: PropTypes.object,
curGroupId: PropTypes.number
};
render() {
return (
<div className="g-row">
<section className="news-box m-panel">
<TimeTree type={'group'} typeid={this.props.curGroupId} />
</section>
</div>
);
}
}
export default GroupLog;

View File

@@ -0,0 +1,306 @@
import React, { PureComponent as Component } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { Input, Button, message, Icon, Card, Alert, Modal, Switch, Row, Col, Tooltip } from 'antd';
import { fetchNewsData } from '../../../reducer/modules/news.js';
import {
changeGroupMsg,
fetchGroupList,
setCurrGroup,
fetchGroupMsg,
updateGroupList,
deleteGroup
} from '../../../reducer/modules/group.js';
const { TextArea } = Input;
import { trim } from '../../../common.js';
import _ from 'underscore';
import './GroupSetting.scss';
const confirm = Modal.confirm;
@connect(
state => {
return {
groupList: state.group.groupList,
currGroup: state.group.currGroup,
curUserRole: state.user.role
};
},
{
changeGroupMsg,
fetchGroupList,
setCurrGroup,
fetchGroupMsg,
fetchNewsData,
updateGroupList,
deleteGroup
}
)
class GroupSetting extends Component {
constructor(props) {
super(props);
this.state = {
currGroupDesc: '',
currGroupName: '',
showDangerOptions: false,
custom_field1_name: '',
custom_field1_enable: false,
custom_field1_rule: false
};
}
static propTypes = {
currGroup: PropTypes.object,
curUserRole: PropTypes.string,
changeGroupMsg: PropTypes.func,
fetchGroupList: PropTypes.func,
setCurrGroup: PropTypes.func,
fetchGroupMsg: PropTypes.func,
fetchNewsData: PropTypes.func,
updateGroupList: PropTypes.func,
deleteGroup: PropTypes.func,
groupList: PropTypes.array
};
initState(props) {
this.setState({
currGroupName: props.currGroup.group_name,
currGroupDesc: props.currGroup.group_desc,
custom_field1_name: props.currGroup.custom_field1.name,
custom_field1_enable: props.currGroup.custom_field1.enable
});
}
// 修改分组名称
changeName = e => {
this.setState({
currGroupName: e.target.value
});
};
// 修改分组描述
changeDesc = e => {
this.setState({
currGroupDesc: e.target.value
});
};
// 修改自定义字段名称
changeCustomName = e => {
let custom_field1_rule = this.state.custom_field1_enable ? !e.target.value : false;
this.setState({
custom_field1_name: e.target.value,
custom_field1_rule
});
};
// 修改开启状态
changeCustomEnable = e => {
let custom_field1_rule = e ? !this.state.custom_field1_name : false;
this.setState({
custom_field1_enable: e,
custom_field1_rule
});
};
UNSAFE_componentWillMount() {
// console.log('custom_field1',this.props.currGroup.custom_field1)
this.initState(this.props);
}
// 点击“查看危险操作”按钮
toggleDangerOptions = () => {
// console.log(this.state.showDangerOptions);
this.setState({
showDangerOptions: !this.state.showDangerOptions
});
};
// 编辑分组信息
editGroup = async () => {
const id = this.props.currGroup._id;
if (this.state.custom_field1_rule) {
return;
}
const res = await this.props.changeGroupMsg({
group_name: this.state.currGroupName,
group_desc: this.state.currGroupDesc,
custom_field1: {
name: this.state.custom_field1_name,
enable: this.state.custom_field1_enable
},
id: this.props.currGroup._id
});
if (!res.payload.data.errcode) {
message.success('修改成功!');
await this.props.fetchGroupList(this.props.groupList);
this.props.updateGroupList(this.props.groupList);
const currGroup = _.find(this.props.groupList, group => {
return +group._id === +id;
});
this.props.setCurrGroup(currGroup);
this.props.fetchGroupMsg(this.props.currGroup._id);
this.props.fetchNewsData(this.props.currGroup._id, 'group', 1, 10);
}
};
// 删除分组
deleteGroup = async () => {
const that = this;
const { currGroup } = that.props;
const res = await this.props.deleteGroup({ id: currGroup._id });
if (!res.payload.data.errcode) {
message.success('删除成功');
await that.props.fetchGroupList();
const currGroup = that.props.groupList[0] || { group_name: '', group_desc: '' };
that.setState({ groupList: that.props.groupList });
that.props.setCurrGroup(currGroup);
}
};
// 删除分组的二次确认
showConfirm = () => {
const that = this;
confirm({
title: '确认删除 ' + that.props.currGroup.group_name + ' 分组吗?',
content: (
<div style={{ marginTop: '10px', fontSize: '13px', lineHeight: '25px' }}>
<Alert
message="警告:此操作非常危险,会删除该分组下面所有项目和接口,并且无法恢复!"
type="warning"
/>
<div style={{ marginTop: '16px' }}>
<p>
<b>请输入分组名称确认此操作:</b>
</p>
<Input id="group_name" />
</div>
</div>
),
onOk() {
const groupName = trim(document.getElementById('group_name').value);
if (that.props.currGroup.group_name !== groupName) {
message.error('分组名称有误');
return new Promise((resolve, reject) => {
reject('error');
});
} else {
that.deleteGroup();
}
},
iconType: 'delete',
onCancel() {}
});
};
UNSAFE_componentWillReceiveProps(nextProps) {
// 切换分组时,更新分组信息并关闭删除分组操作
if (this.props.currGroup._id !== nextProps.currGroup._id) {
this.initState(nextProps);
this.setState({
showDangerOptions: false
});
}
}
render() {
return (
<div className="m-panel card-panel card-panel-s panel-group">
<Row type="flex" justify="space-around" className="row" align="middle">
<Col span={4} className="label">
分组名
</Col>
<Col span={20}>
<Input
size="large"
placeholder="请输入分组名称"
value={this.state.currGroupName}
onChange={this.changeName}
/>
</Col>
</Row>
<Row type="flex" justify="space-around" className="row" align="middle">
<Col span={4} className="label">
简介
</Col>
<Col span={20}>
<TextArea
size="large"
rows={3}
placeholder="请输入分组描述"
value={this.state.currGroupDesc}
onChange={this.changeDesc}
/>
</Col>
</Row>
<Row type="flex" justify="space-around" className="row" align="middle">
<Col span={4} className="label">
接口自定义字段&nbsp;
<Tooltip title={'可以在接口中添加 额外字段 数据'}>
<Icon type="question-circle-o" style={{ width: '10px' }} />
</Tooltip>
</Col>
<Col span={12} style={{ position: 'relative' }}>
<Input
placeholder="请输入自定义字段名称"
style={{ borderColor: this.state.custom_field1_rule ? '#f5222d' : '' }}
value={this.state.custom_field1_name}
onChange={this.changeCustomName}
/>
<div
className="custom-field-rule"
style={{ display: this.state.custom_field1_rule ? 'block' : 'none' }}
>
自定义字段名称不能为空
</div>
</Col>
<Col span={2} className="label">
开启
</Col>
<Col span={6}>
<Switch
checked={this.state.custom_field1_enable}
checkedChildren="开"
unCheckedChildren="关"
onChange={this.changeCustomEnable}
/>
</Col>
</Row>
<Row type="flex" justify="center" className="row save">
<Col span={4} className="save-button">
<Button className="m-btn btn-save" icon="save" type="primary" onClick={this.editGroup}>
</Button>
</Col>
</Row>
{/* 只有超级管理员能删除分组 */}
{this.props.curUserRole === 'admin' ? (
<Row type="flex" justify="center" className="danger-container">
<Col span={24} className="title">
<h2 className="content">
<Icon type="exclamation-circle-o" /> 危险操作
</h2>
<Button onClick={this.toggleDangerOptions}>
<Icon type={this.state.showDangerOptions ? 'up' : 'down'} />
</Button>
</Col>
{this.state.showDangerOptions ? (
<Card hoverable={true} className="card-danger" style={{ width: '100%' }}>
<div className="card-danger-content">
<h3>删除分组</h3>
<p>分组一旦删除将无法恢复数据请慎重操作</p>
<p>只有超级管理员有权限删除分组</p>
</div>
<Button type="danger" ghost className="card-danger-btn" onClick={this.showConfirm}>
删除
</Button>
</Card>
) : null}
</Row>
) : null}
</div>
);
}
}
export default GroupSetting;

View File

@@ -0,0 +1,29 @@
.panel-group {
.row {
// display: flex;
// align-items: center;
margin-bottom: .24rem;
}
.save {
margin-top: .48rem
}
.left {
flex: 100px 0 1;
text-align: right;
}
.right {
flex: 830px 0 1;
}
.label {
text-align: right;
}
.save-button{
text-align: center;
}
.custom-field-rule{
padding-top: 4px;
position: absolute;
color: #f5222d ;
}
}

View File

@@ -0,0 +1,326 @@
import React, { PureComponent as Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Table, Select, Button, Modal, Row, Col, message, Popconfirm } from 'antd';
import { Link } from 'react-router-dom';
import './MemberList.scss';
import { autobind } from 'core-decorators';
import {
fetchGroupMemberList,
fetchGroupMsg,
addMember,
delMember,
changeMemberRole
} from '../../../reducer/modules/group.js';
import ErrMsg from '../../../components/ErrMsg/ErrMsg.js';
import UsernameAutoComplete from '../../../components/UsernameAutoComplete/UsernameAutoComplete.js';
const Option = Select.Option;
function arrayAddKey(arr) {
return arr.map((item, index) => {
return {
...item,
key: index
};
});
}
@connect(
state => {
return {
currGroup: state.group.currGroup,
uid: state.user.uid,
role: state.group.role
};
},
{
fetchGroupMemberList,
fetchGroupMsg,
addMember,
delMember,
changeMemberRole
}
)
class MemberList extends Component {
constructor(props) {
super(props);
this.state = {
userInfo: [],
role: '',
visible: false,
dataSource: [],
inputUids: [],
inputRole: 'dev'
};
}
static propTypes = {
currGroup: PropTypes.object,
uid: PropTypes.number,
fetchGroupMemberList: PropTypes.func,
fetchGroupMsg: PropTypes.func,
addMember: PropTypes.func,
delMember: PropTypes.func,
changeMemberRole: PropTypes.func,
role: PropTypes.string
};
showAddMemberModal = () => {
this.setState({
visible: true
});
};
// 重新获取列表
reFetchList = () => {
this.props.fetchGroupMemberList(this.props.currGroup._id).then(res => {
this.setState({
userInfo: arrayAddKey(res.payload.data.data),
visible: false
});
});
};
// 增 - 添加成员
handleOk = () => {
this.props
.addMember({
id: this.props.currGroup._id,
member_uids: this.state.inputUids,
role: this.state.inputRole
})
.then(res => {
if (!res.payload.data.errcode) {
const { add_members, exist_members } = res.payload.data.data;
const addLength = add_members.length;
const existLength = exist_members.length;
this.setState({
inputRole: 'dev',
inputUids: []
});
message.success(`添加成功! 已成功添加 ${addLength} 人,其中 ${existLength} 人已存在`);
this.reFetchList(); // 添加成功后重新获取分组成员列表
}
});
};
// 添加成员时 选择新增成员权限
changeNewMemberRole = value => {
this.setState({
inputRole: value
});
};
// 删 - 删除分组成员
deleteConfirm = member_uid => {
return () => {
const id = this.props.currGroup._id;
this.props.delMember({ id, member_uid }).then(res => {
if (!res.payload.data.errcode) {
message.success(res.payload.data.errmsg);
this.reFetchList(); // 添加成功后重新获取分组成员列表
}
});
};
};
// 改 - 修改成员权限
changeUserRole = e => {
const id = this.props.currGroup._id;
const role = e.split('-')[0];
const member_uid = e.split('-')[1];
this.props.changeMemberRole({ id, member_uid, role }).then(res => {
if (!res.payload.data.errcode) {
message.success(res.payload.data.errmsg);
this.reFetchList(); // 添加成功后重新获取分组成员列表
}
});
};
// 关闭模态框
handleCancel = () => {
this.setState({
visible: false
});
};
UNSAFE_componentWillReceiveProps(nextProps) {
if (this._groupId !== this._groupId) {
return null;
}
if (this.props.currGroup._id !== nextProps.currGroup._id) {
this.props.fetchGroupMemberList(nextProps.currGroup._id).then(res => {
this.setState({
userInfo: arrayAddKey(res.payload.data.data)
});
});
this.props.fetchGroupMsg(nextProps.currGroup._id).then(res => {
this.setState({
role: res.payload.data.data.role
});
});
}
}
componentDidMount() {
const currGroupId = (this._groupId = this.props.currGroup._id);
this.props.fetchGroupMsg(currGroupId).then(res => {
this.setState({
role: res.payload.data.data.role
});
});
this.props.fetchGroupMemberList(currGroupId).then(res => {
this.setState({
userInfo: arrayAddKey(res.payload.data.data)
});
});
}
@autobind
onUserSelect(uids) {
this.setState({
inputUids: uids
});
}
render() {
const columns = [
{
title:
this.props.currGroup.group_name + ' 分组成员 (' + this.state.userInfo.length + ') 人',
dataIndex: 'username',
key: 'username',
render: (text, record) => {
return (
<div className="m-user">
<Link to={`/user/profile/${record.uid}`}>
<img
src={
location.protocol + '//' + location.host + '/api/user/avatar?uid=' + record.uid
}
className="m-user-img"
/>
</Link>
<Link to={`/user/profile/${record.uid}`}>
<p className="m-user-name">{text}</p>
</Link>
</div>
);
}
},
{
title:
this.state.role === 'owner' || this.state.role === 'admin' ? (
<div className="btn-container">
<Button className="btn" type="primary" onClick={this.showAddMemberModal}>
添加成员
</Button>
</div>
) : (
''
),
key: 'action',
className: 'member-opration',
render: (text, record) => {
if (this.state.role === 'owner' || this.state.role === 'admin') {
return (
<div>
<Select
value={record.role + '-' + record.uid}
className="select"
onChange={this.changeUserRole}
>
<Option value={'owner-' + record.uid}>组长</Option>
<Option value={'dev-' + record.uid}>开发者</Option>
<Option value={'guest-' + record.uid}>访客</Option>
</Select>
<Popconfirm
placement="topRight"
title="你确定要删除吗? "
onConfirm={this.deleteConfirm(record.uid)}
okText="确定"
cancelText=""
>
<Button type="danger" icon="delete" className="btn-danger" />
{/* <Icon type="delete" className="btn-danger"/> */}
</Popconfirm>
</div>
);
} else {
// 非管理员可以看到权限 但无法修改
if (record.role === 'owner') {
return '组长';
} else if (record.role === 'dev') {
return '开发者';
} else if (record.role === 'guest') {
return '访客';
} else {
return '';
}
}
}
}
];
let userinfo = this.state.userInfo;
let ownerinfo = [];
let devinfo = [];
let guestinfo = [];
for (let i = 0; i < userinfo.length; i++) {
if (userinfo[i].role === 'owner') {
ownerinfo.push(userinfo[i]);
}
if (userinfo[i].role === 'dev') {
devinfo.push(userinfo[i]);
}
if (userinfo[i].role === 'guest') {
guestinfo.push(userinfo[i]);
}
}
userinfo = [...ownerinfo, ...devinfo, ...guestinfo];
return (
<div className="m-panel">
{this.state.visible ? (
<Modal
title="添加成员"
visible={this.state.visible}
onOk={this.handleOk}
onCancel={this.handleCancel}
>
<Row gutter={6} className="modal-input">
<Col span="5">
<div className="label usernamelabel">用户名: </div>
</Col>
<Col span="15">
<UsernameAutoComplete callbackState={this.onUserSelect} />
</Col>
</Row>
<Row gutter={6} className="modal-input">
<Col span="5">
<div className="label usernameauth">权限: </div>
</Col>
<Col span="15">
<Select defaultValue="dev" className="select" onChange={this.changeNewMemberRole}>
<Option value="owner">组长</Option>
<Option value="dev">开发者</Option>
<Option value="guest">访客</Option>
</Select>
</Col>
</Row>
</Modal>
) : (
''
)}
<Table
columns={columns}
dataSource={userinfo}
pagination={false}
locale={{ emptyText: <ErrMsg type="noMemberInGroup" /> }}
/>
</div>
);
}
}
export default MemberList;

View File

@@ -0,0 +1,58 @@
@import '../../../styles/mixin.scss';
.m-panel{
background-color: #fff;
padding: 24px;
min-height: 4.68rem;
margin-top: 0;
// box-shadow: $box-shadow-panel;
}
.m-tab {
overflow: inherit !important;
}
.btn-container {
text-align: right;
}
.modal-input {
display: flex;
align-items: center;
margin-bottom: .24rem;
.label {
text-align: right;
}
.select {
width: 1.2rem;
}
}
.member-opration {
text-align: right;
.select {
width: 1.2rem;
}
.btn-danger {
margin-left: .08rem;
// background-color: transparent;
border-color: transparent;
}
}
.m-user {
display: flex;
align-items: center;
.m-user-img {
width: .32rem;
height: .32rem;
border-radius: .04rem;
}
.m-user-name {
margin-left: 8px;
}
}
.usernamelabel,.usernameauth{
line-height: 36px;
}

View File

@@ -0,0 +1,217 @@
import React, { PureComponent as Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Row, Col, Button, Tooltip } from 'antd';
import { Link } from 'react-router-dom';
import {
addProject,
fetchProjectList,
delProject,
changeUpdateModal
} from '../../../reducer/modules/project';
import ProjectCard from '../../../components/ProjectCard/ProjectCard.js';
import ErrMsg from '../../../components/ErrMsg/ErrMsg.js';
import { autobind } from 'core-decorators';
import { setBreadcrumb } from '../../../reducer/modules/user';
import './ProjectList.scss';
@connect(
state => {
return {
projectList: state.project.projectList,
userInfo: state.project.userInfo,
tableLoading: state.project.tableLoading,
currGroup: state.group.currGroup,
currPage: state.project.currPage
};
},
{
fetchProjectList,
addProject,
delProject,
changeUpdateModal,
setBreadcrumb
}
)
class ProjectList extends Component {
constructor(props) {
super(props);
this.state = {
visible: false,
protocol: 'http://',
projectData: []
};
}
static propTypes = {
form: PropTypes.object,
fetchProjectList: PropTypes.func,
addProject: PropTypes.func,
delProject: PropTypes.func,
changeUpdateModal: PropTypes.func,
projectList: PropTypes.array,
userInfo: PropTypes.object,
tableLoading: PropTypes.bool,
currGroup: PropTypes.object,
setBreadcrumb: PropTypes.func,
currPage: PropTypes.number,
studyTip: PropTypes.number,
study: PropTypes.bool
};
// 取消修改
@autobind
handleCancel() {
this.props.form.resetFields();
this.setState({
visible: false
});
}
// 修改线上域名的协议类型 (http/https)
@autobind
protocolChange(value) {
this.setState({
protocol: value
});
}
// 获取 ProjectCard 组件的关注事件回调,收到后更新数据
receiveRes = () => {
this.props.fetchProjectList(this.props.currGroup._id, this.props.currPage);
};
UNSAFE_componentWillReceiveProps(nextProps) {
this.props.setBreadcrumb([{ name: '' + (nextProps.currGroup.group_name || '') }]);
// 切换分组
if (this.props.currGroup !== nextProps.currGroup && nextProps.currGroup._id) {
this.props.fetchProjectList(nextProps.currGroup._id, this.props.currPage);
}
// 切换项目列表
if (this.props.projectList !== nextProps.projectList) {
// console.log(nextProps.projectList);
const data = nextProps.projectList.map((item, index) => {
item.key = index;
return item;
});
this.setState({
projectData: data
});
}
}
render() {
let projectData = this.state.projectData;
let noFollow = [];
let followProject = [];
for (var i in projectData) {
if (projectData[i].follow) {
followProject.push(projectData[i]);
} else {
noFollow.push(projectData[i]);
}
}
followProject = followProject.sort((a, b) => {
return b.up_time - a.up_time;
});
noFollow = noFollow.sort((a, b) => {
return b.up_time - a.up_time;
});
projectData = [...followProject, ...noFollow];
const isShow = /(admin)|(owner)|(dev)/.test(this.props.currGroup.role);
const Follow = () => {
return followProject.length ? (
<Row>
<h3 className="owner-type">我的关注</h3>
{followProject.map((item, index) => {
return (
<Col xs={8} lg={6} xxl={4} key={index}>
<ProjectCard projectData={item} callbackResult={this.receiveRes} />
</Col>
);
})}
</Row>
) : null;
};
const NoFollow = () => {
return noFollow.length ? (
<Row style={{ borderBottom: '1px solid #eee', marginBottom: '15px' }}>
<h3 className="owner-type">我的项目</h3>
{noFollow.map((item, index) => {
return (
<Col xs={8} lg={6} xxl={4} key={index}>
<ProjectCard projectData={item} callbackResult={this.receiveRes} isShow={isShow} />
</Col>
);
})}
</Row>
) : null;
};
const OwnerSpace = () => {
return projectData.length ? (
<div>
<NoFollow />
<Follow />
</div>
) : (
<ErrMsg type="noProject" />
);
};
return (
<div style={{ paddingTop: '24px' }} className="m-panel card-panel card-panel-s project-list">
<Row className="project-list-header">
<Col span={16} style={{ textAlign: 'left' }}>
{this.props.currGroup.group_name} 分组共 ({projectData.length}) 个项目
</Col>
<Col span={8}>
{isShow ? (
<Link to="/add-project">
<Button type="primary">添加项目</Button>
</Link>
) : (
<Tooltip title="您没有权限,请联系该分组组长或管理员">
<Button type="primary" disabled>
添加项目
</Button>
</Tooltip>
)}
</Col>
</Row>
<Row>
{/* {projectData.length ? projectData.map((item, index) => {
return (
<Col xs={8} md={6} xl={4} key={index}>
<ProjectCard projectData={item} callbackResult={this.receiveRes} />
</Col>);
}) : <ErrMsg type="noProject" />} */}
{this.props.currGroup.type === 'private' ? (
<OwnerSpace />
) : projectData.length ? (
projectData.map((item, index) => {
return (
<Col xs={8} lg={6} xxl={4} key={index}>
<ProjectCard
projectData={item}
callbackResult={this.receiveRes}
isShow={isShow}
/>
</Col>
);
})
) : (
<ErrMsg type="noProject" />
)}
</Row>
</div>
);
}
}
export default ProjectList;

View File

@@ -0,0 +1,62 @@
.ant-tabs-bar {
border-bottom: 1px solid transparent;
margin-bottom: 0;
}
.m-panel{
background-color: #fff;
padding: 24px;
min-height: 4.68rem;
margin-top: 0;
}
.project-list{
.project-list-header{
background: #eee;
height: 64px;
line-height: 40px;
border-radius: 4px;
text-align: right;
padding: 0 10px;
font-weight: bold;
margin-bottom: 15px;
display: flex;
align-items: center;
color: rgba(39, 56, 72, 0.85);
font-weight: 500;
}
.owner-type{
// padding: 10px;
font-size: 15px;
// background-color: #eee;
// border-radius: 4px;
// margin-bottom: 15px;
font-weight: 400;
margin-bottom: 0.16rem;
border-left: 3px solid #2395f1;
padding-left: 8px;
}
}
.ant-input-group-wrapper {
width: 100%;
}
.dynamic-delete-button {
cursor: pointer;
position: relative;
top: 4px;
font-size: 24px;
color: #999;
transition: all .3s;
}
.dynamic-delete-button:hover {
color: #777;
}
.dynamic-delete-button[disabled] {
cursor: not-allowed;
opacity: 0.5;
}

View File

@@ -0,0 +1,387 @@
import React, { PureComponent as Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Modal, Form, Input, Icon, Tooltip, Select, message, Button, Row, Col } from 'antd';
import {
updateProject,
fetchProjectList,
delProject,
changeUpdateModal,
changeTableLoading
} from '../../../reducer/modules/project';
const { TextArea } = Input;
const FormItem = Form.Item;
const Option = Select.Option;
import './ProjectList.scss';
// layout
const formItemLayout = {
labelCol: {
xs: { span: 24 },
sm: { span: 6 }
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 14 }
}
};
const formItemLayoutWithOutLabel = {
wrapperCol: {
xs: { span: 24, offset: 0 },
sm: { span: 20, offset: 6 }
}
};
let uuid = 0;
@connect(
state => {
return {
projectList: state.project.projectList,
isUpdateModalShow: state.project.isUpdateModalShow,
handleUpdateIndex: state.project.handleUpdateIndex,
tableLoading: state.project.tableLoading,
currGroup: state.group.currGroup
};
},
{
fetchProjectList,
updateProject,
delProject,
changeUpdateModal,
changeTableLoading
}
)
class UpDateModal extends Component {
constructor(props) {
super(props);
this.state = {
protocol: 'http://',
envProtocolChange: 'http://'
};
}
static propTypes = {
form: PropTypes.object,
fetchProjectList: PropTypes.func,
updateProject: PropTypes.func,
delProject: PropTypes.func,
changeUpdateModal: PropTypes.func,
changeTableLoading: PropTypes.func,
projectList: PropTypes.array,
currGroup: PropTypes.object,
isUpdateModalShow: PropTypes.bool,
handleUpdateIndex: PropTypes.number
};
// 修改线上域名的协议类型 (http/https)
protocolChange = value => {
this.setState({
protocol: value
});
};
handleCancel = () => {
this.props.form.resetFields();
this.props.changeUpdateModal(false, -1);
};
// 确认修改
handleOk = e => {
e.preventDefault();
const {
form,
updateProject,
changeUpdateModal,
currGroup,
projectList,
handleUpdateIndex,
fetchProjectList,
changeTableLoading
} = this.props;
form.validateFields((err, values) => {
if (!err) {
// console.log(projectList[handleUpdateIndex]);
let assignValue = Object.assign(projectList[handleUpdateIndex], values);
values.protocol = this.state.protocol.split(':')[0];
assignValue.env = assignValue.envs.map((item, index) => {
return {
name: values['envs-name-' + index],
domain: values['envs-protocol-' + index] + values['envs-domain-' + index]
};
});
// console.log(assignValue);
changeTableLoading(true);
updateProject(assignValue)
.then(res => {
if (res.payload.data.errcode == 0) {
changeUpdateModal(false, -1);
message.success('修改成功! ');
fetchProjectList(currGroup._id).then(() => {
changeTableLoading(false);
});
} else {
changeTableLoading(false);
message.error(res.payload.data.errmsg);
}
})
.catch(() => {
changeTableLoading(false);
});
form.resetFields();
}
});
};
// 项目的修改操作 - 删除一项环境配置
remove = id => {
const { form } = this.props;
// can use data-binding to get
const envs = form.getFieldValue('envs');
// We need at least one passenger
if (envs.length === 0) {
return;
}
// can use data-binding to set
form.setFieldsValue({
envs: envs.filter(key => {
const realKey = key._id ? key._id : key;
return realKey !== id;
})
});
};
// 项目的修改操作 - 添加一项环境配置
add = () => {
uuid++;
const { form } = this.props;
// can use data-binding to get
const envs = form.getFieldValue('envs');
const nextKeys = envs.concat(uuid);
// can use data-binding to set
// important! notify form to detect changes
form.setFieldsValue({
envs: nextKeys
});
};
render() {
const { getFieldDecorator, getFieldValue } = this.props.form;
// const that = this;
const { isUpdateModalShow, projectList, handleUpdateIndex } = this.props;
let initFormValues = {};
let envMessage = [];
// 如果列表存在且用户点击修改按钮时,设置表单默认值
if (projectList.length !== 0 && handleUpdateIndex !== -1) {
// console.log(projectList[handleUpdateIndex]);
const { name, basepath, desc, env } = projectList[handleUpdateIndex];
initFormValues = { name, basepath, desc, env };
if (env.length !== 0) {
envMessage = env;
}
initFormValues.prd_host = projectList[handleUpdateIndex].prd_host;
initFormValues.prd_protocol = projectList[handleUpdateIndex].protocol + '://';
}
getFieldDecorator('envs', { initialValue: envMessage });
const envs = getFieldValue('envs');
const formItems = envs.map((k, index) => {
const secondIndex = 'next' + index; // 为保证key的唯一性
return (
<Row key={index} type="flex" justify="space-between" align={index === 0 ? 'middle' : 'top'}>
<Col span={10} offset={2}>
<FormItem label={index === 0 ? <span>环境名称</span> : ''} required={false} key={index}>
{getFieldDecorator(`envs-name-${index}`, {
validateTrigger: ['onChange', 'onBlur'],
initialValue: envMessage.length !== 0 ? k.name : '',
rules: [
{
required: false,
whitespace: true,
validator(rule, value, callback) {
if (value) {
if (value.length === 0) {
callback('请输入环境域名');
} else if (!/\S/.test(value)) {
callback('请输入环境域名');
} else if (/prd/.test(value)) {
callback('环境域名不能是"prd"');
} else {
return callback();
}
} else {
callback('请输入环境域名');
}
}
}
]
})(<Input placeholder="请输入环境名称" style={{ width: '90%', marginRight: 8 }} />)}
</FormItem>
</Col>
<Col span={10}>
<FormItem
label={index === 0 ? <span>环境域名</span> : ''}
required={false}
key={secondIndex}
>
{getFieldDecorator(`envs-domain-${index}`, {
validateTrigger: ['onChange', 'onBlur'],
initialValue: envMessage.length !== 0 && k.domain ? k.domain.split('//')[1] : '',
rules: [
{
required: false,
whitespace: true,
message: '请输入环境域名',
validator(rule, value, callback) {
if (value) {
if (value.length === 0) {
callback('请输入环境域名');
} else if (!/\S/.test(value)) {
callback('请输入环境域名');
} else {
return callback();
}
} else {
callback('请输入环境域名');
}
}
}
]
})(
<Input
placeholder="请输入环境域名"
style={{ width: '90%', marginRight: 8 }}
addonBefore={getFieldDecorator(`envs-protocol-${index}`, {
initialValue:
envMessage.length !== 0 && k.domain
? k.domain.split('//')[0] + '//'
: 'http://',
rules: [
{
required: true
}
]
})(
<Select>
<Option value="http://">{'http://'}</Option>
<Option value="https://">{'https://'}</Option>
</Select>
)}
/>
)}
</FormItem>
</Col>
<Col span={2}>
{/* 新增的项中,只有最后一项有删除按钮 */}
{(envs.length > 0 && k._id) || envs.length == index + 1 ? (
<Icon
className="dynamic-delete-button"
type="minus-circle-o"
onClick={() => {
return this.remove(k._id ? k._id : k);
}}
/>
) : null}
</Col>
</Row>
);
});
return (
<Modal
title="修改项目"
visible={isUpdateModalShow}
onOk={this.handleOk}
onCancel={this.handleCancel}
>
<Form>
<FormItem {...formItemLayout} label="项目名称">
{getFieldDecorator('name', {
initialValue: initFormValues.name,
rules: [
{
required: true,
message: '请输入项目名称!'
}
]
})(<Input />)}
</FormItem>
<FormItem
{...formItemLayout}
label={
<span>
线上域名&nbsp;
<Tooltip title="将根据配置的线上域名访问mock数据">
<Icon type="question-circle-o" />
</Tooltip>
</span>
}
>
{getFieldDecorator('prd_host', {
initialValue: initFormValues.prd_host,
rules: [
{
required: true,
message: '请输入项目线上域名!'
}
]
})(
<Input
addonBefore={
<Select defaultValue={initFormValues.prd_protocol} onChange={this.protocolChange}>
<Option value="http://">{'http://'}</Option>
<Option value="https://">{'https://'}</Option>
</Select>
}
/>
)}
</FormItem>
<FormItem
{...formItemLayout}
label={
<span>
基本路径&nbsp;
<Tooltip title="基本路径为空表示根路径">
<Icon type="question-circle-o" />
</Tooltip>
</span>
}
>
{getFieldDecorator('basepath', {
initialValue: initFormValues.basepath,
rules: [
{
required: false,
message: '请输入项目基本路径! '
}
]
})(<Input />)}
</FormItem>
<FormItem {...formItemLayout} label="描述">
{getFieldDecorator('desc', {
initialValue: initFormValues.desc,
rules: [
{
required: false,
message: '请输入描述!'
}
]
})(<TextArea rows={4} />)}
</FormItem>
{formItems}
<FormItem {...formItemLayoutWithOutLabel}>
<Button type="dashed" onClick={this.add} style={{ width: '60%' }}>
<Icon type="plus" /> 添加环境配置
</Button>
</FormItem>
</Form>
</Modal>
);
}
}
export default Form.create()(UpDateModal);