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,524 @@
import React, { PureComponent as Component } from 'react';
import {
Upload,
Icon,
message,
Select,
Tooltip,
Button,
Spin,
Switch,
Modal,
Radio,
Input,
Checkbox
} from 'antd';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import './ProjectData.scss';
import axios from 'axios';
import URL from 'url';
const Dragger = Upload.Dragger;
import { saveImportData } from '../../../../reducer/modules/interface';
import { fetchUpdateLogData } from '../../../../reducer/modules/news.js';
import { handleSwaggerUrlData } from '../../../../reducer/modules/project';
const Option = Select.Option;
const confirm = Modal.confirm;
const plugin = require('client/plugin.js');
const RadioGroup = Radio.Group;
const importDataModule = {};
const exportDataModule = {};
const HandleImportData = require('common/HandleImportData');
function handleExportRouteParams(url, status, isWiki) {
if (!url) {
return;
}
let urlObj = URL.parse(url, true),
query = {};
query = Object.assign(query, urlObj.query, { status, isWiki });
return URL.format({
pathname: urlObj.pathname,
query
});
}
// exportDataModule.pdf = {
// name: 'Pdf',
// route: '/api/interface/download_crx',
// desc: '导出项目接口文档为 pdf 文件'
// }
@connect(
state => {
return {
curCatid: -(-state.inter.curdata.catid),
basePath: state.project.currProject.basepath,
updateLogList: state.news.updateLogList,
swaggerUrlData: state.project.swaggerUrlData
};
},
{
saveImportData,
fetchUpdateLogData,
handleSwaggerUrlData
}
)
class ProjectData extends Component {
constructor(props) {
super(props);
this.state = {
selectCatid: '',
menuList: [],
curImportType: 'swagger',
curExportType: null,
showLoading: false,
dataSync: 'merge',
exportContent: 'all',
isSwaggerUrl: false,
swaggerUrl: '',
isWiki: false
};
}
static propTypes = {
match: PropTypes.object,
curCatid: PropTypes.number,
basePath: PropTypes.string,
saveImportData: PropTypes.func,
fetchUpdateLogData: PropTypes.func,
updateLogList: PropTypes.array,
handleSwaggerUrlData: PropTypes.func,
swaggerUrlData: PropTypes.string
};
UNSAFE_componentWillMount() {
axios.get(`/api/interface/getCatMenu?project_id=${this.props.match.params.id}`).then(data => {
if (data.data.errcode === 0) {
let menuList = data.data.data;
this.setState({
menuList: menuList,
selectCatid: menuList[0]._id
});
}
});
plugin.emitHook('import_data', importDataModule);
plugin.emitHook('export_data', exportDataModule, this.props.match.params.id);
}
selectChange(value) {
this.setState({
selectCatid: +value
});
}
uploadChange = info => {
const status = info.file.status;
if (status !== 'uploading') {
console.log(info.file, info.fileList);
}
if (status === 'done') {
message.success(`${info.file.name} 文件上传成功`);
} else if (status === 'error') {
message.error(`${info.file.name} 文件上传失败`);
}
};
handleAddInterface = async res => {
return await HandleImportData(
res,
this.props.match.params.id,
this.state.selectCatid,
this.state.menuList,
this.props.basePath,
this.state.dataSync,
message.error,
message.success,
() => this.setState({ showLoading: false })
);
};
// 本地文件上传
handleFile = info => {
if (!this.state.curImportType) {
return message.error('请选择导入数据的方式');
}
if (this.state.selectCatid) {
this.setState({ showLoading: true });
let reader = new FileReader();
reader.readAsText(info.file);
reader.onload = async res => {
res = await importDataModule[this.state.curImportType].run(res.target.result);
if (this.state.dataSync === 'merge') {
// 开启同步
this.showConfirm(res);
} else {
// 未开启同步
await this.handleAddInterface(res);
}
};
} else {
message.error('请选择上传的默认分类');
}
};
showConfirm = async res => {
let that = this;
let typeid = this.props.match.params.id;
let apiCollections = res.apis.map(item => {
return {
method: item.method,
path: item.path
};
});
let result = await this.props.fetchUpdateLogData({
type: 'project',
typeid,
apis: apiCollections
});
let domainData = result.payload.data.data;
const ref = confirm({
title: '您确认要进行数据同步????',
width: 600,
okType: 'danger',
iconType: 'exclamation-circle',
className: 'dataImport-confirm',
okText: '确认',
cancelText: '取消',
content: (
<div className="postman-dataImport-modal">
<div className="postman-dataImport-modal-content">
{domainData.map((item, index) => {
return (
<div key={index} className="postman-dataImport-show-diff">
<span className="logcontent" dangerouslySetInnerHTML={{ __html: item.content }} />
</div>
);
})}
</div>
<p className="info">温馨提示 数据同步后可能会造成原本的修改数据丢失</p>
</div>
),
async onOk() {
await that.handleAddInterface(res);
},
onCancel() {
that.setState({ showLoading: false, dataSync: 'normal' });
ref.destroy();
}
});
};
handleImportType = val => {
this.setState({
curImportType: val,
isSwaggerUrl: false
});
};
handleExportType = val => {
this.setState({
curExportType: val,
isWiki: false
});
};
// 处理导入信息同步
onChange = checked => {
this.setState({
dataSync: checked
});
};
// 处理swagger URL 导入
handleUrlChange = checked => {
this.setState({
isSwaggerUrl: checked
});
};
// 记录输入的url
swaggerUrlInput = url => {
this.setState({
swaggerUrl: url
});
};
// url导入上传
onUrlUpload = async () => {
if (!this.state.curImportType) {
return message.error('请选择导入数据的方式');
}
if (!this.state.swaggerUrl) {
return message.error('url 不能为空');
}
if (this.state.selectCatid) {
this.setState({ showLoading: true });
try {
// 处理swagger url 导入
await this.props.handleSwaggerUrlData(this.state.swaggerUrl);
// let result = json5_parse(this.props.swaggerUrlData)
let res = await importDataModule[this.state.curImportType].run(this.props.swaggerUrlData);
if (this.state.dataSync === 'merge') {
// merge
this.showConfirm(res);
} else {
// 未开启同步
await this.handleAddInterface(res);
}
} catch (e) {
this.setState({ showLoading: false });
message.error(e.message);
}
} else {
message.error('请选择上传的默认分类');
}
};
// 处理导出接口是全部还是公开
handleChange = e => {
this.setState({ exportContent: e.target.value });
};
// 处理是否开启wiki导出
handleWikiChange = e => {
this.setState({
isWiki: e.target.checked
});
};
/**
*
*
* @returns
* @memberof ProjectData
*/
render() {
const uploadMess = {
name: 'interfaceData',
multiple: true,
showUploadList: false,
action: '/api/interface/interUpload',
customRequest: this.handleFile,
onChange: this.uploadChange
};
let exportUrl =
this.state.curExportType &&
exportDataModule[this.state.curExportType] &&
exportDataModule[this.state.curExportType].route;
let exportHref = handleExportRouteParams(
exportUrl,
this.state.exportContent,
this.state.isWiki
);
// console.log('inter', this.state.exportContent);
return (
<div className="g-row">
<div className="m-panel">
<div className="postman-dataImport">
<div className="dataImportCon">
<div>
<h3>
数据导入&nbsp;
<a
target="_blank"
rel="noopener noreferrer"
href="https://hellosean1025.github.io/yapi/documents/data.html"
>
<Tooltip title="点击查看文档">
<Icon type="question-circle-o" />
</Tooltip>
</a>
</h3>
</div>
<div className="dataImportTile">
<Select
placeholder="请选择导入数据的方式"
value={this.state.curImportType}
onChange={this.handleImportType}
>
{Object.keys(importDataModule).map(name => {
return (
<Option key={name} value={name}>
{importDataModule[name].name}
</Option>
);
})}
</Select>
</div>
<div className="catidSelect">
<Select
value={this.state.selectCatid + ''}
showSearch
style={{ width: '100%' }}
placeholder="请选择数据导入的默认分类"
optionFilterProp="children"
onChange={this.selectChange.bind(this)}
filterOption={(input, option) =>
option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
>
{this.state.menuList.map((item, key) => {
return (
<Option key={key} value={item._id + ''}>
{item.name}
</Option>
);
})}
</Select>
</div>
<div className="dataSync">
<span className="label">
数据同步&nbsp;
<Tooltip
title={
<div>
<h3 style={{ color: 'white' }}>普通模式</h3>
<p>不导入已存在的接口</p>
<br />
<h3 style={{ color: 'white' }}>智能合并</h3>
<p>
已存在的接口将合并返回数据的 response适用于导入了 swagger
数据保留对数据结构的改动
</p>
<br />
<h3 style={{ color: 'white' }}>完全覆盖</h3>
<p>不保留旧数据完全使用新数据适用于接口定义完全交给后端定义</p>
</div>
}
>
<Icon type="question-circle-o" />
</Tooltip>{' '}
</span>
<Select value={this.state.dataSync} onChange={this.onChange}>
<Option value="normal">普通模式</Option>
<Option value="good">智能合并</Option>
<Option value="merge">完全覆盖</Option>
</Select>
{/* <Switch checked={this.state.dataSync} onChange={this.onChange} /> */}
</div>
{this.state.curImportType === 'swagger' && (
<div className="dataSync">
<span className="label">
开启url导入&nbsp;
<Tooltip title="swagger url 导入">
<Icon type="question-circle-o" />
</Tooltip>{' '}
&nbsp;&nbsp;
</span>
<Switch checked={this.state.isSwaggerUrl} onChange={this.handleUrlChange} />
</div>
)}
{this.state.isSwaggerUrl ? (
<div className="import-content url-import-content">
<Input
placeholder="http://demo.swagger.io/v2/swagger.json"
onChange={e => this.swaggerUrlInput(e.target.value)}
/>
<Button
type="primary"
className="url-btn"
onClick={this.onUrlUpload}
loading={this.state.showLoading}
>
上传
</Button>
</div>
) : (
<div className="import-content">
<Spin spinning={this.state.showLoading} tip="上传中...">
<Dragger {...uploadMess}>
<p className="ant-upload-drag-icon">
<Icon type="inbox" />
</p>
<p className="ant-upload-text">点击或者拖拽文件到上传区域</p>
<p
className="ant-upload-hint"
onClick={e => {
e.stopPropagation();
}}
dangerouslySetInnerHTML={{
__html: this.state.curImportType
? importDataModule[this.state.curImportType].desc
: null
}}
/>
</Dragger>
</Spin>
</div>
)}
</div>
<div
className="dataImportCon"
style={{
marginLeft: '20px',
display: Object.keys(exportDataModule).length > 0 ? '' : 'none'
}}
>
<div>
<h3>数据导出</h3>
</div>
<div className="dataImportTile">
<Select placeholder="请选择导出数据的方式" onChange={this.handleExportType}>
{Object.keys(exportDataModule).map(name => {
return (
<Option key={name} value={name}>
{exportDataModule[name].name}
</Option>
);
})}
</Select>
</div>
<div className="dataExport">
<RadioGroup defaultValue="all" onChange={this.handleChange}>
<Radio value="all">全部接口</Radio>
<Radio value="open">公开接口</Radio>
</RadioGroup>
</div>
<div className="export-content">
{this.state.curExportType ? (
<div>
<p className="export-desc">{exportDataModule[this.state.curExportType].desc}</p>
<a
target="_blank"
rel="noopener noreferrer"
href={exportHref}>
<Button className="export-button" type="primary" size="large">
{' '}
导出{' '}
</Button>
</a>
<Checkbox
checked={this.state.isWiki}
onChange={this.handleWikiChange}
className="wiki-btn"
disabled={this.state.curExportType === 'json'}
>
添加wiki&nbsp;
<Tooltip title="开启后 html 和 markdown 数据导出会带上wiki数据">
<Icon type="question-circle-o" />
</Tooltip>{' '}
</Checkbox>
</div>
) : (
<Button disabled className="export-button" type="primary" size="large">
{' '}
导出{' '}
</Button>
)}
</div>
</div>
</div>
</div>
</div>
);
}
}
export default ProjectData;

View File

@@ -0,0 +1,103 @@
.postman-dataImport{
display: flex;
.dataImportCon{
min-width: 304px;
background-color: #ececec;
padding: 16px;
border-radius: 4px;
.ant-upload-drag{
padding: 16px;
background-color: white;
}
.dataImportTile{
color: #2395f1;
padding: 16px 0px;
font-weight: 500;
width: 100%;
.ant-select{
width: 100%;
}
}
.dataExport{
padding-bottom: 16px;
font-weight: 500;
width: 100%;
}
.dataSync{
padding-top: 16px;
font-weight: 500;
width: 100%;
.label{
padding-right: 8px;
width: 150px;
display: inline-block;
}
.label:after {
content: ":";
margin: 0 8px 0 2px;
position: relative;
top: -.5px;
}
}
.import-content {
margin-top: 16px;
height: 180px;
}
.url-import-content {
text-align: center;
.url-btn{
margin-top: 16px;
}
}
.export-content{
text-align: center;
}
.export-desc{
padding-bottom: 15px;
}
.export-button{
width: 100px;
height:35px;
}
.wiki-btn {
margin-left: 8px;
}
}
}
.postman-dataImport-modal{
.postman-dataImport-modal-content{
max-height: 600px;
min-width: 534px;
overflow-y: scroll;
padding-top: 8px;
}
.postman-dataImport-show-diff{
padding: 4px 0;
}
.info{
font-weight: 400;
font-size: 16px;
padding-top: 24px;
}
}
.dataImport-confirm{
.ant-modal-content .ant-confirm-btns{
margin-top: 10px;
}
}

View File

@@ -0,0 +1,382 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import './index.scss';
import { Icon, Row, Col, Form, Input, Select, Button, AutoComplete, Tooltip } from 'antd';
const FormItem = Form.Item;
const Option = Select.Option;
import constants from 'client/constants/variable.js';
const initMap = {
header: [
{
name: '',
value: ''
}
],
cookie: [
{
name: '',
value: ''
}
],
global: [
{
name: '',
value: ''
}
]
};
class ProjectEnvContent extends Component {
static propTypes = {
projectMsg: PropTypes.object,
form: PropTypes.object,
onSubmit: PropTypes.func,
handleEnvInput: PropTypes.func
};
initState(curdata) {
let header = [
{
name: '',
value: ''
}
];
let cookie = [
{
name: '',
value: ''
}
];
let global = [
{
name: '',
value: ''
}
];
const curheader = curdata.header;
const curGlobal = curdata.global;
if (curheader && curheader.length !== 0) {
curheader.forEach(item => {
if (item.name === 'Cookie') {
let cookieStr = item.value;
if (cookieStr) {
cookieStr = cookieStr.split(';').forEach(c => {
if (c) {
c = c.split('=');
cookie.unshift({
name: c[0] ? c[0].trim() : '',
value: c[1] ? c[1].trim() : ''
});
}
});
}
} else {
header.unshift(item);
}
});
}
if (curGlobal && curGlobal.length !== 0) {
curGlobal.forEach(item => {
global.unshift(item);
});
}
return { header, cookie, global };
}
constructor(props) {
super(props);
this.state = Object.assign({}, initMap);
}
addHeader = (value, index, name) => {
let nextHeader = this.state[name][index + 1];
if (nextHeader && typeof nextHeader === 'object') {
return;
}
let newValue = {};
let data = { name: '', value: '' };
newValue[name] = [].concat(this.state[name], data);
this.setState(newValue);
};
delHeader = (key, name) => {
let curValue = this.props.form.getFieldValue(name);
let newValue = {};
newValue[name] = curValue.filter((val, index) => {
return index !== key;
});
this.props.form.setFieldsValue(newValue);
this.setState(newValue);
};
handleInit(data) {
this.props.form.resetFields();
let newValue = this.initState(data);
this.setState({ ...newValue });
}
UNSAFE_componentWillReceiveProps(nextProps) {
let curEnvName = this.props.projectMsg.name;
let nextEnvName = nextProps.projectMsg.name;
if (curEnvName !== nextEnvName) {
this.handleInit(nextProps.projectMsg);
}
}
handleOk = e => {
e.preventDefault();
const { form, onSubmit, projectMsg } = this.props;
form.validateFields((err, values) => {
if (!err) {
let header = values.header.filter(val => {
return val.name !== '';
});
let cookie = values.cookie.filter(val => {
return val.name !== '';
});
let global = values.global.filter(val => {
return val.name !== '';
});
if (cookie.length > 0) {
header.push({
name: 'Cookie',
value: cookie.map(item => item.name + '=' + item.value).join(';')
});
}
let assignValue = {};
assignValue.env = Object.assign(
{ _id: projectMsg._id },
{
name: values.env.name,
domain: values.env.protocol + values.env.domain,
header: header,
global
}
);
onSubmit(assignValue);
}
});
};
render() {
const { projectMsg } = this.props;
const { getFieldDecorator } = this.props.form;
const headerTpl = (item, index) => {
const headerLength = this.state.header.length - 1;
return (
<Row gutter={2} key={index}>
<Col span={10}>
<FormItem>
{getFieldDecorator('header[' + index + '].name', {
validateTrigger: ['onChange', 'onBlur'],
initialValue: item.name || ''
})(
<AutoComplete
style={{ width: '200px' }}
allowClear={true}
dataSource={constants.HTTP_REQUEST_HEADER}
placeholder="请输入header名称"
onChange={() => this.addHeader(item, index, 'header')}
filterOption={(inputValue, option) =>
option.props.children.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1
}
/>
)}
</FormItem>
</Col>
<Col span={12}>
<FormItem>
{getFieldDecorator('header[' + index + '].value', {
validateTrigger: ['onChange', 'onBlur'],
initialValue: item.value || ''
})(<Input placeholder="请输入参数内容" style={{ width: '90%', marginRight: 8 }} />)}
</FormItem>
</Col>
<Col span={2} className={index === headerLength ? ' env-last-row' : null}>
{/* 新增的项中,只有最后一项没有有删除按钮 */}
<Icon
className="dynamic-delete-button delete"
type="delete"
onClick={e => {
e.stopPropagation();
this.delHeader(index, 'header');
}}
/>
</Col>
</Row>
);
};
const commonTpl = (item, index, name) => {
const length = this.state[name].length - 1;
return (
<Row gutter={2} key={index}>
<Col span={10}>
<FormItem>
{getFieldDecorator(`${name}[${index}].name`, {
validateTrigger: ['onChange', 'onBlur'],
initialValue: item.name || ''
})(
<Input
placeholder={`请输入 ${name} Name`}
style={{ width: '200px' }}
onChange={() => this.addHeader(item, index, name)}
/>
)}
</FormItem>
</Col>
<Col span={12}>
<FormItem>
{getFieldDecorator(`${name}[${index}].value`, {
validateTrigger: ['onChange', 'onBlur'],
initialValue: item.value || ''
})(<Input placeholder="请输入参数内容" style={{ width: '90%', marginRight: 8 }} />)}
</FormItem>
</Col>
<Col span={2} className={index === length ? ' env-last-row' : null}>
{/* 新增的项中,只有最后一项没有有删除按钮 */}
<Icon
className="dynamic-delete-button delete"
type="delete"
onClick={e => {
e.stopPropagation();
this.delHeader(index, name);
}}
/>
</Col>
</Row>
);
};
const envTpl = data => {
return (
<div>
<h3 className="env-label">环境名称</h3>
<FormItem required={false}>
{getFieldDecorator('env.name', {
validateTrigger: ['onChange', 'onBlur'],
initialValue: data.name === '新环境' ? '' : data.name || '',
rules: [
{
required: false,
whitespace: true,
validator(rule, value, callback) {
if (value) {
if (value.length === 0) {
callback('请输入环境名称');
} else if (!/\S/.test(value)) {
callback('请输入环境名称');
} else {
return callback();
}
} else {
callback('请输入环境名称');
}
}
}
]
})(
<Input
onChange={e => this.props.handleEnvInput(e.target.value)}
placeholder="请输入环境名称"
style={{ width: '90%', marginRight: 8 }}
/>
)}
</FormItem>
<h3 className="env-label">环境域名</h3>
<FormItem required={false}>
{getFieldDecorator('env.domain', {
validateTrigger: ['onChange', 'onBlur'],
initialValue: data.domain ? data.domain.split('//')[1] : '',
rules: [
{
required: false,
whitespace: true,
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('env.protocol', {
initialValue: data.domain ? data.domain.split('//')[0] + '//' : 'http://',
rules: [
{
required: true
}
]
})(
<Select>
<Option value="http://">{'http://'}</Option>
<Option value="https://">{'https://'}</Option>
</Select>
)}
/>
)}
</FormItem>
<h3 className="env-label">Header</h3>
{this.state.header.map((item, index) => {
return headerTpl(item, index);
})}
<h3 className="env-label">Cookie</h3>
{this.state.cookie.map((item, index) => {
return commonTpl(item, index, 'cookie');
})}
<h3 className="env-label">
global
<a
target="_blank"
rel="noopener noreferrer"
href="https://hellosean1025.github.io/yapi/documents/project.html#%E9%85%8D%E7%BD%AE%E7%8E%AF%E5%A2%83"
style={{ marginLeft: 8 }}
>
<Tooltip title="点击查看文档">
<Icon type="question-circle-o" style={{fontSize: '13px'}}/>
</Tooltip>
</a>
</h3>
{this.state.global.map((item, index) => {
return commonTpl(item, index, 'global');
})}
</div>
);
};
return (
<div>
{envTpl(projectMsg)}
<div className="btnwrap-changeproject">
<Button
className="m-btn btn-save"
icon="save"
type="primary"
size="large"
onClick={this.handleOk}
>
</Button>
</div>
</div>
);
}
}
export default Form.create()(ProjectEnvContent);

View File

@@ -0,0 +1,228 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import './index.scss';
import { Icon, Layout, Tooltip, message, Row, Popconfirm } from 'antd';
const { Content, Sider } = Layout;
import ProjectEnvContent from './ProjectEnvContent.js';
import { connect } from 'react-redux';
import { updateEnv, getProject, getEnv } from '../../../../reducer/modules/project';
import EasyDragSort from '../../../../components/EasyDragSort/EasyDragSort.js';
@connect(
state => {
return {
projectMsg: state.project.currProject
};
},
{
updateEnv,
getProject,
getEnv
}
)
class ProjectEnv extends Component {
static propTypes = {
projectId: PropTypes.number,
updateEnv: PropTypes.func,
getProject: PropTypes.func,
projectMsg: PropTypes.object,
onOk: PropTypes.func,
getEnv: PropTypes.func
};
constructor(props) {
super(props);
this.state = {
env: [],
_id: null,
currentEnvMsg: {},
delIcon: null,
currentKey: -2
};
}
initState(curdata, id) {
let newValue = {};
newValue['env'] = [].concat(curdata);
newValue['_id'] = id;
this.setState({
...this.state,
...newValue
});
}
async UNSAFE_componentWillMount() {
this._isMounted = true;
await this.props.getProject(this.props.projectId);
const { env, _id } = this.props.projectMsg;
this.initState(env, _id);
this.handleClick(0, env[0]);
}
componentWillUnmount() {
this._isMounted = false;
}
handleClick = (key, data) => {
this.setState({
currentEnvMsg: data,
currentKey: key
});
};
// 增加环境变量项
addParams = (name, data) => {
let newValue = {};
data = { name: '新环境', domain: '', header: [] };
newValue[name] = [].concat(data, this.state[name]);
this.setState(newValue);
this.handleClick(0, data);
};
// 删除提示信息
showConfirm(key, name) {
let assignValue = this.delParams(key, name);
this.onSave(assignValue);
}
// 删除环境变量项
delParams = (key, name) => {
let curValue = this.state.env;
let newValue = {};
newValue[name] = curValue.filter((val, index) => {
return index !== key;
});
this.setState(newValue);
this.handleClick(0, newValue[name][0]);
newValue['_id'] = this.state._id;
return newValue;
};
enterItem = key => {
this.setState({ delIcon: key });
};
// 保存设置
async onSave(assignValue) {
await this.props
.updateEnv(assignValue)
.then(res => {
if (res.payload.data.errcode == 0) {
this.props.getProject(this.props.projectId);
this.props.getEnv(this.props.projectId);
message.success('修改成功! ');
if(this._isMounted) {
this.setState({ ...assignValue });
}
}
})
.catch(() => {
message.error('环境设置不成功 ');
});
}
// 提交保存信息
onSubmit = (value, index) => {
let assignValue = {};
assignValue['env'] = [].concat(this.state.env);
assignValue['env'].splice(index, 1, value['env']);
assignValue['_id'] = this.state._id;
this.onSave(assignValue);
this.props.onOk && this.props.onOk(assignValue['env'], index);
};
// 动态修改环境名称
handleInputChange = (value, currentKey) => {
let newValue = [].concat(this.state.env);
newValue[currentKey].name = value || '新环境';
this.setState({ env: newValue });
};
// 侧边栏拖拽
handleDragMove = name => {
return (data, from, to) => {
let newValue = {
[name]: data
};
this.setState(newValue);
newValue['_id'] = this.state._id;
this.handleClick(to, newValue[name][to]);
this.onSave(newValue);
};
};
render() {
const { env, currentKey } = this.state;
const envSettingItems = env.map((item, index) => {
return (
<Row
key={index}
className={'menu-item ' + (index === currentKey ? 'menu-item-checked' : '')}
onClick={() => this.handleClick(index, item)}
onMouseEnter={() => this.enterItem(index)}
>
<span className="env-icon-style">
<span className="env-name" style={{ color: item.name === '新环境' && '#2395f1' }}>
{item.name}
</span>
<Popconfirm
title="您确认删除此环境变量?"
onConfirm={e => {
e.stopPropagation();
this.showConfirm(index, 'env');
}}
okText="确定"
cancelText="取消"
>
<Icon
type="delete"
className="interface-delete-icon"
style={{
display: this.state.delIcon == index && env.length - 1 !== 0 ? 'block' : 'none'
}}
/>
</Popconfirm>
</span>
</Row>
);
});
return (
<div className="m-env-panel">
<Layout className="project-env">
<Sider width={195} style={{ background: '#fff' }}>
<div style={{ height: '100%', borderRight: 0 }}>
<Row className="first-menu-item menu-item">
<div className="env-icon-style">
<h3>
环境列表&nbsp;<Tooltip placement="top" title="在这里添加项目的环境配置">
<Icon type="question-circle-o" />
</Tooltip>
</h3>
<Tooltip title="添加环境变量">
<Icon type="plus" onClick={() => this.addParams('env')} />
</Tooltip>
</div>
</Row>
<EasyDragSort data={() => env} onChange={this.handleDragMove('env')}>
{envSettingItems}
</EasyDragSort>
</div>
</Sider>
<Layout className="env-content">
<Content style={{ background: '#fff', padding: 24, margin: 0, minHeight: 280 }}>
<ProjectEnvContent
projectMsg={this.state.currentEnvMsg}
onSubmit={e => this.onSubmit(e, currentKey)}
handleEnvInput={e => this.handleInputChange(e, currentKey)}
/>
</Content>
</Layout>
</Layout>
</div>
);
}
}
export default ProjectEnv;

View File

@@ -0,0 +1,93 @@
.m-env-panel {
// padding-top: 8px;
min-height: 4.68rem;
margin-top: 0;
background-color: #fff;
}
.project-env{
min-height: 4.68rem;
.env-icon-style{
display: flex;
justify-content: space-between;
align-items: center;
.anticon{
font-size: 15px;
}
}
.menu-item{
padding: 0 16px;
font-size: 13px;
line-height: 42px;
height: 42px;
overflow: hidden;
text-overflow: ellipsis;
cursor: pointer;
}
.menu-item-checked{
background-color: #eef7fe;
color: #2395f1;
font-size: 14px;
margin-right: -1px;
border-right: 2px solid #2395f1;
}
.first-menu-item{
background-color: #eceef1;
}
.delete{
font-size: 20px;
top: 8px;
}
.env-content{
border-left: 1px solid #ccc;
}
.ant-menu-item-disabled{
cursor: pointer;
}
.m-empty-prompt{
display: flex;
height: 400px;
span{
margin: auto;
font-size: 16px;
.anticon {
padding-right: 16px;
font-size: 24px;
}
}
}
.env-label{
padding-bottom: 8px;
a {
color: #636363;
}
}
.env-last-row {
display: none;
}
.env-name{
width: 150px;
overflow: hidden;
text-overflow: ellipsis;
}
.btnwrap-changeproject {
text-align: center;
padding: .16rem 0;
background: #fff;
background-color: #fff;
margin: 0 -.4rem;
background-size: 4px 4px;
}
}

View File

@@ -0,0 +1,444 @@
import React, { PureComponent as Component } from 'react';
import {
Table,
Card,
Badge,
Select,
Button,
Modal,
Row,
Col,
message,
Popconfirm,
Switch,
Tooltip
} from 'antd';
import PropTypes from 'prop-types';
import { fetchGroupMsg } from '../../../../reducer/modules/group';
import { connect } from 'react-redux';
import ErrMsg from '../../../../components/ErrMsg/ErrMsg.js';
import { fetchGroupMemberList } from '../../../../reducer/modules/group.js';
import {
fetchProjectList,
getProjectMemberList,
getProject,
addMember,
delMember,
changeMemberRole,
changeMemberEmailNotice
} from '../../../../reducer/modules/project.js';
import UsernameAutoComplete from '../../../../components/UsernameAutoComplete/UsernameAutoComplete.js';
import '../Setting.scss';
const Option = Select.Option;
const arrayAddKey = arr => {
return arr.map((item, index) => {
return {
...item,
key: index
};
});
};
@connect(
state => {
return {
projectMsg: state.project.currProject,
uid: state.user.uid,
projectList: state.project.projectList
};
},
{
fetchGroupMemberList,
getProjectMemberList,
addMember,
delMember,
fetchGroupMsg,
changeMemberRole,
getProject,
fetchProjectList,
changeMemberEmailNotice
}
)
class ProjectMember extends Component {
constructor(props) {
super(props);
this.state = {
groupMemberList: [],
projectMemberList: [],
groupName: '',
role: '',
visible: false,
dataSource: [],
inputUids: [],
inputRole: 'dev',
modalVisible: false,
selectProjectId: 0
};
}
static propTypes = {
match: PropTypes.object,
projectId: PropTypes.number,
projectMsg: PropTypes.object,
uid: PropTypes.number,
addMember: PropTypes.func,
delMember: PropTypes.func,
changeMemberRole: PropTypes.func,
getProject: PropTypes.func,
fetchGroupMemberList: PropTypes.func,
fetchGroupMsg: PropTypes.func,
getProjectMemberList: PropTypes.func,
fetchProjectList: PropTypes.func,
projectList: PropTypes.array,
changeMemberEmailNotice: PropTypes.func
};
showAddMemberModal = () => {
this.setState({
visible: true
});
};
showImportMemberModal = async () => {
await this.props.fetchProjectList(this.props.projectMsg.group_id);
this.setState({
modalVisible: true
});
};
// 重新获取列表
reFetchList = () => {
this.props.getProjectMemberList(this.props.match.params.id).then(res => {
this.setState({
projectMemberList: arrayAddKey(res.payload.data.data),
visible: false,
modalVisible: false
});
});
};
handleOk = () => {
this.addMembers(this.state.inputUids);
};
// 增 - 添加成员
addMembers = memberUids => {
this.props
.addMember({
id: this.props.match.params.id,
member_uids: memberUids,
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.match.params.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.match.params.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(); // 添加成功后重新获取分组成员列表
}
});
};
// 修改用户是否接收消息通知
changeEmailNotice = async (notice, member_uid) => {
const id = this.props.match.params.id;
await this.props.changeMemberEmailNotice({ id, member_uid, notice });
this.reFetchList(); // 添加成功后重新获取项目成员列表
};
// 关闭模态框
handleCancel = () => {
this.setState({
visible: false
});
};
// 关闭批量导入模态框
handleModalCancel = () => {
this.setState({
modalVisible: false
});
};
// 处理选择项目
handleChange = key => {
this.setState({
selectProjectId: key
});
};
// 确定批量导入模态框
handleModalOk = async () => {
// 获取项目中的成员列表
const menberList = await this.props.getProjectMemberList(this.state.selectProjectId);
const memberUidList = menberList.payload.data.data.map(item => {
return item.uid;
});
this.addMembers(memberUidList);
};
onUserSelect = uids => {
this.setState({
inputUids: uids
});
};
async UNSAFE_componentWillMount() {
const groupMemberList = await this.props.fetchGroupMemberList(this.props.projectMsg.group_id);
const groupMsg = await this.props.fetchGroupMsg(this.props.projectMsg.group_id);
const projectMemberList = await this.props.getProjectMemberList(this.props.match.params.id);
this.setState({
groupMemberList: groupMemberList.payload.data.data,
groupName: groupMsg.payload.data.data.group_name,
projectMemberList: arrayAddKey(projectMemberList.payload.data.data),
role: this.props.projectMsg.role
});
}
render() {
const isEmailChangeEable = this.state.role === 'owner' || this.state.role === 'admin';
const columns = [
{
title:
this.props.projectMsg.name + ' 项目成员 (' + this.state.projectMemberList.length + ') 人',
dataIndex: 'username',
key: 'username',
render: (text, record) => {
return (
<div className="m-user">
<img src={'/api/user/avatar?uid=' + record.uid} className="m-user-img" />
<p className="m-user-name">{text}</p>
<Tooltip placement="top" title="消息通知">
<span>
<Switch
size="small"
checkedChildren="开"
unCheckedChildren="关"
checked={record.email_notice}
disabled={!(isEmailChangeEable || record.uid === this.props.uid)}
onChange={e => this.changeEmailNotice(e, record.uid)}
/>
</span>
</Tooltip>
</div>
);
}
},
{
title:
this.state.role === 'owner' || this.state.role === 'admin' ? (
<div className="btn-container">
<Button className="btn" type="primary" icon="plus" onClick={this.showAddMemberModal}>
添加成员
</Button>
<Button className="btn" icon="plus" onClick={this.showImportMemberModal}>
批量导入成员
</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" />
</Popconfirm>
</div>
);
} else {
// 非管理员可以看到权限 但无法修改
if (record.role === 'owner') {
return '组长';
} else if (record.role === 'dev') {
return '开发者';
} else if (record.role === 'guest') {
return '访客';
} else {
return '';
}
}
}
}
];
// 获取当前分组下的所有项目名称
const children = this.props.projectList.map((item, index) => (
<Option key={index} value={'' + item._id}>
{item.name}
</Option>
));
return (
<div className="g-row">
<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 usernamelabel">权限: </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>
) : (
''
)}
<Modal
title="批量导入成员"
visible={this.state.modalVisible}
onOk={this.handleModalOk}
onCancel={this.handleModalCancel}
>
<Row gutter={6} className="modal-input">
<Col span="5">
<div className="label usernamelabel">项目名: </div>
</Col>
<Col span="15">
<Select
showSearch
style={{ width: 200 }}
placeholder="请选择项目名称"
optionFilterProp="children"
onChange={this.handleChange}
>
{children}
</Select>
</Col>
</Row>
</Modal>
<Table
columns={columns}
dataSource={this.state.projectMemberList}
pagination={false}
locale={{ emptyText: <ErrMsg type="noMemberInProject" /> }}
className="setting-project-member"
/>
<Card
bordered={false}
title={
this.state.groupName + ' 分组成员 ' + '(' + this.state.groupMemberList.length + ') 人'
}
hoverable={true}
className="setting-group"
>
{this.state.groupMemberList.length ? (
this.state.groupMemberList.map((item, index) => {
return (
<div key={index} className="card-item">
<img
src={
location.protocol +
'//' +
location.host +
'/api/user/avatar?uid=' +
item.uid
}
className="item-img"
/>
<p className="item-name">
{item.username}
{item.uid === this.props.uid ? (
<Badge
count={'我'}
style={{
backgroundColor: '#689bd0',
fontSize: '13px',
marginLeft: '8px',
borderRadius: '4px'
}}
/>
) : null}
</p>
{item.role === 'owner' ? <p className="item-role">组长</p> : null}
{item.role === 'dev' ? <p className="item-role">开发者</p> : null}
{item.role === 'guest' ? <p className="item-role">访客</p> : null}
</div>
);
})
) : (
<ErrMsg type="noMemberInGroup" />
)}
</Card>
</div>
</div>
);
}
}
export default ProjectMember;

