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

View File

@@ -0,0 +1,156 @@
import React, { Component } from 'react';
// import { connect } from 'react-redux'
import axios from 'axios';
import PropTypes from 'prop-types';
import { withRouter } from 'react-router-dom';
import { Form, Switch, Button, message, Icon, Tooltip, Radio } from 'antd';
import MockCol from './MockCol/MockCol.js';
import mockEditor from 'client/components/AceEditor/mockEditor';
import constants from '../../client/constants/variable.js';
const FormItem = Form.Item;
class AdvMock extends Component {
static propTypes = {
form: PropTypes.object,
match: PropTypes.object
};
constructor(props) {
super(props);
this.state = {
enable: false,
mock_script: '',
tab: 'case'
};
}
handleSubmit = e => {
e.preventDefault();
let projectId = this.props.match.params.id;
let interfaceId = this.props.match.params.actionId;
let params = {
project_id: projectId,
interface_id: interfaceId,
mock_script: this.state.mock_script,
enable: this.state.enable
};
axios.post('/api/plugin/advmock/save', params).then(res => {
if (res.data.errcode === 0) {
message.success('保存成功');
} else {
message.error(res.data.errmsg);
}
});
};
UNSAFE_componentWillMount() {
this.getAdvMockData();
}
async getAdvMockData() {
let interfaceId = this.props.match.params.actionId;
let result = await axios.get('/api/plugin/advmock/get?interface_id=' + interfaceId);
if (result.data.errcode === 0) {
let mockData = result.data.data;
this.setState({
enable: mockData.enable,
mock_script: mockData.mock_script
});
}
let that = this;
mockEditor({
container: 'mock-script',
data: that.state.mock_script,
onChange: function(d) {
that.setState({
mock_script: d.text
});
}
});
}
onChange = v => {
this.setState({
enable: v
});
};
handleTapChange = e => {
this.setState({
tab: e.target.value
});
};
render() {
const formItemLayout = {
labelCol: {
sm: { span: 4 }
},
wrapperCol: {
sm: { span: 16 }
}
};
const tailFormItemLayout = {
wrapperCol: {
sm: {
span: 16,
offset: 11
}
}
};
const { tab } = this.state;
const isShowCase = tab === 'case';
return (
<div style={{ padding: '20px 10px' }}>
<div style={{ textAlign: 'center', marginBottom: 20 }}>
<Radio.Group value={tab} size="large" onChange={this.handleTapChange}>
<Radio.Button value="case">期望</Radio.Button>
<Radio.Button value="script">脚本</Radio.Button>
</Radio.Group>
</div>
<div style={{ display: isShowCase ? 'none' : '' }}>
<Form onSubmit={this.handleSubmit}>
<FormItem
label={
<span>
是否开启&nbsp;<a
target="_blank"
rel="noopener noreferrer"
href={constants.docHref.adv_mock_script}
>
<Tooltip title="点击查看文档">
<Icon type="question-circle-o" />
</Tooltip>
</a>
</span>
}
{...formItemLayout}
>
<Switch
checked={this.state.enable}
onChange={this.onChange}
checkedChildren="开"
unCheckedChildren="关"
/>
</FormItem>
<FormItem label="Mock脚本" {...formItemLayout}>
<div id="mock-script" style={{ minHeight: '500px' }} />
</FormItem>
<FormItem {...tailFormItemLayout}>
<Button type="primary" htmlType="submit">
保存
</Button>
</FormItem>
</Form>
</div>
<div style={{ display: isShowCase ? '' : 'none' }}>
<MockCol />
</div>
</div>
);
}
}
module.exports = Form.create()(withRouter(AdvMock));

View File

@@ -0,0 +1,482 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import {
Form,
Select,
InputNumber,
Switch,
Col,
message,
Row,
Input,
Button,
Icon,
AutoComplete,
Modal
} from 'antd';
const Option = Select.Option;
const FormItem = Form.Item;
import { safeAssign } from 'client/common.js';
import AceEditor from 'client/components/AceEditor/AceEditor';
import constants from 'client/constants/variable.js';
import { httpCodes } from '../index.js';
import './CaseDesModal.scss';
import { connect } from 'react-redux';
import json5 from 'json5';
const formItemLayout = {
labelCol: { span: 5 },
wrapperCol: { span: 12 }
};
const formItemLayoutWithOutLabel = {
wrapperCol: { span: 12, offset: 5 }
};
@connect(state => {
return {
currInterface: state.inter.curdata
};
})
class CaseDesForm extends Component {
static propTypes = {
form: PropTypes.object,
caseData: PropTypes.object,
currInterface: PropTypes.object,
onOk: PropTypes.func,
onCancel: PropTypes.func,
isAdd: PropTypes.bool,
visible: PropTypes.bool
};
// 初始化输入数据
preProcess = caseData => {
try {
caseData = JSON.parse(JSON.stringify(caseData));
} catch (error) {
console.log(error);
}
const initCaseData = {
ip: '',
ip_enable: false,
name: '',
code: '200',
delay: 0,
headers: [{ name: '', value: '' }],
paramsArr: [{ name: '', value: '' }],
params: {},
res_body: '',
paramsForm: 'form'
};
caseData.params = caseData.params || {};
const paramsArr = Object.keys(caseData.params).length
? Object.keys(caseData.params)
.map(key => {
return { name: key, value: caseData.params[key] };
})
.filter(item => {
if (typeof item.value === 'object') {
// this.setState({ paramsForm: 'json' })
caseData.paramsForm = 'json';
}
return typeof item.value !== 'object';
})
: [{ name: '', value: '' }];
const headers =
caseData.headers && caseData.headers.length ? caseData.headers : [{ name: '', value: '' }];
caseData.code = '' + caseData.code;
caseData.params = JSON.stringify(caseData.params, null, 2);
caseData = safeAssign(initCaseData, { ...caseData, headers, paramsArr });
return caseData;
};
constructor(props) {
super(props);
const { caseData } = this.props;
this.state = this.preProcess(caseData);
}
// 处理request_body编译器
handleRequestBody = d => {
this.setState({ res_body: d.text });
};
// 处理参数编译器
handleParams = d => {
this.setState({ params: d.text });
};
// 增加参数信息
addValues = key => {
const { getFieldValue } = this.props.form;
let values = getFieldValue(key);
values = values.concat({ name: '', value: '' });
this.setState({ [key]: values });
};
// 删除参数信息
removeValues = (key, index) => {
const { setFieldsValue, getFieldValue } = this.props.form;
let values = getFieldValue(key);
values = values.filter((val, index2) => index !== index2);
setFieldsValue({ [key]: values });
this.setState({ [key]: values });
};
// 处理参数
getParamsKey = () => {
let {
req_query,
req_body_form,
req_body_type,
method,
req_body_other,
req_body_is_json_schema,
req_params
} = this.props.currInterface;
let keys = [];
req_query &&
Array.isArray(req_query) &&
req_query.forEach(item => {
keys.push(item.name);
});
req_params &&
Array.isArray(req_params) &&
req_params.forEach(item => {
keys.push(item.name);
});
if (constants.HTTP_METHOD[method.toUpperCase()].request_body && req_body_type === 'form') {
req_body_form &&
Array.isArray(req_body_form) &&
req_body_form.forEach(item => {
keys.push(item.name);
});
} else if (
constants.HTTP_METHOD[method.toUpperCase()].request_body &&
req_body_type === 'json' &&
req_body_other
) {
let bodyObj;
try {
// 针对json-schema的处理
if (req_body_is_json_schema) {
bodyObj = json5.parse(this.props.caseData.req_body_other);
} else {
bodyObj = json5.parse(req_body_other);
}
keys = keys.concat(Object.keys(bodyObj));
} catch (error) {
console.log(error);
}
}
return keys;
};
endProcess = caseData => {
const headers = [];
const params = {};
const { paramsForm } = this.state;
caseData.headers &&
Array.isArray(caseData.headers) &&
caseData.headers.forEach(item => {
if (item.name) {
headers.push({
name: item.name,
value: item.value
});
}
});
caseData.paramsArr &&
Array.isArray(caseData.paramsArr) &&
caseData.paramsArr.forEach(item => {
if (item.name) {
params[item.name] = item.value;
}
});
caseData.headers = headers;
if (paramsForm === 'form') {
caseData.params = params;
} else {
try {
caseData.params = json5.parse(caseData.params);
} catch (error) {
console.log(error);
message.error('请求参数 json 格式有误,请修改');
return false;
}
}
delete caseData.paramsArr;
return caseData;
};
handleOk = () => {
const form = this.props.form;
form.validateFieldsAndScroll((err, values) => {
if (!err) {
values.res_body = this.state.res_body;
values.params = this.state.params;
this.props.onOk(this.endProcess(values));
}
});
};
render() {
const { getFieldDecorator, getFieldValue } = this.props.form;
const { isAdd, visible, onCancel } = this.props;
const {
name,
code,
headers,
ip,
ip_enable,
params,
paramsArr,
paramsForm,
res_body,
delay
} = this.state;
this.props.form.initialValue;
const valuesTpl = (values, title) => {
const dataSource = this.getParamsKey();
const display = paramsForm === 'json' ? 'none' : '';
return values.map((item, index) => (
<div key={index} className="paramsArr" style={{ display }}>
<FormItem
{...(index === 0 ? formItemLayout : formItemLayoutWithOutLabel)}
wrapperCol={index === 0 ? { span: 19 } : { span: 19, offset: 5 }}
label={index ? '' : title}
>
<Row gutter={8}>
<Col span={10}>
<FormItem>
{getFieldDecorator(`paramsArr[${index}].name`, { initialValue: item.name })(
<AutoComplete
dataSource={dataSource}
placeholder="参数名称"
filterOption={(inputValue, option) =>
option.props.children.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1
}
/>
)}
</FormItem>
</Col>
<Col span={10}>
<FormItem>
{getFieldDecorator(`paramsArr[${index}].value`, { initialValue: item.value })(
<Input placeholder="参数值" />
)}
</FormItem>
</Col>
<Col span={4}>
{values.length > 1 ? (
<Icon
className="dynamic-delete-button"
type="minus-circle-o"
onClick={() => this.removeValues('paramsArr', index)}
/>
) : null}
</Col>
</Row>
</FormItem>
</div>
));
};
const headersTpl = (values, title) => {
const dataSource = constants.HTTP_REQUEST_HEADER;
return values.map((item, index) => (
<div key={index} className="headers">
<FormItem
{...(index === 0 ? formItemLayout : formItemLayoutWithOutLabel)}
wrapperCol={index === 0 ? { span: 19 } : { span: 19, offset: 5 }}
label={index ? '' : title}
>
<Row gutter={8}>
<Col span={10}>
<FormItem>
{getFieldDecorator(`headers[${index}].name`, { initialValue: item.name })(
<AutoComplete
dataSource={dataSource}
placeholder="参数名称"
filterOption={(inputValue, option) =>
option.props.children.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1
}
/>
)}
</FormItem>
</Col>
<Col span={10}>
<FormItem>
{getFieldDecorator(`headers[${index}].value`, { initialValue: item.value })(
<Input placeholder="参数值" />
)}
</FormItem>
</Col>
<Col span={4}>
{values.length > 1 ? (
<Icon
className="dynamic-delete-button"
type="minus-circle-o"
onClick={() => this.removeValues('headers', index)}
/>
) : null}
</Col>
</Row>
</FormItem>
</div>
));
};
return (
<Modal
title={isAdd ? '添加期望' : '编辑期望'}
visible={visible}
maskClosable={false}
onOk={this.handleOk}
width={780}
onCancel={() => onCancel()}
afterClose={() => this.setState({ paramsForm: 'form' })}
className="case-des-modal"
>
<Form onSubmit={this.handleOk}>
<h2 className="sub-title" style={{ marginTop: 0 }}>
基本信息
</h2>
<FormItem {...formItemLayout} label="期望名称">
{getFieldDecorator('name', {
initialValue: name,
rules: [{ required: true, message: '请输入期望名称!' }]
})(<Input placeholder="请输入期望名称" />)}
</FormItem>
<FormItem {...formItemLayout} label="IP 过滤" className="ip-filter">
<Col span={6} className="ip-switch">
<FormItem>
{getFieldDecorator('ip_enable', {
initialValue: ip_enable,
valuePropName: 'checked',
rules: [{ type: 'boolean' }]
})(<Switch />)}
</FormItem>
</Col>
<Col span={18}>
<div style={{ display: getFieldValue('ip_enable') ? '' : 'none' }} className="ip">
<FormItem>
{getFieldDecorator(
'ip',
getFieldValue('ip_enable')
? {
initialValue: ip,
rules: [
{
pattern: constants.IP_REGEXP,
message: '请填写正确的 IP 地址',
required: true
}
]
}
: {}
)(<Input placeholder="请输入过滤的 IP 地址" />)}
</FormItem>
</div>
</Col>
</FormItem>
<Row className="params-form" style={{ marginBottom: 8 }}>
<Col {...{ span: 12, offset: 5 }}>
<Switch
size="small"
checkedChildren="JSON"
unCheckedChildren="JSON"
checked={paramsForm === 'json'}
onChange={bool => {
this.setState({ paramsForm: bool ? 'json' : 'form' });
}}
/>
</Col>
</Row>
{valuesTpl(paramsArr, '参数过滤')}
<FormItem
wrapperCol={{ span: 6, offset: 5 }}
style={{ display: paramsForm === 'form' ? '' : 'none' }}
>
<Button
size="default"
type="primary"
onClick={() => this.addValues('paramsArr')}
style={{ width: '100%' }}
>
<Icon type="plus" /> 添加参数
</Button>
</FormItem>
<FormItem
{...formItemLayout}
wrapperCol={{ span: 17 }}
label="参数过滤"
style={{ display: paramsForm === 'form' ? 'none' : '' }}
>
<AceEditor className="pretty-editor" data={params} onChange={this.handleParams} />
<FormItem>
{getFieldDecorator(
'params',
paramsForm === 'json'
? {
rules: [
{ validator: this.jsonValidator, message: '请输入正确的 JSON 字符串!' }
]
}
: {}
)(<Input style={{ display: 'none' }} />)}
</FormItem>
</FormItem>
<h2 className="sub-title">响应</h2>
<FormItem {...formItemLayout} required label="HTTP Code">
{getFieldDecorator('code', {
initialValue: code
})(
<Select showSearch>
{httpCodes.map(code => (
<Option key={'' + code} value={'' + code}>
{'' + code}
</Option>
))}
</Select>
)}
</FormItem>
<FormItem {...formItemLayout} label="延时">
{getFieldDecorator('delay', {
initialValue: delay,
rules: [{ required: true, message: '请输入延时时间!', type: 'integer' }]
})(<InputNumber placeholder="请输入延时时间" min={0} />)}
<span>ms</span>
</FormItem>
{headersTpl(headers, 'HTTP 头')}
<FormItem wrapperCol={{ span: 6, offset: 5 }}>
<Button
size="default"
type="primary"
onClick={() => this.addValues('headers')}
style={{ width: '100%' }}
>
<Icon type="plus" /> 添加 HTTP
</Button>
</FormItem>
<FormItem {...formItemLayout} wrapperCol={{ span: 17 }} label="Body" required>
<FormItem>
<AceEditor
className="pretty-editor"
data={res_body}
mode={this.props.currInterface.res_body_type === 'json' ? null : 'text'}
onChange={this.handleRequestBody}
/>
</FormItem>
</FormItem>
</Form>
</Modal>
);
}
}
const CaseDesModal = Form.create()(CaseDesForm);
export default CaseDesModal;

View File

@@ -0,0 +1,26 @@
.case-des-modal {
.ant-modal-body {
max-height: 520px;
overflow-y: scroll;
}
.ip-filter .ip>.ant-form-item, .ip-filter .ip-switch>.ant-form-item {
margin-bottom: 0;
}
.headers>.ant-form-item, .paramsArr>.ant-form-item, .params-form>.ant-form-item {
margin-bottom: 0;
}
.sub-title {
clear: both;
font-weight: normal;
margin-top: .48rem;
margin-bottom: .16rem;
border-left: 3px solid #2395f1;
padding-left: 8px;
}
.pretty-editor{
min-height: 300px;
border: 1px solid #d9d9d9;
border-radius: 4px;
}
}

View File

