2
0
mirror of https://github.com/YMFE/yapi.git synced 2025-04-24 15:30:44 +08:00

Merge branch 'master' into login

This commit is contained in:
sean1025 2018-11-01 11:16:34 +08:00 committed by GitHub
commit 5ccf315ff1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
99 changed files with 7839 additions and 5393 deletions
CHANGELOG.mdREADME.md
client
common
docs
exts
yapi-plugin-advanced-mock
yapi-plugin-export-data
yapi-plugin-import-swagger
yapi-plugin-statistics
package-lock.jsonpackage.json
server
static
test

@ -1,3 +1,20 @@
### v1.3.23
* 接口tag功能
* 数据导入增加 merge 功能
* 增加参数的批量导入功能
* json schema 可视化编辑器增加 mock 功能
#### Bug Fixed
* 接口path中写入 ?name=xxx bug
* 高级mock 匹配 data: [{item: XXX}] 时匹配不成功
* 接口运行 query params 自动勾选
* mock get 带 cookie 时跨域
* json schema 嵌套多层 array 预览不展示 bug
* swagger URL 导入 跨域问题
### v1.3.22
* json schema number和integer支持枚举
@ -5,7 +22,7 @@
* 增加 mock 接口请求字段参数验证
* 增加返回数据验证
### Bug Fixed
#### Bug Fixed
* 命令行导入成员信息为 undefined
* 修复form 参数为空时 接口无法保存的问题

