Merge branch 'dev-mock' into dev

This commit is contained in:
zwjamnsss 2017-10-24 20:54:07 +08:00
commit 8b2d7aed2f
8 changed files with 309 additions and 134 deletions

View File

@ -164,5 +164,6 @@ export default {
{ name: '挑选(枚举)', mock: '@pick' },
{ name: '打乱数组', mock: '@shuffle' },
{ name: '协议', mock: '@protocol' }
]
],
IP_REGEXP: /^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])(\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])){3}$/
}

View File

@ -13,7 +13,7 @@ export default (state = initialState, action) => {
case FETCH_MOCK_COL:
return {
...state,
list: action.payload.data.data
list: action.payload.data
}
default:
return state

View File

@ -3,7 +3,8 @@ import React, { Component } from 'react'
import axios from 'axios'
import PropTypes from 'prop-types'
import { withRouter } from 'react-router-dom';
import { Form, Switch, Button, message, Icon, Tooltip } from 'antd';
import { Form, Switch, Button, message, Icon, Tooltip, Radio } from 'antd';
import MockCol from './MockCol/MockCol.js'
import mockEditor from 'client/containers/Project/Interface/InterfaceList/mockEditor';
const FormItem = Form.Item;
@ -18,7 +19,8 @@ class AdvMock extends Component {
super(props);
this.state = {
enable: false,
mock_script: ''
mock_script: '',
tab: 'case'
}
}
@ -74,6 +76,12 @@ class AdvMock extends Component {
})
}
handleTapChange = (e) => {
this.setState({
tab: e.target.value
})
}
render() {
const formItemLayout = {
labelCol: {
@ -91,27 +99,39 @@ class AdvMock extends Component {
}
}
};
const { tab } = this.state;
const isShowCase = tab === 'case';
return <div style={{ padding: '20px 10px' }}>
<Form onSubmit={this.handleSubmit}>
<FormItem
label={<span>是否开启&nbsp;<a target="_blank" rel="noopener noreferrer" href="https://yapi.ymfe.org/mock.html#高级Mock" ><Tooltip title="点击查看文档"><Icon type="question-circle-o" /></Tooltip></a></span>}
{...formItemLayout}
>
<Switch checked={this.state.enable} onChange={this.onChange} checkedChildren="开" unCheckedChildren="关" />
</FormItem>
<div style={{textAlign: 'center', marginBottom: 20}}>
<Radio.Group value={tab} size="large" onChange={this.handleTapChange}>
<Radio.Button value="case">期望</Radio.Button>
<Radio.Button value="script">脚本</Radio.Button>
</Radio.Group>
</div>
<div style={{display: isShowCase ? 'none' : ''}}>
<Form onSubmit={this.handleSubmit}>
<FormItem
label={<span>是否开启&nbsp;<a target="_blank" rel="noopener noreferrer" href="https://yapi.ymfe.org/mock.html#高级Mock" ><Tooltip title="点击查看文档"><Icon type="question-circle-o" /></Tooltip></a></span>}
{...formItemLayout}
>
<Switch checked={this.state.enable} onChange={this.onChange} checkedChildren="开" unCheckedChildren="关" />
</FormItem>
<FormItem
label="Mock脚本"
{...formItemLayout}
>
<div id="mock-script" style={{ minHeight: '500px' }} ></div>
</FormItem>
<FormItem {...tailFormItemLayout}>
<Button type="primary" htmlType="submit">保存</Button>
</FormItem>
<FormItem
label="Mock脚本"
{...formItemLayout}
>
<div id="mock-script" style={{ minHeight: '500px' }} ></div>
</FormItem>
<FormItem {...tailFormItemLayout}>
<Button type="primary" htmlType="submit">保存</Button>
</FormItem>
</Form>
</Form>
</div>
<div style={{display: isShowCase ? '' : 'none'}}>
<MockCol/>
</div>
</div>
}
}

View File