@@ -0,0 +1,269 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import axios from 'axios';
import PropTypes from 'prop-types';
import { withRouter } from 'react-router-dom';
import { Table, Button, message, Popconfirm, Tooltip, Icon } from 'antd';
import { fetchMockCol } from 'client/reducer/modules/mockCol';
import { formatTime } from 'client/common.js';
import constants from 'client/constants/variable.js';
import CaseDesModal from './CaseDesModal';
import { json5_parse } from '../../../client/common';
import _ from 'underscore';
@connect(
state => {
return {
list: state.mockCol.list,
currInterface: state.inter.curdata,
currProject: state.project.currProject
};
},
{
fetchMockCol
}
)
@withRouter
export default class MockCol extends Component {
static propTypes = {
list: PropTypes.array,
currInterface: PropTypes.object,
match: PropTypes.object,
fetchMockCol: PropTypes.func,
currProject: PropTypes.object
};
state = {
caseData: {},
caseDesModalVisible: false,
isAdd: false
};
constructor(props) {
super(props);
}
UNSAFE_componentWillMount() {
const interfaceId = this.props.match.params.actionId;
this.props.fetchMockCol(interfaceId);
}
openModal = (record, isAdd) => {
return async () => {
if (this.props.currInterface.res_body_is_json_schema && isAdd) {
let result = await axios.post('/api/interface/schema2json', {
schema: json5_parse(this.props.currInterface.res_body),
required: true
});
record.res_body = JSON.stringify(result.data);
}
// 参数过滤schema形式
if (this.props.currInterface.req_body_is_json_schema) {
let result = await axios.post('/api/interface/schema2json', {
schema: json5_parse(this.props.currInterface.req_body_other),
required: true
});
record.req_body_other = JSON.stringify(result.data);
}
this.setState({
isAdd: isAdd,
caseDesModalVisible: true,
caseData: record
});
};
};
handleOk = async caseData => {
if (!caseData) {
return null;
}
const { caseData: currcase } = this.state;
const interface_id = this.props.match.params.actionId;
const project_id = this.props.match.params.id;
caseData = Object.assign({
...caseData,
interface_id: interface_id,
project_id: project_id
});
if (!this.state.isAdd) {
caseData.id = currcase._id;
}
await axios.post('/api/plugin/advmock/case/save', caseData).then(async res => {
if (res.data.errcode === 0) {
message.success(this.state.isAdd ? '添加成功' : '保存成功');
await this.props.fetchMockCol(interface_id);
this.setState({ caseDesModalVisible: false });
} else {
message.error(res.data.errmsg);
}
});
};
deleteCase = async id => {
const interface_id = this.props.match.params.actionId;
await axios.post('/api/plugin/advmock/case/del', { id }).then(async res => {
if (res.data.errcode === 0) {
message.success('删除成功');
await this.props.fetchMockCol(interface_id);
} else {
message.error(res.data.errmsg);
}
});
};
// mock case 可以设置开启的关闭
openMockCase = async (id , enable=true)=> {
const interface_id = this.props.match.params.actionId;
await axios.post('/api/plugin/advmock/case/hide', {
id,
enable: !enable
}).then(async res => {
if (res.data.errcode === 0) {
message.success('修改成功');
await this.props.fetchMockCol(interface_id);
} else {
message.error(res.data.errmsg);
}
})
}
render() {
const { list: data, currInterface } = this.props;
const { isAdd, caseData, caseDesModalVisible } = this.state;
const role = this.props.currProject.role;
const isGuest = role === 'guest';
const initCaseData = {
ip: '',
ip_enable: false,
name: currInterface.title,
code: '200',
delay: 0,
headers: [{ name: '', value: '' }],
params: {},
res_body: currInterface.res_body
};
let ipFilters = [];
let ipObj = {};
let userFilters = [];
let userObj = {};
_.isArray(data) &&
data.forEach(item => {
ipObj[item.ip_enable ? item.ip : ''] = '';
userObj[item.username] = '';
});
ipFilters = Object.keys(Object.assign(ipObj)).map(value => {
if (!value) {
value = '无过滤';
}
return { text: value, value };
});
userFilters = Object.keys(Object.assign(userObj)).map(value => {
return { text: value, value };
});
const columns = [
{
title: '期望名称',
dataIndex: 'name',
key: 'name'
},
{
title: 'ip',
dataIndex: 'ip',
key: 'ip',
render: (text, recode) => {
if (!recode.ip_enable) {
text = '';
}
return text;
},
onFilter: (value, record) =>
(record.ip === value && record.ip_enable) || (value === '无过滤' && !record.ip_enable),
filters: ipFilters
},
{
title: '创建人',
dataIndex: 'username',
key: 'username',
onFilter: (value, record) => record.username === value,
filters: userFilters
},
{
title: '编辑时间',
dataIndex: 'up_time',
key: 'up_time',
render: text => formatTime(text)
},
{
title: '操作',
dataIndex: '_id',
key: '_id',
render: (_id, recode) => {
// console.log(recode)
return (
!isGuest && (
<div>
<span style={{ marginRight: 5 }}>
<Button size="small" onClick={this.openModal(recode)}>
编辑
</Button>
</span>
<span style={{ marginRight: 5 }}>
<Popconfirm
title="你确定要删除这条期望?"
onConfirm={() => this.deleteCase(_id)}
okText="确定"
cancelText="取消"
>
<Button size="small" onClick={() => {}}>
删除
</Button>
</Popconfirm>
</span>
<span>
<Button size="small" onClick={() => this.openMockCase(_id, recode.case_enable)}>
{recode.case_enable ? <span>已开启</span> : <span></span>}
</Button>
</span>
</div>
)
);
}
}
];
return (
<div>
<div style={{ marginBottom: 8 }}>
<Button type="primary" onClick={this.openModal(initCaseData, true)} disabled={isGuest}>
添加期望
</Button>
<a
target="_blank"
rel="noopener noreferrer"
href={constants.docHref.adv_mock_case}
style={{ marginLeft: 8 }}
>
<Tooltip title="点击查看文档">
<Icon type="question-circle-o" />
</Tooltip>
</a>
</div>
<Table columns={columns} dataSource={data} pagination={false} rowKey="_id" />
{caseDesModalVisible && (
<CaseDesModal
visible={caseDesModalVisible}
isAdd={isAdd}
caseData={caseData}
onOk={this.handleOk}
onCancel={() => this.setState({ caseDesModalVisible: false })}
ref={this.saveFormRef}
/>
)}
</div>
);
}
}

View File

@@ -0,0 +1,36 @@
import axios from 'axios'
import { message } from 'antd'
// Actions
const FETCH_MOCK_COL = 'yapi/mockCol/FETCH_MOCK_COL';
// Reducer
const initialState = {
list: []
}
export default (state = initialState, action) => {
switch (action.type) {
case FETCH_MOCK_COL:
return {
...state,
list: action.payload.data
}
default:
return state
}
}
// Action Creators
export async function fetchMockCol(interfaceId) {
let result = await axios.get('/api/plugin/advmock/case/list?interface_id=' + interfaceId);
if(result.errcode !==0 ){
message.error(result.errmsg);
}
return {
type: FETCH_MOCK_COL,
payload: result.data
}
}

View File

@@ -0,0 +1,61 @@
const yapi = require('yapi.js');
const baseModel = require('models/base.js');
class advMockModel extends baseModel {
getName() {
return 'adv_mock';
}
getSchema() {
return {
interface_id: { type: Number, required: true },
project_id: {type: Number, required: true},
enable: {type: Boolean, default: false},
mock_script: String,
uid: String,
up_time: Number
};
}
get(interface_id) {
return this.model.findOne({
interface_id: interface_id
});
}
delByInterfaceId(interface_id) {
return this.model.remove({
interface_id: interface_id
});
}
delByProjectId(project_id){
return this.model.remove({
project_id: project_id
})
}
save(data) {
data.up_time = yapi.commons.time();
let m = new this.model(data);
return m.save();
}
up(data) {
data.up_time = yapi.commons.time();
return this.model.update({
interface_id: data.interface_id
}, {
uid: data.uid,
up_time: data.up_time,
mock_script: data.mock_script,
enable: data.enable
}, {
upsert: true
})
}
}
module.exports = advMockModel;

View File

@@ -0,0 +1,76 @@
const yapi = require('yapi.js');
const baseModel = require('models/base.js');
const mongoose = require('mongoose');
class caseModel extends baseModel {
getName() {
return 'adv_mock_case';
}
getSchema() {
return {
interface_id: { type: Number, required: true },
project_id: {type: Number, required: true},
ip: {type: String},
ip_enable: {type: Boolean, default: false},
name: {type: String, required: true},
code: {type: Number, default: 200},
delay: {type: Number, default: 0},
headers: [{
name: {type: String, required: true},
value: {type: String}
}],
params: mongoose.Schema.Types.Mixed,
uid: String,
up_time: Number,
res_body: {type: String, required: true},
case_enable: {type: Boolean, default: true}
};
}
get(data) {
return this.model.findOne(data);
}
list(id){
return this.model.find({
interface_id: id
})
}
delByInterfaceId(interface_id) {
return this.model.remove({
interface_id: interface_id
});
}
delByProjectId(project_id){
return this.model.remove({
project_id: project_id
})
}
save(data) {
data.up_time = yapi.commons.time();
let m = new this.model(data);
return m.save();
}
up(data) {
let id = data.id;
delete data.id;
data.up_time = yapi.commons.time();
return this.model.update({
_id: id
}, data)
}
del(id){
return this.model.remove({
_id: id
})
}
}
module.exports = caseModel;

View File

@@ -0,0 +1,14 @@
import AdvMock from './AdvMock'
import mockCol from './MockCol/mockColReducer.js'
module.exports = function(){
this.bindHook('interface_tab', function(tabs){
tabs.advMock = {
name: '高级Mock',
component: AdvMock
}
})
this.bindHook('add_reducer', function(reducerModules){
reducerModules.mockCol = mockCol;
})
}

View File

@@ -0,0 +1,186 @@
const baseController = require('controllers/base.js');
const advModel = require('./advMockModel.js');
const yapi = require('yapi.js');
const caseModel = require('./caseModel.js');
const userModel = require('models/user.js');
const config = require('./index.js');
class advMockController extends baseController {
constructor(ctx) {
super(ctx);
this.Model = yapi.getInst(advModel);
this.caseModel = yapi.getInst(caseModel);
this.userModel = yapi.getInst(userModel);
}
async getMock(ctx) {
let id = ctx.query.interface_id;
let mockData = await this.Model.get(id);
if (!mockData) {
return (ctx.body = yapi.commons.resReturn(null, 408, 'mock脚本不存在'));
}
return (ctx.body = yapi.commons.resReturn(mockData));
}
async upMock(ctx) {
let params = ctx.request.body;
try {
let auth = await this.checkAuth(params.project_id, 'project', 'edit');
if (!auth) {
return (ctx.body = yapi.commons.resReturn(null, 40033, '没有权限'));
}
if (!params.interface_id) {
return (ctx.body = yapi.commons.resReturn(null, 408, '缺少interface_id'));
}
if (!params.project_id) {
return (ctx.body = yapi.commons.resReturn(null, 408, '缺少project_id'));
}
let data = {
interface_id: params.interface_id,
mock_script: params.mock_script || '',
project_id: params.project_id,
uid: this.getUid(),
enable: params.enable === true ? true : false
};
let result;
let mockData = await this.Model.get(data.interface_id);
if (mockData) {
result = await this.Model.up(data);
} else {
result = await this.Model.save(data);
}
return (ctx.body = yapi.commons.resReturn(result));
} catch (e) {
return (ctx.body = yapi.commons.resReturn(null, 400, e.message));
}
}
async list(ctx) {
try {
let id = ctx.query.interface_id;
if (!id) {
return (ctx.body = yapi.commons.resReturn(null, 400, '缺少 interface_id'));
}
let result = await this.caseModel.list(id);
for (let i = 0, len = result.length; i < len; i++) {
let userinfo = await this.userModel.findById(result[i].uid);
result[i] = result[i].toObject();
// if (userinfo) {
result[i].username = userinfo.username;
// }
}
ctx.body = yapi.commons.resReturn(result);
} catch (err) {
ctx.body = yapi.commons.resReturn(null, 400, err.message);
}
}
async getCase(ctx) {
let id = ctx.query.id;
if (!id) {
return (ctx.body = yapi.commons.resReturn(null, 400, '缺少 id'));
}
let result = await this.caseModel.get({
_id: id
});
ctx.body = yapi.commons.resReturn(result);
}
async saveCase(ctx) {
let params = ctx.request.body;
if (!params.interface_id) {
return (ctx.body = yapi.commons.resReturn(null, 408, '缺少interface_id'));
}
if (!params.project_id) {
return (ctx.body = yapi.commons.resReturn(null, 408, '缺少project_id'));
}
if (!params.res_body) {
return (ctx.body = yapi.commons.resReturn(null, 408, '请输入 Response Body'));
}
let data = {
interface_id: params.interface_id,
project_id: params.project_id,
ip_enable: params.ip_enable,
name: params.name,
params: params.params || [],
uid: this.getUid(),
code: params.code || 200,
delay: params.delay || 0,
headers: params.headers || [],
up_time: yapi.commons.time(),
res_body: params.res_body,
ip: params.ip
};
data.code = isNaN(data.code) ? 200 : +data.code;
data.delay = isNaN(data.delay) ? 0 : +data.delay;
if (config.httpCodes.indexOf(data.code) === -1) {
return (ctx.body = yapi.commons.resReturn(null, 408, '非法的 httpCode'));
}
let findRepeat, findRepeatParams;
findRepeatParams = {
project_id: data.project_id,
interface_id: data.interface_id,
ip_enable: data.ip_enable
};
if (data.params && typeof data.params === 'object' && Object.keys(data.params).length > 0) {
for (let i in data.params) {
findRepeatParams['params.' + i] = data.params[i];
}
}
if (data.ip_enable) {
findRepeatParams.ip = data.ip;
}
findRepeat = await this.caseModel.get(findRepeatParams);
if (findRepeat && findRepeat._id !== params.id) {
return (ctx.body = yapi.commons.resReturn(null, 400, '已存在的期望'));
}
let result;
if (params.id && !isNaN(params.id)) {
data.id = +params.id;
result = await this.caseModel.up(data);
} else {
result = await this.caseModel.save(data);
}
return (ctx.body = yapi.commons.resReturn(result));
}
async delCase(ctx) {
let id = ctx.request.body.id;
if (!id) {
return (ctx.body = yapi.commons.resReturn(null, 408, '缺少 id'));
}
let result = await this.caseModel.del(id);
return (ctx.body = yapi.commons.resReturn(result));
}
async hideCase(ctx) {
let id = ctx.request.body.id;
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));
}
}
module.exports = advMockController;

View File

@@ -0,0 +1,7 @@
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
]
}

View File

@@ -0,0 +1,212 @@
const controller = require('./controller');
const advModel = require('./advMockModel.js');
const caseModel = require('./caseModel.js');
const yapi = require('yapi.js');
const mongoose = require('mongoose');
const _ = require('underscore');
const path = require('path');
const lib = require(path.resolve(yapi.WEBROOT, 'common/lib.js'));
const Mock = require('mockjs');
const mockExtra = require(path.resolve(yapi.WEBROOT, 'common/mock-extra.js'));
function arrToObj(arr) {
let obj = { 'Set-Cookie': [] };
arr.forEach(item => {
if (item.name === 'Set-Cookie') {
obj['Set-Cookie'].push(item.value);
} else obj[item.name] = item.value;
});
return obj;
}
module.exports = function() {
yapi.connect.then(function() {
let Col = mongoose.connection.db.collection('adv_mock');
Col.createIndex({
interface_id: 1
});
Col.createIndex({
project_id: 1
});
let caseCol = mongoose.connection.db.collection('adv_mock_case');
caseCol.createIndex({
interface_id: 1
});
caseCol.createIndex({
project_id: 1
});
});
async function checkCase(ctx, interfaceId) {
let reqParams = Object.assign({}, ctx.query, ctx.request.body);
let caseInst = yapi.getInst(caseModel);
// let ip = ctx.ip.match(/\d+.\d+.\d+.\d+/)[0];
// request.ip
let ip = yapi.commons.getIp(ctx);
// 数据库信息查询
// 过滤 开启IP
let listWithIp = await caseInst.model
.find({
interface_id: interfaceId,
ip_enable: true,
ip: ip
})
.select('_id params case_enable');
let matchList = [];
listWithIp.forEach(item => {
let params = item.params;
if (item.case_enable && lib.isDeepMatch(reqParams, params)) {
matchList.push(item);
}
});
// 其他数据
if (matchList.length === 0) {
let list = await caseInst.model
.find({
interface_id: interfaceId,
ip_enable: false
})
.select('_id params case_enable');
list.forEach(item => {
let params = item.params;
if (item.case_enable && lib.isDeepMatch(reqParams, params)) {
matchList.push(item);
}
});
}
if (matchList.length > 0) {
let maxItem = _.max(matchList, item => (item.params && Object.keys(item.params).length) || 0);
return maxItem;
}
return null;
}
async function handleByCase(caseData) {
let caseInst = yapi.getInst(caseModel);
let result = await caseInst.get({
_id: caseData._id
});
return result;
}
this.bindHook('add_router', function(addRouter) {
addRouter({
controller: controller,
method: 'get',
path: 'advmock/get',
action: 'getMock'
});
addRouter({
controller: controller,
method: 'post',
path: 'advmock/save',
action: 'upMock'
});
addRouter({
/**
* 保存期望
*/
controller: controller,
method: 'post',
path: 'advmock/case/save',
action: 'saveCase'
});
addRouter({
controller: controller,
method: 'get',
path: 'advmock/case/get',
action: 'getCase'
});
addRouter({
/**
* 获取期望列表
*/
controller: controller,
method: 'get',
path: 'advmock/case/list',
action: 'list'
});
addRouter({
/**
* 删除期望列表
*/
controller: controller,
method: 'post',
path: 'advmock/case/del',
action: 'delCase'
});
addRouter({
/**
* 隐藏期望列表
*/
controller: controller,
method: 'post',
path: 'advmock/case/hide',
action: 'hideCase'
});
});
this.bindHook('interface_del', async function(id) {
let inst = yapi.getInst(advModel);
await inst.delByInterfaceId(id);
});
this.bindHook('project_del', async function(id) {
let inst = yapi.getInst(advModel);
await inst.delByProjectId(id);
});
/**
* let context = {
projectData: project,
interfaceData: interfaceData,
ctx: ctx,
mockJson: res
}
*/
this.bindHook('mock_after', async function(context) {
let interfaceId = context.interfaceData._id;
let caseData = await checkCase(context.ctx, interfaceId);
// 只有开启高级mock才可用
if (caseData && caseData.case_enable) {
// 匹配到高级mock
let data = await handleByCase(caseData);
context.mockJson = yapi.commons.json_parse(data.res_body);
try {
context.mockJson = Mock.mock(
mockExtra(context.mockJson, {
query: context.ctx.query,
body: context.ctx.request.body,
params: Object.assign({}, context.ctx.query, context.ctx.request.body)
})
);
} catch (err) {
yapi.commons.log(err, 'error');
}
context.resHeader = arrToObj(data.headers);
context.httpCode = data.code;
context.delay = data.delay;
return true;
}
let inst = yapi.getInst(advModel);
let data = await inst.get(interfaceId);
if (!data || !data.enable || !data.mock_script) {
return context;
}
// mock 脚本
let script = data.mock_script;
await yapi.commons.handleMockScript(script, context);
});
};

View File

@@ -0,0 +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 文件'
// }
}
module.exports = function() {
this.bindHook('export_data', exportData);
};

View File

