mirror of
https://github.com/YMFE/yapi.git
synced 2024-11-21 01:13:51 +08:00
feat: 增加版本通知和siderbar滚动条
This commit is contained in:
commit
4441f7e797
11
CHANGELOG.md
11
CHANGELOG.md
@ -1,3 +1,14 @@
|
||||
### v1.3.19
|
||||
|
||||
* 增加项目文档记录wiki
|
||||
* 支持swagger URL 导入
|
||||
* 接口运行和测试集合中加入参数备注信息
|
||||
|
||||
#### Bug Fixed
|
||||
|
||||
* 修复测试用例名称为空时保存测试用例出现的bug
|
||||
|
||||
|
||||
### v1.3.18
|
||||
|
||||
* 增加全局接口搜索功能
|
||||
|
52
a.markdown
52
a.markdown
@ -1,26 +1,60 @@
|
||||
|
||||
<h1 class="curproject-name"> 导入导出测试 </h1>
|
||||
<h1 class="curproject-name"> 跨项目测试集合 </h1>
|
||||
|
||||
|
||||
|
||||
# my-test
|
||||
[TOC]
|
||||
|
||||
|
||||
# %u516C%u5171%u5206%u7C7B
|
||||
[TOC]
|
||||
|
||||
|
||||
# www
|
||||
[TOC]
|
||||
|
||||
|
||||
## test%0A%3Ca%20id%3Dtest%3E%20%3C/a%3E
|
||||
## test_2%0A%3Ca%20id%3Dtest_2%3E%20%3C/a%3E
|
||||
[TOC]
|
||||
|
||||
### 基本信息
|
||||
|
||||
**Path:** /api/col/list
|
||||
**Path:** /api/test/2
|
||||
|
||||
**Method:** GET
|
||||
**Method:** POST
|
||||
|
||||
**接口描述:**
|
||||
undefined
|
||||
|
||||
|
||||
### 请求参数
|
||||
**Headers**
|
||||
|
||||
| 参数名称 | 参数值 | 是否必须 | 示例 | 备注 |
|
||||
| ------------ | ------------ | ------------ | ------------ | ------------ |
|
||||
| Content-Type | application/json | 是 | | |
|
||||
**Body**
|
||||
|
||||
```javascript
|
||||
{
|
||||
"aa": 12
|
||||
}
|
||||
```
|
||||
### 返回数据
|
||||
|
||||
<table>
|
||||
<thead class="ant-table-thead">
|
||||
<tr>
|
||||
<th key=name>名称</th><th key=type>类型</th><th key=required>是否必须</th><th key=default>默认值</th><th key=desc>备注</th><th key=sub>其他信息</th>
|
||||
</tr>
|
||||
</thead><tbody className="ant-table-tbody"><tr key=0-0><td key=0><span style="padding-left: 0px"><span style="color: #8c8a8a"></span> a</span></td><td key=1><span>object []</span></td><td key=2>必须</td><td key=3></td><td key=4><span></span></td><td key=5><p key=3><span style="font-weight: '700'">item 类型: </span><span>object</span></p></td></tr><tr key=0-0-0><td key=0><span style="padding-left: 20px"><span style="color: #8c8a8a">├─</span> item1</span></td><td key=1><span>string</span></td><td key=2>非必须</td><td key=3></td><td key=4><span></span></td><td key=5></td></tr><tr key=0-0-1><td key=0><span style="padding-left: 20px"><span style="color: #8c8a8a">├─</span> item2</span></td><td key=1><span>string</span></td><td key=2>非必须</td><td key=3></td><td key=4><span></span></td><td key=5></td></tr><tr key=0-1><td key=0><span style="padding-left: 0px"><span style="color: #8c8a8a"></span> b</span></td><td key=1><span>string</span></td><td key=2>必须</td><td key=3></td><td key=4><span></span></td><td key=5></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
# %u4EA4%u6613%u4E2D%u53F0
|
||||
[TOC]
|
||||
|
||||
|
||||
# %u60F9%u6211
|
||||
[TOC]
|
||||
|
||||
|
||||
# %u6258%u5C14%u65AF%u6CF0
|
||||
[TOC]
|
||||
|
||||
|
@ -66,6 +66,36 @@ const InsertCodeMap = [
|
||||
}
|
||||
];
|
||||
|
||||
const ParamsNameComponent = props => {
|
||||
const { example, desc, name } = props;
|
||||
const isNull = !example && !desc;
|
||||
const TooltipTitle = () => {
|
||||
return (
|
||||
<div>
|
||||
{example && <div>示例: {example}</div>}
|
||||
{desc && <div>备注: {desc}</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{isNull ? (
|
||||
<Input disabled value={name} className="key" />
|
||||
) : (
|
||||
<Tooltip placement="topLeft" title={<TooltipTitle />}>
|
||||
<Input disabled value={name} className="key" />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
ParamsNameComponent.propTypes = {
|
||||
example: PropTypes.string,
|
||||
desc: PropTypes.string,
|
||||
name: PropTypes.string
|
||||
};
|
||||
|
||||
export default class Run extends Component {
|
||||
static propTypes = {
|
||||
data: PropTypes.object, //接口原有数据
|
||||
@ -160,7 +190,6 @@ export default class Run extends Component {
|
||||
required: true
|
||||
});
|
||||
body = JSON.stringify(result.data);
|
||||
console.log('body', body);
|
||||
}
|
||||
|
||||
this.setState(
|
||||
@ -291,7 +320,6 @@ export default class Run extends Component {
|
||||
res_body_type: 'json'
|
||||
});
|
||||
}
|
||||
|
||||
this.setState({
|
||||
resStatusCode: result.status,
|
||||
resStatusText: result.statusText,
|
||||
@ -559,7 +587,13 @@ export default class Run extends Component {
|
||||
{req_params.map((item, index) => {
|
||||
return (
|
||||
<div key={index} className="key-value-wrap">
|
||||
<Input disabled value={item.name} className="key" />
|
||||
{/* <Tooltip
|
||||
placement="topLeft"
|
||||
title={<TooltipContent example={item.example} desc={item.desc} />}
|
||||
>
|
||||
<Input disabled value={item.name} className="key" />
|
||||
</Tooltip> */}
|
||||
<ParamsNameComponent example={item.example} desc={item.desc} name={item.name} />
|
||||
<span className="eq-symbol">=</span>
|
||||
<Input
|
||||
value={item.value}
|
||||
@ -594,7 +628,13 @@ export default class Run extends Component {
|
||||
{req_query.map((item, index) => {
|
||||
return (
|
||||
<div key={index} className="key-value-wrap">
|
||||
<Input disabled value={item.name} className="key" />
|
||||
{/* <Tooltip
|
||||
placement="topLeft"
|
||||
title={<TooltipContent example={item.example} desc={item.desc} />}
|
||||
>
|
||||
<Input disabled value={item.name} className="key" />
|
||||
</Tooltip> */}
|
||||
<ParamsNameComponent example={item.example} desc={item.desc} name={item.name} />
|
||||
|
||||
{item.required == 1 ? (
|
||||
<Checkbox className="params-enable" checked={true} disabled />
|
||||
@ -632,7 +672,13 @@ export default class Run extends Component {
|
||||
{req_headers.map((item, index) => {
|
||||
return (
|
||||
<div key={index} className="key-value-wrap">
|
||||
<Input disabled value={item.name} className="key" />
|
||||
{/* <Tooltip
|
||||
placement="topLeft"
|
||||
title={<TooltipContent example={item.example} desc={item.desc} />}
|
||||
>
|
||||
<Input disabled value={item.name} className="key" />
|
||||
</Tooltip> */}
|
||||
<ParamsNameComponent example={item.example} desc={item.desc} name={item.name} />
|
||||
<span className="eq-symbol">=</span>
|
||||
<Input
|
||||
value={item.value}
|
||||
@ -682,7 +728,8 @@ export default class Run extends Component {
|
||||
高级参数设置
|
||||
</Button>
|
||||
<Tooltip title="高级参数设置只在json字段值中生效">
|
||||
{' '}<Icon type="question-circle-o" />
|
||||
{' '}
|
||||
<Icon type="question-circle-o" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
@ -703,7 +750,17 @@ export default class Run extends Component {
|
||||
{req_body_form.map((item, index) => {
|
||||
return (
|
||||
<div key={index} className="key-value-wrap">
|
||||
<Input disabled value={item.name} className="key" />
|
||||
{/* <Tooltip
|
||||
placement="topLeft"
|
||||
title={<TooltipContent example={item.example} desc={item.desc} />}
|
||||
>
|
||||
<Input disabled value={item.name} className="key" />
|
||||
</Tooltip> */}
|
||||
<ParamsNameComponent
|
||||
example={item.example}
|
||||
desc={item.desc}
|
||||
name={item.name}
|
||||
/>
|
||||
|
||||
{item.required == 1 ? (
|
||||
<Checkbox className="params-enable" checked={true} disabled />
|
||||
|
@ -16,13 +16,16 @@ import 'jsondiffpatch/public/formatters-styles/html.css';
|
||||
|
||||
import './TimeLine.scss';
|
||||
|
||||
const Option = AutoComplete.Option;
|
||||
// const Option = AutoComplete.Option;
|
||||
const {Option, OptGroup} = AutoComplete;
|
||||
|
||||
const AddDiffView = props => {
|
||||
const { title, content, className } = props;
|
||||
|
||||
if (!content) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<h3 className="title">{title}</h3>
|
||||
@ -93,8 +96,8 @@ function timeago(timestamp) {
|
||||
};
|
||||
},
|
||||
{
|
||||
fetchNewsData: fetchNewsData,
|
||||
fetchMoreNews: fetchMoreNews,
|
||||
fetchNewsData,
|
||||
fetchMoreNews,
|
||||
fetchInterfaceList
|
||||
}
|
||||
)
|
||||
@ -121,7 +124,7 @@ class TimeTree extends Component {
|
||||
curDiffData: {},
|
||||
apiList: []
|
||||
};
|
||||
this.curInterfaceId = '';
|
||||
this.curSelectValue = '';
|
||||
}
|
||||
|
||||
getMore() {
|
||||
@ -135,7 +138,7 @@ class TimeTree extends Component {
|
||||
this.props.type,
|
||||
this.props.curpage + 1,
|
||||
10,
|
||||
this.curInterfaceId
|
||||
this.curSelectValue
|
||||
)
|
||||
.then(function() {
|
||||
that.setState({ loading: false });
|
||||
@ -176,9 +179,9 @@ class TimeTree extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
handleSelectApi = interfaceId => {
|
||||
this.curInterfaceId = interfaceId;
|
||||
this.props.fetchNewsData(this.props.typeid, this.props.type, 1, 10, interfaceId);
|
||||
handleSelectApi = selectValue => {
|
||||
this.curSelectValue = selectValue;
|
||||
this.props.fetchNewsData(this.props.typeid, this.props.type, 1, 10, selectValue);
|
||||
};
|
||||
|
||||
render() {
|
||||
@ -198,7 +201,7 @@ class TimeTree extends Component {
|
||||
let methodColor = variable.METHOD_COLOR[item.method ? item.method.toLowerCase() : 'get'];
|
||||
return (
|
||||
<Option title={item.title} value={item._id + ''} path={item.path} key={item._id}>
|
||||
{item.path}{' '}
|
||||
{item.title}{' '}
|
||||
<Tag
|
||||
style={{ color: methodColor.color, backgroundColor: methodColor.bac, border: 'unset' }}
|
||||
>
|
||||
@ -217,7 +220,8 @@ class TimeTree extends Component {
|
||||
if (data && data.length) {
|
||||
data = data.map((item, i) => {
|
||||
let interfaceDiff = false;
|
||||
if (item.data && typeof item.data === 'object' && item.data.interface_id) {
|
||||
// 去掉了 && item.data.interface_id
|
||||
if (item.data && typeof item.data === 'object') {
|
||||
interfaceDiff = true;
|
||||
}
|
||||
return (
|
||||
@ -257,7 +261,8 @@ class TimeTree extends Component {
|
||||
pending = <Spin />;
|
||||
}
|
||||
let diffView = showDiffMsg(jsondiffpatch, formattersHtml, curDiffData);
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<section className="news-timeline">
|
||||
<Modal
|
||||
@ -290,12 +295,9 @@ class TimeTree extends Component {
|
||||
onSelect={this.handleSelectApi}
|
||||
style={{ width: '100%' }}
|
||||
placeholder="Select Api"
|
||||
optionLabelProp="path"
|
||||
optionLabelProp="title"
|
||||
filterOption={(inputValue, options) => {
|
||||
if (options.props.value == '')
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (options.props.value == '') return true;
|
||||
if (
|
||||
options.props.path.indexOf(inputValue) !== -1 ||
|
||||
options.props.title.indexOf(inputValue) !== -1
|
||||
@ -305,7 +307,14 @@ class TimeTree extends Component {
|
||||
return false;
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
{/* {children} */}
|
||||
<OptGroup label="other">
|
||||
<Option value="wiki" path="" title="wiki">wiki</Option>
|
||||
</OptGroup>
|
||||
<OptGroup label="api">
|
||||
{children}
|
||||
</OptGroup>
|
||||
|
||||
</AutoComplete>
|
||||
</Col>
|
||||
</Row>
|
||||
|
@ -218,6 +218,7 @@ class InterfaceColContent extends Component {
|
||||
result;
|
||||
try {
|
||||
result = await this.handleTest(curitem);
|
||||
|
||||
if (result.code === 400) {
|
||||
status = 'error';
|
||||
} else if (result.code === 0) {
|
||||
@ -288,6 +289,7 @@ class InterfaceColContent extends Component {
|
||||
}
|
||||
);
|
||||
await this.handleScriptTest(interfaceData, responseData, validRes, requestParams);
|
||||
|
||||
if (validRes.length === 0) {
|
||||
result.code = 0;
|
||||
result.validRes = [
|
||||
@ -316,7 +318,9 @@ class InterfaceColContent extends Component {
|
||||
};
|
||||
|
||||
//response, validRes
|
||||
// 断言测试
|
||||
handleScriptTest = async (interfaceData, response, validRes, requestParams) => {
|
||||
// 是否启动断言
|
||||
if (interfaceData.enable_script !== true) {
|
||||
return null;
|
||||
}
|
||||
|
@ -110,8 +110,8 @@ class Content extends Component {
|
||||
document.getElementsByTagName('title')[0].innerText =
|
||||
this.props.curdata.title + '-' + this.title;
|
||||
}
|
||||
|
||||
let InterfaceTabs = {
|
||||
|
||||
let InterfaceTabs = {
|
||||
view: {
|
||||
component: View,
|
||||
name: '预览'
|
||||
|
@ -22,7 +22,7 @@ require('tui-editor/dist/tui-editor.css'); // editor ui
|
||||
require('tui-editor/dist/tui-editor-contents.css'); // editor content
|
||||
require('highlight.js/styles/github.css'); // code block highlight
|
||||
require('./editor.css');
|
||||
var Editor = require('tui-editor');
|
||||
var Editor = require('tui-editor');
|
||||
|
||||
function checkIsJsonSchema(json) {
|
||||
try {
|
||||
|
@ -1,24 +1,24 @@
|
||||
import React, { PureComponent as Component } from 'react'
|
||||
import React, { PureComponent as Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types'
|
||||
import PropTypes from 'prop-types';
|
||||
import { Route, Switch, Redirect, matchPath } from 'react-router-dom';
|
||||
import { Subnav } from '../../components/index';
|
||||
import { fetchGroupMsg } from '../../reducer/modules/group';
|
||||
import { setBreadcrumb } from '../../reducer/modules/user';
|
||||
import { getProject } from '../../reducer/modules/project';
|
||||
import Interface from './Interface/Interface.js'
|
||||
import Activity from './Activity/Activity.js'
|
||||
import Setting from './Setting/Setting.js'
|
||||
import Interface from './Interface/Interface.js';
|
||||
import Activity from './Activity/Activity.js';
|
||||
import Setting from './Setting/Setting.js';
|
||||
import Loading from '../../components/Loading/Loading';
|
||||
import ProjectMember from './Setting/ProjectMember/ProjectMember.js';
|
||||
import ProjectData from './Setting/ProjectData/ProjectData.js';
|
||||
|
||||
const plugin = require('client/plugin.js');
|
||||
@connect(
|
||||
state => {
|
||||
return {
|
||||
curProject: state.project.currProject,
|
||||
currGroup: state.group.currGroup
|
||||
}
|
||||
};
|
||||
},
|
||||
{
|
||||
getProject,
|
||||
@ -26,9 +26,7 @@ import ProjectData from './Setting/ProjectData/ProjectData.js';
|
||||
setBreadcrumb
|
||||
}
|
||||
)
|
||||
|
||||
export default class Project extends Component {
|
||||
|
||||
static propTypes = {
|
||||
match: PropTypes.object,
|
||||
curProject: PropTypes.object,
|
||||
@ -37,80 +35,108 @@ export default class Project extends Component {
|
||||
fetchGroupMsg: PropTypes.func,
|
||||
setBreadcrumb: PropTypes.func,
|
||||
currGroup: PropTypes.object
|
||||
}
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
super(props);
|
||||
}
|
||||
|
||||
async componentWillMount() {
|
||||
await this.props.getProject(this.props.match.params.id);
|
||||
await this.props.fetchGroupMsg(this.props.curProject.group_id);
|
||||
|
||||
this.props.setBreadcrumb([{
|
||||
name: this.props.currGroup.group_name,
|
||||
href: '/group/' + this.props.currGroup._id
|
||||
}, {
|
||||
name: this.props.curProject.name
|
||||
}]);
|
||||
this.props.setBreadcrumb([
|
||||
{
|
||||
name: this.props.currGroup.group_name,
|
||||
href: '/group/' + this.props.currGroup._id
|
||||
},
|
||||
{
|
||||
name: this.props.curProject.name
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
async componentWillReceiveProps(nextProps) {
|
||||
const currProjectId = this.props.match.params.id;
|
||||
const nextProjectId = nextProps.match.params.id;
|
||||
if(currProjectId !== nextProjectId) {
|
||||
if (currProjectId !== nextProjectId) {
|
||||
await this.props.getProject(nextProjectId);
|
||||
await this.props.fetchGroupMsg(this.props.curProject.group_id);
|
||||
this.props.setBreadcrumb([{
|
||||
name: this.props.currGroup.group_name,
|
||||
href: '/group/' + this.props.currGroup._id
|
||||
}, {
|
||||
name: this.props.curProject.name
|
||||
}]);
|
||||
this.props.setBreadcrumb([
|
||||
{
|
||||
name: this.props.currGroup.group_name,
|
||||
href: '/group/' + this.props.currGroup._id
|
||||
},
|
||||
{
|
||||
name: this.props.curProject.name
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
render() {
|
||||
const { match, location } = this.props;
|
||||
let routers = {
|
||||
activity: { name: '动态', path: "/project/:id/activity" },
|
||||
interface: { name: '接口', path: "/project/:id/interface/:action" },
|
||||
setting: { name: '设置', path: "/project/:id/setting" },
|
||||
members: { name: '成员管理', path: "/project/:id/members" },
|
||||
data: { name: '数据管理', path: "/project/:id/data" }
|
||||
}
|
||||
interface: { name: '接口', path: '/project/:id/interface/:action', component: Interface },
|
||||
activity: { name: '动态', path: '/project/:id/activity', component: Activity },
|
||||
data: { name: '数据管理', path: '/project/:id/data', component: ProjectData },
|
||||
members: { name: '成员管理', path: '/project/:id/members', component: ProjectMember },
|
||||
setting: { name: '设置', path: '/project/:id/setting', component: Setting }
|
||||
};
|
||||
|
||||
plugin.emitHook('sub_nav', routers);
|
||||
|
||||
let key, defaultName;
|
||||
for (key in routers) {
|
||||
if (matchPath(location.pathname, {
|
||||
path: routers[key].path
|
||||
}) !== null) {
|
||||
if (
|
||||
matchPath(location.pathname, {
|
||||
path: routers[key].path
|
||||
}) !== null
|
||||
) {
|
||||
defaultName = routers[key].name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let subnavData = [{
|
||||
name: routers.interface.name,
|
||||
path: `/project/${match.params.id}/interface/api`
|
||||
}, {
|
||||
name: routers.activity.name,
|
||||
path: `/project/${match.params.id}/activity`
|
||||
}, {
|
||||
name: routers.data.name,
|
||||
path: `/project/${match.params.id}/data`
|
||||
}, {
|
||||
name: routers.members.name,
|
||||
path: `/project/${match.params.id}/members`
|
||||
}, {
|
||||
name: routers.setting.name,
|
||||
path: `/project/${match.params.id}/setting`
|
||||
}];
|
||||
|
||||
// let subnavData = [{
|
||||
// name: routers.interface.name,
|
||||
// path: `/project/${match.params.id}/interface/api`
|
||||
// }, {
|
||||
// name: routers.activity.name,
|
||||
// path: `/project/${match.params.id}/activity`
|
||||
// }, {
|
||||
// name: routers.data.name,
|
||||
// path: `/project/${match.params.id}/data`
|
||||
// }, {
|
||||
// name: routers.members.name,
|
||||
// path: `/project/${match.params.id}/members`
|
||||
// }, {
|
||||
// name: routers.setting.name,
|
||||
// path: `/project/${match.params.id}/setting`
|
||||
// }];
|
||||
|
||||
let subnavData = [];
|
||||
Object.keys(routers).forEach(key => {
|
||||
let item = routers[key];
|
||||
let value = {};
|
||||
if (key === 'interface') {
|
||||
value = {
|
||||
name: item.name,
|
||||
path: `/project/${match.params.id}/interface/api`
|
||||
};
|
||||
} else {
|
||||
value = {
|
||||
name: item.name,
|
||||
path: item.path.replace(/\:id/gi, match.params.id)
|
||||
};
|
||||
}
|
||||
subnavData.push(value);
|
||||
});
|
||||
|
||||
if (this.props.currGroup.type === 'private') {
|
||||
subnavData = subnavData.filter(item => {
|
||||
return item.name != '成员管理'
|
||||
})
|
||||
return item.name != '成员管理';
|
||||
});
|
||||
}
|
||||
|
||||
if (Object.keys(this.props.curProject).length === 0) {
|
||||
@ -119,22 +145,31 @@ export default class Project extends Component {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Subnav
|
||||
default={defaultName}
|
||||
data={subnavData} />
|
||||
<Subnav default={defaultName} data={subnavData} />
|
||||
<Switch>
|
||||
<Redirect exact from="/project/:id" to={`/project/${match.params.id}/interface/api`} />
|
||||
<Route path={routers.activity.path} component={Activity} />
|
||||
<Route path={routers.interface.path} component={Interface} />
|
||||
{/* <Route path={routers.activity.path} component={Activity} />
|
||||
|
||||
<Route path={routers.setting.path} component={Setting} />
|
||||
{this.props.currGroup.type !== 'private' ?
|
||||
<Route path={routers.members.path} component={ProjectMember} />
|
||||
<Route path={routers.members.path} component={routers.members.component}/>
|
||||
: null
|
||||
}
|
||||
|
||||
<Route path={routers.data.path} component={ProjectData} />
|
||||
<Route path={routers.data.path} component={ProjectData} /> */}
|
||||
{Object.keys(routers).map(key => {
|
||||
let item = routers[key];
|
||||
|
||||
return key === 'members' ? (
|
||||
this.props.currGroup.type !== 'private' ? (
|
||||
<Route path={item.path} component={item.component} key={key}/>
|
||||
) : null
|
||||
) : (
|
||||
<Route path={item.path} component={item.component} key={key}/>
|
||||
);
|
||||
})}
|
||||
</Switch>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { PureComponent as Component } from 'react';
|
||||
import { Upload, Icon, message, Select, Tooltip, Button, Spin, Switch, Modal, Radio } from 'antd';
|
||||
import { Upload, Icon, message, Select, Tooltip, Button, Spin, Switch, Modal, Radio, Input } from 'antd';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import './ProjectData.scss';
|
||||
@ -59,7 +59,9 @@ class ProjectData extends Component {
|
||||
curExportType: null,
|
||||
showLoading: false,
|
||||
dataSync: false,
|
||||
exportContent: 'all'
|
||||
exportContent: 'all',
|
||||
isSwaggerUrl: false,
|
||||
swaggerUrl: ''
|
||||
};
|
||||
}
|
||||
static propTypes = {
|
||||
@ -116,6 +118,7 @@ class ProjectData extends Component {
|
||||
);
|
||||
};
|
||||
|
||||
// 本地文件上传
|
||||
handleFile = info => {
|
||||
if (!this.state.curImportType) {
|
||||
return message.error('请选择导入数据的方式');
|
||||
@ -198,12 +201,61 @@ class ProjectData extends Component {
|
||||
});
|
||||
};
|
||||
|
||||
// 处理导入信息同步
|
||||
onChange = checked => {
|
||||
this.setState({
|
||||
dataSync: checked
|
||||
});
|
||||
};
|
||||
|
||||
// 处理swagger URL 导入
|
||||
handleUrlChange = checked => {
|
||||
this.setState({
|
||||
isSwaggerUrl: checked
|
||||
});
|
||||
};
|
||||
|
||||
// 记录输入的url
|
||||
swaggerUrlInput = url => {
|
||||
this.setState({
|
||||
swaggerUrl: url
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
// url导入上传
|
||||
onUrlUpload = async () => {
|
||||
|
||||
if (!this.state.curImportType) {
|
||||
return message.error('请选择导入数据的方式');
|
||||
}
|
||||
|
||||
if(!this.state.swaggerUrl) {
|
||||
return message.error('url 不能为空');
|
||||
}
|
||||
if (this.state.selectCatid) {
|
||||
this.setState({ showLoading: true });
|
||||
try {
|
||||
let content = await axios(this.state.swaggerUrl);
|
||||
content = content.data;
|
||||
let res = await importDataModule[this.state.curImportType].run(content);
|
||||
if (this.state.dataSync) {
|
||||
// 开启同步
|
||||
this.showConfirm(res);
|
||||
} else {
|
||||
// 未开启同步
|
||||
await this.handleAddInterface(res);
|
||||
}
|
||||
} catch (e) {
|
||||
this.setState({ showLoading: false });
|
||||
message.error(e.message);
|
||||
}
|
||||
} else {
|
||||
message.error('请选择上传的默认分类');
|
||||
}
|
||||
};
|
||||
|
||||
handleChange = e => {
|
||||
this.setState({ exportContent: e.target.value });
|
||||
};
|
||||
@ -290,27 +342,51 @@ class ProjectData extends Component {
|
||||
|
||||
<Switch checked={this.state.dataSync} onChange={this.onChange} />
|
||||
</div>
|
||||
<div style={{ marginTop: 16, height: 180 }}>
|
||||
<Spin spinning={this.state.showLoading} tip="上传中...">
|
||||
<Dragger {...uploadMess}>
|
||||
<p className="ant-upload-drag-icon">
|
||||
<Icon type="inbox" />
|
||||
</p>
|
||||
<p className="ant-upload-text">点击或者拖拽文件到上传区域</p>
|
||||
<p
|
||||
className="ant-upload-hint"
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: this.state.curImportType
|
||||
? importDataModule[this.state.curImportType].desc
|
||||
: null
|
||||
}}
|
||||
/>
|
||||
</Dragger>
|
||||
</Spin>
|
||||
</div>
|
||||
{this.state.curImportType === 'swagger' && (
|
||||
<div className="dataSync">
|
||||
<span>
|
||||
开启url导入<Tooltip title="swagger url 导入">
|
||||
<Icon type="question-circle-o" />
|
||||
</Tooltip>{' '}
|
||||
:
|
||||
</span>
|
||||
|
||||
<Switch checked={this.state.isSwaggerUrl} onChange={this.handleUrlChange} />
|
||||
</div>
|
||||
)}
|
||||
{this.state.isSwaggerUrl ? (
|
||||
<div className="import-content url-import-content">
|
||||
<Input
|
||||
placeholder="http://demo.swagger.io/v2/swagger.json"
|
||||
onChange={e => this.swaggerUrlInput(e.target.value)}
|
||||
/>
|
||||
<Button type="primary" className="url-btn" onClick={this.onUrlUpload} loading={this.state.showLoading}>
|
||||
上传
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="import-content">
|
||||
<Spin spinning={this.state.showLoading} tip="上传中...">
|
||||
<Dragger {...uploadMess}>
|
||||
<p className="ant-upload-drag-icon">
|
||||
<Icon type="inbox" />
|
||||
</p>
|
||||
<p className="ant-upload-text">点击或者拖拽文件到上传区域</p>
|
||||
<p
|
||||
className="ant-upload-hint"
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: this.state.curImportType
|
||||
? importDataModule[this.state.curImportType].desc
|
||||
: null
|
||||
}}
|
||||
/>
|
||||
</Dragger>
|
||||
</Spin>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
@ -35,6 +35,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
.import-content {
|
||||
margin-top: 16px;
|
||||
height: 180px;
|
||||
}
|
||||
|
||||
.url-import-content {
|
||||
text-align: center;
|
||||
.url-btn{
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.export-content{
|
||||
text-align: center;
|
||||
}
|
||||
|
@ -170,7 +170,27 @@ hooks = {
|
||||
type: 'listener',
|
||||
mulit: true,
|
||||
listener: []
|
||||
},
|
||||
|
||||
/*
|
||||
* 添加 subnav 钩子
|
||||
* @param Object reducerModules
|
||||
*
|
||||
* let routers = {
|
||||
interface: { name: '接口', path: "/project/:id/interface/:action", component:Interface },
|
||||
activity: { name: '动态', path: "/project/:id/activity", component: Activity},
|
||||
data: { name: '数据管理', path: "/project/:id/data", component: ProjectData},
|
||||
members: { name: '成员管理', path: "/project/:id/members" , component: ProjectMember},
|
||||
setting: { name: '设置', path: "/project/:id/setting" , component: Setting}
|
||||
}
|
||||
*
|
||||
*/
|
||||
sub_nav: {
|
||||
type: 'listener',
|
||||
mulit: true,
|
||||
listener: []
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
function bindHook(name, listener) {
|
||||
|
@ -53,14 +53,14 @@ export default (state = initialState, action) => {
|
||||
import axios from 'axios';
|
||||
import variable from '../../constants/variable';
|
||||
|
||||
export function fetchNewsData(typeid, type, page, limit, interfaceId) {
|
||||
export function fetchNewsData(typeid, type, page, limit, selectValue) {
|
||||
let param = {
|
||||
typeid: typeid,
|
||||
type: type,
|
||||
page: page,
|
||||
limit: limit ? limit : variable.PAGE_LIMIT,
|
||||
interface_id: interfaceId
|
||||
};
|
||||
selectValue
|
||||
}
|
||||
|
||||
return {
|
||||
type: FETCH_NEWS_DATA,
|
||||
@ -69,14 +69,14 @@ export function fetchNewsData(typeid, type, page, limit, interfaceId) {
|
||||
})
|
||||
};
|
||||
}
|
||||
export function fetchMoreNews(typeid, type, page, limit, interfaceId) {
|
||||
export function fetchMoreNews(typeid, type, page, limit, selectValue) {
|
||||
const param = {
|
||||
typeid: typeid,
|
||||
type: type,
|
||||
page: page,
|
||||
limit: limit ? limit : variable.PAGE_LIMIT,
|
||||
interface_id: interfaceId
|
||||
};
|
||||
selectValue
|
||||
}
|
||||
return {
|
||||
type: FETCH_MORE_NEWS,
|
||||
payload: axios.get('/api/log/list', {
|
||||
|
@ -13,5 +13,7 @@ module.exports = {
|
||||
name: 'export-data'
|
||||
},{
|
||||
name: 'import-yapi-json'
|
||||
},{
|
||||
name: 'wiki'
|
||||
}]
|
||||
}
|
@ -66,7 +66,17 @@ module.exports = function (jsondiffpatch, formattersHtml, curDiffData) {
|
||||
|
||||
|
||||
if (curDiffData && typeof curDiffData === 'object' && curDiffData.current) {
|
||||
const { current, old } = curDiffData;
|
||||
const { current, old, type } = curDiffData;
|
||||
// wiki 信息的diff 输出
|
||||
if(type === 'wiki') {
|
||||
if (current != old) {
|
||||
diffView.push({
|
||||
title: 'wiki更新',
|
||||
content: diffText(old, current)
|
||||
})
|
||||
}
|
||||
return diffView = diffView.filter(item => item.content)
|
||||
}
|
||||
if (current.path != old.path) {
|
||||
diffView.push({
|
||||
title: 'Api 路径',
|
||||
|
@ -169,4 +169,5 @@ exports.json_format= function(json){
|
||||
}catch(e){
|
||||
return json;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,7 +5,7 @@
|
||||
YApi 为了解决这个问题,开发了可视化接口自动化测试功能,只需要配置每个接口的入参和对 RESPONSE 断言,即可实现对接口的自动化测试,大大提升了接口测试的效率。
|
||||
|
||||
## 第一步,测试集合
|
||||
使用 YApi 自动化测试,第一步需要做得是创建测试集合和导入接口,点击添加集合创建,创建完成后导入接口。
|
||||
使用 YApi 自动化测试,第一步需要做得是创建测试集合和导入接口,点击添加集合创建,创建完成后导入接口(同一个接口可以多次导入)。
|
||||
|
||||
![](case-col.png)
|
||||
|
||||
|
@ -43,13 +43,15 @@ module.exports = function(){
|
||||
let caseInst = yapi.getInst(caseModel);
|
||||
|
||||
// let ip = ctx.ip.match(/\d+.\d+.\d+.\d+/)[0];
|
||||
// request.ip
|
||||
let ip = yapi.commons.getIp(ctx)
|
||||
// 数据库信息查询
|
||||
let listWithIp =await caseInst.model.find({
|
||||
interface_id: interfaceId,
|
||||
ip_enable: true,
|
||||
ip: ip
|
||||
}).select('_id params');
|
||||
}).select('_id params');
|
||||
|
||||
let matchList = [];
|
||||
listWithIp.forEach(item=>{
|
||||
let params = item.params;
|
||||
@ -158,8 +160,10 @@ module.exports = function(){
|
||||
this.bindHook('mock_after', async function(context){
|
||||
let interfaceId = context.interfaceData._id;
|
||||
let caseData = await checkCase(context.ctx, interfaceId);
|
||||
|
||||
if(caseData){
|
||||
let data = await handleByCase(caseData);
|
||||
|
||||
context.mockJson = yapi.commons.json_parse(data.res_body);
|
||||
try{
|
||||
context.mockJson = Mock.mock(mockExtra(context.mockJson, {
|
||||
|
12
exts/yapi-plugin-wiki/client.js
Normal file
12
exts/yapi-plugin-wiki/client.js
Normal file
@ -0,0 +1,12 @@
|
||||
import WikiPage from './wikiPage/index'
|
||||
// const WikiPage = require('./wikiPage/index')
|
||||
|
||||
module.exports = function(){
|
||||
this.bindHook('sub_nav', function(app){
|
||||
app.wiki = {
|
||||
name: 'Wiki',
|
||||
path: '/project/:id/wiki',
|
||||
component: WikiPage
|
||||
}
|
||||
})
|
||||
}
|
222
exts/yapi-plugin-wiki/controller.js
Normal file
222
exts/yapi-plugin-wiki/controller.js
Normal file
@ -0,0 +1,222 @@
|
||||
const baseController = require('controllers/base.js');
|
||||
const wikiModel = require('./wikiModel.js');
|
||||
const projectModel = require('models/project.js');
|
||||
const userModel = require('models/user.js');
|
||||
const jsondiffpatch = require('jsondiffpatch');
|
||||
const formattersHtml = jsondiffpatch.formatters.html;
|
||||
const yapi = require('yapi.js');
|
||||
// const util = require('./util.js');
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
const showDiffMsg = require('../../common/diff-view.js');
|
||||
class wikiController extends baseController {
|
||||
constructor(ctx) {
|
||||
super(ctx);
|
||||
this.Model = yapi.getInst(wikiModel);
|
||||
this.projectModel = yapi.getInst(projectModel);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取wiki信息
|
||||
* @interface wiki_desc/get
|
||||
* @method get
|
||||
* @category statistics
|
||||
* @foldnumber 10
|
||||
* @returns {Object}
|
||||
*/
|
||||
async getWikiDesc(ctx) {
|
||||
try {
|
||||
let project_id = ctx.request.query.project_id;
|
||||
if (!project_id) {
|
||||
return (ctx.body = yapi.commons.resReturn(null, 400, '项目id不能为空'));
|
||||
}
|
||||
let result = await this.Model.get(project_id);
|
||||
return (ctx.body = yapi.commons.resReturn(result));
|
||||
} catch (err) {
|
||||
ctx.body = yapi.commons.resReturn(null, 400, err.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存wiki信息
|
||||
* @interface wiki_desc/get
|
||||
* @method get
|
||||
* @category statistics
|
||||
* @foldnumber 10
|
||||
* @returns {Object}
|
||||
*/
|
||||
|
||||
async uplodaWikiDesc(ctx) {
|
||||
try {
|
||||
let params = ctx.request.body;
|
||||
params = yapi.commons.handleParams(params, {
|
||||
project_id: 'number',
|
||||
desc: 'string',
|
||||
markdown: 'string'
|
||||
});
|
||||
|
||||
if (!params.project_id) {
|
||||
return (ctx.body = yapi.commons.resReturn(null, 400, '项目id不能为空'));
|
||||
}
|
||||
if (!this.$tokenAuth) {
|
||||
let auth = await this.checkAuth(params.project_id, 'project', 'edit');
|
||||
if (!auth) {
|
||||
return (ctx.body = yapi.commons.resReturn(null, 400, '没有权限'));
|
||||
}
|
||||
}
|
||||
|
||||
let notice = params.email_notice;
|
||||
delete params.email_notice;
|
||||
const username = this.getUsername();
|
||||
const uid = this.getUid();
|
||||
|
||||
// 如果当前数据库里面没有数据
|
||||
let result = await this.Model.get(params.project_id);
|
||||
if (!result) {
|
||||
let data = Object.assign(params, {
|
||||
username,
|
||||
uid,
|
||||
add_time: yapi.commons.time(),
|
||||
up_time: yapi.commons.time()
|
||||
});
|
||||
|
||||
let res = await this.Model.save(data);
|
||||
ctx.body = yapi.commons.resReturn(res);
|
||||
} else {
|
||||
let data = Object.assign(params, {
|
||||
username,
|
||||
uid,
|
||||
up_time: yapi.commons.time()
|
||||
});
|
||||
let upRes = await this.Model.up(result._id, data);
|
||||
ctx.body = yapi.commons.resReturn(upRes);
|
||||
}
|
||||
|
||||
let logData = {
|
||||
type: 'wiki',
|
||||
project_id: params.project_id,
|
||||
current: params.desc,
|
||||
old: result ? result.toObject().desc : ''
|
||||
};
|
||||
let wikiUrl = `http://${ctx.request.host}/project/${params.project_id}/wiki`;
|
||||
|
||||
if (notice) {
|
||||
let diffView = showDiffMsg(jsondiffpatch, formattersHtml, logData);
|
||||
|
||||
let annotatedCss = fs.readFileSync(
|
||||
path.resolve(
|
||||
yapi.WEBROOT,
|
||||
'node_modules/jsondiffpatch/public/formatters-styles/annotated.css'
|
||||
),
|
||||
'utf8'
|
||||
);
|
||||
let htmlCss = fs.readFileSync(
|
||||
path.resolve(
|
||||
yapi.WEBROOT,
|
||||
'node_modules/jsondiffpatch/public/formatters-styles/html.css'
|
||||
),
|
||||
'utf8'
|
||||
);
|
||||
let project = await this.projectModel.getBaseInfo(params.project_id);
|
||||
|
||||
yapi.commons.sendNotice(params.project_id, {
|
||||
title: `${username} 更新了wiki说明`,
|
||||
content: `<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<style>
|
||||
${annotatedCss}
|
||||
${htmlCss}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div><h3>${username}更新了wiki说明</h3>
|
||||
<p>修改用户: ${username}</p>
|
||||
<p>修改项目: <a href="${wikiUrl}">${project.name}</a></p>
|
||||
<p>详细改动日志: ${this.diffHTML(diffView)}</p></div>
|
||||
</body>
|
||||
</html>`
|
||||
});
|
||||
}
|
||||
|
||||
// 保存修改日志信息
|
||||
yapi.commons.saveLog({
|
||||
content: `<a href="/user/profile/${uid}">${username}</a> 更新了 <a href="${wikiUrl}">wiki</a> 的信息`,
|
||||
type: 'project',
|
||||
uid,
|
||||
username: username,
|
||||
typeid: params.project_id,
|
||||
data: logData
|
||||
});
|
||||
return 1;
|
||||
} catch (err) {
|
||||
ctx.body = yapi.commons.resReturn(null, 400, err.message);
|
||||
}
|
||||
}
|
||||
diffHTML(html) {
|
||||
if (html.length === 0) {
|
||||
return `<span style="color: #555">没有改动,该操作未改动wiki数据</span>`;
|
||||
}
|
||||
|
||||
return html.map(item => {
|
||||
return `<div>
|
||||
<h4 class="title">${item.title}</h4>
|
||||
<div>${item.content}</div>
|
||||
</div>`;
|
||||
});
|
||||
}
|
||||
|
||||
// 处理编辑冲突
|
||||
async wikiConflict(ctx) {
|
||||
try {
|
||||
let result;
|
||||
ctx.websocket.on('message', async message => {
|
||||
let id = parseInt(ctx.query.id, 10);
|
||||
if (!id) {
|
||||
return ctx.websocket.send('id 参数有误');
|
||||
}
|
||||
result = await this.Model.get(id);
|
||||
console.log(message);
|
||||
|
||||
switch (message) {
|
||||
case 'start':
|
||||
if (result && result.edit_uid === this.getUid()) {
|
||||
await this.Model.upEditUid(result._id, 0);
|
||||
}
|
||||
break;
|
||||
case 'editor':
|
||||
let userInst, userinfo, data;
|
||||
|
||||
if (result && result.edit_uid !== 0 && result.edit_uid !== this.getUid()) {
|
||||
userInst = yapi.getInst(userModel);
|
||||
userinfo = await userInst.findById(result.edit_uid);
|
||||
data = {
|
||||
errno: result.edit_uid,
|
||||
data: { uid: result.edit_uid, username: userinfo.username }
|
||||
};
|
||||
} else {
|
||||
if (result) {
|
||||
await this.Model.upEditUid(result._id, this.getUid());
|
||||
}
|
||||
data = {
|
||||
errno: 0,
|
||||
data: result
|
||||
};
|
||||
}
|
||||
ctx.websocket.send(JSON.stringify(data));
|
||||
break;
|
||||
case 'end':
|
||||
await this.Model.upEditUid(result._id, 0);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
ctx.websocket.on('close', async () => {});
|
||||
} catch (err) {
|
||||
yapi.commons.log(err, 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = wikiController;
|
64
exts/yapi-plugin-wiki/index.js
Normal file
64
exts/yapi-plugin-wiki/index.js
Normal file
@ -0,0 +1,64 @@
|
||||
module.exports = {
|
||||
server: true,
|
||||
client: true,
|
||||
httpCodes: [
|
||||
100,
|
||||
101,
|
||||
102,
|
||||
200,
|
||||
201,
|
||||
202,
|
||||
203,
|
||||
204,
|
||||
205,
|
||||
206,
|
||||
207,
|
||||
208,
|
||||
226,
|
||||
300,
|
||||
301,
|
||||
302,
|
||||
303,
|
||||
304,
|
||||
305,
|
||||
307,
|
||||
308,
|
||||
400,
|
||||
401,
|
||||
402,
|
||||
403,
|
||||
404,
|
||||
405,
|
||||
406,
|
||||
407,
|
||||
408,
|
||||
409,
|
||||
410,
|
||||
411,
|
||||
412,
|
||||
413,
|
||||
414,
|
||||
415,
|
||||
416,
|
||||
417,
|
||||
418,
|
||||
422,
|
||||
423,
|
||||
424,
|
||||
426,
|
||||
428,
|
||||
429,
|
||||
431,
|
||||
500,
|
||||
501,
|
||||
502,
|
||||
503,
|
||||
504,
|
||||
505,
|
||||
506,
|
||||
507,
|
||||
508,
|
||||
510,
|
||||
511
|
||||
]
|
||||
};
|
39
exts/yapi-plugin-wiki/server.js
Normal file
39
exts/yapi-plugin-wiki/server.js
Normal file
@ -0,0 +1,39 @@
|
||||
const yapi = require('yapi.js');
|
||||
const mongoose = require('mongoose');
|
||||
const controller = require('./controller');
|
||||
|
||||
module.exports = function() {
|
||||
yapi.connect.then(function() {
|
||||
let Col = mongoose.connection.db.collection('wiki');
|
||||
Col.createIndex({
|
||||
project_id: 1
|
||||
});
|
||||
});
|
||||
|
||||
this.bindHook('add_router', function(addRouter) {
|
||||
addRouter({
|
||||
// 获取wiki信息
|
||||
controller: controller,
|
||||
method: 'get',
|
||||
path: 'wiki_desc/get',
|
||||
action: 'getWikiDesc'
|
||||
});
|
||||
|
||||
addRouter({
|
||||
// 更新wiki信息
|
||||
controller: controller,
|
||||
method: 'post',
|
||||
path: 'wiki_desc/up',
|
||||
action: 'uplodaWikiDesc'
|
||||
});
|
||||
});
|
||||
|
||||
this.bindHook('add_ws_router', function(wsRouter) {
|
||||
wsRouter({
|
||||
controller: controller,
|
||||
method: 'get',
|
||||
path: 'wiki_desc/solve_conflict',
|
||||
action: 'wikiConflict'
|
||||
});
|
||||
});
|
||||
};
|
74
exts/yapi-plugin-wiki/util.js
Normal file
74
exts/yapi-plugin-wiki/util.js
Normal file
@ -0,0 +1,74 @@
|
||||
|
||||
// 时间
|
||||
const convert2Decimal = num => (num > 9 ? num : `0${num}`);
|
||||
|
||||
/**
|
||||
* 格式化 年、月、日、时、分、秒
|
||||
* @param val {Object or String or Number} 日期对象 或是可new Date的对象或时间戳
|
||||
* @return {String} 2017-01-20 20:00:00
|
||||
*/
|
||||
exports.formatDate = val => {
|
||||
let date = val;
|
||||
if (typeof val !== 'object') {
|
||||
date = new Date(val);
|
||||
}
|
||||
return `${[
|
||||
date.getFullYear(),
|
||||
convert2Decimal(date.getMonth() + 1),
|
||||
convert2Decimal(date.getDate())
|
||||
].join('-')} ${[
|
||||
convert2Decimal(date.getHours()),
|
||||
convert2Decimal(date.getMinutes()),
|
||||
convert2Decimal(date.getSeconds())
|
||||
].join(':')}`;
|
||||
};
|
||||
|
||||
|
||||
|
||||
// const json5_parse = require('../client/common.js').json5_parse;
|
||||
|
||||
exports.timeago = timestamp => {
|
||||
let minutes, hours, days, seconds, mouth, year;
|
||||
const timeNow = parseInt(new Date().getTime() / 1000);
|
||||
seconds = timeNow - timestamp;
|
||||
if (seconds > 86400 * 30 * 12) {
|
||||
year = parseInt(seconds / (86400 * 30 * 12));
|
||||
} else {
|
||||
year = 0;
|
||||
}
|
||||
if (seconds > 86400 * 30) {
|
||||
mouth = parseInt(seconds / (86400 * 30));
|
||||
} else {
|
||||
mouth = 0;
|
||||
}
|
||||
if (seconds > 86400) {
|
||||
days = parseInt(seconds / 86400);
|
||||
} else {
|
||||
days = 0;
|
||||
}
|
||||
if (seconds > 3600) {
|
||||
hours = parseInt(seconds / 3600);
|
||||
} else {
|
||||
hours = 0;
|
||||
}
|
||||
minutes = parseInt(seconds / 60);
|
||||
if (year > 0) {
|
||||
return year + '年前';
|
||||
} else if (mouth > 0 && year <= 0) {
|
||||
return mouth + '月前';
|
||||
} else if (days > 0 && mouth <= 0) {
|
||||
return days + '天前';
|
||||
} else if (days <= 0 && hours > 0) {
|
||||
return hours + '小时前';
|
||||
} else if (hours <= 0 && minutes > 0) {
|
||||
return minutes + '分钟前';
|
||||
} else if (minutes <= 0 && seconds > 0) {
|
||||
if (seconds < 30) {
|
||||
return '刚刚';
|
||||
} else {
|
||||
return seconds + '秒前';
|
||||
}
|
||||
} else {
|
||||
return '刚刚';
|
||||
}
|
||||
};
|
58
exts/yapi-plugin-wiki/wikiModel.js
Normal file
58
exts/yapi-plugin-wiki/wikiModel.js
Normal file
@ -0,0 +1,58 @@
|
||||
const yapi = require('yapi.js');
|
||||
const baseModel = require('models/base.js');
|
||||
|
||||
class statisMockModel extends baseModel {
|
||||
getName() {
|
||||
return 'wiki';
|
||||
}
|
||||
|
||||
getSchema() {
|
||||
return {
|
||||
project_id: { type: Number, required: true },
|
||||
username: String,
|
||||
uid: { type: Number, required: true },
|
||||
edit_uid: { type: Number, default: 0 },
|
||||
desc: String,
|
||||
markdown: String,
|
||||
add_time: Number,
|
||||
up_time: Number
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
save(data) {
|
||||
let m = new this.model(data);
|
||||
return m.save();
|
||||
}
|
||||
|
||||
|
||||
|
||||
get(project_id) {
|
||||
return this.model.findOne({
|
||||
project_id: project_id
|
||||
})
|
||||
.exec();
|
||||
}
|
||||
|
||||
|
||||
up(id, data) {
|
||||
return this.model.update(
|
||||
{
|
||||
_id: id
|
||||
},
|
||||
data,
|
||||
{ runValidators: true }
|
||||
);
|
||||
}
|
||||
|
||||
upEditUid(id, uid) {
|
||||
return this.model.update({
|
||||
_id: id
|
||||
},
|
||||
{ edit_uid: uid },
|
||||
{ runValidators: true });
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = statisMockModel;
|
71
exts/yapi-plugin-wiki/wikiPage/Editor.js
Normal file
71
exts/yapi-plugin-wiki/wikiPage/Editor.js
Normal file
@ -0,0 +1,71 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, Checkbox } from 'antd';
|
||||
// deps for editor
|
||||
require('codemirror/lib/codemirror.css'); // codemirror
|
||||
require('tui-editor/dist/tui-editor.css'); // editor ui
|
||||
require('tui-editor/dist/tui-editor-contents.css'); // editor content
|
||||
require('highlight.js/styles/github.css'); // code block highlight
|
||||
var Editor = require('tui-editor');
|
||||
|
||||
class WikiEditor extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
isConflict: PropTypes.bool,
|
||||
onUpload: PropTypes.func,
|
||||
onCancel: PropTypes.func,
|
||||
notice: PropTypes.bool,
|
||||
onEmailNotice: PropTypes.func,
|
||||
desc: PropTypes.string
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.editor = new Editor({
|
||||
el: document.querySelector('#desc'),
|
||||
initialEditType: 'wysiwyg',
|
||||
height: '500px',
|
||||
initialValue: this.props.desc
|
||||
});
|
||||
}
|
||||
|
||||
onUpload = () => {
|
||||
let desc = this.editor.getHtml();
|
||||
let markdown = this.editor.getMarkdown();
|
||||
this.props.onUpload(desc, markdown);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { isConflict, onCancel, notice, onEmailNotice } = this.props;
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
id="desc"
|
||||
className="wiki-editor"
|
||||
style={{ display: !isConflict ? 'block' : 'none' }}
|
||||
/>
|
||||
<div className="wiki-title wiki-up">
|
||||
<Button
|
||||
icon="upload"
|
||||
type="primary"
|
||||
className="upload-btn"
|
||||
disabled={isConflict}
|
||||
onClick={this.onUpload}
|
||||
>
|
||||
更新
|
||||
</Button>
|
||||
<Button onClick={onCancel} className="upload-btn">
|
||||
取消
|
||||
</Button>
|
||||
<Checkbox checked={notice} onChange={onEmailNotice}>
|
||||
通知相关人员
|
||||
</Checkbox>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default WikiEditor;
|
41
exts/yapi-plugin-wiki/wikiPage/View.js
Normal file
41
exts/yapi-plugin-wiki/wikiPage/View.js
Normal file
@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button } from 'antd';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
const WikiView = props => {
|
||||
const { editorEable, onEditor, uid, username, editorTime, desc } = props;
|
||||
return (
|
||||
<div className="wiki-view-content">
|
||||
<div className="wiki-title">
|
||||
<Button icon="edit" onClick={onEditor} disabled={!editorEable}>
|
||||
编辑
|
||||
</Button>
|
||||
{username && (
|
||||
<div className="wiki-user">
|
||||
由{' '}
|
||||
<Link className="user-name" to={`/user/profile/${uid || 11}`}>
|
||||
{username}
|
||||
</Link>{' '}
|
||||
修改于 {editorTime}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="tui-editor-contents"
|
||||
dangerouslySetInnerHTML={{ __html: desc }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
WikiView.propTypes = {
|
||||
editorEable: PropTypes.bool,
|
||||
onEditor: PropTypes.func,
|
||||
uid: PropTypes.number,
|
||||
username: PropTypes.string,
|
||||
editorTime: PropTypes.string,
|
||||
desc: PropTypes.string
|
||||
};
|
||||
|
||||
export default WikiView;
|
255
exts/yapi-plugin-wiki/wikiPage/index.js
Normal file
255
exts/yapi-plugin-wiki/wikiPage/index.js
Normal file
@ -0,0 +1,255 @@
|
||||
import React, { Component } from 'react';
|
||||
import { message } from 'antd';
|
||||
import { connect } from 'react-redux';
|
||||
import axios from 'axios';
|
||||
import PropTypes from 'prop-types';
|
||||
import './index.scss';
|
||||
import { timeago } from '../util.js';
|
||||
import { Link } from 'react-router-dom';
|
||||
import WikiView from './View.js';
|
||||
import WikiEditor from './Editor.js';
|
||||
|
||||
@connect(
|
||||
state => {
|
||||
return {
|
||||
projectMsg: state.project.currProject
|
||||
};
|
||||
},
|
||||
{}
|
||||
)
|
||||
class WikiPage extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isEditor: false,
|
||||
isUpload: true,
|
||||
desc: '',
|
||||
markdown: '',
|
||||
notice: props.projectMsg.switch_notice,
|
||||
status: 'INIT',
|
||||
editUid: '',
|
||||
editName: '',
|
||||
curdata: null
|
||||
};
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
match: PropTypes.object,
|
||||
projectMsg: PropTypes.object
|
||||
};
|
||||
|
||||
async componentDidMount() {
|
||||
const currProjectId = this.props.match.params.id;
|
||||
await this.handleData({ project_id: currProjectId });
|
||||
this.handleConflict();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
// willUnmount
|
||||
try {
|
||||
if (this.state.status === 'CLOSE') {
|
||||
this.WebSocket.send('end');
|
||||
this.WebSocket.close();
|
||||
}
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
// 结束编辑websocket
|
||||
endWebSocket = () => {
|
||||
try {
|
||||
if (this.state.status === 'CLOSE') {
|
||||
const sendEnd = () => {
|
||||
this.WebSocket.send('end');
|
||||
};
|
||||
this.handleWebsocketAccidentClose(sendEnd);
|
||||
}
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// 处理多人编辑冲突问题
|
||||
handleConflict = () => {
|
||||
// console.log(location)
|
||||
let domain = location.hostname + (location.port !== '' ? ':' + location.port : '');
|
||||
let s;
|
||||
//因后端 node 仅支持 ws, 暂不支持 wss
|
||||
let wsProtocol = location.protocol === 'https' ? 'ws' : 'ws';
|
||||
s = new WebSocket(
|
||||
wsProtocol +
|
||||
'://' +
|
||||
domain +
|
||||
'/api/ws_plugin/wiki_desc/solve_conflict?id=' +
|
||||
this.props.match.params.id
|
||||
);
|
||||
s.onopen = () => {
|
||||
this.WebSocket = s;
|
||||
s.send('start');
|
||||
};
|
||||
|
||||
s.onmessage = e => {
|
||||
let result = JSON.parse(e.data);
|
||||
if (result.errno === 0) {
|
||||
// 更新
|
||||
if (result.data) {
|
||||
this.setState({
|
||||
// curdata: result.data,
|
||||
desc: result.data.desc,
|
||||
username: result.data.username,
|
||||
uid: result.data.uid,
|
||||
editorTime: timeago(result.data.up_time)
|
||||
});
|
||||
}
|
||||
// 新建
|
||||
this.setState({
|
||||
isEditor: !this.state.isEditor,
|
||||
status: 'CLOSE'
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
editUid: result.data.uid,
|
||||
editName: result.data.username,
|
||||
status: 'EDITOR'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
s.onerror = () => {
|
||||
this.setState({
|
||||
status: 'CLOSE'
|
||||
});
|
||||
console.warn('websocket 连接失败,将导致多人编辑同一个接口冲突。');
|
||||
};
|
||||
};
|
||||
|
||||
// 点击编辑按钮 发送 websocket 获取数据
|
||||
onEditor = () => {
|
||||
// this.WebSocket.send('editor');
|
||||
const sendEditor = () => {
|
||||
this.WebSocket.send('editor');
|
||||
};
|
||||
this.handleWebsocketAccidentClose(sendEditor, status => {
|
||||
// 如果websocket 启动不成功用户依旧可以对wiki 进行编辑
|
||||
if (!status) {
|
||||
this.setState({
|
||||
isEditor: !this.state.isEditor
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 处理websocket 意外断开问题
|
||||
handleWebsocketAccidentClose = (fn, callback) => {
|
||||
// websocket 是否启动
|
||||
if (this.WebSocket) {
|
||||
// websocket 断开
|
||||
if (this.WebSocket.readyState !== 1) {
|
||||
message.error('websocket 链接失败,请重新刷新页面');
|
||||
} else {
|
||||
fn();
|
||||
}
|
||||
callback(true);
|
||||
} else {
|
||||
callback(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取数据
|
||||
handleData = async params => {
|
||||
let result = await axios.get('/api/plugin/wiki_desc/get', { params });
|
||||
if (result.data.errcode === 0) {
|
||||
const data = result.data.data;
|
||||
if (data) {
|
||||
this.setState({
|
||||
desc: data.desc,
|
||||
markdown: data.markdown,
|
||||
username: data.username,
|
||||
uid: data.uid,
|
||||
editorTime: timeago(data.up_time)
|
||||
});
|
||||
}
|
||||
} else {
|
||||
message.error(`请求数据失败: ${result.data.errmsg}`);
|
||||
}
|
||||
};
|
||||
|
||||
// 数据上传
|
||||
onUpload = async (desc, markdown) => {
|
||||
const currProjectId = this.props.match.params.id;
|
||||
let option = {
|
||||
project_id: currProjectId,
|
||||
desc,
|
||||
markdown,
|
||||
email_notice: this.state.notice
|
||||
};
|
||||
let result = await axios.post('/api/plugin/wiki_desc/up', option);
|
||||
if (result.data.errcode === 0) {
|
||||
await this.handleData({ project_id: currProjectId });
|
||||
this.setState({ isEditor: false });
|
||||
} else {
|
||||
message.error(`更新失败: ${result.data.errmsg}`);
|
||||
}
|
||||
this.endWebSocket();
|
||||
// this.WebSocket.send('end');
|
||||
};
|
||||
// 取消编辑
|
||||
onCancel = () => {
|
||||
this.setState({ isEditor: false });
|
||||
this.endWebSocket();
|
||||
};
|
||||
|
||||
// 邮件通知
|
||||
onEmailNotice = e => {
|
||||
this.setState({
|
||||
notice: e.target.checked
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { isEditor, username, editorTime, notice, uid, status, editUid, editName } = this.state;
|
||||
const editorEable =
|
||||
this.props.projectMsg.role === 'admin' ||
|
||||
this.props.projectMsg.role === 'owner' ||
|
||||
this.props.projectMsg.role === 'dev';
|
||||
const isConflict = status === 'EDITOR';
|
||||
|
||||
return (
|
||||
<div className="g-row">
|
||||
<div className="m-panel wiki-content">
|
||||
<div className="wiki-content">
|
||||
{isConflict && (
|
||||
<div className="wiki-conflict">
|
||||
<Link to={`/user/profile/${editUid || uid}`}>
|
||||
<b>{editName || username}</b>
|
||||
</Link>
|
||||
<span>正在编辑该wiki,请稍后再试...</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!isEditor ? (
|
||||
<WikiView
|
||||
editorEable={editorEable}
|
||||
onEditor={this.onEditor}
|
||||
uid={uid}
|
||||
username={username}
|
||||
editorTime={editorTime}
|
||||
desc={this.state.desc}
|
||||
/>
|
||||
) : (
|
||||
<WikiEditor
|
||||
isConflict={isConflict}
|
||||
onUpload={this.onUpload}
|
||||
onCancel={this.onCancel}
|
||||
notice={notice}
|
||||
onEmailNotice={this.onEmailNotice}
|
||||
desc={this.state.desc}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default WikiPage;
|
27
exts/yapi-plugin-wiki/wikiPage/index.scss
Normal file
27
exts/yapi-plugin-wiki/wikiPage/index.scss
Normal file
@ -0,0 +1,27 @@
|
||||
.wiki-content {
|
||||
.wiki-user {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.wiki-editor {
|
||||
padding-top: 16px;
|
||||
}
|
||||
|
||||
.upload-btn {
|
||||
margin-right: 16px;
|
||||
}
|
||||
.wiki-conflict {
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.wiki-up {
|
||||
text-align: right;
|
||||
padding-top: 16px;
|
||||
}
|
||||
|
||||
.wiki-title {
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
}
|
@ -740,7 +740,7 @@ class interfaceController extends baseController {
|
||||
ctx.body = yapi.commons.resReturn(null, 402, err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 处理编辑冲突
|
||||
async solveConflict(ctx) {
|
||||
try {
|
||||
let id = parseInt(ctx.query.id, 10),
|
||||
@ -1066,10 +1066,10 @@ class interfaceController extends baseController {
|
||||
async schema2json(ctx) {
|
||||
let schema = ctx.request.body.schema;
|
||||
let required = ctx.request.body.required;
|
||||
|
||||
|
||||
let res = yapi.commons.schemaToJson(schema, {
|
||||
alwaysFakeOptionals: required ? true : false
|
||||
});
|
||||
alwaysFakeOptionals: _.isUndefined(required) ? true : require
|
||||
})
|
||||
// console.log('res',res)
|
||||
return (ctx.body = res);
|
||||
}
|
||||
|
@ -559,6 +559,10 @@ class interfaceColController extends baseController {
|
||||
return (ctx.body = yapi.commons.resReturn(null, 400, '用例id不能为空'));
|
||||
}
|
||||
|
||||
if (!params.casename) {
|
||||
return (ctx.body = yapi.commons.resReturn(null, 400, '用例名称不能为空'));
|
||||
}
|
||||
|
||||
let caseData = await this.caseModel.get(params.id);
|
||||
let auth = await this.checkAuth(caseData.project_id, 'project', 'edit');
|
||||
if (!auth) {
|
||||
@ -671,7 +675,7 @@ class interfaceColController extends baseController {
|
||||
let result = await this.colModel.up(id, params);
|
||||
let username = this.getUsername();
|
||||
yapi.commons.saveLog({
|
||||
content: `<a href="/user/profile/${this.getUid()}">${username}</a> 更新了接口集 <a href="/project/${
|
||||
content: `<a href="/user/profile/${this.getUid()}">${username}</a> 更新了测试集合 <a href="/project/${
|
||||
colData.project_id
|
||||
}/interface/col/${id}">${colData.name}</a> 的信息`,
|
||||
type: 'project',
|
||||
|
@ -44,7 +44,7 @@ class logController extends baseController {
|
||||
page = ctx.request.query.page || 1,
|
||||
limit = ctx.request.query.limit || 10,
|
||||
type = ctx.request.query.type,
|
||||
interfaceId = ctx.request.query.interface_id;
|
||||
selectValue = ctx.request.query.selectValue;
|
||||
if (!typeid) {
|
||||
return (ctx.body = yapi.commons.resReturn(null, 400, 'typeid不能为空'));
|
||||
}
|
||||
@ -80,9 +80,9 @@ class logController extends baseController {
|
||||
list: projectLogList,
|
||||
total: Math.ceil(total / limit)
|
||||
});
|
||||
} else if (type === 'project') {
|
||||
let result = await this.Model.listWithPaging(typeid, type, page, limit, interfaceId);
|
||||
let count = await this.Model.listCount(typeid, type, interfaceId);
|
||||
} else if (type === "project") {
|
||||
let result = await this.Model.listWithPaging(typeid, type, page, limit, selectValue);
|
||||
let count = await this.Model.listCount(typeid, type, selectValue);
|
||||
|
||||
ctx.body = yapi.commons.resReturn({
|
||||
total: Math.ceil(count / limit),
|
||||
|
@ -936,9 +936,10 @@ class projectController extends baseController {
|
||||
return (ctx.body = yapi.commons.resReturn(null, 405, '项目id不能为空'));
|
||||
}
|
||||
|
||||
if ((await this.checkAuth(project_id, 'project', 'edit')) !== true) {
|
||||
return (ctx.body = yapi.commons.resReturn(null, 405, '没有权限'));
|
||||
}
|
||||
// 去掉权限判断
|
||||
// if ((await this.checkAuth(project_id, 'project', 'edit')) !== true) {
|
||||
// return (ctx.body = yapi.commons.resReturn(null, 405, '没有权限'));
|
||||
// }
|
||||
|
||||
let env = await this.Model.getByEnv(project_id);
|
||||
// console.log('project', projectData)
|
||||
|
@ -18,6 +18,11 @@ class interfaceColController extends baseController {
|
||||
async testGet(ctx) {
|
||||
try {
|
||||
let query = ctx.query;
|
||||
// cookie 检测
|
||||
ctx.cookies.set('_uid', 12, {
|
||||
expires: yapi.commons.expireDate(7),
|
||||
httpOnly: true
|
||||
});
|
||||
ctx.body = yapi.commons.resReturn(query);
|
||||
} catch (e) {
|
||||
ctx.body = yapi.commons.resReturn(null, 402, e.message);
|
||||
|
@ -218,7 +218,9 @@ module.exports = async (ctx, next) => {
|
||||
if (interfaceData.res_body_is_json_schema === true) {
|
||||
//json-schema
|
||||
const schema = yapi.commons.json_parse(interfaceData.res_body);
|
||||
res = yapi.commons.schemaToJson(schema);
|
||||
res = yapi.commons.schemaToJson(schema, {
|
||||
alwaysFakeOptionals: true
|
||||
});
|
||||
} else {
|
||||
// console.log('header', ctx.request.header['content-type'].indexOf('multipart/form-data'))
|
||||
// 处理 format-data
|
||||
|
@ -4,118 +4,152 @@ var mongoose = require('mongoose');
|
||||
var Schema = mongoose.Schema;
|
||||
|
||||
class logModel extends baseModel {
|
||||
getName() {
|
||||
return 'log';
|
||||
getName() {
|
||||
return 'log';
|
||||
}
|
||||
|
||||
getSchema() {
|
||||
return {
|
||||
uid: { type: Number, required: true },
|
||||
typeid: { type: Number, required: true },
|
||||
type: {
|
||||
type: String,
|
||||
enum: ['user', 'group', 'interface', 'project', 'other', 'interface_col'],
|
||||
required: true
|
||||
},
|
||||
content: { type: String, required: true },
|
||||
username: { type: String, required: true },
|
||||
add_time: Number,
|
||||
data: Schema.Types.Mixed //用于原始数据存储
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String} content log内容
|
||||
* @param {Enum} type log类型, ['user', 'group', 'interface', 'project', 'other']
|
||||
* @param {Number} uid 用户id
|
||||
* @param {String} username 用户名
|
||||
* @param {Number} typeid 类型id
|
||||
* @param {Number} add_time 时间
|
||||
*/
|
||||
save(data) {
|
||||
let saveData = {
|
||||
content: data.content,
|
||||
type: data.type,
|
||||
uid: data.uid,
|
||||
username: data.username,
|
||||
typeid: data.typeid,
|
||||
add_time: yapi.commons.time(),
|
||||
data: data.data
|
||||
};
|
||||
|
||||
let log = new this.model(saveData);
|
||||
|
||||
return log.save();
|
||||
}
|
||||
|
||||
del(id) {
|
||||
return this.model.remove({
|
||||
_id: id
|
||||
});
|
||||
}
|
||||
|
||||
list(typeid, type) {
|
||||
return this.model
|
||||
.find({
|
||||
typeid: typeid,
|
||||
type: type
|
||||
})
|
||||
.exec();
|
||||
}
|
||||
|
||||
listWithPaging(typeid, type, page, limit, selectValue) {
|
||||
page = parseInt(page);
|
||||
limit = parseInt(limit);
|
||||
const params = {
|
||||
type: type,
|
||||
typeid: typeid
|
||||
};
|
||||
|
||||
if (selectValue === 'wiki') {
|
||||
params['data.type'] = selectValue;
|
||||
}
|
||||
|
||||
getSchema() {
|
||||
return {
|
||||
uid: { type: Number, required: true },
|
||||
typeid: { type: Number, required: true },
|
||||
type: { type: String,enum:['user', 'group', 'interface','project', 'other', 'interface_col'], required: true },
|
||||
content: { type: String, required: true },
|
||||
username: { type: String, required: true },
|
||||
add_time: Number,
|
||||
data: Schema.Types.Mixed //用于原始数据存储
|
||||
};
|
||||
if (selectValue && !isNaN(selectValue)) {
|
||||
params['data.interface_id'] = +selectValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String} content log内容
|
||||
* @param {Enum} type log类型, ['user', 'group', 'interface', 'project', 'other']
|
||||
* @param {Number} uid 用户id
|
||||
* @param {String} username 用户名
|
||||
* @param {Number} typeid 类型id
|
||||
* @param {Number} add_time 时间
|
||||
*/
|
||||
save(data) {
|
||||
let saveData = {
|
||||
content: data.content,
|
||||
type: data.type,
|
||||
uid: data.uid,
|
||||
username: data.username,
|
||||
typeid: data.typeid,
|
||||
add_time: yapi.commons.time(),
|
||||
data: data.data
|
||||
};
|
||||
|
||||
let log = new this.model(saveData);
|
||||
|
||||
return log.save();
|
||||
}
|
||||
|
||||
del(id) {
|
||||
return this.model.remove({
|
||||
_id: id
|
||||
});
|
||||
}
|
||||
|
||||
list(typeid,type) {
|
||||
return this.model.find({
|
||||
typeid: typeid,
|
||||
type: type
|
||||
})
|
||||
.exec();
|
||||
}
|
||||
|
||||
|
||||
listWithPaging(typeid,type, page, limit, interfaceId) {
|
||||
page = parseInt(page);
|
||||
limit = parseInt(limit);
|
||||
const params = {
|
||||
type: type,
|
||||
typeid: typeid
|
||||
}
|
||||
if(interfaceId && !isNaN(interfaceId)){
|
||||
params['data.interface_id'] = +interfaceId
|
||||
}
|
||||
return this.model.find(params).sort({add_time:-1}).skip((page - 1) * limit).limit(limit).exec();
|
||||
}
|
||||
listWithPagingByGroup(typeid, pidList, page, limit) {
|
||||
page = parseInt(page);
|
||||
limit = parseInt(limit);
|
||||
return this.model.find({
|
||||
"$or":[{
|
||||
type: "project",
|
||||
typeid: {"$in": pidList}
|
||||
},{
|
||||
type: "group",
|
||||
typeid: typeid
|
||||
}]
|
||||
}).sort({add_time:-1}).skip((page - 1) * limit).limit(limit).exec();
|
||||
}
|
||||
listCountByGroup(typeid,pidList) {
|
||||
return this.model.count({
|
||||
"$or":[{
|
||||
type: "project",
|
||||
typeid: {"$in": pidList}
|
||||
},{
|
||||
type: "group",
|
||||
typeid: typeid
|
||||
}]
|
||||
});
|
||||
}
|
||||
listCount(typeid,type, interfaceId) {
|
||||
const params = {
|
||||
type: type,
|
||||
return this.model
|
||||
.find(params)
|
||||
.sort({ add_time: -1 })
|
||||
.skip((page - 1) * limit)
|
||||
.limit(limit)
|
||||
.exec();
|
||||
}
|
||||
listWithPagingByGroup(typeid, pidList, page, limit) {
|
||||
page = parseInt(page);
|
||||
limit = parseInt(limit);
|
||||
return this.model
|
||||
.find({
|
||||
$or: [
|
||||
{
|
||||
type: 'project',
|
||||
typeid: { $in: pidList }
|
||||
},
|
||||
{
|
||||
type: 'group',
|
||||
typeid: typeid
|
||||
}
|
||||
]
|
||||
})
|
||||
.sort({ add_time: -1 })
|
||||
.skip((page - 1) * limit)
|
||||
.limit(limit)
|
||||
.exec();
|
||||
}
|
||||
listCountByGroup(typeid, pidList) {
|
||||
return this.model.count({
|
||||
$or: [
|
||||
{
|
||||
type: 'project',
|
||||
typeid: { $in: pidList }
|
||||
},
|
||||
{
|
||||
type: 'group',
|
||||
typeid: typeid
|
||||
}
|
||||
if(interfaceId && !isNaN(interfaceId)){
|
||||
params['data.interface_id'] = +interfaceId
|
||||
}
|
||||
return this.model.count(params);
|
||||
]
|
||||
});
|
||||
}
|
||||
listCount(typeid, type, selectValue) {
|
||||
const params = {
|
||||
type: type,
|
||||
typeid: typeid
|
||||
};
|
||||
|
||||
if (selectValue === 'wiki') {
|
||||
params['data.type'] = selectValue;
|
||||
}
|
||||
|
||||
listWithCatid(typeid,type, interfaceId) {
|
||||
const params = {
|
||||
type: type,
|
||||
typeid: typeid
|
||||
if (selectValue && !isNaN(selectValue)) {
|
||||
params['data.interface_id'] = +selectValue;
|
||||
}
|
||||
if(interfaceId && !isNaN(interfaceId)){
|
||||
params['data.interface_id'] = +interfaceId
|
||||
}
|
||||
return this.model.find(params).sort({add_time:-1}).limit(1).select('uid content type username typeid add_time').exec();
|
||||
return this.model.count(params);
|
||||
}
|
||||
|
||||
listWithCatid(typeid, type, interfaceId) {
|
||||
const params = {
|
||||
type: type,
|
||||
typeid: typeid
|
||||
};
|
||||
if (interfaceId && !isNaN(interfaceId)) {
|
||||
params['data.interface_id'] = +interfaceId;
|
||||
}
|
||||
return this.model
|
||||
.find(params)
|
||||
.sort({ add_time: -1 })
|
||||
.limit(1)
|
||||
.select('uid content type username typeid add_time')
|
||||
.exec();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = logModel;
|
||||
module.exports = logModel;
|
||||
|
@ -35,6 +35,7 @@ formats.forEach(item => {
|
||||
|
||||
exports.schemaToJson = function(schema, options = {}) {
|
||||
Object.assign(options, defaultOptions);
|
||||
|
||||
jsf.option(options);
|
||||
let result;
|
||||
try {
|
||||
@ -274,6 +275,7 @@ exports.sandbox = (sandbox, script) => {
|
||||
script.runInContext(context, {
|
||||
timeout: 3000
|
||||
});
|
||||
|
||||
return sandbox;
|
||||
};
|
||||
|
||||
@ -504,6 +506,7 @@ function convertString(variable) {
|
||||
|
||||
exports.runCaseScript = async function runCaseScript(params) {
|
||||
let script = params.script;
|
||||
// script 是断言
|
||||
if (!script) {
|
||||
return yapi.commons.resReturn('ok');
|
||||
}
|
||||
@ -523,11 +526,13 @@ exports.runCaseScript = async function runCaseScript(params) {
|
||||
let result = {};
|
||||
try {
|
||||
result = yapi.commons.sandbox(context, script);
|
||||
|
||||
result.logs = logs;
|
||||
return yapi.commons.resReturn(result);
|
||||
} catch (err) {
|
||||
logs.push(convertString(err));
|
||||
result.logs = logs;
|
||||
|
||||
return yapi.commons.resReturn(result, 400, err.name + ': ' + err.message);
|
||||
}
|
||||
};
|
||||
|
@ -20,11 +20,14 @@ function addPluginRouter(config) {
|
||||
pluginsRouterPath.push(routerPath);
|
||||
createAction(router, "/api", config.controller, config.action, routerPath, method, true);
|
||||
}
|
||||
|
||||
|
||||
function websocket(app) {
|
||||
createAction(router, "/api", interfaceController, "solveConflict", "/interface/solve_conflict", "get")
|
||||
|
||||
yapi.emitHookSync('add_ws_router', addPluginRouter);
|
||||
|
||||
|
||||
app.ws.use(router.routes())
|
||||
app.ws.use(router.allowedMethods());
|
||||
app.ws.use(function (ctx, next) {
|
||||
|
@ -7,7 +7,15 @@
|
||||
<div class="icon"></div>
|
||||
<input type="text" class="input js-input" placeholder="搜索" />
|
||||
<div class="m-search-result js-search-result"></div>
|
||||
</div></div><nav class="m-header-nav js-nav"><ul class="m-header-items"><li class="item active"><a class="href" href="index.html">教程</a></li><li class="item "><a class="href" href="../devops/index.html">内网部署</a></li></ul></nav><div id="js-nav-btn" class="m-header-btn ui-font-ydoc"></div></header><div class="m-content" id="js-content"><div id="markdown-body" class="m-content-container markdown-body"><h3 id="v1.3.18">v1.3.18</h3>
|
||||
</div></div><nav class="m-header-nav js-nav"><ul class="m-header-items"><li class="item active"><a class="href" href="index.html">教程</a></li><li class="item "><a class="href" href="../devops/index.html">内网部署</a></li></ul></nav><div id="js-nav-btn" class="m-header-btn ui-font-ydoc"></div></header><div class="m-content" id="js-content"><div id="markdown-body" class="m-content-container markdown-body"><h3 id="v1.3.19">v1.3.19</h3>
|
||||
<ul>
|
||||
<li>增加项目文档记录wiki</li>
|
||||
</ul>
|
||||
<h4>Bug Fixed</h4>
|
||||
<ul>
|
||||
<li>修复测试用例名称为空时保存测试用例出现的bug</li>
|
||||
</ul>
|
||||
<h3 id="v1.3.18">v1.3.18</h3>
|
||||
<ul>
|
||||
<li>增加全局接口搜索功能</li>
|
||||
<li>邮件通知过滤功能</li>
|
||||
|
@ -11,7 +11,7 @@
|
||||
<p>传统的接口自动化测试成本高,大量的项目没有使用自动化测试保证接口的质量,仅仅依靠手动测试,是非常不可靠和容易出错的。</p>
|
||||
<p>YApi 为了解决这个问题,开发了可视化接口自动化测试功能,只需要配置每个接口的入参和对 RESPONSE 断言,即可实现对接口的自动化测试,大大提升了接口测试的效率。</p>
|
||||
<h2 id="第一步,测试集合">第一步,测试集合</h2>
|
||||
<p>使用 YApi 自动化测试,第一步需要做得是创建测试集合和导入接口,点击添加集合创建,创建完成后导入接口。</p>
|
||||
<p>使用 YApi 自动化测试,第一步需要做得是创建测试集合和导入接口,点击添加集合创建,创建完成后导入接口(同一个接口可以多次导入)。</p>
|
||||
<p><img src="case-col.png" alt=""></p>
|
||||
<p><img src="import-case.png" alt=""></p>
|
||||
<h2 id="第二步,编辑测试用例">第二步,编辑测试用例</h2>
|
||||
|
@ -596,7 +596,7 @@ window.ydoc_plugin_search_json = {
|
||||
{
|
||||
"title": "第一步,测试集合",
|
||||
"url": "/documents/case.html#第一步,测试集合",
|
||||
"content": "第一步,测试集合使用 YApi 自动化测试,第一步需要做得是创建测试集合和导入接口,点击添加集合创建,创建完成后导入接口。"
|
||||
"content": "第一步,测试集合使用 YApi 自动化测试,第一步需要做得是创建测试集合和导入接口,点击添加集合创建,创建完成后导入接口(同一个接口可以多次导入)。"
|
||||
},
|
||||
{
|
||||
"title": "第二步,编辑测试用例",
|
||||
@ -653,7 +653,7 @@ window.ydoc_plugin_search_json = {
|
||||
{
|
||||
"title": "第一步,测试集合",
|
||||
"url": "/documents/case.html#第一步,测试集合",
|
||||
"content": "第一步,测试集合使用 YApi 自动化测试,第一步需要做得是创建测试集合和导入接口,点击添加集合创建,创建完成后导入接口。"
|
||||
"content": "第一步,测试集合使用 YApi 自动化测试,第一步需要做得是创建测试集合和导入接口,点击添加集合创建,创建完成后导入接口(同一个接口可以多次导入)。"
|
||||
},
|
||||
{
|
||||
"title": "第二步,编辑测试用例",
|
||||
@ -1149,6 +1149,11 @@ window.ydoc_plugin_search_json = {
|
||||
"content": "",
|
||||
"url": "/documents/CHANGELOG.html",
|
||||
"children": [
|
||||
{
|
||||
"title": "v1.3.19",
|
||||
"url": "/documents/CHANGELOG.html#v1.3.19",
|
||||
"content": "v1.3.19增加项目文档记录wiki\nBug Fixed修复测试用例名称为空时保存测试用例出现的bug\n"
|
||||
},
|
||||
{
|
||||
"title": "v1.3.18",
|
||||
"url": "/documents/CHANGELOG.html#v1.3.18",
|
||||
@ -1306,6 +1311,11 @@ window.ydoc_plugin_search_json = {
|
||||
"content": "",
|
||||
"url": "/documents/CHANGELOG.html",
|
||||
"children": [
|
||||
{
|
||||
"title": "v1.3.19",
|
||||
"url": "/documents/CHANGELOG.html#v1.3.19",
|
||||
"content": "v1.3.19增加项目文档记录wiki\nBug Fixed修复测试用例名称为空时保存测试用例出现的bug\n"
|
||||
},
|
||||
{
|
||||
"title": "v1.3.18",
|
||||
"url": "/documents/CHANGELOG.html#v1.3.18",
|
||||
|
Loading…
Reference in New Issue
Block a user