Merge branch 'dev' of gitlab.corp.qunar.com:mfe/yapi into dev

This commit is contained in:
suxiaoxin 2017-11-03 11:16:41 +08:00
commit fbea724e50
23 changed files with 313 additions and 198 deletions

View File

@ -3,7 +3,7 @@ import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { Link } from 'react-router-dom'
import { Icon, Layout, Menu, Dropdown, message, Tooltip, Avatar, Popover, Tag } from 'antd'
import { Icon, Layout, Menu, Dropdown, message, Tooltip, Popover, Tag } from 'antd'
import { checkLoginState, logoutActions, loginTypeAction } from '../../reducer/modules/user'
import { changeMenuItem } from '../../reducer/modules/menu'
import { withRouter } from 'react-router';
@ -102,6 +102,7 @@ MenuUser.propTypes = {
}
const ToolUser = (props) => {
let imageUrl = props.imageUrl ? props.imageUrl : `/api/user/avatar?uid=${props.uid}`;
return (
<ul>
<li className="toolbar-li item-search">
@ -170,9 +171,10 @@ const ToolUser = (props) => {
/>
}>
<a className="dropdown-link">
<Avatar src={`/api/user/avatar?uid=${props.uid}`} />
{/*<img style={{width:24,height:24}} src={`/api/user/avatar?uid=${props.uid}`} />*/}
{/*<span className="name">{props.user}</span>*/}
<span className="avatar-image">
<img src={imageUrl} />
</span>
{/*props.imageUrl? <Avatar src={props.imageUrl} />: <Avatar src={`/api/user/avatar?uid=${props.uid}`} />*/}
<span className="name"><Icon type="down" /></span>
</a>
</Dropdown>
@ -190,7 +192,8 @@ ToolUser.propTypes = {
logout: PropTypes.func,
groupList: PropTypes.array,
studyTip: PropTypes.number,
study: PropTypes.bool
study: PropTypes.bool,
imageUrl: PropTypes.any
};
@ -203,7 +206,8 @@ ToolUser.propTypes = {
role: state.user.role,
login: state.user.isLogin,
studyTip: state.user.studyTip,
study: state.user.study
study: state.user.study,
imageUrl: state.user.imageUrl
}
},
{
@ -234,7 +238,8 @@ export default class HeaderCom extends Component {
history: PropTypes.object,
location: PropTypes.object,
study: PropTypes.bool,
studyTip: PropTypes.number
studyTip: PropTypes.number,
imageUrl: PropTypes.any
}
linkTo = (e) => {
if (e.key != '/doc') {
@ -281,7 +286,7 @@ export default class HeaderCom extends Component {
render() {
const { login, user, msg, uid, role, studyTip, study } = this.props;
const { login, user, msg, uid, role, studyTip, study, imageUrl } = this.props;
return (
<Header className="header-box m-header">
<div className="content g-row">
@ -296,7 +301,7 @@ export default class HeaderCom extends Component {
style={{ position: 'relative', zIndex: this.props.studyTip > 0 ? 3 : 1 }}>
{login ?
<ToolUser
{...{ studyTip, study, user, msg, uid, role }}
{...{ studyTip, study, user, msg, uid, role, imageUrl }}
relieveLink={this.relieveLink}
logout={this.logout}
/>

View File

@ -106,12 +106,33 @@
.dropdown-link {
color: #ccc;
transition: color .2s;
.ant-avatar-image{
// .ant-avatar-image{
// margin-bottom: -10px;
// }
// .ant-avatar > img{
// height: auto;
// }
.avatar-image{
margin-bottom: -10px;
display: inline-block;
text-align: center;
background: #ccc;
color: #fff;
white-space: nowrap;
position: relative;
overflow: hidden;
width: 32px;
height: 32px;
line-height: 32px;
border-radius: 16px;
}
.ant-avatar > img{
.avatar-image > img{
height: auto;
width:100%;
display: bloack;
}
}
.anticon.active {
color: #2395f1;

View File

@ -262,7 +262,7 @@ export default class Run extends Component {
} else if (that.state.bodyType === 'json') {
body = json_parse(that.state.bodyOther);
}
if (res_body && res_body_type === 'json' && typeof res === 'object') {
if (res_body && res_body_type === 'json' && typeof res === 'object' && this.state.resMockTest === true) {
let tpl = MockExtra(json_parse(res_body), {
query: query,
body: body

View File

@ -81,16 +81,17 @@ export default class GroupList extends Component {
async componentWillMount() {
const groupId = !isNaN(this.props.match.params.groupId) ? parseInt(this.props.match.params.groupId) : 0;
await this.props.fetchGroupList();
let currGroup = false;
let currGroup = false;
if (this.props.groupList.length && groupId) {
for (let i = 0; i < this.props.groupList.length; i++) {
if (this.props.groupList[i]._id === groupId) {
currGroup = this.props.groupList[i];
} }
}
}
} else if (!groupId && this.props.groupList.length) {
this.props.history.push(`/group/${this.props.groupList[0]._id}`);
}
if(!currGroup){
if (!currGroup) {
currGroup = this.props.groupList[0] || { group_name: '', group_desc: '' };
this.props.history.replace(`${currGroup._id}`);
}
@ -141,7 +142,9 @@ export default class GroupList extends Component {
});
await this.props.fetchGroupList();
this.setState({ groupList: this.props.groupList });
this.props.setCurrGroup(res.data.data);
const id = res.data.data._id;
const currGroup = _.find(this.props.groupList, (group) => { return +group._id === +id });
this.props.setCurrGroup(currGroup);
this.props.fetchGroupMsg(this.props.currGroup._id);
this.props.fetchNewsData(this.props.currGroup._id, "group", 1, 10)
} else {
@ -160,8 +163,12 @@ export default class GroupList extends Component {
editGroupModalVisible: false
});
await this.props.fetchGroupList();
this.setState({ groupList: this.props.groupList });
this.props.setCurrGroup({ group_name, group_desc, _id: id});
const currGroup = _.find(this.props.groupList, (group) => { return +group._id === +id });
this.props.setCurrGroup(currGroup);
// this.props.setCurrGroup({ group_name, group_desc, _id: id });
this.props.fetchGroupMsg(this.props.currGroup._id);
this.props.fetchNewsData(this.props.currGroup._id, "group", 1, 10)
}
@ -269,7 +276,7 @@ export default class GroupList extends Component {
let menu = <Menu>
{
this.props.curUserRole === "admin" && this.props.currGroup.type!=='private' ? (editmark) : ''
this.props.curUserRole === "admin" && this.props.currGroup.type !== 'private' ? (editmark) : ''
}
{
(this.props.curUserRole === "admin" || this.props.curUserRoleInGroup === 'owner') && this.props.currGroup.type !== 'private' ? (delmark) : ''
@ -278,7 +285,7 @@ export default class GroupList extends Component {
this.props.curUserRole === 'admin' ? (addmark) : ''
}
</Menu>;
menu = (this.props.curUserRoleInGroup === 'owner' && this.props.curUserRole !== 'admin') ? <a className="editSet"><Icon type="setting" onClick={() => this.showModal(TYPE_EDIT)} /></a> : <Dropdown overlay={menu}>
menu = (this.props.curUserRoleInGroup === 'owner' && this.props.curUserRole !== 'admin') ? <a className="editSet"><Icon type="setting" onClick={() => this.showModal(TYPE_EDIT)} /></a> : <Dropdown overlay={menu}>
<a className="ant-dropdown-link" href="#">
<Icon type="setting" />
</a>
@ -313,16 +320,16 @@ export default class GroupList extends Component {
selectedKeys={[`${currGroup._id}`]}
>
{this.state.groupList.map((group) => {
if(group.type === 'private') {
return <Menu.Item key={`${group._id}`} className="group-item" style={{zIndex: this.props.studyTip === 0 ? 3 : 1}}>
if (group.type === 'private') {
return <Menu.Item key={`${group._id}`} className="group-item" style={{ zIndex: this.props.studyTip === 0 ? 3 : 1 }}>
<Icon type="user" />
<Popover
overlayClassName="popover-index"
content={<GuideBtns/>}
content={<GuideBtns />}
title={tip}
placement="right"
visible={(this.props.studyTip === 0) && !this.props.study}
>
>
{group.group_name}
</Popover>
</Menu.Item>

View File

@ -1,7 +1,7 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import {Table, Select, Button, Modal, Row, Col, message, Popconfirm } from 'antd';
import { Table, Select, Button, Modal, Row, Col, message, Popconfirm } from 'antd';
import { Link } from 'react-router-dom'
import './MemberList.scss';
import { autobind } from 'core-decorators';
@ -10,7 +10,7 @@ import ErrMsg from '../../../components/ErrMsg/ErrMsg.js';
import UsernameAutoComplete from '../../../components/UsernameAutoComplete/UsernameAutoComplete.js';
const Option = Select.Option;
function arrayAddKey (arr) {
function arrayAddKey(arr) {
return arr.map((item, index) => {
return {
...item,
@ -86,7 +86,10 @@ class MemberList extends Component {
role: this.state.inputRole
}).then((res) => {
if (!res.payload.data.errcode) {
message.success('添加成功!');
const { add_members, exist_members } = res.payload.data.data;
const addLength = add_members.length;
const existLength = exist_members.length;
message.success(`添加成功! 已成功添加 ${addLength} 人,其中 ${existLength} 人已存在`);
this.reFetchList(); // 添加成功后重新获取分组成员列表
}
});
@ -137,7 +140,7 @@ class MemberList extends Component {
componentWillReceiveProps(nextProps) {
if(this._groupId !== this._groupId){
if (this._groupId !== this._groupId) {
return null;
}
if (this.props.currGroup !== nextProps.currGroup) {
@ -177,7 +180,7 @@ class MemberList extends Component {
render() {
const columns = [{
title: this.props.currGroup.group_name + ' 分组成员 ('+this.state.userInfo.length + ') 人',
title: this.props.currGroup.group_name + ' 分组成员 (' + this.state.userInfo.length + ') 人',
dataIndex: 'username',
key: 'username',
render: (text, record) => {
@ -198,10 +201,10 @@ class MemberList extends Component {
if (this.state.role === 'owner' || this.state.role === 'admin') {
return (
<div>
<Select value={ record.role+'-'+record.uid} className="select" onChange={this.changeUserRole}>
<Option value={ 'owner-'+record.uid}>组长</Option>
<Option value={'dev-'+record.uid}>开发者</Option>
<Option value={'guest-'+record.uid}>访客</Option>
<Select value={record.role + '-' + record.uid} className="select" onChange={this.changeUserRole}>
<Option value={'owner-' + record.uid}>组长</Option>
<Option value={'dev-' + record.uid}>开发者</Option>
<Option value={'guest-' + record.uid}>访客</Option>
</Select>
<Popconfirm placement="topRight" title="你确定要删除吗? " onConfirm={this.deleteConfirm(record.uid)} okText="确定" cancelText="">
<Button type="danger" icon="minus" className="btn-danger" />
@ -226,26 +229,26 @@ class MemberList extends Component {
let ownerinfo = [];
let devinfo = [];
let guestinfo = [];
for(let i = 0;i<userinfo.length;i++){
if(userinfo[i].role === "owner"){
for (let i = 0; i < userinfo.length; i++) {
if (userinfo[i].role === "owner") {
ownerinfo.push(userinfo[i]);
}
if(userinfo[i].role === "dev"){
if (userinfo[i].role === "dev") {
devinfo.push(userinfo[i]);
}
if(userinfo[i].role === "guest"){
if (userinfo[i].role === "guest") {
guestinfo.push(userinfo[i]);
}
}
userinfo = [...ownerinfo,...devinfo,...guestinfo];
userinfo = [...ownerinfo, ...devinfo, ...guestinfo];
return (
<div className="m-panel">
{this.state.visible?<Modal
{this.state.visible ? <Modal
title="添加成员"
visible={this.state.visible}
onOk={this.handleOk}
onCancel={this.handleCancel}
>
>
<Row gutter={6} className="modal-input">
<Col span="5"><div className="label usernamelabel">用户名: </div></Col>
<Col span="15">
@ -262,8 +265,8 @@ class MemberList extends Component {
</Select>
</Col>
</Row>
</Modal>:""}
<Table columns={columns} dataSource={userinfo} pagination={false} locale={{emptyText: <ErrMsg type="noMemberInGroup"/>}} />
</Modal> : ""}
<Table columns={columns} dataSource={userinfo} pagination={false} locale={{ emptyText: <ErrMsg type="noMemberInGroup" /> }} />
</div>
);
}

View File

@ -104,20 +104,20 @@ class ProjectList extends Component {
let projectData = this.state.projectData;
let noFollow = [];
let followProject = [];
for(var i in projectData){
if(projectData[i].follow){
for (var i in projectData) {
if (projectData[i].follow) {
followProject.push(projectData[i]);
}else{
} else {
noFollow.push(projectData[i]);
}
}
followProject = followProject.sort((a,b)=>{
followProject = followProject.sort((a, b) => {
return b.up_time - a.up_time;
})
noFollow = noFollow.sort((a,b)=>{
noFollow = noFollow.sort((a, b) => {
return b.up_time - a.up_time;
})
projectData = [...followProject,...noFollow]
projectData = [...followProject, ...noFollow]
return (
<div style={{ paddingTop: '24px' }} className="m-panel card-panel card-panel-s project-list" >
<Row className="project-list-header">
@ -126,7 +126,7 @@ class ProjectList extends Component {
</Col>
<Col>
{/(admin)|(owner)|(dev)/.test(this.props.currGroup.role) ?
<Link to="/add-project"><Button type="primary">添加项目</Button></Link>:
<Link to="/add-project"><Button type="primary">添加项目</Button></Link> :
<Tooltip title="您没有权限,请联系该分组组长或管理员">
<Button type="primary" disabled >添加项目</Button>
</Tooltip>}
@ -135,7 +135,7 @@ class ProjectList extends Component {
<Row gutter={16}>
{projectData.length ? projectData.map((item, index) => {
return (
<Col xs={8} md={6} xl={4} key={index}>
<Col xs={8} md={6} xl={4} key={index}>
<ProjectCard projectData={item} callbackResult={this.receiveRes} />
</Col>);
}) : <ErrMsg type="noProject" />}

View File

@ -2,7 +2,7 @@ import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux';
import InterfaceEditForm from './InterfaceEditForm.js'
import { updateInterfaceData,fetchInterfaceList } from '../../../../reducer/modules/interface.js';
import { updateInterfaceData, fetchInterfaceList, fetchInterfaceData } from '../../../../reducer/modules/interface.js';
import axios from 'axios'
import { message } from 'antd'
import './Edit.scss'
@ -16,7 +16,8 @@ import { withRouter, Link } from 'react-router-dom';
}
}, {
updateInterfaceData,
fetchInterfaceList
fetchInterfaceList,
fetchInterfaceData
}
)
@ -26,6 +27,7 @@ class InterfaceEdit extends Component {
currProject: PropTypes.object,
updateInterfaceData: PropTypes.func,
fetchInterfaceList: PropTypes.func,
fetchInterfaceData: PropTypes.func,
match: PropTypes.object,
switchToView: PropTypes.func
}
@ -44,8 +46,10 @@ class InterfaceEdit extends Component {
params.id = this.props.match.params.actionId;
let result = await axios.post('/api/interface/up', params);
this.props.fetchInterfaceList(this.props.currProject._id).then();
this.props.fetchInterfaceData(params.id).then()
if (result.data.errcode === 0) {
this.props.updateInterfaceData(params);
console.log('switch');
message.success('保存成功');
this.props.switchToView()
} else {
@ -73,12 +77,12 @@ class InterfaceEdit extends Component {
//因后端 node 仅支持 ws 暂不支持 wss
let wsProtocol = location.protocol === 'https' ? 'ws' : 'ws';
try{
s = new WebSocket( wsProtocol + '://' + domain + '/api/interface/solve_conflict?id=' + this.props.match.params.actionId);
try {
s = new WebSocket(wsProtocol + '://' + domain + '/api/interface/solve_conflict?id=' + this.props.match.params.actionId);
s.onopen = () => {
this.WebSocket = s;
}
s.onmessage = (e) => {
let result = JSON.parse(e.data);
if (result.errno === 0) {
@ -92,13 +96,13 @@ class InterfaceEdit extends Component {
status: 2
})
}
}
s.onerror = () => {
console.error('websocket connect failed.')
}
}catch(e){
} catch (e) {
console.error(e);
}

View File

@ -2,6 +2,7 @@
.interface-edit{
padding: 24px;
// overflow: hidden;
.interface-edit-item{
margin-bottom: 16px;
}
@ -19,12 +20,31 @@
margin-left: 5px;
cursor: pointer
}
.interface-edit-direc-icon{
margin-top: 5px;
margin-left: 5px;
cursor: pointer
}
.ant-select-selection__rendered{
line-height: 34px;
}
.interace-edit-desc{
height: 250px;
}
.ant-affix{
background-color: #f3f4f6;
padding: 16px 0;
z-index: 10002;
}
.interface-edit-submit-button{
background-color: #32325d;
color: #fff;
}
.interface-edit-submit-button:hover{
color: #32325d;
border-color: #32325d;
}
}
.table-interfacelist {

View File

@ -6,7 +6,7 @@ import constants from '../../../../constants/variable.js'
import { handlePath, nameLengthLimit } from '../../../../common.js'
import { changeEditStatus } from '../../../../reducer/modules/interface.js';
import json5 from 'json5'
import { message, Tabs } from 'antd'
import { message, Tabs, Affix } from 'antd'
import Editor from 'wangeditor'
const TabPane = Tabs.TabPane;
let EditFormContext;
@ -163,12 +163,12 @@ class InterfaceEditForm extends Component {
})
}
} else if (values.req_body_type === 'json') {
values.req_headers?values.req_headers.map((item) => {
values.req_headers ? values.req_headers.map((item) => {
if (item.name === 'Content-Type') {
item.value = 'application/json'
isHavaContentType = true;
}
}):[];
}) : [];
if (isHavaContentType === false) {
values.req_headers = values.req_headers || [];
values.req_headers.unshift({
@ -249,10 +249,10 @@ class InterfaceEditForm extends Component {
}
editor.create();
editor.txt.html(this.state.desc)
if(navigator.userAgent.indexOf("Firefox")>0){
if (navigator.userAgent.indexOf("Firefox") > 0) {
document.getElementById('title').focus()
}
}
componentWillUnmount() {
@ -277,16 +277,18 @@ class InterfaceEditForm extends Component {
this.setState(newValue)
}
handleMockPreview = ()=>{
handleMockPreview = () => {
let str = '';
try{
if(this.resBodyEditor.curData.format === true){
str = JSON.stringify(this.resBodyEditor.curData.mockData(), null , ' ');
}else{
try {
if (this.resBodyEditor.curData.format === true) {
str = JSON.stringify(this.resBodyEditor.curData.mockData(), null, ' ');
} else {
str = '解析出错: ' + this.resBodyEditor.curData.format;
}
}catch(err){
} catch (err) {
str = '解析出错: ' + err.message;
}
this.mockPreview.setValue(
@ -296,7 +298,7 @@ class InterfaceEditForm extends Component {
handleJsonType = (key) => {
key = key || 'tpl';
if(key === 'preview'){
if (key === 'preview') {
this.handleMockPreview()
}
this.setState({
@ -365,6 +367,7 @@ class InterfaceEditForm extends Component {
};
const queryTpl = (data, index) => {
return <Row key={index} className="interface-edit-item-content">
<Col span="5" className="interface-edit-item-content-col">
{getFieldDecorator('req_query[' + index + '].name', {
@ -400,7 +403,6 @@ class InterfaceEditForm extends Component {
<Col span="1" className="interface-edit-item-content-col" >
<Icon type="delete" className="interface-edit-del-icon" onClick={() => this.delParams(index, 'req_query')} />
</Col>
</Row>
}
@ -530,15 +532,15 @@ class InterfaceEditForm extends Component {
return queryTpl(item, index)
})
const headerList = this.state.req_headers?this.state.req_headers.map((item, index) => {
const headerList = this.state.req_headers ? this.state.req_headers.map((item, index) => {
return headerTpl(item, index)
}):[];
}) : [];
const requestBodyList = this.state.req_body_form.map((item, index) => {
return requestBodyTpl(item, index)
})
return (
<Form onSubmit={this.handleSubmit}>
<Form onSubmit={this.handleSubmit}>
<h2 className="interface-title" style={{ marginTop: 0 }}>基本设置</h2>
<div className="panel-sub">
@ -551,7 +553,7 @@ class InterfaceEditForm extends Component {
initialValue: this.state.title,
rules: nameLengthLimit('接口')
})(
<Input id="title" placeholder="接口名称" />
<Input id="title" placeholder="接口名称" />
)}
</FormItem>
@ -757,7 +759,7 @@ class InterfaceEditForm extends Component {
<TabPane tab="模板" key="tpl">
</TabPane>
<TabPane tab="预览" key="preview">
<TabPane tab="预览" key="preview">
</TabPane>
@ -822,7 +824,10 @@ class InterfaceEditForm extends Component {
className="interface-edit-item"
style={{ textAlign: 'center', marginTop: '16px' }}
>
<Button type="primary" htmlType="submit">保存</Button>
{/* <Button type="primary" htmlType="submit">保存1</Button> */}
<Affix offsetBottom={0}>
<Button className="interface-edit-submit-button" size="large" htmlType="submit">保存</Button>
</Affix>
</FormItem>
</Form>
);

View File

@ -6,7 +6,7 @@ import {
Table, Button, Modal, message, Tooltip, Select
} from 'antd';
import AddInterfaceForm from './AddInterfaceForm';
import { fetchInterfaceList} from '../../../../reducer/modules/interface.js';
import { fetchInterfaceList } from '../../../../reducer/modules/interface.js';
import { Link } from 'react-router-dom';
import variable from '../../../../constants/variable';
import './Edit.scss';
@ -19,7 +19,7 @@ const Option = Select.Option;
curProject: state.project.currProject,
catList: state.inter.list
}
},{
}, {
fetchInterfaceList
})
class InterfaceList extends Component {
@ -87,11 +87,11 @@ class InterfaceList extends Component {
}
}
handleAddInterface =(data)=> {
handleAddInterface = (data) => {
data.project_id = this.props.curProject._id;
axios.post('/api/interface/add', data).then((res) => {
if (res.data.errcode !== 0) {
return message.error(res.data.errmsg);
return message.error(`${res.data.errmsg}, 你可以在左侧的接口列表中对接口进行删改`);
}
message.success('接口添加成功')
let interfaceId = res.data.data._id;
@ -126,7 +126,7 @@ class InterfaceList extends Component {
return a.title.localeCompare(b.title) === 1
},
sortOrder: sortedInfo.columnKey === 'title' && sortedInfo.order,
render: (text, item)=>{
render: (text, item) => {
return <Link to={"/project/" + item.project_id + "/interface/api/" + item._id} ><span className="path">{text}</span></Link>
}
}, {
@ -145,7 +145,7 @@ class InterfaceList extends Component {
width: 12,
render: (item) => {
let methodColor = variable.METHOD_COLOR[item ? item.toLowerCase() : 'get'];
return <span style={{color:methodColor.color,backgroundColor:methodColor.bac}} className="colValue">{item}</span>
return <span style={{ color: methodColor.color, backgroundColor: methodColor.bac }} className="colValue">{item}</span>
}
}, {
title: '状态',
@ -169,9 +169,9 @@ class InterfaceList extends Component {
onFilter: (value, record) => record.status.indexOf(value) === 0
}]
let intername = '';
if(this.props.curProject.cat){
for(let i = 0;i<this.props.curProject.cat.length;i++){
if(this.props.curProject.cat[i]._id === this.state.catid){
if (this.props.curProject.cat) {
for (let i = 0; i < this.props.curProject.cat.length; i++) {
if (this.props.curProject.cat[i]._id === this.state.catid) {
intername = this.props.curProject.cat[i].name;
}
}
@ -180,11 +180,12 @@ class InterfaceList extends Component {
item.key = item._id;
return item;
});
return (
<div style={{ padding: '24px' }}>
<h2 className="interface-title" style={{ display: 'inline-block', margin: 0}}>{intername?intername:'全部接口'}</h2>
<Button style={{float: 'right'}} type="primary" onClick={() => this.setState({ visible: true })}>添加接口</Button>
<Table className="table-interfacelist" pagination={false} columns={columns} onChange={this.handleChange} dataSource={data} />
<h2 className="interface-title" style={{ display: 'inline-block', margin: 0 }}>{intername ? intername : '全部接口'}</h2>
<Button style={{ float: 'right' }} type="primary" onClick={() => this.setState({ visible: true })}>添加接口</Button>
<Table className="table-interfacelist" pagination={false} columns={columns} onChange={this.handleChange} dataSource={data} />
<Modal
title="添加接口"
visible={this.state.visible}

View File

@ -157,7 +157,8 @@ class InterfaceMenu extends Component {
}
handleChangeInterfaceCat = (data) => {
data.project_id = this.props.projectId;
console.log('change',data);
let params = {
catid: this.state.curCatdata._id,
name: data.name
@ -169,6 +170,7 @@ class InterfaceMenu extends Component {
}
message.success('接口分类更新成功')
this.getList()
this.props.getProject(data.project_id)
this.setState({
change_cat_modal_visible: false
});
@ -203,6 +205,7 @@ class InterfaceMenu extends Component {
async onOk() {
await that.props.deleteInterfaceCatData(catid, that.props.projectId)
await that.getList()
await that.props.getProject(that.props.projectId)
that.props.history.push('/project/' + that.props.match.params.id + '/interface/api')
ref.destroy()
},

View File

@ -94,7 +94,10 @@ class ProjectMember extends Component {
role: this.state.inputRole
}).then((res) => {
if (!res.payload.data.errcode) {
message.success('添加成功!');
const { add_members, exist_members } = res.payload.data.data;
const addLength = add_members.length;
const existLength = exist_members.length;
message.success(`添加成功! 已成功添加 ${addLength} 人,其中 ${existLength} 人已存在`);
this.reFetchList(); // 添加成功后重新获取分组成员列表
}
});

View File

@ -1,20 +1,20 @@
import React, { Component } from 'react'
import { Row, Col, Input, Button, Select, message, Upload, Tooltip} from 'antd'
import { Row, Col, Input, Button, Select, message, Upload, Tooltip } from 'antd'
import axios from 'axios';
import {formatTime} from '../../common.js'
import { formatTime } from '../../common.js'
import PropTypes from 'prop-types'
import { setBreadcrumb } from '../../reducer/modules/user';
import { setBreadcrumb, setImageUrl } from '../../reducer/modules/user';
import { connect } from 'react-redux'
@connect(state=>{
@connect(state => {
return {
curUid: state.user.uid,
userType: state.user.type,
curRole: state.user.role
}
},{
setBreadcrumb
})
}, {
setBreadcrumb
})
class Profile extends Component {
@ -23,7 +23,8 @@ class Profile extends Component {
curUid: PropTypes.number,
userType: PropTypes.string,
setBreadcrumb: PropTypes.func,
curRole: PropTypes.string
curRole: PropTypes.string,
upload: PropTypes.bool
}
constructor(props) {
@ -40,19 +41,19 @@ class Profile extends Component {
}
componentDidMount(){
componentDidMount() {
this._uid = this.props.match.params.uid;
this.handleUserinfo(this.props)
}
componentWillReceiveProps(nextProps){
if(!nextProps.match.params.uid) return;
if(this._uid !== nextProps.match.params.uid){
componentWillReceiveProps(nextProps) {
if (!nextProps.match.params.uid) return;
if (this._uid !== nextProps.match.params.uid) {
this.handleUserinfo(nextProps)
}
}
handleUserinfo(props){
handleUserinfo(props) {
const uid = props.match.params.uid;
this.getUserInfo(uid)
}
@ -72,22 +73,22 @@ class Profile extends Component {
_userinfo: res.data.data
})
if (curUid === +id) {
this.props.setBreadcrumb([{name: res.data.data.username}]);
this.props.setBreadcrumb([{ name: res.data.data.username }]);
} else {
this.props.setBreadcrumb([{name: '管理: ' + res.data.data.username}]);
this.props.setBreadcrumb([{ name: '管理: ' + res.data.data.username }]);
}
});
}
updateUserinfo = (name) =>{
updateUserinfo = (name) => {
var state = this.state;
let value = this.state._userinfo[name];
let params = {uid: state.userinfo.uid}
let params = { uid: state.userinfo.uid }
params[name] = value;
axios.post('/api/user/update', params).then( (res)=>{
axios.post('/api/user/update', params).then((res) => {
let data = res.data;
if(data.errcode === 0){
if (data.errcode === 0) {
let userinfo = this.state.userinfo;
userinfo[name] = value;
this.setState({
@ -96,16 +97,16 @@ class Profile extends Component {
this.handleEdit(name + 'Edit', false)
message.success('更新用户信息成功');
}else{
} else {
message.error(data.errmsg)
}
}, (err) => {
message.error(err.message)
} )
})
}
changeUserinfo = (e) =>{
changeUserinfo = (e) => {
let dom = e.target;
let name = dom.getAttribute("name");
let value = dom.value;
@ -116,7 +117,7 @@ class Profile extends Component {
})
}
changeRole = (val) =>{
changeRole = (val) => {
let userinfo = this.state.userinfo;
userinfo.role = val;
this.setState({
@ -125,11 +126,11 @@ class Profile extends Component {
this.updateUserinfo('role');
}
updatePassword = () =>{
updatePassword = () => {
let old_password = document.getElementById('old_password').value;
let password = document.getElementById('password').value;
let verify_pass = document.getElementById('verify_pass').value;
if(password != verify_pass){
if (password != verify_pass) {
return message.error('两次输入的密码不一样');
}
let params = {
@ -139,19 +140,19 @@ class Profile extends Component {
}
axios.post('/api/user/change_password', params).then( (res)=>{
axios.post('/api/user/change_password', params).then((res) => {
let data = res.data;
if(data.errcode === 0){
if (data.errcode === 0) {
this.handleEdit('secureEdit', false)
message.success('修改密码成功');
location.reload()
}else{
} else {
message.error(data.errmsg)
}
}, (err) => {
message.error(err.message)
} )
})
}
@ -163,26 +164,26 @@ class Profile extends Component {
let _userinfo = this.state._userinfo;
let roles = { admin: '管理员', member: '会员' };
let userType = "";
if(this.props.userType === "third"){
if (this.props.userType === "third") {
userType = false;
}else if(this.props.userType === "site"){
} else if (this.props.userType === "site") {
userType = true;
}else{
} else {
userType = false;
}
if (this.state.usernameEdit === false) {
let btn = "";
if(userType){
if(userinfo.uid === this.props.curUid){//本人
btn = <Button icon="edit" onClick={() => { this.handleEdit('usernameEdit', true) }}>修改</Button>;
}else{
if(this.props.curRole === "admin"){
btn = <Button icon="edit" onClick={() => { this.handleEdit('usernameEdit', true) }}>修改</Button>;
}else{
if (userType) {
if (userinfo.uid === this.props.curUid) {//本人
btn = <Button icon="edit" onClick={() => { this.handleEdit('usernameEdit', true) }}>修改</Button>;
} else {
if (this.props.curRole === "admin") {
btn = <Button icon="edit" onClick={() => { this.handleEdit('usernameEdit', true) }}>修改</Button>;
} else {
btn = "";
}
}
}else{
} else {
// if(userinfo.uid === this.props.curUid){//本人
// btn = <Button icon="edit" onClick={() => { this.handleEdit('usernameEdit', true) }}>修改</Button>;
// }else{
@ -198,33 +199,33 @@ class Profile extends Component {
</div>
} else {
userNameEditHtml = <div>
<Input value={_userinfo.username} name="username" onChange={this.changeUserinfo} placeholder="用户名" />
<Input value={_userinfo.username} name="username" onChange={this.changeUserinfo} placeholder="用户名" />
<ButtonGroup className="edit-buttons" >
<Button className="edit-button" onClick={() => { this.handleEdit('usernameEdit', false) }} >取消</Button>
<Button className="edit-button" onClick={ () => { this.updateUserinfo('username')} } type="primary">确定</Button>
<Button className="edit-button" onClick={() => { this.handleEdit('usernameEdit', false) }} >取消</Button>
<Button className="edit-button" onClick={() => { this.updateUserinfo('username') }} type="primary">确定</Button>
</ButtonGroup>
</div>
}
if (this.state.emailEdit === false) {
let btn = "";
if(userType){
if(userinfo.uid === this.props.curUid){//本人
btn = <Button icon="edit" onClick={() => { this.handleEdit('emailEdit', true) }}>修改</Button>
if(userinfo.role === 'admin'){
if (userType) {
if (userinfo.uid === this.props.curUid) {//本人
btn = <Button icon="edit" onClick={() => { this.handleEdit('emailEdit', true) }}>修改</Button>
if (userinfo.role === 'admin') {
btn = "";
}
}else{
if(this.props.curRole === "admin"){
btn = <Button icon="edit" onClick={() => { this.handleEdit('emailEdit', true) }}>修改</Button>
}else{
} else {
if (this.props.curRole === "admin") {
btn = <Button icon="edit" onClick={() => { this.handleEdit('emailEdit', true) }}>修改</Button>
} else {
btn = "";
}
}
}else{
if(userinfo.uid === this.props.curUid){//本人
} else {
if (userinfo.uid === this.props.curUid) {//本人
// btn = <Button icon="edit" onClick={() => { this.handleEdit('emailEdit', true) }}>修改</Button>
}else{
} else {
btn = "";
}
}
@ -237,8 +238,8 @@ class Profile extends Component {
emailEditHtml = <div>
<Input placeholder="Email" value={_userinfo.email} name="email" onChange={this.changeUserinfo} />
<ButtonGroup className="edit-buttons" >
<Button className="edit-button" onClick={() => { this.handleEdit('emailEdit', false) }} >取消</Button>
<Button className="edit-button" type="primary" onClick={ () => { this.updateUserinfo('email')} }>确定</Button>
<Button className="edit-button" onClick={() => { this.handleEdit('emailEdit', false) }} >取消</Button>
<Button className="edit-button" type="primary" onClick={() => { this.updateUserinfo('email') }}>确定</Button>
</ButtonGroup>
</div>
}
@ -250,7 +251,7 @@ class Profile extends Component {
{btn}
</div>
} else {
roleEditHtml = <Select defaultValue={_userinfo.role} onChange={ this.changeRole} style={{ width: 150 }} >
roleEditHtml = <Select defaultValue={_userinfo.role} onChange={this.changeRole} style={{ width: 150 }} >
<Option value="admin">管理员</Option>
<Option value="member">会员</Option>
</Select>
@ -258,27 +259,31 @@ class Profile extends Component {
if (this.state.secureEdit === false) {
let btn = "";
if(userType){
btn = <Button icon="edit" onClick={() => { this.handleEdit('secureEdit', true) }}>修改</Button>
if (userType) {
btn = <Button icon="edit" onClick={() => { this.handleEdit('secureEdit', true) }}>修改</Button>
}
secureEditHtml = btn;
} else {
secureEditHtml = <div>
<Input style={{display: this.props.curRole === 'admin'&& userinfo.role!='admin' ? 'none': ''}} placeholder="旧的密码" type="password" name="old_password" id="old_password" />
<Input style={{ display: this.props.curRole === 'admin' && userinfo.role != 'admin' ? 'none' : '' }} placeholder="旧的密码" type="password" name="old_password" id="old_password" />
<Input placeholder="新的密码" type="password" name="password" id="password" />
<Input placeholder="确认密码" type="password" name="verify_pass" id="verify_pass" />
<ButtonGroup className="edit-buttons" >
<Button className="edit-button" onClick={() => { this.handleEdit('secureEdit', false) }}>取消</Button>
<Button className="edit-button" onClick={this.updatePassword} type="primary">确定</Button>
<Button className="edit-button" onClick={() => { this.handleEdit('secureEdit', false) }}>取消</Button>
<Button className="edit-button" onClick={this.updatePassword} type="primary">确定</Button>
</ButtonGroup>
</div>
}
return <div className="user-profile">
<div className="user-item-body">
{userinfo.uid === this.props.curUid?<h3>个人设置</h3>:<h3>{userinfo.username} </h3>}
{userinfo.uid === this.props.curUid ? <h3>个人设置</h3> : <h3>{userinfo.username} </h3>}
<Row className="avatarCon" type="flex" justify="start">
<Col span={24}>{userinfo.uid === this.props.curUid?<AvatarUpload uid={userinfo.uid}>点击上传头像</AvatarUpload>:<div className = "avatarImg"><img src = {`/api/user/avatar?uid=${userinfo.uid}`} /></div>}</Col>
<Row className="avatarCon" type="flex" justify="start">
<Col span={24}>
{
userinfo.uid === this.props.curUid ? <AvatarUpload uid={userinfo.uid}>点击上传头像</AvatarUpload> : <div className="avatarImg"><img src={`/api/user/avatar?uid=${userinfo.uid}`} /></div>
}
</Col>
</Row>
<Row className="user-item" type="flex" justify="start">
<div className="maoboli"></div>
@ -294,14 +299,14 @@ class Profile extends Component {
{userNameEditHtml}
</Col>
</Row>
<Row className="user-item" type="flex" justify="start">
<Row className="user-item" type="flex" justify="start">
<div className="maoboli"></div>
<Col span={4}>Email</Col>
<Col span={12}>
{emailEditHtml}
</Col>
</Row>
<Row className="user-item" style={{display: this.props.curRole === 'admin'? '': 'none'}} type="flex" justify="start">
<Row className="user-item" style={{ display: this.props.curRole === 'admin' ? '' : 'none' }} type="flex" justify="start">
<div className="maoboli"></div>
<Col span={4}>角色</Col>
<Col span={12}>
@ -323,47 +328,54 @@ class Profile extends Component {
</Col>
</Row>
{(userType)?<Row className="user-item" type="flex" justify="start">
{(userType) ? <Row className="user-item" type="flex" justify="start">
<div className="maoboli"></div>
<Col span={4}>密码</Col>
<Col span={12}>
{secureEditHtml}
</Col>
</Row>:""}
</Row> : ""}
</div>
</div>
}
}
@connect(state => {
return {
url: state.user.imageUrl
}
}, {
setImageUrl
})
class AvatarUpload extends Component {
constructor(props) {
super(props);
this.state = {
imageUrl: ""
}
}
static propTypes = {
uid: PropTypes.number
uid: PropTypes.number,
setImageUrl: PropTypes.func,
url: PropTypes.any
}
uploadAvatar(basecode){
axios.post("/api/user/upload_avatar",{basecode: basecode}).then(()=>{
this.setState({ imageUrl: basecode });
}).catch((e)=>{
uploadAvatar(basecode) {
axios.post("/api/user/upload_avatar", { basecode: basecode }).then(() => {
// this.setState({ imageUrl: basecode });
this.props.setImageUrl(basecode)
}).catch((e) => {
console.log(e);
})
}
handleChange(info){
handleChange(info) {
if (info.file.status === 'done') {
// Get this url from response in real world.
getBase64(info.file.originFileObj, basecode=>{this.uploadAvatar(basecode)});
getBase64(info.file.originFileObj, basecode => { this.uploadAvatar(basecode) });
}
}
render() {
let imageUrl = this.state.imageUrl?this.state.imageUrl:`/api/user/avatar?uid=${this.props.uid}`;
const { url } = this.props;
let imageUrl = url ? url : `/api/user/avatar?uid=${this.props.uid}`
// let imageUrl = this.state.imageUrl ? this.state.imageUrl : `/api/user/avatar?uid=${this.props.uid}`;
// console.log(this.props.uid);
return <div className="avatar-box">
<Tooltip placement="right" title={<div>点击头像更换 (只支持jpgpng格式且大小不超过200kb的图片)</div>}>
<div>
@ -375,7 +387,9 @@ class AvatarUpload extends Component {
beforeUpload={beforeUpload}
onChange={this.handleChange.bind(this)} >
{/*<Avatar size="large" src={imageUrl} />*/}
<img className = "avatar" src = {imageUrl} />
<div style={{ width: 100, height: 100 }}>
<img className="avatar" src={imageUrl} />
</div>
</Upload>
</div>
</Tooltip>
@ -395,7 +409,7 @@ function beforeUpload(file) {
message.error('图片必须小于 200kb!');
}
return (isPNG||isJPG) && isLt2M;
return (isPNG || isJPG) && isLt2M;
}
function getBase64(img, callback) {

View File

@ -64,7 +64,6 @@ export function initInterface(){
}
export function updateInterfaceData(updata) {
return {
type: UPDATE_INTERFACE_DATA,
updata: updata,
@ -93,7 +92,6 @@ export async function deleteInterfaceCatData(id) {
// Action Creators
export async function fetchInterfaceData(interfaceId) {
let result = await axios.get('/api/interface/get?id=' + interfaceId);
return {
type: FETCH_INTERFACE_DATA,
payload: result.data

View File

@ -9,6 +9,7 @@ const REGISTER = 'yapi/user/REGISTER';
const SET_BREADCRUMB = 'yapi/user/SET_BREADCRUMB';
const CHANGE_STUDY_TIP = 'yapi/user/CHANGE_STUDY_TIP';
const FINISH_STUDY = 'yapi/user/FINISH_STUDY';
const SET_IMAGE_URL = 'yapi/user/SET_IMAGE_URL';
// Reducer
const LOADING_STATUS = 0;
@ -32,7 +33,8 @@ const initialState = {
// }]
breadcrumb: [],
studyTip: 0,
study: false
study: false,
imageUrl: ''
};
export default (state = initialState, action) => {
@ -41,8 +43,8 @@ export default (state = initialState, action) => {
return {
...state,
isLogin: (action.payload.data.errcode == 0),
role: action.payload.data.data ? action.payload.data.data.role:null,
loginState: (action.payload.data.errcode == 0)?MEMBER_STATUS:GUEST_STATUS,
role: action.payload.data.data ? action.payload.data.data.role : null,
loginState: (action.payload.data.errcode == 0) ? MEMBER_STATUS : GUEST_STATUS,
userName: action.payload.data.data ? action.payload.data.data.username : null,
uid: action.payload.data.data ? action.payload.data.data._id : null,
type: action.payload.data.data ? action.payload.data.data.type : null,
@ -66,7 +68,7 @@ export default (state = initialState, action) => {
}
}
case LOGIN_OUT: {
return{
return {
...state,
isLogin: false,
loginState: GUEST_STATUS,
@ -102,7 +104,7 @@ export default (state = initialState, action) => {
case CHANGE_STUDY_TIP: {
return {
...state,
studyTip: state.studyTip + 1
studyTip: state.studyTip + 1
}
}
case FINISH_STUDY: {
@ -112,6 +114,14 @@ export default (state = initialState, action) => {
studyTip: 0
};
}
case SET_IMAGE_URL: {
// console.log('state', state);
return {
...state,
imageUrl: action.data
}
}
default:
return state;
}
@ -119,7 +129,7 @@ export default (state = initialState, action) => {
// Action Creators
export function checkLoginState() {
return(dispatch)=> {
return (dispatch) => {
axios.get('/api/user/status').then((res) => {
dispatch({
type: GET_LOGIN_STATE,
@ -157,19 +167,28 @@ export function logoutActions() {
}
export function loginTypeAction(index) {
return{
return {
type: LOGIN_TYPE,
index
}
}
export function setBreadcrumb(data) {
return{
return {
type: SET_BREADCRUMB,
data
}
}
export function setImageUrl(data) {
return {
type: SET_IMAGE_URL,
data
}
}
export function changeStudyTip() {
return {
type: CHANGE_STUDY_TIP

View File

@ -31,7 +31,6 @@ class statisMockController extends baseController {
* @returns {Object}
*/
async getStatisCount(ctx) {
let groupCount = await this.groupModel.getGroupListCount();
let projectCount = await this.projectModel.getProjectListCount();
let interfaceCount = await this.interfaceModel.getInterfaceListCount();
@ -50,7 +49,6 @@ class statisMockController extends baseController {
*/
async getMockDateList(ctx) {
let mockCount = await this.Model.getTotalCount();
// let list = await this.Model.list();
let mockDateList = [];
if (!this.getRole() === 'admin') {

View File

@ -66,6 +66,7 @@ module.exports = function () {
try {
let result = await inst.save(data);
result = yapi.commons.fieldSelect(result, ['interface_id', 'project_id', 'group_id', 'time', 'ip', 'date']);
} catch (e) {
yapi.commons.log('mockStatisError', e);
}

View File

@ -47,6 +47,7 @@
.statis-chart{
margin:0 auto;
text-align: center;
}
.statis-footer{

View File

@ -87,6 +87,7 @@
"prop-types": "^15.5.10",
"rc-queue-anim": "^1.2.0",
"rc-scroll-anim": "^1.0.7",
"rc-tween-one": "^1.5.5",
"react": "^15.6.1",
"react-dnd": "^2.5.1",
"react-dnd-html5-backend": "^2.5.1",

View File

@ -361,9 +361,7 @@ class groupController extends baseController {
let projectInst = yapi.getInst(projectModel);
let userInst = yapi.getInst(userModel);
let result = await groupInst.list();
// let count = await groupInst.getListCount();
// let count = await projectInst.getProjectListCount();
// console.log('count', count);
let privateGroup = await groupInst.getByPrivateUid(this.getUid());
let newResult = [];

View File

@ -122,11 +122,13 @@ class interfaceController extends baseController {
};
if (!_.isUndefined(params.req_query)) {
data.req_query = params.req_query;
data.req_query = this.requiredSort(params.req_query);
// data.req_query = params.req_query;
}
if (!_.isUndefined(params.req_body_form)) {
data.req_body_form = params.req_body_form;
data.req_body_form = this.requiredSort(params.req_body_form);
// data.req_body_form = params.req_body_form;
}
if (params.path.indexOf(":") > 0) {
@ -167,7 +169,8 @@ class interfaceController extends baseController {
username: username,
typeid: params.project_id
});
this.projectModel.up(params.project_id,{up_time: new Date().getTime()}).then();
this.projectModel.up(params.project_id,{up_time: new Date().getTime()}).then();
//let project = await this.projectModel.getBaseInfo(params.project_id);
// let interfaceUrl = `http://${ctx.request.host}/project/${params.project_id}/interface/api/${result._id}`
// this.sendNotice(params.project_id, {
@ -444,7 +447,8 @@ class interfaceController extends baseController {
}
if (!_.isUndefined(params.req_body_form)) {
data.req_body_form = params.req_body_form;
data.req_body_form = this.requiredSort(params.req_body_form);
// data.req_body_form = params.req_body_form;
}
if (!_.isUndefined(params.req_params) && Array.isArray(params.req_params) && params.req_params.length > 0) {
if(Array.isArray(params.req_params) && params.req_params.length > 0){
@ -458,7 +462,9 @@ class interfaceController extends baseController {
}
if (!_.isUndefined(params.req_query)) {
data.req_query = params.req_query;
// data.req_query = params.req_query;
data.req_query = this.requiredSort(params.req_query);
// console.log("req",this.requiredSort(params.req_query));
}
if (!_.isUndefined(params.req_body_other)) {
@ -495,6 +501,7 @@ class interfaceController extends baseController {
typeid: cate.project_id
});
});
this.projectModel.up(interfaceData.project_id,{up_time: new Date().getTime()}).then();
} else {
let cateid = interfaceData.catid;
@ -507,6 +514,7 @@ class interfaceController extends baseController {
typeid: cate.project_id
});
});
this.projectModel.up(interfaceData.project_id,{up_time: new Date().getTime()}).then();
}
if (params.switch_notice === true) {
@ -574,7 +582,6 @@ class interfaceController extends baseController {
});
})
this.projectModel.up(data.project_id,{up_time: new Date().getTime()}).then();
ctx.body = yapi.commons.resReturn(result);
} catch (err) {
ctx.body = yapi.commons.resReturn(null, 402, err.message);
@ -783,6 +790,12 @@ class interfaceController extends baseController {
}
requiredSort(params) {
return params.sort((item1, item2) => {
return item2.required - item1.required;
})
}
}
module.exports = interfaceController;

View File

@ -14,7 +14,7 @@ const WEBCONFIG = config;
fs.ensureDirSync(WEBROOT_LOG);
if (WEBCONFIG.mail && WEBCONFIG.mail.enable ) {
if (WEBCONFIG.mail && WEBCONFIG.mail.enable) {
mail = nodemailer.createTransport(WEBCONFIG.mail);
}

View File

@ -185,7 +185,7 @@
<p>这里比新建项目页面新增的功能如下:</p>
<h3 class="subject" id="修改项目图标">修改项目图标 <a class="hashlink" href="#修改项目图标">#</a></h3><p>点击项目图标,可以修改图标及背景色:</p>
<p><img src="./images/usage/project_setting_logo.png" /></p>
<h3 class="subject" id="配置环境">配置环境 <a class="hashlink" href="#配置环境">#</a></h3><p><code>环境配置</code> 一项可以添加该项目下接口的实际环境,供 <a href="./usage-使用测试集.html">接口测试</a> 使用。</p>
<h3 class="subject" id="配置环境">配置环境 <a class="hashlink" href="#配置环境">#</a></h3><p><code>环境配置</code> 一项可以添加该项目下接口的实际环境,供 <a href="case.html">接口测试</a> 使用。</p>
<p><img src="./images/usage/project_setting_env.png" /></p>
<h2 class="subject" id="删除项目">删除项目 <a class="hashlink" href="#删除项目">#</a></h2><p>点击下方的删除按钮,输入项目名称进行删除。</p>
<blockquote>