@@ -0,0 +1,189 @@
const baseController = require('controllers/base.js');
const interfaceModel = require('models/interface.js');
const projectModel = require('models/project.js');
// const wikiModel = require('../yapi-plugin-wiki/wikiModel.js');
const interfaceCatModel = require('models/interfaceCat.js');
const yapi = require('yapi.js');
const markdownIt = require('markdown-it');
const markdownItAnchor = require('markdown-it-anchor');
const markdownItTableOfContents = require('markdown-it-table-of-contents');
const defaultTheme = require('./defaultTheme.js');
const md = require('../../common/markdown');
// const htmlToPdf = require("html-pdf");
class exportController extends baseController {
constructor(ctx) {
super(ctx);
this.catModel = yapi.getInst(interfaceCatModel);
this.interModel = yapi.getInst(interfaceModel);
this.projectModel = yapi.getInst(projectModel);
}
async handleListClass(pid, status) {
let result = await this.catModel.list(pid),
newResult = [];
for (let i = 0, item, list; i < result.length; i++) {
item = result[i].toObject();
list = await this.interModel.listByInterStatus(item._id, status);
list = list.sort((a, b) => {
return a.index - b.index;
});
if (list.length > 0) {
item.list = list;
newResult.push(item);
}
}
return newResult;
}
handleExistId(data) {
function delArrId(arr, fn) {
if (!Array.isArray(arr)) return;
arr.forEach(item => {
delete item._id;
delete item.__v;
delete item.uid;
delete item.edit_uid;
delete item.catid;
delete item.project_id;
if (typeof fn === 'function') fn(item);
});
}
delArrId(data, function(item) {
delArrId(item.list, function(api) {
delArrId(api.req_body_form);
delArrId(api.req_params);
delArrId(api.req_query);
delArrId(api.req_headers);
if (api.query_path && typeof api.query_path === 'object') {
delArrId(api.query_path.params);
}
});
});
return data;
}
async exportData(ctx) {
let pid = ctx.request.query.pid;
let type = ctx.request.query.type;
let status = ctx.request.query.status;
let isWiki = ctx.request.query.isWiki;
if (!pid) {
ctx.body = yapi.commons.resReturn(null, 200, 'pid 不为空');
}
let curProject, wikiData;
let tp = '';
try {
curProject = await this.projectModel.get(pid);
if (isWiki === 'true') {
const wikiModel = require('../yapi-plugin-wiki/wikiModel.js');
wikiData = await yapi.getInst(wikiModel).get(pid);
}
ctx.set('Content-Type', 'application/octet-stream');
const list = await this.handleListClass(pid, status);
switch (type) {
case 'markdown': {
tp = await createMarkdown.bind(this)(list, false);
ctx.set('Content-Disposition', `attachment; filename=api.md`);
return (ctx.body = tp);
}
case 'json': {
let data = this.handleExistId(list);
tp = JSON.stringify(data, null, 2);
ctx.set('Content-Disposition', `attachment; filename=api.json`);
return (ctx.body = tp);
}
default: {
//默认为html
tp = await createHtml.bind(this)(list);
ctx.set('Content-Disposition', `attachment; filename=api.html`);
return (ctx.body = tp);
}
}
} catch (error) {
yapi.commons.log(error, 'error');
ctx.body = yapi.commons.resReturn(null, 502, '下载出错');
}
async function createHtml(list) {
let md = await createMarkdown.bind(this)(list, true);
let markdown = markdownIt({ html: true, breaks: true });
markdown.use(markdownItAnchor); // Optional, but makes sense as you really want to link to something
markdown.use(markdownItTableOfContents, {
markerPattern: /^\[toc\]/im
});
// require('fs').writeFileSync('./a.markdown', md);
let tp = unescape(markdown.render(md));
// require('fs').writeFileSync('./a.html', tp);
let left;
// console.log('tp',tp);
let content = tp.replace(
/<div\s+?class="table-of-contents"\s*>[\s\S]*?<\/ul>\s*<\/div>/gi,
function(match) {
left = match;
return '';
}
);
return createHtml5(left || '', content);
}
function createHtml5(left, tp) {
//html5模板
let html = `<!DOCTYPE html>
<html>
<head>
<title>${curProject.name}</title>
<meta charset="utf-8" />
${defaultTheme}
</head>
<body>
<div class="m-header">
<a href="#" style="display: inherit;"><svg class="svg" width="32px" height="32px" viewBox="0 0 64 64" version="1.1"><title>Icon</title><desc>Created with Sketch.</desc><defs><linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-1"><stop stop-color="#FFFFFF" offset="0%"></stop><stop stop-color="#F2F2F2" offset="100%"></stop></linearGradient><circle id="path-2" cx="31.9988602" cy="31.9988602" r="2.92886048"></circle><filter x="-85.4%" y="-68.3%" width="270.7%" height="270.7%" filterUnits="objectBoundingBox" id="filter-3"><feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset><feGaussianBlur stdDeviation="1.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur><feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.159703351 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix></filter></defs><g id="首页" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><g id="大屏幕"><g id="Icon"><circle id="Oval-1" fill="url(#linearGradient-1)" cx="32" cy="32" r="32"></circle><path d="M36.7078009,31.8054514 L36.7078009,51.7110548 C36.7078009,54.2844537 34.6258634,56.3695395 32.0579205,56.3695395 C29.4899777,56.3695395 27.4099998,54.0704461 27.4099998,51.7941246 L27.4099998,31.8061972 C27.4099998,29.528395 29.4909575,27.218453 32.0589004,27.230043 C34.6268432,27.241633 36.7078009,29.528395 36.7078009,31.8054514 Z" id="blue" fill="#2359F1" fill-rule="nonzero"></path><path d="M45.2586091,17.1026914 C45.2586091,17.1026914 45.5657231,34.0524383 45.2345291,37.01141 C44.9033351,39.9703817 43.1767091,41.6667796 40.6088126,41.6667796 C38.040916,41.6667796 35.9609757,39.3676862 35.9609757,37.0913646 L35.9609757,17.1034372 C35.9609757,14.825635 38.0418959,12.515693 40.6097924,12.527283 C43.177689,12.538873 45.2586091,14.825635 45.2586091,17.1026914 Z" id="green" fill="#57CF27" fill-rule="nonzero" transform="translate(40.674608, 27.097010) rotate(60.000000) translate(-40.674608, -27.097010) "></path><path d="M28.0410158,17.0465598 L28.0410158,36.9521632 C28.0410158,39.525562 25.9591158,41.6106479 23.3912193,41.6106479 C20.8233227,41.6106479 18.7433824,39.3115545 18.7433824,37.035233 L18.7433824,17.0473055 C18.7433824,14.7695034 20.8243026,12.4595614 23.3921991,12.4711513 C25.9600956,12.4827413 28.0410158,14.7695034 28.0410158,17.0465598 Z" id="red" fill="#FF561B" fill-rule="nonzero" transform="translate(23.392199, 27.040878) rotate(-60.000000) translate(-23.392199, -27.040878) "></path><g id="inner-round"><use fill="black" fill-opacity="1" filter="url(#filter-3)" xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#path-2"></use><use fill="#F7F7F7" fill-rule="evenodd" xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#path-2"></use></g></g></g></g></svg></a>
<a href="#"><h1 class="title">YAPI 接口文档</h1></a>
<div class="nav">
<a href="https://hellosean1025.github.io/yapi/">YApi</a>
</div>
</div>
<div class="g-doc">
${left}
<div id="right" class="content-right">
${tp}
<footer class="m-footer">
<p>Build by <a href="https://blog.opendeveloper.cn/yapi">YMFE</a>.</p>
</footer>
</div>
</div>
</body>
</html>
`;
return html;
}
function createMarkdown(list, isToc) {
//拼接markdown
//模板
let mdTemplate = ``;
try {
// 项目名称信息
mdTemplate += md.createProjectMarkdown(curProject, wikiData);
// 分类信息
mdTemplate += md.createClassMarkdown(curProject, list, isToc);
return mdTemplate;
} catch (e) {
yapi.commons.log(e, 'error');
ctx.body = yapi.commons.resReturn(null, 502, '下载出错');
}
}
}
}
module.exports = exportController;

View File

@@ -0,0 +1,351 @@
@charset "UTF-8";
html,
body,
h1,
h2,
h3,
h4,
h5,
h6,
p,
blockquote {
margin: 0;
padding: 0;
font-weight: normal;
-webkit-font-smoothing: antialiased;
}
/* 设置滚动条的样式 */
::-webkit-scrollbar {
width: 6px;
}
/* 外层轨道 */
::-webkit-scrollbar-track {
-webkit-box-shadow: inset006pxrgba(255, 0, 0, 0.3);
background: rgba(0, 0, 0, 0.1);
}
/* 滚动条滑块 */
::-webkit-scrollbar-thumb {
border-radius: 4px;
background: rgba(0, 0, 0, 0.2);
-webkit-box-shadow: inset006pxrgba(0, 0, 0, 0.5);
}
::-webkit-scrollbar-thumb:window-inactive {
background: rgba(0, 0, 0, 0.2);
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", SimSun, sans-serif;
font-size: 13px;
line-height: 25px;
color: #393838;
position: relative;
}
table {
margin: 10px 0 15px 0;
border-collapse: collapse;
}
td,
th {
border: 1px solid #ddd;
padding: 3px 10px;
}
th {
padding: 5px 10px;
}
a, a:link, a:visited {
color: #34495e;
text-decoration: none;
}
a:hover, a:focus {
color: #59d69d;
text-decoration: none;
}
a img {
border: none;
}
p {
padding-left: 10px;
margin-bottom: 9px;
}
h1,
h2,
h3,
h4,
h5,
h6 {
color: #404040;
line-height: 36px;
}
h1 {
color: #2c3e50;
font-weight: 600;
margin-bottom: 16px;
font-size: 32px;
padding-bottom: 16px;
border-bottom: 1px solid #ddd;
line-height: 50px;
}
h2 {
font-size: 28px;
padding-top: 10px;
padding-bottom: 10px;
}
h3 {
clear: both;
font-weight: 400;
margin-top: 20px;
margin-bottom: 20px;
border-left: 3px solid #59d69d;
padding-left: 8px;
font-size: 18px;
}
h4 {
font-size: 16px;
}
h5 {
font-size: 14px;
}
h6 {
font-size: 13px;
}
hr {
margin: 0 0 19px;
border: 0;
border-bottom: 1px solid #ccc;
}
blockquote {
padding: 13px 13px 21px 15px;
margin-bottom: 18px;
font-family: georgia, serif;
font-style: italic;
}
blockquote:before {
font-size: 40px;
margin-left: -10px;
font-family: georgia, serif;
color: #eee;
}
blockquote p {
font-size: 14px;
font-weight: 300;
line-height: 18px;
margin-bottom: 0;
font-style: italic;
}
code,
pre {
font-family: Monaco, Andale Mono, Courier New, monospace;
}
code {
background-color: #fee9cc;
color: rgba(0, 0, 0, 0.75);
padding: 1px 3px;
font-size: 12px;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
}
pre {
display: block;
padding: 14px;
margin: 0 0 18px;
line-height: 16px;
font-size: 11px;
border: 1px solid #d9d9d9;
white-space: pre-wrap;
word-wrap: break-word;
background: #f6f6f6;
}
pre code {
background-color: #f6f6f6;
color: #737373;
font-size: 11px;
padding: 0;
}
sup {
font-size: 0.83em;
vertical-align: super;
line-height: 0;
}
* {
-webkit-print-color-adjust: exact;
}
@media print {
body,
code,
pre code,
h1,
h2,
h3,
h4,
h5,
h6 {
color: black;
}
table,
pre {
page-break-inside: avoid;
}
}
html,
body {
height: 100%;
}
.table-of-contents {
position: fixed;
top: 61px;
left: 0;
bottom: 0;
overflow-x: hidden;
overflow-y: auto;
width: 260px;
}
.table-of-contents > ul > li > a {
font-size: 20px;
margin-bottom: 16px;
margin-top: 16px;
}
.table-of-contents ul {
overflow: auto;
margin: 0px;
height: 100%;
padding: 0px 0px;
box-sizing: border-box;
list-style-type: none;
}
.table-of-contents ul li {
padding-left: 20px;
}
.table-of-contents a {
padding: 2px 0px;
display: block;
text-decoration: none;
}
.content-right {
max-width: 700px;
margin-left: 290px;
padding-left: 70px;
flex-grow: 1;
}
.content-right h2:target {
padding-top: 80px;
}
body > p {
margin-left: 30px;
}
body > table {
margin-left: 30px;
}
body > pre {
margin-left: 30px;
}
.curProject {
position: fixed;
top: 20px;
font-size: 25px;
color: black;
margin-left: -240px;
width: 240px;
padding: 5px;
line-height: 25px;
box-sizing: border-box;
}
.g-doc {
margin-top: 56px;
padding-top: 24px;
display: flex;
}
.curproject-name {
font-size: 42px;
}
.m-header {
background: #32363a;
height: 56px;
line-height: 56px;
padding-left: 60px;
display: flex;
align-items: center;
position: fixed;
z-index: 9;
top: 0;
left: 0;
right: 0;
}
.m-header .title {
font-size: 22px;
color: #fff;
font-weight: normal;
-webkit-font-smoothing: antialiased;
margin: 0;
margin-left: 16px;
padding: 0;
line-height: 56px;
border: none;
}
.m-header .nav {
color: #fff;
font-size: 16px;
position: absolute;
right: 32px;
top: 0;
}
.m-header .nav a {
color: #fff;
margin-left: 16px;
padding: 8px;
transition: color .2s;
}
.m-header .nav a:hover {
color: #59d69d;
}
.m-footer {
border-top: 1px solid #ddd;
padding-top: 16px;
padding-bottom: 16px;
}
/*# sourceMappingURL=defaultTheme.css.map */

View File

@@ -0,0 +1,7 @@
{
"version": 3,
"mappings": "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",
"sources": ["defaultTheme.scss"],
"names": [],
"file": "defaultTheme.css"
}

View File

@@ -0,0 +1,4 @@
const fs = require('fs');
const sysPath = require('path');
const css = fs.readFileSync(sysPath.join(__dirname, './defaultTheme.css'));
module.exports = '<style>' + css + '</style>';

View File

@@ -0,0 +1,355 @@
html,
body,
h1,
h2,
h3,
h4,
h5,
h6,
p,
blockquote {
margin: 0;
padding: 0;
font-weight: normal;
-webkit-font-smoothing: antialiased;
}
/* 设置滚动条的样式 */
::-webkit-scrollbar {
width: 6px;
}
/* 外层轨道 */
::-webkit-scrollbar-track {
-webkit-box-shadow: inset006pxrgba(255, 0, 0, 0.3);
background: rgba(0, 0, 0, 0.1);
}
/* 滚动条滑块 */
::-webkit-scrollbar-thumb {
border-radius: 4px;
background: rgba(0, 0, 0, 0.2);
-webkit-box-shadow: inset006pxrgba(0, 0, 0, 0.5);
}
::-webkit-scrollbar-thumb:window-inactive {
background: rgba(0, 0, 0, 0.2);
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", SimSun, sans-serif;
font-size: 13px;
line-height: 25px;
color: #393838;
position: relative;
// overflow-x: hidden;
}
table {
margin: 10px 0 15px 0;
border-collapse: collapse;
}
td,
th {
border: 1px solid #ddd;
padding: 3px 10px;
}
th {
padding: 5px 10px;
}
a, a:link, a:visited {
color: #34495e;
text-decoration: none;
}
a:hover, a:focus {
color: #59d69d;
text-decoration: none;
}
a img {
border: none;
}
p {
padding-left: 10px;
margin-bottom: 9px;
}
h1,
h2,
h3,
h4,
h5,
h6 {
color: #404040;
line-height: 36px;
}
h1 {
color: #2c3e50;
font-weight: 600;
// margin-top: 35px;
margin-bottom: 16px;
font-size: 32px;
padding-bottom: 16px;
border-bottom: 1px solid #ddd;
line-height: 50px;
}
h2 {
font-size: 28px;
padding-top: 10px;
padding-bottom: 10px;
}
h3 {
clear: both;
font-weight: 400;
margin-top: 20px;
margin-bottom: 20px;
border-left: 3px solid #59d69d;
padding-left: 8px;
font-size: 18px;
}
h4 {
font-size: 16px;
}
h5 {
font-size: 14px;
}
h6 {
font-size: 13px;
}
hr {
margin: 0 0 19px;
border: 0;
border-bottom: 1px solid #ccc;
}
blockquote {
padding: 13px 13px 21px 15px;
margin-bottom: 18px;
font-family: georgia, serif;
font-style: italic;
}
blockquote:before {
font-size: 40px;
margin-left: -10px;
font-family: georgia, serif;
color: #eee;
}
blockquote p {
font-size: 14px;
font-weight: 300;
line-height: 18px;
margin-bottom: 0;
font-style: italic;
}
code,
pre {
font-family: Monaco, Andale Mono, Courier New, monospace;
}
code {
background-color: #fee9cc;
color: rgba(0, 0, 0, 0.75);
padding: 1px 3px;
font-size: 12px;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
}
pre {
display: block;
padding: 14px;
margin: 0 0 18px;
line-height: 16px;
font-size: 11px;
border: 1px solid #d9d9d9;
white-space: pre-wrap;
word-wrap: break-word;
background: #f6f6f6;
}
pre code {
background-color: #f6f6f6;
color: #737373;
font-size: 11px;
padding: 0;
}
sup {
font-size: 0.83em;
vertical-align: super;
line-height: 0;
}
* {
-webkit-print-color-adjust: exact;
}
@media print {
body,
code,
pre code,
h1,
h2,
h3,
h4,
h5,
h6 {
color: black;
}
table,
pre {
page-break-inside: avoid;
}
}
html,
body {
height: 100%;
}
.table-of-contents {
position: fixed;
top: 61px;
left: 0;
bottom: 0;
overflow-x: hidden;
overflow-y: auto;
width: 260px;
}
.table-of-contents>ul>li>a {
font-size: 20px;
margin-bottom: 16px;
margin-top: 16px;
}
.table-of-contents ul {
// position: fixed;
// top: 80px;
// left: 40px;
overflow: auto;
margin: 0px;
height: 100%;
padding: 0px 0px;
box-sizing: border-box;
list-style-type: none;
}
.table-of-contents ul li {
padding-left: 20px;
}
.table-of-contents a {
padding: 2px 0px;
display: block;
text-decoration: none;
}
.content-right {
// position: relative;
// top: -20px;
max-width: 700px;
margin-left: 290px;
padding-left: 70px;
flex-grow: 1;
h2:target{
padding-top: 80px;
}
}
body>p {
margin-left: 30px;
}
body>table {
margin-left: 30px;
}
body>pre {
margin-left: 30px;
}
.curProject {
position: fixed;
top: 20px;
font-size: 25px;
color: black;
margin-left: -240px;
width: 240px;
padding: 5px;
line-height: 25px;
box-sizing: border-box;
}
.g-doc {
margin-top: 56px;
padding-top: 24px;
display: flex;
}
.curproject-name{
font-size: 42px;
}
.m-header {
background: #32363a;
height: 56px;
line-height: 56px;
padding-left: 60px;
display: flex;
align-items: center;
position: fixed;
z-index: 9;
top: 0;
left: 0;
right: 0;
.title {
font-size: 22px;
color: #fff;
font-weight: normal;
-webkit-font-smoothing: antialiased;
margin: 0;
margin-left: 16px;
padding: 0;
line-height: 56px;
border: none;
}
.nav {
color: #fff;
font-size: 16px;
position: absolute;
right: 32px;
top: 0;
a {
color: #fff;
margin-left: 16px;
padding: 8px;
transition: color .2s;
}
a:hover {
color: #59d69d;
}
}
}
.m-footer {
border-top: 1px solid #ddd;
padding-top: 16px;
padding-bottom: 16px;
}

View File

@@ -0,0 +1,4 @@
module.exports = {
server: true,
client: true
}

View File

@@ -0,0 +1,16 @@
const controller = require('./controller');
// const mongoose = require('mongoose');
// const _ = require('underscore');
module.exports = function(){
this.bindHook('add_router', function(addRouter){
addRouter({
controller: controller,
method: 'get',
path: 'export',
action: 'exportData'
})
})
}