@ -45,6 +45,9 @@ YApi 是<strong>高效</strong>、<strong>易用</strong>、<strong>功能强大
* [yapi sso 登录插件](https://github.com/YMFE/yapi-plugin-qsso)
* [yapi cas 登录插件](https://github.com/wsfe/yapi-plugin-cas) By wsfe
### YApi 一些工具
* [mysql服务http工具,可配合做自动化测试](https://github.com/hellosean1025/http-mysql-server)
### YApi docker部署非官方
* [使用 alpine 版 docker 镜像快速部署 yapi](https://www.jianshu.com/p/a97d2efb23c5)
@ -61,6 +64,7 @@ YApi 是<strong>高效</strong>、<strong>易用</strong>、<strong>功能强大
* 快手
* 便利蜂
* 中商惠民
* 新浪
### Authors
* [hellosean1025](https://github.com/hellosean1025)

@ -45,6 +45,9 @@ function isJson5(json) {
return false;
}
}
exports.safeArray = function(arr) {
return Array.isArray(arr) ? arr : [];
};
exports.json5_parse = function(json) {
try {
@ -173,6 +176,13 @@ exports.nameLengthLimit = type => {
];
};
// 去除所有html标签只保留文字
exports.htmlFilter = html => {
let reg = /<\/?.+?\/?>/g;
return html.replace(reg, '') || '新项目';
};
// 实现 Object.entries() 方法
exports.entries = obj => {
let res = [];

@ -367,10 +367,14 @@ export default class Run extends Component {
};
changeParam = (name, v, index, key) => {
key = key || 'value';
const pathParam = deepCopyJson(this.state[name]);
pathParam[index][key] = v;
if (key === 'value') {
pathParam[index].enable = !!v;
}
this.setState({
[name]: pathParam
});
@ -380,7 +384,7 @@ export default class Run extends Component {
const bodyForm = deepCopyJson(this.state.req_body_form);
key = key || 'value';
if (key === 'value') {
bodyForm[index].enable = true;
bodyForm[index].enable = !!v;
if (bodyForm[index].type === 'file') {
bodyForm[index].value = 'file_' + index;
} else {

@ -20,7 +20,8 @@ const messageMap = {
uniqueItems: '元素是否都不同',
itemType: 'item 类型',
format: 'format',
itemFormat: 'format'
itemFormat: 'format',
mock: 'mock'
};
const columns = [
@ -78,17 +79,20 @@ const columns = [
title: '其他信息',
dataIndex: 'sub',
key: 'sub',
width: 80,
render: text => {
return Object.keys(text || []).map((item, index) => {
width: 180,
render: (text, record) => {
let result = text || record;
return Object.keys(result).map((item, index) => {
let name = messageMap[item];
let value = text[item];
let value = result[item];
let isShow = !_.isUndefined(result[item]) && !_.isUndefined(name);
return (
!_.isUndefined(text[item]) && (
isShow && (
<p key={index}>
<span style={{ fontWeight: '700' }}>{name}: </span>
<span >{value.toString()}</span>
<span>{value.toString()}</span>
</p>
)
);

@ -191,10 +191,10 @@ class ProjectList extends Component {
<span className="radio-desc">只有组长和项目开发者可以索引并查看项目信息</span>
</Radio>
<br />
<Radio value="public" className="radio">
{/* <Radio value="public" className="radio">
<Icon type="unlock" />公开<br />
<span className="radio-desc">任何人都可以索引并查看项目信息</span>
</Radio>
</Radio> */}
</RadioGroup>
)}
</FormItem>

@ -221,17 +221,12 @@ export default class GroupList extends Component {
<div className="curr-group">
<div className="curr-group-name">
<span className="name">{currGroup.group_name}</span>
{/* this.props.curUserRole === "admin" || this.props.curUserRoleInGroup === 'owner' ? (menu) : '' */}
{/* 只有超级管理员能添加分组 */
this.props.curUserRole === 'admin' ? (
<Tooltip title="添加分组">
<a className="editSet">
<Icon className="btn" type="folder-add" onClick={this.showModal} />
</a>
</Tooltip>
) : (
''
)}
<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>

@ -993,7 +993,10 @@ class InterfaceColContent extends Component {
</Row>
<Row type="flex" justify="space-around" className="row" align="middle">
<Col span={21} className="autoTestUrl">
<a href={localUrl + autoTestsUrl} target="_blank">
<a
target="_blank"
rel="noopener noreferrer"
href={localUrl + autoTestsUrl} >
{autoTestsUrl}
</a>
</Col>

@ -1,26 +1,33 @@
import React, { PureComponent as Component } from 'react'
import PropTypes from 'prop-types'
import React, { PureComponent as Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import InterfaceEditForm from './InterfaceEditForm.js'
import { updateInterfaceData, fetchInterfaceListMenu, fetchInterfaceData } from '../../../../reducer/modules/interface.js';
import axios from 'axios'
import { message } from 'antd'
import './Edit.scss'
import InterfaceEditForm from './InterfaceEditForm.js';
import {
updateInterfaceData,
fetchInterfaceListMenu,
fetchInterfaceData
} from '../../../../reducer/modules/interface.js';
import { getProject } from '../../../../reducer/modules/project.js';
import axios from 'axios';
import { message, Modal } from 'antd';
import './Edit.scss';
import { withRouter, Link } from 'react-router-dom';
import ProjectTag from '../../Setting/ProjectMessage/ProjectTag.js';
@connect(
state => {
return {
curdata: state.inter.curdata,
currProject: state.project.currProject
}
}, {
};
},
{
updateInterfaceData,
fetchInterfaceListMenu,
fetchInterfaceData
fetchInterfaceData,
getProject
}
)
class InterfaceEdit extends Component {
static propTypes = {
curdata: PropTypes.object,
@ -29,125 +36,193 @@ class InterfaceEdit extends Component {
fetchInterfaceListMenu: PropTypes.func,
fetchInterfaceData: PropTypes.func,
match: PropTypes.object,
switchToView: PropTypes.func
}
switchToView: PropTypes.func,
getProject: PropTypes.func
};
constructor(props) {
super(props)
super(props);
const { curdata, currProject } = this.props;
this.state = {
mockUrl: location.protocol + '//' + location.hostname + (location.port !== "" ? ":" + location.port : "") + `/mock/${currProject._id}${currProject.basepath}${curdata.path}`,
mockUrl:
location.protocol +
'//' +
location.hostname +
(location.port !== '' ? ':' + location.port : '') +
`/mock/${currProject._id}${currProject.basepath}${curdata.path}`,
curdata: {},
status: 0
}
status: 0,
visible: false
// tag: []
};
}
onSubmit = async (params) => {
onSubmit = async params => {
params.id = this.props.match.params.actionId;
let result = await axios.post('/api/interface/up', params);
this.props.fetchInterfaceListMenu(this.props.currProject._id).then();
this.props.fetchInterfaceData(params.id).then()
this.props.fetchInterfaceData(params.id).then();
if (result.data.errcode === 0) {
this.props.updateInterfaceData(params);
message.success('保存成功');
} else {
message.error(result.data.errmsg)
message.error(result.data.errmsg);
}
}
};
componentWillUnmount() {
try {
if (this.state.status === 1) {
this.WebSocket.close()
this.WebSocket.close();
}
} catch (e) {
return null
return null;
}
}
componentDidMount() {
let domain = location.hostname + (location.port !== "" ? ":" + location.port : "");
let s, initData = false;
let domain = location.hostname + (location.port !== '' ? ':' + location.port : '');
let s,
initData = false;
//因后端 node 仅支持 ws 暂不支持 wss
let wsProtocol = location.protocol === 'https:' ? 'wss' : 'ws';
setTimeout(()=>{
if(initData === false){
setTimeout(() => {
if (initData === false) {
this.setState({
curdata: this.props.curdata,
status: 1
})
});
initData = true;
}
}, 3000)
}, 3000);
try {
s = new WebSocket(wsProtocol + '://' + domain + '/api/interface/solve_conflict?id=' + this.props.match.params.actionId);
s = new WebSocket(
wsProtocol +
'://' +
domain +
'/api/interface/solve_conflict?id=' +
this.props.match.params.actionId
);
s.onopen = () => {
this.WebSocket = s;
}
};
s.onmessage = (e) => {
s.onmessage = e => {
initData = true;
let result = JSON.parse(e.data);
if (result.errno === 0) {
this.setState({
curdata: result.data,
status: 1
})
});
} else {
this.setState({
curdata: result.data,
status: 2
})
});
}
}
};
s.onerror = () => {
this.setState({
curdata: this.props.curdata,
status: 1
})
console.warn('websocket 连接失败,将导致多人编辑同一个接口冲突。')
}
});
console.warn('websocket 连接失败,将导致多人编辑同一个接口冲突。');
};
} catch (e) {
this.setState({
curdata: this.props.curdata,
status: 1
})
});
console.error('websocket 连接失败,将导致多人编辑同一个接口冲突。');
}
}
onTagClick = () => {
this.setState({
visible: true
});
};
handleOk = async () => {
let { tag } = this.tag.state;
tag = tag.filter(val => {
return val.name !== '';
});
let id = this.props.currProject._id;
let params = {
id,
tag
};
let result = await axios.post('/api/project/up_tag', params);
if (result.data.errcode === 0) {
await this.props.getProject(id);
message.success('保存成功');
} else {
message.error(result.data.errmsg);
}
this.setState({
visible: false
});
};
handleCancel = () => {
this.setState({
visible: false
});
};
tagSubmit = tagRef => {
this.tag = tagRef;
// this.setState({tag})
};
render() {
return <div className="interface-edit">
{this.state.status === 1 ?
<InterfaceEditForm
cat={this.props.currProject.cat}
mockUrl={this.state.mockUrl}
basepath={this.props.currProject.basepath}
noticed={this.props.currProject.switch_notice}
onSubmit={this.onSubmit}
curdata={this.state.curdata} />
:
null}
{
this.state.status === 2 ?
const { cat, basepath, switch_notice, tag } = this.props.currProject;
return (
<div className="interface-edit">
{this.state.status === 1 ? (
<InterfaceEditForm
cat={cat}
mockUrl={this.state.mockUrl}
basepath={basepath}
noticed={switch_notice}
onSubmit={this.onSubmit}
curdata={this.state.curdata}
onTagClick={this.onTagClick}
/>
) : null}
{this.state.status === 2 ? (
<div style={{ textAlign: 'center', fontSize: '14px', paddingTop: '10px' }}>
<Link to={'/user/profile/' + this.state.curdata.uid}><b>{this.state.curdata.username}</b></Link>
<Link to={'/user/profile/' + this.state.curdata.uid}>
<b>{this.state.curdata.username}</b>
</Link>
<span>正在编辑该接口请稍后再试...</span>
</div>
:
null}
{
this.state.status === 0 && '正在加载,请耐心等待...'
}
) : null}
{this.state.status === 0 && '正在加载,请耐心等待...'}
</div>
<Modal
title="Tag 设置"
width={680}
visible={this.state.visible}
onOk={this.handleOk}
onCancel={this.handleCancel}
okText="保存"
>
<div className="tag-modal-center">
<ProjectTag tagMsg={tag} ref={this.tagSubmit} />
</div>
</Modal>
</div>
);
}
}

@ -77,7 +77,8 @@
text-overflow:ellipsis;
white-space: nowrap;
padding-right: 24px;
line-height: 100%;
// line-height: 100%;
vertical-align: middle;
}
.opened {
@ -109,3 +110,7 @@
word-break: break-all;
}
.tag-modal-center {
padding-left: 48px;
}

File diff suppressed because it is too large Load Diff

@ -186,6 +186,11 @@ class InterfaceList extends Component {
};
render() {
let tag = this.props.curProject.tag;
let filter = tag.map(item => {
return { text: item.name, value: item.name };
});
const columns = [
{
title: '接口名称',
@ -232,12 +237,12 @@ class InterfaceList extends Component {
title: '接口分类',
dataIndex: 'catid',
key: 'catid',
width: 18,
width: 28,
render: (item, record) => {
return (
<Select
value={item + ''}
className="select"
className="select path"
onChange={catid => this.changeInterfaceCat(record._id, catid)}
>
{this.props.catList.map(cat => {
@ -255,7 +260,7 @@ class InterfaceList extends Component {
title: '状态',
dataIndex: 'status',
key: 'status',
width: 14,
width: 24,
render: (text, record) => {
const key = record.key;
return (
@ -284,6 +289,20 @@ class InterfaceList extends Component {
}
],
onFilter: (value, record) => record.status.indexOf(value) === 0
},
{
title: 'tag',
dataIndex: 'tag',
key: 'tag',
width: 14,
render: text => {
let textMsg = text.length > 0 ? text.join('\n') : '未设置';
return <div className="table-desc">{textMsg}</div>;
},
filters: filter,
onFilter: (value, record) => {
return record.tag.indexOf(value) >= 0;
}
}
];
let intername = '',
@ -328,6 +347,8 @@ class InterfaceList extends Component {
const isDisabled = this.props.catList.length === 0;
// console.log(this.props.curProject.tag)
return (
<div style={{ padding: '24px' }}>
<h2 className="interface-title" style={{ display: 'inline-block', margin: 0 }}>

@ -114,6 +114,7 @@ class InterfaceMenu extends Component {
componentWillReceiveProps(nextProps) {
if (this.props.list !== nextProps.list) {
// console.log('next', nextProps.list)
this.setState({
list: nextProps.list
});
@ -254,14 +255,14 @@ class InterfaceMenu extends Component {
draftData.path = draftData.path + '_' + Date.now();
});
axios.post('/api/interface/add', newData).then(res => {
axios.post('/api/interface/add', newData).then(async res => {
if (res.data.errcode !== 0) {
return message.error(res.data.errmsg);
}
message.success('接口添加成功');
let interfaceId = res.data.data._id;
await this.getList();
this.props.history.push('/project/' + this.props.projectId + '/interface/api/' + interfaceId);
this.getList();
this.setState({
visible: false
});
@ -329,10 +330,38 @@ class InterfaceMenu extends Component {
this.props.fetchInterfaceListMenu(this.props.projectId);
}
};
// 数据过滤
filterList = (list, arr) => {
let that = this;
let menuList = produce(list, draftList => {
draftList.filter(item => {
let interfaceFilter = false;
if (item.name.indexOf(that.state.filter) === -1) {
item.list = item.list.filter(inter => {
if (
inter.title.indexOf(that.state.filter) === -1 &&
inter.path.indexOf(that.state.filter) === -1
) {
return false;
}
//arr.push('cat_' + inter.catid)
interfaceFilter = true;
return true;
});
arr.push('cat_' + item._id);
return interfaceFilter === true;
}
arr.push('cat_' + item._id);
return true;
});
});
return menuList;
};
render() {
const matchParams = this.props.match.params;
let menuList = this.state.list;
// let menuList = this.state.list;
const searchBox = (
<div className="interface-filter">
<Input onChange={this.onFilter} value={this.state.filter} placeholder="搜索接口" />
@ -398,9 +427,6 @@ class InterfaceMenu extends Component {
)}
</div>
);
if (menuList.length === 0) {
return searchBox;
}
const defaultExpandedKeys = () => {
const { router, inter, list } = this.props,
rNull = { expands: [], selects: [] };
@ -432,26 +458,6 @@ class InterfaceMenu extends Component {
};
const itemInterfaceCreate = item => {
// let color;
// switch (item.method) {
// case 'GET': color = "green"; break;
// case 'POST': color = "blue"; break;
// case 'PUT': color = "yellow"; break;
// case 'DELETE': color = 'red'; break;
// default: color = "yellow";
// }
// const menu = (item) => {
// return <Menu>
// <Menu.Item>
// <span onClick={() => { this.showConfirm(item._id) }}>删除接口</span>
// </Menu.Item>
// <Menu.Item>
// <span onClick={() => {
// this.copyInterface(item)
// }}>复制接口</span>
// </Menu.Item>
// </Menu>
// };
return (
<TreeNode
@ -501,62 +507,14 @@ class InterfaceMenu extends Component {
/>
);
};
// const menu = (item) => {
// return <Menu>
// <Menu.Item>
// <span onClick={() => {
// this.changeModal('visible', true);
// this.setState({
// curCatid: item._id
// })
// }}>添加接口</span>
// </Menu.Item>
// <Menu.Item>
// <span onClick={() => {
// this.changeModal('change_cat_modal_visible', true);
// this.setState({
// curCatdata: item
// })
// }}>修改分类</span>
// </Menu.Item>
// <Menu.Item>
// <span onClick={() => {
// this.showDelCatConfirm(item._id)
// }}>删除分类</span>
// </Menu.Item>
// </Menu>
// };
let currentKes = defaultExpandedKeys();
let menuList;
if (this.state.filter) {
let arr = [];
menuList = menuList.filter(item => {
let interfaceFilter = false;
if (item.name.indexOf(this.state.filter) === -1) {
item.list = item.list.filter(inter => {
if (
inter.title.indexOf(this.state.filter) === -1 &&
inter.path.indexOf(this.state.filter) === -1
) {
return false;
}
//arr.push('cat_' + inter.catid)
interfaceFilter = true;
return true;
});
arr.push('cat_' + item._id);
return interfaceFilter === true;
}
arr.push('cat_' + item._id);
return true;
});
// console.log('arr', arr);
if (arr.length > 0) {
currentKes.expands = arr;
}
menuList = this.filterList(this.state.list, arr);
} else {
menuList = this.state.list;
}
return (

@ -5,7 +5,7 @@ import PropTypes from 'prop-types';
import { Table, Icon, Row, Col, Tooltip, message } from 'antd';
import { Link } from 'react-router-dom';
import AceEditor from 'client/components/AceEditor/AceEditor';
import { formatTime } from '../../../../common.js';
import { formatTime, safeArray } from '../../../../common.js';
import ErrMsg from '../../../../components/ErrMsg/ErrMsg.js';
import variable from '../../../../constants/variable';
import constants from '../../../../constants/variable.js';
@ -244,7 +244,7 @@ class View extends Component {
flagMsg = (mock, strice) => {
if (mock && strice) {
return <span>( 全局mock & 严格模式 )</span>;
} else if (!mock && strice) {
} else if (!mock && strice) {
return <span>( 严格模式 )</span>;
} else if (mock && !strice) {
return <span>( 全局mock )</span>;
@ -372,6 +372,8 @@ class View extends Component {
methodColor = 'get';
}
const { tag, up_time, title, uid, username } = this.props.curData;
let res = (
<div className="caseContainer">
<h2 className="interface-title" style={{ marginTop: 0 }}>
@ -383,15 +385,15 @@ class View extends Component {
接口名称
</Col>
<Col span={8} className="colName">
{this.props.curData.title}
{title}
</Col>
<Col span={4} className="colKey">
&ensp;&ensp;
</Col>
<Col span={8} className="colValue">
<Link className="user-name" to={'/user/profile/' + this.props.curData.uid}>
<img src={'/api/user/avatar?uid=' + this.props.curData.uid} className="user-img" />
{this.props.curData.username}
<Link className="user-name" to={'/user/profile/' + uid}>
<img src={'/api/user/avatar?uid=' + uid} className="user-img" />
{username}
</Link>
</Col>
</Row>
@ -405,8 +407,19 @@ class View extends Component {
<Col span={4} className="colKey">
更新时间
</Col>
<Col span={8}>{formatTime(this.props.curData.up_time)}</Col>
<Col span={8}>{formatTime(up_time)}</Col>
</Row>
{safeArray(tag) &&
safeArray(tag).length > 0 && (
<Row className="row remark">
<Col span={4} className="colKey">
Tag
</Col>
<Col span={18} className="colValue">
{tag.join(' , ')}
</Col>
</Row>
)}
<Row className="row">
<Col span={4} className="colKey">
接口路径
@ -443,8 +456,6 @@ class View extends Component {
</Col>
<Col span={18} className="colValue">
{this.flagMsg(this.props.currProject.is_mock_open, this.props.currProject.strice)}
{/* {this.props.currProject.is_mock_open ? <span>( mock </span> : <span>( </span>}
{this.props.currProject.strice ? <span> & 严格模式 ) </span> : <span>) </span>} */}
<span
className="href"
onClick={() =>

@ -1,173 +1,169 @@
@import '../../../styles/mixin.scss';
.left-menu{
min-height: 5rem;
// background: #FFF;
// .item-all-interface {
// background-color: red;
// }
// .ant-tabs-bar{
// border-bottom: none;
// margin-bottom: 0
// }
.ant-tag {
margin-right: .16rem;
.left-menu {
min-height: 5rem;
// background: #FFF;
// .item-all-interface {
// background-color: red;
// }
// .ant-tabs-bar{
// border-bottom: none;
// margin-bottom: 0
// }
.ant-tag {
margin-right: 0.16rem;
}
.ant-tabs-nav {
width: 100%;
background-color: $color-bg-gray;
}
.ant-tabs-tab {
min-width: 49.4%;
}
.ant-tabs.ant-tabs-card > .ant-tabs-bar .ant-tabs-tab {
height: 39px;
background: #fff;
border-bottom: 0;
border-radius: 0;
}
.ant-tabs.ant-tabs-card > .ant-tabs-bar .ant-tabs-tab:nth-of-type(2) {
border-left: 0;
}
// .ant-tabs.ant-tabs-card > .ant-tabs-bar .ant-tabs-tab:last-of-type {
// border-right: 0;
// }
.ant-tabs.ant-tabs-card > .ant-tabs-bar .ant-tabs-tab-active {
height: 40px;
background-color: #ddd;
// color: $color-white;
}
.ant-tabs.ant-tabs-card > .ant-tabs-bar .ant-tabs-nav-container {
height: 40px;
}
.ant-tabs.ant-tabs-card > .ant-tabs-bar {
text-align: center;
// background: #ececec;
}
.ant-tabs-nav-wrap {
height: 40px;
line-height: 31px;
// border-bottom: 1px solid #d9d9d9;
}
.ant-input {
width: 100%;
}
.interface-filter {
padding: 12px 16px;
padding-right: 110px;
line-height: 32px;
background-color: #ddd;
position: relative;
}
.btn-filter {
position: absolute;
right: 16px;
top: 50%;
transform: translateY(-50%);
}
.ant-tree li .ant-tree-node-content-wrapper {
width: calc(100% - 28px);
position: relative;
.container-title {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ant-tabs-nav{
width:100%;
background-color: $color-bg-gray;
.btns {
background-color: #eef7fe;
position: absolute;
top: 50%;
right: 0;
transform: translateY(-60%);
transition: all 0.2s;
}
.ant-tabs-tab{
min-width: 49.4%;
}
.ant-tree li .ant-tree-node-selected {
.btns {
background-color: #d5ebfc;
transition: all 0.2s;
}
.ant-tabs.ant-tabs-card > .ant-tabs-bar .ant-tabs-tab{
height: 39px;
background: #fff;
border-bottom: 0;
border-radius: 0;
}
.ant-tabs.ant-tabs-card > .ant-tabs-bar .ant-tabs-tab:nth-of-type(2) {
border-left: 0;
}
// .ant-tabs.ant-tabs-card > .ant-tabs-bar .ant-tabs-tab:last-of-type {
// border-right: 0;
// }
.ant-tabs.ant-tabs-card > .ant-tabs-bar .ant-tabs-tab-active{
height: 40px;
background-color: #ddd;
// color: $color-white;
}
.interface-delete-icon {
position: relative;
right: 0;
float: right;
line-height: 25px;
width: 24px;
font-weight: bold;
}
.anticon-ellipsis {
transform: rotate(90deg);
}
.interface-delete-icon:hover {
color: #2395f1;
}
.interface-list {
//max-height: 600px;
//overflow-y: scroll;
.cat_switch_hidden {
.ant-tree-switcher {
visibility: hidden;
}
}
.ant-tabs.ant-tabs-card > .ant-tabs-bar .ant-tabs-nav-container{
height: 40px;
a {
color: rgba(13, 27, 62, 0.65);
}
.ant-tabs.ant-tabs-card > .ant-tabs-bar{
text-align: center;
// background: #ececec;
.btn-http {
height: 23px;
font-size: 10px;
margin-right: 7px;
padding: 0 5px;
width: auto !important;
}
.interface-item {
display: inline-block;
overflow: hidden;
top: 0px;
}
.interface-item-nav {
line-height: 25px;
}
.interface-list {
.cat_switch_hidden {
.ant-tree-switcher {
visibility: hidden;
}
}
.ant-tabs-nav-wrap{
height: 40px;
line-height: 31px;
// border-bottom: 1px solid #d9d9d9;
}
.ant-input {
width: 100%;
}
.interface-filter{
padding: 12px 16px;
padding-right: 110px;
line-height: 32px;
background-color: #ddd;
position: relative;
}
.btn-filter {
position: absolute;
right: 16px;
top: 50%;
transform: translateY(-50%);
}
.ant-tree li .ant-tree-node-content-wrapper {
width: calc(100% - 28px);
position: relative;
.container-title {
.btn-http {
height: 23px;
font-size: 10px;
margin-right: 7px;
padding: 0 5px;
width: auto !important;
}
.interface-item {
display: inline-block;
overflow: hidden;
text-overflow:ellipsis;
white-space: nowrap;
top: 0px;
line-height: 100%;
text-decoration: none;
}
.btns {
background-color: #eef7fe;
position: absolute;
top: 50%;
right: 0;
transform: translateY(-60%);
transition: all .2s;
}
}
.ant-tree li .ant-tree-node-selected {
.btns {
background-color: #d5ebfc;
transition: all .2s;
}
}
.interface-delete-icon{
position: relative;
right: 0;
float: right;
.interface-item-nav {
line-height: 25px;
width: 24px;
font-weight: bold;
}
.anticon-ellipsis {
transform: rotate(90deg);
}
.interface-delete-icon:hover {
color: #2395f1;
}
.interface-list{
//max-height: 600px;
//overflow-y: scroll;
.cat_switch_hidden{
.ant-tree-switcher{
visibility: hidden;
}
}
a{
color: rgba(13, 27, 62, 0.65);
}
.btn-http{
height: 23px;
font-size: 10px;
margin-right: 7px;
padding: 0 5px;
width:auto !important;
}
.interface-item{
display: inline-block;
overflow: hidden;
top: 0px;
line-height: 100%;
}
.interface-item-nav{
line-height:25px;
}
.interface-list{
.cat_switch_hidden{
.ant-tree-switcher{
visibility: hidden;
}
}
.btn-http{
height: 23px;
font-size: 10px;
margin-right: 7px;
padding: 0 5px;
width:auto !important;
}
.interface-item{
display: inline-block;
overflow: hidden;
top: 0px;
line-height: 100%;
text-decoration: none;
}
.interface-item-nav{
line-height:25px;
}
}
}
}
}
}
.right-content{
.right-content {
min-height: 5rem;
background: #fff;
.caseContainer {
@ -179,33 +175,45 @@
text-align: left;
background-color: #f8f8f8;
}
tr:nth-child(even){background: #f8f8f8;}
tr:nth-child(even) {
background: #f8f8f8;
}
}
.interface-content{
.ant-tabs-nav{
width:100%;
.interface-content {
.ant-tabs-nav {
width: 100%;
// background-color: #ddd;
// color: $color-white;
}
.ant-tabs-nav-wrap{
.ant-tabs-nav-wrap {
text-align: left;
}
}
.interface-title {
clear: both;
font-weight: normal;
margin-top: .48rem;
margin-bottom: .16rem;
margin-top: 0.48rem;
margin-bottom: 0.16rem;
border-left: 3px solid #2395f1;
padding-left: 8px;
.tooltip {
font-size: 13px;
font-weight: normal;
}
}
.container-radiogroup {
text-align: center;
margin-bottom: .16rem;
margin-bottom: 0.16rem;
}
.panel-sub {
background: rgba(236, 238, 241, 0.67);
padding: .10rem;
padding: 0.1rem;
.bulk-import {
color: #2395f1;
text-align: right;
margin-right: 16px;
cursor: pointer;
}
}
.ant-radio-button-wrapper-checked {
color: #fff;
@ -229,37 +237,41 @@
text-align: left;
font-weight: normal;
background-color: #f8f8f8;
text-indent: .4em;
text-indent: 0.4em;
}
tr {
text-indent: .4em;
text-indent: 0.4em;
}
th, td {
th,
td {
border: 1px solid #e9e9e9;
}
tr:nth-child(odd){background: #f8f8f8;}
tr:nth-child(even){background: #fff;}
tr:nth-child(odd) {
background: #f8f8f8;
}
tr:nth-child(even) {
background: #fff;
}
}
}
.addcatmodal{
.ant-modal-body{
padding: 10px 0px;
.ant-form-item{
margin-bottom: 10px;
}
.catModalfoot{
border-top: 1px solid #e9e9e9;
margin-bottom: 0px;
padding-top: 10px;
margin-top: 0px;
.ant-form-item-control-wrapper{
margin-left: 0px;
}
.ant-form-item-control {
float: right;
margin-right: 10px;
}
}
.addcatmodal {
.ant-modal-body {
padding: 10px 0px;
.ant-form-item {
margin-bottom: 10px;
}
}
.catModalfoot {
border-top: 1px solid #e9e9e9;
margin-bottom: 0px;
padding-top: 10px;
margin-top: 0px;
.ant-form-item-control-wrapper {
margin-left: 0px;
}
.ant-form-item-control {
float: right;
margin-right: 10px;
}
}
}
}

@ -23,6 +23,7 @@ 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');
@ -30,7 +31,6 @@ const RadioGroup = Radio.Group;
const importDataModule = {};
const exportDataModule = {};
const HandleImportData = require('common/HandleImportData');
function handleExportRouteParams(url, status, isWiki) {
if (!url) {
return;
@ -54,12 +54,14 @@ function handleExportRouteParams(url, status, isWiki) {
return {
curCatid: -(-state.inter.curdata.catid),
basePath: state.project.currProject.basepath,
updateLogList: state.news.updateLogList
updateLogList: state.news.updateLogList,
swaggerUrlData: state.project.swaggerUrlData
};
},
{
saveImportData,
fetchUpdateLogData
fetchUpdateLogData,
handleSwaggerUrlData
}
)
class ProjectData extends Component {
@ -84,7 +86,9 @@ class ProjectData extends Component {
basePath: PropTypes.string,
saveImportData: PropTypes.func,
fetchUpdateLogData: PropTypes.func,
updateLogList: PropTypes.array
updateLogList: PropTypes.array,
handleSwaggerUrlData: PropTypes.func,
swaggerUrlData: PropTypes.string
};
componentWillMount() {
@ -95,7 +99,6 @@ class ProjectData extends Component {
menuList: menuList,
selectCatid: menuList[0]._id
});
}
});
plugin.emitHook('import_data', importDataModule);
@ -252,13 +255,14 @@ class ProjectData extends Component {
if (this.state.selectCatid) {
this.setState({ showLoading: true });
try {
let content = await axios(this.state.swaggerUrl);
content = content.data;
let res = await importDataModule[this.state.curImportType].run(content);
// 处理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 {
} else {
// 未开启同步
await this.handleAddInterface(res);
}
@ -303,7 +307,11 @@ class ProjectData extends Component {
this.state.curExportType &&
exportDataModule[this.state.curExportType] &&
exportDataModule[this.state.curExportType].route;
let exportHref = handleExportRouteParams(exportUrl, this.state.exportContent, this.state.isWiki);
let exportHref = handleExportRouteParams(
exportUrl,
this.state.exportContent,
this.state.isWiki
);
// console.log('inter', this.state.exportContent);
return (
@ -313,7 +321,8 @@ class ProjectData extends Component {
<div className="dataImportCon">
<div>
<h3>
数据导入&nbsp;<a
数据导入&nbsp;
<a
target="_blank"
rel="noopener noreferrer"
href="https://yapi.ymfe.org/documents/data.html"
@ -325,7 +334,10 @@ class ProjectData extends Component {
</h3>
</div>
<div className="dataImportTile">
<Select defaultValue="swagger" placeholder="请选择导入数据的方式" onChange={this.handleImportType}>
<Select
placeholder="请选择导入数据的方式"
onChange={this.handleImportType}
>
{Object.keys(importDataModule).map(name => {
return (
<Option key={name} value={name}>
@ -337,7 +349,7 @@ class ProjectData extends Component {
</div>
<div className="catidSelect">
<Select
value={this.state.selectCatid+''}
value={this.state.selectCatid + ''}
showSearch
style={{ width: '100%' }}
placeholder="请选择数据导入的默认分类"
@ -358,16 +370,24 @@ class ProjectData extends Component {
</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>}>
数据同步&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>
@ -376,13 +396,14 @@ class ProjectData extends Component {
<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 导入">
开启url导入&nbsp;
<Tooltip title="swagger url 导入">
<Icon type="question-circle-o" />
</Tooltip>{' '}
&nbsp;&nbsp;
@ -463,7 +484,10 @@ class ProjectData extends Component {
{this.state.curExportType ? (
<div>
<p className="export-desc">{exportDataModule[this.state.curExportType].desc}</p>
<a target="_blank" href={exportHref}>
<a
target="_blank"
rel="noopener noreferrer"
href={exportHref}>
<Button className="export-button" type="primary" size="large">
{' '}
导出{' '}
@ -473,9 +497,10 @@ class ProjectData extends Component {
checked={this.state.isWiki}
onChange={this.handleWikiChange}
className="wiki-btn"
disabled = {this.state.curExportType === 'json'}
disabled={this.state.curExportType === 'json'}
>
添加wiki&nbsp;<Tooltip title="开启后 html 和 markdown 数据导出会带上wiki数据">
添加wiki&nbsp;
<Tooltip title="开启后 html 和 markdown 数据导出会带上wiki数据">
<Icon type="question-circle-o" />
</Tooltip>{' '}
</Checkbox>

@ -34,9 +34,10 @@ const RadioGroup = Radio.Group;
const RadioButton = Radio.Button;
import constants from '../../../../constants/variable.js';
const confirm = Modal.confirm;
import { nameLengthLimit, entries, trim } from '../../../../common';
import { nameLengthLimit, entries, trim, htmlFilter } from '../../../../common';
import '../Setting.scss';
import _ from 'underscore';
import ProjectTag from './ProjectTag.js';
// layout
const formItemLayout = {
labelCol: {
@ -106,12 +107,19 @@ class ProjectMessage extends Component {
const { form, updateProject, projectMsg, groupList } = this.props;
form.validateFields((err, values) => {
if (!err) {
let assignValue = Object.assign(projectMsg, values);
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) {
@ -121,13 +129,14 @@ class ProjectMessage extends Component {
// 如果如果项目所在的分组位置发生改变
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: assignValue.name
name: projectName
}
]);
}
@ -138,6 +147,10 @@ class ProjectMessage extends Component {
});
};
tagSubmit = tag => {
this.tag = tag;
};
showConfirm = () => {
let that = this;
confirm({
@ -207,7 +220,6 @@ class ProjectMessage extends Component {
async componentWillMount() {
await this.props.fetchGroupList();
// await this.props.getProject(this.props.projectId);
await this.props.fetchGroupMsg(this.props.projectMsg.group_id);
}
@ -221,8 +233,28 @@ class ProjectMessage extends Component {
(location.port !== '' ? ':' + location.port : '') +
`/mock/${projectMsg._id}${projectMsg.basepath}+$接口请求路径`;
let initFormValues = {};
const { name, basepath, desc, project_type, group_id, switch_notice, strice, is_json5 } = projectMsg;
initFormValues = { name, basepath, desc, project_type, group_id, switch_notice, strice , is_json5};
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 = (
@ -358,6 +390,21 @@ class ProjectMessage extends Component {
]
})(<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={
@ -412,10 +459,11 @@ class ProjectMessage extends Component {
<span className="radio-desc">只有组长和项目开发者可以索引并查看项目信息</span>
</Radio>
<br />
<Radio value="public" className="radio">
{projectMsg.role === 'admin' && <Radio value="public" className="radio">
<Icon type="unlock" />公开<br />
<span className="radio-desc">任何人都可以索引并查看项目信息</span>
</Radio>
</Radio>}
</RadioGroup>
)}
</FormItem>

@ -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;

@ -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;
}
}

@ -1,5 +1,6 @@
import axios from 'axios';
import variable from '../../constants/variable';
import {htmlFilter} from '../../common';
// Actions
const FETCH_PROJECT_LIST = 'yapi/project/FETCH_PROJECT_LIST';
@ -20,7 +21,7 @@ const CHECK_PROJECT_NAME = 'yapi/project/CHECK_PROJECT_NAME';
const COPY_PROJECT_MSG = 'yapi/project/COPY_PROJECT_MSG';
const PROJECT_GET_ENV = 'yapi/project/PROJECT_GET_ENV';
const CHANGE_MEMBER_EMAIL_NOTICE = 'yapi/project/CHANGE_MEMBER_EMAIL_NOTICE';
const GET_SWAGGER_URL_DATA = 'yapi/project/GET_SWAGGER_URL_DATA'
// Reducer
const initialState = {
isUpdateModalShow: false,
@ -39,7 +40,8 @@ const initialState = {
header: []
}
]
}
},
swaggerUrlData: ''
};
export default (state = initialState, action) => {
@ -97,6 +99,13 @@ export default (state = initialState, action) => {
...state
};
}
case GET_SWAGGER_URL_DATA: {
return {
...state,
swaggerUrlData: action.payload.data.data
}
}
default:
return state;
}
@ -173,7 +182,7 @@ export function getProjectMemberList(id) {
// }
export function addProject(data) {
const {
let {
name,
prd_host,
basepath,
@ -185,6 +194,9 @@ export function addProject(data) {
color,
project_type
} = data;
// 过滤项目名称中有html标签存在的情况
name = htmlFilter(name);
const param = {
name,
prd_host,
@ -205,7 +217,10 @@ export function addProject(data) {
// 修改项目
export function updateProject(data) {
const { name, project_type, basepath, desc, _id, env, group_id, switch_notice, strice, is_json5 } = data;
let { name, project_type, basepath, desc, _id, env, group_id, switch_notice, strice, is_json5, tag } = data;
// 过滤项目名称中有html标签存在的情况
name = htmlFilter(name);
const param = {
name,
project_type,
@ -216,7 +231,8 @@ export function updateProject(data) {
env,
group_id,
strice,
is_json5
is_json5,
tag
};
return {
type: PROJECT_UPDATE,
@ -312,3 +328,10 @@ export async function checkProjectName(name, group_id) {
})
};
}
export async function handleSwaggerUrlData(url) {
return {
type: GET_SWAGGER_URL_DATA,
payload: axios.get('/api/project/swagger_url?url='+url)
};
}

@ -64,6 +64,7 @@ async function handle(
let existNum = 0;
if (len === 0) {
messageError(`解析数据为空`);
callback({ showLoading: false });
return;
}
for (let index = 0; index < res.length; index++) {

@ -115,6 +115,14 @@ module.exports = function(jsondiffpatch, formattersHtml, curDiffData) {
content: diffText(valueMaps[old.status], valueMaps[current.status])
});
}
if (current.tag !== old.tag) {
diffView.push({
title: '接口tag',
content: diffText(old.tag, current.tag)
});
}
diffView.push({
title: 'Request Path Params',
content: diffArray(old.req_params, current.req_params)

@ -18,6 +18,9 @@ function getLength(object) {
function Compare(objA, objB) {
if (!isObj(objA) && !isObj(objB)) {
if (isArray(objA) && isArray(objB)) {
return CompareArray(objA, objB, true);
}
return objA == objB;
}
if (!isObj(objA) || !isObj(objB)) return false;
@ -25,6 +28,18 @@ function Compare(objA, objB) {
return CompareObj(objA, objB, true);
}
function CompareArray(objA, objB, flag) {
if (objA.length != objB.length) return false;
for (let i in objB) {
if (!Compare(objA[i], objB[i])) {
flag = false;
break;
}
}
return flag;
}
function CompareObj(objA, objB, flag) {
for (var key in objA) {
if (!flag) break;
@ -60,6 +75,7 @@ function CompareObj(objA, objB, flag) {
exports.jsonEqual = Compare;
exports.isDeepMatch = function(obj, properties) {
if (!properties || typeof properties !== 'object' || Object.keys(properties).length === 0) {
return true;
}

@ -285,6 +285,7 @@ async function crossRequest(defaultOptions, preScript, afterScript) {
}
resolve(data);
};
window.crossRequest(options);
});
}
@ -421,7 +422,6 @@ function handleParams(interfaceData, handleValue, requestParams) {
requestOptions.file = 'single-file';
}
}
return requestOptions;
}

@ -1,4 +1,5 @@
const _ = require('underscore');
let fieldNum = 1;
exports.schemaTransformToTable = schema => {
try {
@ -112,7 +113,8 @@ const SchemaString = data => {
minLength: data.minLength,
enum: data.enum,
enumDesc: data.enumDesc,
format: data.format
format: data.format,
mock: data.mock && data.mock.mock
};
return item;
};
@ -121,6 +123,12 @@ const SchemaArray = (data, index) => {
data.items = data.items || { type: 'string' };
let items = checkJsonSchema(data.items);
let optionForm = mapping(items, index);
// 处理array嵌套array的问题
let children =optionForm ;
if (!_.isArray(optionForm) && !_.isUndefined(optionForm)) {
optionForm.key = 'array-' + fieldNum++;
children = [optionForm];
}
let item = {
desc: data.description,
@ -129,7 +137,7 @@ const SchemaArray = (data, index) => {
uniqueItems: data.uniqueItems,
maxItems: data.maxItems,
itemType: items.type,
children: optionForm
children
};
if (items.type === 'string') {
item = Object.assign({}, item, { itemFormat: items.format });
@ -145,7 +153,8 @@ const SchemaNumber = data => {
default: data.default,
format: data.format,
enum: data.enum,
enumDesc: data.enumDesc
enumDesc: data.enumDesc,
mock: data.mock && data.mock.mock
};
return item;
};
@ -158,7 +167,8 @@ const SchemaInt = data => {
default: data.default,
format: data.format,
enum: data.enum,
enumDesc: data.enumDesc
enumDesc: data.enumDesc,
mock: data.mock && data.mock.mock
};
return item;
};
@ -167,7 +177,8 @@ const SchemaBoolean = data => {
let item = {
desc: data.description,
default: data.default,
enum: data.enum
enum: data.enum,
mock: data.mock && data.mock.mock
};
return item;
};
@ -175,7 +186,8 @@ const SchemaBoolean = data => {
const SchemaOther = data => {
let item = {
desc: data.description,
default: data.default
default: data.default,
mock: data.mock && data.mock.mock
};
return item;
};

@ -3,7 +3,11 @@
* [安装](index.md#安装)
* [服务器管理](index.md#服务器管理)
* [升级](index.md#升级)
---
* [mongodb集群](index.md#mongodb集群)
* [配置邮箱](index.md#配置邮箱)
* [配置LDAP登录](index.md#配置LDAP登录)
* [禁止注册](index.md#禁止注册)
* [版本通知](index.md#版本通知)
* [版本通知](index.md#版本通知)

@ -112,7 +112,7 @@ node server/app.js //启动服务器后,请访问 127.0.0.1:{config.json配置
"mail": {...},
"ldapLogin": {
"enable": true,
"server": "ldap://l-ldapt1.ops.dev.cn0.qunar.com",
"server": "ldap://l-ldapt1.com",
"baseDn": "CN=Admin,CN=Users,DC=test,DC=com",
"bindPassword": "password123",
"searchDn": "OU=UserContainer,DC=test,DC=com",
@ -128,8 +128,8 @@ node server/app.js //启动服务器后,请访问 127.0.0.1:{config.json配置
- `enable` 表示是否配置 LDAP 登录true(支持 LDAP登录 )/false(不支持LDAP登录);
- `server ` LDAP 服务器地址,前面需要加上 ldap:// 前缀,也可以是 ldaps:// 表示是通过 SSL 连接;
- `baseDn` LDAP 服务器的登录用户名,必须是从根结点到用户节点的全路径;
- `bindPassword` 登录该 LDAP 服务器的密码;
- `baseDn` LDAP 服务器的登录用户名,必须是从根结点到用户节点的全路径(非必须);
- `bindPassword` 登录该 LDAP 服务器的密码(非必须);
- `searchDn` 查询用户数据的路径,类似数据库中的一张表的地址,注意这里也必须是全路径;
- `searchStandard` 查询条件,这里是 mail 表示查询用户信息是通过邮箱信息来查询的。注意该字段信息与LDAP数据库存储数据的字段相对应如果如果存储用户邮箱信息的字段是 email, 这里就需要修改成 email.1.3.18+支持自定义filter表达式基本形式为&(objectClass=user)(cn=%s), 其中%s会被username替换
- `emailPostfix` 登陆邮箱后缀(非必须)
@ -163,4 +163,24 @@ node server/app.js //启动服务器后,请访问 127.0.0.1:{config.json配置
"versionNotify": true
}
```
```
### 如何配置mongodb集群
请升级到 yapi >= **1.4.0**以上版本,然后在 config.json db项配置 connectString:
```json
{
"port": "***",
"db": {
"connectString": "mongodb://127.0.0.100:8418,127.0.0.101:8418,127.0.0.102:8418/yapidb?slaveOk=true",
"user": "******",
"pass": "******"
},
}
```
详细配置参考: [wiki](https://mongoosejs.com/docs/connections.html#multiple_connections)

@ -8,6 +8,7 @@
### 进阶篇
* [权限](manage.md)
* [项目操作](project.md)
* [基本设置](project.md#基本设置)
* [新建项目](project.md#新建项目)
* [修改项目](project.md#修改项目)
* [项目迁移](project.md#项目迁移)

@ -11,9 +11,9 @@ Mock 期望就是根据设置的请求过滤规则,返回期望数据。
<div class="doc-img-wrapper"><img class="doc-img-r" src="./images/usage/adv-mock-case1.png"/></div>
2. 点击『添加期望』,填写过滤规则以及期望返回数据,点击『确定』保存。
<div class="doc-img-wrapper"><img class="doc-img-r" src="./images/usage/adv-mock-case3.png"/></div>
<div class="doc-img-wrapper"><img class="doc-img-r" src="./images/usage/adv-mock-case4.png"/></div>
<div class="doc-img-wrapper"><img class="doc-img-r" src="./images/usage/adv-mock-case4-new.png"/></div>
3. 然后尝试在浏览器里发送符合规则的请求,查看返回的数据是否符合期望。
<div class="doc-img-wrapper"><img class="doc-img-r" src="./images/usage/adv-mock-case5.png"/></div>
<div class="doc-img-wrapper"><img class="doc-img-r" src="./images/usage/adv-mock-case5-new.png"/></div>
### 期望填写

@ -15,6 +15,7 @@
- 接口路径:可以更改 HTTP 请求方式,并且支持 restful 动态路由,例如 /api/{id}/{name}, id和name是动态参数
- 选择分类:可以更改接口所在分类
- 状态:用于标识接口是否开发完成。
- Tag用于标识接口tag信息v1.3.23+,在接口list页可以根据tag过滤接口
<img src="./images/baseSet.png" />

@ -2,35 +2,46 @@
在数据管理可快速导入其他格式的接口数据方便快速添加接口。YApi 目前支持 postman, swagger, har 数据导入。
v1.3.23+ 增加数据导入的3种同步方式 normal, good, mergin
1. 普通模式(normal):不导入已存在的接口;
2. 智能合并(good):已存在的接口,将合并返回数据的 response适用于导入了 swagger 数据保留对数据结构的改动例如用户对字段code 添加了mock信息, 当再次数据导入的时候 mock 字段将不会被覆盖
3. 完全覆盖(mergin):不保留旧数据,完全使用新数据,适用于接口定义完全交给后端定义, 默认为 normal
## Postman 数据导入
1.首先在postman导出接口
1.首先在 postman 导出接口
<div><img class="doc-img" style="width:50%" src="./images/usage/postman-1.jpg" /></div>
2.选择collection_v1,点击export导出接口到文件xxx
2.选择 collection_v1,点击 export 导出接口到文件 xxx
<div><img class="doc-img" style="width:70%" src="./images/usage/postman-2.jpg" /></div>
3.打开yapi平台进入到项目页面点击数据管理选择相应的分组和postman导入方式,选择刚才保存的文件路径,开始导入数据
3.打开 yapi 平台,进入到项目页面,点击数据管理,选择相应的分组和 postman 导入  方式, 选择刚才保存的文件路径,开始导入数据
<div><img class="doc-img" style="width:90%" src="./images/usage/postman-3.jpg" /></div>
## HAR 数据导入
<p>可用 chrome 实现录制接口数据的功能,方便开发者快速导入项目接口</p>
1.打开 Chrome 浏览器开发者工具点击network首次使用请先clear所有请求信息确保录制功能开启红色为开启状态
1.打开 Chrome 浏览器开发者工具,点击 network首次使用请先 clear 所有请求信息,确保录制功能开启(红色为开启状态)
<div><img class="doc-img" style="width:70%" src="./images/usage/chrome-1.jpg" /></div>
2.操作页面实际功能完成后点击save as HAR with content,将数据保存到文件xxx
2.操作页面实际功能,完成后点击 save as HAR with content,将数据保存到文件 xxx
<div><img class="doc-img" style="width:70%" src="./images/usage/chrome-2.jpg" /></div>
3.打开yapi平台进入到项目页面点击数据管理选择相应的分组和har导入方式,选择刚才保存的文件路径,开始导入数据
3.打开 yapi 平台,进入到项目页面,点击数据管理,选择相应的分组和 har 导入  方式, 选择刚才保存的文件路径,开始导入数据
<div><img class="doc-img" style="width:50%" src="./images/usage/chrome-3.jpg" /></div>
> Tips: har 数据导入只支持 response.content.mimeType 为 application/json 类型的数据
## Swagger 数据导入
<p>什么是 Swagger </p>
<div>[Swagger从入门到精通](https://www.gitbook.com/book/huangwenchao/swagger/details)</div>
@ -38,9 +49,9 @@
1.生成 JSON 语言编写的 Swagger API 文档文件<div> 例如这样的数据 <a href="http://petstore.swagger.io/v2/swagger.json" target="blank">http://petstore.swagger.io/v2/swagger.json</a>),可以将其内容复制到 JSON 文件中。</div>
<br />
> Tips: v1.3.19 版本开始支持swagger url 导入功能
> Tips: v1.3.19 版本开始支持 swagger url 导入功能
2.打开yapi平台进入到项目页面点击数据管理选择相应的分组和swagger导入方式,选择刚才的文件,开始导入数据
2.打开 yapi 平台,进入到项目页面,点击数据管理,选择相应的分组和 swagger 导入  方式, 选择刚才的文件,开始导入数据
<div><img class="doc-img" style="width:50%" src="./images/usage/chrome-4.jpg" /></div>
@ -48,16 +59,14 @@
<div><img class="doc-img" style="width:90%" src="./images/usage/chrome-6.jpg" /></div>
## YApi 接口 JSON 数据导入
## YApi接口JSON数据导入
该功能在 v1.3.12 版本上线,可导入在 yapi 平台导出的 json 接口数据。
![](import-json-data.png)
## 通过命令行导入接口数据
YApi 支持通过命令行导入接口数据,他的应用场景是做自动化集成,比如配合 swagger ,接口文档前端不用维护,交由后端生成。
### 使用方法
@ -69,25 +78,29 @@ npm install -g yapi-cli
```
第二步,在任意一个目录下新建配置文件 `yapi-import.json`,内容如下:
```json
{
"type": "swagger",
"token": "17fba0027f300248b804",
"file": "swagger.json",
"merge": false,
"merge": "normal",
"server": "http://yapi.local.qunar.com:3000"
}
```
`type` 是数据数据方式,目前官方只支持 swagger
`token` 是项目token`项目设置 -> token` 设置获取
`token` 是项目 token`项目设置 -> token` 设置获取
`file` 是 swagger 接口文档文件,可使用绝对路径或 url
`merge` 是否覆盖旧的接口,默认不开启,配置 `true` 开启
`merge` 有三种导入方式(v1.3.23+支持) normal, good, mergin
1. 普通模式(normal):不导入已存在的接口;
2. 智能合并(good):已存在的接口,将合并返回数据的 response适用于导入了 swagger 数据,保留对数据结构的改动;
3. 完全覆盖(mergin):不保留旧数据,完全使用新数据,适用于接口定义完全交给后端定义, 默认为 normal
`server` 是yapi服务器地址
`server` yapi 服务器地址
第三步,在`新建配置文件的当前目录`,执行下面指令

Binary file not shown.

Before

(image error) Size: 35 KiB

After

(image error) Size: 46 KiB

Binary file not shown.

After

(image error) Size: 11 KiB

Binary file not shown.

After

(image error) Size: 22 KiB

Binary file not shown.

After

(image error) Size: 42 KiB

Binary file not shown.

After

(image error) Size: 102 KiB

Binary file not shown.

After

(image error) Size: 46 KiB

@ -67,19 +67,29 @@
详细使用文档请查看:<a href="http://mockjs.com/examples.html">Mockjs 官网</a>
详细使用文档请查看:<a href="http://mockjs.com/examples.html" target="_blank">Mockjs 官网</a>
## 方式2. json-schema
<img src="./images/usage/json-schema-demo.jpg" />
开启 json-schema 功能后,将不再使用 mockjs 解析定义的返回数据,而是根据 json-schema 定义的数据结构,生成随机数据。
开启 json-schema 功能后,根据 json-schema 定义的数据结构,生成随机数据。
### 如何生成随机的邮箱或 ip
### 如何生成随机的邮箱或 ip(该方法在v1.3.22之后不再适用)
<img src="./images/usage/json-schema-mock.jpg" />
点击高级设置,选择 `format` 选项,比如选择 `email` 则该字段生成随机邮箱字符串。
### 集成 mockjs
基本书写方式为 mock 的数据占位符@xxx, 具体字段详见<a href="http://mockjs.com/examples.html" target="_blank">Mockjs 官网</a>
<img src="./images/schema-mock-2.png" />
<img src="./images/schema-mock-1.png" />
> 如果不是以@字符开头的话或者匹配不到Mockjs中的占位符就会直接生成输入的值
## 如何使用 Mock

@ -26,7 +26,7 @@ var hooks = {
},
/**
* 客户端删除接口成功后触发
* @param data 删除接口的详细信息
* @param data 接口id
*/
interface_del: {
type: 'multi',

@ -1,5 +1,13 @@
# 项目操作
## 基本设置
- tag 信息可自定义tag名称和tag描述tag信息可用在接口tag标识中;
- mock 严格模式:开启后 mock 请求会对 querybody form 的必须字段和 json schema 进行校验;
- 开启json5开启后允许接口请求body 和返回值中写 json 字段。yapi建议用户关闭 json5 因为json-schema 格式可以进行接口格式校验。
<img src="./images/usage/project-message.png" />
## 新建项目
点击右上角的 `+` 新建项目,进入新建项目页面。

@ -69,13 +69,13 @@ class advMockController extends baseController {
let userinfo = await this.userModel.findById(result[i].uid);
result[i] = result[i].toObject();
// if (userinfo) {
result[i].username = userinfo.username;
result[i].username = userinfo.username;
// }
}
ctx.body = yapi.commons.resReturn(result);
} catch (err) {
ctx.body = yapi.commons.resReturn(null, 400, err.message)
ctx.body = yapi.commons.resReturn(null, 400, err.message);
}
}
@ -137,8 +137,6 @@ class advMockController extends baseController {
for (let i in data.params) {
findRepeatParams['params.' + i] = data.params[i];
}
} else {
findRepeatParams.params = null;
}
if (data.ip_enable) {
@ -146,6 +144,7 @@ class advMockController extends baseController {
}
findRepeat = await this.caseModel.get(findRepeatParams);
if (findRepeat && findRepeat._id !== params.id) {
return (ctx.body = yapi.commons.resReturn(null, 400, '已存在的期望'));
}
@ -171,17 +170,16 @@ class advMockController extends baseController {
async hideCase(ctx) {
let id = ctx.request.body.id;
let enable = ctx.request.body.enable
let enable = ctx.request.body.enable;
if (!id) {
return (ctx.body = yapi.commons.resReturn(null, 408, '缺少 id'));
}
let data = {
id,
case_enable: enable
}
};
let result = await this.caseModel.up(data);
return (ctx.body = yapi.commons.resReturn(result));
}
}

@ -1,34 +1,28 @@
// import {message} from 'antd'
function exportData(exportDataModule,pid){
exportDataModule.html = {
name: 'html',
route: `/api/plugin/export?type=html&pid=${pid}`,
desc: '导出项目接口文档为 html 文件'
}
exportDataModule.markdown = {
name: 'markdown',
route: `/api/plugin/export?type=markdown&pid=${pid}`,
desc: '导出项目接口文档为 markdown 文件'
},
exportDataModule.json = {
name: 'json',
route: `/api/plugin/export?type=json&pid=${pid}`,
desc: '导出项目接口文档为 json 文件,可使用该文件导入接口数据'
}
// exportDataModule.pdf = {
// name: 'pdf',
// route: `/api/plugin/export?type=pdf&pid=${pid}`,
// desc: '导出项目接口文档为 pdf 文件'
// }
function exportData(exportDataModule, pid) {
exportDataModule.html = {
name: 'html',
route: `/api/plugin/export?type=html&pid=${pid}`,
desc: '导出项目接口文档为 html 文件'
};
(exportDataModule.markdown = {
name: 'markdown',
route: `/api/plugin/export?type=markdown&pid=${pid}`,
desc: '导出项目接口文档为 markdown 文件'
}),
(exportDataModule.json = {
name: 'json',
route: `/api/plugin/export?type=json&pid=${pid}`,
desc: '导出项目接口文档为 json 文件,可使用该文件导入接口数据'
});
// exportDataModule.pdf = {
// name: 'pdf',
// route: `/api/plugin/export?type=pdf&pid=${pid}`,
// desc: '导出项目接口文档为 pdf 文件'
// }
}
module.exports = function(){
this.bindHook('export_data', exportData)
}
module.exports = function() {
this.bindHook('export_data', exportData);
};

@ -65,7 +65,11 @@ const swagger = require('swagger-client');
async function run(res) {
let interfaceData = { apis: [], cats: [] };
if(typeof res === 'string' && res){
res = JSON.parse(res);
try{
res = JSON.parse(res);
} catch (e) {
console.error('json 解析出错',e.message)
}
}
isOAS3 = res.openapi && res.openapi === '3.0.0';

@ -21,7 +21,7 @@ class statisMockModel extends baseModel {
}
countByGroupId(id){
return this.model.count({
return this.model.countDocuments({
group_id: id
})
}
@ -32,7 +32,7 @@ class statisMockModel extends baseModel {
}
getTotalCount() {
return this.model.count({});
return this.model.countDocuments({});
}
getDayCount(timeInterval) {
@ -62,7 +62,7 @@ class statisMockModel extends baseModel {
up(id, data) {
data.up_time = yapi.commons.time();
return this.model.update({
return this.model.updateOne({
_id: id
}, data, { runValidators: true });
}

10042
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -1,6 +1,6 @@
{
"name": "yapi",
"version": "1.3.22",
"version": "1.4.0",
"description": "YAPI",
"main": "index.js",
"scripts": {
@ -62,8 +62,9 @@
"markdown-it-table-of-contents": "0.3.2",
"md5": "2.2.1",
"mockjs": "1.0.1-beta3",
"moment": "2.18.1",
"mongoose": "4.7.0",
"moment": "^2.19.3",
"mongodb": "^3.1.8",
"mongoose": "5.3.2",
"mongoose-auto-increment": "5.0.1",
"moox": "^1.0.2",
"nodemailer": "4.0.1",
@ -109,7 +110,7 @@
"extract-text-webpack-plugin": "2.0.0",
"ghooks": "^2.0.0",
"happypack": "^4.0.0-beta.5",
"json-schema-editor-visual": "^1.0.20",
"json-schema-editor-visual": "^1.0.21",
"less": "^2.7.2",
"less-loader": "^4.0.5",
"markdown-it-include": "^1.0.0",

@ -24,7 +24,7 @@ app.proxy = true;
yapi.app = app;
// app.use(bodyParser({multipart: true}));
app.use(koaBody({ multipart: true }));
app.use(koaBody({ multipart: true, jsonLimit: '2mb', formLimit: '1mb', textLimit: '1mb' }));
app.use(mockServer);
app.use(router.routes());
app.use(router.allowedMethods());
@ -55,5 +55,7 @@ app.use(koaStatic(yapi.path.join(yapi.WEBROOT, 'static'), { index: indexFile, gz
app.listen(yapi.WEBCONFIG.port);
commons.log(
`服务已启动,请打开下面链接访问: \nhttp://127.0.0.1${yapi.WEBCONFIG.port == '80' ? '' : ':' + yapi.WEBCONFIG.port}/`
`服务已启动,请打开下面链接访问: \nhttp://127.0.0.1${
yapi.WEBCONFIG.port == '80' ? '' : ':' + yapi.WEBCONFIG.port
}/`
);

@ -125,12 +125,20 @@ class groupController extends baseController {
async add(ctx) {
let params = ctx.params;
if (this.getRole() !== 'admin') {
return (ctx.body = yapi.commons.resReturn(null, 401, '没有权限'));
}
// 新版每个人都有权限添加分组
// if (this.getRole() !== 'admin') {
// return (ctx.body = yapi.commons.resReturn(null, 401, '没有权限'));
// }
let owners = [];
if(params.owner_uids.length === 0){
params.owner_uids.push(
this.getUid()
)
}
if (params.owner_uids) {
for (let i = 0, len = params.owner_uids.length; i < len; i++) {
let id = params.owner_uids[i];

@ -105,7 +105,8 @@ class interfaceController extends baseController {
method: minLengthStringField,
catid: 'number',
switch_notice: 'boolean',
message: minLengthStringField
message: minLengthStringField,
tag: 'array'
},
addAndUpCommonField
),
@ -192,13 +193,13 @@ class interfaceController extends baseController {
});
});
let checkRepeat = await this.Model.checkRepeat(params.project_id, params.path, params.method);
let checkRepeat = await this.Model.checkRepeat(params.project_id, http_path.pathname, params.method);
if (checkRepeat > 0) {
return (ctx.body = yapi.commons.resReturn(
null,
40022,
'已存在的接口:' + params.path + '[' + params.method + ']'
'已存在的接口:' + http_path.pathname + '[' + params.method + ']'
));
}
@ -375,7 +376,6 @@ class interfaceController extends baseController {
if (userinfo) {
result.username = userinfo.username;
}
ctx.body = yapi.commons.resReturn(result);
} catch (e) {
ctx.body = yapi.commons.resReturn(null, 402, e.message);
@ -567,9 +567,9 @@ class interfaceController extends baseController {
},
params
);
let http_path;
if (params.path) {
let http_path = url.parse(params.path, true);
http_path = url.parse(params.path, true);
if (!yapi.commons.verifyPath(http_path.pathname)) {
return (ctx.body = yapi.commons.resReturn(
@ -596,14 +596,14 @@ class interfaceController extends baseController {
) {
let checkRepeat = await this.Model.checkRepeat(
interfaceData.project_id,
params.path,
http_path.pathname,
params.method
);
if (checkRepeat > 0) {
return (ctx.body = yapi.commons.resReturn(
null,
401,
'已存在的接口:' + params.path + '[' + params.method + ']'
'已存在的接口:' + http_path.pathname + '[' + params.method + ']'
));
}
}
@ -616,7 +616,6 @@ class interfaceController extends baseController {
data.req_params = [];
}
}
let result = await this.Model.up(id, data);
let username = this.getUsername();
let CurrentInterfaceData = await this.Model.get(id);
@ -645,7 +644,6 @@ class interfaceController extends baseController {
});
this.projectModel.up(interfaceData.project_id, { up_time: new Date().getTime() }).then();
if (params.switch_notice === true) {
let diffView = showDiffMsg(jsondiffpatch, formattersHtml, logData);
let annotatedCss = fs.readFileSync(
@ -661,6 +659,7 @@ class interfaceController extends baseController {
);
let project = await this.projectModel.getBaseInfo(interfaceData.project_id);
let interfaceUrl = `http://${ctx.request.host}/project/${
interfaceData.project_id
}/interface/api/${id}`;
@ -733,7 +732,7 @@ class interfaceController extends baseController {
// let inter = await this.Model.get(id);
let result = await this.Model.del(id);
yapi.emitHook('interface_del', data).then();
yapi.emitHook('interface_del', id).then();
await this.caseModel.delByInterfaceId(id);
let username = this.getUsername();
this.catModel.get(data.catid).then(cate => {
@ -902,7 +901,7 @@ class interfaceController extends baseController {
interfaceData.forEach(async item => {
try {
yapi.emitHook('interface_del', item).then();
yapi.emitHook('interface_del', item._id).then();
await this.caseModel.delByInterfaceId(item._id);
} catch (e) {
yapi.commons.log(e.message, 'error');

@ -64,8 +64,8 @@ class openController extends baseController {
json: 'string',
project_id: 'string',
merge: {
type: 'boolean',
default: false
type: 'string',
default: 'normal'
}
}
};
@ -97,7 +97,7 @@ class openController extends baseController {
let successMessage;
let errorMessage = [];
let data = await HanldeImportData(
await HanldeImportData(
res,
project_id,
selectCatid,

@ -12,6 +12,7 @@ const userModel = require('../models/user.js');
const logModel = require('../models/log.js');
const followModel = require('../models/follow.js');
const tokenModel = require('../models/token.js');
const url = require('url');
const sha = require('sha.js');
@ -257,6 +258,7 @@ class projectController extends baseController {
username: username,
typeid: result._id
});
yapi.emitHook('project_add', result).then();
ctx.body = yapi.commons.resReturn(result);
}
@ -534,7 +536,7 @@ class projectController extends baseController {
}
result.role = await this.getProjectRole(params.id, 'project');
yapi.emitHook('project_add', params.id).then();
yapi.emitHook('project_get', result).then();
ctx.body = yapi.commons.resReturn(result);
}
@ -826,6 +828,7 @@ class projectController extends baseController {
username: username,
typeid: id
});
yapi.emitHook('project_up', result).then();
ctx.body = yapi.commons.resReturn(result);
} catch (e) {
ctx.body = yapi.commons.resReturn(null, 402, e.message);
@ -889,6 +892,58 @@ class projectController extends baseController {
}
}
/**
* 编辑项目
* @interface /project/up_tag
* @method POST
* @category project
* @foldnumber 10
* @param {Number} id 项目id不能为空
* @param {Array} [tag] 项目tag配置
* @param {String} [tag[].name] tag名称
* @param {String} [tag[].desc] tag描述
* @returns {Object}
* @example
*/
async upTag(ctx) {
try {
let id = ctx.request.body.id;
let params = ctx.request.body;
if (!id) {
return (ctx.body = yapi.commons.resReturn(null, 405, '项目id不能为空'));
}
if ((await this.checkAuth(id, 'project', 'edit')) !== true) {
return (ctx.body = yapi.commons.resReturn(null, 405, '没有权限'));
}
if (!params.tag || !Array.isArray(params.tag)) {
return (ctx.body = yapi.commons.resReturn(null, 405, 'tag参数格式有误'));
}
let projectData = await this.Model.get(id);
let data = {
up_time: yapi.commons.time()
};
data.tag = params.tag;
let result = await this.Model.up(id, data);
let username = this.getUsername();
yapi.commons.saveLog({
content: `<a href="/user/profile/${this.getUid()}">${username}</a> 更新了项目 <a href="/project/${id}/interface/api">${
projectData.name
}</a> tag`,
type: 'project',
uid: this.getUid(),
username: username,
typeid: id
});
ctx.body = yapi.commons.resReturn(result);
} catch (e) {
ctx.body = yapi.commons.resReturn(null, 402, e.message);
}
}
/**
* 获取项目的环境变量值
* @interface /project/get_env
@ -1057,6 +1112,18 @@ class projectController extends baseController {
return (ctx.body = yapi.commons.resReturn(queryList, 0, 'ok'));
}
// 输入 swagger url 的时候node端请求数据
async swaggerUrl(ctx) {
try {
let ops = url.parse(ctx.request.query.url);
let result = await yapi.commons.createWebAPIRequest(ops);
ctx.body = yapi.commons.resReturn(result);
} catch (err) {
ctx.body = yapi.commons.resReturn(null, 402, err.message);
}
}
}
module.exports = projectController;

@ -147,8 +147,8 @@ class interfaceColController extends baseController {
*/
async testDelete(ctx) {
try {
let params = ctx.request.query;
ctx.body = yapi.commons.resReturn(params);
let body = ctx.request.body;
ctx.body = yapi.commons.resReturn(body);
} catch (e) {
ctx.body = yapi.commons.resReturn(null, 402, e.message);
}
@ -226,9 +226,12 @@ class interfaceColController extends baseController {
*/
async testResponse(ctx) {
try {
let result = `<div><h2>12222222</h2></div>`;
// let result = `<div><h2>12222222</h2></div>`;
// let result = `wieieieieiieieie`
// let result = { b: '12', c: '23' };
let result = { b: '12', c: '23' };
ctx.set('Access-Control-Allow-Origin', '*');
ctx.set('Content-Type', 'text');
console.log(ctx.response);
ctx.body = result;
} catch (e) {
ctx.body = yapi.commons.resReturn(null, 402, e.message);

@ -139,6 +139,7 @@ module.exports = async (ctx, next) => {
// let hostname = ctx.hostname;
// let config = yapi.WEBCONFIG;
let path = ctx.path;
let header = ctx.request.header;
if (path.indexOf('/mock/') !== 0) {
if (next) await next();
@ -150,7 +151,10 @@ module.exports = async (ctx, next) => {
paths.splice(0, 3);
path = '/' + paths.join('/');
ctx.set('Access-Control-Allow-Origin', '*');
ctx.set('Access-Control-Allow-Origin', header.origin);
ctx.set('Access-Control-Allow-Credentials', true);
// ctx.set('Access-Control-Allow-Origin', '*');
if (!projectId) {
return (ctx.body = yapi.commons.resReturn(null, 400, 'projectId不能为空'));
@ -227,7 +231,7 @@ module.exports = async (ctx, next) => {
if (ctx.method === 'OPTIONS' && ctx.request.header['access-control-request-method']) {
return handleCorsRequest(ctx);
}
return (ctx.body = yapi.commons.resReturn(
null,
404,
@ -245,7 +249,6 @@ module.exports = async (ctx, next) => {
interfaceData = interfaceData[0];
}
// 必填字段是否填写好
if (project.strice) {
const validResult = mockValidator(interfaceData, ctx);
@ -258,7 +261,6 @@ module.exports = async (ctx, next) => {
}
}
let res;
// mock 返回值处理
res = interfaceData.res_body;
@ -345,12 +347,15 @@ module.exports = async (ctx, next) => {
}
});
}
} else ctx.set(i, context.resHeader[i]);
} else {
ctx.set(i, context.resHeader[i]);
}
}
}
ctx.status = context.httpCode;
return (ctx.body = context.mockJson);
ctx.body = context.mockJson;
return;
} catch (e) {
yapi.commons.log(e, 'error');
return (ctx.body = {

@ -1,6 +1,6 @@
const yapi = require('../yapi.js');
const mongoose = require('mongoose');
const autoIncrement = require('mongoose-auto-increment');
const autoIncrement = require('../utils/mongoose-auto-increment');
/**
* 所有的model都需要继承baseModel, 且需要 getSchema和getName方法不然会报错

@ -57,7 +57,7 @@ class followModel extends baseModel {
}
checkProjectRepeat(uid, projectid) {
return this.model.count({
return this.model.countDocuments({
uid: uid,
projectid: projectid
});

@ -86,13 +86,13 @@ class groupModel extends baseModel {
}
checkRepeat(name) {
return this.model.count({
return this.model.countDocuments({
group_name: name
});
}
// 分组数量统计
getGroupListCount() {
return this.model.count({ type: 'public' });
return this.model.countDocuments({ type: 'public' });
}
addMember(id, data) {
@ -131,7 +131,7 @@ class groupModel extends baseModel {
}
checkMemberRepeat(id, uid) {
return this.model.count({
return this.model.countDocuments({
_id: id,
'members.uid': uid
});

@ -93,7 +93,8 @@ class interfaceModel extends baseModel {
field2: String,
field3: String,
api_opened: { type: Boolean, default: false },
index: { type: Number, default: 0 }
index: { type: Number, default: 0 },
tag: Array
};
}
@ -155,15 +156,15 @@ class interfaceModel extends baseModel {
}
checkRepeat(id, path, method) {
return this.model.count({
return this.model.countDocuments({
project_id: id,
path: path,
'query_path.path': path,
method: method
});
}
countByProjectId(id) {
return this.model.count({
return this.model.countDocuments({
project_id: id
});
}
@ -191,7 +192,7 @@ class interfaceModel extends baseModel {
.skip((page - 1) * limit)
.limit(limit)
.select(
'_id title uid path method project_id catid api_opened edit_uid status add_time up_time'
'_id title uid path method project_id catid api_opened edit_uid status add_time up_time tag'
)
.exec();
}
@ -207,12 +208,12 @@ class interfaceModel extends baseModel {
//获取全部接口信息
getInterfaceListCount() {
return this.model.count({});
return this.model.countDocuments({});
}
listByCatid(catid, select) {
select =
select || '_id title uid path method project_id catid edit_uid status add_time up_time index';
select || '_id title uid path method project_id catid edit_uid status add_time up_time index tag';
return this.model
.find({
catid: catid
@ -233,7 +234,7 @@ class interfaceModel extends baseModel {
.skip((page - 1) * limit)
.limit(limit)
.select(
'_id title uid path method project_id catid edit_uid api_opened status add_time up_time, index'
'_id title uid path method project_id catid edit_uid api_opened status add_time up_time, index, tag'
)
.exec();
}
@ -308,7 +309,7 @@ class interfaceModel extends baseModel {
}
listCount(option) {
return this.model.count(option);
return this.model.countDocuments(option);
}
upIndex(id, index) {
@ -331,4 +332,4 @@ class interfaceModel extends baseModel {
}
}
module.exports = interfaceModel;
module.exports = interfaceModel;

@ -63,7 +63,7 @@ class interfaceCase extends baseModel {
//获取全部测试接口信息
getInterfaceCaseListCount() {
return this.model.count({});
return this.model.countDocuments({});
}
get(id) {

@ -35,7 +35,7 @@ class interfaceCat extends baseModel {
}
checkRepeat(name) {
return this.model.count({
return this.model.countDocuments({
name: name
});
}

@ -33,7 +33,7 @@ class interfaceCol extends baseModel {
}
checkRepeat(name) {
return this.model.count({
return this.model.countDocuments({
name: name
});
}

@ -106,7 +106,7 @@ class logModel extends baseModel {
.exec();
}
listCountByGroup(typeid, pidList) {
return this.model.count({
return this.model.countDocuments({
$or: [
{
type: 'project',
@ -132,7 +132,7 @@ class logModel extends baseModel {
if (selectValue && !isNaN(selectValue)) {
params['data.interface_id'] = +selectValue;
}
return this.model.count(params);
return this.model.countDocuments(params);
}
listWithCatid(typeid, type, interfaceId) {

@ -34,7 +34,8 @@ class projectModel extends baseModel {
project_mock_script: String,
is_mock_open: { type: Boolean, default: false },
strice: { type: Boolean, default: false },
is_json5: { type: Boolean, default: true }
is_json5: { type: Boolean, default: true },
tag: [{name: String, desc: String}]
};
}
@ -75,7 +76,7 @@ class projectModel extends baseModel {
}
getProjectWithAuth(group_id, uid) {
return this.model.count({
return this.model.countDocuments({
group_id: group_id,
'members.uid': uid
});
@ -84,7 +85,7 @@ class projectModel extends baseModel {
getBaseInfo(id, select) {
select =
select ||
'_id uid name basepath switch_notice desc group_id project_type env icon color add_time up_time pre_script after_script project_mock_script is_mock_open strice is_json5';
'_id uid name basepath switch_notice desc group_id project_type env icon color add_time up_time pre_script after_script project_mock_script is_mock_open strice is_json5 tag';
return this.model
.findOne({
_id: id
@ -102,14 +103,14 @@ class projectModel extends baseModel {
}
checkNameRepeat(name, groupid) {
return this.model.count({
return this.model.countDocuments({
name: name,
group_id: groupid
});
}
checkDomainRepeat(domain, basepath) {
return this.model.count({
return this.model.countDocuments({
prd_host: domain,
basepath: basepath
});
@ -128,12 +129,12 @@ class projectModel extends baseModel {
// 获取项目数量统计
getProjectListCount() {
return this.model.count();
return this.model.countDocuments();
}
countWithPublic(group_id) {
let params = { group_id: group_id, project_type: 'public' };
return this.model.count(params);
return this.model.countDocuments(params);
}
listWithPaging(group_id, page, limit) {
@ -150,13 +151,13 @@ class projectModel extends baseModel {
}
listCount(group_id) {
return this.model.count({
return this.model.countDocuments({
group_id: group_id
});
}
countByGroupId(group_id) {
return this.model.count({
return this.model.countDocuments({
group_id: group_id
});
}
@ -208,7 +209,7 @@ class projectModel extends baseModel {
}
checkMemberRepeat(id, uid) {
return this.model.count({
return this.model.countDocuments({
_id: id,
'members.uid': uid
});

@ -34,7 +34,7 @@ class userModel extends baseModel {
}
checkRepeat(email) {
return this.model.count({
return this.model.countDocuments({
email: email
});
}
@ -68,7 +68,7 @@ class userModel extends baseModel {
}
listCount() {
return this.model.count();
return this.model.countDocuments();
}
findByEmail(email) {

@ -27,7 +27,7 @@ var hooks = {
},
/**
* 客户端删除接口成功后触发
* @param data 删除接口的详细信息
* @param data 接口id
*/
interface_del: {
type: 'multi',
@ -65,6 +65,22 @@ var hooks = {
type: 'multi',
listener: []
},
/**
* 客户端更新一个新项目
* @param id 项目id
*/
project_up: {
type: 'multi',
listener: []
},
/**
* 客户端获取一个项目
* @param id 项目id
*/
project_get: {
type: 'multi',
listener: []
},
/**
* 客户端删除删除一个项目
* @param id 项目id

@ -254,6 +254,11 @@ let routerConfig = {
path: 'up_env',
method: 'post'
},
{
action: 'upTag',
path: 'up_tag',
method: 'post'
},
{
action: 'token',
path: 'token',
@ -273,6 +278,11 @@ let routerConfig = {
action: 'copy',
path: 'copy',
method: 'post'
},
{
action: 'swaggerUrl',
path: 'swagger_url',
method: 'get'
}
],
interface: [

@ -18,6 +18,7 @@ const ejs = require('easy-json-schema');
const jsf = require('json-schema-faker');
const formats = require('../../common/formats');
const http = require('http');
jsf.extend ('mock', function () {
return {
@ -626,3 +627,39 @@ exports.handleMockScript = function(script, context) {
context.delay = sandbox.delay;
};
exports.createWebAPIRequest = function(ops) {
return new Promise(function(resolve, reject) {
let req = '';
let http_client = http.request(
{
host: ops.hostname,
method: 'GET',
port: ops.port,
path: ops.path
},
function(res) {
res.on('error', function(err) {
reject(err);
});
res.setEncoding('utf8');
if (res.statusCode != 200) {
reject({message: 'statusCode != 200'});
} else {
res.on('data', function(chunk) {
req += chunk;
});
res.on('end', function() {
resolve(req);
});
}
}
);
http_client.on('error', (e) => {
reject({message: 'request error'});
});
http_client.end();
});
}

@ -1,6 +1,6 @@
const mongoose = require('mongoose');
const yapi = require('../yapi.js');
const autoIncrement = require('mongoose-auto-increment');
const autoIncrement = require('./mongoose-auto-increment');
function model(model, schema) {
if (schema instanceof mongoose.Schema === false) {
@ -9,26 +9,32 @@ function model(model, schema) {
schema.set('autoIndex', false);
return yapi.connect.model(model, schema, model);
return mongoose.model(model, schema, model);
}
function connect(callback) {
mongoose.Promise = global.Promise;
let config = yapi.WEBCONFIG;
let options = { useMongoClient: true };
let options = {useNewUrlParser: true };
if (config.db.user) {
options.user = config.db.user;
options.pass = config.db.pass;
}
var connectString = `mongodb://${config.db.servername}:${config.db.port}/${config.db.DATABASE}`;
if (config.db.authSource) {
connectString = connectString + `?authSource=${config.db.authSource}`;
}
options = Object.assign({}, options, config.db.options)
//yapi.commons.log(connectString);
var connectString = '';
if(config.db.connectString){
connectString = config.db.connectString;
}else{
connectString = `mongodb://${config.db.servername}:${config.db.port}/${config.db.DATABASE}`;
if (config.db.authSource) {
connectString = connectString + `?authSource=${config.db.authSource}`;
}
}
let db = mongoose.connect(
connectString,

@ -0,0 +1,178 @@
// Module Scope
var mongoose = require('mongoose'),
extend = require('extend'),
counterSchema,
IdentityCounter;
// Initialize plugin by creating counter collection in database.
exports.initialize = function (connection) {
try {
IdentityCounter = mongoose.model('IdentityCounter');
} catch (ex) {
if (ex.name === 'MissingSchemaError') {
// Create new counter schema.
counterSchema = new mongoose.Schema({
model: { type: String, require: true },
field: { type: String, require: true },
count: { type: Number, default: 0 }
});
// Create a unique index using the "field" and "model" fields.
counterSchema.index({ field: 1, model: 1 }, { unique: true, required: true, index: -1 });
// Create model using new schema.
IdentityCounter = mongoose.model('IdentityCounter', counterSchema);
}
else
throw ex;
}
};
// The function to use when invoking the plugin on a custom schema.
exports.plugin = function (schema, options) {
// If we don't have reference to the counterSchema or the IdentityCounter model then the plugin was most likely not
// initialized properly so throw an error.
if (!counterSchema || !IdentityCounter) throw new Error("mongoose-auto-increment has not been initialized");
// Default settings and plugin scope variables.
var settings = {
model: null, // The model to configure the plugin for.
field: '_id', // The field the plugin should track.
startAt: 0, // The number the count should start at.
incrementBy: 1, // The number by which to increment the count each time.
unique: true // Should we create a unique index for the field
},
fields = {}, // A hash of fields to add properties to in Mongoose.
ready = false; // True if the counter collection has been updated and the document is ready to be saved.
switch (typeof(options)) {
// If string, the user chose to pass in just the model name.
case 'string':
settings.model = options;
break;
// If object, the user passed in a hash of options.
case 'object':
extend(settings, options);
break;
}
if (settings.model == null)
throw new Error("model must be set");
// Add properties for field in schema.
fields[settings.field] = {
type: Number,
require: true
};
if (settings.field !== '_id')
fields[settings.field].unique = settings.unique
schema.add(fields);
// Find the counter for this model and the relevant field.
IdentityCounter.findOne(
{ model: settings.model, field: settings.field },
function (err, counter) {
if (!counter) {
// If no counter exists then create one and save it.
counter = new IdentityCounter({ model: settings.model, field: settings.field, count: settings.startAt - settings.incrementBy });
counter.save(function () {
ready = true;
});
}
else {
ready = true;
}
}
);
// Declare a function to get the next counter for the model/schema.
var nextCount = function (callback) {
IdentityCounter.findOne({
model: settings.model,
field: settings.field
}, function (err, counter) {
if (err) return callback(err);
callback(null, counter === null ? settings.startAt : counter.count + settings.incrementBy);
});
};
// Add nextCount as both a method on documents and a static on the schema for convenience.
schema.method('nextCount', nextCount);
schema.static('nextCount', nextCount);
// Declare a function to reset counter at the start value - increment value.
var resetCount = function (callback) {
IdentityCounter.findOneAndUpdate(
{ model: settings.model, field: settings.field },
{ count: settings.startAt - settings.incrementBy },
{ new: true }, // new: true specifies that the callback should get the updated counter.
function (err) {
if (err) return callback(err);
callback(null, settings.startAt);
}
);
};
// Add resetCount as both a method on documents and a static on the schema for convenience.
schema.method('resetCount', resetCount);
schema.static('resetCount', resetCount);
// Every time documents in this schema are saved, run this logic.
schema.pre('save', function (next) {
// Get reference to the document being saved.
var doc = this;
// Only do this if it is a new document (see http://mongoosejs.com/docs/api.html#document_Document-isNew)
if (doc.isNew) {
// Declare self-invoking save function.
(function save() {
// If ready, run increment logic.
// Note: ready is true when an existing counter collection is found or after it is created for the
// first time.
if (ready) {
// check that a number has already been provided, and update the counter to that number if it is
// greater than the current count
if (typeof doc[settings.field] === 'number') {
IdentityCounter.findOneAndUpdate(
// IdentityCounter documents are identified by the model and field that the plugin was invoked for.
// Check also that count is less than field value.
{ model: settings.model, field: settings.field, count: { $lt: doc[settings.field] } },
// Change the count of the value found to the new field value.
{ count: doc[settings.field] },
function (err) {
if (err) return next(err);
// Continue with default document save functionality.
next();
}
);
} else {
// Find the counter collection entry for this model and field and update it.
IdentityCounter.findOneAndUpdate(
// IdentityCounter documents are identified by the model and field that the plugin was invoked for.
{ model: settings.model, field: settings.field },
// Increment the count by `incrementBy`.
{ $inc: { count: settings.incrementBy } },
// new:true specifies that the callback should get the counter AFTER it is updated (incremented).
{ new: true },
// Receive the updated counter.
function (err, updatedIdentityCounter) {
if (err) return next(err);
// If there are no errors then go ahead and set the document's field to the current count.
doc[settings.field] = updatedIdentityCounter.count;
// Continue with default document save functionality.
next();
}
);
}
}
// If not ready then set a 5 millisecond timer and try to save again. It will keep doing this until
// the counter collection is ready.
else
setTimeout(save, 5);
})();
}
// If the document does not have the field we're interested in or that field isn't a number AND the user did
// not specify that we should increment on updates, then just continue the save without any increment logic.
else
next();
});
};

@ -119,8 +119,8 @@ yapi update -v v1.1.0 //升级到指定版本
<ul>
<li><code>enable</code> 表示是否配置 LDAP 登录true(支持 LDAP登录 )/false(不支持LDAP登录);</li>
<li><code>server</code> LDAP 服务器地址,前面需要加上 ldap:// 前缀,也可以是 ldaps:// 表示是通过 SSL 连接;</li>
<li><code>baseDn</code> LDAP 服务器的登录用户名,必须是从根结点到用户节点的全路径;</li>
<li><code>bindPassword</code> 登录该 LDAP 服务器的密码;</li>
<li><code>baseDn</code> LDAP 服务器的登录用户名,必须是从根结点到用户节点的全路径(非必须);</li>
<li><code>bindPassword</code> 登录该 LDAP 服务器的密码(非必须);</li>
<li><code>searchDn</code> 查询用户数据的路径,类似数据库中的一张表的地址,注意这里也必须是全路径;</li>
<li><code>searchStandard</code> 查询条件,这里是 mail 表示查询用户信息是通过邮箱信息来查询的。注意该字段信息与LDAP数据库存储数据的字段相对应如果如果存储用户邮箱信息的字段是 email, 这里就需要修改成 email.1.3.18+支持自定义filter表达式基本形式为&amp;(objectClass=user)(cn=%s), 其中%s会被username替换</li>
<li><code>emailPostfix</code> 登陆邮箱后缀(非必须)</li>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

(image error) Size: 35 KiB

After

(image error) Size: 46 KiB

Binary file not shown.

After

(image error) Size: 11 KiB

Binary file not shown.

After

(image error) Size: 22 KiB

Binary file not shown.

After

(image error) Size: 42 KiB

Binary file not shown.

After

(image error) Size: 102 KiB

Binary file not shown.

After

(image error) Size: 46 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -177,6 +177,11 @@ window.ydoc_plugin_search_json = {
"content": "",
"url": "/documents/project.html",
"children": [
{
"title": "基本设置",
"url": "/documents/project.html#基本设置",
"content": "基本设置tag 信息可自定义tag名称和tag描述tag信息可用在接口tag标识中;\nmock 严格模式:开启后 mock 请求会对 querybody form 的必须字段和 json schema 进行校验;\n开启json5开启后允许接口请求body 和返回值中写 json 字段。yapi建议用户关闭 json5 因为json-schema 格式可以进行接口格式校验。\n"
},
{
"title": "新建项目",
"url": "/documents/project.html#新建项目",
@ -259,6 +264,11 @@ window.ydoc_plugin_search_json = {
"content": "",
"url": "/documents/project.html",
"children": [
{
"title": "基本设置",
"url": "/documents/project.html#基本设置",
"content": "基本设置tag 信息可自定义tag名称和tag描述tag信息可用在接口tag标识中;\nmock 严格模式:开启后 mock 请求会对 querybody form 的必须字段和 json schema 进行校验;\n开启json5开启后允许接口请求body 和返回值中写 json 字段。yapi建议用户关闭 json5 因为json-schema 格式可以进行接口格式校验。\n"
},
{
"title": "新建项目",
"url": "/documents/project.html#新建项目",
@ -349,7 +359,7 @@ window.ydoc_plugin_search_json = {
{
"title": "基本设置",
"url": "/documents/api.html#接口配置-基本设置",
"content": "基本设置接口路径:可以更改 HTTP 请求方式,并且支持 restful 动态路由,例如 /api/{id}/{name}, id和name是动态参数\n选择分类可以更改接口所在分类\n状态用于标识接口是否开发完成。\n"
"content": "基本设置接口路径:可以更改 HTTP 请求方式,并且支持 restful 动态路由,例如 /api/{id}/{name}, id和name是动态参数\n选择分类可以更改接口所在分类\n状态用于标识接口是否开发完成。\nTag用于标识接口tag信息v1.3.23+,在接口list页可以根据tag过滤接口\n"
},
{
"title": "请求参数设置",
@ -391,7 +401,7 @@ window.ydoc_plugin_search_json = {
{
"title": "基本设置",
"url": "/documents/api.html#接口配置-基本设置",
"content": "基本设置接口路径:可以更改 HTTP 请求方式,并且支持 restful 动态路由,例如 /api/{id}/{name}, id和name是动态参数\n选择分类可以更改接口所在分类\n状态用于标识接口是否开发完成。\n"
"content": "基本设置接口路径:可以更改 HTTP 请求方式,并且支持 restful 动态路由,例如 /api/{id}/{name}, id和name是动态参数\n选择分类可以更改接口所在分类\n状态用于标识接口是否开发完成。\nTag用于标识接口tag信息v1.3.23+,在接口list页可以根据tag过滤接口\n"
},
{
"title": "请求参数设置",
@ -443,12 +453,17 @@ window.ydoc_plugin_search_json = {
{
"title": "方式2. json-schema",
"url": "/documents/mock.html#方式2.-json-schema",
"content": "方式2. json-schema开启 json-schema 功能后,将不再使用 mockjs 解析定义的返回数据,而是根据 json-schema 定义的数据结构,生成随机数据。"
"content": "方式2. json-schema开启 json-schema 功能后,根据 json-schema 定义的数据结构,生成随机数据。"
},
{
"title": "如何生成随机的邮箱或 ip",
"url": "/documents/mock.html#方式2.-json-schema-如何生成随机的邮箱或-ip",
"content": "如何生成随机的邮箱或 ip点击高级设置选择 format 选项,比如选择 email 则该字段生成随机邮箱字符串。"
"title": "如何生成随机的邮箱或 ip(该方法在v1.3.22之后不再适用)",
"url": "/documents/mock.html#方式2.-json-schema-如何生成随机的邮箱或-ip该方法在v1.3.22之后不再适用?",
"content": "如何生成随机的邮箱或 ip(该方法在v1.3.22之后不再适用)?点击高级设置,选择 format 选项,比如选择 email 则该字段生成随机邮箱字符串。"
},
{
"title": "集成 mockjs",
"url": "/documents/mock.html#方式2.-json-schema-集成-mockjs",
"content": "集成 mockjs基本书写方式为 mock 的数据占位符@xxx, 具体字段详见Mockjs 官网\n如果不是以@字符开头的话或者匹配不到Mockjs中的占位符就会直接生成输入的值\n"
},
{
"title": "如何使用 Mock",
@ -495,12 +510,17 @@ window.ydoc_plugin_search_json = {
{
"title": "方式2. json-schema",
"url": "/documents/mock.html#方式2.-json-schema",
"content": "方式2. json-schema开启 json-schema 功能后,将不再使用 mockjs 解析定义的返回数据,而是根据 json-schema 定义的数据结构,生成随机数据。"
"content": "方式2. json-schema开启 json-schema 功能后,根据 json-schema 定义的数据结构,生成随机数据。"
},
{
"title": "如何生成随机的邮箱或 ip",
"url": "/documents/mock.html#方式2.-json-schema-如何生成随机的邮箱或-ip",
"content": "如何生成随机的邮箱或 ip点击高级设置选择 format 选项,比如选择 email 则该字段生成随机邮箱字符串。"
"title": "如何生成随机的邮箱或 ip(该方法在v1.3.22之后不再适用)",
"url": "/documents/mock.html#方式2.-json-schema-如何生成随机的邮箱或-ip该方法在v1.3.22之后不再适用?",
"content": "如何生成随机的邮箱或 ip(该方法在v1.3.22之后不再适用)?点击高级设置,选择 format 选项,比如选择 email 则该字段生成随机邮箱字符串。"
},
{
"title": "集成 mockjs",
"url": "/documents/mock.html#方式2.-json-schema-集成-mockjs",
"content": "集成 mockjs基本书写方式为 mock 的数据占位符@xxx, 具体字段详见Mockjs 官网\n如果不是以@字符开头的话或者匹配不到Mockjs中的占位符就会直接生成输入的值\n"
},
{
"title": "如何使用 Mock",
@ -744,28 +764,28 @@ window.ydoc_plugin_search_json = {
},
{
"title": "数据导入",
"content": "在数据管理可快速导入其他格式的接口数据方便快速添加接口。YApi 目前支持 postman, swagger, har 数据导入。",
"content": "在数据管理可快速导入其他格式的接口数据方便快速添加接口。YApi 目前支持 postman, swagger, har 数据导入。v1.3.23+ 增加数据导入的3种同步方式 normal, good, mergin普通模式(normal):不导入已存在的接口;\n智能合并(good):已存在的接口,将合并返回数据的 response适用于导入了 swagger 数据保留对数据结构的改动例如用户对字段code 添加了mock信息, 当再次数据导入的时候 mock 字段将不会被覆盖\n完全覆盖(mergin):不保留旧数据,完全使用新数据,适用于接口定义完全交给后端定义, 默认为 normal\n",
"url": "/documents/data.html",
"children": [
{
"title": "Postman 数据导入",
"url": "/documents/data.html#postman-数据导入",
"content": "Postman 数据导入1.首先在postman导出接口2.选择collection_v1,点击export导出接口到文件xxx3.打开yapi平台进入到项目页面点击数据管理选择相应的分组和postman导入\b方式\b选择刚才保存的文件路径开始导入数据"
"content": "Postman 数据导入1.首先在 postman 导出接口2.选择 collection_v1,点击 export 导出接口到文件 xxx3.打开 yapi 平台,进入到项目页面,点击数据管理,选择相应的分组和 postman 导入 \b 方式,\b 选择刚才保存的文件路径,开始导入数据"
},
{
"title": "HAR\b\b 数据导入",
"url": "/documents/data.html#har\b\b-数据导入",
"content": "HAR\b\b 数据导入可用 chrome 实现录制接口数据的功能方便开发者快速导入项目接口1.打开 Chrome 浏览器开发者工具,点击network首次使用请先clear所有请求信息确保录制功能开启红色为开启状态2.操作页面实际功能完成后点击save as HAR with content,将数据保存到文件xxx3.打开yapi平台进入到项目页面点击数据管理选择相应的分组和har导入\b方式\b选择刚才保存的文件路径开始导入数据"
"content": "HAR\b\b 数据导入可用 chrome 实现录制接口数据的功能方便开发者快速导入项目接口1.打开 Chrome 浏览器开发者工具,点击 network首次使用请先 clear 所有请求信息确保录制功能开启红色为开启状态2.操作页面实际功能,完成后点击 save as HAR with content,将数据保存到文件 xxx3.打开 yapi 平台,进入到项目页面,点击数据管理,选择相应的分组和 har 导入 \b 方式,\b 选择刚才保存的文件路径,开始导入数据Tips: har 数据导入只支持 response.content.mimeType 为 application/json 类型的数据\n"
},
{
"title": "Swagger 数据导入",
"url": "/documents/data.html#swagger-数据导入",
"content": "Swagger 数据导入什么是 Swagger [Swagger从入门到精通](https://www.gitbook.com/book/huangwenchao/swagger/details)1.生成 JSON 语言编写的 Swagger API 文档文件 例如这样的数据 http://petstore.swagger.io/v2/swagger.json可以将其内容复制到 JSON 文件中。Tips: v1.3.19 版本开始支持swagger url 导入功能\n2.打开yapi平台进入到项目页面点击数据管理选择相应的分组和swagger导入\b方式\b选择刚才的文件开始导入数据"
"content": "Swagger 数据导入什么是 Swagger [Swagger从入门到精通](https://www.gitbook.com/book/huangwenchao/swagger/details)1.生成 JSON 语言编写的 Swagger API 文档文件 例如这样的数据 http://petstore.swagger.io/v2/swagger.json可以将其内容复制到 JSON 文件中。Tips: v1.3.19 版本开始支持 swagger url 导入功能\n2.打开 yapi 平台,进入到项目页面,点击数据管理,选择相应的分组和 swagger 导入 \b 方式,\b 选择刚才的文件,开始导入数据"
},
{
"title": "YApi接口JSON数据导入",
"url": "/documents/data.html#yapi接口json数据导入",
"content": "YApi接口JSON数据导入该功能在 v1.3.12 版本上线,可导入在 yapi 平台导出的 json 接口数据。"
"title": "YApi 接口 JSON 数据导入",
"url": "/documents/data.html#yapi-接口-json-数据导入",
"content": "YApi 接口 JSON 数据导入该功能在 v1.3.12 版本上线,可导入在 yapi 平台导出的 json 接口数据。"
},
{
"title": "通过命令行导入接口数据",
@ -775,34 +795,34 @@ window.ydoc_plugin_search_json = {
{
"title": "使用方法",
"url": "/documents/data.html#通过命令行导入接口数据-使用方法",
"content": "使用方法第一步,确保 yapi-cli >= 1.2.7 版本,如果低于此版本请升级 yapi-cli 工具npm install -g yapi-cli第二步在任意一个目录下新建配置文件 yapi-import.json内容如下{ \"type\": \"swagger\",\n \"token\": \"17fba0027f300248b804\",\n \"file\": \"swagger.json\",\n \"merge\": false,\n \"server\": \"http://yapi.local.qunar.com:3000\"\n}\ntype 是数据数据方式,目前官方只支持 swaggertoken 是项目token在 项目设置 -> token 设置获取file 是 swagger 接口文档文件,可使用绝对路径或 urlmerge 是否覆盖旧的接口,默认不开启,配置 true 开启server 是yapi服务器地址第三步在新建配置文件的当前目录执行下面指令yapi import"
"content": "使用方法第一步,确保 yapi-cli >= 1.2.7 版本,如果低于此版本请升级 yapi-cli 工具npm install -g yapi-cli第二步在任意一个目录下新建配置文件 yapi-import.json内容如下{ \"type\": \"swagger\",\n \"token\": \"17fba0027f300248b804\",\n \"file\": \"swagger.json\",\n \"merge\": \"normal\",\n \"server\": \"http://yapi.local.qunar.com:3000\"\n}\ntype 是数据数据方式,目前官方只支持 swaggertoken 是项目 token在 项目设置 -> token 设置获取file 是 swagger 接口文档文件,可使用绝对路径或 urlmerge 有三种导入方式(v1.3.23+支持) normal, good, mergin普通模式(normal):不导入已存在的接口;\n智能合并(good):已存在的接口,将合并返回数据的 response适用于导入了 swagger 数据,保留对数据结构的改动;\n完全覆盖(mergin):不保留旧数据,完全使用新数据,适用于接口定义完全交给后端定义, 默认为 normal\nserver 是 yapi 服务器地址第三步在新建配置文件的当前目录执行下面指令yapi import"
}
]
},
{
"title": "数据导入",
"content": "在数据管理可快速导入其他格式的接口数据方便快速添加接口。YApi 目前支持 postman, swagger, har 数据导入。",
"content": "在数据管理可快速导入其他格式的接口数据方便快速添加接口。YApi 目前支持 postman, swagger, har 数据导入。v1.3.23+ 增加数据导入的3种同步方式 normal, good, mergin普通模式(normal):不导入已存在的接口;\n智能合并(good):已存在的接口,将合并返回数据的 response适用于导入了 swagger 数据保留对数据结构的改动例如用户对字段code 添加了mock信息, 当再次数据导入的时候 mock 字段将不会被覆盖\n完全覆盖(mergin):不保留旧数据,完全使用新数据,适用于接口定义完全交给后端定义, 默认为 normal\n",
"url": "/documents/data.html",
"children": [
{
"title": "Postman 数据导入",
"url": "/documents/data.html#postman-数据导入",
"content": "Postman 数据导入1.首先在postman导出接口2.选择collection_v1,点击export导出接口到文件xxx3.打开yapi平台进入到项目页面点击数据管理选择相应的分组和postman导入\b方式\b选择刚才保存的文件路径开始导入数据"
"content": "Postman 数据导入1.首先在 postman 导出接口2.选择 collection_v1,点击 export 导出接口到文件 xxx3.打开 yapi 平台,进入到项目页面,点击数据管理,选择相应的分组和 postman 导入 \b 方式,\b 选择刚才保存的文件路径,开始导入数据"
},
{
"title": "HAR\b\b 数据导入",
"url": "/documents/data.html#har\b\b-数据导入",
"content": "HAR\b\b 数据导入可用 chrome 实现录制接口数据的功能方便开发者快速导入项目接口1.打开 Chrome 浏览器开发者工具,点击network首次使用请先clear所有请求信息确保录制功能开启红色为开启状态2.操作页面实际功能完成后点击save as HAR with content,将数据保存到文件xxx3.打开yapi平台进入到项目页面点击数据管理选择相应的分组和har导入\b方式\b选择刚才保存的文件路径开始导入数据"
"content": "HAR\b\b 数据导入可用 chrome 实现录制接口数据的功能方便开发者快速导入项目接口1.打开 Chrome 浏览器开发者工具,点击 network首次使用请先 clear 所有请求信息确保录制功能开启红色为开启状态2.操作页面实际功能,完成后点击 save as HAR with content,将数据保存到文件 xxx3.打开 yapi 平台,进入到项目页面,点击数据管理,选择相应的分组和 har 导入 \b 方式,\b 选择刚才保存的文件路径,开始导入数据Tips: har 数据导入只支持 response.content.mimeType 为 application/json 类型的数据\n"
},
{
"title": "Swagger 数据导入",
"url": "/documents/data.html#swagger-数据导入",
"content": "Swagger 数据导入什么是 Swagger [Swagger从入门到精通](https://www.gitbook.com/book/huangwenchao/swagger/details)1.生成 JSON 语言编写的 Swagger API 文档文件 例如这样的数据 http://petstore.swagger.io/v2/swagger.json可以将其内容复制到 JSON 文件中。Tips: v1.3.19 版本开始支持swagger url 导入功能\n2.打开yapi平台进入到项目页面点击数据管理选择相应的分组和swagger导入\b方式\b选择刚才的文件开始导入数据"
"content": "Swagger 数据导入什么是 Swagger [Swagger从入门到精通](https://www.gitbook.com/book/huangwenchao/swagger/details)1.生成 JSON 语言编写的 Swagger API 文档文件 例如这样的数据 http://petstore.swagger.io/v2/swagger.json可以将其内容复制到 JSON 文件中。Tips: v1.3.19 版本开始支持 swagger url 导入功能\n2.打开 yapi 平台,进入到项目页面,点击数据管理,选择相应的分组和 swagger 导入 \b 方式,\b 选择刚才的文件,开始导入数据"
},
{
"title": "YApi接口JSON数据导入",
"url": "/documents/data.html#yapi接口json数据导入",
"content": "YApi接口JSON数据导入该功能在 v1.3.12 版本上线,可导入在 yapi 平台导出的 json 接口数据。"
"title": "YApi 接口 JSON 数据导入",
"url": "/documents/data.html#yapi-接口-json-数据导入",
"content": "YApi 接口 JSON 数据导入该功能在 v1.3.12 版本上线,可导入在 yapi 平台导出的 json 接口数据。"
},
{
"title": "通过命令行导入接口数据",
@ -812,7 +832,7 @@ window.ydoc_plugin_search_json = {
{
"title": "使用方法",
"url": "/documents/data.html#通过命令行导入接口数据-使用方法",
"content": "使用方法第一步,确保 yapi-cli >= 1.2.7 版本,如果低于此版本请升级 yapi-cli 工具npm install -g yapi-cli第二步在任意一个目录下新建配置文件 yapi-import.json内容如下{ \"type\": \"swagger\",\n \"token\": \"17fba0027f300248b804\",\n \"file\": \"swagger.json\",\n \"merge\": false,\n \"server\": \"http://yapi.local.qunar.com:3000\"\n}\ntype 是数据数据方式,目前官方只支持 swaggertoken 是项目token在 项目设置 -> token 设置获取file 是 swagger 接口文档文件,可使用绝对路径或 urlmerge 是否覆盖旧的接口,默认不开启,配置 true 开启server 是yapi服务器地址第三步在新建配置文件的当前目录执行下面指令yapi import"
"content": "使用方法第一步,确保 yapi-cli >= 1.2.7 版本,如果低于此版本请升级 yapi-cli 工具npm install -g yapi-cli第二步在任意一个目录下新建配置文件 yapi-import.json内容如下{ \"type\": \"swagger\",\n \"token\": \"17fba0027f300248b804\",\n \"file\": \"swagger.json\",\n \"merge\": \"normal\",\n \"server\": \"http://yapi.local.qunar.com:3000\"\n}\ntype 是数据数据方式,目前官方只支持 swaggertoken 是项目 token在 项目设置 -> token 设置获取file 是 swagger 接口文档文件,可使用绝对路径或 urlmerge 有三种导入方式(v1.3.23+支持) normal, good, mergin普通模式(normal):不导入已存在的接口;\n智能合并(good):已存在的接口,将合并返回数据的 response适用于导入了 swagger 数据,保留对数据结构的改动;\n完全覆盖(mergin):不保留旧数据,完全使用新数据,适用于接口定义完全交给后端定义, 默认为 normal\nserver 是 yapi 服务器地址第三步在新建配置文件的当前目录执行下面指令yapi import"
}
]
},
@ -1030,7 +1050,7 @@ window.ydoc_plugin_search_json = {
{
"title": "后端 hookList",
"url": "/documents/plugin-hooks.html#后端-hooklist",
"content": "后端 hookList目前 hooksList 只有下面列出的部分,如果您有其他的需求,可提建议到 github 或者 qq 群/** * 钩子配置\n */\nvar hooks = {\n /**\n * 第三方sso登录钩子暂只支持设置一个\n * @param ctx\n * @return 必需返回一个 promise 对象resolve({username: '', email: ''})\n */\n 'third_login': {\n type: 'single',\n listener: null\n },\n /**\n * 客户端增加接口成功后触发\n * @param data 接口的详细信息\n */\n interface_add: {\n type: 'multi',\n listener: []\n },\n /**\n * 客户端删除接口成功后触发\n * @param data 删除接口的详细信息\n */\n interface_del: {\n type: 'multi',\n listener: []\n },\n /**\n * 客户端更新接口成功后触发\n * @param id 接口id\n */\n interface_update: {\n type: 'multi',\n listener: []\n },\n /**\n * 客户端获取接口数据列表\n * @param list 返回接口的数据列表\n */\n interface_list: {\n type: 'multi',\n listener: []\n },\n /**\n * 客户端获取一条接口信息触发\n * @param data 接口的详细信息\n */\n interface_get: {\n type: 'multi',\n listener: []\n },\n /**\n * 客户端增加一个新项目\n * @param id 项目id\n */\n 'project_add':{\n type: 'multi',\n listener: []\n },\n /**\n * 客户端删除删除一个项目\n * @param id 项目id\n */\n 'project_del':{\n type: 'multi',\n listener: []\n },\n /**\n * MockServer生成mock数据后触发\n * @param context Object\n * {\n * projectData: project,\n interfaceData: interfaceData,\n ctx: ctx,\n mockJson: res\n * }\n *\n */\n mock_after: {\n type: 'multi',\n listener: []\n },\n /**\n * 增加路由的钩子\n * type Sync\n * @param addPluginRouter Function\n * addPLuginPLugin(config)\n * config = {\n * path, // String\n * method, // String\n * controller // Class 继承baseController的class\n * action // String controller的Action\n * }\n */\n add_router: {\n type: 'multi',\n listener: []\n }\n};\n"
"content": "后端 hookList目前 hooksList 只有下面列出的部分,如果您有其他的需求,可提建议到 github 或者 qq 群/** * 钩子配置\n */\nvar hooks = {\n /**\n * 第三方sso登录钩子暂只支持设置一个\n * @param ctx\n * @return 必需返回一个 promise 对象resolve({username: '', email: ''})\n */\n 'third_login': {\n type: 'single',\n listener: null\n },\n /**\n * 客户端增加接口成功后触发\n * @param data 接口的详细信息\n */\n interface_add: {\n type: 'multi',\n listener: []\n },\n /**\n * 客户端删除接口成功后触发\n * @param data 接口id\n */\n interface_del: {\n type: 'multi',\n listener: []\n },\n /**\n * 客户端更新接口成功后触发\n * @param id 接口id\n */\n interface_update: {\n type: 'multi',\n listener: []\n },\n /**\n * 客户端获取接口数据列表\n * @param list 返回接口的数据列表\n */\n interface_list: {\n type: 'multi',\n listener: []\n },\n /**\n * 客户端获取一条接口信息触发\n * @param data 接口的详细信息\n */\n interface_get: {\n type: 'multi',\n listener: []\n },\n /**\n * 客户端增加一个新项目\n * @param id 项目id\n */\n 'project_add':{\n type: 'multi',\n listener: []\n },\n /**\n * 客户端删除删除一个项目\n * @param id 项目id\n */\n 'project_del':{\n type: 'multi',\n listener: []\n },\n /**\n * MockServer生成mock数据后触发\n * @param context Object\n * {\n * projectData: project,\n interfaceData: interfaceData,\n ctx: ctx,\n mockJson: res\n * }\n *\n */\n mock_after: {\n type: 'multi',\n listener: []\n },\n /**\n * 增加路由的钩子\n * type Sync\n * @param addPluginRouter Function\n * addPLuginPLugin(config)\n * config = {\n * path, // String\n * method, // String\n * controller // Class 继承baseController的class\n * action // String controller的Action\n * }\n */\n add_router: {\n type: 'multi',\n listener: []\n }\n};\n"
},
{
"title": "前端 hookList",
@ -1047,7 +1067,7 @@ window.ydoc_plugin_search_json = {
{
"title": "后端 hookList",
"url": "/documents/plugin-hooks.html#后端-hooklist",
"content": "后端 hookList目前 hooksList 只有下面列出的部分,如果您有其他的需求,可提建议到 github 或者 qq 群/** * 钩子配置\n */\nvar hooks = {\n /**\n * 第三方sso登录钩子暂只支持设置一个\n * @param ctx\n * @return 必需返回一个 promise 对象resolve({username: '', email: ''})\n */\n 'third_login': {\n type: 'single',\n listener: null\n },\n /**\n * 客户端增加接口成功后触发\n * @param data 接口的详细信息\n */\n interface_add: {\n type: 'multi',\n listener: []\n },\n /**\n * 客户端删除接口成功后触发\n * @param data 删除接口的详细信息\n */\n interface_del: {\n type: 'multi',\n listener: []\n },\n /**\n * 客户端更新接口成功后触发\n * @param id 接口id\n */\n interface_update: {\n type: 'multi',\n listener: []\n },\n /**\n * 客户端获取接口数据列表\n * @param list 返回接口的数据列表\n */\n interface_list: {\n type: 'multi',\n listener: []\n },\n /**\n * 客户端获取一条接口信息触发\n * @param data 接口的详细信息\n */\n interface_get: {\n type: 'multi',\n listener: []\n },\n /**\n * 客户端增加一个新项目\n * @param id 项目id\n */\n 'project_add':{\n type: 'multi',\n listener: []\n },\n /**\n * 客户端删除删除一个项目\n * @param id 项目id\n */\n 'project_del':{\n type: 'multi',\n listener: []\n },\n /**\n * MockServer生成mock数据后触发\n * @param context Object\n * {\n * projectData: project,\n interfaceData: interfaceData,\n ctx: ctx,\n mockJson: res\n * }\n *\n */\n mock_after: {\n type: 'multi',\n listener: []\n },\n /**\n * 增加路由的钩子\n * type Sync\n * @param addPluginRouter Function\n * addPLuginPLugin(config)\n * config = {\n * path, // String\n * method, // String\n * controller // Class 继承baseController的class\n * action // String controller的Action\n * }\n */\n add_router: {\n type: 'multi',\n listener: []\n }\n};\n"
"content": "后端 hookList目前 hooksList 只有下面列出的部分,如果您有其他的需求,可提建议到 github 或者 qq 群/** * 钩子配置\n */\nvar hooks = {\n /**\n * 第三方sso登录钩子暂只支持设置一个\n * @param ctx\n * @return 必需返回一个 promise 对象resolve({username: '', email: ''})\n */\n 'third_login': {\n type: 'single',\n listener: null\n },\n /**\n * 客户端增加接口成功后触发\n * @param data 接口的详细信息\n */\n interface_add: {\n type: 'multi',\n listener: []\n },\n /**\n * 客户端删除接口成功后触发\n * @param data 接口id\n */\n interface_del: {\n type: 'multi',\n listener: []\n },\n /**\n * 客户端更新接口成功后触发\n * @param id 接口id\n */\n interface_update: {\n type: 'multi',\n listener: []\n },\n /**\n * 客户端获取接口数据列表\n * @param list 返回接口的数据列表\n */\n interface_list: {\n type: 'multi',\n listener: []\n },\n /**\n * 客户端获取一条接口信息触发\n * @param data 接口的详细信息\n */\n interface_get: {\n type: 'multi',\n listener: []\n },\n /**\n * 客户端增加一个新项目\n * @param id 项目id\n */\n 'project_add':{\n type: 'multi',\n listener: []\n },\n /**\n * 客户端删除删除一个项目\n * @param id 项目id\n */\n 'project_del':{\n type: 'multi',\n listener: []\n },\n /**\n * MockServer生成mock数据后触发\n * @param context Object\n * {\n * projectData: project,\n interfaceData: interfaceData,\n ctx: ctx,\n mockJson: res\n * }\n *\n */\n mock_after: {\n type: 'multi',\n listener: []\n },\n /**\n * 增加路由的钩子\n * type Sync\n * @param addPluginRouter Function\n * addPLuginPLugin(config)\n * config = {\n * path, // String\n * method, // String\n * controller // Class 继承baseController的class\n * action // String controller的Action\n * }\n */\n add_router: {\n type: 'multi',\n listener: []\n }\n};\n"
},
{
"title": "前端 hookList",
@ -1190,14 +1210,14 @@ window.ydoc_plugin_search_json = {
"url": "/documents/CHANGELOG.html",
"children": [
{
"title": "v1.3.22",
"url": "/documents/CHANGELOG.html#v1.3.22",
"content": "v1.3.22json schema number和integer支持枚举\n服务端测试增加下载功能\n增加 mock 接口请求字段参数验证\n增加返回数据验证\n"
"title": "v1.3.23",
"url": "/documents/CHANGELOG.html#v1.3.23",
"content": "v1.3.23接口tag功能\n数据导入增加 merge 功能\n增加参数的批量导入功能\nBug Fixed接口path中写入 ?name=xxx bug\n高级mock 匹配 data: [{item: XXX}] 时匹配不成功\n接口运行 query params 自动勾选\nmock get 带 cookie 时跨域\njson schema 嵌套多层 array 预览不展示 bug\nswagger URL 导入 跨域问题\n"
},
{
"title": "Bug Fixed",
"url": "/documents/CHANGELOG.html#bug-fixed",
"content": "Bug Fixed命令行导入成员信息为 undefined\n修复form 参数为空时 接口无法保存的问题\n"
"title": "v1.3.22",
"url": "/documents/CHANGELOG.html#v1.3.22",
"content": "v1.3.22json schema number和integer支持枚举\n服务端测试增加下载功能\n增加 mock 接口请求字段参数验证\n增加返回数据验证\nBug Fixed命令行导入成员信息为 undefined\n修复form 参数为空时 接口无法保存的问题\n"
},
{
"title": "v1.3.21",
@ -1372,14 +1392,14 @@ window.ydoc_plugin_search_json = {
"url": "/documents/CHANGELOG.html",
"children": [
{
"title": "v1.3.22",
"url": "/documents/CHANGELOG.html#v1.3.22",
"content": "v1.3.22json schema number和integer支持枚举\n服务端测试增加下载功能\n增加 mock 接口请求字段参数验证\n增加返回数据验证\n"
"title": "v1.3.23",
"url": "/documents/CHANGELOG.html#v1.3.23",
"content": "v1.3.23接口tag功能\n数据导入增加 merge 功能\n增加参数的批量导入功能\nBug Fixed接口path中写入 ?name=xxx bug\n高级mock 匹配 data: [{item: XXX}] 时匹配不成功\n接口运行 query params 自动勾选\nmock get 带 cookie 时跨域\njson schema 嵌套多层 array 预览不展示 bug\nswagger URL 导入 跨域问题\n"
},
{
"title": "Bug Fixed",
"url": "/documents/CHANGELOG.html#bug-fixed",
"content": "Bug Fixed命令行导入成员信息为 undefined\n修复form 参数为空时 接口无法保存的问题\n"
"title": "v1.3.22",
"url": "/documents/CHANGELOG.html#v1.3.22",
"content": "v1.3.22json schema number和integer支持枚举\n服务端测试增加下载功能\n增加 mock 接口请求字段参数验证\n增加返回数据验证\nBug Fixed命令行导入成员信息为 undefined\n修复form 参数为空时 接口无法保存的问题\n"
},
{
"title": "v1.3.21",
@ -1593,7 +1613,7 @@ window.ydoc_plugin_search_json = {
{
"title": "配置LDAP登录",
"url": "/devops/index.html#配置ldap登录",
"content": "配置LDAP登录打开项目目录 config.json 文件,添加如下字段:{ \"port\": \"*****\",\n \"adminAccount\": \"********\",\n \"db\": {...},\n \"mail\": {...},\n \"ldapLogin\": {\n \"enable\": true,\n \"server\": \"ldap://l-ldapt1.ops.dev.cn0.qunar.com\",\n \"baseDn\": \"CN=Admin,CN=Users,DC=test,DC=com\",\n \"bindPassword\": \"password123\",\n \"searchDn\": \"OU=UserContainer,DC=test,DC=com\",\n \"searchStandard\": \"mail\", // 自定义格式: \"searchStandard\": \"&(objectClass=user)(cn=%s)\"\n \"emailPostfix\": \"@163.com\",\n \"emailKey\": \"mail\",\n \"usernameKey\": \"name\"\n }\n}\n\n这里面的配置项含义如下enable 表示是否配置 LDAP 登录true(支持 LDAP登录 )/false(不支持LDAP登录);\nserver LDAP 服务器地址,前面需要加上 ldap:// 前缀,也可以是 ldaps:// 表示是通过 SSL 连接;\nbaseDn LDAP 服务器的登录用户名,必须是从根结点到用户节点的全路径;\nbindPassword 登录该 LDAP 服务器的密码;\nsearchDn 查询用户数据的路径,类似数据库中的一张表的地址,注意这里也必须是全路径;\nsearchStandard 查询条件,这里是 mail 表示查询用户信息是通过邮箱信息来查询的。注意该字段信息与LDAP数据库存储数据的字段相对应如果如果存储用户邮箱信息的字段是 email, 这里就需要修改成 email.1.3.18+支持自定义filter表达式基本形式为&(objectClass=user)(cn=%s), 其中%s会被username替换\nemailPostfix 登陆邮箱后缀(非必须)\nemailKey: ldap数据库存放邮箱信息的字段v1.3.21 新增 非必须)\nusernameKey: ldap数据库存放用户名信息的字段v1.3.21 新增 非必须)\n重启服务器后可以在登录页看到如下画面说明 ladp 配置成功"
"content": "配置LDAP登录打开项目目录 config.json 文件,添加如下字段:{ \"port\": \"*****\",\n \"adminAccount\": \"********\",\n \"db\": {...},\n \"mail\": {...},\n \"ldapLogin\": {\n \"enable\": true,\n \"server\": \"ldap://l-ldapt1.ops.dev.cn0.qunar.com\",\n \"baseDn\": \"CN=Admin,CN=Users,DC=test,DC=com\",\n \"bindPassword\": \"password123\",\n \"searchDn\": \"OU=UserContainer,DC=test,DC=com\",\n \"searchStandard\": \"mail\", // 自定义格式: \"searchStandard\": \"&(objectClass=user)(cn=%s)\"\n \"emailPostfix\": \"@163.com\",\n \"emailKey\": \"mail\",\n \"usernameKey\": \"name\"\n }\n}\n\n这里面的配置项含义如下enable 表示是否配置 LDAP 登录true(支持 LDAP登录 )/false(不支持LDAP登录);\nserver LDAP 服务器地址,前面需要加上 ldap:// 前缀,也可以是 ldaps:// 表示是通过 SSL 连接;\nbaseDn LDAP 服务器的登录用户名,必须是从根结点到用户节点的全路径(非必须);\nbindPassword 登录该 LDAP 服务器的密码(非必须);\nsearchDn 查询用户数据的路径,类似数据库中的一张表的地址,注意这里也必须是全路径;\nsearchStandard 查询条件,这里是 mail 表示查询用户信息是通过邮箱信息来查询的。注意该字段信息与LDAP数据库存储数据的字段相对应如果如果存储用户邮箱信息的字段是 email, 这里就需要修改成 email.1.3.18+支持自定义filter表达式基本形式为&(objectClass=user)(cn=%s), 其中%s会被username替换\nemailPostfix 登陆邮箱后缀(非必须)\nemailKey: ldap数据库存放邮箱信息的字段v1.3.21 新增 非必须)\nusernameKey: ldap数据库存放用户名信息的字段v1.3.21 新增 非必须)\n重启服务器后可以在登录页看到如下画面说明 ladp 配置成功"
},
{
"title": "禁止注册",
@ -1650,7 +1670,7 @@ window.ydoc_plugin_search_json = {
{
"title": "配置LDAP登录",
"url": "/devops/index.html#配置ldap登录",
"content": "配置LDAP登录打开项目目录 config.json 文件,添加如下字段:{ \"port\": \"*****\",\n \"adminAccount\": \"********\",\n \"db\": {...},\n \"mail\": {...},\n \"ldapLogin\": {\n \"enable\": true,\n \"server\": \"ldap://l-ldapt1.ops.dev.cn0.qunar.com\",\n \"baseDn\": \"CN=Admin,CN=Users,DC=test,DC=com\",\n \"bindPassword\": \"password123\",\n \"searchDn\": \"OU=UserContainer,DC=test,DC=com\",\n \"searchStandard\": \"mail\", // 自定义格式: \"searchStandard\": \"&(objectClass=user)(cn=%s)\"\n \"emailPostfix\": \"@163.com\",\n \"emailKey\": \"mail\",\n \"usernameKey\": \"name\"\n }\n}\n\n这里面的配置项含义如下enable 表示是否配置 LDAP 登录true(支持 LDAP登录 )/false(不支持LDAP登录);\nserver LDAP 服务器地址,前面需要加上 ldap:// 前缀,也可以是 ldaps:// 表示是通过 SSL 连接;\nbaseDn LDAP 服务器的登录用户名,必须是从根结点到用户节点的全路径;\nbindPassword 登录该 LDAP 服务器的密码;\nsearchDn 查询用户数据的路径,类似数据库中的一张表的地址,注意这里也必须是全路径;\nsearchStandard 查询条件,这里是 mail 表示查询用户信息是通过邮箱信息来查询的。注意该字段信息与LDAP数据库存储数据的字段相对应如果如果存储用户邮箱信息的字段是 email, 这里就需要修改成 email.1.3.18+支持自定义filter表达式基本形式为&(objectClass=user)(cn=%s), 其中%s会被username替换\nemailPostfix 登陆邮箱后缀(非必须)\nemailKey: ldap数据库存放邮箱信息的字段v1.3.21 新增 非必须)\nusernameKey: ldap数据库存放用户名信息的字段v1.3.21 新增 非必须)\n重启服务器后可以在登录页看到如下画面说明 ladp 配置成功"
"content": "配置LDAP登录打开项目目录 config.json 文件,添加如下字段:{ \"port\": \"*****\",\n \"adminAccount\": \"********\",\n \"db\": {...},\n \"mail\": {...},\n \"ldapLogin\": {\n \"enable\": true,\n \"server\": \"ldap://l-ldapt1.ops.dev.cn0.qunar.com\",\n \"baseDn\": \"CN=Admin,CN=Users,DC=test,DC=com\",\n \"bindPassword\": \"password123\",\n \"searchDn\": \"OU=UserContainer,DC=test,DC=com\",\n \"searchStandard\": \"mail\", // 自定义格式: \"searchStandard\": \"&(objectClass=user)(cn=%s)\"\n \"emailPostfix\": \"@163.com\",\n \"emailKey\": \"mail\",\n \"usernameKey\": \"name\"\n }\n}\n\n这里面的配置项含义如下enable 表示是否配置 LDAP 登录true(支持 LDAP登录 )/false(不支持LDAP登录);\nserver LDAP 服务器地址,前面需要加上 ldap:// 前缀,也可以是 ldaps:// 表示是通过 SSL 连接;\nbaseDn LDAP 服务器的登录用户名,必须是从根结点到用户节点的全路径(非必须);\nbindPassword 登录该 LDAP 服务器的密码(非必须);\nsearchDn 查询用户数据的路径,类似数据库中的一张表的地址,注意这里也必须是全路径;\nsearchStandard 查询条件,这里是 mail 表示查询用户信息是通过邮箱信息来查询的。注意该字段信息与LDAP数据库存储数据的字段相对应如果如果存储用户邮箱信息的字段是 email, 这里就需要修改成 email.1.3.18+支持自定义filter表达式基本形式为&(objectClass=user)(cn=%s), 其中%s会被username替换\nemailPostfix 登陆邮箱后缀(非必须)\nemailKey: ldap数据库存放邮箱信息的字段v1.3.21 新增 非必须)\nusernameKey: ldap数据库存放用户名信息的字段v1.3.21 新增 非必须)\n重启服务器后可以在登录页看到如下画面说明 ladp 配置成功"
},
{
"title": "禁止注册",

@ -1 +1 @@
window.WEBPACK_ASSETS = {"index.js":{"js":"index@841cead422a517b69145.js","css":"index@841cead422a517b69145.css"},"lib":{"js":"lib@250ff659130357ed53a3.js"},"lib2":{"js":"lib2@dd0309d1dda071c015ba.js"},"lib3":{"js":"lib3@117a77c58de265bbb1ec.js"},"manifest":{"js":"manifest@f2f4bd774d6c221b3d5f.js"}}

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

@ -165,4 +165,16 @@ test('isDeepMatch', t=>{
params: { a: 'x', b: 'y' },
res_body: '111',
code: 1 }, {t:'1'}))
})
})
test('isDeepMatch', t=>{
t.true(lib.isDeepMatch({ t:[{a: 1}]}, { t:[{a: 1}]}))
})
test('isDeepMatch', t=>{
t.false(lib.isDeepMatch({ t:[{a: 1, b: 12}]}, { t:[{a: 1}]}))
})
test('isDeepMatch', t=>{
t.true(lib.isDeepMatch([{a: 1}], [{a: 1}]))
})