@ -1,21 +1,22 @@
import React, { Component } from 'react'
// import axios from 'axios'
import PropTypes from 'prop-types'
import { Button, Form, Input, Switch, Select, Icon, Modal } from 'antd';
import { Button, Form, Input, Switch, Select, Icon, Modal, Col, Row, InputNumber } from 'antd';
import { safeAssign } from '../../../client/common.js';
import mockEditor from '../../../client/containers/Project/Interface/InterfaceList/mockEditor';
import constants from '../../../client/constants/variable.js'
import { httpCodes } from '../index.js';
import './CaseDesModal.scss'
const Option = Select.Option;
const FormItem = Form.Item;
const formItemLayout = {
labelCol: {
xs: { span: 24 },
sm: { span: 5 }
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 12 }
}
labelCol: { span: 5 },
wrapperCol: { span: 12 }
};
const formItemLayoutWithOutLabel = {
wrapperCol: { span: 12, offset: 5 }
};
@Form.create()
@ -28,12 +29,19 @@ export default class CaseDesModal extends Component {
isAdd: PropTypes.bool,
visible: PropTypes.bool
}
state = {
headers: [],
paramsArr: [],
res_body: ''
}
constructor(props) {
super(props);
}
preProcess = caseData => {
// caseModel
// const a = {
// interface_id: { type: Number, required: true },
// project_id: {type: Number, required: true},
@ -51,7 +59,7 @@ export default class CaseDesModal extends Component {
// up_time: Number,
// res_body: {type: String, required: true}
// }
const initCaseData = {
const initCaseData = {
ip: '',
ip_enable: false,
name: '',
@ -61,17 +69,24 @@ export default class CaseDesModal extends Component {
paramsArr: [{name: '', value: ''}],
res_body: ''
}
caseData.paramsArr = caseData.params && caseData.params.length ? Object.keys(caseData.params).map(key => {
const paramsArr = caseData.params && Object.keys(caseData.params).length ? Object.keys(caseData.params).map(key => {
return { name: key, value: caseData.params[key] }
}) : [{name: '', value: ''}];
caseData.headers = caseData.headers && caseData.headers.length ? caseData.headers : [{name: '', value: ''}];
caseData = safeAssign(initCaseData, caseData);
const headers = caseData.headers && caseData.headers.length ? caseData.headers : [{name: '', value: ''}];
caseData.code = ''+caseData.code;
this.setState({
headers,
paramsArr
})
caseData = safeAssign(initCaseData, { ...caseData, headers, paramsArr });
return caseData;
}
endProcess = caseData => {
const headers = [];
const params = {};
const { res_body } = this.state;
caseData.headers.forEach(item => {
if (item.name) {
headers.push({
@ -87,28 +102,120 @@ export default class CaseDesModal extends Component {
})
caseData.headers = headers;
caseData.params = params;
caseData.res_body = res_body;
delete caseData.paramsArr;
return caseData;
}
componentDidMount() {
this.props.form.setFieldsValue(this.preProcess(this.props.caseData))
this.shouldLoadBodyEditor = true
}
componentDidUpdate() {
if (this.shouldLoadBodyEditor) {
this.loadBodyEditor()
this.shouldLoadBodyEditor = false
}
}
componentWillReceiveProps(nextProps) {
if (this.props.caseData !== nextProps.caseData) {
this.props.form.setFieldsValue(this.preProcess(this.props.caseData))
if (
this.props.caseData !== nextProps.caseData ||
this.props.visible !== nextProps.visible
) {
this.props.form.setFieldsValue(this.preProcess(nextProps.caseData))
this.shouldLoadBodyEditor = true
}
}
handleOk = () => {
const form = this.props.form;
this.props.onOk(this.endProcess(form.getFieldsValue()));
form.validateFields((err, values) => {
if (!err) {
this.props.onOk(this.endProcess(values));
}
})
}
addValues = (key) => {
const { setFieldsValue, getFieldValue } = this.props.form;
let values = getFieldValue(key);
values = values.concat({ name: '', value: ''});
this.setState({ [key]: values })
setFieldsValue({ [key]: values })
}
removeValues = (key, index) => {
const { setFieldsValue, getFieldValue } = this.props.form;
let values = getFieldValue(key);
values = values.filter((item, index2) => index !== index2);
this.setState({ [key]: values })
setFieldsValue({ [key]: values })
}
loadBodyEditor = () => {
const that = this;
const { setFieldsValue } = this.props.form;
this.props.visible && mockEditor({
container: 'res_body_json',
data: that.props.caseData.res_body,
onChange: function (d) {
if (d.format !== true) return false;
that.setState({
res_body: d.text
})
setFieldsValue({ res_body: d.text })
}
});
}
render() {
const { getFieldDecorator, getFieldValue } = this.props.form;
const { isAdd, visible, onCancel } = this.props;
const { headers, paramsArr } = this.state;
const valuesTpl = (name, values, title) => {
getFieldDecorator(name)
return values.map((item, index) => (
<div key={index} className={name}>
<FormItem
{...(index === 0 ? formItemLayout : formItemLayoutWithOutLabel)}
wrapperCol={index === 0 ? { span: 19 } : { span: 19, offset: 5 }}
label={index ? '' : title}
>
<Row gutter={8}>
<Col span={10}>
<FormItem>
{getFieldDecorator(`${name}[${index}].name`, { initialValue: item.name })(
name === 'headers' ? <Select showSearch>
{constants.HTTP_REQUEST_HEADER.map(item => (
<Option value={item} key={item}>{item}</Option>
))}
</Select> : <Input />
)}
</FormItem>
</Col>
<Col span={10}>
<FormItem>
{getFieldDecorator(`${name}[${index}].value`, { initialValue: item.value })(
<Input />
)}
</FormItem>
</Col>
<Col span={4}>
{values.length > 1 ? (
<Icon
className="dynamic-delete-button"
type="minus-circle-o"
onClick={() => this.removeValues(name, index)}
/>
) : null}
</Col>
</Row>
</FormItem>
</div>
))
}
return (
<Modal
@ -116,7 +223,9 @@ export default class CaseDesModal extends Component {
visible={visible}
maskClosable={false}
onOk={this.handleOk}
width={780}
onCancel={() => onCancel()}
className="case-des-modal"
>
<Form>
<FormItem
@ -129,23 +238,36 @@ export default class CaseDesModal extends Component {
<Input placeholder="请输入期望名称" />
)}
</FormItem>
<FormItem {...formItemLayout} label="IP 过滤">
{getFieldDecorator('ip_enable', {
valuePropName: 'checked',
rules: [{ type: 'boolean' }]
})(
<Switch />
)}
{getFieldDecorator('ip')(
<Input placeholder="请输入过滤的 IP 地址" />
)}
<FormItem {...formItemLayout} label="IP 过滤" className="ip-filter">
<Col span={6} className="ip-switch">
<FormItem>
{getFieldDecorator('ip_enable', {
valuePropName: 'checked',
rules: [{ type: 'boolean' }]
})(
<Switch />
)}
</FormItem>
</Col>
<Col span={18}>
<div style={{display: getFieldValue('ip_enable') ? '' : 'none'}} className="ip">
<FormItem>
{getFieldDecorator('ip', getFieldValue('ip_enable') ? {
rules: [{ pattern: constants.IP_REGEXP, message: '请填写正确的 IP 地址' }]
} : {})(
<Input placeholder="请输入过滤的 IP 地址" />
)}
</FormItem>
</div>
</Col>
</FormItem>
<FormItem
{...formItemLayout}
required
label="HTTP CODE"
>
{getFieldDecorator('code')(
<Select search>
<Select showSearch>
{
httpCodes.map(code => <Option key={''+code} value={''+code}>{''+code}</Option>)
}
@ -160,87 +282,42 @@ export default class CaseDesModal extends Component {
initialValue: 0,
rules: [{ required: true, message: '请输入延时时间!', type: 'integer' }]
})(
<Input placeholder="请输入延时时间" />
<InputNumber placeholder="请输入延时时间"/>
)}
<span>ms</span>
</FormItem>
{
getFieldDecorator('headers', { initialValue: [] }) &&
getFieldValue('headers').map((item, index) => (
<div key={index}>
<FormItem
{...formItemLayout}
label={index ? '' : 'HTTP 头'}
>
{getFieldDecorator(`headers[${index}].name`)(
<Input />
)}
</FormItem>
<FormItem
{...formItemLayout}
>
{getFieldDecorator(`headers[${index}].value`)(
<Input />
)}
{getFieldValue('headers').length > 1 ? (
<Icon
className="dynamic-delete-button"
type="minus-circle-o"
onClick={() => this.removeHeaders(index)}
/>
) : null}
</FormItem>
</div>
))
valuesTpl('headers', headers, 'HTTP 头')
}
<FormItem>
<Button type="dashed" onClick={this.addHeaders} style={{ width: '60%' }}>
<FormItem {...formItemLayoutWithOutLabel}>
<Button type="dashed" onClick={() => this.addValues('headers')} style={{ width: '80%' }}>
<Icon type="plus" /> 添加 HTTP
</Button>
</FormItem>
{
getFieldDecorator('paramsArr', { initialValue: [] }) &&
getFieldValue('paramsArr').map((item, index) => (
<div key={index}>
<FormItem
{...formItemLayout}
label={index ? '' : '参数'}
>
{getFieldDecorator(`paramsArr[${index}].name`)(
<Input />
)}
</FormItem>
<FormItem
>
{getFieldDecorator(`paramsArr[${index}].value`)(
<Input />
)}
{getFieldValue('paramsArr').length > 1 ? (
<Icon
className="dynamic-delete-button"
type="minus-circle-o"
onClick={() => this.removeParams(index)}
/>
) : null}
</FormItem>
</div>
))
valuesTpl('paramsArr', paramsArr, '参数')
}
<FormItem>
<Button type="dashed" onClick={this.addParams} style={{ width: '60%' }}>
<FormItem {...formItemLayoutWithOutLabel}>
<Button type="dashed" onClick={() => this.addValues('paramsArr')} style={{ width: '80%' }}>
<Icon type="plus" /> 添加参数
</Button>
</FormItem>
<FormItem
{...formItemLayout}
label="返回 JSON"
>
{getFieldDecorator('res_body', {
rules: [{ required: true, message: '请输入期望名称!' }]
})(
<Input placeholder="返回 JSON" />
)}
<FormItem {...formItemLayout} wrapperCol={{ span: 17 }} label="返回 JSON" required>
<div id="res_body_json" style={{
minHeight: "300px",
border: "1px solid #d9d9d9",
borderRadius: 4
}} ></div>
<FormItem
{...formItemLayoutWithOutLabel}
>
{getFieldDecorator('res_body', {
rules: [{ required: true, message: '请输入返回 JSON' }]
})(
<Input placeholder="请输入期望名称" style={{display: 'none'}} />
)}
</FormItem>
</FormItem>
</Form>
</Modal>

View File

@ -0,0 +1,12 @@
.case-des-modal {
.ant-modal-body {
max-height: 520px;
overflow-y: scroll;
}
.ip-filter .ip>.ant-form-item, .ip-filter .ip-switch>.ant-form-item {
margin-bottom: 0;
}
.headers>.ant-form-item, .paramsArr>.ant-form-item {
margin-bottom: 0;
}
}

View File

@ -3,7 +3,7 @@ import { connect } from 'react-redux'
import axios from 'axios'
import PropTypes from 'prop-types'
import { withRouter } from 'react-router-dom';
import { Table, Button, message } from 'antd';
import { Table, Button, message, Popconfirm } from 'antd';
import { fetchMockCol } from '../../../client/reducer/modules/mockCol'
import { formatTime } from '../../../client/common.js';
import CaseDesModal from './CaseDesModal';
@ -11,7 +11,8 @@ import CaseDesModal from './CaseDesModal';
@connect(
state => {
return {
list: state.mockCol.list
list: state.mockCol.list,
currInterface: state.inter.curdata
}
},
{
@ -22,6 +23,7 @@ import CaseDesModal from './CaseDesModal';
export default class MockCol extends Component {
static propTypes = {
list: PropTypes.array,
currInterface: PropTypes.object,
match: PropTypes.object,
fetchMockCol: PropTypes.func
}
@ -42,7 +44,8 @@ export default class MockCol extends Component {
}
handleOk = async (caseData) => {
const interface_id = this.props.match.params.action;
const { caseData: currcase } = this.state;
const interface_id = this.props.match.params.actionId;
const project_id = this.props.match.params.id;
caseData = Object.assign({
...caseData,
@ -50,11 +53,26 @@ export default class MockCol extends Component {
project_id: project_id
})
if (!this.state.isAdd) {
caseData.id = 0;
caseData.id = currcase._id;
}
axios.post('/api/plugin/advmock/case/save', caseData).then(res => {
await axios.post('/api/plugin/advmock/case/save', caseData).then(async res => {
if (res.data.errcode === 0) {
message.success(this.state.isAdd ? '添加成功' : '保存成功');
await this.props.fetchMockCol(interface_id);
this.setState({ caseDesModalVisible: false })
} else {
message.error(res.data.errmsg);
}
})
}
deleteCase = async (id) => {
console.log(id)
const interface_id = this.props.match.params.actionId;
await axios.post('/api/plugin/advmock/case/del', {id}).then(async res => {
if (res.data.errcode === 0) {
message.success('删除成功');
await this.props.fetchMockCol(interface_id);
} else {
message.error(res.data.errmsg);
}
@ -69,12 +87,21 @@ export default class MockCol extends Component {
const data = this.props.list;
const { isAdd, caseData, caseDesModalVisible } = this.state;
const initCaseData = {
ip: '',
ip_enable: false,
name: this.props.currInterface.title,
code: '200',
deplay: 0,
headers: [{name: '', value: ''}],
paramsArr: [{name: '', value: ''}],
res_body: ''
}
const columns = [{
title: '期望名称',
dataIndex: 'name',
key: 'name',
render: text => <a href="#">{text}</a>
key: 'name'
}, {
title: 'ip',
dataIndex: 'ip',
@ -85,18 +112,48 @@ export default class MockCol extends Component {
key: 'username'
}, {
title: '编辑时间',
key: 'action',
dataIndex: 'up_time',
key: 'up_time',
render: text => formatTime(text)
}, {
title: '操作',
dataIndex: 'address',
key: 'address'
dataIndex: '_id',
key: '_id',
render: (_id, recode) => {
return (
<div>
<span style={{marginRight: 5}}>
<Button size="small" onClick={() => this.setState({
isAdd: false,
caseDesModalVisible: true,
caseData: recode
})}>编辑</Button>
</span>
<span>
<Popconfirm
title="你确定要删除此条期望?"
onConfirm={() => this.deleteCase(_id)}
okText="确定"
cancelText="取消"
>
<Button size="small" onClick={() => {}}>删除</Button>
</Popconfirm>
</span>
</div>
)
}
}];
return (
<div style={{ padding: '20px 10px' }}>
<Button type="primary" onClick={() => this.setState({isAdd: true, caseDesModalVisible: true})}>添加期望</Button>
<Table columns={columns} dataSource={data} />
<div>
<div style={{marginBottom: 8}}>
<Button type="primary" onClick={() => this.setState({
isAdd: true,
caseDesModalVisible: true,
caseData: initCaseData
})}>添加期望</Button>
</div>
<Table columns={columns} dataSource={data} pagination={false} rowKey='_id' />
<CaseDesModal
visible={caseDesModalVisible}
isAdd={isAdd}

View File

@ -1,4 +1,4 @@
import AdvMock from './MockCol/MockCol.js'
import AdvMock from './AdvMock'
module.exports = function(){
this.bindHook('interface_tab', function(tabs){

View File

@ -2,6 +2,7 @@ const baseController = require('controllers/base.js');
const advModel = require('./advMockModel.js');
const yapi = require('yapi.js');
const caseModel = require('./caseModel.js');
const userModel = require('../../server/models/user.js');
const config = require('./index.js');
class advMockController extends baseController{
@ -9,6 +10,7 @@ class advMockController extends baseController{
super(ctx);
this.Model = yapi.getInst(advModel);
this.caseModel = yapi.getInst(caseModel);
this.userModel = yapi.getInst(userModel);
}
async getMock(ctx){
@ -56,6 +58,11 @@ class advMockController extends baseController{
return ctx.body = yapi.commons.resReturn(null, 400, '缺少 interface_id');
}
let result = await this.caseModel.list(id);
for(let i = 0, len = result.length; i < len; i++) {
let userinfo = await this.userModel.findById(result[i].uid);
result[i] = result[i].toObject();
result[i].username = userinfo.username;
}
ctx.body = yapi.commons.resReturn(result);
}
@ -68,6 +75,7 @@ class advMockController extends baseController{
let result = await this.caseModel.get({
_id: id
})
ctx.body = yapi.commons.resReturn(result);
}
@ -118,8 +126,7 @@ class advMockController extends baseController{
}
findRepeat = await this.caseModel.get(findRepeatParams);
if(findRepeat){
if(findRepeat && findRepeat._id !== params.id){
return ctx.body = yapi.commons.resReturn(null,400, '已存在的期望');
}
@ -138,7 +145,8 @@ class advMockController extends baseController{
if(!id){
return ctx.body =yapi.commons.resReturn(null, 408, '缺少 id');
}
ctx.body = await this.caseModel.del(id);
let result = await this.caseModel.del(id);
return ctx.body = yapi.commons.resReturn(result);
}