View File

@@ -0,0 +1,11 @@
function exportData(exportDataModule, pid) {
exportDataModule.swaggerjson = {
name: 'swaggerjson',
route: `/api/plugin/exportSwagger?type=OpenAPIV2&pid=${pid}`,
desc: '导出项目接口文档为(Swagger 2.0)Json文件'
};
}
module.exports = function() {
this.bindHook('export_data', exportData);
};

View File

@@ -0,0 +1,288 @@
const baseController = require('controllers/base.js');
const interfaceModel = require('models/interface.js');
const projectModel = require('models/project.js');
const interfaceCatModel = require('models/interfaceCat.js');
const yapi = require('yapi.js');
class exportSwaggerController extends baseController {
constructor(ctx) {
super(ctx);
this.catModel = yapi.getInst(interfaceCatModel);
this.interModel = yapi.getInst(interfaceModel);
this.projectModel = yapi.getInst(projectModel);
}
/*
handleListClass,handleExistId is same as the exportController(yapi-plugin-export-data).
No DRY,but i have no idea to optimize it.
*/
async handleListClass(pid, status) {
let result = await this.catModel.list(pid),
newResult = [];
for (let i = 0, item, list; i < result.length; i++) {
item = result[i].toObject();
list = await this.interModel.listByInterStatus(item._id, status);
list = list.sort((a, b) => {
return a.index - b.index;
});
if (list.length > 0) {
item.list = list;
newResult.push(item);
}
}
return newResult;
}
handleExistId(data) {
function delArrId(arr, fn) {
if (!Array.isArray(arr)) return;
arr.forEach(item => {
delete item._id;
delete item.__v;
delete item.uid;
delete item.edit_uid;
delete item.catid;
delete item.project_id;
if (typeof fn === 'function') fn(item);
});
}
delArrId(data, function (item) {
delArrId(item.list, function (api) {
delArrId(api.req_body_form);
delArrId(api.req_params);
delArrId(api.req_query);
delArrId(api.req_headers);
if (api.query_path && typeof api.query_path === 'object') {
delArrId(api.query_path.params);
}
});
});
return data;
}
async exportData(ctx) {
let pid = ctx.request.query.pid;
let type = ctx.request.query.type;
let status = ctx.request.query.status;
if (!pid) {
ctx.body = yapi.commons.resReturn(null, 200, 'pid 不为空');
}
let curProject;
let tp = '';
try {
curProject = await this.projectModel.get(pid);
ctx.set('Content-Type', 'application/octet-stream');
const list = await this.handleListClass(pid, status);
switch (type) {
case 'OpenAPIV2':
{ //in this time, only implemented OpenAPI V2.0
let data = this.handleExistId(list);
let model = await convertToSwaggerV2Model(data);
tp = JSON.stringify(model, null, 2);
ctx.set('Content-Disposition', `attachment; filename=swaggerApi.json`);
return (ctx.body = tp);
}
default:
{
ctx.body = yapi.commons.resReturn(null, 400, 'type 无效参数')
}
}
} catch (error) {
yapi.commons.log(error, 'error');
ctx.body = yapi.commons.resReturn(null, 502, '下载出错');
}
//Convert to SwaggerV2.0 (OpenAPI 2.0)
async function convertToSwaggerV2Model(list) {
const swaggerObj = {
swagger: '2.0',
info: {
title: curProject.name,
version: 'last', // last version
description: curProject.desc
},
//host: "", // No find any info of host in this point :-)
basePath: curProject.basepath ? curProject.basepath : '/', //default base path is '/'(root)
tags: (() => {
let tagArray = [];
list.forEach(t => {
tagArray.push({
name: t.name,
description: t.desc
/*externalDocs:{
descroption:"",
url:""
} */
});
});
return tagArray;
})(),
schemes: [
"http" //Only http
],
paths: (() => {
let apisObj = {};
for (let aptTag of list) { //list of category
for (let api of aptTag.list) //list of api
{
if (apisObj[api.path] == null) {
apisObj[api.path] = {};
}
apisObj[api.path][api.method.toLowerCase()] = (() => {
let apiItem = {};
apiItem['tags'] = [aptTag.name];
apiItem['summary'] = api.title;
apiItem['description'] = api.markdown;
switch (api.req_body_type) {
case 'form':
case 'file':
apiItem['consumes'] = ['multipart/form-data']; //form data required
break;
case 'json':
apiItem['consumes'] = ['application/json'];
break;
case 'raw':
apiItem['consumes'] = ['text/plain'];
break;
default:
break;
}
apiItem['parameters'] = (() => {
let paramArray = [];
for (let p of api.req_headers) //Headers parameters
{
//swagger has consumes proprety, so skip proprety "Content-Type"
if (p.name === 'Content-Type') {
continue;
}
paramArray.push({
name: p.name,
in: 'header',
description: `${p.name} (Only:${p.value})`,
required: Number(p.required) === 1,
type: 'string', //always be type string
default: p.value
});
}
for (let p of api.req_params) //Path parameters
{
paramArray.push({
name: p.name,
in: 'path',
description: p.desc,
required: true, //swagger path parameters required proprety must be always true,
type: 'string' //always be type string
});
}
for (let p of api.req_query) //Query parameters
{
paramArray.push({
name: p.name,
in: 'query',
required: Number(p.required) === 1,
description: p.desc,
type: 'string' //always be type string
});
}
switch (api.req_body_type) //Body parameters
{
case 'form':
{
for (let p of api.req_body_form) {
paramArray.push({
name: p.name,
in: 'formData',
required: Number(p.required) === 1,
description: p.desc,
type: p.type === 'text' ? 'string' : 'file' //in this time .formData type have only text or file
});
}
break;
}
case 'json':
{
if (api.req_body_other) {
let jsonParam = JSON.parse(api.req_body_other);
if (jsonParam) {
paramArray.push({
name: 'root',
in: 'body',
description: jsonParam.description,
schema: jsonParam //as same as swagger's format
});
}
}
break;
}
case 'file':
{
paramArray.push({
name: 'upfile',
in: 'formData', //use formData
description: api.req_body_other,
type: 'file'
});
break;
}
case 'raw':
{
paramArray.push({
name: 'raw',
in: 'body',
description: 'raw paramter',
schema: {
type: 'string',
format: 'binary',
default: api.req_body_other
}
});
break;
}
default:
break;
}
return paramArray;
})();
apiItem['responses'] = {
'200': {
description: 'successful operation',
schema: (() => {
let schemaObj = {};
if (api.res_body_type === 'raw') {
schemaObj['type'] = 'string';
schemaObj['format'] = 'binary';
schemaObj['default'] = api.res_body;
} else if (api.res_body_type === 'json') {
if (api.res_body) {
let resBody = JSON.parse(api.res_body);
if (resBody !== null) {
//schemaObj['type']=resBody.type;
schemaObj = resBody; //as the parameters,
}
}
}
return schemaObj;
})()
}
};
return apiItem;
})();
}
}
return apisObj;
})()
};
return swaggerObj;
}
}
}
module.exports = exportSwaggerController;

View File

@@ -0,0 +1,4 @@
module.exports = {
server: true,
client: true
}

View File

@@ -0,0 +1,12 @@
const exportSwaggerController = require('./controller');
module.exports = function(){
this.bindHook('add_router', function(addRouter){
addRouter({
controller: exportSwaggerController,
method: 'get',
path: 'exportSwagger',
action: 'exportData'
})
})
}

View File

@@ -0,0 +1,76 @@
import React, { PureComponent as Component } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux';
import { getToken } from '../../../client/reducer/modules/project.js'
import './Services.scss';
@connect(
state => {
return {
token: state.project.token
}
},
{
getToken
}
)
export default class Services extends Component {
static propTypes = {
projectId: PropTypes.number,
token: PropTypes.string,
getToken: PropTypes.func
}
async componentDidMount() {
const id = this.props.projectId;
await this.props.getToken(id);
}
render () {
const id = this.props.projectId;
return (
<div className="project-services">
<section className="news-box m-panel">
<div className="token">
<h5>安装工具</h5>
<pre>{`
npm i sm2tsservice -D
`}</pre>
<h5>配置3.2.0及以上版本</h5>
<pre>{`
touch json2service.json
`}</pre>
<pre>{`
{
"url": "yapi-swagger.json",
"remoteUrl": "${location.protocol}//${location.hostname}${location.port ? `:${location.port}` : ''}/api/open/plugin/export-full?type=json&pid=${id}&status=all&token=${this.props.token}",
"type": "yapi",
"swaggerParser": {}
}
`}
</pre>
<h5>配置3.2.0以下版本</h5>
<pre>{`
touch json2service.json
`}</pre>
<pre>{`
{
"url": "${location.protocol}//${location.hostname}${location.port ? `:${location.port}` : ''}/api/open/plugin/export-full?type=json&pid=${id}&status=all&token=${this.props.token}",
"type": "yapi",
"swaggerParser": {}
}
`}
</pre>
<h5>生成services代码</h5>
<pre>{`
(./node_modules/.bin/)sm2tsservice --clear
`}</pre>
</div>
<a href="https://github.com/gogoyqj/sm2tsservice">更多说明 sm2tsservice</a>
</section>
</div>
);
}
}

View File

@@ -0,0 +1,6 @@
.project-services {
margin: 0;
pre {
background: #efefef;
}
}

View File

@@ -0,0 +1,12 @@
import Services from './Services/Services.js';
function genServices(routers) {
routers['services'] = {
name: '生成 ts services',
component: Services
}
}
module.exports = function() {
this.bindHook('sub_setting_nav', genServices);
};

View File

@@ -0,0 +1,209 @@
const baseController = require('controllers/base.js');
const interfaceModel = require('models/interface.js');
const projectModel = require('models/project.js');
// const wikiModel = require('../yapi-plugin-wiki/wikiModel.js');
const interfaceCatModel = require('models/interfaceCat.js');
const yapi = require('yapi.js');
const markdownIt = require('markdown-it');
const markdownItAnchor = require('markdown-it-anchor');
const markdownItTableOfContents = require('markdown-it-table-of-contents');
const defaultTheme = require('./defaultTheme.js');
const md = require('../../common/markdown');
// const htmlToPdf = require("html-pdf");
class exportController extends baseController {
constructor(ctx) {
super(ctx);
this.catModel = yapi.getInst(interfaceCatModel);
this.interModel = yapi.getInst(interfaceModel);
this.projectModel = yapi.getInst(projectModel);
}
async handleListClass(pid, status) {
let result = await this.catModel.list(pid),
newResult = [];
for (let i = 0, item, list; i < result.length; i++) {
item = result[i].toObject();
list = await this.interModel.listByInterStatus(item._id, status);
list = list.sort((a, b) => {
return a.index - b.index;
});
if (list.length > 0) {
item.list = list;
newResult.push(item);
}
}
return newResult;
}
handleExistId(data) {
function delArrId(arr, fn) {
if (!Array.isArray(arr)) return;
arr.forEach(item => {
delete item._id;
delete item.__v;
delete item.uid;
delete item.edit_uid;
delete item.catid;
delete item.project_id;
if (typeof fn === 'function') fn(item);
});
}
delArrId(data, function(item) {
delArrId(item.list, function(api) {
delArrId(api.req_body_form);
delArrId(api.req_params);
delArrId(api.req_query);
delArrId(api.req_headers);
if (api.query_path && typeof api.query_path === 'object') {
delArrId(api.query_path.params);
}
});
});
return data;
}
// @feat: serives
async exportFullData (ctx) {
return this.exportData(ctx, 'full-path');
}
async exportData(ctx, fullPath) {
let pid = ctx.request.query.pid;
let type = ctx.request.query.type;
let status = ctx.request.query.status;
let isWiki = ctx.request.query.isWiki;
if (!pid) {
return ctx.body = yapi.commons.resReturn(null, 200, 'pid 不为空');
}
let curProject, wikiData;
let tp = '';
try {
curProject = await this.projectModel.get(pid);
const basepath = curProject.basepath;
if (isWiki === 'true') {
const wikiModel = require('../yapi-plugin-wiki/wikiModel.js');
wikiData = await yapi.getInst(wikiModel).get(pid);
}
ctx.set('Content-Type', 'application/octet-stream');
const list = await this.handleListClass(pid, status);
switch (type) {
case 'markdown': {
tp = await createMarkdown.bind(this)(list, false);
ctx.set('Content-Disposition', `attachment; filename=api.md`);
return (ctx.body = tp);
}
case 'json': {
let data = this.handleExistId(list);
if (Array.isArray(data) && fullPath === 'full-path' && basepath) {
data.forEach(function(cate) {
if (Array.isArray(cate.list)) {
cate.proBasepath = basepath;
cate.proName = curProject.name;
cate.proDescription = curProject.desc;
cate.list = cate.list.map(function(api) {
api.path = api.query_path.path = (basepath + '/' + api.path).replace(/[\/]{2,}/g, '/');
return api;
});
}
})
}
tp = JSON.stringify(data, null, 2);
ctx.set('Content-Disposition', `attachment; filename=api.json`);
return (ctx.body = tp);
}
default: {
//默认为html
tp = await createHtml.bind(this)(list);
ctx.set('Content-Disposition', `attachment; filename=api.html`);
return (ctx.body = tp);
}
}
} catch (error) {
yapi.commons.log(error, 'error');
ctx.body = yapi.commons.resReturn(null, 502, '下载出错');
}
async function createHtml(list) {
let md = await createMarkdown.bind(this)(list, true);
let markdown = markdownIt({ html: true, breaks: true });
markdown.use(markdownItAnchor); // Optional, but makes sense as you really want to link to something
markdown.use(markdownItTableOfContents, {
markerPattern: /^\[toc\]/im
});
// require('fs').writeFileSync('./a.markdown', md);
let tp = unescape(markdown.render(md));
// require('fs').writeFileSync('./a.html', tp);
let left;
// console.log('tp',tp);
let content = tp.replace(
/<div\s+?class="table-of-contents"\s*>[\s\S]*?<\/ul>\s*<\/div>/gi,
function(match) {
left = match;
return '';
}
);
return createHtml5(left || '', content);
}
function createHtml5(left, tp) {
//html5模板
let html = `<!DOCTYPE html>
<html>
<head>
<title>${curProject.name}</title>
<meta charset="utf-8" />
${defaultTheme}
</head>
<body>
<div class="m-header">
<a href="#" style="display: inherit;"><svg class="svg" width="32px" height="32px" viewBox="0 0 64 64" version="1.1"><title>Icon</title><desc>Created with Sketch.</desc><defs><linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-1"><stop stop-color="#FFFFFF" offset="0%"></stop><stop stop-color="#F2F2F2" offset="100%"></stop></linearGradient><circle id="path-2" cx="31.9988602" cy="31.9988602" r="2.92886048"></circle><filter x="-85.4%" y="-68.3%" width="270.7%" height="270.7%" filterUnits="objectBoundingBox" id="filter-3"><feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset><feGaussianBlur stdDeviation="1.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur><feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.159703351 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix></filter></defs><g id="首页" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><g id="大屏幕"><g id="Icon"><circle id="Oval-1" fill="url(#linearGradient-1)" cx="32" cy="32" r="32"></circle><path d="M36.7078009,31.8054514 L36.7078009,51.7110548 C36.7078009,54.2844537 34.6258634,56.3695395 32.0579205,56.3695395 C29.4899777,56.3695395 27.4099998,54.0704461 27.4099998,51.7941246 L27.4099998,31.8061972 C27.4099998,29.528395 29.4909575,27.218453 32.0589004,27.230043 C34.6268432,27.241633 36.7078009,29.528395 36.7078009,31.8054514 Z" id="blue" fill="#2359F1" fill-rule="nonzero"></path><path d="M45.2586091,17.1026914 C45.2586091,17.1026914 45.5657231,34.0524383 45.2345291,37.01141 C44.9033351,39.9703817 43.1767091,41.6667796 40.6088126,41.6667796 C38.040916,41.6667796 35.9609757,39.3676862 35.9609757,37.0913646 L35.9609757,17.1034372 C35.9609757,14.825635 38.0418959,12.515693 40.6097924,12.527283 C43.177689,12.538873 45.2586091,14.825635 45.2586091,17.1026914 Z" id="green" fill="#57CF27" fill-rule="nonzero" transform="translate(40.674608, 27.097010) rotate(60.000000) translate(-40.674608, -27.097010) "></path><path d="M28.0410158,17.0465598 L28.0410158,36.9521632 C28.0410158,39.525562 25.9591158,41.6106479 23.3912193,41.6106479 C20.8233227,41.6106479 18.7433824,39.3115545 18.7433824,37.035233 L18.7433824,17.0473055 C18.7433824,14.7695034 20.8243026,12.4595614 23.3921991,12.4711513 C25.9600956,12.4827413 28.0410158,14.7695034 28.0410158,17.0465598 Z" id="red" fill="#FF561B" fill-rule="nonzero" transform="translate(23.392199, 27.040878) rotate(-60.000000) translate(-23.392199, -27.040878) "></path><g id="inner-round"><use fill="black" fill-opacity="1" filter="url(#filter-3)" xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#path-2"></use><use fill="#F7F7F7" fill-rule="evenodd" xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#path-2"></use></g></g></g></g></svg></a>
<a href="#"><h1 class="title">YAPI 接口文档</h1></a>
<div class="nav">
<a href="https://hellosean1025.github.io/yapi/">YApi</a>
</div>
</div>
<div class="g-doc">
${left}
<div id="right" class="content-right">
${tp}
<footer class="m-footer">
<p>Build by <a href="https://ymfe.org/">YMFE</a>.</p>
</footer>
</div>
</div>
</body>
</html>
`;
return html;
}
function createMarkdown(list, isToc) {
//拼接markdown
//模板
let mdTemplate = ``;
try {
// 项目名称信息
mdTemplate += md.createProjectMarkdown(curProject, wikiData);
// 分类信息
mdTemplate += md.createClassMarkdown(curProject, list, isToc);
return mdTemplate;
} catch (e) {
yapi.commons.log(e, 'error');
ctx.body = yapi.commons.resReturn(null, 502, '下载出错');
}
}
}
}
module.exports = exportController;

View File