View File

@@ -0,0 +1,520 @@
import React, { PureComponent as Component } from 'react';
import {
Form,
Input,
Switch,
Select,
Icon,
Tooltip,
Button,
Row,
Col,
message,
Card,
Radio,
Alert,
Modal,
Popover
} from 'antd';
import PropTypes from 'prop-types';
import {
updateProject,
delProject,
getProject,
upsetProject
} from '../../../../reducer/modules/project';
import { fetchGroupMsg } from '../../../../reducer/modules/group';
import { fetchGroupList } from '../../../../reducer/modules/group.js';
import { setBreadcrumb } from '../../../../reducer/modules/user';
import { connect } from 'react-redux';
const { TextArea } = Input;
import { withRouter } from 'react-router';
const FormItem = Form.Item;
const RadioGroup = Radio.Group;
const RadioButton = Radio.Button;
import constants from '../../../../constants/variable.js';
const confirm = Modal.confirm;
import { nameLengthLimit, entries, trim, htmlFilter } from '../../../../common';
import '../Setting.scss';
import _ from 'underscore';
import ProjectTag from './ProjectTag.js';
// layout
const formItemLayout = {
labelCol: {
lg: { offset: 1, span: 3 },
xs: { span: 24 },
sm: { span: 6 }
},
wrapperCol: {
lg: { span: 19 },
xs: { span: 24 },
sm: { span: 14 }
},
className: 'form-item'
};
const Option = Select.Option;
@connect(
state => {
return {
projectList: state.project.projectList,
groupList: state.group.groupList,
projectMsg: state.project.currProject,
currGroup: state.group.currGroup
};
},
{
updateProject,
delProject,
getProject,
fetchGroupMsg,
upsetProject,
fetchGroupList,
setBreadcrumb
}
)
@withRouter
class ProjectMessage extends Component {
constructor(props) {
super(props);
this.state = {
protocol: 'http://',
projectMsg: {},
showDangerOptions: false
};
}
static propTypes = {
projectId: PropTypes.number,
form: PropTypes.object,
updateProject: PropTypes.func,
delProject: PropTypes.func,
getProject: PropTypes.func,
history: PropTypes.object,
fetchGroupMsg: PropTypes.func,
upsetProject: PropTypes.func,
groupList: PropTypes.array,
projectList: PropTypes.array,
projectMsg: PropTypes.object,
fetchGroupList: PropTypes.func,
currGroup: PropTypes.object,
setBreadcrumb: PropTypes.func
};
// 确认修改
handleOk = e => {
e.preventDefault();
const { form, updateProject, projectMsg, groupList } = this.props;
form.validateFields((err, values) => {
if (!err) {
let { tag } = this.tag.state;
// let tag = this.refs.tag;
tag = tag.filter(val => {
return val.name !== '';
});
let assignValue = Object.assign(projectMsg, values, { tag });
values.protocol = this.state.protocol.split(':')[0];
const group_id = assignValue.group_id;
const selectGroup = _.find(groupList, item => {
return item._id == group_id;
});
updateProject(assignValue)
.then(res => {
if (res.payload.data.errcode == 0) {
this.props.getProject(this.props.projectId);
message.success('修改成功! ');
// 如果如果项目所在的分组位置发生改变
this.props.fetchGroupMsg(group_id);
// this.props.history.push('/group');
let projectName = htmlFilter(assignValue.name);
this.props.setBreadcrumb([
{
name: selectGroup.group_name,
href: '/group/' + group_id
},
{
name: projectName
}
]);
}
})
.catch(() => {});
form.resetFields();
}
});
};
tagSubmit = tag => {
this.tag = tag;
};
showConfirm = () => {
let that = this;
confirm({
title: '确认删除 ' + that.props.projectMsg.name + ' 项目吗?',
content: (
<div style={{ marginTop: '10px', fontSize: '13px', lineHeight: '25px' }}>
<Alert
message="警告:此操作非常危险,会删除该项目下面所有接口,并且无法恢复!"
type="warning"
banner
/>
<div style={{ marginTop: '16px' }}>
<p style={{ marginBottom: '8px' }}>
<b>请输入项目名称确认此操作:</b>
</p>
<Input id="project_name" size="large" />
</div>
</div>
),
onOk() {
let groupName = trim(document.getElementById('project_name').value);
if (that.props.projectMsg.name !== groupName) {
message.error('项目名称有误');
return new Promise((resolve, reject) => {
reject('error');
});
} else {
that.props.delProject(that.props.projectId).then(res => {
if (res.payload.data.errcode == 0) {
message.success('删除成功!');
that.props.history.push('/group/' + that.props.projectMsg.group_id);
}
});
}
},
iconType: 'delete',
onCancel() {}
});
};
// 修改项目头像的背景颜色
changeProjectColor = e => {
const { _id, color, icon } = this.props.projectMsg;
this.props.upsetProject({ id: _id, color: e.target.value || color, icon }).then(res => {
if (res.payload.data.errcode === 0) {
this.props.getProject(this.props.projectId);
}
});
};
// 修改项目头像的图标
changeProjectIcon = e => {
const { _id, color, icon } = this.props.projectMsg;
this.props.upsetProject({ id: _id, color, icon: e.target.value || icon }).then(res => {
if (res.payload.data.errcode === 0) {
this.props.getProject(this.props.projectId);
}
});
};
// 点击“查看危险操作”按钮
toggleDangerOptions = () => {
// console.log(this.state.showDangerOptions);
this.setState({
showDangerOptions: !this.state.showDangerOptions
});
};
async UNSAFE_componentWillMount() {
await this.props.fetchGroupList();
await this.props.fetchGroupMsg(this.props.projectMsg.group_id);
}
render() {
const { getFieldDecorator } = this.props.form;
const { projectMsg, currGroup } = this.props;
const mockUrl =
location.protocol +
'//' +
location.hostname +
(location.port !== '' ? ':' + location.port : '') +
`/mock/${projectMsg._id}${projectMsg.basepath}+$接口请求路径`;
let initFormValues = {};
const {
name,
basepath,
desc,
project_type,
group_id,
switch_notice,
strice,
is_json5,
tag
} = projectMsg;
initFormValues = {
name,
basepath,
desc,
project_type,
group_id,
switch_notice,
strice,
is_json5,
tag
};
const colorArr = entries(constants.PROJECT_COLOR);
const colorSelector = (
<RadioGroup onChange={this.changeProjectColor} value={projectMsg.color} className="color">
{colorArr.map((item, index) => {
return (
<RadioButton
key={index}
value={item[0]}
style={{ backgroundColor: item[1], color: '#fff', fontWeight: 'bold' }}
>
{item[0] === projectMsg.color ? <Icon type="check" /> : null}
</RadioButton>
);
})}
</RadioGroup>
);
const iconSelector = (
<RadioGroup onChange={this.changeProjectIcon} value={projectMsg.icon} className="icon">
{constants.PROJECT_ICON.map(item => {
return (
<RadioButton key={item} value={item} style={{ fontWeight: 'bold' }}>
<Icon type={item} />
</RadioButton>
);
})}
</RadioGroup>
);
const selectDisbaled = projectMsg.role === 'owner' || projectMsg.role === 'admin';
return (
<div>
<div className="m-panel">
<Row className="project-setting">
<Col xs={6} lg={{ offset: 1, span: 3 }} className="setting-logo">
<Popover
placement="bottom"
title={colorSelector}
content={iconSelector}
trigger="click"
overlayClassName="change-project-container"
>
<Icon
type={projectMsg.icon || 'star-o'}
className="ui-logo"
style={{
backgroundColor:
constants.PROJECT_COLOR[projectMsg.color] || constants.PROJECT_COLOR.blue
}}
/>
</Popover>
</Col>
<Col xs={18} sm={15} lg={19} className="setting-intro">
<h2 className="ui-title">
{(currGroup.group_name || '') + ' / ' + (projectMsg.name || '')}
</h2>
{/* <p className="ui-desc">{projectMsg.desc}</p> */}
</Col>
</Row>
<hr className="breakline" />
<Form>
<FormItem {...formItemLayout} label="项目ID">
<span>{this.props.projectMsg._id}</span>
</FormItem>
<FormItem {...formItemLayout} label="项目名称">
{getFieldDecorator('name', {
initialValue: initFormValues.name,
rules: nameLengthLimit('项目')
})(<Input />)}
</FormItem>
<FormItem {...formItemLayout} label="所属分组">
{getFieldDecorator('group_id', {
initialValue: initFormValues.group_id + '',
rules: [
{
required: true,
message: '请选择项目所属的分组!'
}
]
})(
<Select disabled={!selectDisbaled}>
{this.props.groupList.map((item, index) => (
<Option value={item._id.toString()} key={index}>
{item.group_name}
</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={
<span>
MOCK地址&nbsp;
<Tooltip title="具体使用方法请查看文档">
<Icon type="question-circle-o" />
</Tooltip>
</span>
}
>
<Input disabled value={mockUrl} onChange={() => {}} />
</FormItem>
<FormItem {...formItemLayout} label="描述">
{getFieldDecorator('desc', {
initialValue: initFormValues.desc,
rules: [
{
required: false
}
]
})(<TextArea rows={8} />)}
</FormItem>
<FormItem
{...formItemLayout}
label={
<span>
tag 信息&nbsp;
<Tooltip title="定义 tag 信息,过滤接口">
<Icon type="question-circle-o" />
</Tooltip>
</span>
}
>
<ProjectTag tagMsg={tag} ref={this.tagSubmit} />
{/* <Tag tagMsg={tag} ref={this.tagSubmit} /> */}
</FormItem>
<FormItem
{...formItemLayout}
label={
<span>
mock严格模式&nbsp;
<Tooltip title="开启后 mock 请求会对 querybody form 的必须字段和 json schema 进行校验">
<Icon type="question-circle-o" />
</Tooltip>
</span>
}
>
{getFieldDecorator('strice', {
valuePropName: 'checked',
initialValue: initFormValues.strice
})(<Switch checkedChildren="开" unCheckedChildren="关" />)}
</FormItem>
<FormItem
{...formItemLayout}
label={
<span>
开启json5&nbsp;
<Tooltip title="开启后可在接口 body 和返回值中写 json 字段">
<Icon type="question-circle-o" />
</Tooltip>
</span>
}
>
{getFieldDecorator('is_json5', {
valuePropName: 'checked',
initialValue: initFormValues.is_json5
})(<Switch checkedChildren="开" unCheckedChildren="关" />)}
</FormItem>
<FormItem {...formItemLayout} label="默认开启消息通知">
{getFieldDecorator('switch_notice', {
valuePropName: 'checked',
initialValue: initFormValues.switch_notice
})(<Switch checkedChildren="开" unCheckedChildren="关" />)}
</FormItem>
<FormItem {...formItemLayout} label="权限">
{getFieldDecorator('project_type', {
rules: [
{
required: true
}
],
initialValue: initFormValues.project_type
})(
<RadioGroup>
<Radio value="private" className="radio">
<Icon type="lock" />私有<br />
<span className="radio-desc">只有组长和项目开发者可以索引并查看项目信息</span>
</Radio>
<br />
{projectMsg.role === 'admin' && <Radio value="public" className="radio">
<Icon type="unlock" />公开<br />
<span className="radio-desc">任何人都可以索引并查看项目信息</span>
</Radio>}
</RadioGroup>
)}
</FormItem>
</Form>
<div className="btnwrap-changeproject">
<Button
className="m-btn btn-save"
icon="save"
type="primary"
size="large"
onClick={this.handleOk}
>
</Button>
</div>
{/* 只有组长和管理员有权限删除项目 */}
{projectMsg.role === 'owner' || projectMsg.role === 'admin' ? (
<div className="danger-container">
<div className="title">
<h2 className="content">
<Icon type="exclamation-circle-o" /> 危险操作
</h2>
<Button onClick={this.toggleDangerOptions}>
<Icon type={this.state.showDangerOptions ? 'up' : 'down'} />
</Button>
</div>
{this.state.showDangerOptions ? (
<Card hoverable={true} className="card-danger">
<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}
</div>
) : null}
</div>
</div>
);
}
}
export default Form.create()(ProjectMessage);

View File

@@ -0,0 +1,116 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Icon, Row, Col, Input } from 'antd';
import './ProjectTag.scss';
class ProjectTag extends Component {
static propTypes = {
tagMsg: PropTypes.array,
tagSubmit: PropTypes.func
};
constructor(props) {
super(props);
this.state = {
tag: [{ name: '', desc: '' }]
};
}
initState(curdata) {
let tag = [
{
name: '',
desc: ''
}
];
if (curdata && curdata.length !== 0) {
curdata.forEach(item => {
tag.unshift(item);
});
}
return { tag };
}
componentDidMount() {
this.handleInit(this.props.tagMsg);
}
handleInit(data) {
let newValue = this.initState(data);
this.setState({ ...newValue });
}
addHeader = (val, index, name, label) => {
let newValue = {};
newValue[name] = [].concat(this.state[name]);
newValue[name][index][label] = val;
let nextData = this.state[name][index + 1];
if (!(nextData && typeof nextData === 'object')) {
let data = { name: '', desc: '' };
newValue[name] = [].concat(this.state[name], data);
}
this.setState(newValue);
};
delHeader = (key, name) => {
let curValue = this.state[name];
let newValue = {};
newValue[name] = curValue.filter((val, index) => {
return index !== key;
});
this.setState(newValue);
};
handleChange = (val, index, name, label) => {
let newValue = this.state;
newValue[name][index][label] = val;
this.setState(newValue);
};
render() {
const commonTpl = (item, index, name) => {
const length = this.state[name].length - 1;
return (
<Row key={index} className="tag-item">
<Col span={6} className="item-name">
<Input
placeholder={`请输入 ${name} 名称`}
// style={{ width: '200px' }}
value={item.name || ''}
onChange={e => this.addHeader(e.target.value, index, name, 'name')}
/>
</Col>
<Col span={12}>
<Input
placeholder="请输入tag 描述信息"
style={{ width: '90%', marginRight: 8 }}
onChange={e => this.handleChange(e.target.value, index, name, 'desc')}
value={item.desc || ''}
/>
</Col>
<Col span={2} className={index === length ? ' tag-last-row' : null}>
{/* 新增的项中,只有最后一项没有有删除按钮 */}
<Icon
className="dynamic-delete-button delete"
type="delete"
onClick={e => {
e.stopPropagation();
this.delHeader(index, name);
}}
/>
</Col>
</Row>
);
};
return (
<div className="project-tag">
{this.state.tag.map((item, index) => {
return commonTpl(item, index, 'tag');
})}
</div>
);
}
}
export default ProjectTag;

View File

@@ -0,0 +1,18 @@
.project-tag {
.item-name {
margin-right: 16px;
}
.delete {
font-size: 16px;
}
.tag-item {
margin-bottom: 8px;
}
.tag-last-row {
display: none;
}
}

View File

@@ -0,0 +1,136 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { Form, Switch, Button, Icon, Tooltip, message } from 'antd';
import AceEditor from '../../../../components/AceEditor/AceEditor';
const FormItem = Form.Item;
import { updateProjectMock, getProject } from '../../../../reducer/modules/project';
const formItemLayout = {
labelCol: {
sm: { span: 4 }
},
wrapperCol: {
sm: { span: 16 }
}
};
const tailFormItemLayout = {
wrapperCol: {
sm: {
span: 16,
offset: 11
}
}
};
@connect(
state => {
return {
projectMsg: state.project.currProject
};
},
{
updateProjectMock,
getProject
}
)
@Form.create()
export default class ProjectMock extends Component {
static propTypes = {
form: PropTypes.object,
match: PropTypes.object,
projectId: PropTypes.number,
updateProjectMock: PropTypes.func,
projectMsg: PropTypes.object,
getProject: PropTypes.func
};
constructor(props) {
super(props);
this.state = {
is_mock_open: false,
project_mock_script: ''
};
}
handleSubmit = async () => {
let params = {
id: this.props.projectId,
project_mock_script: this.state.project_mock_script,
is_mock_open: this.state.is_mock_open
};
let result = await this.props.updateProjectMock(params);
if (result.payload.data.errcode === 0) {
message.success('保存成功');
await this.props.getProject(this.props.projectId);
} else {
message.success('保存失败, ' + result.payload.data.errmsg);
}
};
UNSAFE_componentWillMount() {
this.setState({
is_mock_open: this.props.projectMsg.is_mock_open,
project_mock_script: this.props.projectMsg.project_mock_script
});
}
// 是否开启
onChange = v => {
this.setState({
is_mock_open: v
});
};
handleMockJsInput = e => {
this.setState({
project_mock_script: e.text
});
};
render() {
return (
<div className="m-panel">
<Form>
<FormItem
label={
<span>
是否开启&nbsp;<a
target="_blank"
rel="noopener noreferrer"
href="https://hellosean1025.github.io/yapi/documents/project.html#%E5%85%A8%E5%B1%80mock"
>
<Tooltip title="点击查看文档">
<Icon type="question-circle-o" />
</Tooltip>
</a>
</span>
}
{...formItemLayout}
>
<Switch
checked={this.state.is_mock_open}
onChange={this.onChange}
checkedChildren="开"
unCheckedChildren="关"
/>
</FormItem>
<FormItem label="Mock脚本" {...formItemLayout}>
<AceEditor
data={this.state.project_mock_script}
onChange={this.handleMockJsInput}
style={{ minHeight: '500px' }}
/>
</FormItem>
<FormItem {...tailFormItemLayout}>
<Button type="primary" htmlType="submit" onClick={this.handleSubmit}>
保存
</Button>
</FormItem>
</Form>
</div>
);
}
}

View File

@@ -0,0 +1,106 @@
import React, { PureComponent as Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Form, Button, message } from 'antd';
const FormItem = Form.Item;
import './project-request.scss';
import AceEditor from 'client/components/AceEditor/AceEditor';
import { updateProjectScript, getProject } from '../../../../reducer/modules/project';
@connect(
state => {
return {
projectMsg: state.project.currProject
};
},
{
updateProjectScript,
getProject
}
)
@Form.create()
export default class ProjectRequest extends Component {
static propTypes = {
projectMsg: PropTypes.object,
updateProjectScript: PropTypes.func,
getProject: PropTypes.func,
projectId: PropTypes.number
};
UNSAFE_componentWillMount() {
this.setState({
pre_script: this.props.projectMsg.pre_script,
after_script: this.props.projectMsg.after_script
});
}
handleSubmit = async () => {
let result = await this.props.updateProjectScript({
id: this.props.projectId,
pre_script: this.state.pre_script,
after_script: this.state.after_script
});
if (result.payload.data.errcode === 0) {
message.success('保存成功');
await this.props.getProject(this.props.projectId);
} else {
message.success('保存失败, ' + result.payload.data.errmsg);
}
};
render() {
const formItemLayout = {
labelCol: {
xs: { span: 24 },
sm: { span: 6 }
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 16 }
}
};
const tailFormItemLayout = {
wrapperCol: {
xs: {
span: 24,
offset: 0
},
sm: {
span: 16,
offset: 8
}
}
};
const { pre_script, after_script } = this.state;
return (
<div className="project-request">
<Form onSubmit={this.handleSubmit}>
<FormItem {...formItemLayout} label="Pre-request Script(请求参数处理脚本)">
<AceEditor
data={pre_script}
onChange={editor => this.setState({ pre_script: editor.text })}
fullScreen={true}
className="request-editor"
/>
</FormItem>
<FormItem {...formItemLayout} label="Pre-response Script(响应数据处理脚本)">
<AceEditor
data={after_script}
onChange={editor => this.setState({ after_script: editor.text })}
fullScreen={true}
className="request-editor"
/>
</FormItem>
<FormItem {...tailFormItemLayout}>
<Button onClick={this.handleSubmit} type="primary">
保存
</Button>
</FormItem>
</Form>
</div>
);
}
}

View File

@@ -0,0 +1,10 @@
.project-request{
background: #fff;
padding: 15px;
.request-editor{
min-height: 300px;
margin-top: 10px;
background-color: #eee;
}
}

View File

@@ -0,0 +1,99 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import './ProjectToken.scss';
import { getToken, updateToken } from '../../../../reducer/modules/project';
import { connect } from 'react-redux';
import { Icon, Tooltip, message, Modal } from 'antd';
import copy from 'copy-to-clipboard';
const confirm = Modal.confirm;
@connect(
state => {
return {
token: state.project.token
};
},
{
getToken,
updateToken
}
)
class ProjectToken extends Component {
static propTypes = {
projectId: PropTypes.number,
getToken: PropTypes.func,
token: PropTypes.string,
updateToken: PropTypes.func,
curProjectRole: PropTypes.string
};
async componentDidMount() {
await this.props.getToken(this.props.projectId);
}
copyToken = () => {
copy(this.props.token);
message.success('已经成功复制到剪切板');
};
updateToken = () => {
let that = this;
confirm({
title: '重新生成key',
content: '重新生成之后之前的key将无法使用确认重新生成吗',
okText: '确认',
cancelText: '取消',
async onOk() {
await that.props.updateToken(that.props.projectId);
message.success('更新成功');
},
onCancel() {}
});
};
render() {
return (
<div className="project-token">
<h2 className="token-title">工具标识</h2>
<div className="message">
每个项目都有唯一的标识token用户可以使用这个token值来请求项目 openapi.
</div>
<div className="token">
<span>
token: <span className="token-message">{this.props.token}</span>
</span>
<Tooltip title="复制">
<Icon className="token-btn" type="copy" onClick={this.copyToken} />
</Tooltip>
{this.props.curProjectRole === 'admin' || this.props.curProjectRole === 'owner' ? (
<Tooltip title="刷新">
<Icon className="token-btn" type="reload" onClick={this.updateToken} />
</Tooltip>
) : null}
</div>
<div className="blockquote">
为确保项目内数据的安全性和私密性请勿轻易将该token暴露给项目组外用户
</div>
<br />
<h2 className="token-title">open接口</h2>
<p><a target="_blank" rel="noopener noreferrer" href="https://hellosean1025.github.io/yapi/openapi.html">详细接口文档</a></p>
<div>
<ul className="open-api">
<li>/api/open/run_auto_test []</li>
<li>/api/open/import_data []</li>
<li>/api/interface/add []</li>
<li>/api/interface/save []</li>
<li>/api/interface/up []</li>
<li>/api/interface/get []</li>
<li>/api/interface/list []</li>
<li>/api/interface/list_menu []</li>
<li>/api/interface/add_cat []</li>
<li>/api/interface/getCatMenu []</li>
</ul>
</div>
</div>
);
}
}
export default ProjectToken;

View File

@@ -0,0 +1,70 @@
.project-token {
background: #fff;
padding: 15px;
min-height: 4.68rem;
.token{
padding: 16px;
font-size: 16px;
font-weight: 500;
}
.token-message{
padding: 8px;
margin-right: 8px;
background-color: #f5f5f5;
}
.open-api{
margin-top: 10px;
margin-left: 20px;
li{
margin-bottom: 10px;
}
}
.message{
padding: 16px 0 0 16px;
font-size: 14px;
}
.token-title{
font-size: 16px;
// background-color: #eee;
// border-radius: 4px;
// margin-bottom: 15px;
font-weight: 400;
margin-bottom: 0.16rem;
border-left: 3px solid #2395f1;
padding-left: 8px;
}
.blockquote{
border-left: 4px solid #ff561b;
// background-color: #f8f8f8;
padding: .12rem .24rem;
position: relative;
margin-left: 16px;
}
.blockquote:before {
content: '!';
display: block;
position: absolute;
left: -.12rem;
top: .12rem;
width: 20px;
height: 20px;
background-color: #ff561b;
color: #fff;
border-radius: 50%;
text-align: center;
font-family: 'Dosis', 'Source Sans Pro', 'Helvetica Neue', Arial, sans-serif;
}
.token-btn{
margin-right: 8px;
}
}

View File

@@ -0,0 +1,62 @@
import React, { PureComponent as Component } from 'react';
import { Tabs } from 'antd';
import PropTypes from 'prop-types';
import ProjectMessage from './ProjectMessage/ProjectMessage.js';
import ProjectEnv from './ProjectEnv/index.js';
import ProjectRequest from './ProjectRequest/ProjectRequest';
import ProjectToken from './ProjectToken/ProjectToken';
import ProjectMock from './ProjectMock/index.js';
import { connect } from 'react-redux';
const TabPane = Tabs.TabPane;
const plugin = require('client/plugin.js');
const routers = {}
import './Setting.scss';
@connect(state => {
return {
curProjectRole: state.project.currProject.role
};
})
class Setting extends Component {
static propTypes = {
match: PropTypes.object,
curProjectRole: PropTypes.string
};
render() {
const id = this.props.match.params.id;
plugin.emitHook('sub_setting_nav', routers);
return (
<div className="g-row">
<Tabs type="card" className="has-affix-footer tabs-large">
<TabPane tab="项目配置" key="1">
<ProjectMessage projectId={+id} />
</TabPane>
<TabPane tab="环境配置" key="2">
<ProjectEnv projectId={+id} />
</TabPane>
<TabPane tab="请求配置" key="3">
<ProjectRequest projectId={+id} />
</TabPane>
{this.props.curProjectRole !== 'guest' ? (
<TabPane tab="token配置" key="4">
<ProjectToken projectId={+id} curProjectRole={this.props.curProjectRole} />
</TabPane>
) : null}
<TabPane tab="全局mock脚本" key="5">
<ProjectMock projectId={+id} />
</TabPane>
{Object.keys(routers).map(key=>{
const C = routers[key].component;
return <TabPane tab={routers[key].name} key={routers[key].name}>
<C projectId={+id} />
</TabPane>
})}
</Tabs>
</div>
);
}
}
export default Setting;

View File

@@ -0,0 +1,227 @@
.form-item {
margin-bottom: .16rem;
}
.breakline {
margin-top: .18rem;
margin-bottom: .18rem;
border: 0;
border-top: 1px solid #eeeeee;
}
.card-danger {
border-color: #ff561b;
border-radius: 4px;
.ant-card-body {
display: flex;
align-items: center;
padding: .24rem !important;
}
.card-danger-content {
flex: 1;
}
.card-danger-btn {
flex-grow: 0;
flex-shrink: 1;
}
}
.setting-project-member {
.btn{
margin-left: 8px;
}
.m-user-name {
padding-right: 16px;
}
}
.setting-group {
margin-top: .48rem;
border-radius: 2px;
border-bottom: 1px solid #eee;
.ant-card-head {
background-color: #eee;
padding: 0 .08rem !important;
}
.ant-card-head-title {
font-size: .12rem;
float: inherit;
}
.ant-card-body {
padding: 0 !important;
}
.card-item {
padding: .1rem .15rem;
position: relative;
.item-img {
width: .2rem;
height: .2rem;
margin-right: .08rem;
vertical-align: middle;
}
.item-name {
position: absolute;
left: .43rem;
top: 50%;
transform: translateY(-50%);
}
.item-role {
position: absolute;
right: .15rem;
top: 50%;
transform: translateY(-50%);
}
}
.card-item + .card-item {
border-top: 1px solid #eee;
}
}
.project-setting {
.setting-logo {
text-align: right;
padding: .24rem;
cursor: pointer;
}
.setting-intro {
padding: .24rem;
height: 1.48rem;
display: flex;
align-items: center;
flex-wrap: wrap;
.ui-title {
font-size: .32rem;
font-weight: normal;
width: 100%;
}
.ui-desc {
font-size: .16rem;
}
}
.ui-logo {
width: 1rem;
height: 1rem;
border-radius: 50%;
font-size: .5rem;
color: #fff;
background-color: #2395f1;
line-height: 1rem;
box-shadow: 0 4px 6px rgba(50,50,93,.11), 0 1px 3px rgba(0,0,0,.08);
position: relative;
&:after {
opacity: 0;
content: '点击修改';
display: block;
transition: all .4s;
position: absolute;
left: 0;
top: 0;
border-radius: 50%;
font-size: .14rem;
color: #fff;
width: 100%;
height: 100%;
background-color: rgba(0,0,0, .25);
}
&:hover:after {
opacity: 1;
}
}
}
.change-project-container {
max-width: 320px;
.ant-popover-inner {
text-align: center;
}
.ant-popover-title {
padding: 8px .16rem;
height: auto;
}
.ant-radio-button-wrapper {
font-size: 16px;
border: none;
&:first-child {
border: none;
}
&:not(:first-child)::before {
display: none !important;
}
}
.ant-radio-button-wrapper-checked {
box-shadow: none;
color: #fff;
background-color: #2395f1;
border-radius: 4px;
}
.color {
// .ant-radio-button-wrapper {
// &:first-child {
// border: none;
// }
// }
.ant-radio-button-wrapper-checked {
border-radius: 0;
&:hover {
border: none;
box-shadow: none;
}
}
}
}
.danger-container {
margin-top: .48rem;
}
.btnwrap-changeproject {
text-align: center;
padding: .16rem 0;
background: #fff;
background-color: #fff;
margin: 0 -.4rem;
// background-image: linear-gradient(45deg, #d9d9d9 25%, transparent 0),linear-gradient(45deg, transparent 75%, #d9d9d9 0);
background-size: 4px 4px;
.btn-save {
font-size: .15rem;
font-weight: 200;
letter-spacing: 1px;
border: none;
box-shadow: 0 4px 6px rgba(50, 50, 93, 0.11), 0 1px 3px rgba(0, 0, 0, 0.08);
transform: translateY(0);
transition: all .2s;
&:hover {
transform: translateY(-1px);
}
&:active {
transform: translateY(1px);
}
}
}
.project-env{
// 环境配置中首个item的删除按钮定位调整
.ant-row-flex {
display: flex;
flex-flow: row wrap;
height: 60px;
}
}
// 危险操作
.danger-container {
.title {
margin-bottom: .48rem;
text-align: center;
.content {
color: rgba(39, 56, 72, 0.65);
margin-bottom: .16rem;
}
}
}
.radio.ant-radio-wrapper{
line-height: unset
}