fork from bc4552c5a8
This commit is contained in:
156
exts/yapi-plugin-advanced-mock/AdvMock.js
Normal file
156
exts/yapi-plugin-advanced-mock/AdvMock.js
Normal 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>
|
||||
是否开启 <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));
|
||||
482
exts/yapi-plugin-advanced-mock/MockCol/CaseDesModal.js
Normal file
482
exts/yapi-plugin-advanced-mock/MockCol/CaseDesModal.js
Normal 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;
|
||||
26
exts/yapi-plugin-advanced-mock/MockCol/CaseDesModal.scss
Normal file
26
exts/yapi-plugin-advanced-mock/MockCol/CaseDesModal.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
269
exts/yapi-plugin-advanced-mock/MockCol/MockCol.js
Normal file
269
exts/yapi-plugin-advanced-mock/MockCol/MockCol.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
36
exts/yapi-plugin-advanced-mock/MockCol/mockColReducer.js
Normal file
36
exts/yapi-plugin-advanced-mock/MockCol/mockColReducer.js
Normal 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
|
||||
}
|
||||
}
|
||||
61
exts/yapi-plugin-advanced-mock/advMockModel.js
Normal file
61
exts/yapi-plugin-advanced-mock/advMockModel.js
Normal 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;
|
||||
76
exts/yapi-plugin-advanced-mock/caseModel.js
Normal file
76
exts/yapi-plugin-advanced-mock/caseModel.js
Normal 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;
|
||||
14
exts/yapi-plugin-advanced-mock/client.js
Normal file
14
exts/yapi-plugin-advanced-mock/client.js
Normal 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;
|
||||
})
|
||||
}
|
||||
186
exts/yapi-plugin-advanced-mock/controller.js
Normal file
186
exts/yapi-plugin-advanced-mock/controller.js
Normal 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;
|
||||
7
exts/yapi-plugin-advanced-mock/index.js
Normal file
7
exts/yapi-plugin-advanced-mock/index.js
Normal 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
|
||||
]
|
||||
}
|
||||
212
exts/yapi-plugin-advanced-mock/server.js
Normal file
212
exts/yapi-plugin-advanced-mock/server.js
Normal 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);
|
||||
});
|
||||
};
|
||||
Binary file not shown.
Binary file not shown.
28
exts/yapi-plugin-export-data/client.js
Normal file
28
exts/yapi-plugin-export-data/client.js
Normal 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);
|
||||
};
|
||||
189
exts/yapi-plugin-export-data/controller.js
Normal file
189
exts/yapi-plugin-export-data/controller.js
Normal 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;
|
||||
351
exts/yapi-plugin-export-data/defaultTheme.css
Normal file
351
exts/yapi-plugin-export-data/defaultTheme.css
Normal 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 */
|
||||
7
exts/yapi-plugin-export-data/defaultTheme.css.map
Normal file
7
exts/yapi-plugin-export-data/defaultTheme.css.map
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"mappings": ";AAAA;;;;;;;;;UASW;EACP,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;EACV,WAAW,EAAE,MAAM;EACnB,sBAAsB,EAAE,WAAW;;;AAEvC,cAAc;AACd,mBAAoB;EAChB,KAAK,EAAE,GAAG;;;AAEd,UAAU;AACV,yBAA0B;EACtB,kBAAkB,EAAE,8BAA8B;EAClD,UAAU,EAAE,kBAAkB;;;AAElC,WAAW;AACX,yBAA0B;EACtB,aAAa,EAAE,GAAG;EAClB,UAAU,EAAE,kBAAkB;EAC9B,kBAAkB,EAAE,4BAA4B;;;AAEpD,yCAA0C;EACtC,UAAU,EAAE,kBAAkB;;;AAGlC,IAAK;EACD,WAAW,EAAE,4JAA4J;EACzK,SAAS,EAAE,IAAI;EACf,WAAW,EAAE,IAAI;EACjB,KAAK,EAAE,OAAO;EACd,QAAQ,EAAE,QAAQ;;;AAItB,KAAM;EACF,MAAM,EAAE,aAAa;EACrB,eAAe,EAAE,QAAQ;;;AAG7B;EACG;EACC,MAAM,EAAE,cAAc;EACtB,OAAO,EAAE,QAAQ;;;AAGrB,EAAG;EACC,OAAO,EAAE,QAAQ;;;AAGrB,oBAAqB;EACjB,KAAK,EAAE,OAAO;EACd,eAAe,EAAE,IAAI;;;AAGzB,gBAAiB;EACb,KAAK,EAAE,OAAO;EACd,eAAe,EAAE,IAAI;;;AAGzB,KAAM;EACF,MAAM,EAAE,IAAI;;;AAGhB,CAAE;EACE,YAAY,EAAE,IAAI;EAClB,aAAa,EAAE,GAAG;;;AAGtB;;;;;EAKG;EACC,KAAK,EAAE,OAAO;EACd,WAAW,EAAE,IAAI;;;AAGrB,EAAG;EACC,KAAK,EAAE,OAAO;EACd,WAAW,EAAE,GAAG;EAEhB,aAAa,EAAE,IAAI;EACnB,SAAS,EAAE,IAAI;EACf,cAAc,EAAE,IAAI;EACpB,aAAa,EAAE,cAAc;EAC7B,WAAW,EAAE,IAAI;;;AAGrB,EAAG;EACC,SAAS,EAAE,IAAI;EACf,WAAW,EAAE,IAAI;EACjB,cAAc,EAAE,IAAI;;;AAGxB,EAAG;EACC,KAAK,EAAE,IAAI;EACX,WAAW,EAAE,GAAG;EAChB,UAAU,EAAE,IAAI;EAChB,aAAa,EAAE,IAAI;EACnB,WAAW,EAAE,iBAAiB;EAC9B,YAAY,EAAE,GAAG;EACjB,SAAS,EAAE,IAAI;;;AAGnB,EAAG;EACC,SAAS,EAAE,IAAI;;;AAGnB,EAAG;EACC,SAAS,EAAE,IAAI;;;AAGnB,EAAG;EACC,SAAS,EAAE,IAAI;;;AAGnB,EAAG;EACC,MAAM,EAAE,QAAQ;EAChB,MAAM,EAAE,CAAC;EACT,aAAa,EAAE,cAAc;;;AAGjC,UAAW;EACP,OAAO,EAAE,mBAAmB;EAC5B,aAAa,EAAE,IAAI;EACnB,WAAW,EAAE,cAAc;EAC3B,UAAU,EAAE,MAAM;;;AAGtB,iBAAkB;EACd,SAAS,EAAE,IAAI;EACf,WAAW,EAAE,KAAK;EAClB,WAAW,EAAE,cAAc;EAC3B,KAAK,EAAE,IAAI;;;AAGf,YAAa;EACT,SAAS,EAAE,IAAI;EACf,WAAW,EAAE,GAAG;EAChB,WAAW,EAAE,IAAI;EACjB,aAAa,EAAE,CAAC;EAChB,UAAU,EAAE,MAAM;;;AAGtB;GACI;EACA,WAAW,EAAE,2CAA2C;;;AAG5D,IAAK;EACD,gBAAgB,EAAE,OAAO;EACzB,KAAK,EAAE,mBAAmB;EAC1B,OAAO,EAAE,OAAO;EAChB,SAAS,EAAE,IAAI;EACf,qBAAqB,EAAE,GAAG;EAC1B,kBAAkB,EAAE,GAAG;EACvB,aAAa,EAAE,GAAG;;;AAGtB,GAAI;EACA,OAAO,EAAE,KAAK;EACd,OAAO,EAAE,IAAI;EACb,MAAM,EAAE,QAAQ;EAChB,WAAW,EAAE,IAAI;EACjB,SAAS,EAAE,IAAI;EACf,MAAM,EAAE,iBAAiB;EACzB,WAAW,EAAE,QAAQ;EACrB,SAAS,EAAE,UAAU;EACrB,UAAU,EAAE,OAAO;;;AAGvB,QAAS;EACL,gBAAgB,EAAE,OAAO;EACzB,KAAK,EAAE,OAAO;EACd,SAAS,EAAE,IAAI;EACf,OAAO,EAAE,CAAC;;;AAGd,GAAI;EACA,SAAS,EAAE,MAAM;EACjB,cAAc,EAAE,KAAK;EACrB,WAAW,EAAE,CAAC;;;AAGlB,CAAE;EACE,0BAA0B,EAAE,KAAK;;;AAGrC,YAAa;EACT;;;;;;;;IAQG;IACC,KAAK,EAAE,KAAK;;;EAEhB;KACI;IACA,iBAAiB,EAAE,KAAK;;;AAIhC;IACK;EACD,MAAM,EAAE,IAAI;;;AAGhB,kBAAmB;EACf,QAAQ,EAAE,KAAK;EACf,GAAG,EAAE,IAAI;EACT,IAAI,EAAE,CAAC;EACP,MAAM,EAAE,CAAC;EACT,UAAU,EAAE,MAAM;EAClB,UAAU,EAAE,IAAI;EAChB,KAAK,EAAE,KAAK;;;AAGhB,gCAA2B;EACzB,SAAS,EAAE,IAAI;EACf,aAAa,EAAE,IAAI;EACnB,UAAU,EAAE,IAAI;;;AAGlB,qBAAsB;EAIlB,QAAQ,EAAE,IAAI;EACd,MAAM,EAAE,GAAG;EACX,MAAM,EAAE,IAAI;EACZ,OAAO,EAAE,OAAO;EAChB,UAAU,EAAE,UAAU;EACtB,eAAe,EAAE,IAAI;;;AAGzB,wBAAyB;EACrB,YAAY,EAAE,IAAI;;;AAGtB,oBAAqB;EACjB,OAAO,EAAE,OAAO;EAChB,OAAO,EAAE,KAAK;EACd,eAAe,EAAE,IAAI;;;AAKzB,cAAe;EAGX,SAAS,EAAE,KAAK;EAChB,WAAW,EAAE,KAAK;EAClB,YAAY,EAAE,IAAI;EAClB,SAAS,EAAE,CAAC;;AACZ,wBAAS;EACP,WAAW,EAAE,IAAI;;;AAMvB,QAAO;EACH,WAAW,EAAE,IAAI;;;AAGrB,YAAW;EACP,WAAW,EAAE,IAAI;;;AAGrB,UAAS;EACL,WAAW,EAAE,IAAI;;;AAGrB,WAAY;EACR,QAAQ,EAAE,KAAK;EACf,GAAG,EAAE,IAAI;EACT,SAAS,EAAE,IAAI;EACf,KAAK,EAAE,KAAK;EACZ,WAAW,EAAE,MAAM;EACnB,KAAK,EAAE,KAAK;EACZ,OAAO,EAAE,GAAG;EACZ,WAAW,EAAE,IAAI;EACjB,UAAU,EAAE,UAAU;;;AAG1B,MAAO;EACH,UAAU,EAAE,IAAI;EAChB,WAAW,EAAE,IAAI;EACjB,OAAO,EAAE,IAAI;;;AAGjB,gBAAgB;EACd,SAAS,EAAE,IAAI;;;AAGjB,SAAU;EACN,UAAU,EAAE,OAAO;EACnB,MAAM,EAAE,IAAI;EACZ,WAAW,EAAE,IAAI;EACjB,YAAY,EAAE,IAAI;EAClB,OAAO,EAAE,IAAI;EACb,WAAW,EAAE,MAAM;EACnB,QAAQ,EAAE,KAAK;EACf,OAAO,EAAE,CAAC;EACV,GAAG,EAAE,CAAC;EACN,IAAI,EAAE,CAAC;EACP,KAAK,EAAE,CAAC;;AACR,gBAAO;EACH,SAAS,EAAE,IAAI;EACf,KAAK,EAAE,IAAI;EACX,WAAW,EAAE,MAAM;EACnB,sBAAsB,EAAE,WAAW;EACnC,MAAM,EAAE,CAAC;EACT,WAAW,EAAE,IAAI;EACjB,OAAO,EAAE,CAAC;EACV,WAAW,EAAE,IAAI;EACjB,MAAM,EAAE,IAAI;;AAEhB,cAAK;EACD,KAAK,EAAE,IAAI;EACX,SAAS,EAAE,IAAI;EACf,QAAQ,EAAE,QAAQ;EAClB,KAAK,EAAE,IAAI;EACX,GAAG,EAAE,CAAC;;AACN,gBAAE;EACE,KAAK,EAAE,IAAI;EACX,WAAW,EAAE,IAAI;EACjB,OAAO,EAAE,GAAG;EACZ,UAAU,EAAE,SAAS;;AAEzB,sBAAQ;EACJ,KAAK,EAAE,OAAO;;;AAK1B,SAAU;EACN,UAAU,EAAE,cAAc;EAC1B,WAAW,EAAE,IAAI;EACjB,cAAc,EAAE,IAAI",
|
||||
"sources": ["defaultTheme.scss"],
|
||||
"names": [],
|
||||
"file": "defaultTheme.css"
|
||||
}
|
||||
4
exts/yapi-plugin-export-data/defaultTheme.js
Normal file
4
exts/yapi-plugin-export-data/defaultTheme.js
Normal 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>';
|
||||
355
exts/yapi-plugin-export-data/defaultTheme.scss
Normal file
355
exts/yapi-plugin-export-data/defaultTheme.scss
Normal 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;
|
||||
}
|
||||
4
exts/yapi-plugin-export-data/index.js
Normal file
4
exts/yapi-plugin-export-data/index.js
Normal file
@@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
server: true,
|
||||
client: true
|
||||
}
|
||||
16
exts/yapi-plugin-export-data/server.js
Normal file
16
exts/yapi-plugin-export-data/server.js
Normal 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'
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
11
exts/yapi-plugin-export-swagger2-data/client.js
Normal file
11
exts/yapi-plugin-export-swagger2-data/client.js
Normal 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);
|
||||
};
|
||||
288
exts/yapi-plugin-export-swagger2-data/controller.js
Normal file
288
exts/yapi-plugin-export-swagger2-data/controller.js
Normal 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;
|
||||
4
exts/yapi-plugin-export-swagger2-data/index.js
Normal file
4
exts/yapi-plugin-export-swagger2-data/index.js
Normal file
@@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
server: true,
|
||||
client: true
|
||||
}
|
||||
12
exts/yapi-plugin-export-swagger2-data/server.js
Normal file
12
exts/yapi-plugin-export-swagger2-data/server.js
Normal 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'
|
||||
})
|
||||
})
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
76
exts/yapi-plugin-gen-services/Services/Services.js
Normal file
76
exts/yapi-plugin-gen-services/Services/Services.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
6
exts/yapi-plugin-gen-services/Services/Services.scss
Normal file
6
exts/yapi-plugin-gen-services/Services/Services.scss
Normal file
@@ -0,0 +1,6 @@
|
||||
.project-services {
|
||||
margin: 0;
|
||||
pre {
|
||||
background: #efefef;
|
||||
}
|
||||
}
|
||||
12
exts/yapi-plugin-gen-services/client.js
Normal file
12
exts/yapi-plugin-gen-services/client.js
Normal 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);
|
||||
};
|
||||
209
exts/yapi-plugin-gen-services/controller.js
Normal file
209
exts/yapi-plugin-gen-services/controller.js
Normal 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;
|
||||
351
exts/yapi-plugin-gen-services/defaultTheme.css
Normal file
351
exts/yapi-plugin-gen-services/defaultTheme.css
Normal 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 */
|
||||
7
exts/yapi-plugin-gen-services/defaultTheme.css.map
Normal file
7
exts/yapi-plugin-gen-services/defaultTheme.css.map
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"mappings": ";AAAA;;;;;;;;;UASW;EACP,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;EACV,WAAW,EAAE,MAAM;EACnB,sBAAsB,EAAE,WAAW;;;AAEvC,cAAc;AACd,mBAAoB;EAChB,KAAK,EAAE,GAAG;;;AAEd,UAAU;AACV,yBAA0B;EACtB,kBAAkB,EAAE,8BAA8B;EAClD,UAAU,EAAE,kBAAkB;;;AAElC,WAAW;AACX,yBAA0B;EACtB,aAAa,EAAE,GAAG;EAClB,UAAU,EAAE,kBAAkB;EAC9B,kBAAkB,EAAE,4BAA4B;;;AAEpD,yCAA0C;EACtC,UAAU,EAAE,kBAAkB;;;AAGlC,IAAK;EACD,WAAW,EAAE,4JAA4J;EACzK,SAAS,EAAE,IAAI;EACf,WAAW,EAAE,IAAI;EACjB,KAAK,EAAE,OAAO;EACd,QAAQ,EAAE,QAAQ;;;AAItB,KAAM;EACF,MAAM,EAAE,aAAa;EACrB,eAAe,EAAE,QAAQ;;;AAG7B;EACG;EACC,MAAM,EAAE,cAAc;EACtB,OAAO,EAAE,QAAQ;;;AAGrB,EAAG;EACC,OAAO,EAAE,QAAQ;;;AAGrB,oBAAqB;EACjB,KAAK,EAAE,OAAO;EACd,eAAe,EAAE,IAAI;;;AAGzB,gBAAiB;EACb,KAAK,EAAE,OAAO;EACd,eAAe,EAAE,IAAI;;;AAGzB,KAAM;EACF,MAAM,EAAE,IAAI;;;AAGhB,CAAE;EACE,YAAY,EAAE,IAAI;EAClB,aAAa,EAAE,GAAG;;;AAGtB;;;;;EAKG;EACC,KAAK,EAAE,OAAO;EACd,WAAW,EAAE,IAAI;;;AAGrB,EAAG;EACC,KAAK,EAAE,OAAO;EACd,WAAW,EAAE,GAAG;EAEhB,aAAa,EAAE,IAAI;EACnB,SAAS,EAAE,IAAI;EACf,cAAc,EAAE,IAAI;EACpB,aAAa,EAAE,cAAc;EAC7B,WAAW,EAAE,IAAI;;;AAGrB,EAAG;EACC,SAAS,EAAE,IAAI;EACf,WAAW,EAAE,IAAI;EACjB,cAAc,EAAE,IAAI;;;AAGxB,EAAG;EACC,KAAK,EAAE,IAAI;EACX,WAAW,EAAE,GAAG;EAChB,UAAU,EAAE,IAAI;EAChB,aAAa,EAAE,IAAI;EACnB,WAAW,EAAE,iBAAiB;EAC9B,YAAY,EAAE,GAAG;EACjB,SAAS,EAAE,IAAI;;;AAGnB,EAAG;EACC,SAAS,EAAE,IAAI;;;AAGnB,EAAG;EACC,SAAS,EAAE,IAAI;;;AAGnB,EAAG;EACC,SAAS,EAAE,IAAI;;;AAGnB,EAAG;EACC,MAAM,EAAE,QAAQ;EAChB,MAAM,EAAE,CAAC;EACT,aAAa,EAAE,cAAc;;;AAGjC,UAAW;EACP,OAAO,EAAE,mBAAmB;EAC5B,aAAa,EAAE,IAAI;EACnB,WAAW,EAAE,cAAc;EAC3B,UAAU,EAAE,MAAM;;;AAGtB,iBAAkB;EACd,SAAS,EAAE,IAAI;EACf,WAAW,EAAE,KAAK;EAClB,WAAW,EAAE,cAAc;EAC3B,KAAK,EAAE,IAAI;;;AAGf,YAAa;EACT,SAAS,EAAE,IAAI;EACf,WAAW,EAAE,GAAG;EAChB,WAAW,EAAE,IAAI;EACjB,aAAa,EAAE,CAAC;EAChB,UAAU,EAAE,MAAM;;;AAGtB;GACI;EACA,WAAW,EAAE,2CAA2C;;;AAG5D,IAAK;EACD,gBAAgB,EAAE,OAAO;EACzB,KAAK,EAAE,mBAAmB;EAC1B,OAAO,EAAE,OAAO;EAChB,SAAS,EAAE,IAAI;EACf,qBAAqB,EAAE,GAAG;EAC1B,kBAAkB,EAAE,GAAG;EACvB,aAAa,EAAE,GAAG;;;AAGtB,GAAI;EACA,OAAO,EAAE,KAAK;EACd,OAAO,EAAE,IAAI;EACb,MAAM,EAAE,QAAQ;EAChB,WAAW,EAAE,IAAI;EACjB,SAAS,EAAE,IAAI;EACf,MAAM,EAAE,iBAAiB;EACzB,WAAW,EAAE,QAAQ;EACrB,SAAS,EAAE,UAAU;EACrB,UAAU,EAAE,OAAO;;;AAGvB,QAAS;EACL,gBAAgB,EAAE,OAAO;EACzB,KAAK,EAAE,OAAO;EACd,SAAS,EAAE,IAAI;EACf,OAAO,EAAE,CAAC;;;AAGd,GAAI;EACA,SAAS,EAAE,MAAM;EACjB,cAAc,EAAE,KAAK;EACrB,WAAW,EAAE,CAAC;;;AAGlB,CAAE;EACE,0BAA0B,EAAE,KAAK;;;AAGrC,YAAa;EACT;;;;;;;;IAQG;IACC,KAAK,EAAE,KAAK;;;EAEhB;KACI;IACA,iBAAiB,EAAE,KAAK;;;AAIhC;IACK;EACD,MAAM,EAAE,IAAI;;;AAGhB,kBAAmB;EACf,QAAQ,EAAE,KAAK;EACf,GAAG,EAAE,IAAI;EACT,IAAI,EAAE,CAAC;EACP,MAAM,EAAE,CAAC;EACT,UAAU,EAAE,MAAM;EAClB,UAAU,EAAE,IAAI;EAChB,KAAK,EAAE,KAAK;;;AAGhB,gCAA2B;EACzB,SAAS,EAAE,IAAI;EACf,aAAa,EAAE,IAAI;EACnB,UAAU,EAAE,IAAI;;;AAGlB,qBAAsB;EAIlB,QAAQ,EAAE,IAAI;EACd,MAAM,EAAE,GAAG;EACX,MAAM,EAAE,IAAI;EACZ,OAAO,EAAE,OAAO;EAChB,UAAU,EAAE,UAAU;EACtB,eAAe,EAAE,IAAI;;;AAGzB,wBAAyB;EACrB,YAAY,EAAE,IAAI;;;AAGtB,oBAAqB;EACjB,OAAO,EAAE,OAAO;EAChB,OAAO,EAAE,KAAK;EACd,eAAe,EAAE,IAAI;;;AAKzB,cAAe;EAGX,SAAS,EAAE,KAAK;EAChB,WAAW,EAAE,KAAK;EAClB,YAAY,EAAE,IAAI;EAClB,SAAS,EAAE,CAAC;;AACZ,wBAAS;EACP,WAAW,EAAE,IAAI;;;AAMvB,QAAO;EACH,WAAW,EAAE,IAAI;;;AAGrB,YAAW;EACP,WAAW,EAAE,IAAI;;;AAGrB,UAAS;EACL,WAAW,EAAE,IAAI;;;AAGrB,WAAY;EACR,QAAQ,EAAE,KAAK;EACf,GAAG,EAAE,IAAI;EACT,SAAS,EAAE,IAAI;EACf,KAAK,EAAE,KAAK;EACZ,WAAW,EAAE,MAAM;EACnB,KAAK,EAAE,KAAK;EACZ,OAAO,EAAE,GAAG;EACZ,WAAW,EAAE,IAAI;EACjB,UAAU,EAAE,UAAU;;;AAG1B,MAAO;EACH,UAAU,EAAE,IAAI;EAChB,WAAW,EAAE,IAAI;EACjB,OAAO,EAAE,IAAI;;;AAGjB,gBAAgB;EACd,SAAS,EAAE,IAAI;;;AAGjB,SAAU;EACN,UAAU,EAAE,OAAO;EACnB,MAAM,EAAE,IAAI;EACZ,WAAW,EAAE,IAAI;EACjB,YAAY,EAAE,IAAI;EAClB,OAAO,EAAE,IAAI;EACb,WAAW,EAAE,MAAM;EACnB,QAAQ,EAAE,KAAK;EACf,OAAO,EAAE,CAAC;EACV,GAAG,EAAE,CAAC;EACN,IAAI,EAAE,CAAC;EACP,KAAK,EAAE,CAAC;;AACR,gBAAO;EACH,SAAS,EAAE,IAAI;EACf,KAAK,EAAE,IAAI;EACX,WAAW,EAAE,MAAM;EACnB,sBAAsB,EAAE,WAAW;EACnC,MAAM,EAAE,CAAC;EACT,WAAW,EAAE,IAAI;EACjB,OAAO,EAAE,CAAC;EACV,WAAW,EAAE,IAAI;EACjB,MAAM,EAAE,IAAI;;AAEhB,cAAK;EACD,KAAK,EAAE,IAAI;EACX,SAAS,EAAE,IAAI;EACf,QAAQ,EAAE,QAAQ;EAClB,KAAK,EAAE,IAAI;EACX,GAAG,EAAE,CAAC;;AACN,gBAAE;EACE,KAAK,EAAE,IAAI;EACX,WAAW,EAAE,IAAI;EACjB,OAAO,EAAE,GAAG;EACZ,UAAU,EAAE,SAAS;;AAEzB,sBAAQ;EACJ,KAAK,EAAE,OAAO;;;AAK1B,SAAU;EACN,UAAU,EAAE,cAAc;EAC1B,WAAW,EAAE,IAAI;EACjB,cAAc,EAAE,IAAI",
|
||||
"sources": ["defaultTheme.scss"],
|
||||
"names": [],
|
||||
"file": "defaultTheme.css"
|
||||
}
|
||||
4
exts/yapi-plugin-gen-services/defaultTheme.js
Normal file
4
exts/yapi-plugin-gen-services/defaultTheme.js
Normal 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>';
|
||||
355
exts/yapi-plugin-gen-services/defaultTheme.scss
Normal file
355
exts/yapi-plugin-gen-services/defaultTheme.scss
Normal 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;
|
||||
}
|
||||
4
exts/yapi-plugin-gen-services/index.js
Normal file
4
exts/yapi-plugin-gen-services/index.js
Normal file
@@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
server: true,
|
||||
client: true
|
||||
}
|
||||
18
exts/yapi-plugin-gen-services/server.js
Normal file
18
exts/yapi-plugin-gen-services/server.js
Normal 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'
|
||||
});
|
||||
})
|
||||
|
||||
}
|
||||
229
exts/yapi-plugin-import-har/client.js
Normal file
229
exts/yapi-plugin-import-har/client.js
Normal 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);
|
||||
};
|
||||
4
exts/yapi-plugin-import-har/index.js
Normal file
4
exts/yapi-plugin-import-har/index.js
Normal file
@@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
server: false,
|
||||
client: true
|
||||
}
|
||||
267
exts/yapi-plugin-import-postman/client.js
Normal file
267
exts/yapi-plugin-import-postman/client.js
Normal 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);
|
||||
};
|
||||
4
exts/yapi-plugin-import-postman/index.js
Normal file
4
exts/yapi-plugin-import-postman/index.js
Normal file
@@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
server: false,
|
||||
client: true
|
||||
}
|
||||
27
exts/yapi-plugin-import-swagger/client.js
Normal file
27
exts/yapi-plugin-import-swagger/client.js
Normal 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>
|
||||
`
|
||||
};
|
||||
});
|
||||
};
|
||||
4
exts/yapi-plugin-import-swagger/index.js
Normal file
4
exts/yapi-plugin-import-swagger/index.js
Normal file
@@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
server: true,
|
||||
client: true
|
||||
}
|
||||
326
exts/yapi-plugin-import-swagger/run.js
Normal file
326
exts/yapi-plugin-import-swagger/run.js
Normal 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;
|
||||
12
exts/yapi-plugin-import-swagger/server.js
Normal file
12
exts/yapi-plugin-import-swagger/server.js
Normal 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;
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
39
exts/yapi-plugin-import-yapi-json/client.js
Normal file
39
exts/yapi-plugin-import-yapi-json/client.js
Normal 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);
|
||||
};
|
||||
4
exts/yapi-plugin-import-yapi-json/index.js
Normal file
4
exts/yapi-plugin-import-yapi-json/index.js
Normal file
@@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
server: false,
|
||||
client: true
|
||||
}
|
||||
23
exts/yapi-plugin-statistics/client.js
Normal file
23
exts/yapi-plugin-statistics/client.js
Normal 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
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
176
exts/yapi-plugin-statistics/controller.js
Normal file
176
exts/yapi-plugin-statistics/controller.js
Normal 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;
|
||||
10
exts/yapi-plugin-statistics/index.js
Normal file
10
exts/yapi-plugin-statistics/index.js
Normal 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
|
||||
]
|
||||
}
|
||||
82
exts/yapi-plugin-statistics/server.js
Normal file
82
exts/yapi-plugin-statistics/server.js
Normal 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);
|
||||
}
|
||||
});
|
||||
};
|
||||
75
exts/yapi-plugin-statistics/statisMockModel.js
Normal file
75
exts/yapi-plugin-statistics/statisMockModel.js
Normal 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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
210
exts/yapi-plugin-statistics/statisticsClientPage/index.js
Normal file
210
exts/yapi-plugin-statistics/statisticsClientPage/index.js
Normal 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;
|
||||
83
exts/yapi-plugin-statistics/statisticsClientPage/index.scss
Normal file
83
exts/yapi-plugin-statistics/statisticsClientPage/index.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
56
exts/yapi-plugin-statistics/test.js
Normal file
56
exts/yapi-plugin-statistics/test.js
Normal 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();
|
||||
150
exts/yapi-plugin-statistics/util.js
Normal file
150
exts/yapi-plugin-statistics/util.js
Normal 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)}`;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取所需的时间差值,
|
||||
* tip:new 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)
|
||||
}
|
||||
12
exts/yapi-plugin-swagger-auto-sync/client.js
Normal file
12
exts/yapi-plugin-swagger-auto-sync/client.js
Normal 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);
|
||||
};
|
||||
@@ -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;
|
||||
4
exts/yapi-plugin-swagger-auto-sync/index.js
Normal file
4
exts/yapi-plugin-swagger-auto-sync/index.js
Normal file
@@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
server: true,
|
||||
client: true
|
||||
}
|
||||
221
exts/yapi-plugin-swagger-auto-sync/interfaceSyncUtils.js
Normal file
221
exts/yapi-plugin-swagger-auto-sync/interfaceSyncUtils.js
Normal 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;
|
||||
23
exts/yapi-plugin-swagger-auto-sync/server.js
Normal file
23
exts/yapi-plugin-swagger-auto-sync/server.js
Normal 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'
|
||||
});
|
||||
});
|
||||
|
||||
};
|
||||
@@ -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">
|
||||
数据同步
|
||||
<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分钟更新一次) <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>
|
||||
);
|
||||
}
|
||||
}
|
||||
90
exts/yapi-plugin-swagger-auto-sync/syncModel.js
Normal file
90
exts/yapi-plugin-swagger-auto-sync/syncModel.js
Normal 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;
|
||||
10
exts/yapi-plugin-test/client.js
Normal file
10
exts/yapi-plugin-test/client.js
Normal file
@@ -0,0 +1,10 @@
|
||||
function hander(routers) {
|
||||
routers.test = {
|
||||
name: 'test',
|
||||
component: ()=> 'hello world.'
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = function() {
|
||||
this.bindHook('sub_setting_nav', hander);
|
||||
};
|
||||
4
exts/yapi-plugin-test/index.js
Normal file
4
exts/yapi-plugin-test/index.js
Normal file
@@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
server: false,
|
||||
client: true
|
||||
}
|
||||
12
exts/yapi-plugin-wiki/client.js
Normal file
12
exts/yapi-plugin-wiki/client.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import WikiPage from './wikiPage/index';
|
||||
// const WikiPage = require('./wikiPage/index')
|
||||
|
||||
module.exports = function() {
|
||||
this.bindHook('sub_nav', function(app) {
|
||||
app.wiki = {
|
||||
name: 'Wiki',
|
||||
path: '/project/:id/wiki',
|
||||
component: WikiPage
|
||||
};
|
||||
});
|
||||
};
|
||||
234
exts/yapi-plugin-wiki/controller.js
Normal file
234
exts/yapi-plugin-wiki/controller.js
Normal 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;
|
||||
64
exts/yapi-plugin-wiki/index.js
Normal file
64
exts/yapi-plugin-wiki/index.js
Normal file
@@ -0,0 +1,64 @@
|
||||
module.exports = {
|
||||
server: true,
|
||||
client: true,
|
||||
httpCodes: [
|
||||
100,
|
||||
101,
|
||||
102,
|
||||
200,
|
||||
201,
|
||||
202,
|
||||
203,
|
||||
204,
|
||||
205,
|
||||
206,
|
||||
207,
|
||||
208,
|
||||
226,
|
||||
300,
|
||||
301,
|
||||
302,
|
||||
303,
|
||||
304,
|
||||
305,
|
||||
307,
|
||||
308,
|
||||
400,
|
||||
401,
|
||||
402,
|
||||
403,
|
||||
404,
|
||||
405,
|
||||
406,
|
||||
407,
|
||||
408,
|
||||
409,
|
||||
410,
|
||||
411,
|
||||
412,
|
||||
413,
|
||||
414,
|
||||
415,
|
||||
416,
|
||||
417,
|
||||
418,
|
||||
422,
|
||||
423,
|
||||
424,
|
||||
426,
|
||||
428,
|
||||
429,
|
||||
431,
|
||||
500,
|
||||
501,
|
||||
502,
|
||||
503,
|
||||
504,
|
||||
505,
|
||||
506,
|
||||
507,
|
||||
508,
|
||||
510,
|
||||
511
|
||||
]
|
||||
};
|
||||
39
exts/yapi-plugin-wiki/server.js
Normal file
39
exts/yapi-plugin-wiki/server.js
Normal file
@@ -0,0 +1,39 @@
|
||||
const yapi = require('yapi.js');
|
||||
const mongoose = require('mongoose');
|
||||
const controller = require('./controller');
|
||||
|
||||
module.exports = function() {
|
||||
yapi.connect.then(function() {
|
||||
let Col = mongoose.connection.db.collection('wiki');
|
||||
Col.createIndex({
|
||||
project_id: 1
|
||||
});
|
||||
});
|
||||
|
||||
this.bindHook('add_router', function(addRouter) {
|
||||
addRouter({
|
||||
// 获取wiki信息
|
||||
controller: controller,
|
||||
method: 'get',
|
||||
path: 'wiki_desc/get',
|
||||
action: 'getWikiDesc'
|
||||
});
|
||||
|
||||
addRouter({
|
||||
// 更新wiki信息
|
||||
controller: controller,
|
||||
method: 'post',
|
||||
path: 'wiki_desc/up',
|
||||
action: 'uplodaWikiDesc'
|
||||
});
|
||||
});
|
||||
|
||||
this.bindHook('add_ws_router', function(wsRouter) {
|
||||
wsRouter({
|
||||
controller: controller,
|
||||
method: 'get',
|
||||
path: 'wiki_desc/solve_conflict',
|
||||
action: 'wikiConflict'
|
||||
});
|
||||
});
|
||||
};
|
||||
25
exts/yapi-plugin-wiki/util.js
Normal file
25
exts/yapi-plugin-wiki/util.js
Normal 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;
|
||||
56
exts/yapi-plugin-wiki/wikiModel.js
Normal file
56
exts/yapi-plugin-wiki/wikiModel.js
Normal 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;
|
||||
67
exts/yapi-plugin-wiki/wikiPage/Editor.js
Normal file
67
exts/yapi-plugin-wiki/wikiPage/Editor.js
Normal 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;
|
||||
41
exts/yapi-plugin-wiki/wikiPage/View.js
Normal file
41
exts/yapi-plugin-wiki/wikiPage/View.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button } from 'antd';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
const WikiView = props => {
|
||||
const { editorEable, onEditor, uid, username, editorTime, desc } = props;
|
||||
return (
|
||||
<div className="wiki-view-content">
|
||||
<div className="wiki-title">
|
||||
<Button icon="edit" onClick={onEditor} disabled={!editorEable}>
|
||||
编辑
|
||||
</Button>
|
||||
{username && (
|
||||
<div className="wiki-user">
|
||||
由{' '}
|
||||
<Link className="user-name" to={`/user/profile/${uid || 11}`}>
|
||||
{username}
|
||||
</Link>{' '}
|
||||
修改于 {editorTime}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="tui-editor-contents"
|
||||
dangerouslySetInnerHTML={{ __html: desc }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
WikiView.propTypes = {
|
||||
editorEable: PropTypes.bool,
|
||||
onEditor: PropTypes.func,
|
||||
uid: PropTypes.number,
|
||||
username: PropTypes.string,
|
||||
editorTime: PropTypes.string,
|
||||
desc: PropTypes.string
|
||||
};
|
||||
|
||||
export default WikiView;
|
||||
255
exts/yapi-plugin-wiki/wikiPage/index.js
Normal file
255
exts/yapi-plugin-wiki/wikiPage/index.js
Normal file
@@ -0,0 +1,255 @@
|
||||
import React, { Component } from 'react';
|
||||
import { message } from 'antd';
|
||||
import { connect } from 'react-redux';
|
||||
import axios from 'axios';
|
||||
import PropTypes from 'prop-types';
|
||||
import './index.scss';
|
||||
import { timeago } from '../../../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;
|
||||
27
exts/yapi-plugin-wiki/wikiPage/index.scss
Normal file
27
exts/yapi-plugin-wiki/wikiPage/index.scss
Normal file
@@ -0,0 +1,27 @@
|
||||
.wiki-content {
|
||||
.wiki-user {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.wiki-editor {
|
||||
padding-top: 16px;
|
||||
}
|
||||
|
||||
.upload-btn {
|
||||
margin-right: 16px;
|
||||
}
|
||||
.wiki-conflict {
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.wiki-up {
|
||||
text-align: right;
|
||||
padding-top: 16px;
|
||||
}
|
||||
|
||||
.wiki-title {
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user