@@ -0,0 +1,351 @@
@charset "UTF-8";
html,
body,
h1,
h2,
h3,
h4,
h5,
h6,
p,
blockquote {
margin: 0;
padding: 0;
font-weight: normal;
-webkit-font-smoothing: antialiased;
}
/* 设置滚动条的样式 */
::-webkit-scrollbar {
width: 6px;
}
/* 外层轨道 */
::-webkit-scrollbar-track {
-webkit-box-shadow: inset006pxrgba(255, 0, 0, 0.3);
background: rgba(0, 0, 0, 0.1);
}
/* 滚动条滑块 */
::-webkit-scrollbar-thumb {
border-radius: 4px;
background: rgba(0, 0, 0, 0.2);
-webkit-box-shadow: inset006pxrgba(0, 0, 0, 0.5);
}
::-webkit-scrollbar-thumb:window-inactive {
background: rgba(0, 0, 0, 0.2);
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", SimSun, sans-serif;
font-size: 13px;
line-height: 25px;
color: #393838;
position: relative;
}
table {
margin: 10px 0 15px 0;
border-collapse: collapse;
}
td,
th {
border: 1px solid #ddd;
padding: 3px 10px;
}
th {
padding: 5px 10px;
}
a, a:link, a:visited {
color: #34495e;
text-decoration: none;
}
a:hover, a:focus {
color: #59d69d;
text-decoration: none;
}
a img {
border: none;
}
p {
padding-left: 10px;
margin-bottom: 9px;
}
h1,
h2,
h3,
h4,
h5,
h6 {
color: #404040;
line-height: 36px;
}
h1 {
color: #2c3e50;
font-weight: 600;
margin-bottom: 16px;
font-size: 32px;
padding-bottom: 16px;
border-bottom: 1px solid #ddd;
line-height: 50px;
}
h2 {
font-size: 28px;
padding-top: 10px;
padding-bottom: 10px;
}
h3 {
clear: both;
font-weight: 400;
margin-top: 20px;
margin-bottom: 20px;
border-left: 3px solid #59d69d;
padding-left: 8px;
font-size: 18px;
}
h4 {
font-size: 16px;
}
h5 {
font-size: 14px;
}
h6 {
font-size: 13px;
}
hr {
margin: 0 0 19px;
border: 0;
border-bottom: 1px solid #ccc;
}
blockquote {
padding: 13px 13px 21px 15px;
margin-bottom: 18px;
font-family: georgia, serif;
font-style: italic;
}
blockquote:before {
font-size: 40px;
margin-left: -10px;
font-family: georgia, serif;
color: #eee;
}
blockquote p {
font-size: 14px;
font-weight: 300;
line-height: 18px;
margin-bottom: 0;
font-style: italic;
}
code,
pre {
font-family: Monaco, Andale Mono, Courier New, monospace;
}
code {
background-color: #fee9cc;
color: rgba(0, 0, 0, 0.75);
padding: 1px 3px;
font-size: 12px;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
}
pre {
display: block;
padding: 14px;
margin: 0 0 18px;
line-height: 16px;
font-size: 11px;
border: 1px solid #d9d9d9;
white-space: pre-wrap;
word-wrap: break-word;
background: #f6f6f6;
}
pre code {
background-color: #f6f6f6;
color: #737373;
font-size: 11px;
padding: 0;
}
sup {
font-size: 0.83em;
vertical-align: super;
line-height: 0;
}
* {
-webkit-print-color-adjust: exact;
}
@media print {
body,
code,
pre code,
h1,
h2,
h3,
h4,
h5,
h6 {
color: black;
}
table,
pre {
page-break-inside: avoid;
}
}
html,
body {
height: 100%;
}
.table-of-contents {
position: fixed;
top: 61px;
left: 0;
bottom: 0;
overflow-x: hidden;
overflow-y: auto;
width: 260px;
}
.table-of-contents > ul > li > a {
font-size: 20px;
margin-bottom: 16px;
margin-top: 16px;
}
.table-of-contents ul {
overflow: auto;
margin: 0px;
height: 100%;
padding: 0px 0px;
box-sizing: border-box;
list-style-type: none;
}
.table-of-contents ul li {
padding-left: 20px;
}
.table-of-contents a {
padding: 2px 0px;
display: block;
text-decoration: none;
}
.content-right {
max-width: 700px;
margin-left: 290px;
padding-left: 70px;
flex-grow: 1;
}
.content-right h2:target {
padding-top: 80px;
}
body > p {
margin-left: 30px;
}
body > table {
margin-left: 30px;
}
body > pre {
margin-left: 30px;
}
.curProject {
position: fixed;
top: 20px;
font-size: 25px;
color: black;
margin-left: -240px;
width: 240px;
padding: 5px;
line-height: 25px;
box-sizing: border-box;
}
.g-doc {
margin-top: 56px;
padding-top: 24px;
display: flex;
}
.curproject-name {
font-size: 42px;
}
.m-header {
background: #32363a;
height: 56px;
line-height: 56px;
padding-left: 60px;
display: flex;
align-items: center;
position: fixed;
z-index: 9;
top: 0;
left: 0;
right: 0;
}
.m-header .title {
font-size: 22px;
color: #fff;
font-weight: normal;
-webkit-font-smoothing: antialiased;
margin: 0;
margin-left: 16px;
padding: 0;
line-height: 56px;
border: none;
}
.m-header .nav {
color: #fff;
font-size: 16px;
position: absolute;
right: 32px;
top: 0;
}
.m-header .nav a {
color: #fff;
margin-left: 16px;
padding: 8px;
transition: color .2s;
}
.m-header .nav a:hover {
color: #59d69d;
}
.m-footer {
border-top: 1px solid #ddd;
padding-top: 16px;
padding-bottom: 16px;
}
/*# sourceMappingURL=defaultTheme.css.map */

View File

@@ -0,0 +1,7 @@
{
"version": 3,
"mappings": "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",
"sources": ["defaultTheme.scss"],
"names": [],
"file": "defaultTheme.css"
}

View File

@@ -0,0 +1,4 @@
const fs = require('fs');
const sysPath = require('path');
const css = fs.readFileSync(sysPath.join(__dirname, './defaultTheme.css'));
module.exports = '<style>' + css + '</style>';

View File

@@ -0,0 +1,355 @@
html,
body,
h1,
h2,
h3,
h4,
h5,
h6,
p,
blockquote {
margin: 0;
padding: 0;
font-weight: normal;
-webkit-font-smoothing: antialiased;
}
/* 设置滚动条的样式 */
::-webkit-scrollbar {
width: 6px;
}
/* 外层轨道 */
::-webkit-scrollbar-track {
-webkit-box-shadow: inset006pxrgba(255, 0, 0, 0.3);
background: rgba(0, 0, 0, 0.1);
}
/* 滚动条滑块 */
::-webkit-scrollbar-thumb {
border-radius: 4px;
background: rgba(0, 0, 0, 0.2);
-webkit-box-shadow: inset006pxrgba(0, 0, 0, 0.5);
}
::-webkit-scrollbar-thumb:window-inactive {
background: rgba(0, 0, 0, 0.2);
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", SimSun, sans-serif;
font-size: 13px;
line-height: 25px;
color: #393838;
position: relative;
// overflow-x: hidden;
}
table {
margin: 10px 0 15px 0;
border-collapse: collapse;
}
td,
th {
border: 1px solid #ddd;
padding: 3px 10px;
}
th {
padding: 5px 10px;
}
a, a:link, a:visited {
color: #34495e;
text-decoration: none;
}
a:hover, a:focus {
color: #59d69d;
text-decoration: none;
}
a img {
border: none;
}
p {
padding-left: 10px;
margin-bottom: 9px;
}
h1,
h2,
h3,
h4,
h5,
h6 {
color: #404040;
line-height: 36px;
}
h1 {
color: #2c3e50;
font-weight: 600;
// margin-top: 35px;
margin-bottom: 16px;
font-size: 32px;
padding-bottom: 16px;
border-bottom: 1px solid #ddd;
line-height: 50px;
}
h2 {
font-size: 28px;
padding-top: 10px;
padding-bottom: 10px;
}
h3 {
clear: both;
font-weight: 400;
margin-top: 20px;
margin-bottom: 20px;
border-left: 3px solid #59d69d;
padding-left: 8px;
font-size: 18px;
}
h4 {
font-size: 16px;
}
h5 {
font-size: 14px;
}
h6 {
font-size: 13px;
}
hr {
margin: 0 0 19px;
border: 0;
border-bottom: 1px solid #ccc;
}
blockquote {
padding: 13px 13px 21px 15px;
margin-bottom: 18px;
font-family: georgia, serif;
font-style: italic;
}
blockquote:before {
font-size: 40px;
margin-left: -10px;
font-family: georgia, serif;
color: #eee;
}
blockquote p {
font-size: 14px;
font-weight: 300;
line-height: 18px;
margin-bottom: 0;
font-style: italic;
}
code,
pre {
font-family: Monaco, Andale Mono, Courier New, monospace;
}
code {
background-color: #fee9cc;
color: rgba(0, 0, 0, 0.75);
padding: 1px 3px;
font-size: 12px;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
}
pre {
display: block;
padding: 14px;
margin: 0 0 18px;
line-height: 16px;
font-size: 11px;
border: 1px solid #d9d9d9;
white-space: pre-wrap;
word-wrap: break-word;
background: #f6f6f6;
}
pre code {
background-color: #f6f6f6;
color: #737373;
font-size: 11px;
padding: 0;
}
sup {
font-size: 0.83em;
vertical-align: super;
line-height: 0;
}
* {
-webkit-print-color-adjust: exact;
}
@media print {
body,
code,
pre code,
h1,
h2,
h3,
h4,
h5,
h6 {
color: black;
}
table,
pre {
page-break-inside: avoid;
}
}
html,
body {
height: 100%;
}
.table-of-contents {
position: fixed;
top: 61px;
left: 0;
bottom: 0;
overflow-x: hidden;
overflow-y: auto;
width: 260px;
}
.table-of-contents>ul>li>a {
font-size: 20px;
margin-bottom: 16px;
margin-top: 16px;
}
.table-of-contents ul {
// position: fixed;
// top: 80px;
// left: 40px;
overflow: auto;
margin: 0px;
height: 100%;
padding: 0px 0px;
box-sizing: border-box;
list-style-type: none;
}
.table-of-contents ul li {
padding-left: 20px;
}
.table-of-contents a {
padding: 2px 0px;
display: block;
text-decoration: none;
}
.content-right {
// position: relative;
// top: -20px;
max-width: 700px;
margin-left: 290px;
padding-left: 70px;
flex-grow: 1;
h2:target{
padding-top: 80px;
}
}
body>p {
margin-left: 30px;
}
body>table {
margin-left: 30px;
}
body>pre {
margin-left: 30px;
}
.curProject {
position: fixed;
top: 20px;
font-size: 25px;
color: black;
margin-left: -240px;
width: 240px;
padding: 5px;
line-height: 25px;
box-sizing: border-box;
}
.g-doc {
margin-top: 56px;
padding-top: 24px;
display: flex;
}
.curproject-name{
font-size: 42px;
}
.m-header {
background: #32363a;
height: 56px;
line-height: 56px;
padding-left: 60px;
display: flex;
align-items: center;
position: fixed;
z-index: 9;
top: 0;
left: 0;
right: 0;
.title {
font-size: 22px;
color: #fff;
font-weight: normal;
-webkit-font-smoothing: antialiased;
margin: 0;
margin-left: 16px;
padding: 0;
line-height: 56px;
border: none;
}
.nav {
color: #fff;
font-size: 16px;
position: absolute;
right: 32px;
top: 0;
a {
color: #fff;
margin-left: 16px;
padding: 8px;
transition: color .2s;
}
a:hover {
color: #59d69d;
}
}
}
.m-footer {
border-top: 1px solid #ddd;
padding-top: 16px;
padding-bottom: 16px;
}

View File

@@ -0,0 +1,4 @@
module.exports = {
server: true,
client: true
}

View File

@@ -0,0 +1,18 @@
const controller = require('./controller');
// const mongoose = require('mongoose');
// const _ = require('underscore');
module.exports = function(){
this.bindHook('add_router', function(addRouter){
// @feat: serives
addRouter({
controller: controller,
method: 'get',
prefix: '/open',
path: 'export-full',
action: 'exportFullData'
});
})
}

View File

@@ -0,0 +1,229 @@
import { message } from 'antd';
import URL from 'url';
const GenerateSchema = require('generate-schema/src/schemas/json.js');
import { json_parse, unbase64 } from '../../common/utils.js';
const transformJsonToSchema = json => {
json = json || {};
let jsonData = json_parse(json);
jsonData = GenerateSchema(jsonData);
let schemaData = JSON.stringify(jsonData);
return schemaData;
};
function postman(importDataModule) {
function parseUrl(url) {
return URL.parse(url);
}
function checkInterRepeat(interData) {
let obj = {};
let arr = [];
for (let item in interData) {
// console.log(interData[item].url + "-" + interData[item].method);
let key = interData[item].request.url + '|' + interData[item].request.method;
if (!obj[key]) {
arr.push(interData[item]);
obj[key] = true;
}
}
return arr;
}
function handleReq_query(query) {
let res = [];
if (query && query.length) {
for (let item in query) {
res.push({
name: query[item].name,
value: query[item].value
});
}
}
return res;
}
// function handleReq_headers(headers){
// let res = [];
// if(headers&&headers.length){
// for(let item in headers){
// res.push({
// name: headers[item].key,
// desc: headers[item].description,
// value: headers[item].value,
// required: headers[item].enable
// });
// }
// }
// return res;
// }
function handleReq_body_form(body_form) {
let res = [];
if (body_form && typeof body_form === 'object') {
for (let item in body_form) {
res.push({
name: body_form[item].name,
value: body_form[item].value,
type: 'text'
});
}
}
return res;
}
function handlePath(path) {
path = parseUrl(path).pathname;
path = decodeURIComponent(path);
if (!path) return '';
path = path.replace(/{{\w*}}/g, '');
if (path[0] != '/') {
path = '/' + path;
}
return path;
}
function run(res) {
try {
res = JSON.parse(res);
res = res.log.entries;
res = res.filter(item => {
if (!item) return false;
return item.response.content.mimeType.indexOf('application/json') === 0;
});
let interfaceData = { apis: [] };
res = checkInterRepeat.bind(this)(res);
if (res && res.length) {
for (let item in res) {
let data = importHar.bind(this)(res[item]);
interfaceData.apis.push(data);
}
}
return interfaceData;
} catch (e) {
console.error(e);
message.error('数据格式有误');
}
}
function importHar(data, key) {
let reflect = {
//数据字段映射关系
title: 'url',
path: 'url',
method: 'method',
desc: 'description',
req_query: 'queryString',
req_body_form: 'params',
req_body_other: 'text'
};
let allKey = [
'title',
'path',
'method',
'req_query',
'req_body_type',
'req_body_form',
'req_body_other',
'res_body_type',
'res_body',
'req_headers'
];
key = key || allKey;
let res = {};
let reqType = 'json',
header;
data.request.headers.forEach(item => {
if (!item || !item.name || !item.value) return null;
if (/content-type/i.test(item.name) && item.value.indexOf('application/json') === 0) {
reqType = 'json';
header = 'application/json';
} else if (
/content-type/i.test(item.name) &&
item.value.indexOf('application/x-www-form-urlencoded') === 0
) {
header = 'application/x-www-form-urlencoded';
reqType = 'form';
} else if (
/content-type/i.test(item.name) &&
item.value.indexOf('multipart/form-data') === 0
) {
header = 'multipart/form-data';
reqType = 'form';
}
});
for (let item in key) {
item = key[item];
if (item === 'req_query') {
res[item] = handleReq_query.bind(this)(data.request[reflect[item]]);
} else if (item === 'req_body_form' && reqType === 'form' && data.request.postData) {
if (header === 'application/x-www-form-urlencoded') {
res[item] = handleReq_body_form.bind(this)(data.request.postData[reflect[item]]);
} else if (header === 'multipart/form-data') {
res[item] = [];
}
} else if (item === 'req_body_other' && reqType === 'json' && data.request.postData) {
res.req_body_is_json_schema = true;
res[item] = transformJsonToSchema(data.request.postData.text);
} else if (item === 'req_headers') {
res[item] = [
{
name: 'Content-Type',
value: header
}
];
} else if (item === 'req_body_type') {
res[item] = reqType;
} else if (item === 'path') {
res[item] = handlePath.bind(this)(data.request[reflect[item]]);
} else if (item === 'title') {
let path = handlePath.bind(this)(data.request[reflect['path']]);
if (data.request[reflect[item]].indexOf(path) > -1) {
res[item] = path;
if (res[item] && res[item].indexOf('/:') > -1) {
res[item] = res[item].substr(0, res[item].indexOf('/:'));
}
} else {
res[item] = data.request[reflect[item]];
}
} else if (item === 'res_body_type') {
res[item] = 'json';
} else if (item === 'res_body') {
res.res_body_is_json_schema = true;
if (data.response.content.encoding && data.response.content.encoding == 'base64') {
//base64
res[item] = transformJsonToSchema(unbase64(data.response.content.text));
} else {
res[item] = transformJsonToSchema(data.response.content.text);
}
} else {
res[item] = data.request[reflect[item]];
}
}
return res;
}
if (!importDataModule || typeof importDataModule !== 'object') {
console.error('obj参数必需是一个对象');
return null;
}
importDataModule.har = {
name: 'HAR',
run: run,
desc: '使用chrome录制请求功能具体使用请查看文档'
};
}
module.exports = function() {
this.bindHook('import_data', postman);
};

View File

@@ -0,0 +1,4 @@
module.exports = {
server: false,
client: true
}

View File

@@ -0,0 +1,267 @@
import { message } from 'antd';
import URL from 'url';
import _ from 'underscore';
const GenerateSchema = require('generate-schema/src/schemas/json.js');
import { json_parse } from '../../common/utils.js';
function postman(importDataModule) {
var folders = [];
function parseUrl(url) {
return URL.parse(url);
}
function checkInterRepeat(interData) {
let obj = {};
let arr = [];
for (let item in interData) {
// console.log(interData[item].url + "-" + interData[item].method);
if (!obj[interData[item].url + '-' + interData[item].method + '-' + interData[item].method]) {
arr.push(interData[item]);
obj[
interData[item].url + '-' + interData[item].method + '-' + interData[item].method
] = true;
}
}
return arr;
}
function handleReq_query(query) {
let res = [];
if (query && query.length) {
for (let item in query) {
res.push({
name: query[item].key,
desc: query[item].description,
// example: query[item].value,
value: query[item].value,
required: query[item].enabled ? '1' : '0'
});
}
}
return res;
}
function handleReq_headers(headers) {
let res = [];
if (headers && headers.length) {
for (let item in headers) {
res.push({
name: headers[item].key,
desc: headers[item].description,
value: headers[item].value,
required: headers[item].enabled ? '1' : '0'
});
}
}
return res;
}
function handleReq_body_form(body_form) {
let res = [];
if (body_form && body_form.length) {
for (let item in body_form) {
res.push({
name: body_form[item].key,
// example: body_form[item].value,
value: body_form[item].value,
type: body_form[item].type,
required: body_form[item].enabled ? '1' : '0',
desc: body_form[item].description
});
}
}
return res;
}
function handlePath(path) {
path = parseUrl(path).pathname;
path = decodeURIComponent(path);
if (!path) return '';
path = path.replace(/\{\{.*\}\}/g, '');
if (path[0] != '/') {
path = '/' + path;
}
return path;
}
function run(res) {
try {
res = JSON.parse(res);
let interData = res.requests;
let interfaceData = { apis: [], cats: [] };
interData = checkInterRepeat.bind(this)(interData);
if (res.folders && Array.isArray(res.folders)) {
res.folders.forEach(tag => {
interfaceData.cats.push({
name: tag.name,
desc: tag.description
});
});
}
if (_.find(res.folders, item => item.collectionId === res.id)) {
folders = res.folders;
}
if (interData && interData.length) {
for (let item in interData) {
let data = importPostman.bind(this)(interData[item]);
interfaceData.apis.push(data);
}
}
return interfaceData;
} catch (e) {
message.error('文件格式必须为JSON');
}
}
function importPostman(data, key) {
let reflect = {
//数据字段映射关系
title: 'name',
path: 'url',
method: 'method',
desc: 'description',
req_query: 'queryParams',
req_headers: 'headerData',
req_params: '',
req_body_type: 'dataMode',
req_body_form: 'data',
req_body_other: 'rawModeData',
res_body: 'text',
res_body_type: 'language'
};
let allKey = [
'title',
'path',
'catname',
'method',
'desc',
'req_query',
'req_headers',
'req_body_type',
'req_body_form',
'req_body_other',
'res'
];
key = key || allKey;
let res = {};
try {
for (let item in key) {
item = key[item];
if (item === 'req_query') {
res[item] = handleReq_query.bind(this)(data[reflect[item]]);
} else if (item === 'req_headers') {
res[item] = handleReq_headers.bind(this)(data[reflect[item]]);
} else if (item === 'req_body_form') {
res[item] = handleReq_body_form.bind(this)(data[reflect[item]]);
} else if (item === 'req_body_type') {
if (data[reflect[item]] === 'urlencoded' || data[reflect[item]] === 'params') {
res[item] = 'form';
} else {
if (_.isString(data.headers) && data.headers.indexOf('application/json') > -1) {
res[item] = 'json';
} else {
res[item] = 'raw';
}
}
} else if (item === 'req_body_other') {
if (_.isString(data.headers) && data.headers.indexOf('application/json') > -1) {
res.req_body_is_json_schema = true;
res[item] = transformJsonToSchema(data[reflect[item]]);
} else {
res[item] = data[reflect[item]];
}
} else if (item === 'path') {
res[item] = handlePath.bind(this)(data[reflect[item]]);
if (res[item] && res[item].indexOf('/:') > -1) {
let params = res[item].substr(res[item].indexOf('/:') + 2).split('/:');
// res[item] = res[item].substr(0,res[item].indexOf("/:"));
let arr = [];
for (let i in params) {
arr.push({
name: params[i],
desc: ''
});
}
res['req_params'] = arr;
}
} else if (item === 'title') {
let path = handlePath.bind(this)(data[reflect['path']]);
if (data[reflect[item]].indexOf(path) > -1) {
res[item] = path;
if (res[item] && res[item].indexOf('/:') > -1) {
res[item] = res[item].substr(0, res[item].indexOf('/:'));
}
} else {
res[item] = data[reflect[item]];
}
} else if (item === 'catname') {
let found = folders.filter(item => {
return item.id === data.folder;
});
res[item] = found && Array.isArray(found) && found.length > 0 ? found[0].name : null;
} else if (item === 'res') {
let response = handleResponses(data['responses']);
if (response) {
(res['res_body'] = response['res_body']),
(res['res_body_type'] = response['res_body_type']);
}
} else {
res[item] = data[reflect[item]];
}
}
} catch (err) {
console.log(err.message);
message.error(`${err.message}, 导入的postman格式有误`);
}
return res;
}
const handleResponses = data => {
if (data && data.length) {
let res = data[0];
let response = {};
response['res_body_type'] = res.language === 'json' ? 'json' : 'raw';
// response['res_body'] = res.language === 'json' ? transformJsonToSchema(res.text): res.text;
if (res.language === 'json') {
response['res_body_is_json_schema'] = true;
response['res_body'] = transformJsonToSchema(res.text);
} else {
response['res_body'] = res.text;
}
return response;
}
return null;
};
const transformJsonToSchema = json => {
json = json || {};
let jsonData = json_parse(json);
jsonData = GenerateSchema(jsonData);
let schemaData = JSON.stringify(jsonData);
return schemaData;
};
if (!importDataModule || typeof importDataModule !== 'object') {
console.error('obj参数必需是一个对象');
return null;
}
importDataModule.postman = {
name: 'Postman',
run: run,
desc: '注意只支持json格式数据'
};
}
module.exports = function() {
this.bindHook('import_data', postman);
};

View File

@@ -0,0 +1,4 @@
module.exports = {
server: false,
client: true
}

View File

@@ -0,0 +1,27 @@
import { message } from 'antd';
import run from './run';
module.exports = function() {
this.bindHook('import_data', function(importDataModule) {
if (!importDataModule || typeof importDataModule !== 'object') {
console.error('importDataModule 参数Must be Object Type');
return null;
}
importDataModule.swagger = {
name: 'Swagger',
run: async function(res) {
try {
return await run(res);
} catch (err) {
console.error(err);
message.error('解析失败');
}
},
desc: `<p>Swagger数据导入 支持 v2.0+ </p>
<p>
<a target="_blank" href="https://hellosean1025.github.io/yapi/documents/data.html#通过命令行导入接口数据">通过命令行导入接口数据</a>
</p>
`
};
});
};

View File

@@ -0,0 +1,4 @@
module.exports = {
server: true,
client: true
}

View File

@@ -0,0 +1,326 @@
const _ = require('underscore')
const swagger = require('swagger-client');
const compareVersions = require('compare-versions');
var SwaggerData, isOAS3;
function handlePath(path) {
if (path === '/') return path;
if (path.charAt(0) != '/') {
path = '/' + path;
}
if (path.charAt(path.length - 1) === '/') {
path = path.substr(0, path.length - 1);
}
return path;
}
function openapi2swagger(data) {
data.swagger = '2.0';
_.each(data.paths, apis => {
_.each(apis, api => {
_.each(api.responses, res => {
if (
res.content &&
res.content['application/json'] &&
typeof res.content['application/json'] === 'object'
) {
Object.assign(res, res.content['application/json']);
delete res.content;
}
if (
res.content &&
res.content['application/hal+json'] &&
typeof res.content['application/hal+json'] === 'object'
) {
Object.assign(res, res.content['application/hal+json']);
delete res.content;
}
if (
res.content &&
res.content['*/*'] &&
typeof res.content['*/*'] === 'object'
) {
Object.assign(res, res.content['*/*']);
delete res.content;
}
});
if (api.requestBody) {
if (!api.parameters) api.parameters = [];
let body = {
type: 'object',
name: 'body',
in: 'body'
};
try {
body.schema = api.requestBody.content['application/json'].schema;
} catch (e) {
body.schema = {};
}
api.parameters.push(body);
}
});
});
return data;
}
async function handleSwaggerData(res) {
return await new Promise(resolve => {
let data = swagger({
spec: res
});
data.then(res => {
resolve(res.spec);
});
});
}
async function run(res) {
let interfaceData = { apis: [], cats: [] };
if(typeof res === 'string' && res){
try{
res = JSON.parse(res);
} catch (e) {
console.error('json 解析出错',e.message)
}
}
isOAS3 = res.openapi && compareVersions(res.openapi,'3.0.0') >= 0;
if (isOAS3) {
res = openapi2swagger(res);
}
res = await handleSwaggerData(res);
SwaggerData = res;
interfaceData.basePath = res.basePath || '';
if (res.tags && Array.isArray(res.tags)) {
res.tags.forEach(tag => {
interfaceData.cats.push({
name: tag.name,
desc: tag.description
});
});
}else{
res.tags = []
}
_.each(res.paths, (apis, path) => {
// parameters is common parameters, not a method
delete apis.parameters;
_.each(apis, (api, method) => {
api.path = path;
api.method = method;
let data = null;
try {
data = handleSwagger(api, res.tags);
if (data.catname) {
if (!_.find(interfaceData.cats, item => item.name === data.catname)) {
if(res.tags.length === 0){
interfaceData.cats.push({
name: data.catname,
desc: data.catname
});
}
}
}
} catch (err) {
data = null;
}
if (data) {
interfaceData.apis.push(data);
}
});
});
interfaceData.cats = interfaceData.cats.filter(catData=>{
let catName = catData.name;
return _.find(interfaceData.apis, apiData=>{
return apiData.catname === catName
})
})
return interfaceData;
}
function handleSwagger(data, originTags= []) {
let api = {};
//处理基本信息
api.method = data.method.toUpperCase();
api.title = data.summary || data.path;
api.desc = data.description;
api.catname = null;
if(data.tags && Array.isArray(data.tags)){
api.tag = data.tags;
for(let i=0; i< data.tags.length; i++){
if(/v[0-9\.]+/.test(data.tags[i])){
continue;
}
// 如果根路径有 tags使用根路径 tags,不使用每个接口定义的 tag 做完分类
if(originTags.length > 0 && _.find(originTags, item=>{
return item.name === data.tags[i]
})){
api.catname = data.tags[i];
break;
}
if(originTags.length === 0){
api.catname = data.tags[i];
break;
}
}
}
api.path = handlePath(data.path);
api.req_params = [];
api.req_body_form = [];
api.req_headers = [];
api.req_query = [];
api.req_body_type = 'raw';
api.res_body_type = 'raw';
if (data.produces && data.produces.indexOf('application/json') > -1) {
api.res_body_type = 'json';
api.res_body_is_json_schema = true;
}
if (data.consumes && Array.isArray(data.consumes)) {
if (
data.consumes.indexOf('application/x-www-form-urlencoded') > -1 ||
data.consumes.indexOf('multipart/form-data') > -1
) {
api.req_body_type = 'form';
} else if (data.consumes.indexOf('application/json') > -1) {
api.req_body_type = 'json';
api.req_body_is_json_schema = true;
}
}
//处理response
api.res_body = handleResponse(data.responses);
try {
JSON.parse(api.res_body);
api.res_body_type = 'json';
api.res_body_is_json_schema = true;
} catch (e) {
api.res_body_type = 'raw';
}
//处理参数
function simpleJsonPathParse(key, json) {
if (!key || typeof key !== 'string' || key.indexOf('#/') !== 0 || key.length <= 2) {
return null;
}
let keys = key.substr(2).split('/');
keys = keys.filter(item => {
return item;
});
for (let i = 0, l = keys.length; i < l; i++) {
try {
json = json[keys[i]];
} catch (e) {
json = '';
break;
}
}
return json;
}
if (data.parameters && Array.isArray(data.parameters)) {
data.parameters.forEach(param => {
if (param && typeof param === 'object' && param.$ref) {
param = simpleJsonPathParse(param.$ref, { parameters: SwaggerData.parameters });
}
let defaultParam = {
name: param.name,
desc: param.description,
required: param.required ? '1' : '0'
};
if (param.in) {
switch (param.in) {
case 'path':
api.req_params.push(defaultParam);
break;
case 'query':
api.req_query.push(defaultParam);
break;
case 'body':
handleBodyPamras(param.schema, api);
break;
case 'formData':
defaultParam.type = param.type === 'file' ? 'file' : 'text';
if (param.example) {
defaultParam.example = param.example;
}
api.req_body_form.push(defaultParam);
break;
case 'header':
api.req_headers.push(defaultParam);
break;
}
} else {
api.req_query.push(defaultParam);
}
});
}
return api;
}
function isJson(json) {
try {
return JSON.parse(json);
} catch (e) {
return false;
}
}
function handleBodyPamras(data, api) {
api.req_body_other = JSON.stringify(data, null, 2);
if (isJson(api.req_body_other)) {
api.req_body_type = 'json';
api.req_body_is_json_schema = true;
}
}
function handleResponse(api) {
let res_body = '';
if (!api || typeof api !== 'object') {
return res_body;
}
let codes = Object.keys(api);
let curCode;
if (codes.length > 0) {
if (codes.indexOf('200') > -1) {
curCode = '200';
} else curCode = codes[0];
let res = api[curCode];
if (res && typeof res === 'object') {
if (res.schema) {
res_body = JSON.stringify(res.schema, null, 2);
} else if (res.description) {
res_body = res.description;
}
} else if (typeof res === 'string') {
res_body = res;
} else {
res_body = '';
}
} else {
res_body = '';
}
return res_body;
}
module.exports = run;

View File

@@ -0,0 +1,12 @@
module.exports = function(){
this.bindHook('import_data', function(importDataModule){
importDataModule.swagger = async (res)=>{
try{
return await require('./run.js')(res)
}catch(err){
this.commons.log(err, 'error')
return false;
}
}
})
}

View File

@@ -0,0 +1,39 @@
import { message } from 'antd';
function importData(importDataModule) {
async function run(res) {
try {
let interfaceData = { apis: [], cats: [] };
res = JSON.parse(res);
res.forEach(item => {
interfaceData.cats.push({
name: item.name,
desc: item.desc
});
item.list.forEach(api => {
api.catname = item.name;
});
interfaceData.apis = interfaceData.apis.concat(item.list);
});
return interfaceData;
} catch (e) {
console.error(e);
message.error('数据格式有误');
}
}
if (!importDataModule || typeof importDataModule !== 'object') {
console.error('importDataModule 参数Must be Object Type');
return null;
}
importDataModule.json = {
name: 'json',
run: run,
desc: 'YApi接口 json数据导入'
};
}
module.exports = function() {
this.bindHook('import_data', importData);
};

View File

@@ -0,0 +1,4 @@
module.exports = {
server: false,
client: true
}

View File

@@ -0,0 +1,23 @@
/**
* Created by gxl.gao on 2017/10/24.
*/
import StatisticsPage from './statisticsClientPage/index'
module.exports = function () {
this.bindHook('header_menu', function (menu) {
menu.statisticsPage = {
path: '/statistic',
name: '系统信息',
icon: 'bar-chart',
adminFlag: true
}
})
this.bindHook('app_route', function (app) {
app.statisticsPage = {
path: '/statistic',
component: StatisticsPage
}
})
}

View File

@@ -0,0 +1,176 @@
/**
* Created by gxl.gao on 2017/10/24.
*/
const baseController = require('controllers/base.js');
const statisMockModel = require('./statisMockModel.js');
const groupModel = require('models/group.js');
const projectModel = require('models/project.js');
const interfaceModel = require('models/interface.js');
const interfaceCaseModel = require('models/interfaceCase.js');
const yapi = require('yapi.js');
const config = require('./index.js');
const commons = require('./util.js');
const os = require('os');
let cpu = require('cpu-load');
class statisMockController extends baseController {
constructor(ctx) {
super(ctx);
this.Model = yapi.getInst(statisMockModel);
this.groupModel = yapi.getInst(groupModel);
this.projectModel = yapi.getInst(projectModel);
this.interfaceModel = yapi.getInst(interfaceModel);
this.interfaceCaseModel = yapi.getInst(interfaceCaseModel);
}
/**
* 获取所有统计总数
* @interface statismock/count
* @method get
* @category statistics
* @foldnumber 10
* @returns {Object}
*/
async getStatisCount(ctx) {
try {
let groupCount = await this.groupModel.getGroupListCount();
let projectCount = await this.projectModel.getProjectListCount();
let interfaceCount = await this.interfaceModel.getInterfaceListCount();
let interfaceCaseCount = await this.interfaceCaseModel.getInterfaceCaseListCount();
return (ctx.body = yapi.commons.resReturn({
groupCount,
projectCount,
interfaceCount,
interfaceCaseCount
}));
} catch (err) {
ctx.body = yapi.commons.resReturn(null, 400, err.message);
}
}
/**
* 获取所有mock接口数据信息
* @interface statismock/get
* @method get
* @category statistics
* @foldnumber 10
* @returns {Object}
*/
async getMockDateList(ctx) {
try {
let mockCount = await this.Model.getTotalCount();
let mockDateList = [];
if (!this.getRole() === 'admin') {
return (ctx.body = yapi.commons.resReturn(null, 405, '没有权限'));
}
// 默认时间是30 天为一周期
let dateInterval = commons.getDateRange();
mockDateList = await this.Model.getDayCount(dateInterval);
return (ctx.body = yapi.commons.resReturn({ mockCount, mockDateList }));
} catch (err) {
ctx.body = yapi.commons.resReturn(null, 400, err.message);
}
}
/**
* 获取邮箱状态信息
* @interface statismock/getSystemStatus
* @method get
* @category statistics
* @foldnumber 10
* @returns {Object}
*/
async getSystemStatus(ctx) {
try {
let mail = '';
if (yapi.WEBCONFIG.mail && yapi.WEBCONFIG.mail.enable) {
mail = await this.checkEmail();
// return ctx.body = yapi.commons.resReturn(result);
} else {
mail = '未配置';
}
let load = (await this.cupLoad()) * 100;
let systemName = os.platform();
let totalmem = commons.transformBytesToGB(os.totalmem());
let freemem = commons.transformBytesToGB(os.freemem());
let uptime = commons.transformSecondsToDay(os.uptime());
let data = {
mail,
systemName,
totalmem,
freemem,
uptime,
load: load.toFixed(2)
};
return (ctx.body = yapi.commons.resReturn(data));
} catch (err) {
ctx.body = yapi.commons.resReturn(null, 400, err.message);
}
}
checkEmail() {
return new Promise((resolve, reject) => {
let result = {};
yapi.mail.verify(error => {
if (error) {
result = '不可用';
resolve(result);
} else {
result = '可用';
resolve(result);
}
});
});
}
async groupDataStatis(ctx) {
try {
let groupData = await this.groupModel.list();
let result = [];
for (let i = 0; i < groupData.length; i++) {
let group = groupData[i];
let groupId = group._id;
const data = {
name: group.group_name,
interface: 0,
mock: 0,
project: 0
};
result.push(data);
let projectCount = await this.projectModel.listCount(groupId);
let projectData = await this.projectModel.list(groupId);
let interfaceCount = 0;
for (let j = 0; j < projectData.length; j++) {
let project = projectData[j];
interfaceCount += await this.interfaceModel.listCount({
project_id: project._id
});
}
let mockCount = await this.Model.countByGroupId(groupId);
data.interface = interfaceCount;
data.project = projectCount;
data.mock = mockCount;
}
return (ctx.body = yapi.commons.resReturn(result));
} catch (err) {
ctx.body = yapi.commons.resReturn(null, 400, err.message);
}
}
cupLoad() {
return new Promise((resolve, reject) => {
cpu(1000, function(load) {
resolve(load);
});
});
}
}
module.exports = statisMockController;

View File

@@ -0,0 +1,10 @@
/**
* Created by gxl.gao on 2017/10/24.
*/
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
]
}

View File

@@ -0,0 +1,82 @@
/**
* Created by gxl.gao on 2017/10/24.
*/
const yapi = require('yapi.js');
const mongoose = require('mongoose');
const controller = require('./controller');
const statisModel = require('./statisMockModel.js');
const commons = require('./util.js');
module.exports = function() {
yapi.connect.then(function() {
let Col = mongoose.connection.db.collection('statis_mock');
Col.createIndex({
interface_id: 1
});
Col.createIndex({
project_id: 1
});
Col.createIndex({
group_id: 1
});
Col.createIndex({
time: 1
});
Col.createIndex({
date: 1
});
});
this.bindHook('add_router', function(addRouter) {
addRouter({
controller: controller,
method: 'get',
path: 'statismock/count',
action: 'getStatisCount'
});
addRouter({
controller: controller,
method: 'get',
path: 'statismock/get',
action: 'getMockDateList'
});
addRouter({
controller: controller,
method: 'get',
path: 'statismock/get_system_status',
action: 'getSystemStatus'
});
addRouter({
controller: controller,
method: 'get',
path: 'statismock/group_data_statis',
action: 'groupDataStatis'
});
});
// MockServer生成mock数据后触发
this.bindHook('mock_after', function(context) {
let interfaceId = context.interfaceData._id;
let projectId = context.projectData._id;
let groupId = context.projectData.group_id;
//let ip = context.ctx.originalUrl;
let ip = yapi.commons.getIp(context.ctx);
let data = {
interface_id: interfaceId,
project_id: projectId,
group_id: groupId,
time: yapi.commons.time(),
ip: ip,
date: commons.formatYMD(new Date())
};
let inst = yapi.getInst(statisModel);
try {
inst.save(data).then();
} catch (e) {
yapi.commons.log('mockStatisError', e);
}
});
};

View File

@@ -0,0 +1,75 @@
/**
* Created by gxl.gao on 2017/10/24.
*/
const yapi = require('yapi.js');
const baseModel = require('models/base.js');
class statisMockModel extends baseModel {
getName() {
return 'statis_mock';
}
getSchema() {
return {
interface_id: { type: Number, required: true },
project_id: { type: Number, required: true },
group_id: { type: Number, required: true },
time: Number, //'时间戳'
ip: String,
date: String
};
}
countByGroupId(id){
return this.model.countDocuments({
group_id: id
})
}
save(data) {
let m = new this.model(data);
return m.save();
}
getTotalCount() {
return this.model.countDocuments({});
}
async getDayCount(timeInterval) {
let end = timeInterval[1];
let start = timeInterval[0];
let data = [];
const cursor = this.model.aggregate([
{
$match: {
date: { $gt: start, $lte: end }
}
},
{
$group: {
_id: '$date', //$region is the column name in collection
count: { $sum: 1 }
}
},
{
$sort: { _id: 1 }
}
]).cursor({}).exec();
await cursor.eachAsync(doc => data.push(doc));
return data;
}
list() {
return this.model.find({}).select('date').exec();
}
up(id, data) {
data.up_time = yapi.commons.time();
return this.model.updateOne({
_id: id
}, data, { runValidators: true });
}
}
module.exports = statisMockModel;

View File

@@ -0,0 +1,77 @@
/**
* Created by gxl.gao on 2017/10/25.
*/
import React, { Component } from 'react';
// import PropTypes from 'prop-types'
import axios from 'axios';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend } from 'recharts';
import { Spin } from 'antd';
class StatisChart extends Component {
static propTypes = {};
constructor(props) {
super(props);
this.state = {
showLoading: true,
chartDate: {
mockCount: 0,
mockDateList: []
}
};
}
UNSAFE_componentWillMount() {
this.getMockData();
}
// 获取mock 请求次数信息
async getMockData() {
let result = await axios.get('/api/plugin/statismock/get');
if (result.data.errcode === 0) {
let mockStatisData = result.data.data;
this.setState({
showLoading: false,
chartDate: { ...mockStatisData }
});
}
}
render() {
const width = 1050;
const { mockCount, mockDateList } = this.state.chartDate;
return (
<div>
<Spin spinning={this.state.showLoading}>
<div className="statis-chart-content">
<h3 className="statis-title">mock 接口访问总数为{mockCount.toLocaleString()}</h3>
<div className="statis-chart">
<LineChart
width={width}
height={300}
data={mockDateList}
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
>
<XAxis dataKey="_id" />
<YAxis />
<CartesianGrid strokeDasharray="7 3" />
<Tooltip />
<Legend />
<Line
name="mock统计值"
type="monotone"
dataKey="count"
stroke="#8884d8"
activeDot={{ r: 8 }}
/>
</LineChart>
</div>
<div className="statis-footer">过去3个月mock接口调用情况</div>
</div>
</Spin>
</div>
);
}
}
export default StatisChart;

View File

@@ -0,0 +1,47 @@
import React from 'react';
import { Table } from 'antd';
import PropTypes from 'prop-types';
const columns = [
{
title: 'Group',
dataIndex: 'name',
key: 'name'
},
{
title: '项目',
dataIndex: 'project',
key: 'project'
},
{
title: '接口',
dataIndex: 'interface',
key: 'interface'
},
{
title: 'mock数据',
dataIndex: 'mock',
key: 'mock'
}
];
const StatisTable = props => {
const { dataSource } = props;
return (
<div className="m-row-table">
<h3 className="statis-title">分组数据详情</h3>
<Table
className="statis-table"
pagination={false}
dataSource={dataSource}
columns={columns}
/>
</div>
);
};
StatisTable.propTypes = {
dataSource: PropTypes.array
};
export default StatisTable;

View File

@@ -0,0 +1,210 @@
/**
* Created by gxl.gao on 2017/10/25.
*/
import React, { Component } from 'react';
import { connect } from 'react-redux';
import axios from 'axios';
import PropTypes from 'prop-types';
import './index.scss';
// import { withRouter } from 'react-router-dom';
import { Row, Col, Tooltip, Icon } from 'antd';
import { setBreadcrumb } from 'client/reducer/modules/user';
import StatisChart from './StatisChart';
import StatisTable from './StatisTable';
const CountOverview = props => (
<Row type="flex" justify="space-start" className="m-row">
<Col className="gutter-row" span={6}>
<span>
分组总数
<Tooltip placement="rightTop" title="统计yapi中一共开启了多少可见的公共分组">
<Icon className="m-help" type="question-circle" />
</Tooltip>
</span>
<h2 className="gutter-box">{props.date.groupCount}</h2>
</Col>
<Col className="gutter-row" span={6}>
<span>
项目总数
<Tooltip placement="rightTop" title="统计yapi中建立的所有项目总数">
<Icon className="m-help" type="question-circle" />
</Tooltip>
</span>
<h2 className="gutter-box">{props.date.projectCount}</h2>
</Col>
<Col className="gutter-row" span={6}>
<span>
接口总数
<Tooltip placement="rightTop" title="统计yapi所有项目中的所有接口总数">
{/*<a href="javascript:void(0)" className="m-a-help">?</a>*/}
<Icon className="m-help" type="question-circle" />
</Tooltip>
</span>
<h2 className="gutter-box">{props.date.interfaceCount}</h2>
</Col>
<Col className="gutter-row" span={6}>
<span>
测试接口总数
<Tooltip placement="rightTop" title="统计yapi所有项目中的所有测试接口总数">
{/*<a href="javascript:void(0)" className="m-a-help">?</a>*/}
<Icon className="m-help" type="question-circle" />
</Tooltip>
</span>
<h2 className="gutter-box">{props.date.interfaceCaseCount}</h2>
</Col>
</Row>
);
CountOverview.propTypes = {
date: PropTypes.object
};
const StatusOverview = props => (
<Row type="flex" justify="space-start" className="m-row">
<Col className="gutter-row" span={6}>
<span>
操作系统类型
<Tooltip
placement="rightTop"
title="操作系统类型,返回值有'darwin', 'freebsd', 'linux', 'sunos' , 'win32'"
>
<Icon className="m-help" type="question-circle" />
</Tooltip>
</span>
<h2 className="gutter-box">{props.data.systemName}</h2>
</Col>
<Col className="gutter-row" span={6}>
<span>
cpu负载
<Tooltip placement="rightTop" title="cpu的总负载情况">
<Icon className="m-help" type="question-circle" />
</Tooltip>
</span>
<h2 className="gutter-box">{props.data.load} %</h2>
</Col>
<Col className="gutter-row" span={6}>
<span>
系统空闲内存总量 / 内存总量
<Tooltip placement="rightTop" title="系统空闲内存总量 / 内存总量">
<Icon className="m-help" type="question-circle" />
</Tooltip>
</span>
<h2 className="gutter-box">
{props.data.freemem} G / {props.data.totalmem} G{' '}
</h2>
</Col>
<Col className="gutter-row" span={6}>
<span>
邮箱状态
<Tooltip placement="rightTop" title="检测配置文件中配置邮箱的状态">
<Icon className="m-help" type="question-circle" />
</Tooltip>
</span>
<h2 className="gutter-box">{props.data.mail}</h2>
</Col>
</Row>
);
StatusOverview.propTypes = {
data: PropTypes.object
};
@connect(
null,
{
setBreadcrumb
}
)
class statisticsPage extends Component {
static propTypes = {
setBreadcrumb: PropTypes.func
};
constructor(props) {
super(props);
this.state = {
count: {
groupCount: 0,
projectCount: 0,
interfaceCount: 0,
interfactCaseCount: 0
},
status: {
mail: '',
systemName: '',
totalmem: '',
freemem: '',
uptime: ''
},
dataTotal: []
};
}
async UNSAFE_componentWillMount() {
this.props.setBreadcrumb([{ name: '系统信息' }]);
this.getStatisData();
this.getSystemStatusData();
this.getGroupData();
}
// 获取统计数据
async getStatisData() {
let result = await axios.get('/api/plugin/statismock/count');
if (result.data.errcode === 0) {
let statisData = result.data.data;
this.setState({
count: { ...statisData }
});
}
}
// 获取系统信息
async getSystemStatusData() {
let result = await axios.get('/api/plugin/statismock/get_system_status');
if (result.data.errcode === 0) {
let statusData = result.data.data;
this.setState({
status: { ...statusData }
});
}
}
// 获取分组详细信息
async getGroupData() {
let result = await axios.get('/api/plugin/statismock/group_data_statis');
if (result.data.errcode === 0) {
let statusData = result.data.data;
statusData.map(item => {
return (item['key'] = item.name);
});
this.setState({
dataTotal: statusData
});
}
}
render() {
const { count, status, dataTotal } = this.state;
return (
<div className="g-statistic">
<div className="content">
<h2 className="title">系统状况</h2>
<div className="system-content">
<StatusOverview data={status} />
</div>
<h2 className="title">数据统计</h2>
<div>
<CountOverview date={count} />
<StatisTable dataSource={dataTotal} />
<StatisChart />
</div>
</div>
</div>
);
}
}
export default statisticsPage;

View File

@@ -0,0 +1,83 @@
@import '../../../client/styles/mixin';
.g-statistic {
@include row-width-limit;
margin: 0 auto .24rem;
margin-top: 24px;
min-width: 11.2rem;
.content {
-webkit-box-flex: 1;
padding: 24px;
width:100%;
background: #fff;
min-height: 5rem;
// overflow-x: scroll;
}
.m-row {
border-bottom: 1px solid #f0f0f0;
padding: 16px 0;
}
.m-row-table {
padding-top: 16px
}
.statis-table {
margin-left: 16px;
}
.m-help {
margin-left: 5px;
border-radius: 12px;
color: #2395f1;
}
.gutter-row {
padding-left: 24px;
border-left: 1px solid #f0f0f0;
}
.gutter-row:first-child {
border-left: 0
}
.gutter-box {
margin-top: 8px;
//margin-bottom: 16px;
//margin: 8px 0 16px;
}
.statis-chart-content {
margin-top: 8px;
}
.statis-title{
padding: 8px 8px 24px;
}
.statis-chart{
margin:0 auto;
text-align: center;
}
.statis-footer{
margin:16px 0;
text-align: center;
width: 1050px;
}
.title{
font-size: 16px;
font-weight: 400;
margin-bottom: 0.16rem;
border-left: 3px solid #2395f1;
padding-left: 8px;
}
.system-content{
margin-bottom: 16px;
}
}

View File

@@ -0,0 +1,56 @@
const fs = require('fs-extra');
const yapi = require('../../server/yapi.js');
const commons = require('../../server/utils/commons');
const dbModule = require('../../server/utils/db.js');
const userModel = require('../../server/models/user.js');
const mongoose = require('mongoose');
yapi.commons = commons;
yapi.connect = dbModule.connect();
const convert2Decimal = num => (num > 9 ? num : `0${num}`);
const formatYMD = (val, joinStr = '-') => {
let date = val;
if (typeof val !== 'object') {
val = val * 1000;
date = new Date(val);
}
return `${[
date.getFullYear(),
convert2Decimal(date.getMonth() + 1),
convert2Decimal(date.getDate())
].join(joinStr)}`;
};
function run() {
let time = yapi.commons.time() - 10000000;
let data = i => {
time = time - yapi.commons.rand(10000, 1000000);
return {
interface_id: 94,
project_id: 25,
group_id: 19,
time: time,
ip: '1.1.1.1',
date: formatYMD(time)
};
};
yapi.connect
.then(function() {
let logCol = mongoose.connection.db.collection('statis_mock');
let arr = [];
for (let i = 0; i < 11; i++) {
if (arr.length >= 5) {
logCol.insert(arr);
arr = [];
}
arr.push(data(i));
}
})
.catch(function(err) {
throw new Error(err.message);
});
}
run();

View File

@@ -0,0 +1,150 @@
/**
* 获取所需要的日期区间点
* @param time {Number} Number是ele日期区间选择组件返回的结果
* Number是之前时刻距离今天的间隔天数默认是90天
* @param start {String} 日期对象,日期区间的开始点 '2017-01-17 00:00:00'
* @param withToday {Boolean} 是否包含今天
* @return {Array} ['2017-01-17 00:00:00', '2017-01-20 23:59:59']
*/
exports.getDateRange = (time = 90, start = false, withToday = true) => {
const gapTime = time * 24 * 3600 * 1000;
if (!start) {
// 没有规定start时间
let endTime = getNowMidnightDate().getTime();
if (!withToday) {
endTime -= 86400000;
}
return [this.formatYMD(endTime - gapTime), this.formatYMD(endTime - 1000)];
}
const startTime = dateSpacialWithSafari(start);
const endTime = startTime + (gapTime - 1000);
return [start, this.formatYMD(endTime)];
}
// 时间
const convert2Decimal = num => (num > 9 ? num : `0${num}`)
/**
* 获取距今天之前多少天的所有时间
* @param time {Number} Number是ele日期区间选择组件返回的结果
* Number是之前时刻距离今天的间隔天数默认是30天
* @return {Array} ['2017-01-17', '2017-01-28', '2017-10-29',...]
*/
exports.getDateInterval = (time = 30) => {
// const gapTime = time * 24 * 3600 * 1000;
// 今天
let endTime = new Date().getTime();
let timeList = []
for (let i = 0; i < time; i++) {
const gapTime = i * 24 * 3600 * 1000;
const time = this.formatYMD(endTime - gapTime);
timeList.push(time);
}
return timeList;
}
/**获取2017-10-27 00:00:00 和 2017-10-27 23:59:59的时间戳
* @param date {String} "2017-10-27"
* @return {Array} [ 1509033600000, 1509119999000 ]
*/
exports.getTimeInterval = (date) => {
const startTime = (getNowMidnightDate(date).getTime()-86400000)/1000;
const endTime =(getNowMidnightDate(date).getTime()-1000)/1000;
return [startTime, endTime];
}
/**
* 获取当前时间午夜0点的日期对象
*/
const getNowMidnightDate = (time) => {
let date;
if (time) {
date = new Date(time);
} else {
date = new Date();
}
return new Date(date.getFullYear(), date.getMonth(), date.getDate() + 1);
}
/**
* 格式化 年、月、日、时、分、秒
* @param val {Object or String or Number} 日期对象 或是可new Date的对象或时间戳
* @return {String} 2017-01-20 20:00:00
*/
const 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(':')}`;
}
/**
* 格式化年、月、日
* @param val {Object or String or Number} 日期对象 或是可new Date的对象或时间戳
* @return {String} 2017-01-20
*/
exports.formatYMD = (val, joinStr = '-') => {
let date = val;
if (typeof val !== 'object') {
date = new Date(val);
}
return `${[
date.getFullYear(),
convert2Decimal(date.getMonth() + 1),
convert2Decimal(date.getDate())
].join(joinStr)}`;
}
/**
* 获取所需的时间差值,
* tipnew Date('2017-01-17 00:00:00')在safari下不可用需进行替换
* @param Array ['2017-01-17 00:00:00', '2017-01-20 23:59:59']
* @return {Number} 3
*/
exports.getDayGapFromRange = dateRange => {
const startTime = dateSpacialWithSafari(dateRange[0]);
const endTime = dateSpacialWithSafari(dateRange[1]);
return Math.ceil((endTime - startTime) / 86400000);
}
/**
* dateSpacialWithSafari 格式话safari下通用的格式
* @param str {String} 2017-04-19T11:01:19.074+0800 or 2017-10-10 10:10:10
* @return {number} date.getTime()
*/
const dateSpacialWithSafari = str => {
if (str.indexOf('T') > -1) {
let date;
str.replace(/(\d{4})-(\d{2})-(\d{2})\w(\d{2}):(\d{2}):(\d{2})/, (match, p1, p2, p3, p4, p5, p6) => {
date = new Date(p1, +p2 - 1, p3, p4, p5, p6);
return;
})
return date.getTime();
}
return new Date(str.replace(/-/g, '/')).getTime();
}
/**
* 将内存单位从字节(b)变成GB
*/
exports.transformBytesToGB = bytes => {
return (bytes/1024/1024/1024).toFixed(2)
}
exports.transformSecondsToDay = seconds => {
return (seconds/3600/24).toFixed(2)
}

View File

@@ -0,0 +1,12 @@
import swaggerAutoSync from './swaggerAutoSync/swaggerAutoSync.js'
function hander(routers) {
routers.test = {
name: 'Swagger自动同步',
component: swaggerAutoSync
};
}
module.exports = function() {
this.bindHook('sub_setting_nav', hander);
};

View File

@@ -0,0 +1,62 @@
const baseController = require('controllers/base.js');
const yapi = require('yapi.js');
const syncModel = require('../syncModel.js');
const projectModel = require('models/project.js');
const interfaceSyncUtils = require('../interfaceSyncUtils.js')
class syncController extends baseController {
constructor(ctx) {
super(ctx);
this.syncModel = yapi.getInst(syncModel);
this.projectModel = yapi.getInst(projectModel);
this.interfaceSyncUtils = yapi.getInst(interfaceSyncUtils);
}
/**
* 保存定时任务
* @param {*} ctx
*/
async upSync(ctx) {
let requestBody = ctx.request.body;
let projectId = requestBody.project_id;
if (!projectId) {
return (ctx.body = yapi.commons.resReturn(null, 408, '缺少项目Id'));
}
if ((await this.checkAuth(projectId, 'project', 'edit')) !== true) {
return (ctx.body = yapi.commons.resReturn(null, 405, '没有权限'));
}
let result;
if (requestBody.id) {
result = await this.syncModel.up(requestBody);
} else {
result = await this.syncModel.save(requestBody);
}
//操作定时任务
if (requestBody.is_sync_open) {
this.interfaceSyncUtils.addSyncJob(projectId, requestBody.sync_cron, requestBody.sync_json_url, requestBody.sync_mode, requestBody.uid);
} else {
this.interfaceSyncUtils.deleteSyncJob(projectId);
}
return (ctx.body = yapi.commons.resReturn(result));
}
/**
* 查询定时任务
* @param {*} ctx
*/
async getSync(ctx) {
let projectId = ctx.query.project_id;
if (!projectId) {
return (ctx.body = yapi.commons.resReturn(null, 408, '缺少项目Id'));
}
let result = await this.syncModel.getByProjectId(projectId);
return (ctx.body = yapi.commons.resReturn(result));
}
}
module.exports = syncController;

View File

@@ -0,0 +1,4 @@
module.exports = {
server: true,
client: true
}

View File

@@ -0,0 +1,221 @@
const schedule = require('node-schedule');
const openController = require('controllers/open.js');
const projectModel = require('models/project.js');
const syncModel = require('./syncModel.js');
const tokenModel = require('models/token.js');
const yapi = require('yapi.js')
const sha = require('sha.js');
const md5 = require('md5');
const { getToken } = require('utils/token');
const jobMap = new Map();
class syncUtils {
constructor(ctx) {
yapi.commons.log("-------------------------------------swaggerSyncUtils constructor-----------------------------------------------");
this.ctx = ctx;
this.openController = yapi.getInst(openController);
this.syncModel = yapi.getInst(syncModel);
this.tokenModel = yapi.getInst(tokenModel)
this.projectModel = yapi.getInst(projectModel);
this.init()
}
//初始化定时任务
async init() {
let allSyncJob = await this.syncModel.listAll();
for (let i = 0, len = allSyncJob.length; i < len; i++) {
let syncItem = allSyncJob[i];
if (syncItem.is_sync_open) {
this.addSyncJob(syncItem.project_id, syncItem.sync_cron, syncItem.sync_json_url, syncItem.sync_mode, syncItem.uid);
}
}
}
/**
* 新增同步任务.
* @param {*} projectId 项目id
* @param {*} cronExpression cron表达式,针对定时任务
* @param {*} swaggerUrl 获取swagger的地址
* @param {*} syncMode 同步模式
* @param {*} uid 用户id
*/
async addSyncJob(projectId, cronExpression, swaggerUrl, syncMode, uid) {
if(!swaggerUrl)return;
let projectToken = await this.getProjectToken(projectId, uid);
//立即执行一次
this.syncInterface(projectId, swaggerUrl, syncMode, uid, projectToken);
let scheduleItem = schedule.scheduleJob(cronExpression, async () => {
this.syncInterface(projectId, swaggerUrl, syncMode, uid, projectToken);
});
//判断是否已经存在这个任务
let jobItem = jobMap.get(projectId);
if (jobItem) {
jobItem.cancel();
}
jobMap.set(projectId, scheduleItem);
}
//同步接口
async syncInterface(projectId, swaggerUrl, syncMode, uid, projectToken) {
yapi.commons.log('定时器触发, syncJsonUrl:' + swaggerUrl + ",合并模式:" + syncMode);
let oldPorjectData;
try {
oldPorjectData = await this.projectModel.get(projectId);
} catch(e) {
yapi.commons.log('获取项目:' + projectId + '失败');
this.deleteSyncJob(projectId);
//删除数据库定时任务
await this.syncModel.delByProjectId(projectId);
return;
}
//如果项目已经删除了
if (!oldPorjectData) {
yapi.commons.log('项目:' + projectId + '不存在');
this.deleteSyncJob(projectId);
//删除数据库定时任务
await this.syncModel.delByProjectId(projectId);
return;
}
let newSwaggerJsonData;
try {
newSwaggerJsonData = await this.getSwaggerContent(swaggerUrl)
if (!newSwaggerJsonData || typeof newSwaggerJsonData !== 'object') {
yapi.commons.log('数据格式出错,请检查')
this.saveSyncLog(0, syncMode, "数据格式出错,请检查", uid, projectId);
}
newSwaggerJsonData = JSON.stringify(newSwaggerJsonData)
} catch (e) {
this.saveSyncLog(0, syncMode, "获取数据失败,请检查", uid, projectId);
yapi.commons.log('获取数据失败' + e.message)
}
let oldSyncJob = await this.syncModel.getByProjectId(projectId);
//更新之前判断本次swagger json数据是否跟上次的相同,相同则不更新
if (newSwaggerJsonData && oldSyncJob.old_swagger_content && oldSyncJob.old_swagger_content == md5(newSwaggerJsonData)) {
//记录日志
// this.saveSyncLog(0, syncMode, "接口无更新", uid, projectId);
oldSyncJob.last_sync_time = yapi.commons.time();
await this.syncModel.upById(oldSyncJob._id, oldSyncJob);
return;
}
let _params = {
type: 'swagger',
json: newSwaggerJsonData,
project_id: projectId,
merge: syncMode,
token: projectToken
}
let requestObj = {
params: _params
};
await this.openController.importData(requestObj);
//同步成功就更新同步表的数据
if (requestObj.body.errcode == 0) {
//修改sync_model的属性
oldSyncJob.last_sync_time = yapi.commons.time();
oldSyncJob.old_swagger_content = md5(newSwaggerJsonData);
await this.syncModel.upById(oldSyncJob._id, oldSyncJob);
}
//记录日志
this.saveSyncLog(requestObj.body.errcode, syncMode, requestObj.body.errmsg, uid, projectId);
}
getSyncJob(projectId) {
return jobMap.get(projectId);
}
deleteSyncJob(projectId) {
let jobItem = jobMap.get(projectId);
if (jobItem) {
jobItem.cancel();
}
}
/**
* 记录同步日志
* @param {*} errcode
* @param {*} syncMode
* @param {*} moremsg
* @param {*} uid
* @param {*} projectId
*/
saveSyncLog(errcode, syncMode, moremsg, uid, projectId) {
yapi.commons.saveLog({
content: '自动同步接口状态:' + (errcode == 0 ? '成功,' : '失败,') + "合并模式:" + this.getSyncModeName(syncMode) + ",更多信息:" + moremsg,
type: 'project',
uid: uid,
username: "自动同步用户",
typeid: projectId
});
}
/**
* 获取项目token,因为导入接口需要鉴权.
* @param {*} project_id 项目id
* @param {*} uid 用户id
*/
async getProjectToken(project_id, uid) {
try {
let data = await this.tokenModel.get(project_id);
let token;
if (!data) {
let passsalt = yapi.commons.randStr();
token = sha('sha1')
.update(passsalt)
.digest('hex')
.substr(0, 20);
await this.tokenModel.save({ project_id, token });
} else {
token = data.token;
}
token = getToken(token, uid);
return token;
} catch (err) {
return "";
}
}
getUid(uid) {
return parseInt(uid, 10);
}
/**
* 转换合并模式的值为中文.
* @param {*} syncMode 合并模式
*/
getSyncModeName(syncMode) {
if (syncMode == 'good') {
return '智能合并';
} else if (syncMode == 'normal') {
return '普通模式';
} else if (syncMode == 'merge') {
return '完全覆盖';
}
return '';
}
async getSwaggerContent(swaggerUrl) {
const axios = require('axios')
try {
let response = await axios.get(swaggerUrl);
if (response.status > 400) {
throw new Error(`http status "${response.status}"` + '获取数据失败,请确认 swaggerUrl 是否正确')
}
return response.data;
} catch (e) {
let response = e.response || {status: e.message || 'error'};
throw new Error(`http status "${response.status}"` + '获取数据失败,请确认 swaggerUrl 是否正确')
}
}
}
module.exports = syncUtils;

View File

@@ -0,0 +1,23 @@
const controller = require('./controller/syncController.js');
const yapi =require('yapi.js');
const interfaceSyncUtils = require('./interfaceSyncUtils.js');
module.exports = function () {
yapi.getInst(interfaceSyncUtils);
this.bindHook('add_router', function (addRouter) {
addRouter({
controller: controller,
method: 'get',
path: 'autoSync/get',
action: 'getSync'
});
addRouter({
controller: controller,
method: 'post',
path: 'autoSync/save',
action: 'upSync'
});
});
};

View File

@@ -0,0 +1,239 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { formatTime } from 'client/common.js';
import { Form, Switch, Button, Icon, Tooltip, message, Input, Select } from 'antd';
import {handleSwaggerUrlData} from 'client/reducer/modules/project';
const FormItem = Form.Item;
const Option = Select.Option;
import axios from 'axios';
// layout
const formItemLayout = {
labelCol: {
lg: { span: 5 },
xs: { span: 24 },
sm: { span: 10 }
},
wrapperCol: {
lg: { span: 16 },
xs: { span: 24 },
sm: { span: 12 }
},
className: 'form-item'
};
const tailFormItemLayout = {
wrapperCol: {
sm: {
span: 16,
offset: 11
}
}
};
@connect(
state => {
return {
projectMsg: state.project.currProject
};
},
{
handleSwaggerUrlData
}
)
@Form.create()
export default class ProjectInterfaceSync extends Component {
static propTypes = {
form: PropTypes.object,
match: PropTypes.object,
projectId: PropTypes.number,
projectMsg: PropTypes.object,
handleSwaggerUrlData: PropTypes.func
};
constructor(props) {
super(props);
this.state = {
sync_data: { is_sync_open: false }
};
}
handleSubmit = async () => {
const { form, projectId } = this.props;
let params = {
project_id: projectId,
is_sync_open: this.state.sync_data.is_sync_open,
uid: this.props.projectMsg.uid
};
if (this.state.sync_data._id) {
params.id = this.state.sync_data._id;
}
form.validateFields(async (err, values) => {
if (!err) {
let assignValue = Object.assign(params, values);
await axios.post('/api/plugin/autoSync/save', assignValue).then(res => {
if (res.data.errcode === 0) {
message.success('保存成功');
} else {
message.error(res.data.errmsg);
}
});
}
});
};
validSwaggerUrl = async (rule, value, callback) => {
if(!value)return;
try{
await this.props.handleSwaggerUrlData(value);
} catch(e) {
callback('swagger地址不正确');
}
callback()
}
UNSAFE_componentWillMount() {
//查询同步任务
this.setState({
sync_data: {}
});
//默认每份钟同步一次,取一个随机数
this.setState({
random_corn: '*/2 * * * *'
});
this.getSyncData();
}
async getSyncData() {
let projectId = this.props.projectMsg._id;
let result = await axios.get('/api/plugin/autoSync/get?project_id=' + projectId);
if (result.data.errcode === 0) {
if (result.data.data) {
this.setState({
sync_data: result.data.data
});
}
}
}
// 是否开启
onChange = v => {
let sync_data = this.state.sync_data;
sync_data.is_sync_open = v;
this.setState({
sync_data: sync_data
});
};
sync_cronCheck(rule, value, callback){
if(!value)return;
value = value.trim();
if(value.split(/ +/).length > 5){
callback('不支持秒级别的设置,建议使用 "*/10 * * * *" ,每隔10分钟更新')
}
callback()
}
render() {
const { getFieldDecorator } = this.props.form;
return (
<div className="m-panel">
<Form>
<FormItem
label="是否开启自动同步"
{...formItemLayout}
>
<Switch
checked={this.state.sync_data.is_sync_open}
onChange={this.onChange}
checkedChildren="开"
unCheckedChildren="关"
/>
{this.state.sync_data.last_sync_time != null ? (<div>上次更新时间:<span className="logtime">{formatTime(this.state.sync_data.last_sync_time)}</span></div>) : null}
</FormItem>
<div>
<FormItem {...formItemLayout} label={
<span className="label">
数据同步&nbsp;
<Tooltip
title={
<div>
<h3 style={{ color: 'white' }}>普通模式</h3>
<p>不导入已存在的接口</p>
<br />
<h3 style={{ color: 'white' }}>智能合并</h3>
<p>
已存在的接口将合并返回数据的 response适用于导入了 swagger
数据保留对数据结构的改动
</p>
<br />
<h3 style={{ color: 'white' }}>完全覆盖</h3>
<p>不保留旧数据完全使用新数据适用于接口定义完全交给后端定义</p>
</div>
}
>
<Icon type="question-circle-o" />
</Tooltip>{' '}
</span>
}>
{getFieldDecorator('sync_mode', {
initialValue: this.state.sync_data.sync_mode,
rules: [
{
required: true,
message: '请选择同步方式!'
}
]
})(
<Select>
<Option value="normal">普通模式</Option>
<Option value="good">智能合并</Option>
<Option value="merge">完全覆盖</Option>
</Select>
)}
</FormItem>
<FormItem {...formItemLayout} label="项目的swagger json地址">
{getFieldDecorator('sync_json_url', {
rules: [
{
required: true,
message: '输入swagger地址'
},
{
validator: this.validSwaggerUrl
}
],
validateTrigger: 'onBlur',
initialValue: this.state.sync_data.sync_json_url
})(<Input />)}
</FormItem>
<FormItem {...formItemLayout} label={<span>类cron风格表达式(默认10分钟更新一次)&nbsp;<a href="https://blog.csdn.net/shouldnotappearcalm/article/details/89469047">参考</a></span>}>
{getFieldDecorator('sync_cron', {
rules: [
{
required: true,
message: '输入node-schedule的类cron表达式!'
},
{
validator: this.sync_cronCheck
}
],
initialValue: this.state.sync_data.sync_cron ? this.state.sync_data.sync_cron : this.state.random_corn
})(<Input />)}
</FormItem>
</div>
<FormItem {...tailFormItemLayout}>
<Button type="primary" htmlType="submit" icon="save" size="large" onClick={this.handleSubmit}>
保存
</Button>
</FormItem>
</Form>
</div>
);
}
}

View File

@@ -0,0 +1,90 @@
const yapi = require('yapi.js');
const baseModel = require('models/base.js');
const mongoose = require('mongoose');
class syncModel extends baseModel {
getName() {
return 'interface_auto_sync';
}
getSchema() {
return {
uid: { type: Number},
project_id: { type: Number, required: true },
//是否开启自动同步
is_sync_open: { type: Boolean, default: false },
//自动同步定时任务的cron表达式
sync_cron: String,
//自动同步获取json的url
sync_json_url: String,
//接口合并模式 good,nomarl等等 意思也就是智能合并,完全覆盖等
sync_mode: String,
//上次成功同步接口时间,
last_sync_time: Number,
//上次同步的swagger 文档内容
old_swagger_content: String,
add_time: Number,
up_time: Number,
};
}
getByProjectId(id) {
return this.model.findOne({
project_id: id
})
}
delByProjectId(project_id){
return this.model.remove({
project_id: project_id
})
}
save(data) {
data.up_time = yapi.commons.time();
let m = new this.model(data);
return m.save();
}
listAll() {
return this.model
.find({})
.select(
'_id uid project_id add_time up_time is_sync_open sync_cron sync_json_url sync_mode old_swagger_content last_sync_time'
)
.sort({ _id: -1 })
.exec();
}
up(data) {
let id = data.id;
delete data.id;
data.up_time = yapi.commons.time();
return this.model.update({
_id: id
}, data)
}
upById(id, data) {
delete data.id;
data.up_time = yapi.commons.time();
return this.model.update({
_id: id
}, data)
}
del(id){
return this.model.remove({
_id: id
})
}
delByProjectId(projectId){
return this.model.remove({
project_id: projectId
})
}
}
module.exports = syncModel;

View File

@@ -0,0 +1,10 @@
function hander(routers) {
routers.test = {
name: 'test',
component: ()=> 'hello world.'
};
}
module.exports = function() {
this.bindHook('sub_setting_nav', hander);
};

View File

@@ -0,0 +1,4 @@
module.exports = {
server: false,
client: true
}

View 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
};
});
};

View File

@@ -0,0 +1,234 @@
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 = `${ctx.request.origin}/project/${params.project_id}/wiki`;
if (notice) {
let diffView = showDiffMsg(jsondiffpatch, formattersHtml, logData);
let annotatedCss = fs.readFileSync(
path.resolve(
yapi.WEBROOT,
'node_modules/jsondiffpatch/dist/formatters-styles/annotated.css'
),
'utf8'
);
let htmlCss = fs.readFileSync(
path.resolve(yapi.WEBROOT, 'node_modules/jsondiffpatch/dist/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);
let data = await this.websocketMsgMap(message, result);
if (data) {
ctx.websocket.send(JSON.stringify(data));
}
});
ctx.websocket.on('close', async () => {});
} catch (err) {
yapi.commons.log(err, 'error');
}
}
websocketMsgMap(msg, result) {
const map = {
start: this.startFunc.bind(this),
end: this.endFunc.bind(this),
editor: this.editorFunc.bind(this)
};
return map[msg](result);
}
// socket 开始链接
async startFunc(result) {
if (result && result.edit_uid === this.getUid()) {
await this.Model.upEditUid(result._id, 0);
}
}
// socket 结束链接
async endFunc(result) {
if (result) {
await this.Model.upEditUid(result._id, 0);
}
}
// 正在编辑
async editorFunc(result) {
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
};
}
return data;
}
}
module.exports = wikiController;

View 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
]
};

View 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'
});
});
};

View File

@@ -0,0 +1,25 @@
// 时间
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;

View File

@@ -0,0 +1,56 @@
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;

View File

@@ -0,0 +1,67 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Button, Checkbox } from 'antd';
import Editor from 'common/tui-editor/dist/tui-editor-Editor-all.min.js';
require('common/tui-editor/dist/tui-editor.min.css'); // editor ui
require('common/tui-editor/dist/tui-editor-contents.min.css'); // editor content
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;

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

View 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 '../../../common/utils';
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:' ? 'wss' : '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;

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